@dtlabs/ai-toolkit 0.13.0 → 0.14.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,48 @@
1
1
  ## 0.1.4 (2026-07-21)
2
+ ## [0.14.0](https://github.com/dtlab-tech/ai-toolkit/compare/v0.13.0...v0.14.0) (2026-09-30)
3
+
4
+
5
+ ### Features
6
+
7
+ * **FTR-018:** add agent provenance verification via agents resolve ([27fce79](https://github.com/dtlab-tech/ai-toolkit/commit/27fce7943bda58c546528e839fd83c7c7b2ee1ce))
8
+ * **FTR-018:** add checkpoint intent persistence ([27332e3](https://github.com/dtlab-tech/ai-toolkit/commit/27332e3a38ece600af2f3c3964637dc5f09a5607))
9
+ * **FTR-018:** add Claude subprocess adapter with result validation ([ddcfcd0](https://github.com/dtlab-tech/ai-toolkit/commit/ddcfcd07de8253465a4faf98805956491457706c))
10
+ * **FTR-018:** add commit creation with task trailers ([b11c5af](https://github.com/dtlab-tech/ai-toolkit/commit/b11c5afd995b30c3c8ff136e1449d9c94397e1e7))
11
+ * **FTR-018:** add controlled staging with path enumeration ([81cdc26](https://github.com/dtlab-tech/ai-toolkit/commit/81cdc26904191f189eb9aefc5f86941857368db7))
12
+ * **FTR-018:** add cooperative and immediate stop modes ([2a410ba](https://github.com/dtlab-tech/ai-toolkit/commit/2a410ba0b28d6fdfa9369b439a10e098a250bdf0))
13
+ * **FTR-018:** add DAG validation and cycle detection ([ae08348](https://github.com/dtlab-tech/ai-toolkit/commit/ae0834872f41e339d68a43088c229d6185528d2b))
14
+ * **FTR-018:** add evidence evaluation per edge-cases table ([767a9e0](https://github.com/dtlab-tech/ai-toolkit/commit/767a9e040c6600ae6e7ce81816454bde9923c7ea))
15
+ * **FTR-018:** add exclusive lease protocol ([68f42d1](https://github.com/dtlab-tech/ai-toolkit/commit/68f42d14b87ac6c2672415307248e5a47fada574))
16
+ * **FTR-018:** add executor CLI commands to bin/cli.js ([6b59e65](https://github.com/dtlab-tech/ai-toolkit/commit/6b59e650aebe73b47593c7ee24ed2b803468e53c))
17
+ * **FTR-018:** add executor state protocol (store.js) with versioned schema and atomic writes ([f6d989b](https://github.com/dtlab-tech/ai-toolkit/commit/f6d989b42796d8e37099770e2ebc7fa4f282cab3))
18
+ * **FTR-018:** add ledger finalizeActivity for executor task completion ([be6187a](https://github.com/dtlab-tech/ai-toolkit/commit/be6187aead37c0ab7caf956be867df45c5db8fae))
19
+ * **FTR-018:** add N-limit concurrency control and slot reservation ([6f6cfae](https://github.com/dtlab-tech/ai-toolkit/commit/6f6cfae15d47b6f7f371168bc1ef91db12091236))
20
+ * **FTR-018:** add ownership guard for atomic lease transitions ([a2da308](https://github.com/dtlab-tech/ai-toolkit/commit/a2da3080c5bed54e375a5ce1467a2cc01b0482b7))
21
+ * **FTR-018:** add plan snapshot with content digest ([512bfbf](https://github.com/dtlab-tech/ai-toolkit/commit/512bfbfa6d097d52f7e913f1be16031dd2f214fa))
22
+ * **FTR-018:** add platform qualification guard (Windows-only) ([96c09a0](https://github.com/dtlab-tech/ai-toolkit/commit/96c09a0d00280f8a5893cdaa7bc2a1a190247f9c))
23
+ * **FTR-018:** add replan command with approval validation ([094ef1a](https://github.com/dtlab-tech/ai-toolkit/commit/094ef1a683f91f33350feb314d02adcc932a7e66))
24
+ * **FTR-018:** add resume and reconcile command logic ([be3d9a0](https://github.com/dtlab-tech/ai-toolkit/commit/be3d9a078b00666db8e9521e7e9c714c58f97554))
25
+ * **FTR-018:** add review agent dispatch ([0a463e6](https://github.com/dtlab-tech/ai-toolkit/commit/0a463e6346af977f4bee66bd8b9c443df40112c5))
26
+ * **FTR-018:** add sequential executor main loop (execute()) ([32895ad](https://github.com/dtlab-tech/ai-toolkit/commit/32895ad387d509620d143ca03e63d692aa47f102))
27
+ * **FTR-018:** add serial integration with dispatch-order sequencing ([9681bb7](https://github.com/dtlab-tech/ai-toolkit/commit/9681bb7c853c4c42644a05e6a37fd424ec5e4c0b))
28
+ * **FTR-018:** add stable topological scheduler for task ordering ([63ba632](https://github.com/dtlab-tech/ai-toolkit/commit/63ba632bcccb2ba9996836cf9280e5facec819c6))
29
+ * **FTR-018:** add stale lock detection with process liveness ([391295c](https://github.com/dtlab-tech/ai-toolkit/commit/391295c862ac76e4def7ad6d74fcaac6f2cceb69))
30
+ * **FTR-018:** add task dispatch orchestration ([429c892](https://github.com/dtlab-tech/ai-toolkit/commit/429c892d9f6d2b1b9c53bb2987a8104775e873f3))
31
+ * **FTR-018:** add task-executor module skeleton (index.js) ([339fd57](https://github.com/dtlab-tech/ai-toolkit/commit/339fd5708aa02e55cddc8759be13de05b34aa367))
32
+ * **FTR-018:** add verification command executor ([f03a342](https://github.com/dtlab-tech/ai-toolkit/commit/f03a3425144c3d807e30884874f55e5c76ad6e4b))
33
+ * **FTR-018:** add Work Breakdown CSV parser and phase mapping ([84830ac](https://github.com/dtlab-tech/ai-toolkit/commit/84830ac35ca02615c628d6bd1a96c7cbc2dcc6ff))
34
+ * **FTR-018:** add Work Breakdown Markdown parser (plan.js) ([b26ef0e](https://github.com/dtlab-tech/ai-toolkit/commit/b26ef0e2d949ab4d11278266e67ae8c57565de0c))
35
+ * **FTR-018:** document executor CLI contract; fix unreachable exit code 8 ([3e7a02b](https://github.com/dtlab-tech/ai-toolkit/commit/3e7a02bb341dc8e98f964299b54427eedd06a1f2))
36
+ * **FTR-018:** integrate executor into implement-feature skill ([24eb4cd](https://github.com/dtlab-tech/ai-toolkit/commit/24eb4cdacb477aeed2008199dbe44b56f29d16ce))
37
+ * **runtime:** add deterministic workflow host and owned ledger lifecycle ([aa3c350](https://github.com/dtlab-tech/ai-toolkit/commit/aa3c3503a676c77ff3c76019464a738f8bbc060e))
38
+
39
+
40
+ ### Bug Fixes
41
+
42
+ * **FTR-018:** fix CI failures on Linux (ubuntu-latest / GitHub Actions) ([7054af9](https://github.com/dtlab-tech/ai-toolkit/commit/7054af9eb4f9109063c0254fe29df75fcabe5a4e))
43
+ * **workflows:** deterministic workflow control plane and owned ledger lifecycle ([aec810f](https://github.com/dtlab-tech/ai-toolkit/commit/aec810f2b93b2ccbb38034e66c0d25897f621298))
44
+ * **workflows:** replace LLM control-plane wrappers with deterministic operations ([603ec2f](https://github.com/dtlab-tech/ai-toolkit/commit/603ec2fd197e3e381362ef2fc974977b45b56798))
45
+
2
46
  ## [0.13.0](https://github.com/dtlab-tech/ai-toolkit/compare/v0.12.0...v0.13.0) (2026-09-09)
3
47
 
4
48
 
package/bin/cli.js CHANGED
@@ -1876,7 +1876,9 @@ async function main() {
1876
1876
  return;
1877
1877
  }
1878
1878
 
1879
- if (argv[0] === '--local') {
1879
+ if (argv[0] === 'workflow') {
1880
+ await require('../lib/workflow-runner').main(argv.slice(1));
1881
+ } else if (argv[0] === '--local') {
1880
1882
  await installLocal(argv[1] || '.', force, dryRun);
1881
1883
  } else if (argv[0] === '--global') {
1882
1884
  const homeIdx = argv.indexOf('--home');
@@ -2021,7 +2023,7 @@ async function main() {
2021
2023
  process.stderr.write(`Error spawning script: ${result.error.message}\n`);
2022
2024
  process.exit(1);
2023
2025
  }
2024
- process.exit(result.status || 0);
2026
+ process.exit(Number.isInteger(result.status) ? result.status : 1);
2025
2027
  } catch (err) {
2026
2028
  process.stderr.write(err.message + '\n');
2027
2029
  process.exit(1);
@@ -2111,6 +2113,8 @@ async function main() {
2111
2113
  await handleAgentsCommand(argv.slice(1));
2112
2114
  } else if (argv[0] === 'ledger') {
2113
2115
  handleLedgerCommand(argv.slice(1));
2116
+ } else if (argv[0] === 'executor') {
2117
+ await handleExecutorCommand(argv.slice(1));
2114
2118
  } else if (fs.existsSync(argv[0]) && fs.statSync(argv[0]).isDirectory()) {
2115
2119
  await installLocal(argv[0], force, dryRun);
2116
2120
  } else {
@@ -2554,6 +2558,365 @@ async function handleAgentsCommand(argv) {
2554
2558
  }
2555
2559
  }
2556
2560
 
2561
+ // ── executor CLI git-context helpers ─────────────────────────────────────────
2562
+ // US-09-TASK-INFRA-01 (FTR-018): lib/task-executor/index.js's execute()/stop()
2563
+ // take only `project` and derive commonDir/executionRoot themselves via their
2564
+ // own internal _resolveCommonGitDir/_currentBranchRef helpers — but
2565
+ // reconcile()/resume()/replan() take `executionRoot`/`projectDir`/`taskRef` as
2566
+ // SEPARATE, already-derived arguments instead. Neither internal helper is
2567
+ // exported from that module (module.exports there is deliberately narrow —
2568
+ // see its own export list), and this task's brief is explicit: do not modify
2569
+ // lib/task-executor/index.js. These two helpers duplicate that exact same
2570
+ // git-command technique (same argv, same error codes: GIT_SPAWN_FAILED,
2571
+ // GIT_REF_RESOLUTION_FAILED, EXECUTE_VALIDATION_ERROR for a detached HEAD) so
2572
+ // the CLI can compute the values reconcile/resume/replan need from the single
2573
+ // --project flag the Tech-Spec's CLI contract actually exposes for them.
2574
+ function _executorResolveCommonGitDir(projectDir) {
2575
+ const { spawnSync } = require('child_process');
2576
+ const res = spawnSync('git', ['rev-parse', '--git-common-dir'], {
2577
+ cwd: projectDir, shell: false, encoding: 'utf8', windowsHide: true,
2578
+ });
2579
+ if (res.error || res.status !== 0) {
2580
+ const err = new Error(
2581
+ 'executor: failed to resolve the common Git directory for "' + projectDir + '": ' +
2582
+ (res.error ? res.error.message : (res.stderr || '').trim())
2583
+ );
2584
+ err.code = 'GIT_SPAWN_FAILED';
2585
+ throw err;
2586
+ }
2587
+ return path.resolve(projectDir, res.stdout.trim());
2588
+ }
2589
+
2590
+ function _executorExecutionRoot(projectDir) {
2591
+ return path.join(_executorResolveCommonGitDir(projectDir), 'ai-toolkit', 'execution');
2592
+ }
2593
+
2594
+ function _executorCurrentBranchRef(projectDir) {
2595
+ const { spawnSync } = require('child_process');
2596
+ const res = spawnSync('git', ['rev-parse', '--abbrev-ref', 'HEAD'], {
2597
+ cwd: projectDir, shell: false, encoding: 'utf8', windowsHide: true,
2598
+ });
2599
+ if (res.error || res.status !== 0) {
2600
+ const err = new Error(
2601
+ 'executor: failed to resolve the current branch for "' + projectDir + '": ' +
2602
+ (res.error ? res.error.message : (res.stderr || '').trim())
2603
+ );
2604
+ err.code = 'GIT_REF_RESOLUTION_FAILED';
2605
+ throw err;
2606
+ }
2607
+ const name = res.stdout.trim();
2608
+ if (name === 'HEAD') {
2609
+ const err = new Error(
2610
+ 'executor: "' + projectDir + '" has a detached HEAD — a task ref cannot be derived automatically'
2611
+ );
2612
+ err.code = 'EXECUTE_VALIDATION_ERROR';
2613
+ throw err;
2614
+ }
2615
+ return name;
2616
+ }
2617
+
2618
+ // ── executor exit-code mapping ───────────────────────────────────────────────
2619
+ // FTR-018-Tech-Spec.md section 10 defines a FIXED 9-value exit-code enum for
2620
+ // the executor CLI (0 success; 1 unexpected I/O; 2 invalid config/input;
2621
+ // 3 ownership/live worker conflict; 4 evidence/plan/approval mismatch;
2622
+ // 5 worker/verification/review failure; 6 integration conflict; 7 runtime not
2623
+ // qualified; 8 paused awaiting human decision) — but the lib/task-executor/*.js
2624
+ // + lib/execution-ledger.js + lib/agent-registry.js modules underneath it
2625
+ // collectively throw several dozen distinct `err.code` values. This table is
2626
+ // the single explicit, documented judgment call synthesizing all of them into
2627
+ // the fixed enum; it does not re-derive or duplicate the underlying modules'
2628
+ // own validation, it only classifies their already-thrown codes for exit
2629
+ // status purposes. Grouping rules, in the order applied:
2630
+ //
2631
+ // 3 (ownership/live worker conflict) — every lease/ownership-lock code, and
2632
+ // every code meaning "a competing coordinator or live worker exists":
2633
+ // LEASE_HELD, LEASE_NOT_OWNER, LEASE_STILL_LIVE, LEASE_LIVENESS_UNKNOWN,
2634
+ // LEASE_NOT_FOUND, GUARD_HELD, NO_LOCK_HELD, COMPETING_COORDINATOR_LIVE,
2635
+ // COMPETING_COORDINATOR_LIVENESS_UNKNOWN, LIVE_WORKER_BLOCKS_REPLAN.
2636
+ //
2637
+ // 4 (evidence/plan/approval mismatch) — everything meaning "the plan,
2638
+ // approval, or recorded evidence does not match what was expected",
2639
+ // including "expected evidence/record could not be found" (same bucket as
2640
+ // PLAN_NOT_FOUND, per this task's own brief) and immutable-record replay
2641
+ // conflicts (a conflicting second decision is itself an evidence
2642
+ // mismatch, not an I/O failure): SUCCESSOR_NOT_APPROVED,
2643
+ // SUCCESSOR_PLAN_NOT_FOUND, PLAN_NOT_FOUND, PLAN_PARSE_ERROR,
2644
+ // REPLAN_UNKNOWN_TASK_MAPPING, ATTEMPT_NUMBER_MISMATCH,
2645
+ // STATE_READBACK_MISMATCH, STATE_NOT_FOUND, STATE_TASK_NOT_FOUND,
2646
+ // STATE_ATTEMPT_NOT_FOUND, STATE_TASK_EXISTS, TREE_MISMATCH,
2647
+ // PARENT_MISMATCH, COMMIT_REINSPECTION_MISMATCH,
2648
+ // INTEGRATION_PARENT_MISMATCH, INTEGRATION_REINSPECTION_MISMATCH,
2649
+ // INTEGRATION_WORKTREE_STATE_MISMATCH, INTEGRATION_WORKTREE_NOT_CLEAN,
2650
+ // INTEGRATION_SOURCE_NOT_FOUND, INTEGRATION_SOURCE_NOT_ON_BRANCH,
2651
+ // UNEXPECTED_INDEX_CHANGE, STAGING_MISMATCH, DEFINITION_HASH_MISMATCH,
2652
+ // SHA_REGISTRATION_CONFLICT, SHA_REGISTRATION_STAGE_ERROR,
2653
+ // INTEGRATION_SHA_CONFLICT, INTEGRATION_SHA_STAGE_ERROR,
2654
+ // CHECKPOINT_INTENT_STAGE_ERROR, RECEIPT_CONFLICT, RECEIPT_NOT_FOUND,
2655
+ // INTENT_CONFLICT, INTENT_NOT_FOUND, METADATA_CONFLICT,
2656
+ // ACTIVITY_NOT_FOUND, TERMINAL_CONFLICT.
2657
+ //
2658
+ // 5 (worker/verification/review failure) — the dispatched agent/worker
2659
+ // process itself failed, could not be verified, or could not be spawned:
2660
+ // CLAUDE_SPAWN_FAILED, AGENT_NOT_VERIFIED, RESOLUTION_FAILED.
2661
+ //
2662
+ // 6 (integration conflict) — INTEGRATION_CONFLICT.
2663
+ //
2664
+ // 7 (runtime not qualified) — PLATFORM_NOT_QUALIFIED.
2665
+ //
2666
+ // 2 (invalid config/input) — every *_VALIDATION_ERROR code (there are many;
2667
+ // matched by suffix in mapExecutorErrorToExitCode below rather than
2668
+ // enumerated one-by-one), plus UNSUPPORTED_CONCURRENCY (Tech-Spec:
2669
+ // "reject, never downgrade"), CLAUDE_EXECUTABLE_NOT_FOUND ("unsupported
2670
+ // executable ... fail before mutation"), UNKNOWN_AGENT_TYPE,
2671
+ // METADATA_RESERVED_FIELD, METADATA_UNKNOWN_KEY, INVALID_STATUS.
2672
+ //
2673
+ // 1 (unexpected I/O) — the DEFAULT for everything else, including every
2674
+ // *_CORRUPTED/CORRUPT_* code, every GIT_*_FAILED/*_FAILED git-command-
2675
+ // spawn code, STATE_UNRECOVERABLE, and — explicitly, per this task's own
2676
+ // brief — NOT_IMPLEMENTED. `status`/`diagnose` are still NOT_IMPLEMENTED
2677
+ // stubs in lib/task-executor/index.js (attributed to US-06-TASK-BE-01/02,
2678
+ // neither of which has actually built their real body yet); this CLI
2679
+ // wires them through exactly like the other five commands and lets that
2680
+ // error surface through this same mapping table honestly — it is not
2681
+ // hidden or special-cased, it simply falls into the default bucket.
2682
+ //
2683
+ // 8 (paused awaiting human decision) has no throw site and is deliberately
2684
+ // absent from this table: execute()'s/resume()'s 'paused' run status is a
2685
+ // SUCCESS-path result field (returned, not thrown), so no error code maps to
2686
+ // exit 8 here. It is instead selected on the success path, in
2687
+ // handleExecutorCommand's own emitSuccess helper below, by inspecting
2688
+ // result.runStatus.
2689
+ const EXECUTOR_EXIT_CODE_BY_ERROR_CODE = {
2690
+ // 3 — ownership / live worker conflict
2691
+ LEASE_HELD: 3, LEASE_NOT_OWNER: 3, LEASE_STILL_LIVE: 3, LEASE_LIVENESS_UNKNOWN: 3,
2692
+ LEASE_NOT_FOUND: 3, GUARD_HELD: 3, NO_LOCK_HELD: 3, COMPETING_COORDINATOR_LIVE: 3,
2693
+ COMPETING_COORDINATOR_LIVENESS_UNKNOWN: 3, LIVE_WORKER_BLOCKS_REPLAN: 3,
2694
+
2695
+ // 4 — evidence / plan / approval mismatch
2696
+ SUCCESSOR_NOT_APPROVED: 4, SUCCESSOR_PLAN_NOT_FOUND: 4, PLAN_NOT_FOUND: 4,
2697
+ PLAN_PARSE_ERROR: 4, REPLAN_UNKNOWN_TASK_MAPPING: 4, ATTEMPT_NUMBER_MISMATCH: 4,
2698
+ STATE_READBACK_MISMATCH: 4, STATE_NOT_FOUND: 4, STATE_TASK_NOT_FOUND: 4,
2699
+ STATE_ATTEMPT_NOT_FOUND: 4, STATE_TASK_EXISTS: 4, TREE_MISMATCH: 4,
2700
+ PARENT_MISMATCH: 4, COMMIT_REINSPECTION_MISMATCH: 4, INTEGRATION_PARENT_MISMATCH: 4,
2701
+ INTEGRATION_REINSPECTION_MISMATCH: 4, INTEGRATION_WORKTREE_STATE_MISMATCH: 4,
2702
+ INTEGRATION_WORKTREE_NOT_CLEAN: 4, INTEGRATION_SOURCE_NOT_FOUND: 4,
2703
+ INTEGRATION_SOURCE_NOT_ON_BRANCH: 4, UNEXPECTED_INDEX_CHANGE: 4, STAGING_MISMATCH: 4,
2704
+ DEFINITION_HASH_MISMATCH: 4, SHA_REGISTRATION_CONFLICT: 4, SHA_REGISTRATION_STAGE_ERROR: 4,
2705
+ INTEGRATION_SHA_CONFLICT: 4, INTEGRATION_SHA_STAGE_ERROR: 4, CHECKPOINT_INTENT_STAGE_ERROR: 4,
2706
+ RECEIPT_CONFLICT: 4, RECEIPT_NOT_FOUND: 4, INTENT_CONFLICT: 4, INTENT_NOT_FOUND: 4,
2707
+ METADATA_CONFLICT: 4, ACTIVITY_NOT_FOUND: 4, TERMINAL_CONFLICT: 4,
2708
+
2709
+ // 5 — worker / verification / review failure
2710
+ CLAUDE_SPAWN_FAILED: 5, AGENT_NOT_VERIFIED: 5, RESOLUTION_FAILED: 5,
2711
+
2712
+ // 6 — integration conflict
2713
+ INTEGRATION_CONFLICT: 6,
2714
+
2715
+ // 7 — runtime not qualified
2716
+ PLATFORM_NOT_QUALIFIED: 7,
2717
+
2718
+ // 2 — invalid config/input (non-*_VALIDATION_ERROR entries; every
2719
+ // *_VALIDATION_ERROR code is matched by suffix in mapExecutorErrorToExitCode
2720
+ // below rather than listed here)
2721
+ UNSUPPORTED_CONCURRENCY: 2, CLAUDE_EXECUTABLE_NOT_FOUND: 2, UNKNOWN_AGENT_TYPE: 2,
2722
+ METADATA_RESERVED_FIELD: 2, METADATA_UNKNOWN_KEY: 2, INVALID_STATUS: 2,
2723
+
2724
+ // 1 — unexpected I/O (explicit entries kept for documentation clarity even
2725
+ // though 1 is also the default fallback below)
2726
+ STATE_CORRUPTED: 1, RECEIPT_CORRUPTED: 1, INTENT_CORRUPTED: 1, LEASE_CORRUPTED: 1,
2727
+ CORRUPT_LEDGER: 1, STATE_UNRECOVERABLE: 1, NOT_IMPLEMENTED: 1,
2728
+ GIT_SPAWN_FAILED: 1, GIT_STATUS_FAILED: 1, GIT_ADD_FAILED: 1, GIT_WRITE_TREE_FAILED: 1,
2729
+ GIT_DIFF_FAILED: 1, GIT_COMMIT_TREE_FAILED: 1, REF_UPDATE_FAILED: 1, GIT_CAT_FILE_FAILED: 1,
2730
+ GIT_MERGE_BASE_FAILED: 1, GIT_REV_PARSE_FAILED: 1, GIT_CHERRY_PICK_FAILED: 1,
2731
+ GIT_REF_RESOLUTION_FAILED: 1, INTEGRATION_CLEANUP_FAILED: 1,
2732
+ };
2733
+
2734
+ function mapExecutorErrorToExitCode(code) {
2735
+ if (typeof code === 'string' && Object.prototype.hasOwnProperty.call(EXECUTOR_EXIT_CODE_BY_ERROR_CODE, code)) {
2736
+ return EXECUTOR_EXIT_CODE_BY_ERROR_CODE[code];
2737
+ }
2738
+ // Suffix-based catch-all for the many *_VALIDATION_ERROR codes
2739
+ // (DISPATCH_VALIDATION_ERROR, LEASE_VALIDATION_ERROR,
2740
+ // CLAUDE_PROCESS_VALIDATION_ERROR, STATE_VALIDATION_ERROR,
2741
+ // STAGING_VALIDATION_ERROR, DETECT_CHANGED_PATHS_VALIDATION_ERROR,
2742
+ // COMMIT_VALIDATION_ERROR, INTEGRATION_VALIDATION_ERROR,
2743
+ // WORKTREE_VALIDATION_ERROR, SLOT_POOL_VALIDATION_ERROR,
2744
+ // SHA_REGISTRATION_VALIDATION_ERROR, INTEGRATION_SHA_VALIDATION_ERROR,
2745
+ // CHECKPOINT_INTENT_VALIDATION_ERROR, EXECUTE_VALIDATION_ERROR,
2746
+ // STOP_VALIDATION_ERROR, and any future addition following the same naming
2747
+ // convention) — every one of them means "malformed caller input", i.e.
2748
+ // exit 2, without needing its own table row.
2749
+ if (typeof code === 'string' && code.endsWith('_VALIDATION_ERROR')) {
2750
+ return 2;
2751
+ }
2752
+ // Unknown/unmapped code, including a missing .code entirely: safe default.
2753
+ return 1;
2754
+ }
2755
+
2756
+ // ── executor dispatcher ───────────────────────────────────────────────────────
2757
+ // US-09-TASK-INFRA-01 (FTR-018): Routes `ai-toolkit executor <subcommand>
2758
+ // [flags]` to lib/task-executor/index.js's execute/status/diagnose/stop/
2759
+ // reconcile/resume/replan (the seven commands of the CLI contract in
2760
+ // FTR-018-Tech-Spec.md section 10). All executor domain logic lives in
2761
+ // lib/task-executor/*.js; this function only parses CLI flags, derives the
2762
+ // couple of git-context values reconcile()/resume()/replan() require but do
2763
+ // not derive themselves (see _executorExecutionRoot/_executorCurrentBranchRef
2764
+ // above), and maps thrown error codes to the Tech-Spec's fixed exit-code enum
2765
+ // (see mapExecutorErrorToExitCode above) — it is NOT a second copy of any
2766
+ // executor logic.
2767
+ //
2768
+ // Flag-rejection mechanism for "no --force*, lock/state path overrides,
2769
+ // implicit --abandon or runtime concurrency adjustment" (Tech-Spec section
2770
+ // 10): this parser simply never recognizes such flags for any subcommand
2771
+ // below. An unrecognized flag is silently ignored rather than explicitly
2772
+ // rejected — there is nothing to reject because nothing here ever reads an
2773
+ // arbitrary flag by name and forwards it to a filesystem path or lock
2774
+ // primitive; only the whitelisted flags per subcommand are ever read via
2775
+ // getFlag()/hasFlag().
2776
+ async function handleExecutorCommand(argv) {
2777
+ const subcommand = argv[0];
2778
+ const remaining = argv.slice(1);
2779
+
2780
+ function getFlag(name) {
2781
+ const idx = remaining.indexOf(name);
2782
+ return idx !== -1 && remaining[idx + 1] !== undefined ? remaining[idx + 1] : undefined;
2783
+ }
2784
+
2785
+ function emitSuccess(result) {
2786
+ process.stdout.write(JSON.stringify(result, null, 2) + '\n');
2787
+ // Tech-Spec section 10 exit code 8 ("paused awaiting human decision") is a
2788
+ // SUCCESS-path outcome, not an error — only execute()'s and resume()'s
2789
+ // result objects can legitimately carry `runStatus: 'paused'` (see the
2790
+ // exit-code mapping comment above EXECUTOR_EXIT_CODE_BY_ERROR_CODE for why
2791
+ // no *_ERROR code maps to 8). reconcile/stop/replan results never carry a
2792
+ // `runStatus` field at all, and status/diagnose never reach this success
2793
+ // path (still NOT_IMPLEMENTED) — so this check is a no-op for every other
2794
+ // subcommand's result shape and never needs to be scoped by subcommand.
2795
+ process.exitCode = result && result.runStatus === 'paused' ? 8 : 0;
2796
+ }
2797
+
2798
+ function emitError(err) {
2799
+ const code = err && err.code ? err.code : undefined;
2800
+ const exitCode = mapExecutorErrorToExitCode(code);
2801
+ process.stderr.write(JSON.stringify({
2802
+ status: 'error',
2803
+ code: code || 'UNKNOWN',
2804
+ message: err && err.message ? err.message : String(err),
2805
+ }, null, 2) + '\n');
2806
+ process.exitCode = exitCode;
2807
+ }
2808
+
2809
+ function usageError(message) {
2810
+ process.stderr.write('Error: ' + message + '\n');
2811
+ process.exitCode = 2;
2812
+ }
2813
+
2814
+ const projectDir = path.resolve(getFlag('--project') || process.cwd());
2815
+
2816
+ try {
2817
+ const taskExecutor = require('../lib/task-executor');
2818
+
2819
+ if (subcommand === 'start') {
2820
+ const feature = getFlag('--feature');
2821
+ if (!feature) { usageError('executor start requires --feature'); return; }
2822
+ const maxConcurrencyRaw = getFlag('--max-concurrency');
2823
+ const taskTimeoutMsRaw = getFlag('--task-timeout-ms');
2824
+ const agentBudgetUsdRaw = getFlag('--agent-budget-usd');
2825
+ const result = await taskExecutor.execute({
2826
+ project: projectDir,
2827
+ feature: feature,
2828
+ maxConcurrency: maxConcurrencyRaw === undefined ? undefined : Number(maxConcurrencyRaw),
2829
+ claudePath: getFlag('--claude-path'),
2830
+ taskTimeoutMs: taskTimeoutMsRaw === undefined ? undefined : Number(taskTimeoutMsRaw),
2831
+ agentBudgetUsd: agentBudgetUsdRaw === undefined ? undefined : Number(agentBudgetUsdRaw),
2832
+ });
2833
+ emitSuccess(result);
2834
+
2835
+ } else if (subcommand === 'status') {
2836
+ const runId = getFlag('--run-id');
2837
+ if (!runId) { usageError('executor status requires --run-id'); return; }
2838
+ const result = await taskExecutor.status({ project: projectDir, runId: runId });
2839
+ emitSuccess(result);
2840
+
2841
+ } else if (subcommand === 'diagnose') {
2842
+ const runId = getFlag('--run-id');
2843
+ if (!runId) { usageError('executor diagnose requires --run-id'); return; }
2844
+ const result = await taskExecutor.diagnose({ project: projectDir, runId: runId });
2845
+ emitSuccess(result);
2846
+
2847
+ } else if (subcommand === 'stop') {
2848
+ const runId = getFlag('--run-id');
2849
+ if (!runId) { usageError('executor stop requires --run-id'); return; }
2850
+ const result = await taskExecutor.stop({
2851
+ project: projectDir,
2852
+ runId: runId,
2853
+ mode: getFlag('--mode') || 'graceful',
2854
+ });
2855
+ emitSuccess(result);
2856
+
2857
+ } else if (subcommand === 'reconcile' || subcommand === 'resume') {
2858
+ const runId = getFlag('--run-id');
2859
+ if (!runId) { usageError('executor ' + subcommand + ' requires --run-id'); return; }
2860
+ const executionRoot = _executorExecutionRoot(projectDir);
2861
+ const taskRef = _executorCurrentBranchRef(projectDir);
2862
+ const fn = subcommand === 'reconcile' ? taskExecutor.reconcile : taskExecutor.resume;
2863
+ const result = await fn({
2864
+ executionRoot: executionRoot,
2865
+ runId: runId,
2866
+ projectDir: projectDir,
2867
+ taskRef: taskRef,
2868
+ });
2869
+ emitSuccess(result);
2870
+
2871
+ } else if (subcommand === 'replan') {
2872
+ const runId = getFlag('--run-id');
2873
+ const feature = getFlag('--feature');
2874
+ if (!runId) { usageError('executor replan requires --run-id'); return; }
2875
+ if (!feature) { usageError('executor replan requires --feature (the approved successor plan)'); return; }
2876
+
2877
+ // --task-mapping-json is NOT one of the Tech-Spec table's listed
2878
+ // arguments for replan (only --run-id/--feature are) — but
2879
+ // lib/task-executor/index.js's replan() requires args.taskMapping to be
2880
+ // an array (DISPATCH_VALIDATION_ERROR otherwise) and never invents one
2881
+ // itself (see its own doc comment: "taskMapping is an explicit,
2882
+ // caller-supplied old->new task ID mapping"). Following this session's
2883
+ // established pattern of flagging gaps rather than papering over them:
2884
+ // this flag is documented here as a necessary CLI-layer extension
2885
+ // beyond the literal table, using the same `--*-json` structured-input
2886
+ // convention the ledger subcommand already established
2887
+ // (--metadata-json). Omitting it defaults to an empty mapping (a valid
2888
+ // array — "no task carries forward" — never rejected by replan()).
2889
+ let taskMapping = [];
2890
+ const taskMappingJson = getFlag('--task-mapping-json');
2891
+ if (taskMappingJson !== undefined) {
2892
+ try {
2893
+ taskMapping = JSON.parse(taskMappingJson);
2894
+ } catch (parseErr) {
2895
+ usageError('--task-mapping-json is not valid JSON: ' + parseErr.message);
2896
+ return;
2897
+ }
2898
+ }
2899
+
2900
+ const executionRoot = _executorExecutionRoot(projectDir);
2901
+ const taskRef = _executorCurrentBranchRef(projectDir);
2902
+ const result = await taskExecutor.replan({
2903
+ executionRoot: executionRoot,
2904
+ runId: runId,
2905
+ projectDir: projectDir,
2906
+ taskRef: taskRef,
2907
+ feature: feature,
2908
+ taskMapping: taskMapping,
2909
+ });
2910
+ emitSuccess(result);
2911
+
2912
+ } else {
2913
+ usageError('unknown executor subcommand: ' + (subcommand || '(none)'));
2914
+ }
2915
+ } catch (err) {
2916
+ emitError(err);
2917
+ }
2918
+ }
2919
+
2557
2920
  // ── entry point guard ─────────────────────────────────────────────────────────
2558
2921
  // Run the CLI only when invoked directly (node bin/cli.js).
2559
2922
  // When required as a module (e.g., by Jest), skip main() and export pure
@@ -2600,5 +2963,11 @@ if (require.main === module) {
2600
2963
  resolveFeaturesRoot,
2601
2964
  computeFileSha256,
2602
2965
  handleAgentsCommand,
2966
+ handleExecutorCommand,
2967
+ mapExecutorErrorToExitCode,
2968
+ EXECUTOR_EXIT_CODE_BY_ERROR_CODE,
2969
+ _executorResolveCommonGitDir,
2970
+ _executorExecutionRoot,
2971
+ _executorCurrentBranchRef,
2603
2972
  };
2604
2973
  }
@@ -0,0 +1,195 @@
1
+ # Task Executor Bootstrap
2
+
3
+ > Companion to [`docs/task-executor-cli.md`](task-executor-cli.md), which documents the CLI
4
+ > contract (commands, flags, exit codes, JSON schema) in detail and is not repeated here. This
5
+ > document covers the **operational** side: how a real operator/coordinator starts a run after
6
+ > Gates 1/2, the Windows-only qualification that gates whether `start` will run at all, and the
7
+ > persistence caveats an operator must know about before trusting a run's state — all pulled
8
+ > from FTR-018's own approved artifacts and test findings, not restated from memory.
9
+
10
+ ---
11
+
12
+ ## 1. The supervised delivery procedure after Gates 1/2
13
+
14
+ Once a feature's Requirements, Tech-Spec and Work Breakdown are approved (Gate 1 and Gate 2 both
15
+ recorded in `{PREFIX}-Approvals.md`), the `implement-feature` skill's **Step 6 — Launch the task
16
+ executor (Implementation Phase)** (`src/claude/skills/implement-feature/SKILL.md`) takes over.
17
+
18
+ The key behavioral change from the retired `pm-phase3` model, as stated in Step 6 itself:
19
+
20
+ - Implementation is **no longer dispatched as a subagent**. There is no `pm-phase3` workflow, no
21
+ Tier-1 dispatch guard, and no `pm-phase3:dispatch` ledger entry for this step.
22
+ - Once Gate 2 is approved, the skill's job is limited to **presenting or launching one explicit
23
+ `task-executor` start command** — the executor process itself then owns the repo, plans,
24
+ executes, checkpoints and resumes tasks deterministically, independent of the chat.
25
+ - **No subagent orchestrates the run, and no assistant or agent involvement is required for each
26
+ task transition once the run has started.** A single durable command launch is enough, and the
27
+ run may keep going, checkpointing and resuming, even after the launching conversation ends.
28
+
29
+ Concretely (Step 6a), the skill builds:
30
+
31
+ ```bash
32
+ ai-toolkit executor start \
33
+ --project . \
34
+ --feature <path-to-feature.md> \
35
+ --max-concurrency 1 \
36
+ --claude-path <resolved-claude-cli-path> \
37
+ --task-timeout-ms <task-timeout-ms, e.g. 900000> \
38
+ --agent-budget-usd <per-agent budget in USD, e.g. 5.00>
39
+ ```
40
+
41
+ If the host's ordinary terminal capability can launch a durable external command, the skill
42
+ launches it directly so the process persists independently of the chat. If not, it shows the
43
+ exact command for the user to run themselves in a terminal — it does **not** fake a durable
44
+ launch by running it as a blocking in-chat command.
45
+
46
+ In both cases, the `run-id` printed by `start` must be captured — it is required for every
47
+ subsequent `status`, `diagnose`, `stop`, `reconcile`, `resume`, and `replan` call; there is no
48
+ implicit last-run selection.
49
+
50
+ **The run may outlive the launching conversation.** The skill explicitly does not block on the
51
+ run reaching a terminal state; it proceeds to its own Step 7 with whatever the latest status
52
+ reports at that point. To check on a run later — from the same conversation, a new one, or no
53
+ conversation at all — the operator uses the read-only `status` subcommand documented in
54
+ `docs/task-executor-cli.md`:
55
+
56
+ ```bash
57
+ ai-toolkit executor status --project <path> --run-id <uuid>
58
+ ```
59
+
60
+ Note, per the CLI reference, that `status` (like `diagnose`) is currently a stub that always
61
+ throws `NOT_IMPLEMENTED` (exit 1) — the command surface and its intended read-only summary shape
62
+ (task status counts, run status, total cost from the ledger) are defined, but the real body has
63
+ not been built yet (attributed there to `US-06-TASK-BE-01`). Until it lands, checking on a
64
+ running or finished executor run means reading the ledger and per-run state directly, or using
65
+ `reconcile`/`resume`, both of which are fully implemented and report each task's classification.
66
+
67
+ For the full flag reference, exit-code table, and JSON result schemas of every subcommand
68
+ (`start`, `status`, `diagnose`, `stop`, `reconcile`, `resume`, `replan`), see
69
+ `docs/task-executor-cli.md` — this document intentionally does not restate that contract.
70
+
71
+ ---
72
+
73
+ ## 2. Windows-only E-02 qualification
74
+
75
+ The executor's process-supervision guarantees — tree-kill of in-flight attempts, tag-based
76
+ worker-liveness scanning, and immediate-stop confirmation — are proven for **Windows only**. This
77
+ is FTR-018's Gate-1 binding constraint #3 (`FTR-018-Approvals.md`):
78
+
79
+ > **E-02 is Windows-only qualified.** Other platforms require dedicated proofs before their
80
+ > execution is enabled; no implicit cross-platform guarantee.
81
+
82
+ This is not a paper constraint: `execute()` in `lib/task-executor/index.js` checks
83
+ `process.platform !== 'win32'` as its **very first validation step** — before any other argument
84
+ validation, before deriving the execution root, before touching the plan snapshot, and before
85
+ acquiring the ownership lease — and throws `PLATFORM_NOT_QUALIFIED` (CLI exit code 7) on any
86
+ non-Windows platform (US-08-TASK-BE-05, behaviorally tested by US-08-TASK-TEST-02's
87
+ `platform-guard.test.js`, which asserts no agent dispatch and no worktree creation occur on a
88
+ simulated non-win32 platform).
89
+
90
+ In other words: there is **no implicit cross-platform guarantee, and no partial or best-effort
91
+ execution on other platforms**. On any platform other than Windows, `start` refuses to run at
92
+ all rather than degrading silently.
93
+
94
+ ---
95
+
96
+ ## 3. Persistence caveats
97
+
98
+ These are the real, already-confirmed caveats and limitations discovered during this feature's
99
+ own delivery. Each one is cited to its source rather than paraphrased from memory.
100
+
101
+ ### a. E-01 residual: worktree persistence not re-reproduced in the isolated proof run
102
+
103
+ Per `FTR-018-Approvals.md`, Gate 1 section (OQ-01a / E-01 sub-point), the 2026-09-28 binding
104
+ closure of the agent-provenance proof carries an explicit residual:
105
+
106
+ > **Residual:** persistence not re-reproduced in this run (Write permission-denied); proven
107
+ > separately by the original Test B.
108
+
109
+ That is: the isolated 2026-09-28 proof run that closed the *definition-binding* question (verified
110
+ agent-hash → loaded definition) did **not** itself re-demonstrate worktree persistence, because
111
+ the Write tool was permission-denied in that isolated cwd. Persistence is proven by a separate,
112
+ earlier proof (the original Test B referenced in the same evidence record), not by this run. An
113
+ operator relying only on the E-01 dossier for "the whole chain is proven end-to-end in one run"
114
+ should know that persistence and definition-binding were proven in two separate runs, not one.
115
+
116
+ ### b. Windows filesystem-race flake on atomic write (operational, not a code defect)
117
+
118
+ `store.js`'s state persistence uses a temp-write-then-rename atomic-write protocol
119
+ (`_atomicWriteFileSync` in `lib/task-executor/store.js`: open a same-directory exclusive temp
120
+ file, write, `fsyncSync`, close, then `fs.renameSync(tmpPath, filePath)`). This is the same
121
+ atomic-write discipline the executor's own production state persistence (`writeState`) relies on.
122
+
123
+ **Operational note:** under heavy concurrent I/O load on Windows (e.g. many parallel Jest workers
124
+ writing state files simultaneously), this rename step can occasionally hit a transient
125
+ filesystem error (EPERM / rename race) that is environment-load-dependent rather than
126
+ deterministic — it has not reproduced when run in isolation, and is not a defect in the
127
+ atomic-write logic itself. Anyone operating or testing the executor under high Windows I/O
128
+ contention should expect this as a known environmental characteristic, not treat an occasional
129
+ transient failure of this shape as evidence of a code bug.
130
+
131
+ ### c. `resume()`/`reconcile()` only inspect and report — no automatic continuation
132
+
133
+ Confirmed by US-07-TASK-TEST-01 (`tests/task-executor/sequential.test.js`, commit `25b2663`):
134
+
135
+ > Finding, confirmed and NOT worked around: resume()/reconcile() only inspect and report - there
136
+ > is currently no way to actually continue/finish an interrupted run (execute() always mints a
137
+ > fresh runId and resets all tasks to pending). This is a known, explicit architecture gap, not a
138
+ > bug - documented in the test file for any future work that wants to add real continuation.
139
+
140
+ **Operator implication:** "resuming" a run today means safely inspecting what has already been
141
+ durably completed and getting an evidence-based classification per task — it does **not** mean
142
+ the interrupted run will automatically finish its remaining tasks. `execute()` always starts a
143
+ brand-new run (a fresh `runId`, all tasks reset to pending); there is no run loop wired into
144
+ `resume()`.
145
+
146
+ ### d. Dormant concurrency gap in `dispatchTaskAttempt` (must be resolved before N>1 wiring)
147
+
148
+ Confirmed by US-08-TASK-TEST-01 (`tests/task-executor/parallel.test.js`, commit `2f06883`),
149
+ Finding B:
150
+
151
+ > a real, currently-dormant concurrency gap: two concurrent dispatchTaskAttempt calls requesting
152
+ > the IDENTICAL, not-yet-existing attemptNumber for the same task both dispatch real subprocesses
153
+ > - neither is rejected, only one attempt record persists, and its processIdentity is
154
+ > byte-identical for both calls (derived only from runId/taskId/attemptNumber), so persisted state
155
+ > cannot even reveal two real processes ran. Currently unreachable via any real production entry
156
+ > point (execute()'s only caller dispatches one attempt at a time sequentially, and no N>1
157
+ > orchestrator exists), but flagged prominently for whoever eventually wires real N>1 dispatch.
158
+
159
+ **Operator implication:** this race cannot fire today because `execute()` is strictly
160
+ sequential (`maxConcurrency=1` only; see docs/task-executor-cli.md — any other value is
161
+ rejected as `UNSUPPORTED_CONCURRENCY`, never silently downgraded) and no orchestrator calls
162
+ `dispatchTaskAttempt` concurrently. It is a **constraint that must be resolved before any future
163
+ N>1 wiring work** lands, not something safe to rely on or ignore once that work begins.
164
+
165
+ ### e. Verification receipts already on disk are not consulted on resume (efficiency gap)
166
+
167
+ Confirmed by US-09-TASK-TEST-01 (`tests/task-executor/fault-injection.test.js`, commit `e601e3d`),
168
+ Finding 1:
169
+
170
+ > there is no distinct durable state for "verification passed, review not yet dispatched" - a
171
+ > crash in that exact window leaves reconcile() unable to tell it apart from "verification never
172
+ > ran", so a resume may safely but redundantly re-run verification commands whose receipts are
173
+ > already sitting on disk. Not an AC-10 violation (nothing is ever wrongly marked complete) - a
174
+ > design/efficiency observation for a future decision, not fixed here.
175
+
176
+ **Operator implication:** a resume after a crash in that narrow window is always *safe* (nothing
177
+ is ever falsely marked complete), but may be redundant — it can re-run verification commands,
178
+ including potentially expensive ones, that already passed and whose receipts already exist on
179
+ disk.
180
+
181
+ ### f. N>1 parallel-execution primitives exist and are tested, but are not wired into `execute()`
182
+
183
+ `createTaskWorktree`, `createSlotPool` (US-08-TASK-BE-01/02), `integrateAttempt`
184
+ (`lib/task-executor/git.js`, US-08-TASK-BE-03) and `registerIntegratedSHA`
185
+ (`lib/task-executor/store.js`, US-08-TASK-BE-04) are all real, already-implemented functions, and
186
+ US-08-TASK-TEST-01's `parallel.test.js` exercises them together end-to-end for N=2/3 scenarios.
187
+ But as that same commit's message states plainly:
188
+
189
+ > there is no real "execute() with maxConcurrency=N" path yet (execute() still rejects anything
190
+ > but N=1), so this proves the standalone primitives compose correctly rather than exercising a
191
+ > not-yet-existing orchestrator.
192
+
193
+ **Operator implication:** these primitives are proven to compose correctly in isolation, but
194
+ `execute()` remains N=1 / sequential only. Do not expect `--max-concurrency` values other than
195
+ `1` to do anything but fail fast with `UNSUPPORTED_CONCURRENCY` today.