@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/lib/canon-guard.js
CHANGED
|
@@ -98,9 +98,14 @@ export function realpathCanonicalize(p) {
|
|
|
98
98
|
* @param {string} [args.featuresDir='docs/features']
|
|
99
99
|
* @param {(p:string)=>string} [args.canonicalize] - map a path to its real, alias-free form.
|
|
100
100
|
* The runtime wrapper passes realpathCanonicalize; pure tests inject a stub or omit it.
|
|
101
|
+
* @param {string} [args.profile] - the caller's MCP profile (COMPOSE_SESSION_PROFILE).
|
|
102
|
+
* Only changes the ESCAPE SENTENCE of the deny message, never the verdict:
|
|
103
|
+
* `canon_override_grant` is denied to restricted profiles by IMPLEMENTER_DENY /
|
|
104
|
+
* REVIEWER_ALLOW, so telling a restricted caller to mint a grant sends it to a
|
|
105
|
+
* tool it cannot call. Absent/unknown → the unrestricted wording (fail-open).
|
|
101
106
|
* @returns {{deny: boolean, reason?: string, path?: string}}
|
|
102
107
|
*/
|
|
103
|
-
export function decideCanonGuard({ toolName, toolInput, cwd, projectRoot, featuresDir = 'docs/features', canonicalize } = {}) {
|
|
108
|
+
export function decideCanonGuard({ toolName, toolInput, cwd, projectRoot, featuresDir = 'docs/features', canonicalize, profile } = {}) {
|
|
104
109
|
try {
|
|
105
110
|
if (!GUARDED_TOOLS.has(toolName)) return { deny: false };
|
|
106
111
|
const raw = toolInput && (toolInput.file_path ?? toolInput.notebook_path);
|
|
@@ -121,16 +126,33 @@ export function decideCanonGuard({ toolName, toolInput, cwd, projectRoot, featur
|
|
|
121
126
|
const entry = matchEntry(relPosix, { featuresDir, point: 'hook' });
|
|
122
127
|
if (!entry) return { deny: false };
|
|
123
128
|
|
|
129
|
+
// Classification only — this function stays pure. The atomic claim happens
|
|
130
|
+
// in the hook wrapper, because a destructive callback here would make the
|
|
131
|
+
// verdict depend on how many times it was evaluated.
|
|
132
|
+
const overrideEligible = entry.overrideEligible !== false;
|
|
124
133
|
const tools = entry.tools.join(', ');
|
|
134
|
+
// COMP-COVERAGE-GATE C4: canon_override_grant is now denied to restricted
|
|
135
|
+
// profiles, so pointing one at it would loop it against its own tool gate.
|
|
136
|
+
const restricted = profile === 'implementer' || profile === 'reviewer';
|
|
137
|
+
const escape = !overrideEligible
|
|
138
|
+
? `This path is the override's own governance state, so it cannot be overridden: a bypass `
|
|
139
|
+
+ `must not be able to authorise rewriting its own record.`
|
|
140
|
+
: restricted
|
|
141
|
+
? `A canon override exists, but canon_override_grant is not available to the '${profile}' `
|
|
142
|
+
+ `profile — the session subject to canon enforcement cannot exempt itself from it. `
|
|
143
|
+
+ `Escalate: report what you need to write and why, and let the orchestrator decide.`
|
|
144
|
+
: `To do it deliberately anyway, mint a single-use grant with canon_override_grant `
|
|
145
|
+
+ `({ path, reason, operation }) — the bypass is recorded before the grant exists.`;
|
|
146
|
+
|
|
125
147
|
return {
|
|
126
148
|
deny: true,
|
|
127
149
|
path: relPosix,
|
|
150
|
+
overrideEligible,
|
|
128
151
|
reason:
|
|
129
|
-
`${relPosix} is tool-owned canon (COMP-CANON-GUARD). A direct ${toolName} is blocked — `
|
|
130
|
-
`write it through one of: ${tools}. These tools stamp provenance and regenerate the `
|
|
131
|
-
`
|
|
132
|
-
|
|
133
|
-
`the registry hook set if you truly must hand-edit).`,
|
|
152
|
+
`${relPosix} is tool-owned canon (COMP-CANON-GUARD). A direct ${toolName} is blocked — `
|
|
153
|
+
+ `write it through one of: ${tools}. These tools stamp provenance and regenerate the `
|
|
154
|
+
+ `projections from records; a hand-edit is unattributed and overwritten on the next regen. `
|
|
155
|
+
+ `${escape}`,
|
|
134
156
|
};
|
|
135
157
|
} catch {
|
|
136
158
|
return { deny: false }; // fail open — never wedge the session
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* canon-override.js — the canon override: grant-then-write, ledger-first.
|
|
3
|
+
*
|
|
4
|
+
* COMP-CANON-OVERRIDE S2, implementing COMP-CANON-GUARD Decision 4.
|
|
5
|
+
*
|
|
6
|
+
* WHAT THIS IS: audit and careless-drift tooling. It makes the cooperative
|
|
7
|
+
* path logged and the accidental path hard. It is NOT enforcement and must
|
|
8
|
+
* never be described as such — `Bash` and Codex writes never reach the hook,
|
|
9
|
+
* and every piece of state here lives in the workspace the agent can write.
|
|
10
|
+
* A determined actor who forges a token AND its ledger row AND the baseline
|
|
11
|
+
* passes. See design.md, "The in-workspace ceiling".
|
|
12
|
+
*
|
|
13
|
+
* ORDERING IS THE ATOMICITY ANSWER. The bypass row and the attest baseline are
|
|
14
|
+
* written BEFORE the token is minted, so the failure mode is over-recording,
|
|
15
|
+
* never under-recording: a token cannot exist without its row. If the write
|
|
16
|
+
* that follows never happens, a bypass entry exists for a write that did not
|
|
17
|
+
* occur — deliberately the safe direction.
|
|
18
|
+
*
|
|
19
|
+
* SINGLE-USE IS A RENAME, NOT A LOCK. Each grant is its own file, claimed by
|
|
20
|
+
* renaming it into `consumed/`. Exactly one caller can rename a given path;
|
|
21
|
+
* every loser gets ENOENT. An earlier draft used one shared JSON file with
|
|
22
|
+
* temp-write-plus-rename, which gives atomic *publication* but not mutual
|
|
23
|
+
* exclusion — two readers could each drop the token from their own snapshot
|
|
24
|
+
* and both succeed.
|
|
25
|
+
*/
|
|
26
|
+
import {
|
|
27
|
+
mkdirSync, writeFileSync, readFileSync, renameSync, readdirSync, existsSync, appendFileSync,
|
|
28
|
+
} from 'node:fs';
|
|
29
|
+
import { join, dirname } from 'node:path';
|
|
30
|
+
import { randomUUID } from 'node:crypto';
|
|
31
|
+
import { isOverrideEligible } from './canon-registry.js';
|
|
32
|
+
import { baselineFor } from './append-integrity.js';
|
|
33
|
+
|
|
34
|
+
export const LEDGER_REL = '.compose/canon-overrides.jsonl';
|
|
35
|
+
export const ATTEST_REL = '.compose/canon-overrides-attest.json';
|
|
36
|
+
export const GRANTS_REL = '.compose/data/canon-grants';
|
|
37
|
+
const CONSUMED_SUBDIR = 'consumed';
|
|
38
|
+
|
|
39
|
+
/** 5 minutes: long enough for a grant-then-write round trip through an agent
|
|
40
|
+
* turn, short enough that a forgotten grant is not a standing hole. */
|
|
41
|
+
export const GRANT_TTL_MS = 5 * 60 * 1000;
|
|
42
|
+
|
|
43
|
+
function typedError(code, message) {
|
|
44
|
+
const err = new Error(message);
|
|
45
|
+
err.code = code;
|
|
46
|
+
return err;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function ensureDir(path) {
|
|
50
|
+
mkdirSync(path, { recursive: true });
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Read the bypass ledger as parsed rows. Malformed lines are skipped rather
|
|
54
|
+
* than thrown on — a corrupt row must not make every claim fail open. */
|
|
55
|
+
export function readOverrideLedger(cwd) {
|
|
56
|
+
const path = join(cwd, LEDGER_REL);
|
|
57
|
+
let raw;
|
|
58
|
+
try {
|
|
59
|
+
raw = readFileSync(path, 'utf8');
|
|
60
|
+
} catch (err) {
|
|
61
|
+
if (err?.code === 'ENOENT') return [];
|
|
62
|
+
throw err;
|
|
63
|
+
}
|
|
64
|
+
const rows = [];
|
|
65
|
+
for (const line of raw.split('\n')) {
|
|
66
|
+
if (!line.trim()) continue;
|
|
67
|
+
try { rows.push(JSON.parse(line)); } catch { /* skip a corrupt row */ }
|
|
68
|
+
}
|
|
69
|
+
return rows;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Mint a single-use, path-scoped grant.
|
|
74
|
+
*
|
|
75
|
+
* @param {string} cwd workspace root
|
|
76
|
+
* @param {{path:string, reason:string, operation?:string, featuresDir?:string}} args
|
|
77
|
+
* `actor` is deliberately NOT accepted — it is stamped here per Decision 3.
|
|
78
|
+
* @returns {{token_id:string, path:string, expires_at:string}}
|
|
79
|
+
*/
|
|
80
|
+
export function mintGrant(cwd, { path, reason, operation, featuresDir = 'docs/features' } = {}) {
|
|
81
|
+
if (typeof reason !== 'string' || reason.trim() === '') {
|
|
82
|
+
throw typedError(
|
|
83
|
+
'CANON_OVERRIDE_REASON',
|
|
84
|
+
'canon_override_grant: a non-empty reason is required — the bypass row is the whole point',
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
if (typeof path !== 'string' || path.trim() === '') {
|
|
88
|
+
throw typedError('CANON_OVERRIDE_PATH', 'canon_override_grant: path is required');
|
|
89
|
+
}
|
|
90
|
+
if (!isOverrideEligible(path, { featuresDir, point: 'hook' })) {
|
|
91
|
+
throw typedError(
|
|
92
|
+
'CANON_OVERRIDE_INELIGIBLE',
|
|
93
|
+
`canon_override_grant: ${path} is not override-eligible. Either it is not guarded at the `
|
|
94
|
+
+ 'write-time hook (nothing is blocking it), or it is the override\'s own governance state, '
|
|
95
|
+
+ 'which is deliberately ungrantable so a bypass cannot authorise rewriting its own record.',
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const now = new Date();
|
|
100
|
+
const tokenId = randomUUID();
|
|
101
|
+
const row = {
|
|
102
|
+
ts: now.toISOString(),
|
|
103
|
+
actor: 'agent', // stamped here; never caller-supplied
|
|
104
|
+
path,
|
|
105
|
+
reason: reason.trim(),
|
|
106
|
+
operation: typeof operation === 'string' ? operation : null,
|
|
107
|
+
token_id: tokenId,
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
// ── Ledger and baseline FIRST ──────────────────────────────────────────────
|
|
111
|
+
const ledgerPath = join(cwd, LEDGER_REL);
|
|
112
|
+
ensureDir(dirname(ledgerPath));
|
|
113
|
+
appendFileSync(ledgerPath, `${JSON.stringify(row)}\n`);
|
|
114
|
+
writeAttestBaseline(cwd);
|
|
115
|
+
|
|
116
|
+
// ── Then the token ─────────────────────────────────────────────────────────
|
|
117
|
+
const grantsDir = join(cwd, GRANTS_REL);
|
|
118
|
+
ensureDir(grantsDir);
|
|
119
|
+
const expiresAt = new Date(now.getTime() + GRANT_TTL_MS).toISOString();
|
|
120
|
+
writeFileSync(
|
|
121
|
+
join(grantsDir, `${tokenId}.json`),
|
|
122
|
+
`${JSON.stringify({
|
|
123
|
+
token_id: tokenId,
|
|
124
|
+
path,
|
|
125
|
+
actor: 'agent',
|
|
126
|
+
operation: row.operation,
|
|
127
|
+
issued_at: now.toISOString(),
|
|
128
|
+
expires_at: expiresAt,
|
|
129
|
+
}, null, 2)}\n`,
|
|
130
|
+
{ flag: 'wx' }, // exclusive create: mint is serialized too
|
|
131
|
+
);
|
|
132
|
+
|
|
133
|
+
return { token_id: tokenId, path, expires_at: expiresAt };
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** Re-attest the bypass ledger. Called only from the append path — never as a
|
|
137
|
+
* standalone "fix", which would be the laundering step (S5 R1). */
|
|
138
|
+
function writeAttestBaseline(cwd) {
|
|
139
|
+
const ledgerPath = join(cwd, LEDGER_REL);
|
|
140
|
+
const bytes = existsSync(ledgerPath) ? readFileSync(ledgerPath) : Buffer.alloc(0);
|
|
141
|
+
const attestPath = join(cwd, ATTEST_REL);
|
|
142
|
+
ensureDir(dirname(attestPath));
|
|
143
|
+
writeFileSync(attestPath, `${JSON.stringify(baselineFor(bytes), null, 2)}\n`);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Claim a live grant for exactly `path`. Returns true if the write may proceed.
|
|
148
|
+
*
|
|
149
|
+
* Never throws: the hook's policy is fail-open, and a claim that explodes must
|
|
150
|
+
* not wedge the session. A false return means "no grant" and the caller denies
|
|
151
|
+
* on its own terms.
|
|
152
|
+
*/
|
|
153
|
+
export function claimGrant(cwd, path) {
|
|
154
|
+
try {
|
|
155
|
+
const grantsDir = join(cwd, GRANTS_REL);
|
|
156
|
+
if (!existsSync(grantsDir)) return false;
|
|
157
|
+
|
|
158
|
+
// A token is only honoured if its row is in the ledger. This is what stops
|
|
159
|
+
// a raw-written token file from being consumable — the Bash-forgery case.
|
|
160
|
+
// It binds the two artifacts without pretending to be crypto: an actor who
|
|
161
|
+
// writes BOTH still passes, which is the documented ceiling.
|
|
162
|
+
const ledgerTokenIds = new Set(
|
|
163
|
+
readOverrideLedger(cwd).filter((r) => r?.path === path).map((r) => r?.token_id),
|
|
164
|
+
);
|
|
165
|
+
if (ledgerTokenIds.size === 0) return false;
|
|
166
|
+
|
|
167
|
+
const now = Date.now();
|
|
168
|
+
for (const entry of readdirSync(grantsDir, { withFileTypes: true })) {
|
|
169
|
+
if (!entry.isFile() || !entry.name.endsWith('.json')) continue;
|
|
170
|
+
const tokenPath = join(grantsDir, entry.name);
|
|
171
|
+
|
|
172
|
+
let token;
|
|
173
|
+
try { token = JSON.parse(readFileSync(tokenPath, 'utf8')); } catch { continue; }
|
|
174
|
+
if (token?.path !== path) continue;
|
|
175
|
+
if (!ledgerTokenIds.has(token?.token_id)) continue;
|
|
176
|
+
|
|
177
|
+
// Expiry from the immutable stamp, NOT mtime: a checkout gives a file a
|
|
178
|
+
// fresh mtime, which would revive a mistakenly committed token.
|
|
179
|
+
const expiresAt = Date.parse(token?.expires_at ?? '');
|
|
180
|
+
if (!Number.isFinite(expiresAt) || expiresAt <= now) continue;
|
|
181
|
+
|
|
182
|
+
// The claim. Exactly one caller can rename a given path; losers get ENOENT.
|
|
183
|
+
const consumedDir = join(grantsDir, CONSUMED_SUBDIR);
|
|
184
|
+
ensureDir(consumedDir);
|
|
185
|
+
try {
|
|
186
|
+
renameSync(tokenPath, join(consumedDir, entry.name));
|
|
187
|
+
return true;
|
|
188
|
+
} catch {
|
|
189
|
+
continue; // another process won; try the next token
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
return false;
|
|
193
|
+
} catch {
|
|
194
|
+
return false;
|
|
195
|
+
}
|
|
196
|
+
}
|
package/lib/canon-registry.js
CHANGED
|
@@ -30,13 +30,25 @@
|
|
|
30
30
|
// contract test pins these against the legacy values.
|
|
31
31
|
const TOOLS_FOR_ROADMAP = ['add_roadmap_entry', 'set_feature_status', 'propose_followup'];
|
|
32
32
|
const TOOLS_FOR_CHANGELOG = ['add_changelog_entry'];
|
|
33
|
+
/** The override's governance state is written only by the grant tool itself. */
|
|
34
|
+
const TOOLS_FOR_OVERRIDE = ['canon_override_grant'];
|
|
33
35
|
const TOOLS_FOR_FEATURE_JSON = [
|
|
34
36
|
'add_roadmap_entry',
|
|
35
37
|
'set_feature_status',
|
|
36
38
|
'link_artifact',
|
|
37
39
|
'link_features',
|
|
38
40
|
'record_completion',
|
|
41
|
+
// COMP-LIFECYCLE-BACKFILL S3-2: writes feature.json through the completion gate via HTTP.
|
|
42
|
+
'backfill_completion',
|
|
39
43
|
'propose_followup',
|
|
44
|
+
// COMP-COVERAGE-GATE C2 (2026-08-24): both write feature.json server-side via
|
|
45
|
+
// _postLifecycle (server/vision-routes.js:366 status write-back, :451
|
|
46
|
+
// kill→KILLED) and were missing here. `entry.tools` feeds only the
|
|
47
|
+
// canon-guard deny message ("use one of these tools instead") — no allow/deny
|
|
48
|
+
// decision reads it — so this widens no enforcement; it stops the rejection
|
|
49
|
+
// message from omitting two legitimate alternatives.
|
|
50
|
+
'complete_feature',
|
|
51
|
+
'kill_feature',
|
|
40
52
|
];
|
|
41
53
|
|
|
42
54
|
/**
|
|
@@ -82,14 +94,26 @@ function matchJudgment(path) {
|
|
|
82
94
|
return typeof path === 'string' && path.startsWith('docs/judgment/');
|
|
83
95
|
}
|
|
84
96
|
|
|
97
|
+
/**
|
|
98
|
+
* Prefix match on a directory, requiring the separator so that a sibling with
|
|
99
|
+
* a longer name (`canon-grants-backup/`) cannot masquerade as a child.
|
|
100
|
+
*/
|
|
101
|
+
function matchUnder(dir) {
|
|
102
|
+
return (path) => typeof path === 'string' && path.startsWith(`${dir}/`);
|
|
103
|
+
}
|
|
104
|
+
|
|
85
105
|
// ── The registry ─────────────────────────────────────────────────────────────
|
|
86
106
|
|
|
87
107
|
/**
|
|
88
108
|
* @typedef {object} CanonEntry
|
|
89
109
|
* @property {string} id — stable identifier
|
|
110
|
+
* @property {string} display — human-readable path pattern, for status/help output
|
|
90
111
|
* @property {string} writer — the module that legitimately produces this path
|
|
91
112
|
* @property {string[]} tools — typed tools authorised to write it
|
|
92
113
|
* @property {Array<'ship'|'hook'|'pre-commit'>} enforcedBy — points that guard it
|
|
114
|
+
* @property {boolean} [overrideEligible] — may a canon override be granted FOR
|
|
115
|
+
* this path? Absent means yes. Set false for the override's own governance
|
|
116
|
+
* state, which must be guarded without being grantable.
|
|
93
117
|
* @property {(path:string, featuresDir:string)=>boolean} matches
|
|
94
118
|
*/
|
|
95
119
|
|
|
@@ -97,6 +121,7 @@ function matchJudgment(path) {
|
|
|
97
121
|
const REGISTRY = [
|
|
98
122
|
{
|
|
99
123
|
id: 'roadmap',
|
|
124
|
+
display: 'ROADMAP.md',
|
|
100
125
|
writer: 'lib/roadmap-gen.js',
|
|
101
126
|
tools: TOOLS_FOR_ROADMAP,
|
|
102
127
|
enforcedBy: ['ship'],
|
|
@@ -104,6 +129,7 @@ const REGISTRY = [
|
|
|
104
129
|
},
|
|
105
130
|
{
|
|
106
131
|
id: 'changelog',
|
|
132
|
+
display: 'CHANGELOG.md',
|
|
107
133
|
writer: 'lib/changelog-writer.js',
|
|
108
134
|
tools: TOOLS_FOR_CHANGELOG,
|
|
109
135
|
enforcedBy: ['ship'],
|
|
@@ -111,6 +137,7 @@ const REGISTRY = [
|
|
|
111
137
|
},
|
|
112
138
|
{
|
|
113
139
|
id: 'feature-json',
|
|
140
|
+
display: '<features>/*/feature.json',
|
|
114
141
|
writer: 'lib/feature-writer.js',
|
|
115
142
|
tools: TOOLS_FOR_FEATURE_JSON,
|
|
116
143
|
enforcedBy: ['ship'],
|
|
@@ -118,11 +145,50 @@ const REGISTRY = [
|
|
|
118
145
|
},
|
|
119
146
|
{
|
|
120
147
|
id: 'judgment',
|
|
148
|
+
display: 'docs/judgment/**',
|
|
121
149
|
writer: 'lib/judgment-writer.js',
|
|
122
150
|
tools: JUDGMENT_WRITE_TOOLS,
|
|
123
151
|
enforcedBy: ['hook'],
|
|
124
152
|
matches: (path) => matchJudgment(path),
|
|
125
153
|
},
|
|
154
|
+
|
|
155
|
+
// ── Governance class (COMP-CANON-OVERRIDE S1) ──────────────────────────────
|
|
156
|
+
// The override's own state. Guarded like any canon, and additionally
|
|
157
|
+
// `overrideEligible: false` so the override cannot be turned on itself:
|
|
158
|
+
// without this, "hook-registered" and "grantable" are the same set, and an
|
|
159
|
+
// agent could grant a bypass FOR the bypass ledger and then rewrite it
|
|
160
|
+
// (gate round 2, finding 2). The grant directory is governance state for the
|
|
161
|
+
// same reason — unregistered, a raw-written token would be consumable with
|
|
162
|
+
// no ledger row at all (finding 1).
|
|
163
|
+
//
|
|
164
|
+
// Runtime-scoped, like every hook guarantee: `Bash` never reaches this.
|
|
165
|
+
{
|
|
166
|
+
id: 'override-ledger',
|
|
167
|
+
display: '.compose/canon-overrides.jsonl',
|
|
168
|
+
writer: 'lib/canon-override.js',
|
|
169
|
+
tools: TOOLS_FOR_OVERRIDE,
|
|
170
|
+
enforcedBy: ['hook'],
|
|
171
|
+
overrideEligible: false,
|
|
172
|
+
matches: matchExact('.compose/canon-overrides.jsonl'),
|
|
173
|
+
},
|
|
174
|
+
{
|
|
175
|
+
id: 'override-attest',
|
|
176
|
+
display: '.compose/canon-overrides-attest.json',
|
|
177
|
+
writer: 'lib/canon-override.js',
|
|
178
|
+
tools: TOOLS_FOR_OVERRIDE,
|
|
179
|
+
enforcedBy: ['hook'],
|
|
180
|
+
overrideEligible: false,
|
|
181
|
+
matches: matchExact('.compose/canon-overrides-attest.json'),
|
|
182
|
+
},
|
|
183
|
+
{
|
|
184
|
+
id: 'override-grants',
|
|
185
|
+
display: '.compose/data/canon-grants/**',
|
|
186
|
+
writer: 'lib/canon-override.js',
|
|
187
|
+
tools: TOOLS_FOR_OVERRIDE,
|
|
188
|
+
enforcedBy: ['hook'],
|
|
189
|
+
overrideEligible: false,
|
|
190
|
+
matches: matchUnder('.compose/data/canon-grants'),
|
|
191
|
+
},
|
|
126
192
|
];
|
|
127
193
|
|
|
128
194
|
// ── Public API ───────────────────────────────────────────────────────────────
|
|
@@ -149,6 +215,33 @@ export function isGuarded(path, opts) {
|
|
|
149
215
|
return matchEntry(path, opts) !== null;
|
|
150
216
|
}
|
|
151
217
|
|
|
218
|
+
/**
|
|
219
|
+
* True if a canon override may be granted for `path` at `point`.
|
|
220
|
+
*
|
|
221
|
+
* Deliberately NOT the same predicate as `isGuarded`. Two paths are guarded
|
|
222
|
+
* but ungrantable:
|
|
223
|
+
* - governance state (`overrideEligible: false`) — else the override could
|
|
224
|
+
* authorise rewriting its own audit trail;
|
|
225
|
+
* - anything not guarded at THIS point — a grant for a ship-only path is
|
|
226
|
+
* meaningless, because the hook already allows it, and would write a
|
|
227
|
+
* misleading bypass row.
|
|
228
|
+
* An unguarded path is likewise ineligible: nothing is blocking it, so there
|
|
229
|
+
* is nothing to override (the lockout invariant).
|
|
230
|
+
*/
|
|
231
|
+
/**
|
|
232
|
+
* Human-readable path patterns guarded at `point`, for status and help output.
|
|
233
|
+
* Derived rather than hand-written: a hardcoded string in `compose guard
|
|
234
|
+
* status` silently under-reported the guarded set the moment S1 widened it.
|
|
235
|
+
*/
|
|
236
|
+
export function guardedDisplaysFor(point) {
|
|
237
|
+
return REGISTRY.filter((e) => e.enforcedBy.includes(point)).map((e) => e.display);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
export function isOverrideEligible(path, opts) {
|
|
241
|
+
const entry = matchEntry(path, opts);
|
|
242
|
+
return entry !== null && entry.overrideEligible !== false;
|
|
243
|
+
}
|
|
244
|
+
|
|
152
245
|
/** The typed tools authorised to write `path` at `point`, or [] if unguarded. */
|
|
153
246
|
export function toolsForPath(path, opts) {
|
|
154
247
|
const entry = matchEntry(path, opts);
|
|
@@ -173,6 +266,17 @@ export function featureCodeForPath(path, { featuresDir }) {
|
|
|
173
266
|
return middle;
|
|
174
267
|
}
|
|
175
268
|
|
|
269
|
+
/**
|
|
270
|
+
* The registry as plain data — `{ id, display, tools }` per entry, deep-copied.
|
|
271
|
+
*
|
|
272
|
+
* Exists so COMP-COVERAGE-GATE can cross-check `entry.tools` against the tool
|
|
273
|
+
* inventory without reaching into `_internals` (which is test-only) and without
|
|
274
|
+
* being able to mutate the live registry.
|
|
275
|
+
*/
|
|
276
|
+
export function canonEntries() {
|
|
277
|
+
return REGISTRY.map((e) => ({ id: e.id, display: e.display, tools: [...e.tools] }));
|
|
278
|
+
}
|
|
279
|
+
|
|
176
280
|
/** The entry ids guarded at `point` (for the contract test + introspection). */
|
|
177
281
|
export function guardedPatternIdsFor(point) {
|
|
178
282
|
return REGISTRY.filter((e) => e.enforcedBy.includes(point)).map((e) => e.id);
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* cli-commands.js — COMP-AUDIT-13: the single source of truth for compose's
|
|
3
|
+
* top-level command surface.
|
|
4
|
+
*
|
|
5
|
+
* `compose --help` (bin/compose.js) and the Command Index in docs/cli.md are
|
|
6
|
+
* both rendered from COMMANDS here, so installed capability can never again be
|
|
7
|
+
* invisible unless a command is missing from this table — and a drift-guard test
|
|
8
|
+
* (test/cli-commands.test.js) asserts this table matches the real dispatch
|
|
9
|
+
* branches in bin/compose.js in both directions.
|
|
10
|
+
*
|
|
11
|
+
* To add a command: add its `cmd === '<name>'` branch in bin/compose.js AND a
|
|
12
|
+
* row here. The test fails until both exist.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* @typedef {Object} Command
|
|
17
|
+
* @property {string} name Canonical command token (matches `cmd === '<name>'`)
|
|
18
|
+
* @property {string[]} aliases Alternate tokens that dispatch the same command
|
|
19
|
+
* @property {string} group Display group (must be one of COMMAND_GROUPS)
|
|
20
|
+
* @property {string} summary One-line description (help + docs index)
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
/** Ordered display groups. */
|
|
24
|
+
export const COMMAND_GROUPS = [
|
|
25
|
+
'Getting started',
|
|
26
|
+
'Features & roadmap',
|
|
27
|
+
'Build & implement',
|
|
28
|
+
'Lifecycle, gates & review',
|
|
29
|
+
'Vision, ideas & tracking',
|
|
30
|
+
'App, integrations & runtime',
|
|
31
|
+
'Maintenance & info',
|
|
32
|
+
];
|
|
33
|
+
|
|
34
|
+
/** @type {Command[]} */
|
|
35
|
+
export const COMMANDS = [
|
|
36
|
+
// Getting started
|
|
37
|
+
{ name: 'init', aliases: [], group: 'Getting started', summary: 'Initialize Compose in the current project' },
|
|
38
|
+
{ name: 'setup', aliases: ['sync'], group: 'Getting started', summary: 'Install/sync global Compose skills' },
|
|
39
|
+
{ name: 'install', aliases: [], group: 'Getting started', summary: 'Legacy bootstrap — runs init + setup' },
|
|
40
|
+
{ name: 'import', aliases: [], group: 'Getting started', summary: 'Scan an existing project and generate a structured analysis' },
|
|
41
|
+
{ name: 'doctor', aliases: [], group: 'Getting started', summary: 'Check external skill dependencies' },
|
|
42
|
+
{ name: 'update', aliases: ['upgrade'], group: 'Getting started', summary: 'Pull latest compose, reinstall deps, refresh global skill' },
|
|
43
|
+
|
|
44
|
+
// Features & roadmap
|
|
45
|
+
{ name: 'new', aliases: [], group: 'Features & roadmap', summary: 'Kickoff a product (research, brainstorm, roadmap, scaffold)' },
|
|
46
|
+
{ name: 'feature', aliases: [], group: 'Features & roadmap', summary: 'Add a single feature (folder, design seed, ROADMAP entry)' },
|
|
47
|
+
{ name: 'roadmap', aliases: [], group: 'Features & roadmap', summary: 'Show roadmap status; generate/migrate/check ROADMAP.md' },
|
|
48
|
+
{ name: 'triage', aliases: [], group: 'Features & roadmap', summary: 'Analyze a feature and recommend a build profile' },
|
|
49
|
+
{ name: 'qa-scope', aliases: [], group: 'Features & roadmap', summary: "Show affected routes from a feature's changed files" },
|
|
50
|
+
|
|
51
|
+
// Build & implement
|
|
52
|
+
{ name: 'build', aliases: [], group: 'Build & implement', summary: 'Run a feature through the headless lifecycle' },
|
|
53
|
+
{ name: 'fix', aliases: [], group: 'Build & implement', summary: 'Run a bug through the headless bug-fix lifecycle' },
|
|
54
|
+
{ name: 'plan', aliases: [], group: 'Build & implement', summary: 'Plan work into a structured roadmap from a prompt' },
|
|
55
|
+
{ name: 'gsd', aliases: [], group: 'Build & implement', summary: 'Per-task fresh-context dispatch from a blueprint + Boundary Map' },
|
|
56
|
+
{ name: 'pipeline', aliases: [], group: 'Build & implement', summary: 'View and edit the build pipeline' },
|
|
57
|
+
{ name: 'experiment', aliases: [], group: 'Build & implement', summary: 'Run an A/B model experiment from a spec' },
|
|
58
|
+
|
|
59
|
+
// Lifecycle, gates & review
|
|
60
|
+
{ name: 'gates', aliases: ['gate'], group: 'Lifecycle, gates & review', summary: 'List and resolve pending gates' },
|
|
61
|
+
{ name: 'loops', aliases: [], group: 'Lifecycle, gates & review', summary: 'Manage iteration loops for a feature' },
|
|
62
|
+
{ name: 'guard', aliases: [], group: 'Lifecycle, gates & review', summary: 'Manage the canon guard and drift detection' },
|
|
63
|
+
{ name: 'validate', aliases: [], group: 'Lifecycle, gates & review', summary: 'Validate feature/project artifacts against contracts' },
|
|
64
|
+
{ name: 'record-completion', aliases: [], group: 'Lifecycle, gates & review', summary: 'Record a completion bound to a commit SHA (flips status to COMPLETE)' },
|
|
65
|
+
{ name: 'lineage', aliases: [], group: 'Lifecycle, gates & review', summary: 'PROV-O artifact lineage: stamp | stale | show' },
|
|
66
|
+
{ name: 'context', aliases: [], group: 'Lifecycle, gates & review', summary: 'Show the build decision log' },
|
|
67
|
+
|
|
68
|
+
// Vision, ideas & tracking
|
|
69
|
+
{ name: 'items', aliases: [], group: 'Vision, ideas & tracking', summary: 'List vision items from local state (no server)' },
|
|
70
|
+
{ name: 'ideabox', aliases: [], group: 'Vision, ideas & tracking', summary: 'Capture, review, and promote product ideas' },
|
|
71
|
+
{ name: 'judgment', aliases: [], group: 'Vision, ideas & tracking', summary: "Judgment records: trace a position's causal ancestry" },
|
|
72
|
+
{ name: 'metrics', aliases: [], group: 'Vision, ideas & tracking', summary: 'Report dispatch, settlement, and triage metrics' },
|
|
73
|
+
{ name: 'tracker', aliases: [], group: 'Vision, ideas & tracking', summary: 'Tracker provider status and op-log sync' },
|
|
74
|
+
|
|
75
|
+
// App, integrations & runtime
|
|
76
|
+
{ name: 'start', aliases: [], group: 'App, integrations & runtime', summary: 'Start the compose app (UI + API) for this project' },
|
|
77
|
+
{ name: 'remote', aliases: [], group: 'App, integrations & runtime', summary: 'Manage remote access: pair, list, revoke, status' },
|
|
78
|
+
{ name: 'smartmemory', aliases: [], group: 'App, integrations & runtime', summary: 'Sync feature-events/journal/artifacts into SmartMemory' },
|
|
79
|
+
|
|
80
|
+
// Maintenance & info
|
|
81
|
+
{ name: 'migrate-state', aliases: [], group: 'Maintenance & info', summary: 'Run pending feature.json state migrations' },
|
|
82
|
+
{ name: 'migrate-anon', aliases: [], group: 'Maintenance & info', summary: 'Promote anonymous ROADMAP rows to typed features (interactive)' },
|
|
83
|
+
{ name: 'hooks', aliases: [], group: 'Maintenance & info', summary: 'Manage Claude Code hooks (install | uninstall | status)' },
|
|
84
|
+
{ name: 'version', aliases: ['--version', '-V'], group: 'Maintenance & info', summary: 'Print compose version, git SHA, and install root' },
|
|
85
|
+
];
|
|
86
|
+
|
|
87
|
+
/** Every token (names + aliases) that should dispatch to a command. */
|
|
88
|
+
export function allCommandTokens() {
|
|
89
|
+
return COMMANDS.flatMap((c) => [c.name, ...c.aliases]);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** A command's display label: `name` plus any aliases, e.g. `setup, sync`. */
|
|
93
|
+
function labelOf(cmd) {
|
|
94
|
+
return cmd.aliases.length ? `${cmd.name}, ${cmd.aliases.join(', ')}` : cmd.name;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Render the grouped `compose --help` body from COMMANDS.
|
|
99
|
+
* @returns {string}
|
|
100
|
+
*/
|
|
101
|
+
export function renderHelp() {
|
|
102
|
+
const lines = ['Usage: compose <command>', ''];
|
|
103
|
+
const width = Math.max(...COMMANDS.map((c) => labelOf(c).length));
|
|
104
|
+
for (const group of COMMAND_GROUPS) {
|
|
105
|
+
const inGroup = COMMANDS.filter((c) => c.group === group);
|
|
106
|
+
if (!inGroup.length) continue;
|
|
107
|
+
lines.push(`${group}:`);
|
|
108
|
+
for (const cmd of inGroup) {
|
|
109
|
+
lines.push(` ${labelOf(cmd).padEnd(width)} ${cmd.summary}`);
|
|
110
|
+
}
|
|
111
|
+
lines.push('');
|
|
112
|
+
}
|
|
113
|
+
lines.push('Run `compose <command> --help` (or see docs/cli.md) for command details.');
|
|
114
|
+
return lines.join('\n');
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Render the docs/cli.md Command Index — a complete, grouped table generated
|
|
119
|
+
* from COMMANDS. The completeness test in test/cli-commands.test.js asserts
|
|
120
|
+
* every command appears in cli.md, so this table cannot silently omit one.
|
|
121
|
+
* @returns {string}
|
|
122
|
+
*/
|
|
123
|
+
export function renderCommandIndex() {
|
|
124
|
+
const out = [
|
|
125
|
+
'## Command Index',
|
|
126
|
+
'',
|
|
127
|
+
'<!-- Generated from lib/cli-commands.js (COMP-AUDIT-13). Every shipped command appears here. -->',
|
|
128
|
+
'',
|
|
129
|
+
];
|
|
130
|
+
for (const group of COMMAND_GROUPS) {
|
|
131
|
+
const inGroup = COMMANDS.filter((c) => c.group === group);
|
|
132
|
+
if (!inGroup.length) continue;
|
|
133
|
+
out.push(`### ${group}`, '', '| Command | Summary |', '|---|---|');
|
|
134
|
+
for (const cmd of inGroup) {
|
|
135
|
+
const label = cmd.aliases.length
|
|
136
|
+
? `\`compose ${cmd.name}\` (alias: ${cmd.aliases.map((a) => `\`${a}\``).join(', ')})`
|
|
137
|
+
: `\`compose ${cmd.name}\``;
|
|
138
|
+
// Escape pipes so a summary like "stamp | stale | show" can't break the table.
|
|
139
|
+
out.push(`| ${label} | ${cmd.summary.replace(/\|/g, '\\|')} |`);
|
|
140
|
+
}
|
|
141
|
+
out.push('');
|
|
142
|
+
}
|
|
143
|
+
return out.join('\n');
|
|
144
|
+
}
|
package/lib/codex-preflight.js
CHANGED
|
@@ -115,29 +115,40 @@ export async function preflightCodexWorktreeProbe({
|
|
|
115
115
|
// Unique per-run filename so a file committed in the repo (carried into the
|
|
116
116
|
// detached worktree from HEAD) can never make the probe false-pass.
|
|
117
117
|
const probeName = `codex-probe-${ts}.txt`;
|
|
118
|
+
let terminationUnconfirmed = false;
|
|
118
119
|
try {
|
|
119
120
|
const prompt =
|
|
120
121
|
`Write a file named ${probeName} in the current working directory whose exact ` +
|
|
121
122
|
`contents are the single line: ${PROBE_SENTINEL}\n` +
|
|
122
123
|
`Do nothing else. This is an environment write-probe.`;
|
|
123
124
|
try {
|
|
124
|
-
//
|
|
125
|
-
//
|
|
126
|
-
|
|
127
|
-
|
|
125
|
+
// Wait for acknowledged termination before removing the worktree. A
|
|
126
|
+
// Promise.race timeout could delete it while the agent was still writing.
|
|
127
|
+
const controller = new AbortController();
|
|
128
|
+
const timer = setTimeout(() => controller.abort(), PROBE_AGENT_TIMEOUT_MS);
|
|
129
|
+
timer.unref?.();
|
|
130
|
+
try {
|
|
131
|
+
await stratum.runAgentText('codex', prompt, {
|
|
128
132
|
cwd: wtPath,
|
|
133
|
+
sandboxMode: 'workspace-write',
|
|
134
|
+
signal: controller.signal,
|
|
129
135
|
telemetry: {
|
|
130
136
|
site: 'preflight',
|
|
131
137
|
project_cwd: projectCwd,
|
|
132
138
|
build_id: buildId,
|
|
133
139
|
feature_code: featureCode,
|
|
134
140
|
},
|
|
135
|
-
})
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
]);
|
|
141
|
+
});
|
|
142
|
+
} finally {
|
|
143
|
+
clearTimeout(timer);
|
|
144
|
+
}
|
|
140
145
|
} catch (err) {
|
|
146
|
+
if (['CANCELLATION_UNCONFIRMED', 'CANCELLATION_TEARDOWN_TIMEOUT'].includes(err?.code)) {
|
|
147
|
+
terminationUnconfirmed = true;
|
|
148
|
+
err.worktreePath = wtPath;
|
|
149
|
+
err.message += `; probe worktree retained at ${wtPath}`;
|
|
150
|
+
throw err;
|
|
151
|
+
}
|
|
141
152
|
const result = { ok: false, reason: `Codex agent run failed/timed out inside the worktree: ${err?.message ?? err}` };
|
|
142
153
|
writeProbeCache(dataDir, result);
|
|
143
154
|
return result;
|
|
@@ -153,10 +164,12 @@ export async function preflightCodexWorktreeProbe({
|
|
|
153
164
|
writeProbeCache(dataDir, result);
|
|
154
165
|
return result;
|
|
155
166
|
} finally {
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
167
|
+
if (!terminationUnconfirmed) {
|
|
168
|
+
try {
|
|
169
|
+
execSync(`git worktree remove "${wtPath}" --force`, { cwd, encoding: 'utf-8', timeout: 30_000, stdio: 'pipe' });
|
|
170
|
+
} catch {
|
|
171
|
+
try { execSync(`rm -rf "${wtPath}"`, { encoding: 'utf-8', timeout: 10_000 }); } catch { /* give up */ }
|
|
172
|
+
}
|
|
160
173
|
}
|
|
161
174
|
}
|
|
162
175
|
}
|