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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (28) hide show
  1. package/dist/cli/index.js +30 -0
  2. package/dist/cli/index.js.map +1 -1
  3. package/dist/pipeline/auto-chain.d.ts +168 -0
  4. package/dist/pipeline/auto-chain.d.ts.map +1 -1
  5. package/dist/pipeline/auto-chain.js +1854 -880
  6. package/dist/pipeline/auto-chain.js.map +1 -1
  7. package/dist/pipeline/enterprise/artifact-renderers.d.ts +30 -0
  8. package/dist/pipeline/enterprise/artifact-renderers.d.ts.map +1 -1
  9. package/dist/pipeline/enterprise/artifact-renderers.js +129 -1
  10. package/dist/pipeline/enterprise/artifact-renderers.js.map +1 -1
  11. package/dist/pipeline/gate/phase-dependency-gate.d.ts +93 -0
  12. package/dist/pipeline/gate/phase-dependency-gate.d.ts.map +1 -0
  13. package/dist/pipeline/gate/phase-dependency-gate.js +349 -0
  14. package/dist/pipeline/gate/phase-dependency-gate.js.map +1 -0
  15. package/dist/pipeline/phase3-sparc/phase3-sparc-coordinator.d.ts.map +1 -1
  16. package/dist/pipeline/phase3-sparc/phase3-sparc-coordinator.js +280 -40
  17. package/dist/pipeline/phase3-sparc/phase3-sparc-coordinator.js.map +1 -1
  18. package/dist/pipeline/phase4-adrs/phase4-adrs-coordinator.d.ts.map +1 -1
  19. package/dist/pipeline/phase4-adrs/phase4-adrs-coordinator.js +363 -87
  20. package/dist/pipeline/phase4-adrs/phase4-adrs-coordinator.js.map +1 -1
  21. package/dist/pipeline/phases/prompt-generator.d.ts.map +1 -1
  22. package/dist/pipeline/phases/prompt-generator.js +95 -6
  23. package/dist/pipeline/phases/prompt-generator.js.map +1 -1
  24. package/dist/pipeline/ruflo-phase-executor.d.ts +124 -1
  25. package/dist/pipeline/ruflo-phase-executor.d.ts.map +1 -1
  26. package/dist/pipeline/ruflo-phase-executor.js +319 -4
  27. package/dist/pipeline/ruflo-phase-executor.js.map +1 -1
  28. package/package.json +1 -1
@@ -24,6 +24,25 @@ const CHECK_TIMEOUT = 30_000;
24
24
  const DEFAULT_SWARM_TIMEOUT = 900_000; // 15 min — SPARC generation takes ~10 min via ruflo
25
25
  const DEFAULT_MAX_AGENTS = 8;
26
26
  const MAX_ARTIFACT_CONTENT_BYTES = 32_000;
27
+ /**
28
+ * Per-phase wall-clock budget for `runPrimaryPhaseExecution` (ADR-PIPELINE-091 §6).
29
+ * Read once at module scope; overridable via `AGENTICS_RUFLO_PHASE_TIMEOUT` env var.
30
+ * Default 600000 ms (10 min) matches the ADR.
31
+ */
32
+ const DEFAULT_PHASE_TIMEOUT = (() => {
33
+ const raw = process.env['AGENTICS_RUFLO_PHASE_TIMEOUT'];
34
+ if (!raw)
35
+ return 600_000;
36
+ const parsed = Number(raw);
37
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : 600_000;
38
+ })();
39
+ /** True when the operator has explicitly disabled the Ruflo primary executor. */
40
+ function isRufloDisabledByEnv() {
41
+ const raw = process.env['AGENTICS_DISABLE_RUFLO'];
42
+ if (!raw)
43
+ return false;
44
+ return /^(1|true|yes|on)$/i.test(raw.trim());
45
+ }
27
46
  /**
28
47
  * Naming convention rules embedded in implementation prompts to prevent
29
48
  * double-prefix bugs like "IIbudgetManagementPort".
@@ -121,11 +140,22 @@ export function resolveRufloBinary() {
121
140
  return null;
122
141
  return null;
123
142
  }
124
- /** Check ruflo is installed and has swarm support. */
143
+ /**
144
+ * Check ruflo is installed and has swarm support.
145
+ *
146
+ * Adds an optional `reason` field when Ruflo is unavailable so callers
147
+ * (notably `runPrimaryPhaseExecution`) can distinguish operator-disabled
148
+ * from binary-missing from swarm-unsupported. Per ADR-PIPELINE-091 §6,
149
+ * `AGENTICS_DISABLE_RUFLO=true` short-circuits this check before any binary
150
+ * resolution so CI environments never attempt Ruflo at all.
151
+ */
125
152
  export function checkRufloAvailable() {
153
+ if (isRufloDisabledByEnv()) {
154
+ return { available: false, version: 'N/A', binary: '', reason: 'disabled-by-env' };
155
+ }
126
156
  const binary = resolveRufloBinary();
127
157
  if (!binary)
128
- return { available: false, version: 'N/A', binary: '' };
158
+ return { available: false, version: 'N/A', binary: '', reason: 'binary-not-found' };
129
159
  try {
130
160
  const output = execSync(`${binary} --version`, {
131
161
  encoding: 'utf-8', timeout: CHECK_TIMEOUT, stdio: 'pipe',
@@ -136,12 +166,12 @@ export function checkRufloAvailable() {
136
166
  execSync(`${binary} swarm --help`, { encoding: 'utf-8', timeout: CHECK_TIMEOUT, stdio: 'pipe' });
137
167
  }
138
168
  catch {
139
- return { available: false, version: `${version} (swarm not supported)`, binary };
169
+ return { available: false, version: `${version} (swarm not supported)`, binary, reason: 'swarm-unsupported' };
140
170
  }
141
171
  return { available: true, version, binary };
142
172
  }
143
173
  catch {
144
- return { available: false, version: 'N/A', binary };
174
+ return { available: false, version: 'N/A', binary, reason: 'version-probe-failed' };
145
175
  }
146
176
  }
147
177
  // ============================================================================
@@ -492,6 +522,7 @@ export function groupIntoWaves(sortedLevels, _language, scenarioQuery, artifacts
492
522
  `base repository interfaces, and ERP client stubs. Source: ${artifactCtx}`,
493
523
  targetDir: 'src/shared',
494
524
  wave: 1,
525
+ argv: ['--local-only'],
495
526
  }],
496
527
  dependsOn: [],
497
528
  });
@@ -519,6 +550,7 @@ export function groupIntoWaves(sortedLevels, _language, scenarioQuery, artifacts
519
550
  `base repository interfaces, and ERP client stubs. Source: ${artifactCtx}`,
520
551
  targetDir: 'src/shared',
521
552
  wave: 1,
553
+ argv: ['--local-only'],
522
554
  }],
523
555
  dependsOn: [],
524
556
  });
@@ -564,6 +596,7 @@ export function groupIntoWaves(sortedLevels, _language, scenarioQuery, artifacts
564
596
  targetDir: 'ui',
565
597
  wave: 4,
566
598
  dependsOn: ['Database & Shared Infrastructure'],
599
+ argv: ['--local-only'],
567
600
  },
568
601
  {
569
602
  label: 'Infrastructure & Integration',
@@ -575,6 +608,7 @@ export function groupIntoWaves(sortedLevels, _language, scenarioQuery, artifacts
575
608
  targetDir: 'src/infra',
576
609
  wave: 4,
577
610
  dependsOn: ['Database & Shared Infrastructure'],
611
+ argv: ['--local-only'],
578
612
  },
579
613
  {
580
614
  label: 'Test Suite',
@@ -592,6 +626,7 @@ export function groupIntoWaves(sortedLevels, _language, scenarioQuery, artifacts
592
626
  targetDir: 'tests',
593
627
  wave: 4,
594
628
  dependsOn: testDependsOn,
629
+ argv: ['--local-only'],
595
630
  },
596
631
  {
597
632
  label: 'Deployment Configuration',
@@ -603,6 +638,7 @@ export function groupIntoWaves(sortedLevels, _language, scenarioQuery, artifacts
603
638
  targetDir: 'deploy',
604
639
  wave: 4,
605
640
  dependsOn: ['Database & Shared Infrastructure'],
641
+ argv: ['--local-only'],
606
642
  },
607
643
  ],
608
644
  dependsOn: [1, 2, 3],
@@ -804,6 +840,7 @@ export function buildIterativeImplementationPrompts(artifacts, scenarioQuery, la
804
840
  targetDir: `src/${ctxSlug}`,
805
841
  wave: wave.wave,
806
842
  dependsOn: taskDependsOn.length > 0 ? taskDependsOn : undefined,
843
+ argv: ['--local-only'],
807
844
  });
808
845
  }
809
846
  // Merge context tasks with any pre-existing wave tasks (e.g., infrastructure, cross-cutting)
@@ -1140,6 +1177,7 @@ Write all output to the ./plans folder as markdown files:
1140
1177
 
1141
1178
  The implementation must be enterprise grade, commercially viable, production ready, bug and error free with no compilation issues.`,
1142
1179
  targetDir: 'plans',
1180
+ argv: ['--local-only'],
1143
1181
  },
1144
1182
  ];
1145
1183
  }
@@ -1163,6 +1201,7 @@ Write all output to the ./plans folder:
1163
1201
  - plans/adrs/ (one .md file per ADR)
1164
1202
  - plans/ddd/ (domain-model.md + context-map.md)`,
1165
1203
  targetDir: 'plans',
1204
+ argv: ['--local-only'],
1166
1205
  },
1167
1206
  ];
1168
1207
  }
@@ -1193,6 +1232,7 @@ Write all output to the ./plans/prompts/ folder:
1193
1232
  - ... (continue for all phases, typically 10-30 depending on complexity)
1194
1233
  - plans/prompts/execution-plan.json (ordered list of all prompts with dependencies)`,
1195
1234
  targetDir: 'plans/prompts',
1235
+ argv: ['--local-only'],
1196
1236
  },
1197
1237
  ];
1198
1238
  }
@@ -1232,6 +1272,7 @@ CRITICAL — DO NOT generate scaffolding or stubs. This is a REAL implementation
1232
1272
  Engineers on the client's team should be able to plug this into their ERP using the ERP surface endpoints that are already established, with light massaging only.`,
1233
1273
  targetDir: 'src',
1234
1274
  wave: 1,
1275
+ argv: ['--local-only'],
1235
1276
  },
1236
1277
  ];
1237
1278
  }
@@ -1257,6 +1298,7 @@ function buildPhase5TasksLegacy(scenarioQuery, language, artifacts) {
1257
1298
  `and comprehensive error handling. Follow the technology decisions in the ADRs. Source files: ${artifactCtx}`,
1258
1299
  targetDir: 'src',
1259
1300
  wave: 1,
1301
+ argv: ['--local-only'],
1260
1302
  },
1261
1303
  {
1262
1304
  label: 'Frontend Application',
@@ -1269,6 +1311,7 @@ function buildPhase5TasksLegacy(scenarioQuery, language, artifacts) {
1269
1311
  targetDir: 'ui',
1270
1312
  wave: 2,
1271
1313
  dependsOn: ['Backend Implementation'],
1314
+ argv: ['--local-only'],
1272
1315
  },
1273
1316
  {
1274
1317
  label: 'Infrastructure & Integration',
@@ -1281,6 +1324,7 @@ function buildPhase5TasksLegacy(scenarioQuery, language, artifacts) {
1281
1324
  targetDir: 'src/infra',
1282
1325
  wave: 2,
1283
1326
  dependsOn: ['Backend Implementation'],
1327
+ argv: ['--local-only'],
1284
1328
  },
1285
1329
  {
1286
1330
  label: 'Database & SQL',
@@ -1292,6 +1336,7 @@ function buildPhase5TasksLegacy(scenarioQuery, language, artifacts) {
1292
1336
  `database-specific optimizations for the database engine specified in the ADRs. Source files: ${artifactCtx}`,
1293
1337
  targetDir: 'sql',
1294
1338
  wave: 1,
1339
+ argv: ['--local-only'],
1295
1340
  },
1296
1341
  {
1297
1342
  label: 'Test Suite',
@@ -1310,6 +1355,7 @@ function buildPhase5TasksLegacy(scenarioQuery, language, artifacts) {
1310
1355
  targetDir: 'tests',
1311
1356
  wave: 4,
1312
1357
  dependsOn: ['Backend Implementation', 'Database & SQL'],
1358
+ argv: ['--local-only'],
1313
1359
  },
1314
1360
  {
1315
1361
  label: 'Deployment Configuration',
@@ -1324,6 +1370,7 @@ function buildPhase5TasksLegacy(scenarioQuery, language, artifacts) {
1324
1370
  targetDir: 'deploy',
1325
1371
  wave: 2,
1326
1372
  dependsOn: ['Backend Implementation'],
1373
+ argv: ['--local-only'],
1327
1374
  },
1328
1375
  ];
1329
1376
  }
@@ -1364,9 +1411,269 @@ export function buildPhase6Tasks(scenarioQuery, artifacts) {
1364
1411
  `data transformation adapters specific to this domain, and health monitoring for ERP connectivity. ` +
1365
1412
  `Source files: ${artifactCtx}`,
1366
1413
  targetDir: 'erp',
1414
+ argv: ['--local-only'],
1415
+ },
1416
+ ];
1417
+ }
1418
+ // ============================================================================
1419
+ // Phase 5/5a task builders — ADR-PIPELINE-091
1420
+ // ============================================================================
1421
+ /**
1422
+ * Canonical SPARC section ordering. `buildImplPromptsTasks` emits one Ruflo
1423
+ * task per section so downstream tests can assert `task count ===
1424
+ * SPARC_SECTIONS.length` on the default path.
1425
+ */
1426
+ const SPARC_SECTIONS = ['specification', 'pseudocode', 'architecture', 'refinement', 'completion'];
1427
+ /**
1428
+ * Build Ruflo tasks for the coverage-gap slot (normally handled by the remote
1429
+ * `quality-engineering/coverage-gap-detect` agent). Under ADR-PIPELINE-091
1430
+ * the local swarm produces the same output so remote 503s do not gate the
1431
+ * pipeline. The task writes a `coverage-gaps.json` under the phase-5
1432
+ * coverage directory.
1433
+ */
1434
+ export function buildCoverageGapTasks(dossier, context) {
1435
+ const language = context.language ?? 'typescript';
1436
+ const sparcRef = dossier.artifacts?.['SPARC'] ?? dossier.artifacts?.['sparc-combined'] ?? '';
1437
+ const tddRef = dossier.artifacts?.['TDD'] ?? dossier.artifacts?.['tdd'] ?? '';
1438
+ const adrRef = dossier.artifacts?.['ADRs'] ?? dossier.artifacts?.['adrs'] ?? '';
1439
+ const dddRef = dossier.artifacts?.['DDD'] ?? dossier.artifacts?.['ddd'] ?? '';
1440
+ const sources = [
1441
+ sparcRef ? `SPARC=${sparcRef}` : '',
1442
+ tddRef ? `TDD=${tddRef}` : '',
1443
+ adrRef ? `ADRs=${adrRef}` : '',
1444
+ dddRef ? `DDD=${dddRef}` : '',
1445
+ ].filter(Boolean).join(', ') || '(none yet — infer gaps from scenario query alone)';
1446
+ return [
1447
+ {
1448
+ label: 'Coverage Gap Detection',
1449
+ description: [
1450
+ `Task: emit the coverage-gap list the remote quality-engineering/coverage-gap-detect agent would normally produce.`,
1451
+ ``,
1452
+ `## Project scenario`,
1453
+ dossier.scenarioQuery,
1454
+ ``,
1455
+ `## Target language`,
1456
+ language,
1457
+ ``,
1458
+ `## Source artifacts`,
1459
+ sources,
1460
+ ``,
1461
+ `## Required output (write exactly these files under ./coverage/)`,
1462
+ `- coverage/coverage-gaps.json`,
1463
+ ` Shape: { "traceId": "${dossier.traceId}", "gaps": [{ "area": string, "file": string, "severity": "high"|"medium"|"low", "reason": string, "suggestedTests": string[] }], "summary": { "high": number, "medium": number, "low": number } }`,
1464
+ `- coverage/coverage-gaps.md (human-readable companion report).`,
1465
+ ``,
1466
+ `Rules:`,
1467
+ `1. Every gap must cite the SPARC or DDD element it comes from (command, query, aggregate, or domain event).`,
1468
+ `2. Severity uses "high" for unvalidated domain invariants, "medium" for missing command-handler integration tests, "low" for style/docstring gaps.`,
1469
+ `3. Do NOT invent files that are not in the SPARC/DDD/ADR set above — if no source is available, emit an empty gaps array and a summary with zeroes.`,
1470
+ `4. This is a LOCAL-ONLY run. Do not call any remote diligence agent; the primary executor has already marked this pass as local-only via --local-only.`,
1471
+ ].join('\n'),
1472
+ targetDir: 'coverage',
1473
+ argv: ['--local-only'],
1367
1474
  },
1368
1475
  ];
1369
1476
  }
1477
+ /**
1478
+ * Build Ruflo tasks for the implementation-prompts slot (normally produced
1479
+ * from SPARC + ADRs + DDD). Emits one Ruflo task per canonical SPARC section
1480
+ * so the combined output populates `plans/prompts/impl-NNN-<slug>.md` plus
1481
+ * `plans/prompts/execution-plan.json`. Matches the filename format auto-chain
1482
+ * consumes: `impl-${String(order).padStart(3, '0')}-${phase.slug}.md`.
1483
+ */
1484
+ export function buildImplPromptsTasks(dossier, context, adrs = [], ddd = []) {
1485
+ const language = context.language ?? 'typescript';
1486
+ const adrSummary = adrs.length
1487
+ ? adrs.slice(0, 24).map(a => ` - ${a.id}: ${a.title}${a.decision ? ' — ' + a.decision.slice(0, 160) : ''}`).join('\n')
1488
+ : ' (no ADRs supplied — infer from SPARC alone)';
1489
+ const dddSummary = ddd.length
1490
+ ? ddd.slice(0, 24).map(c => ` - ${c.name}${c.aggregates?.length ? ' (' + c.aggregates.slice(0, 6).join(', ') + ')' : ''}`).join('\n')
1491
+ : ' (no DDD contexts supplied — infer from SPARC alone)';
1492
+ const sparcRef = dossier.artifacts?.['SPARC'] ?? dossier.artifacts?.['sparc-combined'] ?? '';
1493
+ return SPARC_SECTIONS.map((section, idx) => {
1494
+ const order = idx + 1;
1495
+ const slug = section;
1496
+ const filename = `impl-${String(order).padStart(3, '0')}-${slug}.md`;
1497
+ return {
1498
+ label: `Impl Prompt — ${section}`,
1499
+ description: [
1500
+ `Task: produce the implementation prompt for the SPARC "${section}" section.`,
1501
+ ``,
1502
+ `## Scenario`,
1503
+ dossier.scenarioQuery,
1504
+ ``,
1505
+ `## Target language`,
1506
+ language,
1507
+ ``,
1508
+ `## ADRs in scope`,
1509
+ adrSummary,
1510
+ ``,
1511
+ `## Bounded contexts in scope`,
1512
+ dddSummary,
1513
+ ``,
1514
+ `## Source SPARC artifact`,
1515
+ sparcRef || '(none — use scenario query alone)',
1516
+ ``,
1517
+ `## Required output`,
1518
+ `Write exactly one file: plans/prompts/${filename}`,
1519
+ `The prompt must be dependency-coherent with its predecessors (prompts 001..${String(order - 1).padStart(3, '0')}).`,
1520
+ `Include:`,
1521
+ `- Exact typed interfaces with domain-specific fields (no Record<string, unknown>).`,
1522
+ `- The algorithms or database DDL this step implements.`,
1523
+ `- Test cases validating domain behavior (not "id is defined").`,
1524
+ `- A clear "Prerequisites" block that references prior prompts by filename.`,
1525
+ ``,
1526
+ `After writing the ${filename} file, append an entry to plans/prompts/execution-plan.json (create if missing) with shape: `,
1527
+ ` { "order": ${order}, "file": "${filename}", "section": "${section}", "dependsOn": [${order > 1 ? `"impl-${String(order - 1).padStart(3, '0')}-${SPARC_SECTIONS[idx - 1]}.md"` : ''}] }.`,
1528
+ ``,
1529
+ `This is a LOCAL-ONLY run. --local-only is already set on the task; do not invoke remote diligence agents.`,
1530
+ ].join('\n'),
1531
+ targetDir: 'plans/prompts',
1532
+ wave: order === 1 ? 1 : 2,
1533
+ dependsOn: order === 1 ? undefined : [`Impl Prompt — ${SPARC_SECTIONS[idx - 1]}`],
1534
+ argv: ['--local-only'],
1535
+ };
1536
+ });
1537
+ }
1538
+ /**
1539
+ * Map a canonical phase id onto the matching task builder + label + output
1540
+ * slot. Centralised here so both auto-chain and the ADR-093 dependency gate
1541
+ * see the same mapping.
1542
+ */
1543
+ function buildTasksForPrimaryPhase(phaseId, dossier, context) {
1544
+ const artifacts = dossier.artifacts ?? {};
1545
+ switch (phaseId) {
1546
+ case 'phase3-sparc':
1547
+ return {
1548
+ tasks: buildPhase3Tasks(dossier.scenarioQuery, artifacts),
1549
+ label: 'SPARC + London TDD (primary)',
1550
+ phaseNumber: 3,
1551
+ };
1552
+ case 'phase4-adrs-ddd':
1553
+ return {
1554
+ tasks: buildPhase4Tasks(dossier.scenarioQuery, artifacts),
1555
+ label: 'ADRs + DDD (primary)',
1556
+ phaseNumber: 4,
1557
+ };
1558
+ case 'phase5a-prompts': {
1559
+ // Try to extract ADR + DDD summaries from dossier artifacts if present.
1560
+ const adrSummaries = artifacts['ADRs']
1561
+ ? extractADRSummary(artifacts['ADRs']).map(a => ({ id: a.id, title: a.title, decision: a.decision }))
1562
+ : [];
1563
+ const dddSummaries = artifacts['DDD']
1564
+ ? extractDDDSummary(artifacts['DDD']).contexts.map(c => ({ name: c.name, aggregates: c.aggregates }))
1565
+ : [];
1566
+ return {
1567
+ tasks: buildImplPromptsTasks(dossier, context, adrSummaries, dddSummaries),
1568
+ label: 'Implementation Prompts (primary)',
1569
+ phaseNumber: 5,
1570
+ };
1571
+ }
1572
+ case 'phase5-coverage':
1573
+ return {
1574
+ tasks: buildCoverageGapTasks(dossier, context),
1575
+ label: 'Coverage Gap Detection (primary)',
1576
+ phaseNumber: 5,
1577
+ };
1578
+ }
1579
+ }
1580
+ /**
1581
+ * Canonical primary-executor entry point per ADR-PIPELINE-091.
1582
+ *
1583
+ * Invariants:
1584
+ * 1. Never throws. Returns `{ success: false, executionTier: 'unavailable' }`
1585
+ * when Ruflo is not runnable — the caller decides fallback.
1586
+ * 2. Honors `AGENTICS_DISABLE_RUFLO=true` (short-circuits without touching
1587
+ * the binary; reason is `ruflo-disabled-by-env`).
1588
+ * 3. Timeout defaults to `DEFAULT_PHASE_TIMEOUT`, overridable per-call. When
1589
+ * the Ruflo swarm exceeds the budget, tier stays `ruflo-local` but
1590
+ * `success` flips to false with reason `ruflo-timeout` so the gate can
1591
+ * trigger template fallback.
1592
+ * 4. Every dispatched task carries `--local-only` (enforced both in builders
1593
+ * and defensively by the executor loop).
1594
+ */
1595
+ export async function runPrimaryPhaseExecution(phaseId, dossier, context, timeout) {
1596
+ const startTime = Date.now();
1597
+ const effectiveTimeout = typeof timeout === 'number' && timeout > 0 ? timeout : DEFAULT_PHASE_TIMEOUT;
1598
+ const check = checkRufloAvailable();
1599
+ if (!check.available) {
1600
+ const reason = check.reason === 'disabled-by-env' ? 'ruflo-disabled-by-env' : 'ruflo-not-available';
1601
+ return {
1602
+ phaseId,
1603
+ success: false,
1604
+ executionTier: 'unavailable',
1605
+ reason,
1606
+ filesModified: 0,
1607
+ timing: Date.now() - startTime,
1608
+ message: check.reason === 'disabled-by-env'
1609
+ ? `Ruflo disabled via AGENTICS_DISABLE_RUFLO; caller should fall through to templates for ${phaseId}`
1610
+ : `Ruflo not available (${check.version || 'no binary'}); caller should fall through to templates for ${phaseId}`,
1611
+ };
1612
+ }
1613
+ const { tasks, label, phaseNumber } = buildTasksForPrimaryPhase(phaseId, dossier, context);
1614
+ if (tasks.length === 0) {
1615
+ return {
1616
+ phaseId,
1617
+ success: false,
1618
+ executionTier: 'ruflo-local',
1619
+ reason: 'no-tasks-built',
1620
+ filesModified: 0,
1621
+ timing: Date.now() - startTime,
1622
+ message: `No Ruflo tasks built for ${phaseId}`,
1623
+ };
1624
+ }
1625
+ // Ensure output dir exists before dispatch — executeRufloPhaseSwarm does
1626
+ // this too but the caller expects the directory to exist on success.
1627
+ try {
1628
+ fs.mkdirSync(context.outputDir, { recursive: true, mode: 0o700 });
1629
+ }
1630
+ catch {
1631
+ // non-fatal; downstream will surface any permission problems
1632
+ }
1633
+ let result;
1634
+ try {
1635
+ result = executeRufloPhaseSwarm({
1636
+ phase: phaseNumber,
1637
+ label,
1638
+ scenarioQuery: dossier.scenarioQuery,
1639
+ runDir: dossier.runDir,
1640
+ traceId: dossier.traceId,
1641
+ outputDir: context.outputDir,
1642
+ tasks,
1643
+ agenticsResults: context.agenticsResults ?? [],
1644
+ priorArtifacts: dossier.artifacts ?? {},
1645
+ maxAgents: context.maxAgents ?? DEFAULT_MAX_AGENTS,
1646
+ timeoutMs: effectiveTimeout,
1647
+ });
1648
+ }
1649
+ catch (err) {
1650
+ const msg = err instanceof Error ? err.message : String(err);
1651
+ return {
1652
+ phaseId,
1653
+ success: false,
1654
+ executionTier: 'ruflo-local',
1655
+ reason: 'swarm-error',
1656
+ filesModified: 0,
1657
+ timing: Date.now() - startTime,
1658
+ message: `Ruflo swarm errored for ${phaseId}: ${msg.slice(0, 200)}`,
1659
+ };
1660
+ }
1661
+ const timing = Date.now() - startTime;
1662
+ const success = result.filesModified > 0;
1663
+ const timedOut = timing >= effectiveTimeout && !result.swarmCompleted;
1664
+ return {
1665
+ phaseId,
1666
+ success,
1667
+ executionTier: 'ruflo-local',
1668
+ reason: success ? undefined : (timedOut ? 'ruflo-timeout' : 'swarm-error'),
1669
+ rufloResult: result,
1670
+ filesModified: result.filesModified,
1671
+ timing,
1672
+ message: success
1673
+ ? `Ruflo primary executor produced ${result.filesModified} files for ${phaseId} in ${timing}ms`
1674
+ : `Ruflo primary executor produced no files for ${phaseId} in ${timing}ms`,
1675
+ };
1676
+ }
1370
1677
  // ============================================================================
1371
1678
  // Main Executor
1372
1679
  // ============================================================================
@@ -1442,11 +1749,19 @@ export function executeRufloPhaseSwarm(config) {
1442
1749
  agenticsContext,
1443
1750
  artifactContent.length > 0 ? `\n--- PRIOR PHASE ARTIFACTS (actual content) ---${artifactContent}\n--- END PRIOR PHASE ARTIFACTS ---` : '',
1444
1751
  ].join('\n');
1752
+ // Per ADR-PIPELINE-091: every engineering task is dispatched `--local-only`
1753
+ // so the local swarm does not round-trip to the remote fleet it is meant
1754
+ // to be independent of. Task builders normalize this; if a legacy task
1755
+ // slipped through without argv, we defensively inject it here too.
1756
+ const extraArgs = Array.from(task.argv ?? []);
1757
+ if (!extraArgs.includes('--local-only'))
1758
+ extraArgs.push('--local-only');
1445
1759
  try {
1446
1760
  rufloExec(check.binary, [
1447
1761
  'task', 'create',
1448
1762
  '--type', 'implementation',
1449
1763
  '--description', fullDescription,
1764
+ ...extraArgs,
1450
1765
  ], config.outputDir, CHECK_TIMEOUT);
1451
1766
  tasksCreated++;
1452
1767
  }