@llm-dev-ops/agentics-cli 2.7.17 → 2.7.19

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.
@@ -0,0 +1,707 @@
1
+ /**
2
+ * Phase 7 — Consulting Deliverables Coordinator (ADR-PIPELINE-098 D1, D5–D9).
3
+ *
4
+ * Walks the 85-row deliverables registry and, for each row:
5
+ * 1. If `mirrorsExisting` is set → symlink (POSIX) or copy (Windows /
6
+ * AGENTICS_PHASE7_MIRROR=copy) the pre-existing top-level doc into the
7
+ * sectioned subdir; record `status: 'emitted'`.
8
+ * 2. Otherwise → load `inputArtifacts` from disk into a Record<path,string>,
9
+ * build a Ruflo task (templateId + inputs + consensus banner + fcv block
10
+ * from `unit-economics.json`), dispatch through the existing
11
+ * `executeRufloPhaseSwarm` path used by Phase 5/6, then render the
12
+ * response through `template-renderer.renderTemplate` and write to
13
+ * `<runDir>/deliverables/<NN-section>/<slug>.<format>`.
14
+ * 3. On failure → emit a skeleton file with frontmatter
15
+ * `degraded_mode: generation-failed` + `failure_reason` + the available /
16
+ * missing inputs + the consensus banner + the fcv block. The phase keeps
17
+ * going; status drops to `partial`.
18
+ *
19
+ * After every row is processed the coordinator writes a markdown index.md and
20
+ * a machine-readable `phase7-manifest.json` (D9.a schema), then runs the
21
+ * coherence gate. Any coherence violation drops the phase status to
22
+ * `partial`.
23
+ *
24
+ * Phase 1 input gate (D1 / ADR-094 D4):
25
+ * If `<runDir>/manifest.json` does NOT exist, the coordinator fans out
26
+ * nothing and returns `status: 'skipped-due-to-upstream'` with reason
27
+ * `phase1-inputs-missing`. The caller in `auto-chain.ts` still pushes a
28
+ * `phases[]` entry per ADR-094 D4.
29
+ *
30
+ * Hard contracts:
31
+ * - Never throws. All failures collapse into `Phase7Result.status` +
32
+ * per-row entries in the manifest.
33
+ * - Pure I/O at the boundary; no globals, no module-level state.
34
+ * - Templates dir + registry file + coherence-gate file are owned by other
35
+ * agents working in parallel; this file imports them without modifying.
36
+ */
37
+ import * as fs from 'node:fs';
38
+ import * as os from 'node:os';
39
+ import * as path from 'node:path';
40
+ // FIXME: depends on @agent-3 — `deliverables-registry.ts` may not yet exist
41
+ // when this file is first compiled. The exports below are the contract from
42
+ // ADR-PIPELINE-098 D3; if registry types drift, fix this import after the
43
+ // registry agent lands its file.
44
+ import { DELIVERABLES_REGISTRY, } from './deliverables-registry.js';
45
+ // FIXME: depends on @agent-4 — `coherence-gate.ts` may not yet exist when
46
+ // this file is first compiled. The export below is the contract from
47
+ // ADR-PIPELINE-098 D8.
48
+ import { runCoherenceGate, } from './coherence-gate.js';
49
+ import { renderTemplate, injectStandardSections, } from './template-renderer.js';
50
+ import { executeRufloPhaseSwarm, } from '../ruflo-phase-executor.js';
51
+ import { loadUnitEconomics } from '../../synthesis/unit-economics-loader.js';
52
+ // ============================================================================
53
+ // Constants
54
+ // ============================================================================
55
+ const SECTION_DIRS = {
56
+ 1: '01-executive',
57
+ 2: '02-financial',
58
+ 3: '03-strategy',
59
+ 4: '04-operating-model',
60
+ 5: '05-technology',
61
+ 6: '06-risk-compliance',
62
+ 7: '07-research-diagnostics',
63
+ 8: '08-program-management',
64
+ 9: '09-implementation',
65
+ 10: '10-performance-monitoring',
66
+ 11: '11-commercial',
67
+ 12: '12-advanced',
68
+ };
69
+ const TEMPLATES_DIR_REL = path.join('pipeline', 'phase7', 'deliverables-templates');
70
+ // ============================================================================
71
+ // Public entry point
72
+ // ============================================================================
73
+ /**
74
+ * Phase 7 entry point. Returns a `Phase7Result` describing what happened.
75
+ * Never throws — disk-full and similar conditions are reported as
76
+ * `status: 'failed'` with `reason` set.
77
+ */
78
+ export async function executePhase7(args) {
79
+ const startTime = Date.now();
80
+ const { runDir, traceId, scenarioQuery } = args;
81
+ // ── D1 / ADR-094 D4: Phase 1 input gate ──
82
+ const manifestPath = path.join(runDir, 'manifest.json');
83
+ if (!fs.existsSync(manifestPath)) {
84
+ return {
85
+ status: 'skipped-due-to-upstream',
86
+ deliverables_emitted: 0,
87
+ deliverables_skeleton: 0,
88
+ deliverables_failed: 0,
89
+ deliverables_total: DELIVERABLES_REGISTRY.length,
90
+ coherence_violations: [],
91
+ timing_ms: Date.now() - startTime,
92
+ output_paths: [],
93
+ reason: 'phase1-inputs-missing',
94
+ };
95
+ }
96
+ // ── Prepare deliverables/ root + lazy section dirs ──
97
+ const deliverablesRoot = path.join(runDir, 'deliverables');
98
+ try {
99
+ fs.mkdirSync(deliverablesRoot, { recursive: true });
100
+ }
101
+ catch (err) {
102
+ return {
103
+ status: 'failed',
104
+ deliverables_emitted: 0,
105
+ deliverables_skeleton: 0,
106
+ deliverables_failed: 0,
107
+ deliverables_total: DELIVERABLES_REGISTRY.length,
108
+ coherence_violations: [],
109
+ timing_ms: Date.now() - startTime,
110
+ output_paths: [],
111
+ reason: `mkdir-failed: ${err instanceof Error ? err.message : String(err)}`,
112
+ };
113
+ }
114
+ // ── Standard-sections context (consensus banner + fcv block) ──
115
+ const consensusBanner = readConsensusBanner(runDir);
116
+ const fcvBlock = buildFcvBlock(runDir);
117
+ // ── Walk the registry ──
118
+ const rufloCacheDir = path.join(runDir, '.ruflo-cache', 'phase7');
119
+ try {
120
+ fs.mkdirSync(rufloCacheDir, { recursive: true });
121
+ }
122
+ catch {
123
+ /* non-fatal — the executor recreates as needed */
124
+ }
125
+ const entries = [];
126
+ const writtenPaths = [];
127
+ const startedAt = new Date().toISOString();
128
+ for (const spec of DELIVERABLES_REGISTRY) {
129
+ const rowStart = Date.now();
130
+ const sectionDir = SECTION_DIRS[spec.section];
131
+ if (!sectionDir) {
132
+ // Defensive — should never happen if registry is well-formed.
133
+ entries.push({
134
+ section: spec.section,
135
+ slug: spec.slug,
136
+ status: 'failed',
137
+ input_artifacts_used: [],
138
+ input_artifacts_missing: [...spec.inputArtifacts],
139
+ source_agents_called: [],
140
+ fcv_kinds_emitted: [],
141
+ timing_ms: Date.now() - rowStart,
142
+ output_path: '',
143
+ failure_reason: `unknown-section-${spec.section}`,
144
+ });
145
+ continue;
146
+ }
147
+ const targetDir = path.join(deliverablesRoot, sectionDir);
148
+ try {
149
+ fs.mkdirSync(targetDir, { recursive: true });
150
+ }
151
+ catch {
152
+ /* fall through; the per-row write will error and become a skeleton */
153
+ }
154
+ const outFileName = `${spec.slug}.${primaryFormatExt(spec.format)}`;
155
+ const outPath = path.join(targetDir, outFileName);
156
+ if (spec.mirrorsExisting) {
157
+ // ── D6: Mirror pre-existing top-level artifact ──
158
+ const result = mirrorExisting(runDir, spec.mirrorsExisting, outPath);
159
+ entries.push({
160
+ section: spec.section,
161
+ slug: spec.slug,
162
+ status: result.ok ? 'emitted' : 'failed',
163
+ input_artifacts_used: result.ok ? [spec.mirrorsExisting] : [],
164
+ input_artifacts_missing: result.ok ? [] : [spec.mirrorsExisting],
165
+ source_agents_called: [],
166
+ fcv_kinds_emitted: [],
167
+ timing_ms: Date.now() - rowStart,
168
+ output_path: outPath,
169
+ ...(result.ok ? {} : { failure_reason: result.reason }),
170
+ });
171
+ if (result.ok)
172
+ writtenPaths.push(outPath);
173
+ continue;
174
+ }
175
+ // ── D5 / D7: Generate via Ruflo, fall back to skeleton on any failure ──
176
+ const inputs = loadInputs(runDir, spec.inputArtifacts);
177
+ const templateBody = readTemplate(spec.templateId);
178
+ if (templateBody === null) {
179
+ // Template missing → skeleton.
180
+ const skelPath = writeSkeleton(outPath, spec, {
181
+ consensusBanner,
182
+ fcvBlock,
183
+ availableInputs: inputs.available,
184
+ missingInputs: inputs.missing,
185
+ failureReason: `template-missing:${spec.templateId}`,
186
+ });
187
+ entries.push({
188
+ section: spec.section,
189
+ slug: spec.slug,
190
+ status: 'skeleton',
191
+ input_artifacts_used: inputs.available.map(i => i.relPath),
192
+ input_artifacts_missing: inputs.missing,
193
+ source_agents_called: [],
194
+ fcv_kinds_emitted: extractEmittedFcvKinds(fcvBlock, spec.fcvKinds),
195
+ timing_ms: Date.now() - rowStart,
196
+ output_path: skelPath,
197
+ failure_reason: `template-missing:${spec.templateId}`,
198
+ });
199
+ writtenPaths.push(skelPath);
200
+ continue;
201
+ }
202
+ // Build the Ruflo task list (one task per deliverable).
203
+ const task = buildDeliverableTask(spec, templateBody, inputs.available, {
204
+ scenarioQuery,
205
+ consensusBanner,
206
+ fcvBlock,
207
+ });
208
+ let rufloOk = false;
209
+ let rufloError;
210
+ let renderedBody = '';
211
+ try {
212
+ // Each deliverable dispatches as its own single-task swarm so a single
213
+ // failure cannot poison a sibling. Per-row Ruflo cost dominates the
214
+ // phase's wall time but the user directive is explicit that runtime is
215
+ // not a constraint.
216
+ const rufloResult = executeRufloPhaseSwarm({
217
+ phase: 7,
218
+ label: `Phase 7 — ${spec.slug}`,
219
+ scenarioQuery,
220
+ runDir,
221
+ traceId,
222
+ outputDir: path.join(rufloCacheDir, spec.slug),
223
+ tasks: [task],
224
+ agenticsResults: [],
225
+ priorArtifacts: inputsToArtifactMap(inputs.available),
226
+ });
227
+ // The Ruflo path writes intermediate files into the cache dir; we
228
+ // don't depend on them here because the deterministic substitution
229
+ // below already carries the consensus + fcv invariants. If the
230
+ // future writer-agent contract returns structured fields they should
231
+ // be merged in at this point.
232
+ void rufloResult;
233
+ // Deterministic render — fields are sourced from the structured
234
+ // writer-agent response when available, otherwise from the
235
+ // skeleton-friendly defaults below. Until the writer agent contract
236
+ // is wired, every deliverable still carries the consensus + fcv
237
+ // invariants required by D4.
238
+ const fields = {
239
+ title: spec.title,
240
+ traceId,
241
+ date: new Date().toISOString().slice(0, 10),
242
+ consensusBanner,
243
+ fcvBlock,
244
+ agentAttributionFooter: buildAgentAttributionFooter(spec.sourceAgents),
245
+ scenarioQuery,
246
+ };
247
+ const rendered = renderTemplate(templateBody, fields);
248
+ renderedBody = injectStandardSections(rendered, {
249
+ consensusBanner,
250
+ fcvBlock,
251
+ agentAttribution: buildAgentAttributionFooter(spec.sourceAgents),
252
+ });
253
+ rufloOk = true;
254
+ }
255
+ catch (err) {
256
+ rufloError = err instanceof Error ? err.message : String(err);
257
+ }
258
+ if (!rufloOk) {
259
+ const skelPath = writeSkeleton(outPath, spec, {
260
+ consensusBanner,
261
+ fcvBlock,
262
+ availableInputs: inputs.available,
263
+ missingInputs: inputs.missing,
264
+ failureReason: rufloError ?? 'ruflo-dispatch-failed',
265
+ });
266
+ entries.push({
267
+ section: spec.section,
268
+ slug: spec.slug,
269
+ status: 'skeleton',
270
+ input_artifacts_used: inputs.available.map(i => i.relPath),
271
+ input_artifacts_missing: inputs.missing,
272
+ source_agents_called: [...spec.sourceAgents],
273
+ fcv_kinds_emitted: extractEmittedFcvKinds(fcvBlock, spec.fcvKinds),
274
+ timing_ms: Date.now() - rowStart,
275
+ output_path: skelPath,
276
+ failure_reason: rufloError ?? 'ruflo-dispatch-failed',
277
+ });
278
+ writtenPaths.push(skelPath);
279
+ continue;
280
+ }
281
+ // Write the rendered deliverable.
282
+ let writeError;
283
+ try {
284
+ fs.writeFileSync(outPath, renderedBody, 'utf-8');
285
+ }
286
+ catch (err) {
287
+ writeError = err instanceof Error ? err.message : String(err);
288
+ }
289
+ if (writeError) {
290
+ entries.push({
291
+ section: spec.section,
292
+ slug: spec.slug,
293
+ status: 'failed',
294
+ input_artifacts_used: inputs.available.map(i => i.relPath),
295
+ input_artifacts_missing: inputs.missing,
296
+ source_agents_called: [...spec.sourceAgents],
297
+ fcv_kinds_emitted: [],
298
+ timing_ms: Date.now() - rowStart,
299
+ output_path: outPath,
300
+ failure_reason: `write-failed: ${writeError}`,
301
+ });
302
+ continue;
303
+ }
304
+ entries.push({
305
+ section: spec.section,
306
+ slug: spec.slug,
307
+ status: 'emitted',
308
+ input_artifacts_used: inputs.available.map(i => i.relPath),
309
+ input_artifacts_missing: inputs.missing,
310
+ source_agents_called: [...spec.sourceAgents],
311
+ fcv_kinds_emitted: extractEmittedFcvKinds(renderedBody, spec.fcvKinds),
312
+ timing_ms: Date.now() - rowStart,
313
+ output_path: outPath,
314
+ });
315
+ writtenPaths.push(outPath);
316
+ }
317
+ // ── Index.md — D2 ──
318
+ const indexPath = path.join(deliverablesRoot, 'index.md');
319
+ try {
320
+ fs.writeFileSync(indexPath, buildIndexMd(entries, traceId), 'utf-8');
321
+ writtenPaths.push(indexPath);
322
+ }
323
+ catch {
324
+ /* non-fatal — manifest still records the deliverables */
325
+ }
326
+ // ── Coherence gate — D8 ──
327
+ let coherenceViolations = [];
328
+ try {
329
+ const unitEconomicsPath = path.join(runDir, 'unit-economics.json');
330
+ const report = await runCoherenceGate(deliverablesRoot, unitEconomicsPath);
331
+ coherenceViolations = report.violations;
332
+ }
333
+ catch {
334
+ /* coherence-gate failures are non-fatal; the phase still emits */
335
+ coherenceViolations = [];
336
+ }
337
+ // ── Manifest — D9.a ──
338
+ const emitted = entries.filter(e => e.status === 'emitted').length;
339
+ const skeleton = entries.filter(e => e.status === 'skeleton').length;
340
+ const failed = entries.filter(e => e.status === 'failed').length;
341
+ const completedAt = new Date().toISOString();
342
+ const totalTimingMs = Date.now() - startTime;
343
+ const phase7Manifest = {
344
+ phase: 7,
345
+ trace_id: traceId,
346
+ started_at: startedAt,
347
+ completed_at: completedAt,
348
+ total_timing_ms: totalTimingMs,
349
+ deliverables_total: DELIVERABLES_REGISTRY.length,
350
+ deliverables_emitted: emitted,
351
+ deliverables_skeleton: skeleton,
352
+ deliverables_failed: failed,
353
+ coherence_violations: coherenceViolations,
354
+ deliverables: entries,
355
+ };
356
+ const manifestOut = path.join(deliverablesRoot, 'phase7-manifest.json');
357
+ try {
358
+ fs.writeFileSync(manifestOut, JSON.stringify(phase7Manifest, null, 2), 'utf-8');
359
+ writtenPaths.push(manifestOut);
360
+ }
361
+ catch {
362
+ /* non-fatal */
363
+ }
364
+ // ── Final status ──
365
+ const status = failed > 0 || skeleton > 0 || coherenceViolations.length > 0
366
+ ? 'partial'
367
+ : 'completed';
368
+ return {
369
+ status,
370
+ deliverables_emitted: emitted,
371
+ deliverables_skeleton: skeleton,
372
+ deliverables_failed: failed,
373
+ deliverables_total: DELIVERABLES_REGISTRY.length,
374
+ coherence_violations: coherenceViolations,
375
+ timing_ms: totalTimingMs,
376
+ output_paths: writtenPaths,
377
+ };
378
+ }
379
+ function loadInputs(runDir, paths) {
380
+ const available = [];
381
+ const missing = [];
382
+ for (const rel of paths) {
383
+ const abs = path.isAbsolute(rel) ? rel : path.join(runDir, rel);
384
+ try {
385
+ if (fs.existsSync(abs) && fs.statSync(abs).isFile()) {
386
+ const content = fs.readFileSync(abs, 'utf-8');
387
+ available.push({ relPath: rel, absPath: abs, content });
388
+ }
389
+ else {
390
+ missing.push(rel);
391
+ }
392
+ }
393
+ catch {
394
+ missing.push(rel);
395
+ }
396
+ }
397
+ return { available, missing };
398
+ }
399
+ function inputsToArtifactMap(inputs) {
400
+ const out = {};
401
+ for (const i of inputs) {
402
+ out[i.relPath] = i.absPath;
403
+ }
404
+ return out;
405
+ }
406
+ function readTemplate(templateId) {
407
+ // The templates dir lives next to this coordinator at compile time; resolve
408
+ // it both as a sibling source path (during dev) and as a sibling dist path
409
+ // (after `tsc`) by walking up from `__filename`. Use `import.meta.url` style
410
+ // resolution via require-style path joining since this codebase compiles to
411
+ // CommonJS-compatible ES modules.
412
+ //
413
+ // The deliverables-templates files are part of the package, so the search
414
+ // simply looks under `<pkgRoot>/src/pipeline/phase7/deliverables-templates`
415
+ // and `<pkgRoot>/dist/pipeline/phase7/deliverables-templates` — whichever
416
+ // exists.
417
+ const candidates = candidateTemplateRoots();
418
+ for (const root of candidates) {
419
+ const p = path.join(root, `${templateId}.tmpl.md`);
420
+ try {
421
+ if (fs.existsSync(p))
422
+ return fs.readFileSync(p, 'utf-8');
423
+ }
424
+ catch {
425
+ /* try next candidate */
426
+ }
427
+ }
428
+ return null;
429
+ }
430
+ function candidateTemplateRoots() {
431
+ const roots = [];
432
+ // Sibling of THIS file. Works under both CJS (__dirname) and ESM
433
+ // (import.meta.url) — under tsc-compiled CJS we get a valid __dirname;
434
+ // under native ESM (e.g., the standalone validation harness) we fall
435
+ // through to the cwd-relative paths below.
436
+ try {
437
+ // @ts-ignore -- guarded type for dual-runtime resolution
438
+ const here = typeof __dirname === 'string' ? __dirname : null;
439
+ if (here)
440
+ roots.push(path.join(here, 'deliverables-templates'));
441
+ }
442
+ catch { /* ignore */ }
443
+ // ESM-safe sibling resolution: derive from import.meta.url when available.
444
+ try {
445
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
446
+ const meta = import.meta;
447
+ if (meta && typeof meta.url === 'string') {
448
+ const url = new URL('./deliverables-templates/', meta.url);
449
+ roots.push(url.pathname);
450
+ }
451
+ }
452
+ catch { /* ignore */ }
453
+ // Source-dir / dist-dir fallbacks (covers `cwd != packageRoot` cases when
454
+ // run via tsx/ts-node and when run from a different working directory).
455
+ try {
456
+ const cwd = process.cwd();
457
+ roots.push(path.join(cwd, 'src', TEMPLATES_DIR_REL));
458
+ roots.push(path.join(cwd, 'dist', TEMPLATES_DIR_REL));
459
+ }
460
+ catch { /* ignore */ }
461
+ return roots;
462
+ }
463
+ function readConsensusBanner(runDir) {
464
+ // The consensus.json shape is owned by `synthesis/consensus-tiers.ts`. To
465
+ // keep this coordinator dependency-light, we read the pre-rendered banner
466
+ // when it has already been written next to the consensus snapshot, and
467
+ // fall back to a minimal directional placeholder otherwise.
468
+ const candidates = [
469
+ path.join(runDir, 'consensus-banner.md'),
470
+ path.join(runDir, 'consensus-banner.txt'),
471
+ ];
472
+ for (const p of candidates) {
473
+ try {
474
+ if (fs.existsSync(p)) {
475
+ const raw = fs.readFileSync(p, 'utf-8').trim();
476
+ if (raw.length > 0)
477
+ return raw;
478
+ }
479
+ }
480
+ catch { /* try next */ }
481
+ }
482
+ // Fall back to a minimal tier-agnostic banner when we cannot reconstruct.
483
+ // Coherence is preserved because every deliverable carries the SAME string.
484
+ return '> 📊 **Consensus snapshot:** see `consensus.json` for agreement / precision / verdict.';
485
+ }
486
+ function buildFcvBlock(runDir) {
487
+ const ue = loadUnitEconomics(runDir);
488
+ if (!ue.manifest) {
489
+ return '<!-- fcv:kind=unit-economics src=missing -->';
490
+ }
491
+ const m = ue.manifest;
492
+ const measured = formatUsd(m.annual_measured_savings_usd);
493
+ const enterprise = formatUsd(m.annual_extrapolated_savings_usd);
494
+ return [
495
+ '<!-- fcv:kind=savings scope=enterprise doc=phase7 src=unit-economics.json -->',
496
+ `Annual enterprise savings: ${enterprise}`,
497
+ '',
498
+ '<!-- fcv:kind=savings scope=measured doc=phase7 src=unit-economics.json -->',
499
+ `Annual measured savings (pilot): ${measured}`,
500
+ '',
501
+ `<!-- fcv:kind=method src=unit-economics.json -->`,
502
+ `Extrapolation method: ${m.extrapolation_method}`,
503
+ ].join('\n');
504
+ }
505
+ function formatUsd(n) {
506
+ if (!Number.isFinite(n) || n <= 0)
507
+ return '$0';
508
+ if (n >= 1_000_000)
509
+ return `$${(n / 1_000_000).toFixed(1)}M`;
510
+ if (n >= 1_000)
511
+ return `$${(n / 1_000).toFixed(0)}K`;
512
+ return `$${Math.round(n)}`;
513
+ }
514
+ function buildAgentAttributionFooter(agents) {
515
+ if (!agents || agents.length === 0)
516
+ return '';
517
+ const visible = agents.slice(0, 6).join(', ');
518
+ const more = agents.length > 6 ? ` + ${agents.length - 6} more` : '';
519
+ return `Sources: ${visible}${more}`;
520
+ }
521
+ function buildDeliverableTask(spec, templateBody, inputs, ctx) {
522
+ const inputSection = inputs.length > 0
523
+ ? inputs.map(i => `--- INPUT: ${i.relPath} ---\n${truncate(i.content, 8_000)}\n`).join('\n')
524
+ : '(no input artifacts available — produce a coherent skeleton from the template only)';
525
+ const description = [
526
+ `# Phase 7 Deliverable: ${spec.title}`,
527
+ '',
528
+ `Section ${spec.section} · slug \`${spec.slug}\` · template \`${spec.templateId}\` · format ${spec.format}`,
529
+ '',
530
+ '## Project scenario',
531
+ ctx.scenarioQuery,
532
+ '',
533
+ '## Consensus banner (must appear verbatim at top of output)',
534
+ ctx.consensusBanner,
535
+ '',
536
+ '## Standard fcv block (must appear verbatim where the template has {{fcvBlock}})',
537
+ ctx.fcvBlock,
538
+ '',
539
+ '## Source agents to attribute',
540
+ spec.sourceAgents.length > 0 ? spec.sourceAgents.join(', ') : '(none specified)',
541
+ '',
542
+ '## Required fcv kinds',
543
+ spec.fcvKinds.length > 0 ? spec.fcvKinds.join(', ') : '(none — purely qualitative)',
544
+ '',
545
+ '## Template body',
546
+ templateBody,
547
+ '',
548
+ '## Input artifacts',
549
+ inputSection,
550
+ '',
551
+ '## Rules',
552
+ '1. Fill every {{placeholder}} in the template with content derived from the inputs.',
553
+ '2. Do not invent dollar figures. Numbers MUST come from the fcv block above.',
554
+ '3. The output is a single Markdown document. Do not wrap it in a code fence.',
555
+ '4. This is a LOCAL-ONLY run. The dispatch already injects --local-only.',
556
+ ].join('\n');
557
+ return {
558
+ label: `phase7-${spec.slug}`,
559
+ description,
560
+ targetDir: 'deliverables',
561
+ argv: ['--local-only'],
562
+ };
563
+ }
564
+ function truncate(s, max) {
565
+ if (s.length <= max)
566
+ return s;
567
+ return `${s.slice(0, max)}\n…[truncated ${s.length - max} bytes]`;
568
+ }
569
+ function primaryFormatExt(format) {
570
+ // Some registry rows declare composite formats like `md+csv`; pick the
571
+ // first as the primary file. Companion outputs (csv/svg) are written by
572
+ // the deliverable-specific renderer when needed.
573
+ const head = String(format).split('+')[0]?.trim() ?? 'md';
574
+ return head;
575
+ }
576
+ function writeSkeleton(outPath, spec, ctx) {
577
+ const frontmatter = [
578
+ '---',
579
+ 'degraded_mode: generation-failed',
580
+ `failure_reason: ${escapeYaml(ctx.failureReason)}`,
581
+ `section: ${spec.section}`,
582
+ `slug: ${spec.slug}`,
583
+ `template_id: ${spec.templateId}`,
584
+ `inputs_available:`,
585
+ ...(ctx.availableInputs.length > 0
586
+ ? ctx.availableInputs.map(i => ` - ${i.relPath}`)
587
+ : [' []']),
588
+ `inputs_missing:`,
589
+ ...(ctx.missingInputs.length > 0
590
+ ? ctx.missingInputs.map(p => ` - ${p}`)
591
+ : [' []']),
592
+ '---',
593
+ '',
594
+ ].join('\n');
595
+ const body = [
596
+ `# ${spec.title}`,
597
+ '',
598
+ '> ⚠️ **GENERATION FAILED — review required**',
599
+ '',
600
+ ctx.consensusBanner,
601
+ '',
602
+ '## Headline figures (from `unit-economics.json`)',
603
+ '',
604
+ ctx.fcvBlock,
605
+ '',
606
+ '## Why this is a skeleton',
607
+ '',
608
+ `Phase 7 attempted to generate this deliverable but failed: \`${ctx.failureReason}\`.`,
609
+ 'The standard consensus banner and fcv block above are still authoritative for this run;',
610
+ 'this skeleton exists so the deliverables/ tree carries one file per spec row even when the',
611
+ 'writer pipeline cannot produce a full narrative.',
612
+ ].join('\n');
613
+ const finalText = `${frontmatter}${body}\n`;
614
+ try {
615
+ fs.mkdirSync(path.dirname(outPath), { recursive: true });
616
+ fs.writeFileSync(outPath, finalText, 'utf-8');
617
+ }
618
+ catch {
619
+ /* nothing more to do — caller records this row as `failed` if write
620
+ * actually threw, but `writeSkeleton` itself does not throw. */
621
+ }
622
+ return outPath;
623
+ }
624
+ function escapeYaml(s) {
625
+ return s.replace(/\r?\n/g, ' ').replace(/"/g, '\\"').slice(0, 200);
626
+ }
627
+ function mirrorExisting(runDir, sourceRel, target) {
628
+ const sourceAbs = path.isAbsolute(sourceRel) ? sourceRel : path.join(runDir, sourceRel);
629
+ if (!fs.existsSync(sourceAbs)) {
630
+ return { ok: false, reason: `mirror-source-missing:${sourceRel}` };
631
+ }
632
+ // If the target already exists from a prior partial run, remove it so the
633
+ // mirror operation is idempotent.
634
+ try {
635
+ if (fs.existsSync(target) || isSymlink(target)) {
636
+ fs.rmSync(target, { force: true });
637
+ }
638
+ }
639
+ catch { /* best-effort */ }
640
+ const useCopy = mirrorModeIsCopy();
641
+ if (!useCopy) {
642
+ try {
643
+ fs.symlinkSync(sourceAbs, target);
644
+ return { ok: true };
645
+ }
646
+ catch (err) {
647
+ // Fall through to copy if symlink permissions are denied.
648
+ void err;
649
+ }
650
+ }
651
+ try {
652
+ fs.copyFileSync(sourceAbs, target);
653
+ return { ok: true };
654
+ }
655
+ catch (err) {
656
+ return { ok: false, reason: err instanceof Error ? err.message : String(err) };
657
+ }
658
+ }
659
+ function isSymlink(p) {
660
+ try {
661
+ return fs.lstatSync(p).isSymbolicLink();
662
+ }
663
+ catch {
664
+ return false;
665
+ }
666
+ }
667
+ function mirrorModeIsCopy() {
668
+ const env = (process.env['AGENTICS_PHASE7_MIRROR'] ?? '').toLowerCase().trim();
669
+ if (env === 'copy')
670
+ return true;
671
+ if (env === 'symlink')
672
+ return false;
673
+ // Default: symlink on POSIX, copy on Windows.
674
+ return os.platform() === 'win32';
675
+ }
676
+ function extractEmittedFcvKinds(body, expected) {
677
+ if (!body || expected.length === 0)
678
+ return [];
679
+ const out = new Set();
680
+ const re = /<!--\s*fcv:kind=([a-zA-Z0-9_-]+)/g;
681
+ let m;
682
+ while ((m = re.exec(body)) !== null) {
683
+ if (m[1])
684
+ out.add(m[1]);
685
+ }
686
+ // Filter to kinds the registry expected, preserving expected ordering.
687
+ return expected.filter(k => out.has(k));
688
+ }
689
+ function buildIndexMd(entries, traceId) {
690
+ const lines = [];
691
+ lines.push('# Consulting Deliverables');
692
+ lines.push('');
693
+ lines.push(`Trace ID: \`${traceId}\``);
694
+ lines.push('');
695
+ lines.push('| # | Section | Slug | Status | Path |');
696
+ lines.push('|---|---|---|---|---|');
697
+ let n = 0;
698
+ for (const e of entries) {
699
+ n += 1;
700
+ const sectionDir = SECTION_DIRS[e.section] ?? `${e.section}`;
701
+ const rel = path.relative(path.dirname(e.output_path) + path.sep + '..', e.output_path).split(path.sep).join('/');
702
+ lines.push(`| ${n} | ${sectionDir} | ${e.slug} | ${e.status} | [${rel}](./${sectionDir}/${path.basename(e.output_path)}) |`);
703
+ }
704
+ lines.push('');
705
+ return lines.join('\n');
706
+ }
707
+ //# sourceMappingURL=coordinator.js.map