@dtlabs/ai-toolkit 0.22.1 → 0.23.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,12 @@
|
|
|
1
1
|
## 0.1.4 (2026-07-21)
|
|
2
|
+
## [0.23.0](https://github.com/dtlab-tech/ai-toolkit/compare/v0.22.1...v0.23.0) (2026-10-22)
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
### Features
|
|
6
|
+
|
|
7
|
+
* **pm-phase2,task-executor:** shrink WBS skeleton payload, add dispatch evidence, short-circuit already-satisfied tasks (FTR-032) ([1ff9609](https://github.com/dtlab-tech/ai-toolkit/commit/1ff96090811b3ddeea2346fc3425bdae248c6d19))
|
|
8
|
+
* **pm-phase2,task-executor:** WBS skeleton weight + dispatch evidence + already-satisfied short-circuit (FTR-032) ([fbfdb00](https://github.com/dtlab-tech/ai-toolkit/commit/fbfdb00f5a2bbd8afb944f5bcd2616ae1b073a1a))
|
|
9
|
+
|
|
2
10
|
## [0.22.1](https://github.com/dtlab-tech/ai-toolkit/compare/v0.22.0...v0.22.1) (2026-10-21)
|
|
3
11
|
|
|
4
12
|
|
|
@@ -37,6 +37,7 @@ const claudeProcess = require('./claude-process');
|
|
|
37
37
|
const git = require('./git');
|
|
38
38
|
const ledger = require('../execution-ledger');
|
|
39
39
|
const agentRegistry = require('../agent-registry');
|
|
40
|
+
const { createEvidence } = require('../workflow-worker-evidence');
|
|
40
41
|
|
|
41
42
|
const IS_WINDOWS = process.platform === 'win32';
|
|
42
43
|
|
|
@@ -628,13 +629,32 @@ async function dispatchTaskAttempt(args) {
|
|
|
628
629
|
// not yet authorized, so this cannot be empirically validated here. Flagged
|
|
629
630
|
// for verification once that real-runtime test is authorized.
|
|
630
631
|
const spawnArgs = baseSpawnArgs.concat([processIdentity]);
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
632
|
+
|
|
633
|
+
// FTR-032 (UC-02): persists a raw transcript/response for this specific dispatch attempt,
|
|
634
|
+
// independent of whether it succeeds, fails, or times out — the same createEvidence/save
|
|
635
|
+
// primitive pm-phase1/pm-phase2 already use for their own worker calls, scoped under this run's
|
|
636
|
+
// own executionRoot (not os.tmpdir()) so it survives alongside the run's other durable state.
|
|
637
|
+
const evidence = createEvidence(
|
|
638
|
+
{ diagnosticsDir: path.join(executionRoot, 'evidence') },
|
|
639
|
+
runId,
|
|
640
|
+
identity,
|
|
641
|
+
{ dispatchKind: 'implementation', taskId: task.id, attemptNumber: attemptNumber,
|
|
642
|
+
executable: args.claudePath, cwd: args.projectDir, args: spawnArgs }
|
|
643
|
+
);
|
|
644
|
+
let spawnResult;
|
|
645
|
+
try {
|
|
646
|
+
spawnResult = await claudeProcess.spawnClaudeAgent({
|
|
647
|
+
claudePath: args.claudePath,
|
|
648
|
+
args: spawnArgs,
|
|
649
|
+
prompt: prompt,
|
|
650
|
+
cwd: args.projectDir,
|
|
651
|
+
taskTimeoutMs: args.taskTimeoutMs,
|
|
652
|
+
});
|
|
653
|
+
evidence.save({ outcome: 'done', response: spawnResult, completedAt: new Date().toISOString() });
|
|
654
|
+
} catch (error) {
|
|
655
|
+
evidence.save({ outcome: 'failed', error: String(error), completedAt: new Date().toISOString() });
|
|
656
|
+
throw error;
|
|
657
|
+
}
|
|
638
658
|
|
|
639
659
|
// 5. Raw capture only — no verification/review/checkpoint here.
|
|
640
660
|
return {
|
|
@@ -646,6 +666,7 @@ async function dispatchTaskAttempt(args) {
|
|
|
646
666
|
ledgerOperationId: ledgerOpenResult.operation_id,
|
|
647
667
|
processIdentity: processIdentity,
|
|
648
668
|
spawnResult: spawnResult,
|
|
669
|
+
evidencePath: evidence.file,
|
|
649
670
|
};
|
|
650
671
|
}
|
|
651
672
|
|
|
@@ -913,13 +934,29 @@ async function runReview(args) {
|
|
|
913
934
|
const spawnArgs = Array.isArray(args.spawnArgs)
|
|
914
935
|
? args.spawnArgs
|
|
915
936
|
: ['--print', '--output-format', 'json', '--agent', identity.nativeName, '--no-session-persistence'].concat(EXECUTOR_PERMISSION_BYPASS_ARGS);
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
937
|
+
// FTR-032 (UC-02): same evidence capture as dispatchTaskAttempt's own implementation dispatch,
|
|
938
|
+
// scoped under this run's own executionRoot — see that function's own comment for rationale.
|
|
939
|
+
const evidence = createEvidence(
|
|
940
|
+
{ diagnosticsDir: path.join(executionRoot, 'evidence') },
|
|
941
|
+
runId,
|
|
942
|
+
identity,
|
|
943
|
+
{ dispatchKind: 'review', taskId: task.id, attemptNumber: attemptNumber,
|
|
944
|
+
executable: args.claudePath, cwd: args.projectDir, args: spawnArgs }
|
|
945
|
+
);
|
|
946
|
+
let spawnResult;
|
|
947
|
+
try {
|
|
948
|
+
spawnResult = await claudeProcess.spawnClaudeAgent({
|
|
949
|
+
claudePath: args.claudePath,
|
|
950
|
+
args: spawnArgs,
|
|
951
|
+
prompt: prompt,
|
|
952
|
+
cwd: args.projectDir,
|
|
953
|
+
taskTimeoutMs: args.taskTimeoutMs,
|
|
954
|
+
});
|
|
955
|
+
evidence.save({ outcome: 'done', response: spawnResult, completedAt: new Date().toISOString() });
|
|
956
|
+
} catch (error) {
|
|
957
|
+
evidence.save({ outcome: 'failed', error: String(error), completedAt: new Date().toISOString() });
|
|
958
|
+
throw error;
|
|
959
|
+
}
|
|
923
960
|
|
|
924
961
|
// 5. Raw capture + best-effort verdict extraction — no outcome persistence
|
|
925
962
|
// here (US-04-TASK-BE-03 owns closing this out).
|
|
@@ -933,6 +970,7 @@ async function runReview(args) {
|
|
|
933
970
|
nativeAgentName: identity.nativeName,
|
|
934
971
|
ledgerOperationId: ledgerOpenResult.operation_id,
|
|
935
972
|
spawnResult: spawnResult,
|
|
973
|
+
evidencePath: evidence.file,
|
|
936
974
|
reviewPassed: verdict.passed,
|
|
937
975
|
criticalFindings: verdict.criticalFindings,
|
|
938
976
|
responseText: verdict.responseText,
|
|
@@ -2023,6 +2061,38 @@ async function _runTaskToResolution(ctx) {
|
|
|
2023
2061
|
|
|
2024
2062
|
const attemptNumber = state.tasks[taskId].attempts.length + 1;
|
|
2025
2063
|
|
|
2064
|
+
// FTR-032 (UC-03): on a task's FIRST attempt only, and only when it declares one or more
|
|
2065
|
+
// verification commands, check once whether it is already satisfied BEFORE paying for a full
|
|
2066
|
+
// implementation + review dispatch cycle — reported at ~20 real minutes of cost to conclude
|
|
2067
|
+
// "nothing to do" on an already-satisfied 4-line task. Never applies to a retry (a prior
|
|
2068
|
+
// failed attempt already proves verification did not already pass at task start) and never
|
|
2069
|
+
// applies when there is nothing to confirm "already satisfied" against.
|
|
2070
|
+
//
|
|
2071
|
+
// Deliberately calls _runVerificationCommand directly, NOT runVerifications — the latter
|
|
2072
|
+
// persists an immutable receipt per command keyed by taskId+attemptNumber+index; calling it
|
|
2073
|
+
// here under attemptNumber=1 would collide with the REAL post-dispatch verification receipt
|
|
2074
|
+
// for the same attempt whenever this check does NOT short-circuit (different timestamps,
|
|
2075
|
+
// same receipt ID, a genuine RECEIPT_CONFLICT) — confirmed by a real test failure. This
|
|
2076
|
+
// pre-check has nothing to attribute a receipt to yet (no attempt has been dispatched), so it
|
|
2077
|
+
// intentionally persists nothing.
|
|
2078
|
+
if (attemptNumber === 1 && task.verificationCommands.length > 0) {
|
|
2079
|
+
let alreadySatisfied = true;
|
|
2080
|
+
for (const command of task.verificationCommands) {
|
|
2081
|
+
const outcome = await _runVerificationCommand(command, projectDir, ctx.taskTimeoutMs);
|
|
2082
|
+
if (!(outcome.spawnError === null && outcome.exitCode === 0)) {
|
|
2083
|
+
alreadySatisfied = false;
|
|
2084
|
+
break;
|
|
2085
|
+
}
|
|
2086
|
+
}
|
|
2087
|
+
if (alreadySatisfied) {
|
|
2088
|
+
state = store.readState(executionRoot, runId);
|
|
2089
|
+
state = state.setTaskStatus(taskId, 'skipped', { reason: 'already-satisfied-pre-dispatch' });
|
|
2090
|
+
store.writeState(executionRoot, runId, state);
|
|
2091
|
+
return;
|
|
2092
|
+
}
|
|
2093
|
+
// any failure: fall through to a normal dispatch below, unchanged
|
|
2094
|
+
}
|
|
2095
|
+
|
|
2026
2096
|
let dispatchResult;
|
|
2027
2097
|
try {
|
|
2028
2098
|
dispatchResult = await dispatchTaskAttempt({
|
|
@@ -257,13 +257,17 @@ class State {
|
|
|
257
257
|
return task;
|
|
258
258
|
}
|
|
259
259
|
|
|
260
|
-
|
|
260
|
+
// extra (FTR-032): optional fields merged into the task entry alongside status — e.g.
|
|
261
|
+
// { reason: 'already-satisfied-pre-dispatch' } to distinguish a pre-dispatch short-circuit
|
|
262
|
+
// skip from the existing post-hoc no-op-checkpoint skip. Every existing call site passes
|
|
263
|
+
// exactly 2 arguments; extra defaults to undefined, and `extra || {}` makes this a no-op there.
|
|
264
|
+
setTaskStatus(taskId, status, extra) {
|
|
261
265
|
const task = this._requireTask(taskId, 'setTaskStatus');
|
|
262
266
|
if (TASK_STATUSES.indexOf(status) === -1) {
|
|
263
267
|
throw _err('STATE_VALIDATION_ERROR', 'setTaskStatus: invalid status "' + status + '"; allowed: ' + TASK_STATUSES.join(', '));
|
|
264
268
|
}
|
|
265
269
|
const tasks = Object.assign({}, this.tasks);
|
|
266
|
-
tasks[taskId] = Object.assign({}, task, { status: status });
|
|
270
|
+
tasks[taskId] = Object.assign({}, task, { status: status }, extra || {});
|
|
267
271
|
return this._clone({ tasks: tasks });
|
|
268
272
|
}
|
|
269
273
|
|
package/package.json
CHANGED
|
@@ -33,6 +33,19 @@ function _stringOrNull(value) {
|
|
|
33
33
|
return value === DASH ? null : value
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
+
// Mirrors lib/task-executor/plan.js's own identically-named function — a non-numeric value is a
|
|
37
|
+
// parse error, not silently coerced (the task's own "Estimate — agent minutes"/"Estimate —
|
|
38
|
+
// tokens" fields are never `—`, per gaia-task-template.md's own placeholder contract, so `null`
|
|
39
|
+
// here only ever means the label itself was missing from the field block).
|
|
40
|
+
function _numberOrNull(value) {
|
|
41
|
+
if (value == null) return null
|
|
42
|
+
const n = Number(value)
|
|
43
|
+
if (Number.isNaN(n)) {
|
|
44
|
+
throw new Error(`wbs-assembler: expected a numeric value, got ${JSON.stringify(value)}`)
|
|
45
|
+
}
|
|
46
|
+
return n
|
|
47
|
+
}
|
|
48
|
+
|
|
36
49
|
// ── Template engine ───────────────────────────────────────────────────────────
|
|
37
50
|
// Loads a canonical templates/gaia-{name}-template.md file and fills in its placeholders. The
|
|
38
51
|
// template file is the single source of truth for field labels, their order, and section
|
|
@@ -147,6 +160,13 @@ function _parseVerificationCommands(text) {
|
|
|
147
160
|
function parseTaskDetailBlock(text) {
|
|
148
161
|
const fields = _parseFieldBlock(text)
|
|
149
162
|
return {
|
|
163
|
+
// FTR-032: the per-task detail call's own template already asks for a real estimate in every
|
|
164
|
+
// response — the WBS skeleton call no longer does (see loadWorkBreakdown's per-task mapping
|
|
165
|
+
// below), so THIS is now the authoritative source, not a value silently discarded as before.
|
|
166
|
+
estimate: {
|
|
167
|
+
agentMinutes: _numberOrNull(fields['Estimate — agent minutes']),
|
|
168
|
+
tokens: _numberOrNull(fields['Estimate — tokens']),
|
|
169
|
+
},
|
|
150
170
|
groupingRationale: _stringOrNull(fields['Grouping rationale']),
|
|
151
171
|
verificationCommands: _parseVerificationCommands(text),
|
|
152
172
|
}
|
|
@@ -298,7 +318,7 @@ function loadWorkBreakdown(inputPath) {
|
|
|
298
318
|
agentType: skeletonTask.agentType,
|
|
299
319
|
dependsOn: skeletonTask.dependsOn,
|
|
300
320
|
acceptanceCriteria: skeletonTask.acceptanceCriteria,
|
|
301
|
-
estimate:
|
|
321
|
+
estimate: detail.estimate,
|
|
302
322
|
outputCount: skeletonTask.outputCount,
|
|
303
323
|
groupingRationale: detail.groupingRationale,
|
|
304
324
|
commit: skeletonTask.commit,
|
|
@@ -35,7 +35,7 @@ const SKELETON_SCHEMA = {
|
|
|
35
35
|
tasks: { type: 'object', additionalProperties: {
|
|
36
36
|
type: 'object',
|
|
37
37
|
required: ['title', 'outcome', 'domain', 'agentType', 'dependsOn',
|
|
38
|
-
'acceptanceCriteria', '
|
|
38
|
+
'acceptanceCriteria', 'outputCount', 'commit'],
|
|
39
39
|
properties: {
|
|
40
40
|
title: { type: 'string' }, outcome: { type: 'string' },
|
|
41
41
|
domain: { enum: ['BE', 'FE', 'DB', 'DevOps', 'INFRA', 'TEST'] },
|
|
@@ -46,8 +46,11 @@ const SKELETON_SCHEMA = {
|
|
|
46
46
|
['agentType']: { enum: ['developer-backend', 'developer-frontend', 'developer-testing', 'review-solution'] },
|
|
47
47
|
dependsOn: { type: 'array', items: { type: 'string' } },
|
|
48
48
|
acceptanceCriteria: { type: 'array', items: { type: 'string' } },
|
|
49
|
-
|
|
50
|
-
|
|
49
|
+
// FTR-032: estimate (agentMinutes/tokens) deliberately removed from this schema — the
|
|
50
|
+
// per-task detail call already produces a real one in every response (its own template
|
|
51
|
+
// asks for it); wbs-assembler.js's parseTaskDetailBlock/loadWorkBreakdown now reads that
|
|
52
|
+
// value back instead, so asking for it again here would be pure duplicated cost on top
|
|
53
|
+
// of a call that already has to reason about every task in the feature in one turn.
|
|
51
54
|
outputCount: { type: 'integer' },
|
|
52
55
|
commit: { type: 'object', required: ['type', 'subject'],
|
|
53
56
|
properties: { type: { type: 'string' }, subject: { type: 'string' } } },
|