amsd-pipeline-workflow 0.1.21 → 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/driver/runtime.js +9 -0
- package/driver/stages.js +11 -7
- package/generator/generate.js +8 -0
- package/generator/spec-workflow.js +1 -1
- package/generator/writer-workflow.js +5 -1
- package/hooks/pipeline-step.js +50 -20
- package/package.json +1 -1
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,6 +68,12 @@ 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
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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 };
|
package/generator/generate.js
CHANGED
|
@@ -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 }
|
package/hooks/pipeline-step.js
CHANGED
|
@@ -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
|
|
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