@dtlabs/ai-toolkit 0.11.0 → 0.12.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.
@@ -11,38 +11,39 @@ 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
+ // ── Ledger facade helpers ─────────────────────────────────────────────────────
15
+ // Route all ledger I/O through the ai-toolkit CLI facade.
16
+ // Open calls fire-and-forget (matching pm-phase1/pm-phase2 pattern).
17
+ // Terminal calls (close, fail, skip) are fail-closed: a non-zero exit status
18
+ // hard-stops the workflow because the terminal state was not persisted.
17
19
 
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
- )
20
+ const LEDGER_TERMINAL_SCHEMA = {
21
+ type: 'object',
22
+ properties: {
23
+ exitCode: { type: 'number' },
24
+ stdout: { type: 'string' },
25
+ stderr: { type: 'string' },
26
+ },
27
+ required: ['exitCode'],
29
28
  }
30
29
 
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' }
30
+ async function ledgerTerminal(cmd, label, phaseName) {
31
+ const result = await agent(
32
+ `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.`,
33
+ { label, phase: phaseName, model: 'haiku', schema: LEDGER_TERMINAL_SCHEMA }
43
34
  )
35
+ const status = (result && typeof result.exitCode === 'number') ? result.exitCode : 1
36
+ if (status !== 0) {
37
+ throw new Error(`HARD STOP — ledger terminal state not persisted. Exit code: ${status}. Command: ${cmd}`)
38
+ }
44
39
  }
45
40
 
41
+ // null/0/not_available => data unavailable, never an observable real zero.
42
+ // A cached or resumed agent returns a budget delta of 0, which is not a real
43
+ // measurement — omit --tokens from close so the module preserves any existing
44
+ // positive phase_delta_tokens on disk rather than clobbering it with zero.
45
+ function tokensAvailable(t) { return Number.isInteger(t) && t > 0 }
46
+
46
47
  // ── Parse args ────────────────────────────────────────────────────────────────
47
48
 
48
49
  // args: "<path-to-feature.md> --branch feature/FTR-NNN-slug"
@@ -65,13 +66,21 @@ const csvPath = `${featureDir}/${prefix}-Work-Breakdown.csv`
65
66
 
66
67
  log(`Reading CSV: ${csvPath}`)
67
68
 
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 })
69
+ const csvKey = 'read-wb-csv:phase3'
70
+ await agent(
71
+ `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.`,
72
+ { label: 'ledger-open-read-wb-csv', phase: 'Parse', model: 'haiku' }
73
+ )
69
74
  const beforeCsv = budget.spent()
70
75
  const csvContent = await agent(
71
76
  `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
77
  { label: 'read-wb-csv', phase: 'Parse', model: 'haiku' }
73
78
  )
74
- await updateLedgerEntry(featureDir, prefix, 'read-wb-csv:phase3', { status: 'done', completed_at: '__TS__', phase_delta_tokens: budget.spent() - beforeCsv })
79
+ const csvTokens = budget.spent() - beforeCsv
80
+ await ledgerTerminal(
81
+ `ai-toolkit ledger close --dir ${featureDir} --prefix ${prefix} --agent ${csvKey}${tokensAvailable(csvTokens) ? ` --tokens ${csvTokens}` : ''} --attempt 1`,
82
+ 'ledger-close-read-wb-csv', 'Parse'
83
+ )
75
84
 
76
85
  // Parse CSV into structured phases — pure JS, no AI
77
86
  const rows = csvContent
@@ -186,20 +195,40 @@ const executePhase = async (implPhase) => {
186
195
  // Step 1 — impl groups in parallel (INFRA / BE / FE)
187
196
  if (implPhase.impl_groups.length > 0) {
188
197
  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 })
198
+ await agent(
199
+ `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.`,
200
+ { label: `ledger-open-impl:${implPhase.phase_id}`, phase: 'Implementation', model: 'haiku' }
201
+ )
190
202
  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
- ))
203
+ let implFailed = false
204
+ let implErrMsg = ''
205
+ try {
206
+ await parallel(implPhase.impl_groups.map(group => () =>
207
+ agent(
208
+ `${featurePath} ${group.task_ids.join(',')}${reworkSuffix}`,
209
+ {
210
+ agentType: group.agent_type,
211
+ label: `${group.agent_type}:${implPhase.phase_id}${reworkCycle > 0 ? ':rework' + reworkCycle : ''}`,
212
+ phase: 'Implementation',
213
+ }
214
+ )
215
+ ))
216
+ } catch (err) {
217
+ implFailed = true
218
+ implErrMsg = err && err.message ? err.message.slice(0, 120) : 'impl dispatch failed'
219
+ }
201
220
  const implTokens = budget.spent() - beforeImpl
202
- await updateLedgerEntry(featureDir, prefix, implKey, { status: 'done', completed_at: '__TS__', phase_delta_tokens: implTokens })
221
+ if (implFailed) {
222
+ await ledgerTerminal(
223
+ `ai-toolkit ledger fail --dir ${featureDir} --prefix ${prefix} --agent ${implKey} --error "impl dispatch failed" --attempt 1`,
224
+ `ledger-fail-impl:${implPhase.phase_id}`, 'Implementation'
225
+ )
226
+ throw new Error(`Phase ${implPhase.phase_id} impl dispatch failed: ${implErrMsg}`)
227
+ }
228
+ await ledgerTerminal(
229
+ `ai-toolkit ledger close --dir ${featureDir} --prefix ${prefix} --agent ${implKey}${tokensAvailable(implTokens) ? ` --tokens ${implTokens}` : ''} --attempt 1`,
230
+ `ledger-close-impl:${implPhase.phase_id}`, 'Implementation'
231
+ )
203
232
  tokenLedger.push({ agent: implKey, model: 'sonnet', phase_delta_tokens: implTokens })
204
233
  log(`Phase ${implPhase.phase_id} impl groups done — ${implTokens} tokens`)
205
234
  }
@@ -207,27 +236,50 @@ const executePhase = async (implPhase) => {
207
236
  // Step 2 — test groups in parallel (TEST) — after impl groups
208
237
  if (implPhase.test_groups.length > 0) {
209
238
  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
- }
239
+ await agent(
240
+ `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.`,
241
+ { label: `ledger-open-test:${implPhase.phase_id}`, phase: 'Implementation', model: 'haiku' }
242
+ )
243
+ const beforeTestGroup = budget.spent()
244
+ let testFailed = false
245
+ let testErrMsg = ''
246
+ try {
247
+ await parallel(implPhase.test_groups.map(group => () =>
248
+ agent(
249
+ `${featurePath} ${group.task_ids.join(',')}${reworkSuffix}`,
250
+ {
251
+ agentType: group.agent_type,
252
+ label: `${group.agent_type}:${implPhase.phase_id}${reworkCycle > 0 ? ':rework' + reworkCycle : ''}`,
253
+ phase: 'Implementation',
254
+ }
255
+ )
256
+ ))
257
+ } catch (err) {
258
+ testFailed = true
259
+ testErrMsg = err && err.message ? err.message.slice(0, 120) : 'test dispatch failed'
260
+ }
261
+ const testGroupTokens = budget.spent() - beforeTestGroup
262
+ if (testFailed) {
263
+ await ledgerTerminal(
264
+ `ai-toolkit ledger fail --dir ${featureDir} --prefix ${prefix} --agent ${testKey} --error "test dispatch failed" --attempt 1`,
265
+ `ledger-fail-test:${implPhase.phase_id}`, 'Implementation'
220
266
  )
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`)
267
+ throw new Error(`Phase ${implPhase.phase_id} test dispatch failed: ${testErrMsg}`)
268
+ }
269
+ await ledgerTerminal(
270
+ `ai-toolkit ledger close --dir ${featureDir} --prefix ${prefix} --agent ${testKey}${tokensAvailable(testGroupTokens) ? ` --tokens ${testGroupTokens}` : ''} --attempt 1`,
271
+ `ledger-close-test:${implPhase.phase_id}`, 'Implementation'
272
+ )
273
+ tokenLedger.push({ agent: testKey, model: 'sonnet', phase_delta_tokens: testGroupTokens })
274
+ log(`Phase ${implPhase.phase_id} test groups done — ${testGroupTokens} tokens`)
226
275
  }
227
276
 
228
277
  // Step 3 — review-solution
229
278
  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 })
279
+ await agent(
280
+ `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.`,
281
+ { label: `ledger-open-review:${implPhase.phase_id}`, phase: 'Implementation', model: 'haiku' }
282
+ )
231
283
  const beforeReview = budget.spent()
232
284
  const review = await agent(
233
285
  `${featurePath} --scope ${implPhase.phase_id}`,
@@ -239,7 +291,10 @@ const executePhase = async (implPhase) => {
239
291
  }
240
292
  )
241
293
  const reviewTokens = budget.spent() - beforeReview
242
- await updateLedgerEntry(featureDir, prefix, reviewKey, { status: 'done', completed_at: '__TS__', phase_delta_tokens: reviewTokens })
294
+ await ledgerTerminal(
295
+ `ai-toolkit ledger close --dir ${featureDir} --prefix ${prefix} --agent ${reviewKey}${tokensAvailable(reviewTokens) ? ` --tokens ${reviewTokens}` : ''} --attempt 1`,
296
+ `ledger-close-review:${implPhase.phase_id}`, 'Implementation'
297
+ )
243
298
  tokenLedger.push({
244
299
  agent: reviewKey,
245
300
  model: 'sonnet',
@@ -313,8 +368,12 @@ log(`Implementation complete — ${phasesDone} phases, ${usPassed.length} US pas
313
368
  // contention down to a single centralized run with one consolidated failure report.
314
369
  phase('Test')
315
370
 
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()
371
+ const testRunKey = 'final-test-run'
372
+ await agent(
373
+ `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.`,
374
+ { label: 'ledger-open-final-test-run', phase: 'Test', model: 'haiku' }
375
+ )
376
+ const beforeFinalTest = budget.spent()
318
377
  await agent(
319
378
  `Run the full test suite in the repository root and report the result.
320
379
 
@@ -325,10 +384,13 @@ await agent(
325
384
  Do NOT fix failing tests — only report them.`,
326
385
  { label: 'final-test-run', phase: 'Test', model: 'haiku' }
327
386
  )
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`)
387
+ const finalTestTokens = budget.spent() - beforeFinalTest
388
+ await ledgerTerminal(
389
+ `ai-toolkit ledger close --dir ${featureDir} --prefix ${prefix} --agent ${testRunKey}${tokensAvailable(finalTestTokens) ? ` --tokens ${finalTestTokens}` : ''} --attempt 1`,
390
+ 'ledger-close-final-test-run', 'Test'
391
+ )
392
+ tokenLedger.push({ agent: testRunKey, model: 'haiku', phase_delta_tokens: finalTestTokens })
393
+ log(`Test run complete — ${finalTestTokens} tokens`)
332
394
 
333
395
  // ── Remediation ───────────────────────────────────────────────────────────────
334
396
  phase('Remediation')
@@ -345,9 +407,13 @@ const REM_SCHEMA = {
345
407
  let issuesFixed = 0
346
408
  let issuesDeferred = 0
347
409
 
410
+ const remKey = 'remediation'
348
411
  if (issuesOpen > 0 && issuesPath) {
349
412
  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 })
413
+ await agent(
414
+ `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.`,
415
+ { label: 'ledger-open-remediation', phase: 'Remediation', model: 'haiku' }
416
+ )
351
417
  const beforeRem = budget.spent()
352
418
 
353
419
  const remResult = await agent(
@@ -370,13 +436,20 @@ Update the Issues Register Status column in place for each resolved/deferred iss
370
436
  )
371
437
 
372
438
  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 })
439
+ await ledgerTerminal(
440
+ `ai-toolkit ledger close --dir ${featureDir} --prefix ${prefix} --agent ${remKey}${tokensAvailable(remTokens) ? ` --tokens ${remTokens}` : ''} --attempt 1`,
441
+ 'ledger-close-remediation', 'Remediation'
442
+ )
443
+ tokenLedger.push({ agent: remKey, model: 'sonnet', phase_delta_tokens: remTokens })
375
444
  issuesFixed = remResult.issues_fixed || 0
376
445
  issuesDeferred = remResult.issues_deferred || 0
377
446
  log(`Remediation done — fixed: ${issuesFixed}, deferred: ${issuesDeferred} — ${remTokens} tokens`)
378
447
  } else {
379
448
  log('Remediation: no OPEN issues — skipped')
449
+ await ledgerTerminal(
450
+ `ai-toolkit ledger skip --dir ${featureDir} --prefix ${prefix} --agent ${remKey} --phase phase3 --model sonnet --attempt 1`,
451
+ 'ledger-skip-remediation', 'Remediation'
452
+ )
380
453
  }
381
454
 
382
455
  // ── PR ────────────────────────────────────────────────────────────────────────
@@ -391,7 +464,11 @@ const PR_SCHEMA = {
391
464
  required: ['pr_url'],
392
465
  }
393
466
 
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 })
467
+ const prKey = 'pr-and-registry'
468
+ await agent(
469
+ `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.`,
470
+ { label: 'ledger-open-pr-and-registry', phase: 'PR', model: 'haiku' }
471
+ )
395
472
  const beforePR = budget.spent()
396
473
 
397
474
  const prResult = await agent(
@@ -441,8 +518,11 @@ If the PR creation fails, return { "pr_url": "(PR creation failed)", "registry_u
441
518
  )
442
519
 
443
520
  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 })
521
+ await ledgerTerminal(
522
+ `ai-toolkit ledger close --dir ${featureDir} --prefix ${prefix} --agent ${prKey}${tokensAvailable(prTokens) ? ` --tokens ${prTokens}` : ''} --attempt 1`,
523
+ 'ledger-close-pr-and-registry', 'PR'
524
+ )
525
+ tokenLedger.push({ agent: prKey, model: 'sonnet', phase_delta_tokens: prTokens })
446
526
  log(`PR done: ${prResult.pr_url} — ${prTokens} tokens`)
447
527
 
448
528
  // ── Actuals ───────────────────────────────────────────────────────────────────
@@ -468,7 +548,7 @@ try {
468
548
  tokenLedger.push(entry)
469
549
  inMemoryByAgent.set(entry.agent, tokenLedger.length - 1)
470
550
  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) {
551
+ } else if (!tokensAvailable(tokenLedger[idx].phase_delta_tokens) && tokensAvailable(entry.phase_delta_tokens)) {
472
552
  // in-memory entry has delta=0 (cached agent on resume) but disk has real data — prefer disk
473
553
  tokenLedger[idx] = entry
474
554
  log(`Restored real token count from disk: ${entry.agent} (${entry.phase_delta_tokens} tokens)`)
@@ -479,7 +559,7 @@ try {
479
559
  log(`Could not parse persisted token ledger — using in-memory ledger only`)
480
560
  }
481
561
 
482
- const totalPhase3Tokens = tokenLedger.reduce((s, e) => s + (e.phase_delta_tokens || 0), 0)
562
+ const totalPhase3Tokens = tokenLedger.reduce((s, e) => s + (tokensAvailable(e.phase_delta_tokens) ? e.phase_delta_tokens : 0), 0)
483
563
 
484
564
  // ── Load token pricing for cost columns ──────────────────────────────────────
485
565
  const pricingRaw3 = await agent(
@@ -509,7 +589,7 @@ const roleTotals = {}
509
589
  const roleModels = {}
510
590
  for (const e of tokenLedger) {
511
591
  const role = String(e.agent).split(':')[0]
512
- roleTotals[role] = (roleTotals[role] || 0) + (e.phase_delta_tokens || 0)
592
+ roleTotals[role] = (roleTotals[role] || 0) + (tokensAvailable(e.phase_delta_tokens) ? e.phase_delta_tokens : 0)
513
593
  roleModels[role] = e.model || 'sonnet'
514
594
  }
515
595
  const roleRows = Object.entries(roleTotals)
@@ -528,7 +608,11 @@ const ACTUALS_SCHEMA = {
528
608
  required: [],
529
609
  }
530
610
 
531
- await appendLedgerEntry(featureDir, prefix, { agent: 'write-actuals', phase: 'phase3', model: 'sonnet', status: 'running', phase_delta_tokens: 0, started_at: '__TS__', completed_at: null })
611
+ const actualsKey = 'write-actuals'
612
+ await agent(
613
+ `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.`,
614
+ { label: 'ledger-open-write-actuals', phase: 'Actuals', model: 'haiku' }
615
+ )
532
616
  const beforeActuals = budget.spent()
533
617
 
534
618
  await agent(
@@ -622,8 +706,11 @@ Return { "token_estimate_path": "<path>", "effort_estimate_path": "<path>" }.`,
622
706
  )
623
707
 
624
708
  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 })
709
+ await ledgerTerminal(
710
+ `ai-toolkit ledger close --dir ${featureDir} --prefix ${prefix} --agent ${actualsKey}${tokensAvailable(actualsTokens) ? ` --tokens ${actualsTokens}` : ''} --attempt 1`,
711
+ 'ledger-close-write-actuals', 'Actuals'
712
+ )
713
+ tokenLedger.push({ agent: actualsKey, model: 'sonnet', phase_delta_tokens: actualsTokens })
627
714
  log(`Actuals written — ${actualsTokens} tokens`)
628
715
 
629
716
  // ── Append to process-log ─────────────────────────────────────────────────────