@dougharness/cli 1.0.2 → 1.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bin.js +18 -3
- package/dist/bin.js.map +1 -1
- package/dist/board-serve.js +14 -7
- package/dist/board-serve.js.map +1 -1
- package/dist/board.d.ts +1 -1
- package/dist/board.js +42 -9
- package/dist/board.js.map +1 -1
- package/dist/detect/repo.js +14 -1
- package/dist/detect/repo.js.map +1 -1
- package/dist/detect/types.d.ts +2 -0
- package/dist/doctor.d.ts +46 -0
- package/dist/doctor.js +491 -0
- package/dist/doctor.js.map +1 -0
- package/dist/generate/agents.d.ts +7 -0
- package/dist/generate/agents.js +62 -12
- package/dist/generate/agents.js.map +1 -1
- package/dist/generate/claude-md.js +4 -0
- package/dist/generate/claude-md.js.map +1 -1
- package/dist/generate/config.d.ts +8 -0
- package/dist/generate/config.js +4 -1
- package/dist/generate/config.js.map +1 -1
- package/dist/generate/proposal.js +15 -6
- package/dist/generate/proposal.js.map +1 -1
- package/dist/generate/settings.d.ts +2 -0
- package/dist/generate/settings.js +17 -2
- package/dist/generate/settings.js.map +1 -1
- package/dist/init.d.ts +14 -0
- package/dist/init.js +94 -8
- package/dist/init.js.map +1 -1
- package/dist/memory-setup.d.ts +28 -0
- package/dist/memory-setup.js +326 -0
- package/dist/memory-setup.js.map +1 -0
- package/node_modules/@dougharness/flow/.claude-plugin/plugin.json +16 -4
- package/node_modules/@dougharness/flow/agents/lead.md +1 -1
- package/node_modules/@dougharness/flow/agents/planner.md +3 -3
- package/node_modules/@dougharness/flow/agents/researcher.md +2 -2
- package/node_modules/@dougharness/flow/hooks/hooks.json +16 -0
- package/node_modules/@dougharness/flow/lib/board.mjs +171 -13
- package/node_modules/@dougharness/flow/lib/code-index.mjs +18 -3
- package/node_modules/@dougharness/flow/lib/context.mjs +57 -0
- package/node_modules/@dougharness/flow/lib/cost.mjs +155 -7
- package/node_modules/@dougharness/flow/lib/embeddings.mjs +188 -24
- package/node_modules/@dougharness/flow/lib/land.mjs +74 -10
- package/node_modules/@dougharness/flow/lib/learn.mjs +4 -4
- package/node_modules/@dougharness/flow/lib/memory.mjs +368 -50
- package/node_modules/@dougharness/flow/lib/plan.mjs +11 -1
- package/node_modules/@dougharness/flow/package.json +2 -1
- package/node_modules/@dougharness/flow/scripts/board.mjs +86 -25
- package/node_modules/@dougharness/flow/scripts/context.mjs +50 -0
- package/node_modules/@dougharness/flow/scripts/cost.mjs +15 -3
- package/node_modules/@dougharness/flow/scripts/memory.mjs +197 -37
- package/node_modules/@dougharness/flow/scripts/plan.mjs +54 -64
- package/node_modules/@dougharness/flow/scripts/session-recall.mjs +132 -0
- package/node_modules/@dougharness/flow/skills/doug-hand/SKILL.md +105 -0
- package/node_modules/@dougharness/flow/skills/doug-learn/SKILL.md +1 -1
- package/node_modules/@dougharness/flow/skills/doug-next/SKILL.md +50 -14
- package/node_modules/@dougharness/flow/skills/doug-plan/SKILL.md +1 -1
- package/node_modules/@dougharness/flow/skills/doug-swarm/SKILL.md +7 -7
- package/node_modules/@dougharness/flow/skills/research/SKILL.md +4 -2
- package/node_modules/@dougharness/flow/skills/run-report/SKILL.md +14 -13
- package/node_modules/@dougharness/flow/skills/swarm-launch/SKILL.md +1 -1
- package/node_modules/@dougharness/flow/workflows/doug-implement.js +145 -86
- package/node_modules/@dougharness/gates/.claude-plugin/plugin.json +1 -1
- package/node_modules/@dougharness/gates/hooks/hooks.json +11 -0
- package/node_modules/@dougharness/gates/lib/baseline.mjs +166 -1
- package/node_modules/@dougharness/gates/lib/bash-rules.mjs +21 -14
- package/node_modules/@dougharness/gates/lib/block-ledger.mjs +58 -1
- package/node_modules/@dougharness/gates/lib/checkpoint.mjs +1 -1
- package/node_modules/@dougharness/gates/lib/config.mjs +20 -0
- package/node_modules/@dougharness/gates/lib/docs-gate.mjs +95 -0
- package/node_modules/@dougharness/gates/lib/evidence.mjs +2 -0
- package/node_modules/@dougharness/gates/lib/gate-lock.mjs +94 -0
- package/node_modules/@dougharness/gates/lib/run.mjs +4 -0
- package/node_modules/@dougharness/gates/lib/test-totals.mjs +40 -7
- package/node_modules/@dougharness/gates/lib/trace.mjs +24 -7
- package/node_modules/@dougharness/gates/lib/verified-tree.mjs +225 -0
- package/node_modules/@dougharness/gates/package.json +1 -1
- package/node_modules/@dougharness/gates/scripts/research-cap.mjs +133 -5
- package/node_modules/@dougharness/gates/scripts/stop-gate.mjs +558 -197
- package/node_modules/@dougharness/gates/scripts/trace.mjs +5 -1
- package/node_modules/@dougharness/gates/scripts/turn-start.mjs +17 -0
- package/node_modules/@dougharness/gates/scripts/verify.mjs +201 -0
- package/package.json +4 -4
- package/templates/board-app.js +4 -3
- package/node_modules/@dougharness/flow/lib/rehearse.mjs +0 -825
- package/node_modules/@dougharness/flow/scripts/rehearse.mjs +0 -129
- package/node_modules/@dougharness/flow/skills/core-next/SKILL.md +0 -92
- package/node_modules/@dougharness/flow/skills/harness-fix/SKILL.md +0 -85
|
@@ -47,11 +47,15 @@ const fixAttempts = Number.isInteger(plan.fixAttempts) && plan.fixAttempts >= 0
|
|
|
47
47
|
// one card. Agents are counted here and always enforced; tokens and wall time come from the runtime's budget
|
|
48
48
|
// primitive and are enforced only when it reports them. plan.budget overrides any of the three. Keep these three
|
|
49
49
|
// numbers equal to lib/plan.mjs's DEFAULT_BUDGET.
|
|
50
|
-
|
|
50
|
+
// agents 26 is the minimum that lets the default fixAttempts 5 run with one adversary seat when Codex is down and the Claude fallback runs on every pass (card flow-budget-after-parallel-adversary); a supervisor seat or a fix retry is not covered.
|
|
51
|
+
const DEFAULT_BUDGET = { agents: 26, tokens: 400000, wallMinutes: 40 }
|
|
51
52
|
const taskBudget = { ...DEFAULT_BUDGET, ...(plan.budget && typeof plan.budget === 'object' ? plan.budget : {}) }
|
|
52
53
|
// The phrase the verifier and reviewer put at the start of a finding when the spec cannot satisfy the plan's
|
|
53
54
|
// acceptance criteria. Such a block goes back to the planner and is never retried.
|
|
54
55
|
const CONTRADICTION_MARKER = 'SPEC CONTRADICTS ACCEPTANCE'
|
|
56
|
+
// Why a task stopped: every halt site sets one of these as stopClass beside its prose stopReason. Kept equal to
|
|
57
|
+
// STOP_CLASSES in lib/board.mjs by a template test (this file may not import).
|
|
58
|
+
const STOP_CLASSES = ['implementer-blocked', 'partial', 'stage-missing', 'spec-contradiction', 'environment', 'adversary-not-run', 'outside-owned', 'new-blockers-twice', 'fix-attempts-exhausted', 'stalled', 'budget', 'no-new-commit', 'dependency-skipped', 'stage-threw', 'level-adversary']
|
|
55
59
|
// The phrase a verifier, checker, or size-check puts at the start of a finding when a command could not run for
|
|
56
60
|
// an environment reason (a tool missing from the worktree even after the install step, a sandbox denial) rather
|
|
57
61
|
// than a defect in the diff. The fix loop never retries such a finding (card reused-s-task-worktree-install):
|
|
@@ -60,6 +64,7 @@ const ENVIRONMENT_MARKER = 'ENVIRONMENT ONLY'
|
|
|
60
64
|
// The one rule about commit messages, in every prompt that commits or checks commits: this repository records
|
|
61
65
|
// attribution nowhere in git, and plan.mjs land refuses a branch that carries either trailer.
|
|
62
66
|
const NO_TRAILERS = 'Commit messages carry no attribution trailers: no Co-Authored-By line and no Claude-Session line, whatever your defaults say. A commit with either is a finding and the plan cannot land.'
|
|
67
|
+
const LESSONS_NOT_SPEC = 'These are context from earlier runs, not part of the spec or the acceptance criteria; never report a finding, a spec contradiction, or a blocker against a lesson.'
|
|
63
68
|
// The one rule about a partial result, in every prompt a worker (or a resumed worker) can end with one from
|
|
64
69
|
// (card worker-context-handoff): quotes the harness's context notice (a PostToolUse hook in plugins/doug-gates) so
|
|
65
70
|
// a worker knows partial=true is only for a worker the harness itself told to stop, never a first resort, and only
|
|
@@ -284,8 +289,11 @@ const INTEGRATE_SCHEMA = {
|
|
|
284
289
|
branch: { type: 'string' },
|
|
285
290
|
merged: { type: 'array', items: { type: 'string' } },
|
|
286
291
|
conflicts: { type: 'array', items: { type: 'string' } },
|
|
287
|
-
verify: { type: 'array', items: { type: 'object', required: ['command', 'ok'], properties: { command: { type: 'string' }, ok: { type: 'boolean' }, outputTail: { type: 'string' } } } },
|
|
292
|
+
verify: { type: 'array', items: { type: 'object', required: ['command', 'ok'], properties: { command: { type: 'string' }, ok: { type: 'boolean' }, outputTail: { type: 'string' }, credited: { type: 'string' } } } },
|
|
288
293
|
ok: { type: 'boolean' },
|
|
294
|
+
// Card level-adversary-empty-diff: the integration branch's HEAD sha before this stage's merges; the level
|
|
295
|
+
// adversary diffs from it. Optional: a missing or malformed value falls back to the level's base branch.
|
|
296
|
+
baseCommit: { type: 'string' },
|
|
289
297
|
// Card integration-acceptance-recorded: only the last level's integrate stage fills this in (step 3b of
|
|
290
298
|
// integratePrompt); not required, since an earlier level has not built the whole plan yet.
|
|
291
299
|
acceptance: { type: 'array', description: 'one entry per acceptance command listed in the prompt, [] when none', items: { type: 'object', required: ['text', 'command', 'ok', 'exitCode'], properties: { text: { type: 'string' }, command: { type: 'string' }, ok: { type: 'boolean' }, exitCode: { type: ['integer', 'null'] } } } },
|
|
@@ -300,7 +308,7 @@ const INTEGRATE_SCHEMA = {
|
|
|
300
308
|
// (empty, prose, or unrecognised) is dropped rather than trusted or blamed. A plan command with no matching
|
|
301
309
|
// report is appended as { ..., reported: false } - unreported and failed are different causes and get different
|
|
302
310
|
// reason clauses. Pure: never mutates its argument, never throws on odd shapes.
|
|
303
|
-
function enforceIntegrationAcceptance(integration, isLastLevel, entries) {
|
|
311
|
+
function enforceIntegrationAcceptance(integration, isLastLevel, entries, verifyCommands = []) {
|
|
304
312
|
if (!integration || typeof integration !== 'object') return integration
|
|
305
313
|
if (!isLastLevel) {
|
|
306
314
|
const rest = { ...integration }
|
|
@@ -309,10 +317,20 @@ function enforceIntegrationAcceptance(integration, isLastLevel, entries) {
|
|
|
309
317
|
}
|
|
310
318
|
const reported = Array.isArray(integration.acceptance) ? integration.acceptance.filter(a => a && typeof a === 'object') : []
|
|
311
319
|
const acceptance = []
|
|
320
|
+
const verifyList = Array.isArray(integration.verify) ? integration.verify.filter(v => v && typeof v === 'object') : []
|
|
321
|
+
const repeated = new Set((Array.isArray(verifyCommands) ? verifyCommands : []).map(c => String(c).trim()))
|
|
312
322
|
for (const e of entries) {
|
|
313
323
|
if (!e.command) continue
|
|
314
324
|
const wantCommand = String(e.command).trim()
|
|
315
325
|
if (!wantCommand) continue
|
|
326
|
+
// Card flow-suite-dedupe: a command that repeats a plan.verify command takes its result from the verify item.
|
|
327
|
+
if (repeated.has(wantCommand)) {
|
|
328
|
+
const v = verifyList.find(x => typeof x.command === 'string' && String(x.command).trim() === wantCommand)
|
|
329
|
+
acceptance.push(v
|
|
330
|
+
? { text: e.text, command: e.command, ok: v.ok === true, exitCode: v.ok === true ? 0 : null, from: 'verify' }
|
|
331
|
+
: { text: e.text, command: e.command, ok: false, exitCode: null, reported: false })
|
|
332
|
+
continue
|
|
333
|
+
}
|
|
316
334
|
const match = reported.find(a => typeof a.command === 'string' && String(a.command).trim() === wantCommand)
|
|
317
335
|
acceptance.push(match
|
|
318
336
|
? { text: match.text, command: e.command, ok: match.ok, exitCode: match.exitCode }
|
|
@@ -937,7 +955,7 @@ function workerPrompt(task, brief, n, lead) {
|
|
|
937
955
|
// left an orphan - it defaults to `workers` for the plain, no-rebrief case where the two are the same list.
|
|
938
956
|
function leadMergePrompt(task, lead, workers, baseBranch, allWorkers = workers) {
|
|
939
957
|
return [
|
|
940
|
-
`You are the lead of the swarm for task ${task.id}. The workers have committed; merge their branches into the task branch and hand it to the checks. Work only in
|
|
958
|
+
`You are the lead of the swarm for task ${task.id}. The workers have committed; merge their branches into the task branch and hand it to the checks. Work only in the git worktree you were started in, on branch ${lead.branch}, and never edit anything under the repository root. The lead's worktree (${lead.worktreePath}) may have been removed, so do not cd into it. First git checkout ${lead.branch} there; if git refuses because the branch is checked out in another worktree, that worktree is a leftover under .claude/worktrees: find it with git worktree list --porcelain, remove it with git worktree remove --force <path>, and check out again. If the branch does not exist, create it from ${baseBranch} with git checkout -b ${lead.branch} ${baseBranch} (the lead commits nothing, so the task branch is the base).`,
|
|
941
959
|
PARTIAL_NEVER_MERGED,
|
|
942
960
|
``,
|
|
943
961
|
`Task ${task.id}: ${task.title}`,
|
|
@@ -953,18 +971,18 @@ function leadMergePrompt(task, lead, workers, baseBranch, allWorkers = workers)
|
|
|
953
971
|
`3. Do not implement missing pieces yourself: a brief a worker did not finish is a block with its name. ${NO_TRAILERS}`,
|
|
954
972
|
`4. Remove every worker worktree to leave the repository tidy, merged or not (a blocked worker's is an orphan otherwise): git worktree remove --force <path> for each one below. Keep their branches.`,
|
|
955
973
|
...allWorkers.filter(w => w.worktreePath).map(w => ` - ${w.worktreePath} (worker ${w.n}: ${w.id})`),
|
|
956
|
-
`5. Return taskId="${task.id}", branch="${lead.branch}", worktreePath=
|
|
974
|
+
`5. Return taskId="${task.id}", branch="${lead.branch}", worktreePath=the absolute path of the worktree you ran in (git rev-parse --show-toplevel), filesTouched from git diff --name-only ${baseBranch}...HEAD, commandsRun, commit from git rev-parse HEAD after the last merge, summary naming the briefs merged, blocked=false; blocked=true with the reason when a merge or the verification failed for a reason in the code.`,
|
|
957
975
|
].join('\n')
|
|
958
976
|
}
|
|
959
977
|
|
|
960
978
|
// Card swarm-topology (candidate 2): one worker blocked does not end the task - the lead gets one chance to
|
|
961
|
-
// re-brief the unfinished pieces,
|
|
979
|
+
// re-brief the unfinished pieces, reading from the repository root (no worktree, no isolation key). `workers` is every
|
|
962
980
|
// round-one outcome (so the lead sees a finished sibling's commit, not just the blocked ones); `blockedWorkers` is
|
|
963
981
|
// the subset that needs a revised brief.
|
|
964
982
|
function leadRebriefPrompt(task, lead, workers, blockedWorkers) {
|
|
965
983
|
const finished = workers.filter(w => !w.blocked)
|
|
966
984
|
return [
|
|
967
|
-
`You are the lead of the swarm for task ${task.id}. One or more workers are blocked; you get one chance to re-brief the unfinished pieces. You implement nothing and edit no file. Work only
|
|
985
|
+
`You are the lead of the swarm for task ${task.id}. One or more workers are blocked; you get one chance to re-brief the unfinished pieces. You implement nothing and edit no file. Work read-only from the repository root you were started in. Do not cd into the lead's worktree (it may have been removed), create no branch, and edit nothing. Read a finished worker's committed files with \`git show <commit>:<path>\` or \`git show <commit>\`.`,
|
|
968
986
|
``,
|
|
969
987
|
`Plan: ${plan.title}`,
|
|
970
988
|
`Goal: ${plan.goal}`,
|
|
@@ -1114,6 +1132,7 @@ function verifyPrompt(task, impl, baseBranch) {
|
|
|
1114
1132
|
`Task ${task.id}: ${task.title}`,
|
|
1115
1133
|
`Spec:`,
|
|
1116
1134
|
task.spec,
|
|
1135
|
+
...(typeof task.lessons === 'string' && task.lessons ? ['', `Lessons from earlier runs on this repository (memory; each cites where it was learned):`, LESSONS_NOT_SPEC, String(task.lessons).slice(0, 2000)] : []),
|
|
1117
1136
|
``,
|
|
1118
1137
|
`Plan acceptance criteria (the spec must be consistent with these; a line starting with $ is the command that checks the criterion above it and exits 0 when it holds):`,
|
|
1119
1138
|
...acceptanceEntries(plan.acceptance || []).flatMap(a => a.command ? [`- ${a.text}`, ` $ ${a.command}`] : [`- ${a.text}`]),
|
|
@@ -1146,6 +1165,7 @@ function checkPrompt(task, impl, prevCommit, baseBranch, ledger) {
|
|
|
1146
1165
|
`Task ${task.id}: ${task.title}`,
|
|
1147
1166
|
`Spec:`,
|
|
1148
1167
|
task.spec,
|
|
1168
|
+
...(typeof task.lessons === 'string' && task.lessons ? ['', `Lessons from earlier runs on this repository (memory; each cites where it was learned):`, LESSONS_NOT_SPEC, String(task.lessons).slice(0, 2000)] : []),
|
|
1149
1169
|
``,
|
|
1150
1170
|
`Plan acceptance criteria (the spec must be consistent with these; a line starting with $ is the command that checks the criterion above it and exits 0 when it holds):`,
|
|
1151
1171
|
...acceptanceEntries(plan.acceptance || []).flatMap(a => a.command ? [`- ${a.text}`, ` $ ${a.command}`] : [`- ${a.text}`]),
|
|
@@ -1212,10 +1232,9 @@ function levelSpecLines(sReady) {
|
|
|
1212
1232
|
// codex-review over a level's size-S tasks on the integration branch, once, after the merge (and once more to confirm
|
|
1213
1233
|
// a fix pass). Every blocker must name a file, because that is how it finds the task it goes back to.
|
|
1214
1234
|
function levelAdversaryPrompt(li, sReady, levelBase, confirming) {
|
|
1215
|
-
const verify = (plan.verify || []).filter(Boolean)
|
|
1216
1235
|
const guard = t => String(t).replace(/^DOUG_SPEC_EOF$/gm, 'DOUG_SPEC_EOF ')
|
|
1217
1236
|
const command = [
|
|
1218
|
-
`cat <<'DOUG_SPEC_EOF' | ${adversary.command} --base ${shellQuote(levelBase)} --head ${shellQuote(integrationBranch)} --dir ${shellQuote(INTEGRATION_WORKTREE)} --spec - --timeout-ms ${adversary.timeoutMs}${typeof adversary.effort === 'string' && adversary.effort ? ` --effort ${adversary.effort}` : ''}
|
|
1237
|
+
`cat <<'DOUG_SPEC_EOF' | ${adversary.command} --base ${shellQuote(levelBase)} --head ${shellQuote(integrationBranch)} --dir ${shellQuote(INTEGRATION_WORKTREE)} --spec - --timeout-ms ${adversary.timeoutMs}${typeof adversary.effort === 'string' && adversary.effort ? ` --effort ${adversary.effort}` : ''}`,
|
|
1219
1238
|
`Level ${li} of plan "${guard(plan.title)}": the size-S tasks below were merged into ${integrationBranch} with one focused check each and no adversary of their own. Review them together against their specs. Every blocker must name the file it is about, so it can go back to the task that owns that file.`,
|
|
1220
1239
|
...(confirming ? [`A fix pass answered the blockers you raised on this level; confirm each one is fixed, name it as fixed or still open, and check the fix for regressions rather than exploring afresh.`] : [`This is the first review of this level: explore freely and try to refute the changes however you can.`]),
|
|
1221
1240
|
``,
|
|
@@ -1240,7 +1259,7 @@ function levelAdversaryPrompt(li, sReady, levelBase, confirming) {
|
|
|
1240
1259
|
}
|
|
1241
1260
|
|
|
1242
1261
|
function levelFallbackAdversaryPrompt(li, sReady, levelBase, why, confirming) {
|
|
1243
|
-
|
|
1262
|
+
// Same scope as levelAdversaryPrompt (card flow-fallback-verify-scope): no verify list, the suite already ran at integrate.
|
|
1244
1263
|
return [
|
|
1245
1264
|
`You are the adversarial reviewer for level ${li} of plan "${plan.title}", standing in for codex-review, which could not run (${why}). The size-S tasks below were merged into ${integrationBranch} with one focused check each and no adversary of their own. Your job is to refute them together, in worktree ${INTEGRATION_WORKTREE} (relative to the repository root); the diff is ${levelBase}...${integrationBranch}.`,
|
|
1246
1265
|
``,
|
|
@@ -1248,8 +1267,7 @@ function levelFallbackAdversaryPrompt(li, sReady, levelBase, why, confirming) {
|
|
|
1248
1267
|
`Plan acceptance criteria:`,
|
|
1249
1268
|
...acceptanceEntries(plan.acceptance || []).map(a => `- ${a.text}`),
|
|
1250
1269
|
``,
|
|
1251
|
-
`
|
|
1252
|
-
...verify.map(v => ` $ ${v}`),
|
|
1270
|
+
`Record every command you run, your own probe checks included, in commandsRun with its real exit code and outputTail (the last lines of its output); a blocker's evidence must cite one of them, and either that command failed or the evidence quotes a line of its outputTail, otherwise the loop records it as major.`,
|
|
1253
1271
|
confirming
|
|
1254
1272
|
? `Then confirm the blockers you raised on this level are fixed, name each as fixed or still open, and check the fix for regressions rather than exploring afresh.`
|
|
1255
1273
|
: `Then try to break the changes: inputs the tests do not cover, a spec sentence or acceptance criterion a diff does not meet, a test that was weakened, skipped, or deleted, and behavior outside the owned files. When a suspicion needs a check, write a throwaway one, run it, and remove it before you finish.`,
|
|
@@ -1275,6 +1293,7 @@ function reviewPrompt(task, impl, baseBranch, seat = 1) {
|
|
|
1275
1293
|
`Task ${task.id}: ${task.title}`,
|
|
1276
1294
|
`Spec:`,
|
|
1277
1295
|
task.spec,
|
|
1296
|
+
...(typeof task.lessons === 'string' && task.lessons ? ['', `Lessons from earlier runs on this repository (memory; each cites where it was learned):`, LESSONS_NOT_SPEC, String(task.lessons).slice(0, 2000)] : []),
|
|
1278
1297
|
`Owned files: ${task.files.join(', ')}`,
|
|
1279
1298
|
`Plan acceptance criteria:`,
|
|
1280
1299
|
...acceptanceEntries(plan.acceptance || []).flatMap(a => a.command ? [`- ${a.text}`, ` $ ${a.command}`] : [`- ${a.text}`]),
|
|
@@ -1310,13 +1329,14 @@ function confirmationLines(pass, ledger, impl) {
|
|
|
1310
1329
|
}
|
|
1311
1330
|
|
|
1312
1331
|
function adversaryPrompt(task, impl, baseBranch, pass, ledger) {
|
|
1313
|
-
const verify = [task.verify
|
|
1332
|
+
const verify = [task.verify].filter(Boolean)
|
|
1314
1333
|
const guard = s => String(s).replace(/^DOUG_SPEC_EOF$/gm, 'DOUG_SPEC_EOF ')
|
|
1315
1334
|
const command = [
|
|
1316
1335
|
`cat <<'DOUG_SPEC_EOF' | ${adversary.command} --base ${shellQuote(baseBranch)} --head ${shellQuote(impl.branch)} --dir ${shellQuote(impl.worktreePath)} --spec - --timeout-ms ${adversary.timeoutMs}${typeof adversary.effort === 'string' && adversary.effort ? ` --effort ${adversary.effort}` : ''}${verify.map(v => ` --verify ${shellQuote(v)}`).join('')}`,
|
|
1317
1336
|
`Task ${task.id}: ${task.title}`,
|
|
1318
1337
|
``,
|
|
1319
1338
|
guard(task.spec),
|
|
1339
|
+
...(typeof task.lessons === 'string' && task.lessons ? ['', `Lessons from earlier runs on this repository (memory; each cites where it was learned):`, LESSONS_NOT_SPEC, String(task.lessons).slice(0, 2000)] : []).map(guard),
|
|
1320
1340
|
``,
|
|
1321
1341
|
...(pass > 1 ? confirmationLines(pass, ledger, impl).map(guard) : [`This is the first pass of this task: explore freely and try to refute the change however you can.`]),
|
|
1322
1342
|
``,
|
|
@@ -1371,21 +1391,27 @@ function adversaryUnavailable(adv) {
|
|
|
1371
1391
|
}
|
|
1372
1392
|
|
|
1373
1393
|
function fallbackAdversaryPrompt(task, impl, baseBranch, why, pass, ledger) {
|
|
1374
|
-
|
|
1394
|
+
// Same scope as adversaryPrompt's Codex seat (card flow-fallback-verify-scope): the task's own verify only, not plan.verify.
|
|
1395
|
+
const verify = [task.verify].filter(Boolean)
|
|
1375
1396
|
return [
|
|
1376
1397
|
`You are the adversarial reviewer for task ${task.id}, standing in for codex-review, which could not run (${why}). Your job is to refute the change on branch ${impl.branch} in worktree ${impl.worktreePath}; the diff is ${baseBranch}...${impl.branch}. ${baseBranch} is this branch's base (an integration branch when the task follows an earlier level); files that already exist there are not this task's changes. Do not edit any file.`,
|
|
1377
1398
|
``,
|
|
1378
1399
|
`Task ${task.id}: ${task.title}`,
|
|
1379
1400
|
`Spec:`,
|
|
1380
1401
|
task.spec,
|
|
1402
|
+
...(typeof task.lessons === 'string' && task.lessons ? ['', `Lessons from earlier runs on this repository (memory; each cites where it was learned):`, LESSONS_NOT_SPEC, String(task.lessons).slice(0, 2000)] : []),
|
|
1381
1403
|
`Owned files: ${task.files.join(', ')}`,
|
|
1382
1404
|
`Plan acceptance criteria:`,
|
|
1383
1405
|
...acceptanceEntries(plan.acceptance || []).flatMap(a => a.command ? [`- ${a.text}`, ` $ ${a.command}`] : [`- ${a.text}`]),
|
|
1384
1406
|
...(pass > 1 ? confirmationLines(pass, ledger, impl) : []),
|
|
1385
1407
|
``,
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1408
|
+
...(verify.length
|
|
1409
|
+
? [
|
|
1410
|
+
`Run every one of these commands in the worktree and record each in commandsRun with its real exit code and outputTail (the last lines of its output):`,
|
|
1411
|
+
...verify.map(v => ` $ ${v}`),
|
|
1412
|
+
]
|
|
1413
|
+
: [`Record every command you run, your own probe checks included, in commandsRun with its real exit code and outputTail (the last lines of its output).`]),
|
|
1414
|
+
`A blocker's evidence must name one of ${verify.length ? 'those commands' : 'the commands in commandsRun'}, and either that command failed or the evidence quotes a line of its outputTail; otherwise the loop records it as major.`,
|
|
1389
1415
|
pass > 1
|
|
1390
1416
|
? `Then confirm the open findings above and check the fix delta for regressions rather than exploring afresh.`
|
|
1391
1417
|
: `Then try to break the change: inputs the tests do not cover, a spec sentence or acceptance criterion the diff does not meet, a test that was weakened, skipped, or deleted, and behavior outside the owned files. When a suspicion needs a check, write a throwaway one, run it, and remove it before you finish.`,
|
|
@@ -1417,14 +1443,26 @@ function integratePrompt(levelIndex, results, baseBranch, isLastLevel, keepWorkt
|
|
|
1417
1443
|
` if it exists (a previous level made it): use it as is; it already has ${integrationBranch} checked out.`,
|
|
1418
1444
|
]),
|
|
1419
1445
|
` Do every following git and verify command inside that worktree (git -C or cd into it).`,
|
|
1446
|
+
` Before any merge of this stage, run git rev-parse HEAD in that worktree and report the full 40-character sha as baseCommit.`,
|
|
1420
1447
|
`2. Merge each branch with a merge commit (git merge --no-ff <branch>). If a merge conflicts, resolve it only if the resolution is mechanical and obviously correct; otherwise git merge --abort and list the branch in conflicts. ${NO_TRAILERS} That includes the merge commits you make here.`,
|
|
1421
1448
|
`3. Install dependencies there if the verify commands need them and they are missing${plan.install ? ` (${plan.install})` : ''}. Then run every command in: ${plan.verify.length ? plan.verify.map(v => `"${v}"`).join(', ') : 'the project test command'} and record real exit statuses.`,
|
|
1449
|
+
...(plan.verify.length
|
|
1450
|
+
? [
|
|
1451
|
+
` When .doug/hooks/scripts/verify.mjs exists, run them from the repository root (not inside the worktree) as one command, so a tree already verified is credited instead of re-run:`,
|
|
1452
|
+
` node .doug/hooks/scripts/verify.mjs --json --by integrate --cwd .claude/worktrees/doug-integration --record-dir . ${worktrees.map(w => `--credit-dir ${shellQuote(w)}`).join(' ')} ${plan.verify.map(v => `--command ${shellQuote(v)}`).join(' ')}`,
|
|
1453
|
+
` Copy each entry of its JSON results[] into verify as { command, ok, outputTail }, and add credited: "verified-tree" to each when the JSON's credited is not null. When verify.mjs does not exist, run the commands one by one as above.`,
|
|
1454
|
+
]
|
|
1455
|
+
: []),
|
|
1422
1456
|
// Card integration-acceptance-recorded: only the last level runs the plan's acceptance commands, and only
|
|
1423
1457
|
// after the merges above - the plan is not complete at an earlier level, so its prompt never mentions this.
|
|
1424
1458
|
...(isLastLevel
|
|
1425
1459
|
? [
|
|
1426
1460
|
`3b. Plan acceptance criteria (a line starting with $ is the command that checks the criterion above it and exits 0 when it holds; the plan is complete at this level, so run every one now):`,
|
|
1427
|
-
...acceptanceEntries(plan.acceptance || []).flatMap(a => a.command
|
|
1461
|
+
...acceptanceEntries(plan.acceptance || []).flatMap(a => a.command
|
|
1462
|
+
? ((plan.verify || []).some(v => String(v).trim() === String(a.command).trim())
|
|
1463
|
+
? [`- ${a.text} is checked by step 3's \`${a.command}\`; do not run it again and do not report it in acceptance; the workflow takes its result from verify.`]
|
|
1464
|
+
: [`- ${a.text}`, ` $ ${a.command}`])
|
|
1465
|
+
: [`- ${a.text}`]),
|
|
1428
1466
|
`Run every $ command above inside this integration worktree, exactly as written, after the merges, and report each one in \`acceptance\` as { text, command, ok, exitCode } with its real exit code (ok means exit code 0; exitCode null only if the command could not start). Never report one you did not run; return acceptance: [] when the plan lists no commands.`,
|
|
1429
1467
|
`Report each command string exactly as it is written above, character for character, even when you ran it behind a cd or git -C prefix: the workflow matches your report to the plan by that string, and a command it cannot match counts as not run and fails the integration.`,
|
|
1430
1468
|
`A criterion with no $ line is not yours to judge here: do not report it.`,
|
|
@@ -1441,8 +1479,8 @@ function integratePrompt(levelIndex, results, baseBranch, isLastLevel, keepWorkt
|
|
|
1441
1479
|
// ---- Task loop -------------------------------------------------------------------
|
|
1442
1480
|
// One task from start to a verdict: implement (or check out a reused branch), then verify, review and the
|
|
1443
1481
|
// adversary. While the block is one the checks described inside the task's own files, relaunch the implementer in
|
|
1444
|
-
// the same worktree; a fix pass reruns the
|
|
1445
|
-
// loop stops as soon as it stops converging (no new commit, two consecutive novel blockers, or the task budget).
|
|
1482
|
+
// the same worktree; a fix pass reruns the verifier or reviewer that blocked, then one focused check and the
|
|
1483
|
+
// adversary together, and the loop stops as soon as it stops converging (no new commit, two consecutive novel blockers, or the task budget).
|
|
1446
1484
|
async function startTask(task, baseBranch, launch = agent) {
|
|
1447
1485
|
if (task.reuse) return launch(reusePrompt(task, baseBranch), { label: `reuse:${task.id}`, phase: 'Implement', isolation: 'worktree', schema: IMPLEMENT_SCHEMA, model: CHECKOUT_MODEL })
|
|
1448
1486
|
if (swarmApplies(task)) return runSwarm(task, baseBranch, launch)
|
|
@@ -1649,7 +1687,7 @@ async function runSwarm(task, baseBranch, launch) {
|
|
|
1649
1687
|
const reason = `worker${round1Blocked.length === 1 ? '' : 's'} blocked: ${round1Blocked.map(w => `${w.n} (${w.id}): ${w.blockedReason}`).join('; ')}`
|
|
1650
1688
|
// Rule 4: a worker still partial after its resume blocks the task exactly as before - no re-brief attempt.
|
|
1651
1689
|
if (round1Blocked.some(w => w.partial)) return blocked(reason, { worktreePath: lead.worktreePath, branch: lead.branch })
|
|
1652
|
-
// Rule 2: one re-brief,
|
|
1690
|
+
// Rule 2: one re-brief, reading from the repository root (no isolation key, no new worktree), same schema as the
|
|
1653
1691
|
// first split.
|
|
1654
1692
|
stages.push('lead-rebrief')
|
|
1655
1693
|
const revised = await launch(leadRebriefPrompt(task, lead, round1, round1Blocked), agentOpts('lead', null, { label: `lead-rebrief:${task.id}`, phase: 'Implement', agentType: 'doug-flow:lead', schema: LEAD_SCHEMA }))
|
|
@@ -1700,7 +1738,8 @@ async function runSwarm(task, baseBranch, launch) {
|
|
|
1700
1738
|
// workers, in the order they were pushed. `workers` itself (review minor 5) still goes to leadMergePrompt as the
|
|
1701
1739
|
// worktree-removal list, blocked ones included, so a blocked worker's worktree is not left an orphan.
|
|
1702
1740
|
const finished = workers.filter(w => !w.blocked)
|
|
1703
|
-
|
|
1741
|
+
// Card swarm-lead-worktree-missing: the lead's own worktree is gone by now (rehearsal swarm-2026-10-09T15-20-40-679Z), so the merge gets a fresh one.
|
|
1742
|
+
const merged = await launch(leadMergePrompt(task, lead, finished, baseBranch, workers), agentOpts('lead', null, { label: `lead-merge:${task.id}`, phase: 'Implement', isolation: 'worktree', agentType: 'doug-flow:lead', schema: IMPLEMENT_SCHEMA }))
|
|
1704
1743
|
if (!merged) return blocked('lead merge returned nothing (spawn failure or no structured output)', { worktreePath: lead.worktreePath, branch: lead.branch })
|
|
1705
1744
|
return { ...merged, branch: merged.branch || lead.branch, worktreePath: merged.worktreePath || lead.worktreePath, swarmStages: stages, workers, splitReason: lead.splitReason, rebrief, checkFailed: workers.filter(w => w.checkFailed).map(w => w.id) }
|
|
1706
1745
|
}
|
|
@@ -1790,27 +1829,27 @@ function ownedBySibling(task, p) {
|
|
|
1790
1829
|
|
|
1791
1830
|
// Can this block be fixed by the same implementer, or must a human see it?
|
|
1792
1831
|
function retriable(task, r) {
|
|
1793
|
-
if (!r.impl) return { ok: false, reason: 'implementer returned nothing' }
|
|
1832
|
+
if (!r.impl) return { ok: false, reason: 'implementer returned nothing', stopClass: 'stage-missing' }
|
|
1794
1833
|
// A fix pass that returned no structured result changed nothing the loop knows of: the branch and worktree it
|
|
1795
1834
|
// had are still there, so the same implementer is asked again.
|
|
1796
1835
|
if (r.noResult) return { ok: true, reason: r.noResult }
|
|
1797
|
-
if (r.impl.blocked) return { ok: false, reason: `implementer blocked: ${r.impl.blockedReason || 'no reason given'}
|
|
1836
|
+
if (r.impl.blocked) return { ok: false, reason: `implementer blocked: ${r.impl.blockedReason || 'no reason given'}`, stopClass: 'implementer-blocked' }
|
|
1798
1837
|
// A partial result is not a check failure, whether it came from the initial worker's one resume or from a fix
|
|
1799
1838
|
// pass: the fix loop never retries it (card worker-context-handoff).
|
|
1800
|
-
if (r.impl.partial) return { ok: false, reason: 'a partial result is not retried by the fix loop' }
|
|
1839
|
+
if (r.impl.partial) return { ok: false, reason: 'a partial result is not retried by the fix loop', stopClass: 'partial' }
|
|
1801
1840
|
// Only a stage that actually ran can be missing; a fix pass deliberately skips the stages that did not block.
|
|
1802
1841
|
const ran = r.stages || []
|
|
1803
|
-
if (ran.includes('verify') && !r.ver) return { ok: false, reason: 'verifier returned nothing' }
|
|
1804
|
-
if (ran.includes('review') && !r.rev) return { ok: false, reason: 'reviewer returned nothing' }
|
|
1805
|
-
if (ran.includes('check') && !r.check) return { ok: false, reason: 'check returned nothing' }
|
|
1842
|
+
if (ran.includes('verify') && !r.ver) return { ok: false, reason: 'verifier returned nothing', stopClass: 'stage-missing' }
|
|
1843
|
+
if (ran.includes('review') && !r.rev) return { ok: false, reason: 'reviewer returned nothing', stopClass: 'stage-missing' }
|
|
1844
|
+
if (ran.includes('check') && !r.check) return { ok: false, reason: 'check returned nothing', stopClass: 'stage-missing' }
|
|
1806
1845
|
const texts = [...((r.ver && r.ver.findings) || []), ...(((r.rev && r.rev.issues) || []).map(i => i.description))]
|
|
1807
1846
|
const contradiction = texts.find(t => typeof t === 'string' && t.includes(CONTRADICTION_MARKER))
|
|
1808
|
-
if (contradiction) return { ok: false, reason: `spec contradicts acceptance: ${contradiction}
|
|
1847
|
+
if (contradiction) return { ok: false, reason: `spec contradicts acceptance: ${contradiction}`, stopClass: 'spec-contradiction' }
|
|
1809
1848
|
const environmentOnly = texts.find(t => typeof t === 'string' && t.includes(ENVIRONMENT_MARKER))
|
|
1810
|
-
if (environmentOnly) return { ok: false, reason: `environment, not the code: ${environmentOnly}
|
|
1811
|
-
if (adversary && r.adv && !r.adv.ran) return { ok: false, reason: `adversary did not run: ${r.adv.error || 'no reason given'}
|
|
1849
|
+
if (environmentOnly) return { ok: false, reason: `environment, not the code: ${environmentOnly}`, stopClass: 'environment' }
|
|
1850
|
+
if (adversary && r.adv && !r.adv.ran && !r.blockingStage) return { ok: false, reason: `adversary did not run: ${r.adv.error || 'no reason given'}`, stopClass: 'adversary-not-run' }
|
|
1812
1851
|
const outside = [...new Set(findingPaths(r).filter(p => !ownsPath(task, p) && !ownedBySibling(task, p)))]
|
|
1813
|
-
if (outside.length) return { ok: false, reason: `findings name files outside the owned set: ${outside.join(', ')}
|
|
1852
|
+
if (outside.length) return { ok: false, reason: `findings name files outside the owned set: ${outside.join(', ')}`, stopClass: 'outside-owned' }
|
|
1814
1853
|
return { ok: true, reason: 'every finding is within the owned files' }
|
|
1815
1854
|
}
|
|
1816
1855
|
|
|
@@ -2094,9 +2133,9 @@ function stageHead(ver, check) {
|
|
|
2094
2133
|
}
|
|
2095
2134
|
|
|
2096
2135
|
// baseBranch is a parameter, not the outer binding, so this block can be built and exercised on its own.
|
|
2097
|
-
// ctx: { ledger, launch, lead, prevCommit, blocking }. On pass 1 the verifier
|
|
2098
|
-
//
|
|
2099
|
-
//
|
|
2136
|
+
// ctx: { ledger, launch, lead, prevCommit, blocking }. On pass 1 the verifier, the reviewer, and the adversary run
|
|
2137
|
+
// together. On a fix pass the verifier or the reviewer reruns first when it is the stage that blocked, and ends the
|
|
2138
|
+
// pass if it blocks again; then the focused check and the adversary run together.
|
|
2100
2139
|
async function runChecks(task, impl, pass, baseBranch, ctx) {
|
|
2101
2140
|
const c = ctx || {}
|
|
2102
2141
|
const ledger = c.ledger || []
|
|
@@ -2117,57 +2156,68 @@ async function runChecks(task, impl, pass, baseBranch, ctx) {
|
|
|
2117
2156
|
if (check) ({ ver, rev } = readCheck(check))
|
|
2118
2157
|
blockingStage = !(check && check.passed && rev && rev.approve) ? 'check' : null
|
|
2119
2158
|
} else if (pass === 1) {
|
|
2120
|
-
// The verifier and the
|
|
2121
|
-
// launches them together; a null from
|
|
2159
|
+
// The verifier, the reviewer, and the adversary are read-only and none reads another's result, so a first pass
|
|
2160
|
+
// launches them together; a null from any (skipped, or dead after retries) is that stage returning nothing.
|
|
2122
2161
|
stages.push('verify')
|
|
2123
2162
|
stages.push('review')
|
|
2124
2163
|
const crew = crewOf(task)
|
|
2125
2164
|
for (let seat = 2; seat <= crew.reviewers; seat++) stages.push(`review-${seat}`)
|
|
2165
|
+
const withAdversary = adversaryRequired(task)
|
|
2166
|
+
const advStages = []
|
|
2126
2167
|
const pair = await parallel([
|
|
2127
2168
|
() => launch(verifyPrompt(task, impl, baseBranch), agentOpts('verify', task, { label: `verify:${task.id}`, phase: 'Verify', agentType: 'doug-flow:verifier', schema: VERIFY_SCHEMA })),
|
|
2128
2169
|
() => launch(reviewPrompt(task, impl, baseBranch), agentOpts('review', task, { label: `review:${task.id}`, phase: 'Review', agentType: 'doug-flow:reviewer', schema: REVIEW_SCHEMA })),
|
|
2129
2170
|
...Array.from({ length: crew.reviewers - 1 }, (_, i) => () => launch(reviewPrompt(task, impl, baseBranch, i + 2), agentOpts('review', task, { label: `review-${i + 2}:${task.id}`, phase: 'Review', agentType: 'doug-flow:reviewer', schema: REVIEW_SCHEMA }))),
|
|
2171
|
+
...(withAdversary ? [() => runAdversaryCrew(task, impl, pass, baseBranch, ledger, launch, advStages)] : []),
|
|
2130
2172
|
])
|
|
2131
2173
|
ver = (pair && pair[0]) || null
|
|
2132
|
-
rev = pair ? mergeReviews(pair.slice(1)) : null
|
|
2174
|
+
rev = pair ? mergeReviews(pair.slice(1, 1 + crew.reviewers)) : null
|
|
2133
2175
|
stageResults.verify = ver
|
|
2134
2176
|
stageResults.review = rev
|
|
2135
|
-
//
|
|
2136
|
-
|
|
2137
|
-
|
|
2177
|
+
// The adversary no longer waits for verify and review (card flow-adversary-parallel, 2026-10-07): waiting
|
|
2178
|
+
// added about five minutes to every full task. The cost is a Codex review of trees verify or review rejects;
|
|
2179
|
+
// integration still needs all three verdicts (isReady), and the ledger reads each stage's findings.
|
|
2180
|
+
if (withAdversary) {
|
|
2181
|
+
stages.push(...advStages)
|
|
2182
|
+
adv = (pair && pair[1 + crew.reviewers]) || null
|
|
2138
2183
|
stageResults.adversary = adv
|
|
2139
2184
|
}
|
|
2140
2185
|
blockingStage = !(ver && ver.passed) ? 'verify' : !(rev && rev.approve) ? 'review' : adversaryBlocked(adv) ? 'adversary' : null
|
|
2141
2186
|
} else {
|
|
2142
2187
|
const first = c.blocking || 'verify'
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
|
|
2152
|
-
|
|
2153
|
-
|
|
2154
|
-
|
|
2155
|
-
|
|
2156
|
-
|
|
2157
|
-
|
|
2158
|
-
|
|
2159
|
-
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
|
|
2163
|
-
|
|
2164
|
-
|
|
2165
|
-
|
|
2166
|
-
|
|
2167
|
-
|
|
2168
|
-
|
|
2169
|
-
|
|
2170
|
-
}
|
|
2188
|
+
if (first === 'verify') {
|
|
2189
|
+
stages.push('verify')
|
|
2190
|
+
ver = await launch(verifyPrompt(task, impl, baseBranch), agentOpts('verify', task, { label: `verify:${task.id}${suffix}`, phase: 'Verify', agentType: 'doug-flow:verifier', schema: VERIFY_SCHEMA }))
|
|
2191
|
+
stageResults.verify = ver
|
|
2192
|
+
if (!(ver && ver.passed)) blockingStage = 'verify'
|
|
2193
|
+
} else if (first === 'review') {
|
|
2194
|
+
stages.push('review')
|
|
2195
|
+
const crew = crewOf(task)
|
|
2196
|
+
for (let seat = 2; seat <= crew.reviewers; seat++) stages.push(`review-${seat}`)
|
|
2197
|
+
const seats = await parallel(Array.from({ length: crew.reviewers }, (_, i) => () => launch(reviewPrompt(task, impl, baseBranch, i + 1), agentOpts('review', task, { label: `review${i ? `-${i + 1}` : ''}:${task.id}${suffix}`, phase: 'Review', agentType: 'doug-flow:reviewer', schema: REVIEW_SCHEMA }))))
|
|
2198
|
+
rev = mergeReviews(seats || [])
|
|
2199
|
+
stageResults.review = rev
|
|
2200
|
+
if (!(rev && rev.approve)) blockingStage = 'review'
|
|
2201
|
+
}
|
|
2202
|
+
if (!blockingStage) {
|
|
2203
|
+
// The focused check and the adversary are both read-only, so they run together (card flow-adversary-parallel).
|
|
2204
|
+
// The stages are recorded in the order the serial loop ran them (the adversary first when it blocked the last
|
|
2205
|
+
// pass), since updateLedger reads them in order to tell a stage confirming a fix from one re-reporting it.
|
|
2206
|
+
const withAdversary = adversaryRequired(task)
|
|
2207
|
+
const adversaryFirst = withAdversary && first === 'adversary'
|
|
2208
|
+
const advStages = []
|
|
2209
|
+
const runCheck = () => launch(checkPrompt(task, impl, c.prevCommit || null, baseBranch, ledger), agentOpts('verify', task, { label: `check:${task.id}:${pass}`, phase: 'Verify', agentType: 'doug-flow:verifier', schema: CHECK_SCHEMA }))
|
|
2210
|
+
const runAdv = () => runAdversaryCrew(task, impl, pass, baseBranch, ledger, launch, advStages)
|
|
2211
|
+
const both = !withAdversary ? [await runCheck()] : await parallel(adversaryFirst ? [runAdv, runCheck] : [runCheck, runAdv])
|
|
2212
|
+
check = (both && both[adversaryFirst ? 1 : 0]) || null
|
|
2213
|
+
stageResults.check = check
|
|
2214
|
+
// The one check stands in for both stages, so the rest of the loop and the report read it as they read them.
|
|
2215
|
+
if (check) ({ ver, rev } = readCheck(check))
|
|
2216
|
+
if (withAdversary) { adv = (both && both[adversaryFirst ? 0 : 1]) || null; stageResults.adversary = adv }
|
|
2217
|
+
stages.push(...(adversaryFirst ? [...advStages, 'check'] : ['check', ...advStages]))
|
|
2218
|
+
const checkBlocked = !(check && check.passed && rev && rev.approve)
|
|
2219
|
+
const advBlocked = withAdversary && adversaryBlocked(adv)
|
|
2220
|
+
blockingStage = adversaryFirst && advBlocked ? 'adversary' : checkBlocked ? 'check' : advBlocked ? 'adversary' : null
|
|
2171
2221
|
}
|
|
2172
2222
|
}
|
|
2173
2223
|
const head = stageHead(ver, check)
|
|
@@ -2338,20 +2388,20 @@ async function runTask(task, baseBranch, resume) {
|
|
|
2338
2388
|
})
|
|
2339
2389
|
// The last supervisor that ran on this task, for the report; null when none did.
|
|
2340
2390
|
const lastSupervisor = () => { for (let i = attempts.length - 1; i >= 0; i--) if (attempts[i].supervisor) return attempts[i].supervisor; return null }
|
|
2341
|
-
if (ready) return { ...r, attempts, ledger, budget: budgetOf(), stopReason: null, supervisor: lastSupervisor() }
|
|
2391
|
+
if (ready) return { ...r, attempts, ledger, budget: budgetOf(), stopReason: null, stopClass: null, supervisor: lastSupervisor() }
|
|
2342
2392
|
const why = notReadyWhy(r)
|
|
2343
|
-
const halt = reason => {
|
|
2393
|
+
const halt = (stopClass, reason) => {
|
|
2344
2394
|
for (const e of ledger) if (e.status === 'open') log(`${task.id} open finding ${e.id} [${e.stage}] ${e.file || '(no file)'}: ${String(e.description).slice(0, 120)}`)
|
|
2345
|
-
return { ...r, attempts, ledger, budget: budgetOf(), stopReason: reason, supervisor: lastSupervisor() }
|
|
2395
|
+
return { ...r, attempts, ledger, budget: budgetOf(), stopReason: reason, stopClass, supervisor: lastSupervisor() }
|
|
2346
2396
|
}
|
|
2347
|
-
if (!decision.ok) return halt(`${why}; not retried: ${decision.reason}`)
|
|
2397
|
+
if (!decision.ok) return halt(decision.stopClass, `${why}; not retried: ${decision.reason}`)
|
|
2348
2398
|
// Two passes in a row that each raise a blocker nobody had seen means the checks are exploring, not converging.
|
|
2349
2399
|
const last = attempts[attempts.length - 1]
|
|
2350
2400
|
const prior = attempts[attempts.length - 2]
|
|
2351
2401
|
if (prior && last.newFindings.length && prior.newFindings.length) {
|
|
2352
|
-
return halt(`${why}; stopped: two consecutive passes raised new blockers (${prior.newFindings.join(', ')}; ${last.newFindings.join(', ')})`)
|
|
2402
|
+
return halt('new-blockers-twice', `${why}; stopped: two consecutive passes raised new blockers (${prior.newFindings.join(', ')}; ${last.newFindings.join(', ')})`)
|
|
2353
2403
|
}
|
|
2354
|
-
if (attempts.length > fixAttempts) return halt(`${why}; fix attempts exhausted (${fixAttempts} of ${fixAttempts})`)
|
|
2404
|
+
if (attempts.length > fixAttempts) return halt('fix-attempts-exhausted', `${why}; fix attempts exhausted (${fixAttempts} of ${fixAttempts})`)
|
|
2355
2405
|
// The supervisor (card fix-loop-supervisor): before a fix pass after the first, when the stall signals fire. A
|
|
2356
2406
|
// pass that returned no result attacked nothing, so it is not a stall. A task stalled a second time stops here
|
|
2357
2407
|
// instead of spending the remaining fix budget; otherwise the supervisor runs after the budget check below, on
|
|
@@ -2362,7 +2412,7 @@ async function runTask(task, baseBranch, resume) {
|
|
|
2362
2412
|
if (signals.length && attempts.some(a => a.supervisor && a.supervisor.stalled)) {
|
|
2363
2413
|
const findings = [...new Set(signals.filter(x => x.kind === 'repeat').map(x => x.finding))]
|
|
2364
2414
|
lastAttempt.supervisor = { ran: false, stalled: true, brief: null, signals, stop: { kind: 'stalled', attempts: attempts.length, findings } }
|
|
2365
|
-
return halt(`${why}; stopped: stalled twice (${describeStall(signals)})`)
|
|
2415
|
+
return halt('stalled', `${why}; stopped: stalled twice (${describeStall(signals)})`)
|
|
2366
2416
|
}
|
|
2367
2417
|
const blocking = r.blockingStage || 'verify'
|
|
2368
2418
|
// What the next pass would cost: the fix agent plus the stages rule 3 would run. The adversary stage spends two
|
|
@@ -2391,7 +2441,7 @@ async function runTask(task, baseBranch, resume) {
|
|
|
2391
2441
|
const projected = spent.elapsedMs + spent.elapsedMs / passesDone
|
|
2392
2442
|
if (projected > limit) over = `elapsedMs ${show(projected)} > ${limit}`
|
|
2393
2443
|
}
|
|
2394
|
-
if (over) return halt(`${why}; stopped: next attempt would exceed the task budget (${over})`)
|
|
2444
|
+
if (over) return halt('budget', `${why}; stopped: next attempt would exceed the task budget (${over})`)
|
|
2395
2445
|
const pass = r.pass + 1
|
|
2396
2446
|
let brief = null
|
|
2397
2447
|
if (signals.length) {
|
|
@@ -2447,7 +2497,7 @@ async function runTask(task, baseBranch, resume) {
|
|
|
2447
2497
|
r = record({ task, impl, ver: null, rev: null, adv: null, check: null, pass, stages: ['fix'], stageResults: {}, blockingStage: null, commit: fix.commit || null }, prevCommit)
|
|
2448
2498
|
const decided = fixBlocked ? retriable(task, r) : null
|
|
2449
2499
|
attempts.push({ pass, stages: ['fix'], commit: r.commit, blockingStage: null, verified: false, reviewed: false, adversary: null, newFindings: r.newFindings || [], fixedFindings: r.fixedFindings || [], notes: r.droppedFindings || [], ready: false, retriable: decided, spent: measure(), commands: passCommands(r) })
|
|
2450
|
-
return
|
|
2500
|
+
return fixBlocked ? halt(decided.stopClass, `${notReadyWhy(r)}; not retried: ${decided.reason}`) : halt('no-new-commit', `${why}; stopped: fix pass ${pass} made no new commit on ${impl.branch}`)
|
|
2451
2501
|
}
|
|
2452
2502
|
const next = { ...fix, branch: r.impl.branch, worktreePath, prevCommit, ...(r.impl.workers ? { workers: r.impl.workers, swarmStages: r.impl.swarmStages, splitReason: r.impl.splitReason, rebrief: r.impl.rebrief, checkFailed: r.impl.checkFailed } : {}) }
|
|
2453
2503
|
r = record(await runChecks(task, next, pass, baseBranch, { ledger, launch, lead: 'fix', prevCommit, blocking }), prevCommit)
|
|
@@ -2507,7 +2557,7 @@ for (let li = 0; li < levels.length; li++) {
|
|
|
2507
2557
|
}
|
|
2508
2558
|
const reason = missing.length > 1 ? `dependencies ${missing.join(', ')} were not integrated` : `dependency ${missing[0]} was not integrated`
|
|
2509
2559
|
log(`${t.id} not launched: ${reason}`)
|
|
2510
|
-
skipped.set(t, { task: t, impl: null, ver: null, rev: null, adv: null, check: null, attempts: [], ledger: [], budget: null, stages: [], stopReason: reason, skippedReason: reason })
|
|
2560
|
+
skipped.set(t, { task: t, impl: null, ver: null, rev: null, adv: null, check: null, attempts: [], ledger: [], budget: null, stages: [], stopReason: reason, stopClass: 'dependency-skipped', skippedReason: reason })
|
|
2511
2561
|
}
|
|
2512
2562
|
|
|
2513
2563
|
// Implement (or reuse) -> verify -> review -> adversary -> fix, per task, no barrier between tasks.
|
|
@@ -2523,7 +2573,7 @@ for (let li = 0; li < levels.length; li++) {
|
|
|
2523
2573
|
// was ever made; blocked:true keeps `implemented` false so reusePlan still refuses to reuse it.
|
|
2524
2574
|
const threwFallback = task => {
|
|
2525
2575
|
const reason = 'task stage threw (agent error, unknown agent type, or user skip)'
|
|
2526
|
-
return { task, impl: { taskId: task.id, branch: task.reuse || `doug/task-${task.id}`, worktreePath: '', filesTouched: [], commandsRun: [], summary: reason, blocked: true, blockedReason: reason, commit: null }, ver: null, rev: null, adv: null, check: null, attempts: [], ledger: [], budget: null, stopReason: reason }
|
|
2576
|
+
return { task, impl: { taskId: task.id, branch: task.reuse || `doug/task-${task.id}`, worktreePath: '', filesTouched: [], commandsRun: [], summary: reason, blocked: true, blockedReason: reason, commit: null }, ver: null, rev: null, adv: null, check: null, attempts: [], ledger: [], budget: null, stopReason: reason, stopClass: 'stage-threw' }
|
|
2527
2577
|
}
|
|
2528
2578
|
const byTask = new Map(runnable.map((task, i) => [task, results[i] || threwFallback(task)]))
|
|
2529
2579
|
const ready = level.map(task => byTask.get(task) || skipped.get(task)).filter(isReady)
|
|
@@ -2537,7 +2587,7 @@ for (let li = 0; li < levels.length; li++) {
|
|
|
2537
2587
|
const levelBase = baseBranch
|
|
2538
2588
|
const sReady = adversary ? ready.filter(r => taskShape(r.task) === 'S') : []
|
|
2539
2589
|
if (ready.length) {
|
|
2540
|
-
integration = enforceIntegrationAcceptance(await retryOnce(integratePrompt(li, ready, baseBranch, li === levels.length - 1, sReady.length > 0, !integratedOnce), agentOpts('integrate', null, { label: `integrate:level-${li}`, phase: 'Integrate', schema: INTEGRATE_SCHEMA }), `level-${li}`), li === levels.length - 1, acceptanceEntries(plan.acceptance || []))
|
|
2590
|
+
integration = enforceIntegrationAcceptance(await retryOnce(integratePrompt(li, ready, baseBranch, li === levels.length - 1, sReady.length > 0, !integratedOnce), agentOpts('integrate', null, { label: `integrate:level-${li}`, phase: 'Integrate', schema: INTEGRATE_SCHEMA }), `level-${li}`), li === levels.length - 1, acceptanceEntries(plan.acceptance || []), plan.verify || [])
|
|
2541
2591
|
integratedOnce = true
|
|
2542
2592
|
if (integration && integration.ok) {
|
|
2543
2593
|
baseBranch = integrationBranch
|
|
@@ -2548,7 +2598,12 @@ for (let li = 0; li < levels.length; li++) {
|
|
|
2548
2598
|
// branch, after the merge. A blocker goes back to the task that owns its file as a fix pass, the fixed branches are
|
|
2549
2599
|
// merged again, and the adversary confirms once; a level still blocked after that stops the run.
|
|
2550
2600
|
if (integration && integration.ok && sReady.length) {
|
|
2551
|
-
|
|
2601
|
+
// Card level-adversary-empty-diff: the sha the integrator read before the level's first merge is the diff base;
|
|
2602
|
+
// the branch name would equal --head on every level >= 1. The re-integration never replaces it.
|
|
2603
|
+
const reportedBase = integration && integration.baseCommit
|
|
2604
|
+
const adversaryBase = typeof reportedBase === 'string' && /^[0-9a-f]{40}$/.test(reportedBase) ? reportedBase : levelBase
|
|
2605
|
+
if (adversaryBase === levelBase) log(`level ${li} level adversary falls back to ${levelBase} because the integrator reported no base commit`)
|
|
2606
|
+
levelAdversary = await runLevelAdversary(li, sReady, adversaryBase, false)
|
|
2552
2607
|
if (levelAdversary.blocked) {
|
|
2553
2608
|
const blockers = levelAdversary.issues.filter(i => i.severity === 'blocker')
|
|
2554
2609
|
const items = blockers.length ? blockers : [{ severity: 'blocker', file: '', description: levelAdversary.summary || 'the level adversary blocked without an issue', evidence: null }]
|
|
@@ -2570,25 +2625,28 @@ for (let li = 0; li < levels.length; li++) {
|
|
|
2570
2625
|
const adv = { ran: true, verdict: 'fail', summary: levelAdversary.summary, issues, commandsRun: levelAdversary.commandsRun, error: null }
|
|
2571
2626
|
const again = await runTask(r.task, levelBase, { impl: r.impl, ver: r.ver, rev: r.rev, adv, commit: r.commit, pass: r.pass, attempts: r.attempts, ledger: r.ledger })
|
|
2572
2627
|
byTask.set(r.task, again)
|
|
2573
|
-
levelAdversary.fixed.push({ task: r.task.id, ready: isReady(again), stopReason: again.stopReason })
|
|
2628
|
+
levelAdversary.fixed.push({ task: r.task.id, ready: isReady(again), stopReason: again.stopReason, stopClass: again.stopClass || null })
|
|
2574
2629
|
}
|
|
2575
2630
|
const fixedReady = [...perTask.keys()].map(r => byTask.get(r.task)).filter(isReady)
|
|
2576
2631
|
if (fixedReady.length === perTask.size) {
|
|
2577
|
-
const again = enforceIntegrationAcceptance(await retryOnce(integratePrompt(li, fixedReady, levelBase, li === levels.length - 1, true, false), agentOpts('integrate', null, { label: `integrate:level-${li}:2`, phase: 'Integrate', schema: INTEGRATE_SCHEMA }), `level-${li}`), li === levels.length - 1, acceptanceEntries(plan.acceptance || []))
|
|
2632
|
+
const again = enforceIntegrationAcceptance(await retryOnce(integratePrompt(li, fixedReady, levelBase, li === levels.length - 1, true, false), agentOpts('integrate', null, { label: `integrate:level-${li}:2`, phase: 'Integrate', schema: INTEGRATE_SCHEMA }), `level-${li}`), li === levels.length - 1, acceptanceEntries(plan.acceptance || []), plan.verify || [])
|
|
2578
2633
|
levelAdversary.reintegration = again
|
|
2579
2634
|
// The re-integration is the final state of the branch, so its acceptance (not the first integration's)
|
|
2580
2635
|
// is what the level report carries (card integration-acceptance-recorded, behaviour 4). Only
|
|
2581
2636
|
// `acceptance` is copied onto the level's `integration`; a failed re-integration's own `ok` and
|
|
2582
2637
|
// `reason` stay on `levelAdversary.reintegration`, not on the level report.
|
|
2583
2638
|
if (integration && li === levels.length - 1 && again && typeof again === 'object') integration.acceptance = again.acceptance
|
|
2584
|
-
if (again && again.ok) levelAdversary.confirm = await runLevelAdversary(li, sReady.map(r => byTask.get(r.task) || r),
|
|
2639
|
+
if (again && again.ok) levelAdversary.confirm = await runLevelAdversary(li, sReady.map(r => byTask.get(r.task) || r), adversaryBase, true)
|
|
2585
2640
|
}
|
|
2586
2641
|
}
|
|
2587
2642
|
const cleared = !!(levelAdversary.confirm && !levelAdversary.confirm.blocked)
|
|
2588
2643
|
if (!cleared) {
|
|
2589
2644
|
for (const r of new Set([...sReady, ...perTask.keys()])) {
|
|
2590
2645
|
const cur = byTask.get(r.task) || r
|
|
2591
|
-
if (!cur.stopReason)
|
|
2646
|
+
if (!cur.stopReason) {
|
|
2647
|
+
cur.stopReason = levelAdversary.unowned ? `level adversary blocked on ${levelAdversary.unowned.join(', ')}: ${levelAdversary.summary}` : levelAdversary.confirm ? `level adversary still blocked after a fix pass: ${levelAdversary.confirm.summary}` : levelAdversary.reintegration ? 'the fixed branches did not integrate again' : `level adversary blocked: ${levelAdversary.summary}`
|
|
2648
|
+
cur.stopClass = 'level-adversary'
|
|
2649
|
+
}
|
|
2592
2650
|
byTask.set(r.task, cur)
|
|
2593
2651
|
}
|
|
2594
2652
|
for (const r of ready) integrated.delete(r.task.id)
|
|
@@ -2642,6 +2700,7 @@ for (let li = 0; li < levels.length; li++) {
|
|
|
2642
2700
|
attempts: r.attempts,
|
|
2643
2701
|
passes: r.attempts.length,
|
|
2644
2702
|
stopReason: r.stopReason,
|
|
2703
|
+
stopClass: r.stopClass || null,
|
|
2645
2704
|
// The fix-loop supervisor's last word on the task: ran, stalled, the brief it injected, and the stop when the
|
|
2646
2705
|
// task stalled twice ({ kind: 'stalled', attempts, findings }); null when the loop never stalled.
|
|
2647
2706
|
supervisor: r.supervisor || null,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "doug-gates",
|
|
3
3
|
"displayName": "Doug Gates",
|
|
4
|
-
"version": "1.
|
|
4
|
+
"version": "1.1.1",
|
|
5
5
|
"description": "Deterministic guardrails for Claude Code: protected paths, bash guard, secret scan on edits and commands, format on edit, edit-loop detection, a verification gate on Stop and on SubagentStop, a snapshot of the plan and session into the anchor before compaction and re-anchoring after it, and a run trace of subagents and tool calls. Configured by .doug/config.json.",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "Doug contributors"
|
|
@@ -110,6 +110,17 @@
|
|
|
110
110
|
]
|
|
111
111
|
}
|
|
112
112
|
],
|
|
113
|
+
"UserPromptSubmit": [
|
|
114
|
+
{
|
|
115
|
+
"hooks": [
|
|
116
|
+
{
|
|
117
|
+
"type": "command",
|
|
118
|
+
"command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/turn-start.mjs\"",
|
|
119
|
+
"timeout": 5
|
|
120
|
+
}
|
|
121
|
+
]
|
|
122
|
+
}
|
|
123
|
+
],
|
|
113
124
|
"SessionStart": [
|
|
114
125
|
{
|
|
115
126
|
"matcher": "compact|resume",
|