amsd-pipeline-workflow 0.1.7 → 0.1.9

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/README.md CHANGED
@@ -10,7 +10,7 @@ Three steps, in this order, into one install:
10
10
 
11
11
  ```bash
12
12
  npx amsd-pipeline@2.0.80 --dest <dir> --ref v2.0.80 --no-docker # 1. the pipeline, as always
13
- npx amsd-pipeline-shared@<version> install --into <dir> # 2. the shared gateway, reviewer, messages
13
+ npx amsd-pipeline-shared@<version> install --into <dir> # 2. the shared gateway, reviewer, messages, pre-launch
14
14
  npx amsd-pipeline-workflow@<version> install --into <dir> [--codeline-cache <cache>] # 3. the workflows
15
15
  ```
16
16
 
@@ -20,7 +20,20 @@ into the same project, so the pipeline reuses them instead of minting and genera
20
20
  launch whether each is current. Reinstalling replaces `claude-workflows/`. Without step 2 the workflows still run, but
21
21
  messaging is off (said at generation) and the gateway's hub keeps 2.0.80's defect B56 (a failed call reads as success).
22
22
 
23
+ Then seed the codeline the ticket's work belongs to with the pipeline's own restore, so the launch starts from its
24
+ recorded agents and reviewed prompts (a fresh install's project prompts are not the ticket's):
25
+
26
+ ```bash
27
+ EPAM_PROJECT_CONFIG_DIR=<dir>/orchestrations/projects/<project> \
28
+ bash -c '. <dir>/orchestrations/scripts/lib/prompt-variant.sh; codeline_minted_restore <codeline>'
29
+ ```
30
+
31
+ A codeline cache built with `tools/cache-from-checkpoint.js` carries both of the builder's layers: the prompt set and, with
32
+ `--entries`, the source run's per-prompt entries (only where the entry is the cached prompt), plus `--marker`, the source
33
+ run's own completion marker. A template change then rebuilds only that template's prompt.
34
+
23
35
  Needs: Node 20+, the `claude` CLI, `unshare` (util-linux) and `systemd-run --user` (the memory bound, `bin/bounded`).
36
+ ClickHouse is not used: Langfuse runs on postgres and redis only.
24
37
 
25
38
  ## What every agent call gets
26
39
 
@@ -44,6 +57,19 @@ sent as one generation through the pipeline's own emitter.
44
57
 
45
58
  ## Live runs: the run note and the abort
46
59
 
60
+ Before any leg, the launch runs the **shared pre-launch** (amsd-pipeline-shared `lib/prelaunch.js`, the same one every bash
61
+ launcher runs): the pipeline's pre-flight, Langfuse, seed, cache, cost, alone, memory, no-timeout and the 12-item checklist.
62
+ Any red refuses the launch before a model call (`<run>/prelaunch-leg-<n>/prelaunch.json`). A live launch states the seeded
63
+ codeline and, if it accepts some prompts being rebuilt, how many:
64
+
65
+ ```bash
66
+ node tools/rehearse.js --install <install> --out <run dir> --live yes --tag <n> --start live --stop-at pause-1 \
67
+ --seed-codeline <codeline> --expect-scope <codeline> [--max-rebuild <n>]
68
+ ```
69
+
70
+ `--expect-scope` stops the leg the moment ingest scopes the story to any other set of codelines (`tools/scope-watch.js`). A
71
+ leg has no time limit. Right before launching, check that no other memory-heavy job is running on the machine.
72
+
47
73
  `tools/rehearse.js --live yes` (and `--start live`, `--leg 2|3`, `--tag`) keeps one note per run in the operator's vault
48
74
  (`pipeline/live/<run>.md`, `tools/live-tracker.js`), refreshed every 2 minutes while a leg runs: legs, cost by stage,
49
75
  the Langfuse session id (proven by a launch trace read back before each leg), the lanes, the messages, and the six
@@ -54,3 +80,18 @@ At £0, `--plant writer,reviewer` makes those agents give no answer once per lan
54
80
  ## Version
55
81
 
56
82
  `amsd-pipeline-workflow version`. `package.json` `amsdPipeline.tested` names the pipeline release it was tested against.
83
+
84
+ ## The critical agent transitions as `claude plugin eval` cases
85
+
86
+ `evals/` holds the critical agent transitions of the pipeline as a [`claude plugin eval`](https://code.claude.com/docs/en/plugin-evals)
87
+ suite: discovery's repository descriptions, the classifier's scope, role assignment for every story/codeline pair, the
88
+ self-heal analyst's corrective, and the Writer's reviewer (approves run 9's real fix; rejects the same change with a planted
89
+ accessibility defect — prompts rendered by `team-lead-review.sh` itself, `tools/capture-review-prompt.sh`). Each case is the transition's real rendered prompt recorded from a run (`evals/source/`, exported
90
+ from Langfuse), handed verbatim to that seam's own agent definition, with graders on its answer. Build the plugin from a run's
91
+ generated agents, then run it (every run is a real model call):
92
+
93
+ ```bash
94
+ node tools/build-eval-plugin.js --agents <run>/w1/gen/agents # writes evals/plugin/
95
+ claude plugin validate evals/plugin # free: structure only
96
+ claude plugin eval evals/plugin --ablation none # paid: 6 cases x 3 runs
97
+ ```
@@ -96,13 +96,34 @@ const sum = (t) => TYPES.reduce((a, k) => a + (t[k] || 0), 0);
96
96
  /** modelUsage's entry for a transcript's model: the same name, or one a prefix of the other (a dated id). */
97
97
  const usageFor = (modelUsage, model) => modelUsage[model] || Object.entries(modelUsage).find(([k]) => model.startsWith(k) || k.startsWith(model))?.[1] || null;
98
98
 
99
- /** Each entry's share of its model's session cost, by its tokens. */
100
- function allocate(entries, modelUsage) {
99
+ /**
100
+ * A call's cost from its own tokens and the pipeline's price table (config/model-pricing.json, or MODEL_PRICING_FILE), for
101
+ * a call its session reported no cost for — a leg killed before its session answered (LIVE-AMSD-2974-8/9, 2026-10-19:
102
+ * every call booked $0). null when the table does not price the model: unknown stays unknown, never a guessed figure.
103
+ * Cache writes and reads at the table's own rates, else the vendor's standard multiples of the input price (1.25x, 0.1x).
104
+ */
105
+ function priceFromTable(prices, model, t) {
106
+ const p = prices[model] || Object.entries(prices).find(([k]) => model.startsWith(k) || k.startsWith(model))?.[1];
107
+ if (!p || p.inputPerMillion == null || p.outputPerMillion == null) return null;
108
+ const write = p.cacheWrite5mPerMillion != null ? p.cacheWrite5mPerMillion : p.inputPerMillion * 1.25;
109
+ const read = p.cacheReadPerMillion != null ? p.cacheReadPerMillion : p.inputPerMillion * 0.1;
110
+ return ((t.in || 0) * p.inputPerMillion + (t.cacheCreate || 0) * write + (t.cacheRead || 0) * read + (t.out || 0) * p.outputPerMillion) / 1e6;
111
+ }
112
+ function priceTable(env) {
113
+ const f = (env && env.MODEL_PRICING_FILE) || (env && env.SCRIPT_DIR ? path.join(env.SCRIPT_DIR, '..', 'config', 'model-pricing.json') : '');
114
+ try { return (JSON.parse(fs.readFileSync(f, 'utf8')) || {}).models || {}; } catch { return {}; }
115
+ }
116
+
117
+ /** Each entry's share of its model's session cost, by its tokens; a model the session did not report, priced from the table. */
118
+ function allocate(entries, modelUsage, prices = {}) {
101
119
  const totals = {};
102
120
  for (const e of entries) { const key = usageFor(modelUsage, e.model) ? e.model : null; if (key) totals[key] = (totals[key] || 0) + sum(e.tokens); }
103
121
  return entries.map((e) => {
104
122
  const mu = usageFor(modelUsage, e.model);
105
- if (!mu) return { ...e, costUsd: 0, costUnknown: true };
123
+ if (!mu) {
124
+ const est = priceFromTable(prices, e.model, e.tokens);
125
+ return est == null ? { ...e, costUsd: 0, costUnknown: true } : { ...e, costUsd: est, costUnknown: false, pricedFromTable: true };
126
+ }
106
127
  const w = totals[e.model] ? sum(e.tokens) / totals[e.model] : 0;
107
128
  return { ...e, costUsd: (mu.costUSD || 0) * w, costUnknown: false };
108
129
  });
@@ -128,7 +149,7 @@ function recordSession(env, session, { stage, storyId, phase, projectsDir, log }
128
149
  const rows = [];
129
150
  try {
130
151
  if (entries && entries.length) {
131
- for (const e of allocate(entries, modelUsage)) {
152
+ for (const e of allocate(entries, modelUsage, priceTable(env))) {
132
153
  rows.push(appendLedgerRecord({ ledgerFile: ledger, agent: e.agent, storyId, phase, model: e.model,
133
154
  cost: { costUsd: e.costUsd, tokensIn: e.tokens.in, tokensOut: e.tokens.out, tokensCached: e.tokens.cacheRead, tokensCacheCreate: e.tokens.cacheCreate,
134
155
  costIsEstimate: true, costUnknown: e.costUnknown } }));
@@ -156,4 +177,4 @@ function recordSession(env, session, { stage, storyId, phase, projectsDir, log }
156
177
  }
157
178
  }
158
179
 
159
- module.exports = { recordSession, sessionEntries, allocate, transcriptTokens, callContent };
180
+ module.exports = { recordSession, sessionEntries, allocate, transcriptTokens, callContent, priceFromTable };
@@ -55,8 +55,14 @@ const STAGE_PHASES = [
55
55
  { title: 'Role assignment', step: 'agents', seams: ['role-assigner'] }, // :1080–1087
56
56
  { title: 'Project prompts', step: 'agents', seams: ['prompt-builder', 'prompt-review', 'prompt-claim-adjudicator'] }, // :1159–1406
57
57
  ];
58
- // asked from inside any seam's call, never a phase of its own: the hub's self-heal analyst (lib/self-heal.js)
59
- const ANY_PHASE_SEAMS = ['agent-failure-analyst'];
58
+ // ASKED FROM INSIDE ANY SEAM'S CALL, never a phase of its own: the pipeline's self-heal — the analyst that diagnoses a
59
+ // failed attempt and the synthesiser that turns its episodes into KB constraints (content-retry.js, self-heal.js,
60
+ // agent-attempt-analyst.sh, kb-synthesizer.js). Read from the registry by what those seams produce, not listed by name:
61
+ // the list named only the analyst, so the synthesiser was "out of the pipeline's order" and self-heal could not finish
62
+ // inside the stage (LIVE-AMSD-2974-7, 2026-10-19).
63
+ const SELF_HEAL_PRODUCES = ['diagnosis', 'kb-constraint'];
64
+ const anyPhaseSeams = (registry, agentMap) => Object.keys(registry.profiles || {})
65
+ .filter((s) => SELF_HEAL_PRODUCES.includes((registry.profiles[s] || {}).produces) && agentMap[s]).sort();
60
66
  const PAUSE_STEP = 'pause-1';
61
67
  const HOOK = path.resolve(__dirname, '..', 'hooks', 'agents-step.js');
62
68
  const q = (x) => `'${String(x).replace(/'/g, "'\\''")}'`;
@@ -219,7 +225,7 @@ function main(a) {
219
225
  const features = JSON.parse(fs.readFileSync(path.join(__dirname, 'features.json'), 'utf8')).features;
220
226
  const plan = {
221
227
  source: `${a.project} run ${a.run} (Codeline, Agents; issues ${issuesFrom.file || `rebuilt from run ${issuesFrom.recordedRun}`})`,
222
- steps: STEPS, pauseStep: PAUSE_STEP, agents: agentMap, runners, si, stagePhases: STAGE_PHASES, anyPhaseSeams: ANY_PHASE_SEAMS,
228
+ steps: STEPS, pauseStep: PAUSE_STEP, agents: agentMap, runners, si, stagePhases: STAGE_PHASES, anyPhaseSeams: anyPhaseSeams(registry, agentMap),
223
229
  // The 1,000-agent limit of one workflow run (decision 15), less the step runners.
224
230
  maxCalls: 900,
225
231
  features,
@@ -238,7 +244,7 @@ function main(a) {
238
244
  return manifest;
239
245
  }
240
246
 
241
- module.exports = { main, STEPS, STAGE_PHASES, ANY_PHASE_SEAMS };
247
+ module.exports = { main, STEPS, STAGE_PHASES, SELF_HEAL_PRODUCES, anyPhaseSeams };
242
248
  if (require.main === module) {
243
249
  try { const m = main(args(process.argv.slice(2))); process.stdout.write(`${JSON.stringify({ agents: m.agents.length })}\n`); } catch (e) { process.stderr.write(`${e.message}\n`); process.exit(1); }
244
250
  }
@@ -686,7 +686,12 @@ if (require.main === module) {
686
686
  const answer = input.tool_input || {};
687
687
  const pj = JSON.parse(fs.readFileSync(a.pipeline, 'utf8'));
688
688
  const env = JSON.parse(fs.readFileSync(pj.env, 'utf8'));
689
- const out = (result) => process.stdout.write(`${JSON.stringify({ hookSpecificOutput: { hookEventName: 'PreToolUse', permissionDecision: 'allow',
689
+ // THE HOOK'S STDOUT IS ITS JSON ONLY (B60, LIVE-AMSD-2974-10): the pipeline's Spec code runs in this process and logs with
690
+ // console.log (spec-mode-runner.js "… file(s) left its scope"); one such line ahead of the JSON and Claude Code drops the
691
+ // result. Everything else written to stdout goes to stderr; only `out` writes stdout.
692
+ const stdout = process.stdout.write.bind(process.stdout);
693
+ process.stdout.write = (chunk, ...rest) => process.stderr.write(chunk, ...rest);
694
+ const out = (result) => stdout(`${JSON.stringify({ hookSpecificOutput: { hookEventName: 'PreToolUse', permissionDecision: 'allow',
690
695
  updatedInput: { ...answer, result } } })}\n`);
691
696
  Promise.resolve().then(() => sr.withEnv(env, () => handle(event, a, answer, pj)))
692
697
  .then((r) => out(r))
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "amsd-pipeline-workflow",
3
- "version": "0.1.7",
3
+ "version": "0.1.9",
4
4
  "description": "The AMSD pipeline as Claude workflows. Installed by the amsd-pipeline installer: install.sh --workflows",
5
5
  "license": "UNLICENSED",
6
6
  "bin": {