@dtlabs/ai-toolkit 0.13.0 → 0.14.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/CHANGELOG.md +52 -0
- package/bin/cli.js +371 -2
- package/docs/task-executor-bootstrap.md +195 -0
- package/docs/task-executor-cli.md +319 -0
- package/docs/workflow-control-plane.md +102 -0
- package/lib/execution-ledger.js +186 -4
- package/lib/task-executor/claude-process.js +563 -0
- package/lib/task-executor/git.js +1362 -0
- package/lib/task-executor/index.js +3729 -0
- package/lib/task-executor/ownership.js +590 -0
- package/lib/task-executor/plan.js +725 -0
- package/lib/task-executor/store.js +1164 -0
- package/lib/workflow-artifacts.js +102 -0
- package/lib/workflow-control.js +225 -0
- package/lib/workflow-runner.js +39 -0
- package/package.json +1 -1
- package/src/claude/agents/gaia-intervention-documentation-standard.md +9 -9
- package/src/claude/agents/gaia-validate-feature-docs.md +31 -28
- package/src/claude/skills/assess-codebase/SKILL.md +9 -103
- package/src/claude/skills/implement-feature/SKILL.md +74 -159
- package/src/claude/workflows/am-phase1.js +43 -294
- package/src/claude/workflows/am-phase2.js +10 -164
- package/src/claude/workflows/pm-phase1.js +45 -358
- package/src/claude/workflows/pm-phase2.js +50 -571
- package/src/claude/workflows/pm-phase3.js +5 -824
|
@@ -1,828 +1,9 @@
|
|
|
1
1
|
export const meta = {
|
|
2
2
|
name: 'pm-phase3',
|
|
3
|
-
description: '
|
|
4
|
-
phases: [
|
|
5
|
-
{ title: 'Parse', detail: 'Parse Work Breakdown into structured implementation phases' },
|
|
6
|
-
{ title: 'Implementation', detail: 'Dispatch developer agents per US, review, commit' },
|
|
7
|
-
{ title: 'Test', detail: 'Single test run after all phases complete' },
|
|
8
|
-
{ title: 'Remediation', detail: 'Fix OPEN issues from Issues Register' },
|
|
9
|
-
{ title: 'PR', detail: 'Push branch, create PR, update registry' },
|
|
10
|
-
{ title: 'Actuals', detail: 'Write Token-Estimate and Effort-Estimate actuals' },
|
|
11
|
-
],
|
|
3
|
+
description: 'Retired compatibility entry point. Implementation is owned by the FTR-018 task executor.',
|
|
4
|
+
phases: [],
|
|
12
5
|
}
|
|
13
6
|
|
|
14
|
-
//
|
|
15
|
-
//
|
|
16
|
-
|
|
17
|
-
const RESOLVE_SCHEMA = {
|
|
18
|
-
type: 'object',
|
|
19
|
-
properties: {
|
|
20
|
-
nativeName: { type: 'string' },
|
|
21
|
-
status: { type: 'string' },
|
|
22
|
-
agentId: { type: 'string' },
|
|
23
|
-
exitNonZero: { type: 'boolean' },
|
|
24
|
-
error: { type: 'string' },
|
|
25
|
-
},
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
async function resolveAgentTier2(agentId, label, phaseName) {
|
|
29
|
-
const result = await agent(
|
|
30
|
-
`Run this command via Bash:\n\nai-toolkit agents resolve --project . --id ${agentId} --require-verified\n\nIf the command exits 0: parse the JSON from stdout and return it with exitNonZero: false.\nIf the command exits non-zero: return {"exitNonZero": true, "error": "<stderr text>", "agentId": "${agentId}"}.`,
|
|
31
|
-
{ label, phase: phaseName, model: 'haiku', schema: RESOLVE_SCHEMA }
|
|
32
|
-
)
|
|
33
|
-
if (!result || result.exitNonZero || !result.nativeName) {
|
|
34
|
-
throw new Error(`HARD STOP — Tier 2 resolution failed for ${agentId}: ${result && result.error ? result.error : 'no nativeName returned'}`)
|
|
35
|
-
}
|
|
36
|
-
return result.nativeName
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
// Map from Work-Breakdown agent_type values to canonical agent IDs.
|
|
40
|
-
// Used by the Tier 2 guard to resolve before each dispatch.
|
|
41
|
-
const IMPL_AGENT_IDS = {
|
|
42
|
-
'developer-backend': 'gaia.agent.developer.backend',
|
|
43
|
-
'developer-frontend': 'gaia.agent.developer.frontend',
|
|
44
|
-
'developer-testing': 'gaia.agent.developer.testing',
|
|
45
|
-
'review-solution': 'gaia.agent.review.solution',
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
// ── Ledger facade helpers ─────────────────────────────────────────────────────
|
|
49
|
-
// Route all ledger I/O through the ai-toolkit CLI facade.
|
|
50
|
-
// Open calls fire-and-forget (matching pm-phase1/pm-phase2 pattern).
|
|
51
|
-
// Terminal calls (close, fail, skip) are fail-closed: a non-zero exit status
|
|
52
|
-
// hard-stops the workflow because the terminal state was not persisted.
|
|
53
|
-
|
|
54
|
-
const LEDGER_TERMINAL_SCHEMA = {
|
|
55
|
-
type: 'object',
|
|
56
|
-
properties: {
|
|
57
|
-
exitCode: { type: 'number' },
|
|
58
|
-
stdout: { type: 'string' },
|
|
59
|
-
stderr: { type: 'string' },
|
|
60
|
-
},
|
|
61
|
-
required: ['exitCode'],
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
async function ledgerTerminal(cmd, label, phaseName) {
|
|
65
|
-
const result = await agent(
|
|
66
|
-
`Run this shell command via Bash and return the exit code as structured output.\n\nCommand: ${cmd}\n\nCapture: exitCode (integer), stdout (string), stderr (string). Return all three.`,
|
|
67
|
-
{ label, phase: phaseName, model: 'haiku', schema: LEDGER_TERMINAL_SCHEMA }
|
|
68
|
-
)
|
|
69
|
-
const status = (result && typeof result.exitCode === 'number') ? result.exitCode : 1
|
|
70
|
-
if (status !== 0) {
|
|
71
|
-
throw new Error(`HARD STOP — ledger terminal state not persisted. Exit code: ${status}. Command: ${cmd}`)
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
// null/0/not_available => data unavailable, never an observable real zero.
|
|
76
|
-
// A cached or resumed agent returns a budget delta of 0, which is not a real
|
|
77
|
-
// measurement — omit --tokens from close so the module preserves any existing
|
|
78
|
-
// positive phase_delta_tokens on disk rather than clobbering it with zero.
|
|
79
|
-
function tokensAvailable(t) { return Number.isInteger(t) && t > 0 }
|
|
80
|
-
|
|
81
|
-
// ── Parse args ────────────────────────────────────────────────────────────────
|
|
82
|
-
|
|
83
|
-
// args: "<path-to-feature.md> --branch feature/FTR-NNN-slug"
|
|
84
|
-
const argStr = typeof args === 'string' ? args : ''
|
|
85
|
-
const featurePath = argStr.trim().split(/\s+/)[0]
|
|
86
|
-
const branchMatch = argStr.match(/--branch\s+(\S+)/)
|
|
87
|
-
const branch = branchMatch ? branchMatch[1] : null
|
|
88
|
-
|
|
89
|
-
const tokenLedger = []
|
|
90
|
-
const escalations = []
|
|
91
|
-
|
|
92
|
-
// ── Parse Work Breakdown CSV (deterministic, no AI) ───────────────────────────
|
|
93
|
-
phase('Parse')
|
|
94
|
-
|
|
95
|
-
// Derive feature_dir and prefix from featurePath (e.g. "docs/features/FTR-004-.../feature.md")
|
|
96
|
-
const featureDir = featurePath.replace(/\/[^/]+$/, '')
|
|
97
|
-
const prefixMatch = featureDir.match(/([A-Z]+-\d+)/)
|
|
98
|
-
const prefix = prefixMatch ? prefixMatch[1] : 'FTR-000'
|
|
99
|
-
const csvPath = `${featureDir}/${prefix}-Work-Breakdown.csv`
|
|
100
|
-
|
|
101
|
-
// ── Tier 3 self-registration (open BEFORE any main logic, fail-closed) ────────
|
|
102
|
-
await ledgerTerminal(
|
|
103
|
-
`ai-toolkit ledger open --prefix ${prefix} --agent pm-phase3:self --phase phase3 --dir "${featureDir}" --attempt 1`,
|
|
104
|
-
'ledger-open-pm-phase3-self', 'Parse'
|
|
105
|
-
)
|
|
106
|
-
|
|
107
|
-
log(`Reading CSV: ${csvPath}`)
|
|
108
|
-
|
|
109
|
-
const csvKey = 'read-wb-csv:phase3'
|
|
110
|
-
await agent(
|
|
111
|
-
`Run this shell command via Bash. If the --dir path contains spaces, enclose it in double quotes.\n\nai-toolkit ledger open --dir ${featureDir} --prefix ${prefix} --agent ${csvKey} --phase phase3 --model haiku --attempt 1\n\nReturn no output.`,
|
|
112
|
-
{ label: 'ledger-open-read-wb-csv', phase: 'Parse', model: 'haiku' }
|
|
113
|
-
)
|
|
114
|
-
const beforeCsv = budget.spent()
|
|
115
|
-
const csvContent = await agent(
|
|
116
|
-
`Read the file at path: ${csvPath}\nReturn ONLY the raw file contents, nothing else. No explanation, no formatting, no JSON wrapping — just the raw text of the file.`,
|
|
117
|
-
{ label: 'read-wb-csv', phase: 'Parse', model: 'haiku' }
|
|
118
|
-
)
|
|
119
|
-
const csvTokens = budget.spent() - beforeCsv
|
|
120
|
-
await ledgerTerminal(
|
|
121
|
-
`ai-toolkit ledger close --dir ${featureDir} --prefix ${prefix} --agent ${csvKey}${tokensAvailable(csvTokens) ? ` --tokens ${csvTokens}` : ''} --attempt 1`,
|
|
122
|
-
'ledger-close-read-wb-csv', 'Parse'
|
|
123
|
-
)
|
|
124
|
-
|
|
125
|
-
// Parse CSV into structured phases — pure JS, no AI
|
|
126
|
-
const rows = csvContent
|
|
127
|
-
.split('\n')
|
|
128
|
-
.map(l => l.trim())
|
|
129
|
-
.filter(l => l && !l.startsWith('phase_id')) // skip header and empty lines
|
|
130
|
-
.map(l => {
|
|
131
|
-
const [phase_id, phase_title, commit_message, depends_on, task_id, task_title, domain, agent_type] = l.split('|')
|
|
132
|
-
return { phase_id, phase_title, commit_message, depends_on: depends_on || '', task_id, task_title, domain, agent_type }
|
|
133
|
-
})
|
|
134
|
-
.filter(r => r.phase_id && r.task_id)
|
|
135
|
-
|
|
136
|
-
// Group rows into phases preserving order
|
|
137
|
-
const phaseMap = new Map()
|
|
138
|
-
for (const row of rows) {
|
|
139
|
-
if (!phaseMap.has(row.phase_id)) {
|
|
140
|
-
phaseMap.set(row.phase_id, {
|
|
141
|
-
phase_id: row.phase_id,
|
|
142
|
-
title: row.phase_title,
|
|
143
|
-
commit_message: row.commit_message,
|
|
144
|
-
depends_on: row.depends_on.split(' ').filter(Boolean), // [] for empty
|
|
145
|
-
impl_tasks: [],
|
|
146
|
-
test_tasks: [],
|
|
147
|
-
})
|
|
148
|
-
}
|
|
149
|
-
const p = phaseMap.get(row.phase_id)
|
|
150
|
-
if (row.domain === 'TEST') {
|
|
151
|
-
p.test_tasks.push({ task_id: row.task_id, agent_type: row.agent_type })
|
|
152
|
-
} else {
|
|
153
|
-
p.impl_tasks.push({ task_id: row.task_id, agent_type: row.agent_type })
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
// Build impl_groups and test_groups (merge by agent_type within each phase)
|
|
158
|
-
const buildGroups = (tasks) => {
|
|
159
|
-
const map = new Map()
|
|
160
|
-
for (const t of tasks) {
|
|
161
|
-
if (!map.has(t.agent_type)) map.set(t.agent_type, [])
|
|
162
|
-
map.get(t.agent_type).push(t.task_id)
|
|
163
|
-
}
|
|
164
|
-
return Array.from(map.entries()).map(([agent_type, task_ids]) => ({ agent_type, task_ids }))
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
const allPhases = Array.from(phaseMap.values()).map(p => ({
|
|
168
|
-
phase_id: p.phase_id,
|
|
169
|
-
title: p.title,
|
|
170
|
-
commit_message: p.commit_message,
|
|
171
|
-
depends_on: p.depends_on,
|
|
172
|
-
impl_groups: buildGroups(p.impl_tasks),
|
|
173
|
-
test_groups: buildGroups(p.test_tasks),
|
|
174
|
-
}))
|
|
175
|
-
|
|
176
|
-
const wb = { prefix, feature_dir: featureDir, phases: allPhases }
|
|
177
|
-
|
|
178
|
-
log(`CSV parsed: ${rows.length} tasks → ${wb.phases.length} phases for ${wb.prefix}`)
|
|
179
|
-
|
|
180
|
-
// Build execution waves: phases whose dependencies are all satisfied can run in parallel.
|
|
181
|
-
// Wave 0 = no deps, Wave 1 = depends only on Wave 0 phases, etc.
|
|
182
|
-
const buildWaves = (phases) => {
|
|
183
|
-
const done = new Set()
|
|
184
|
-
const waves = []
|
|
185
|
-
let remaining = [...phases]
|
|
186
|
-
while (remaining.length > 0) {
|
|
187
|
-
const ready = remaining.filter(p => p.depends_on.every(d => done.has(d)))
|
|
188
|
-
if (ready.length === 0) {
|
|
189
|
-
// Circular or unresolvable deps — run the rest sequentially to avoid deadlock
|
|
190
|
-
waves.push(remaining)
|
|
191
|
-
break
|
|
192
|
-
}
|
|
193
|
-
waves.push(ready)
|
|
194
|
-
ready.forEach(p => done.add(p.phase_id))
|
|
195
|
-
remaining = remaining.filter(p => !done.has(p.phase_id))
|
|
196
|
-
}
|
|
197
|
-
return waves
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
const waves = buildWaves(wb.phases)
|
|
201
|
-
log(`Execution plan: ${waves.length} wave(s) — ${waves.map((w, i) => `wave${i+1}:[${w.map(p => p.phase_id).join(',')}]`).join(' ')}`)
|
|
202
|
-
|
|
203
|
-
// Pre-resolve all implementation agent types (Tier 2 guard, fail-closed before any phase runs)
|
|
204
|
-
const resolvedNativeNames = {}
|
|
205
|
-
for (const [legacyName, canonicalId] of Object.entries(IMPL_AGENT_IDS)) {
|
|
206
|
-
resolvedNativeNames[legacyName] = await resolveAgentTier2(canonicalId, `resolve-${legacyName}`, 'Parse')
|
|
207
|
-
}
|
|
208
|
-
log(`Tier 2 resolution complete: ${Object.keys(resolvedNativeNames).join(', ')}`)
|
|
209
|
-
|
|
210
|
-
let phasesDone = 0
|
|
211
|
-
const usPassed = []
|
|
212
|
-
const usEscalated = []
|
|
213
|
-
let issuesPath = ''
|
|
214
|
-
let issuesOpen = 0
|
|
215
|
-
|
|
216
|
-
// ── Implementation ────────────────────────────────────────────────────────────
|
|
217
|
-
phase('Implementation')
|
|
218
|
-
|
|
219
|
-
const REVIEW_SCHEMA = {
|
|
220
|
-
type: 'object',
|
|
221
|
-
properties: {
|
|
222
|
-
has_critical: { type: 'boolean' },
|
|
223
|
-
findings: { type: 'string' },
|
|
224
|
-
issues_md: { type: 'string' },
|
|
225
|
-
},
|
|
226
|
-
required: ['has_critical'],
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
// executePhase: runs impl_groups → test_groups → review → rework → commit for one phase
|
|
230
|
-
const executePhase = async (implPhase) => {
|
|
231
|
-
log(`Starting phase ${implPhase.phase_id}: ${implPhase.title}`)
|
|
232
|
-
|
|
233
|
-
let reworkCycle = 0
|
|
234
|
-
let passed = false
|
|
235
|
-
let lastFindings = ''
|
|
236
|
-
|
|
237
|
-
while (reworkCycle <= 2 && !passed) {
|
|
238
|
-
const reworkSuffix = reworkCycle > 0
|
|
239
|
-
? ` -- REWORK CYCLE ${reworkCycle}: address these critical findings:\n${lastFindings}`
|
|
240
|
-
: ''
|
|
241
|
-
|
|
242
|
-
// Step 1 — impl groups in parallel (INFRA / BE / FE)
|
|
243
|
-
if (implPhase.impl_groups.length > 0) {
|
|
244
|
-
const implKey = `${implPhase.impl_groups.map(g => g.agent_type).join('+')}:${implPhase.phase_id}${reworkCycle > 0 ? ':rework' + reworkCycle : ''}`
|
|
245
|
-
await agent(
|
|
246
|
-
`Run this shell command via Bash. If the --dir path contains spaces, enclose it in double quotes.\n\nai-toolkit ledger open --dir ${featureDir} --prefix ${prefix} --agent ${implKey} --phase phase3 --model sonnet --attempt 1\n\nReturn no output.`,
|
|
247
|
-
{ label: `ledger-open-impl:${implPhase.phase_id}`, phase: 'Implementation', model: 'haiku' }
|
|
248
|
-
)
|
|
249
|
-
const beforeImpl = budget.spent()
|
|
250
|
-
let implFailed = false
|
|
251
|
-
let implErrMsg = ''
|
|
252
|
-
try {
|
|
253
|
-
await parallel(implPhase.impl_groups.map(group => () => {
|
|
254
|
-
const nativeName = resolvedNativeNames[group.agent_type]
|
|
255
|
-
if (!nativeName) throw new Error(`HARD STOP — Tier 2: no resolved nativeName for agent_type: ${group.agent_type}`)
|
|
256
|
-
return agent(
|
|
257
|
-
`${featurePath} ${group.task_ids.join(',')}${reworkSuffix}`,
|
|
258
|
-
{
|
|
259
|
-
agentType: nativeName,
|
|
260
|
-
label: `${group.agent_type}:${implPhase.phase_id}${reworkCycle > 0 ? ':rework' + reworkCycle : ''}`,
|
|
261
|
-
phase: 'Implementation',
|
|
262
|
-
}
|
|
263
|
-
)
|
|
264
|
-
}))
|
|
265
|
-
} catch (err) {
|
|
266
|
-
implFailed = true
|
|
267
|
-
implErrMsg = err && err.message ? err.message.slice(0, 120) : 'impl dispatch failed'
|
|
268
|
-
}
|
|
269
|
-
const implTokens = budget.spent() - beforeImpl
|
|
270
|
-
if (implFailed) {
|
|
271
|
-
await ledgerTerminal(
|
|
272
|
-
`ai-toolkit ledger fail --dir ${featureDir} --prefix ${prefix} --agent ${implKey} --error "impl dispatch failed" --attempt 1`,
|
|
273
|
-
`ledger-fail-impl:${implPhase.phase_id}`, 'Implementation'
|
|
274
|
-
)
|
|
275
|
-
throw new Error(`Phase ${implPhase.phase_id} impl dispatch failed: ${implErrMsg}`)
|
|
276
|
-
}
|
|
277
|
-
await ledgerTerminal(
|
|
278
|
-
`ai-toolkit ledger close --dir ${featureDir} --prefix ${prefix} --agent ${implKey}${tokensAvailable(implTokens) ? ` --tokens ${implTokens}` : ''} --attempt 1`,
|
|
279
|
-
`ledger-close-impl:${implPhase.phase_id}`, 'Implementation'
|
|
280
|
-
)
|
|
281
|
-
tokenLedger.push({ agent: implKey, model: 'sonnet', phase_delta_tokens: implTokens })
|
|
282
|
-
log(`Phase ${implPhase.phase_id} impl groups done — ${implTokens} tokens`)
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
// Step 2 — test groups in parallel (TEST) — after impl groups
|
|
286
|
-
if (implPhase.test_groups.length > 0) {
|
|
287
|
-
const testKey = `developer-testing:${implPhase.phase_id}${reworkCycle > 0 ? ':rework' + reworkCycle : ''}`
|
|
288
|
-
await agent(
|
|
289
|
-
`Run this shell command via Bash. If the --dir path contains spaces, enclose it in double quotes.\n\nai-toolkit ledger open --dir ${featureDir} --prefix ${prefix} --agent ${testKey} --phase phase3 --model sonnet --attempt 1\n\nReturn no output.`,
|
|
290
|
-
{ label: `ledger-open-test:${implPhase.phase_id}`, phase: 'Implementation', model: 'haiku' }
|
|
291
|
-
)
|
|
292
|
-
const beforeTestGroup = budget.spent()
|
|
293
|
-
let testFailed = false
|
|
294
|
-
let testErrMsg = ''
|
|
295
|
-
try {
|
|
296
|
-
await parallel(implPhase.test_groups.map(group => () => {
|
|
297
|
-
const nativeName = resolvedNativeNames[group.agent_type]
|
|
298
|
-
if (!nativeName) throw new Error(`HARD STOP — Tier 2: no resolved nativeName for agent_type: ${group.agent_type}`)
|
|
299
|
-
return agent(
|
|
300
|
-
`${featurePath} ${group.task_ids.join(',')}${reworkSuffix}`,
|
|
301
|
-
{
|
|
302
|
-
agentType: nativeName,
|
|
303
|
-
label: `${group.agent_type}:${implPhase.phase_id}${reworkCycle > 0 ? ':rework' + reworkCycle : ''}`,
|
|
304
|
-
phase: 'Implementation',
|
|
305
|
-
}
|
|
306
|
-
)
|
|
307
|
-
}))
|
|
308
|
-
} catch (err) {
|
|
309
|
-
testFailed = true
|
|
310
|
-
testErrMsg = err && err.message ? err.message.slice(0, 120) : 'test dispatch failed'
|
|
311
|
-
}
|
|
312
|
-
const testGroupTokens = budget.spent() - beforeTestGroup
|
|
313
|
-
if (testFailed) {
|
|
314
|
-
await ledgerTerminal(
|
|
315
|
-
`ai-toolkit ledger fail --dir ${featureDir} --prefix ${prefix} --agent ${testKey} --error "test dispatch failed" --attempt 1`,
|
|
316
|
-
`ledger-fail-test:${implPhase.phase_id}`, 'Implementation'
|
|
317
|
-
)
|
|
318
|
-
throw new Error(`Phase ${implPhase.phase_id} test dispatch failed: ${testErrMsg}`)
|
|
319
|
-
}
|
|
320
|
-
await ledgerTerminal(
|
|
321
|
-
`ai-toolkit ledger close --dir ${featureDir} --prefix ${prefix} --agent ${testKey}${tokensAvailable(testGroupTokens) ? ` --tokens ${testGroupTokens}` : ''} --attempt 1`,
|
|
322
|
-
`ledger-close-test:${implPhase.phase_id}`, 'Implementation'
|
|
323
|
-
)
|
|
324
|
-
tokenLedger.push({ agent: testKey, model: 'sonnet', phase_delta_tokens: testGroupTokens })
|
|
325
|
-
log(`Phase ${implPhase.phase_id} test groups done — ${testGroupTokens} tokens`)
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
// Step 3 — review-solution
|
|
329
|
-
const reviewKey = `review-solution:${implPhase.phase_id}`
|
|
330
|
-
await agent(
|
|
331
|
-
`Run this shell command via Bash. If the --dir path contains spaces, enclose it in double quotes.\n\nai-toolkit ledger open --dir ${featureDir} --prefix ${prefix} --agent ${reviewKey} --phase phase3 --model sonnet --attempt 1\n\nReturn no output.`,
|
|
332
|
-
{ label: `ledger-open-review:${implPhase.phase_id}`, phase: 'Implementation', model: 'haiku' }
|
|
333
|
-
)
|
|
334
|
-
const beforeReview = budget.spent()
|
|
335
|
-
const review = await agent(
|
|
336
|
-
`${featurePath} --scope ${implPhase.phase_id}`,
|
|
337
|
-
{
|
|
338
|
-
agentType: resolvedNativeNames['review-solution'],
|
|
339
|
-
label: `review-solution:${implPhase.phase_id}`,
|
|
340
|
-
phase: 'Implementation',
|
|
341
|
-
schema: REVIEW_SCHEMA,
|
|
342
|
-
}
|
|
343
|
-
)
|
|
344
|
-
const reviewTokens = budget.spent() - beforeReview
|
|
345
|
-
await ledgerTerminal(
|
|
346
|
-
`ai-toolkit ledger close --dir ${featureDir} --prefix ${prefix} --agent ${reviewKey}${tokensAvailable(reviewTokens) ? ` --tokens ${reviewTokens}` : ''} --attempt 1`,
|
|
347
|
-
`ledger-close-review:${implPhase.phase_id}`, 'Implementation'
|
|
348
|
-
)
|
|
349
|
-
tokenLedger.push({
|
|
350
|
-
agent: reviewKey,
|
|
351
|
-
model: 'sonnet',
|
|
352
|
-
phase_delta_tokens: reviewTokens,
|
|
353
|
-
})
|
|
354
|
-
|
|
355
|
-
if (review && !review.has_critical) {
|
|
356
|
-
passed = true
|
|
357
|
-
if (review.issues_md) {
|
|
358
|
-
await agent(
|
|
359
|
-
`Append the following WARNING/INFO findings to ${wb.feature_dir}/${prefix}-Issues.md (create the file if it does not exist, with a header "# Issues Register — ${prefix}"):\n\n${review.issues_md}`,
|
|
360
|
-
{ label: `log-issues:${implPhase.phase_id}`, phase: 'Implementation' }
|
|
361
|
-
)
|
|
362
|
-
}
|
|
363
|
-
} else {
|
|
364
|
-
lastFindings = review?.findings || 'unspecified critical issues'
|
|
365
|
-
reworkCycle++
|
|
366
|
-
log(`Phase ${implPhase.phase_id}: critical findings — rework cycle ${reworkCycle}`)
|
|
367
|
-
}
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
if (!passed) {
|
|
371
|
-
log(`Phase ${implPhase.phase_id} ESCALATED after 2 rework cycles`)
|
|
372
|
-
escalations.push({ phaseId: implPhase.phase_id, reason: 'CRITICAL review findings unresolved after 2 rework cycles' })
|
|
373
|
-
usEscalated.push(implPhase.phase_id)
|
|
374
|
-
} else {
|
|
375
|
-
const commitMsg = implPhase.commit_message || `feat(${prefix}): implement ${implPhase.phase_id}`
|
|
376
|
-
await agent(
|
|
377
|
-
`Run these git commands in the repository root:\ngit add -A\ngit commit -m "${commitMsg}"\nIf there is nothing to commit, that is fine — just report success.`,
|
|
378
|
-
{ label: `commit:${implPhase.phase_id}`, phase: 'Implementation' }
|
|
379
|
-
)
|
|
380
|
-
if (implPhase.phase_id !== 'INFRA') usPassed.push(implPhase.phase_id)
|
|
381
|
-
log(`Phase ${implPhase.phase_id} passed review and committed`)
|
|
382
|
-
}
|
|
383
|
-
|
|
384
|
-
phasesDone++
|
|
385
|
-
}
|
|
386
|
-
|
|
387
|
-
// Execute waves: phases in the same wave run in parallel, waves are sequential
|
|
388
|
-
for (const wave of waves) {
|
|
389
|
-
if (wave.length === 1) {
|
|
390
|
-
await executePhase(wave[0])
|
|
391
|
-
} else {
|
|
392
|
-
log(`Wave: running ${wave.map(p => p.phase_id).join(', ')} in parallel`)
|
|
393
|
-
await parallel(wave.map(p => () => executePhase(p)))
|
|
394
|
-
}
|
|
395
|
-
}
|
|
396
|
-
|
|
397
|
-
// Count open issues
|
|
398
|
-
const ISSUES_SCHEMA = {
|
|
399
|
-
type: 'object',
|
|
400
|
-
properties: {
|
|
401
|
-
exists: { type: 'boolean' },
|
|
402
|
-
open_count: { type: 'number' },
|
|
403
|
-
path: { type: 'string' },
|
|
404
|
-
},
|
|
405
|
-
required: ['exists', 'open_count'],
|
|
406
|
-
}
|
|
407
|
-
const issuesResult = await agent(
|
|
408
|
-
`Check if ${wb.feature_dir}/${prefix}-Issues.md exists. If it does, count rows with status OPEN. Return exists, open_count, and the full path.`,
|
|
409
|
-
{ label: 'count-open-issues', phase: 'Implementation', schema: ISSUES_SCHEMA }
|
|
410
|
-
)
|
|
411
|
-
issuesPath = issuesResult?.path || ''
|
|
412
|
-
issuesOpen = issuesResult?.open_count || 0
|
|
413
|
-
log(`Implementation complete — ${phasesDone} phases, ${usPassed.length} US passed, ${usEscalated.length} escalated, ${issuesOpen} open issues`)
|
|
414
|
-
|
|
415
|
-
// ── Final test run ────────────────────────────────────────────────────────────
|
|
416
|
-
// Developer agents no longer run the test suite themselves (they do a syntax-check
|
|
417
|
-
// build at most). Running the suite once here, after all files are written, replaces
|
|
418
|
-
// N parallel per-agent test runs — cutting ~1h of duplicated builds and artifact
|
|
419
|
-
// contention down to a single centralized run with one consolidated failure report.
|
|
420
|
-
phase('Test')
|
|
421
|
-
|
|
422
|
-
const testRunKey = 'final-test-run'
|
|
423
|
-
await agent(
|
|
424
|
-
`Run this shell command via Bash. If the --dir path contains spaces, enclose it in double quotes.\n\nai-toolkit ledger open --dir ${featureDir} --prefix ${prefix} --agent ${testRunKey} --phase phase3 --model haiku --attempt 1\n\nReturn no output.`,
|
|
425
|
-
{ label: 'ledger-open-final-test-run', phase: 'Test', model: 'haiku' }
|
|
426
|
-
)
|
|
427
|
-
const beforeFinalTest = budget.spent()
|
|
428
|
-
await agent(
|
|
429
|
-
`Run the full test suite in the repository root and report the result.
|
|
430
|
-
|
|
431
|
-
1. Read AGENTS.md in the repository root to find the exact test command for this project.
|
|
432
|
-
2. Run that command (e.g. \`dotnet test --no-restore --logger "console;verbosity=normal"\`, \`npm test\`).
|
|
433
|
-
3. Report the outcome: total passed/failed, and for any failures the test name and first error line.
|
|
434
|
-
|
|
435
|
-
Do NOT fix failing tests — only report them.`,
|
|
436
|
-
{ label: 'final-test-run', phase: 'Test', model: 'haiku' }
|
|
437
|
-
)
|
|
438
|
-
const finalTestTokens = budget.spent() - beforeFinalTest
|
|
439
|
-
await ledgerTerminal(
|
|
440
|
-
`ai-toolkit ledger close --dir ${featureDir} --prefix ${prefix} --agent ${testRunKey}${tokensAvailable(finalTestTokens) ? ` --tokens ${finalTestTokens}` : ''} --attempt 1`,
|
|
441
|
-
'ledger-close-final-test-run', 'Test'
|
|
442
|
-
)
|
|
443
|
-
tokenLedger.push({ agent: testRunKey, model: 'haiku', phase_delta_tokens: finalTestTokens })
|
|
444
|
-
log(`Test run complete — ${finalTestTokens} tokens`)
|
|
445
|
-
|
|
446
|
-
// ── Remediation ───────────────────────────────────────────────────────────────
|
|
447
|
-
phase('Remediation')
|
|
448
|
-
|
|
449
|
-
const REM_SCHEMA = {
|
|
450
|
-
type: 'object',
|
|
451
|
-
properties: {
|
|
452
|
-
issues_fixed: { type: 'number' },
|
|
453
|
-
issues_deferred: { type: 'number' },
|
|
454
|
-
},
|
|
455
|
-
required: ['issues_fixed', 'issues_deferred'],
|
|
456
|
-
}
|
|
457
|
-
|
|
458
|
-
let issuesFixed = 0
|
|
459
|
-
let issuesDeferred = 0
|
|
460
|
-
|
|
461
|
-
const remKey = 'remediation'
|
|
462
|
-
if (issuesOpen > 0 && issuesPath) {
|
|
463
|
-
log(`Remediation: ${issuesOpen} OPEN issues`)
|
|
464
|
-
await agent(
|
|
465
|
-
`Run this shell command via Bash. If the --dir path contains spaces, enclose it in double quotes.\n\nai-toolkit ledger open --dir ${featureDir} --prefix ${prefix} --agent ${remKey} --phase phase3 --model sonnet --attempt 1\n\nReturn no output.`,
|
|
466
|
-
{ label: 'ledger-open-remediation', phase: 'Remediation', model: 'haiku' }
|
|
467
|
-
)
|
|
468
|
-
const beforeRem = budget.spent()
|
|
469
|
-
|
|
470
|
-
const remResult = await agent(
|
|
471
|
-
`You are a remediation agent for the feature delivery pipeline.
|
|
472
|
-
|
|
473
|
-
Read the Issues Register at: ${issuesPath}
|
|
474
|
-
|
|
475
|
-
For each OPEN issue:
|
|
476
|
-
- If severity is INFO: mark it DEFERRED immediately (update Status column to DEFERRED). Do not dispatch a developer agent.
|
|
477
|
-
- If severity is WARNING: dispatch the appropriate developer agent (developer-backend for most cases) with the issue description. Then spawn review-solution to verify the fix. Max 1 retry per issue. If unresolved after 1 retry, mark DEFERRED.
|
|
478
|
-
|
|
479
|
-
After processing all issues, return: { "issues_fixed": N, "issues_deferred": N }
|
|
480
|
-
|
|
481
|
-
Update the Issues Register Status column in place for each resolved/deferred issue.`,
|
|
482
|
-
{
|
|
483
|
-
label: 'remediation',
|
|
484
|
-
phase: 'Remediation',
|
|
485
|
-
schema: REM_SCHEMA,
|
|
486
|
-
}
|
|
487
|
-
)
|
|
488
|
-
|
|
489
|
-
const remTokens = budget.spent() - beforeRem
|
|
490
|
-
await ledgerTerminal(
|
|
491
|
-
`ai-toolkit ledger close --dir ${featureDir} --prefix ${prefix} --agent ${remKey}${tokensAvailable(remTokens) ? ` --tokens ${remTokens}` : ''} --attempt 1`,
|
|
492
|
-
'ledger-close-remediation', 'Remediation'
|
|
493
|
-
)
|
|
494
|
-
tokenLedger.push({ agent: remKey, model: 'sonnet', phase_delta_tokens: remTokens })
|
|
495
|
-
issuesFixed = remResult.issues_fixed || 0
|
|
496
|
-
issuesDeferred = remResult.issues_deferred || 0
|
|
497
|
-
log(`Remediation done — fixed: ${issuesFixed}, deferred: ${issuesDeferred} — ${remTokens} tokens`)
|
|
498
|
-
} else {
|
|
499
|
-
log('Remediation: no OPEN issues — skipped')
|
|
500
|
-
await ledgerTerminal(
|
|
501
|
-
`ai-toolkit ledger skip --dir ${featureDir} --prefix ${prefix} --agent ${remKey} --phase phase3 --model sonnet --attempt 1`,
|
|
502
|
-
'ledger-skip-remediation', 'Remediation'
|
|
503
|
-
)
|
|
504
|
-
}
|
|
505
|
-
|
|
506
|
-
// ── PR ────────────────────────────────────────────────────────────────────────
|
|
507
|
-
phase('PR')
|
|
508
|
-
|
|
509
|
-
const PR_SCHEMA = {
|
|
510
|
-
type: 'object',
|
|
511
|
-
properties: {
|
|
512
|
-
pr_url: { type: 'string' },
|
|
513
|
-
registry_updated: { type: 'boolean' },
|
|
514
|
-
},
|
|
515
|
-
required: ['pr_url'],
|
|
516
|
-
}
|
|
517
|
-
|
|
518
|
-
const prKey = 'pr-and-registry'
|
|
519
|
-
await agent(
|
|
520
|
-
`Run this shell command via Bash. If the --dir path contains spaces, enclose it in double quotes.\n\nai-toolkit ledger open --dir ${featureDir} --prefix ${prefix} --agent ${prKey} --phase phase3 --model sonnet --attempt 1\n\nReturn no output.`,
|
|
521
|
-
{ label: 'ledger-open-pr-and-registry', phase: 'PR', model: 'haiku' }
|
|
522
|
-
)
|
|
523
|
-
const beforePR = budget.spent()
|
|
524
|
-
|
|
525
|
-
const prResult = await agent(
|
|
526
|
-
`You are the PR and registry agent for the feature delivery pipeline.
|
|
527
|
-
|
|
528
|
-
Feature path: ${featurePath}
|
|
529
|
-
Branch: ${branch || 'current branch'}
|
|
530
|
-
Prefix: ${prefix}
|
|
531
|
-
Issues fixed: ${issuesFixed}, deferred: ${issuesDeferred}
|
|
532
|
-
|
|
533
|
-
Your tasks:
|
|
534
|
-
|
|
535
|
-
1. If there are remediation fixes, commit them:
|
|
536
|
-
git add -A
|
|
537
|
-
git commit -m "fix(${prefix}): remediate review issues"
|
|
538
|
-
|
|
539
|
-
2. Push the branch:
|
|
540
|
-
git push -u origin ${branch || 'HEAD'}
|
|
541
|
-
|
|
542
|
-
3. Create a PR targeting develop:
|
|
543
|
-
gh pr create \\
|
|
544
|
-
--title "feat(${prefix}): <Feature Title from feature.md>" \\
|
|
545
|
-
--base develop \\
|
|
546
|
-
--body "## Summary
|
|
547
|
-
- Implements <Feature Title> (<N> User Stories, <N> tasks)
|
|
548
|
-
- Source docs: ${prefix}-Requirements.md, ${prefix}-Tech-Spec.md
|
|
549
|
-
|
|
550
|
-
## Implementation
|
|
551
|
-
- Architect review: all US passed
|
|
552
|
-
- Issues Register: ${issuesFixed} fixed, ${issuesDeferred} deferred → see ${prefix}-Issues.md
|
|
553
|
-
|
|
554
|
-
## Test plan
|
|
555
|
-
- [ ] Build passes
|
|
556
|
-
- [ ] Tests pass
|
|
557
|
-
- [ ] Manual smoke test of key flows"
|
|
558
|
-
|
|
559
|
-
4. Update the Feature Registry (internal_docs/features/REGISTRY.md):
|
|
560
|
-
Find or append the entry for ${prefix}. Set Status to "completed".
|
|
561
|
-
|
|
562
|
-
Return { "pr_url": "<url>", "registry_updated": true/false }.
|
|
563
|
-
If the PR creation fails, return { "pr_url": "(PR creation failed)", "registry_updated": false }.`,
|
|
564
|
-
{
|
|
565
|
-
label: 'pr-and-registry',
|
|
566
|
-
phase: 'PR',
|
|
567
|
-
schema: PR_SCHEMA,
|
|
568
|
-
}
|
|
569
|
-
)
|
|
570
|
-
|
|
571
|
-
const prTokens = budget.spent() - beforePR
|
|
572
|
-
await ledgerTerminal(
|
|
573
|
-
`ai-toolkit ledger close --dir ${featureDir} --prefix ${prefix} --agent ${prKey}${tokensAvailable(prTokens) ? ` --tokens ${prTokens}` : ''} --attempt 1`,
|
|
574
|
-
'ledger-close-pr-and-registry', 'PR'
|
|
575
|
-
)
|
|
576
|
-
tokenLedger.push({ agent: prKey, model: 'sonnet', phase_delta_tokens: prTokens })
|
|
577
|
-
log(`PR done: ${prResult.pr_url} — ${prTokens} tokens`)
|
|
578
|
-
|
|
579
|
-
// ── Actuals ───────────────────────────────────────────────────────────────────
|
|
580
|
-
phase('Actuals')
|
|
581
|
-
|
|
582
|
-
// Fallback: merge in any ledger entries that were persisted to disk but are missing
|
|
583
|
-
// from the in-memory ledger (e.g. this is a resumed run and earlier phases ran in a
|
|
584
|
-
// previous, interrupted invocation). The in-memory ledger has priority for entries
|
|
585
|
-
// present in both — it reflects the most recent measurements.
|
|
586
|
-
const persistedLedgerRaw = await agent(
|
|
587
|
-
`Read ${featureDir}/${prefix}-token-ledger.json and return its contents as a raw JSON string, nothing else. If the file does not exist, return exactly "[]".`,
|
|
588
|
-
{ label: 'read-ledger', phase: 'Actuals', model: 'haiku' }
|
|
589
|
-
)
|
|
590
|
-
try {
|
|
591
|
-
const persisted = JSON.parse(typeof persistedLedgerRaw === 'string' ? persistedLedgerRaw.trim() : '[]')
|
|
592
|
-
if (Array.isArray(persisted)) {
|
|
593
|
-
const inMemoryByAgent = new Map(tokenLedger.map((e, i) => [e.agent, i]))
|
|
594
|
-
for (const entry of persisted) {
|
|
595
|
-
if (!entry || !entry.agent) continue
|
|
596
|
-
const idx = inMemoryByAgent.get(entry.agent)
|
|
597
|
-
if (idx === undefined) {
|
|
598
|
-
// entry exists on disk but not in memory — recover it (e.g. from a prior interrupted run)
|
|
599
|
-
tokenLedger.push(entry)
|
|
600
|
-
inMemoryByAgent.set(entry.agent, tokenLedger.length - 1)
|
|
601
|
-
log(`Recovered ledger entry from disk: ${entry.agent} (${entry.phase_delta_tokens} tokens)`)
|
|
602
|
-
} else if (!tokensAvailable(tokenLedger[idx].phase_delta_tokens) && tokensAvailable(entry.phase_delta_tokens)) {
|
|
603
|
-
// in-memory entry has delta=0 (cached agent on resume) but disk has real data — prefer disk
|
|
604
|
-
tokenLedger[idx] = entry
|
|
605
|
-
log(`Restored real token count from disk: ${entry.agent} (${entry.phase_delta_tokens} tokens)`)
|
|
606
|
-
}
|
|
607
|
-
}
|
|
608
|
-
}
|
|
609
|
-
} catch (e) {
|
|
610
|
-
log(`Could not parse persisted token ledger — using in-memory ledger only`)
|
|
611
|
-
}
|
|
612
|
-
|
|
613
|
-
const totalPhase3Tokens = tokenLedger.reduce((s, e) => s + (tokensAvailable(e.phase_delta_tokens) ? e.phase_delta_tokens : 0), 0)
|
|
614
|
-
|
|
615
|
-
// ── Load token pricing for cost columns ──────────────────────────────────────
|
|
616
|
-
const pricingRaw3 = await agent(
|
|
617
|
-
`Read the file docs/token-pricing.json and return its raw JSON contents as a string, nothing else.`,
|
|
618
|
-
{ label: 'read-pricing', phase: 'Actuals', model: 'haiku' }
|
|
619
|
-
)
|
|
620
|
-
let pricing3 = null
|
|
621
|
-
try { pricing3 = JSON.parse(typeof pricingRaw3 === 'string' ? pricingRaw3.trim() : '{}') } catch (_) {}
|
|
622
|
-
|
|
623
|
-
function tokenCostEur3(tokens, model) {
|
|
624
|
-
if (!pricing3 || !pricing3.models) return null
|
|
625
|
-
const m = pricing3.models[model] || pricing3.models['sonnet']
|
|
626
|
-
const usdPer1M = 0.8 * m.input_per_1m_usd + 0.2 * m.output_per_1m_usd
|
|
627
|
-
return (tokens * usdPer1M / 1_000_000) * (pricing3.usd_to_eur || 1)
|
|
628
|
-
}
|
|
629
|
-
|
|
630
|
-
function formatEur3(val) {
|
|
631
|
-
if (val === null || val === undefined) return '—'
|
|
632
|
-
return '€' + val.toFixed(4)
|
|
633
|
-
}
|
|
634
|
-
|
|
635
|
-
// Aggregate the per-agent ledger into per-role totals so the actuals line up with the
|
|
636
|
-
// per-role estimate rows pm-phase2 wrote (developer-backend, developer-testing,
|
|
637
|
-
// review-solution, pr-and-registry, write-actuals, plus final-test-run). Precomputing
|
|
638
|
-
// here keeps the write-actuals agent from having to reason about grouping.
|
|
639
|
-
const roleTotals = {}
|
|
640
|
-
const roleModels = {}
|
|
641
|
-
for (const e of tokenLedger) {
|
|
642
|
-
const role = String(e.agent).split(':')[0]
|
|
643
|
-
roleTotals[role] = (roleTotals[role] || 0) + (tokensAvailable(e.phase_delta_tokens) ? e.phase_delta_tokens : 0)
|
|
644
|
-
roleModels[role] = e.model || 'sonnet'
|
|
645
|
-
}
|
|
646
|
-
const roleRows = Object.entries(roleTotals)
|
|
647
|
-
.map(([role, tok]) => `| ${role} | ${tok} | ${formatEur3(tokenCostEur3(tok, roleModels[role]))} |`)
|
|
648
|
-
.join('\n')
|
|
649
|
-
const totalPhase3CostEur = formatEur3(
|
|
650
|
-
Object.entries(roleTotals).reduce((s, [role, tok]) => s + (tokenCostEur3(tok, roleModels[role]) || 0), 0)
|
|
651
|
-
)
|
|
652
|
-
|
|
653
|
-
const ACTUALS_SCHEMA = {
|
|
654
|
-
type: 'object',
|
|
655
|
-
properties: {
|
|
656
|
-
token_estimate_path: { type: 'string' },
|
|
657
|
-
effort_estimate_path: { type: 'string' },
|
|
658
|
-
},
|
|
659
|
-
required: [],
|
|
660
|
-
}
|
|
661
|
-
|
|
662
|
-
const actualsKey = 'write-actuals'
|
|
663
|
-
await agent(
|
|
664
|
-
`Run this shell command via Bash. If the --dir path contains spaces, enclose it in double quotes.\n\nai-toolkit ledger open --dir ${featureDir} --prefix ${prefix} --agent ${actualsKey} --phase phase3 --model sonnet --attempt 1\n\nReturn no output.`,
|
|
665
|
-
{ label: 'ledger-open-write-actuals', phase: 'Actuals', model: 'haiku' }
|
|
666
|
-
)
|
|
667
|
-
const beforeActuals = budget.spent()
|
|
668
|
-
|
|
669
|
-
await agent(
|
|
670
|
-
`You are the actuals recorder for the feature delivery pipeline.
|
|
671
|
-
|
|
672
|
-
Feature path: ${featurePath}
|
|
673
|
-
Prefix: ${prefix}
|
|
674
|
-
|
|
675
|
-
Token ledger (phase-level measurements):
|
|
676
|
-
${JSON.stringify(tokenLedger, null, 2)}
|
|
677
|
-
|
|
678
|
-
Total tokens consumed by pm-phase3 workflow: ${totalPhase3Tokens} (exact, from budget tracking)
|
|
679
|
-
|
|
680
|
-
TASK 1 — Fill in the Phase 3 actuals in ${prefix}-Token-Estimate.md IN PLACE.
|
|
681
|
-
Read the file in the feature directory first (it was written by pm-phase2 and already contains
|
|
682
|
-
Phase 1/Phase 2 actuals and the Phase 3 estimate table). If it does not exist, create it.
|
|
683
|
-
|
|
684
|
-
Do NOT append a second Phase 3 table. Instead:
|
|
685
|
-
|
|
686
|
-
1. Locate the existing "## Phase 3 — Implementation (Estimates)" section. Replace its heading
|
|
687
|
-
with "## Phase 3 — Implementation (Est. vs Actual, by role)" and REPLACE its table with the
|
|
688
|
-
per-role reconciliation below — filling the "Tokens Actual" and "Actual cost €" columns of the
|
|
689
|
-
existing per-role estimate rows (keep the existing Tokens Est. and Est. cost € values; add a Delta column):
|
|
690
|
-
|
|
691
|
-
| Role | Model | Tokens Actual | Actual cost € |
|
|
692
|
-
|------|-------|--------------|--------------|
|
|
693
|
-
${roleRows}
|
|
694
|
-
| **Phase 3 total** | | **${totalPhase3Tokens}** | **${totalPhase3CostEur}** |
|
|
695
|
-
|
|
696
|
-
Match each existing estimate row (developer-backend, developer-testing, review-solution,
|
|
697
|
-
remediation, pr-and-registry, write-actuals) to the actual role total above by name, write the
|
|
698
|
-
actual into its "Tokens Actual" and "Actual cost €" cells, and compute Delta = Actual − Est (tokens).
|
|
699
|
-
Roles present in the actuals but absent from the estimate rows (e.g. final-test-run) get a new row with Est. = —.
|
|
700
|
-
|
|
701
|
-
2. Immediately after that table, add the per-agent detail as a sub-table (source of truth):
|
|
702
|
-
|
|
703
|
-
### Phase 3 — per-agent detail (actuals)
|
|
704
|
-
|
|
705
|
-
| Agent | Model | Tokens Actual | Actual cost € |
|
|
706
|
-
|-------|-------|--------------|--------------|
|
|
707
|
-
${tokenLedger.map(e => `| ${e.agent} | ${e.model} | ${e.phase_delta_tokens} | ${formatEur3(tokenCostEur3(e.phase_delta_tokens, e.model))} |`).join('\n')}
|
|
708
|
-
| **Detail total** | | **${totalPhase3Tokens}** | **${totalPhase3CostEur}** |
|
|
709
|
-
|
|
710
|
-
3. Update the existing "## Grand Total" table. Read the Phase 1 and Phase 2 actual totals from
|
|
711
|
-
the top of THIS SAME file (the "Phase 1 total" and "Phase 2 total" rows already present) and
|
|
712
|
-
copy them verbatim — never the literal text "(see above)". If a value there is "—" (Phase 1 is
|
|
713
|
-
written as a placeholder by pm-phase2 and filled later by the orchestrator), keep it as "—";
|
|
714
|
-
do NOT invent a number. The table must read:
|
|
715
|
-
|
|
716
|
-
| Phase | Tokens Est. | Est. cost € | Tokens Actual | Actual cost € | Delta (tokens) |
|
|
717
|
-
|-------|------------|------------|--------------|--------------|---------------|
|
|
718
|
-
| Phase 1 — Documentation | — | — | {Phase 1 total from this file, or — if placeholder} | — | — |
|
|
719
|
-
| Phase 2 — Work Breakdown | — | — | {Phase 2 total from this file} | {Phase 2 cost from this file} | — |
|
|
720
|
-
| Phase 3 — Implementation | {existing Phase 3 estimate total} | {existing Phase 3 est. cost} | ${totalPhase3Tokens} | ${totalPhase3CostEur} | {Actual − Est} |
|
|
721
|
-
| **Total** | **{sum of numeric Est}** | **{sum of numeric Est. cost}** | **{sum of numeric Actual}** | **{sum of numeric Actual cost}** | **{Actual − Est}** |
|
|
722
|
-
|
|
723
|
-
4. Append (or update if present) the Implementation Summary section:
|
|
724
|
-
|
|
725
|
-
## Implementation Summary
|
|
726
|
-
|
|
727
|
-
| Metric | Value |
|
|
728
|
-
|--------|-------|
|
|
729
|
-
| Implementation phases done | ${phasesDone} |
|
|
730
|
-
| US passed | ${usPassed.join(', ') || 'N/A'} |
|
|
731
|
-
| US escalated | ${usEscalated.join(', ') || 'none'} |
|
|
732
|
-
|
|
733
|
-
Remove the stale "*Actuals will be appended by pm-phase3...*" footer line if it is still present.
|
|
734
|
-
|
|
735
|
-
TASK 2 — Update actuals in ${prefix}-Effort-Estimate.md:
|
|
736
|
-
Find the file in the feature directory. Read the existing Per-Phase Breakdown table and update the "Actual Agent" column for each phase that was implemented. Use the token ledger to infer which phases completed:
|
|
737
|
-
- Phases in usPassed: ${usPassed.join(', ') || 'none'} — mark Actual Agent as completed (write the wall-clock duration if available, otherwise write "done (agent)")
|
|
738
|
-
- Phases escalated: ${usEscalated.join(', ') || 'none'} — mark Actual Agent as "escalated"
|
|
739
|
-
- Also append this summary section at the end of the file:
|
|
740
|
-
|
|
741
|
-
---
|
|
742
|
-
|
|
743
|
-
## Implementation Summary
|
|
744
|
-
|
|
745
|
-
| Metric | Estimated | Actual |
|
|
746
|
-
|--------|-----------|--------|
|
|
747
|
-
| Implementation phases | ${phasesDone} | ${phasesDone} |
|
|
748
|
-
| Issues fixed | — | ${issuesFixed} |
|
|
749
|
-
| Issues deferred | — | ${issuesDeferred} |
|
|
750
|
-
|
|
751
|
-
Return { "token_estimate_path": "<path>", "effort_estimate_path": "<path>" }.`,
|
|
752
|
-
{
|
|
753
|
-
label: 'write-actuals',
|
|
754
|
-
phase: 'Actuals',
|
|
755
|
-
schema: ACTUALS_SCHEMA,
|
|
756
|
-
}
|
|
757
|
-
)
|
|
758
|
-
|
|
759
|
-
const actualsTokens = budget.spent() - beforeActuals
|
|
760
|
-
await ledgerTerminal(
|
|
761
|
-
`ai-toolkit ledger close --dir ${featureDir} --prefix ${prefix} --agent ${actualsKey}${tokensAvailable(actualsTokens) ? ` --tokens ${actualsTokens}` : ''} --attempt 1`,
|
|
762
|
-
'ledger-close-write-actuals', 'Actuals'
|
|
763
|
-
)
|
|
764
|
-
tokenLedger.push({ agent: actualsKey, model: 'sonnet', phase_delta_tokens: actualsTokens })
|
|
765
|
-
log(`Actuals written — ${actualsTokens} tokens`)
|
|
766
|
-
|
|
767
|
-
// ── Append to process-log ─────────────────────────────────────────────────────
|
|
768
|
-
const usPassed3 = usPassed.join(', ') || 'none'
|
|
769
|
-
const usEscalated3 = usEscalated.join(', ') || 'none'
|
|
770
|
-
const phase3Events = [
|
|
771
|
-
`APPROVAL GRANTED by user — Gate 2`,
|
|
772
|
-
`pm-phase3 START — implementation phase — branch: ${branch || '(current)'}`,
|
|
773
|
-
`WB parsed: ${wb.phases.length} phases`,
|
|
774
|
-
`Implementation DONE — ${phasesDone} phases | US passed: ${usPassed3} | US escalated: ${usEscalated3}`,
|
|
775
|
-
issuesFixed + issuesDeferred > 0
|
|
776
|
-
? `Remediation: ${issuesFixed} fixed, ${issuesDeferred} deferred`
|
|
777
|
-
: `Remediation: skipped (no open issues)`,
|
|
778
|
-
`PR created: ${prResult.pr_url}`,
|
|
779
|
-
`Actuals written: Token-Estimate.md + Effort-Estimate.md`,
|
|
780
|
-
`RUN COMPLETE`,
|
|
781
|
-
`════════════════════════════════════════════════════════`,
|
|
782
|
-
].join('\n')
|
|
783
|
-
|
|
784
|
-
const featureDir3 = featurePath.replace(/\/[^/]+$/, '')
|
|
785
|
-
|
|
786
|
-
await agent(
|
|
787
|
-
`Append phase 3 events to the process-log for this feature delivery run.
|
|
788
|
-
|
|
789
|
-
Process-log path: ${featureDir3}/${prefix}-process-log.txt
|
|
790
|
-
|
|
791
|
-
Steps:
|
|
792
|
-
1. Get current UTC datetime via Bash: run \`date -u +"%Y-%m-%dT%H:%M:%S"\`
|
|
793
|
-
2. Append the following lines to the existing file (create it if it does not exist).
|
|
794
|
-
Format each event line as: [{datetime}] {event text}
|
|
795
|
-
The final separator line (════…) is appended as-is, without a timestamp.
|
|
796
|
-
|
|
797
|
-
Events to append:
|
|
798
|
-
${phase3Events}`,
|
|
799
|
-
{ label: 'finalize-process-log', phase: 'Actuals' }
|
|
800
|
-
)
|
|
801
|
-
|
|
802
|
-
await agent(
|
|
803
|
-
`Commit the actuals files for this feature delivery run.
|
|
804
|
-
|
|
805
|
-
Run these exact git commands in the repository root:
|
|
806
|
-
git add "${featureDir3}/${prefix}-Token-Estimate.md" "${featureDir3}/${prefix}-Effort-Estimate.md" "${featureDir3}/${prefix}-process-log.txt"
|
|
807
|
-
git commit -m "docs(${prefix}): add token/effort actuals and process log"
|
|
808
|
-
|
|
809
|
-
If there is nothing to commit (all files already committed), that is fine — report success.`,
|
|
810
|
-
{ label: 'commit-actuals', phase: 'Actuals' }
|
|
811
|
-
)
|
|
812
|
-
|
|
813
|
-
// ── Tier 3 self-registration — close on successful completion ─────────────────
|
|
814
|
-
await ledgerTerminal(
|
|
815
|
-
`ai-toolkit ledger close --prefix ${prefix} --agent pm-phase3:self --dir "${featureDir}" --attempt 1`,
|
|
816
|
-
'ledger-close-pm-phase3-self', 'Actuals'
|
|
817
|
-
)
|
|
818
|
-
|
|
819
|
-
return {
|
|
820
|
-
pr_url: prResult.pr_url,
|
|
821
|
-
token_ledger: tokenLedger,
|
|
822
|
-
issues_summary: {
|
|
823
|
-
escalations: escalations.length,
|
|
824
|
-
escalation_details: escalations,
|
|
825
|
-
issues_fixed: issuesFixed,
|
|
826
|
-
issues_deferred: issuesDeferred,
|
|
827
|
-
},
|
|
828
|
-
}
|
|
7
|
+
// Never dispatch this workflow as an agentType. The old CSV/ledger/CLI wrappers
|
|
8
|
+
// bypassed FTR-018 ownership, checkpoints and resume. Do not restore that path.
|
|
9
|
+
throw new Error('pm-phase3 is retired. Use ai-toolkit tasks start/run as documented in docs/task-executor-bootstrap.md. No implementation was started.')
|