agent-templates 0.1.0 → 0.2.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.
Files changed (38) hide show
  1. package/ADOPTING.md +57 -57
  2. package/CLAUDE.md +128 -125
  3. package/LICENSE +21 -21
  4. package/README.md +32 -32
  5. package/package.json +32 -32
  6. package/patterns/three-agent-architect-builder-reviewer/README.md +164 -153
  7. package/patterns/three-agent-architect-builder-reviewer/scaffold/.claude/agents/architect.md +31 -31
  8. package/patterns/three-agent-architect-builder-reviewer/scaffold/.claude/agents/builder.md +25 -25
  9. package/patterns/three-agent-architect-builder-reviewer/scaffold/.claude/agents/reviewer.md +33 -33
  10. package/patterns/three-agent-architect-builder-reviewer/scaffold/.claude/agents/triage.md +31 -31
  11. package/patterns/three-agent-architect-builder-reviewer/scaffold/.claude/commands/breakdown-prd.md +18 -18
  12. package/patterns/three-agent-architect-builder-reviewer/scaffold/.claude/commands/nightly-issues.md +14 -14
  13. package/patterns/three-agent-architect-builder-reviewer/scaffold/.claude/commands/review-ticket.md +14 -14
  14. package/patterns/three-agent-architect-builder-reviewer/scaffold/.claude/commands/start-all.md +17 -0
  15. package/patterns/three-agent-architect-builder-reviewer/scaffold/.claude/commands/start-milestone.md +15 -15
  16. package/patterns/three-agent-architect-builder-reviewer/scaffold/.claude/commands/verify-delivery.md +20 -20
  17. package/patterns/three-agent-architect-builder-reviewer/scaffold/.claude/hooks/guard-main-session-writes.mjs +53 -53
  18. package/patterns/three-agent-architect-builder-reviewer/scaffold/.claude/scripts/milestone-dag.mjs +94 -0
  19. package/patterns/three-agent-architect-builder-reviewer/scaffold/.claude/scripts/publish-tickets.mjs +263 -263
  20. package/patterns/three-agent-architect-builder-reviewer/scaffold/.claude/settings.json +15 -15
  21. package/patterns/three-agent-architect-builder-reviewer/scaffold/.claude/workflows/nightly-issues.js +141 -139
  22. package/patterns/three-agent-architect-builder-reviewer/scaffold/.claude/workflows/run-milestone.js +225 -223
  23. package/patterns/three-agent-architect-builder-reviewer/scaffold/.claude/workflows/start-all.js +120 -0
  24. package/patterns/three-agent-architect-builder-reviewer/scaffold/INSTALL.md +65 -65
  25. package/patterns/three-agent-architect-builder-reviewer/scaffold/claude-md-snippet.md +38 -37
  26. package/scripts/adopt.mjs +207 -177
  27. package/scripts/build-site.mjs +366 -264
  28. package/scripts/cli.mjs +52 -52
  29. package/templates/pattern-README.template.md +65 -65
  30. package/templates/ticket.template.md +104 -104
  31. package/templates/tracker/github/ISSUE_TEMPLATE/bug-report.md +28 -28
  32. package/templates/tracker/github/ISSUE_TEMPLATE/decision-record.md +25 -25
  33. package/templates/tracker/github/ISSUE_TEMPLATE/task.md +36 -36
  34. package/templates/tracker/github/PULL_REQUEST_TEMPLATE.md +54 -54
  35. package/templates/tracker/gitlab/issue_templates/bug-report.md +23 -23
  36. package/templates/tracker/gitlab/issue_templates/decision-record.md +20 -20
  37. package/templates/tracker/gitlab/issue_templates/task.md +31 -31
  38. package/templates/tracker/gitlab/merge_request_templates/default.md +55 -55
@@ -1,223 +1,225 @@
1
- export const meta = {
2
- name: 'run-milestone',
3
- description: 'Autonomous milestone runner: each ticket flows architect -> builder -> reviewer (bounce-capped in code) -> deliver',
4
- }
5
-
6
- // Deterministic orchestrator for the three-agent pattern. Control flow that used to be
7
- // prose ("max 2 bounces", "never merge without CLEAR") is enforced HERE, in code.
8
- //
9
- // args:
10
- // {
11
- // tickets: [{ id: 'FND-01', path: 'docs/prd/01-foo/tickets/FND-01-x.md', issue: 12 }],
12
- // mode: 'supervised' | 'autonomous',
13
- // defaultBranch: 'main', // optional, default 'main'
14
- // maxBounces: 2, // optional, default 2
15
- // continueOnFailure: false, // optional; default fail-fast (tickets may depend on earlier ones)
16
- // platform: 'gh' | 'glab' // tracker CLI for the deliver step, default 'gh'
17
- // }
18
- //
19
- // Guarantees encoded below (each one exists because prose alone failed before):
20
- // - reviewer prompts carry ONLY artifact refs (ticket path, computed plan path, branch name)
21
- // - a reviewer infrastructure failure never consumes the bounce budget or dispatches a fix
22
- // - nothing counts as delivered unless merged AND issueClosed AND dodPassed are all true
23
- // - supervised mode STOPS the run after each CLEAR (later tickets may depend on the merge);
24
- // re-running /start-milestone continues — already-closed issues are filtered out upstream
25
-
26
- const cfg = Object.assign(
27
- { maxBounces: 2, continueOnFailure: false, defaultBranch: 'main', platform: 'gh' },
28
- args
29
- )
30
- if (!Array.isArray(cfg.tickets) || cfg.tickets.length === 0) {
31
- throw new Error('args.tickets must be a non-empty array of {id, path, issue}')
32
- }
33
- for (const t of cfg.tickets) {
34
- if (!t || typeof t.id !== 'string' || !t.id || typeof t.path !== 'string' || !t.path) {
35
- throw new Error('every ticket needs a non-empty string id and path; got: ' + JSON.stringify(t))
36
- }
37
- }
38
- if (cfg.mode !== 'supervised' && cfg.mode !== 'autonomous') {
39
- throw new Error("args.mode must be 'supervised' or 'autonomous'")
40
- }
41
- if (!Number.isInteger(cfg.maxBounces) || cfg.maxBounces < 0) {
42
- throw new Error('args.maxBounces must be an integer >= 0')
43
- }
44
- if (typeof cfg.defaultBranch !== 'string' || !cfg.defaultBranch) throw new Error('args.defaultBranch must be a non-empty string')
45
- if (cfg.platform !== 'gh' && cfg.platform !== 'glab') throw new Error("args.platform must be 'gh' or 'glab'")
46
-
47
- const PLAN = {
48
- type: 'object',
49
- properties: { planPath: { type: 'string' }, summary: { type: 'string' } },
50
- required: ['planPath', 'summary'],
51
- }
52
- const BUILD = {
53
- type: 'object',
54
- properties: {
55
- branch: { type: 'string' },
56
- testsPassed: { type: 'boolean' },
57
- testOutput: { type: 'string' },
58
- deviations: { type: 'string' },
59
- },
60
- required: ['branch', 'testsPassed', 'testOutput'],
61
- }
62
- const VERDICT = {
63
- type: 'object',
64
- properties: {
65
- verdict: { enum: ['CLEAR', 'BOUNCE'] },
66
- checkedNote: { type: 'string' },
67
- findings: {
68
- type: 'array',
69
- items: {
70
- type: 'object',
71
- properties: {
72
- file: { type: 'string' },
73
- line: { type: 'integer' },
74
- scenario: { type: 'string' },
75
- severity: { enum: ['blocker', 'major', 'minor'] },
76
- },
77
- required: ['file', 'scenario', 'severity'],
78
- },
79
- },
80
- },
81
- required: ['verdict'],
82
- }
83
- const DELIVERY = {
84
- type: 'object',
85
- properties: {
86
- merged: { type: 'boolean' },
87
- issueClosed: { type: 'boolean' },
88
- dodPassed: { type: 'boolean' },
89
- notes: { type: 'string' },
90
- },
91
- required: ['merged', 'issueClosed', 'dodPassed'],
92
- }
93
-
94
- const normalizePath = function (p) {
95
- return String(p || '').replace(/\\/g, '/').replace(/^\.\//, '').trim()
96
- }
97
-
98
- const results = []
99
- let stopRun = false
100
-
101
- for (const t of cfg.tickets) {
102
- if (stopRun) break
103
- const P = 'T:' + t.id
104
- const branch = 'ticket/' + t.id
105
- const planPath = 'docs/plans/' + t.id + '.md' // computed in code; agent-returned paths are verified, never trusted
106
-
107
- log('[' + t.id + '] architect: planning')
108
- const plan = await agent(
109
- 'You are running as the Architect stage of the three-agent pattern. Ticket file: ' + t.path +
110
- '. Produce the implementation plan per your role definition and write it to EXACTLY ' + planPath +
111
- '. Return planPath (must be ' + planPath + ') and a one-paragraph summary.',
112
- { agentType: 'architect', label: 'plan:' + t.id, phase: P, schema: PLAN }
113
- )
114
- if (!plan || normalizePath(plan.planPath) !== planPath) {
115
- results.push({ id: t.id, status: 'failed', stage: 'architect', detail: plan ? 'plan written to unexpected path: ' + plan.planPath : 'architect agent returned nothing' })
116
- if (!cfg.continueOnFailure) break
117
- continue
118
- }
119
-
120
- log('[' + t.id + '] builder: implementing on ' + branch)
121
- let build = await agent(
122
- 'Builder stage. Ticket: ' + t.path + '. Plan: ' + planPath + '. Create branch ' + branch +
123
- ' from ' + cfg.defaultBranch + ', implement the plan there, commit, run the tests. Do NOT merge and do NOT touch the tracker. ' +
124
- 'Return branch (must be ' + branch + '), testsPassed, testOutput (paste real output), deviations.',
125
- { agentType: 'builder', label: 'build:' + t.id, phase: P, schema: BUILD }
126
- )
127
- const buildBad = function (b) { return !b || !b.testsPassed || String(b.branch).trim() !== branch }
128
- if (buildBad(build)) {
129
- results.push({
130
- id: t.id, status: 'failed', stage: 'builder',
131
- detail: !build ? 'builder agent returned nothing' : (String(build.branch).trim() !== branch ? 'worked on wrong branch: ' + build.branch : build.testOutput),
132
- })
133
- if (!cfg.continueOnFailure) break
134
- continue
135
- }
136
-
137
- // Review loop — the bounce cap lives here, in code, not in prose.
138
- const reviewOnce = function (tag) {
139
- return agent(
140
- 'Reviewer stage. Inputs (artifact refs only): ticket ' + t.path + ', plan ' + planPath +
141
- ', diff = branch ' + branch + ' vs ' + cfg.defaultBranch + '. Review per your role definition; ' +
142
- 'run the tests yourself no test results are provided on purpose. Return verdict CLEAR or BOUNCE with findings ' +
143
- '(a BOUNCE with zero findings is invalid).',
144
- { agentType: 'reviewer', label: 'review:' + t.id + '#' + tag, phase: P, schema: VERDICT }
145
- )
146
- }
147
- // A null/invalid reviewer result is an infrastructure failure, not a code finding:
148
- // retry once (not counted against the bounce budget), then escalate — never sent to the builder.
149
- const reviewValid = function (v) {
150
- if (!v) return false
151
- if (v.verdict === 'BOUNCE' && (!v.findings || v.findings.length === 0)) return false
152
- return true
153
- }
154
-
155
- let bounces = 0
156
- let verdict = await reviewOnce('0')
157
- if (!reviewValid(verdict)) { log('[' + t.id + '] reviewer returned no usable verdict — retrying once'); verdict = await reviewOnce('0-retry') }
158
- let reviewerBroken = !reviewValid(verdict)
159
- let fixBroken = false
160
-
161
- while (!reviewerBroken && verdict.verdict === 'BOUNCE' && bounces < cfg.maxBounces) {
162
- bounces += 1
163
- log('[' + t.id + '] bounce ' + bounces + '/' + cfg.maxBounces + ': back to builder with ' + verdict.findings.length + ' finding(s)')
164
- build = await agent(
165
- 'Builder stage, bounce fix. Ticket: ' + t.path + '. Plan: ' + planPath + '. Stay on branch ' + branch +
166
- ' do NOT merge and do NOT touch the tracker. Reviewer findings — address ALL of them and add regression tests: ' +
167
- JSON.stringify(verdict.findings) + '. Run the tests. Return branch (must be ' + branch + '), testsPassed, testOutput, deviations.',
168
- { agentType: 'builder', label: 'fix:' + t.id + '#' + bounces, phase: P, schema: BUILD }
169
- )
170
- if (buildBad(build)) { fixBroken = true; break }
171
- verdict = await reviewOnce(String(bounces))
172
- if (!reviewValid(verdict)) { log('[' + t.id + '] reviewer returned no usable verdict — retrying once'); verdict = await reviewOnce(bounces + '-retry') }
173
- reviewerBroken = !reviewValid(verdict)
174
- }
175
-
176
- if (reviewerBroken || fixBroken || verdict.verdict !== 'CLEAR') {
177
- const stage = reviewerBroken ? 'reviewer-failed' : (fixBroken ? 'bounce-fix-build' : 'review')
178
- results.push({
179
- id: t.id, status: 'escalated', stage: stage, bounces: bounces,
180
- findings: reviewValid(verdict) ? (verdict.findings || []) : [],
181
- detail: fixBroken ? (!build ? 'fix builder returned nothing' : (String(build.branch).trim() !== branch ? 'fix worked on wrong branch: ' + build.branch : build.testOutput)) : (reviewerBroken ? 'reviewer produced no usable verdict after one retry' : 'bounce cap exhausted'),
182
- })
183
- log('[' + t.id + '] escalated to a human (stage: ' + stage + ', after ' + bounces + ' bounce(s))')
184
- if (!cfg.continueOnFailure) break
185
- continue
186
- }
187
-
188
- if (cfg.mode === 'supervised') {
189
- results.push({ id: t.id, status: 'awaiting-human-merge', branch: branch, bounces: bounces, note: verdict.checkedNote || '' })
190
- log('[' + t.id + '] CLEAR — supervised mode: stopping the run for the human merge. Re-run /start-milestone after merging (closed issues are skipped).')
191
- stopRun = true
192
- continue
193
- }
194
-
195
- log('[' + t.id + '] deliver: merge + close issue + DoD')
196
- const delivery = await agent(
197
- 'Delivery step (autonomous mode pre-authorized by the human Gate 1 start signal). ' +
198
- 'Merge branch ' + branch + ' into ' + cfg.defaultBranch + ' with --no-ff and push. ' +
199
- 'Close tracker issue ' + (t.issue ? '#' + t.issue : 'for ticket ' + t.id + ' (find it by its "[' + t.id + ']" title prefix)') +
200
- ' via ' + cfg.platform + ' and verify it is ACTUALLY closed afterwards. ' +
201
- 'Then run the Definition-of-Done checks from .claude/commands/verify-delivery.md for ticket ' + t.id +
202
- ' (plan exists; tests green on the merged ' + cfg.defaultBranch + ' run them yourself; CLEAR verdict recorded; merged to ' + cfg.defaultBranch + '; issue closed; writeback done). ' +
203
- 'Never report an item you did not check. Return merged, issueClosed, dodPassed, notes.',
204
- { label: 'deliver:' + t.id, phase: P, schema: DELIVERY }
205
- )
206
- // Delivered requires ALL THREE flags a hallucinated dodPassed alone must not count.
207
- if (!delivery || !(delivery.merged && delivery.issueClosed && delivery.dodPassed)) {
208
- const missing = !delivery ? 'delivery agent returned nothing' : ['merged', 'issueClosed', 'dodPassed'].filter(function (k) { return !delivery[k] }).join(', ') + ' = false'
209
- results.push({ id: t.id, status: 'delivery-incomplete', detail: missing + (delivery && delivery.notes ? ' — ' + delivery.notes : '') })
210
- if (!cfg.continueOnFailure) break
211
- continue
212
- }
213
- results.push({ id: t.id, status: 'delivered', bounces: bounces })
214
- }
215
-
216
- const throughPipeline = results.filter(function (r) { return r.status === 'delivered' || r.status === 'awaiting-human-merge' }).length
217
- log('milestone run finished: ' + throughPipeline + '/' + cfg.tickets.length + ' tickets through the pipeline')
218
-
219
- return {
220
- mode: cfg.mode,
221
- results: results,
222
- notStarted: cfg.tickets.length - results.length, // > 0 means the run stopped early (fail-fast or supervised pause)
223
- }
1
+ export const meta = {
2
+ name: 'run-milestone',
3
+ description: 'Autonomous milestone runner: each ticket flows architect -> builder -> reviewer (bounce-capped in code) -> deliver',
4
+ }
5
+
6
+ // Deterministic orchestrator for the three-agent pattern. Control flow that used to be
7
+ // prose ("max 2 bounces", "never merge without CLEAR") is enforced HERE, in code.
8
+ //
9
+ // args:
10
+ // {
11
+ // tickets: [{ id: 'FND-01', path: 'docs/prd/01-foo/tickets/FND-01-x.md', issue: 12 }],
12
+ // mode: 'supervised' | 'autonomous',
13
+ // defaultBranch: 'main', // optional, default 'main'
14
+ // maxBounces: 2, // optional, default 2
15
+ // continueOnFailure: false, // optional; default fail-fast (tickets may depend on earlier ones)
16
+ // platform: 'gh' | 'glab' // tracker CLI for the deliver step, default 'gh'
17
+ // }
18
+ //
19
+ // Guarantees encoded below (each one exists because prose alone failed before):
20
+ // - reviewer prompts carry ONLY artifact refs (ticket path, computed plan path, branch name)
21
+ // - a reviewer infrastructure failure never consumes the bounce budget or dispatches a fix
22
+ // - nothing counts as delivered unless merged AND issueClosed AND dodPassed are all true
23
+ // - supervised mode STOPS the run after each CLEAR (later tickets may depend on the merge);
24
+ // re-running /start-milestone continues — already-closed issues are filtered out upstream
25
+
26
+ // args may arrive as a JSON string depending on the harness (catalog issue #23)
27
+ const parsedArgs = typeof args === 'string' ? JSON.parse(args) : (args || {})
28
+ const cfg = Object.assign(
29
+ { maxBounces: 2, continueOnFailure: false, defaultBranch: 'main', platform: 'gh' },
30
+ parsedArgs
31
+ )
32
+ if (!Array.isArray(cfg.tickets) || cfg.tickets.length === 0) {
33
+ throw new Error('args.tickets must be a non-empty array of {id, path, issue}')
34
+ }
35
+ for (const t of cfg.tickets) {
36
+ if (!t || typeof t.id !== 'string' || !t.id || typeof t.path !== 'string' || !t.path) {
37
+ throw new Error('every ticket needs a non-empty string id and path; got: ' + JSON.stringify(t))
38
+ }
39
+ }
40
+ if (cfg.mode !== 'supervised' && cfg.mode !== 'autonomous') {
41
+ throw new Error("args.mode must be 'supervised' or 'autonomous'")
42
+ }
43
+ if (!Number.isInteger(cfg.maxBounces) || cfg.maxBounces < 0) {
44
+ throw new Error('args.maxBounces must be an integer >= 0')
45
+ }
46
+ if (typeof cfg.defaultBranch !== 'string' || !cfg.defaultBranch) throw new Error('args.defaultBranch must be a non-empty string')
47
+ if (cfg.platform !== 'gh' && cfg.platform !== 'glab') throw new Error("args.platform must be 'gh' or 'glab'")
48
+
49
+ const PLAN = {
50
+ type: 'object',
51
+ properties: { planPath: { type: 'string' }, summary: { type: 'string' } },
52
+ required: ['planPath', 'summary'],
53
+ }
54
+ const BUILD = {
55
+ type: 'object',
56
+ properties: {
57
+ branch: { type: 'string' },
58
+ testsPassed: { type: 'boolean' },
59
+ testOutput: { type: 'string' },
60
+ deviations: { type: 'string' },
61
+ },
62
+ required: ['branch', 'testsPassed', 'testOutput'],
63
+ }
64
+ const VERDICT = {
65
+ type: 'object',
66
+ properties: {
67
+ verdict: { enum: ['CLEAR', 'BOUNCE'] },
68
+ checkedNote: { type: 'string' },
69
+ findings: {
70
+ type: 'array',
71
+ items: {
72
+ type: 'object',
73
+ properties: {
74
+ file: { type: 'string' },
75
+ line: { type: 'integer' },
76
+ scenario: { type: 'string' },
77
+ severity: { enum: ['blocker', 'major', 'minor'] },
78
+ },
79
+ required: ['file', 'scenario', 'severity'],
80
+ },
81
+ },
82
+ },
83
+ required: ['verdict'],
84
+ }
85
+ const DELIVERY = {
86
+ type: 'object',
87
+ properties: {
88
+ merged: { type: 'boolean' },
89
+ issueClosed: { type: 'boolean' },
90
+ dodPassed: { type: 'boolean' },
91
+ notes: { type: 'string' },
92
+ },
93
+ required: ['merged', 'issueClosed', 'dodPassed'],
94
+ }
95
+
96
+ const normalizePath = function (p) {
97
+ return String(p || '').replace(/\\/g, '/').replace(/^\.\//, '').trim()
98
+ }
99
+
100
+ const results = []
101
+ let stopRun = false
102
+
103
+ for (const t of cfg.tickets) {
104
+ if (stopRun) break
105
+ const P = 'T:' + t.id
106
+ const branch = 'ticket/' + t.id
107
+ const planPath = 'docs/plans/' + t.id + '.md' // computed in code; agent-returned paths are verified, never trusted
108
+
109
+ log('[' + t.id + '] architect: planning')
110
+ const plan = await agent(
111
+ 'You are running as the Architect stage of the three-agent pattern. Ticket file: ' + t.path +
112
+ '. Produce the implementation plan per your role definition and write it to EXACTLY ' + planPath +
113
+ '. Return planPath (must be ' + planPath + ') and a one-paragraph summary.',
114
+ { agentType: 'architect', label: 'plan:' + t.id, phase: P, schema: PLAN }
115
+ )
116
+ if (!plan || normalizePath(plan.planPath) !== planPath) {
117
+ results.push({ id: t.id, status: 'failed', stage: 'architect', detail: plan ? 'plan written to unexpected path: ' + plan.planPath : 'architect agent returned nothing' })
118
+ if (!cfg.continueOnFailure) break
119
+ continue
120
+ }
121
+
122
+ log('[' + t.id + '] builder: implementing on ' + branch)
123
+ let build = await agent(
124
+ 'Builder stage. Ticket: ' + t.path + '. Plan: ' + planPath + '. Create branch ' + branch +
125
+ ' from ' + cfg.defaultBranch + ', implement the plan there, commit, run the tests. Do NOT merge and do NOT touch the tracker. ' +
126
+ 'Return branch (must be ' + branch + '), testsPassed, testOutput (paste real output), deviations.',
127
+ { agentType: 'builder', label: 'build:' + t.id, phase: P, schema: BUILD }
128
+ )
129
+ const buildBad = function (b) { return !b || !b.testsPassed || String(b.branch).trim() !== branch }
130
+ if (buildBad(build)) {
131
+ results.push({
132
+ id: t.id, status: 'failed', stage: 'builder',
133
+ detail: !build ? 'builder agent returned nothing' : (String(build.branch).trim() !== branch ? 'worked on wrong branch: ' + build.branch : build.testOutput),
134
+ })
135
+ if (!cfg.continueOnFailure) break
136
+ continue
137
+ }
138
+
139
+ // Review loop — the bounce cap lives here, in code, not in prose.
140
+ const reviewOnce = function (tag) {
141
+ return agent(
142
+ 'Reviewer stage. Inputs (artifact refs only): ticket ' + t.path + ', plan ' + planPath +
143
+ ', diff = branch ' + branch + ' vs ' + cfg.defaultBranch + '. Review per your role definition; ' +
144
+ 'run the tests yourself no test results are provided on purpose. Return verdict CLEAR or BOUNCE with findings ' +
145
+ '(a BOUNCE with zero findings is invalid).',
146
+ { agentType: 'reviewer', label: 'review:' + t.id + '#' + tag, phase: P, schema: VERDICT }
147
+ )
148
+ }
149
+ // A null/invalid reviewer result is an infrastructure failure, not a code finding:
150
+ // retry once (not counted against the bounce budget), then escalate — never sent to the builder.
151
+ const reviewValid = function (v) {
152
+ if (!v) return false
153
+ if (v.verdict === 'BOUNCE' && (!v.findings || v.findings.length === 0)) return false
154
+ return true
155
+ }
156
+
157
+ let bounces = 0
158
+ let verdict = await reviewOnce('0')
159
+ if (!reviewValid(verdict)) { log('[' + t.id + '] reviewer returned no usable verdict — retrying once'); verdict = await reviewOnce('0-retry') }
160
+ let reviewerBroken = !reviewValid(verdict)
161
+ let fixBroken = false
162
+
163
+ while (!reviewerBroken && verdict.verdict === 'BOUNCE' && bounces < cfg.maxBounces) {
164
+ bounces += 1
165
+ log('[' + t.id + '] bounce ' + bounces + '/' + cfg.maxBounces + ': back to builder with ' + verdict.findings.length + ' finding(s)')
166
+ build = await agent(
167
+ 'Builder stage, bounce fix. Ticket: ' + t.path + '. Plan: ' + planPath + '. Stay on branch ' + branch +
168
+ ' do NOT merge and do NOT touch the tracker. Reviewer findings address ALL of them and add regression tests: ' +
169
+ JSON.stringify(verdict.findings) + '. Run the tests. Return branch (must be ' + branch + '), testsPassed, testOutput, deviations.',
170
+ { agentType: 'builder', label: 'fix:' + t.id + '#' + bounces, phase: P, schema: BUILD }
171
+ )
172
+ if (buildBad(build)) { fixBroken = true; break }
173
+ verdict = await reviewOnce(String(bounces))
174
+ if (!reviewValid(verdict)) { log('[' + t.id + '] reviewer returned no usable verdict — retrying once'); verdict = await reviewOnce(bounces + '-retry') }
175
+ reviewerBroken = !reviewValid(verdict)
176
+ }
177
+
178
+ if (reviewerBroken || fixBroken || verdict.verdict !== 'CLEAR') {
179
+ const stage = reviewerBroken ? 'reviewer-failed' : (fixBroken ? 'bounce-fix-build' : 'review')
180
+ results.push({
181
+ id: t.id, status: 'escalated', stage: stage, bounces: bounces,
182
+ findings: reviewValid(verdict) ? (verdict.findings || []) : [],
183
+ detail: fixBroken ? (!build ? 'fix builder returned nothing' : (String(build.branch).trim() !== branch ? 'fix worked on wrong branch: ' + build.branch : build.testOutput)) : (reviewerBroken ? 'reviewer produced no usable verdict after one retry' : 'bounce cap exhausted'),
184
+ })
185
+ log('[' + t.id + '] escalated to a human (stage: ' + stage + ', after ' + bounces + ' bounce(s))')
186
+ if (!cfg.continueOnFailure) break
187
+ continue
188
+ }
189
+
190
+ if (cfg.mode === 'supervised') {
191
+ results.push({ id: t.id, status: 'awaiting-human-merge', branch: branch, bounces: bounces, note: verdict.checkedNote || '' })
192
+ log('[' + t.id + '] CLEAR — supervised mode: stopping the run for the human merge. Re-run /start-milestone after merging (closed issues are skipped).')
193
+ stopRun = true
194
+ continue
195
+ }
196
+
197
+ log('[' + t.id + '] deliver: merge + close issue + DoD')
198
+ const delivery = await agent(
199
+ 'Delivery step (autonomous mode pre-authorized by the human Gate 1 start signal). ' +
200
+ 'Merge branch ' + branch + ' into ' + cfg.defaultBranch + ' with --no-ff and push. ' +
201
+ 'Close tracker issue ' + (t.issue ? '#' + t.issue : 'for ticket ' + t.id + ' (find it by its "[' + t.id + ']" title prefix)') +
202
+ ' via ' + cfg.platform + ' and verify it is ACTUALLY closed afterwards. ' +
203
+ 'Then run the Definition-of-Done checks from .claude/commands/verify-delivery.md for ticket ' + t.id +
204
+ ' (plan exists; tests green on the merged ' + cfg.defaultBranch + ' — run them yourself; CLEAR verdict recorded; merged to ' + cfg.defaultBranch + '; issue closed; writeback done). ' +
205
+ 'Never report an item you did not check. Return merged, issueClosed, dodPassed, notes.',
206
+ { label: 'deliver:' + t.id, phase: P, schema: DELIVERY }
207
+ )
208
+ // Delivered requires ALL THREE flags a hallucinated dodPassed alone must not count.
209
+ if (!delivery || !(delivery.merged && delivery.issueClosed && delivery.dodPassed)) {
210
+ const missing = !delivery ? 'delivery agent returned nothing' : ['merged', 'issueClosed', 'dodPassed'].filter(function (k) { return !delivery[k] }).join(', ') + ' = false'
211
+ results.push({ id: t.id, status: 'delivery-incomplete', detail: missing + (delivery && delivery.notes ? ' — ' + delivery.notes : '') })
212
+ if (!cfg.continueOnFailure) break
213
+ continue
214
+ }
215
+ results.push({ id: t.id, status: 'delivered', bounces: bounces })
216
+ }
217
+
218
+ const throughPipeline = results.filter(function (r) { return r.status === 'delivered' || r.status === 'awaiting-human-merge' }).length
219
+ log('milestone run finished: ' + throughPipeline + '/' + cfg.tickets.length + ' tickets through the pipeline')
220
+
221
+ return {
222
+ mode: cfg.mode,
223
+ results: results,
224
+ notStarted: cfg.tickets.length - results.length, // > 0 means the run stopped early (fail-fast or supervised pause)
225
+ }
@@ -0,0 +1,120 @@
1
+ export const meta = {
2
+ name: 'start-all',
3
+ description: 'Whole-PRD driver: runs every module through the milestone pipeline in DAG order, composing run-milestone per module',
4
+ }
5
+
6
+ // Module-level driver above run-milestone (which is unchanged: per ticket it still runs
7
+ // architect plan -> builder execute -> fresh-context reviewer, bounce-capped in code).
8
+ //
9
+ // args:
10
+ // {
11
+ // modules: [{ name: '01-foundation', dependsOn: ['00-x'], tickets: [{id,path,issue}] }],
12
+ // // in DAG order (from milestone-dag.mjs); tickets pre-filtered of closed issues,
13
+ // // so an [] tickets list means "module already complete" (resume semantics)
14
+ // mode: 'supervised' | 'autonomous',
15
+ // defaultBranch: 'main', // optional
16
+ // platform: 'gh' | 'glab' // optional, default 'gh'
17
+ // }
18
+ //
19
+ // Failure policy (maintainer decisions, catalog issue #20, 2026-07-18):
20
+ // - a failed module blocks its dependents;
21
+ // - autonomous: independent DAG branches CONTINUE past a failure;
22
+ // - supervised: ANY pause or failure stops the whole run (re-run to resume — closed
23
+ // issues are filtered upstream, so completed modules arrive as already-complete);
24
+ // - no cost ceiling.
25
+
26
+ // args may arrive as a JSON string depending on the harness (catalog issue #23)
27
+ const parsedArgs = typeof args === 'string' ? JSON.parse(args) : (args || {})
28
+ const cfg = Object.assign({ defaultBranch: 'main', platform: 'gh' }, parsedArgs)
29
+ if (!Array.isArray(cfg.modules) || cfg.modules.length === 0) {
30
+ throw new Error('args.modules must be a non-empty array of {name, dependsOn, tickets}')
31
+ }
32
+ for (const m of cfg.modules) {
33
+ if (!m || typeof m.name !== 'string' || !m.name || !Array.isArray(m.tickets) || !Array.isArray(m.dependsOn || [])) {
34
+ throw new Error('every module needs a name, a tickets array, and a dependsOn array; got: ' + JSON.stringify(m))
35
+ }
36
+ }
37
+ if (cfg.mode !== 'supervised' && cfg.mode !== 'autonomous') {
38
+ throw new Error("args.mode must be 'supervised' or 'autonomous'")
39
+ }
40
+ if (cfg.platform !== 'gh' && cfg.platform !== 'glab') throw new Error("args.platform must be 'gh' or 'glab'")
41
+
42
+ const state = {} // name -> 'completed' | 'failed' | 'skipped' | 'paused'
43
+ const results = []
44
+ let stopped = false
45
+
46
+ for (const m of cfg.modules) {
47
+ const deps = m.dependsOn || []
48
+ if (stopped) {
49
+ results.push({ name: m.name, state: 'not-started', detail: 'run stopped before this module' })
50
+ continue
51
+ }
52
+ for (const d of deps) {
53
+ if (!(d in state)) throw new Error('module order violates the DAG: ' + m.name + ' scheduled before its dependency ' + d)
54
+ }
55
+ const badDep = deps.find(function (d) { return state[d] !== 'completed' })
56
+ if (badDep) {
57
+ state[m.name] = 'skipped'
58
+ results.push({ name: m.name, state: 'skipped-dependency', detail: 'dependency ' + badDep + ' did not complete' })
59
+ log('module ' + m.name + ': skipped (dependency ' + badDep + ' did not complete)')
60
+ continue
61
+ }
62
+ if (m.tickets.length === 0) {
63
+ state[m.name] = 'completed'
64
+ results.push({ name: m.name, state: 'already-complete', detail: 'all tickets already closed (resume)' })
65
+ log('module ' + m.name + ': already complete')
66
+ continue
67
+ }
68
+
69
+ log('module ' + m.name + ': running ' + m.tickets.length + ' ticket(s) through run-milestone')
70
+ let child = null
71
+ let err = null
72
+ try {
73
+ child = await workflow('run-milestone', {
74
+ tickets: m.tickets,
75
+ mode: cfg.mode,
76
+ defaultBranch: cfg.defaultBranch,
77
+ platform: cfg.platform,
78
+ })
79
+ } catch (e) {
80
+ err = e && e.message ? e.message : String(e)
81
+ }
82
+ const rs = (child && child.results) || []
83
+
84
+ if (cfg.mode === 'supervised') {
85
+ // supervised run-milestone stops after each CLEAR; a module "completes" only via
86
+ // resume (its issues close, its ticket list arrives empty next run)
87
+ const paused = !err && rs.some(function (r) { return r.status === 'awaiting-human-merge' })
88
+ state[m.name] = 'paused'
89
+ results.push({
90
+ name: m.name,
91
+ state: paused ? 'paused-for-merge' : 'failed',
92
+ detail: err || (paused ? 'CLEAR reached — merge it, then re-run /start-all to continue' : 'module stopped without reaching a CLEAR'),
93
+ ticketResults: rs,
94
+ })
95
+ stopped = true
96
+ continue
97
+ }
98
+
99
+ const allDelivered = !err && rs.length === m.tickets.length && rs.every(function (r) { return r.status === 'delivered' })
100
+ if (allDelivered) {
101
+ state[m.name] = 'completed'
102
+ results.push({ name: m.name, state: 'completed', detail: rs.length + ' ticket(s) delivered', ticketResults: rs })
103
+ log('module ' + m.name + ': completed')
104
+ } else {
105
+ state[m.name] = 'failed'
106
+ const firstBad = rs.find(function (r) { return r.status !== 'delivered' })
107
+ results.push({
108
+ name: m.name,
109
+ state: 'failed',
110
+ detail: err || (firstBad ? firstBad.id + ': ' + firstBad.status + (firstBad.stage ? ' (' + firstBad.stage + ')' : '') : 'incomplete results'),
111
+ ticketResults: rs,
112
+ })
113
+ log('module ' + m.name + ': FAILED — dependents will be skipped; independent branches continue')
114
+ }
115
+ }
116
+
117
+ const done = results.filter(function (r) { return r.state === 'completed' || r.state === 'already-complete' }).length
118
+ log('start-all finished: ' + done + '/' + cfg.modules.length + ' modules complete')
119
+
120
+ return { mode: cfg.mode, results: results, stoppedEarly: stopped }