@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
- const spawnResult = await claudeProcess.spawnClaudeAgent({
632
- claudePath: args.claudePath,
633
- args: spawnArgs,
634
- prompt: prompt,
635
- cwd: args.projectDir,
636
- taskTimeoutMs: args.taskTimeoutMs,
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
- const spawnResult = await claudeProcess.spawnClaudeAgent({
917
- claudePath: args.claudePath,
918
- args: spawnArgs,
919
- prompt: prompt,
920
- cwd: args.projectDir,
921
- taskTimeoutMs: args.taskTimeoutMs,
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
- setTaskStatus(taskId, status) {
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dtlabs/ai-toolkit",
3
- "version": "0.22.1",
3
+ "version": "0.23.0",
4
4
  "description": "AI Toolkit — reusable agents and procedures for developers to build AI-powered applications.",
5
5
  "files": [
6
6
  "docs/",
@@ -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: skeletonTask.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', 'estimate', 'outputCount', 'commit'],
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
- estimate: { type: 'object', required: ['agentMinutes', 'tokens'],
50
- properties: { agentMinutes: { type: 'integer' }, tokens: { type: 'integer' } } },
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' } } },