@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.
- package/dist/cli/index.js +30 -0
- package/dist/cli/index.js.map +1 -1
- package/dist/pipeline/auto-chain.d.ts +196 -2
- package/dist/pipeline/auto-chain.d.ts.map +1 -1
- package/dist/pipeline/auto-chain.js +1920 -884
- package/dist/pipeline/auto-chain.js.map +1 -1
- package/dist/pipeline/enterprise/agent-error-capture.d.ts +76 -0
- package/dist/pipeline/enterprise/agent-error-capture.d.ts.map +1 -0
- package/dist/pipeline/enterprise/agent-error-capture.js +141 -0
- package/dist/pipeline/enterprise/agent-error-capture.js.map +1 -0
- package/dist/pipeline/enterprise/artifact-renderers.d.ts +30 -0
- package/dist/pipeline/enterprise/artifact-renderers.d.ts.map +1 -1
- package/dist/pipeline/enterprise/artifact-renderers.js +129 -1
- package/dist/pipeline/enterprise/artifact-renderers.js.map +1 -1
- package/dist/pipeline/enterprise/pass-executor.d.ts.map +1 -1
- package/dist/pipeline/enterprise/pass-executor.js +52 -0
- package/dist/pipeline/enterprise/pass-executor.js.map +1 -1
- package/dist/pipeline/enterprise/pipeline-orchestrator.d.ts.map +1 -1
- package/dist/pipeline/enterprise/pipeline-orchestrator.js +15 -0
- package/dist/pipeline/enterprise/pipeline-orchestrator.js.map +1 -1
- package/dist/pipeline/enterprise/types.d.ts +21 -0
- package/dist/pipeline/enterprise/types.d.ts.map +1 -1
- package/dist/pipeline/gate/feature-flags.d.ts +30 -0
- package/dist/pipeline/gate/feature-flags.d.ts.map +1 -0
- package/dist/pipeline/gate/feature-flags.js +37 -0
- package/dist/pipeline/gate/feature-flags.js.map +1 -0
- package/dist/pipeline/gate/phase-dependency-gate.d.ts +179 -0
- package/dist/pipeline/gate/phase-dependency-gate.d.ts.map +1 -0
- package/dist/pipeline/gate/phase-dependency-gate.js +571 -0
- package/dist/pipeline/gate/phase-dependency-gate.js.map +1 -0
- package/dist/pipeline/local-fallback/phase1-consensus-reader.d.ts +33 -0
- package/dist/pipeline/local-fallback/phase1-consensus-reader.d.ts.map +1 -0
- package/dist/pipeline/local-fallback/phase1-consensus-reader.js +99 -0
- package/dist/pipeline/local-fallback/phase1-consensus-reader.js.map +1 -0
- package/dist/pipeline/local-fallback/phase3-local-fallback.d.ts +26 -0
- package/dist/pipeline/local-fallback/phase3-local-fallback.d.ts.map +1 -0
- package/dist/pipeline/local-fallback/phase3-local-fallback.js +127 -0
- package/dist/pipeline/local-fallback/phase3-local-fallback.js.map +1 -0
- package/dist/pipeline/local-fallback/phase4-local-fallback.d.ts +21 -0
- package/dist/pipeline/local-fallback/phase4-local-fallback.d.ts.map +1 -0
- package/dist/pipeline/local-fallback/phase4-local-fallback.js +240 -0
- package/dist/pipeline/local-fallback/phase4-local-fallback.js.map +1 -0
- package/dist/pipeline/local-fallback/phase5a-local-fallback.d.ts +28 -0
- package/dist/pipeline/local-fallback/phase5a-local-fallback.d.ts.map +1 -0
- package/dist/pipeline/local-fallback/phase5a-local-fallback.js +166 -0
- package/dist/pipeline/local-fallback/phase5a-local-fallback.js.map +1 -0
- package/dist/pipeline/phase3-sparc/phase3-sparc-coordinator.d.ts.map +1 -1
- package/dist/pipeline/phase3-sparc/phase3-sparc-coordinator.js +280 -40
- package/dist/pipeline/phase3-sparc/phase3-sparc-coordinator.js.map +1 -1
- package/dist/pipeline/phase4-adrs/phase4-adrs-coordinator.d.ts.map +1 -1
- package/dist/pipeline/phase4-adrs/phase4-adrs-coordinator.js +363 -87
- package/dist/pipeline/phase4-adrs/phase4-adrs-coordinator.js.map +1 -1
- package/dist/pipeline/phases/prompt-generator.d.ts.map +1 -1
- package/dist/pipeline/phases/prompt-generator.js +303 -6
- package/dist/pipeline/phases/prompt-generator.js.map +1 -1
- package/dist/pipeline/ruflo-phase-executor.d.ts +104 -1
- package/dist/pipeline/ruflo-phase-executor.d.ts.map +1 -1
- package/dist/pipeline/ruflo-phase-executor.js +406 -4
- package/dist/pipeline/ruflo-phase-executor.js.map +1 -1
- package/dist/pipeline/swarm-orchestrator.d.ts +47 -0
- package/dist/pipeline/swarm-orchestrator.d.ts.map +1 -1
- package/dist/pipeline/swarm-orchestrator.js +130 -3
- package/dist/pipeline/swarm-orchestrator.js.map +1 -1
- 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
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
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-
|
|
1455
|
-
//
|
|
1456
|
-
//
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
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
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
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
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
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
|
-
|
|
1794
|
-
|
|
2179
|
+
catch {
|
|
2180
|
+
// Cleanup is best-effort — never block scaffold emission on a scan failure.
|
|
1795
2181
|
}
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
//
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
2182
|
+
totalCopied += 3; // 3 TS files (logger, config, errors) — PY counted below if emitted
|
|
2183
|
+
} // end if (emitTsScaffold)
|
|
2184
|
+
// ADR-PIPELINE-093 §Rule 5 — Python 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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
|
|
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
|
-
|
|
2213
|
-
//
|
|
2214
|
-
|
|
2215
|
-
const
|
|
2216
|
-
|
|
2217
|
-
|
|
2218
|
-
|
|
2219
|
-
|
|
2220
|
-
|
|
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
|
-
|
|
2242
|
-
|
|
2243
|
-
|
|
2244
|
-
|
|
2245
|
-
//
|
|
2246
|
-
|
|
2247
|
-
const
|
|
2248
|
-
|
|
2249
|
-
|
|
2250
|
-
|
|
2251
|
-
|
|
2252
|
-
|
|
2253
|
-
|
|
2254
|
-
|
|
2255
|
-
|
|
2256
|
-
|
|
2257
|
-
|
|
2258
|
-
|
|
2259
|
-
|
|
2260
|
-
|
|
2261
|
-
|
|
2262
|
-
|
|
2263
|
-
|
|
2264
|
-
|
|
2265
|
-
|
|
2266
|
-
|
|
2267
|
-
|
|
2268
|
-
|
|
2269
|
-
|
|
2270
|
-
|
|
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
|
-
|
|
2313
|
-
//
|
|
2314
|
-
|
|
2315
|
-
const
|
|
2316
|
-
|
|
2317
|
-
|
|
2318
|
-
|
|
2319
|
-
|
|
2320
|
-
|
|
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
|
-
|
|
2342
|
-
|
|
2343
|
-
|
|
2344
|
-
|
|
2345
|
-
//
|
|
2346
|
-
|
|
2347
|
-
|
|
2348
|
-
|
|
2349
|
-
|
|
2350
|
-
|
|
2351
|
-
|
|
2352
|
-
|
|
2353
|
-
|
|
2354
|
-
|
|
2355
|
-
|
|
2356
|
-
|
|
2357
|
-
|
|
2358
|
-
|
|
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
|
-
//
|
|
2368
|
-
if
|
|
2369
|
-
|
|
2370
|
-
|
|
2371
|
-
|
|
2372
|
-
|
|
2373
|
-
|
|
2374
|
-
|
|
2375
|
-
|
|
2376
|
-
|
|
2377
|
-
|
|
2378
|
-
|
|
2379
|
-
|
|
2380
|
-
|
|
2381
|
-
|
|
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
|
-
|
|
2387
|
-
|
|
2388
|
-
|
|
2389
|
-
|
|
2390
|
-
|
|
2391
|
-
|
|
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
|
-
|
|
2397
|
-
|
|
2398
|
-
|
|
2399
|
-
|
|
2400
|
-
|
|
2401
|
-
|
|
2402
|
-
|
|
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
|
-
|
|
2420
|
-
|
|
2421
|
-
const
|
|
2422
|
-
if (
|
|
2423
|
-
const
|
|
2424
|
-
|
|
2425
|
-
|
|
2426
|
-
|
|
2427
|
-
|
|
2428
|
-
|
|
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
|
-
|
|
2431
|
-
|
|
2432
|
-
|
|
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
|
-
|
|
2438
|
-
console.error(
|
|
2899
|
+
else {
|
|
2900
|
+
console.error(' [WARN] SPARC or dossier not found — cannot generate ADRs');
|
|
2439
2901
|
}
|
|
2440
|
-
recovered = true;
|
|
2441
2902
|
}
|
|
2442
|
-
|
|
2443
|
-
console.error(
|
|
2903
|
+
catch (recoverErr) {
|
|
2904
|
+
console.error(` [WARN] ADR recovery failed: ${recoverErr instanceof Error ? recoverErr.message : String(recoverErr)}`);
|
|
2444
2905
|
}
|
|
2445
2906
|
}
|
|
2446
|
-
|
|
2447
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
2474
|
-
|
|
2475
|
-
|
|
2476
|
-
|
|
2477
|
-
|
|
2478
|
-
|
|
2479
|
-
|
|
2480
|
-
|
|
2481
|
-
|
|
2482
|
-
|
|
2483
|
-
|
|
2484
|
-
|
|
2485
|
-
|
|
2486
|
-
|
|
2487
|
-
|
|
2488
|
-
|
|
2489
|
-
|
|
2490
|
-
|
|
2491
|
-
|
|
2492
|
-
|
|
2493
|
-
|
|
2494
|
-
|
|
2495
|
-
|
|
2496
|
-
|
|
2497
|
-
|
|
2498
|
-
|
|
2499
|
-
|
|
2500
|
-
|
|
2501
|
-
|
|
2502
|
-
|
|
2503
|
-
|
|
2504
|
-
|
|
2505
|
-
|
|
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(
|
|
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
|
-
|
|
2515
|
-
|
|
2516
|
-
|
|
2517
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2528
|
-
|
|
2529
|
-
|
|
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
|
-
|
|
2566
|
-
|
|
2567
|
-
|
|
2568
|
-
|
|
2569
|
-
|
|
2570
|
-
|
|
2571
|
-
|
|
2572
|
-
|
|
2573
|
-
|
|
2574
|
-
|
|
2575
|
-
|
|
2576
|
-
|
|
2577
|
-
|
|
2578
|
-
|
|
2579
|
-
|
|
2580
|
-
|
|
2581
|
-
|
|
2582
|
-
|
|
2583
|
-
|
|
2584
|
-
|
|
2585
|
-
|
|
2586
|
-
|
|
2587
|
-
|
|
2588
|
-
|
|
2589
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
2620
|
-
|
|
2621
|
-
|
|
3078
|
+
try {
|
|
3079
|
+
adrMarkdownByFile.set(f, fs.readFileSync(path.join(adrDir, f), 'utf-8'));
|
|
3080
|
+
}
|
|
3081
|
+
catch { /* skip */ }
|
|
2622
3082
|
}
|
|
2623
|
-
|
|
2624
|
-
|
|
2625
|
-
|
|
2626
|
-
|
|
2627
|
-
|
|
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
|
-
|
|
2630
|
-
|
|
2631
|
-
|
|
2632
|
-
|
|
2633
|
-
|
|
2634
|
-
|
|
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
|
-
|
|
2639
|
-
|
|
2640
|
-
|
|
2641
|
-
|
|
2642
|
-
|
|
2643
|
-
|
|
2644
|
-
|
|
2645
|
-
|
|
2646
|
-
|
|
2647
|
-
|
|
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
|
-
|
|
2654
|
-
|
|
2655
|
-
|
|
2656
|
-
|
|
2657
|
-
|
|
2658
|
-
|
|
2659
|
-
|
|
2660
|
-
|
|
2661
|
-
|
|
2662
|
-
|
|
2663
|
-
|
|
2664
|
-
|
|
2665
|
-
|
|
2666
|
-
|
|
2667
|
-
|
|
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
|
-
|
|
2674
|
-
|
|
2675
|
-
|
|
2676
|
-
|
|
2677
|
-
|
|
2678
|
-
|
|
2679
|
-
|
|
2680
|
-
|
|
2681
|
-
|
|
2682
|
-
|
|
2683
|
-
|
|
2684
|
-
|
|
2685
|
-
|
|
2686
|
-
|
|
2687
|
-
|
|
2688
|
-
|
|
2689
|
-
|
|
2690
|
-
|
|
2691
|
-
|
|
2692
|
-
|
|
2693
|
-
|
|
2694
|
-
|
|
2695
|
-
|
|
2696
|
-
|
|
2697
|
-
|
|
2698
|
-
|
|
2699
|
-
|
|
2700
|
-
|
|
2701
|
-
|
|
2702
|
-
|
|
2703
|
-
|
|
2704
|
-
|
|
2705
|
-
|
|
2706
|
-
|
|
2707
|
-
|
|
2708
|
-
|
|
2709
|
-
|
|
2710
|
-
|
|
2711
|
-
|
|
2712
|
-
|
|
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
|
-
|
|
2716
|
-
|
|
2717
|
-
|
|
2718
|
-
|
|
2719
|
-
|
|
2720
|
-
|
|
2721
|
-
|
|
2722
|
-
|
|
2723
|
-
|
|
2724
|
-
|
|
2725
|
-
|
|
2726
|
-
|
|
2727
|
-
|
|
2728
|
-
|
|
2729
|
-
|
|
2730
|
-
|
|
2731
|
-
|
|
2732
|
-
|
|
2733
|
-
|
|
2734
|
-
|
|
2735
|
-
|
|
2736
|
-
|
|
2737
|
-
|
|
2738
|
-
|
|
2739
|
-
|
|
2740
|
-
|
|
2741
|
-
|
|
2742
|
-
|
|
2743
|
-
|
|
2744
|
-
|
|
2745
|
-
|
|
2746
|
-
|
|
2747
|
-
|
|
2748
|
-
|
|
2749
|
-
|
|
2750
|
-
|
|
2751
|
-
|
|
2752
|
-
|
|
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
|
-
|
|
2756
|
-
|
|
2757
|
-
|
|
2758
|
-
|
|
2759
|
-
|
|
2760
|
-
|
|
2761
|
-
|
|
2762
|
-
|
|
2763
|
-
|
|
2764
|
-
|
|
2765
|
-
|
|
2766
|
-
|
|
2767
|
-
|
|
2768
|
-
|
|
2769
|
-
|
|
2770
|
-
|
|
2771
|
-
|
|
2772
|
-
|
|
2773
|
-
|
|
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
|
-
|
|
2865
|
-
|
|
2866
|
-
|
|
2867
|
-
|
|
2868
|
-
|
|
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
|
-
|
|
2931
|
-
|
|
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
|
-
|
|
2960
|
-
|
|
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
|
-
|
|
2985
|
-
|
|
2986
|
-
|
|
2987
|
-
|
|
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
|
-
|
|
3120
|
-
|
|
3121
|
-
|
|
3122
|
-
|
|
3123
|
-
|
|
3124
|
-
|
|
3125
|
-
|
|
3126
|
-
|
|
3127
|
-
|
|
3128
|
-
|
|
3129
|
-
|
|
3130
|
-
|
|
3131
|
-
|
|
3132
|
-
|
|
3133
|
-
|
|
3134
|
-
|
|
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
|
-
|
|
3142
|
-
|
|
3143
|
-
|
|
3144
|
-
|
|
3145
|
-
|
|
3146
|
-
//
|
|
3147
|
-
const
|
|
3148
|
-
|
|
3149
|
-
|
|
3150
|
-
|
|
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
|
-
//
|
|
3184
|
-
|
|
3185
|
-
const
|
|
3186
|
-
|
|
3187
|
-
|
|
3188
|
-
|
|
3189
|
-
if (
|
|
3190
|
-
parts.push(`This phase
|
|
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
|
-
|
|
3197
|
-
|
|
3198
|
-
|
|
3199
|
-
|
|
3200
|
-
|
|
3201
|
-
|
|
3202
|
-
|
|
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
|
-
|
|
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
|
-
|
|
3282
|
-
|
|
3283
|
-
|
|
3284
|
-
|
|
3285
|
-
|
|
3286
|
-
|
|
3287
|
-
|
|
3288
|
-
|
|
3289
|
-
|
|
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
|
-
|
|
3295
|
-
|
|
3296
|
-
|
|
3297
|
-
|
|
3298
|
-
|
|
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
|
-
|
|
3301
|
-
|
|
3302
|
-
|
|
3303
|
-
|
|
3304
|
-
|
|
3305
|
-
|
|
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
|
-
|
|
3308
|
-
|
|
3309
|
-
|
|
3310
|
-
|
|
3311
|
-
|
|
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
|
-
|
|
3330
|
-
|
|
3331
|
-
console.error(`
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
3881
|
-
|
|
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
|