@llm-dev-ops/agentics-cli 2.6.0 → 2.7.1

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 (34) hide show
  1. package/dist/cli/index.js +30 -0
  2. package/dist/cli/index.js.map +1 -1
  3. package/dist/mcp/agent-event-parser.d.ts +53 -0
  4. package/dist/mcp/agent-event-parser.d.ts.map +1 -0
  5. package/dist/mcp/agent-event-parser.js +159 -0
  6. package/dist/mcp/agent-event-parser.js.map +1 -0
  7. package/dist/mcp/mcp-server.js +34 -15
  8. package/dist/mcp/mcp-server.js.map +1 -1
  9. package/dist/pipeline/auto-chain.d.ts +168 -0
  10. package/dist/pipeline/auto-chain.d.ts.map +1 -1
  11. package/dist/pipeline/auto-chain.js +1854 -880
  12. package/dist/pipeline/auto-chain.js.map +1 -1
  13. package/dist/pipeline/enterprise/artifact-renderers.d.ts +30 -0
  14. package/dist/pipeline/enterprise/artifact-renderers.d.ts.map +1 -1
  15. package/dist/pipeline/enterprise/artifact-renderers.js +129 -1
  16. package/dist/pipeline/enterprise/artifact-renderers.js.map +1 -1
  17. package/dist/pipeline/gate/phase-dependency-gate.d.ts +93 -0
  18. package/dist/pipeline/gate/phase-dependency-gate.d.ts.map +1 -0
  19. package/dist/pipeline/gate/phase-dependency-gate.js +349 -0
  20. package/dist/pipeline/gate/phase-dependency-gate.js.map +1 -0
  21. package/dist/pipeline/phase3-sparc/phase3-sparc-coordinator.d.ts.map +1 -1
  22. package/dist/pipeline/phase3-sparc/phase3-sparc-coordinator.js +280 -40
  23. package/dist/pipeline/phase3-sparc/phase3-sparc-coordinator.js.map +1 -1
  24. package/dist/pipeline/phase4-adrs/phase4-adrs-coordinator.d.ts.map +1 -1
  25. package/dist/pipeline/phase4-adrs/phase4-adrs-coordinator.js +363 -87
  26. package/dist/pipeline/phase4-adrs/phase4-adrs-coordinator.js.map +1 -1
  27. package/dist/pipeline/phases/prompt-generator.d.ts.map +1 -1
  28. package/dist/pipeline/phases/prompt-generator.js +95 -6
  29. package/dist/pipeline/phases/prompt-generator.js.map +1 -1
  30. package/dist/pipeline/ruflo-phase-executor.d.ts +124 -1
  31. package/dist/pipeline/ruflo-phase-executor.d.ts.map +1 -1
  32. package/dist/pipeline/ruflo-phase-executor.js +319 -4
  33. package/dist/pipeline/ruflo-phase-executor.js.map +1 -1
  34. package/package.json +1 -1
@@ -14,6 +14,7 @@
14
14
  * Phase 6: ERP Surface Push (registration + project materialization)
15
15
  */
16
16
  import { execSync } from 'node:child_process';
17
+ import { createHash } from 'node:crypto';
17
18
  import * as fs from 'node:fs';
18
19
  import * as path from 'node:path';
19
20
  import { executePhase2Command, formatPhase2ForDisplay } from '../commands/phase2.js';
@@ -23,7 +24,8 @@ import { executePhase5Command, formatPhase5ForDisplay } from '../commands/phase5
23
24
  import { executePhase6Command, formatPhase6ForDisplay, copyTreeForOutput } from '../commands/phase6.js';
24
25
  import { detectExecutionMode, commitScenarioFiles, } from './execution-context.js';
25
26
  import { initSwarm, dispatchPhaseAgents, storePhaseArtifacts, reviewPhaseOutput, recordPhaseFailure, shutdownSwarm, invokeRufloCodingSwarm, PHASE_AGENTS, } from './swarm-orchestrator.js';
26
- import { executeRufloPhaseSwarm, buildPhase2Tasks, buildPhase3Tasks, buildPhase4Tasks, buildPhase5Tasks, buildPhase6Tasks, collectPhase2Artifacts, collectPhase3Artifacts, collectPhase4Artifacts, collectPhase5Artifacts, collectPhase6Artifacts, extractPhase1AgentCode, } from './ruflo-phase-executor.js';
27
+ import { executeRufloPhaseSwarm, buildPhase2Tasks, buildPhase3Tasks, buildPhase4Tasks, buildPhase5Tasks, buildPhase6Tasks, collectPhase2Artifacts, collectPhase3Artifacts, collectPhase4Artifacts, collectPhase5Artifacts, collectPhase6Artifacts, extractPhase1AgentCode, runPrimaryPhaseExecution, } from './ruflo-phase-executor.js';
28
+ import { gatePhaseInputs, determineBlockedPhases, } from './gate/phase-dependency-gate.js';
27
29
  /** Extract path strings from any manifest artifact array (objects with .path or plain strings). */
28
30
  function extractArtifactPaths(artifacts) {
29
31
  return artifacts.map(a => typeof a === 'string' ? a : a.path);
@@ -77,6 +79,347 @@ function copyDirRecursive(src, dest) {
77
79
  }
78
80
  return { copied, skipped };
79
81
  }
82
+ /**
83
+ * Ordered candidate-source table (Rule 1). First non-empty source wins for
84
+ * each slot; later sources only fill a slot that is still empty, unless the
85
+ * source is marked `additive`.
86
+ */
87
+ const PLAN_SLOT_SOURCES = {
88
+ sparc: [
89
+ { relPath: 'phase3/sparc', kind: 'dir' },
90
+ { relPath: 'phase2/sparc', kind: 'dir' },
91
+ {
92
+ relPath: 'engineering/sparc-specification.md',
93
+ kind: 'file',
94
+ destFilename: 'specification.md',
95
+ requireSlotEmpty: true,
96
+ engineeringPrimary: true,
97
+ rufloHeader: '<!-- Ruflo primary output (ADR-PIPELINE-092) — will be overwritten when Phase 3 produces structured SPARC artifacts -->',
98
+ },
99
+ ],
100
+ adrs: [
101
+ { relPath: 'phase4/adrs', kind: 'dir' },
102
+ { relPath: 'phase2/adrs', kind: 'dir' },
103
+ {
104
+ relPath: 'engineering/architecture-decisions.md',
105
+ kind: 'file',
106
+ destFilename: 'ADR-ENG-001-architecture-decisions.md',
107
+ requireSlotEmpty: true,
108
+ engineeringPrimary: true,
109
+ rufloHeader: '<!-- Ruflo primary output (ADR-PIPELINE-092) — will be overwritten when Phase 4 produces structured ADRs -->',
110
+ },
111
+ { relPath: 'adrs', kind: 'dir' },
112
+ ],
113
+ ddd: [
114
+ { relPath: 'phase4/ddd', kind: 'dir' },
115
+ { relPath: 'phase2/ddd', kind: 'dir' },
116
+ {
117
+ relPath: 'engineering/domain-model.md',
118
+ kind: 'file',
119
+ destFilename: 'domain-model.md',
120
+ requireSlotEmpty: true,
121
+ engineeringPrimary: true,
122
+ rufloHeader: '<!-- Ruflo primary output (ADR-PIPELINE-092) — will be overwritten when Phase 4 produces a structured domain model -->',
123
+ },
124
+ ],
125
+ tdd: [
126
+ { relPath: 'phase3/tdd', kind: 'dir' },
127
+ { relPath: 'phase2/tdd', kind: 'dir' },
128
+ {
129
+ relPath: 'engineering/tdd-test-plan.md',
130
+ kind: 'file',
131
+ destFilename: 'test-plan.md',
132
+ requireSlotEmpty: true,
133
+ engineeringPrimary: true,
134
+ rufloHeader: '<!-- Ruflo primary output (ADR-PIPELINE-092) — will be overwritten when Phase 3 produces a structured TDD plan -->',
135
+ },
136
+ ],
137
+ prompts: [
138
+ { relPath: 'prompts', kind: 'dir' },
139
+ { relPath: 'phase4/prompts', kind: 'dir' },
140
+ {
141
+ relPath: 'phase3/implementation-prompt.md',
142
+ kind: 'file',
143
+ destFilename: 'phase3-implementation-prompt.md',
144
+ },
145
+ ],
146
+ implementation: [
147
+ { relPath: 'phase1/roadmap.json', kind: 'file', destFilename: 'roadmap.json' },
148
+ { relPath: 'roadmap.json', kind: 'file', destFilename: 'roadmap.json' },
149
+ { relPath: 'scenario.json', kind: 'file', destFilename: 'scenario.json' },
150
+ {
151
+ relPath: 'engineering/implementation-roadmap.md',
152
+ kind: 'file',
153
+ destFilename: 'roadmap.md',
154
+ additive: true,
155
+ engineeringPrimary: true,
156
+ rufloHeader: '<!-- Ruflo primary output (ADR-PIPELINE-092) — promoted alongside Phase 1 roadmap.json -->',
157
+ },
158
+ ],
159
+ };
160
+ /**
161
+ * Engineering (Ruflo primary-output) placeholder threshold — any single-file
162
+ * engineering source whose size is at or below this value is rejected as a
163
+ * stub and never promoted.
164
+ */
165
+ const ENGINEERING_MIN_SIZE_BYTES = 256;
166
+ /**
167
+ * sha256 hashes of known placeholder templates that Ruflo may emit before it
168
+ * has real content. Any engineering/*.md file whose content hash matches is
169
+ * rejected even if its size is above the threshold. Starts empty but the
170
+ * `has()` lookup is O(1) and additions here propagate automatically.
171
+ */
172
+ const ENGINEERING_PLACEHOLDER_HASHES = new Set([]);
173
+ /**
174
+ * Pick the single best candidate source for a slot given which slot files
175
+ * are already present. Phase-refined sources (no `requireSlotEmpty`) win
176
+ * over `engineering/*` (which sets `requireSlotEmpty: true`). Returns `null`
177
+ * when nothing qualifies.
178
+ */
179
+ function pickBestSourceForSlot(runDir, _slot, slotHasContent, candidateSources) {
180
+ for (const source of candidateSources) {
181
+ if (source.requireSlotEmpty && slotHasContent)
182
+ continue;
183
+ const abs = path.join(runDir, source.relPath);
184
+ if (!fs.existsSync(abs))
185
+ continue;
186
+ if (source.kind === 'dir') {
187
+ try {
188
+ const entries = fs.readdirSync(abs);
189
+ if (entries.length === 0)
190
+ continue;
191
+ }
192
+ catch {
193
+ continue;
194
+ }
195
+ }
196
+ else {
197
+ try {
198
+ const st = fs.statSync(abs);
199
+ if (!st.isFile() || st.size === 0)
200
+ continue;
201
+ }
202
+ catch {
203
+ continue;
204
+ }
205
+ }
206
+ return source;
207
+ }
208
+ return null;
209
+ }
210
+ /** Returns true if the directory exists and has at least one file inside it (recursively). */
211
+ function slotDirHasFiles(slotDir) {
212
+ if (!fs.existsSync(slotDir))
213
+ return false;
214
+ try {
215
+ const entries = fs.readdirSync(slotDir, { withFileTypes: true });
216
+ for (const entry of entries) {
217
+ if (entry.isFile())
218
+ return true;
219
+ if (entry.isDirectory()) {
220
+ if (slotDirHasFiles(path.join(slotDir, entry.name)))
221
+ return true;
222
+ }
223
+ }
224
+ return false;
225
+ }
226
+ catch {
227
+ return false;
228
+ }
229
+ }
230
+ /**
231
+ * Gate an engineering/*.md source through the placeholder threshold: size
232
+ * must exceed ENGINEERING_MIN_SIZE_BYTES and its sha256 must not be in the
233
+ * ENGINEERING_PLACEHOLDER_HASHES allow-list. Returns the file buffer on
234
+ * pass, or `null` on reject.
235
+ */
236
+ function readEngineeringPrimaryOrReject(absPath) {
237
+ try {
238
+ const stat = fs.statSync(absPath);
239
+ if (stat.size <= ENGINEERING_MIN_SIZE_BYTES)
240
+ return null;
241
+ const buf = fs.readFileSync(absPath);
242
+ const hash = createHash('sha256').update(buf).digest('hex');
243
+ if (ENGINEERING_PLACEHOLDER_HASHES.has(hash))
244
+ return null;
245
+ return buf;
246
+ }
247
+ catch {
248
+ return null;
249
+ }
250
+ }
251
+ /**
252
+ * Promote a single source into a slot. Returns `{copied, skipped}` counts.
253
+ * For `kind === 'dir'` sources this performs an additive file-level merge
254
+ * (dedup by destination filename, byte-identical skips). For `kind === 'file'`
255
+ * sources it writes a single file, optionally prepending the Ruflo header.
256
+ */
257
+ function promoteSourceIntoSlot(runDir, slotDir, source) {
258
+ const absSrc = path.join(runDir, source.relPath);
259
+ if (!fs.existsSync(absSrc))
260
+ return { copied: 0, skipped: 0 };
261
+ if (source.kind === 'dir') {
262
+ return copyDirRecursive(absSrc, slotDir);
263
+ }
264
+ // kind === 'file'
265
+ const destName = source.destFilename ?? path.basename(source.relPath);
266
+ const destPath = path.join(slotDir, destName);
267
+ let buf;
268
+ if (source.engineeringPrimary) {
269
+ buf = readEngineeringPrimaryOrReject(absSrc);
270
+ if (buf === null)
271
+ return { copied: 0, skipped: 0 };
272
+ }
273
+ else {
274
+ try {
275
+ buf = fs.readFileSync(absSrc);
276
+ }
277
+ catch {
278
+ return { copied: 0, skipped: 0 };
279
+ }
280
+ }
281
+ // Prepend Ruflo header when applicable.
282
+ let outBuf = buf;
283
+ if (source.rufloHeader) {
284
+ const needsHeader = !buf.toString('utf-8').startsWith(source.rufloHeader);
285
+ if (needsHeader) {
286
+ outBuf = Buffer.from(`${source.rufloHeader}\n\n${buf.toString('utf-8')}`, 'utf-8');
287
+ }
288
+ }
289
+ // Dedup: if destination already exists and matches byte-for-byte, skip.
290
+ if (fs.existsSync(destPath)) {
291
+ try {
292
+ const existing = fs.readFileSync(destPath);
293
+ if (existing.equals(outBuf))
294
+ return { copied: 0, skipped: 1 };
295
+ }
296
+ catch { /* fall through to overwrite */ }
297
+ }
298
+ fs.mkdirSync(path.dirname(destPath), { recursive: true });
299
+ fs.writeFileSync(destPath, outBuf);
300
+ return { copied: 1, skipped: 0 };
301
+ }
302
+ /**
303
+ * Shared table-driven promotion routine (ADR-PIPELINE-092, Rules 1–5).
304
+ *
305
+ * Walks `PLAN_SLOT_SOURCES` for every slot, picks the best source via
306
+ * `pickBestSourceForSlot`, promotes the source into the slot, then runs
307
+ * any additive sources on top. Emits the `[PLANS] promoted ...` stderr
308
+ * summary and one `[PLANS] MISSING <slot>` line per empty-and-unrecoverable
309
+ * slot. Returns the summary so callers (final safety-net call) can merge
310
+ * `planPromotion` into `manifest.json`.
311
+ */
312
+ export function promotePlanningArtifacts(runDir, projectRoot) {
313
+ const plansDir = path.join(projectRoot, '.agentics', 'plans');
314
+ const counts = {
315
+ sparc: 0, adrs: 0, ddd: 0, tdd: 0, prompts: 0, implementation: 0,
316
+ };
317
+ const missing_slots = [];
318
+ let total_copied = 0;
319
+ let total_skipped = 0;
320
+ const slots = ['sparc', 'adrs', 'ddd', 'tdd', 'prompts', 'implementation'];
321
+ for (const slot of slots) {
322
+ const slotDir = path.join(plansDir, slot);
323
+ const sources = PLAN_SLOT_SOURCES[slot];
324
+ // Step 1: pick-best — the first qualifying non-additive source.
325
+ let slotHasContent = slotDirHasFiles(slotDir);
326
+ const best = pickBestSourceForSlot(runDir, slot, slotHasContent, sources.filter(s => !s.additive));
327
+ if (best) {
328
+ const r = promoteSourceIntoSlot(runDir, slotDir, best);
329
+ counts[slot] += r.copied;
330
+ total_copied += r.copied;
331
+ total_skipped += r.skipped;
332
+ if (r.copied > 0 || r.skipped > 0)
333
+ slotHasContent = true;
334
+ }
335
+ // Step 2: additive sources always run (currently only
336
+ // engineering/implementation-roadmap.md → plans/implementation/roadmap.md).
337
+ for (const source of sources) {
338
+ if (!source.additive)
339
+ continue;
340
+ const abs = path.join(runDir, source.relPath);
341
+ if (!fs.existsSync(abs))
342
+ continue;
343
+ const r = promoteSourceIntoSlot(runDir, slotDir, source);
344
+ counts[slot] += r.copied;
345
+ total_copied += r.copied;
346
+ total_skipped += r.skipped;
347
+ if (r.copied > 0 || r.skipped > 0)
348
+ slotHasContent = true;
349
+ }
350
+ // Step 3: MISSING escalation — only when every candidate source genuinely
351
+ // had zero promotable content. For engineering-primary sources, "promotable"
352
+ // also means passing the placeholder threshold; a rejected stub counts as
353
+ // empty so the MISSING line still fires. This keeps the warning honest.
354
+ if (!slotHasContent) {
355
+ const anyCandidateExists = sources.some(s => {
356
+ const abs = path.join(runDir, s.relPath);
357
+ if (!fs.existsSync(abs))
358
+ return false;
359
+ if (s.kind === 'dir') {
360
+ try {
361
+ return fs.readdirSync(abs).length > 0;
362
+ }
363
+ catch {
364
+ return false;
365
+ }
366
+ }
367
+ try {
368
+ if (fs.statSync(abs).size === 0)
369
+ return false;
370
+ }
371
+ catch {
372
+ return false;
373
+ }
374
+ if (s.engineeringPrimary) {
375
+ return readEngineeringPrimaryOrReject(abs) !== null;
376
+ }
377
+ return true;
378
+ });
379
+ if (!anyCandidateExists) {
380
+ missing_slots.push(slot);
381
+ }
382
+ }
383
+ }
384
+ // Rule 5 — one-line stderr summary with per-slot counts.
385
+ const summaryLine = `[PLANS] promoted ${total_copied} files across {sparc:${counts.sparc}, adrs:${counts.adrs}, ddd:${counts.ddd}, tdd:${counts.tdd}, prompts:${counts.prompts}, implementation:${counts.implementation}}`;
386
+ process.stderr.write(` ${summaryLine}\n`);
387
+ for (const slot of missing_slots) {
388
+ process.stderr.write(` [PLANS] MISSING ${slot} — no source artifacts found in any candidate directory\n`);
389
+ }
390
+ return { counts, missing_slots, total_copied, total_skipped };
391
+ }
392
+ /**
393
+ * Merge the ADR-PIPELINE-092 `planPromotion` block into `runDir/manifest.json`.
394
+ * Read-modify-write pattern mirrors `writeExecutionBlockToManifest`.
395
+ */
396
+ export function writePlanPromotionToManifest(runDir, summary) {
397
+ const manifestPath = path.join(runDir, 'manifest.json');
398
+ try {
399
+ let manifest = {};
400
+ if (fs.existsSync(manifestPath)) {
401
+ try {
402
+ manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf-8'));
403
+ }
404
+ catch {
405
+ manifest = {};
406
+ }
407
+ }
408
+ manifest['planPromotion'] = {
409
+ sparc: summary.counts.sparc,
410
+ adrs: summary.counts.adrs,
411
+ ddd: summary.counts.ddd,
412
+ tdd: summary.counts.tdd,
413
+ prompts: summary.counts.prompts,
414
+ implementation: summary.counts.implementation,
415
+ missing_slots: summary.missing_slots,
416
+ };
417
+ fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2), { mode: 0o600, encoding: 'utf-8' });
418
+ }
419
+ catch (err) {
420
+ process.stderr.write(` [PLANS] Failed to merge planPromotion into manifest.json: ${err instanceof Error ? err.message : String(err)}\n`);
421
+ }
422
+ }
80
423
  /**
81
424
  * Copy whatever planning artifacts exist from ~/.agentics/runs/<traceId>/
82
425
  * to .agentics/plans/ in the current working directory. Uses .agentics/plans/
@@ -1388,7 +1731,7 @@ export function detectScaffoldDuplicates(projectRoot, owned = OWNED_SCAFFOLD_MOD
1388
1731
  walk(projectRoot);
1389
1732
  return findings;
1390
1733
  }
1391
- function copyPlanningArtifacts(runDir, targetRoot) {
1734
+ export function copyPlanningArtifacts(runDir, targetRoot) {
1392
1735
  try {
1393
1736
  // ADR-051: Use git repo root (or explicit target) instead of process.cwd()
1394
1737
  // When running as MCP server, process.cwd() may not be the user's project directory
@@ -1408,62 +1751,101 @@ function copyPlanningArtifacts(runDir, targetRoot) {
1408
1751
  fs.copyFileSync(src, dest);
1409
1752
  totalCopied++;
1410
1753
  }
1411
- /** Accumulate results from copyDirRecursive. */
1412
- function copyDirAccum(src, dest) {
1413
- const r = copyDirRecursive(src, dest);
1414
- totalCopied += r.copied;
1415
- totalSkipped += r.skipped;
1416
- }
1417
- // SPARC: prefer Phase 3 refined, fall back to Phase 2
1418
- const phase3Sparc = path.join(runDir, 'phase3', 'sparc');
1419
- const phase2Sparc = path.join(runDir, 'phase2', 'sparc');
1420
- const sparcSource = fs.existsSync(phase3Sparc) ? phase3Sparc : phase2Sparc;
1421
- copyDirAccum(sparcSource, path.join(plansDir, 'sparc'));
1422
- // ADRs from Phase 4
1423
- copyDirAccum(path.join(runDir, 'phase4', 'adrs'), path.join(plansDir, 'adrs'));
1424
- // DDD from Phase 4 (or Phase 2 fallback)
1425
- const phase4Ddd = path.join(runDir, 'phase4', 'ddd');
1426
- const phase2Ddd = path.join(runDir, 'phase2', 'ddd');
1427
- const dddSource = fs.existsSync(phase4Ddd) ? phase4Ddd : phase2Ddd;
1428
- copyDirAccum(dddSource, path.join(plansDir, 'ddd'));
1429
- // TDD from Phase 2 or 3
1430
- const phase3Tdd = path.join(runDir, 'phase3', 'tdd');
1431
- const phase2Tdd = path.join(runDir, 'phase2', 'tdd');
1432
- const tddSource = fs.existsSync(phase3Tdd) ? phase3Tdd : phase2Tdd;
1433
- copyDirAccum(tddSource, path.join(plansDir, 'tdd'));
1754
+ // ADR-PIPELINE-092: table-driven plan-slot promotion. The shared
1755
+ // `promotePlanningArtifacts` helper handles candidate-source discovery
1756
+ // (Rule 1), engineering/ → plans/ mapping with placeholder rejection
1757
+ // (Rule 2 + 2.5), conflict resolution (Rule 4), and `[PLANS]` stderr
1758
+ // observability (Rule 5). Returns per-slot counts used to merge the
1759
+ // `planPromotion` block into `manifest.json` at the end of this call.
1760
+ const promotionSummary = promotePlanningArtifacts(runDir, projectRoot);
1761
+ totalCopied += promotionSummary.total_copied;
1762
+ totalSkipped += promotionSummary.total_skipped;
1434
1763
  // Phase 1 artifacts (always available after Phase 1)
1435
- for (const file of ['scenario.json', 'roadmap.json']) {
1436
- copySingleFile(path.join(runDir, file), path.join(plansDir, 'implementation', file));
1437
- }
1438
1764
  for (const file of ['executive-summary.md', 'decision-memo.md', 'risk-assessment.json', 'manifest.json']) {
1439
1765
  copySingleFile(path.join(runDir, file), path.join(plansDir, file));
1440
1766
  }
1441
1767
  // Research dossier from Phase 2
1442
1768
  copySingleFile(path.join(runDir, 'phase2', 'research-dossier.json'), path.join(plansDir, 'research-dossier.json'));
1443
1769
  copySingleFile(path.join(runDir, 'phase2', 'research-dossier.md'), path.join(plansDir, 'research-dossier.md'));
1444
- // Implementation prompts from prompt-generator (ADR-driven prompts)
1445
- const promptsDir = path.join(runDir, 'prompts');
1446
- copyDirAccum(promptsDir, path.join(plansDir, 'prompts'));
1447
- // Implementation prompt from Phase 3 (Ruflo swarm directive)
1448
- copySingleFile(path.join(runDir, 'phase3', 'implementation-prompt.md'), path.join(plansDir, 'prompts', 'phase3-implementation-prompt.md'));
1449
- // Implementation prompts from Phase 4 (domain codegen prompts)
1450
- const phase4Prompts = path.join(runDir, 'phase4', 'prompts');
1451
- copyDirAccum(phase4Prompts, path.join(plansDir, 'prompts', 'phase4'));
1452
1770
  // Phase 5 integration inference results
1453
1771
  copySingleFile(path.join(runDir, 'phase5', 'integration-inference.json'), path.join(plansDir, 'integration-inference.json'));
1454
- // ADR-PIPELINE-039: Write actual scaffold files that Claude Code can include directly.
1455
- // These are the cross-cutting infrastructure modules that every project needs.
1456
- // Instead of hoping the coding agent reads the prompts, we deliver the files.
1457
- const scaffoldDir = path.join(plansDir, 'scaffold', 'src');
1458
- fs.mkdirSync(scaffoldDir, { recursive: true });
1459
- // ADR-PIPELINE-074: logger scaffold now uses AsyncLocalStorage for
1460
- // concurrent-request correctness. Body lives in LOGGER_SCAFFOLD at
1461
- // module scope so it can be unit-tested independently.
1462
- const loggerCode = LOGGER_SCAFFOLD;
1463
- // ADR-PIPELINE-039 + ADR-PIPELINE-075: AppConfig now includes a `db`
1464
- // block so the composition root can pick sqlite (production) vs
1465
- // in-memory (tests) without code changes.
1466
- const configCode = `// Auto-generated by Agentics pipeline (ADR-039 + ADR-PIPELINE-075)
1772
+ // ADR-PIPELINE-093 Rule 5 Scaffold-language gate. Detect the project
1773
+ // language ONCE and let `decideScaffoldEmission` pick which template
1774
+ // blocks to emit. TS project only TS block runs. Python project
1775
+ // only Python block runs. Go/Rust/unknown → both blocks skip and the
1776
+ // mismatch is persisted into `manifest.json`.
1777
+ let emitTsScaffold = true;
1778
+ let emitPyScaffold = true;
1779
+ try {
1780
+ const phase1ManifestPath = path.join(runDir, 'manifest.json');
1781
+ let phase1Manifest;
1782
+ if (fs.existsSync(phase1ManifestPath)) {
1783
+ try {
1784
+ phase1Manifest = JSON.parse(fs.readFileSync(phase1ManifestPath, 'utf-8'));
1785
+ }
1786
+ catch {
1787
+ phase1Manifest = undefined;
1788
+ }
1789
+ }
1790
+ const detected = detectProjectLanguage(projectRoot, phase1Manifest);
1791
+ const decision = decideScaffoldEmission(detected);
1792
+ emitTsScaffold = decision.emitTs;
1793
+ emitPyScaffold = decision.emitPy;
1794
+ if (decision.skipped) {
1795
+ process.stderr.write(` [SCAFFOLD] skipped — language mismatch (detected: ${decision.skipped.detected}, templates: ${decision.skipped.template})\n`);
1796
+ // Persist the skip into manifest.json so `writeGateBlockToManifest`
1797
+ // (later in the pipeline) can merge it into the gate block without
1798
+ // losing it. Read-modify-write matches the pattern used by sibling
1799
+ // writers in this file.
1800
+ try {
1801
+ let mf = {};
1802
+ if (fs.existsSync(phase1ManifestPath)) {
1803
+ try {
1804
+ mf = JSON.parse(fs.readFileSync(phase1ManifestPath, 'utf-8'));
1805
+ }
1806
+ catch {
1807
+ mf = {};
1808
+ }
1809
+ }
1810
+ mf['phase5b_skipped'] = decision.skipped;
1811
+ fs.writeFileSync(phase1ManifestPath, JSON.stringify(mf, null, 2), { mode: 0o600, encoding: 'utf-8' });
1812
+ }
1813
+ catch { /* best-effort */ }
1814
+ }
1815
+ }
1816
+ catch { /* language detection is best-effort; default to emit both (prev behaviour) */ }
1817
+ if (!emitTsScaffold && !emitPyScaffold) {
1818
+ // Rule 5 — no matching scaffold templates for this language. Plan
1819
+ // promotion (sparc/adrs/ddd/tdd/prompts slots) already ran above and
1820
+ // is language-agnostic, so it stays.
1821
+ if (totalCopied > 0 || totalSkipped > 0) {
1822
+ const parts = [];
1823
+ if (totalCopied > 0)
1824
+ parts.push(`${totalCopied} copied`);
1825
+ if (totalSkipped > 0)
1826
+ parts.push(`${totalSkipped} unchanged (skipped)`);
1827
+ console.error(` [PLANS] ${parts.join(', ')} → .agentics/plans/`);
1828
+ }
1829
+ writePlanPromotionToManifest(runDir, promotionSummary);
1830
+ return;
1831
+ }
1832
+ // The TS-scaffold block below is fenced by this flag. Everything from
1833
+ // the scaffoldDir creation through the OWNED_MODULES + duplicate-scan
1834
+ // pass is TS-only — when the detected language is Python, skip it.
1835
+ if (emitTsScaffold) {
1836
+ // ADR-PIPELINE-039: Write actual scaffold files that Claude Code can include directly.
1837
+ // These are the cross-cutting infrastructure modules that every project needs.
1838
+ // Instead of hoping the coding agent reads the prompts, we deliver the files.
1839
+ const scaffoldDir = path.join(plansDir, 'scaffold', 'src');
1840
+ fs.mkdirSync(scaffoldDir, { recursive: true });
1841
+ // ADR-PIPELINE-074: logger scaffold now uses AsyncLocalStorage for
1842
+ // concurrent-request correctness. Body lives in LOGGER_SCAFFOLD at
1843
+ // module scope so it can be unit-tested independently.
1844
+ const loggerCode = LOGGER_SCAFFOLD;
1845
+ // ADR-PIPELINE-039 + ADR-PIPELINE-075: AppConfig now includes a `db`
1846
+ // block so the composition root can pick sqlite (production) vs
1847
+ // in-memory (tests) without code changes.
1848
+ const configCode = `// Auto-generated by Agentics pipeline (ADR-039 + ADR-PIPELINE-075)
1467
1849
  export interface AppConfig {
1468
1850
  env: 'development' | 'staging' | 'production' | 'test';
1469
1851
  port: number;
@@ -1496,7 +1878,7 @@ export const config: AppConfig = {
1496
1878
  },
1497
1879
  };
1498
1880
  `;
1499
- const errorsCode = `// Auto-generated by Agentics pipeline (ADR-039)
1881
+ const errorsCode = `// Auto-generated by Agentics pipeline (ADR-039)
1500
1882
  export class AppError extends Error {
1501
1883
  constructor(message: string, public readonly code: string, public readonly statusCode = 500) {
1502
1884
  super(message);
@@ -1522,35 +1904,35 @@ export class ERPError extends AppError {
1522
1904
  }
1523
1905
  }
1524
1906
  `;
1525
- // TypeScript scaffold (default)
1526
- fs.writeFileSync(path.join(scaffoldDir, 'logger.ts'), loggerCode, 'utf-8');
1527
- // ADR-PIPELINE-074: ship a concurrency regression test next to the
1528
- // logger so generated projects fail loudly if someone reintroduces
1529
- // a module-level correlation-ID store.
1530
- fs.writeFileSync(path.join(scaffoldDir, 'logger.concurrency.test.ts'), LOGGER_CONCURRENCY_TEST_SCAFFOLD, 'utf-8');
1531
- fs.writeFileSync(path.join(scaffoldDir, 'config.ts'), configCode, 'utf-8');
1532
- fs.writeFileSync(path.join(scaffoldDir, 'errors.ts'), errorsCode, 'utf-8');
1533
- // ADR-PIPELINE-075: Scaffolded persistence layer — Repository<T> +
1534
- // InMemoryRepository + SqliteRepository + AppendOnlyAuditRepository.
1535
- // Every stateful service should import these instead of rolling a
1536
- // Map<string, T> store. PGV-017 flags the anti-pattern at post-gen time.
1537
- const persistenceDir = path.join(scaffoldDir, 'persistence');
1538
- fs.mkdirSync(persistenceDir, { recursive: true });
1539
- fs.writeFileSync(path.join(persistenceDir, 'repository.ts'), REPOSITORY_SCAFFOLD, 'utf-8');
1540
- fs.writeFileSync(path.join(persistenceDir, 'in-memory-repository.ts'), IN_MEMORY_REPO_SCAFFOLD, 'utf-8');
1541
- fs.writeFileSync(path.join(persistenceDir, 'sqlite-repository.ts'), SQLITE_REPO_SCAFFOLD, 'utf-8');
1542
- fs.writeFileSync(path.join(persistenceDir, 'audit-repository.ts'), AUDIT_REPO_SCAFFOLD, 'utf-8');
1543
- // ADR-PIPELINE-078: deep canonical JSON + audit hash helper.
1544
- // Every generated AuditService should import hashAuditEntry from this
1545
- // helper instead of hand-rolling JSON.stringify + createHash chains.
1546
- // PGV-022 flags the anti-pattern at post-gen time.
1547
- fs.writeFileSync(path.join(persistenceDir, 'canonical-json.ts'), CANONICAL_JSON_SCAFFOLD, 'utf-8');
1548
- fs.writeFileSync(path.join(persistenceDir, 'audit-hash.ts'), AUDIT_HASH_SCAFFOLD, 'utf-8');
1549
- // ADR-051 + ADR-PIPELINE-074: Correlation ID middleware now wraps next()
1550
- // in runWithCorrelation so the ID lives in AsyncLocalStorage for the
1551
- // request's entire async continuation. Concurrent requests cannot
1552
- // cross-contaminate log lines.
1553
- const middlewareCode = `// Auto-generated by Agentics pipeline (ADR-051 + ADR-PIPELINE-074)
1907
+ // TypeScript scaffold (default)
1908
+ fs.writeFileSync(path.join(scaffoldDir, 'logger.ts'), loggerCode, 'utf-8');
1909
+ // ADR-PIPELINE-074: ship a concurrency regression test next to the
1910
+ // logger so generated projects fail loudly if someone reintroduces
1911
+ // a module-level correlation-ID store.
1912
+ fs.writeFileSync(path.join(scaffoldDir, 'logger.concurrency.test.ts'), LOGGER_CONCURRENCY_TEST_SCAFFOLD, 'utf-8');
1913
+ fs.writeFileSync(path.join(scaffoldDir, 'config.ts'), configCode, 'utf-8');
1914
+ fs.writeFileSync(path.join(scaffoldDir, 'errors.ts'), errorsCode, 'utf-8');
1915
+ // ADR-PIPELINE-075: Scaffolded persistence layer — Repository<T> +
1916
+ // InMemoryRepository + SqliteRepository + AppendOnlyAuditRepository.
1917
+ // Every stateful service should import these instead of rolling a
1918
+ // Map<string, T> store. PGV-017 flags the anti-pattern at post-gen time.
1919
+ const persistenceDir = path.join(scaffoldDir, 'persistence');
1920
+ fs.mkdirSync(persistenceDir, { recursive: true });
1921
+ fs.writeFileSync(path.join(persistenceDir, 'repository.ts'), REPOSITORY_SCAFFOLD, 'utf-8');
1922
+ fs.writeFileSync(path.join(persistenceDir, 'in-memory-repository.ts'), IN_MEMORY_REPO_SCAFFOLD, 'utf-8');
1923
+ fs.writeFileSync(path.join(persistenceDir, 'sqlite-repository.ts'), SQLITE_REPO_SCAFFOLD, 'utf-8');
1924
+ fs.writeFileSync(path.join(persistenceDir, 'audit-repository.ts'), AUDIT_REPO_SCAFFOLD, 'utf-8');
1925
+ // ADR-PIPELINE-078: deep canonical JSON + audit hash helper.
1926
+ // Every generated AuditService should import hashAuditEntry from this
1927
+ // helper instead of hand-rolling JSON.stringify + createHash chains.
1928
+ // PGV-022 flags the anti-pattern at post-gen time.
1929
+ fs.writeFileSync(path.join(persistenceDir, 'canonical-json.ts'), CANONICAL_JSON_SCAFFOLD, 'utf-8');
1930
+ fs.writeFileSync(path.join(persistenceDir, 'audit-hash.ts'), AUDIT_HASH_SCAFFOLD, 'utf-8');
1931
+ // ADR-051 + ADR-PIPELINE-074: Correlation ID middleware now wraps next()
1932
+ // in runWithCorrelation so the ID lives in AsyncLocalStorage for the
1933
+ // request's entire async continuation. Concurrent requests cannot
1934
+ // cross-contaminate log lines.
1935
+ const middlewareCode = `// Auto-generated by Agentics pipeline (ADR-051 + ADR-PIPELINE-074)
1554
1936
  import { randomUUID } from 'node:crypto';
1555
1937
  import { createLogger, runWithCorrelation } from './logger.js';
1556
1938
 
@@ -1629,35 +2011,35 @@ export function metricsHandlerExpress(_req: any, res: any): void {
1629
2011
  // while making the canonical class importable from a dedicated file.
1630
2012
  export { CircuitBreaker } from './circuit-breaker.js';
1631
2013
  `;
1632
- fs.writeFileSync(path.join(scaffoldDir, 'middleware.ts'), middlewareCode, 'utf-8');
1633
- totalCopied += 1;
1634
- // ADR-PIPELINE-069: standalone scaffolded circuit-breaker module so
1635
- // generators can import CircuitBreaker without pulling all of
1636
- // middleware.ts. Owned by the scaffold (see OWNED_SCAFFOLD_MODULES).
1637
- fs.writeFileSync(path.join(scaffoldDir, 'circuit-breaker.ts'), CIRCUIT_BREAKER_SCAFFOLD, 'utf-8');
1638
- totalCopied += 1;
1639
- // ADR-PIPELINE-076: wire-complete Hono base app. Generators extend
1640
- // via createBaseApp(deps).route('/api/<domain>', router) instead of
1641
- // rebuilding the middleware/metrics/health plumbing from scratch.
1642
- // PGV-018 will fail the build if /metrics, /health/live, /health/ready
1643
- // are missing from the final project tree.
1644
- const apiDir = path.join(scaffoldDir, 'api');
1645
- fs.mkdirSync(apiDir, { recursive: true });
1646
- fs.writeFileSync(path.join(apiDir, 'base-app.ts'), BASE_APP_SCAFFOLD_HONO, 'utf-8');
1647
- totalCopied += 1;
1648
- // ADR-PIPELINE-077: ERP schema provenance helper. Every generated
1649
- // ERP adapter MUST export an ERP_SCHEMA_PROVENANCE constant and call
1650
- // assertErpProvenanceOrFail at construction so strict-mode deployments
1651
- // block on unreviewed schemas. PGV-020 enforces presence, PGV-021
1652
- // enforces reviewer + catalog_version on validated entries.
1653
- const erpDir = path.join(scaffoldDir, 'erp');
1654
- fs.mkdirSync(erpDir, { recursive: true });
1655
- fs.writeFileSync(path.join(erpDir, 'schema-provenance.ts'), ERP_SCHEMA_PROVENANCE_SCAFFOLD, 'utf-8');
1656
- totalCopied += 1;
1657
- // ADR-PIPELINE-066: unit-economics helper. The demo script calls
1658
- // writeUnitEconomics() to emit a machine-readable manifest that the
1659
- // executive renderer prefers over per-employee heuristics.
1660
- const unitEconomicsCode = `// Auto-generated by Agentics pipeline (ADR-PIPELINE-066)
2014
+ fs.writeFileSync(path.join(scaffoldDir, 'middleware.ts'), middlewareCode, 'utf-8');
2015
+ totalCopied += 1;
2016
+ // ADR-PIPELINE-069: standalone scaffolded circuit-breaker module so
2017
+ // generators can import CircuitBreaker without pulling all of
2018
+ // middleware.ts. Owned by the scaffold (see OWNED_SCAFFOLD_MODULES).
2019
+ fs.writeFileSync(path.join(scaffoldDir, 'circuit-breaker.ts'), CIRCUIT_BREAKER_SCAFFOLD, 'utf-8');
2020
+ totalCopied += 1;
2021
+ // ADR-PIPELINE-076: wire-complete Hono base app. Generators extend
2022
+ // via createBaseApp(deps).route('/api/<domain>', router) instead of
2023
+ // rebuilding the middleware/metrics/health plumbing from scratch.
2024
+ // PGV-018 will fail the build if /metrics, /health/live, /health/ready
2025
+ // are missing from the final project tree.
2026
+ const apiDir = path.join(scaffoldDir, 'api');
2027
+ fs.mkdirSync(apiDir, { recursive: true });
2028
+ fs.writeFileSync(path.join(apiDir, 'base-app.ts'), BASE_APP_SCAFFOLD_HONO, 'utf-8');
2029
+ totalCopied += 1;
2030
+ // ADR-PIPELINE-077: ERP schema provenance helper. Every generated
2031
+ // ERP adapter MUST export an ERP_SCHEMA_PROVENANCE constant and call
2032
+ // assertErpProvenanceOrFail at construction so strict-mode deployments
2033
+ // block on unreviewed schemas. PGV-020 enforces presence, PGV-021
2034
+ // enforces reviewer + catalog_version on validated entries.
2035
+ const erpDir = path.join(scaffoldDir, 'erp');
2036
+ fs.mkdirSync(erpDir, { recursive: true });
2037
+ fs.writeFileSync(path.join(erpDir, 'schema-provenance.ts'), ERP_SCHEMA_PROVENANCE_SCAFFOLD, 'utf-8');
2038
+ totalCopied += 1;
2039
+ // ADR-PIPELINE-066: unit-economics helper. The demo script calls
2040
+ // writeUnitEconomics() to emit a machine-readable manifest that the
2041
+ // executive renderer prefers over per-employee heuristics.
2042
+ const unitEconomicsCode = `// Auto-generated by Agentics pipeline (ADR-PIPELINE-066)
1661
2043
  // Writes .agentics/runs/<run-id>/unit-economics.json so the executive
1662
2044
  // renderer can use bottom-up unit economics instead of heuristics.
1663
2045
  import { mkdirSync, writeFileSync, existsSync, readFileSync } from 'node:fs';
@@ -1750,57 +2132,62 @@ export function readUnitEconomics(runDir: string): UnitEconomics | null {
1750
2132
  }
1751
2133
  }
1752
2134
  `;
1753
- fs.writeFileSync(path.join(scaffoldDir, 'unit-economics.ts'), unitEconomicsCode, 'utf-8');
1754
- totalCopied += 1;
1755
- // ADR-PIPELINE-068: ESM-safe simulation-lineage helper. Every generated
1756
- // project gets a loader that reads .agentics/plans/manifest.json using
1757
- // readFileSync + fileURLToPath. NEVER use CommonJS require() in the
1758
- // generated project — it is "type": "module" and require() throws at
1759
- // runtime, producing a silent sim-unknown fallback that severs lineage.
1760
- // The string body lives at module scope (SIMULATION_LINEAGE_SCAFFOLD)
1761
- // so it can be unit-tested without invoking copyPlanningArtifacts.
1762
- fs.writeFileSync(path.join(scaffoldDir, 'simulation-lineage.ts'), SIMULATION_LINEAGE_SCAFFOLD, 'utf-8');
1763
- totalCopied += 1;
1764
- // ADR-PIPELINE-069: Sidecar manifest listing every scaffold-owned
1765
- // module + its public exports. Consumed by:
1766
- // - prompt-generator.ts (injects "do not reimplement" block)
1767
- // - post-generation-validator PGV-012 (bans duplicate declarations)
1768
- // The manifest is the single source of truth — generators MUST NOT
1769
- // re-emit any export listed here.
1770
- const ownedManifestPath = path.join(plansDir, 'scaffold', 'OWNED_MODULES.json');
1771
- fs.writeFileSync(ownedManifestPath, JSON.stringify(buildOwnedModulesManifest(), null, 2) + '\n', 'utf-8');
1772
- totalCopied += 1;
1773
- // ADR-PIPELINE-069: Cleanup pass — scan the project tree for files
1774
- // that redeclare an export listed in OWNED_MODULES.json. Logs the
1775
- // count; under AGENTICS_AUTO_DEDUPE=true, deletes the duplicates.
1776
- try {
1777
- const projectRootForScan = projectRoot;
1778
- const dupes = detectScaffoldDuplicates(projectRootForScan, OWNED_SCAFFOLD_MODULES);
1779
- const autoDedupe = process.env['AGENTICS_AUTO_DEDUPE'] === 'true';
1780
- if (dupes.length > 0) {
1781
- console.error(` [SCAFFOLD] scaffold.duplicate.detected count=${dupes.length}` +
1782
- (autoDedupe ? ' (auto-deduping)' : ' (set AGENTICS_AUTO_DEDUPE=true to delete)'));
1783
- for (const d of dupes) {
1784
- console.error(` - ${d.path} redeclares ${d.exportName} (owned by ${d.ownedPath})`);
1785
- if (autoDedupe) {
1786
- try {
1787
- fs.unlinkSync(d.path);
2135
+ fs.writeFileSync(path.join(scaffoldDir, 'unit-economics.ts'), unitEconomicsCode, 'utf-8');
2136
+ totalCopied += 1;
2137
+ // ADR-PIPELINE-068: ESM-safe simulation-lineage helper. Every generated
2138
+ // project gets a loader that reads .agentics/plans/manifest.json using
2139
+ // readFileSync + fileURLToPath. NEVER use CommonJS require() in the
2140
+ // generated project — it is "type": "module" and require() throws at
2141
+ // runtime, producing a silent sim-unknown fallback that severs lineage.
2142
+ // The string body lives at module scope (SIMULATION_LINEAGE_SCAFFOLD)
2143
+ // so it can be unit-tested without invoking copyPlanningArtifacts.
2144
+ fs.writeFileSync(path.join(scaffoldDir, 'simulation-lineage.ts'), SIMULATION_LINEAGE_SCAFFOLD, 'utf-8');
2145
+ totalCopied += 1;
2146
+ // ADR-PIPELINE-069: Sidecar manifest listing every scaffold-owned
2147
+ // module + its public exports. Consumed by:
2148
+ // - prompt-generator.ts (injects "do not reimplement" block)
2149
+ // - post-generation-validator PGV-012 (bans duplicate declarations)
2150
+ // The manifest is the single source of truth — generators MUST NOT
2151
+ // re-emit any export listed here.
2152
+ const ownedManifestPath = path.join(plansDir, 'scaffold', 'OWNED_MODULES.json');
2153
+ fs.writeFileSync(ownedManifestPath, JSON.stringify(buildOwnedModulesManifest(), null, 2) + '\n', 'utf-8');
2154
+ totalCopied += 1;
2155
+ // ADR-PIPELINE-069: Cleanup pass — scan the project tree for files
2156
+ // that redeclare an export listed in OWNED_MODULES.json. Logs the
2157
+ // count; under AGENTICS_AUTO_DEDUPE=true, deletes the duplicates.
2158
+ try {
2159
+ const projectRootForScan = projectRoot;
2160
+ const dupes = detectScaffoldDuplicates(projectRootForScan, OWNED_SCAFFOLD_MODULES);
2161
+ const autoDedupe = process.env['AGENTICS_AUTO_DEDUPE'] === 'true';
2162
+ if (dupes.length > 0) {
2163
+ console.error(` [SCAFFOLD] scaffold.duplicate.detected count=${dupes.length}` +
2164
+ (autoDedupe ? ' (auto-deduping)' : ' (set AGENTICS_AUTO_DEDUPE=true to delete)'));
2165
+ for (const d of dupes) {
2166
+ console.error(` - ${d.path} redeclares ${d.exportName} (owned by ${d.ownedPath})`);
2167
+ if (autoDedupe) {
2168
+ try {
2169
+ fs.unlinkSync(d.path);
2170
+ }
2171
+ catch { /* best-effort */ }
1788
2172
  }
1789
- catch { /* best-effort */ }
1790
2173
  }
1791
2174
  }
2175
+ else {
2176
+ console.error(' [SCAFFOLD] scaffold.duplicate.detected: 0');
2177
+ }
1792
2178
  }
1793
- else {
1794
- console.error(' [SCAFFOLD] scaffold.duplicate.detected: 0');
2179
+ catch {
2180
+ // Cleanup is best-effort — never block scaffold emission on a scan failure.
1795
2181
  }
1796
- }
1797
- catch {
1798
- // Cleanup is best-effortnever block scaffold emission on a scan failure.
1799
- }
1800
- // Python scaffold (if SPARC specifies Python or query mentions it)
1801
- const pyDir = path.join(plansDir, 'scaffold', 'python');
1802
- fs.mkdirSync(pyDir, { recursive: true });
1803
- fs.writeFileSync(path.join(pyDir, 'logger.py'), `"""Structured JSON logger — auto-generated by Agentics pipeline."""
2182
+ totalCopied += 3; // 3 TS files (logger, config, errors) — PY counted below if emitted
2183
+ } // end if (emitTsScaffold)
2184
+ // ADR-PIPELINE-093 §Rule 5Python scaffold block is gated on
2185
+ // `emitPyScaffold`. TypeScript projects no longer get a stray `python/`
2186
+ // directory full of `.py` files (the originating contamination bug).
2187
+ if (emitPyScaffold) {
2188
+ const pyDir = path.join(plansDir, 'scaffold', 'python');
2189
+ fs.mkdirSync(pyDir, { recursive: true });
2190
+ fs.writeFileSync(path.join(pyDir, 'logger.py'), `"""Structured JSON logger — auto-generated by Agentics pipeline."""
1804
2191
  import json, sys, time
1805
2192
  from typing import Any, Optional
1806
2193
 
@@ -1830,7 +2217,7 @@ class Logger:
1830
2217
  def create_logger(service: str) -> Logger:
1831
2218
  return Logger(service)
1832
2219
  `, 'utf-8');
1833
- fs.writeFileSync(path.join(pyDir, 'config.py'), `"""Environment-based configuration — auto-generated by Agentics pipeline."""
2220
+ fs.writeFileSync(path.join(pyDir, 'config.py'), `"""Environment-based configuration — auto-generated by Agentics pipeline."""
1834
2221
  import os
1835
2222
 
1836
2223
  class AppConfig:
@@ -1844,7 +2231,7 @@ class AppConfig:
1844
2231
 
1845
2232
  config = AppConfig()
1846
2233
  `, 'utf-8');
1847
- fs.writeFileSync(path.join(pyDir, 'errors.py'), `"""Typed error hierarchy — auto-generated by Agentics pipeline."""
2234
+ fs.writeFileSync(path.join(pyDir, 'errors.py'), `"""Typed error hierarchy — auto-generated by Agentics pipeline."""
1848
2235
 
1849
2236
  class AppError(Exception):
1850
2237
  def __init__(self, message: str, code: str, status_code: int = 500):
@@ -1865,7 +2252,8 @@ class ERPError(AppError):
1865
2252
  super().__init__(message, "ERP_ERROR", 502)
1866
2253
  self.retryable = retryable
1867
2254
  `, 'utf-8');
1868
- totalCopied += 6; // 3 TS + 3 Python
2255
+ totalCopied += 3; // 3 Python files (logger, config, errors)
2256
+ } // end if (emitPyScaffold)
1869
2257
  if (totalCopied > 0 || totalSkipped > 0) {
1870
2258
  const parts = [];
1871
2259
  if (totalCopied > 0)
@@ -1874,6 +2262,10 @@ class ERPError(AppError):
1874
2262
  parts.push(`${totalSkipped} unchanged (skipped)`);
1875
2263
  console.error(` [PLANS] ${parts.join(', ')} → .agentics/plans/`);
1876
2264
  }
2265
+ // ADR-PIPELINE-092, Rule 5 + `planPromotion` manifest block:
2266
+ // merge per-slot counts + missing_slots into runDir/manifest.json so CI
2267
+ // can assert on plan-promotion completeness without parsing stderr.
2268
+ writePlanPromotionToManifest(runDir, promotionSummary);
1877
2269
  }
1878
2270
  catch {
1879
2271
  // Non-fatal — never block the pipeline for artifact copy
@@ -2065,6 +2457,23 @@ export async function executeAutoChain(traceId, options = {}) {
2065
2457
  const homeDir = process.env['HOME'] ?? '/tmp';
2066
2458
  const runDir = path.join(homeDir, '.agentics', 'runs', traceId);
2067
2459
  const phases = [];
2460
+ // ADR-PIPELINE-091: per-phase Ruflo primary-executor results. Filled in by
2461
+ // `runRufloPrimaryForPhase` before each downstream coordinator runs; read
2462
+ // at the end of this function to build the manifest `execution` block.
2463
+ const rufloPrimaryResults = {};
2464
+ // ADR-PIPELINE-093: per-phase gate outcomes. Fed by `runPhaseGate` before
2465
+ // each coordinator runs; flushed to `manifest.json` by
2466
+ // `writeGateBlockToManifest` at pipeline completion.
2467
+ const gateAcc = newGateAccumulator();
2468
+ // Per-phase block flags — set by the gate and consulted by downstream
2469
+ // phases to decide whether to attempt their coordinator at all.
2470
+ const phaseBlocked = {
2471
+ phase2: false,
2472
+ 'phase3-sparc': false,
2473
+ 'phase4-adrs-ddd': false,
2474
+ 'phase5a-prompts': false,
2475
+ 'phase5b-scaffold': false,
2476
+ };
2068
2477
  // ── Detect execution mode ──
2069
2478
  const execCtx = detectExecutionMode();
2070
2479
  const mode = options.executionMode ?? execCtx.mode;
@@ -2140,53 +2549,64 @@ export async function executeAutoChain(traceId, options = {}) {
2140
2549
  phases.push({ phase: 2, label: 'Deep Research', status: 'skipped', timing: 0, artifacts: [], outputDir: phaseDir });
2141
2550
  }
2142
2551
  else {
2143
- // Clean up incomplete phase2 directory from a prior failed run
2144
- if (fs.existsSync(phaseDir) && !fs.existsSync(path.join(phaseDir, 'sparc', 'sparc-combined.json'))) {
2145
- console.error(' [CLEANUP] Removing incomplete phase2 directory from prior run');
2146
- fs.rmSync(phaseDir, { recursive: true, force: true });
2147
- }
2148
- const agentResults = await dispatchPhaseAgents(PHASE_AGENTS[2], traceId, runDir, scenarioQuery);
2149
- // Ruflo swarm + agentics agents: write deep research cooperatively
2150
- // Output to .ruflo-cache/ so we don't pre-create phaseDir (append-only guard in executePhaseNCommand)
2151
- const rufloOutputDir = path.join(runDir, '.ruflo-cache', 'phase2');
2152
- const rufloResult = executeRufloPhaseSwarm({
2153
- phase: 2, label: 'Deep Research', scenarioQuery,
2154
- runDir, traceId, outputDir: rufloOutputDir,
2155
- tasks: buildPhase2Tasks(scenarioQuery, collectPhase2Artifacts(runDir)),
2156
- agenticsResults: agentResults,
2157
- priorArtifacts: collectPhase2Artifacts(runDir),
2158
- });
2159
- if (rufloResult.filesModified > 0) {
2160
- console.error(` [RUFLO-P2] ${rufloResult.filesModified} files generated (swarm+agents) in ${rufloResult.timing}ms`);
2161
- }
2162
- // Persist agent results BEFORE phase command so the SPARC generator,
2163
- // research dossier, ADR generator, and DDD generator can read agent findings.
2164
- // Write to .pre-phase2 — generators check this location if phase2Dir doesn't have the report yet.
2165
- // CANNOT write to phase2Dir directly — the phase command has an append-only guard that
2166
- // throws ECLI-P2-003 if the directory already exists.
2167
- persistAgenticsResults(path.join(runDir, '.pre-phase2'), agentResults);
2168
- try {
2169
- const result = await executePhase2Command({ trace: traceId });
2170
- mergeRufloCacheIntoPhase(runDir, 2);
2171
- const timing = Date.now() - phaseStart;
2172
- const artifactPaths = extractArtifactPaths(result.manifest.artifacts);
2173
- console.error(formatPhase2ForDisplay(result));
2174
- printArtifactLinks(phaseDir, artifactPaths);
2175
- storePhaseArtifacts(PHASE_AGENTS[2], traceId, runDir, artifactPaths, timing);
2176
- await reviewPhaseOutput(PHASE_AGENTS[2], traceId, runDir);
2177
- persistAgenticsResults(phaseDir, agentResults);
2178
- phases.push({ phase: 2, label: 'Deep Research', status: 'completed', timing, artifacts: artifactPaths, outputDir: phaseDir, agenticsAgents: buildAgentSummary(agentResults, PHASE_AGENTS[2].agenticsServices.length) });
2179
- copyPlanningArtifacts(runDir, projectRoot);
2180
- }
2181
- catch (err) {
2182
- const errMsg = err instanceof Error ? err.message : String(err);
2183
- console.error(` [FAIL] Phase 2 failed: ${errMsg}`);
2184
- recordPhaseFailure(PHASE_AGENTS[2], traceId, errMsg);
2185
- phases.push({ phase: 2, label: 'Deep Research', status: 'failed', timing: Date.now() - phaseStart, artifacts: [], outputDir: phaseDir, error: errMsg });
2186
- copyPlanningArtifacts(runDir, projectRoot);
2187
- await ensureAdrsExist(runDir, traceId, scenarioQuery);
2188
- return buildResult(traceId, runDir, phases, pipelineStart, mode);
2552
+ // ADR-PIPELINE-093 Phase 2 gate. Verifies Phase 1 required inputs
2553
+ // (manifest.json + executive-summary.md) are present before dispatch.
2554
+ const gate = await runPhaseGate('phase2', scenarioQuery, traceId, runDir, gateAcc);
2555
+ if (!gate.ok && gate.stillMissing.length > 0) {
2556
+ phaseBlocked['phase2'] = true;
2557
+ console.error(' [SKIP] Phase 2 blocked by gate required Phase 1 inputs missing');
2558
+ phases.push({ phase: 2, label: 'Deep Research', status: 'skipped', timing: Date.now() - phaseStart, artifacts: [], outputDir: phaseDir, error: `blocked: ${gate.stillMissing.join(', ')}` });
2559
+ // Fall through to downstream phases; each has its own gate.
2189
2560
  }
2561
+ else {
2562
+ // Clean up incomplete phase2 directory from a prior failed run
2563
+ if (fs.existsSync(phaseDir) && !fs.existsSync(path.join(phaseDir, 'sparc', 'sparc-combined.json'))) {
2564
+ console.error(' [CLEANUP] Removing incomplete phase2 directory from prior run');
2565
+ fs.rmSync(phaseDir, { recursive: true, force: true });
2566
+ }
2567
+ const agentResults = await dispatchPhaseAgents(PHASE_AGENTS[2], traceId, runDir, scenarioQuery);
2568
+ // Ruflo swarm + agentics agents: write deep research cooperatively
2569
+ // Output to .ruflo-cache/ so we don't pre-create phaseDir (append-only guard in executePhaseNCommand)
2570
+ const rufloOutputDir = path.join(runDir, '.ruflo-cache', 'phase2');
2571
+ const rufloResult = executeRufloPhaseSwarm({
2572
+ phase: 2, label: 'Deep Research', scenarioQuery,
2573
+ runDir, traceId, outputDir: rufloOutputDir,
2574
+ tasks: buildPhase2Tasks(scenarioQuery, collectPhase2Artifacts(runDir)),
2575
+ agenticsResults: agentResults,
2576
+ priorArtifacts: collectPhase2Artifacts(runDir),
2577
+ });
2578
+ if (rufloResult.filesModified > 0) {
2579
+ console.error(` [RUFLO-P2] ${rufloResult.filesModified} files generated (swarm+agents) in ${rufloResult.timing}ms`);
2580
+ }
2581
+ // Persist agent results BEFORE phase command so the SPARC generator,
2582
+ // research dossier, ADR generator, and DDD generator can read agent findings.
2583
+ // Write to .pre-phase2 — generators check this location if phase2Dir doesn't have the report yet.
2584
+ // CANNOT write to phase2Dir directly — the phase command has an append-only guard that
2585
+ // throws ECLI-P2-003 if the directory already exists.
2586
+ persistAgenticsResults(path.join(runDir, '.pre-phase2'), agentResults);
2587
+ try {
2588
+ const result = await executePhase2Command({ trace: traceId });
2589
+ mergeRufloCacheIntoPhase(runDir, 2);
2590
+ const timing = Date.now() - phaseStart;
2591
+ const artifactPaths = extractArtifactPaths(result.manifest.artifacts);
2592
+ console.error(formatPhase2ForDisplay(result));
2593
+ printArtifactLinks(phaseDir, artifactPaths);
2594
+ storePhaseArtifacts(PHASE_AGENTS[2], traceId, runDir, artifactPaths, timing);
2595
+ await reviewPhaseOutput(PHASE_AGENTS[2], traceId, runDir);
2596
+ persistAgenticsResults(phaseDir, agentResults);
2597
+ phases.push({ phase: 2, label: 'Deep Research', status: 'completed', timing, artifacts: artifactPaths, outputDir: phaseDir, agenticsAgents: buildAgentSummary(agentResults, PHASE_AGENTS[2].agenticsServices.length) });
2598
+ copyPlanningArtifacts(runDir, projectRoot);
2599
+ }
2600
+ catch (err) {
2601
+ const errMsg = err instanceof Error ? err.message : String(err);
2602
+ console.error(` [FAIL] Phase 2 failed: ${errMsg}`);
2603
+ recordPhaseFailure(PHASE_AGENTS[2], traceId, errMsg);
2604
+ phases.push({ phase: 2, label: 'Deep Research', status: 'failed', timing: Date.now() - phaseStart, artifacts: [], outputDir: phaseDir, error: errMsg });
2605
+ copyPlanningArtifacts(runDir, projectRoot);
2606
+ await ensureAdrsExist(runDir, traceId, scenarioQuery);
2607
+ return buildResult(traceId, runDir, phases, pipelineStart, mode);
2608
+ }
2609
+ } // close ADR-PIPELINE-093 phase2 gate-else
2190
2610
  }
2191
2611
  }
2192
2612
  // ── Phase 3: SPARC + London TDD ──
@@ -2209,82 +2629,99 @@ export async function executeAutoChain(traceId, options = {}) {
2209
2629
  console.error(' [CLEANUP] Removing incomplete phase3 directory from prior run');
2210
2630
  fs.rmSync(phaseDir, { recursive: true, force: true });
2211
2631
  }
2212
- const agentResults = await dispatchPhaseAgents(PHASE_AGENTS[3], traceId, runDir, scenarioQuery);
2213
- // Ruflo swarm + agentics agents: write SPARC specs + TDD plans cooperatively
2214
- const rufloP3Dir = path.join(runDir, '.ruflo-cache', 'phase3');
2215
- const rufloResult = executeRufloPhaseSwarm({
2216
- phase: 3, label: 'SPARC + London TDD', scenarioQuery,
2217
- runDir, traceId, outputDir: rufloP3Dir,
2218
- tasks: buildPhase3Tasks(scenarioQuery, collectPhase3Artifacts(runDir)),
2219
- agenticsResults: agentResults,
2220
- priorArtifacts: collectPhase3Artifacts(runDir),
2221
- });
2222
- if (rufloResult.filesModified > 0) {
2223
- console.error(` [RUFLO-P3] ${rufloResult.filesModified} files generated (swarm+agents) in ${rufloResult.timing}ms`);
2224
- }
2225
- // Persist agent results to .pre-phase3 so generators can find them.
2226
- // CANNOT write to phase3Dir — append-only guard throws if it exists.
2227
- persistAgenticsResults(path.join(runDir, '.pre-phase3'), agentResults);
2228
- try {
2229
- const result = await executePhase3Command({ trace: traceId });
2230
- mergeRufloCacheIntoPhase(runDir, 3);
2231
- const timing = Date.now() - phaseStart;
2232
- const artifactPaths = extractArtifactPaths(result.manifest.artifacts);
2233
- console.error(formatPhase3ForDisplay(result));
2234
- printArtifactLinks(phaseDir, artifactPaths);
2235
- storePhaseArtifacts(PHASE_AGENTS[3], traceId, runDir, artifactPaths, timing);
2236
- await reviewPhaseOutput(PHASE_AGENTS[3], traceId, runDir);
2237
- persistAgenticsResults(phaseDir, agentResults);
2238
- phases.push({ phase: 3, label: 'SPARC + London TDD', status: 'completed', timing, artifacts: artifactPaths, outputDir: phaseDir, agenticsAgents: buildAgentSummary(agentResults, PHASE_AGENTS[3].agenticsServices.length) });
2239
- copyPlanningArtifacts(runDir, projectRoot);
2632
+ // ADR-PIPELINE-093 Phase 3 gate runs BEFORE the ADR-091 Ruflo-primary
2633
+ // call so the gate verifies (and, if necessary, regenerates) upstream
2634
+ // inputs before the phase's own Ruflo pass executes.
2635
+ const phase3Gate = await runPhaseGate('phase3-sparc', scenarioQuery, traceId, runDir, gateAcc);
2636
+ if (!phase3Gate.ok && phase3Gate.stillMissing.length > 0) {
2637
+ phaseBlocked['phase3-sparc'] = true;
2638
+ console.error(' [SKIP] Phase 3 blocked by gate — upstream inputs unrecoverable');
2639
+ phases.push({ phase: 3, label: 'SPARC + London TDD', status: 'skipped', timing: Date.now() - phaseStart, artifacts: [], outputDir: phaseDir, error: `blocked: ${phase3Gate.stillMissing.join(', ')}` });
2640
+ // Continue to Phase 4 — its own gate decides whether it can run.
2240
2641
  }
2241
- catch (err) {
2242
- const errMsg = err instanceof Error ? err.message : String(err);
2243
- console.error(` [FAIL] Phase 3 failed: ${errMsg}`);
2244
- recordPhaseFailure(PHASE_AGENTS[3], traceId, errMsg);
2245
- // Phase 3 refines Phase 2 SPARC via LLM. If it fails (timeout, no LLM),
2246
- // carry Phase 2 artifacts forward so Phases 4-6 can still proceed.
2247
- const phase2SparcDir = path.join(runDir, 'phase2', 'sparc');
2248
- const phase2SparcCombined = path.join(phase2SparcDir, 'sparc-combined.json');
2249
- if (fs.existsSync(phase2SparcCombined)) {
2250
- console.error(' [RECOVER] Phase 2 SPARC artifacts exist — copying to phase3/ so pipeline can continue');
2251
- const phase3SparcDir = path.join(phaseDir, 'sparc');
2252
- fs.mkdirSync(phase3SparcDir, { recursive: true });
2253
- // Copy all Phase 2 SPARC files to Phase 3 directory
2254
- const phase2SparcFiles = fs.readdirSync(phase2SparcDir);
2255
- for (const file of phase2SparcFiles) {
2256
- const src = path.join(phase2SparcDir, file);
2257
- const dest = path.join(phase3SparcDir, file);
2258
- if (fs.statSync(src).isFile()) {
2259
- fs.copyFileSync(src, dest);
2260
- }
2261
- }
2262
- // Also copy TDD if available from Phase 2
2263
- const phase2TddDir = path.join(runDir, 'phase2', 'tdd');
2264
- if (fs.existsSync(phase2TddDir)) {
2265
- const phase3TddDir = path.join(phaseDir, 'tdd');
2266
- fs.mkdirSync(phase3TddDir, { recursive: true });
2267
- const phase2TddFiles = fs.readdirSync(phase2TddDir);
2268
- for (const file of phase2TddFiles) {
2269
- const src = path.join(phase2TddDir, file);
2270
- const dest = path.join(phase3TddDir, file);
2642
+ else {
2643
+ // ADR-PIPELINE-091: Ruflo runs FIRST as the primary engineering-artifact
2644
+ // producer. Its output lands in runDir/engineering/*.md; the downstream
2645
+ // phase3 coordinator picks it up as its primary input. Remote diligence
2646
+ // agents that DID return from Phase 1 are attached as enrichment only.
2647
+ rufloPrimaryResults.phase3 = await runRufloPrimaryForPhase('phase3-sparc', scenarioQuery, traceId, runDir, collectPhase3Artifacts(runDir), readRemoteEnrichmentSnapshot(runDir), path.join(runDir, '.ruflo-cache', 'phase3-primary'));
2648
+ const agentResults = await dispatchPhaseAgents(PHASE_AGENTS[3], traceId, runDir, scenarioQuery);
2649
+ // Ruflo swarm + agentics agents: write SPARC specs + TDD plans cooperatively
2650
+ const rufloP3Dir = path.join(runDir, '.ruflo-cache', 'phase3');
2651
+ const rufloResult = executeRufloPhaseSwarm({
2652
+ phase: 3, label: 'SPARC + London TDD', scenarioQuery,
2653
+ runDir, traceId, outputDir: rufloP3Dir,
2654
+ tasks: buildPhase3Tasks(scenarioQuery, collectPhase3Artifacts(runDir)),
2655
+ agenticsResults: agentResults,
2656
+ priorArtifacts: collectPhase3Artifacts(runDir),
2657
+ });
2658
+ if (rufloResult.filesModified > 0) {
2659
+ console.error(` [RUFLO-P3] ${rufloResult.filesModified} files generated (swarm+agents) in ${rufloResult.timing}ms`);
2660
+ }
2661
+ // Persist agent results to .pre-phase3 so generators can find them.
2662
+ // CANNOT write to phase3Dir — append-only guard throws if it exists.
2663
+ persistAgenticsResults(path.join(runDir, '.pre-phase3'), agentResults);
2664
+ try {
2665
+ const result = await executePhase3Command({ trace: traceId });
2666
+ mergeRufloCacheIntoPhase(runDir, 3);
2667
+ const timing = Date.now() - phaseStart;
2668
+ const artifactPaths = extractArtifactPaths(result.manifest.artifacts);
2669
+ console.error(formatPhase3ForDisplay(result));
2670
+ printArtifactLinks(phaseDir, artifactPaths);
2671
+ storePhaseArtifacts(PHASE_AGENTS[3], traceId, runDir, artifactPaths, timing);
2672
+ await reviewPhaseOutput(PHASE_AGENTS[3], traceId, runDir);
2673
+ persistAgenticsResults(phaseDir, agentResults);
2674
+ phases.push({ phase: 3, label: 'SPARC + London TDD', status: 'completed', timing, artifacts: artifactPaths, outputDir: phaseDir, agenticsAgents: buildAgentSummary(agentResults, PHASE_AGENTS[3].agenticsServices.length) });
2675
+ copyPlanningArtifacts(runDir, projectRoot);
2676
+ }
2677
+ catch (err) {
2678
+ const errMsg = err instanceof Error ? err.message : String(err);
2679
+ console.error(` [FAIL] Phase 3 failed: ${errMsg}`);
2680
+ recordPhaseFailure(PHASE_AGENTS[3], traceId, errMsg);
2681
+ // Phase 3 refines Phase 2 SPARC via LLM. If it fails (timeout, no LLM),
2682
+ // carry Phase 2 artifacts forward so Phases 4-6 can still proceed.
2683
+ const phase2SparcDir = path.join(runDir, 'phase2', 'sparc');
2684
+ const phase2SparcCombined = path.join(phase2SparcDir, 'sparc-combined.json');
2685
+ if (fs.existsSync(phase2SparcCombined)) {
2686
+ console.error(' [RECOVER] Phase 2 SPARC artifacts exist — copying to phase3/ so pipeline can continue');
2687
+ const phase3SparcDir = path.join(phaseDir, 'sparc');
2688
+ fs.mkdirSync(phase3SparcDir, { recursive: true });
2689
+ // Copy all Phase 2 SPARC files to Phase 3 directory
2690
+ const phase2SparcFiles = fs.readdirSync(phase2SparcDir);
2691
+ for (const file of phase2SparcFiles) {
2692
+ const src = path.join(phase2SparcDir, file);
2693
+ const dest = path.join(phase3SparcDir, file);
2271
2694
  if (fs.statSync(src).isFile()) {
2272
2695
  fs.copyFileSync(src, dest);
2273
2696
  }
2274
2697
  }
2698
+ // Also copy TDD if available from Phase 2
2699
+ const phase2TddDir = path.join(runDir, 'phase2', 'tdd');
2700
+ if (fs.existsSync(phase2TddDir)) {
2701
+ const phase3TddDir = path.join(phaseDir, 'tdd');
2702
+ fs.mkdirSync(phase3TddDir, { recursive: true });
2703
+ const phase2TddFiles = fs.readdirSync(phase2TddDir);
2704
+ for (const file of phase2TddFiles) {
2705
+ const src = path.join(phase2TddDir, file);
2706
+ const dest = path.join(phase3TddDir, file);
2707
+ if (fs.statSync(src).isFile()) {
2708
+ fs.copyFileSync(src, dest);
2709
+ }
2710
+ }
2711
+ }
2712
+ console.error(' [RECOVER] Phase 2 artifacts carried forward — continuing to Phase 4');
2713
+ phases.push({ phase: 3, label: 'SPARC + London TDD', status: 'completed', timing: Date.now() - phaseStart, artifacts: [], outputDir: phaseDir, error: `Recovered from Phase 2: ${errMsg}` });
2714
+ copyPlanningArtifacts(runDir, projectRoot);
2715
+ }
2716
+ else {
2717
+ console.error(' [ABORT] No Phase 2 SPARC artifacts to recover from — pipeline cannot continue');
2718
+ phases.push({ phase: 3, label: 'SPARC + London TDD', status: 'failed', timing: Date.now() - phaseStart, artifacts: [], outputDir: phaseDir, error: errMsg });
2719
+ copyPlanningArtifacts(runDir, projectRoot);
2720
+ await ensureAdrsExist(runDir, traceId, scenarioQuery);
2721
+ return buildResult(traceId, runDir, phases, pipelineStart, mode);
2275
2722
  }
2276
- console.error(' [RECOVER] Phase 2 artifacts carried forward — continuing to Phase 4');
2277
- phases.push({ phase: 3, label: 'SPARC + London TDD', status: 'completed', timing: Date.now() - phaseStart, artifacts: [], outputDir: phaseDir, error: `Recovered from Phase 2: ${errMsg}` });
2278
- copyPlanningArtifacts(runDir, projectRoot);
2279
- }
2280
- else {
2281
- console.error(' [ABORT] No Phase 2 SPARC artifacts to recover from — pipeline cannot continue');
2282
- phases.push({ phase: 3, label: 'SPARC + London TDD', status: 'failed', timing: Date.now() - phaseStart, artifacts: [], outputDir: phaseDir, error: errMsg });
2283
- copyPlanningArtifacts(runDir, projectRoot);
2284
- await ensureAdrsExist(runDir, traceId, scenarioQuery);
2285
- return buildResult(traceId, runDir, phases, pipelineStart, mode);
2286
2723
  }
2287
- }
2724
+ } // close ADR-PIPELINE-093 phase3-sparc gate-else
2288
2725
  }
2289
2726
  }
2290
2727
  // ── Phase 4: ADRs + DDDs ──
@@ -2309,153 +2746,170 @@ export async function executeAutoChain(traceId, options = {}) {
2309
2746
  console.error(' [CLEANUP] Removing incomplete phase4 directory from prior run');
2310
2747
  fs.rmSync(phaseDir, { recursive: true, force: true });
2311
2748
  }
2312
- const agentResults = await dispatchPhaseAgents(PHASE_AGENTS[4], traceId, runDir, scenarioQuery);
2313
- // Ruflo swarm + agentics agents: write ADRs + DDDs cooperatively
2314
- const rufloP4Dir = path.join(runDir, '.ruflo-cache', 'phase4');
2315
- const rufloResult = executeRufloPhaseSwarm({
2316
- phase: 4, label: 'ADRs + DDDs', scenarioQuery,
2317
- runDir, traceId, outputDir: rufloP4Dir,
2318
- tasks: buildPhase4Tasks(scenarioQuery, collectPhase4Artifacts(runDir)),
2319
- agenticsResults: agentResults,
2320
- priorArtifacts: collectPhase4Artifacts(runDir),
2321
- });
2322
- if (rufloResult.filesModified > 0) {
2323
- console.error(` [RUFLO-P4] ${rufloResult.filesModified} files generated (swarm+agents) in ${rufloResult.timing}ms`);
2324
- }
2325
- // Persist agent results to .pre-phase4 so ADR/DDD generators can find them.
2326
- // CANNOT write to phase4Dir — append-only guard throws if it exists.
2327
- persistAgenticsResults(path.join(runDir, '.pre-phase4'), agentResults);
2328
- try {
2329
- const result = await executePhase4Command({ trace: traceId });
2330
- mergeRufloCacheIntoPhase(runDir, 4);
2331
- const timing = Date.now() - phaseStart;
2332
- const artifactPaths = extractArtifactPaths(result.manifest.artifacts);
2333
- console.error(formatPhase4ForDisplay(result));
2334
- printArtifactLinks(phaseDir, artifactPaths);
2335
- storePhaseArtifacts(PHASE_AGENTS[4], traceId, runDir, artifactPaths, timing);
2336
- await reviewPhaseOutput(PHASE_AGENTS[4], traceId, runDir);
2337
- persistAgenticsResults(phaseDir, agentResults);
2338
- phases.push({ phase: 4, label: 'ADRs + DDDs', status: 'completed', timing, artifacts: artifactPaths, outputDir: phaseDir, agenticsAgents: buildAgentSummary(agentResults, PHASE_AGENTS[4].agenticsServices.length) });
2339
- copyPlanningArtifacts(runDir, projectRoot);
2749
+ // ADR-PIPELINE-093 Phase 4 gate runs BEFORE the ADR-091 Ruflo-primary
2750
+ // call so the gate verifies (and regenerates) the SPARC spec upstream
2751
+ // requirement before the phase's own Ruflo pass runs.
2752
+ const phase4Gate = await runPhaseGate('phase4-adrs-ddd', scenarioQuery, traceId, runDir, gateAcc);
2753
+ if (!phase4Gate.ok && phase4Gate.stillMissing.length > 0) {
2754
+ phaseBlocked['phase4-adrs-ddd'] = true;
2755
+ console.error(' [SKIP] Phase 4 blocked by gate — upstream inputs unrecoverable');
2756
+ phases.push({ phase: 4, label: 'ADRs + DDDs', status: 'skipped', timing: Date.now() - phaseStart, artifacts: [], outputDir: phaseDir, error: `blocked: ${phase4Gate.stillMissing.join(', ')}` });
2757
+ // Continue to Phase 5a — its own gate decides reachability.
2340
2758
  }
2341
- catch (err) {
2342
- const errMsg = err instanceof Error ? err.message : String(err);
2343
- console.error(` [FAIL] Phase 4 failed: ${errMsg}`);
2344
- recordPhaseFailure(PHASE_AGENTS[4], traceId, errMsg);
2345
- // Phase 4 refines Phase 2 ADRs/DDD. If it fails, try recovery paths.
2346
- const phase2AdrIndex = path.join(runDir, 'phase2', 'adrs', 'adr-index.json');
2347
- let recovered = false;
2348
- if (fs.existsSync(phase2AdrIndex)) {
2349
- // Path A: Phase 2 has ADRs — copy them to phase4/
2350
- console.error(' [RECOVER] Phase 2 ADRs/DDD exist — copying to phase4/ so pipeline can continue');
2351
- for (const sub of ['adrs', 'ddd']) {
2352
- const src = path.join(runDir, 'phase2', sub);
2353
- const dest = path.join(phaseDir, sub);
2354
- if (fs.existsSync(src)) {
2355
- fs.mkdirSync(dest, { recursive: true });
2356
- for (const file of fs.readdirSync(src)) {
2357
- const srcFile = path.join(src, file);
2358
- if (fs.statSync(srcFile).isFile()) {
2359
- fs.copyFileSync(srcFile, path.join(dest, file));
2360
- }
2361
- }
2362
- }
2363
- }
2364
- console.error(' [RECOVER] Phase 2 ADRs/DDD carried forward — continuing to Phase 5');
2365
- recovered = true;
2759
+ else {
2760
+ // ADR-PIPELINE-091: Ruflo runs FIRST for ADR + DDD generation. Its output
2761
+ // lands in runDir/engineering/architecture-decisions.md + domain-model.md
2762
+ // and the phase4 coordinator refines it. Remote diligence agents feed in
2763
+ // as optional enrichment only.
2764
+ rufloPrimaryResults.phase4 = await runRufloPrimaryForPhase('phase4-adrs-ddd', scenarioQuery, traceId, runDir, collectPhase4Artifacts(runDir), readRemoteEnrichmentSnapshot(runDir), path.join(runDir, '.ruflo-cache', 'phase4-primary'));
2765
+ const agentResults = await dispatchPhaseAgents(PHASE_AGENTS[4], traceId, runDir, scenarioQuery);
2766
+ // Ruflo swarm + agentics agents: write ADRs + DDDs cooperatively
2767
+ const rufloP4Dir = path.join(runDir, '.ruflo-cache', 'phase4');
2768
+ const rufloResult = executeRufloPhaseSwarm({
2769
+ phase: 4, label: 'ADRs + DDDs', scenarioQuery,
2770
+ runDir, traceId, outputDir: rufloP4Dir,
2771
+ tasks: buildPhase4Tasks(scenarioQuery, collectPhase4Artifacts(runDir)),
2772
+ agenticsResults: agentResults,
2773
+ priorArtifacts: collectPhase4Artifacts(runDir),
2774
+ });
2775
+ if (rufloResult.filesModified > 0) {
2776
+ console.error(` [RUFLO-P4] ${rufloResult.filesModified} files generated (swarm+agents) in ${rufloResult.timing}ms`);
2366
2777
  }
2367
- // Path B: No Phase 2 ADRs generate them from SPARC + dossier directly
2368
- if (!recovered) {
2369
- console.error(' [RECOVER] No Phase 2 ADRs — generating ADRs from SPARC + dossier (no LLM required)');
2370
- try {
2371
- const { buildPhase2ADRs } = await import('../pipeline/phase2/phases/adr-generator.js');
2372
- const { buildPhase2DDD } = await import('../pipeline/phase2/phases/ddd-generator.js');
2373
- // Load SPARC and dossier from wherever they exist
2374
- let sparc = null;
2375
- let dossier = null;
2376
- for (const sub of ['phase3/sparc/sparc-combined.json', 'phase2/sparc/sparc-combined.json']) {
2377
- const p = path.join(runDir, sub);
2378
- if (fs.existsSync(p)) {
2379
- try {
2380
- sparc = JSON.parse(fs.readFileSync(p, 'utf-8'));
2381
- break;
2778
+ // Persist agent results to .pre-phase4 so ADR/DDD generators can find them.
2779
+ // CANNOT write to phase4Dir — append-only guard throws if it exists.
2780
+ persistAgenticsResults(path.join(runDir, '.pre-phase4'), agentResults);
2781
+ try {
2782
+ const result = await executePhase4Command({ trace: traceId });
2783
+ mergeRufloCacheIntoPhase(runDir, 4);
2784
+ const timing = Date.now() - phaseStart;
2785
+ const artifactPaths = extractArtifactPaths(result.manifest.artifacts);
2786
+ console.error(formatPhase4ForDisplay(result));
2787
+ printArtifactLinks(phaseDir, artifactPaths);
2788
+ storePhaseArtifacts(PHASE_AGENTS[4], traceId, runDir, artifactPaths, timing);
2789
+ await reviewPhaseOutput(PHASE_AGENTS[4], traceId, runDir);
2790
+ persistAgenticsResults(phaseDir, agentResults);
2791
+ phases.push({ phase: 4, label: 'ADRs + DDDs', status: 'completed', timing, artifacts: artifactPaths, outputDir: phaseDir, agenticsAgents: buildAgentSummary(agentResults, PHASE_AGENTS[4].agenticsServices.length) });
2792
+ copyPlanningArtifacts(runDir, projectRoot);
2793
+ }
2794
+ catch (err) {
2795
+ const errMsg = err instanceof Error ? err.message : String(err);
2796
+ console.error(` [FAIL] Phase 4 failed: ${errMsg}`);
2797
+ recordPhaseFailure(PHASE_AGENTS[4], traceId, errMsg);
2798
+ // Phase 4 refines Phase 2 ADRs/DDD. If it fails, try recovery paths.
2799
+ const phase2AdrIndex = path.join(runDir, 'phase2', 'adrs', 'adr-index.json');
2800
+ let recovered = false;
2801
+ if (fs.existsSync(phase2AdrIndex)) {
2802
+ // Path A: Phase 2 has ADRs — copy them to phase4/
2803
+ console.error(' [RECOVER] Phase 2 ADRs/DDD exist — copying to phase4/ so pipeline can continue');
2804
+ for (const sub of ['adrs', 'ddd']) {
2805
+ const src = path.join(runDir, 'phase2', sub);
2806
+ const dest = path.join(phaseDir, sub);
2807
+ if (fs.existsSync(src)) {
2808
+ fs.mkdirSync(dest, { recursive: true });
2809
+ for (const file of fs.readdirSync(src)) {
2810
+ const srcFile = path.join(src, file);
2811
+ if (fs.statSync(srcFile).isFile()) {
2812
+ fs.copyFileSync(srcFile, path.join(dest, file));
2813
+ }
2382
2814
  }
2383
- catch { /* skip */ }
2384
2815
  }
2385
2816
  }
2386
- for (const sub of ['phase2/research-dossier.json']) {
2387
- const p = path.join(runDir, sub);
2388
- if (fs.existsSync(p)) {
2389
- try {
2390
- dossier = JSON.parse(fs.readFileSync(p, 'utf-8'));
2391
- break;
2817
+ console.error(' [RECOVER] Phase 2 ADRs/DDD carried forward — continuing to Phase 5');
2818
+ recovered = true;
2819
+ }
2820
+ // Path B: No Phase 2 ADRs — generate them from SPARC + dossier directly
2821
+ if (!recovered) {
2822
+ console.error(' [RECOVER] No Phase 2 ADRs — generating ADRs from SPARC + dossier (no LLM required)');
2823
+ try {
2824
+ const { buildPhase2ADRs } = await import('../pipeline/phase2/phases/adr-generator.js');
2825
+ const { buildPhase2DDD } = await import('../pipeline/phase2/phases/ddd-generator.js');
2826
+ // Load SPARC and dossier from wherever they exist
2827
+ let sparc = null;
2828
+ let dossier = null;
2829
+ for (const sub of ['phase3/sparc/sparc-combined.json', 'phase2/sparc/sparc-combined.json']) {
2830
+ const p = path.join(runDir, sub);
2831
+ if (fs.existsSync(p)) {
2832
+ try {
2833
+ sparc = JSON.parse(fs.readFileSync(p, 'utf-8'));
2834
+ break;
2835
+ }
2836
+ catch { /* skip */ }
2392
2837
  }
2393
- catch { /* skip */ }
2394
2838
  }
2395
- }
2396
- if (sparc && dossier) {
2397
- // Generate ADRs
2398
- const adrs = buildPhase2ADRs(sparc, dossier, scenarioQuery, true /* skipLLM */);
2399
- if (adrs.length > 0) {
2400
- const adrDir = path.join(phaseDir, 'adrs');
2401
- fs.mkdirSync(adrDir, { recursive: true });
2402
- for (const adr of adrs) {
2403
- const slug = (adr.title || 'untitled').toLowerCase().replace(/[^a-z0-9]+/g, '-').slice(0, 60);
2404
- const filename = `${adr.id}-${slug}.md`;
2405
- const md = [
2406
- `# ${adr.id}: ${adr.title}`,
2407
- `\n**Status:** ${adr.status}`,
2408
- `**Date:** ${adr.date}`,
2409
- `\n## Context\n${adr.context}`,
2410
- `\n## Decision\n${adr.decision}`,
2411
- adr.alternatives?.length > 0 ? `\n## Alternatives Considered\n${adr.alternatives.map((a) => `- **${a.option}** ${a.rejected ? '(rejected)' : '(selected)'}: ${a.rationale}`).join('\n')}` : '',
2412
- adr.consequences?.length > 0 ? `\n## Consequences\n${adr.consequences.map((c) => `- [${c.type === 'positive' ? '+' : c.type === 'negative' ? '-' : '~'}] ${c.description}`).join('\n')}` : '',
2413
- ].filter(Boolean).join('\n');
2414
- fs.writeFileSync(path.join(adrDir, filename), md + '\n', 'utf-8');
2839
+ for (const sub of ['phase2/research-dossier.json']) {
2840
+ const p = path.join(runDir, sub);
2841
+ if (fs.existsSync(p)) {
2842
+ try {
2843
+ dossier = JSON.parse(fs.readFileSync(p, 'utf-8'));
2844
+ break;
2845
+ }
2846
+ catch { /* skip */ }
2415
2847
  }
2416
- fs.writeFileSync(path.join(adrDir, 'adr-index.json'), JSON.stringify(adrs, null, 2) + '\n', 'utf-8');
2417
- console.error(` [RECOVER] Generated ${adrs.length} ADRs from SPARC template`);
2418
2848
  }
2419
- // Generate DDD
2420
- try {
2421
- const dddModel = buildPhase2DDD(sparc, adrs, dossier);
2422
- if (dddModel?.contexts?.length > 0) {
2423
- const dddDir = path.join(phaseDir, 'ddd');
2424
- const contextsDir = path.join(dddDir, 'contexts');
2425
- fs.mkdirSync(contextsDir, { recursive: true });
2426
- fs.writeFileSync(path.join(dddDir, 'domain-model.json'), JSON.stringify(dddModel, null, 2) + '\n', 'utf-8');
2427
- if (dddModel.contextMap) {
2428
- fs.writeFileSync(path.join(dddDir, 'context-map.json'), JSON.stringify(dddModel.contextMap, null, 2) + '\n', 'utf-8');
2849
+ if (sparc && dossier) {
2850
+ // Generate ADRs
2851
+ const adrs = buildPhase2ADRs(sparc, dossier, scenarioQuery, true /* skipLLM */);
2852
+ if (adrs.length > 0) {
2853
+ const adrDir = path.join(phaseDir, 'adrs');
2854
+ fs.mkdirSync(adrDir, { recursive: true });
2855
+ for (const adr of adrs) {
2856
+ const slug = (adr.title || 'untitled').toLowerCase().replace(/[^a-z0-9]+/g, '-').slice(0, 60);
2857
+ const filename = `${adr.id}-${slug}.md`;
2858
+ const md = [
2859
+ `# ${adr.id}: ${adr.title}`,
2860
+ `\n**Status:** ${adr.status}`,
2861
+ `**Date:** ${adr.date}`,
2862
+ `\n## Context\n${adr.context}`,
2863
+ `\n## Decision\n${adr.decision}`,
2864
+ adr.alternatives?.length > 0 ? `\n## Alternatives Considered\n${adr.alternatives.map((a) => `- **${a.option}** ${a.rejected ? '(rejected)' : '(selected)'}: ${a.rationale}`).join('\n')}` : '',
2865
+ adr.consequences?.length > 0 ? `\n## Consequences\n${adr.consequences.map((c) => `- [${c.type === 'positive' ? '+' : c.type === 'negative' ? '-' : '~'}] ${c.description}`).join('\n')}` : '',
2866
+ ].filter(Boolean).join('\n');
2867
+ fs.writeFileSync(path.join(adrDir, filename), md + '\n', 'utf-8');
2429
2868
  }
2430
- for (const ctx of dddModel.contexts) {
2431
- const ctxSlug = ctx.name.toLowerCase().replace(/[^a-z0-9]+/g, '-').slice(0, 60);
2432
- fs.writeFileSync(path.join(contextsDir, `${ctxSlug}.json`), JSON.stringify(ctx, null, 2) + '\n', 'utf-8');
2869
+ fs.writeFileSync(path.join(adrDir, 'adr-index.json'), JSON.stringify(adrs, null, 2) + '\n', 'utf-8');
2870
+ console.error(` [RECOVER] Generated ${adrs.length} ADRs from SPARC template`);
2871
+ }
2872
+ // Generate DDD
2873
+ try {
2874
+ const dddModel = buildPhase2DDD(sparc, adrs, dossier);
2875
+ if (dddModel?.contexts?.length > 0) {
2876
+ const dddDir = path.join(phaseDir, 'ddd');
2877
+ const contextsDir = path.join(dddDir, 'contexts');
2878
+ fs.mkdirSync(contextsDir, { recursive: true });
2879
+ fs.writeFileSync(path.join(dddDir, 'domain-model.json'), JSON.stringify(dddModel, null, 2) + '\n', 'utf-8');
2880
+ if (dddModel.contextMap) {
2881
+ fs.writeFileSync(path.join(dddDir, 'context-map.json'), JSON.stringify(dddModel.contextMap, null, 2) + '\n', 'utf-8');
2882
+ }
2883
+ for (const ctx of dddModel.contexts) {
2884
+ const ctxSlug = ctx.name.toLowerCase().replace(/[^a-z0-9]+/g, '-').slice(0, 60);
2885
+ fs.writeFileSync(path.join(contextsDir, `${ctxSlug}.json`), JSON.stringify(ctx, null, 2) + '\n', 'utf-8');
2886
+ }
2887
+ console.error(` [RECOVER] Generated DDD model with ${dddModel.contexts.length} bounded contexts`);
2433
2888
  }
2434
- console.error(` [RECOVER] Generated DDD model with ${dddModel.contexts.length} bounded contexts`);
2435
2889
  }
2890
+ catch (dddErr) {
2891
+ console.error(` [WARN] DDD generation failed: ${dddErr instanceof Error ? dddErr.message : String(dddErr)}`);
2892
+ }
2893
+ recovered = true;
2436
2894
  }
2437
- catch (dddErr) {
2438
- console.error(` [WARN] DDD generation failed: ${dddErr instanceof Error ? dddErr.message : String(dddErr)}`);
2895
+ else {
2896
+ console.error(' [WARN] SPARC or dossier not found cannot generate ADRs');
2439
2897
  }
2440
- recovered = true;
2441
2898
  }
2442
- else {
2443
- console.error(' [WARN] SPARC or dossier not found cannot generate ADRs');
2899
+ catch (recoverErr) {
2900
+ console.error(` [WARN] ADR recovery failed: ${recoverErr instanceof Error ? recoverErr.message : String(recoverErr)}`);
2444
2901
  }
2445
2902
  }
2446
- catch (recoverErr) {
2447
- console.error(` [WARN] ADR recovery failed: ${recoverErr instanceof Error ? recoverErr.message : String(recoverErr)}`);
2903
+ if (recovered) {
2904
+ phases.push({ phase: 4, label: 'ADRs + DDDs', status: 'completed', timing: Date.now() - phaseStart, artifacts: [], outputDir: phaseDir, error: `Recovered: ${errMsg}` });
2448
2905
  }
2906
+ else {
2907
+ console.error(' [WARN] No recovery possible — continuing without ADRs');
2908
+ phases.push({ phase: 4, label: 'ADRs + DDDs', status: 'failed', timing: Date.now() - phaseStart, artifacts: [], outputDir: phaseDir, error: errMsg });
2909
+ }
2910
+ copyPlanningArtifacts(runDir, projectRoot);
2449
2911
  }
2450
- if (recovered) {
2451
- phases.push({ phase: 4, label: 'ADRs + DDDs', status: 'completed', timing: Date.now() - phaseStart, artifacts: [], outputDir: phaseDir, error: `Recovered: ${errMsg}` });
2452
- }
2453
- else {
2454
- console.error(' [WARN] No recovery possible — continuing without ADRs');
2455
- phases.push({ phase: 4, label: 'ADRs + DDDs', status: 'failed', timing: Date.now() - phaseStart, artifacts: [], outputDir: phaseDir, error: errMsg });
2456
- }
2457
- copyPlanningArtifacts(runDir, projectRoot);
2458
- }
2912
+ } // close ADR-PIPELINE-093 phase4-adrs-ddd gate-else
2459
2913
  }
2460
2914
  }
2461
2915
  // ── Phase 4.5: Generate implementation prompts by reading ADR + DDD files ──
@@ -2470,64 +2924,80 @@ export async function executeAutoChain(traceId, options = {}) {
2470
2924
  console.error(' [PROMPTS] Implementation prompts already exist — skipping');
2471
2925
  }
2472
2926
  else {
2473
- // Gather all ADR and DDD files (prefer phase4, fall back to phase2)
2474
- const adrDir = fs.existsSync(path.join(runDir, 'phase4', 'adrs'))
2475
- ? path.join(runDir, 'phase4', 'adrs')
2476
- : fs.existsSync(path.join(runDir, 'phase2', 'adrs'))
2477
- ? path.join(runDir, 'phase2', 'adrs')
2478
- : null;
2479
- const dddDir = fs.existsSync(path.join(runDir, 'phase4', 'ddd'))
2480
- ? path.join(runDir, 'phase4', 'ddd')
2481
- : fs.existsSync(path.join(runDir, 'phase2', 'ddd'))
2482
- ? path.join(runDir, 'phase2', 'ddd')
2483
- : null;
2484
- const sparcDir = fs.existsSync(path.join(runDir, 'phase3', 'sparc'))
2485
- ? path.join(runDir, 'phase3', 'sparc')
2486
- : fs.existsSync(path.join(runDir, 'phase2', 'sparc'))
2487
- ? path.join(runDir, 'phase2', 'sparc')
2488
- : null;
2489
- if (adrDir) {
2490
- try {
2491
- // Read all ADR markdown files
2492
- const adrFiles = fs.readdirSync(adrDir).filter(f => f.endsWith('.md'));
2493
- const adrContent = adrFiles.map(f => {
2494
- try {
2495
- return `### ${f}\n\n${fs.readFileSync(path.join(adrDir, f), 'utf-8')}`;
2496
- }
2497
- catch {
2498
- return '';
2499
- }
2500
- }).filter(Boolean).join('\n\n---\n\n');
2501
- // Read DDD model files
2502
- let dddContent = '';
2503
- if (dddDir) {
2504
- const dddFiles = fs.readdirSync(dddDir).filter(f => f.endsWith('.md') || f.endsWith('.json'));
2505
- dddContent = dddFiles.map(f => {
2927
+ // ADR-PIPELINE-093 Phase 5a gate. Verifies SPARC + (ADRs OR DDD OR
2928
+ // engineering/architecture-decisions.md) are present. Missing upstream
2929
+ // triggers the gate's single-shot Ruflo invocation of the owning phase.
2930
+ const phase5aGate = await runPhaseGate('phase5a-prompts', scenarioQuery, traceId, runDir, gateAcc);
2931
+ if (!phase5aGate.ok && phase5aGate.stillMissing.length > 0) {
2932
+ phaseBlocked['phase5a-prompts'] = true;
2933
+ console.error(' [SKIP] Phase 5a (prompts) blocked by gate — upstream unrecoverable');
2934
+ // Fall through: the FATAL-path check later will decide whether
2935
+ // to exit loudly or continue degraded.
2936
+ }
2937
+ else {
2938
+ // ADR-PIPELINE-091: Ruflo runs FIRST as the primary implementation-prompt
2939
+ // producer. Its output lands in runDir/engineering/implementation-roadmap.md
2940
+ // (and impl-NNN-*.md). The template-derivation path below only fires as
2941
+ // the last-resort when Ruflo was unavailable or produced no output.
2942
+ rufloPrimaryResults.phase5a = await runRufloPrimaryForPhase('phase5a-prompts', scenarioQuery, traceId, runDir, collectPhase4Artifacts(runDir), readRemoteEnrichmentSnapshot(runDir), path.join(runDir, '.ruflo-cache', 'phase5a-primary'));
2943
+ // Gather all ADR and DDD files (prefer phase4, fall back to phase2)
2944
+ const adrDir = fs.existsSync(path.join(runDir, 'phase4', 'adrs'))
2945
+ ? path.join(runDir, 'phase4', 'adrs')
2946
+ : fs.existsSync(path.join(runDir, 'phase2', 'adrs'))
2947
+ ? path.join(runDir, 'phase2', 'adrs')
2948
+ : null;
2949
+ const dddDir = fs.existsSync(path.join(runDir, 'phase4', 'ddd'))
2950
+ ? path.join(runDir, 'phase4', 'ddd')
2951
+ : fs.existsSync(path.join(runDir, 'phase2', 'ddd'))
2952
+ ? path.join(runDir, 'phase2', 'ddd')
2953
+ : null;
2954
+ const sparcDir = fs.existsSync(path.join(runDir, 'phase3', 'sparc'))
2955
+ ? path.join(runDir, 'phase3', 'sparc')
2956
+ : fs.existsSync(path.join(runDir, 'phase2', 'sparc'))
2957
+ ? path.join(runDir, 'phase2', 'sparc')
2958
+ : null;
2959
+ if (adrDir) {
2960
+ try {
2961
+ // Read all ADR markdown files
2962
+ const adrFiles = fs.readdirSync(adrDir).filter(f => f.endsWith('.md'));
2963
+ const adrContent = adrFiles.map(f => {
2506
2964
  try {
2507
- return `### ${f}\n\n${fs.readFileSync(path.join(dddDir, f), 'utf-8')}`;
2965
+ return `### ${f}\n\n${fs.readFileSync(path.join(adrDir, f), 'utf-8')}`;
2508
2966
  }
2509
2967
  catch {
2510
2968
  return '';
2511
2969
  }
2512
2970
  }).filter(Boolean).join('\n\n---\n\n');
2513
- }
2514
- // Read SPARC specification summary
2515
- let sparcContent = '';
2516
- if (sparcDir) {
2517
- for (const candidate of ['specification.md', 'architecture.md', 'sparc-combined.json']) {
2518
- const p = path.join(sparcDir, candidate);
2519
- if (fs.existsSync(p)) {
2971
+ // Read DDD model files
2972
+ let dddContent = '';
2973
+ if (dddDir) {
2974
+ const dddFiles = fs.readdirSync(dddDir).filter(f => f.endsWith('.md') || f.endsWith('.json'));
2975
+ dddContent = dddFiles.map(f => {
2520
2976
  try {
2521
- sparcContent += `### ${candidate}\n\n${fs.readFileSync(p, 'utf-8').slice(0, 5000)}\n\n`;
2977
+ return `### ${f}\n\n${fs.readFileSync(path.join(dddDir, f), 'utf-8')}`;
2978
+ }
2979
+ catch {
2980
+ return '';
2981
+ }
2982
+ }).filter(Boolean).join('\n\n---\n\n');
2983
+ }
2984
+ // Read SPARC specification summary
2985
+ let sparcContent = '';
2986
+ if (sparcDir) {
2987
+ for (const candidate of ['specification.md', 'architecture.md', 'sparc-combined.json']) {
2988
+ const p = path.join(sparcDir, candidate);
2989
+ if (fs.existsSync(p)) {
2990
+ try {
2991
+ sparcContent += `### ${candidate}\n\n${fs.readFileSync(p, 'utf-8').slice(0, 5000)}\n\n`;
2992
+ }
2993
+ catch { /* skip */ }
2522
2994
  }
2523
- catch { /* skip */ }
2524
2995
  }
2525
2996
  }
2526
- }
2527
- // Build the ruflo swarm task: read the files, write implementation prompts
2528
- fs.mkdirSync(promptsDir, { recursive: true, mode: 0o700 });
2529
- const rufloPromptDir = path.join(runDir, '.ruflo-cache', 'prompts');
2530
- const promptTask = `You are an implementation prompt writer. You have been given the complete Architecture Decision Records (ADRs), Domain-Driven Design (DDD) model, and SPARC specification for this project.
2997
+ // Build the ruflo swarm task: read the files, write implementation prompts
2998
+ fs.mkdirSync(promptsDir, { recursive: true, mode: 0o700 });
2999
+ const rufloPromptDir = path.join(runDir, '.ruflo-cache', 'prompts');
3000
+ const promptTask = `You are an implementation prompt writer. You have been given the complete Architecture Decision Records (ADRs), Domain-Driven Design (DDD) model, and SPARC specification for this project.
2531
3001
 
2532
3002
  Your job: Read ALL of these documents carefully, then write a series of ordered implementation prompts. Each prompt should be a self-contained build step that a developer (or coding agent) can execute to build one piece of the platform.
2533
3003
 
@@ -2562,216 +3032,216 @@ ${sparcContent || 'No SPARC specification found.'}
2562
3032
  6. Write ALL files to the current working directory.
2563
3033
 
2564
3034
  The prompts must be production-grade — they will be given directly to a coding swarm to implement.`;
2565
- const promptSwarmResult = executeRufloPhaseSwarm({
2566
- phase: 4,
2567
- label: 'Implementation Prompts',
2568
- scenarioQuery,
2569
- runDir,
2570
- traceId,
2571
- outputDir: rufloPromptDir,
2572
- tasks: [{
2573
- label: 'Implementation Prompt Generation',
2574
- description: promptTask,
2575
- targetDir: '.',
2576
- }],
2577
- agenticsResults: [],
2578
- priorArtifacts: collectPhase4Artifacts(runDir),
2579
- timeoutMs: 600_000, // 10 min
2580
- });
2581
- // Copy ruflo output to prompts dir
2582
- if (fs.existsSync(rufloPromptDir)) {
2583
- const rufloFiles = fs.readdirSync(rufloPromptDir);
2584
- let promptsCopied = 0;
2585
- for (const f of rufloFiles) {
2586
- const src = path.join(rufloPromptDir, f);
2587
- if (fs.statSync(src).isFile()) {
2588
- fs.copyFileSync(src, path.join(promptsDir, f));
2589
- promptsCopied++;
2590
- }
2591
- }
2592
- if (promptsCopied > 0) {
2593
- console.error(` [PROMPTS] Ruflo swarm generated ${promptsCopied} implementation prompt files in ${promptSwarmResult.timing}ms`);
2594
- }
2595
- }
2596
- // Check if ruflo produced prompts; if not, write them directly from the gathered content
2597
- const generatedPrompts = fs.existsSync(promptsDir)
2598
- ? fs.readdirSync(promptsDir).filter(f => f.startsWith('impl-'))
2599
- : [];
2600
- if (generatedPrompts.length === 0) {
2601
- // ── Read all source material for prompt generation ──
2602
- const adrMarkdownByFile = new Map();
2603
- for (const f of adrFiles) {
2604
- try {
2605
- adrMarkdownByFile.set(f, fs.readFileSync(path.join(adrDir, f), 'utf-8'));
3035
+ const promptSwarmResult = executeRufloPhaseSwarm({
3036
+ phase: 4,
3037
+ label: 'Implementation Prompts',
3038
+ scenarioQuery,
3039
+ runDir,
3040
+ traceId,
3041
+ outputDir: rufloPromptDir,
3042
+ tasks: [{
3043
+ label: 'Implementation Prompt Generation',
3044
+ description: promptTask,
3045
+ targetDir: '.',
3046
+ }],
3047
+ agenticsResults: [],
3048
+ priorArtifacts: collectPhase4Artifacts(runDir),
3049
+ timeoutMs: 600_000, // 10 min
3050
+ });
3051
+ // Copy ruflo output to prompts dir
3052
+ if (fs.existsSync(rufloPromptDir)) {
3053
+ const rufloFiles = fs.readdirSync(rufloPromptDir);
3054
+ let promptsCopied = 0;
3055
+ for (const f of rufloFiles) {
3056
+ const src = path.join(rufloPromptDir, f);
3057
+ if (fs.statSync(src).isFile()) {
3058
+ fs.copyFileSync(src, path.join(promptsDir, f));
3059
+ promptsCopied++;
3060
+ }
2606
3061
  }
2607
- catch { /* skip */ }
2608
- }
2609
- const adrIndexPath = path.join(adrDir, 'adr-index.json');
2610
- let adrs = [];
2611
- if (fs.existsSync(adrIndexPath)) {
2612
- try {
2613
- adrs = JSON.parse(fs.readFileSync(adrIndexPath, 'utf-8'));
3062
+ if (promptsCopied > 0) {
3063
+ console.error(` [PROMPTS] Ruflo swarm generated ${promptsCopied} implementation prompt files in ${promptSwarmResult.timing}ms`);
2614
3064
  }
2615
- catch { /* empty */ }
2616
3065
  }
2617
- if (!Array.isArray(adrs) || adrs.length === 0) {
3066
+ // Check if ruflo produced prompts; if not, write them directly from the gathered content
3067
+ const generatedPrompts = fs.existsSync(promptsDir)
3068
+ ? fs.readdirSync(promptsDir).filter(f => f.startsWith('impl-'))
3069
+ : [];
3070
+ if (generatedPrompts.length === 0) {
3071
+ // ── Read all source material for prompt generation ──
3072
+ const adrMarkdownByFile = new Map();
2618
3073
  for (const f of adrFiles) {
2619
- const content = adrMarkdownByFile.get(f) ?? '';
2620
- const titleMatch = content.match(/^#\s+(.+)/m);
2621
- adrs.push({ id: f.replace(/\.md$/, ''), title: titleMatch?.[1] ?? f, context: content.slice(0, 500), decision: '', consequences: [], alternatives: [] });
3074
+ try {
3075
+ adrMarkdownByFile.set(f, fs.readFileSync(path.join(adrDir, f), 'utf-8'));
3076
+ }
3077
+ catch { /* skip */ }
2622
3078
  }
2623
- }
2624
- function getAdrMarkdown(adr) {
2625
- for (const [filename, content] of adrMarkdownByFile) {
2626
- if (filename.startsWith(adr.id))
2627
- return content;
3079
+ const adrIndexPath = path.join(adrDir, 'adr-index.json');
3080
+ let adrs = [];
3081
+ if (fs.existsSync(adrIndexPath)) {
3082
+ try {
3083
+ adrs = JSON.parse(fs.readFileSync(adrIndexPath, 'utf-8'));
3084
+ }
3085
+ catch { /* empty */ }
2628
3086
  }
2629
- return '';
2630
- }
2631
- // Read DDD as reference material (not as driver)
2632
- let dddContent = '';
2633
- const dddModelPath = dddDir ? path.join(dddDir, 'domain-model.json') : '';
2634
- if (dddModelPath && fs.existsSync(dddModelPath)) {
2635
- try {
2636
- dddContent = fs.readFileSync(dddModelPath, 'utf-8');
3087
+ if (!Array.isArray(adrs) || adrs.length === 0) {
3088
+ for (const f of adrFiles) {
3089
+ const content = adrMarkdownByFile.get(f) ?? '';
3090
+ const titleMatch = content.match(/^#\s+(.+)/m);
3091
+ adrs.push({ id: f.replace(/\.md$/, ''), title: titleMatch?.[1] ?? f, context: content.slice(0, 500), decision: '', consequences: [], alternatives: [] });
3092
+ }
2637
3093
  }
2638
- catch { /* skip */ }
2639
- }
2640
- // Also read DDD markdown if available
2641
- if (dddDir) {
2642
- for (const f of ['domain-model.md', 'ddd-model.md']) {
2643
- const p = path.join(dddDir, f);
2644
- if (fs.existsSync(p)) {
2645
- try {
2646
- dddContent += '\n\n' + fs.readFileSync(p, 'utf-8');
2647
- break;
3094
+ function getAdrMarkdown(adr) {
3095
+ for (const [filename, content] of adrMarkdownByFile) {
3096
+ if (filename.startsWith(adr.id))
3097
+ return content;
3098
+ }
3099
+ return '';
3100
+ }
3101
+ // Read DDD as reference material (not as driver)
3102
+ let dddContent = '';
3103
+ const dddModelPath = dddDir ? path.join(dddDir, 'domain-model.json') : '';
3104
+ if (dddModelPath && fs.existsSync(dddModelPath)) {
3105
+ try {
3106
+ dddContent = fs.readFileSync(dddModelPath, 'utf-8');
3107
+ }
3108
+ catch { /* skip */ }
3109
+ }
3110
+ // Also read DDD markdown if available
3111
+ if (dddDir) {
3112
+ for (const f of ['domain-model.md', 'ddd-model.md']) {
3113
+ const p = path.join(dddDir, f);
3114
+ if (fs.existsSync(p)) {
3115
+ try {
3116
+ dddContent += '\n\n' + fs.readFileSync(p, 'utf-8');
3117
+ break;
3118
+ }
3119
+ catch { /* skip */ }
2648
3120
  }
2649
- catch { /* skip */ }
2650
3121
  }
2651
3122
  }
2652
- }
2653
- // Read SPARC documents
2654
- let sparcSpec = '';
2655
- let sparcArch = '';
2656
- if (sparcDir) {
2657
- for (const [file, setter] of [
2658
- ['specification.md', 'spec'], ['architecture.md', 'arch'],
2659
- ]) {
2660
- const p = path.join(sparcDir, file);
2661
- if (fs.existsSync(p)) {
2662
- try {
2663
- const content = fs.readFileSync(p, 'utf-8');
2664
- if (setter === 'spec')
2665
- sparcSpec = content;
2666
- else if (setter === 'arch')
2667
- sparcArch = content;
3123
+ // Read SPARC documents
3124
+ let sparcSpec = '';
3125
+ let sparcArch = '';
3126
+ if (sparcDir) {
3127
+ for (const [file, setter] of [
3128
+ ['specification.md', 'spec'], ['architecture.md', 'arch'],
3129
+ ]) {
3130
+ const p = path.join(sparcDir, file);
3131
+ if (fs.existsSync(p)) {
3132
+ try {
3133
+ const content = fs.readFileSync(p, 'utf-8');
3134
+ if (setter === 'spec')
3135
+ sparcSpec = content;
3136
+ else if (setter === 'arch')
3137
+ sparcArch = content;
3138
+ }
3139
+ catch { /* skip */ }
2668
3140
  }
2669
- catch { /* skip */ }
2670
3141
  }
2671
3142
  }
2672
- }
2673
- console.error(' [PROMPTS] Ruflo swarm did not produce prompts — deriving from SPARC + ADRs');
2674
- const derivedPhases = [];
2675
- // Parse SPARC architecture for service/component sections
2676
- if (sparcArch) {
2677
- // Look for ## or ### headings that describe components/services
2678
- const sections = sparcArch.split(/^(?=#{2,3}\s)/m).filter(s => s.trim().length > 20);
2679
- for (const section of sections) {
2680
- const headingMatch = section.match(/^#{2,3}\s+(.+)/);
2681
- if (!headingMatch)
2682
- continue;
2683
- const heading = headingMatch[1].trim();
2684
- // Skip meta-sections
2685
- if (/^(overview|introduction|summary|table of contents|references|constraints|non-functional)/i.test(heading))
2686
- continue;
2687
- const slug = heading.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '').slice(0, 50);
2688
- // Determine target folder based on content
2689
- const sectionLower = section.toLowerCase();
2690
- let folder = 'backend';
2691
- if (/\bfrontend\b|\bui\b|\bdashboard\b|\breact\b|\bvue\b|\bangular\b/.test(sectionLower))
2692
- folder = 'frontend';
2693
- else if (/erp|netsuite|sap|dynamics|oracle/.test(sectionLower))
2694
- folder = 'erp';
2695
- else if (/integration|connector|webhook|external|third.party/.test(sectionLower))
2696
- folder = 'integrations';
2697
- else if (/api|service|backend|server|handler/.test(sectionLower))
2698
- folder = 'backend';
2699
- else if (/test|quality|coverage/.test(sectionLower))
2700
- folder = 'tests';
2701
- else if (/deploy|infra|docker|cloud|ci.cd/.test(sectionLower))
2702
- folder = 'src';
2703
- else if (/doc|guide|reference/.test(sectionLower))
2704
- folder = 'docs';
2705
- // Match ADRs to this section by keyword overlap
2706
- const sectionWords = new Set(sectionLower.split(/\W+/).filter(w => w.length > 3));
2707
- const matchedAdrIds = adrs.filter(adr => {
2708
- const adrWords = `${adr.title} ${adr.context} ${adr.decision}`.toLowerCase().split(/\W+/).filter(w => w.length > 3);
2709
- const overlap = adrWords.filter(w => sectionWords.has(w)).length;
2710
- return overlap >= 2;
2711
- }).map(a => a.id);
2712
- derivedPhases.push({ title: heading, slug, content: section, folder, adrIds: matchedAdrIds });
3143
+ console.error(' [PROMPTS] Ruflo swarm did not produce prompts — deriving from SPARC + ADRs');
3144
+ const derivedPhases = [];
3145
+ // Parse SPARC architecture for service/component sections
3146
+ if (sparcArch) {
3147
+ // Look for ## or ### headings that describe components/services
3148
+ const sections = sparcArch.split(/^(?=#{2,3}\s)/m).filter(s => s.trim().length > 20);
3149
+ for (const section of sections) {
3150
+ const headingMatch = section.match(/^#{2,3}\s+(.+)/);
3151
+ if (!headingMatch)
3152
+ continue;
3153
+ const heading = headingMatch[1].trim();
3154
+ // Skip meta-sections
3155
+ if (/^(overview|introduction|summary|table of contents|references|constraints|non-functional)/i.test(heading))
3156
+ continue;
3157
+ const slug = heading.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '').slice(0, 50);
3158
+ // Determine target folder based on content
3159
+ const sectionLower = section.toLowerCase();
3160
+ let folder = 'backend';
3161
+ if (/\bfrontend\b|\bui\b|\bdashboard\b|\breact\b|\bvue\b|\bangular\b/.test(sectionLower))
3162
+ folder = 'frontend';
3163
+ else if (/erp|netsuite|sap|dynamics|oracle/.test(sectionLower))
3164
+ folder = 'erp';
3165
+ else if (/integration|connector|webhook|external|third.party/.test(sectionLower))
3166
+ folder = 'integrations';
3167
+ else if (/api|service|backend|server|handler/.test(sectionLower))
3168
+ folder = 'backend';
3169
+ else if (/test|quality|coverage/.test(sectionLower))
3170
+ folder = 'tests';
3171
+ else if (/deploy|infra|docker|cloud|ci.cd/.test(sectionLower))
3172
+ folder = 'src';
3173
+ else if (/doc|guide|reference/.test(sectionLower))
3174
+ folder = 'docs';
3175
+ // Match ADRs to this section by keyword overlap
3176
+ const sectionWords = new Set(sectionLower.split(/\W+/).filter(w => w.length > 3));
3177
+ const matchedAdrIds = adrs.filter(adr => {
3178
+ const adrWords = `${adr.title} ${adr.context} ${adr.decision}`.toLowerCase().split(/\W+/).filter(w => w.length > 3);
3179
+ const overlap = adrWords.filter(w => sectionWords.has(w)).length;
3180
+ return overlap >= 2;
3181
+ }).map(a => a.id);
3182
+ derivedPhases.push({ title: heading, slug, content: section, folder, adrIds: matchedAdrIds });
3183
+ }
2713
3184
  }
2714
- }
2715
- // ADR-PIPELINE-031: Enrich with ADR-derived phases when SPARC sections alone are insufficient.
2716
- // Each ADR that isn't already covered by a SPARC-derived phase becomes its own build step.
2717
- if (derivedPhases.length < 8 && adrs.length > 0) {
2718
- const existingSlugs = new Set(derivedPhases.map(p => p.slug));
2719
- const existingTitleWords = new Set(derivedPhases.flatMap(p => p.title.toLowerCase().split(/\W+/).filter(w => w.length > 3)));
2720
- for (const adr of adrs) {
2721
- const slug = adr.title.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '').slice(0, 50);
2722
- // Skip if a phase with similar slug or overlapping title already exists
2723
- if (existingSlugs.has(slug))
2724
- continue;
2725
- const adrKeywords = adr.title.toLowerCase().split(/\W+/).filter(w => w.length > 3);
2726
- const overlap = adrKeywords.filter(w => existingTitleWords.has(w)).length;
2727
- if (overlap >= 2)
2728
- continue; // sufficiently covered by an existing phase
2729
- const adrLower = `${adr.title} ${adr.context} ${adr.decision}`.toLowerCase();
2730
- let folder = 'backend';
2731
- if (/frontend|ui|dashboard/.test(adrLower))
2732
- folder = 'frontend';
2733
- else if (/erp|netsuite|sap|dynamics|coupa|workday|maximo|oracle/.test(adrLower))
2734
- folder = 'erp';
2735
- else if (/integration|connector|webhook/.test(adrLower))
2736
- folder = 'integrations';
2737
- else if (/deploy|infra|docker/.test(adrLower))
2738
- folder = 'src';
2739
- else if (/test|quality/.test(adrLower))
2740
- folder = 'tests';
2741
- else if (/audit|governance|approval/.test(adrLower))
2742
- folder = 'backend';
2743
- derivedPhases.push({
2744
- title: adr.title,
2745
- slug,
2746
- content: getAdrMarkdown(adr) || `**Context:** ${adr.context}\n\n**Decision:** ${adr.decision}`,
2747
- folder,
2748
- adrIds: [adr.id],
2749
- });
2750
- existingSlugs.add(slug);
2751
- for (const w of adrKeywords)
2752
- existingTitleWords.add(w);
3185
+ // ADR-PIPELINE-031: Enrich with ADR-derived phases when SPARC sections alone are insufficient.
3186
+ // Each ADR that isn't already covered by a SPARC-derived phase becomes its own build step.
3187
+ if (derivedPhases.length < 8 && adrs.length > 0) {
3188
+ const existingSlugs = new Set(derivedPhases.map(p => p.slug));
3189
+ const existingTitleWords = new Set(derivedPhases.flatMap(p => p.title.toLowerCase().split(/\W+/).filter(w => w.length > 3)));
3190
+ for (const adr of adrs) {
3191
+ const slug = adr.title.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '').slice(0, 50);
3192
+ // Skip if a phase with similar slug or overlapping title already exists
3193
+ if (existingSlugs.has(slug))
3194
+ continue;
3195
+ const adrKeywords = adr.title.toLowerCase().split(/\W+/).filter(w => w.length > 3);
3196
+ const overlap = adrKeywords.filter(w => existingTitleWords.has(w)).length;
3197
+ if (overlap >= 2)
3198
+ continue; // sufficiently covered by an existing phase
3199
+ const adrLower = `${adr.title} ${adr.context} ${adr.decision}`.toLowerCase();
3200
+ let folder = 'backend';
3201
+ if (/frontend|ui|dashboard/.test(adrLower))
3202
+ folder = 'frontend';
3203
+ else if (/erp|netsuite|sap|dynamics|coupa|workday|maximo|oracle/.test(adrLower))
3204
+ folder = 'erp';
3205
+ else if (/integration|connector|webhook/.test(adrLower))
3206
+ folder = 'integrations';
3207
+ else if (/deploy|infra|docker/.test(adrLower))
3208
+ folder = 'src';
3209
+ else if (/test|quality/.test(adrLower))
3210
+ folder = 'tests';
3211
+ else if (/audit|governance|approval/.test(adrLower))
3212
+ folder = 'backend';
3213
+ derivedPhases.push({
3214
+ title: adr.title,
3215
+ slug,
3216
+ content: getAdrMarkdown(adr) || `**Context:** ${adr.context}\n\n**Decision:** ${adr.decision}`,
3217
+ folder,
3218
+ adrIds: [adr.id],
3219
+ });
3220
+ existingSlugs.add(slug);
3221
+ for (const w of adrKeywords)
3222
+ existingTitleWords.add(w);
3223
+ }
2753
3224
  }
2754
- }
2755
- // Always add foundation (first) and testing/deployment (last) if not present
2756
- const hasTesting = derivedPhases.some(p => /test/i.test(p.title));
2757
- const hasDeployment = derivedPhases.some(p => /deploy|infra/i.test(p.title));
2758
- // Detect multi-language requirements (e.g., TypeScript + Rust)
2759
- const queryLower = scenarioQuery.toLowerCase();
2760
- const hasRust = /\brust\b/.test(queryLower);
2761
- const hasTypeScript = /\btypescript\b/.test(queryLower);
2762
- const rustPurpose = scenarioQuery.match(/rust\s+(?:used\s+)?(?:for\s+)?(.{10,100}?)(?:\.|,|$)/i)?.[1]?.trim() ?? '';
2763
- const languageNote = hasRust && hasTypeScript
2764
- ? `\n\nThis is a HYBRID TypeScript + Rust project. TypeScript handles orchestration and service layers. Rust handles ${rustPurpose || 'compute-intensive components'}. Set up both package.json and Cargo.toml. Create a rust/ directory for Rust crates with WASM or FFI bindings to TypeScript.`
2765
- : hasRust
2766
- ? `\n\nThis project uses Rust. Set up Cargo.toml workspace.`
2767
- : '';
2768
- // Insert foundation at the beginning ADR-039: logger + config are FIRST outputs
2769
- // Detect language from query to make scaffold language-appropriate
2770
- const detectedLang = hasRust ? 'Rust' : hasTypeScript ? 'TypeScript' : /\bpython\b/i.test(queryLower) ? 'Python' : /\bgo\b|\bgolang\b/i.test(queryLower) ? 'Go' : /\bjava\b/i.test(queryLower) ? 'Java' : 'TypeScript';
2771
- derivedPhases.unshift({
2772
- title: 'Project Foundation & Core Types',
2773
- slug: 'foundation',
2774
- content: `Set up project structure, build tooling, and shared infrastructure using **${detectedLang}**. All subsequent phases import from this.${languageNote}
3225
+ // Always add foundation (first) and testing/deployment (last) if not present
3226
+ const hasTesting = derivedPhases.some(p => /test/i.test(p.title));
3227
+ const hasDeployment = derivedPhases.some(p => /deploy|infra/i.test(p.title));
3228
+ // Detect multi-language requirements (e.g., TypeScript + Rust)
3229
+ const queryLower = scenarioQuery.toLowerCase();
3230
+ const hasRust = /\brust\b/.test(queryLower);
3231
+ const hasTypeScript = /\btypescript\b/.test(queryLower);
3232
+ const rustPurpose = scenarioQuery.match(/rust\s+(?:used\s+)?(?:for\s+)?(.{10,100}?)(?:\.|,|$)/i)?.[1]?.trim() ?? '';
3233
+ const languageNote = hasRust && hasTypeScript
3234
+ ? `\n\nThis is a HYBRID TypeScript + Rust project. TypeScript handles orchestration and service layers. Rust handles ${rustPurpose || 'compute-intensive components'}. Set up both package.json and Cargo.toml. Create a rust/ directory for Rust crates with WASM or FFI bindings to TypeScript.`
3235
+ : hasRust
3236
+ ? `\n\nThis project uses Rust. Set up Cargo.toml workspace.`
3237
+ : '';
3238
+ // Insert foundation at the beginning — ADR-039: logger + config are FIRST outputs
3239
+ // Detect language from query to make scaffold language-appropriate
3240
+ const detectedLang = hasRust ? 'Rust' : hasTypeScript ? 'TypeScript' : /\bpython\b/i.test(queryLower) ? 'Python' : /\bgo\b|\bgolang\b/i.test(queryLower) ? 'Go' : /\bjava\b/i.test(queryLower) ? 'Java' : 'TypeScript';
3241
+ derivedPhases.unshift({
3242
+ title: 'Project Foundation & Core Types',
3243
+ slug: 'foundation',
3244
+ content: `Set up project structure, build tooling, and shared infrastructure using **${detectedLang}**. All subsequent phases import from this.${languageNote}
2775
3245
 
2776
3246
  ## FIRST: Create these shared modules (before domain types)
2777
3247
 
@@ -2840,32 +3310,32 @@ Create \`src/container.ts\` that wires all services via constructor injection:
2840
3310
 
2841
3311
  Base folder structure: src/, tests/, docs/. Create package.json, tsconfig.json, vitest.config.ts.
2842
3312
  Define all shared domain types in src/types.ts.`,
2843
- folder: 'src',
2844
- adrIds: adrs.map(a => a.id),
2845
- });
2846
- if (!hasTesting) {
2847
- derivedPhases.push({
2848
- title: 'Testing & Quality Assurance',
2849
- slug: 'testing',
2850
- content: 'Write comprehensive tests for all components built in prior phases.',
2851
- folder: 'tests',
2852
- adrIds: [],
2853
- });
2854
- }
2855
- if (!hasDeployment) {
2856
- derivedPhases.push({
2857
- title: 'Deployment & Infrastructure',
2858
- slug: 'deployment',
2859
- content: 'Create deployment configs, Dockerfiles, CI/CD, and infrastructure-as-code for the target cloud platform.',
2860
3313
  folder: 'src',
2861
- adrIds: [],
3314
+ adrIds: adrs.map(a => a.id),
2862
3315
  });
2863
- }
2864
- // ADR-PIPELINE-031: Guarantee ≥10 prompts by adding standard cross-cutting phases
2865
- // when domain-specific + ADR-derived phases don't reach the minimum.
2866
- const crossCutting = [
2867
- { title: 'Observability & Structured Logging', slug: 'observability', folder: 'backend',
2868
- content: `ADR-PIPELINE-034: Implement complete observability infrastructure.
3316
+ if (!hasTesting) {
3317
+ derivedPhases.push({
3318
+ title: 'Testing & Quality Assurance',
3319
+ slug: 'testing',
3320
+ content: 'Write comprehensive tests for all components built in prior phases.',
3321
+ folder: 'tests',
3322
+ adrIds: [],
3323
+ });
3324
+ }
3325
+ if (!hasDeployment) {
3326
+ derivedPhases.push({
3327
+ title: 'Deployment & Infrastructure',
3328
+ slug: 'deployment',
3329
+ content: 'Create deployment configs, Dockerfiles, CI/CD, and infrastructure-as-code for the target cloud platform.',
3330
+ folder: 'src',
3331
+ adrIds: [],
3332
+ });
3333
+ }
3334
+ // ADR-PIPELINE-031: Guarantee ≥10 prompts by adding standard cross-cutting phases
3335
+ // when domain-specific + ADR-derived phases don't reach the minimum.
3336
+ const crossCutting = [
3337
+ { title: 'Observability & Structured Logging', slug: 'observability', folder: 'backend',
3338
+ content: `ADR-PIPELINE-034: Implement complete observability infrastructure.
2869
3339
 
2870
3340
  ## 1. Structured Logger (src/logger.ts)
2871
3341
 
@@ -2927,8 +3397,8 @@ If the project includes an API layer, add GET /health returning:
2927
3397
  - Test that logger outputs valid JSON to stderr
2928
3398
  - Test that correlationId flows through analysis → decision → ERP sync
2929
3399
  - Test that ERP error handling retries on 429 and logs failures` },
2930
- { title: 'Configuration & Environment Management', slug: 'configuration', folder: 'src',
2931
- content: `ADR-PIPELINE-034: Configuration module for environment-based settings.
3400
+ { title: 'Configuration & Environment Management', slug: 'configuration', folder: 'src',
3401
+ content: `ADR-PIPELINE-034: Configuration module for environment-based settings.
2932
3402
 
2933
3403
  Create src/config.ts with a typed configuration object:
2934
3404
 
@@ -2956,8 +3426,8 @@ Requirements:
2956
3426
  - Provide sensible defaults for optional values (timeoutMs, maxRetries, logLevel)
2957
3427
  - Export a singleton config object used by all services
2958
3428
  - Include tests that verify validation catches missing required vars` },
2959
- { title: 'API Layer & Request Handling', slug: 'api-layer', folder: 'backend',
2960
- content: `ADR-PIPELINE-043: Thin route handlers with Zod validation.
3429
+ { title: 'API Layer & Request Handling', slug: 'api-layer', folder: 'backend',
3430
+ content: `ADR-PIPELINE-043: Thin route handlers with Zod validation.
2961
3431
 
2962
3432
  ARCHITECTURE: Route handlers MUST be thin:
2963
3433
  - Route: parse request → validate with Zod schema → call service function → format response
@@ -2981,10 +3451,10 @@ REQUIRED ENDPOINTS:
2981
3451
  - GET /api/metrics → Prometheus-format metrics (request count, latency histogram, error rate)
2982
3452
  - All domain endpoints with proper HTTP status codes and structured error responses
2983
3453
  - Every response includes X-Correlation-Id header` },
2984
- { title: 'Persistence & Data Access Layer', slug: 'persistence', folder: 'backend',
2985
- content: 'Implement repository interfaces per aggregate (ports pattern). Database adapter implementations. Audit trail persistence (append-only, tamper-evident). Transaction management for multi-aggregate operations.' },
2986
- { title: 'Demo Script & CLI Entry Point', slug: 'demo', folder: 'src',
2987
- content: `ADR-PIPELINE-037: User-facing entry points for the prototype.
3454
+ { title: 'Persistence & Data Access Layer', slug: 'persistence', folder: 'backend',
3455
+ content: 'Implement repository interfaces per aggregate (ports pattern). Database adapter implementations. Audit trail persistence (append-only, tamper-evident). Transaction management for multi-aggregate operations.' },
3456
+ { title: 'Demo Script & CLI Entry Point', slug: 'demo', folder: 'src',
3457
+ content: `ADR-PIPELINE-037: User-facing entry points for the prototype.
2988
3458
 
2989
3459
  ## 1. Demo Script (src/demo.ts) — REQUIRED
2990
3460
 
@@ -3115,227 +3585,242 @@ services when the pilot validates the approach.
3115
3585
  ## Tests
3116
3586
  - Test that demo.ts runs without errors (import and execute main function)
3117
3587
  - Test that it produces non-empty stdout output` },
3118
- ];
3119
- const existingSlugSet = new Set(derivedPhases.map(p => p.slug));
3120
- for (const cc of crossCutting) {
3121
- if (derivedPhases.length >= 12)
3122
- break;
3123
- if (existingSlugSet.has(cc.slug))
3124
- continue;
3125
- // Don't add if a phase already covers this topic
3126
- if (derivedPhases.some(p => p.title.toLowerCase().includes(cc.slug.split('-')[0])))
3127
- continue;
3128
- derivedPhases.push({ ...cc, adrIds: [] });
3129
- existingSlugSet.add(cc.slug);
3130
- }
3131
- console.error(` [PROMPTS] ${derivedPhases.length} phases derived (SPARC + ADRs + cross-cutting, minimum 10 per ADR-PIPELINE-031)`);
3132
- // ── Helper: extract DDD context names and summaries relevant to a phase ──
3133
- function getDddSummaryForPhase(phaseTitle, phaseContent) {
3134
- if (!dddContent)
3135
- return '';
3136
- try {
3137
- const model = JSON.parse(dddContent);
3138
- const contexts = model.contexts ?? [];
3139
- if (contexts.length === 0)
3588
+ ];
3589
+ const existingSlugSet = new Set(derivedPhases.map(p => p.slug));
3590
+ for (const cc of crossCutting) {
3591
+ if (derivedPhases.length >= 12)
3592
+ break;
3593
+ if (existingSlugSet.has(cc.slug))
3594
+ continue;
3595
+ // Don't add if a phase already covers this topic
3596
+ if (derivedPhases.some(p => p.title.toLowerCase().includes(cc.slug.split('-')[0])))
3597
+ continue;
3598
+ derivedPhases.push({ ...cc, adrIds: [] });
3599
+ existingSlugSet.add(cc.slug);
3600
+ }
3601
+ console.error(` [PROMPTS] ${derivedPhases.length} phases derived (SPARC + ADRs + cross-cutting, minimum 10 per ADR-PIPELINE-031)`);
3602
+ // ── Helper: extract DDD context names and summaries relevant to a phase ──
3603
+ function getDddSummaryForPhase(phaseTitle, phaseContent) {
3604
+ if (!dddContent)
3140
3605
  return '';
3141
- // Exclude common words that would match everything
3142
- const excludeWords = new Set(['domain', 'logic', 'crud', 'operations', 'manage', 'business', 'rules', 'event', 'handling', 'service', 'system', 'data', 'component', 'module', 'implementation', 'integration']);
3143
- const phaseWords = new Set(`${phaseTitle} ${phaseContent}`.toLowerCase().split(/\W+/).filter((w) => w.length > 3 && !excludeWords.has(w)));
3144
- const relevant = contexts.filter((c) => {
3145
- const ctxText = `${c['name']}`.toLowerCase().split(/[-_\s]+/).filter((w) => w.length > 3 && !excludeWords.has(w));
3146
- // Must match on context NAME keywords, not generic description words
3147
- const matches = ctxText.filter((w) => phaseWords.has(w)).length;
3148
- return matches >= 1;
3149
- });
3150
- if (relevant.length === 0)
3606
+ try {
3607
+ const model = JSON.parse(dddContent);
3608
+ const contexts = model.contexts ?? [];
3609
+ if (contexts.length === 0)
3610
+ return '';
3611
+ // Exclude common words that would match everything
3612
+ const excludeWords = new Set(['domain', 'logic', 'crud', 'operations', 'manage', 'business', 'rules', 'event', 'handling', 'service', 'system', 'data', 'component', 'module', 'implementation', 'integration']);
3613
+ const phaseWords = new Set(`${phaseTitle} ${phaseContent}`.toLowerCase().split(/\W+/).filter((w) => w.length > 3 && !excludeWords.has(w)));
3614
+ const relevant = contexts.filter((c) => {
3615
+ const ctxText = `${c['name']}`.toLowerCase().split(/[-_\s]+/).filter((w) => w.length > 3 && !excludeWords.has(w));
3616
+ // Must match on context NAME keywords, not generic description words
3617
+ const matches = ctxText.filter((w) => phaseWords.has(w)).length;
3618
+ return matches >= 1;
3619
+ });
3620
+ if (relevant.length === 0)
3621
+ return '';
3622
+ const parts = [];
3623
+ for (const c of relevant) {
3624
+ const name = String(c['name'] ?? '');
3625
+ const desc = String(c['description'] ?? '');
3626
+ const aggs = (c['aggregates'] ?? []).map((a) => String(a['name'] ?? '')).filter(Boolean);
3627
+ const cmds = (c['commands'] ?? []).map((cmd) => typeof cmd === 'string' ? cmd : String(cmd['name'] ?? '')).filter(Boolean);
3628
+ const queries = (c['queries'] ?? []).map((q) => typeof q === 'string' ? q : String(q['name'] ?? '')).filter(Boolean);
3629
+ let summary = `**${name}**: ${desc}`;
3630
+ if (aggs.length > 0)
3631
+ summary += ` (aggregates: ${aggs.join(', ')})`;
3632
+ if (cmds.length > 0)
3633
+ summary += ` (commands: ${cmds.join(', ')})`;
3634
+ if (queries.length > 0)
3635
+ summary += ` (queries: ${queries.join(', ')})`;
3636
+ parts.push(summary);
3637
+ }
3638
+ return parts.join('\n\n');
3639
+ }
3640
+ catch {
3151
3641
  return '';
3152
- const parts = [];
3153
- for (const c of relevant) {
3154
- const name = String(c['name'] ?? '');
3155
- const desc = String(c['description'] ?? '');
3156
- const aggs = (c['aggregates'] ?? []).map((a) => String(a['name'] ?? '')).filter(Boolean);
3157
- const cmds = (c['commands'] ?? []).map((cmd) => typeof cmd === 'string' ? cmd : String(cmd['name'] ?? '')).filter(Boolean);
3158
- const queries = (c['queries'] ?? []).map((q) => typeof q === 'string' ? q : String(q['name'] ?? '')).filter(Boolean);
3159
- let summary = `**${name}**: ${desc}`;
3160
- if (aggs.length > 0)
3161
- summary += ` (aggregates: ${aggs.join(', ')})`;
3162
- if (cmds.length > 0)
3163
- summary += ` (commands: ${cmds.join(', ')})`;
3164
- if (queries.length > 0)
3165
- summary += ` (queries: ${queries.join(', ')})`;
3166
- parts.push(summary);
3167
3642
  }
3168
- return parts.join('\n\n');
3169
- }
3170
- catch {
3171
- return '';
3172
3643
  }
3173
- }
3174
- // ── Helper: build a narrative description paragraph for a phase ──
3175
- function buildPhaseNarrative(phase, phaseAdrs, dddSummary, completedList) {
3176
- const parts = [];
3177
- // Opening: what this phase builds
3178
- parts.push(`In this phase, you are building the **${phase.title}** component of the platform.`);
3179
- // What prior phases provide
3180
- if (completedList.length > 0) {
3181
- parts.push(`This builds on ${completedList.length} previously completed phase(s) — import shared types, interfaces, and services from those modules.`);
3182
- }
3183
- // ADR narrative: which decisions govern this phase and what they decided
3184
- if (phaseAdrs.length > 0) {
3185
- const adrSentences = phaseAdrs.map(a => {
3186
- const decision = a.decision?.split('.')[0] ?? a.title;
3187
- return `${a.id} ("${a.title}") which decided: ${decision}`;
3188
- });
3189
- if (adrSentences.length === 1) {
3190
- parts.push(`This phase is governed by architecture decision ${adrSentences[0]}.`);
3191
- }
3192
- else {
3193
- parts.push(`This phase is governed by ${adrSentences.length} architecture decisions: ${adrSentences.join('; ')}.`);
3644
+ // ── Helper: build a narrative description paragraph for a phase ──
3645
+ function buildPhaseNarrative(phase, phaseAdrs, dddSummary, completedList) {
3646
+ const parts = [];
3647
+ // Opening: what this phase builds
3648
+ parts.push(`In this phase, you are building the **${phase.title}** component of the platform.`);
3649
+ // What prior phases provide
3650
+ if (completedList.length > 0) {
3651
+ parts.push(`This builds on ${completedList.length} previously completed phase(s) — import shared types, interfaces, and services from those modules.`);
3194
3652
  }
3195
- }
3196
- // DDD narrative: which domain concepts are relevant
3197
- if (dddSummary) {
3198
- parts.push(`The domain model defines the following relevant contexts and entities for this phase:\n\n${dddSummary}`);
3199
- }
3200
- return parts.join(' ');
3201
- }
3202
- const totalSteps = derivedPhases.length;
3203
- const completedPhases = [];
3204
- for (let i = 0; i < totalSteps; i++) {
3205
- const phase = derivedPhases[i];
3206
- const order = i + 1;
3207
- const filename = `impl-${String(order).padStart(3, '0')}-${phase.slug}.md`;
3208
- // Resolve ADRs for this phase
3209
- const phaseAdrs = phase.adrIds.length > 0
3210
- ? adrs.filter(a => phase.adrIds.includes(a.id))
3211
- : (order === 1 ? adrs : []);
3212
- // Resolve DDD summary for this phase
3213
- const dddSummary = getDddSummaryForPhase(phase.title, phase.content);
3214
- // Build the narrative paragraph
3215
- const narrative = buildPhaseNarrative(phase, phaseAdrs, dddSummary, completedPhases);
3216
- const lines = [
3217
- `# Implementation Prompt ${order} of ${totalSteps}: ${phase.title}`,
3218
- '',
3219
- `**Target folder:** \`${phase.folder}/\``,
3220
- '',
3221
- ];
3222
- // ADR-PIPELINE-033: Simulation lineage in every prompt
3223
- if (simulationId || traceId) {
3224
- lines.push('## Simulation Lineage', '');
3225
- if (simulationId)
3226
- lines.push(`Originating simulation: \`${simulationId}\``);
3227
- lines.push(`Trace ID: \`${traceId}\``);
3228
- lines.push('');
3229
- }
3230
- // ── Narrative description — the core of each prompt ──
3231
- lines.push('## Overview', '', narrative, '');
3232
- // Previously completed phases (brief list)
3233
- if (completedPhases.length > 0) {
3234
- lines.push('## Previously Completed (available for import)', '');
3235
- for (const prev of completedPhases)
3236
- lines.push(`- ${prev}`);
3237
- lines.push('');
3238
- }
3239
- // Full project requirements on first prompt only
3240
- if (order === 1) {
3241
- lines.push('## Project Requirements', '', scenarioQuery, '');
3242
- }
3243
- // SPARC-derived content for this phase (the actual architecture section)
3244
- if (phase.content && phase.content.length > 50) {
3245
- lines.push('## SPARC Architecture for This Phase', '', phase.content, '');
3246
- }
3247
- // SPARC spec + architecture on first two prompts for full context
3248
- if (order === 1 && sparcSpec) {
3249
- lines.push('## Full SPARC Specification', '', sparcSpec, '');
3250
- }
3251
- if (order <= 2 && sparcArch && phase.content !== sparcArch) {
3252
- lines.push('## Full SPARC Architecture', '', sparcArch, '');
3253
- }
3254
- // ADR details — full markdown with file paths for each relevant ADR
3255
- if (phaseAdrs.length > 0) {
3256
- lines.push(`## Architecture Decisions (${phaseAdrs.length} ADRs)`, '');
3257
- lines.push('ADR files are in `.agentics/plans/adrs/` — read them for full context:', '');
3258
- for (const adr of phaseAdrs) {
3259
- const slug = (adr.title || 'untitled').toLowerCase().replace(/[^a-z0-9]+/g, '-').slice(0, 60);
3260
- const adrFilename = `${adr.id}-${slug}.md`;
3261
- lines.push(`**File:** \`.agentics/plans/adrs/${adrFilename}\``, '');
3262
- const fullMarkdown = getAdrMarkdown(adr);
3263
- if (fullMarkdown) {
3264
- lines.push(fullMarkdown, '', '---', '');
3653
+ // ADR narrative: which decisions govern this phase and what they decided
3654
+ if (phaseAdrs.length > 0) {
3655
+ const adrSentences = phaseAdrs.map(a => {
3656
+ const decision = a.decision?.split('.')[0] ?? a.title;
3657
+ return `${a.id} ("${a.title}") which decided: ${decision}`;
3658
+ });
3659
+ if (adrSentences.length === 1) {
3660
+ parts.push(`This phase is governed by architecture decision ${adrSentences[0]}.`);
3265
3661
  }
3266
3662
  else {
3267
- lines.push(`### ${adr.id}: ${adr.title}`, '');
3268
- if (adr.context)
3269
- lines.push(`**Context:** ${adr.context}`, '');
3270
- if (adr.decision)
3271
- lines.push(`**Decision:** ${adr.decision}`, '');
3272
- if (adr.consequences?.length) {
3273
- lines.push('**Consequences:**');
3274
- for (const c of adr.consequences)
3275
- lines.push(`- ${c}`);
3276
- }
3277
- lines.push('');
3663
+ parts.push(`This phase is governed by ${adrSentences.length} architecture decisions: ${adrSentences.join('; ')}.`);
3278
3664
  }
3279
3665
  }
3666
+ // DDD narrative: which domain concepts are relevant
3667
+ if (dddSummary) {
3668
+ parts.push(`The domain model defines the following relevant contexts and entities for this phase:\n\n${dddSummary}`);
3669
+ }
3670
+ return parts.join(' ');
3280
3671
  }
3281
- // List ALL ADR files for reference even if not directly linked to this phase
3282
- if (adrDir && order === 1) {
3283
- try {
3284
- const allAdrFiles = fs.readdirSync(adrDir).filter(f => f.endsWith('.md')).sort();
3285
- if (allAdrFiles.length > 0) {
3286
- lines.push('## All Architecture Decision Records', '');
3287
- lines.push('Read these files in `.agentics/plans/adrs/` for complete design rationale:', '');
3288
- for (const f of allAdrFiles) {
3289
- lines.push(`- \`.agentics/plans/adrs/${f}\``);
3672
+ const totalSteps = derivedPhases.length;
3673
+ const completedPhases = [];
3674
+ for (let i = 0; i < totalSteps; i++) {
3675
+ const phase = derivedPhases[i];
3676
+ const order = i + 1;
3677
+ const filename = `impl-${String(order).padStart(3, '0')}-${phase.slug}.md`;
3678
+ // Resolve ADRs for this phase
3679
+ const phaseAdrs = phase.adrIds.length > 0
3680
+ ? adrs.filter(a => phase.adrIds.includes(a.id))
3681
+ : (order === 1 ? adrs : []);
3682
+ // Resolve DDD summary for this phase
3683
+ const dddSummary = getDddSummaryForPhase(phase.title, phase.content);
3684
+ // Build the narrative paragraph
3685
+ const narrative = buildPhaseNarrative(phase, phaseAdrs, dddSummary, completedPhases);
3686
+ const lines = [
3687
+ `# Implementation Prompt ${order} of ${totalSteps}: ${phase.title}`,
3688
+ '',
3689
+ `**Target folder:** \`${phase.folder}/\``,
3690
+ '',
3691
+ ];
3692
+ // ADR-PIPELINE-033: Simulation lineage in every prompt
3693
+ if (simulationId || traceId) {
3694
+ lines.push('## Simulation Lineage', '');
3695
+ if (simulationId)
3696
+ lines.push(`Originating simulation: \`${simulationId}\``);
3697
+ lines.push(`Trace ID: \`${traceId}\``);
3698
+ lines.push('');
3699
+ }
3700
+ // ── Narrative description — the core of each prompt ──
3701
+ lines.push('## Overview', '', narrative, '');
3702
+ // Previously completed phases (brief list)
3703
+ if (completedPhases.length > 0) {
3704
+ lines.push('## Previously Completed (available for import)', '');
3705
+ for (const prev of completedPhases)
3706
+ lines.push(`- ${prev}`);
3707
+ lines.push('');
3708
+ }
3709
+ // Full project requirements on first prompt only
3710
+ if (order === 1) {
3711
+ lines.push('## Project Requirements', '', scenarioQuery, '');
3712
+ }
3713
+ // SPARC-derived content for this phase (the actual architecture section)
3714
+ if (phase.content && phase.content.length > 50) {
3715
+ lines.push('## SPARC Architecture for This Phase', '', phase.content, '');
3716
+ }
3717
+ // SPARC spec + architecture on first two prompts for full context
3718
+ if (order === 1 && sparcSpec) {
3719
+ lines.push('## Full SPARC Specification', '', sparcSpec, '');
3720
+ }
3721
+ if (order <= 2 && sparcArch && phase.content !== sparcArch) {
3722
+ lines.push('## Full SPARC Architecture', '', sparcArch, '');
3723
+ }
3724
+ // ADR details — full markdown with file paths for each relevant ADR
3725
+ if (phaseAdrs.length > 0) {
3726
+ lines.push(`## Architecture Decisions (${phaseAdrs.length} ADRs)`, '');
3727
+ lines.push('ADR files are in `.agentics/plans/adrs/` — read them for full context:', '');
3728
+ for (const adr of phaseAdrs) {
3729
+ const slug = (adr.title || 'untitled').toLowerCase().replace(/[^a-z0-9]+/g, '-').slice(0, 60);
3730
+ const adrFilename = `${adr.id}-${slug}.md`;
3731
+ lines.push(`**File:** \`.agentics/plans/adrs/${adrFilename}\``, '');
3732
+ const fullMarkdown = getAdrMarkdown(adr);
3733
+ if (fullMarkdown) {
3734
+ lines.push(fullMarkdown, '', '---', '');
3735
+ }
3736
+ else {
3737
+ lines.push(`### ${adr.id}: ${adr.title}`, '');
3738
+ if (adr.context)
3739
+ lines.push(`**Context:** ${adr.context}`, '');
3740
+ if (adr.decision)
3741
+ lines.push(`**Decision:** ${adr.decision}`, '');
3742
+ if (adr.consequences?.length) {
3743
+ lines.push('**Consequences:**');
3744
+ for (const c of adr.consequences)
3745
+ lines.push(`- ${c}`);
3746
+ }
3747
+ lines.push('');
3290
3748
  }
3291
- lines.push('');
3292
3749
  }
3293
3750
  }
3294
- catch { /* non-fatal */ }
3295
- }
3296
- // DDD reference — structured summary, not raw JSON dump
3297
- if (dddSummary) {
3298
- lines.push('## Domain Model Reference', '', dddSummary, '');
3751
+ // List ALL ADR files for reference even if not directly linked to this phase
3752
+ if (adrDir && order === 1) {
3753
+ try {
3754
+ const allAdrFiles = fs.readdirSync(adrDir).filter(f => f.endsWith('.md')).sort();
3755
+ if (allAdrFiles.length > 0) {
3756
+ lines.push('## All Architecture Decision Records', '');
3757
+ lines.push('Read these files in `.agentics/plans/adrs/` for complete design rationale:', '');
3758
+ for (const f of allAdrFiles) {
3759
+ lines.push(`- \`.agentics/plans/adrs/${f}\``);
3760
+ }
3761
+ lines.push('');
3762
+ }
3763
+ }
3764
+ catch { /* non-fatal */ }
3765
+ }
3766
+ // DDD reference — structured summary, not raw JSON dump
3767
+ if (dddSummary) {
3768
+ lines.push('## Domain Model Reference', '', dddSummary, '');
3769
+ }
3770
+ // Implementation instructions — ADR-PIPELINE-033: include traceability requirements
3771
+ lines.push('## Implementation Requirements', '', '- All code must be CUSTOM to this project — implement the specific business logic described above', '- Follow the technology decisions from the ADRs referenced in the overview', '- Write production-quality code with proper error handling', '- Include unit tests (London School TDD — mock at module boundaries)', '- Export public interfaces so later build phases can import them', `- Place all files in the \`${phase.folder}/\` directory`, '', '## Traceability & Simulation Alignment (ADR-033 + ADR-049)', '', '### Domain Type Fields (REQUIRED on Decision, Recommendation, ERPTransaction):', '```', 'interface Recommendation {', ' // ... domain fields ...', ' simulationId?: string; // Pipeline simulation that produced the analysis', ' traceId?: string; // Correlation ID for end-to-end tracing', ' pipelineVersion?: string; // Version of the pipeline that generated this', '}', '```', '', `${simulationId ? `### Simulation ID: \`${simulationId}\`\nUse this as the default simulationId when creating records.` : '### Simulation ID: load from .agentics/plans/manifest.json at startup.'}`, '', '### Simulation Config Integration:', '- Load .agentics/plans/manifest.json at startup (if it exists)', '- Read simulationId and traceId from it', '- Pass to all service constructors so every record carries lineage', '- Log simulationId in every analysis run: `logger.info("analysis.start", { simulationId })`', '', '### Confidence Disclosure in Demo Output:', '- The demo script MUST print an "Analysis Confidence" section', '- State the confidence level and what it means', '- If confidence < 60%: "The pilot resolves remaining uncertainty with real data"', '- If confidence >= 80%: "High confidence — proceed to pilot with standard monitoring"', '');
3772
+ // ADR-PIPELINE-039: Mandatory cross-cutting requirements in EVERY prompt
3773
+ lines.push('## Mandatory Cross-Cutting Requirements (EVERY module must implement these)', '', '### Logging', '- Create `src/logger.ts` (if it does not exist) with `createLogger(service: string)` that outputs JSON to stderr', '- Every log entry: `{ timestamp, level, service, event, correlationId?, ...data }`', '- Import and use the logger in THIS module. Log: function entry with key params, completion with duration, errors with context', '', '### Configuration', '- Create `src/config.ts` (if it does not exist) with typed `AppConfig` read from environment variables', '- NEVER hardcode: ports, URLs, thresholds, weights, API keys, timeouts', '- Import config in THIS module for any configurable value', '', '### Input Validation (ADR-046: Zod at EVERY boundary)', '- EVERY function receiving external data MUST Zod-parse it:', ' API request bodies: `const parsed = Schema.safeParse(req.body); if (!parsed.success) return res.status(400).json({ error: parsed.error.format() });`', ' Database rows: `const row = RowSchema.parse(rawDbRow);` — NEVER `as SomeType`', ' ERP/API responses: `const data = ResponseSchema.parse(response.data);`', ' Config values: validate ALL config with Zod at startup; refuse to start if invalid', '- NEVER use `as SomeType` to cast external data — always Zod-parse first', '', '### Error Handling', '- Define error classes extending `AppError` (create in `src/errors.ts` if needed)', '- Never `catch (e: any)` — always narrow error types', '- Log errors with logger.error() including stack trace and context', '- Event bus/emitter: wrap EACH handler in try/catch — one failure must NOT block others', '', '### Config-Driven (ADR-046: zero hardcoded magic numbers)', '- EVERY threshold, weight, factor, timeout MUST come from config (not hardcoded constants)', '- Config module validates ALL values with Zod at startup', '- If config invalid, service refuses to start with clear error', '', '### Deterministic Code (ADR-046)', '- No Math.random() in production code — use crypto.randomUUID() for IDs', '- Seed data generators may use seeded random; service code must be deterministic', '', '### Architecture (ADR-047: constructor injection, thin handlers)', '- Every service class receives dependencies via constructor — no module-level singletons', '- Route handlers: (1) Zod-validate request (2) call service (3) format response — NO business logic', '- Multi-step workflows → application service layer (src/services/workflow.ts or similar)', '- Periodic sync/ingestion: setInterval or cron, interval from config, log every run', '- Composition root (src/container.ts) wires everything — the ONLY place services are instantiated', '', '### ERP Integration (ADR-050: resilience, idempotency, schema as code)', '- Retry with exponential backoff: 1s → 2s → 4s, max 3 attempts, on 429/503/timeout', '- Idempotency key per write: hash(decisionId + timestamp) — prevents duplicate records on retry', '- Check-before-write: query for existing record before creating', '- Circuit breaker: open after 5 consecutive failures, half-open after 30s, log every state change', '- All operations logged with correlationId and responseTimeMs', '- ERP schema as code: define a TypeScript interface mapping domain fields → ERP fields, include as src/erp/schema.ts', '', '### Governance (ADR-048: RBAC, immutable audit, separation of duties)', '- Approval endpoints enforce roles via middleware: read X-User-Role header, reject if insufficient', '- Separation of duties: approver MUST NOT be the recommendation creator — check and reject with 403', '- Audit trail: SHA-256 hash chain — each entry hashes previous entry\'s hash, genesis entry uses \'genesis\'', '- Verification endpoint: GET /api/audit/verify — walks the chain, reports any broken links', '- Every state transition records: who (userId + role), what (from → to), when (server timestamp), why (rationale min 10 chars), correlationId', '', '## Anti-Patterns to AVOID (evaluation checks — violations REDUCE score)', '', '- Do NOT use console.log — use createLogger()', '- Do NOT use `as SomeType` to cast DB rows, API responses, or request bodies — Zod-parse instead', '- Do NOT put business logic in route handlers — extract to service layer', '- Do NOT hardcode thresholds, weights, or magic numbers — read from config', '- Do NOT use Math.random() in production code — use crypto.randomUUID()', '- Do NOT list unused deps in package.json — if listed, USE them', '- Do NOT let event handlers fail silently — wrap each in try/catch with logging', '- Do NOT initialize required fields to empty string \'\'', '', '## Observability Enforcement (ADR-051)', '', '### Logger — EVERY module:', '- Import createLogger from ../logger.js. Include correlationId in every log entry.', '- Entry: `logger.info(\'fn.start\', { correlationId, params })`', '- Exit: `logger.info(\'fn.complete\', { correlationId, durationMs })`', '- Error: `logger.error(\'fn.failed\', err, { correlationId, context })` — NEVER `{ error: err }`', '', '### Correlation IDs — NON-NEGOTIABLE:', '- Copy src/middleware.ts from scaffold (correlationId + requestLogger + metricsHandler + CircuitBreaker)', '- Wire: `app.use(correlationId); app.use(requestLogger); app.get(\'/metrics\', metricsHandler);`', '- Pass correlationId through all service calls, audit entries, and ERP headers', '', '### Metrics: `GET /metrics` via metricsHandler from middleware.ts', '### Circuit breaker: `new CircuitBreaker(\'erp\')` wraps all external calls', '');
3774
+ fs.writeFileSync(path.join(promptsDir, filename), lines.join('\n'), { mode: 0o600, encoding: 'utf-8' });
3775
+ completedPhases.push(`${order}. ${phase.title}`);
3299
3776
  }
3300
- // Implementation instructions ADR-PIPELINE-033: include traceability requirements
3301
- lines.push('## Implementation Requirements', '', '- All code must be CUSTOM to this project — implement the specific business logic described above', '- Follow the technology decisions from the ADRs referenced in the overview', '- Write production-quality code with proper error handling', '- Include unit tests (London School TDD — mock at module boundaries)', '- Export public interfaces so later build phases can import them', `- Place all files in the \`${phase.folder}/\` directory`, '', '## Traceability & Simulation Alignment (ADR-033 + ADR-049)', '', '### Domain Type Fields (REQUIRED on Decision, Recommendation, ERPTransaction):', '```', 'interface Recommendation {', ' // ... domain fields ...', ' simulationId?: string; // Pipeline simulation that produced the analysis', ' traceId?: string; // Correlation ID for end-to-end tracing', ' pipelineVersion?: string; // Version of the pipeline that generated this', '}', '```', '', `${simulationId ? `### Simulation ID: \`${simulationId}\`\nUse this as the default simulationId when creating records.` : '### Simulation ID: load from .agentics/plans/manifest.json at startup.'}`, '', '### Simulation Config Integration:', '- Load .agentics/plans/manifest.json at startup (if it exists)', '- Read simulationId and traceId from it', '- Pass to all service constructors so every record carries lineage', '- Log simulationId in every analysis run: `logger.info("analysis.start", { simulationId })`', '', '### Confidence Disclosure in Demo Output:', '- The demo script MUST print an "Analysis Confidence" section', '- State the confidence level and what it means', '- If confidence < 60%: "The pilot resolves remaining uncertainty with real data"', '- If confidence >= 80%: "High confidence — proceed to pilot with standard monitoring"', '');
3302
- // ADR-PIPELINE-039: Mandatory cross-cutting requirements in EVERY prompt
3303
- lines.push('## Mandatory Cross-Cutting Requirements (EVERY module must implement these)', '', '### Logging', '- Create `src/logger.ts` (if it does not exist) with `createLogger(service: string)` that outputs JSON to stderr', '- Every log entry: `{ timestamp, level, service, event, correlationId?, ...data }`', '- Import and use the logger in THIS module. Log: function entry with key params, completion with duration, errors with context', '', '### Configuration', '- Create `src/config.ts` (if it does not exist) with typed `AppConfig` read from environment variables', '- NEVER hardcode: ports, URLs, thresholds, weights, API keys, timeouts', '- Import config in THIS module for any configurable value', '', '### Input Validation (ADR-046: Zod at EVERY boundary)', '- EVERY function receiving external data MUST Zod-parse it:', ' API request bodies: `const parsed = Schema.safeParse(req.body); if (!parsed.success) return res.status(400).json({ error: parsed.error.format() });`', ' Database rows: `const row = RowSchema.parse(rawDbRow);` — NEVER `as SomeType`', ' ERP/API responses: `const data = ResponseSchema.parse(response.data);`', ' Config values: validate ALL config with Zod at startup; refuse to start if invalid', '- NEVER use `as SomeType` to cast external data — always Zod-parse first', '', '### Error Handling', '- Define error classes extending `AppError` (create in `src/errors.ts` if needed)', '- Never `catch (e: any)` — always narrow error types', '- Log errors with logger.error() including stack trace and context', '- Event bus/emitter: wrap EACH handler in try/catch — one failure must NOT block others', '', '### Config-Driven (ADR-046: zero hardcoded magic numbers)', '- EVERY threshold, weight, factor, timeout MUST come from config (not hardcoded constants)', '- Config module validates ALL values with Zod at startup', '- If config invalid, service refuses to start with clear error', '', '### Deterministic Code (ADR-046)', '- No Math.random() in production code — use crypto.randomUUID() for IDs', '- Seed data generators may use seeded random; service code must be deterministic', '', '### Architecture (ADR-047: constructor injection, thin handlers)', '- Every service class receives dependencies via constructor — no module-level singletons', '- Route handlers: (1) Zod-validate request (2) call service (3) format response — NO business logic', '- Multi-step workflows → application service layer (src/services/workflow.ts or similar)', '- Periodic sync/ingestion: setInterval or cron, interval from config, log every run', '- Composition root (src/container.ts) wires everything — the ONLY place services are instantiated', '', '### ERP Integration (ADR-050: resilience, idempotency, schema as code)', '- Retry with exponential backoff: 1s → 2s → 4s, max 3 attempts, on 429/503/timeout', '- Idempotency key per write: hash(decisionId + timestamp) — prevents duplicate records on retry', '- Check-before-write: query for existing record before creating', '- Circuit breaker: open after 5 consecutive failures, half-open after 30s, log every state change', '- All operations logged with correlationId and responseTimeMs', '- ERP schema as code: define a TypeScript interface mapping domain fields → ERP fields, include as src/erp/schema.ts', '', '### Governance (ADR-048: RBAC, immutable audit, separation of duties)', '- Approval endpoints enforce roles via middleware: read X-User-Role header, reject if insufficient', '- Separation of duties: approver MUST NOT be the recommendation creator — check and reject with 403', '- Audit trail: SHA-256 hash chain — each entry hashes previous entry\'s hash, genesis entry uses \'genesis\'', '- Verification endpoint: GET /api/audit/verify — walks the chain, reports any broken links', '- Every state transition records: who (userId + role), what (from → to), when (server timestamp), why (rationale min 10 chars), correlationId', '', '## Anti-Patterns to AVOID (evaluation checks — violations REDUCE score)', '', '- Do NOT use console.log — use createLogger()', '- Do NOT use `as SomeType` to cast DB rows, API responses, or request bodies — Zod-parse instead', '- Do NOT put business logic in route handlers — extract to service layer', '- Do NOT hardcode thresholds, weights, or magic numbers — read from config', '- Do NOT use Math.random() in production code — use crypto.randomUUID()', '- Do NOT list unused deps in package.json — if listed, USE them', '- Do NOT let event handlers fail silently — wrap each in try/catch with logging', '- Do NOT initialize required fields to empty string \'\'', '', '## Observability Enforcement (ADR-051)', '', '### Logger — EVERY module:', '- Import createLogger from ../logger.js. Include correlationId in every log entry.', '- Entry: `logger.info(\'fn.start\', { correlationId, params })`', '- Exit: `logger.info(\'fn.complete\', { correlationId, durationMs })`', '- Error: `logger.error(\'fn.failed\', err, { correlationId, context })` — NEVER `{ error: err }`', '', '### Correlation IDs — NON-NEGOTIABLE:', '- Copy src/middleware.ts from scaffold (correlationId + requestLogger + metricsHandler + CircuitBreaker)', '- Wire: `app.use(correlationId); app.use(requestLogger); app.get(\'/metrics\', metricsHandler);`', '- Pass correlationId through all service calls, audit entries, and ERP headers', '', '### Metrics: `GET /metrics` via metricsHandler from middleware.ts', '### Circuit breaker: `new CircuitBreaker(\'erp\')` wraps all external calls', '');
3304
- fs.writeFileSync(path.join(promptsDir, filename), lines.join('\n'), { mode: 0o600, encoding: 'utf-8' });
3305
- completedPhases.push(`${order}. ${phase.title}`);
3777
+ const planItems = derivedPhases.map((phase, i) => ({
3778
+ order: i + 1,
3779
+ file: `impl-${String(i + 1).padStart(3, '0')}-${phase.slug}.md`,
3780
+ title: phase.title,
3781
+ folder: phase.folder,
3782
+ adrs: phase.adrIds,
3783
+ }));
3784
+ // ADR-PIPELINE-033: Include simulation lineage in execution plan
3785
+ const plan = {
3786
+ totalSteps,
3787
+ prompts: planItems,
3788
+ lineage: {
3789
+ simulationId: simulationId || undefined,
3790
+ traceId: traceId || undefined,
3791
+ pipelineVersion: '1.7.3',
3792
+ },
3793
+ };
3794
+ fs.writeFileSync(path.join(promptsDir, 'execution-plan.json'), JSON.stringify(plan, null, 2), { mode: 0o600, encoding: 'utf-8' });
3795
+ console.error(` [PROMPTS] Wrote ${totalSteps} implementation prompts derived from SPARC architecture + ADRs`);
3306
3796
  }
3307
- const planItems = derivedPhases.map((phase, i) => ({
3308
- order: i + 1,
3309
- file: `impl-${String(i + 1).padStart(3, '0')}-${phase.slug}.md`,
3310
- title: phase.title,
3311
- folder: phase.folder,
3312
- adrs: phase.adrIds,
3313
- }));
3314
- // ADR-PIPELINE-033: Include simulation lineage in execution plan
3315
- const plan = {
3316
- totalSteps,
3317
- prompts: planItems,
3318
- lineage: {
3319
- simulationId: simulationId || undefined,
3320
- traceId: traceId || undefined,
3321
- pipelineVersion: '1.7.3',
3322
- },
3323
- };
3324
- fs.writeFileSync(path.join(promptsDir, 'execution-plan.json'), JSON.stringify(plan, null, 2), { mode: 0o600, encoding: 'utf-8' });
3325
- console.error(` [PROMPTS] Wrote ${totalSteps} implementation prompts derived from SPARC architecture + ADRs`);
3797
+ // ADR-PIPELINE-091 §5: record the execution block on manifest.json
3798
+ // BEFORE copyPlanningArtifacts propagates the manifest into the
3799
+ // project tree, so downstream readers see the engineering tier +
3800
+ // enrichment depth in the copied artifact.
3801
+ try {
3802
+ const execBlock = computeExecutionBlock([rufloPrimaryResults.phase3, rufloPrimaryResults.phase4, rufloPrimaryResults.phase5a], readRemoteEnrichmentSnapshot(runDir));
3803
+ writeExecutionBlockToManifest(runDir, execBlock);
3804
+ process.stderr.write(` [ADR-091] engineering_tier=${execBlock.engineering_tier} ` +
3805
+ `tier_counts=ruflo-local:${execBlock.tier_counts['ruflo-local']}/template:${execBlock.tier_counts.template} ` +
3806
+ `enrichment_depth_pct=${execBlock.enrichment.enrichment_depth_pct ?? 'null'}\n`);
3807
+ }
3808
+ catch (err) {
3809
+ process.stderr.write(` [ADR-091] execution block write failed: ${err instanceof Error ? err.message : String(err)}\n`);
3810
+ }
3811
+ copyPlanningArtifacts(runDir, projectRoot);
3812
+ }
3813
+ catch (err) {
3814
+ const errMsg = err instanceof Error ? err.message : String(err);
3815
+ console.error(` [PROMPTS] Implementation prompt generation failed: ${errMsg}`);
3326
3816
  }
3327
- copyPlanningArtifacts(runDir, projectRoot);
3328
3817
  }
3329
- catch (err) {
3330
- const errMsg = err instanceof Error ? err.message : String(err);
3331
- console.error(` [PROMPTS] Implementation prompt generation failed: ${errMsg}`);
3818
+ else {
3819
+ console.error(' [PROMPTS] Skipped no ADR directory found');
3820
+ console.error(` Checked: ${path.join(runDir, 'phase4', 'adrs')} (${fs.existsSync(path.join(runDir, 'phase4', 'adrs')) ? 'exists' : 'MISSING'})`);
3821
+ console.error(` Checked: ${path.join(runDir, 'phase2', 'adrs')} (${fs.existsSync(path.join(runDir, 'phase2', 'adrs')) ? 'exists' : 'MISSING'})`);
3332
3822
  }
3333
- }
3334
- else {
3335
- console.error(' [PROMPTS] Skipped — no ADR directory found');
3336
- console.error(` Checked: ${path.join(runDir, 'phase4', 'adrs')} (${fs.existsSync(path.join(runDir, 'phase4', 'adrs')) ? 'exists' : 'MISSING'})`);
3337
- console.error(` Checked: ${path.join(runDir, 'phase2', 'adrs')} (${fs.existsSync(path.join(runDir, 'phase2', 'adrs')) ? 'exists' : 'MISSING'})`);
3338
- }
3823
+ } // close ADR-PIPELINE-093 phase5a-prompts gate-else
3339
3824
  }
3340
3825
  }
3341
3826
  // ── Pre-Phase 5: Detect implementation language from prior artifacts ──
@@ -3543,12 +4028,103 @@ services when the pilot validates the approach.
3543
4028
  }
3544
4029
  // ADR-028: Final ADR guarantee before returning
3545
4030
  await ensureAdrsExist(runDir, traceId, scenarioQuery);
4031
+ // ADR-PIPELINE-093 — Rule 5 scaffold-skip carry-over.
4032
+ // `copyPlanningArtifacts` may have persisted a `phase5b_skipped` block
4033
+ // into manifest.json. Re-read it so `writeGateBlockToManifest` below
4034
+ // preserves it when it merges in blocked_phases + inputs_produced_by_gate.
4035
+ try {
4036
+ const phase1ManifestPath = path.join(runDir, 'manifest.json');
4037
+ if (fs.existsSync(phase1ManifestPath)) {
4038
+ try {
4039
+ const mf = JSON.parse(fs.readFileSync(phase1ManifestPath, 'utf-8'));
4040
+ const skip = mf['phase5b_skipped'];
4041
+ if (skip && typeof skip === 'object') {
4042
+ const s = skip;
4043
+ if (typeof s['reason'] === 'string' && typeof s['detected'] === 'string' && typeof s['template'] === 'string') {
4044
+ gateAcc.phase5bSkipped = { reason: s['reason'], detected: s['detected'], template: s['template'] };
4045
+ }
4046
+ }
4047
+ }
4048
+ catch { /* best-effort */ }
4049
+ }
4050
+ }
4051
+ catch { /* best-effort */ }
4052
+ // ADR-PIPELINE-093 — Flush gate block into manifest.json BEFORE the
4053
+ // FATAL-path decision so the preserved run directory carries the
4054
+ // blocked_phases / inputs_produced_by_gate / phase5b_skipped diagnostics.
4055
+ try {
4056
+ // refresh blocked list from accumulator (determineBlockedPhases mirrors
4057
+ // what recordGateResult did; we use it as a sanity check)
4058
+ const blockedFromAcc = determineBlockedPhases(gateAcc.blocked.map(b => ({ phaseId: b.phase, gateResult: { ok: false, missing: [], ruflo_invoked: [], stillMissing: [...b.missing] } })));
4059
+ void blockedFromAcc; // type-guard: same shape used by determineBlockedPhases
4060
+ writeGateBlockToManifest(runDir, gateAcc);
4061
+ // ADR-PIPELINE-093 §Rule 3 surface (a): same payload into status.json
4062
+ // so the CLI `status` command / MCP `agentics-status` tool expose
4063
+ // `blocked_phases` + per-phase `inputs_produced_by_gate` arrays.
4064
+ writeGateBlockToStatusJson(runDir, gateAcc);
4065
+ }
4066
+ catch { /* best-effort */ }
4067
+ // ADR-PIPELINE-093 Rule 4 — Mandatory Phase 5a emission. The prompt-generator
4068
+ // MUST produce a valid `execution-plan.json` with ≥1 implementations[] entry
4069
+ // and at least one impl-NNN-*.md. If either is missing at this point — even
4070
+ // after all gate-triggered Ruflo invocations — the pipeline exits FATAL
4071
+ // rather than silently returning degraded output.
4072
+ {
4073
+ const promotedPromptsDir = path.join(projectRoot, '.agentics', 'plans', 'prompts');
4074
+ const executionPlanPath = path.join(promotedPromptsDir, 'execution-plan.json');
4075
+ let hasValidPlan = false;
4076
+ try {
4077
+ if (fs.existsSync(executionPlanPath)) {
4078
+ const parsed = JSON.parse(fs.readFileSync(executionPlanPath, 'utf-8'));
4079
+ const implCount = Array.isArray(parsed.implementations)
4080
+ ? parsed.implementations.length
4081
+ : Array.isArray(parsed.prompts)
4082
+ ? parsed.prompts.length
4083
+ : 0;
4084
+ hasValidPlan = implCount >= 1;
4085
+ }
4086
+ }
4087
+ catch {
4088
+ hasValidPlan = false;
4089
+ }
4090
+ let implFileCount = 0;
4091
+ try {
4092
+ if (fs.existsSync(promotedPromptsDir)) {
4093
+ implFileCount = fs.readdirSync(promotedPromptsDir).filter(f => /^impl-\d+-.+\.md$/.test(f)).length;
4094
+ }
4095
+ }
4096
+ catch {
4097
+ implFileCount = 0;
4098
+ }
4099
+ if (!hasValidPlan || implFileCount < 1) {
4100
+ const missingList = [];
4101
+ if (!hasValidPlan)
4102
+ missingList.push('execution-plan.json');
4103
+ if (implFileCount < 1)
4104
+ missingList.push('impl-NNN-*.md');
4105
+ // Final banner — the one place ADR-093 allows a loud exit.
4106
+ process.stderr.write(`[PIPELINE-093] FATAL: prompt-generator blocked — upstream artifacts unrecoverable\n`);
4107
+ process.stderr.write(` missing: ${missingList.join(', ')}\n`);
4108
+ process.stderr.write(` attempted: Ruflo invocation for phase 4 (failed or timed out)\n`);
4109
+ process.stderr.write(` run directory preserved: ${runDir}\n`);
4110
+ // Best-effort shutdown so file handles + metrics flush before exit.
4111
+ try {
4112
+ const totalTiming = Date.now() - pipelineStart;
4113
+ shutdownSwarm(traceId, false, totalTiming);
4114
+ }
4115
+ catch { /* best-effort */ }
4116
+ process.exit(1);
4117
+ }
4118
+ }
3546
4119
  // ── Shutdown swarm and persist metrics ──
3547
4120
  const totalTiming = Date.now() - pipelineStart;
3548
4121
  const pipelineSuccess = phases.every(p => p.status === 'completed' || p.status === 'skipped');
3549
4122
  shutdownSwarm(traceId, pipelineSuccess, totalTiming);
3550
4123
  // ── Final Summary ──
3551
- const result = buildResult(traceId, runDir, phases, pipelineStart, mode, scenarioBranch, commitHash, execCtx.remoteUrl);
4124
+ const result = buildResult(traceId, runDir, phases, pipelineStart, mode, scenarioBranch, commitHash, execCtx.remoteUrl,
4125
+ // ADR-PIPELINE-093 §Rule 3 (c): blocked phases threaded into the
4126
+ // result so the final `agentics ask` banner can warn the user.
4127
+ gateAcc.blocked.map(b => ({ phase: b.phase, missing: [...b.missing] })));
3552
4128
  printFinalSummary(result);
3553
4129
  return result;
3554
4130
  }
@@ -3791,6 +4367,386 @@ function persistAgenticsResults(phaseDir, results) {
3791
4367
  // Non-fatal — agent result persistence should never block the pipeline
3792
4368
  }
3793
4369
  }
4370
+ /**
4371
+ * Read `manifest.agents_invoked` from the run dir and partition into
4372
+ * successful vs errored remote enrichment agents. ADR-PIPELINE-091 §5.
4373
+ *
4374
+ * Treats `status === "success"` and `status === "invoked"` as available; any
4375
+ * other status (including HTTP error codes, `"timeout"`, `"error"`) is
4376
+ * counted as errored. Missing / unreadable manifest => empty lists.
4377
+ */
4378
+ export function readRemoteEnrichmentSnapshot(runDir) {
4379
+ const manifestPath = path.join(runDir, 'manifest.json');
4380
+ if (!fs.existsSync(manifestPath)) {
4381
+ return { available: [], errored: [], entries: [] };
4382
+ }
4383
+ let entries = [];
4384
+ try {
4385
+ const raw = fs.readFileSync(manifestPath, 'utf-8');
4386
+ const manifest = JSON.parse(raw);
4387
+ entries = Array.isArray(manifest.agents_invoked) ? manifest.agents_invoked : [];
4388
+ }
4389
+ catch {
4390
+ return { available: [], errored: [], entries: [] };
4391
+ }
4392
+ const available = [];
4393
+ const errored = [];
4394
+ for (const entry of entries) {
4395
+ if (!entry || typeof entry.agent !== 'string')
4396
+ continue;
4397
+ const isOk = entry.status === 'success' || entry.status === 'invoked';
4398
+ (isOk ? available : errored).push(entry.agent);
4399
+ }
4400
+ return { available, errored, entries };
4401
+ }
4402
+ /**
4403
+ * Resolve a per-phase execution tier from a Ruflo primary-executor result.
4404
+ * A phase is tagged `"ruflo-local"` iff Ruflo actually succeeded. Anything
4405
+ * else (unavailable, timeout, swarm error, missing result) => `"template"`.
4406
+ */
4407
+ function phaseTierFromRufloResult(result) {
4408
+ if (!result)
4409
+ return 'template';
4410
+ if (result.executionTier === 'ruflo-local' && result.success)
4411
+ return 'ruflo-local';
4412
+ return 'template';
4413
+ }
4414
+ /**
4415
+ * Build the `execution` block that ADR-PIPELINE-091 §5 writes back into
4416
+ * `runDir/manifest.json`. Pure function so it can be unit-tested without
4417
+ * spinning up the actual pipeline.
4418
+ */
4419
+ export function computeExecutionBlock(rufloResults, enrichment) {
4420
+ const perPhaseTiers = rufloResults.map(phaseTierFromRufloResult);
4421
+ const rufloCount = perPhaseTiers.filter(t => t === 'ruflo-local').length;
4422
+ const templateCount = perPhaseTiers.filter(t => t === 'template').length;
4423
+ const engineering_tier = rufloCount > 0 ? 'ruflo-local' : 'template';
4424
+ const totalRemote = enrichment.available.length + enrichment.errored.length;
4425
+ const enrichment_depth_pct = totalRemote === 0
4426
+ ? null
4427
+ : Math.round((enrichment.available.length / totalRemote) * 100);
4428
+ return {
4429
+ engineering_tier,
4430
+ tier_counts: { 'ruflo-local': rufloCount, template: templateCount },
4431
+ enrichment: {
4432
+ remote_agents_available: [...enrichment.available],
4433
+ remote_agents_errored: [...enrichment.errored],
4434
+ enrichment_depth_pct,
4435
+ },
4436
+ };
4437
+ }
4438
+ /**
4439
+ * Merge the ADR-091 `execution` block into `runDir/manifest.json` in-place.
4440
+ * Reads the existing manifest, sets `execution`, writes it back with the
4441
+ * same mode/encoding convention used elsewhere in this file. Non-fatal on
4442
+ * I/O or JSON errors — the pipeline continues if manifest merge fails.
4443
+ */
4444
+ export function writeExecutionBlockToManifest(runDir, block) {
4445
+ const manifestPath = path.join(runDir, 'manifest.json');
4446
+ try {
4447
+ let manifest = {};
4448
+ if (fs.existsSync(manifestPath)) {
4449
+ try {
4450
+ manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf-8'));
4451
+ }
4452
+ catch {
4453
+ manifest = {};
4454
+ }
4455
+ }
4456
+ manifest['execution'] = block;
4457
+ fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2), { mode: 0o600, encoding: 'utf-8' });
4458
+ }
4459
+ catch (err) {
4460
+ process.stderr.write(` [ADR-091] Failed to merge execution block into manifest.json: ${err instanceof Error ? err.message : String(err)}\n`);
4461
+ }
4462
+ }
4463
+ function newGateAccumulator() {
4464
+ return {
4465
+ blocked: [],
4466
+ producedByGate: {
4467
+ phase2: [],
4468
+ 'phase3-sparc': [],
4469
+ 'phase4-adrs-ddd': [],
4470
+ 'phase5a-prompts': [],
4471
+ 'phase5b-scaffold': [],
4472
+ },
4473
+ phase5bSkipped: null,
4474
+ };
4475
+ }
4476
+ /**
4477
+ * Record a gate result into the accumulator. `inputs_produced_by_gate`
4478
+ * reflects the initially-missing paths that became present after Ruflo
4479
+ * ran (i.e. `missing` minus `stillMissing`). A stable shape is important
4480
+ * for downstream readers even when the gate passed on first check.
4481
+ */
4482
+ function recordGateResult(acc, phaseId, result) {
4483
+ if (!result.ok && result.stillMissing.length > 0) {
4484
+ acc.blocked.push({ phase: phaseId, missing: result.stillMissing });
4485
+ }
4486
+ if (result.ruflo_invoked.length > 0) {
4487
+ const stillMissingSet = new Set(result.stillMissing);
4488
+ const produced = result.missing.filter(p => !stillMissingSet.has(p));
4489
+ if (produced.length > 0) {
4490
+ acc.producedByGate[phaseId] = [...acc.producedByGate[phaseId], ...produced];
4491
+ }
4492
+ }
4493
+ }
4494
+ /**
4495
+ * ADR-PIPELINE-093 — merge `blocked_phases`, per-phase
4496
+ * `phase{N}_inputs_produced_by_gate`, and `phase5b_skipped` into
4497
+ * `runDir/manifest.json`. Read-modify-write pattern mirrors
4498
+ * `writeExecutionBlockToManifest` / `writePlanPromotionToManifest`.
4499
+ *
4500
+ * `blocked_phases` is always present (empty array when no phase blocked).
4501
+ * The per-phase `inputs_produced_by_gate` keys are always present so
4502
+ * downstream readers can treat them as a stable schema.
4503
+ */
4504
+ export function writeGateBlockToManifest(runDir, acc) {
4505
+ const manifestPath = path.join(runDir, 'manifest.json');
4506
+ try {
4507
+ let manifest = {};
4508
+ if (fs.existsSync(manifestPath)) {
4509
+ try {
4510
+ manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf-8'));
4511
+ }
4512
+ catch {
4513
+ manifest = {};
4514
+ }
4515
+ }
4516
+ manifest['blocked_phases'] = acc.blocked.map(b => ({ phase: b.phase, missing: [...b.missing] }));
4517
+ manifest['phase2_inputs_produced_by_gate'] = [...acc.producedByGate['phase2']];
4518
+ manifest['phase3_inputs_produced_by_gate'] = [...acc.producedByGate['phase3-sparc']];
4519
+ manifest['phase4_inputs_produced_by_gate'] = [...acc.producedByGate['phase4-adrs-ddd']];
4520
+ manifest['phase5a_inputs_produced_by_gate'] = [...acc.producedByGate['phase5a-prompts']];
4521
+ if (acc.phase5bSkipped) {
4522
+ manifest['phase5b_skipped'] = { ...acc.phase5bSkipped };
4523
+ }
4524
+ fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2), { mode: 0o600, encoding: 'utf-8' });
4525
+ }
4526
+ catch (err) {
4527
+ process.stderr.write(` [PIPELINE-093] Failed to merge gate block into manifest.json: ${err instanceof Error ? err.message : String(err)}\n`);
4528
+ }
4529
+ }
4530
+ /**
4531
+ * ADR-PIPELINE-093 §Rule 3 (surface b) — same gate-block payload, written
4532
+ * into `runDir/status.json` so the CLI `status` command + MCP
4533
+ * `agentics-status` tool carry `blocked_phases` and the per-phase
4534
+ * `phase{N}_inputs_produced_by_gate` arrays alongside the ADR-088 keys.
4535
+ *
4536
+ * Read-modify-write pattern matches the CLI's local `updateStatus` helper
4537
+ * (`src/cli/index.ts`:~2501). Best-effort: if `status.json` does not exist
4538
+ * (e.g. running outside the MCP fast-return path), the file is created so
4539
+ * the gate block is always observable regardless of entry point.
4540
+ *
4541
+ * Both `writeGateBlockToManifest` and this function should be called from
4542
+ * the same accumulator snapshot so manifest.json and status.json stay in
4543
+ * sync. The CLI-surfaced `status.json` is the source of truth for the MCP
4544
+ * `agentics-status` tool (which shells out to `agentics status`).
4545
+ */
4546
+ export function writeGateBlockToStatusJson(runDir, acc) {
4547
+ const statusPath = path.join(runDir, 'status.json');
4548
+ try {
4549
+ let status = {};
4550
+ if (fs.existsSync(statusPath)) {
4551
+ try {
4552
+ status = JSON.parse(fs.readFileSync(statusPath, 'utf-8'));
4553
+ }
4554
+ catch {
4555
+ status = {};
4556
+ }
4557
+ }
4558
+ status['blocked_phases'] = acc.blocked.map(b => ({ phase: b.phase, missing: [...b.missing] }));
4559
+ status['phase2_inputs_produced_by_gate'] = [...acc.producedByGate['phase2']];
4560
+ status['phase3_inputs_produced_by_gate'] = [...acc.producedByGate['phase3-sparc']];
4561
+ status['phase4_inputs_produced_by_gate'] = [...acc.producedByGate['phase4-adrs-ddd']];
4562
+ status['phase5a_inputs_produced_by_gate'] = [...acc.producedByGate['phase5a-prompts']];
4563
+ if (acc.phase5bSkipped) {
4564
+ status['phase5b_skipped'] = { ...acc.phase5bSkipped };
4565
+ }
4566
+ status['updatedAt'] = new Date().toISOString();
4567
+ const tmp = statusPath + '.tmp';
4568
+ fs.writeFileSync(tmp, JSON.stringify(status, null, 2), { mode: 0o600, encoding: 'utf-8' });
4569
+ fs.renameSync(tmp, statusPath);
4570
+ }
4571
+ catch (err) {
4572
+ process.stderr.write(` [PIPELINE-093] Failed to merge gate block into status.json: ${err instanceof Error ? err.message : String(err)}\n`);
4573
+ }
4574
+ }
4575
+ /**
4576
+ * ADR-PIPELINE-093 — thin wrapper around `gatePhaseInputs` that surfaces
4577
+ * a consistent `[GATE]` log line and records the result into the
4578
+ * pipeline-wide accumulator. Returns the raw `GateResult` so the caller
4579
+ * can branch on `ok`.
4580
+ */
4581
+ async function runPhaseGate(phaseId, scenarioQuery, traceId, runDir, acc) {
4582
+ const dossier = {
4583
+ scenarioQuery,
4584
+ traceId,
4585
+ runDir,
4586
+ artifacts: {},
4587
+ };
4588
+ const context = {
4589
+ outputDir: path.join(runDir, '.ruflo-cache', `gate-${phaseId}`),
4590
+ };
4591
+ let result;
4592
+ try {
4593
+ result = await gatePhaseInputs(phaseId, { runDir, dossier, context });
4594
+ }
4595
+ catch (err) {
4596
+ // gatePhaseInputs never throws, but defensive fallback keeps the
4597
+ // pipeline running if the gate's own internals crash.
4598
+ const msg = err instanceof Error ? err.message : String(err);
4599
+ process.stderr.write(` [GATE] ${phaseId} threw unexpectedly: ${msg}\n`);
4600
+ result = { ok: false, missing: [], ruflo_invoked: [], stillMissing: [] };
4601
+ }
4602
+ recordGateResult(acc, phaseId, result);
4603
+ if (!result.ok && result.stillMissing.length > 0) {
4604
+ process.stderr.write(` [GATE] phase ${phaseId} blocked — stillMissing: ${result.stillMissing.join(', ')}\n`);
4605
+ }
4606
+ return result;
4607
+ }
4608
+ /**
4609
+ * Detect the target project language from Phase 1 manifest.json
4610
+ * (`project.language`) first, then falls back to common project root
4611
+ * marker files. Returns `'unknown'` when no signal is available — the
4612
+ * caller decides whether to skip or proceed in that case.
4613
+ */
4614
+ /**
4615
+ * ADR-PIPELINE-093 §Rule 5 — single arbiter for which scaffold template set
4616
+ * `copyPlanningArtifacts` should emit. Called ONCE per invocation so the TS
4617
+ * block and the Python block gate on the same decision.
4618
+ *
4619
+ * - `typescript` → emit TS, skip Python, no manifest record.
4620
+ * - `python` → skip TS, emit Python, no manifest record.
4621
+ * - `go`|`rust`|`unknown` → skip both, record `phase5b_skipped` with the
4622
+ * detected language and `template: 'typescript'`
4623
+ * (kept for Scenario-5 compatibility — the
4624
+ * skip block's shape is the ADR-093 schema).
4625
+ *
4626
+ * The detected-language + template-name pair in `skipped` matches what the
4627
+ * pre-093-fix gate wrote to `manifest.json`, so downstream status.json /
4628
+ * MCP consumers don't see a shape change on the mismatch path.
4629
+ */
4630
+ export function decideScaffoldEmission(detected) {
4631
+ if (detected === 'typescript')
4632
+ return { emitTs: true, emitPy: false, skipped: null };
4633
+ if (detected === 'python')
4634
+ return { emitTs: false, emitPy: true, skipped: null };
4635
+ // go / rust / unknown — no matching template set, record mismatch.
4636
+ return {
4637
+ emitTs: false,
4638
+ emitPy: false,
4639
+ skipped: { reason: 'language-mismatch', detected, template: 'typescript' },
4640
+ };
4641
+ }
4642
+ export function detectProjectLanguage(projectRoot, phase1Manifest) {
4643
+ // 1. Explicit signal from Phase 1 manifest.
4644
+ const projBlock = phase1Manifest?.['project'];
4645
+ if (projBlock && typeof projBlock === 'object') {
4646
+ const lang = projBlock['language'];
4647
+ if (typeof lang === 'string') {
4648
+ const normalized = lang.toLowerCase();
4649
+ if (normalized === 'typescript' || normalized === 'javascript')
4650
+ return 'typescript';
4651
+ if (normalized === 'python')
4652
+ return 'python';
4653
+ if (normalized === 'go' || normalized === 'golang')
4654
+ return 'go';
4655
+ if (normalized === 'rust')
4656
+ return 'rust';
4657
+ }
4658
+ }
4659
+ // 2. Project-root marker files.
4660
+ const fileExists = (p) => {
4661
+ try {
4662
+ return fs.statSync(p).isFile();
4663
+ }
4664
+ catch {
4665
+ return false;
4666
+ }
4667
+ };
4668
+ if (fileExists(path.join(projectRoot, 'package.json')))
4669
+ return 'typescript';
4670
+ if (fileExists(path.join(projectRoot, 'pyproject.toml')) ||
4671
+ fileExists(path.join(projectRoot, 'setup.py')))
4672
+ return 'python';
4673
+ if (fileExists(path.join(projectRoot, 'go.mod')))
4674
+ return 'go';
4675
+ if (fileExists(path.join(projectRoot, 'Cargo.toml')))
4676
+ return 'rust';
4677
+ return 'unknown';
4678
+ }
4679
+ /**
4680
+ * Build the `extras` payload attached to a `Phase1Dossier` for the Ruflo
4681
+ * primary executor. Successful remote enrichment agents are surfaced so
4682
+ * Ruflo can weave their output into task descriptions. ADR-091 §2.
4683
+ *
4684
+ * When no remote enrichment exists for this run, `remote_enrichment` is
4685
+ * still present but its arrays are empty — Ruflo task builders tolerate
4686
+ * either shape.
4687
+ */
4688
+ function buildRufloDossierExtras(runDir, enrichment) {
4689
+ const successfulEntries = enrichment.entries.filter(e => e.status === 'success' || e.status === 'invoked');
4690
+ const erroredEntries = enrichment.entries.filter(e => !(e.status === 'success' || e.status === 'invoked'));
4691
+ return {
4692
+ remote_enrichment: {
4693
+ runDir,
4694
+ available: successfulEntries.map(e => ({ domain: e.domain, agent: e.agent })),
4695
+ errored: erroredEntries.map(e => ({ domain: e.domain, agent: e.agent, status: e.status })),
4696
+ },
4697
+ };
4698
+ }
4699
+ /**
4700
+ * Invoke the Ruflo primary executor for a given engineering phase. Returns
4701
+ * the result (or a synthesized unavailable result on unexpected throw) so
4702
+ * auto-chain can always tag the phase for the ADR-091 execution block.
4703
+ *
4704
+ * This is a thin wrapper around `runPrimaryPhaseExecution` that:
4705
+ * 1. Ensures the dossier carries `scenarioQuery`, `traceId`, `runDir`,
4706
+ * plus Phase 1 artifacts + remote enrichment extras.
4707
+ * 2. Surfaces a `[ADR-091]` log line so operators can see which phases
4708
+ * actually executed via the local swarm.
4709
+ * 3. Never throws.
4710
+ */
4711
+ async function runRufloPrimaryForPhase(phaseId, scenarioQuery, traceId, runDir, priorArtifacts, enrichment, outputDir, language) {
4712
+ const dossier = {
4713
+ scenarioQuery,
4714
+ traceId,
4715
+ runDir,
4716
+ artifacts: priorArtifacts,
4717
+ extras: buildRufloDossierExtras(runDir, enrichment),
4718
+ };
4719
+ const context = {
4720
+ outputDir,
4721
+ ...(language ? { language } : {}),
4722
+ };
4723
+ try {
4724
+ const result = await runPrimaryPhaseExecution(phaseId, dossier, context);
4725
+ if (result.executionTier === 'ruflo-local' && result.success) {
4726
+ process.stderr.write(` [ADR-091] Ruflo primary executor produced ${result.filesModified} file(s) for ${phaseId} in ${result.timing}ms\n`);
4727
+ }
4728
+ else if (result.executionTier === 'unavailable') {
4729
+ process.stderr.write(` [ADR-091] Ruflo unavailable for ${phaseId} (${result.reason ?? 'no-reason'}) — falling through to template coordinator\n`);
4730
+ }
4731
+ else {
4732
+ process.stderr.write(` [ADR-091] Ruflo primary executor did not produce output for ${phaseId} (${result.reason ?? 'no-reason'}) — falling through\n`);
4733
+ }
4734
+ return result;
4735
+ }
4736
+ catch (err) {
4737
+ const msg = err instanceof Error ? err.message : String(err);
4738
+ process.stderr.write(` [ADR-091] Ruflo primary executor threw for ${phaseId}: ${msg.slice(0, 200)}\n`);
4739
+ return {
4740
+ phaseId,
4741
+ success: false,
4742
+ executionTier: 'unavailable',
4743
+ reason: 'swarm-error',
4744
+ filesModified: 0,
4745
+ timing: 0,
4746
+ message: `Ruflo primary executor threw for ${phaseId}: ${msg.slice(0, 200)}`,
4747
+ };
4748
+ }
4749
+ }
3794
4750
  // ============================================================================
3795
4751
  // Stdout Display Formatter
3796
4752
  // ============================================================================
@@ -3812,6 +4768,21 @@ export function formatAutoChainForDisplay(result) {
3812
4768
  lines.push(` Duration: ${(result.totalTiming / 1000).toFixed(1)}s`);
3813
4769
  lines.push(` Overall: ${result.success ? 'SUCCESS' : 'FAILED'}`);
3814
4770
  lines.push('');
4771
+ // ADR-PIPELINE-093 §Rule 3 surface (c) — blocked-phases warning banner.
4772
+ // Emits only when one or more phases could not run because their
4773
+ // prerequisites were unreachable after Ruflo retry. Zero-blocked runs
4774
+ // render no warning section.
4775
+ const blocked = result.blockedPhases ?? [];
4776
+ if (blocked.length > 0) {
4777
+ lines.push(' WARNING — BLOCKED PHASES:');
4778
+ for (const b of blocked) {
4779
+ const missingList = b.missing.length > 0 ? b.missing.join(', ') : '(unspecified)';
4780
+ lines.push(` - ${b.phase}: missing ${missingList}`);
4781
+ }
4782
+ lines.push(' Consider running the pipeline again, or check');
4783
+ lines.push(` ~/.agentics/runs/${result.traceId}/ for partial output.`);
4784
+ lines.push('');
4785
+ }
3815
4786
  // Phase-by-phase summary
3816
4787
  lines.push(' Phase Results:');
3817
4788
  lines.push(' ' + '-'.repeat(68));
@@ -3877,7 +4848,7 @@ export function formatAutoChainForDisplay(result) {
3877
4848
  lines.push('');
3878
4849
  return lines.join('\n');
3879
4850
  }
3880
- function buildResult(traceId, runDir, phases, startTime, executionMode = 'development', scenarioBranch, commitHash, remoteUrl) {
4851
+ function buildResult(traceId, runDir, phases, startTime, executionMode = 'development', scenarioBranch, commitHash, remoteUrl, blockedPhases) {
3881
4852
  const success = phases.every(p => p.status === 'completed' || p.status === 'skipped');
3882
4853
  // Build GitHub URL if we have a remote and a branch
3883
4854
  let githubUrl;
@@ -3897,6 +4868,9 @@ function buildResult(traceId, runDir, phases, startTime, executionMode = 'develo
3897
4868
  scenarioBranch,
3898
4869
  commitHash,
3899
4870
  githubUrl,
4871
+ // ADR-PIPELINE-093 §Rule 3 surface (c): carry blocked phases into the
4872
+ // result so `formatAutoChainForDisplay` can emit the banner warning.
4873
+ blockedPhases: blockedPhases ? blockedPhases.map(b => ({ phase: b.phase, missing: [...b.missing] })) : [],
3900
4874
  };
3901
4875
  }
3902
4876
  //# sourceMappingURL=auto-chain.js.map