@dtlabs/ai-toolkit 0.17.5 → 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 +29 -0
- package/lib/agent-registry.js +39 -0
- 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 +124 -105
- package/src/claude/agents/gaia-validate-feature-docs.md +13 -8
- package/src/claude/agents/gaia-validate-work-breakdown-semantic.md +9 -4
- 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 +121 -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 +324 -18
- package/src/claude/workflows/pm-phase2.js +358 -15
|
@@ -0,0 +1,368 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const fs = require('fs')
|
|
4
|
+
const path = require('path')
|
|
5
|
+
|
|
6
|
+
// Directory/legacy loader + task/phase detail block codec for the split Work Breakdown
|
|
7
|
+
// format (FTR-020). Exports loadWorkBreakdown(inputPath), which detects single-file
|
|
8
|
+
// (legacy schema v2) vs. directory (wbs/, schema v3) input and returns the same
|
|
9
|
+
// { schemaVersion, feature, phases: [{ id, type, title, commit, tasks: [...] }] } shape
|
|
10
|
+
// wb-validate.js/wb-render.js already consume — their own logic is otherwise unchanged.
|
|
11
|
+
// Also exports parseTaskDetailBlock/renderTaskDetailBlock and, for the phase tier (OQ-04),
|
|
12
|
+
// parsePhaseDetailBlock/renderPhaseDetailBlock — all four independently reimplemented from
|
|
13
|
+
// the field-line/fence conventions already proven in lib/task-executor/plan.js, NOT
|
|
14
|
+
// imported from that module (feature.md Out of Scope — plan.js is not touched).
|
|
15
|
+
|
|
16
|
+
const DASH = '—'
|
|
17
|
+
|
|
18
|
+
// Field values are read from the CONTIGUOUS RUN of `- **Label:** value` lines starting
|
|
19
|
+
// immediately after the heading, stopping at the first non-matching line — NOT a global
|
|
20
|
+
// regex scan over the whole file. A task's Implementation notes section may legitimately
|
|
21
|
+
// illustrate this exact block format inside a fenced code example; a global scan would
|
|
22
|
+
// mistake that illustrative text for real field values. Found empirically while authoring
|
|
23
|
+
// this feature's own `wbs/` fixture (see `US-03-TASK-INFRA-01.md`).
|
|
24
|
+
const FIELD_LINE_RE = /^- \*\*(.+?):\*\*[ \t]?(.*)$/
|
|
25
|
+
const FENCE_RE = /^(`{3,})\r?\n([\s\S]*?)\r?\n\1[ \t]*$/gm
|
|
26
|
+
|
|
27
|
+
function _listOrEmpty(value) {
|
|
28
|
+
if (value === DASH) return []
|
|
29
|
+
return value.split(',').map(s => s.trim()).filter(Boolean)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function _stringOrNull(value) {
|
|
33
|
+
return value === DASH ? null : value
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// ── Template engine ───────────────────────────────────────────────────────────
|
|
37
|
+
// Loads a canonical templates/gaia-{name}-template.md file and fills in its placeholders. The
|
|
38
|
+
// template file is the single source of truth for field labels, their order, and section
|
|
39
|
+
// headings — these renderers only ever supply VALUES, never hardcode the shape themselves (the
|
|
40
|
+
// whole point of FTR-025's fix: a field added/reworded/reordered in the template is picked up
|
|
41
|
+
// automatically, instead of the template and the renderer silently drifting apart the way the
|
|
42
|
+
// FTR-020 phase-detail template did for years before anything used it). Fails closed (throws) if
|
|
43
|
+
// the template references a label/section the caller didn't supply a value for, or vice versa —
|
|
44
|
+
// a silently-blank or stale placeholder slipping into real output is exactly the bug class this
|
|
45
|
+
// fix exists to prevent.
|
|
46
|
+
|
|
47
|
+
function _loadTemplate(name) {
|
|
48
|
+
return fs.readFileSync(path.join(__dirname, 'templates', `gaia-${name}-template.md`), 'utf8')
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function _stripTemplateComments(text) {
|
|
52
|
+
return text.replace(/<!--[\s\S]*?-->/g, '')
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Fills the contiguous run of `- **Label:** <placeholder>` fields starting immediately after the
|
|
56
|
+
// heading — same contiguous-run rule _parseFieldBlock uses when reading an instance, applied here
|
|
57
|
+
// to a template to both discover its shape and substitute real values in one pass. A field's own
|
|
58
|
+
// placeholder guidance may itself be hard-wrapped across several physical lines (several of the
|
|
59
|
+
// real FTR-020 templates do this for a long explanation) — a continuation line never starts with
|
|
60
|
+
// `- **`, so it is skipped rather than mistaken for the end of the field block; only a blank line
|
|
61
|
+
// (or EOF) ends the run. The whole multi-line span for a given field is collapsed to the single
|
|
62
|
+
// substituted line.
|
|
63
|
+
function _fillFieldBlock(templateText, values) {
|
|
64
|
+
const lines = templateText.split('\n')
|
|
65
|
+
let start = 0
|
|
66
|
+
while (start < lines.length && !FIELD_LINE_RE.test(lines[start])) start++
|
|
67
|
+
const starts = []
|
|
68
|
+
let end = start
|
|
69
|
+
for (let j = start; j < lines.length; j++) {
|
|
70
|
+
const m = lines[j].match(FIELD_LINE_RE)
|
|
71
|
+
if (m) starts.push({ label: m[1] })
|
|
72
|
+
else if (lines[j].trim() === '') { end = j; break }
|
|
73
|
+
end = j + 1
|
|
74
|
+
}
|
|
75
|
+
const seen = new Set()
|
|
76
|
+
const newFieldLines = starts.map(({ label }) => {
|
|
77
|
+
if (!(label in values)) throw new Error(`Template field "${label}" has no supplied value`)
|
|
78
|
+
seen.add(label)
|
|
79
|
+
return `- **${label}:** ${values[label]}`
|
|
80
|
+
})
|
|
81
|
+
for (const label of Object.keys(values)) {
|
|
82
|
+
if (!seen.has(label)) throw new Error(`Supplied value for "${label}" matches no template field`)
|
|
83
|
+
}
|
|
84
|
+
return [...lines.slice(0, start), ...newFieldLines, ...lines.slice(end)].join('\n')
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Replaces the inline value after a `**Label:** <placeholder>` line (a bold field OUTSIDE the
|
|
88
|
+
// dash-prefixed field block, e.g. "Depends on phases:" — the value lives on the same line as the
|
|
89
|
+
// label, not in a separate block below it).
|
|
90
|
+
// Same multi-line-wrapped-placeholder handling as _fillFieldBlock: the value after the label may
|
|
91
|
+
// itself wrap across several physical lines (gaia-user-story-template.md's own "Depends on
|
|
92
|
+
// phases" placeholder does), so this consumes every line from the label's own line up to the next
|
|
93
|
+
// blank line (or EOF), not just the first line — a naive single-line regex would leave the
|
|
94
|
+
// wrapped continuation behind as orphaned text in the real output.
|
|
95
|
+
function _fillInlineBoldField(templateText, label, value) {
|
|
96
|
+
const lines = templateText.split('\n')
|
|
97
|
+
const marker = `**${label}:**`
|
|
98
|
+
const startIdx = lines.findIndex(l => l.includes(marker))
|
|
99
|
+
if (startIdx === -1) throw new Error(`Template is missing the "${label}" inline field`)
|
|
100
|
+
let end = startIdx + 1
|
|
101
|
+
while (end < lines.length && lines[end].trim() !== '') end++
|
|
102
|
+
return [...lines.slice(0, startIdx), `${marker} ${value}`, ...lines.slice(end)].join('\n')
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Replaces everything between a `**Section Name:**` marker and the next `**...:**` marker (or end
|
|
106
|
+
// of file) with real content — used for block-shaped sections (a paragraph, a bullet list), as
|
|
107
|
+
// opposed to _fillInlineBoldField's single-line value.
|
|
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
|
+
|
|
118
|
+
function _parseFieldBlock(text) {
|
|
119
|
+
const lines = text.split(/\r?\n/)
|
|
120
|
+
const fields = {}
|
|
121
|
+
let i = 0
|
|
122
|
+
while (i < lines.length && !FIELD_LINE_RE.test(lines[i])) i++
|
|
123
|
+
for (; i < lines.length; i++) {
|
|
124
|
+
const m = lines[i].match(FIELD_LINE_RE)
|
|
125
|
+
if (!m) break // first non-field line ends the contiguous run
|
|
126
|
+
fields[m[1].trim()] = m[2].trim()
|
|
127
|
+
}
|
|
128
|
+
return fields
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function _parseVerificationCommands(text) {
|
|
132
|
+
const marker = '**Verification commands:**'
|
|
133
|
+
const idx = text.lastIndexOf(marker) // LAST occurrence — the real section is always the
|
|
134
|
+
// final one in a well-formed file; an earlier occurrence could be inside an illustrative
|
|
135
|
+
// example in the Implementation notes section (same collision risk as the field lines)
|
|
136
|
+
if (idx === -1) return []
|
|
137
|
+
const verifPart = text.slice(idx + marker.length)
|
|
138
|
+
const commands = []
|
|
139
|
+
let m
|
|
140
|
+
FENCE_RE.lastIndex = 0
|
|
141
|
+
while ((m = FENCE_RE.exec(verifPart))) commands.push(m[2])
|
|
142
|
+
return commands
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// ── Task detail block codec ──────────────────────────────────────────────────
|
|
146
|
+
|
|
147
|
+
function parseTaskDetailBlock(text) {
|
|
148
|
+
const fields = _parseFieldBlock(text)
|
|
149
|
+
return {
|
|
150
|
+
groupingRationale: _stringOrNull(fields['Grouping rationale']),
|
|
151
|
+
verificationCommands: _parseVerificationCommands(text),
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Loads templates/gaia-task-template.md and fills it in — the template is the single source of
|
|
156
|
+
// truth for the 14 field labels/order and the two section headings; this function only supplies
|
|
157
|
+
// values. `implementationNotes` is optional: the real per-task agent (gaia-generate-work-
|
|
158
|
+
// breakdown-task.md) writes its own full response directly, so this function's only real caller
|
|
159
|
+
// is the round-trip tests, which never populate it — a neutral, clearly-a-placeholder default
|
|
160
|
+
// keeps the template's own required section non-blank without inventing fake content.
|
|
161
|
+
function renderTaskDetailBlock(task, implementationNotes) {
|
|
162
|
+
const cmds = (task.verification && task.verification.commands) || []
|
|
163
|
+
const cmdBlocks = cmds.length
|
|
164
|
+
? cmds.map(c => '```\n' + c + '\n```').join('\n\n')
|
|
165
|
+
: '_No verification commands._'
|
|
166
|
+
const dependsOn = Array.isArray(task.dependsOn) && task.dependsOn.length ? task.dependsOn.join(', ') : DASH
|
|
167
|
+
const acceptanceCriteria = Array.isArray(task.acceptanceCriteria) && task.acceptanceCriteria.length
|
|
168
|
+
? task.acceptanceCriteria.join(', ')
|
|
169
|
+
: DASH
|
|
170
|
+
let text = _stripTemplateComments(_loadTemplate('task'))
|
|
171
|
+
text = text.replace(/\{TASK_ID\}/g, task.id)
|
|
172
|
+
text = _fillFieldBlock(text, {
|
|
173
|
+
'Task ID': task.id,
|
|
174
|
+
'Title': task.title,
|
|
175
|
+
'Outcome': task.outcome,
|
|
176
|
+
'Domain': task.domain,
|
|
177
|
+
'Agent type': task.agentType,
|
|
178
|
+
'Dependencies': dependsOn,
|
|
179
|
+
'Acceptance criteria': acceptanceCriteria,
|
|
180
|
+
'Estimate — agent minutes': task.estimate.agentMinutes,
|
|
181
|
+
'Estimate — tokens': task.estimate.tokens,
|
|
182
|
+
'Output count': task.outputCount,
|
|
183
|
+
'Grouping rationale': task.groupingRationale == null ? DASH : task.groupingRationale,
|
|
184
|
+
'Commit type': task.commit.type,
|
|
185
|
+
'Commit scope': task.commit.scope == null ? DASH : task.commit.scope,
|
|
186
|
+
'Commit subject': task.commit.subject,
|
|
187
|
+
})
|
|
188
|
+
text = _fillNamedSection(text, 'Implementation notes', implementationNotes == null ? '_No implementation notes supplied._' : implementationNotes)
|
|
189
|
+
text = _fillNamedSection(text, 'Verification commands', cmdBlocks)
|
|
190
|
+
return text.trim() + '\n'
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// ── Phase detail block codec (OQ-04, third tier — no fenced section) ─────────
|
|
194
|
+
|
|
195
|
+
function parsePhaseDetailBlock(text) {
|
|
196
|
+
const fields = _parseFieldBlock(text)
|
|
197
|
+
return {
|
|
198
|
+
title: fields['Title'],
|
|
199
|
+
commit: {
|
|
200
|
+
type: _stringOrNull(fields['Commit type']),
|
|
201
|
+
scope: _stringOrNull(fields['Commit scope']),
|
|
202
|
+
subject: _stringOrNull(fields['Commit subject']),
|
|
203
|
+
},
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// Loads templates/gaia-user-story-template.md and fills it in — the template is the single
|
|
208
|
+
// source of truth for the 7 field labels/order and the three section headings (Context/Tasks in
|
|
209
|
+
// this phase/Depends on phases); this function only supplies values, mechanically derived where
|
|
210
|
+
// possible (Source UC from phase.sourceUC, Tasks-in-phase from each task's own title, Depends-on-
|
|
211
|
+
// phases from the task dependsOn graph — see pm-phase2.js's computeDependsOnPhases) or taken
|
|
212
|
+
// as-is from the one field that genuinely needs narrative judgment (Context). taskTitles/
|
|
213
|
+
// dependsOnPhases default to [] so the minimal round-trip tests (which only exercise title/
|
|
214
|
+
// commit) can keep calling this with just a phase object.
|
|
215
|
+
function renderPhaseDetailBlock(phase, taskTitles = [], dependsOnPhases = []) {
|
|
216
|
+
const sourceUC = Array.isArray(phase.sourceUC) && phase.sourceUC.length ? phase.sourceUC.join(', ') : DASH
|
|
217
|
+
const tasksBlock = taskTitles.length
|
|
218
|
+
? taskTitles.map(t => `- \`${t.id}\` — ${t.title}`).join('\n')
|
|
219
|
+
: '_No tasks._'
|
|
220
|
+
const dependsOn = dependsOnPhases.length ? dependsOnPhases.join(', ') : 'none'
|
|
221
|
+
let text = _stripTemplateComments(_loadTemplate('user-story'))
|
|
222
|
+
text = text.replace(/\{PHASE_ID\}/g, phase.id)
|
|
223
|
+
text = _fillFieldBlock(text, {
|
|
224
|
+
'Phase ID': phase.id,
|
|
225
|
+
'Type': phase.type,
|
|
226
|
+
'Source UC': sourceUC,
|
|
227
|
+
'Title': phase.title,
|
|
228
|
+
'Commit type': phase.commit.type,
|
|
229
|
+
'Commit scope': phase.commit.scope == null ? DASH : phase.commit.scope,
|
|
230
|
+
'Commit subject': phase.commit.subject,
|
|
231
|
+
})
|
|
232
|
+
text = _fillNamedSection(text, 'Context', phase.context == null ? DASH : phase.context)
|
|
233
|
+
text = _fillNamedSection(text, 'Tasks in this phase', tasksBlock)
|
|
234
|
+
text = _fillInlineBoldField(text, 'Depends on phases', dependsOn)
|
|
235
|
+
return text.trim() + '\n'
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// ── loadWorkBreakdown(inputPath) ──────────────────────────────────────────────
|
|
239
|
+
|
|
240
|
+
function _assemblyError(message) {
|
|
241
|
+
const err = new Error(message)
|
|
242
|
+
err.code = 'WBS_ASSEMBLY_ERROR'
|
|
243
|
+
return err
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function _isDirectory(p) {
|
|
247
|
+
try {
|
|
248
|
+
return fs.statSync(p).isDirectory()
|
|
249
|
+
} catch (err) {
|
|
250
|
+
throw _assemblyError(`cannot access "${p}": ${err.message}`)
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function _readFile(p) {
|
|
255
|
+
try {
|
|
256
|
+
return fs.readFileSync(p, 'utf8')
|
|
257
|
+
} catch (err) {
|
|
258
|
+
throw _assemblyError(`cannot read "${p}": ${err.message}`)
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function loadWorkBreakdown(inputPath) {
|
|
263
|
+
if (_isDirectory(inputPath)) {
|
|
264
|
+
const indexPath = path.join(inputPath, 'index.json')
|
|
265
|
+
let index
|
|
266
|
+
try {
|
|
267
|
+
index = JSON.parse(_readFile(indexPath))
|
|
268
|
+
} catch (err) {
|
|
269
|
+
if (err.code === 'WBS_ASSEMBLY_ERROR') throw err
|
|
270
|
+
throw _assemblyError(`"${indexPath}" is not valid JSON: ${err.message}`)
|
|
271
|
+
}
|
|
272
|
+
if (index.schemaVersion !== 3) {
|
|
273
|
+
throw _assemblyError(`"${indexPath}" must have schemaVersion 3, got ${JSON.stringify(index.schemaVersion)}`)
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
const phases = (index.phases || []).map(phaseSkeleton => {
|
|
277
|
+
const phaseDetailPath = path.join(inputPath, phaseSkeleton.detailFile)
|
|
278
|
+
if (!fs.existsSync(phaseDetailPath)) {
|
|
279
|
+
throw _assemblyError(`phase "${phaseSkeleton.id}" has no detail file at ${phaseDetailPath}`)
|
|
280
|
+
}
|
|
281
|
+
const phaseDetail = parsePhaseDetailBlock(_readFile(phaseDetailPath))
|
|
282
|
+
|
|
283
|
+
const tasks = (phaseSkeleton.taskIds || []).map(taskId => {
|
|
284
|
+
const skeletonTask = index.tasks ? index.tasks[taskId] : null
|
|
285
|
+
if (skeletonTask == null) {
|
|
286
|
+
throw _assemblyError(`index.json references unknown task "${taskId}"`)
|
|
287
|
+
}
|
|
288
|
+
const detailPath = path.join(inputPath, skeletonTask.detailFile)
|
|
289
|
+
if (!fs.existsSync(detailPath)) {
|
|
290
|
+
throw _assemblyError(`task "${taskId}" has no detail file at ${detailPath}`)
|
|
291
|
+
}
|
|
292
|
+
const detail = parseTaskDetailBlock(_readFile(detailPath))
|
|
293
|
+
return {
|
|
294
|
+
id: taskId,
|
|
295
|
+
title: skeletonTask.title,
|
|
296
|
+
outcome: skeletonTask.outcome,
|
|
297
|
+
domain: skeletonTask.domain,
|
|
298
|
+
agentType: skeletonTask.agentType,
|
|
299
|
+
dependsOn: skeletonTask.dependsOn,
|
|
300
|
+
acceptanceCriteria: skeletonTask.acceptanceCriteria,
|
|
301
|
+
estimate: skeletonTask.estimate,
|
|
302
|
+
outputCount: skeletonTask.outputCount,
|
|
303
|
+
groupingRationale: detail.groupingRationale,
|
|
304
|
+
commit: skeletonTask.commit,
|
|
305
|
+
verification: { commands: detail.verificationCommands },
|
|
306
|
+
}
|
|
307
|
+
})
|
|
308
|
+
|
|
309
|
+
return {
|
|
310
|
+
id: phaseSkeleton.id,
|
|
311
|
+
type: phaseSkeleton.type,
|
|
312
|
+
title: phaseDetail.title,
|
|
313
|
+
commit: phaseDetail.commit,
|
|
314
|
+
// sourceUC was dropped here from FTR-020 through FTR-024 — wb-graph-checks.js's
|
|
315
|
+
// check 20 (AC scope validation) needs it to correctly scope an AC to the phase(s)
|
|
316
|
+
// that actually realize its UC, instead of assuming phase ID numbering matches UC
|
|
317
|
+
// numbering 1:1 (which the worked example behind this initiative already found false
|
|
318
|
+
// in general: one phase can span several UCs, one UC can span several phases).
|
|
319
|
+
sourceUC: phaseSkeleton.sourceUC,
|
|
320
|
+
tasks,
|
|
321
|
+
}
|
|
322
|
+
})
|
|
323
|
+
|
|
324
|
+
// Normalized to the existing in-memory shape wb-validate.js/wb-render.js already
|
|
325
|
+
// consume; schemaVersion is reported as 2 here because that is the *assembled,
|
|
326
|
+
// in-memory* contract version those scripts' downstream logic was written against —
|
|
327
|
+
// index.json's own on-disk schemaVersion stays 3, a separate, unrelated version number.
|
|
328
|
+
return { schemaVersion: 2, feature: index.feature, phases }
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// Legacy path: unchanged behavior — no schemaVersion gate here. wb-validate.js's own
|
|
332
|
+
// check 2 is what reports a schemaVersion mismatch, as a NON-FATAL report entry (exit 1),
|
|
333
|
+
// not a fatal parse error (exit 2); gating on it here would turn that into a fatal abort
|
|
334
|
+
// and break the existing contract (AC-06: "loaded exactly as today").
|
|
335
|
+
return JSON.parse(_readFile(inputPath))
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
module.exports = {
|
|
339
|
+
loadWorkBreakdown,
|
|
340
|
+
parseTaskDetailBlock,
|
|
341
|
+
renderTaskDetailBlock,
|
|
342
|
+
parsePhaseDetailBlock,
|
|
343
|
+
renderPhaseDetailBlock,
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// ── CLI entry point ───────────────────────────────────────────────────────────
|
|
347
|
+
// pm-phase2.js's workflow-script runtime has no `require` (see lib/workflow-runner.js),
|
|
348
|
+
// so it cannot call loadWorkBreakdown() in-process the way wb-validate.js/wb-render.js do.
|
|
349
|
+
// This CLI mode lets it get the assembled structure (needed for c.estimates()'s metrics)
|
|
350
|
+
// via the same `c.command(['run-asset', 'scripts/wbs-assembler.js', ...])` subprocess
|
|
351
|
+
// pattern already used for wb-validate.js/wb-render.js — prints the assembled JSON to
|
|
352
|
+
// stdout. Coexists with the module.exports above; this guard only runs when the file is
|
|
353
|
+
// executed directly, not when required.
|
|
354
|
+
if (require.main === module) {
|
|
355
|
+
const inputPath = process.argv[2]
|
|
356
|
+
if (!inputPath) {
|
|
357
|
+
process.stderr.write('Usage: node wbs-assembler.js <path-to-work-breakdown-or-wbs-dir>\n')
|
|
358
|
+
process.exit(2)
|
|
359
|
+
}
|
|
360
|
+
try {
|
|
361
|
+
const wb = loadWorkBreakdown(path.resolve(inputPath))
|
|
362
|
+
process.stdout.write(JSON.stringify(wb) + '\n')
|
|
363
|
+
process.exit(0)
|
|
364
|
+
} catch (err) {
|
|
365
|
+
process.stderr.write(`Error: ${err.message}\n`)
|
|
366
|
+
process.exit(2)
|
|
367
|
+
}
|
|
368
|
+
}
|
|
@@ -65,8 +65,11 @@ The host verifies the installed workflow and each worker and owns all ledger tra
|
|
|
65
65
|
|
|
66
66
|
Extract from the result:
|
|
67
67
|
- `prefix` — feature prefix (e.g. `FTR-009`)
|
|
68
|
-
- `requirements.summary` —
|
|
69
|
-
|
|
68
|
+
- `requirements.path` / `requirements.summary` — Requirements artifact path (the split
|
|
69
|
+
`req/index.md` by default — generation no longer produces a single assembled file
|
|
70
|
+
automatically) and its summary
|
|
71
|
+
- `tech_spec.path` / `tech_spec.summary` — Tech-Spec artifact path (the split `spec/index.md` by
|
|
72
|
+
default) and its summary
|
|
70
73
|
- `validation.summary` — validation result (gaps or clean)
|
|
71
74
|
- `docs_digest` — content digest binding this approval to the exact Requirements/Tech-Spec
|
|
72
75
|
bytes just produced; embed verbatim into `{PREFIX}-Approvals.md` in Step 2 below
|
|
@@ -85,9 +88,12 @@ Present the following to the user:
|
|
|
85
88
|
```
|
|
86
89
|
Documents generated for {PREFIX}:
|
|
87
90
|
|
|
88
|
-
{
|
|
89
|
-
{
|
|
91
|
+
{requirements.path} — {requirements.summary}
|
|
92
|
+
{tech_spec.path} — {tech_spec.summary}
|
|
90
93
|
{PREFIX}-Validation-Report.md — {validation.summary}
|
|
94
|
+
|
|
95
|
+
To view a single combined document instead of the split source files, run:
|
|
96
|
+
/assemble-docs {PREFIX} [req|spec|all]
|
|
91
97
|
```
|
|
92
98
|
|
|
93
99
|
Then output this hard-stop message and **wait for a text reply from the user**:
|
|
@@ -115,8 +121,8 @@ The pipeline CANNOT continue until you reply directly.
|
|
|
115
121
|
|
|
116
122
|
| Document | Status | Date | Notes |
|
|
117
123
|
|----------|--------|------|-------|
|
|
118
|
-
| {
|
|
119
|
-
| {
|
|
124
|
+
| {requirements.path} | ✅ Approved | {today} | {notes or —} |
|
|
125
|
+
| {tech_spec.path} | ✅ Approved | {today} | {notes or —} |
|
|
120
126
|
| {PREFIX}-Validation-Report.md | ✅ Approved | {today} | {notes or —} |
|
|
121
127
|
|
|
122
128
|
**Docs Digest:** `{docs_digest}`
|