agent-templates 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ADOPTING.md +57 -57
- package/CLAUDE.md +128 -125
- package/LICENSE +21 -21
- package/README.md +32 -32
- package/package.json +32 -32
- package/patterns/three-agent-architect-builder-reviewer/README.md +161 -153
- package/patterns/three-agent-architect-builder-reviewer/scaffold/.claude/agents/architect.md +31 -31
- package/patterns/three-agent-architect-builder-reviewer/scaffold/.claude/agents/builder.md +25 -25
- package/patterns/three-agent-architect-builder-reviewer/scaffold/.claude/agents/reviewer.md +33 -33
- package/patterns/three-agent-architect-builder-reviewer/scaffold/.claude/agents/triage.md +31 -31
- package/patterns/three-agent-architect-builder-reviewer/scaffold/.claude/commands/breakdown-prd.md +18 -18
- package/patterns/three-agent-architect-builder-reviewer/scaffold/.claude/commands/nightly-issues.md +14 -14
- package/patterns/three-agent-architect-builder-reviewer/scaffold/.claude/commands/review-ticket.md +14 -14
- package/patterns/three-agent-architect-builder-reviewer/scaffold/.claude/commands/start-all.md +17 -0
- package/patterns/three-agent-architect-builder-reviewer/scaffold/.claude/commands/start-milestone.md +15 -15
- package/patterns/three-agent-architect-builder-reviewer/scaffold/.claude/commands/verify-delivery.md +20 -20
- package/patterns/three-agent-architect-builder-reviewer/scaffold/.claude/hooks/guard-main-session-writes.mjs +53 -53
- package/patterns/three-agent-architect-builder-reviewer/scaffold/.claude/scripts/milestone-dag.mjs +94 -0
- package/patterns/three-agent-architect-builder-reviewer/scaffold/.claude/scripts/publish-tickets.mjs +263 -263
- package/patterns/three-agent-architect-builder-reviewer/scaffold/.claude/settings.json +15 -15
- package/patterns/three-agent-architect-builder-reviewer/scaffold/.claude/workflows/nightly-issues.js +139 -139
- package/patterns/three-agent-architect-builder-reviewer/scaffold/.claude/workflows/run-milestone.js +223 -223
- package/patterns/three-agent-architect-builder-reviewer/scaffold/.claude/workflows/start-all.js +118 -0
- package/patterns/three-agent-architect-builder-reviewer/scaffold/INSTALL.md +65 -65
- package/patterns/three-agent-architect-builder-reviewer/scaffold/claude-md-snippet.md +38 -37
- package/scripts/adopt.mjs +187 -177
- package/scripts/build-site.mjs +264 -264
- package/scripts/cli.mjs +52 -52
- package/templates/pattern-README.template.md +65 -65
- package/templates/ticket.template.md +104 -104
- package/templates/tracker/github/ISSUE_TEMPLATE/bug-report.md +28 -28
- package/templates/tracker/github/ISSUE_TEMPLATE/decision-record.md +25 -25
- package/templates/tracker/github/ISSUE_TEMPLATE/task.md +36 -36
- package/templates/tracker/github/PULL_REQUEST_TEMPLATE.md +54 -54
- package/templates/tracker/gitlab/issue_templates/bug-report.md +23 -23
- package/templates/tracker/gitlab/issue_templates/decision-record.md +20 -20
- package/templates/tracker/gitlab/issue_templates/task.md +31 -31
- package/templates/tracker/gitlab/merge_request_templates/default.md +55 -55
package/patterns/three-agent-architect-builder-reviewer/scaffold/.claude/workflows/run-milestone.js
CHANGED
|
@@ -1,223 +1,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
|
-
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
|
+
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
|
+
}
|
package/patterns/three-agent-architect-builder-reviewer/scaffold/.claude/workflows/start-all.js
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
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
|
+
const cfg = Object.assign({ defaultBranch: 'main', platform: 'gh' }, args)
|
|
27
|
+
if (!Array.isArray(cfg.modules) || cfg.modules.length === 0) {
|
|
28
|
+
throw new Error('args.modules must be a non-empty array of {name, dependsOn, tickets}')
|
|
29
|
+
}
|
|
30
|
+
for (const m of cfg.modules) {
|
|
31
|
+
if (!m || typeof m.name !== 'string' || !m.name || !Array.isArray(m.tickets) || !Array.isArray(m.dependsOn || [])) {
|
|
32
|
+
throw new Error('every module needs a name, a tickets array, and a dependsOn array; got: ' + JSON.stringify(m))
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
if (cfg.mode !== 'supervised' && cfg.mode !== 'autonomous') {
|
|
36
|
+
throw new Error("args.mode must be 'supervised' or 'autonomous'")
|
|
37
|
+
}
|
|
38
|
+
if (cfg.platform !== 'gh' && cfg.platform !== 'glab') throw new Error("args.platform must be 'gh' or 'glab'")
|
|
39
|
+
|
|
40
|
+
const state = {} // name -> 'completed' | 'failed' | 'skipped' | 'paused'
|
|
41
|
+
const results = []
|
|
42
|
+
let stopped = false
|
|
43
|
+
|
|
44
|
+
for (const m of cfg.modules) {
|
|
45
|
+
const deps = m.dependsOn || []
|
|
46
|
+
if (stopped) {
|
|
47
|
+
results.push({ name: m.name, state: 'not-started', detail: 'run stopped before this module' })
|
|
48
|
+
continue
|
|
49
|
+
}
|
|
50
|
+
for (const d of deps) {
|
|
51
|
+
if (!(d in state)) throw new Error('module order violates the DAG: ' + m.name + ' scheduled before its dependency ' + d)
|
|
52
|
+
}
|
|
53
|
+
const badDep = deps.find(function (d) { return state[d] !== 'completed' })
|
|
54
|
+
if (badDep) {
|
|
55
|
+
state[m.name] = 'skipped'
|
|
56
|
+
results.push({ name: m.name, state: 'skipped-dependency', detail: 'dependency ' + badDep + ' did not complete' })
|
|
57
|
+
log('module ' + m.name + ': skipped (dependency ' + badDep + ' did not complete)')
|
|
58
|
+
continue
|
|
59
|
+
}
|
|
60
|
+
if (m.tickets.length === 0) {
|
|
61
|
+
state[m.name] = 'completed'
|
|
62
|
+
results.push({ name: m.name, state: 'already-complete', detail: 'all tickets already closed (resume)' })
|
|
63
|
+
log('module ' + m.name + ': already complete')
|
|
64
|
+
continue
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
log('module ' + m.name + ': running ' + m.tickets.length + ' ticket(s) through run-milestone')
|
|
68
|
+
let child = null
|
|
69
|
+
let err = null
|
|
70
|
+
try {
|
|
71
|
+
child = await workflow('run-milestone', {
|
|
72
|
+
tickets: m.tickets,
|
|
73
|
+
mode: cfg.mode,
|
|
74
|
+
defaultBranch: cfg.defaultBranch,
|
|
75
|
+
platform: cfg.platform,
|
|
76
|
+
})
|
|
77
|
+
} catch (e) {
|
|
78
|
+
err = e && e.message ? e.message : String(e)
|
|
79
|
+
}
|
|
80
|
+
const rs = (child && child.results) || []
|
|
81
|
+
|
|
82
|
+
if (cfg.mode === 'supervised') {
|
|
83
|
+
// supervised run-milestone stops after each CLEAR; a module "completes" only via
|
|
84
|
+
// resume (its issues close, its ticket list arrives empty next run)
|
|
85
|
+
const paused = !err && rs.some(function (r) { return r.status === 'awaiting-human-merge' })
|
|
86
|
+
state[m.name] = 'paused'
|
|
87
|
+
results.push({
|
|
88
|
+
name: m.name,
|
|
89
|
+
state: paused ? 'paused-for-merge' : 'failed',
|
|
90
|
+
detail: err || (paused ? 'CLEAR reached — merge it, then re-run /start-all to continue' : 'module stopped without reaching a CLEAR'),
|
|
91
|
+
ticketResults: rs,
|
|
92
|
+
})
|
|
93
|
+
stopped = true
|
|
94
|
+
continue
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const allDelivered = !err && rs.length === m.tickets.length && rs.every(function (r) { return r.status === 'delivered' })
|
|
98
|
+
if (allDelivered) {
|
|
99
|
+
state[m.name] = 'completed'
|
|
100
|
+
results.push({ name: m.name, state: 'completed', detail: rs.length + ' ticket(s) delivered', ticketResults: rs })
|
|
101
|
+
log('module ' + m.name + ': completed')
|
|
102
|
+
} else {
|
|
103
|
+
state[m.name] = 'failed'
|
|
104
|
+
const firstBad = rs.find(function (r) { return r.status !== 'delivered' })
|
|
105
|
+
results.push({
|
|
106
|
+
name: m.name,
|
|
107
|
+
state: 'failed',
|
|
108
|
+
detail: err || (firstBad ? firstBad.id + ': ' + firstBad.status + (firstBad.stage ? ' (' + firstBad.stage + ')' : '') : 'incomplete results'),
|
|
109
|
+
ticketResults: rs,
|
|
110
|
+
})
|
|
111
|
+
log('module ' + m.name + ': FAILED — dependents will be skipped; independent branches continue')
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const done = results.filter(function (r) { return r.state === 'completed' || r.state === 'already-complete' }).length
|
|
116
|
+
log('start-all finished: ' + done + '/' + cfg.modules.length + ' modules complete')
|
|
117
|
+
|
|
118
|
+
return { mode: cfg.mode, results: results, stoppedEarly: stopped }
|