@dtlabs/ai-toolkit 0.2.3 → 0.3.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.
@@ -0,0 +1 @@
1
+ 0.2.1
@@ -50,7 +50,9 @@ Extract the **feature prefix** from the folder name containing `feature.md`:
50
50
 
51
51
  The prefix is everything up to and including the second hyphen-separated segment (pattern: `[A-Z]+-[0-9]+`).
52
52
 
53
- Output file: `{PREFIX}-Work-Breakdown.md` in the same directory as `feature.md`.
53
+ Output files (both in the same directory as `feature.md`):
54
+ - `{PREFIX}-Work-Breakdown.md` — full human-readable document
55
+ - `{PREFIX}-Work-Breakdown.csv` — machine-readable task list for automated dispatch
54
56
 
55
57
  ---
56
58
 
@@ -123,6 +125,45 @@ Provide **two time estimates** for each task — one for a human developer, one
123
125
 
124
126
  Write the Work Breakdown document following the Output Template below.
125
127
 
128
+ ### Step 8 — Generate Work Breakdown CSV
129
+
130
+ After writing the `.md` file, write `{PREFIX}-Work-Breakdown.csv` in the same directory.
131
+
132
+ **Format:** pipe-separated (`|`), no quoting, one row per task.
133
+
134
+ **Header row:**
135
+ ```
136
+ phase_id|phase_title|commit_message|depends_on|task_id|task_title|domain|agent_type
137
+ ```
138
+
139
+ **Rules:**
140
+ - `phase_id`: `INFRA` for shared infrastructure tasks; `US-XX` for User Story tasks
141
+ - `phase_title`: the phase/US title (e.g. "Shared Infrastructure", "Query AssemblyItems Collection")
142
+ - `commit_message`: the git commit message for that phase:
143
+ - INFRA → `feat({PREFIX}): implement shared infrastructure (INFRA)`
144
+ - US-XX → `feat({PREFIX}): implement {US-ID} — {US title}`
145
+ - `depends_on`: space-separated list of `phase_id` values this phase depends on (e.g. `INFRA US-01`). Empty string `""` for phases with no dependencies. Derived from the Dependency Graph in Section 4 of the WB.
146
+ - `task_id`: the task ID (e.g. `INFRA-T01`, `US-01-T03`)
147
+ - `task_title`: short task title (no pipes — replace any `|` with `-`)
148
+ - `domain`: `DB`, `BE`, `FE`, `INFRA`, or `TEST`
149
+ - `agent_type`: derived from domain:
150
+ - `DB`, `BE`, `INFRA` → `developer-backend`
151
+ - `FE` → `developer-frontend`
152
+ - `TEST` → `developer-testing`
153
+
154
+ **Example rows:**
155
+ ```
156
+ phase_id|phase_title|commit_message|depends_on|task_id|task_title|domain|agent_type
157
+ INFRA|Shared Infrastructure|feat(FTR-004): implement shared infrastructure (INFRA)||INFRA-T01|Create IConstructionValidator interface|INFRA|developer-backend
158
+ US-01|Query AssemblyItems Collection|feat(FTR-004): implement US-01 — Query AssemblyItems Collection|INFRA|US-01-T01|Refactor IAssemblyItemRepository interface|INFRA|developer-backend
159
+ US-02|Query Single AssemblyItem|feat(FTR-004): implement US-02 — Query Single AssemblyItem|INFRA US-01|US-02-T01|Update controller Get(key)|INFRA|developer-backend
160
+ US-03|Query Volumes Collection|feat(FTR-004): implement US-03 — Query Volumes Collection|INFRA|US-03-T01|Refactor IVolumeRepository interface|INFRA|developer-backend
161
+ ```
162
+
163
+ Note: `depends_on` is the same for ALL rows of the same phase — it is a phase-level attribute repeated on every task row for simplicity.
164
+
165
+ All tasks from all phases must appear in the CSV, in implementation order (INFRA tasks first, then US tasks in phase order). Within each phase, non-TEST tasks before TEST tasks.
166
+
126
167
  ---
127
168
 
128
169
  ## Output Template
@@ -43,6 +43,7 @@ const PARSE_SCHEMA = {
43
43
  work_breakdown_path: { type: 'string' },
44
44
  effort_estimate_path: { type: 'string' },
45
45
  token_estimate_path: { type: 'string' },
46
+ tasks_csv_path: { type: 'string' },
46
47
  },
47
48
  required: ['prefix', 'user_stories', 'total_tasks', 'work_breakdown_path'],
48
49
  }
@@ -150,6 +151,7 @@ Token estimates use ~15,000 tokens per BE task, ~8,000 per TEST task, ~5,000 per
150
151
  TASK 4 — Set return values.
151
152
  Set effort_estimate_path to the full path of the written Effort-Estimate.md.
152
153
  Set token_estimate_path to the full path of the written Token-Estimate.md.
154
+ 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).
153
155
 
154
156
  Return the extracted metrics as structured output.`,
155
157
  {
@@ -202,6 +204,7 @@ return {
202
204
  work_breakdown_path: metrics.work_breakdown_path,
203
205
  effort_estimate_path: metrics.effort_estimate_path || '',
204
206
  token_estimate_path: metrics.token_estimate_path || '',
207
+ tasks_csv_path: metrics.tasks_csv_path || '',
205
208
  token_ledger: tokenLedger,
206
209
  errors: [],
207
210
  }
@@ -1,8 +1,9 @@
1
1
  export const meta = {
2
2
  name: 'pm-phase3',
3
- description: 'Feature delivery phase 3: implementation loop (developer agents + review-solution per US, max 2 rework cycles, git commit per US) → remediation → PR → Feature Registry → Token-Estimate actuals → Effort-Estimate actuals.',
3
+ description: 'Feature delivery phase 3: parse WB → dispatch specialist agents per US (developer-backend/frontend/testing + review-solution per US, max 2 rework cycles, git commit per US) → remediation → PR → Token-Estimate actuals → Effort-Estimate actuals.',
4
4
  phases: [
5
- { title: 'Implementation', detail: 'Dispatch developer agents per work breakdown, review per US' },
5
+ { title: 'Parse', detail: 'Parse Work Breakdown into structured implementation phases' },
6
+ { title: 'Implementation', detail: 'Dispatch developer agents per US, review, commit' },
6
7
  { title: 'Remediation', detail: 'Fix OPEN issues from Issues Register' },
7
8
  { title: 'PR', detail: 'Push branch, create PR, update registry' },
8
9
  { title: 'Actuals', detail: 'Write Token-Estimate and Effort-Estimate actuals' },
@@ -15,92 +16,263 @@ const featurePath = argStr.trim().split(/\s+/)[0]
15
16
  const branchMatch = argStr.match(/--branch\s+(\S+)/)
16
17
  const branch = branchMatch ? branchMatch[1] : null
17
18
 
18
- // ── Implementation ────────────────────────────────────────────────────────────
19
- phase('Implementation')
19
+ const tokenLedger = []
20
+ const escalations = []
21
+
22
+ // ── Parse Work Breakdown CSV (deterministic, no AI) ───────────────────────────
23
+ phase('Parse')
24
+
25
+ // Derive feature_dir and prefix from featurePath (e.g. "docs/features/FTR-004-.../feature.md")
26
+ const featureDir = featurePath.replace(/\/[^/]+$/, '')
27
+ const prefixMatch = featureDir.match(/([A-Z]+-\d+)/)
28
+ const prefix = prefixMatch ? prefixMatch[1] : 'FTR-000'
29
+ const csvPath = `${featureDir}/${prefix}-Work-Breakdown.csv`
30
+
31
+ log(`Reading CSV: ${csvPath}`)
32
+
33
+ const READ_SCHEMA = {
34
+ type: 'object',
35
+ properties: { content: { type: 'string' } },
36
+ required: ['content'],
37
+ }
38
+
39
+ const csvResult = await agent(
40
+ `Read the file at path: ${csvPath}\nReturn its full text content as the "content" field.`,
41
+ { label: 'read-wb-csv', phase: 'Parse', schema: READ_SCHEMA, model: 'haiku' }
42
+ )
43
+
44
+ // Parse CSV into structured phases — pure JS, no AI
45
+ const rows = csvResult.content
46
+ .split('\n')
47
+ .map(l => l.trim())
48
+ .filter(l => l && !l.startsWith('phase_id')) // skip header and empty lines
49
+ .map(l => {
50
+ const [phase_id, phase_title, commit_message, depends_on, task_id, task_title, domain, agent_type] = l.split('|')
51
+ return { phase_id, phase_title, commit_message, depends_on: depends_on || '', task_id, task_title, domain, agent_type }
52
+ })
53
+ .filter(r => r.phase_id && r.task_id)
54
+
55
+ // Group rows into phases preserving order
56
+ const phaseMap = new Map()
57
+ for (const row of rows) {
58
+ if (!phaseMap.has(row.phase_id)) {
59
+ phaseMap.set(row.phase_id, {
60
+ phase_id: row.phase_id,
61
+ title: row.phase_title,
62
+ commit_message: row.commit_message,
63
+ depends_on: row.depends_on.split(' ').filter(Boolean), // [] for empty
64
+ impl_tasks: [],
65
+ test_tasks: [],
66
+ })
67
+ }
68
+ const p = phaseMap.get(row.phase_id)
69
+ if (row.domain === 'TEST') {
70
+ p.test_tasks.push({ task_id: row.task_id, agent_type: row.agent_type })
71
+ } else {
72
+ p.impl_tasks.push({ task_id: row.task_id, agent_type: row.agent_type })
73
+ }
74
+ }
75
+
76
+ // Build impl_groups and test_groups (merge by agent_type within each phase)
77
+ const buildGroups = (tasks) => {
78
+ const map = new Map()
79
+ for (const t of tasks) {
80
+ if (!map.has(t.agent_type)) map.set(t.agent_type, [])
81
+ map.get(t.agent_type).push(t.task_id)
82
+ }
83
+ return Array.from(map.entries()).map(([agent_type, task_ids]) => ({ agent_type, task_ids }))
84
+ }
85
+
86
+ const allPhases = Array.from(phaseMap.values()).map(p => ({
87
+ phase_id: p.phase_id,
88
+ title: p.title,
89
+ commit_message: p.commit_message,
90
+ depends_on: p.depends_on,
91
+ impl_groups: buildGroups(p.impl_tasks),
92
+ test_groups: buildGroups(p.test_tasks),
93
+ }))
94
+
95
+ const wb = { prefix, feature_dir: featureDir, phases: allPhases }
96
+
97
+ log(`CSV parsed: ${rows.length} tasks → ${wb.phases.length} phases for ${wb.prefix}`)
98
+
99
+ // Build execution waves: phases whose dependencies are all satisfied can run in parallel.
100
+ // Wave 0 = no deps, Wave 1 = depends only on Wave 0 phases, etc.
101
+ const buildWaves = (phases) => {
102
+ const done = new Set()
103
+ const waves = []
104
+ let remaining = [...phases]
105
+ while (remaining.length > 0) {
106
+ const ready = remaining.filter(p => p.depends_on.every(d => done.has(d)))
107
+ if (ready.length === 0) {
108
+ // Circular or unresolvable deps — run the rest sequentially to avoid deadlock
109
+ waves.push(remaining)
110
+ break
111
+ }
112
+ waves.push(ready)
113
+ ready.forEach(p => done.add(p.phase_id))
114
+ remaining = remaining.filter(p => !done.has(p.phase_id))
115
+ }
116
+ return waves
117
+ }
20
118
 
21
- log(`Starting implementation for ${featurePath}${branch ? ` on branch ${branch}` : ''}`)
119
+ const waves = buildWaves(wb.phases)
120
+ log(`Execution plan: ${waves.length} wave(s) — ${waves.map((w, i) => `wave${i+1}:[${w.map(p => p.phase_id).join(',')}]`).join(' ')}`)
22
121
 
23
- const tokenLedger = []
24
- const escalations = []
122
+ const prefix = wb.prefix
123
+ let phasesDone = 0
124
+ const usPassed = []
125
+ const usEscalated = []
126
+ let issuesPath = ''
127
+ let issuesOpen = 0
25
128
 
26
- // Read the Work Breakdown and execute implementation
27
- // A single orchestration agent handles the full implementation loop:
28
- // parse WB → per-phase dispatch → review per US → rework (max 2 cycles) → commit per US
29
- // It uses sub-tool calls (Read, Bash, etc.) to do git work and reads the WB itself.
30
- // It does NOT spawn further subagents — it reads files and calls Bash for git commands.
129
+ // ── Implementation ────────────────────────────────────────────────────────────
130
+ phase('Implementation')
31
131
 
32
- const IMPL_SCHEMA = {
132
+ const REVIEW_SCHEMA = {
33
133
  type: 'object',
34
134
  properties: {
35
- prefix: { type: 'string' },
36
- phases_done: { type: 'number' },
37
- us_passed: { type: 'array', items: { type: 'string' } },
38
- us_escalated: { type: 'array', items: { type: 'string' } },
39
- issues_path: { type: 'string' },
40
- issues_open: { type: 'number' },
135
+ has_critical: { type: 'boolean' },
136
+ findings: { type: 'string' },
137
+ issues_md: { type: 'string' },
41
138
  },
42
- required: ['prefix', 'phases_done'],
139
+ required: ['has_critical'],
43
140
  }
44
141
 
45
- const beforeImpl = budget.spent()
142
+ // executePhase: runs impl_groups → test_groups → review → rework → commit for one phase
143
+ const executePhase = async (implPhase) => {
144
+ log(`Starting phase ${implPhase.phase_id}: ${implPhase.title}`)
145
+
146
+ let reworkCycle = 0
147
+ let passed = false
148
+ let lastFindings = ''
149
+
150
+ while (reworkCycle <= 2 && !passed) {
151
+ const reworkSuffix = reworkCycle > 0
152
+ ? ` -- REWORK CYCLE ${reworkCycle}: address these critical findings:\n${lastFindings}`
153
+ : ''
154
+
155
+ // Step 1 — impl groups in parallel (INFRA / BE / FE)
156
+ if (implPhase.impl_groups.length > 0) {
157
+ const beforeImpl = budget.spent()
158
+ await parallel(implPhase.impl_groups.map(group => () =>
159
+ agent(
160
+ `${featurePath} ${group.task_ids.join(',')}${reworkSuffix}`,
161
+ {
162
+ agentType: group.agent_type,
163
+ label: `${group.agent_type}:${implPhase.phase_id}${reworkCycle > 0 ? ':rework' + reworkCycle : ''}`,
164
+ phase: 'Implementation',
165
+ }
166
+ )
167
+ ))
168
+ const implTokens = budget.spent() - beforeImpl
169
+ tokenLedger.push({
170
+ agent: `${implPhase.impl_groups.map(g => g.agent_type).join('+')}:${implPhase.phase_id}${reworkCycle > 0 ? ':rework' + reworkCycle : ''}`,
171
+ model: 'sonnet',
172
+ phase_delta_tokens: implTokens,
173
+ })
174
+ log(`Phase ${implPhase.phase_id} impl groups done — ${implTokens} tokens`)
175
+ }
46
176
 
47
- const implResult = await agent(
48
- `You are the implementation orchestrator for the feature delivery pipeline.
177
+ // Step 2 — test groups in parallel (TEST) — after impl groups
178
+ if (implPhase.test_groups.length > 0) {
179
+ const beforeTest = budget.spent()
180
+ await parallel(implPhase.test_groups.map(group => () =>
181
+ agent(
182
+ `${featurePath} ${group.task_ids.join(',')}${reworkSuffix}`,
183
+ {
184
+ agentType: group.agent_type,
185
+ label: `${group.agent_type}:${implPhase.phase_id}${reworkCycle > 0 ? ':rework' + reworkCycle : ''}`,
186
+ phase: 'Implementation',
187
+ }
188
+ )
189
+ ))
190
+ const testTokens = budget.spent() - beforeTest
191
+ tokenLedger.push({
192
+ agent: `developer-testing:${implPhase.phase_id}${reworkCycle > 0 ? ':rework' + reworkCycle : ''}`,
193
+ model: 'sonnet',
194
+ phase_delta_tokens: testTokens,
195
+ })
196
+ log(`Phase ${implPhase.phase_id} test groups done — ${testTokens} tokens`)
197
+ }
49
198
 
50
- Feature path: ${featurePath}
51
- Branch: ${branch || '(already checked out)'}
52
-
53
- Your job:
54
-
55
- 1. Read {PREFIX}-Work-Breakdown.md (same directory as feature.md).
56
- Extract all phases, tasks, domains, and User Story groupings from Section 4.
57
-
58
- 2. For each phase in order:
59
- a. Identify tasks grouped by User Story (US-XX) or INFRA.
60
- b. For each group, spawn the correct developer agent:
61
- - DB/BE/INFRA tasks → subagent_type: developer-backend
62
- - FE tasks → subagent_type: developer-frontend
63
- - TEST tasks → subagent_type: developer-testing
64
- Prompt: "${featurePath} <comma-separated task IDs>"
65
- Independent groups within a phase can be dispatched in parallel (multiple Agent calls in same response).
66
- c. When ALL tasks for a User Story are done → spawn review-solution:
67
- subagent_type: review-solution, prompt: "${featurePath} --scope <US-ID>"
68
- - PASS (no CRITICAL): git add -A && git commit -m "feat({PREFIX}): implement {US-ID} — {title}"
69
- Log WARNING/INFO findings to {PREFIX}-Issues.md.
70
- - FAIL (CRITICAL): rework — re-dispatch the developer agent with the review findings appended to the prompt.
71
- Max 2 rework cycles. After 2 failed cycles → escalate (add to escalations list).
72
- INFRA tasks: reviewed as a group, commit: "feat({PREFIX}): implement shared infrastructure (INFRA)"
73
-
74
- 3. After all phases complete:
75
- - Read {PREFIX}-Issues.md if it exists. Count OPEN items.
76
- - Report: prefix, phases_done, us_passed (list of US IDs that passed), us_escalated (list that escalated), issues_path, issues_open.
77
-
78
- IMPORTANT RULES:
79
- - Never use run_in_background: true when spawning agents.
80
- - Track escalations: if a US fails review after 2 rework cycles, add its ID to us_escalated and continue.
81
- - Partial failures are acceptable — continue with remaining phases.
82
- - All git commands via Bash tool.
83
- - Commit message format: feat({PREFIX}): implement {US-ID} — {US title}`,
84
- {
85
- label: 'implementation-orchestrator',
86
- phase: 'Implementation',
87
- schema: IMPL_SCHEMA,
199
+ // Step 3 — review-solution
200
+ const beforeReview = budget.spent()
201
+ const review = await agent(
202
+ `${featurePath} --scope ${implPhase.phase_id}`,
203
+ {
204
+ agentType: 'review-solution',
205
+ label: `review-solution:${implPhase.phase_id}`,
206
+ phase: 'Implementation',
207
+ schema: REVIEW_SCHEMA,
208
+ }
209
+ )
210
+ const reviewTokens = budget.spent() - beforeReview
211
+ tokenLedger.push({
212
+ agent: `review-solution:${implPhase.phase_id}`,
213
+ model: 'sonnet',
214
+ phase_delta_tokens: reviewTokens,
215
+ })
216
+
217
+ if (review && !review.has_critical) {
218
+ passed = true
219
+ if (review.issues_md) {
220
+ await agent(
221
+ `Append the following WARNING/INFO findings to ${wb.feature_dir}/${prefix}-Issues.md (create the file if it does not exist, with a header "# Issues Register — ${prefix}"):\n\n${review.issues_md}`,
222
+ { label: `log-issues:${implPhase.phase_id}`, phase: 'Implementation' }
223
+ )
224
+ }
225
+ } else {
226
+ lastFindings = review?.findings || 'unspecified critical issues'
227
+ reworkCycle++
228
+ log(`Phase ${implPhase.phase_id}: critical findings — rework cycle ${reworkCycle}`)
229
+ }
88
230
  }
89
- )
90
231
 
91
- const implTokens = budget.spent() - beforeImpl
92
- tokenLedger.push({ agent: 'implementation-orchestrator', model: 'sonnet', phase_delta_tokens: implTokens })
93
- log(`Implementation done — ${implResult.phases_done} phases, ${(implResult.us_passed || []).length} US passed, ${(implResult.us_escalated || []).length} escalated`)
94
- log(`Phase delta: ${implTokens} tokens`)
232
+ if (!passed) {
233
+ log(`Phase ${implPhase.phase_id} ESCALATED after 2 rework cycles`)
234
+ escalations.push({ phaseId: implPhase.phase_id, reason: 'CRITICAL review findings unresolved after 2 rework cycles' })
235
+ usEscalated.push(implPhase.phase_id)
236
+ } else {
237
+ const commitMsg = implPhase.commit_message || `feat(${prefix}): implement ${implPhase.phase_id}`
238
+ await agent(
239
+ `Run these git commands in the repository root:\ngit add -A\ngit commit -m "${commitMsg}"\nIf there is nothing to commit, that is fine — just report success.`,
240
+ { label: `commit:${implPhase.phase_id}`, phase: 'Implementation' }
241
+ )
242
+ if (implPhase.phase_id !== 'INFRA') usPassed.push(implPhase.phase_id)
243
+ log(`Phase ${implPhase.phase_id} passed review and committed`)
244
+ }
245
+
246
+ phasesDone++
247
+ }
95
248
 
96
- if (implResult.us_escalated?.length) {
97
- for (const usId of implResult.us_escalated) {
98
- escalations.push({ usId, reason: 'CRITICAL review finding unresolved after 2 rework cycles' })
249
+ // Execute waves: phases in the same wave run in parallel, waves are sequential
250
+ for (const wave of waves) {
251
+ if (wave.length === 1) {
252
+ await executePhase(wave[0])
253
+ } else {
254
+ log(`Wave: running ${wave.map(p => p.phase_id).join(', ')} in parallel`)
255
+ await parallel(wave.map(p => () => executePhase(p)))
99
256
  }
100
257
  }
101
258
 
102
- const prefix = implResult.prefix
103
- const issuesPath = implResult.issues_path || ''
259
+ // Count open issues
260
+ const ISSUES_SCHEMA = {
261
+ type: 'object',
262
+ properties: {
263
+ exists: { type: 'boolean' },
264
+ open_count: { type: 'number' },
265
+ path: { type: 'string' },
266
+ },
267
+ required: ['exists', 'open_count'],
268
+ }
269
+ const issuesResult = await agent(
270
+ `Check if ${wb.feature_dir}/${prefix}-Issues.md exists. If it does, count rows with status OPEN. Return exists, open_count, and the full path.`,
271
+ { label: 'count-open-issues', phase: 'Implementation', schema: ISSUES_SCHEMA }
272
+ )
273
+ issuesPath = issuesResult?.path || ''
274
+ issuesOpen = issuesResult?.open_count || 0
275
+ log(`Implementation complete — ${phasesDone} phases, ${usPassed.length} US passed, ${usEscalated.length} escalated, ${issuesOpen} open issues`)
104
276
 
105
277
  // ── Remediation ───────────────────────────────────────────────────────────────
106
278
  phase('Remediation')
@@ -117,8 +289,8 @@ const REM_SCHEMA = {
117
289
  let issuesFixed = 0
118
290
  let issuesDeferred = 0
119
291
 
120
- if (implResult.issues_open > 0 && issuesPath) {
121
- log(`Remediation: ${implResult.issues_open} OPEN issues`)
292
+ if (issuesOpen > 0 && issuesPath) {
293
+ log(`Remediation: ${issuesOpen} OPEN issues`)
122
294
  const beforeRem = budget.spent()
123
295
 
124
296
  const remResult = await agent(
@@ -127,11 +299,10 @@ if (implResult.issues_open > 0 && issuesPath) {
127
299
  Read the Issues Register at: ${issuesPath}
128
300
 
129
301
  For each OPEN issue:
130
- - If severity is INFO: mark it DEFERRED immediately (update the Status column to DEFERRED). Do not dispatch a developer agent.
302
+ - If severity is INFO: mark it DEFERRED immediately (update Status column to DEFERRED). Do not dispatch a developer agent.
131
303
  - If severity is WARNING: dispatch the appropriate developer agent (developer-backend for most cases) with the issue description. Then spawn review-solution to verify the fix. Max 1 retry per issue. If unresolved after 1 retry, mark DEFERRED.
132
304
 
133
- After processing all issues, return:
134
- { "issues_fixed": N, "issues_deferred": N }
305
+ After processing all issues, return: { "issues_fixed": N, "issues_deferred": N }
135
306
 
136
307
  Update the Issues Register Status column in place for each resolved/deferred issue.`,
137
308
  {
@@ -145,7 +316,7 @@ Update the Issues Register Status column in place for each resolved/deferred iss
145
316
  tokenLedger.push({ agent: 'remediation', model: 'sonnet', phase_delta_tokens: remTokens })
146
317
  issuesFixed = remResult.issues_fixed || 0
147
318
  issuesDeferred = remResult.issues_deferred || 0
148
- log(`Remediation done — fixed: ${issuesFixed}, deferred: ${issuesDeferred} | phase delta: ${remTokens} tokens`)
319
+ log(`Remediation done — fixed: ${issuesFixed}, deferred: ${issuesDeferred} — ${remTokens} tokens`)
149
320
  } else {
150
321
  log('Remediation: no OPEN issues — skipped')
151
322
  }
@@ -156,8 +327,8 @@ phase('PR')
156
327
  const PR_SCHEMA = {
157
328
  type: 'object',
158
329
  properties: {
159
- pr_url: { type: 'string' },
160
- registry_updated: { type: 'boolean' },
330
+ pr_url: { type: 'string' },
331
+ registry_updated: { type: 'boolean' },
161
332
  },
162
333
  required: ['pr_url'],
163
334
  }
@@ -212,20 +383,17 @@ If the PR creation fails, return { "pr_url": "(PR creation failed)", "registry_u
212
383
 
213
384
  const prTokens = budget.spent() - beforePR
214
385
  tokenLedger.push({ agent: 'pr-and-registry', model: 'sonnet', phase_delta_tokens: prTokens })
215
- log(`PR done: ${prResult.pr_url} | phase delta: ${prTokens} tokens`)
386
+ log(`PR done: ${prResult.pr_url} — ${prTokens} tokens`)
216
387
 
217
388
  // ── Actuals ───────────────────────────────────────────────────────────────────
218
389
  phase('Actuals')
219
390
 
220
- // Compute proportional distribution of phase tokens across logical agents
221
- // Phase tokens are exact (budget.spent() deltas); per-agent breakdown is proportional.
222
- // This is documented in the Token-Estimate file with a disclaimer.
223
- const totalImplTokens = tokenLedger.reduce((s, e) => s + (e.phase_delta_tokens || 0), 0)
391
+ const totalPhase3Tokens = tokenLedger.reduce((s, e) => s + (e.phase_delta_tokens || 0), 0)
224
392
 
225
393
  const ACTUALS_SCHEMA = {
226
394
  type: 'object',
227
395
  properties: {
228
- token_estimate_path: { type: 'string' },
396
+ token_estimate_path: { type: 'string' },
229
397
  effort_estimate_path: { type: 'string' },
230
398
  },
231
399
  required: [],
@@ -242,38 +410,35 @@ Prefix: ${prefix}
242
410
  Token ledger (phase-level measurements):
243
411
  ${JSON.stringify(tokenLedger, null, 2)}
244
412
 
245
- Total tokens consumed by pm-phase3 workflow (all phases combined): ${totalImplTokens} (exact, from budget tracking)
413
+ Total tokens consumed by pm-phase3 workflow: ${totalPhase3Tokens} (exact, from budget tracking)
246
414
 
247
- TASK 1 — Update Token-Estimate.md:
248
- Find ${prefix}-Token-Estimate.md in the feature directory.
249
- If it doesn't exist, create it with the structure from docs/procedures/token-estimation.md.
415
+ TASK 1 — Append actuals to ${prefix}-Token-Estimate.md:
416
+ Find the file in the feature directory. If it does not exist, create it.
250
417
 
251
- Append this section at the end:
418
+ Append this section at the end of the file:
252
419
 
253
420
  ---
254
421
 
255
- ## Actuals vs Estimate
422
+ ## Actuals vs Estimate (pm-phase3)
256
423
 
257
- > ⚠️ Per-agent values are proportional distributions of the phase total.
258
- > Phase totals (pm-phase3 total: ${totalImplTokens} tokens) are exact measurements.
259
- > Individual agent breakdown is estimated proportionally.
424
+ > Phase totals are exact measurements from budget tracking.
425
+ > Per-agent values are exact where each agent ran separately; proportional where merged.
260
426
 
261
- | Agent | Task / Scope | Model | Phase delta tokens | Notes |
262
- |-------|-------------|-------|-------------------|-------|
263
- ${tokenLedger.map(e => `| ${e.agent} | — | ${e.model} | ${e.phase_delta_tokens} | exact phase delta |`).join('\n')}
427
+ | Agent | Phase | Model | Phase delta tokens |
428
+ |-------|-------|-------|-------------------|
429
+ ${tokenLedger.map(e => `| ${e.agent} | — | ${e.model} | ${e.phase_delta_tokens} |`).join('\n')}
264
430
 
265
431
  ## Grand Total (pm-phase3)
266
432
 
267
433
  | Metric | Value |
268
434
  |--------|-------|
269
- | Total tokens (pm-phase3) | ${totalImplTokens} (exact) |
270
- | Implementation phases | ${implResult.phases_done} |
271
- | US passed | ${(implResult.us_passed || []).join(', ') || 'N/A'} |
272
- | US escalated | ${(implResult.us_escalated || []).join(', ') || 'none'} |
435
+ | Total tokens (pm-phase3) | ${totalPhase3Tokens} (exact) |
436
+ | Implementation phases done | ${phasesDone} |
437
+ | US passed | ${usPassed.join(', ') || 'N/A'} |
438
+ | US escalated | ${usEscalated.join(', ') || 'none'} |
273
439
 
274
- TASK 2 — Append actuals to Effort-Estimate.md:
275
- Find ${prefix}-Effort-Estimate.md in the feature directory.
276
- Append:
440
+ TASK 2 — Append actuals to ${prefix}-Effort-Estimate.md:
441
+ Find the file in the feature directory. Append:
277
442
 
278
443
  ---
279
444
 
@@ -281,7 +446,7 @@ Append:
281
446
 
282
447
  | Metric | Estimated | Actual |
283
448
  |--------|-----------|--------|
284
- | Implementation phases | ${implResult.phases_done} | ${implResult.phases_done} |
449
+ | Implementation phases | ${phasesDone} | ${phasesDone} |
285
450
  | Issues fixed | — | ${issuesFixed} |
286
451
  | Issues deferred | — | ${issuesDeferred} |
287
452
 
@@ -295,15 +460,16 @@ Return { "token_estimate_path": "<path>", "effort_estimate_path": "<path>" }.`,
295
460
 
296
461
  const actualsTokens = budget.spent() - beforeActuals
297
462
  tokenLedger.push({ agent: 'write-actuals', model: 'sonnet', phase_delta_tokens: actualsTokens })
298
- log(`Actuals written | phase delta: ${actualsTokens} tokens`)
463
+ log(`Actuals written — ${actualsTokens} tokens`)
299
464
 
300
- // ── Append to process-log (phase 3 events + RUN COMPLETE) ────────────────────
301
- const usPassed = (implResult.us_passed || []).join(', ') || 'none'
302
- const usEscalated = (implResult.us_escalated || []).join(', ') || 'none'
465
+ // ── Append to process-log ─────────────────────────────────────────────────────
466
+ const usPassed3 = usPassed.join(', ') || 'none'
467
+ const usEscalated3 = usEscalated.join(', ') || 'none'
303
468
  const phase3Events = [
304
469
  `APPROVAL GRANTED by user — Gate 2`,
305
470
  `pm-phase3 START — implementation phase — branch: ${branch || '(current)'}`,
306
- `Implementation DONE — ${implResult.phases_done} phases | US passed: ${usPassed} | US escalated: ${usEscalated}`,
471
+ `WB parsed: ${wb.phases.length} phases`,
472
+ `Implementation DONE — ${phasesDone} phases | US passed: ${usPassed3} | US escalated: ${usEscalated3}`,
307
473
  issuesFixed + issuesDeferred > 0
308
474
  ? `Remediation: ${issuesFixed} fixed, ${issuesDeferred} deferred`
309
475
  : `Remediation: skipped (no open issues)`,
@@ -313,7 +479,6 @@ const phase3Events = [
313
479
  `════════════════════════════════════════════════════════`,
314
480
  ].join('\n')
315
481
 
316
- // feature_dir is not directly available in pm-phase3; derive from featurePath
317
482
  const featureDir3 = featurePath.replace(/\/[^/]+$/, '')
318
483
 
319
484
  await agent(
@@ -323,9 +488,9 @@ Process-log path: ${featureDir3}/${prefix}-process-log.txt
323
488
 
324
489
  Steps:
325
490
  1. Get current UTC datetime via Bash: run \`date -u +"%Y-%m-%dT%H:%M:%S"\`
326
- 2. Append the following lines to the existing file. Use the datetime from step 1 for ALL event lines.
491
+ 2. Append the following lines to the existing file (create it if it does not exist).
327
492
  Format each event line as: [{datetime}] {event text}
328
- The final line (════…) is a separator — append it as-is, without a timestamp prefix.
493
+ The final separator line (════…) is appended as-is, without a timestamp.
329
494
 
330
495
  Events to append:
331
496
  ${phase3Events}`,
@@ -336,9 +501,9 @@ return {
336
501
  pr_url: prResult.pr_url,
337
502
  token_ledger: tokenLedger,
338
503
  issues_summary: {
339
- escalations: escalations.length,
504
+ escalations: escalations.length,
340
505
  escalation_details: escalations,
341
- issues_fixed: issuesFixed,
342
- issues_deferred: issuesDeferred,
506
+ issues_fixed: issuesFixed,
507
+ issues_deferred: issuesDeferred,
343
508
  },
344
509
  }
package/CHANGELOG.md CHANGED
@@ -1,4 +1,24 @@
1
1
  ## 0.1.4 (2026-07-21)
2
+ ## [0.3.0](https://github.com/dtlab-tech/ai-toolkit/compare/v0.2.3...v0.3.0) (2026-07-28)
3
+
4
+
5
+ ### Features
6
+
7
+ * **FTR-010:** add feature docs and delivery artifacts for unit test suite ([0ffd712](https://github.com/dtlab-tech/ai-toolkit/commit/0ffd7123d899655aea4d5acaee5e8b25ce2e1b43))
8
+ * **FTR-010:** add token estimate and process log for unit test suite ([ef2f38e](https://github.com/dtlab-tech/ai-toolkit/commit/ef2f38e03d063161f0521ca0c5b54119eff37cb6))
9
+ * **FTR-010:** implement shared infrastructure (INFRA) ([dfbc72c](https://github.com/dtlab-tech/ai-toolkit/commit/dfbc72caf0906e5ea0421f079881e6b020d3124c))
10
+ * **FTR-010:** implement US-01 — Developer Runs Unit Tests for CLI Functions ([60170aa](https://github.com/dtlab-tech/ai-toolkit/commit/60170aa21f245cbb76a1a201feab53439f4588f3))
11
+ * **FTR-010:** implement US-02 and US-03 — coverage report and frontmatter validation ([d939bc2](https://github.com/dtlab-tech/ai-toolkit/commit/d939bc20fe04ab5832e556f7df42a525833de1ab))
12
+ * **FTR-010:** implement US-04 — CI Pipeline Test Automation ([466ab0b](https://github.com/dtlab-tech/ai-toolkit/commit/466ab0bf6dc77d53e67304c2fde2a1be06ad1b70))
13
+ * **FTR-010:** Unit Test Suite — CLI Logic and Frontmatter Validation ([ad7d012](https://github.com/dtlab-tech/ai-toolkit/commit/ad7d012c7e6ac1cbb339d0249deaf34c2f42ee7a))
14
+ * **pm-phase2/3:** generate Work-Breakdown CSV for deterministic phase dispatch ([3ffa25e](https://github.com/dtlab-tech/ai-toolkit/commit/3ffa25ef30fdee6033f045e84a941ef9fc97a0cd))
15
+ * **wb-csv:** add depends_on column and parallel wave execution in pm-phase3 ([6939972](https://github.com/dtlab-tech/ai-toolkit/commit/6939972813573a893b070be2770826212172f5c3))
16
+
17
+
18
+ ### Bug Fixes
19
+
20
+ * **pm-phase3:** dispatch specialist agents per US instead of single orchestrator ([bef21c1](https://github.com/dtlab-tech/ai-toolkit/commit/bef21c1e4c1354c29bc9366b959237986a588f91))
21
+
2
22
  ## [0.2.3](https://github.com/dtlab-tech/ai-toolkit/compare/v0.2.2...v0.2.3) (2026-07-28)
3
23
 
4
24
 
package/README.md CHANGED
@@ -91,6 +91,47 @@ To let the toolkit work as designed, raise the allowed depth to **2** (main loop
91
91
 
92
92
  The orchestrator agents already omit the `tools:` frontmatter field, so they inherit the `Agent` tool automatically once the depth allows it — no per-agent changes are needed.
93
93
 
94
+ ## Development
95
+
96
+ ### Running tests locally
97
+
98
+ ```bash
99
+ npm install # install devDependencies (jest, gray-matter)
100
+ npm test # run all tests; stops at first failure (--bail)
101
+ npm run test:coverage # run all tests and generate a coverage report
102
+ ```
103
+
104
+ The test suite covers:
105
+
106
+ - **`tests/cli/`** — unit tests for pure functions in `bin/cli.js`
107
+ (`fileHash`, `walkDir`, `expandMappings`, `categorize`, `readInstalledVersion`, `isMattPocockInstalled`)
108
+ - **`tests/frontmatter/`** — structural validation of all `.claude/agents/*.md`
109
+ and `.claude/skills/**/SKILL.md` frontmatter fields
110
+
111
+ Coverage reports land in `coverage/` (gitignored). Open `coverage/index.html`
112
+ in a browser to browse line-level coverage. No coverage threshold is enforced;
113
+ the report is diagnostic only.
114
+
115
+ ### CI
116
+
117
+ A GitHub Actions workflow (`.github/workflows/ci.yml`) runs automatically on
118
+ every pull request targeting `main`:
119
+
120
+ 1. Checks out the repo on `ubuntu-latest` with Node 20
121
+ 2. Installs dependencies with `npm ci`
122
+ 3. Runs `npm test` — the PR check is red if any test fails
123
+ 4. Runs `npm run test:coverage` and uploads the `coverage/` directory as a
124
+ 30-day workflow artifact
125
+
126
+ ### Adding or changing tests
127
+
128
+ - When you modify a function in `bin/cli.js`, update the matching test file
129
+ in `tests/cli/`.
130
+ - When you add an agent `.md` or skill `SKILL.md`, the frontmatter tests in
131
+ `tests/frontmatter/` run against it automatically — just make sure the
132
+ required fields (`name`, `description`, `model` for agents; `description`
133
+ for skills) are present and valid.
134
+
94
135
  ## Documentation
95
136
 
96
137
  - [Quick Reference](docs/reference.md) — cheatsheet of every skill, command, agent, and procedure
package/bin/cli.js CHANGED
@@ -406,4 +406,20 @@ async function main() {
406
406
  }
407
407
  }
408
408
 
409
- main();
409
+ // ── entry point guard ─────────────────────────────────────────────────────────
410
+ // Run the CLI only when invoked directly (node bin/cli.js).
411
+ // When required as a module (e.g., by Jest), skip main() and export pure
412
+ // functions so they can be unit-tested without side effects.
413
+ if (require.main === module) {
414
+ main();
415
+ } else {
416
+ module.exports = {
417
+ fileHash,
418
+ walkDir,
419
+ expandMappings,
420
+ categorize,
421
+ readInstalledVersion,
422
+ isMattPocockInstalled,
423
+ NEVER_COPY,
424
+ };
425
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dtlabs/ai-toolkit",
3
- "version": "0.2.3",
3
+ "version": "0.3.0",
4
4
  "description": "AI Toolkit — reusable agents and procedures for developers to build AI-powered applications.",
5
5
  "files": [
6
6
  "docs/",
@@ -13,7 +13,13 @@
13
13
  "ai-toolkit": "bin/cli.js"
14
14
  },
15
15
  "scripts": {
16
- "prepublishOnly": "npm pack --dry-run"
16
+ "prepublishOnly": "npm pack --dry-run",
17
+ "test": "jest --bail",
18
+ "test:coverage": "jest --coverage"
19
+ },
20
+ "devDependencies": {
21
+ "gray-matter": "^4.0.3",
22
+ "jest": "^29.7.0"
17
23
  },
18
24
  "repository": {
19
25
  "type": "git",