@dtlabs/ai-toolkit 0.19.0 → 0.19.2
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 +15 -0
- package/bin/cli.js +45 -0
- package/lib/workflow-control.js +67 -1
- package/package.json +1 -1
- package/src/claude/agents/gaia-generate-requirements-use-case.md +11 -8
- package/src/claude/agents/gaia-generate-requirements.md +9 -5
- package/src/claude/agents/gaia-generate-tech-spec-component.md +9 -4
- package/src/claude/agents/gaia-generate-tech-spec.md +9 -5
- package/src/claude/agents/gaia-generate-work-breakdown-task.md +11 -9
- package/src/claude/scripts/tech-spec-assembler.js +12 -1
- package/src/claude/workflows/pm-phase1.js +29 -8
- package/src/claude/workflows/pm-phase2.js +10 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,4 +1,19 @@
|
|
|
1
1
|
## 0.1.4 (2026-07-21)
|
|
2
|
+
## [0.19.2](https://github.com/dtlab-tech/ai-toolkit/compare/v0.19.1...v0.19.2) (2026-10-19)
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
### Bug Fixes
|
|
6
|
+
|
|
7
|
+
* **workflow-control, tech-spec-assembler:** unwrap whole response fences in Markdown documents to prevent content loss ([fe88582](https://github.com/dtlab-tech/ai-toolkit/commit/fe885821bb7b813237a1d53ed827c70e09b6c9d0))
|
|
8
|
+
* **workflow-control, tech-spec-assembler:** unwrap whole response fences in Markdown documents to prevent content loss ([358e484](https://github.com/dtlab-tech/ai-toolkit/commit/358e4847fe74c314c3c1158dbeda08669530c1f8))
|
|
9
|
+
|
|
10
|
+
## [0.19.1](https://github.com/dtlab-tech/ai-toolkit/compare/v0.19.0...v0.19.1) (2026-10-19)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
### Bug Fixes
|
|
14
|
+
|
|
15
|
+
* stop headless workers from Reading their own toolkit templates ([0f2c707](https://github.com/dtlab-tech/ai-toolkit/commit/0f2c7079799cdfb95ab99dcf7333f615a8f49a8e))
|
|
16
|
+
|
|
2
17
|
## [0.19.0](https://github.com/dtlab-tech/ai-toolkit/compare/v0.18.0...v0.19.0) (2026-10-19)
|
|
3
18
|
|
|
4
19
|
|
package/bin/cli.js
CHANGED
|
@@ -2056,6 +2056,51 @@ async function main() {
|
|
|
2056
2056
|
process.stderr.write(err.message + '\n');
|
|
2057
2057
|
process.exit(1);
|
|
2058
2058
|
}
|
|
2059
|
+
} else if (argv[0] === 'print-asset') {
|
|
2060
|
+
// print-asset <relativePath> [--project <dir>] [--home <dir>]
|
|
2061
|
+
// Security constraints: scripts/templates/ subpath only; .md extension only — the
|
|
2062
|
+
// read-only counterpart to run-asset's "scripts category, .js only, execute" contract.
|
|
2063
|
+
//
|
|
2064
|
+
// Exists so a sandboxed workflow script (pm-phase1.js/pm-phase2.js — no `require()`, no
|
|
2065
|
+
// filesystem access outside its own project root) can fetch a toolkit template's raw
|
|
2066
|
+
// text and inline it directly into a worker's prompt, instead of telling the headless
|
|
2067
|
+
// agent to Read the template file itself. That file lives outside the project root for
|
|
2068
|
+
// a globally-installed toolkit, which is exactly the boundary a non-interactive `--print`
|
|
2069
|
+
// session (no TTY to answer a permission prompt) cannot grant — confirmed by a real
|
|
2070
|
+
// worker run failing with "Worker permission denied: ...; tools: Read" against exactly
|
|
2071
|
+
// this path. Inlining via the host (a plain Node process, never subject to the Claude
|
|
2072
|
+
// Code tool-permission system) sidesteps the problem entirely, the same pattern FTR-025
|
|
2073
|
+
// already established for inlining Requirements/Tech-Spec text into gaia-validate-
|
|
2074
|
+
// feature-docs' prompt instead of telling it to Read those files itself.
|
|
2075
|
+
const remaining = argv.slice(1);
|
|
2076
|
+
let relativePath = null;
|
|
2077
|
+
let projectDir = process.cwd();
|
|
2078
|
+
let home;
|
|
2079
|
+
for (let i = 0; i < remaining.length; i++) {
|
|
2080
|
+
if (remaining[i] === '--project' && remaining[i + 1]) { projectDir = remaining[++i]; }
|
|
2081
|
+
else if (remaining[i] === '--home' && remaining[i + 1]) { home = remaining[++i]; }
|
|
2082
|
+
else if (!remaining[i].startsWith('-')) { relativePath = remaining[i]; }
|
|
2083
|
+
}
|
|
2084
|
+
if (!relativePath) {
|
|
2085
|
+
process.stderr.write('Error: print-asset requires a relative path argument\n');
|
|
2086
|
+
process.exit(1);
|
|
2087
|
+
}
|
|
2088
|
+
if (!relativePath.startsWith('scripts/templates/')) {
|
|
2089
|
+
process.stderr.write(`Error: print-asset is restricted to scripts/templates/ (got: ${relativePath})\n`);
|
|
2090
|
+
process.exit(1);
|
|
2091
|
+
}
|
|
2092
|
+
if (!relativePath.endsWith('.md')) {
|
|
2093
|
+
process.stderr.write(`Error: print-asset only reads .md files (got: ${relativePath})\n`);
|
|
2094
|
+
process.exit(1);
|
|
2095
|
+
}
|
|
2096
|
+
try {
|
|
2097
|
+
const assetPath = resolveClaudeRuntimeAsset(relativePath, { projectDir, home });
|
|
2098
|
+
process.stdout.write(fs.readFileSync(assetPath, 'utf8'));
|
|
2099
|
+
process.exit(0);
|
|
2100
|
+
} catch (err) {
|
|
2101
|
+
process.stderr.write(err.message + '\n');
|
|
2102
|
+
process.exit(1);
|
|
2103
|
+
}
|
|
2059
2104
|
} else if (argv[0] === 'list-assets') {
|
|
2060
2105
|
// list-assets --project <dir> --category <name> [--format json|plain] [--home <dir>]
|
|
2061
2106
|
// Both --project and --category are MANDATORY (Tech Spec contract).
|
package/lib/workflow-control.js
CHANGED
|
@@ -106,7 +106,7 @@ function createControl(options) {
|
|
|
106
106
|
}
|
|
107
107
|
function command(argv, accepted = [0]) {
|
|
108
108
|
if (!Array.isArray(argv) || argv.some(a => typeof a !== 'string')) throw new Error('CLI argv array required');
|
|
109
|
-
if (options.home && argv[0] === 'run-asset') {
|
|
109
|
+
if (options.home && (argv[0] === 'run-asset' || argv[0] === 'print-asset')) {
|
|
110
110
|
argv = argv.slice();
|
|
111
111
|
const split = argv.indexOf('--');
|
|
112
112
|
argv.splice(split < 0 ? argv.length : split, 0, '--home', options.home);
|
|
@@ -238,6 +238,13 @@ function createControl(options) {
|
|
|
238
238
|
const candidate = extractDelimitedStructuredOutput(result.result.result, opts.schema);
|
|
239
239
|
if (candidate != null) { payload = candidate; usedTextFallback = true; }
|
|
240
240
|
}
|
|
241
|
+
// A non-schema worker's payload IS the document — unwrap a whole-response fence/preamble
|
|
242
|
+
// before anything downstream (the caller's own write, or this function's own outputTarget
|
|
243
|
+
// write below) ever sees it. See extractMarkdownDocument's own comment for the full story.
|
|
244
|
+
if (!opts.schema && typeof payload === 'string') {
|
|
245
|
+
const cleaned = extractMarkdownDocument(payload);
|
|
246
|
+
if (cleaned !== payload) { payload = cleaned; usedTextFallback = true; }
|
|
247
|
+
}
|
|
241
248
|
if (payload == null || (typeof payload === 'string' && !payload.trim())) {
|
|
242
249
|
if (opts.schema) {
|
|
243
250
|
const fields = (opts.schema.required || []).join(', ') || '(see requested schema)';
|
|
@@ -476,6 +483,65 @@ function extractJsonDocument(text) {
|
|
|
476
483
|
return text;
|
|
477
484
|
}
|
|
478
485
|
|
|
486
|
+
// Same problem extractJsonDocument fixes, for a Markdown document deliverable (a component/
|
|
487
|
+
// use-case/task detail file, or a Requirements/Tech-Spec skeleton) instead of a JSON one: a
|
|
488
|
+
// worker's entire response becomes the file verbatim (these agents have no Write tool, see
|
|
489
|
+
// worker()'s own comment on why — the host writes whatever text they return), and a model that
|
|
490
|
+
// wraps its deliverable in conversational prose and/or an outer code fence silently corrupts the
|
|
491
|
+
// saved file. Found empirically in a real spec/components/*.md file: the agent's own real
|
|
492
|
+
// "## Change"/"## Tests" headings ended up INSIDE an outer ```markdown fence and were (correctly)
|
|
493
|
+
// treated as fenced, non-heading content by tech-spec-assembler.js's own fence-aware parser
|
|
494
|
+
// (FTR-025) — the file wrote to disk with its real content unreachable, surfacing only much later
|
|
495
|
+
// as an opaque "component file is missing its ## Change section" error during final assembly, with
|
|
496
|
+
// no filename attached to say which one (see loadTechSpec's own fix for that half of the bug).
|
|
497
|
+
//
|
|
498
|
+
// Same "try as-is first, re-validate the candidate, never a blind substring guess, never throws"
|
|
499
|
+
// discipline as extractJsonDocument: "valid" here means "has at least one unfenced Markdown
|
|
500
|
+
// heading" — the same fence-length-aware scan tech-spec-assembler.js's own
|
|
501
|
+
// _nonFencedHeadingIndexes uses, reimplemented here as a presence check rather than a line-index
|
|
502
|
+
// list, for the identical "sandboxed runtime has no require()"-shaped reason these two files are
|
|
503
|
+
// already siblings, not shared, elsewhere in this codebase. A document that legitimately ends in
|
|
504
|
+
// its own small fenced code example (a real Tests section's last line) is never mistaken for a
|
|
505
|
+
// whole-response wrap: its real headings already appear BEFORE that trailing fence, unfenced, so
|
|
506
|
+
// the as-is candidate already satisfies this check and no unwrapping is even attempted.
|
|
507
|
+
function _hasUnfencedHeading(text) {
|
|
508
|
+
let fenceLen = 0;
|
|
509
|
+
for (const line of text.split('\n')) {
|
|
510
|
+
const m = line.match(/^(`{3,})/);
|
|
511
|
+
if (m) { const n = m[1].length; fenceLen = fenceLen === 0 ? n : (n >= fenceLen ? 0 : fenceLen); continue; }
|
|
512
|
+
if (fenceLen === 0 && /^#{1,6}\s/.test(line)) return true;
|
|
513
|
+
}
|
|
514
|
+
return false;
|
|
515
|
+
}
|
|
516
|
+
// Line-range based, not regex-based: a regex scan for the "first ``` ... last ```" span would
|
|
517
|
+
// wrongly stop at the first NESTED closing fence instead of the real outer one (exactly the shape
|
|
518
|
+
// a Markdown document's own "## Data Model" section legitimately needs — a fenced JSON example
|
|
519
|
+
// nested inside the model's outer ```markdown wrap). Only the LAST non-blank line being a bare
|
|
520
|
+
// fence-close, matched back to the first fence-open of the SAME backtick-run length, correctly
|
|
521
|
+
// identifies the true outer boundary regardless of what's nested between them.
|
|
522
|
+
function _unwrapWholeResponseFence(text) {
|
|
523
|
+
const lines = text.split('\n');
|
|
524
|
+
let end = lines.length;
|
|
525
|
+
while (end > 0 && lines[end - 1].trim() === '') end--;
|
|
526
|
+
if (end === 0) return null;
|
|
527
|
+
const closeMatch = lines[end - 1].match(/^(`{3,})\s*$/);
|
|
528
|
+
if (!closeMatch) return null;
|
|
529
|
+
const fenceLen = closeMatch[1].length;
|
|
530
|
+
let start = -1;
|
|
531
|
+
for (let i = 0; i < end - 1; i++) {
|
|
532
|
+
const openMatch = lines[i].match(/^(`{3,})\S*\s*$/);
|
|
533
|
+
if (openMatch && openMatch[1].length === fenceLen) { start = i; break; }
|
|
534
|
+
}
|
|
535
|
+
if (start === -1) return null;
|
|
536
|
+
return lines.slice(start + 1, end - 1).join('\n').trim() + '\n';
|
|
537
|
+
}
|
|
538
|
+
function extractMarkdownDocument(text) {
|
|
539
|
+
if (_hasUnfencedHeading(text)) return text;
|
|
540
|
+
const unwrapped = _unwrapWholeResponseFence(text);
|
|
541
|
+
if (unwrapped != null && _hasUnfencedHeading(unwrapped)) return unwrapped;
|
|
542
|
+
return text;
|
|
543
|
+
}
|
|
544
|
+
|
|
479
545
|
function validateSchema(value, schema) {
|
|
480
546
|
if (schema.type === 'object') {
|
|
481
547
|
if (!value || typeof value !== 'object' || Array.isArray(value)) throw new Error('Invalid structured object');
|
package/package.json
CHANGED
|
@@ -23,18 +23,21 @@ The host supplies, in the dispatch prompt:
|
|
|
23
23
|
3. The path to `feature.md` — read it in full before proceeding, to ground this use case's flow
|
|
24
24
|
in the feature's actual described behavior rather than inventing detail the row alone would
|
|
25
25
|
not carry.
|
|
26
|
+
4. The canonical field-table/flow structure, inlined under a `Template (req/use-cases/{UC-ID}.md):`
|
|
27
|
+
label. Use this text — do NOT attempt to Read, Glob, or otherwise search the filesystem for
|
|
28
|
+
`gaia-use-case-template.md` yourself; it is not guaranteed to be reachable from where this agent
|
|
29
|
+
runs, and searching for it is not a safe fallback either.
|
|
26
30
|
|
|
27
31
|
You do not read `req/index.md` yourself and you never decide which use cases exist — that is
|
|
28
32
|
already fixed by the skeleton call that produced the row you were given.
|
|
29
33
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
output contract; stop after Postconditions.
|
|
34
|
+
Note: the inlined template also shows "Related Business Rules"/"Related Acceptance Criteria"
|
|
35
|
+
cross-reference lines at the end of the file — do NOT produce them yourself. The host computes
|
|
36
|
+
both, mechanically, from `req/index.md`'s own Business Rules/Acceptance Criteria tables, and
|
|
37
|
+
appends them to your response after you return it — you are not given that table data (and must
|
|
38
|
+
not re-read `req/index.md` yourself to get it), so any version you wrote would risk being wrong or
|
|
39
|
+
duplicated. The Output section below is your actual, complete output contract; stop after
|
|
40
|
+
Postconditions.
|
|
38
41
|
|
|
39
42
|
## Mandatory instruction
|
|
40
43
|
|
|
@@ -19,11 +19,15 @@ together with those per-use-case files afterward (FTR-021).
|
|
|
19
19
|
|
|
20
20
|
The user provides the path to a `feature.md` file. Read it in full before proceeding.
|
|
21
21
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
22
|
+
The prompt also includes, inlined under a `Template (req/index.md):` label, the canonical,
|
|
23
|
+
authoritative structure for `req/index.md` (section order, exact headings, table column
|
|
24
|
+
contracts, and the parsing contract `req-skeleton-check.js`/`req-assembler.js` rely on). Use the
|
|
25
|
+
inlined text — do NOT attempt to Read, Glob, or otherwise search the filesystem for
|
|
26
|
+
`gaia-requirements-index-template.md` yourself; it is not guaranteed to be reachable from where
|
|
27
|
+
this agent runs, and searching for it is not a safe fallback either (your Glob/Grep tools remain
|
|
28
|
+
available for exploring the feature's own project, just not for locating this template). The
|
|
29
|
+
Output Structure section below summarizes it; the inlined template text is the source of truth if
|
|
30
|
+
the two ever disagree.
|
|
27
31
|
|
|
28
32
|
## Process
|
|
29
33
|
|
|
@@ -28,10 +28,15 @@ The host supplies, in the dispatch prompt:
|
|
|
28
28
|
You do not read `spec/index.md` yourself and you never decide which components exist — that is
|
|
29
29
|
already fixed by the skeleton call that produced the row you were given.
|
|
30
30
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
contract `tech-spec-assembler.js`'s `parseComponentDetail` relies on).
|
|
34
|
-
|
|
31
|
+
The prompt also includes, inlined under a `Template (spec/components/{fileSlug}.md):` label, the
|
|
32
|
+
canonical, authoritative structure for your output (Change/design-body/Tests, and the parsing
|
|
33
|
+
contract `tech-spec-assembler.js`'s `parseComponentDetail` relies on). Use the inlined text — do
|
|
34
|
+
NOT attempt to Read, Glob, or otherwise search the filesystem for `gaia-component-template.md`
|
|
35
|
+
yourself under any circumstances, including as a fallback if you are unsure of its exact path; it
|
|
36
|
+
is not guaranteed to be reachable from where this agent runs, and guessing at or searching for its
|
|
37
|
+
absolute path is not a safe substitute for the text already given to you below. The Output
|
|
38
|
+
section below is a summary; the inlined template text is the source of truth if
|
|
39
|
+
the two ever disagree.
|
|
35
40
|
|
|
36
41
|
## Mandatory instruction
|
|
37
42
|
|
|
@@ -19,11 +19,15 @@ together with those per-component files afterward (FTR-024).
|
|
|
19
19
|
|
|
20
20
|
The user provides the path to a `feature.md` file. Read it in full before proceeding. Also read the relevant existing codebase files to ensure the spec aligns with current architecture.
|
|
21
21
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
22
|
+
The prompt also includes, inlined under a `Template (spec/index.md):` label, the canonical,
|
|
23
|
+
authoritative structure for `spec/index.md` (section order, exact headings, the Section 9
|
|
24
|
+
column-order contract `tech-spec-graph-checks.js` relies on, and the parsing contract
|
|
25
|
+
`tech-spec-assembler.js` relies on). Use the inlined text — do NOT attempt to Read, Glob, or
|
|
26
|
+
otherwise search the filesystem for `gaia-tech-spec-index-template.md` yourself; it is not
|
|
27
|
+
guaranteed to be reachable from where this agent runs, and searching for it is not a safe fallback
|
|
28
|
+
either (your Glob/Grep tools remain available for exploring the project's existing codebase, just
|
|
29
|
+
not for locating this template). The Output Structure section below summarizes it; the inlined
|
|
30
|
+
template text is the source of truth if the two ever disagree.
|
|
27
31
|
|
|
28
32
|
## Step 0 — Read Project Conventions (MANDATORY)
|
|
29
33
|
|
|
@@ -9,11 +9,6 @@ tools: Read, Glob, Grep
|
|
|
9
9
|
|
|
10
10
|
You are a **senior implementation planner**. You are given exactly one task's already-decided skeleton entry — its ID, title, outcome, domain, agent type, dependencies, acceptance criteria, estimate, output count, and commit subject are already fixed — and you fill in the fields that are still missing: **implementation notes**, **verification commands**, and, if applicable, **grouping rationale**. You do not re-decide anything the skeleton already fixed.
|
|
11
11
|
|
|
12
|
-
Read `.claude/scripts/templates/gaia-task-template.md` before writing your response — it is the
|
|
13
|
-
canonical, authoritative structure for your output (field order, the two free-content sections,
|
|
14
|
-
and the exact parsing contract downstream tooling relies on). This section and the Output section
|
|
15
|
-
below summarize it; the template itself is the source of truth if the two ever disagree.
|
|
16
|
-
|
|
17
12
|
This is the **second of two generation passes** (FTR-020). The skeleton pass (`gaia-generate-work-breakdown`) already produced the whole task graph in one bounded call; this agent is dispatched once per task, so a failure or truncation on one task never requires regenerating any other task.
|
|
18
13
|
|
|
19
14
|
---
|
|
@@ -25,8 +20,15 @@ The caller supplies, in the prompt (you do not read `{PREFIX}-Work-Breakdown.jso
|
|
|
25
20
|
1. **This task's own skeleton entry**, verbatim: `id`, `title`, `outcome`, `domain`, `agentType`, `dependsOn`, `acceptanceCriteria`, `estimate`, `outputCount`, `commit`.
|
|
26
21
|
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
22
|
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.
|
|
23
|
+
4. **The canonical output template**, inlined under a `Template (tasks/{TASK_ID}.md):` label — the
|
|
24
|
+
authoritative structure for your output (field order, the two free-content sections, and the
|
|
25
|
+
exact parsing contract downstream tooling relies on). Use the inlined text — do NOT attempt to
|
|
26
|
+
Read, Glob, or otherwise search the filesystem for `gaia-task-template.md` yourself; it is not
|
|
27
|
+
guaranteed to be reachable from where this agent runs, and searching for it is not a safe
|
|
28
|
+
fallback either. This section and the Output section below summarize it; the inlined template
|
|
29
|
+
text is the source of truth if the two ever disagree.
|
|
28
30
|
|
|
29
|
-
If the prompt is missing any of these
|
|
31
|
+
If the prompt is missing any of these four, stop and report exactly what is missing — do not guess at a skeleton entry you were not given.
|
|
30
32
|
|
|
31
33
|
---
|
|
32
34
|
|
|
@@ -142,9 +144,9 @@ Return exactly this block as your final response, and nothing else (no preamble,
|
|
|
142
144
|
```
|
|
143
145
|
|
|
144
146
|
This is the same format already rendered today inside a Work Breakdown's "Task Details"
|
|
145
|
-
section — canonical reference:
|
|
146
|
-
|
|
147
|
-
note applies to your own output too: a naive parser reads only the field lines immediately
|
|
147
|
+
section — canonical reference: the `Template (tasks/{TASK_ID}.md):`-labeled text inlined in your
|
|
148
|
+
own prompt (see Input §4 above; rely on that, not just this summary). That template's
|
|
149
|
+
parsing-contract note applies to your own output too: a naive parser reads only the field lines immediately
|
|
148
150
|
following the heading, and the *last* `**Implementation notes:**`/`**Verification commands:**`
|
|
149
151
|
occurrence — so if you need to illustrate this format inside a longer explanation (e.g. showing
|
|
150
152
|
another agent's expected output shape), make sure your own real sections remain the last
|
|
@@ -148,7 +148,18 @@ function loadTechSpec(inputPath) {
|
|
|
148
148
|
for (const c of components) {
|
|
149
149
|
const compPath = path.join(inputPath, 'components', c.fileSlug)
|
|
150
150
|
if (!fs.existsSync(compPath)) throw new AssemblyError(`component "${c.filePath}" has no detail file at ${compPath}`)
|
|
151
|
-
|
|
151
|
+
try {
|
|
152
|
+
detailByPath[c.filePath] = parseComponentDetail(fs.readFileSync(compPath, 'utf8'))
|
|
153
|
+
} catch (err) {
|
|
154
|
+
// parseComponentDetail's own error names only the symptom ("missing its ## Change
|
|
155
|
+
// section"), never which file — a real diagnosability bug, found when exactly this error
|
|
156
|
+
// surfaced during final assembly with nothing identifying which of ~60 component files was
|
|
157
|
+
// actually malformed (the real culprit here was a whole-response-wrapped-in-a-fence worker
|
|
158
|
+
// reply — see lib/workflow-control.js's extractMarkdownDocument for the fix to that other
|
|
159
|
+
// half of this same incident). Re-thrown with the file identified, never silently swallowed.
|
|
160
|
+
if (err instanceof AssemblyError) throw new AssemblyError(`${err.message} (component "${c.filePath}" at ${compPath})`)
|
|
161
|
+
throw err
|
|
162
|
+
}
|
|
152
163
|
}
|
|
153
164
|
|
|
154
165
|
let doc = rewriteSection9(indexContent, components)
|
|
@@ -36,6 +36,21 @@ return c.run(meta.name, featureDir, prefix, async () => {
|
|
|
36
36
|
const valId = 'gaia.agent.planner.validate-feature-docs'
|
|
37
37
|
const output = (suffix, inputs = [featurePath]) => ({ path: file(suffix), inputs })
|
|
38
38
|
|
|
39
|
+
// Templates live under the toolkit's own installed scripts/templates/ — outside this project
|
|
40
|
+
// for a globally-installed toolkit, which a non-interactive `--print` worker invocation has no
|
|
41
|
+
// way to get permission for (no TTY to answer a prompt; confirmed by a real run failing with
|
|
42
|
+
// "Worker permission denied: ...; tools: Read" against exactly this kind of path). The host
|
|
43
|
+
// reads the template itself (a plain Node process, never subject to the Claude Code
|
|
44
|
+
// tool-permission system) via the dedicated print-asset CLI command, and inlines its text
|
|
45
|
+
// directly into the prompt — the agent is never asked to Read it. Same pattern FTR-025 already
|
|
46
|
+
// established for inlining Requirements/Tech-Spec text into gaia-validate-feature-docs' prompt.
|
|
47
|
+
function readTemplate(templateFile) {
|
|
48
|
+
return c.command(['print-asset', `scripts/templates/${templateFile}`, '--project', c.root]).stdout
|
|
49
|
+
}
|
|
50
|
+
function withTemplate(prompt, label, templateFile) {
|
|
51
|
+
return `${prompt}\n\nTemplate (${label}):\n${readTemplate(templateFile)}`
|
|
52
|
+
}
|
|
53
|
+
|
|
39
54
|
// Deliberately NOT file('req') — same reasoning as FTR-020's wbsDir: this is a directory
|
|
40
55
|
// nested inside the feature's own already-prefixed folder, so a second prefix would be
|
|
41
56
|
// redundant.
|
|
@@ -109,11 +124,12 @@ return c.run(meta.name, featureDir, prefix, async () => {
|
|
|
109
124
|
(relatedACs.length ? relatedACs.join(', ') : '—') + ' (see [index.md](../index.md#7-acceptance-criteria))\n'
|
|
110
125
|
}
|
|
111
126
|
|
|
112
|
-
function buildUcPrompt(row, ucTable, featurePathArg) {
|
|
127
|
+
function buildUcPrompt(row, ucTable, featurePathArg, ucTemplate) {
|
|
113
128
|
const compactIndex = ucTable.map(r => `${r.id}: ${r.title} (${r.actor})`).join('\n')
|
|
114
|
-
|
|
129
|
+
const base = `${featurePathArg}\n\nGenerate the full detail for this use case:\n` +
|
|
115
130
|
`UC ID: ${row.id}\nTitle: ${row.title}\nActor: ${row.actor}\nPriority: ${row.priority}\n\n` +
|
|
116
131
|
`Full use-case index (for cross-UC awareness, do not restate):\n${compactIndex}`
|
|
132
|
+
return `${base}\n\nTemplate (req/use-cases/{UC-ID}.md):\n${ucTemplate}`
|
|
117
133
|
}
|
|
118
134
|
|
|
119
135
|
// forceAll: bypasses resume-by-existence entirely, re-dispatching the skeleton and every
|
|
@@ -144,7 +160,7 @@ return c.run(meta.name, featureDir, prefix, async () => {
|
|
|
144
160
|
// 1. Skeleton (no declared worker() output — host writes it itself, mirroring FTR-020's
|
|
145
161
|
// skeleton step)
|
|
146
162
|
if (regenSkeleton) {
|
|
147
|
-
const skeleton = await c.worker(reqId, featurePath)
|
|
163
|
+
const skeleton = await c.worker(reqId, withTemplate(featurePath, 'req/index.md', 'gaia-requirements-index-template.md'))
|
|
148
164
|
c.writeOutput(reqIndexPath, skeleton)
|
|
149
165
|
}
|
|
150
166
|
|
|
@@ -162,13 +178,15 @@ return c.run(meta.name, featureDir, prefix, async () => {
|
|
|
162
178
|
|
|
163
179
|
// 3. Per-use-case generation, resume-by-existence, sequential
|
|
164
180
|
const ucTable = parseUseCaseIndexTable(c.read(reqIndexPath))
|
|
181
|
+
// Read once, outside the loop — the template is identical for every use case in this run.
|
|
182
|
+
const ucTemplate = ucTable.length ? readTemplate('gaia-use-case-template.md') : null
|
|
165
183
|
for (const row of ucTable) {
|
|
166
184
|
const ucPath = `${reqDir}/${row.file}`
|
|
167
185
|
// If the skeleton itself was just (re)generated, every use case must regenerate too —
|
|
168
186
|
// an existing use-case file would have been written against the OLD skeleton's row
|
|
169
187
|
// (title/actor/priority may have changed) and cannot be trusted as-is.
|
|
170
188
|
if (!regenSkeleton && c.exists(ucPath)) continue // resume: skip, no dispatch
|
|
171
|
-
const detail = await c.worker(reqUcId, buildUcPrompt(row, ucTable, featurePath),
|
|
189
|
+
const detail = await c.worker(reqUcId, buildUcPrompt(row, ucTable, featurePath, ucTemplate),
|
|
172
190
|
{ label: `requirements-use-case:${row.id}` })
|
|
173
191
|
c.writeOutput(ucPath, detail.trimEnd() + ucCrossReferences(c.read(reqIndexPath), row.id))
|
|
174
192
|
}
|
|
@@ -223,11 +241,12 @@ return c.run(meta.name, featureDir, prefix, async () => {
|
|
|
223
241
|
return entries
|
|
224
242
|
}
|
|
225
243
|
|
|
226
|
-
function buildComponentPrompt(row, components, featurePathArg) {
|
|
244
|
+
function buildComponentPrompt(row, components, featurePathArg, componentTemplate) {
|
|
227
245
|
const compactIndex = components.map(c => `${c.filePath} (${c.domain})`).join('\n')
|
|
228
|
-
|
|
246
|
+
const base = `${featurePathArg}\n\nGenerate the full technical design for this component:\n` +
|
|
229
247
|
`Path: ${row.filePath}\nChange: ${row.changeType}\nDomain: ${row.domain}\nAddresses: ${row.addresses}\n\n` +
|
|
230
248
|
`Full component index (for cross-component awareness, do not restate):\n${compactIndex}`
|
|
249
|
+
return `${base}\n\nTemplate (spec/components/{fileSlug}.md):\n${componentTemplate}`
|
|
231
250
|
}
|
|
232
251
|
|
|
233
252
|
// Deterministic, host-computed — never asks the model to pick unique names (same principle
|
|
@@ -279,7 +298,7 @@ return c.run(meta.name, featureDir, prefix, async () => {
|
|
|
279
298
|
|
|
280
299
|
// 1. Skeleton (no declared worker() output — host writes it itself)
|
|
281
300
|
if (regenSkeleton) {
|
|
282
|
-
const skeleton = await c.worker(specId, featurePath)
|
|
301
|
+
const skeleton = await c.worker(specId, withTemplate(featurePath, 'spec/index.md', 'gaia-tech-spec-index-template.md'))
|
|
283
302
|
c.writeOutput(specIndexPath, disambiguateFileSlugs(skeleton))
|
|
284
303
|
}
|
|
285
304
|
|
|
@@ -298,10 +317,12 @@ return c.run(meta.name, featureDir, prefix, async () => {
|
|
|
298
317
|
|
|
299
318
|
// 3. Per-component generation, resume-by-existence, sequential
|
|
300
319
|
const components = parseComponentsTable(c.read(specIndexPath))
|
|
320
|
+
// Read once, outside the loop — the template is identical for every component in this run.
|
|
321
|
+
const componentTemplate = components.length ? readTemplate('gaia-component-template.md') : null
|
|
301
322
|
for (const comp of components) {
|
|
302
323
|
const compPath = `${specDir}/components/${comp.fileSlug}`
|
|
303
324
|
if (!regenSkeleton && c.exists(compPath)) continue // resume: skip, no dispatch
|
|
304
|
-
const detail = await c.worker(specComponentId, buildComponentPrompt(comp, components, featurePath),
|
|
325
|
+
const detail = await c.worker(specComponentId, buildComponentPrompt(comp, components, featurePath, componentTemplate),
|
|
305
326
|
{ label: `tech-spec-component:${comp.fileSlug}` })
|
|
306
327
|
c.writeOutput(compPath, detail)
|
|
307
328
|
}
|
|
@@ -239,7 +239,7 @@ function buildTaskTechSpecExcerpt(components, task, specDir, specIndexContent) {
|
|
|
239
239
|
// implementation awareness), and the Tech-Spec excerpt (FTR-026: already pre-sliced to this
|
|
240
240
|
// task's own relevant component(s) by buildTaskTechSpecExcerpt, or the full legacy Tech-Spec
|
|
241
241
|
// text for a feature with no spec/ directory).
|
|
242
|
-
function buildTaskPrompt(index, taskId, techSpecText) {
|
|
242
|
+
function buildTaskPrompt(index, taskId, techSpecText, taskTemplate) {
|
|
243
243
|
const task = index.tasks[taskId]
|
|
244
244
|
const siblingList = Object.entries(index.tasks)
|
|
245
245
|
.map(([id, t]) => `- ${id}: ${t.title} — ${t.outcome}`).join('\n')
|
|
@@ -247,6 +247,7 @@ function buildTaskPrompt(index, taskId, techSpecText) {
|
|
|
247
247
|
`Task skeleton entry (restate these fields verbatim; do not re-derive them):\n${JSON.stringify({ id: taskId, ...task }, null, 2)}`,
|
|
248
248
|
`Tech-Spec:\n${techSpecText}`,
|
|
249
249
|
`All tasks in this feature (for duplicate-implementation awareness):\n${siblingList}`,
|
|
250
|
+
`Template (tasks/{TASK_ID}.md):\n${taskTemplate}`,
|
|
250
251
|
].join('\n\n')
|
|
251
252
|
}
|
|
252
253
|
|
|
@@ -339,6 +340,13 @@ return c.run(meta.name, featureDir, prefix, async () => {
|
|
|
339
340
|
// own Acceptance Criteria, not the entire assembled spec — computed once, before the loop, by
|
|
340
341
|
// matching each task's acceptanceCriteria against spec/index.md's own Addresses column.
|
|
341
342
|
const components = hasSpec ? parseComponentsTableLocal(c.read(specIndexPath)) : []
|
|
343
|
+
// The template lives under the toolkit's own installed scripts/templates/ — outside the
|
|
344
|
+
// project for a globally-installed toolkit, which a non-interactive worker invocation has no
|
|
345
|
+
// way to get Read permission for. Read once via print-asset, inline into every task's prompt,
|
|
346
|
+
// instead of telling the agent to Read it itself (same reasoning as pm-phase1.js's
|
|
347
|
+
// readTemplate/withTemplate — this runtime duplicates the primitive, not requires it, for the
|
|
348
|
+
// same "no `require`" reason as every other duplicated helper in this file).
|
|
349
|
+
const taskTemplate = c.command(['print-asset', 'scripts/templates/gaia-task-template.md', '--project', c.root]).stdout
|
|
342
350
|
for (const phase of index.phases) {
|
|
343
351
|
for (const taskId of phase.taskIds) {
|
|
344
352
|
const task = index.tasks[taskId]
|
|
@@ -348,7 +356,7 @@ return c.run(meta.name, featureDir, prefix, async () => {
|
|
|
348
356
|
? buildTaskTechSpecExcerpt(components, task, specDir, c.read(specIndexPath))
|
|
349
357
|
: c.read(file('Tech-Spec.md')) // legacy fallback: unchanged, full text
|
|
350
358
|
const detail = await c.worker('gaia.agent.planner.work-breakdown-task',
|
|
351
|
-
buildTaskPrompt(index, taskId, techSpecExcerpt), { label: `work-breakdown-task:${taskId}` })
|
|
359
|
+
buildTaskPrompt(index, taskId, techSpecExcerpt, taskTemplate), { label: `work-breakdown-task:${taskId}` })
|
|
352
360
|
c.writeOutput(detailPath, detail)
|
|
353
361
|
task.status = 'generated'
|
|
354
362
|
c.writeOutput(indexPath, JSON.stringify(index, null, 2)) // persist status after each task
|