@dtlabs/ai-toolkit 0.16.4 → 0.16.6
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 +18 -0
- package/bin/cli.js +24 -15
- package/docs/task-executor-bootstrap.md +6 -6
- package/docs/task-executor-cli.md +51 -5
- package/lib/task-executor/index.js +282 -7
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,4 +1,22 @@
|
|
|
1
1
|
## 0.1.4 (2026-07-21)
|
|
2
|
+
## [0.16.6](https://github.com/dtlab-tech/ai-toolkit/compare/v0.16.5...v0.16.6) (2026-10-06)
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
### Bug Fixes
|
|
6
|
+
|
|
7
|
+
* **task-executor:** drop --permission-prompts, it doesn't exist ([903dea5](https://github.com/dtlab-tech/ai-toolkit/commit/903dea58fba475e3bf5caa83a41723977f6d742e))
|
|
8
|
+
* **task-executor:** drop --permission-prompts, it doesn't exist ([9c49c78](https://github.com/dtlab-tech/ai-toolkit/commit/9c49c789f385e88763d606d70bdfd9653e37a948))
|
|
9
|
+
|
|
10
|
+
## [0.16.5](https://github.com/dtlab-tech/ai-toolkit/compare/v0.16.4...v0.16.5) (2026-10-05)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
### Bug Fixes
|
|
14
|
+
|
|
15
|
+
* **task-executor:** implement the real executor status command ([58710aa](https://github.com/dtlab-tech/ai-toolkit/commit/58710aa5824108355076df697291a39dc8cd22f1))
|
|
16
|
+
* **task-executor:** permissions, ledger finalize, review verdicts ([91fedef](https://github.com/dtlab-tech/ai-toolkit/commit/91fedef1e3adb0b8ca64114f786d324e43991d64))
|
|
17
|
+
* **task-executor:** restore permission bypass and close ledger activities ([40aa1d5](https://github.com/dtlab-tech/ai-toolkit/commit/40aa1d5ba115a2438b6af66ff26a50956b9dcd3e))
|
|
18
|
+
* **task-executor:** surface build/test-driven review FAIL reasons ([09c0052](https://github.com/dtlab-tech/ai-toolkit/commit/09c0052109195d2b2b2f336a7b0fa1a892512787))
|
|
19
|
+
|
|
2
20
|
## [0.16.4](https://github.com/dtlab-tech/ai-toolkit/compare/v0.16.3...v0.16.4) (2026-10-02)
|
|
3
21
|
|
|
4
22
|
|
package/bin/cli.js
CHANGED
|
@@ -2671,14 +2671,18 @@ function _executorCurrentBranchRef(projectDir) {
|
|
|
2671
2671
|
// METADATA_RESERVED_FIELD, METADATA_UNKNOWN_KEY, INVALID_STATUS.
|
|
2672
2672
|
//
|
|
2673
2673
|
// 1 (unexpected I/O) — the DEFAULT for everything else, including every
|
|
2674
|
-
// *_CORRUPTED/CORRUPT_* code
|
|
2675
|
-
//
|
|
2676
|
-
//
|
|
2677
|
-
//
|
|
2678
|
-
//
|
|
2679
|
-
//
|
|
2680
|
-
//
|
|
2681
|
-
//
|
|
2674
|
+
// *_CORRUPTED/CORRUPT_* code (LEDGER_CORRUPTED/PRICING_CORRUPTED among
|
|
2675
|
+
// them — status()'s own ledger/pricing-file reads), every
|
|
2676
|
+
// LEDGER_READ_FAILED/PRICING_READ_FAILED/GIT_*_FAILED/*_FAILED
|
|
2677
|
+
// git-command-spawn code, STATE_UNRECOVERABLE, and — explicitly, per this
|
|
2678
|
+
// task's own brief — NOT_IMPLEMENTED. `diagnose` is still a
|
|
2679
|
+
// NOT_IMPLEMENTED stub in lib/task-executor/index.js (attributed to
|
|
2680
|
+
// US-06-TASK-BE-02, which has not actually built its real body yet);
|
|
2681
|
+
// `status` now has a real implementation (it never was actually assigned
|
|
2682
|
+
// a Work Breakdown task of its own — its stub's US-06-TASK-BE-01
|
|
2683
|
+
// attribution was stale, that task is resume/reconcile) and is wired
|
|
2684
|
+
// through exactly like the other commands, its errors surfacing through
|
|
2685
|
+
// this same mapping table honestly.
|
|
2682
2686
|
//
|
|
2683
2687
|
// 8 (paused awaiting human decision) has no throw site and is deliberately
|
|
2684
2688
|
// absent from this table: execute()'s/resume()'s 'paused' run status is a
|
|
@@ -2725,6 +2729,7 @@ const EXECUTOR_EXIT_CODE_BY_ERROR_CODE = {
|
|
|
2725
2729
|
// though 1 is also the default fallback below)
|
|
2726
2730
|
STATE_CORRUPTED: 1, RECEIPT_CORRUPTED: 1, INTENT_CORRUPTED: 1, LEASE_CORRUPTED: 1,
|
|
2727
2731
|
CORRUPT_LEDGER: 1, STATE_UNRECOVERABLE: 1, NOT_IMPLEMENTED: 1,
|
|
2732
|
+
LEDGER_READ_FAILED: 1, LEDGER_CORRUPTED: 1, PRICING_READ_FAILED: 1, PRICING_CORRUPTED: 1,
|
|
2728
2733
|
GIT_SPAWN_FAILED: 1, GIT_STATUS_FAILED: 1, GIT_ADD_FAILED: 1, GIT_WRITE_TREE_FAILED: 1,
|
|
2729
2734
|
GIT_DIFF_FAILED: 1, GIT_COMMIT_TREE_FAILED: 1, REF_UPDATE_FAILED: 1, GIT_CAT_FILE_FAILED: 1,
|
|
2730
2735
|
GIT_MERGE_BASE_FAILED: 1, GIT_REV_PARSE_FAILED: 1, GIT_CHERRY_PICK_FAILED: 1,
|
|
@@ -2785,13 +2790,17 @@ async function handleExecutorCommand(argv) {
|
|
|
2785
2790
|
function emitSuccess(result) {
|
|
2786
2791
|
process.stdout.write(JSON.stringify(result, null, 2) + '\n');
|
|
2787
2792
|
// Tech-Spec section 10 exit code 8 ("paused awaiting human decision") is a
|
|
2788
|
-
// SUCCESS-path outcome, not an error —
|
|
2789
|
-
// result objects can legitimately carry `runStatus: 'paused'`
|
|
2790
|
-
// exit-code mapping comment above EXECUTOR_EXIT_CODE_BY_ERROR_CODE
|
|
2791
|
-
// no *_ERROR code maps to 8).
|
|
2792
|
-
//
|
|
2793
|
-
//
|
|
2794
|
-
//
|
|
2793
|
+
// SUCCESS-path outcome, not an error — execute()'s, resume()'s and now
|
|
2794
|
+
// status()'s result objects can legitimately carry `runStatus: 'paused'`
|
|
2795
|
+
// (see the exit-code mapping comment above EXECUTOR_EXIT_CODE_BY_ERROR_CODE
|
|
2796
|
+
// for why no *_ERROR code maps to 8). A `status` call truthfully reporting
|
|
2797
|
+
// a paused run surfaces the same exit 8 signal a direct execute()/resume()
|
|
2798
|
+
// call would — deliberate, not a special case: the exit code reflects the
|
|
2799
|
+
// run's actual state, not which subcommand observed it. reconcile/stop/
|
|
2800
|
+
// replan results never carry a `runStatus` field at all, and diagnose
|
|
2801
|
+
// never reaches this success path (still NOT_IMPLEMENTED) — so this check
|
|
2802
|
+
// stays a no-op for those subcommands' result shapes without needing to be
|
|
2803
|
+
// scoped by subcommand.
|
|
2795
2804
|
process.exitCode = result && result.runStatus === 'paused' ? 8 : 0;
|
|
2796
2805
|
}
|
|
2797
2806
|
|
|
@@ -57,12 +57,12 @@ conversation at all — the operator uses the read-only `status` subcommand docu
|
|
|
57
57
|
ai-toolkit executor status --project <path> --run-id <uuid>
|
|
58
58
|
```
|
|
59
59
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
60
|
+
`status` returns task status counts, run status, and (when measurable) total tokens/cost from
|
|
61
|
+
the run's own ledger — see `docs/task-executor-cli.md` for the exact result shape and the
|
|
62
|
+
null-compatibility rules around `totalTokens`/`totalCostUsd`. `diagnose` is still a stub that
|
|
63
|
+
always throws `NOT_IMPLEMENTED` (exit 1, attributed to `US-06-TASK-BE-02`); until it lands,
|
|
64
|
+
deeper per-attempt evidence inspection means using `reconcile`/`resume`, both fully implemented
|
|
65
|
+
and reporting each task's classification.
|
|
66
66
|
|
|
67
67
|
For the full flag reference, exit-code table, and JSON result schemas of every subcommand
|
|
68
68
|
(`start`, `status`, `diagnose`, `stop`, `reconcile`, `resume`, `replan`), see
|
|
@@ -57,6 +57,23 @@ Only `maxConcurrency=1` is implemented — any other requested value is **reject
|
|
|
57
57
|
on any platform other than `win32` (see "Platform qualification" below), before touching any
|
|
58
58
|
other validation.
|
|
59
59
|
|
|
60
|
+
**Permission bypass.** Every real implementation/review worker this loop dispatches is spawned
|
|
61
|
+
with `--permission-mode auto` appended to its CLI args (`EXECUTOR_PERMISSION_BYPASS_ARGS` in
|
|
62
|
+
`lib/task-executor/index.js`). A non-interactive `--print` invocation has no TTY to answer a
|
|
63
|
+
permission prompt, so without this every Write/Edit/mkdir tool call is denied by default,
|
|
64
|
+
regardless of task content — confirmed against a real run where every BE/FE task failed
|
|
65
|
+
verification because its expected files/directories were never created. `auto` alone was then
|
|
66
|
+
independently confirmed sufficient (a live dispatch created all of its task's expected
|
|
67
|
+
directories and files). An earlier version of this fix also appended `--permission-prompts
|
|
68
|
+
none`, citing Tech-Spec OQ-01's pre-Gate-1 spike note as having validated that exact pair —
|
|
69
|
+
reproduced directly against the real, installed CLI (v2.1.220): `--permission-prompts` is not a
|
|
70
|
+
recognized flag at all (`claude --help` lists no such option; passing it exits 1 with `error:
|
|
71
|
+
unknown option '--permission-prompts'` before any model/API call), so that spike note is stale or
|
|
72
|
+
was never run against the real executable as claimed. The mode is deliberately unscoped within
|
|
73
|
+
the task's own `--project` directory — the executor's job is letting a developer agent write
|
|
74
|
+
anywhere under that project, so (unlike pm-phase1/2's read-only-text workers) a narrower
|
|
75
|
+
allowlist is not an option here.
|
|
76
|
+
|
|
60
77
|
### `status`
|
|
61
78
|
|
|
62
79
|
```bash
|
|
@@ -66,11 +83,40 @@ ai-toolkit executor status --project <path> --run-id <uuid>
|
|
|
66
83
|
`--run-id` is required (usage error, exit 2, if omitted). Read-only summary of a run: task
|
|
67
84
|
status counts, current run status, and total cost from the ledger.
|
|
68
85
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
86
|
+
Backs `lib/task-executor/index.js`'s `status()`. Returns:
|
|
87
|
+
|
|
88
|
+
```json
|
|
89
|
+
{
|
|
90
|
+
"protocolVersion": 1,
|
|
91
|
+
"runId": "...",
|
|
92
|
+
"runStatus": "running",
|
|
93
|
+
"taskCounts": { "pending": 0, "active": 0, "checkpointed": 0, "integrated": 0, "skipped": 0, "blocked": 0 },
|
|
94
|
+
"totalTokens": null,
|
|
95
|
+
"totalCostUsd": null
|
|
96
|
+
}
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
`taskCounts` is derived straight from the persisted `state.json` (`store.readState`) — never
|
|
100
|
+
mutates it. `totalTokens` sums every numeric `phase_delta_tokens` entry in the run's own
|
|
101
|
+
`<runId>-token-ledger.json`; `totalCostUsd` additionally requires the target project's own
|
|
102
|
+
`docs/token-pricing.json` (same file and 80/20 input/output split `lib/workflow-artifacts.js`
|
|
103
|
+
uses for feature Token-Estimate documents) and a non-null `model` on each ledger entry to attach
|
|
104
|
+
a rate to. `dispatchTaskAttempt`/`runReview` (`lib/task-executor/index.js`) always open their
|
|
105
|
+
`implementation`/`review` ledger activities with `model: null`, so `totalCostUsd` is `null` until
|
|
106
|
+
that changes — this is a known gap, not a bug: **null means "unavailable", never a fabricated
|
|
107
|
+
zero** (same null-compatibility convention as the Token-Estimate document). `totalTokens` does
|
|
108
|
+
not share that gap: `_runTaskToResolution` now closes every `implementation`/`review` activity
|
|
109
|
+
(`done` with its measured tokens on a normal return, `failed` with `tokens: null` on a genuine
|
|
110
|
+
spawn-level error) immediately after each dispatch, so a completed or still-running real run
|
|
111
|
+
reports real summed tokens — previously these activities were opened and never closed at all, so
|
|
112
|
+
every run's `totalTokens` was unconditionally `null` regardless of outcome. If `runStatus`
|
|
113
|
+
happens to be `'paused'`, the CLI exits 8, same as `execute`/`resume` reporting that status
|
|
114
|
+
directly — the exit code reflects the run's actual state, not which subcommand observed it.
|
|
115
|
+
|
|
116
|
+
If the run is missing or its `state.json` is corrupted, `STATE_NOT_FOUND`/`STATE_CORRUPTED`
|
|
117
|
+
propagate unmodified (exit 4 / exit 1 respectively). An unreadable or unparseable ledger or
|
|
118
|
+
pricing file throws `LEDGER_READ_FAILED`/`LEDGER_CORRUPTED`/`PRICING_READ_FAILED`/
|
|
119
|
+
`PRICING_CORRUPTED` (exit 1) rather than silently reporting a partial or zeroed-out total.
|
|
74
120
|
|
|
75
121
|
### `diagnose`
|
|
76
122
|
|
|
@@ -74,6 +74,41 @@ function _buildProcessTag(runId, taskId, attemptNumber) {
|
|
|
74
74
|
// the tagged "worker" under test, per this task's own testing constraint.
|
|
75
75
|
const DEFAULT_TAGGED_PROCESS_EXE_BASENAME = 'claude.exe';
|
|
76
76
|
|
|
77
|
+
// Permission-bypass flag required for every real (non-test-fixture) worker
|
|
78
|
+
// spawn — implementation AND review alike. Without it, a non-interactive
|
|
79
|
+
// `--print` invocation has no TTY to answer a permission prompt, so every
|
|
80
|
+
// Write/Edit/mkdir tool call is denied by default: confirmed against a real
|
|
81
|
+
// blocked run (INFRA-TASK-INFRA-01's own post-dispatch verification found
|
|
82
|
+
// none of its expected directories had been created) before this constant
|
|
83
|
+
// existed at all. `--permission-mode auto` is a real, documented choice on
|
|
84
|
+
// the installed CLI (`claude --help`: "(choices: \"acceptEdits\", \"auto\",
|
|
85
|
+
// \"bypassPermissions\", \"manual\", \"dontAsk\", \"plan\")") and is
|
|
86
|
+
// deliberately broad (unscoped within the spawned cwd) — the executor's
|
|
87
|
+
// whole job is letting a developer agent write anywhere under the task's
|
|
88
|
+
// project, so a narrower scoped allowlist is not an option here the way it
|
|
89
|
+
// was for pm-phase1/2's read-only-text workers (see
|
|
90
|
+
// lib/workflow-control.js's own doc comment on why THAT subsystem sidesteps
|
|
91
|
+
// Write entirely instead).
|
|
92
|
+
//
|
|
93
|
+
// CORRECTION: an earlier version of this constant also appended
|
|
94
|
+
// `--permission-prompts none`, citing Tech-Spec OQ-01's pre-Gate-1 spike
|
|
95
|
+
// note ("Percorso C", resolved 2026-09-18) as having validated that exact
|
|
96
|
+
// pair. Reproduced directly against the real, currently-installed CLI
|
|
97
|
+
// (v2.1.220): `--permission-prompts` is NOT a recognized flag at all —
|
|
98
|
+
// `claude --print --permission-mode auto --permission-prompts none` fails
|
|
99
|
+
// immediately with `error: unknown option '--permission-prompts'` (exit 1,
|
|
100
|
+
// commander.js argv validation, before any model/API call). Every dispatch
|
|
101
|
+
// would have failed closed again, just with a different symptom (an argv
|
|
102
|
+
// crash instead of a denied Write) — so the historical spike note is either
|
|
103
|
+
// stale against a since-changed CLI surface or was never run against the
|
|
104
|
+
// real executable as literally claimed (this Tech-Spec history already
|
|
105
|
+
// contains one prior instance of an overclaimed, later-audited-away result —
|
|
106
|
+
// see FTR-018-Gate1-Review-Actions.md's own "Correction (2026-09-22)"). Only
|
|
107
|
+
// `--permission-mode auto` is carried forward; it alone was independently
|
|
108
|
+
// confirmed sufficient (a live dispatch created all of its task's expected
|
|
109
|
+
// directories and files).
|
|
110
|
+
const EXECUTOR_PERMISSION_BYPASS_ARGS = ['--permission-mode', 'auto'];
|
|
111
|
+
|
|
77
112
|
// Real, positive-evidence-only liveness query for a tagged process, using
|
|
78
113
|
// the exact E-02-qualified Win32_Process/CIM technique proven in the Gate-1
|
|
79
114
|
// evidence spike, adapted from `Name='node.exe'` to the caller-supplied
|
|
@@ -255,6 +290,44 @@ function _err(code, message) {
|
|
|
255
290
|
return e;
|
|
256
291
|
}
|
|
257
292
|
|
|
293
|
+
// Mirrors lib/workflow-control.js's worker() token-measurement convention
|
|
294
|
+
// exactly (same four usage fields, same "only trust a whole, non-negative
|
|
295
|
+
// total" rule) so this module reports token usage under the same shared
|
|
296
|
+
// definition of "measured" vs "unavailable" the rest of the toolkit uses.
|
|
297
|
+
// Returns null (never 0) when usage is missing, partial, or computes to a
|
|
298
|
+
// non-positive total.
|
|
299
|
+
function _measureTokensFromSpawnResult(spawnResult) {
|
|
300
|
+
const usage = spawnResult && spawnResult.result && spawnResult.result.usage;
|
|
301
|
+
if (!usage) return null;
|
|
302
|
+
const counts = [
|
|
303
|
+
usage.input_tokens, usage.output_tokens,
|
|
304
|
+
usage.cache_read_input_tokens || 0, usage.cache_creation_input_tokens || 0,
|
|
305
|
+
];
|
|
306
|
+
if (!counts.every(function (n) { return Number.isInteger(n) && n >= 0; })) return null;
|
|
307
|
+
const total = counts.reduce(function (a, b) { return a + b; }, 0);
|
|
308
|
+
return Number.isSafeInteger(total) && total > 0 ? total : null;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// Finalizes the 'implementation' or 'review' ledger activity dispatchTaskAttempt/
|
|
312
|
+
// runReview already opened for this attempt (Tech-Spec section 6 ledger-kind
|
|
313
|
+
// strings) — previously left open forever by _runTaskToResolution regardless
|
|
314
|
+
// of outcome (confirmed against a real run: every 'implementation'/'review'
|
|
315
|
+
// entry stayed status:'running' indefinitely, even for permanently-failed
|
|
316
|
+
// tasks). `kind` must be 'implementation' or 'review'; the agent key this
|
|
317
|
+
// reconstructs must match, byte for byte, the literal string
|
|
318
|
+
// dispatchTaskAttempt/runReview used when calling ledger.open (their own
|
|
319
|
+
// 'executor:'+runId+':'+task.id+':'+kind construction) — finalizeActivity
|
|
320
|
+
// requires an exact operation_id match, no name-only fallback.
|
|
321
|
+
function _finalizeDispatchLedgerActivity(executionRoot, runId, taskId, attemptNumber, kind, options) {
|
|
322
|
+
return ledger.finalizeActivity(
|
|
323
|
+
store._executionPaths(executionRoot, runId).runDir,
|
|
324
|
+
runId,
|
|
325
|
+
'executor:' + runId + ':' + taskId + ':' + kind,
|
|
326
|
+
attemptNumber,
|
|
327
|
+
options
|
|
328
|
+
);
|
|
329
|
+
}
|
|
330
|
+
|
|
258
331
|
function _requireNonEmptyString(value, label) {
|
|
259
332
|
if (typeof value !== 'string' || value.length === 0) {
|
|
260
333
|
throw _err('DISPATCH_VALIDATION_ERROR', label + ' must be a non-empty string');
|
|
@@ -529,7 +602,7 @@ async function dispatchTaskAttempt(args) {
|
|
|
529
602
|
const prompt = typeof args.prompt === 'string' ? args.prompt : _buildTaskPrompt(task);
|
|
530
603
|
const baseSpawnArgs = Array.isArray(args.spawnArgs)
|
|
531
604
|
? args.spawnArgs
|
|
532
|
-
: ['--print', '--output-format', 'json', '--agent', identity.nativeName];
|
|
605
|
+
: ['--print', '--output-format', 'json', '--agent', identity.nativeName].concat(EXECUTOR_PERMISSION_BYPASS_ARGS);
|
|
533
606
|
// Append the same processIdentity tag already persisted above as one extra
|
|
534
607
|
// trailing argv element, so it appears verbatim in the spawned process's
|
|
535
608
|
// own command line — this is what makes _findLiveTaggedProcess's later OS
|
|
@@ -625,6 +698,12 @@ function _buildReviewPrompt(task, diff) {
|
|
|
625
698
|
// text> }. When the reply does not match the template (missing/malformed
|
|
626
699
|
// envelope, or free text that doesn't follow it), `passed` is reported as
|
|
627
700
|
// `null` rather than guessed — this function never fabricates a pass.
|
|
701
|
+
// `criticalFindings` captures both the "CRITICAL (blocks merge):" section AND
|
|
702
|
+
// any "Build:"/"Tests:" empirical-verification line carrying the template's
|
|
703
|
+
// own ❌ marker — the agent's contract allows FAIL via either path ("FAIL =
|
|
704
|
+
// 1+ CRITICAL findings OR build/test failure"), so a build/test-driven FAIL
|
|
705
|
+
// is reported just as explainably as a findings-driven one, never as an
|
|
706
|
+
// empty, unexplained list.
|
|
628
707
|
// Authoritative pass/fail enforcement against the Tech-Spec's binding rule
|
|
629
708
|
// ("review passed=true with no blocking finding") is intentionally left to
|
|
630
709
|
// US-04-TASK-BE-03 (closing / combined verification+review outcome
|
|
@@ -655,6 +734,26 @@ function _extractReviewVerdict(spawnResult) {
|
|
|
655
734
|
});
|
|
656
735
|
}
|
|
657
736
|
|
|
737
|
+
// The agent's own contract (src/claude/agents/gaia-review-solution.md,
|
|
738
|
+
// "## Output": "FAIL = 1+ CRITICAL findings OR build/test failure") allows
|
|
739
|
+
// a FAIL verdict with zero CRITICAL findings, driven instead by its
|
|
740
|
+
// "Build: ✅/❌ ..." / "Tests: ✅/❌ ..." empirical-verification lines.
|
|
741
|
+
// Without surfacing those too, a build/test-driven FAIL was
|
|
742
|
+
// indistinguishable from an unexplained one — criticalFindings: [] either
|
|
743
|
+
// way (confirmed against a real run: a review-only task with zero code
|
|
744
|
+
// changes failed review twice with reviewPassed:false, criticalFindings:
|
|
745
|
+
// [], and no other evidence persisted anywhere to tell the two cases
|
|
746
|
+
// apart). Any Build/Tests line carrying the template's own ❌ failure
|
|
747
|
+
// marker is folded in here — it is exactly as blocking per the agent's own
|
|
748
|
+
// documented contract, so the outcome receipt now always explains a FAIL,
|
|
749
|
+
// not only a CRITICAL-findings one.
|
|
750
|
+
const empiricalMatches = text.matchAll(/^[ \t]*(Build|Tests):[ \t]*(.*)$/gim);
|
|
751
|
+
for (const m of empiricalMatches) {
|
|
752
|
+
if (m[2].indexOf('❌') !== -1) {
|
|
753
|
+
criticalFindings.push(m[1] + ': ' + m[2].trim());
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
|
|
658
757
|
return { passed: passed, criticalFindings: criticalFindings };
|
|
659
758
|
}
|
|
660
759
|
|
|
@@ -796,7 +895,7 @@ async function runReview(args) {
|
|
|
796
895
|
const prompt = _buildReviewPrompt(task, diff);
|
|
797
896
|
const spawnArgs = Array.isArray(args.spawnArgs)
|
|
798
897
|
? args.spawnArgs
|
|
799
|
-
: ['--print', '--output-format', 'json', '--agent', identity.nativeName];
|
|
898
|
+
: ['--print', '--output-format', 'json', '--agent', identity.nativeName].concat(EXECUTOR_PERMISSION_BYPASS_ARGS);
|
|
800
899
|
const spawnResult = await claudeProcess.spawnClaudeAgent({
|
|
801
900
|
claudePath: args.claudePath,
|
|
802
901
|
args: spawnArgs,
|
|
@@ -1924,6 +2023,14 @@ async function _runTaskToResolution(ctx) {
|
|
|
1924
2023
|
// via its own try/finally).
|
|
1925
2024
|
throw dispatchErr;
|
|
1926
2025
|
}
|
|
2026
|
+
// Close the 'implementation' ledger activity dispatchTaskAttempt already
|
|
2027
|
+
// opened before this threw — the same persist-before-invoke ordering
|
|
2028
|
+
// that proves the attempt was durably recorded (the check just above)
|
|
2029
|
+
// also proves ledger.open already ran for it. Otherwise this activity
|
|
2030
|
+
// would stay open forever, exactly the gap the success path below also
|
|
2031
|
+
// fixes.
|
|
2032
|
+
_finalizeDispatchLedgerActivity(executionRoot, runId, taskId, attemptNumber, 'implementation',
|
|
2033
|
+
{ status: 'failed', tokens: null, reason: dispatchErr.code || 'UNKNOWN' });
|
|
1927
2034
|
// Genuine spawn-level failure (spawnClaudeAgent itself threw) AFTER
|
|
1928
2035
|
// the attempt was already durably recorded — this DOES consume the
|
|
1929
2036
|
// attempt, exactly like a verification/review failure does.
|
|
@@ -1952,7 +2059,19 @@ async function _runTaskToResolution(ctx) {
|
|
|
1952
2059
|
continue;
|
|
1953
2060
|
}
|
|
1954
2061
|
|
|
1955
|
-
|
|
2062
|
+
// Close the 'implementation' ledger activity opened by dispatchTaskAttempt
|
|
2063
|
+
// — previously left open forever (confirmed against a real run: every
|
|
2064
|
+
// 'implementation'/'review' entry stayed status:'running' indefinitely,
|
|
2065
|
+
// even for tasks that had already reached a terminal 'failed' stage).
|
|
2066
|
+
// "done" here means the subprocess ran to completion and returned a
|
|
2067
|
+
// result, independent of whether verification/review later judges that
|
|
2068
|
+
// result a pass or fail — that judgment is recorded separately, on the
|
|
2069
|
+
// task's own outcome receipt (persistVerificationReviewOutcome below),
|
|
2070
|
+
// never on this activity's ledger status. Mirrors the 'task'-kind
|
|
2071
|
+
// control-only close below (finalizeTaskCheckpoint) in structure, but
|
|
2072
|
+
// carries real measured tokens instead of an always-null control signal.
|
|
2073
|
+
_finalizeDispatchLedgerActivity(executionRoot, runId, taskId, attemptNumber, 'implementation',
|
|
2074
|
+
{ status: 'done', tokens: _measureTokensFromSpawnResult(dispatchResult.spawnResult) });
|
|
1956
2075
|
|
|
1957
2076
|
const verification = await runVerifications({
|
|
1958
2077
|
executionRoot: executionRoot, runId: runId, task: task, attemptNumber: attemptNumber,
|
|
@@ -1972,6 +2091,12 @@ async function _runTaskToResolution(ctx) {
|
|
|
1972
2091
|
claudePath: ctx.claudePath, projectDir: projectDir, diff: diff, taskTimeoutMs: ctx.taskTimeoutMs,
|
|
1973
2092
|
spawnArgs: ctx.reviewSpawnArgs,
|
|
1974
2093
|
});
|
|
2094
|
+
// Close the 'review' ledger activity runReview already opened — same
|
|
2095
|
+
// previously-open-forever gap the 'implementation' close above fixes,
|
|
2096
|
+
// same reasoning: "done" reflects the subprocess completing, not the
|
|
2097
|
+
// review's own pass/fail verdict (that lives on the outcome receipt).
|
|
2098
|
+
_finalizeDispatchLedgerActivity(executionRoot, runId, taskId, attemptNumber, 'review',
|
|
2099
|
+
{ status: 'done', tokens: _measureTokensFromSpawnResult(review.spawnResult) });
|
|
1975
2100
|
}
|
|
1976
2101
|
|
|
1977
2102
|
const outcome = store.persistVerificationReviewOutcome(executionRoot, runId, taskId, attemptNumber, verification, review);
|
|
@@ -2792,14 +2917,121 @@ async function execute(args) {
|
|
|
2792
2917
|
}
|
|
2793
2918
|
}
|
|
2794
2919
|
|
|
2920
|
+
// Reads the run's own token ledger (<executionRoot>/runs/<runId>/<runId>-
|
|
2921
|
+
// token-ledger.json, written by ../execution-ledger.js's open/close/fail/
|
|
2922
|
+
// skip) as a flat array of entries. No activity opened yet is a normal,
|
|
2923
|
+
// non-error state (returns []) — a present-but-unparseable file fails closed
|
|
2924
|
+
// instead, since that is evidence of a corrupted run, not an empty one.
|
|
2925
|
+
function _readRunLedgerEntries(executionRoot, runId) {
|
|
2926
|
+
const ledgerPath = path.join(executionRoot, 'runs', runId, runId + '-token-ledger.json');
|
|
2927
|
+
let raw;
|
|
2928
|
+
try {
|
|
2929
|
+
raw = fs.readFileSync(ledgerPath, 'utf8');
|
|
2930
|
+
} catch (err) {
|
|
2931
|
+
if (err.code === 'ENOENT') return [];
|
|
2932
|
+
throw _err('LEDGER_READ_FAILED', 'status: failed to read token ledger at ' + ledgerPath + ': ' + err.message);
|
|
2933
|
+
}
|
|
2934
|
+
let parsed;
|
|
2935
|
+
try {
|
|
2936
|
+
parsed = JSON.parse(raw);
|
|
2937
|
+
} catch (err) {
|
|
2938
|
+
throw _err('LEDGER_CORRUPTED', 'status: token ledger at ' + ledgerPath + ' is not valid JSON: ' + err.message);
|
|
2939
|
+
}
|
|
2940
|
+
if (!Array.isArray(parsed)) {
|
|
2941
|
+
throw _err('LEDGER_CORRUPTED', 'status: token ledger at ' + ledgerPath + ' must be a JSON array, got ' + typeof parsed);
|
|
2942
|
+
}
|
|
2943
|
+
return parsed;
|
|
2944
|
+
}
|
|
2945
|
+
|
|
2946
|
+
// Loads docs/token-pricing.json from the TARGET project (args.project), not
|
|
2947
|
+
// this toolkit's own checkout — the same file and project-relative
|
|
2948
|
+
// resolution lib/workflow-artifacts.js's pricing() uses for feature
|
|
2949
|
+
// Token-Estimate documents (see lib/workflow-control.js). A missing file
|
|
2950
|
+
// means "cost unavailable", not an error: not every consuming project ships
|
|
2951
|
+
// pricing data, and status must still report token counts for one that
|
|
2952
|
+
// doesn't. A present-but-invalid file fails closed.
|
|
2953
|
+
function _loadTokenPricing(projectDir) {
|
|
2954
|
+
const pricingPath = path.join(projectDir, 'docs', 'token-pricing.json');
|
|
2955
|
+
let raw;
|
|
2956
|
+
try {
|
|
2957
|
+
raw = fs.readFileSync(pricingPath, 'utf8');
|
|
2958
|
+
} catch (err) {
|
|
2959
|
+
if (err.code === 'ENOENT') return null;
|
|
2960
|
+
throw _err('PRICING_READ_FAILED', 'status: failed to read token pricing at ' + pricingPath + ': ' + err.message);
|
|
2961
|
+
}
|
|
2962
|
+
let parsed;
|
|
2963
|
+
try {
|
|
2964
|
+
parsed = JSON.parse(raw);
|
|
2965
|
+
} catch (err) {
|
|
2966
|
+
throw _err('PRICING_CORRUPTED', 'status: token pricing at ' + pricingPath + ' is not valid JSON: ' + err.message);
|
|
2967
|
+
}
|
|
2968
|
+
if (!parsed || !parsed.models || !Number.isFinite(parsed.usd_to_eur) || parsed.usd_to_eur <= 0) {
|
|
2969
|
+
throw _err('PRICING_CORRUPTED', 'status: token pricing at ' + pricingPath + ' is missing "models" or a valid "usd_to_eur" rate');
|
|
2970
|
+
}
|
|
2971
|
+
return parsed;
|
|
2972
|
+
}
|
|
2973
|
+
|
|
2974
|
+
// USD cost for one ledger entry's measured tokens, using the estimated
|
|
2975
|
+
// 80/20 input/output split already established by lib/workflow-artifacts.js's
|
|
2976
|
+
// cost() for the same pricing file — unlike that helper, this never converts
|
|
2977
|
+
// to EUR (the field this feeds is totalCostUsd). Returns null (not 0) when
|
|
2978
|
+
// the entry's model is missing/unknown or its tokens are not a finite number
|
|
2979
|
+
// — an unattributable entry is excluded from the total, never coerced into a
|
|
2980
|
+
// zero-cost one.
|
|
2981
|
+
function _usdCostForLedgerEntry(tokens, model, pricing) {
|
|
2982
|
+
if (!Number.isFinite(tokens) || typeof model !== 'string' || model.length === 0) return null;
|
|
2983
|
+
const rate = pricing.models[model];
|
|
2984
|
+
if (!rate || !Number.isFinite(rate.input_per_1m_usd) || !Number.isFinite(rate.output_per_1m_usd)) return null;
|
|
2985
|
+
return tokens * (0.8 * rate.input_per_1m_usd + 0.2 * rate.output_per_1m_usd) / 1000000;
|
|
2986
|
+
}
|
|
2987
|
+
|
|
2988
|
+
// Null-compatibility (mirrors the convention already established by
|
|
2989
|
+
// src/claude/skills/implement-feature/SKILL.md's Step 7 and
|
|
2990
|
+
// lib/workflow-artifacts.js's pricing/cost helpers): a token value that is
|
|
2991
|
+
// null/undefined/not a finite number is "not yet measured", never coerced
|
|
2992
|
+
// into a real, observable zero. totalTokens/totalCostUsd stay null — not 0 —
|
|
2993
|
+
// when nothing measurable exists yet, so a caller can tell "nothing spent"
|
|
2994
|
+
// apart from "nothing reported". Every executor ledger entry is currently
|
|
2995
|
+
// opened with model: null (see _runTaskToResolution's ledger.open calls
|
|
2996
|
+
// above), so totalCostUsd is null whenever no pricing file is present OR
|
|
2997
|
+
// every entry's model is still unattributed — both are legitimate,
|
|
2998
|
+
// non-error outcomes, not a bug in this function.
|
|
2999
|
+
function _summarizeLedgerTotals(entries, projectDir) {
|
|
3000
|
+
let totalTokens = null;
|
|
3001
|
+
let totalCostUsd = null;
|
|
3002
|
+
let pricing;
|
|
3003
|
+
let pricingLoaded = false;
|
|
3004
|
+
|
|
3005
|
+
entries.forEach(function (entry) {
|
|
3006
|
+
const tokens = entry.phase_delta_tokens;
|
|
3007
|
+
if (!Number.isFinite(tokens)) return;
|
|
3008
|
+
totalTokens = (totalTokens || 0) + tokens;
|
|
3009
|
+
|
|
3010
|
+
if (!pricingLoaded) {
|
|
3011
|
+
pricing = _loadTokenPricing(projectDir);
|
|
3012
|
+
pricingLoaded = true;
|
|
3013
|
+
}
|
|
3014
|
+
if (!pricing) return;
|
|
3015
|
+
|
|
3016
|
+
const entryCost = _usdCostForLedgerEntry(tokens, entry.model, pricing);
|
|
3017
|
+
if (entryCost === null) return;
|
|
3018
|
+
totalCostUsd = (totalCostUsd || 0) + entryCost;
|
|
3019
|
+
});
|
|
3020
|
+
|
|
3021
|
+
return { totalTokens: totalTokens, totalCostUsd: totalCostUsd };
|
|
3022
|
+
}
|
|
3023
|
+
|
|
2795
3024
|
/**
|
|
2796
3025
|
* `status` command. Read-only summary of a run: counts of tasks per
|
|
2797
3026
|
* status, current run status, and total cost as reported by the ledger
|
|
2798
3027
|
* (the sole cost source). Never mutates state.
|
|
2799
3028
|
*
|
|
2800
|
-
*
|
|
2801
|
-
*
|
|
2802
|
-
*
|
|
3029
|
+
* Built entirely from three already-implemented data sources — no new Work
|
|
3030
|
+
* Breakdown task was required: store.readState (INFRA-TASK-BE-01) for task
|
|
3031
|
+
* statuses/runStatus, the run's own token-ledger.json
|
|
3032
|
+
* (INFRA-TASK-BE-02/../execution-ledger.js) for token counts, and the
|
|
3033
|
+
* target project's docs/token-pricing.json (already read the same way by
|
|
3034
|
+
* lib/workflow-artifacts.js) for USD cost attribution.
|
|
2803
3035
|
*
|
|
2804
3036
|
* @param {object} args
|
|
2805
3037
|
* @param {string} args.project - Absolute or project-relative repo path.
|
|
@@ -2808,9 +3040,50 @@ async function execute(args) {
|
|
|
2808
3040
|
* { protocolVersion, runId, runStatus,
|
|
2809
3041
|
* taskCounts: { pending, active, checkpointed, integrated, skipped, blocked },
|
|
2810
3042
|
* totalTokens, totalCostUsd }.
|
|
3043
|
+
* totalTokens/totalCostUsd are null (never 0) when nothing measurable has
|
|
3044
|
+
* been recorded yet, or (totalCostUsd only) when the project ships no
|
|
3045
|
+
* docs/token-pricing.json.
|
|
3046
|
+
* @throws {Error} DISPATCH_VALIDATION_ERROR for a malformed args.project/
|
|
3047
|
+
* args.runId (via _requireNonEmptyString, same as every other command in
|
|
3048
|
+
* this module); any error store.readState throws (STATE_NOT_FOUND,
|
|
3049
|
+
* STATE_CORRUPTED) propagates unmodified — a corrupted/missing run is
|
|
3050
|
+
* never silently defaulted; LEDGER_READ_FAILED/LEDGER_CORRUPTED/
|
|
3051
|
+
* PRICING_READ_FAILED/PRICING_CORRUPTED for an unreadable or unparseable
|
|
3052
|
+
* ledger or pricing file.
|
|
2811
3053
|
*/
|
|
2812
3054
|
async function status(args) {
|
|
2813
|
-
|
|
3055
|
+
args = args || {};
|
|
3056
|
+
_requireNonEmptyString(args.project, 'status: args.project');
|
|
3057
|
+
_requireNonEmptyString(args.runId, 'status: args.runId');
|
|
3058
|
+
|
|
3059
|
+
const projectDir = args.project;
|
|
3060
|
+
const runId = args.runId;
|
|
3061
|
+
const commonDir = _resolveCommonGitDir(projectDir);
|
|
3062
|
+
const executionRoot = path.join(commonDir, 'ai-toolkit', 'execution');
|
|
3063
|
+
|
|
3064
|
+
// Propagates STATE_NOT_FOUND / STATE_CORRUPTED unmodified — never
|
|
3065
|
+
// fabricates an empty/default run for a corrupted/missing one.
|
|
3066
|
+
const state = store.readState(executionRoot, runId);
|
|
3067
|
+
|
|
3068
|
+
const taskCounts = { pending: 0, active: 0, checkpointed: 0, integrated: 0, skipped: 0, blocked: 0 };
|
|
3069
|
+
Object.keys(state.tasks).forEach(function (taskId) {
|
|
3070
|
+
const st = state.tasks[taskId].status;
|
|
3071
|
+
if (Object.prototype.hasOwnProperty.call(taskCounts, st)) {
|
|
3072
|
+
taskCounts[st] += 1;
|
|
3073
|
+
}
|
|
3074
|
+
});
|
|
3075
|
+
|
|
3076
|
+
const ledgerEntries = _readRunLedgerEntries(executionRoot, runId);
|
|
3077
|
+
const totals = _summarizeLedgerTotals(ledgerEntries, projectDir);
|
|
3078
|
+
|
|
3079
|
+
return {
|
|
3080
|
+
protocolVersion: store.PROTOCOL_VERSION,
|
|
3081
|
+
runId: runId,
|
|
3082
|
+
runStatus: state.runStatus,
|
|
3083
|
+
taskCounts: taskCounts,
|
|
3084
|
+
totalTokens: totals.totalTokens,
|
|
3085
|
+
totalCostUsd: totals.totalCostUsd,
|
|
3086
|
+
};
|
|
2814
3087
|
}
|
|
2815
3088
|
|
|
2816
3089
|
/**
|
|
@@ -3726,4 +3999,6 @@ module.exports = {
|
|
|
3726
3999
|
DEFAULT_TAGGED_PROCESS_EXE_BASENAME,
|
|
3727
4000
|
_buildProcessTag,
|
|
3728
4001
|
_findLiveTaggedProcess,
|
|
4002
|
+
_measureTokensFromSpawnResult,
|
|
4003
|
+
_finalizeDispatchLedgerActivity,
|
|
3729
4004
|
};
|