amsd-pipeline-workflow 0.1.20 → 0.1.22

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/bin/cpu-bound ADDED
@@ -0,0 +1,22 @@
1
+ #!/bin/bash
2
+ # cpu-bound <command...> — the live session's tools may not size their pools from the whole machine.
3
+ #
4
+ # Run 11, 2026-10-20 23:07 UTC: the Writer agent ran the codeline's whole jest suite; jest sized 15 workers from the host's 16 cores
5
+ # (~270 MB each) inside the session's 4096 MB memory scope, and the kernel OOM-killed the session. The Bash pipeline fixed exactly this in
6
+ # run 9 (lib/bounded-exec.sh): the number of workers is DERIVED from the tightest cgroup memory limit (resolve_test_workers), and the
7
+ # process's CPU affinity is restricted to that many CPUs (bound_self_affinity) — everything it starts, the agent's own tool commands
8
+ # included, inherits it and sizes its pool from it; a preload (lib/node-cpu-bound.js) makes runners that count os.cpus() see the bound too.
9
+ # Those are the PIPELINE's functions, sourced from the install (SCRIPT_DIR); nothing is copied here.
10
+ #
11
+ # Placed INSIDE the memory scope (bin/bounded), so the cgroup it reads is the one the session lives in: bounded cpu-bound claude -p …
12
+ # A bound that cannot be applied is SAID (stderr) and the command still runs — as the pipeline's own wrapper does.
13
+ set -u
14
+ [ $# -gt 0 ] || { echo "cpu-bound: no command" >&2; exit 64; }
15
+ if [ -n "${SCRIPT_DIR:-}" ] && [ -f "$SCRIPT_DIR/lib/bounded-exec.sh" ]; then
16
+ # shellcheck source=/dev/null
17
+ . "$SCRIPT_DIR/lib/bounded-exec.sh"
18
+ bound_self_affinity "$(resolve_test_workers)"
19
+ else
20
+ echo "[cpu-bound] WARNING: the pipeline's lib/bounded-exec.sh was not found (SCRIPT_DIR='${SCRIPT_DIR:-}') — this session's tools run UNBOUNDED" >&2
21
+ fi
22
+ exec "$@"
package/driver/runtime.js CHANGED
@@ -19,6 +19,9 @@ const { spawnSync } = require('child_process');
19
19
  const ROOT = path.resolve(__dirname, '..');
20
20
  const { recordSession } = require('./session-cost.js');
21
21
 
22
+ // the names workflows were placed under by earlier versions of this driver and that a test runner's default pattern matches
23
+ const LEGACY_WORKFLOW_FILES = ['spec'];
24
+
22
25
  function toMarkdown(name, def) {
23
26
  // generator/agents.js toMarkdown, from the agents JSON entry (frontmatter keys + `prompt` as the body)
24
27
  const { prompt, ...fm } = def;
@@ -65,13 +68,20 @@ function liveCommand({ name, script, agentsJson, args, sessionDir, env, cwd }) {
65
68
  if (Object.keys(map).length) pre.push({ matcher: [...matchers].join('|') || '.*', hooks: [{ type: 'command', command: dispatch, ...(timeout ? { timeout } : {}) }] });
66
69
  settings.hooks = { ...(settings.hooks || {}), PreToolUse: pre };
67
70
  fs.writeFileSync(settingsFile, JSON.stringify(settings, null, 2));
71
+ // A LEGACY FILE OF THE PIPELINE'S OWN goes (the Spec workflow was placed as spec.js, which a client's jest runs as a test — run 11): only a file named like a legacy workflow that carries a workflow header
72
+ // of that same name; a client's file of the same name is never touched
73
+ for (const legacy of LEGACY_WORKFLOW_FILES) {
74
+ const f = path.join(wfDir, `${legacy}.js`);
75
+ try { if (new RegExp(`^export const meta = \\{\\s*name: '${legacy}'`).test(fs.readFileSync(f, 'utf8')) && legacy !== name) fs.rmSync(f); } catch { /* none */ }
76
+ }
68
77
  fs.copyFileSync(script, path.join(wfDir, `${name}.js`));
69
78
  // THE RUNNER WAITS FOR ITS WORKFLOW (LIVE-AMSD-2974-3, 2026-10-18: the runner answered "It's still running in the
70
79
  // background…" and the session exited, taking the Spec workflow down with it): it answers only with the finished result
71
80
  // the session itself waits for its workflow (CLAUDE_CODE_PRINT_BG_WAIT_CEILING_MS=0 below); the runner has no task-wait tool
72
81
  const prompt = `Run the workflow "${name}" with these args, exactly: ${JSON.stringify(args || {})}. When it has finished, reply with its result as JSON and nothing else.`;
73
82
  const claude = (env && env.CLAUDE_BIN) || 'claude';
74
- const argv = [path.join(__dirname, '..', 'bin', 'bounded'), claude, '-p', prompt, '--output-format', 'json',
83
+ // memory scope (bin/bounded) → CPU bound derived from that scope (bin/cpu-bound, the pipeline's own bound_self_affinity) → the session: its tools (jest included) cannot out-size the scope (run 11 OOM)
84
+ const argv = [path.join(__dirname, '..', 'bin', 'bounded'), path.join(__dirname, '..', 'bin', 'cpu-bound'), claude, '-p', prompt, '--output-format', 'json',
75
85
  '--setting-sources', 'project', '--permission-mode', 'bypassPermissions'];
76
86
  return { cwd: home, argv, agents: Object.keys(defs).length, workflow: path.join(wfDir, `${name}.js`) };
77
87
  }
package/driver/stages.js CHANGED
@@ -8,6 +8,10 @@ const { spawnSync } = require('child_process');
8
8
  const runtime = require('./runtime.js');
9
9
  const { bash } = require('./pipeline.js');
10
10
 
11
+ // THE NAME EACH WORKFLOW IS RUN BY (and so the file it is placed in, <codeline>/.claude/workflows/<name>.js). No name may match a test runner's default pattern: the Spec workflow was "spec", the client's jest ran
12
+ // spec.js as a test, and every whole-suite run in a codeline reported the pipeline's own file as a failing suite (run 11, 2026-10-20; test/integration/workflow-placement.test.js).
13
+ const WORKFLOW_NAMES = { agents: 'codeline-agents', spec: 'spec-stage', writer: 'writer', assess: 'assess', bridge: 'bridge' };
14
+
11
15
  const ROOT = path.resolve(__dirname, '..');
12
16
  const gen = (script, args, env) => {
13
17
  const r = spawnSync(process.execPath, [path.join(ROOT, 'generator', script), ...args], { encoding: 'utf8', env, maxBuffer: 256 * 1024 * 1024 });
@@ -71,7 +75,7 @@ async function runGeneratedAgents(ctx) {
71
75
  ...(ctx.agentsDocuments ? ['--documents', ctx.agentsDocuments] : []), ...(env.JIRA_TICKET ? ['--ticket', env.JIRA_TICKET] : []),
72
76
  ...(env.EPAM_ESTIMATION_MANIFEST ? ['--estimation-manifest', env.EPAM_ESTIMATION_MANIFEST] : [])], env);
73
77
  const plan = JSON.parse(fs.readFileSync(path.join(out, 'plan.json'), 'utf8'));
74
- const res = await runtime.run({ runtime: ctx.runtime, log: ctx.log, name: 'codeline-agents', script: path.join(out, 'workflows', 'codeline-agents.js'),
78
+ const res = await runtime.run({ runtime: ctx.runtime, log: ctx.log, name: WORKFLOW_NAMES.agents, script: path.join(out, 'workflows', 'codeline-agents.js'),
75
79
  agentsJson: path.join(out, 'agents.json'), args: withFeatures(ctx, {}), env: liveStart ? (({ EPAM_SKIP_JIRA_INGEST: _s, ...rest }) => rest)(env) : { ...env, EPAM_SKIP_JIRA_INGEST: '1' }, // a live start runs the stage's own ingest
76
80
  standin: ctx.standin && ctx.standin('codeline-agents', { plan, out }), sessionDir: path.join(out, 'session'), approved: ctx.liveApproved });
77
81
  log(`[driver] Codeline, Agents: ${res.status}${res.reason ? ` — ${res.reason}` : ''} (${res.calls || 0} question(s); phases: ${(res.phases || []).join(' → ') || 'none'})`);
@@ -141,7 +145,7 @@ async function runPlan(ctx, env, { prd, story, phase, keep, out }) {
141
145
  '--codeline-map', Object.entries(ctx.map).map(([k, v]) => `${k}=${v}`).join(','),
142
146
  '--plan-only', '--story', story.id, '--plan-keep', keep, ...(ctx.docsFrom ? ['--docs-from', ctx.docsFrom] : [])], env);
143
147
  const specEnv = JSON.parse(fs.readFileSync(path.join(out, 'checks', 'spec', 'env.json'), 'utf8'));
144
- return runtime.run({ runtime: ctx.runtime, log: ctx.log, name: 'spec', script: path.join(out, 'workflows', 'spec.js'),
148
+ return runtime.run({ runtime: ctx.runtime, log: ctx.log, name: WORKFLOW_NAMES.spec, script: path.join(out, 'workflows', 'spec.js'),
145
149
  agentsJson: path.join(out, 'agents-spec.json'), args: withFeatures(ctx, {}), env: specEnv, standin: ctx.standin && ctx.standin('spec', { lane: { cl: first, wt: ctx.map[first] }, out }),
146
150
  sessionDir: path.join(out, 'session'), cwd: ctx.map[first], approved: ctx.liveApproved });
147
151
  }
@@ -154,7 +158,7 @@ async function runSpec(ctx, lane, env, { prd, phase, out }) {
154
158
  '--lane-log-dir', env.LOG_DIR, ...(ctx.docsFrom ? ['--docs-from', ctx.docsFrom] : []),
155
159
  ...(env.EPAM_RESUME_RUN ? ['--resume'] : [])], env);
156
160
  const specEnv = JSON.parse(fs.readFileSync(path.join(out, 'checks', 'spec', 'env.json'), 'utf8'));
157
- const result = await runtime.run({ runtime: ctx.runtime, log: ctx.log, name: 'spec', script: path.join(out, 'workflows', 'spec.js'),
161
+ const result = await runtime.run({ runtime: ctx.runtime, log: ctx.log, name: WORKFLOW_NAMES.spec, script: path.join(out, 'workflows', 'spec.js'),
158
162
  agentsJson: path.join(out, 'agents-spec.json'), args: withFeatures(ctx, {}), env: specEnv, standin: ctx.standin && ctx.standin('spec', { lane, out }),
159
163
  sessionDir: path.join(out, 'session'), cwd: lane.wt, approved: ctx.liveApproved });
160
164
  // the spec pass's PRD is the lane's PRD (its PRD_FILE): the workflow worked on its own copy
@@ -227,7 +231,7 @@ async function runWriter(ctx, lane, env, { story, out, lineSlug, reviewCommitted
227
231
  if (k in ctx.launchEnv && String(ctx.launchEnv[k]) !== String(val)) throw new Error(`the Writer was generated with ${k}=${val}, the run was launched with ${ctx.launchEnv[k]}`);
228
232
  }
229
233
  }
230
- const res = await runtime.run({ runtime: ctx.runtime, log: ctx.log, name: 'writer', script: path.join(out, 'workflows', 'writer.js'),
234
+ const res = await runtime.run({ runtime: ctx.runtime, log: ctx.log, name: WORKFLOW_NAMES.writer, script: path.join(out, 'workflows', 'writer.js'),
231
235
  agentsJson: path.join(out, 'agents.json'), args: withFeatures(ctx, { runId: ctx.run, story }), env, standin: ctx.standin && ctx.standin('writer', { lane, out, story }),
232
236
  sessionDir: path.join(out, 'session'), cwd: lane.wt, approved: ctx.liveApproved, costStory: story });
233
237
  persistFailures(out, env.LOG_DIR);
@@ -257,7 +261,7 @@ async function runAssess(ctx, lane, env, { label, out }) {
257
261
  fs.rmSync(out, { recursive: true, force: true });
258
262
  gen(path.join('..', 'driver', 'generate-assess.js'), ['--install', ctx.install, '--codeline', lane.wt, '--out', out, '--label', label, '--set', ctx.set,
259
263
  '--env', JSON.stringify(env)], env);
260
- return runtime.run({ runtime: ctx.runtime, log: ctx.log, name: 'assess', script: path.join(out, 'workflows', 'assess.js'),
264
+ return runtime.run({ runtime: ctx.runtime, log: ctx.log, name: WORKFLOW_NAMES.assess, script: path.join(out, 'workflows', 'assess.js'),
261
265
  agentsJson: path.join(out, 'agents-assess.json'), args: {}, env, standin: ctx.standin && ctx.standin('assess', { lane, out }),
262
266
  sessionDir: path.join(out, 'session'), approved: ctx.liveApproved });
263
267
  }
@@ -267,9 +271,9 @@ async function runBridge(ctx, lane, env, { prd, out }) {
267
271
  fs.rmSync(out, { recursive: true, force: true });
268
272
  gen(path.join('..', 'driver', 'generate-bridge.js'), ['--install', ctx.install, '--project', ctx.project, '--run', ctx.run, '--lane', lane.cl,
269
273
  '--codeline', lane.wt, '--prd', prd, '--out', out, '--set', ctx.set, '--env', JSON.stringify({ LOG_DIR: ctx.env.LOG_DIR })], env);
270
- return runtime.run({ runtime: ctx.runtime, log: ctx.log, name: 'bridge', script: path.join(out, 'workflows', 'bridge.js'),
274
+ return runtime.run({ runtime: ctx.runtime, log: ctx.log, name: WORKFLOW_NAMES.bridge, script: path.join(out, 'workflows', 'bridge.js'),
271
275
  agentsJson: path.join(out, 'agents-bridge.json'), args: {}, env, standin: ctx.standin && ctx.standin('bridge', { lane, out }),
272
276
  sessionDir: path.join(out, 'session'), approved: ctx.liveApproved });
273
277
  }
274
278
 
275
- module.exports = { runPlan, runCodelineAgents, runSpec, runPrePause, runWriter, runBridge, runAssess, featureOn, persistFailures };
279
+ module.exports = { WORKFLOW_NAMES, runPlan, runCodelineAgents, runSpec, runPrePause, runWriter, runBridge, runAssess, featureOn, persistFailures };
@@ -533,6 +533,14 @@ function main(a) {
533
533
  const chain = [d.frontmatter && d.frontmatter.model].filter(Boolean);
534
534
  while (pairs[chain[chain.length - 1]] && !chain.includes(pairs[chain[chain.length - 1]])) chain.push(pairs[chain[chain.length - 1]]);
535
535
  return chain;
536
+ })(),
537
+ // WHY THE EARLIER ATTEMPTS OF THIS STORY WERE BLOCKED (run 11, 2026-10-20: five blind retries of one gate block): the last three reproduction-gate reasons in the lane's failure history
538
+ // (hooks/pipeline-step.js recordGateBlock appends them; the driver persists the file), for the repro-test writer's prompt
539
+ priorBlocks: (() => {
540
+ try {
541
+ return fs.readFileSync(path.join(process.env.LOG_DIR || '', 'story-failures.jsonl'), 'utf8').split('\n').filter(Boolean).map((l) => { try { return JSON.parse(l); } catch { return null; } })
542
+ .filter((r) => r && r.storyId === a.story && r.failureClass === 'repro-gate-block' && String(r.reason || '').trim()).map((r) => String(r.reason).trim()).slice(-3);
543
+ } catch { return []; }
536
544
  })() },
537
545
  // The path with no jumps (for reading), and the step from every reachable state (generator/ladder.sh): the workflow walks
538
546
  // the steps, so a jump lands where the pipeline's would.
@@ -12,7 +12,7 @@ const { packPlan } = require('./pack-plan.js');
12
12
  function emit(plan) {
13
13
  const packed = packPlan(plan);
14
14
  return `export const meta = {
15
- name: 'spec',
15
+ name: 'spec-stage',
16
16
  description: 'Spec stage for one lane: spec coordinator, ticket links, detective, openspec with its verification-criteria guard, change review, speckit, coordinator and model review, pause 2',
17
17
  phases: [{ title: 'Spec' }, { title: 'Review' }, { title: 'Pause 2' }],
18
18
  }
@@ -390,9 +390,13 @@ async function reproStep(prep) {
390
390
  const schema = { ...SCHEMAS.repro, properties: { ...SCHEMAS.repro.properties,
391
391
  source: { type: 'string', enum: [source], description: 'the source file your test covers: exactly this path' },
392
392
  test: { type: 'string', description: \`the path of the test file you wrote, relative to the codeline root (planned: \${cand.target})\` } } }
393
+ // EARLIER ATTEMPTS OF THIS STORY WERE BLOCKED BY THE REPRODUCTION GATE (the lane's failure history, plan.repro.priorBlocks): the writer is told why, after the pipeline's own prompt
394
+ const blocked = (PLAN.repro.priorBlocks || []).length
395
+ ? \`\\n\\nEARLIER ATTEMPTS OF THIS STORY WERE BLOCKED BY THE REPRODUCTION GATE — read why before you write the test. The gate runs your test with the fix reverted to the story's baseline and requires it to FAIL there; a test that passes without the fix reproduces nothing.\\n\${PLAN.repro.priorBlocks.map((r) => \`- \${r}\`).join('\\n')}\\n\`
396
+ : ''
393
397
  for (let i = 0; i < PLAN.repro.maxAttempts; i++) {
394
398
  const model = PLAN.repro.ladder[Math.min(i, PLAN.repro.ladder.length - 1)]
395
- const a = await agentCall(relativize(cand.prompt) + fb.typecheck + fb.assertions + fb.commit_rejected,
399
+ const a = await agentCall(relativize(cand.prompt) + blocked + fb.typecheck + fb.assertions + fb.commit_rejected,
396
400
  { agentType: PLAN.agents.reproWriter, model, schema, label: \`repro test #\${i + 1}\`, phase: 'Writer' })
397
401
  const v = (a && a.validation) || { valid: false, committed: false, class: 'no_file', feedback: '', reason: a ? 'the answer carries no validation' : 'the agent ended without an answer' }
398
402
  if (v.valid) return { committed: !!v.committed, attempts: i + 1, test: a.test, reason: v.reason }
@@ -16,28 +16,58 @@ const STEPS = { 'repro-gate': 'step-repro-gate.sh', 'uit-check': 'step-uit-check
16
16
  'plan-start': 'step-plan-start.sh', 'plan-fail': 'step-plan-fail.sh', 'call-failed': 'step-call-failed.sh' }; // the step scripts, beside this file
17
17
  function args(argv) { const a = {}; for (let i = 0; i < argv.length; i += 2) a[argv[i].replace(/^--/, '')] = argv[i + 1]; return a; }
18
18
 
19
+ /** The step's result: its script's last line (or why there is none). */
20
+ function runScriptStep(a, input) {
21
+ const script = STEPS[a.step];
22
+ if (!script) return { ran: false, passed: false, reason: `unknown step '${a.step}'` };
23
+ // a per-attempt step runner's own values (decision 27): its retry and the model the previous attempt ran on
24
+ const env = { ...process.env, ...(a.retry !== undefined ? { RUNG_RETRY: String(a.retry) } : {}), ...(a['prev-model'] ? { RUNG_PREV_MODEL: a['prev-model'] } : {}),
25
+ ...(a.model ? { RUNG_MODEL: a.model } : {}),
26
+ // a failed agent call (the gateway, hooks/step-call-failed.sh): the call the step runner was told to hand on
27
+ ...(() => { const c = (input.tool_input || {}).call || {}; return { CALL_AGENT: String(c.agent || ''), CALL_MODEL: String(c.model || ''),
28
+ CALL_STORY: String(c.story || 'global'), CALL_REASON: String(c.reason || ''), CALL_ATTEMPT: String(c.attempt || 1) }; })() };
29
+ const r = spawnSync('bash', [path.join(__dirname, script), a.pipeline, a.state], { encoding: 'utf8', env, maxBuffer: 64 * 1024 * 1024 });
30
+ // THE STEP'S RESULT IS ITS LAST LINE (each step script ends with one `jq -c` line); anything the pipeline printed ahead of
31
+ // it is kept on stderr, never read as the result and never let onto this hook's stdout (R3, the B60 class)
32
+ const lines = String(r.stdout || '').split('\n').filter((l) => l.trim());
33
+ if (lines.length > 1) process.stderr.write(`${lines.slice(0, -1).join('\n')}\n`);
34
+ try { return JSON.parse(lines[lines.length - 1]); } catch { return { ran: false, passed: false, reason: (r.stderr || `exit ${r.status}`).slice(-400) }; }
35
+ }
36
+
37
+ /**
38
+ * A GATE BLOCK IS FAILURE HISTORY (run 11, 2026-10-20: the reproduction gate blocked one story five times and nothing carried the reason to the next attempt).
39
+ * When the reproduction gate RAN and did not pass, one record is appended to the run's story-failures.jsonl in the pipeline's own format
40
+ * (story-attempt.sh:2666–2688: storyId, failureClass, model, attempt, exitCode, rawBytes, outputTokens, maxIterations, timestamp) plus the gate's reason: the
41
+ * driver persists the file to the lane's log (driver/stages.js persistFailures) and the next generation reads it (generator/generate.js plan.repro.priorBlocks).
42
+ */
43
+ function recordGateBlock(a, result) {
44
+ if (a.step !== 'repro-gate' || !result || result.ran !== true || result.passed !== false) return null;
45
+ const pj = JSON.parse(fs.readFileSync(a.pipeline, 'utf8'));
46
+ const file = path.join(a.state, 'pipeline-env', 'logs', 'story-failures.jsonl');
47
+ let prior = 0;
48
+ try { prior = fs.readFileSync(file, 'utf8').split('\n').filter(Boolean).map((l) => JSON.parse(l)).filter((r) => r.storyId === pj.story && r.failureClass === 'repro-gate-block').length; } catch { prior = 0; }
49
+ const rec = { storyId: pj.story, failureClass: 'repro-gate-block', model: 'gate', attempt: prior + 1, exitCode: 2, rawBytes: 0, outputTokens: 0, maxIterations: 0,
50
+ timestamp: new Date().toISOString(), gate: '3.55', reason: String(result.reason || '').slice(0, 1500) };
51
+ fs.mkdirSync(path.dirname(file), { recursive: true });
52
+ fs.appendFileSync(file, `${JSON.stringify(rec)}\n`);
53
+ return rec;
54
+ }
55
+
56
+ /** The hook's answer for one StructuredOutput call: the step's result, written back into the answer. runStep is injectable for tests. */
57
+ function answer(a, input, runStep) {
58
+ if (input.tool_name !== 'StructuredOutput') return false;
59
+ const result = (runStep || runScriptStep)(a, input);
60
+ try { recordGateBlock(a, result); } catch { /* a record that cannot be written never changes what the workflow is told */ }
61
+ process.stdout.write(`${JSON.stringify({ hookSpecificOutput: { hookEventName: 'PreToolUse', permissionDecision: 'allow',
62
+ updatedInput: { ...(input.tool_input || {}), step: a.step, result } } })}\n`);
63
+ return true;
64
+ }
65
+
19
66
  if (require.main === module) {
20
67
  const a = args(process.argv.slice(3));
21
68
  let input = {};
22
69
  try { input = JSON.parse(fs.readFileSync(0, 'utf8') || '{}'); } catch { /* nothing */ }
23
- if (input.tool_name !== 'StructuredOutput') process.exit(0);
24
- let result;
25
- const script = STEPS[a.step];
26
- if (!script) result = { ran: false, passed: false, reason: `unknown step '${a.step}'` };
27
- else {
28
- // a per-attempt step runner's own values (decision 27): its retry and the model the previous attempt ran on
29
- const env = { ...process.env, ...(a.retry !== undefined ? { RUNG_RETRY: String(a.retry) } : {}), ...(a['prev-model'] ? { RUNG_PREV_MODEL: a['prev-model'] } : {}),
30
- ...(a.model ? { RUNG_MODEL: a.model } : {}),
31
- // a failed agent call (the gateway, hooks/step-call-failed.sh): the call the step runner was told to hand on
32
- ...(() => { const c = (input.tool_input || {}).call || {}; return { CALL_AGENT: String(c.agent || ''), CALL_MODEL: String(c.model || ''),
33
- CALL_STORY: String(c.story || 'global'), CALL_REASON: String(c.reason || ''), CALL_ATTEMPT: String(c.attempt || 1) }; })() };
34
- const r = spawnSync('bash', [path.join(__dirname, script), a.pipeline, a.state], { encoding: 'utf8', env, maxBuffer: 64 * 1024 * 1024 });
35
- // THE STEP'S RESULT IS ITS LAST LINE (each step script ends with one `jq -c` line); anything the pipeline printed ahead of
36
- // it is kept on stderr, never read as the result and never let onto this hook's stdout (R3, the B60 class)
37
- const lines = String(r.stdout || '').split('\n').filter((l) => l.trim());
38
- if (lines.length > 1) process.stderr.write(`${lines.slice(0, -1).join('\n')}\n`);
39
- try { result = JSON.parse(lines[lines.length - 1]); } catch { result = { ran: false, passed: false, reason: (r.stderr || `exit ${r.status}`).slice(-400) }; }
40
- }
41
- process.stdout.write(`${JSON.stringify({ hookSpecificOutput: { hookEventName: 'PreToolUse', permissionDecision: 'allow',
42
- updatedInput: { ...(input.tool_input || {}), step: a.step, result } } })}\n`);
70
+ if (!answer(a, input)) process.exit(0);
43
71
  }
72
+
73
+ module.exports = { answer, recordGateBlock, STEPS };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "amsd-pipeline-workflow",
3
- "version": "0.1.20",
3
+ "version": "0.1.22",
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": {