@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.
- package/CHANGELOG.md +37 -0
- package/bin/cli.js +279 -63
- package/lib/execution-ledger.js +515 -0
- package/package.json +1 -1
- package/src/claude/agents/define-feature.md +36 -60
- package/src/claude/scripts/wb-render.js +101 -2
- package/src/claude/scripts/wb-validate.js +32 -2
- package/src/claude/skills/implement-feature/SKILL.md +4 -2
- package/src/claude/workflows/pm-phase1.js +25 -75
- package/src/claude/workflows/pm-phase2.js +55 -106
- package/src/claude/workflows/pm-phase3.js +162 -75
|
@@ -11,38 +11,39 @@ export const meta = {
|
|
|
11
11
|
],
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
-
// ── Ledger
|
|
15
|
-
// Route all ledger I/O through
|
|
16
|
-
//
|
|
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
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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
|
|
32
|
-
const
|
|
33
|
-
|
|
34
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
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
|
-
|
|
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
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
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
|
-
|
|
223
|
-
await
|
|
224
|
-
|
|
225
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
317
|
-
|
|
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
|
|
329
|
-
await
|
|
330
|
-
|
|
331
|
-
|
|
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
|
|
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
|
|
374
|
-
|
|
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
|
-
|
|
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
|
|
445
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
626
|
-
|
|
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 ─────────────────────────────────────────────────────
|