@dtlabs/ai-toolkit 0.16.9 → 0.17.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 CHANGED
@@ -1,4 +1,17 @@
1
1
  ## 0.1.4 (2026-07-21)
2
+ ## [0.17.0](https://github.com/dtlab-tech/ai-toolkit/compare/v0.16.9...v0.17.0) (2026-10-08)
3
+
4
+
5
+ ### Features
6
+
7
+ * **task-executor:** consume replan's carry-over record on start ([f4bdbc1](https://github.com/dtlab-tech/ai-toolkit/commit/f4bdbc1da55a89e82196377a09ed66f274136185))
8
+ * **task-executor:** consume replan's carry-over record on start ([ee669fc](https://github.com/dtlab-tech/ai-toolkit/commit/ee669fc83a6eaf248358f4c3bf9087804074192e))
9
+
10
+
11
+ ### Bug Fixes
12
+
13
+ * **tests:** gate replan carry-over test on Windows only ([bd46109](https://github.com/dtlab-tech/ai-toolkit/commit/bd4610949537106734f6a47f40b2925d9a53c621))
14
+
2
15
  ## [0.16.9](https://github.com/dtlab-tech/ai-toolkit/compare/v0.16.8...v0.16.9) (2026-10-07)
3
16
 
4
17
 
package/bin/cli.js CHANGED
@@ -2642,6 +2642,7 @@ function _executorCurrentBranchRef(projectDir) {
2642
2642
  // mismatch, not an I/O failure): SUCCESSOR_NOT_APPROVED,
2643
2643
  // SUCCESSOR_PLAN_NOT_FOUND, PLAN_NOT_FOUND, PLAN_PARSE_ERROR,
2644
2644
  // REPLAN_UNKNOWN_TASK_MAPPING, ATTEMPT_NUMBER_MISMATCH,
2645
+ // REPLAN_SOURCE_NOT_SUPERSEDED, REPLAN_PLAN_DIGEST_MISMATCH,
2645
2646
  // STATE_READBACK_MISMATCH, STATE_NOT_FOUND, STATE_TASK_NOT_FOUND,
2646
2647
  // STATE_ATTEMPT_NOT_FOUND, STATE_TASK_EXISTS, TREE_MISMATCH,
2647
2648
  // PARENT_MISMATCH, COMMIT_REINSPECTION_MISMATCH,
@@ -2699,6 +2700,7 @@ const EXECUTOR_EXIT_CODE_BY_ERROR_CODE = {
2699
2700
  // 4 — evidence / plan / approval mismatch
2700
2701
  SUCCESSOR_NOT_APPROVED: 4, SUCCESSOR_PLAN_NOT_FOUND: 4, PLAN_NOT_FOUND: 4,
2701
2702
  PLAN_PARSE_ERROR: 4, REPLAN_UNKNOWN_TASK_MAPPING: 4, ATTEMPT_NUMBER_MISMATCH: 4,
2703
+ REPLAN_SOURCE_NOT_SUPERSEDED: 4, REPLAN_PLAN_DIGEST_MISMATCH: 4,
2702
2704
  STATE_READBACK_MISMATCH: 4, STATE_NOT_FOUND: 4, STATE_TASK_NOT_FOUND: 4,
2703
2705
  STATE_ATTEMPT_NOT_FOUND: 4, STATE_TASK_EXISTS: 4, TREE_MISMATCH: 4,
2704
2706
  PARENT_MISMATCH: 4, COMMIT_REINSPECTION_MISMATCH: 4, INTEGRATION_PARENT_MISMATCH: 4,
@@ -2838,6 +2840,7 @@ async function handleExecutorCommand(argv) {
2838
2840
  claudePath: getFlag('--claude-path'),
2839
2841
  taskTimeoutMs: taskTimeoutMsRaw === undefined ? undefined : Number(taskTimeoutMsRaw),
2840
2842
  agentBudgetUsd: agentBudgetUsdRaw === undefined ? undefined : Number(agentBudgetUsdRaw),
2843
+ fromReplanRunId: getFlag('--from-replan-run-id'),
2841
2844
  });
2842
2845
  emitSuccess(result);
2843
2846
 
@@ -70,7 +70,69 @@ For the full flag reference, exit-code table, and JSON result schemas of every s
70
70
 
71
71
  ---
72
72
 
73
- ## 2. Windows-only E-02 qualification
73
+ ## 2. Resolving the Claude CLI executable path
74
+
75
+ `--claude-path` has no documented discovery procedure anywhere in this toolkit —
76
+ `implement-feature`'s SKILL.md only says "ask the user if unknown," and this document, until now,
77
+ only showed `<resolved-claude-cli-path>` as an unexplained placeholder. Found twice independently
78
+ (once resolving it for a real run, once from a fresh install on a different machine reporting no
79
+ standalone `claude` executable anywhere on `PATH`): **try automated discovery first, then
80
+ validate, and only ask the user when discovery is inconclusive** — never silently guess, and
81
+ never trust a path (found or user-supplied) without confirming it actually runs.
82
+
83
+ **Case A — a standalone CLI install.** A bare `claude`/`claude.exe` reachable on `PATH`
84
+ (`where claude` on Windows, `which claude` on macOS/Linux). This is the common case for a
85
+ terminal-only install (e.g. `npm install -g @anthropic-ai/claude-code`).
86
+
87
+ **Case B — Claude Code running as a VS Code (or VS Code-family) extension, no standalone CLI on
88
+ `PATH`.** The extension caches its own Claude Agent SDK binary under the editor's own per-OS
89
+ user-data directory, at:
90
+
91
+ ```
92
+ <editor-userdata-root>/agent-host/sdk-cache/claude/<sdk-version>/<platform>/node_modules/@anthropic-ai/claude-agent-sdk-<platform>/claude[.exe]
93
+ ```
94
+
95
+ `<editor-userdata-root>` is Electron's standard per-OS application-data directory (not an
96
+ extension-specific `globalStorage` subfolder — `agent-host` sits directly under it, a sibling of
97
+ `User`/`Cache`/`logs`):
98
+ - Windows: `%APPDATA%\Code` — confirmed directly on a real machine:
99
+ `C:\Users\<user>\AppData\Roaming\Code\agent-host\sdk-cache\claude\0.3.220\win32-x64\node_modules\@anthropic-ai\claude-agent-sdk-win32-x64\claude.exe`
100
+ - macOS: `~/Library/Application Support/Code` (same relative layout, by VS Code's own standard
101
+ per-OS data-directory convention — not independently verified in this session)
102
+ - Linux: `~/.config/Code` (same caveat)
103
+ - For VS Code Insiders or another fork, substitute `Code` with that variant's own folder name
104
+ (e.g. `Code - Insiders`)
105
+
106
+ Discovery commands (search, do not assume a fixed version — the cached SDK version drifts with
107
+ editor/extension updates):
108
+
109
+ ```powershell
110
+ # Windows
111
+ Get-ChildItem -Path "$env:APPDATA\Code\agent-host\sdk-cache\claude" -Recurse -Filter "claude.exe" -ErrorAction SilentlyContinue |
112
+ Select-Object -ExpandProperty FullName
113
+ ```
114
+
115
+ ```bash
116
+ # macOS/Linux
117
+ find "$HOME/Library/Application Support/Code/agent-host/sdk-cache/claude" \
118
+ "$HOME/.config/Code/agent-host/sdk-cache/claude" \
119
+ -iname "claude" -type f 2>/dev/null
120
+ ```
121
+
122
+ **Validate before trusting any path, found or user-supplied** — this is exactly the kind of
123
+ unattended, potentially costly action (`executor start` spawns real, budget-consuming agent
124
+ processes) this toolkit's own Gate Protocol treats with caution elsewhere:
125
+ - Confirm it is a real, executable file, then run `<path> --version` to confirm it actually
126
+ responds as a Claude CLI — a matching filename is not evidence it works.
127
+ - If discovery finds **zero or more than one** candidate (e.g. several cached SDK versions from
128
+ past extension updates), do not guess which one — present every candidate found to the user and
129
+ have them confirm or choose, per SKILL.md's own "ask the user if unknown" rule. A single,
130
+ unambiguous candidate that passes the `--version` check may be used directly; anything less
131
+ certain goes back to the user before `executor start` ever runs.
132
+
133
+ ---
134
+
135
+ ## 3. Windows-only E-02 qualification
74
136
 
75
137
  The executor's process-supervision guarantees — tree-kill of in-flight attempts, tag-based
76
138
  worker-liveness scanning, and immediate-stop confirmation — are proven for **Windows only**. This
@@ -93,7 +155,7 @@ all rather than degrading silently.
93
155
 
94
156
  ---
95
157
 
96
- ## 3. Persistence caveats
158
+ ## 4. Persistence caveats
97
159
 
98
160
  These are the real, already-confirmed caveats and limitations discovered during this feature's
99
161
  own delivery. Each one is cited to its source rather than paraphrased from memory.
@@ -35,7 +35,7 @@ text-mode output in the current implementation.
35
35
  ### `start`
36
36
 
37
37
  ```bash
38
- ai-toolkit executor start --project <path> --feature <path> [--max-concurrency N] [--claude-path <path>] [--task-timeout-ms <ms>] [--agent-budget-usd <amount>]
38
+ ai-toolkit executor start --project <path> --feature <path> [--max-concurrency N] [--claude-path <path>] [--task-timeout-ms <ms>] [--agent-budget-usd <amount>] [--from-replan-run-id <uuid>]
39
39
  ```
40
40
 
41
41
  | Flag | Required | Default | Notes |
@@ -46,6 +46,7 @@ ai-toolkit executor start --project <path> --feature <path> [--max-concurrency N
46
46
  | `--claude-path` | no | — | Absolute path to the qualified Claude CLI executable |
47
47
  | `--task-timeout-ms` | no | — | Forwarded as a Number |
48
48
  | `--agent-budget-usd` | no | — | Forwarded as a Number; persisted into run config, not itself enforced |
49
+ | `--from-replan-run-id` | no | — | UUID of a prior run `replan` has already superseded; seeds its carry-over-eligible tasks straight to `checkpointed` instead of `pending` (see "Carrying forward a replan" below) |
49
50
 
50
51
  Backs `lib/task-executor/index.js`'s `execute()`. Validates gates/config, acquires the
51
52
  repo-wide execution lease for a brand-new run, builds an immutable plan snapshot from the
@@ -74,6 +75,22 @@ the task's own `--project` directory — the executor's job is letting a develop
74
75
  anywhere under that project, so (unlike pm-phase1/2's read-only-text workers) a narrower
75
76
  allowlist is not an option here.
76
77
 
78
+ **Carrying forward a replan.** `replan` (below) computes which of an original run's completed
79
+ tasks are eligible to carry forward into a corrected successor plan (`carriesCompletion`,
80
+ commit-reachability based) and durably records that decision — but, on its own, never acts on
81
+ it: a plain `start` on the successor plan dispatches every task fresh from `pending`, discarding
82
+ the eligibility `replan` just computed. `--from-replan-run-id <uuid>` is the explicit way to
83
+ actually consume that record: given the UUID of a run `replan` has already superseded, `execute()`
84
+ validates that run really is `superseded`, reads its `replan` intent, and confirms that intent's
85
+ `successorPlanDigest` matches the plan this `start` call just parsed (`REPLAN_PLAN_DIGEST_MISMATCH`
86
+ otherwise, exit 4) — a prior run that was never actually `replan`-ed throws
87
+ `REPLAN_SOURCE_NOT_SUPERSEDED` (exit 4). Only then are the mapped `newTaskId`s whose
88
+ `carriesCompletion` is `true` seeded straight to `checkpointed`, with a synthetic attempt carrying
89
+ the original task's already-reachable `originalSha` (never re-verified, never re-dispatched in the
90
+ successor run — `terminalReason` records the exact old run/task/attempt provenance for later
91
+ `diagnose`/audit). Never inferred/auto-detected: omitting the flag (the default) carries nothing
92
+ forward, exactly like today.
93
+
77
94
  ### `status`
78
95
 
79
96
  ```bash
@@ -218,7 +235,10 @@ way. Validates the caller-supplied old→new task mapping against both the origi
218
235
  tasks and the successor's plan snapshot, computes carry-completion eligibility per pair
219
236
  (read-only), refuses if any original-run attempt's worker liveness is not confirmed dead
220
237
  (`LIVE_WORKER_BLOCKS_REPLAN`), then records the mapping as a durable intent and marks the
221
- original run `superseded`. Never deletes or rewrites any task/attempt evidence.
238
+ original run `superseded`. Never deletes or rewrites any task/attempt evidence. The record this
239
+ writes is not write-only forever: `start`'s own `--from-replan-run-id` (above) is the explicit
240
+ consumer — pass this run's own `runId` there on the successor's `start` call to actually carry the
241
+ eligible tasks forward, instead of the eligibility decision going unused.
222
242
 
223
243
  ---
224
244
 
@@ -2715,6 +2715,14 @@ function createSlotPool(maxConcurrency) {
2715
2715
  * @param {string[]} [args.reviewSpawnArgs] - Test-only override of the full
2716
2716
  * CLI args array forwarded to every runReview call (see runReview's own
2717
2717
  * args.spawnArgs). Omitted in production.
2718
+ * @param {string} [args.fromReplanRunId] - UUID of a prior run that
2719
+ * replan() has already superseded, whose carry-over-eligible tasks
2720
+ * (taskMapping entries with carriesCompletion: true) should be seeded
2721
+ * directly as 'checkpointed' in this new run instead of 'pending' — never
2722
+ * inferred/auto-detected, always an explicit operator decision. The
2723
+ * referenced run's own replan record must have been computed against
2724
+ * EXACTLY this run's plan (successorPlanDigest must match), or this call
2725
+ * fails closed before minting a new run at all.
2718
2726
  * @returns {Promise<object>} Versioned result object:
2719
2727
  * { protocolVersion, runId, runStatus, tasks: [{ taskId, status }] }.
2720
2728
  * @throws {Error} DISPATCH_VALIDATION_ERROR for malformed maxConcurrency;
@@ -2723,9 +2731,14 @@ function createSlotPool(maxConcurrency) {
2723
2731
  * detached with no explicit args.taskRef; PLAN_NOT_FOUND when this
2724
2732
  * feature's own Work-Breakdown.md/.csv cannot be read; PLAN_PARSE_ERROR
2725
2733
  * propagated unmodified from plan.createPlanSnapshot; LEASE_HELD when a
2726
- * repo-wide execution lease is already held; any error propagated
2727
- * unmodified from dispatchTaskAttempt/runVerifications/runReview/git.js/
2728
- * store.js for a systemic (non-task-attempt) failure.
2734
+ * repo-wide execution lease is already held; REPLAN_SOURCE_NOT_SUPERSEDED
2735
+ * when args.fromReplanRunId's own run is not runStatus 'superseded';
2736
+ * REPLAN_PLAN_DIGEST_MISMATCH when its replan record was computed against
2737
+ * a different plan than the one this call just parsed; STATE_NOT_FOUND/
2738
+ * STATE_CORRUPTED/INTENT_NOT_FOUND/INTENT_CORRUPTED propagated unmodified
2739
+ * while reading args.fromReplanRunId's state/replan record; any error
2740
+ * propagated unmodified from dispatchTaskAttempt/runVerifications/
2741
+ * runReview/git.js/store.js for a systemic (non-task-attempt) failure.
2729
2742
  */
2730
2743
  async function execute(args) {
2731
2744
  // Platform qualification guard (US-08-TASK-BE-05) — the single most
@@ -2803,6 +2816,70 @@ async function execute(args) {
2803
2816
  const commonDir = _resolveCommonGitDir(projectDir);
2804
2817
  const executionRoot = path.join(commonDir, 'ai-toolkit', 'execution');
2805
2818
 
2819
+ // Carry-over from a prior replan() decision (US-06-TASK-BE-03's own
2820
+ // "does not auto-start successor" scope boundary — this is the previously
2821
+ // missing other half: an explicit, never-implicit way for a successor
2822
+ // execute() to actually consume what replan() already computed and
2823
+ // durably recorded, instead of that record being write-only forever).
2824
+ // Validated BEFORE minting a new runId/acquiring the lease — a bad
2825
+ // --from-replan-run-id must fail closed before any new run exists at all,
2826
+ // never after. `args.fromReplanRunId` is never inferred/auto-detected
2827
+ // (e.g. by scanning for a 'superseded' run whose replan record's
2828
+ // successorPlanDigest matches this plan) — same "never guess, always
2829
+ // explicit" discipline replan()'s own --task-mapping-json already
2830
+ // established; the operator names the exact source run.
2831
+ let carryOverByNewTaskId = null;
2832
+ if (args.fromReplanRunId !== undefined) {
2833
+ _requireNonEmptyString(args.fromReplanRunId, 'execute: args.fromReplanRunId');
2834
+ const originalRunId = args.fromReplanRunId;
2835
+
2836
+ // Propagates STATE_NOT_FOUND/STATE_CORRUPTED unmodified — a bad run-id
2837
+ // is never silently treated as "nothing carries forward".
2838
+ const originalState = store.readState(executionRoot, originalRunId);
2839
+ if (originalState.runStatus !== 'superseded') {
2840
+ throw _err(
2841
+ 'REPLAN_SOURCE_NOT_SUPERSEDED',
2842
+ 'execute: --from-replan-run-id "' + originalRunId + '" has runStatus "' + originalState.runStatus +
2843
+ '", not "superseded" — only a run replan() has already superseded may be used as a carry-over ' +
2844
+ 'source (never a still-running, paused, or already-completed run)'
2845
+ );
2846
+ }
2847
+
2848
+ // Propagates INTENT_NOT_FOUND/INTENT_CORRUPTED unmodified — replan()
2849
+ // always writes this record BEFORE superseding (see its own step 7/8
2850
+ // ordering), so a superseded run with no readable record here is treated
2851
+ // as corrupted evidence, never as "nothing carries forward".
2852
+ const replanRecord = store.readIntent(executionRoot, originalRunId, 'replan');
2853
+
2854
+ if (replanRecord.successorPlanDigest !== snapshot.planDigest) {
2855
+ throw _err(
2856
+ 'REPLAN_PLAN_DIGEST_MISMATCH',
2857
+ 'execute: the Work Breakdown just parsed for this run (planDigest "' + snapshot.planDigest +
2858
+ '") does not match the successor plan replan() recorded for run "' + originalRunId +
2859
+ '" (planDigest "' + replanRecord.successorPlanDigest + '") — refusing to carry forward ' +
2860
+ 'completions computed against a plan this run is not actually starting'
2861
+ );
2862
+ }
2863
+
2864
+ // Only entries the human/caller already approved as carrying forward
2865
+ // (replanRecord.taskMapping's own carriesCompletion, computed by
2866
+ // replan() from real commit-reachability — see its own doc comment) are
2867
+ // ever applied; every other mapped or unmapped task starts 'pending' and
2868
+ // is dispatched normally.
2869
+ carryOverByNewTaskId = new Map();
2870
+ replanRecord.taskMapping.forEach(function (m) {
2871
+ if (m.carriesCompletion !== true) return;
2872
+ const oldTask = originalState.tasks[m.oldTaskId];
2873
+ const oldAttempt = oldTask.attempts[oldTask.attempts.length - 1];
2874
+ carryOverByNewTaskId.set(m.newTaskId, {
2875
+ oldTaskId: m.oldTaskId,
2876
+ oldAttemptNumber: oldAttempt.number,
2877
+ originalSha: oldAttempt.originalSha,
2878
+ integratedSha: oldAttempt.integratedSha,
2879
+ });
2880
+ });
2881
+ }
2882
+
2806
2883
  const runId = crypto.randomUUID();
2807
2884
  // Fail closed if already held — this is a brand-new run; reclaiming a
2808
2885
  // stale lease is an explicit reconcile/resume/replan decision, never an
@@ -2836,6 +2913,34 @@ async function execute(args) {
2836
2913
  snapshot.tasks.forEach(function (task) {
2837
2914
  state = state.addTask(task.id, { dependencies: task.dependsOn });
2838
2915
  });
2916
+
2917
+ // Seed carry-over tasks straight to 'checkpointed' with a synthetic
2918
+ // attempt recording the ORIGINAL run's already-reachable commit SHA —
2919
+ // never dispatched, never re-verified in this run, exactly mirroring
2920
+ // what a real successful N=1 completion already looks like
2921
+ // (finalizeTaskCheckpoint also only ever registers originalSha, never
2922
+ // integratedSha, for the N=1 case this executor supports today — copied
2923
+ // here verbatim, usually null, never fabricated). 'checkpointed' (not
2924
+ // 'skipped' — that status means "verified+reviewed in THIS run and
2925
+ // found to need no change", a different, honest outcome) is already
2926
+ // terminal-success everywhere that matters: the nonTerminalIds filter,
2927
+ // allTerminalSuccess, and the readyTaskId dependency-satisfaction check
2928
+ // all already treat it as such, unchanged. terminalReason documents the
2929
+ // provenance for any later diagnose/audit — this task has zero real
2930
+ // attempts in the CURRENT run otherwise.
2931
+ if (carryOverByNewTaskId) {
2932
+ carryOverByNewTaskId.forEach(function (carry, newTaskId) {
2933
+ if (!state.tasks[newTaskId]) return; // replan() already validated newTaskId exists; defensive only
2934
+ state = state.addAttempt(newTaskId, { stage: 'committed' });
2935
+ state = state.updateAttempt(newTaskId, 1, {
2936
+ originalSha: carry.originalSha,
2937
+ integratedSha: carry.integratedSha,
2938
+ terminalReason: 'carried-over-from-replan:' + args.fromReplanRunId + ':' + carry.oldTaskId + '#' + carry.oldAttemptNumber,
2939
+ });
2940
+ state = state.setTaskStatus(newTaskId, 'checkpointed');
2941
+ });
2942
+ }
2943
+
2839
2944
  store.writeState(executionRoot, runId, state);
2840
2945
 
2841
2946
  const tasksById = new Map(snapshot.tasks.map(function (t) { return [t.id, t]; }));
@@ -3792,7 +3897,10 @@ function _hasGate2Approval(approvalsText) {
3792
3897
  *
3793
3898
  * Does NOT dispatch, start, or touch the successor run in any way (per its
3794
3899
  * own scope — see the module-level file comment's original stub doc, which
3795
- * this replaces).
3900
+ * this replaces). The record this writes is NOT write-only forever, though:
3901
+ * execute()'s own args.fromReplanRunId explicitly consumes it, seeding any
3902
+ * carriesCompletion:true mapping straight to 'checkpointed' in the
3903
+ * successor run — see execute()'s own doc comment.
3796
3904
  *
3797
3905
  * @param {object} args
3798
3906
  * @param {string} args.executionRoot - Absolute path to
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dtlabs/ai-toolkit",
3
- "version": "0.16.9",
3
+ "version": "0.17.0",
4
4
  "description": "AI Toolkit — reusable agents and procedures for developers to build AI-powered applications.",
5
5
  "files": [
6
6
  "docs/",
@@ -248,7 +248,11 @@ ai-toolkit executor start \
248
248
 
249
249
  Placeholders:
250
250
  - `<path-to-feature.md>` — the feature.md path passed to this skill
251
- - `<resolved-claude-cli-path>` — the Claude CLI executable path on this host; ask the user if unknown
251
+ - `<resolved-claude-cli-path>` — the Claude CLI executable path on this host; try automated
252
+ discovery first (`docs/task-executor-bootstrap.md` section 2 — covers both a standalone CLI on
253
+ `PATH` and the VS Code-extension-cached SDK binary case), validate any candidate with
254
+ `--version` before trusting it, and ask the user only when discovery is inconclusive or finds
255
+ more than one candidate
252
256
  - `<task-timeout-ms>` and `<agent-budget-usd>` — must be supplied explicitly; there is no hidden default timeout or spend
253
257
 
254
258
  **If the host's ordinary terminal capability can launch a durable external command**, launch