@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.
- package/cli/commands/workflow-gc.mjs +227 -0
- package/config/workflows/bizar-debug.js +17 -5
- package/config/workflows/bizar-implement.js +22 -6
- package/config/workflows/bizar-research.js +33 -6
- package/config/workflows/lib/dispatch.js +426 -1
- package/config/workflows/ultracode-research.js +13 -3
- package/config/workflows/ultracode-review.js +7 -2
- package/config/workflows/ultracode.js +31 -6
- package/package.json +4 -2
|
@@ -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 {
|
|
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
|
|
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.\
|
|
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.\
|
|
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.\
|
|
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 {
|
|
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: ${
|
|
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}".\
|
|
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.\
|
|
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.\
|
|
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.\
|
|
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 {
|
|
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}\
|
|
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}\
|
|
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}".\
|
|
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.\
|
|
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
|
-
|
|
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 {
|
|
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}\
|
|
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.\
|
|
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 {
|
|
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${
|
|
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 {
|
|
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}\
|
|
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
|
-
|
|
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}".\
|
|
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.\
|
|
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
|
-
|
|
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.
|
|
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",
|