@smartmemory/compose 0.5.0 → 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.
@@ -0,0 +1,100 @@
1
+ /** Git object/ref plumbing. Never checks out a branch or changes the real index. */
2
+ import { execFileSync } from 'node:child_process';
3
+ import { withTemporaryIndex, snapshotWorkingTree } from './consumer-fanout.js';
4
+
5
+ export class WaveCheckpointError extends Error {
6
+ constructor(code, message) { super(message); this.name = 'WaveCheckpointError'; this.code = code; }
7
+ }
8
+ const fail = (code, message) => { throw new WaveCheckpointError(code, message); };
9
+ function git(cwd, args, opts = {}) {
10
+ return execFileSync('git', args, { cwd, encoding: 'utf8', stdio: 'pipe', maxBuffer: 512 * 1024 * 1024, ...opts }).trim();
11
+ }
12
+ function symbolicRef(cwd, ref) {
13
+ try { return git(cwd, ['symbolic-ref', '-q', ref]); }
14
+ catch (error) { if (error.status === 1) return null; throw error; }
15
+ }
16
+ function validateRef(cwd, ref) {
17
+ if (typeof ref !== 'string' || !ref.startsWith('refs/heads/compose/wave/')) fail('WAVE_CHECKPOINT_DIVERGED', 'Expected a Compose wave ref');
18
+ try { git(cwd, ['check-ref-format', ref]); }
19
+ catch { fail('WAVE_CHECKPOINT_DIVERGED', 'Invalid wave ref'); }
20
+ if (symbolicRef(cwd, ref)) fail('WAVE_CHECKPOINT_DIVERGED', 'Symbolic wave refs are not allowed');
21
+ // Include other linked worktrees: updating their checked-out branch moves HEAD too.
22
+ const worktrees = git(cwd, ['worktree', 'list', '--porcelain']);
23
+ if (worktrees.split('\n').includes(`branch ${ref}`)) fail('WAVE_CHECKPOINT_DIVERGED', 'Wave ref is checked out in a worktree');
24
+ }
25
+ export function readCheckpointRef({ cwd, ref }) {
26
+ validateRef(cwd, ref);
27
+ try { return git(cwd, ['show-ref', '--verify', '--hash', ref]); }
28
+ catch (error) { if (error.status === 1 || error.status === 128 && /not a valid ref/.test(error.stderr?.toString())) return null; throw error; }
29
+ }
30
+ export function prepareCheckpoint({ cwd, ref, parentCommit, tree, workingTree, message, commitMetadata }) {
31
+ validateRef(cwd, ref);
32
+ if (typeof parentCommit !== 'string' || !/^[a-f0-9]{40,64}$/.test(parentCommit)) fail('WAVE_CHECKPOINT_EVIDENCE_MISSING', 'Parent must be a commit OID');
33
+ const parent = git(cwd, ['rev-parse', '--verify', `${parentCommit}^{commit}`]);
34
+ const capturedTree = tree ?? (workingTree === true ? snapshotWorkingTree(cwd) : workingTree);
35
+ if (!capturedTree) fail('WAVE_CHECKPOINT_EVIDENCE_MISSING', 'Checkpoint needs a tree or workingTree:true');
36
+ const treeId = git(cwd, ['rev-parse', '--verify', `${capturedTree}^{tree}`]);
37
+ const metadata = structuredClone(commitMetadata ?? {
38
+ authorName: git(cwd, ['config', 'user.name']), authorEmail: git(cwd, ['config', 'user.email']),
39
+ date: new Date().toISOString(),
40
+ });
41
+ if (!metadata.authorName || !metadata.authorEmail || !Number.isFinite(Date.parse(metadata.date))) fail('WAVE_CHECKPOINT_EVIDENCE_MISSING', 'Invalid pinned commit metadata');
42
+ metadata.date = new Date(metadata.date).toISOString();
43
+ if (typeof message !== 'string' || !message) fail('WAVE_CHECKPOINT_EVIDENCE_MISSING', 'Checkpoint message is required');
44
+ const env = { ...process.env, GIT_AUTHOR_NAME: metadata.authorName, GIT_COMMITTER_NAME: metadata.authorName,
45
+ GIT_AUTHOR_EMAIL: metadata.authorEmail, GIT_COMMITTER_EMAIL: metadata.authorEmail,
46
+ GIT_AUTHOR_DATE: metadata.date, GIT_COMMITTER_DATE: metadata.date };
47
+ const commit = git(cwd, ['commit-tree', treeId, '-p', parent], { input: message, env });
48
+ return { ref, parentCommit: parent, tree: treeId, commit, message, commitMetadata: metadata };
49
+ }
50
+ export function publishCheckpoint({ cwd, ref, expected, commit }) {
51
+ validateRef(cwd, ref);
52
+ git(cwd, ['cat-file', '-e', `${commit}^{commit}`]);
53
+ const zero = '0'.repeat(git(cwd, ['rev-parse', 'HEAD']).length);
54
+ try { git(cwd, ['update-ref', ref, commit, expected ?? zero]); }
55
+ catch (error) { fail('WAVE_CHECKPOINT_DIVERGED', `Checkpoint compare-and-swap refused: ${error.message}`); }
56
+ return commit;
57
+ }
58
+ export function worktreeBaseFor({ journal, ref }) {
59
+ const wave = journal?.wave;
60
+ if (!wave) return null;
61
+ if (wave.checkpoints.some(checkpoint => checkpoint.state !== 'published')) fail('WAVE_CHECKPOINT_EVIDENCE_MISSING', 'Checkpoint publication is pending');
62
+ const expected = wave.checkpoints.at(-1)?.commit ?? null;
63
+ if (ref !== expected) fail('WAVE_CHECKPOINT_DIVERGED', 'Wave ref differs from the published journal tip');
64
+ return expected ?? wave.baseCommit;
65
+ }
66
+ export function squashOntoBase({ cwd, ref, base }) {
67
+ if (git(cwd, ['rev-parse', 'HEAD']) !== base) fail('WAVE_CHECKPOINT_DIVERGED', 'HEAD moved from the pinned base');
68
+ const tip = readCheckpointRef({ cwd, ref });
69
+ if (!tip) fail('WAVE_CHECKPOINT_EVIDENCE_MISSING', 'Wave ref is missing');
70
+ return withTemporaryIndex(cwd, env => {
71
+ git(cwd, ['read-tree', base], { env });
72
+ const diff = execFileSync('git', ['diff', '--binary', base, tip, '--'], { cwd, encoding: 'utf8', maxBuffer: 512 * 1024 * 1024 });
73
+ if (diff) git(cwd, ['apply', '--cached', '--binary', '-'], { env, input: diff });
74
+ const tree = git(cwd, ['write-tree'], { env });
75
+ if (tree !== git(cwd, ['rev-parse', `${tip}^{tree}`])) fail('WAVE_CHECKPOINT_DIVERGED', 'Squashed tree differs from checkpoint');
76
+ return tree;
77
+ });
78
+ }
79
+ export function removeCheckpointRef({ cwd, ref, expected }) {
80
+ validateRef(cwd, ref);
81
+ if (!expected) fail('WAVE_CHECKPOINT_EVIDENCE_MISSING', 'Deletion requires the expected tip');
82
+ git(cwd, ['update-ref', '-d', ref, expected]);
83
+ }
84
+ /** Pure resume-table classification; all supplied decisions are durable ordinal evidence. */
85
+ export function reconcileCheckpoint({ journalEntry: entry, refValue }) {
86
+ if (!entry) return refValue == null ? 'ADMIT_FROM_BASE' : 'WAVE_CHECKPOINT_DIVERGED';
87
+ if (entry.evidenceMissing) return 'WAVE_CHECKPOINT_EVIDENCE_MISSING';
88
+ if (entry.diverged) return 'WAVE_CHECKPOINT_DIVERGED';
89
+ if (!entry.commit) {
90
+ if (refValue !== (entry.previousCommit ?? null)) return 'WAVE_CHECKPOINT_DIVERGED';
91
+ return entry.gateOutcome === 'approve' ? 'PREPARE_AND_PUBLISH' : 'RECOVER_WAITING_GATE';
92
+ }
93
+ if (refValue === entry.commit) {
94
+ if (entry.state === 'prepared') return 'MARK_PUBLISHED';
95
+ return entry.terminal ? 'PRESERVE_PUBLISHED' : 'ALREADY_PUBLISHED';
96
+ }
97
+ if (refValue === entry.parentCommit || refValue == null && entry.waveNumber === 1) return 'REPLAY_AND_PUBLISH';
98
+ if (entry.knownAncestorCommits?.includes(refValue)) return 'RECONCILE_IN_ORDER';
99
+ return 'WAVE_CHECKPOINT_DIVERGED';
100
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@smartmemory/compose",
3
- "version": "0.5.0",
3
+ "version": "0.5.1",
4
4
  "description": "Structured AI dev pipeline: your agent writes the code, Compose makes it prove it. Gated design decisions, enforced postconditions, and independent review from goal to shipped code.",
5
5
  "author": "SmartMemory",
6
6
  "license": "MIT",
@@ -88,7 +88,7 @@
88
88
  "@radix-ui/react-toggle-group": "^1.1.11",
89
89
  "@radix-ui/react-tooltip": "^1.2.8",
90
90
  "@smartmemory/sdk-js": "^1.4.60",
91
- "@smartmemory/stratum": "^0.5.0",
91
+ "@smartmemory/stratum": "^0.5.2",
92
92
  "@tanstack/react-virtual": "^3.13.23",
93
93
  "ajv": "^8.18.0",
94
94
  "ajv-formats": "^3.0.1",
@@ -0,0 +1,18 @@
1
+ {
2
+ "plan": "claude:orchestrator:coordinator",
3
+ "execute": { "default": "codex:implementer:critical", "tier_from": "item.tier" },
4
+ "verify": "claude:orchestrator:standard",
5
+ "review": "codex:read-only-reviewer:critical",
6
+ "assess": "claude:orchestrator:coordinator",
7
+ "assess_gate": {
8
+ "decide_from": {
9
+ "step": "assess", "field": "action",
10
+ "approve": ["complete"], "revise": ["repair", "implement"], "kill": ["blocked"]
11
+ },
12
+ "validators": [{ "name": "WaveDecision", "review_step": "review", "tasks_field": "tasks" }]
13
+ },
14
+ "_consumer": {
15
+ "execute": { "ownership": "item.files_owned", "independent": true, "checkpoint_gate": "execute_merge" }
16
+ },
17
+ "_costCeiling": { "input": "cost_ceiling_usd", "default": 150, "gates": ["assess_gate"] }
18
+ }
@@ -0,0 +1,236 @@
1
+ # TEAM PRESET: fable-astra
2
+ #
3
+ # Purpose: Fable plans and assesses bounded waves; Astra implements and reviews.
4
+ # Pattern: Plan → parallel execute → merge → verify → fresh review → assess
5
+ # → repair/implement another wave, ship on complete, or kill on blocked.
6
+ # Capability: Per-task Codex tiers; fresh read-only Astra reviewer; Fable decisions.
7
+ # Isolation: worktree, literal file ownership; sequential merge and checkpoints.
8
+ # Use with: compose build <feature-code> --team fable-astra
9
+ # Customize: Copy BOTH team-fable-astra.stratum.yaml AND its .profiles.json
10
+ # sidecar into your project's pipelines/ and edit them together.
11
+ # Ruling: Concurrency is literal 3, customized by copying this preset, rather
12
+ # than an input as designed (TS schema accepts numeric literals only).
13
+ # cost_ceiling_usd remains an optional flow input, default 150 in the
14
+ # sidecar; compose build --cost-ceiling-usd overrides that ceiling.
15
+ # Carry rules: References use ${} only, never when/set/ensure/iterate.until.
16
+ # Every wave consumer must descend from plan, be reset by the
17
+ # assess_gate revise closure, and precede that gate through after.
18
+ # execute_merge retries preserve wave; assess_gate replaces it.
19
+
20
+ version: 1
21
+
22
+ contracts:
23
+ Task:
24
+ id: string
25
+ description: string
26
+ files_owned: string[]
27
+ files_read: string[]
28
+ depends_on: string[]
29
+ tier: critical|standard|fast
30
+ tier_rationale: string
31
+ TaskGraph:
32
+ tasks: Task[]
33
+ Verification:
34
+ commands: string[]
35
+ outcomes: string[]
36
+ TaskResult:
37
+ outcome: string
38
+ summary: string
39
+ files_changed: string[]
40
+ verification: Verification
41
+ VerifyResult:
42
+ tests_pass: boolean
43
+ summary: string
44
+ verification: Verification
45
+ merged_diff: string
46
+ Finding:
47
+ severity: string
48
+ files: string[]
49
+ claim: string
50
+ evidence: string
51
+ ReviewFindings:
52
+ findings: Finding[]
53
+ blocking: boolean
54
+ WaveDecision:
55
+ action: repair|implement|complete|blocked
56
+ tasks: Task[]
57
+ rationale: string
58
+ addressed_findings: Finding[]
59
+ open_findings: Finding[]
60
+ blocking: boolean
61
+ open_count: integer
62
+ ShipResult:
63
+ phase: string
64
+ artifact: string
65
+ outcome: string
66
+ summary: string
67
+ files_changed: string[]?
68
+ commit_hash: string?
69
+
70
+ flows:
71
+ entry: team_fable_astra
72
+ team_fable_astra:
73
+ input:
74
+ featureCode: string
75
+ description: string
76
+ # Accept the existing feature-build envelope; providers stay pinned below.
77
+ implementer_agent: string?
78
+ reviewer_agent: string?
79
+ pre_merge_gate: string[]?
80
+ cost_ceiling_usd: number?
81
+ output:
82
+ from: "${ship.output}"
83
+ contract: ShipResult
84
+ max_rounds: 4
85
+ carry:
86
+ wave:
87
+ initial: "${plan.output.tasks}"
88
+ on_revise:
89
+ assess_gate: "${assess.output.tasks}"
90
+ steps:
91
+ - id: plan
92
+ agent: claude
93
+ do: |
94
+ Plan wave 1 for ${input.featureCode}. Goal and acceptance criteria:
95
+ ${input.description}
96
+ Read the feature's design, brief, plan and acceptance criteria before planning.
97
+ MUST checklist:
98
+ - Return TaskGraph with 1..6 tasks, each with id, description, files_owned,
99
+ files_read, depends_on, tier and a one-line tier_rationale.
100
+ - Wave 1 must contain only mutually independent tasks: depends_on is empty
101
+ for every task. Waves carry dependencies; dependent work goes in later
102
+ waves after its prerequisite checkpoint, not into this wave.
103
+ - No two tasks may share files_owned. Redo the graph if ownership overlaps.
104
+ - files_owned must be literal repo-relative paths, never globs or directories.
105
+ - Use critical for tasks with design judgment or root-causing, standard
106
+ for brief-bounded implementation, fast for transcription-level edits; repair
107
+ waves default to the tier of the task being repaired or higher, never lower.
108
+ - Each task must fit one focused session; target 2-4 tasks for a typical feature.
109
+ Return the task graph only; do not implement or commit.
110
+ out: TaskGraph
111
+ ensure:
112
+ - expr: "len(result.tasks) >= 1"
113
+ - expr: "len(result.tasks) <= 6"
114
+ attempts: 3
115
+
116
+ - id: execute
117
+ after: [plan]
118
+ fanout:
119
+ over: "${wave}"
120
+ dispatch: consumer
121
+ concurrency: 3
122
+ isolation: worktree
123
+ require: all
124
+ merge: sequential
125
+ steps:
126
+ - agent: codex
127
+ do: |
128
+ Implement the task described by ${item} using TDD. Write the test
129
+ first, watch it fail, implement, and watch it pass. Honor the
130
+ item's id, description, files_owned (you may create/modify these)
131
+ and files_read (you may read but NOT modify these).
132
+ MUST touch only files_owned — anything else fails the task at merge.
133
+ MUST return {outcome, summary, files_changed, verification}, with
134
+ verification {commands, outcomes}: exact commands and corresponding
135
+ observed outcomes, including failures. Do not commit.
136
+ out: TaskResult
137
+
138
+ - id: execute_merge
139
+ after: [execute]
140
+ gate:
141
+ on_approve: verify
142
+ on_revise: execute
143
+ on_kill: null
144
+ max_rounds: 2
145
+
146
+ - id: verify
147
+ after: [execute_merge]
148
+ agent: claude
149
+ do: |
150
+ Verify the integrated tree for ${input.featureCode} against the goal and
151
+ acceptance criteria: ${input.description}
152
+ MUST run the project's relevant tests and integration checks on the merged
153
+ tree, check for merge conflicts, and record exact commands and outcomes.
154
+ MUST capture the full cumulative merged diff: git diff HEAD -- for tracked
155
+ files, plus git ls-files --others --exclude-standard and added-file diffs
156
+ using git diff --no-index -- /dev/null <path> for each new feature file
157
+ (exit 1 means differences). HEAD remains the build base until ship; new
158
+ merged files can still be untracked. Return VerifyResult with tests_pass,
159
+ summary, verification {commands, outcomes}, and merged_diff. Describe only
160
+ observed verification evidence; never copy worker summaries. Do not edit.
161
+ Report failures truthfully so review and assess can request a repair wave.
162
+ out: VerifyResult
163
+ ensure:
164
+ - expr: "len(result.summary) > 0"
165
+
166
+ - id: review
167
+ after: [verify]
168
+ agent: codex
169
+ do: |
170
+ Fresh independent read-only review of ${input.featureCode}.
171
+ Goal and acceptance criteria: ${input.description}
172
+ VerifyResult (including the cumulative merged diff): ${verify.output}
173
+ MUST checklist:
174
+ - Read the feature's design, brief and plan for its acceptance criteria.
175
+ Read the merged diff against the goal, those criteria and VerifyResult;
176
+ inspect the integrated source and affected callers.
177
+ - Probe cross-module wiring: a green unit suite is not evidence that the
178
+ user-facing path reaches the implementation. Cite the actual call path.
179
+ - Never read worker summaries, TaskResults, worker journals or transcripts.
180
+ Form your judgment from the diff, source, criteria and verification only.
181
+ - Return one finding per defect, with severity, files (literal repo-relative
182
+ paths), claim and concrete evidence. Include failed verification and unmet
183
+ criteria as findings; set blocking when any defect prevents completion.
184
+ - Return ReviewFindings {findings, blocking}. Do not edit or commit.
185
+ out: ReviewFindings
186
+
187
+ - id: assess
188
+ after: [review]
189
+ agent: claude
190
+ do: |
191
+ Assess ${input.featureCode} against its goal and acceptance criteria:
192
+ ${input.description}
193
+ Current tasks: ${wave}
194
+ TaskResults with verification evidence: ${execute.output}
195
+ VerifyResult and cumulative merged diff: ${verify.output}
196
+ Fresh ReviewFindings: ${review.output}
197
+ MUST checklist:
198
+ - Address every finding: reproduce its full finding object in exactly one
199
+ of addressed_findings or open_findings; justify dispositions in rationale.
200
+ - Copy blocking from review. Set open_count to len(open_findings).
201
+ - repair tasks must each own at least one file named by an open finding.
202
+ Dispatch only affected work; do not repeat accepted unaffected tasks.
203
+ - For repair or implement return 1..6 mutually independent tasks with empty
204
+ depends_on, disjoint literal repo-relative files_owned, and all Task fields.
205
+ Later waves start from the previous checkpoint containing prerequisites.
206
+ - Use critical for tasks with design judgment or root-causing, standard
207
+ for brief-bounded implementation, fast for transcription-level edits; repair
208
+ waves default to the tier of the task being repaired or higher, never lower.
209
+ - Choose implement when additional scoped work is needed; repair for defects.
210
+ - complete only when all acceptance criteria are met, verification passes,
211
+ there are zero open findings and blocking is false; return tasks: [].
212
+ - blocked when repair cannot proceed; retain the open findings (open_count
213
+ must be positive), explain why, and return tasks: [].
214
+ Return WaveDecision only. Do not edit or commit.
215
+ out: WaveDecision
216
+ ensure:
217
+ - expr: "result.action != 'complete' || (result.open_count == 0 && result.blocking == false)"
218
+ - expr: "result.action != 'blocked' || result.open_count > 0"
219
+ - expr: "result.open_count >= 0"
220
+
221
+ - id: assess_gate
222
+ after: [assess]
223
+ gate:
224
+ on_approve: ship
225
+ on_revise: execute
226
+ on_kill: null
227
+ max_rounds: 2
228
+
229
+ # Compose intercepts ship: squash the wave checkpoints and selectively commit.
230
+ - id: ship
231
+ after: [assess_gate]
232
+ agent: claude
233
+ do: |
234
+ Ship ${input.featureCode}: run final tests and squash the accepted wave
235
+ changes into one commit whose parent is the build base. Return ShipResult.
236
+ out: ShipResult
@@ -2,21 +2,26 @@
2
2
  * model-tiers.js — Model tier routing for STRAT-TIER.
3
3
  *
4
4
  * Maps symbolic tier names to provider-specific model IDs.
5
- * Tiers let pipeline specs declare intent (critical / standard / fast)
6
- * without hard-coding model strings — the map here is the single source of truth.
5
+ * Tiers let pipeline specs declare intent (critical / standard / fast / coordinator)
6
+ * without hard-coding model strings — the map here is the single source of truth,
7
+ * including the agent-string tier allow-list. Coordinator lets one preset role
8
+ * explicitly name Fable while critical stays Opus 5; other presets do not move
9
+ * silently to Fable.
7
10
  */
8
11
 
9
12
  /** @type {Record<string, string>} */
10
13
  export const MODEL_TIERS = {
11
- critical: 'claude-opus-4-7',
12
- standard: 'claude-sonnet-4-6',
14
+ critical: 'claude-opus-5',
15
+ standard: 'claude-sonnet-5',
13
16
  fast: 'claude-haiku-4-5-20251001',
17
+ coordinator: 'claude-fable-5-1',
14
18
  };
15
19
 
16
20
  export const CODEX_MODEL_TIERS = {
17
21
  critical: 'gpt-6-astra',
18
22
  standard: 'gpt-5.6-terra',
19
23
  fast: 'gpt-5.3-codex-spark',
24
+ coordinator: null,
20
25
  };
21
26
 
22
27
  // C12: codex efforts follow the routing convention — `low` is for trivial
@@ -26,11 +31,13 @@ const CODEX_TIER_THINKING = {
26
31
  critical: { mode: null, effort: 'high' },
27
32
  standard: { mode: null, effort: 'high' },
28
33
  fast: { mode: null, effort: 'medium' },
34
+ coordinator: null,
29
35
  };
30
36
 
31
37
  /**
32
38
  * Default thinking config per tier.
33
- * - Opus 4.7 / Sonnet 4.6 support adaptive thinking and the effort parameter.
39
+ * - Opus 5 / Sonnet 5 support adaptive thinking and the effort parameter.
40
+ * - Fable 5.1 thinking is always on; adaptive thinking uses effort to control depth.
34
41
  * - Haiku 4.5 doesn't accept the effort parameter (400 error), so fast tier stays off.
35
42
  *
36
43
  * @type {Record<string, { mode: 'adaptive'|'off', effort: 'low'|'medium'|'high'|'xhigh'|'max'|null }>}
@@ -39,13 +46,14 @@ export const TIER_THINKING = {
39
46
  critical: { mode: 'adaptive', effort: 'xhigh' },
40
47
  standard: { mode: 'adaptive', effort: 'high' },
41
48
  fast: { mode: 'off', effort: null },
49
+ coordinator: { mode: 'adaptive', effort: 'high' },
42
50
  };
43
51
 
44
52
  /**
45
53
  * Resolve a tier name to a concrete model ID.
46
54
  *
47
55
  * @param {string|null|undefined} tier
48
- * @returns {string|null} Model ID, or null if tier is unknown / not provided.
56
+ * @returns {string|null} Model ID, or null if tier is unknown, unavailable for the provider, or not provided.
49
57
  */
50
58
  export function resolveTierModel(tier, provider = 'claude') {
51
59
  if (!tier) return null;