@dtlabs/ai-toolkit 0.2.3 → 0.3.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.
- package/.claude/.ai-toolkit-version +1 -0
- package/.claude/agents/generate-work-breakdown.md +42 -1
- package/.claude/workflows/pm-phase2.js +3 -0
- package/.claude/workflows/pm-phase3.js +281 -116
- package/CHANGELOG.md +28 -0
- package/README.md +41 -0
- package/bin/cli.js +17 -1
- package/package.json +9 -2
|
@@ -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
|
|
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:
|
|
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: '
|
|
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
|
-
|
|
19
|
-
|
|
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
|
-
|
|
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
|
|
24
|
-
|
|
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
|
-
//
|
|
27
|
-
|
|
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
|
|
132
|
+
const REVIEW_SCHEMA = {
|
|
33
133
|
type: 'object',
|
|
34
134
|
properties: {
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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: ['
|
|
139
|
+
required: ['has_critical'],
|
|
43
140
|
}
|
|
44
141
|
|
|
45
|
-
|
|
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
|
-
|
|
48
|
-
|
|
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
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
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
|
-
|
|
97
|
-
|
|
98
|
-
|
|
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
|
-
|
|
103
|
-
const
|
|
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 (
|
|
121
|
-
log(`Remediation: ${
|
|
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
|
|
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}
|
|
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:
|
|
160
|
-
registry_updated:
|
|
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}
|
|
386
|
+
log(`PR done: ${prResult.pr_url} — ${prTokens} tokens`)
|
|
216
387
|
|
|
217
388
|
// ── Actuals ───────────────────────────────────────────────────────────────────
|
|
218
389
|
phase('Actuals')
|
|
219
390
|
|
|
220
|
-
|
|
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:
|
|
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
|
|
413
|
+
Total tokens consumed by pm-phase3 workflow: ${totalPhase3Tokens} (exact, from budget tracking)
|
|
246
414
|
|
|
247
|
-
TASK 1 —
|
|
248
|
-
Find
|
|
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
|
-
>
|
|
258
|
-
>
|
|
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 |
|
|
262
|
-
|
|
263
|
-
${tokenLedger.map(e => `| ${e.agent} | — | ${e.model} | ${e.phase_delta_tokens}
|
|
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) | ${
|
|
270
|
-
| Implementation phases | ${
|
|
271
|
-
| US passed | ${
|
|
272
|
-
| US escalated | ${
|
|
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
|
|
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 | ${
|
|
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
|
|
463
|
+
log(`Actuals written — ${actualsTokens} tokens`)
|
|
299
464
|
|
|
300
|
-
// ── Append to process-log
|
|
301
|
-
const
|
|
302
|
-
const
|
|
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
|
-
`
|
|
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
|
|
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
|
|
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:
|
|
504
|
+
escalations: escalations.length,
|
|
340
505
|
escalation_details: escalations,
|
|
341
|
-
issues_fixed:
|
|
342
|
-
issues_deferred:
|
|
506
|
+
issues_fixed: issuesFixed,
|
|
507
|
+
issues_deferred: issuesDeferred,
|
|
343
508
|
},
|
|
344
509
|
}
|
package/CHANGELOG.md
CHANGED
|
@@ -1,4 +1,32 @@
|
|
|
1
1
|
## 0.1.4 (2026-07-21)
|
|
2
|
+
## [0.3.1](https://github.com/dtlab-tech/ai-toolkit/compare/v0.3.0...v0.3.1) (2026-07-28)
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
### Bug Fixes
|
|
6
|
+
|
|
7
|
+
* include README.md in npm package files ([1998d47](https://github.com/dtlab-tech/ai-toolkit/commit/1998d476d23de1c2791af97b3217492b1364a9c8))
|
|
8
|
+
* include README.md in npm package files ([f710534](https://github.com/dtlab-tech/ai-toolkit/commit/f71053472afeb87c2a4cd8e8a0b826103c6205f0))
|
|
9
|
+
|
|
10
|
+
## [0.3.0](https://github.com/dtlab-tech/ai-toolkit/compare/v0.2.3...v0.3.0) (2026-07-28)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
### Features
|
|
14
|
+
|
|
15
|
+
* **FTR-010:** add feature docs and delivery artifacts for unit test suite ([0ffd712](https://github.com/dtlab-tech/ai-toolkit/commit/0ffd7123d899655aea4d5acaee5e8b25ce2e1b43))
|
|
16
|
+
* **FTR-010:** add token estimate and process log for unit test suite ([ef2f38e](https://github.com/dtlab-tech/ai-toolkit/commit/ef2f38e03d063161f0521ca0c5b54119eff37cb6))
|
|
17
|
+
* **FTR-010:** implement shared infrastructure (INFRA) ([dfbc72c](https://github.com/dtlab-tech/ai-toolkit/commit/dfbc72caf0906e5ea0421f079881e6b020d3124c))
|
|
18
|
+
* **FTR-010:** implement US-01 — Developer Runs Unit Tests for CLI Functions ([60170aa](https://github.com/dtlab-tech/ai-toolkit/commit/60170aa21f245cbb76a1a201feab53439f4588f3))
|
|
19
|
+
* **FTR-010:** implement US-02 and US-03 — coverage report and frontmatter validation ([d939bc2](https://github.com/dtlab-tech/ai-toolkit/commit/d939bc20fe04ab5832e556f7df42a525833de1ab))
|
|
20
|
+
* **FTR-010:** implement US-04 — CI Pipeline Test Automation ([466ab0b](https://github.com/dtlab-tech/ai-toolkit/commit/466ab0bf6dc77d53e67304c2fde2a1be06ad1b70))
|
|
21
|
+
* **FTR-010:** Unit Test Suite — CLI Logic and Frontmatter Validation ([ad7d012](https://github.com/dtlab-tech/ai-toolkit/commit/ad7d012c7e6ac1cbb339d0249deaf34c2f42ee7a))
|
|
22
|
+
* **pm-phase2/3:** generate Work-Breakdown CSV for deterministic phase dispatch ([3ffa25e](https://github.com/dtlab-tech/ai-toolkit/commit/3ffa25ef30fdee6033f045e84a941ef9fc97a0cd))
|
|
23
|
+
* **wb-csv:** add depends_on column and parallel wave execution in pm-phase3 ([6939972](https://github.com/dtlab-tech/ai-toolkit/commit/6939972813573a893b070be2770826212172f5c3))
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
### Bug Fixes
|
|
27
|
+
|
|
28
|
+
* **pm-phase3:** dispatch specialist agents per US instead of single orchestrator ([bef21c1](https://github.com/dtlab-tech/ai-toolkit/commit/bef21c1e4c1354c29bc9366b959237986a588f91))
|
|
29
|
+
|
|
2
30
|
## [0.2.3](https://github.com/dtlab-tech/ai-toolkit/compare/v0.2.2...v0.2.3) (2026-07-28)
|
|
3
31
|
|
|
4
32
|
|
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
|
-
|
|
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,11 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dtlabs/ai-toolkit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.1",
|
|
4
4
|
"description": "AI Toolkit — reusable agents and procedures for developers to build AI-powered applications.",
|
|
5
5
|
"files": [
|
|
6
6
|
"docs/",
|
|
7
7
|
"CLAUDE.md",
|
|
8
8
|
"CHANGELOG.md",
|
|
9
|
+
"README.md",
|
|
9
10
|
".claude",
|
|
10
11
|
"bin"
|
|
11
12
|
],
|
|
@@ -13,7 +14,13 @@
|
|
|
13
14
|
"ai-toolkit": "bin/cli.js"
|
|
14
15
|
},
|
|
15
16
|
"scripts": {
|
|
16
|
-
"prepublishOnly": "npm pack --dry-run"
|
|
17
|
+
"prepublishOnly": "npm pack --dry-run",
|
|
18
|
+
"test": "jest --bail",
|
|
19
|
+
"test:coverage": "jest --coverage"
|
|
20
|
+
},
|
|
21
|
+
"devDependencies": {
|
|
22
|
+
"gray-matter": "^4.0.3",
|
|
23
|
+
"jest": "^29.7.0"
|
|
17
24
|
},
|
|
18
25
|
"repository": {
|
|
19
26
|
"type": "git",
|