amsd-pipeline-shared 0.5.17 → 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.
@@ -12,7 +12,8 @@
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
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
+ "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."
16
17
  ],
17
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.",
18
19
  "id": "detective-overall-plan",
@@ -20,8 +21,12 @@
20
21
  "code-graph-detective"
21
22
  ],
22
23
  "placeholders": [
24
+ "__EXISTING_RELEASE_LINES__",
23
25
  "__PLAN_CODELINES__"
24
26
  ],
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"
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
+ ]
27
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": 1,
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
  }
@@ -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 (the only manifest the doctor can read)
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(', ')}`); else out.install.manifest = i.manifest; }
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>: the manifest's entries expanded from the branch's tree, plus the manifest file itself. */
108
- function packaged(repo, branch) {
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, rel);
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 }) {
@@ -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;
@@ -11021,6 +11047,7 @@ function costLabelFor(tag, env) {
11021
11047
  }
11022
11048
 
11023
11049
  module.exports = {
11050
+ overallPlanBlock, existingReleaseLines,
11024
11051
  stampSpecification,
11025
11052
  rewireDependenciesOnSplit,
11026
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.17",
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": {