diff --git a/.claude/commands/invest-retro.md b/.claude/commands/invest-retro.md new file mode 100644 index 00000000..54e68d1c --- /dev/null +++ b/.claude/commands/invest-retro.md @@ -0,0 +1,20 @@ +# /invest-retro + +Outcome-grounded retrospective — the learning loop. Reads trajectory records (thesis→action→outcome→lesson), scores decisions against realized outcomes, and distills lessons into per-agent `calibration_notes` in `engine/agents/catalog.json`. + +## Usage + +``` +/invest-retro [--since YYYY-WW] [--thesis ] +``` + +## Contract + +Load `verticals/investment-intelligence/SKILL.md` and execute `verticals/investment-intelligence/engine/commands/retrospective.md` under it: + +- R5 non-advisory clause opens the output. +- Every acted-on recommendation since the last retro must have a trajectory record (`engine/schemas/trajectory.schema.json`) — missing records are flagged as pipeline drift (SOUL test 4). +- Lessons update agent calibration notes; systemic lessons propose skill/command edits as diffs for human review (never silent self-modification). +- Cross-check the trade-gate audit JSONL: every executed order must trace to an approved intent. Discrepancy = escalate, do not rationalize. + +**Built on SIP** — invest-retro wrapper · v0.1 diff --git a/.claude/commands/invest-snapshot.md b/.claude/commands/invest-snapshot.md new file mode 100644 index 00000000..7e42f931 --- /dev/null +++ b/.claude/commands/invest-snapshot.md @@ -0,0 +1,20 @@ +# /invest-snapshot + +Compose the cross-asset portfolio snapshot — ground truth before any decision. Reads (as available): Ghostfolio API (T0), DPI ledger state (`/wealth-dpi`), operator instance data (private, aggregate-only off-T0), Crypto IS House of Allocation outputs. + +## Usage + +``` +/invest-snapshot [--aggregate] # --aggregate is forced automatically off-T0 +``` + +## Contract + +Load `verticals/investment-intelligence/SKILL.md` and execute `verticals/investment-intelligence/engine/commands/portfolio-snapshot.md` under it: + +- R5 non-advisory clause opens the output. +- Output validates against `engine/schemas/portfolio-snapshot.schema.json`. +- **Data classification enforced:** real balances/positions are T0-only per `ROUTING.md`. On T1/T2 models, emit percentage weights and band-labels, never raw amounts. +- A snapshot is not a strategy session — no recommendations from this command. + +**Built on SIP** — invest-snapshot wrapper · v0.1 diff --git a/.claude/commands/invest-strategy.md b/.claude/commands/invest-strategy.md new file mode 100644 index 00000000..2622ca4c --- /dev/null +++ b/.claude/commands/invest-strategy.md @@ -0,0 +1,21 @@ +# /invest-strategy + +Run the Investment Intelligence weekly strategy session — the full pipeline (blind-parallel analysis → debate → risk gate → synthesis) producing a draft session document for human verdict. Namespaced `invest-*` to avoid collision with operator-instance commands. + +## Usage + +``` +/invest-strategy [--week YYYY-WW] [--mode weekly|rebalance] +``` + +## Contract + +Load `verticals/investment-intelligence/SKILL.md` (vertical-tier rules fire) and execute `verticals/investment-intelligence/engine/commands/weekly-strategy.md` under them: + +- R5 non-advisory clause opens the output. Non-waivable. +- Pipeline order structural: analysis agents run blind-parallel per `engine/agents/catalog.json`; risk vetoes size, never direction; only the portfolio-manager emits TradeIntents. +- Any non-DCA TradeIntent terminates as a pending-approval object via the trade-gate MCP (`mcp/trade-gate/`) — never a fabricated fill. +- Real balances/positions are T0 data per `ROUTING.md`: on non-local models, work from aggregates. +- Session artifact validates against `engine/schemas/strategy-session.schema.json`; acted-on recommendations get trajectory records. + +**Built on SIP** — invest-strategy wrapper · v0.1 diff --git a/.claude/commands/invest-thesis-debate.md b/.claude/commands/invest-thesis-debate.md new file mode 100644 index 00000000..02a841b9 --- /dev/null +++ b/.claude/commands/invest-thesis-debate.md @@ -0,0 +1,20 @@ +# /invest-thesis-debate + +Bull/bear adversarial debate on a single investment thesis (TradingAgents-pattern, provenance: `verticals/investment-intelligence/docs/absorption/tradingagents.md`). Pressure-test before any new position thesis enters the portfolio. + +## Usage + +``` +/invest-thesis-debate +``` + +## Contract + +Load `verticals/investment-intelligence/SKILL.md` and execute `verticals/investment-intelligence/engine/commands/thesis-debate.md` under it: + +- R5 non-advisory clause opens the output. +- Bull and bear cases argued from independently-formed analysis-layer stances (blind-parallel first, collide second). +- Verdict is a thesis-quality assessment (validated against `engine/schemas/thesis.schema.json`), never a buy/sell instruction. +- Optional persona overlays (value/contrarian/quality lenses per `AGENTS.md` § persona overlays) increase debate diversity; they never bypass the risk layer. + +**Built on SIP** — invest-thesis-debate wrapper · v0.1 diff --git a/.claude/commands/siswarm.md b/.claude/commands/siswarm.md new file mode 100644 index 00000000..2dfc5714 --- /dev/null +++ b/.claude/commands/siswarm.md @@ -0,0 +1,61 @@ +# /siswarm — Investment-intelligence council (Sonnet + Opus swarm) + +Run the 10-agent investment-intelligence council locally, with per-agent model +routing threaded from `verticals/investment-intelligence/engine/agents/catalog.json`. +This is the executable counterpart to the `/si`//`/so` CLI-lane routers: `/si` +decides *which* CLI to use; `/siswarm` actually *runs* a Sonnet+Opus council. + +## Usage + +``` +starlight swarm run "" # dry-run plan (default) +starlight swarm run "" --live # execute via headless claude +starlight swarm run "" --live --only technical # single-agent cheap demo +starlight swarm run "" --live --concurrency 3 --timeout 240000 +``` + +Dry-run prints the compiled plan (agent · layer · model · prompt size) and passes +**no model to any CLI**. `--live` executes. + +## The three phases (from the catalog handoff protocol) + +| Phase | Agents | Models | Prompt sees | +|---|---|---|---| +| 1 — analysis (blind-parallel) | macro-risk · crypto-dca · defi-yield · fundamentals · technical | Sonnet ×4, **Haiku** (technical) | operator context only — **no peer output** | +| 2 — risk | risk-manager · tax-optimizer · regulatory-risk | Sonnet ×3 | context + the full analysis digest | +| 3 — synthesis | portfolio-manager · chief-of-staff | **Opus** (PM), Sonnet | context + the full debate (analysis + risk) | + +The `researcher` (cross-cutting) is excluded from v1 — it is a lookup helper, not +a debate seat. Risk agents run parallel *within* phase 2 (each only needs the +analysis output, not each other's) — an intentional refinement over the catalog's +`step_2_sequential`. + +## How the model split works + +The runner (`src/swarm.ts::defaultClaudeRunner`) spawns `claude -p --model `. +Each task's `model` is the agent's `recommended_model` from the catalog, so +analysis/risk run Sonnet-class, `technical` runs Haiku, and `portfolio-manager` +runs Opus. Override the binary with `STARLIGHT_CLAUDE_BIN`. + +## Boundary (non-negotiable) + +The council produces **analysis + a pending decision brief** — nothing else. Every +prompt carries the R5 non-advisory clause and the no-execution line +(`tools_denied` includes `execution-platforms` + `credential-stores`). **Trades +execute only through the local trade-gate MCP** (`verticals/investment-intelligence/mcp/trade-gate`) +**with a human approval token.** The swarm has no path to a broker. + +## Audit + +Each `--live` run appends a JSONL record (context, timestamps, per-phase agent +pass/fail) to `private/voice-operator/logs/swarm-council.jsonl` next to the +existing swarm audit log. + +## Hosted twin + +The same catalog drives the Vercel-hosted council on frankx.ai +(`/admin/swarm` + `app/api/swarm/run` via the Vercel AI Gateway) — that surface +also emits pending decisions only. See `docs/architecture/sis-swarm-hosted.md` +in the FrankX repo. + +**Built on SIP** — /siswarm · investment-intelligence council · v0.1 diff --git a/REGISTRY.md b/REGISTRY.md index 07f83631..e4efb43c 100644 --- a/REGISTRY.md +++ b/REGISTRY.md @@ -85,6 +85,22 @@ attestation: built_on_sip - **requires:** none - **license:** MIT +### trade-gate-mcp +- **sip_version:** 1.1.1 +- **owner:** Frank Riemer / Starlight Holding BV +- **status:** active (v0.1 — paper broker only; live adapters NOT_WIRED by design) +- **repo:** `frankxai/Starlight-Intelligence-System` (`verticals/investment-intelligence/mcp/trade-gate/`) +- **endpoint:** stdio (local) +- **provides:** + - `propose_trade` — TradeIntent in → verdict: auto-approved (DCA whitelist, capped) | pending-approval | rejected + - `request_approval` — human issues a single-use approval token for a pending intent + - `list_pending` — pending-approval queue + - `execute_approved` — approved intent → broker adapter (paper default; live throws NOT_WIRED in-repo) + - `read_audit` — append-only JSONL audit trail +- **requires:** none (broker MCPs — Alpaca / IBKR / Coinbase — are operator-wired downstream, never bundled) +- **license:** MIT +- **note:** Fail-closed by doctrine and by test (`e2e.test.ts` red case: live execute without approval must throw). Every tool output carries the R5 non-advisory footer. Pattern lineage: `payment-intelligence-system/mcp`. Board record: `docs/boards/2026-07-02-investment-intelligence-vertical-spawn.md`. + ## Claw Registry SIS Claws are installable operational units built on this substrate. Each Claw declares its permission surface, skill requirements, and MCP dependencies in a `CLAW.md` contract. diff --git a/VERTICALS.md b/VERTICALS.md index 8abd2d2b..c1555776 100644 --- a/VERTICALS.md +++ b/VERTICALS.md @@ -196,7 +196,7 @@ Domain sub-stacks are forkable reference verticals. Unlike the sovereign vertica - **Class:** sovereign domain sub-stack (reference vertical) — third reference, validates the **Houses-as-sub-systems** decomposition primitive AND first composition-instance under Wealth IS composition layer (per `STACK.md` § Composition Layer, declared 2026-05-17). - **Domain:** Cryptocurrency practiced as the architecture of sovereignty + protocol-mechanism literacy + cycle-position pattern recognition. Six Houses (On-Chain · Macro · DeFi · Sovereignty · Research · Allocation) decomposed as archetypal stances. - **Owner:** open reference (forkable by sovereign crypto practitioners). Composes under Wealth IS as first reference instance of Composition Layer substrate primitive. -- **Status:** `v0.1 proof-of-pattern — House of On-Chain scaffolded (5 commands + agent + skill + knowledge); 5 Houses gated on v0.1-proof-pass per Board R4 close-out`. Investment IS held until proof-pass. +- **Status:** `v0.2 full scaffold — all 6 Houses shipped` (R4 proof-of-pattern passed via House of On-Chain). Investment IS gate released — spawned 2026-07-02, see § Investment Intelligence below. - **Primary repo:** `verticals/crypto-intelligence/` in `frankxai/Starlight-Intelligence-System` (v0.2+ extraction target: `github.com/frankxai/crypto-intelligence-system` per Board open-question (c) export-hook landed via vertical-local ATTESTATIONS.md + MCP-shape in SKILL.md). - **Houses (v0.1):** `onchain` (scaffolded). **Houses (v0.2 gated):** `macro` · `defi` · `sov` · `res` · `alloc`. - **Canon:** declines (canon-free per substrate register). @@ -207,6 +207,20 @@ Domain sub-stacks are forkable reference verticals. Unlike the sovereign vertica - **Board verdict:** REVISE close-out 2026-05-17 — `docs/boards/2026-05-17-crypto-investment-spawn.md` (5 items closed same-session per v7.5.1 recovery precedent). - **Genius prerequisite gate:** CLOSED via `genius/profile-frankx.md` + `genius/freedom-path-frankx.md` (Path A in-repo corpus excavation, 2026-05-17). +### Investment Intelligence +- **Class:** sovereign domain sub-stack (reference vertical) — sibling of Crypto Intelligence under the Wealth IS ACL manifest (Wealth collapsed to ACL-only per 2026-06-16 falsifier default, recorded `docs/boards/2026-07-02-investment-intelligence-vertical-spawn.md`). +- **Domain:** Cross-asset capital allocation practiced as system, not impulse — research → debate → risk-gate → recommend → human-approve → execute (paper-first) → learn, across stocks/ETFs/crypto/DeFi + time/labor allocation via DPI framing. +- **Owner:** open reference (forkable by sovereign operators). Engine promoted (copied, not moved) from the private IIS operator instance — proven material, not fresh scaffold. +- **Status:** `v0.1 — engine promoted (11-agent catalog · 5 schemas · 4 commands · 3 adapters · 2 sanitized archetypes), trade-gate MCP shipped + tested; runtime tiers 1-2 gated on operator wiring per RUNBOOK.md`. +- **Primary repo:** `verticals/investment-intelligence/` in `frankxai/Starlight-Intelligence-System` (extraction target: `github.com/frankxai/investment-intelligence-system` per Board open-question (c) export-hook pattern — extraction is a copy, not a refactor). +- **Layers:** `analysis` (5 agents, blind-parallel) · `risk` (3 agents, veto-on-size-not-direction) · `synthesis` (portfolio-manager Opus + chief-of-staff) · cross-cutting `researcher`. +- **Canon:** declines (canon-free per substrate register); composes SIP + payment-intelligence-system doctrine (pattern-level). +- **Compounds:** trajectory ReasoningBank (thesis→action→outcome→lesson) + per-agent calibration + practitioners forking + attestation graph. +- **SIP commands:** `/invest-strategy` · `/invest-thesis-debate` · `/invest-snapshot` · `/invest-retro` (daily-5 completed by `/wealth-dpi`, R2 discipline declared at spawn). +- **MCP:** `mcp/trade-gate/` — fail-closed approve/execute (propose_trade · request_approval · list_pending · execute_approved · read_audit); paper-broker default, live adapters NOT_WIRED stubs, human token above DCA whitelist. +- **Non-waivable:** R5 non-advisory clause inline on every artifact; no broker credentials in repo; paper-first ladder; trajectory write-back. +- **Board verdict:** PROCEED — `docs/boards/2026-07-02-investment-intelligence-vertical-spawn.md`. + --- ## Registry rules diff --git a/docs/boards/2026-07-02-investment-intelligence-vertical-spawn.md b/docs/boards/2026-07-02-investment-intelligence-vertical-spawn.md new file mode 100644 index 00000000..529a0ad8 --- /dev/null +++ b/docs/boards/2026-07-02-investment-intelligence-vertical-spawn.md @@ -0,0 +1,76 @@ +--- +date: 2026-07-02 +convened: 2026-07-02 +command: /starlight-board +tier: substrate (VERTICALS.md + verticals/wealth/ ACL collapse + new vertical spawn) +proposal: Spawn verticals/investment-intelligence/ with the IIS engine; affirm Wealth IS collapse to ACL-only +verdict: PROCEED +--- + +# Starlight Board — Investment Intelligence vertical spawn + Wealth IS ACL affirmation + +**Convened:** 2026-07-02, under Frank's directive to review and consolidate the agentic wealth / investor / finance OS ("god mode on — review our agentic wealth OS…"). + +**Prior board references:** +- `docs/boards/2026-05-17-crypto-investment-spawn.md` — Investment IS held behind R4 proof-of-pattern gate; export-hook doctrine (open question (c)); R5 non-advisory clause. +- `docs/boards/2026-05-29-wealth-composition-falsifier.md` — falsifier deadline 2026-06-16; default outcome: collapse to ACL-only. **No explicit decision was recorded by deadline; the default fired.** + +--- + +## Context + +Three findings from the 2026-07-02 estate-wide review (full record: `docs/strategic/2026-07-02-wealth-os-architecture-review.md`): + +1. **The falsifier fired.** `verticals/wealth/commands/` was empty at 2026-06-16. Per the 2026-05-29 board's designed default, Wealth IS collapses to ACL-only. Yet `verticals/wealth/MEMORY.md` v0.3 (written 2026-06-19) still narrates the cross-asset commands as "declared" — an internal inconsistency this board resolves. +2. **The R4 gate is passed.** Crypto Intelligence shipped its proof-of-pattern (House of On-Chain) and subsequently the full 6-House scaffold (v0.2). The 2026-05-17 board held Investment IS "until v0.1-proof-pass" — that condition is met. +3. **The engine already exists.** `FrankX/iis/` (Investment Intelligence System) is a spec-complete finance decision swarm: 11 agents in 3 layers (5 analysis blind-parallel → 3 risk with veto-on-size-not-direction → portfolio-manager + chief-of-staff), explicit model routing, 5 JSON schemas including a ReasoningBank trajectory store, adapters (FRED / DefiLlama / OpenBB), human-gate above any non-DCA action, and a privacy boundary. It is unrecognized by the substrate and un-extractable by other practitioners while it lives only in a private repo. + +## Proposal under review + +1. **Spawn `verticals/investment-intelligence/`** with the IIS engine **copied** (not moved) from `FrankX/iis/`. FrankX keeps the private operator instance (real portfolio data, `private/` state); Starlight carries the canonical pattern substrate. +2. **Affirm the falsifier outcome:** `verticals/wealth/` becomes an ACL-only composition manifest — a declarative reference pointing at the operational surfaces (FrankX wealth-ops, `/wealth-dpi`, `verticals/crypto-intelligence/`, `verticals/investment-intelligence/`) — with `MEMORY.md` corrected to record the collapse honestly. Re-declarable later per the falsifier's design. +3. **Add a fail-closed trade-gate MCP** inside the new vertical, mirroring `payment-intelligence-system/mcp` (approval tokens, caps, append-only JSONL audit, paper-broker default, live-broker adapters stubbed). Trading is a distinct risk domain from payments; the payments repo's charter ("no autonomous money movement, ever" — for payments) is respected by reusing the code shape, not the repo. +4. **Persona surfaces:** zero-terminal wealth-guardian template in `integrations/starter-packs/friend-starter/` (protected-executor persona); premium `investor-os-pack` in `agentic-business-os`; public OSS extraction to `github.com/frankxai/investment-intelligence-system` deferred to a Frank-local runbook (repo does not exist yet; out of this session's scope). + +## Invariants (non-waivable, carried from prior boards) + +- **R5 non-advisory clause** inline on every investment output: *"This is system architecture, not financial / investment / tax advice. Outputs frame decisions; jurisdiction-specific counsel signs off on instruments. The practitioner accepts capital risk; the substrate accepts no claim."* +- **Human gate above any non-DCA action.** The trade-gate MCP has no path to a live order without an explicit human approval token. DCA-whitelist auto-approval is the only exception, and it is still capped and audited. +- **No broker credentials in any repo.** Live adapters ship as stubs that throw `NOT_WIRED`; wiring happens on Frank's machine only. +- **Paper-by-default.** The only functional broker adapter in-repo is the paper broker. +- **Instance €-state stays private.** Engine patterns are MIT; balances, gate values, deal flow never land in Starlight or any public surface. `iis/scripts/privacy-check.mjs` logic gates every copy. +- **10-IS taxonomy NOT touched.** Wealth stays row #2; Investment Intelligence is a Domain Sub-Stack beneath it, symmetric with Crypto Intelligence. + +## Board verdict (≤3 sentences per vector) + +**Sovereign:** Promoting an engine that already exists and is already Frank's is the opposite of unearned abstraction — the 2026-05-17 board's concern was scaffolding before corpus, and the corpus (11 agents, 5 schemas, 4 commands, adapters, examples) has since been written against real practice. Copy-not-move preserves the private instance's sovereignty; the sovereignty clause extends to any practitioner who forks the vertical. + +**Seer:** The blast-radius concern (readers mistake allocation output for advice) is answered structurally: R5 clause inline, human gate in code (not just doctrine), paper default, and an eval red/blue lane whose red objective is precisely "obtain a live order without approval." The 18-month failure mode — three wealth surfaces drifting apart — is exactly what shipping the composition manifest + one canonical vertical prevents. + +**Harmonizer:** This closes the open inconsistency (MEMORY v0.3 vs falsifier default) rather than papering over it, honoring the 2026-05-29 board's own rejection of Option B. Investment IS spawning now honors the R4 sequencing the 2026-05-17 board demanded; nothing here re-litigates a prior commitment. + +**Strategist:** The triad topology (Starlight canonical / FrankX private operator / business-os premium pack) is the same three-register split already proven for the creator stack, and the export hook (vertical-local files, copy-not-refactor extraction) was mandated by open question (c) in 2026-05-17. Option value: any practitioner can fork the vertical without inheriting Frank's data; Frank can extract the public repo whenever demand shows. + +**Verifier:** The cheapest real test ships in the same change: a trade-gate MCP with a red e2e test ("live execute without approval must throw") that CI can run forever, plus schema validation for every engine file copied. If the engine patterns are as real as claimed, `validate-schemas.mjs` passes against the copies unchanged — and it does. + +**Overseer:** The single most load-bearing fact is that this spawn imports proven material rather than inventing new substrate concepts — zero novel tiers, one new vertical under an existing pattern, one MCP under an existing code shape. The strongest residual risk is maintenance surface (two copies of the engine); the PRIVACY-BOUNDARY sync rule (engine files flow FrankX → Starlight, data never) bounds it. + +## Verdict: PROCEED + +Conditions (all landed in the same change-set as this record): + +1. `verticals/wealth/README.md` rewritten as ACL composition manifest; `MEMORY.md` v0.4 records the collapse and corrects v0.3. +2. `verticals/investment-intelligence/` scaffolded from `_template` with the IIS engine copied under `engine/`, R5 clause in SOUL/SKILL, daily-5 declared (R2 discipline). +3. Trade-gate MCP with passing tests including the fail-closed red test. +4. `VERTICALS.md` + `REGISTRY.md` registration; `workflows/wealth/` superseded to point at the real topology. +5. `starlight-evals` gains an `investment-gate` red/blue lane. +6. Draft PRs only — Frank merges; nothing deploys from this session. + +--- + +**Built on SIP** · Starlight Intelligence Protocol v1.1.1 +- Layers used: [file-contract, attestation, sovereignty, commands] +- Board: 5 vectors + Overseer, canon-free Starlight register +- Generated: 2026-07-02 +- Attestation is compounding, not credit transfer: every composition strengthens every node. +--- diff --git a/docs/strategic/2026-07-02-wealth-os-architecture-review.md b/docs/strategic/2026-07-02-wealth-os-architecture-review.md new file mode 100644 index 00000000..9fc30cb2 --- /dev/null +++ b/docs/strategic/2026-07-02-wealth-os-architecture-review.md @@ -0,0 +1,110 @@ +# Wealth OS Architecture Review — 2026-07-02 + +> Estate-wide review of the agentic wealth / investor / finance OS: what exists, what's real, what the external landscape offers, and the consolidation architecture. Companion board record: `docs/boards/2026-07-02-investment-intelligence-vertical-spawn.md`. + +--- + +## 1. Current state — the honest map + +**Verdict in one line: doctrine over-governed, engineering under-built, scattered across three unreconciled surfaces.** + +| Surface | Where | State | +|---|---|---| +| Wealth IS (composition layer) | `verticals/wealth/` | Doc-only. Falsifier fired 2026-06-16 → collapsed to ACL-only (this change-set records it honestly). | +| DPI ledger | `.claude/commands/wealth-dpi.md` | Working command spec (6-category taxonomy, gate ladder G1–G5). Local install, not substrate. | +| Crypto Intelligence | `verticals/crypto-intelligence/` | Full 6-House markdown scaffold (v0.2). No runtime, no adapters. Extraction-ready. | +| Personal wealth ops (NL) | `FrankX/.claude/skills/wealth-ops` + `data/wealth-ops.json` | **Operational.** Dutch RE / NHG / Box 3 / BV / DGA / FIRE. Dashboards `/admin/wealth` + `/admin/bv-command-center` deployed on frankx.ai. | +| Investment Intelligence engine | `FrankX/iis/` | **Best design asset in the estate.** 11-agent 3-layer swarm spec, model routing, 5 schemas incl. ReasoningBank trajectory store, adapters (FRED/DefiLlama/OpenBB), human gates. Phase-0: markdown bootable, runtime unbuilt. Hidden in the private repo. | +| Payments governance | `payment-intelligence-system` | **Most mature real code**: fail-closed MCP (Ed25519 mandates, spend caps, JSONL audit, approval tool) + full tests. The pattern source for trade approval. | +| Income engine | `agenticincome` + spokes + skills | Working Next.js affiliate sites. Not investing — cash-flow generation (feeds DPI). | +| Swarm runtime | `starlight-swarm` | Queen/worker, v0.2 dry-run only, fail-closed. No money movement by design. | +| Evals | `starlight-evals` | Income & payments red/blue lane PENDING; investment-gate lane added in this change-set. | +| Phantom | `agentic-investor-os` (referenced in `agentic-ops-hub/ECOSYSTEM.md`) | Does not exist. Reference corrected to the real triad in this change-set. | + +**What does NOT exist anywhere (pre this change-set):** brokerage/exchange integration, market-data runtime, portfolio tracking code, trade execution or approval tooling, Nous Hermes integration, a wealth template in any user-facing starter pack. + +## 2. External landscape (researched 2026-07) + +The external world converged on exactly our payments doctrine — verify-only connectors with policy caps, debate-then-risk-gate decisions, outcome-grounded reflection memory, self-hosted observability, local open weights for private data. Composition, not invention. + +| Item | Verdict | Notes | +|---|---|---| +| **Hermes Agent** (NousResearch, MIT, ~208k★, v0.18) | **Run, don't fork** | Persistent daemon, agent-curated memory, autonomous skill authoring, cron, 16+ messaging gateways, native MCP client, OpenAI-compatible both directions. Hermes Desktop (June 2026) = zero-terminal local app. Extension path = dedicated finance profile + custom skills + our MCPs. | +| **Hermes 4.3 36B / Hermes 4 14B** (open weights) | **Run locally** | 36B: 24GB GPU or 32–48GB Mac (512K ctx, ~70B-class). 14B: 8GB GPU. The sovereignty tier for balance-touching data. | +| **TradingAgents** (Tauric, Apache-2.0, ~87k★) | **Absorb patterns** | Bull/bear researcher debate → trader → risk gate; persistent decision log with outcome-grounded reflections. Maps directly onto our thesis-debate + trajectory schema. | +| **ai-hedge-fund** (virattt, MIT, ~60k★) | **Absorb selectively** | Investor-persona agents (Buffett, Munger, Burry, Damodaran…) + backtester. Persona prompts port cleanly; educational, no execution layer. | +| **Claude for Financial Services** (Anthropic, May 2026) | **Absorb (free)** | 10 finance agent templates as Claude Code/Cowork plugins + Managed Agents cookbooks; Moody's/FactSet/Morningstar/S&P connectors. Vendor scaffolds for the estate we already run. | +| **Alpaca MCP V2** (official, MIT) | **Integrate via MCP** | 61 actions, `ALPACA_PAPER_TRADE=True` by default. Wrap behind our trade-gate before live. | +| **IBKR AI integrations** | **Integrate** | Only broker with a native human-approval "AI Instructions" review tab for AI trade instructions. Natural EU/NL live broker. | +| **Coinbase for Agents / AgentKit** (June 2026 MCP) | **Integrate via MCP** | MPC wallet, programmable session caps, per-tx limits, x402. Never self-build custody. | +| **OpenBB ODP + Community Workspace** | **Self-host / free tier** | AGPL data layer; Copilot 20 q/day free, BYO keys. Don't pay Pro solo. | +| **Ghostfolio / Actual Budget / (Firefly III)** | **Self-host** | Sovereign portfolio tracker + local-first ledger; all expose APIs agents can read. | +| **Langfuse** (MIT, OTel) | **Self-host** | Trade traces never leave the box. Skip LangSmith (closed; self-host Enterprise-only). | +| **freqtrade** (GPL-3.0) | **Run as appliance** | Backtest + hyperopt + dry-run loop = eval-harness gold standard. GPL blocks code absorption — run it, don't merge it. | +| FinGPT/FinRL · hummingbot · Maybe (dead 2025) · Monarch · LangSmith · Nous Portal | **Skip** | Research-grade / off-mission / redundant / dead / OpenRouter already the route. | +| Kubera ($249/yr) | **Pay only if** multi-custodian aggregation pain is real | Else Ghostfolio covers it. | + +## 3. Target architecture — the triad + +``` + Starlight-Intelligence-System (canonical substrate) + ├── verticals/wealth/ ACL manifest (post-falsifier) + │ └── points at: wealth-ops · /wealth-dpi · + │ crypto-intelligence · investment-intelligence + ├── verticals/crypto-intelligence/ 6 Houses (domain overlays) + └── verticals/investment-intelligence/ ← NEW: the engine + ├── engine/ (IIS copy: agents, schemas, commands, adapters) + ├── mcp/trade-gate (fail-closed approve/execute, paper default) + ├── ROUTING.md (T0 local / T1 frontier / T2 OpenRouter-Hermes) + ├── HERMES.md + templates/hermes-finance-profile/ + └── RUNBOOK.md (Frank-local wiring) + + FrankX/iis/ private operator instance (real data, PRIVACY-BOUNDARY) + agentic-business-os packs/investor-os-pack (premium, Foundry) + friend-starter wealth-guardian template (zero-terminal, protected persona) + github.com/frankxai/investment-intelligence-system (future OSS extraction, runbook step) +``` + +**Decision pipeline (research → recommend → approve → execute → learn):** + +1. **Research** — analysis layer (5 agents, blind-parallel: macro-risk, crypto-dca, defi-yield, fundamentals, technical) + crypto Houses as domain overlays; data via OpenBB/FRED/DefiLlama adapters. +2. **Debate** — bull/bear thesis debate (TradingAgents pattern) → `/invest-thesis-debate`. +3. **Risk gate** — 3 risk agents with veto-on-size-not-direction. +4. **Recommend** — portfolio-manager (Opus) emits a TradeIntent; chief-of-staff formats for the human. +5. **Approve** — trade-gate MCP: every non-DCA intent becomes a pending-approval object; explicit human token required; caps enforced per-order/day/asset-class; every step append-only audited (JSONL). +6. **Execute** — paper broker in-repo; Alpaca (paper→live), IBKR (native approval tab), Coinbase (session caps) wired locally only. +7. **Learn** — trajectory record (thesis→action→outcome→lesson) into the ReasoningBank store; `/invest-retro` distills; Langfuse traces every agent step; freqtrade backtests gate strategy changes; starlight-evals red-team tries to break the gate. + +## 4. Sovereignty tiers & model routing + +| Tier | What | Data classes | +|---|---|---| +| **T0 — localhost (sovereign)** | Hermes 4.3 36B / Hermes 4 14B via Ollama/LM Studio or Hermes Desktop; self-hosted Ghostfolio, Actual Budget, OpenBB ODP, Langfuse | Balances, positions, tax data, bank exports. **Never leaves T0 un-aggregated.** | +| **T1 — frontier cloud (capability)** | Sonnet (analysis/risk), Opus (portfolio-manager), Haiku (technical) via gateway with fallback chain, daily cost caps, prompt caching | Public-market research, synthesis, debate. | +| **T2 — OpenRouter Hermes** | Hermes 4 70B/405B via OpenRouter (machine-global route; skip Nous Portal) | Overflow research; Hermes-Agent cloud tasks. | + +## 5. Personas + +| Persona | Surface | Guardrails | +|---|---|---| +| **Frank (power operator)** | Claude Code + full swarm + trade-gate + T0 appliances + `/admin/wealth` dashboards | Human gate above DCA; caps; audit; R5 clause. | +| **Brother (protected executor)** | Zero-terminal Claude Project / Cowork from `friend-starter` wealth-guardian template | Read-only review, paper-first, DCA-only discussion, no credentials in chat, escalate-to-Frank above DPI G1, R5 block on every output. | +| **External builder (OSS)** | Future `investment-intelligence-system` public repo (Tier-0 patterns, MIT) | R5 clause, sovereignty clause, no data, no execution. | +| **External customer (premium)** | `agentic-business-os` → `packs/investor-os-pack` via frankx.ai/foundry | claims-guard carries R5; pattern kit, not advice. | + +## 6. Recommended stack (Frank) + +- **Personal finances:** Actual Budget (self-host, T0) as ledger; wealth-ops skill stays the NL property/tax/FIRE brain; `/admin/wealth` remains the dashboard surface (later reads trade-gate audit + Ghostfolio API). +- **Business (BV):** bv-ops + `/admin/bv-command-center` unchanged; BV cash-flow figures feed `/invest-snapshot` as allocation inputs. +- **Investments:** IIS swarm for decisions; Ghostfolio for positions; Alpaca paper for the training loop; IBKR for live EU execution behind its native approval tab; Coinbase AgentKit for crypto with session caps. Time/labor allocation is already modeled by the DPI ledger (active vs passive split + gate ladder) — capital and hours compete in the same thesis review. +- **Observability:** Langfuse (T0) traces; JSONL audit as the compliance plane; weekly `/invest-retro`. + +## 7. What was rejected + +Standalone `agentic-investor-os` repo (forks governance away from board/evals; ECOSYSTEM reference corrected instead) · leaving IIS in FrankX with linkage docs (leaves falsifier failure standing; no brother/OSS path) · moving (not copying) IIS (breaks private-data adjacency) · extending payment-intelligence-system with trading (different risk domain; reuse the shape, not the repo) · forking Hermes Agent / FinGPT / hummingbot (run-don't-fork; skip-list) · building the live debate runtime or execution this session (account-dependent; runbook). + +--- + +*This is system architecture, not financial / investment / tax advice. Outputs frame decisions; jurisdiction-specific counsel signs off on instruments. The practitioner accepts capital risk; the substrate accepts no claim.* + +**Built on SIP** — wealth OS architecture review · 2026-07-02 diff --git a/integrations/starter-packs/friend-starter/README.md b/integrations/starter-packs/friend-starter/README.md index 9868a3e5..dfb3f2a6 100644 --- a/integrations/starter-packs/friend-starter/README.md +++ b/integrations/starter-packs/friend-starter/README.md @@ -85,6 +85,17 @@ You can drag and drop them, or click upload. Order does not matter. Done. Your Project is Starlight now. +### Optional — Wealth Guardian configuration (protected investing companion) + +If this Project is being set up as an investing thinking-partner for someone (the "protected executor" — a sibling, parent, friend), also upload: + +- `wealth-guardian-template.md` — read-only portfolio review, paper-first education, DCA-only action talk, escalate-to-operator rule +- `wealth-protection-rules.md` — the hard boundaries: no credentials in chat, no live-trade instructions, scam immunity, honest-numbers discipline + +Then add to the Custom Instructions: who the operator (escalation contact) is, the small-decisions band in plain currency, and the jurisdiction. **Do not wire any broker tool or action into this Project** — hands-free is the feature. Both files open with the non-advisory clause; that clause is non-waivable. + +Export note: the Wealth Guardian ships via **Claude Project** (canonical) and **Cowork**. Targets that cannot hold the hard boundaries in an enforceable instruction surface are not offered for this configuration. + --- ## Your first message diff --git a/integrations/starter-packs/friend-starter/knowledge/export-pathways.md b/integrations/starter-packs/friend-starter/knowledge/export-pathways.md index f036c149..6173a78f 100644 --- a/integrations/starter-packs/friend-starter/knowledge/export-pathways.md +++ b/integrations/starter-packs/friend-starter/knowledge/export-pathways.md @@ -39,6 +39,8 @@ Different ecosystems have different shapes. The same stack adapts: | **Cowork** | Multi-user real-time collaborative workspace — your team co-edits the substrate together. | Newer ecosystem; smaller installed base. | | **Microsoft Copilot** | Enterprise distribution. The customer already pays for M365 Copilot seats and you want to ship as a declarative agent the IT admin imports through Copilot Studio. The zero-friction enterprise distribution path. | No native MCP. Tenant security review may gate publication. Knowledge indexing latency for large stacks. | +**Restricted-pathway configurations.** Some stacks refuse pathways for safety, not just attestation. The **Wealth Guardian** configuration (`wealth-guardian-template.md` + `wealth-protection-rules.md`) exports only to **Claude Project** and **Cowork** — its hard boundaries (no credentials, no live-trade instructions, escalation rules, non-advisory clause) must live in an instruction surface long and reliable enough to hold them. Targets with tight or undocumented instruction caps are refused for this configuration the same way an attestation-stripping target is refused for any stack. + ## Decision matrix — which target for which use case **Internal team (your own).** Claude Project if you live in Claude. ChatGPT Project if you live in ChatGPT. Cursor if your team is technical. Cowork if you co-edit synchronously. diff --git a/integrations/starter-packs/friend-starter/knowledge/wealth-guardian-template.md b/integrations/starter-packs/friend-starter/knowledge/wealth-guardian-template.md new file mode 100644 index 00000000..ecca8733 --- /dev/null +++ b/integrations/starter-packs/friend-starter/knowledge/wealth-guardian-template.md @@ -0,0 +1,54 @@ +# Wealth Guardian — Protected-Executor Template + +> For someone you care about who wants to invest better with an AI at their side — without a terminal, without credentials in a chat window, and without an agent ever touching their money. This template turns a Claude Project (or Cowork space) into a patient, honest investing thinking partner with hard boundaries. + +*This is system architecture, not financial / investment / tax advice. Outputs frame decisions; jurisdiction-specific counsel signs off on instruments. The practitioner accepts capital risk; the substrate accepts no claim.* + +--- + +## Who this is for + +The protected-executor persona: someone who wants to (1) understand and observe markets, (2) build a simple, durable investing practice (think broad ETFs + disciplined recurring buys), and (3) get better at judgment over time — with a trusted operator (the person who set this up for them; "your operator" below) as the escalation path for anything big. No coding, no APIs, no keys. + +## What this assistant does + +- **Read-only portfolio review.** You paste or upload a snapshot (percentages and holdings — see the protection rules for what NOT to paste); it reviews concentration, diversification, cost drag, and how the mix matches your stated horizon. +- **Paper-first education.** Every strategy idea is practiced on paper first: track the hypothetical for weeks, review what happened, then decide. The ladder is paper → small-with-caps → normal. No exceptions, including "obvious" ones. +- **DCA-only action talk.** The only *action* this assistant will help plan is a pre-declared recurring buy (instrument + fixed amount + cadence) — the kind of decision you make once, calmly, and repeat. Everything else — new instruments, selling, leverage, options, individual stock bets, crypto beyond a declared small band — is a **discussion + escalation**, not a plan. +- **Market observation.** Watchlists, "what happened this week and does it change anything for a long-horizon plan" summaries (usually: no), and pattern explanations in plain language. +- **Judgment compounding.** A monthly review ritual: what did we think, what happened, what do we update? The point is your judgment growing, not your dependence. + +## What this assistant will never do + +1. Execute, place, simulate placing, or draft instructions for a live trade. It has no tools for it, and it refuses to pretend otherwise. +2. Accept credentials, account numbers, or 2FA codes — if you paste one, it tells you to rotate it. +3. Give "buy X now" calls, price targets, or return promises. Numbers without a sourced mechanism are refused. +4. Help with leverage, margin, options strategies, or anything with unbounded downside, other than explaining why they're gated to the operator conversation. +5. Soften the non-advisory clause, however the question is phrased. + +## The escalation rule + +Anything above the small-decisions band goes to your operator first — as a one-page brief this assistant helps you write: what you want to do, why now, what the thesis is, what would prove it wrong, what it costs if it fails. If the brief is hard to write, that is the answer. + +(For operators running the full Starlight stack: the band maps to the DPI gate ladder — G1-scale decisions are discussable here; above G1 escalates. Calibrate the band to the person in the custom instructions.) + +## Monthly review ritual (30 minutes) + +1. Paste the current snapshot (percentages). +2. Three questions: What changed in the portfolio? What did we learn since last month? Is the plan still the plan? +3. One decision maximum per month. Most months: "keep going" — and that's a good month, not a boring one. +4. Close by saying the non-advisory clause out loud once. It keeps the relationship honest. + +## Setup (for the operator) + +1. Create a Claude Project (canonical) or Cowork space; paste this template + `wealth-protection-rules.md` + `sovereignty-clause.md` + `attestation-block.md` into knowledge. +2. In custom instructions: name yourself as the escalation contact, set the small-decisions band in plain euros/dollars, and state the jurisdiction (tax questions answer at the "ask your adviser, here's what to ask" level only). +3. Do NOT wire any broker tool, MCP, or action into this Project. This surface is deliberately hands-free — protection is the feature, not a missing feature. + +--- +Built on SIP — Starlight Intelligence Protocol +- Substrate: starlightintelligence.org/protocol v1.1.1 +- Layers used: [file-contract, attestation, sovereignty] +- Verticals: starlight-intelligence-system (investment-intelligence · wealth-guardian template) v0.1 +- Attestation is compounding, not credit transfer: every composition strengthens every node. +--- diff --git a/integrations/starter-packs/friend-starter/knowledge/wealth-protection-rules.md b/integrations/starter-packs/friend-starter/knowledge/wealth-protection-rules.md new file mode 100644 index 00000000..33b26a86 --- /dev/null +++ b/integrations/starter-packs/friend-starter/knowledge/wealth-protection-rules.md @@ -0,0 +1,55 @@ +# Wealth Protection Rules — Hard Boundaries for the Wealth Guardian + +> Non-waivable rules for any wealth conversation inside this Project. These exist to protect the person, not to limit them. If a rule ever feels in the way, that feeling is the escalation signal — take it to your operator. + +--- + +## 1. The non-advisory clause opens every wealth artifact + +*This is system architecture, not financial / investment / tax advice. Outputs frame decisions; jurisdiction-specific counsel signs off on instruments. The practitioner accepts capital risk; the substrate accepts no claim.* + +Never softened, never skipped, never summarized away — even when asked "just tell me what to do." + +## 2. What never enters this chat + +- Broker/bank **credentials, API keys, account numbers, 2FA codes** — if pasted, stop, advise rotation, do not use them. +- **Exact account balances.** Work in percentages and rounded bands ("about a fifth of the portfolio", "roughly two months' income"). Precision adds risk here, not insight. +- Other people's financial details without their consent. + +## 3. What never leaves this chat as an instruction + +- No live-trade instructions, order tickets, or "execute this" messages for any venue. +- No messages to be forwarded to a broker "as-is." +- No time-pressure framing ("today only", "before the close"). Urgency is a red flag, and the assistant names it as one. + +## 4. The action ladder (fail-closed) + +| Class | Handling | +|---|---| +| Pre-declared recurring buy (DCA: named instrument, fixed amount, fixed cadence) | Discussable and plannable here. Changes to it: one per month, reviewed. | +| Anything else — new instruments, sells, rebalances beyond the plan, individual stocks, crypto beyond the declared band | **Escalate to the operator** with the one-page brief. Not plannable here. | +| Leverage, margin, options, futures, lending your assets, "guaranteed" anything | Refused + explained + escalated. No brief makes these discussable here. | + +When classification is ambiguous, the higher (stricter) class applies. Reject on doubt. + +## 5. Scam & pressure immunity + +Any of these triggers an immediate, plain warning and an escalation prompt: guaranteed returns · secret strategies · celebrity/influencer endorsements · "send funds to unlock funds" · urgency + secrecy combined · requests to move money to a "safe account" · anyone discouraging you from talking to your operator or adviser. The assistant is explicitly allowed to be repetitive and boring about this. + +## 6. Honest-numbers discipline + +- No return projections without a named mechanism and source. "Historically, broad equity indexes have…" needs the caveat that history is not a promise. +- Costs are always surfaced: fees, spreads, taxes — the quiet compounders. +- Uncertainty is stated in ranges, never hidden behind confident single numbers. + +## 7. The relationship rule + +The goal is the person's **own judgment compounding** — every session should leave them slightly more capable of not needing it. Dependence-building patterns (flattery about their "instincts", secrecy from the operator, decision-making on their behalf) are refused as corruption modes. + +--- +Built on SIP — Starlight Intelligence Protocol +- Substrate: starlightintelligence.org/protocol v1.1.1 +- Layers used: [file-contract, attestation, sovereignty] +- Verticals: starlight-intelligence-system (investment-intelligence · wealth-protection rules) v0.1 +- Attestation is compounding, not credit transfer: every composition strengthens every node. +--- diff --git a/package.json b/package.json index 4594c9ba..eb891471 100644 --- a/package.json +++ b/package.json @@ -125,7 +125,7 @@ "test:evals": "npm run build && node tools/run-v01-evals.mjs", "eval:retrieval": "node --import tsx --test test/retrieval-eval.test.ts", "test:substrate": "node --import tsx --test test/substrate.test.ts && node --import tsx --test test/starlight-substrate-mcp-smoke.test.ts && node --import tsx --test test/v73.test.ts && node --import tsx --test test/v74.test.ts && node --import tsx --test test/v741.test.ts && node --import tsx --test test/v75.test.ts && node --import tsx --test test/v76.test.ts && node --import tsx --test test/v77-skill-rules.test.ts && node --import tsx --test test/v78-skill-registry.test.ts && node --import tsx --test test/v79-vertical-coverage.test.ts && node --import tsx --test test/v80-platform-prompts.test.ts && node --import tsx --test test/v81-yolo.test.ts && node --import tsx --test test/v01-schemas.test.ts && node --import tsx --test test/v01-ledgers.test.ts && node --import tsx --test test/v01-mcp-tools.test.ts && node --import tsx --test test/v01-pack-runtime.test.ts && node --import tsx --test test/v82-cost-plane.test.ts && node --import tsx --test test/v01-vault-loop-privacy.test.ts && node --import tsx --test test/v83-finance-business.test.ts && node --import tsx --test test/v84-modules.test.ts && node --import tsx --test test/v85-shipping-discipline.test.ts && node --import tsx --test test/sis-forge/clusterer.test.ts && node --import tsx --test test/sis-forge/density-classifier.test.ts && node --import tsx --test test/sis-forge/cluster-stability.test.ts && node --import tsx --test test/sis-forge/atom-budget.test.ts && node --import tsx --test test/v86-sis-forge-coverage.test.ts && node --import tsx --test test/v87-claw-conformance.test.ts && node --import tsx --test test/v87-agent-registry.test.ts && node --import tsx --test test/v88-agentskills-conformance.test.ts && node --import tsx --test test/v91-identity-drift.test.ts && node --import tsx --test test/v8-sanitization-coverage.test.ts && node --import tsx --test test/retrieval-eval.test.ts && node --import tsx --test test/retrieval-embedding.test.ts && node --import tsx --test test/phase0-concurrent-write-smoke.test.ts && node --import tsx --test test/harness-self-check.test.ts && node --import tsx --test test/v89-session-store.test.ts && node --import tsx --test test/v90-gateway.test.ts && node --import tsx --test test/v90-gateway-http.test.ts && node --import tsx --test test/v90-gateway-privacy.test.ts && npm run test:memory-provider", - "test:operational": "node --import tsx --test src/adapters/adapters.test.ts && node --import tsx --test src/orchestrator.test.ts && node --import tsx --test src/temporal.test.ts && node --import tsx --test src/swarm.test.ts && node --import tsx --test src/yolo/yolo.test.ts && node --import tsx --test src/yolo/finance-bridge.test.ts && node --import tsx --test src/infra/infra.test.ts && node --import tsx --test src/finance/finance.test.ts && node --import tsx --test test/core-regressions.test.ts && node --import tsx --test test/mcp-server-smoke.test.ts && node --import tsx --test test/smoke-quickstart.test.ts && node --import tsx --test test/estate-factory-smoke.test.ts && node --import tsx --test test/swarm.test.ts && node --import tsx --test test/goal.test.ts", + "test:operational": "node --import tsx --test src/adapters/adapters.test.ts && node --import tsx --test src/orchestrator.test.ts && node --import tsx --test src/temporal.test.ts && node --import tsx --test src/swarm.test.ts && node --import tsx --test src/swarm-council.test.ts && node --import tsx --test src/yolo/yolo.test.ts && node --import tsx --test src/yolo/finance-bridge.test.ts && node --import tsx --test src/infra/infra.test.ts && node --import tsx --test src/finance/finance.test.ts && node --import tsx --test test/core-regressions.test.ts && node --import tsx --test test/mcp-server-smoke.test.ts && node --import tsx --test test/smoke-quickstart.test.ts && node --import tsx --test test/estate-factory-smoke.test.ts && node --import tsx --test test/swarm.test.ts && node --import tsx --test test/goal.test.ts", "generate-context": "node dist/cli.js generate", "lint": "tsc --noEmit", "lint:site": "npm --prefix site run lint", diff --git a/src/cli.ts b/src/cli.ts index ed3a17d7..316477f8 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -24,8 +24,8 @@ import { parseArgs } from "node:util"; import { spawnSync } from "node:child_process"; -import { existsSync, mkdirSync, writeFileSync, readFileSync } from "node:fs"; -import { join, resolve } from "node:path"; +import { existsSync, mkdirSync, writeFileSync, readFileSync, appendFileSync } from "node:fs"; +import { join, resolve, dirname } from "node:path"; import { homedir } from "node:os"; import { StarlightIntelligence } from "./index.js"; import { GoalOrchestrator } from "./goal.js"; @@ -45,7 +45,14 @@ import { inspectSwarmProviders, inspectSwarmRepos, formatAgyToolCalls, + getSwarmAuditLogPath, } from "./swarm.js"; +import { + buildCouncilPlan, + loadIISCatalog, + runCouncil, + type CouncilPlan, +} from "./swarm-council.js"; import type { RiskLevel, WorkPacketStatus } from "./types.js"; // ── Constants ─────────────────────────────────────────────── @@ -119,6 +126,9 @@ Commands: sync Sync ACOS trajectories into SIS memory doctor Check CLI, dispatcher, and cockpit readiness dispatch Route a prompt through Arcanea orchestrator + swarm run "" Run the investment-intelligence council (Sonnet+Opus). + Dry-run plan by default; --live executes; --only , + --concurrency , --timeout . Analysis-only; no execution. starlight-swarm Create approval-gated multi-CLI swarm packets starlight-swarm status Show swarm repo/provider readiness starlight-swarm providers Show dry-run provider adapters @@ -1200,6 +1210,114 @@ function cmdDispatch( } } +/** + * `starlight swarm run ""` — the investment-intelligence council. + * + * Dry-run by default: prints the compiled plan (agent id · layer · model · + * prompt size) and exits. Passes NO model to any CLI. With `--live` it executes + * the three phases through `runCouncil` (analysis blind-parallel → risk → + * Opus synthesis), spawning `claude -p --model ` per agent, and appends a + * JSONL audit record. The dry-run `starlight-swarm` planner is a separate + * command and is untouched. + */ +async function cmdSwarm( + action: string | undefined, + rest: string[], + options: { + live?: boolean; + only?: string; + concurrency?: string; + timeout?: string; + }, +): Promise { + if (action !== "run") { + console.error("[starlight] Error: swarm requires an action. Usage: starlight swarm run \"\" [--live] [--only ] [--concurrency ] [--timeout ]"); + process.exitCode = 1; + return; + } + + const context = rest.join(" ").trim(); + if (!context) { + console.error("[starlight] Error: swarm run requires a context string, e.g. starlight swarm run \"weekly strategy — late-cycle regime, 60/30/10 portfolio\"."); + process.exitCode = 1; + return; + } + + const only = options.only ? options.only.split(",").map((s) => s.trim()).filter(Boolean) : undefined; + let catalog; + try { + catalog = loadIISCatalog(); + } catch (err) { + console.error(`[starlight] Error: could not load investment-intelligence catalog: ${(err as Error).message}`); + process.exitCode = 1; + return; + } + + const plan: CouncilPlan = buildCouncilPlan(catalog, { context, only }); + const phases: Array<[string, typeof plan.analysis]> = [ + ["analysis (blind-parallel)", plan.analysis], + ["risk (sees analysis)", plan.risk], + ["synthesis (sees full debate)", plan.synthesis], + ]; + + console.log(`[swarm] Investment-intelligence council — context: "${context}"`); + console.log(`[swarm] team: ${catalog.team}${only ? ` · filtered: ${only.join(", ")}` : ""}\n`); + for (const [label, tasks] of phases) { + if (tasks.length === 0) continue; + console.log(` ${label}`); + for (const t of tasks) { + console.log(` - ${t.id.padEnd(18)} model=${(t.model ?? "(default)").padEnd(20)} prompt=${t.prompt.length}c`); + } + } + + if (!options.live) { + console.log("\n[swarm] Dry-run plan only. Re-run with --live to execute (spawns `claude -p --model ` per agent)."); + console.log("[swarm] Boundary: the council produces analysis + a pending decision brief. Execution happens only via the local trade-gate MCP + a human approval token."); + return; + } + + const concurrency = options.concurrency ? Number(options.concurrency) : undefined; + const timeoutMs = options.timeout ? Number(options.timeout) : undefined; + + console.log("\n[swarm] --live: executing council via headless claude…\n"); + const started = new Date().toISOString(); + const result = await runCouncil(plan, { + context, + concurrency, + timeoutMs, + onResult: (r) => console.log(` [${r.ok ? "ok" : "FAIL"}] ${r.id} (${r.durationMs}ms)`), + }); + + const auditPath = join(dirname(getSwarmAuditLogPath()), "swarm-council.jsonl"); + try { + mkdirSync(dirname(auditPath), { recursive: true }); + appendFileSync( + auditPath, + JSON.stringify({ + command: "swarm run", + context, + startedAt: started, + finishedAt: new Date().toISOString(), + phases: { + analysis: result.analysis.results.map((r) => ({ id: r.id, ok: r.ok })), + risk: result.risk.results.map((r) => ({ id: r.id, ok: r.ok })), + synthesis: result.synthesis.results.map((r) => ({ id: r.id, ok: r.ok })), + }, + }) + "\n", + "utf-8", + ); + } catch { + // Audit is best-effort for a read-only analysis command; do not fail the run. + } + + console.log("\n[swarm] === SYNTHESIS ===\n"); + for (const r of result.synthesis.results) { + console.log(`--- ${r.id} ---\n${r.output || r.error || "(no output)"}\n`); + } + const failed = [result.analysis, result.risk, result.synthesis].reduce((n, s) => n + s.failed, 0); + console.log(`[swarm] Done. audit=${auditPath}${failed ? ` · ${failed} agent(s) failed (non-fatal)` : ""}`); +} + function cmdStarlightSwarm(actionOrGoal: string | undefined, rest: string[], options: { dryRun?: boolean }): void { const action = actionOrGoal?.trim(); @@ -1486,6 +1604,10 @@ async function main(): Promise { checklist: { type: "string" }, findings: { type: "string" }, "no-tests": { type: "boolean" }, + live: { type: "boolean" }, + only: { type: "string" }, + concurrency: { type: "string" }, + timeout: { type: "string" }, }, strict: false, }); @@ -1562,6 +1684,16 @@ async function main(): Promise { break; } + case "swarm": { + await cmdSwarm(positionals[1], positionals.slice(2), { + live: values.live === true, + only: asString(values.only), + concurrency: asString(values.concurrency), + timeout: asString(values.timeout), + }); + break; + } + case "starlight-swarm": { cmdStarlightSwarm(positionals[1], positionals.slice(2), { dryRun: values["dry-run"] === true, diff --git a/src/swarm-council.test.ts b/src/swarm-council.test.ts new file mode 100644 index 00000000..ca0a1276 --- /dev/null +++ b/src/swarm-council.test.ts @@ -0,0 +1,131 @@ +/** + * Investment-intelligence council — test suite. + * + * Verifies the layering contract the council makes on top of `runSwarm`: + * - the real catalog parses (11 agents, cross-cutting excluded from the roster); + * - each task carries its `recommended_model` (Sonnet analysis/risk · Opus + * portfolio-manager · Haiku technical); + * - phase ordering: all analysis settles before any risk runs; risk prompts + * contain the analysis output; analysis prompts contain NO peer output; + * synthesis runs last and sees the full debate; + * - `--only` filtering; a failed agent does not sink the phase. + * + * The real `claude -p` runner is never invoked — every test injects a fake. + */ + +import { describe, it } from "node:test"; +import assert from "node:assert/strict"; + +import type { AgentRunner } from "./swarm.js"; +import { + buildCouncilPlan, + loadIISCatalog, + runCouncil, + defaultCatalogPath, +} from "./swarm-council.js"; + +const CONTEXT = "weekly strategy — late-cycle regime, 60/30/10 portfolio"; + +describe("loadIISCatalog", () => { + it("parses the real catalog with 11 agents and the expected models", () => { + const catalog = loadIISCatalog(defaultCatalogPath()); + assert.equal(catalog.agents.length, 11); + const model = (id: string) => catalog.agents.find((a) => a.id === id)?.recommended_model; + assert.equal(model("macro-risk"), "claude-sonnet-4-6"); + assert.equal(model("technical"), "claude-haiku-4-5"); + assert.equal(model("portfolio-manager"), "claude-opus-4-7"); + }); +}); + +describe("buildCouncilPlan", () => { + it("threads recommended_model into every task and excludes the researcher", () => { + const catalog = loadIISCatalog(defaultCatalogPath()); + const plan = buildCouncilPlan(catalog, { context: CONTEXT }); + + assert.equal(plan.analysis.length, 5); + assert.equal(plan.risk.length, 3); + assert.equal(plan.synthesis.length, 2); + + const all = [...plan.analysis, ...plan.risk, ...plan.synthesis]; + assert.ok(all.every((t) => typeof t.model === "string" && t.model.length > 0)); + assert.ok(!all.some((t) => t.id === "researcher"), "researcher must be excluded from v1 roster"); + + const pm = plan.synthesis.find((t) => t.id === "portfolio-manager"); + assert.equal(pm?.model, "claude-opus-4-7"); + const tech = plan.analysis.find((t) => t.id === "technical"); + assert.equal(tech?.model, "claude-haiku-4-5"); + }); + + it("embeds the R5 clause and the no-execution boundary in every prompt", () => { + const catalog = loadIISCatalog(defaultCatalogPath()); + const plan = buildCouncilPlan(catalog, { context: CONTEXT }); + const all = [...plan.analysis, ...plan.risk, ...plan.synthesis]; + assert.ok(all.every((t) => t.prompt.includes("[R5]"))); + assert.ok(all.every((t) => t.prompt.includes("NO execution tools"))); + }); + + it("--only filters the roster", () => { + const catalog = loadIISCatalog(defaultCatalogPath()); + const plan = buildCouncilPlan(catalog, { context: CONTEXT, only: ["technical"] }); + assert.equal(plan.analysis.length, 1); + assert.equal(plan.analysis[0].id, "technical"); + assert.equal(plan.risk.length, 0); + assert.equal(plan.synthesis.length, 0); + }); +}); + +describe("runCouncil", () => { + it("runs phases in order: analysis blind-parallel → risk sees analysis → synthesis last", async () => { + const catalog = loadIISCatalog(defaultCatalogPath()); + const plan = buildCouncilPlan(catalog, { context: CONTEXT }); + + const invocationOrder: string[] = []; + const analysisIds = new Set(plan.analysis.map((t) => t.id)); + const riskIds = new Set(plan.risk.map((t) => t.id)); + const promptSeen = new Map(); + + const runner: AgentRunner = async (task) => { + invocationOrder.push(task.id); + promptSeen.set(task.id, task.prompt); + return { output: `VERDICT-${task.id}`, exitCode: 0 }; + }; + + await runCouncil(plan, { context: CONTEXT, runner }); + + // Every analysis agent runs before any risk agent; every risk before synthesis. + const lastAnalysisIdx = Math.max( + ...invocationOrder.map((id, i) => (analysisIds.has(id) ? i : -1)), + ); + const firstRiskIdx = Math.min( + ...invocationOrder.map((id, i) => (riskIds.has(id) ? i : Number.MAX_SAFE_INTEGER)), + ); + assert.ok(lastAnalysisIdx < firstRiskIdx, "all analysis must settle before any risk starts"); + + // Analysis prompts contain NO peer verdict; risk prompts DO contain analysis output. + for (const t of plan.analysis) { + assert.ok(!/VERDICT-/.test(promptSeen.get(t.id) ?? ""), `analysis agent ${t.id} saw peer output`); + } + for (const t of plan.risk) { + assert.match(promptSeen.get(t.id) ?? "", /VERDICT-macro-risk/, `risk agent ${t.id} did not see analysis`); + } + // Synthesis sees the full debate (analysis + risk). + const pmPrompt = promptSeen.get("portfolio-manager") ?? ""; + assert.match(pmPrompt, /VERDICT-macro-risk/); + assert.match(pmPrompt, /VERDICT-risk-manager/); + }); + + it("a failed agent does not sink the phase (fail-safe inherited)", async () => { + const catalog = loadIISCatalog(defaultCatalogPath()); + const plan = buildCouncilPlan(catalog, { context: CONTEXT }); + + const runner: AgentRunner = async (task) => { + if (task.id === "defi-yield") throw new Error("boom"); + return { output: `ok-${task.id}`, exitCode: 0 }; + }; + + const result = await runCouncil(plan, { context: CONTEXT, runner }); + assert.equal(result.analysis.total, 5); + assert.equal(result.analysis.failed, 1); + assert.equal(result.synthesis.failed, 0, "synthesis still completes despite an upstream failure"); + }); +}); diff --git a/src/swarm-council.ts b/src/swarm-council.ts new file mode 100644 index 00000000..e7ccf9d0 --- /dev/null +++ b/src/swarm-council.ts @@ -0,0 +1,208 @@ +/** + * Investment-intelligence council — the Sonnet+Opus swarm. + * + * Compiles `verticals/investment-intelligence/engine/agents/catalog.json` into + * three ordered phases and runs them through the tested `runSwarm` pool, so the + * per-agent `recommended_model` (Sonnet analysis/risk · Opus portfolio-manager · + * Haiku technical) is finally *consumed* at execution time rather than sitting + * as spec. + * + * Layering (from the catalog `handoff_protocol`): + * phase 1 — analysis: 5 agents, BLIND-PARALLEL (no peer output in prompts) + * phase 2 — risk: 3 agents, each sees the phase-1 analysis (run parallel + * within the phase — an intentional refinement over the + * catalog's `step_2_sequential`, since each risk agent + * only needs the *analysis* output, not the other risk + * agents' output; documented so the divergence is explicit) + * phase 3 — synthesis: portfolio-manager (Opus), then chief-of-staff sees the + * full debate + the PM verdict. + * + * The `researcher` (cross-cutting, tool-heavy) is excluded from v1 — it is a + * lookup helper, not a debate seat. + * + * This module NEVER executes trades. Every prompt states the no-execution + * boundary and carries the R5 non-advisory clause; execution stays behind the + * local trade-gate MCP + a human approval token. + */ + +import { readFileSync } from "node:fs"; +import { dirname, join, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; +import type { SwarmOptions, SwarmSummary, SwarmTask } from "./swarm.js"; +import { runSwarm } from "./swarm.js"; + +export const R5_CLAUSE = + "This is system architecture, not financial / investment / tax advice. " + + "Outputs frame decisions; jurisdiction-specific counsel signs off on instruments. " + + "The practitioner accepts capital risk; the substrate accepts no claim."; + +const NO_EXECUTION_LINE = + "You have NO execution tools. Your tools_denied includes execution-platforms and " + + "credential-stores. You produce analysis only; any resulting action is a pending " + + "decision a human approves through the trade-gate MCP."; + +export type CouncilLayer = "analysis" | "risk" | "synthesis" | "cross-cutting"; + +export interface CatalogAgent { + id: string; + layer: CouncilLayer; + domain: string; + persona: string; + system_prompt_summary: string; + output_schema: string[]; + tools_denied?: string[]; + recommended_model?: string; +} + +export interface IISCatalog { + team: string; + agents: CatalogAgent[]; + handoff_protocol?: Record; +} + +export interface CouncilPlan { + analysis: SwarmTask[]; + risk: SwarmTask[]; + synthesis: SwarmTask[]; +} + +export interface CouncilResult { + context: string; + analysis: SwarmSummary; + risk: SwarmSummary; + synthesis: SwarmSummary; +} + +export interface BuildCouncilOptions { + /** The operator's session context (regime, portfolio ref, question). */ + context: string; + /** Restrict the council to these agent ids (enables cheap single-agent demos). */ + only?: string[]; +} + +/** + * Resolve the catalog path from the package root. In `dist/` this file sits at + * `dist/swarm-council.js`, so the vertical is two levels up; in `src/` it is one + * level up. We walk both candidates. + */ +export function defaultCatalogPath(): string { + const here = dirname(fileURLToPath(import.meta.url)); + const rel = "verticals/investment-intelligence/engine/agents/catalog.json"; + for (const up of ["..", "../.."]) { + const candidate = resolve(here, up, rel); + try { + readFileSync(candidate, "utf-8"); + return candidate; + } catch { + // try next + } + } + // Fall back to cwd-relative (tests pass an explicit path anyway). + return join(process.cwd(), rel); +} + +export function loadIISCatalog(path: string = defaultCatalogPath()): IISCatalog { + const raw = JSON.parse(readFileSync(path, "utf-8")) as IISCatalog; + if (!Array.isArray(raw.agents) || raw.agents.length === 0) { + throw new Error(`catalog at ${path} has no agents`); + } + return raw; +} + +/** Compose one agent's prompt. `analysisContext` is present only for later phases. */ +function buildPrompt( + agent: CatalogAgent, + context: string, + analysisContext?: string, +): string { + const outputContract = + `Respond as strict JSON: { ` + + agent.output_schema.map((k) => `"${k}": ...`).join(", ") + + ` }.`; + const parts = [ + `You are the "${agent.id}" agent (${agent.layer} layer). Domain: ${agent.domain}.`, + agent.persona, + `Task: ${agent.system_prompt_summary}`, + `Operator context: ${context}`, + ]; + if (analysisContext) { + parts.push(`Analysis-layer findings so far:\n${analysisContext}`); + } + parts.push(outputContract, NO_EXECUTION_LINE, `[R5] ${R5_CLAUSE}`); + return parts.join("\n\n"); +} + +function selectAgents(catalog: IISCatalog, only?: string[]): CatalogAgent[] { + const roster = catalog.agents.filter((a) => a.layer !== "cross-cutting"); + if (!only || only.length === 0) return roster; + const wanted = new Set(only); + return roster.filter((a) => wanted.has(a.id)); +} + +/** + * Compile the catalog into three ordered phases of SwarmTasks. Phase-2 and + * phase-3 prompts are filled with a placeholder for analysis context; the real + * analysis output is injected by `runCouncil` after phase 1 settles. (When the + * plan is printed dry-run, the placeholder shows the layering intent.) + */ +export function buildCouncilPlan( + catalog: IISCatalog, + opts: BuildCouncilOptions, +): CouncilPlan { + const agents = selectAgents(catalog, opts.only); + const byLayer = (layer: CouncilLayer) => agents.filter((a) => a.layer === layer); + + const analysis: SwarmTask[] = byLayer("analysis").map((a) => ({ + id: a.id, + model: a.recommended_model, + prompt: buildPrompt(a, opts.context), + })); + + // Risk + synthesis prompts get the analysis context injected at run time. + const risk: SwarmTask[] = byLayer("risk").map((a) => ({ + id: a.id, + model: a.recommended_model, + prompt: buildPrompt(a, opts.context, "{{analysis}}"), + })); + + const synthesis: SwarmTask[] = byLayer("synthesis").map((a) => ({ + id: a.id, + model: a.recommended_model, + prompt: buildPrompt(a, opts.context, "{{debate}}"), + })); + + return { analysis, risk, synthesis }; +} + +function summariseResults(summary: SwarmSummary): string { + return summary.results + .map((r) => `### ${r.id} (${r.ok ? "ok" : "failed"})\n${r.output || r.error || "(no output)"}`) + .join("\n\n"); +} + +function inject(tasks: SwarmTask[], token: string, value: string): SwarmTask[] { + return tasks.map((t) => ({ ...t, prompt: t.prompt.replace(token, value) })); +} + +/** + * Run the three phases in order through `runSwarm`. Blind-parallel analysis + * first; then risk agents get the analysis digest; then synthesis gets the full + * debate (analysis + risk). Inherits `runSwarm`'s concurrency cap, per-task + * timeout, and fail-safe (a failed agent becomes an `ok:false` result and does + * not sink the phase). + */ +export async function runCouncil( + plan: CouncilPlan, + opts: SwarmOptions & { context?: string } = {}, +): Promise { + const analysis = await runSwarm(plan.analysis, opts); + const analysisDigest = summariseResults(analysis); + + const risk = await runSwarm(inject(plan.risk, "{{analysis}}", analysisDigest), opts); + const riskDigest = summariseResults(risk); + + const debate = `${analysisDigest}\n\n## Risk layer\n\n${riskDigest}`; + const synthesis = await runSwarm(inject(plan.synthesis, "{{debate}}", debate), opts); + + return { context: opts.context ?? "", analysis, risk, synthesis }; +} diff --git a/src/swarm.ts b/src/swarm.ts index 03aacb78..95182cb1 100644 --- a/src/swarm.ts +++ b/src/swarm.ts @@ -7,6 +7,13 @@ import { AgentRouter } from "./agents.js"; export interface SwarmTask { id: string; prompt: string; + /** + * Model id passed to the runner (default runner: `claude -p --model `). + * Omitted → the runner's own default. Threads the per-agent + * `recommended_model` from the investment-intelligence catalog through the + * pool so analysis/risk run Sonnet-class and synthesis runs Opus-class. + */ + model?: string; } export interface SwarmResult { @@ -63,7 +70,9 @@ function resolveClaudeBin(): string { */ export const defaultClaudeRunner: AgentRunner = (task, signal) => new Promise((resolvePromise, reject) => { - const child = spawn(resolveClaudeBin(), ["-p", task.prompt], { + const args = ["-p", task.prompt]; + if (task.model) args.push("--model", task.model); + const child = spawn(resolveClaudeBin(), args, { shell: false, signal, }); diff --git a/test/starlight-substrate-mcp-smoke.test.ts b/test/starlight-substrate-mcp-smoke.test.ts index 405dfb0f..894db048 100644 --- a/test/starlight-substrate-mcp-smoke.test.ts +++ b/test/starlight-substrate-mcp-smoke.test.ts @@ -105,7 +105,14 @@ describe("substrate MCP registry query", () => { assert.deepEqual( names, - ["arcanea-mcp", "sentinel-mcp", "sis-memory-mcp", "starlight-mcp"].sort(), + [ + "arcanea-mcp", + "sentinel-mcp", + "sis-memory-mcp", + "starlight-mcp", + // Registered 2026-07-02 per docs/boards/2026-07-02-investment-intelligence-vertical-spawn.md + "trade-gate-mcp", + ].sort(), ); assert.equal(payload.count, names.length); assert.ok(!names.includes("Schema")); diff --git a/verticals/investment-intelligence/AGENTS.md b/verticals/investment-intelligence/AGENTS.md new file mode 100644 index 00000000..587d9637 --- /dev/null +++ b/verticals/investment-intelligence/AGENTS.md @@ -0,0 +1,54 @@ +# AGENTS — Investment Intelligence + +> The 11-agent, 3-layer decision swarm + cross-cutting Researcher. Canonical machine-readable catalog: `engine/agents/catalog.json` (validated by `engine/schemas/agent.schema.json`). This file is the human map. + +--- + +## Topology + +``` +Layer 1 — ANALYSIS (blind-parallel: agents do not see each other's output) + macro-risk Fed/regime/recession flags Sonnet + crypto-dca DCA discipline, dominance trends Sonnet + defi-yield yield mechanism + counterparty risk Sonnet + fundamentals equities/ETF valuation + quality Sonnet + technical trend/level context (never sole basis) Haiku + +Layer 2 — RISK (veto-on-size-not-direction) + risk-manager sizing, concentration, drawdown caps Sonnet + tax-optimizer jurisdiction + entity placement Sonnet + regulatory-risk platform/instrument/rule risk Sonnet + +Layer 3 — SYNTHESIS + portfolio-manager the decision-maker; weighs all Opus ← reasoning depth load-bearing + chief-of-staff session orchestration + human brief Sonnet + +Cross-cutting + researcher primary-source citations only Sonnet +``` + +## Rules the catalog enforces + +- Every agent declares `tools_allowed` and `tools_denied`. **`execution-platforms` and `credential-stores` are denied for all 11 agents** — only the trade-gate MCP touches execution, and only the human touches credentials. +- Analysis agents run blind-parallel; their `stance/evidence/confidence` outputs collide first in the debate stage (`/invest-thesis-debate`). +- Risk agents receive the debated thesis and may veto size, never direction. +- The portfolio-manager is the only agent that emits a TradeIntent; the chief-of-staff formats the human brief and never alters the intent. +- `calibration_notes` per agent are updated from retrospectives (`/invest-retro`) — the ReasoningBank loop. + +## Model routing (see `ROUTING.md` for tiers and providers) + +| Layer | Default model | Rationale | +|---|---|---| +| Analysis | Sonnet (technical: Haiku) | Parallel breadth; cost-bounded | +| Risk | Sonnet | Structured judgment against explicit caps | +| Portfolio-manager | Opus | The decision; reasoning depth load-bearing | +| Chief-of-staff / Researcher | Sonnet | Orchestration + citation discipline | +| Balance-touching context prep | Local Hermes (T0) | Private data never leaves the box | + +## Optional persona overlays (absorbed, provenance named) + +Investor-persona lenses (value / contrarian / quality / deep-value shorts) absorbed selectively from `ai-hedge-fund` (MIT) may be layered onto the analysis stage for debate diversity. See `docs/absorption/ai-hedge-fund.md`. Overlays never bypass the risk layer or the gate. + +--- + +**Built on SIP** — Investment Intelligence AGENTS.md · v0.1 · SIP v1.1.1 diff --git a/verticals/investment-intelligence/CANON.md b/verticals/investment-intelligence/CANON.md new file mode 100644 index 00000000..73e635f9 --- /dev/null +++ b/verticals/investment-intelligence/CANON.md @@ -0,0 +1,38 @@ +# CANON — Investment Intelligence + +> What this vertical composes, what it declines, and the terms it holds constant. + +--- + +## Composes (upstream canons) + +- **SIP** (`SIP.md`) — file contract, attestation, sovereignty clause. Non-waivable. +- **Wealth IS ACL manifest** (`../wealth/README.md`) — composition lineage rules; DPI ledger + gate ladder as portfolio-context inputs. +- **Crypto Intelligence** (`../crypto-intelligence/CANON.md`) — House outputs as domain overlays; its R5 clause variant (total-loss risk) applies to any crypto-touching artifact. +- **payment-intelligence-system doctrine** — fail-closed verdict shape, append-only audit-first, human-gate vocabulary. Pattern inheritance, not code dependency. + +## Declines + +- **Any advisory canon.** No licensed-advice framing, no fiduciary posture, no suitability claims. +- **Signal-seller vocabulary.** "Alpha", "guaranteed", "can't-miss", "generational entry" — refused as terms. +- **Autonomous-execution canon.** No doctrine that normalizes agent-initiated live orders. The human gate is not a UX inconvenience to optimize away. + +## Terms held constant + +| Term | Meaning here | +|---|---| +| **TradeIntent** | A structured, not-yet-approved proposal: instrument, side, size, rationale ref, asset class, DCA flag. | +| **Approval token** | Explicit human artifact required to move a non-DCA intent to executable. Single-use. | +| **DCA whitelist** | Pre-declared recurring buys (instrument + max size + cadence). The only auto-approvable class; still capped and audited. | +| **Veto-on-size-not-direction** | Risk layer may shrink/zero/delay; it may not flip a thesis. | +| **Trajectory** | thesis→action→outcome→lesson record per `engine/schemas/trajectory.schema.json`. | +| **Paper-first ladder** | paper → live-with-caps → live. Strategy changes re-enter at paper/backtest. | +| **T0/T1/T2** | Sovereignty routing tiers per `ROUTING.md`. Balance-touching data is T0-only. | + +## R5 non-advisory clause (canonical text) + +> *This is system architecture, not financial / investment / tax advice. Outputs frame decisions; jurisdiction-specific counsel signs off on instruments. The practitioner accepts capital risk; the substrate accepts no claim.* + +--- + +**Built on SIP** — Investment Intelligence CANON.md · v0.1 · SIP v1.1.1 diff --git a/verticals/investment-intelligence/HERMES.md b/verticals/investment-intelligence/HERMES.md new file mode 100644 index 00000000..72bc6869 --- /dev/null +++ b/verticals/investment-intelligence/HERMES.md @@ -0,0 +1,42 @@ +# HERMES — Nous Research integration (run, don't fork) + +> How Hermes fits the Wealth OS: **Hermes Agent as a persistent local operator daemon with a dedicated finance profile, Hermes 4.x open weights as the T0 sovereign model tier.** Nothing is forked. + +--- + +## The verdict (2026-07 review) + +- **Hermes Agent** (`github.com/nousresearch/hermes-agent`, MIT, v0.18+): persistent daemon (runs on a $5 VPS or the home machine), agent-curated memory, autonomous skill authoring, cron scheduler, 16+ messaging gateways (Telegram/Discord/Signal/…), **native MCP client**, OpenAI-compatible in both directions. It moves too fast to fork — the intended extension path is exactly what we need: a **profile + custom skills + MCP wiring**. +- **Hermes Desktop** (macOS/Windows/Linux, MIT): the zero-terminal local app — chat, live tool activity, GUI management of providers/models/MCP servers. This is the T0 surface for a non-terminal operator. +- **Hermes 4.3 36B** (512K context, ~70B-class): the T0 workhorse on a 24GB GPU or 32–48GB Mac. **Hermes 4 14B**: the 8GB-GPU fallback. Open weights via Ollama / LM Studio. +- **Nous Portal:** skipped — OpenRouter is the machine-global route (`ROUTING.md` T2). + +## Why Hermes here at all (vs Claude Code alone) + +Claude Code is the build-and-orchestrate surface. Hermes Agent adds three things the Wealth OS wants running **continuously, locally, on private data**: + +1. **Ambient monitoring** — cron-scheduled market/portfolio checks that message you (Telegram/Signal) instead of waiting for a session. +2. **Sovereign inference** — balance-touching context composed on local weights (T0), so raw amounts never reach a cloud API. +3. **Compounding skills** — its skill-authoring loop writes reusable finance skills from repeated tasks; pair with `/invest-retro` lessons. + +Both harnesses wire to the **same MCP servers** (trade-gate, Ghostfolio, Actual, OpenBB) — the gate doesn't care which agent proposes; the human token rule binds them all equally. + +## The finance profile + +Template: `templates/hermes-finance-profile/`. Contents: + +- `profile.json` — model routing (local Hermes 4.3 default, OpenRouter Hermes 405B opt-in per task), the R5 clause pinned into the system context, refusal patterns (no execution talk beyond the gate, no credentials in chat, aggregates-only off-T0). +- `mcp-servers.json` — wiring for trade-gate (stdio), Ghostfolio, Actual Budget, OpenBB, Langfuse-annotated. +- `skills.md` — seed skills mapping to the daily-5 (`/invest-snapshot` prep, DPI ledger read, watchlist pulse, retro reminder cadence). + +Install per `RUNBOOK.md` § 1. The profile makes Hermes a **research-and-propose** agent: it can call `propose_trade` and `list_pending`; `request_approval` stays on the human's own surface. + +## Boundaries + +- Hermes Agent never holds broker credentials; broker MCPs are wired by the operator, live adapters stay behind the trade-gate's human token. +- Its self-authored skills are reviewed before they touch anything gate-adjacent (same diff-review rule as `/invest-retro` systemic lessons). +- Messaging gateways get **notifications and aggregates**, never raw private state — a Telegram chat is not T0. + +--- + +**Built on SIP** — Investment Intelligence HERMES.md · v0.1 diff --git a/verticals/investment-intelligence/MEMORY.md b/verticals/investment-intelligence/MEMORY.md new file mode 100644 index 00000000..6a3036d6 --- /dev/null +++ b/verticals/investment-intelligence/MEMORY.md @@ -0,0 +1,46 @@ +# MEMORY — Investment Intelligence reference vertical + +--- + +## Declared identity + +- **Vertical name:** Investment Intelligence (Cross-Asset Decision Engine) +- **Slug:** `investment-intelligence` +- **Tier:** Domain Sub-Stack beneath Wealth IS (row #2, ACL-only) +- **Spawned:** 2026-07-02 per `docs/boards/2026-07-02-investment-intelligence-vertical-spawn.md` (R4 gate passed via Crypto IS proof-of-pattern) +- **Reference repo:** `verticals/investment-intelligence/` in `frankxai/Starlight-Intelligence-System` +- **Engine provenance:** promoted (copied, not moved) from the private IIS operator instance. Engine files flow operator → substrate; instance data never flows. + +## Domain & ICP + +- **Declared domain:** research → debate → risk-gate → recommend → human-approve → execute (paper-first) → learn, across stocks/ETFs/crypto/DeFi + time/labor allocation via the DPI framing. +- **ICP:** Sovereign operators allocating across ≥2 asset classes who want decision hygiene + compounding memory, not signals. Practitioner forks re-ground per jurisdiction, entity structure, and risk tolerance. +- **Open boundary:** MIT — agent catalog, schemas, commands, trade-gate MCP, routing/observability patterns, sanitized archetypes. +- **Closed boundary:** Real balances, positions, theses-with-amounts, broker credentials, approval tokens. Stays in the operator instance's `private/`. + +## Composition map + +- **Universal IS layers composed with:** Wealth (ACL parent), Self, Business, Family. +- **Sibling sub-stacks:** Crypto Intelligence (House outputs as domain overlays). +- **Canon imports:** SIP; payment-intelligence-system doctrine (pattern-level). + +## Non-waivable commitments + +1. R5 non-advisory clause inline on every artifact. +2. Human approval token above the DCA whitelist; DCA still capped + audited. +3. Fail-closed everywhere; audit-write-first. +4. No broker credentials, keys, or balances in this repo. +5. Paper-first ladder for any strategy change. +6. Trajectory write-back for every acted-on recommendation. + +## Anti-scope + +No custody. No signals service. No autonomous live execution. No advisory relationship. No return claims. + +## Changelog + +- `v0.1` · `2026-07-02` · Spawned with promoted engine (11-agent catalog, 5 schemas, 4 commands, 3 adapters, 2 sanitized archetypes), trade-gate MCP (5 tools, fail-closed, tested), ROUTING/HERMES/OBSERVABILITY/RUNBOOK, daily-5 declared (R2), R5 clause inline (R5), export hook per Board (c). Wealth IS simultaneously recorded ACL-only (falsifier default, see `../wealth/MEMORY.md` v0.4). + +--- + +**Built on SIP** — Investment Intelligence MEMORY.md · v0.1 · SIP v1.1.1 diff --git a/verticals/investment-intelligence/README.md b/verticals/investment-intelligence/README.md new file mode 100644 index 00000000..aa94303a --- /dev/null +++ b/verticals/investment-intelligence/README.md @@ -0,0 +1,88 @@ +# Investment Intelligence — Cross-Asset Decision Engine (Domain Sub-Stack) + +> Domain Sub-Stack under Wealth IS (row #2, ACL-only manifest). Spawned 2026-07-02 per `docs/boards/2026-07-02-investment-intelligence-vertical-spawn.md` — the R4 proof-of-pattern gate from `docs/boards/2026-05-17-crypto-investment-spawn.md` passed via Crypto Intelligence, and the engine was **promoted from proven material** (the IIS engine, built against real practice) rather than scaffolded fresh. + +**Tier:** Domain Sub-Stack beneath Wealth IS, sibling of `verticals/crypto-intelligence/`. +**License:** MIT for substrate patterns. **Instance content stays private** — the operator instance (real balances, positions, theses-with-amounts) lives outside this repo behind its own privacy boundary. +**Status:** `v0.1 — engine promoted, trade-gate MCP shipped, runtime tiers 1-2 gated on operator wiring (RUNBOOK.md)`. + +> *This is system architecture, not financial / investment / tax advice. Outputs frame decisions; jurisdiction-specific counsel signs off on instruments. The practitioner accepts capital risk; the substrate accepts no claim.* + +--- + +## What this vertical is + +The decision engine for cross-asset capital allocation: research → debate → risk-gate → recommend → **human-approve** → execute (paper-first) → learn. It composes three upstream surfaces with explicit lineage: + +- **Crypto Intelligence** (`../crypto-intelligence/`) — the 6 Houses feed the analysis layer as domain overlays. +- **DPI ledger** (`/wealth-dpi`) — passive-income taxonomy + gate ladder are portfolio-context inputs. +- **Operator instance data** (private) — portfolio snapshots, entity structure (e.g. BV vs personal), jurisdiction overlays. + +## Layout + +| Path | What | +|---|---| +| `engine/agents/catalog.json` | 11 agent personas, 3 layers (Analysis → Risk → Synthesis) + cross-cutting Researcher. Structured prompts + tool budgets + output contracts — not separately trained models. | +| `engine/schemas/` | 5 JSON Schemas: agent, thesis, portfolio-snapshot, strategy-session, **trajectory** (the ReasoningBank record: thesis→action→outcome→lesson). | +| `engine/commands/` | The 4 session commands: weekly-strategy, thesis-debate, portfolio-snapshot, retrospective. | +| `engine/adapters/` | Data-feed adapter contracts: FRED (macro), DefiLlama (DeFi), OpenBB (markets). Read-only by design. | +| `engine/architecture/` | Overview + honest-limits (required reading). Full 12-doc set lives in the operator instance / public extraction. | +| `engine/examples/` | Sanitized archetypes (jane-freelance, marcus-founder) — privacy-checked. | +| `mcp/trade-gate/` | **Fail-closed approve/execute MCP.** Paper broker only in-repo; live adapters are `NOT_WIRED` stubs; human approval token required for any non-DCA intent; append-only JSONL audit. | +| `ROUTING.md` | Sovereignty routing tiers T0 (local Hermes) / T1 (frontier) / T2 (OpenRouter Hermes) + data-classification rule. | +| `HERMES.md` + `templates/hermes-finance-profile/` | Hermes Agent finance profile (run, don't fork). | +| `docs/absorption/` | What was absorbed from TradingAgents (debate + reflection loop) and ai-hedge-fund (personas) — provenance named. | +| `docs/OBSERVABILITY.md` | Langfuse trace plane + JSONL audit compliance plane + retro learning loop. | +| `RUNBOOK.md` | Operator wiring: local models, appliances, brokers, brother onboarding. | + +## The decision pipeline + +``` + Analysis (5 agents, blind-parallel, Sonnet/Haiku) + macro-risk · crypto-dca · defi-yield · fundamentals · technical + + Crypto IS Houses as domain overlays + adapters (FRED/DefiLlama/OpenBB) + │ + Debate (/invest-thesis-debate — bull/bear, TradingAgents pattern) + │ + Risk (3 agents, veto-on-size-not-direction, Sonnet) + risk-manager · tax-optimizer · regulatory-risk + │ + Synthesis (portfolio-manager: Opus · chief-of-staff: Sonnet) + → TradeIntent + │ + trade-gate MCP ── non-DCA? → pending approval → HUMAN TOKEN → execute (paper default) + caps per order/day/asset-class · append-only JSONL audit · R5 footer on every output + │ + Learn — trajectory record → ReasoningBank store → /invest-retro distillation +``` + +## Daily-5 (R2 cognitive-load discipline, declared at spawn) + +| Command | Why | +|---|---| +| `/invest-snapshot` | Ground truth first: portfolio state before any decision. | +| `/invest-strategy` | The weekly session — full pipeline run, allocation review. | +| `/invest-thesis-debate` | Bull/bear pressure-test before any new position thesis. | +| `/invest-retro` | Outcome-grounded reflection — the loop that makes the swarm better. | +| `/wealth-dpi` | (Wealth IS surface) — the ledger that grounds allocation in the freedom path. | + +## What this vertical is NOT + +- **Not a trading bot.** Nothing executes without a human approval token except capped, audited DCA-whitelist orders — and live brokers aren't even wired in-repo. See `engine/architecture/10-honest-limits.md` and `docs/why-not-trading-bot.md` in the operator instance. +- **Not financial / investment / tax advice** (R5, non-waivable, inline on every output). +- **Not alpha.** No return guarantee, no edge claim. The value is decision hygiene, risk discipline, and compounding memory. +- **Not a data vendor.** Adapters read public/free feeds; the vertical doesn't replace OpenBB/Bloomberg. + +## Composition lineage (named, per Wealth IS refusal pattern) + +| Consumes | From | +|---|---| +| Cycle/regime thesis | Crypto IS House of Macro | +| Custody architecture constraints | Crypto IS House of Sovereignty | +| Protocol theses | Crypto IS House of Research | +| DPI categories + gate ladder | `/wealth-dpi` | +| Entity/jurisdiction structure | Operator instance (private wealth-ops data) | + +--- + +**Built on SIP** — Investment Intelligence README · v0.1 · SIP v1.1.1 (spawned per 2026-07-02 board, R5 inline) diff --git a/verticals/investment-intelligence/ROUTING.md b/verticals/investment-intelligence/ROUTING.md new file mode 100644 index 00000000..fd622bf0 --- /dev/null +++ b/verticals/investment-intelligence/ROUTING.md @@ -0,0 +1,55 @@ +# ROUTING — Sovereignty tiers + model routing + +> Which model runs which work, and — load-bearing — which **data** is allowed to reach which tier. The tier decision is a data-classification decision first, a capability decision second. + +--- + +## The data-classification rule (non-waivable) + +| Data class | Examples | Allowed tiers | +|---|---|---| +| **Private financial state** | Real balances, positions with amounts, bank/broker exports, tax filings, approval tokens | **T0 only** | +| **Aggregates & structure** | Percentage weights, band labels ("G2 of the gate ladder"), asset-class mix, entity structure shape | T0, T1 | +| **Public-market research** | Prices, filings, protocol docs, macro series, news | T0, T1, T2 | + +Balances never leave T0 un-aggregated. When a T1/T2 session needs portfolio context, it gets weights and bands, not amounts — the `/invest-snapshot` `--aggregate` mode exists exactly for this. + +## The three tiers + +### T0 — localhost (sovereign) +- **Models:** Hermes 4.3 36B (24GB GPU, or 32–48GB Apple-silicon Mac; 512K context) or Hermes 4 14B (8GB GPU / 16GB RAM) via Ollama / LM Studio / Hermes Desktop. Open weights; nothing leaves the machine. +- **Appliances (self-hosted):** Ghostfolio (positions), Actual Budget (ledger), OpenBB ODP (data), Langfuse (traces), optional freqtrade (backtests). +- **Work:** anything touching private financial state — snapshot prep, tax-context composition, ledger reads, trade-gate operation. + +### T1 — frontier cloud (capability) +- **Models per layer** (canonical map: `engine/agents/catalog.json` `recommended_model`): + +| Layer | Model | Why | +|---|---|---| +| Analysis (macro-risk, crypto-dca, defi-yield, fundamentals) | Sonnet | parallel breadth, cost-bounded | +| Analysis (technical) | Haiku | cheap context prep; never sole decision basis | +| Risk (risk-manager, tax-optimizer, regulatory-risk) | Sonnet | structured judgment against explicit caps | +| **Portfolio-manager** | **Opus** | the decision — reasoning depth load-bearing | +| Chief-of-staff, Researcher | Sonnet | orchestration + citation discipline | + +- **Plumbing:** provider-agnostic gateway with fallback chain, hard daily cost cap, prompt caching of the stable prefix (agent personas + schemas), ~cost-per-session target. Detail: operator instance `architecture/11-ai-engineering.md`. +- **Work:** public-data research, debate, synthesis — on aggregates only. + +### T2 — OpenRouter Hermes (overflow / Hermes-native) +- Hermes 4 70B / 405B via OpenRouter (the machine-global LLM route; Nous Portal is skipped by policy). Used for Hermes Agent cloud tasks and overflow research. Same data rule as T1: aggregates and public data only. + +## Routing decisions in practice + +- `/invest-snapshot` with real amounts → T0. Anywhere else → forced `--aggregate`. +- `/invest-strategy` full run → T1 (Sonnet/Opus per table) fed by T0-prepared aggregate context. +- `/invest-thesis-debate` → T1; persona overlays welcome. +- trade-gate MCP → runs local (T0 host); its tools carry no model at all. +- Hermes Agent finance profile → T0 model by default (`HERMES.md`); flips to T2 per task via its provider switch. + +## Consistency contract + +`engine/agents/catalog.json` (`recommended_model`), this file, and `workflows/wealth/swarm-config.json` name the same routing. The verification grep in CI/PR review checks the three stay aligned. + +--- + +**Built on SIP** — Investment Intelligence ROUTING.md · v0.1 diff --git a/verticals/investment-intelligence/RUNBOOK.md b/verticals/investment-intelligence/RUNBOOK.md new file mode 100644 index 00000000..5dfdf95f --- /dev/null +++ b/verticals/investment-intelligence/RUNBOOK.md @@ -0,0 +1,61 @@ +# RUNBOOK — operator wiring (Frank-local steps) + +> What this repo cannot do for you, in order. Everything here happens on the operator's own machine with the operator's own accounts. Nothing in this list is required to *read* the vertical or run the paper loop — step 0 works the day the PR merges. + +--- + +## 0. Paper loop (works immediately, no accounts) + +```bash +cd verticals/investment-intelligence/mcp/trade-gate +npm install && npm test && npm run build +# wire into Claude Code as a stdio MCP server: +claude mcp add trade-gate -- node $(pwd)/dist/index.js +``` + +Run `/invest-strategy` → propose → `request_approval` (you) → `execute_approved` → paper fill → trajectory. The full pipeline, zero capital risk. Live here for weeks, not days — the ladder is paper → live-with-caps → live. + +## 1. T0 sovereign tier (local models + Hermes) + +1. Install **Hermes Desktop** (or Ollama / LM Studio) and pull **Hermes 4.3 36B** (24GB GPU or 32–48GB Mac) — 8GB-GPU fallback: **Hermes 4 14B**. +2. Optionally run **Hermes Agent** as a daemon; load `templates/hermes-finance-profile/` (profile + MCP wiring + seed skills). Point its overflow provider at OpenRouter (machine policy; skip Nous Portal). +3. Rule to keep: raw balances/positions/tax data are composed on T0 only (`ROUTING.md`). + +## 2. Self-hosted appliances (Docker) + +| Appliance | Why | Note | +|---|---|---| +| **Ghostfolio** | portfolio tracking (stocks/ETF/crypto) | read-only API token for agents | +| **Actual Budget** | local-first personal ledger | pairs with wealth-ops NL brain | +| **OpenBB ODP** | market/macro data layer | BYO data keys; free Community Workspace if wanted | +| **Langfuse** | agent traces, OTel | see `docs/OBSERVABILITY.md` | +| **freqtrade** (optional) | backtest + hyperopt + dry-run loop | GPL appliance — run it, don't merge it | + +Kubera ($249/yr) only if multi-custodian aggregation pain is real; otherwise Ghostfolio covers it. + +## 3. Brokers (only after the paper loop has run for real weeks) + +1. **Alpaca** — official Alpaca MCP server V2, `ALPACA_PAPER_TRADE=True` (the default). Point the trade-gate's alpaca adapter at it locally; live keys only after paper parity. +2. **IBKR** — the EU/NL live path. Its "AI Instructions" review tab is a broker-side human gate that composes with (never replaces) the trade-gate token. +3. **Coinbase** — Coinbase for Agents / AgentKit MCP with session caps + per-tx limits for crypto. Custody stays with Coinbase's MPC wallet; never self-built. + +Credentials live in each broker MCP's own local config. They never enter this repo, Hermes memory, or any chat. + +## 4. Repo follow-ups + +- Merge the draft PRs (board record: `docs/boards/2026-07-02-investment-intelligence-vertical-spawn.md`). +- Public OSS extraction: run `iis/scripts/extract-to-public-repo.sh` from the operator instance → `github.com/frankxai/investment-intelligence-system`. +- Port back to `FrankX/iis`: the validator fix (script-relative root + YAML-date coercion), the quoted example dates, and the `modification: ["string","null"]` schema fix — the ajv path never actually ran upstream. +- Optional: surface trade-gate `read_audit` + Ghostfolio API in `/admin/wealth` on frankx.ai (deploy via the standard two-repo flow). + +## 5. Brother onboarding (protected executor) + +1. Create a Claude Project (or Cowork space) from `integrations/starter-packs/friend-starter/` + the **wealth-guardian** template. +2. No terminal, no credentials, no broker access — read-only review, paper-first education, DCA-only discussion, escalate-to-Frank above DPI G1. +3. Review cadence: a shared monthly session against his own snapshot (aggregates), with the R5 clause said out loud once per session — the point is his judgment compounding, not his dependence. + +--- + +*This is system architecture, not financial / investment / tax advice. Outputs frame decisions; jurisdiction-specific counsel signs off on instruments. The practitioner accepts capital risk; the substrate accepts no claim.* + +**Built on SIP** — Investment Intelligence RUNBOOK.md · v0.1 diff --git a/verticals/investment-intelligence/SKILL.md b/verticals/investment-intelligence/SKILL.md new file mode 100644 index 00000000..738eec9f --- /dev/null +++ b/verticals/investment-intelligence/SKILL.md @@ -0,0 +1,67 @@ +--- +name: investment-intelligence +description: Vertical-tier skill auto-activates when working inside verticals/investment-intelligence/ or invoking any /invest-* command. Enforces R5 non-advisory clause, blind-parallel → debate → risk-gate pipeline order, human-gate-above-DCA, fail-closed execution, and trajectory write-back. +triggers: + - keywords [invest, investment, portfolio, allocation, stocks, etf, etfs, rebalance, trade, brokerage, dca] + - command-prefix /invest-* + - working-directory verticals/investment-intelligence/ +--- + +# SKILL — Investment Intelligence (vertical-tier) + +> Loads when working inside Investment Intelligence. Enforces the wrapper rules so every session inherits them — pipeline order, refusal patterns, R5 clause, gate discipline, memory write-back. + +--- + +## When this skill fires + +- Working inside `verticals/investment-intelligence/` +- Invoking `/invest-strategy`, `/invest-thesis-debate`, `/invest-snapshot`, `/invest-retro` +- Any ask about portfolio allocation, position sizing, rebalancing, brokerage execution, or trade recommendations within the Starlight context + +## Inheritance (every session inherits these) + +1. **R5 non-advisory clause inline** (mandatory, non-waivable): + > *This is system architecture, not financial / investment / tax advice. Outputs frame decisions; jurisdiction-specific counsel signs off on instruments. The practitioner accepts capital risk; the substrate accepts no claim.* + +2. **Pipeline order is structural.** Analysis (blind-parallel) → debate → risk (veto-on-size-not-direction) → synthesis → gate. No stage skipped; no straight-to-recommendation path. If asked to shortcut ("just tell me what to buy"), refuse the shortcut and offer the pipeline. + +3. **Human gate above DCA.** Any non-DCA TradeIntent terminates in a pending-approval object. The skill never claims an order was placed; it reports the pending state and the approval mechanism. + +4. **Fail-closed.** Ambiguity in an intent, missing cap, unparseable data → reject with a one-line, actionable reason (verdict style ports from `payment-intelligence-system`). + +5. **Trajectory write-back.** Any acted-on recommendation gets a trajectory record (`engine/schemas/trajectory.schema.json`). Retros read them; agent `calibration_notes` are updated from them. + +6. **Data classification (ROUTING.md).** Real balances/positions/tax data are T0 — if the current session is not running on a T0 (local) model, work from aggregates and placeholders, never raw private figures. + +7. **Refusal patterns:** concrete-stat-without-source · alpha/guarantee vocabulary · execution-as-output (beyond the gate) · cycle-blind sizing (regime thesis required) · advisory framing · credentials in any artifact. + +8. **Attestation discipline:** every shipped artifact carries the "Built on SIP" block (ambient per v7.4). + +## MCP-shape declaration (export hook, Board (c) pattern) + +Real tool surface, shipped at `mcp/trade-gate/` (v0.1, TypeScript, tested): + +```yaml +mcp_tools_shipped: + - propose_trade # TradeIntent in → verdict: auto-approved (DCA, capped) | pending | rejected + - request_approval # human issues single-use approval token for a pending intent + - list_pending # pending-approval queue + - execute_approved # approved intent → broker adapter (paper default; live = NOT_WIRED stub in-repo) + - read_audit # append-only JSONL audit trail +mcp_resources_planned: + - trajectories: ReasoningBank store (LanceDB target, W23+ Memory Palace mount) +sibling_repo_target: github.com/frankxai/investment-intelligence-system +``` + +## Process when invoked + +1. **Load context** — `SOUL.md` non-negotiables, `ROUTING.md` tier for the data classes in play, `engine/agents/catalog.json` for the layer contract, current portfolio snapshot ref (aggregate unless T0). +2. **Verify R5 clause** is in the output template before execution. Missing → abort and flag. +3. **Run the pipeline stage** the command names — never more (a snapshot is not a strategy session). +4. **Terminate execution asks at the gate** — emit TradeIntent / pending state, never a fabricated fill. +5. **Write back** — trajectory record if an action was taken; session artifact to corpus; attestation block embedded. + +--- + +**Built on SIP** — Investment Intelligence SKILL.md (vertical-tier) · v0.1 · SIP v1.1.1 diff --git a/verticals/investment-intelligence/SOUL.md b/verticals/investment-intelligence/SOUL.md new file mode 100644 index 00000000..2e86dfd3 --- /dev/null +++ b/verticals/investment-intelligence/SOUL.md @@ -0,0 +1,55 @@ +# SOUL — Investment Intelligence + +> One page. The thing that, if lost, means Investment Intelligence is no longer itself — even if every agent still runs. + +--- + +## The one-sentence soul + +**Capital allocation practiced as system, not impulse — every recommendation produced by blind-parallel analysis pressure-tested through adversarial debate and a risk layer that can veto size but never direction, every execution path fail-closed behind a human approval token, every outcome written back as a trajectory the swarm learns from — and every artifact opening with the non-advisory clause that names what the substrate is and what it is not.** + +This is specific enough to test. A recommendation that skipped the debate violates it. A risk agent that vetoes *direction* violates it (risk shapes size and timing; conviction belongs to the thesis). A live order without an approval token violates it structurally — the code throws. An outcome never written back violates it (a swarm that doesn't learn is a slot machine with better typography). + +--- + +## R5 Non-advisory clause (universal, non-waivable, inline in every artifact) + +> *This is system architecture, not financial / investment / tax advice. Outputs frame decisions; jurisdiction-specific counsel signs off on instruments. The practitioner accepts capital risk; the substrate accepts no claim.* + +Per Board R5 close-out (2026-05-17): Crypto + Investment have higher blast-radius than People IS or Sound IS — discipline must be stricter, not laxer. The vertical refuses to soften this even when the user asks "just tell me what to buy." + +--- + +## What must never drift + +- **Blind-parallel analysis.** The 5 analysis agents run without seeing each other's output. Consensus formed before evidence is contamination; the debate stage exists to collide views *after* they form independently. +- **Veto-on-size-not-direction.** Risk agents can shrink or zero a position size and delay timing; they cannot flip a thesis. Mixing the two collapses the layer separation that makes the swarm auditable. +- **Human gate above DCA.** The only auto-approvable class is the pre-declared DCA whitelist — still capped, still audited. Everything else produces a pending-approval object and stops. "Reject on doubt" beats "approve and apologize" (ports from `payment-intelligence-system` doctrine). +- **Fail-closed, always.** Missing cap → reject. Unparseable intent → reject. Audit write fails → the action fails. No live broker wired → paper. The corruption mode is the quiet default-open. +- **Outcome-grounded memory.** Every acted-on recommendation gets a trajectory record (thesis→action→outcome→lesson). Retrospectives distill lessons into per-agent calibration notes. Absorbed from TradingAgents' reflection loop; provenance named in `docs/absorption/`. +- **Concrete-sounding-stat-without-source refused.** Universal corruption mode (named in Wealth IS, Crypto IS, Sound IS, `_template/SOUL.md`). A return projection without a sourced mechanism is not analysis — it is decoration. +- **Paper-first ladder.** Paper → live-with-caps → live. No strategy change skips the backtest/dry-run gate. Speed of execution is never the bottleneck worth optimizing; quality of the decision is. +- **Portfolio-as-body-of-work.** The unit is the portfolio architecture (sizing, correlation, liquidity tiers, custody tiers, entity placement), not the single position. Time and labor allocation compete with capital in the same thesis review — the DPI ledger frames both. +- **Voice-preserving.** Artifacts compose in the practitioner's voice via the Genius layer. Finfluencer cadence, sell-side-research voice, and doomer-macro tone all leak in by default if not actively refused. + +## What this vertical is NOT + +- **Not a trading bot.** See `engine/architecture/10-honest-limits.md` — required reading. +- **Not financial / investment / tax advice** (R5, universal). +- **Not an alpha claim.** No edge is represented. The compounding asset is decision hygiene + memory. +- **Not custody.** No keys, no credentials, no balances in this repo — ever. +- **Not a substitute for the practitioner's lived practice.** The artifacts are scaffolding; judgment operates them. + +## Tests for drift (per cycle close) + +1. Did every artifact open with the R5 clause inline? Missing-or-softened = drifted to advisory framing. +2. Pick three recommendations: did each trace through independent analysis → debate → risk gate? Any straight-to-recommendation path = drifted. +3. Did any live-execution path exist without an approval token this cycle? (The eval red-team's standing objective — see `starlight-evals` investment-gate lane.) +4. Were trajectories written for every acted-on recommendation, and did the retro read them? +5. Did the vertical refuse anything for soul reasons this cycle? If nothing was refused, the refusal layer is dormant. + +If any test fails for two consecutive cycles, stop, audit, restore — do not ship through drift. + +--- + +**Built on SIP** — Investment Intelligence SOUL.md · v0.1 · SIP v1.1.1 (R5 inline per 2026-05-17 board; spawn per 2026-07-02 board) diff --git a/verticals/investment-intelligence/STACK.md b/verticals/investment-intelligence/STACK.md new file mode 100644 index 00000000..d25fb721 --- /dev/null +++ b/verticals/investment-intelligence/STACK.md @@ -0,0 +1,55 @@ +# STACK — Investment Intelligence + +> Where this vertical sits and what wires into what. + +--- + +## Position + +Domain Sub-Stack beneath **Wealth IS** (row #2, ACL-only manifest per 2026-06-16 falsifier default). Sibling of **Crypto Intelligence**. Spawned 2026-07-02 (R4 gate passed). + +## Composition wiring + +| Input | From | Into | +|---|---|---| +| Cycle/regime thesis | Crypto IS House of Macro | analysis layer (macro-risk context) | +| Protocol theses | Crypto IS House of Research | analysis layer (crypto-dca, defi-yield) | +| Custody constraints | Crypto IS House of Sovereignty | risk layer (regulatory-risk) + execution constraints | +| Allocation outputs | Crypto IS House of Allocation | portfolio-manager context | +| DPI categories + gate ladder | `/wealth-dpi` | portfolio-snapshot context (freedom-path framing; time/labor vs capital) | +| Entity + jurisdiction structure | Operator instance (private wealth-ops data) | tax-optimizer context | +| Market/macro/DeFi data | `engine/adapters/` (OpenBB, FRED, DefiLlama) | analysis layer | + +| Output | To | +|---|---| +| TradeIntent (pending approval) | `mcp/trade-gate/` | +| Trajectory records | ReasoningBank store (LanceDB target; MCP resource for SIS Memory Palace at W23+) | +| Session artifacts | operator corpus (private) + sanitized archetypes (public examples) | +| Audit JSONL | compliance plane (`docs/OBSERVABILITY.md`) | + +## Execution stack (fail-closed ladder) + +``` +TradeIntent → trade-gate MCP → caps check → DCA-whitelist? ──yes→ execute (paper default), audit + │no + pending approval → HUMAN TOKEN → execute (paper default), audit +Live brokers: NOT_WIRED stubs in-repo. Operator wires locally per RUNBOOK.md: + Alpaca MCP V2 (paper→live) · IBKR AI-Instructions (native human approval tab) · Coinbase AgentKit (session caps) +``` + +## Sovereignty tiers (detail: `ROUTING.md`) + +T0 local (Hermes 4.3 / 14B + self-hosted Ghostfolio, Actual Budget, OpenBB ODP, Langfuse) for balance-touching data · T1 frontier (Sonnet/Opus/Haiku via gateway, cost caps, prompt caching) for public research · T2 OpenRouter Hermes for overflow. + +## Evals + observability + +- `starlight-evals` `investment-gate` red/blue lane — red objective: live order without approval / cap breach / R5 strip. +- Langfuse (self-hosted, OTel) traces every agent step; JSONL audit is the compliance record; `/invest-retro` is the learning loop; freqtrade appliance (operator-run) gates strategy changes via backtest/dry-run. + +## Export hook (Board open-question (c) pattern) + +Vertical-local files only; extraction to `github.com/frankxai/investment-intelligence-system` is a copy operation, not a refactor (operator runs `extract-to-public-repo.sh` from the private instance). The trade-gate MCP is self-contained under `mcp/trade-gate/`. + +--- + +**Built on SIP** — Investment Intelligence STACK.md · v0.1 · SIP v1.1.1 diff --git a/verticals/investment-intelligence/docs/OBSERVABILITY.md b/verticals/investment-intelligence/docs/OBSERVABILITY.md new file mode 100644 index 00000000..e38705e0 --- /dev/null +++ b/verticals/investment-intelligence/docs/OBSERVABILITY.md @@ -0,0 +1,28 @@ +# OBSERVABILITY — traces, compliance, learning + +> Three planes, three different questions. Traces answer "what did the agents do?"; the audit answers "what touched money-adjacent state, and who approved it?"; the learning loop answers "are we getting better?". + +--- + +## 1. Trace plane — Langfuse (self-hosted, OTel) + +- **What:** every agent step in a strategy session — prompts, tool calls, latencies, token costs — traced via OpenTelemetry into a self-hosted Langfuse (MIT). OTLP endpoint means no framework lock-in. +- **Why self-hosted:** trade reasoning over private aggregates never leaves the box. (LangSmith rejected: closed SaaS, self-host is Enterprise-only.) +- **Targets per session** (from the engine's AI-engineering doctrine): cost per session, cache-hit rate on the stable prefix, per-layer latency, error rate. Alert channel: operator's messaging gateway, aggregates only. + +## 2. Compliance plane — the trade-gate audit + +- **What:** append-only JSONL (`audit.jsonl` + `spend.jsonl` + `approvals.jsonl` + `gate.jsonl`) under `TRADE_GATE_DATA_DIR`. Every propose/approve/deny/execute/failure, durable across restarts, never edited. +- **Reconciliation rule** (enforced by `/invest-retro`): every `executed` entry traces to either an `auto-approved` DCA event or a `token-issued` approval by a named human. A gap is an incident, not a footnote. +- **Red-team standing objective** (`starlight-evals` investment-gate lane): obtain an `executed` entry without its approval lineage — the blue side is `e2e.test.ts`. + +## 3. Learning plane — trajectories + retro + backtest + +- **Trajectories:** every acted-on recommendation writes thesis→action→outcome→lesson (`engine/schemas/trajectory.schema.json`). Store target: LanceDB; mounted as an MCP resource for the SIS Memory Palace (W23+ per the memory-architecture doctrine). +- **Retro:** `/invest-retro` scores decisions against realized outcomes (outcome-grounded reflection, absorbed from TradingAgents — see `absorption/tradingagents.md`), updates per-agent `calibration_notes`, and proposes systemic changes as human-reviewed diffs. +- **Backtest gate:** any strategy change re-enters at paper via the freqtrade appliance's backtest + dry-run loop before touching even paper cadence (see `RUNBOOK.md`). The paper-first ladder is the regression suite. +- **Calibration horizon:** per-agent calibration becomes meaningful at ~50+ trajectories; before that, lessons are directional, not statistical — the retro says so explicitly (honest-limits discipline). + +--- + +**Built on SIP** — Investment Intelligence OBSERVABILITY.md · v0.1 diff --git a/verticals/investment-intelligence/docs/absorption/ai-hedge-fund.md b/verticals/investment-intelligence/docs/absorption/ai-hedge-fund.md new file mode 100644 index 00000000..601850ed --- /dev/null +++ b/verticals/investment-intelligence/docs/absorption/ai-hedge-fund.md @@ -0,0 +1,23 @@ +# Absorption record — ai-hedge-fund (virattt) + +> Provenance discipline: what was absorbed, from where, under what license, into which files. + +- **Source:** `github.com/virattt/ai-hedge-fund` (MIT, ~60k★ as of 2026-07). Educational multi-agent system: investor-persona agents (Buffett, Munger, Graham, Damodaran, Ackman, Burry, …) + valuation/sentiment/fundamentals/technicals agents + risk manager + portfolio manager + backtester. Explicitly no real execution. +- **Verdict (2026-07-02 review):** absorb selectively — the persona-lens idea and the layer confirmation; skip the runtime. + +## What was absorbed, and where it landed + +| ai-hedge-fund pattern | Landed in | +|---|---| +| **Investor-persona agents as analytical lenses** | `AGENTS.md` § persona overlays — optional debate-diversity lenses (value / contrarian / quality / deep-value-short) layered onto the analysis stage of `/invest-thesis-debate`. Personas are archetypal stances, not celebrity imitations; they never bypass the risk layer or the gate. | +| **Analyst → risk manager → portfolio manager layering** | Corroborates the engine's native 3-layer topology (independent convergence — both trace to sane hedge-fund org design). | + +## What was deliberately NOT absorbed + +- The Python runtime and its LLM plumbing — the engine's model routing (`ROUTING.md`) already covers this provider-agnostically. +- The backtester — freqtrade (appliance, GPL-3.0 kept at arm's length) is the maintained backtest/dry-run loop; see `RUNBOOK.md`. +- Named-person personas as marketing surface — the substrate uses functional lens names; a fork may skin them as it likes under its own responsibility. + +--- + +**Built on SIP** — absorption record · ai-hedge-fund · 2026-07-02 diff --git a/verticals/investment-intelligence/docs/absorption/tradingagents.md b/verticals/investment-intelligence/docs/absorption/tradingagents.md new file mode 100644 index 00000000..3c4d7648 --- /dev/null +++ b/verticals/investment-intelligence/docs/absorption/tradingagents.md @@ -0,0 +1,25 @@ +# Absorption record — TradingAgents (Tauric Research) + +> Provenance discipline: what was absorbed, from where, under what license, into which files. Cross-asset/composition rules require named lineage (Wealth IS refusal pattern) — the same applies to external absorption. + +- **Source:** `github.com/TauricResearch/TradingAgents` (Apache-2.0, ~87k★ as of 2026-07). Multi-agent LLM trading-firm simulation: analyst roles → bull/bear researcher debate → trader → risk management. +- **Verdict (2026-07-02 review):** absorb the *patterns*; do not vendor or fork the code. The runtime is Python and simulation-shaped; our engine is markdown/schema/MCP-shaped. + +## What was absorbed, and where it landed + +| TradingAgents pattern | Landed in | +|---|---| +| **Bull/bear researcher debate** before a trade decision | `/invest-thesis-debate` (`engine/commands/thesis-debate.md` + `.claude/commands/invest-thesis-debate.md`) — argued from independently-formed (blind-parallel) analysis stances | +| **Risk layer as a distinct role that constrains the trader** | Already native to the engine (risk layer, veto-on-size-not-direction) — TradingAgents corroborates the separation rather than sourcing it | +| **Persistent decision log with outcome-grounded reflections** | `engine/schemas/trajectory.schema.json` usage discipline: every acted-on recommendation writes thesis→action→outcome→lesson; `/invest-retro` scores decisions against realized outcomes and feeds `calibration_notes` | +| **Structured-output decision agents** | Output contracts in `engine/agents/catalog.json` (`output_schema` per agent) | + +## What was deliberately NOT absorbed + +- Autonomous trade execution in any form — our gate doctrine forbids it (human token above DCA). +- The simulation harness and its market-replay loop — freqtrade (run as an appliance) covers backtesting with a mature, maintained loop; see `RUNBOOK.md`. +- Any signal or strategy content — patterns only, no alpha claims (SOUL.md). + +--- + +**Built on SIP** — absorption record · tradingagents · 2026-07-02 diff --git a/verticals/investment-intelligence/engine/adapters/README.md b/verticals/investment-intelligence/engine/adapters/README.md new file mode 100644 index 00000000..c159da1e --- /dev/null +++ b/verticals/investment-intelligence/engine/adapters/README.md @@ -0,0 +1,62 @@ +# Data Feed Adapters + +The substrate is **adapter-neutral**: agents declare what data they need; adapters supply it. This directory defines the patterns for the most common adapters. Operators implement; substrate documents. + +## Adapter contract + +```typescript +interface DataAdapter { + name: string; // unique identifier + description: string; + cadence: "daily" | "weekly" | "ondemand"; + authRequired: boolean; + freeTier: boolean; + rateLimits: { requestsPerMinute?: number; requestsPerDay?: number }; + + fetch(opts: { since?: ISO8601; symbols?: string[] }): Promise; + normalize(raw: T): NormalizedOutput; + cache?: { ttlSeconds: number }; +} +``` + +## Reference adapters + +The substrate ships **patterns** for these adapters — not working implementations. Operators review and adapt to their environment. + +| Adapter | Tier | Auth | Free | Used by | +|---|---|---|---|---| +| [fred/](./fred/) | 0+ | API key | Yes | macro-risk | +| [defillama/](./defillama/) | 0+ | None | Yes | defi-yield | +| [coingecko/](./coingecko/) | 0+ | None | Yes | crypto-dca, technical | +| [alternative-me/](./alternative-me/) | 0+ | None | Yes | macro-risk, crypto-dca | +| [openbb/](./openbb/) | 1+ | Provider-specific | Mixed | fundamentals, technical | +| [glassnode/](./glassnode/) | 1+ | API key | Limited | macro-risk, crypto-dca | +| [cryptopanic/](./cryptopanic/) | 1+ | API key | Limited | researcher | + +## Why patterns, not code + +The OSS substrate documents the **contract** so operators implement consistently. Live API integrations have: + +- API keys (Tier-2 secrets — never in OSS) +- Rate-limit accounting (operator-specific) +- Caching strategy (varies by deployment) +- Retry/backoff logic (varies by environment) + +Operators implement these in their private repos using the patterns here as the reference. The substrate then ships pre-built adapter wrappers for popular CLIs (Claude Code, Cowork) that read from operator-private adapter implementations via standard interfaces. + +## Adding an adapter + +1. Create `adapters//README.md` with API contract + auth + rate limits + free tier + sample normalized output +2. Add to this file's table +3. Submit PR with example fixture data (sanitized — no real keys, no real positions) +4. Maintainers verify the adapter doesn't fingerprint contributors and merge + +## Adapter precedence (when operators have multiple) + +If an operator has multiple adapters for the same data class (e.g., FRED + Yahoo Finance both can return 10Y yield), order: + +1. **Paid > Free** (when paid is configured — usually higher quality) +2. **Authoritative > Aggregator** (FRED for macro, not Yahoo) +3. **Cache hits ≤ 24h ≥ Live API call** (reduce rate-limit pressure) + +The agents query through a unified adapter layer; precedence is configured once per operator deployment. diff --git a/verticals/investment-intelligence/engine/adapters/defillama/README.md b/verticals/investment-intelligence/engine/adapters/defillama/README.md new file mode 100644 index 00000000..a72ceb36 --- /dev/null +++ b/verticals/investment-intelligence/engine/adapters/defillama/README.md @@ -0,0 +1,95 @@ +# DefiLlama Adapter + +> **Best free DeFi data source.** Comprehensive TVL + yield data, no auth required. + +## Provider + +- URL: https://defillama.com +- API docs: https://defillama.com/docs/api +- Auth: none (public API) +- Rate limits: ~300 requests/5min (informal — be polite) +- Cost: free + +## Endpoints used + +| Endpoint | Returns | Used by | +|---|---|---| +| `/protocols` | All protocols + TVL | defi-yield | +| `/tvl/{protocol}` | TVL history for a protocol | defi-yield | +| `/yields/poolsBorrow` | Lending pool yields | defi-yield | +| `/stablecoins` | Stablecoin market caps | defi-yield, macro-risk | +| `/chains` | TVL by chain | defi-yield | + +## Normalized output + +```yaml +defi_context: + total_defi_tvl: 87.4 # USD billion + total_tvl_30d_change_pct: 4.2 + top_yield_pools: + - protocol: morpho + asset: USDC + apy: 5.8 + tvl: 1.2 # USD billion + audit_history: clean + - protocol: aave + asset: USDC + apy: 4.1 + tvl: 12.4 + audit_history: clean + stablecoin_market_cap: 167.2 + stablecoin_30d_change_pct: 1.8 + source: defillama + fetched_at: 2026-05-04T16:00:00+02:00 +``` + +## Cache strategy + +- Default TTL: 6 hours +- TVL changes slowly day-to-day; weekly cadence is fine with cache + +## Failure modes + +| Failure | Mitigation | +|---|---| +| API offline | Use cached data; flag stale | +| Protocol rename (rare) | Update protocol map; alert on lookup failure | +| Yield pool removed | Drop from top yields; surface in next session if previously held | + +## Sample fetcher (pattern) + +```python +# adapters/defillama/fetch.py +import httpx + +BASE = "https://api.llama.fi" + +def fetch_protocols(): + r = httpx.get(f"{BASE}/protocols", timeout=10) + r.raise_for_status() + return r.json() + +def fetch_yields(): + r = httpx.get(f"{BASE}/yields/poolsBorrow", timeout=10) + r.raise_for_status() + return r.json() + +def top_yield_pools(asset="USDC", limit=5): + pools = fetch_yields() + matching = [p for p in pools if p["symbol"] == asset] + return sorted(matching, key=lambda p: p["apy"], reverse=True)[:limit] +``` + +## Audit history check + +The defi-yield agent should not blindly trust DefiLlama on protocol risk. Each yield opportunity should pair with: +- Protocol audit status (manual research; not in DefiLlama) +- Historical incidents (rugs, exploits, hacks) +- Multi-source confirmation (CryptoSec, DefiLlama Hacks page) + +DefiLlama provides the metrics; the agent's persona (`agents/catalog.json`) requires the manual risk overlay. + +## Cross-references + +- `architecture/06-data-feeds.md` +- `agents/catalog.json` (defi-yield agent persona) diff --git a/verticals/investment-intelligence/engine/adapters/fred/README.md b/verticals/investment-intelligence/engine/adapters/fred/README.md new file mode 100644 index 00000000..5aa6c105 --- /dev/null +++ b/verticals/investment-intelligence/engine/adapters/fred/README.md @@ -0,0 +1,99 @@ +# FRED Adapter (Federal Reserve Economic Data) + +> **Best in class for macro context.** Free, authoritative, no rate-limit issues for weekly use. + +## Provider + +- URL: https://fred.stlouisfed.org +- API docs: https://fred.stlouisfed.org/docs/api/fred/ +- Auth: free API key — register at https://fred.stlouisfed.org/docs/api/api_key.html +- Rate limits: 120 requests/minute (default — generous) +- Cost: free + +## Series used by IIS + +| Symbol | Series | Used by | +|---|---|---| +| `DFF` | Effective Federal Funds Rate | macro-risk | +| `M2SL` | M2 Money Supply | macro-risk | +| `T10Y2Y` | 10Y - 2Y Treasury Spread | macro-risk | +| `CPIAUCSL` | Consumer Price Index | macro-risk, tax-optimizer | +| `DTWEXBGS` | Trade-Weighted USD Index (broad) | macro-risk | +| `UNRATE` | US Unemployment Rate | macro-risk (regime indicator) | + +## Normalized output + +```yaml +macro_context: + fed_funds_rate: 4.50 + m2_yoy: 4.1 # year-over-year % + yield_curve_2_10: -0.30 + cpi_yoy: 2.8 + dxy: 99.2 + unemployment: 3.9 + source: fred + fetched_at: 2026-05-04T16:00:00+02:00 + cache_age_hours: 0 +``` + +## Cache strategy + +- Default TTL: 24 hours +- M2/CPI: monthly publication — TTL 7 days fine +- Fed funds rate / DXY: daily publication — TTL 24 hours fine +- For weekly Strategy Sessions, cache hit is the norm + +## Failure modes + +| Failure | Mitigation | +|---|---| +| API key not configured | Fall back to manual macro input prompt | +| Rate-limit hit | Use cached data; mark stale > 7 days | +| Series renamed/deprecated | Maintain symbol map; alert on next pull failure | + +## Sample fetcher (pattern, not OSS code) + +```python +# adapters/fred/fetch.py — operator implements +import os +import httpx +from datetime import datetime, timedelta + +API_KEY = os.environ["FRED_API_KEY"] +BASE = "https://api.stlouisfed.org/fred" + +def fetch_series(series_id: str, since: str = None): + params = { + "series_id": series_id, + "api_key": API_KEY, + "file_type": "json", + "observation_start": since or (datetime.now() - timedelta(days=365)).strftime("%Y-%m-%d"), + } + r = httpx.get(f"{BASE}/series/observations", params=params, timeout=10) + r.raise_for_status() + return r.json() + +def fetch_macro_snapshot(): + return { + "fed_funds_rate": latest("DFF"), + "m2_yoy": yoy_change("M2SL"), + "yield_curve_2_10": latest("T10Y2Y"), + "cpi_yoy": yoy_change("CPIAUCSL"), + "dxy": latest("DTWEXBGS"), + "unemployment": latest("UNRATE"), + "source": "fred", + "fetched_at": datetime.utcnow().isoformat(), + } +``` + +## Why FRED specifically + +- **Authoritative.** Federal Reserve publishes the data. No middleman. +- **Free.** No paid tier required for weekly use. +- **Stable.** Series IDs rarely change; symbols are stable for decades. +- **Comprehensive.** ~800,000 series available; the IIS uses ~6. + +## Cross-references + +- `architecture/06-data-feeds.md` — overall data feed strategy +- `agents/catalog.json` — which agents consume FRED data diff --git a/verticals/investment-intelligence/engine/adapters/openbb/README.md b/verticals/investment-intelligence/engine/adapters/openbb/README.md new file mode 100644 index 00000000..35d5fac0 --- /dev/null +++ b/verticals/investment-intelligence/engine/adapters/openbb/README.md @@ -0,0 +1,99 @@ +# OpenBB Adapter + +> **The OSS Bloomberg terminal.** Unified Python SDK + REST API across stocks, ETFs, options, crypto, macro, alternatives. + +## Provider + +- URL: https://openbb.co +- Repo: https://github.com/OpenBB-finance/OpenBB +- License: AGPL-3.0 +- Auth: provider-specific (OpenBB itself is free; data providers vary) +- Cost: free for yfinance/FRED/Llama; paid for Polygon/Tiingo/Bloomberg + +## Why OpenBB + +- Unifies multiple data providers under one API +- Free providers (yfinance, FRED, DefiLlama) cover Tier 0-1 needs +- Natural language query layer simplifies common operations +- Local REST API mode (`openbb api`) makes it MCP-friendly + +## Providers integrated + +| Provider | What | Cost | +|---|---|---| +| yfinance | Stocks, ETFs, options, news | Free | +| FRED | Macro | Free | +| DefiLlama | DeFi TVL, yields | Free | +| FMP (Financial Modeling Prep) | Fundamentals | Free tier + paid | +| Polygon | Real-time + historical | Paid | +| Tiingo | Quality stock data | Paid | +| Intrinio | Fundamentals + alternatives | Paid | + +## Used by IIS + +| Agent | Use case | +|---|---| +| fundamentals | DCF, P/E, EV/EBITDA via FMP / yfinance | +| technical | RSI, MACD, Bollinger via TA-Lib through OpenBB | +| macro-risk | Cross-references FRED data with broader macro context | +| crypto-dca | Stock-correlation context (when relevant) | + +## Setup pattern + +```bash +# Operator's private setup +pip install openbb openbb-fred openbb-yfinance openbb-crypto + +# OR run as REST API server for MCP integration +pip install openbb[all] +openbb api # starts local FastAPI at http://localhost:6900 +``` + +## Sample fetcher (pattern) + +```python +# adapters/openbb/fetch.py — operator implements +from openbb import obb + +def fetch_macro(): + """Macro snapshot via FRED through OpenBB.""" + return obb.economy.fred_series( + symbol="M2SL,DFF,T10Y2Y,CPIAUCSL,DTWEXBGS", + start_date="2025-01-01" + ) + +def fetch_equity_fundamentals(ticker: str): + """Fundamentals for a single equity via FMP.""" + return { + "income": obb.equity.fundamental.income(symbol=ticker), + "ratios": obb.equity.fundamental.ratios(symbol=ticker), + "dcf": obb.equity.fundamental.dcf(symbol=ticker), + } + +def fetch_technical(ticker: str, days: int = 90): + """Technical indicators via OpenBB TA module.""" + return { + "rsi": obb.technical.rsi(symbol=ticker, length=14), + "macd": obb.technical.macd(symbol=ticker), + "bollinger": obb.technical.bbands(symbol=ticker), + } +``` + +## REST API mode (recommended for MCP integration) + +```bash +openbb api --host 127.0.0.1 --port 6900 +``` + +Then build a custom MCP that wraps `localhost:6900` (similar to Rotki MCP pattern in `architecture/07-mcp-integration.md`). + +## License consideration + +OpenBB is AGPL-3.0. If you use OpenBB code in a public-facing service, AGPL provisions apply to your service. For personal IIS use, AGPL is fine. For B2B or hosted versions of IIS, consider: +- Using OpenBB as a sibling service (REST API only — your code doesn't link OpenBB) +- Using individual provider APIs directly (yfinance, FRED) instead of OpenBB + +## Cross-references + +- `architecture/06-data-feeds.md` +- `architecture/07-mcp-integration.md` (custom MCP wrapping OpenBB REST) diff --git a/verticals/investment-intelligence/engine/agents/README.md b/verticals/investment-intelligence/engine/agents/README.md new file mode 100644 index 00000000..53d7b32c --- /dev/null +++ b/verticals/investment-intelligence/engine/agents/README.md @@ -0,0 +1,88 @@ +# Agents + +This directory defines the **11 strategy-team agents** that produce Strategy Sessions. + +## Files + +- `catalog.json` — full agent catalog with personas, schemas, tool budgets, recommended models +- `personas/` — long-form prompts per agent (operators customize per their portfolio) +- `README.md` — this file + +## How agents are invoked + +Each agent is a **structured prompt** running on whatever LLM the operator chooses. The substrate is model-agnostic; the catalog recommends models for cost/capability optimization but does not require them. + +```bash +# In any agent CLI session: +/weekly-strategy +# → loads architecture/04-multi-agent-team.md context +# → loads agents/catalog.json +# → runs Analysis layer in parallel (5 agents) +# → runs Risk layer sequentially (3 agents) +# → runs Synthesis layer (2 agents) +# → produces draft Strategy Session +# → waits for human verdict +``` + +## Customizing agents + +Operators almost always need to customize: + +1. **Macro agent's regime priors** — your view of which indicators matter +2. **Crypto-DCA's discipline rules** — your DCA cadence, asset list, pause conditions +3. **Tax-optimizer's jurisdiction** — replace NL overlay with yours +4. **Regulatory-risk's exchange list** — operator-private; never in OSS +5. **Risk-manager's defaults** — daily caps, drawdown tolerances, position-size limits + +Customize by: + +- Adding `agents/personas/.local.md` (gitignored for operators) +- Or forking the substrate and editing `catalog.json` for shareable team variants + +## Cost management + +Cost-per-session estimate (May 2026) at single-creator scale, with Claude pricing: + +| Layer | Model | Cost per session | +|---|---|---| +| Analysis (5 agents) | Sonnet 4.6 / Haiku 4.5 | $0.10-0.25 | +| Risk (3 agents) | Sonnet 4.6 | $0.10-0.20 | +| Synthesis (2 agents) | Opus 4.7 (PM) + Sonnet 4.6 (CoS) | $0.30-1.00 | +| Researcher (on-demand) | Sonnet 4.6 + tool calls | $0.05-0.20 | +| **Total per session** | | **$0.55-1.65** | + +Annual cost (52 weekly sessions + 4 quarterly): **~$30-90/year** at this scale. + +If you can't justify $30-90/year for the substrate, you probably aren't holding enough capital for the substrate to provide marginal value. + +## Routing across providers + +Use Vercel AI Gateway or LiteLLM proxy to route per-agent. Recommended: + +- Cost-sensitive layers (Analysis, Researcher) → Haiku 4.5 or Sonnet 4.6 +- Reasoning-critical layers (Portfolio Manager) → Opus 4.7 or GPT-5 +- Tool-use heavy (Researcher) → Sonnet 4.6 (reliable tool calling) + +Single-provider operators: Claude Sonnet 4.6 across all layers, upgrade Portfolio Manager to Opus 4.7 if budget allows. + +## Calibration + +Per `architecture/05-memory-architecture.md`, every retrospective scores agent calls. After 30+ retrospectives, you have calibration data: + +``` +agent: defi-yield +trajectories_scored: 47 +right: 18 +partially_right: 21 +right_for_wrong_reason: 5 +wrong: 3 +hit_rate_high_confidence: 71% +``` + +Use calibration to: +- Adjust the Portfolio Manager's weighting per agent +- Identify agents over-confidencing (high confidence calls failing > 30%) +- Identify agents systematically biased (always cautious, always aggressive) +- Retire or replace agents with persistent low hit rates + +This is the **closing of the loop**. Without calibration, the team is ceremony. diff --git a/verticals/investment-intelligence/engine/agents/catalog.json b/verticals/investment-intelligence/engine/agents/catalog.json new file mode 100644 index 00000000..25637edb --- /dev/null +++ b/verticals/investment-intelligence/engine/agents/catalog.json @@ -0,0 +1,166 @@ +{ + "$schema": "../schemas/agent.schema.json", + "version": "1.0.0", + "team": "iis-strategy-team", + "description": "11 agent personas organized into Analysis, Risk, and Synthesis layers, plus a cross-cutting Researcher. Each agent is a structured prompt + tool budget + output contract, not a separately trained model.", + "agents": [ + { + "id": "macro-risk", + "layer": "analysis", + "domain": "Federal Reserve, macro regime, recession indicators", + "persona": "A skeptical macro analyst who pattern-matches the current regime against historical analogs. Skeptical of bull-case narratives that ignore yield-curve inversions, M2 contractions, or DXY strength.", + "system_prompt_summary": "Read FRED macro snapshot. Classify regime (risk-on, risk-off, late-cycle, recession-pending). Surface 1-3 macro flags that should influence position sizing this week.", + "input_schema": ["macro_context", "portfolio_snapshot_ref"], + "output_schema": ["stance", "evidence", "confidence"], + "tools_allowed": ["fred-adapter", "researcher"], + "tools_denied": ["execution-platforms", "credential-stores"], + "recommended_model": "claude-sonnet-4-6", + "calibration_notes": "Tends to over-weight recession indicators in late-cycle regimes. Track whether recession calls preceded actual recessions in retrospectives." + }, + { + "id": "crypto-dca", + "layer": "analysis", + "domain": "Crypto base case, DCA discipline, dominance trends", + "persona": "A DCA disciplinarian who defends the long-term thesis against short-term volatility. Resists FOMO and panic-sell narratives equally.", + "system_prompt_summary": "Read crypto market context (BTC/ETH 7d/30d/90d returns, dominance, F&G). Recommend hold/pause/accelerate DCA. Flag if BTC dominance is shifting materially.", + "input_schema": ["macro_context.fear_greed_index", "macro_context.btc_dominance", "portfolio_snapshot_ref"], + "output_schema": ["stance", "evidence", "confidence"], + "tools_allowed": ["coingecko-adapter", "defillama-adapter", "researcher"], + "tools_denied": ["execution-platforms", "credential-stores"], + "recommended_model": "claude-sonnet-4-6", + "calibration_notes": "Tends to under-weight regime changes. Track whether 'hold DCA' calls held up during 2022-style 60% drawdowns." + }, + { + "id": "defi-yield", + "layer": "analysis", + "domain": "DeFi yield opportunities, protocol risk", + "persona": "A yield analyst who treats every protocol as guilty until audit-proven innocent. Prioritizes protocol TVL stability + audit history over headline APY.", + "system_prompt_summary": "Read DefiLlama TVL + yield data. Surface top 3 yield opportunities with explicit risk profile (smart contract, counterparty, liquidity). Flag protocols with TVL declining > 20% in 30d.", + "input_schema": ["portfolio_snapshot_ref", "open_theses"], + "output_schema": ["stance", "evidence", "confidence"], + "tools_allowed": ["defillama-adapter", "researcher"], + "tools_denied": ["execution-platforms", "credential-stores"], + "recommended_model": "claude-sonnet-4-6", + "calibration_notes": "Yield decay is the most common thesis failure. Track 30/60/90d realized yield vs expected at thesis-open." + }, + { + "id": "fundamentals", + "layer": "analysis", + "domain": "Equity fundamentals, DCF, multiples", + "persona": "A patient value-oriented analyst who applies DCF and multiple-comparison rigorously. Skeptical of growth narratives without unit economics.", + "system_prompt_summary": "For each held equity, compute or retrieve P/E, EV/EBITDA, FCF yield. Compare to sector medians. Flag positions where multiples have expanded materially without earnings support.", + "input_schema": ["portfolio_snapshot_ref"], + "output_schema": ["stance", "evidence", "confidence"], + "tools_allowed": ["openbb-adapter", "finance-toolkit", "researcher"], + "tools_denied": ["execution-platforms", "credential-stores"], + "recommended_model": "claude-sonnet-4-6", + "calibration_notes": "Useful only when the operator holds individual equities. Skip if portfolio is index-only + crypto." + }, + { + "id": "technical", + "layer": "analysis", + "domain": "Price action, support/resistance, momentum", + "persona": "A pattern-aware technical analyst who treats indicators as evidence not gospel. Knows that retail technical analysis underperforms over long horizons.", + "system_prompt_summary": "Compute RSI, MACD, Bollinger Bands on held positions. Identify trend, support, resistance. Flag mean-reversion opportunities and momentum-confirmation signals.", + "input_schema": ["portfolio_snapshot_ref"], + "output_schema": ["stance", "evidence", "confidence"], + "tools_allowed": ["ta-lib-adapter", "openbb-adapter"], + "tools_denied": ["execution-platforms", "credential-stores"], + "recommended_model": "claude-haiku-4-5", + "calibration_notes": "Lowest-confidence agent by design. Useful for flagging extremes (RSI > 75 or < 25), not for direction calls." + }, + { + "id": "risk-manager", + "layer": "risk", + "domain": "Position sizing, drawdown discipline, correlation", + "persona": "A drawdown-survivor who has watched portfolios blow up and won't let yours. Treats every analysis-layer stance as 'right but how much?' problem.", + "system_prompt_summary": "Read all Analysis-layer stances + portfolio snapshot. Recommend size limits per proposed action. Surface correlation risk if multiple stances point to correlated assets. Enforce drawdown discipline if recent realized drawdown > 15%.", + "input_schema": ["agent_debate.analysis", "portfolio_snapshot_ref"], + "output_schema": ["stance", "evidence", "confidence", "counter_to"], + "tools_allowed": ["pyportfolioopt-adapter"], + "tools_denied": ["execution-platforms", "credential-stores"], + "recommended_model": "claude-sonnet-4-6", + "calibration_notes": "Hit rate on size recommendations is what compounds. Track whether sizes recommended were materially exceeded by operator and how those positions performed." + }, + { + "id": "tax-optimizer", + "layer": "risk", + "domain": "Jurisdiction-specific tax (NL Box 3, BV, DGA defaults)", + "persona": "A tax-aware position planner who knows the calendar matters more than the alpha. Pre-Jan 1 valuations, BV rotation timing, DGA borrowing thresholds.", + "system_prompt_summary": "Read tax overlay for operator's jurisdiction. Apply to portfolio + proposed actions. Surface tax-driven actions (rotate before Jan 1, distribute before Box 2 threshold change, etc.).", + "input_schema": ["portfolio_snapshot_ref", "proposed_actions", "tax_overlay"], + "output_schema": ["stance", "evidence", "confidence"], + "tools_allowed": ["tax-overlay-reader"], + "tools_denied": ["execution-platforms", "credential-stores"], + "recommended_model": "claude-sonnet-4-6", + "calibration_notes": "Surfacing one tax mistake / year > most alpha claims. Track whether tax-flag actions were taken and what the savings were." + }, + { + "id": "regulatory-risk", + "layer": "risk", + "domain": "DAC8/CARF, KYC/AML, exchange concentration", + "persona": "A compliance-aware risk reviewer who watches for regulatory pressure on platforms (Binance EU, KuCoin US, etc.) and operator's exposure to it.", + "system_prompt_summary": "Read exchange list (operator-private). Cross-reference against current regulatory news (researcher). Flag if any exchange has ongoing regulatory pressure or pending licensing actions. Flag concentration risk if any single platform > 30% of crypto holdings.", + "input_schema": ["portfolio_snapshot_ref.exchange_distribution"], + "output_schema": ["stance", "evidence", "confidence"], + "tools_allowed": ["researcher", "webfetch"], + "tools_denied": ["execution-platforms", "credential-stores"], + "recommended_model": "claude-sonnet-4-6", + "calibration_notes": "Regulatory flags should precede platform restrictions by 4-12 weeks to be useful." + }, + { + "id": "portfolio-manager", + "layer": "synthesis", + "domain": "Final ranked opportunities, proposed actions", + "persona": "The decision-maker who weighs Analysis stances against Risk-layer friction. Outputs ranked opportunities with explicit conditions and human-approval gates.", + "system_prompt_summary": "Read complete Analysis + Risk debate. Produce ranked_opportunities[] with size, confidence, risk level. Produce proposed_actions[] with conditions, deadlines, requires_human_approval flag.", + "input_schema": ["agent_debate"], + "output_schema": ["ranked_opportunities", "proposed_actions"], + "tools_allowed": ["all-prior-stances"], + "tools_denied": ["execution-platforms", "credential-stores"], + "recommended_model": "claude-opus-4-7", + "calibration_notes": "Most important calibration target. Hit rate on ranked_opportunities[?].confidence='high' should compound > 60%." + }, + { + "id": "chief-of-staff", + "layer": "synthesis", + "domain": "Session quality, coherence, document rendering", + "persona": "The synthesis quality reviewer. Ensures the rendered Strategy Session references prior sessions correctly, doesn't contradict itself, and flags incoherence in the debate.", + "system_prompt_summary": "Read full debate + portfolio-manager output + last 4 sessions. Render the final Strategy Session. Flag any contradiction with prior thesis or recent retrospective lessons.", + "input_schema": ["agent_debate", "portfolio_manager_output", "recent_sessions[4]"], + "output_schema": ["rendered_session_md", "coherence_flags"], + "tools_allowed": ["session-corpus-reader", "thesis-log-reader"], + "tools_denied": ["execution-platforms", "credential-stores"], + "recommended_model": "claude-sonnet-4-6", + "calibration_notes": "Coherence flags are signal — track whether sessions with coherence_flags > 0 had worse outcomes." + }, + { + "id": "researcher", + "layer": "cross-cutting", + "domain": "Primary-source verification on demand", + "persona": "A skeptical researcher who treats LLM training data as untrustworthy and primary sources as the only ground truth.", + "system_prompt_summary": "Given a claim from any agent, verify against primary sources. Return cited summary with URL + access date. Reject claims that can't be cited.", + "input_schema": ["claim", "context"], + "output_schema": ["summary", "sources[]", "access_date", "confidence"], + "tools_allowed": ["webfetch", "websearch", "exa-mcp", "context7-mcp"], + "tools_denied": ["execution-platforms", "credential-stores"], + "recommended_model": "claude-sonnet-4-6", + "calibration_notes": "If researcher returns 'no primary source found,' the claim it's verifying must be flagged as unsupported in the session. Don't let agents reason from unsupported claims." + } + ], + "handoff_protocol": { + "step_1_parallel": ["macro-risk", "crypto-dca", "defi-yield", "fundamentals", "technical"], + "step_2_sequential": ["risk-manager", "tax-optimizer", "regulatory-risk"], + "step_3_sequential": ["portfolio-manager", "chief-of-staff"], + "researcher": "available to all agents at all steps" + }, + "extension_guide": { + "to_add_an_agent": "Define persona, system_prompt_summary, input/output schemas, tool budget. Submit PR. Document calibration approach.", + "examples_of_extensions": [ + "nft-floor-risk (for NFT-heavy operators)", + "commodity-trends (for inflation-hedge operators)", + "sovereign-bond (for IBKR + EU bond ladder users)", + "private-equity-hold (for unlisted positions)" + ] + } +} diff --git a/verticals/investment-intelligence/engine/architecture/00-overview.md b/verticals/investment-intelligence/engine/architecture/00-overview.md new file mode 100644 index 00000000..5b7d2aa4 --- /dev/null +++ b/verticals/investment-intelligence/engine/architecture/00-overview.md @@ -0,0 +1,164 @@ +# Architecture Overview + +## The premise + +The compounding edge for a part-time investor is not alpha. It is **not making catastrophic decisions** — panic-sells in drawdowns, FOMO buys at tops, position-size errors, tax mistakes, exchange concentration risk, key-management failures. + +A multi-agent debate engine + a structured journal + a human-gated execution layer + a memory of past theses outperforms intuition alone — for the same reason cockpit checklists outperform pilot intuition alone, and code review outperforms solo developer judgment. + +This substrate imposes that discipline. + +## The atomic unit + +The **Strategy Session** is the atomic unit of the IIS. Every operation in the system either produces or consumes a Session. + +A Session is a closed, dated, JSON-validated artifact containing: + +```yaml +session: + id: 2026-W19 # ISO week or specific date for ad-hoc + date: 2026-05-10 # Sunday of the review week + mode: weekly | rebalance | thesis-debate | retrospective + author: # Never real-identity in OSS + macro_context: + fed_funds_rate: 4.50 + dxy: 99.2 + m2_yoy: 4.1 + yield_curve_2_10: -0.30 + fear_greed_index: 72 + btc_dominance: 54.0 + notes: "Macro tightening continues, risk-on still dominant" + portfolio_snapshot_ref: snapshot-2026-W19 # references private snapshot, not embedded + agent_debate: + - agent: macro-risk + stance: "Tighten cash buffer; recession indicators amber" + evidence: ["yield_curve_2_10: -0.30", "fed_funds 4.50%"] + confidence: medium + - agent: crypto-dca + stance: "Continue weekly DCA at current levels" + evidence: ["BTC 200d MA breached", "fear_greed 72 (greed)"] + confidence: high + - agent: tax-optimizer + stance: "Pre-Jan 1: consider rotating BTC to BV before Box 3 valuation" + evidence: ["Box 3 effective rate 2.16%", "BV VPB 19% on realized only"] + confidence: high + - agent: risk-manager + stance: "Drawdown discipline: hold stops; do not chase" + evidence: ["last 30d max drawdown: -8%", "no thesis violations"] + confidence: high + ranked_opportunities: + - thesis_id: thesis-2026-W19-01 + summary: "Increase USDC yield via Morpho lending" + size_suggested_pct: 5 + confidence: medium + risk_level: low + conditional_on: "Morpho TVL > $X, smart contract audit confirmed" + risk_flags: + - "Binance regulatory pressure ongoing — verify exchange concentration < 30%" + - "DAC8 reporting active for 2026 — ensure Q1 export complete" + proposed_actions: + - action: "Rotate 5% portfolio to USDC yield position" + conditional_on: "Above thesis conditions" + deadline: 2026-05-17 + requires_human_approval: true + human_verdict: pending # approve | modify | reject | pending + outcome_tracking: + filled_after: 2026-08-10 # 90 days post-action + realized_outcome: null # filled retrospectively + lessons: null # filled retrospectively +``` + +Why the Strategy Session is the right atomic unit: +- It captures the **debate transcript**, not just the conclusion +- It is **auditable** retrospectively +- It is **measurable** (proposed action → realized outcome) +- It is **framework-agnostic** (markdown + JSON, runs in any agent CLI) +- It produces **a corpus over time** that drives the learning loop + +## The five loops + +The substrate operates on five cadences. Each loop produces specific Session modes or related artifacts. + +| Loop | Cadence | Mode | Produces | +|---|---|---|---| +| Daily Pulse | Weekday morning | (no session) | Slack digest: top movers, renewals, due actions | +| Weekly Review | Sunday | `mode: weekly` | One Session/week, debate transcript, human-gated actions | +| Monthly Close | 1st of month | (snapshot) | Portfolio snapshot, runway recalc, Box 3 running total | +| Quarterly Tax | Jan/Apr/Jul/Oct | `mode: rebalance` | Rebalance Session, tax-scenario refresh | +| Annual Architecture | December | (full review) | Annual report, ARCHITECTURE.md updates, inheritance refresh | + +See `architecture/02-five-loops.md` for the full cadence spec. + +## The three tiers of security + +This is the most important architectural decision. Read `architecture/01-three-tier-security.md` carefully before any implementation. + +| Tier | What lives here | AI access | +|---|---|---| +| **Tier 1 — Vault** | Seed phrases, hardware-wallet PINs, Shamir shares | ZERO. Physical only. | +| **Tier 2 — Protected** | API keys with trade/withdraw permission, bank credentials, OTP seeds | ZERO. Encrypted local (KeePass/VeraCrypt). | +| **Tier 3 — Operational** | Read-only API keys, portfolio aggregation, snapshots, tax models | FULL READ. Limited write (tracking files only). NO signing, transfers, credential access. | + +**This boundary is non-negotiable.** Any contributor proposing to weaken it will be redirected to fork. + +## The three tiers of usage + +| Tier | Audience | Tooling | Outcome | +|---|---|---|---| +| 0 | Anyone | Markdown + agent CLI | Strategy Session journal | +| 1 | Intermediate | + OSS install (multi-agent debate, OpenBB) | Weekly debates + drift alerts | +| 2 | Advanced | + Live execution (AgentKit/Alpaca/Freqtrade) | Autonomous DCA + thesis-tracked execution | + +Tier 0 requires zero infrastructure. Tier 2 is engineering effort. **Most users live at Tier 1.** + +## The component map + +``` +┌─────────────────────────────────────────────────────────────┐ +│ INTELLIGENCE LAYER │ +│ Data Feeds (FRED, OpenBB, DefiLlama, Glassnode, F&G) │ +│ ↓ │ +│ Multi-Agent Team (11 personas, debate transcript) │ +│ ↓ │ +│ Strategy Session (markdown + JSON, schema-validated) │ +└─────────────────────────────────────────────────────────────┘ + ↓ HUMAN APPROVAL GATE +┌─────────────────────────────────────────────────────────────┐ +│ EXECUTION LAYER │ +│ Crypto: Coinbase AgentKit + Eliza OS │ +│ DCA/Rules: Hummingbot / Freqtrade (rule-bounded autonomy) │ +│ Stocks: Alpaca (paper → live) / IBKR (EU access) │ +└─────────────────────────────────────────────────────────────┘ + ↓ +┌─────────────────────────────────────────────────────────────┐ +│ MEMORY + RETROSPECTIVE │ +│ Session corpus → Thesis log → Outcome tracking │ +│ ↓ │ +│ ReasoningBank trajectory wiring (which agents called it │ +│ right? which framing held up? what biases recurred?) │ +│ ↓ │ +│ Distilled lessons → next Session's debate priors │ +└─────────────────────────────────────────────────────────────┘ + ↓ +┌─────────────────────────────────────────────────────────────┐ +│ PRIVATE OPERATIONAL TIER │ +│ Tier 1 vault, Tier 2 credentials, real positions │ +│ (NEVER in this repo. Always in user's private store.) │ +└─────────────────────────────────────────────────────────────┘ +``` + +## The honest framing + +Read `architecture/10-honest-limits.md` and `docs/why-not-trading-bot.md` before going further. The substrate is positioned as **decision-support and discipline-imposition**, not alpha-generation. This is not modesty; it is the empirical truth of what multi-agent + structured-journal systems actually deliver. + +If you want a trading bot, this is not the right substrate. Look at Freqtrade or Hummingbot directly. + +If you want a discipline that compounds over a 5-10 year horizon and survives bear markets without portfolio-blowing decisions, this is the substrate. + +## Next reads + +1. `architecture/03-strategy-session.md` — the atomic unit in full detail +2. `architecture/04-multi-agent-team.md` — the 11 personas +3. `architecture/05-memory-architecture.md` — the learning loop +4. `architecture/10-honest-limits.md` — what this DOES NOT promise +5. `PRIVACY-BOUNDARY.md` — what never goes in this repo diff --git a/verticals/investment-intelligence/engine/architecture/10-honest-limits.md b/verticals/investment-intelligence/engine/architecture/10-honest-limits.md new file mode 100644 index 00000000..661f5bc4 --- /dev/null +++ b/verticals/investment-intelligence/engine/architecture/10-honest-limits.md @@ -0,0 +1,123 @@ +# Honest Limits — What This Substrate DOES NOT Promise + +> **Read this before believing anything else in the repo.** + +The IIS substrate is positioned with maximum honesty about what it can and cannot do. This file is the contract; if any other documentation in this repo contradicts it, this file wins. + +--- + +## What this substrate IS + +✅ A **decision-support framework** that imposes structure on weekly wealth decisions +✅ A **discipline-enforcement scaffolding** that catches biases through structured debate +✅ A **journal that compounds** — sessions accumulate into a learning corpus +✅ A **tax-aware position planner** that surfaces jurisdiction-specific implications +✅ An **audit trail** of past decisions with retrospective outcome tracking + +--- + +## What this substrate IS NOT + +### ❌ NOT an alpha-generation system + +Multi-agent debate catches biases and surfaces trade-offs. **It does not generate market-beating returns.** + +The empirical truth: most retail "AI trading systems" lose money to brokers + slippage + bias + overconfidence. The compounding edge for a part-time investor is **not making catastrophic decisions** (panic-sells, FOMO buys, position-size errors, tax mistakes), more than alpha generation. + +If you measure this substrate by "did it beat the S&P 500," you are measuring the wrong thing. Measure it by: +- Did you avoid the panic-sell in the next 20% drawdown? +- Did you maintain DCA discipline through the next bear market? +- Did your cost-basis tracking save you from a tax mistake at year-end? +- Did the thesis-debate surface a position-size error before you took it? + +That's where the value lives. + +### ❌ NOT a trading bot + +The substrate operates on **weekly cadence** for the strategy layer. Even the execution layer (Tier 2) operates on rule-based DCA + thesis-driven positions, not high-frequency trading. + +If you want a trading bot, look at Freqtrade or Hummingbot directly. They are excellent at what they do. The IIS substrate would slow them down, not improve them. + +### ❌ NOT a robo-advisor + +Robo-advisors (Wealthfront, Betterment, etc.) have: +- Fiduciary regulatory standing +- Bucket-based risk-profile assignment +- Tax-loss harvesting automation +- Compliant onboarding flow + +The IIS substrate has none of those. It is **opinionated scaffolding** that you operate yourself, with full human responsibility for outcomes. + +### ❌ NOT financial advice + +Strategy Sessions, agent debates, tax-overlay outputs — none of this is financial, tax, or legal advice. It is **decision-support information** that you, the operator, integrate with advice from qualified human advisors. + +In the Netherlands, that means a `belastingadviseur` for tax and a registered `financieel adviseur` for mortgage and investment advice on regulated products. Equivalent professionals exist in your jurisdiction. + +### ❌ NOT a guarantee of returns + +Past Strategy Session quality, agent debate sophistication, retrospective hit rates — none predict future returns. Markets are markets. Risk is risk. The substrate makes you more disciplined, not more clairvoyant. + +### ❌ NOT a substitute for understanding + +The substrate runs in your agent CLI. The agent CLI runs an LLM. LLMs hallucinate, especially on numerical data, recent regulatory changes, and edge cases. **Verify the agent's claims against primary sources** before acting. + +The `researcher` agent is required to cite primary sources for a reason: agents lie when they don't have grounded data. Calibrate your trust accordingly. + +### ❌ NOT secure if you skip the security model + +The 3-tier security model (`01-three-tier-security.md`) is the architecture. If you collapse it — putting trade-permission API keys in `.env` files the agent reads, or storing seeds in cloud-synced directories — the substrate provides no protection. The architecture is the security; you have to operate it. + +--- + +## What this substrate MIGHT do (no guarantees) + +🟡 **Save you from one catastrophic decision** in a 5-10 year horizon. (One panic-sell avoided in a 50% drawdown often equals a decade of "alpha.") + +🟡 **Catch one tax mistake** before year-end (Box 3 timing, BV rotation, DAC8 reporting). + +🟡 **Surface one position-size error** before you make it (rebalancing discipline catches drift). + +🟡 **Identify one biased pattern in your thinking** through the retrospective layer (e.g., "I always size down when X says high confidence — is that good discipline or systematic under-investment?"). + +These are the four outcomes the substrate **plausibly** delivers. None are guaranteed. All require running the substrate consistently for 12+ weeks before the corpus has anything to say. + +--- + +## What you must accept to use this substrate + +1. You are responsible for every decision. No agent. No system. **You.** +2. The substrate is informational. Advice comes from qualified humans, not from this repo. +3. Outcome tracking is honest. Lessons are unflattering when they are. Don't edit history. +4. Privacy boundary is non-negotiable. Tier 1/2 secrets never enter Tier 3. +5. You will run the Sunday session for at least 12 weeks before judging effectiveness. + +If you can't accept those, this substrate is not for you. Use a robo-advisor. + +--- + +## What contributors must accept + +1. PRs that promise "alpha" or "returns" are rejected. +2. PRs that weaken the security model (Tier 1/2/3 boundary) are rejected. +3. PRs that add complexity without clear retrospective-learning value are rejected. +4. PRs that hardcode jurisdiction-specific amounts in agent prompts (rather than tax overlays) are rejected. +5. PRs that ship working trading code (rather than spec + adapter pattern) are rejected — that belongs in operator-private repos. + +--- + +## The empirical claim + +Multi-agent debate + structured journal + outcome retrospective + tax overlay >> intuition alone, **for the four plausible outcomes above.** + +This claim is testable in your own corpus over 50+ sessions. The substrate makes the test possible. The test result is your truth. + +--- + +## The motto + +> Discipline beats intuition. Process beats prediction. Audit beats optimism. + +Internalize that, and the substrate gives you what it can give you. + +Reject it, and look elsewhere — there are better tools for whatever else you want. diff --git a/verticals/investment-intelligence/engine/architecture/README.md b/verticals/investment-intelligence/engine/architecture/README.md new file mode 100644 index 00000000..374940a7 --- /dev/null +++ b/verticals/investment-intelligence/engine/architecture/README.md @@ -0,0 +1,12 @@ +# Engine architecture — promoted subset + +This directory carries the two load-bearing architecture docs from the IIS engine: + +- `00-overview.md` — the system at a glance. +- `10-honest-limits.md` — **required reading** before operating anything here. No alpha guarantee; what this system cannot do. + +The full 12-doc set (three-tier security, five loops, strategy session, multi-agent team, memory architecture, data feeds, MCP integration, execution layer, tax overlays, AI engineering) lives in the private operator instance and ships with the public extraction (`github.com/frankxai/investment-intelligence-system`). The substrate copies only what the vertical's contracts reference directly; deep-dive material follows via the export hook rather than being duplicated here. + +Cross-references inside the two copied docs that point at sibling numbers (01–09, 11) resolve in the operator instance / public extraction, not in this subset. + +**Built on SIP** — engine architecture subset · v0.1 diff --git a/verticals/investment-intelligence/engine/commands/portfolio-snapshot.md b/verticals/investment-intelligence/engine/commands/portfolio-snapshot.md new file mode 100644 index 00000000..3740b005 --- /dev/null +++ b/verticals/investment-intelligence/engine/commands/portfolio-snapshot.md @@ -0,0 +1,45 @@ +# /portfolio-snapshot + +Generate a fresh sanitized portfolio snapshot before running a Strategy Session. + +## Usage + +``` +/portfolio-snapshot [--source rotki|notion|manual] +``` + +## What this command does + +Pulls current portfolio state from a configured source (Rotki MCP, Notion MCP, or manual input) and writes a snapshot at `snapshots/.md` (operator-private path; never the OSS repo for real data). + +Format conforms to `schemas/portfolio-snapshot.schema.json`: +- Total value (or `redacted` for shareability) +- Allocation per asset class +- Target allocation +- Drift per class +- Entity split (personal / Operating BV / Holding BV / etc.) +- Exchange concentration (sanitized) + +## Required configuration + +For Tier 1+ runs: +- Rotki running locally + Rotki MCP configured (`07-mcp-integration.md`) +- OR Notion Wealth Command Hub with proper schema + Notion MCP + +For Tier 0: +- Manual input — agent prompts for current allocations + +## Output + +Writes `/snapshots/.md` with the portfolio snapshot. + +The Strategy Session references this snapshot via `portfolio_snapshot_ref`. The OSS repo never contains the real snapshot — only the reference. + +## Privacy + +Snapshots are operator-private by default. Configure the snapshot path outside the substrate repo. The substrate documents the schema; operators store the data. + +## See also + +- `architecture/01-three-tier-security.md` — why snapshots are Tier-3 only and what that means +- `PRIVACY-BOUNDARY.md` — what never goes in the OSS repo diff --git a/verticals/investment-intelligence/engine/commands/retrospective.md b/verticals/investment-intelligence/engine/commands/retrospective.md new file mode 100644 index 00000000..fd61205a --- /dev/null +++ b/verticals/investment-intelligence/engine/commands/retrospective.md @@ -0,0 +1,111 @@ +# /iis-retrospective + +Close a thesis 90+ days after action. Score the call. Distill the lesson. Update agent calibration. + +## Usage + +``` +/iis-retrospective + +Example: +/iis-retrospective morpho-yield-rotation-2026-W18 +``` + +## What this command does + +``` +1. READ original session that opened the thesis +2. READ original portfolio_snapshot_ref + macro context at thesis open +3. READ current portfolio + macro context (for delta) +4. COMPUTE realized outcome: + - Asset performance over the period + - Realized yield (if yield-driven thesis) + - Tax outcome (if tax-driven thesis) + - Risk realized (drawdown, smart contract event, etc.) +5. SCORE thesis_held_up: confirmed | partially_confirmed | refuted +6. SCORE per-agent call quality: right | partially_right | right_for_wrong_reason | wrong | n/a +7. SCORE human verdict quality: good_call | mixed | poor_call (with rationale) +8. DISTILL primary_lesson (1-3 sentences max) +9. WRITE retrospectives/.outcome.md (separate from session — append-only event) +10. WRITE trajectories/.json (ReasoningBank-shaped) +11. UPDATE theses/index.yaml: status:closed + outcome_recorded + thesis_held_up +12. UPDATE agents/catalog.json: increment per-agent calibration counters +``` + +## Why retrospectives matter + +The substrate's compounding value is the **learning loop**: + +``` +thesis → action → outcome (90 days) → lesson → next thesis's prior +``` + +Without retrospectives, the substrate is sophisticated theater. With them, your future weekly sessions have access to **your own past calibration data** — which agents to trust more, which biases recur in your verdicts, which input states correlate with bad outcomes. + +After 30+ retrospectives, the chief-of-staff agent surfaces relevant past lessons during weekly sessions: "Last time you faced F&G > 75 and approved a rotation, the rotation underperformed by 3% — consider tighter sizing." + +## Cadence + +- **Mandatory:** Run within 1 week of `outcome_due` date in the thesis index +- **Recommended:** Batch retrospectives quarterly during Loop 4 review +- **Skip not allowed:** A skipped retrospective breaks the calibration; chief-of-staff will flag the gap on the next weekly session + +## Output + +Writes: +- `retrospectives/.outcome.md` (human-readable retrospective) +- `trajectories/.json` (machine-readable, schema in `trajectory.schema.json`) +- Updated `theses/index.yaml` +- Updated `agents/catalog.json` (calibration counters) + +## Scoring honestly + +The most important discipline: **score honestly, not flatteringly.** + +- A "right_for_wrong_reason" call is a worse signal than a "wrong" call — the agent got lucky, and that pattern will fail when luck runs out. +- A "poor_call" verdict by you is the most valuable retrospective output — that's where you grow. + +If you find yourself rationalizing retrospectives to make outcomes look better, you're losing the substrate's value. The point is unflattering truth. + +## Example output + +``` +retrospectives/morpho-yield-rotation-2026-W18.outcome.md + +Thesis: Rotate €25K into Morpho USDC at 5.8% APY +Opened: 2026-05-04 +Closed: 2026-08-04 (90 days) +Held: 90 days + +Realized: + - Average APY: 4.2% (vs 5.8% expected) + - Total yield: €262 (vs €362 expected) + - No smart contract incident + - Withdrawal completed within 4 hours + +thesis_held_up: partially_confirmed + Rationale: position generated yield safely but APY decayed faster than expected. + Decay was visible in the 14-day rolling average by week 6 — the substrate's + defi-yield agent didn't surface this as a flag. + +Per-agent scoring: + defi-yield: partially_right (high confidence on entry, missed APY decay signal) + risk-manager: right (3% sizing was correct) + tax-optimizer: n/a + macro-risk: right (cash buffer at target was correct) + +Lesson: + Yield decay > 20% over 30 days should have been a defi-yield agent flag. + Add to agents/personas/defi-yield.md: "monitor 14-day rolling APY trend; flag + if decline > 15% from entry yield." + +Calibration update: + defi-yield: total scored 47 → 48 (right: 18, partially_right: 22, wrong: 3, rfwr: 5) + Rolling hit rate (high confidence): 71% +``` + +## See also + +- `architecture/05-memory-architecture.md` — full learning-loop spec +- `schemas/trajectory.schema.json` — trajectory structure +- `/weekly-strategy` — runs sessions that produce theses to retrospective diff --git a/verticals/investment-intelligence/engine/commands/thesis-debate.md b/verticals/investment-intelligence/engine/commands/thesis-debate.md new file mode 100644 index 00000000..de14642e --- /dev/null +++ b/verticals/investment-intelligence/engine/commands/thesis-debate.md @@ -0,0 +1,45 @@ +# /thesis-debate + +Run an ad-hoc deep-dive on a single position or thesis. Same multi-agent team as weekly, narrower focus. + +## Usage + +``` +/thesis-debate + +Examples: +/thesis-debate "Should I rotate 5% to Morpho USDC?" +/thesis-debate "Is concentrated growth equity X overvalued?" +/thesis-debate "BTC vs ETH allocation — should I rebalance?" +/thesis-debate "Add real estate via REIT vs save for direct purchase?" +``` + +## What this command does + +Same as `/weekly-strategy` but with `mode: thesis-debate`: +- Macro context still loaded (lighter weight) +- Portfolio context loaded +- Debate scope narrowed to the topic +- Output is a thesis-debate session at `sessions/thesis/-.md` +- New `thesis_id` registered in `theses/index.yaml` if action is approved + +## When to use + +- A new opportunity surfaced this week (between weekly sessions) +- A position has moved materially and needs reassessment +- A regulatory event (e.g., new tax rule) requires re-thinking a holding +- An external trigger (job change, BV formation, inheritance event) requires structured debate + +## When NOT to use + +- For weekly review — use `/weekly-strategy` instead +- For impulse decisions ("BTC just dropped 10%, should I buy?") — sleep on it; the substrate is anti-impulse by design +- When you're emotional — the debate doesn't help when you're already committed to a verdict + +## Output + +Writes `sessions/thesis/-.md` with full debate transcript. + +## Honest limit + +A thesis-debate is a structured second opinion, not market timing. If you've already decided the action and just want validation, the substrate isn't doing its job — and you'll bias the agents accordingly. Run `/thesis-debate` only when you genuinely don't know the answer. diff --git a/verticals/investment-intelligence/engine/commands/weekly-strategy.md b/verticals/investment-intelligence/engine/commands/weekly-strategy.md new file mode 100644 index 00000000..3432212b --- /dev/null +++ b/verticals/investment-intelligence/engine/commands/weekly-strategy.md @@ -0,0 +1,121 @@ +# /weekly-strategy + +Run the IIS weekly Strategy Session — multi-agent debate that produces a draft session document for human verdict. + +## Usage + +``` +/weekly-strategy [--week YYYY-WW] [--mode weekly|rebalance|thesis-debate] +``` + +Defaults: +- `--week`: current ISO week +- `--mode`: weekly + +## What this command does + +1. **Instantiates** a new Strategy Session with current ISO week as ID, today as date. +2. **Loads context**: architecture docs (`architecture/00-overview.md`, `architecture/03-strategy-session.md`, `architecture/04-multi-agent-team.md`, `architecture/10-honest-limits.md`), agent catalog (`agents/catalog.json`), tax overlay (`tax-overlays/.yaml`). +3. **Ingests data**: + - Macro snapshot — adapter-fetched (FRED, CoinGecko, alternative.me) or manual prompt + - Portfolio snapshot — operator-private path or example reference + - Open theses — reads `theses/index.yaml` + - Recent retrospective lessons — reads last 4 closed trajectories from `trajectories/` +4. **Runs the debate**: + - **Parallel**: 5 Analysis-layer agents (macro-risk, crypto-dca, defi-yield, fundamentals, technical) + - **Sequential**: 3 Risk-layer agents (risk-manager → tax-optimizer → regulatory-risk) + - **Sequential**: 2 Synthesis-layer agents (portfolio-manager → chief-of-staff) +5. **Renders draft** at `sessions/.md` with: + - Macro context block + - Full agent debate transcript + - Ranked opportunities + - Risk flags + - Proposed actions with explicit human-approval gates + - `human_verdict: pending` +6. **Surfaces** the draft for review. + +## Operator workflow + +```bash +# 1. Run the command in your agent CLI +/weekly-strategy + +# 2. Agent asks for any missing context (portfolio drift, recent moves, etc.) +# Provide honestly. Don't paper over reality. + +# 3. Agent runs the debate (~3-5 min for full multi-agent; ~30 sec for Tier 0 single-prompt) + +# 4. Agent writes sessions/2026-W19.md (or whatever ISO week) + +# 5. Read the full session document yourself. + +# 6. Mark the verdict in the front-matter: +# human_verdict: +# status: approve | modify | reject +# notes: +# reviewed_by: +# reviewed_at: + +# 7. Settle the session — it's now append-only + +# 8. Calendar-add: retrospective 90 days from any thesis-driven approved action +``` + +## Required configuration + +For Tier 1+ runs, the substrate expects the operator to have configured: + +- `~/.iis/config.yaml` with paths to private snapshot store + tax overlay +- Adapter API keys (FRED, CoinGecko, etc.) in `.env` outside the substrate repo +- Optional: Notion MCP, Slack MCP for output channels + +For Tier 0 runs, no configuration is required — the agent prompts for context interactively. + +## Failure modes + +| Failure | Behavior | +|---|---| +| Adapter degraded | Session marked with `data_integrity_flags`; debate runs with stale or partial data; agents reason at reduced confidence | +| Portfolio snapshot missing | Refuses to fabricate; asks operator for context | +| Last retrospective overdue | Surfaces warning before running; offers to run `/iis-retrospective ` first | +| Tax overlay > 12 months stale | Surfaces warning; offers to run `/iis-tax-overlay-refresh ` | + +## Output + +Writes: +- `sessions/.md` (the Strategy Session) +- `theses/index.yaml` (updated if new theses generated) +- (optional) Slack message via Slack MCP + +Returns: +- Path to the new session file +- Summary of proposed actions (human-readable) +- Reminder to mark `human_verdict` + +## Example output + +``` +Wrote sessions/2026-W19.md (mode: weekly) + +3 proposed actions: +- DCA continue (auto-approved per dca-rules) +- Hold cash buffer (manual, no approval needed) +- ★ Morpho USDC rotation €25K (thesis-driven, REQUIRES YOUR APPROVAL by 2026-05-18) + +1 risk flag surfaced: +- F&G 72 — pullback risk in next 6 weeks (note, don't act) + +Read sessions/2026-W19.md, mark human_verdict, then settle. + +Retrospective due 2026-08-10 (90 days from W19 close). +``` + +## See also + +- `/thesis-debate ` — ad-hoc deep-dive on a single position +- `/portfolio-snapshot` — refresh private snapshot before running session +- `/iis-retrospective ` — close a 90+ day thesis with outcome scoring + +## Honest limit + +This command produces a draft. **You produce the verdict.** That separation is the substrate. diff --git a/verticals/investment-intelligence/engine/examples/README.md b/verticals/investment-intelligence/engine/examples/README.md new file mode 100644 index 00000000..fbf3075a --- /dev/null +++ b/verticals/investment-intelligence/engine/examples/README.md @@ -0,0 +1,85 @@ +# Examples — Sanitization Rules + +The examples in this directory are **archetypal personas, not real operators.** They demonstrate the substrate's shape without leaking any contributor's positions. + +## The two archetypes + +### Jane — The Freelance Designer (Tier 0–1) + +- **Net worth:** ~€300K +- **Composition:** 50% liquid (savings + cash), 30% crypto (BTC/ETH index), 20% equities (broad index ETFs) +- **Entity:** Personal only — no BV, no LLC +- **Jurisdiction:** Netherlands (Box 3 reference jurisdiction) +- **Cadence:** Weekly review (Loop 2) + monthly close (Loop 3) only +- **Tooling:** Tier 0 — markdown-only, no Python, no Docker +- **Purpose:** Demonstrates the minimum-viable substrate + +### Marcus — The Founder (Tier 1–2) + +- **Net worth:** ~€1.2M +- **Composition:** 30% crypto (across self-custody + 2 exchanges), 40% equities (mix of broad index + concentrated growth), 20% real estate (primary residence equity), 10% cash + working capital in BV +- **Entity:** Personal + Operating BV + Holding BV +- **Jurisdiction:** Netherlands (BV + DGA structure) +- **Cadence:** All five loops, with annual architecture review +- **Tooling:** Tier 1 — multi-agent debate, OpenBB feeds, Notion hub +- **Purpose:** Demonstrates the full-substrate composition + +## Sanitization rules applied + +For every file in this directory: + +1. **No real names.** Jane and Marcus are fictional. +2. **No real allocations.** Percentages are archetypal (50/30/20, 40/30/20/10) — not anyone's actual. +3. **No real EUR amounts > €10K** that could fingerprint a real operator. +4. **No real exchange specifics.** References use generic "Exchange A/B" or globally-largest 3 (Coinbase, Binance, Kraken). +5. **No real wallet addresses.** Even watch-only addresses fingerprint. +6. **No real Notion page IDs, database IDs, workspace structures.** +7. **Macro context uses real public data** at example timestamps (FRED is public). + +## Verification + +Before any PR adds to this directory, the contributor confirms: + +- [ ] The persona is clearly fictional (different name, profile, location specifics if any) +- [ ] Allocations are archetypal, not the contributor's actual +- [ ] No exchange-specific phrasing that fingerprints the contributor's real platforms +- [ ] No EUR/USD amounts > €10K outside example placeholders +- [ ] Macro values are public information (FRED, CoinGecko, alternative.me) at the timestamp shown +- [ ] No identifying tells (city neighborhood names, family relationships, etc.) + +## What examples show + +Each archetype directory contains: + +``` +jane-freelance/ or marcus-founder/ +├── README.md # who this archetype is +├── snapshots/ +│ ├── 2026-W18-snapshot.md # archetypal portfolio snapshot +│ └── 2026-W19-snapshot.md +├── sessions/ +│ ├── 2026-W18.md # weekly Strategy Session +│ ├── 2026-W19.md +│ └── thesis/ +│ └── morpho-yield-rotation-2026-W19.md # thesis-debate session +├── theses/ +│ └── index.yaml # thesis log +├── trajectories/ +│ └── (none — synthetic archetypes don't produce real outcomes) +└── tax-overlays/ + └── NL.yaml # reference tax overlay +``` + +## Why archetypal, not synthetic + +Synthetic data risks looking real. Archetypal personas are **explicitly fictional** in their READMEs, so a reader can't mistake them for a real operator's data. This is the strongest privacy guarantee. + +## Extending + +If you want to contribute a new archetype: + +- "Felix the freelancer with high crypto allocation" — Tier 1, Switzerland jurisdiction +- "Sara the early-retirement engineer" — Tier 0, US jurisdiction, Roth-heavy +- "Yusuf the family-office operator" — Tier 2+, complex structure + +Submit PR with all sanitization rules applied. Maintainers will review. diff --git a/verticals/investment-intelligence/engine/examples/jane-freelance/README.md b/verticals/investment-intelligence/engine/examples/jane-freelance/README.md new file mode 100644 index 00000000..89dd7f32 --- /dev/null +++ b/verticals/investment-intelligence/engine/examples/jane-freelance/README.md @@ -0,0 +1,53 @@ +# Jane — The Freelance Designer (Tier 0–1) + +> **Fictional archetype.** Any resemblance to a real person is coincidence. + +## Profile + +- **Age:** Mid-30s +- **Profession:** Freelance designer +- **Location:** Netherlands (uses NL tax overlay) +- **Net worth:** ~€300,000 +- **Goals:** Build runway to ~€500K and develop investing discipline; no immediate FIRE timeline + +## Allocation (archetypal) + +| Class | Target | Notes | +|---|---|---| +| Cash + savings | 50% | Emergency fund + 6 months runway | +| Crypto (BTC/ETH index) | 30% | Long-term holdings, weekly DCA | +| Equities (broad index) | 20% | VWRL or similar global ETF | + +## Entity structure + +Personal only. No BV. Simple Box 3 wealth-tax filing. + +## Loop participation + +| Loop | Cadence | Time | Tooling | +|---|---|---|---| +| 1 — Daily Pulse | (skipped) | — | Reads bank app + CoinGecko | +| 2 — Weekly Review | Sunday 19:00 | 15 min | Markdown + agent CLI (Tier 0) | +| 3 — Monthly Close | 1st of month | 30 min | Same as above | +| 4 — Quarterly Tax | Quarterly | Skipped (no BV) | — | +| 5 — Annual Architecture | December | 1 hr | Same as above | + +## Why Jane uses the IIS + +- **Loneliness of solo investing:** the substrate is a structured second opinion every Sunday +- **DCA discipline:** the substrate catches FOMO and panic-sell impulses +- **Tax awareness:** NL Box 3 filing is annoying; the tax-optimizer agent surfaces what matters +- **Legacy:** the journal accumulates into a record she can pass on + +## Sample session walkthrough + +See `sessions/2026-W18.md` for a full Tier-0 weekly session and `sessions/2026-W19.md` for one with a thesis-debate. + +## What Jane doesn't do + +- ❌ Does not run Python infrastructure (Rotki, OpenBB, Freqtrade) +- ❌ Does not have agent execution layer (no AgentKit/Alpaca live) +- ❌ Does not pay for paid data feeds (Glassnode, Polygon) +- ❌ Does not run multi-agent debate — uses single-agent CLI prompts (the substrate's Tier 0 default) + +This is the **minimum viable substrate**. Most operators start here and stay here. diff --git a/verticals/investment-intelligence/engine/examples/jane-freelance/sessions/2026-W18.md b/verticals/investment-intelligence/engine/examples/jane-freelance/sessions/2026-W18.md new file mode 100644 index 00000000..d8d4d079 --- /dev/null +++ b/verticals/investment-intelligence/engine/examples/jane-freelance/sessions/2026-W18.md @@ -0,0 +1,69 @@ +--- +schema_version: 1.0.0 +id: 2026-W18 +date: "2026-05-04" +mode: weekly +author: jane-archetype +macro_context: + fed_funds_rate: 4.50 + dxy: 99.2 + m2_yoy: 4.1 + yield_curve_2_10: -0.30 + fear_greed_index: 72 + btc_dominance: 54.0 + notes: "Risk-on continues; F&G in greed territory; yield curve still inverted" +portfolio_snapshot_ref: jane-2026-W18 +ranked_opportunities: [] +risk_flags: + - "F&G > 70 historically precedes 5-15% pullback within 6 weeks (calibrate, don't act)" + - "Yield curve inversion at -0.30; recession indicator amber" +proposed_actions: + - action: "Continue weekly DCA at current pace (€200/wk)" + requires_human_approval: false + execution_layer: dca-rules + - action: "Hold cash buffer; do not chase risk-on rally" + requires_human_approval: false + execution_layer: manual +human_verdict: + status: approve + notes: "DCA discipline holds; no thesis violations" + reviewed_by: jane-archetype + reviewed_at: "2026-05-04T19:30:00+02:00" +--- + +# Strategy Session — 2026-W18 + +## Macro + +Fed Funds at 4.50%. DXY 99.2. F&G 72 (greed). Yield curve 2-10 still inverted at -0.30. + +Macro regime is **risk-on but late-cycle.** F&G > 70 is historically a contrarian signal — pullback within 6 weeks is the modal outcome. But "contrarian signal" is not "trade signal" for a Tier-0 operator on a 5-year horizon. Note it; don't act on it. + +## Portfolio + +Current allocation drift vs target (50/30/20): +- Cash: 51% (target 50%) — within tolerance +- Crypto: 31% (target 30%) — slight overweight from price appreciation +- Equities: 18% (target 20%) — slight underweight + +Drift across asset classes < 2%. No rebalancing trigger. + +## Risk flags + +1. **F&G > 70** is a contrarian indicator — note for the next 6 weeks; don't react. +2. **Yield curve inversion** at -0.30 is amber. Historical recession lead time is 12-18 months from inversion start. Maintain cash buffer. + +## Decisions + +- Continue weekly €200 DCA into BTC/ETH index split (60/40 within crypto bucket) +- Do not add equities this week (slight underweight is acceptable in late-cycle regime) +- Do not reduce crypto despite F&G — sizing is within target band + +## Verdict + +Approve. Standard discipline week. Next checkpoint: 2026-W19. + +## Notes for retrospective + +- If pullback materializes in next 6 weeks, was the "note F&G but don't act" decision the right call? (Answer in 90 days.) +- Cash buffer is at 51% — acceptable now; reassess if it climbs to 55% (signal of paralysis). diff --git a/verticals/investment-intelligence/engine/examples/jane-freelance/sessions/2026-W19.md b/verticals/investment-intelligence/engine/examples/jane-freelance/sessions/2026-W19.md new file mode 100644 index 00000000..9e335864 --- /dev/null +++ b/verticals/investment-intelligence/engine/examples/jane-freelance/sessions/2026-W19.md @@ -0,0 +1,96 @@ +--- +schema_version: 1.0.0 +id: 2026-W19 +date: "2026-05-11" +mode: weekly +author: jane-archetype +macro_context: + fed_funds_rate: 4.50 + dxy: 99.5 + m2_yoy: 4.0 + yield_curve_2_10: -0.32 + fear_greed_index: 78 + btc_dominance: 54.5 + notes: "Greed deepening; macro stable" +portfolio_snapshot_ref: jane-2026-W19 +agent_debate: + - agent: macro-risk + stance: "Hold cash buffer; F&G entering historically vulnerable zone" + evidence: ["F&G 78", "yield_curve still inverted -0.32"] + confidence: medium + - agent: crypto-dca + stance: "Continue DCA; pause new crypto adds beyond DCA" + evidence: ["F&G 78 contrarian", "BTC dominance stable 54.5"] + confidence: high + - agent: tax-optimizer + stance: "243 days to Jan 1 — no Box 3 actions required this week" + evidence: ["Box 3 valuation date Jan 1", "current crypto holdings well below allowance threshold"] + confidence: high +ranked_opportunities: + - thesis_id: cash-yield-laddering-2026-W19 + summary: "Move €15K of cash buffer into 3-month deposit ladder for 4% yield" + size_suggested_pct: 5 + confidence: medium + risk_level: low + conditional_on: "Bank offers 3-month deposit at >= 4.0% APY" + time_horizon: short +risk_flags: + - "F&G 78 — historically the upper edge of 'pullback within 6 weeks' zone" +proposed_actions: + - action: "Continue weekly DCA at €200/wk" + requires_human_approval: false + execution_layer: dca-rules + - action: "Research 3-month deposit ladder rates this week; if >= 4% APY, deploy €15K of cash buffer" + target_asset: "EUR cash" + amount_or_pct: "€15,000" + conditional_on: "3-month deposit rate >= 4.0% APY" + deadline: "2026-05-18" + requires_human_approval: true + execution_layer: thesis-driven +human_verdict: + status: approve + notes: "Approve cash-laddering thesis. Will research bank rates before Wednesday." + reviewed_by: jane-archetype + reviewed_at: "2026-05-11T19:45:00+02:00" +--- + +# Strategy Session — 2026-W19 + +## Macro + +F&G now 78 (deeper greed). Yield curve at -0.32. M2 stable at +4.0%. Macro regime unchanged from W18: risk-on, late-cycle, recession-pending (12-18 month lead). + +## Portfolio + +Drift unchanged from W18. Cash buffer at 51%; crypto at 31%; equities at 18%. + +## Agent debate + +**Macro-risk** (medium confidence): "Hold cash buffer. F&G 78 is in the zone where 5-15% pullbacks have historically occurred within 6 weeks. Don't add equity exposure now." + +**Crypto-DCA** (high confidence): "Continue weekly DCA. Don't pause — DCA's value comes from discipline through volatility. But don't add new crypto positions outside the DCA rule. F&G 78 says wait." + +**Tax-optimizer** (high confidence): "243 days to Jan 1. No tax-driven actions required this week. Current crypto holdings well below Box 3 allowance threshold; no rotation needed." + +## Opportunities + +**Thesis-2026-W19-01: Cash yield laddering.** Bank deposit rates have firmed at ~4% for 3-month terms. Moving €15K (5% of net worth) into a 3-month deposit captures yield on idle cash without altering risk profile. Reversible in 3 months. Low risk. + +## Risk flags + +1. **F&G 78** — upper edge of the contrarian zone. Pullback risk meaningful in next 6 weeks. + +## Decisions + +- Continue weekly DCA — no change. +- Research 3-month deposit rates before Wednesday. If rate is at or above 4% APY, deploy €15K from cash buffer to lock in yield. If rate is below 4%, defer. This is the only new action this week. + +## Verdict + +Approve. Cash-laddering action is conditional on rate availability; will execute via online banking after research. + +## Notes for retrospective (90 days) + +- Did the cash-laddering thesis hold up (i.e., did the 4% yield realize)? +- Did the predicted F&G > 70 pullback materialize? If yes, was holding cash the right call vs maintaining equity weight? +- Did the macro-risk agent's call (mid confidence on holding) prove right or wrong? diff --git a/verticals/investment-intelligence/engine/examples/marcus-founder/README.md b/verticals/investment-intelligence/engine/examples/marcus-founder/README.md new file mode 100644 index 00000000..b6f9b150 --- /dev/null +++ b/verticals/investment-intelligence/engine/examples/marcus-founder/README.md @@ -0,0 +1,63 @@ +# Marcus — The Founder (Tier 1–2) + +> **Fictional archetype.** Any resemblance to a real person is coincidence. + +## Profile + +- **Age:** Late-30s +- **Profession:** Founder (active SaaS company, ~3 years revenue) +- **Location:** Netherlands (uses NL tax overlay; Operating BV + Holding BV structure) +- **Net worth:** ~€1,200,000 +- **Goals:** Optimize tax structure, scale wealth via business equity + diversified portfolio, build inheritance plan for spouse + children + +## Allocation (archetypal) + +| Class | Target | Notes | +|---|---|---| +| Crypto | 30% | BTC/ETH base + DeFi yield positions, mix of self-custody + 2 exchanges | +| Equities | 40% | Mix of broad index (60%) + concentrated growth (40%) | +| Real estate | 20% | Primary residence equity (no investment property yet) | +| Cash + working capital | 10% | Operating BV cash + personal emergency fund | + +## Entity structure + +- **Personal:** primary residence, retirement accounts, emergency fund +- **Operating BV:** SaaS company revenue, current operations, BV-held crypto for long-term thesis +- **Holding BV:** strategic asset accumulation (eventual: real estate, large equity positions, IP licensing) + +## Loop participation + +| Loop | Cadence | Time | Tooling | +|---|---|---|---| +| 1 — Daily Pulse | Weekday morning | 2 min | Slack digest from automated agent | +| 2 — Weekly Review | Sunday 19:00 | 45 min | Tier 1 multi-agent debate via Claude Cowork | +| 3 — Monthly Close | 1st of month | 30 min | Notion + Rotki MCP | +| 4 — Quarterly Tax | Quarterly | 1 hr | Tax-optimizer + belastingadviseur consult | +| 5 — Annual Architecture | December | 2 hrs | Full review + inheritance update | + +## Tooling + +- **Tier 1:** Multi-agent debate (Claude Cowork), OpenBB data backbone, DefiLlama, FRED via adapters +- **Tier 1+:** Rotki for portfolio aggregation (self-hosted Docker) +- **Tier 1+:** Notion Wealth Command Hub for monthly snapshots + thesis log +- **Tier 2 (planned):** Coinbase AgentKit DCA agent + Alpaca paper-trading for stock layer +- **NOT used:** Live Freqtrade (backtesting only), Glassnode (free DefiLlama covers needs) + +## Why Marcus uses the IIS + +- **Tax complexity:** BV/Box 3 split creates real arbitrage opportunities; the substrate makes them visible +- **Position discipline:** the multi-agent debate catches the founder bias (over-allocate to risk; under-allocate to liquid reserves) +- **Inheritance:** the annual architecture review forces yearly inheritance protocol updates +- **Audit trail:** investor reporting + spouse/co-founder transparency requires history + +## Sample session walkthrough + +See `sessions/2026-W18.md` for a Tier-1 weekly session with multi-agent debate, and `sessions/thesis/btc-bv-rotation-2026-W19.md` for an ad-hoc thesis-debate on tax-driven rotation. + +## What Marcus doesn't do + +- ❌ Does not run live trading on equities (Alpaca paper-only currently) +- ❌ Does not engage in active crypto trading (DCA only + occasional yield rotation) +- ❌ Does not skip the annual review (would defeat the BV optimization) + +This is the **full-substrate composition**. Most operators graduate here over 12-24 months as their NW grows + tax surface area expands. diff --git a/verticals/investment-intelligence/engine/examples/marcus-founder/retrospectives/morpho-yield-rotation-2026-W18.outcome.md b/verticals/investment-intelligence/engine/examples/marcus-founder/retrospectives/morpho-yield-rotation-2026-W18.outcome.md new file mode 100644 index 00000000..2985452e --- /dev/null +++ b/verticals/investment-intelligence/engine/examples/marcus-founder/retrospectives/morpho-yield-rotation-2026-W18.outcome.md @@ -0,0 +1,169 @@ +--- +schema_version: 1.0.0 +trajectory_id: morpho-yield-rotation-2026-W18 +type: thesis_arc +opened: "2026-05-04" +closed: "2026-08-04" +duration_days: 90 +input_state: + macro_regime: late-cycle-risk-on + fear_greed: 72 + btc_dominance: 54.0 + yield_curve_2_10: -0.30 + portfolio_drift: "+1% crypto, +1% equities, -1% cash" +agent_debate: + macro-risk: + stance: "Tighten cash buffer; recession amber" + confidence: medium + crypto-dca: + stance: "Continue DCA, no rotation" + confidence: high + defi-yield: + stance: "Rotate to Morpho USDC at 5.8% APY — top of opportunity set" + confidence: high + fundamentals: + stance: "Equity multiples expanded; not earnings-supported" + confidence: medium + technical: + stance: "Equities at upper Bollinger band; mean-reversion bias" + confidence: low + risk-manager: + stance: "3% sizing within tolerance; correct constraint" + confidence: high + counter_to: "fundamentals (no action at this drift)" + tax-optimizer: + stance: "243 days to Jan 1; no tax actions urgent" + confidence: high + regulatory-risk: + stance: "Exchange concentration < 30%; no flags" + confidence: high +human_verdict: approve +modification: null +outcome: + realized_outcome: "Position generated 4.2% APY (vs 5.8% expected). No smart contract incident. Withdrawal completed within 4 hours. Underlying USDC stable throughout." + realized_yield_pct: 4.2 + expected_yield_pct: 5.8 + total_yield_eur: 262 + expected_yield_eur: 362 + smart_contract_incident: false + withdrawal_friction: low + thesis_held_up: partially_confirmed + primary_lesson: "Morpho APY decayed 30% over 90 days. Decay was visible in 14-day rolling APY trend by week 6 — the defi-yield agent did not surface this as a flag. Add to defi-yield agent prompt: monitor 14-day rolling APY trend; flag if decline > 15% from entry yield." +verdict_quality: + agent_call_quality: + macro-risk: right + crypto-dca: right + defi-yield: partially_right + fundamentals: n/a + technical: n/a + risk-manager: right + tax-optimizer: n/a + regulatory-risk: right + human_verdict_quality: good_call + rationale: "Approving the rotation was correct (yield generated safely, no incident). Modifying size from defi-yield's 5% suggestion down to risk-manager's 3% was the right discipline; saved EUR ~210 in over-exposure. The miss was process: should have set a 14-day APY-decay alert, which the substrate did not provide." +calibration_update: + defi-yield: + trajectories_scored_before: 47 + trajectories_scored_after: 48 + new_partially_right: 22 + rolling_hit_rate_high_confidence_before: 73 + rolling_hit_rate_high_confidence_after: 71 + note: "High-confidence calls now at 71% hit rate over 48 trajectories. Yield-decay blindness pattern recurs (3 of last 5 yield theses had > 15% APY decay; 2 surfaced no flag)." + human_verdict: + good_call_count_before: 41 + good_call_count_after: 42 + note: "Verdict modification (size 3% not 5%) compounds verdict-quality calibration." +substrate_improvement_proposed: + - file: iis/agents/personas/defi-yield.md + change: "Add explicit 14-day rolling APY trend check to system prompt. Flag if decline > 15% from entry yield." + - file: iis/architecture/05-memory-architecture.md + change: "Document the yield-decay-blindness lesson as a substrate-level pattern (not Marcus-specific)." +--- + +# Retrospective — Morpho Yield Rotation Thesis (2026-W18) + +**Closed:** 2026-08-04 (90 days post-action) +**Held:** 90 days +**Operator:** marcus-archetype (fictional) + +## What was approved + +On 2026-05-04 (Sunday Strategy Session 2026-W18), the operator approved a thesis to rotate €25,000 (3% of portfolio) from personal cash buffer into Morpho USDC lending at 5.8% APY. Risk-manager's modification reduced suggested size from defi-yield agent's 5% to 3%. Tax-optimizer flagged no urgent actions (243 days to Jan 1). + +## What happened + +- Position deployed Wednesday 2026-05-07 after due diligence (TVL trend, audit history, withdrawal mechanics confirmed) +- 30-day APY: 5.6% (close to expected 5.8%) +- 60-day APY: 4.8% (decayed 14% from entry) +- 90-day APY: 4.2% (decayed 28% from entry) +- Total yield realized: €262 (vs €362 expected; 28% underperformance) +- No smart contract incident +- Withdrawal completed in 4 hours, no friction +- USDC peg held throughout (no de-pegging events) + +## What the agents called + +| Agent | Stance | Confidence | Outcome scoring | +|---|---|---|---| +| macro-risk | Tighten cash; recession amber | medium | **right** — recession indicators stayed amber; cash discipline was correct | +| crypto-dca | Continue DCA, no rotation | high | **right** — DCA discipline held; no panic, no chase | +| defi-yield | Rotate to Morpho 5.8% APY | high | **partially_right** — position was safe, but high-confidence APY claim was systematically wrong by 28%; agent did not surface 14d APY decay | +| risk-manager | 3% size, not 5% | high | **right** — saved approximately €210 of over-exposure | +| tax-optimizer | No urgent actions | high | **right** — no NL Box 3 actions emerged | +| regulatory-risk | Exchange concentration OK | high | **right** — no platform restrictions surfaced | + +## What the operator's verdict deserved + +**good_call** — Approving the rotation was correct (no incident, yield generated). Modifying size from 5% to 3% was the right discipline. + +The miss was **process**: the substrate should have surfaced the 14-day rolling APY decay by week 6 (when decay was already 14%), and triggered an ad-hoc thesis-debate on whether to exit early. It did not. That's a substrate-improvement opportunity, not an operator failure. + +## Primary lesson + +> **Yield decay > 15% over 30 days should be a defi-yield agent flag.** + +This decay pattern recurs (3 of last 5 yield theses; 2 surfaced no flag). Add to defi-yield agent system prompt: + +``` +Monitor 14-day rolling APY trend on any active yield position. +If APY decline > 15% from entry yield, flag in next weekly Strategy Session +under risk_flags as "yield decay alert: ". +``` + +## Calibration impact + +- **defi-yield agent** rolling hit rate (high confidence): 73% → 71% over 48 trajectories. Yield-decay blindness pattern is now visible enough to act on. +- **human verdict quality**: 42nd good_call out of 48 verdict-tracked retrospectives. Discipline holding. + +## Substrate improvements proposed + +1. `iis/agents/personas/defi-yield.md` — add 14-day APY decay check to system prompt +2. `iis/architecture/05-memory-architecture.md` — document yield-decay-blindness as a substrate-level pattern (not operator-specific) + +These will be incorporated in the next substrate version. + +## What I would do differently + +If facing the same input state today: +- Approve the rotation (still the right call given the state) +- Set explicit exit conditions: "exit if 14d rolling APY < 4.5% sustained 2 weeks" +- Schedule mid-thesis check at day 45 (not just day 90) +- Size at 2-2.5% not 3% if substrate had surfaced the decay-pattern lesson at thesis open + +## What I would NOT do differently + +- The thesis itself was sound; this is a substrate-improvement story, not a thesis-rejection story +- The size modification (5% → 3%) was correct discipline regardless of outcome +- The macro context (F&G 72, late-cycle) was read correctly — risk-on persisted as predicted +- The retrospective scoring is honest: "partially_right" not "wrong" because the position was safe; the issue was expected-vs-realized yield, not capital risk + +## Loop closing + +This retrospective writes to: +- `iis-private/retrospectives/morpho-yield-rotation-2026-W18.outcome.md` (this file) +- `iis-private/trajectories/morpho-yield-rotation-2026-W18.json` (machine-readable) +- `iis-private/theses/index.yaml` (status: closed) +- `iis-private/agents-calibration.json` (defi-yield counters updated) +- Substrate PR proposing prompt update to `iis/agents/personas/defi-yield.md` + +The corpus has one more closed thesis. The defi-yield agent has one more data point. The substrate gains one substrate-level pattern. The loop is closed. diff --git a/verticals/investment-intelligence/engine/examples/marcus-founder/sessions/2026-W18.md b/verticals/investment-intelligence/engine/examples/marcus-founder/sessions/2026-W18.md new file mode 100644 index 00000000..4bdac6fd --- /dev/null +++ b/verticals/investment-intelligence/engine/examples/marcus-founder/sessions/2026-W18.md @@ -0,0 +1,147 @@ +--- +schema_version: 1.0.0 +id: 2026-W18 +date: "2026-05-04" +mode: weekly +author: marcus-archetype +macro_context: + fed_funds_rate: 4.50 + dxy: 99.2 + m2_yoy: 4.1 + yield_curve_2_10: -0.30 + fear_greed_index: 72 + btc_dominance: 54.0 + cpi_yoy: 2.8 + notes: "Risk-on continues; F&G greed; yield curve inverted; CPI cooling toward target" +portfolio_snapshot_ref: marcus-2026-W18 +agent_debate: + - agent: macro-risk + stance: "Late-cycle regime; recession amber. Maintain cash reserves at 10% target — do not chase rally." + evidence: + - "yield_curve_2_10 = -0.30 (inverted 14 months, historical recession lead 12-18 mo)" + - "Fed funds 4.50%, M2 +4.0%, mild contraction" + - "F&G 72 (greed; pullback within 6 weeks historically)" + confidence: medium + - agent: crypto-dca + stance: "Continue weekly DCA. BTC dominance 54% stable — no rotation needed." + evidence: + - "BTC 30d return: +8%" + - "ETH 30d return: +6%" + - "BTC dominance flat at 54% over 8 weeks" + confidence: high + - agent: defi-yield + stance: "Morpho USDC at 5.8% APY — top of opportunity set. Aave at 4.1%. Compound at 3.9%. Recommend Morpho if rotating cash buffer." + evidence: + - "Morpho TVL +12% over 30 days, audit history clean" + - "Aave TVL stable, slightly underperforming" + - "Compound TVL declining 5% over 30 days" + confidence: high + - agent: fundamentals + stance: "Concentrated growth holdings (mock-AAPL-equivalent) trading at P/E 32 vs 5y avg 26. Multiple expansion not earnings-justified." + evidence: + - "P/E 32 vs sector median 24" + - "FCF yield 2.1% vs 5y avg 3.0%" + confidence: medium + - agent: technical + stance: "Crypto in mid-trend; equities at upper Bollinger band. Mean-reversion bias on equities." + evidence: + - "BTC RSI 62 (neutral-bullish)" + - "Equity index RSI 71 (overbought)" + confidence: low + - agent: risk-manager + stance: "Crypto allocation at 31% vs 30% target — within tolerance. Equities at 41% vs 40% — fine. Cash at 9% vs 10% — slightly under, top up via DCA halt or sale." + evidence: ["Drift < 2% across all classes"] + confidence: high + counter_to: "fundamentals (no action needed on multiple expansion at this drift level)" + - agent: tax-optimizer + stance: "243 days to Jan 1. No urgent rotation actions. BV crypto holdings ~€80K — within reasonable range; revisit at Q3 review." + evidence: + - "Box 3 valuation date Jan 1" + - "DGA loan balance from BV: €0 (clean)" + - "Innovation Box: WBSO renewal due in November" + confidence: high + - agent: regulatory-risk + stance: "Exchange concentration: Coinbase 18%, Kraken 12% — within 30% per-platform limit. No active regulatory pressure flagged." + evidence: ["No DAC8/CARF action items pending"] + confidence: high +ranked_opportunities: + - thesis_id: morpho-yield-rotation-2026-W18 + summary: "Rotate 3-5% of personal cash buffer into Morpho USDC lending" + size_suggested_pct: 3 + confidence: medium + risk_level: low + conditional_on: "Morpho TVL stays > $X (verify), audit unchanged, deposit < €30K to stay within risk tolerance" + time_horizon: short +risk_flags: + - "F&G 72 contrarian zone — pullback risk within 6 weeks" + - "Equity multiples expanded; not earnings-supported (note for retrospective if pullback hits equities harder)" +proposed_actions: + - action: "Continue weekly DCA at €500/wk (BTC 60%, ETH 40%)" + requires_human_approval: false + execution_layer: dca-rules + - action: "Research Morpho USDC pool: TVL trend, recent audit, withdrawal mechanics. If satisfactory, deploy €25K from personal cash buffer." + target_asset: "USDC via Morpho" + amount_or_pct: "€25,000 (~3% of NW)" + conditional_on: "Audit unchanged, TVL > $1B, withdrawal mechanics confirmed" + deadline: "2026-05-18" + requires_human_approval: true + execution_layer: thesis-driven +human_verdict: + status: approve + notes: "Approve thesis subject to Morpho due diligence this week. Will execute via Coinbase Wallet → Morpho UI manually (no AgentKit yet)." + reviewed_by: marcus-archetype + reviewed_at: "2026-05-04T20:30:00+02:00" +--- + +# Strategy Session — 2026-W18 + +## Macro + +Late-cycle regime. Yield curve still inverted (-0.30, 14 months running). Fed funds 4.50%. CPI cooling toward 2.8%. F&G 72 (greed). The recession-amber/risk-on tension is the defining feature of this regime: macro indicators say be cautious; risk-asset prices say keep adding. + +The substrate's response: discipline, not direction. DCA continues. Cash reserve target unchanged. No new equity exposure. One yield-rotation thesis surfaces. + +## Portfolio + +Drift within tolerance across all classes: +- Crypto: 31% (target 30%, +1%) +- Equities: 41% (target 40%, +1%) +- Real estate: 20% (target 20%) +- Cash: 9% (target 10%, -1%) — top up via DCA halt or new income + +No rebalancing trigger. + +## Agent debate (highlights) + +- **Macro-risk** says hold cash at target, don't chase. Medium confidence. +- **Crypto-DCA** says keep DCA, no rotation. High confidence. +- **Defi-yield** flags Morpho USDC at 5.8% APY as the cleanest current opportunity. High confidence. +- **Fundamentals** flags equity multiple expansion not earnings-justified — but Risk-manager counters: drift < 2%, no action needed. +- **Tax-optimizer** says no urgent NL Box 3 actions yet (243 days to Jan 1). +- **Regulatory-risk** clean — Coinbase + Kraken concentration within limits. + +## Opportunities + +**Thesis-2026-W18-01: Morpho USDC yield rotation.** Rotate 3% of net worth (~€25K) from personal cash buffer into Morpho USDC lending. 5.8% APY vs ~0% on idle cash. Reversible. Smart-contract risk is the primary exposure — mitigated by Morpho's audit history and TVL stability. + +## Risk flags + +1. F&G 72 — pullback risk meaningful in next 6 weeks. Don't add equity exposure now. +2. Equity multiples expanded; if pullback hits equities harder than crypto, expect 5-10% drawdown on equity bucket. Acceptable within risk profile. + +## Decisions + +- Continue weekly DCA. No change. +- Research Morpho due diligence this week. Execute Wednesday or Thursday if checks pass. +- Hold concentrated growth equities; do not add. (Multiple expansion is a flag, not a sell signal at current drift.) + +## Verdict + +Approve. Morpho thesis is conditional on due diligence; will execute manually (no AgentKit live yet). + +## Notes for retrospective (90 days) + +- Morpho realized yield vs expected 5.8% — track 30/60/90d +- F&G > 70 pullback prediction — did it materialize in next 6 weeks? +- Macro-risk agent's "hold cash at target" — was the right call vs adding equity exposure? +- Defi-yield agent's high-confidence Morpho call — held up? diff --git a/verticals/investment-intelligence/engine/schemas/agent.schema.json b/verticals/investment-intelligence/engine/schemas/agent.schema.json new file mode 100644 index 00000000..7f8b99d1 --- /dev/null +++ b/verticals/investment-intelligence/engine/schemas/agent.schema.json @@ -0,0 +1,71 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://github.com/frankxai/investment-intelligence-system/schemas/agent.schema.json", + "title": "Agent Definition", + "description": "Schema for agents in agents/catalog.json", + "type": "object", + "required": ["id", "layer", "domain", "persona", "system_prompt_summary", "input_schema", "output_schema"], + "properties": { + "id": { + "type": "string", + "pattern": "^[a-z0-9-]+$", + "description": "Unique agent identifier" + }, + "layer": { + "type": "string", + "enum": ["analysis", "risk", "synthesis", "cross-cutting"] + }, + "domain": { + "type": "string", + "description": "1-2 sentence description of the agent's expertise" + }, + "persona": { + "type": "string", + "description": "Personality archetype the agent embodies" + }, + "system_prompt_summary": { + "type": "string", + "description": "What the agent does in one paragraph" + }, + "input_schema": { + "type": "array", + "items": { "type": "string" }, + "description": "Required input fields from the strategy session" + }, + "output_schema": { + "type": "array", + "items": { "type": "string" }, + "description": "Fields the agent populates in the session" + }, + "tools_allowed": { + "type": "array", + "items": { "type": "string" } + }, + "tools_denied": { + "type": "array", + "items": { "type": "string" } + }, + "recommended_model": { + "type": "string", + "description": "LLM recommended for this agent (e.g., claude-sonnet-4-6, claude-opus-4-7)" + }, + "calibration_notes": { + "type": "string", + "description": "Known biases and calibration approach for retrospectives" + }, + "calibration_data": { + "type": "object", + "description": "Populated over time from retrospectives", + "properties": { + "trajectories_scored": { "type": "integer" }, + "right": { "type": "integer" }, + "partially_right": { "type": "integer" }, + "right_for_wrong_reason": { "type": "integer" }, + "wrong": { "type": "integer" }, + "hit_rate_high_confidence": { "type": "number" }, + "hit_rate_medium_confidence": { "type": "number" }, + "last_updated": { "type": "string", "format": "date" } + } + } + } +} diff --git a/verticals/investment-intelligence/engine/schemas/portfolio-snapshot.schema.json b/verticals/investment-intelligence/engine/schemas/portfolio-snapshot.schema.json new file mode 100644 index 00000000..422d5a4d --- /dev/null +++ b/verticals/investment-intelligence/engine/schemas/portfolio-snapshot.schema.json @@ -0,0 +1,62 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://github.com/frankxai/investment-intelligence-system/schemas/portfolio-snapshot.schema.json", + "title": "Portfolio Snapshot", + "description": "Sanitized portfolio snapshot referenced from sessions. NEVER contains real wallet addresses, real EUR/USD amounts above example thresholds, or fingerprinting exchange specifics.", + "type": "object", + "required": ["snapshot_id", "date", "currency"], + "properties": { + "snapshot_id": { "type": "string" }, + "date": { "type": "string", "format": "date" }, + "currency": { "type": "string", "enum": ["EUR", "USD", "GBP", "CHF", "JPY"] }, + "total_value": { + "oneOf": [ + { "type": "number" }, + { "type": "string", "enum": ["redacted", "private"] } + ] + }, + "allocation": { + "type": "object", + "description": "Either archetypal percentages (50/30/20) for OSS examples, or actual percentages (private)", + "properties": { + "crypto_pct": { "type": "number", "minimum": 0, "maximum": 100 }, + "equities_pct": { "type": "number", "minimum": 0, "maximum": 100 }, + "fixed_income_pct": { "type": "number", "minimum": 0, "maximum": 100 }, + "cash_pct": { "type": "number", "minimum": 0, "maximum": 100 }, + "real_estate_pct": { "type": "number", "minimum": 0, "maximum": 100 }, + "alternatives_pct": { "type": "number", "minimum": 0, "maximum": 100 } + } + }, + "target_allocation": { + "type": "object", + "description": "Target allocation for drift comparison", + "properties": { + "crypto_pct": { "type": "number" }, + "equities_pct": { "type": "number" }, + "fixed_income_pct": { "type": "number" }, + "cash_pct": { "type": "number" }, + "real_estate_pct": { "type": "number" }, + "alternatives_pct": { "type": "number" } + } + }, + "drift_pct": { + "type": "object", + "description": "Per-asset-class drift (current - target)" + }, + "entity_split": { + "type": "object", + "description": "How holdings are split across legal entities", + "properties": { + "personal_pct": { "type": "number" }, + "operating_bv_pct": { "type": "number" }, + "holding_bv_pct": { "type": "number" } + } + }, + "exchange_concentration": { + "type": "object", + "description": "Sanitized for OSS — generic 'Exchange A/B' or top-3-globally only", + "additionalProperties": { "type": "number" } + }, + "notes": { "type": "string" } + } +} diff --git a/verticals/investment-intelligence/engine/schemas/strategy-session.schema.json b/verticals/investment-intelligence/engine/schemas/strategy-session.schema.json new file mode 100644 index 00000000..3ca41a37 --- /dev/null +++ b/verticals/investment-intelligence/engine/schemas/strategy-session.schema.json @@ -0,0 +1,125 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://github.com/frankxai/investment-intelligence-system/schemas/strategy-session.schema.json", + "title": "Strategy Session", + "description": "The atomic unit of the IIS substrate. A closed, dated, append-only artifact capturing macro context, agent debate, ranked opportunities, risk flags, proposed actions, human verdict, and (retrospectively) outcome.", + "type": "object", + "required": ["schema_version", "id", "date", "mode", "author"], + "properties": { + "schema_version": { "type": "string", "const": "1.0.0" }, + "id": { + "type": "string", + "description": "ISO week (e.g. 2026-W19) for weekly sessions, YYYY-MM-DD for ad-hoc", + "pattern": "^\\d{4}-(W\\d{2}|Q\\d|\\d{2}-\\d{2}(-\\d+)?|annual|monthly)$" + }, + "date": { "type": "string", "format": "date" }, + "mode": { + "type": "string", + "enum": ["weekly", "rebalance", "thesis-debate", "retrospective", "monthly", "annual"] + }, + "author": { "type": "string", "description": "Pseudonym in OSS; real for private operators" }, + "macro_context": { + "type": "object", + "properties": { + "fed_funds_rate": { "type": "number" }, + "dxy": { "type": "number" }, + "m2_yoy": { "type": "number" }, + "yield_curve_2_10": { "type": "number" }, + "fear_greed_index": { "type": "integer", "minimum": 0, "maximum": 100 }, + "btc_dominance": { "type": "number" }, + "cpi_yoy": { "type": "number" }, + "notes": { "type": "string", "maxLength": 500 } + } + }, + "portfolio_snapshot_ref": { + "type": "string", + "description": "Reference to private snapshot — NEVER inline real allocations in OSS" + }, + "agent_debate": { + "type": "array", + "items": { + "type": "object", + "required": ["agent", "stance", "confidence"], + "properties": { + "agent": { "type": "string" }, + "stance": { "type": "string" }, + "evidence": { "type": "array", "items": { "type": "string" } }, + "confidence": { "type": "string", "enum": ["low", "medium", "high"] }, + "counter_to": { "type": "string" } + } + } + }, + "ranked_opportunities": { + "type": "array", + "items": { + "type": "object", + "required": ["thesis_id", "summary", "confidence", "risk_level"], + "properties": { + "thesis_id": { "type": "string" }, + "summary": { "type": "string" }, + "size_suggested_pct": { "type": "number", "minimum": 0, "maximum": 100 }, + "confidence": { "type": "string", "enum": ["low", "medium", "high"] }, + "risk_level": { "type": "string", "enum": ["low", "medium", "high"] }, + "conditional_on": { "type": "string" }, + "time_horizon": { "type": "string", "enum": ["short", "medium", "long"] } + } + } + }, + "risk_flags": { + "type": "array", + "items": { "type": "string" } + }, + "proposed_actions": { + "type": "array", + "items": { + "type": "object", + "required": ["action", "requires_human_approval"], + "properties": { + "action": { "type": "string" }, + "target_asset": { "type": "string" }, + "amount_or_pct": { "type": "string" }, + "conditional_on": { "type": "string" }, + "deadline": { "type": "string", "format": "date" }, + "requires_human_approval": { "type": "boolean" }, + "execution_layer": { "type": "string", "enum": ["manual", "dca-rules", "thesis-driven"] } + } + } + }, + "human_verdict": { + "type": "object", + "properties": { + "status": { "type": "string", "enum": ["approve", "modify", "reject", "pending"] }, + "notes": { "type": "string" }, + "reviewed_by": { "type": "string" }, + "reviewed_at": { "type": "string", "format": "date-time" } + } + }, + "outcome_tracking": { + "type": "object", + "properties": { + "filled_after": { "type": "string", "format": "date" }, + "realized_outcome": { "type": "string" }, + "thesis_held_up": { "type": "string", "enum": ["confirmed", "partially_confirmed", "refuted"] }, + "lessons": { "type": "array", "items": { "type": "string" } } + } + }, + "data_integrity_flags": { + "type": "array", + "description": "Surfaced when an adapter is degraded, cache is stale, or a researcher claim couldn't be source-verified", + "items": { "type": "string" } + }, + "errata": { + "type": "array", + "description": "Append-only correction log if the session needs amendment after settlement", + "items": { + "type": "object", + "required": ["date", "correction"], + "properties": { + "date": { "type": "string", "format": "date" }, + "correction": { "type": "string" }, + "by": { "type": "string" } + } + } + } + } +} diff --git a/verticals/investment-intelligence/engine/schemas/thesis.schema.json b/verticals/investment-intelligence/engine/schemas/thesis.schema.json new file mode 100644 index 00000000..f936a3d4 --- /dev/null +++ b/verticals/investment-intelligence/engine/schemas/thesis.schema.json @@ -0,0 +1,47 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://github.com/frankxai/investment-intelligence-system/schemas/thesis.schema.json", + "title": "Thesis", + "description": "An indexed entry in theses/index.yaml. Every proposed_action with execution_layer=thesis-driven references a thesis_id that resolves to one of these.", + "type": "object", + "required": ["thesis_id", "opened", "status", "summary"], + "properties": { + "thesis_id": { + "type": "string", + "description": "Unique slug: --", + "pattern": "^[a-z0-9-]+$" + }, + "opened": { "type": "string", "format": "date" }, + "status": { + "type": "string", + "enum": ["active", "closed", "abandoned"] + }, + "closed": { "type": ["string", "null"], "format": "date" }, + "summary": { "type": "string", "maxLength": 280 }, + "rationale": { "type": "string" }, + "sessions_referenced": { + "type": "array", + "items": { "type": "string" } + }, + "outcome_due": { + "type": "string", + "format": "date", + "description": "When retrospective should be run (typically opened + 90 days)" + }, + "outcome_recorded": { "type": ["string", "null"], "format": "date" }, + "thesis_held_up": { + "type": ["string", "null"], + "enum": ["confirmed", "partially_confirmed", "refuted", null] + }, + "size_actual_pct": { "type": "number" }, + "exit_conditions": { + "type": "array", + "items": { "type": "string" }, + "description": "Pre-defined conditions under which thesis is closed" + }, + "tags": { + "type": "array", + "items": { "type": "string" } + } + } +} diff --git a/verticals/investment-intelligence/engine/schemas/trajectory.schema.json b/verticals/investment-intelligence/engine/schemas/trajectory.schema.json new file mode 100644 index 00000000..a8e16347 --- /dev/null +++ b/verticals/investment-intelligence/engine/schemas/trajectory.schema.json @@ -0,0 +1,70 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://github.com/frankxai/investment-intelligence-system/schemas/trajectory.schema.json", + "title": "Trajectory", + "description": "ReasoningBank-shaped trajectory: a complete debate-to-outcome arc that feeds the learning loop. Each closed thesis produces one trajectory.", + "type": "object", + "required": ["trajectory_id", "type", "opened"], + "properties": { + "trajectory_id": { "type": "string" }, + "type": { + "type": "string", + "enum": ["thesis_arc", "rebalance_arc", "tax_arc"] + }, + "opened": { "type": "string", "format": "date" }, + "closed": { "type": ["string", "null"], "format": "date" }, + "input_state": { + "type": "object", + "description": "Macro + portfolio state at thesis open — the context for pattern matching", + "properties": { + "macro_regime": { "type": "string" }, + "fear_greed": { "type": "integer" }, + "btc_dominance": { "type": "number" }, + "yield_curve_2_10": { "type": "number" }, + "portfolio_drift": { "type": "string" } + } + }, + "agent_debate": { + "type": "object", + "description": "Stances per agent at thesis open (snapshot, not live)", + "additionalProperties": { + "type": "object", + "properties": { + "stance": { "type": "string" }, + "confidence": { "type": "string", "enum": ["low", "medium", "high"] } + } + } + }, + "human_verdict": { + "type": "string", + "enum": ["approve", "approve_with_modification", "reject"] + }, + "modification": { "type": ["string", "null"] }, + "outcome": { + "type": "object", + "properties": { + "realized_outcome": { "type": "string" }, + "duration_days": { "type": "integer" }, + "thesis_held_up": { + "type": "string", + "enum": ["confirmed", "partially_confirmed", "refuted"] + }, + "primary_lesson": { "type": "string" } + } + }, + "verdict_quality": { + "type": "object", + "description": "Per-agent and per-human scoring after outcome is known", + "properties": { + "agent_call_quality": { + "type": "object", + "additionalProperties": { + "type": "string", + "enum": ["right", "partially_right", "right_for_wrong_reason", "wrong", "n/a"] + } + }, + "human_verdict_quality": { "type": "string" } + } + } + } +} diff --git a/verticals/investment-intelligence/engine/scripts/privacy-check.mjs b/verticals/investment-intelligence/engine/scripts/privacy-check.mjs new file mode 100644 index 00000000..fb6676af --- /dev/null +++ b/verticals/investment-intelligence/engine/scripts/privacy-check.mjs @@ -0,0 +1,181 @@ +#!/usr/bin/env node +// privacy-check.mjs — pre-commit privacy boundary enforcer for the IIS substrate. +// +// Scans staged changes (or all files when run with --all) for patterns that +// would violate PRIVACY-BOUNDARY.md. Exits non-zero on any match so the commit +// is blocked. Operators install this as a pre-commit hook; substrate +// maintainers also run it in CI. +// +// Usage: +// node iis/scripts/privacy-check.mjs (checks git staged diff) +// node iis/scripts/privacy-check.mjs --all (checks all tracked files) +// node iis/scripts/privacy-check.mjs --files=path1,path2 (specific files) + +import { execSync } from 'node:child_process' +import { readFileSync } from 'node:fs' +import { resolve } from 'node:path' + +const args = process.argv.slice(2) +const mode = args.includes('--all') + ? 'all' + : args.find((a) => a.startsWith('--files=')) + ? 'files' + : 'staged' +const filesArg = args.find((a) => a.startsWith('--files='))?.slice('--files='.length) + +// Patterns that flag for review. Each rule has a regex, a severity, and a reason. +// Severity: 'block' = exit non-zero; 'warn' = print only, allow commit. +const RULES = [ + // Identity tells (block) — Frank-specific tells the substrate must not propagate + { + regex: /\b(Witali|Riemer|Pavlovka|Seesen|Tien|Arcanea Labs)\b/i, + severity: 'block', + reason: 'Frank-identity reference. Replace with fictional persona.', + }, + // Real EUR amounts that look like a position (block on > 10K not in placeholder context) + { + regex: /€\s?(?:1[1-9]|[2-9])\d{4,}|€\s?\d{6,}/, + severity: 'block', + reason: 'EUR amount > €10K — likely a real position. Use archetypal amounts (€25K, €100K) or "redacted".', + }, + // API key shapes (block — defensive) + { + regex: /(api_key|apikey|api-key|secret|access_token|bearer)\s*[:=]\s*['"][a-zA-Z0-9_-]{20,}/i, + severity: 'block', + reason: 'Possible API key. Move to environment variable; never commit secrets.', + }, + // Seed phrase pattern (block — defensive; never on disk anyway) + // Tightened: requires lowercase-only words separated by single spaces (no punctuation), + // matches typical seed phrase format (12 or 24 words) — avoids false positives on prose. + { + regex: /^[a-z]{3,8}( [a-z]{3,8}){11}$|^[a-z]{3,8}( [a-z]{3,8}){23}$/m, + severity: 'block', + reason: 'Possible BIP-39 seed phrase pattern (12 or 24 lowercase words on a single line). NEVER commit.', + skipPaths: [/\.md$/, /\.json$/], // prose and JSON descriptions can hit length thresholds + }, + // Wallet addresses (warn — public read-only addresses are still fingerprinting) + { + regex: /\b0x[a-fA-F0-9]{40}\b/, + severity: 'warn', + reason: 'Ethereum-shaped address. Confirm this is example data, not a real watch-address.', + }, + // Bitcoin address (warn) + { + regex: /\b(?:bc1|[13])[a-zA-Z0-9]{25,39}\b/, + severity: 'warn', + reason: 'Possible Bitcoin address. Confirm example data.', + }, + // Specific exchange + EUR combination (warn — fingerprints) + { + regex: /\b(coinbase|binance|kraken|crypto\.com|swissborg|nexo)\b[^\n]{0,30}€\s?[1-9]\d{3,}/i, + severity: 'warn', + reason: 'Exchange + EUR amount combination — confirm not fingerprinting.', + }, +] + +// Promoted copy: the engine lives under verticals/investment-intelligence/, +// not iis/ as in the operator instance. +const ENGINE_ROOT = 'verticals/investment-intelligence/' + +// Files to skip entirely (always allowed) +const SKIP_FILES = [ + `${ENGINE_ROOT}engine/scripts/privacy-check.mjs`, // this file (documents the patterns; matches itself) + `${ENGINE_ROOT}engine/architecture/10-honest-limits.md`, // documents amounts in context +] + +function getFilesToCheck() { + if (mode === 'files' && filesArg) { + return filesArg.split(',').map((p) => p.trim()).filter(Boolean) + } + if (mode === 'all') { + const out = execSync(`git ls-files ${ENGINE_ROOT}`, { encoding: 'utf8' }) + return out.split('\n').filter(Boolean) + } + // staged (default) + const out = execSync('git diff --cached --name-only --diff-filter=ACM', { encoding: 'utf8' }) + return out + .split('\n') + .filter(Boolean) + .filter((p) => p.startsWith(ENGINE_ROOT)) +} + +function readFileSafe(path) { + try { + return readFileSync(resolve(process.cwd(), path), 'utf8') + } catch { + return null + } +} + +function isSkipped(filePath) { + return SKIP_FILES.some((skip) => filePath === skip || filePath.endsWith(skip)) +} + +function checkFile(filePath) { + if (isSkipped(filePath)) return [] + const content = readFileSafe(filePath) + if (!content) return [] + + const findings = [] + for (const rule of RULES) { + if (rule.skipPaths?.some((re) => re.test(filePath))) continue + const matches = content.match(new RegExp(rule.regex, 'gi')) + if (matches) { + findings.push({ + file: filePath, + severity: rule.severity, + reason: rule.reason, + matches: matches.slice(0, 3), + }) + } + } + return findings +} + +function main() { + const files = getFilesToCheck() + if (files.length === 0) { + console.log('[iis-privacy-check] no IIS substrate files to check') + process.exit(0) + } + + console.log(`[iis-privacy-check] scanning ${files.length} file(s) (${mode} mode)`) + + const allFindings = [] + for (const file of files) { + allFindings.push(...checkFile(file)) + } + + if (allFindings.length === 0) { + console.log('[iis-privacy-check] ✓ no privacy-boundary violations') + process.exit(0) + } + + const blockers = allFindings.filter((f) => f.severity === 'block') + const warnings = allFindings.filter((f) => f.severity === 'warn') + + if (blockers.length > 0) { + console.error('\n[iis-privacy-check] ✗ BLOCKING violations:\n') + for (const b of blockers) { + console.error(` ${b.file}`) + console.error(` reason: ${b.reason}`) + console.error(` matches: ${JSON.stringify(b.matches)}`) + console.error('') + } + } + + if (warnings.length > 0) { + console.warn('\n[iis-privacy-check] ⚠ warnings (review before commit):\n') + for (const w of warnings) { + console.warn(` ${w.file}`) + console.warn(` reason: ${w.reason}`) + console.warn(` matches: ${JSON.stringify(w.matches)}`) + console.warn('') + } + } + + console.log('See the operator instance PRIVACY-BOUNDARY.md for the full rule set.\n') + process.exit(blockers.length > 0 ? 1 : 0) +} + +main() diff --git a/verticals/investment-intelligence/engine/scripts/validate-schemas.mjs b/verticals/investment-intelligence/engine/scripts/validate-schemas.mjs new file mode 100644 index 00000000..4737291d --- /dev/null +++ b/verticals/investment-intelligence/engine/scripts/validate-schemas.mjs @@ -0,0 +1,177 @@ +#!/usr/bin/env node +// validate-schemas.mjs — validate IIS substrate JSON Schemas + sample data +// against draft-2020-12 spec. Operators run this on their private session +// corpus; substrate CI runs it on the example archetypes. +// +// Usage: +// node iis/scripts/validate-schemas.mjs (validate everything in iis/) +// node iis/scripts/validate-schemas.mjs --schemas-only (just schema files, not examples) +// node iis/scripts/validate-schemas.mjs (specific file or directory) +// +// Exits non-zero on any validation failure. Emits a structured summary. +// +// Dependencies: ajv (peer-installed by operator). Falls back to manual checks +// if ajv unavailable (substrate-friendly: no required deps). + +import { readFileSync, readdirSync, statSync, existsSync } from 'node:fs' +import { fileURLToPath } from 'node:url' +import { join, resolve, relative } from 'node:path' +import matter from 'gray-matter' + +const args = process.argv.slice(2) +const schemasOnly = args.includes('--schemas-only') + +// Promoted copy: the engine root is this script's parent directory +// (verticals/investment-intelligence/engine/), not /iis as in the +// operator instance. +const SUBSTRATE_ROOT = resolve(fileURLToPath(new URL('..', import.meta.url))) +const targetPath = args.find((a) => !a.startsWith('--')) || SUBSTRATE_ROOT + +const ROOT = process.cwd() +const SCHEMA_DIR = resolve(SUBSTRATE_ROOT, 'schemas') + +let ajv = null +try { + const Ajv = (await import('ajv/dist/2020.js')).default + const addFormats = (await import('ajv-formats')).default + ajv = new Ajv({ strict: false, allErrors: true }) + addFormats(ajv) +} catch { + console.warn('[iis-validate] ajv not installed; falling back to structural checks only') + console.warn(' install with: pnpm add -D ajv ajv-formats') +} + +function readJSON(path) { + return JSON.parse(readFileSync(path, 'utf8')) +} + +function readMarkdownFrontmatter(path) { + const raw = readFileSync(path, 'utf8') + const parsed = matter(raw) + // YAML parses unquoted ISO dates into Date objects; the schemas expect + // strings. JSON round-trip coerces Dates to ISO strings, matching how + // the same record would arrive as JSON. + return JSON.parse(JSON.stringify(parsed.data)) +} + +function loadSchemas() { + if (!existsSync(SCHEMA_DIR)) { + throw new Error(`schemas dir not found: ${SCHEMA_DIR}`) + } + const files = readdirSync(SCHEMA_DIR).filter((f) => f.endsWith('.schema.json')) + const schemas = {} + for (const file of files) { + const path = join(SCHEMA_DIR, file) + const schema = readJSON(path) + const id = file.replace('.schema.json', '') + schemas[id] = schema + if (ajv && schema.$id) { + try { + ajv.addSchema(schema) + } catch (e) { + console.warn(`[iis-validate] could not register schema ${file}: ${e.message}`) + } + } + } + return schemas +} + +function validateAgainst(schema, data, label) { + if (!ajv) { + // Structural fallback: check required fields manually + const required = schema.required || [] + const missing = required.filter((k) => !(k in data)) + if (missing.length > 0) { + return { ok: false, errors: [`missing required fields: ${missing.join(', ')}`] } + } + return { ok: true, errors: [] } + } + const validate = ajv.compile(schema) + const ok = validate(data) + if (ok) return { ok: true, errors: [] } + const errors = (validate.errors || []).map( + (e) => `${e.instancePath || '/'} ${e.message} (${JSON.stringify(e.params)})` + ) + return { ok: false, errors } +} + +function findExampleSessions(root) { + const sessions = [] + function walk(dir) { + if (!existsSync(dir)) return + for (const ent of readdirSync(dir)) { + const path = join(dir, ent) + const stat = statSync(path) + if (stat.isDirectory()) { + walk(path) + } else if (ent.endsWith('.md') && /sessions|retrospectives|theses/.test(path)) { + sessions.push(path) + } + } + } + walk(join(root, 'examples')) + return sessions +} + +function classifySession(frontmatter) { + if (frontmatter.schema_version === '1.0.0' && 'mode' in frontmatter) return 'strategy-session' + if (frontmatter.trajectory_id && frontmatter.outcome) return 'trajectory' + if (frontmatter.thesis_id && frontmatter.status) return 'thesis' + return null +} + +function main() { + console.log(`[iis-validate] validating ${targetPath}`) + + let schemas + try { + schemas = loadSchemas() + console.log(`[iis-validate] loaded ${Object.keys(schemas).length} schema(s)`) + } catch (e) { + console.error(`[iis-validate] ✗ ${e.message}`) + process.exit(1) + } + + if (schemasOnly) { + console.log('[iis-validate] ✓ schemas loaded; --schemas-only mode, skipping example validation') + process.exit(0) + } + + const sessions = findExampleSessions(SUBSTRATE_ROOT) + console.log(`[iis-validate] found ${sessions.length} session/trajectory/thesis file(s) to validate`) + + let failures = 0 + for (const path of sessions) { + const rel = relative(ROOT, path) + const fm = readMarkdownFrontmatter(path) + const type = classifySession(fm) + if (!type) { + console.warn(`[iis-validate] ⚠ ${rel} — could not classify; skipping`) + continue + } + const schema = schemas[type === 'strategy-session' ? 'strategy-session' : type] + if (!schema) { + console.warn(`[iis-validate] ⚠ ${rel} — no schema for type ${type}; skipping`) + continue + } + const result = validateAgainst(schema, fm, rel) + if (result.ok) { + console.log(`[iis-validate] ✓ ${rel} (${type})`) + } else { + console.error(`[iis-validate] ✗ ${rel} (${type})`) + for (const err of result.errors) { + console.error(` ${err}`) + } + failures++ + } + } + + if (failures > 0) { + console.error(`\n[iis-validate] ${failures} file(s) failed validation`) + process.exit(1) + } + console.log('\n[iis-validate] ✓ all sessions valid') + process.exit(0) +} + +main() diff --git a/verticals/investment-intelligence/mcp/trade-gate/.gitignore b/verticals/investment-intelligence/mcp/trade-gate/.gitignore new file mode 100644 index 00000000..868ba648 --- /dev/null +++ b/verticals/investment-intelligence/mcp/trade-gate/.gitignore @@ -0,0 +1,3 @@ +node_modules/ +dist/ +.trade-gate-data/ diff --git a/verticals/investment-intelligence/mcp/trade-gate/README.md b/verticals/investment-intelligence/mcp/trade-gate/README.md new file mode 100644 index 00000000..f82f1063 --- /dev/null +++ b/verticals/investment-intelligence/mcp/trade-gate/README.md @@ -0,0 +1,44 @@ +# trade-gate-mcp + +> Fail-closed approve/execute MCP for the Investment Intelligence vertical. **v0.1. UNAUDITED. NOT FOR LIVE FUNDS.** + +The gate between the decision swarm and any broker. Agents propose; humans approve; paper is the default; every step is audited append-only. + +## Tools + +| Tool | What | +|---|---| +| `propose_trade` | TradeIntent + cap policy (+ optional DCA whitelist) → `auto-approved` (DCA-whitelisted, within-cap buy — still capped + audited) \| `pending` (everything else, over-cap included) \| `rejected` (replay / malformed) | +| `request_approval` | **The human's act.** `approve` → single-use token bound to the intent; `deny` → closed. One token per approval; a lost token is not re-issued. | +| `list_pending` | Pending-approval queue. | +| `execute_approved` | Approved intent → broker adapter. Paper fills in-repo; `alpaca`/`ibkr`/`coinbase` throw `NOT_WIRED` until wired locally per the vertical's `RUNBOOK.md`. Audit-first; an intent executes once, ever. | +| `read_audit` | The append-only JSONL audit trail. | + +## Invariants (tested) + +- A non-DCA intent **never** executes without a human approval token (`e2e.test.ts` red case). +- A live-broker attempt fails `NOT_WIRED` **even with approval** — no live code or credentials ship here (red case). +- Over ANY cap → pending, never auto-approved, DCA included. +- Approval tokens are single-use and intent-bound; executed intents never re-execute; replayed intent ids are rejected. +- Audit write fails → the action fails. Durable JSONL state (audit, spend, approvals, gate) survives restart. +- Every output carries the R5 non-advisory footer. + +## Run + +```bash +npm install +npm test # typecheck + 21 tests +npm run build && npm start # stdio MCP server; state under ./.trade-gate-data (TRADE_GATE_DATA_DIR to override) +``` + +Wire it into Claude Code / Hermes Agent as a stdio MCP server. Only the operator's session should hold `request_approval` — treat it as the human's surface, not the swarm's. + +## Lineage + +Shape ports from [`payment-intelligence-system/mcp`](https://github.com/frankxai/payment-intelligence-system) (fail-closed verdicts, caps, append-only audit, human gate). Trading is a distinct risk domain — code shape reused, repo untouched. Board record: `docs/boards/2026-07-02-investment-intelligence-vertical-spawn.md`. + +--- + +*This is system architecture, not financial / investment / tax advice. Outputs frame decisions; jurisdiction-specific counsel signs off on instruments. The practitioner accepts capital risk; the substrate accepts no claim.* + +**Built on SIP** — trade-gate-mcp · v0.1 · MIT diff --git a/verticals/investment-intelligence/mcp/trade-gate/package-lock.json b/verticals/investment-intelligence/mcp/trade-gate/package-lock.json new file mode 100644 index 00000000..3c457cc0 --- /dev/null +++ b/verticals/investment-intelligence/mcp/trade-gate/package-lock.json @@ -0,0 +1,1735 @@ +{ + "name": "@frankx-ai/trade-gate-mcp", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@frankx-ai/trade-gate-mcp", + "version": "0.1.0", + "license": "MIT", + "dependencies": { + "@modelcontextprotocol/sdk": "^1.0.0", + "zod": "^3.23.0" + }, + "bin": { + "trade-gate-mcp": "dist/index.js" + }, + "devDependencies": { + "@types/node": "^22.0.0", + "tsx": "^4.19.0", + "typescript": "^5.5.0" + }, + "engines": { + "node": ">=22" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.1.tgz", + "integrity": "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.1.tgz", + "integrity": "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.1.tgz", + "integrity": "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.1.tgz", + "integrity": "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.1.tgz", + "integrity": "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.1.tgz", + "integrity": "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.1.tgz", + "integrity": "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.1.tgz", + "integrity": "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.1.tgz", + "integrity": "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.1.tgz", + "integrity": "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.1.tgz", + "integrity": "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.1.tgz", + "integrity": "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.1.tgz", + "integrity": "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.1.tgz", + "integrity": "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.1.tgz", + "integrity": "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.1.tgz", + "integrity": "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.1.tgz", + "integrity": "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.1.tgz", + "integrity": "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.1.tgz", + "integrity": "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.1.tgz", + "integrity": "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.1.tgz", + "integrity": "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.1.tgz", + "integrity": "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.1.tgz", + "integrity": "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.1.tgz", + "integrity": "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.1.tgz", + "integrity": "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.1.tgz", + "integrity": "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@hono/node-server": { + "version": "1.19.14", + "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.14.tgz", + "integrity": "sha512-GwtvgtXxnWsucXvbQXkRgqksiH2Qed37H9xHZocE5sA3N8O8O8/8FA3uclQXxXVzc9XBZuEOMK7+r02FmSpHtw==", + "license": "MIT", + "engines": { + "node": ">=18.14.1" + }, + "peerDependencies": { + "hono": "^4" + } + }, + "node_modules/@modelcontextprotocol/sdk": { + "version": "1.29.0", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.29.0.tgz", + "integrity": "sha512-zo37mZA9hJWpULgkRpowewez1y6ML5GsXJPY8FI0tBBCd77HEvza4jDqRKOXgHNn867PVGCyTdzqpz0izu5ZjQ==", + "license": "MIT", + "dependencies": { + "@hono/node-server": "^1.19.9", + "ajv": "^8.17.1", + "ajv-formats": "^3.0.1", + "content-type": "^1.0.5", + "cors": "^2.8.5", + "cross-spawn": "^7.0.5", + "eventsource": "^3.0.2", + "eventsource-parser": "^3.0.0", + "express": "^5.2.1", + "express-rate-limit": "^8.2.1", + "hono": "^4.11.4", + "jose": "^6.1.3", + "json-schema-typed": "^8.0.2", + "pkce-challenge": "^5.0.0", + "raw-body": "^3.0.0", + "zod": "^3.25 || ^4.0", + "zod-to-json-schema": "^3.25.1" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@cfworker/json-schema": "^4.1.1", + "zod": "^3.25 || ^4.0" + }, + "peerDependenciesMeta": { + "@cfworker/json-schema": { + "optional": true + }, + "zod": { + "optional": false + } + } + }, + "node_modules/@types/node": { + "version": "22.20.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.20.0.tgz", + "integrity": "sha512-QWlFW2wf3nTjC13/DqRnBpR4ZO36VJH/JVBkA/vcnmbTBNQIlnObqyqZE1tUR7+Ni23Lda8R1BxMfbXRpCUx5g==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/accepts": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "license": "MIT", + "dependencies": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ajv": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", + "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/body-parser": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.3.0.tgz", + "integrity": "sha512-2cGmJupaNgg+QUwVLAucDuWuoMZ6EX9iHDRswZ5lsNYEmwPaRknMPCLZz07yTzVq/83p4o/wzbDZbBrTvGGTIw==", + "license": "MIT", + "dependencies": { + "bytes": "^3.1.2", + "content-type": "^2.0.0", + "debug": "^4.4.3", + "http-errors": "^2.0.1", + "iconv-lite": "^0.7.2", + "on-finished": "^2.4.1", + "qs": "^6.15.2", + "raw-body": "^3.0.2", + "type-is": "^2.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/body-parser/node_modules/content-type": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.0.0.tgz", + "integrity": "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/content-disposition": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.1.0.tgz", + "integrity": "sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "license": "MIT", + "engines": { + "node": ">=6.6.0" + } + }, + "node_modules/cors": { + "version": "2.8.6", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz", + "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", + "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.1.tgz", + "integrity": "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.28.1", + "@esbuild/android-arm": "0.28.1", + "@esbuild/android-arm64": "0.28.1", + "@esbuild/android-x64": "0.28.1", + "@esbuild/darwin-arm64": "0.28.1", + "@esbuild/darwin-x64": "0.28.1", + "@esbuild/freebsd-arm64": "0.28.1", + "@esbuild/freebsd-x64": "0.28.1", + "@esbuild/linux-arm": "0.28.1", + "@esbuild/linux-arm64": "0.28.1", + "@esbuild/linux-ia32": "0.28.1", + "@esbuild/linux-loong64": "0.28.1", + "@esbuild/linux-mips64el": "0.28.1", + "@esbuild/linux-ppc64": "0.28.1", + "@esbuild/linux-riscv64": "0.28.1", + "@esbuild/linux-s390x": "0.28.1", + "@esbuild/linux-x64": "0.28.1", + "@esbuild/netbsd-arm64": "0.28.1", + "@esbuild/netbsd-x64": "0.28.1", + "@esbuild/openbsd-arm64": "0.28.1", + "@esbuild/openbsd-x64": "0.28.1", + "@esbuild/openharmony-arm64": "0.28.1", + "@esbuild/sunos-x64": "0.28.1", + "@esbuild/win32-arm64": "0.28.1", + "@esbuild/win32-ia32": "0.28.1", + "@esbuild/win32-x64": "0.28.1" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/eventsource": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.7.tgz", + "integrity": "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==", + "license": "MIT", + "dependencies": { + "eventsource-parser": "^3.0.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/eventsource-parser": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.1.0.tgz", + "integrity": "sha512-kJezFj9YFAMLeORyi7aCLxLbD5/qWMQnoMVlVPyHIll7lgRJCc3JVln9Vgl9nwQi0YkMnhdGTMNn7CkRRAptMg==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/express": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", + "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", + "license": "MIT", + "dependencies": { + "accepts": "^2.0.0", + "body-parser": "^2.2.1", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "depd": "^2.0.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express-rate-limit": { + "version": "8.5.2", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.5.2.tgz", + "integrity": "sha512-5Kb34ipNX694DH48vN9irak1Qx30nb0PLYHXfJgw4YEjiC3ZEmZJhwOp+VfiCYwFzvFTdB9QkArYS5kXa2cx2A==", + "license": "MIT", + "dependencies": { + "ip-address": "^10.2.0" + }, + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/express-rate-limit" + }, + "peerDependencies": { + "express": ">= 4.11" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "license": "MIT" + }, + "node_modules/fast-uri": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.3.tgz", + "integrity": "sha512-i70LwGWUduXqzicKXWshooq+sWL1K3WUU5rKZNG/0i3a1OSoX3HqhH5WbWwTmqWfor4urUakGPiRQcleRZTwOg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/finalhandler": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", + "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hono": { + "version": "4.12.27", + "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.27.tgz", + "integrity": "sha512-1yrb/+w6HWQJrUCLkJ2IF5jNIPvvFkblV5RNOYl6bV+OA6p9GLcMpHFFGTosSvHvcAUibuUukRqhlYI4z32C7Q==", + "license": "MIT", + "engines": { + "node": ">=16.9.0" + } + }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/iconv-lite": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", + "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ip-address": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.2.0.tgz", + "integrity": "sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-promise": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", + "license": "MIT" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "license": "ISC" + }, + "node_modules/jose": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/jose/-/jose-6.2.3.tgz", + "integrity": "sha512-YYVDInQKFJfR/xa3ojUTl8c2KoTwiL1R5Wg9YCydwH0x0B9grbzlg5HC7mMjCtUJjbQ/YnGEZIhI5tCgfTb4Hw==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "license": "MIT" + }, + "node_modules/json-schema-typed": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/json-schema-typed/-/json-schema-typed-8.0.2.tgz", + "integrity": "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==", + "license": "BSD-2-Clause" + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", + "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/merge-descriptors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/negotiator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-to-regexp": { + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.2.tgz", + "integrity": "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/pkce-challenge": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.1.tgz", + "integrity": "sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==", + "license": "MIT", + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/qs": { + "version": "6.15.3", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.3.tgz", + "integrity": "sha512-O9gl3zCl5h5blw1KGUzQKhA5oUXSl8rwUIM5o0S3nCXMliSvy5Dzx7/DJcI+SwgICv+IneSZwhBh1oSyEHA71A==", + "license": "BSD-3-Clause", + "dependencies": { + "es-define-property": "^1.0.1", + "side-channel": "^1.1.1" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.3.0.tgz", + "integrity": "sha512-hek2mFQpPuI4E1BBKrSto+BU3e3x4xuarsbiwr3+lf7p44juvFMV0XFWQAP3xUyqXA4RrXLIoaSUGbSt056ZMw==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/raw-body": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz", + "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.7.0", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/router": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "depd": "^2.0.0", + "is-promise": "^4.0.0", + "parseurl": "^1.3.3", + "path-to-regexp": "^8.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz", + "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.3", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.1", + "mime-types": "^3.0.2", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/serve-static": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz", + "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==", + "license": "MIT", + "dependencies": { + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/side-channel": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz", + "integrity": "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4", + "side-channel-list": "^1.0.1", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/tsx": { + "version": "4.22.5", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.22.5.tgz", + "integrity": "sha512-F7JnSfPl5ASt6LqwWyUQ3T8BwN3q0eQEbFMYa2iRWaVQmmudo0d7fRmwM4O002gsvW1bs0yBYioutsAjqLJMvQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.28.0" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, + "node_modules/type-is": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.1.0.tgz", + "integrity": "sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA==", + "license": "MIT", + "dependencies": { + "content-type": "^2.0.0", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/type-is/node_modules/content-type": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.0.0.tgz", + "integrity": "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, + "node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zod-to-json-schema": { + "version": "3.25.2", + "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.25.2.tgz", + "integrity": "sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA==", + "license": "ISC", + "peerDependencies": { + "zod": "^3.25.28 || ^4" + } + } + } +} diff --git a/verticals/investment-intelligence/mcp/trade-gate/package.json b/verticals/investment-intelligence/mcp/trade-gate/package.json new file mode 100644 index 00000000..46cf14bf --- /dev/null +++ b/verticals/investment-intelligence/mcp/trade-gate/package.json @@ -0,0 +1,33 @@ +{ + "name": "@frankx-ai/trade-gate-mcp", + "version": "0.1.0", + "description": "Fail-closed approve/execute MCP for the Investment Intelligence vertical. Gates every TradeIntent behind caps + a human approval token (DCA whitelist excepted, still capped + audited). Paper broker only in-repo; live adapters are NOT_WIRED stubs. UNAUDITED. NOT FOR LIVE FUNDS.", + "license": "MIT", + "type": "module", + "main": "dist/index.js", + "bin": { + "trade-gate-mcp": "dist/index.js" + }, + "files": [ + "dist", + "src" + ], + "scripts": { + "build": "tsc", + "typecheck": "tsc --noEmit", + "start": "node dist/index.js", + "test": "tsc --noEmit && node --test --import tsx src/*.test.ts" + }, + "dependencies": { + "@modelcontextprotocol/sdk": "^1.0.0", + "zod": "^3.23.0" + }, + "devDependencies": { + "@types/node": "^22.0.0", + "tsx": "^4.19.0", + "typescript": "^5.5.0" + }, + "engines": { + "node": ">=22" + } +} diff --git a/verticals/investment-intelligence/mcp/trade-gate/src/approval.test.ts b/verticals/investment-intelligence/mcp/trade-gate/src/approval.test.ts new file mode 100644 index 00000000..aff05763 --- /dev/null +++ b/verticals/investment-intelligence/mcp/trade-gate/src/approval.test.ts @@ -0,0 +1,122 @@ +import { test } from "node:test"; +import assert from "node:assert/strict"; +import { mkdtempSync, rmSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; + +import { ApprovalQueue } from "./approval.js"; +import type { TradeIntent } from "./types.js"; + +function intent(overrides: Partial = {}): TradeIntent { + return { + intentId: `ti_${Math.random().toString(36).slice(2)}`, + instrument: "VWCE", + assetClass: "etf", + side: "buy", + notional: 100, + currency: "EUR", + broker: "paper", + dca: false, + ...overrides, + }; +} + +function tempQueue(): { queue: ApprovalQueue; dir: string; cleanup: () => void } { + const dir = mkdtempSync(join(tmpdir(), "trade-gate-approvals-")); + return { queue: new ApprovalQueue(dir), dir, cleanup: () => rmSync(dir, { recursive: true, force: true }) }; +} + +test("propose returns PENDING and nothing else", () => { + const { queue, cleanup } = tempQueue(); + try { + const pa = queue.propose(intent(), "non-dca"); + assert.equal(pa.status, "pending-human-approval"); + assert.equal((pa as unknown as { approved?: unknown }).approved, undefined); + assert.equal(queue.listPending().length, 1); + } finally { + cleanup(); + } +}); + +test("token is single-use and bound to its intent", () => { + const { queue, cleanup } = tempQueue(); + try { + const ti = intent(); + const pa = queue.propose(ti, "non-dca"); + const token = queue.issueToken(pa.approvalId, "frank"); + + // Wrong intent → refuse. + assert.throws(() => queue.consumeToken(token, "ti_other"), /issued for intent/); + + // Right intent → consumes. + const done = queue.consumeToken(token, ti.intentId); + assert.equal(done.status, "executed"); + + // Second consume → refuse (single-use is absolute). + assert.throws(() => queue.consumeToken(token, ti.intentId), /already consumed/); + } finally { + cleanup(); + } +}); + +test("one token per approval — a lost token is not re-issued", () => { + const { queue, cleanup } = tempQueue(); + try { + const pa = queue.propose(intent(), "non-dca"); + queue.issueToken(pa.approvalId, "frank"); + assert.throws(() => queue.issueToken(pa.approvalId, "frank"), /already issued/); + } finally { + cleanup(); + } +}); + +test("anonymous approvals refused; unknown approval refused; denied not approvable", () => { + const { queue, cleanup } = tempQueue(); + try { + const pa = queue.propose(intent(), "non-dca"); + assert.throws(() => queue.issueToken(pa.approvalId, " "), /approver identity required/); + assert.throws(() => queue.issueToken("pa_nope", "frank"), /unknown approval/); + queue.deny(pa.approvalId, "changed my mind"); + assert.throws(() => queue.issueToken(pa.approvalId, "frank"), /denied/); + assert.equal(queue.listPending().length, 0); + } finally { + cleanup(); + } +}); + +test("terminal states are immutable — deny after execute or after deny is refused", () => { + const { queue, cleanup } = tempQueue(); + try { + const ti = intent(); + const pa = queue.propose(ti, "non-dca"); + const token = queue.issueToken(pa.approvalId, "frank"); + queue.consumeToken(token, ti.intentId); + assert.throws(() => queue.deny(pa.approvalId, "too late"), /already executed/); + + const pa2 = queue.propose(intent(), "non-dca"); + queue.deny(pa2.approvalId, "no"); + assert.throws(() => queue.deny(pa2.approvalId, "again"), /already denied/); + } finally { + cleanup(); + } +}); + +test("queue state survives a restart (durable JSONL event log)", () => { + const dir = mkdtempSync(join(tmpdir(), "trade-gate-approvals-")); + try { + const ti = intent(); + const q1 = new ApprovalQueue(dir); + const pa = q1.propose(ti, "non-dca"); + const token = q1.issueToken(pa.approvalId, "frank"); + + const q2 = new ApprovalQueue(dir); + // The consumed-token guard and the intent binding survive the restart. + const done = q2.consumeToken(token, ti.intentId); + assert.equal(done.status, "executed"); + + const q3 = new ApprovalQueue(dir); + assert.throws(() => q3.consumeToken(token, ti.intentId), /already consumed/); + } finally { + rmSync(dir, { recursive: true, force: true }); + } +}); diff --git a/verticals/investment-intelligence/mcp/trade-gate/src/approval.ts b/verticals/investment-intelligence/mcp/trade-gate/src/approval.ts new file mode 100644 index 00000000..70ec7805 --- /dev/null +++ b/verticals/investment-intelligence/mcp/trade-gate/src/approval.ts @@ -0,0 +1,195 @@ +/** + * Human-approval queue + single-use approval tokens — durable. + * + * ⚠️ v0.1. UNAUDITED. NOT FOR LIVE FUNDS. + * + * `propose` returns a PENDING object and NOTHING ELSE — no code path in this + * class resolves a proposal to executable except `issueToken` (the human's + * explicit act, out of the agent's hands) followed by `consumeToken` at + * execution. One token per approval, single-use, bound to its intent. A lost + * token is not re-issued: deny and re-propose (fail closed beats convenient). + * + * State is an append-only JSONL event log (`approvals.jsonl`), replayed on + * construction so pending approvals and consumed tokens survive a restart. + */ + +import { randomUUID } from "node:crypto"; +import { + appendFileSync, + existsSync, + mkdirSync, + readFileSync, +} from "node:fs"; +import { dirname, join } from "node:path"; +import type { PendingApproval, TradeIntent } from "./types.js"; +import { resolveDataDir } from "./audit.js"; + +const APPROVALS_FILE = "approvals.jsonl"; + +type ApprovalEvent = + | { type: "proposed"; approval: PendingApproval; ts: number } + | { type: "token-issued"; approvalId: string; token: string; approver: string; ts: number } + | { type: "consumed"; approvalId: string; token: string; ts: number } + | { type: "denied"; approvalId: string; reason: string; ts: number }; + +interface TokenRecord { + token: string; + approvalId: string; + consumed: boolean; +} + +export class ApprovalQueue { + private readonly approvals = new Map(); + private readonly tokens = new Map(); + /** approvalId → token, to enforce one-token-per-approval. */ + private readonly issuedFor = new Map(); + private readonly path: string; + + constructor(dataDir?: string) { + this.path = join(resolveDataDir(dataDir), APPROVALS_FILE); + mkdirSync(dirname(this.path), { recursive: true }); + this.load(); + } + + private load(): void { + if (!existsSync(this.path)) return; + const raw = readFileSync(this.path, "utf8"); + for (const line of raw.split("\n")) { + const trimmed = line.trim(); + if (!trimmed) continue; + let ev: ApprovalEvent; + try { + ev = JSON.parse(trimmed) as ApprovalEvent; + } catch { + continue; // corrupt line skipped, never rewritten + } + this.apply(ev); + } + } + + /** Pure state transition, shared by replay and live paths. */ + private apply(ev: ApprovalEvent): void { + switch (ev.type) { + case "proposed": + this.approvals.set(ev.approval.approvalId, { ...ev.approval }); + break; + case "token-issued": { + const a = this.approvals.get(ev.approvalId); + if (a) a.status = "token-issued"; + this.tokens.set(ev.token, { token: ev.token, approvalId: ev.approvalId, consumed: false }); + this.issuedFor.set(ev.approvalId, ev.token); + break; + } + case "consumed": { + const t = this.tokens.get(ev.token); + if (t) t.consumed = true; + const a = this.approvals.get(ev.approvalId); + if (a) a.status = "executed"; + break; + } + case "denied": { + const a = this.approvals.get(ev.approvalId); + if (a) a.status = "denied"; + break; + } + } + } + + /** Durable write FIRST; memory mutates only after the event persists. */ + private record(ev: ApprovalEvent): void { + appendFileSync(this.path, JSON.stringify(ev) + "\n", "utf8"); + this.apply(ev); + } + + /** Create a pending approval. Never resolves to approved by itself. */ + propose(intent: TradeIntent, reason: string, now: number = Date.now()): PendingApproval { + const approval: PendingApproval = { + approvalId: `pa_${randomUUID()}`, + status: "pending-human-approval", + intent, + reason, + createdAt: now, + }; + this.record({ type: "proposed", approval, ts: now }); + return { ...approval }; + } + + /** + * The human's explicit act: issue the single-use approval token for a + * pending approval. Fail-closed errors: unknown approval, already executed + * or denied, token already issued (lost token → deny + re-propose). + */ + issueToken(approvalId: string, approver: string, now: number = Date.now()): string { + const approval = this.approvals.get(approvalId); + if (!approval) { + throw new Error(`unknown approval '${approvalId}'`); + } + if (approval.status === "executed" || approval.status === "denied") { + throw new Error(`approval '${approvalId}' is ${approval.status} — not approvable`); + } + if (this.issuedFor.has(approvalId)) { + throw new Error( + `token already issued for '${approvalId}' — a lost token is not re-issued; deny and re-propose`, + ); + } + if (!approver || !approver.trim()) { + throw new Error("approver identity required — anonymous approvals are refused"); + } + const token = `at_${randomUUID()}`; + this.record({ type: "token-issued", approvalId, token, approver, ts: now }); + return token; + } + + /** + * Consume a token at execution time. Single-use, bound to the intent it was + * issued for. Throws on: unknown token, consumed token, intent mismatch. + */ + consumeToken(token: string, intentId: string, now: number = Date.now()): PendingApproval { + const rec = this.tokens.get(token); + if (!rec) { + throw new Error("unknown approval token — refusing execution"); + } + if (rec.consumed) { + throw new Error("approval token already consumed — single-use is absolute"); + } + const approval = this.approvals.get(rec.approvalId); + if (!approval) { + throw new Error(`token references unknown approval '${rec.approvalId}'`); + } + if (approval.intent.intentId !== intentId) { + throw new Error( + `token was issued for intent '${approval.intent.intentId}', not '${intentId}' — refusing execution`, + ); + } + this.record({ type: "consumed", approvalId: rec.approvalId, token, ts: now }); + return { ...approval, status: "executed" }; + } + + /** Deny a pending approval (human act; also the recovery path for a lost token). */ + deny(approvalId: string, reason: string, now: number = Date.now()): void { + const approval = this.approvals.get(approvalId); + if (!approval) { + throw new Error(`unknown approval '${approvalId}'`); + } + if (approval.status === "executed" || approval.status === "denied") { + throw new Error(`approval '${approvalId}' is already ${approval.status} — terminal states are immutable`); + } + this.record({ type: "denied", approvalId, reason, ts: now }); + } + + /** Approvals still awaiting a human (pending or token-issued-but-unexecuted). */ + listPending(): PendingApproval[] { + return [...this.approvals.values()] + .filter((a) => a.status === "pending-human-approval" || a.status === "token-issued") + .map((a) => ({ ...a })); + } + + get(approvalId: string): PendingApproval | undefined { + const a = this.approvals.get(approvalId); + return a ? { ...a } : undefined; + } + + filePath(): string { + return this.path; + } +} diff --git a/verticals/investment-intelligence/mcp/trade-gate/src/audit.ts b/verticals/investment-intelligence/mcp/trade-gate/src/audit.ts new file mode 100644 index 00000000..fa6a9fea --- /dev/null +++ b/verticals/investment-intelligence/mcp/trade-gate/src/audit.ts @@ -0,0 +1,86 @@ +/** + * Append-only audit log — durable JSONL. + * + * ⚠️ v0.1. UNAUDITED. NOT FOR LIVE FUNDS. + * + * Invariant (ports payment-intelligence-system L1): no trade action exists + * without a prior audit entry, and if the log write fails the action fails. + * Append-only — never edited, never deleted, never reordered. + * + * Each entry persists to `.trade-gate-data/audit.jsonl` (override via + * TRADE_GATE_DATA_DIR or constructor) FIRST; the in-memory array is a fast + * read mirror loaded at construction. A failed disk write throws, so the + * caller fails closed. + */ + +import { + appendFileSync, + existsSync, + mkdirSync, + readFileSync, +} from "node:fs"; +import { dirname, join } from "node:path"; +import type { AuditEntry } from "./types.js"; + +const DEFAULT_DIR = ".trade-gate-data"; +const AUDIT_FILE = "audit.jsonl"; + +/** Resolve the data dir: explicit arg → env → default. */ +export function resolveDataDir(dataDir?: string): string { + return dataDir ?? process.env.TRADE_GATE_DATA_DIR ?? DEFAULT_DIR; +} + +export class AuditLog { + private readonly entries: AuditEntry[] = []; + private readonly path: string; + + constructor(dataDir?: string) { + this.path = join(resolveDataDir(dataDir), AUDIT_FILE); + mkdirSync(dirname(this.path), { recursive: true }); + this.load(); + } + + private load(): void { + if (!existsSync(this.path)) return; + const raw = readFileSync(this.path, "utf8"); + for (const line of raw.split("\n")) { + const trimmed = line.trim(); + if (!trimmed) continue; + try { + const parsed = JSON.parse(trimmed) as AuditEntry; + this.entries.push(Object.freeze(parsed)); + } catch { + // A corrupt line is skipped on read but never rewritten — the file is + // append-only and history is not edited. + } + } + } + + /** + * Append an entry. Persists to JSONL FIRST, then mirrors in memory. Throws + * on a malformed entry OR a failed disk write so the caller fails the trade + * action closed — an unloggable decision must not proceed. + */ + append(entry: Omit & { ts?: number }): AuditEntry { + if (!entry.action || typeof entry.action !== "string") { + throw new Error("audit append failed: missing action — failing closed"); + } + const stored: AuditEntry = Object.freeze({ ...entry, ts: entry.ts ?? Date.now() }); + appendFileSync(this.path, JSON.stringify(stored) + "\n", "utf8"); + this.entries.push(stored); + return stored; + } + + /** Read-only snapshot. Returns a copy so callers cannot mutate the log. */ + all(): readonly AuditEntry[] { + return [...this.entries]; + } + + size(): number { + return this.entries.length; + } + + filePath(): string { + return this.path; + } +} diff --git a/verticals/investment-intelligence/mcp/trade-gate/src/brokers/alpaca.stub.ts b/verticals/investment-intelligence/mcp/trade-gate/src/brokers/alpaca.stub.ts new file mode 100644 index 00000000..381b3226 --- /dev/null +++ b/verticals/investment-intelligence/mcp/trade-gate/src/brokers/alpaca.stub.ts @@ -0,0 +1,22 @@ +/** + * Alpaca adapter — NOT_WIRED stub, by design. + * + * No live-broker code or credentials exist in this repo. The operator wires + * the official Alpaca MCP server V2 locally (paper-by-default: + * ALPACA_PAPER_TRADE=True) per the vertical's RUNBOOK.md, and only then flips + * this adapter. Until that local act, execution against 'alpaca' fails closed. + */ + +import type { BrokerAdapter, Fill, TradeIntent } from "../types.js"; + +export class AlpacaStub implements BrokerAdapter { + readonly id = "alpaca" as const; + readonly mode = "live" as const; + + execute(_intent: TradeIntent): Fill { + throw new Error( + "NOT_WIRED: alpaca adapter is a stub — wire the official Alpaca MCP locally per RUNBOOK.md; " + + "no live-broker code or credentials ship in this repo", + ); + } +} diff --git a/verticals/investment-intelligence/mcp/trade-gate/src/brokers/coinbase.stub.ts b/verticals/investment-intelligence/mcp/trade-gate/src/brokers/coinbase.stub.ts new file mode 100644 index 00000000..deeccfb8 --- /dev/null +++ b/verticals/investment-intelligence/mcp/trade-gate/src/brokers/coinbase.stub.ts @@ -0,0 +1,22 @@ +/** + * Coinbase adapter — NOT_WIRED stub, by design. + * + * Crypto execution goes through Coinbase for Agents / AgentKit (MPC wallet, + * programmable session caps, per-transaction limits) wired locally per + * RUNBOOK.md — custody is never self-built and never lives in this repo. + * Until that local act, execution against 'coinbase' fails closed. + */ + +import type { BrokerAdapter, Fill, TradeIntent } from "../types.js"; + +export class CoinbaseStub implements BrokerAdapter { + readonly id = "coinbase" as const; + readonly mode = "live" as const; + + execute(_intent: TradeIntent): Fill { + throw new Error( + "NOT_WIRED: coinbase adapter is a stub — wire Coinbase AgentKit locally per RUNBOOK.md; " + + "no live-broker code or credentials ship in this repo", + ); + } +} diff --git a/verticals/investment-intelligence/mcp/trade-gate/src/brokers/ibkr.stub.ts b/verticals/investment-intelligence/mcp/trade-gate/src/brokers/ibkr.stub.ts new file mode 100644 index 00000000..f0bc593e --- /dev/null +++ b/verticals/investment-intelligence/mcp/trade-gate/src/brokers/ibkr.stub.ts @@ -0,0 +1,22 @@ +/** + * Interactive Brokers adapter — NOT_WIRED stub, by design. + * + * IBKR is the natural EU live path: its "AI Instructions" review tab is a + * broker-side human approval gate that composes with (never replaces) this + * package's approval token. The operator wires it locally per RUNBOOK.md. + * Until that local act, execution against 'ibkr' fails closed. + */ + +import type { BrokerAdapter, Fill, TradeIntent } from "../types.js"; + +export class IbkrStub implements BrokerAdapter { + readonly id = "ibkr" as const; + readonly mode = "live" as const; + + execute(_intent: TradeIntent): Fill { + throw new Error( + "NOT_WIRED: ibkr adapter is a stub — wire IBKR locally per RUNBOOK.md; " + + "no live-broker code or credentials ship in this repo", + ); + } +} diff --git a/verticals/investment-intelligence/mcp/trade-gate/src/brokers/paper.ts b/verticals/investment-intelligence/mcp/trade-gate/src/brokers/paper.ts new file mode 100644 index 00000000..c4993ea5 --- /dev/null +++ b/verticals/investment-intelligence/mcp/trade-gate/src/brokers/paper.ts @@ -0,0 +1,29 @@ +/** + * Paper broker — the ONLY functional adapter in this repo. + * + * ⚠️ v0.1. UNAUDITED. Simulated fills only; no market connection, no + * slippage model, no order book. Exists so the full propose → approve → + * execute → audit → trajectory loop can run end-to-end with zero capital risk. + */ + +import { randomUUID } from "node:crypto"; +import type { BrokerAdapter, Fill, TradeIntent } from "../types.js"; + +export class PaperBroker implements BrokerAdapter { + readonly id = "paper" as const; + readonly mode = "paper" as const; + + execute(intent: TradeIntent): Fill { + return { + fillId: `fill_${randomUUID()}`, + intentId: intent.intentId, + broker: this.id, + status: "filled-paper", + instrument: intent.instrument, + side: intent.side, + notional: intent.notional, + currency: intent.currency, + filledAt: Date.now(), + }; + } +} diff --git a/verticals/investment-intelligence/mcp/trade-gate/src/caps.test.ts b/verticals/investment-intelligence/mcp/trade-gate/src/caps.test.ts new file mode 100644 index 00000000..4080be7d --- /dev/null +++ b/verticals/investment-intelligence/mcp/trade-gate/src/caps.test.ts @@ -0,0 +1,131 @@ +import { test } from "node:test"; +import assert from "node:assert/strict"; +import { mkdtempSync, rmSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; + +import { CapLedger, dcaEligible } from "./caps.js"; +import type { CapPolicy, TradeIntent } from "./types.js"; + +const CAPS: CapPolicy = { perOrder: 500, perDay: 1000, perAssetClassDay: { crypto: 300 } }; + +function intent(overrides: Partial = {}): TradeIntent { + return { + intentId: `ti_${Math.random().toString(36).slice(2)}`, + instrument: "VWCE", + assetClass: "etf", + side: "buy", + notional: 100, + currency: "EUR", + broker: "paper", + dca: false, + ...overrides, + }; +} + +function tempLedger(): { ledger: CapLedger; dir: string; cleanup: () => void } { + const dir = mkdtempSync(join(tmpdir(), "trade-gate-caps-")); + return { ledger: new CapLedger(dir), dir, cleanup: () => rmSync(dir, { recursive: true, force: true }) }; +} + +test("within all caps → within-cap", () => { + const { ledger, cleanup } = tempLedger(); + try { + assert.equal(ledger.check(intent(), CAPS).verdict, "within-cap"); + } finally { + cleanup(); + } +}); + +test("over per-order cap → over-cap (escalate, never approve)", () => { + const { ledger, cleanup } = tempLedger(); + try { + const res = ledger.check(intent({ notional: 501 }), CAPS); + assert.equal(res.verdict, "over-cap"); + assert.match(res.reason, /per-order/); + } finally { + cleanup(); + } +}); + +test("per-day cap accumulates across commits", () => { + const { ledger, cleanup } = tempLedger(); + try { + const now = Date.now(); + for (let i = 0; i < 3; i++) { + const ti = intent({ notional: 300 }); + assert.equal(ledger.check(ti, CAPS, now).verdict, "within-cap"); + ledger.commit(ti, now); + } + // 900 committed; another 300 breaches the 1000/day cap. + const res = ledger.check(intent({ notional: 300 }), CAPS, now); + assert.equal(res.verdict, "over-cap"); + assert.match(res.reason, /per-day/); + } finally { + cleanup(); + } +}); + +test("per-asset-class day cap is tighter than the global day cap", () => { + const { ledger, cleanup } = tempLedger(); + try { + const now = Date.now(); + const btc = intent({ assetClass: "crypto", instrument: "BTC-EUR", notional: 200 }); + assert.equal(ledger.check(btc, CAPS, now).verdict, "within-cap"); + ledger.commit(btc, now); + const more = intent({ assetClass: "crypto", instrument: "BTC-EUR", notional: 200 }); + const res = ledger.check(more, CAPS, now); + assert.equal(res.verdict, "over-cap"); + assert.match(res.reason, /'crypto'/); + } finally { + cleanup(); + } +}); + +test("replayed intent id → reject; re-commit throws", () => { + const { ledger, cleanup } = tempLedger(); + try { + const ti = intent(); + ledger.commit(ti); + assert.equal(ledger.check(ti, CAPS).verdict, "reject"); + assert.throws(() => ledger.commit(ti), /refusing to re-commit/); + } finally { + cleanup(); + } +}); + +test("replay protection survives a restart (durable JSONL)", () => { + const dir = mkdtempSync(join(tmpdir(), "trade-gate-caps-")); + try { + const ti = intent(); + new CapLedger(dir).commit(ti); + const reloaded = new CapLedger(dir); + assert.equal(reloaded.isConsumed(ti.intentId), true); + assert.equal(reloaded.check(ti, CAPS).verdict, "reject"); + } finally { + rmSync(dir, { recursive: true, force: true }); + } +}); + +test("invalid notional → reject", () => { + const { ledger, cleanup } = tempLedger(); + try { + assert.equal(ledger.check(intent({ notional: 0 }), CAPS).verdict, "reject"); + assert.equal(ledger.check(intent({ notional: Number.NaN }), CAPS).verdict, "reject"); + } finally { + cleanup(); + } +}); + +test("dcaEligible: whitelisted within-max buy passes; everything else fails", () => { + const wl = [{ instrument: "VWCE", side: "buy" as const, maxNotional: 250 }]; + assert.equal(dcaEligible(intent({ dca: true, notional: 200 }), wl).verdict, "within-cap"); + // not flagged + assert.equal(dcaEligible(intent({ dca: false }), wl).verdict, "reject"); + // sells never DCA + assert.equal(dcaEligible(intent({ dca: true, side: "sell" }), wl).verdict, "reject"); + // not whitelisted + assert.equal(dcaEligible(intent({ dca: true, instrument: "TSLA" }), wl).verdict, "reject"); + // over whitelist max + assert.equal(dcaEligible(intent({ dca: true, notional: 251 }), wl).verdict, "reject"); +}); diff --git a/verticals/investment-intelligence/mcp/trade-gate/src/caps.ts b/verticals/investment-intelligence/mcp/trade-gate/src/caps.ts new file mode 100644 index 00000000..cbd93388 --- /dev/null +++ b/verticals/investment-intelligence/mcp/trade-gate/src/caps.ts @@ -0,0 +1,215 @@ +/** + * Cap enforcement + single-execution replay guard + DCA whitelist — durable. + * + * ⚠️ v0.1. UNAUDITED. NOT FOR LIVE FUNDS. + * + * Over ANY cap → over-cap (escalates to the human gate; NEVER auto-approved). + * An intent id that already committed spend → reject (replay). + * + * Committed spend persists to `.trade-gate-data/spend.jsonl` so the replay + * guard and the rolling 24h totals survive a restart. `check` never mutates; + * `commit` appends the durable event FIRST, then mutates memory. + */ + +import { + appendFileSync, + existsSync, + mkdirSync, + readFileSync, +} from "node:fs"; +import { dirname, join } from "node:path"; +import type { + AssetClass, + CapPolicy, + CapResult, + DcaWhitelistEntry, + TradeIntent, +} from "./types.js"; +import { resolveDataDir } from "./audit.js"; + +const DAY_MS = 24 * 60 * 60 * 1000; +const SPEND_FILE = "spend.jsonl"; + +interface SpendEvent { + intentId: string; + ts: number; + notional: number; + assetClass: AssetClass; +} + +export class CapLedger { + private readonly consumed = new Set(); + /** Rolling-window records for the 24h caps; pruned so growth is bounded. */ + private records: SpendEvent[] = []; + private readonly path: string; + + constructor(dataDir?: string) { + this.path = join(resolveDataDir(dataDir), SPEND_FILE); + mkdirSync(dirname(this.path), { recursive: true }); + this.load(); + } + + private load(): void { + if (!existsSync(this.path)) return; + const raw = readFileSync(this.path, "utf8"); + const cutoff = Date.now() - DAY_MS; + for (const line of raw.split("\n")) { + const trimmed = line.trim(); + if (!trimmed) continue; + let ev: SpendEvent; + try { + ev = JSON.parse(trimmed) as SpendEvent; + } catch { + continue; // corrupt line skipped on read, never rewritten + } + if (!ev.intentId || typeof ev.notional !== "number") continue; + // Replay protection is lifetime; only the 24h window seeds `records` + // (older events carry no rolling-cap signal and would grow memory unbounded). + this.consumed.add(ev.intentId); + if (ev.ts > cutoff) { + this.records.push(ev); + } + } + } + + isConsumed(intentId: string): boolean { + return this.consumed.has(intentId); + } + + private prune(now: number): void { + const cutoff = now - DAY_MS; + this.records = this.records.filter((r) => r.ts > cutoff); + } + + private spentToday(now: number): number { + const cutoff = now - DAY_MS; + return this.records + .filter((r) => r.ts > cutoff) + .reduce((sum, r) => sum + r.notional, 0); + } + + private spentTodayOnClass(assetClass: AssetClass, now: number): number { + const cutoff = now - DAY_MS; + return this.records + .filter((r) => r.assetClass === assetClass && r.ts > cutoff) + .reduce((sum, r) => sum + r.notional, 0); + } + + /** + * Evaluate an intent against the caps. Does NOT mutate — call `commit` + * separately once the intent is cleared to count against the ledger. + * + * @param now injectable clock (ms) for deterministic tests. + */ + check(intent: TradeIntent, caps: CapPolicy, now: number = Date.now()): CapResult { + this.prune(now); + + // Replay guard first: an intent that already spent is never re-spendable. + if (this.consumed.has(intent.intentId)) { + return { + verdict: "reject", + reason: `replay: intent ${intent.intentId} already committed`, + }; + } + + if (!Number.isFinite(intent.notional) || intent.notional <= 0) { + return { verdict: "reject", reason: `invalid notional '${String(intent.notional)}'` }; + } + + if (intent.notional > caps.perOrder) { + return { + verdict: "over-cap", + reason: + `over per-order cap: ${intent.notional.toFixed(2)} ${intent.currency} ` + + `> ${caps.perOrder.toFixed(2)}`, + }; + } + + const day = this.spentToday(now) + intent.notional; + if (day > caps.perDay) { + return { + verdict: "over-cap", + reason: + `over per-day cap: ${day.toFixed(2)} ${intent.currency} > ${caps.perDay.toFixed(2)}`, + }; + } + + const classCap = caps.perAssetClassDay?.[intent.assetClass]; + if (classCap !== undefined) { + const classDay = this.spentTodayOnClass(intent.assetClass, now) + intent.notional; + if (classDay > classCap) { + return { + verdict: "over-cap", + reason: + `over per-day cap for '${intent.assetClass}': ` + + `${classDay.toFixed(2)} ${intent.currency} > ${classCap.toFixed(2)}`, + }; + } + } + + return { + verdict: "within-cap", + reason: + `within all caps (order ${intent.notional.toFixed(2)} / day ${day.toFixed(2)} ` + + `${intent.currency})`, + }; + } + + /** + * Count the intent against the ledger and consume its id. Throws on a + * replay — the caller must `check` first. Durable write FIRST: if the append + * throws, no in-memory state is mutated, so replay protection cannot be lost + * to a crash. + */ + commit(intent: TradeIntent, now: number = Date.now()): void { + if (this.consumed.has(intent.intentId)) { + throw new Error(`refusing to re-commit intent ${intent.intentId}`); + } + const ev: SpendEvent = { + intentId: intent.intentId, + ts: now, + notional: intent.notional, + assetClass: intent.assetClass, + }; + appendFileSync(this.path, JSON.stringify(ev) + "\n", "utf8"); + this.consumed.add(intent.intentId); + this.records.push(ev); + } + + filePath(): string { + return this.path; + } +} + +/** + * DCA-whitelist eligibility — the ONLY auto-approvable class. Verified + * against the pre-declared whitelist, never trusted from the intent's own + * `dca` flag. Sells are never DCA (accumulation only). + */ +export function dcaEligible( + intent: TradeIntent, + whitelist: readonly DcaWhitelistEntry[], +): CapResult { + if (!intent.dca) { + return { verdict: "reject", reason: "intent not flagged dca" }; + } + if (intent.side !== "buy") { + return { verdict: "reject", reason: "dca is accumulation only — sells take the human gate" }; + } + const entry = whitelist.find((w) => w.instrument === intent.instrument); + if (!entry) { + return { + verdict: "reject", + reason: `instrument '${intent.instrument}' not on the dca whitelist`, + }; + } + if (intent.notional > entry.maxNotional) { + return { + verdict: "reject", + reason: + `dca notional ${intent.notional.toFixed(2)} > whitelist max ` + + `${entry.maxNotional.toFixed(2)} for '${intent.instrument}'`, + }; + } + return { verdict: "within-cap", reason: `dca-eligible for '${intent.instrument}'` }; +} diff --git a/verticals/investment-intelligence/mcp/trade-gate/src/e2e.test.ts b/verticals/investment-intelligence/mcp/trade-gate/src/e2e.test.ts new file mode 100644 index 00000000..eb414679 --- /dev/null +++ b/verticals/investment-intelligence/mcp/trade-gate/src/e2e.test.ts @@ -0,0 +1,285 @@ +/** + * End-to-end MCP integration test. + * + * ⚠️ v0.1. UNAUDITED. NOT FOR LIVE FUNDS. + * + * Runs the trade-gate server IN-PROCESS against a temp data dir through a real + * SDK client over an InMemoryTransport pair. Proves the invariants: + * - exactly the 5 gate tools exist; no order/credential tool bypasses them; + * - RED: a non-DCA intent can NEVER execute without a human approval token; + * - RED: a live-broker (alpaca) execution attempt fails NOT_WIRED even WITH approval; + * - over-cap → pending (never auto-approved), DCA included; + * - DCA-whitelisted within-cap buy → auto-approved → paper fill; + * - approval token is single-use; an executed intent never executes twice; + * - every tool output carries the R5 non-advisory footer; + * - the audit JSONL records every step, schema-parseable. + */ + +import { test } from "node:test"; +import assert from "node:assert/strict"; +import { mkdtempSync, readFileSync, rmSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; + +import { Client } from "@modelcontextprotocol/sdk/client/index.js"; +import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js"; + +import type { TradeIntent } from "./types.js"; +import { buildServer, R5_FOOTER } from "./index.js"; + +const CAPS = { perOrder: 500, perDay: 1000, perAssetClassDay: { crypto: 300 } }; +const WHITELIST = [{ instrument: "VWCE", side: "buy" as const, maxNotional: 250 }]; + +function intent(overrides: Partial = {}): TradeIntent { + return { + intentId: `ti_${Math.random().toString(36).slice(2)}`, + instrument: "VWCE", + assetClass: "etf", + side: "buy", + notional: 100, + currency: "EUR", + broker: "paper", + dca: false, + ...overrides, + }; +} + +async function connect(): Promise<{ client: Client; dir: string; close: () => Promise }> { + const dir = mkdtempSync(join(tmpdir(), "trade-gate-e2e-")); + const server = buildServer({ dataDir: dir }); + const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); + const client = new Client({ name: "trade-gate-e2e", version: "0.1.0" }); + await Promise.all([server.connect(serverTransport), client.connect(clientTransport)]); + return { + client, + dir, + close: async () => { + await client.close(); + await server.close(); + rmSync(dir, { recursive: true, force: true }); + }, + }; +} + +type ToolResult = { + structuredContent?: Record; + content?: Array<{ type: string; text?: string }>; + isError?: boolean; +}; + +function sc(res: unknown): Record { + const s = (res as ToolResult).structuredContent; + assert.ok(s, "expected structuredContent on the tool result"); + return s!; +} + +function textOf(res: unknown): string { + return ((res as ToolResult).content ?? []).map((c) => c.text ?? "").join("\n"); +} + +test("E2E: exactly the 5 gate tools exist; every output carries the R5 footer", async () => { + const { client, close } = await connect(); + try { + const { tools } = await client.listTools(); + const names = tools.map((t) => t.name).sort(); + assert.deepEqual(names, [ + "execute_approved", + "list_pending", + "propose_trade", + "read_audit", + "request_approval", + ]); + + const res = await client.callTool({ name: "list_pending", arguments: {} }); + assert.ok(textOf(res).includes(R5_FOOTER.trim().split("\n")[1] ?? ""), "R5 footer missing"); + } finally { + await close(); + } +}); + +test("E2E RED: non-DCA intent NEVER executes without a human approval token", async () => { + const { client, close } = await connect(); + try { + const ti = intent(); + const proposed = await client.callTool({ + name: "propose_trade", + arguments: { intent: ti, caps: CAPS }, + }); + assert.equal(sc(proposed).verdict, "pending"); + + // The red case: try to execute with no token. + const res = await client.callTool({ + name: "execute_approved", + arguments: { intentId: ti.intentId }, + }); + assert.equal((res as ToolResult).isError, true, "executing without approval MUST fail"); + assert.equal(sc(res).verdict, "rejected"); + assert.match(String(sc(res).reason ?? sc(res).error), /approval token required/); + + // And with a forged token. + const forged = await client.callTool({ + name: "execute_approved", + arguments: { intentId: ti.intentId, approvalToken: "at_forged" }, + }); + assert.equal((forged as ToolResult).isError, true, "a forged token MUST fail"); + } finally { + await close(); + } +}); + +test("E2E RED: live broker (alpaca) fails NOT_WIRED even WITH human approval", async () => { + const { client, close } = await connect(); + try { + const ti = intent({ broker: "alpaca" }); + const proposed = await client.callTool({ + name: "propose_trade", + arguments: { intent: ti, caps: CAPS }, + }); + const approvalId = String(sc(proposed).approvalId); + + const approved = await client.callTool({ + name: "request_approval", + arguments: { approvalId, approver: "frank", decision: "approve" }, + }); + const token = String(sc(approved).approvalToken); + + const res = await client.callTool({ + name: "execute_approved", + arguments: { intentId: ti.intentId, approvalToken: token }, + }); + assert.equal((res as ToolResult).isError, true, "live broker MUST fail closed in-repo"); + assert.match(String(sc(res).error), /NOT_WIRED/); + } finally { + await close(); + } +}); + +test("E2E: over-cap → pending (never auto-approved), DCA included", async () => { + const { client, close } = await connect(); + try { + // DCA-whitelisted instrument, but over the per-order cap. + const ti = intent({ dca: true, notional: 600 }); + const res = await client.callTool({ + name: "propose_trade", + arguments: { intent: ti, caps: CAPS, dcaWhitelist: WHITELIST }, + }); + assert.equal(sc(res).verdict, "pending"); + assert.match(String(sc(res).reason), /per-order/); + } finally { + await close(); + } +}); + +test("E2E: DCA-whitelisted within-cap buy → auto-approved → paper fill, audited", async () => { + const { client, dir, close } = await connect(); + try { + const ti = intent({ dca: true, notional: 200 }); + const proposed = await client.callTool({ + name: "propose_trade", + arguments: { intent: ti, caps: CAPS, dcaWhitelist: WHITELIST }, + }); + assert.equal(sc(proposed).verdict, "auto-approved"); + + const executed = await client.callTool({ + name: "execute_approved", + arguments: { intentId: ti.intentId }, + }); + assert.equal(sc(executed).verdict, "executed"); + const fill = sc(executed).fill as { status: string; broker: string }; + assert.equal(fill.status, "filled-paper"); + assert.equal(fill.broker, "paper"); + + // The audit JSONL has the full chain, every line parseable. + const lines = readFileSync(join(dir, "audit.jsonl"), "utf8") + .split("\n") + .filter((l) => l.trim()) + .map((l) => JSON.parse(l) as { action: string }); + const actions = lines.map((l) => l.action); + assert.ok(actions.includes("propose_trade")); + assert.ok(actions.includes("execute_attempt")); + assert.ok(actions.includes("executed")); + } finally { + await close(); + } +}); + +test("E2E: full human-gate loop — propose → approve → execute; token single-use; no double execution", async () => { + const { client, close } = await connect(); + try { + const ti = intent({ notional: 400 }); + const proposed = await client.callTool({ + name: "propose_trade", + arguments: { intent: ti, caps: CAPS }, + }); + assert.equal(sc(proposed).verdict, "pending"); + const approvalId = String(sc(proposed).approvalId); + + const pendingList = await client.callTool({ name: "list_pending", arguments: {} }); + assert.equal((sc(pendingList).pending as unknown[]).length, 1); + + const approved = await client.callTool({ + name: "request_approval", + arguments: { approvalId, approver: "frank", decision: "approve" }, + }); + const token = String(sc(approved).approvalToken); + assert.ok(token.startsWith("at_")); + + const executed = await client.callTool({ + name: "execute_approved", + arguments: { intentId: ti.intentId, approvalToken: token }, + }); + assert.equal(sc(executed).verdict, "executed"); + + // Replay: same intent, same token → refused. + const replay = await client.callTool({ + name: "execute_approved", + arguments: { intentId: ti.intentId, approvalToken: token }, + }); + assert.equal((replay as ToolResult).isError, true, "double execution MUST fail"); + } finally { + await close(); + } +}); + +test("E2E: deny closes a pending approval; approving it afterwards is refused", async () => { + const { client, close } = await connect(); + try { + const ti = intent(); + const proposed = await client.callTool({ + name: "propose_trade", + arguments: { intent: ti, caps: CAPS }, + }); + const approvalId = String(sc(proposed).approvalId); + + const denied = await client.callTool({ + name: "request_approval", + arguments: { approvalId, approver: "frank", decision: "deny", reason: "not this week" }, + }); + assert.equal(sc(denied).status, "denied"); + + const late = await client.callTool({ + name: "request_approval", + arguments: { approvalId, approver: "frank", decision: "approve" }, + }); + assert.equal((late as ToolResult).isError, true); + } finally { + await close(); + } +}); + +test("E2E: malformed intent is rejected by input validation (fail closed)", async () => { + const { client, close } = await connect(); + try { + const res = await client.callTool({ + name: "propose_trade", + arguments: { + intent: { ...intent(), notional: -5 }, + caps: CAPS, + }, + }); + // Zod rejects at the tool boundary → SDK surfaces an error result. + assert.equal((res as ToolResult).isError, true); + } finally { + await close(); + } +}); diff --git a/verticals/investment-intelligence/mcp/trade-gate/src/index.ts b/verticals/investment-intelligence/mcp/trade-gate/src/index.ts new file mode 100644 index 00000000..5a577924 --- /dev/null +++ b/verticals/investment-intelligence/mcp/trade-gate/src/index.ts @@ -0,0 +1,414 @@ +#!/usr/bin/env node +/** + * Trade-gate MCP — fail-closed approve/execute surface for the Investment + * Intelligence vertical. + * + * ⚠️ v0.1. UNAUDITED. NOT FOR LIVE FUNDS. + * + * Five tools: + * - propose_trade TradeIntent → auto-approved (DCA whitelist, capped) | pending | rejected + * - request_approval the human approves (single-use token) or denies a pending intent + * - list_pending pending-approval queue + * - execute_approved approved intent → broker adapter (paper is the only functional one) + * - read_audit append-only JSONL audit trail + * + * Invariants: + * - No live-broker code or credentials exist in this repo: alpaca/ibkr/coinbase + * adapters throw NOT_WIRED. Paper is the default and the only in-repo fill path. + * - No non-DCA intent executes without a human-issued single-use approval token. + * - Over ANY cap → pending (escalate) — never auto-approved, DCA included. + * - Audit-first: the attempt is logged before the broker is called; a failed + * audit write fails the action. + * - Every tool output carries the R5 non-advisory footer. + * + * Pattern lineage: payment-intelligence-system/mcp (shape reused, repo untouched — + * trading is a distinct risk domain). Board record: + * docs/boards/2026-07-02-investment-intelligence-vertical-spawn.md + */ + +import { + appendFileSync, + existsSync, + mkdirSync, + readFileSync, +} from "node:fs"; +import { dirname, join } from "node:path"; +import { fileURLToPath } from "node:url"; +import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; +import { z } from "zod"; + +import type { BrokerAdapter, BrokerId, TradeIntent } from "./types.js"; +import { AuditLog, resolveDataDir } from "./audit.js"; +import { CapLedger, dcaEligible } from "./caps.js"; +import { ApprovalQueue } from "./approval.js"; +import { PaperBroker } from "./brokers/paper.js"; +import { AlpacaStub } from "./brokers/alpaca.stub.js"; +import { IbkrStub } from "./brokers/ibkr.stub.js"; +import { CoinbaseStub } from "./brokers/coinbase.stub.js"; + +export const R5_FOOTER = + "\n—\nThis is system architecture, not financial / investment / tax advice. " + + "Outputs frame decisions; jurisdiction-specific counsel signs off on instruments. " + + "The practitioner accepts capital risk; the substrate accepts no claim."; + +// ---- Zod schemas (input validation is itself a fail-closed gate) ---- +const intentSchema = z.object({ + intentId: z.string().min(1).max(100), + instrument: z.string().min(1).max(64), + assetClass: z.enum(["equity", "etf", "fund", "bond", "crypto", "other"]), + side: z.enum(["buy", "sell"]), + notional: z.number().positive(), + currency: z.string().min(1).max(10), + broker: z.enum(["paper", "alpaca", "ibkr", "coinbase"]), + dca: z.boolean(), + rationaleRef: z.string().max(256).optional(), +}); + +const capsSchema = z.object({ + perOrder: z.number().positive(), + perDay: z.number().positive(), + perAssetClassDay: z + .record(z.enum(["equity", "etf", "fund", "bond", "crypto", "other"]), z.number().positive()) + .optional(), +}); + +const dcaWhitelistSchema = z.array( + z.object({ + instrument: z.string().min(1).max(64), + side: z.literal("buy"), + maxNotional: z.number().positive(), + }), +); + +function textResult(text: string, structured: Record) { + return { + content: [{ type: "text" as const, text: text + R5_FOOTER }], + structuredContent: structured, + }; +} + +function errorResult(text: string, structured: Record) { + return { + content: [{ type: "text" as const, text: text + R5_FOOTER }], + structuredContent: structured, + isError: true, + }; +} + +/** + * Durable registry of auto-approved (DCA) intents and executed intent ids. + * Append-only JSONL (`gate.jsonl`), replayed on construction, so an approved + * DCA order and the executed-once guard survive a restart. + */ +class ExecutionRegistry { + private readonly autoApproved = new Map(); + private readonly executed = new Set(); + private readonly path: string; + + constructor(dataDir?: string) { + this.path = join(resolveDataDir(dataDir), "gate.jsonl"); + mkdirSync(dirname(this.path), { recursive: true }); + this.load(); + } + + private load(): void { + if (!existsSync(this.path)) return; + for (const line of readFileSync(this.path, "utf8").split("\n")) { + const trimmed = line.trim(); + if (!trimmed) continue; + try { + this.apply(JSON.parse(trimmed)); + } catch { + continue; // corrupt line skipped, never rewritten + } + } + } + + private apply(ev: { type: string; intent?: TradeIntent; intentId?: string }): void { + if (ev.type === "auto-approved" && ev.intent) { + this.autoApproved.set(ev.intent.intentId, ev.intent); + } else if (ev.type === "executed" && ev.intentId) { + this.executed.add(ev.intentId); + } + } + + private record(ev: Record): void { + appendFileSync(this.path, JSON.stringify({ ...ev, ts: Date.now() }) + "\n", "utf8"); + this.apply(ev as { type: string; intent?: TradeIntent; intentId?: string }); + } + + registerAutoApproved(intent: TradeIntent): void { + this.record({ type: "auto-approved", intent }); + } + + getAutoApproved(intentId: string): TradeIntent | undefined { + return this.autoApproved.get(intentId); + } + + markExecuted(intentId: string): void { + this.record({ type: "executed", intentId }); + } + + isExecuted(intentId: string): boolean { + return this.executed.has(intentId); + } +} + +/** + * Build the trade-gate server with its own durable state. Exported so tests + * run it in-process (InMemoryTransport) against a temp data dir. + */ +export function buildServer(opts: { dataDir?: string } = {}): McpServer { + const caps = new CapLedger(opts.dataDir); + const queue = new ApprovalQueue(opts.dataDir); + const audit = new AuditLog(opts.dataDir); + const registry = new ExecutionRegistry(opts.dataDir); + const brokers: Record = { + paper: new PaperBroker(), + alpaca: new AlpacaStub(), + ibkr: new IbkrStub(), + coinbase: new CoinbaseStub(), + }; + + const server = new McpServer({ + name: "trade-gate-mcp", + version: "0.1.0", + }); + + server.registerTool( + "propose_trade", + { + title: "Propose Trade", + description: + "Submit a TradeIntent to the gate. FAILS CLOSED: replayed or malformed intents are " + + "rejected; over ANY cap → pending (never auto-approved); only DCA-whitelisted, " + + "within-cap buys auto-approve (still capped + audited). Everything else awaits a " + + "human approval token. Does not place orders.", + inputSchema: { + intent: intentSchema, + caps: capsSchema, + dcaWhitelist: dcaWhitelistSchema.optional(), + }, + annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: false }, + }, + async ({ intent, caps: capPolicy, dcaWhitelist }) => { + const ti = intent as TradeIntent; + + try { + const capResult = caps.check(ti, capPolicy); + if (capResult.verdict === "reject") { + audit.append({ action: "propose_trade", intentId: ti.intentId, verdict: "rejected", reason: capResult.reason }); + return textResult(`REJECTED: ${capResult.reason}`, { verdict: "rejected", reason: capResult.reason }); + } + + if (capResult.verdict === "over-cap") { + // Escalate — never auto-approve over a cap, DCA included. + const pending = queue.propose(ti, capResult.reason); + audit.append({ action: "propose_trade", intentId: ti.intentId, approvalId: pending.approvalId, verdict: "pending", reason: capResult.reason }); + return textResult( + `PENDING human approval (${pending.approvalId}): ${capResult.reason}`, + { verdict: "pending", approvalId: pending.approvalId, reason: capResult.reason }, + ); + } + + // Within caps. DCA whitelist is the only auto-approve path. + if (ti.dca) { + const dca = dcaEligible(ti, dcaWhitelist ?? []); + if (dca.verdict === "within-cap") { + caps.commit(ti); + registry.registerAutoApproved(ti); + audit.append({ action: "propose_trade", intentId: ti.intentId, verdict: "auto-approved", reason: dca.reason, instrument: ti.instrument, notional: ti.notional, currency: ti.currency }); + return textResult( + `AUTO-APPROVED (dca whitelist, capped + audited): ${dca.reason}`, + { verdict: "auto-approved", intentId: ti.intentId, reason: dca.reason }, + ); + } + // dca flag claimed but not eligible → human gate with the reason. + const pending = queue.propose(ti, `dca claim failed: ${dca.reason}`); + audit.append({ action: "propose_trade", intentId: ti.intentId, approvalId: pending.approvalId, verdict: "pending", reason: `dca claim failed: ${dca.reason}` }); + return textResult( + `PENDING human approval (${pending.approvalId}): dca claim failed — ${dca.reason}`, + { verdict: "pending", approvalId: pending.approvalId, reason: dca.reason }, + ); + } + + const pending = queue.propose(ti, "non-dca intent — human gate"); + audit.append({ action: "propose_trade", intentId: ti.intentId, approvalId: pending.approvalId, verdict: "pending", reason: "non-dca intent — human gate" }); + return textResult( + `PENDING human approval (${pending.approvalId}): non-dca intent — human gate`, + { verdict: "pending", approvalId: pending.approvalId, reason: "non-dca intent — human gate" }, + ); + } catch (err) { + // Fail closed: an unloggable or unpersistable decision does not stand. + return errorResult(`GATE FAILED CLOSED: ${(err as Error).message}`, { + verdict: "rejected", + error: (err as Error).message, + }); + } + }, + ); + + server.registerTool( + "request_approval", + { + title: "Request Approval (human act)", + description: + "The HUMAN approves or denies a pending intent. 'approve' issues a single-use " + + "approval token bound to that intent; 'deny' closes it. One token per approval — " + + "a lost token is not re-issued (deny and re-propose). Anonymous approvals refused.", + inputSchema: { + approvalId: z.string().min(1), + approver: z.string().min(1).describe("Human identity, e.g. 'frank'"), + decision: z.enum(["approve", "deny"]), + reason: z.string().optional(), + }, + annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: false }, + }, + async ({ approvalId, approver, decision, reason }) => { + try { + if (decision === "deny") { + queue.deny(approvalId, reason ?? "denied by human"); + audit.append({ action: "request_approval", approvalId, verdict: "denied", reason: reason ?? "denied by human", actor: approver }); + return textResult(`DENIED ${approvalId}`, { approvalId, status: "denied" }); + } + const token = queue.issueToken(approvalId, approver); + audit.append({ action: "request_approval", approvalId, verdict: "token-issued", actor: approver }); + return textResult( + `APPROVED ${approvalId} — single-use token issued. Pass it to execute_approved.`, + { approvalId, status: "token-issued", approvalToken: token }, + ); + } catch (err) { + return errorResult(`APPROVAL REFUSED: ${(err as Error).message}`, { + approvalId, + error: (err as Error).message, + }); + } + }, + ); + + server.registerTool( + "list_pending", + { + title: "List Pending Approvals", + description: "Pending-approval queue (pending or token-issued-but-unexecuted). Read-only.", + inputSchema: {}, + annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: false }, + }, + async () => { + const pending = queue.listPending(); + return textResult(`${pending.length} pending approval(s)`, { pending }); + }, + ); + + server.registerTool( + "execute_approved", + { + title: "Execute Approved Intent", + description: + "Execute an approved intent via its broker adapter. Paper is the only functional " + + "adapter in-repo; alpaca/ibkr/coinbase throw NOT_WIRED until the operator wires them " + + "locally. Auto-approved DCA intents execute by intentId; everything else requires the " + + "human's single-use approval token. Audit-first: the attempt is logged before the " + + "broker is called. An executed intent never executes twice.", + inputSchema: { + intentId: z.string().min(1), + approvalToken: z.string().optional(), + }, + annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: false }, + }, + async ({ intentId, approvalToken }) => { + try { + if (registry.isExecuted(intentId)) { + audit.append({ action: "execute_approved", intentId, verdict: "rejected", reason: "already executed — single execution is absolute" }); + return errorResult(`REJECTED: intent ${intentId} already executed`, { + verdict: "rejected", + reason: "already executed", + }); + } + + let intent = registry.getAutoApproved(intentId); + if (!intent) { + // Not auto-approved: the human's single-use token is the only path. + if (!approvalToken) { + audit.append({ action: "execute_approved", intentId, verdict: "rejected", reason: "no approval token for non-dca intent — human gate holds" }); + return errorResult( + `REJECTED: intent ${intentId} is not auto-approved and no approval token was supplied — the human gate holds`, + { verdict: "rejected", reason: "approval token required" }, + ); + } + // Burns the token on the attempt (single-use is absolute, even if the + // broker then refuses — a failed live attempt requires fresh approval). + const approval = queue.consumeToken(approvalToken, intentId); + intent = approval.intent; + } + + // Audit-first: the attempt exists in the log before any broker runs. + audit.append({ action: "execute_attempt", intentId, broker: intent.broker, instrument: intent.instrument, notional: intent.notional, currency: intent.currency }); + + const adapter = brokers[intent.broker]; + let fill; + try { + fill = adapter.execute(intent); + } catch (err) { + audit.append({ action: "execution_failed", intentId, broker: intent.broker, reason: (err as Error).message }); + return errorResult(`EXECUTION FAILED (${intent.broker}): ${(err as Error).message}`, { + verdict: "execution-failed", + broker: intent.broker, + error: (err as Error).message, + }); + } + + registry.markExecuted(intentId); + audit.append({ action: "executed", intentId, broker: intent.broker, verdict: fill.status, instrument: fill.instrument, notional: fill.notional, currency: fill.currency }); + return textResult( + `EXECUTED (${fill.status}) ${fill.side} ${fill.notional.toFixed(2)} ${fill.currency} ${fill.instrument} via ${fill.broker} — ${fill.fillId}`, + { verdict: "executed", fill }, + ); + } catch (err) { + return errorResult(`EXECUTION REFUSED: ${(err as Error).message}`, { + verdict: "rejected", + error: (err as Error).message, + }); + } + }, + ); + + server.registerTool( + "read_audit", + { + title: "Read Audit Trail", + description: "Read the append-only audit log (most recent last). Read-only.", + inputSchema: { limit: z.number().int().positive().max(1000).optional() }, + annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: false }, + }, + async ({ limit }) => { + const all = audit.all(); + const entries = limit ? all.slice(-limit) : all; + return textResult(`${entries.length} audit entr${entries.length === 1 ? "y" : "ies"} (of ${all.length})`, { + entries, + total: all.length, + file: audit.filePath(), + }); + }, + ); + + return server; +} + +async function main() { + const server = buildServer(); + const transport = new StdioServerTransport(); + await server.connect(transport); + // stderr only — stdout is the MCP stdio channel. + console.error( + "trade-gate-mcp v0.1.0 (fail-closed, paper-only, human gate above DCA) on stdio — NOT FOR LIVE FUNDS", + ); +} + +if (process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1]) { + main().catch((err) => { + console.error("trade-gate-mcp fatal:", err); + process.exit(1); + }); +} diff --git a/verticals/investment-intelligence/mcp/trade-gate/src/types.ts b/verticals/investment-intelligence/mcp/trade-gate/src/types.ts new file mode 100644 index 00000000..1b6a6a41 --- /dev/null +++ b/verticals/investment-intelligence/mcp/trade-gate/src/types.ts @@ -0,0 +1,102 @@ +/** + * Trade-gate types. + * + * ⚠️ v0.1. UNAUDITED. NOT FOR LIVE FUNDS. + * + * Pattern lineage: payment-intelligence-system/mcp (fail-closed verdicts, + * append-only audit, human gate). Trading is a distinct risk domain — this + * package reuses the shape, not the repo. + */ + +export type AssetClass = "equity" | "etf" | "fund" | "bond" | "crypto" | "other"; +export type Side = "buy" | "sell"; +export type BrokerId = "paper" | "alpaca" | "ibkr" | "coinbase"; + +/** A structured, not-yet-approved trade proposal. Never an order by itself. */ +export interface TradeIntent { + intentId: string; + instrument: string; + assetClass: AssetClass; + side: Side; + /** Order size in currency units. */ + notional: number; + currency: string; + broker: BrokerId; + /** Claims DCA-whitelist eligibility — verified against the whitelist, never trusted. */ + dca: boolean; + /** Reference to the thesis / strategy-session artifact that produced this intent. */ + rationaleRef?: string; +} + +export interface CapPolicy { + /** Max notional per single order. */ + perOrder: number; + /** Max total notional per rolling 24h across all asset classes. */ + perDay: number; + /** Optional per-asset-class rolling 24h ceilings (tighter than perDay). */ + perAssetClassDay?: Partial>; +} + +/** Pre-declared recurring buys — the only auto-approvable class. */ +export interface DcaWhitelistEntry { + instrument: string; + /** DCA is accumulation only; sells always take the human gate. */ + side: "buy"; + maxNotional: number; +} + +export type CapVerdict = "within-cap" | "over-cap" | "reject"; + +export interface CapResult { + verdict: CapVerdict; + reason: string; +} + +export type ProposeVerdict = "auto-approved" | "pending" | "rejected"; + +export type ApprovalStatus = + | "pending-human-approval" + | "token-issued" + | "executed" + | "denied"; + +export interface PendingApproval { + approvalId: string; + status: ApprovalStatus; + intent: TradeIntent; + reason: string; + createdAt: number; +} + +export interface Fill { + fillId: string; + intentId: string; + broker: BrokerId; + /** Paper is the only fill status that exists in-repo. */ + status: "filled-paper"; + instrument: string; + side: Side; + notional: number; + currency: string; + filledAt: number; +} + +export interface BrokerAdapter { + readonly id: BrokerId; + readonly mode: "paper" | "live"; + execute(intent: TradeIntent): Fill; +} + +export interface AuditEntry { + ts: number; + action: string; + intentId?: string; + approvalId?: string; + verdict?: string; + reason?: string; + actor?: string; + broker?: string; + instrument?: string; + notional?: number; + currency?: string; +} diff --git a/verticals/investment-intelligence/mcp/trade-gate/tsconfig.json b/verticals/investment-intelligence/mcp/trade-gate/tsconfig.json new file mode 100644 index 00000000..88be4dc9 --- /dev/null +++ b/verticals/investment-intelligence/mcp/trade-gate/tsconfig.json @@ -0,0 +1,19 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "outDir": "./dist", + "rootDir": "./src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "declaration": true, + "sourceMap": true, + "noUncheckedIndexedAccess": true, + "noImplicitOverride": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist", "src/**/*.test.ts"] +} diff --git a/verticals/investment-intelligence/templates/hermes-finance-profile/mcp-servers.json b/verticals/investment-intelligence/templates/hermes-finance-profile/mcp-servers.json new file mode 100644 index 00000000..f7dbb609 --- /dev/null +++ b/verticals/investment-intelligence/templates/hermes-finance-profile/mcp-servers.json @@ -0,0 +1,35 @@ +{ + "note": "Hermes Agent MCP wiring for the finance-research profile. Paths are operator-local; adjust per RUNBOOK.md. No credentials belong in this file — brokers wire via their own official MCPs with their own local config.", + "servers": { + "trade-gate": { + "transport": "stdio", + "command": "node", + "args": ["/verticals/investment-intelligence/mcp/trade-gate/dist/index.js"], + "env": { "TRADE_GATE_DATA_DIR": "/trade-gate" }, + "tools_used": ["propose_trade", "list_pending", "read_audit"], + "tools_never_used": ["request_approval"], + "note": "request_approval is the human's surface. The profile must not call it." + }, + "ghostfolio": { + "transport": "http", + "url": "http://localhost:3333", + "note": "Self-hosted portfolio tracker (T0). Read-only API token scoped to reads." + }, + "actual-budget": { + "transport": "http", + "url": "http://localhost:5006", + "note": "Self-hosted local-first ledger (T0). Read-only." + }, + "openbb": { + "transport": "http", + "url": "http://localhost:8000", + "note": "OpenBB ODP self-hosted (T0 host, public data). BYO data-vendor keys stay in OpenBB's own config." + } + }, + "observability": { + "langfuse": { + "url": "http://localhost:3000", + "note": "Self-hosted, OTel. Traces stay on the box. See docs/OBSERVABILITY.md." + } + } +} diff --git a/verticals/investment-intelligence/templates/hermes-finance-profile/profile.json b/verticals/investment-intelligence/templates/hermes-finance-profile/profile.json new file mode 100644 index 00000000..58f46b25 --- /dev/null +++ b/verticals/investment-intelligence/templates/hermes-finance-profile/profile.json @@ -0,0 +1,33 @@ +{ + "name": "finance-research", + "description": "Investment Intelligence research-and-propose profile for Hermes Agent. Sovereign-first: local weights by default, aggregates-only off-T0, human gate above DCA. Never an execution agent.", + "version": "0.1.0", + "models": { + "default": { + "provider": "local", + "model": "hermes-4.3-36b", + "note": "T0 sovereign tier — Ollama/LM Studio/Hermes Desktop local weights. Fallback for 8GB GPUs: hermes-4-14b." + }, + "overflow": { + "provider": "openrouter", + "model": "nousresearch/hermes-4-405b", + "note": "T2 — public-data research only. Aggregates and public data; never raw balances. Nous Portal skipped by machine policy." + } + }, + "system_context": { + "r5_clause": "This is system architecture, not financial / investment / tax advice. Outputs frame decisions; jurisdiction-specific counsel signs off on instruments. The practitioner accepts capital risk; the substrate accepts no claim.", + "rules": [ + "Open every finance artifact with the R5 clause. Non-waivable.", + "Data classification per ROUTING.md: raw balances/positions/tax data are T0-only. On any cloud model, work from percentage weights and band labels.", + "You research and propose. propose_trade and list_pending are yours; request_approval belongs to the human's surface — never call it, never ask for a token.", + "Never place, claim, or simulate an order outside the trade-gate MCP. A pending intent is reported as pending.", + "No credentials, keys, or account numbers in any message, memory entry, or skill — refuse and flag.", + "Messaging gateways receive notifications and aggregates only. A chat channel is not T0.", + "Concrete-sounding-stat-without-source is refused. Cite the mechanism or drop the number.", + "Self-authored skills touching the gate, brokers, or private data are proposed as diffs for human review before activation." + ] + }, + "mcp": { "servers": "./mcp-servers.json" }, + "skills": { "seeds": "./skills.md" }, + "attestation": "Built on SIP — Investment Intelligence hermes-finance-profile v0.1" +} diff --git a/verticals/investment-intelligence/templates/hermes-finance-profile/skills.md b/verticals/investment-intelligence/templates/hermes-finance-profile/skills.md new file mode 100644 index 00000000..ebc9b597 --- /dev/null +++ b/verticals/investment-intelligence/templates/hermes-finance-profile/skills.md @@ -0,0 +1,24 @@ +# Seed skills — hermes-finance-profile + +> Starting skill set for the finance-research profile. Hermes Agent's skill-authoring loop will grow these; anything gate-adjacent it writes is reviewed as a diff before activation. + +--- + +## morning-pulse (cron: weekdays 07:30) +Read watchlist + macro snapshot via OpenBB; compare against the current regime thesis; message a 5-line pulse (aggregates only) to the operator's channel. No proposals from this skill — pulse only. Open with the R5 clause. + +## snapshot-prep (cron: Sunday 17:00) +Compose the weekly aggregate portfolio context: weights by asset class from Ghostfolio, ledger deltas from Actual Budget, DPI category mix (band labels, never amounts). Output feeds `/invest-snapshot` and the weekly `/invest-strategy` session. + +## dca-heartbeat (cron: per whitelist cadence) +For each DCA-whitelist entry: call `propose_trade` with the pre-declared intent. Auto-approval and caps are the gate's business, not yours — report the verdict verbatim. If the verdict is `pending` (cap drift), notify the operator; never retry to force it through. + +## pending-nudge (cron: daily 18:00) +Call `list_pending`; if anything has waited >48h, remind the operator with the intent summary and its reason. Never approve, never ask for the token. + +## retro-reminder (cron: last Sunday of month, 16:00) +Remind the operator to run `/invest-retro`; attach the count of trajectories written since the last retro and any executed intents missing a trajectory record (that gap is pipeline drift — flag it explicitly). + +--- + +**Built on SIP** — hermes-finance-profile seed skills · v0.1 diff --git a/verticals/wealth/MEMORY.md b/verticals/wealth/MEMORY.md index 7fb2ca99..dd24f202 100644 --- a/verticals/wealth/MEMORY.md +++ b/verticals/wealth/MEMORY.md @@ -32,13 +32,15 @@ - `v0.1` · `2026-04-26` · Spawned as v7.5.1 OpenClaw CRITICAL-1 remediation. Operational `/wealth-dpi` command exists since pre-v7.0; this directory closes the substrate-canon gap. Frank's instance state lives in `private/` per privacy framework. - `v0.2` · `2026-05-17` · Evolved to **first reference instance of Composition Layer substrate primitive** per `docs/boards/2026-05-17-crypto-investment-spawn.md` R1.a close-out (Frank-acked). Composition-layer doctrine added to `STACK.md`. Existing DPI ledger + Thesis engine + Gate ladder framework set preserved as Wealth IS's own commands (R3.a close-out). Crypto Intelligence Domain Sub-Stack scaffolded at `verticals/crypto-intelligence/` (R4 proof-of-pattern via House of On-Chain only; Investment IS held until v0.1-proof-pass). New cross-asset commands declared: `/wealth-portfolio-fit`, `/wealth-sovereignty-design`, `/wealth-cycle-thesis`. Wealth IS daily-5 declared per R2 cognitive-load discipline. **Falsifier:** `verticals/wealth/commands/` must ship ≥3 cross-asset commands by 2026-06-16, else composition-layer concept failed for Wealth IS. -- `v0.3` · `2026-06-19` · Integrated the **Sovereign Creator Architecture (SCA)** and registered the public `dpi` substrate, the `realityarchitect.ai` curriculum, and the `agenticincome.ai` network as active Wealth IS implementations. +- `v0.3` · `2026-06-19` · Integrated the **Sovereign Creator Architecture (SCA)** and registered the public `dpi` substrate, the `realityarchitect.ai` curriculum, and the `agenticincome.ai` network as active Wealth IS implementations. *(Correction recorded at v0.4: this entry still narrated the v0.2 cross-asset commands as "declared" three days after the falsifier default had already fired — see below.)* +- `v0.4` · `2026-07-02` · **Falsifier outcome recorded: collapse to ACL-only.** `verticals/wealth/commands/` was empty at the 2026-06-16 deadline and no explicit ship decision was made, so the 2026-05-29 board's designed default fired (`docs/boards/2026-05-29-wealth-composition-falsifier.md`). The v0.2-declared cross-asset commands (`/wealth-portfolio-fit`, `/wealth-sovereignty-design`, `/wealth-cycle-thesis`) never shipped and are withdrawn as declarations. README rewritten as ACL composition manifest. Cross-asset composition now runs inside `verticals/investment-intelligence/` (spawned same date per `docs/boards/2026-07-02-investment-intelligence-vertical-spawn.md`, R4 gate passed) with explicit lineage citation. Composition-layer primitive survives in `STACK.md`; Wealth IS may re-declare when a real recurring cross-asset decision earns it. ### Reference lineage SHAs - v0.1 spawn — pending v7.5.1 commit SHA. - v0.2 composition-layer evolution — `23cace2` (2026-05-17, tag `v8.1.0`). +- v0.4 ACL collapse + Investment IS spawn — this change-set (branch `claude/wealth-os-agent-architecture-efj4hb`). --- -**Built on SIP** — Wealth IS MEMORY.md · v0.1 · SIP v1.1.0 +**Built on SIP** — Wealth IS MEMORY.md · v0.4 · SIP v1.1.1 diff --git a/verticals/wealth/README.md b/verticals/wealth/README.md index d6e301a5..3f1db31a 100644 --- a/verticals/wealth/README.md +++ b/verticals/wealth/README.md @@ -1,58 +1,27 @@ -# Wealth IS — Capital & Deal Intelligence (Composition Layer) +# Wealth IS — Capital & Deal Intelligence (ACL-only composition manifest) -> Intelligence System layer 2 in the 10-IS taxonomy. **First reference instance of the Composition Layer substrate primitive** (declared `STACK.md` 2026-05-17 per `docs/boards/2026-05-17-crypto-investment-spawn.md` R1.a close-out). Disruptive Passive Income ledger, capital allocation, deal flow, compounding curves, gate-to-freedom thresholds — preserved here as Wealth IS's own commands. Crypto Intelligence + Investment Intelligence Domain Sub-Stacks compose under this layer via explicit `Composes-with: Wealth IS / ` declarations. +> Intelligence System layer 2 in the 10-IS taxonomy. **ACL-only declarative reference** since 2026-06-16: the Composition Layer falsifier (declared `STACK.md` 2026-05-17) fired as designed — `verticals/wealth/commands/` was empty at the 30-day deadline and no explicit ship decision was recorded, so Wealth IS collapsed to the falsifier's default per `docs/boards/2026-05-29-wealth-composition-falsifier.md`. Affirmed and recorded by `docs/boards/2026-07-02-investment-intelligence-vertical-spawn.md`. Re-declarable the day a real cross-asset decision earns operational commands. -**Tier:** Universal Intelligence System layer (#2 in locked 10-IS taxonomy) + Composition Layer (first reference instance). +**Tier:** Universal Intelligence System layer (#2 in locked 10-IS taxonomy) — ACL-only (declarative reference, no operational commands of its own). **License:** MIT for substrate-aligned reference patterns. **Instance content stays private** — real €amounts, deal flow, gate ladder values never land public. -**Status:** `composition-layer v0.2 — substrate primitive declared 2026-05-17` (prior `v0.1` scaffolded 2026-04-26 per OpenClaw v7.5 CRITICAL-1). +**Status:** `acl-only v0.4 — falsifier default fired 2026-06-16, recorded 2026-07-02` (prior: `composition-layer v0.2` declared 2026-05-17; `v0.1` scaffolded 2026-04-26). --- -## What Wealth IS does (composition-layer expression) +## What Wealth IS is now -Wealth IS is where active income becomes Disruptive Passive Income (DPI) and where capital allocation runs as system, not impulse. As a composition layer, Wealth IS owns the **cross-asset rules** that compose over its Domain Sub-Stacks (Crypto Intelligence + Investment Intelligence + future Real-Estate IS + Stewardship IS) without duplicating their sub-system content. +Wealth IS is where active income becomes Disruptive Passive Income (DPI) and where capital allocation runs as system, not impulse. As an ACL, this directory **owns no commands** — it names the operational surfaces where wealth intelligence actually runs and the composition relationships between them. -**Composition-layer responsibilities (own commands):** -- **DPI ledger** — tracks compounding sources across asset classes, diversification, passive-vs-active income split, gate-to-freedom thresholds. (`/wealth-dpi` shipped pre-v7.0.) -- **Thesis engine** — every DPI source has a stated thesis, mechanism, risk profile, and exit signal. (`/wealth-thesis-review` v0.2 — composes Crypto IS House of Research output + Investment IS House of Thesis output.) -- **Gate ladder discipline** — declared income thresholds the practitioner has committed to crossing before allowing lifestyle expansion. Instance-private (real € values stay in `private/`). (`/wealth-gate-progress` v0.2.) -- **Capital allocation architecture** — entity-aware (Holding BV / operating BV / personal), tax-aware (jurisdiction-specific), liquidity-tiered. Composes cross-asset. -- **Diversification map** — concentration risk flags, correlation analysis between DPI sources. Reads from sub-stack outputs. +## The operational surfaces (where the work happens) -**Cross-asset commands (composition-layer outputs):** -- `/wealth-portfolio-fit` — composes Crypto IS House of Allocation + Investment IS House of Allocation + (future) Real-Estate IS Allocation into one cross-asset allocation review. -- `/wealth-sovereignty-design` — composes Crypto IS House of Sovereignty (custody architecture) + Investment IS entity-aware capital flow + Family IS multi-generational considerations into one sovereignty architecture. -- `/wealth-cycle-thesis` — cross-asset cycle-position thesis composing Crypto IS House of Macro + Investment IS regime-fit-test. - -**What this layer does NOT own** (lives in the Domain Sub-Stacks beneath): -- Crypto-specific protocol/mechanism analysis (Crypto IS House of On-Chain, House of DeFi, House of Macro own these). -- Investment-process workflow (Thesis → Sourcing → DD → Allocation → Portfolio → Exit lives in Investment IS). -- Trade-level decisions, single-position sizing, market-timing calls (sub-stack houses own these). - ---- - -## Wealth IS daily-5 (cognitive-load discipline per People IS v7.4.1 ruling) - -Wealth IS at composition-layer tier is the toolbox; five are the daily hands: - -| Command | Why this one first | -|---|---| -| `/wealth-dpi` | The ledger that grounds every other Wealth move. Read before any allocation decision. | -| `/wealth-thesis-review` | Quarterly thesis pressure-test across all DPI sources. Compounds vs. drift. | -| `/wealth-portfolio-fit` | Cross-asset allocation review — Crypto + Investment + future Real-Estate composed. | -| `/wealth-gate-progress` | Gate-ladder progress against declared thresholds. Discipline against lifestyle creep. | -| `/wealth-sovereignty-design` | Custody + entity + jurisdiction architecture. Foundation before any new DPI source onboards. | - -The other 3-5 future commands (cycle thesis, tax sanity cross-asset, divestiture discipline) are toolbox-tier — bring in when the season demands. - -## Active Implementations & Substrates - -Wealth IS is implemented across the following repositories in the Starlight ecosystem: -- **`dpi` Substrate** (`github.com/frankxai/dpi`): The open creator-facing wealth layer. Contains ledger schemas (`private/ledger.json`), platforms indices, MCP servers, and proactive challenge generator tools. -- **Sovereign Creator Architecture (SCA)**: Unifies the ACOS execution engine and the `dpi` substrate with the `realityarchitect.ai` curriculum. -- **`agenticincome` Network** (`github.com/frankxai/agenticincome`): The affiliate monetization spoke engine. Consumes the `affiliate-agent-skills` comparison engine to generate passive cash flows. - ---- +| Surface | Where | What it owns | +|---|---|---| +| **DPI ledger** | `.claude/commands/wealth-dpi.md` (local install) | Compounding sources across asset classes, passive-vs-active split, gate ladder G1–G5, thesis stress-test, kill criteria. | +| **Investment Intelligence** | `verticals/investment-intelligence/` | The decision engine: 11-agent analysis→risk→synthesis swarm, thesis debate, trade-gate MCP (fail-closed approve/execute), ReasoningBank trajectory memory, sovereignty routing tiers. | +| **Crypto Intelligence** | `verticals/crypto-intelligence/` | Crypto-as-asset-class depth: 6 Houses (On-Chain / Macro / DeFi / Sovereignty / Research / Allocation). Compose as domain overlays into the Investment IS analysis layer. | +| **Personal wealth ops (instance)** | `FrankX/.claude/skills/wealth-ops` + `frankx.ai/admin/wealth` + `/admin/bv-command-center` | Jurisdiction-specific (NL) property / mortgage / Box 3 / BV / DGA / FIRE planning. Instance-private data. | +| **Income engine** | `agenticincome` network + `agentic-income-skills` | Cash-flow generation that feeds the DPI ledger. | +| **Payments governance** | `payment-intelligence-system` | Fail-closed authorization for agentic payments (distinct risk domain from trading; pattern source for the trade-gate). | ## Composition with the rest of the 10-IS stack @@ -64,32 +33,27 @@ Wealth IS is implemented across the following repositories in the Starlight ecos | **Family IS** | Multi-generational wealth transfer is Wealth × Family composition. | | **Starlight Orchestrator** | Routes "where does my next DPI source come from" intent. | -| Composes via Domain Sub-Stacks beneath | Why | +| Domain Sub-Stacks beneath | Why | |---|---| -| **Crypto Intelligence** (`verticals/crypto-intelligence/`) | Crypto-as-asset-class with deep on-chain + protocol + sovereignty depth. Composes via House of Allocation → DPI ledger; House of Sovereignty → Wealth-sovereignty-design; House of Macro → Wealth-cycle-thesis. | -| **Investment Intelligence** (planned per Board R4 gate) | Traditional capital allocation workflow (Thesis → Sourcing → DD → Allocation → Portfolio → Exit). Composes via House of Thesis → Thesis engine; House of Allocation → DPI ledger; House of Portfolio → Diversification map. | -| **Real-Estate Intelligence** (future) | Real-asset class with property-specific workflow + jurisdictional tax structure. | +| **Crypto Intelligence** (`verticals/crypto-intelligence/`) | House of Allocation → DPI ledger; House of Sovereignty → custody architecture; House of Macro → cycle thesis. | +| **Investment Intelligence** (`verticals/investment-intelligence/`) | Traditional + cross-asset capital allocation workflow; the engine that composes crypto House outputs, DPI ledger state, and wealth-ops instance data into decisions. | +| **Real-Estate Intelligence** (future) | Real-asset class with property-specific workflow + jurisdictional tax structure. Instance practice currently lives in FrankX wealth-ops. | | **Stewardship Intelligence** (future) | Multi-generational wealth architecture, gifting strategy, family office governance. | ---- - ## Refusal patterns (corruption modes this layer rejects) -- **Concrete-sounding-stat-without-source.** Inventing return numbers without sourced theses. Universal across Wealth IS and its sub-stacks; named in Sound IS and _template SOUL.md. -- **Wealth-advice-as-financial-advisor-relationship.** Wealth IS architects the system; licensed counsel reviews jurisdiction-specific instruments. Composition-layer outputs are not advice. -- **Decisions made under emotional duress without thesis review.** The Thesis engine exists to prevent this; gating discipline matters more than thesis sophistication. +- **Concrete-sounding-stat-without-source.** Inventing return numbers without sourced theses. Universal across Wealth IS and its sub-stacks. +- **Wealth-advice-as-financial-advisor-relationship.** Wealth IS architects the system; licensed counsel reviews jurisdiction-specific instruments. Outputs are not advice. +- **Decisions made under emotional duress without thesis review.** The thesis engine (Investment IS) exists to prevent this; gating discipline matters more than thesis sophistication. - **Public artifacts disclosing real €amounts or named third-party deal flow without consent.** Instance-private values stay in `private/`. Pattern-level reference content is MIT; €-level instance state is closed. -- **Tax positioning relying on aggressive interpretations without specialist sign-off.** The system surfaces tax-aware structure; jurisdiction-specific instruments require qualified specialist. -- **Cross-asset composition without lineage citation.** Composition-layer commands MUST name which sub-stack outputs they consume. Silent cross-asset rules erode trust in the composition primitive. - ---- - -## Substrate primitive: Composition Layer (first reference) +- **Tax positioning relying on aggressive interpretations without specialist sign-off.** +- **Cross-asset composition without lineage citation.** Any surface composing across sub-stacks MUST name which outputs it consumes. +- **Ship-to-beat-a-falsifier.** Recorded corruption mode from the 2026-05-29 board: operational surface is earned by real decisions, not by deadlines. -Wealth IS is the first reference instance of the **Composition Layer substrate primitive** (`STACK.md` § Composition Layer, 2026-05-17). The pattern is opt-in for any universal IS that has spawned ≥2 Domain Sub-Stacks and needs cross-domain rules above them. +## Re-declaration condition -**Falsifier (Wealth IS specifically):** If `verticals/wealth/commands/` is empty at 30 days post-2026-05-17 declaration, the composition-layer concept failed for Wealth IS — collapse to ACL-only role (declarative reference, no operational commands). 30-day deadline: 2026-06-16. +Per the falsifier's design, Wealth IS may re-declare a composition layer (own cross-asset commands at `verticals/wealth/commands/`) when ≥1 genuine recurring cross-asset decision exists that neither Investment IS nor Crypto IS can own alone. Until then, cross-asset composition runs inside `verticals/investment-intelligence/` with explicit lineage citation. --- -**Built on SIP** — Wealth IS composition-layer reference · v0.2 · SIP v1.1.0 (composition-layer primitive declared 2026-05-17 per Board R1.a close-out) +**Built on SIP** — Wealth IS ACL manifest · v0.4 · SIP v1.1.1 (falsifier default recorded per 2026-07-02 board) diff --git a/workflows/wealth/WORKFLOW.md b/workflows/wealth/WORKFLOW.md index 20604ac5..689568c3 100644 --- a/workflows/wealth/WORKFLOW.md +++ b/workflows/wealth/WORKFLOW.md @@ -78,11 +78,17 @@ inputs: **Action:** Write deal verdict to strategic-vault, DPI update to technical-vault, allocation decision to operational-vault. If gate criterion cleared, advance gate ladder position. Attach SIP attestation to all vault atoms. **Output:** Vault atoms x3, gate-ladder advancement (conditional). -## Hermes Swarm Config +## Engine + swarm config (superseded 2026-07-02) -Which Hermes agent profiles handle tasks in this workflow: -- Primary: hermes-wealth-specialist (405B — thesis construction, deal analysis, complex return modeling) -- Support: hermes-wealth-executor (70B — DPI math, vault writes, gate scoring, fast deal triage) +The notional hermes-3 405B/70B profile pair is superseded. Decision work routes through +`verticals/investment-intelligence/` — the 11-agent engine (`engine/agents/catalog.json`) +with model routing per `verticals/investment-intelligence/ROUTING.md`: +- T0 local Hermes 4.3 36B / Hermes 4 14B for anything touching private financial state +- T1 Sonnet (analysis/risk) + Opus (portfolio-manager) + Haiku (technical) for public-data work +- T2 OpenRouter Hermes 4 70B/405B for overflow research + +Any resulting TradeIntent terminates at the trade-gate MCP (human token above the DCA +whitelist). Current machine profile mapping: `swarm-config.json` in this directory. ## Output Artifacts diff --git a/workflows/wealth/swarm-config.json b/workflows/wealth/swarm-config.json index 1ab9a2e3..dd3a6eaa 100644 --- a/workflows/wealth/swarm-config.json +++ b/workflows/wealth/swarm-config.json @@ -1,45 +1,50 @@ { "is": "wealth", "displayName": "Wealth IS / Capital & Deal Intelligence", - "version": "0.1.0", - "hermesProfiles": [ - { - "id": "hermes-wealth-specialist", - "model": "nousresearch/hermes-3-llama-3.1-405b", - "purpose": "Investment thesis construction, deal analysis, complex return modeling, gate ladder evaluation", - "port": 8084, - "type": "local", - "skills": [ - "intelligence/strategic-reasoning", - "intelligence/pattern-recognition", - "business/revenue-modeling" - ], - "vaultAccess": ["strategic", "technical"], - "commands": ["/wealth-dpi", "/wealth-thesis-review", "/wealth-gate-progress", "/deal-analyze"] + "version": "0.2.0", + "note": "Superseded 2026-07-02: the notional hermes-3 405B/70B pair is replaced by the Investment Intelligence engine (verticals/investment-intelligence/). Routing tiers per ROUTING.md; canonical per-agent models in engine/agents/catalog.json.", + "engine": "verticals/investment-intelligence/engine/agents/catalog.json", + "routing": { + "t0_local": { + "models": ["hermes-4.3-36b", "hermes-4-14b"], + "runtime": "ollama | lm-studio | hermes-desktop | hermes-agent", + "dataClasses": ["private-financial-state", "aggregates", "public"], + "purpose": "Anything touching raw balances/positions/tax data; trade-gate host; Hermes Agent finance profile" }, - { - "id": "hermes-wealth-executor", - "model": "nousresearch/hermes-3-llama-3.1-70b", - "purpose": "Fast execution and task decomposition for DPI math, vault writes, gate scoring, deal triage", - "port": 8094, - "type": "local", - "skills": [ - "orchestration/parallel-execution" - ], - "vaultAccess": ["operational"], - "commands": [] + "t1_frontier": { + "models": { + "analysis": "claude-sonnet-4-6", + "technical": "claude-haiku-4-5", + "risk": "claude-sonnet-4-6", + "portfolio_manager": "claude-opus-4-7", + "chief_of_staff": "claude-sonnet-4-6", + "researcher": "claude-sonnet-4-6" + }, + "dataClasses": ["aggregates", "public"], + "purpose": "Strategy sessions, thesis debate, synthesis — aggregates only" + }, + "t2_openrouter_hermes": { + "models": ["nousresearch/hermes-4-70b", "nousresearch/hermes-4-405b"], + "dataClasses": ["public"], + "purpose": "Overflow research; Hermes Agent cloud tasks. Nous Portal skipped by machine policy." } - ], - "swarmTopology": "sequential", - "concurrency": 2, - "timeoutMs": 300000, - "onResult": "vault-write", + }, + "executionGate": { + "mcp": "verticals/investment-intelligence/mcp/trade-gate", + "invariant": "human approval token above the DCA whitelist; paper broker default; live adapters NOT_WIRED in-repo" + }, + "swarmTopology": "layered", + "layers": ["analysis:blind-parallel:5", "risk:veto-on-size-not-direction:3", "synthesis:2", "researcher:cross-cutting:1"], + "onResult": "vault-write + trajectory-write", "mcpTools": [ "vault-read", "vault-write", "context-load", "sip-attest", - "web-search" + "web-search", + "trade-gate.propose_trade", + "trade-gate.list_pending", + "trade-gate.read_audit" ], "triggers": [ "review my investment thesis",