@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.
- package/.claude/agents/compose-architect.md +40 -0
- package/.claude/agents/compose-explorer.md +35 -0
- package/.claude/hooks/canon-guard.mjs +52 -0
- package/README.md +15 -1
- package/bin/compose.js +57 -17
- package/bin/git-hooks/pre-push.template +26 -1
- package/bin/receipts-gate.js +39 -0
- package/contracts/fluid-record.schema.json +5 -0
- package/dist/assets/{App-Z4MU-H_F.js → App-DC7paCZv.js} +190 -190
- package/dist/assets/{_baseUniq-ClWoCPFl.js → _baseUniq-Czad7yiy.js} +1 -1
- package/dist/assets/{arc-DY26UIVo.js → arc-EquvLk8y.js} +1 -1
- package/dist/assets/{architectureDiagram-Q4EWVU46-6Ggq4DqJ.js → architectureDiagram-Q4EWVU46-Dr_qinWi.js} +1 -1
- package/dist/assets/{blockDiagram-DXYQGD6D-CH3Ked0l.js → blockDiagram-DXYQGD6D-D2z46ED_.js} +1 -1
- package/dist/assets/{c4Diagram-AHTNJAMY-Bk8dYilu.js → c4Diagram-AHTNJAMY-BHob1Yt0.js} +1 -1
- package/dist/assets/channel-B-7ZRCKC.js +1 -0
- package/dist/assets/{chunk-4BX2VUAB-BMR0XaAQ.js → chunk-4BX2VUAB-DomWBRa_.js} +1 -1
- package/dist/assets/{chunk-4TB4RGXK-JytR14a9.js → chunk-4TB4RGXK-WyC_x_DH.js} +1 -1
- package/dist/assets/{chunk-55IACEB6-B4Q97BCP.js → chunk-55IACEB6-BajRv3zx.js} +1 -1
- package/dist/assets/{chunk-EDXVE4YY-R_qarkSf.js → chunk-EDXVE4YY-rMnedK_r.js} +1 -1
- package/dist/assets/{chunk-FMBD7UC4-C9s7KR9m.js → chunk-FMBD7UC4-BPi03Hcb.js} +1 -1
- package/dist/assets/{chunk-OYMX7WX6-BySQzVxc.js → chunk-OYMX7WX6-B7J_mKX0.js} +1 -1
- package/dist/assets/{chunk-QZHKN3VN-DdpSYZsW.js → chunk-QZHKN3VN-BLXTVr8N.js} +1 -1
- package/dist/assets/{chunk-YZCP3GAM-iE_tzriw.js → chunk-YZCP3GAM-BYWjo2OJ.js} +1 -1
- package/dist/assets/classDiagram-6PBFFD2Q-Balz1OEB.js +1 -0
- package/dist/assets/classDiagram-v2-HSJHXN6E-Balz1OEB.js +1 -0
- package/dist/assets/clone-CfNV0lUO.js +1 -0
- package/dist/assets/{cose-bilkent-S5V4N54A-BdlU6ZX_.js → cose-bilkent-S5V4N54A-Coaq0xaU.js} +1 -1
- package/dist/assets/{dagre-KV5264BT-Cp3F5KTn.js → dagre-KV5264BT-DvUvAxlj.js} +1 -1
- package/dist/assets/{diagram-5BDNPKRD-DiR6_2q_.js → diagram-5BDNPKRD-70bXRUXV.js} +1 -1
- package/dist/assets/{diagram-G4DWMVQ6-w0i-p5HX.js → diagram-G4DWMVQ6-hMA8wgzx.js} +1 -1
- package/dist/assets/{diagram-MMDJMWI5-tIHhwUv3.js → diagram-MMDJMWI5-BNir7C6i.js} +1 -1
- package/dist/assets/{diagram-TYMM5635-BAeY3B19.js → diagram-TYMM5635-BCYl1xrE.js} +1 -1
- package/dist/assets/{erDiagram-SMLLAGMA-Ckx_Knko.js → erDiagram-SMLLAGMA-bjxP0_bt.js} +1 -1
- package/dist/assets/{flowDiagram-DWJPFMVM-DeoNka6J.js → flowDiagram-DWJPFMVM-CBn9fhEp.js} +1 -1
- package/dist/assets/{ganttDiagram-T4ZO3ILL-BmGnFbEg.js → ganttDiagram-T4ZO3ILL-y1O7mWzn.js} +1 -1
- package/dist/assets/{gitGraphDiagram-UUTBAWPF-Dk48IHsx.js → gitGraphDiagram-UUTBAWPF-DIxwDXHB.js} +1 -1
- package/dist/assets/{graph-BNzKGvoy.js → graph-9D1ZumWp.js} +1 -1
- package/dist/assets/{index-BEfrNBp8.js → index-Ds_IXQo3.js} +2 -2
- package/dist/assets/{infoDiagram-42DDH7IO-BRf827i0.js → infoDiagram-42DDH7IO-DsWLGhaY.js} +1 -1
- package/dist/assets/{ishikawaDiagram-UXIWVN3A-0kCZaeCM.js → ishikawaDiagram-UXIWVN3A-CipZIE90.js} +1 -1
- package/dist/assets/{journeyDiagram-VCZTEJTY-rvU7ayRt.js → journeyDiagram-VCZTEJTY-Vr5xqcQm.js} +1 -1
- package/dist/assets/{kanban-definition-6JOO6SKY-DpQwX1C5.js → kanban-definition-6JOO6SKY-EqUYneyh.js} +1 -1
- package/dist/assets/{layout-BI8cXFPI.js → layout-hfWIIs0-.js} +1 -1
- package/dist/assets/{linear-a0glcDiw.js → linear-BdDWoN0t.js} +1 -1
- package/dist/assets/{min-vPHfnXcC.js → min-Bn_xAS7n.js} +1 -1
- package/dist/assets/{mindmap-definition-QFDTVHPH-D14eF-7C.js → mindmap-definition-QFDTVHPH-qsgubzCF.js} +1 -1
- package/dist/assets/{pieDiagram-DEJITSTG-Cno-gETh.js → pieDiagram-DEJITSTG-Bv1xq_58.js} +1 -1
- package/dist/assets/{quadrantDiagram-34T5L4WZ-BUQM1Hfm.js → quadrantDiagram-34T5L4WZ-DwMbAegF.js} +1 -1
- package/dist/assets/{requirementDiagram-MS252O5E-pOXlN2-q.js → requirementDiagram-MS252O5E-BJVmLNcp.js} +1 -1
- package/dist/assets/{sankeyDiagram-XADWPNL6-Crynd3_b.js → sankeyDiagram-XADWPNL6-o5GZb8Y1.js} +1 -1
- package/dist/assets/{sequenceDiagram-FGHM5R23-D9fZdCM8.js → sequenceDiagram-FGHM5R23-ocqJp2qk.js} +1 -1
- package/dist/assets/{stateDiagram-FHFEXIEX-CW9qVec8.js → stateDiagram-FHFEXIEX-DGaDUFxP.js} +1 -1
- package/dist/assets/stateDiagram-v2-QKLJ7IA2-Dz-15i-r.js +1 -0
- package/dist/assets/{timeline-definition-GMOUNBTQ-BcHzhm_8.js → timeline-definition-GMOUNBTQ-C4YwFvAn.js} +1 -1
- package/dist/assets/{vennDiagram-DHZGUBPP-BfytJcWk.js → vennDiagram-DHZGUBPP-uOKn9j-y.js} +1 -1
- package/dist/assets/{wardley-RL74JXVD-DLj-IjyB.js → wardley-RL74JXVD-DIQSmQde.js} +1 -1
- package/dist/assets/{wardleyDiagram-NUSXRM2D-Ds0Ue68c.js → wardleyDiagram-NUSXRM2D-CdamsEDC.js} +1 -1
- package/dist/assets/{xychartDiagram-5P7HB3ND-vjWDXFL6.js → xychartDiagram-5P7HB3ND-DhLs41yk.js} +1 -1
- package/dist/index.html +1 -1
- package/lib/agent-string.js +9 -4
- package/lib/build-cancel.js +205 -0
- package/lib/build-stream-writer.js +6 -0
- package/lib/build.js +1189 -165
- package/lib/canon-guard.js +3 -24
- package/lib/canon-registry.js +2 -71
- package/lib/codex-preflight.js +8 -0
- package/lib/colleague/context.js +123 -0
- package/lib/consumer-fanout.js +427 -17
- package/lib/decision-blocks.js +38 -0
- package/lib/dispatch-ledger.js +7 -0
- package/lib/experiment-pricing.js +5 -1
- package/lib/flow-state.js +38 -0
- package/lib/fluid/factory.js +112 -1
- package/lib/fluid/ideabox-manifest.js +203 -0
- package/lib/fluid/ideabox-migrate.js +177 -29
- package/lib/fluid/ideabox-preamble.js +155 -0
- package/lib/fluid/ideabox-readable.js +83 -0
- package/lib/fluid/ideabox-recover.js +393 -0
- package/lib/fluid/import-ideabox.js +188 -45
- package/lib/fluid/local-provider.js +6 -0
- package/lib/fluid/portfolio.js +255 -0
- package/lib/fluid/record-shape.js +7 -0
- package/lib/fluid/render-ideabox.js +153 -7
- package/lib/fluid/smartmemory-provider.js +6 -0
- package/lib/gate-prompt.js +14 -7
- package/lib/gsd.js +95 -48
- package/lib/ideabox-cli.js +68 -0
- package/lib/ideabox.js +209 -9
- package/lib/maya-identity.js +16 -2
- package/lib/model-pricing.js +4 -1
- package/lib/output-gate.js +81 -0
- package/lib/pipeline-profiles.js +200 -0
- package/lib/process-termination.js +121 -3
- package/lib/receipts-gate.js +268 -0
- package/lib/result-normalizer.js +41 -1
- package/lib/smartmemory-client.js +68 -1
- package/lib/stratum-mcp-client.js +104 -5
- package/lib/team-flag.js +1 -1
- package/lib/tool-inventory.js +0 -1
- package/lib/version-check.js +9 -3
- package/lib/wave-checkpoint.js +100 -0
- package/package.json +7 -5
- package/presets/team-fable-astra.profiles.json +18 -0
- package/presets/team-fable-astra.stratum.yaml +236 -0
- package/server/build-stream-bridge.js +43 -1
- package/server/cc-session-watcher.js +54 -5
- package/server/compose-mcp-tools.js +48 -50
- package/server/compose-mcp.js +0 -2
- package/server/design-routes.js +1 -1
- package/server/file-watcher.js +14 -0
- package/server/ideabox-routes.js +10 -0
- package/server/index.js +5 -1
- package/server/lifecycle-guard.js +13 -0
- package/server/maya-routes.js +111 -7
- package/server/mcp-tool-defs.js +0 -25
- package/server/mcp-tool-policy.js +6 -13
- package/server/model-tiers.js +14 -6
- package/server/stratum-client.js +61 -15
- package/server/supervisor.js +18 -4
- package/server/vision-routes.js +9 -3
- package/dist/assets/channel-SnZzzh7k.js +0 -1
- package/dist/assets/classDiagram-6PBFFD2Q-CBu92dSH.js +0 -1
- package/dist/assets/classDiagram-v2-HSJHXN6E-CBu92dSH.js +0 -1
- package/dist/assets/clone-DgklGjHm.js +0 -1
- package/dist/assets/stateDiagram-v2-QKLJ7IA2-DkVLzHbY.js +0 -1
- package/lib/append-integrity.js +0 -81
- 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 #
|
|
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
|
|
569
|
-
//
|
|
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('
|
|
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
|
|
2350
|
-
// silently
|
|
2351
|
-
//
|
|
2352
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
2851
|
-
|
|
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
|
-
|
|
2968
|
-
|
|
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
|
-
|
|
3079
|
-
|
|
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
|
|
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" }
|