amsd-pipeline-shared 0.5.16 → 0.5.20
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/docs/gateway-baseline-2.0.80.md5 +1 -0
- package/docs/gateway-files.txt +1 -0
- package/orchestrations/prompts/templates/detective-overall-plan-given.json +4 -3
- package/orchestrations/prompts/templates/detective-overall-plan.json +9 -3
- package/orchestrations/prompts/templates/spec-review-criteria.json +5 -4
- package/orchestrations/prompts/templates/spec-shared-consumer.json +4 -3
- package/orchestrations/scripts/lib/codeline-lanes.sh +56 -0
- package/orchestrations/scripts/lib/doctor/profile.js +5 -2
- package/orchestrations/scripts/lib/doctor/repair.js +14 -7
- package/orchestrations/scripts/lib/producers/fix-plan.js +327 -0
- package/orchestrations/scripts/lib/run-checkpoint.sh +3 -0
- package/orchestrations/scripts/spec-mode-runner.js +30 -1
- package/orchestrations/scripts/team-lead-review.sh +12 -0
- package/package.json +1 -1
|
@@ -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
|
package/docs/gateway-files.txt
CHANGED
|
@@ -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":
|
|
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
|
|
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,9 @@
|
|
|
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.",
|
|
16
|
+
"v6 (operator, 2026-10-22): the plan agent is told the story's existing release lines — run 11's re-plan named the approved '7.x' line '7.x backport', and a second port on a new branch would have been written."
|
|
15
17
|
],
|
|
16
18
|
"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
19
|
"id": "detective-overall-plan",
|
|
@@ -19,8 +21,12 @@
|
|
|
19
21
|
"code-graph-detective"
|
|
20
22
|
],
|
|
21
23
|
"placeholders": [
|
|
24
|
+
"__EXISTING_RELEASE_LINES__",
|
|
22
25
|
"__PLAN_CODELINES__"
|
|
23
26
|
],
|
|
24
|
-
"version":
|
|
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>\
|
|
27
|
+
"version": 6,
|
|
28
|
+
"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. THE LINES THIS STORY ALREADY HAS, with the work on them — reviewed, perhaps approved: __EXISTING_RELEASE_LINES__ A line you need on the same base as one of these IS that line: give it exactly that name, never a second line for the same base. 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",
|
|
29
|
+
"mayBeEmpty": [
|
|
30
|
+
"__EXISTING_RELEASE_LINES__"
|
|
31
|
+
]
|
|
26
32
|
}
|
|
@@ -7,7 +7,8 @@
|
|
|
7
7
|
"split check are both moot on a brownfield ticket, whose AC array is empty by policy -- the",
|
|
8
8
|
"observable checks live in verificationCriteria. Asked anyway, the reviewer reported the",
|
|
9
9
|
"empty array as a hallucination or a persistence failure and lowered qualityScore for it,",
|
|
10
|
-
"which is a gate blocking on a policy it was not told about."
|
|
10
|
+
"which is a gate blocking on a policy it was not told about.",
|
|
11
|
+
"v2 (operator, 2026-10-22): plan and criteria are reconciled before pause 2 — run 11's gotransit plan said 'no change' while its own criterion asked for a toast longer than 5 s; nothing checked one against the other."
|
|
11
12
|
],
|
|
12
13
|
"description": "The criteria a spec-review coordinator judges a story's collaborative spec work against.",
|
|
13
14
|
"id": "spec-review-criteria",
|
|
@@ -15,9 +16,9 @@
|
|
|
15
16
|
"spec-coordinator"
|
|
16
17
|
],
|
|
17
18
|
"placeholders": [],
|
|
18
|
-
"version":
|
|
19
|
+
"version": 2,
|
|
19
20
|
"bodies": {
|
|
20
|
-
"brownfield": "For each story, evaluate the quality of the collaborative spec work:\n1. Did both agents add meaningful, non-overlapping value?\n2. Flag any story needing human review.\n3. PLAN ALIGNMENT — where planAlignmentEvidence is present, it tells you what the\n code-graph-detective SAID it would investigate (detectivePlan) and a deterministic,\n cheap signal (aligned: true/false) for whether the final fixSiteAnalysis shares any\n named symbol/file with that plan. This signal is NOT your verdict — it is evidence, the\n same as MANIFEST EVIDENCE below. A plan can legitimately turn out wrong once real\n exploration starts. Read detectivePlan and fixSiteAnalysis yourself and judge: did the\n detective explain WHY it moved from its plan to its final answer (a stated pivot,\n reasoning that connects the two), or did the answer just change with no explanation?\n The latter is a real defect (a fix has shipped that shared no term with the detective's\n own stated plan, with no explanation, more than once on the same ticket) — set\n planAlignment to justified_deviation when the detective justifies its pivot, or\n unexplained_mismatch when it does not, and say why in reviewNotes. aligned:true needs\n no action.\n\n(Brownfield tickets never split and their acceptance criteria are immutable —\ndo not evaluate split quality or AC completeness; there is nothing there for\neither agent to have changed.\n\nEXPECTED, NOT A DEFECT: an agent's notes may describe acceptance criteria it\nauthored while the story's acceptanceCriteria array is empty, and report\nacceptanceChanged=false. That is this pipeline REDACTING them on purpose: on a\nbrownfield ticket the ACs are the ticket's own, and a thin ticket legitimately\nhas none. The observable checks live in verificationCriteria — judge THOSE.\nDo not report the mismatch as a hallucination, a persistence failure or a\nmetadata inconsistency, and do not lower qualityScore for it. An empty\nacceptanceCriteria array on a brownfield story is correct by policy.)",
|
|
21
|
-
"greenfield": "For each story, evaluate the quality of the collaborative spec work:\n1. Did both agents add meaningful, non-overlapping value?\n2. Are the acceptance criteria complete, testable, and non-overlapping?\n3. Are story splits logical and properly scoped?\n4. Flag any story needing human review."
|
|
21
|
+
"brownfield": "For each story, evaluate the quality of the collaborative spec work:\n1. Did both agents add meaningful, non-overlapping value?\n2. Flag any story needing human review.\n3. PLAN ALIGNMENT — where planAlignmentEvidence is present, it tells you what the\n code-graph-detective SAID it would investigate (detectivePlan) and a deterministic,\n cheap signal (aligned: true/false) for whether the final fixSiteAnalysis shares any\n named symbol/file with that plan. This signal is NOT your verdict — it is evidence, the\n same as MANIFEST EVIDENCE below. A plan can legitimately turn out wrong once real\n exploration starts. Read detectivePlan and fixSiteAnalysis yourself and judge: did the\n detective explain WHY it moved from its plan to its final answer (a stated pivot,\n reasoning that connects the two), or did the answer just change with no explanation?\n The latter is a real defect (a fix has shipped that shared no term with the detective's\n own stated plan, with no explanation, more than once on the same ticket) — set\n planAlignment to justified_deviation when the detective justifies its pivot, or\n unexplained_mismatch when it does not, and say why in reviewNotes. aligned:true needs\n no action.\n\n(Brownfield tickets never split and their acceptance criteria are immutable —\ndo not evaluate split quality or AC completeness; there is nothing there for\neither agent to have changed.\n\nEXPECTED, NOT A DEFECT: an agent's notes may describe acceptance criteria it\nauthored while the story's acceptanceCriteria array is empty, and report\nacceptanceChanged=false. That is this pipeline REDACTING them on purpose: on a\nbrownfield ticket the ACs are the ticket's own, and a thin ticket legitimately\nhas none. The observable checks live in verificationCriteria — judge THOSE.\nDo not report the mismatch as a hallucination, a persistence failure or a\nmetadata inconsistency, and do not lower qualityScore for it. An empty\nacceptanceCriteria array on a brownfield story is correct by policy.)\n\nEVERY VERIFICATION CRITERION HAS A PLANNED CHANGE THAT MAKES IT TRUE. For each verification criterion, name what in the plan makes it observable in this codeline: a fix site whose changeRequired is true, this codeline's own change or update in overallPlanForThisCodeline, or — where this codeline consumes another's package — the provider's change it receives, which its Writer is tested against through the provider's build the pipeline places in this codeline. A criterion nothing planned makes true cannot be met by the writer: flag it as criterion-without-planned-change, severity blocking, with a why that quotes the criterion and names the change that would meet it, or says that the criterion asks for something this story does not change and should be dropped. Do not approve a spec with such a criterion.\n",
|
|
22
|
+
"greenfield": "For each story, evaluate the quality of the collaborative spec work:\n1. Did both agents add meaningful, non-overlapping value?\n2. Are the acceptance criteria complete, testable, and non-overlapping?\n3. Are story splits logical and properly scoped?\n4. Flag any story needing human review.\n\nEVERY VERIFICATION CRITERION HAS A PLANNED CHANGE THAT MAKES IT TRUE. For each verification criterion, name what in the plan makes it observable in this codeline: a fix site whose changeRequired is true, this codeline's own change or update in overallPlanForThisCodeline, or — where this codeline consumes another's package — the provider's change it receives, which its Writer is tested against through the provider's build the pipeline places in this codeline. A criterion nothing planned makes true cannot be met by the writer: flag it as criterion-without-planned-change, severity blocking, with a why that quotes the criterion and names the change that would meet it, or says that the criterion asks for something this story does not change and should be dropped. Do not approve a spec with such a criterion.\n"
|
|
22
23
|
}
|
|
23
24
|
}
|
|
@@ -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":
|
|
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
|
|
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
|
}
|
|
@@ -340,6 +340,40 @@ link_sibling_builds() {
|
|
|
340
340
|
return 0
|
|
341
341
|
}
|
|
342
342
|
|
|
343
|
+
# sibling_link_in_place <root> <vendor-dir> / sibling_unlink_in_place <root> <vendor-dir> — A CONSUMER'S WRITER RUNS AGAINST ITS
|
|
344
|
+
# PROVIDER'S CHANGE (operator, 2026-10-22; backlog B92). The bash pipeline runs a story in a lane worktree whose vendor directory holds
|
|
345
|
+
# the provider's fresh build (link_sibling_builds) while the codeline keeps its installed release for the baseline. A pipeline that
|
|
346
|
+
# works the story in the codeline itself (the Claude workflows) gets the same: the installed release is set aside as
|
|
347
|
+
# <vendor>/<scope>/.<name>.epam-installed and the package link_sibling_builds placed takes its place — for the Writer, after the
|
|
348
|
+
# baseline was taken; unlink puts the installed release back (and is safe to call when nothing is linked).
|
|
349
|
+
sibling_link_in_place() {
|
|
350
|
+
local _root="$1" _vd="$2" _tmp _p _rel _at _aside
|
|
351
|
+
[ -n "$_root" ] && [ -n "$_vd" ] && [ -d "$_root/$_vd" ] || return 0
|
|
352
|
+
sibling_unlink_in_place "$_root" "$_vd"
|
|
353
|
+
_tmp=$(mktemp -d) || return 1
|
|
354
|
+
link_sibling_builds "$_root" "$_tmp" "$_vd"
|
|
355
|
+
while IFS= read -r _p; do
|
|
356
|
+
_rel="${_p#"$_tmp/$_vd/"}"; _rel="${_rel%/.epam-sibling-copy}"
|
|
357
|
+
_at="$_root/$_vd/$_rel"; _aside="$(dirname "$_at")/.$(basename "$_at").epam-installed"
|
|
358
|
+
if [ -e "$_at" ] && [ ! -f "$_at/.epam-sibling-copy" ]; then mv -- "$_at" "$_aside" || continue; fi
|
|
359
|
+
[ -e "$_at" ] && rm -rf -- "${_at:?}"
|
|
360
|
+
mv -- "$(dirname "$_p")" "$_at" && log " [sibling] ${_rel} → the provider's build, in place for the Writer (the installed release is set aside)"
|
|
361
|
+
done < <(find "$_tmp/$_vd" -mindepth 2 -maxdepth 3 -name .epam-sibling-copy -type f 2>/dev/null)
|
|
362
|
+
rm -rf -- "${_tmp:?}"
|
|
363
|
+
return 0
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
sibling_unlink_in_place() {
|
|
367
|
+
local _root="$1" _vd="$2" _aside _at
|
|
368
|
+
[ -n "$_root" ] && [ -n "$_vd" ] && [ -d "$_root/$_vd" ] || return 0
|
|
369
|
+
while IFS= read -r _aside; do
|
|
370
|
+
_at="$(dirname "$_aside")/$(basename "$_aside" .epam-installed)"; _at="$(dirname "$_at")/${_at##*/.}"
|
|
371
|
+
if [ -e "$_at" ] && [ -f "$_at/.epam-sibling-copy" ]; then rm -rf -- "${_at:?}"; fi
|
|
372
|
+
[ -e "$_at" ] || { mv -- "$_aside" "$_at" && log " [sibling] ${_at#"$_root/$_vd/"}: its installed release is back"; }
|
|
373
|
+
done < <(find "$_root/$_vd" -mindepth 1 -maxdepth 2 -name '.*.epam-installed' -type d 2>/dev/null)
|
|
374
|
+
return 0
|
|
375
|
+
}
|
|
376
|
+
|
|
343
377
|
resolve_codeline_node() {
|
|
344
378
|
local codeline_root="$1"
|
|
345
379
|
local pkg="$codeline_root/package.json"
|
|
@@ -707,9 +741,27 @@ record_lanes_pause() {
|
|
|
707
741
|
# marked completed was un-marked 33 s later and a paid writer re-wrote approved work on every resume
|
|
708
742
|
# (AMSD-2974 run 9, resumes 9-12). A lane resumed from its checkpoint (_lane_spec_mode=0 under EPAM_RESUME_RUN)
|
|
709
743
|
# passes nothing; every other lane, and every retry after gate remediation, still resets.
|
|
744
|
+
# APPROVED WORK IS NEVER RESET (operator, 2026-10-22: "we should never throw away approved work"). A lane specified again on a
|
|
745
|
+
# resume (its plan changed) passed --reset, which set its completed stories back to pending, and the Writer cut the branch fresh
|
|
746
|
+
# from its base: LIVE-AMSD-2974-11's rehearsal dropped cx-shared's change approved at 9/10. A lane whose every story has an approval
|
|
747
|
+
# record (review-verdict-<story>.json in the lane's log, written by the reviewer on approval) keeps it — a resume judges it again
|
|
748
|
+
# where the operator asks (EPAM_REREVIEW_APPROVED), never from scratch. The caller's _cl_prd and _lane_log_dir are in scope.
|
|
749
|
+
_lane_stories_approved() {
|
|
750
|
+
local _prd="${1:-}" _log="${2:-}" _id _n=0
|
|
751
|
+
[ -f "$_prd" ] && [ -d "$_log" ] || return 1
|
|
752
|
+
while IFS= read -r _id; do
|
|
753
|
+
[ -n "$_id" ] || continue
|
|
754
|
+
_n=$((_n + 1))
|
|
755
|
+
[ "$(jq -r '.verdict // empty' "$_log/review-verdict-${_id}.json" 2>/dev/null)" = "approved" ] || return 1
|
|
756
|
+
done < <(jq -r '(.stories // [])[] | select(.status != "deprecated") | .id // empty' "$_prd" 2>/dev/null)
|
|
757
|
+
[ "$_n" -gt 0 ]
|
|
758
|
+
}
|
|
759
|
+
|
|
710
760
|
_lane_phase_args() {
|
|
711
761
|
if [ -n "${EPAM_RESUME_RUN:-}" ] && [ "${_lane_spec_mode:-}" = "0" ]; then
|
|
712
762
|
printf ''
|
|
763
|
+
elif [ -n "${EPAM_RESUME_RUN:-}" ] && _lane_stories_approved "${_cl_prd:-}" "${_lane_log_dir:-}"; then
|
|
764
|
+
printf ''
|
|
713
765
|
else
|
|
714
766
|
printf '%s' '--reset'
|
|
715
767
|
fi
|
|
@@ -1049,6 +1101,7 @@ _run_codeline_loop() {
|
|
|
1049
1101
|
_lane_resume_prd() {
|
|
1050
1102
|
local _rcl="$1" _rout="$2"
|
|
1051
1103
|
_lane_spec_mode=""
|
|
1104
|
+
_lane_respecified=""
|
|
1052
1105
|
[ -n "${EPAM_RESUME_RUN:-}" ] || return 1
|
|
1053
1106
|
local _rck="${EPAM_PROJECT_CONFIG_DIR:-${PROJECT_CONFIG_DIR:-}}/runs/${EPAM_RESUME_RUN}/lanes/${_rcl}/checkpoint"
|
|
1054
1107
|
[ -f "$_rck/prd.json" ] && [ -f "$_rck/checkpoint.json" ] || return 1
|
|
@@ -1065,6 +1118,7 @@ _run_codeline_loop() {
|
|
|
1065
1118
|
# A SPEC BUILT FROM INPUTS THAT NO LONGER HOLD IS SPECIFIED AGAIN (lib/run-checkpoint.sh).
|
|
1066
1119
|
if ! lane_spec_is_current "$_rck" "$_rout"; then
|
|
1067
1120
|
log "[orch] Codeline '${_rcl}': not resuming its pre-writer checkpoint — the lane is specified again"
|
|
1121
|
+
_lane_respecified=1
|
|
1068
1122
|
return 1
|
|
1069
1123
|
fi
|
|
1070
1124
|
if _operator_edited "$_rout" "$_rck/reviewed/$(basename "$_rout")"; then
|
|
@@ -1212,6 +1266,7 @@ _run_codeline_loop() {
|
|
|
1212
1266
|
AGENT_IO_DIR="$_lane_log_dir/agent-io" \
|
|
1213
1267
|
PRD_FILE="$_cl_prd" \
|
|
1214
1268
|
EPAM_SPEC_MODE="${_lane_spec_mode:-${EPAM_SPEC_MODE:-}}" \
|
|
1269
|
+
EPAM_LANE_RESPECIFIED="${_lane_respecified:-}" \
|
|
1215
1270
|
PROJECT_ROOT="$_wt" \
|
|
1216
1271
|
OUTPUT_DIR="$_wt" \
|
|
1217
1272
|
PHASE="$_phase" \
|
|
@@ -1459,6 +1514,7 @@ _run_codeline_loop() {
|
|
|
1459
1514
|
AGENT_IO_DIR="$_lane_log_dir/agent-io" \
|
|
1460
1515
|
PRD_FILE="$_cl_prd" \
|
|
1461
1516
|
EPAM_SPEC_MODE="${_lane_spec_mode:-${EPAM_SPEC_MODE:-}}" \
|
|
1517
|
+
EPAM_LANE_RESPECIFIED="${_lane_respecified:-}" \
|
|
1462
1518
|
PROJECT_ROOT="$_wt" \
|
|
1463
1519
|
OUTPUT_DIR="$_wt" \
|
|
1464
1520
|
PHASE="$_phase" \
|
|
@@ -9,7 +9,10 @@ const fs = require('fs');
|
|
|
9
9
|
|
|
10
10
|
const SECTIONS = ['component', 'scope', 'test', 'install', 'crash', 'classify', 'settings', 'restart', 'model', 'logs', 'branch', 'boundaries', 'agentic'];
|
|
11
11
|
const ENV_NAME = /^[A-Z][A-Z0-9_]*$/;
|
|
12
|
-
const MANIFESTS = ['package.json#files']; // how the files a component ships are listed
|
|
12
|
+
const MANIFESTS = ['package.json#files']; // how the files a component ships are listed
|
|
13
|
+
// OR a list file the component's installer copies from (operator, 2026-10-22: the doctor repairs any pipeline code — the shared component installs
|
|
14
|
+
// the files docs/gateway-files.txt lists, from its orchestrations/ folder into <install>/orchestrations): "lines:<list file>:<folder the lines are under>"
|
|
15
|
+
const LINES_MANIFEST = /^lines:[^:\s]+:[^:\s]+$/;
|
|
13
16
|
|
|
14
17
|
function validate(input) {
|
|
15
18
|
const problems = [];
|
|
@@ -72,7 +75,7 @@ function validate(input) {
|
|
|
72
75
|
if (need('install', i, 'dest')) out.install.dest = rel('install.dest', i.dest);
|
|
73
76
|
if (need('install', i, 'requires')) { const v = strs('install.requires', i.requires, { nonEmpty: false }); if (v) { v.forEach((d, n) => rel(`install.requires[${n}]`, d)); out.install.requires = v; } }
|
|
74
77
|
if (need('install', i, 'command')) { const v = strs('install.command', i.command, { nonEmpty: true }); if (v) { for (const ph of ['{worktree}', '{install}']) if (!v.some((x) => x.includes(ph))) bad('install.command', `must contain ${ph}`); out.install.command = v; } }
|
|
75
|
-
if (need('install', i, 'manifest')) { if (!MANIFESTS.includes(i.manifest)) bad('install.manifest', `must be one of: ${MANIFESTS.join(', ')}
|
|
78
|
+
if (need('install', i, 'manifest')) { if (!MANIFESTS.includes(i.manifest) && !LINES_MANIFEST.test(String(i.manifest))) bad('install.manifest', `must be one of: ${MANIFESTS.join(', ')}, or lines:<list file>:<folder>`); else out.install.manifest = i.manifest; }
|
|
76
79
|
if (need('install', i, 'patchRecord')) out.install.patchRecord = file('install.patchRecord', i.patchRecord);
|
|
77
80
|
}
|
|
78
81
|
|
|
@@ -104,21 +104,28 @@ function verifyRepair({ repo, base, branch, profile, layer, abortKind, install }
|
|
|
104
104
|
// ── the reinstall: the component's own installer, then every packaged file compared byte for byte ─────────────────────
|
|
105
105
|
const show = (repo, rev, rel) => spawnSync('git', ['-C', repo, 'show', `${rev}:${rel}`], { encoding: 'buffer', maxBuffer: 64 * 1024 * 1024 });
|
|
106
106
|
|
|
107
|
-
/** The files the component ships at <branch
|
|
108
|
-
|
|
107
|
+
/** The files the component ships at <branch>, each as { repo: its path in the repository, inst: its path under <install>/<dest> }:
|
|
108
|
+
* package.json#files — the manifest's entries expanded from the branch's tree, plus package.json, at the same path; or
|
|
109
|
+
* lines:<list>:<folder> — every line of the list file, found under <folder> in the repository and at the line's own path in the install. */
|
|
110
|
+
function packaged(repo, branch, manifest = 'package.json#files') {
|
|
111
|
+
const m = /^lines:([^:]+):([^:]+)$/.exec(String(manifest));
|
|
112
|
+
if (m) {
|
|
113
|
+
const list = show(repo, branch, m[1]); if (list.status !== 0) return [];
|
|
114
|
+
return list.stdout.toString().split('\n').map((l) => l.trim()).filter(Boolean).sort().map((l) => ({ repo: `${m[2]}/${l}`, inst: l }));
|
|
115
|
+
}
|
|
109
116
|
const pj = show(repo, branch, 'package.json'); if (pj.status !== 0) return [];
|
|
110
117
|
const prefixes = (JSON.parse(pj.stdout.toString()).files || []).map((f) => f.replace(/\/$/, ''));
|
|
111
118
|
const all = git(repo, 'ls-tree', '-r', '--name-only', branch).stdout.split('\n').filter(Boolean);
|
|
112
|
-
return [...new Set([...all.filter((f) => prefixes.some((p) => f === p || f.startsWith(`${p}/`))), 'package.json'])].sort();
|
|
119
|
+
return [...new Set([...all.filter((f) => prefixes.some((p) => f === p || f.startsWith(`${p}/`))), 'package.json'])].sort().map((f) => ({ repo: f, inst: f }));
|
|
113
120
|
}
|
|
114
121
|
|
|
115
|
-
/** The packaged files that are not byte-identical under <install>/<dest> (missing counts). */
|
|
122
|
+
/** The packaged files that are not byte-identical under <install>/<dest> (missing counts) — by their path in the repository. */
|
|
116
123
|
function installedMismatches({ repo, branch, install, profile }) {
|
|
117
124
|
const dest = path.join(install, profile.install.dest);
|
|
118
|
-
return packaged(repo, branch).filter((rel) => {
|
|
119
|
-
const want = show(repo, branch, rel); const f = path.join(dest,
|
|
125
|
+
return packaged(repo, branch, profile.install.manifest).filter(({ repo: rel, inst }) => {
|
|
126
|
+
const want = show(repo, branch, rel); const f = path.join(dest, inst);
|
|
120
127
|
return want.status !== 0 || !fs.existsSync(f) || !want.stdout.equals(fs.readFileSync(f));
|
|
121
|
-
});
|
|
128
|
+
}).map((x) => x.repo);
|
|
122
129
|
}
|
|
123
130
|
|
|
124
131
|
function reinstall({ repo, branch, install, run, attempt, files, profile }) {
|
|
@@ -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
|
+
}
|
|
@@ -168,6 +168,9 @@ _is_known_stage() {
|
|
|
168
168
|
# _pause_already_passed <stage-this-pause-guards>
|
|
169
169
|
_pause_already_passed() {
|
|
170
170
|
local _guards="${1:-}"
|
|
171
|
+
# A LANE SPECIFIED AGAIN IN THIS RUN HAS NOT PASSED THE PAUSE FOR ITS NEW SPEC (operator, 2026-10-22): the pause it passed was
|
|
172
|
+
# for the spec it no longer has — skipping it sent a new plan straight into the Writer with nobody seeing it.
|
|
173
|
+
[ "${EPAM_LANE_RESPECIFIED:-}" = "1" ] && return 1
|
|
171
174
|
# The SNAPSHOT, not a fresh reading. EPAM_RESUMED_FROM_STAGE is published by resume_skip_env at
|
|
172
175
|
# startup and inherited by every lane; a fresh run never sets it, so nothing is ever skipped.
|
|
173
176
|
# Re-deriving here is the trap described above: the run would test state it had just written.
|
|
@@ -1199,6 +1199,9 @@ function buildReviewPayload(stories, isBrownfieldReview, allStories = [], logDir
|
|
|
1199
1199
|
technicalNotes: s.technicalNotes,
|
|
1200
1200
|
specification: s.specification,
|
|
1201
1201
|
fixSiteAnalysis: _fixSiteAnalysis,
|
|
1202
|
+
// THE STORY'S PLAN FOR THIS CODELINE (operator, 2026-10-22): what it changes, receives and updates, as its Writer reads it
|
|
1203
|
+
// (lib/producers/fix-plan.js) — the coordinator judges whether each verification criterion has a planned change behind it
|
|
1204
|
+
...(s.overallPlan ? { overallPlanForThisCodeline: require('./lib/producers/fix-plan.js').overallPlanText(s.overallPlan, s.codeline) } : {}),
|
|
1202
1205
|
planAlignmentEvidence,
|
|
1203
1206
|
...(isBrownfieldReview ? {} : {
|
|
1204
1207
|
splitChildren: (allStories || [])
|
|
@@ -6368,9 +6371,32 @@ function overallPlanBlock(story, prd) {
|
|
|
6368
6371
|
}
|
|
6369
6372
|
return renderEngineTemplate('detective-overall-plan', {
|
|
6370
6373
|
__PLAN_CODELINES__: roots.map((r) => `- ${r.codeline}${r.path ? ` — ${r.path}` : ''}${r.codeline === (story && story.codeline) ? ' (the codeline you are in)' : ''}`).join('\n'),
|
|
6374
|
+
__EXISTING_RELEASE_LINES__: existingReleaseLines(story, roots),
|
|
6371
6375
|
});
|
|
6372
6376
|
}
|
|
6373
6377
|
|
|
6378
|
+
/**
|
|
6379
|
+
* THE RELEASE LINES THE STORY ALREADY HAS (operator, 2026-10-22): its line branches in each codeline, named by the pipeline's one
|
|
6380
|
+
* branch rule (lib/git-ops.sh story_branch_name — "<prefix>AI-<story>" and the line's "-<name>"), each with the release it was cut
|
|
6381
|
+
* from (the newest tag in its history). A re-plan that renamed a line made a second port of approved work. 'none' when there are none.
|
|
6382
|
+
*/
|
|
6383
|
+
function existingReleaseLines(story, roots) {
|
|
6384
|
+
const { spawnSync } = require('child_process');
|
|
6385
|
+
const id = story && story.id; if (!id) return 'none';
|
|
6386
|
+
const stem = spawnSync('bash', ['-c', '. "$1/lib/git-ops.sh" >/dev/null 2>&1; story_branch_name "$2" ""', '_', __dirname, String(id)], { encoding: 'utf8' }).stdout.trim();
|
|
6387
|
+
if (!stem) return 'none';
|
|
6388
|
+
const lines = [];
|
|
6389
|
+
for (const r of roots) {
|
|
6390
|
+
if (!r.path) continue;
|
|
6391
|
+
const refs = spawnSync('git', ['-C', r.path, 'for-each-ref', '--format=%(refname:short)', 'refs/heads/'], { encoding: 'utf8' });
|
|
6392
|
+
for (const b of (refs.status === 0 ? refs.stdout.split('\n') : []).filter((x) => x.startsWith(`${stem}-`))) {
|
|
6393
|
+
const base = spawnSync('git', ['-C', r.path, 'describe', '--tags', '--abbrev=0', b], { encoding: 'utf8' }).stdout.trim();
|
|
6394
|
+
lines.push(`line "${b.slice(stem.length + 1)}" in ${r.codeline} (branch ${b}${base ? `, cut from ${base}` : ''})`);
|
|
6395
|
+
}
|
|
6396
|
+
}
|
|
6397
|
+
return lines.length ? lines.join('; ') + '.' : 'none.';
|
|
6398
|
+
}
|
|
6399
|
+
|
|
6374
6400
|
function detectiveSharedBlock(story, prd) {
|
|
6375
6401
|
const lanes = (story && Array.isArray(story.codelines)) ? story.codelines.filter(Boolean) : [];
|
|
6376
6402
|
const here = story && story.codeline;
|
|
@@ -6627,7 +6653,9 @@ function parseOverallPlan(out) {
|
|
|
6627
6653
|
...(str(l.carries) ? { carries: str(l.carries) } : {}) })),
|
|
6628
6654
|
consumers: (Array.isArray(p.consumers) ? p.consumers : []).filter((c) => c && typeof c.codeline === 'string')
|
|
6629
6655
|
.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'),
|
|
6656
|
+
usage: (Array.isArray(c.usage) ? c.usage : []).filter((u) => typeof u === 'string'),
|
|
6657
|
+
// the update that brings the provider's release into this consumer (detective-overall-plan v5; operator, 2026-10-22)
|
|
6658
|
+
...(str(c.update) ? { update: str(c.update) } : {}), change: str(c.change) })),
|
|
6631
6659
|
}));
|
|
6632
6660
|
return { codelines, packages };
|
|
6633
6661
|
}
|
|
@@ -11019,6 +11047,7 @@ function costLabelFor(tag, env) {
|
|
|
11019
11047
|
}
|
|
11020
11048
|
|
|
11021
11049
|
module.exports = {
|
|
11050
|
+
overallPlanBlock, existingReleaseLines,
|
|
11022
11051
|
stampSpecification,
|
|
11023
11052
|
rewireDependenciesOnSplit,
|
|
11024
11053
|
storyAcsBlock,
|
|
@@ -233,6 +233,15 @@ reviewer_model_for() {
|
|
|
233
233
|
#
|
|
234
234
|
# Absent means the writer named no model, and the seam's own declared rung stands. Nothing here
|
|
235
235
|
# invents one.
|
|
236
|
+
# record_review_approval <story_id> — the approval, recorded where a resume reads it (operator, 2026-10-22: approved work is
|
|
237
|
+
# never reset — lib/codeline-lanes.sh _lane_stories_approved): <LOG_DIR>/review-verdict-<story>.json, the same record the Claude
|
|
238
|
+
# workflows' driver writes. HEAD is the commit the approval judged.
|
|
239
|
+
record_review_approval() {
|
|
240
|
+
jq -cn --arg h "$(git -C "${PROJECT_ROOT:-.}" rev-parse HEAD 2>/dev/null)" --arg at "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
|
|
241
|
+
'{verdict: "approved", head: (if $h == "" then null else $h end), at: $at}' \
|
|
242
|
+
> "${LOG_DIR:-$AUTOMATION_DIR/logs}/review-verdict-${1}.json" 2>/dev/null || true
|
|
243
|
+
}
|
|
244
|
+
|
|
236
245
|
run_review_prompt() {
|
|
237
246
|
local prompt_text="$1"
|
|
238
247
|
local writer_model="$2"
|
|
@@ -978,6 +987,9 @@ $(render_engine_prompt story-diff-not-inlined "$_sdni_vals" excluded)"
|
|
|
978
987
|
success " Review: approved — ${STORY_SUMMARY:-no issues found}"
|
|
979
988
|
# Clear any stale feedback from a prior cycle now that this story passed.
|
|
980
989
|
rm -f "${LOG_DIR:-$AUTOMATION_DIR/logs}/review-feedback-${story_id}.json" 2>/dev/null || true
|
|
990
|
+
# THE APPROVAL IS RECORDED (operator, 2026-10-22): a resume must know this story's work was approved so it is never reset
|
|
991
|
+
# (lib/codeline-lanes.sh _lane_stories_approved) — the same record the Claude workflows' driver writes.
|
|
992
|
+
record_review_approval "$story_id"
|
|
981
993
|
fi
|
|
982
994
|
|
|
983
995
|
# THE REVIEWER'S MEMORY OF THIS STORY. Written HERE, inside the loop, because this is
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "amsd-pipeline-shared",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.20",
|
|
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": {
|