@smartmemory/compose 0.4.1 → 0.5.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.
Files changed (114) 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 +1 -1
  5. package/bin/compose.js +33 -14
  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/build-cancel.js +205 -0
  61. package/lib/build.js +552 -87
  62. package/lib/canon-guard.js +3 -24
  63. package/lib/canon-registry.js +2 -71
  64. package/lib/codex-preflight.js +8 -0
  65. package/lib/colleague/context.js +123 -0
  66. package/lib/consumer-fanout.js +24 -1
  67. package/lib/decision-blocks.js +38 -0
  68. package/lib/dispatch-ledger.js +7 -0
  69. package/lib/fluid/factory.js +112 -1
  70. package/lib/fluid/ideabox-manifest.js +203 -0
  71. package/lib/fluid/ideabox-migrate.js +177 -29
  72. package/lib/fluid/ideabox-preamble.js +155 -0
  73. package/lib/fluid/ideabox-readable.js +83 -0
  74. package/lib/fluid/ideabox-recover.js +393 -0
  75. package/lib/fluid/import-ideabox.js +188 -45
  76. package/lib/fluid/local-provider.js +6 -0
  77. package/lib/fluid/portfolio.js +255 -0
  78. package/lib/fluid/record-shape.js +7 -0
  79. package/lib/fluid/render-ideabox.js +153 -7
  80. package/lib/fluid/smartmemory-provider.js +6 -0
  81. package/lib/gate-prompt.js +14 -7
  82. package/lib/ideabox-cli.js +68 -0
  83. package/lib/ideabox.js +209 -9
  84. package/lib/maya-identity.js +16 -2
  85. package/lib/process-termination.js +121 -3
  86. package/lib/receipts-gate.js +268 -0
  87. package/lib/result-normalizer.js +28 -1
  88. package/lib/smartmemory-client.js +68 -1
  89. package/lib/stratum-mcp-client.js +104 -5
  90. package/lib/tool-inventory.js +0 -1
  91. package/lib/version-check.js +9 -3
  92. package/package.json +7 -5
  93. package/server/build-stream-bridge.js +43 -1
  94. package/server/cc-session-watcher.js +54 -5
  95. package/server/compose-mcp-tools.js +48 -50
  96. package/server/compose-mcp.js +0 -2
  97. package/server/design-routes.js +1 -1
  98. package/server/file-watcher.js +14 -0
  99. package/server/ideabox-routes.js +10 -0
  100. package/server/index.js +5 -1
  101. package/server/lifecycle-guard.js +13 -0
  102. package/server/maya-routes.js +111 -7
  103. package/server/mcp-tool-defs.js +0 -25
  104. package/server/mcp-tool-policy.js +6 -13
  105. package/server/stratum-client.js +61 -15
  106. package/server/supervisor.js +18 -4
  107. package/server/vision-routes.js +9 -3
  108. package/dist/assets/channel-SnZzzh7k.js +0 -1
  109. package/dist/assets/classDiagram-6PBFFD2Q-CBu92dSH.js +0 -1
  110. package/dist/assets/classDiagram-v2-HSJHXN6E-CBu92dSH.js +0 -1
  111. package/dist/assets/clone-DgklGjHm.js +0 -1
  112. package/dist/assets/stateDiagram-v2-QKLJ7IA2-DkVLzHbY.js +0 -1
  113. package/lib/append-integrity.js +0 -81
  114. 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
@@ -198,7 +198,7 @@ Signed descriptors live in `.compose/guard-upgrades/<sha256>/` (immutable) with
198
198
  The mobile cockpit at `/m` can be reached from outside localhost — bring your own tunnel, compose handles auth and pairing:
199
199
 
200
200
  ```bash
201
- npm run build # remote serves the built PWA from the API server
201
+ npm run build # source checkouts only; the npm package already ships dist/
202
202
  COMPOSE_REMOTE_AUTH=enabled compose start --host=0.0.0.0
203
203
  compose remote pair --public-host=https://your-tunnel-host # prints a QR — scan it with your phone
204
204
  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
 
@@ -968,6 +970,16 @@ if (cmd === 'new') {
968
970
  console.warn(`--from-idea: idea not found: ${fromIdeaId}`)
969
971
  }
970
972
  } catch (err) {
973
+ // An unreadable ideabox is not a missing idea, and it must not be
974
+ // downgraded into a warning that the build then walks straight past.
975
+ // `--from-idea` is an explicit request for THAT idea's content; building
976
+ // the feature without it produces a plausible-looking design derived from
977
+ // nothing the user asked for, while the ideas sit unread on disk
978
+ // (COMP-IDEABOX-MIGRATE-DIALECT FU-2).
979
+ if (err?.code === 'IDEABOX_UNREADABLE') {
980
+ console.error(`--from-idea: ${err.message}`)
981
+ process.exit(1)
982
+ }
971
983
  console.warn(`--from-idea: could not load idea: ${err.message}`)
972
984
  }
973
985
  }
@@ -2313,7 +2325,8 @@ if (cmd === 'guard') {
2313
2325
  if (sub === 'install') {
2314
2326
  if (!exSync(hookScript)) {
2315
2327
  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')
2328
+ console.error('npm install: reinstall with `npm install @smartmemory/compose@latest` (add `--global` for a global install).')
2329
+ console.error('source checkout: update the checkout, or restore the tracked .claude/hooks/canon-guard.mjs file.')
2317
2330
  process.exit(1)
2318
2331
  }
2319
2332
  const { settings, changed } = installGuardHook(readSettings())
@@ -2346,10 +2359,10 @@ if (cmd === 'guard') {
2346
2359
  else if (st.state === 'stale') console.log('canon-guard: installed (stale — re-run `compose guard install`)')
2347
2360
  else console.log('canon-guard: absent — run `compose guard install`')
2348
2361
  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).
2362
+ // Derived from the registry, never hardcoded, so registry changes cannot
2363
+ // silently desynchronize status output. "Claude-runtime only" is not
2364
+ // modesty Bash and Codex writes never reach this hook (drift detection,
2365
+ // not enforcement).
2353
2366
  const { guardedDisplaysFor } = await import('../lib/canon-registry.js')
2354
2367
  for (const pattern of guardedDisplaysFor('hook')) {
2355
2368
  console.log(` guards: ${pattern} (Write|Edit|NotebookEdit) — Claude-runtime only`)
@@ -2846,9 +2859,11 @@ if (cmd === 'build') {
2846
2859
  if (fresh) singleOpts.fresh = true
2847
2860
  if (nonInteractiveBuild) singleOpts.gateOpts = { nonInteractive: true }
2848
2861
  if (resumeFlowId) singleOpts.resumeFlowId = resumeFlowId
2849
- runBuild(featureCode, singleOpts).then(() => {
2850
- process.exit(0)
2851
- }).catch((err) => {
2862
+ runBuild(featureCode, singleOpts).then(async (result) => {
2863
+ await pendingTeardown()
2864
+ process.exit(abort && result?.ok === false ? 1 : 0)
2865
+ }).catch(async (err) => {
2866
+ await pendingTeardown()
2852
2867
  console.error(`Build failed: ${err.message}`)
2853
2868
  process.exit(1)
2854
2869
  })
@@ -2963,9 +2978,11 @@ if (cmd === 'build') {
2963
2978
  if (agentWorkDir) opts.workingDirectory = agentWorkDir
2964
2979
  if (bugDescription) opts.description = bugDescription
2965
2980
  if (resumeFlowId) opts.resumeFlowId = resumeFlowId
2966
- runBuild(bugCode, opts).then(() => {
2967
- process.exit(0)
2968
- }).catch((err) => {
2981
+ runBuild(bugCode, opts).then(async (result) => {
2982
+ await pendingTeardown()
2983
+ process.exit(abort && result?.ok === false ? 1 : 0)
2984
+ }).catch(async (err) => {
2985
+ await pendingTeardown()
2969
2986
  console.error(`Fix failed: ${err.message}`)
2970
2987
  process.exit(1)
2971
2988
  })
@@ -3074,9 +3091,11 @@ if (cmd === 'build') {
3074
3091
  const opts = { abort, template: 'plan', mode: 'plan', description: intent }
3075
3092
  if (agentWorkDir) opts.workingDirectory = agentWorkDir
3076
3093
  if (resumeFlowId) opts.resumeFlowId = resumeFlowId
3077
- runBuild(planCode, opts).then(() => {
3078
- process.exit(0)
3079
- }).catch((err) => {
3094
+ runBuild(planCode, opts).then(async (result) => {
3095
+ await pendingTeardown()
3096
+ process.exit(abort && result?.ok === false ? 1 : 0)
3097
+ }).catch(async (err) => {
3098
+ await pendingTeardown()
3080
3099
  console.error(`Plan failed: ${err.message}`)
3081
3100
  process.exit(1)
3082
3101
  })
@@ -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" }