@dtlabs/ai-toolkit 0.20.0 → 0.21.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 CHANGED
@@ -1,4 +1,12 @@
1
1
  ## 0.1.4 (2026-07-21)
2
+ ## [0.21.0](https://github.com/dtlab-tech/ai-toolkit/compare/v0.20.0...v0.21.0) (2026-10-20)
3
+
4
+
5
+ ### Features
6
+
7
+ * bound the feature-doc validator to the Tech-Spec skeleton (FTR-028) ([80869ce](https://github.com/dtlab-tech/ai-toolkit/commit/80869ced8b8d24ef7b0669fabbb152e8bf243794))
8
+ * **pm-phase1, agent-registry:** bound the feature-doc validator to the Tech-Spec skeleton (FTR-028) ([9eb7f72](https://github.com/dtlab-tech/ai-toolkit/commit/9eb7f7222ff80eb2b26815a4f58f923f9ca09f1d))
9
+
2
10
  ## [0.20.0](https://github.com/dtlab-tech/ai-toolkit/compare/v0.19.2...v0.20.0) (2026-10-19)
3
11
 
4
12
 
@@ -259,6 +259,19 @@ const CATALOG = [
259
259
  deprecationTarget: null,
260
260
  platforms: ['claude'],
261
261
  },
262
+ {
263
+ agentId: 'gaia.agent.planner.validate-tech-spec-component-detail',
264
+ nativeNames: { claude: 'gaia-validate-tech-spec-component-detail' },
265
+ relativeInstallPath: '.claude/agents/gaia-validate-tech-spec-component-detail.md',
266
+ role: 'planner',
267
+ type: 'validation',
268
+ authorisedPhases: ['planning'],
269
+ allowedPipelines: ['implement-feature'],
270
+ minimumToolkitVersion: '0.13.0',
271
+ deprecated: false,
272
+ deprecationTarget: null,
273
+ platforms: ['claude'],
274
+ },
262
275
  {
263
276
  agentId: 'gaia.agent.planner.define-feature',
264
277
  nativeNames: { claude: 'gaia-define-feature' },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dtlabs/ai-toolkit",
3
- "version": "0.20.0",
3
+ "version": "0.21.0",
4
4
  "description": "AI Toolkit — reusable agents and procedures for developers to build AI-powered applications.",
5
5
  "files": [
6
6
  "docs/",
@@ -16,11 +16,15 @@ A QA agent that cross-references `feature.md` against the Requirements and Tech-
16
16
  1. Extract the prefix from the folder name (`FTR-001-user-management` → `FTR-001`)
17
17
  2. Read `feature.md` fully — its path is given to you first; use your Read tool.
18
18
  3. The Requirements and Tech-Spec document text is included directly below, inlined into this
19
- prompt under `Requirements:` and `Tech-Spec:` labels — do NOT attempt to locate or Read
20
- `{PREFIX}-Requirements.md`/`{PREFIX}-Tech-Spec.md` on disk; they are not guaranteed to exist
21
- (both documents are generated and stored in a split, per-item format by default — `req/`,
22
- `spec/` — and only assembled into a single file when a user explicitly requests it). Use the
23
- inlined text as the complete, authoritative content of each document for this validation pass.
19
+ prompt under `Requirements:` and `Tech-Spec:` labels. The Requirements text is the FULL document
20
+ (every Use Case's own flow detail included). The Tech-Spec text is its SKELETON ONLY — Section
21
+ 9's own component index table (Purpose, Addresses, Domain, Spec-link columns, one row per
22
+ component) plus every feature-wide section (Overview, Architecture, Security, Database,
23
+ Configuration, Implementation Order, Risks). It deliberately does NOT include any component's
24
+ own Change/Algorithm/Schema/Tests design prose — that per-component detail is validated
25
+ separately, on request, via `needsComponentDetail` (Phase 3). Do NOT attempt to locate or Read
26
+ `{PREFIX}-Requirements.md`/`{PREFIX}-Tech-Spec.md` or any `spec/components/*` file on disk;
27
+ use the inlined skeleton as the complete Tech-Spec-level input for this pass.
24
28
  4. If either inlined document's text is empty or missing, record a finding identifying it, skip
25
29
  checks that require it, and complete Phases 6 and 7 with `valid: false`. Never claim full
26
30
  coverage when an input could not be read.
@@ -60,16 +64,26 @@ For each claim extracted in Phase 2, check whether it is addressed in the target
60
64
  | Every out-of-scope item → Out of Scope section | Listed |
61
65
  | Every functional behaviour → at least one Acceptance Criterion | AC in Given/When/Then format |
62
66
 
63
- ### Tech-Spec coverage (against the inlined Tech-Spec text)
67
+ ### Tech-Spec coverage (against the inlined Tech-Spec SKELETON — Section 9's component table and
68
+ feature-wide sections only; no per-component design body is available in this pass)
64
69
 
65
70
  | Check | Pass condition |
66
71
  |-------|---------------|
67
- | Every API endpoint in feature.md → endpoint spec in Tech-Spec | Method, path, request/response documented |
68
- | Every data model field → entity definition in Tech-Spec | Model/class definition present |
69
- | Every new service/component → listed in File Inventory | New files section |
70
- | Every modified file → listed in File Inventory | Modified files section |
71
- | Every Use Case ID from Requirements → referenced in Implementation Order | Traceability present |
72
- | Every external integration → documented in External Integrations | Integration section present |
72
+ | Every new/modified service/component in feature.md → listed in File Inventory | Row present with matching Purpose/Domain |
73
+ | Every external integration → documented in External Integrations (Section 5's own feature-wide prose, if any) | Mentioned explicitly |
74
+ | A claim needs exact technical detail not visible in Purpose/Addresses/Domain (e.g. an endpoint's precise method/path/response shape, a schema field, an error code) | Do NOT guess and do NOT mark it a gap outright. Emit a `needsComponentDetail` entry instead (see below) — never silently drop the check |
75
+
76
+ AC↔component traceability (every Use Case/AC referenced by the right component, and vice versa) is
77
+ NOT this agent's responsibility — it is checked deterministically, by exact ID, by the host after
78
+ this call returns. Do not attempt it here; a prose-based traceability finding in this pass would
79
+ only duplicate or conflict with that exact-ID check.
80
+
81
+ **When to use `needsComponentDetail` vs a normal finding:** if feature.md describes something a
82
+ component row's Purpose plausibly relates to, but you cannot confirm the EXACT technical detail
83
+ without reading that component's own design body, emit a `needsComponentDetail` entry naming that
84
+ row's own Spec-link `fileSlug` and the specific claim — do not guess, and do not silently treat it
85
+ as either covered or missing. If NO component row is even plausibly related to the claim at all,
86
+ that is a normal missing-coverage finding, not a drill-down case.
73
87
 
74
88
  ---
75
89
 
@@ -154,18 +168,34 @@ Compose `{PREFIX}-Validation-Report.md`'s full content following this template:
154
168
  Return a single JSON object with exactly these fields, without Markdown fences or explanatory prose:
155
169
 
156
170
  ```json
157
- {"valid": true, "findings": [], "report": "# Validation Report — FTR-001\n\n## Summary\n..."}
171
+ {"valid": true, "findings": [], "report": "# Validation Report — FTR-001\n\n## Summary\n...", "needsComponentDetail": []}
158
172
  ```
159
173
 
160
174
  For remaining gaps:
161
175
 
162
176
  ```json
163
- {"valid": false, "findings": ["Requirements: UC-01 lacks an acceptance criterion", "Tech-Spec: POST /api/users lacks a response definition"], "report": "# Validation Report — FTR-001\n\n## Summary\n..."}
177
+ {"valid": false, "findings": ["Requirements: UC-01 lacks an acceptance criterion", "Tech-Spec: POST /api/users lacks a response definition"], "report": "# Validation Report — FTR-001\n\n## Summary\n...", "needsComponentDetail": []}
178
+ ```
179
+
180
+ For a claim needing component-body detail:
181
+
182
+ ```json
183
+ {"valid": false, "findings": [], "report": "# Validation Report — FTR-001\n\n## Summary\n...", "needsComponentDetail": [{"claim": "endpoint returns 401 on expired token", "fileSlug": "AuthMiddleware.cs.md"}]}
164
184
  ```
165
185
 
166
186
  - `valid` is a boolean: `true` only when all required inputs were read, coverage is complete, and no unresolved gaps remain.
167
187
  - `findings` is an array of strings, empty only for a clean validation. Missing inputs and ambiguous claims are unresolved findings.
168
188
  - `report` is a non-empty string: the complete Phase 6 document content, verbatim, as the host will write it byte-for-byte to `{PREFIX}-Validation-Report.md`.
189
+ - `needsComponentDetail` is an array of `{ claim, fileSlug }` objects — one per claim you could not
190
+ confirm from the Tech-Spec skeleton alone, naming the specific component (by its Spec-link
191
+ `fileSlug`) whose Purpose/Domain plausibly covers it. Empty when every claim was resolvable from
192
+ the skeleton (the common case for small/medium features). An entry here is NOT the same as a
193
+ `findings` entry — do not also add a `findings` string for the same claim; the host resolves
194
+ `needsComponentDetail` entries separately and may add its own finding if the drill-down fails.
195
+ - A `valid: false` result caused only by `needsComponentDetail` entries (no other findings) is
196
+ still `valid: false` with an empty `findings` array — this is expected, not a contradiction: the
197
+ host treats pending component-detail checks as unresolved until its own drill-down pass clears
198
+ them.
169
199
  - The caller supplies `--json-schema`; use the runtime's structured-output mechanism to return this object in `structured_output`. Plain-text JSON alone does not satisfy that contract.
170
200
  - There is only one deliverable now: the structured verdict, with `report` inside it. There is no separate Write step.
171
201
 
@@ -182,4 +212,4 @@ If a claim is contradictory or its correct resolution is ambiguous, do not guess
182
212
  - **Be specific about gaps** — point to the exact section, claim, and what is missing
183
213
  - **Perform one validation pass per invocation** — return remaining gaps to the host
184
214
  - **Out-of-scope items are also validated** — ensure they are not accidentally included in outputs
185
- - **Traceability is mandatory** — every Use Case ID in Requirements must appear in Tech-Spec
215
+ - **AC↔component traceability is NOT your job** — the host checks it deterministically, by exact ID, after this call returns (see Phase 3)
@@ -0,0 +1,54 @@
1
+ ---
2
+ name: gaia-validate-tech-spec-component-detail
3
+ description: "Verifies a small batch of claims against the specific component design files named by the host. Input: a list of {claim, component file path} pairs"
4
+ model: haiku
5
+ tools: Read, Glob, Grep
6
+ ---
7
+
8
+ # Validate Tech-Spec Component Detail
9
+
10
+ A narrow QA agent: for each claim in this batch, Read ONLY the one named component file and decide
11
+ whether its own content satisfies that claim. Do not Read, Glob, or Grep for `feature.md`,
12
+ `req/index.md`, `spec/index.md`, or any component file NOT explicitly named in this prompt — every
13
+ file you need is already named below; searching for anything else is out of scope for this agent
14
+ and wastes the narrow batch this call was sized for.
15
+
16
+ ## Phase 1 — Load This Batch
17
+
18
+ The host supplies, in the dispatch prompt, one entry per line:
19
+ `Claim: {claim text}` followed by `Component file: {absolute path}`.
20
+ Multiple claims may name the SAME component file — Read each distinct file only once, then check
21
+ every claim assigned to it against that one reading.
22
+
23
+ ## Phase 2 — Verify Each Claim
24
+
25
+ For each claim, Read its named file (if not already read for an earlier claim in this batch) and
26
+ decide: does this file's own actual content (its Change/Algorithm/Schema/Tests sections, or
27
+ whichever of those the file actually has) satisfy the claim? Be specific and literal — a claim
28
+ about an exact endpoint method/path, a schema field, or an error code is satisfied only if that
29
+ exact detail is actually present in the file, not merely plausible given the file's general topic.
30
+
31
+ ## Phase 3 — Structured Completion (MANDATORY — your final response)
32
+
33
+ Return a single JSON object with exactly one field:
34
+
35
+ ```json
36
+ {"findings": ["Tech-Spec: components/AuthMiddleware.cs.md does not document a 401 response for an expired token, as feature.md requires"]}
37
+ ```
38
+
39
+ - `findings` is an array of strings, one per claim that is NOT satisfied by its named file's
40
+ content. A satisfied claim produces NO entry — do not report successes.
41
+ - Every finding string is prefixed `"Tech-Spec: "`, matching the same convention
42
+ `gaia-validate-feature-docs`'s own `findings` array and the host's deterministic
43
+ AC↔component check already use, so the host can merge every source into one array with no
44
+ format-specific handling for this agent's own output.
45
+ - An empty array (`{"findings": []}`) means every claim in this batch was confirmed.
46
+ - The caller supplies `--json-schema`; use the runtime's structured-output mechanism to return this
47
+ object in `structured_output`.
48
+
49
+ ## Guidelines
50
+
51
+ - **Read only the named files** — never search for or infer any other document.
52
+ - **One call, one batch** — do not ask the host for more context; if a named file's content is
53
+ genuinely ambiguous about a claim, report it as NOT satisfied (fail closed) rather than guessing.
54
+ - **Be literal, not generous** — "plausibly related" is not the same as "actually documented".
@@ -281,6 +281,67 @@ return c.run(meta.name, featureDir, prefix, async () => {
281
281
  return changeIdx !== -1 && testsIdx !== -1
282
282
  }
283
283
 
284
+ // FTR-028/UC-02: purely deterministic, zero-LLM-cost AC<->component traceability check. Reuses
285
+ // the two table parsers already duplicated in this file — no new parsing contract invented.
286
+ // Both directions produce a "Tech-Spec: " finding (always closed by a Tech-Spec revision —
287
+ // either add missing coverage, or fix a bad AC-ID reference).
288
+ function computeAcComponentTraceabilityGaps(reqIndexContent, specIndexContent) {
289
+ const acIds = new Set(parseAcTableRelatedUc(reqIndexContent).map(r => r.id))
290
+ const components = parseComponentsTable(specIndexContent)
291
+ const addressedAcIds = new Set()
292
+ const findings = []
293
+ for (const comp of components) {
294
+ const refs = (comp.addresses || '').split(',').map(s => s.trim()).filter(s => s && s !== '—')
295
+ for (const ref of refs) {
296
+ addressedAcIds.add(ref)
297
+ if (!acIds.has(ref)) findings.push(`Tech-Spec: component ${comp.filePath} references nonexistent ${ref}`)
298
+ }
299
+ }
300
+ for (const id of acIds) {
301
+ if (!addressedAcIds.has(id)) findings.push(`Tech-Spec: ${id} is not addressed by any Tech-Spec component`)
302
+ }
303
+ return findings
304
+ }
305
+
306
+ // FTR-028/UC-03: validates each needsComponentDetail entry's fileSlug against the real
307
+ // component table (a hallucinated/mistyped reference is dropped and reported, never
308
+ // dispatched), deduplicates by fileSlug (two claims against the same file collapse into one
309
+ // batch slot), and splits the remaining unique components into groups of at most
310
+ // DRILL_DOWN_BATCH_SIZE — never one call per claim, never one call covering every flagged
311
+ // component regardless of count.
312
+ const DRILL_DOWN_BATCH_SIZE = 8 // fixed constant, not externally configurable
313
+ function prepareDrillDownBatches(needsComponentDetail, specIndexContent) {
314
+ const validSlugs = new Set(parseComponentsTable(specIndexContent).map(c => c.fileSlug))
315
+ const extraFindings = []
316
+ const claimsByFileSlug = new Map()
317
+ for (const entry of needsComponentDetail || []) {
318
+ if (!validSlugs.has(entry.fileSlug)) {
319
+ extraFindings.push(`Tech-Spec: component-detail check referenced a non-existent file "${entry.fileSlug}" for claim "${entry.claim}"`)
320
+ continue
321
+ }
322
+ if (!claimsByFileSlug.has(entry.fileSlug)) claimsByFileSlug.set(entry.fileSlug, [])
323
+ claimsByFileSlug.get(entry.fileSlug).push(entry.claim)
324
+ }
325
+ const fileSlugs = [...claimsByFileSlug.keys()]
326
+ const batches = []
327
+ for (let i = 0; i < fileSlugs.length; i += DRILL_DOWN_BATCH_SIZE) {
328
+ batches.push(fileSlugs.slice(i, i + DRILL_DOWN_BATCH_SIZE)
329
+ .map(slug => ({ fileSlug: slug, claims: claimsByFileSlug.get(slug) })))
330
+ }
331
+ return { batches, extraFindings }
332
+ }
333
+
334
+ // specDir is already absolute (derived from c.feature()'s realpath-resolved featureDir), so
335
+ // this needs no extra path.resolve — same reasoning that already lets featurePath be handed to
336
+ // agents directly elsewhere in this file.
337
+ function buildDrillDownPrompt(batch) {
338
+ return batch.map(({ fileSlug, claims }) =>
339
+ claims.map(claim => `Claim: ${claim}\nComponent file: ${specDir}/components/${fileSlug}`).join('\n\n')
340
+ ).join('\n\n')
341
+ }
342
+
343
+ const drillDownId = 'gaia.agent.planner.validate-tech-spec-component-detail'
344
+
284
345
  function buildComponentPrompt(row, components, featurePathArg, componentTemplate) {
285
346
  const compactIndex = components.map(c => `${c.filePath} (${c.domain})`).join('\n')
286
347
  const base = `${featurePathArg}\n\nGenerate the full technical design for this component:\n` +
@@ -445,26 +506,81 @@ return c.run(meta.name, featureDir, prefix, async () => {
445
506
  // finding mentioning both document names, or neither, would mis-route or
446
507
  // silently skip). The validator agent itself decides which document(s) its
447
508
  // own findings implicate.
509
+ // FTR-028/UC-01: Tech-Spec text is the raw skeleton (c.read(specIndexPath)), never
510
+ // assembledTechSpec's fully-spliced output — bounds this prompt's size to component COUNT,
511
+ // not per-component prose length. Requirements stays assembledRequirements (full UC
512
+ // detail), unchanged — no real incident has shown UC count x flow length to pose the same
513
+ // risk.
448
514
  const result = await c.worker(valId,
449
- `${featurePath}\n\nRequirements:\n${assembledRequirements}\n\nTech-Spec:\n${assembledTechSpec}\n\n` +
450
- 'Validate only; the host owns the revision loop. Return valid, a findings array (one string per remaining gap), needsRequirementsFix/needsTechSpecFix (true only if at least one finding implicates that document), and the full validation report content. Do not dispatch other agents.',
451
- { label: `validation:${cycle}`, outputs: [output('Validation-Report', [featurePath, reqIndexPath, specIndexPath])], schema: { type: 'object', properties: { valid: { type: 'boolean' }, findings: { type: 'array', items: { type: 'string' } }, needsRequirementsFix: { type: 'boolean' }, needsTechSpecFix: { type: 'boolean' }, report: { type: 'string' } }, required: ['valid', 'findings', 'needsRequirementsFix', 'needsTechSpecFix', 'report'] } })
452
- // A model returning valid:true alongside non-empty findings (or valid:false
453
- // with none) contradicts its own schema's intent — never silently treated as
454
- // "not clean yet" and spent on another cycle without surfacing the
455
- // inconsistency; fail closed instead of masking a confused response.
456
- if (result.valid === (result.findings.length > 0)) {
457
- throw new Error(`Validation agent returned a contradictory result on cycle ${cycle}: valid=${result.valid} with ${result.findings.length} finding(s)`)
515
+ `${featurePath}\n\nRequirements:\n${assembledRequirements}\n\nTech-Spec:\n${c.read(specIndexPath)}\n\n` +
516
+ 'Validate only; the host owns the revision loop. Return valid, a findings array (one string per remaining gap), needsRequirementsFix/needsTechSpecFix (true only if at least one finding implicates that document), needsComponentDetail (claims needing a component\'s own design body to verify — see your own instructions for when to use this instead of a finding), and the full validation report content. Do not dispatch other agents.',
517
+ { label: `validation:${cycle}`, outputs: [output('Validation-Report', [featurePath, reqIndexPath, specIndexPath])], schema: { type: 'object', properties: { valid: { type: 'boolean' }, findings: { type: 'array', items: { type: 'string' } }, needsRequirementsFix: { type: 'boolean' }, needsTechSpecFix: { type: 'boolean' }, report: { type: 'string' }, needsComponentDetail: { type: 'array', items: { type: 'object', properties: { claim: { type: 'string' }, fileSlug: { type: 'string' } }, required: ['claim', 'fileSlug'] } } }, required: ['valid', 'findings', 'needsRequirementsFix', 'needsTechSpecFix', 'report'] } })
518
+ // needsComponentDetail is declared in `properties` but deliberately NOT in the schema's
519
+ // top-level `required` array: every pre-existing mocked test response for this agent
520
+ // (predating this feature) omits the field entirely, and making it hard-required would
521
+ // break every one of them for no real gain — prepareDrillDownBatches already treats an
522
+ // absent/undefined value as "no entries" via `needsComponentDetail || []`.
523
+
524
+ // Raw self-consistency guard on the MODEL'S OWN response, before any host-side merge: a
525
+ // pending needsComponentDetail entry is itself a form of "not yet resolved", same as a
526
+ // non-empty findings array — valid:true requires BOTH to be empty, valid:false requires at
527
+ // least one to be non-empty. This is intentionally separate from mergedValid below (which
528
+ // is NOT required to track this raw self-report: a valid:false caused only by
529
+ // needsComponentDetail entries that the drill-down subsequently confirms must still resolve
530
+ // to a clean cycle, not a false "contradiction").
531
+ const rawHasOpenItems = result.findings.length > 0 || (result.needsComponentDetail || []).length > 0
532
+ if (result.valid === rawHasOpenItems) {
533
+ throw new Error(`Validation agent returned a contradictory result on cycle ${cycle}: valid=${result.valid} with ${result.findings.length} finding(s) and ${(result.needsComponentDetail || []).length} needsComponentDetail entry(ies)`)
534
+ }
535
+
536
+ // FTR-028/UC-02 + UC-03: every finding source for this cycle, computed/dispatched AFTER the
537
+ // main call returns (re-reading the index files fresh, since a prior cycle may have
538
+ // regenerated them). Zero added LLM cost when both come back empty (the common case).
539
+ const reqIndexContent = c.read(reqIndexPath)
540
+ const specIndexContent = c.read(specIndexPath)
541
+ const structuralFindings = computeAcComponentTraceabilityGaps(reqIndexContent, specIndexContent)
542
+ const { batches, extraFindings } = prepareDrillDownBatches(result.needsComponentDetail, specIndexContent)
543
+
544
+ const drillDownFindings = []
545
+ for (let b = 0; b < batches.length; b++) {
546
+ const batchResult = await c.worker(drillDownId, buildDrillDownPrompt(batches[b]),
547
+ { label: `validation:${cycle}:component-detail:${b + 1}`,
548
+ schema: { type: 'object', properties: { findings: { type: 'array', items: { type: 'string' } } }, required: ['findings'] } })
549
+ drillDownFindings.push(...batchResult.findings)
550
+ }
551
+
552
+ // BR-04: merge everything BEFORE the contradiction guard. A model returning valid:true
553
+ // while the host's own independent checks found a real gap is not a self-contradiction (it
554
+ // was never shown that gap) — it is simply now-invalid, same as any other unresolved
555
+ // finding.
556
+ const newFindings = [...structuralFindings, ...extraFindings, ...drillDownFindings]
557
+ const mergedFindings = [...result.findings, ...newFindings]
558
+ // Deliberately NOT `result.valid && ...`: result.valid may be false SOLELY because of
559
+ // needsComponentDetail entries that the drill-down above has now fully confirmed (zero
560
+ // drillDownFindings) — that must resolve to clean, not stay stuck on the model's own
561
+ // now-stale self-report. The only authoritative signal for the final verdict is whether any
562
+ // finding remains, from any source, after every check this cycle has run.
563
+ const mergedValid = mergedFindings.length === 0
564
+ const mergedNeedsTechSpecFix = result.needsTechSpecFix || newFindings.some(f => f.startsWith('Tech-Spec:'))
565
+ const mergedNeedsRequirementsFix = result.needsRequirementsFix || newFindings.some(f => f.startsWith('Requirements:'))
566
+
567
+ // c.worker() already wrote result.report verbatim via its own outputs[0] contract; only
568
+ // overwrite it when this cycle actually added findings the LLM's own report text doesn't
569
+ // mention — the common clean case (newFindings.length === 0) never re-writes the file.
570
+ if (newFindings.length) {
571
+ c.write(file('Validation-Report'), result.report +
572
+ '\n\n## Additional findings (deterministic + component-detail drill-down)\n' +
573
+ newFindings.map(f => `- ${f}`).join('\n') + '\n')
458
574
  }
459
- if (result.valid) {
575
+ if (mergedValid) {
460
576
  if (!fresh(file('Validation-Report'), [featurePath, reqIndexPath, specIndexPath])) throw new Error('Validation agent did not produce its report')
461
577
  validationSummary = `0 gaps (clean on cycle ${cycle})`
462
578
  break
463
579
  }
464
580
  if (cycle === 3) throw new Error('Validation gaps remain after 3 cycles')
465
- if (!result.needsRequirementsFix && !result.needsTechSpecFix) throw new Error(`Validation agent reported gaps on cycle ${cycle} but flagged neither document for revision`)
466
- if (result.needsRequirementsFix) { await generateRequirements(true); assembledRequirements = assembleRequirements() }
467
- if (result.needsTechSpecFix) { await generateTechSpec(true); assembledTechSpec = assembleTechSpec() }
581
+ if (!mergedNeedsRequirementsFix && !mergedNeedsTechSpecFix) throw new Error(`Validation agent reported gaps on cycle ${cycle} but flagged neither document for revision`)
582
+ if (mergedNeedsRequirementsFix) { await generateRequirements(true); assembledRequirements = assembleRequirements() }
583
+ if (mergedNeedsTechSpecFix) { await generateTechSpec(true); assembledTechSpec = assembleTechSpec() }
468
584
  }
469
585
  }
470
586
  // Binds Gate 1's eventual approval to the EXACT Requirements/Tech-Spec bytes