@smartmemory/compose 0.3.6-beta → 0.3.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/skills/compose/SKILL.md +42 -88
- package/.compose-deps.json +1 -13
- package/README.md +72 -5
- package/bin/compose.js +754 -347
- package/bin/git-hooks/pre-push.template +29 -0
- package/bin/judgment-import.js +7 -0
- package/bin/judgment-migrate.js +387 -0
- package/contracts/comp-obs-contract.schema.json +9 -3
- package/contracts/feature-json.schema.json +5 -0
- package/contracts/fluid-record.schema.json +209 -0
- package/contracts/judgment-record.schema.json +425 -4
- package/contracts/lifecycle-backfill.schema.json +322 -0
- package/dist/assets/App-Z4MU-H_F.js +916 -0
- package/dist/assets/_baseUniq-ClWoCPFl.js +1 -0
- package/dist/assets/arc-DY26UIVo.js +1 -0
- package/dist/assets/architectureDiagram-Q4EWVU46-6Ggq4DqJ.js +36 -0
- package/dist/assets/blockDiagram-DXYQGD6D-CH3Ked0l.js +132 -0
- package/dist/assets/{browser-BSM23If2.js → browser-BWkrenen.js} +6 -6
- package/dist/assets/{c4Diagram-LMCZKHZV-DZf45Fbz.js → c4Diagram-AHTNJAMY-Bk8dYilu.js} +1 -1
- package/dist/assets/channel-SnZzzh7k.js +1 -0
- package/dist/assets/{chunk-JWPE2WC7-_7ujgd_Q.js → chunk-4BX2VUAB-BMR0XaAQ.js} +1 -1
- package/dist/assets/chunk-4TB4RGXK-JytR14a9.js +206 -0
- package/dist/assets/{chunk-XXDRQBXY-DfdVhbmA.js → chunk-55IACEB6-B4Q97BCP.js} +1 -1
- package/dist/assets/{chunk-VR4S4FIN-Dt9NZ67m.js → chunk-EDXVE4YY-R_qarkSf.js} +1 -1
- package/dist/assets/{chunk-5VM5RSS4-BY4_PV5H.js → chunk-FMBD7UC4-C9s7KR9m.js} +1 -1
- package/dist/assets/chunk-OYMX7WX6-BySQzVxc.js +231 -0
- package/dist/assets/{chunk-2Q5K7J3B-Dn1spZYu.js → chunk-QZHKN3VN-DdpSYZsW.js} +1 -1
- package/dist/assets/{chunk-32BRIVSS-pURGrJDk.js → chunk-YZCP3GAM-iE_tzriw.js} +1 -1
- package/dist/assets/classDiagram-6PBFFD2Q-CBu92dSH.js +1 -0
- package/dist/assets/classDiagram-v2-HSJHXN6E-CBu92dSH.js +1 -0
- package/dist/assets/clone-DgklGjHm.js +1 -0
- package/dist/assets/{cose-bilkent-JH36ORCC-BieYif4o.js → cose-bilkent-S5V4N54A-BdlU6ZX_.js} +1 -1
- package/dist/assets/dagre-KV5264BT-Cp3F5KTn.js +4 -0
- package/dist/assets/diagram-5BDNPKRD-DiR6_2q_.js +10 -0
- package/dist/assets/diagram-G4DWMVQ6-w0i-p5HX.js +24 -0
- package/dist/assets/diagram-MMDJMWI5-tIHhwUv3.js +43 -0
- package/dist/assets/diagram-TYMM5635-BAeY3B19.js +24 -0
- package/dist/assets/erDiagram-SMLLAGMA-Ckx_Knko.js +85 -0
- package/dist/assets/flowDiagram-DWJPFMVM-DeoNka6J.js +162 -0
- package/dist/assets/ganttDiagram-T4ZO3ILL-BmGnFbEg.js +292 -0
- package/dist/assets/gitGraphDiagram-UUTBAWPF-Dk48IHsx.js +106 -0
- package/dist/assets/graph-BNzKGvoy.js +1 -0
- package/dist/assets/graph-CI_1htl0.js +331 -0
- package/dist/assets/index-BEfrNBp8.js +123 -0
- package/dist/assets/index-yyrA5OZd.css +1 -0
- package/dist/assets/infoDiagram-42DDH7IO-BRf827i0.js +2 -0
- package/dist/assets/{ishikawaDiagram-FXEZZL3T-CzEB9fQS.js → ishikawaDiagram-UXIWVN3A-0kCZaeCM.js} +5 -5
- package/dist/assets/{journeyDiagram-5HDEW3XC-Bz8TCdz2.js → journeyDiagram-VCZTEJTY-rvU7ayRt.js} +1 -1
- package/dist/assets/{kanban-definition-HUTT4EX6-tozrMoV_.js → kanban-definition-6JOO6SKY-DpQwX1C5.js} +7 -7
- package/dist/assets/katex-DkKDou_j.js +257 -0
- package/dist/assets/layout-BI8cXFPI.js +1 -0
- package/dist/assets/{linear-Ck7gpa5N.js → linear-a0glcDiw.js} +1 -1
- package/dist/assets/min-vPHfnXcC.js +1 -0
- package/dist/assets/{mindmap-definition-LN4V7U3C-DTcHO0DJ.js → mindmap-definition-QFDTVHPH-D14eF-7C.js} +7 -7
- package/dist/assets/mobile-B7m9EO9D.js +17 -0
- package/dist/assets/pieDiagram-DEJITSTG-Cno-gETh.js +30 -0
- package/dist/assets/quadrantDiagram-34T5L4WZ-BUQM1Hfm.js +7 -0
- package/dist/assets/{requirementDiagram-TGXJPOKE-bnI2zJeT.js → requirementDiagram-MS252O5E-pOXlN2-q.js} +3 -3
- package/dist/assets/sankeyDiagram-XADWPNL6-Crynd3_b.js +10 -0
- package/dist/assets/sequenceDiagram-FGHM5R23-D9fZdCM8.js +157 -0
- package/dist/assets/stateDiagram-FHFEXIEX-CW9qVec8.js +1 -0
- package/dist/assets/stateDiagram-v2-QKLJ7IA2-DkVLzHbY.js +1 -0
- package/dist/assets/{timeline-definition-FHXFAJF6-D267GQFF.js → timeline-definition-GMOUNBTQ-BcHzhm_8.js} +3 -3
- package/dist/assets/vennDiagram-DHZGUBPP-BfytJcWk.js +34 -0
- package/dist/assets/wardley-RL74JXVD-DLj-IjyB.js +162 -0
- package/dist/assets/wardleyDiagram-NUSXRM2D-Ds0Ue68c.js +20 -0
- package/dist/assets/xychartDiagram-5P7HB3ND-vjWDXFL6.js +7 -0
- package/dist/index.html +3 -3
- package/lib/agent-string.js +7 -5
- package/lib/append-integrity.js +81 -0
- package/lib/backfill-evidence.js +109 -0
- package/lib/bug-escalation.js +39 -4
- package/lib/build-stream-schema.js +3 -1
- package/lib/build-stream-writer.js +25 -0
- package/lib/build.js +1624 -195
- package/lib/canon-guard.js +245 -0
- package/lib/canon-override.js +196 -0
- package/lib/canon-registry.js +291 -0
- package/lib/cli-commands.js +144 -0
- package/lib/codex-preflight.js +50 -15
- package/lib/colleague/context.js +215 -0
- package/lib/colleague/writeback.js +95 -0
- package/lib/completion-gate.js +1421 -0
- package/lib/completion-writer.js +47 -47
- package/lib/consumer-fanout.js +105 -11
- package/lib/coverage-gate.js +200 -0
- package/lib/dir-lock.js +170 -0
- package/lib/dispatch-ledger.js +301 -0
- package/lib/dispatch-metrics.js +236 -0
- package/lib/experiment-judge.js +6 -1
- package/lib/feature-json.js +1 -1
- package/lib/feature-reconciler.js +8 -0
- package/lib/feature-validator.js +64 -1
- package/lib/feature-writer.js +66 -2
- package/lib/fluid/factory.js +167 -0
- package/lib/fluid/ideabox-dates.js +73 -0
- package/lib/fluid/ideabox-migrate.js +154 -0
- package/lib/fluid/ideabox-ops.js +585 -0
- package/lib/fluid/ideabox-view.js +146 -0
- package/lib/fluid/import-ideabox.js +186 -0
- package/lib/fluid/local-provider.js +606 -0
- package/lib/fluid/provider.js +684 -0
- package/lib/fluid/record-shape.js +214 -0
- package/lib/fluid/record-store.js +328 -0
- package/lib/fluid/render-ideabox.js +261 -0
- package/lib/fluid/schema.js +40 -0
- package/lib/fluid/smartmemory-provider.js +1695 -0
- package/lib/gsd.js +63 -14
- package/lib/guard-cli.js +175 -0
- package/lib/guard-custody.js +141 -0
- package/lib/guard-descriptors.js +530 -0
- package/lib/guard-enrol.js +254 -0
- package/lib/health-score.js +1 -1
- package/lib/hooks-status.js +32 -3
- package/lib/ideabox-cli.js +315 -0
- package/lib/ideabox.js +121 -21
- package/lib/judgment/store/index.js +166 -0
- package/lib/judgment/store/records.js +184 -25
- package/lib/judgment/trace.js +380 -0
- package/lib/judgment-attest.js +259 -0
- package/lib/judgment-decision-write.js +277 -0
- package/lib/judgment-decisions.js +466 -0
- package/lib/judgment-gen.js +375 -22
- package/lib/judgment-verify.js +153 -0
- package/lib/judgment-writer.js +2842 -262
- package/lib/lane-gate.js +2 -0
- package/lib/lifecycle-modes.js +4 -4
- package/lib/lineage.js +400 -0
- package/lib/local-claude-connector.js +250 -54
- package/lib/maya-client.js +302 -0
- package/lib/maya-config.js +53 -0
- package/lib/maya-identity.js +283 -0
- package/lib/mcp-enforcement.js +21 -35
- package/lib/migrate-anon.js +5 -0
- package/lib/migrate-roadmap.js +15 -0
- package/lib/new.js +13 -1
- package/lib/pipeline-compat.js +104 -0
- package/lib/policy-catalog.js +295 -0
- package/lib/policy-check.js +0 -0
- package/lib/process-termination.js +98 -0
- package/lib/resolve-workspace.js +5 -1
- package/lib/result-normalizer.js +428 -153
- package/lib/review-normalize.js +4 -0
- package/lib/roadmap-errors.js +65 -0
- package/lib/roadmap-preservers.js +24 -4
- package/lib/roadmap-residue.js +299 -0
- package/lib/smartmemory-client.js +614 -78
- package/lib/smartmemory-config.js +54 -0
- package/lib/smartmemory-ingest.js +19 -2
- package/lib/step-prompt.js +7 -6
- package/lib/stratum-engine.js +53 -4
- package/lib/stratum-mcp-client.js +391 -31
- package/lib/test-bootstrap.js +31 -0
- package/lib/tool-inventory.js +122 -0
- package/lib/version-check.js +91 -19
- package/lib/vision-writer.js +88 -1
- package/package.json +7 -6
- package/pipelines/bug-fix.stratum.yaml +205 -211
- package/pipelines/build-quick.profiles.json +12 -0
- package/pipelines/build-quick.stratum.yaml +263 -350
- package/pipelines/content.stratum.yaml +81 -77
- package/pipelines/coverage-sweep.stratum.yaml +49 -30
- package/pipelines/plan.stratum.yaml +76 -86
- package/pipelines/refactor.stratum.yaml +125 -125
- package/pipelines/research.stratum.yaml +56 -58
- package/pipelines/review-fix.profiles.json +6 -0
- package/pipelines/review-fix.stratum.yaml +110 -83
- package/presets/team-feature.profiles.json +6 -0
- package/presets/team-feature.stratum.yaml +93 -66
- package/presets/team-research.profiles.json +6 -0
- package/presets/team-research.stratum.yaml +89 -80
- package/presets/team-review.profiles.json +8 -0
- package/presets/team-review.stratum.yaml +98 -80
- package/scripts/cost-census.mjs +70 -0
- package/scripts/guard-sign/compose-guard-sign.sh +62 -0
- package/server/agent-health.js +22 -0
- package/server/agent-hooks.js +14 -1
- package/server/agent-server.js +5 -248
- package/server/agent-spawn.js +3 -4
- package/server/agent-workspace.js +294 -0
- package/server/build-routes.js +6 -5
- package/server/build-stream-bridge.js +53 -0
- package/server/cc-session-watcher.js +4 -1
- package/server/coalescing-buffer.js +7 -1
- package/server/completion-projection.js +228 -0
- package/server/compose-mcp-tools.js +124 -24
- package/server/compose-mcp.js +91 -790
- package/server/decision-event-emit.js +41 -2
- package/server/decision-event-id.js +17 -0
- package/server/decision-events-snapshot.js +3 -0
- package/server/design-routes.js +14 -8
- package/server/feature-scan.js +76 -2
- package/server/file-watcher.js +170 -21
- package/server/ideabox-routes.js +166 -224
- package/server/index.js +70 -100
- package/server/lifecycle-guard.js +240 -10
- package/server/lifecycle-phase-history.js +276 -0
- package/server/maya-routes.js +507 -0
- package/server/mcp-tool-defs.js +940 -0
- package/server/mcp-tool-policy.js +35 -3
- package/server/model-tiers.js +22 -5
- package/server/pipeline-routes.js +21 -11
- package/server/project-root.js +58 -19
- package/server/remote-utils.js +3 -1
- package/server/schema-validator.js +7 -1
- package/server/session-manager.js +5 -6
- package/server/session-routes.js +3 -1
- package/server/stratum-client.js +57 -10
- package/server/stratum-sync.js +6 -3
- package/server/summarizer.js +3 -4
- package/server/supervisor.js +0 -1
- package/server/vision-routes.js +208 -98
- package/server/vision-server.js +86 -23
- package/server/vision-store.js +60 -6
- package/server/vision-utils.js +3 -4
- package/server/workspace-activity.js +18 -0
- package/server/workspace-middleware.js +2 -2
- package/server/workspace-runtime.js +243 -0
- package/server/worktree-gc.js +1 -0
- package/dist/assets/App-BG3ngu8H.js +0 -896
- package/dist/assets/abnfDiagram-VRR7QNED-CjB_sD3D.js +0 -1
- package/dist/assets/arc-_v4hR_uD.js +0 -1
- package/dist/assets/architectureDiagram-ZJ3FMSHR-DreJmzXQ.js +0 -36
- package/dist/assets/blockDiagram-677ZJIJ3-BG9-c0O1.js +0 -132
- package/dist/assets/channel-B3U5wFAT.js +0 -1
- package/dist/assets/chunk-EX3LRPZG-DdELs1qP.js +0 -231
- package/dist/assets/chunk-MOJQB5TN-D-ky35G-.js +0 -88
- package/dist/assets/chunk-RYQCIY6F-Dag_kVlO.js +0 -1
- package/dist/assets/chunk-V7JOEXUC-BtewURat.js +0 -206
- package/dist/assets/classDiagram-OUVF2IWQ-B6fCN-ht.js +0 -1
- package/dist/assets/classDiagram-v2-EOCWNBFH-B6fCN-ht.js +0 -1
- package/dist/assets/cynefin-VYW2F7L2-CT2BA6KE.js +0 -178
- package/dist/assets/cynefinDiagram-TSTJHNR4-Bh6exbyg.js +0 -62
- package/dist/assets/dagre-VKFMJZFB-aXMLSmQL.js +0 -4
- package/dist/assets/diagram-FQU43EPY-Dr7JAOuQ.js +0 -3
- package/dist/assets/diagram-G47NLZAW-DUvA3FQK.js +0 -24
- package/dist/assets/diagram-NH7WQ7WH-BQUARqcu.js +0 -24
- package/dist/assets/diagram-OA4YK3LP-dDUc1zHi.js +0 -30
- package/dist/assets/diagram-WEI45ONY-B2h5Qlb1.js +0 -41
- package/dist/assets/ebnfDiagram-CCIWWBDH-DThRGupB.js +0 -1
- package/dist/assets/erDiagram-Q63AITRT-BUCsprO2.js +0 -85
- package/dist/assets/flowDiagram-23GEKE2U-DXtNNi6r.js +0 -156
- package/dist/assets/ganttDiagram-NO4QXBWP-D4zbBHh_.js +0 -292
- package/dist/assets/gitGraphDiagram-IHSO6WYX-DpoQws0W.js +0 -106
- package/dist/assets/graph-BXPQrYYB.js +0 -331
- package/dist/assets/graph-C9eacEi8.js +0 -1
- package/dist/assets/index-3ZH5eMcZ.js +0 -119
- package/dist/assets/index-LIwREYgH.css +0 -1
- package/dist/assets/infoDiagram-FWYZ7A6U-Bbas2GAo.js +0 -2
- package/dist/assets/katex-C5jXJg4s.js +0 -257
- package/dist/assets/layout-DEXfKzaS.js +0 -1
- package/dist/assets/map-Czzmt4hB.js +0 -1
- package/dist/assets/mobile-CaoXUwAr.js +0 -17
- package/dist/assets/pegDiagram-2B236MQR-CHiINrNy.js +0 -1
- package/dist/assets/pieDiagram-ENE6RG2P-CfS4YFlR.js +0 -39
- package/dist/assets/quadrantDiagram-ABIIQ3AL-CadesS9w.js +0 -7
- package/dist/assets/railroadDiagram-RFXS5EU6-CgWEspBN.js +0 -1
- package/dist/assets/sankeyDiagram-HTMAVEWB-YWKFgOGw.js +0 -40
- package/dist/assets/sequenceDiagram-DBY2YBRQ-BvkNOyF9.js +0 -162
- package/dist/assets/sizeCapture-X5ZJPWSS-DlFPA2yO.js +0 -1
- package/dist/assets/stateDiagram-2N3HPSRC-h8NIx0kQ.js +0 -1
- package/dist/assets/stateDiagram-v2-6OUMAXLB-DjPgZtJ9.js +0 -1
- package/dist/assets/swimlanes-5IMT3BWC-CT5n22kG.js +0 -2
- package/dist/assets/swimlanesDiagram-G3AALYLV-Dn318Bhq.js +0 -8
- package/dist/assets/vennDiagram-L72KCM5P-Dj-wWLYG.js +0 -34
- package/dist/assets/wardleyDiagram-EHGQE667-BxCeYxkG.js +0 -78
- package/dist/assets/xychartDiagram-FW5EYKEG-DMFqWn7z.js +0 -7
- package/lib/staleness.js +0 -87
- package/server/ideabox-cache.js +0 -77
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* maya-config.js — COMP-FOH FOH-6 S1
|
|
3
|
+
*
|
|
4
|
+
* Config readers for the Maya colleague coupling. Leaf module (no fetch, no
|
|
5
|
+
* state writes), mirroring lib/smartmemory-config.js: uncached direct reads of
|
|
6
|
+
* `.compose/compose.json`, try/catch → absent on missing/malformed.
|
|
7
|
+
*
|
|
8
|
+
* The feature switch is the PRESENCE of the `maya` block (design-foh-6.md
|
|
9
|
+
* §Config): absent means "not installed" (no chrome button), which is a
|
|
10
|
+
* different state from "installed but degraded" (a funnel).
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { readFileSync } from 'node:fs';
|
|
14
|
+
import { join } from 'node:path';
|
|
15
|
+
|
|
16
|
+
function readComposeJson(cwd) {
|
|
17
|
+
try {
|
|
18
|
+
return JSON.parse(readFileSync(join(cwd, '.compose', 'compose.json'), 'utf-8'));
|
|
19
|
+
} catch {
|
|
20
|
+
return {};
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* The `maya` block, or null when the feature is not installed.
|
|
26
|
+
* @param {string} cwd
|
|
27
|
+
* @returns {{ baseUrl?: string, auth?: { mode?: 'provision'|'static' } } | null}
|
|
28
|
+
*/
|
|
29
|
+
export function getMayaConfig(cwd) {
|
|
30
|
+
const block = readComposeJson(cwd).maya;
|
|
31
|
+
return block && typeof block === 'object' ? block : null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* The fluid SmartMemory workspace id — the value the colleague identity's
|
|
36
|
+
* workspace claim must NOT equal (shallow-binding isolation, design §2).
|
|
37
|
+
* @param {string} cwd
|
|
38
|
+
* @returns {string | null}
|
|
39
|
+
*/
|
|
40
|
+
export function getFluidWorkspaceId(cwd) {
|
|
41
|
+
const ws = readComposeJson(cwd).fluid?.smartmemory?.workspaceId;
|
|
42
|
+
return typeof ws === 'string' && ws ? ws : null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Whether the fluid provider is SmartMemory-backed. The colleague never runs
|
|
47
|
+
* degraded (COLLEAGUE-ALL-IN): on the local floor the panel is a
|
|
48
|
+
* connect-SmartMemory funnel, not a plain chat.
|
|
49
|
+
* @param {string} cwd
|
|
50
|
+
*/
|
|
51
|
+
export function hasSmartmemoryFluidProvider(cwd) {
|
|
52
|
+
return readComposeJson(cwd).fluid?.provider === 'smartmemory';
|
|
53
|
+
}
|
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* maya-identity.js — COMP-FOH FOH-6 S1
|
|
3
|
+
*
|
|
4
|
+
* The colleague identity store: `data/maya-identity.json` (covered by
|
|
5
|
+
* `.gitignore`'s blanket `data/` entry). One identity per project; it OWNS
|
|
6
|
+
* Maya's single standing conversation (session per JWT identity,
|
|
7
|
+
* session_registry.py:486-527), so nothing here re-provisions implicitly —
|
|
8
|
+
* minting a fresh identity destroys the thread AND the colleague-workspace
|
|
9
|
+
* memory, and is only ever an explicit user action (design §2).
|
|
10
|
+
*
|
|
11
|
+
* Provisioning uses smart-memory-service's sanctioned test surface, the same
|
|
12
|
+
* path the FOH-4/5/6 live-fires used:
|
|
13
|
+
* POST /test/provision-user {email} → {user_id, tenant_id, team_id, access_token}
|
|
14
|
+
* (409 on a repeated email — probed live 2026-08-11; there is no
|
|
15
|
+
* token-refresh path on the test surface yet, upstream ask filed with the
|
|
16
|
+
* channel_context issue)
|
|
17
|
+
* POST /memory/beta/nda/accept {"version":"v1"} (Bearer-authenticated)
|
|
18
|
+
* DELETE /test/provision-user {email, user_id, tenant_id}
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { readFileSync, writeFileSync, renameSync, rmSync, mkdirSync, chmodSync } from 'node:fs';
|
|
22
|
+
import { join } from 'node:path';
|
|
23
|
+
import { randomBytes } from 'node:crypto';
|
|
24
|
+
|
|
25
|
+
/** Provisioning/NDA failure. `kind` mirrors the relay's funnel taxonomy. */
|
|
26
|
+
export class MayaIdentityError extends Error {
|
|
27
|
+
constructor(message, kind = 'auth') {
|
|
28
|
+
super(message);
|
|
29
|
+
this.name = 'MayaIdentityError';
|
|
30
|
+
this.kind = kind;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** The colleague token's workspace claim equals the fluid workspace —
|
|
35
|
+
* accepting it would silently enable the deferred deep-binding behaviour
|
|
36
|
+
* (Maya's turn-ingestion writing into the fluid workspace). */
|
|
37
|
+
export class MayaWorkspaceCollisionError extends Error {
|
|
38
|
+
constructor(claim) {
|
|
39
|
+
super(
|
|
40
|
+
`maya: the colleague token's workspace claim (${claim}) IS the fluid workspace — `
|
|
41
|
+
+ 'refusing. Shallow binding requires a dedicated colleague workspace; '
|
|
42
|
+
+ 'provision a separate identity or paste a token scoped elsewhere.',
|
|
43
|
+
);
|
|
44
|
+
this.name = 'MayaWorkspaceCollisionError';
|
|
45
|
+
this.claim = claim;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function identityPath(projectRoot) {
|
|
50
|
+
return join(projectRoot, 'data', 'maya-identity.json');
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** @returns {object|null} the stored identity, or null when none exists */
|
|
54
|
+
export function loadIdentity(projectRoot) {
|
|
55
|
+
const path = identityPath(projectRoot);
|
|
56
|
+
try {
|
|
57
|
+
const parsed = JSON.parse(readFileSync(path, 'utf-8'));
|
|
58
|
+
if (!parsed || typeof parsed !== 'object') return null;
|
|
59
|
+
// Enforce owner-only permissions on files created before the 0600 write
|
|
60
|
+
// path existed (Codex r1 P2). Best-effort — a read-only checkout must not
|
|
61
|
+
// break loading.
|
|
62
|
+
try { chmodSync(path, 0o600); } catch { /* best-effort */ }
|
|
63
|
+
return parsed;
|
|
64
|
+
} catch {
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Owner-only (0600) and ATOMIC: the file holds a bearer credential, so it must
|
|
71
|
+
* never be group/world-readable, and a crash mid-write must never truncate it
|
|
72
|
+
* — a truncated file loads as null and the next turn would silently provision
|
|
73
|
+
* a fresh identity, destroying the standing conversation (Codex r1 P2).
|
|
74
|
+
*/
|
|
75
|
+
export function saveIdentity(projectRoot, identity) {
|
|
76
|
+
mkdirSync(join(projectRoot, 'data'), { recursive: true });
|
|
77
|
+
const path = identityPath(projectRoot);
|
|
78
|
+
const tmp = `${path}.tmp.${randomBytes(6).toString('hex')}`;
|
|
79
|
+
try {
|
|
80
|
+
writeFileSync(tmp, JSON.stringify(identity, null, 2) + '\n', { mode: 0o600 });
|
|
81
|
+
renameSync(tmp, path);
|
|
82
|
+
} catch (err) {
|
|
83
|
+
rmSync(tmp, { force: true });
|
|
84
|
+
throw err;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function clearIdentity(projectRoot) {
|
|
89
|
+
rmSync(identityPath(projectRoot), { force: true });
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
async function requestJson(url, { body, token, timeoutMs = 30000, fetchFn = fetch, method = 'POST' }) {
|
|
93
|
+
const controller = new AbortController();
|
|
94
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
95
|
+
try {
|
|
96
|
+
const res = await fetchFn(url, {
|
|
97
|
+
method,
|
|
98
|
+
headers: {
|
|
99
|
+
...(body === undefined ? {} : { 'Content-Type': 'application/json' }),
|
|
100
|
+
...(token ? { Authorization: `Bearer ${token}` } : {}),
|
|
101
|
+
},
|
|
102
|
+
...(body === undefined ? {} : { body: JSON.stringify(body) }),
|
|
103
|
+
signal: controller.signal,
|
|
104
|
+
});
|
|
105
|
+
const text = await res.text();
|
|
106
|
+
let parsed = null;
|
|
107
|
+
try { parsed = JSON.parse(text); } catch { /* tolerated; shape-checked below */ }
|
|
108
|
+
return { status: res.status, body: parsed };
|
|
109
|
+
} catch (err) {
|
|
110
|
+
throw new MayaIdentityError(`maya: ${method} ${url} failed: ${err?.message ?? 'unknown'}`);
|
|
111
|
+
} finally {
|
|
112
|
+
clearTimeout(timer);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Mint a fresh colleague identity. A RANDOM email per call — the provision
|
|
118
|
+
* surface refuses repeats (409), and a colleague identity is never shared
|
|
119
|
+
* across projects (documented-unsupported, design §2).
|
|
120
|
+
*
|
|
121
|
+
* Does NOT save: the caller decides persistence (ensureIdentity does both).
|
|
122
|
+
*/
|
|
123
|
+
export async function provisionIdentity({ smBaseUrl, fetchFn = fetch }) {
|
|
124
|
+
const email = `maya-colleague-${randomBytes(6).toString('hex')}@compose.invalid`;
|
|
125
|
+
const res = await requestJson(`${smBaseUrl}/test/provision-user`, { body: { email }, fetchFn });
|
|
126
|
+
if (res.status !== 200 || typeof res.body?.access_token !== 'string') {
|
|
127
|
+
throw new MayaIdentityError(
|
|
128
|
+
`maya: provisioning failed (HTTP ${res.status})`
|
|
129
|
+
+ (res.body?.detail ? ` — ${String(res.body.detail).slice(0, 200)}` : ''),
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
const { user_id, tenant_id, team_id, access_token } = res.body;
|
|
133
|
+
return {
|
|
134
|
+
mode: 'provision', email, user_id, tenant_id, team_id, access_token,
|
|
135
|
+
ndaAccepted: false, provisionedAt: new Date().toISOString(),
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** Accept the beta NDA for a provisioned identity (403 `nda_required` gates
|
|
140
|
+
* every memory route until this runs — FOH-4 ledger prereq 3). */
|
|
141
|
+
export async function acceptNda({ smBaseUrl, token, fetchFn = fetch }) {
|
|
142
|
+
const res = await requestJson(`${smBaseUrl}/memory/beta/nda/accept`, {
|
|
143
|
+
body: { version: 'v1' }, token, fetchFn,
|
|
144
|
+
});
|
|
145
|
+
if (res.status < 200 || res.status >= 300) {
|
|
146
|
+
throw new MayaIdentityError(`maya: NDA accept failed (HTTP ${res.status})`);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** Tear down a provisioned identity (explicit user action only). */
|
|
151
|
+
export async function teardownIdentity(projectRoot, { smBaseUrl, fetchFn = fetch }) {
|
|
152
|
+
const identity = loadIdentity(projectRoot);
|
|
153
|
+
if (!identity) return { removed: false };
|
|
154
|
+
if (identity.mode === 'provision' && identity.email) {
|
|
155
|
+
await requestJson(`${smBaseUrl}/test/provision-user`, {
|
|
156
|
+
method: 'DELETE', fetchFn,
|
|
157
|
+
body: { email: identity.email, user_id: identity.user_id, tenant_id: identity.tenant_id },
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
clearIdentity(projectRoot);
|
|
161
|
+
return { removed: true };
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* The identity's workspace claim, or null when none is derivable. Provisioned
|
|
166
|
+
* identities carry `team_id` from the provision response (VERIFY-1: the JWT
|
|
167
|
+
* payload itself carries NO workspace claims — Maya resolves the workspace
|
|
168
|
+
* server-side from the verified user record). Static tokens fall back to
|
|
169
|
+
* decoding the JWT body for the claims Maya's `_extract_team_id` reads.
|
|
170
|
+
*/
|
|
171
|
+
export function workspaceClaimOf(identity) {
|
|
172
|
+
if (!identity) return null;
|
|
173
|
+
if (typeof identity.team_id === 'string' && identity.team_id) return identity.team_id;
|
|
174
|
+
const token = identity.access_token;
|
|
175
|
+
if (typeof token !== 'string') return null;
|
|
176
|
+
const parts = token.split('.');
|
|
177
|
+
if (parts.length < 2) return null;
|
|
178
|
+
try {
|
|
179
|
+
const payload = JSON.parse(Buffer.from(parts[1], 'base64url').toString());
|
|
180
|
+
return payload.workspace_id ?? payload.current_workspace_id
|
|
181
|
+
?? payload.default_team_id ?? payload.team_id ?? null;
|
|
182
|
+
} catch {
|
|
183
|
+
return null;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* The shallow-binding invariant (design §2, VERIFY-3): refuse a colleague
|
|
189
|
+
* token whose workspace claim equals the fluid workspace. An underivable claim
|
|
190
|
+
* is ALLOWED here for STORED identities — provisioned identities always carry
|
|
191
|
+
* `team_id`, and static identities have their claim resolved and stored at
|
|
192
|
+
* paste time by `fetchVerifiedWorkspace` (which fails CLOSED — see below), so
|
|
193
|
+
* an underivable claim on a stored identity means only that this project has
|
|
194
|
+
* no fluid workspace to collide with. Cross-project token reuse remains
|
|
195
|
+
* documented-unsupported, not detected.
|
|
196
|
+
*/
|
|
197
|
+
export function validateWorkspaceIsolation(identity, fluidWorkspaceId) {
|
|
198
|
+
if (!fluidWorkspaceId) return;
|
|
199
|
+
const claim = workspaceClaimOf(identity);
|
|
200
|
+
if (claim && claim === fluidWorkspaceId) throw new MayaWorkspaceCollisionError(claim);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Resolve a token's VERIFIED workspace from smart-memory-service itself
|
|
205
|
+
* (`GET /auth/me` → `default_team_id` — the same user-record field Maya's
|
|
206
|
+
* `_extract_team_id` falls back to). Exists because the JWT-decode fallback is
|
|
207
|
+
* vacuous in practice: real tokens carry NO workspace claims (VERIFY-1), so a
|
|
208
|
+
* pasted static token could silently defeat the isolation check (Codex r1 P1).
|
|
209
|
+
*
|
|
210
|
+
* FAILS CLOSED: an unreachable service, a rejected token, or a user record
|
|
211
|
+
* without a resolvable team all throw — the paste flow refuses to store a
|
|
212
|
+
* token whose workspace it cannot verify.
|
|
213
|
+
*/
|
|
214
|
+
export async function fetchVerifiedWorkspace({ smBaseUrl, token, fetchFn = fetch }) {
|
|
215
|
+
if (!smBaseUrl) {
|
|
216
|
+
throw new MayaIdentityError('maya: cannot verify the token — smartmemory baseUrl is not configured');
|
|
217
|
+
}
|
|
218
|
+
const res = await requestJson(`${smBaseUrl}/auth/me`, { method: 'GET', token, fetchFn });
|
|
219
|
+
if (res.status !== 200 || !res.body || typeof res.body !== 'object') {
|
|
220
|
+
throw new MayaIdentityError(
|
|
221
|
+
`maya: could not verify the token against SmartMemory (HTTP ${res.status}) — not storing it`,
|
|
222
|
+
);
|
|
223
|
+
}
|
|
224
|
+
const team = res.body.default_team_id;
|
|
225
|
+
if (typeof team !== 'string' || !team) {
|
|
226
|
+
throw new MayaIdentityError(
|
|
227
|
+
'maya: the token verified but resolves no workspace (no default_team_id) — '
|
|
228
|
+
+ 'refusing to store a token whose isolation cannot be checked',
|
|
229
|
+
);
|
|
230
|
+
}
|
|
231
|
+
return team;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Per-project serialization for ensureIdentity. Two concurrent first turns
|
|
236
|
+
* that both observe "no identity" would otherwise provision two competing
|
|
237
|
+
* users, overwrite each other's files, and fork the standing conversation
|
|
238
|
+
* (Codex r1 P2). The server is a single process, so an in-process promise
|
|
239
|
+
* chain is the whole fix: every caller re-loads at the head of its own turn
|
|
240
|
+
* in the chain and sees the winner's identity.
|
|
241
|
+
*/
|
|
242
|
+
const _ensureChain = new Map(); // projectRoot -> tail Promise
|
|
243
|
+
|
|
244
|
+
async function ensureIdentityUnserialized(projectRoot, { smBaseUrl, mode, fetchFn = fetch }) {
|
|
245
|
+
let identity = loadIdentity(projectRoot);
|
|
246
|
+
if (!identity) {
|
|
247
|
+
if (mode !== 'provision') {
|
|
248
|
+
throw new MayaIdentityError('maya: no token configured — paste one in the panel', 'auth');
|
|
249
|
+
}
|
|
250
|
+
if (!smBaseUrl) {
|
|
251
|
+
throw new MayaIdentityError('maya: provisioning needs the smartmemory baseUrl', 'auth');
|
|
252
|
+
}
|
|
253
|
+
identity = await provisionIdentity({ smBaseUrl, fetchFn });
|
|
254
|
+
saveIdentity(projectRoot, identity);
|
|
255
|
+
}
|
|
256
|
+
// A saved-but-unaccepted NDA (crash between provision and accept) is retried
|
|
257
|
+
// on the SAME identity rather than minting a new one.
|
|
258
|
+
if (identity.mode === 'provision' && !identity.ndaAccepted) {
|
|
259
|
+
await acceptNda({ smBaseUrl, token: identity.access_token, fetchFn });
|
|
260
|
+
identity.ndaAccepted = true;
|
|
261
|
+
saveIdentity(projectRoot, identity);
|
|
262
|
+
}
|
|
263
|
+
return identity;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* The relay's per-turn identity resolution: load, lazily provision on FIRST
|
|
268
|
+
* use (provision mode only — static mode with no pasted token is an auth
|
|
269
|
+
* funnel), and complete a pending NDA. Never called on a 401 — that funnel is
|
|
270
|
+
* explicit-action-only. Serialized per project (see `_ensureChain`).
|
|
271
|
+
*/
|
|
272
|
+
export async function ensureIdentity(projectRoot, opts) {
|
|
273
|
+
const tail = _ensureChain.get(projectRoot) ?? Promise.resolve();
|
|
274
|
+
const run = tail
|
|
275
|
+
.catch(() => {}) // a predecessor's failure must not poison the chain
|
|
276
|
+
.then(() => ensureIdentityUnserialized(projectRoot, opts));
|
|
277
|
+
_ensureChain.set(projectRoot, run);
|
|
278
|
+
try {
|
|
279
|
+
return await run;
|
|
280
|
+
} finally {
|
|
281
|
+
if (_ensureChain.get(projectRoot) === run) _ensureChain.delete(projectRoot);
|
|
282
|
+
}
|
|
283
|
+
}
|
package/lib/mcp-enforcement.js
CHANGED
|
@@ -11,19 +11,20 @@
|
|
|
11
11
|
|
|
12
12
|
import { existsSync, readFileSync } from 'fs';
|
|
13
13
|
import { join } from 'path';
|
|
14
|
+
import {
|
|
15
|
+
isGuarded as registryIsGuarded,
|
|
16
|
+
toolsForPath as registryToolsForPath,
|
|
17
|
+
featureCodeForPath as registryFeatureCodeForPath,
|
|
18
|
+
} from './canon-registry.js';
|
|
14
19
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
'link_features',
|
|
24
|
-
'record_completion',
|
|
25
|
-
'propose_followup',
|
|
26
|
-
];
|
|
20
|
+
// COMP-CANON-GUARD S1: the guarded-path/tool declarations that used to live here
|
|
21
|
+
// as literal sets now live in lib/canon-registry.js — the single source of truth
|
|
22
|
+
// shared with the write-time hook. This module is the 'ship' enforcement point;
|
|
23
|
+
// it consumes ONLY the registry's ship subset, so its behavior is unchanged
|
|
24
|
+
// (docs/judgment/** is a hook-only path and is invisible here, exactly as before
|
|
25
|
+
// this refactor, when it was not declared at all). The contract test
|
|
26
|
+
// (test/canon-registry-contract.test.js) pins that equivalence.
|
|
27
|
+
const ENFORCEMENT_POINT = 'ship';
|
|
27
28
|
|
|
28
29
|
/**
|
|
29
30
|
* Read `enforcement.mcpForFeatureMgmt` and normalize to 'block' | 'log' | 'off'.
|
|
@@ -63,11 +64,7 @@ export function filterGuarded(dirtyFiles, featuresDir) {
|
|
|
63
64
|
*/
|
|
64
65
|
export function isGuardedPath(path, featuresDir) {
|
|
65
66
|
if (typeof path !== 'string') return false;
|
|
66
|
-
|
|
67
|
-
// <featuresDir>/<CODE>/feature.json
|
|
68
|
-
const prefix = featuresDir.replace(/\/$/, '') + '/';
|
|
69
|
-
if (!path.startsWith(prefix)) return false;
|
|
70
|
-
return path.endsWith('/feature.json');
|
|
67
|
+
return registryIsGuarded(path, { featuresDir, point: ENFORCEMENT_POINT });
|
|
71
68
|
}
|
|
72
69
|
|
|
73
70
|
/**
|
|
@@ -80,13 +77,7 @@ export function isGuardedPath(path, featuresDir) {
|
|
|
80
77
|
* @returns {string[]}
|
|
81
78
|
*/
|
|
82
79
|
export function expectedToolsForPath(path, featuresDir) {
|
|
83
|
-
|
|
84
|
-
if (path === 'CHANGELOG.md') return [...TOOLS_FOR_CHANGELOG];
|
|
85
|
-
const prefix = featuresDir.replace(/\/$/, '') + '/';
|
|
86
|
-
if (path.startsWith(prefix) && path.endsWith('/feature.json')) {
|
|
87
|
-
return [...TOOLS_FOR_FEATURE_JSON];
|
|
88
|
-
}
|
|
89
|
-
return [];
|
|
80
|
+
return registryToolsForPath(path, { featuresDir, point: ENFORCEMENT_POINT });
|
|
90
81
|
}
|
|
91
82
|
|
|
92
83
|
/**
|
|
@@ -98,11 +89,7 @@ export function expectedToolsForPath(path, featuresDir) {
|
|
|
98
89
|
* @returns {string|null}
|
|
99
90
|
*/
|
|
100
91
|
export function featureCodeFromPath(path, featuresDir) {
|
|
101
|
-
|
|
102
|
-
if (!path.startsWith(prefix) || !path.endsWith('/feature.json')) return null;
|
|
103
|
-
const middle = path.slice(prefix.length, -'/feature.json'.length);
|
|
104
|
-
if (!middle || middle.includes('/')) return null;
|
|
105
|
-
return middle;
|
|
92
|
+
return registryFeatureCodeForPath(path, { featuresDir });
|
|
106
93
|
}
|
|
107
94
|
|
|
108
95
|
/**
|
|
@@ -165,9 +152,8 @@ export function enforcementError(violations) {
|
|
|
165
152
|
return err;
|
|
166
153
|
}
|
|
167
154
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
};
|
|
155
|
+
// NOTE: the `_internals` export (guarded-file set + tool sets) was removed in
|
|
156
|
+
// COMP-CANON-GUARD S1. Those declarations now live in lib/canon-registry.js —
|
|
157
|
+
// the single source of truth — with no in-repo consumer of the old shim. Import
|
|
158
|
+
// from canon-registry.js if you need the raw sets, so there is exactly ONE
|
|
159
|
+
// declaration, not a copy here that can drift.
|
package/lib/migrate-anon.js
CHANGED
|
@@ -181,6 +181,11 @@ export async function promoteAnonRow(cwd, row, { code, status = 'PLANNED', scaff
|
|
|
181
181
|
status,
|
|
182
182
|
complexity: 'S',
|
|
183
183
|
position,
|
|
184
|
+
// COMP-COMPLETION-GATE: promoting an anonymous ROADMAP row transcribes a
|
|
185
|
+
// status that already exists in the roadmap — including COMPLETE for work
|
|
186
|
+
// finished long before the gate. That is history, not a new completion, so
|
|
187
|
+
// it takes the named migration exemption rather than the completion path.
|
|
188
|
+
_migration: { reason: `promoting anonymous ROADMAP row in phase ${row.phaseId}` },
|
|
184
189
|
});
|
|
185
190
|
} catch (err) {
|
|
186
191
|
if (err && err.code === 'ROADMAP_PARTIAL_WRITE') throw err;
|
package/lib/migrate-roadmap.js
CHANGED
|
@@ -81,6 +81,21 @@ export function migrateRoadmap(cwd, opts = {}) {
|
|
|
81
81
|
feature.created = new Date().toISOString().slice(0, 10);
|
|
82
82
|
}
|
|
83
83
|
|
|
84
|
+
// COMP-COMPLETION-GATE slice 3 (AC-17, path 13): the migration legitimately
|
|
85
|
+
// copies historical COMPLETE rows into feature.json — it transcribes a
|
|
86
|
+
// completion that already happened, it does not mint one. The exemption is
|
|
87
|
+
// NAMED and LOGGED per write and covers BOTH branches: an existing file
|
|
88
|
+
// (`--overwrite`) and a missing one (created without it — §2.7 round 2
|
|
89
|
+
// finding 7: an exemption keyed on --overwrite alone would leave the
|
|
90
|
+
// create path as an unlogged bypass). `writeFeature` is the raw file
|
|
91
|
+
// primitive; this callsite is on the completion-write allowlist.
|
|
92
|
+
if (feature.status === 'COMPLETE') {
|
|
93
|
+
console.warn(
|
|
94
|
+
`[migrate-roadmap] migration exemption (${existing ? 'overwrite' : 'create'}): writing ` +
|
|
95
|
+
`${entry.code} as COMPLETE from its ROADMAP row without a completion record — ` +
|
|
96
|
+
`ROADMAP is the source of truth during migration.`,
|
|
97
|
+
);
|
|
98
|
+
}
|
|
84
99
|
if (opts.dryRun) {
|
|
85
100
|
console.log(`${existing ? 'update' : 'create'}: ${featuresDir}/${entry.code}/feature.json`);
|
|
86
101
|
} else {
|
package/lib/new.js
CHANGED
|
@@ -13,6 +13,7 @@ import { promptGate } from './gate-prompt.js';
|
|
|
13
13
|
import { VisionWriter } from './vision-writer.js';
|
|
14
14
|
import { readFlowRound } from './flow-state.js';
|
|
15
15
|
import { validateStep } from './step-validator.js';
|
|
16
|
+
import { tsCompatibilityOf, quarantineMessage } from './pipeline-compat.js';
|
|
16
17
|
|
|
17
18
|
const KICKOFF_VALIDATION = new Map([
|
|
18
19
|
['research', {
|
|
@@ -81,7 +82,18 @@ export async function runNew(intent, opts = {}) {
|
|
|
81
82
|
throw new Error(`Kickoff spec not found: ${specPath}. Run 'compose init' to get default pipelines.`);
|
|
82
83
|
}
|
|
83
84
|
|
|
84
|
-
|
|
85
|
+
// COMP-PIPELINE-QUARANTINE follow-up: `compose new` has its own runner and
|
|
86
|
+
// calls stratum.plan directly, so it never passed through runBuild's
|
|
87
|
+
// compatibility check. An existing workspace pinning an old new.stratum.yaml
|
|
88
|
+
// (init never overwrites one) still got the opaque engine rejection this check
|
|
89
|
+
// exists to replace.
|
|
90
|
+
const specText = readFileSync(specPath, 'utf-8');
|
|
91
|
+
const specCompat = tsCompatibilityOf(specText);
|
|
92
|
+
if (!specCompat.compatible) {
|
|
93
|
+
throw new Error(quarantineMessage(specPath, specCompat));
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const spec = YAML.parse(specText);
|
|
85
97
|
if (opts.skipResearch) {
|
|
86
98
|
const research = spec.flows?.new?.steps?.find((step) => step.id === 'research');
|
|
87
99
|
if (research) research.when = 'false';
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TS-engine spec compatibility.
|
|
3
|
+
*
|
|
4
|
+
* COMP-PIPELINE-QUARANTINE: STRAT-PY-RETIRE converted only the two production
|
|
5
|
+
* pipelines (build, gsd) from the v0.3 dialect to TS v1 — see compose commit
|
|
6
|
+
* 9221548, "Both production pipelines (build, gsd) are re-authored as TS v1".
|
|
7
|
+
* Everything else in pipelines/ and presets/ stayed behind — most on v0.3,
|
|
8
|
+
* bug-fix on v0.1 — and the older execution paths were deleted, so those specs
|
|
9
|
+
* cannot run at all: the engine
|
|
10
|
+
* refuses them at `stratum_plan` with a bare `MCP error -32602: spec validation
|
|
11
|
+
* failed`, which tells the user nothing about why.
|
|
12
|
+
*
|
|
13
|
+
* This module is the single place that answers "can the TS engine run this
|
|
14
|
+
* spec". It reads the spec's OWN version stamp rather than consulting a list of
|
|
15
|
+
* pipeline names — so migrating a spec to `version: 1` lifts its quarantine
|
|
16
|
+
* automatically, with nothing here to remember to update.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { parse as parseYaml } from 'yaml';
|
|
20
|
+
|
|
21
|
+
/** The only spec dialect the TS engine accepts. */
|
|
22
|
+
export const TS_SPEC_VERSION = 1;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* The specs `compose init` seeds into a workspace's pipelines/ directory.
|
|
26
|
+
*
|
|
27
|
+
* Shared with resolveTemplatePath, which deliberately does NOT fall back to the
|
|
28
|
+
* bundled copy for these: their absence means the workspace was never
|
|
29
|
+
* initialized, and failing loudly with "Lifecycle spec not found" is the honest
|
|
30
|
+
* answer. Silently running Compose's own bundled build pipeline against an
|
|
31
|
+
* uninitialized project would be worse than the error. Every OTHER shipped
|
|
32
|
+
* pipeline (content, coverage-sweep, refactor, research, review-fix) is never
|
|
33
|
+
* copied by init, so the bundled copy is its only source and the fallback is the
|
|
34
|
+
* only way `--template research` can resolve at all.
|
|
35
|
+
*/
|
|
36
|
+
export const INIT_PROVISIONED_SPECS = Object.freeze([
|
|
37
|
+
'build',
|
|
38
|
+
'build-quick',
|
|
39
|
+
'bug-fix',
|
|
40
|
+
'new',
|
|
41
|
+
'plan',
|
|
42
|
+
]);
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Classify a spec's engine compatibility.
|
|
46
|
+
*
|
|
47
|
+
* Deliberately shallow: a `version: 1` stamp means "authored for this engine",
|
|
48
|
+
* not "valid". Full validation belongs to the engine, which reports precise
|
|
49
|
+
* errors. This exists to turn the common, uninformative failure — a spec from
|
|
50
|
+
* the retired dialect — into a message that names the cause.
|
|
51
|
+
*
|
|
52
|
+
* @param {string} specText Raw spec YAML
|
|
53
|
+
* @returns {{compatible: boolean, version: unknown, reason: string|null}}
|
|
54
|
+
*/
|
|
55
|
+
export function tsCompatibilityOf(specText) {
|
|
56
|
+
let parsed;
|
|
57
|
+
try {
|
|
58
|
+
parsed = parseYaml(specText);
|
|
59
|
+
} catch (err) {
|
|
60
|
+
return { compatible: false, version: null, reason: `spec is not parseable YAML: ${err.message}` };
|
|
61
|
+
}
|
|
62
|
+
const version = parsed?.version ?? null;
|
|
63
|
+
if (version === TS_SPEC_VERSION) return { compatible: true, version, reason: null };
|
|
64
|
+
if (version === null) {
|
|
65
|
+
return { compatible: false, version, reason: 'spec declares no `version`' };
|
|
66
|
+
}
|
|
67
|
+
return {
|
|
68
|
+
compatible: false,
|
|
69
|
+
version,
|
|
70
|
+
reason: `spec declares version ${JSON.stringify(version)}, but the TS engine only runs version ${TS_SPEC_VERSION}`,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** True iff the TS engine will accept this spec's dialect. */
|
|
75
|
+
export function isTsEngineSpec(specText) {
|
|
76
|
+
return tsCompatibilityOf(specText).compatible;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* The message shown when a quarantined pipeline is invoked. Names the spec, the
|
|
81
|
+
* cause, and the only two ways forward — never a raw engine error code.
|
|
82
|
+
*
|
|
83
|
+
* @param {string} specPath Absolute path of the offending spec
|
|
84
|
+
* @param {{reason: string|null}} compat Result of tsCompatibilityOf
|
|
85
|
+
*/
|
|
86
|
+
export function quarantineMessage(specPath, compat) {
|
|
87
|
+
// Only a spec that declares an OLDER dialect gets the migration story. A
|
|
88
|
+
// malformed or version-less spec is far more likely to be a fresh typo, and
|
|
89
|
+
// telling its author it was "left behind by STRAT-PY-RETIRE" is a confident
|
|
90
|
+
// false history that sends them looking in the wrong place.
|
|
91
|
+
const isRetiredDialect = typeof compat.version === 'string' && /^0\./.test(compat.version);
|
|
92
|
+
const lines = [
|
|
93
|
+
`This pipeline cannot run: ${specPath}`,
|
|
94
|
+
` ${compat.reason}.`,
|
|
95
|
+
];
|
|
96
|
+
if (isRetiredDialect) {
|
|
97
|
+
lines.push(
|
|
98
|
+
' It was left on a retired dialect when STRAT-PY-RETIRE cut the engine over to TS v1 and',
|
|
99
|
+
' deleted the older execution path, so it has been unrunnable since that cutover.',
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
lines.push(` The spec must declare \`version: ${TS_SPEC_VERSION}\` and use the TS v1 dialect.`);
|
|
103
|
+
return lines.join('\n');
|
|
104
|
+
}
|