Wealth OS: spawn Investment Intelligence vertical + trade-gate MCP + Wealth IS ACL collapse#32
Wealth OS: spawn Investment Intelligence vertical + trade-gate MCP + Wealth IS ACL collapse#32frankxai wants to merge 6 commits into
Conversation
…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
|
Important Review skippedDraft detected. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
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.
| /** 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 }); |
There was a problem hiding this comment.
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.
| /** 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 }); | |
| } |
| // 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/')) | ||
| } |
There was a problem hiding this comment.
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, |
There was a problem hiding this comment.
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.
| 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, |
| 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); | ||
| } | ||
| } |
There was a problem hiding this comment.
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);
}
}
}| 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 }); |
There was a problem hiding this comment.
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"; |
| ); | ||
| } | ||
|
|
||
| if (import.meta.url === `file://${process.argv[1]}`) { |
There was a problem hiding this comment.
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.
| 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). | ||
|
|
| // 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) |
There was a problem hiding this comment.
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)))…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
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Queen review verdict — NEEDS-BOARD-REVIEW (substrate) — code quality is highClassification: SUBSTRATE — touches What I verified (2026-07-06):
Two findings existing tests structurally CANNOT catch — board agenda items before any live wiring:
Checks: harness ✅, Vercel ✅. MERGEABLE/CLEAN. |
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).verticals/wealth/collapses to an ACL-only composition manifest (the 2026-05-29 board's designed default; deadline passed withcommands/empty), andMEMORY.mdv0.4 corrects the v0.3 "declared commands" inconsistency.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, nullabletrajectory.modification).mcp/trade-gate/, TypeScript, mirrorspayment-intelligence-system/mcpshape):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.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).friend-startergains 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(withajv+gray-matterinstalled) → all sessions valid.iis/scripts/privacy-check.mjs --files=<engine copy + wealth templates>→ zero blocking hits..claude/commands/; manifest paths inverticals/wealth/README.mdall 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