@dtlabs/ai-toolkit 0.8.0 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/agents/generate-work-breakdown.md +212 -192
- package/.claude/agents/install-toolkit.md +6 -1
- package/.claude/agents/validate-work-breakdown-semantic.md +96 -0
- package/.claude/scripts/.gitkeep +0 -0
- package/.claude/scripts/tests/fixtures/ac-table-format.md +48 -0
- package/.claude/scripts/tests/fixtures/test-feature/TEST-001-Work-Breakdown.json +234 -0
- package/.claude/scripts/tests/fixtures/test-feature/TEST-001-semantic-output.json +4 -0
- package/.claude/scripts/tests/fixtures/test-feature/feature.md +45 -0
- package/.claude/scripts/tests/fixtures/wb-invalid-bad-domain.json +39 -0
- package/.claude/scripts/tests/fixtures/wb-invalid-cycle.json +61 -0
- package/.claude/scripts/tests/fixtures/wb-invalid-duplicate-id.json +72 -0
- package/.claude/scripts/tests/fixtures/wb-invalid-missing-field.json +38 -0
- package/.claude/scripts/tests/fixtures/wb-valid.json +189 -0
- package/.claude/scripts/tests/helpers/buildWaves.js +21 -0
- package/.claude/scripts/tests/helpers/pm-phase2-flow.js +29 -0
- package/.claude/scripts/tests/helpers/pm-phase2-gate2.js +55 -0
- package/.claude/scripts/tests/helpers/pm-phase2-ledger.js +108 -0
- package/.claude/scripts/tests/installer.scripts-distribution.test.js +131 -0
- package/.claude/scripts/tests/pm-phase2.gate2-payload.test.js +353 -0
- package/.claude/scripts/tests/pm-phase2.ledger-transitions.test.js +346 -0
- package/.claude/scripts/tests/pm-phase2.orchestration-flow.test.js +276 -0
- package/.claude/scripts/tests/verify-us-01-t03.js +288 -0
- package/.claude/scripts/tests/verify-us-03-t04.js +262 -0
- package/.claude/scripts/tests/wb-render.checks-md-csv-deps.test.js +337 -0
- package/.claude/scripts/tests/wb-render.checks-sanitize-commit-paths.test.js +352 -0
- package/.claude/scripts/tests/wb-render.regression-pm-phase3-csv.test.js +219 -0
- package/.claude/scripts/tests/wb-validate.checks-1-7.test.js +330 -0
- package/.claude/scripts/tests/wb-validate.checks-11.test.js +166 -0
- package/.claude/scripts/tests/wb-validate.checks-12-14.test.js +222 -0
- package/.claude/scripts/tests/wb-validate.checks-15-18.test.js +359 -0
- package/.claude/scripts/tests/wb-validate.checks-19-21.test.js +362 -0
- package/.claude/scripts/tests/wb-validate.checks-22-23-report.test.js +297 -0
- package/.claude/scripts/tests/wb-validate.checks-8-10.test.js +243 -0
- package/.claude/scripts/wb-render.js +309 -0
- package/.claude/scripts/wb-validate.js +1007 -0
- package/.claude/workflows/pm-phase1.js +22 -13
- package/.claude/workflows/pm-phase2.js +304 -32
- package/.claude/workflows/pm-phase3.js +57 -21
- package/CHANGELOG.md +82 -0
- package/bin/cli.js +1 -0
- package/package.json +1 -1
|
@@ -13,18 +13,30 @@ export const meta = {
|
|
|
13
13
|
|
|
14
14
|
async function appendLedgerEntry(featureDir, prefix, entry) {
|
|
15
15
|
const ledgerPath = `${featureDir}/${prefix}-token-ledger.json`
|
|
16
|
-
|
|
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 })
|
|
17
18
|
await agent(
|
|
18
|
-
`Append a JSON object to the ledger array at: ${ledgerPath}\n\
|
|
19
|
+
`Append a JSON object to the ledger array at: ${ledgerPath}\n\n` +
|
|
20
|
+
`1. Run: date -u +"%Y-%m-%dT%H:%M:%SZ" and capture the output as NOW.\n` +
|
|
21
|
+
`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` +
|
|
23
|
+
`4. Write the full array back (JSON, 2-space indent). Return no output.`,
|
|
19
24
|
{ label: 'append-ledger', phase: 'Requirements', model: 'haiku' }
|
|
20
25
|
)
|
|
21
26
|
}
|
|
22
27
|
|
|
23
28
|
async function updateLedgerEntry(featureDir, prefix, agentKey, updates) {
|
|
24
29
|
const ledgerPath = `${featureDir}/${prefix}-token-ledger.json`
|
|
25
|
-
|
|
30
|
+
// completed_at is generated via Bash inside the agent to get a real UTC timestamp with time
|
|
31
|
+
const updatesWithoutTs = JSON.stringify({ ...updates, completed_at: '__TS__' })
|
|
26
32
|
await agent(
|
|
27
|
-
`Update an entry in the ledger array at: ${ledgerPath}\n\
|
|
33
|
+
`Update an entry in the ledger array at: ${ledgerPath}\n\n` +
|
|
34
|
+
`1. Run: date -u +"%Y-%m-%dT%H:%M:%SZ" and capture the output as NOW.\n` +
|
|
35
|
+
`2. Read the file. If it does not exist or cannot be parsed as a JSON array, do nothing.\n` +
|
|
36
|
+
`3. Search from the end for the last entry where agent === "${agentKey}".\n` +
|
|
37
|
+
`4. If found, merge these fields into that entry, replacing "__TS__" in completed_at with NOW: ${updatesWithoutTs}\n` +
|
|
38
|
+
`5. Write the full array back (JSON, 2-space indent). Return no output.\n` +
|
|
39
|
+
`6. If not found, do nothing.`,
|
|
28
40
|
{ label: 'update-ledger', phase: 'Requirements', model: 'haiku' }
|
|
29
41
|
)
|
|
30
42
|
}
|
|
@@ -108,14 +120,13 @@ const errors = []
|
|
|
108
120
|
|
|
109
121
|
if (discoveryResult.needs_requirements) {
|
|
110
122
|
const reqKey = 'generate-requirements:phase1'
|
|
111
|
-
const reqStartedAt = new Date().toISOString()
|
|
112
123
|
await appendLedgerEntry(feature_dir, prefix, {
|
|
113
124
|
agent: reqKey,
|
|
114
125
|
phase: 'phase1',
|
|
115
126
|
model: 'haiku',
|
|
116
127
|
status: 'running',
|
|
117
128
|
phase_delta_tokens: 0,
|
|
118
|
-
started_at:
|
|
129
|
+
started_at: '__TS__',
|
|
119
130
|
completed_at: null,
|
|
120
131
|
})
|
|
121
132
|
const beforeReq = budget.spent()
|
|
@@ -123,7 +134,7 @@ if (discoveryResult.needs_requirements) {
|
|
|
123
134
|
const reqTokens = budget.spent() - beforeReq
|
|
124
135
|
await updateLedgerEntry(feature_dir, prefix, reqKey, {
|
|
125
136
|
status: 'done',
|
|
126
|
-
completed_at:
|
|
137
|
+
completed_at: '__TS__',
|
|
127
138
|
phase_delta_tokens: reqTokens,
|
|
128
139
|
})
|
|
129
140
|
tokenLedger.push({ agent: 'generate-requirements', model: 'haiku', phase_delta_tokens: reqTokens })
|
|
@@ -137,14 +148,13 @@ phase('Tech-Spec')
|
|
|
137
148
|
|
|
138
149
|
if (discoveryResult.needs_tech_spec) {
|
|
139
150
|
const specKey = 'generate-tech-spec:phase1'
|
|
140
|
-
const specStartedAt = new Date().toISOString()
|
|
141
151
|
await appendLedgerEntry(feature_dir, prefix, {
|
|
142
152
|
agent: specKey,
|
|
143
153
|
phase: 'phase1',
|
|
144
154
|
model: 'haiku',
|
|
145
155
|
status: 'running',
|
|
146
156
|
phase_delta_tokens: 0,
|
|
147
|
-
started_at:
|
|
157
|
+
started_at: '__TS__',
|
|
148
158
|
completed_at: null,
|
|
149
159
|
})
|
|
150
160
|
const beforeSpec = budget.spent()
|
|
@@ -152,7 +162,7 @@ if (discoveryResult.needs_tech_spec) {
|
|
|
152
162
|
const specTokens = budget.spent() - beforeSpec
|
|
153
163
|
await updateLedgerEntry(feature_dir, prefix, specKey, {
|
|
154
164
|
status: 'done',
|
|
155
|
-
completed_at:
|
|
165
|
+
completed_at: '__TS__',
|
|
156
166
|
phase_delta_tokens: specTokens,
|
|
157
167
|
})
|
|
158
168
|
tokenLedger.push({ agent: 'generate-tech-spec', model: 'haiku', phase_delta_tokens: specTokens })
|
|
@@ -171,14 +181,13 @@ const MAX_CYCLES = 3
|
|
|
171
181
|
for (let cycle = 1; cycle <= MAX_CYCLES; cycle++) {
|
|
172
182
|
log(`validate-feature-docs cycle ${cycle}`)
|
|
173
183
|
const valKey = `validate-feature-docs:phase1:cycle${cycle}`
|
|
174
|
-
const valStartedAt = new Date().toISOString()
|
|
175
184
|
await appendLedgerEntry(feature_dir, prefix, {
|
|
176
185
|
agent: valKey,
|
|
177
186
|
phase: 'phase1',
|
|
178
187
|
model: 'haiku',
|
|
179
188
|
status: 'running',
|
|
180
189
|
phase_delta_tokens: 0,
|
|
181
|
-
started_at:
|
|
190
|
+
started_at: '__TS__',
|
|
182
191
|
completed_at: null,
|
|
183
192
|
})
|
|
184
193
|
const beforeVal = budget.spent()
|
|
@@ -186,7 +195,7 @@ for (let cycle = 1; cycle <= MAX_CYCLES; cycle++) {
|
|
|
186
195
|
const valTokens = budget.spent() - beforeVal
|
|
187
196
|
await updateLedgerEntry(feature_dir, prefix, valKey, {
|
|
188
197
|
status: 'done',
|
|
189
|
-
completed_at:
|
|
198
|
+
completed_at: '__TS__',
|
|
190
199
|
phase_delta_tokens: valTokens,
|
|
191
200
|
})
|
|
192
201
|
tokenLedger.push({ agent: `validate-feature-docs (cycle ${cycle})`, model: 'haiku', phase_delta_tokens: valTokens })
|
|
@@ -13,22 +13,78 @@ export const meta = {
|
|
|
13
13
|
|
|
14
14
|
async function appendLedgerEntry(featureDir, prefix, entry) {
|
|
15
15
|
const ledgerPath = `${featureDir}/${prefix}-token-ledger.json`
|
|
16
|
-
|
|
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 })
|
|
17
20
|
await agent(
|
|
18
|
-
`Append a JSON object to the ledger array at: ${ledgerPath}\n\
|
|
21
|
+
`Append a JSON object to the ledger array at: ${ledgerPath}\n\n` +
|
|
22
|
+
`1. Run: date -u +"%Y-%m-%dT%H:%M:%SZ" and capture the output as NOW.\n` +
|
|
23
|
+
`2. Read the file. If it does not exist or cannot be parsed as a JSON array, start with [].\n` +
|
|
24
|
+
`3. Push this object onto the array, replacing every "__TS__" string value in the object with NOW: ${entryWithoutTs}\n` +
|
|
25
|
+
`4. Write the full array back (JSON, 2-space indent). Return no output.`,
|
|
19
26
|
{ label: 'append-ledger', phase: 'Work Breakdown', model: 'haiku' }
|
|
20
27
|
)
|
|
21
28
|
}
|
|
22
29
|
|
|
23
30
|
async function updateLedgerEntry(featureDir, prefix, agentKey, updates) {
|
|
24
31
|
const ledgerPath = `${featureDir}/${prefix}-token-ledger.json`
|
|
25
|
-
|
|
32
|
+
// completed_at is generated via Bash inside the agent to get a real UTC timestamp with time
|
|
33
|
+
const updatesWithoutTs = JSON.stringify({ ...updates, completed_at: '__TS__' })
|
|
26
34
|
await agent(
|
|
27
|
-
`Update an entry in the ledger array at: ${ledgerPath}\n\
|
|
35
|
+
`Update an entry in the ledger array at: ${ledgerPath}\n\n` +
|
|
36
|
+
`1. Run: date -u +"%Y-%m-%dT%H:%M:%SZ" and capture the output as NOW.\n` +
|
|
37
|
+
`2. Read the file. If it does not exist or cannot be parsed as a JSON array, do nothing.\n` +
|
|
38
|
+
`3. Search from the end for the last entry where agent === "${agentKey}".\n` +
|
|
39
|
+
`4. If found, merge these fields into that entry, replacing "__TS__" in completed_at with NOW: ${updatesWithoutTs}\n` +
|
|
40
|
+
`5. Write the full array back (JSON, 2-space indent). Return no output.\n` +
|
|
41
|
+
`6. If not found, do nothing.`,
|
|
28
42
|
{ label: 'update-ledger', phase: 'Work Breakdown', model: 'haiku' }
|
|
29
43
|
)
|
|
30
44
|
}
|
|
31
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
|
+
|
|
32
88
|
// ── Parse args ────────────────────────────────────────────────────────────────
|
|
33
89
|
|
|
34
90
|
// args: "<path-to-feature.md>"
|
|
@@ -45,14 +101,13 @@ phase('Work Breakdown')
|
|
|
45
101
|
const tokenLedger = []
|
|
46
102
|
|
|
47
103
|
log(`Running generate-work-breakdown for ${featurePath}`)
|
|
48
|
-
const wbStartedAt = new Date().toISOString()
|
|
49
104
|
await appendLedgerEntry(featureDir, prefix, {
|
|
50
105
|
agent: 'generate-work-breakdown:phase2',
|
|
51
106
|
phase: 'phase2',
|
|
52
107
|
model: 'haiku',
|
|
53
108
|
status: 'running',
|
|
54
109
|
phase_delta_tokens: 0,
|
|
55
|
-
started_at:
|
|
110
|
+
started_at: '__TS__',
|
|
56
111
|
completed_at: null,
|
|
57
112
|
})
|
|
58
113
|
const beforeWB = budget.spent()
|
|
@@ -64,12 +119,231 @@ await agent(featurePath, {
|
|
|
64
119
|
const wbTokens = budget.spent() - beforeWB
|
|
65
120
|
await updateLedgerEntry(featureDir, prefix, 'generate-work-breakdown:phase2', {
|
|
66
121
|
status: 'done',
|
|
67
|
-
completed_at:
|
|
122
|
+
completed_at: '__TS__',
|
|
68
123
|
phase_delta_tokens: wbTokens,
|
|
69
124
|
})
|
|
70
125
|
tokenLedger.push({ agent: 'generate-work-breakdown', model: 'haiku', phase_delta_tokens: wbTokens })
|
|
71
126
|
log(`generate-work-breakdown done — phase delta: ${wbTokens} tokens`)
|
|
72
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
|
+
|
|
73
347
|
// ── Parse WB + write Effort-Estimate + Token-Estimate ────────────────────────
|
|
74
348
|
phase('Effort Estimate')
|
|
75
349
|
|
|
@@ -124,30 +398,27 @@ function formatEur(val) {
|
|
|
124
398
|
const metrics = await agent(
|
|
125
399
|
`You have four tasks:
|
|
126
400
|
|
|
127
|
-
TASK 1 — Read the Work Breakdown file.
|
|
401
|
+
TASK 1 — Read the Work Breakdown JSON file.
|
|
128
402
|
The feature.md is at: ${featurePath}
|
|
129
|
-
The
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
-
|
|
135
|
-
-
|
|
136
|
-
-
|
|
137
|
-
-
|
|
138
|
-
-
|
|
139
|
-
-
|
|
140
|
-
-
|
|
141
|
-
|
|
142
|
-
-
|
|
143
|
-
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
-
|
|
147
|
-
-
|
|
148
|
-
- task count
|
|
149
|
-
- domains involved
|
|
150
|
-
- 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)
|
|
151
422
|
|
|
152
423
|
TASK 2 — Write the Effort-Estimate.md file.
|
|
153
424
|
Write {feature_dir}/{PREFIX}-Effort-Estimate.md with this format:
|
|
@@ -234,7 +505,7 @@ For the "Est. cost €" column in Phase 3: use the formula tokens * (0.8 * 3.00
|
|
|
234
505
|
TASK 4 — Set return values.
|
|
235
506
|
Set effort_estimate_path to the full path of the written Effort-Estimate.md.
|
|
236
507
|
Set token_estimate_path to the full path of the written Token-Estimate.md.
|
|
237
|
-
Set tasks_csv_path to
|
|
508
|
+
Set tasks_csv_path to "${featureDir}/${prefix}-Work-Breakdown.csv" (written by wb-render in the same directory as feature.md).
|
|
238
509
|
|
|
239
510
|
Return the extracted metrics as structured output.`,
|
|
240
511
|
{
|
|
@@ -276,7 +547,7 @@ ${phase2Events}`,
|
|
|
276
547
|
)
|
|
277
548
|
|
|
278
549
|
return {
|
|
279
|
-
prefix: metrics.prefix,
|
|
550
|
+
prefix: metrics.prefix || prefix,
|
|
280
551
|
feature_path: featurePath,
|
|
281
552
|
user_stories: metrics.user_stories,
|
|
282
553
|
total_tasks: metrics.total_tasks,
|
|
@@ -290,4 +561,5 @@ return {
|
|
|
290
561
|
tasks_csv_path: metrics.tasks_csv_path || '',
|
|
291
562
|
token_ledger: tokenLedger,
|
|
292
563
|
errors: [],
|
|
564
|
+
gate2_payload,
|
|
293
565
|
}
|