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