@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,294 @@
|
|
|
1
|
+
import { prepareProject, withProjectContext, getTargetRoot } from './project-root.js';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { readFileSync, realpathSync } from 'node:fs';
|
|
4
|
+
import express from 'express';
|
|
5
|
+
import { requireSensitiveToken } from './security.js';
|
|
6
|
+
import { hooksForProject } from './agent-hooks.js';
|
|
7
|
+
import { CoalescingBuffer } from './coalescing-buffer.js';
|
|
8
|
+
import { BuildStreamBridge } from './build-stream-bridge.js';
|
|
9
|
+
import { hasPersistedWork } from './workspace-activity.js';
|
|
10
|
+
|
|
11
|
+
/** One SDK session, message history and stream bridge per project. */
|
|
12
|
+
export function createAgentWorkspace(binding, { query }) {
|
|
13
|
+
const _agentBuffer = new CoalescingBuffer((flushed) => {
|
|
14
|
+
if (flushed.agentMessage) {
|
|
15
|
+
for (const msg of flushed.agentMessage) {
|
|
16
|
+
_trackMessage(msg);
|
|
17
|
+
broadcast(msg);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
}, { intervalMs: 16 });
|
|
21
|
+
_agentBuffer.register('agentMessage', 'append');
|
|
22
|
+
|
|
23
|
+
const _recentMessages = [];
|
|
24
|
+
const HYDRATE_LIMIT = 50;
|
|
25
|
+
|
|
26
|
+
function _trackMessage(msg) {
|
|
27
|
+
_recentMessages.push(msg);
|
|
28
|
+
if (_recentMessages.length > HYDRATE_LIMIT) _recentMessages.shift();
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function getAgentSnapshot() {
|
|
32
|
+
return _recentMessages.length > 0 ? [..._recentMessages] : null;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const SETTINGS_FILE = path.join(binding.dataDir, 'settings.json');
|
|
36
|
+
|
|
37
|
+
function _readModelSetting() {
|
|
38
|
+
try {
|
|
39
|
+
const raw = readFileSync(SETTINGS_FILE, 'utf-8');
|
|
40
|
+
return JSON.parse(raw)?.models?.interactive || null;
|
|
41
|
+
} catch {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const app = express.Router();
|
|
47
|
+
|
|
48
|
+
// C12: /api/health is registered once, on the app in createAgentApp. A copy
|
|
49
|
+
// here was unreachable (the app-level route answers first) and would have
|
|
50
|
+
// made liveness depend on resolving a workspace.
|
|
51
|
+
|
|
52
|
+
// ---------------------------------------------------------------------------
|
|
53
|
+
// Session state — one active SDK session at a time
|
|
54
|
+
// ---------------------------------------------------------------------------
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Active session state:
|
|
58
|
+
* id: string|null — SDK session_id captured from init message
|
|
59
|
+
* queryIter: Query|null — SDK async iterator, has .interrupt() method
|
|
60
|
+
*/
|
|
61
|
+
let _session = { id: null, queryIter: null };
|
|
62
|
+
const _queries = new Set();
|
|
63
|
+
|
|
64
|
+
/** SSE clients waiting for messages */
|
|
65
|
+
const _sseClients = new Set();
|
|
66
|
+
|
|
67
|
+
function broadcast(msg) {
|
|
68
|
+
const line = `data: ${JSON.stringify(msg)}\n\n`;
|
|
69
|
+
for (const client of _sseClients) {
|
|
70
|
+
try {
|
|
71
|
+
client.write(line);
|
|
72
|
+
} catch {
|
|
73
|
+
_sseClients.delete(client);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// ---------------------------------------------------------------------------
|
|
79
|
+
// GET /api/agent/stream — SSE subscription
|
|
80
|
+
// ---------------------------------------------------------------------------
|
|
81
|
+
|
|
82
|
+
app.get('/api/agent/stream', (req, res) => {
|
|
83
|
+
res.setHeader('Content-Type', 'text/event-stream');
|
|
84
|
+
res.setHeader('Cache-Control', 'no-cache');
|
|
85
|
+
res.setHeader('Connection', 'keep-alive');
|
|
86
|
+
res.setHeader('X-Accel-Buffering', 'no'); // disable nginx buffering if proxied
|
|
87
|
+
res.flushHeaders();
|
|
88
|
+
|
|
89
|
+
_sseClients.add(res);
|
|
90
|
+
|
|
91
|
+
const snapshot = getAgentSnapshot();
|
|
92
|
+
if (snapshot) {
|
|
93
|
+
res.write(`event: hydrate\ndata: ${JSON.stringify(snapshot)}\n\n`);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Immediately tell the new client whether a session exists
|
|
97
|
+
if (_session.id) {
|
|
98
|
+
res.write(`data: ${JSON.stringify({
|
|
99
|
+
type: 'system', subtype: 'connected', sessionId: _session.id,
|
|
100
|
+
})}\n\n`);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
req.on('close', () => _sseClients.delete(res));
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
// ---------------------------------------------------------------------------
|
|
107
|
+
// POST /api/agent/session — create a fresh session
|
|
108
|
+
// ---------------------------------------------------------------------------
|
|
109
|
+
|
|
110
|
+
app.post('/api/agent/session', requireSensitiveToken, (req, res) => {
|
|
111
|
+
const { prompt = '' } = req.body || {};
|
|
112
|
+
if (!prompt.trim()) return res.status(400).json({ error: 'prompt is required' });
|
|
113
|
+
|
|
114
|
+
_killCurrentSession();
|
|
115
|
+
|
|
116
|
+
try {
|
|
117
|
+
const q = _startQuery(prompt, null);
|
|
118
|
+
_session = { id: null, queryIter: q };
|
|
119
|
+
_consumeStream(q);
|
|
120
|
+
res.json({ ok: true });
|
|
121
|
+
} catch (err) {
|
|
122
|
+
res.status(500).json({ error: err.message });
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
// ---------------------------------------------------------------------------
|
|
127
|
+
// POST /api/agent/message — send a follow-up message (resumes session)
|
|
128
|
+
// ---------------------------------------------------------------------------
|
|
129
|
+
|
|
130
|
+
app.post('/api/agent/message', requireSensitiveToken, (req, res) => {
|
|
131
|
+
const { prompt } = req.body || {};
|
|
132
|
+
if (!prompt?.trim()) return res.status(400).json({ error: 'prompt is required' });
|
|
133
|
+
|
|
134
|
+
const resumeId = _session.id;
|
|
135
|
+
_killCurrentSession();
|
|
136
|
+
|
|
137
|
+
try {
|
|
138
|
+
const q = _startQuery(prompt, resumeId);
|
|
139
|
+
_session = { id: resumeId, queryIter: q };
|
|
140
|
+
_consumeStream(q);
|
|
141
|
+
res.json({ ok: true, resumeSessionId: resumeId });
|
|
142
|
+
} catch (err) {
|
|
143
|
+
res.status(500).json({ error: err.message });
|
|
144
|
+
}
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
// ---------------------------------------------------------------------------
|
|
148
|
+
// POST /api/agent/interrupt — interrupt current query
|
|
149
|
+
// ---------------------------------------------------------------------------
|
|
150
|
+
|
|
151
|
+
app.post('/api/agent/interrupt', requireSensitiveToken, (req, res) => {
|
|
152
|
+
if (!_session.queryIter) {
|
|
153
|
+
return res.status(404).json({ error: 'No active query' });
|
|
154
|
+
}
|
|
155
|
+
try {
|
|
156
|
+
_session.queryIter.interrupt();
|
|
157
|
+
|
|
158
|
+
// COMP-AGT-1: Escalation — if not resolved within 5s, force-kill the session
|
|
159
|
+
const capturedIter = _session.queryIter;
|
|
160
|
+
setTimeout(() => {
|
|
161
|
+
if (_session.queryIter === capturedIter) {
|
|
162
|
+
console.log('[agent-server] Interrupt escalation: killing session after 5s timeout');
|
|
163
|
+
_killCurrentSession();
|
|
164
|
+
}
|
|
165
|
+
}, 5000);
|
|
166
|
+
|
|
167
|
+
res.json({ ok: true });
|
|
168
|
+
} catch (err) {
|
|
169
|
+
res.status(500).json({ error: err.message });
|
|
170
|
+
}
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
// ---------------------------------------------------------------------------
|
|
174
|
+
// GET /api/agent/session/status
|
|
175
|
+
// ---------------------------------------------------------------------------
|
|
176
|
+
|
|
177
|
+
app.get('/api/agent/session/status', (_req, res) => {
|
|
178
|
+
res.json({
|
|
179
|
+
active: !!_session.queryIter,
|
|
180
|
+
sessionId: _session.id || null,
|
|
181
|
+
});
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
// ---------------------------------------------------------------------------
|
|
185
|
+
// Internals
|
|
186
|
+
// ---------------------------------------------------------------------------
|
|
187
|
+
|
|
188
|
+
function _buildOptions(prompt, resumeId) {
|
|
189
|
+
return {
|
|
190
|
+
cwd: binding.targetRoot,
|
|
191
|
+
env: { ...process.env, COMPOSE_TARGET: binding.targetRoot },
|
|
192
|
+
model: _readModelSetting() || 'claude-sonnet-4-6',
|
|
193
|
+
permissionMode: 'acceptEdits',
|
|
194
|
+
settingSources: ['project'],
|
|
195
|
+
tools: { type: 'preset', preset: 'claude_code' },
|
|
196
|
+
hooks: hooksForProject(binding),
|
|
197
|
+
...(resumeId ? { resume: resumeId } : {}),
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function _startQuery(prompt, resumeId) {
|
|
202
|
+
return query({ prompt, options: _buildOptions(prompt, resumeId) });
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function _killCurrentSession() {
|
|
206
|
+
if (_session.queryIter) {
|
|
207
|
+
try { Promise.resolve(_session.queryIter.return()).catch(() => {}); } catch { /* ignore */ }
|
|
208
|
+
}
|
|
209
|
+
_session = { id: null, queryIter: null };
|
|
210
|
+
_recentMessages.length = 0;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
async function _consumeStream(q) {
|
|
214
|
+
_queries.add(q);
|
|
215
|
+
try {
|
|
216
|
+
for await (const msg of q) {
|
|
217
|
+
// Capture session_id from the init message so we can resume
|
|
218
|
+
if (_session.queryIter !== q) break;
|
|
219
|
+
if (msg.type === 'system' && msg.subtype === 'init') {
|
|
220
|
+
_session.id = msg.session_id;
|
|
221
|
+
}
|
|
222
|
+
_agentBuffer.put('agentMessage', msg);
|
|
223
|
+
}
|
|
224
|
+
} catch (err) {
|
|
225
|
+
if (err?.name !== 'AbortError') {
|
|
226
|
+
broadcast({ type: 'error', message: err.message || String(err) });
|
|
227
|
+
}
|
|
228
|
+
} finally {
|
|
229
|
+
_queries.delete(q);
|
|
230
|
+
// Signal completion to clients regardless of success/error
|
|
231
|
+
if (_session.queryIter === q) {
|
|
232
|
+
_session.queryIter = null;
|
|
233
|
+
}
|
|
234
|
+
// Keep _recentMessages populated after natural session end so clients
|
|
235
|
+
// reloading the tab post-turn can still hydrate recent history.
|
|
236
|
+
// Ring is cleared only on force-kill / new-session start in _killCurrentSession.
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
const bridge = new BuildStreamBridge(path.join(binding.targetRoot, '.compose'), broadcast);
|
|
242
|
+
bridge.start();
|
|
243
|
+
return {
|
|
244
|
+
router: app,
|
|
245
|
+
getAgentSnapshot,
|
|
246
|
+
get busy() { return _queries.size > 0 || _sseClients.size > 0 || hasPersistedWork(binding.dataDir); },
|
|
247
|
+
close() {
|
|
248
|
+
_agentBuffer.stop(); bridge.stop(); _killCurrentSession();
|
|
249
|
+
for (const client of _sseClients) client.end();
|
|
250
|
+
_sseClients.clear();
|
|
251
|
+
},
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/** Shared by the production process and executable-boundary integration probes. */
|
|
256
|
+
export function createAgentApp({ query, maxWorkspaces = 8 }) {
|
|
257
|
+
if (!Number.isInteger(maxWorkspaces) || maxWorkspaces < 1) throw new Error("maxWorkspaces must be a positive integer");
|
|
258
|
+
const app = express();
|
|
259
|
+
app.use(express.json());
|
|
260
|
+
app.get('/api/health', (_req, res) => res.json({ ok: true }));
|
|
261
|
+
const workspaces = new Map();
|
|
262
|
+
app.use((req, res, next) => {
|
|
263
|
+
const root = req.get('x-compose-project-root');
|
|
264
|
+
const dispatch = () => {
|
|
265
|
+
try {
|
|
266
|
+
const target = realpathSync(root || getTargetRoot());
|
|
267
|
+
// A token authorizes the proxy, not turning arbitrary directories into
|
|
268
|
+
// writable SDK workspaces. Header targets must already be configured.
|
|
269
|
+
if (root && target !== realpathSync(getTargetRoot())) {
|
|
270
|
+
const config = JSON.parse(readFileSync(path.join(target, '.compose', 'compose.json'), 'utf8'));
|
|
271
|
+
if (!config || typeof config !== 'object' || Array.isArray(config)) throw new Error('Target must be a configured Compose workspace');
|
|
272
|
+
}
|
|
273
|
+
const binding = prepareProject(target);
|
|
274
|
+
if (!workspaces.has(target) && workspaces.size >= maxWorkspaces) {
|
|
275
|
+
const idle = [...workspaces].find(([, workspace]) => !workspace.busy);
|
|
276
|
+
if (!idle) return res.status(409).json({ error: `Workspace capacity reached (${maxWorkspaces}); every retained workspace is busy`, code: 'WorkspaceCapacityExceeded' });
|
|
277
|
+
idle[1].close(); workspaces.delete(idle[0]);
|
|
278
|
+
}
|
|
279
|
+
let workspace = workspaces.get(binding.targetRoot);
|
|
280
|
+
if (!workspace) {
|
|
281
|
+
workspace = withProjectContext(binding, () => createAgentWorkspace(binding, { query }));
|
|
282
|
+
workspaces.set(binding.targetRoot, workspace);
|
|
283
|
+
}
|
|
284
|
+
// Map insertion order is the LRU order, including status/stream reads.
|
|
285
|
+
workspaces.delete(binding.targetRoot); workspaces.set(binding.targetRoot, workspace);
|
|
286
|
+
return withProjectContext(binding, () => workspace.router(req, res, next));
|
|
287
|
+
} catch (err) { return res.status(400).json({ error: err.message }); }
|
|
288
|
+
};
|
|
289
|
+
// The proxy selects cwd with a server-stamped root and sensitive credential.
|
|
290
|
+
if (root) return requireSensitiveToken(req, res, dispatch);
|
|
291
|
+
return dispatch();
|
|
292
|
+
});
|
|
293
|
+
return { app, close: () => { for (const workspace of workspaces.values()) workspace.close(); } };
|
|
294
|
+
}
|
package/server/build-routes.js
CHANGED
|
@@ -20,7 +20,7 @@ import { runNew as defaultRunNew } from '../lib/new.js';
|
|
|
20
20
|
import { runBuildAll as defaultRunBuildAll } from '../lib/build-all.js';
|
|
21
21
|
import { runGsd as defaultRunGsd } from '../lib/gsd.js';
|
|
22
22
|
import { requireSensitiveOrPaired as requireSensitiveToken } from './security.js';
|
|
23
|
-
import { getDataDir as defaultGetDataDir, getTargetRoot as defaultGetTargetRoot } from './project-root.js';
|
|
23
|
+
import { getDataDir as defaultGetDataDir, getTargetRoot as defaultGetTargetRoot, trackProjectWork } from './project-root.js';
|
|
24
24
|
import { readBuildHistory } from '../lib/build-history.js';
|
|
25
25
|
|
|
26
26
|
// A launcher conflict (another build/GSD run already owns the feature) is a
|
|
@@ -32,11 +32,12 @@ function launcherErrorStatus(err) {
|
|
|
32
32
|
}
|
|
33
33
|
|
|
34
34
|
export function attachBuildRoutes(app, deps = {}) {
|
|
35
|
-
const
|
|
35
|
+
const tracked = run => (...args) => trackProjectWork(() => run(...args));
|
|
36
|
+
const runBuild = tracked(deps.runBuild || defaultRunBuild);
|
|
36
37
|
const abortBuild = deps.abortBuild || defaultAbortBuild;
|
|
37
|
-
const runNew = deps.runNew || defaultRunNew;
|
|
38
|
-
const runBuildAll = deps.runBuildAll || defaultRunBuildAll;
|
|
39
|
-
const runGsd = deps.runGsd || defaultRunGsd;
|
|
38
|
+
const runNew = tracked(deps.runNew || defaultRunNew);
|
|
39
|
+
const runBuildAll = tracked(deps.runBuildAll || defaultRunBuildAll);
|
|
40
|
+
const runGsd = tracked(deps.runGsd || defaultRunGsd);
|
|
40
41
|
const getDataDir = deps.getDataDir || defaultGetDataDir;
|
|
41
42
|
// Bind build dispatch to the active project root (the same global target the
|
|
42
43
|
// vision store + getDataDir() use). Without this the runners default to
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
import { readFileSync, statSync, watch } from 'node:fs';
|
|
10
10
|
import { join, dirname, basename } from 'node:path';
|
|
11
11
|
import { existsSync } from 'node:fs';
|
|
12
|
+
import { emitDecisionEvent, buildPolicyViolationEvent } from './decision-event-emit.js';
|
|
12
13
|
|
|
13
14
|
const JSONL_FILENAME = 'build-stream.jsonl';
|
|
14
15
|
const DEFAULT_CRASH_TIMEOUT_MS = 300_000; // 5 min
|
|
@@ -16,6 +17,12 @@ const STALE_GATE_TIMEOUT_MS = 86_400_000; // 24h
|
|
|
16
17
|
const DEBOUNCE_MS = 50;
|
|
17
18
|
const POLL_INTERVAL_MS = 2000;
|
|
18
19
|
|
|
20
|
+
// COMP-AGENT-LANES: forward a producer-stamped lane envelope untouched; absent,
|
|
21
|
+
// contribute nothing so lane-less events keep their exact historical shape.
|
|
22
|
+
function laneOf(event) {
|
|
23
|
+
return event.lane && typeof event.lane === 'object' ? { lane: event.lane } : {};
|
|
24
|
+
}
|
|
25
|
+
|
|
19
26
|
export class BuildStreamBridge {
|
|
20
27
|
#filePath;
|
|
21
28
|
#composeDir;
|
|
@@ -219,6 +226,23 @@ export class BuildStreamBridge {
|
|
|
219
226
|
if (mapped) {
|
|
220
227
|
this.#broadcast(mapped);
|
|
221
228
|
}
|
|
229
|
+
|
|
230
|
+
// COMP-POLICY-CHECK-5: a policy match also lands on the feature-level
|
|
231
|
+
// decision timeline, alongside the step event, for live cockpit visibility.
|
|
232
|
+
if (event.type === 'policy_violation') {
|
|
233
|
+
try {
|
|
234
|
+
emitDecisionEvent(this.#broadcast, buildPolicyViolationEvent({
|
|
235
|
+
featureCode: event.featureCode ?? null,
|
|
236
|
+
buildId: event.buildId ?? null,
|
|
237
|
+
stepId: event.stepId,
|
|
238
|
+
rule: event.rule,
|
|
239
|
+
matched: event.matched,
|
|
240
|
+
suppressed: event.suppressed,
|
|
241
|
+
userMode: event.userMode,
|
|
242
|
+
timestamp: event._ts ? new Date(event._ts).toISOString() : undefined,
|
|
243
|
+
}));
|
|
244
|
+
} catch { /* timeline emit is best-effort — never break the tail loop */ }
|
|
245
|
+
}
|
|
222
246
|
}
|
|
223
247
|
}
|
|
224
248
|
|
|
@@ -304,6 +328,7 @@ export class BuildStreamBridge {
|
|
|
304
328
|
flowId: event.flowId,
|
|
305
329
|
...(event.parentFlowId ? { parentFlowId: event.parentFlowId } : {}),
|
|
306
330
|
...(event.parallel ? { parallel: true } : {}),
|
|
331
|
+
...laneOf(event),
|
|
307
332
|
_source: 'build',
|
|
308
333
|
};
|
|
309
334
|
|
|
@@ -311,6 +336,7 @@ export class BuildStreamBridge {
|
|
|
311
336
|
return {
|
|
312
337
|
type: 'assistant',
|
|
313
338
|
message: { content: [{ type: 'tool_use', name: event.tool, input: event.input }] },
|
|
339
|
+
...laneOf(event),
|
|
314
340
|
_source: 'build',
|
|
315
341
|
};
|
|
316
342
|
|
|
@@ -318,6 +344,7 @@ export class BuildStreamBridge {
|
|
|
318
344
|
return {
|
|
319
345
|
type: 'assistant', subtype: 'tool_use_summary',
|
|
320
346
|
summary: event.summary, output: event.output,
|
|
347
|
+
...laneOf(event),
|
|
321
348
|
_source: 'build',
|
|
322
349
|
};
|
|
323
350
|
|
|
@@ -332,6 +359,7 @@ export class BuildStreamBridge {
|
|
|
332
359
|
return {
|
|
333
360
|
type: 'assistant',
|
|
334
361
|
message: { content: [{ type: 'text', text: event.content }] },
|
|
362
|
+
...laneOf(event),
|
|
335
363
|
_source: 'build',
|
|
336
364
|
};
|
|
337
365
|
|
|
@@ -348,6 +376,11 @@ export class BuildStreamBridge {
|
|
|
348
376
|
cumulative_cost_usd: event.cumulative_cost_usd ?? 0,
|
|
349
377
|
...(event.parentFlowId ? { parentFlowId: event.parentFlowId } : {}),
|
|
350
378
|
...(event.parallel ? { parallel: true } : {}),
|
|
379
|
+
// COMP-AGENT-LANES: explicit terminal status — the UI must not infer
|
|
380
|
+
// "complete" from the done event's existence.
|
|
381
|
+
...(event.status ? { status: event.status } : {}),
|
|
382
|
+
...(event.outcome ? { outcome: event.outcome } : {}),
|
|
383
|
+
...laneOf(event),
|
|
351
384
|
_source: 'build',
|
|
352
385
|
};
|
|
353
386
|
|
|
@@ -374,6 +407,13 @@ export class BuildStreamBridge {
|
|
|
374
407
|
return {
|
|
375
408
|
type: 'error',
|
|
376
409
|
message: event.message, source: 'build',
|
|
410
|
+
// COMP-AGENT-LANES: stepId + lane let the cockpit scope a failure to
|
|
411
|
+
// its worker lane (C5 — dropped here before, so lane-scoped failure
|
|
412
|
+
// could never fire). laneTerminal distinguishes a lane-closing error
|
|
413
|
+
// from an advisory one — the reducer closes only on laneTerminal.
|
|
414
|
+
...(event.stepId ? { stepId: event.stepId } : {}),
|
|
415
|
+
...(event.laneTerminal === true ? { laneTerminal: true } : {}),
|
|
416
|
+
...laneOf(event),
|
|
377
417
|
_source: 'build',
|
|
378
418
|
};
|
|
379
419
|
|
|
@@ -505,6 +545,19 @@ export class BuildStreamBridge {
|
|
|
505
545
|
_source: 'build',
|
|
506
546
|
};
|
|
507
547
|
|
|
548
|
+
// COMP-POLICY-CHECK-5: pre-response policy check pattern matches
|
|
549
|
+
case 'policy_violation':
|
|
550
|
+
return {
|
|
551
|
+
type: 'system', subtype: 'policy_violation',
|
|
552
|
+
stepId: event.stepId,
|
|
553
|
+
rule: event.rule,
|
|
554
|
+
matched: event.matched,
|
|
555
|
+
suppressed: Boolean(event.suppressed),
|
|
556
|
+
detail: event.detail,
|
|
557
|
+
userMode: event.userMode,
|
|
558
|
+
_source: 'build',
|
|
559
|
+
};
|
|
560
|
+
|
|
508
561
|
default:
|
|
509
562
|
return null; // unknown event type — skip
|
|
510
563
|
}
|
|
@@ -273,7 +273,9 @@ export class CCSessionWatcher {
|
|
|
273
273
|
}
|
|
274
274
|
|
|
275
275
|
start() {
|
|
276
|
-
|
|
276
|
+
// C5: the fs.watch fallback leaves `_watcher` null, so guarding on it alone
|
|
277
|
+
// let every resume() start ANOTHER poll interval — one leaked per switch.
|
|
278
|
+
if (this._watcher || this._pollTimer) return;
|
|
277
279
|
if (!fs.existsSync(this.projectsRoot)) {
|
|
278
280
|
fs.mkdirSync(this.projectsRoot, { recursive: true });
|
|
279
281
|
}
|
|
@@ -296,6 +298,7 @@ export class CCSessionWatcher {
|
|
|
296
298
|
}
|
|
297
299
|
|
|
298
300
|
_startPolling(intervalMs = 2000) {
|
|
301
|
+
if (this._pollTimer) return;
|
|
299
302
|
this._pollTimer = setInterval(async () => {
|
|
300
303
|
const files = listJsonlFiles(this.projectsRoot);
|
|
301
304
|
for (const f of files) {
|
|
@@ -5,10 +5,16 @@ export class CoalescingBuffer {
|
|
|
5
5
|
#pending = new Map();
|
|
6
6
|
#timer = null;
|
|
7
7
|
#flushFn;
|
|
8
|
+
#intervalMs;
|
|
8
9
|
|
|
9
10
|
constructor(flushFn, { intervalMs = 16 } = {}) {
|
|
10
11
|
this.#flushFn = flushFn;
|
|
11
|
-
this.#
|
|
12
|
+
this.#intervalMs = intervalMs;
|
|
13
|
+
this.start();
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
start() {
|
|
17
|
+
this.#timer ??= setInterval(() => this.#flush(), this.#intervalMs);
|
|
12
18
|
}
|
|
13
19
|
|
|
14
20
|
register(key, mode) {
|