amsd-pipeline-shared 0.5.16 → 0.5.17

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.
@@ -235,3 +235,4 @@ ff9ea38d594f181363c40295ea42012a prompts/templates/writer-plan-section.json
235
235
  28adf858dafddc3ef7bb03d3686dfe6d scripts/lib/project-prompt-contract.js
236
236
  4120de0abec9f84c3b0fa98cd2584233 scripts/lib/worktree-rungs.sh
237
237
  515f80e7a9228cdfbc39e71c06066d95 scripts/lib/story-attempt.sh
238
+ 6a31e809386b4114794bb773c850b46b scripts/lib/producers/fix-plan.js
@@ -223,6 +223,7 @@ scripts/lib/plan-of-record.sh
223
223
  scripts/lib/plan-scope.js
224
224
  scripts/lib/preflight.sh
225
225
  scripts/lib/prelaunch.js
226
+ scripts/lib/producers/fix-plan.js
226
227
  scripts/lib/project-prompt-contract.js
227
228
  scripts/lib/qa-gate-evidence.sh
228
229
  scripts/lib/relative-paths.js
@@ -3,7 +3,8 @@
3
3
  "Operator, 2026-10-06: one plan for the story, written once and passed to every codeline as a shared artefact.",
4
4
  "The first detective writes it (detective-overall-plan); the detective of every later codeline is given it here and",
5
5
  "investigates its own codeline's part, rather than planning the story again from one codeline's view.",
6
- "2026-10-06, real-model rounds: given a plan that gave it no change, a consumer lane marked its lock file and locale files as requiring change."
6
+ "2026-10-06, real-model rounds: given a plan that gave it no change, a consumer lane marked its lock file and locale files as requiring change.",
7
+ "v2 (operator, 2026-10-22: \"If the plan agent is not given proper instructions to provide package updates then there is your rc\"): a consumer's plan names the update that brings in the provider's release, and any change of its own its criteria need — run 11's plan gave gotransit 'no change', it never received cx-shared 8.12.0, and its Writer looped."
7
8
  ],
8
9
  "description": "Composed into the code-graph detective's prescription once the story has an overall plan: work this codeline's part of it, and say plainly where this codeline's code contradicts it.",
9
10
  "id": "detective-overall-plan-given",
@@ -14,6 +15,6 @@
14
15
  "__OVERALL_PLAN__",
15
16
  "__THIS_CODELINE__"
16
17
  ],
17
- "version": 1,
18
- "body": "\nTHIS STORY ALREADY HAS ITS PLAN, written by the first investigation for every codeline it is worked in. You are investigating codeline \"__THIS_CODELINE__\": locate the fix sites for ITS part of the plan below, and nothing the plan gives to another codeline.\n__OVERALL_PLAN__\nWhere the plan gives this codeline no change of its own, none of your fix sites requires a change — receiving the provider's release (manifest, lock file, install) is not this codeline's work in this story.\nThe plan is the story's, not a lead to re-derive. Where this codeline's own code contradicts it — a use of the changed code the plan does not list, a part that cannot be done as stated — say so in the fix site's reason, naming what you found; do not write a different plan.\n"
18
+ "version": 2,
19
+ "body": "\nTHIS STORY ALREADY HAS ITS PLAN, written by the first investigation for every codeline it is worked in. You are investigating codeline \"__THIS_CODELINE__\": locate the fix sites for ITS part of the plan below, and nothing the plan gives to another codeline.\n__OVERALL_PLAN__\nWhere this codeline consumes the provider's package, the plan's `update` for it (the lock file or its own update script, within its declared range) is this codeline's work, and so is its `change`, where it has one. Where the plan gives it no change of its own beyond the update, none of your other fix sites requires a change — and a note that a file needs no edit is never written into that file.\nThe plan is the story's, not a lead to re-derive. Where this codeline's own code contradicts it — a use of the changed code the plan does not list, a part that cannot be done as stated — say so in the fix site's reason, naming what you found; do not write a different plan.\n"
19
20
  }
@@ -11,7 +11,8 @@
11
11
  "2026-10-06, real-model rounds: the plan wrote a consumer's change as \"none in shared code; update installed package within range\", and that consumer's lane then prescribed its lock file.",
12
12
  "",
13
13
  "2026-10-06, real-model round: a consumer's change read \"none (dependency bump only; review own toast messages)\", and that lane then changed its locale file.",
14
- "2026-10-06: a shared codeline is worked once per release line the plan names (lib/release-lines.sh); the plan says which line the codeline's own base is on, so no version arithmetic is written into the engine."
14
+ "2026-10-06: a shared codeline is worked once per release line the plan names (lib/release-lines.sh); the plan says which line the codeline's own base is on, so no version arithmetic is written into the engine.",
15
+ "v5 (operator, 2026-10-22: \"If the plan agent is not given proper instructions to provide package updates then there is your rc\"): a consumer's plan names the update that brings in the provider's release, and any change of its own its criteria need — run 11's plan gave gotransit 'no change', it never received cx-shared 8.12.0, and its Writer looped."
15
16
  ],
16
17
  "description": "Composed into every code-graph detective's prescription: before naming fix sites, write the one plan the whole story is worked from — every codeline's part, every consumer's blast radius, every release line.",
17
18
  "id": "detective-overall-plan",
@@ -21,6 +22,6 @@
21
22
  "placeholders": [
22
23
  "__PLAN_CODELINES__"
23
24
  ],
24
- "version": 4,
25
- "body": "\nTHE OVERALL PLAN. This story is worked in the codelines below, and every one of them is worked from the plan you write here — it is the one plan for the whole story, not this codeline's notes. You can read each codeline at the location given.\n__PLAN_CODELINES__\nEstablish, by reading the code, not by assuming:\n1. EACH CODELINE'S PART: what it must change in its own code, or that it changes nothing — and the order the codelines must be worked in, when one needs another's change first.\n2. FOR EVERY PACKAGE ONE OF THESE CODELINES PROVIDES AND ANOTHER CONSUMES: the BLAST RADIUS — in each consumer, the files and calls that reach the code being changed, read in that consumer; and whether the change alters anything those calls depend on.\n3. THE RELEASE LINES the change must ship on, so that every consumer receives it through the range it ALREADY declares. A consumer's declared range is never changed by this story. Say how each consumer receives its release: within its declared range (an update the range already allows) — never by installing the latest release, which can move the range; read what a consumer's own scripts do before naming one. Each line is worked on its own branch from its base; say for each whether the provider codeline's own base is already on it (onCodelineBase). One line per distinct line the consumers' ranges require; for each, the base it is cut from (the newest existing release on that line, from the release facts above) and the version it ships as.\n\nWrite the plan as ONE JSON object between these tags, before your fix-site answer:\n<OVERALL_PLAN>\n{\"codelines\":[{\"codeline\":\"<name>\",\"part\":\"<what it changes in its own code, or that it changes nothing and how it receives the change>\",\"order\":1}],\"packages\":[{\"package\":\"<name, as its manifest declares it>\",\"providedBy\":\"<codeline>\",\"releaseLines\":[{\"line\":\"<the release line>\",\"base\":\"<the existing release it is cut from>\",\"version\":\"<the version it ships as>\",\"onCodelineBase\":<true if the provider codeline's own base is on this line — even when newer releases were cut on it since; the change merges forward onto it. false only for a different, older line>,\"carries\":\"<what this line carries: for a line off the codeline's own base, the change made on the codeline's own line, ported in full: the story's whole change on the codeline's own line — what is already there and what this plan adds — every part of it, adapted where this line's code differs>\"}],\"consumers\":[{\"codeline\":\"<codeline>\",\"declaredRange\":\"<as its manifest declares it>\",\"receivesVia\":\"<the version, from the release lines above>\",\"usage\":[\"<file>: <how it reaches the code being changed>\"],\"change\":\"<exactly none, or the change this consumer makes in its OWN code for this story — never none plus something to do or review>\"}]}]}\n</OVERALL_PLAN>\nReceiving the provider's release is never a consumer's change in this story — not its manifest, not its lock file, not an install: that release does not exist until the provider ships it.\nYour fix sites, after the plan, are files of THIS codeline only. Another codeline's files belong in the plan — its part and its usage — never among your fix sites.\nA story worked in ONE codeline writes the same plan: one entry under codelines, and packages empty unless that codeline provides one another codeline consumes.\n"
25
+ "version": 5,
26
+ "body": "\nTHE OVERALL PLAN. This story is worked in the codelines below, and every one of them is worked from the plan you write here — it is the one plan for the whole story, not this codeline's notes. You can read each codeline at the location given.\n__PLAN_CODELINES__\nEstablish, by reading the code, not by assuming:\n1. EACH CODELINE'S PART: what it must change in its own code, or that it changes nothing — and the order the codelines must be worked in, when one needs another's change first.\n2. FOR EVERY PACKAGE ONE OF THESE CODELINES PROVIDES AND ANOTHER CONSUMES: the BLAST RADIUS — in each consumer, the files and calls that reach the code being changed, read in that consumer; and whether the change alters anything those calls depend on.\n3. THE RELEASE LINES the change must ship on, so that every consumer receives it through the range it ALREADY declares. A consumer's declared range is never changed by this story. Say how each consumer receives its release: within its declared range (an update the range already allows) — never by installing the latest release, which can move the range; read what a consumer's own scripts do before naming one. Each line is worked on its own branch from its base; say for each whether the provider codeline's own base is already on it (onCodelineBase). One line per distinct line the consumers' ranges require; for each, the base it is cut from (the newest existing release on that line, from the release facts above) and the version it ships as.\n\nWrite the plan as ONE JSON object between these tags, before your fix-site answer:\n<OVERALL_PLAN>\n{\"codelines\":[{\"codeline\":\"<name>\",\"part\":\"<what it changes in its own code, or that it changes nothing and how it receives the change>\",\"order\":1}],\"packages\":[{\"package\":\"<name, as its manifest declares it>\",\"providedBy\":\"<codeline>\",\"releaseLines\":[{\"line\":\"<the release line>\",\"base\":\"<the existing release it is cut from>\",\"version\":\"<the version it ships as>\",\"onCodelineBase\":<true if the provider codeline's own base is on this line — even when newer releases were cut on it since; the change merges forward onto it. false only for a different, older line>,\"carries\":\"<what this line carries: for a line off the codeline's own base, the change made on the codeline's own line, ported in full: the story's whole change on the codeline's own line — what is already there and what this plan adds — every part of it, adapted where this line's code differs>\"}],\"consumers\":[{\"codeline\":\"<codeline>\",\"declaredRange\":\"<as its manifest declares it>\",\"receivesVia\":\"<the version, from the release lines above>\",\"usage\":[\"<file>: <how it reaches the code being changed>\"],\"update\":\"<the update that brings receivesVia into this consumer within its declared range — the lock file, or what its own update script does (read its scripts)>\",\"change\":\"<exactly none, or the change this consumer makes in its OWN code for this story — for example passing an option the provider adds — never none plus something to do or review>\"}]}]}\n</OVERALL_PLAN>\nRECEIVING THE PROVIDER'S RELEASE IS PART OF EVERY CONSUMER'S WORK in this story: a consumer that does not take the new version never gets the change. Name, for each consumer, the update that brings it in within the range it already declares (`update`) — its declared range itself is not changed. Where the consumer's own criteria need more than the new version — an option the provider adds, its own wording, its own call sites — that is its `change`. A consumer's `change` is code; a note that a file needs no edit is never written into that file.\nYour fix sites, after the plan, are files of THIS codeline only. Another codeline's files belong in the plan — its part and its usage — never among your fix sites.\nA story worked in ONE codeline writes the same plan: one entry under codelines, and packages empty unless that codeline provides one another codeline consumes.\n"
26
27
  }
@@ -7,7 +7,8 @@
7
7
  "criteria left contradicted each other; the change reviewer rejected it on every attempt.",
8
8
  "Appended to the located fix sites only when a finding is shared, so every other story's prompt is",
9
9
  "unchanged.",
10
- "Operator, 2026-10-06: a consumer's declared range is never changed by a story ('avoid package.json upgrades in this change'; a range may be locked for a reason); the provider ships the change on a release line inside each consumer's range."
10
+ "Operator, 2026-10-06: a consumer's declared range is never changed by a story ('avoid package.json upgrades in this change'; a range may be locked for a reason); the provider ships the change on a release line inside each consumer's range.",
11
+ "v3 (operator, 2026-10-22: \"If the plan agent is not given proper instructions to provide package updates then there is your rc\"): a consumer's plan names the update that brings in the provider's release, and any change of its own its criteria need — run 11's plan gave gotransit 'no change', it never received cx-shared 8.12.0, and its Writer looped."
11
12
  ],
12
13
  "description": "Composed into the spec agent's located fix sites when the detective reported a shared fix site: the shared facts, and how a codeline that consumes or provides a shared change is verified.",
13
14
  "id": "spec-shared-consumer",
@@ -15,6 +16,6 @@
15
16
  "placeholders": [
16
17
  "__SHARED_FACTS__"
17
18
  ],
18
- "version": 2,
19
- "body": "\nSHARED WITH ANOTHER CODELINE OF THIS STORY — the investigation reported:\n__SHARED_FACTS__\nHOW A SHARED CHANGE IS VERIFIED HERE. Where this codeline CONSUMES a package another codeline provides, its declared range is never part of this codeline's change: where the range does not take the version, the provider ships the change on a release line inside it, and this codeline receives it unedited. The dependency entry is not what this codeline's verification observes either: a criterion that reads the dependency entry, the lock file or what an install resolves describes HOW, not what a tester sees, and will be rejected. State each of this codeline's criteria as behaviour a tester observes through THIS codeline's own user-facing surface once it builds against the changed package from a release its declared range already takes — the place in this codeline that shows the shared behaviour now does what the story asks. Keep the description consistent with those criteria: never say this codeline's verification is limited to the dependency range, and never put out of scope the behaviour the criteria observe. Where this codeline PROVIDES the package, its criteria observe the behaviour through its own tests or its own surface. NEVER DECLARE ANOTHER CODELINE'S CHANGE out of scope, \"tracked separately\" or \"not needed\" unless the ticket itself says so: this story spans those codelines, and each of them is specified in its own right.\n"
19
+ "version": 3,
20
+ "body": "\nSHARED WITH ANOTHER CODELINE OF THIS STORY — the investigation reported:\n__SHARED_FACTS__\nHOW A SHARED CHANGE IS VERIFIED HERE. Where this codeline CONSUMES a package another codeline provides, its declared range is never part of this codeline's change: where the range does not take the version, the provider ships the change on a release line inside it, and this codeline receives it through the update its plan names. The dependency entry is not what this codeline's verification observes either: a criterion that reads the dependency entry, the lock file or what an install resolves describes HOW, not what a tester sees, and will be rejected. State each of this codeline's criteria as behaviour a tester observes through THIS codeline's own user-facing surface once it builds against the changed package from a release its declared range already takes — the place in this codeline that shows the shared behaviour now does what the story asks. Keep the description consistent with those criteria: never say this codeline's verification is limited to the dependency range, and never put out of scope the behaviour the criteria observe. Where this codeline PROVIDES the package, its criteria observe the behaviour through its own tests or its own surface. NEVER DECLARE ANOTHER CODELINE'S CHANGE out of scope, \"tracked separately\" or \"not needed\" unless the ticket itself says so: this story spans those codelines, and each of them is specified in its own right.\n"
20
21
  }
@@ -0,0 +1,327 @@
1
+ /**
2
+ * THE CODE-GRAPH-DETECTIVE RENDERS ITS OWN ANSWER.
3
+ *
4
+ * KIND: fix-plan. PRODUCER: code-graph-detective (see invocation-profiles.json).
5
+ *
6
+ * Before this file, the detective's answer became prompt text in two places, and the two had
7
+ * drifted in opposite directions:
8
+ *
9
+ * claude.sh:2045 deliveryRole, runsIn, the UNVERIFIED and UNGROUNDED warnings, and
10
+ * the "nothing here produces the value" alarm
11
+ * team-lead-review.sh:410 the changeRequired:false marker — which claude.sh never rendered
12
+ *
13
+ * So the writer was never told which sites were deliberately left alone, and the reviewer was
14
+ * never told that a prescribed helper does not exist. Each copy held a fact the other had lost.
15
+ * Nobody decided that; it is simply what two copies do — proven again on 2026-08-13, when
16
+ * deliveryRole was added to one renderer and the other never got it, within the hour.
17
+ *
18
+ * WHAT BELONGS HERE, AND WHAT DOES NOT
19
+ *
20
+ * Here: what the detective FOUND, in the detective's own terms, including how sure it is. It is
21
+ * the only actor that knows what its fields mean.
22
+ *
23
+ * Not here: what a reader should DO about it. "This is the plan of record, apply it" and "this is
24
+ * what the implementer was working from, judge the diff against it" are the same facts under two
25
+ * authorities, and the authority belongs to the consumer that declared the input. Bake one in and
26
+ * this module can only ever serve one consumer, which is where it came from.
27
+ */
28
+ 'use strict';
29
+
30
+ const agentIo = require('../agent-io.js');
31
+
32
+ /** The kind this producer publishes, and who it is published as. Declared once, read from here. */
33
+ const KIND = 'fix-plan';
34
+ const PRODUCER = 'code-graph-detective';
35
+
36
+ /** false only when the producer actually SAID false. Absent is unknown, and unknown is not a defect. */
37
+ function provenFalse(v) {
38
+ return v === false;
39
+ }
40
+
41
+ function renderSite(s) {
42
+ const file = s && s.file ? String(s.file) : '';
43
+ const fn = s && s.function ? String(s.function) : '';
44
+ const reason = s && s.reason != null ? String(s.reason) : '';
45
+
46
+ let line = `- **${file}**`;
47
+ if (fn !== '') line += ` (\`${fn}\`)`;
48
+
49
+ // A site the detective deliberately left alone. Without it a reader assumes an untouched file
50
+ // is an omission — the reviewer used to be the only one told, and it is the reviewer that would
51
+ // otherwise raise a finding about work that was correctly not done.
52
+ if (provenFalse(s.changeRequired)) {
53
+ line += ' [NO EDIT REQUIRED — part of the fix, correctly left unchanged]';
54
+ }
55
+
56
+ line += `: ${reason}`;
57
+
58
+ if (s.deliveryRole === 'produces') {
59
+ line += ' <== **THIS CHANGE PRODUCES THE VALUE** — it is what makes the new or corrected data'
60
+ + ' exist. Without it, every other change here only moves a value that never changed.';
61
+ } else if (s.deliveryRole === 'carries') {
62
+ line += ' (carries the value — moves or re-exposes what something else produced)';
63
+ } else if (s.deliveryRole === 'verifies') {
64
+ line += ' (verify only — no edit expected here)';
65
+ }
66
+
67
+ const runsIn = s.runsIn == null ? '' : String(s.runsIn);
68
+ if (runsIn !== '' && runsIn !== 'n/a') line += ` [runs: ${runsIn}]`;
69
+
70
+ if (s.shared && typeof s.shared === 'object') line += `\n - **Shared:** ${sharedFact(s.shared)}`;
71
+
72
+ const fix = s.fix == null ? '' : String(s.fix);
73
+ if (fix !== '') {
74
+ line += `\n - **Minimal fix:** ${fix}`;
75
+
76
+ // The helper named in the fix was not found in the repository. Live 2026-07-26 an answer of
77
+ // this shape sent a writer looking for a function that had never existed.
78
+ if (provenFalse(s.fixVerified)) {
79
+ line += ` ⚠️ UNVERIFIED: the helper named here (\`${s.helper || '?'}\`) was NOT found in the`
80
+ + ' repo — treat it as a HYPOTHESIS, not fact. Confirm it exists with the CodeGraph tool'
81
+ + ' before importing it; if it does not exist, do not invent it — solve the fix another'
82
+ + ' minimal way.';
83
+ }
84
+
85
+ // The quoted broken code is not in the file, so the diagnosis is about code that does not
86
+ // exist. Live 2026-07-26 exactly this prescribed halving a discount that is never applied,
87
+ // because the real defect was a key mismatch elsewhere.
88
+ if (provenFalse(s.evidenceVerified)) {
89
+ line += ` ⛔ UNGROUNDED DIAGNOSIS: the detective quoted \`${s.brokenLine || '?'}\` as the`
90
+ + ' broken code, but that expression is NOT in this file. The root cause below is'
91
+ + ' therefore a GUESS about code that does not exist — live 2026-07-26 an answer of'
92
+ + ' exactly this shape prescribed halving a discount that is in fact never applied at'
93
+ + ' all, because the real defect was a key mismatch elsewhere. Do NOT implement it as'
94
+ + ' written: re-derive the cause from the actual file contents first, and if the'
95
+ + ' prescribed change has no basis in the code you read, fix what IS broken instead.';
96
+ }
97
+ }
98
+
99
+ return line;
100
+ }
101
+
102
+ /** The detective's "shared" answer in its own terms: what, who provides it, and who receives it. */
103
+ function sharedFact(sh) {
104
+ const consumers = (Array.isArray(sh.consumers) ? sh.consumers : []).map((c) => {
105
+ const range = c.declaredRange ? ` declares ${c.declaredRange}` : '';
106
+ const gets = c.receivesChange === true ? 'receives this change'
107
+ : c.receivesChange === false ? 'does NOT receive this change through that range'
108
+ : 'not known whether it receives this change';
109
+ return `${c.codeline}${range} — ${gets}`;
110
+ });
111
+ let out = `package \`${sh.package}\``;
112
+ if (sh.providedBy) out += `, provided by ${sh.providedBy}`;
113
+ if (sh.shipsInVersion) out += `, the change ships in version ${sh.shipsInVersion}`;
114
+ if (consumers.length) out += `; consumers: ${consumers.join('; ')}`;
115
+ if (sh.note) out += `. ${sh.note}`;
116
+ return out;
117
+ }
118
+
119
+ /**
120
+ * THE STORY'S OVERALL PLAN, as every agent of every codeline reads it — one rendering, so the detective of a
121
+ * later codeline, the spec agent, the writer and both reviewers are told the same plan (2026-10-06). The
122
+ * codeline being worked is marked, so each sees its own part within the whole.
123
+ */
124
+ function overallPlanText(plan, here) {
125
+ if (!plan || !Array.isArray(plan.codelines) || !plan.codelines.length) return '';
126
+ const lines = ['THE OVERALL PLAN for this story — written once, for every codeline it is worked in:'];
127
+ const ordered = [...plan.codelines].sort((a, b) => (a.order == null ? 1e9 : a.order) - (b.order == null ? 1e9 : b.order));
128
+ for (const c of ordered) {
129
+ lines.push(`- ${c.order != null ? `${c.order}. ` : ''}${c.codeline}${c.codeline === here ? ' (THIS CODELINE)' : ''}: ${c.part}`);
130
+ }
131
+ for (const p of (Array.isArray(plan.packages) ? plan.packages : [])) {
132
+ lines.push(`Package \`${p.package}\`${p.providedBy ? `, provided by ${p.providedBy}` : ''}.`);
133
+ // THE LINE THIS PASS WORKS ON (lib/release-lines.sh sets it), so a line's agents know which branch they are on.
134
+ const _thisLine = process.env.EPAM_RELEASE_LINE || '';
135
+ for (const l of p.releaseLines || []) lines.push(` release line ${l.line}: cut from ${l.base}, ships as ${l.version}${_thisLine && l.line === _thisLine ? ' (THIS RELEASE LINE — the one this pass works on)' : ''}${l.carries ? `; carries: ${l.carries}` : ''}`);
136
+ for (const c of p.consumers || []) {
137
+ lines.push(` consumer ${c.codeline}${c.codeline === here ? ' (THIS CODELINE)' : ''}: declares ${c.declaredRange}, receives ${c.receivesVia}${c.update ? `; its update: ${c.update}` : ''}; its own change: ${c.change || 'none'}`);
138
+ for (const u of c.usage || []) lines.push(` uses: ${u}`);
139
+ }
140
+ }
141
+ // WHO SETS THE VERSION (live run 3, 2026-10-07: told "ships as 8.12.0", the writer bumped package.json, and the lock
142
+ // file in a later attempt). The codeline's own release practice decides — read from its history, never assumed.
143
+ if ((Array.isArray(plan.packages) ? plan.packages : []).some((p) => (p.releaseLines || []).length)) {
144
+ lines.push('The version a release line ships as is set the way this codeline makes its releases — its history shows how; this story\'s change is the fix.');
145
+ }
146
+ return lines.join('\n');
147
+ }
148
+
149
+ /**
150
+ * @param {Array|null|undefined} sites the detective's fixSiteAnalysis, verbatim
151
+ * @returns {string} markdown, or '' when there is nothing to say
152
+ */
153
+ function renderFixPlan(sites) {
154
+ const list = Array.isArray(sites) ? sites.filter((s) => s && typeof s === 'object') : [];
155
+ if (list.length === 0) return '';
156
+
157
+ let out = list.map(renderSite).join('\n');
158
+
159
+ // A plan in which every site only carries or verifies obtains nothing. Implementing it exactly
160
+ // yields code that runs, changes nothing anyone can see, and passes every check — which is how
161
+ // a story reaches its retry ceiling with a green diff. Only raised when the detective assigned
162
+ // roles at all: an older answer that was never asked the question is not accused of dodging it.
163
+ const roled = list.filter((s) => s.deliveryRole != null);
164
+ const producers = list.filter((s) => s.deliveryRole === 'produces');
165
+ if (producers.length === 0 && roled.length > 0) {
166
+ out += '\n\n⚠️ NONE OF THESE PRODUCE THE VALUE. Every site above only carries or verifies it —'
167
+ + ' they move, expose or re-render something that already exists. If this story needs a'
168
+ + ' value that does not exist yet, NO prescribed change obtains it, and implementing this'
169
+ + ' plan exactly will produce code that runs, changes nothing a user can see, and passes'
170
+ + ' every check. Find what must read, fetch, query or compute the new value, change that'
171
+ + ' too, and say so in your final message.';
172
+ }
173
+
174
+ return out;
175
+ }
176
+
177
+ /**
178
+ * The sites that belong to THIS story's codeline.
179
+ *
180
+ * Three codelines run as parallel lanes and each has its own answer. The canonical PRD's flat list
181
+ * is the UNION of all of them — on AMSD-2041, 13 sites where gotransit has 4, including three
182
+ * different prescriptions for one file naming five different env vars, two of them mutually
183
+ * exclusive designs. A writer handed three conflicting instructions for the same file picks one,
184
+ * and nothing makes it pick its own lane's.
185
+ *
186
+ * run-agent-orchestration.sh already scopes each lane's PRD this way; doing it here as well means
187
+ * publication is correct from EITHER PRD, rather than depending on which one a caller passes.
188
+ *
189
+ * An explicitly EMPTY entry is honoured: "this lane found nothing" is a real state and differs
190
+ * from "this lane has not run". A codeline ABSENT from the map falls back to the flat list —
191
+ * never to nothing, which would hand a writer an empty plan.
192
+ */
193
+ function sitesFor(story) {
194
+ const per = story && story.fixSiteAnalysisPerCodeline;
195
+ const lane = story && story.codeline;
196
+ if (lane && per && Object.prototype.hasOwnProperty.call(per, lane)) return per[lane];
197
+ return story && story.fixSiteAnalysis;
198
+ }
199
+
200
+ /**
201
+ * A STORY'S PLAN: its own sites, and — when the detective reported a site SHARED between this
202
+ * story's codelines, in this lane or another — what is shared and where each sibling codeline's
203
+ * working copy is, RELATIVE to this codeline (detective-shared-package / fix-plan-shared). A story
204
+ * with nothing shared renders exactly renderFixPlan(sites).
205
+ */
206
+ function renderStoryPlan(story, prd, sites) {
207
+ const own = sites === undefined ? sitesFor(story) : sites;
208
+ return renderFixPlan(own) + renderShared('fix-plan-shared', story, prd, own);
209
+ }
210
+
211
+ /**
212
+ * The shared section alone, under the template a consumer declares: what the detective reported as
213
+ * shared (this lane and its siblings) and where each sibling's working copy is, RELATIVE to this
214
+ * codeline. '' when nothing is shared. One gathering for the writer (fix-plan-shared) and the diff
215
+ * reviewers (review-shared), so the two can never be told different facts.
216
+ */
217
+ function renderShared(templateId, story, prd, sites) {
218
+ const own = sites === undefined ? sitesFor(story) : sites;
219
+ const here = story && story.codeline;
220
+ const lanes = (story && Array.isArray(story.codelines)) ? story.codelines.filter(Boolean) : [];
221
+ const per = (story && story.fixSiteAnalysisPerCodeline) || {};
222
+ const shared = [];
223
+ for (const s of (Array.isArray(own) ? own : [])) {
224
+ if (s && s.shared && typeof s.shared === 'object') shared.push(`- ${here || 'this codeline'}: ${s.file} — ${sharedFact(s.shared)}`);
225
+ }
226
+ for (const cl of lanes.filter((c) => c !== here)) {
227
+ for (const s of (Array.isArray(per[cl]) ? per[cl] : [])) {
228
+ if (s && s.shared && typeof s.shared === 'object') shared.push(`- ${cl}: ${s.file} — ${sharedFact(s.shared)}`);
229
+ }
230
+ }
231
+ // THE STORY'S PLAN COMES FIRST, when there is one: it is what this codeline is worked from.
232
+ const plan = story && story.overallPlan ? overallPlanText(story.overallPlan, here) : '';
233
+ const port = releaseLinePort();
234
+ if (shared.length === 0 && !plan) return port;
235
+
236
+ const dirs = (prd && prd.project && Array.isArray(prd.project.outputDirs)) ? prd.project.outputDirs : [];
237
+ const dirOf = (cl) => (dirs.find((d) => d && d.codeline === cl) || {}).path;
238
+ const hereDir = dirOf(here) || (prd && prd.project && prd.project.outputDir);
239
+ const path = require('path');
240
+ const locations = lanes.filter((c) => c !== here).map((cl) => {
241
+ const there = dirOf(cl);
242
+ return there && hereDir ? `- ${cl}: ${path.relative(hereDir, there)}` : `- ${cl}: not recorded in this run's PRD`;
243
+ });
244
+ // eslint-disable-next-line global-require
245
+ const { renderEngineTemplate } = require('../engine-prompt.js');
246
+ return renderEngineTemplate(templateId, {
247
+ __SHARED_SITES__: [plan, ...shared].filter(Boolean).join('\n'),
248
+ __SIBLING_LOCATIONS__: locations.join('\n') || '- none recorded',
249
+ }) + port;
250
+ }
251
+
252
+ /**
253
+ * ON A RELEASE LINE, THE CHANGE IT PORTS: what the codeline's own line committed for this story, verbatim
254
+ * (lib/release-lines.sh writes it and names it in EPAM_MAIN_LINE_CHANGE). Part of renderShared, so the line's writer
255
+ * and its diff reviewers are given the same change to port and to compare against (AMSD-2974 run 9: both were given
256
+ * only the plan's summary, and a back-port carrying a fraction of the change was approved). '' off a release line,
257
+ * or when the own line committed nothing. A named change that cannot be read is an error, never an empty section.
258
+ */
259
+ function releaseLinePort(env = process.env) {
260
+ const file = env.EPAM_MAIN_LINE_CHANGE || '';
261
+ const line = env.EPAM_RELEASE_LINE || '';
262
+ if (!file || !line) return '';
263
+ const change = require('fs').readFileSync(file, 'utf8');
264
+ if (!change.trim()) return '';
265
+ // eslint-disable-next-line global-require
266
+ const { renderEngineTemplate } = require('../engine-prompt.js');
267
+ return renderEngineTemplate('release-line-port', {
268
+ __RELEASE_LINE__: line,
269
+ __MAIN_LINE__: env.EPAM_MAIN_LINE_BRANCH || 'its own line',
270
+ __MAIN_LINE_CHANGE__: change.replace(/\n$/, ''),
271
+ });
272
+ }
273
+
274
+ /**
275
+ * Publish every story's plan, so consumers stop reading the detective's fields.
276
+ *
277
+ * A story with no sites publishes NOTHING, which clears any plan published earlier. That is the
278
+ * point rather than an optimisation: the detective re-runs, and a re-run that withdraws its
279
+ * findings must retract the plan too. Live 2026-08-13 a re-run degraded every site on three
280
+ * codelines and reported success — had the previous plan survived that, every consumer would have
281
+ * gone on acting on a plan the detective had taken back.
282
+ *
283
+ * @returns {number} how many stories now have a published plan
284
+ */
285
+ function publishFixPlans(prd, env) {
286
+ const stories = (prd && Array.isArray(prd.stories)) ? prd.stories : [];
287
+ let published = 0;
288
+ for (const story of stories) {
289
+ if (!story || !story.id) continue;
290
+ const text = renderStoryPlan(story, prd);
291
+ agentIo.publish(PRODUCER, KIND, story.id, text, env);
292
+ if (text !== '') published += 1;
293
+ }
294
+ return published;
295
+ }
296
+
297
+ module.exports = {
298
+ overallPlanText, renderFixPlan, renderStoryPlan, renderShared, releaseLinePort, publishFixPlans, sharedFact, KIND, PRODUCER };
299
+
300
+ // CLI: node fix-plan.js <prd.json> <story-id> — emits the rendered plan on stdout.
301
+ // The shell consumers call it this way; it is the same function either way, never a second copy.
302
+ if (require.main === module) {
303
+ const argv = process.argv.slice(2);
304
+ try {
305
+ if (argv[0] === '--review-shared') {
306
+ // node fix-plan.js --review-shared <prd.json> <story-id> — the reviewers' shared section, or nothing.
307
+ const prd = JSON.parse(require('fs').readFileSync(argv[1], 'utf8'));
308
+ const story = (prd.stories || []).find((s) => s && s.id === argv[2]);
309
+ process.stdout.write(story ? renderShared('review-shared', story, prd) : '');
310
+ } else if (argv[0] === '--publish') {
311
+ // The orchestrator calls this after any step that can change the plan.
312
+ const prd = JSON.parse(require('fs').readFileSync(argv[1], 'utf8'));
313
+ const n = publishFixPlans(prd);
314
+ process.stderr.write(`[fix-plan] published for ${n} story(ies)\n`);
315
+ } else {
316
+ const [prdPath, storyId] = argv;
317
+ const prd = JSON.parse(require('fs').readFileSync(prdPath, 'utf8'));
318
+ const story = (prd.stories || []).find((s) => s && s.id === storyId);
319
+ process.stdout.write(story ? renderStoryPlan(story, prd, story.fixSiteAnalysis) : '');
320
+ }
321
+ } catch (e) {
322
+ // A rendering that cannot be produced must not look like a detective that found nothing:
323
+ // the caller sees a non-zero status and says so, rather than silently prompting without it.
324
+ process.stderr.write(`[fix-plan] ${(e && e.message) || e}\n`);
325
+ process.exit(1);
326
+ }
327
+ }
@@ -6627,7 +6627,9 @@ function parseOverallPlan(out) {
6627
6627
  ...(str(l.carries) ? { carries: str(l.carries) } : {}) })),
6628
6628
  consumers: (Array.isArray(p.consumers) ? p.consumers : []).filter((c) => c && typeof c.codeline === 'string')
6629
6629
  .map((c) => ({ codeline: c.codeline, declaredRange: str(c.declaredRange), receivesVia: str(c.receivesVia),
6630
- usage: (Array.isArray(c.usage) ? c.usage : []).filter((u) => typeof u === 'string'), change: str(c.change) })),
6630
+ usage: (Array.isArray(c.usage) ? c.usage : []).filter((u) => typeof u === 'string'),
6631
+ // the update that brings the provider's release into this consumer (detective-overall-plan v5; operator, 2026-10-22)
6632
+ ...(str(c.update) ? { update: str(c.update) } : {}), change: str(c.change) })),
6631
6633
  }));
6632
6634
  return { codelines, packages };
6633
6635
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "amsd-pipeline-shared",
3
- "version": "0.5.16",
3
+ "version": "0.5.17",
4
4
  "description": "The AMSD pipeline's shared components, one source for the bash pipeline and its Claude workflows: the model-call gateway (llm-handler.sh — retry, ladder, self-heal, cost) and the libraries it runs.",
5
5
  "license": "UNLICENSED",
6
6
  "bin": {