amsd-pipeline-shared 0.3.0 → 0.4.0

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.
@@ -6,6 +6,7 @@
6
6
  // amsd-pipeline-shared verify --into <install> every file is this package's (exit 1 and the list when not)
7
7
  // amsd-pipeline-shared revert --into <install> put back the files this package replaced
8
8
  // amsd-pipeline-shared messages send|receive|list --dir <bus> … the agent-to-agent bus (lib/agent-messages.js)
9
+ // amsd-pipeline-shared prelaunch --install <i> --project <p> --run-id <id> --out <dir> … the shared pre-launch
9
10
  // amsd-pipeline-shared version
10
11
  // A file is replaced only when the install holds the released copy this package was built from (docs/gateway-baseline-
11
12
  // 2.0.80.md5) or this package's own copy (a re-install); anything else is refused, file by file, and nothing is written.
@@ -23,6 +24,8 @@ const fail = (m, code = 1) => { process.stderr.write(`amsd-pipeline-shared: ${m}
23
24
 
24
25
  const [cmd, ...rest] = process.argv.slice(2);
25
26
  if (cmd === 'version') { process.stdout.write(`${PKG.version}\n`); process.exit(0); }
27
+ // THE SHARED PRE-LAUNCH, for every pipeline (lib/prelaunch.js): amsd-pipeline-shared prelaunch --install <i> --project <p> …
28
+ if (cmd === 'prelaunch') process.exit(require(path.join(ROOT, 'orchestrations', 'scripts', 'lib', 'prelaunch.js')).prelaunchCli(rest));
26
29
  // THE MESSAGE BUS FOR SHELL CALLERS (the bash pipeline's adoption, backlog B55): one JSON line out
27
30
  if (cmd === 'messages') {
28
31
  const m = require(path.join(ROOT, 'orchestrations', 'scripts', 'lib', 'agent-messages.js'));
@@ -24,6 +24,7 @@ c0a3a88d1a895e0a86ea483cada15411 scripts/lib/kb-store.js
24
24
  ed15fbd317db2643b19a3fd7ecd1979a scripts/lib/model-ladder.sh
25
25
  4d2dc2a636736b5b77711ad7736f9075 scripts/lib/model-ladders.sh
26
26
  97d25b9108a01292d1b28da0d3c129e9 scripts/lib/pipeline-tools.js
27
+ c806c7a54ebfb7ea178ef8415bb70c1c scripts/lib/preflight.sh
27
28
  6bfffd3ba6eb845f738d3157168bf056 scripts/lib/relative-paths.js
28
29
  6894e248c7a86d07626b7e981d534354 scripts/lib/render-prompt-section.js
29
30
  912511e457b0a6f4801c479400b1c3a3 scripts/lib/seam-invocation.js
@@ -33,6 +34,9 @@ ed15fbd317db2643b19a3fd7ecd1979a scripts/lib/model-ladder.sh
33
34
  be6d0a378fa9c3bc9da6240877aaeae3 scripts/lib/tool-timeouts.js
34
35
  5574182fb27aaeabbfff261b3a8d6d39 scripts/lib/transcript-tool-calls.js
35
36
  c4b96230c15b52a845cf20fd3266fe01 scripts/llm-handler.sh
37
+ 9b05c5b99b4f803c3a568ce5c069e6d3 scripts/preflight-check.sh
38
+ 740a55fe2ae0527a0598f3c751bacca7 scripts/preflight-prd-integrity.sh
39
+ bc5acdb9260923082e05ef3b67304028 scripts/preflight-static.sh
36
40
  f206025f304e8320b8417a6dd6606044 scripts/spec-mode-runner.js
37
41
  dfa070b168a7c01fab8aab5a46d16192 scripts/team-lead-review.sh
38
42
  40c65de8360d5a9f59902fca26d1d7a8 scripts/update-monitor.sh
@@ -25,6 +25,8 @@ scripts/lib/llm-settings-resolve.js
25
25
  scripts/lib/model-ladder.sh
26
26
  scripts/lib/model-ladders.sh
27
27
  scripts/lib/pipeline-tools.js
28
+ scripts/lib/preflight.sh
29
+ scripts/lib/prelaunch.js
28
30
  scripts/lib/relative-paths.js
29
31
  scripts/lib/render-prompt-section.js
30
32
  scripts/lib/seam-invocation.js
@@ -34,6 +36,9 @@ scripts/lib/stage-coverage-gate.sh
34
36
  scripts/lib/tool-timeouts.js
35
37
  scripts/lib/transcript-tool-calls.js
36
38
  scripts/llm-handler.sh
39
+ scripts/preflight-check.sh
40
+ scripts/preflight-prd-integrity.sh
41
+ scripts/preflight-static.sh
37
42
  scripts/spec-mode-runner.js
38
43
  scripts/team-lead-review.sh
39
44
  scripts/update-monitor.sh
@@ -0,0 +1,65 @@
1
+ #!/usr/bin/env bash
2
+ # preflight.sh — run the pre-launch assessment from any launcher, with no launcher having
3
+ # to know the check script's arguments.
4
+ #
5
+ # preflight-check.sh existed with six sections and was wired into exactly TWO of the eight
6
+ # launchers. The two being run daily (metrolinx, mock) were not among them, which is why
7
+ # four separate launch failures on 2026-08-05 — a stale dist twice, a dead observability
8
+ # stack, a project with no synthesis template — were each discovered by spending a launch
9
+ # instead of by the check that exists to prevent exactly that.
10
+ #
11
+ # The runner name and the project config directory are DERIVED here rather than passed:
12
+ # a launcher that had to repeat them is a launcher that can get them wrong, and one already
13
+ # did — tier3-skyscanner-app-run.sh passed --runner tier3-travel-app-run.sh, so it
14
+ # pre-flighted a different launcher than the one it was about to run.
15
+ #
16
+ # Usage: . lib/preflight.sh ; require_preflight # aborts the launch if anything fails
17
+
18
+ require_preflight() {
19
+ local script_dir="${SCRIPT_DIR:-$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)}"
20
+ local check="$script_dir/preflight-check.sh"
21
+
22
+ if [ ! -f "$check" ]; then
23
+ echo " ✗ preflight-check.sh not found at $check — refusing to launch unassessed" >&2
24
+ return 1
25
+ fi
26
+
27
+ local -a args=()
28
+ # The runner is whichever launcher sourced this file. $0 is that launcher.
29
+ args+=(--runner "$(basename "${0:-unknown}")")
30
+ [ -n "${PRD_FILE:-}" ] && args+=(--prd "$PRD_FILE")
31
+ [ -n "${EPAM_PROJECT_CONFIG_DIR:-}" ] && args+=(--project-config "$EPAM_PROJECT_CONFIG_DIR")
32
+
33
+ echo "── Pre-flight assessment ─────────────────────────────────────────────────────"
34
+ if bash "$check" "${args[@]}"; then
35
+ # THE SHARED PRE-LAUNCH (amsd-pipeline-shared, operator 2026-10-19: one pre-launch for every pipeline): the checks
36
+ # every launch owes after the pipeline's own — Langfuse, the seeded ticket, the codeline cache, cost, nothing else
37
+ # running, memory, no clock above an agent, the 12-item checklist. Skipped only when it is the shared pre-launch
38
+ # that is running these checks (AMSD_PRELAUNCH_INNER), so the two never loop.
39
+ [ "${AMSD_PRELAUNCH_INNER:-}" = "1" ] && return 0
40
+ local _pl="$script_dir/lib/prelaunch.js" _install _plargs=()
41
+ _install="$(cd "$script_dir/../.." && pwd)"
42
+ if [ ! -f "$_pl" ]; then
43
+ echo " ✗ the shared pre-launch is not installed ($_pl) — refusing to launch unassessed" >&2
44
+ return 1
45
+ fi
46
+ _plargs=(--install "$_install" --project "$(basename "${EPAM_PROJECT_CONFIG_DIR:-${PROJECT_CONFIG_DIR:-unknown}}")"
47
+ --run-id "${ORCH_RUN_ID:-prelaunch-$(date -u +%Y%m%dT%H%M%SZ)}" --out "${LOG_DIR:-$_install/orchestrations/logs}/prelaunch"
48
+ --runner "$(basename "${0:-unknown}")" --preflight no)
49
+ [ -n "${JIRA_TICKET:-}" ] && [ -n "${AMSD_PRELAUNCH_RUNS:-}" ] && _plargs+=(--ticket "$JIRA_TICKET" --runs "$AMSD_PRELAUNCH_RUNS")
50
+ [ -n "${EPAM_CODELINE_ID:-}" ] && _plargs+=(--codeline "$EPAM_CODELINE_ID")
51
+ [ -n "${EPAM_PROVIDER_SET:-}" ] && _plargs+=(--set "$EPAM_PROVIDER_SET")
52
+ if "${NODE_BIN:-node}" "$_pl" "${_plargs[@]}"; then
53
+ return 0
54
+ fi
55
+ echo "" >&2
56
+ echo " ✗ Pre-launch FAILED — aborting before spending. Fix the items above." >&2
57
+ return 1
58
+ fi
59
+
60
+ # Deliberately not skippable by an env var. Every failure this catches cost a launch
61
+ # cycle or credits; an escape hatch here would be used on the run that needed it most.
62
+ echo "" >&2
63
+ echo " ✗ Pre-flight FAILED — aborting before spending. Fix the items above." >&2
64
+ return 1
65
+ }
@@ -0,0 +1,294 @@
1
+ 'use strict';
2
+ // THE PRE-LAUNCH, SHARED BY EVERY PIPELINE (operator, 2026-10-19: "Make pre-launch a common component. It needs to be
3
+ // shared between all pipelines."). Before any model call: the pipeline's own pre-flight (preflight-check.sh through
4
+ // lib/preflight.sh require_preflight, unchanged) and the checks the operator approved the same day, each from a failure
5
+ // that reached a live launch:
6
+ // 1 langfuse Langfuse answers and a launch trace is written and read back under the run's session id
7
+ // 2 seed the seeded prompts and agent files are the launched ticket's (byte-identical to a recorded run of it)
8
+ // 3 cache the codeline cache carries both layers: the prompt set and the per-prompt entries the builder reuses
9
+ // 4 cost the ledger writer records a priced call, and every model on the active ladders has a price
10
+ // 5 alone no other pipeline run or test is working against this install
11
+ // 6 memory the host can give the run what it needs and keep 3 GB for itself
12
+ // 7 no-timeout nothing above the launch can cut an agent off on a clock
13
+ // 8 checklist the operator's 12-item run checklist, scored: green / red / not-yet (decided in the run)
14
+ // Each check returns {name, ok, detail}; nothing here makes a model call. Any red stops the launch.
15
+ const fs = require('fs');
16
+ const os = require('os');
17
+ const path = require('path');
18
+ const crypto = require('crypto');
19
+ const { spawnSync } = require('child_process');
20
+
21
+ const md5 = (f) => crypto.createHash('md5').update(fs.readFileSync(f)).digest('hex');
22
+ const readJson = (f, d = null) => { try { return JSON.parse(fs.readFileSync(f, 'utf8')); } catch { return d; } };
23
+ const orchOf = (install) => path.join(install, 'orchestrations');
24
+ const scriptsOf = (install) => path.join(orchOf(install), 'scripts');
25
+ const res = (name, ok, detail, extra = {}) => ({ name, ok: !!ok, detail, ...extra });
26
+
27
+ // ── 0. the pipeline's own pre-flight, unchanged ──────────────────────────────────────────────────────────────────────
28
+ function preflight({ install, env, runner, prd }) {
29
+ const sd = scriptsOf(install);
30
+ // require_preflight names the runner from $0 (the launcher that sourced it): the runner's own name is passed as $0
31
+ // AMSD_PRELAUNCH_INNER: require_preflight runs this pre-launch after its own checks (every bash launcher); called from
32
+ // here, it runs the pipeline's checks alone — no loop
33
+ const r = spawnSync('bash', ['-c', '. "$1/lib/preflight.sh" && require_preflight', runner || 'orchestrate.sh', sd],
34
+ { env: { ...env, SCRIPT_DIR: sd, AMSD_PRELAUNCH_INNER: '1', ...(prd ? { PRD_FILE: prd } : {}) }, encoding: 'utf8', maxBuffer: 32 * 1024 * 1024 });
35
+ const out = `${r.stdout || ''}${r.stderr || ''}`;
36
+ const failed = (out.match(/^\s*✗ .*$/gm) || []).map((l) => l.trim());
37
+ return res('preflight', r.status === 0, r.status === 0 ? `the pipeline's pre-flight passed (${(out.match(/^\s*✓ /gm) || []).length} checks)` : `${failed.length} failed: ${failed.slice(0, 6).join(' | ')}`, { log: out });
38
+ }
39
+
40
+ // ── 1. langfuse ──────────────────────────────────────────────────────────────────────────────────────────────────────
41
+ /** The install's LANGFUSE_* settings, added to `env` where `env` has none. */
42
+ function langfuseEnv(install, env) {
43
+ const out = { ...env };
44
+ for (const l of (fs.existsSync(path.join(install, '.env')) ? fs.readFileSync(path.join(install, '.env'), 'utf8') : '').split('\n')) {
45
+ const m = l.match(/^(LANGFUSE_[A-Z_]+)=(.*)$/); if (m && !out[m[1]]) out[m[1]] = m[2].replace(/^["']|["']$/g, '');
46
+ }
47
+ return out;
48
+ }
49
+ function langfuse({ install, env, runId, outDir, traceName = 'launch-prelaunch', waitMs = 2000, tries = 6 }) {
50
+ const e = langfuseEnv(install, env);
51
+ const lf = require(path.join(scriptsOf(install), 'lib', 'langfuse-emit.js'));
52
+ const cfg = lf.config(e);
53
+ const probe = cfg ? spawnSync('curl', ['-s', '-o', '/dev/null', '-w', '%{http_code}', '--max-time', '5', `${cfg.base}/api/public/health`], { encoding: 'utf8' }).stdout : '';
54
+ if (probe !== '200') return res('langfuse', false, `Langfuse is required and is not answering (${cfg ? `${cfg.base} → ${probe || 'no answer'}` : 'not configured: no keys or endpoint'})`);
55
+ const session = lf.sessionId({ ...e, ORCH_RUN_ID: runId });
56
+ fs.mkdirSync(outDir, { recursive: true });
57
+ const marker = path.join(outDir, `langfuse-${traceName}.js`);
58
+ fs.writeFileSync(marker, `require(${JSON.stringify(path.join(scriptsOf(install), 'lib', 'langfuse-emit.js'))}).emitGeneration({ agent: ${JSON.stringify(traceName)}, model: 'none', costUsd: 0, tokensIn: 0, tokensOut: 0, input: ${JSON.stringify(`pre-launch of ${runId}`)}, output: 'launch' }, process.env).then((ok) => process.exit(ok ? 0 : 1))`);
59
+ const sent = spawnSync(process.execPath, [marker], { env: { ...process.env, ...e, ORCH_RUN_ID: runId }, encoding: 'utf8' }).status === 0;
60
+ let found = 0;
61
+ for (let i = 0; i < tries && sent && !found; i += 1) {
62
+ spawnSync('sleep', [String(waitMs / 1000)]);
63
+ const q = spawnSync('curl', ['-s', '--max-time', '5', '-u', `${e.LANGFUSE_PUBLIC_KEY}:${e.LANGFUSE_SECRET_KEY}`, `${cfg.base}/api/public/traces?sessionId=${encodeURIComponent(session)}&name=${traceName}&limit=5`], { encoding: 'utf8' }).stdout;
64
+ try { found = (JSON.parse(q).data || []).filter((t) => t.sessionId === session && t.name === traceName).length; } catch { found = 0; }
65
+ }
66
+ return found ? res('langfuse', true, `trace '${traceName}' read back under session ${session} (${cfg.base})`, { session, base: cfg.base })
67
+ : res('langfuse', false, `no trace under Langfuse session ${session} could be read back (${sent ? 'sent, not found' : 'not sent'})`);
68
+ }
69
+
70
+ // ── 2. seed ──────────────────────────────────────────────────────────────────────────────────────────────────────────
71
+ function seededFiles(install, project) {
72
+ const pcd = path.join(orchOf(install), 'projects', project);
73
+ const { MINTED_FILES } = require(path.join(scriptsOf(install), 'lib', 'project-prompt-builder.js'));
74
+ const out = []; const pd = path.join(pcd, 'prompts');
75
+ if (fs.existsSync(pd)) for (const f of fs.readdirSync(pd).filter((n) => n.endsWith('.json')).sort()) out.push({ rel: path.join('prompts', f), abs: path.join(pd, f), name: f, kind: 'prompt' });
76
+ for (const f of MINTED_FILES) if (fs.existsSync(path.join(pcd, f))) out.push({ rel: f, abs: path.join(pcd, f), name: f, kind: 'minted' });
77
+ return out;
78
+ }
79
+ const prdNames = (prd, ticket) => ((prd && (prd.stories || prd.userStories)) || []).some((s) => s && (s.id === ticket || s.jiraKey === ticket));
80
+ function ticketSnapshots(runsRoots, ticket) {
81
+ const snaps = [];
82
+ for (const root of runsRoots) {
83
+ let runs = []; try { runs = fs.readdirSync(root); } catch { continue; }
84
+ for (const r of runs.sort()) {
85
+ const rd = path.join(root, r);
86
+ if (!prdNames(readJson(path.join(rd, 'checkpoint', 'prd.json')), ticket)) continue;
87
+ const walk = (d, depth) => {
88
+ if (depth > 6) return;
89
+ let ents = []; try { ents = fs.readdirSync(d, { withFileTypes: true }); } catch { return; }
90
+ for (const e of ents) if (e.isDirectory()) { const p = path.join(d, e.name); if (e.name === 'checkpoint') snaps.push({ run: rd, dir: p }); else walk(p, depth + 1); }
91
+ };
92
+ walk(rd, 0);
93
+ }
94
+ }
95
+ return snaps;
96
+ }
97
+ const snapshotCopy = (snap, f) => (f.kind === 'prompt' ? [path.join(snap.dir, 'prompts', f.name)] : [path.join(snap.dir, f.name), path.join(snap.dir, 'reviewed', f.name)]).find((p) => fs.existsSync(p)) || null;
98
+ function seed({ install, project, ticket, runsRoots }) {
99
+ const files = seededFiles(install, project);
100
+ if (!files.length) return res('seed', false, 'nothing is seeded — no prompts and no minted files on disk');
101
+ const prefix = String(ticket).replace(/-\d+$/, '').replace(/[^A-Za-z0-9]/g, '');
102
+ const keyRe = new RegExp(`\\b${prefix}-\\d+\\b`, 'g');
103
+ const otherTickets = files.map((f) => ({ file: f.rel, keys: [...new Set((fs.readFileSync(f.abs, 'utf8').match(keyRe) || []).filter((k) => k !== ticket))] })).filter((o) => o.keys.length);
104
+ const snaps = ticketSnapshots(runsRoots, ticket);
105
+ if (!snaps.length) return res('seed', false, `no recorded run of ${ticket} under ${runsRoots.join(', ')}`, { otherTickets });
106
+ const best = snaps.map((s) => {
107
+ const mismatched = []; const unproven = []; let same = 0;
108
+ for (const f of files) { const c = snapshotCopy(s, f); if (!c) unproven.push(f.rel); else if (md5(c) === md5(f.abs)) same += 1; else mismatched.push(f.rel); }
109
+ return { s, same, mismatched, unproven };
110
+ }).sort((x, y) => y.same - x.same)[0];
111
+ const snapshot = path.relative(path.dirname(best.s.run), best.s.dir);
112
+ const ok = !best.mismatched.length && !best.unproven.length && !otherTickets.length;
113
+ const why = [best.mismatched.length && `${best.mismatched.length} differ from ${snapshot} (${best.mismatched.slice(0, 4).join(', ')})`,
114
+ best.unproven.length && `${best.unproven.length} have no copy in it`, otherTickets.length && `${otherTickets.length} name another ticket`].filter(Boolean).join('; ');
115
+ return res('seed', ok, ok ? `all ${files.length} seeded file(s) are ${ticket}'s (${snapshot}), no other ticket named` : `not proven ${ticket}'s: ${why}`,
116
+ { files: files.length, snapshot, mismatched: best.mismatched, unproven: best.unproven, otherTickets });
117
+ }
118
+
119
+ // ── 3. cache ─────────────────────────────────────────────────────────────────────────────────────────────────────────
120
+ /**
121
+ * The prompts the builder GENERATES for a codeline, and so must find a per-prompt entry for to reuse: its own provisioning
122
+ * list, less the templates it installs as they are (copyVerbatim, and multi-body templates), present in the cached set.
123
+ * An entry is <cache>/<id>.json, or <id>.<variant>.json for a variant run (brownfield).
124
+ */
125
+ function cache({ install, project, codeline, variant = process.env.EPAM_BROWNFIELD === '1' ? 'brownfield' : '' }) {
126
+ const O = orchOf(install);
127
+ const C = path.join(O, 'projects', project, '.prompt-cache', codeline);
128
+ const b = require(path.join(scriptsOf(install), 'lib', 'project-prompt-builder.js'));
129
+ const tpl = (t) => path.join(O, 'prompts', 'templates', `${t}.json`);
130
+ const problems = [];
131
+ const prompts = fs.existsSync(path.join(C, 'prompts')) ? fs.readdirSync(path.join(C, 'prompts')).filter((n) => n.endsWith('.json')).map((n) => n.replace(/\.json$/, '')) : [];
132
+ if (!prompts.length) problems.push(`no cached prompt set (${path.join(C, 'prompts')})`);
133
+ const minted = b.MINTED_FILES.filter((f) => !fs.existsSync(path.join(C, 'minted', f)));
134
+ if (minted.length) problems.push(`minted files missing: ${minted.join(', ')}`);
135
+ const marker = path.join(O, 'projects', project, '.prompt-cache', `.complete-${codeline}${variant ? `.${variant}` : ''}`);
136
+ if (!fs.existsSync(marker)) problems.push(`no completion marker ${path.basename(marker)}`);
137
+ const list = b.provisioningList({ bootstrap: readJson(path.join(O, 'prompts', 'bootstrap.json'), {}), registry: readJson(path.join(O, 'agents', 'invocation-profiles.json'), {}), templateExists: (t) => fs.existsSync(tpl(t)) });
138
+ const multi = (t) => Object.prototype.hasOwnProperty.call(readJson(tpl(t), {}), 'bodies');
139
+ const required = list.filter((i) => prompts.includes(i) && !multi(i));
140
+ const has = (i) => fs.existsSync(path.join(C, `${i}.json`)) || (variant && fs.existsSync(path.join(C, `${i}.${variant}.json`)));
141
+ const missing = required.filter((i) => !has(i));
142
+ if (missing.length) problems.push(`${missing.length} of ${required.length} generated prompt(s) have no per-prompt entry, so any template change rebuilds them all (${missing.slice(0, 5).join(', ')}${missing.length > 5 ? ', …' : ''})`);
143
+ return res('cache', !problems.length, problems.length ? problems.join('; ') : `${codeline}: ${prompts.length} cached prompt(s), ${b.MINTED_FILES.length} minted file(s), ${required.length} per-prompt entries, marker present`,
144
+ { required: required.length, missing });
145
+ }
146
+
147
+ // ── 4. cost ──────────────────────────────────────────────────────────────────────────────────────────────────────────
148
+ /** Every model a provider set's ladders name (startModel, model, models, from/to), from its own declaration. */
149
+ function ladderModels(install, set) {
150
+ const d = readJson(path.join(orchOf(install), 'config', `llm-defaults.${set}.json`), {});
151
+ const out = new Set();
152
+ const walk = (v, key) => {
153
+ if (Array.isArray(v)) { for (const x of v) walk(x, key); return; }
154
+ if (v && typeof v === 'object') { for (const [k, x] of Object.entries(v)) walk(x, k); return; }
155
+ if (typeof v === 'string' && /model|^from$|^to$/i.test(key || '')) out.add(v);
156
+ };
157
+ walk(d.ladders || {}, '');
158
+ return [...out].sort();
159
+ }
160
+ function cost({ install, set, tmpDir }) {
161
+ const problems = [];
162
+ // (a) the ledger writer records a priced call
163
+ const ledger = path.join(tmpDir, 'prelaunch-ledger.jsonl');
164
+ fs.mkdirSync(tmpDir, { recursive: true }); fs.rmSync(ledger, { force: true });
165
+ try {
166
+ const { appendLedgerRecord } = require(path.join(scriptsOf(install), 'lib', 'cost-emitter.js'));
167
+ appendLedgerRecord({ ledgerFile: ledger, agent: 'prelaunch-cost-probe', phase: 'prelaunch', model: 'prelaunch-probe', cost: { costUsd: 0.0123, tokensIn: 10, tokensOut: 5 } });
168
+ const rows = fs.readFileSync(ledger, 'utf8').trim().split('\n').filter(Boolean).map((l) => JSON.parse(l));
169
+ if (!(rows.length === 1 && Number(rows[0].task_cost_usd) === 0.0123)) problems.push(`the ledger writer did not record the probe's cost (${JSON.stringify(rows[0] || null).slice(0, 160)})`);
170
+ } catch (e) { problems.push(`the ledger writer failed: ${e.message}`); }
171
+ // (b) every ladder model is priced, so a call whose session never reports its cost (a killed leg) can still be priced
172
+ const prices = (readJson(path.join(orchOf(install), 'config', 'model-pricing.json'), {}) || {}).models || {};
173
+ const models = ladderModels(install, set);
174
+ const unpriced = models.filter((m) => !prices[m] || prices[m].inputPerMillion == null || prices[m].outputPerMillion == null);
175
+ if (!models.length) problems.push(`the ${set} set's ladders name no model`);
176
+ if (unpriced.length) problems.push(`no price in config/model-pricing.json for ladder model(s) ${unpriced.join(', ')} — a leg killed before its session reports a cost cannot be priced`);
177
+ return res('cost', !problems.length, problems.length ? problems.join('; ') : `the ledger records a priced call; all ${models.length} ladder model(s) priced`, { models, unpriced });
178
+ }
179
+
180
+ // ── 5. alone ─────────────────────────────────────────────────────────────────────────────────────────────────────────
181
+ function ancestors(pid, procDir = '/proc') {
182
+ const out = new Set();
183
+ let p = String(pid);
184
+ for (let i = 0; i < 64 && p && p !== '0'; i += 1) {
185
+ out.add(p);
186
+ let st = ''; try { st = fs.readFileSync(`${procDir}/${p}/stat`, 'utf8'); } catch { break; }
187
+ p = st.slice(st.lastIndexOf(')') + 2).split(' ')[1];
188
+ }
189
+ return out;
190
+ }
191
+ const cmdline = (pid, procDir = '/proc') => { try { return fs.readFileSync(`${procDir}/${pid}/cmdline`, 'utf8').split('\0').filter(Boolean); } catch { return []; } };
192
+ function alone({ install, selfPid = process.pid, procDir = '/proc' }) {
193
+ const mine = ancestors(selfPid, procDir);
194
+ const inst = path.resolve(install);
195
+ const others = [];
196
+ let pids = []; try { pids = fs.readdirSync(procDir).filter((n) => /^\d+$/.test(n)); } catch { pids = []; }
197
+ for (const p of pids) {
198
+ if (mine.has(p)) continue;
199
+ const a = cmdline(p, procDir); if (!a.length) continue;
200
+ const s = a.join(' ');
201
+ let cwd = ''; try { cwd = fs.readlinkSync(`${procDir}/${p}/cwd`); } catch { cwd = ''; }
202
+ // a pipeline process of THIS install: anything running its scripts or a run against it; my own shell is excluded above
203
+ if ((s.includes(inst) || cwd.startsWith(inst)) && /\b(node|bash|sh|python3?|claude)\b/.test(a[0] + ' ' + (a[1] || ''))) others.push(`${p} ${s.slice(0, 140)}`);
204
+ }
205
+ return res('alone', !others.length, others.length ? `${others.length} other process(es) are working against ${inst}: ${others.slice(0, 3).join(' | ')}` : `nothing else is working against ${inst}`, { others });
206
+ }
207
+
208
+ // ── 6. memory ────────────────────────────────────────────────────────────────────────────────────────────────────────
209
+ function memory({ needMb, reserveMb = 3072, meminfo = '/proc/meminfo' }) {
210
+ const m = (fs.readFileSync(meminfo, 'utf8').match(/^MemAvailable:\s+(\d+) kB/m) || [])[1];
211
+ if (!m) return res('memory', false, `cannot read MemAvailable from ${meminfo}`);
212
+ const avail = Math.floor(Number(m) / 1024);
213
+ const ok = avail - reserveMb >= needMb;
214
+ return res('memory', ok, `${avail} MB available, ${reserveMb} MB kept for the host → ${avail - reserveMb} MB for the run; it needs ${needMb} MB`, { availMb: avail });
215
+ }
216
+
217
+ // ── 7. no-timeout ────────────────────────────────────────────────────────────────────────────────────────────────────
218
+ // operator: no timeout may abort an agent. A leg time limit the launcher declares, or a `timeout` wrapper anywhere above
219
+ // this process, is a clock that can cut an agent off mid-answer.
220
+ function noTimeout({ legTimeoutSecs = 0, selfPid = process.pid, procDir = '/proc' }) {
221
+ const clocks = [];
222
+ if (Number(legTimeoutSecs) > 0) clocks.push(`the launcher declares a ${legTimeoutSecs}s leg time limit`);
223
+ for (const p of ancestors(selfPid, procDir)) { const a = cmdline(p, procDir); if (a.length && path.basename(a[0]) === 'timeout') clocks.push(`process ${p} runs under '${a.slice(0, 3).join(' ')}'`); }
224
+ return res('no-timeout', !clocks.length, clocks.length ? clocks.join('; ') : 'no time limit above the launch', { clocks });
225
+ }
226
+
227
+ // ── 8. checklist ─────────────────────────────────────────────────────────────────────────────────────────────────────
228
+ // The operator's 12 items (2026-10-09). At pre-launch an item is GREEN or RED from what can be read now, or NOT-YET with
229
+ // the phase that decides it; any RED stops the launch. "by design skipped" is not a state.
230
+ function checklist({ env, results }) {
231
+ const by = Object.fromEntries(results.map((r) => [r.name, r]));
232
+ const man = env.EPAM_ESTIMATION_MANIFEST;
233
+ const it = (n, item, state, evidence) => ({ n, item, state, evidence });
234
+ const pf = by.preflight || {};
235
+ const items = [
236
+ it(1, 'Estimation manifest injected', !man ? 'RED' : (fs.existsSync(man) ? 'GREEN' : 'RED'), man ? `EPAM_ESTIMATION_MANIFEST=${man}${fs.existsSync(man) ? '' : ' (not found)'}` : 'EPAM_ESTIMATION_MANIFEST unset'),
237
+ it(2, 'Agent generation done agentically (reused roster in good order)', by.seed && by.cache ? (by.seed.ok && by.cache.ok ? 'GREEN' : 'RED') : 'NOT-YET', by.seed ? `${by.seed.detail}; ${by.cache ? by.cache.detail : ''}` : 'no seed to judge: decided at pause 1'),
238
+ it(3, 'Prompt generation done agentically (reused prompts in good order)', by.cache ? (by.cache.ok ? 'GREEN' : 'RED') : 'NOT-YET', by.cache ? by.cache.detail : 'decided at pause 1'),
239
+ it(4, 'Plan solid (checked against git facts)', 'NOT-YET', 'decided at pause 2 (plan of record)'),
240
+ it(5, 'Codelines correct', 'NOT-YET', 'decided at ingest (discovery + classification) and pause 1'),
241
+ it(6, 'Self-heal in place for ALL agents', pf.ok === false && /self-heal|routing/i.test(pf.detail || '') ? 'RED' : (pf.ok ? 'GREEN' : 'NOT-YET'), pf.ok ? 'pre-flight: self-heal observability routing passed' : (pf.detail || 'pre-flight not run')),
242
+ it(7, 'Ladder in place for ALL agents', pf.ok === false && /ladder|rung/i.test(pf.detail || '') ? 'RED' : (pf.ok ? 'GREEN' : 'NOT-YET'), pf.ok ? 'pre-flight: every assigned model is a rung of a declared ladder' : (pf.detail || 'pre-flight not run')),
243
+ it(8, 'Pause 1 and pause 2 working', env.EPAM_PAUSE_AFTER_AGENT_MINT === '1' && env.EPAM_PAUSE_BEFORE_WRITER === '1' ? 'GREEN' : 'RED', `EPAM_PAUSE_AFTER_AGENT_MINT=${env.EPAM_PAUSE_AFTER_AGENT_MINT || ''} EPAM_PAUSE_BEFORE_WRITER=${env.EPAM_PAUSE_BEFORE_WRITER || ''}`),
244
+ it(9, 'Cost collection and reporting working', by.cost ? (by.cost.ok ? 'GREEN' : 'RED') : 'NOT-YET', by.cost ? by.cost.detail : 'not checked'),
245
+ it(10, 'Worktrees up and working', 'NOT-YET', 'decided when the lanes open their worktrees'),
246
+ it(11, 'Langfuse cassette recording working', by.langfuse ? (by.langfuse.ok ? 'GREEN' : 'RED') : 'NOT-YET', by.langfuse ? by.langfuse.detail : 'not checked'),
247
+ it(12, 'Dashboards up, controlled by eleventy', pf.ok === false && /dashboard/i.test(pf.detail || '') ? 'RED' : (pf.ok ? 'GREEN' : 'NOT-YET'), pf.ok ? 'pre-flight: dashboard is up' : (pf.detail || 'pre-flight not run')),
248
+ ];
249
+ const red = items.filter((i) => i.state === 'RED');
250
+ return res('checklist', !red.length, `${items.filter((i) => i.state === 'GREEN').length} green, ${red.length} red, ${items.filter((i) => i.state === 'NOT-YET').length} not yet${red.length ? ` — red: ${red.map((i) => i.n).join(', ')}` : ''}`, { items });
251
+ }
252
+
253
+ /** The whole pre-launch, in order; every check runs (a report lists every red at once), the verdict is all-green. */
254
+ function prelaunch(o) {
255
+ const results = [];
256
+ if (o.preflight !== false) results.push(preflight(o));
257
+ results.push(langfuse({ install: o.install, env: o.env, runId: o.runId, outDir: o.outDir }));
258
+ if (o.ticket) results.push(seed({ install: o.install, project: o.project, ticket: o.ticket, runsRoots: o.runsRoots || [] }));
259
+ if (o.codeline) results.push(cache({ install: o.install, project: o.project, codeline: o.codeline }));
260
+ results.push(cost({ install: o.install, set: o.set || o.env.EPAM_PROVIDER_SET || 'claude', tmpDir: o.outDir }));
261
+ results.push(alone({ install: o.install }));
262
+ results.push(memory({ needMb: Number(o.needMb || 4096) }));
263
+ results.push(noTimeout({ legTimeoutSecs: o.legTimeoutSecs || 0 }));
264
+ results.push(checklist({ env: o.env, results }));
265
+ const report = { at: new Date().toISOString(), runId: o.runId, install: o.install, project: o.project, ticket: o.ticket || null,
266
+ ok: results.every((r) => r.ok), results: results.map(({ log, ...r }) => r) };
267
+ if (o.outDir) { fs.mkdirSync(o.outDir, { recursive: true }); fs.writeFileSync(path.join(o.outDir, 'prelaunch.json'), `${JSON.stringify(report, null, 2)}\n`);
268
+ const pf = results.find((r) => r.name === 'preflight'); if (pf && pf.log) fs.writeFileSync(path.join(o.outDir, 'prelaunch-preflight.log'), pf.log); }
269
+ return report;
270
+ }
271
+
272
+ module.exports = { prelaunch, prelaunchCli, preflight, langfuse, langfuseEnv, seed, cache, cost, ladderModels, alone, memory, noTimeout, checklist, ancestors };
273
+
274
+ /**
275
+ * THE COMMAND, for every pipeline:
276
+ * prelaunch.js --install <i> --project <p> --run-id <id> --out <dir> [--ticket <KEY> --runs <runs dir>[,…]]
277
+ * [--codeline <cl>] [--set <provider set>] [--need-mb <n>] [--leg-timeout-secs <s>] [--runner <launcher>]
278
+ * [--prd <file>] [--preflight no]
279
+ * Prints each check and the verdict; writes <out>/prelaunch.json; exit 0 all green, 1 any red, 64 usage.
280
+ */
281
+ function prelaunchCli(argv, env = process.env) {
282
+ const a = {}; for (let i = 0; i < argv.length; i += 2) a[argv[i].replace(/^--/, '')] = argv[i + 1];
283
+ for (const k of ['install', 'project', 'run-id', 'out']) if (!a[k]) { process.stderr.write(`prelaunch: --${k} is required\n`); return 64; }
284
+ const r = prelaunch({ install: path.resolve(a.install), project: a.project, runId: a['run-id'], outDir: path.resolve(a.out), env,
285
+ ticket: a.ticket, runsRoots: a.runs ? a.runs.split(',').map((d) => path.resolve(d)) : [], codeline: a.codeline,
286
+ set: a.set, needMb: a['need-mb'], legTimeoutSecs: a['leg-timeout-secs'], runner: a.runner, prd: a.prd, preflight: a.preflight !== 'no' });
287
+ process.stdout.write('── Pre-launch (amsd-pipeline-shared) ─────────────────────────────────────────\n');
288
+ for (const c of r.results) process.stdout.write(` ${c.ok ? '✓' : '✗'} ${c.name}: ${c.detail}\n`);
289
+ const cl = r.results.find((c) => c.name === 'checklist');
290
+ if (cl) for (const it of cl.items) process.stdout.write(` ${String(it.n).padStart(2)}. ${it.state.padEnd(7)} ${it.item} — ${it.evidence}\n`);
291
+ process.stdout.write(r.ok ? ` PRE-LAUNCH GREEN — ${path.join(a.out, 'prelaunch.json')}\n` : ` ✗ PRE-LAUNCH RED — the launch stops before any model call (${path.join(a.out, 'prelaunch.json')})\n`);
292
+ return r.ok ? 0 : 1;
293
+ }
294
+ if (require.main === module) process.exit(prelaunchCli(process.argv.slice(2)));