@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
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* server/completion-projection.js — COMP-COMPLETION-GATE slice 3 (§2.3b).
|
|
3
|
+
*
|
|
4
|
+
* The self-verifying projection: ONE predicate that decides whether a vision
|
|
5
|
+
* item may be shown as `complete`, and FOUR transports that apply it —
|
|
6
|
+
*
|
|
7
|
+
* verifiedCompleteProjection({ item, featureCode, cwd, … })
|
|
8
|
+
* ├── POST /api/vision/items/:id/completion-projection (the gate over REST)
|
|
9
|
+
* ├── applyVerifiedProjection(store, …) (in-process: the
|
|
10
|
+
* │ /lifecycle/complete route, the startup scanner, the reconciler)
|
|
11
|
+
* └── VisionWriter.completeItem direct mode (no server running)
|
|
12
|
+
*
|
|
13
|
+
* The property that matters: the predicate carries NO authority of its own. It
|
|
14
|
+
* re-reads canonical state — feature.json, and the guard ledger when a guard
|
|
15
|
+
* resource exists — and refuses unless the completion is ALREADY recorded
|
|
16
|
+
* there. A caller who can satisfy it could simply have read the same truth, so
|
|
17
|
+
* it can be public without reopening the bypass AC-10/AC-16 close. There is no
|
|
18
|
+
* token, no secret, and nothing to forge.
|
|
19
|
+
*
|
|
20
|
+
* Three verification tiers, stamped on the projection so a reader can never
|
|
21
|
+
* mistake a weaker one for a stronger one (AC-16b):
|
|
22
|
+
*
|
|
23
|
+
* 'guarded' — a guard resource exists and its state is complete
|
|
24
|
+
* 'canonical-status-only' — feature.json is COMPLETE, no guard resource exists
|
|
25
|
+
* (the 230 legacy features; startup seeding)
|
|
26
|
+
* 'document-derived' — no feature.json at all; the scanner derived
|
|
27
|
+
* completion from documents (unmanaged folders).
|
|
28
|
+
* Display state, never a completion compose vouches for.
|
|
29
|
+
*
|
|
30
|
+
* Only `guard_not_found` means legacy. Any other guard outcome — timeout, spawn
|
|
31
|
+
* failure, corrupt registry — fails CLOSED (§2.3b, round 5).
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
import { existsSync } from 'node:fs';
|
|
35
|
+
import path from 'node:path';
|
|
36
|
+
|
|
37
|
+
import { readFeature, featuresBase } from '../lib/feature-json.js';
|
|
38
|
+
import { loadFeaturesDir } from '../lib/project-paths.js';
|
|
39
|
+
import { resolveMode } from '../lib/lifecycle-modes.js';
|
|
40
|
+
import { guardEnabled, currentGuardState } from '../lib/completion-gate.js';
|
|
41
|
+
import { resourceId } from './lifecycle-guard.js';
|
|
42
|
+
|
|
43
|
+
export const VERIFIED_BY = Object.freeze({
|
|
44
|
+
GUARDED: 'guarded',
|
|
45
|
+
CANONICAL: 'canonical-status-only',
|
|
46
|
+
DOCUMENT: 'document-derived',
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* The guard states that attest a completion (COMP-LIFECYCLE-BACKFILL S1-1).
|
|
51
|
+
* `complete` is a walked lifecycle; `complete_backfilled` is one reconstructed
|
|
52
|
+
* from evidence after the fact. Both are terminal and both verify — the state
|
|
53
|
+
* itself is what tells a reader which one it was.
|
|
54
|
+
*/
|
|
55
|
+
export const GUARDED_TERMINAL_STATES = new Set(['complete', 'complete_backfilled']);
|
|
56
|
+
|
|
57
|
+
/** The mode a vision item's lifecycle runs under; build when unstated. */
|
|
58
|
+
export function itemMode(item) {
|
|
59
|
+
return resolveMode(item?.lifecycle?.mode ?? 'build');
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* A MANAGED build-mode feature item: it is bound to a feature code by its
|
|
64
|
+
* lifecycle, that lifecycle is build mode, and the workspace has a feature.json
|
|
65
|
+
* for the code. This is the exact set the general status writers refuse
|
|
66
|
+
* `complete` for (AC-10, AC-16). Everything else — fix/plan items, UI-created
|
|
67
|
+
* items with no lifecycle, the `compose new` kickoff item (a build-mode item
|
|
68
|
+
* with no feature.json) — is not a feature completion the gate governs, and
|
|
69
|
+
* keeps its existing path (§2.3d; design §1.3 "documented, not changed").
|
|
70
|
+
*/
|
|
71
|
+
export function isManagedBuildItem(item, cwd) {
|
|
72
|
+
const code = item?.lifecycle?.featureCode;
|
|
73
|
+
if (!code || itemMode(item) !== 'build') return false;
|
|
74
|
+
// EXISTENCE, not parseability: a present-but-malformed feature.json is a
|
|
75
|
+
// managed feature whose canon is broken, and the refusals must still apply
|
|
76
|
+
// (Codex r1 #1 — a corrupt file must not read as "unmanaged, anything goes").
|
|
77
|
+
return canonicalFile(cwd, code).exists;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** feature.json for `code`: whether it exists, and its parsed content (null when unreadable). */
|
|
81
|
+
export function canonicalFile(cwd, code) {
|
|
82
|
+
const dir = loadFeaturesDir(cwd);
|
|
83
|
+
const file = path.join(featuresBase(cwd, dir), code, 'feature.json');
|
|
84
|
+
const exists = existsSync(file);
|
|
85
|
+
return { exists, file, feature: exists ? readFeature(cwd, code, dir) : null };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Decide whether `item` may be projected as complete for `featureCode`.
|
|
90
|
+
*
|
|
91
|
+
* @param {object} a
|
|
92
|
+
* @param {object} a.item the vision item (any transport's copy)
|
|
93
|
+
* @param {string} a.featureCode
|
|
94
|
+
* @param {string} a.cwd workspace root (feature.json, capabilities, resourceId)
|
|
95
|
+
* @param {boolean} [a.consultGuard=true] read the guard ledger (async subprocess). The
|
|
96
|
+
* synchronous startup path passes false (§2.3b).
|
|
97
|
+
* @param {boolean} [a.allowDocumentDerived=false] scanner only: accept a folder with no
|
|
98
|
+
* feature.json as the weakest tier
|
|
99
|
+
* @returns {Promise<{ok:boolean, verified_by?:string, reasons:string[], guardState?:string|null}>}
|
|
100
|
+
*/
|
|
101
|
+
export async function verifiedCompleteProjection({
|
|
102
|
+
item, featureCode, cwd, consultGuard = true, allowDocumentDerived = false,
|
|
103
|
+
guardEnabledOverride = undefined,
|
|
104
|
+
}) {
|
|
105
|
+
const reasons = [];
|
|
106
|
+
if (!item) return { ok: false, reasons: ['vision item not found'] };
|
|
107
|
+
if (!featureCode) return { ok: false, reasons: ['featureCode is required'] };
|
|
108
|
+
if (!cwd) return { ok: false, reasons: ['cwd is required'] };
|
|
109
|
+
|
|
110
|
+
// 3. the item is bound to this feature. An unbound item (no lifecycle) may be
|
|
111
|
+
// projected only when its id or featureCode field names the code — the
|
|
112
|
+
// same match `findFeatureItem` uses — so the scanner's freshly created
|
|
113
|
+
// items and legacy UI items are reachable, but a lifecycle bound to a
|
|
114
|
+
// DIFFERENT code is never overwritten.
|
|
115
|
+
const bound = item.lifecycle?.featureCode;
|
|
116
|
+
if (bound && bound !== featureCode) {
|
|
117
|
+
return { ok: false, reasons: [`item ${item.id} is bound to ${bound}, not ${featureCode}`] };
|
|
118
|
+
}
|
|
119
|
+
if (!bound && item.id !== featureCode && item.featureCode !== featureCode && item.title !== featureCode) {
|
|
120
|
+
return { ok: false, reasons: [`item ${item.id} is not bound to ${featureCode}`] };
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// 1. canonical status
|
|
124
|
+
const canon = canonicalFile(cwd, featureCode);
|
|
125
|
+
if (canon.exists && !canon.feature) {
|
|
126
|
+
// Present but unreadable: canon is BROKEN, not absent. Never downgrade to
|
|
127
|
+
// the document tier; nothing can be verified against a corrupt file.
|
|
128
|
+
return { ok: false, reasons: [`feature.json for ${featureCode} exists but could not be parsed — refusing to project a completion against broken canon`] };
|
|
129
|
+
}
|
|
130
|
+
const feature = canon.feature;
|
|
131
|
+
if (!feature) {
|
|
132
|
+
if (allowDocumentDerived) {
|
|
133
|
+
return { ok: true, verified_by: VERIFIED_BY.DOCUMENT, reasons: [] };
|
|
134
|
+
}
|
|
135
|
+
return { ok: false, reasons: [`no feature.json for ${featureCode} — nothing canonical records a completion`] };
|
|
136
|
+
}
|
|
137
|
+
if (feature.status !== 'COMPLETE') {
|
|
138
|
+
reasons.push(`feature.json for ${featureCode} reads ${feature.status ?? '(no status)'}, not COMPLETE`);
|
|
139
|
+
return { ok: false, reasons };
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// 2. the guard, only if a resource exists — and only when the workspace has
|
|
143
|
+
// the guard enabled; with it off no resource can have been created by
|
|
144
|
+
// compose and there is no stratum to ask.
|
|
145
|
+
// COMP-LIFECYCLE-BACKFILL §5.10a/R3-7: `guardEnabledOverride` is an
|
|
146
|
+
// OPERATION-level override of a WORKSPACE-level config read. The config
|
|
147
|
+
// answers "is the guard on in this workspace now"; a resumed completion needs
|
|
148
|
+
// "was the guard on for THIS operation" — the flag it persisted. Read with
|
|
149
|
+
// `??`, never `||`: a persisted `false` must not fall through to live config.
|
|
150
|
+
const guardOn = guardEnabledOverride ?? guardEnabled(cwd);
|
|
151
|
+
if (!consultGuard || !guardOn) {
|
|
152
|
+
return { ok: true, verified_by: VERIFIED_BY.CANONICAL, reasons: [], guardState: null };
|
|
153
|
+
}
|
|
154
|
+
const rid = resourceId(featureCode, cwd, itemMode(item));
|
|
155
|
+
const g = await currentGuardState(rid);
|
|
156
|
+
if (g.error) {
|
|
157
|
+
// Fail closed. An unreachable guard is not a legacy feature.
|
|
158
|
+
return {
|
|
159
|
+
ok: false,
|
|
160
|
+
reasons: [`guard state for ${featureCode} could not be read (${g.error.code || g.error.error_type || 'error'}: ${g.error.message || ''}) — refusing rather than downgrading the verification tier`],
|
|
161
|
+
guardState: null,
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
if (g.state === null) {
|
|
165
|
+
return { ok: true, verified_by: VERIFIED_BY.CANONICAL, reasons: [], guardState: null };
|
|
166
|
+
}
|
|
167
|
+
// COMP-LIFECYCLE-BACKFILL: both terminals verify a completion. `complete` is a
|
|
168
|
+
// walked lifecycle; `complete_backfilled` is one reconstructed from evidence.
|
|
169
|
+
// The state itself is the provenance signal (Decision 11), so the stamp keeps
|
|
170
|
+
// the ACTUAL state rather than flattening the two.
|
|
171
|
+
if (!GUARDED_TERMINAL_STATES.has(g.state)) {
|
|
172
|
+
return { ok: false, reasons: [`guard for ${featureCode} is in state "${g.state}", not a completion terminal`], guardState: g.state };
|
|
173
|
+
}
|
|
174
|
+
return { ok: true, verified_by: VERIFIED_BY.GUARDED, reasons: [], guardState: g.state };
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* The projection record written next to `status: 'complete'`.
|
|
179
|
+
* @param {{ok:true, verified_by:string}} verdict
|
|
180
|
+
* @param {{commitSha?:string, ledgerRef?:string, source?:string}} [evidence]
|
|
181
|
+
*/
|
|
182
|
+
export function projectionStamp(verdict, evidence = {}) {
|
|
183
|
+
return {
|
|
184
|
+
verified_by: verdict.verified_by,
|
|
185
|
+
at: new Date().toISOString(),
|
|
186
|
+
...(evidence.commitSha ? { commit_sha: evidence.commitSha } : {}),
|
|
187
|
+
...(evidence.ledgerRef ? { ledger_ref: evidence.ledgerRef } : {}),
|
|
188
|
+
...(evidence.source ? { source: evidence.source } : {}),
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* In-process transport: verify, then write to the live VisionStore.
|
|
194
|
+
* Used by the /lifecycle/complete route, the reconciler, and the startup
|
|
195
|
+
* scanner (the latter with `consultGuard:false, allowDocumentDerived:true`).
|
|
196
|
+
*
|
|
197
|
+
* @returns {Promise<{ok:boolean, verified_by?:string, reasons:string[], item?:object}>}
|
|
198
|
+
*/
|
|
199
|
+
export async function applyVerifiedProjection(store, {
|
|
200
|
+
itemId, featureCode, cwd, consultGuard = true, allowDocumentDerived = false, evidence = {},
|
|
201
|
+
guardEnabledOverride = undefined,
|
|
202
|
+
}) {
|
|
203
|
+
const item = store.items.get(itemId);
|
|
204
|
+
const v = await verifiedCompleteProjection({
|
|
205
|
+
item, featureCode, cwd, consultGuard, allowDocumentDerived, guardEnabledOverride,
|
|
206
|
+
});
|
|
207
|
+
if (!v.ok) return v;
|
|
208
|
+
const prior = { status: item.status, completion_projection: item.completion_projection ?? null };
|
|
209
|
+
const updated = store.updateItem(itemId, {
|
|
210
|
+
status: 'complete',
|
|
211
|
+
completion_projection: projectionStamp(v, evidence),
|
|
212
|
+
});
|
|
213
|
+
// `_save` swallows disk failures into a boolean the store records. A
|
|
214
|
+
// projection that only reached memory is not a projection: it vanishes on
|
|
215
|
+
// restart with no partial result and no recovery identity (Codex r1 #2).
|
|
216
|
+
// Roll the live item back so memory and disk agree, and report the failure.
|
|
217
|
+
if (store.lastSaveOk === false) {
|
|
218
|
+
try { store.updateItem(itemId, prior); } catch { /* best-effort rollback */ }
|
|
219
|
+
return { ok: false, reasons: [`vision-state could not be persisted for ${itemId} — projection rolled back`] };
|
|
220
|
+
}
|
|
221
|
+
return { ok: true, verified_by: v.verified_by, reasons: [], item: updated };
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/** True when `p` is inside the workspace's features dir (used by the allowlist test's docs). */
|
|
225
|
+
export function isFeatureJsonPath(cwd, p) {
|
|
226
|
+
const base = path.resolve(cwd, loadFeaturesDir(cwd));
|
|
227
|
+
return path.resolve(p).startsWith(base) && existsSync(p);
|
|
228
|
+
}
|
|
@@ -9,7 +9,7 @@ import fs from 'node:fs';
|
|
|
9
9
|
import http from 'node:http';
|
|
10
10
|
import path from 'node:path';
|
|
11
11
|
import { ArtifactManager, artifactKeysForMode } from './artifact-manager.js';
|
|
12
|
-
import { getTargetRoot, getDataDir, resolveProjectPath, switchProject, setCurrentWorkspaceId, loadProjectConfig, isLifecycleEnabled } from './project-root.js';
|
|
12
|
+
import { getTargetRoot, getDataDir, resolveProjectPath, switchProject, setCurrentWorkspaceId, getCurrentWorkspaceId, loadProjectConfig, isLifecycleEnabled } from './project-root.js';
|
|
13
13
|
import { resolveProfile, isToolAllowed } from './mcp-tool-policy.js';
|
|
14
14
|
import { getRoadmap } from '../lib/get-roadmap.js';
|
|
15
15
|
|
|
@@ -19,7 +19,7 @@ import { getRoadmap } from '../lib/get-roadmap.js';
|
|
|
19
19
|
* status/roadmap mutation is the bypass STRAT-GUARD exists to close, so it is
|
|
20
20
|
* rejected unless it carries a valid out-of-band override token (the agent
|
|
21
21
|
* cannot mint it). Guard off → legacy behavior (no-op). Internal callers
|
|
22
|
-
* (
|
|
22
|
+
* (the completion gate → persistFeatureRaw) never pass through here.
|
|
23
23
|
*
|
|
24
24
|
* @param {object} args tool args (may carry force / override_token)
|
|
25
25
|
* @param {string} toolName for the error message
|
|
@@ -474,6 +474,29 @@ export async function toolAddChangelogEntry(args) {
|
|
|
474
474
|
return addChangelogEntry(getTargetRoot(), args);
|
|
475
475
|
}
|
|
476
476
|
|
|
477
|
+
/**
|
|
478
|
+
* COMP-CANON-OVERRIDE — mint a single-use, path-scoped canon override.
|
|
479
|
+
*
|
|
480
|
+
* `actor` is deliberately not forwarded from args: it is stamped by the writer
|
|
481
|
+
* per Decision 3 and must never be caller-supplied.
|
|
482
|
+
*/
|
|
483
|
+
export async function toolCanonOverrideGrant(args) {
|
|
484
|
+
const { mintGrant } = await import('../lib/canon-override.js');
|
|
485
|
+
const { loadFeaturesDir } = await import('../lib/project-paths.js');
|
|
486
|
+
const root = getTargetRoot();
|
|
487
|
+
const grant = mintGrant(root, {
|
|
488
|
+
path: args?.path,
|
|
489
|
+
reason: args?.reason,
|
|
490
|
+
operation: args?.operation,
|
|
491
|
+
featuresDir: loadFeaturesDir(root),
|
|
492
|
+
});
|
|
493
|
+
return {
|
|
494
|
+
...grant,
|
|
495
|
+
recorded_in: '.compose/canon-overrides.jsonl',
|
|
496
|
+
note: 'Single-use and path-scoped. Audit tooling for the Claude Write/Edit path, not enforcement.',
|
|
497
|
+
};
|
|
498
|
+
}
|
|
499
|
+
|
|
477
500
|
export async function toolGetChangelogEntries(args) {
|
|
478
501
|
const { getChangelogEntries } = await import('../lib/changelog-writer.js');
|
|
479
502
|
return getChangelogEntries(getTargetRoot(), args);
|
|
@@ -498,9 +521,45 @@ export async function toolGetJournalEntries(args) {
|
|
|
498
521
|
// ---------------------------------------------------------------------------
|
|
499
522
|
|
|
500
523
|
export async function toolRecordCompletion(args) {
|
|
501
|
-
|
|
524
|
+
// COMP-COMPLETION-GATE slice 1: route the deliberate completion path through
|
|
525
|
+
// the gate, which verifies the same evidence AND takes a guarded transition —
|
|
526
|
+
// the step that has never happened for any real feature (design.md §1).
|
|
527
|
+
//
|
|
528
|
+
// `set_status:false` is not a completion: it records evidence without moving
|
|
529
|
+
// the feature, so it must not drive the guard to a terminal state. It keeps
|
|
530
|
+
// the evidence check and the original write.
|
|
531
|
+
const cwd = getTargetRoot();
|
|
502
532
|
const { recordCompletion } = await import('../lib/completion-writer.js');
|
|
503
|
-
|
|
533
|
+
const { completionGate } = await import('../lib/completion-gate.js');
|
|
534
|
+
|
|
535
|
+
if (args?.set_status === false) {
|
|
536
|
+
await assertCompletionEvidence(args);
|
|
537
|
+
return recordCompletion(cwd, args);
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
const gated = await completionGate({
|
|
541
|
+
featureCode: args?.feature_code,
|
|
542
|
+
commitSha: args?.commit_sha,
|
|
543
|
+
testsPass: args?.tests_pass,
|
|
544
|
+
filesChanged: args?.files_changed || [],
|
|
545
|
+
notes: args?.notes,
|
|
546
|
+
force: args?.force,
|
|
547
|
+
builtVia: args?.built_via,
|
|
548
|
+
idempotencyKey: args?.idempotency_key,
|
|
549
|
+
workspaceRoot: cwd,
|
|
550
|
+
});
|
|
551
|
+
if (!gated.ok) {
|
|
552
|
+
const e = new Error(
|
|
553
|
+
`record_completion: refused at ${gated.refusedAt} — ${gated.reasons.join('; ')}`,
|
|
554
|
+
);
|
|
555
|
+
// Preserve the established code for the evidence case so existing callers
|
|
556
|
+
// and tests keep matching on it; the guard cases get their own.
|
|
557
|
+
e.code = gated.refusedAt === 'evidence'
|
|
558
|
+
? 'COMPLETION_EVIDENCE_REQUIRED'
|
|
559
|
+
: 'COMPLETION_GATE_REFUSED';
|
|
560
|
+
throw e;
|
|
561
|
+
}
|
|
562
|
+
return gated.result;
|
|
504
563
|
}
|
|
505
564
|
|
|
506
565
|
export async function toolGetCompletions(args) {
|
|
@@ -627,8 +686,8 @@ export async function toolBindSession({ featureCode, profile } = {}) {
|
|
|
627
686
|
// differ from the request arg) so the anchor never drifts. The trusted env
|
|
628
687
|
// profile is the floor; the bind `profile` arg may only NARROW it.
|
|
629
688
|
const boundCode = (body && typeof body === 'object' && body.featureCode) || featureCode;
|
|
630
|
-
if (boundCode)
|
|
631
|
-
|
|
689
|
+
if (boundCode) sessionContext().featureCode = boundCode;
|
|
690
|
+
sessionContext().profile = resolveProfile(process.env.COMPOSE_SESSION_PROFILE, profile);
|
|
632
691
|
return body;
|
|
633
692
|
}
|
|
634
693
|
|
|
@@ -649,10 +708,14 @@ async function _postLifecycle(itemId, action, body) {
|
|
|
649
708
|
}
|
|
650
709
|
const { status, body: respBody } = result;
|
|
651
710
|
if (status >= 400) {
|
|
652
|
-
const
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
711
|
+
const body = respBody && typeof respBody === 'object' ? respBody : null;
|
|
712
|
+
if (body) {
|
|
713
|
+
const parts = [body.error ?? `HTTP ${status}`];
|
|
714
|
+
if (Array.isArray(body.reasons) && body.reasons.length) parts.push(body.reasons.join('; '));
|
|
715
|
+
if (body.hint) parts.push(`hint: ${body.hint}`);
|
|
716
|
+
throw new Error(parts.join(' — '));
|
|
717
|
+
}
|
|
718
|
+
throw new Error(`HTTP ${status}: ${typeof respBody === 'string' ? respBody : JSON.stringify(respBody)}`);
|
|
656
719
|
}
|
|
657
720
|
return respBody;
|
|
658
721
|
}
|
|
@@ -682,7 +745,8 @@ async function _postGate(gateId, action, body) {
|
|
|
682
745
|
async function _httpRequest(method, urlPath, body = null) {
|
|
683
746
|
const port = resolvePort();
|
|
684
747
|
const headers = { 'Content-Type': 'application/json' };
|
|
685
|
-
|
|
748
|
+
const workspaceId = getCurrentWorkspaceId() || _binding?.id;
|
|
749
|
+
if (workspaceId) headers['X-Compose-Workspace-Id'] = workspaceId;
|
|
686
750
|
let payload = null;
|
|
687
751
|
if (body !== null && body !== undefined) {
|
|
688
752
|
payload = JSON.stringify(body);
|
|
@@ -725,6 +789,17 @@ export async function toolCompleteFeature({ id, commit_sha, tests_pass, files_ch
|
|
|
725
789
|
return _postLifecycle(id, 'complete', body);
|
|
726
790
|
}
|
|
727
791
|
|
|
792
|
+
export async function toolBackfillCompletion({ id, commit_sha, tests_pass, files_changed, notes, reason, occurrences }) {
|
|
793
|
+
const body = {};
|
|
794
|
+
if (commit_sha !== undefined) body.commit_sha = commit_sha;
|
|
795
|
+
if (tests_pass !== undefined) body.tests_pass = tests_pass;
|
|
796
|
+
if (files_changed !== undefined) body.files_changed = files_changed;
|
|
797
|
+
if (notes !== undefined) body.notes = notes;
|
|
798
|
+
body.reason = reason;
|
|
799
|
+
body.occurrences = occurrences ?? [];
|
|
800
|
+
return _postLifecycle(id, 'backfill', body);
|
|
801
|
+
}
|
|
802
|
+
|
|
728
803
|
export async function toolIterationStart({ id, loopType, maxIterations }) {
|
|
729
804
|
return _postLifecycle(id, 'iteration/start', { loopType, maxIterations });
|
|
730
805
|
}
|
|
@@ -855,6 +930,11 @@ export async function toolGetJudgmentState() {
|
|
|
855
930
|
return getJudgmentState(getTargetRoot());
|
|
856
931
|
}
|
|
857
932
|
|
|
933
|
+
export async function toolGetJudgmentTrace(args = {}) {
|
|
934
|
+
const { getJudgmentTrace } = await import('../lib/judgment-writer.js');
|
|
935
|
+
return getJudgmentTrace(getTargetRoot(), args.slug);
|
|
936
|
+
}
|
|
937
|
+
|
|
858
938
|
// ---------------------------------------------------------------------------
|
|
859
939
|
// COMP-MCP-ENFORCE-1 — phase-scoped MCP tool gate (profile × phase)
|
|
860
940
|
//
|
|
@@ -862,26 +942,32 @@ export async function toolGetJudgmentState() {
|
|
|
862
942
|
// rewrite its own launch env); bind_session may only NARROW it. The bound
|
|
863
943
|
// feature anchor (_boundFeatureCode) lets the gate resolve the current phase
|
|
864
944
|
// on-disk and check that re-permitted mutations target the bound feature.
|
|
865
|
-
//
|
|
945
|
+
// One MCP child may rebind while older calls await; keep policy state per root.
|
|
866
946
|
// ---------------------------------------------------------------------------
|
|
867
947
|
|
|
868
|
-
|
|
869
|
-
|
|
948
|
+
const sessionContexts = new Map();
|
|
949
|
+
function sessionContext() {
|
|
950
|
+
const root = getTargetRoot();
|
|
951
|
+
if (!sessionContexts.has(root)) {
|
|
952
|
+
sessionContexts.set(root, { profile: resolveProfile(process.env.COMPOSE_SESSION_PROFILE, null), featureCode: null });
|
|
953
|
+
}
|
|
954
|
+
return sessionContexts.get(root);
|
|
955
|
+
}
|
|
870
956
|
|
|
871
|
-
export function _getSessionProfile() { return
|
|
872
|
-
export function _getBoundFeatureCode() { return
|
|
957
|
+
export function _getSessionProfile() { return sessionContext().profile; }
|
|
958
|
+
export function _getBoundFeatureCode() { return sessionContext().featureCode; }
|
|
873
959
|
/** @internal test seam */
|
|
874
960
|
export function _testOnly_setSessionContext({ profile, boundFeatureCode } = {}) {
|
|
875
|
-
if (profile !== undefined)
|
|
876
|
-
if (boundFeatureCode !== undefined)
|
|
961
|
+
if (profile !== undefined) sessionContext().profile = profile;
|
|
962
|
+
if (boundFeatureCode !== undefined) sessionContext().featureCode = boundFeatureCode;
|
|
877
963
|
}
|
|
878
964
|
|
|
879
965
|
/** The bound feature's current lifecycle phase from vision-state.json, or null. */
|
|
880
966
|
export function resolveBoundPhase() {
|
|
881
|
-
if (!
|
|
967
|
+
if (!sessionContext().featureCode) return null;
|
|
882
968
|
try {
|
|
883
969
|
const { items } = loadVisionState();
|
|
884
|
-
const item = items.find((i) => i.lifecycle?.featureCode ===
|
|
970
|
+
const item = items.find((i) => i.lifecycle?.featureCode === sessionContext().featureCode);
|
|
885
971
|
return item?.lifecycle?.currentPhase ?? null;
|
|
886
972
|
} catch {
|
|
887
973
|
return null;
|
|
@@ -909,9 +995,9 @@ function _resolveTargetFeatureCode(tool, args = {}) {
|
|
|
909
995
|
}
|
|
910
996
|
|
|
911
997
|
function _targetMatchesBoundFeature(tool, args) {
|
|
912
|
-
if (!
|
|
998
|
+
if (!sessionContext().featureCode) return false;
|
|
913
999
|
const code = _resolveTargetFeatureCode(tool, args);
|
|
914
|
-
return code !== null && code ===
|
|
1000
|
+
return code !== null && code === sessionContext().featureCode;
|
|
915
1001
|
}
|
|
916
1002
|
|
|
917
1003
|
/**
|
|
@@ -929,7 +1015,7 @@ export function assertToolPhaseAllowed(tool, args = {}, _testCtx) {
|
|
|
929
1015
|
if (!guardOn) return;
|
|
930
1016
|
if (_overrideOk(args)) return;
|
|
931
1017
|
|
|
932
|
-
const profile = _testCtx?.profile ??
|
|
1018
|
+
const profile = _testCtx?.profile ?? sessionContext().profile;
|
|
933
1019
|
const phase = _testCtx?.phase ?? resolveBoundPhase();
|
|
934
1020
|
const targetMatchesBoundFeature = _testCtx?.targetMatches ?? _targetMatchesBoundFeature(tool, args);
|
|
935
1021
|
|