@llm-dev-ops/agentics-cli 2.7.0 → 2.7.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. package/dist/cli/index.js +30 -0
  2. package/dist/cli/index.js.map +1 -1
  3. package/dist/pipeline/auto-chain.d.ts +196 -2
  4. package/dist/pipeline/auto-chain.d.ts.map +1 -1
  5. package/dist/pipeline/auto-chain.js +1920 -884
  6. package/dist/pipeline/auto-chain.js.map +1 -1
  7. package/dist/pipeline/enterprise/agent-error-capture.d.ts +76 -0
  8. package/dist/pipeline/enterprise/agent-error-capture.d.ts.map +1 -0
  9. package/dist/pipeline/enterprise/agent-error-capture.js +141 -0
  10. package/dist/pipeline/enterprise/agent-error-capture.js.map +1 -0
  11. package/dist/pipeline/enterprise/artifact-renderers.d.ts +30 -0
  12. package/dist/pipeline/enterprise/artifact-renderers.d.ts.map +1 -1
  13. package/dist/pipeline/enterprise/artifact-renderers.js +129 -1
  14. package/dist/pipeline/enterprise/artifact-renderers.js.map +1 -1
  15. package/dist/pipeline/enterprise/pass-executor.d.ts.map +1 -1
  16. package/dist/pipeline/enterprise/pass-executor.js +52 -0
  17. package/dist/pipeline/enterprise/pass-executor.js.map +1 -1
  18. package/dist/pipeline/enterprise/pipeline-orchestrator.d.ts.map +1 -1
  19. package/dist/pipeline/enterprise/pipeline-orchestrator.js +15 -0
  20. package/dist/pipeline/enterprise/pipeline-orchestrator.js.map +1 -1
  21. package/dist/pipeline/enterprise/types.d.ts +21 -0
  22. package/dist/pipeline/enterprise/types.d.ts.map +1 -1
  23. package/dist/pipeline/gate/feature-flags.d.ts +30 -0
  24. package/dist/pipeline/gate/feature-flags.d.ts.map +1 -0
  25. package/dist/pipeline/gate/feature-flags.js +37 -0
  26. package/dist/pipeline/gate/feature-flags.js.map +1 -0
  27. package/dist/pipeline/gate/phase-dependency-gate.d.ts +179 -0
  28. package/dist/pipeline/gate/phase-dependency-gate.d.ts.map +1 -0
  29. package/dist/pipeline/gate/phase-dependency-gate.js +571 -0
  30. package/dist/pipeline/gate/phase-dependency-gate.js.map +1 -0
  31. package/dist/pipeline/local-fallback/phase1-consensus-reader.d.ts +33 -0
  32. package/dist/pipeline/local-fallback/phase1-consensus-reader.d.ts.map +1 -0
  33. package/dist/pipeline/local-fallback/phase1-consensus-reader.js +99 -0
  34. package/dist/pipeline/local-fallback/phase1-consensus-reader.js.map +1 -0
  35. package/dist/pipeline/local-fallback/phase3-local-fallback.d.ts +26 -0
  36. package/dist/pipeline/local-fallback/phase3-local-fallback.d.ts.map +1 -0
  37. package/dist/pipeline/local-fallback/phase3-local-fallback.js +127 -0
  38. package/dist/pipeline/local-fallback/phase3-local-fallback.js.map +1 -0
  39. package/dist/pipeline/local-fallback/phase4-local-fallback.d.ts +21 -0
  40. package/dist/pipeline/local-fallback/phase4-local-fallback.d.ts.map +1 -0
  41. package/dist/pipeline/local-fallback/phase4-local-fallback.js +240 -0
  42. package/dist/pipeline/local-fallback/phase4-local-fallback.js.map +1 -0
  43. package/dist/pipeline/local-fallback/phase5a-local-fallback.d.ts +28 -0
  44. package/dist/pipeline/local-fallback/phase5a-local-fallback.d.ts.map +1 -0
  45. package/dist/pipeline/local-fallback/phase5a-local-fallback.js +166 -0
  46. package/dist/pipeline/local-fallback/phase5a-local-fallback.js.map +1 -0
  47. package/dist/pipeline/phase3-sparc/phase3-sparc-coordinator.d.ts.map +1 -1
  48. package/dist/pipeline/phase3-sparc/phase3-sparc-coordinator.js +280 -40
  49. package/dist/pipeline/phase3-sparc/phase3-sparc-coordinator.js.map +1 -1
  50. package/dist/pipeline/phase4-adrs/phase4-adrs-coordinator.d.ts.map +1 -1
  51. package/dist/pipeline/phase4-adrs/phase4-adrs-coordinator.js +363 -87
  52. package/dist/pipeline/phase4-adrs/phase4-adrs-coordinator.js.map +1 -1
  53. package/dist/pipeline/phases/prompt-generator.d.ts.map +1 -1
  54. package/dist/pipeline/phases/prompt-generator.js +303 -6
  55. package/dist/pipeline/phases/prompt-generator.js.map +1 -1
  56. package/dist/pipeline/ruflo-phase-executor.d.ts +104 -1
  57. package/dist/pipeline/ruflo-phase-executor.d.ts.map +1 -1
  58. package/dist/pipeline/ruflo-phase-executor.js +406 -4
  59. package/dist/pipeline/ruflo-phase-executor.js.map +1 -1
  60. package/dist/pipeline/swarm-orchestrator.d.ts +47 -0
  61. package/dist/pipeline/swarm-orchestrator.d.ts.map +1 -1
  62. package/dist/pipeline/swarm-orchestrator.js +130 -3
  63. package/dist/pipeline/swarm-orchestrator.js.map +1 -1
  64. 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,66 @@ 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
+ // ADR-094 Decision 4: emit skipped-due-to-upstream entries for phases 3..6.
2608
+ pushSkippedDueToUpstream(phases, { phase: 2, label: 'Deep Research', reason: errMsg }, runDir);
2609
+ return buildResult(traceId, runDir, phases, pipelineStart, mode);
2610
+ }
2611
+ } // close ADR-PIPELINE-093 phase2 gate-else
2190
2612
  }
2191
2613
  }
2192
2614
  // ── Phase 3: SPARC + London TDD ──
@@ -2209,82 +2631,101 @@ export async function executeAutoChain(traceId, options = {}) {
2209
2631
  console.error(' [CLEANUP] Removing incomplete phase3 directory from prior run');
2210
2632
  fs.rmSync(phaseDir, { recursive: true, force: true });
2211
2633
  }
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);
2634
+ // ADR-PIPELINE-093 Phase 3 gate runs BEFORE the ADR-091 Ruflo-primary
2635
+ // call so the gate verifies (and, if necessary, regenerates) upstream
2636
+ // inputs before the phase's own Ruflo pass executes.
2637
+ const phase3Gate = await runPhaseGate('phase3-sparc', scenarioQuery, traceId, runDir, gateAcc);
2638
+ if (!phase3Gate.ok && phase3Gate.stillMissing.length > 0) {
2639
+ phaseBlocked['phase3-sparc'] = true;
2640
+ console.error(' [SKIP] Phase 3 blocked by gate — upstream inputs unrecoverable');
2641
+ phases.push({ phase: 3, label: 'SPARC + London TDD', status: 'skipped', timing: Date.now() - phaseStart, artifacts: [], outputDir: phaseDir, error: `blocked: ${phase3Gate.stillMissing.join(', ')}` });
2642
+ // Continue to Phase 4 — its own gate decides whether it can run.
2240
2643
  }
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);
2644
+ else {
2645
+ // ADR-PIPELINE-091: Ruflo runs FIRST as the primary engineering-artifact
2646
+ // producer. Its output lands in runDir/engineering/*.md; the downstream
2647
+ // phase3 coordinator picks it up as its primary input. Remote diligence
2648
+ // agents that DID return from Phase 1 are attached as enrichment only.
2649
+ rufloPrimaryResults.phase3 = await runRufloPrimaryForPhase('phase3-sparc', scenarioQuery, traceId, runDir, collectPhase3Artifacts(runDir), readRemoteEnrichmentSnapshot(runDir), path.join(runDir, '.ruflo-cache', 'phase3-primary'));
2650
+ const agentResults = await dispatchPhaseAgents(PHASE_AGENTS[3], traceId, runDir, scenarioQuery);
2651
+ // Ruflo swarm + agentics agents: write SPARC specs + TDD plans cooperatively
2652
+ const rufloP3Dir = path.join(runDir, '.ruflo-cache', 'phase3');
2653
+ const rufloResult = executeRufloPhaseSwarm({
2654
+ phase: 3, label: 'SPARC + London TDD', scenarioQuery,
2655
+ runDir, traceId, outputDir: rufloP3Dir,
2656
+ tasks: buildPhase3Tasks(scenarioQuery, collectPhase3Artifacts(runDir)),
2657
+ agenticsResults: agentResults,
2658
+ priorArtifacts: collectPhase3Artifacts(runDir),
2659
+ });
2660
+ if (rufloResult.filesModified > 0) {
2661
+ console.error(` [RUFLO-P3] ${rufloResult.filesModified} files generated (swarm+agents) in ${rufloResult.timing}ms`);
2662
+ }
2663
+ // Persist agent results to .pre-phase3 so generators can find them.
2664
+ // CANNOT write to phase3Dir — append-only guard throws if it exists.
2665
+ persistAgenticsResults(path.join(runDir, '.pre-phase3'), agentResults);
2666
+ try {
2667
+ const result = await executePhase3Command({ trace: traceId });
2668
+ mergeRufloCacheIntoPhase(runDir, 3);
2669
+ const timing = Date.now() - phaseStart;
2670
+ const artifactPaths = extractArtifactPaths(result.manifest.artifacts);
2671
+ console.error(formatPhase3ForDisplay(result));
2672
+ printArtifactLinks(phaseDir, artifactPaths);
2673
+ storePhaseArtifacts(PHASE_AGENTS[3], traceId, runDir, artifactPaths, timing);
2674
+ await reviewPhaseOutput(PHASE_AGENTS[3], traceId, runDir);
2675
+ persistAgenticsResults(phaseDir, agentResults);
2676
+ phases.push({ phase: 3, label: 'SPARC + London TDD', status: 'completed', timing, artifacts: artifactPaths, outputDir: phaseDir, agenticsAgents: buildAgentSummary(agentResults, PHASE_AGENTS[3].agenticsServices.length) });
2677
+ copyPlanningArtifacts(runDir, projectRoot);
2678
+ }
2679
+ catch (err) {
2680
+ const errMsg = err instanceof Error ? err.message : String(err);
2681
+ console.error(` [FAIL] Phase 3 failed: ${errMsg}`);
2682
+ recordPhaseFailure(PHASE_AGENTS[3], traceId, errMsg);
2683
+ // Phase 3 refines Phase 2 SPARC via LLM. If it fails (timeout, no LLM),
2684
+ // carry Phase 2 artifacts forward so Phases 4-6 can still proceed.
2685
+ const phase2SparcDir = path.join(runDir, 'phase2', 'sparc');
2686
+ const phase2SparcCombined = path.join(phase2SparcDir, 'sparc-combined.json');
2687
+ if (fs.existsSync(phase2SparcCombined)) {
2688
+ console.error(' [RECOVER] Phase 2 SPARC artifacts exist — copying to phase3/ so pipeline can continue');
2689
+ const phase3SparcDir = path.join(phaseDir, 'sparc');
2690
+ fs.mkdirSync(phase3SparcDir, { recursive: true });
2691
+ // Copy all Phase 2 SPARC files to Phase 3 directory
2692
+ const phase2SparcFiles = fs.readdirSync(phase2SparcDir);
2693
+ for (const file of phase2SparcFiles) {
2694
+ const src = path.join(phase2SparcDir, file);
2695
+ const dest = path.join(phase3SparcDir, file);
2271
2696
  if (fs.statSync(src).isFile()) {
2272
2697
  fs.copyFileSync(src, dest);
2273
2698
  }
2274
2699
  }
2700
+ // Also copy TDD if available from Phase 2
2701
+ const phase2TddDir = path.join(runDir, 'phase2', 'tdd');
2702
+ if (fs.existsSync(phase2TddDir)) {
2703
+ const phase3TddDir = path.join(phaseDir, 'tdd');
2704
+ fs.mkdirSync(phase3TddDir, { recursive: true });
2705
+ const phase2TddFiles = fs.readdirSync(phase2TddDir);
2706
+ for (const file of phase2TddFiles) {
2707
+ const src = path.join(phase2TddDir, file);
2708
+ const dest = path.join(phase3TddDir, file);
2709
+ if (fs.statSync(src).isFile()) {
2710
+ fs.copyFileSync(src, dest);
2711
+ }
2712
+ }
2713
+ }
2714
+ console.error(' [RECOVER] Phase 2 artifacts carried forward — continuing to Phase 4');
2715
+ phases.push({ phase: 3, label: 'SPARC + London TDD', status: 'completed', timing: Date.now() - phaseStart, artifacts: [], outputDir: phaseDir, error: `Recovered from Phase 2: ${errMsg}` });
2716
+ copyPlanningArtifacts(runDir, projectRoot);
2717
+ }
2718
+ else {
2719
+ console.error(' [ABORT] No Phase 2 SPARC artifacts to recover from — pipeline cannot continue');
2720
+ phases.push({ phase: 3, label: 'SPARC + London TDD', status: 'failed', timing: Date.now() - phaseStart, artifacts: [], outputDir: phaseDir, error: errMsg });
2721
+ copyPlanningArtifacts(runDir, projectRoot);
2722
+ await ensureAdrsExist(runDir, traceId, scenarioQuery);
2723
+ // ADR-094 Decision 4: emit skipped-due-to-upstream entries for phases 4..6.
2724
+ pushSkippedDueToUpstream(phases, { phase: 3, label: 'SPARC + London TDD', reason: errMsg }, runDir);
2725
+ return buildResult(traceId, runDir, phases, pipelineStart, mode);
2275
2726
  }
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
2727
  }
2287
- }
2728
+ } // close ADR-PIPELINE-093 phase3-sparc gate-else
2288
2729
  }
2289
2730
  }
2290
2731
  // ── Phase 4: ADRs + DDDs ──
@@ -2309,153 +2750,170 @@ export async function executeAutoChain(traceId, options = {}) {
2309
2750
  console.error(' [CLEANUP] Removing incomplete phase4 directory from prior run');
2310
2751
  fs.rmSync(phaseDir, { recursive: true, force: true });
2311
2752
  }
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);
2753
+ // ADR-PIPELINE-093 Phase 4 gate runs BEFORE the ADR-091 Ruflo-primary
2754
+ // call so the gate verifies (and regenerates) the SPARC spec upstream
2755
+ // requirement before the phase's own Ruflo pass runs.
2756
+ const phase4Gate = await runPhaseGate('phase4-adrs-ddd', scenarioQuery, traceId, runDir, gateAcc);
2757
+ if (!phase4Gate.ok && phase4Gate.stillMissing.length > 0) {
2758
+ phaseBlocked['phase4-adrs-ddd'] = true;
2759
+ console.error(' [SKIP] Phase 4 blocked by gate — upstream inputs unrecoverable');
2760
+ phases.push({ phase: 4, label: 'ADRs + DDDs', status: 'skipped', timing: Date.now() - phaseStart, artifacts: [], outputDir: phaseDir, error: `blocked: ${phase4Gate.stillMissing.join(', ')}` });
2761
+ // Continue to Phase 5a — its own gate decides reachability.
2340
2762
  }
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;
2763
+ else {
2764
+ // ADR-PIPELINE-091: Ruflo runs FIRST for ADR + DDD generation. Its output
2765
+ // lands in runDir/engineering/architecture-decisions.md + domain-model.md
2766
+ // and the phase4 coordinator refines it. Remote diligence agents feed in
2767
+ // as optional enrichment only.
2768
+ rufloPrimaryResults.phase4 = await runRufloPrimaryForPhase('phase4-adrs-ddd', scenarioQuery, traceId, runDir, collectPhase4Artifacts(runDir), readRemoteEnrichmentSnapshot(runDir), path.join(runDir, '.ruflo-cache', 'phase4-primary'));
2769
+ const agentResults = await dispatchPhaseAgents(PHASE_AGENTS[4], traceId, runDir, scenarioQuery);
2770
+ // Ruflo swarm + agentics agents: write ADRs + DDDs cooperatively
2771
+ const rufloP4Dir = path.join(runDir, '.ruflo-cache', 'phase4');
2772
+ const rufloResult = executeRufloPhaseSwarm({
2773
+ phase: 4, label: 'ADRs + DDDs', scenarioQuery,
2774
+ runDir, traceId, outputDir: rufloP4Dir,
2775
+ tasks: buildPhase4Tasks(scenarioQuery, collectPhase4Artifacts(runDir)),
2776
+ agenticsResults: agentResults,
2777
+ priorArtifacts: collectPhase4Artifacts(runDir),
2778
+ });
2779
+ if (rufloResult.filesModified > 0) {
2780
+ console.error(` [RUFLO-P4] ${rufloResult.filesModified} files generated (swarm+agents) in ${rufloResult.timing}ms`);
2366
2781
  }
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;
2782
+ // Persist agent results to .pre-phase4 so ADR/DDD generators can find them.
2783
+ // CANNOT write to phase4Dir — append-only guard throws if it exists.
2784
+ persistAgenticsResults(path.join(runDir, '.pre-phase4'), agentResults);
2785
+ try {
2786
+ const result = await executePhase4Command({ trace: traceId });
2787
+ mergeRufloCacheIntoPhase(runDir, 4);
2788
+ const timing = Date.now() - phaseStart;
2789
+ const artifactPaths = extractArtifactPaths(result.manifest.artifacts);
2790
+ console.error(formatPhase4ForDisplay(result));
2791
+ printArtifactLinks(phaseDir, artifactPaths);
2792
+ storePhaseArtifacts(PHASE_AGENTS[4], traceId, runDir, artifactPaths, timing);
2793
+ await reviewPhaseOutput(PHASE_AGENTS[4], traceId, runDir);
2794
+ persistAgenticsResults(phaseDir, agentResults);
2795
+ phases.push({ phase: 4, label: 'ADRs + DDDs', status: 'completed', timing, artifacts: artifactPaths, outputDir: phaseDir, agenticsAgents: buildAgentSummary(agentResults, PHASE_AGENTS[4].agenticsServices.length) });
2796
+ copyPlanningArtifacts(runDir, projectRoot);
2797
+ }
2798
+ catch (err) {
2799
+ const errMsg = err instanceof Error ? err.message : String(err);
2800
+ console.error(` [FAIL] Phase 4 failed: ${errMsg}`);
2801
+ recordPhaseFailure(PHASE_AGENTS[4], traceId, errMsg);
2802
+ // Phase 4 refines Phase 2 ADRs/DDD. If it fails, try recovery paths.
2803
+ const phase2AdrIndex = path.join(runDir, 'phase2', 'adrs', 'adr-index.json');
2804
+ let recovered = false;
2805
+ if (fs.existsSync(phase2AdrIndex)) {
2806
+ // Path A: Phase 2 has ADRs — copy them to phase4/
2807
+ console.error(' [RECOVER] Phase 2 ADRs/DDD exist — copying to phase4/ so pipeline can continue');
2808
+ for (const sub of ['adrs', 'ddd']) {
2809
+ const src = path.join(runDir, 'phase2', sub);
2810
+ const dest = path.join(phaseDir, sub);
2811
+ if (fs.existsSync(src)) {
2812
+ fs.mkdirSync(dest, { recursive: true });
2813
+ for (const file of fs.readdirSync(src)) {
2814
+ const srcFile = path.join(src, file);
2815
+ if (fs.statSync(srcFile).isFile()) {
2816
+ fs.copyFileSync(srcFile, path.join(dest, file));
2817
+ }
2382
2818
  }
2383
- catch { /* skip */ }
2384
2819
  }
2385
2820
  }
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;
2821
+ console.error(' [RECOVER] Phase 2 ADRs/DDD carried forward — continuing to Phase 5');
2822
+ recovered = true;
2823
+ }
2824
+ // Path B: No Phase 2 ADRs — generate them from SPARC + dossier directly
2825
+ if (!recovered) {
2826
+ console.error(' [RECOVER] No Phase 2 ADRs — generating ADRs from SPARC + dossier (no LLM required)');
2827
+ try {
2828
+ const { buildPhase2ADRs } = await import('../pipeline/phase2/phases/adr-generator.js');
2829
+ const { buildPhase2DDD } = await import('../pipeline/phase2/phases/ddd-generator.js');
2830
+ // Load SPARC and dossier from wherever they exist
2831
+ let sparc = null;
2832
+ let dossier = null;
2833
+ for (const sub of ['phase3/sparc/sparc-combined.json', 'phase2/sparc/sparc-combined.json']) {
2834
+ const p = path.join(runDir, sub);
2835
+ if (fs.existsSync(p)) {
2836
+ try {
2837
+ sparc = JSON.parse(fs.readFileSync(p, 'utf-8'));
2838
+ break;
2839
+ }
2840
+ catch { /* skip */ }
2392
2841
  }
2393
- catch { /* skip */ }
2394
2842
  }
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');
2843
+ for (const sub of ['phase2/research-dossier.json']) {
2844
+ const p = path.join(runDir, sub);
2845
+ if (fs.existsSync(p)) {
2846
+ try {
2847
+ dossier = JSON.parse(fs.readFileSync(p, 'utf-8'));
2848
+ break;
2849
+ }
2850
+ catch { /* skip */ }
2415
2851
  }
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
2852
  }
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');
2853
+ if (sparc && dossier) {
2854
+ // Generate ADRs
2855
+ const adrs = buildPhase2ADRs(sparc, dossier, scenarioQuery, true /* skipLLM */);
2856
+ if (adrs.length > 0) {
2857
+ const adrDir = path.join(phaseDir, 'adrs');
2858
+ fs.mkdirSync(adrDir, { recursive: true });
2859
+ for (const adr of adrs) {
2860
+ const slug = (adr.title || 'untitled').toLowerCase().replace(/[^a-z0-9]+/g, '-').slice(0, 60);
2861
+ const filename = `${adr.id}-${slug}.md`;
2862
+ const md = [
2863
+ `# ${adr.id}: ${adr.title}`,
2864
+ `\n**Status:** ${adr.status}`,
2865
+ `**Date:** ${adr.date}`,
2866
+ `\n## Context\n${adr.context}`,
2867
+ `\n## Decision\n${adr.decision}`,
2868
+ adr.alternatives?.length > 0 ? `\n## Alternatives Considered\n${adr.alternatives.map((a) => `- **${a.option}** ${a.rejected ? '(rejected)' : '(selected)'}: ${a.rationale}`).join('\n')}` : '',
2869
+ adr.consequences?.length > 0 ? `\n## Consequences\n${adr.consequences.map((c) => `- [${c.type === 'positive' ? '+' : c.type === 'negative' ? '-' : '~'}] ${c.description}`).join('\n')}` : '',
2870
+ ].filter(Boolean).join('\n');
2871
+ fs.writeFileSync(path.join(adrDir, filename), md + '\n', 'utf-8');
2429
2872
  }
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');
2873
+ fs.writeFileSync(path.join(adrDir, 'adr-index.json'), JSON.stringify(adrs, null, 2) + '\n', 'utf-8');
2874
+ console.error(` [RECOVER] Generated ${adrs.length} ADRs from SPARC template`);
2875
+ }
2876
+ // Generate DDD
2877
+ try {
2878
+ const dddModel = buildPhase2DDD(sparc, adrs, dossier);
2879
+ if (dddModel?.contexts?.length > 0) {
2880
+ const dddDir = path.join(phaseDir, 'ddd');
2881
+ const contextsDir = path.join(dddDir, 'contexts');
2882
+ fs.mkdirSync(contextsDir, { recursive: true });
2883
+ fs.writeFileSync(path.join(dddDir, 'domain-model.json'), JSON.stringify(dddModel, null, 2) + '\n', 'utf-8');
2884
+ if (dddModel.contextMap) {
2885
+ fs.writeFileSync(path.join(dddDir, 'context-map.json'), JSON.stringify(dddModel.contextMap, null, 2) + '\n', 'utf-8');
2886
+ }
2887
+ for (const ctx of dddModel.contexts) {
2888
+ const ctxSlug = ctx.name.toLowerCase().replace(/[^a-z0-9]+/g, '-').slice(0, 60);
2889
+ fs.writeFileSync(path.join(contextsDir, `${ctxSlug}.json`), JSON.stringify(ctx, null, 2) + '\n', 'utf-8');
2890
+ }
2891
+ console.error(` [RECOVER] Generated DDD model with ${dddModel.contexts.length} bounded contexts`);
2433
2892
  }
2434
- console.error(` [RECOVER] Generated DDD model with ${dddModel.contexts.length} bounded contexts`);
2435
2893
  }
2894
+ catch (dddErr) {
2895
+ console.error(` [WARN] DDD generation failed: ${dddErr instanceof Error ? dddErr.message : String(dddErr)}`);
2896
+ }
2897
+ recovered = true;
2436
2898
  }
2437
- catch (dddErr) {
2438
- console.error(` [WARN] DDD generation failed: ${dddErr instanceof Error ? dddErr.message : String(dddErr)}`);
2899
+ else {
2900
+ console.error(' [WARN] SPARC or dossier not found cannot generate ADRs');
2439
2901
  }
2440
- recovered = true;
2441
2902
  }
2442
- else {
2443
- console.error(' [WARN] SPARC or dossier not found cannot generate ADRs');
2903
+ catch (recoverErr) {
2904
+ console.error(` [WARN] ADR recovery failed: ${recoverErr instanceof Error ? recoverErr.message : String(recoverErr)}`);
2444
2905
  }
2445
2906
  }
2446
- catch (recoverErr) {
2447
- console.error(` [WARN] ADR recovery failed: ${recoverErr instanceof Error ? recoverErr.message : String(recoverErr)}`);
2907
+ if (recovered) {
2908
+ phases.push({ phase: 4, label: 'ADRs + DDDs', status: 'completed', timing: Date.now() - phaseStart, artifacts: [], outputDir: phaseDir, error: `Recovered: ${errMsg}` });
2448
2909
  }
2910
+ else {
2911
+ console.error(' [WARN] No recovery possible — continuing without ADRs');
2912
+ phases.push({ phase: 4, label: 'ADRs + DDDs', status: 'failed', timing: Date.now() - phaseStart, artifacts: [], outputDir: phaseDir, error: errMsg });
2913
+ }
2914
+ copyPlanningArtifacts(runDir, projectRoot);
2449
2915
  }
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
- }
2916
+ } // close ADR-PIPELINE-093 phase4-adrs-ddd gate-else
2459
2917
  }
2460
2918
  }
2461
2919
  // ── Phase 4.5: Generate implementation prompts by reading ADR + DDD files ──
@@ -2470,64 +2928,80 @@ export async function executeAutoChain(traceId, options = {}) {
2470
2928
  console.error(' [PROMPTS] Implementation prompts already exist — skipping');
2471
2929
  }
2472
2930
  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 => {
2931
+ // ADR-PIPELINE-093 Phase 5a gate. Verifies SPARC + (ADRs OR DDD OR
2932
+ // engineering/architecture-decisions.md) are present. Missing upstream
2933
+ // triggers the gate's single-shot Ruflo invocation of the owning phase.
2934
+ const phase5aGate = await runPhaseGate('phase5a-prompts', scenarioQuery, traceId, runDir, gateAcc);
2935
+ if (!phase5aGate.ok && phase5aGate.stillMissing.length > 0) {
2936
+ phaseBlocked['phase5a-prompts'] = true;
2937
+ console.error(' [SKIP] Phase 5a (prompts) blocked by gate — upstream unrecoverable');
2938
+ // Fall through: the FATAL-path check later will decide whether
2939
+ // to exit loudly or continue degraded.
2940
+ }
2941
+ else {
2942
+ // ADR-PIPELINE-091: Ruflo runs FIRST as the primary implementation-prompt
2943
+ // producer. Its output lands in runDir/engineering/implementation-roadmap.md
2944
+ // (and impl-NNN-*.md). The template-derivation path below only fires as
2945
+ // the last-resort when Ruflo was unavailable or produced no output.
2946
+ rufloPrimaryResults.phase5a = await runRufloPrimaryForPhase('phase5a-prompts', scenarioQuery, traceId, runDir, collectPhase4Artifacts(runDir), readRemoteEnrichmentSnapshot(runDir), path.join(runDir, '.ruflo-cache', 'phase5a-primary'));
2947
+ // Gather all ADR and DDD files (prefer phase4, fall back to phase2)
2948
+ const adrDir = fs.existsSync(path.join(runDir, 'phase4', 'adrs'))
2949
+ ? path.join(runDir, 'phase4', 'adrs')
2950
+ : fs.existsSync(path.join(runDir, 'phase2', 'adrs'))
2951
+ ? path.join(runDir, 'phase2', 'adrs')
2952
+ : null;
2953
+ const dddDir = fs.existsSync(path.join(runDir, 'phase4', 'ddd'))
2954
+ ? path.join(runDir, 'phase4', 'ddd')
2955
+ : fs.existsSync(path.join(runDir, 'phase2', 'ddd'))
2956
+ ? path.join(runDir, 'phase2', 'ddd')
2957
+ : null;
2958
+ const sparcDir = fs.existsSync(path.join(runDir, 'phase3', 'sparc'))
2959
+ ? path.join(runDir, 'phase3', 'sparc')
2960
+ : fs.existsSync(path.join(runDir, 'phase2', 'sparc'))
2961
+ ? path.join(runDir, 'phase2', 'sparc')
2962
+ : null;
2963
+ if (adrDir) {
2964
+ try {
2965
+ // Read all ADR markdown files
2966
+ const adrFiles = fs.readdirSync(adrDir).filter(f => f.endsWith('.md'));
2967
+ const adrContent = adrFiles.map(f => {
2506
2968
  try {
2507
- return `### ${f}\n\n${fs.readFileSync(path.join(dddDir, f), 'utf-8')}`;
2969
+ return `### ${f}\n\n${fs.readFileSync(path.join(adrDir, f), 'utf-8')}`;
2508
2970
  }
2509
2971
  catch {
2510
2972
  return '';
2511
2973
  }
2512
2974
  }).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)) {
2975
+ // Read DDD model files
2976
+ let dddContent = '';
2977
+ if (dddDir) {
2978
+ const dddFiles = fs.readdirSync(dddDir).filter(f => f.endsWith('.md') || f.endsWith('.json'));
2979
+ dddContent = dddFiles.map(f => {
2520
2980
  try {
2521
- sparcContent += `### ${candidate}\n\n${fs.readFileSync(p, 'utf-8').slice(0, 5000)}\n\n`;
2981
+ return `### ${f}\n\n${fs.readFileSync(path.join(dddDir, f), 'utf-8')}`;
2982
+ }
2983
+ catch {
2984
+ return '';
2985
+ }
2986
+ }).filter(Boolean).join('\n\n---\n\n');
2987
+ }
2988
+ // Read SPARC specification summary
2989
+ let sparcContent = '';
2990
+ if (sparcDir) {
2991
+ for (const candidate of ['specification.md', 'architecture.md', 'sparc-combined.json']) {
2992
+ const p = path.join(sparcDir, candidate);
2993
+ if (fs.existsSync(p)) {
2994
+ try {
2995
+ sparcContent += `### ${candidate}\n\n${fs.readFileSync(p, 'utf-8').slice(0, 5000)}\n\n`;
2996
+ }
2997
+ catch { /* skip */ }
2522
2998
  }
2523
- catch { /* skip */ }
2524
2999
  }
2525
3000
  }
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.
3001
+ // Build the ruflo swarm task: read the files, write implementation prompts
3002
+ fs.mkdirSync(promptsDir, { recursive: true, mode: 0o700 });
3003
+ const rufloPromptDir = path.join(runDir, '.ruflo-cache', 'prompts');
3004
+ 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
3005
 
2532
3006
  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
3007
 
@@ -2562,216 +3036,216 @@ ${sparcContent || 'No SPARC specification found.'}
2562
3036
  6. Write ALL files to the current working directory.
2563
3037
 
2564
3038
  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'));
3039
+ const promptSwarmResult = executeRufloPhaseSwarm({
3040
+ phase: 4,
3041
+ label: 'Implementation Prompts',
3042
+ scenarioQuery,
3043
+ runDir,
3044
+ traceId,
3045
+ outputDir: rufloPromptDir,
3046
+ tasks: [{
3047
+ label: 'Implementation Prompt Generation',
3048
+ description: promptTask,
3049
+ targetDir: '.',
3050
+ }],
3051
+ agenticsResults: [],
3052
+ priorArtifacts: collectPhase4Artifacts(runDir),
3053
+ timeoutMs: 600_000, // 10 min
3054
+ });
3055
+ // Copy ruflo output to prompts dir
3056
+ if (fs.existsSync(rufloPromptDir)) {
3057
+ const rufloFiles = fs.readdirSync(rufloPromptDir);
3058
+ let promptsCopied = 0;
3059
+ for (const f of rufloFiles) {
3060
+ const src = path.join(rufloPromptDir, f);
3061
+ if (fs.statSync(src).isFile()) {
3062
+ fs.copyFileSync(src, path.join(promptsDir, f));
3063
+ promptsCopied++;
3064
+ }
2606
3065
  }
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'));
3066
+ if (promptsCopied > 0) {
3067
+ console.error(` [PROMPTS] Ruflo swarm generated ${promptsCopied} implementation prompt files in ${promptSwarmResult.timing}ms`);
2614
3068
  }
2615
- catch { /* empty */ }
2616
3069
  }
2617
- if (!Array.isArray(adrs) || adrs.length === 0) {
3070
+ // Check if ruflo produced prompts; if not, write them directly from the gathered content
3071
+ const generatedPrompts = fs.existsSync(promptsDir)
3072
+ ? fs.readdirSync(promptsDir).filter(f => f.startsWith('impl-'))
3073
+ : [];
3074
+ if (generatedPrompts.length === 0) {
3075
+ // ── Read all source material for prompt generation ──
3076
+ const adrMarkdownByFile = new Map();
2618
3077
  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: [] });
3078
+ try {
3079
+ adrMarkdownByFile.set(f, fs.readFileSync(path.join(adrDir, f), 'utf-8'));
3080
+ }
3081
+ catch { /* skip */ }
2622
3082
  }
2623
- }
2624
- function getAdrMarkdown(adr) {
2625
- for (const [filename, content] of adrMarkdownByFile) {
2626
- if (filename.startsWith(adr.id))
2627
- return content;
3083
+ const adrIndexPath = path.join(adrDir, 'adr-index.json');
3084
+ let adrs = [];
3085
+ if (fs.existsSync(adrIndexPath)) {
3086
+ try {
3087
+ adrs = JSON.parse(fs.readFileSync(adrIndexPath, 'utf-8'));
3088
+ }
3089
+ catch { /* empty */ }
2628
3090
  }
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');
3091
+ if (!Array.isArray(adrs) || adrs.length === 0) {
3092
+ for (const f of adrFiles) {
3093
+ const content = adrMarkdownByFile.get(f) ?? '';
3094
+ const titleMatch = content.match(/^#\s+(.+)/m);
3095
+ adrs.push({ id: f.replace(/\.md$/, ''), title: titleMatch?.[1] ?? f, context: content.slice(0, 500), decision: '', consequences: [], alternatives: [] });
3096
+ }
2637
3097
  }
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;
3098
+ function getAdrMarkdown(adr) {
3099
+ for (const [filename, content] of adrMarkdownByFile) {
3100
+ if (filename.startsWith(adr.id))
3101
+ return content;
3102
+ }
3103
+ return '';
3104
+ }
3105
+ // Read DDD as reference material (not as driver)
3106
+ let dddContent = '';
3107
+ const dddModelPath = dddDir ? path.join(dddDir, 'domain-model.json') : '';
3108
+ if (dddModelPath && fs.existsSync(dddModelPath)) {
3109
+ try {
3110
+ dddContent = fs.readFileSync(dddModelPath, 'utf-8');
3111
+ }
3112
+ catch { /* skip */ }
3113
+ }
3114
+ // Also read DDD markdown if available
3115
+ if (dddDir) {
3116
+ for (const f of ['domain-model.md', 'ddd-model.md']) {
3117
+ const p = path.join(dddDir, f);
3118
+ if (fs.existsSync(p)) {
3119
+ try {
3120
+ dddContent += '\n\n' + fs.readFileSync(p, 'utf-8');
3121
+ break;
3122
+ }
3123
+ catch { /* skip */ }
2648
3124
  }
2649
- catch { /* skip */ }
2650
3125
  }
2651
3126
  }
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;
3127
+ // Read SPARC documents
3128
+ let sparcSpec = '';
3129
+ let sparcArch = '';
3130
+ if (sparcDir) {
3131
+ for (const [file, setter] of [
3132
+ ['specification.md', 'spec'], ['architecture.md', 'arch'],
3133
+ ]) {
3134
+ const p = path.join(sparcDir, file);
3135
+ if (fs.existsSync(p)) {
3136
+ try {
3137
+ const content = fs.readFileSync(p, 'utf-8');
3138
+ if (setter === 'spec')
3139
+ sparcSpec = content;
3140
+ else if (setter === 'arch')
3141
+ sparcArch = content;
3142
+ }
3143
+ catch { /* skip */ }
2668
3144
  }
2669
- catch { /* skip */ }
2670
3145
  }
2671
3146
  }
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 });
3147
+ console.error(' [PROMPTS] Ruflo swarm did not produce prompts — deriving from SPARC + ADRs');
3148
+ const derivedPhases = [];
3149
+ // Parse SPARC architecture for service/component sections
3150
+ if (sparcArch) {
3151
+ // Look for ## or ### headings that describe components/services
3152
+ const sections = sparcArch.split(/^(?=#{2,3}\s)/m).filter(s => s.trim().length > 20);
3153
+ for (const section of sections) {
3154
+ const headingMatch = section.match(/^#{2,3}\s+(.+)/);
3155
+ if (!headingMatch)
3156
+ continue;
3157
+ const heading = headingMatch[1].trim();
3158
+ // Skip meta-sections
3159
+ if (/^(overview|introduction|summary|table of contents|references|constraints|non-functional)/i.test(heading))
3160
+ continue;
3161
+ const slug = heading.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '').slice(0, 50);
3162
+ // Determine target folder based on content
3163
+ const sectionLower = section.toLowerCase();
3164
+ let folder = 'backend';
3165
+ if (/\bfrontend\b|\bui\b|\bdashboard\b|\breact\b|\bvue\b|\bangular\b/.test(sectionLower))
3166
+ folder = 'frontend';
3167
+ else if (/erp|netsuite|sap|dynamics|oracle/.test(sectionLower))
3168
+ folder = 'erp';
3169
+ else if (/integration|connector|webhook|external|third.party/.test(sectionLower))
3170
+ folder = 'integrations';
3171
+ else if (/api|service|backend|server|handler/.test(sectionLower))
3172
+ folder = 'backend';
3173
+ else if (/test|quality|coverage/.test(sectionLower))
3174
+ folder = 'tests';
3175
+ else if (/deploy|infra|docker|cloud|ci.cd/.test(sectionLower))
3176
+ folder = 'src';
3177
+ else if (/doc|guide|reference/.test(sectionLower))
3178
+ folder = 'docs';
3179
+ // Match ADRs to this section by keyword overlap
3180
+ const sectionWords = new Set(sectionLower.split(/\W+/).filter(w => w.length > 3));
3181
+ const matchedAdrIds = adrs.filter(adr => {
3182
+ const adrWords = `${adr.title} ${adr.context} ${adr.decision}`.toLowerCase().split(/\W+/).filter(w => w.length > 3);
3183
+ const overlap = adrWords.filter(w => sectionWords.has(w)).length;
3184
+ return overlap >= 2;
3185
+ }).map(a => a.id);
3186
+ derivedPhases.push({ title: heading, slug, content: section, folder, adrIds: matchedAdrIds });
3187
+ }
2713
3188
  }
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);
3189
+ // ADR-PIPELINE-031: Enrich with ADR-derived phases when SPARC sections alone are insufficient.
3190
+ // Each ADR that isn't already covered by a SPARC-derived phase becomes its own build step.
3191
+ if (derivedPhases.length < 8 && adrs.length > 0) {
3192
+ const existingSlugs = new Set(derivedPhases.map(p => p.slug));
3193
+ const existingTitleWords = new Set(derivedPhases.flatMap(p => p.title.toLowerCase().split(/\W+/).filter(w => w.length > 3)));
3194
+ for (const adr of adrs) {
3195
+ const slug = adr.title.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '').slice(0, 50);
3196
+ // Skip if a phase with similar slug or overlapping title already exists
3197
+ if (existingSlugs.has(slug))
3198
+ continue;
3199
+ const adrKeywords = adr.title.toLowerCase().split(/\W+/).filter(w => w.length > 3);
3200
+ const overlap = adrKeywords.filter(w => existingTitleWords.has(w)).length;
3201
+ if (overlap >= 2)
3202
+ continue; // sufficiently covered by an existing phase
3203
+ const adrLower = `${adr.title} ${adr.context} ${adr.decision}`.toLowerCase();
3204
+ let folder = 'backend';
3205
+ if (/frontend|ui|dashboard/.test(adrLower))
3206
+ folder = 'frontend';
3207
+ else if (/erp|netsuite|sap|dynamics|coupa|workday|maximo|oracle/.test(adrLower))
3208
+ folder = 'erp';
3209
+ else if (/integration|connector|webhook/.test(adrLower))
3210
+ folder = 'integrations';
3211
+ else if (/deploy|infra|docker/.test(adrLower))
3212
+ folder = 'src';
3213
+ else if (/test|quality/.test(adrLower))
3214
+ folder = 'tests';
3215
+ else if (/audit|governance|approval/.test(adrLower))
3216
+ folder = 'backend';
3217
+ derivedPhases.push({
3218
+ title: adr.title,
3219
+ slug,
3220
+ content: getAdrMarkdown(adr) || `**Context:** ${adr.context}\n\n**Decision:** ${adr.decision}`,
3221
+ folder,
3222
+ adrIds: [adr.id],
3223
+ });
3224
+ existingSlugs.add(slug);
3225
+ for (const w of adrKeywords)
3226
+ existingTitleWords.add(w);
3227
+ }
2753
3228
  }
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}
3229
+ // Always add foundation (first) and testing/deployment (last) if not present
3230
+ const hasTesting = derivedPhases.some(p => /test/i.test(p.title));
3231
+ const hasDeployment = derivedPhases.some(p => /deploy|infra/i.test(p.title));
3232
+ // Detect multi-language requirements (e.g., TypeScript + Rust)
3233
+ const queryLower = scenarioQuery.toLowerCase();
3234
+ const hasRust = /\brust\b/.test(queryLower);
3235
+ const hasTypeScript = /\btypescript\b/.test(queryLower);
3236
+ const rustPurpose = scenarioQuery.match(/rust\s+(?:used\s+)?(?:for\s+)?(.{10,100}?)(?:\.|,|$)/i)?.[1]?.trim() ?? '';
3237
+ const languageNote = hasRust && hasTypeScript
3238
+ ? `\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.`
3239
+ : hasRust
3240
+ ? `\n\nThis project uses Rust. Set up Cargo.toml workspace.`
3241
+ : '';
3242
+ // Insert foundation at the beginning — ADR-039: logger + config are FIRST outputs
3243
+ // Detect language from query to make scaffold language-appropriate
3244
+ 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';
3245
+ derivedPhases.unshift({
3246
+ title: 'Project Foundation & Core Types',
3247
+ slug: 'foundation',
3248
+ content: `Set up project structure, build tooling, and shared infrastructure using **${detectedLang}**. All subsequent phases import from this.${languageNote}
2775
3249
 
2776
3250
  ## FIRST: Create these shared modules (before domain types)
2777
3251
 
@@ -2840,32 +3314,32 @@ Create \`src/container.ts\` that wires all services via constructor injection:
2840
3314
 
2841
3315
  Base folder structure: src/, tests/, docs/. Create package.json, tsconfig.json, vitest.config.ts.
2842
3316
  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
3317
  folder: 'src',
2861
- adrIds: [],
3318
+ adrIds: adrs.map(a => a.id),
2862
3319
  });
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.
3320
+ if (!hasTesting) {
3321
+ derivedPhases.push({
3322
+ title: 'Testing & Quality Assurance',
3323
+ slug: 'testing',
3324
+ content: 'Write comprehensive tests for all components built in prior phases.',
3325
+ folder: 'tests',
3326
+ adrIds: [],
3327
+ });
3328
+ }
3329
+ if (!hasDeployment) {
3330
+ derivedPhases.push({
3331
+ title: 'Deployment & Infrastructure',
3332
+ slug: 'deployment',
3333
+ content: 'Create deployment configs, Dockerfiles, CI/CD, and infrastructure-as-code for the target cloud platform.',
3334
+ folder: 'src',
3335
+ adrIds: [],
3336
+ });
3337
+ }
3338
+ // ADR-PIPELINE-031: Guarantee ≥10 prompts by adding standard cross-cutting phases
3339
+ // when domain-specific + ADR-derived phases don't reach the minimum.
3340
+ const crossCutting = [
3341
+ { title: 'Observability & Structured Logging', slug: 'observability', folder: 'backend',
3342
+ content: `ADR-PIPELINE-034: Implement complete observability infrastructure.
2869
3343
 
2870
3344
  ## 1. Structured Logger (src/logger.ts)
2871
3345
 
@@ -2927,8 +3401,8 @@ If the project includes an API layer, add GET /health returning:
2927
3401
  - Test that logger outputs valid JSON to stderr
2928
3402
  - Test that correlationId flows through analysis → decision → ERP sync
2929
3403
  - 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.
3404
+ { title: 'Configuration & Environment Management', slug: 'configuration', folder: 'src',
3405
+ content: `ADR-PIPELINE-034: Configuration module for environment-based settings.
2932
3406
 
2933
3407
  Create src/config.ts with a typed configuration object:
2934
3408
 
@@ -2956,8 +3430,8 @@ Requirements:
2956
3430
  - Provide sensible defaults for optional values (timeoutMs, maxRetries, logLevel)
2957
3431
  - Export a singleton config object used by all services
2958
3432
  - 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.
3433
+ { title: 'API Layer & Request Handling', slug: 'api-layer', folder: 'backend',
3434
+ content: `ADR-PIPELINE-043: Thin route handlers with Zod validation.
2961
3435
 
2962
3436
  ARCHITECTURE: Route handlers MUST be thin:
2963
3437
  - Route: parse request → validate with Zod schema → call service function → format response
@@ -2981,10 +3455,10 @@ REQUIRED ENDPOINTS:
2981
3455
  - GET /api/metrics → Prometheus-format metrics (request count, latency histogram, error rate)
2982
3456
  - All domain endpoints with proper HTTP status codes and structured error responses
2983
3457
  - 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.
3458
+ { title: 'Persistence & Data Access Layer', slug: 'persistence', folder: 'backend',
3459
+ content: 'Implement repository interfaces per aggregate (ports pattern). Database adapter implementations. Audit trail persistence (append-only, tamper-evident). Transaction management for multi-aggregate operations.' },
3460
+ { title: 'Demo Script & CLI Entry Point', slug: 'demo', folder: 'src',
3461
+ content: `ADR-PIPELINE-037: User-facing entry points for the prototype.
2988
3462
 
2989
3463
  ## 1. Demo Script (src/demo.ts) — REQUIRED
2990
3464
 
@@ -3115,227 +3589,242 @@ services when the pilot validates the approach.
3115
3589
  ## Tests
3116
3590
  - Test that demo.ts runs without errors (import and execute main function)
3117
3591
  - 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)
3592
+ ];
3593
+ const existingSlugSet = new Set(derivedPhases.map(p => p.slug));
3594
+ for (const cc of crossCutting) {
3595
+ if (derivedPhases.length >= 12)
3596
+ break;
3597
+ if (existingSlugSet.has(cc.slug))
3598
+ continue;
3599
+ // Don't add if a phase already covers this topic
3600
+ if (derivedPhases.some(p => p.title.toLowerCase().includes(cc.slug.split('-')[0])))
3601
+ continue;
3602
+ derivedPhases.push({ ...cc, adrIds: [] });
3603
+ existingSlugSet.add(cc.slug);
3604
+ }
3605
+ console.error(` [PROMPTS] ${derivedPhases.length} phases derived (SPARC + ADRs + cross-cutting, minimum 10 per ADR-PIPELINE-031)`);
3606
+ // ── Helper: extract DDD context names and summaries relevant to a phase ──
3607
+ function getDddSummaryForPhase(phaseTitle, phaseContent) {
3608
+ if (!dddContent)
3140
3609
  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)
3610
+ try {
3611
+ const model = JSON.parse(dddContent);
3612
+ const contexts = model.contexts ?? [];
3613
+ if (contexts.length === 0)
3614
+ return '';
3615
+ // Exclude common words that would match everything
3616
+ const excludeWords = new Set(['domain', 'logic', 'crud', 'operations', 'manage', 'business', 'rules', 'event', 'handling', 'service', 'system', 'data', 'component', 'module', 'implementation', 'integration']);
3617
+ const phaseWords = new Set(`${phaseTitle} ${phaseContent}`.toLowerCase().split(/\W+/).filter((w) => w.length > 3 && !excludeWords.has(w)));
3618
+ const relevant = contexts.filter((c) => {
3619
+ const ctxText = `${c['name']}`.toLowerCase().split(/[-_\s]+/).filter((w) => w.length > 3 && !excludeWords.has(w));
3620
+ // Must match on context NAME keywords, not generic description words
3621
+ const matches = ctxText.filter((w) => phaseWords.has(w)).length;
3622
+ return matches >= 1;
3623
+ });
3624
+ if (relevant.length === 0)
3625
+ return '';
3626
+ const parts = [];
3627
+ for (const c of relevant) {
3628
+ const name = String(c['name'] ?? '');
3629
+ const desc = String(c['description'] ?? '');
3630
+ const aggs = (c['aggregates'] ?? []).map((a) => String(a['name'] ?? '')).filter(Boolean);
3631
+ const cmds = (c['commands'] ?? []).map((cmd) => typeof cmd === 'string' ? cmd : String(cmd['name'] ?? '')).filter(Boolean);
3632
+ const queries = (c['queries'] ?? []).map((q) => typeof q === 'string' ? q : String(q['name'] ?? '')).filter(Boolean);
3633
+ let summary = `**${name}**: ${desc}`;
3634
+ if (aggs.length > 0)
3635
+ summary += ` (aggregates: ${aggs.join(', ')})`;
3636
+ if (cmds.length > 0)
3637
+ summary += ` (commands: ${cmds.join(', ')})`;
3638
+ if (queries.length > 0)
3639
+ summary += ` (queries: ${queries.join(', ')})`;
3640
+ parts.push(summary);
3641
+ }
3642
+ return parts.join('\n\n');
3643
+ }
3644
+ catch {
3151
3645
  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
3646
  }
3168
- return parts.join('\n\n');
3169
- }
3170
- catch {
3171
- return '';
3172
- }
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
3647
  }
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('; ')}.`);
3648
+ // ── Helper: build a narrative description paragraph for a phase ──
3649
+ function buildPhaseNarrative(phase, phaseAdrs, dddSummary, completedList) {
3650
+ const parts = [];
3651
+ // Opening: what this phase builds
3652
+ parts.push(`In this phase, you are building the **${phase.title}** component of the platform.`);
3653
+ // What prior phases provide
3654
+ if (completedList.length > 0) {
3655
+ parts.push(`This builds on ${completedList.length} previously completed phase(s) import shared types, interfaces, and services from those modules.`);
3194
3656
  }
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, '', '---', '');
3657
+ // ADR narrative: which decisions govern this phase and what they decided
3658
+ if (phaseAdrs.length > 0) {
3659
+ const adrSentences = phaseAdrs.map(a => {
3660
+ const decision = a.decision?.split('.')[0] ?? a.title;
3661
+ return `${a.id} ("${a.title}") which decided: ${decision}`;
3662
+ });
3663
+ if (adrSentences.length === 1) {
3664
+ parts.push(`This phase is governed by architecture decision ${adrSentences[0]}.`);
3265
3665
  }
3266
3666
  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('');
3667
+ parts.push(`This phase is governed by ${adrSentences.length} architecture decisions: ${adrSentences.join('; ')}.`);
3278
3668
  }
3279
3669
  }
3670
+ // DDD narrative: which domain concepts are relevant
3671
+ if (dddSummary) {
3672
+ parts.push(`The domain model defines the following relevant contexts and entities for this phase:\n\n${dddSummary}`);
3673
+ }
3674
+ return parts.join(' ');
3280
3675
  }
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}\``);
3676
+ const totalSteps = derivedPhases.length;
3677
+ const completedPhases = [];
3678
+ for (let i = 0; i < totalSteps; i++) {
3679
+ const phase = derivedPhases[i];
3680
+ const order = i + 1;
3681
+ const filename = `impl-${String(order).padStart(3, '0')}-${phase.slug}.md`;
3682
+ // Resolve ADRs for this phase
3683
+ const phaseAdrs = phase.adrIds.length > 0
3684
+ ? adrs.filter(a => phase.adrIds.includes(a.id))
3685
+ : (order === 1 ? adrs : []);
3686
+ // Resolve DDD summary for this phase
3687
+ const dddSummary = getDddSummaryForPhase(phase.title, phase.content);
3688
+ // Build the narrative paragraph
3689
+ const narrative = buildPhaseNarrative(phase, phaseAdrs, dddSummary, completedPhases);
3690
+ const lines = [
3691
+ `# Implementation Prompt ${order} of ${totalSteps}: ${phase.title}`,
3692
+ '',
3693
+ `**Target folder:** \`${phase.folder}/\``,
3694
+ '',
3695
+ ];
3696
+ // ADR-PIPELINE-033: Simulation lineage in every prompt
3697
+ if (simulationId || traceId) {
3698
+ lines.push('## Simulation Lineage', '');
3699
+ if (simulationId)
3700
+ lines.push(`Originating simulation: \`${simulationId}\``);
3701
+ lines.push(`Trace ID: \`${traceId}\``);
3702
+ lines.push('');
3703
+ }
3704
+ // ── Narrative description — the core of each prompt ──
3705
+ lines.push('## Overview', '', narrative, '');
3706
+ // Previously completed phases (brief list)
3707
+ if (completedPhases.length > 0) {
3708
+ lines.push('## Previously Completed (available for import)', '');
3709
+ for (const prev of completedPhases)
3710
+ lines.push(`- ${prev}`);
3711
+ lines.push('');
3712
+ }
3713
+ // Full project requirements on first prompt only
3714
+ if (order === 1) {
3715
+ lines.push('## Project Requirements', '', scenarioQuery, '');
3716
+ }
3717
+ // SPARC-derived content for this phase (the actual architecture section)
3718
+ if (phase.content && phase.content.length > 50) {
3719
+ lines.push('## SPARC Architecture for This Phase', '', phase.content, '');
3720
+ }
3721
+ // SPARC spec + architecture on first two prompts for full context
3722
+ if (order === 1 && sparcSpec) {
3723
+ lines.push('## Full SPARC Specification', '', sparcSpec, '');
3724
+ }
3725
+ if (order <= 2 && sparcArch && phase.content !== sparcArch) {
3726
+ lines.push('## Full SPARC Architecture', '', sparcArch, '');
3727
+ }
3728
+ // ADR details — full markdown with file paths for each relevant ADR
3729
+ if (phaseAdrs.length > 0) {
3730
+ lines.push(`## Architecture Decisions (${phaseAdrs.length} ADRs)`, '');
3731
+ lines.push('ADR files are in `.agentics/plans/adrs/` — read them for full context:', '');
3732
+ for (const adr of phaseAdrs) {
3733
+ const slug = (adr.title || 'untitled').toLowerCase().replace(/[^a-z0-9]+/g, '-').slice(0, 60);
3734
+ const adrFilename = `${adr.id}-${slug}.md`;
3735
+ lines.push(`**File:** \`.agentics/plans/adrs/${adrFilename}\``, '');
3736
+ const fullMarkdown = getAdrMarkdown(adr);
3737
+ if (fullMarkdown) {
3738
+ lines.push(fullMarkdown, '', '---', '');
3739
+ }
3740
+ else {
3741
+ lines.push(`### ${adr.id}: ${adr.title}`, '');
3742
+ if (adr.context)
3743
+ lines.push(`**Context:** ${adr.context}`, '');
3744
+ if (adr.decision)
3745
+ lines.push(`**Decision:** ${adr.decision}`, '');
3746
+ if (adr.consequences?.length) {
3747
+ lines.push('**Consequences:**');
3748
+ for (const c of adr.consequences)
3749
+ lines.push(`- ${c}`);
3750
+ }
3751
+ lines.push('');
3290
3752
  }
3291
- lines.push('');
3292
3753
  }
3293
3754
  }
3294
- catch { /* non-fatal */ }
3295
- }
3296
- // DDD reference — structured summary, not raw JSON dump
3297
- if (dddSummary) {
3298
- lines.push('## Domain Model Reference', '', dddSummary, '');
3755
+ // List ALL ADR files for reference even if not directly linked to this phase
3756
+ if (adrDir && order === 1) {
3757
+ try {
3758
+ const allAdrFiles = fs.readdirSync(adrDir).filter(f => f.endsWith('.md')).sort();
3759
+ if (allAdrFiles.length > 0) {
3760
+ lines.push('## All Architecture Decision Records', '');
3761
+ lines.push('Read these files in `.agentics/plans/adrs/` for complete design rationale:', '');
3762
+ for (const f of allAdrFiles) {
3763
+ lines.push(`- \`.agentics/plans/adrs/${f}\``);
3764
+ }
3765
+ lines.push('');
3766
+ }
3767
+ }
3768
+ catch { /* non-fatal */ }
3769
+ }
3770
+ // DDD reference — structured summary, not raw JSON dump
3771
+ if (dddSummary) {
3772
+ lines.push('## Domain Model Reference', '', dddSummary, '');
3773
+ }
3774
+ // Implementation instructions — ADR-PIPELINE-033: include traceability requirements
3775
+ 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"', '');
3776
+ // ADR-PIPELINE-039: Mandatory cross-cutting requirements in EVERY prompt
3777
+ 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', '');
3778
+ fs.writeFileSync(path.join(promptsDir, filename), lines.join('\n'), { mode: 0o600, encoding: 'utf-8' });
3779
+ completedPhases.push(`${order}. ${phase.title}`);
3299
3780
  }
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}`);
3781
+ const planItems = derivedPhases.map((phase, i) => ({
3782
+ order: i + 1,
3783
+ file: `impl-${String(i + 1).padStart(3, '0')}-${phase.slug}.md`,
3784
+ title: phase.title,
3785
+ folder: phase.folder,
3786
+ adrs: phase.adrIds,
3787
+ }));
3788
+ // ADR-PIPELINE-033: Include simulation lineage in execution plan
3789
+ const plan = {
3790
+ totalSteps,
3791
+ prompts: planItems,
3792
+ lineage: {
3793
+ simulationId: simulationId || undefined,
3794
+ traceId: traceId || undefined,
3795
+ pipelineVersion: '1.7.3',
3796
+ },
3797
+ };
3798
+ fs.writeFileSync(path.join(promptsDir, 'execution-plan.json'), JSON.stringify(plan, null, 2), { mode: 0o600, encoding: 'utf-8' });
3799
+ console.error(` [PROMPTS] Wrote ${totalSteps} implementation prompts derived from SPARC architecture + ADRs`);
3800
+ }
3801
+ // ADR-PIPELINE-091 §5: record the execution block on manifest.json
3802
+ // BEFORE copyPlanningArtifacts propagates the manifest into the
3803
+ // project tree, so downstream readers see the engineering tier +
3804
+ // enrichment depth in the copied artifact.
3805
+ try {
3806
+ const execBlock = computeExecutionBlock([rufloPrimaryResults.phase3, rufloPrimaryResults.phase4, rufloPrimaryResults.phase5a], readRemoteEnrichmentSnapshot(runDir));
3807
+ writeExecutionBlockToManifest(runDir, execBlock);
3808
+ process.stderr.write(` [ADR-091] engineering_tier=${execBlock.engineering_tier} ` +
3809
+ `tier_counts=ruflo-local:${execBlock.tier_counts['ruflo-local']}/template:${execBlock.tier_counts.template} ` +
3810
+ `enrichment_depth_pct=${execBlock.enrichment.enrichment_depth_pct ?? 'null'}\n`);
3811
+ }
3812
+ catch (err) {
3813
+ process.stderr.write(` [ADR-091] execution block write failed: ${err instanceof Error ? err.message : String(err)}\n`);
3306
3814
  }
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`);
3815
+ copyPlanningArtifacts(runDir, projectRoot);
3816
+ }
3817
+ catch (err) {
3818
+ const errMsg = err instanceof Error ? err.message : String(err);
3819
+ console.error(` [PROMPTS] Implementation prompt generation failed: ${errMsg}`);
3326
3820
  }
3327
- copyPlanningArtifacts(runDir, projectRoot);
3328
3821
  }
3329
- catch (err) {
3330
- const errMsg = err instanceof Error ? err.message : String(err);
3331
- console.error(` [PROMPTS] Implementation prompt generation failed: ${errMsg}`);
3822
+ else {
3823
+ console.error(' [PROMPTS] Skipped no ADR directory found');
3824
+ console.error(` Checked: ${path.join(runDir, 'phase4', 'adrs')} (${fs.existsSync(path.join(runDir, 'phase4', 'adrs')) ? 'exists' : 'MISSING'})`);
3825
+ console.error(` Checked: ${path.join(runDir, 'phase2', 'adrs')} (${fs.existsSync(path.join(runDir, 'phase2', 'adrs')) ? 'exists' : 'MISSING'})`);
3332
3826
  }
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
- }
3827
+ } // close ADR-PIPELINE-093 phase5a-prompts gate-else
3339
3828
  }
3340
3829
  }
3341
3830
  // ── Pre-Phase 5: Detect implementation language from prior artifacts ──
@@ -3452,6 +3941,10 @@ services when the pilot validates the approach.
3452
3941
  const phase5ManifestPath = path.join(phaseDir, 'phase5-manifest.json');
3453
3942
  if (!fs.existsSync(phase5ManifestPath)) {
3454
3943
  await ensureAdrsExist(runDir, traceId, scenarioQuery);
3944
+ // ADR-094 Decision 4: when Phase 6 cannot attempt degraded mode,
3945
+ // emit skipped-due-to-upstream entry so Phase 6 stays visible in
3946
+ // chainResult.phases — the trace contract that pre-094 dropped.
3947
+ pushSkippedDueToUpstream(phases, { phase: 5, label: 'Build', reason: errMsg }, runDir);
3455
3948
  return buildResult(traceId, runDir, phases, pipelineStart, mode);
3456
3949
  }
3457
3950
  console.error(' [INFO] Phase 5 manifest written — Phase 6 will attempt degraded mode.');
@@ -3543,12 +4036,104 @@ services when the pilot validates the approach.
3543
4036
  }
3544
4037
  // ADR-028: Final ADR guarantee before returning
3545
4038
  await ensureAdrsExist(runDir, traceId, scenarioQuery);
4039
+ // ADR-PIPELINE-093 — Rule 5 scaffold-skip carry-over.
4040
+ // `copyPlanningArtifacts` may have persisted a `phase5b_skipped` block
4041
+ // into manifest.json. Re-read it so `writeGateBlockToManifest` below
4042
+ // preserves it when it merges in blocked_phases + inputs_produced_by_gate.
4043
+ try {
4044
+ const phase1ManifestPath = path.join(runDir, 'manifest.json');
4045
+ if (fs.existsSync(phase1ManifestPath)) {
4046
+ try {
4047
+ const mf = JSON.parse(fs.readFileSync(phase1ManifestPath, 'utf-8'));
4048
+ const skip = mf['phase5b_skipped'];
4049
+ if (skip && typeof skip === 'object') {
4050
+ const s = skip;
4051
+ if (typeof s['reason'] === 'string' && typeof s['detected'] === 'string' && typeof s['template'] === 'string') {
4052
+ gateAcc.phase5bSkipped = { reason: s['reason'], detected: s['detected'], template: s['template'] };
4053
+ }
4054
+ }
4055
+ }
4056
+ catch { /* best-effort */ }
4057
+ }
4058
+ }
4059
+ catch { /* best-effort */ }
4060
+ // ADR-PIPELINE-093 — Flush gate block into manifest.json BEFORE the
4061
+ // FATAL-path decision so the preserved run directory carries the
4062
+ // blocked_phases / inputs_produced_by_gate / phase5b_skipped diagnostics.
4063
+ try {
4064
+ // refresh blocked list from accumulator (determineBlockedPhases mirrors
4065
+ // what recordGateResult did; we use it as a sanity check)
4066
+ const blockedFromAcc = determineBlockedPhases(gateAcc.blocked.map(b => ({ phaseId: b.phase, gateResult: { ok: false, missing: [], ruflo_invoked: [], stillMissing: [...b.missing] } })));
4067
+ void blockedFromAcc; // type-guard: same shape used by determineBlockedPhases
4068
+ writeGateBlockToManifest(runDir, gateAcc);
4069
+ // ADR-PIPELINE-093 §Rule 3 surface (a): same payload into status.json
4070
+ // so the CLI `status` command / MCP `agentics-status` tool expose
4071
+ // `blocked_phases` + per-phase `inputs_produced_by_gate` arrays.
4072
+ writeGateBlockToStatusJson(runDir, gateAcc);
4073
+ }
4074
+ catch { /* best-effort */ }
4075
+ // ADR-PIPELINE-093 Rule 4 — Mandatory Phase 5a emission. The prompt-generator
4076
+ // MUST produce a valid `execution-plan.json` with ≥1 implementations[] entry
4077
+ // and at least one impl-NNN-*.md. If either is missing at this point — even
4078
+ // after all gate-triggered Ruflo invocations — the pipeline exits FATAL
4079
+ // rather than silently returning degraded output.
4080
+ {
4081
+ const promotedPromptsDir = path.join(projectRoot, '.agentics', 'plans', 'prompts');
4082
+ const executionPlanPath = path.join(promotedPromptsDir, 'execution-plan.json');
4083
+ let hasValidPlan = false;
4084
+ try {
4085
+ if (fs.existsSync(executionPlanPath)) {
4086
+ const parsed = JSON.parse(fs.readFileSync(executionPlanPath, 'utf-8'));
4087
+ const implCount = Array.isArray(parsed.implementations)
4088
+ ? parsed.implementations.length
4089
+ : Array.isArray(parsed.prompts)
4090
+ ? parsed.prompts.length
4091
+ : 0;
4092
+ hasValidPlan = implCount >= 1;
4093
+ }
4094
+ }
4095
+ catch {
4096
+ hasValidPlan = false;
4097
+ }
4098
+ let implFileCount = 0;
4099
+ try {
4100
+ if (fs.existsSync(promotedPromptsDir)) {
4101
+ implFileCount = fs.readdirSync(promotedPromptsDir).filter(f => /^impl-\d+-.+\.md$/.test(f)).length;
4102
+ }
4103
+ }
4104
+ catch {
4105
+ implFileCount = 0;
4106
+ }
4107
+ if (!hasValidPlan || implFileCount < 1) {
4108
+ const missingList = [];
4109
+ if (!hasValidPlan)
4110
+ missingList.push('execution-plan.json');
4111
+ if (implFileCount < 1)
4112
+ missingList.push('impl-NNN-*.md');
4113
+ // Final banner — the one place ADR-093 allows a loud exit.
4114
+ process.stderr.write(`[PIPELINE-093] FATAL: prompt-generator blocked — upstream artifacts unrecoverable\n`);
4115
+ process.stderr.write(` missing: ${missingList.join(', ')}\n`);
4116
+ process.stderr.write(` attempted: Ruflo invocation for phase 4 (failed or timed out)\n`);
4117
+ process.stderr.write(` run directory preserved: ${runDir}\n`);
4118
+ // Best-effort shutdown so file handles + metrics flush before exit.
4119
+ try {
4120
+ const totalTiming = Date.now() - pipelineStart;
4121
+ shutdownSwarm(traceId, false, totalTiming);
4122
+ }
4123
+ catch { /* best-effort */ }
4124
+ process.exit(1);
4125
+ }
4126
+ }
3546
4127
  // ── Shutdown swarm and persist metrics ──
3547
4128
  const totalTiming = Date.now() - pipelineStart;
3548
- const pipelineSuccess = phases.every(p => p.status === 'completed' || p.status === 'skipped');
4129
+ // ADR-094 Decision 4: skipped-due-to-upstream is non-failure for success calc.
4130
+ const pipelineSuccess = phases.every(p => p.status === 'completed' || p.status === 'skipped' || p.status === 'skipped-due-to-upstream');
3549
4131
  shutdownSwarm(traceId, pipelineSuccess, totalTiming);
3550
4132
  // ── Final Summary ──
3551
- const result = buildResult(traceId, runDir, phases, pipelineStart, mode, scenarioBranch, commitHash, execCtx.remoteUrl);
4133
+ const result = buildResult(traceId, runDir, phases, pipelineStart, mode, scenarioBranch, commitHash, execCtx.remoteUrl,
4134
+ // ADR-PIPELINE-093 §Rule 3 (c): blocked phases threaded into the
4135
+ // result so the final `agentics ask` banner can warn the user.
4136
+ gateAcc.blocked.map(b => ({ phase: b.phase, missing: [...b.missing] })));
3552
4137
  printFinalSummary(result);
3553
4138
  return result;
3554
4139
  }
@@ -3671,7 +4256,12 @@ function printFinalSummary(result) {
3671
4256
  console.error(' Phase Summary:');
3672
4257
  console.error(' ' + '-'.repeat(60));
3673
4258
  for (const phase of result.phases) {
3674
- const icon = phase.status === 'completed' ? '[OK]' : phase.status === 'skipped' ? '[--]' : '[!!]';
4259
+ // ADR-094 Decision 4: distinct icon for skipped-due-to-upstream so the
4260
+ // user can see which phases were propagation-skipped vs. opted-out.
4261
+ const icon = phase.status === 'completed' ? '[OK]'
4262
+ : phase.status === 'skipped' ? '[--]'
4263
+ : phase.status === 'skipped-due-to-upstream' ? '[<-]'
4264
+ : '[!!]';
3675
4265
  const timingStr = phase.timing > 0 ? `${(phase.timing / 1000).toFixed(1)}s` : 'cached';
3676
4266
  const agentStr = phase.agenticsAgents
3677
4267
  ? ` agents: ${phase.agenticsAgents.responded}/${phase.agenticsAgents.total}`
@@ -3791,6 +4381,386 @@ function persistAgenticsResults(phaseDir, results) {
3791
4381
  // Non-fatal — agent result persistence should never block the pipeline
3792
4382
  }
3793
4383
  }
4384
+ /**
4385
+ * Read `manifest.agents_invoked` from the run dir and partition into
4386
+ * successful vs errored remote enrichment agents. ADR-PIPELINE-091 §5.
4387
+ *
4388
+ * Treats `status === "success"` and `status === "invoked"` as available; any
4389
+ * other status (including HTTP error codes, `"timeout"`, `"error"`) is
4390
+ * counted as errored. Missing / unreadable manifest => empty lists.
4391
+ */
4392
+ export function readRemoteEnrichmentSnapshot(runDir) {
4393
+ const manifestPath = path.join(runDir, 'manifest.json');
4394
+ if (!fs.existsSync(manifestPath)) {
4395
+ return { available: [], errored: [], entries: [] };
4396
+ }
4397
+ let entries = [];
4398
+ try {
4399
+ const raw = fs.readFileSync(manifestPath, 'utf-8');
4400
+ const manifest = JSON.parse(raw);
4401
+ entries = Array.isArray(manifest.agents_invoked) ? manifest.agents_invoked : [];
4402
+ }
4403
+ catch {
4404
+ return { available: [], errored: [], entries: [] };
4405
+ }
4406
+ const available = [];
4407
+ const errored = [];
4408
+ for (const entry of entries) {
4409
+ if (!entry || typeof entry.agent !== 'string')
4410
+ continue;
4411
+ const isOk = entry.status === 'success' || entry.status === 'invoked';
4412
+ (isOk ? available : errored).push(entry.agent);
4413
+ }
4414
+ return { available, errored, entries };
4415
+ }
4416
+ /**
4417
+ * Resolve a per-phase execution tier from a Ruflo primary-executor result.
4418
+ * A phase is tagged `"ruflo-local"` iff Ruflo actually succeeded. Anything
4419
+ * else (unavailable, timeout, swarm error, missing result) => `"template"`.
4420
+ */
4421
+ function phaseTierFromRufloResult(result) {
4422
+ if (!result)
4423
+ return 'template';
4424
+ if (result.executionTier === 'ruflo-local' && result.success)
4425
+ return 'ruflo-local';
4426
+ return 'template';
4427
+ }
4428
+ /**
4429
+ * Build the `execution` block that ADR-PIPELINE-091 §5 writes back into
4430
+ * `runDir/manifest.json`. Pure function so it can be unit-tested without
4431
+ * spinning up the actual pipeline.
4432
+ */
4433
+ export function computeExecutionBlock(rufloResults, enrichment) {
4434
+ const perPhaseTiers = rufloResults.map(phaseTierFromRufloResult);
4435
+ const rufloCount = perPhaseTiers.filter(t => t === 'ruflo-local').length;
4436
+ const templateCount = perPhaseTiers.filter(t => t === 'template').length;
4437
+ const engineering_tier = rufloCount > 0 ? 'ruflo-local' : 'template';
4438
+ const totalRemote = enrichment.available.length + enrichment.errored.length;
4439
+ const enrichment_depth_pct = totalRemote === 0
4440
+ ? null
4441
+ : Math.round((enrichment.available.length / totalRemote) * 100);
4442
+ return {
4443
+ engineering_tier,
4444
+ tier_counts: { 'ruflo-local': rufloCount, template: templateCount },
4445
+ enrichment: {
4446
+ remote_agents_available: [...enrichment.available],
4447
+ remote_agents_errored: [...enrichment.errored],
4448
+ enrichment_depth_pct,
4449
+ },
4450
+ };
4451
+ }
4452
+ /**
4453
+ * Merge the ADR-091 `execution` block into `runDir/manifest.json` in-place.
4454
+ * Reads the existing manifest, sets `execution`, writes it back with the
4455
+ * same mode/encoding convention used elsewhere in this file. Non-fatal on
4456
+ * I/O or JSON errors — the pipeline continues if manifest merge fails.
4457
+ */
4458
+ export function writeExecutionBlockToManifest(runDir, block) {
4459
+ const manifestPath = path.join(runDir, 'manifest.json');
4460
+ try {
4461
+ let manifest = {};
4462
+ if (fs.existsSync(manifestPath)) {
4463
+ try {
4464
+ manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf-8'));
4465
+ }
4466
+ catch {
4467
+ manifest = {};
4468
+ }
4469
+ }
4470
+ manifest['execution'] = block;
4471
+ fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2), { mode: 0o600, encoding: 'utf-8' });
4472
+ }
4473
+ catch (err) {
4474
+ process.stderr.write(` [ADR-091] Failed to merge execution block into manifest.json: ${err instanceof Error ? err.message : String(err)}\n`);
4475
+ }
4476
+ }
4477
+ function newGateAccumulator() {
4478
+ return {
4479
+ blocked: [],
4480
+ producedByGate: {
4481
+ phase2: [],
4482
+ 'phase3-sparc': [],
4483
+ 'phase4-adrs-ddd': [],
4484
+ 'phase5a-prompts': [],
4485
+ 'phase5b-scaffold': [],
4486
+ },
4487
+ phase5bSkipped: null,
4488
+ };
4489
+ }
4490
+ /**
4491
+ * Record a gate result into the accumulator. `inputs_produced_by_gate`
4492
+ * reflects the initially-missing paths that became present after Ruflo
4493
+ * ran (i.e. `missing` minus `stillMissing`). A stable shape is important
4494
+ * for downstream readers even when the gate passed on first check.
4495
+ */
4496
+ function recordGateResult(acc, phaseId, result) {
4497
+ if (!result.ok && result.stillMissing.length > 0) {
4498
+ acc.blocked.push({ phase: phaseId, missing: result.stillMissing });
4499
+ }
4500
+ if (result.ruflo_invoked.length > 0) {
4501
+ const stillMissingSet = new Set(result.stillMissing);
4502
+ const produced = result.missing.filter(p => !stillMissingSet.has(p));
4503
+ if (produced.length > 0) {
4504
+ acc.producedByGate[phaseId] = [...acc.producedByGate[phaseId], ...produced];
4505
+ }
4506
+ }
4507
+ }
4508
+ /**
4509
+ * ADR-PIPELINE-093 — merge `blocked_phases`, per-phase
4510
+ * `phase{N}_inputs_produced_by_gate`, and `phase5b_skipped` into
4511
+ * `runDir/manifest.json`. Read-modify-write pattern mirrors
4512
+ * `writeExecutionBlockToManifest` / `writePlanPromotionToManifest`.
4513
+ *
4514
+ * `blocked_phases` is always present (empty array when no phase blocked).
4515
+ * The per-phase `inputs_produced_by_gate` keys are always present so
4516
+ * downstream readers can treat them as a stable schema.
4517
+ */
4518
+ export function writeGateBlockToManifest(runDir, acc) {
4519
+ const manifestPath = path.join(runDir, 'manifest.json');
4520
+ try {
4521
+ let manifest = {};
4522
+ if (fs.existsSync(manifestPath)) {
4523
+ try {
4524
+ manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf-8'));
4525
+ }
4526
+ catch {
4527
+ manifest = {};
4528
+ }
4529
+ }
4530
+ manifest['blocked_phases'] = acc.blocked.map(b => ({ phase: b.phase, missing: [...b.missing] }));
4531
+ manifest['phase2_inputs_produced_by_gate'] = [...acc.producedByGate['phase2']];
4532
+ manifest['phase3_inputs_produced_by_gate'] = [...acc.producedByGate['phase3-sparc']];
4533
+ manifest['phase4_inputs_produced_by_gate'] = [...acc.producedByGate['phase4-adrs-ddd']];
4534
+ manifest['phase5a_inputs_produced_by_gate'] = [...acc.producedByGate['phase5a-prompts']];
4535
+ if (acc.phase5bSkipped) {
4536
+ manifest['phase5b_skipped'] = { ...acc.phase5bSkipped };
4537
+ }
4538
+ fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2), { mode: 0o600, encoding: 'utf-8' });
4539
+ }
4540
+ catch (err) {
4541
+ process.stderr.write(` [PIPELINE-093] Failed to merge gate block into manifest.json: ${err instanceof Error ? err.message : String(err)}\n`);
4542
+ }
4543
+ }
4544
+ /**
4545
+ * ADR-PIPELINE-093 §Rule 3 (surface b) — same gate-block payload, written
4546
+ * into `runDir/status.json` so the CLI `status` command + MCP
4547
+ * `agentics-status` tool carry `blocked_phases` and the per-phase
4548
+ * `phase{N}_inputs_produced_by_gate` arrays alongside the ADR-088 keys.
4549
+ *
4550
+ * Read-modify-write pattern matches the CLI's local `updateStatus` helper
4551
+ * (`src/cli/index.ts`:~2501). Best-effort: if `status.json` does not exist
4552
+ * (e.g. running outside the MCP fast-return path), the file is created so
4553
+ * the gate block is always observable regardless of entry point.
4554
+ *
4555
+ * Both `writeGateBlockToManifest` and this function should be called from
4556
+ * the same accumulator snapshot so manifest.json and status.json stay in
4557
+ * sync. The CLI-surfaced `status.json` is the source of truth for the MCP
4558
+ * `agentics-status` tool (which shells out to `agentics status`).
4559
+ */
4560
+ export function writeGateBlockToStatusJson(runDir, acc) {
4561
+ const statusPath = path.join(runDir, 'status.json');
4562
+ try {
4563
+ let status = {};
4564
+ if (fs.existsSync(statusPath)) {
4565
+ try {
4566
+ status = JSON.parse(fs.readFileSync(statusPath, 'utf-8'));
4567
+ }
4568
+ catch {
4569
+ status = {};
4570
+ }
4571
+ }
4572
+ status['blocked_phases'] = acc.blocked.map(b => ({ phase: b.phase, missing: [...b.missing] }));
4573
+ status['phase2_inputs_produced_by_gate'] = [...acc.producedByGate['phase2']];
4574
+ status['phase3_inputs_produced_by_gate'] = [...acc.producedByGate['phase3-sparc']];
4575
+ status['phase4_inputs_produced_by_gate'] = [...acc.producedByGate['phase4-adrs-ddd']];
4576
+ status['phase5a_inputs_produced_by_gate'] = [...acc.producedByGate['phase5a-prompts']];
4577
+ if (acc.phase5bSkipped) {
4578
+ status['phase5b_skipped'] = { ...acc.phase5bSkipped };
4579
+ }
4580
+ status['updatedAt'] = new Date().toISOString();
4581
+ const tmp = statusPath + '.tmp';
4582
+ fs.writeFileSync(tmp, JSON.stringify(status, null, 2), { mode: 0o600, encoding: 'utf-8' });
4583
+ fs.renameSync(tmp, statusPath);
4584
+ }
4585
+ catch (err) {
4586
+ process.stderr.write(` [PIPELINE-093] Failed to merge gate block into status.json: ${err instanceof Error ? err.message : String(err)}\n`);
4587
+ }
4588
+ }
4589
+ /**
4590
+ * ADR-PIPELINE-093 — thin wrapper around `gatePhaseInputs` that surfaces
4591
+ * a consistent `[GATE]` log line and records the result into the
4592
+ * pipeline-wide accumulator. Returns the raw `GateResult` so the caller
4593
+ * can branch on `ok`.
4594
+ */
4595
+ async function runPhaseGate(phaseId, scenarioQuery, traceId, runDir, acc) {
4596
+ const dossier = {
4597
+ scenarioQuery,
4598
+ traceId,
4599
+ runDir,
4600
+ artifacts: {},
4601
+ };
4602
+ const context = {
4603
+ outputDir: path.join(runDir, '.ruflo-cache', `gate-${phaseId}`),
4604
+ };
4605
+ let result;
4606
+ try {
4607
+ result = await gatePhaseInputs(phaseId, { runDir, dossier, context });
4608
+ }
4609
+ catch (err) {
4610
+ // gatePhaseInputs never throws, but defensive fallback keeps the
4611
+ // pipeline running if the gate's own internals crash.
4612
+ const msg = err instanceof Error ? err.message : String(err);
4613
+ process.stderr.write(` [GATE] ${phaseId} threw unexpectedly: ${msg}\n`);
4614
+ result = { ok: false, missing: [], ruflo_invoked: [], stillMissing: [] };
4615
+ }
4616
+ recordGateResult(acc, phaseId, result);
4617
+ if (!result.ok && result.stillMissing.length > 0) {
4618
+ process.stderr.write(` [GATE] phase ${phaseId} blocked — stillMissing: ${result.stillMissing.join(', ')}\n`);
4619
+ }
4620
+ return result;
4621
+ }
4622
+ /**
4623
+ * Detect the target project language from Phase 1 manifest.json
4624
+ * (`project.language`) first, then falls back to common project root
4625
+ * marker files. Returns `'unknown'` when no signal is available — the
4626
+ * caller decides whether to skip or proceed in that case.
4627
+ */
4628
+ /**
4629
+ * ADR-PIPELINE-093 §Rule 5 — single arbiter for which scaffold template set
4630
+ * `copyPlanningArtifacts` should emit. Called ONCE per invocation so the TS
4631
+ * block and the Python block gate on the same decision.
4632
+ *
4633
+ * - `typescript` → emit TS, skip Python, no manifest record.
4634
+ * - `python` → skip TS, emit Python, no manifest record.
4635
+ * - `go`|`rust`|`unknown` → skip both, record `phase5b_skipped` with the
4636
+ * detected language and `template: 'typescript'`
4637
+ * (kept for Scenario-5 compatibility — the
4638
+ * skip block's shape is the ADR-093 schema).
4639
+ *
4640
+ * The detected-language + template-name pair in `skipped` matches what the
4641
+ * pre-093-fix gate wrote to `manifest.json`, so downstream status.json /
4642
+ * MCP consumers don't see a shape change on the mismatch path.
4643
+ */
4644
+ export function decideScaffoldEmission(detected) {
4645
+ if (detected === 'typescript')
4646
+ return { emitTs: true, emitPy: false, skipped: null };
4647
+ if (detected === 'python')
4648
+ return { emitTs: false, emitPy: true, skipped: null };
4649
+ // go / rust / unknown — no matching template set, record mismatch.
4650
+ return {
4651
+ emitTs: false,
4652
+ emitPy: false,
4653
+ skipped: { reason: 'language-mismatch', detected, template: 'typescript' },
4654
+ };
4655
+ }
4656
+ export function detectProjectLanguage(projectRoot, phase1Manifest) {
4657
+ // 1. Explicit signal from Phase 1 manifest.
4658
+ const projBlock = phase1Manifest?.['project'];
4659
+ if (projBlock && typeof projBlock === 'object') {
4660
+ const lang = projBlock['language'];
4661
+ if (typeof lang === 'string') {
4662
+ const normalized = lang.toLowerCase();
4663
+ if (normalized === 'typescript' || normalized === 'javascript')
4664
+ return 'typescript';
4665
+ if (normalized === 'python')
4666
+ return 'python';
4667
+ if (normalized === 'go' || normalized === 'golang')
4668
+ return 'go';
4669
+ if (normalized === 'rust')
4670
+ return 'rust';
4671
+ }
4672
+ }
4673
+ // 2. Project-root marker files.
4674
+ const fileExists = (p) => {
4675
+ try {
4676
+ return fs.statSync(p).isFile();
4677
+ }
4678
+ catch {
4679
+ return false;
4680
+ }
4681
+ };
4682
+ if (fileExists(path.join(projectRoot, 'package.json')))
4683
+ return 'typescript';
4684
+ if (fileExists(path.join(projectRoot, 'pyproject.toml')) ||
4685
+ fileExists(path.join(projectRoot, 'setup.py')))
4686
+ return 'python';
4687
+ if (fileExists(path.join(projectRoot, 'go.mod')))
4688
+ return 'go';
4689
+ if (fileExists(path.join(projectRoot, 'Cargo.toml')))
4690
+ return 'rust';
4691
+ return 'unknown';
4692
+ }
4693
+ /**
4694
+ * Build the `extras` payload attached to a `Phase1Dossier` for the Ruflo
4695
+ * primary executor. Successful remote enrichment agents are surfaced so
4696
+ * Ruflo can weave their output into task descriptions. ADR-091 §2.
4697
+ *
4698
+ * When no remote enrichment exists for this run, `remote_enrichment` is
4699
+ * still present but its arrays are empty — Ruflo task builders tolerate
4700
+ * either shape.
4701
+ */
4702
+ function buildRufloDossierExtras(runDir, enrichment) {
4703
+ const successfulEntries = enrichment.entries.filter(e => e.status === 'success' || e.status === 'invoked');
4704
+ const erroredEntries = enrichment.entries.filter(e => !(e.status === 'success' || e.status === 'invoked'));
4705
+ return {
4706
+ remote_enrichment: {
4707
+ runDir,
4708
+ available: successfulEntries.map(e => ({ domain: e.domain, agent: e.agent })),
4709
+ errored: erroredEntries.map(e => ({ domain: e.domain, agent: e.agent, status: e.status })),
4710
+ },
4711
+ };
4712
+ }
4713
+ /**
4714
+ * Invoke the Ruflo primary executor for a given engineering phase. Returns
4715
+ * the result (or a synthesized unavailable result on unexpected throw) so
4716
+ * auto-chain can always tag the phase for the ADR-091 execution block.
4717
+ *
4718
+ * This is a thin wrapper around `runPrimaryPhaseExecution` that:
4719
+ * 1. Ensures the dossier carries `scenarioQuery`, `traceId`, `runDir`,
4720
+ * plus Phase 1 artifacts + remote enrichment extras.
4721
+ * 2. Surfaces a `[ADR-091]` log line so operators can see which phases
4722
+ * actually executed via the local swarm.
4723
+ * 3. Never throws.
4724
+ */
4725
+ async function runRufloPrimaryForPhase(phaseId, scenarioQuery, traceId, runDir, priorArtifacts, enrichment, outputDir, language) {
4726
+ const dossier = {
4727
+ scenarioQuery,
4728
+ traceId,
4729
+ runDir,
4730
+ artifacts: priorArtifacts,
4731
+ extras: buildRufloDossierExtras(runDir, enrichment),
4732
+ };
4733
+ const context = {
4734
+ outputDir,
4735
+ ...(language ? { language } : {}),
4736
+ };
4737
+ try {
4738
+ const result = await runPrimaryPhaseExecution(phaseId, dossier, context);
4739
+ if (result.executionTier === 'ruflo-local' && result.success) {
4740
+ process.stderr.write(` [ADR-091] Ruflo primary executor produced ${result.filesModified} file(s) for ${phaseId} in ${result.timing}ms\n`);
4741
+ }
4742
+ else if (result.executionTier === 'unavailable') {
4743
+ process.stderr.write(` [ADR-091] Ruflo unavailable for ${phaseId} (${result.reason ?? 'no-reason'}) — falling through to template coordinator\n`);
4744
+ }
4745
+ else {
4746
+ process.stderr.write(` [ADR-091] Ruflo primary executor did not produce output for ${phaseId} (${result.reason ?? 'no-reason'}) — falling through\n`);
4747
+ }
4748
+ return result;
4749
+ }
4750
+ catch (err) {
4751
+ const msg = err instanceof Error ? err.message : String(err);
4752
+ process.stderr.write(` [ADR-091] Ruflo primary executor threw for ${phaseId}: ${msg.slice(0, 200)}\n`);
4753
+ return {
4754
+ phaseId,
4755
+ success: false,
4756
+ executionTier: 'unavailable',
4757
+ reason: 'swarm-error',
4758
+ filesModified: 0,
4759
+ timing: 0,
4760
+ message: `Ruflo primary executor threw for ${phaseId}: ${msg.slice(0, 200)}`,
4761
+ };
4762
+ }
4763
+ }
3794
4764
  // ============================================================================
3795
4765
  // Stdout Display Formatter
3796
4766
  // ============================================================================
@@ -3812,11 +4782,30 @@ export function formatAutoChainForDisplay(result) {
3812
4782
  lines.push(` Duration: ${(result.totalTiming / 1000).toFixed(1)}s`);
3813
4783
  lines.push(` Overall: ${result.success ? 'SUCCESS' : 'FAILED'}`);
3814
4784
  lines.push('');
4785
+ // ADR-PIPELINE-093 §Rule 3 surface (c) — blocked-phases warning banner.
4786
+ // Emits only when one or more phases could not run because their
4787
+ // prerequisites were unreachable after Ruflo retry. Zero-blocked runs
4788
+ // render no warning section.
4789
+ const blocked = result.blockedPhases ?? [];
4790
+ if (blocked.length > 0) {
4791
+ lines.push(' WARNING — BLOCKED PHASES:');
4792
+ for (const b of blocked) {
4793
+ const missingList = b.missing.length > 0 ? b.missing.join(', ') : '(unspecified)';
4794
+ lines.push(` - ${b.phase}: missing ${missingList}`);
4795
+ }
4796
+ lines.push(' Consider running the pipeline again, or check');
4797
+ lines.push(` ~/.agentics/runs/${result.traceId}/ for partial output.`);
4798
+ lines.push('');
4799
+ }
3815
4800
  // Phase-by-phase summary
3816
4801
  lines.push(' Phase Results:');
3817
4802
  lines.push(' ' + '-'.repeat(68));
3818
4803
  for (const phase of result.phases) {
3819
- const icon = phase.status === 'completed' ? '[OK]' : phase.status === 'skipped' ? '[--]' : '[!!]';
4804
+ // ADR-094 Decision 4: same icon set as printFinalSummary.
4805
+ const icon = phase.status === 'completed' ? '[OK]'
4806
+ : phase.status === 'skipped' ? '[--]'
4807
+ : phase.status === 'skipped-due-to-upstream' ? '[<-]'
4808
+ : '[!!]';
3820
4809
  const timingStr = phase.timing > 0 ? `${(phase.timing / 1000).toFixed(1)}s` : 'cached';
3821
4810
  lines.push(` ${icon} Phase ${phase.phase}: ${phase.label.padEnd(30)} ${timingStr.padStart(8)} (${phase.artifacts.length} artifacts)`);
3822
4811
  if (phase.error) {
@@ -3877,8 +4866,52 @@ export function formatAutoChainForDisplay(result) {
3877
4866
  lines.push('');
3878
4867
  return lines.join('\n');
3879
4868
  }
3880
- function buildResult(traceId, runDir, phases, startTime, executionMode = 'development', scenarioBranch, commitHash, remoteUrl) {
3881
- const success = phases.every(p => p.status === 'completed' || p.status === 'skipped');
4869
+ /**
4870
+ * ADR-094 Decision 4 Phase 6 always emits a phase-result entry. When an
4871
+ * upstream phase ends in `failed` or `blocked`, every downstream phase that
4872
+ * has not already been pushed gets a `skipped-due-to-upstream` entry. This
4873
+ * preserves the `chainResult.phases[]` trace contract: every run produces
4874
+ * an entry per phase, regardless of where the chain ended.
4875
+ *
4876
+ * Mutates `phases` in place. Idempotent — phases already present on the list
4877
+ * are not re-pushed. Exported for unit tests; production callers go through
4878
+ * the early-return sites in `executeAutoChain`.
4879
+ */
4880
+ export function pushSkippedDueToUpstream(phases, upstream, runDir) {
4881
+ // Phase labels mirror the dispatch sites. Output dirs follow the existing
4882
+ // <runDir>/phaseN convention from auto-chain. Keep the table in this one
4883
+ // place so future phases land here too.
4884
+ const REMAINING = [
4885
+ { phase: 2, label: 'Deep Research', dir: 'phase2' },
4886
+ { phase: 3, label: 'SPARC + London TDD', dir: 'phase3' },
4887
+ { phase: 4, label: 'ADRs + DDDs', dir: 'phase4' },
4888
+ { phase: 5, label: 'Build', dir: 'phase5' },
4889
+ { phase: 6, label: 'ERP Surface Push', dir: 'phase6' },
4890
+ ];
4891
+ const reasonExcerpt = upstream.reason.length > 200
4892
+ ? upstream.reason.slice(0, 200) + '…'
4893
+ : upstream.reason;
4894
+ for (const r of REMAINING) {
4895
+ if (r.phase <= upstream.phase)
4896
+ continue;
4897
+ if (phases.some((ph) => ph.phase === r.phase))
4898
+ continue;
4899
+ phases.push({
4900
+ phase: r.phase,
4901
+ label: r.label,
4902
+ status: 'skipped-due-to-upstream',
4903
+ timing: 0,
4904
+ artifacts: [],
4905
+ outputDir: path.join(runDir, r.dir),
4906
+ error: `Upstream phase ${upstream.phase} (${upstream.label}) failed: ${reasonExcerpt}`,
4907
+ });
4908
+ }
4909
+ }
4910
+ function buildResult(traceId, runDir, phases, startTime, executionMode = 'development', scenarioBranch, commitHash, remoteUrl, blockedPhases) {
4911
+ // ADR-094 Decision 4: `skipped-due-to-upstream` is treated like `skipped`
4912
+ // for the success check — it is not a failure of THIS phase, just a
4913
+ // propagation marker. `failed` remains the only status that flips success.
4914
+ const success = phases.every((p) => p.status === 'completed' || p.status === 'skipped' || p.status === 'skipped-due-to-upstream');
3882
4915
  // Build GitHub URL if we have a remote and a branch
3883
4916
  let githubUrl;
3884
4917
  if (remoteUrl && scenarioBranch) {
@@ -3897,6 +4930,9 @@ function buildResult(traceId, runDir, phases, startTime, executionMode = 'develo
3897
4930
  scenarioBranch,
3898
4931
  commitHash,
3899
4932
  githubUrl,
4933
+ // ADR-PIPELINE-093 §Rule 3 surface (c): carry blocked phases into the
4934
+ // result so `formatAutoChainForDisplay` can emit the banner warning.
4935
+ blockedPhases: blockedPhases ? blockedPhases.map(b => ({ phase: b.phase, missing: [...b.missing] })) : [],
3900
4936
  };
3901
4937
  }
3902
4938
  //# sourceMappingURL=auto-chain.js.map