@smartmemory/compose 0.3.7 → 0.4.0
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 +12 -3
- package/.compose-deps.json +51 -25
- package/README.md +79 -7
- package/bin/compose.js +495 -360
- 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/deps.js +164 -7
- 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/vision-routes.js
CHANGED
|
@@ -44,6 +44,7 @@ function getSchemaValidator() {
|
|
|
44
44
|
import { randomUUID } from 'node:crypto';
|
|
45
45
|
import { getTargetRoot, resolveProjectPath } from './project-root.js';
|
|
46
46
|
import { resolveFeaturesPathFromConfig } from '../lib/project-paths.js';
|
|
47
|
+
import { isManagedBuildItem, applyVerifiedProjection } from './completion-projection.js';
|
|
47
48
|
import { getCompletions } from '../lib/completion-writer.js';
|
|
48
49
|
import { isFeatureCode } from '../lib/feature-code.js';
|
|
49
50
|
import { anchorBoundary } from '../lib/checkpoint/checkpoint-writer.js';
|
|
@@ -59,7 +60,6 @@ import {
|
|
|
59
60
|
} from '../lib/lifecycle-modes.js';
|
|
60
61
|
import { requireSensitiveOrPaired as requireSensitiveToken } from './security.js';
|
|
61
62
|
|
|
62
|
-
const PROJECT_ROOT = getTargetRoot();
|
|
63
63
|
|
|
64
64
|
/**
|
|
65
65
|
* Read the compose.json config for an ARBITRARY workspace root (not the bound
|
|
@@ -81,10 +81,10 @@ function loadComposeConfig(root) {
|
|
|
81
81
|
* @param {object} app — Express app
|
|
82
82
|
* @param {{ store: object, scheduleBroadcast: function, broadcastMessage: function, projectRoot: string, settingsStore?: object, capabilities?: object }} deps
|
|
83
83
|
*/
|
|
84
|
-
export function attachVisionRoutes(app, { store, scheduleBroadcast, broadcastMessage, projectRoot =
|
|
84
|
+
export function attachVisionRoutes(app, { store, scheduleBroadcast, broadcastMessage, projectRoot = getTargetRoot(), settingsStore, capabilities }) {
|
|
85
85
|
// COMP-MCP-ENFORCE: when enabled, lifecycle transitions are verdict-gated by
|
|
86
86
|
// stratum's STRAT-GUARD (fail-closed). Default OFF — legacy behavior intact.
|
|
87
|
-
const guardEnabled = capabilities?.guard === true;
|
|
87
|
+
const guardEnabled = () => capabilities?.guard === true;
|
|
88
88
|
|
|
89
89
|
// COMP-MCP-ENFORCE Slice 4: opt-in loopback REST auth on vision MUTATION
|
|
90
90
|
// endpoints (lifecycle transitions, iterations, gate resolve, item CRUD,
|
|
@@ -95,9 +95,8 @@ export function attachVisionRoutes(app, { store, scheduleBroadcast, broadcastMes
|
|
|
95
95
|
// returns 503 (mutations disabled) rather than silently allowing them — enabling
|
|
96
96
|
// auth without a token is a misconfiguration, not an open door. Pass-through
|
|
97
97
|
// when off, so route wiring is unconditional.
|
|
98
|
-
const guardAuthEnabled = capabilities?.guardAuth === true;
|
|
99
98
|
const guardAuth = (req, res, next) =>
|
|
100
|
-
|
|
99
|
+
capabilities?.guardAuth === true ? requireSensitiveToken(req, res, next) : next();
|
|
101
100
|
|
|
102
101
|
// PARITY-5: GET /api/completions — read-only recorded-completion log for a
|
|
103
102
|
// feature. Reads are open (not guardAuth-wrapped); wraps the same
|
|
@@ -148,6 +147,34 @@ export function attachVisionRoutes(app, { store, scheduleBroadcast, broadcastMes
|
|
|
148
147
|
// PATCH /api/vision/items/:id — update item
|
|
149
148
|
app.patch('/api/vision/items/:id', guardAuth, (req, res) => {
|
|
150
149
|
try {
|
|
150
|
+
// COMP-COMPLETION-GATE slice 3 (AC-10, path 7): `status: 'complete'` on a
|
|
151
|
+
// MANAGED BUILD-MODE feature item is refused here. The general PATCH has
|
|
152
|
+
// no evidence to offer; the legitimate write is the self-verifying
|
|
153
|
+
// projection endpoint below, which the gate reaches after the guard has
|
|
154
|
+
// applied. Fix/plan items and unmanaged items are untouched (§2.3d) —
|
|
155
|
+
// round 5 caught that an unconditional refusal bricks those modes
|
|
156
|
+
// whenever a server is running, since every mode shares the terminal call.
|
|
157
|
+
// The verification stamp is SERVER-OWNED: only the §2.3b predicate writes
|
|
158
|
+
// it. A PATCH carrying it could relabel a legacy item as `guarded` with a
|
|
159
|
+
// fake ledger ref (Codex r2 #1).
|
|
160
|
+
if (req.body && Object.prototype.hasOwnProperty.call(req.body, 'completion_projection')) {
|
|
161
|
+
return res.status(422).json({
|
|
162
|
+
error: 'completion_projection is written by the completion projection only and cannot be set through PATCH',
|
|
163
|
+
code: 'PROJECTION_STAMP_READONLY',
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
if (req.body?.status === 'complete') {
|
|
167
|
+
const existing = store.items.get(req.params.id);
|
|
168
|
+
if (existing && isManagedBuildItem(existing, projectRoot)) {
|
|
169
|
+
return res.status(422).json({
|
|
170
|
+
error: `refusing status: complete for ${existing.lifecycle.featureCode} — a managed build-mode ` +
|
|
171
|
+
'feature completes through the completion gate (record_completion / the build runner / ' +
|
|
172
|
+
'POST …/lifecycle/complete), which verifies evidence and projects the item via ' +
|
|
173
|
+
'POST /api/vision/items/:id/completion-projection.',
|
|
174
|
+
code: 'COMPLETE_VIA_GATE_ONLY',
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
}
|
|
151
178
|
const item = store.updateItem(req.params.id, req.body);
|
|
152
179
|
// If group changed, write back to docs/features/<code>/feature.json
|
|
153
180
|
// so the change survives restart and re-scan. Non-fatal on failure.
|
|
@@ -166,6 +193,31 @@ export function attachVisionRoutes(app, { store, scheduleBroadcast, broadcastMes
|
|
|
166
193
|
}
|
|
167
194
|
});
|
|
168
195
|
|
|
196
|
+
// POST /api/vision/items/:id/completion-projection — COMP-COMPLETION-GATE
|
|
197
|
+
// slice 3 (§2.3b, AC-4b/4d). Self-verifying, NOT privileged: before writing
|
|
198
|
+
// it re-reads feature.json (must already be COMPLETE — §2.3a step 2, the
|
|
199
|
+
// durable truth) and, if a guard resource exists, the guard ledger (must be
|
|
200
|
+
// complete). It cannot be used to complete anything; it only mirrors a
|
|
201
|
+
// completion canonical state already records — which is why it needs no
|
|
202
|
+
// token and does not reopen the bypass the PATCH refusal closes. Never calls
|
|
203
|
+
// the gate (no recursion: gate → here is one-directional).
|
|
204
|
+
app.post('/api/vision/items/:id/completion-projection', guardAuth, async (req, res) => {
|
|
205
|
+
try {
|
|
206
|
+
const { featureCode, commitSha, ledgerRef } = req.body || {};
|
|
207
|
+
if (!featureCode) return res.status(400).json({ error: 'featureCode required' });
|
|
208
|
+
if (!store.items.get(req.params.id)) return res.status(404).json({ error: `Item not found: ${req.params.id}` });
|
|
209
|
+
const r = await applyVerifiedProjection(store, {
|
|
210
|
+
itemId: req.params.id, featureCode, cwd: projectRoot, consultGuard: true,
|
|
211
|
+
evidence: { commitSha: commitSha || undefined, ledgerRef: ledgerRef || undefined, source: 'rest' },
|
|
212
|
+
});
|
|
213
|
+
if (!r.ok) return res.status(422).json({ error: 'completion projection refused', reasons: r.reasons });
|
|
214
|
+
scheduleBroadcast();
|
|
215
|
+
res.json({ ok: true, itemId: req.params.id, verified_by: r.verified_by });
|
|
216
|
+
} catch (err) {
|
|
217
|
+
res.status(500).json({ error: err.message });
|
|
218
|
+
}
|
|
219
|
+
});
|
|
220
|
+
|
|
169
221
|
// DELETE /api/vision/items/:id — delete item + connections
|
|
170
222
|
app.delete('/api/vision/items/:id', guardAuth, (req, res) => {
|
|
171
223
|
try {
|
|
@@ -213,9 +265,7 @@ export function attachVisionRoutes(app, { store, scheduleBroadcast, broadcastMes
|
|
|
213
265
|
});
|
|
214
266
|
|
|
215
267
|
// ── Lifecycle endpoints (simplified — no state machine) ──────────────
|
|
216
|
-
const featuresPath = projectRoot
|
|
217
|
-
? resolveFeaturesPathFromConfig(projectRoot, loadComposeConfig(projectRoot))
|
|
218
|
-
: resolveProjectPath('features');
|
|
268
|
+
const featuresPath = () => resolveFeaturesPathFromConfig(projectRoot, loadComposeConfig(projectRoot));
|
|
219
269
|
|
|
220
270
|
// Phase graph + skippable + terminal are owned by the mode registry (single
|
|
221
271
|
// source of truth shared with the STRAT-GUARD graph) — see COMP-MCP-ENFORCE /
|
|
@@ -283,7 +333,7 @@ export function attachVisionRoutes(app, { store, scheduleBroadcast, broadcastMes
|
|
|
283
333
|
// (the clean bootstrap path; backfill for pre-rollout items happens lazily
|
|
284
334
|
// on first transition). Best-effort — a registration hiccup must not block
|
|
285
335
|
// starting a lifecycle; the next guarded transition re-attempts ensureGuard.
|
|
286
|
-
if (guardEnabled) {
|
|
336
|
+
if (guardEnabled()) {
|
|
287
337
|
try { await ensureGuard(featureCode, genesis, projectRoot, mode); }
|
|
288
338
|
catch (e) { console.warn(`[lifecycle/start] guard register for ${featureCode} failed: ${e.message}`); }
|
|
289
339
|
// Slice 2: starting a lifecycle projects the genesis phase → IN_PROGRESS so
|
|
@@ -352,7 +402,7 @@ export function attachVisionRoutes(app, { store, scheduleBroadcast, broadcastMes
|
|
|
352
402
|
if (!valid?.includes(targetPhase)) return res.status(400).json({ error: `Invalid transition: ${from} → ${targetPhase}` });
|
|
353
403
|
|
|
354
404
|
// COMP-MCP-ENFORCE: verdict-gate the transition (fail-closed) before mutating.
|
|
355
|
-
if (guardEnabled) {
|
|
405
|
+
if (guardEnabled()) {
|
|
356
406
|
const g = await guardedTransition({ featureCode: item.lifecycle.featureCode, from, to: targetPhase, workspaceRoot: projectRoot, resolvedBy: 'agent', mode: modeOf(item) });
|
|
357
407
|
if (!g.applied) return res.status(422).json({ error: 'transition refused by guard', from, to: targetPhase, verdict: g.verdict, guardError: g.error });
|
|
358
408
|
}
|
|
@@ -365,7 +415,7 @@ export function attachVisionRoutes(app, { store, scheduleBroadcast, broadcastMes
|
|
|
365
415
|
// COMP-MCP-ENFORCE Slice 2 (lifecycle-as-truth): project the new phase onto
|
|
366
416
|
// feature.json STATUS (best-effort; idempotent — only writes on a real change).
|
|
367
417
|
// COMP-ROADMAP-PLAN: skipped for tracksFeatureJson:false modes (no feature.json).
|
|
368
|
-
if (guardEnabled && getMode(modeOf(item)).runner.tracksFeatureJson) await projectFeatureStatus({ featureCode: item.lifecycle.featureCode, phase: targetPhase, cwd: projectRoot });
|
|
418
|
+
if (guardEnabled() && getMode(modeOf(item)).runner.tracksFeatureJson) await projectFeatureStatus({ featureCode: item.lifecycle.featureCode, phase: targetPhase, cwd: projectRoot });
|
|
369
419
|
scheduleBroadcast();
|
|
370
420
|
broadcastMessage({ type: 'lifecycleTransition', itemId: req.params.id, from, to: targetPhase, outcome, timestamp: now });
|
|
371
421
|
emitDecisionEvent(broadcastMessage, buildPhaseTransitionEvent({ featureCode: item.lifecycle.featureCode, from, to: targetPhase, outcome, timestamp: now }));
|
|
@@ -394,7 +444,7 @@ export function attachVisionRoutes(app, { store, scheduleBroadcast, broadcastMes
|
|
|
394
444
|
if (!valid?.includes(targetPhase)) return res.status(400).json({ error: `Invalid transition: ${from} → ${targetPhase}` });
|
|
395
445
|
|
|
396
446
|
// COMP-MCP-ENFORCE: verdict-gate the skip (fail-closed) before mutating.
|
|
397
|
-
if (guardEnabled) {
|
|
447
|
+
if (guardEnabled()) {
|
|
398
448
|
const g = await guardedTransition({ featureCode: item.lifecycle.featureCode, from, to: targetPhase, workspaceRoot: projectRoot, resolvedBy: 'agent', mode: modeOf(item) });
|
|
399
449
|
if (!g.applied) return res.status(422).json({ error: 'transition refused by guard', from, to: targetPhase, verdict: g.verdict, guardError: g.error });
|
|
400
450
|
}
|
|
@@ -406,7 +456,7 @@ export function attachVisionRoutes(app, { store, scheduleBroadcast, broadcastMes
|
|
|
406
456
|
store.updateLifecycle(req.params.id, item.lifecycle);
|
|
407
457
|
// COMP-MCP-ENFORCE Slice 2 (lifecycle-as-truth): project phase → STATUS.
|
|
408
458
|
// COMP-ROADMAP-PLAN: skipped for tracksFeatureJson:false modes (no feature.json).
|
|
409
|
-
if (guardEnabled && getMode(modeOf(item)).runner.tracksFeatureJson) await projectFeatureStatus({ featureCode: item.lifecycle.featureCode, phase: targetPhase, cwd: projectRoot });
|
|
459
|
+
if (guardEnabled() && getMode(modeOf(item)).runner.tracksFeatureJson) await projectFeatureStatus({ featureCode: item.lifecycle.featureCode, phase: targetPhase, cwd: projectRoot });
|
|
410
460
|
scheduleBroadcast();
|
|
411
461
|
broadcastMessage({ type: 'lifecycleTransition', itemId: req.params.id, from, to: targetPhase, outcome: 'skipped', timestamp: now });
|
|
412
462
|
emitDecisionEvent(broadcastMessage, buildPhaseTransitionEvent({ featureCode: item.lifecycle.featureCode, from, to: targetPhase, outcome: 'skipped', timestamp: now }));
|
|
@@ -435,7 +485,7 @@ export function attachVisionRoutes(app, { store, scheduleBroadcast, broadcastMes
|
|
|
435
485
|
// The `<from>→killed` edge has no predicate, so it only fails if the guard
|
|
436
486
|
// is unreachable — consistent with advance/skip/complete. Authorized
|
|
437
487
|
// bypass remains stratum_guard_override (Slice 3).
|
|
438
|
-
if (guardEnabled) {
|
|
488
|
+
if (guardEnabled()) {
|
|
439
489
|
const g = await guardedTransition({ featureCode: item.lifecycle.featureCode, from, to: 'killed', workspaceRoot: projectRoot, resolvedBy: 'agent', mode: modeOf(item) });
|
|
440
490
|
if (!g.applied) return res.status(422).json({ error: 'kill refused by guard', from, to: 'killed', verdict: g.verdict, guardError: g.error });
|
|
441
491
|
}
|
|
@@ -451,7 +501,7 @@ export function attachVisionRoutes(app, { store, scheduleBroadcast, broadcastMes
|
|
|
451
501
|
// COMP-MCP-ENFORCE Slice 2: project kill → KILLED onto feature.json
|
|
452
502
|
// (closes the COMP-PARITY-7 gap — kill previously wrote vision-state only).
|
|
453
503
|
// COMP-ROADMAP-PLAN: skipped for tracksFeatureJson:false modes (no feature.json).
|
|
454
|
-
if (guardEnabled && getMode(modeOf(item)).runner.tracksFeatureJson) await projectFeatureStatus({ featureCode: item.lifecycle.featureCode, phase: 'killed', cwd: projectRoot });
|
|
504
|
+
if (guardEnabled() && getMode(modeOf(item)).runner.tracksFeatureJson) await projectFeatureStatus({ featureCode: item.lifecycle.featureCode, phase: 'killed', cwd: projectRoot });
|
|
455
505
|
scheduleBroadcast();
|
|
456
506
|
broadcastMessage({ type: 'lifecycleTransition', itemId: req.params.id, from, to: 'killed', outcome: 'killed', timestamp: now });
|
|
457
507
|
emitDecisionEvent(broadcastMessage, buildPhaseTransitionEvent({ featureCode: item.lifecycle.featureCode, from, to: 'killed', outcome: 'killed', timestamp: now }));
|
|
@@ -479,39 +529,93 @@ export function attachVisionRoutes(app, { store, scheduleBroadcast, broadcastMes
|
|
|
479
529
|
return res.status(400).json({ error: `Can only complete from ${completable} phase, currently in: ${item.lifecycle.currentPhase}` });
|
|
480
530
|
}
|
|
481
531
|
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
//
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
532
|
+
const featureCode = item.lifecycle.featureCode;
|
|
533
|
+
const completionResult = { partial: false };
|
|
534
|
+
let now;
|
|
535
|
+
|
|
536
|
+
// MANAGED build item = bound to a code, build mode, feature.json exists
|
|
537
|
+
// (the same key AC-10/AC-16 use). An unmanaged build item — a lifecycle
|
|
538
|
+
// started for a code that has no feature.json — has nothing canonical to
|
|
539
|
+
// complete and keeps the pre-slice-3 path, as fix/plan do.
|
|
540
|
+
if (isManagedBuildItem(item, projectRoot)) {
|
|
541
|
+
// COMP-COMPLETION-GATE slice 3 (§2.1, AC-10): for a mode that tracks
|
|
542
|
+
// feature.json (build), the cockpit completion IS a completion and goes
|
|
543
|
+
// through the gate — evidence, ONE guarded transition, then the gate
|
|
544
|
+
// performs record → status → ROADMAP → vision → events (§2.3a). The
|
|
545
|
+
// vision step is an in-process projector against this server's live
|
|
546
|
+
// store (a REST call back into ourselves would race it; a direct file
|
|
547
|
+
// write would leave it stale). Under the guard a request with no
|
|
548
|
+
// commit_sha is refused by the evidence check: an evidence-free cockpit
|
|
549
|
+
// completion was path 7's quieter twin and is closed on purpose.
|
|
550
|
+
const { completionGate } = await import('../lib/completion-gate.js');
|
|
551
|
+
const { commit_sha, tests_pass, files_changed, notes } = req.body || {};
|
|
552
|
+
const gated = await completionGate({
|
|
553
|
+
featureCode,
|
|
554
|
+
...(commit_sha ? { commitSha: commit_sha } : {}),
|
|
555
|
+
// Guard on: no default — the evidence check attests (test command) or
|
|
556
|
+
// the caller states it. Guard off: the cockpit's pre-existing default
|
|
557
|
+
// (flag-off parity, the same opt-out the gate itself honours in AC-5).
|
|
558
|
+
testsPass: guardEnabled() ? tests_pass : (tests_pass ?? true),
|
|
559
|
+
filesChanged: files_changed ?? [],
|
|
560
|
+
notes: notes ?? `cockpit lifecycle: ${featureCode} complete`,
|
|
561
|
+
workspaceRoot: projectRoot,
|
|
562
|
+
mode: modeOf(item),
|
|
563
|
+
visionItemId: req.params.id,
|
|
564
|
+
visionProjector: ({ visionItemId, commitSha, ledgerRef, guarded }) => applyVerifiedProjection(store, {
|
|
565
|
+
itemId: visionItemId, featureCode, cwd: projectRoot, consultGuard: guarded ?? true,
|
|
566
|
+
guardEnabledOverride: guarded,
|
|
567
|
+
evidence: { commitSha, ledgerRef, source: 'lifecycle/complete' },
|
|
568
|
+
}).then((r) => { if (!r.ok) throw new Error(r.reasons.join('; ')); return r; }),
|
|
494
569
|
});
|
|
495
|
-
if (!
|
|
496
|
-
return res.status(422).json({
|
|
570
|
+
if (!gated.ok) {
|
|
571
|
+
return res.status(422).json({
|
|
572
|
+
error: gated.refusedAt === 'evidence' ? 'completion evidence not satisfied' : 'completion refused',
|
|
573
|
+
refusedAt: gated.refusedAt, reasons: gated.reasons,
|
|
574
|
+
...(gated.verdict ? { verdict: gated.verdict } : {}),
|
|
575
|
+
...(gated.error ? { guardError: gated.error } : {}),
|
|
576
|
+
});
|
|
497
577
|
}
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
578
|
+
now = new Date().toISOString();
|
|
579
|
+
item.lifecycle.currentPhase = 'complete';
|
|
580
|
+
item.lifecycle.completedAt = now;
|
|
581
|
+
appendPhaseHistory(item, { from: completable, to: 'complete', outcome: 'approved', timestamp: now });
|
|
582
|
+
store.updateLifecycle(req.params.id, item.lifecycle);
|
|
583
|
+
const failures = [...(gated.failures || [])];
|
|
584
|
+
if (store.lastSaveOk === false) {
|
|
585
|
+
// The projection reached disk; this lifecycle write did not. Report it
|
|
586
|
+
// as a partial like any other projection failure (Codex r2 #3).
|
|
587
|
+
failures.push({ step: 'lifecycle-persist', message: 'vision-state could not be persisted after the lifecycle transition', recover: 'restart the server or re-run compose validate --fix' });
|
|
588
|
+
}
|
|
589
|
+
if (failures.length > 0) {
|
|
590
|
+
completionResult.partial = true;
|
|
591
|
+
completionResult.failures = failures;
|
|
592
|
+
}
|
|
593
|
+
completionResult.ledgerRef = gated.ledgerRef ?? null;
|
|
594
|
+
completionResult.verified_by = gated.visionProjection?.verified_by ?? null;
|
|
595
|
+
} else {
|
|
596
|
+
// Fix/plan (no feature.json — COMP-COMPLETION-GATE-MODES) and items
|
|
597
|
+
// with no feature code: the pre-slice-3 path, byte-identical.
|
|
598
|
+
if (guardEnabled()) {
|
|
599
|
+
const ev = await verifyCompletionEvidence({
|
|
600
|
+
commitSha: req.body?.commit_sha,
|
|
601
|
+
cwd: projectRoot,
|
|
602
|
+
testCommand: guardTestCommand(projectRoot),
|
|
603
|
+
testsPassClaim: req.body?.tests_pass,
|
|
604
|
+
});
|
|
605
|
+
if (!ev.ok) {
|
|
606
|
+
return res.status(422).json({ error: 'completion evidence not satisfied', reasons: ev.reasons });
|
|
607
|
+
}
|
|
608
|
+
const g = await guardedTransition({ featureCode: item.lifecycle.featureCode, from: completable, to: 'complete', workspaceRoot: projectRoot, commitSha: req.body?.commit_sha, resolvedBy: 'agent', mode: modeOf(item) });
|
|
609
|
+
if (!g.applied) return res.status(422).json({ error: 'completion refused by guard', from: completable, to: 'complete', verdict: g.verdict, guardError: g.error });
|
|
610
|
+
}
|
|
611
|
+
now = new Date().toISOString();
|
|
612
|
+
item.lifecycle.currentPhase = 'complete';
|
|
613
|
+
item.lifecycle.completedAt = now;
|
|
614
|
+
appendPhaseHistory(item, { from: completable, to: 'complete', outcome: 'approved', timestamp: now });
|
|
615
|
+
store.updateLifecycle(req.params.id, item.lifecycle);
|
|
616
|
+
store.updateItem(req.params.id, { status: 'complete' });
|
|
502
617
|
}
|
|
503
618
|
|
|
504
|
-
const now = new Date().toISOString();
|
|
505
|
-
item.lifecycle.currentPhase = 'complete';
|
|
506
|
-
item.lifecycle.completedAt = now;
|
|
507
|
-
// COMP-OBS-TIMELINE: populate phaseHistory + emit phase_transition DecisionEvent
|
|
508
|
-
appendPhaseHistory(item, { from: completable, to: 'complete', outcome: 'approved', timestamp: now });
|
|
509
|
-
store.updateLifecycle(req.params.id, item.lifecycle);
|
|
510
|
-
store.updateItem(req.params.id, { status: 'complete' });
|
|
511
|
-
// COMP-MCP-ENFORCE Slice 2: status projection for the no-commit path is
|
|
512
|
-
// applied BELOW (in the `else` branch) so it does not pre-empt and mask the
|
|
513
|
-
// recordCompletion bridge, which is the authority + partial-write reporter
|
|
514
|
-
// on the commit_sha path.
|
|
515
619
|
scheduleBroadcast();
|
|
516
620
|
broadcastMessage({ type: 'lifecycleTransition', itemId: req.params.id, from: completable, to: 'complete', outcome: 'approved', timestamp: now });
|
|
517
621
|
emitDecisionEvent(broadcastMessage, buildPhaseTransitionEvent({ featureCode: item.lifecycle.featureCode, from: completable, to: 'complete', outcome: 'approved', timestamp: now }));
|
|
@@ -522,61 +626,67 @@ export function attachVisionRoutes(app, { store, scheduleBroadcast, broadcastMes
|
|
|
522
626
|
// COMP-RESUME: best-effort anchor checkpoint at the (terminal) phase boundary
|
|
523
627
|
anchorBoundary(getTargetRoot(), { item, trigger: 'phase-transition' });
|
|
524
628
|
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
629
|
+
res.json({ completedAt: now, ...completionResult });
|
|
630
|
+
} catch (err) {
|
|
631
|
+
const status = err.message.includes('not found') ? 404 : 400;
|
|
632
|
+
res.status(status).json({ error: err.message });
|
|
633
|
+
}
|
|
634
|
+
});
|
|
635
|
+
|
|
636
|
+
// A reconstructed completion has the same completion evidence and durable
|
|
637
|
+
// writes as the normal gate, but may be reached from any active lifecycle
|
|
638
|
+
// phase. The gate owns the terminal history/item mutation as one recoverable
|
|
639
|
+
// transaction; this route only supplies the live store and emits its effects.
|
|
640
|
+
app.post('/api/vision/items/:id/lifecycle/backfill', guardAuth, async (req, res) => {
|
|
641
|
+
try {
|
|
642
|
+
const item = store.items.get(req.params.id);
|
|
643
|
+
if (!item?.lifecycle) return res.status(404).json({ error: 'No lifecycle on this item' });
|
|
644
|
+
const { commit_sha, tests_pass, files_changed, notes, reason, occurrences } = req.body || {};
|
|
645
|
+
if (typeof reason !== 'string' || reason.trim().length === 0) {
|
|
646
|
+
return res.status(400).json({ error: 'reason is required' });
|
|
647
|
+
}
|
|
648
|
+
const { completionGate } = await import('../lib/completion-gate.js');
|
|
528
649
|
const featureCode = item.lifecycle.featureCode;
|
|
529
|
-
const
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
completionResult.completion_failed = err.code || 'UNKNOWN';
|
|
550
|
-
completionResult.message = err.message;
|
|
551
|
-
try {
|
|
552
|
-
emitDecisionEvent(broadcastMessage, {
|
|
553
|
-
type: eventType,
|
|
554
|
-
featureCode,
|
|
555
|
-
timestamp: now,
|
|
556
|
-
error: { code: err.code, message: err.message },
|
|
557
|
-
});
|
|
558
|
-
} catch { /* decision event emit best-effort */ }
|
|
559
|
-
// eslint-disable-next-line no-console
|
|
560
|
-
console.warn(`[lifecycle/complete] record_completion ${eventType} for ${featureCode}: ${err.message}`);
|
|
561
|
-
}
|
|
562
|
-
} else {
|
|
563
|
-
// No SHA — emit a skip event so validate_feature can flag drift later
|
|
564
|
-
try {
|
|
565
|
-
emitDecisionEvent(broadcastMessage, {
|
|
566
|
-
type: 'cockpit_completion_skipped',
|
|
567
|
-
featureCode,
|
|
568
|
-
timestamp: now,
|
|
569
|
-
reason: 'no_commit_sha',
|
|
570
|
-
});
|
|
571
|
-
} catch { /* decision event emit best-effort */ }
|
|
572
|
-
// No recordCompletion bridge ran — project complete → COMPLETE so
|
|
573
|
-
// lifecycle-as-truth still reaches feature.json (best-effort).
|
|
574
|
-
// COMP-ROADMAP-PLAN: skipped for tracksFeatureJson:false modes (no feature.json).
|
|
575
|
-
if (guardEnabled && getMode(modeOf(item)).runner.tracksFeatureJson) await projectFeatureStatus({ featureCode, phase: 'complete', cwd: projectRoot });
|
|
576
|
-
}
|
|
650
|
+
const gated = await completionGate({
|
|
651
|
+
intent: 'backfill', featureCode,
|
|
652
|
+
...(commit_sha !== undefined ? { commitSha: commit_sha } : {}),
|
|
653
|
+
testsPass: tests_pass,
|
|
654
|
+
filesChanged: files_changed ?? [], notes, reason, occurrences: occurrences ?? [],
|
|
655
|
+
workspaceRoot: projectRoot, mode: modeOf(item), item, store,
|
|
656
|
+
visionItemId: req.params.id,
|
|
657
|
+
visionProjector: ({ visionItemId, commitSha, ledgerRef, guarded }) => applyVerifiedProjection(store, {
|
|
658
|
+
itemId: visionItemId, featureCode, cwd: projectRoot,
|
|
659
|
+
consultGuard: guarded ?? true, guardEnabledOverride: guarded,
|
|
660
|
+
evidence: { commitSha, ledgerRef, source: 'lifecycle/backfill' },
|
|
661
|
+
}).then((r) => { if (!r.ok) throw new Error(r.reasons.join('; ')); return r; }),
|
|
662
|
+
});
|
|
663
|
+
if (!gated.ok) {
|
|
664
|
+
return res.status(422).json({
|
|
665
|
+
error: 'backfill refused', refusedAt: gated.refusedAt, reasons: gated.reasons,
|
|
666
|
+
...(gated.verdict ? { verdict: gated.verdict } : {}),
|
|
667
|
+
...(gated.error ? { guardError: gated.error } : {}),
|
|
668
|
+
...(gated.error?.hint ? { hint: gated.error.hint } : {}),
|
|
669
|
+
});
|
|
577
670
|
}
|
|
578
671
|
|
|
579
|
-
|
|
672
|
+
const timestamp = item.lifecycle.completedAt;
|
|
673
|
+
if (!gated.replayed) {
|
|
674
|
+
const terminal = item.lifecycle.phaseHistory?.findLast((entry) => entry.phase === 'complete_backfilled');
|
|
675
|
+
scheduleBroadcast();
|
|
676
|
+
broadcastMessage({
|
|
677
|
+
type: 'lifecycleTransition', itemId: req.params.id, from: terminal?.from ?? null,
|
|
678
|
+
to: 'complete_backfilled', outcome: terminal?.outcome ?? 'backfilled', timestamp,
|
|
679
|
+
});
|
|
680
|
+
emitDecisionEvent(broadcastMessage, buildPhaseTransitionEvent({
|
|
681
|
+
featureCode, from: terminal?.from ?? null, to: 'complete_backfilled',
|
|
682
|
+
outcome: terminal?.outcome ?? 'backfilled', timestamp,
|
|
683
|
+
origin: terminal?.origin, recordedAt: terminal?.recordedAt, confidence: terminal?.confidence,
|
|
684
|
+
}));
|
|
685
|
+
emitDriftAxes(broadcastMessage, store, item, projectRoot, timestamp);
|
|
686
|
+
emitStatusSnapshot(broadcastMessage, store, featureCode, timestamp);
|
|
687
|
+
anchorBoundary(getTargetRoot(), { item, trigger: 'phase-transition' });
|
|
688
|
+
}
|
|
689
|
+
res.json({ completedAt: timestamp, backfill: gated.backfill ?? null, ...gated });
|
|
580
690
|
} catch (err) {
|
|
581
691
|
const status = err.message.includes('not found') ? 404 : 400;
|
|
582
692
|
res.status(status).json({ error: err.message });
|
|
@@ -794,7 +904,7 @@ export function attachVisionRoutes(app, { store, scheduleBroadcast, broadcastMes
|
|
|
794
904
|
if (!item.lifecycle?.featureCode) {
|
|
795
905
|
return res.status(400).json({ error: 'Item has no lifecycle featureCode' });
|
|
796
906
|
}
|
|
797
|
-
const assessment = new ArtifactManager(featuresPath, item.lifecycle?.mode).assess(item.lifecycle.featureCode);
|
|
907
|
+
const assessment = new ArtifactManager(featuresPath(), item.lifecycle?.mode).assess(item.lifecycle.featureCode);
|
|
798
908
|
res.json(assessment);
|
|
799
909
|
} catch (err) {
|
|
800
910
|
res.status(500).json({ error: err.message });
|
|
@@ -808,7 +918,7 @@ export function attachVisionRoutes(app, { store, scheduleBroadcast, broadcastMes
|
|
|
808
918
|
if (!item.lifecycle?.featureCode) {
|
|
809
919
|
return res.status(400).json({ error: 'Item has no lifecycle featureCode' });
|
|
810
920
|
}
|
|
811
|
-
const result = new ArtifactManager(featuresPath, item.lifecycle?.mode).scaffold(item.lifecycle.featureCode, req.body);
|
|
921
|
+
const result = new ArtifactManager(featuresPath(), item.lifecycle?.mode).scaffold(item.lifecycle.featureCode, req.body);
|
|
812
922
|
res.json(result);
|
|
813
923
|
} catch (err) {
|
|
814
924
|
res.status(400).json({ error: err.message });
|
package/server/vision-server.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import express from 'express';
|
|
1
2
|
/**
|
|
2
3
|
* Vision Server — REST endpoints + WebSocket broadcast for the vision surface.
|
|
3
4
|
* Follows the same attach() pattern as FileWatcherServer.
|
|
@@ -31,6 +32,7 @@ import { attachHealthRoutes } from './health-routes.js';
|
|
|
31
32
|
import { attachValidateRoutes } from './validate-routes.js';
|
|
32
33
|
import { attachQaScopeRoutes } from './qa-scope-routes.js';
|
|
33
34
|
import { attachSmartmemoryRoutes } from './smartmemory-routes.js';
|
|
35
|
+
import { attachMayaRoutes } from './maya-routes.js';
|
|
34
36
|
import { attachDesignRoutes } from './design-routes.js';
|
|
35
37
|
import { DesignSessionManager } from './design-session.js';
|
|
36
38
|
import { attachPipelineRoutes } from './pipeline-routes.js';
|
|
@@ -80,6 +82,18 @@ export class VisionServer {
|
|
|
80
82
|
this._stratumSync = null;
|
|
81
83
|
}
|
|
82
84
|
|
|
85
|
+
/**
|
|
86
|
+
* Create the Stratum poller on first use. Never called while
|
|
87
|
+
* `capabilities.stratum` is false, so a stratum-disabled workspace keeps
|
|
88
|
+
* `_stratumSync === null` (no timers, no poller) for its whole lifetime.
|
|
89
|
+
*/
|
|
90
|
+
_ensureStratumSync() {
|
|
91
|
+
if (!this._stratumSync) {
|
|
92
|
+
this._stratumSync = new StratumSync(this.store, () => this.scheduleBroadcast());
|
|
93
|
+
}
|
|
94
|
+
return this._stratumSync;
|
|
95
|
+
}
|
|
96
|
+
|
|
83
97
|
attach(httpServer, app) {
|
|
84
98
|
// ── Settings store ────────────────────────────────────────────────────
|
|
85
99
|
this.settingsStore = new SettingsStore(undefined, SETTINGS_DEFAULTS);
|
|
@@ -108,6 +122,11 @@ export class VisionServer {
|
|
|
108
122
|
// answers {enabled:false} from config alone when the flag is OFF.
|
|
109
123
|
attachSmartmemoryRoutes(app);
|
|
110
124
|
|
|
125
|
+
// ── Maya colleague relay (COMP-FOH FOH-6, opt-in) ──────────────────────
|
|
126
|
+
// GET /api/maya/status (degrade-never-fail funnel probe) + POST
|
|
127
|
+
// /api/maya/message. Auth-gated in remote mode — never allowlisted.
|
|
128
|
+
attachMayaRoutes(app);
|
|
129
|
+
|
|
111
130
|
// ── Vision CRUD + lifecycle routes ─────────────────────────────────────
|
|
112
131
|
attachVisionRoutes(app, {
|
|
113
132
|
store: this.store,
|
|
@@ -155,17 +174,14 @@ export class VisionServer {
|
|
|
155
174
|
});
|
|
156
175
|
|
|
157
176
|
// ── Design conversation routes ──────────────────────────────────────────
|
|
158
|
-
//
|
|
177
|
+
// The workspace owns its manager; pending conversations retain their data root.
|
|
159
178
|
let _designSessionManager = null;
|
|
160
|
-
let _designDataDir = null;
|
|
161
179
|
|
|
162
180
|
attachDesignRoutes(app, {
|
|
163
181
|
getSessionManager: () => {
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
_designSessionManager = new DesignSessionManager(dataDir);
|
|
168
|
-
_designDataDir = dataDir;
|
|
182
|
+
if (!_designSessionManager) {
|
|
183
|
+
_designSessionManager = new DesignSessionManager(getDataDir());
|
|
184
|
+
this._designSessionManager = _designSessionManager;
|
|
169
185
|
}
|
|
170
186
|
return _designSessionManager;
|
|
171
187
|
},
|
|
@@ -262,28 +278,32 @@ export class VisionServer {
|
|
|
262
278
|
});
|
|
263
279
|
|
|
264
280
|
// ── Stratum (conditional) ────────────────────────────────────────────
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
}
|
|
281
|
+
const self = this;
|
|
282
|
+
const stratumRouter = express.Router();
|
|
283
|
+
stratumRouter.use('/api/stratum', createStratumRouter());
|
|
284
|
+
attachStratumRoutes(stratumRouter, {
|
|
285
|
+
store: this.store,
|
|
286
|
+
scheduleBroadcast: () => this.scheduleBroadcast(),
|
|
287
|
+
broadcastMessage: (msg) => this.broadcastMessage(msg),
|
|
288
|
+
// Lazily resolved: the poller only exists while stratum is enabled
|
|
289
|
+
// (capabilities can flip at runtime via refreshConfig/switch).
|
|
290
|
+
get sync() { return self._stratumSync; },
|
|
291
|
+
});
|
|
292
|
+
app.use((req, res, next) => {
|
|
293
|
+
if (this._config.capabilities?.stratum) return stratumRouter(req, res, next);
|
|
294
|
+
if (req.path.startsWith('/api/stratum')) return res.status(503).json({ error: 'Stratum not enabled', hint: 'compose init (TS engine; python retired — see python-legacy branch)' });
|
|
295
|
+
return next();
|
|
296
|
+
});
|
|
297
|
+
if (this._config.capabilities?.stratum) this._ensureStratumSync().start();
|
|
281
298
|
|
|
282
299
|
// ── COMP-OBS-BRANCH: CC-session watcher (opt-in) ──────────────────────
|
|
283
300
|
// Default OFF. Enable by setting `capabilities.cc_session_watcher: true` in compose.json
|
|
284
301
|
// or by setting the `CC_SESSION_WATCHER=1` env var. When enabled, Forge reads
|
|
285
302
|
// `~/.claude/projects/**/*.jsonl` and emits BranchLineage + DecisionEvents tied to the
|
|
286
303
|
// current feature via sessions.json's transcriptPath basename.
|
|
304
|
+
this._refreshCCWatcher = () => {
|
|
305
|
+
this._ccWatcher?.stop();
|
|
306
|
+
this._ccWatcher = null;
|
|
287
307
|
const ccWatcherEnabled =
|
|
288
308
|
this._config.capabilities?.cc_session_watcher === true ||
|
|
289
309
|
process.env.CC_SESSION_WATCHER === '1';
|
|
@@ -354,6 +374,9 @@ export class VisionServer {
|
|
|
354
374
|
}
|
|
355
375
|
}
|
|
356
376
|
|
|
377
|
+
};
|
|
378
|
+
this._refreshCCWatcher();
|
|
379
|
+
|
|
357
380
|
// ── Haiku summary broadcast ─────────────────────────────────────────────
|
|
358
381
|
if (this.sessionManager) {
|
|
359
382
|
this.sessionManager.onSummary((summary) => {
|
|
@@ -470,7 +493,47 @@ export class VisionServer {
|
|
|
470
493
|
}
|
|
471
494
|
}
|
|
472
495
|
|
|
496
|
+
/** The live config object routes and the workspace binding both hold. */
|
|
497
|
+
get config() { return this._config; }
|
|
498
|
+
|
|
499
|
+
refreshConfig(config) {
|
|
500
|
+
this._stratumSync?.stop();
|
|
501
|
+
// C4: the incoming config may BE the object we already hold — the workspace
|
|
502
|
+
// binding aliases it, so `switch(root, ctx.binding.config)` hands it straight
|
|
503
|
+
// back. Snapshot first: clearing the holder would otherwise empty the very
|
|
504
|
+
// config being installed, leaving the workspace with no capabilities at all.
|
|
505
|
+
const next = JSON.parse(JSON.stringify(config ?? {}));
|
|
506
|
+
// Existing routes hold this capabilities object; update it in place.
|
|
507
|
+
const capabilities = this._config.capabilities ?? {};
|
|
508
|
+
for (const key of Object.keys(capabilities)) delete capabilities[key];
|
|
509
|
+
Object.assign(capabilities, next.capabilities);
|
|
510
|
+
for (const key of Object.keys(this._config)) delete this._config[key];
|
|
511
|
+
Object.assign(this._config, next, { capabilities });
|
|
512
|
+
if (this.sessionManager) this.sessionManager._featureRoot = resolveProjectPath('features');
|
|
513
|
+
this._refreshCCWatcher?.();
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
suspend() {
|
|
517
|
+
this._healthMonitor?.suspend();
|
|
518
|
+
this._coalescingBuffer?.stop();
|
|
519
|
+
this._stratumSync?.stop();
|
|
520
|
+
this._ccWatcher?.stop();
|
|
521
|
+
this._worktreeGC?.stop();
|
|
522
|
+
for (const client of this.clients) client.close(1000, 'Workspace changed');
|
|
523
|
+
this.clients.clear();
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
resume() {
|
|
527
|
+
this._healthMonitor?.resume();
|
|
528
|
+
this._coalescingBuffer?.start();
|
|
529
|
+
if (this._config.capabilities?.stratum) this._ensureStratumSync().start();
|
|
530
|
+
this._ccWatcher?.start();
|
|
531
|
+
this._worktreeGC?.start();
|
|
532
|
+
}
|
|
533
|
+
|
|
473
534
|
close() {
|
|
535
|
+
this.suspend();
|
|
536
|
+
this._designSessionManager?.destroy();
|
|
474
537
|
this._coalescingBuffer?.stop();
|
|
475
538
|
if (this._stratumSync) this._stratumSync.stop();
|
|
476
539
|
if (this._healthMonitor) this._healthMonitor.destroy();
|