@dtlabs/ai-toolkit 0.11.0 → 0.13.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 (38) hide show
  1. package/CHANGELOG.md +65 -0
  2. package/bin/cli.js +757 -68
  3. package/lib/agent-registry.js +1018 -0
  4. package/lib/execution-ledger.js +609 -0
  5. package/package.json +1 -1
  6. package/src/claude/agents/{concurrency-safety-assessment.md → gaia-concurrency-safety-assessment.md} +1 -1
  7. package/src/claude/agents/{define-feature.md → gaia-define-feature.md} +37 -61
  8. package/src/claude/agents/{dependency-injection-refactoring.md → gaia-dependency-injection-refactoring.md} +1 -1
  9. package/src/claude/agents/{dependency-supply-chain-security.md → gaia-dependency-supply-chain-security.md} +1 -1
  10. package/src/claude/agents/{developer-backend.md → gaia-developer-backend.md} +1 -1
  11. package/src/claude/agents/{developer-frontend.md → gaia-developer-frontend.md} +1 -1
  12. package/src/claude/agents/{developer-testing.md → gaia-developer-testing.md} +1 -1
  13. package/src/claude/agents/{domain-model-refactoring.md → gaia-domain-model-refactoring.md} +1 -1
  14. package/src/claude/agents/{generate-requirements.md → gaia-generate-requirements.md} +1 -1
  15. package/src/claude/agents/{generate-tech-spec.md → gaia-generate-tech-spec.md} +1 -1
  16. package/src/claude/agents/{generate-work-breakdown.md → gaia-generate-work-breakdown.md} +5 -5
  17. package/src/claude/agents/{generic-software-assessment.md → gaia-generic-software-assessment.md} +1 -1
  18. package/src/claude/agents/{god-class-decomposition.md → gaia-god-class-decomposition.md} +1 -1
  19. package/src/claude/agents/{init-agents-md.md → gaia-init-agents-md.md} +1 -1
  20. package/src/claude/agents/{install-toolkit.md → gaia-install-toolkit.md} +1 -1
  21. package/src/claude/agents/{intervention-documentation-standard.md → gaia-intervention-documentation-standard.md} +1 -1
  22. package/src/claude/agents/{layered-architecture-assessment.md → gaia-layered-architecture-assessment.md} +1 -1
  23. package/src/claude/agents/{review-solution.md → gaia-review-solution.md} +1 -1
  24. package/src/claude/agents/{security-hardening.md → gaia-security-hardening.md} +1 -1
  25. package/src/claude/agents/{validate-feature-docs.md → gaia-validate-feature-docs.md} +2 -2
  26. package/src/claude/agents/{validate-work-breakdown-semantic.md → gaia-validate-work-breakdown-semantic.md} +1 -1
  27. package/src/claude/commands/next-task.md +4 -4
  28. package/src/claude/scripts/wb-render.js +101 -2
  29. package/src/claude/scripts/wb-validate.js +32 -2
  30. package/src/claude/skills/assess-codebase/SKILL.md +1 -1
  31. package/src/claude/skills/define-feature/SKILL.md +2 -2
  32. package/src/claude/skills/hi-gaia/SKILL.md +1 -1
  33. package/src/claude/skills/implement-feature/SKILL.md +190 -10
  34. package/src/claude/skills/init-agents/SKILL.md +2 -2
  35. package/src/claude/skills/install-toolkit/SKILL.md +2 -2
  36. package/src/claude/workflows/pm-phase1.js +94 -73
  37. package/src/claude/workflows/pm-phase2.js +118 -108
  38. package/src/claude/workflows/pm-phase3.js +219 -75
@@ -11,38 +11,73 @@ export const meta = {
11
11
  ],
12
12
  }
13
13
 
14
- // ── Ledger helper functions ───────────────────────────────────────────────────
15
- // Route all ledger I/O through agent() — fs and require() are not available in
16
- // the Workflow runtime. Mirrors the pattern used by pm-phase2.js.
14
+ // ── Tier 2 resolution guard ───────────────────────────────────────────────────
15
+ // All worker agents must be resolved via the CLI before dispatch.
16
+ // No direct require('lib/agent-registry') — resolution goes through the CLI facade.
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
+ }
17
27
 
18
- async function appendLedgerEntry(featureDir, prefix, entry) {
19
- const ledgerPath = `${featureDir}/${prefix}-token-ledger.json`
20
- const entryWithoutTs = JSON.stringify({ ...entry, started_at: '__TS__', completed_at: null })
21
- await agent(
22
- `Append a JSON object to the ledger array at: ${ledgerPath}\n\n` +
23
- `1. Run: date -u +"%Y-%m-%dT%H:%M:%SZ" and capture the output as NOW.\n` +
24
- `2. Read the file. If it does not exist or cannot be parsed as a JSON array, start with [].\n` +
25
- `3. Push this object onto the array, replacing "__TS__" in started_at with NOW: ${entryWithoutTs}\n` +
26
- `4. Write the full array back (JSON, 2-space indent). Return no output.`,
27
- { label: 'append-ledger', phase: 'Implementation', model: 'haiku' }
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 }
28
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
29
37
  }
30
38
 
31
- async function updateLedgerEntry(featureDir, prefix, agentKey, updates) {
32
- const ledgerPath = `${featureDir}/${prefix}-token-ledger.json`
33
- const updatesWithoutTs = JSON.stringify({ ...updates, completed_at: '__TS__' })
34
- await agent(
35
- `Update an entry in the ledger array at: ${ledgerPath}\n\n` +
36
- `1. Run: date -u +"%Y-%m-%dT%H:%M:%SZ" and capture the output as NOW.\n` +
37
- `2. Read the file. If it does not exist or cannot be parsed as a JSON array, do nothing.\n` +
38
- `3. Search from the end for the last entry where agent === "${agentKey}".\n` +
39
- `4. If found, merge these fields into that entry, replacing "__TS__" in completed_at with NOW: ${updatesWithoutTs}\n` +
40
- `5. Write the full array back (JSON, 2-space indent). Return no output.\n` +
41
- `6. If not found, do nothing.`,
42
- { label: 'update-ledger', phase: 'Implementation', model: 'haiku' }
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 }
43
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
+ }
44
73
  }
45
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
+
46
81
  // ── Parse args ────────────────────────────────────────────────────────────────
47
82
 
48
83
  // args: "<path-to-feature.md> --branch feature/FTR-NNN-slug"
@@ -63,15 +98,29 @@ const prefixMatch = featureDir.match(/([A-Z]+-\d+)/)
63
98
  const prefix = prefixMatch ? prefixMatch[1] : 'FTR-000'
64
99
  const csvPath = `${featureDir}/${prefix}-Work-Breakdown.csv`
65
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
+
66
107
  log(`Reading CSV: ${csvPath}`)
67
108
 
68
- await appendLedgerEntry(featureDir, prefix, { agent: 'read-wb-csv:phase3', phase: 'phase3', model: 'haiku', status: 'running', phase_delta_tokens: 0, started_at: '__TS__', completed_at: null })
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
+ )
69
114
  const beforeCsv = budget.spent()
70
115
  const csvContent = await agent(
71
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.`,
72
117
  { label: 'read-wb-csv', phase: 'Parse', model: 'haiku' }
73
118
  )
74
- await updateLedgerEntry(featureDir, prefix, 'read-wb-csv:phase3', { status: 'done', completed_at: '__TS__', phase_delta_tokens: budget.spent() - beforeCsv })
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
+ )
75
124
 
76
125
  // Parse CSV into structured phases — pure JS, no AI
77
126
  const rows = csvContent
@@ -151,6 +200,13 @@ const buildWaves = (phases) => {
151
200
  const waves = buildWaves(wb.phases)
152
201
  log(`Execution plan: ${waves.length} wave(s) — ${waves.map((w, i) => `wave${i+1}:[${w.map(p => p.phase_id).join(',')}]`).join(' ')}`)
153
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
+
154
210
  let phasesDone = 0
155
211
  const usPassed = []
156
212
  const usEscalated = []
@@ -186,20 +242,42 @@ const executePhase = async (implPhase) => {
186
242
  // Step 1 — impl groups in parallel (INFRA / BE / FE)
187
243
  if (implPhase.impl_groups.length > 0) {
188
244
  const implKey = `${implPhase.impl_groups.map(g => g.agent_type).join('+')}:${implPhase.phase_id}${reworkCycle > 0 ? ':rework' + reworkCycle : ''}`
189
- await appendLedgerEntry(featureDir, prefix, { agent: implKey, phase: 'phase3', model: 'sonnet', status: 'running', phase_delta_tokens: 0, started_at: '__TS__', completed_at: null })
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
+ )
190
249
  const beforeImpl = budget.spent()
191
- await parallel(implPhase.impl_groups.map(group => () =>
192
- agent(
193
- `${featurePath} ${group.task_ids.join(',')}${reworkSuffix}`,
194
- {
195
- agentType: group.agent_type,
196
- label: `${group.agent_type}:${implPhase.phase_id}${reworkCycle > 0 ? ':rework' + reworkCycle : ''}`,
197
- phase: 'Implementation',
198
- }
199
- )
200
- ))
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
+ }
201
269
  const implTokens = budget.spent() - beforeImpl
202
- await updateLedgerEntry(featureDir, prefix, implKey, { status: 'done', completed_at: '__TS__', phase_delta_tokens: implTokens })
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
+ )
203
281
  tokenLedger.push({ agent: implKey, model: 'sonnet', phase_delta_tokens: implTokens })
204
282
  log(`Phase ${implPhase.phase_id} impl groups done — ${implTokens} tokens`)
205
283
  }
@@ -207,39 +285,67 @@ const executePhase = async (implPhase) => {
207
285
  // Step 2 — test groups in parallel (TEST) — after impl groups
208
286
  if (implPhase.test_groups.length > 0) {
209
287
  const testKey = `developer-testing:${implPhase.phase_id}${reworkCycle > 0 ? ':rework' + reworkCycle : ''}`
210
- await appendLedgerEntry(featureDir, prefix, { agent: testKey, phase: 'phase3', model: 'sonnet', status: 'running', phase_delta_tokens: 0, started_at: '__TS__', completed_at: null })
211
- const beforeTest = budget.spent()
212
- await parallel(implPhase.test_groups.map(group => () =>
213
- agent(
214
- `${featurePath} ${group.task_ids.join(',')}${reworkSuffix}`,
215
- {
216
- agentType: group.agent_type,
217
- label: `${group.agent_type}:${implPhase.phase_id}${reworkCycle > 0 ? ':rework' + reworkCycle : ''}`,
218
- phase: 'Implementation',
219
- }
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'
220
317
  )
221
- ))
222
- const testTokens = budget.spent() - beforeTest
223
- await updateLedgerEntry(featureDir, prefix, testKey, { status: 'done', completed_at: '__TS__', phase_delta_tokens: testTokens })
224
- tokenLedger.push({ agent: testKey, model: 'sonnet', phase_delta_tokens: testTokens })
225
- log(`Phase ${implPhase.phase_id} test groups done — ${testTokens} tokens`)
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`)
226
326
  }
227
327
 
228
328
  // Step 3 — review-solution
229
329
  const reviewKey = `review-solution:${implPhase.phase_id}`
230
- await appendLedgerEntry(featureDir, prefix, { agent: reviewKey, phase: 'phase3', model: 'sonnet', status: 'running', phase_delta_tokens: 0, started_at: '__TS__', completed_at: null })
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
+ )
231
334
  const beforeReview = budget.spent()
232
335
  const review = await agent(
233
336
  `${featurePath} --scope ${implPhase.phase_id}`,
234
337
  {
235
- agentType: 'review-solution',
338
+ agentType: resolvedNativeNames['review-solution'],
236
339
  label: `review-solution:${implPhase.phase_id}`,
237
340
  phase: 'Implementation',
238
341
  schema: REVIEW_SCHEMA,
239
342
  }
240
343
  )
241
344
  const reviewTokens = budget.spent() - beforeReview
242
- await updateLedgerEntry(featureDir, prefix, reviewKey, { status: 'done', completed_at: '__TS__', phase_delta_tokens: reviewTokens })
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
+ )
243
349
  tokenLedger.push({
244
350
  agent: reviewKey,
245
351
  model: 'sonnet',
@@ -313,8 +419,12 @@ log(`Implementation complete — ${phasesDone} phases, ${usPassed.length} US pas
313
419
  // contention down to a single centralized run with one consolidated failure report.
314
420
  phase('Test')
315
421
 
316
- await appendLedgerEntry(featureDir, prefix, { agent: 'final-test-run', phase: 'phase3', model: 'haiku', status: 'running', phase_delta_tokens: 0, started_at: '__TS__', completed_at: null })
317
- const beforeTest = budget.spent()
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()
318
428
  await agent(
319
429
  `Run the full test suite in the repository root and report the result.
320
430
 
@@ -325,10 +435,13 @@ await agent(
325
435
  Do NOT fix failing tests — only report them.`,
326
436
  { label: 'final-test-run', phase: 'Test', model: 'haiku' }
327
437
  )
328
- const testTokens = budget.spent() - beforeTest
329
- await updateLedgerEntry(featureDir, prefix, 'final-test-run', { status: 'done', completed_at: '__TS__', phase_delta_tokens: testTokens })
330
- tokenLedger.push({ agent: 'final-test-run', model: 'haiku', phase_delta_tokens: testTokens })
331
- log(`Test run complete — ${testTokens} tokens`)
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`)
332
445
 
333
446
  // ── Remediation ───────────────────────────────────────────────────────────────
334
447
  phase('Remediation')
@@ -345,9 +458,13 @@ const REM_SCHEMA = {
345
458
  let issuesFixed = 0
346
459
  let issuesDeferred = 0
347
460
 
461
+ const remKey = 'remediation'
348
462
  if (issuesOpen > 0 && issuesPath) {
349
463
  log(`Remediation: ${issuesOpen} OPEN issues`)
350
- await appendLedgerEntry(featureDir, prefix, { agent: 'remediation', phase: 'phase3', model: 'sonnet', status: 'running', phase_delta_tokens: 0, started_at: '__TS__', completed_at: null })
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
+ )
351
468
  const beforeRem = budget.spent()
352
469
 
353
470
  const remResult = await agent(
@@ -370,13 +487,20 @@ Update the Issues Register Status column in place for each resolved/deferred iss
370
487
  )
371
488
 
372
489
  const remTokens = budget.spent() - beforeRem
373
- await updateLedgerEntry(featureDir, prefix, 'remediation', { status: 'done', completed_at: '__TS__', phase_delta_tokens: remTokens })
374
- tokenLedger.push({ agent: 'remediation', model: 'sonnet', phase_delta_tokens: remTokens })
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 })
375
495
  issuesFixed = remResult.issues_fixed || 0
376
496
  issuesDeferred = remResult.issues_deferred || 0
377
497
  log(`Remediation done — fixed: ${issuesFixed}, deferred: ${issuesDeferred} — ${remTokens} tokens`)
378
498
  } else {
379
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
+ )
380
504
  }
381
505
 
382
506
  // ── PR ────────────────────────────────────────────────────────────────────────
@@ -391,7 +515,11 @@ const PR_SCHEMA = {
391
515
  required: ['pr_url'],
392
516
  }
393
517
 
394
- await appendLedgerEntry(featureDir, prefix, { agent: 'pr-and-registry', phase: 'phase3', model: 'sonnet', status: 'running', phase_delta_tokens: 0, started_at: '__TS__', completed_at: null })
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
+ )
395
523
  const beforePR = budget.spent()
396
524
 
397
525
  const prResult = await agent(
@@ -441,8 +569,11 @@ If the PR creation fails, return { "pr_url": "(PR creation failed)", "registry_u
441
569
  )
442
570
 
443
571
  const prTokens = budget.spent() - beforePR
444
- await updateLedgerEntry(featureDir, prefix, 'pr-and-registry', { status: 'done', completed_at: '__TS__', phase_delta_tokens: prTokens })
445
- tokenLedger.push({ agent: 'pr-and-registry', model: 'sonnet', phase_delta_tokens: prTokens })
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 })
446
577
  log(`PR done: ${prResult.pr_url} — ${prTokens} tokens`)
447
578
 
448
579
  // ── Actuals ───────────────────────────────────────────────────────────────────
@@ -468,7 +599,7 @@ try {
468
599
  tokenLedger.push(entry)
469
600
  inMemoryByAgent.set(entry.agent, tokenLedger.length - 1)
470
601
  log(`Recovered ledger entry from disk: ${entry.agent} (${entry.phase_delta_tokens} tokens)`)
471
- } else if (tokenLedger[idx].phase_delta_tokens === 0 && (entry.phase_delta_tokens || 0) > 0) {
602
+ } else if (!tokensAvailable(tokenLedger[idx].phase_delta_tokens) && tokensAvailable(entry.phase_delta_tokens)) {
472
603
  // in-memory entry has delta=0 (cached agent on resume) but disk has real data — prefer disk
473
604
  tokenLedger[idx] = entry
474
605
  log(`Restored real token count from disk: ${entry.agent} (${entry.phase_delta_tokens} tokens)`)
@@ -479,7 +610,7 @@ try {
479
610
  log(`Could not parse persisted token ledger — using in-memory ledger only`)
480
611
  }
481
612
 
482
- const totalPhase3Tokens = tokenLedger.reduce((s, e) => s + (e.phase_delta_tokens || 0), 0)
613
+ const totalPhase3Tokens = tokenLedger.reduce((s, e) => s + (tokensAvailable(e.phase_delta_tokens) ? e.phase_delta_tokens : 0), 0)
483
614
 
484
615
  // ── Load token pricing for cost columns ──────────────────────────────────────
485
616
  const pricingRaw3 = await agent(
@@ -509,7 +640,7 @@ const roleTotals = {}
509
640
  const roleModels = {}
510
641
  for (const e of tokenLedger) {
511
642
  const role = String(e.agent).split(':')[0]
512
- roleTotals[role] = (roleTotals[role] || 0) + (e.phase_delta_tokens || 0)
643
+ roleTotals[role] = (roleTotals[role] || 0) + (tokensAvailable(e.phase_delta_tokens) ? e.phase_delta_tokens : 0)
513
644
  roleModels[role] = e.model || 'sonnet'
514
645
  }
515
646
  const roleRows = Object.entries(roleTotals)
@@ -528,7 +659,11 @@ const ACTUALS_SCHEMA = {
528
659
  required: [],
529
660
  }
530
661
 
531
- await appendLedgerEntry(featureDir, prefix, { agent: 'write-actuals', phase: 'phase3', model: 'sonnet', status: 'running', phase_delta_tokens: 0, started_at: '__TS__', completed_at: null })
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
+ )
532
667
  const beforeActuals = budget.spent()
533
668
 
534
669
  await agent(
@@ -622,8 +757,11 @@ Return { "token_estimate_path": "<path>", "effort_estimate_path": "<path>" }.`,
622
757
  )
623
758
 
624
759
  const actualsTokens = budget.spent() - beforeActuals
625
- await updateLedgerEntry(featureDir, prefix, 'write-actuals', { status: 'done', completed_at: '__TS__', phase_delta_tokens: actualsTokens })
626
- tokenLedger.push({ agent: 'write-actuals', model: 'sonnet', phase_delta_tokens: actualsTokens })
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 })
627
765
  log(`Actuals written — ${actualsTokens} tokens`)
628
766
 
629
767
  // ── Append to process-log ─────────────────────────────────────────────────────
@@ -672,6 +810,12 @@ If there is nothing to commit (all files already committed), that is fine — re
672
810
  { label: 'commit-actuals', phase: 'Actuals' }
673
811
  )
674
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
+
675
819
  return {
676
820
  pr_url: prResult.pr_url,
677
821
  token_ledger: tokenLedger,