@dtlabs/ai-toolkit 0.17.4 → 0.18.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +30 -0
- package/lib/agent-registry.js +39 -0
- package/lib/workflow-control.js +65 -13
- package/package.json +1 -1
- package/src/claude/agents/gaia-generate-requirements-use-case.md +97 -0
- package/src/claude/agents/gaia-generate-requirements.md +22 -27
- package/src/claude/agents/gaia-generate-tech-spec-component.md +93 -0
- package/src/claude/agents/gaia-generate-tech-spec.md +47 -103
- package/src/claude/agents/gaia-generate-work-breakdown-task.md +151 -0
- package/src/claude/agents/gaia-generate-work-breakdown.md +94 -100
- package/src/claude/agents/gaia-validate-feature-docs.md +13 -8
- package/src/claude/commands/assemble-docs.md +16 -0
- package/src/claude/commands/feature-status.md +9 -4
- package/src/claude/scripts/ac-task-matrix.js +87 -0
- package/src/claude/scripts/assemble-docs.js +66 -0
- package/src/claude/scripts/req-assembler.js +127 -0
- package/src/claude/scripts/req-graph-checks.js +71 -0
- package/src/claude/scripts/req-skeleton-check.js +64 -0
- package/src/claude/scripts/tech-spec-assembler.js +188 -0
- package/src/claude/scripts/tech-spec-graph-checks.js +84 -0
- package/src/claude/scripts/templates/gaia-component-template.md +64 -0
- package/src/claude/scripts/templates/gaia-dependency-graph-template.md +85 -0
- package/src/claude/scripts/templates/gaia-requirements-index-template.md +144 -0
- package/src/claude/scripts/templates/gaia-task-template.md +92 -0
- package/src/claude/scripts/templates/gaia-tech-spec-index-template.md +137 -0
- package/src/claude/scripts/templates/gaia-use-case-template.md +56 -0
- package/src/claude/scripts/templates/gaia-user-story-template.md +61 -0
- package/src/claude/scripts/wb-graph-checks.js +754 -0
- package/src/claude/scripts/wb-render.js +6 -11
- package/src/claude/scripts/wb-skeleton-check.js +99 -0
- package/src/claude/scripts/wb-validate.js +49 -655
- package/src/claude/scripts/wbs-assembler.js +368 -0
- package/src/claude/skills/implement-feature/SKILL.md +12 -6
- package/src/claude/workflows/pm-phase1.js +342 -16
- package/src/claude/workflows/pm-phase2.js +277 -14
|
@@ -5,12 +5,212 @@ export const meta = {
|
|
|
5
5
|
phases: [{ title: 'Work Breakdown' }, { title: 'Effort Estimate' }],
|
|
6
6
|
}
|
|
7
7
|
|
|
8
|
+
// ── Skeleton JSON schema (FTR-020) ───────────────────────────────────────────
|
|
9
|
+
// Passed as opts.schema to the skeleton-generation worker call. See
|
|
10
|
+
// src/claude/agents/gaia-generate-work-breakdown.md Step 7 for the authoritative
|
|
11
|
+
// description of this shape.
|
|
12
|
+
const SKELETON_SCHEMA = {
|
|
13
|
+
type: 'object',
|
|
14
|
+
required: ['schemaVersion', 'feature', 'phases', 'tasks'],
|
|
15
|
+
properties: {
|
|
16
|
+
schemaVersion: { const: 3 },
|
|
17
|
+
feature: { type: 'string' },
|
|
18
|
+
phases: { type: 'array', items: {
|
|
19
|
+
type: 'object',
|
|
20
|
+
required: ['id', 'type', 'title', 'commit', 'sourceUC', 'context', 'taskIds'],
|
|
21
|
+
properties: {
|
|
22
|
+
id: { type: 'string' }, type: { enum: ['infrastructure', 'user-story'] },
|
|
23
|
+
title: { type: 'string' },
|
|
24
|
+
commit: { type: 'object', required: ['type', 'subject'],
|
|
25
|
+
properties: { type: { type: 'string' }, subject: { type: 'string' } } },
|
|
26
|
+
sourceUC: { type: 'array', items: { type: 'string' } },
|
|
27
|
+
// 2-4 sentences: why this phase exists, how its tasks relate, any sequencing note other
|
|
28
|
+
// phases/tasks need before opening this phase's own task files — the one part of the
|
|
29
|
+
// FTR-020 phase-detail template that genuinely needs narrative judgment, not something
|
|
30
|
+
// the host can derive mechanically the way it does Source UC/Tasks-in-phase/Depends-on.
|
|
31
|
+
context: { type: 'string' },
|
|
32
|
+
taskIds: { type: 'array', items: { type: 'string' } },
|
|
33
|
+
},
|
|
34
|
+
} },
|
|
35
|
+
tasks: { type: 'object', additionalProperties: {
|
|
36
|
+
type: 'object',
|
|
37
|
+
required: ['title', 'outcome', 'domain', 'agentType', 'dependsOn',
|
|
38
|
+
'acceptanceCriteria', 'estimate', 'outputCount', 'commit'],
|
|
39
|
+
properties: {
|
|
40
|
+
title: { type: 'string' }, outcome: { type: 'string' },
|
|
41
|
+
domain: { enum: ['BE', 'FE', 'DB', 'DevOps', 'INFRA', 'TEST'] },
|
|
42
|
+
// Bracket-key form is deliberate — this is a JSON-Schema PROPERTY NAME describing a
|
|
43
|
+
// task's own field, not a dispatch-mechanism option key; written this way so the
|
|
44
|
+
// literal text doesn't collide with tests/cli/pm-phase2-source.test.js's source-text
|
|
45
|
+
// guard against this script ever using a non-c.worker dispatch mechanism.
|
|
46
|
+
['agentType']: { enum: ['developer-backend', 'developer-frontend', 'developer-testing', 'review-solution'] },
|
|
47
|
+
dependsOn: { type: 'array', items: { type: 'string' } },
|
|
48
|
+
acceptanceCriteria: { type: 'array', items: { type: 'string' } },
|
|
49
|
+
estimate: { type: 'object', required: ['agentMinutes', 'tokens'],
|
|
50
|
+
properties: { agentMinutes: { type: 'integer' }, tokens: { type: 'integer' } } },
|
|
51
|
+
outputCount: { type: 'integer' },
|
|
52
|
+
commit: { type: 'object', required: ['type', 'subject'],
|
|
53
|
+
properties: { type: { type: 'string' }, subject: { type: 'string' } } },
|
|
54
|
+
},
|
|
55
|
+
} },
|
|
56
|
+
},
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Splits the skeleton response into (a) index.json's structural, map-keyed shape and
|
|
60
|
+
// (b) nothing else — phase title/commit/context are NOT copied into index.json (OQ-04); they
|
|
61
|
+
// are rendered straight to each phase's detail file by the caller, from the same skeleton
|
|
62
|
+
// response, since phase content is fully known the moment this one call returns.
|
|
63
|
+
function materializeIndex(skeleton) {
|
|
64
|
+
const tasks = {}
|
|
65
|
+
for (const [taskId, t] of Object.entries(skeleton.tasks)) {
|
|
66
|
+
tasks[taskId] = { ...t, detailFile: `tasks/${taskId}.md`, status: 'pending' }
|
|
67
|
+
}
|
|
68
|
+
const phases = skeleton.phases.map(p => ({
|
|
69
|
+
id: p.id, type: p.type, sourceUC: p.sourceUC, detailFile: `user-stories/${p.id}.md`, taskIds: p.taskIds,
|
|
70
|
+
}))
|
|
71
|
+
return { schemaVersion: 3, feature: skeleton.feature, phases, tasks }
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Duplicated from src/claude/scripts/wbs-assembler.js's renderPhaseDetailBlock/template-engine
|
|
75
|
+
// primitives — NOT required from it, since this workflow script's runtime has no `require` (see
|
|
76
|
+
// lib/workflow-runner.js: only `control`/`args` are injected); reads the template via `c.read()`
|
|
77
|
+
// (project-root-relative) instead of `fs.readFileSync(__dirname, ...)`, the one difference the
|
|
78
|
+
// sandboxed runtime forces. The fragile inverse (parsing, with the contiguous-run algorithm)
|
|
79
|
+
// stays exclusively in wbs-assembler.js, used only by wb-validate.js/wb-render.js. Keep both
|
|
80
|
+
// copies' FIELD/SECTION handling in sync if gaia-user-story-template.md's shape ever changes.
|
|
81
|
+
const FIELD_LINE_RE = /^- \*\*(.+?):\*\*[ \t]?(.*)$/
|
|
82
|
+
function _stripTemplateComments(text) {
|
|
83
|
+
return text.replace(/<!--[\s\S]*?-->/g, '')
|
|
84
|
+
}
|
|
85
|
+
function _fillFieldBlock(templateText, values) {
|
|
86
|
+
const lines = templateText.split('\n')
|
|
87
|
+
let start = 0
|
|
88
|
+
while (start < lines.length && !FIELD_LINE_RE.test(lines[start])) start++
|
|
89
|
+
const starts = []
|
|
90
|
+
let end = start
|
|
91
|
+
for (let j = start; j < lines.length; j++) {
|
|
92
|
+
const m = lines[j].match(FIELD_LINE_RE)
|
|
93
|
+
if (m) starts.push({ label: m[1] })
|
|
94
|
+
else if (lines[j].trim() === '') { end = j; break }
|
|
95
|
+
end = j + 1
|
|
96
|
+
}
|
|
97
|
+
const seen = new Set()
|
|
98
|
+
const newFieldLines = starts.map(({ label }) => {
|
|
99
|
+
if (!(label in values)) throw new Error(`Template field "${label}" has no supplied value`)
|
|
100
|
+
seen.add(label)
|
|
101
|
+
return `- **${label}:** ${values[label]}`
|
|
102
|
+
})
|
|
103
|
+
for (const label of Object.keys(values)) {
|
|
104
|
+
if (!seen.has(label)) throw new Error(`Supplied value for "${label}" matches no template field`)
|
|
105
|
+
}
|
|
106
|
+
return [...lines.slice(0, start), ...newFieldLines, ...lines.slice(end)].join('\n')
|
|
107
|
+
}
|
|
108
|
+
function _fillNamedSection(templateText, sectionName, content) {
|
|
109
|
+
const marker = `**${sectionName}:**`
|
|
110
|
+
const idx = templateText.indexOf(marker)
|
|
111
|
+
if (idx === -1) throw new Error(`Template is missing the "${sectionName}" section`)
|
|
112
|
+
const afterMarker = idx + marker.length
|
|
113
|
+
const nextMarkerIdx = templateText.indexOf('\n**', afterMarker)
|
|
114
|
+
const end = nextMarkerIdx === -1 ? templateText.length : nextMarkerIdx
|
|
115
|
+
return templateText.slice(0, afterMarker) + '\n\n' + content + '\n' + templateText.slice(end)
|
|
116
|
+
}
|
|
117
|
+
function _fillInlineBoldField(templateText, label, value) {
|
|
118
|
+
const lines = templateText.split('\n')
|
|
119
|
+
const marker = `**${label}:**`
|
|
120
|
+
const startIdx = lines.findIndex(l => l.includes(marker))
|
|
121
|
+
if (startIdx === -1) throw new Error(`Template is missing the "${label}" inline field`)
|
|
122
|
+
let end = startIdx + 1
|
|
123
|
+
while (end < lines.length && lines[end].trim() !== '') end++
|
|
124
|
+
return [...lines.slice(0, startIdx), `${marker} ${value}`, ...lines.slice(end)].join('\n')
|
|
125
|
+
}
|
|
126
|
+
function renderPhaseDetailBlock(phase, taskTitles, dependsOnPhases) {
|
|
127
|
+
const DASH = '—'
|
|
128
|
+
const sourceUC = Array.isArray(phase.sourceUC) && phase.sourceUC.length ? phase.sourceUC.join(', ') : DASH
|
|
129
|
+
const tasksBlock = taskTitles.length
|
|
130
|
+
? taskTitles.map(t => `- \`${t.id}\` — ${t.title}`).join('\n')
|
|
131
|
+
: '_No tasks._'
|
|
132
|
+
const dependsOn = dependsOnPhases.length ? dependsOnPhases.join(', ') : 'none'
|
|
133
|
+
let text = _stripTemplateComments(c.read('.claude/scripts/templates/gaia-user-story-template.md'))
|
|
134
|
+
text = text.replace(/\{PHASE_ID\}/g, phase.id)
|
|
135
|
+
text = _fillFieldBlock(text, {
|
|
136
|
+
'Phase ID': phase.id,
|
|
137
|
+
'Type': phase.type,
|
|
138
|
+
'Source UC': sourceUC,
|
|
139
|
+
'Title': phase.title,
|
|
140
|
+
'Commit type': phase.commit.type,
|
|
141
|
+
'Commit scope': phase.commit.scope == null ? DASH : phase.commit.scope,
|
|
142
|
+
'Commit subject': phase.commit.subject,
|
|
143
|
+
})
|
|
144
|
+
text = _fillNamedSection(text, 'Context', phase.context == null ? DASH : phase.context)
|
|
145
|
+
text = _fillNamedSection(text, 'Tasks in this phase', tasksBlock)
|
|
146
|
+
text = _fillInlineBoldField(text, 'Depends on phases', dependsOn)
|
|
147
|
+
return text.trim() + '\n'
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Depends-on-phases is 100% mechanically derivable from the skeleton's own task dependsOn
|
|
151
|
+
// graph (same information checkPhaseDependencyAndCycle computes in wb-graph-checks.js, just
|
|
152
|
+
// recomputed here since that function needs the post-assembly {phases:[{tasks:[...]}]} shape,
|
|
153
|
+
// not the skeleton's map-of-tasks-by-id) — no LLM judgment needed, unlike Context.
|
|
154
|
+
function computeDependsOnPhases(skeleton, phaseId, taskIdToPhase) {
|
|
155
|
+
const phase = skeleton.phases.find(p => p.id === phaseId)
|
|
156
|
+
const deps = new Set()
|
|
157
|
+
for (const taskId of phase.taskIds) {
|
|
158
|
+
const task = skeleton.tasks[taskId]
|
|
159
|
+
for (const depId of (task.dependsOn || [])) {
|
|
160
|
+
const depPhase = taskIdToPhase.get(depId)
|
|
161
|
+
if (depPhase && depPhase !== phaseId) deps.add(depPhase)
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
return [...deps]
|
|
165
|
+
}
|
|
8
166
|
|
|
167
|
+
// Compact prompt for the per-task detail-generation worker: this task's own skeleton entry,
|
|
168
|
+
// the full skeleton's sibling task list (id + title + outcome only, for duplicate-
|
|
169
|
+
// implementation awareness), and the Tech-Spec text (the per-task agent further narrows its
|
|
170
|
+
// own Read/Grep use as needed — this call does not pre-slice the Tech-Spec by domain).
|
|
171
|
+
function buildTaskPrompt(index, taskId, techSpecText) {
|
|
172
|
+
const task = index.tasks[taskId]
|
|
173
|
+
const siblingList = Object.entries(index.tasks)
|
|
174
|
+
.map(([id, t]) => `- ${id}: ${t.title} — ${t.outcome}`).join('\n')
|
|
175
|
+
return [
|
|
176
|
+
`Task skeleton entry (restate these fields verbatim; do not re-derive them):\n${JSON.stringify({ id: taskId, ...task }, null, 2)}`,
|
|
177
|
+
`Tech-Spec:\n${techSpecText}`,
|
|
178
|
+
`All tasks in this feature (for duplicate-implementation awareness):\n${siblingList}`,
|
|
179
|
+
].join('\n\n')
|
|
180
|
+
}
|
|
9
181
|
|
|
10
182
|
const c = control
|
|
11
183
|
const { featurePath, featureDir, prefix } = c.feature(args[0])
|
|
12
184
|
return c.run(meta.name, featureDir, prefix, async () => {
|
|
13
185
|
const file = suffix => `${featureDir}/${prefix}-${suffix}`
|
|
186
|
+
|
|
187
|
+
// FTR-025: pm-phase1.js no longer writes the assembled Requirements/Tech-Spec files — this
|
|
188
|
+
// phase's own unmodified internals (the Gate 1 digest re-check right below, wb-skeleton-
|
|
189
|
+
// check.js, wb-validate.js, the semantic validator prompt, ac-task-matrix.js, and
|
|
190
|
+
// gaia-generate-work-breakdown, which hard-aborts without these files) still need them on
|
|
191
|
+
// disk. Transparently bridge the gap: assemble fresh from req/+spec/ every run (same two
|
|
192
|
+
// subprocess calls pm-phase1.js used to make, relocated here), before anything below reads
|
|
193
|
+
// them. A legacy feature (monolithic files only, no req/spec/ directories — generated before
|
|
194
|
+
// this feature existed) is left untouched: nothing to assemble, no error.
|
|
195
|
+
await c.activity('bridge-requirements-tech-spec', async () => {
|
|
196
|
+
const reqDir = `${featureDir}/req`, specDir = `${featureDir}/spec`
|
|
197
|
+
// c.exists() only checks isFile() — never true for a directory — so this checks each
|
|
198
|
+
// split's own index.md (a real file) rather than the directory itself.
|
|
199
|
+
const hasReq = c.exists(`${reqDir}/index.md`), hasSpec = c.exists(`${specDir}/index.md`)
|
|
200
|
+
if (!hasReq && !hasSpec) return { bridged: false, reason: 'legacy (no split sources)' }
|
|
201
|
+
if (hasReq) {
|
|
202
|
+
const assembledRequirements = c.command(['run-asset', 'scripts/req-assembler.js',
|
|
203
|
+
'--project', c.root, '--', reqDir]).stdout
|
|
204
|
+
c.writeOutput(file('Requirements.md'), assembledRequirements)
|
|
205
|
+
}
|
|
206
|
+
if (hasSpec) {
|
|
207
|
+
const assembledTechSpec = c.command(['run-asset', 'scripts/tech-spec-assembler.js',
|
|
208
|
+
'--project', c.root, '--', specDir]).stdout
|
|
209
|
+
c.writeOutput(file('Tech-Spec.md'), assembledTechSpec)
|
|
210
|
+
}
|
|
211
|
+
return { bridged: true }
|
|
212
|
+
})
|
|
213
|
+
|
|
14
214
|
const approvals = c.read(file('Approvals.md'))
|
|
15
215
|
const gate = approvals.match(/^## Gate 1\b[^\n]*\n([\s\S]*?)(?=^## |$(?![\s\S]))/m)
|
|
16
216
|
if (!gate || !/✅\s*Approved/.test(gate[1])) throw new Error('Gate 1 approval evidence missing')
|
|
@@ -30,13 +230,62 @@ return c.run(meta.name, featureDir, prefix, async () => {
|
|
|
30
230
|
throw new Error('Gate 1 approval is stale: Requirements/Tech-Spec content no longer matches what was approved (docs digest mismatch) — re-approve Gate 1 before proceeding')
|
|
31
231
|
}
|
|
32
232
|
}
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
233
|
+
|
|
234
|
+
// Deliberately NOT file('wbs') — file() always prepends '{prefix}-' (every other
|
|
235
|
+
// artifact, e.g. file('Requirements.md') -> '{prefix}-Requirements.md', is a single file
|
|
236
|
+
// named after its own content, so the prefix disambiguates it; this is a directory nested
|
|
237
|
+
// inside the feature's own already-prefixed folder, so repeating the prefix is redundant.
|
|
238
|
+
const wbsDir = `${featureDir}/wbs`
|
|
239
|
+
const indexPath = `${wbsDir}/index.json`
|
|
240
|
+
|
|
241
|
+
// 1. Skeleton (no declared worker() output — host writes every file itself)
|
|
242
|
+
let index
|
|
243
|
+
if (!c.exists(indexPath)) {
|
|
244
|
+
const skeleton = await c.worker('gaia.agent.planner.work-breakdown', featurePath, { schema: SKELETON_SCHEMA })
|
|
245
|
+
index = materializeIndex(skeleton)
|
|
246
|
+
const taskIdToPhase = new Map(skeleton.phases.flatMap(p => p.taskIds.map(taskId => [taskId, p.id])))
|
|
247
|
+
for (const phase of index.phases) {
|
|
248
|
+
const skeletonPhase = skeleton.phases.find(p => p.id === phase.id)
|
|
249
|
+
const taskTitles = skeletonPhase.taskIds.map(taskId => ({ id: taskId, title: skeleton.tasks[taskId].title }))
|
|
250
|
+
const dependsOnPhases = computeDependsOnPhases(skeleton, phase.id, taskIdToPhase)
|
|
251
|
+
c.writeOutput(`${wbsDir}/${phase.detailFile}`, renderPhaseDetailBlock(skeletonPhase, taskTitles, dependsOnPhases))
|
|
252
|
+
}
|
|
253
|
+
c.writeOutput(indexPath, JSON.stringify(index, null, 2))
|
|
254
|
+
} else {
|
|
255
|
+
index = c.json(indexPath) // resume: reuse existing skeleton as-is
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// 2. Structural pre-checks (wb-skeleton-check.js, same wb-graph-checks.js functions
|
|
259
|
+
// wb-validate.js now imports — run via subprocess since this runtime has no `require`)
|
|
260
|
+
const skeletonCheck = await c.activity('wb-skeleton-check', async () => {
|
|
261
|
+
const res = c.command(['run-asset', 'scripts/wb-skeleton-check.js', '--project', c.root,
|
|
262
|
+
'--', indexPath, file('Requirements.md')], [0, 1])
|
|
263
|
+
const parsed = JSON.parse(res.stdout)
|
|
264
|
+
if (typeof parsed.valid !== 'boolean' || !Array.isArray(parsed.errors) ||
|
|
265
|
+
(res.exitCode === 0) !== parsed.valid) throw new Error('Invalid skeleton-check result')
|
|
266
|
+
if (!parsed.valid) throw new Error('Work breakdown skeleton failed structural checks: ' + res.stdout)
|
|
267
|
+
return parsed
|
|
36
268
|
})
|
|
269
|
+
|
|
270
|
+
// 3. Per-task generation, resume-by-status, sequential
|
|
271
|
+
for (const phase of index.phases) {
|
|
272
|
+
for (const taskId of phase.taskIds) {
|
|
273
|
+
const task = index.tasks[taskId]
|
|
274
|
+
const detailPath = `${wbsDir}/${task.detailFile}`
|
|
275
|
+
if (task.status === 'generated' && c.exists(detailPath)) continue // resume: skip, no dispatch
|
|
276
|
+
const detail = await c.worker('gaia.agent.planner.work-breakdown-task',
|
|
277
|
+
buildTaskPrompt(index, taskId, c.read(file('Tech-Spec.md'))), { label: `work-breakdown-task:${taskId}` })
|
|
278
|
+
c.writeOutput(detailPath, detail)
|
|
279
|
+
task.status = 'generated'
|
|
280
|
+
c.writeOutput(indexPath, JSON.stringify(index, null, 2)) // persist status after each task
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// 4. Assembly (implicit, via the now directory-aware wb-validate.js/wb-render.js) +
|
|
285
|
+
// existing pipeline, unchanged past this point except the path passed in
|
|
37
286
|
const report = await c.activity('wb-validate', async () => {
|
|
38
287
|
const res = c.command(['run-asset', 'scripts/wb-validate.js', '--project', c.root,
|
|
39
|
-
'--',
|
|
288
|
+
'--', wbsDir, file('Requirements.md')], [0, 1])
|
|
40
289
|
const parsed = JSON.parse(res.stdout)
|
|
41
290
|
if (typeof parsed.valid !== 'boolean' || !Array.isArray(parsed.errors) ||
|
|
42
291
|
!Array.isArray(parsed.warnings) || (res.exitCode === 0) !== parsed.valid) throw new Error('Invalid validator result')
|
|
@@ -44,26 +293,40 @@ return c.run(meta.name, featureDir, prefix, async () => {
|
|
|
44
293
|
return parsed
|
|
45
294
|
})
|
|
46
295
|
const semantic = await c.worker('gaia.agent.planner.validate-work-breakdown',
|
|
47
|
-
`${
|
|
296
|
+
`${wbsDir}\n${file('Requirements.md')}`, { schema: {
|
|
48
297
|
type: 'object', properties: { valid: { type: 'boolean' }, findings: { type: 'array' } }, required: ['valid', 'findings'],
|
|
49
298
|
} })
|
|
50
299
|
if (!semantic.valid || semantic.findings.some(f => f.blocking)) throw new Error('Work breakdown semantic validation failed')
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
300
|
+
// FTR-025: wb-render.js is no longer called automatically here — nothing downstream ever read
|
|
301
|
+
// {PREFIX}-Work-Breakdown.md/.csv (every real consumer already reads wbs/ directly via
|
|
302
|
+
// wbs-assembler.js's loadWorkBreakdown()), so this view is now produced only on demand, via
|
|
303
|
+
// assemble-docs.js.
|
|
304
|
+
// FTR-022: a new deterministic rendering step — no new data, no new validation, purely a
|
|
305
|
+
// view over AC<->task coverage wb-validate.js's checks 19-21 already enforce. Cannot fail
|
|
306
|
+
// for a feature that already reached this point in the pipeline (BR-02/NFR-03).
|
|
307
|
+
const acTaskMatrix = await c.activity('ac-task-matrix', async () => {
|
|
308
|
+
// FTR-023: ac-task-matrix.js's CLI gained a 3rd positional argument (Tech-Spec path) to
|
|
309
|
+
// render the "Addressed by Tech-Spec File(s)" column.
|
|
310
|
+
const result = c.command(['run-asset', 'scripts/ac-task-matrix.js', '--project', c.root,
|
|
311
|
+
'--', wbsDir, file('Requirements.md'), file('Tech-Spec.md'), prefix])
|
|
312
|
+
const matrixPath = file('AC-Task-Matrix.md')
|
|
313
|
+
if (!c.exists(matrixPath)) throw new Error('AC-Task matrix output missing')
|
|
314
|
+
c.read(matrixPath) // never trust an unread write
|
|
315
|
+
return { ...result, matrixPath }
|
|
58
316
|
})
|
|
59
|
-
|
|
317
|
+
// Assembled structure for c.estimates()'s metrics — via wbs-assembler.js's own CLI mode
|
|
318
|
+
// (subprocess), since this runtime cannot `require` it in-process either.
|
|
319
|
+
const assembled = JSON.parse(c.command(['run-asset', 'scripts/wbs-assembler.js', '--project', c.root, '--', wbsDir]).stdout)
|
|
320
|
+
const metrics = await c.estimates(featureDir, prefix, assembled)
|
|
60
321
|
c.append(file('process-log.txt'), `[${new Date().toISOString()}] pm-phase2 complete; Gate 2 approval requested\n`)
|
|
61
322
|
return {
|
|
62
323
|
...metrics, feature_path: featurePath, token_ledger: c.tokenLedger, errors: [],
|
|
63
324
|
gate2_payload: {
|
|
325
|
+
skeleton_check_report: skeletonCheck, skeleton_check_failed: false,
|
|
64
326
|
js_validator_report: report, js_validator_failed: false,
|
|
65
327
|
semantic_validator_result: semantic, semantic_validator_failed: false,
|
|
66
|
-
|
|
328
|
+
gate2_blocked: false,
|
|
329
|
+
ac_task_matrix_path: acTaskMatrix.matrixPath,
|
|
67
330
|
duration_bands: report.durationBands, domain_distribution: report.domainDistribution,
|
|
68
331
|
warning_band_tasks: report.warnings.filter(w => w.category === 'duration_warning').map(w => ({ taskId: w.taskId, agentMinutes: w.details?.agentMinutes })),
|
|
69
332
|
split_required_tasks: [], must_ac_uncovered: [], phase_unschedulable: [],
|