@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.
Files changed (35) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/lib/agent-registry.js +39 -0
  3. package/package.json +1 -1
  4. package/src/claude/agents/gaia-generate-requirements-use-case.md +97 -0
  5. package/src/claude/agents/gaia-generate-requirements.md +22 -27
  6. package/src/claude/agents/gaia-generate-tech-spec-component.md +93 -0
  7. package/src/claude/agents/gaia-generate-tech-spec.md +47 -103
  8. package/src/claude/agents/gaia-generate-work-breakdown-task.md +151 -0
  9. package/src/claude/agents/gaia-generate-work-breakdown.md +124 -105
  10. package/src/claude/agents/gaia-validate-feature-docs.md +13 -8
  11. package/src/claude/agents/gaia-validate-work-breakdown-semantic.md +9 -4
  12. package/src/claude/commands/assemble-docs.md +16 -0
  13. package/src/claude/commands/feature-status.md +9 -4
  14. package/src/claude/scripts/ac-task-matrix.js +121 -0
  15. package/src/claude/scripts/assemble-docs.js +66 -0
  16. package/src/claude/scripts/req-assembler.js +127 -0
  17. package/src/claude/scripts/req-graph-checks.js +71 -0
  18. package/src/claude/scripts/req-skeleton-check.js +64 -0
  19. package/src/claude/scripts/tech-spec-assembler.js +188 -0
  20. package/src/claude/scripts/tech-spec-graph-checks.js +84 -0
  21. package/src/claude/scripts/templates/gaia-component-template.md +64 -0
  22. package/src/claude/scripts/templates/gaia-dependency-graph-template.md +85 -0
  23. package/src/claude/scripts/templates/gaia-requirements-index-template.md +144 -0
  24. package/src/claude/scripts/templates/gaia-task-template.md +92 -0
  25. package/src/claude/scripts/templates/gaia-tech-spec-index-template.md +137 -0
  26. package/src/claude/scripts/templates/gaia-use-case-template.md +56 -0
  27. package/src/claude/scripts/templates/gaia-user-story-template.md +61 -0
  28. package/src/claude/scripts/wb-graph-checks.js +754 -0
  29. package/src/claude/scripts/wb-render.js +6 -11
  30. package/src/claude/scripts/wb-skeleton-check.js +99 -0
  31. package/src/claude/scripts/wb-validate.js +49 -655
  32. package/src/claude/scripts/wbs-assembler.js +368 -0
  33. package/src/claude/skills/implement-feature/SKILL.md +12 -6
  34. package/src/claude/workflows/pm-phase1.js +324 -18
  35. package/src/claude/workflows/pm-phase2.js +358 -15
@@ -0,0 +1,188 @@
1
+ #!/usr/bin/env node
2
+ 'use strict'
3
+
4
+ // FTR-024: loader/renderer for the split Tech-Spec format (spec/index.md +
5
+ // spec/components/*.md). Reconstructs the exact 12-section monolithic shape
6
+ // gaia-generate-tech-spec already produced in one turn before this feature: Section 9 rebuilt
7
+ // from the skeleton's own Components table (authoritative, restated from each component, never
8
+ // re-derived); Sections 3 (Backend)/4 (Frontend)/5 (External Integrations) populated per
9
+ // component Domain routing; Section 10 (Testing Strategy) built from every component's own
10
+ // Tests subsection, unconditionally. Legacy input (a single monolithic file) is returned
11
+ // unchanged.
12
+
13
+ const fs = require('fs')
14
+ const path = require('path')
15
+ const { findSectionRange, parseTableRows } = require('./req-assembler')
16
+
17
+ const SECTION_BY_DOMAIN = { BE: 3, FE: 4 } // anything else (DB/DevOps/INFRA/TEST) routes to 5
18
+
19
+ class AssemblyError extends Error {}
20
+
21
+ // Same helper tech-spec-graph-checks.js already has (FTR-023) — duplicated here deliberately,
22
+ // not imported: this script reads 5-column rows for a structurally different purpose (section
23
+ // routing, not Addresses validation), and importing a function named for a different module's
24
+ // job would read as a false shared contract. Same precedent as wbs-assembler.js independently
25
+ // reimplementing plan.js's field-parsing convention (FTR-020).
26
+ function findSubsectionRange(lines, start, end, heading) {
27
+ let subStart = -1
28
+ for (let i = start; i < end; i++) {
29
+ if (lines[i].trim() === `### ${heading}`) { subStart = i; break }
30
+ }
31
+ if (subStart === -1) return null
32
+ let subEnd = end
33
+ for (let i = subStart + 1; i < end; i++) {
34
+ if (/^#{2,3}\s/.test(lines[i])) { subEnd = i; break }
35
+ }
36
+ return { start: subStart, end: subEnd }
37
+ }
38
+
39
+ function parseComponentsTable(indexContent) {
40
+ const { start, end, lines } = findSectionRange(indexContent, 9)
41
+ const entries = []
42
+ // changeType ('New'/'Modified') comes from WHICH table a row was found in, not from any
43
+ // column's text — the "Purpose-or-Change" column is free-text (e.g. "does backend stuff"),
44
+ // never literally the word "New"/"Modified". A real bug, found only by actually assembling a
45
+ // hand-built fixture and inspecting Section 9's rebuilt New/Modified split: every row landed
46
+ // under "Modified" because `change.toLowerCase() === 'new'` almost never matched free text.
47
+ for (const [heading, changeType] of [['New files', 'New'], ['Modified files', 'Modified']]) {
48
+ const sub = findSubsectionRange(lines, start, end, heading)
49
+ if (!sub) continue
50
+ for (const cells of parseTableRows(lines, sub.start, sub.end)) {
51
+ // Column order: Addresses MUST stay 3rd (index 2) — the exact position
52
+ // tech-spec-graph-checks.js's unmodified, positional parser already reads from a
53
+ // 3-column legacy table (FTR-023). Domain/Spec are new trailing columns.
54
+ const [filePath, purposeOrChange, addresses, domain, specLink] = cells
55
+ const m = specLink && specLink.match(/\(components\/([^)]+)\)/)
56
+ if (!m) throw new AssemblyError(`malformed Spec column for "${filePath}" in spec/index.md`)
57
+ entries.push({ filePath, purposeOrChange, changeType, domain, addresses, fileSlug: m[1] })
58
+ }
59
+ }
60
+ return entries
61
+ }
62
+
63
+ // Parses one component file into { change, body, tests }: `body` is everything between
64
+ // "## Change" (exclusive of its own heading) and "## Tests" — the component's own
65
+ // Algorithm/Schema content, whatever heading it actually used (the component template's own
66
+ // section-naming is a free-text "menu, not a checklist" — this parser does not care what that
67
+ // heading is literally called, only that it comes after Change and before Tests).
68
+ //
69
+ // Fence-aware: a component's own Change/body/Tests content may legitimately quote another
70
+ // file's literal "## Heading" line inside a fenced code example (e.g. a before/after excerpt of
71
+ // a markdown prompt file being modified, exactly the shape FTR-025's own spec/components/
72
+ // gaia-validate-feature-docs.md.md needed) — a naive line scan mistook that quoted line for a
73
+ // real section boundary, corrupting the assembled Section 3/4/5/10 content. Lines inside a
74
+ // fence are never treated as a heading, regardless of whether the fence appears before or after
75
+ // the real heading in the file.
76
+ //
77
+ // Length-aware, not a plain open/close boolean toggle: real CommonMark fences close only on a
78
+ // backtick run at least as long as the one that opened them, which is exactly what lets a
79
+ // longer outer fence (e.g. ````markdown) legitimately contain a shorter inner one (```) as a
80
+ // nested quoted example — a simple boolean toggle miscounts any such nesting (every backtick
81
+ // run flips it, regardless of length), landing in the wrong state for the rest of the file the
82
+ // moment the total run count is odd. Found exactly this way: FTR-025's own spec/components/
83
+ // SKILL.md.md quotes a before/after Markdown example that itself contains a fenced block.
84
+ function _nonFencedHeadingIndexes(lines, predicate) {
85
+ let fenceLen = 0 // 0 = not in a fence; otherwise the backtick-run length that opened it
86
+ const indexes = []
87
+ for (let i = 0; i < lines.length; i++) {
88
+ const m = lines[i].match(/^(`{3,})/)
89
+ if (m) {
90
+ const runLen = m[1].length
91
+ if (fenceLen === 0) fenceLen = runLen // opens a new fence
92
+ else if (runLen >= fenceLen) fenceLen = 0 // a run at least as long as the opener closes it
93
+ // a shorter run while already fenced is just literal backtick content inside the fence
94
+ continue
95
+ }
96
+ if (fenceLen !== 0) continue
97
+ if (predicate(lines[i])) indexes.push(i)
98
+ }
99
+ return indexes
100
+ }
101
+ function parseComponentDetail(content) {
102
+ const lines = content.split('\n')
103
+ const changeIdx = _nonFencedHeadingIndexes(lines, l => l.trim() === '## Change')[0] ?? -1
104
+ const testsIdx = _nonFencedHeadingIndexes(lines, l => l.trim() === '## Tests')[0] ?? -1
105
+ if (changeIdx === -1) throw new AssemblyError('component file is missing its "## Change" section')
106
+ if (testsIdx === -1) throw new AssemblyError('component file is missing its "## Tests" section')
107
+ const nextHeadingIdx = _nonFencedHeadingIndexes(lines, l => /^##\s/.test(l)).find(i => i > changeIdx)
108
+ const changeEnd = nextHeadingIdx === undefined ? lines.length : nextHeadingIdx
109
+ return {
110
+ change: lines.slice(changeIdx + 1, changeEnd).join('\n').trim(),
111
+ body: lines.slice(changeEnd, testsIdx).join('\n').trim(),
112
+ tests: lines.slice(testsIdx + 1).join('\n').trim(),
113
+ }
114
+ }
115
+
116
+ // Replaces everything between "## {n}. " and the next "## " heading with `body`, keeping the
117
+ // heading line itself — same splice primitive as req-assembler.js's spliceSection2, generalized
118
+ // to any section number instead of hardcoding 2.
119
+ function spliceSectionBody(doc, n, body) {
120
+ const { start, end, lines } = findSectionRange(doc, n)
121
+ const heading = lines[start]
122
+ const before = lines.slice(0, start).concat([heading]).join('\n')
123
+ const after = lines.slice(end).join('\n')
124
+ return `${before}\n\n${body}\n\n${after}`
125
+ }
126
+
127
+ function rewriteSection9(doc, components) {
128
+ const newFiles = components.filter(c => c.changeType === 'New')
129
+ const modifiedFiles = components.filter(c => c.changeType === 'Modified')
130
+ const table = rows => '| Path | Purpose-or-Change | Addresses |\n|---|---|---|\n' +
131
+ rows.map(c => `| ${c.filePath} | ${c.purposeOrChange} | ${c.addresses} |`).join('\n')
132
+ const body = `### New files\n${table(newFiles)}\n\n### Modified files\n${table(modifiedFiles)}`
133
+ return spliceSectionBody(doc, 9, body)
134
+ }
135
+
136
+ function loadTechSpec(inputPath) {
137
+ const stat = fs.statSync(inputPath, { throwIfNoEntry: false })
138
+ if (!stat) throw new AssemblyError(`path does not exist: ${inputPath}`)
139
+ if (stat.isFile()) return fs.readFileSync(inputPath, 'utf8') // legacy path
140
+
141
+ const indexPath = path.join(inputPath, 'index.md')
142
+ if (!fs.existsSync(indexPath)) throw new AssemblyError(`missing spec/index.md at ${indexPath}`)
143
+ const indexContent = fs.readFileSync(indexPath, 'utf8')
144
+ const components = parseComponentsTable(indexContent)
145
+ if (components.length === 0) throw new AssemblyError('spec/index.md has no rows in its Components table')
146
+
147
+ const detailByPath = {}
148
+ for (const c of components) {
149
+ const compPath = path.join(inputPath, 'components', c.fileSlug)
150
+ if (!fs.existsSync(compPath)) throw new AssemblyError(`component "${c.filePath}" has no detail file at ${compPath}`)
151
+ detailByPath[c.filePath] = parseComponentDetail(fs.readFileSync(compPath, 'utf8'))
152
+ }
153
+
154
+ let doc = rewriteSection9(indexContent, components)
155
+ for (const sectionNum of [3, 4, 5]) {
156
+ const matching = components.filter(c => (SECTION_BY_DOMAIN[c.domain] || 5) === sectionNum)
157
+ const body = matching.length
158
+ ? matching.map(c => `### ${c.filePath}\n\n${detailByPath[c.filePath].body}`).join('\n\n')
159
+ : '_No components in this category._'
160
+ doc = spliceSectionBody(doc, sectionNum, body)
161
+ }
162
+ const testsBody = components.map(c => `### ${c.filePath}\n\n${detailByPath[c.filePath].tests}`).join('\n\n')
163
+ doc = spliceSectionBody(doc, 10, testsBody)
164
+ return doc
165
+ }
166
+
167
+ if (require.main === module) {
168
+ const inputPath = process.argv[2]
169
+ if (!inputPath) {
170
+ process.stderr.write('Usage: node tech-spec-assembler.js <path-to-spec-dir-or-legacy-file>\n')
171
+ process.exit(1)
172
+ }
173
+ try {
174
+ process.stdout.write(loadTechSpec(path.resolve(inputPath)))
175
+ } catch (err) {
176
+ process.stderr.write(`Error: ${err.message}\n`)
177
+ process.exit(err instanceof AssemblyError ? 1 : 2)
178
+ }
179
+ }
180
+
181
+ module.exports = {
182
+ loadTechSpec,
183
+ parseComponentsTable,
184
+ parseComponentDetail,
185
+ spliceSectionBody,
186
+ findSubsectionRange,
187
+ AssemblyError,
188
+ }
@@ -0,0 +1,84 @@
1
+ #!/usr/bin/env node
2
+ 'use strict'
3
+
4
+ // FTR-023: deterministic structural check that every Tech-Spec File Inventory "Addresses"
5
+ // reference resolves to a real Requirements AC ID. Combined parse/check/CLI in one file — unlike
6
+ // wb-graph-checks.js/req-graph-checks.js (several checks, multiple consumers), this feature has
7
+ // exactly one check used in exactly one context, so a separate checks-module + CLI-wrapper split
8
+ // isn't justified.
9
+
10
+ const fs = require('fs')
11
+ const path = require('path')
12
+ const { findSectionRange, parseTableRows } = require('./req-assembler')
13
+ const { parseAcTable } = require('./wb-graph-checks')
14
+
15
+ // Locates a "### {heading}" line within [start, end) and returns its own sub-range, up to the
16
+ // next "###" or "##" heading (or `end`). Tech-Spec-specific: req-assembler.js's
17
+ // findSectionRange only knows about numbered "## N." headings, one level up from this.
18
+ function findSubsectionRange(lines, start, end, heading) {
19
+ let subStart = -1
20
+ for (let i = start; i < end; i++) {
21
+ if (lines[i].trim() === `### ${heading}`) { subStart = i; break }
22
+ }
23
+ if (subStart === -1) return null
24
+ let subEnd = end
25
+ for (let i = subStart + 1; i < end; i++) {
26
+ if (/^#{2,3}\s/.test(lines[i])) { subEnd = i; break }
27
+ }
28
+ return { start: subStart, end: subEnd }
29
+ }
30
+
31
+ function parseFileInventoryEntries(techSpecContent) {
32
+ const { start, end, lines } = findSectionRange(techSpecContent, 9)
33
+ const entries = []
34
+ for (const heading of ['New files', 'Modified files']) {
35
+ const sub = findSubsectionRange(lines, start, end, heading)
36
+ if (!sub) continue // a Tech-Spec with only one of the two tables is valid (e.g. a pure-addition feature)
37
+ for (const cells of parseTableRows(lines, sub.start, sub.end)) {
38
+ const filePath = cells[0]
39
+ const addressesCell = cells[2] // Path | Purpose-or-Change-description | Addresses
40
+ const acIds = (!addressesCell || addressesCell.trim() === '—') ? []
41
+ : addressesCell.split(',').map(s => s.trim()).filter(Boolean)
42
+ entries.push({ filePath, acIds })
43
+ }
44
+ }
45
+ return entries
46
+ }
47
+
48
+ function checkAddressesResolve(entries, validAcIds) {
49
+ const idSet = new Set(validAcIds)
50
+ const errors = []
51
+ for (const entry of entries) {
52
+ for (const acId of entry.acIds) {
53
+ if (!idSet.has(acId)) {
54
+ errors.push({
55
+ category: 'DANGLING_TECH_SPEC_AC_REFERENCE', severity: 'error',
56
+ filePath: entry.filePath, acId,
57
+ message: `File "${entry.filePath}"'s "Addresses" column references unknown AC "${acId}"`,
58
+ })
59
+ }
60
+ }
61
+ }
62
+ return errors
63
+ }
64
+
65
+ if (require.main === module) {
66
+ const [techSpecPath, requirementsPath] = process.argv.slice(2)
67
+ if (!techSpecPath || !requirementsPath) {
68
+ process.stderr.write('Usage: node tech-spec-graph-checks.js <tech-spec-path> <requirements-path>\n')
69
+ process.exit(2)
70
+ }
71
+ let entries, acMap
72
+ try {
73
+ entries = parseFileInventoryEntries(fs.readFileSync(path.resolve(techSpecPath), 'utf8'))
74
+ acMap = parseAcTable(path.resolve(requirementsPath))
75
+ } catch (e) {
76
+ console.log(JSON.stringify({ valid: false, errors: [{ message: `parse failure: ${e.message}` }] }))
77
+ process.exit(2)
78
+ }
79
+ const errors = checkAddressesResolve(entries, [...acMap.keys()])
80
+ console.log(JSON.stringify({ valid: errors.length === 0, errors }, null, 2))
81
+ process.exit(errors.length === 0 ? 0 : 1)
82
+ }
83
+
84
+ module.exports = { parseFileInventoryEntries, checkAddressesResolve, findSubsectionRange }
@@ -0,0 +1,64 @@
1
+ <!--
2
+ COMPONENT TEMPLATE — spec/components/{fileSlug}.md
3
+
4
+ Canonical structure for one component's technical design in the split Tech-Spec format (FTR-024).
5
+ A "component" is one file this feature creates or modifies — the granularity is the same as one
6
+ row in spec/index.md's own Section 9 "File Inventory" / Components table, not an arbitrary
7
+ subdivision.
8
+
9
+ This is the REAL, currently-implemented contract — authoritative source:
10
+ `src/claude/agents/gaia-generate-tech-spec-component.md`'s own "Output" section, and
11
+ `src/claude/scripts/tech-spec-assembler.js`'s `parseComponentDetail`, which requires exactly
12
+ `## Change` and `## Tests` to exist, in that relative order, to parse a component file correctly.
13
+ An earlier FTR-020 draft of this template (component-template.md) additionally specified
14
+ `Related Open Questions`/`Related components`/`Addresses` header lines and a `Decision:` section —
15
+ none of those exist in the real, shipped format; this file reflects what is actually generated and
16
+ parsed today, not that earlier draft.
17
+
18
+ Fill in every placeholder (angle brackets) and delete every HTML comment block (including this
19
+ one) before treating the file as finished.
20
+ -->
21
+
22
+ # {Path} — {New | Modified}
23
+
24
+ ## Change
25
+
26
+ <!--
27
+ One paragraph: what changes about this file and why — name the specific functions/sections
28
+ affected, not just "this file is updated to support the new feature". If New: what it exports and
29
+ the shape of its inputs/outputs. If Modified: what specifically is added, replaced, or removed,
30
+ and what is explicitly left unchanged.
31
+ -->
32
+
33
+ <prose>
34
+
35
+ ## {Algorithm | Data Model | Service Interface | Component Props | whichever name fits this
36
+ component's own real nature — pick one, do not invent multiple competing headings}
37
+
38
+ <!--
39
+ Real pseudocode or real code, not a prose description of "what it does". If this component
40
+ relocates logic from an existing file, name the exact source file and line ranges if known.
41
+
42
+ If this component genuinely needs no design content beyond Change (e.g. a trivial configuration
43
+ file), omit this entire middle heading rather than inventing content — Change and Tests are always
44
+ required, this one is not.
45
+ -->
46
+
47
+ <pseudocode/code + prose explaining non-obvious choices>
48
+
49
+ ## Tests
50
+
51
+ <!--
52
+ The specific test file(s) this component's change requires, and the key cases that matter — not a
53
+ generic "add tests for X".
54
+ -->
55
+
56
+ <prose / bullet list of test file(s) and key cases>
57
+
58
+ <!--
59
+ PARSING CONTRACT — read by tech-spec-assembler.js's parseComponentDetail: `## Change` and
60
+ `## Tests` must both exist, in that relative order; everything between them (including the middle
61
+ heading above, whatever it's actually called) is this component's own design body, concatenated
62
+ verbatim into the assembled Tech-Spec's Section 3 (Backend) / 4 (Frontend) / 5 (External
63
+ Integrations), routed by this component's own Domain from spec/index.md's Components table.
64
+ -->
@@ -0,0 +1,85 @@
1
+ <!--
2
+ DEPENDENCY GRAPH TEMPLATE — developer-plan/dependency-graph.md
3
+
4
+ STATUS (as of FTR-025): reference/design-only. No generator currently produces this file — grep
5
+ for "dependency-graph"/"dependencyGraph" across lib/ and src/claude/ turns up nothing that writes
6
+ it. Kept here, moved and renamed alongside the other six templates, for whenever the
7
+ developer-plan/dependency-graph.md concept is actually implemented; do not assume it is wired into
8
+ any real pipeline today.
9
+
10
+ Human-readable companion to developer-plan/plan.json, produced BY THE SAME GENERATOR that writes
11
+ plan.json — never hand-authored per feature, and never a place to restate plan.json's own arrays.
12
+ This file is pure output: nothing reads it back programmatically (unlike the wbs/ task and phase
13
+ templates, there is no parsing contract here — delete this whole comment block in a generated
14
+ instance, there is nothing downstream to keep honest).
15
+
16
+ Structure, in this fixed order: (1) a one-line pointer note, (2) the Mermaid dependency graph —
17
+ the ONLY data visualization this file keeps, because a diagram is a genuinely different
18
+ representation of plan.json's `readyQueue[].dependsOn`, not the same facts reformatted as another
19
+ table, (3) a fixed "Sequence and waves" section that explicitly refuses to reprint
20
+ `readyQueue`/`phaseWaves` as Markdown tables, (4) a fixed "Integrity" section. An earlier draft of
21
+ this file DID reprint the ready-queue and phase-waves as tables and was reverted after review —
22
+ same facts in two places drifts the moment plan.json regenerates and this file doesn't. Do not
23
+ reintroduce those tables.
24
+
25
+ Placeholders:
26
+ {FEATURE} feature ID, e.g. FTR-020
27
+ {PHASE_ID} a phase ID from plan.json's phases[].id
28
+ {TASK_ID} a task ID from plan.json's readyQueue[].id
29
+ {NODE_ID} {TASK_ID} with every hyphen removed/underscored so it's a legal Mermaid node
30
+ name (Mermaid node IDs can't contain hyphens) — drop the phase prefix once the
31
+ node already lives inside that phase's subgraph, drop the literal "TASK", keep
32
+ it short and unique within the subgraph (e.g. US-02-TASK-BE-01 -> US02_BE01)
33
+ {sourceDigest} plan.json's own sourceDigest value, copied verbatim, including the "sha256:"
34
+ prefix
35
+ -->
36
+
37
+ # {FEATURE} — Dependency Graph & Implementation Sequence
38
+
39
+ > Human-readable companion to [`plan.json`](plan.json) — the machine-readable file is
40
+ > authoritative; this is a rendered view of the same data, regenerated alongside it, never
41
+ > hand-edited independently (same convention as `{PREFIX}-Work-Breakdown.md` being generated
42
+ > from the JSON it describes — see `wb-render.js`).
43
+
44
+ ## Dependency graph
45
+
46
+ <!--
47
+ One `subgraph {PHASE_ID}` block per phase, in plan.json's phases[] order, listing that phase's
48
+ own taskIds as Mermaid nodes ({NODE_ID}[{TASK_ID}]). Below every subgraph block, one
49
+ `{FROM_NODE_ID} --> {TO_NODE_ID}` edge per (task, dependency) pair found by walking every
50
+ readyQueue[].dependsOn entry — one edge per dependency, not one edge per task (a task with 3
51
+ entries in dependsOn gets 3 edges). Edge order: by readyQueue[].sequence, then by the order the
52
+ dependency appears inside that task's own dependsOn array. A task with an empty dependsOn
53
+ contributes no edges, only its node.
54
+ -->
55
+
56
+ ```mermaid
57
+ graph TD
58
+ subgraph {PHASE_ID}
59
+ {NODE_ID}[{TASK_ID}]
60
+ end
61
+
62
+ {FROM_NODE_ID} --> {TO_NODE_ID}
63
+ ```
64
+
65
+ ## Sequence and waves
66
+
67
+ The dispatch order (`readyQueue`), the per-task dependencies (`dependsOn`), and the phase
68
+ waves (`phaseWaves`) are **not repeated here** — they already exist as structured data in
69
+ [`plan.json`](plan.json) and reprinting them as a Markdown table would just be the same facts
70
+ in a second place, free to drift the moment `plan.json` is regenerated and this file isn't.
71
+ Read them straight from there; the Mermaid diagram above is the only rendering kept in this
72
+ file, because it's a genuinely different representation (visual graph) rather than the same
73
+ rows reformatted.
74
+
75
+ ## Integrity
76
+
77
+ - **Source digest:** `{sourceDigest}` — computed over every file under `wbs/` (sorted by
78
+ relative path, each length-prefixed before hashing, same technique as
79
+ `lib/task-executor/plan.js`'s own `_computePlanDigest`, extended from "two byte blobs" to
80
+ "every file in a directory"). A re-run of the same computation against the current `wbs/`
81
+ must reproduce this exact value, or this plan is stale and must be regenerated before
82
+ implementation proceeds — mirrors `plan.js`'s existing resume-safety contract, made an
83
+ explicit, reviewable file instead of an internal runtime-only check.
84
+ - Per-task and per-phase checksums in `plan.json` let a drift check point at the *specific*
85
+ file that changed, not just "something changed somewhere."
@@ -0,0 +1,144 @@
1
+ <!--
2
+ REQUIREMENTS INDEX TEMPLATE — req/index.md
3
+
4
+ Canonical structure for the Requirements index in the split format (FTR-021). Holds everything
5
+ that is NOT naturally owned by a single Use Case — i.e. everything except each Use Case's own flow
6
+ body, which gets its own file under use-cases/{UC-ID}.md (see gaia-use-case-template.md).
7
+
8
+ This is the REAL, currently-implemented 9-section contract — authoritative source:
9
+ `src/claude/agents/gaia-generate-requirements.md`'s own "Output Structure" section, and
10
+ `src/claude/scripts/req-skeleton-check.js`/`req-assembler.js`, which require exactly these 9
11
+ numbered top-level `## N. ` headings, in order, to exist. An earlier FTR-020 exploratory draft of
12
+ this template used a different 7-section numbering (no "UI Requirements" or "Open Questions" as
13
+ their own sections) that was never what Requirements generation actually shipped — this file
14
+ reflects the real, current format, not that earlier draft.
15
+
16
+ SPLITTING PRINCIPLE (apply this test to any new section before deciding where it goes): does this
17
+ content belong to exactly one UC, or does it reference/apply to several? A Business Rule that
18
+ applies to 3 UCs, a Data Requirement describing a schema used by the whole feature, an NFR that
19
+ constrains the feature as a whole — none of these are owned by one UC any more than a shared setup
20
+ module is owned by one Work Breakdown task. Only split out content that is genuinely per-item;
21
+ everything cross-cutting stays here, in one place.
22
+
23
+ EVERY table in this document (and in every use-case file) MUST be followed immediately by a
24
+ one-line legend (blockquote) explaining each column.
25
+
26
+ Fill in every placeholder (angle brackets) and delete every HTML comment block (including this
27
+ one) before treating the file as finished.
28
+ -->
29
+
30
+ # {PREFIX} — Functional Requirements (Index)
31
+
32
+ ## 1. Introduction
33
+
34
+ ### 1.1 Purpose
35
+
36
+ <prose: brief description of what this requirements document covers>
37
+
38
+ ### 1.2 Scope
39
+
40
+ <prose: what is in scope and out of scope>
41
+
42
+ ### 1.3 Actors
43
+
44
+ | Actor | Description |
45
+ |-------|-------------|
46
+ | <actor> | <description> |
47
+
48
+ ## 2. Use Cases
49
+
50
+ <!-- One row per UC. This table is the single place that lists every UC that exists — the
51
+ use-cases/ directory listing must match it exactly. Title here is a short label, NOT the
52
+ full flow description — that's in the UC's own file (FTR-021: a separate call per UC fills
53
+ in each row's full detail after this skeleton is accepted). -->
54
+
55
+ | UC ID | Title | Actor | Priority | File |
56
+ |-------|-------|-------|----------|------|
57
+ | UC-{NN} | <short title> | <actor(s)> | <Must \| Should \| Could> | [use-cases/UC-{NN}.md](use-cases/UC-{NN}.md) |
58
+
59
+ > **Legend:** **UC ID** — unique identifier, referenced from Business Rules' `Applies to` and
60
+ > Acceptance Criteria's `Related UC` columns. **Title** — short label; full flow is in the linked
61
+ > file. **Actor** — who/what initiates or participates. **Priority** — MoSCoW; drives downstream
62
+ > Must-coverage checks. **File** — this UC's own detail file.
63
+
64
+ ## 3. Business Rules
65
+
66
+ | ID | Rule | Applies to |
67
+ |----|------|-----------|
68
+ | BR-{NN} | <rule statement> | <UC-ID, UC-ID, ... \| All UCs> |
69
+
70
+ > **Legend:** **ID** — unique identifier. **Rule** — the constraint itself. **Applies to** —
71
+ > which UC(s) this rule constrains, or "All UCs" if universal.
72
+
73
+ ## 4. Data Requirements
74
+
75
+ ### 4.1 Entities
76
+
77
+ <!-- For each entity: fields, types, constraints, relationships. -->
78
+
79
+ <prose>
80
+
81
+ ### 4.2 Validation Rules
82
+
83
+ | Field | Rule |
84
+ |-------|------|
85
+ | <field> | <rule> |
86
+
87
+ ## 5. Non-Functional Requirements
88
+
89
+ | ID | Category | Requirement |
90
+ |----|----------|-------------|
91
+ | NFR-{NN} | <Security \| Performance \| Usability \| ...> | <requirement statement> |
92
+
93
+ ## 6. UI Requirements
94
+
95
+ <!-- N/A — internal/technical feature; no UI surface is introduced or changed, if that's true for
96
+ this feature — otherwise fill in both subsections below. -->
97
+
98
+ ### 6.1 Pages / Screens
99
+
100
+ <!-- For each page: purpose, layout description, components, interactions. -->
101
+
102
+ <prose>
103
+
104
+ ### 6.2 Navigation Flow
105
+
106
+ <prose: how pages connect and how the user moves between them>
107
+
108
+ ## 7. Acceptance Criteria
109
+
110
+ <!--
111
+ Column contract: first three columns MUST be exactly "ID", "Criterion", "Related UC" in that
112
+ order — parsed as-is by wb-graph-checks.js's parseAcTable (do not reinvent the column format).
113
+ "Related UC" must be a single UC-ID, a comma-separated list, or the literal "All UCs".
114
+ -->
115
+
116
+ | ID | Criterion | Related UC | Priority |
117
+ |----|-----------|-------------|----------|
118
+ | AC-{NN} | <Given/When/Then or plain testable statement> | <UC-ID \| UC-ID, UC-ID \| All UCs> | <Must \| Should \| Could> |
119
+
120
+ > **Legend:** **ID** — unique identifier; the anchor used across the whole
121
+ > feature.md↔req↔spec↔Work-Breakdown traceability chain. **Criterion** — a testable Given/When/Then
122
+ > statement. **Related UC** — which UC(s) this AC belongs to, or "All UCs"; machine-parsed by
123
+ > `wb-graph-checks.js`'s `parseAcTable`, not just descriptive. **Priority** — MoSCoW; every Must AC
124
+ > must be covered by ≥1 WB task.
125
+
126
+ ## 8. Dependencies and Assumptions
127
+
128
+ <!-- External prerequisites, prior features this one builds on, fixed constraints, and anything
129
+ that must be resolved before Gate 1. -->
130
+
131
+ <prose / bullet list>
132
+
133
+ ## 9. Open Questions
134
+
135
+ | # | Question | Impact | Suggested resolution |
136
+ |---|----------|--------|---------------------|
137
+ | <N> | <question> | <impact if unresolved> | <suggested resolution, or "open — deferred"> |
138
+
139
+ <!--
140
+ PARSING CONTRACT — req-skeleton-check.js/req-assembler.js require exactly these 9 numbered
141
+ `## N. ` headings, in order, to exist (assertCompleteSections's truncation guard in pm-phase1.js
142
+ checks the same 9 headings). Section 2's table is the index into use-cases/{UC-ID}.md — never
143
+ write a UC's flow body inline here.
144
+ -->
@@ -0,0 +1,92 @@
1
+ <!--
2
+ TASK DETAIL TEMPLATE — wbs/tasks/{TASK_ID}.md
3
+
4
+ Canonical structure for a task file in the FTR-020 split Work Breakdown format. Every task file
5
+ has exactly three parts, in this order: (1) the field block, (2) Implementation notes,
6
+ (3) Verification commands. Fill in every placeholder below (angle brackets) and delete every
7
+ HTML comment block (including this one) before treating the file as finished. Do not add, remove,
8
+ or reorder the 14 fields — they are parsed positionally as the contiguous run immediately after
9
+ the heading (see "Parsing contract" at the bottom of this file).
10
+ -->
11
+ <a id="task-{TASK_ID}"></a>
12
+ ### {TASK_ID}
13
+
14
+ - **Task ID:** {TASK_ID}
15
+ - **Title:** <Short, specific, action-oriented. Not a restatement of the outcome.>
16
+ - **Outcome:** <The ONE observable thing that exists/changed after this task is done — one
17
+ factual, verifiable sentence. If you need "and" to describe it, the task is probably not
18
+ atomic — split it (see Atomicity Contract in gaia-generate-work-breakdown.md).>
19
+ - **Domain:** <one of: BE | FE | DB | DevOps | INFRA | TEST>
20
+ - **Agent type:** <one of: developer-backend | developer-frontend | developer-testing | review-solution>
21
+ - **Dependencies:** <comma-separated task IDs this task's work requires to exist first, or `—` if none>
22
+ - **Acceptance criteria:** <comma-separated AC IDs from Requirements that this task covers, scoped
23
+ to this task's own phase — or `—` if none>
24
+ - **Estimate — agent minutes:** <integer. Target ≤15 (no flag); 16–20 acceptable (no flag);
25
+ 21–30 is a non-blocking warning — justify in Grouping rationale below; >30 is rejected, split
26
+ the task instead.>
27
+ - **Estimate — tokens:** <integer, rough order of magnitude for the agent invocation>
28
+ - **Output count:** <integer ≥1. Almost always 1 — see Atomicity Contract. >1 requires a non-null
29
+ Grouping rationale below.>
30
+ - **Grouping rationale:** <required (non-`—`) only when Output count >1 or the estimate is in the
31
+ 21–30 min warning band; otherwise `—`>
32
+ - **Commit type:** <conventional-commit type: feat | fix | refactor | test | chore | docs | ...>
33
+ - **Commit scope:** <optional conventional-commit scope, or `—`>
34
+ - **Commit subject:** <imperative, concise, no trailing period — the renderer prepends
35
+ `type(scope): ` automatically, do not include it here>
36
+
37
+ **Implementation notes:**
38
+
39
+ <!--
40
+ This is the part that makes a task file self-sufficient: someone (human or agent) reading ONLY
41
+ this file — not the whole Tech-Spec — should know exactly what to do and how.
42
+
43
+ Write:
44
+ - If this task RELOCATES or MODIFIES existing code: name the exact file(s) and, if you know
45
+ them, the exact current line ranges to copy from or change. Show a before/after snippet, not
46
+ just a description of the change.
47
+ - If this task CREATES something new: give its real signature(s) (function names, parameters,
48
+ return shape) and the actual algorithm as pseudocode or real code — not just a description of
49
+ what it should do.
50
+ - If this task is a TEST: show at least one concrete `test()`/`describe()` block with real
51
+ assertions against the real function/CLI being tested, not a prose list of "things to check".
52
+ - Length target: not 10 lines (too thin to act on without re-deriving the design yourself), not
53
+ 10 pages (defeats the point of a small, bounded, independently generated task). A few concrete
54
+ snippets plus the reasoning that connects them is usually right.
55
+ - It is fine — often good — to include a literal example of this exact task-file format inside a
56
+ fenced code block (e.g. to show another agent's expected output shape). If you do, see the
57
+ parsing-contract warning at the bottom of this file: it is specifically about this situation.
58
+ -->
59
+
60
+ <prose + pseudocode/code here>
61
+
62
+ **Verification commands:**
63
+
64
+ <!--
65
+ One or more real shell commands (use AGENTS.md's actual build/test commands for this project)
66
+ that verify this task's output. Must be non-empty. Each command gets its own fenced block,
67
+ preserved byte-for-byte verbatim — never edited, escaped, or "cleaned up" by any tool that
68
+ touches this file; this is what lets a command contain `|`, backticks, or other shell syntax
69
+ safely.
70
+ -->
71
+
72
+ ```
73
+ <command>
74
+ ```
75
+
76
+ <!--
77
+ PARSING CONTRACT (do not delete this section from the real template file; delete only from a
78
+ filled-in task instance) — read by wbs-assembler.js's parseTaskDetailBlock:
79
+ 1. Field values are read from the CONTIGUOUS RUN of `- **Label:** value` lines starting
80
+ immediately after the `### {TASK_ID}` heading. Scanning STOPS at the first line that is not
81
+ in that shape — so an example block further down in Implementation notes that happens to
82
+ contain `- **Task ID:** ...`-shaped lines is never mistaken for the real fields. (A real bug,
83
+ found and fixed during this feature's own definition — see US-03-TASK-INFRA-01.md and
84
+ Tech-Spec §4.)
85
+ 2. The verification commands are the fenced block(s) after the LAST occurrence of
86
+ "**Verification commands:**" in the file — not the first — for the same reason: an
87
+ illustrative example earlier in the file must never be mistaken for the real section.
88
+ 3. Conclusion for anyone writing Implementation notes: you may safely show an example of this
89
+ format inside your own notes. You may NOT rely on your example being the last occurrence of
90
+ "**Verification commands:**" in the file — the real one (at the bottom) always is, by
91
+ construction, as long as you don't add a second real verification section.
92
+ -->