amsd-pipeline-workflow 0.1.25 → 0.1.27
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/hook-dispatch.js +6 -2
- package/driver/runtime.js +59 -46
- package/driver/session-cost.js +9 -8
- package/hooks/attempt-verify.sh +19 -4
- package/package.json +1 -1
package/driver/hook-dispatch.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
'use strict';
|
|
3
3
|
// THE AGENTS' HOOKS, IN A LIVE SESSION (2026-10-18): the real Workflow tool does not run a hook declared in a workflow
|
|
4
4
|
// agent's frontmatter (tools/hook-probe.js: never fired), but does run one declared in the session's project settings, and
|
|
5
|
-
// its input names the calling agent (agent_type). driver/runtime.js liveCommand
|
|
5
|
+
// its input names the calling agent (agent_type). driver/runtime.js liveCommand writes this one PreToolUse hook into the session's settings file; it
|
|
6
6
|
// runs the calling agent's own hook command — exactly as its definition declares it — with the same input, and passes its
|
|
7
7
|
// output and exit code back unchanged. An agent with no hook for this tool passes through (no output, exit 0).
|
|
8
8
|
// node driver/hook-dispatch.js <map.json> map: { "<agent_type>": [{ matcher, command }...] }
|
|
@@ -15,7 +15,11 @@ process.stdin.on('end', () => {
|
|
|
15
15
|
let ev = {};
|
|
16
16
|
try { ev = JSON.parse(input); } catch { process.exit(0); }
|
|
17
17
|
const map = JSON.parse(fs.readFileSync(process.argv[2], 'utf8'));
|
|
18
|
-
|
|
18
|
+
// THE PLUGIN NAMES ITS AGENTS "<plugin>:<agent>" (driver/runtime.js): the agent is the part after the colon, and the hook is handed its own agent's
|
|
19
|
+
// name as the pipeline's hooks know it — every hook reads agent_type as it did when the agents were placed bare
|
|
20
|
+
const agent = String(ev.agent_type || '').replace(/^.*:/, '');
|
|
21
|
+
if (ev.agent_type !== undefined) { ev.agent_type = agent; input = JSON.stringify(ev); }
|
|
22
|
+
const hooks = (map[agent] || []).filter((h) => !h.matcher || new RegExp(`^(?:${h.matcher})$`).test(ev.tool_name || ''));
|
|
19
23
|
for (const h of hooks) {
|
|
20
24
|
const r = spawnSync('bash', ['-c', h.command], { input, encoding: 'utf8', maxBuffer: 256 * 1024 * 1024 });
|
|
21
25
|
if (r.stdout) process.stdout.write(r.stdout);
|
package/driver/runtime.js
CHANGED
|
@@ -7,8 +7,9 @@
|
|
|
7
7
|
// (test/layer1/hook-harness.js); each agent is answered by a generative stand-in (`standin(call)`); the
|
|
8
8
|
// hooks are the real ones, run in `env`.
|
|
9
9
|
// live — one clean headless Claude Code session per workflow (decision 15), memory-bounded with
|
|
10
|
-
// bin/bounded (this package's memory bound), the workflow
|
|
11
|
-
//
|
|
10
|
+
// bin/bounded (this package's memory bound), the workflow, its agent definitions (as FILES: the Writer has ~150 of
|
|
11
|
+
// them, one --agents argument would pass Linux's 128 KB argument cap) and its hook settings in a BUNDLE under the session's
|
|
12
|
+
// folder — OUTSIDE the codeline the session works in (--plugin-dir, --settings): the pipeline puts nothing in a client's checkout. Live runs
|
|
12
13
|
// spend tokens: liveCommand() only builds the command; runLive() refuses unless the operator's yes is
|
|
13
14
|
// given (EPAM_DRIVER_LIVE_APPROVED=yes AND opts.approved === true). Never run by any test.
|
|
14
15
|
const fs = require('fs');
|
|
@@ -19,8 +20,8 @@ const { spawnSync } = require('child_process');
|
|
|
19
20
|
const ROOT = path.resolve(__dirname, '..');
|
|
20
21
|
const { recordSession } = require('./session-cost.js');
|
|
21
22
|
|
|
22
|
-
|
|
23
|
-
const
|
|
23
|
+
/** The plugin the session's bundle is: its agents are named "<PLUGIN>:<agent>" and its workflows run as "<PLUGIN>:<workflow>". */
|
|
24
|
+
const PLUGIN = 'amsd';
|
|
24
25
|
|
|
25
26
|
function toMarkdown(name, def) {
|
|
26
27
|
// generator/agents.js toMarkdown, from the agents JSON entry (frontmatter keys + `prompt` as the body)
|
|
@@ -33,23 +34,55 @@ function toMarkdown(name, def) {
|
|
|
33
34
|
return `---${yaml({ name, ...fm })}\n---\n\n${String(prompt || '').trim()}\n`;
|
|
34
35
|
}
|
|
35
36
|
|
|
37
|
+
/** The end of a script's `export const meta = {…}` literal (and the rest of its line): braces inside strings do not count. */
|
|
38
|
+
function metaEnd(src) {
|
|
39
|
+
const m = /^export const meta = \{/.exec(src);
|
|
40
|
+
if (!m) throw new Error('a workflow script must begin with `export const meta = {…}`');
|
|
41
|
+
let depth = 0; let quote = '';
|
|
42
|
+
for (let i = m[0].length - 1; i < src.length; i += 1) {
|
|
43
|
+
const c = src[i];
|
|
44
|
+
if (quote) { if (c === '\\') i += 1; else if (c === quote) quote = ''; continue; }
|
|
45
|
+
if (c === "'" || c === '"' || c === '`') quote = c;
|
|
46
|
+
else if (c === '{') depth += 1;
|
|
47
|
+
else if (c === '}' && (depth -= 1) === 0) { const nl = src.indexOf('\n', i); return nl < 0 ? src.length : nl + 1; }
|
|
48
|
+
}
|
|
49
|
+
throw new Error('the workflow script\'s `export const meta` literal is not closed');
|
|
50
|
+
}
|
|
51
|
+
|
|
36
52
|
/**
|
|
37
|
-
*
|
|
38
|
-
*
|
|
53
|
+
* THE PLUGIN NAMES ITS AGENTS "<plugin>:<agent>" (an unprefixed type is "not found", tried 2026-10-21) and the generated scripts name them bare:
|
|
54
|
+
* the placed copy shadows agent() with one that adds the prefix where the call is made. Nothing in the generated script changes.
|
|
55
|
+
*/
|
|
56
|
+
function agentPrelude(plugin) {
|
|
57
|
+
return `const __amsdAgent = globalThis.agent\nconst agent = (p, o) => __amsdAgent(p, o && o.agentType && !String(o.agentType).includes(':') ? { ...o, agentType: '${plugin}:' + o.agentType } : o)\n`;
|
|
58
|
+
}
|
|
59
|
+
function withAgentPrefix(src, plugin = PLUGIN) {
|
|
60
|
+
const at = metaEnd(src);
|
|
61
|
+
return `${src.slice(0, at)}${agentPrelude(plugin)}${src.slice(at)}`;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* The live session, prepared but not started: its bundle and the exact command. Writes only under sessionDir — NEVER in the folder the session
|
|
66
|
+
* works in (operator, 2026-10-21: the pipeline puts nothing in a client's checkout; run 11's workflow file there became a deliverable, was
|
|
67
|
+
* restored by the Writer and run as a test by the client's jest).
|
|
68
|
+
*
|
|
69
|
+
* <sessionDir>/bundle/ the plugin (--plugin-dir): .claude-plugin/plugin.json, workflows/<name>.js, agents/<agent>.md
|
|
70
|
+
* <sessionDir>/settings.json the agents' hooks (--settings): one dispatcher over <sessionDir>/hook-map.json
|
|
71
|
+
*
|
|
72
|
+
* THE FOLDER THE AGENTS WORK IN (cwd): prompts' paths are relative to the codeline, as the hub runs every agent from its PROJECT_ROOT
|
|
73
|
+
* (lib/relative-paths.js), so the session starts there. Without one, the session's own folder.
|
|
39
74
|
*/
|
|
40
75
|
function liveCommand({ name, script, agentsJson, args, sessionDir, env, cwd }) {
|
|
41
|
-
// THE FOLDER THE AGENTS WORK IN (cwd): prompts' paths are relative to the codeline, as the hub runs every agent from its
|
|
42
|
-
// PROJECT_ROOT (lib/relative-paths.js), so the session starts there; its .claude/ is listed in that copy's own
|
|
43
|
-
// .git/info/exclude — never committed, never reported as a change. Without one, the session's own folder.
|
|
44
76
|
const home = cwd || sessionDir;
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
const
|
|
48
|
-
fs.mkdirSync(agentsDir, { recursive: true }); fs.mkdirSync(wfDir, { recursive: true });
|
|
77
|
+
const bundle = path.join(sessionDir, 'bundle');
|
|
78
|
+
fs.rmSync(bundle, { recursive: true, force: true }); // a later session in this folder must not inherit an agent this one does not have
|
|
79
|
+
const agentsDir = path.join(bundle, 'agents'); const wfDir = path.join(bundle, 'workflows');
|
|
80
|
+
fs.mkdirSync(agentsDir, { recursive: true }); fs.mkdirSync(wfDir, { recursive: true }); fs.mkdirSync(path.join(bundle, '.claude-plugin'), { recursive: true });
|
|
81
|
+
fs.writeFileSync(path.join(bundle, '.claude-plugin', 'plugin.json'), `${JSON.stringify({ name: PLUGIN, version: '1.0.0', description: 'The pipeline\'s workflow, agents and hooks for one session' })}\n`);
|
|
49
82
|
const defs = JSON.parse(fs.readFileSync(agentsJson, 'utf8'));
|
|
50
|
-
// THE AGENTS' HOOKS RUN FROM THE SESSION'S
|
|
51
|
-
//
|
|
52
|
-
//
|
|
83
|
+
// THE AGENTS' HOOKS RUN FROM THE SESSION'S SETTINGS (driver/hook-dispatch.js): the real Workflow tool does not run a hook in a workflow
|
|
84
|
+
// agent's frontmatter, and does run one in the settings it is given, whose input names the agent (tools/hook-probe.js, 2026-10-18).
|
|
85
|
+
// The frontmatter is written without them, so nothing could ever run twice.
|
|
53
86
|
const map = {}; const matchers = new Set(); let timeout = 0;
|
|
54
87
|
for (const [n, d] of Object.entries(defs)) {
|
|
55
88
|
for (const g of (((d.hooks || {}).PreToolUse) || [])) for (const h of (g.hooks || [])) {
|
|
@@ -60,41 +93,21 @@ function liveCommand({ name, script, agentsJson, args, sessionDir, env, cwd }) {
|
|
|
60
93
|
const { hooks: _h, ...noHooks } = d;
|
|
61
94
|
fs.writeFileSync(path.join(agentsDir, `${n}.md`), toMarkdown(n, noHooks));
|
|
62
95
|
}
|
|
63
|
-
const mapFile = path.join(
|
|
64
|
-
const settingsFile = path.join(
|
|
65
|
-
let settings = {}; try { settings = JSON.parse(fs.readFileSync(settingsFile, 'utf8')); } catch { settings = {}; }
|
|
96
|
+
const mapFile = path.join(sessionDir, 'hook-map.json'); fs.writeFileSync(mapFile, JSON.stringify(map, null, 2));
|
|
97
|
+
const settingsFile = path.join(sessionDir, 'settings.json');
|
|
66
98
|
const dispatch = `${JSON.stringify(process.execPath)} ${JSON.stringify(path.join(__dirname, 'hook-dispatch.js'))} ${JSON.stringify(mapFile)}`;
|
|
67
|
-
const pre = (
|
|
68
|
-
|
|
69
|
-
|
|
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
|
-
}
|
|
77
|
-
fs.copyFileSync(script, path.join(wfDir, `${name}.js`));
|
|
99
|
+
const pre = Object.keys(map).length ? [{ matcher: [...matchers].join('|') || '.*', hooks: [{ type: 'command', command: dispatch, ...(timeout ? { timeout } : {}) }] }] : [];
|
|
100
|
+
fs.writeFileSync(settingsFile, JSON.stringify({ hooks: { PreToolUse: pre } }, null, 2));
|
|
101
|
+
fs.writeFileSync(path.join(wfDir, `${name}.js`), withAgentPrefix(fs.readFileSync(script, 'utf8')));
|
|
78
102
|
// THE RUNNER WAITS FOR ITS WORKFLOW (LIVE-AMSD-2974-3, 2026-10-18: the runner answered "It's still running in the
|
|
79
103
|
// background…" and the session exited, taking the Spec workflow down with it): it answers only with the finished result
|
|
80
104
|
// the session itself waits for its workflow (CLAUDE_CODE_PRINT_BG_WAIT_CEILING_MS=0 below); the runner has no task-wait tool
|
|
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.`;
|
|
105
|
+
const prompt = `Run the workflow "${PLUGIN}:${name}" with these args, exactly: ${JSON.stringify(args || {})}. When it has finished, reply with its result as JSON and nothing else.`;
|
|
82
106
|
const claude = (env && env.CLAUDE_BIN) || 'claude';
|
|
83
107
|
// 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
108
|
const argv = [path.join(__dirname, '..', 'bin', 'bounded'), path.join(__dirname, '..', 'bin', 'cpu-bound'), claude, '-p', prompt, '--output-format', 'json',
|
|
85
|
-
'--setting-sources', 'project', '--permission-mode', 'bypassPermissions'];
|
|
86
|
-
return { cwd: home, argv, agents: Object.keys(defs).length, workflow: path.join(wfDir, `${name}.js`) };
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
/** A pattern in a repository's own, uncommitted exclude list (.git/info/exclude), once. */
|
|
90
|
-
function excludeLocally(repo, pattern) {
|
|
91
|
-
const g = spawnSync('git', ['-C', repo, 'rev-parse', '--git-path', 'info/exclude'], { encoding: 'utf8' });
|
|
92
|
-
if (g.status !== 0) return;
|
|
93
|
-
const f = path.resolve(repo, g.stdout.trim());
|
|
94
|
-
const cur = fs.existsSync(f) ? fs.readFileSync(f, 'utf8') : '';
|
|
95
|
-
if (cur.split('\n').includes(pattern)) return;
|
|
96
|
-
fs.mkdirSync(path.dirname(f), { recursive: true });
|
|
97
|
-
fs.writeFileSync(f, `${cur.replace(/\n?$/, cur ? '\n' : '')}${pattern}\n`);
|
|
109
|
+
'--plugin-dir', bundle, '--settings', settingsFile, '--setting-sources', 'project', '--permission-mode', 'bypassPermissions'];
|
|
110
|
+
return { cwd: home, argv, agents: Object.keys(defs).length, workflow: path.join(wfDir, `${name}.js`), bundle, settings: settingsFile };
|
|
98
111
|
}
|
|
99
112
|
|
|
100
113
|
/** The pipeline's tool commands as named variables (llm-handler.sh:240–245: lib/pipeline-tools.js commandEnv(), exported
|
|
@@ -219,11 +232,11 @@ async function run(o) {
|
|
|
219
232
|
// carry its story, as the story's attempts do in the pipeline
|
|
220
233
|
if (res && (res.__session || !res.__throw) && opts.env.SCRIPT_DIR && (opts.env.PHASE_COST_FILE || opts.env.LOG_DIR)) {
|
|
221
234
|
res.__cost = recordSession(opts.env, res.__session, { stage: opts.name, storyId: opts.costStory || null, phase: opts.env.PHASE || null,
|
|
222
|
-
projectsDir: res.__session && res.__session.projectsDir, log: opts.log });
|
|
235
|
+
projectsDir: res.__session && res.__session.projectsDir, agentsDir: opts.sessionDir ? path.join(opts.sessionDir, 'bundle', 'agents') : undefined, log: opts.log });
|
|
223
236
|
await Promise.all(res.__cost.traces || []);
|
|
224
237
|
}
|
|
225
238
|
if (res && res.__throw) throw res.__throw;
|
|
226
239
|
return res;
|
|
227
240
|
}
|
|
228
241
|
|
|
229
|
-
module.exports = { run, liveCommand, toMarkdown };
|
|
242
|
+
module.exports = { run, liveCommand, toMarkdown, withAgentPrefix, agentPrelude, metaEnd, PLUGIN };
|
package/driver/session-cost.js
CHANGED
|
@@ -41,7 +41,7 @@ function transcriptTokens(file) {
|
|
|
41
41
|
* toolCallsInTranscript, read from the call's own transcript). For a workflow agent: its prompt is the first user turn of its
|
|
42
42
|
* transcript, its reply the answer it gave (its StructuredOutput, else its last text), its tool calls by the pipeline's own
|
|
43
43
|
* reader. Operator, 2026-10-18: "I need all llm calls captured"; "Did you not read the code from the current pipeline?" */
|
|
44
|
-
function callContent(file, scriptsDir, agentName) {
|
|
44
|
+
function callContent(file, scriptsDir, agentName, agentsDir) {
|
|
45
45
|
const lines = fs.readFileSync(file, 'utf8').split('\n').filter((l) => l.trim()).map((l) => { try { return JSON.parse(l); } catch { return null; } }).filter(Boolean);
|
|
46
46
|
const first = lines.find((d) => d.type === 'user' && d.message);
|
|
47
47
|
const asst = lines.filter((d) => d.type === 'assistant' && d.message && Array.isArray(d.message.content)).flatMap((d) => d.message.content);
|
|
@@ -50,11 +50,11 @@ function callContent(file, scriptsDir, agentName) {
|
|
|
50
50
|
// eslint-disable-next-line global-require, import/no-dynamic-require
|
|
51
51
|
const tx = require(path.join(scriptsDir, 'lib', 'transcript-tool-calls.js'));
|
|
52
52
|
const ts = lines.map((d) => d.timestamp).filter(Boolean);
|
|
53
|
-
// the agent's standing instructions: its definition in the session's
|
|
54
|
-
// driver/runtime.js liveCommand) — sent to the model as its system prompt, not in the transcript
|
|
55
|
-
const
|
|
53
|
+
// the agent's standing instructions: its definition in the session's bundle (<session>/bundle/agents/<agent>.md, written by
|
|
54
|
+
// driver/runtime.js liveCommand; the plugin names the agent "<plugin>:<agent>") — sent to the model as its system prompt, not in the transcript
|
|
55
|
+
const agentType = String((lines.find((d) => d.agentType) || {}).agentType || agentName || '').replace(/^.*:/, '');
|
|
56
56
|
let system = '';
|
|
57
|
-
try { const md = fs.readFileSync(path.join(
|
|
57
|
+
try { const md = fs.readFileSync(path.join(agentsDir, `${agentType}.md`), 'utf8'); system = md.replace(/^---[\s\S]*?\n---\n+/, ''); } catch { system = ''; }
|
|
58
58
|
const prompt = first ? first.message.content : '';
|
|
59
59
|
return { input: system ? { system, prompt } : prompt, output: so ? JSON.stringify(so.input) : (text || ''), // text: the emitter's output is text
|
|
60
60
|
toolCalls: tx.toolCallsInTranscript(file), startedAt: ts[0] || null, endedAt: ts[ts.length - 1] || null };
|
|
@@ -85,7 +85,8 @@ function sessionEntries(projectsDir, sessionId) {
|
|
|
85
85
|
for (const f of fs.readdirSync(rd).filter((x) => /^agent-.*\.jsonl$/.test(x)).sort()) {
|
|
86
86
|
let meta = {}; try { meta = JSON.parse(fs.readFileSync(path.join(rd, f.replace(/\.jsonl$/, '.meta.json')), 'utf8')); } catch { meta = {}; }
|
|
87
87
|
const label = meta.description || f;
|
|
88
|
-
|
|
88
|
+
// the plugin names an agent "<plugin>:<agent>" (driver/runtime.js): the ledger and the traces name it as the pipeline does
|
|
89
|
+
push(meta.agentType && meta.agentType !== 'workflow-subagent' ? String(meta.agentType).replace(/^.*:/, '') : label, label, path.join(rd, f));
|
|
89
90
|
}
|
|
90
91
|
}
|
|
91
92
|
}
|
|
@@ -133,7 +134,7 @@ function allocate(entries, modelUsage, prices = {}) {
|
|
|
133
134
|
* Record one session's cost. session: {sessionId, modelUsage, totalCostUsd} (the session's result).
|
|
134
135
|
* Returns {rows, totalCostUsd, exact: bool (per-call rows written), reason, traces: the Langfuse sends, Promise<boolean>[]}.
|
|
135
136
|
*/
|
|
136
|
-
function recordSession(env, session, { stage, storyId, phase, projectsDir, log }) {
|
|
137
|
+
function recordSession(env, session, { stage, storyId, phase, projectsDir, agentsDir, log }) {
|
|
137
138
|
const ledger = env.PHASE_COST_FILE || path.join(env.LOG_DIR, 'phase-cost.jsonl');
|
|
138
139
|
// eslint-disable-next-line global-require, import/no-dynamic-require
|
|
139
140
|
const { appendLedgerRecord } = require(path.join(env.SCRIPT_DIR, 'lib', 'cost-emitter.js'));
|
|
@@ -155,7 +156,7 @@ function recordSession(env, session, { stage, storyId, phase, projectsDir, log }
|
|
|
155
156
|
costIsEstimate: true, costUnknown: e.costUnknown } }));
|
|
156
157
|
// each agent call to Langfuse with its prompt, its answer and its tool calls, as the pipeline's cost seam traces each
|
|
157
158
|
// of its calls (lib/cost-emitter.js → lib/langfuse-emit.js); off without keys
|
|
158
|
-
const c = e.file ? callContent(e.file, env.SCRIPT_DIR, e.agent) : {};
|
|
159
|
+
const c = e.file ? callContent(e.file, env.SCRIPT_DIR, e.agent, agentsDir) : {};
|
|
159
160
|
traces.push(emitGeneration({ agent: e.agent, storyId, phase, model: e.model, startedAt: c.startedAt, endedAt: c.endedAt,
|
|
160
161
|
input: c.input, output: c.output, toolCalls: c.toolCalls, costUsd: e.costUsd, tokensIn: e.tokens.in, tokensOut: e.tokens.out,
|
|
161
162
|
cacheRead: e.tokens.cacheRead, cacheCreate: e.tokens.cacheCreate, costIsEstimate: true }, { ...process.env, ...env }));
|
package/hooks/attempt-verify.sh
CHANGED
|
@@ -43,12 +43,27 @@ ruled=false
|
|
|
43
43
|
# the rung attribution.
|
|
44
44
|
retry_count="${ATTEMPT_RETRY:-}"; STORY_MODEL="${ATTEMPT_MODEL:-}"
|
|
45
45
|
_err="$TMPDIR/attempt-verify.log"; _out="$TMPDIR/attempt-output.log"; : > "$_out"
|
|
46
|
-
stage=""
|
|
46
|
+
stage=""; plan_note=""
|
|
47
|
+
# THE STRUCTURED NO-CHANGE ROUTE (run 11, 2026-10-21): verify_story_deliverables excuses "all declared files unchanged" only through the
|
|
48
|
+
# pipeline's prose handler (the consumer's change line is exactly "none"). A consumer whose fix sites ALL say changeRequired:false
|
|
49
|
+
# (generator/plan-no-change-structured.js) has had its plan carried out by a Writer that edited nothing. Only that one failure class
|
|
50
|
+
# (STORY_REJECTION_KEY unchanged-all:) is excused — a missing file or any other finding is not.
|
|
51
|
+
planned_no_change() {
|
|
52
|
+
case "${STORY_REJECTION_KEY:-}" in unchanged-all:*) ;; *) return 1 ;; esac
|
|
53
|
+
local _p _sj
|
|
54
|
+
for _p in "${PRD_FILE:-}" "${MAIN_PRD_FILE:-}"; do
|
|
55
|
+
[ -n "$_p" ] && [ -f "$_p" ] || continue
|
|
56
|
+
_sj=$("$NODE_BIN" "$(dirname "${BASH_SOURCE[0]}")/../generator/plan-no-change-structured.js" "$_p" "$STORY" "$EPAM_CODELINE" 2>/dev/null) || continue
|
|
57
|
+
plan_note="the story's plan gives ${EPAM_CODELINE} no change ($(printf '%s' "$_sj" | jq -r '.reason // empty')) — its declared files are unchanged, as planned"
|
|
58
|
+
STORY_REJECTION_KEY=""; return 0
|
|
59
|
+
done
|
|
60
|
+
return 1
|
|
61
|
+
}
|
|
47
62
|
complete_codeline_manifests "$PROJECT_ROOT" >/dev/null 2>&1
|
|
48
63
|
for s in deliverables typecheck lint tests; do
|
|
49
64
|
: > "$_err"
|
|
50
65
|
case "$s" in
|
|
51
|
-
deliverables) verify_story_deliverables "$STORY" 2>"$_err" ;;
|
|
66
|
+
deliverables) verify_story_deliverables "$STORY" 2>"$_err" || planned_no_change ;;
|
|
52
67
|
typecheck) run_tsc_verification "$STORY" "$_out" 2>"$_err" ;;
|
|
53
68
|
lint) run_repo_lint_verification "$STORY" "$_out" 2>"$_err" ;;
|
|
54
69
|
tests) run_external_verification "$STORY" "$_out" 2>"$_err" \
|
|
@@ -83,6 +98,6 @@ fi
|
|
|
83
98
|
# a passing attempt ends this writer process; a failing one leaves its globals to the failure handling and the next attempt
|
|
84
99
|
if [ "$passed" = true ]; then wp_clear "$STATE"; else wp_save "$STATE/writer-process.json"; fi
|
|
85
100
|
jq -cn --argjson p "$passed" --argjson ruled "$ruled" --arg st "$stage" --argjson d "$det" --arg vf "$VERIFICATION_FAILURE" --rawfile m "$_err" \
|
|
86
|
-
--argjson s "$same" --arg k "$([ "$passed" = false ] && printf '%s' "${STORY_REJECTION_KEY:-}")" --arg t "${LAST_TEST_CMD:-}" \
|
|
101
|
+
--arg pn "$plan_note" --argjson s "$same" --arg k "$([ "$passed" = false ] && printf '%s' "${STORY_REJECTION_KEY:-}")" --arg t "${LAST_TEST_CMD:-}" \
|
|
87
102
|
'{passed:$p, stage:(if $st == "" then null else $st end), deterministic:$d, verificationFailure:$vf, message:$m,
|
|
88
|
-
sameViolation:$s, rejectionKey:$k, testCommand:$t, ruledEnvironmental:$ruled}'
|
|
103
|
+
planNote:$pn, sameViolation:$s, rejectionKey:$k, testCommand:$t, ruledEnvironmental:$ruled}'
|
package/package.json
CHANGED