@dtlabs/ai-toolkit 0.13.0 → 0.14.1
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 +52 -0
- package/bin/cli.js +371 -2
- package/docs/task-executor-bootstrap.md +195 -0
- package/docs/task-executor-cli.md +319 -0
- package/docs/workflow-control-plane.md +102 -0
- package/lib/execution-ledger.js +186 -4
- package/lib/task-executor/claude-process.js +563 -0
- package/lib/task-executor/git.js +1362 -0
- package/lib/task-executor/index.js +3729 -0
- package/lib/task-executor/ownership.js +590 -0
- package/lib/task-executor/plan.js +725 -0
- package/lib/task-executor/store.js +1164 -0
- package/lib/workflow-artifacts.js +102 -0
- package/lib/workflow-control.js +225 -0
- package/lib/workflow-runner.js +39 -0
- package/package.json +1 -1
- package/src/claude/agents/gaia-intervention-documentation-standard.md +9 -9
- package/src/claude/agents/gaia-validate-feature-docs.md +31 -28
- package/src/claude/skills/assess-codebase/SKILL.md +9 -103
- package/src/claude/skills/implement-feature/SKILL.md +74 -159
- package/src/claude/workflows/am-phase1.js +43 -294
- package/src/claude/workflows/am-phase2.js +10 -164
- package/src/claude/workflows/pm-phase1.js +45 -358
- package/src/claude/workflows/pm-phase2.js +50 -571
- package/src/claude/workflows/pm-phase3.js +5 -824
package/CHANGELOG.md
CHANGED
|
@@ -1,4 +1,56 @@
|
|
|
1
1
|
## 0.1.4 (2026-07-21)
|
|
2
|
+
## [0.14.1](https://github.com/dtlab-tech/ai-toolkit/compare/v0.14.0...v0.14.1) (2026-09-30)
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
### Bug Fixes
|
|
6
|
+
|
|
7
|
+
* **workflows:** make gaia-validate-feature-docs a single-pass, host-owned validator ([2721088](https://github.com/dtlab-tech/ai-toolkit/commit/27210884375c7302cfcf84433d3da3412c857cc1))
|
|
8
|
+
* **workflows:** single-pass, host-owned document validator ([2b606fb](https://github.com/dtlab-tech/ai-toolkit/commit/2b606fb2d0df54e244abe473ea81b613eda30537))
|
|
9
|
+
|
|
10
|
+
## [0.14.0](https://github.com/dtlab-tech/ai-toolkit/compare/v0.13.0...v0.14.0) (2026-09-30)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
### Features
|
|
14
|
+
|
|
15
|
+
* **FTR-018:** add agent provenance verification via agents resolve ([27fce79](https://github.com/dtlab-tech/ai-toolkit/commit/27fce7943bda58c546528e839fd83c7c7b2ee1ce))
|
|
16
|
+
* **FTR-018:** add checkpoint intent persistence ([27332e3](https://github.com/dtlab-tech/ai-toolkit/commit/27332e3a38ece600af2f3c3964637dc5f09a5607))
|
|
17
|
+
* **FTR-018:** add Claude subprocess adapter with result validation ([ddcfcd0](https://github.com/dtlab-tech/ai-toolkit/commit/ddcfcd07de8253465a4faf98805956491457706c))
|
|
18
|
+
* **FTR-018:** add commit creation with task trailers ([b11c5af](https://github.com/dtlab-tech/ai-toolkit/commit/b11c5afd995b30c3c8ff136e1449d9c94397e1e7))
|
|
19
|
+
* **FTR-018:** add controlled staging with path enumeration ([81cdc26](https://github.com/dtlab-tech/ai-toolkit/commit/81cdc26904191f189eb9aefc5f86941857368db7))
|
|
20
|
+
* **FTR-018:** add cooperative and immediate stop modes ([2a410ba](https://github.com/dtlab-tech/ai-toolkit/commit/2a410ba0b28d6fdfa9369b439a10e098a250bdf0))
|
|
21
|
+
* **FTR-018:** add DAG validation and cycle detection ([ae08348](https://github.com/dtlab-tech/ai-toolkit/commit/ae0834872f41e339d68a43088c229d6185528d2b))
|
|
22
|
+
* **FTR-018:** add evidence evaluation per edge-cases table ([767a9e0](https://github.com/dtlab-tech/ai-toolkit/commit/767a9e040c6600ae6e7ce81816454bde9923c7ea))
|
|
23
|
+
* **FTR-018:** add exclusive lease protocol ([68f42d1](https://github.com/dtlab-tech/ai-toolkit/commit/68f42d14b87ac6c2672415307248e5a47fada574))
|
|
24
|
+
* **FTR-018:** add executor CLI commands to bin/cli.js ([6b59e65](https://github.com/dtlab-tech/ai-toolkit/commit/6b59e650aebe73b47593c7ee24ed2b803468e53c))
|
|
25
|
+
* **FTR-018:** add executor state protocol (store.js) with versioned schema and atomic writes ([f6d989b](https://github.com/dtlab-tech/ai-toolkit/commit/f6d989b42796d8e37099770e2ebc7fa4f282cab3))
|
|
26
|
+
* **FTR-018:** add ledger finalizeActivity for executor task completion ([be6187a](https://github.com/dtlab-tech/ai-toolkit/commit/be6187aead37c0ab7caf956be867df45c5db8fae))
|
|
27
|
+
* **FTR-018:** add N-limit concurrency control and slot reservation ([6f6cfae](https://github.com/dtlab-tech/ai-toolkit/commit/6f6cfae15d47b6f7f371168bc1ef91db12091236))
|
|
28
|
+
* **FTR-018:** add ownership guard for atomic lease transitions ([a2da308](https://github.com/dtlab-tech/ai-toolkit/commit/a2da3080c5bed54e375a5ce1467a2cc01b0482b7))
|
|
29
|
+
* **FTR-018:** add plan snapshot with content digest ([512bfbf](https://github.com/dtlab-tech/ai-toolkit/commit/512bfbfa6d097d52f7e913f1be16031dd2f214fa))
|
|
30
|
+
* **FTR-018:** add platform qualification guard (Windows-only) ([96c09a0](https://github.com/dtlab-tech/ai-toolkit/commit/96c09a0d00280f8a5893cdaa7bc2a1a190247f9c))
|
|
31
|
+
* **FTR-018:** add replan command with approval validation ([094ef1a](https://github.com/dtlab-tech/ai-toolkit/commit/094ef1a683f91f33350feb314d02adcc932a7e66))
|
|
32
|
+
* **FTR-018:** add resume and reconcile command logic ([be3d9a0](https://github.com/dtlab-tech/ai-toolkit/commit/be3d9a078b00666db8e9521e7e9c714c58f97554))
|
|
33
|
+
* **FTR-018:** add review agent dispatch ([0a463e6](https://github.com/dtlab-tech/ai-toolkit/commit/0a463e6346af977f4bee66bd8b9c443df40112c5))
|
|
34
|
+
* **FTR-018:** add sequential executor main loop (execute()) ([32895ad](https://github.com/dtlab-tech/ai-toolkit/commit/32895ad387d509620d143ca03e63d692aa47f102))
|
|
35
|
+
* **FTR-018:** add serial integration with dispatch-order sequencing ([9681bb7](https://github.com/dtlab-tech/ai-toolkit/commit/9681bb7c853c4c42644a05e6a37fd424ec5e4c0b))
|
|
36
|
+
* **FTR-018:** add stable topological scheduler for task ordering ([63ba632](https://github.com/dtlab-tech/ai-toolkit/commit/63ba632bcccb2ba9996836cf9280e5facec819c6))
|
|
37
|
+
* **FTR-018:** add stale lock detection with process liveness ([391295c](https://github.com/dtlab-tech/ai-toolkit/commit/391295c862ac76e4def7ad6d74fcaac6f2cceb69))
|
|
38
|
+
* **FTR-018:** add task dispatch orchestration ([429c892](https://github.com/dtlab-tech/ai-toolkit/commit/429c892d9f6d2b1b9c53bb2987a8104775e873f3))
|
|
39
|
+
* **FTR-018:** add task-executor module skeleton (index.js) ([339fd57](https://github.com/dtlab-tech/ai-toolkit/commit/339fd5708aa02e55cddc8759be13de05b34aa367))
|
|
40
|
+
* **FTR-018:** add verification command executor ([f03a342](https://github.com/dtlab-tech/ai-toolkit/commit/f03a3425144c3d807e30884874f55e5c76ad6e4b))
|
|
41
|
+
* **FTR-018:** add Work Breakdown CSV parser and phase mapping ([84830ac](https://github.com/dtlab-tech/ai-toolkit/commit/84830ac35ca02615c628d6bd1a96c7cbc2dcc6ff))
|
|
42
|
+
* **FTR-018:** add Work Breakdown Markdown parser (plan.js) ([b26ef0e](https://github.com/dtlab-tech/ai-toolkit/commit/b26ef0e2d949ab4d11278266e67ae8c57565de0c))
|
|
43
|
+
* **FTR-018:** document executor CLI contract; fix unreachable exit code 8 ([3e7a02b](https://github.com/dtlab-tech/ai-toolkit/commit/3e7a02bb341dc8e98f964299b54427eedd06a1f2))
|
|
44
|
+
* **FTR-018:** integrate executor into implement-feature skill ([24eb4cd](https://github.com/dtlab-tech/ai-toolkit/commit/24eb4cdacb477aeed2008199dbe44b56f29d16ce))
|
|
45
|
+
* **runtime:** add deterministic workflow host and owned ledger lifecycle ([aa3c350](https://github.com/dtlab-tech/ai-toolkit/commit/aa3c3503a676c77ff3c76019464a738f8bbc060e))
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
### Bug Fixes
|
|
49
|
+
|
|
50
|
+
* **FTR-018:** fix CI failures on Linux (ubuntu-latest / GitHub Actions) ([7054af9](https://github.com/dtlab-tech/ai-toolkit/commit/7054af9eb4f9109063c0254fe29df75fcabe5a4e))
|
|
51
|
+
* **workflows:** deterministic workflow control plane and owned ledger lifecycle ([aec810f](https://github.com/dtlab-tech/ai-toolkit/commit/aec810f2b93b2ccbb38034e66c0d25897f621298))
|
|
52
|
+
* **workflows:** replace LLM control-plane wrappers with deterministic operations ([603ec2f](https://github.com/dtlab-tech/ai-toolkit/commit/603ec2fd197e3e381362ef2fc974977b45b56798))
|
|
53
|
+
|
|
2
54
|
## [0.13.0](https://github.com/dtlab-tech/ai-toolkit/compare/v0.12.0...v0.13.0) (2026-09-09)
|
|
3
55
|
|
|
4
56
|
|
package/bin/cli.js
CHANGED
|
@@ -1876,7 +1876,9 @@ async function main() {
|
|
|
1876
1876
|
return;
|
|
1877
1877
|
}
|
|
1878
1878
|
|
|
1879
|
-
if (argv[0] === '
|
|
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
|
|
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.
|