@dtlabs/ai-toolkit 0.18.0 → 0.19.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 +8 -0
- package/package.json +1 -1
- package/src/claude/agents/gaia-generate-work-breakdown-task.md +1 -1
- package/src/claude/agents/gaia-generate-work-breakdown.md +30 -5
- package/src/claude/agents/gaia-validate-work-breakdown-semantic.md +9 -4
- package/src/claude/scripts/ac-task-matrix.js +42 -8
- package/src/claude/workflows/pm-phase2.js +119 -39
package/CHANGELOG.md
CHANGED
|
@@ -1,4 +1,12 @@
|
|
|
1
1
|
## 0.1.4 (2026-07-21)
|
|
2
|
+
## [0.19.0](https://github.com/dtlab-tech/ai-toolkit/compare/v0.18.0...v0.19.0) (2026-10-19)
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
### Features
|
|
6
|
+
|
|
7
|
+
* make the Work Breakdown pipeline split-source-native (FTR-026) ([9d08a17](https://github.com/dtlab-tech/ai-toolkit/commit/9d08a1743e199534753b6c6d47b855ec7953f032))
|
|
8
|
+
* **pm-phase2, ac-task-matrix:** make the Work Breakdown pipeline split-source-native (FTR-026) ([067a8e3](https://github.com/dtlab-tech/ai-toolkit/commit/067a8e369f8a5db72678670bebb62c91c93303d1))
|
|
9
|
+
|
|
2
10
|
## [0.18.0](https://github.com/dtlab-tech/ai-toolkit/compare/v0.17.5...v0.18.0) (2026-10-19)
|
|
3
11
|
|
|
4
12
|
|
package/package.json
CHANGED
|
@@ -23,7 +23,7 @@ This is the **second of two generation passes** (FTR-020). The skeleton pass (`g
|
|
|
23
23
|
The caller supplies, in the prompt (you do not read `{PREFIX}-Work-Breakdown.json`/`wbs/index.json` yourself — the host extracts and hands you exactly what you need):
|
|
24
24
|
|
|
25
25
|
1. **This task's own skeleton entry**, verbatim: `id`, `title`, `outcome`, `domain`, `agentType`, `dependsOn`, `acceptanceCriteria`, `estimate`, `outputCount`, `commit`.
|
|
26
|
-
2. **The relevant Tech-Spec excerpt** for this task's
|
|
26
|
+
2. **The relevant Tech-Spec excerpt** for this task's own Acceptance Criteria (matched by the host against each component's `Addresses` column in `spec/index.md` — not the whole Tech-Spec, which is the whole point of the per-task split). If no component's `Addresses` column cites any of this task's Acceptance Criteria, you instead receive the full Components table from `spec/index.md` as a fallback — in that case, use your own judgment about which listed component(s) are actually relevant, the same way you would if asked to work from the full Tech-Spec directly.
|
|
27
27
|
3. **The compact full-skeleton task list**: every task's `id` + `title` + `outcome` only (no detail), so you can tell whether another task already built the behavior this one would otherwise duplicate.
|
|
28
28
|
|
|
29
29
|
If the prompt is missing any of these three, stop and report exactly what is missing — do not guess at a skeleton entry you were not given.
|
|
@@ -17,12 +17,20 @@ This is the **first of two generation passes** (FTR-020). This call decides the
|
|
|
17
17
|
|
|
18
18
|
The user provides the path to a `feature.md` file. From the same directory, read:
|
|
19
19
|
|
|
20
|
-
1. `
|
|
21
|
-
|
|
20
|
+
1. Requirements — `req/index.md` (the split format, FTR-021; source of User Stories derived from
|
|
21
|
+
Use Cases, acceptance criteria, business rules) if it exists, otherwise the legacy
|
|
22
|
+
`{PREFIX}-Requirements.md`
|
|
23
|
+
2. Tech-Spec — `spec/index.md` (the split format, FTR-024; source of technical tasks,
|
|
24
|
+
implementation order, file inventory, architecture decisions) if it exists, otherwise the
|
|
25
|
+
legacy `{PREFIX}-Tech-Spec.md`
|
|
22
26
|
3. `{PREFIX}-Validation-Report.md` — confirms both documents passed coverage validation
|
|
23
27
|
4. `{PREFIX}-Approvals.md` — confirms user has approved the documents
|
|
24
28
|
|
|
25
|
-
|
|
29
|
+
See Step 1 below for exactly how to read whichever shape (1)/(2) actually are, including per-item
|
|
30
|
+
drilldown into `req/use-cases/*.md`/`spec/components/*.md`.
|
|
31
|
+
|
|
32
|
+
**This agent runs ONLY after validation and human approval.** If Requirements/Tech-Spec exist in
|
|
33
|
+
neither shape, or (3)/(4) are missing, abort and report:
|
|
26
34
|
```
|
|
27
35
|
Cannot generate work breakdown: `{file}` not found.
|
|
28
36
|
The work breakdown requires validated and approved Requirements + Tech-Spec.
|
|
@@ -79,8 +87,25 @@ Every task must be the **smallest independently implementable, verifiable, and c
|
|
|
79
87
|
|
|
80
88
|
### Step 1 — Load and Cross-Reference
|
|
81
89
|
|
|
82
|
-
1.
|
|
83
|
-
|
|
90
|
+
1. Check whether `req/index.md` exists (the split Requirements format, FTR-021). If it does:
|
|
91
|
+
- Read `req/index.md` fully — it already contains the full Use Cases index table, all Business
|
|
92
|
+
Rules, all Acceptance Criteria, and all NFRs (FTR-021 kept these feature-wide, not per-UC)
|
|
93
|
+
- For any UC whose own full flow description you need beyond the index's short Title/Actor
|
|
94
|
+
summary, Read `req/use-cases/{UC-ID}.md` directly
|
|
95
|
+
If `req/index.md` does not exist (a legacy feature), Read `{PREFIX}-Requirements.md` fully
|
|
96
|
+
instead — extract the same information from the monolithic file.
|
|
97
|
+
2. Check whether `spec/index.md` exists (the split Tech-Spec format, FTR-024). If it does:
|
|
98
|
+
- Read `spec/index.md` fully — it already contains Architecture, Section 9's complete File
|
|
99
|
+
Inventory/Components table (with Domain/Addresses routing), Implementation Order, and Risks
|
|
100
|
+
- For any component whose own Backend/Frontend/design detail you need beyond the Components
|
|
101
|
+
table's one-line Purpose/Addresses summary, Read `spec/components/{file-slug}.md` directly —
|
|
102
|
+
the `Spec` column in Section 9's table gives you the exact path
|
|
103
|
+
- If, after reading the index, you cannot determine from its own summary tables which per-item
|
|
104
|
+
files are relevant to a given phase/task decision, fall back to reading every file under
|
|
105
|
+
`spec/components/` rather than proceeding with incomplete context
|
|
106
|
+
If `spec/index.md` does not exist (a legacy feature), Read `{PREFIX}-Tech-Spec.md` fully
|
|
107
|
+
instead — extract architecture, file inventory, implementation order, data model, API
|
|
108
|
+
endpoints, frontend components from the monolithic file.
|
|
84
109
|
|
|
85
110
|
### Step 2 — Derive User Stories from Use Cases
|
|
86
111
|
|
|
@@ -15,12 +15,17 @@ You do NOT repeat structural checks (schema conformance, required fields, ID for
|
|
|
15
15
|
|
|
16
16
|
## Input
|
|
17
17
|
|
|
18
|
-
The
|
|
18
|
+
The prompt gives you two paths, one per line, in this order:
|
|
19
19
|
|
|
20
|
-
1.
|
|
21
|
-
|
|
20
|
+
1. The Work Breakdown directory (the split format, FTR-020) — contains `index.json` (the
|
|
21
|
+
validated skeleton, schema v2) plus `user-stories/*.md`/`tasks/*.md` detail files. Use Glob/Read
|
|
22
|
+
to explore it as needed; do not expect a single `{PREFIX}-Work-Breakdown.json` file.
|
|
23
|
+
2. The Requirements source — `req/index.md` for a feature using the split Requirements format
|
|
24
|
+
(FTR-021; User Story descriptions and Acceptance Criteria text live in its own tables there,
|
|
25
|
+
unaffected by the per-UC split), or a legacy `{PREFIX}-Requirements.md` file otherwise. Read
|
|
26
|
+
whichever path is actually given — do not assume either shape.
|
|
22
27
|
|
|
23
|
-
Derive the prefix from the
|
|
28
|
+
Derive the prefix from the Work Breakdown directory's own `index.json`'s `feature` field.
|
|
24
29
|
|
|
25
30
|
---
|
|
26
31
|
|
|
@@ -45,6 +45,10 @@ function computeTechSpecCoverage(entries, acMap) {
|
|
|
45
45
|
}
|
|
46
46
|
|
|
47
47
|
function renderMatrix(prefix, acMap, coverage, techSpecCoverage = new Map()) {
|
|
48
|
+
const mustEntries = [...acMap.entries()].filter(([, ac]) => ac.priority === 'Must')
|
|
49
|
+
const mustCovered = mustEntries.filter(([id]) => coverage.get(id).taskIds.size > 0).length
|
|
50
|
+
const summary = `**Must-AC coverage:** ${mustCovered}/${mustEntries.length} covered. ` +
|
|
51
|
+
'Machine-readable data: [index.json](index.json).'
|
|
48
52
|
const rows = [...acMap.entries()].map(([acId, ac]) => {
|
|
49
53
|
const relatedUc = ac.unscoped ? 'All UCs' : ac.relatedUCs.join(', ')
|
|
50
54
|
const cov = coverage.get(acId)
|
|
@@ -53,7 +57,7 @@ function renderMatrix(prefix, acMap, coverage, techSpecCoverage = new Map()) {
|
|
|
53
57
|
const techSpecFiles = [...(techSpecCoverage.get(acId) || [])].join(', ') || '—'
|
|
54
58
|
return `| ${acId} | ${ac.criterion} | ${ac.priority} | ${relatedUc} | ${techSpecFiles} | ${taskIds} | ${phaseIds} |`
|
|
55
59
|
})
|
|
56
|
-
return `# AC ↔ Task Traceability Matrix — ${prefix}\n\n` +
|
|
60
|
+
return `# AC ↔ Task Traceability Matrix — ${prefix}\n\n${summary}\n\n` +
|
|
57
61
|
'| AC ID | Criterion | Priority | Related UC | Addressed by Tech-Spec File(s) | Covering Task(s) | Covering Phase(s) |\n' +
|
|
58
62
|
'|---|---|---|---|---|---|---|\n' +
|
|
59
63
|
rows.join('\n') + '\n\n' +
|
|
@@ -68,10 +72,36 @@ function renderMatrix(prefix, acMap, coverage, techSpecCoverage = new Map()) {
|
|
|
68
72
|
'belong to, comma-separated, or `—` if none.\n'
|
|
69
73
|
}
|
|
70
74
|
|
|
75
|
+
// FTR-026: serializes the same in-memory data renderMatrix() already consumes into a
|
|
76
|
+
// machine-readable shape — one entry per Requirements AC, in table order. No new computation.
|
|
77
|
+
function buildMatrixJson(prefix, acMap, coverage, techSpecCoverage = new Map()) {
|
|
78
|
+
const acceptanceCriteria = [...acMap.entries()].map(([acId, ac]) => {
|
|
79
|
+
const cov = coverage.get(acId)
|
|
80
|
+
return {
|
|
81
|
+
id: acId,
|
|
82
|
+
criterion: ac.criterion,
|
|
83
|
+
priority: ac.priority,
|
|
84
|
+
unscoped: !!ac.unscoped,
|
|
85
|
+
relatedUCs: ac.unscoped ? [] : ac.relatedUCs,
|
|
86
|
+
techSpecFiles: [...(techSpecCoverage.get(acId) || [])],
|
|
87
|
+
taskIds: [...cov.taskIds],
|
|
88
|
+
phaseIds: [...cov.phaseIds],
|
|
89
|
+
}
|
|
90
|
+
})
|
|
91
|
+
return { prefix, generatedAt: new Date().toISOString(), acceptanceCriteria }
|
|
92
|
+
}
|
|
93
|
+
|
|
71
94
|
if (require.main === module) {
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
95
|
+
// FTR-026: featureDir is REQUIRED (not optional) — confirmed by grep that pm-phase2.js and this
|
|
96
|
+
// script's own test suite are the only callers, so there is no backward-compatible no-arg case
|
|
97
|
+
// worth preserving. Output moves from one loose {prefix}-AC-Task-Matrix.md file (whose location
|
|
98
|
+
// used to be derived from path.dirname(requirementsPath) — a real bug: that would have silently
|
|
99
|
+
// moved once a caller passes a Requirements path living under req/) into a dedicated
|
|
100
|
+
// coverage-matrix/ subfolder under featureDir, as a JSON+MD pair.
|
|
101
|
+
const [wbsPath, requirementsPath, techSpecPath, prefix, featureDir] = process.argv.slice(2)
|
|
102
|
+
if (!wbsPath || !requirementsPath || !techSpecPath || !prefix || !featureDir) {
|
|
103
|
+
process.stderr.write(
|
|
104
|
+
'Usage: node ac-task-matrix.js <wbs-path> <requirements-path> <tech-spec-path> <prefix> <feature-dir>\n')
|
|
75
105
|
process.exit(1)
|
|
76
106
|
}
|
|
77
107
|
const wb = loadWorkBreakdown(path.resolve(wbsPath))
|
|
@@ -79,9 +109,13 @@ if (require.main === module) {
|
|
|
79
109
|
const coverage = computeCoverage(wb, acMap)
|
|
80
110
|
const techSpecEntries = parseFileInventoryEntries(fs.readFileSync(path.resolve(techSpecPath), 'utf8'))
|
|
81
111
|
const techSpecCoverage = computeTechSpecCoverage(techSpecEntries, acMap)
|
|
82
|
-
const
|
|
83
|
-
fs.
|
|
84
|
-
|
|
112
|
+
const outDir = path.join(path.resolve(featureDir), 'coverage-matrix')
|
|
113
|
+
fs.mkdirSync(outDir, { recursive: true })
|
|
114
|
+
const jsonPath = path.join(outDir, 'index.json')
|
|
115
|
+
const mdPath = path.join(outDir, 'index.md')
|
|
116
|
+
fs.writeFileSync(jsonPath, JSON.stringify(buildMatrixJson(prefix, acMap, coverage, techSpecCoverage), null, 2))
|
|
117
|
+
fs.writeFileSync(mdPath, renderMatrix(prefix, acMap, coverage, techSpecCoverage))
|
|
118
|
+
console.log(JSON.stringify({ jsonPath, mdPath }))
|
|
85
119
|
}
|
|
86
120
|
|
|
87
|
-
module.exports = { computeCoverage, renderMatrix, computeTechSpecCoverage }
|
|
121
|
+
module.exports = { computeCoverage, renderMatrix, computeTechSpecCoverage, buildMatrixJson }
|
|
@@ -164,10 +164,81 @@ function computeDependsOnPhases(skeleton, phaseId, taskIdToPhase) {
|
|
|
164
164
|
return [...deps]
|
|
165
165
|
}
|
|
166
166
|
|
|
167
|
+
// FTR-026: local re-implementation of tech-spec-assembler.js's findSectionRange/parseTableRows
|
|
168
|
+
// (from req-assembler.js)/findSubsectionRange/parseComponentsTable — duplicated, not imported,
|
|
169
|
+
// same "sandboxed runtime has no `require`" reason as the template-engine primitives above.
|
|
170
|
+
// Keep in sync with tech-spec-assembler.js if spec/index.md's Section 9 table shape ever changes.
|
|
171
|
+
function _findSectionRange(content, sectionNumber) {
|
|
172
|
+
const lines = content.split('\n')
|
|
173
|
+
let start = -1, end = lines.length
|
|
174
|
+
for (let i = 0; i < lines.length; i++) {
|
|
175
|
+
const m = lines[i].match(/^## (\d+)\.\s/)
|
|
176
|
+
if (!m) continue
|
|
177
|
+
const n = Number(m[1])
|
|
178
|
+
if (n === sectionNumber) start = i
|
|
179
|
+
else if (start !== -1 && n > sectionNumber) { end = i; break }
|
|
180
|
+
}
|
|
181
|
+
if (start === -1) throw new Error(`missing section ${sectionNumber}`)
|
|
182
|
+
return { start, end, lines }
|
|
183
|
+
}
|
|
184
|
+
function _parseTableRows(lines, start, end) {
|
|
185
|
+
const tableLines = lines.slice(start, end).filter(l => l.trim().startsWith('|'))
|
|
186
|
+
if (tableLines.length < 2) return []
|
|
187
|
+
return tableLines.slice(2).map(line =>
|
|
188
|
+
line.split('|').map(c => c.trim()).filter((_, i, a) => i > 0 && i < a.length - 1))
|
|
189
|
+
}
|
|
190
|
+
function _findSubsectionRange(lines, start, end, heading) {
|
|
191
|
+
let subStart = -1
|
|
192
|
+
for (let i = start; i < end; i++) {
|
|
193
|
+
if (lines[i].trim() === `### ${heading}`) { subStart = i; break }
|
|
194
|
+
}
|
|
195
|
+
if (subStart === -1) return null
|
|
196
|
+
let subEnd = end
|
|
197
|
+
for (let i = subStart + 1; i < end; i++) {
|
|
198
|
+
if (/^#{2,3}\s/.test(lines[i])) { subEnd = i; break }
|
|
199
|
+
}
|
|
200
|
+
return { start: subStart, end: subEnd }
|
|
201
|
+
}
|
|
202
|
+
function parseComponentsTableLocal(specIndexContent) {
|
|
203
|
+
const { start, end, lines } = _findSectionRange(specIndexContent, 9)
|
|
204
|
+
const entries = []
|
|
205
|
+
for (const [heading, changeType] of [['New files', 'New'], ['Modified files', 'Modified']]) {
|
|
206
|
+
const sub = _findSubsectionRange(lines, start, end, heading)
|
|
207
|
+
if (!sub) continue
|
|
208
|
+
for (const cells of _parseTableRows(lines, sub.start, sub.end)) {
|
|
209
|
+
const [filePath, purposeOrChange, addresses, domain, specLink] = cells
|
|
210
|
+
const m = specLink && specLink.match(/\(components\/([^)]+)\)/)
|
|
211
|
+
if (!m) throw new Error(`malformed Spec column for "${filePath}" in spec/index.md`)
|
|
212
|
+
entries.push({ filePath, purposeOrChange, changeType, domain, addresses, fileSlug: m[1] })
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
return entries
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Matches a task's own acceptanceCriteria against each component's Addresses column (the same
|
|
219
|
+
// relation spec/index.md's own Section 9 table already expresses) to give the per-task worker
|
|
220
|
+
// only the Tech-Spec content relevant to its own task, instead of the entire assembled spec —
|
|
221
|
+
// the single largest structural token cost this feature removes. Falls back to the full
|
|
222
|
+
// Components table (never zero content) when nothing matches, e.g. a purely organizational task.
|
|
223
|
+
function buildTaskTechSpecExcerpt(components, task, specDir, specIndexContent) {
|
|
224
|
+
const acSet = new Set(task.acceptanceCriteria || [])
|
|
225
|
+
const relevant = components.filter(comp =>
|
|
226
|
+
(comp.addresses || '').split(',').map(s => s.trim()).some(acId => acSet.has(acId)))
|
|
227
|
+
if (relevant.length === 0) {
|
|
228
|
+
return "_No component's Addresses column cites this task's Acceptance Criteria — full " +
|
|
229
|
+
'Components table below._\n\n' + specIndexContent
|
|
230
|
+
}
|
|
231
|
+
return relevant.map(comp => {
|
|
232
|
+
const body = c.read(`${specDir}/components/${comp.fileSlug}`)
|
|
233
|
+
return `### ${comp.filePath}\n\n${body}`
|
|
234
|
+
}).join('\n\n')
|
|
235
|
+
}
|
|
236
|
+
|
|
167
237
|
// Compact prompt for the per-task detail-generation worker: this task's own skeleton entry,
|
|
168
238
|
// the full skeleton's sibling task list (id + title + outcome only, for duplicate-
|
|
169
|
-
// implementation awareness), and the Tech-Spec
|
|
170
|
-
// own
|
|
239
|
+
// implementation awareness), and the Tech-Spec excerpt (FTR-026: already pre-sliced to this
|
|
240
|
+
// task's own relevant component(s) by buildTaskTechSpecExcerpt, or the full legacy Tech-Spec
|
|
241
|
+
// text for a feature with no spec/ directory).
|
|
171
242
|
function buildTaskPrompt(index, taskId, techSpecText) {
|
|
172
243
|
const task = index.tasks[taskId]
|
|
173
244
|
const siblingList = Object.entries(index.tasks)
|
|
@@ -184,32 +255,19 @@ const { featurePath, featureDir, prefix } = c.feature(args[0])
|
|
|
184
255
|
return c.run(meta.name, featureDir, prefix, async () => {
|
|
185
256
|
const file = suffix => `${featureDir}/${prefix}-${suffix}`
|
|
186
257
|
|
|
187
|
-
// FTR-
|
|
188
|
-
//
|
|
189
|
-
// check
|
|
190
|
-
//
|
|
191
|
-
//
|
|
192
|
-
//
|
|
193
|
-
//
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
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
|
-
})
|
|
258
|
+
// FTR-026: pm-phase2.js is now split-source-native itself — every call site below reads
|
|
259
|
+
// req/index.md/spec/index.md directly, in-memory assembly feeds only the Gate 1 digest
|
|
260
|
+
// re-check, and nothing here writes an assembled monolithic file to disk (the
|
|
261
|
+
// bridge-requirements-tech-spec activity FTR-025 added as a transitional shim is gone; see
|
|
262
|
+
// git history for FTR-025's original version of this block). A legacy feature (monolithic
|
|
263
|
+
// files only, no req/spec/ directories) falls back to reading those files directly at every
|
|
264
|
+
// site below, unchanged from pre-FTR-026 behavior.
|
|
265
|
+
const reqDir = `${featureDir}/req`, specDir = `${featureDir}/spec`
|
|
266
|
+
// c.exists() only checks isFile() — never true for a directory — so this checks each split's
|
|
267
|
+
// own index.md (a real file) rather than the directory itself.
|
|
268
|
+
const hasReq = c.exists(`${reqDir}/index.md`), hasSpec = c.exists(`${specDir}/index.md`)
|
|
269
|
+
const reqIndexPath = hasReq ? `${reqDir}/index.md` : file('Requirements.md')
|
|
270
|
+
const specIndexPath = hasSpec ? `${specDir}/index.md` : file('Tech-Spec.md')
|
|
213
271
|
|
|
214
272
|
const approvals = c.read(file('Approvals.md'))
|
|
215
273
|
const gate = approvals.match(/^## Gate 1\b[^\n]*\n([\s\S]*?)(?=^## |$(?![\s\S]))/m)
|
|
@@ -225,7 +283,16 @@ return c.run(meta.name, featureDir, prefix, async () => {
|
|
|
225
283
|
// re-approved, not silently carried forward on stale consent.
|
|
226
284
|
const digestMatch = gate[1].match(/\*\*Docs Digest:\*\*\s*`([^`]+)`/)
|
|
227
285
|
if (digestMatch) {
|
|
228
|
-
|
|
286
|
+
// FTR-026: recomputed in-memory from the split sources (same req-assembler.js/
|
|
287
|
+
// tech-spec-assembler.js subprocess calls pm-phase1.js already makes for its own
|
|
288
|
+
// docsDigest) instead of reading an assembled file from disk — never written here.
|
|
289
|
+
const assembledRequirements = hasReq
|
|
290
|
+
? c.command(['run-asset', 'scripts/req-assembler.js', '--project', c.root, '--', reqDir]).stdout
|
|
291
|
+
: c.read(file('Requirements.md'))
|
|
292
|
+
const assembledTechSpec = hasSpec
|
|
293
|
+
? c.command(['run-asset', 'scripts/tech-spec-assembler.js', '--project', c.root, '--', specDir]).stdout
|
|
294
|
+
: c.read(file('Tech-Spec.md'))
|
|
295
|
+
const currentDigest = c.hash(assembledRequirements + '\u0000' + assembledTechSpec)
|
|
229
296
|
if (digestMatch[1] !== currentDigest) {
|
|
230
297
|
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')
|
|
231
298
|
}
|
|
@@ -259,7 +326,7 @@ return c.run(meta.name, featureDir, prefix, async () => {
|
|
|
259
326
|
// wb-validate.js now imports — run via subprocess since this runtime has no `require`)
|
|
260
327
|
const skeletonCheck = await c.activity('wb-skeleton-check', async () => {
|
|
261
328
|
const res = c.command(['run-asset', 'scripts/wb-skeleton-check.js', '--project', c.root,
|
|
262
|
-
'--', indexPath,
|
|
329
|
+
'--', indexPath, reqIndexPath], [0, 1])
|
|
263
330
|
const parsed = JSON.parse(res.stdout)
|
|
264
331
|
if (typeof parsed.valid !== 'boolean' || !Array.isArray(parsed.errors) ||
|
|
265
332
|
(res.exitCode === 0) !== parsed.valid) throw new Error('Invalid skeleton-check result')
|
|
@@ -268,13 +335,20 @@ return c.run(meta.name, featureDir, prefix, async () => {
|
|
|
268
335
|
})
|
|
269
336
|
|
|
270
337
|
// 3. Per-task generation, resume-by-status, sequential
|
|
338
|
+
// FTR-026: each task's own worker call now receives only the Tech-Spec content relevant to its
|
|
339
|
+
// own Acceptance Criteria, not the entire assembled spec — computed once, before the loop, by
|
|
340
|
+
// matching each task's acceptanceCriteria against spec/index.md's own Addresses column.
|
|
341
|
+
const components = hasSpec ? parseComponentsTableLocal(c.read(specIndexPath)) : []
|
|
271
342
|
for (const phase of index.phases) {
|
|
272
343
|
for (const taskId of phase.taskIds) {
|
|
273
344
|
const task = index.tasks[taskId]
|
|
274
345
|
const detailPath = `${wbsDir}/${task.detailFile}`
|
|
275
346
|
if (task.status === 'generated' && c.exists(detailPath)) continue // resume: skip, no dispatch
|
|
347
|
+
const techSpecExcerpt = hasSpec
|
|
348
|
+
? buildTaskTechSpecExcerpt(components, task, specDir, c.read(specIndexPath))
|
|
349
|
+
: c.read(file('Tech-Spec.md')) // legacy fallback: unchanged, full text
|
|
276
350
|
const detail = await c.worker('gaia.agent.planner.work-breakdown-task',
|
|
277
|
-
buildTaskPrompt(index, taskId,
|
|
351
|
+
buildTaskPrompt(index, taskId, techSpecExcerpt), { label: `work-breakdown-task:${taskId}` })
|
|
278
352
|
c.writeOutput(detailPath, detail)
|
|
279
353
|
task.status = 'generated'
|
|
280
354
|
c.writeOutput(indexPath, JSON.stringify(index, null, 2)) // persist status after each task
|
|
@@ -285,7 +359,7 @@ return c.run(meta.name, featureDir, prefix, async () => {
|
|
|
285
359
|
// existing pipeline, unchanged past this point except the path passed in
|
|
286
360
|
const report = await c.activity('wb-validate', async () => {
|
|
287
361
|
const res = c.command(['run-asset', 'scripts/wb-validate.js', '--project', c.root,
|
|
288
|
-
'--', wbsDir,
|
|
362
|
+
'--', wbsDir, reqIndexPath], [0, 1])
|
|
289
363
|
const parsed = JSON.parse(res.stdout)
|
|
290
364
|
if (typeof parsed.valid !== 'boolean' || !Array.isArray(parsed.errors) ||
|
|
291
365
|
!Array.isArray(parsed.warnings) || (res.exitCode === 0) !== parsed.valid) throw new Error('Invalid validator result')
|
|
@@ -293,7 +367,7 @@ return c.run(meta.name, featureDir, prefix, async () => {
|
|
|
293
367
|
return parsed
|
|
294
368
|
})
|
|
295
369
|
const semantic = await c.worker('gaia.agent.planner.validate-work-breakdown',
|
|
296
|
-
`${wbsDir}\n${
|
|
370
|
+
`${wbsDir}\n${reqIndexPath}`, { schema: {
|
|
297
371
|
type: 'object', properties: { valid: { type: 'boolean' }, findings: { type: 'array' } }, required: ['valid', 'findings'],
|
|
298
372
|
} })
|
|
299
373
|
if (!semantic.valid || semantic.findings.some(f => f.blocking)) throw new Error('Work breakdown semantic validation failed')
|
|
@@ -307,12 +381,17 @@ return c.run(meta.name, featureDir, prefix, async () => {
|
|
|
307
381
|
const acTaskMatrix = await c.activity('ac-task-matrix', async () => {
|
|
308
382
|
// FTR-023: ac-task-matrix.js's CLI gained a 3rd positional argument (Tech-Spec path) to
|
|
309
383
|
// render the "Addressed by Tech-Spec File(s)" column.
|
|
384
|
+
// FTR-026: gained a required 5th (featureDir) argument; output moved from one loose
|
|
385
|
+
// {PREFIX}-AC-Task-Matrix.md file into a coverage-matrix/ subfolder as a JSON+MD pair.
|
|
310
386
|
const result = c.command(['run-asset', 'scripts/ac-task-matrix.js', '--project', c.root,
|
|
311
|
-
'--', wbsDir,
|
|
312
|
-
const
|
|
313
|
-
|
|
314
|
-
c.
|
|
315
|
-
|
|
387
|
+
'--', wbsDir, reqIndexPath, specIndexPath, prefix, featureDir])
|
|
388
|
+
const matrixJsonPath = `${featureDir}/coverage-matrix/index.json`
|
|
389
|
+
const matrixMdPath = `${featureDir}/coverage-matrix/index.md`
|
|
390
|
+
if (!c.exists(matrixJsonPath)) throw new Error('AC-Task matrix JSON output missing')
|
|
391
|
+
if (!c.exists(matrixMdPath)) throw new Error('AC-Task matrix MD output missing')
|
|
392
|
+
c.read(matrixJsonPath) // never trust an unread write
|
|
393
|
+
c.read(matrixMdPath)
|
|
394
|
+
return { ...result, matrixJsonPath, matrixMdPath }
|
|
316
395
|
})
|
|
317
396
|
// Assembled structure for c.estimates()'s metrics — via wbs-assembler.js's own CLI mode
|
|
318
397
|
// (subprocess), since this runtime cannot `require` it in-process either.
|
|
@@ -326,7 +405,8 @@ return c.run(meta.name, featureDir, prefix, async () => {
|
|
|
326
405
|
js_validator_report: report, js_validator_failed: false,
|
|
327
406
|
semantic_validator_result: semantic, semantic_validator_failed: false,
|
|
328
407
|
gate2_blocked: false,
|
|
329
|
-
|
|
408
|
+
ac_task_matrix_json_path: acTaskMatrix.matrixJsonPath,
|
|
409
|
+
ac_task_matrix_md_path: acTaskMatrix.matrixMdPath,
|
|
330
410
|
duration_bands: report.durationBands, domain_distribution: report.domainDistribution,
|
|
331
411
|
warning_band_tasks: report.warnings.filter(w => w.category === 'duration_warning').map(w => ({ taskId: w.taskId, agentMinutes: w.details?.agentMinutes })),
|
|
332
412
|
split_required_tasks: [], must_ac_uncovered: [], phase_unschedulable: [],
|