@smartmemory/compose 0.3.6-beta → 0.3.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/skills/compose/SKILL.md +42 -88
- package/.compose-deps.json +1 -13
- package/README.md +72 -5
- package/bin/compose.js +754 -347
- package/bin/git-hooks/pre-push.template +29 -0
- package/bin/judgment-import.js +7 -0
- package/bin/judgment-migrate.js +387 -0
- package/contracts/comp-obs-contract.schema.json +9 -3
- package/contracts/feature-json.schema.json +5 -0
- package/contracts/fluid-record.schema.json +209 -0
- package/contracts/judgment-record.schema.json +425 -4
- package/contracts/lifecycle-backfill.schema.json +322 -0
- package/dist/assets/App-Z4MU-H_F.js +916 -0
- package/dist/assets/_baseUniq-ClWoCPFl.js +1 -0
- package/dist/assets/arc-DY26UIVo.js +1 -0
- package/dist/assets/architectureDiagram-Q4EWVU46-6Ggq4DqJ.js +36 -0
- package/dist/assets/blockDiagram-DXYQGD6D-CH3Ked0l.js +132 -0
- package/dist/assets/{browser-BSM23If2.js → browser-BWkrenen.js} +6 -6
- package/dist/assets/{c4Diagram-LMCZKHZV-DZf45Fbz.js → c4Diagram-AHTNJAMY-Bk8dYilu.js} +1 -1
- package/dist/assets/channel-SnZzzh7k.js +1 -0
- package/dist/assets/{chunk-JWPE2WC7-_7ujgd_Q.js → chunk-4BX2VUAB-BMR0XaAQ.js} +1 -1
- package/dist/assets/chunk-4TB4RGXK-JytR14a9.js +206 -0
- package/dist/assets/{chunk-XXDRQBXY-DfdVhbmA.js → chunk-55IACEB6-B4Q97BCP.js} +1 -1
- package/dist/assets/{chunk-VR4S4FIN-Dt9NZ67m.js → chunk-EDXVE4YY-R_qarkSf.js} +1 -1
- package/dist/assets/{chunk-5VM5RSS4-BY4_PV5H.js → chunk-FMBD7UC4-C9s7KR9m.js} +1 -1
- package/dist/assets/chunk-OYMX7WX6-BySQzVxc.js +231 -0
- package/dist/assets/{chunk-2Q5K7J3B-Dn1spZYu.js → chunk-QZHKN3VN-DdpSYZsW.js} +1 -1
- package/dist/assets/{chunk-32BRIVSS-pURGrJDk.js → chunk-YZCP3GAM-iE_tzriw.js} +1 -1
- package/dist/assets/classDiagram-6PBFFD2Q-CBu92dSH.js +1 -0
- package/dist/assets/classDiagram-v2-HSJHXN6E-CBu92dSH.js +1 -0
- package/dist/assets/clone-DgklGjHm.js +1 -0
- package/dist/assets/{cose-bilkent-JH36ORCC-BieYif4o.js → cose-bilkent-S5V4N54A-BdlU6ZX_.js} +1 -1
- package/dist/assets/dagre-KV5264BT-Cp3F5KTn.js +4 -0
- package/dist/assets/diagram-5BDNPKRD-DiR6_2q_.js +10 -0
- package/dist/assets/diagram-G4DWMVQ6-w0i-p5HX.js +24 -0
- package/dist/assets/diagram-MMDJMWI5-tIHhwUv3.js +43 -0
- package/dist/assets/diagram-TYMM5635-BAeY3B19.js +24 -0
- package/dist/assets/erDiagram-SMLLAGMA-Ckx_Knko.js +85 -0
- package/dist/assets/flowDiagram-DWJPFMVM-DeoNka6J.js +162 -0
- package/dist/assets/ganttDiagram-T4ZO3ILL-BmGnFbEg.js +292 -0
- package/dist/assets/gitGraphDiagram-UUTBAWPF-Dk48IHsx.js +106 -0
- package/dist/assets/graph-BNzKGvoy.js +1 -0
- package/dist/assets/graph-CI_1htl0.js +331 -0
- package/dist/assets/index-BEfrNBp8.js +123 -0
- package/dist/assets/index-yyrA5OZd.css +1 -0
- package/dist/assets/infoDiagram-42DDH7IO-BRf827i0.js +2 -0
- package/dist/assets/{ishikawaDiagram-FXEZZL3T-CzEB9fQS.js → ishikawaDiagram-UXIWVN3A-0kCZaeCM.js} +5 -5
- package/dist/assets/{journeyDiagram-5HDEW3XC-Bz8TCdz2.js → journeyDiagram-VCZTEJTY-rvU7ayRt.js} +1 -1
- package/dist/assets/{kanban-definition-HUTT4EX6-tozrMoV_.js → kanban-definition-6JOO6SKY-DpQwX1C5.js} +7 -7
- package/dist/assets/katex-DkKDou_j.js +257 -0
- package/dist/assets/layout-BI8cXFPI.js +1 -0
- package/dist/assets/{linear-Ck7gpa5N.js → linear-a0glcDiw.js} +1 -1
- package/dist/assets/min-vPHfnXcC.js +1 -0
- package/dist/assets/{mindmap-definition-LN4V7U3C-DTcHO0DJ.js → mindmap-definition-QFDTVHPH-D14eF-7C.js} +7 -7
- package/dist/assets/mobile-B7m9EO9D.js +17 -0
- package/dist/assets/pieDiagram-DEJITSTG-Cno-gETh.js +30 -0
- package/dist/assets/quadrantDiagram-34T5L4WZ-BUQM1Hfm.js +7 -0
- package/dist/assets/{requirementDiagram-TGXJPOKE-bnI2zJeT.js → requirementDiagram-MS252O5E-pOXlN2-q.js} +3 -3
- package/dist/assets/sankeyDiagram-XADWPNL6-Crynd3_b.js +10 -0
- package/dist/assets/sequenceDiagram-FGHM5R23-D9fZdCM8.js +157 -0
- package/dist/assets/stateDiagram-FHFEXIEX-CW9qVec8.js +1 -0
- package/dist/assets/stateDiagram-v2-QKLJ7IA2-DkVLzHbY.js +1 -0
- package/dist/assets/{timeline-definition-FHXFAJF6-D267GQFF.js → timeline-definition-GMOUNBTQ-BcHzhm_8.js} +3 -3
- package/dist/assets/vennDiagram-DHZGUBPP-BfytJcWk.js +34 -0
- package/dist/assets/wardley-RL74JXVD-DLj-IjyB.js +162 -0
- package/dist/assets/wardleyDiagram-NUSXRM2D-Ds0Ue68c.js +20 -0
- package/dist/assets/xychartDiagram-5P7HB3ND-vjWDXFL6.js +7 -0
- package/dist/index.html +3 -3
- package/lib/agent-string.js +7 -5
- package/lib/append-integrity.js +81 -0
- package/lib/backfill-evidence.js +109 -0
- package/lib/bug-escalation.js +39 -4
- package/lib/build-stream-schema.js +3 -1
- package/lib/build-stream-writer.js +25 -0
- package/lib/build.js +1624 -195
- package/lib/canon-guard.js +245 -0
- package/lib/canon-override.js +196 -0
- package/lib/canon-registry.js +291 -0
- package/lib/cli-commands.js +144 -0
- package/lib/codex-preflight.js +50 -15
- package/lib/colleague/context.js +215 -0
- package/lib/colleague/writeback.js +95 -0
- package/lib/completion-gate.js +1421 -0
- package/lib/completion-writer.js +47 -47
- package/lib/consumer-fanout.js +105 -11
- package/lib/coverage-gate.js +200 -0
- package/lib/dir-lock.js +170 -0
- package/lib/dispatch-ledger.js +301 -0
- package/lib/dispatch-metrics.js +236 -0
- package/lib/experiment-judge.js +6 -1
- package/lib/feature-json.js +1 -1
- package/lib/feature-reconciler.js +8 -0
- package/lib/feature-validator.js +64 -1
- package/lib/feature-writer.js +66 -2
- package/lib/fluid/factory.js +167 -0
- package/lib/fluid/ideabox-dates.js +73 -0
- package/lib/fluid/ideabox-migrate.js +154 -0
- package/lib/fluid/ideabox-ops.js +585 -0
- package/lib/fluid/ideabox-view.js +146 -0
- package/lib/fluid/import-ideabox.js +186 -0
- package/lib/fluid/local-provider.js +606 -0
- package/lib/fluid/provider.js +684 -0
- package/lib/fluid/record-shape.js +214 -0
- package/lib/fluid/record-store.js +328 -0
- package/lib/fluid/render-ideabox.js +261 -0
- package/lib/fluid/schema.js +40 -0
- package/lib/fluid/smartmemory-provider.js +1695 -0
- package/lib/gsd.js +63 -14
- package/lib/guard-cli.js +175 -0
- package/lib/guard-custody.js +141 -0
- package/lib/guard-descriptors.js +530 -0
- package/lib/guard-enrol.js +254 -0
- package/lib/health-score.js +1 -1
- package/lib/hooks-status.js +32 -3
- package/lib/ideabox-cli.js +315 -0
- package/lib/ideabox.js +121 -21
- package/lib/judgment/store/index.js +166 -0
- package/lib/judgment/store/records.js +184 -25
- package/lib/judgment/trace.js +380 -0
- package/lib/judgment-attest.js +259 -0
- package/lib/judgment-decision-write.js +277 -0
- package/lib/judgment-decisions.js +466 -0
- package/lib/judgment-gen.js +375 -22
- package/lib/judgment-verify.js +153 -0
- package/lib/judgment-writer.js +2842 -262
- package/lib/lane-gate.js +2 -0
- package/lib/lifecycle-modes.js +4 -4
- package/lib/lineage.js +400 -0
- package/lib/local-claude-connector.js +250 -54
- package/lib/maya-client.js +302 -0
- package/lib/maya-config.js +53 -0
- package/lib/maya-identity.js +283 -0
- package/lib/mcp-enforcement.js +21 -35
- package/lib/migrate-anon.js +5 -0
- package/lib/migrate-roadmap.js +15 -0
- package/lib/new.js +13 -1
- package/lib/pipeline-compat.js +104 -0
- package/lib/policy-catalog.js +295 -0
- package/lib/policy-check.js +0 -0
- package/lib/process-termination.js +98 -0
- package/lib/resolve-workspace.js +5 -1
- package/lib/result-normalizer.js +428 -153
- package/lib/review-normalize.js +4 -0
- package/lib/roadmap-errors.js +65 -0
- package/lib/roadmap-preservers.js +24 -4
- package/lib/roadmap-residue.js +299 -0
- package/lib/smartmemory-client.js +614 -78
- package/lib/smartmemory-config.js +54 -0
- package/lib/smartmemory-ingest.js +19 -2
- package/lib/step-prompt.js +7 -6
- package/lib/stratum-engine.js +53 -4
- package/lib/stratum-mcp-client.js +391 -31
- package/lib/test-bootstrap.js +31 -0
- package/lib/tool-inventory.js +122 -0
- package/lib/version-check.js +91 -19
- package/lib/vision-writer.js +88 -1
- package/package.json +7 -6
- package/pipelines/bug-fix.stratum.yaml +205 -211
- package/pipelines/build-quick.profiles.json +12 -0
- package/pipelines/build-quick.stratum.yaml +263 -350
- package/pipelines/content.stratum.yaml +81 -77
- package/pipelines/coverage-sweep.stratum.yaml +49 -30
- package/pipelines/plan.stratum.yaml +76 -86
- package/pipelines/refactor.stratum.yaml +125 -125
- package/pipelines/research.stratum.yaml +56 -58
- package/pipelines/review-fix.profiles.json +6 -0
- package/pipelines/review-fix.stratum.yaml +110 -83
- package/presets/team-feature.profiles.json +6 -0
- package/presets/team-feature.stratum.yaml +93 -66
- package/presets/team-research.profiles.json +6 -0
- package/presets/team-research.stratum.yaml +89 -80
- package/presets/team-review.profiles.json +8 -0
- package/presets/team-review.stratum.yaml +98 -80
- package/scripts/cost-census.mjs +70 -0
- package/scripts/guard-sign/compose-guard-sign.sh +62 -0
- package/server/agent-health.js +22 -0
- package/server/agent-hooks.js +14 -1
- package/server/agent-server.js +5 -248
- package/server/agent-spawn.js +3 -4
- package/server/agent-workspace.js +294 -0
- package/server/build-routes.js +6 -5
- package/server/build-stream-bridge.js +53 -0
- package/server/cc-session-watcher.js +4 -1
- package/server/coalescing-buffer.js +7 -1
- package/server/completion-projection.js +228 -0
- package/server/compose-mcp-tools.js +124 -24
- package/server/compose-mcp.js +91 -790
- package/server/decision-event-emit.js +41 -2
- package/server/decision-event-id.js +17 -0
- package/server/decision-events-snapshot.js +3 -0
- package/server/design-routes.js +14 -8
- package/server/feature-scan.js +76 -2
- package/server/file-watcher.js +170 -21
- package/server/ideabox-routes.js +166 -224
- package/server/index.js +70 -100
- package/server/lifecycle-guard.js +240 -10
- package/server/lifecycle-phase-history.js +276 -0
- package/server/maya-routes.js +507 -0
- package/server/mcp-tool-defs.js +940 -0
- package/server/mcp-tool-policy.js +35 -3
- package/server/model-tiers.js +22 -5
- package/server/pipeline-routes.js +21 -11
- package/server/project-root.js +58 -19
- package/server/remote-utils.js +3 -1
- package/server/schema-validator.js +7 -1
- package/server/session-manager.js +5 -6
- package/server/session-routes.js +3 -1
- package/server/stratum-client.js +57 -10
- package/server/stratum-sync.js +6 -3
- package/server/summarizer.js +3 -4
- package/server/supervisor.js +0 -1
- package/server/vision-routes.js +208 -98
- package/server/vision-server.js +86 -23
- package/server/vision-store.js +60 -6
- package/server/vision-utils.js +3 -4
- package/server/workspace-activity.js +18 -0
- package/server/workspace-middleware.js +2 -2
- package/server/workspace-runtime.js +243 -0
- package/server/worktree-gc.js +1 -0
- package/dist/assets/App-BG3ngu8H.js +0 -896
- package/dist/assets/abnfDiagram-VRR7QNED-CjB_sD3D.js +0 -1
- package/dist/assets/arc-_v4hR_uD.js +0 -1
- package/dist/assets/architectureDiagram-ZJ3FMSHR-DreJmzXQ.js +0 -36
- package/dist/assets/blockDiagram-677ZJIJ3-BG9-c0O1.js +0 -132
- package/dist/assets/channel-B3U5wFAT.js +0 -1
- package/dist/assets/chunk-EX3LRPZG-DdELs1qP.js +0 -231
- package/dist/assets/chunk-MOJQB5TN-D-ky35G-.js +0 -88
- package/dist/assets/chunk-RYQCIY6F-Dag_kVlO.js +0 -1
- package/dist/assets/chunk-V7JOEXUC-BtewURat.js +0 -206
- package/dist/assets/classDiagram-OUVF2IWQ-B6fCN-ht.js +0 -1
- package/dist/assets/classDiagram-v2-EOCWNBFH-B6fCN-ht.js +0 -1
- package/dist/assets/cynefin-VYW2F7L2-CT2BA6KE.js +0 -178
- package/dist/assets/cynefinDiagram-TSTJHNR4-Bh6exbyg.js +0 -62
- package/dist/assets/dagre-VKFMJZFB-aXMLSmQL.js +0 -4
- package/dist/assets/diagram-FQU43EPY-Dr7JAOuQ.js +0 -3
- package/dist/assets/diagram-G47NLZAW-DUvA3FQK.js +0 -24
- package/dist/assets/diagram-NH7WQ7WH-BQUARqcu.js +0 -24
- package/dist/assets/diagram-OA4YK3LP-dDUc1zHi.js +0 -30
- package/dist/assets/diagram-WEI45ONY-B2h5Qlb1.js +0 -41
- package/dist/assets/ebnfDiagram-CCIWWBDH-DThRGupB.js +0 -1
- package/dist/assets/erDiagram-Q63AITRT-BUCsprO2.js +0 -85
- package/dist/assets/flowDiagram-23GEKE2U-DXtNNi6r.js +0 -156
- package/dist/assets/ganttDiagram-NO4QXBWP-D4zbBHh_.js +0 -292
- package/dist/assets/gitGraphDiagram-IHSO6WYX-DpoQws0W.js +0 -106
- package/dist/assets/graph-BXPQrYYB.js +0 -331
- package/dist/assets/graph-C9eacEi8.js +0 -1
- package/dist/assets/index-3ZH5eMcZ.js +0 -119
- package/dist/assets/index-LIwREYgH.css +0 -1
- package/dist/assets/infoDiagram-FWYZ7A6U-Bbas2GAo.js +0 -2
- package/dist/assets/katex-C5jXJg4s.js +0 -257
- package/dist/assets/layout-DEXfKzaS.js +0 -1
- package/dist/assets/map-Czzmt4hB.js +0 -1
- package/dist/assets/mobile-CaoXUwAr.js +0 -17
- package/dist/assets/pegDiagram-2B236MQR-CHiINrNy.js +0 -1
- package/dist/assets/pieDiagram-ENE6RG2P-CfS4YFlR.js +0 -39
- package/dist/assets/quadrantDiagram-ABIIQ3AL-CadesS9w.js +0 -7
- package/dist/assets/railroadDiagram-RFXS5EU6-CgWEspBN.js +0 -1
- package/dist/assets/sankeyDiagram-HTMAVEWB-YWKFgOGw.js +0 -40
- package/dist/assets/sequenceDiagram-DBY2YBRQ-BvkNOyF9.js +0 -162
- package/dist/assets/sizeCapture-X5ZJPWSS-DlFPA2yO.js +0 -1
- package/dist/assets/stateDiagram-2N3HPSRC-h8NIx0kQ.js +0 -1
- package/dist/assets/stateDiagram-v2-6OUMAXLB-DjPgZtJ9.js +0 -1
- package/dist/assets/swimlanes-5IMT3BWC-CT5n22kG.js +0 -2
- package/dist/assets/swimlanesDiagram-G3AALYLV-Dn318Bhq.js +0 -8
- package/dist/assets/vennDiagram-L72KCM5P-Dj-wWLYG.js +0 -34
- package/dist/assets/wardleyDiagram-EHGQE667-BxCeYxkG.js +0 -78
- package/dist/assets/xychartDiagram-FW5EYKEG-DMFqWn7z.js +0 -7
- package/lib/staleness.js +0 -87
- package/server/ideabox-cache.js +0 -77
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
4
|
+
import { homedir } from 'node:os';
|
|
5
|
+
import { resolve, join } from 'node:path';
|
|
6
|
+
|
|
7
|
+
const [runId, featureCode, projectArg] = process.argv.slice(2);
|
|
8
|
+
if (!runId || !featureCode) {
|
|
9
|
+
console.error('Usage: node scripts/cost-census.mjs <runId> <featureCode> [projectCwd]');
|
|
10
|
+
process.exitCode = 2;
|
|
11
|
+
} else {
|
|
12
|
+
const projectCwd = resolve(projectArg ?? process.cwd());
|
|
13
|
+
const flowPath = join(homedir(), '.stratum', 'ts', 'flows', `${runId}.json`);
|
|
14
|
+
const accumulatorPath = join(
|
|
15
|
+
projectCwd,
|
|
16
|
+
'.compose',
|
|
17
|
+
'data',
|
|
18
|
+
'build-accumulator',
|
|
19
|
+
`${featureCode}.json`,
|
|
20
|
+
);
|
|
21
|
+
const flow = JSON.parse(readFileSync(flowPath, 'utf8'));
|
|
22
|
+
// The live accumulator is cleared when a build completes or aborts
|
|
23
|
+
// (lib/build.js clearBuildAccumulator); after that the terminal totals live in
|
|
24
|
+
// build-history.jsonl. Prefer the live file, fall back to the last history row.
|
|
25
|
+
let accumulator;
|
|
26
|
+
let accumulatorSource;
|
|
27
|
+
if (existsSync(accumulatorPath)) {
|
|
28
|
+
accumulator = JSON.parse(readFileSync(accumulatorPath, 'utf8'));
|
|
29
|
+
accumulatorSource = 'build-accumulator (live)';
|
|
30
|
+
} else {
|
|
31
|
+
const historyPath = join(projectCwd, '.compose', 'data', 'build-history.jsonl');
|
|
32
|
+
const rows = existsSync(historyPath)
|
|
33
|
+
? readFileSync(historyPath, 'utf8').split('\n').filter(Boolean).map((line) => JSON.parse(line))
|
|
34
|
+
: [];
|
|
35
|
+
const last = rows.filter((row) => row?.feature_code === featureCode).at(-1);
|
|
36
|
+
if (!last) {
|
|
37
|
+
console.error(`no build accumulator or build-history row for ${featureCode} under ${projectCwd}`);
|
|
38
|
+
process.exit(2);
|
|
39
|
+
}
|
|
40
|
+
accumulator = last;
|
|
41
|
+
accumulatorSource = 'build-history.jsonl (terminal)';
|
|
42
|
+
}
|
|
43
|
+
console.log(`accumulator.source=${accumulatorSource}`);
|
|
44
|
+
const receipts = Array.isArray(flow.receipts) ? flow.receipts : [];
|
|
45
|
+
const costReceipts = receipts.filter((receipt) => receipt?.source !== 'engine');
|
|
46
|
+
const clientReceipts = costReceipts.filter(
|
|
47
|
+
(receipt) => !receipt?.dispatchId?.startsWith('legacy:') && receipt?.source !== 'server_dispatch',
|
|
48
|
+
);
|
|
49
|
+
const sumTokens = (rows) => rows.reduce(
|
|
50
|
+
(sum, receipt) => sum + (Number(receipt?.amount?.tokens) || 0),
|
|
51
|
+
0,
|
|
52
|
+
);
|
|
53
|
+
const sourceCounts = costReceipts.reduce((counts, receipt) => {
|
|
54
|
+
const source = typeof receipt?.source === 'string' && receipt.source.length > 0
|
|
55
|
+
? receipt.source
|
|
56
|
+
: 'unknown';
|
|
57
|
+
counts[source] = (counts[source] ?? 0) + 1;
|
|
58
|
+
return counts;
|
|
59
|
+
}, {});
|
|
60
|
+
const allHaveModel = receipts.every(
|
|
61
|
+
(receipt) => typeof receipt?.telemetry?.model === 'string' && receipt.telemetry.model.length > 0,
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
console.log(`receipts.tokens.client_sources=${sumTokens(clientReceipts)}`);
|
|
65
|
+
console.log(`receipts.tokens.all_sources=${sumTokens(costReceipts)}`);
|
|
66
|
+
console.log(`flowSpent.tokens=${Number(flow?.flowSpent?.tokens) || 0}`);
|
|
67
|
+
console.log(`accumulator.tokens_total=${Number(accumulator?.tokens_total) || 0}`);
|
|
68
|
+
console.log(`receipts.counts_by_source=${JSON.stringify(sourceCounts)}`);
|
|
69
|
+
console.log(`every_receipt_has_model=${allHaveModel}`);
|
|
70
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
# compose-guard-sign — COMP-GUARD-ONE-TAP root-owned signer.
|
|
3
|
+
#
|
|
4
|
+
# Installed by `compose guard enrol` to /Library/Compose/guard/sign, owner
|
|
5
|
+
# root:wheel, mode 0755, and named by absolute path in
|
|
6
|
+
# /etc/sudoers.d/compose-guard with `timestamp_timeout=0`. Compose invokes it as
|
|
7
|
+
# `sudo -k /Library/Compose/guard/sign <namespace>` so every invocation
|
|
8
|
+
# re-authenticates (Touch ID via pam_tid, or the account password) and leaves
|
|
9
|
+
# no cached credential behind: one approval, exactly one signature.
|
|
10
|
+
#
|
|
11
|
+
# Usage: sign <namespace> < message > armored-signature
|
|
12
|
+
#
|
|
13
|
+
# The private key lives at /Library/Compose/guard/private/signing-key (root,
|
|
14
|
+
# 0600, in a root 0700 directory; the public half sits readable one level up at
|
|
15
|
+
# /Library/Compose/guard/signing-key.pub) and is never readable by the
|
|
16
|
+
# operator's user. This
|
|
17
|
+
# script pins PATH, calls every binary by absolute path, stages stdin under a
|
|
18
|
+
# root-private directory (never the caller's TMPDIR), signs with
|
|
19
|
+
# `ssh-keygen -Y sign`, prints the armored signature, and removes the staging
|
|
20
|
+
# directory on every exit path. The namespace is the only argument and is
|
|
21
|
+
# validated so it cannot smuggle options into ssh-keygen.
|
|
22
|
+
#
|
|
23
|
+
# Test seam: when NOT running as root, COMPOSE_GUARD_SIGN_KEY may point at an
|
|
24
|
+
# alternative key so the sshsig output can be verified in-suite without sudo,
|
|
25
|
+
# and staging falls back to TMPDIR. As root both are ignored — root's
|
|
26
|
+
# environment is not a caller input (and sudo's env_reset strips them anyway).
|
|
27
|
+
|
|
28
|
+
set -eu
|
|
29
|
+
umask 077
|
|
30
|
+
PATH=/usr/bin:/bin
|
|
31
|
+
export PATH
|
|
32
|
+
|
|
33
|
+
GUARD_DIR=/Library/Compose/guard
|
|
34
|
+
KEY="$GUARD_DIR/private/signing-key"
|
|
35
|
+
STAGE_ROOT="$GUARD_DIR/private/tmp"
|
|
36
|
+
|
|
37
|
+
NAMESPACE="${1:-}"
|
|
38
|
+
case "$NAMESPACE" in
|
|
39
|
+
'' ) echo "compose-guard-sign: usage: sign <namespace>" >&2; exit 64 ;;
|
|
40
|
+
*[!a-z0-9-]* ) echo "compose-guard-sign: invalid namespace" >&2; exit 64 ;;
|
|
41
|
+
esac
|
|
42
|
+
case "$NAMESPACE" in
|
|
43
|
+
[a-z]* ) ;;
|
|
44
|
+
* ) echo "compose-guard-sign: invalid namespace" >&2; exit 64 ;;
|
|
45
|
+
esac
|
|
46
|
+
if [ "${#NAMESPACE}" -gt 64 ]; then echo "compose-guard-sign: invalid namespace" >&2; exit 64; fi
|
|
47
|
+
|
|
48
|
+
if [ "$(/usr/bin/id -u)" -ne 0 ]; then
|
|
49
|
+
# Test seam only. Root ignores both.
|
|
50
|
+
if [ -n "${COMPOSE_GUARD_SIGN_KEY:-}" ]; then KEY="$COMPOSE_GUARD_SIGN_KEY"; fi
|
|
51
|
+
STAGE_ROOT="${TMPDIR:-/tmp}"
|
|
52
|
+
fi
|
|
53
|
+
[ -r "$KEY" ] || { echo "compose-guard-sign: signing key not readable: $KEY" >&2; exit 66; }
|
|
54
|
+
[ -d "$STAGE_ROOT" ] || { echo "compose-guard-sign: staging directory missing: $STAGE_ROOT" >&2; exit 66; }
|
|
55
|
+
|
|
56
|
+
WORK="$(/usr/bin/mktemp -d "$STAGE_ROOT/sign.XXXXXX")"
|
|
57
|
+
trap '/bin/rm -rf "$WORK"' EXIT INT TERM HUP
|
|
58
|
+
|
|
59
|
+
/bin/cat > "$WORK/message"
|
|
60
|
+
# -q: no chatter on stderr; the signature is written next to the message.
|
|
61
|
+
/usr/bin/ssh-keygen -Y sign -q -f "$KEY" -n "$NAMESPACE" "$WORK/message"
|
|
62
|
+
/bin/cat "$WORK/message.sig"
|
package/server/agent-health.js
CHANGED
|
@@ -36,6 +36,7 @@ export class HealthMonitor {
|
|
|
36
36
|
#silenceKillMs;
|
|
37
37
|
#defaultTimeoutMs;
|
|
38
38
|
#memoryLimitMB;
|
|
39
|
+
#suspended = false;
|
|
39
40
|
|
|
40
41
|
/** @type {Map<string, TrackedAgent>} */
|
|
41
42
|
#agents = new Map();
|
|
@@ -91,6 +92,7 @@ export class HealthMonitor {
|
|
|
91
92
|
// Start silence timers
|
|
92
93
|
this._resetSilenceTimers(agentId);
|
|
93
94
|
|
|
95
|
+
if (this.#suspended) return;
|
|
94
96
|
// Wall-clock timeout
|
|
95
97
|
entry.wallClockTimer = setTimeout(() => this._kill(agentId, 'wall_clock_timeout'), this.#defaultTimeoutMs);
|
|
96
98
|
|
|
@@ -117,6 +119,8 @@ export class HealthMonitor {
|
|
|
117
119
|
return this.#agents.has(agentId);
|
|
118
120
|
}
|
|
119
121
|
|
|
122
|
+
get hasActiveProcesses() { return this.#agents.size > 0; }
|
|
123
|
+
|
|
120
124
|
getTerminalReason(agentId) {
|
|
121
125
|
return this.#agents.get(agentId)?.terminalReason ?? null;
|
|
122
126
|
}
|
|
@@ -126,6 +130,23 @@ export class HealthMonitor {
|
|
|
126
130
|
if (entry) entry.terminalReason = reason;
|
|
127
131
|
}
|
|
128
132
|
|
|
133
|
+
suspend() {
|
|
134
|
+
this.#suspended = true;
|
|
135
|
+
for (const entry of this.#agents.values()) this._clearTimers(entry);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
resume() {
|
|
139
|
+
if (!this.#suspended) return;
|
|
140
|
+
this.#suspended = false;
|
|
141
|
+
for (const [id, entry] of this.#agents) {
|
|
142
|
+
if (entry.terminalReason) continue;
|
|
143
|
+
this._resetSilenceTimers(id);
|
|
144
|
+
entry.wallClockTimer = setTimeout(() => this._kill(id, 'wall_clock_timeout'),
|
|
145
|
+
Math.max(0, this.#defaultTimeoutMs - (Date.now() - entry.startedAt)));
|
|
146
|
+
if (this.#memoryLimitMB > 0) entry.memoryPollTimer = setInterval(() => this._checkMemory(id), MEMORY_POLL_INTERVAL_MS);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
129
150
|
/** Clean up all tracked agents and timers. */
|
|
130
151
|
destroy() {
|
|
131
152
|
for (const agentId of [...this.#agents.keys()]) {
|
|
@@ -136,6 +157,7 @@ export class HealthMonitor {
|
|
|
136
157
|
// ── Internal ────────────────────────────────────────────────────────────
|
|
137
158
|
|
|
138
159
|
_resetSilenceTimers(agentId) {
|
|
160
|
+
if (this.#suspended) return;
|
|
139
161
|
const entry = this.#agents.get(agentId);
|
|
140
162
|
if (!entry) return;
|
|
141
163
|
|
package/server/agent-hooks.js
CHANGED
|
@@ -9,6 +9,8 @@
|
|
|
9
9
|
* Terminal.jsx and vision-server.js).
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
+
import { getTargetRoot, withProjectContext } from './project-root.js';
|
|
13
|
+
import { deriveId } from '../lib/discover-workspaces.js';
|
|
12
14
|
import { resolvePort } from '../lib/resolve-port.js';
|
|
13
15
|
|
|
14
16
|
const API_SERVER = `http://127.0.0.1:${resolvePort()}`;
|
|
@@ -26,7 +28,11 @@ async function post(path, body) {
|
|
|
26
28
|
try {
|
|
27
29
|
const res = await fetch(`${API_SERVER}${path}`, {
|
|
28
30
|
method: 'POST',
|
|
29
|
-
headers: {
|
|
31
|
+
headers: {
|
|
32
|
+
'Content-Type': 'application/json',
|
|
33
|
+
'x-compose-workspace-id': deriveId({ root: getTargetRoot() }).id,
|
|
34
|
+
...(process.env.COMPOSE_API_TOKEN ? { 'x-compose-token': process.env.COMPOSE_API_TOKEN } : {}),
|
|
35
|
+
},
|
|
30
36
|
body: JSON.stringify(body),
|
|
31
37
|
signal: AbortSignal.timeout(5000),
|
|
32
38
|
});
|
|
@@ -102,3 +108,10 @@ export const HOOK_OPTIONS = {
|
|
|
102
108
|
SessionStart: [{ hooks: [sessionStartHook] }],
|
|
103
109
|
SessionEnd: [{ hooks: [sessionEndHook] }],
|
|
104
110
|
};
|
|
111
|
+
|
|
112
|
+
/** SDK callbacks can be invoked outside the HTTP async context. Pin explicitly. */
|
|
113
|
+
export function hooksForProject(binding) {
|
|
114
|
+
return Object.fromEntries(Object.entries(HOOK_OPTIONS).map(([event, groups]) => [event,
|
|
115
|
+
groups.map(group => ({ ...group, hooks: group.hooks.map(hook => (...args) => withProjectContext(binding, () => hook(...args))) })),
|
|
116
|
+
]));
|
|
117
|
+
}
|
package/server/agent-server.js
CHANGED
|
@@ -1,249 +1,15 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Agent Server (Tier 1 — immortal, port 3003 during migration, then 3002)
|
|
3
|
-
*
|
|
4
|
-
* Replaces terminal-server.js. Uses @anthropic-ai/claude-agent-sdk instead of
|
|
5
|
-
* node-pty + tmux. Streams typed SDK messages via SSE to the browser.
|
|
6
|
-
* User input arrives via POST — SSE is the right transport for a unidirectional
|
|
7
|
-
* async iterator.
|
|
8
|
-
*
|
|
9
|
-
* Process isolation: this server only talks to api-server (4001) via SDK hooks.
|
|
10
|
-
* SessionManager and VisionServer require zero changes.
|
|
11
|
-
*/
|
|
12
|
-
|
|
1
|
+
/** Immortal SDK server. Workspace sessions remain pinned across UI switches. */
|
|
13
2
|
import http from 'node:http';
|
|
14
|
-
import { readFileSync } from 'node:fs';
|
|
15
|
-
import path from 'node:path';
|
|
16
3
|
import express from 'express';
|
|
17
4
|
import cors from 'cors';
|
|
18
5
|
import { query } from '@anthropic-ai/claude-agent-sdk';
|
|
19
|
-
import {
|
|
20
|
-
import { HOOK_OPTIONS } from './agent-hooks.js';
|
|
21
|
-
import { getTargetRoot, getDataDir } from './project-root.js';
|
|
22
|
-
import { BuildStreamBridge } from './build-stream-bridge.js';
|
|
23
|
-
import { CoalescingBuffer } from './coalescing-buffer.js';
|
|
24
|
-
|
|
25
|
-
const _agentBuffer = new CoalescingBuffer((flushed) => {
|
|
26
|
-
if (flushed.agentMessage) {
|
|
27
|
-
for (const msg of flushed.agentMessage) {
|
|
28
|
-
_trackMessage(msg);
|
|
29
|
-
broadcast(msg);
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
}, { intervalMs: 16 });
|
|
33
|
-
_agentBuffer.register('agentMessage', 'append');
|
|
34
|
-
|
|
35
|
-
const _recentMessages = [];
|
|
36
|
-
const HYDRATE_LIMIT = 50;
|
|
37
|
-
|
|
38
|
-
function _trackMessage(msg) {
|
|
39
|
-
_recentMessages.push(msg);
|
|
40
|
-
if (_recentMessages.length > HYDRATE_LIMIT) _recentMessages.shift();
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
export function getAgentSnapshot() {
|
|
44
|
-
return _recentMessages.length > 0 ? [..._recentMessages] : null;
|
|
45
|
-
}
|
|
6
|
+
import { createAgentApp } from './agent-workspace.js';
|
|
46
7
|
|
|
47
8
|
const PORT = process.env.AGENT_PORT || 4002;
|
|
48
|
-
const
|
|
49
|
-
|
|
50
|
-
function _readModelSetting() {
|
|
51
|
-
try {
|
|
52
|
-
const raw = readFileSync(SETTINGS_FILE, 'utf-8');
|
|
53
|
-
return JSON.parse(raw)?.models?.interactive || null;
|
|
54
|
-
} catch {
|
|
55
|
-
return null;
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
|
|
9
|
+
const workspaceServer = createAgentApp({ query });
|
|
59
10
|
const app = express();
|
|
60
11
|
app.use(cors({ origin: /^https?:\/\/(localhost|127\.0\.0\.1)(:\d+)?$/ }));
|
|
61
|
-
app.use(
|
|
62
|
-
|
|
63
|
-
app.get('/api/health', (_req, res) => res.json({ ok: true }));
|
|
64
|
-
|
|
65
|
-
// ---------------------------------------------------------------------------
|
|
66
|
-
// Session state — one active SDK session at a time
|
|
67
|
-
// ---------------------------------------------------------------------------
|
|
68
|
-
|
|
69
|
-
/**
|
|
70
|
-
* Active session state:
|
|
71
|
-
* id: string|null — SDK session_id captured from init message
|
|
72
|
-
* queryIter: Query|null — SDK async iterator, has .interrupt() method
|
|
73
|
-
*/
|
|
74
|
-
let _session = { id: null, queryIter: null };
|
|
75
|
-
|
|
76
|
-
/** SSE clients waiting for messages */
|
|
77
|
-
const _sseClients = new Set();
|
|
78
|
-
|
|
79
|
-
function broadcast(msg) {
|
|
80
|
-
const line = `data: ${JSON.stringify(msg)}\n\n`;
|
|
81
|
-
for (const client of _sseClients) {
|
|
82
|
-
try {
|
|
83
|
-
client.write(line);
|
|
84
|
-
} catch {
|
|
85
|
-
_sseClients.delete(client);
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
// ---------------------------------------------------------------------------
|
|
91
|
-
// GET /api/agent/stream — SSE subscription
|
|
92
|
-
// ---------------------------------------------------------------------------
|
|
93
|
-
|
|
94
|
-
app.get('/api/agent/stream', (req, res) => {
|
|
95
|
-
res.setHeader('Content-Type', 'text/event-stream');
|
|
96
|
-
res.setHeader('Cache-Control', 'no-cache');
|
|
97
|
-
res.setHeader('Connection', 'keep-alive');
|
|
98
|
-
res.setHeader('X-Accel-Buffering', 'no'); // disable nginx buffering if proxied
|
|
99
|
-
res.flushHeaders();
|
|
100
|
-
|
|
101
|
-
_sseClients.add(res);
|
|
102
|
-
|
|
103
|
-
const snapshot = getAgentSnapshot();
|
|
104
|
-
if (snapshot) {
|
|
105
|
-
res.write(`event: hydrate\ndata: ${JSON.stringify(snapshot)}\n\n`);
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
// Immediately tell the new client whether a session exists
|
|
109
|
-
if (_session.id) {
|
|
110
|
-
res.write(`data: ${JSON.stringify({
|
|
111
|
-
type: 'system', subtype: 'connected', sessionId: _session.id,
|
|
112
|
-
})}\n\n`);
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
req.on('close', () => _sseClients.delete(res));
|
|
116
|
-
});
|
|
117
|
-
|
|
118
|
-
// ---------------------------------------------------------------------------
|
|
119
|
-
// POST /api/agent/session — create a fresh session
|
|
120
|
-
// ---------------------------------------------------------------------------
|
|
121
|
-
|
|
122
|
-
app.post('/api/agent/session', requireSensitiveToken, (req, res) => {
|
|
123
|
-
const { prompt = '' } = req.body || {};
|
|
124
|
-
if (!prompt.trim()) return res.status(400).json({ error: 'prompt is required' });
|
|
125
|
-
|
|
126
|
-
_killCurrentSession();
|
|
127
|
-
|
|
128
|
-
try {
|
|
129
|
-
const q = _startQuery(prompt, null);
|
|
130
|
-
_session = { id: null, queryIter: q };
|
|
131
|
-
_consumeStream(q);
|
|
132
|
-
res.json({ ok: true });
|
|
133
|
-
} catch (err) {
|
|
134
|
-
res.status(500).json({ error: err.message });
|
|
135
|
-
}
|
|
136
|
-
});
|
|
137
|
-
|
|
138
|
-
// ---------------------------------------------------------------------------
|
|
139
|
-
// POST /api/agent/message — send a follow-up message (resumes session)
|
|
140
|
-
// ---------------------------------------------------------------------------
|
|
141
|
-
|
|
142
|
-
app.post('/api/agent/message', requireSensitiveToken, (req, res) => {
|
|
143
|
-
const { prompt } = req.body || {};
|
|
144
|
-
if (!prompt?.trim()) return res.status(400).json({ error: 'prompt is required' });
|
|
145
|
-
|
|
146
|
-
const resumeId = _session.id;
|
|
147
|
-
_killCurrentSession();
|
|
148
|
-
|
|
149
|
-
try {
|
|
150
|
-
const q = _startQuery(prompt, resumeId);
|
|
151
|
-
_session = { id: resumeId, queryIter: q };
|
|
152
|
-
_consumeStream(q);
|
|
153
|
-
res.json({ ok: true, resumeSessionId: resumeId });
|
|
154
|
-
} catch (err) {
|
|
155
|
-
res.status(500).json({ error: err.message });
|
|
156
|
-
}
|
|
157
|
-
});
|
|
158
|
-
|
|
159
|
-
// ---------------------------------------------------------------------------
|
|
160
|
-
// POST /api/agent/interrupt — interrupt current query
|
|
161
|
-
// ---------------------------------------------------------------------------
|
|
162
|
-
|
|
163
|
-
app.post('/api/agent/interrupt', requireSensitiveToken, (req, res) => {
|
|
164
|
-
if (!_session.queryIter) {
|
|
165
|
-
return res.status(404).json({ error: 'No active query' });
|
|
166
|
-
}
|
|
167
|
-
try {
|
|
168
|
-
_session.queryIter.interrupt();
|
|
169
|
-
|
|
170
|
-
// COMP-AGT-1: Escalation — if not resolved within 5s, force-kill the session
|
|
171
|
-
const capturedIter = _session.queryIter;
|
|
172
|
-
setTimeout(() => {
|
|
173
|
-
if (_session.queryIter === capturedIter) {
|
|
174
|
-
console.log('[agent-server] Interrupt escalation: killing session after 5s timeout');
|
|
175
|
-
_killCurrentSession();
|
|
176
|
-
}
|
|
177
|
-
}, 5000);
|
|
178
|
-
|
|
179
|
-
res.json({ ok: true });
|
|
180
|
-
} catch (err) {
|
|
181
|
-
res.status(500).json({ error: err.message });
|
|
182
|
-
}
|
|
183
|
-
});
|
|
184
|
-
|
|
185
|
-
// ---------------------------------------------------------------------------
|
|
186
|
-
// GET /api/agent/session/status
|
|
187
|
-
// ---------------------------------------------------------------------------
|
|
188
|
-
|
|
189
|
-
app.get('/api/agent/session/status', (_req, res) => {
|
|
190
|
-
res.json({
|
|
191
|
-
active: !!_session.queryIter,
|
|
192
|
-
sessionId: _session.id || null,
|
|
193
|
-
});
|
|
194
|
-
});
|
|
195
|
-
|
|
196
|
-
// ---------------------------------------------------------------------------
|
|
197
|
-
// Internals
|
|
198
|
-
// ---------------------------------------------------------------------------
|
|
199
|
-
|
|
200
|
-
function _buildOptions(prompt, resumeId) {
|
|
201
|
-
return {
|
|
202
|
-
cwd: getTargetRoot(),
|
|
203
|
-
model: _readModelSetting() || 'claude-sonnet-4-6',
|
|
204
|
-
permissionMode: 'acceptEdits',
|
|
205
|
-
settingSources: ['project'],
|
|
206
|
-
tools: { type: 'preset', preset: 'claude_code' },
|
|
207
|
-
hooks: HOOK_OPTIONS,
|
|
208
|
-
...(resumeId ? { resume: resumeId } : {}),
|
|
209
|
-
};
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
function _startQuery(prompt, resumeId) {
|
|
213
|
-
return query({ prompt, options: _buildOptions(prompt, resumeId) });
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
function _killCurrentSession() {
|
|
217
|
-
if (_session.queryIter) {
|
|
218
|
-
try { _session.queryIter.return(); } catch { /* ignore */ }
|
|
219
|
-
}
|
|
220
|
-
_session = { id: null, queryIter: null };
|
|
221
|
-
_recentMessages.length = 0;
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
async function _consumeStream(q) {
|
|
225
|
-
try {
|
|
226
|
-
for await (const msg of q) {
|
|
227
|
-
// Capture session_id from the init message so we can resume
|
|
228
|
-
if (msg.type === 'system' && msg.subtype === 'init') {
|
|
229
|
-
_session.id = msg.session_id;
|
|
230
|
-
}
|
|
231
|
-
_agentBuffer.put('agentMessage', msg);
|
|
232
|
-
}
|
|
233
|
-
} catch (err) {
|
|
234
|
-
if (err?.name !== 'AbortError') {
|
|
235
|
-
broadcast({ type: 'error', message: err.message || String(err) });
|
|
236
|
-
}
|
|
237
|
-
} finally {
|
|
238
|
-
// Signal completion to clients regardless of success/error
|
|
239
|
-
if (_session.queryIter === q) {
|
|
240
|
-
_session.queryIter = null;
|
|
241
|
-
}
|
|
242
|
-
// Keep _recentMessages populated after natural session end so clients
|
|
243
|
-
// reloading the tab post-turn can still hydrate recent history.
|
|
244
|
-
// Ring is cleared only on force-kill / new-session start in _killCurrentSession.
|
|
245
|
-
}
|
|
246
|
-
}
|
|
12
|
+
app.use(workspaceServer.app);
|
|
247
13
|
|
|
248
14
|
// ---------------------------------------------------------------------------
|
|
249
15
|
// Server startup
|
|
@@ -252,23 +18,14 @@ async function _consumeStream(q) {
|
|
|
252
18
|
let serverListening = false;
|
|
253
19
|
const server = http.createServer(app);
|
|
254
20
|
|
|
255
|
-
// Build stream bridge — tails JSONL from CLI build, rebroadcasts via SSE
|
|
256
|
-
const _bridge = new BuildStreamBridge(
|
|
257
|
-
path.join(getTargetRoot(), '.compose'),
|
|
258
|
-
broadcast
|
|
259
|
-
);
|
|
260
|
-
|
|
261
21
|
server.listen(PORT, '127.0.0.1', () => {
|
|
262
22
|
serverListening = true;
|
|
263
|
-
_bridge.start();
|
|
264
23
|
console.log(`Agent server running on http://127.0.0.1:${PORT}`);
|
|
265
24
|
});
|
|
266
25
|
|
|
267
26
|
function shutdown(sig) {
|
|
268
27
|
console.log(`[agent-server] ${sig}, shutting down`);
|
|
269
|
-
|
|
270
|
-
_bridge.stop();
|
|
271
|
-
_killCurrentSession();
|
|
28
|
+
workspaceServer.close();
|
|
272
29
|
server.close();
|
|
273
30
|
setTimeout(() => process.exit(0), 1000);
|
|
274
31
|
}
|
package/server/agent-spawn.js
CHANGED
|
@@ -11,7 +11,6 @@ import { getTargetRoot } from './project-root.js';
|
|
|
11
11
|
import { gracefulKill } from './agent-health.js';
|
|
12
12
|
import { resolveSpawnProfile } from './mcp-tool-policy.js';
|
|
13
13
|
|
|
14
|
-
const PROJECT_ROOT = getTargetRoot();
|
|
15
14
|
|
|
16
15
|
// ---------------------------------------------------------------------------
|
|
17
16
|
// Route registration
|
|
@@ -35,7 +34,7 @@ function deriveAgentType(prompt) {
|
|
|
35
34
|
return 'claude';
|
|
36
35
|
}
|
|
37
36
|
|
|
38
|
-
export function attachAgentSpawnRoutes(app, { projectRoot =
|
|
37
|
+
export function attachAgentSpawnRoutes(app, { projectRoot = getTargetRoot(), broadcastMessage, requireSensitiveToken, registry, sessionManager, healthMonitor, worktreeGC }) {
|
|
39
38
|
const _agents = new Map();
|
|
40
39
|
// POST /api/agent/spawn — spawn a hidden Claude subagent
|
|
41
40
|
app.post('/api/agent/spawn', requireSensitiveToken, (req, res) => {
|
|
@@ -48,7 +47,7 @@ export function attachAgentSpawnRoutes(app, { projectRoot = PROJECT_ROOT, broadc
|
|
|
48
47
|
return res.status(409).json({ error: `Agent ${agentId} already running` });
|
|
49
48
|
}
|
|
50
49
|
|
|
51
|
-
const cleanEnv = { ...process.env, NO_COLOR: '1' };
|
|
50
|
+
const cleanEnv = { ...process.env, COMPOSE_TARGET: projectRoot, NO_COLOR: '1' };
|
|
52
51
|
delete cleanEnv.CLAUDECODE;
|
|
53
52
|
// COMP-MCP-ENFORCE-1: inject a TRUSTED, spawn-time MCP profile the subagent
|
|
54
53
|
// cannot rewrite (its compose-mcp child inherits this env). Only restrictive
|
|
@@ -217,7 +216,7 @@ export function attachAgentSpawnRoutes(app, { projectRoot = PROJECT_ROOT, broadc
|
|
|
217
216
|
const currentSessionId = sessionManager?.currentSession?.id ?? null;
|
|
218
217
|
if (agentId === currentSessionId) {
|
|
219
218
|
const agentPort = process.env.AGENT_PORT || 4002;
|
|
220
|
-
const headers = { 'Content-Type': 'application/json' };
|
|
219
|
+
const headers = { 'Content-Type': 'application/json', 'x-compose-project-root': projectRoot };
|
|
221
220
|
if (process.env.COMPOSE_API_TOKEN) headers['x-compose-token'] = process.env.COMPOSE_API_TOKEN;
|
|
222
221
|
fetch(`http://127.0.0.1:${agentPort}/api/agent/interrupt`, {
|
|
223
222
|
method: 'POST',
|