@smartmemory/compose 0.3.7 → 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/.compose-deps.json +1 -13
- package/README.md +72 -5
- package/bin/compose.js +470 -351
- package/bin/judgment-migrate.js +387 -0
- package/contracts/comp-obs-contract.schema.json +9 -3
- package/contracts/fluid-record.schema.json +209 -0
- package/contracts/lifecycle-backfill.schema.json +322 -0
- package/dist/assets/App-Z4MU-H_F.js +916 -0
- package/dist/assets/{_baseUniq-Bo837sRJ.js → _baseUniq-ClWoCPFl.js} +1 -1
- package/dist/assets/{arc-BafGpyqE.js → arc-DY26UIVo.js} +1 -1
- package/dist/assets/{architectureDiagram-Q4EWVU46-BOBfUsqL.js → architectureDiagram-Q4EWVU46-6Ggq4DqJ.js} +1 -1
- package/dist/assets/{blockDiagram-DXYQGD6D-Dwodev1a.js → blockDiagram-DXYQGD6D-CH3Ked0l.js} +1 -1
- package/dist/assets/{browser-1ntj1-x_.js → browser-BWkrenen.js} +1 -1
- package/dist/assets/{c4Diagram-AHTNJAMY-CU_bhYag.js → c4Diagram-AHTNJAMY-Bk8dYilu.js} +1 -1
- package/dist/assets/channel-SnZzzh7k.js +1 -0
- package/dist/assets/{chunk-4BX2VUAB-p8WsDwnO.js → chunk-4BX2VUAB-BMR0XaAQ.js} +1 -1
- package/dist/assets/{chunk-4TB4RGXK-B8h7-eR0.js → chunk-4TB4RGXK-JytR14a9.js} +1 -1
- package/dist/assets/{chunk-55IACEB6-DxeEr98s.js → chunk-55IACEB6-B4Q97BCP.js} +1 -1
- package/dist/assets/{chunk-EDXVE4YY-BYt8F151.js → chunk-EDXVE4YY-R_qarkSf.js} +1 -1
- package/dist/assets/{chunk-FMBD7UC4-DGSOVeie.js → chunk-FMBD7UC4-C9s7KR9m.js} +1 -1
- package/dist/assets/{chunk-OYMX7WX6-B-QdgYR2.js → chunk-OYMX7WX6-BySQzVxc.js} +1 -1
- package/dist/assets/{chunk-QZHKN3VN-Du5UAZLs.js → chunk-QZHKN3VN-DdpSYZsW.js} +1 -1
- package/dist/assets/{chunk-YZCP3GAM-C8JbNBSk.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-S5V4N54A-O1ESaqge.js → cose-bilkent-S5V4N54A-BdlU6ZX_.js} +1 -1
- package/dist/assets/{dagre-KV5264BT-CPTmFPHw.js → dagre-KV5264BT-Cp3F5KTn.js} +1 -1
- package/dist/assets/{diagram-5BDNPKRD-B3PNrWs5.js → diagram-5BDNPKRD-DiR6_2q_.js} +1 -1
- package/dist/assets/{diagram-G4DWMVQ6-Cscfr6vc.js → diagram-G4DWMVQ6-w0i-p5HX.js} +1 -1
- package/dist/assets/{diagram-MMDJMWI5-CSfqZ-TM.js → diagram-MMDJMWI5-tIHhwUv3.js} +1 -1
- package/dist/assets/{diagram-TYMM5635-Cg4aYS7W.js → diagram-TYMM5635-BAeY3B19.js} +1 -1
- package/dist/assets/{erDiagram-SMLLAGMA-_ZqwG5pl.js → erDiagram-SMLLAGMA-Ckx_Knko.js} +1 -1
- package/dist/assets/{flowDiagram-DWJPFMVM-C83boxFT.js → flowDiagram-DWJPFMVM-DeoNka6J.js} +1 -1
- package/dist/assets/{ganttDiagram-T4ZO3ILL-CWnIjuEi.js → ganttDiagram-T4ZO3ILL-BmGnFbEg.js} +1 -1
- package/dist/assets/{gitGraphDiagram-UUTBAWPF-DrMdxZfH.js → gitGraphDiagram-UUTBAWPF-Dk48IHsx.js} +1 -1
- package/dist/assets/{graph-RE4I7Ty7.js → graph-BNzKGvoy.js} +1 -1
- package/dist/assets/{graph-Bi99_6Yf.js → graph-CI_1htl0.js} +1 -1
- package/dist/assets/{index-Rm2RE-c0.js → index-BEfrNBp8.js} +3 -3
- package/dist/assets/index-yyrA5OZd.css +1 -0
- package/dist/assets/{infoDiagram-42DDH7IO-BLmP4Epr.js → infoDiagram-42DDH7IO-BRf827i0.js} +1 -1
- package/dist/assets/{ishikawaDiagram-UXIWVN3A-yuWWshKN.js → ishikawaDiagram-UXIWVN3A-0kCZaeCM.js} +1 -1
- package/dist/assets/{journeyDiagram-VCZTEJTY-BOfhaJov.js → journeyDiagram-VCZTEJTY-rvU7ayRt.js} +1 -1
- package/dist/assets/{kanban-definition-6JOO6SKY-Bbolde15.js → kanban-definition-6JOO6SKY-DpQwX1C5.js} +1 -1
- package/dist/assets/{layout-BSf33zm8.js → layout-BI8cXFPI.js} +1 -1
- package/dist/assets/{linear-AvSTWMqx.js → linear-a0glcDiw.js} +1 -1
- package/dist/assets/{min-QBM8H4xN.js → min-vPHfnXcC.js} +1 -1
- package/dist/assets/{mindmap-definition-QFDTVHPH-BuvgtqIc.js → mindmap-definition-QFDTVHPH-D14eF-7C.js} +1 -1
- package/dist/assets/mobile-B7m9EO9D.js +17 -0
- package/dist/assets/{pieDiagram-DEJITSTG-DIzF16vh.js → pieDiagram-DEJITSTG-Cno-gETh.js} +1 -1
- package/dist/assets/{quadrantDiagram-34T5L4WZ-D-mbUIjS.js → quadrantDiagram-34T5L4WZ-BUQM1Hfm.js} +1 -1
- package/dist/assets/{requirementDiagram-MS252O5E-CEs4kCLd.js → requirementDiagram-MS252O5E-pOXlN2-q.js} +1 -1
- package/dist/assets/{sankeyDiagram-XADWPNL6-DFsnCr9n.js → sankeyDiagram-XADWPNL6-Crynd3_b.js} +1 -1
- package/dist/assets/{sequenceDiagram-FGHM5R23-BEJYdTjQ.js → sequenceDiagram-FGHM5R23-D9fZdCM8.js} +1 -1
- package/dist/assets/{stateDiagram-FHFEXIEX-BBXs57uY.js → stateDiagram-FHFEXIEX-CW9qVec8.js} +1 -1
- package/dist/assets/stateDiagram-v2-QKLJ7IA2-DkVLzHbY.js +1 -0
- package/dist/assets/{timeline-definition-GMOUNBTQ-BGvLoVAY.js → timeline-definition-GMOUNBTQ-BcHzhm_8.js} +1 -1
- package/dist/assets/{vennDiagram-DHZGUBPP-9LaBTMe0.js → vennDiagram-DHZGUBPP-BfytJcWk.js} +1 -1
- package/dist/assets/{wardley-RL74JXVD-P4MEqMTP.js → wardley-RL74JXVD-DLj-IjyB.js} +1 -1
- package/dist/assets/{wardleyDiagram-NUSXRM2D-o-tmxnlC.js → wardleyDiagram-NUSXRM2D-Ds0Ue68c.js} +1 -1
- package/dist/assets/{xychartDiagram-5P7HB3ND-Dpn7V6qk.js → xychartDiagram-5P7HB3ND-vjWDXFL6.js} +1 -1
- 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 +9 -0
- package/lib/build-stream-schema.js +3 -1
- package/lib/build-stream-writer.js +25 -0
- package/lib/build.js +874 -170
- package/lib/canon-guard.js +28 -6
- package/lib/canon-override.js +196 -0
- package/lib/canon-registry.js +104 -0
- package/lib/cli-commands.js +144 -0
- package/lib/codex-preflight.js +26 -13
- 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 +3 -3
- 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 +57 -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 -23
- 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/ideabox-cli.js +315 -0
- package/lib/ideabox.js +121 -21
- package/lib/judgment/store/index.js +9 -1
- package/lib/judgment/store/records.js +1 -1
- package/lib/judgment/trace.js +380 -0
- package/lib/judgment-decision-write.js +277 -0
- package/lib/judgment-decisions.js +466 -0
- package/lib/judgment-gen.js +5 -1
- package/lib/judgment-writer.js +56 -2
- package/lib/lifecycle-modes.js +4 -4
- package/lib/lineage.js +400 -0
- package/lib/local-claude-connector.js +52 -1
- package/lib/maya-client.js +302 -0
- package/lib/maya-config.js +53 -0
- package/lib/maya-identity.js +283 -0
- 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 +396 -199
- 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 +271 -36
- 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 +109 -23
- package/server/compose-mcp.js +88 -882
- 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 +34 -2
- 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-PkZzHeMj.js +0 -894
- package/dist/assets/channel-qVK_qn4E.js +0 -1
- package/dist/assets/classDiagram-6PBFFD2Q-B8UcfC1q.js +0 -1
- package/dist/assets/classDiagram-v2-HSJHXN6E-B8UcfC1q.js +0 -1
- package/dist/assets/clone-Pu3RyLUh.js +0 -1
- package/dist/assets/index-LIwREYgH.css +0 -1
- package/dist/assets/mobile-BnXEOE3U.js +0 -17
- package/dist/assets/stateDiagram-v2-QKLJ7IA2-BqKuX4rj.js +0 -1
- package/lib/staleness.js +0 -87
- package/server/ideabox-cache.js +0 -77
package/server/index.js
CHANGED
|
@@ -3,16 +3,10 @@ import cors from 'cors';
|
|
|
3
3
|
import http from 'node:http';
|
|
4
4
|
import path from 'node:path';
|
|
5
5
|
import { existsSync, statSync, readFileSync } from 'node:fs';
|
|
6
|
-
import {
|
|
7
|
-
import { VisionStore } from './vision-store.js';
|
|
8
|
-
import { VisionServer } from './vision-server.js';
|
|
9
|
-
import { SessionManager } from './session-manager.js';
|
|
10
|
-
import { scanFeatures, seedFeatures, scanSubPackages, seedSubPackages } from './feature-scan.js';
|
|
11
|
-
import { attachGraphExportRoutes } from './graph-export.js';
|
|
6
|
+
import { WorkspaceRuntime } from './workspace-runtime.js';
|
|
12
7
|
import { attachWorkspaceRoutes } from './workspace-routes.js';
|
|
13
|
-
import { attachGraphLayoutRoutes } from './graph-layout-routes.js';
|
|
14
8
|
import { createWorkspaceMiddleware } from './workspace-middleware.js';
|
|
15
|
-
import { getTargetRoot, getDataDir, ensureDataDir, loadProjectConfig,
|
|
9
|
+
import { getTargetRoot, getDataDir, ensureDataDir, loadProjectConfig, withProjectContext, COMPOSE_HOME } from './project-root.js';
|
|
16
10
|
import { resolveStratumEngine } from './stratum-client.js';
|
|
17
11
|
import { probeStratumBin, resolveStratumBin } from '../lib/stratum-engine.js';
|
|
18
12
|
import { createAuthStore } from './auth-store.js';
|
|
@@ -64,21 +58,44 @@ if (projectConfig.capabilities.stratum) {
|
|
|
64
58
|
}
|
|
65
59
|
|
|
66
60
|
// Handle unexpected errors — fatal startup errors exit (supervisor retries),
|
|
67
|
-
// runtime errors keep the process alive to preserve PTY sessions
|
|
61
|
+
// runtime errors keep the process alive to preserve PTY sessions.
|
|
62
|
+
//
|
|
63
|
+
// stdio death is fatal by design: when the parent process dies, stdout/stderr
|
|
64
|
+
// become dead sockets and every console.* write throws EPIPE. Logging from the
|
|
65
|
+
// uncaughtException handler then throws again, which re-enters the handler via
|
|
66
|
+
// the stream's error emission — an infinite setImmediate loop pinning a core
|
|
67
|
+
// (observed 2026-09-01: 2 days at 100% CPU as an orphan). So: stream errors on
|
|
68
|
+
// stdio exit immediately, and every log in an error/signal path is guarded.
|
|
69
|
+
const isDeadStdio = (err) =>
|
|
70
|
+
err && (err.code === 'EPIPE' || err.code === 'ERR_STREAM_DESTROYED' || err.code === 'ERR_STREAM_WRITE_AFTER_END');
|
|
71
|
+
const safeLog = (...args) => {
|
|
72
|
+
try {
|
|
73
|
+
console.error(...args);
|
|
74
|
+
} catch {
|
|
75
|
+
process.exit(0); // stdio is gone — parent died, nothing left to serve logs to
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
process.stdout.on('error', (err) => {
|
|
79
|
+
if (isDeadStdio(err)) process.exit(0);
|
|
80
|
+
});
|
|
81
|
+
process.stderr.on('error', (err) => {
|
|
82
|
+
if (isDeadStdio(err)) process.exit(0);
|
|
83
|
+
});
|
|
68
84
|
let serverListening = false;
|
|
69
85
|
process.on('uncaughtException', (err) => {
|
|
86
|
+
if (isDeadStdio(err)) process.exit(0);
|
|
70
87
|
if (!serverListening && err.code === 'EADDRINUSE') {
|
|
71
|
-
|
|
88
|
+
safeLog(`[compose] Port in use, exiting for supervisor retry: ${err.message}`);
|
|
72
89
|
process.exit(1);
|
|
73
90
|
}
|
|
74
|
-
|
|
75
|
-
|
|
91
|
+
safeLog('[compose] Uncaught exception (process kept alive):', err.message);
|
|
92
|
+
safeLog(err.stack);
|
|
76
93
|
});
|
|
77
94
|
process.on('unhandledRejection', (reason) => {
|
|
78
|
-
|
|
95
|
+
safeLog('[compose] Unhandled rejection (process kept alive):', reason);
|
|
79
96
|
});
|
|
80
97
|
process.on('SIGTERM', () => {
|
|
81
|
-
|
|
98
|
+
safeLog('[compose] SIGTERM received, shutting down gracefully');
|
|
82
99
|
process.exit(0);
|
|
83
100
|
});
|
|
84
101
|
|
|
@@ -141,23 +158,21 @@ attachAuthRoutes(app, {
|
|
|
141
158
|
} catch { return null; }
|
|
142
159
|
},
|
|
143
160
|
broadcast: (msg) => {
|
|
144
|
-
if (typeof visionServer?.broadcastMessage === 'function') {
|
|
145
|
-
|
|
161
|
+
if (typeof workspaces?.active?.visionServer?.broadcastMessage === 'function') {
|
|
162
|
+
activeVision().broadcastMessage(msg);
|
|
146
163
|
}
|
|
147
164
|
},
|
|
148
165
|
requireSensitive: requireSensitiveToken,
|
|
149
166
|
});
|
|
150
167
|
|
|
151
168
|
// ---------------------------------------------------------------------------
|
|
152
|
-
//
|
|
153
|
-
//
|
|
169
|
+
// Each workspace installs its agent proxy after workspace resolution. The
|
|
170
|
+
// proxy stamps the selected root and sensitive token for the SDK process.
|
|
154
171
|
// ---------------------------------------------------------------------------
|
|
155
172
|
const _agentPort = parseInt(process.env.AGENT_PORT || '4002', 10);
|
|
156
|
-
attachAgentProxy(app, { agentPort: _agentPort });
|
|
157
173
|
|
|
158
174
|
attachWorkspaceRoutes(app);
|
|
159
|
-
|
|
160
|
-
app.use(createWorkspaceMiddleware());
|
|
175
|
+
app.use(createWorkspaceMiddleware({ resolveKnownWorkspace: (id) => workspaces.resolveKnownWorkspace(id) }));
|
|
161
176
|
|
|
162
177
|
// `remote` lets clients (desktop cockpit served through a tunnel) detect
|
|
163
178
|
// remote mode at boot and switch their WS/SSE URLs to token-carrying form.
|
|
@@ -174,81 +189,27 @@ app.get('/api/project', (_req, res) => {
|
|
|
174
189
|
});
|
|
175
190
|
});
|
|
176
191
|
|
|
192
|
+
const server = http.createServer(app);
|
|
193
|
+
const workspaces = new WorkspaceRuntime(server, { agentPort: _agentPort });
|
|
194
|
+
try { workspaces.switch(getTargetRoot(), projectConfig); }
|
|
195
|
+
catch (err) {
|
|
196
|
+
console.error(`[compose] Workspace startup failed: ${err.message}`);
|
|
197
|
+
process.exit(1);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Accessors always select the current context; old requests retain their own router.
|
|
201
|
+
const activeVision = () => workspaces.active.visionServer;
|
|
177
202
|
app.post('/api/project/switch', (req, res) => {
|
|
178
203
|
const { path: projectPath } = req.body || {};
|
|
179
|
-
if (!projectPath) return res.status(400).json({ error: 'path is required' });
|
|
204
|
+
if (typeof projectPath !== 'string' || !projectPath) return res.status(400).json({ error: 'path is required' });
|
|
180
205
|
try {
|
|
181
|
-
const
|
|
182
|
-
|
|
183
|
-
// Reload store from new data directory
|
|
184
|
-
visionStore.reloadFrom(result.dataDir);
|
|
185
|
-
// Re-scan features and sub-packages from new project
|
|
186
|
-
try {
|
|
187
|
-
const features = scanFeatures();
|
|
188
|
-
if (features.length > 0) seedFeatures(features, visionStore);
|
|
189
|
-
const packages = scanSubPackages();
|
|
190
|
-
if (packages.length > 0) seedSubPackages(packages, visionStore);
|
|
191
|
-
} catch (err) {
|
|
192
|
-
console.error('[compose] Feature scan after switch:', err.message);
|
|
193
|
-
}
|
|
194
|
-
// Broadcast new state to all connected clients
|
|
195
|
-
visionServer.scheduleBroadcast();
|
|
196
|
-
res.json({ ok: true, targetRoot: result.targetRoot, name: path.basename(result.targetRoot) });
|
|
206
|
+
const { binding } = workspaces.switch(projectPath);
|
|
207
|
+
res.json({ ok: true, targetRoot: binding.targetRoot, name: path.basename(binding.targetRoot) });
|
|
197
208
|
} catch (err) {
|
|
198
209
|
res.status(400).json({ error: err.message });
|
|
199
210
|
}
|
|
200
211
|
});
|
|
201
|
-
|
|
202
|
-
const server = http.createServer(app);
|
|
203
|
-
const fileWatcher = new FileWatcherServer();
|
|
204
|
-
fileWatcher.attach(server, app);
|
|
205
|
-
|
|
206
|
-
const visionStore = new VisionStore(getDataDir());
|
|
207
|
-
const sessionManager = new SessionManager({
|
|
208
|
-
getFeaturePhase: (featureCode) => {
|
|
209
|
-
const item = visionStore.getItemByFeatureCode(featureCode);
|
|
210
|
-
return item?.lifecycle?.currentPhase || null;
|
|
211
|
-
},
|
|
212
|
-
featureRoot: resolveProjectPath('features'),
|
|
213
|
-
});
|
|
214
|
-
const visionServer = new VisionServer(visionStore, sessionManager, { config: projectConfig });
|
|
215
|
-
visionServer.attach(server, app);
|
|
216
|
-
|
|
217
|
-
// Seed feature folders and sub-packages into vision store on startup
|
|
218
|
-
try {
|
|
219
|
-
const features = scanFeatures();
|
|
220
|
-
if (features.length > 0) seedFeatures(features, visionStore);
|
|
221
|
-
const packages = scanSubPackages();
|
|
222
|
-
if (packages.length > 0) seedSubPackages(packages, visionStore);
|
|
223
|
-
} catch (err) {
|
|
224
|
-
console.error('[compose] Feature scan startup error:', err.message);
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
// Wire feature folder changes → auto-reseed vision store
|
|
228
|
-
fileWatcher.onFeatureChanged = (_relativePath) => {
|
|
229
|
-
try {
|
|
230
|
-
const features = scanFeatures();
|
|
231
|
-
seedFeatures(features, visionStore);
|
|
232
|
-
visionServer.scheduleBroadcast();
|
|
233
|
-
} catch (err) {
|
|
234
|
-
console.error('[compose] Feature reseed error:', err.message);
|
|
235
|
-
}
|
|
236
|
-
};
|
|
237
|
-
|
|
238
|
-
// Wire build state changes → broadcast over /ws/vision
|
|
239
|
-
fileWatcher.onBuildStateChanged = (state) => {
|
|
240
|
-
if (state) {
|
|
241
|
-
// Broadcast flat payload per STRAT-COMP-4 contract
|
|
242
|
-
visionServer.broadcastMessage({ type: 'buildState', ...state });
|
|
243
|
-
}
|
|
244
|
-
};
|
|
245
|
-
|
|
246
|
-
// Wire pipelines/*.stratum.yaml external changes → broadcast `specChanged` on
|
|
247
|
-
// the VISION WS (COMP-PIPE-EDIT-6 — the channel the pipeline editor store uses,
|
|
248
|
-
// not /ws/files). The message already carries { type:'specChanged', file, path }.
|
|
249
|
-
fileWatcher.onSpecChanged = (message) => {
|
|
250
|
-
visionServer.broadcastMessage(message);
|
|
251
|
-
};
|
|
212
|
+
app.use((req, res, next) => workspaces.handle(req, res, next));
|
|
252
213
|
|
|
253
214
|
// ---------------------------------------------------------------------------
|
|
254
215
|
// Static serving + SPA fallback (BOTH modes — additive)
|
|
@@ -282,18 +243,27 @@ server.on('upgrade', (req, socket, head) => {
|
|
|
282
243
|
return;
|
|
283
244
|
}
|
|
284
245
|
|
|
285
|
-
const
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
246
|
+
const url = new URL(req.url, 'http://localhost');
|
|
247
|
+
const workspaceId = req.headers['x-compose-workspace-id'] || url.searchParams.get('workspaceId');
|
|
248
|
+
let context = workspaces.active;
|
|
249
|
+
if (workspaceId) {
|
|
250
|
+
try {
|
|
251
|
+
const resolved = workspaces.resolveKnownWorkspace(workspaceId);
|
|
252
|
+
context = resolved && workspaces.get(resolved.root);
|
|
253
|
+
} catch { context = null; }
|
|
254
|
+
if (!context) {
|
|
255
|
+
socket.write('HTTP/1.1 400 Bad Request\r\n\r\n');
|
|
256
|
+
socket.destroy();
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
296
259
|
}
|
|
260
|
+
const { fileWatcher, visionServer } = context;
|
|
261
|
+
const wss = url.pathname === '/ws/files' ? fileWatcher.wss
|
|
262
|
+
: url.pathname === '/ws/vision' ? visionServer.wss : null;
|
|
263
|
+
if (!wss) { socket.destroy(); return; }
|
|
264
|
+
withProjectContext(context.binding, () => {
|
|
265
|
+
wss.handleUpgrade(req, socket, head, (ws) => wss.emit('connection', ws, req));
|
|
266
|
+
});
|
|
297
267
|
});
|
|
298
268
|
|
|
299
269
|
server.listen(PORT, _host, () => {
|
|
@@ -14,12 +14,20 @@
|
|
|
14
14
|
|
|
15
15
|
import { createHash } from 'node:crypto';
|
|
16
16
|
import { readFileSync } from 'node:fs';
|
|
17
|
-
import { spawnSync } from 'node:child_process';
|
|
17
|
+
import { execFile, spawnSync } from 'node:child_process';
|
|
18
18
|
import path from 'node:path';
|
|
19
|
+
import { promisify } from 'node:util';
|
|
20
|
+
|
|
21
|
+
import { descriptorIdFor, deriveBackfillPolicy, ensureSignedDescriptors } from '../lib/guard-descriptors.js';
|
|
22
|
+
import { acquireDirLock } from '../lib/dir-lock.js';
|
|
23
|
+
import { custodySign, DESCRIPTOR_LOCK_TIMEOUT_MS, HINT_APPROVE } from '../lib/guard-custody.js';
|
|
19
24
|
|
|
20
25
|
import {
|
|
21
26
|
guardRegister as _guardRegister,
|
|
22
27
|
guardTransition as _guardTransition,
|
|
28
|
+
guardPolicy as _guardPolicy,
|
|
29
|
+
guardApplyUpgrade as _guardApplyUpgrade,
|
|
30
|
+
guardDescriptors as _guardDescriptors,
|
|
23
31
|
} from './stratum-client.js';
|
|
24
32
|
import { setFeatureStatus as _setFeatureStatus } from '../lib/feature-writer.js';
|
|
25
33
|
import {
|
|
@@ -31,6 +39,8 @@ import {
|
|
|
31
39
|
edgeEvidenceOf,
|
|
32
40
|
} from '../lib/lifecycle-modes.js';
|
|
33
41
|
|
|
42
|
+
const execFileAsync = promisify(execFile);
|
|
43
|
+
|
|
34
44
|
// ---------------------------------------------------------------------------
|
|
35
45
|
// Canonical phase graph (compose-owned data — single source of truth)
|
|
36
46
|
// ---------------------------------------------------------------------------
|
|
@@ -53,7 +63,12 @@ export const BASE_TRANSITIONS = {
|
|
|
53
63
|
export const SKIPPABLE = new Set(['prd', 'architecture', 'report']);
|
|
54
64
|
|
|
55
65
|
/** Terminal phases — no outgoing edges. */
|
|
56
|
-
export const TERMINAL = new Set(['complete', 'killed']);
|
|
66
|
+
export const TERMINAL = new Set(['complete', 'killed', 'complete_backfilled']);
|
|
67
|
+
|
|
68
|
+
/** Normalise raw stratum and local adapter error envelopes at the boundary. */
|
|
69
|
+
export const isGuardError = (result) => !result || Boolean(result.error) || result.status === 'error';
|
|
70
|
+
export const guardErrorType = (result) => (result && (result.error?.code ?? result.error_type)) ?? null;
|
|
71
|
+
export const guardErrorMessage = (result) => (result && (result.error?.message ?? result.message)) ?? 'no guard response';
|
|
57
72
|
|
|
58
73
|
/**
|
|
59
74
|
* Assemble the FULL guarded graph the design requires: the forward
|
|
@@ -77,6 +92,13 @@ export function buildPhaseGraph(mode = 'build') {
|
|
|
77
92
|
// separately in vision-routes at /lifecycle/complete, so absent from the mode's
|
|
78
93
|
// forward transitions). For build this is `ship → complete`.
|
|
79
94
|
graph[completable] = [...(graph[completable] || []), 'complete'];
|
|
95
|
+
// Every non-terminal phase → complete_backfilled. This must precede the
|
|
96
|
+
// killed loop: stratum fingerprints adjacency arrays in their stored order.
|
|
97
|
+
for (const s of nodes) {
|
|
98
|
+
if (terminal.has(s)) continue;
|
|
99
|
+
graph[s] = graph[s] || [];
|
|
100
|
+
if (!graph[s].includes('complete_backfilled')) graph[s].push('complete_backfilled');
|
|
101
|
+
}
|
|
80
102
|
// Every non-terminal phase → killed (vision-routes /lifecycle/kill allows
|
|
81
103
|
// kill from any non-terminal phase, including the completable phase).
|
|
82
104
|
for (const s of nodes) {
|
|
@@ -141,6 +163,7 @@ export function resourceId(featureCode, workspaceRoot, mode = 'build') {
|
|
|
141
163
|
* and stay set_feature_status's domain.
|
|
142
164
|
*/
|
|
143
165
|
export function phaseToStatus(phase) {
|
|
166
|
+
if (phase === 'complete_backfilled') return 'COMPLETE';
|
|
144
167
|
if (phase === 'complete') return 'COMPLETE';
|
|
145
168
|
if (phase === 'killed') return 'KILLED';
|
|
146
169
|
return 'IN_PROGRESS';
|
|
@@ -223,20 +246,110 @@ export async function verifyCompletionEvidence({ commitSha, cwd, testCommand, te
|
|
|
223
246
|
return { ok: reasons.length === 0, reasons, testsAttested };
|
|
224
247
|
}
|
|
225
248
|
|
|
249
|
+
/**
|
|
250
|
+
* Async equivalent for backfill completion while a directory lock heartbeat is
|
|
251
|
+
* active. The synchronous live-completion verifier above remains unchanged.
|
|
252
|
+
*/
|
|
253
|
+
export async function verifyCompletionEvidenceAsync({ commitSha, cwd, testCommand, testsPassClaim }) {
|
|
254
|
+
const reasons = [];
|
|
255
|
+
|
|
256
|
+
if (!commitSha || typeof commitSha !== 'string' || !commitSha.trim()) {
|
|
257
|
+
reasons.push('commit_sha is required for evidence-bound completion');
|
|
258
|
+
} else {
|
|
259
|
+
try {
|
|
260
|
+
await execFileAsync('git', ['rev-parse', '--verify', '--quiet', `${commitSha.trim()}^{commit}`], { cwd, encoding: 'utf8' });
|
|
261
|
+
} catch {
|
|
262
|
+
reasons.push(`commit ${commitSha.trim()} not found in repository (server-read git verification)`);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
let testsAttested = false;
|
|
267
|
+
if (Array.isArray(testCommand) && testCommand.length > 0) {
|
|
268
|
+
const [bin, ...rest] = testCommand;
|
|
269
|
+
try {
|
|
270
|
+
await execFileAsync(bin, rest, { cwd, encoding: 'utf8' });
|
|
271
|
+
testsAttested = true;
|
|
272
|
+
} catch (error) {
|
|
273
|
+
if (error?.code === 'ENOENT') reasons.push(`test command failed to run: ${error.message}`);
|
|
274
|
+
else reasons.push(`test command exited ${error?.code ?? 'null'} (not 0)`);
|
|
275
|
+
}
|
|
276
|
+
} else if (testsPassClaim !== true) {
|
|
277
|
+
reasons.push('tests_pass must be explicitly true (no configured test command to attest test results)');
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
return { ok: reasons.length === 0, reasons, testsAttested };
|
|
281
|
+
}
|
|
282
|
+
|
|
226
283
|
// ---------------------------------------------------------------------------
|
|
227
284
|
// Guard client (injectable for tests)
|
|
228
285
|
// ---------------------------------------------------------------------------
|
|
229
286
|
|
|
230
|
-
let _client = {
|
|
287
|
+
let _client = {
|
|
288
|
+
register: _guardRegister,
|
|
289
|
+
transition: _guardTransition,
|
|
290
|
+
policy: _guardPolicy,
|
|
291
|
+
applyUpgrade: _guardApplyUpgrade,
|
|
292
|
+
descriptors: _guardDescriptors,
|
|
293
|
+
};
|
|
231
294
|
/** @internal test seam */
|
|
232
295
|
export function _testOnly_setGuardClient(c) { _client = c; }
|
|
233
296
|
|
|
297
|
+
const _custody = { sign: custodySign };
|
|
298
|
+
let _ensureDescriptors = ensureSignedDescriptors;
|
|
299
|
+
/** @internal test seam: real enumeration bypasses the injectable guard client. */
|
|
300
|
+
export function _testOnly_setEnsureDescriptors(ensure) {
|
|
301
|
+
_ensureDescriptors = ensure ?? ensureSignedDescriptors;
|
|
302
|
+
}
|
|
303
|
+
|
|
234
304
|
// Per-process registration cache — register is idempotent server-side, but the
|
|
235
305
|
// cache avoids a subprocess per request once a resource is known-registered.
|
|
236
|
-
const _registered = new
|
|
306
|
+
const _registered = new Map();
|
|
237
307
|
/** @internal test seam */
|
|
238
308
|
export function _testOnly_resetGuardCache() { _registered.clear(); }
|
|
239
309
|
|
|
310
|
+
/** The four and only four fields covered by stratum's policy checksum. */
|
|
311
|
+
export function policyChecksumFields(policy) {
|
|
312
|
+
return {
|
|
313
|
+
graph: policy.graph,
|
|
314
|
+
edge_predicates: policy.edge_predicates,
|
|
315
|
+
terminal: policy.terminal,
|
|
316
|
+
stakes: policy.stakes,
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/** Reverse buildPhaseGraph's complete_backfilled additions for legacy comparison. */
|
|
321
|
+
export function legacyPolicyProjection(policy) {
|
|
322
|
+
const fields = policyChecksumFields(policy);
|
|
323
|
+
const graph = {};
|
|
324
|
+
for (const [phase, targets] of Object.entries(fields.graph || {})) {
|
|
325
|
+
if (phase === 'complete_backfilled') continue;
|
|
326
|
+
graph[phase] = (targets || []).filter((target) => target !== 'complete_backfilled');
|
|
327
|
+
}
|
|
328
|
+
return {
|
|
329
|
+
graph,
|
|
330
|
+
edge_predicates: fields.edge_predicates,
|
|
331
|
+
terminal: (fields.terminal || []).filter((phase) => phase !== 'complete_backfilled'),
|
|
332
|
+
stakes: fields.stakes,
|
|
333
|
+
};
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
function _sortPolicyObject(value) {
|
|
337
|
+
if (Array.isArray(value)) return value.map(_sortPolicyObject);
|
|
338
|
+
if (value && typeof value === 'object') {
|
|
339
|
+
return Object.fromEntries(Object.keys(value).sort().map((key) => [key, _sortPolicyObject(value[key])]));
|
|
340
|
+
}
|
|
341
|
+
return value;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/** Object key order is irrelevant; terminal order is not; adjacency order is. */
|
|
345
|
+
export function policiesEqual(a, b) {
|
|
346
|
+
const normalise = (policy) => {
|
|
347
|
+
const fields = policyChecksumFields(policy);
|
|
348
|
+
return _sortPolicyObject({ ...fields, terminal: [...(fields.terminal || [])].sort() });
|
|
349
|
+
};
|
|
350
|
+
return JSON.stringify(normalise(a)) === JSON.stringify(normalise(b));
|
|
351
|
+
}
|
|
352
|
+
|
|
240
353
|
/**
|
|
241
354
|
* Resolve the feature directory RELATIVE to the served `workspaceRoot` — read
|
|
242
355
|
* from `<workspaceRoot>/.compose/compose.json` (NOT the process-global config,
|
|
@@ -298,11 +411,13 @@ export function guardTestCommand(workspaceRoot) {
|
|
|
298
411
|
*/
|
|
299
412
|
export async function ensureGuard(featureCode, currentPhase, workspaceRoot, mode = 'build') {
|
|
300
413
|
const rid = resourceId(featureCode, workspaceRoot, mode);
|
|
301
|
-
if (_registered.has(rid))
|
|
414
|
+
if (_registered.has(rid)) {
|
|
415
|
+
return { guard_id: rid, status: _registered.get(rid) === 'legacy' ? 'legacy' : 'cached' };
|
|
416
|
+
}
|
|
302
417
|
|
|
303
418
|
let res;
|
|
304
419
|
try {
|
|
305
|
-
|
|
420
|
+
const newPolicy = {
|
|
306
421
|
resourceId: rid,
|
|
307
422
|
graph: buildPhaseGraph(mode),
|
|
308
423
|
edgePredicates: edgePredicates(_featureRelDir(featureCode, workspaceRoot, mode), mode),
|
|
@@ -310,18 +425,123 @@ export async function ensureGuard(featureCode, currentPhase, workspaceRoot, mode
|
|
|
310
425
|
terminal: terminalOf(mode),
|
|
311
426
|
stakes: {},
|
|
312
427
|
workspaceRoot,
|
|
313
|
-
}
|
|
428
|
+
};
|
|
429
|
+
res = await _client.register(newPolicy);
|
|
430
|
+
if (guardErrorType(res) === 'guard_already_registered') {
|
|
431
|
+
const stored = await _client.policy(rid);
|
|
432
|
+
if (isGuardError(stored)) {
|
|
433
|
+
return { error: { code: guardErrorType(stored), message: guardErrorMessage(stored) } };
|
|
434
|
+
}
|
|
435
|
+
const projected = legacyPolicyProjection({
|
|
436
|
+
graph: newPolicy.graph,
|
|
437
|
+
edge_predicates: newPolicy.edgePredicates,
|
|
438
|
+
terminal: newPolicy.terminal,
|
|
439
|
+
stakes: newPolicy.stakes,
|
|
440
|
+
});
|
|
441
|
+
if (policiesEqual(projected, policyChecksumFields(stored))) {
|
|
442
|
+
_registered.set(rid, 'legacy');
|
|
443
|
+
return { guard_id: rid, status: 'legacy', storedChecksum: stored.checksum };
|
|
444
|
+
}
|
|
445
|
+
return {
|
|
446
|
+
error: {
|
|
447
|
+
code: 'GUARD_POLICY_DIVERGED',
|
|
448
|
+
message: `stored guard policy for ${rid} differs from the expected legacy policy`,
|
|
449
|
+
},
|
|
450
|
+
};
|
|
451
|
+
}
|
|
314
452
|
} catch (e) {
|
|
315
453
|
// A thrown Stratum CLI failure must NOT escape as
|
|
316
454
|
// a generic 500/400 — normalise to a fail-closed error result.
|
|
317
455
|
return { error: { code: 'GUARD_UNREACHABLE', message: e.message } };
|
|
318
456
|
}
|
|
319
457
|
if (res && (res.status === 'registered' || res.status === 'exists')) {
|
|
320
|
-
_registered.
|
|
458
|
+
_registered.set(rid, 'cached');
|
|
321
459
|
}
|
|
322
460
|
return res;
|
|
323
461
|
}
|
|
324
462
|
|
|
463
|
+
/** Lazily apply the signed legacy-policy upgrade required for backfilled completion. */
|
|
464
|
+
export async function applyBackfillUpgrade({ featureCode, workspaceRoot, mode = 'build' }) {
|
|
465
|
+
const rid = resourceId(featureCode, workspaceRoot, mode);
|
|
466
|
+
let stored;
|
|
467
|
+
try {
|
|
468
|
+
stored = await _client.policy(rid);
|
|
469
|
+
} catch (error) {
|
|
470
|
+
const message = error?.message || String(error);
|
|
471
|
+
return { ok: false, reasons: ['could not read stored guard policy'], error: { code: 'GUARD_UNREACHABLE', message, hint: HINT_APPROVE } };
|
|
472
|
+
}
|
|
473
|
+
if (isGuardError(stored)) {
|
|
474
|
+
const message = guardErrorMessage(stored);
|
|
475
|
+
return { ok: false, reasons: ['could not read stored guard policy'], error: { code: guardErrorType(stored) ?? 'upgrade_descriptor_unavailable', message, hint: HINT_APPROVE } };
|
|
476
|
+
}
|
|
477
|
+
const expected = deriveBackfillPolicy(policyChecksumFields(stored), mode);
|
|
478
|
+
if (policiesEqual(expected, policyChecksumFields(stored))) {
|
|
479
|
+
_registered.set(rid, 'cached');
|
|
480
|
+
return { ok: true, status: 'unchanged', checksum: stored.checksum };
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
let release;
|
|
484
|
+
try {
|
|
485
|
+
release = await acquireDirLock(
|
|
486
|
+
path.join(workspaceRoot, '.compose', 'data', 'locks', 'guard-descriptors'),
|
|
487
|
+
{ timeoutMs: DESCRIPTOR_LOCK_TIMEOUT_MS },
|
|
488
|
+
);
|
|
489
|
+
} catch (error) {
|
|
490
|
+
const message = error?.message || String(error);
|
|
491
|
+
return { ok: false, reasons: [message], error: { code: 'upgrade_descriptor_unavailable', message, hint: HINT_APPROVE } };
|
|
492
|
+
}
|
|
493
|
+
try {
|
|
494
|
+
const ensured = await _ensureDescriptors({
|
|
495
|
+
workspaceRoot,
|
|
496
|
+
needChecksums: [stored.checksum],
|
|
497
|
+
custody: _custody,
|
|
498
|
+
verifier: _client.descriptors,
|
|
499
|
+
});
|
|
500
|
+
if (ensured.status === 'refused') {
|
|
501
|
+
return {
|
|
502
|
+
ok: false,
|
|
503
|
+
reasons: [ensured.message],
|
|
504
|
+
error: { code: ensured.code ?? 'upgrade_descriptor_unavailable', message: ensured.message, hint: ensured.hint ?? HINT_APPROVE },
|
|
505
|
+
};
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
let applied;
|
|
509
|
+
try {
|
|
510
|
+
applied = await _client.applyUpgrade({
|
|
511
|
+
resourceId: rid,
|
|
512
|
+
descriptorId: descriptorIdFor(stored.checksum, mode),
|
|
513
|
+
descriptorsPath: ensured.path,
|
|
514
|
+
});
|
|
515
|
+
} catch (error) {
|
|
516
|
+
const message = error?.message || String(error);
|
|
517
|
+
return { ok: false, reasons: ['guard upgrade failed'], error: { code: 'GUARD_UNREACHABLE', message, hint: HINT_APPROVE } };
|
|
518
|
+
}
|
|
519
|
+
if (isGuardError(applied)) {
|
|
520
|
+
const message = guardErrorMessage(applied);
|
|
521
|
+
return {
|
|
522
|
+
ok: false,
|
|
523
|
+
reasons: ['guard upgrade failed'],
|
|
524
|
+
error: { code: guardErrorType(applied) ?? 'upgrade_descriptor_unavailable', message, hint: HINT_APPROVE },
|
|
525
|
+
};
|
|
526
|
+
}
|
|
527
|
+
if (applied.status !== 'applied' && applied.status !== 'unchanged') {
|
|
528
|
+
const message = 'guard upgrade returned an unexpected result';
|
|
529
|
+
return { ok: false, reasons: [message], error: { code: 'upgrade_descriptor_unavailable', message, hint: HINT_APPROVE } };
|
|
530
|
+
}
|
|
531
|
+
_registered.set(rid, 'cached');
|
|
532
|
+
return {
|
|
533
|
+
ok: true,
|
|
534
|
+
status: applied.status,
|
|
535
|
+
ledgerRef: applied.ledger_ref,
|
|
536
|
+
checksum: applied.checksum,
|
|
537
|
+
descriptorId: descriptorIdFor(stored.checksum, mode),
|
|
538
|
+
generation: ensured.sha,
|
|
539
|
+
};
|
|
540
|
+
} finally {
|
|
541
|
+
await release();
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
|
|
325
545
|
/**
|
|
326
546
|
* Attempt a guarded lifecycle transition. Registers (idempotently) then
|
|
327
547
|
* transitions. FAIL-CLOSED: any guard error (unreachable, illegal edge, error
|
|
@@ -330,14 +550,22 @@ export async function ensureGuard(featureCode, currentPhase, workspaceRoot, mode
|
|
|
330
550
|
* @returns {Promise<{applied:boolean, refused?:boolean, verdict?:object,
|
|
331
551
|
* ledgerRef?:string, currentState?:string, error?:object}>}
|
|
332
552
|
*/
|
|
333
|
-
export async function guardedTransition({ featureCode, from, to, workspaceRoot, commitSha, resolvedBy = 'agent', mode = 'build' }) {
|
|
553
|
+
export async function guardedTransition({ featureCode, from, to, workspaceRoot, commitSha, resolvedBy = 'agent', mode = 'build', artifacts: extraArtifacts, idempotencyKey, expectedPolicyChecksum }) {
|
|
334
554
|
const reg = await ensureGuard(featureCode, from, workspaceRoot, mode);
|
|
335
555
|
if (reg && (reg.error || reg.status === 'error')) {
|
|
336
556
|
return { applied: false, error: reg.error || reg };
|
|
337
557
|
}
|
|
338
558
|
|
|
339
559
|
const rid = resourceId(featureCode, workspaceRoot, mode);
|
|
340
|
-
|
|
560
|
+
// COMP-COMPLETION-GATE: callers may add artifacts (e.g. the completion gate's
|
|
561
|
+
// `operation_id`). Artifacts feed the ledger's payload_digest, which is the
|
|
562
|
+
// only way two commit-less transitions on one resource are distinguishable —
|
|
563
|
+
// without it, every null-SHA completion hashes identically and a crash-recovery
|
|
564
|
+
// check cannot tell one operation from another.
|
|
565
|
+
const artifacts = {
|
|
566
|
+
...(commitSha ? { commit_sha: commitSha } : {}),
|
|
567
|
+
...(extraArtifacts || {}),
|
|
568
|
+
};
|
|
341
569
|
// No idempotency_key: a refuse→fix→retry is a NEW logical attempt that must
|
|
342
570
|
// re-evaluate evidence, but it carries an identical (from,to,artifacts)
|
|
343
571
|
// payload — an idempotency_key would make the guard replay the prior refusal.
|
|
@@ -350,6 +578,8 @@ export async function guardedTransition({ featureCode, from, to, workspaceRoot,
|
|
|
350
578
|
fromState: from,
|
|
351
579
|
toState: to,
|
|
352
580
|
artifacts,
|
|
581
|
+
idempotencyKey,
|
|
582
|
+
expectedPolicyChecksum,
|
|
353
583
|
resolvedBy,
|
|
354
584
|
});
|
|
355
585
|
} catch (e) {
|