@dtlabs/ai-toolkit 0.13.0 → 0.14.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/CHANGELOG.md +44 -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 +95 -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 +219 -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/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,575 +1,54 @@
|
|
|
1
1
|
export const meta = {
|
|
2
|
+
controlPlaneVersion: 1,
|
|
2
3
|
name: 'pm-phase2',
|
|
3
|
-
description: '
|
|
4
|
-
phases: [
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
const
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
// ── Tier 2 resolution guard ───────────────────────────────────────────────────
|
|
53
|
-
// All worker agents must be resolved via the CLI before dispatch.
|
|
54
|
-
// No direct require('lib/agent-registry') — resolution goes through the CLI facade.
|
|
55
|
-
const RESOLVE_SCHEMA = {
|
|
56
|
-
type: 'object',
|
|
57
|
-
properties: {
|
|
58
|
-
nativeName: { type: 'string' },
|
|
59
|
-
status: { type: 'string' },
|
|
60
|
-
agentId: { type: 'string' },
|
|
61
|
-
exitNonZero: { type: 'boolean' },
|
|
62
|
-
error: { type: 'string' },
|
|
63
|
-
},
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
async function resolveAgentTier2(agentId, label, phaseName) {
|
|
67
|
-
const result = await agent(
|
|
68
|
-
`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}"}.`,
|
|
69
|
-
{ label, phase: phaseName, model: 'haiku', schema: RESOLVE_SCHEMA }
|
|
70
|
-
)
|
|
71
|
-
if (!result || result.exitNonZero || !result.nativeName) {
|
|
72
|
-
throw new Error(`HARD STOP — Tier 2 resolution failed for ${agentId}: ${result && result.error ? result.error : 'no nativeName returned'}`)
|
|
4
|
+
description: 'Generate, validate and render a work breakdown with deterministic control-plane operations.',
|
|
5
|
+
phases: [{ title: 'Work Breakdown' }, { title: 'Effort Estimate' }],
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
const c = control
|
|
11
|
+
const { featurePath, featureDir, prefix } = c.feature(args[0])
|
|
12
|
+
return c.run(meta.name, featureDir, prefix, async () => {
|
|
13
|
+
const file = suffix => `${featureDir}/${prefix}-${suffix}`
|
|
14
|
+
const approvals = c.read(file('Approvals.md'))
|
|
15
|
+
const gate = approvals.match(/^## Gate 1\b[^\n]*\n([\s\S]*?)(?=^## |$(?![\s\S]))/m)
|
|
16
|
+
if (!gate || !/✅\s*Approved/.test(gate[1])) throw new Error('Gate 1 approval evidence missing')
|
|
17
|
+
await c.worker('gaia.agent.planner.work-breakdown', featurePath)
|
|
18
|
+
const jsonPath = file('Work-Breakdown.json')
|
|
19
|
+
const report = await c.activity('wb-validate', async () => {
|
|
20
|
+
const res = c.command(['run-asset', 'scripts/wb-validate.js', '--project', c.root,
|
|
21
|
+
'--', jsonPath, file('Requirements.md')], [0, 1])
|
|
22
|
+
const parsed = JSON.parse(res.stdout)
|
|
23
|
+
if (typeof parsed.valid !== 'boolean' || !Array.isArray(parsed.errors) ||
|
|
24
|
+
!Array.isArray(parsed.warnings) || (res.exitCode === 0) !== parsed.valid) throw new Error('Invalid validator result')
|
|
25
|
+
if (!parsed.valid || parsed.errors.length) throw new Error('Work breakdown structural validation failed: ' + res.stdout)
|
|
26
|
+
return parsed
|
|
27
|
+
})
|
|
28
|
+
const semantic = await c.worker('gaia.agent.planner.validate-work-breakdown',
|
|
29
|
+
`${jsonPath}\n${file('Requirements.md')}`, { schema: {
|
|
30
|
+
type: 'object', properties: { valid: { type: 'boolean' }, findings: { type: 'array' } }, required: ['valid', 'findings'],
|
|
31
|
+
} })
|
|
32
|
+
if (!semantic.valid || semantic.findings.some(f => f.blocking)) throw new Error('Work breakdown semantic validation failed')
|
|
33
|
+
const rendered = await c.activity('wb-render', async () => {
|
|
34
|
+
const result = c.command(['run-asset', 'scripts/wb-render.js', '--project', c.root, '--', jsonPath, prefix])
|
|
35
|
+
const markdownPath = file('Work-Breakdown.md'), csvPath = file('Work-Breakdown.csv')
|
|
36
|
+
if (!c.exists(markdownPath) || !c.exists(csvPath)) throw new Error('Renderer outputs missing')
|
|
37
|
+
// Read the actual bytes now: unreadable output must never produce an approved gate.
|
|
38
|
+
c.read(markdownPath); c.read(csvPath)
|
|
39
|
+
return { ...result, markdownPath, csvPath, markdownExists: true, csvExists: true }
|
|
40
|
+
})
|
|
41
|
+
const metrics = await c.estimates(featureDir, prefix, c.json(jsonPath))
|
|
42
|
+
c.append(file('process-log.txt'), `[${new Date().toISOString()}] pm-phase2 complete; Gate 2 approval requested\n`)
|
|
43
|
+
return {
|
|
44
|
+
...metrics, feature_path: featurePath, token_ledger: c.tokenLedger, errors: [],
|
|
45
|
+
gate2_payload: {
|
|
46
|
+
js_validator_report: report, js_validator_failed: false,
|
|
47
|
+
semantic_validator_result: semantic, semantic_validator_failed: false,
|
|
48
|
+
renderer_result: rendered, renderer_failed: false, gate2_blocked: false,
|
|
49
|
+
duration_bands: report.durationBands, domain_distribution: report.domainDistribution,
|
|
50
|
+
warning_band_tasks: report.warnings.filter(w => w.category === 'duration_warning').map(w => ({ taskId: w.taskId, agentMinutes: w.details?.agentMinutes })),
|
|
51
|
+
split_required_tasks: [], must_ac_uncovered: [], phase_unschedulable: [],
|
|
52
|
+
},
|
|
73
53
|
}
|
|
74
|
-
return result.nativeName
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
// ── Parse args ────────────────────────────────────────────────────────────────
|
|
78
|
-
|
|
79
|
-
// args: "<path-to-feature.md>"
|
|
80
|
-
const featurePath = (typeof args === 'string' ? args : '').trim().split(/\s+/)[0]
|
|
81
|
-
|
|
82
|
-
// Feature directory = parent of the feature.md path. Separator-agnostic: the workflow
|
|
83
|
-
// runtime provides no `path` module, so we cannot use path.dirname. Handle BOTH POSIX
|
|
84
|
-
// "/" and Windows "\": strip a trailing "<sep>feature.md" tail when present; if the
|
|
85
|
-
// input is already a directory (no feature.md tail), strip only a trailing separator.
|
|
86
|
-
// This replaces the previous POSIX-only regex (/\/[^/]+$/), which left "\feature.md"
|
|
87
|
-
// attached on Windows paths and produced malformed "…\feature.md/…json" paths that only
|
|
88
|
-
// "worked" because an LLM wrapper agent silently repaired them.
|
|
89
|
-
function deriveFeatureDir(featurePath) {
|
|
90
|
-
return /[/\\]feature\.md$/i.test(featurePath)
|
|
91
|
-
? featurePath.replace(/[/\\]feature\.md$/i, '')
|
|
92
|
-
: featurePath.replace(/[/\\]+$/, '')
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
// Derive feature directory and prefix early (needed for ledger writes before metrics agent runs)
|
|
96
|
-
const featureDir = deriveFeatureDir(featurePath)
|
|
97
|
-
const prefixMatch = featureDir.match(/([A-Z]+-\d+)/)
|
|
98
|
-
const prefix = prefixMatch ? prefixMatch[1] : 'FTR-000'
|
|
99
|
-
|
|
100
|
-
// ── Tier 3 — self-ledger helper ───────────────────────────────────────────────
|
|
101
|
-
const SELF_LEDGER_SCHEMA = {
|
|
102
|
-
type: 'object',
|
|
103
|
-
properties: {
|
|
104
|
-
exitCode: { type: 'number' },
|
|
105
|
-
stdout: { type: 'string' },
|
|
106
|
-
stderr: { type: 'string' },
|
|
107
|
-
},
|
|
108
|
-
required: ['exitCode'],
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
async function selfLedgerOp(cmd, label, phaseName) {
|
|
112
|
-
const result = await agent(
|
|
113
|
-
`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.`,
|
|
114
|
-
{ label, phase: phaseName, model: 'haiku', schema: SELF_LEDGER_SCHEMA }
|
|
115
|
-
)
|
|
116
|
-
const status = (result && typeof result.exitCode === 'number') ? result.exitCode : 1
|
|
117
|
-
if (status !== 0) {
|
|
118
|
-
throw new Error(`HARD STOP — self-ledger operation failed. Exit code: ${status}. Command: ${cmd}`)
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
// ── Tier 3 self-registration (open BEFORE any main logic, fail-closed) ────────
|
|
123
|
-
await selfLedgerOp(
|
|
124
|
-
`ai-toolkit ledger open --prefix ${prefix} --agent pm-phase2:self --phase phase2 --dir "${featureDir}" --attempt 1`,
|
|
125
|
-
'ledger-open-pm-phase2-self', 'Work Breakdown'
|
|
126
|
-
)
|
|
127
|
-
|
|
128
|
-
// ── generate-work-breakdown ───────────────────────────────────────────────────
|
|
129
|
-
phase('Work Breakdown')
|
|
130
|
-
|
|
131
|
-
const tokenLedger = []
|
|
132
|
-
|
|
133
|
-
log(`Running generate-work-breakdown for ${featurePath}`)
|
|
134
|
-
const wbNativeName = await resolveAgentTier2('gaia.agent.planner.work-breakdown', 'resolve-generate-work-breakdown', 'Work Breakdown')
|
|
135
|
-
const wbKey = 'generate-work-breakdown:phase2'
|
|
136
|
-
await agent(
|
|
137
|
-
`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 ${wbKey} --phase phase2 --model haiku --attempt 1\n\nReturn no output.`,
|
|
138
|
-
{ label: 'ledger-open-generate-work-breakdown', phase: 'Work Breakdown', model: 'haiku' }
|
|
139
|
-
)
|
|
140
|
-
const beforeWB = budget.spent()
|
|
141
|
-
await agent(featurePath, {
|
|
142
|
-
agentType: wbNativeName,
|
|
143
|
-
label: 'generate-work-breakdown',
|
|
144
|
-
phase: 'Work Breakdown',
|
|
145
54
|
})
|
|
146
|
-
const wbTokens = budget.spent() - beforeWB
|
|
147
|
-
await agent(
|
|
148
|
-
`Run this shell command via Bash. If the --dir path contains spaces, enclose it in double quotes.\n\nai-toolkit ledger close --dir ${featureDir} --prefix ${prefix} --agent ${wbKey} --tokens ${wbTokens} --attempt 1\n\nReturn no output.`,
|
|
149
|
-
{ label: 'ledger-close-generate-work-breakdown', phase: 'Work Breakdown', model: 'haiku' }
|
|
150
|
-
)
|
|
151
|
-
tokenLedger.push({ agent: 'generate-work-breakdown', model: 'haiku', phase_delta_tokens: wbTokens })
|
|
152
|
-
log(`generate-work-breakdown done — phase delta: ${wbTokens} tokens`)
|
|
153
|
-
|
|
154
|
-
// ── Step 2: wb-validate.js (deterministic structural validator) ───────────────
|
|
155
|
-
let wbValidatorReport = null
|
|
156
|
-
let validateFailed = false
|
|
157
|
-
let validateExitCode = null
|
|
158
|
-
const wbValidateKey = 'wb-validate:phase2'
|
|
159
|
-
await agent(
|
|
160
|
-
`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 ${wbValidateKey} --phase phase2 --model haiku --attempt 1\n\nReturn no output.`,
|
|
161
|
-
{ label: 'ledger-open-wb-validate', phase: 'Work Breakdown', model: 'haiku' }
|
|
162
|
-
)
|
|
163
|
-
const beforeValidate = budget.spent()
|
|
164
|
-
try {
|
|
165
|
-
const validateWrapper = await agent(
|
|
166
|
-
`Run this command and return the result as structured output:\n` +
|
|
167
|
-
`ai-toolkit run-asset scripts/wb-validate.js -- "${featureDir}/${prefix}-Work-Breakdown.json" "${featureDir}/${prefix}-Requirements.md"\n\n` +
|
|
168
|
-
`Capture exitCode (integer), stdout (string), stderr (string). Return all three.`,
|
|
169
|
-
{
|
|
170
|
-
label: 'wb-validate',
|
|
171
|
-
phase: 'Work Breakdown',
|
|
172
|
-
model: 'haiku',
|
|
173
|
-
schema: WB_WRAPPER_SCHEMA,
|
|
174
|
-
}
|
|
175
|
-
)
|
|
176
|
-
validateExitCode = validateWrapper.exitCode
|
|
177
|
-
if (validateWrapper.stderr?.trim()) {
|
|
178
|
-
log(`wb-validate stderr: ${validateWrapper.stderr.trim()}`)
|
|
179
|
-
}
|
|
180
|
-
if (validateWrapper.exitCode === 2) {
|
|
181
|
-
throw Object.assign(
|
|
182
|
-
new Error(validateWrapper.stderr?.trim() || 'wb-validate runtime error (exit 2)'),
|
|
183
|
-
{ _exitCode: validateWrapper.exitCode }
|
|
184
|
-
)
|
|
185
|
-
}
|
|
186
|
-
if (![0, 1].includes(validateWrapper.exitCode)) {
|
|
187
|
-
throw Object.assign(
|
|
188
|
-
new Error(`wb-validate unexpected exit code: ${validateWrapper.exitCode}`),
|
|
189
|
-
{ _exitCode: validateWrapper.exitCode }
|
|
190
|
-
)
|
|
191
|
-
}
|
|
192
|
-
if (!validateWrapper.stdout?.trim()) {
|
|
193
|
-
throw new Error('wb-validate returned empty stdout')
|
|
194
|
-
}
|
|
195
|
-
wbValidatorReport = JSON.parse(validateWrapper.stdout)
|
|
196
|
-
if (validateWrapper.exitCode === 0 && !wbValidatorReport.valid) {
|
|
197
|
-
throw new Error('Inconsistent wb-validate result: exit 0 with valid=false')
|
|
198
|
-
}
|
|
199
|
-
if (validateWrapper.exitCode === 1 && wbValidatorReport.valid) {
|
|
200
|
-
throw new Error('Inconsistent wb-validate result: exit 1 with valid=true')
|
|
201
|
-
}
|
|
202
|
-
} catch (err) {
|
|
203
|
-
validateFailed = true
|
|
204
|
-
}
|
|
205
|
-
const validateTokens = budget.spent() - beforeValidate
|
|
206
|
-
await agent(
|
|
207
|
-
`Run this shell command via Bash. If the --dir path contains spaces, enclose it in double quotes.\n\nai-toolkit ledger close --dir ${featureDir} --prefix ${prefix} --agent ${wbValidateKey} --tokens ${validateTokens} --attempt 1\n\nReturn no output.`,
|
|
208
|
-
{ label: 'ledger-close-wb-validate', phase: 'Work Breakdown', model: 'haiku' }
|
|
209
|
-
)
|
|
210
|
-
const wbValidatorPassed = !validateFailed && wbValidatorReport !== null
|
|
211
|
-
&& wbValidatorReport.valid === true && wbValidatorReport.errors.length === 0
|
|
212
|
-
log(`wb-validate: ${wbValidatorPassed ? 'passed' : 'failed/errors'} — ${validateTokens} tokens`)
|
|
213
|
-
|
|
214
|
-
// ── Step 3: Semantic validator (only if wb-validate passed) ───────────────────
|
|
215
|
-
let semanticResult = null
|
|
216
|
-
let semanticFailed = false
|
|
217
|
-
let semanticTokens = 0
|
|
218
|
-
const semanticKey = 'validate-work-breakdown-semantic:phase2'
|
|
219
|
-
if (!wbValidatorPassed) {
|
|
220
|
-
log('validate-work-breakdown-semantic: skipped (wb-validate did not pass)')
|
|
221
|
-
} else {
|
|
222
|
-
const semanticNativeName = await resolveAgentTier2('gaia.agent.planner.validate-work-breakdown', 'resolve-validate-work-breakdown-semantic', 'Work Breakdown')
|
|
223
|
-
await agent(
|
|
224
|
-
`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 ${semanticKey} --phase phase2 --model sonnet --attempt 1\n\nReturn no output.`,
|
|
225
|
-
{ label: 'ledger-open-semantic-validator', phase: 'Work Breakdown', model: 'haiku' }
|
|
226
|
-
)
|
|
227
|
-
const beforeSemantic = budget.spent()
|
|
228
|
-
try {
|
|
229
|
-
semanticResult = await agent(
|
|
230
|
-
`${featureDir}/${prefix}-Work-Breakdown.json\n${featureDir}/${prefix}-Requirements.md`,
|
|
231
|
-
{
|
|
232
|
-
label: 'validate-work-breakdown-semantic',
|
|
233
|
-
phase: 'Work Breakdown',
|
|
234
|
-
agentType: semanticNativeName,
|
|
235
|
-
schema: WB_SEMANTIC_SCHEMA,
|
|
236
|
-
}
|
|
237
|
-
)
|
|
238
|
-
} catch (err) {
|
|
239
|
-
semanticFailed = true
|
|
240
|
-
}
|
|
241
|
-
semanticTokens = budget.spent() - beforeSemantic
|
|
242
|
-
await agent(
|
|
243
|
-
`Run this shell command via Bash. If the --dir path contains spaces, enclose it in double quotes.\n\nai-toolkit ledger close --dir ${featureDir} --prefix ${prefix} --agent ${semanticKey} --tokens ${semanticTokens} --attempt 1\n\nReturn no output.`,
|
|
244
|
-
{ label: 'ledger-close-semantic-validator', phase: 'Work Breakdown', model: 'haiku' }
|
|
245
|
-
)
|
|
246
|
-
}
|
|
247
|
-
log(`semantic validator: ${semanticFailed ? 'failed' : semanticResult ? 'done' : 'skipped'}`)
|
|
248
|
-
|
|
249
|
-
// ── Step 4: wb-render.js (only if wb-validate passed and semantic did not fail) ─
|
|
250
|
-
const canRender = wbValidatorPassed && !semanticFailed
|
|
251
|
-
let renderFailed = false
|
|
252
|
-
let renderResult = null
|
|
253
|
-
const wbRenderKey = 'wb-render:phase2'
|
|
254
|
-
if (!canRender) {
|
|
255
|
-
log('wb-render: skipped (wb-validate or semantic validator did not pass)')
|
|
256
|
-
} else {
|
|
257
|
-
await agent(
|
|
258
|
-
`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 ${wbRenderKey} --phase phase2 --model haiku --attempt 1\n\nReturn no output.`,
|
|
259
|
-
{ label: 'ledger-open-wb-render', phase: 'Work Breakdown', model: 'haiku' }
|
|
260
|
-
)
|
|
261
|
-
const beforeRender = budget.spent()
|
|
262
|
-
try {
|
|
263
|
-
renderResult = await agent(
|
|
264
|
-
`Run this command and return the result as structured output:\n` +
|
|
265
|
-
`ai-toolkit run-asset scripts/wb-render.js -- "${featureDir}/${prefix}-Work-Breakdown.json" "${prefix}"\n\n` +
|
|
266
|
-
`Capture exitCode (integer), stdout (string), stderr (string).\n` +
|
|
267
|
-
`Resolve the expected output paths:\n` +
|
|
268
|
-
` markdownPath = "${featureDir}/${prefix}-Work-Breakdown.md"\n` +
|
|
269
|
-
` csvPath = "${featureDir}/${prefix}-Work-Breakdown.csv"\n` +
|
|
270
|
-
`Check whether each file exists on disk and return:\n` +
|
|
271
|
-
` markdownExists (boolean), csvExists (boolean).\n` +
|
|
272
|
-
`Return all seven fields.`,
|
|
273
|
-
{
|
|
274
|
-
label: 'wb-render',
|
|
275
|
-
phase: 'Work Breakdown',
|
|
276
|
-
model: 'haiku',
|
|
277
|
-
schema: WB_RENDER_SCHEMA,
|
|
278
|
-
}
|
|
279
|
-
)
|
|
280
|
-
if (renderResult.stderr?.trim()) {
|
|
281
|
-
log(`wb-render stderr: ${renderResult.stderr.trim()}`)
|
|
282
|
-
}
|
|
283
|
-
if (renderResult.exitCode !== 0) {
|
|
284
|
-
throw Object.assign(
|
|
285
|
-
new Error(`wb-render exited ${renderResult.exitCode}: ${renderResult.stderr?.trim() || renderResult.stdout?.trim()}`),
|
|
286
|
-
{ _exitCode: renderResult.exitCode }
|
|
287
|
-
)
|
|
288
|
-
}
|
|
289
|
-
if (!renderResult.markdownPath?.trim()) throw new Error('wb-render did not return markdownPath')
|
|
290
|
-
if (!renderResult.csvPath?.trim()) throw new Error('wb-render did not return csvPath')
|
|
291
|
-
if (!renderResult.markdownExists) {
|
|
292
|
-
throw Object.assign(
|
|
293
|
-
new Error(`wb-render output file not found: ${renderResult.markdownPath}`),
|
|
294
|
-
{ _exitCode: renderResult.exitCode }
|
|
295
|
-
)
|
|
296
|
-
}
|
|
297
|
-
if (!renderResult.csvExists) {
|
|
298
|
-
throw Object.assign(
|
|
299
|
-
new Error(`wb-render output file not found: ${renderResult.csvPath}`),
|
|
300
|
-
{ _exitCode: renderResult.exitCode }
|
|
301
|
-
)
|
|
302
|
-
}
|
|
303
|
-
} catch (err) {
|
|
304
|
-
renderFailed = true
|
|
305
|
-
}
|
|
306
|
-
const renderTokens = budget.spent() - beforeRender
|
|
307
|
-
await agent(
|
|
308
|
-
`Run this shell command via Bash. If the --dir path contains spaces, enclose it in double quotes.\n\nai-toolkit ledger close --dir ${featureDir} --prefix ${prefix} --agent ${wbRenderKey} --tokens ${renderTokens} --attempt 1\n\nReturn no output.`,
|
|
309
|
-
{ label: 'ledger-close-wb-render', phase: 'Work Breakdown', model: 'haiku' }
|
|
310
|
-
)
|
|
311
|
-
tokenLedger.push({ agent: 'wb-render', model: 'haiku', phase_delta_tokens: renderTokens })
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
tokenLedger.push({ agent: 'wb-validate', model: 'haiku', phase_delta_tokens: validateTokens })
|
|
315
|
-
tokenLedger.push({ agent: 'validate-work-breakdown-semantic', model: 'sonnet', phase_delta_tokens: semanticTokens })
|
|
316
|
-
|
|
317
|
-
// ── Step 5: Assemble gate2_payload ────────────────────────────────────────────
|
|
318
|
-
const gate2_payload = {
|
|
319
|
-
js_validator_report: wbValidatorReport,
|
|
320
|
-
js_validator_failed: validateFailed,
|
|
321
|
-
semantic_validator_result: semanticResult,
|
|
322
|
-
semantic_validator_failed: semanticFailed,
|
|
323
|
-
renderer_result: renderResult,
|
|
324
|
-
renderer_failed: renderFailed,
|
|
325
|
-
duration_bands: wbValidatorReport ? wbValidatorReport.durationBands : null,
|
|
326
|
-
domain_distribution: wbValidatorReport ? wbValidatorReport.domainDistribution : null,
|
|
327
|
-
warning_band_tasks: wbValidatorReport
|
|
328
|
-
? wbValidatorReport.warnings.filter(w => w.category === 'duration_warning')
|
|
329
|
-
.map(w => ({ taskId: w.taskId, agentMinutes: w.details ? w.details.agentMinutes : null }))
|
|
330
|
-
: [],
|
|
331
|
-
split_required_tasks: wbValidatorReport
|
|
332
|
-
? wbValidatorReport.errors.filter(e => e.category === 'split_required')
|
|
333
|
-
.map(e => ({ taskId: e.taskId, agentMinutes: e.details ? e.details.agentMinutes : null }))
|
|
334
|
-
: [],
|
|
335
|
-
must_ac_uncovered: wbValidatorReport
|
|
336
|
-
? wbValidatorReport.errors.filter(e => e.category === 'must_ac_uncovered')
|
|
337
|
-
.map(e => ({ acId: e.details ? e.details.acId : null }))
|
|
338
|
-
: [],
|
|
339
|
-
phase_unschedulable: wbValidatorReport
|
|
340
|
-
? (wbValidatorReport.dependencies ? wbValidatorReport.dependencies.phaseUnschedulable || [] : [])
|
|
341
|
-
: [],
|
|
342
|
-
gate2_blocked:
|
|
343
|
-
validateFailed ||
|
|
344
|
-
!wbValidatorPassed ||
|
|
345
|
-
semanticFailed ||
|
|
346
|
-
renderFailed ||
|
|
347
|
-
Boolean(semanticResult && semanticResult.findings && semanticResult.findings.some(f => f.blocking)),
|
|
348
|
-
}
|
|
349
|
-
log(`gate2_payload assembled — gate2_blocked: ${gate2_payload.gate2_blocked}`)
|
|
350
|
-
|
|
351
|
-
// ── Parse WB + write Effort-Estimate + Token-Estimate ────────────────────────
|
|
352
|
-
phase('Effort Estimate')
|
|
353
|
-
|
|
354
|
-
const PARSE_SCHEMA = {
|
|
355
|
-
type: 'object',
|
|
356
|
-
properties: {
|
|
357
|
-
prefix: { type: 'string' },
|
|
358
|
-
feature_dir: { type: 'string' },
|
|
359
|
-
user_stories: { type: 'number' },
|
|
360
|
-
total_tasks: { type: 'number' },
|
|
361
|
-
domain_breakdown: { type: 'string' },
|
|
362
|
-
implementation_phases: { type: 'number' },
|
|
363
|
-
human_estimate: { type: 'string' },
|
|
364
|
-
agent_estimate: { type: 'string' },
|
|
365
|
-
work_breakdown_path: { type: 'string' },
|
|
366
|
-
effort_estimate_path: { type: 'string' },
|
|
367
|
-
token_estimate_path: { type: 'string' },
|
|
368
|
-
tasks_csv_path: { type: 'string' },
|
|
369
|
-
},
|
|
370
|
-
required: ['prefix', 'user_stories', 'total_tasks', 'work_breakdown_path'],
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
// ── Load token pricing ────────────────────────────────────────────────────────
|
|
374
|
-
// Pricing config is read inline (haiku agent) so it survives path-agnostic installs.
|
|
375
|
-
// Cost formula: tokens * (0.8 * input + 0.2 * output) / 1_000_000 * usd_to_eur
|
|
376
|
-
// (80/20 input/output split — no per-agent breakdown available in the ledger)
|
|
377
|
-
const pricingRaw = await agent(
|
|
378
|
-
`Read the file docs/token-pricing.json and return its raw JSON contents as a string, nothing else.`,
|
|
379
|
-
{ label: 'read-pricing', phase: 'Effort Estimate', model: 'haiku' }
|
|
380
|
-
)
|
|
381
|
-
let pricing = null
|
|
382
|
-
try { pricing = JSON.parse(typeof pricingRaw === 'string' ? pricingRaw.trim() : '{}') } catch (_) {}
|
|
383
|
-
|
|
384
|
-
function tokenCostEur(tokens, model) {
|
|
385
|
-
if (!pricing || !pricing.models) return null
|
|
386
|
-
const m = pricing.models[model] || pricing.models['sonnet']
|
|
387
|
-
const usdPer1M = 0.8 * m.input_per_1m_usd + 0.2 * m.output_per_1m_usd
|
|
388
|
-
return (tokens * usdPer1M / 1_000_000) * (pricing.usd_to_eur || 1)
|
|
389
|
-
}
|
|
390
|
-
|
|
391
|
-
function formatEur(val) {
|
|
392
|
-
if (val === null || val === undefined) return '—'
|
|
393
|
-
return '€' + val.toFixed(4)
|
|
394
|
-
}
|
|
395
|
-
|
|
396
|
-
// NOTE: pm-phase2 is a separate workflow and does NOT receive pm-phase1's token ledger,
|
|
397
|
-
// so the real Phase 1 actuals (generate-requirements/tech-spec/validate) are unknown here.
|
|
398
|
-
// They are written as "—" placeholders below and filled later by the orchestrator (the
|
|
399
|
-
// implement-feature skill Step 7, which holds all three phase ledgers). Do NOT reuse wbTokens
|
|
400
|
-
// as a stand-in for Phase 1 — that mislabels Phase 2's tokens as Phase 1's.
|
|
401
|
-
|
|
402
|
-
const metrics = await agent(
|
|
403
|
-
`You have four tasks:
|
|
404
|
-
|
|
405
|
-
TASK 1 — Read the Work Breakdown JSON file.
|
|
406
|
-
The feature.md is at: ${featurePath}
|
|
407
|
-
The JSON work breakdown is at: ${featureDir}/${prefix}-Work-Breakdown.json
|
|
408
|
-
Read the JSON file and extract:
|
|
409
|
-
- prefix (e.g. "FTR-009") — use "${prefix}"
|
|
410
|
-
- feature_dir (directory containing feature.md) — use "${featureDir}"
|
|
411
|
-
- feature_title — from wb.title or wb.feature field
|
|
412
|
-
- total_tasks (integer) — count all tasks across all phases
|
|
413
|
-
- user_stories (integer) — count phases whose id starts with "US-"
|
|
414
|
-
- domain_breakdown (string) — e.g. "BE: N, FE: N, DB: N, INFRA: N, TEST: N"
|
|
415
|
-
- implementation_phases (integer) — total number of phases
|
|
416
|
-
- human_estimate — compute as: (BE_tasks * 120 + TEST_tasks * 60 + INFRA_tasks * 30 + FE_tasks * 90 + DB_tasks * 90) minutes, format as "~Xh Ymin"
|
|
417
|
-
- agent_estimate — human_estimate / 4 (parallel dispatch), format as "~Xmin" or "~Xh Ymin"
|
|
418
|
-
- work_breakdown_path — ${featureDir}/${prefix}-Work-Breakdown.json
|
|
419
|
-
|
|
420
|
-
Also extract per-User-Story details for the Effort-Estimate:
|
|
421
|
-
- US ID (phase.id where it starts with "US-")
|
|
422
|
-
- US title (phase.title)
|
|
423
|
-
- task count (phase.tasks.length)
|
|
424
|
-
- domains involved (unique task.domain values in the phase)
|
|
425
|
-
- estimated hours (per the formula above)
|
|
426
|
-
|
|
427
|
-
TASK 2 — Write the Effort-Estimate.md file.
|
|
428
|
-
Write {feature_dir}/{PREFIX}-Effort-Estimate.md with this format:
|
|
429
|
-
|
|
430
|
-
# Effort Estimate — {PREFIX} — {feature_title}
|
|
431
|
-
|
|
432
|
-
## Summary
|
|
433
|
-
|
|
434
|
-
| Metric | Value |
|
|
435
|
-
|--------|-------|
|
|
436
|
-
| User Stories | {user_stories} |
|
|
437
|
-
| Total tasks | {total_tasks} ({domain_breakdown}) |
|
|
438
|
-
| Implementation phases | {implementation_phases} |
|
|
439
|
-
| Human estimate | {human_estimate} (sequential, no parallelism) |
|
|
440
|
-
| Agent estimate | {agent_estimate} (parallel dispatch, critical path only) |
|
|
441
|
-
|
|
442
|
-
## Per-Phase Breakdown
|
|
443
|
-
|
|
444
|
-
| Phase | Title | Tasks | Domains | Est. Human | Est. Agent | Actual Human | Actual Agent |
|
|
445
|
-
|-------|-------|-------|---------|-----------|-----------|-------------|-------------|
|
|
446
|
-
{one row per phase (INFRA + each US) extracted from Section 4 of the Work Breakdown — use ~2h per BE task, ~1h per TEST task, ~30min per INFRA task for human estimate; agent estimate = human / 6 as baseline}
|
|
447
|
-
| **Total** | | **{total_tasks}** | | **{human_estimate}** | **{agent_estimate}** | **—** | **—** |
|
|
448
|
-
|
|
449
|
-
## Notes
|
|
450
|
-
- Human estimate assumes sequential execution with no parallelism.
|
|
451
|
-
- Agent estimate assumes parallel dispatch of independent tasks within each phase (critical path only).
|
|
452
|
-
- Actual Human: filled in if a human developer performed or reviewed the implementation.
|
|
453
|
-
- Actual Agent: filled in by pm-phase3 after implementation completes.
|
|
454
|
-
|
|
455
|
-
TASK 3 — Write the Token-Estimate.md file.
|
|
456
|
-
Write {feature_dir}/{PREFIX}-Token-Estimate.md with this format:
|
|
457
|
-
|
|
458
|
-
# Token Estimate — {PREFIX} — {feature_title}
|
|
459
|
-
|
|
460
|
-
## Phase 1 — Documentation (Actuals)
|
|
461
|
-
|
|
462
|
-
Phase 1 ran in a separate workflow (pm-phase1); its per-agent token actuals are filled in
|
|
463
|
-
by the orchestrator after implementation. Leave the Tokens Actual cells as "—" here.
|
|
464
|
-
|
|
465
|
-
| Agent | Task | Model | Tokens Est. | Est. cost € | Tokens Actual | Actual cost € |
|
|
466
|
-
|-------|------|-------|------------|------------|--------------|--------------|
|
|
467
|
-
| generate-requirements | Generate requirements from feature.md | haiku | — | — | — | — |
|
|
468
|
-
| generate-tech-spec | Generate tech spec from feature.md | haiku | — | — | — | — |
|
|
469
|
-
| validate-feature-docs | Validate requirements + tech spec | haiku | — | — | — | — |
|
|
470
|
-
| **Phase 1 total** | | | **—** | **—** | **—** | **—** |
|
|
471
|
-
|
|
472
|
-
## Phase 2 — Work Breakdown (Actuals)
|
|
473
|
-
|
|
474
|
-
| Agent | Task | Model | Tokens Est. | Est. cost € | Tokens Actual | Actual cost € |
|
|
475
|
-
|-------|------|-------|------------|------------|--------------|--------------|
|
|
476
|
-
| generate-work-breakdown | Generate work breakdown from docs | haiku | — | — | {wbTokens from above} | ${formatEur(tokenCostEur(wbTokens, 'haiku'))} |
|
|
477
|
-
| **Phase 2 total** | | | **—** | **—** | **{wbTokens from above}** | **${formatEur(tokenCostEur(wbTokens, 'haiku'))}** |
|
|
478
|
-
|
|
479
|
-
## Phase 3 — Implementation (Estimates)
|
|
480
|
-
|
|
481
|
-
Estimates based on {total_tasks} tasks ({domain_breakdown}), {user_stories} User Stories.
|
|
482
|
-
Baseline: ~15,000 tokens/BE task, ~8,000/TEST task, ~5,000/INFRA task.
|
|
483
|
-
|
|
484
|
-
| Agent | Task | Model | Tokens Est. | Est. cost € | Tokens Actual | Actual cost € |
|
|
485
|
-
|-------|------|-------|------------|------------|--------------|--------------|
|
|
486
|
-
| developer-backend | Implement BE/INFRA tasks | sonnet | {estimated: be_tasks * 15000 + infra_tasks * 5000} | {cost for that estimate, sonnet} | — | — |
|
|
487
|
-
| developer-testing | Implement TEST tasks | sonnet | {estimated: test_tasks * 8000} | {cost for that estimate, sonnet} | — | — |
|
|
488
|
-
| review-solution (×{user_stories}) | Architect review per US | sonnet | {estimated: user_stories * 8000} | {cost for that estimate, sonnet} | — | — |
|
|
489
|
-
| remediation | Fix review issues | sonnet | ~10,000 | {cost for 10000, sonnet} | — | — |
|
|
490
|
-
| pr-and-registry | Push branch, create PR | sonnet | ~5,000 | {cost for 5000, sonnet} | — | — |
|
|
491
|
-
| write-actuals | Update Token/Effort Estimate | sonnet | ~3,000 | {cost for 3000, sonnet} | — | — |
|
|
492
|
-
| **Phase 3 total** | | | **{sum of phase 3 token estimates}** | **{sum of phase 3 cost estimates}** | **—** | **—** |
|
|
493
|
-
|
|
494
|
-
For the "Est. cost €" column in Phase 3: use the formula tokens * (0.8 * 3.00 + 0.2 * 15.00) / 1_000_000 * ${pricing ? pricing.usd_to_eur : 0.92} for sonnet rows. Round to 4 decimal places, prefix with €.
|
|
495
|
-
|
|
496
|
-
## Grand Total
|
|
497
|
-
|
|
498
|
-
| Phase | Tokens Est. | Est. cost € | Tokens Actual | Actual cost € |
|
|
499
|
-
|-------|------------|------------|--------------|--------------|
|
|
500
|
-
| Phase 1 — Documentation | — | — | — (filled by orchestrator) | — |
|
|
501
|
-
| Phase 2 — Work Breakdown | — | — | {wbTokens from above} | ${formatEur(tokenCostEur(wbTokens, 'haiku'))} |
|
|
502
|
-
| Phase 3 — Implementation | {sum of phase 3 token estimates} | {sum of phase 3 cost estimates} | — | — |
|
|
503
|
-
| **Total** | **{sum of phase 3 token estimates}** | **{sum of phase 3 cost estimates}** | **{wbTokens} (partial)** | **${formatEur(tokenCostEur(wbTokens, 'haiku'))} (partial)** |
|
|
504
|
-
|
|
505
|
-
---
|
|
506
|
-
*Actuals will be appended by pm-phase3 after implementation completes.*
|
|
507
|
-
*Cost assumes 80% input / 20% output token split. Pricing: sonnet $3.00/$15.00 per 1M, haiku $0.80/$4.00 per 1M (USD). Rate: $1 = €${pricing ? pricing.usd_to_eur : '0.92'} (${pricing ? pricing.usd_to_eur_date : 'see docs/token-pricing.json'}).*
|
|
508
|
-
|
|
509
|
-
TASK 4 — Set return values.
|
|
510
|
-
Set effort_estimate_path to the full path of the written Effort-Estimate.md.
|
|
511
|
-
Set token_estimate_path to the full path of the written Token-Estimate.md.
|
|
512
|
-
Set tasks_csv_path to "${featureDir}/${prefix}-Work-Breakdown.csv" (written by wb-render in the same directory as feature.md).
|
|
513
|
-
|
|
514
|
-
Return the extracted metrics as structured output.`,
|
|
515
|
-
{
|
|
516
|
-
label: 'parse-wb-write-estimates',
|
|
517
|
-
phase: 'Effort Estimate',
|
|
518
|
-
schema: PARSE_SCHEMA,
|
|
519
|
-
}
|
|
520
|
-
)
|
|
521
|
-
|
|
522
|
-
log(`WB parsed: ${metrics.user_stories} US, ${metrics.total_tasks} tasks, ${metrics.implementation_phases} phases`)
|
|
523
|
-
log(`Effort-Estimate written: ${metrics.effort_estimate_path}`)
|
|
524
|
-
log(`Token-Estimate written: ${metrics.token_estimate_path}`)
|
|
525
|
-
|
|
526
|
-
// ── Append to process-log (phase 2 events) ───────────────────────────────────
|
|
527
|
-
const wbEntry = tokenLedger.find(e => e.agent === 'generate-work-breakdown')
|
|
528
|
-
const phase2Events = [
|
|
529
|
-
`APPROVAL GRANTED by user — Gate 1`,
|
|
530
|
-
`pm-phase2 START — work breakdown phase`,
|
|
531
|
-
`Agent DONE: generate-work-breakdown — tokens: ${wbEntry ? wbEntry.phase_delta_tokens : 'N/A'}`,
|
|
532
|
-
`Written: ${metrics.prefix}-Work-Breakdown.md (${metrics.user_stories} US, ${metrics.total_tasks} tasks, ${metrics.implementation_phases} phases)`,
|
|
533
|
-
`Written: ${metrics.prefix}-Effort-Estimate.md`,
|
|
534
|
-
`pm-phase2 COMPLETE`,
|
|
535
|
-
`APPROVAL REQUESTED — Gate 2`,
|
|
536
|
-
].join('\n')
|
|
537
|
-
|
|
538
|
-
await agent(
|
|
539
|
-
`Append phase 2 events to the process-log for this feature delivery run.
|
|
540
|
-
|
|
541
|
-
Process-log path: ${metrics.feature_dir}/${metrics.prefix}-process-log.txt
|
|
542
|
-
|
|
543
|
-
Steps:
|
|
544
|
-
1. Get current UTC datetime via Bash: run \`date -u +"%Y-%m-%dT%H:%M:%S"\`
|
|
545
|
-
2. Append the following lines to the existing file. Use the datetime from step 1 for ALL event lines.
|
|
546
|
-
Format each event line as: [{datetime}] {event text}
|
|
547
|
-
|
|
548
|
-
Events to append:
|
|
549
|
-
${phase2Events}`,
|
|
550
|
-
{ label: 'append-process-log', phase: 'Effort Estimate' }
|
|
551
|
-
)
|
|
552
|
-
|
|
553
|
-
// ── Tier 3 self-registration — close on successful completion ─────────────────
|
|
554
|
-
await selfLedgerOp(
|
|
555
|
-
`ai-toolkit ledger close --prefix ${prefix} --agent pm-phase2:self --dir "${featureDir}" --attempt 1`,
|
|
556
|
-
'ledger-close-pm-phase2-self', 'Effort Estimate'
|
|
557
|
-
)
|
|
558
|
-
|
|
559
|
-
return {
|
|
560
|
-
prefix: metrics.prefix || prefix,
|
|
561
|
-
feature_path: featurePath,
|
|
562
|
-
user_stories: metrics.user_stories,
|
|
563
|
-
total_tasks: metrics.total_tasks,
|
|
564
|
-
domain_breakdown: metrics.domain_breakdown || 'N/A',
|
|
565
|
-
implementation_phases: metrics.implementation_phases,
|
|
566
|
-
human_estimate: metrics.human_estimate || 'N/A',
|
|
567
|
-
agent_estimate: metrics.agent_estimate || 'N/A',
|
|
568
|
-
work_breakdown_path: metrics.work_breakdown_path,
|
|
569
|
-
effort_estimate_path: metrics.effort_estimate_path || '',
|
|
570
|
-
token_estimate_path: metrics.token_estimate_path || '',
|
|
571
|
-
tasks_csv_path: metrics.tasks_csv_path || '',
|
|
572
|
-
token_ledger: tokenLedger,
|
|
573
|
-
errors: [],
|
|
574
|
-
gate2_payload,
|
|
575
|
-
}
|