@llm-dev-ops/agentics-cli 2.7.17 → 2.7.18
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/pipeline/auto-chain.d.ts.map +1 -1
- package/dist/pipeline/auto-chain.js +70 -1
- package/dist/pipeline/auto-chain.js.map +1 -1
- package/dist/pipeline/phase7/coherence-gate.d.ts +71 -0
- package/dist/pipeline/phase7/coherence-gate.d.ts.map +1 -0
- package/dist/pipeline/phase7/coherence-gate.js +541 -0
- package/dist/pipeline/phase7/coherence-gate.js.map +1 -0
- package/dist/pipeline/phase7/coordinator.d.ts +94 -0
- package/dist/pipeline/phase7/coordinator.d.ts.map +1 -0
- package/dist/pipeline/phase7/coordinator.js +693 -0
- package/dist/pipeline/phase7/coordinator.js.map +1 -0
- package/dist/pipeline/phase7/deliverables-registry.d.ts +97 -0
- package/dist/pipeline/phase7/deliverables-registry.d.ts.map +1 -0
- package/dist/pipeline/phase7/deliverables-registry.js +1132 -0
- package/dist/pipeline/phase7/deliverables-registry.js.map +1 -0
- package/dist/pipeline/phase7/template-renderer.d.ts +81 -0
- package/dist/pipeline/phase7/template-renderer.d.ts.map +1 -0
- package/dist/pipeline/phase7/template-renderer.js +108 -0
- package/dist/pipeline/phase7/template-renderer.js.map +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,693 @@
|
|
|
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('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 the compiled file (dist).
|
|
433
|
+
try {
|
|
434
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
435
|
+
const dirnameLocal = typeof __dirname === 'string' ? __dirname : undefined;
|
|
436
|
+
if (dirnameLocal)
|
|
437
|
+
roots.push(path.join(dirnameLocal, 'deliverables-templates'));
|
|
438
|
+
}
|
|
439
|
+
catch { /* ignore */ }
|
|
440
|
+
// Source-dir form (when running via tsx/ts-node).
|
|
441
|
+
try {
|
|
442
|
+
const cwd = process.cwd();
|
|
443
|
+
roots.push(path.join(cwd, 'src', TEMPLATES_DIR_REL));
|
|
444
|
+
roots.push(path.join(cwd, 'dist', TEMPLATES_DIR_REL));
|
|
445
|
+
}
|
|
446
|
+
catch { /* ignore */ }
|
|
447
|
+
return roots;
|
|
448
|
+
}
|
|
449
|
+
function readConsensusBanner(runDir) {
|
|
450
|
+
// The consensus.json shape is owned by `synthesis/consensus-tiers.ts`. To
|
|
451
|
+
// keep this coordinator dependency-light, we read the pre-rendered banner
|
|
452
|
+
// when it has already been written next to the consensus snapshot, and
|
|
453
|
+
// fall back to a minimal directional placeholder otherwise.
|
|
454
|
+
const candidates = [
|
|
455
|
+
path.join(runDir, 'consensus-banner.md'),
|
|
456
|
+
path.join(runDir, 'consensus-banner.txt'),
|
|
457
|
+
];
|
|
458
|
+
for (const p of candidates) {
|
|
459
|
+
try {
|
|
460
|
+
if (fs.existsSync(p)) {
|
|
461
|
+
const raw = fs.readFileSync(p, 'utf-8').trim();
|
|
462
|
+
if (raw.length > 0)
|
|
463
|
+
return raw;
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
catch { /* try next */ }
|
|
467
|
+
}
|
|
468
|
+
// Fall back to a minimal tier-agnostic banner when we cannot reconstruct.
|
|
469
|
+
// Coherence is preserved because every deliverable carries the SAME string.
|
|
470
|
+
return '> 📊 **Consensus snapshot:** see `consensus.json` for agreement / precision / verdict.';
|
|
471
|
+
}
|
|
472
|
+
function buildFcvBlock(runDir) {
|
|
473
|
+
const ue = loadUnitEconomics(runDir);
|
|
474
|
+
if (!ue.manifest) {
|
|
475
|
+
return '<!-- fcv:kind=unit-economics src=missing -->';
|
|
476
|
+
}
|
|
477
|
+
const m = ue.manifest;
|
|
478
|
+
const measured = formatUsd(m.annual_measured_savings_usd);
|
|
479
|
+
const enterprise = formatUsd(m.annual_extrapolated_savings_usd);
|
|
480
|
+
return [
|
|
481
|
+
'<!-- fcv:kind=savings scope=enterprise doc=phase7 src=unit-economics.json -->',
|
|
482
|
+
`Annual enterprise savings: ${enterprise}`,
|
|
483
|
+
'',
|
|
484
|
+
'<!-- fcv:kind=savings scope=measured doc=phase7 src=unit-economics.json -->',
|
|
485
|
+
`Annual measured savings (pilot): ${measured}`,
|
|
486
|
+
'',
|
|
487
|
+
`<!-- fcv:kind=method src=unit-economics.json -->`,
|
|
488
|
+
`Extrapolation method: ${m.extrapolation_method}`,
|
|
489
|
+
].join('\n');
|
|
490
|
+
}
|
|
491
|
+
function formatUsd(n) {
|
|
492
|
+
if (!Number.isFinite(n) || n <= 0)
|
|
493
|
+
return '$0';
|
|
494
|
+
if (n >= 1_000_000)
|
|
495
|
+
return `$${(n / 1_000_000).toFixed(1)}M`;
|
|
496
|
+
if (n >= 1_000)
|
|
497
|
+
return `$${(n / 1_000).toFixed(0)}K`;
|
|
498
|
+
return `$${Math.round(n)}`;
|
|
499
|
+
}
|
|
500
|
+
function buildAgentAttributionFooter(agents) {
|
|
501
|
+
if (!agents || agents.length === 0)
|
|
502
|
+
return '';
|
|
503
|
+
const visible = agents.slice(0, 6).join(', ');
|
|
504
|
+
const more = agents.length > 6 ? ` + ${agents.length - 6} more` : '';
|
|
505
|
+
return `Sources: ${visible}${more}`;
|
|
506
|
+
}
|
|
507
|
+
function buildDeliverableTask(spec, templateBody, inputs, ctx) {
|
|
508
|
+
const inputSection = inputs.length > 0
|
|
509
|
+
? inputs.map(i => `--- INPUT: ${i.relPath} ---\n${truncate(i.content, 8_000)}\n`).join('\n')
|
|
510
|
+
: '(no input artifacts available — produce a coherent skeleton from the template only)';
|
|
511
|
+
const description = [
|
|
512
|
+
`# Phase 7 Deliverable: ${spec.title}`,
|
|
513
|
+
'',
|
|
514
|
+
`Section ${spec.section} · slug \`${spec.slug}\` · template \`${spec.templateId}\` · format ${spec.format}`,
|
|
515
|
+
'',
|
|
516
|
+
'## Project scenario',
|
|
517
|
+
ctx.scenarioQuery,
|
|
518
|
+
'',
|
|
519
|
+
'## Consensus banner (must appear verbatim at top of output)',
|
|
520
|
+
ctx.consensusBanner,
|
|
521
|
+
'',
|
|
522
|
+
'## Standard fcv block (must appear verbatim where the template has {{fcvBlock}})',
|
|
523
|
+
ctx.fcvBlock,
|
|
524
|
+
'',
|
|
525
|
+
'## Source agents to attribute',
|
|
526
|
+
spec.sourceAgents.length > 0 ? spec.sourceAgents.join(', ') : '(none specified)',
|
|
527
|
+
'',
|
|
528
|
+
'## Required fcv kinds',
|
|
529
|
+
spec.fcvKinds.length > 0 ? spec.fcvKinds.join(', ') : '(none — purely qualitative)',
|
|
530
|
+
'',
|
|
531
|
+
'## Template body',
|
|
532
|
+
templateBody,
|
|
533
|
+
'',
|
|
534
|
+
'## Input artifacts',
|
|
535
|
+
inputSection,
|
|
536
|
+
'',
|
|
537
|
+
'## Rules',
|
|
538
|
+
'1. Fill every {{placeholder}} in the template with content derived from the inputs.',
|
|
539
|
+
'2. Do not invent dollar figures. Numbers MUST come from the fcv block above.',
|
|
540
|
+
'3. The output is a single Markdown document. Do not wrap it in a code fence.',
|
|
541
|
+
'4. This is a LOCAL-ONLY run. The dispatch already injects --local-only.',
|
|
542
|
+
].join('\n');
|
|
543
|
+
return {
|
|
544
|
+
label: `phase7-${spec.slug}`,
|
|
545
|
+
description,
|
|
546
|
+
targetDir: 'deliverables',
|
|
547
|
+
argv: ['--local-only'],
|
|
548
|
+
};
|
|
549
|
+
}
|
|
550
|
+
function truncate(s, max) {
|
|
551
|
+
if (s.length <= max)
|
|
552
|
+
return s;
|
|
553
|
+
return `${s.slice(0, max)}\n…[truncated ${s.length - max} bytes]`;
|
|
554
|
+
}
|
|
555
|
+
function primaryFormatExt(format) {
|
|
556
|
+
// Some registry rows declare composite formats like `md+csv`; pick the
|
|
557
|
+
// first as the primary file. Companion outputs (csv/svg) are written by
|
|
558
|
+
// the deliverable-specific renderer when needed.
|
|
559
|
+
const head = String(format).split('+')[0]?.trim() ?? 'md';
|
|
560
|
+
return head;
|
|
561
|
+
}
|
|
562
|
+
function writeSkeleton(outPath, spec, ctx) {
|
|
563
|
+
const frontmatter = [
|
|
564
|
+
'---',
|
|
565
|
+
'degraded_mode: generation-failed',
|
|
566
|
+
`failure_reason: ${escapeYaml(ctx.failureReason)}`,
|
|
567
|
+
`section: ${spec.section}`,
|
|
568
|
+
`slug: ${spec.slug}`,
|
|
569
|
+
`template_id: ${spec.templateId}`,
|
|
570
|
+
`inputs_available:`,
|
|
571
|
+
...(ctx.availableInputs.length > 0
|
|
572
|
+
? ctx.availableInputs.map(i => ` - ${i.relPath}`)
|
|
573
|
+
: [' []']),
|
|
574
|
+
`inputs_missing:`,
|
|
575
|
+
...(ctx.missingInputs.length > 0
|
|
576
|
+
? ctx.missingInputs.map(p => ` - ${p}`)
|
|
577
|
+
: [' []']),
|
|
578
|
+
'---',
|
|
579
|
+
'',
|
|
580
|
+
].join('\n');
|
|
581
|
+
const body = [
|
|
582
|
+
`# ${spec.title}`,
|
|
583
|
+
'',
|
|
584
|
+
'> ⚠️ **GENERATION FAILED — review required**',
|
|
585
|
+
'',
|
|
586
|
+
ctx.consensusBanner,
|
|
587
|
+
'',
|
|
588
|
+
'## Headline figures (from `unit-economics.json`)',
|
|
589
|
+
'',
|
|
590
|
+
ctx.fcvBlock,
|
|
591
|
+
'',
|
|
592
|
+
'## Why this is a skeleton',
|
|
593
|
+
'',
|
|
594
|
+
`Phase 7 attempted to generate this deliverable but failed: \`${ctx.failureReason}\`.`,
|
|
595
|
+
'The standard consensus banner and fcv block above are still authoritative for this run;',
|
|
596
|
+
'this skeleton exists so the deliverables/ tree carries one file per spec row even when the',
|
|
597
|
+
'writer pipeline cannot produce a full narrative.',
|
|
598
|
+
].join('\n');
|
|
599
|
+
const finalText = `${frontmatter}${body}\n`;
|
|
600
|
+
try {
|
|
601
|
+
fs.mkdirSync(path.dirname(outPath), { recursive: true });
|
|
602
|
+
fs.writeFileSync(outPath, finalText, 'utf-8');
|
|
603
|
+
}
|
|
604
|
+
catch {
|
|
605
|
+
/* nothing more to do — caller records this row as `failed` if write
|
|
606
|
+
* actually threw, but `writeSkeleton` itself does not throw. */
|
|
607
|
+
}
|
|
608
|
+
return outPath;
|
|
609
|
+
}
|
|
610
|
+
function escapeYaml(s) {
|
|
611
|
+
return s.replace(/\r?\n/g, ' ').replace(/"/g, '\\"').slice(0, 200);
|
|
612
|
+
}
|
|
613
|
+
function mirrorExisting(runDir, sourceRel, target) {
|
|
614
|
+
const sourceAbs = path.isAbsolute(sourceRel) ? sourceRel : path.join(runDir, sourceRel);
|
|
615
|
+
if (!fs.existsSync(sourceAbs)) {
|
|
616
|
+
return { ok: false, reason: `mirror-source-missing:${sourceRel}` };
|
|
617
|
+
}
|
|
618
|
+
// If the target already exists from a prior partial run, remove it so the
|
|
619
|
+
// mirror operation is idempotent.
|
|
620
|
+
try {
|
|
621
|
+
if (fs.existsSync(target) || isSymlink(target)) {
|
|
622
|
+
fs.rmSync(target, { force: true });
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
catch { /* best-effort */ }
|
|
626
|
+
const useCopy = mirrorModeIsCopy();
|
|
627
|
+
if (!useCopy) {
|
|
628
|
+
try {
|
|
629
|
+
fs.symlinkSync(sourceAbs, target);
|
|
630
|
+
return { ok: true };
|
|
631
|
+
}
|
|
632
|
+
catch (err) {
|
|
633
|
+
// Fall through to copy if symlink permissions are denied.
|
|
634
|
+
void err;
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
try {
|
|
638
|
+
fs.copyFileSync(sourceAbs, target);
|
|
639
|
+
return { ok: true };
|
|
640
|
+
}
|
|
641
|
+
catch (err) {
|
|
642
|
+
return { ok: false, reason: err instanceof Error ? err.message : String(err) };
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
function isSymlink(p) {
|
|
646
|
+
try {
|
|
647
|
+
return fs.lstatSync(p).isSymbolicLink();
|
|
648
|
+
}
|
|
649
|
+
catch {
|
|
650
|
+
return false;
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
function mirrorModeIsCopy() {
|
|
654
|
+
const env = (process.env['AGENTICS_PHASE7_MIRROR'] ?? '').toLowerCase().trim();
|
|
655
|
+
if (env === 'copy')
|
|
656
|
+
return true;
|
|
657
|
+
if (env === 'symlink')
|
|
658
|
+
return false;
|
|
659
|
+
// Default: symlink on POSIX, copy on Windows.
|
|
660
|
+
return os.platform() === 'win32';
|
|
661
|
+
}
|
|
662
|
+
function extractEmittedFcvKinds(body, expected) {
|
|
663
|
+
if (!body || expected.length === 0)
|
|
664
|
+
return [];
|
|
665
|
+
const out = new Set();
|
|
666
|
+
const re = /<!--\s*fcv:kind=([a-zA-Z0-9_-]+)/g;
|
|
667
|
+
let m;
|
|
668
|
+
while ((m = re.exec(body)) !== null) {
|
|
669
|
+
if (m[1])
|
|
670
|
+
out.add(m[1]);
|
|
671
|
+
}
|
|
672
|
+
// Filter to kinds the registry expected, preserving expected ordering.
|
|
673
|
+
return expected.filter(k => out.has(k));
|
|
674
|
+
}
|
|
675
|
+
function buildIndexMd(entries, traceId) {
|
|
676
|
+
const lines = [];
|
|
677
|
+
lines.push('# Consulting Deliverables');
|
|
678
|
+
lines.push('');
|
|
679
|
+
lines.push(`Trace ID: \`${traceId}\``);
|
|
680
|
+
lines.push('');
|
|
681
|
+
lines.push('| # | Section | Slug | Status | Path |');
|
|
682
|
+
lines.push('|---|---|---|---|---|');
|
|
683
|
+
let n = 0;
|
|
684
|
+
for (const e of entries) {
|
|
685
|
+
n += 1;
|
|
686
|
+
const sectionDir = SECTION_DIRS[e.section] ?? `${e.section}`;
|
|
687
|
+
const rel = path.relative(path.dirname(e.output_path) + path.sep + '..', e.output_path).split(path.sep).join('/');
|
|
688
|
+
lines.push(`| ${n} | ${sectionDir} | ${e.slug} | ${e.status} | [${rel}](./${sectionDir}/${path.basename(e.output_path)}) |`);
|
|
689
|
+
}
|
|
690
|
+
lines.push('');
|
|
691
|
+
return lines.join('\n');
|
|
692
|
+
}
|
|
693
|
+
//# sourceMappingURL=coordinator.js.map
|