@dtlabs/ai-toolkit 0.11.0 → 0.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +37 -0
- package/bin/cli.js +279 -63
- package/lib/execution-ledger.js +515 -0
- package/package.json +1 -1
- package/src/claude/agents/define-feature.md +36 -60
- package/src/claude/scripts/wb-render.js +101 -2
- package/src/claude/scripts/wb-validate.js +32 -2
- package/src/claude/skills/implement-feature/SKILL.md +4 -2
- package/src/claude/workflows/pm-phase1.js +25 -75
- package/src/claude/workflows/pm-phase2.js +55 -106
- package/src/claude/workflows/pm-phase3.js +162 -75
package/CHANGELOG.md
CHANGED
|
@@ -1,4 +1,41 @@
|
|
|
1
1
|
## 0.1.4 (2026-07-21)
|
|
2
|
+
## [0.12.0](https://github.com/dtlab-tech/ai-toolkit/compare/v0.11.0...v0.12.0) (2026-09-04)
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
### Features
|
|
6
|
+
|
|
7
|
+
* add atomic temp+fsync+rename ledger I/O primitives ([846e911](https://github.com/dtlab-tech/ai-toolkit/commit/846e9110c7c3df52168649346246eedf9fd9a12f))
|
|
8
|
+
* add close() with resume-safe token preservation ([fbd6d3b](https://github.com/dtlab-tech/ai-toolkit/commit/fbd6d3b1114949aa509eb9f244943af1f30e5d0c))
|
|
9
|
+
* add deterministic features-root resolution and grammar ([a21b99c](https://github.com/dtlab-tech/ai-toolkit/commit/a21b99c0245a6b7a7010c2a93fc296ac10831fcf))
|
|
10
|
+
* add fail-closed corrupt-ledger backup handling ([87c2b06](https://github.com/dtlab-tech/ai-toolkit/commit/87c2b0679e083cc064eb00f9b70f920f9abd4f97))
|
|
11
|
+
* add fail() with clear never-opened rejection ([c24226d](https://github.com/dtlab-tech/ai-toolkit/commit/c24226d4f8700ce12233e0dea95ce4cc0542ca40))
|
|
12
|
+
* add ledger CLI argument validator ([825d4a7](https://github.com/dtlab-tech/ai-toolkit/commit/825d4a71a05ef14e0ed0e0206bd3efbb624a086d))
|
|
13
|
+
* add ledger CLI dispatcher wired to the module ([5658ec5](https://github.com/dtlab-tech/ai-toolkit/commit/5658ec583921ddca663221167f4c0d3c21f4c841))
|
|
14
|
+
* add ledger close subcommand with --tokens validation ([ff17cf1](https://github.com/dtlab-tech/ai-toolkit/commit/ff17cf186bae8c4d89225a2d000ce9a2c575ad4c))
|
|
15
|
+
* add ledger fail subcommand with fail-closed exit contract ([c71c9ad](https://github.com/dtlab-tech/ai-toolkit/commit/c71c9ad9d9078b35f74955c9b2ae03eb31ac9bdd))
|
|
16
|
+
* add ledger open subcommand with fail-closed exit contract ([6aa5c1f](https://github.com/dtlab-tech/ai-toolkit/commit/6aa5c1f301d9fb031ab05b8c0c2d62e0f8ecec3a))
|
|
17
|
+
* add ledger skip subcommand with fail-closed exit contract ([5e22856](https://github.com/dtlab-tech/ai-toolkit/commit/5e22856793add03e903653dec47f4a49f8adf6de))
|
|
18
|
+
* add O_EXCL lock with owner token and ABA-safe release ([9e4b774](https://github.com/dtlab-tech/ai-toolkit/commit/9e4b774e1c890c1120cf4933e697a5c9c4e497eb))
|
|
19
|
+
* add open() operation with JS-generated ISO timestamps ([11f7205](https://github.com/dtlab-tech/ai-toolkit/commit/11f72050c9d033944d568b859576387e8da04253))
|
|
20
|
+
* add POSIX shell quoting helper ([bf79da2](https://github.com/dtlab-tech/ai-toolkit/commit/bf79da28100b1a23b1c3e5e7c756074db6dbbf24))
|
|
21
|
+
* add resolve-features-root invocable command ([ab64250](https://github.com/dtlab-tech/ai-toolkit/commit/ab642505f8b682caa47b361b899d87ec8b0fa9d6))
|
|
22
|
+
* add skip() with atomic create-or-update semantics ([455deaf](https://github.com/dtlab-tech/ai-toolkit/commit/455deaf19f864b33a77f37e0539f2c09619296fa))
|
|
23
|
+
* add stale and orphan lock classification ([78fcf85](https://github.com/dtlab-tech/ai-toolkit/commit/78fcf85559af5e632bc760c2f7ef03fd419671ff))
|
|
24
|
+
* implement 128-bit collision-resistant operation_id ([926a38a](https://github.com/dtlab-tech/ai-toolkit/commit/926a38aa41e8bc9a216d5b6f365b8174f78ded75))
|
|
25
|
+
* scaffold lib/execution-ledger.js public API surface ([ba65c53](https://github.com/dtlab-tech/ai-toolkit/commit/ba65c53fc5e29bd028168de8f8379efd5cefbc01))
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
### Bug Fixes
|
|
29
|
+
|
|
30
|
+
* **FTR-015:** stamp real package version in complete-install test fixtures (CI fix) ([c032ded](https://github.com/dtlab-tech/ai-toolkit/commit/c032ded8eb392f60e60ad36d5192b2a2dfe65fff))
|
|
31
|
+
* **pm-phase2:** derive featureDir with a separator-agnostic parser ([4ae54ed](https://github.com/dtlab-tech/ai-toolkit/commit/4ae54ed893d253882efa4247ae9d7adccbd60988))
|
|
32
|
+
* reclaim stale/orphan locks in _acquireLock (AC-08/AC-26) ([14d0deb](https://github.com/dtlab-tech/ai-toolkit/commit/14d0deb6b25759b912b44980c054ebe99c244f05))
|
|
33
|
+
* treat null/0/not_available tokens as unavailable in actuals reader ([7bf2c08](https://github.com/dtlab-tech/ai-toolkit/commit/7bf2c08af2158fe0b9a73bdf7ca6c33cdd673a48))
|
|
34
|
+
* treat null/0/not_available tokens as unavailable in pm-phase3 ([505edda](https://github.com/dtlab-tech/ai-toolkit/commit/505eddaf9ed0d63908ddc7f79f0444d5b285a28d))
|
|
35
|
+
* **wb-render:** preserve verification commands verbatim in Markdown ([b57e06c](https://github.com/dtlab-tech/ai-toolkit/commit/b57e06cacf99dc4f9c950509c9684eed1ea273fd))
|
|
36
|
+
* **wb-render:** render complete authoritative task details ([b696c12](https://github.com/dtlab-tech/ai-toolkit/commit/b696c12e8c7e5f0fb30e0681b2e6123fe8cb2501))
|
|
37
|
+
* **wb-validate:** treat escaped pipe (\|) as literal inside table cells ([c604810](https://github.com/dtlab-tech/ai-toolkit/commit/c60481021b5c7f4b6664952200d7022fc9e2767d))
|
|
38
|
+
|
|
2
39
|
## [0.11.0](https://github.com/dtlab-tech/ai-toolkit/compare/v0.10.1...v0.11.0) (2026-08-26)
|
|
3
40
|
|
|
4
41
|
|
package/bin/cli.js
CHANGED
|
@@ -3,6 +3,7 @@ const fs = require('fs');
|
|
|
3
3
|
const path = require('path');
|
|
4
4
|
const crypto = require('crypto');
|
|
5
5
|
const readline = require('readline');
|
|
6
|
+
const executionLedger = require('../lib/execution-ledger');
|
|
6
7
|
const packageRoot = path.join(__dirname, '..');
|
|
7
8
|
|
|
8
9
|
// ── colors ────────────────────────────────────────────────────────────────────
|
|
@@ -580,67 +581,6 @@ function mergeAllowlist(destDir) {
|
|
|
580
581
|
return { status: 'merged', preserved };
|
|
581
582
|
}
|
|
582
583
|
|
|
583
|
-
// INFRA-T01 (FTR-013):
|
|
584
|
-
// Append a new entry to {featureDir}/{prefix}-token-ledger.json atomically.
|
|
585
|
-
// If the file does not exist it is created. If JSON is malformed the file is
|
|
586
|
-
// overwritten with a single-element array containing the new entry.
|
|
587
|
-
//
|
|
588
|
-
// Algorithm:
|
|
589
|
-
// 1. Read and parse the existing ledger (or start with [])
|
|
590
|
-
// 2. Push the new entry
|
|
591
|
-
// 3. Write the full array back in one synchronous write (atomic)
|
|
592
|
-
function appendLedgerEntry(featureDir, prefix, entry) {
|
|
593
|
-
const filePath = path.join(featureDir, `${prefix}-token-ledger.json`);
|
|
594
|
-
let ledger = [];
|
|
595
|
-
if (fs.existsSync(filePath)) {
|
|
596
|
-
try {
|
|
597
|
-
const parsed = JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
598
|
-
ledger = Array.isArray(parsed) ? parsed : [];
|
|
599
|
-
} catch (_) {
|
|
600
|
-
console.log(`Warning: could not parse token ledger at ${filePath} — starting fresh`);
|
|
601
|
-
ledger = [];
|
|
602
|
-
}
|
|
603
|
-
}
|
|
604
|
-
ledger.push(entry);
|
|
605
|
-
const dir = path.dirname(filePath);
|
|
606
|
-
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
607
|
-
fs.writeFileSync(filePath, JSON.stringify(ledger, null, 2), 'utf8');
|
|
608
|
-
}
|
|
609
|
-
|
|
610
|
-
// INFRA-T02 (FTR-013):
|
|
611
|
-
// Find and update an existing entry in {featureDir}/{prefix}-token-ledger.json
|
|
612
|
-
// by agent key, atomically. Searches from the end of the array so that the most
|
|
613
|
-
// recent entry for a given key is updated (handles any accidental duplicates).
|
|
614
|
-
// If the file does not exist or the key is not found the call is a silent no-op.
|
|
615
|
-
//
|
|
616
|
-
// Algorithm:
|
|
617
|
-
// 1. Read and parse the existing ledger (silent return on missing/malformed)
|
|
618
|
-
// 2. Find the last entry where entry.agent === agentKey
|
|
619
|
-
// 3. Object.assign the updates onto that entry
|
|
620
|
-
// 4. Write the full array back in one synchronous write (atomic)
|
|
621
|
-
function updateLedgerEntry(featureDir, prefix, agentKey, updates) {
|
|
622
|
-
const filePath = path.join(featureDir, `${prefix}-token-ledger.json`);
|
|
623
|
-
if (!fs.existsSync(filePath)) return;
|
|
624
|
-
let ledger;
|
|
625
|
-
try {
|
|
626
|
-
const parsed = JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
627
|
-
ledger = Array.isArray(parsed) ? parsed : [];
|
|
628
|
-
} catch (_) {
|
|
629
|
-
console.log(`Warning: could not parse token ledger at ${filePath} — skipping update for "${agentKey}"`);
|
|
630
|
-
return;
|
|
631
|
-
}
|
|
632
|
-
let idx = -1;
|
|
633
|
-
for (let i = ledger.length - 1; i >= 0; i--) {
|
|
634
|
-
if (ledger[i] && ledger[i].agent === agentKey) { idx = i; break; }
|
|
635
|
-
}
|
|
636
|
-
if (idx === -1) {
|
|
637
|
-
console.log(`Warning: ledger entry not found for agent key "${agentKey}"`);
|
|
638
|
-
return;
|
|
639
|
-
}
|
|
640
|
-
Object.assign(ledger[idx], updates);
|
|
641
|
-
fs.writeFileSync(filePath, JSON.stringify(ledger, null, 2), 'utf8');
|
|
642
|
-
}
|
|
643
|
-
|
|
644
584
|
// US-05-T01:
|
|
645
585
|
// Idempotently append `.claude/settings.local.json` to {destDir}/.gitignore.
|
|
646
586
|
// Creates .gitignore if it does not exist (AC-06, AC-07).
|
|
@@ -672,6 +612,113 @@ function updateGitignore(destDir) {
|
|
|
672
612
|
}
|
|
673
613
|
}
|
|
674
614
|
|
|
615
|
+
// ── resolveFeaturesRoot ───────────────────────────────────────────────────────
|
|
616
|
+
|
|
617
|
+
// US-05-TASK-BE-01 (FTR-016):
|
|
618
|
+
// Resolve the features root directory for a given working directory.
|
|
619
|
+
//
|
|
620
|
+
// Ordered precedence — ALL candidates are gathered before deciding so that
|
|
621
|
+
// ambiguity (multiply-declared AGENTS.md entries, multiple inconsistent
|
|
622
|
+
// defaults) can be detected rather than silently swallowed:
|
|
623
|
+
// 1. Explicit override via options.featuresRoot (highest precedence)
|
|
624
|
+
// 2. features_root: convention parsed from <cwd>/AGENTS.md
|
|
625
|
+
// 3. A single existing conventional default directory
|
|
626
|
+
//
|
|
627
|
+
// AGENTS.md grammar parser (deterministic):
|
|
628
|
+
// - Ignores HTML-comment lines of the form <!-- ... --> (same input line)
|
|
629
|
+
// - Strips inline # comments from the value and trims trailing whitespace
|
|
630
|
+
// - Counts only active (non-commented) declarations when detecting the
|
|
631
|
+
// multiply-declared error; throws when two or more are found
|
|
632
|
+
//
|
|
633
|
+
// Returns an absolute path.
|
|
634
|
+
// Throws a clear structured Error on ambiguous or multiply-declared roots.
|
|
635
|
+
//
|
|
636
|
+
// Parameters:
|
|
637
|
+
// cwd (string): project root directory
|
|
638
|
+
// options (object): { featuresRoot? } — explicit override, highest precedence
|
|
639
|
+
function resolveFeaturesRoot(cwd, options) {
|
|
640
|
+
const opts = options || {};
|
|
641
|
+
const resolvedCwd = path.resolve(cwd);
|
|
642
|
+
|
|
643
|
+
// Conventional default directories tried when no higher-precedence source is found.
|
|
644
|
+
const CONVENTIONAL_DEFAULTS = [
|
|
645
|
+
'internal_docs/features',
|
|
646
|
+
'docs/features',
|
|
647
|
+
];
|
|
648
|
+
|
|
649
|
+
// ── AGENTS.md grammar parser ──────────────────────────────────────────────
|
|
650
|
+
// Returns an array of trimmed value strings for each ACTIVE features_root:
|
|
651
|
+
// declaration found in <cwdDir>/AGENTS.md. Deterministic: same input →
|
|
652
|
+
// same result; does not rely on filesystem ordering.
|
|
653
|
+
function parseAgentsMdDeclarations(cwdDir) {
|
|
654
|
+
const agentsMdPath = path.join(cwdDir, 'AGENTS.md');
|
|
655
|
+
if (!fs.existsSync(agentsMdPath)) return [];
|
|
656
|
+
let content;
|
|
657
|
+
try { content = fs.readFileSync(agentsMdPath, 'utf8'); } catch (_) { return []; }
|
|
658
|
+
|
|
659
|
+
const declarations = [];
|
|
660
|
+
for (const line of content.split('\n')) {
|
|
661
|
+
const trimmed = line.trim();
|
|
662
|
+
|
|
663
|
+
// Ignore HTML comment lines: <!-- anything -->
|
|
664
|
+
if (/^<!--.*-->$/.test(trimmed)) continue;
|
|
665
|
+
|
|
666
|
+
// Match active features_root: declarations
|
|
667
|
+
const match = trimmed.match(/^features_root:\s*(.*)$/);
|
|
668
|
+
if (!match) continue;
|
|
669
|
+
|
|
670
|
+
// Strip inline # comment, then trim
|
|
671
|
+
let value = match[1];
|
|
672
|
+
const hashIdx = value.indexOf('#');
|
|
673
|
+
if (hashIdx !== -1) value = value.substring(0, hashIdx);
|
|
674
|
+
value = value.trim();
|
|
675
|
+
|
|
676
|
+
if (value) declarations.push(value);
|
|
677
|
+
}
|
|
678
|
+
return declarations;
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
// ── Source 1: Explicit override ───────────────────────────────────────────
|
|
682
|
+
if (opts.featuresRoot !== undefined && opts.featuresRoot !== null && opts.featuresRoot !== '') {
|
|
683
|
+
return path.resolve(resolvedCwd, opts.featuresRoot);
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
// ── Source 2: AGENTS.md features_root: convention ────────────────────────
|
|
687
|
+
const agentsDeclarations = parseAgentsMdDeclarations(resolvedCwd);
|
|
688
|
+
if (agentsDeclarations.length > 1) {
|
|
689
|
+
throw new Error(
|
|
690
|
+
`resolveFeaturesRoot: AGENTS.md declares features_root: ${agentsDeclarations.length} times ` +
|
|
691
|
+
`(values: ${agentsDeclarations.map(v => JSON.stringify(v)).join(', ')}); ` +
|
|
692
|
+
'only one active declaration is allowed'
|
|
693
|
+
);
|
|
694
|
+
}
|
|
695
|
+
if (agentsDeclarations.length === 1) {
|
|
696
|
+
return path.resolve(resolvedCwd, agentsDeclarations[0]);
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
// ── Source 3: Conventional defaults — use only when exactly one exists ────
|
|
700
|
+
const existingDefaults = CONVENTIONAL_DEFAULTS
|
|
701
|
+
.map(d => path.resolve(resolvedCwd, d))
|
|
702
|
+
.filter(d => { try { return fs.statSync(d).isDirectory(); } catch (_) { return false; } });
|
|
703
|
+
|
|
704
|
+
if (existingDefaults.length === 1) {
|
|
705
|
+
return existingDefaults[0];
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
if (existingDefaults.length > 1) {
|
|
709
|
+
throw new Error(
|
|
710
|
+
`resolveFeaturesRoot: multiple conventional default directories exist ` +
|
|
711
|
+
`(${existingDefaults.join(', ')}); declare features_root: in AGENTS.md to disambiguate`
|
|
712
|
+
);
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
throw new Error(
|
|
716
|
+
`resolveFeaturesRoot: no features root found under ${resolvedCwd}. ` +
|
|
717
|
+
'Declare features_root: in AGENTS.md or create one of the conventional default directories ' +
|
|
718
|
+
`(${CONVENTIONAL_DEFAULTS.join(', ')})`
|
|
719
|
+
);
|
|
720
|
+
}
|
|
721
|
+
|
|
675
722
|
// ── payload & detection ───────────────────────────────────────────────────────
|
|
676
723
|
|
|
677
724
|
// Returns file-level {src, dest} mappings (absolute paths) for ALL distributable
|
|
@@ -1862,6 +1909,17 @@ async function main() {
|
|
|
1862
1909
|
for (const r of results) process.stdout.write(r + '\n');
|
|
1863
1910
|
}
|
|
1864
1911
|
process.exit(0);
|
|
1912
|
+
} else if (argv[0] === 'resolve-features-root') {
|
|
1913
|
+
try {
|
|
1914
|
+
const resolved = resolveFeaturesRoot(process.cwd());
|
|
1915
|
+
process.stdout.write(resolved + '\n');
|
|
1916
|
+
process.exit(0);
|
|
1917
|
+
} catch (err) {
|
|
1918
|
+
process.stderr.write(err.message + '\n');
|
|
1919
|
+
process.exit(1);
|
|
1920
|
+
}
|
|
1921
|
+
} else if (argv[0] === 'ledger') {
|
|
1922
|
+
handleLedgerCommand(argv.slice(1));
|
|
1865
1923
|
} else if (fs.existsSync(argv[0]) && fs.statSync(argv[0]).isDirectory()) {
|
|
1866
1924
|
await installLocal(argv[0], force, dryRun);
|
|
1867
1925
|
} else {
|
|
@@ -1870,6 +1928,161 @@ async function main() {
|
|
|
1870
1928
|
}
|
|
1871
1929
|
}
|
|
1872
1930
|
|
|
1931
|
+
// ── shell quoting helper ──────────────────────────────────────────────────────
|
|
1932
|
+
function shellQuotePosix(arg) {
|
|
1933
|
+
const s = String(arg);
|
|
1934
|
+
if (s.indexOf('\0') !== -1) throw new Error('shellQuotePosix: argument contains a NUL byte');
|
|
1935
|
+
if (s.indexOf('\n') !== -1) throw new Error('shellQuotePosix: argument contains a newline');
|
|
1936
|
+
return "'" + s.replace(/'/g, "'\\''") + "'";
|
|
1937
|
+
}
|
|
1938
|
+
|
|
1939
|
+
function parseLedgerArgs(argv) {
|
|
1940
|
+
const PREFIX_RE = /^[A-Za-z]+-\d+$/;
|
|
1941
|
+
const result = { prefix: undefined, agent: undefined, attempt: 1, tokens: undefined, dir: undefined, phase: undefined, model: undefined, error: undefined };
|
|
1942
|
+
|
|
1943
|
+
for (let i = 0; i < argv.length; i++) {
|
|
1944
|
+
const flag = argv[i];
|
|
1945
|
+
const val = argv[i + 1];
|
|
1946
|
+
|
|
1947
|
+
if (flag === '--prefix') {
|
|
1948
|
+
i++;
|
|
1949
|
+
if (!val || !PREFIX_RE.test(val)) {
|
|
1950
|
+
throw new Error(
|
|
1951
|
+
'parseLedgerArgs: --prefix must match /^[A-Za-z]+-\\d+$/ (e.g. FTR-016), got: ' + val
|
|
1952
|
+
);
|
|
1953
|
+
}
|
|
1954
|
+
result.prefix = val;
|
|
1955
|
+
} else if (flag === '--agent') {
|
|
1956
|
+
i++;
|
|
1957
|
+
if (!val) {
|
|
1958
|
+
throw new Error('parseLedgerArgs: --agent must be a non-empty string');
|
|
1959
|
+
}
|
|
1960
|
+
result.agent = val;
|
|
1961
|
+
} else if (flag === '--attempt') {
|
|
1962
|
+
i++;
|
|
1963
|
+
const n = Number(val);
|
|
1964
|
+
if (!Number.isInteger(n)) {
|
|
1965
|
+
throw new Error('parseLedgerArgs: --attempt must be an integer, got: ' + val);
|
|
1966
|
+
}
|
|
1967
|
+
result.attempt = n;
|
|
1968
|
+
} else if (flag === '--tokens') {
|
|
1969
|
+
i++;
|
|
1970
|
+
const n = Number(val);
|
|
1971
|
+
if (!Number.isInteger(n) || n < 1) {
|
|
1972
|
+
throw new Error('parseLedgerArgs: --tokens must be an integer >= 1, got: ' + val);
|
|
1973
|
+
}
|
|
1974
|
+
result.tokens = n;
|
|
1975
|
+
} else if (flag === '--dir') {
|
|
1976
|
+
i++;
|
|
1977
|
+
if (!val) {
|
|
1978
|
+
throw new Error('parseLedgerArgs: --dir must be a non-empty string');
|
|
1979
|
+
}
|
|
1980
|
+
result.dir = val;
|
|
1981
|
+
} else if (flag === '--phase') {
|
|
1982
|
+
i++;
|
|
1983
|
+
if (!val) {
|
|
1984
|
+
throw new Error('parseLedgerArgs: --phase must be a non-empty string');
|
|
1985
|
+
}
|
|
1986
|
+
result.phase = val;
|
|
1987
|
+
} else if (flag === '--model') {
|
|
1988
|
+
i++;
|
|
1989
|
+
if (!val) {
|
|
1990
|
+
throw new Error('parseLedgerArgs: --model must be a non-empty string');
|
|
1991
|
+
}
|
|
1992
|
+
result.model = val;
|
|
1993
|
+
} else if (flag === '--error') {
|
|
1994
|
+
i++;
|
|
1995
|
+
if (!val) {
|
|
1996
|
+
throw new Error('parseLedgerArgs: --error must be a non-empty string');
|
|
1997
|
+
}
|
|
1998
|
+
result.error = val;
|
|
1999
|
+
}
|
|
2000
|
+
}
|
|
2001
|
+
|
|
2002
|
+
if (!result.prefix) {
|
|
2003
|
+
throw new Error('parseLedgerArgs: --prefix is required');
|
|
2004
|
+
}
|
|
2005
|
+
if (!result.agent) {
|
|
2006
|
+
throw new Error('parseLedgerArgs: --agent is required and must be non-empty');
|
|
2007
|
+
}
|
|
2008
|
+
|
|
2009
|
+
return result;
|
|
2010
|
+
}
|
|
2011
|
+
|
|
2012
|
+
// Serialize obj to JSON with all object keys sorted recursively.
|
|
2013
|
+
// Used by the ledger subcommand to produce deterministic stdout output.
|
|
2014
|
+
function sortedJson(obj) {
|
|
2015
|
+
return JSON.stringify(obj, (key, val) => {
|
|
2016
|
+
if (val !== null && typeof val === 'object' && !Array.isArray(val)) {
|
|
2017
|
+
const sorted = {};
|
|
2018
|
+
for (const k of Object.keys(val).sort()) sorted[k] = val[k];
|
|
2019
|
+
return sorted;
|
|
2020
|
+
}
|
|
2021
|
+
return val;
|
|
2022
|
+
}, 2);
|
|
2023
|
+
}
|
|
2024
|
+
|
|
2025
|
+
// ── ledger dispatcher ─────────────────────────────────────────────────────────
|
|
2026
|
+
// Routes `ai-toolkit ledger <subcommand> [flags]` to the per-operation
|
|
2027
|
+
// handlers in lib/execution-ledger.js. All operation logic lives in that
|
|
2028
|
+
// module; this function only parses shared flags and routes.
|
|
2029
|
+
function handleLedgerCommand(argv) {
|
|
2030
|
+
const subcommand = argv[0];
|
|
2031
|
+
const flags = argv.slice(1);
|
|
2032
|
+
let args;
|
|
2033
|
+
try {
|
|
2034
|
+
args = parseLedgerArgs(flags);
|
|
2035
|
+
} catch (err) {
|
|
2036
|
+
process.stderr.write(sortedJson({ message: err.message, status: 'error' }) + '\n');
|
|
2037
|
+
process.exitCode = 1;
|
|
2038
|
+
return;
|
|
2039
|
+
}
|
|
2040
|
+
|
|
2041
|
+
if (subcommand === 'open') {
|
|
2042
|
+
try {
|
|
2043
|
+
const result = executionLedger.open(args.dir, args.prefix, args.agent, args.phase, args.model, args.attempt);
|
|
2044
|
+
process.stdout.write(sortedJson(result) + '\n');
|
|
2045
|
+
process.exitCode = 0;
|
|
2046
|
+
} catch (err) {
|
|
2047
|
+
process.stderr.write(sortedJson({ message: err.message, status: 'error' }) + '\n');
|
|
2048
|
+
process.exitCode = 1;
|
|
2049
|
+
}
|
|
2050
|
+
return;
|
|
2051
|
+
} else if (subcommand === 'close') {
|
|
2052
|
+
try {
|
|
2053
|
+
const result = executionLedger.close(args.dir, args.prefix, args.agent, args.tokens, args.attempt);
|
|
2054
|
+
process.stdout.write(sortedJson(result) + '\n');
|
|
2055
|
+
process.exitCode = 0;
|
|
2056
|
+
} catch (err) {
|
|
2057
|
+
process.stderr.write(sortedJson({ message: err.message, status: 'error' }) + '\n');
|
|
2058
|
+
process.exitCode = 1;
|
|
2059
|
+
}
|
|
2060
|
+
return;
|
|
2061
|
+
} else if (subcommand === 'fail') {
|
|
2062
|
+
try {
|
|
2063
|
+
const result = executionLedger.fail(args.dir, args.prefix, args.agent, args.error, args.attempt);
|
|
2064
|
+
process.stdout.write(sortedJson(result) + '\n');
|
|
2065
|
+
process.exitCode = 0;
|
|
2066
|
+
} catch (err) {
|
|
2067
|
+
process.stderr.write(sortedJson({ message: err.message, status: 'error' }) + '\n');
|
|
2068
|
+
process.exitCode = 1;
|
|
2069
|
+
}
|
|
2070
|
+
return;
|
|
2071
|
+
} else if (subcommand === 'skip') {
|
|
2072
|
+
try {
|
|
2073
|
+
const result = executionLedger.skip(args.dir, args.prefix, args.agent, args.phase, args.model, args.attempt);
|
|
2074
|
+
process.stdout.write(sortedJson(result) + '\n');
|
|
2075
|
+
process.exitCode = 0;
|
|
2076
|
+
} catch (err) {
|
|
2077
|
+
process.stderr.write(sortedJson({ message: err.message, status: 'error' }) + '\n');
|
|
2078
|
+
process.exitCode = 1;
|
|
2079
|
+
}
|
|
2080
|
+
return;
|
|
2081
|
+
} else {
|
|
2082
|
+
throw new Error('handleLedgerCommand: unknown subcommand: ' + subcommand);
|
|
2083
|
+
}
|
|
2084
|
+
}
|
|
2085
|
+
|
|
1873
2086
|
// ── entry point guard ─────────────────────────────────────────────────────────
|
|
1874
2087
|
// Run the CLI only when invoked directly (node bin/cli.js).
|
|
1875
2088
|
// When required as a module (e.g., by Jest), skip main() and export pure
|
|
@@ -1898,8 +2111,6 @@ if (require.main === module) {
|
|
|
1898
2111
|
writeSettings,
|
|
1899
2112
|
mergeAllowlist,
|
|
1900
2113
|
updateGitignore,
|
|
1901
|
-
appendLedgerEntry,
|
|
1902
|
-
updateLedgerEntry,
|
|
1903
2114
|
resolveClaudeRuntimeAsset,
|
|
1904
2115
|
runDoctorResolution,
|
|
1905
2116
|
validatePurityGuard,
|
|
@@ -1911,5 +2122,10 @@ if (require.main === module) {
|
|
|
1911
2122
|
hasToolkitPayloadFiles,
|
|
1912
2123
|
isToolkitInstalled,
|
|
1913
2124
|
runVerifyInstall,
|
|
2125
|
+
shellQuotePosix,
|
|
2126
|
+
parseLedgerArgs,
|
|
2127
|
+
handleLedgerCommand,
|
|
2128
|
+
sortedJson,
|
|
2129
|
+
resolveFeaturesRoot,
|
|
1914
2130
|
};
|
|
1915
2131
|
}
|