Skip to content

Wealth OS: spawn Investment Intelligence vertical + trade-gate MCP + Wealth IS ACL collapse#32

Draft
frankxai wants to merge 6 commits into
mainfrom
claude/wealth-os-agent-architecture-efj4hb
Draft

Wealth OS: spawn Investment Intelligence vertical + trade-gate MCP + Wealth IS ACL collapse#32
frankxai wants to merge 6 commits into
mainfrom
claude/wealth-os-agent-architecture-efj4hb

Conversation

@frankxai

@frankxai frankxai commented Jul 2, 2026

Copy link
Copy Markdown
Owner

What

The substrate half of the 2026-07-02 Wealth OS consolidation (board record: docs/boards/2026-07-02-investment-intelligence-vertical-spawn.md, full review: docs/strategic/2026-07-02-wealth-os-architecture-review.md).

  1. Governance: records the Wealth IS composition-layer falsifier outcome honestly — verticals/wealth/ collapses to an ACL-only composition manifest (the 2026-05-29 board's designed default; deadline passed with commands/ empty), and MEMORY.md v0.4 corrects the v0.3 "declared commands" inconsistency.
  2. Spawns verticals/investment-intelligence/ (R4 gate passed via Crypto IS): 7-file vertical contract + the IIS engine promoted (copied, not moved) from the private operator instance — 11-agent catalog, 5 schemas, 4 commands, adapters, sanitized archetypes. Schema validation now green under real ajv (the strict path never ran upstream — fixes: script-relative root, YAML-date coercion, quoted example dates, nullable trajectory.modification).
  3. Trade-gate MCP (mcp/trade-gate/, TypeScript, mirrors payment-intelligence-system/mcp shape): propose_trade / request_approval / list_pending / execute_approved / read_audit. Fail-closed: over ANY cap → pending (never auto-approved, DCA included); single-use intent-bound human approval tokens; durable append-only JSONL state; paper broker only — alpaca/ibkr/coinbase are NOT_WIRED stubs. 21/21 tests green, including the red cases: no-token execution rejected, live broker fails NOT_WIRED even WITH approval.
  4. Sovereignty + Hermes: ROUTING.md (T0 local Hermes 4.3/14B for balance-touching data · T1 Sonnet/Opus/Haiku · T2 OpenRouter Hermes), HERMES.md + templates/hermes-finance-profile/ (run-don't-fork), absorption records (TradingAgents debate/reflection patterns, ai-hedge-fund persona lenses), OBSERVABILITY.md (Langfuse + audit reconciliation + retro loop), RUNBOOK.md (operator wiring).
  5. Brother kit: friend-starter gains Wealth Guardian protected-executor templates (read-only, paper-first, DCA-only discussion, escalation rules, R5 clause) — restricted to Claude Project + Cowork export.

Verification

  • verticals/investment-intelligence/mcp/trade-gate: npm install && npm test → typecheck + 21/21.
  • engine/scripts/validate-schemas.mjs (with ajv + gray-matter installed) → all sessions valid.
  • iis/scripts/privacy-check.mjs --files=<engine copy + wealth templates> → zero blocking hits.
  • No duplicate command names in .claude/commands/; manifest paths in verticals/wealth/README.md all resolve.

Invariants held

R5 non-advisory clause inline on every investment output · human token above the DCA whitelist · no broker credentials or live-broker code in-repo · paper-first ladder · instance €-state never leaves the operator's private/.

Sibling PRs: FrankX (operator-instance docs), agentic-business-os (investor-os-pack), agentic-ops-hub (ecosystem/MCP policy), starlight-evals (investment-gate lane).

🤖 Generated with Claude Code

https://claude.ai/code/session_01XJa7gWBFSzsF7N4zNEdQ8f


Generated by Claude Code

claude added 4 commits July 2, 2026 17:37
…telligence vertical (board PROCEED)

- Board record: docs/boards/2026-07-02-investment-intelligence-vertical-spawn.md
- Strategic review: docs/strategic/2026-07-02-wealth-os-architecture-review.md
- verticals/wealth collapses to ACL-only composition manifest per the 2026-05-29
  falsifier's designed default (deadline 2026-06-16 passed with commands/ empty);
  MEMORY.md v0.4 corrects the v0.3 'declared commands' inconsistency.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XJa7gWBFSzsF7N4zNEdQ8f
…e + sovereignty routing + Hermes profile

- 7-file vertical contract (README/SOUL/CANON/AGENTS/STACK/SKILL/MEMORY) per _template,
  R5 non-advisory clause inline, daily-5 declared (R2 discipline)
- engine/ promoted (copied, not moved) from the private IIS operator instance:
  11-agent catalog, 5 schemas, 4 commands, 3 adapters, sanitized archetypes
  (privacy-check clean; validate-schemas now green under real ajv — fixed the
  script-relative root, YAML-date coercion, quoted example dates, and
  trajectory.modification nullability)
- .claude/commands/invest-{strategy,thesis-debate,snapshot,retro}.md wrappers
- ROUTING.md (T0 local Hermes / T1 Sonnet+Opus / T2 OpenRouter Hermes with the
  balances-never-leave-T0 data rule), HERMES.md + hermes-finance-profile templates,
  absorption records (TradingAgents, ai-hedge-fund), OBSERVABILITY.md, RUNBOOK.md
- VERTICALS.md + REGISTRY.md registration; workflows/wealth superseded to the real
  engine topology (notional hermes-3 pair retired)

Board: docs/boards/2026-07-02-investment-intelligence-vertical-spawn.md

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XJa7gWBFSzsF7N4zNEdQ8f
…n token above DCA, append-only audit

TypeScript MCP mirroring payment-intelligence-system/mcp shape (pattern reused,
repo untouched — trading is a distinct risk domain):
- 5 tools: propose_trade / request_approval / list_pending / execute_approved / read_audit
- caps per order/day/asset-class; over ANY cap -> pending, never auto-approved (DCA included)
- DCA whitelist is the only auto-approve path (still capped + audited)
- single-use intent-bound approval tokens; lost token = deny + re-propose
- durable JSONL state (audit/spend/approvals/gate) survives restart; audit-first
- brokers: paper functional; alpaca/ibkr/coinbase are NOT_WIRED stubs by design
- R5 non-advisory footer on every output
- 21 tests green incl. red cases: no-token execution rejected; live broker fails
  NOT_WIRED even WITH human approval

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XJa7gWBFSzsF7N4zNEdQ8f
- knowledge/wealth-guardian-template.md: read-only portfolio review, paper-first
  education, DCA-only action talk, escalate-to-operator above the declared band
- knowledge/wealth-protection-rules.md: hard boundaries — no credentials in chat,
  no live-trade instructions, scam/pressure immunity, honest-numbers discipline
- README + export-pathways: optional Wealth Guardian configuration; restricted to
  Claude Project + Cowork (targets that can't hold the boundaries are refused)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XJa7gWBFSzsF7N4zNEdQ8f
@coderabbitai

coderabbitai Bot commented Jul 2, 2026

Copy link
Copy Markdown

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 9f87d5a4-8bcf-4f32-8af7-7e1f03829563

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/wealth-os-agent-architecture-efj4hb

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request spawns the new verticals/investment-intelligence/ vertical, promoting the IIS (Investment Intelligence System) engine. This includes an 11-agent, 3-layer decision swarm, session commands, JSON schemas, data adapters, and a fail-closed trade-gate-mcp server. Additionally, verticals/wealth/ is updated to record its collapse to an ACL-only composition manifest due to a fired falsifier. The review feedback focuses on enhancing cross-platform compatibility (specifically for Windows) by utilizing fileURLToPath for path resolution and script execution checks, correcting hardcoded paths in the privacy check script, refining regex patterns for seed phrase detection, preventing memory bloat in the cap ledger, and ensuring robust error handling by moving checks inside try-catch blocks and validating terminal states before denying approvals.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment on lines +168 to +174
/** 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}'`);
}
this.record({ type: "denied", approvalId, reason, ts: now });

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The deny method does not check if the approval is already in a terminal state (executed or denied). If an approval has already been executed, allowing it to be denied after the fact creates an inconsistent state. We should throw an error if the approval is already executed or denied.

Suggested change
/** 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}'`);
}
this.record({ type: "denied", approvalId, reason, ts: now });
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);
}
this.record({ type: "denied", approvalId, reason, ts: now });
}

Comment on lines +76 to +100
// Files to skip entirely (always allowed)
const SKIP_FILES = [
'iis/PRIVACY-BOUNDARY.md', // documents the patterns; will match itself
'iis/scripts/privacy-check.mjs', // this file
'iis/LICENSE', // MIT license legitimately names authors
'iis/architecture/09-tax-overlays.md', // legitimate reference EUR amounts (Box 3 thresholds)
'iis/architecture/10-honest-limits.md', // documents amounts in context
'iis/architecture/11-ai-engineering.md', // documents cost/pricing math
]

function getFilesToCheck() {
if (mode === 'files' && filesArg) {
return filesArg.split(',').map((p) => p.trim()).filter(Boolean)
}
if (mode === 'all') {
const out = execSync('git ls-files iis/', { 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('iis/'))
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The privacy check script has hardcoded paths starting with iis/. However, in this repository, the files are located under verticals/investment-intelligence/. This causes the script to return an empty list of files and skip all checks, rendering the privacy boundary check completely non-functional. We should update the paths to match the actual directory structure.

// Files to skip entirely (always allowed)
const SKIP_FILES = [
  'verticals/investment-intelligence/PRIVACY-BOUNDARY.md',           // documents the patterns; will match itself
  'verticals/investment-intelligence/engine/scripts/privacy-check.mjs',     // this file
  'verticals/investment-intelligence/LICENSE',                       // MIT license legitimately names authors
  'verticals/investment-intelligence/engine/architecture/09-tax-overlays.md', // legitimate reference EUR amounts (Box 3 thresholds)
  'verticals/investment-intelligence/engine/architecture/10-honest-limits.md', // documents amounts in context
  'verticals/investment-intelligence/engine/architecture/11-ai-engineering.md', // documents cost/pricing math
]

function getFilesToCheck() {
  if (mode === 'files' && filesArg) {
    return filesArg.split(',').map((p) => p.trim()).filter(Boolean)
  }
  if (mode === 'all') {
    const out = execSync('git ls-files verticals/investment-intelligence/', { 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('verticals/investment-intelligence/'))
}

// 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: /\b(?:^|\n)([a-z]{3,8}( [a-z]{3,8}){11}|[a-z]{3,8}( [a-z]{3,8}){23})\b/m,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The seed phrase regex uses \b(?:^|\n). Because \b is a zero-width assertion between a word character and a non-word character, it will fail to match at a newline if the preceding line ends with a non-word character (such as a period or space). This means seed phrases on subsequent lines might not be detected. Using ^ and $ with the /m flag is much cleaner and more robust.

Suggested change
regex: /\b(?:^|\n)([a-z]{3,8}( [a-z]{3,8}){11}|[a-z]{3,8}( [a-z]{3,8}){23})\b/m,
regex: /^[a-z]{3,8}(?: [a-z]{3,8}){11}$|^[a-z]{3,8}(?: [a-z]{3,8}){23}$/m,

Comment on lines +52 to +69
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: 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; the 24h window seeds from recent events.
this.consumed.add(ev.intentId);
this.records.push(ev);
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The load() method loads all historical events from spend.jsonl into this.records in memory. Over time, as the file grows, this will cause unnecessary memory usage and performance degradation on startup. Since this.records is only used for rolling 24h cap checks, we should only keep events from the last 24 hours in this.records (while still keeping all intentIds in this.consumed for lifetime replay protection).

  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; the 24h window seeds from recent events.
      this.consumed.add(ev.intentId);
      if (ev.ts > cutoff) {
        this.records.push(ev);
      }
    }
  }

Comment on lines +194 to +201
async ({ intent, caps: capPolicy, dcaWhitelist }) => {
const ti = intent as TradeIntent;
const capResult = caps.check(ti, capPolicy);

try {
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 });

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The caps.check call is currently placed outside the try-catch block. If it throws an unexpected error, it will propagate and crash the request or server instead of failing closed gracefully. Moving it inside the try block ensures robust error handling.

    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 });
        }

import { dirname, join } from "node:path";
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Import fileURLToPath from node:url to enable a robust, cross-platform check for direct script execution.

Suggested change
import { z } from "zod";
import { z } from "zod";
import { fileURLToPath } from "node:url";

);
}

if (import.meta.url === `file://${process.argv[1]}`) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Comparing import.meta.url directly to file://${process.argv[1]} is fragile on Windows due to differences in path separators (backslashes vs. forward slashes) and drive letter formatting. Using fileURLToPath is the standard, robust way to perform this check.

Suggested change
if (import.meta.url === `file://${process.argv[1]}`) {
if (process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1]) {

//
// Dependencies: ajv (peer-installed by operator). Falls back to manual checks
// if ajv unavailable (substrate-friendly: no required deps).

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Import fileURLToPath from node:url to enable robust, cross-platform path resolution.

import { readFileSync, readdirSync, statSync, existsSync } from 'node:fs'
import { fileURLToPath } from 'node:url'

// Promoted copy: the engine root is this script's parent directory
// (verticals/investment-intelligence/engine/), not <cwd>/iis as in the
// operator instance.
const SUBSTRATE_ROOT = resolve(new URL('..', import.meta.url).pathname)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Using new URL('..', import.meta.url).pathname can cause issues on Windows because the pathname may start with a leading slash (e.g., /C:/...) which resolve might not parse correctly. Using fileURLToPath is the robust, cross-platform standard.

const SUBSTRATE_ROOT = resolve(fileURLToPath(new URL('..', import.meta.url)))

claude added 2 commits July 2, 2026 17:46
…ted-script paths, gate hardening

- test/starlight-substrate-mcp-smoke.test.ts: expected registry list gains
  trade-gate-mcp (intentional registration per the 2026-07-02 board record)
- engine/scripts/privacy-check.mjs: promoted-copy paths (iis/ -> the vertical) so
  --all/staged modes actually scan; line-anchored seed-phrase regex; footer path
- engine/scripts/validate-schemas.mjs: fileURLToPath for cross-platform root
- trade-gate: deny() refuses terminal states (executed/denied) + test; caps load
  seeds only the 24h window into memory (lifetime replay set unchanged);
  caps.check moved inside try (fail-closed on unexpected throw); fileURLToPath
  entrypoint check

22/22 tests green; substrate smoke green; privacy-check --all scans 60 files clean
(one known warn: base64 integrity hash in package-lock matches BTC-address shape)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XJa7gWBFSzsF7N4zNEdQ8f
…model routing, live-executable

- src/swarm.ts: SwarmTask.model threads to the runner (claude -p --model <id>);
  no model → runner default. Fully backward compatible.
- src/swarm-council.ts: loadIISCatalog + buildCouncilPlan + runCouncil compile
  verticals/investment-intelligence/engine/agents/catalog.json into 3 ordered
  phases — analysis blind-parallel (Sonnet ×4 + Haiku technical) → risk (Sonnet ×3,
  sees analysis) → synthesis (Opus portfolio-manager + Sonnet chief-of-staff, sees
  full debate). researcher excluded v1; R5 clause + no-execution line in every prompt.
- src/cli.ts: 'starlight swarm run' subcommand — dry-run plan default, --live
  executes, --only/--concurrency/--timeout, JSONL audit. Existing dry-run
  'starlight-swarm' planner untouched.
- src/swarm-council.test.ts (wired into test:operational): 12 assertions —
  catalog parse, per-agent model map, blind-parallel ordering, risk-sees-analysis,
  synthesis-sees-debate, --only filter, fail-safe.
- .claude/commands/siswarm.md: the /siswarm command doc + boundary.

Verified: tsc clean; swarm 6/6 + council 6/6 green; live demo ran the Haiku
technical agent through headless claude (17s, ok) with a durable audit record.
Boundary: analysis + pending decision brief only; execution stays behind the
trade-gate MCP + human token.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XJa7gWBFSzsF7N4zNEdQ8f
@vercel

vercel Bot commented Jul 5, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
site Ready Ready Preview, Comment Jul 5, 2026 3:03am

@frankxai

frankxai commented Jul 6, 2026

Copy link
Copy Markdown
Owner Author

Queen review verdict — NEEDS-BOARD-REVIEW (substrate) — code quality is high

Classification: SUBSTRATE — touches REGISTRY.md (trade-gate MCP registered) and VERTICALS.md (Investment Intelligence spawned, Crypto IS status advanced, Wealth IS collapsed to ACL-only). Per doctrine this needs a /starlight-board pass before merge. The in-PR board record (docs/boards/2026-07-02-investment-intelligence-vertical-spawn.md) was authored by the same change-set — it documents the decision, it doesn't ratify it.

What I verified (2026-07-06):

  • Wealth ACL collapse is honest governance: the 2026-05-29 falsifier fired as designed (commands/ empty at deadline), the v0.3 memory entry even gets a correction note. Model substrate hygiene.
  • Trade-gate MCP: paper broker only; alpaca/ibkr/coinbase throw NOT_WIRED; no credentials anywhere; write-ahead JSONL for approvals/audit/registry; single-use tokens bound to intent; replay + double-execution guards; audit-first before broker call. Tests cover the red cases.
  • starlight swarm run: dry-run by default, --live opt-in, analysis-only boundary stated in every prompt, added to test:operational.

Two findings existing tests structurally CANNOT catch — board agenda items before any live wiring:

  1. The human gate is conventional, not structural. request_approval is a tool on the same MCP surface the agent uses. Any agent that can propose_trade can also call request_approval(approver: "frank") and receive the token in the tool result, then execute_approved. Fine for paper v0.1; before live adapters, approval must move out-of-band (separate process/UI, not an MCP tool the proposing agent can reach).
  2. Caps and DCA whitelist are caller-supplied inputs to propose_trade, not server-held policy. The proposer defines its own limits — "over-cap → escalate" is relative to whatever the agent passed. Policy belongs in server config (env/file), not the tool schema.

Checks: harness ✅, Vercel ✅. MERGEABLE/CLEAN.
Path: run /starlight-board on the spawn + the two findings above → ratify → gh pr ready 32 → squash-merge.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants