From a64fea2ca3aa9c389f6ef4ff7bd3880e577f6eb8 Mon Sep 17 00:00:00 2001 From: eggmasonvalue Date: Sat, 4 Jul 2026 23:28:53 +0530 Subject: [PATCH 1/4] docs: enforce decision-log bar and clean up DECISIONS.md --- AGENTS.md | 86 +++++++++++++++++++++--------------------- context/CONVENTIONS.md | 1 + context/DECISIONS.md | 2 + 3 files changed, 45 insertions(+), 44 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 552a494..d5e1cc0 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,66 +1,64 @@ # AGENTS.md -Universal entry point for agents working in this repo. Read this first. +Agent-maintained docs are for durable context only. Code is the source of truth; +docs route agents and preserve non-obvious project rationale. -## Guiding principle +## Guardrails -Only document what an agent **cannot quickly recover by reading the code**. Code -is the source of truth for *what the code does*. Docs exist for *where things -live* (`context/MAP.md`) and *why the tradeoffs were made* (`context/DECISIONS.md`). -Everything else rots — do not write it. +- Never commit directly to `main`; work on a branch and open a PR. +- Never commit secrets. `EDGAR_IDENTITY` and `DISCORD_WEBHOOK_URL` are supplied via environment / CI secrets, never hard-coded. +- Never commit cache output. `sec-cache/`, `signal-sweep-cache/`, and `transcript-cache/` are regenerated on demand and are git-ignored. +- Lint and format must pass before a PR (see `context/CONVENTIONS.md`). CI runs `ruff check`, `ruff format --check`, and `markdownlint-cli2`. +- Keep changes scoped; avoid incidental refactors. +- Verify behavior with commands before documenting claims. -## Hard guardrails +## Read routing -- **Never commit to `main` directly.** Always work on a branch and open a PR. -- **Never commit secrets.** `EDGAR_IDENTITY` and `DISCORD_WEBHOOK_URL` are - supplied via environment / CI secrets, never hard-coded. -- **Never commit cache output.** `sec-cache/`, `signal-sweep-cache/`, and - `transcript-cache/` are regenerated on demand and are git-ignored. -- **Lint and format must pass** before a PR (see `context/CONVENTIONS.md`). CI - runs `ruff check`, `ruff format --check`, and `markdownlint-cli2`. +- Read `context/MAP.md` before changing module layout, ownership, or data flow. +- Read `context/DECISIONS.md` before changing a recorded tradeoff. +- Read `context/CONVENTIONS.md` while writing or editing code. +- Run `todo list` at task start; `todo claim ` before editing orchestrated + todos. -## Read routing +## Write triggers -Do not read everything by default. Read on demand: +- `context/MAP.md`: files/modules added, removed, moved, or data flow changed. +- `context/DECISIONS.md`: only choices that pass the decision-log bar below. +- `context/CONVENTIONS.md`: new repeatable coding/testing rule. +- `README.md`: user-facing setup or usage changed. -- Touching module structure or data flow → read `context/MAP.md` first. -- Changing or re-litigating a tradeoff → read `context/DECISIONS.md` first. -- Writing code → read `context/CONVENTIONS.md`. -- Starting any task → run `todo list` for open items, then `todo claim` before - execution so parallel sessions do not collide. +## Decision-log bar -## Write triggers (event-based) +`context/DECISIONS.md` is a curated ADR file, not a worklog. Append only when a +choice changes architecture, public behavior, data shape, dependency ownership, +or an expensive migration path **and** future agents need non-obvious rationale +to avoid re-litigating it. -- Module added / moved / removed, or data flow changed → update `context/MAP.md`. -- Intentional tradeoff made → **append to `context/DECISIONS.md`** (mandatory; - this is the most-forgotten artifact). -- New repeatable pattern or standard adopted → add to `context/CONVENTIONS.md`. -- User-facing behavior or usage changed → update `README.md`. +Do not append decisions for bug fixes, cleanup, dead-code removal, renames, +mechanical refactors, one-feature implementation tactics, or routine test/lint +chores. Before appending, prefer amending or superseding an existing decision. +When in doubt, do not append; keep task-local rationale in the todo, PR, commit +message, or final response. -## Do NOT document +## What not to document -- Changelog / worklog — that is git history. -- Feature or status lists — code already shows what exists. -- Restatements of what the code plainly does. -- Decisions with no real tradeoff. +- Changelogs/worklogs; git already has history. +- Feature/status checklists duplicated from code/tests. +- Restatements of obvious code behavior. +- Decisions that fail the decision-log bar. ## CONVENTIONS vs DECISIONS -A convention is **one imperative line with no "because"**. The moment it needs a -"because", it is a decision — move the rationale to `context/DECISIONS.md` and let -the convention link to it. +- `CONVENTIONS.md` contains terse imperative rules only. +- Rationale belongs in `DECISIONS.md` only if it passes the decision-log bar. ## Todos ↔ Decisions -The `todo` tool is stateful, not a scratchpad: todos are persisted under -`.pi/todos` with status, tags, body notes, subtasks, and `claim`/`release` -assignment. Keep active working context in the todo body while a task is live. - -Closed/done todos are garbage-collected (default ~7 days after creation), so -when closing a todo that involved a real tradeoff, **graduate the durable part -into `context/DECISIONS.md`** first. Closing is not archiving. +Use todos as stateful task records, not scratch notes. Keep live working context +in the todo body. Before closing a todo, graduate durable rationale to +`context/DECISIONS.md` only if it passes the decision-log bar. ## Definition of Done -A task is done only when the matching durable artifacts reflect the change. An -unrecorded tradeoff means **not done**. +Code, tests/lint, and durable docs must agree. If a change passes the +decision-log bar, its rationale must be recorded before the task is done. diff --git a/context/CONVENTIONS.md b/context/CONVENTIONS.md index 78b9a29..a29cdf3 100644 --- a/context/CONVENTIONS.md +++ b/context/CONVENTIONS.md @@ -32,6 +32,7 @@ Terse imperative code rules. No rationale here — rationale lives in - Keep each `SKILL.md` terse; defer detail to `references/` (progressive disclosure). - Ensure `AGENTS.md`, `README.md`, and everything in `context/` pass markdownlint. +- Append to `context/DECISIONS.md` only when the choice crosses the decision-log bar in `AGENTS.md`. ## Git diff --git a/context/DECISIONS.md b/context/DECISIONS.md index c57ff83..cb3e92f 100644 --- a/context/DECISIONS.md +++ b/context/DECISIONS.md @@ -1,5 +1,7 @@ # DECISIONS.md +This is a curated ADR file for durable, non-obvious project-level choices. It is not a changelog or implementation worklog. + Append-only log of intentional tradeoffs. Newest entries on top. Read before changing or re-litigating a choice. From 3ad98f072d5890513b7bcc0ddd56e802c9cb8a75 Mon Sep 17 00:00:00 2001 From: eggmasonvalue Date: Tue, 11 Aug 2026 01:15:22 +0530 Subject: [PATCH 2/4] feat: package SecStack as isolated Pi profile --- .github/workflows/insider-scan.yml | 6 +- README.md | 221 ++++++----- context/MAP.md | 76 ++-- package.json | 18 + scripts/bootstrap.mjs | 319 ++++++++++++++++ skills/README.md | 118 ++++++ .../bottom-up-analyst}/.gitignore | 0 .../bottom-up-analyst}/README.md | 0 .../bottom-up-analyst}/SKILL.md | 0 .../references/archetypes/compounder.md | 0 .../references/archetypes/cyclical.md | 0 .../references/archetypes/deep_value.md | 0 .../references/archetypes/hypergrowth.md | 0 .../archetypes/special_situation.md | 0 .../references/archetypes/turnaround.md | 0 .../references/guide_competitive.md | 0 .../references/guide_normalization.md | 0 .../references/guide_ownership_signals.md | 0 .../references/guide_valuation.md | 0 .../references/memo_template.md | 0 .../bottom-up-analyst}/scripts/dcf.py | 0 .../bottom-up-analyst}/scripts/epv.py | 0 .../market-scout}/.gitignore | 0 .../market-scout}/README.md | 79 ++-- .../market-scout}/SKILL.md | 8 +- .../market-scout}/requirements.txt | 0 .../market-scout}/scripts/_common.py | 0 .../scripts/fetch_market_data.py | 0 .../scripts/fetch_transcripts.py | 2 +- .../pitch-like-lou}/README.md | 0 .../pitch-like-lou}/SKILL.md | 0 .../Value Investors Club _ MCI (MCPEQ).md | 0 ...ue Investors Club _ NII Holdings (NIHD).md | 0 .../Value Investors Club _ NVR, Inc. (NVR).md | 0 ... _ Quilmes Industrial (Quinsa), S (LQU).md | 0 ...vestors Club _ Sportsman's Guide (SGDE).md | 0 ...b _ TELEMIG CELULAR PARTICIPACOES (TMB).md | 0 ...estors Club _ Winmill & Company (WNMLA).md | 0 .../sec-edgar-skill}/.gitignore | 0 .../sec-edgar-skill}/README.md | 130 +++---- .../sec-edgar-skill}/SKILL.md | 342 +++++++++--------- .../sec-edgar-skill}/references/guide_core.md | 210 +++++------ .../references/guide_filings.md | 230 ++++++------ .../references/guide_financials.md | 130 +++---- .../references/guide_holdings.md | 16 +- .../references/guide_ownership.md | 128 +++---- .../sec-edgar-skill}/requirements.txt | 0 .../sec-edgar-skill}/scripts/_common.py | 0 .../scripts/fetch_13f_holders.py | 0 .../sec-edgar-skill}/scripts/fetch_filing.py | 0 .../sec-edgar-skill}/scripts/fetch_filings.py | 0 .../scripts/fetch_insider_trades.py | 0 .../sec-edgar-skill}/scripts/list_headings.py | 0 .../sec-edgar-skill}/scripts/orient.py | 0 .../scripts/parse_financials.py | 0 .../sec-edgar-skill}/scripts/test_setup.py | 0 .../signal-sweep}/.gitignore | 0 .../signal-sweep}/README.md | 6 +- .../signal-sweep}/SKILL.md | 0 .../docs/conferences-autoresearch.md | 24 +- .../docs/flip_buy_difficulty_analysis.md | 60 +-- .../signal-sweep}/references/guide_screens.md | 0 .../signal-sweep}/requirements.txt | 0 .../signal-sweep}/screens.json | 0 .../signal-sweep}/scripts/_common.py | 0 .../signal-sweep}/scripts/scan_conferences.py | 0 .../signal-sweep}/scripts/scan_insiders.py | 0 .../signal-sweep}/scripts/scan_market.py | 0 .../signal-sweep}/scripts/search_themes.py | 0 69 files changed, 1309 insertions(+), 814 deletions(-) create mode 100644 package.json create mode 100644 scripts/bootstrap.mjs create mode 100644 skills/README.md rename {bottom-up-analyst => skills/bottom-up-analyst}/.gitignore (100%) rename {bottom-up-analyst => skills/bottom-up-analyst}/README.md (100%) rename {bottom-up-analyst => skills/bottom-up-analyst}/SKILL.md (100%) rename {bottom-up-analyst => skills/bottom-up-analyst}/references/archetypes/compounder.md (100%) rename {bottom-up-analyst => skills/bottom-up-analyst}/references/archetypes/cyclical.md (100%) rename {bottom-up-analyst => skills/bottom-up-analyst}/references/archetypes/deep_value.md (100%) rename {bottom-up-analyst => skills/bottom-up-analyst}/references/archetypes/hypergrowth.md (100%) rename {bottom-up-analyst => skills/bottom-up-analyst}/references/archetypes/special_situation.md (100%) rename {bottom-up-analyst => skills/bottom-up-analyst}/references/archetypes/turnaround.md (100%) rename {bottom-up-analyst => skills/bottom-up-analyst}/references/guide_competitive.md (100%) rename {bottom-up-analyst => skills/bottom-up-analyst}/references/guide_normalization.md (100%) rename {bottom-up-analyst => skills/bottom-up-analyst}/references/guide_ownership_signals.md (100%) rename {bottom-up-analyst => skills/bottom-up-analyst}/references/guide_valuation.md (100%) rename {bottom-up-analyst => skills/bottom-up-analyst}/references/memo_template.md (100%) rename {bottom-up-analyst => skills/bottom-up-analyst}/scripts/dcf.py (100%) rename {bottom-up-analyst => skills/bottom-up-analyst}/scripts/epv.py (100%) rename {market-scout => skills/market-scout}/.gitignore (100%) rename {market-scout => skills/market-scout}/README.md (80%) rename {market-scout => skills/market-scout}/SKILL.md (93%) rename {market-scout => skills/market-scout}/requirements.txt (100%) rename {market-scout => skills/market-scout}/scripts/_common.py (100%) rename {market-scout => skills/market-scout}/scripts/fetch_market_data.py (100%) rename {market-scout => skills/market-scout}/scripts/fetch_transcripts.py (99%) rename {pitch-like-lou => skills/pitch-like-lou}/README.md (100%) rename {pitch-like-lou => skills/pitch-like-lou}/SKILL.md (100%) rename {pitch-like-lou => skills/pitch-like-lou}/references/corpus/Value Investors Club _ MCI (MCPEQ).md (100%) rename {pitch-like-lou => skills/pitch-like-lou}/references/corpus/Value Investors Club _ NII Holdings (NIHD).md (100%) rename {pitch-like-lou => skills/pitch-like-lou}/references/corpus/Value Investors Club _ NVR, Inc. (NVR).md (100%) rename {pitch-like-lou => skills/pitch-like-lou}/references/corpus/Value Investors Club _ Quilmes Industrial (Quinsa), S (LQU).md (100%) rename {pitch-like-lou => skills/pitch-like-lou}/references/corpus/Value Investors Club _ Sportsman's Guide (SGDE).md (100%) rename {pitch-like-lou => skills/pitch-like-lou}/references/corpus/Value Investors Club _ TELEMIG CELULAR PARTICIPACOES (TMB).md (100%) rename {pitch-like-lou => skills/pitch-like-lou}/references/corpus/Value Investors Club _ Winmill & Company (WNMLA).md (100%) rename {sec-edgar-skill => skills/sec-edgar-skill}/.gitignore (100%) rename {sec-edgar-skill => skills/sec-edgar-skill}/README.md (93%) rename {sec-edgar-skill => skills/sec-edgar-skill}/SKILL.md (97%) rename {sec-edgar-skill => skills/sec-edgar-skill}/references/guide_core.md (81%) rename {sec-edgar-skill => skills/sec-edgar-skill}/references/guide_filings.md (89%) rename {sec-edgar-skill => skills/sec-edgar-skill}/references/guide_financials.md (81%) rename {sec-edgar-skill => skills/sec-edgar-skill}/references/guide_holdings.md (77%) rename {sec-edgar-skill => skills/sec-edgar-skill}/references/guide_ownership.md (88%) rename {sec-edgar-skill => skills/sec-edgar-skill}/requirements.txt (100%) rename {sec-edgar-skill => skills/sec-edgar-skill}/scripts/_common.py (100%) rename {sec-edgar-skill => skills/sec-edgar-skill}/scripts/fetch_13f_holders.py (100%) rename {sec-edgar-skill => skills/sec-edgar-skill}/scripts/fetch_filing.py (100%) rename {sec-edgar-skill => skills/sec-edgar-skill}/scripts/fetch_filings.py (100%) rename {sec-edgar-skill => skills/sec-edgar-skill}/scripts/fetch_insider_trades.py (100%) rename {sec-edgar-skill => skills/sec-edgar-skill}/scripts/list_headings.py (100%) rename {sec-edgar-skill => skills/sec-edgar-skill}/scripts/orient.py (100%) rename {sec-edgar-skill => skills/sec-edgar-skill}/scripts/parse_financials.py (100%) rename {sec-edgar-skill => skills/sec-edgar-skill}/scripts/test_setup.py (100%) rename {signal-sweep => skills/signal-sweep}/.gitignore (100%) rename {signal-sweep => skills/signal-sweep}/README.md (91%) rename {signal-sweep => skills/signal-sweep}/SKILL.md (100%) rename {signal-sweep => skills/signal-sweep}/docs/conferences-autoresearch.md (96%) rename {signal-sweep => skills/signal-sweep}/docs/flip_buy_difficulty_analysis.md (91%) rename {signal-sweep => skills/signal-sweep}/references/guide_screens.md (100%) rename {signal-sweep => skills/signal-sweep}/requirements.txt (100%) rename {signal-sweep => skills/signal-sweep}/screens.json (100%) rename {signal-sweep => skills/signal-sweep}/scripts/_common.py (100%) rename {signal-sweep => skills/signal-sweep}/scripts/scan_conferences.py (100%) rename {signal-sweep => skills/signal-sweep}/scripts/scan_insiders.py (100%) rename {signal-sweep => skills/signal-sweep}/scripts/scan_market.py (100%) rename {signal-sweep => skills/signal-sweep}/scripts/search_themes.py (100%) diff --git a/.github/workflows/insider-scan.yml b/.github/workflows/insider-scan.yml index ad73426..e417900 100644 --- a/.github/workflows/insider-scan.yml +++ b/.github/workflows/insider-scan.yml @@ -28,13 +28,13 @@ jobs: python-version: "3.12" - name: Install dependencies - run: pip install -r signal-sweep/requirements.txt + run: pip install -r skills/signal-sweep/requirements.txt - name: Run insider scan env: EDGAR_IDENTITY: ${{ secrets.EDGAR_IDENTITY }} DISCORD_WEBHOOK_URL: ${{ secrets.DISCORD_WEBHOOK_URL }} - working-directory: signal-sweep + working-directory: skills/signal-sweep run: | DATE="${{ github.event.inputs.date || 'yesterday' }}" LOOKBACK="${{ github.event.inputs.lookback || '5' }}" @@ -56,5 +56,5 @@ jobs: uses: actions/upload-artifact@v4 with: name: insider-scan-${{ github.run_id }} - path: signal-sweep/signal-sweep-cache/insiders/ + path: skills/signal-sweep/signal-sweep-cache/insiders/ retention-days: 90 diff --git a/README.md b/README.md index 66695ca..2ae4ed1 100644 --- a/README.md +++ b/README.md @@ -1,149 +1,138 @@ -# US Market Research Skills +# SecStack -A composable stack of [agent skills](https://agentskills.io/home) for rigorous bottom-up equity -research on US-listed companies — from idea discovery through primary data (SEC filings + -market data), through an analytical framework, to a finished pitch. Each skill stands on its -own; together they form a pipeline. +SecStack is an isolated [Pi](https://github.com/badlogic/pi-mono) profile for rigorous +bottom-up research on US-listed companies. It bundles five composable skills, selected +Pi extensions and themes, `pi-subagent`, and Pi-managed `agent-browser`. -## The skills +The normal Pi profile is not modified. SecStack uses: -| Layer | Skill | Job | -|---|---|---| -| **Discovery** | [`signal-sweep`](signal-sweep/) | Scan SEC filings and market data across the $50M–$10B universe to surface new investment ideas: insider cluster/rip/dip buys, activist 13D filings, market screens, keyword/theme search, conference discovery. | -| **Data** | [`sec-edgar-skill`](sec-edgar-skill/) | Retrieve & extract SEC EDGAR filings (10-K/10-Q/8-K, 20-F/6-K, XBRL financials, ownership, holdings) and 13F institutional holder data (via 13f.info), token-efficiently. Unopinionated. | -| **Data** | [`market-scout`](market-scout/) | Pull price, returns, peers, sector screens, and earnings call transcripts via Yahoo Finance. Unopinionated. | -| **Analysis** | [`bottom-up-analyst`](bottom-up-analyst/) | Turn one ticker into an earned, auditable investment memo — drives the data skills, classifies the archetype, values it, tries to kill it. | -| **Voice** | [`pitch-like-lou`](pitch-like-lou/) | Render a Norbert Lou–style Value Investors Club pitch from a finished thesis. | +```text +~/.pi/secstack-agent +``` -## Salient Features +## Install -### Composability +Prerequisites: -```text - signal-sweep (surfaces tickers) - │ - ▼ - bottom-up-analyst (deep dive on one ticker) - ├── sec-edgar-skill (SEC filings) - ├── market-scout (price, peers, transcripts) - ▼ - pitch-like-lou (finished pitch) +- Git +- Node.js +- Pi +- Bash (Git Bash on Windows) +- Python 3.11 or newer + +From Bash, bootstrap the profile with one command: + +```bash +tmp=$(mktemp -d) && git clone --depth 1 https://github.com/eggmasonvalue/secstack "$tmp" && node "$tmp/scripts/bootstrap.mjs"; status=$?; rm -rf "$tmp"; exit $status ``` -- **Discovery feeds analysis.** `signal-sweep` scans the universe and produces shortlists of - tickers with reasons. `bottom-up-analyst` takes one of those tickers and does the deep dive. - They are independent — you can skip discovery and hand the analyst a ticker directly. -- **The two data skills are independent and swappable.** `sec-edgar-skill` (filings) and - `market-scout` (market data) know nothing of each other; either can be replaced — e.g. point - the analyst at a paid data provider instead of `market-scout` and nothing else changes. -- **The analyst is the brain and the conductor.** `bottom-up-analyst` decides what to pull, - reasons over it, values the business, and writes the memo. It drives the data skills; they - never decide what matters. -- **The voice renders from a finished thesis.** `pitch-like-lou` turns the analyst's memo into a - pitch; it is not an idea generator. +The bootstrap is safe to rerun. It installs the unpinned top-level Pi package sources, +merges only SecStack-managed package entries and shell-path configuration into the +SecStack profile's `settings.json`, creates a profile-local Python environment, removes +old resource-directory links, and links the installed `pi-setup` `APPEND_SYSTEM.md` into +the SecStack profile. -**Production order:** signal-sweep → analyst → memo → (optionally) Lou pitches from it. +It does not overwrite the profile's `auth.json`, `models.json`, provider settings, +model selections, UI preferences, sessions, or unrelated settings. -### Progressive disclosure at the center of design - lets your model's intelligence shine through +## Launch -#### Current snapshot +The bootstrap offers to add a `secstack-pi` Bash function to `~/.bashrc`. After opening a +new Bash shell (or running `source ~/.bashrc`), use: + +```bash +secstack-pi +``` -From the repository root, reproduce these reports with: +The launcher activates the SecStack virtual environment, exposes the Pi-managed npm +binaries, and starts Pi with the isolated profile. It accepts normal Pi arguments: ```bash -# Agent-loaded entry points -cloc --by-file --include-lang=Markdown bottom-up-analyst/SKILL.md pitch-like-lou/SKILL.md sec-edgar-skill/SKILL.md signal-sweep/SKILL.md market-scout/SKILL.md - -# Referenced skill surface only -cloc \ - bottom-up-analyst/SKILL.md \ - bottom-up-analyst/references/memo_template.md \ - bottom-up-analyst/references/guide_normalization.md \ - bottom-up-analyst/references/guide_competitive.md \ - bottom-up-analyst/references/guide_valuation.md \ - bottom-up-analyst/references/guide_ownership_signals.md \ - bottom-up-analyst/references/archetypes/*.md \ - bottom-up-analyst/scripts/dcf.py bottom-up-analyst/scripts/epv.py \ - market-scout/SKILL.md market-scout/requirements.txt market-scout/scripts/fetch_market_data.py market-scout/scripts/fetch_transcripts.py \ - pitch-like-lou/SKILL.md pitch-like-lou/references/corpus/*.md \ - sec-edgar-skill/SKILL.md \ - sec-edgar-skill/references/guide_core.md sec-edgar-skill/references/guide_filings.md sec-edgar-skill/references/guide_financials.md sec-edgar-skill/references/guide_ownership.md sec-edgar-skill/references/guide_holdings.md \ - sec-edgar-skill/scripts/orient.py sec-edgar-skill/scripts/fetch_filing.py sec-edgar-skill/scripts/fetch_filings.py sec-edgar-skill/scripts/parse_financials.py sec-edgar-skill/scripts/list_headings.py sec-edgar-skill/scripts/fetch_insider_trades.py sec-edgar-skill/scripts/fetch_13f_holders.py sec-edgar-skill/scripts/test_setup.py \ - signal-sweep/SKILL.md signal-sweep/screens.json signal-sweep/references/guide_screens.md \ - signal-sweep/scripts/scan_insiders.py signal-sweep/scripts/scan_market.py signal-sweep/scripts/search_themes.py signal-sweep/scripts/scan_conferences.py +secstack-pi --mode json -p "Summarize the current research workflow." ``` -The second command lists the referenced skill paths explicitly, so repository-level docs and -unreferenced proposals are not counted. +Without the launcher, start the profile directly: -```text ------------------------------------------------------------------------------------------- -File blank comment code ------------------------------------------------------------------------------------------- -./bottom-up-analyst/SKILL.md 51 0 228 -./pitch-like-lou/SKILL.md 39 0 164 -./sec-edgar-skill/SKILL.md 36 0 135 -./signal-sweep/SKILL.md 27 0 81 -./market-scout/SKILL.md 19 0 60 ------------------------------------------------------------------------------------------- -SUM: 172 0 668 ------------------------------------------------------------------------------------------- - -------------------------------------------------------------------------------- -Language files blank comment code -------------------------------------------------------------------------------- -Markdown 29 1608 0 3025 -Python 16 711 534 2890 -JSON 1 0 0 99 -Text 1 1 0 5 -------------------------------------------------------------------------------- -SUM: 47 2320 534 6019 -------------------------------------------------------------------------------- +```bash +PI_CODING_AGENT_DIR="$HOME/.pi/secstack-agent" pi ``` -## Install +## Update -Install the whole stack, or any single skill on its own - point your agent at -this repository, or at a single skill subfolder. Each skill is a self-contained -folder with its own `SKILL.md`. +Update every Pi-managed package in the SecStack profile with: -## Setup +```bash +PI_CODING_AGENT_DIR="$HOME/.pi/secstack-agent" pi update --extensions +``` + +After updating, use `/reload` inside Pi to load the new resources without restarting the +machine. -### SEC identity (required) +Python dependencies are installed or refreshed when the bootstrap is rerun. They are not +part of Pi's `pi update --extensions` lifecycle. -The SEC's [fair-access policy](https://www.sec.gov/os/webmaster-faq#developers) requires a -contact name and email in the User-Agent header. Requests without one are blocked (HTTP 403). -Set it once — `sec-edgar-skill` and `signal-sweep` both read it automatically: +## One-time runtime setup + +The bootstrap installs `agent-browser` as a Pi-managed npm package. Its browser/runtime +installation is a separate one-time step. Start the SecStack profile, then run: ```bash -export EDGAR_IDENTITY="Jane Analyst jane@example.com" # bash/zsh -$env:EDGAR_IDENTITY = "Jane Analyst jane@example.com" # PowerShell +agent-browser install +agent-browser --version ``` -Use your real name and email. The SEC uses this only to contact you if your traffic causes -problems — it is not authentication. +SEC-facing skills require an identity string for the SEC fair-access policy. Set it in the +shell before using those skills; do not commit it: -### Per-skill dependencies +```bash +export EDGAR_IDENTITY="Jane Analyst jane@example.com" +``` + +The insider scan can optionally post to Discord. Set the webhook in the environment when +using that feature: + +```bash +export DISCORD_WEBHOOK_URL="https://discord.com/api/webhooks/..." +``` + +## Included packages + +The SecStack profile manages these as separate top-level Pi packages: + +- `git:github.com/eggmasonvalue/secstack` — this repository's five skills +- `git:github.com/eggmasonvalue/pi-setup` — selected extensions and themes only +- `git:github.com/eggmasonvalue/pi-subagent` +- `npm:agent-browser` + +The selected `pi-setup` resources are `btw`, `notify`, `session-context`, `tavily-web`, +`vibe-spinner`, and the `midnight-pastel`, `pastel-dark`, and `pastel-light` themes. -- **`signal-sweep`** — `pip install -r signal-sweep/requirements.txt` -- **`sec-edgar-skill`** — `pip install -r sec-edgar-skill/requirements.txt` -- **`market-scout`** — install both before use: `pip install -r market-scout/requirements.txt` - and `npm install -g agent-browser && agent-browser install`. No identity needed. -- **`bottom-up-analyst`** — valuation scripts are standard-library only; no install needed. -- **`pitch-like-lou`** — documentation and a reference corpus; nothing to install. +Each independently managed source remains a top-level profile package so +`pi update --extensions` can update it independently. Third-party resources are not copied +into this repository or bundled as nested dependencies. -## Contributing +## Skills -This repo is agent-maintained. Start at [`AGENTS.md`](AGENTS.md), then see -[`context/MAP.md`](context/MAP.md) (where things live), -[`context/DECISIONS.md`](context/DECISIONS.md) (why), and -[`context/CONVENTIONS.md`](context/CONVENTIONS.md) (code rules). Lint with -`uv run ruff check .` and `npx markdownlint-cli2 "**/*.md"`. Work on a branch and -open a PR — never commit to `main` directly. +The five skills are documented in [`skills/README.md`](skills/README.md). The production +flow is: + +```text +signal-sweep → bottom-up-analyst → sec-edgar-skill / market-scout → pitch-like-lou +``` + +## Development + +The repository layout and data flow are documented in [`context/MAP.md`](context/MAP.md). +Project conventions are in [`context/CONVENTIONS.md`](context/CONVENTIONS.md). Run the +following checks before opening a pull request: + +```bash +uv run ruff check . +uv run ruff format --check . +npx markdownlint-cli2 "**/*.md" +``` -## A note on scope +## Scope -These skills produce **research, not advice**. They are tools for doing diligence rigorously and -honestly; nothing they output is a recommendation to buy or sell a security. The whole design — -filings-first grounding, verified-vs-assumed tagging, the pre-mortem — exists to keep an LLM's -fluent prose tethered to auditable evidence so a human can reach their own judgment. +These skills produce research, not investment advice. They are tools for doing diligence +rigorously and honestly; nothing they output is a recommendation to buy or sell a security. diff --git a/context/MAP.md b/context/MAP.md index 3c2c5c1..8085612 100644 --- a/context/MAP.md +++ b/context/MAP.md @@ -2,36 +2,53 @@ Where things live and how data flows. Read before touching structure or data flow. -## Repo shape +## Repository shape -A monorepo of five self-contained agent skills. Each skill is a folder with its -own `SKILL.md` (agent entry point), optional `references/` (progressive-disclosure -guides), `scripts/` (Python helpers), and `requirements.txt`. +A Git Pi package containing five self-contained agent skills. The skills live under +`skills/` so the root of the repository can focus on package/profile setup. ```text -signal-sweep/ Discovery — scan the universe, surface tickers -sec-edgar-skill/ Data — SEC EDGAR filings, ownership, 13F holders -market-scout/ Data — price, peers, transcripts (Yahoo Finance) -bottom-up-analyst/ Analysis — one ticker → auditable memo (the conductor) -pitch-like-lou/ Voice — finished thesis → VIC-style pitch +package.json Pi package manifest for the five SecStack skills +scripts/bootstrap.mjs Isolated-profile bootstrap +skills/ Skill collection and skill-level documentation + signal-sweep/ Discovery — scan the universe, surface tickers + sec-edgar-skill/ Data — SEC EDGAR filings, ownership, 13F holders + market-scout/ Data — price, peers, transcripts (Yahoo Finance) + bottom-up-analyst/ Analysis — one ticker → auditable memo (the conductor) + pitch-like-lou/ Voice — finished thesis → VIC-style pitch +context/ Agent-maintained project documentation ``` -Root config: `pyproject.toml` (ruff + package metadata), `uv.lock`, -`.markdownlint-cli2.jsonc` / `.markdownlint.json`, `.github/workflows/`. +The package manifest exposes the five individual directories under `skills/`. It does not +expose `skills/README.md` as a skill. + +## Pi profile data flow + +The bootstrap configures the isolated profile at `~/.pi/secstack-agent`. Its +`settings.json` owns separate top-level package entries for: + +- this SecStack package; +- the filtered `pi-setup` package; +- `pi-subagent`; and +- `agent-browser`. + +Pi installs and updates each source independently. The bootstrap manages only those package +entries, the profile's Pi-managed shell path, the profile-local Python environment, and the +`APPEND_SYSTEM.md` link. Authentication, model selection, provider configuration, sessions, +and unrelated settings remain profile-local and untouched. ## Skill internals -- `signal-sweep/` — `scripts/scan_insiders.py`, `scan_market.py`, - `scan_conferences.py`, `search_themes.py`; universe config in `screens.json`; - shared bootstrap in `scripts/_common.py`. -- `sec-edgar-skill/` — `scripts/fetch_*.py`, `parse_financials.py`, `orient.py`, +- `skills/signal-sweep/` — `scripts/scan_insiders.py`, `scan_market.py`, + `scan_conferences.py`, `search_themes.py`; universe config in `screens.json`; shared + bootstrap in `scripts/_common.py`. +- `skills/sec-edgar-skill/` — `scripts/fetch_*.py`, `parse_financials.py`, `orient.py`, `list_headings.py`; guides in `references/`; shared bootstrap in `scripts/_common.py`. -- `market-scout/` — `scripts/fetch_market_data.py`, `fetch_transcripts.py`, - shared `scripts/_common.py`. -- `bottom-up-analyst/` — valuation `scripts/dcf.py`, `epv.py`; archetypes and - guides in `references/`, including `guide_ownership_signals.md` for Phase 6 - interpretation. -- `pitch-like-lou/` — reference corpus only, no scripts. +- `skills/market-scout/` — `scripts/fetch_market_data.py`, `fetch_transcripts.py`, and shared + `scripts/_common.py`. +- `skills/bottom-up-analyst/` — valuation `scripts/dcf.py`, `epv.py`; archetypes and guides in + `references/`. +- `skills/pitch-like-lou/` — reference corpus only, no scripts. ## Data flow @@ -43,13 +60,16 @@ flowchart TD BUA --> PLL[pitch-like-lou
renders pitch] ``` -- `bottom-up-analyst` is the brain and conductor: it decides what to pull, - reasons over it, and writes the memo. The data skills never decide what matters. -- The two data skills (`sec-edgar-skill`, `market-scout`) know nothing of each - other and are swappable. -- `signal-sweep` and `sec-edgar-skill` both read `EDGAR_IDENTITY` and share an - on-disk cache contract defined in `_common.py`. +- `bottom-up-analyst` is the brain and conductor: it decides what to pull, reasons over it, + and writes the memo. The data skills never decide what matters. +- The two filing/market data skills know nothing of each other and are swappable. +- `signal-sweep` and `sec-edgar-skill` both read `EDGAR_IDENTITY` and share on-disk cache + contracts defined in their `_common.py` modules. ## Production order -`signal-sweep` → `bottom-up-analyst` → memo → (optionally) `pitch-like-lou`. +`signal-sweep` → `bottom-up-analyst` → memo → optionally `pitch-like-lou`. + +For the package installation and isolated-profile workflow, start at the root +[`README.md`](../README.md). For the collection overview, start at +[`skills/README.md`](../skills/README.md). diff --git a/package.json b/package.json new file mode 100644 index 0000000..c63e1e1 --- /dev/null +++ b/package.json @@ -0,0 +1,18 @@ +{ + "name": "eggmasonvalue-secstack", + "version": "0.1.0", + "private": true, + "description": "US market research skills for Pi", + "keywords": [ + "pi-package" + ], + "pi": { + "skills": [ + "./skills/signal-sweep", + "./skills/sec-edgar-skill", + "./skills/market-scout", + "./skills/bottom-up-analyst", + "./skills/pitch-like-lou" + ] + } +} diff --git a/scripts/bootstrap.mjs b/scripts/bootstrap.mjs new file mode 100644 index 0000000..4b2931a --- /dev/null +++ b/scripts/bootstrap.mjs @@ -0,0 +1,319 @@ +#!/usr/bin/env node +/** + * Install SecStack and its independently managed Pi packages into the isolated + * SecStack profile. + * + * This script deliberately changes only the SecStack profile's package list, + * shell command prefix, APPEND_SYSTEM.md link, and optional Bash launcher. + */ +import { execFileSync } from "node:child_process"; +import { + existsSync, + lstatSync, + mkdirSync, + readFileSync, + renameSync, + rmSync, + symlinkSync, + writeFileSync, +} from "node:fs"; +import { createInterface } from "node:readline/promises"; +import { stdin as input, stdout as output } from "node:process"; +import { homedir } from "node:os"; +import { dirname, join, resolve } from "node:path"; + +const agentDir = resolve(join(homedir(), ".pi", "secstack-agent")); +const settingsPath = join(agentDir, "settings.json"); +const npmBin = join(agentDir, "npm", "node_modules", ".bin"); +const venvDir = join(agentDir, ".venv"); +const appendPath = join(agentDir, "APPEND_SYSTEM.md"); +const bashrcPath = join(homedir(), ".bashrc"); +const secstackSource = "git:github.com/eggmasonvalue/secstack"; +const piSetupSource = "git:github.com/eggmasonvalue/pi-setup"; +const subagentSource = "git:github.com/eggmasonvalue/pi-subagent"; +const agentBrowserSource = "npm:agent-browser"; + +const managedSources = [ + secstackSource, + piSetupSource, + subagentSource, + agentBrowserSource, +]; + +const desiredPackages = [ + secstackSource, + { + source: piSetupSource, + extensions: [ + "extensions/btw.ts", + "extensions/notify.ts", + "extensions/session-context.ts", + "extensions/tavily-web.ts", + "extensions/vibe-spinner.ts", + ], + skills: [], + prompts: [], + themes: [ + "themes/midnight-pastel.json", + "themes/pastel-dark.json", + "themes/pastel-light.json", + ], + }, + subagentSource, + agentBrowserSource, +]; + +const managedPathMarker = ".pi/secstack-agent"; +const launcherStart = "# >>> secstack-pi launcher >>>"; +const launcherEnd = "# <<< secstack-pi launcher <<<"; + +function sourceOf(entry) { + return typeof entry === "string" ? entry : entry?.source; +} + +function run(command, args, env = {}) { + console.log(`\n> ${command} ${args.join(" ")}`); + execFileSync(command, args, { + stdio: "inherit", + shell: process.platform === "win32", + env: { + ...process.env, + ...env, + }, + }); +} + +function runPi(args, env = {}) { + run("pi", args, { + PI_CODING_AGENT_DIR: agentDir, + ...env, + }); +} + +function commandWorks(command, args) { + try { + execFileSync(command, args, { + stdio: "ignore", + shell: process.platform === "win32", + }); + return true; + } catch { + return false; + } +} + +function findPython() { + for (const command of ["python", "python3"]) { + if (commandWorks(command, ["--version"])) return command; + } + throw new Error( + "Python was not found. Install Python 3.11 or newer, ensure python is on PATH, and rerun bootstrap.", + ); +} + +function venvPython() { + return process.platform === "win32" + ? join(venvDir, "Scripts", "python.exe") + : join(venvDir, "bin", "python"); +} + +function installedSecStackPath(...parts) { + return join(agentDir, "git", "github.com", "eggmasonvalue", "secstack", ...parts); +} + +function mergeSettings() { + mkdirSync(agentDir, { recursive: true }); + let settings = {}; + if (existsSync(settingsPath)) { + settings = JSON.parse(readFileSync(settingsPath, "utf8")); + } + + const existing = Array.isArray(settings.packages) ? settings.packages : []; + const managed = new Set(managedSources); + settings.packages = [ + ...existing.filter((entry) => !managed.has(sourceOf(entry))), + ...desiredPackages, + ]; + + // Pi evaluates shellCommandPrefix inside Bash. Add both virtualenv layouts; + // the non-existent layout is harmless and this works in Git Bash on Windows + // as well as Bash on macOS/Linux. + const pathCommand = + 'export PATH="$HOME/.pi/secstack-agent/.venv/Scripts:$HOME/.pi/secstack-agent/.venv/bin:$HOME/.pi/secstack-agent/npm/node_modules/.bin:$PATH"'; + const prefix = typeof settings.shellCommandPrefix === "string" ? settings.shellCommandPrefix : ""; + if (!prefix.includes(managedPathMarker)) { + settings.shellCommandPrefix = prefix ? `${prefix}\n${pathCommand}` : pathCommand; + } + + const temp = join(agentDir, `.settings.${process.pid}.tmp`); + writeFileSync(temp, `${JSON.stringify(settings, null, 2)}\n`, "utf8"); + renameSync(temp, settingsPath); +} + +function ensurePythonEnvironment() { + const python = findPython(); + if (!existsSync(venvPython())) { + console.log(`\nCreating SecStack Python environment at ${venvDir}`); + run(python, ["-m", "venv", venvDir]); + } + + const requirements = [ + installedSecStackPath("skills", "signal-sweep", "requirements.txt"), + installedSecStackPath("skills", "sec-edgar-skill", "requirements.txt"), + installedSecStackPath("skills", "market-scout", "requirements.txt"), + ]; + for (const requirement of requirements) { + if (!existsSync(requirement)) { + throw new Error(`Installed SecStack package is missing ${requirement}`); + } + run(venvPython(), ["-m", "pip", "install", "-r", requirement]); + } +} + +function removeOldResourceLink(name) { + const path = join(agentDir, name); + if (!existsSync(path)) return; + try { + if (lstatSync(path).isSymbolicLink()) { + rmSync(path, { recursive: true, force: true }); + console.log(`Removed old resource link: ${path}`); + } else { + console.warn(`Not removing non-link resource directory: ${path}`); + } + } catch (error) { + console.warn(`Could not inspect ${path}: ${error.message}`); + } +} + +function linkAppendSystem() { + const installed = join( + agentDir, + "git", + "github.com", + "eggmasonvalue", + "pi-setup", + "APPEND_SYSTEM.md", + ); + if (!existsSync(installed)) { + throw new Error(`Installed pi-setup package is missing APPEND_SYSTEM.md: ${installed}`); + } + + let existing; + try { + existing = lstatSync(appendPath); + } catch (error) { + if (error.code !== "ENOENT") throw error; + } + + if (existing?.isSymbolicLink()) { + rmSync(appendPath, { force: true }); + } else if (existing) { + const backup = `${appendPath}.local-backup`; + if (!existsSync(backup)) { + renameSync(appendPath, backup); + console.warn(`Preserved the previous regular file as ${backup}`); + } else { + throw new Error( + `A regular ${appendPath} already exists and ${backup} is also present; refusing to overwrite either file.`, + ); + } + } + + try { + symlinkSync(installed, appendPath, "file"); + console.log(`Linked ${appendPath} -> ${installed}`); + } catch (error) { + throw new Error( + `Could not create the APPEND_SYSTEM.md symlink. Enable Windows Developer Mode or grant symlink privileges, then rerun bootstrap. Original error: ${error.message}`, + ); + } +} + +function launcherBlock() { + return `${launcherStart} +secstack-pi() { + local agent_dir="$HOME/.pi/secstack-agent" + local venv="$agent_dir/.venv" + + if [ -f "$venv/Scripts/activate" ]; then + . "$venv/Scripts/activate" + elif [ -f "$venv/bin/activate" ]; then + . "$venv/bin/activate" + fi + + export PATH="$venv/Scripts:$venv/bin:$agent_dir/npm/node_modules/.bin:$PATH" + PI_CODING_AGENT_DIR="$agent_dir" pi "$@" +} +${launcherEnd}`; +} + +function installLauncher() { + mkdirSync(dirname(bashrcPath), { recursive: true }); + const existing = existsSync(bashrcPath) ? readFileSync(bashrcPath, "utf8") : ""; + const block = launcherBlock(); + const pattern = new RegExp( + `${escapeRegExp(launcherStart)}[\\s\\S]*?${escapeRegExp(launcherEnd)}\\n?`, + ); + const content = pattern.test(existing) + ? existing.replace(pattern, `${block}\n`) + : `${existing.trimEnd()}${existing ? "\n\n" : ""}${block}\n`; + const temp = `${bashrcPath}.${process.pid}.tmp`; + writeFileSync(temp, content, "utf8"); + renameSync(temp, bashrcPath); + console.log(`Added the secstack-pi launcher to ${bashrcPath}`); + console.log("Open a new Bash shell, or run: source ~/.bashrc"); +} + +function escapeRegExp(value) { + return value.replace(/[.*+?^${}()|[\[\]\\]/g, "\\$&"); +} + +async function offerLauncher() { + if (!process.stdin.isTTY || !process.stdout.isTTY) { + console.log(`Launcher not offered because bootstrap is not running in an interactive Bash terminal.`); + return; + } + + const rl = createInterface({ input, output }); + try { + const answer = (await rl.question("Create the secstack-pi Bash launcher? [Y/n] ")) + .trim() + .toLowerCase(); + if (answer === "" || answer === "y" || answer === "yes") { + installLauncher(); + } else { + console.log("Skipped the secstack-pi launcher."); + } + } finally { + rl.close(); + } +} + +async function main() { + console.log(`Configuring SecStack Pi under ${agentDir}`); + for (const source of managedSources) { + runPi(["install", source]); + } + + mergeSettings(); + mkdirSync(npmBin, { recursive: true }); + ensurePythonEnvironment(); + for (const name of ["extensions", "skills", "prompts", "themes"]) { + removeOldResourceLink(name); + } + linkAppendSystem(); + await offerLauncher(); + + console.log("\nSecStack Pi bootstrap complete."); + console.log("Update everything Pi-managed with:"); + console.log(' PI_CODING_AGENT_DIR="$HOME/.pi/secstack-agent" pi update --extensions'); + console.log("One-time browser setup (if not already done): agent-browser install"); + console.log("Verify from the SecStack profile: agent-browser --version"); +} + +try { + await main(); +} catch (error) { + console.error(`\nBootstrap failed: ${error.message}`); + process.exitCode = 1; +} diff --git a/skills/README.md b/skills/README.md new file mode 100644 index 0000000..59bdd4f --- /dev/null +++ b/skills/README.md @@ -0,0 +1,118 @@ +# SecStack skills + +SecStack is a composable stack of five [agent skills](https://agentskills.io/home) for +bottom-up equity research on US-listed companies. Each skill can stand on its own; together +they form a research pipeline. + +## The stack + +| Layer | Skill | Job | +|---|---|---| +| **Discovery** | [`signal-sweep`](signal-sweep/) | Scan SEC filings and market data to surface new investment ideas. | +| **Data** | [`sec-edgar-skill`](sec-edgar-skill/) | Retrieve and extract SEC filings, ownership, and 13F holder data. | +| **Data** | [`market-scout`](market-scout/) | Pull prices, returns, peers, sector screens, and transcripts. | +| **Analysis** | [`bottom-up-analyst`](bottom-up-analyst/) | Turn one ticker into an auditable investment memo. | +| **Voice** | [`pitch-like-lou`](pitch-like-lou/) | Render a finished thesis as a VIC-style pitch. | + +## Data flow + +```text + signal-sweep (surfaces tickers) + │ + ▼ + bottom-up-analyst (deep dive on one ticker) + ├── sec-edgar-skill (filings and ownership) + ├── market-scout (price, peers, transcripts) + ▼ + pitch-like-lou (finished pitch) +``` + +`bottom-up-analyst` is the conductor. It decides what to pull, reasons over the evidence, +values the business, and writes the memo. The data skills never decide what matters. + +`signal-sweep` and `sec-edgar-skill` are independent data sources. `market-scout` is also +swappable: the analyst can use a different market-data provider without changing its +reasoning workflow. + +The voice skill renders from a finished thesis; it is not an idea generator. + +**Production order:** signal-sweep → bottom-up-analyst → memo → optionally pitch-like-lou. + +## Progressive disclosure + +Each skill keeps its `SKILL.md` entry point concise. Detailed guidance lives in +`references/`, executable helpers live in `scripts/`, and configuration/assets stay beside +the skill that owns them. Load only the guide or script required for the current task. + +## Skill resources + +- [`signal-sweep`](signal-sweep/) contains configurable market screens, SEC discovery + scanners, conference discovery, and theme search. +- [`sec-edgar-skill`](sec-edgar-skill/) contains filing, financial, ownership, and 13F + retrieval helpers. +- [`market-scout`](market-scout/) contains Yahoo Finance market and transcript helpers. +- [`bottom-up-analyst`](bottom-up-analyst/) contains valuation arithmetic and memo frameworks. +- [`pitch-like-lou`](pitch-like-lou/) contains the pitch-writing workflow and reference corpus. + +Each skill's README documents its own dependencies and usage. Runtime caches are generated +next to the relevant workspace and are git-ignored. + +## Current snapshot + +The following commands measure the entry-point surface and the explicitly referenced skill +resources from the repository root: + +```bash +cloc --by-file --include-lang=Markdown \ + skills/bottom-up-analyst/SKILL.md \ + skills/pitch-like-lou/SKILL.md \ + skills/sec-edgar-skill/SKILL.md \ + skills/signal-sweep/SKILL.md \ + skills/market-scout/SKILL.md +``` + +```bash +cloc \ + skills/bottom-up-analyst/SKILL.md \ + skills/bottom-up-analyst/references/memo_template.md \ + skills/bottom-up-analyst/references/guide_normalization.md \ + skills/bottom-up-analyst/references/guide_competitive.md \ + skills/bottom-up-analyst/references/guide_valuation.md \ + skills/bottom-up-analyst/references/guide_ownership_signals.md \ + skills/bottom-up-analyst/references/archetypes/*.md \ + skills/bottom-up-analyst/scripts/dcf.py \ + skills/bottom-up-analyst/scripts/epv.py \ + skills/market-scout/SKILL.md \ + skills/market-scout/requirements.txt \ + skills/market-scout/scripts/fetch_market_data.py \ + skills/market-scout/scripts/fetch_transcripts.py \ + skills/pitch-like-lou/SKILL.md \ + skills/pitch-like-lou/references/corpus/*.md \ + skills/sec-edgar-skill/SKILL.md \ + skills/sec-edgar-skill/references/guide_core.md \ + skills/sec-edgar-skill/references/guide_filings.md \ + skills/sec-edgar-skill/references/guide_financials.md \ + skills/sec-edgar-skill/references/guide_ownership.md \ + skills/sec-edgar-skill/references/guide_holdings.md \ + skills/sec-edgar-skill/scripts/orient.py \ + skills/sec-edgar-skill/scripts/fetch_filing.py \ + skills/sec-edgar-skill/scripts/fetch_filings.py \ + skills/sec-edgar-skill/scripts/parse_financials.py \ + skills/sec-edgar-skill/scripts/list_headings.py \ + skills/sec-edgar-skill/scripts/fetch_insider_trades.py \ + skills/sec-edgar-skill/scripts/fetch_13f_holders.py \ + skills/sec-edgar-skill/scripts/test_setup.py \ + skills/signal-sweep/SKILL.md \ + skills/signal-sweep/screens.json \ + skills/signal-sweep/references/guide_screens.md \ + skills/signal-sweep/scripts/scan_insiders.py \ + skills/signal-sweep/scripts/scan_market.py \ + skills/signal-sweep/scripts/search_themes.py \ + skills/signal-sweep/scripts/scan_conferences.py +``` + +## Research scope + +These skills produce research, not advice. Filings-first grounding, verified-versus-assumed +tagging, and pre-mortem analysis keep an LLM's fluent prose tethered to auditable evidence so +a human can reach their own judgment. diff --git a/bottom-up-analyst/.gitignore b/skills/bottom-up-analyst/.gitignore similarity index 100% rename from bottom-up-analyst/.gitignore rename to skills/bottom-up-analyst/.gitignore diff --git a/bottom-up-analyst/README.md b/skills/bottom-up-analyst/README.md similarity index 100% rename from bottom-up-analyst/README.md rename to skills/bottom-up-analyst/README.md diff --git a/bottom-up-analyst/SKILL.md b/skills/bottom-up-analyst/SKILL.md similarity index 100% rename from bottom-up-analyst/SKILL.md rename to skills/bottom-up-analyst/SKILL.md diff --git a/bottom-up-analyst/references/archetypes/compounder.md b/skills/bottom-up-analyst/references/archetypes/compounder.md similarity index 100% rename from bottom-up-analyst/references/archetypes/compounder.md rename to skills/bottom-up-analyst/references/archetypes/compounder.md diff --git a/bottom-up-analyst/references/archetypes/cyclical.md b/skills/bottom-up-analyst/references/archetypes/cyclical.md similarity index 100% rename from bottom-up-analyst/references/archetypes/cyclical.md rename to skills/bottom-up-analyst/references/archetypes/cyclical.md diff --git a/bottom-up-analyst/references/archetypes/deep_value.md b/skills/bottom-up-analyst/references/archetypes/deep_value.md similarity index 100% rename from bottom-up-analyst/references/archetypes/deep_value.md rename to skills/bottom-up-analyst/references/archetypes/deep_value.md diff --git a/bottom-up-analyst/references/archetypes/hypergrowth.md b/skills/bottom-up-analyst/references/archetypes/hypergrowth.md similarity index 100% rename from bottom-up-analyst/references/archetypes/hypergrowth.md rename to skills/bottom-up-analyst/references/archetypes/hypergrowth.md diff --git a/bottom-up-analyst/references/archetypes/special_situation.md b/skills/bottom-up-analyst/references/archetypes/special_situation.md similarity index 100% rename from bottom-up-analyst/references/archetypes/special_situation.md rename to skills/bottom-up-analyst/references/archetypes/special_situation.md diff --git a/bottom-up-analyst/references/archetypes/turnaround.md b/skills/bottom-up-analyst/references/archetypes/turnaround.md similarity index 100% rename from bottom-up-analyst/references/archetypes/turnaround.md rename to skills/bottom-up-analyst/references/archetypes/turnaround.md diff --git a/bottom-up-analyst/references/guide_competitive.md b/skills/bottom-up-analyst/references/guide_competitive.md similarity index 100% rename from bottom-up-analyst/references/guide_competitive.md rename to skills/bottom-up-analyst/references/guide_competitive.md diff --git a/bottom-up-analyst/references/guide_normalization.md b/skills/bottom-up-analyst/references/guide_normalization.md similarity index 100% rename from bottom-up-analyst/references/guide_normalization.md rename to skills/bottom-up-analyst/references/guide_normalization.md diff --git a/bottom-up-analyst/references/guide_ownership_signals.md b/skills/bottom-up-analyst/references/guide_ownership_signals.md similarity index 100% rename from bottom-up-analyst/references/guide_ownership_signals.md rename to skills/bottom-up-analyst/references/guide_ownership_signals.md diff --git a/bottom-up-analyst/references/guide_valuation.md b/skills/bottom-up-analyst/references/guide_valuation.md similarity index 100% rename from bottom-up-analyst/references/guide_valuation.md rename to skills/bottom-up-analyst/references/guide_valuation.md diff --git a/bottom-up-analyst/references/memo_template.md b/skills/bottom-up-analyst/references/memo_template.md similarity index 100% rename from bottom-up-analyst/references/memo_template.md rename to skills/bottom-up-analyst/references/memo_template.md diff --git a/bottom-up-analyst/scripts/dcf.py b/skills/bottom-up-analyst/scripts/dcf.py similarity index 100% rename from bottom-up-analyst/scripts/dcf.py rename to skills/bottom-up-analyst/scripts/dcf.py diff --git a/bottom-up-analyst/scripts/epv.py b/skills/bottom-up-analyst/scripts/epv.py similarity index 100% rename from bottom-up-analyst/scripts/epv.py rename to skills/bottom-up-analyst/scripts/epv.py diff --git a/market-scout/.gitignore b/skills/market-scout/.gitignore similarity index 100% rename from market-scout/.gitignore rename to skills/market-scout/.gitignore diff --git a/market-scout/README.md b/skills/market-scout/README.md similarity index 80% rename from market-scout/README.md rename to skills/market-scout/README.md index 71a5b40..051028e 100644 --- a/market-scout/README.md +++ b/skills/market-scout/README.md @@ -1,38 +1,41 @@ -# Market Scout - -Pull public market data — price, market cap, trailing returns, peers, and sector screens — for -US-listed stocks via [`yfinance`](https://github.com/ranaroussi/yfinance). An unopinionated data -layer: it surfaces facts and rankings; it decides nothing. - -Part of the [us-market-research-skills](../README.md) stack. - -## Installation (do this first) - -Install Python deps and browser runtime before using the scripts: - -```bash -pip install -r requirements.txt -npm install -g agent-browser && agent-browser install -``` - -- `yfinance`/`pandas` power market/peer data. -- `agent-browser` is required for earnings-call transcript pages (JS-rendered Yahoo/Quartr). - -## Use - -```bash -python scripts/fetch_market_data.py --ticker AAPL --peers -``` - -## Earnings call transcripts (Yahoo + Quartr) - -```bash -python scripts/fetch_transcripts.py --ticker AAPL --list -python scripts/fetch_transcripts.py --ticker AAPL --latest 1 -``` - -yfinance offers far more than the script wraps and is self-documenting (`dir()`, `help()`, -`t.info.keys()`) — see [SKILL.md](SKILL.md) for how to discover and construct what you need. - -Market data is best-effort and occasionally stale or missing for thinly-covered names; confirm -anything load-bearing against a primary source. +# Market Scout + +Pull public market data — price, market cap, trailing returns, peers, and sector screens — for +US-listed stocks via [`yfinance`](https://github.com/ranaroussi/yfinance). An unopinionated data +layer: it surfaces facts and rankings; it decides nothing. + +Part of the [SecStack skills](../README.md) collection. + +## Installation (do this first) + +Install Python deps and browser runtime before using the scripts: + +```bash +pip install -r requirements.txt +# The SecStack bootstrap installs agent-browser into the isolated Pi profile. +# Run this once from an active SecStack profile: +agent-browser install +``` + +- `yfinance`/`pandas` power market/peer data. +- `agent-browser` is required for earnings-call transcript pages (JS-rendered Yahoo/Quartr). +- In the packaged SecStack profile, its Pi-managed binary is already on PATH. + +## Use + +```bash +python scripts/fetch_market_data.py --ticker AAPL --peers +``` + +## Earnings call transcripts (Yahoo + Quartr) + +```bash +python scripts/fetch_transcripts.py --ticker AAPL --list +python scripts/fetch_transcripts.py --ticker AAPL --latest 1 +``` + +yfinance offers far more than the script wraps and is self-documenting (`dir()`, `help()`, +`t.info.keys()`) — see [SKILL.md](SKILL.md) for how to discover and construct what you need. + +Market data is best-effort and occasionally stale or missing for thinly-covered names; confirm +anything load-bearing against a primary source. diff --git a/market-scout/SKILL.md b/skills/market-scout/SKILL.md similarity index 93% rename from market-scout/SKILL.md rename to skills/market-scout/SKILL.md index 35daee3..a3ee2eb 100644 --- a/market-scout/SKILL.md +++ b/skills/market-scout/SKILL.md @@ -24,7 +24,8 @@ Install both dependencies before running any script: ```bash pip install -r requirements.txt # yfinance + pandas (Python ≥ 3.10) -npm install -g agent-browser && agent-browser install +# In the packaged SecStack profile, agent-browser is installed by the bootstrap. +agent-browser install ``` - `agent-browser` is required for earnings-call transcripts (`fetch_transcripts.py`). @@ -65,10 +66,11 @@ the scripts don't cover, **discover at runtime** rather than guessing field name ```python import yfinance as yf + t = yf.Ticker("AAPL") -print([a for a in dir(t) if not a.startswith("_")]) # all attributes/methods -list(t.info.keys()) # every field in the snapshot +print([a for a in dir(t) if not a.startswith("_")]) # all attributes/methods +list(t.info.keys()) # every field in the snapshot # Sector/industry screening (theme -> shortlist): ind = yf.Industry(t.info["industryKey"]) diff --git a/market-scout/requirements.txt b/skills/market-scout/requirements.txt similarity index 100% rename from market-scout/requirements.txt rename to skills/market-scout/requirements.txt diff --git a/market-scout/scripts/_common.py b/skills/market-scout/scripts/_common.py similarity index 100% rename from market-scout/scripts/_common.py rename to skills/market-scout/scripts/_common.py diff --git a/market-scout/scripts/fetch_market_data.py b/skills/market-scout/scripts/fetch_market_data.py similarity index 100% rename from market-scout/scripts/fetch_market_data.py rename to skills/market-scout/scripts/fetch_market_data.py diff --git a/market-scout/scripts/fetch_transcripts.py b/skills/market-scout/scripts/fetch_transcripts.py similarity index 99% rename from market-scout/scripts/fetch_transcripts.py rename to skills/market-scout/scripts/fetch_transcripts.py index 887875d..0e1b116 100644 --- a/market-scout/scripts/fetch_transcripts.py +++ b/skills/market-scout/scripts/fetch_transcripts.py @@ -99,7 +99,7 @@ def _run_agent_browser(args: list[str], timeout: int = 45) -> str: except FileNotFoundError: c.log( "ERROR: agent-browser is not installed or not on PATH.\n" - "Install it: npm install -g agent-browser && agent-browser install" + "Run `agent-browser install` from the active SecStack Pi profile after bootstrap." ) sys.exit(2) except subprocess.TimeoutExpired: diff --git a/pitch-like-lou/README.md b/skills/pitch-like-lou/README.md similarity index 100% rename from pitch-like-lou/README.md rename to skills/pitch-like-lou/README.md diff --git a/pitch-like-lou/SKILL.md b/skills/pitch-like-lou/SKILL.md similarity index 100% rename from pitch-like-lou/SKILL.md rename to skills/pitch-like-lou/SKILL.md diff --git a/pitch-like-lou/references/corpus/Value Investors Club _ MCI (MCPEQ).md b/skills/pitch-like-lou/references/corpus/Value Investors Club _ MCI (MCPEQ).md similarity index 100% rename from pitch-like-lou/references/corpus/Value Investors Club _ MCI (MCPEQ).md rename to skills/pitch-like-lou/references/corpus/Value Investors Club _ MCI (MCPEQ).md diff --git a/pitch-like-lou/references/corpus/Value Investors Club _ NII Holdings (NIHD).md b/skills/pitch-like-lou/references/corpus/Value Investors Club _ NII Holdings (NIHD).md similarity index 100% rename from pitch-like-lou/references/corpus/Value Investors Club _ NII Holdings (NIHD).md rename to skills/pitch-like-lou/references/corpus/Value Investors Club _ NII Holdings (NIHD).md diff --git a/pitch-like-lou/references/corpus/Value Investors Club _ NVR, Inc. (NVR).md b/skills/pitch-like-lou/references/corpus/Value Investors Club _ NVR, Inc. (NVR).md similarity index 100% rename from pitch-like-lou/references/corpus/Value Investors Club _ NVR, Inc. (NVR).md rename to skills/pitch-like-lou/references/corpus/Value Investors Club _ NVR, Inc. (NVR).md diff --git a/pitch-like-lou/references/corpus/Value Investors Club _ Quilmes Industrial (Quinsa), S (LQU).md b/skills/pitch-like-lou/references/corpus/Value Investors Club _ Quilmes Industrial (Quinsa), S (LQU).md similarity index 100% rename from pitch-like-lou/references/corpus/Value Investors Club _ Quilmes Industrial (Quinsa), S (LQU).md rename to skills/pitch-like-lou/references/corpus/Value Investors Club _ Quilmes Industrial (Quinsa), S (LQU).md diff --git a/pitch-like-lou/references/corpus/Value Investors Club _ Sportsman's Guide (SGDE).md b/skills/pitch-like-lou/references/corpus/Value Investors Club _ Sportsman's Guide (SGDE).md similarity index 100% rename from pitch-like-lou/references/corpus/Value Investors Club _ Sportsman's Guide (SGDE).md rename to skills/pitch-like-lou/references/corpus/Value Investors Club _ Sportsman's Guide (SGDE).md diff --git a/pitch-like-lou/references/corpus/Value Investors Club _ TELEMIG CELULAR PARTICIPACOES (TMB).md b/skills/pitch-like-lou/references/corpus/Value Investors Club _ TELEMIG CELULAR PARTICIPACOES (TMB).md similarity index 100% rename from pitch-like-lou/references/corpus/Value Investors Club _ TELEMIG CELULAR PARTICIPACOES (TMB).md rename to skills/pitch-like-lou/references/corpus/Value Investors Club _ TELEMIG CELULAR PARTICIPACOES (TMB).md diff --git a/pitch-like-lou/references/corpus/Value Investors Club _ Winmill & Company (WNMLA).md b/skills/pitch-like-lou/references/corpus/Value Investors Club _ Winmill & Company (WNMLA).md similarity index 100% rename from pitch-like-lou/references/corpus/Value Investors Club _ Winmill & Company (WNMLA).md rename to skills/pitch-like-lou/references/corpus/Value Investors Club _ Winmill & Company (WNMLA).md diff --git a/sec-edgar-skill/.gitignore b/skills/sec-edgar-skill/.gitignore similarity index 100% rename from sec-edgar-skill/.gitignore rename to skills/sec-edgar-skill/.gitignore diff --git a/sec-edgar-skill/README.md b/skills/sec-edgar-skill/README.md similarity index 93% rename from sec-edgar-skill/README.md rename to skills/sec-edgar-skill/README.md index 4ec7049..5f49dd5 100644 --- a/sec-edgar-skill/README.md +++ b/skills/sec-edgar-skill/README.md @@ -1,65 +1,65 @@ -# SEC EDGAR Research Skill - -A tools skill that teaches an AI coding agent how to retrieve and extract data from -**SEC EDGAR** filings for US-listed companies (domestic issuers and foreign private -issuers), efficiently and within a token budget. - -It is the **data/tools layer** of a research stack: it fetches and extracts; it does not -decide what matters. Pair it with an analytical-framework skill (which supplies the -judgment and the output shape) and, optionally, a presentation/consumer skill. Keeping -this layer unopinionated lets any framework compose on top of it. - -## What's included - -- **`SKILL.md`** — the entry point: setup, the token-efficient retrieval method, the - cache contract, and routing to the guides and scripts. -- **`references/`** — modular, lazily-loaded guides, one per data domain: - - `guide_core.md` — company lookup, filing discovery, `.to_context()`, `.docs`. - - `guide_filings.md` — filing text by SEC item code (10-K/10-Q/8-K/20-F) or heading discovery (DEF 14A/6-K); attachments (6-K Exhibit 99.1). - - `guide_financials.md` — XBRL statements and facts (US-GAAP & IFRS). - - `guide_ownership.md` — insider transactions (3/4/5) and executive compensation (DEF 14A; 20-F Item 6). - - `guide_holdings.md` — 13F institutional holdings and 13D/13G blockholders. -- **`scripts/`** — thin, self-documenting wrappers around `edgartools` (shared setup - lives in `_common.py`): - - `orient.py` — company summary + filing-mix survey + recent filings (run first). - - `fetch_filing.py`, `fetch_filings.py` — filings (and sections/attachments) to Markdown. - - `parse_financials.py` — XBRL statements to CSV. - - `list_headings.py` — heading→line map for a cached filing. - - `fetch_insider_trades.py` — insider transactions (Form 4 buys/sells). - - `fetch_13f_holders.py` — institutional 13F holders (via 13f.info). - - `test_setup.py` — environment diagnostics. - -## Setup - -1. **Install dependencies** (Python ≥ 3.10): `pip install -r requirements.txt` -2. **Set `EDGAR_IDENTITY`** — see [repo-level setup](../README.md#setup). -3. **Verify:** `python scripts/test_setup.py --live` - -## Add the skill to your agent - -```bash -npx skills add eggmasonvalue/sec-edgar-skill -``` - -## How it works - -Filings are huge, so the skill keeps them on disk and pulls only what's needed into the -agent's context: - -1. **Orient** with `scripts/orient.py` (company summary + filing-mix survey) to decide what to fetch. -2. **Download** filings to a local cache (`./sec-cache/{TICKER}/`) as clean Markdown. -3. **Map** a large filing to a heading→line table of contents. -4. **Search** the cache with native grep and read only the matching line ranges. - -The cache location is configurable (`$SEC_CACHE_DIR` or `--cache-dir`) and filenames are -deterministic (keyed by SEC accession number), so re-runs reuse cached files instead of -re-downloading. - -## Data sources - -Filing data comes from the public SEC EDGAR system via the open-source `edgartools` -library. Respect the source's terms and the SEC fair-access policy — which is why a -contact identity is required. - ---- -Part of the [us-market-research-skills](../README.md) stack. +# SEC EDGAR Research Skill + +A tools skill that teaches an AI coding agent how to retrieve and extract data from +**SEC EDGAR** filings for US-listed companies (domestic issuers and foreign private +issuers), efficiently and within a token budget. + +It is the **data/tools layer** of a research stack: it fetches and extracts; it does not +decide what matters. Pair it with an analytical-framework skill (which supplies the +judgment and the output shape) and, optionally, a presentation/consumer skill. Keeping +this layer unopinionated lets any framework compose on top of it. + +## What's included + +- **`SKILL.md`** — the entry point: setup, the token-efficient retrieval method, the + cache contract, and routing to the guides and scripts. +- **`references/`** — modular, lazily-loaded guides, one per data domain: + - `guide_core.md` — company lookup, filing discovery, `.to_context()`, `.docs`. + - `guide_filings.md` — filing text by SEC item code (10-K/10-Q/8-K/20-F) or heading discovery (DEF 14A/6-K); attachments (6-K Exhibit 99.1). + - `guide_financials.md` — XBRL statements and facts (US-GAAP & IFRS). + - `guide_ownership.md` — insider transactions (3/4/5) and executive compensation (DEF 14A; 20-F Item 6). + - `guide_holdings.md` — 13F institutional holdings and 13D/13G blockholders. +- **`scripts/`** — thin, self-documenting wrappers around `edgartools` (shared setup + lives in `_common.py`): + - `orient.py` — company summary + filing-mix survey + recent filings (run first). + - `fetch_filing.py`, `fetch_filings.py` — filings (and sections/attachments) to Markdown. + - `parse_financials.py` — XBRL statements to CSV. + - `list_headings.py` — heading→line map for a cached filing. + - `fetch_insider_trades.py` — insider transactions (Form 4 buys/sells). + - `fetch_13f_holders.py` — institutional 13F holders (via 13f.info). + - `test_setup.py` — environment diagnostics. + +## Setup + +1. **Install dependencies** (Python ≥ 3.10): `pip install -r requirements.txt` +2. **Set `EDGAR_IDENTITY`** — see [profile setup](../../README.md#one-time-runtime-setup). +3. **Verify:** `python scripts/test_setup.py --live` + +## Add the skill to your agent + +```bash +npx skills add eggmasonvalue/sec-edgar-skill +``` + +## How it works + +Filings are huge, so the skill keeps them on disk and pulls only what's needed into the +agent's context: + +1. **Orient** with `scripts/orient.py` (company summary + filing-mix survey) to decide what to fetch. +2. **Download** filings to a local cache (`./sec-cache/{TICKER}/`) as clean Markdown. +3. **Map** a large filing to a heading→line table of contents. +4. **Search** the cache with native grep and read only the matching line ranges. + +The cache location is configurable (`$SEC_CACHE_DIR` or `--cache-dir`) and filenames are +deterministic (keyed by SEC accession number), so re-runs reuse cached files instead of +re-downloading. + +## Data sources + +Filing data comes from the public SEC EDGAR system via the open-source `edgartools` +library. Respect the source's terms and the SEC fair-access policy — which is why a +contact identity is required. + +--- +Part of the [SecStack skills](../README.md) collection. diff --git a/sec-edgar-skill/SKILL.md b/skills/sec-edgar-skill/SKILL.md similarity index 97% rename from sec-edgar-skill/SKILL.md rename to skills/sec-edgar-skill/SKILL.md index a258034..2657629 100644 --- a/sec-edgar-skill/SKILL.md +++ b/skills/sec-edgar-skill/SKILL.md @@ -1,171 +1,171 @@ ---- -name: sec-edgar-skill -description: >- - Retrieve and extract SEC EDGAR filings and ownership data for US-listed companies. Use this - whenever a task touches a company's filings, financials, ownership, governance, or - institutional holders — even if EDGAR is not named explicitly. Covers 10-K/10-Q/8-K, - 20-F/6-K foreign filings, XBRL financial statements, insider transactions, 13F institutional - holdings (via 13f.info), 13D/13G blockholdings, and DEF 14A proxy/compensation. Always start - by running scripts/orient.py, then pull only the sections you need. Unopinionated data layer: - it fetches and extracts token-efficiently; it does not decide what is significant. ---- - -# SEC EDGAR Research Skill - -A toolkit for retrieving and extracting SEC EDGAR filings for US-listed companies — -efficiently, within a token budget, using `edgartools`. - -## What this skill is — and is not - -This is the **tools layer** of a research stack. It knows *how* to find, download, and -extract SEC filing data, plus the library mechanics to do it reliably. It is -deliberately **unopinionated**: it does not judge what is a good number, a red flag, or -worth looking at. That judgment belongs to whatever **analytical-framework skill** is -driving (e.g. a value-investing framework); presentation belongs to a downstream -consumer skill. Keep this layer neutral so any framework can compose on top of it. - -Provide capability and facts; let the caller reason. The only hard requirements here are -mechanical (set an SEC identity, or requests are blocked) — never analytical. - -## Setup - -Run `python scripts/test_setup.py --live` to verify dependencies and SEC identity. -If identity is missing, the error message tells the user exactly how to set -`$EDGAR_IDENTITY` (see the [repo-level setup](../README.md#setup) for the full -explanation). Every script reads it automatically; you can also pass `--identity` -per-call. - -The scripts handle environment hazards on import (UTF-8 stdout on Windows, truststore -for corporate proxies). When writing **inline Python** that calls `edgartools` directly -(not via a bundled script), replicate that preamble — see `references/guide_core.md` -§ "Inline Python preamble." - -## How to work efficiently: pull only what you need - -Filings are huge — a 10-K can exceed 100k words. Loading one whole wastes the context -window and buries the signal. The method is to keep documents **on disk** and pull only -the exact lines you need into context. Four phases: - -1. **Orient first — always run `scripts/orient.py`.** `python scripts/orient.py --ticker ` - prints the company's `.to_context()` summary, surveys the **mix of forms it has actually - filed** (with date ranges), and lists the most recent filings — the cheapest way to see what - a company files *now* and how that has changed, so you fetch the right forms instead of - assuming a form set. This is the **non-negotiable first step — run it before any web - search, even for breaking news.** When the user says "just reported" or "a few hours ago," - orient.py will show the 8-K filed today immediately; then fetch it with - `fetch_filing.py --form 8-K --date --attachment list` to get the press-release - exhibit (Exhibit 99.1). The filing is always faster and more authoritative than a web - search for what the company itself disclosed. (For finer control, `.to_context()` - on a `Company`, filing collection, or `XBRL` object gives the same preview inline — see - `guide_core.md`.) -2. **Download to the cache as Markdown.** Use the scripts to write filings to disk as - clean Markdown — `edgartools` converts SEC HTML, stripping layout bloat to roughly a - tenth of the size, and the result is greppable. -3. **Get just the section you need.** Item-addressable forms (10-K/10-Q/8-K/20-F) let you - list a filing's SEC item codes and pull one section by code — no scanning. For a full - report, `scripts/list_headings.py` maps its `#` headers to line numbers; tabular - free-form filings (e.g. DEF 14A) instead carry their own table of contents up top — - read it, then grep. `guide_filings.md` has the mechanics. -4. **Search, then read precisely.** Use your native grep/ripgrep over the cached files - to find the lines that matter, then read just those ranges. Let grep and disk do the - heavy lifting; spend context only on the paragraphs you actually need. - -## The cache - -Downloads go to `//
__[__].md` -(statements as `…__.csv`). The root resolves as `--cache-dir` > -`$SEC_CACHE_DIR` > `./sec-cache` — workspace-relative so your grep tool finds it by -default, and persistent across runs so you don't re-hit the SEC. Filenames are -deterministic and keyed by the globally-unique accession number, so **before -downloading, check whether the file already exists** (list or glob `//`) -and reuse it. Every script prints the absolute path(s) it wrote to stdout. - -> If your grep tool uses ripgrep and the cache is gitignored, ripgrep skips it by -> default. Either point the search at `//` explicitly, or pass -> `--no-ignore`. Resolve the path from a script's stdout or `$SEC_CACHE_DIR` — never -> hard-code an absolute cache path. - -## Reference guides (read the relevant one before extracting) - -Each guide is loaded only when its domain is in play, so you carry just the rules you -need. Read the matching guide first — it holds the item codes, taxonomies, and library -quirks that make extraction correct. - -| Guide | Use it for | -| :-- | :-- | -| `references/guide_core.md` | The mechanics behind `scripts/orient.py`: resolving a company, listing/filtering filings, surveying the filing mix, `.to_context()` previews, and `.docs` self-help. Read it to drive orientation inline or go beyond the script. | -| `references/guide_filings.md` | Filing text: pulling a section by SEC item code (10-K/10-Q/8-K/20-F) vs. navigating free-form filings (DEF 14A/6-K) by their own contents, plus attachments and exhibits (incl. 6-K Exhibit 99.1). | -| `references/guide_financials.md` | XBRL financial statements and individual facts (US-GAAP and IFRS), and the period-aggregation pitfalls. | -| `references/guide_ownership.md` | Insider transactions (Forms 3/4/5), beneficial ownership, and executive compensation (DEF 14A; Form 20-F Item 6 for foreign issuers). For the common case — “what are insiders buying/selling?” — use `scripts/fetch_insider_trades.py` directly; no guide needed. | -| `references/guide_holdings.md` | **Deep route only:** raw 13F via edgartools (voting authority, amendments, specific holdings) and 5%+ blockholders (13D/13G). For the common case — “who owns this stock?” — use `scripts/fetch_13f_holders.py` directly (see Scripts above); no guide needed. | - -## Scripts - -Run them with the project's Python. Each prints the absolute cache path(s) it wrote to -stdout and logs progress to stderr. **`--help` is the authoritative flag reference** — -the list below shows one canonical invocation each: - -```bash -# Orient first: company summary + filing-mix survey + recent filings -python scripts/orient.py --ticker AAPL - -# One filing (full), a single section, or an attachment — into the cache -python scripts/fetch_filing.py --ticker AAPL --form 10-K --year 2023 -python scripts/fetch_filing.py --ticker AAPL --form 10-K --year 2023 --section "Item 1A" # or: --section list -python scripts/fetch_filing.py --ticker WIX --form 6-K --attachment "ex-99.1" # or: list | all | - -# Target a filing by date (e.g. an 8-K filed today) instead of just --year: -python scripts/fetch_filing.py --ticker AAPL --form 8-K --date 2026-06-15 - -# Many filings across a year range (add --attachments to capture e.g. 6-K exhibits) -python scripts/fetch_filings.py --ticker AAPL --form 10-Q --start-year 2022 --end-year 2024 - -# XBRL statements (income | balance | cashflow | all) -> CSV (annual or quarterly) -python scripts/parse_financials.py --ticker AAPL --year 2023 --statement all -python scripts/parse_financials.py --ticker AAPL --year 2024 --quarter 1 --statement all - -# Table of contents for a large cached filing -python scripts/list_headings.py --file sec-cache/AAPL/10-K_2023-11-03_0000320193-23-000106.md - -# Insider transactions — what are insiders buying/selling? (Form 4) -python scripts/fetch_insider_trades.py --ticker AAPL -python scripts/fetch_insider_trades.py --ticker AAPL --start 2025-01-01 --end 2026-06-17 -python scripts/fetch_insider_trades.py --ticker AAPL --start 2025-06-01 --buys-only - -# 13F institutional holders — who owns this stock? (via 13f.info, no SEC identity needed) -python scripts/fetch_13f_holders.py --ticker AAPL --top 15 - -# 13F holder history — how has institutional ownership changed? -python scripts/fetch_13f_holders.py --ticker AAPL --history - -# 13F manager search — what does a specific fund hold? -python scripts/fetch_13f_holders.py --manager "Berkshire Hathaway" - -# 13F cross-reference — one manager's position history in one stock -python scripts/fetch_13f_holders.py --cik 0000906304 --cusip 205826209 - -# Environment diagnostics -python scripts/test_setup.py --live -``` - -## When the API surprises you: self-heal with `.docs` - -`edgartools` documents itself at runtime. If a method or attribute isn't what you -expected, query it inline instead of guessing — this recovers from most API uncertainty -without leaving the session: - -```python -company.docs # full API guide for the object -company.docs.search("xbrl") # search it for a topic -``` - -A tool error is almost always a fixable usage detail, not a dead end. When a script or call -fails, **recover here** — re-run `scripts/orient.py`, query `.docs`, or read the relevant -guide — rather than abandoning EDGAR for web search. The filings are the authoritative, -auditable source; don't let a transient error push the work onto unverifiable web results. - -**Amendment vs. original filing.** `fetch_filing.py` always skips amended forms -(10-K/A, 10-Q/A, etc.) and picks the most recent *original* filing. Amendments typically -contain only the amended items (e.g. Part III), not the full filing, so silently picking -one would lose most of the content. If you specifically need an amendment's items, fetch -it by accession number using inline Python as shown in `guide_financials.md`. +--- +name: sec-edgar-skill +description: >- + Retrieve and extract SEC EDGAR filings and ownership data for US-listed companies. Use this + whenever a task touches a company's filings, financials, ownership, governance, or + institutional holders — even if EDGAR is not named explicitly. Covers 10-K/10-Q/8-K, + 20-F/6-K foreign filings, XBRL financial statements, insider transactions, 13F institutional + holdings (via 13f.info), 13D/13G blockholdings, and DEF 14A proxy/compensation. Always start + by running scripts/orient.py, then pull only the sections you need. Unopinionated data layer: + it fetches and extracts token-efficiently; it does not decide what is significant. +--- + +# SEC EDGAR Research Skill + +A toolkit for retrieving and extracting SEC EDGAR filings for US-listed companies — +efficiently, within a token budget, using `edgartools`. + +## What this skill is — and is not + +This is the **tools layer** of a research stack. It knows *how* to find, download, and +extract SEC filing data, plus the library mechanics to do it reliably. It is +deliberately **unopinionated**: it does not judge what is a good number, a red flag, or +worth looking at. That judgment belongs to whatever **analytical-framework skill** is +driving (e.g. a value-investing framework); presentation belongs to a downstream +consumer skill. Keep this layer neutral so any framework can compose on top of it. + +Provide capability and facts; let the caller reason. The only hard requirements here are +mechanical (set an SEC identity, or requests are blocked) — never analytical. + +## Setup + +Run `python scripts/test_setup.py --live` to verify dependencies and SEC identity. +If identity is missing, the error message tells the user exactly how to set +`$EDGAR_IDENTITY` (see the [profile setup](../../README.md#one-time-runtime-setup) for the full +explanation). Every script reads it automatically; you can also pass `--identity` +per-call. + +The scripts handle environment hazards on import (UTF-8 stdout on Windows, truststore +for corporate proxies). When writing **inline Python** that calls `edgartools` directly +(not via a bundled script), replicate that preamble — see `references/guide_core.md` +§ "Inline Python preamble." + +## How to work efficiently: pull only what you need + +Filings are huge — a 10-K can exceed 100k words. Loading one whole wastes the context +window and buries the signal. The method is to keep documents **on disk** and pull only +the exact lines you need into context. Four phases: + +1. **Orient first — always run `scripts/orient.py`.** `python scripts/orient.py --ticker ` + prints the company's `.to_context()` summary, surveys the **mix of forms it has actually + filed** (with date ranges), and lists the most recent filings — the cheapest way to see what + a company files *now* and how that has changed, so you fetch the right forms instead of + assuming a form set. This is the **non-negotiable first step — run it before any web + search, even for breaking news.** When the user says "just reported" or "a few hours ago," + orient.py will show the 8-K filed today immediately; then fetch it with + `fetch_filing.py --form 8-K --date --attachment list` to get the press-release + exhibit (Exhibit 99.1). The filing is always faster and more authoritative than a web + search for what the company itself disclosed. (For finer control, `.to_context()` + on a `Company`, filing collection, or `XBRL` object gives the same preview inline — see + `guide_core.md`.) +2. **Download to the cache as Markdown.** Use the scripts to write filings to disk as + clean Markdown — `edgartools` converts SEC HTML, stripping layout bloat to roughly a + tenth of the size, and the result is greppable. +3. **Get just the section you need.** Item-addressable forms (10-K/10-Q/8-K/20-F) let you + list a filing's SEC item codes and pull one section by code — no scanning. For a full + report, `scripts/list_headings.py` maps its `#` headers to line numbers; tabular + free-form filings (e.g. DEF 14A) instead carry their own table of contents up top — + read it, then grep. `guide_filings.md` has the mechanics. +4. **Search, then read precisely.** Use your native grep/ripgrep over the cached files + to find the lines that matter, then read just those ranges. Let grep and disk do the + heavy lifting; spend context only on the paragraphs you actually need. + +## The cache + +Downloads go to `//__[__].md` +(statements as `…__.csv`). The root resolves as `--cache-dir` > +`$SEC_CACHE_DIR` > `./sec-cache` — workspace-relative so your grep tool finds it by +default, and persistent across runs so you don't re-hit the SEC. Filenames are +deterministic and keyed by the globally-unique accession number, so **before +downloading, check whether the file already exists** (list or glob `//`) +and reuse it. Every script prints the absolute path(s) it wrote to stdout. + +> If your grep tool uses ripgrep and the cache is gitignored, ripgrep skips it by +> default. Either point the search at `//` explicitly, or pass +> `--no-ignore`. Resolve the path from a script's stdout or `$SEC_CACHE_DIR` — never +> hard-code an absolute cache path. + +## Reference guides (read the relevant one before extracting) + +Each guide is loaded only when its domain is in play, so you carry just the rules you +need. Read the matching guide first — it holds the item codes, taxonomies, and library +quirks that make extraction correct. + +| Guide | Use it for | +| :-- | :-- | +| `references/guide_core.md` | The mechanics behind `scripts/orient.py`: resolving a company, listing/filtering filings, surveying the filing mix, `.to_context()` previews, and `.docs` self-help. Read it to drive orientation inline or go beyond the script. | +| `references/guide_filings.md` | Filing text: pulling a section by SEC item code (10-K/10-Q/8-K/20-F) vs. navigating free-form filings (DEF 14A/6-K) by their own contents, plus attachments and exhibits (incl. 6-K Exhibit 99.1). | +| `references/guide_financials.md` | XBRL financial statements and individual facts (US-GAAP and IFRS), and the period-aggregation pitfalls. | +| `references/guide_ownership.md` | Insider transactions (Forms 3/4/5), beneficial ownership, and executive compensation (DEF 14A; Form 20-F Item 6 for foreign issuers). For the common case — “what are insiders buying/selling?” — use `scripts/fetch_insider_trades.py` directly; no guide needed. | +| `references/guide_holdings.md` | **Deep route only:** raw 13F via edgartools (voting authority, amendments, specific holdings) and 5%+ blockholders (13D/13G). For the common case — “who owns this stock?” — use `scripts/fetch_13f_holders.py` directly (see Scripts above); no guide needed. | + +## Scripts + +Run them with the project's Python. Each prints the absolute cache path(s) it wrote to +stdout and logs progress to stderr. **`--help` is the authoritative flag reference** — +the list below shows one canonical invocation each: + +```bash +# Orient first: company summary + filing-mix survey + recent filings +python scripts/orient.py --ticker AAPL + +# One filing (full), a single section, or an attachment — into the cache +python scripts/fetch_filing.py --ticker AAPL --form 10-K --year 2023 +python scripts/fetch_filing.py --ticker AAPL --form 10-K --year 2023 --section "Item 1A" # or: --section list +python scripts/fetch_filing.py --ticker WIX --form 6-K --attachment "ex-99.1" # or: list | all | + +# Target a filing by date (e.g. an 8-K filed today) instead of just --year: +python scripts/fetch_filing.py --ticker AAPL --form 8-K --date 2026-06-15 + +# Many filings across a year range (add --attachments to capture e.g. 6-K exhibits) +python scripts/fetch_filings.py --ticker AAPL --form 10-Q --start-year 2022 --end-year 2024 + +# XBRL statements (income | balance | cashflow | all) -> CSV (annual or quarterly) +python scripts/parse_financials.py --ticker AAPL --year 2023 --statement all +python scripts/parse_financials.py --ticker AAPL --year 2024 --quarter 1 --statement all + +# Table of contents for a large cached filing +python scripts/list_headings.py --file sec-cache/AAPL/10-K_2023-11-03_0000320193-23-000106.md + +# Insider transactions — what are insiders buying/selling? (Form 4) +python scripts/fetch_insider_trades.py --ticker AAPL +python scripts/fetch_insider_trades.py --ticker AAPL --start 2025-01-01 --end 2026-06-17 +python scripts/fetch_insider_trades.py --ticker AAPL --start 2025-06-01 --buys-only + +# 13F institutional holders — who owns this stock? (via 13f.info, no SEC identity needed) +python scripts/fetch_13f_holders.py --ticker AAPL --top 15 + +# 13F holder history — how has institutional ownership changed? +python scripts/fetch_13f_holders.py --ticker AAPL --history + +# 13F manager search — what does a specific fund hold? +python scripts/fetch_13f_holders.py --manager "Berkshire Hathaway" + +# 13F cross-reference — one manager's position history in one stock +python scripts/fetch_13f_holders.py --cik 0000906304 --cusip 205826209 + +# Environment diagnostics +python scripts/test_setup.py --live +``` + +## When the API surprises you: self-heal with `.docs` + +`edgartools` documents itself at runtime. If a method or attribute isn't what you +expected, query it inline instead of guessing — this recovers from most API uncertainty +without leaving the session: + +```python +company.docs # full API guide for the object +company.docs.search("xbrl") # search it for a topic +``` + +A tool error is almost always a fixable usage detail, not a dead end. When a script or call +fails, **recover here** — re-run `scripts/orient.py`, query `.docs`, or read the relevant +guide — rather than abandoning EDGAR for web search. The filings are the authoritative, +auditable source; don't let a transient error push the work onto unverifiable web results. + +**Amendment vs. original filing.** `fetch_filing.py` always skips amended forms +(10-K/A, 10-Q/A, etc.) and picks the most recent *original* filing. Amendments typically +contain only the amended items (e.g. Part III), not the full filing, so silently picking +one would lose most of the content. If you specifically need an amendment's items, fetch +it by accession number using inline Python as shown in `guide_financials.md`. diff --git a/sec-edgar-skill/references/guide_core.md b/skills/sec-edgar-skill/references/guide_core.md similarity index 81% rename from sec-edgar-skill/references/guide_core.md rename to skills/sec-edgar-skill/references/guide_core.md index 6741172..809e666 100644 --- a/sec-edgar-skill/references/guide_core.md +++ b/skills/sec-edgar-skill/references/guide_core.md @@ -1,102 +1,108 @@ -# Core — company lookup, filing discovery, and self-help - -Start here. This guide covers resolving a company, listing and filtering its filings, -and the two built-in efficiency tools — `.to_context()` previews and the `.docs` -self-help system. `scripts/orient.py` wraps the orientation steps below into one command; -this guide is the mechanics behind it, for when you drive them inline. The other guides -build on these basics. - -## Resolve a company - -`Company` accepts a ticker, a CIK, or a name: - -```python -from edgar import Company -company = Company("AAPL") # ticker -company = Company("0000320193") # CIK -company = Company("Apple Inc.") # name -``` - -Metadata lives on the object: `company.cik`, `company.name`, `company.sic`, and the -tickers. - -> The ticker attribute is `company.tickers` (a list) or `company.get_ticker()` (a -> string). There is no `company.ticker` — accessing it raises `AttributeError`. This -> matters when building a cache path, where you want `company.tickers[0]`. - -## List and filter filings - -```python -filings = company.get_filings() # everything -filings = company.get_filings(form="10-Q", year=2024) # by form + year -filings = company.get_filings(quarter=4, year=2024) # by quarter -filings = company.get_filings(date="2023-01-01:2023-12-31") # by date range -``` - -Collections support indexing, slicing, and `.latest()`: - -```python -latest_10k = company.get_filings(form="10-K").latest() -recent = filings[0:10] -``` - -## Survey a company's filing mix - -`scripts/orient.py` does this for you (with per-form date ranges and the most recent -filings); reach for it first. To do it inline — or to tabulate a custom window — pull the -collection to a DataFrame. This is a neutral mechanic; which forms are relevant to your -question is for you (or the framework driving you) to decide. - -```python -df = company.get_filings(date="2024-01-01:2025-12-31").to_pandas() -print(df["form"].value_counts()) -``` - -Survey a multi-year window, not just the latest filing: a company's form set is **not** -fixed over time — it can change as the company's circumstances do (for instance its -domestic-vs-foreign reporting status, a recent listing, or a corporate action), and the -per-form date ranges are what reveal such a shift. The other guides cover how to extract -each type once you've found it. - -## Preview cheaply with `.to_context()` - -Before pulling full text or large tables, print a compact summary so you understand -what's available. This saves the bulk of the tokens a raw dump would cost: - -```python -print(company.to_context()) -print(filings.to_context()) -``` - -## Self-help with `.docs` - -`edgartools` documents itself at runtime. When unsure of a method or attribute, query it -inline rather than guessing: - -```python -company.docs # full API guide for the object -company.docs.search("xbrl") # search it for a topic -latest_10k.docs.search("attachments") -``` - -## Inline Python preamble - -When running `edgartools` inline (not via a bundled script), replicate the environment -setup the scripts do on import: - -```python -import os, sys -if sys.platform.startswith("win"): - sys.stdout.reconfigure(encoding="utf-8") -try: - import truststore; truststore.inject_into_ssl() -except ImportError: - pass -import edgar; edgar.set_identity(os.environ["EDGAR_IDENTITY"]) -``` - -This forces UTF-8 output (edgartools' emoji-rich reprs crash Windows cp1252 consoles), -routes TLS through the OS trust store (so HTTPS works behind an inspecting corporate -proxy instead of raising `CERTIFICATE_VERIFY_FAILED`), and reads the SEC identity from -the environment (the scripts resolve this automatically; inline code must do it -explicitly). +# Core — company lookup, filing discovery, and self-help + +Start here. This guide covers resolving a company, listing and filtering its filings, +and the two built-in efficiency tools — `.to_context()` previews and the `.docs` +self-help system. `scripts/orient.py` wraps the orientation steps below into one command; +this guide is the mechanics behind it, for when you drive them inline. The other guides +build on these basics. + +## Resolve a company + +`Company` accepts a ticker, a CIK, or a name: + +```python +from edgar import Company + +company = Company("AAPL") # ticker +company = Company("0000320193") # CIK +company = Company("Apple Inc.") # name +``` + +Metadata lives on the object: `company.cik`, `company.name`, `company.sic`, and the +tickers. + +> The ticker attribute is `company.tickers` (a list) or `company.get_ticker()` (a +> string). There is no `company.ticker` — accessing it raises `AttributeError`. This +> matters when building a cache path, where you want `company.tickers[0]`. + +## List and filter filings + +```python +filings = company.get_filings() # everything +filings = company.get_filings(form="10-Q", year=2024) # by form + year +filings = company.get_filings(quarter=4, year=2024) # by quarter +filings = company.get_filings(date="2023-01-01:2023-12-31") # by date range +``` + +Collections support indexing, slicing, and `.latest()`: + +```python +latest_10k = company.get_filings(form="10-K").latest() +recent = filings[0:10] +``` + +## Survey a company's filing mix + +`scripts/orient.py` does this for you (with per-form date ranges and the most recent +filings); reach for it first. To do it inline — or to tabulate a custom window — pull the +collection to a DataFrame. This is a neutral mechanic; which forms are relevant to your +question is for you (or the framework driving you) to decide. + +```python +df = company.get_filings(date="2024-01-01:2025-12-31").to_pandas() +print(df["form"].value_counts()) +``` + +Survey a multi-year window, not just the latest filing: a company's form set is **not** +fixed over time — it can change as the company's circumstances do (for instance its +domestic-vs-foreign reporting status, a recent listing, or a corporate action), and the +per-form date ranges are what reveal such a shift. The other guides cover how to extract +each type once you've found it. + +## Preview cheaply with `.to_context()` + +Before pulling full text or large tables, print a compact summary so you understand +what's available. This saves the bulk of the tokens a raw dump would cost: + +```python +print(company.to_context()) +print(filings.to_context()) +``` + +## Self-help with `.docs` + +`edgartools` documents itself at runtime. When unsure of a method or attribute, query it +inline rather than guessing: + +```python +company.docs # full API guide for the object +company.docs.search("xbrl") # search it for a topic +latest_10k.docs.search("attachments") +``` + +## Inline Python preamble + +When running `edgartools` inline (not via a bundled script), replicate the environment +setup the scripts do on import: + +```python +import os, sys + +if sys.platform.startswith("win"): + sys.stdout.reconfigure(encoding="utf-8") +try: + import truststore + + truststore.inject_into_ssl() +except ImportError: + pass +import edgar + +edgar.set_identity(os.environ["EDGAR_IDENTITY"]) +``` + +This forces UTF-8 output (edgartools' emoji-rich reprs crash Windows cp1252 consoles), +routes TLS through the OS trust store (so HTTPS works behind an inspecting corporate +proxy instead of raising `CERTIFICATE_VERIFY_FAILED`), and reads the SEC identity from +the environment (the scripts resolve this automatically; inline code must do it +explicitly). diff --git a/sec-edgar-skill/references/guide_filings.md b/skills/sec-edgar-skill/references/guide_filings.md similarity index 89% rename from sec-edgar-skill/references/guide_filings.md rename to skills/sec-edgar-skill/references/guide_filings.md index 816cce1..fb85eda 100644 --- a/sec-edgar-skill/references/guide_filings.md +++ b/skills/sec-edgar-skill/references/guide_filings.md @@ -1,115 +1,115 @@ -# Filing text — sections, items, and exhibits - -Everything you can pull as *text* from a filing: a whole report, one section, or an -exhibit. Two facts shape how you do it efficiently: - -- **Filings live on disk, not in context.** Convert to Markdown into the cache, then grep - and read line ranges (`edgartools` strips SEC HTML to roughly a tenth of the size). -- **A form is addressed one of two ways.** Some forms expose their sections by the SEC - *item codes* you already know; the rest you navigate by their own table of contents. - Knowing which kind you're holding is the whole game. - -`scripts/fetch_filing.py` wraps every path below; the library snippets show what it calls -under the hood, so you can drop to them inline when a flag doesn't cover your case. For -XBRL numbers (revenue, EPS, balances) use `guide_financials.md` — this guide is the -narrative and event text. - -## 1. Item-addressable forms — pull a section by its code - -Periodic and current reports parse into a typed object whose sections are keyed by SEC -item code. You don't discover the structure — you ask for the item directly. - -```python -filing = company.get_filings(form="10-K").latest() -report = filing.obj() # TenK / TenQ / CurrentReport / TwentyF / ... -report.items # -> ['Item 1', 'Item 1A', 'Item 1B', ...] actually present -risk = report["Item 1A"] # just that item's text, not the whole 100k-word filing -``` - -- `report.items` is the filing's real table of contents in the SEC's own taxonomy — a - neutral structural map. List it first to see what the filing actually contains. -- `report[""]` returns only that section, so you spend tokens on one item. An item - that isn't present returns `None`. -- Script: `fetch_filing.py --section "Item 1A"`, or `--section list` to print the codes the - filing contains. - -> `Filing.markdown()` takes **no** section argument — passing one is silently ignored and -> returns the whole filing. Sections come from the parsed object (`filing.obj()[code]`), -> which is what `--section` uses. - -The codes are fixed by SEC rule, which is why you can address them from memory: - -```text -10-K Item 1 Business · Item 1A Risk Factors · Item 7 MD&A · Item 7A Market Risk · Item 8 Financial Statements -10-Q keyed by Part: "Part I, Item 2" MD&A · "Part II, Item 1" Legal · "Part II, Item 1A" Risk Factors -8-K Item 1.01 Material Agreement · Item 2.02 Results of Operations (earnings) · Item 5.02 Director/Officer change · Item 9.01 Exhibits -20-F Item 3.D Risk Factors · Item 4 Business · Item 5 Operating & Financial Review (MD&A) · Item 6 Directors & Compensation -``` - -`report.items` always returns the complete, authoritative list for the filing in hand; the -lines above just show the code *format*, which differs by form: - -- **10-Q codes are namespaced by Part — and a bare code resolves to the wrong one.** Pass - the full `"Part I, Item 2"` for the MD&A: a bare `"Item 2"` silently returns *Part II's* - Item 2 ("Unregistered Sales"), not what you meant. Run `--section list` to see the exact - keys. -- **An 8-K's `report.items` tells you what it's *about*.** An 8-K reports only the events it - fired (e.g. `['Item 2.02', 'Item 9.01']` is an earnings release with exhibits), so listing - the items is the cheapest way to triage one before reading it. -- **20-F uses a different scheme** from the 10-K (risk factors are Item 3.D, MD&A is Item 5), - because foreign private issuers report on a different schedule. -- **40-F** (Canadian MJDS) usually wraps the home-country annual report as exhibits rather - than US items — fetch the full filing or its attachments (§3). - -## 2. Free-form forms — read the structure, then grep - -DEF 14A proxies, 6-K reports, prospectuses, and the like have no item taxonomy: their -`filing.obj()` exposes no `.items` and isn't subscriptable. Save the whole filing as -Markdown, then navigate by its own structure: - -```python -text = filing.markdown() # clean Markdown for the entire filing -``` - -- A proxy (and most long filings) carries a **table of contents** in its first ~100 lines, - listing every section with its page number — that's the filing's own map. Read it to see - what's inside, then grep the body for the section you want. -- These convert to heavily *tabular* Markdown with few `#` headers, so `list_headings.py` - (which keys on `#`) helps less here than on a periodic report. On a full 10-K/10-Q/20-F — - whose Markdown *is* `#`-structured — `list_headings.py` is the fast way to map it. - -Where a free-form filing's substance lives is in the relevant domain guide — e.g. proxy / -compensation in `guide_ownership.md`. - -## 3. Attachments and exhibits - -Exhibits — press releases, agreements, the foreign annual report inside a 40-F — are -separate documents hanging off the filing: - -```python -attachments = list(filing.attachments) # cast to a list first (see below) -for i, att in enumerate(attachments[:10]): # slice — a filing can have 90+ exhibits - print(i, att.document, att.description) -text = attachments[1].markdown() # convert one exhibit to Markdown -``` - -Script: `fetch_filing.py --attachment "ex-99.1"` (or `list` | `all` | an index), or -`fetch_filings.py --attachments` to capture exhibits across a whole year range. - -> **Index attachments via a list.** `filing.attachments` looks items up by their 1-based -> SEC *sequence number*, which can skip values — so integer indexing on the raw collection -> can return the wrong item or none. Cast to `list(...)` first and use 0-based indices. -> -> **Foreign private issuers and Form 6-K.** A 6-K is how an FPI reports interim results and -> material events — its 8-K/10-Q equivalent — but it's free-form (no item codes), and its -> main body is often just a brief cover note. The actual results or press release is an -> attachment, usually **Exhibit 99.1**. If a 6-K body comes back empty, list the attachments -> and fetch the exhibit (`fetch_filing.py --attachment "ex-99.1"`). - -## Then search locally - -Once a filing (or section, or exhibit) is in the cache, search it with your native -grep/ripgrep and read the matching line ranges. Don't run text searches through the library -— that's slower and round-trips to remote endpoints. *What* you search for, and what you -make of it, is yours (or your framework's) to decide; this skill just makes the text fast to -reach. +# Filing text — sections, items, and exhibits + +Everything you can pull as *text* from a filing: a whole report, one section, or an +exhibit. Two facts shape how you do it efficiently: + +- **Filings live on disk, not in context.** Convert to Markdown into the cache, then grep + and read line ranges (`edgartools` strips SEC HTML to roughly a tenth of the size). +- **A form is addressed one of two ways.** Some forms expose their sections by the SEC + *item codes* you already know; the rest you navigate by their own table of contents. + Knowing which kind you're holding is the whole game. + +`scripts/fetch_filing.py` wraps every path below; the library snippets show what it calls +under the hood, so you can drop to them inline when a flag doesn't cover your case. For +XBRL numbers (revenue, EPS, balances) use `guide_financials.md` — this guide is the +narrative and event text. + +## 1. Item-addressable forms — pull a section by its code + +Periodic and current reports parse into a typed object whose sections are keyed by SEC +item code. You don't discover the structure — you ask for the item directly. + +```python +filing = company.get_filings(form="10-K").latest() +report = filing.obj() # TenK / TenQ / CurrentReport / TwentyF / ... +report.items # -> ['Item 1', 'Item 1A', 'Item 1B', ...] actually present +risk = report["Item 1A"] # just that item's text, not the whole 100k-word filing +``` + +- `report.items` is the filing's real table of contents in the SEC's own taxonomy — a + neutral structural map. List it first to see what the filing actually contains. +- `report[""]` returns only that section, so you spend tokens on one item. An item + that isn't present returns `None`. +- Script: `fetch_filing.py --section "Item 1A"`, or `--section list` to print the codes the + filing contains. + +> `Filing.markdown()` takes **no** section argument — passing one is silently ignored and +> returns the whole filing. Sections come from the parsed object (`filing.obj()[code]`), +> which is what `--section` uses. + +The codes are fixed by SEC rule, which is why you can address them from memory: + +```text +10-K Item 1 Business · Item 1A Risk Factors · Item 7 MD&A · Item 7A Market Risk · Item 8 Financial Statements +10-Q keyed by Part: "Part I, Item 2" MD&A · "Part II, Item 1" Legal · "Part II, Item 1A" Risk Factors +8-K Item 1.01 Material Agreement · Item 2.02 Results of Operations (earnings) · Item 5.02 Director/Officer change · Item 9.01 Exhibits +20-F Item 3.D Risk Factors · Item 4 Business · Item 5 Operating & Financial Review (MD&A) · Item 6 Directors & Compensation +``` + +`report.items` always returns the complete, authoritative list for the filing in hand; the +lines above just show the code *format*, which differs by form: + +- **10-Q codes are namespaced by Part — and a bare code resolves to the wrong one.** Pass + the full `"Part I, Item 2"` for the MD&A: a bare `"Item 2"` silently returns *Part II's* + Item 2 ("Unregistered Sales"), not what you meant. Run `--section list` to see the exact + keys. +- **An 8-K's `report.items` tells you what it's *about*.** An 8-K reports only the events it + fired (e.g. `['Item 2.02', 'Item 9.01']` is an earnings release with exhibits), so listing + the items is the cheapest way to triage one before reading it. +- **20-F uses a different scheme** from the 10-K (risk factors are Item 3.D, MD&A is Item 5), + because foreign private issuers report on a different schedule. +- **40-F** (Canadian MJDS) usually wraps the home-country annual report as exhibits rather + than US items — fetch the full filing or its attachments (§3). + +## 2. Free-form forms — read the structure, then grep + +DEF 14A proxies, 6-K reports, prospectuses, and the like have no item taxonomy: their +`filing.obj()` exposes no `.items` and isn't subscriptable. Save the whole filing as +Markdown, then navigate by its own structure: + +```python +text = filing.markdown() # clean Markdown for the entire filing +``` + +- A proxy (and most long filings) carries a **table of contents** in its first ~100 lines, + listing every section with its page number — that's the filing's own map. Read it to see + what's inside, then grep the body for the section you want. +- These convert to heavily *tabular* Markdown with few `#` headers, so `list_headings.py` + (which keys on `#`) helps less here than on a periodic report. On a full 10-K/10-Q/20-F — + whose Markdown *is* `#`-structured — `list_headings.py` is the fast way to map it. + +Where a free-form filing's substance lives is in the relevant domain guide — e.g. proxy / +compensation in `guide_ownership.md`. + +## 3. Attachments and exhibits + +Exhibits — press releases, agreements, the foreign annual report inside a 40-F — are +separate documents hanging off the filing: + +```python +attachments = list(filing.attachments) # cast to a list first (see below) +for i, att in enumerate(attachments[:10]): # slice — a filing can have 90+ exhibits + print(i, att.document, att.description) +text = attachments[1].markdown() # convert one exhibit to Markdown +``` + +Script: `fetch_filing.py --attachment "ex-99.1"` (or `list` | `all` | an index), or +`fetch_filings.py --attachments` to capture exhibits across a whole year range. + +> **Index attachments via a list.** `filing.attachments` looks items up by their 1-based +> SEC *sequence number*, which can skip values — so integer indexing on the raw collection +> can return the wrong item or none. Cast to `list(...)` first and use 0-based indices. +> +> **Foreign private issuers and Form 6-K.** A 6-K is how an FPI reports interim results and +> material events — its 8-K/10-Q equivalent — but it's free-form (no item codes), and its +> main body is often just a brief cover note. The actual results or press release is an +> attachment, usually **Exhibit 99.1**. If a 6-K body comes back empty, list the attachments +> and fetch the exhibit (`fetch_filing.py --attachment "ex-99.1"`). + +## Then search locally + +Once a filing (or section, or exhibit) is in the cache, search it with your native +grep/ripgrep and read the matching line ranges. Don't run text searches through the library +— that's slower and round-trips to remote endpoints. *What* you search for, and what you +make of it, is yours (or your framework's) to decide; this skill just makes the text fast to +reach. diff --git a/sec-edgar-skill/references/guide_financials.md b/skills/sec-edgar-skill/references/guide_financials.md similarity index 81% rename from sec-edgar-skill/references/guide_financials.md rename to skills/sec-edgar-skill/references/guide_financials.md index a5d77cb..e3c562c 100644 --- a/sec-edgar-skill/references/guide_financials.md +++ b/skills/sec-edgar-skill/references/guide_financials.md @@ -1,65 +1,65 @@ -# Financials — XBRL statements and facts - -How to pull structured financial statements (Income, Balance Sheet, Cash Flow) and -individual XBRL facts, for both US-GAAP and IFRS filers. `scripts/parse_financials.py` -wraps statement extraction to CSV. - -## Get filings for parsing - -```python -filings = company.get_filings(form=["10-K", "20-F", "40-F"], year=2024, amendments=False) -filing = filings.latest() -``` - -> **Pass `year` to `get_filings`, not `.filter()`.** `EntityFilings.filter(year=...)` -> raises `TypeError` — `filter` doesn't accept `year`. -> -> **Use `amendments=False`.** Amendments (`10-K/A`, etc.) often carry only minor text -> changes and lack complete XBRL statement trees; exclude them to get the primary -> statements. - -Annual reports (10-K / 20-F / 40-F) and quarterly reports (10-Q / 6-K) carry the -complete statement trees and can be parsed to CSV using `parse_financials.py`. - -## Parse statements from a filing - -```python -xbrl = filing.xbrl() -print(xbrl.to_context()) # lists available statements - -income = xbrl.statements.income_statement() -balance = xbrl.statements.balance_sheet() -cash = xbrl.statements.cashflow_statement() # note: "cashflow", no underscore -df = income.to_dataframe() -``` - -> Statement accessors live on `xbrl.statements`, not on the `XBRL` object itself, and the -> cash-flow method is `cashflow_statement()` (no underscore in "cashflow"). - -## Multi-period history from the company - -```python -fin = company.get_financials() -print(fin.to_context()) -df = fin.income_statement().to_dataframe() # methods are directly on this object -``` - -On the object returned by `get_financials()`, the statement methods are direct — not -under `.statements`. - -> **Don't blindly `.mean()` / `.sum()` across periods.** A multi-period pull mixes -> current-period values with prior-period comparatives. For balance-sheet (instant) -> facts, filter `period_instant == report_date`; for income/cash-flow (duration) facts, -> filter `period_end == report_date` and sanity-check the duration (~90 days quarterly, -> ~360 days annual). Otherwise you average current figures with comparatives and corrupt -> the series. - -## Individual facts — US-GAAP and IFRS - -```python -rev_us = xbrl.get_fact("us-gaap:Revenues") -rev_ifrs = xbrl.get_fact("ifrs-full:Revenue") # foreign issuers often file IFRS -``` - -If US-GAAP tags come back empty for a foreign private issuer, try the IFRS equivalent — -20-F filers frequently report under IFRS rather than US-GAAP. +# Financials — XBRL statements and facts + +How to pull structured financial statements (Income, Balance Sheet, Cash Flow) and +individual XBRL facts, for both US-GAAP and IFRS filers. `scripts/parse_financials.py` +wraps statement extraction to CSV. + +## Get filings for parsing + +```python +filings = company.get_filings(form=["10-K", "20-F", "40-F"], year=2024, amendments=False) +filing = filings.latest() +``` + +> **Pass `year` to `get_filings`, not `.filter()`.** `EntityFilings.filter(year=...)` +> raises `TypeError` — `filter` doesn't accept `year`. +> +> **Use `amendments=False`.** Amendments (`10-K/A`, etc.) often carry only minor text +> changes and lack complete XBRL statement trees; exclude them to get the primary +> statements. + +Annual reports (10-K / 20-F / 40-F) and quarterly reports (10-Q / 6-K) carry the +complete statement trees and can be parsed to CSV using `parse_financials.py`. + +## Parse statements from a filing + +```python +xbrl = filing.xbrl() +print(xbrl.to_context()) # lists available statements + +income = xbrl.statements.income_statement() +balance = xbrl.statements.balance_sheet() +cash = xbrl.statements.cashflow_statement() # note: "cashflow", no underscore +df = income.to_dataframe() +``` + +> Statement accessors live on `xbrl.statements`, not on the `XBRL` object itself, and the +> cash-flow method is `cashflow_statement()` (no underscore in "cashflow"). + +## Multi-period history from the company + +```python +fin = company.get_financials() +print(fin.to_context()) +df = fin.income_statement().to_dataframe() # methods are directly on this object +``` + +On the object returned by `get_financials()`, the statement methods are direct — not +under `.statements`. + +> **Don't blindly `.mean()` / `.sum()` across periods.** A multi-period pull mixes +> current-period values with prior-period comparatives. For balance-sheet (instant) +> facts, filter `period_instant == report_date`; for income/cash-flow (duration) facts, +> filter `period_end == report_date` and sanity-check the duration (~90 days quarterly, +> ~360 days annual). Otherwise you average current figures with comparatives and corrupt +> the series. + +## Individual facts — US-GAAP and IFRS + +```python +rev_us = xbrl.get_fact("us-gaap:Revenues") +rev_ifrs = xbrl.get_fact("ifrs-full:Revenue") # foreign issuers often file IFRS +``` + +If US-GAAP tags come back empty for a foreign private issuer, try the IFRS equivalent — +20-F filers frequently report under IFRS rather than US-GAAP. diff --git a/sec-edgar-skill/references/guide_holdings.md b/skills/sec-edgar-skill/references/guide_holdings.md similarity index 77% rename from sec-edgar-skill/references/guide_holdings.md rename to skills/sec-edgar-skill/references/guide_holdings.md index da5fd63..54b2f8b 100644 --- a/sec-edgar-skill/references/guide_holdings.md +++ b/skills/sec-edgar-skill/references/guide_holdings.md @@ -13,7 +13,7 @@ ```python manager = Company("Magnetar Capital LLC") f13 = manager.get_filings(form="13F-HR").latest().obj() -df = f13.holdings # the holdings DataFrame +df = f13.holdings # the holdings DataFrame ``` > Operating companies (AAPL, etc.) do **not** file 13F — querying their CIK for `13F-HR` @@ -31,14 +31,12 @@ include the `"SC "`-prefixed names — EDGAR often indexes these schedules that omitting them returns empty results: ```python -blocks = company.get_filings( - form=["13D", "13G", "SC 13D", "SC 13G", "SC 13D/A", "SC 13G/A"] -) -sched = blocks.latest().obj() # Schedule13D / Schedule13G structured object -sched.reporting_persons # who holds — each with voting / dispositive power -sched.issuer_info # the subject company (name, CIK, CUSIP) -sched.total_percent # aggregate % of class -sched.items.item4_purpose_of_transaction # 13D Item 4 narrative, when present +blocks = company.get_filings(form=["13D", "13G", "SC 13D", "SC 13G", "SC 13D/A", "SC 13G/A"]) +sched = blocks.latest().obj() # Schedule13D / Schedule13G structured object +sched.reporting_persons # who holds — each with voting / dispositive power +sched.issuer_info # the subject company (name, CIK, CUSIP) +sched.total_percent # aggregate % of class +sched.items.item4_purpose_of_transaction # 13D Item 4 narrative, when present ``` These schedules parse into a **structured object**, not an item-addressable one — there is diff --git a/sec-edgar-skill/references/guide_ownership.md b/skills/sec-edgar-skill/references/guide_ownership.md similarity index 88% rename from sec-edgar-skill/references/guide_ownership.md rename to skills/sec-edgar-skill/references/guide_ownership.md index 5590c44..00b8616 100644 --- a/sec-edgar-skill/references/guide_ownership.md +++ b/skills/sec-edgar-skill/references/guide_ownership.md @@ -1,64 +1,64 @@ -# Ownership & compensation — insiders, proxies, and exec pay - -Two related questions about the people who run and own a company: what insiders are -buying and selling (Forms 3/4/5), and how executives are paid (the DEF 14A proxy, or -Form 20-F Item 6 for foreign issuers). For *external* 5%+ holders and institutions, see -`guide_holdings.md` instead. - -## Insider transactions (Forms 3, 4, 5) - -There is no `company.get_insiders()`. Query the ownership forms directly: - -```python -form4s = company.get_filings(form="4") # 3 = initial, 4 = changes, 5 = annual -latest = form4s.latest().obj() # parse the XML into a structured object -print(latest.insider_name, latest.position) -df = latest.to_dataframe() -``` - -Access individual trades via DataFrames or the activities helper — not by iterating -`non_derivative_transactions` (which isn't exposed as a standard list): - -```python -df_market = latest.market_trades # open-market buys / sells -df_options = latest.option_exercises -for act in latest.get_transaction_activities(): - print(act.transaction_type, act.code, act.shares, act.price_per_share) - # transaction codes: P = purchase, S = sale, M = option exercise, F = tax withholding -``` - -## Executive compensation — DEF 14A (domestic filers) - -US filers disclose executive compensation in the annual proxy (DEF 14A) and incorporate it -by reference into the 10-K rather than printing it there — so look in the proxy, not the -10-K. A proxy is a free-form filing: `filing.obj()` is a `ProxyStatement` with no item -codes, so you can't pull a section by code the way you can from a 10-K. Download it, read -the table of contents in its first ~100 lines to see its sections, then grep the body for -the part you want. - -```python -proxy = company.get_filings(form="DEF 14A").latest() -text = proxy.markdown() # save to the cache, then read its ToC + grep -``` - -The compensation disclosures (the Summary Compensation Table and the rest) are mandated by -Regulation S-K Item 402 — so a proxy's contents are predictable from that rule. Which tables -and narrative you pull, and what you make of them, is the caller's call. See -`guide_filings.md` for the item-addressable-vs-free-form distinction in general. - -## Foreign private issuers - -FPIs are exempt from several US ownership and governance rules, which changes *where* the -data is: - -- **Section 16 exemption.** FPIs (and Canadian MJDS filers) don't file Forms 3/4/5, so - insider-transaction data won't appear on EDGAR. Check the home-jurisdiction regulator - (e.g. SEDAR+ for Canada) instead. -- **No DEF 14A.** FPIs don't file US proxies. Compensation is in **Form 20-F Item 6.B** - and share ownership in **Item 6.E**. Many FPIs disclose pay only in aggregate unless - their home-country rules require individual figures. - -> **20-F Item 7.A boundary trap.** When slicing "Item 7.A Major Shareholders" by text, -> note the full Item 7 title is "Major Shareholders **and Related Party Transactions**." -> Using "Related Party Transactions" as the end boundary truncates early, because it also -> appears in the title — terminate the slice on "Item 8" instead. +# Ownership & compensation — insiders, proxies, and exec pay + +Two related questions about the people who run and own a company: what insiders are +buying and selling (Forms 3/4/5), and how executives are paid (the DEF 14A proxy, or +Form 20-F Item 6 for foreign issuers). For *external* 5%+ holders and institutions, see +`guide_holdings.md` instead. + +## Insider transactions (Forms 3, 4, 5) + +There is no `company.get_insiders()`. Query the ownership forms directly: + +```python +form4s = company.get_filings(form="4") # 3 = initial, 4 = changes, 5 = annual +latest = form4s.latest().obj() # parse the XML into a structured object +print(latest.insider_name, latest.position) +df = latest.to_dataframe() +``` + +Access individual trades via DataFrames or the activities helper — not by iterating +`non_derivative_transactions` (which isn't exposed as a standard list): + +```python +df_market = latest.market_trades # open-market buys / sells +df_options = latest.option_exercises +for act in latest.get_transaction_activities(): + print(act.transaction_type, act.code, act.shares, act.price_per_share) + # transaction codes: P = purchase, S = sale, M = option exercise, F = tax withholding +``` + +## Executive compensation — DEF 14A (domestic filers) + +US filers disclose executive compensation in the annual proxy (DEF 14A) and incorporate it +by reference into the 10-K rather than printing it there — so look in the proxy, not the +10-K. A proxy is a free-form filing: `filing.obj()` is a `ProxyStatement` with no item +codes, so you can't pull a section by code the way you can from a 10-K. Download it, read +the table of contents in its first ~100 lines to see its sections, then grep the body for +the part you want. + +```python +proxy = company.get_filings(form="DEF 14A").latest() +text = proxy.markdown() # save to the cache, then read its ToC + grep +``` + +The compensation disclosures (the Summary Compensation Table and the rest) are mandated by +Regulation S-K Item 402 — so a proxy's contents are predictable from that rule. Which tables +and narrative you pull, and what you make of them, is the caller's call. See +`guide_filings.md` for the item-addressable-vs-free-form distinction in general. + +## Foreign private issuers + +FPIs are exempt from several US ownership and governance rules, which changes *where* the +data is: + +- **Section 16 exemption.** FPIs (and Canadian MJDS filers) don't file Forms 3/4/5, so + insider-transaction data won't appear on EDGAR. Check the home-jurisdiction regulator + (e.g. SEDAR+ for Canada) instead. +- **No DEF 14A.** FPIs don't file US proxies. Compensation is in **Form 20-F Item 6.B** + and share ownership in **Item 6.E**. Many FPIs disclose pay only in aggregate unless + their home-country rules require individual figures. + +> **20-F Item 7.A boundary trap.** When slicing "Item 7.A Major Shareholders" by text, +> note the full Item 7 title is "Major Shareholders **and Related Party Transactions**." +> Using "Related Party Transactions" as the end boundary truncates early, because it also +> appears in the title — terminate the slice on "Item 8" instead. diff --git a/sec-edgar-skill/requirements.txt b/skills/sec-edgar-skill/requirements.txt similarity index 100% rename from sec-edgar-skill/requirements.txt rename to skills/sec-edgar-skill/requirements.txt diff --git a/sec-edgar-skill/scripts/_common.py b/skills/sec-edgar-skill/scripts/_common.py similarity index 100% rename from sec-edgar-skill/scripts/_common.py rename to skills/sec-edgar-skill/scripts/_common.py diff --git a/sec-edgar-skill/scripts/fetch_13f_holders.py b/skills/sec-edgar-skill/scripts/fetch_13f_holders.py similarity index 100% rename from sec-edgar-skill/scripts/fetch_13f_holders.py rename to skills/sec-edgar-skill/scripts/fetch_13f_holders.py diff --git a/sec-edgar-skill/scripts/fetch_filing.py b/skills/sec-edgar-skill/scripts/fetch_filing.py similarity index 100% rename from sec-edgar-skill/scripts/fetch_filing.py rename to skills/sec-edgar-skill/scripts/fetch_filing.py diff --git a/sec-edgar-skill/scripts/fetch_filings.py b/skills/sec-edgar-skill/scripts/fetch_filings.py similarity index 100% rename from sec-edgar-skill/scripts/fetch_filings.py rename to skills/sec-edgar-skill/scripts/fetch_filings.py diff --git a/sec-edgar-skill/scripts/fetch_insider_trades.py b/skills/sec-edgar-skill/scripts/fetch_insider_trades.py similarity index 100% rename from sec-edgar-skill/scripts/fetch_insider_trades.py rename to skills/sec-edgar-skill/scripts/fetch_insider_trades.py diff --git a/sec-edgar-skill/scripts/list_headings.py b/skills/sec-edgar-skill/scripts/list_headings.py similarity index 100% rename from sec-edgar-skill/scripts/list_headings.py rename to skills/sec-edgar-skill/scripts/list_headings.py diff --git a/sec-edgar-skill/scripts/orient.py b/skills/sec-edgar-skill/scripts/orient.py similarity index 100% rename from sec-edgar-skill/scripts/orient.py rename to skills/sec-edgar-skill/scripts/orient.py diff --git a/sec-edgar-skill/scripts/parse_financials.py b/skills/sec-edgar-skill/scripts/parse_financials.py similarity index 100% rename from sec-edgar-skill/scripts/parse_financials.py rename to skills/sec-edgar-skill/scripts/parse_financials.py diff --git a/sec-edgar-skill/scripts/test_setup.py b/skills/sec-edgar-skill/scripts/test_setup.py similarity index 100% rename from sec-edgar-skill/scripts/test_setup.py rename to skills/sec-edgar-skill/scripts/test_setup.py diff --git a/signal-sweep/.gitignore b/skills/signal-sweep/.gitignore similarity index 100% rename from signal-sweep/.gitignore rename to skills/signal-sweep/.gitignore diff --git a/signal-sweep/README.md b/skills/signal-sweep/README.md similarity index 91% rename from signal-sweep/README.md rename to skills/signal-sweep/README.md index c6ac064..60b54bd 100644 --- a/signal-sweep/README.md +++ b/skills/signal-sweep/README.md @@ -41,7 +41,7 @@ See [SKILL.md](SKILL.md) for invocation details and flags. ``` 2. **Set `EDGAR_IDENTITY`** — required for insider, theme, and conference scans (see - [repo-level setup](../README.md#setup)). + [profile setup](../../README.md#one-time-runtime-setup)). 3. Market screens (`scan_market.py`) use Yahoo Finance only and need no identity. ## Screen customization @@ -58,11 +58,11 @@ Discord, and uploads the Markdown output as a build artifact (90-day retention). **Required secrets:** -- `EDGAR_IDENTITY` — your SEC identity (see [repo-level setup](../README.md#setup)) +- `EDGAR_IDENTITY` — your SEC identity (see [profile setup](../../README.md#one-time-runtime-setup)) - `DISCORD_WEBHOOK_URL` — (optional) Discord webhook for posting alerts The workflow also supports `workflow_dispatch` for manual runs with custom date, lookback, and z-score threshold inputs. --- -Part of the [us-market-research-skills](../README.md) stack. +Part of the [SecStack skills](../README.md) collection. diff --git a/signal-sweep/SKILL.md b/skills/signal-sweep/SKILL.md similarity index 100% rename from signal-sweep/SKILL.md rename to skills/signal-sweep/SKILL.md diff --git a/signal-sweep/docs/conferences-autoresearch.md b/skills/signal-sweep/docs/conferences-autoresearch.md similarity index 96% rename from signal-sweep/docs/conferences-autoresearch.md rename to skills/signal-sweep/docs/conferences-autoresearch.md index 47396bb..fcc3a85 100644 --- a/signal-sweep/docs/conferences-autoresearch.md +++ b/skills/signal-sweep/docs/conferences-autoresearch.md @@ -78,17 +78,29 @@ non-excluded occurrence. Current exclusion list: ```python -["conference call", "conference call and webcast", - "exclusive forum", "forum selection", "alternative forum"] +[ + "conference call", + "conference call and webcast", + "exclusive forum", + "forum selection", + "alternative forum", +] ``` **2b. Attendance verb check** (`_has_attendance_verb`): Require at least one regex pattern to match: ```python -["will present", "presenting at", "participate in", - "scheduled to present", "speak at", "participation at", - "will attend", "will be attending"] +[ + "will present", + "presenting at", + "participate in", + "scheduled to present", + "speak at", + "participation at", + "will attend", + "will be attending", +] ``` `"will attend"` was added after a live test showed the AGA Financial Forum filing @@ -187,7 +199,7 @@ A template is in the `jules-autoresearch` skill at ```bash python /scripts/autoresearch.py \ --source "sources/github/eggmasonvalue/secstack" \ - --eval-script "signal-sweep/scripts/eval_harness.py" \ + --eval-script "skills/signal-sweep/scripts/eval_harness.py" \ --params '{ "exclusions": ["conference call", "conference call and webcast", "exclusive forum", "forum selection", "alternative forum"], diff --git a/signal-sweep/docs/flip_buy_difficulty_analysis.md b/skills/signal-sweep/docs/flip_buy_difficulty_analysis.md similarity index 91% rename from signal-sweep/docs/flip_buy_difficulty_analysis.md rename to skills/signal-sweep/docs/flip_buy_difficulty_analysis.md index 75f0dff..1abdc56 100644 --- a/signal-sweep/docs/flip_buy_difficulty_analysis.md +++ b/skills/signal-sweep/docs/flip_buy_difficulty_analysis.md @@ -22,7 +22,7 @@ In insider-activity analysis: ## 2. Current Architecture vs. Flip-Buy Requirements -### Current Flow in [scan_insiders.py](file:///D:/Misc2/06_backups/us-market-research-skills/signal-sweep/scripts/scan_insiders.py) +### Current Flow in [scan_insiders.py](../scripts/scan_insiders.py) 1. Fetches the daily bulk Form 4 index for the lookback window (e.g., last 5 trading days). 2. Filters to the market-cap universe ($50M–$10B) via `_common.in_universe`. @@ -108,6 +108,7 @@ import json from pathlib import Path from datetime import datetime, timedelta + def load_ticker_history(ticker: str, cache_dir: Path) -> list[dict]: """Load cached insider transactions for a ticker.""" cache_path = cache_dir / "insiders" / f"{ticker}_txns.json" @@ -118,34 +119,36 @@ def load_ticker_history(ticker: str, cache_dir: Path) -> list[dict]: return [] return [] + def save_ticker_history(ticker: str, txns: list[dict], cache_dir: Path) -> None: """Save ticker history to disk.""" cache_path = cache_dir / "insiders" / f"{ticker}_txns.json" cache_path.parent.mkdir(parents=True, exist_ok=True) cache_path.write_text(json.dumps(txns, indent=2), encoding="utf-8") + def update_ticker_history(ticker: str, cache_dir: Path) -> list[dict]: """Incremental fetch of all Form 4 transactions (P and S) over 1 year.""" from edgar import Company - + txns = load_ticker_history(ticker, cache_dir) last_date = max([t["date"] for t in txns]) if txns else None - + # Define start date (1 year lookback) start_dt = datetime.now() - timedelta(days=365) start_str = start_dt.strftime("%Y-%m-%d") - + # If we have cached transactions, start from the latest cached date to prevent re-fetching if last_date and last_date > start_str: fetch_start = last_date else: fetch_start = start_str - + date_range = f"{fetch_start}:{datetime.now().strftime('%Y-%m-%d')}" - + company = Company(ticker) filings = company.get_filings(form="4", date=date_range) - + new_txns = [] if filings: for filing in filings: @@ -156,18 +159,21 @@ def update_ticker_history(ticker: str, cache_dir: Path) -> list[dict]: for _, row in df.iterrows(): code = row.get("Code", "") if code in ("P", "S"): - new_txns.append({ - "date": filing.filing_date, - "insider": row.get("Insider") or obj.insider_name, - "role": row.get("Position") or getattr(obj, "position", "Unknown"), - "code": code, - "shares": row.get("Shares", 0), - "price": row.get("Price", 0), - "remaining": row.get("Remaining Shares") - }) + new_txns.append( + { + "date": filing.filing_date, + "insider": row.get("Insider") or obj.insider_name, + "role": row.get("Position") + or getattr(obj, "position", "Unknown"), + "code": code, + "shares": row.get("Shares", 0), + "price": row.get("Price", 0), + "remaining": row.get("Remaining Shares"), + } + ) except Exception: continue - + # Merge and deduplicate seen = set() merged = [] @@ -177,27 +183,31 @@ def update_ticker_history(ticker: str, cache_dir: Path) -> list[dict]: if key not in seen: seen.add(key) merged.append(t) - + merged.sort(key=lambda x: x["date"]) save_ticker_history(ticker, merged, cache_dir) return merged -def check_flip_buy(ticker: str, purchase_insider: str, purchase_date: str, txns: list[dict], min_sells: int = 2) -> bool: + +def check_flip_buy( + ticker: str, purchase_insider: str, purchase_date: str, txns: list[dict], min_sells: int = 2 +) -> bool: """Check if the purchase was preceded by a series of sells by this insider.""" + # Normalize name for comparison def norm(name): return "".join(name.upper().split()) - + insider_norm = norm(purchase_insider) - + # Filter and sort prior transactions prior_txns = [] for t in txns: if t["date"] < purchase_date and norm(t["insider"]) == insider_norm: prior_txns.append(t) - - prior_txns.sort(key=lambda x: x["date"], reverse=True) # newest first - + + prior_txns.sort(key=lambda x: x["date"], reverse=True) # newest first + sells_count = 0 for t in prior_txns: if t["code"] == "S": @@ -205,7 +215,7 @@ def check_flip_buy(ticker: str, purchase_insider: str, purchase_date: str, txns: elif t["code"] == "P": # An intermediate purchase breaks the "flip" sequence break - + return sells_count >= min_sells ``` diff --git a/signal-sweep/references/guide_screens.md b/skills/signal-sweep/references/guide_screens.md similarity index 100% rename from signal-sweep/references/guide_screens.md rename to skills/signal-sweep/references/guide_screens.md diff --git a/signal-sweep/requirements.txt b/skills/signal-sweep/requirements.txt similarity index 100% rename from signal-sweep/requirements.txt rename to skills/signal-sweep/requirements.txt diff --git a/signal-sweep/screens.json b/skills/signal-sweep/screens.json similarity index 100% rename from signal-sweep/screens.json rename to skills/signal-sweep/screens.json diff --git a/signal-sweep/scripts/_common.py b/skills/signal-sweep/scripts/_common.py similarity index 100% rename from signal-sweep/scripts/_common.py rename to skills/signal-sweep/scripts/_common.py diff --git a/signal-sweep/scripts/scan_conferences.py b/skills/signal-sweep/scripts/scan_conferences.py similarity index 100% rename from signal-sweep/scripts/scan_conferences.py rename to skills/signal-sweep/scripts/scan_conferences.py diff --git a/signal-sweep/scripts/scan_insiders.py b/skills/signal-sweep/scripts/scan_insiders.py similarity index 100% rename from signal-sweep/scripts/scan_insiders.py rename to skills/signal-sweep/scripts/scan_insiders.py diff --git a/signal-sweep/scripts/scan_market.py b/skills/signal-sweep/scripts/scan_market.py similarity index 100% rename from signal-sweep/scripts/scan_market.py rename to skills/signal-sweep/scripts/scan_market.py diff --git a/signal-sweep/scripts/search_themes.py b/skills/signal-sweep/scripts/search_themes.py similarity index 100% rename from signal-sweep/scripts/search_themes.py rename to skills/signal-sweep/scripts/search_themes.py From 1bdced839b8ea52ac82480c68b6c5db90f6059b9 Mon Sep 17 00:00:00 2001 From: eggmasonvalue Date: Tue, 11 Aug 2026 01:36:39 +0530 Subject: [PATCH 3/4] feat: update version to 0.2.0 in package.json and pyproject.toml --- package.json | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index c63e1e1..729715a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "eggmasonvalue-secstack", - "version": "0.1.0", + "version": "0.2.0", "private": true, "description": "US market research skills for Pi", "keywords": [ diff --git a/pyproject.toml b/pyproject.toml index edd66a5..095d1f3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "secstack" -version = "0.1.0" +version = "0.2.0" description = "US market research skills" requires-python = ">=3.11" dependencies = [] From 3c15082681809759a4357df978dff004e241af2a Mon Sep 17 00:00:00 2001 From: eggmasonvalue Date: Wed, 12 Aug 2026 20:26:05 +0000 Subject: [PATCH 4/4] feat: make SecStack a research-first Pi profile --- README.md | 16 +++-- SYSTEM.md | 1 + context/DECISIONS.md | 34 ++++++++++ context/MAP.md | 17 +++-- extensions/system-prompt.ts | 59 +++++++++++++++++ package.json | 3 + scripts/bootstrap.mjs | 126 +++++++++++++++++------------------- 7 files changed, 175 insertions(+), 81 deletions(-) create mode 100644 SYSTEM.md create mode 100644 extensions/system-prompt.ts diff --git a/README.md b/README.md index 2ae4ed1..b73e103 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ SecStack is an isolated [Pi](https://github.com/badlogic/pi-mono) profile for rigorous bottom-up research on US-listed companies. It bundles five composable skills, selected -Pi extensions and themes, `pi-subagent`, and Pi-managed `agent-browser`. +Pi extensions and themes, and Pi-managed `agent-browser`. The normal Pi profile is not modified. SecStack uses: @@ -28,9 +28,10 @@ tmp=$(mktemp -d) && git clone --depth 1 https://github.com/eggmasonvalue/secstac The bootstrap is safe to rerun. It installs the unpinned top-level Pi package sources, merges only SecStack-managed package entries and shell-path configuration into the -SecStack profile's `settings.json`, creates a profile-local Python environment, removes -old resource-directory links, and links the installed `pi-setup` `APPEND_SYSTEM.md` into -the SecStack profile. +SecStack profile's `settings.json` and creates a profile-local Python environment. It links the +profile's `SYSTEM.md` to the installed SecStack package, so `pi update --extensions` updates the +research-agent identity and prompt envelope. It does not install coding-task guidance or link +global `AGENTS.md` or `APPEND_SYSTEM.md` files into the profile. It does not overwrite the profile's `auth.json`, `models.json`, provider settings, model selections, UI preferences, sessions, or unrelated settings. @@ -101,15 +102,16 @@ The SecStack profile manages these as separate top-level Pi packages: - `git:github.com/eggmasonvalue/secstack` — this repository's five skills - `git:github.com/eggmasonvalue/pi-setup` — selected extensions and themes only -- `git:github.com/eggmasonvalue/pi-subagent` - `npm:agent-browser` The selected `pi-setup` resources are `btw`, `notify`, `session-context`, `tavily-web`, -`vibe-spinner`, and the `midnight-pastel`, `pastel-dark`, and `pastel-light` themes. +`vibe-spinner`, and the `midnight-pastel`, `pastel-dark`, and `pastel-light` themes. Its +coding-oriented skills, including `repo-nav` and `bootstrap-docs`, are excluded. Each independently managed source remains a top-level profile package so `pi update --extensions` can update it independently. Third-party resources are not copied -into this repository or bundled as nested dependencies. +into this repository or bundled as nested dependencies. Research runs in the primary Pi +context; the profile does not install or invoke sub-agents. ## Skills diff --git a/SYSTEM.md b/SYSTEM.md new file mode 100644 index 0000000..0adb7f8 --- /dev/null +++ b/SYSTEM.md @@ -0,0 +1 @@ +You are a bottom-up equity research agent for US-listed companies. diff --git a/context/DECISIONS.md b/context/DECISIONS.md index cb3e92f..ac94d94 100644 --- a/context/DECISIONS.md +++ b/context/DECISIONS.md @@ -17,6 +17,40 @@ Status: active | superseded by --- +## 2026-08-12 — Replace Pi's coding identity without duplicating tool or skill guidance + +Context: A profile-level `SYSTEM.md` replaces Pi's default prompt, which removes the +coding-agent identity as required but also suppresses Pi's dynamic tool snippets and guidelines. +A copied system file would also become stale after `pi update --extensions`. +Decision: Keep a one-line research identity in the SecStack package; bootstrap creates a relative +profile `SYSTEM.md` symlink to it. A package extension reconstructs Pi's prompt envelope from +live tool metadata, leaving all research procedure in the matching skills. +Tradeoff: The extension mirrors a small, stable part of Pi's prompt assembly, but preserves +active-tool changes automatically and avoids a second copy of tool or research guidance. +Status: active + +## 2026-08-12 — Keep the profile free of coding-task resources + +Context: The shared `pi-setup` package now includes global coding guidance and coding-oriented +skills, but SecStack is a dedicated research profile. +Decision: Load only SecStack's selected `pi-setup` extensions and themes. Do not link +`AGENTS.md` or `APPEND_SYSTEM.md`, and exclude `repo-nav` and `bootstrap-docs` by keeping the +package's skill filter empty. +Tradeoff: The profile does not inherit general coding workflow improvements, preserving a +smaller research-specific context and avoiding unrelated instructions. +Status: active + +## 2026-08-12 — Keep research in the primary Pi context + +Context: Sub-agent tools consume substantial context for their descriptors, can hide vital +research context, and introduce uncertain quality and cost tradeoffs when only weaker models +are available for delegation. +Decision: Do not install or invoke `pi-subagent`; the research workflow runs in the primary +Pi context. +Tradeoff: Research cannot be split across independent agents, but the full evidence trail and +model judgement stay together without an extra model-selection or context-budget dependency. +Status: active + ## 2025-06-21 — market-scout uses agent-browser instead of a direct HTTP client Context: Yahoo Finance transcript and market endpoints are unstable and diff --git a/context/MAP.md b/context/MAP.md index 8085612..64102c4 100644 --- a/context/MAP.md +++ b/context/MAP.md @@ -8,7 +8,9 @@ A Git Pi package containing five self-contained agent skills. The skills live un `skills/` so the root of the repository can focus on package/profile setup. ```text -package.json Pi package manifest for the five SecStack skills +package.json Pi package manifest for the SecStack prompt extension and five skills +SYSTEM.md Research-agent identity installed into the isolated profile +extensions/ Restores Pi's live tool prompt envelope after SYSTEM.md replacement scripts/bootstrap.mjs Isolated-profile bootstrap skills/ Skill collection and skill-level documentation signal-sweep/ Discovery — scan the universe, surface tickers @@ -28,14 +30,17 @@ The bootstrap configures the isolated profile at `~/.pi/secstack-agent`. Its `settings.json` owns separate top-level package entries for: - this SecStack package; -- the filtered `pi-setup` package; -- `pi-subagent`; and +- the filtered `pi-setup` package; and - `agent-browser`. Pi installs and updates each source independently. The bootstrap manages only those package -entries, the profile's Pi-managed shell path, the profile-local Python environment, and the -`APPEND_SYSTEM.md` link. Authentication, model selection, provider configuration, sessions, -and unrelated settings remain profile-local and untouched. +entries, the profile's Pi-managed shell path, the profile-local Python environment, and a +relative `SYSTEM.md` symlink into the installed SecStack package. The prompt extension restores +Pi's live tool snippets and guidelines after `SYSTEM.md` replaces Pi's coding-agent identity. +Coding-task guidance files are not linked, and the filtered `pi-setup` package contributes no +skills, including `repo-nav` and `bootstrap-docs`. Research runs in the primary Pi context; +no sub-agent package is installed. Authentication, model selection, provider configuration, +sessions, and unrelated settings remain profile-local and untouched. ## Skill internals diff --git a/extensions/system-prompt.ts b/extensions/system-prompt.ts new file mode 100644 index 0000000..8e3769b --- /dev/null +++ b/extensions/system-prompt.ts @@ -0,0 +1,59 @@ +import { + type BuildSystemPromptOptions, + type ExtensionAPI, +} from "@earendil-works/pi-coding-agent"; + +const DEFAULT_TOOLS = ["read", "bash", "edit", "write"]; + +function formatToolEnvelope(options: BuildSystemPromptOptions): string { + const tools = options.selectedTools ?? DEFAULT_TOOLS; + const snippets = Object.entries(options.toolSnippets ?? {}); + const toolsList = + snippets.length > 0 + ? snippets.map(([name, snippet]) => `- ${name}: ${snippet}`).join("\n") + : "(none)"; + + const guidelines: string[] = []; + const seen = new Set(); + const addGuideline = (guideline: string) => { + if (!seen.has(guideline)) { + seen.add(guideline); + guidelines.push(guideline); + } + }; + + if ( + tools.includes("bash") && + !tools.includes("grep") && + !tools.includes("find") && + !tools.includes("ls") + ) { + addGuideline("Use bash for file operations like ls, rg, find"); + } + for (const guideline of options.promptGuidelines ?? []) { + const normalized = guideline.trim(); + if (normalized) addGuideline(normalized); + } + addGuideline("Be concise in your responses"); + addGuideline("Show file paths clearly when working with files"); + + return `Available tools: +${toolsList} + +In addition to the tools above, you may have access to other custom tools depending on the project. + +Guidelines: +${guidelines.map((guideline) => `- ${guideline}`).join("\n")}`; +} + +export default function systemPromptExtension(pi: ExtensionAPI) { + pi.on("before_agent_start", (event) => { + const customPrompt = event.systemPromptOptions.customPrompt; + if (!customPrompt || !event.systemPrompt.startsWith(customPrompt)) return; + + const remainder = event.systemPrompt.slice(customPrompt.length); + return { + systemPrompt: `${customPrompt}\n\n${formatToolEnvelope(event.systemPromptOptions)}${remainder}`, + }; + }); +} diff --git a/package.json b/package.json index 729715a..ffa49bc 100644 --- a/package.json +++ b/package.json @@ -7,6 +7,9 @@ "pi-package" ], "pi": { + "extensions": [ + "./extensions" + ], "skills": [ "./skills/signal-sweep", "./skills/sec-edgar-skill", diff --git a/scripts/bootstrap.mjs b/scripts/bootstrap.mjs index 4b2931a..730d843 100644 --- a/scripts/bootstrap.mjs +++ b/scripts/bootstrap.mjs @@ -4,7 +4,7 @@ * SecStack profile. * * This script deliberately changes only the SecStack profile's package list, - * shell command prefix, APPEND_SYSTEM.md link, and optional Bash launcher. + * shell command prefix, and optional Bash launcher. */ import { execFileSync } from "node:child_process"; import { @@ -12,6 +12,7 @@ import { lstatSync, mkdirSync, readFileSync, + readlinkSync, renameSync, rmSync, symlinkSync, @@ -20,25 +21,19 @@ import { import { createInterface } from "node:readline/promises"; import { stdin as input, stdout as output } from "node:process"; import { homedir } from "node:os"; -import { dirname, join, resolve } from "node:path"; +import { dirname, join, relative, resolve } from "node:path"; const agentDir = resolve(join(homedir(), ".pi", "secstack-agent")); const settingsPath = join(agentDir, "settings.json"); const npmBin = join(agentDir, "npm", "node_modules", ".bin"); const venvDir = join(agentDir, ".venv"); -const appendPath = join(agentDir, "APPEND_SYSTEM.md"); +const systemPromptPath = join(agentDir, "SYSTEM.md"); const bashrcPath = join(homedir(), ".bashrc"); const secstackSource = "git:github.com/eggmasonvalue/secstack"; const piSetupSource = "git:github.com/eggmasonvalue/pi-setup"; -const subagentSource = "git:github.com/eggmasonvalue/pi-subagent"; const agentBrowserSource = "npm:agent-browser"; -const managedSources = [ - secstackSource, - piSetupSource, - subagentSource, - agentBrowserSource, -]; +const managedSources = [secstackSource, piSetupSource, agentBrowserSource]; const desiredPackages = [ secstackSource, @@ -59,7 +54,6 @@ const desiredPackages = [ "themes/pastel-light.json", ], }, - subagentSource, agentBrowserSource, ]; @@ -118,7 +112,14 @@ function venvPython() { } function installedSecStackPath(...parts) { - return join(agentDir, "git", "github.com", "eggmasonvalue", "secstack", ...parts); + return join( + agentDir, + "git", + "github.com", + "eggmasonvalue", + "secstack", + ...parts, + ); } function mergeSettings() { @@ -140,9 +141,14 @@ function mergeSettings() { // as well as Bash on macOS/Linux. const pathCommand = 'export PATH="$HOME/.pi/secstack-agent/.venv/Scripts:$HOME/.pi/secstack-agent/.venv/bin:$HOME/.pi/secstack-agent/npm/node_modules/.bin:$PATH"'; - const prefix = typeof settings.shellCommandPrefix === "string" ? settings.shellCommandPrefix : ""; + const prefix = + typeof settings.shellCommandPrefix === "string" + ? settings.shellCommandPrefix + : ""; if (!prefix.includes(managedPathMarker)) { - settings.shellCommandPrefix = prefix ? `${prefix}\n${pathCommand}` : pathCommand; + settings.shellCommandPrefix = prefix + ? `${prefix}\n${pathCommand}` + : pathCommand; } const temp = join(agentDir, `.settings.${process.pid}.tmp`); @@ -170,61 +176,39 @@ function ensurePythonEnvironment() { } } -function removeOldResourceLink(name) { - const path = join(agentDir, name); - if (!existsSync(path)) return; - try { - if (lstatSync(path).isSymbolicLink()) { - rmSync(path, { recursive: true, force: true }); - console.log(`Removed old resource link: ${path}`); - } else { - console.warn(`Not removing non-link resource directory: ${path}`); - } - } catch (error) { - console.warn(`Could not inspect ${path}: ${error.message}`); - } -} - -function linkAppendSystem() { - const installed = join( - agentDir, - "git", - "github.com", - "eggmasonvalue", - "pi-setup", - "APPEND_SYSTEM.md", - ); +function linkSystemPrompt() { + const installed = installedSecStackPath("SYSTEM.md"); if (!existsSync(installed)) { - throw new Error(`Installed pi-setup package is missing APPEND_SYSTEM.md: ${installed}`); + throw new Error( + `Installed SecStack package is missing SYSTEM.md: ${installed}`, + ); } - let existing; + const target = relative(agentDir, installed); try { - existing = lstatSync(appendPath); - } catch (error) { - if (error.code !== "ENOENT") throw error; - } - - if (existing?.isSymbolicLink()) { - rmSync(appendPath, { force: true }); - } else if (existing) { - const backup = `${appendPath}.local-backup`; - if (!existsSync(backup)) { - renameSync(appendPath, backup); - console.warn(`Preserved the previous regular file as ${backup}`); + if (lstatSync(systemPromptPath).isSymbolicLink()) { + if (readlinkSync(systemPromptPath) === target) return; + rmSync(systemPromptPath, { force: true }); } else { - throw new Error( - `A regular ${appendPath} already exists and ${backup} is also present; refusing to overwrite either file.`, - ); + const backup = `${systemPromptPath}.local-backup`; + if (existsSync(backup)) { + throw new Error( + `A regular ${systemPromptPath} and its backup ${backup} already exist; refusing to overwrite either file.`, + ); + } + renameSync(systemPromptPath, backup); + console.warn(`Preserved the previous regular file as ${backup}`); } + } catch (error) { + if (error.code !== "ENOENT") throw error; } try { - symlinkSync(installed, appendPath, "file"); - console.log(`Linked ${appendPath} -> ${installed}`); + symlinkSync(target, systemPromptPath, "file"); + console.log(`Linked ${systemPromptPath} -> ${target}`); } catch (error) { throw new Error( - `Could not create the APPEND_SYSTEM.md symlink. Enable Windows Developer Mode or grant symlink privileges, then rerun bootstrap. Original error: ${error.message}`, + `Could not create the SYSTEM.md symlink. Enable Windows Developer Mode or grant symlink privileges, then rerun bootstrap. Original error: ${error.message}`, ); } } @@ -249,7 +233,9 @@ ${launcherEnd}`; function installLauncher() { mkdirSync(dirname(bashrcPath), { recursive: true }); - const existing = existsSync(bashrcPath) ? readFileSync(bashrcPath, "utf8") : ""; + const existing = existsSync(bashrcPath) + ? readFileSync(bashrcPath, "utf8") + : ""; const block = launcherBlock(); const pattern = new RegExp( `${escapeRegExp(launcherStart)}[\\s\\S]*?${escapeRegExp(launcherEnd)}\\n?`, @@ -270,13 +256,17 @@ function escapeRegExp(value) { async function offerLauncher() { if (!process.stdin.isTTY || !process.stdout.isTTY) { - console.log(`Launcher not offered because bootstrap is not running in an interactive Bash terminal.`); + console.log( + `Launcher not offered because bootstrap is not running in an interactive Bash terminal.`, + ); return; } const rl = createInterface({ input, output }); try { - const answer = (await rl.question("Create the secstack-pi Bash launcher? [Y/n] ")) + const answer = ( + await rl.question("Create the secstack-pi Bash launcher? [Y/n] ") + ) .trim() .toLowerCase(); if (answer === "" || answer === "y" || answer === "yes") { @@ -294,20 +284,20 @@ async function main() { for (const source of managedSources) { runPi(["install", source]); } - mergeSettings(); + linkSystemPrompt(); mkdirSync(npmBin, { recursive: true }); ensurePythonEnvironment(); - for (const name of ["extensions", "skills", "prompts", "themes"]) { - removeOldResourceLink(name); - } - linkAppendSystem(); await offerLauncher(); console.log("\nSecStack Pi bootstrap complete."); console.log("Update everything Pi-managed with:"); - console.log(' PI_CODING_AGENT_DIR="$HOME/.pi/secstack-agent" pi update --extensions'); - console.log("One-time browser setup (if not already done): agent-browser install"); + console.log( + ' PI_CODING_AGENT_DIR="$HOME/.pi/secstack-agent" pi update --extensions', + ); + console.log( + "One-time browser setup (if not already done): agent-browser install", + ); console.log("Verify from the SecStack profile: agent-browser --version"); }