@smartmemory/compose 0.4.1 → 0.5.1

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.
Files changed (127) hide show
  1. package/.claude/agents/compose-architect.md +40 -0
  2. package/.claude/agents/compose-explorer.md +35 -0
  3. package/.claude/hooks/canon-guard.mjs +52 -0
  4. package/README.md +15 -1
  5. package/bin/compose.js +57 -17
  6. package/bin/git-hooks/pre-push.template +26 -1
  7. package/bin/receipts-gate.js +39 -0
  8. package/contracts/fluid-record.schema.json +5 -0
  9. package/dist/assets/{App-Z4MU-H_F.js → App-DC7paCZv.js} +190 -190
  10. package/dist/assets/{_baseUniq-ClWoCPFl.js → _baseUniq-Czad7yiy.js} +1 -1
  11. package/dist/assets/{arc-DY26UIVo.js → arc-EquvLk8y.js} +1 -1
  12. package/dist/assets/{architectureDiagram-Q4EWVU46-6Ggq4DqJ.js → architectureDiagram-Q4EWVU46-Dr_qinWi.js} +1 -1
  13. package/dist/assets/{blockDiagram-DXYQGD6D-CH3Ked0l.js → blockDiagram-DXYQGD6D-D2z46ED_.js} +1 -1
  14. package/dist/assets/{c4Diagram-AHTNJAMY-Bk8dYilu.js → c4Diagram-AHTNJAMY-BHob1Yt0.js} +1 -1
  15. package/dist/assets/channel-B-7ZRCKC.js +1 -0
  16. package/dist/assets/{chunk-4BX2VUAB-BMR0XaAQ.js → chunk-4BX2VUAB-DomWBRa_.js} +1 -1
  17. package/dist/assets/{chunk-4TB4RGXK-JytR14a9.js → chunk-4TB4RGXK-WyC_x_DH.js} +1 -1
  18. package/dist/assets/{chunk-55IACEB6-B4Q97BCP.js → chunk-55IACEB6-BajRv3zx.js} +1 -1
  19. package/dist/assets/{chunk-EDXVE4YY-R_qarkSf.js → chunk-EDXVE4YY-rMnedK_r.js} +1 -1
  20. package/dist/assets/{chunk-FMBD7UC4-C9s7KR9m.js → chunk-FMBD7UC4-BPi03Hcb.js} +1 -1
  21. package/dist/assets/{chunk-OYMX7WX6-BySQzVxc.js → chunk-OYMX7WX6-B7J_mKX0.js} +1 -1
  22. package/dist/assets/{chunk-QZHKN3VN-DdpSYZsW.js → chunk-QZHKN3VN-BLXTVr8N.js} +1 -1
  23. package/dist/assets/{chunk-YZCP3GAM-iE_tzriw.js → chunk-YZCP3GAM-BYWjo2OJ.js} +1 -1
  24. package/dist/assets/classDiagram-6PBFFD2Q-Balz1OEB.js +1 -0
  25. package/dist/assets/classDiagram-v2-HSJHXN6E-Balz1OEB.js +1 -0
  26. package/dist/assets/clone-CfNV0lUO.js +1 -0
  27. package/dist/assets/{cose-bilkent-S5V4N54A-BdlU6ZX_.js → cose-bilkent-S5V4N54A-Coaq0xaU.js} +1 -1
  28. package/dist/assets/{dagre-KV5264BT-Cp3F5KTn.js → dagre-KV5264BT-DvUvAxlj.js} +1 -1
  29. package/dist/assets/{diagram-5BDNPKRD-DiR6_2q_.js → diagram-5BDNPKRD-70bXRUXV.js} +1 -1
  30. package/dist/assets/{diagram-G4DWMVQ6-w0i-p5HX.js → diagram-G4DWMVQ6-hMA8wgzx.js} +1 -1
  31. package/dist/assets/{diagram-MMDJMWI5-tIHhwUv3.js → diagram-MMDJMWI5-BNir7C6i.js} +1 -1
  32. package/dist/assets/{diagram-TYMM5635-BAeY3B19.js → diagram-TYMM5635-BCYl1xrE.js} +1 -1
  33. package/dist/assets/{erDiagram-SMLLAGMA-Ckx_Knko.js → erDiagram-SMLLAGMA-bjxP0_bt.js} +1 -1
  34. package/dist/assets/{flowDiagram-DWJPFMVM-DeoNka6J.js → flowDiagram-DWJPFMVM-CBn9fhEp.js} +1 -1
  35. package/dist/assets/{ganttDiagram-T4ZO3ILL-BmGnFbEg.js → ganttDiagram-T4ZO3ILL-y1O7mWzn.js} +1 -1
  36. package/dist/assets/{gitGraphDiagram-UUTBAWPF-Dk48IHsx.js → gitGraphDiagram-UUTBAWPF-DIxwDXHB.js} +1 -1
  37. package/dist/assets/{graph-BNzKGvoy.js → graph-9D1ZumWp.js} +1 -1
  38. package/dist/assets/{index-BEfrNBp8.js → index-Ds_IXQo3.js} +2 -2
  39. package/dist/assets/{infoDiagram-42DDH7IO-BRf827i0.js → infoDiagram-42DDH7IO-DsWLGhaY.js} +1 -1
  40. package/dist/assets/{ishikawaDiagram-UXIWVN3A-0kCZaeCM.js → ishikawaDiagram-UXIWVN3A-CipZIE90.js} +1 -1
  41. package/dist/assets/{journeyDiagram-VCZTEJTY-rvU7ayRt.js → journeyDiagram-VCZTEJTY-Vr5xqcQm.js} +1 -1
  42. package/dist/assets/{kanban-definition-6JOO6SKY-DpQwX1C5.js → kanban-definition-6JOO6SKY-EqUYneyh.js} +1 -1
  43. package/dist/assets/{layout-BI8cXFPI.js → layout-hfWIIs0-.js} +1 -1
  44. package/dist/assets/{linear-a0glcDiw.js → linear-BdDWoN0t.js} +1 -1
  45. package/dist/assets/{min-vPHfnXcC.js → min-Bn_xAS7n.js} +1 -1
  46. package/dist/assets/{mindmap-definition-QFDTVHPH-D14eF-7C.js → mindmap-definition-QFDTVHPH-qsgubzCF.js} +1 -1
  47. package/dist/assets/{pieDiagram-DEJITSTG-Cno-gETh.js → pieDiagram-DEJITSTG-Bv1xq_58.js} +1 -1
  48. package/dist/assets/{quadrantDiagram-34T5L4WZ-BUQM1Hfm.js → quadrantDiagram-34T5L4WZ-DwMbAegF.js} +1 -1
  49. package/dist/assets/{requirementDiagram-MS252O5E-pOXlN2-q.js → requirementDiagram-MS252O5E-BJVmLNcp.js} +1 -1
  50. package/dist/assets/{sankeyDiagram-XADWPNL6-Crynd3_b.js → sankeyDiagram-XADWPNL6-o5GZb8Y1.js} +1 -1
  51. package/dist/assets/{sequenceDiagram-FGHM5R23-D9fZdCM8.js → sequenceDiagram-FGHM5R23-ocqJp2qk.js} +1 -1
  52. package/dist/assets/{stateDiagram-FHFEXIEX-CW9qVec8.js → stateDiagram-FHFEXIEX-DGaDUFxP.js} +1 -1
  53. package/dist/assets/stateDiagram-v2-QKLJ7IA2-Dz-15i-r.js +1 -0
  54. package/dist/assets/{timeline-definition-GMOUNBTQ-BcHzhm_8.js → timeline-definition-GMOUNBTQ-C4YwFvAn.js} +1 -1
  55. package/dist/assets/{vennDiagram-DHZGUBPP-BfytJcWk.js → vennDiagram-DHZGUBPP-uOKn9j-y.js} +1 -1
  56. package/dist/assets/{wardley-RL74JXVD-DLj-IjyB.js → wardley-RL74JXVD-DIQSmQde.js} +1 -1
  57. package/dist/assets/{wardleyDiagram-NUSXRM2D-Ds0Ue68c.js → wardleyDiagram-NUSXRM2D-CdamsEDC.js} +1 -1
  58. package/dist/assets/{xychartDiagram-5P7HB3ND-vjWDXFL6.js → xychartDiagram-5P7HB3ND-DhLs41yk.js} +1 -1
  59. package/dist/index.html +1 -1
  60. package/lib/agent-string.js +9 -4
  61. package/lib/build-cancel.js +205 -0
  62. package/lib/build-stream-writer.js +6 -0
  63. package/lib/build.js +1189 -165
  64. package/lib/canon-guard.js +3 -24
  65. package/lib/canon-registry.js +2 -71
  66. package/lib/codex-preflight.js +8 -0
  67. package/lib/colleague/context.js +123 -0
  68. package/lib/consumer-fanout.js +427 -17
  69. package/lib/decision-blocks.js +38 -0
  70. package/lib/dispatch-ledger.js +7 -0
  71. package/lib/experiment-pricing.js +5 -1
  72. package/lib/flow-state.js +38 -0
  73. package/lib/fluid/factory.js +112 -1
  74. package/lib/fluid/ideabox-manifest.js +203 -0
  75. package/lib/fluid/ideabox-migrate.js +177 -29
  76. package/lib/fluid/ideabox-preamble.js +155 -0
  77. package/lib/fluid/ideabox-readable.js +83 -0
  78. package/lib/fluid/ideabox-recover.js +393 -0
  79. package/lib/fluid/import-ideabox.js +188 -45
  80. package/lib/fluid/local-provider.js +6 -0
  81. package/lib/fluid/portfolio.js +255 -0
  82. package/lib/fluid/record-shape.js +7 -0
  83. package/lib/fluid/render-ideabox.js +153 -7
  84. package/lib/fluid/smartmemory-provider.js +6 -0
  85. package/lib/gate-prompt.js +14 -7
  86. package/lib/gsd.js +95 -48
  87. package/lib/ideabox-cli.js +68 -0
  88. package/lib/ideabox.js +209 -9
  89. package/lib/maya-identity.js +16 -2
  90. package/lib/model-pricing.js +4 -1
  91. package/lib/output-gate.js +81 -0
  92. package/lib/pipeline-profiles.js +200 -0
  93. package/lib/process-termination.js +121 -3
  94. package/lib/receipts-gate.js +268 -0
  95. package/lib/result-normalizer.js +41 -1
  96. package/lib/smartmemory-client.js +68 -1
  97. package/lib/stratum-mcp-client.js +104 -5
  98. package/lib/team-flag.js +1 -1
  99. package/lib/tool-inventory.js +0 -1
  100. package/lib/version-check.js +9 -3
  101. package/lib/wave-checkpoint.js +100 -0
  102. package/package.json +7 -5
  103. package/presets/team-fable-astra.profiles.json +18 -0
  104. package/presets/team-fable-astra.stratum.yaml +236 -0
  105. package/server/build-stream-bridge.js +43 -1
  106. package/server/cc-session-watcher.js +54 -5
  107. package/server/compose-mcp-tools.js +48 -50
  108. package/server/compose-mcp.js +0 -2
  109. package/server/design-routes.js +1 -1
  110. package/server/file-watcher.js +14 -0
  111. package/server/ideabox-routes.js +10 -0
  112. package/server/index.js +5 -1
  113. package/server/lifecycle-guard.js +13 -0
  114. package/server/maya-routes.js +111 -7
  115. package/server/mcp-tool-defs.js +0 -25
  116. package/server/mcp-tool-policy.js +6 -13
  117. package/server/model-tiers.js +14 -6
  118. package/server/stratum-client.js +61 -15
  119. package/server/supervisor.js +18 -4
  120. package/server/vision-routes.js +9 -3
  121. package/dist/assets/channel-SnZzzh7k.js +0 -1
  122. package/dist/assets/classDiagram-6PBFFD2Q-CBu92dSH.js +0 -1
  123. package/dist/assets/classDiagram-v2-HSJHXN6E-CBu92dSH.js +0 -1
  124. package/dist/assets/clone-DgklGjHm.js +0 -1
  125. package/dist/assets/stateDiagram-v2-QKLJ7IA2-DkVLzHbY.js +0 -1
  126. package/lib/append-integrity.js +0 -81
  127. package/lib/canon-override.js +0 -196
@@ -0,0 +1,40 @@
1
+ ---
2
+ name: compose-architect
3
+ description: Architecture-proposal agent for Compose's Phase 3. Dispatched 2-3 in parallel, each under a different competing mandate (minimal-changes / clean-architecture / pragmatic-balance), to produce a candidate architecture for a feature. Returns a self-contained proposal with component boundaries, interfaces, and honest trade-offs, grounded in the real codebase. Proposes only — does not implement.
4
+ tools: Read, Grep, Glob, Bash
5
+ ---
6
+
7
+ You are compose-architect. You are dispatched with ONE explicit mandate and must produce the best
8
+ architecture for the feature **under that mandate** — so the main agent can compare competing
9
+ proposals and synthesize. You propose; you do not write production code.
10
+
11
+ ## Mandates (you will be given exactly one)
12
+ - **minimal-changes** — smallest possible diff, maximum reuse of what exists. Bias to extending
13
+ current modules over adding new ones.
14
+ - **clean-architecture** — maintainability first; elegant boundaries and abstractions even if it
15
+ means more moving parts.
16
+ - **pragmatic-balance** — 80/20: good boundaries without over-engineering.
17
+
18
+ ## Your job
19
+ 1. Read the relevant existing code (Grep/Glob/Read; Bash read-only) so the proposal is grounded in
20
+ real modules, signatures, and patterns — cite `path:line`.
21
+ 2. Design the architecture under your mandate: components and their responsibilities, the
22
+ interfaces/contracts between them, data flow, and error/edge handling.
23
+ 3. Be honest about trade-offs — what your mandate buys and what it costs versus the alternatives.
24
+
25
+ ## Hard constraints
26
+ - READ-ONLY. Do not Edit/Write or run mutating commands. The main agent writes `architecture.md`
27
+ and implements.
28
+ - Ground every structural claim in the real codebase (file:line). Flag where the feature would
29
+ cut against existing patterns.
30
+ - Honor your mandate even where you'd personally choose differently — the value is in the contrast.
31
+
32
+ ## Output
33
+ Return a self-contained proposal:
34
+ - **Approach (mandate)** — one line naming your mandate and the core idea.
35
+ - **Components & boundaries** — each component, its responsibility, and its interface/contract.
36
+ - **Data flow** — how a request moves through the components.
37
+ - **Trade-offs** — strengths, weaknesses, and what this costs vs the other mandates.
38
+ - **Reuse / new** — what existing code is reused (file:line) vs what's new.
39
+
40
+ Your final message IS the deliverable — return the proposal itself.
@@ -0,0 +1,35 @@
1
+ ---
2
+ name: compose-explorer
3
+ description: Read-only codebase exploration for Compose's design (Phase 1) and blueprint (Phase 4) phases. Use to find features similar to a target, map the architecture and abstractions of an area, or trace how an existing feature is implemented — dispatched 2-3 in parallel with distinct mandates. Returns findings with concrete file:line references; never edits code.
4
+ tools: Read, Grep, Glob, Bash
5
+ ---
6
+
7
+ You are compose-explorer, a focused read-only research agent for the Compose lifecycle. You are
8
+ dispatched to investigate ONE specific question about the codebase and return grounded findings —
9
+ not to design, plan, or change anything.
10
+
11
+ ## Your job
12
+ Given a research mandate (e.g. "find features similar to X and trace their implementation", "map
13
+ the architecture and abstractions for area Y", "analyze how feature Z works today"):
14
+
15
+ 1. Locate the relevant code with Grep/Glob; read the specific files and lines that matter.
16
+ 2. Identify the patterns, abstractions, and conventions actually in use — with **file:line**
17
+ references for every claim. Prefer reading exact regions over whole files.
18
+ 3. Note anything that contradicts the assumption behind the mandate (a "spec vs reality" gap).
19
+ 4. Use Bash only for read-only inspection (`git log`, `git grep`, `ls`, `rg`). Never write.
20
+
21
+ ## Hard constraints
22
+ - READ-ONLY. Do not use Edit/Write. Do not run mutating commands. You investigate; the main agent
23
+ decides and implements.
24
+ - Every load-bearing claim cites `path:line`. If you can't verify it, say so explicitly.
25
+ - Stay within your mandate — don't expand into adjacent areas unless they're load-bearing for it.
26
+
27
+ ## Output
28
+ Return a concise structured summary:
29
+ - **Findings** — bulleted, each with file:line evidence.
30
+ - **Relevant files** — the files the main agent should read to act on this.
31
+ - **Patterns to follow** — conventions/idioms observed (with refs).
32
+ - **Mismatches / risks** — where reality diverges from the mandate's assumption, if any.
33
+
34
+ Your final message IS the deliverable — return the structured findings, not a narrative of what
35
+ you did.
@@ -0,0 +1,52 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * canon-guard.mjs — COMP-CANON-GUARD S4 PreToolUse hook (runtime wrapper).
4
+ *
5
+ * Registered in .claude/settings.json by `compose guard install`. Claude Code
6
+ * runs it before every Write/Edit/NotebookEdit and pipes the tool call as JSON
7
+ * on stdin. All logic lives in lib/canon-guard.js (pure, tested); this wrapper
8
+ * only does I/O: read stdin → decide → emit the deny envelope.
9
+ *
10
+ * Deny protocol (verified against code.claude.com/docs/en/hooks 2026-07-24):
11
+ * exit 0 + stdout {"hookSpecificOutput":{"hookEventName":"PreToolUse",
12
+ * "permissionDecision":"deny","permissionDecisionReason":"..."}}. No output =
13
+ * allow. FAIL OPEN on any error — a guard that wedges the session is worse than
14
+ * one that misses.
15
+ */
16
+ import { readFileSync } from 'node:fs';
17
+ import { fileURLToPath } from 'node:url';
18
+ import { dirname, resolve } from 'node:path';
19
+ import { decideCanonGuard, realpathCanonicalize } from '../../lib/canon-guard.js';
20
+
21
+ function allow() { process.exit(0); }
22
+
23
+ let payload;
24
+ try {
25
+ payload = JSON.parse(readFileSync(0, 'utf-8'));
26
+ } catch {
27
+ allow(); // unreadable/invalid stdin → don't block
28
+ }
29
+
30
+ try {
31
+ // The hook lives at <repo>/.claude/hooks/canon-guard.mjs → repo root is two up.
32
+ const projectRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..', '..');
33
+ const decision = decideCanonGuard({
34
+ toolName: payload?.tool_name,
35
+ toolInput: payload?.tool_input,
36
+ cwd: payload?.cwd,
37
+ projectRoot,
38
+ canonicalize: realpathCanonicalize,
39
+ });
40
+ if (decision.deny) {
41
+ process.stdout.write(JSON.stringify({
42
+ hookSpecificOutput: {
43
+ hookEventName: 'PreToolUse',
44
+ permissionDecision: 'deny',
45
+ permissionDecisionReason: decision.reason,
46
+ },
47
+ }));
48
+ }
49
+ } catch {
50
+ // fall through to allow
51
+ }
52
+ process.exit(0);
package/README.md CHANGED
@@ -74,6 +74,20 @@ compose plan "a tool that summarizes my team's standups"
74
74
 
75
75
  `compose build` then picks up a plan-authored feature and ratifies its design rather than rewriting it.
76
76
 
77
+ Bundled [team presets](docs/team-presets.md): `feature` (parallel implementation),
78
+ `research` (parallel exploration), `review` (parallel review), and `fable-astra`:
79
+
80
+ ```bash
81
+ compose build FEAT-1 --team fable-astra
82
+ ```
83
+
84
+ Fable plans independent tasks, Codex workers implement in isolated worktrees, and
85
+ a fresh read-only Astra reviewer checks the merged result after verification.
86
+ Fable then requests another implementation or repair wave, declares the work
87
+ blocked, or approves ship into one base-parent commit. Concurrency is 3; the
88
+ $150 cost ceiling is overridable with `--cost-ceiling-usd`. See the
89
+ [loop and limits](docs/pipelines.md#fable-astra-wave-loop).
90
+
77
91
  ## Quick install
78
92
 
79
93
  Prerequisites: Node.js 18+. [Stratum](https://github.com/smartmemory/stratum) needs no separate install — `@smartmemory/stratum` is a dependency, and `compose init` registers the installed copy's MCP entrypoint automatically (a sibling `stratum/` checkout is a development convenience, not a requirement; the python `stratum-mcp` PyPI package is retired). Codex steps additionally need the OpenAI `codex` CLI. Full prereqs in [docs/install.md](docs/install.md).
@@ -198,7 +212,7 @@ Signed descriptors live in `.compose/guard-upgrades/<sha256>/` (immutable) with
198
212
  The mobile cockpit at `/m` can be reached from outside localhost — bring your own tunnel, compose handles auth and pairing:
199
213
 
200
214
  ```bash
201
- npm run build # remote serves the built PWA from the API server
215
+ npm run build # source checkouts only; the npm package already ships dist/
202
216
  COMPOSE_REMOTE_AUTH=enabled compose start --host=0.0.0.0
203
217
  compose remote pair --public-host=https://your-tunnel-host # prints a QR — scan it with your phone
204
218
  compose remote status # bind, devices, tunnel reachability
package/bin/compose.js CHANGED
@@ -98,6 +98,8 @@ import {
98
98
  formatDriftNudge,
99
99
  } from '../lib/version-check.js';
100
100
  import { computeHooksStatus, formatHookStatusLines, HOOK_MARKERS } from '../lib/hooks-status.js';
101
+ // COMP-BUILD-CANCEL S06-3: the CLI must not exit out from under a live teardown (C46).
102
+ import { pendingTeardown } from '../lib/build-cancel.js';
101
103
 
102
104
  const [,, cmd, ...args] = process.argv
103
105
 
@@ -565,8 +567,9 @@ async function runInit(flags, cwdOverride) {
565
567
  // and then fails with "Lifecycle spec not found" because init never copied it —
566
568
  // so the command was unavailable in every fresh workspace independently of the
567
569
  // spec's dialect. Each spec's `<name>.profiles.json` sidecar travels WITH it:
568
- // loadPipelineProfiles fails open, so a spec copied without its sidecar runs on
569
- // bare defaults and silently drops the tool restrictions it declares.
570
+ // loadPipelineProfiles treats a missing string-only sidecar as bare defaults (the
571
+ // tool restrictions it declares silently vanish), and runBuild REFUSES a local copy
572
+ // of a preset whose sidecar configures execution (team-fable-astra) without it.
570
573
  const pipelinesDir = join(cwd, 'pipelines')
571
574
  mkdirSync(pipelinesDir, { recursive: true })
572
575
  const DEFAULT_PIPELINES = [
@@ -968,6 +971,16 @@ if (cmd === 'new') {
968
971
  console.warn(`--from-idea: idea not found: ${fromIdeaId}`)
969
972
  }
970
973
  } catch (err) {
974
+ // An unreadable ideabox is not a missing idea, and it must not be
975
+ // downgraded into a warning that the build then walks straight past.
976
+ // `--from-idea` is an explicit request for THAT idea's content; building
977
+ // the feature without it produces a plausible-looking design derived from
978
+ // nothing the user asked for, while the ideas sit unread on disk
979
+ // (COMP-IDEABOX-MIGRATE-DIALECT FU-2).
980
+ if (err?.code === 'IDEABOX_UNREADABLE') {
981
+ console.error(`--from-idea: ${err.message}`)
982
+ process.exit(1)
983
+ }
971
984
  console.warn(`--from-idea: could not load idea: ${err.message}`)
972
985
  }
973
986
  }
@@ -2313,7 +2326,8 @@ if (cmd === 'guard') {
2313
2326
  if (sub === 'install') {
2314
2327
  if (!exSync(hookScript)) {
2315
2328
  console.error(`Error: hook script missing at ${hookScript}`)
2316
- console.error('It is a tracked source file ensure your checkout includes .claude/hooks/canon-guard.mjs')
2329
+ console.error('npm install: reinstall with `npm install @smartmemory/compose@latest` (add `--global` for a global install).')
2330
+ console.error('source checkout: update the checkout, or restore the tracked .claude/hooks/canon-guard.mjs file.')
2317
2331
  process.exit(1)
2318
2332
  }
2319
2333
  const { settings, changed } = installGuardHook(readSettings())
@@ -2346,10 +2360,10 @@ if (cmd === 'guard') {
2346
2360
  else if (st.state === 'stale') console.log('canon-guard: installed (stale — re-run `compose guard install`)')
2347
2361
  else console.log('canon-guard: absent — run `compose guard install`')
2348
2362
  console.log(` hook script: ${scriptState} (${hookScript})`)
2349
- // Derived from the registry, never hardcoded: a literal string here
2350
- // silently under-reported the guarded set as soon as COMP-CANON-OVERRIDE
2351
- // widened it. "Claude-runtime only" is not modesty Bash and Codex writes
2352
- // never reach this hook (drift detection, not enforcement).
2363
+ // Derived from the registry, never hardcoded, so registry changes cannot
2364
+ // silently desynchronize status output. "Claude-runtime only" is not
2365
+ // modesty Bash and Codex writes never reach this hook (drift detection,
2366
+ // not enforcement).
2353
2367
  const { guardedDisplaysFor } = await import('../lib/canon-registry.js')
2354
2368
  for (const pattern of guardedDisplaysFor('hook')) {
2355
2369
  console.log(` guards: ${pattern} (Write|Edit|NotebookEdit) — Claude-runtime only`)
@@ -2606,6 +2620,20 @@ if (cmd === 'build') {
2606
2620
  }
2607
2621
  let filteredArgs = args.filter((a, i) => i !== cwdIdx && (cwdIdx === -1 || i !== cwdIdx + 1))
2608
2622
 
2623
+ // Extract value-taking build flags before --team counts positional features.
2624
+ // --cwd is already removed; leave --template for the explicit team conflict.
2625
+ const ceilingIdx = filteredArgs.findIndex(a => a === '--cost-ceiling-usd' || a.startsWith('--cost-ceiling-usd='))
2626
+ let costCeilingUsd
2627
+ if (ceilingIdx !== -1) {
2628
+ const inline = filteredArgs[ceilingIdx].includes('=')
2629
+ costCeilingUsd = Number(inline ? filteredArgs[ceilingIdx].split('=')[1] : filteredArgs[ceilingIdx + 1])
2630
+ if (!Number.isFinite(costCeilingUsd) || costCeilingUsd <= 0) {
2631
+ console.error('--cost-ceiling-usd requires a finite positive USD amount')
2632
+ process.exit(1)
2633
+ }
2634
+ filteredArgs.splice(ceilingIdx, inline ? 1 : 2)
2635
+ }
2636
+
2609
2637
  // --team flag (COMP-TEAMS)
2610
2638
  let teamTemplate = null
2611
2639
  try {
@@ -2654,7 +2682,7 @@ if (cmd === 'build') {
2654
2682
  if (teamTemplate && !templateName) {
2655
2683
  templateName = teamTemplate
2656
2684
  }
2657
- const filteredArgs2 = filteredArgs.filter((a, i) => i !== templateIdx && (templateIdx === -1 || i !== templateIdx + 1))
2685
+ let filteredArgs2 = filteredArgs.filter((a, i) => i !== templateIdx && (templateIdx === -1 || i !== templateIdx + 1))
2658
2686
 
2659
2687
  const featureCodes = filteredArgs2.filter(a => !a.startsWith('-'))
2660
2688
  const featureCode = featureCodes[0]
@@ -2712,6 +2740,10 @@ if (cmd === 'build') {
2712
2740
  console.error('Error: --abort and --all/prefix/multi are mutually exclusive')
2713
2741
  process.exit(1)
2714
2742
  }
2743
+ if (costCeilingUsd !== undefined && isBatch) {
2744
+ console.error('--cost-ceiling-usd is single-build only; batch is not supported')
2745
+ process.exit(1)
2746
+ }
2715
2747
  if (resume && fresh) {
2716
2748
  console.error('--resume and --fresh are mutually exclusive')
2717
2749
  process.exit(1)
@@ -2767,6 +2799,7 @@ if (cmd === 'build') {
2767
2799
  console.error(' --abort Abort the active build')
2768
2800
  console.error(' --resume Resume the active build for <feature-code>')
2769
2801
  console.error(' --fresh Start a fresh build, discarding stale failed/resumable state')
2802
+ console.error(' --cost-ceiling-usd <amount> Override a configured ceiling (single build; holds still require a human)')
2770
2803
  console.error(' --all Build all PLANNED features in dependency order')
2771
2804
  console.error(' --dry-run Print build order without executing')
2772
2805
  console.error(' --cwd <path> Agent working directory (for cross-repo features)')
@@ -2843,12 +2876,15 @@ if (cmd === 'build') {
2843
2876
  if (implementerArg) singleOpts.implementer = implementerArg // COMP-MODEL-AB
2844
2877
  if (reviewerArg) singleOpts.reviewer = reviewerArg // COMP-MODEL-AB
2845
2878
  if (resume) singleOpts.resume = true
2879
+ if (costCeilingUsd !== undefined) singleOpts.costCeilingUsd = costCeilingUsd
2846
2880
  if (fresh) singleOpts.fresh = true
2847
2881
  if (nonInteractiveBuild) singleOpts.gateOpts = { nonInteractive: true }
2848
2882
  if (resumeFlowId) singleOpts.resumeFlowId = resumeFlowId
2849
- runBuild(featureCode, singleOpts).then(() => {
2850
- process.exit(0)
2851
- }).catch((err) => {
2883
+ runBuild(featureCode, singleOpts).then(async (result) => {
2884
+ await pendingTeardown()
2885
+ process.exit(abort && result?.ok === false ? 1 : 0)
2886
+ }).catch(async (err) => {
2887
+ await pendingTeardown()
2852
2888
  console.error(`Build failed: ${err.message}`)
2853
2889
  process.exit(1)
2854
2890
  })
@@ -2963,9 +2999,11 @@ if (cmd === 'build') {
2963
2999
  if (agentWorkDir) opts.workingDirectory = agentWorkDir
2964
3000
  if (bugDescription) opts.description = bugDescription
2965
3001
  if (resumeFlowId) opts.resumeFlowId = resumeFlowId
2966
- runBuild(bugCode, opts).then(() => {
2967
- process.exit(0)
2968
- }).catch((err) => {
3002
+ runBuild(bugCode, opts).then(async (result) => {
3003
+ await pendingTeardown()
3004
+ process.exit(abort && result?.ok === false ? 1 : 0)
3005
+ }).catch(async (err) => {
3006
+ await pendingTeardown()
2969
3007
  console.error(`Fix failed: ${err.message}`)
2970
3008
  process.exit(1)
2971
3009
  })
@@ -3074,9 +3112,11 @@ if (cmd === 'build') {
3074
3112
  const opts = { abort, template: 'plan', mode: 'plan', description: intent }
3075
3113
  if (agentWorkDir) opts.workingDirectory = agentWorkDir
3076
3114
  if (resumeFlowId) opts.resumeFlowId = resumeFlowId
3077
- runBuild(planCode, opts).then(() => {
3078
- process.exit(0)
3079
- }).catch((err) => {
3115
+ runBuild(planCode, opts).then(async (result) => {
3116
+ await pendingTeardown()
3117
+ process.exit(abort && result?.ok === false ? 1 : 0)
3118
+ }).catch(async (err) => {
3119
+ await pendingTeardown()
3080
3120
  console.error(`Plan failed: ${err.message}`)
3081
3121
  process.exit(1)
3082
3122
  })
@@ -42,12 +42,16 @@ command -v perl >/dev/null 2>&1 && perl -MFcntl -e '
42
42
  ZERO_SHA="0000000000000000000000000000000000000000"
43
43
  DOCS_ONLY=1
44
44
  SAW_RANGE=0
45
+ RANGES=" "
45
46
  while read -r _local_ref local_sha _remote_ref remote_sha; do
46
47
  [ "$local_sha" = "$ZERO_SHA" ] && continue # ref delete — no content pushed
47
48
  if [ "$remote_sha" = "$ZERO_SHA" ]; then
48
- DOCS_ONLY=0; SAW_RANGE=1; continue # new ref — range unbounded, fail closed
49
+ DOCS_ONLY=0; SAW_RANGE=1
50
+ RANGES="$RANGES $remote_sha:$local_sha" # receipts gate bounds a new ref itself
51
+ continue # new ref — range unbounded, fail closed
49
52
  fi
50
53
  SAW_RANGE=1
54
+ RANGES="$RANGES $remote_sha:$local_sha"
51
55
  CHANGED=$(git diff --name-only "$remote_sha".."$local_sha" 2>/dev/null) || { DOCS_ONLY=0; continue; }
52
56
  while IFS= read -r f; do
53
57
  [ -z "$f" ] && continue
@@ -90,6 +94,27 @@ if [ "$GUARD_EXIT" -ne 0 ]; then
90
94
  fi
91
95
  echo "pre-push: judgment canon drift check green." >&2
92
96
 
97
+ # ── Receipts gate ────────────────────────────────────────────────────────────
98
+ # A claim written as a fact must carry its receipt: a CHECKED box saying a test
99
+ # pins something needs the test path; "flake"/"flaky" needs a measurement or a
100
+ # RESOLVED @sha; a commit saying the suite is green needs the counts. Scans only
101
+ # the ADDED lines of the pushed range, so it never blocks on history. Runs on
102
+ # docs-only pushes too — that is where "known flake" notes get written, and one
103
+ # of those sat on a live product defect for two months (12a357a). Quote a phrase
104
+ # in backticks to talk about it without asserting it. Bypass: --no-verify.
105
+ RECEIPTS_GATE="$(dirname "$COMPOSE_BIN")/receipts-gate.js"
106
+ if [ -f "$RECEIPTS_GATE" ]; then
107
+ for pair in $RANGES; do
108
+ "$COMPOSE_NODE" "$RECEIPTS_GATE" "${pair%%:*}" "${pair##*:}" 2>&1 | tee -a "$LOG" >&2
109
+ RECEIPTS_EXIT=${PIPESTATUS[0]}
110
+ if [ "$RECEIPTS_EXIT" -ne 0 ]; then
111
+ echo "" >&2
112
+ echo "pre-push: a claim has no receipt beside it — push aborted (full log: $LOG)." >&2
113
+ exit "$RECEIPTS_EXIT"
114
+ fi
115
+ done
116
+ fi
117
+
93
118
  # ── Test gate (COMP-RESUME follow-up) ───────────────────────────────────────
94
119
  # Block the push if the suite is red. This is the gate whose absence let a broken
95
120
  # integration test (and a stale-schema consumer bug) reach main — nothing ran the
@@ -0,0 +1,39 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * receipts-gate — pre-push runner. `node bin/receipts-gate.js <base> <head>`.
4
+ *
5
+ * `base` may be git's all-zeros sha (a new ref); then the range is bounded by
6
+ * the merge-base with origin/main, or head~1 when there is no origin/main —
7
+ * fail CLOSED (scan something) rather than open (scan nothing).
8
+ * Exit 1 on any violation. See lib/receipts-gate.js for what counts.
9
+ */
10
+ import { execFileSync } from 'node:child_process';
11
+ import { scanRange, formatViolations } from '../lib/receipts-gate.js';
12
+
13
+ const ZERO = /^0{40}$/;
14
+ const [, , baseArg, headArg = 'HEAD'] = process.argv;
15
+
16
+ function git(args) {
17
+ return execFileSync('git', args, { encoding: 'utf8' }).trim();
18
+ }
19
+
20
+ let base = baseArg;
21
+ if (!base || ZERO.test(base)) {
22
+ try { base = git(['merge-base', 'origin/main', headArg]); }
23
+ catch { base = `${headArg}~1`; }
24
+ }
25
+
26
+ let found;
27
+ try {
28
+ found = scanRange(base, headArg);
29
+ } catch (err) {
30
+ // A gate that cannot read its input must not pass silently.
31
+ console.error(`receipts-gate: could not scan ${base}..${headArg}: ${err.message}`);
32
+ process.exit(2);
33
+ }
34
+
35
+ if (found.length) {
36
+ console.error(formatViolations(found));
37
+ process.exit(1);
38
+ }
39
+ console.error(`receipts-gate: ${base.slice(0, 7)}..${headArg} carries no unreceipted claims.`);
@@ -122,6 +122,11 @@
122
122
  "type": ["string", "null"],
123
123
  "description": "Where the idea came from, as free text."
124
124
  },
125
+ "extra_fields": {
126
+ "type": "array",
127
+ "items": { "type": "string" },
128
+ "description": "Hand-authored markdown field lines the ideabox parser did not recognise, kept verbatim in serializer order. Exists so migrating a markdown ideabox into records is lossless: this contract is closed and the record shape is an allowlist, so a custom field with nowhere to live is deleted from the user's file by the first render after the upgrade, silently. See COMP-IDEABOX-MIGRATE-DIALECT."
129
+ },
125
130
  "links": {
126
131
  "type": "array",
127
132
  "items": { "$ref": "#/definitions/link" }