@dtlabs/ai-toolkit 0.9.0 → 0.10.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.
@@ -13,13 +13,15 @@ export const meta = {
13
13
 
14
14
  async function appendLedgerEntry(featureDir, prefix, entry) {
15
15
  const ledgerPath = `${featureDir}/${prefix}-token-ledger.json`
16
- // started_at is generated via Bash inside the agent to get a real UTC timestamp with time
17
- const entryWithoutTs = JSON.stringify({ ...entry, started_at: '__TS__', completed_at: null })
16
+ // Defaults come first so the caller can override started_at/completed_at.
17
+ // Skipped entries pass completed_at: '__TS__' to get a real timestamp; running entries
18
+ // pass completed_at: null to keep it null. Both are replaced/kept by the agent below.
19
+ const entryWithoutTs = JSON.stringify({ started_at: '__TS__', completed_at: null, ...entry })
18
20
  await agent(
19
21
  `Append a JSON object to the ledger array at: ${ledgerPath}\n\n` +
20
22
  `1. Run: date -u +"%Y-%m-%dT%H:%M:%SZ" and capture the output as NOW.\n` +
21
23
  `2. Read the file. If it does not exist or cannot be parsed as a JSON array, start with [].\n` +
22
- `3. Push this object onto the array, replacing "__TS__" in started_at with NOW: ${entryWithoutTs}\n` +
24
+ `3. Push this object onto the array, replacing every "__TS__" string value in the object with NOW: ${entryWithoutTs}\n` +
23
25
  `4. Write the full array back (JSON, 2-space indent). Return no output.`,
24
26
  { label: 'append-ledger', phase: 'Work Breakdown', model: 'haiku' }
25
27
  )
@@ -41,6 +43,48 @@ async function updateLedgerEntry(featureDir, prefix, agentKey, updates) {
41
43
  )
42
44
  }
43
45
 
46
+ // Output schemas for wb-validate and wb-render agent invocations (used by US-05)
47
+ const WB_WRAPPER_SCHEMA = {
48
+ type: 'object',
49
+ properties: {
50
+ exitCode: { type: 'number' },
51
+ stdout: { type: 'string' },
52
+ stderr: { type: 'string' },
53
+ },
54
+ required: ['exitCode', 'stdout', 'stderr'],
55
+ }
56
+
57
+ const WB_RENDER_SCHEMA = {
58
+ type: 'object',
59
+ properties: {
60
+ exitCode: { type: 'number' },
61
+ stdout: { type: 'string' },
62
+ stderr: { type: 'string' },
63
+ markdownPath: { type: 'string' },
64
+ csvPath: { type: 'string' },
65
+ markdownExists: { type: 'boolean' },
66
+ csvExists: { type: 'boolean' },
67
+ },
68
+ required: ['exitCode', 'stdout', 'stderr', 'markdownPath', 'csvPath', 'markdownExists', 'csvExists'],
69
+ }
70
+
71
+ const WB_SEMANTIC_SCHEMA = {
72
+ type: 'object',
73
+ properties: {
74
+ valid: { type: 'boolean' },
75
+ findings: { type: 'array' },
76
+ },
77
+ required: ['valid', 'findings'],
78
+ }
79
+
80
+ function normalizeError(err) {
81
+ if (!err) return '<unknown error>'
82
+ if (typeof err === 'string') return err
83
+ if (err.message) return err.message
84
+ if (err.stderr) return err.stderr
85
+ try { return JSON.stringify(err) } catch (_) { return '<unknown error>' }
86
+ }
87
+
44
88
  // ── Parse args ────────────────────────────────────────────────────────────────
45
89
 
46
90
  // args: "<path-to-feature.md>"
@@ -81,6 +125,225 @@ await updateLedgerEntry(featureDir, prefix, 'generate-work-breakdown:phase2', {
81
125
  tokenLedger.push({ agent: 'generate-work-breakdown', model: 'haiku', phase_delta_tokens: wbTokens })
82
126
  log(`generate-work-breakdown done — phase delta: ${wbTokens} tokens`)
83
127
 
128
+ // ── Step 2: wb-validate.js (deterministic structural validator) ───────────────
129
+ let wbValidatorReport = null
130
+ let validateFailed = false
131
+ let validateExitCode = null
132
+ await appendLedgerEntry(featureDir, prefix, {
133
+ agent: 'wb-validate:phase2',
134
+ phase: 'phase2', model: 'haiku', status: 'running',
135
+ phase_delta_tokens: 0, started_at: '__TS__', completed_at: null,
136
+ })
137
+ const beforeValidate = budget.spent()
138
+ try {
139
+ const validateWrapper = await agent(
140
+ `Run this command and return the result as structured output:\n` +
141
+ `node .claude/scripts/wb-validate.js "${featureDir}/${prefix}-Work-Breakdown.json" "${featureDir}/${prefix}-Requirements.md"\n\n` +
142
+ `Capture exitCode (integer), stdout (string), stderr (string). Return all three.`,
143
+ {
144
+ label: 'wb-validate',
145
+ phase: 'Work Breakdown',
146
+ model: 'haiku',
147
+ schema: WB_WRAPPER_SCHEMA,
148
+ }
149
+ )
150
+ validateExitCode = validateWrapper.exitCode
151
+ if (validateWrapper.stderr?.trim()) {
152
+ log(`wb-validate stderr: ${validateWrapper.stderr.trim()}`)
153
+ }
154
+ if (validateWrapper.exitCode === 2) {
155
+ throw Object.assign(
156
+ new Error(validateWrapper.stderr?.trim() || 'wb-validate runtime error (exit 2)'),
157
+ { _exitCode: validateWrapper.exitCode }
158
+ )
159
+ }
160
+ if (![0, 1].includes(validateWrapper.exitCode)) {
161
+ throw Object.assign(
162
+ new Error(`wb-validate unexpected exit code: ${validateWrapper.exitCode}`),
163
+ { _exitCode: validateWrapper.exitCode }
164
+ )
165
+ }
166
+ if (!validateWrapper.stdout?.trim()) {
167
+ throw new Error('wb-validate returned empty stdout')
168
+ }
169
+ wbValidatorReport = JSON.parse(validateWrapper.stdout)
170
+ if (validateWrapper.exitCode === 0 && !wbValidatorReport.valid) {
171
+ throw new Error('Inconsistent wb-validate result: exit 0 with valid=false')
172
+ }
173
+ if (validateWrapper.exitCode === 1 && wbValidatorReport.valid) {
174
+ throw new Error('Inconsistent wb-validate result: exit 1 with valid=true')
175
+ }
176
+ await updateLedgerEntry(featureDir, prefix, 'wb-validate:phase2', {
177
+ status: 'done', completed_at: '__TS__',
178
+ phase_delta_tokens: budget.spent() - beforeValidate,
179
+ })
180
+ } catch (err) {
181
+ validateFailed = true
182
+ await updateLedgerEntry(featureDir, prefix, 'wb-validate:phase2', {
183
+ status: 'failed', completed_at: '__TS__',
184
+ phase_delta_tokens: budget.spent() - beforeValidate,
185
+ error_summary: normalizeError(err),
186
+ exit_code: err._exitCode ?? validateExitCode ?? null,
187
+ })
188
+ }
189
+ const validateTokens = budget.spent() - beforeValidate
190
+ const wbValidatorPassed = !validateFailed && wbValidatorReport !== null
191
+ && wbValidatorReport.valid === true && wbValidatorReport.errors.length === 0
192
+ log(`wb-validate: ${wbValidatorPassed ? 'passed' : 'failed/errors'} — ${validateTokens} tokens`)
193
+
194
+ // ── Step 3: Semantic validator (only if wb-validate passed) ───────────────────
195
+ let semanticResult = null
196
+ let semanticFailed = false
197
+ let semanticTokens = 0
198
+ if (!wbValidatorPassed) {
199
+ await appendLedgerEntry(featureDir, prefix, {
200
+ agent: 'validate-work-breakdown-semantic:phase2',
201
+ phase: 'phase2', model: 'sonnet', status: 'skipped',
202
+ phase_delta_tokens: 0, started_at: '__TS__', completed_at: '__TS__',
203
+ })
204
+ } else {
205
+ await appendLedgerEntry(featureDir, prefix, {
206
+ agent: 'validate-work-breakdown-semantic:phase2',
207
+ phase: 'phase2', model: 'sonnet', status: 'running',
208
+ phase_delta_tokens: 0, started_at: '__TS__', completed_at: null,
209
+ })
210
+ const beforeSemantic = budget.spent()
211
+ try {
212
+ semanticResult = await agent(
213
+ `${featureDir}/${prefix}-Work-Breakdown.json\n${featureDir}/${prefix}-Requirements.md`,
214
+ {
215
+ label: 'validate-work-breakdown-semantic',
216
+ phase: 'Work Breakdown',
217
+ agentType: 'validate-work-breakdown-semantic',
218
+ schema: WB_SEMANTIC_SCHEMA,
219
+ }
220
+ )
221
+ semanticTokens = budget.spent() - beforeSemantic
222
+ await updateLedgerEntry(featureDir, prefix, 'validate-work-breakdown-semantic:phase2', {
223
+ status: 'done', completed_at: '__TS__', phase_delta_tokens: semanticTokens,
224
+ })
225
+ } catch (err) {
226
+ semanticFailed = true
227
+ semanticTokens = budget.spent() - beforeSemantic
228
+ await updateLedgerEntry(featureDir, prefix, 'validate-work-breakdown-semantic:phase2', {
229
+ status: 'failed', completed_at: '__TS__', phase_delta_tokens: semanticTokens,
230
+ error_summary: normalizeError(err), exit_code: null,
231
+ })
232
+ }
233
+ }
234
+ log(`semantic validator: ${semanticFailed ? 'failed' : semanticResult ? 'done' : 'skipped'}`)
235
+
236
+ // ── Step 4: wb-render.js (only if wb-validate passed and semantic did not fail) ─
237
+ const canRender = wbValidatorPassed && !semanticFailed
238
+ let renderFailed = false
239
+ let renderResult = null
240
+ if (!canRender) {
241
+ await appendLedgerEntry(featureDir, prefix, {
242
+ agent: 'wb-render:phase2',
243
+ phase: 'phase2', model: 'haiku', status: 'skipped',
244
+ phase_delta_tokens: 0, started_at: '__TS__', completed_at: '__TS__',
245
+ })
246
+ } else {
247
+ await appendLedgerEntry(featureDir, prefix, {
248
+ agent: 'wb-render:phase2',
249
+ phase: 'phase2', model: 'haiku', status: 'running',
250
+ phase_delta_tokens: 0, started_at: '__TS__', completed_at: null,
251
+ })
252
+ const beforeRender = budget.spent()
253
+ try {
254
+ renderResult = await agent(
255
+ `Run this command and return the result as structured output:\n` +
256
+ `node .claude/scripts/wb-render.js "${featureDir}/${prefix}-Work-Breakdown.json" "${prefix}"\n\n` +
257
+ `Capture exitCode (integer), stdout (string), stderr (string).\n` +
258
+ `Resolve the expected output paths:\n` +
259
+ ` markdownPath = "${featureDir}/${prefix}-Work-Breakdown.md"\n` +
260
+ ` csvPath = "${featureDir}/${prefix}-Work-Breakdown.csv"\n` +
261
+ `Check whether each file exists on disk and return:\n` +
262
+ ` markdownExists (boolean), csvExists (boolean).\n` +
263
+ `Return all seven fields.`,
264
+ {
265
+ label: 'wb-render',
266
+ phase: 'Work Breakdown',
267
+ model: 'haiku',
268
+ schema: WB_RENDER_SCHEMA,
269
+ }
270
+ )
271
+ if (renderResult.stderr?.trim()) {
272
+ log(`wb-render stderr: ${renderResult.stderr.trim()}`)
273
+ }
274
+ if (renderResult.exitCode !== 0) {
275
+ throw Object.assign(
276
+ new Error(`wb-render exited ${renderResult.exitCode}: ${renderResult.stderr?.trim() || renderResult.stdout?.trim()}`),
277
+ { _exitCode: renderResult.exitCode }
278
+ )
279
+ }
280
+ if (!renderResult.markdownPath?.trim()) throw new Error('wb-render did not return markdownPath')
281
+ if (!renderResult.csvPath?.trim()) throw new Error('wb-render did not return csvPath')
282
+ if (!renderResult.markdownExists) {
283
+ throw Object.assign(
284
+ new Error(`wb-render output file not found: ${renderResult.markdownPath}`),
285
+ { _exitCode: renderResult.exitCode }
286
+ )
287
+ }
288
+ if (!renderResult.csvExists) {
289
+ throw Object.assign(
290
+ new Error(`wb-render output file not found: ${renderResult.csvPath}`),
291
+ { _exitCode: renderResult.exitCode }
292
+ )
293
+ }
294
+ await updateLedgerEntry(featureDir, prefix, 'wb-render:phase2', {
295
+ status: 'done', completed_at: '__TS__',
296
+ phase_delta_tokens: budget.spent() - beforeRender,
297
+ })
298
+ } catch (err) {
299
+ renderFailed = true
300
+ await updateLedgerEntry(featureDir, prefix, 'wb-render:phase2', {
301
+ status: 'failed', completed_at: '__TS__',
302
+ phase_delta_tokens: budget.spent() - beforeRender,
303
+ error_summary: normalizeError(err),
304
+ exit_code: err._exitCode ?? renderResult?.exitCode ?? null,
305
+ })
306
+ }
307
+ tokenLedger.push({ agent: 'wb-render', model: 'haiku', phase_delta_tokens: budget.spent() - beforeRender })
308
+ }
309
+
310
+ tokenLedger.push({ agent: 'wb-validate', model: 'haiku', phase_delta_tokens: validateTokens })
311
+ tokenLedger.push({ agent: 'validate-work-breakdown-semantic', model: 'sonnet', phase_delta_tokens: semanticTokens })
312
+
313
+ // ── Step 5: Assemble gate2_payload ────────────────────────────────────────────
314
+ const gate2_payload = {
315
+ js_validator_report: wbValidatorReport,
316
+ js_validator_failed: validateFailed,
317
+ semantic_validator_result: semanticResult,
318
+ semantic_validator_failed: semanticFailed,
319
+ renderer_result: renderResult,
320
+ renderer_failed: renderFailed,
321
+ duration_bands: wbValidatorReport ? wbValidatorReport.durationBands : null,
322
+ domain_distribution: wbValidatorReport ? wbValidatorReport.domainDistribution : null,
323
+ warning_band_tasks: wbValidatorReport
324
+ ? wbValidatorReport.warnings.filter(w => w.category === 'duration_warning')
325
+ .map(w => ({ taskId: w.taskId, agentMinutes: w.details ? w.details.agentMinutes : null }))
326
+ : [],
327
+ split_required_tasks: wbValidatorReport
328
+ ? wbValidatorReport.errors.filter(e => e.category === 'split_required')
329
+ .map(e => ({ taskId: e.taskId, agentMinutes: e.details ? e.details.agentMinutes : null }))
330
+ : [],
331
+ must_ac_uncovered: wbValidatorReport
332
+ ? wbValidatorReport.errors.filter(e => e.category === 'must_ac_uncovered')
333
+ .map(e => ({ acId: e.details ? e.details.acId : null }))
334
+ : [],
335
+ phase_unschedulable: wbValidatorReport
336
+ ? (wbValidatorReport.dependencies ? wbValidatorReport.dependencies.phaseUnschedulable || [] : [])
337
+ : [],
338
+ gate2_blocked:
339
+ validateFailed ||
340
+ !wbValidatorPassed ||
341
+ semanticFailed ||
342
+ renderFailed ||
343
+ Boolean(semanticResult && semanticResult.findings && semanticResult.findings.some(f => f.blocking)),
344
+ }
345
+ log(`gate2_payload assembled — gate2_blocked: ${gate2_payload.gate2_blocked}`)
346
+
84
347
  // ── Parse WB + write Effort-Estimate + Token-Estimate ────────────────────────
85
348
  phase('Effort Estimate')
86
349
 
@@ -135,30 +398,27 @@ function formatEur(val) {
135
398
  const metrics = await agent(
136
399
  `You have four tasks:
137
400
 
138
- TASK 1 — Read the Work Breakdown file.
401
+ TASK 1 — Read the Work Breakdown JSON file.
139
402
  The feature.md is at: ${featurePath}
140
- The directory containing feature.md also contains the Work Breakdown file named {PREFIX}-Work-Breakdown.md,
141
- where PREFIX matches the pattern [A-Z]+-[0-9]+ from the directory name (e.g. FTR-009).
142
- Use Glob or Read to find and read that file.
143
-
144
- Extract from the Summary table:
145
- - prefix (e.g. "FTR-009")
146
- - feature_dir (directory containing feature.md)
147
- - feature_title (full feature title from the Work Breakdown header)
148
- - user_stories (integer)
149
- - total_tasks (integer)
150
- - domain_breakdown (e.g. "DB:0, BE:11, FE:0, INFRA:4, TEST:0")
151
- - implementation_phases (integer)
152
- - human_estimate (e.g. "~34h")
153
- - agent_estimate (e.g. "~2h 6min")
154
- - work_breakdown_path (full path to the Work Breakdown file)
155
-
156
- Also read Section 4 of the Work Breakdown to extract per-User-Story details:
157
- - US ID (e.g. US-01)
158
- - US title
159
- - task count
160
- - domains involved
161
- - estimated hours (from the Work Breakdown if present, otherwise estimate based on task count: ~2h per BE task, ~1h per TEST task, ~30min per INFRA task)
403
+ The JSON work breakdown is at: ${featureDir}/${prefix}-Work-Breakdown.json
404
+ Read the JSON file and extract:
405
+ - prefix (e.g. "FTR-009") — use "${prefix}"
406
+ - feature_dir (directory containing feature.md) — use "${featureDir}"
407
+ - feature_title — from wb.title or wb.feature field
408
+ - total_tasks (integer) — count all tasks across all phases
409
+ - user_stories (integer) — count phases whose id starts with "US-"
410
+ - domain_breakdown (string) — e.g. "BE: N, FE: N, DB: N, INFRA: N, TEST: N"
411
+ - implementation_phases (integer) — total number of phases
412
+ - human_estimate — compute as: (BE_tasks * 120 + TEST_tasks * 60 + INFRA_tasks * 30 + FE_tasks * 90 + DB_tasks * 90) minutes, format as "~Xh Ymin"
413
+ - agent_estimate — human_estimate / 4 (parallel dispatch), format as "~Xmin" or "~Xh Ymin"
414
+ - work_breakdown_path — ${featureDir}/${prefix}-Work-Breakdown.json
415
+
416
+ Also extract per-User-Story details for the Effort-Estimate:
417
+ - US ID (phase.id where it starts with "US-")
418
+ - US title (phase.title)
419
+ - task count (phase.tasks.length)
420
+ - domains involved (unique task.domain values in the phase)
421
+ - estimated hours (per the formula above)
162
422
 
163
423
  TASK 2 — Write the Effort-Estimate.md file.
164
424
  Write {feature_dir}/{PREFIX}-Effort-Estimate.md with this format:
@@ -245,7 +505,7 @@ For the "Est. cost €" column in Phase 3: use the formula tokens * (0.8 * 3.00
245
505
  TASK 4 — Set return values.
246
506
  Set effort_estimate_path to the full path of the written Effort-Estimate.md.
247
507
  Set token_estimate_path to the full path of the written Token-Estimate.md.
248
- Set tasks_csv_path to the full path of the {PREFIX}-Work-Breakdown.csv file (written by generate-work-breakdown in the same directory as feature.md).
508
+ Set tasks_csv_path to "${featureDir}/${prefix}-Work-Breakdown.csv" (written by wb-render in the same directory as feature.md).
249
509
 
250
510
  Return the extracted metrics as structured output.`,
251
511
  {
@@ -287,7 +547,7 @@ ${phase2Events}`,
287
547
  )
288
548
 
289
549
  return {
290
- prefix: metrics.prefix,
550
+ prefix: metrics.prefix || prefix,
291
551
  feature_path: featurePath,
292
552
  user_stories: metrics.user_stories,
293
553
  total_tasks: metrics.total_tasks,
@@ -301,4 +561,5 @@ return {
301
561
  tasks_csv_path: metrics.tasks_csv_path || '',
302
562
  token_ledger: tokenLedger,
303
563
  errors: [],
564
+ gate2_payload,
304
565
  }
package/CHANGELOG.md CHANGED
@@ -1,4 +1,79 @@
1
1
  ## 0.1.4 (2026-07-21)
2
+ ## [0.10.1](https://github.com/dtlab-tech/ai-toolkit/compare/v0.10.0...v0.10.1) (2026-08-12)
3
+
4
+
5
+ ### Bug Fixes
6
+
7
+ * **installer:** exclude .claude/scripts/tests from distribution and npm pack ([9ac27c8](https://github.com/dtlab-tech/ai-toolkit/commit/9ac27c8bfe84707f177325afbf71843865a0aa7d))
8
+ * **installer:** exclude .claude/scripts/tests from distribution and npm pack ([3489618](https://github.com/dtlab-tech/ai-toolkit/commit/34896181a873daa17495c0682bd11fd529a11330))
9
+
10
+ ## [0.10.0](https://github.com/dtlab-tech/ai-toolkit/compare/v0.9.0...v0.10.0) (2026-08-12)
11
+
12
+
13
+ ### Features
14
+
15
+ * **FTR-014:** Atomic Work Breakdown validation pipeline ([59a3be5](https://github.com/dtlab-tech/ai-toolkit/commit/59a3be511238229ca49f280262306b26e5ceb9a0))
16
+ * **FTR-014:** INFRA-T01 Create .claude/scripts/ directory structure ([27773ac](https://github.com/dtlab-tech/ai-toolkit/commit/27773ac03d916ddb64acba03e2f9a21985a59886))
17
+ * **FTR-014:** INFRA-T02 Define WB JSON schema v2 constants ([52c45d0](https://github.com/dtlab-tech/ai-toolkit/commit/52c45d06576e2a7717f98d056953ae6ad821cc75))
18
+ * **FTR-014:** INFRA-T03 Create AC table parser test fixture ([4e6a7a3](https://github.com/dtlab-tech/ai-toolkit/commit/4e6a7a3490f4de53354ac7ae384a00f0e4f98e34))
19
+ * **FTR-014:** INFRA-T04 Define WB_WRAPPER_SCHEMA and WB_RENDER_SCHEMA in pm-phase2.js ([b92e81c](https://github.com/dtlab-tech/ai-toolkit/commit/b92e81c348f3ec4fc2b3b8fe14fd6ecdbd60c3fa))
20
+ * **FTR-014:** INFRA-T05 Create valid and invalid WB JSON test fixtures ([ea5233c](https://github.com/dtlab-tech/ai-toolkit/commit/ea5233c4b1fccd782682533596ddd4febadd3c29))
21
+ * **FTR-014:** INFRA-T06 Extract buildWaves algorithm as shared test utility ([0b2fd25](https://github.com/dtlab-tech/ai-toolkit/commit/0b2fd25bf0d11679b0fef9140bf688b811316f8c))
22
+ * **FTR-014:** INFRA-T07 Define duration policy constants in wb-validate.js ([cd6cfee](https://github.com/dtlab-tech/ai-toolkit/commit/cd6cfee7bffb4dfd1d9ebbf1cc4732cc9def6acf))
23
+ * **FTR-014:** INFRA-T08 Define wb-validate error catalog constants ([06a9cd8](https://github.com/dtlab-tech/ai-toolkit/commit/06a9cd8d4f4e3583b84ac62ae94138184a8d96b8))
24
+ * **FTR-014:** US-01-T01 Update generate-work-breakdown.md to produce schema v2 JSON ([44a9674](https://github.com/dtlab-tech/ai-toolkit/commit/44a9674f897bcb70cbddd11cd9f72f220765e512))
25
+ * **FTR-014:** US-01-T02 Generate Work Breakdown JSON for a test feature ([8b1f3ed](https://github.com/dtlab-tech/ai-toolkit/commit/8b1f3ed65cb75350c2f2c181f5a1e8a8a7642a10))
26
+ * **FTR-014:** US-01-T03 Verify schema v2 compliance of generated JSON ([afdc774](https://github.com/dtlab-tech/ai-toolkit/commit/afdc774b0fc56466eaddbb462bb1230ad31b7792))
27
+ * **FTR-014:** US-02-T01 Implement wb-validate.js entry point and schema version check ([8ff1c30](https://github.com/dtlab-tech/ai-toolkit/commit/8ff1c308934830551c0bae6a1122e68af10dbcd9))
28
+ * **FTR-014:** US-02-T02 Implement unique ID and required field validation ([c426adc](https://github.com/dtlab-tech/ai-toolkit/commit/c426adc3796cc0705a40bb8ac34b1b79ca9f85d0))
29
+ * **FTR-014:** US-02-T03 Implement task ID format, domain, agentType validation ([a2166f7](https://github.com/dtlab-tech/ai-toolkit/commit/a2166f76a1b9e4f1cd7de0925e9ff20fe6d2cb36))
30
+ * **FTR-014:** US-02-T04 Implement task dependency resolution — refs, self-dep, phase consistency ([8fe5906](https://github.com/dtlab-tech/ai-toolkit/commit/8fe59060643a2704ce8aa1bd8a363c8a4a1794ef))
31
+ * **FTR-014:** US-02-T05 Implement task cycle detection (DFS gray/black coloring) ([94e18a3](https://github.com/dtlab-tech/ai-toolkit/commit/94e18a342ae025ff7574ee1fd9b038a6de4dea60))
32
+ * **FTR-014:** US-02-T06 Implement phase-level dependency projection and cycle detection ([a6e7469](https://github.com/dtlab-tech/ai-toolkit/commit/a6e7469541957a2dab3a3d525a87f060f64b2ecd))
33
+ * **FTR-014:** US-02-T07 Implement phase schedulability check (buildWaves) ([6a710a1](https://github.com/dtlab-tech/ai-toolkit/commit/6a710a13c4a4e3132f79f90f9f0d286120e64fb0))
34
+ * **FTR-014:** US-02-T08 Implement duration policy checks (four-band classification) ([44a61fb](https://github.com/dtlab-tech/ai-toolkit/commit/44a61fb62370e06eb7ac1f6e20d0b3e2ec628195))
35
+ * **FTR-014:** US-02-T09 Implement verification commands, commit subject, grouping rationale checks ([cbd64f0](https://github.com/dtlab-tech/ai-toolkit/commit/cbd64f00331e03ac9582b656c3e75b0921246630))
36
+ * **FTR-014:** US-02-T10 Implement AC table parser (Section 3.2 contract) ([317abe4](https://github.com/dtlab-tech/ai-toolkit/commit/317abe4f4a5848370a68d6ad3bae7e8a92755c53))
37
+ * **FTR-014:** US-02-T11 Implement AC existence and scope validation (checks 19-20) ([132e97a](https://github.com/dtlab-tech/ai-toolkit/commit/132e97a804175a2d5b2a97cfbc8816031ea81c87))
38
+ * **FTR-014:** US-02-T12 Implement AC priority derivation and Must coverage check (check 21) ([6483986](https://github.com/dtlab-tech/ai-toolkit/commit/6483986959a603ba1dc75c321db9b08d8808a2be))
39
+ * **FTR-014:** US-02-T13 Implement text field character validation and empty phase detection (checks 22-23) ([d93464e](https://github.com/dtlab-tech/ai-toolkit/commit/d93464e13444e0969a5cb9e58ad0a1c0fc3b865c))
40
+ * **FTR-014:** US-02-T14 Implement domain distribution in validator report output ([2d462f0](https://github.com/dtlab-tech/ai-toolkit/commit/2d462f07a00be04275d15608d2916e15b56df44b))
41
+ * **FTR-014:** US-02-T15 Add Jest suite for wb-validate checks 1-7 (schema, IDs, fields, format, domain, agentType) ([0affff6](https://github.com/dtlab-tech/ai-toolkit/commit/0affff6d986ab3da1b321e1baae91cc49fa402e1))
42
+ * **FTR-014:** US-02-T16 Add Jest suite for wb-validate checks 8-10 (task deps, self-dep, phase consistency) ([17427a2](https://github.com/dtlab-tech/ai-toolkit/commit/17427a2ab900238bf757a11040bb1380dc49fa4e))
43
+ * **FTR-014:** US-02-T17 Add Jest suite for wb-validate check 11 (task cycle detection) ([43e06aa](https://github.com/dtlab-tech/ai-toolkit/commit/43e06aa95fc7203be5d5594443d5dd8a891ba71a))
44
+ * **FTR-014:** US-02-T18 Add Jest suite for wb-validate checks 12-14 (phase graph, cycles, schedulability) ([a4f710e](https://github.com/dtlab-tech/ai-toolkit/commit/a4f710e6ab996251d98af5006f519a497d7ca850))
45
+ * **FTR-014:** US-02-T19 Add Jest suite for wb-validate checks 15-18 (duration policy, commands, commit, rationale) ([3bdfc81](https://github.com/dtlab-tech/ai-toolkit/commit/3bdfc810f4dea82e29493e06fb98e83991d21c9c))
46
+ * **FTR-014:** US-02-T20 Add Jest suite for wb-validate checks 19-21 (AC parsing, scope, priority, Must coverage) ([e237c8e](https://github.com/dtlab-tech/ai-toolkit/commit/e237c8e03f83a499e4f265c2f67ed51b629967f2))
47
+ * **FTR-014:** US-02-T21 Add Jest suite for wb-validate checks 22-23 and report output contract ([9cbb57a](https://github.com/dtlab-tech/ai-toolkit/commit/9cbb57a5f8d781fb20e84235d8c492032c102893))
48
+ * **FTR-014:** US-03-T01 Create validate-work-breakdown-semantic agent definition ([6b34656](https://github.com/dtlab-tech/ai-toolkit/commit/6b3465634632f03f9c79ab27f31a8e842971d66d))
49
+ * **FTR-014:** US-03-T02 US-03-T03 Semantic analysis logic and structured output (covered by T01 agent definition) ([762b648](https://github.com/dtlab-tech/ai-toolkit/commit/762b64853d52b42f9535f4531cb4010d66ae4da2))
50
+ * **FTR-014:** US-03-T04 Manual semantic validation on test features (7/7 schema checks PASS) ([db6ee3c](https://github.com/dtlab-tech/ai-toolkit/commit/db6ee3cefc698216876f479b5fdbe64120e7e2bf))
51
+ * **FTR-014:** US-04-T01 Implement wb-render.js entry point (arg parsing, JSON read, file path setup) ([3d82ca3](https://github.com/dtlab-tech/ai-toolkit/commit/3d82ca3d7427df5e6208c58d1d1625224bcd079f))
52
+ * **FTR-014:** US-04-T02 Implement phase-level depends_on aggregation in wb-render.js ([726a756](https://github.com/dtlab-tech/ai-toolkit/commit/726a756cfee589ea9a2879c7f5d2732ccea682c8))
53
+ * **FTR-014:** US-04-T03 Implement defensive character stripping and commit message construction ([d9262dc](https://github.com/dtlab-tech/ai-toolkit/commit/d9262dc25866100afe74fe468a963c5034745c9a))
54
+ * **FTR-014:** US-04-T04 Implement Markdown output generation in wb-render.js ([2a7a384](https://github.com/dtlab-tech/ai-toolkit/commit/2a7a3842f642cb514e6efd48d94d8f818a3a1414))
55
+ * **FTR-014:** US-04-T05 Implement CSV output generation in wb-render.js ([1c2a386](https://github.com/dtlab-tech/ai-toolkit/commit/1c2a386078ae54015048f32b76a3789efa82f03a))
56
+ * **FTR-014:** US-04-T06 Tests for Markdown structure, CSV structure, dependency aggregation ([03de433](https://github.com/dtlab-tech/ai-toolkit/commit/03de433b1643751d68abc5dc121715c75872775d))
57
+ * **FTR-014:** US-04-T07 Tests for character stripping, commit construction, output file paths ([ace21ba](https://github.com/dtlab-tech/ai-toolkit/commit/ace21ba2f1eff5777254a9174a3f4b2ff0383b97))
58
+ * **FTR-014:** US-04-T08 CSV regression test against pm-phase3 parser ([3a83ed5](https://github.com/dtlab-tech/ai-toolkit/commit/3a83ed5a1dd3f9bb8add57e8abc97b8f80303a62))
59
+ * **FTR-014:** US-05-T01..T06 Update pm-phase2.js with validation pipeline and gate2_payload ([711b71e](https://github.com/dtlab-tech/ai-toolkit/commit/711b71e8b3d3f78a7cac43841fc9b42625d4749b))
60
+ * **FTR-014:** US-05-T07 Update implement-feature SKILL.md Gate 2 presentation with validator results ([e617b3d](https://github.com/dtlab-tech/ai-toolkit/commit/e617b3d28036969adecff78c26df382e871e9ed2))
61
+ * **FTR-014:** US-05-T08 Tests for orchestration flow wb-validate to semantic to render ([c1341e2](https://github.com/dtlab-tech/ai-toolkit/commit/c1341e23566d6efaa33c58db368d52fdbbef9fe1))
62
+ * **FTR-014:** US-05-T09 Tests for Gate 2 blocking logic and gate2_payload assembly ([8421695](https://github.com/dtlab-tech/ai-toolkit/commit/8421695ff0aeee3f9fd0d01d21cfc863a9344c91))
63
+ * **FTR-014:** US-06-T01..T04 Ledger tracking for wb-validate, semantic, wb-render in pm-phase2 ([2f9a9f7](https://github.com/dtlab-tech/ai-toolkit/commit/2f9a9f7ff935314bb591a4d1bb8ea8eaf9ad19bc))
64
+ * **FTR-014:** US-07-T01..T02 Add .claude/scripts/ to installer distribution for wb-validate and wb-render ([bcba12a](https://github.com/dtlab-tech/ai-toolkit/commit/bcba12ad340d071b794dd6608318e34dea7e846b))
65
+ * **FTR-014:** US-07-T03..T05 Tests for installer script distribution verification ([2bb57bd](https://github.com/dtlab-tech/ai-toolkit/commit/2bb57bd4a918052ed05a421d310ae2124252a6f8))
66
+
67
+
68
+ ### Bug Fixes
69
+
70
+ * **FTR-014:** align warning/split task details field to agentMinutes ([ba7dbf8](https://github.com/dtlab-tech/ai-toolkit/commit/ba7dbf8f8dd5e34f55bc825bdefabdaaf2160eba))
71
+ * **FTR-014:** enforce groupingRationale as a required task field in check 4 ([e5cb733](https://github.com/dtlab-tech/ai-toolkit/commit/e5cb7336e91f44c653d979cfeba8f21de3da3d19))
72
+ * **FTR-014:** guard wb.phases in checks 22 and 23 against missing/non-array ([32ec505](https://github.com/dtlab-tech/ai-toolkit/commit/32ec50560c5e1fe855ac15bf9d40ec7b0ba3d8ae))
73
+ * **FTR-014:** preserve completed_at for skipped ledger entries (I6) ([389f280](https://github.com/dtlab-tech/ai-toolkit/commit/389f280d4ebdfc6a3d694e9c9e43e57042043493))
74
+ * **FTR-014:** relax symmetric-wrapping assertion in pm-phase2-source test ([8bc663e](https://github.com/dtlab-tech/ai-toolkit/commit/8bc663ec098d645cf3e6dcdd7793a08e44104784))
75
+ * **FTR-014:** use exit(2) for parseAcTable fatal errors (I5) ([1244066](https://github.com/dtlab-tech/ai-toolkit/commit/12440663cf57ddfea523535829ebbb9a66a300fd))
76
+
2
77
  ## [0.9.0](https://github.com/dtlab-tech/ai-toolkit/compare/v0.8.0...v0.9.0) (2026-08-05)
3
78
 
4
79
 
package/bin/cli.js CHANGED
@@ -44,6 +44,20 @@ function banner() {
44
44
  // and advise on these — copying would clobber the user's existing settings.
45
45
  const NEVER_COPY = new Set(['settings.json', 'settings.local.json']);
46
46
 
47
+ // Path segments that identify non-distributable source files. Applied by
48
+ // isDistributable() inside expandMappings() — the single source of truth for
49
+ // whether a file enters the install plan, copy, manifest, and orphan pipeline.
50
+ // Tests, fixtures, and helpers live under .claude/scripts/tests/ and must never
51
+ // be shipped to destination projects (FTR-015 will move them to tests/**).
52
+ const NEVER_DIST_SEGMENTS = ['.claude/scripts/tests/'];
53
+
54
+ function isDistributable(srcPath) {
55
+ const n = srcPath.replace(/\\/g, '/');
56
+ if (NEVER_DIST_SEGMENTS.some(seg => n.includes(seg))) return false;
57
+ if (path.basename(srcPath).endsWith('.test.js') && n.includes('.claude/scripts/')) return false;
58
+ return true;
59
+ }
60
+
47
61
  function fileHash(filePath) {
48
62
  return crypto.createHash('md5').update(fs.readFileSync(filePath)).digest('hex');
49
63
  }
@@ -162,11 +176,13 @@ function expandMappings(mappings) {
162
176
  if (fs.statSync(src).isDirectory()) {
163
177
  for (const entry of walkDir(src)) {
164
178
  if (NEVER_COPY.has(path.basename(entry))) continue; // never clobber user config
179
+ if (!isDistributable(entry)) continue;
165
180
  const rel = path.relative(src, entry);
166
181
  files.push({ src: entry, dest: path.join(dest, rel) });
167
182
  }
168
183
  } else {
169
184
  if (NEVER_COPY.has(path.basename(src))) continue;
185
+ if (!isDistributable(src)) continue;
170
186
  files.push({ src, dest });
171
187
  }
172
188
  }
@@ -679,6 +695,7 @@ async function installGlobal(force) {
679
695
  { src: path.join(packageRoot, '.claude', 'skills'), dest: path.join(target, 'skills') },
680
696
  { src: path.join(packageRoot, '.claude', 'commands'), dest: path.join(target, 'commands') },
681
697
  { src: path.join(packageRoot, '.claude', 'workflows'), dest: path.join(target, 'workflows') },
698
+ { src: path.join(packageRoot, '.claude', 'scripts'), dest: path.join(target, 'scripts') },
682
699
  { src: path.join(packageRoot, 'docs'), dest: path.join(target, 'docs') },
683
700
  { src: path.join(packageRoot, 'CLAUDE.global.md'), dest: path.join(target, 'CLAUDE.md') },
684
701
  ];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dtlabs/ai-toolkit",
3
- "version": "0.9.0",
3
+ "version": "0.10.1",
4
4
  "description": "AI Toolkit — reusable agents and procedures for developers to build AI-powered applications.",
5
5
  "files": [
6
6
  "docs/",
@@ -8,7 +8,12 @@
8
8
  "CHANGELOG.md",
9
9
  "README.md",
10
10
  "CLAUDE.global.md",
11
- ".claude",
11
+ ".claude/agents",
12
+ ".claude/commands",
13
+ ".claude/workflows",
14
+ ".claude/skills",
15
+ ".claude/scripts/*.js",
16
+ ".claude/scripts/.gitkeep",
12
17
  "bin"
13
18
  ],
14
19
  "bin": {
@@ -1 +0,0 @@
1
- 0.2.1
@@ -1,5 +0,0 @@
1
- {
2
- "env": {
3
- "CLAUDE_CODE_MAX_SUBAGENT_SPAWN_DEPTH": "2"
4
- }
5
- }