@polderlabs/bizar 10.20.1 → 10.21.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.
@@ -0,0 +1,227 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * cli/commands/workflow-gc.mjs — Phase B (v10.21.0) B.3
4
+ *
5
+ * Garbage-collects workflow run artifact directories under
6
+ * `<cwd>/.bizar/runs/`. Each run is a directory written by
7
+ * `config/workflows/lib/dispatch.js#writeArtifact` (B.1).
8
+ *
9
+ * Policy:
10
+ * - TTL: 14 days since the directory's mtime (configurable via
11
+ * `--max-age-days`).
12
+ * - In-progress gate: if `feature_list.json` has any feature with
13
+ * `state: in_progress`, NO directories are deleted (conservative;
14
+ * we have no feature -> runId mapping). The dry-run reports this
15
+ * explicitly so the operator sees the skip reason.
16
+ * - Permission failures: skip + warn, never abort the run.
17
+ * - Idempotent: re-running after a successful GC is a no-op.
18
+ *
19
+ * Usage:
20
+ * node cli/commands/workflow-gc.mjs # real deletion
21
+ * node cli/commands/workflow-gc.mjs --dry-run # list only
22
+ * node cli/commands/workflow-gc.mjs --max-age-days=7
23
+ * node cli/commands/workflow-gc.mjs --root <path> # override run root
24
+ *
25
+ * Exit codes:
26
+ * 0 success (every candidate either deleted or explicitly skipped)
27
+ * 1 at least one delete failed (dry-run is exit 0; the operator
28
+ * reviews the per-row status and re-runs)
29
+ */
30
+ import { existsSync, readFileSync, rmSync, writeFileSync, mkdirSync } from 'node:fs';
31
+ import { resolve } from 'node:path';
32
+ import { fileURLToPath } from 'node:url';
33
+ import { dirname } from 'node:path';
34
+ import { pathToFileURL } from 'node:url';
35
+
36
+ const here = dirname(fileURLToPath(import.meta.url));
37
+ const repoRoot = resolve(here, '..', '..');
38
+ const dispatchPath = resolve(repoRoot, 'config', 'workflows', 'lib', 'dispatch.js');
39
+ const dispatch = await import(pathToFileURL(dispatchPath).href);
40
+
41
+ const { listRuns } = dispatch;
42
+
43
+ const DEFAULT_MAX_AGE_DAYS = 14;
44
+ const MS_PER_DAY = 24 * 60 * 60 * 1000;
45
+
46
+ function parseArgs(argv) {
47
+ const args = { dryRun: false, maxAgeDays: DEFAULT_MAX_AGE_DAYS, root: undefined };
48
+ for (const a of argv.slice(2)) {
49
+ if (a === '--dry-run') args.dryRun = true;
50
+ else if (a === '--help' || a === '-h') args.help = true;
51
+ else if (a.startsWith('--max-age-days=')) {
52
+ const n = Number(a.slice('--max-age-days='.length));
53
+ if (Number.isFinite(n) && n >= 0) args.maxAgeDays = n;
54
+ } else if (a.startsWith('--root=')) {
55
+ args.root = a.slice('--root='.length);
56
+ }
57
+ }
58
+ return args;
59
+ }
60
+
61
+ function printHelp() {
62
+ process.stdout.write(`workflow-gc — delete .bizar/runs/<run-id>/ older than N days.
63
+
64
+ Usage:
65
+ node cli/commands/workflow-gc.mjs real deletion
66
+ node cli/commands/workflow-gc.mjs --dry-run list candidates, no deletions
67
+ node cli/commands/workflow-gc.mjs --max-age-days=N override the 14-day default TTL
68
+ node cli/commands/workflow-gc.mjs --root <path> override the artifact root
69
+
70
+ Exit codes:
71
+ 0 success (every candidate either deleted or explicitly skipped)
72
+ 1 at least one delete failed (operator reviews + re-runs)
73
+ `);
74
+ }
75
+
76
+ /**
77
+ * Inspect feature_list.json#in_progress. Returns
78
+ * `{ inProgress: boolean, ids: string[] }`. When the file is missing
79
+ * or unreadable, inProgress is false (no gate) — a missing
80
+ * feature_list.json is not the GC tool's problem to fix.
81
+ */
82
+ function inProgressFeatureIds(cwd) {
83
+ const p = resolve(cwd, 'feature_list.json');
84
+ if (!existsSync(p)) return { inProgress: false, ids: [] };
85
+ try {
86
+ const json = JSON.parse(readFileSync(p, 'utf8'));
87
+ const features = Array.isArray(json?.features) ? json.features : [];
88
+ const ids = features.filter((f) => f && f.state === 'in_progress').map((f) => f.id || '<no-id>');
89
+ return { inProgress: ids.length > 0, ids };
90
+ } catch {
91
+ return { inProgress: false, ids: [] };
92
+ }
93
+ }
94
+
95
+ /**
96
+ * Build a candidate list. Each row carries enough metadata for the
97
+ * dry-run output + the deletion loop:
98
+ * { runId, path, ageDays, size, action: 'delete' | 'skip:<reason>' }
99
+ *
100
+ * Skip reasons:
101
+ * 'too-recent' — mtime within the TTL window
102
+ * 'in-progress' — feature_list.json has any in_progress feature
103
+ * 'permission-denied' — stat/rm threw EACCES or EPERM
104
+ * 'missing' — directory vanished between listRuns() and stat()
105
+ */
106
+ function planCandidates({ runs, nowMs, maxAgeDays, inProgress }) {
107
+ const out = [];
108
+ for (const run of runs) {
109
+ const ageDays = (nowMs - run.mtimeMs) / MS_PER_DAY;
110
+ if (inProgress) {
111
+ out.push({ runId: run.runId, path: run.path, ageDays, size: run.size, action: 'skip:in-progress' });
112
+ continue;
113
+ }
114
+ if (ageDays < maxAgeDays) {
115
+ out.push({ runId: run.runId, path: run.path, ageDays, size: run.size, action: 'skip:too-recent' });
116
+ continue;
117
+ }
118
+ out.push({ runId: run.runId, path: run.path, ageDays, size: run.size, action: 'delete' });
119
+ }
120
+ return out;
121
+ }
122
+
123
+ /**
124
+ * Execute the plan. Returns `{ rows, errors }` where `errors` is the
125
+ * list of (runId, message) pairs for failed deletions. Best-effort:
126
+ * every error is recorded but the loop continues so a single
127
+ * permission-denied directory does not block the rest of the sweep.
128
+ */
129
+ function executePlan(rows, { dryRun, nowMs }) {
130
+ const errors = [];
131
+ const out = [];
132
+ for (const row of rows) {
133
+ if (row.action !== 'delete' || dryRun) {
134
+ out.push(row);
135
+ continue;
136
+ }
137
+ try {
138
+ rmSync(row.path, { recursive: true, force: true });
139
+ out.push({ ...row, deletedAt: new Date(nowMs).toISOString() });
140
+ } catch (err) {
141
+ const message = err && err.message ? err.message : String(err);
142
+ const reason = /EACCES|EPERM/.test(message) ? 'permission-denied' : 'unknown';
143
+ out.push({ ...row, action: `error:${reason}`, error: message });
144
+ errors.push({ runId: row.runId, message });
145
+ }
146
+ }
147
+ return { rows: out, errors };
148
+ }
149
+
150
+ function formatRows(rows) {
151
+ const lines = [];
152
+ for (const row of rows) {
153
+ const tag = row.action.startsWith('skip:')
154
+ ? `SKIP (${row.action.slice('skip:'.length)})`
155
+ : row.action.startsWith('error:')
156
+ ? `ERROR (${row.action.slice('error:'.length)})`
157
+ : row.deletedAt
158
+ ? 'DELETED'
159
+ : 'DELETE';
160
+ lines.push(` ${tag.padEnd(22)} ${row.runId.padEnd(38)} age=${row.ageDays.toFixed(2)}d size=${row.size}B`);
161
+ }
162
+ return lines.join('\n');
163
+ }
164
+
165
+ function emitGcLog(rows, errors, { dryRun, maxAgeDays, inProgressIds, outputPath }) {
166
+ const summary = {
167
+ dryRun,
168
+ maxAgeDays,
169
+ inProgressIds,
170
+ deleted: rows.filter((r) => r.deletedAt).length,
171
+ skipped: rows.filter((r) => r.action.startsWith('skip:')).length,
172
+ errors: rows.filter((r) => r.action.startsWith('error:')).length,
173
+ rows,
174
+ };
175
+ if (outputPath) {
176
+ try {
177
+ mkdirSync(dirname(outputPath), { recursive: true });
178
+ writeFileSync(outputPath, JSON.stringify(summary, null, 2));
179
+ } catch {
180
+ // best-effort: the console output is the source of truth
181
+ }
182
+ }
183
+ return summary;
184
+ }
185
+
186
+ async function main() {
187
+ const args = parseArgs(process.argv);
188
+ if (args.help) {
189
+ printHelp();
190
+ process.exit(0);
191
+ }
192
+ const root = args.root ? resolve(args.root) : undefined;
193
+ const cwd = process.cwd();
194
+ const nowMs = Date.now();
195
+ const inProgress = inProgressFeatureIds(cwd);
196
+ const runs = listRuns({ runRoot: root });
197
+ const plan = planCandidates({ runs, nowMs, maxAgeDays: args.maxAgeDays, inProgress: inProgress.inProgress });
198
+ const { rows } = executePlan(plan, { dryRun: args.dryRun, nowMs });
199
+ const gcLogPath = resolve(cwd, '.bizar', 'runs', 'gc.json');
200
+ const summary = emitGcLog(rows, [], { dryRun: args.dryRun, maxAgeDays: args.maxAgeDays, inProgressIds: inProgress.ids, outputPath: gcLogPath });
201
+
202
+ process.stdout.write(
203
+ [
204
+ `▶ workflow-gc ${args.dryRun ? '(dry-run)' : ''}`,
205
+ ` root: ${root || resolve(cwd, '.bizar', 'runs')}`,
206
+ ` max-age-days: ${args.maxAgeDays}`,
207
+ ` in-progress: ${inProgress.inProgress ? `yes [${inProgress.ids.join(', ')}]` : 'no'}`,
208
+ ` candidates: ${rows.length}`,
209
+ ` deleted: ${summary.deleted}`,
210
+ ` skipped: ${summary.skipped}`,
211
+ ` errors: ${summary.errors}`,
212
+ '',
213
+ formatRows(rows),
214
+ '',
215
+ ].join('\n'),
216
+ );
217
+
218
+ if (summary.errors > 0) {
219
+ process.exit(1);
220
+ }
221
+ process.exit(0);
222
+ }
223
+
224
+ main().catch((err) => {
225
+ process.stderr.write(`workflow-gc failed: ${err && err.message ? err.message : err}\n`);
226
+ process.exit(1);
227
+ });
@@ -1,4 +1,5 @@
1
- import { dispatchAgent } from './lib/dispatch.js'
1
+ import { randomUUID } from 'node:crypto'
2
+ import { dispatchAgent, writeArtifact, barrierRef } from './lib/dispatch.js'
2
3
 
3
4
  export const meta = {
4
5
  name: 'bizar-debug',
@@ -21,6 +22,10 @@ const BUG_ID = typeof args === 'string'
21
22
  ? args.topic
22
23
  : JSON.stringify(args || {})
23
24
 
25
+ // Phase B (v10.21.0) artifact-on-disk barriers: one runId per workflow
26
+ // invocation. Used by every writeArtifact() + barrierRef() in this script.
27
+ const RUN_ID = randomUUID()
28
+
24
29
  const HYPOTHESIS = {
25
30
  type: 'object',
26
31
  required: ['cause', 'experiment', 'predictedOutcome'],
@@ -43,11 +48,15 @@ const iterations = []
43
48
  phase('Hypothesis')
44
49
  const initial = await dispatchAgent(agent, 'rca-hypothesis', `Root-cause bug ${BUG_ID} with the cheapest discriminating experiment. Return {cause, experiment, predictedOutcome}. Do not propose a fix yet.`, { role: 'research-analyst', risk: 'medium', capabilities: ['structured-output', 'reasoning'], label: 'hypothesis:initial', phase: 'Hypothesis', schema: HYPOTHESIS })
45
50
  iterations.push(initial)
51
+ // Phase B: persist initial hypothesis artifact.
52
+ writeArtifact({ runId: RUN_ID, phase: 'Hypothesis', label: 'hypothesis:initial', payload: initial, summary: initial?.cause ? initial.cause.slice(0, 200) : 'initial hypothesis', role: 'research-analyst' })
46
53
 
47
54
  let accepted = null
48
55
  for (let i = 0; i < MAX_ITERATIONS; i++) {
49
56
  phase('AdversarialVerify')
50
- const verdict = await dispatchAgent(agent, 'rca-verifier', `Refute the RCA hypothesis for bug ${BUG_ID}. Inspect the predicted experiment and reject it if it is speculative, pre-existing, unreachable, or already covered by an existing test.\nHypothesis: ${JSON.stringify(iterations[iterations.length - 1])}`, { role: 'adversarial', risk: 'high', capabilities: ['structured-output', 'reasoning'], label: `verify:${i + 1}`, phase: 'AdversarialVerify', schema: VERDICT })
57
+ const prior = iterations[iterations.length - 1];
58
+ const priorLabel = i === 0 ? 'hypothesis:initial' : `refine:${i}`;
59
+ const verdict = await dispatchAgent(agent, 'rca-verifier', `Refute the RCA hypothesis for bug ${BUG_ID}. Inspect the predicted experiment and reject it if it is speculative, pre-existing, unreachable, or already covered by an existing test.\n${barrierRef({ runId: RUN_ID, phase: 'Hypothesis', label: priorLabel, summary: prior?.cause ? prior.cause.slice(0, 200) : `hypothesis iter ${i + 1}` }).promptBlock}`, { role: 'adversarial', risk: 'high', capabilities: ['structured-output', 'reasoning'], label: `verify:${i + 1}`, phase: 'AdversarialVerify', schema: VERDICT })
51
60
  if (verdict && verdict.confirmed) {
52
61
  accepted = { iteration: i + 1, hypothesis: iterations[iterations.length - 1], verdict }
53
62
  break
@@ -57,8 +66,10 @@ for (let i = 0; i < MAX_ITERATIONS; i++) {
57
66
  break
58
67
  }
59
68
  phase('Loop')
60
- const refined = await dispatchAgent(agent, 'rca-refiner', `The previous RCA hypothesis for bug ${BUG_ID} was not confirmed. Produce a refined hypothesis with a new cheapest discriminating experiment.\nPrior: ${JSON.stringify(iterations[iterations.length - 1])}\nRejection: ${JSON.stringify(verdict)}`, { role: 'research-analyst', risk: 'medium', capabilities: ['structured-output', 'reasoning'], label: `refine:${i + 1}`, phase: 'Loop', schema: HYPOTHESIS })
69
+ const refined = await dispatchAgent(agent, 'rca-refiner', `The previous RCA hypothesis for bug ${BUG_ID} was not confirmed. Produce a refined hypothesis with a new cheapest discriminating experiment.\n${barrierRef({ runId: RUN_ID, phase: 'Hypothesis', label: priorLabel, summary: prior?.cause ? prior.cause.slice(0, 200) : `prior iter ${i + 1}` }).promptBlock}\n${barrierRef({ runId: RUN_ID, phase: 'AdversarialVerify', label: `verify:${i + 1}`, summary: verdict?.reason ? verdict.reason.slice(0, 200) : 'no confirmation' }).promptBlock}`, { role: 'research-analyst', risk: 'medium', capabilities: ['structured-output', 'reasoning'], label: `refine:${i + 1}`, phase: 'Loop', schema: HYPOTHESIS })
61
70
  iterations.push(refined)
71
+ // Phase B: persist refined hypothesis artifact.
72
+ writeArtifact({ runId: RUN_ID, phase: 'Hypothesis', label: `refine:${i + 1}`, payload: refined, summary: refined?.cause ? refined.cause.slice(0, 200) : `refined iter ${i + 1}`, role: 'research-analyst' });
62
73
  }
63
74
 
64
75
  if (!accepted) {
@@ -71,10 +82,11 @@ if (!accepted) {
71
82
  }
72
83
 
73
84
  phase('Fix')
74
- const fix = await dispatchAgent(agent, 'fix-author', `Produce the smallest fix + regression test for bug ${BUG_ID} based on the accepted hypothesis. Do not commit, push, publish, or deploy.\nAccepted hypothesis: ${JSON.stringify(accepted.hypothesis)}`, { role: 'implementer', risk: 'medium', capabilities: ['structured-output', 'reasoning'], label: 'fix', phase: 'Fix' })
85
+ const fix = await dispatchAgent(agent, 'fix-author', `Produce the smallest fix + regression test for bug ${BUG_ID} based on the accepted hypothesis. Do not commit, push, publish, or deploy.\n${barrierRef({ runId: RUN_ID, phase: 'Hypothesis', label: 'hypothesis:initial', summary: accepted.hypothesis?.cause ? accepted.hypothesis.cause.slice(0, 200) : 'accepted hypothesis' }).promptBlock}`, { role: 'implementer', risk: 'medium', capabilities: ['structured-output', 'reasoning'], label: 'fix', phase: 'Fix' })
86
+ writeArtifact({ runId: RUN_ID, phase: 'Fix', label: 'fix', payload: fix, summary: typeof fix === 'string' ? fix.slice(0, 200) : 'fix proposed', role: 'implementer' })
75
87
 
76
88
  phase('Verify')
77
- const verify = await dispatchAgent(agent, 'fix-verifier', `Re-check the proposed fix for bug ${BUG_ID} against the regression test and adjacent paths. Reject the fix if it is unbounded, out of scope, or already covered.\nFix: ${String(fix)}`, { role: 'adversarial', risk: 'high', capabilities: ['structured-output', 'reasoning'], label: 'fix-verify', phase: 'Verify' })
89
+ const verify = await dispatchAgent(agent, 'fix-verifier', `Re-check the proposed fix for bug ${BUG_ID} against the regression test and adjacent paths. Reject the fix if it is unbounded, out of scope, or already covered.\n${barrierRef({ runId: RUN_ID, phase: 'Fix', label: 'fix', summary: typeof fix === 'string' ? fix.slice(0, 200) : 'fix artifact' }).promptBlock}`, { role: 'adversarial', risk: 'high', capabilities: ['structured-output', 'reasoning'], label: 'fix-verify', phase: 'Verify' })
78
90
 
79
91
  return {
80
92
  status: 'dry',
@@ -1,4 +1,5 @@
1
- import { dispatchAgent } from './lib/dispatch.js'
1
+ import { randomUUID } from 'node:crypto'
2
+ import { dispatchAgent, writeArtifact, barrierRef } from './lib/dispatch.js'
2
3
 
3
4
  export const meta = {
4
5
  name: 'bizar-implement',
@@ -20,6 +21,10 @@ const TOPIC = typeof args === 'string'
20
21
  : JSON.stringify(args || {})
21
22
  const SCOPE = (args && Array.isArray(args.scope)) ? args.scope : []
22
23
 
24
+ // Phase B (v10.21.0) artifact-on-disk barriers: one runId per workflow
25
+ // invocation. Used by every writeArtifact() + barrierRef() in this script.
26
+ const RUN_ID = randomUUID()
27
+
23
28
  const LANES = {
24
29
  type: 'object',
25
30
  required: ['lanes'],
@@ -40,10 +45,13 @@ const LANES = {
40
45
  }
41
46
 
42
47
  phase('Scope')
43
- const scoped = await dispatchAgent(agent, 'scope-extractor', `Extract 2-6 disjoint edit lanes for: ${TOPIC}\nProvided scope: ${JSON.stringify(SCOPE)}\nEach lane owns a non-overlapping file scope. Shared root/config/lock files must have one owner. Return lanes with name/scope/task.`, { role: 'implementer', risk: 'medium', capabilities: ['structured-output', 'reasoning'], label: 'scope-extract', phase: 'Scope', schema: LANES })
48
+ const scoped = await dispatchAgent(agent, 'scope-extractor', `Extract 2-6 disjoint edit lanes for: ${TOPIC}\nProvided scope: ${SCOPE.length ? SCOPE.join(', ') : '(none supplied)'}\nEach lane owns a non-overlapping file scope. Shared root/config/lock files must have one owner. Return lanes with name/scope/task.`, { role: 'implementer', risk: 'medium', capabilities: ['structured-output', 'reasoning'], label: 'scope-extract', phase: 'Scope', schema: LANES })
44
49
  if (!scoped || !Array.isArray(scoped.lanes) || scoped.lanes.length === 0) {
45
50
  return { status: 'blocked', reason: 'Scope agent produced no lanes.' }
46
51
  }
52
+ // Phase B: persist the scope artifact for the next barrier agent.
53
+ const scopeSummary = `scope lanes: ${scoped.lanes.map((l) => l.name).join(', ')}`
54
+ writeArtifact({ runId: RUN_ID, phase: 'Scope', label: 'barrier', payload: scoped, summary: scopeSummary, role: 'implementer' })
47
55
  const lanes = scoped.lanes.slice(0, 6)
48
56
  if (scoped.lanes.length > lanes.length) {
49
57
  log(`Bounded implementation to 6 of ${scoped.lanes.length} lanes.`)
@@ -54,22 +62,30 @@ const implementations = (await parallel(
54
62
  lanes.map((lane, index) => () => dispatchAgent(
55
63
  agent,
56
64
  `lane-implementer-${index + 1}`,
57
- `Implement this owned lane for the topic "${TOPIC}".\nLane: ${JSON.stringify(lane)}\nDo not edit outside the listed scope. Do not revert sibling work. Add regression tests and run the smallest relevant checks. Return changed files, commands, exact results, and blockers. Do not commit, push, publish, or deploy.`,
65
+ `Implement this owned lane for the topic "${TOPIC}".\n${barrierRef({ runId: RUN_ID, phase: 'Scope', label: 'barrier', summary: `lane ${lane.name}: ${lane.task.slice(0, 120)}` }).promptBlock}\nDo not edit outside the listed scope. Do not revert sibling work. Add regression tests and run the smallest relevant checks. Return changed files, commands, exact results, and blockers. Do not commit, push, publish, or deploy.`,
58
66
  { role: 'implementer', risk: 'medium', capabilities: ['structured-output', 'reasoning'], label: `implement:${index + 1}:${lane.name}`, phase: 'Implement', isolation: 'worktree' },
59
67
  )),
60
68
  )).filter(Boolean)
61
69
  if (implementations.length === 0) {
62
70
  return { status: 'blocked', reason: 'No implementation lane completed successfully.', scope: scoped }
63
71
  }
72
+ // Phase B: persist each implementation artifact.
73
+ for (let i = 0; i < implementations.length; i++) {
74
+ const lane = lanes[i];
75
+ const label = `implement:${i + 1}:${lane.name}`;
76
+ const summary = `lane ${lane.name} files: ${(implementations[i]?.files || []).slice(0, 5).join(', ')}`;
77
+ writeArtifact({ runId: RUN_ID, phase: 'Implement', label, payload: implementations[i], summary, role: 'implementer' });
78
+ }
64
79
 
65
80
  phase('Barrier')
66
- const merge = await dispatchAgent(agent, 'barrier-merger', `Reconcile the lane outputs for topic "${TOPIC}" into one MERGE plan. Identify conflicts between worktrees, exact integration order, shared-file ownership, and any human approvals required.\nLanes: ${JSON.stringify(lanes)}\nImplementations: ${JSON.stringify(implementations)}`, { role: 'implementer', risk: 'high', capabilities: ['structured-output', 'reasoning', 'architecture'], label: 'barrier-merge', phase: 'Barrier' })
81
+ const merge = await dispatchAgent(agent, 'barrier-merger', `Reconcile the lane outputs for topic "${TOPIC}" into one MERGE plan. Identify conflicts between worktrees, exact integration order, shared-file ownership, and any human approvals required.\n${barrierRef({ runId: RUN_ID, phase: 'Implement', label: 'implement:summary', summary: `${implementations.length} lanes complete across ${lanes.length} planned` }).promptBlock}`, { role: 'implementer', risk: 'high', capabilities: ['structured-output', 'reasoning', 'architecture'], label: 'barrier-merge', phase: 'Barrier' })
82
+ writeArtifact({ runId: RUN_ID, phase: 'Barrier', label: 'barrier', payload: merge, summary: typeof merge === 'string' ? merge.slice(0, 200) : `barrier merge complete`, role: 'implementer' })
67
83
 
68
84
  phase('Verify')
69
- const verify = await dispatchAgent(agent, 'barrier-verifier', `Re-check this MERGE plan against the original scope for topic "${TOPIC}". Reject it if any lane output is missing, any conflict is unresolved, or any test gate is unbounded. Return the verified plan plus the exact gating tests.\nScope: ${JSON.stringify(SCOPE)}\nMerge: ${String(merge)}`, { role: 'adversarial', risk: 'high', capabilities: ['structured-output', 'reasoning'], label: 'barrier-verify', phase: 'Verify' })
85
+ const verify = await dispatchAgent(agent, 'barrier-verifier', `Re-check this MERGE plan against the original scope for topic "${TOPIC}". Reject it if any lane output is missing, any conflict is unresolved, or any test gate is unbounded. Return the verified plan plus the exact gating tests.\n${barrierRef({ runId: RUN_ID, phase: 'Barrier', label: 'barrier', summary: `verify against scope: ${SCOPE.length} scope items` }).promptBlock}`, { role: 'adversarial', risk: 'high', capabilities: ['structured-output', 'reasoning'], label: 'barrier-verify', phase: 'Verify' })
70
86
 
71
87
  phase('Synthesis')
72
- const synthesis = await dispatchAgent(agent, 'integration-reporter', `Produce the final integration report for topic "${TOPIC}". State exact integration order, remaining gates, evidence commands to run, and any required human approvals. Do not claim success without fresh command evidence.\nMerge: ${String(merge)}\nVerify: ${String(verify)}`, { role: 'implementer', risk: 'medium', capabilities: ['structured-output', 'reasoning'], label: 'integration-report', phase: 'Synthesis' })
88
+ const synthesis = await dispatchAgent(agent, 'integration-reporter', `Produce the final integration report for topic "${TOPIC}". State exact integration order, remaining gates, evidence commands to run, and any required human approvals. Do not claim success without fresh command evidence.\n${barrierRef({ runId: RUN_ID, phase: 'Barrier', label: 'barrier', summary: `merge plan: ${typeof merge === 'string' ? merge.slice(0, 120) : 'complex'}` }).promptBlock}\n${barrierRef({ runId: RUN_ID, phase: 'Verify', label: 'barrier-verify', summary: typeof verify === 'string' ? verify.slice(0, 120) : 'verified' }).promptBlock}`, { role: 'implementer', risk: 'medium', capabilities: ['structured-output', 'reasoning'], label: 'integration-report', phase: 'Synthesis' })
73
89
 
74
90
  return {
75
91
  status: 'ready-for-integration',
@@ -1,4 +1,5 @@
1
- import { dispatchAgent } from './lib/dispatch.js'
1
+ import { randomUUID } from 'node:crypto'
2
+ import { dispatchAgent, writeArtifact, barrierRef } from './lib/dispatch.js'
2
3
 
3
4
  export const meta = {
4
5
  name: 'bizar-research',
@@ -19,6 +20,11 @@ const TOPIC = typeof args === 'string'
19
20
  ? args.topic
20
21
  : JSON.stringify(args || {})
21
22
 
23
+ // Phase B (v10.21.0) artifact-on-disk barriers: one runId per workflow
24
+ // invocation. Used by every writeArtifact() + barrierRef() in this script
25
+ // so the on-disk store + the 3-line barrier block stay paired.
26
+ const RUN_ID = randomUUID()
27
+
22
28
  const BRIEF = {
23
29
  type: 'object',
24
30
  required: ['summary', 'files', 'risks', 'verification'],
@@ -59,14 +65,22 @@ const research = (await parallel([
59
65
 
60
66
  if (research.length === 0) return { status: 'blocked', reason: 'No research agent completed successfully.' }
61
67
 
68
+ // Phase B: persist the research artifact for the next barrier agent.
69
+ const researchSummary = `research lanes: ${research.map((r) => (r && r.summary) ? r.summary.slice(0, 80) : '<lane>').join(' | ')}`
70
+ writeArtifact({ runId: RUN_ID, phase: 'Research', label: 'barrier', payload: research, summary: researchSummary, role: 'research-analyst' })
71
+
62
72
  phase('Plan')
63
- const plan = await dispatchAgent(agent, 'plan-author', `Design one reversible implementation for: ${TOPIC}\nResearch:\n${JSON.stringify(research)}\nReturn disjoint edit lanes. Shared root/config/lock files must have one owner. Include bounded tests and stop conditions.`, { role: 'architect', risk: 'medium', capabilities: ['structured-output', 'reasoning', 'architecture'], label: 'plan', phase: 'Plan', schema: PLAN })
73
+ const plan = await dispatchAgent(agent, 'plan-author', `Design one reversible implementation for: ${TOPIC}\n${barrierRef({ runId: RUN_ID, phase: 'Research', label: 'barrier', summary: researchSummary }).promptBlock}\nReturn disjoint edit lanes. Shared root/config/lock files must have one owner. Include bounded tests and stop conditions.`, { role: 'architect', risk: 'medium', capabilities: ['structured-output', 'reasoning', 'architecture'], label: 'plan', phase: 'Plan', schema: PLAN })
64
74
  if (!plan || !Array.isArray(plan.lanes) || plan.lanes.length === 0) {
65
75
  return { status: 'blocked', reason: 'Planning produced no implementation lanes.', research }
66
76
  }
67
77
 
78
+ // Phase B: persist the plan artifact for the next barrier agent.
79
+ const planSummary = `plan lanes: ${plan.lanes.map((l) => l.name).join(', ')}`
80
+ writeArtifact({ runId: RUN_ID, phase: 'Plan', label: 'barrier', payload: plan, summary: planSummary, role: 'architect' })
81
+
68
82
  phase('Audit')
69
- const audit = await dispatchAgent(agent, 'plan-auditor', `Adversarially review this plan for correctness, security, conflicting file ownership, missing regression tests, and unbounded retry loops. Return a corrected plan, not commentary. Topic: ${TOPIC}\nPlan: ${JSON.stringify(plan)}`, { role: 'adversarial', risk: 'high', capabilities: ['structured-output', 'reasoning', 'architecture', 'security'], label: 'plan-audit', phase: 'Audit', schema: PLAN })
83
+ const audit = await dispatchAgent(agent, 'plan-auditor', `Adversarially review this plan for correctness, security, conflicting file ownership, missing regression tests, and unbounded retry loops. Return a corrected plan, not commentary. Topic: ${TOPIC}\n${barrierRef({ runId: RUN_ID, phase: 'Plan', label: 'barrier', summary: planSummary }).promptBlock}`, { role: 'adversarial', risk: 'high', capabilities: ['structured-output', 'reasoning', 'architecture', 'security'], label: 'plan-audit', phase: 'Audit', schema: PLAN })
70
84
  const approved = audit || plan
71
85
 
72
86
  phase('Implement')
@@ -78,7 +92,7 @@ const implementation = await parallel(
78
92
  lanes.map((lane, index) => () => dispatchAgent(
79
93
  agent,
80
94
  `lane-implementer-${index + 1}`,
81
- `Implement this owned lane for the topic "${TOPIC}".\nLane: ${JSON.stringify(lane)}\nDo not edit outside the listed scope. Do not revert sibling work. Add regression tests and run the smallest relevant checks. Return changed files, commands, exact results, and blockers. Do not commit, push, publish, or deploy.`,
95
+ `Implement this owned lane for the topic "${TOPIC}".\n${barrierRef({ runId: RUN_ID, phase: 'Plan', label: 'barrier', summary: `lane ${lane.name}: ${lane.task.slice(0, 120)}` }).promptBlock}\nDo not edit outside the listed scope. Do not revert sibling work. Add regression tests and run the smallest relevant checks. Return changed files, commands, exact results, and blockers. Do not commit, push, publish, or deploy.`,
82
96
  { role: 'implementer', risk: 'medium', capabilities: ['structured-output', 'reasoning'], label: `implement:${index + 1}:${lane.name}`, phase: 'Implement', isolation: 'worktree' },
83
97
  )),
84
98
  )
@@ -87,16 +101,29 @@ if (completed.length === 0) {
87
101
  return { status: 'blocked', reason: 'No implementation lane completed successfully.', plan: approved }
88
102
  }
89
103
 
104
+ // Phase B: persist each lane's artifact for the next barrier agent.
105
+ for (let i = 0; i < completed.length; i++) {
106
+ const lane = lanes[i];
107
+ const label = `implement:${i + 1}:${lane.name}`;
108
+ const summary = `lane ${lane.name} files: ${(completed[i]?.files || []).slice(0, 5).join(', ')}`;
109
+ writeArtifact({ runId: RUN_ID, phase: 'Implement', label, payload: completed[i], summary, role: 'implementer' });
110
+ }
111
+
90
112
  phase('Verify')
91
113
  const reviews = await pipeline(
92
114
  completed,
93
115
  (result, _original, index) => dispatchAgent(
94
116
  agent,
95
117
  `reviewer-${index + 1}`,
96
- `Try to refute this implementation result for topic "${TOPIC}". Check correctness, security, scope, test evidence, and integration assumptions. Return only verified findings and required checks.\nResult: ${String(result)}`,
118
+ `Try to refute this implementation result for topic "${TOPIC}". Check correctness, security, scope, test evidence, and integration assumptions. Return only verified findings and required checks.\n${barrierRef({ runId: RUN_ID, phase: 'Implement', label: `implement:${index + 1}:${lanes[index]?.name || ''}`, summary: `review of lane ${lanes[index]?.name || index + 1}` }).promptBlock}`,
97
119
  { role: 'adversarial', risk: 'high', capabilities: ['structured-output', 'reasoning'], label: `review:${index + 1}`, phase: 'Verify' },
98
120
  ),
99
121
  )
100
- const final = await dispatchAgent(agent, 'final-verifier', `Synthesize a bounded integration and verification report for topic "${TOPIC}". Do not claim success without fresh command evidence. Identify conflicts between worktrees, exact integration order, remaining gates, and any required human approvals.\nPlan: ${JSON.stringify(approved)}\nImplementations: ${JSON.stringify(completed)}\nReviews: ${JSON.stringify(reviews.filter(Boolean))}`, { role: 'implementer', risk: 'medium', capabilities: ['structured-output', 'reasoning'], label: 'final-verification', phase: 'Verify' })
122
+ // Phase B: persist review artifacts.
123
+ const verifiedReviews = reviews.filter(Boolean);
124
+ for (let i = 0; i < verifiedReviews.length; i++) {
125
+ writeArtifact({ runId: RUN_ID, phase: 'Verify', label: `review:${i + 1}`, payload: verifiedReviews[i], summary: typeof verifiedReviews[i] === 'string' ? verifiedReviews[i].slice(0, 200) : `review ${i + 1}`, role: 'adversarial' });
126
+ }
127
+ const final = await dispatchAgent(agent, 'final-verifier', `Synthesize a bounded integration and verification report for topic "${TOPIC}". Do not claim success without fresh command evidence. Identify conflicts between worktrees, exact integration order, remaining gates, and any required human approvals.\n${barrierRef({ runId: RUN_ID, phase: 'Plan', label: 'barrier', summary: `approved plan with ${approved.lanes.length} lanes` }).promptBlock}\n${barrierRef({ runId: RUN_ID, phase: 'Implement', label: 'implement:summary', summary: `${completed.length} lanes complete` }).promptBlock}\n${barrierRef({ runId: RUN_ID, phase: 'Verify', label: 'review:summary', summary: `${verifiedReviews.length} reviews complete` }).promptBlock}`, { role: 'implementer', risk: 'medium', capabilities: ['structured-output', 'reasoning'], label: 'final-verification', phase: 'Verify' })
101
128
 
102
129
  return { status: 'ready-for-integration', topic: TOPIC, research, plan: approved, implementation: completed, reviews: reviews.filter(Boolean), final }
@@ -28,7 +28,7 @@
28
28
  */
29
29
 
30
30
  import { randomUUID, createHash } from 'node:crypto';
31
- import { existsSync, readFileSync, appendFileSync, mkdirSync, writeFileSync, renameSync, openSync, closeSync, fsyncSync, constants as fsConstants } from 'node:fs';
31
+ import { existsSync, readFileSync, appendFileSync, mkdirSync, writeFileSync, renameSync, openSync, closeSync, fsyncSync, rmSync, readdirSync, statSync, constants as fsConstants } from 'node:fs';
32
32
  import { homedir } from 'node:os';
33
33
  import { dirname, isAbsolute, join, resolve } from 'node:path';
34
34
 
@@ -857,3 +857,428 @@ export async function dispatchAgent(agentFn, agentName, prompt, opts = {}, conte
857
857
  export async function dispatchAgentDryRun(agentName, prompt, opts = {}) {
858
858
  return dispatchAgent(undefined, agentName, prompt, { ...opts, dryRun: true });
859
859
  }
860
+
861
+ /* ────────────────────────────────────────────────────────────────────────── */
862
+ /* Phase B (v10.21.0) artifact-on-disk barrier store */
863
+ /* ────────────────────────────────────────────────────────────────────────── */
864
+
865
+ /**
866
+ * WorkflowStateError — fail-soft error class used by the artifact store.
867
+ * Duplicated as a 3-line class to avoid a `cli/` ↔ `config/workflows/`
868
+ * import cycle. Mirrors `cli/core/workflow-state.mjs:62-66`.
869
+ */
870
+ export class WorkflowStateError extends Error {
871
+ constructor(code, message, details) {
872
+ super(message);
873
+ this.name = 'WorkflowStateError';
874
+ this.code = code;
875
+ if (details !== undefined) this.details = details;
876
+ }
877
+ }
878
+
879
+ /**
880
+ * Schema version for the artifact store. Bump on any breaking change to
881
+ * `manifest.json` or the per-phase payload envelope.
882
+ */
883
+ export const ARTIFACT_SCHEMA_VERSION = 1;
884
+
885
+ /**
886
+ * Maximum bytes for a barrier reference summary (the `summary:` field
887
+ * passed by the workflow script — the human-readable one-liner that
888
+ * replaces the inline JSON in the next agent's prompt). Default 200 chars
889
+ * per Phase B plan §4.1. The 3-line barrier block itself is bounded by
890
+ * `MAX_BARRIER_BYTES = 3072` (B.2 budget).
891
+ */
892
+ export const MAX_SUMMARY_BYTES = 200;
893
+ export const MAX_BARRIER_BYTES = 3072;
894
+
895
+ /**
896
+ * Reduce any string to a kebab-case slug, capped at 64 chars. Used to
897
+ * derive `phase-slug` and `label-slug` from runtime data
898
+ * (`meta.phases[i].title` + the `label:` field passed to `dispatchAgent`)
899
+ * — no hardcoded phase or workflow lists.
900
+ */
901
+ export function slugify(input, maxLen = 64) {
902
+ if (typeof input !== 'string') return 'unknown';
903
+ const slug = input
904
+ .toLowerCase()
905
+ .replace(/[^a-z0-9]+/g, '-')
906
+ .replace(/^-+|-+$/g, '');
907
+ if (!slug) return 'unknown';
908
+ return slug.length > maxLen ? slug.slice(0, maxLen).replace(/-+$/g, '') : slug;
909
+ }
910
+
911
+ /**
912
+ * Resolve the artifact root directory for a run. Defaults to
913
+ * `<cwd>/.bizar/runs` but can be overridden by `BIZAR_RUNS_DIR` for
914
+ * tests and for sessions where `.bizar/` lives elsewhere.
915
+ */
916
+ export function resolveRunRoot({ cwd, env } = {}) {
917
+ const root = cwd || process.cwd();
918
+ const override = (env || process.env).BIZAR_RUNS_DIR;
919
+ if (override && typeof override === 'string' && override.trim()) {
920
+ return resolve(override);
921
+ }
922
+ return join(root, '.bizar', 'runs');
923
+ }
924
+
925
+ /**
926
+ * Resolve the directory for a single run-id.
927
+ */
928
+ function runDirFor(runRoot, runId) {
929
+ return join(runRoot, runId);
930
+ }
931
+
932
+ /**
933
+ * Manifest file co-located with artifacts in `.bizar/runs/<run-id>/`.
934
+ * Schema: `{ schemaVersion, runId, createdAt, updatedAt, phases: [...] }`
935
+ * where each entry is `{ phase, label, artifactPath, summaryHash, stale }`.
936
+ */
937
+ function manifestPathFor(runDir) {
938
+ return join(runDir, 'manifest.json');
939
+ }
940
+
941
+ /**
942
+ * Load a manifest if present. Returns `null` when missing or corrupt.
943
+ */
944
+ function readManifest(runDir) {
945
+ const p = manifestPathFor(runDir);
946
+ if (!existsSync(p)) return null;
947
+ try {
948
+ const raw = readFileSync(p, 'utf8');
949
+ const parsed = JSON.parse(raw);
950
+ if (!parsed || typeof parsed !== 'object') return null;
951
+ return parsed;
952
+ } catch {
953
+ return null;
954
+ }
955
+ }
956
+
957
+ /**
958
+ * Persist the manifest atomically (tmp + rename). Best-effort: a manifest
959
+ * write failure does NOT abort the artifact write — the next read
960
+ * reconciles via the directory listing.
961
+ */
962
+ function writeManifest(runDir, manifest) {
963
+ const p = manifestPathFor(runDir);
964
+ const tmp = `${p}.tmp-${randomUUID()}`;
965
+ writeFileSync(tmp, JSON.stringify(manifest, null, 2));
966
+ try {
967
+ fsyncSync(openSync(tmp, 'r+'));
968
+ } catch {
969
+ // fsync is best-effort; tolerate filesystems that don't support it.
970
+ }
971
+ renameSync(tmp, p);
972
+ }
973
+
974
+ /**
975
+ * Compute a stable summary hash. The summary is the human-readable
976
+ * one-liner the workflow script passes; the hash lets a future audit
977
+ * prove the summary was not retroactively edited.
978
+ */
979
+ function summaryHashHex(summary) {
980
+ return createHash('sha256').update(String(summary ?? '')).digest('hex').slice(0, 16);
981
+ }
982
+
983
+ /**
984
+ * Write a phase artifact atomically (tmp file → `rename(2)`). Atomic so
985
+ * a mid-write crash never leaves a half-written artifact on disk; the
986
+ * GC tool and the read site can both rely on "every file in the dir is
987
+ * either complete or absent."
988
+ *
989
+ * Naming is data-driven: `<phase-slug>__<label-slug>.json` where slugs
990
+ * derive from runtime `meta.phases[i].title` + `label:` field via
991
+ * `slugify()`. No hardcoded phase or workflow lists.
992
+ *
993
+ * @param {object} args
994
+ * @param {string} args.runId - run identifier (typically `randomUUID()`)
995
+ * @param {string} args.phase - phase title (e.g. "Research", "Plan")
996
+ * @param {string} args.label - dispatch label (e.g. "plan", "implement:1:foo")
997
+ * @param {*} args.payload - serializable artifact body
998
+ * @param {string} [args.summary] - human-readable one-liner (≤200 chars)
999
+ * @param {string} [args.agent] - agent name (e.g. "plan-author")
1000
+ * @param {string} [args.role] - role name from dispatch opts
1001
+ * @param {string} [args.runRoot] - override artifact root (test only)
1002
+ * @returns {{ runDir: string, artifactPath: string, slug: string, manifestPath: string }}
1003
+ */
1004
+ export function writeArtifact(args) {
1005
+ if (!args || typeof args !== 'object') {
1006
+ throw new WorkflowStateError('ARTIFACT_ARGS_REQUIRED', 'writeArtifact requires an args object');
1007
+ }
1008
+ const { runId, phase, label, payload, summary, agent, role } = args;
1009
+ if (!runId || typeof runId !== 'string') {
1010
+ throw new WorkflowStateError('RUN_ID_REQUIRED', 'writeArtifact requires a non-empty runId');
1011
+ }
1012
+ if (!phase || typeof phase !== 'string') {
1013
+ throw new WorkflowStateError('PHASE_REQUIRED', 'writeArtifact requires a non-empty phase');
1014
+ }
1015
+ if (!label || typeof label !== 'string') {
1016
+ throw new WorkflowStateError('LABEL_REQUIRED', 'writeArtifact requires a non-empty label');
1017
+ }
1018
+ if (payload === undefined) {
1019
+ throw new WorkflowStateError('PAYLOAD_REQUIRED', 'writeArtifact requires a payload');
1020
+ }
1021
+
1022
+ const runRoot = args.runRoot || resolveRunRoot();
1023
+ const runDir = runDirFor(runRoot, runId);
1024
+ mkdirSync(runDir, { recursive: true });
1025
+
1026
+ const phaseSlug = slugify(phase);
1027
+ const labelSlug = slugify(label);
1028
+ const slug = `${phaseSlug}__${labelSlug}`;
1029
+ const artifactPath = join(runDir, `${slug}.json`);
1030
+
1031
+ const now = new Date().toISOString();
1032
+ const finalSummary = (() => {
1033
+ if (typeof summary === 'string' && summary.length > 0) {
1034
+ return summary.length > MAX_SUMMARY_BYTES ? summary.slice(0, MAX_SUMMARY_BYTES) : summary;
1035
+ }
1036
+ // Safe default: first 200 chars of canonical payload. Deterministic
1037
+ // so re-running the same upstream agent produces the same summary
1038
+ // and the manifest hash is stable.
1039
+ const text = JSON.stringify(payload) ?? '';
1040
+ return text.length > MAX_SUMMARY_BYTES ? text.slice(0, MAX_SUMMARY_BYTES) : text;
1041
+ })();
1042
+
1043
+ const envelope = {
1044
+ schemaVersion: ARTIFACT_SCHEMA_VERSION,
1045
+ runId,
1046
+ phase,
1047
+ label,
1048
+ agent: typeof agent === 'string' ? agent : undefined,
1049
+ role: typeof role === 'string' ? role : undefined,
1050
+ wroteAt: now,
1051
+ summary: finalSummary,
1052
+ summaryHash: summaryHashHex(finalSummary),
1053
+ payload,
1054
+ };
1055
+
1056
+ // Atomic write: tmp file then rename. fsync before rename so the
1057
+ // file's contents hit disk before the directory entry does. If the
1058
+ // process dies between writeFileSync and renameSync, the tmp file is
1059
+ // orphaned and ignored by readArtifact (which only sees complete files
1060
+ // via the directory listing + manifest).
1061
+ const tmpPath = `${artifactPath}.tmp-${randomUUID()}`;
1062
+ writeFileSync(tmpPath, JSON.stringify(envelope, null, 2));
1063
+ try {
1064
+ fsyncSync(openSync(tmpPath, 'r+'));
1065
+ } catch {
1066
+ // best-effort
1067
+ }
1068
+ renameSync(tmpPath, artifactPath);
1069
+
1070
+ // Manifest update — append/replace the entry for this (phase, label).
1071
+ // Best-effort: if the manifest write fails the artifact is still on
1072
+ // disk and recoverable via readArtifact (which reads the file directly).
1073
+ const prev = readManifest(runDir) || {
1074
+ schemaVersion: ARTIFACT_SCHEMA_VERSION,
1075
+ runId,
1076
+ createdAt: now,
1077
+ updatedAt: now,
1078
+ phases: [],
1079
+ };
1080
+ const entry = {
1081
+ phase,
1082
+ label,
1083
+ artifactPath: `${slug}.json`,
1084
+ summaryHash: envelope.summaryHash,
1085
+ wroteAt: now,
1086
+ stale: false,
1087
+ };
1088
+ const phases = Array.isArray(prev.phases) ? prev.phases.slice() : [];
1089
+ const idx = phases.findIndex((p) => p && p.phase === phase && p.label === label);
1090
+ if (idx >= 0) phases[idx] = entry;
1091
+ else phases.push(entry);
1092
+ const manifest = {
1093
+ schemaVersion: ARTIFACT_SCHEMA_VERSION,
1094
+ runId,
1095
+ createdAt: prev.createdAt || now,
1096
+ updatedAt: now,
1097
+ phases,
1098
+ };
1099
+ try {
1100
+ writeManifest(runDir, manifest);
1101
+ } catch {
1102
+ // Manifest write is best-effort; the artifact itself is durable.
1103
+ }
1104
+
1105
+ return { runDir, artifactPath, slug, manifestPath: manifestPathFor(runDir) };
1106
+ }
1107
+
1108
+ /**
1109
+ * Read a phase artifact by (runId, phase, label). Returns
1110
+ * `{ payload, summary, manifest, stale, missing }`.
1111
+ *
1112
+ * Fail-soft: a missing or stale artifact returns `{ missing: true }`
1113
+ * or `{ stale: true, ... }`. The reader decides whether to re-render
1114
+ * the upstream phase. This is the Q4 audit recommendation.
1115
+ *
1116
+ * @param {object} args
1117
+ * @param {string} args.runId
1118
+ * @param {string} args.phase
1119
+ * @param {string} args.label
1120
+ * @param {string} [args.runRoot]
1121
+ */
1122
+ export function readArtifact(args) {
1123
+ const { runId, phase, label } = args || {};
1124
+ if (!runId || !phase || !label) {
1125
+ throw new WorkflowStateError('ARTIFACT_KEY_REQUIRED', 'readArtifact requires { runId, phase, label }');
1126
+ }
1127
+ const runRoot = args.runRoot || resolveRunRoot();
1128
+ const runDir = runDirFor(runRoot, runId);
1129
+ if (!existsSync(runDir)) {
1130
+ return { missing: true, payload: null, summary: null, manifest: null, stale: false };
1131
+ }
1132
+ const phaseSlug = slugify(phase);
1133
+ const labelSlug = slugify(label);
1134
+ const artifactPath = join(runDir, `${phaseSlug}__${labelSlug}.json`);
1135
+ if (!existsSync(artifactPath)) {
1136
+ return { missing: true, payload: null, summary: null, manifest: readManifest(runDir), stale: false };
1137
+ }
1138
+ let envelope;
1139
+ try {
1140
+ envelope = JSON.parse(readFileSync(artifactPath, 'utf8'));
1141
+ } catch {
1142
+ const manifest = readManifest(runDir);
1143
+ return { stale: true, payload: null, summary: null, manifest, missing: false };
1144
+ }
1145
+ // Stale detection: summary hash mismatch with the manifest entry, or
1146
+ // envelope is missing required fields.
1147
+ const manifest = readManifest(runDir);
1148
+ let stale = false;
1149
+ if (!envelope || typeof envelope !== 'object') stale = true;
1150
+ else if (envelope.summaryHash !== summaryHashHex(envelope.summary)) stale = true;
1151
+ else if (Array.isArray(manifest?.phases)) {
1152
+ const entry = manifest.phases.find((p) => p && p.phase === phase && p.label === label);
1153
+ if (entry && entry.stale === true) stale = true;
1154
+ if (entry && entry.summaryHash && entry.summaryHash !== envelope.summaryHash) stale = true;
1155
+ }
1156
+ return {
1157
+ missing: false,
1158
+ stale,
1159
+ payload: stale ? null : envelope.payload,
1160
+ summary: stale ? null : envelope.summary,
1161
+ wroteAt: envelope.wroteAt,
1162
+ manifest,
1163
+ artifactPath,
1164
+ };
1165
+ }
1166
+
1167
+ /**
1168
+ * List artifacts for a run. Used by the GC tool (B.3) and by tests that
1169
+ * want to introspect the directory shape.
1170
+ *
1171
+ * @param {object} args
1172
+ * @param {string} args.runId
1173
+ * @param {string} [args.runRoot]
1174
+ * @returns {Array<{ phase: string, label: string, slug: string, path: string, size: number, mtimeMs: number }>}
1175
+ */
1176
+ export function listArtifacts(args) {
1177
+ const { runId } = args || {};
1178
+ if (!runId || typeof runId !== 'string') {
1179
+ throw new WorkflowStateError('RUN_ID_REQUIRED', 'listArtifacts requires runId');
1180
+ }
1181
+ const runRoot = args.runRoot || resolveRunRoot();
1182
+ const runDir = runDirFor(runRoot, runId);
1183
+ if (!existsSync(runDir)) return [];
1184
+ const out = [];
1185
+ for (const name of readdirSync(runDir)) {
1186
+ if (!name.endsWith('.json')) continue;
1187
+ if (name === 'manifest.json') continue;
1188
+ if (name.includes('.tmp-')) continue; // orphaned tmp from a crashed write
1189
+ const fullPath = join(runDir, name);
1190
+ let stat;
1191
+ try {
1192
+ stat = statSync(fullPath);
1193
+ } catch {
1194
+ continue;
1195
+ }
1196
+ const sep = name.indexOf('__');
1197
+ if (sep < 0) continue;
1198
+ const phase = name.slice(0, sep);
1199
+ const label = name.slice(sep + 2, -('.json'.length));
1200
+ out.push({ phase, label, slug: name.slice(0, -'.json'.length), path: fullPath, size: stat.size, mtimeMs: stat.mtimeMs });
1201
+ }
1202
+ return out;
1203
+ }
1204
+
1205
+ /**
1206
+ * List all runs (subdirectories of `.bizar/runs/`). Used by GC.
1207
+ *
1208
+ * @param {object} [args]
1209
+ * @param {string} [args.runRoot]
1210
+ * @returns {Array<{ runId: string, path: string, mtimeMs: number, size: number }>}
1211
+ */
1212
+ export function listRuns(args = {}) {
1213
+ const runRoot = args.runRoot || resolveRunRoot();
1214
+ if (!existsSync(runRoot)) return [];
1215
+ const out = [];
1216
+ for (const name of readdirSync(runRoot)) {
1217
+ const fullPath = join(runRoot, name);
1218
+ let stat;
1219
+ try {
1220
+ stat = statSync(fullPath);
1221
+ } catch {
1222
+ continue;
1223
+ }
1224
+ if (!stat.isDirectory()) continue;
1225
+ let size = 0;
1226
+ for (const inner of readdirSync(fullPath)) {
1227
+ try {
1228
+ size += statSync(join(fullPath, inner)).size;
1229
+ } catch {
1230
+ // best-effort size tally
1231
+ }
1232
+ }
1233
+ out.push({ runId: name, path: fullPath, mtimeMs: stat.mtimeMs, size });
1234
+ }
1235
+ return out;
1236
+ }
1237
+
1238
+ /**
1239
+ * Build the 3-line barrier reference block (B.2). Replaces the inline
1240
+ * `JSON.stringify(priorOutput)` in the next agent's prompt with a
1241
+ * compact reference that points at the on-disk artifact.
1242
+ *
1243
+ * prior phase: <phase>
1244
+ * prior label: <label>
1245
+ * summary: <≤200 chars>
1246
+ * path: <absolute path to .bizar/runs/<id>/<slug>.json>
1247
+ *
1248
+ * Block is bounded by `MAX_BARRIER_BYTES`; if the script passes a
1249
+ * `summary` larger than the budget, it is truncated and the call is
1250
+ * noted via `truncated: true` in the return value.
1251
+ *
1252
+ * @param {object} args
1253
+ * @param {string} args.runId
1254
+ * @param {string} args.phase
1255
+ * @param {string} args.label
1256
+ * @param {string} [args.summary]
1257
+ * @param {string} [args.runRoot]
1258
+ * @returns {{ promptBlock: string, path: string, truncated: boolean, bytes: number }}
1259
+ */
1260
+ export function barrierRef(args) {
1261
+ const { runId, phase, label, summary } = args || {};
1262
+ if (!runId || !phase || !label) {
1263
+ throw new WorkflowStateError('BARRIER_KEY_REQUIRED', 'barrierRef requires { runId, phase, label }');
1264
+ }
1265
+ const runRoot = args.runRoot || resolveRunRoot();
1266
+ const runDir = runDirFor(runRoot, runId);
1267
+ const slug = `${slugify(phase)}__${slugify(label)}`;
1268
+ const path = join(runDir, `${slug}.json`);
1269
+
1270
+ let boundedSummary = typeof summary === 'string' ? summary : '';
1271
+ let truncated = false;
1272
+ if (boundedSummary.length > MAX_SUMMARY_BYTES) {
1273
+ boundedSummary = boundedSummary.slice(0, MAX_SUMMARY_BYTES);
1274
+ truncated = true;
1275
+ }
1276
+ const promptBlock = [
1277
+ `prior phase: ${phase}`,
1278
+ `prior label: ${label}`,
1279
+ `summary: ${boundedSummary}`,
1280
+ `path: ${path}`,
1281
+ ].join('\n');
1282
+ return { promptBlock, path, truncated: truncated || promptBlock.length > MAX_BARRIER_BYTES, bytes: Buffer.byteLength(promptBlock, 'utf8') };
1283
+ }
1284
+
@@ -1,4 +1,5 @@
1
- import { dispatchAgent } from './lib/dispatch.js'
1
+ import { randomUUID } from 'node:crypto'
2
+ import { dispatchAgent, writeArtifact, barrierRef } from './lib/dispatch.js'
2
3
 
3
4
  export const meta = {
4
5
  name: 'ultracode-research',
@@ -12,6 +13,10 @@ export const meta = {
12
13
  }
13
14
 
14
15
  const QUESTION = typeof args === 'string' ? args : args?.question || JSON.stringify(args || {})
16
+
17
+ // Phase B (v10.21.0) artifact-on-disk barriers: one runId per workflow
18
+ // invocation. Used by every writeArtifact() + barrierRef() in this script.
19
+ const RUN_ID = randomUUID()
15
20
  const EVIDENCE = {
16
21
  type: 'object',
17
22
  required: ['claims', 'gaps'],
@@ -36,9 +41,14 @@ const evidence = (await parallel([
36
41
  ])).filter(Boolean)
37
42
  if (evidence.length === 0) return { status: 'blocked', reason: 'No research pass completed.' }
38
43
 
44
+ // Phase B: persist evidence artifact for the next barrier agent.
45
+ const evidenceSummary = `${evidence.length} evidence passes with ${evidence.reduce((n, e) => n + (Array.isArray(e?.claims) ? e.claims.length : 0), 0)} total claims`
46
+ writeArtifact({ runId: RUN_ID, phase: 'Research', label: 'barrier', payload: evidence, summary: evidenceSummary, role: 'research-analyst' })
47
+
39
48
  phase('Critique')
40
- const critique = await dispatchAgent(agent, 'completeness-critic', `Challenge these research claims. Identify contradictions, unread sources, outdated assumptions, and claims lacking reproducible evidence.\nQuestion: ${QUESTION}\nEvidence: ${JSON.stringify(evidence)}`, { role: 'adversarial', risk: 'high', capabilities: ['structured-output', 'reasoning'], label: 'completeness-critic', phase: 'Critique', schema: EVIDENCE })
49
+ const critique = await dispatchAgent(agent, 'completeness-critic', `Challenge these research claims. Identify contradictions, unread sources, outdated assumptions, and claims lacking reproducible evidence.\nQuestion: ${QUESTION}\n${barrierRef({ runId: RUN_ID, phase: 'Research', label: 'barrier', summary: evidenceSummary }).promptBlock}`, { role: 'adversarial', risk: 'high', capabilities: ['structured-output', 'reasoning'], label: 'completeness-critic', phase: 'Critique', schema: EVIDENCE })
50
+ writeArtifact({ runId: RUN_ID, phase: 'Critique', label: 'barrier', payload: critique, summary: typeof critique === 'string' ? critique.slice(0, 200) : 'critique complete', role: 'adversarial' })
41
51
 
42
52
  phase('Synthesize')
43
- const synthesis = await dispatchAgent(agent, 'synthesis-author', `Produce a concise sourced decision brief for: ${QUESTION}. Separate verified facts, repository-specific implications, recommendation, risks, and unresolved gaps. Do not invent consensus or hide evidence gaps.\nEvidence: ${JSON.stringify(evidence)}\nCritique: ${JSON.stringify(critique)}`, { role: 'architect', risk: 'medium', capabilities: ['structured-output', 'reasoning'], label: 'synthesis', phase: 'Synthesize' })
53
+ const synthesis = await dispatchAgent(agent, 'synthesis-author', `Produce a concise sourced decision brief for: ${QUESTION}. Separate verified facts, repository-specific implications, recommendation, risks, and unresolved gaps. Do not invent consensus or hide evidence gaps.\n${barrierRef({ runId: RUN_ID, phase: 'Research', label: 'barrier', summary: evidenceSummary }).promptBlock}\n${barrierRef({ runId: RUN_ID, phase: 'Critique', label: 'barrier', summary: typeof critique === 'string' ? critique.slice(0, 200) : 'critique complete' }).promptBlock}`, { role: 'architect', risk: 'medium', capabilities: ['structured-output', 'reasoning'], label: 'synthesis', phase: 'Synthesize' })
44
54
  return { question: QUESTION, evidence, critique, synthesis }
@@ -1,4 +1,5 @@
1
- import { dispatchAgent } from './lib/dispatch.js'
1
+ import { randomUUID } from 'node:crypto'
2
+ import { dispatchAgent, writeArtifact, barrierRef } from './lib/dispatch.js'
2
3
 
3
4
  export const meta = {
4
5
  name: 'ultracode-review',
@@ -11,6 +12,10 @@ export const meta = {
11
12
  }
12
13
 
13
14
  const TARGET = typeof args === 'string' ? args : args?.target || 'the current working diff'
15
+
16
+ // Phase B (v10.21.0) artifact-on-disk barriers: one runId per workflow
17
+ // invocation. Used by every writeArtifact() + barrierRef() in this script.
18
+ const RUN_ID = randomUUID()
14
19
  const FINDINGS = {
15
20
  type: 'object',
16
21
  required: ['findings'],
@@ -57,7 +62,7 @@ const reviewed = await pipeline(
57
62
  lenses,
58
63
  (lens) => dispatchAgent(agent, `reviewer-${lens[0]}`, `Review ${TARGET} through the ${lens[0]} lens. ${lens[1]} Report only actionable defects with a concrete failure scenario; do not praise or speculate.`, { ...lensRole[lens[0]], label: `review:${lens[0]}`, phase: 'Review', schema: FINDINGS }),
59
64
  (review, original) => (review?.findings || []).slice(0, 12).map((finding) => ({ ...finding, lens: original[0] })),
60
- (findings) => parallel(findings.map((finding, index) => () => dispatchAgent(agent, `finding-verifier-${index + 1}`, `Try to refute this proposed finding. Inspect the exact code path and reject it if it is speculative, pre-existing, unreachable, or already covered.\n${JSON.stringify(finding)}`, { role: 'adversarial', risk: 'high', capabilities: ['reasoning', 'structured-output'], label: `verify:${index + 1}:${finding.file}`, phase: 'Verify', schema: VERDICT }).then((verdict) => ({ finding, verdict })))),
65
+ (findings) => parallel(findings.map((finding, index) => () => dispatchAgent(agent, `finding-verifier-${index + 1}`, `Try to refute this proposed finding. Inspect the exact code path and reject it if it is speculative, pre-existing, unreachable, or already covered.\n${barrierRef({ runId: RUN_ID, phase: 'Review', label: `review:${finding.lens || 'mixed'}`, summary: `${finding.summary ? finding.summary.slice(0, 200) : `finding in ${finding.file}`}` }).promptBlock}`, { role: 'adversarial', risk: 'high', capabilities: ['reasoning', 'structured-output'], label: `verify:${index + 1}:${finding.file}`, phase: 'Verify', schema: VERDICT }).then((verdict) => ({ finding, verdict })))),
61
66
  )
62
67
 
63
68
  const verified = reviewed.flat(2).filter(Boolean).filter((item) => item.verdict?.confirmed).map((item) => ({ ...item.finding, verification: item.verdict.reason }))
@@ -1,4 +1,5 @@
1
- import { dispatchAgent } from './lib/dispatch.js'
1
+ import { randomUUID } from 'node:crypto'
2
+ import { dispatchAgent, writeArtifact, barrierRef } from './lib/dispatch.js'
2
3
 
3
4
  export const meta = {
4
5
  name: 'ultracode',
@@ -13,6 +14,10 @@ export const meta = {
13
14
  }
14
15
 
15
16
  const TASK = typeof args === 'string' ? args : args?.task || JSON.stringify(args || {})
17
+
18
+ // Phase B (v10.21.0) artifact-on-disk barriers: one runId per workflow
19
+ // invocation. Used by every writeArtifact() + barrierRef() in this script.
20
+ const RUN_ID = randomUUID()
16
21
  const BRIEF = {
17
22
  type: 'object',
18
23
  required: ['summary', 'files', 'risks', 'verification'],
@@ -52,11 +57,19 @@ const research = (await parallel([
52
57
 
53
58
  if (research.length === 0) return { status: 'blocked', reason: 'No research agent completed successfully.' }
54
59
 
60
+ // Phase B: persist research artifact for the next barrier agent.
61
+ const researchSummary = `research lanes: ${research.map((r) => (r && r.summary) ? r.summary.slice(0, 80) : '<lane>').join(' | ')}`
62
+ writeArtifact({ runId: RUN_ID, phase: 'Research', label: 'barrier', payload: research, summary: researchSummary, role: 'research-analyst' })
63
+
55
64
  phase('Design')
56
- const plan = await dispatchAgent(agent, 'plan-author', `Design one reversible implementation for: ${TASK}\nResearch:\n${JSON.stringify(research)}\nReturn disjoint edit lanes. Shared root/config/lock files must have one owner. Include bounded tests and stop conditions.`, { role: 'architect', risk: 'medium', capabilities: ['structured-output', 'reasoning', 'architecture'], label: 'plan', phase: 'Design', schema: PLAN })
65
+ const plan = await dispatchAgent(agent, 'plan-author', `Design one reversible implementation for: ${TASK}\n${barrierRef({ runId: RUN_ID, phase: 'Research', label: 'barrier', summary: researchSummary }).promptBlock}\nReturn disjoint edit lanes. Shared root/config/lock files must have one owner. Include bounded tests and stop conditions.`, { role: 'architect', risk: 'medium', capabilities: ['structured-output', 'reasoning', 'architecture'], label: 'plan', phase: 'Design', schema: PLAN })
57
66
  if (!plan || !Array.isArray(plan.lanes) || plan.lanes.length === 0) return { status: 'blocked', reason: 'Planning produced no implementation lanes.', research }
58
67
 
59
- const audit = await dispatchAgent(agent, 'plan-auditor', `Adversarially review this plan for correctness, security, conflicting file ownership, missing regression tests, and unbounded retry loops. Return a corrected plan, not commentary. Task: ${TASK}\nPlan: ${JSON.stringify(plan)}`, { role: 'adversarial', risk: 'high', capabilities: ['structured-output', 'reasoning', 'architecture', 'security'], label: 'plan-audit', phase: 'Design', schema: PLAN })
68
+ // Phase B: persist plan artifact for the next barrier agent.
69
+ const planSummary = `plan lanes: ${plan.lanes.map((l) => l.name).join(', ')}`
70
+ writeArtifact({ runId: RUN_ID, phase: 'Design', label: 'barrier', payload: plan, summary: planSummary, role: 'architect' })
71
+
72
+ const audit = await dispatchAgent(agent, 'plan-auditor', `Adversarially review this plan for correctness, security, conflicting file ownership, missing regression tests, and unbounded retry loops. Return a corrected plan, not commentary. Task: ${TASK}\n${barrierRef({ runId: RUN_ID, phase: 'Design', label: 'barrier', summary: planSummary }).promptBlock}`, { role: 'adversarial', risk: 'high', capabilities: ['structured-output', 'reasoning', 'architecture', 'security'], label: 'plan-audit', phase: 'Design', schema: PLAN })
60
73
  const approved = audit || plan
61
74
 
62
75
  phase('Implement')
@@ -64,16 +77,28 @@ const lanes = approved.lanes.slice(0, 8)
64
77
  if (approved.lanes.length > lanes.length) log(`Bounded implementation to 8 of ${approved.lanes.length} lanes; ${approved.lanes.length - lanes.length} lanes were not dispatched.`)
65
78
  const implementation = await pipeline(
66
79
  lanes,
67
- (lane, _original, index) => dispatchAgent(agent, `lane-implementer-${index + 1}`, `Implement this owned lane for the task "${TASK}".\nLane: ${JSON.stringify(lane)}\nDo not edit outside the listed scope. Do not revert sibling work. Add regression tests and run the smallest relevant checks. Return changed files, commands, exact results, and blockers. Do not commit, push, publish, or deploy.`, { role: 'implementer', risk: 'medium', capabilities: ['structured-output', 'reasoning'], label: `implement:${index + 1}:${lane.name}`, phase: 'Implement', isolation: 'worktree' }),
80
+ (lane, _original, index) => dispatchAgent(agent, `lane-implementer-${index + 1}`, `Implement this owned lane for the task "${TASK}".\n${barrierRef({ runId: RUN_ID, phase: 'Design', label: 'barrier', summary: `lane ${lane.name}: ${lane.task.slice(0, 120)}` }).promptBlock}\nDo not edit outside the listed scope. Do not revert sibling work. Add regression tests and run the smallest relevant checks. Return changed files, commands, exact results, and blockers. Do not commit, push, publish, or deploy.`, { role: 'implementer', risk: 'medium', capabilities: ['structured-output', 'reasoning'], label: `implement:${index + 1}:${lane.name}`, phase: 'Implement', isolation: 'worktree' }),
68
81
  )
69
82
  const completed = implementation.filter(Boolean)
70
83
  if (completed.length === 0) return { status: 'blocked', reason: 'No implementation lane completed successfully.', plan: approved }
84
+ // Phase B: persist each implementation artifact.
85
+ for (let i = 0; i < completed.length; i++) {
86
+ const lane = lanes[i];
87
+ const label = `implement:${i + 1}:${lane.name}`;
88
+ const summary = `lane ${lane.name} files: ${(completed[i]?.files || []).slice(0, 5).join(', ')}`;
89
+ writeArtifact({ runId: RUN_ID, phase: 'Implement', label, payload: completed[i], summary, role: 'implementer' });
90
+ }
71
91
 
72
92
  phase('Verify')
73
93
  const reviews = await pipeline(
74
94
  completed,
75
- (result, _original, index) => dispatchAgent(agent, `reviewer-${index + 1}`, `Try to refute this implementation result for task "${TASK}". Check correctness, security, scope, test evidence, and integration assumptions. Return only verified findings and required checks.\nResult: ${String(result)}`, { role: 'adversarial', risk: 'high', capabilities: ['structured-output', 'reasoning'], label: `review:${index + 1}`, phase: 'Verify' }),
95
+ (result, _original, index) => dispatchAgent(agent, `reviewer-${index + 1}`, `Try to refute this implementation result for task "${TASK}". Check correctness, security, scope, test evidence, and integration assumptions. Return only verified findings and required checks.\n${barrierRef({ runId: RUN_ID, phase: 'Implement', label: `implement:${index + 1}:${lanes[index]?.name || ''}`, summary: `review of lane ${lanes[index]?.name || index + 1}` }).promptBlock}`, { role: 'adversarial', risk: 'high', capabilities: ['structured-output', 'reasoning'], label: `review:${index + 1}`, phase: 'Verify' }),
76
96
  )
77
- const final = await dispatchAgent(agent, 'final-verifier', `Synthesize a bounded integration and verification report for task "${TASK}". Do not claim success without fresh command evidence. Identify conflicts between worktrees, exact integration order, remaining gates, and any required human approvals.\nPlan: ${JSON.stringify(approved)}\nImplementations: ${JSON.stringify(completed)}\nReviews: ${JSON.stringify(reviews.filter(Boolean))}`, { role: 'implementer', risk: 'medium', capabilities: ['structured-output', 'reasoning'], label: 'final-verification', phase: 'Verify' })
97
+ // Phase B: persist review artifacts.
98
+ const verifiedReviews = reviews.filter(Boolean);
99
+ for (let i = 0; i < verifiedReviews.length; i++) {
100
+ writeArtifact({ runId: RUN_ID, phase: 'Verify', label: `review:${i + 1}`, payload: verifiedReviews[i], summary: typeof verifiedReviews[i] === 'string' ? verifiedReviews[i].slice(0, 200) : `review ${i + 1}`, role: 'adversarial' });
101
+ }
102
+ const final = await dispatchAgent(agent, 'final-verifier', `Synthesize a bounded integration and verification report for task "${TASK}". Do not claim success without fresh command evidence. Identify conflicts between worktrees, exact integration order, remaining gates, and any required human approvals.\n${barrierRef({ runId: RUN_ID, phase: 'Design', label: 'barrier', summary: `approved plan with ${approved.lanes.length} lanes` }).promptBlock}\n${barrierRef({ runId: RUN_ID, phase: 'Implement', label: 'implement:summary', summary: `${completed.length} lanes complete` }).promptBlock}\n${barrierRef({ runId: RUN_ID, phase: 'Verify', label: 'review:summary', summary: `${verifiedReviews.length} reviews complete` }).promptBlock}`, { role: 'implementer', risk: 'medium', capabilities: ['structured-output', 'reasoning'], label: 'final-verification', phase: 'Verify' })
78
103
 
79
104
  return { status: 'ready-for-integration', task: TASK, research, plan: approved, implementation: completed, reviews: reviews.filter(Boolean), final }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@polderlabs/bizar",
3
- "version": "10.20.1",
3
+ "version": "10.21.0",
4
4
  "description": "Autonomous, human-in-the-loop multi-agent harness for Claude Code with guarded workflows, typed SDK primitives, and MCP tools.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -42,7 +42,9 @@
42
42
  "test": "npm run build:sdk && npm run test:sdk && npm run test:node",
43
43
  "build": "npm run build:sdk",
44
44
  "prepack": "npm run build",
45
- "prepublishOnly": "npm run build"
45
+ "prepublishOnly": "npm run build",
46
+ "workflow:gc": "node cli/commands/workflow-gc.mjs",
47
+ "workflow:gc:dry": "node cli/commands/workflow-gc.mjs --dry-run"
46
48
  },
47
49
  "keywords": [
48
50
  "claude-code",