amsd-pipeline-shared 0.5.1 → 0.5.3

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.
@@ -86,3 +86,5 @@ ac166207d8a1d93a4c50a88568ef6e5c scripts/lib/handlers/write-missing-plans.js
86
86
  8a1bb4df2d1b6e7fd135f757423fb2f2 config/repo-artifacts.json
87
87
  91cec3c50040ebc9050c3d95f037bce2 config/run-modes.json
88
88
  ee4366721c38cbc8cd72ee0f779d22a8 scripts/lib/plan-scope.js
89
+ aab7063cf68a9ecd54f6ef735adf895e scripts/lib/engine-paths.sh
90
+ 1d0346da09c13150e511809034396949 scripts/lib/pipeline-commit.sh
@@ -32,6 +32,7 @@ scripts/lib/doctor/repair.js
32
32
  scripts/lib/doctor/restarter.js
33
33
  scripts/lib/doctor/state.js
34
34
  scripts/lib/ecosystem-registry.js
35
+ scripts/lib/engine-paths.sh
35
36
  scripts/lib/engine-prompt.js
36
37
  scripts/lib/env-file.sh
37
38
  scripts/lib/evidence-windows.js
@@ -73,6 +74,7 @@ scripts/lib/model-ladder.sh
73
74
  scripts/lib/model-ladders.sh
74
75
  scripts/lib/orchestration-resume.sh
75
76
  scripts/lib/phase-exit.sh
77
+ scripts/lib/pipeline-commit.sh
76
78
  scripts/lib/pipeline-tools.js
77
79
  scripts/lib/plan-hash.js
78
80
  scripts/lib/plan-scope.js
@@ -603,25 +603,36 @@ archive_prior_lane_state() {
603
603
  local _lanes="${LOG_DIR:-}/lanes"
604
604
  [ -d "$_lanes" ] || return 0
605
605
  local _run="${EPAM_RESUME_RUN:-${ORCH_RUN_ID:-}}"
606
- [ -n "$_run" ] || return 0
606
+ if [ -z "$_run" ]; then
607
+ warning "[orch] the earlier lane state is not archived: there is no run id (EPAM_RESUME_RUN / ORCH_RUN_ID) — it stays where it is" || true
608
+ return 0
609
+ fi
607
610
  local _marker=""
608
611
  if [ -n "${EPAM_PROJECT_CONFIG_DIR:-}" ]; then
609
612
  _marker="${EPAM_PROJECT_CONFIG_DIR}/runs/${_run}/lane-state-archived"
610
613
  [ -f "$_marker" ] && return 0
611
614
  fi
612
- # The run's start is in its id (YYYYMMDDTHHMMSSZ, UTC).
613
- local _start
615
+ # B80 (RS-7): the run's start is its id when the id is a timestamp (as before); for ANY other id (every Claude workflows id is LIVE-…) it is NOW — the
616
+ # first time this function runs for the run — which is only safe with the marker that stops a resume from archiving the run's own state
617
+ local _start _how=""
614
618
  _start=$(printf '%s' "$_run" | sed -nE 's/^([0-9]{4})([0-9]{2})([0-9]{2})T([0-9]{2})([0-9]{2})([0-9]{2})Z?$/\1-\2-\3 \4:\5:\6 UTC/p')
615
- [ -n "$_start" ] || return 0
619
+ if [ -z "$_start" ]; then
620
+ if [ -z "$_marker" ]; then
621
+ warning "[orch] the earlier lane state is not archived: run id '${_run}' carries no start time and there is no EPAM_PROJECT_CONFIG_DIR to record that it was archived (a resume would archive the run's own state)" || true
622
+ return 0
623
+ fi
624
+ _start=$(date -u '+%Y-%m-%d %H:%M:%S.%N UTC'); _how=" (run '${_run}' carries no start time: it starts now)"
625
+ fi
616
626
  local _ref; _ref=$(mktemp "${TMPDIR:-/tmp}/run-start-XXXXXX") || return 0
617
627
  touch -d "$_start" "$_ref" 2>/dev/null || { rm -f "$_ref"; return 0; }
618
- local _dest="${LOG_DIR}/archive/lanes-before-${_run}" _n=0 _f _rel
628
+ local _dest="${LOG_DIR}/archive/lanes-before-${_run}" _n=0 _failed=0 _f _rel
619
629
  while IFS= read -r -d '' _f; do
620
630
  _rel="${_f#"$_lanes"/}"
621
- mkdir -p "$_dest/$(dirname "$_rel")" 2>/dev/null && mv "$_f" "$_dest/$_rel" 2>/dev/null && _n=$((_n + 1))
631
+ if mkdir -p "$_dest/$(dirname "$_rel")" 2>/dev/null && mv "$_f" "$_dest/$_rel" 2>/dev/null; then _n=$((_n + 1)); else _failed=$((_failed + 1)); fi
622
632
  done < <(find "$_lanes" -type f ! -newer "$_ref" -print0 2>/dev/null)
623
633
  rm -f "$_ref"
624
- [ "$_n" -gt 0 ] && log "[orch] archived ${_n} file(s) of earlier runs' lane state → ${_dest} (this run starts with none of it)"
634
+ [ "$_n" -gt 0 ] && { log "[orch] archived ${_n} file(s) of earlier runs' lane state → ${_dest} (this run starts with none of it)${_how}" || true; }
635
+ [ "$_failed" -gt 0 ] && { warning "[orch] ${_failed} file(s) of earlier runs' lane state could not be archived — they stay in ${_lanes}" || true; }
625
636
  [ -n "$_marker" ] && { mkdir -p "${_marker%/*}" 2>/dev/null; date -u +%Y-%m-%dT%H:%M:%SZ > "$_marker"; }
626
637
  return 0
627
638
  }
@@ -0,0 +1,52 @@
1
+ #!/usr/bin/env bash
2
+ # What the ENGINE owns — the bash half of src/config/enginePaths.ts.
3
+ #
4
+ # These directories hold epam-cli's own state: knowledge base, agent profiles, run logs,
5
+ # code indexes, telemetry, contracts. None of it is client content, none of it is writer
6
+ # output, and none of it may be committed into a client codeline.
7
+ #
8
+ # ONE definition, because this rule has already drifted three ways. lib/git-ops.sh carried
9
+ # the full list, worktree-health-check.sh excluded only orchestrations/logs/* (so
10
+ # orchestrations/agents/KB.md passed straight through), Step 9's auto-commit excluded
11
+ # nothing, and lib/story-outputs.sh had its own separate regex covering only two of the
12
+ # five. Live metrolinx 20260804T225443Z: the engine's KB.md was written into the upexpress
13
+ # client repo and recorded as writer output in that lane's manifest.
14
+ #
15
+ # The real perimeter is the WRITE seam (src/config/enginePaths.ts, enforced in
16
+ # WriteFileTool). Everything here is defence in depth.
17
+
18
+ _ENGINE_OWNED_DIRS=( 'orchestrations' '.epam' '.codegraph' '.deepeval' '.contracts' )
19
+
20
+ # engine_paths_filter — stdin: repo-relative paths. stdout: those that are NOT engine-owned.
21
+ #
22
+ # Matches whole path SEGMENTS, never a substring: `src/orchestrations-ui/App.tsx` is client
23
+ # code and must survive, while `orchestrations/agents/KB.md` and
24
+ # `packages/web/.epam/settings.json` must both be dropped.
25
+ engine_paths_filter() {
26
+ local _line
27
+ local _owned_list
28
+ _owned_list=$(printf '%s\n' "${_ENGINE_OWNED_DIRS[@]}")
29
+ while IFS= read -r _line; do
30
+ [ -n "$_line" ] || continue
31
+ # -x -F: exact, literal, whole-segment. No pattern matching.
32
+ if printf '%s\n' "$_line" | tr '/' '\n' | grep -qxF "$_owned_list" 2>/dev/null; then
33
+ continue
34
+ fi
35
+ printf '%s\n' "$_line"
36
+ done
37
+ }
38
+
39
+ # engine_paths_pathspec — the same owned set, as git pathspec exclusions.
40
+ #
41
+ # engine_paths_filter above filters a LIST of paths. Some callers need the exclusion applied by
42
+ # git itself, because their output is a patch rather than a list and there are no paths to filter.
43
+ # Both read _ENGINE_OWNED_DIRS, so the set is defined once: restating it in pathspec form at a
44
+ # call site is how this rule has already drifted three ways.
45
+ #
46
+ # Usage: mapfile -t _ex < <(engine_paths_pathspec); git diff <ref> -- . "${_ex[@]}"
47
+ engine_paths_pathspec() {
48
+ local _d
49
+ for _d in "${_ENGINE_OWNED_DIRS[@]}"; do
50
+ printf ':!%s/*\n:!*/%s/*\n' "$_d" "$_d"
51
+ done
52
+ }
@@ -370,22 +370,66 @@ git_add_client_outputs() {
370
370
  return $_rc
371
371
  }
372
372
 
373
- # _story_branch_carries_own_work <root> <branch> <story_id> — true when <branch> exists and this run recorded a commit of
374
- # <story_id> (LOG_DIR/story-changes.jsonl) that is in the branch's history; never on a resume sent back to an earlier stage.
373
+ # story_branch_name <story_id> [suffix] — THE ONE RULE for a story's branch name, for every pipeline that works a story branch (B75, RS-10): the project's
374
+ # prefix (EPAM_BRANCH_PREFIX), "AI-<story_id>", and a release line's suffix (the argument, else EPAM_BRANCH_SUFFIX; an explicit empty suffix means none).
375
+ story_branch_name() { printf '%s' "${EPAM_BRANCH_PREFIX:-}AI-${1}${2-${EPAM_BRANCH_SUFFIX:-}}"; }
376
+
377
+ # _story_local_base_ref <root> <baseline> — the baseline as the checkout already has it, WITHOUT the network: the tag, then origin's branch, then the local
378
+ # branch. Empty when none resolves.
379
+ _story_local_base_ref() {
380
+ local _root="$1" _b="${2#refs/tags/}" _ref
381
+ _b="${_b#refs/heads/}"
382
+ [ -n "$_b" ] || return 0
383
+ for _ref in "refs/tags/${_b}" "refs/remotes/origin/${_b}" "refs/heads/${_b}"; do
384
+ git -C "$_root" rev-parse -q --verify "${_ref}^{commit}" >/dev/null 2>&1 && { printf '%s' "$_ref"; return 0; }
385
+ done
386
+ return 0
387
+ }
388
+
389
+ # _story_branch_carries_own_work <root> <branch> <story_id> [baseline] — true when <branch> exists and carries the story's own work. B75 (RS-1): the evidence
390
+ # is GIT — a commit of the story ("<story_id>: …", the convention every commit of the story follows) between the baseline and the branch — so it holds for
391
+ # any pipeline, wherever it keeps its records; a client's own history using the same ticket prefix does not count (only commits since the baseline). The bash
392
+ # pipeline's own record (LOG_DIR/story-changes.jsonl) is evidence too, as before. Never on a resume sent back to an earlier stage.
375
393
  _story_branch_carries_own_work() {
376
- local _root="$1" _br="$2" _id="$3" _f="${LOG_DIR:-}/story-changes.jsonl" _sha
377
- [ -n "$_id" ] && [ -f "$_f" ] || return 1
394
+ local _root="$1" _br="$2" _id="$3" _base_name="${4:-}" _f="${LOG_DIR:-}/story-changes.jsonl" _sha _base _re
395
+ [ -n "$_id" ] || return 1
378
396
  git -C "$_root" rev-parse -q --verify "refs/heads/${_br}" >/dev/null 2>&1 || return 1
379
397
  if [ -n "${EPAM_RESUME_FROM_STAGE:-}" ] && declare -F _stage_rank >/dev/null 2>&1 \
380
398
  && [ "$(_stage_rank "$EPAM_RESUME_FROM_STAGE")" -lt "$(_stage_rank pre-writer)" ]; then
381
399
  return 1
382
400
  fi
401
+ _base=$(_story_local_base_ref "$_root" "$_base_name")
402
+ if [ -n "$_base" ]; then
403
+ _re=$(printf '%s' "$_id" | sed 's/[][\.*^$/]/\\&/g')
404
+ [ -n "$(git -C "$_root" log -1 --format=%h --grep="^${_re}:" "${_base}..refs/heads/${_br}" 2>/dev/null)" ] && return 0
405
+ fi
406
+ [ -f "$_f" ] || return 1
383
407
  while IFS= read -r _sha; do
384
408
  [ -n "$_sha" ] && git -C "$_root" merge-base --is-ancestor "$_sha" "refs/heads/${_br}" 2>/dev/null && return 0
385
409
  done < <(jq -r --arg id "$_id" 'select(.storyId == $id) | .sha // empty' "$_f" 2>/dev/null)
386
410
  return 1
387
411
  }
388
412
 
413
+ # _preserve_uncommitted <root> <story_id> — B75 (RS-3): before ANY reset, whatever the working tree holds that no commit holds (tracked edits and untracked
414
+ # files, the pipeline's own .epam/ aside) is committed to a rescue ref, epam-rescue/<story>-wip-<sha>, without touching the branch, the index or the tree.
415
+ # Nothing to preserve: nothing is done. Fails (non-zero) when it cannot — the caller then refuses to reset, as it does when a commit cannot be pinned.
416
+ _preserve_uncommitted() {
417
+ local _root="$1" _id="$2" _idx _tree _head _commit _ref
418
+ [ -n "$(git -C "$_root" status --porcelain --untracked-files=all -- . ':(exclude).epam' 2>/dev/null)" ] || return 0
419
+ _head=$(git -C "$_root" rev-parse -q --verify HEAD 2>/dev/null) || _head=""
420
+ _idx=$(mktemp -u "${TMPDIR:-/tmp}/epam-wip-index-XXXXXX") || return 1
421
+ GIT_INDEX_FILE="$_idx" git -C "$_root" add -A -- . ':(exclude).epam' >/dev/null 2>&1 || { rm -f "$_idx"; return 1; }
422
+ _tree=$(GIT_INDEX_FILE="$_idx" git -C "$_root" write-tree 2>/dev/null) || { rm -f "$_idx"; return 1; }
423
+ rm -f "$_idx"
424
+ _commit=$(git -C "$_root" -c user.name="${GIT_COMMITTER_NAME:-epam-rescue}" -c user.email="${GIT_COMMITTER_EMAIL:-epam-rescue@local}" \
425
+ commit-tree "$_tree" ${_head:+-p "$_head"} -m "epam-rescue: uncommitted work of ${_id}" 2>/dev/null) || return 1
426
+ _ref="epam-rescue/${_id}-wip-$(git -C "$_root" rev-parse --short "$_commit" 2>/dev/null || echo unknown)"
427
+ git -C "$_root" branch -f "$_ref" "$_commit" --quiet 2>/dev/null || return 1
428
+ warning " [story-branch] ${_id}: uncommitted work preserved on branch '${_ref}'"
429
+ warning " [story-branch] recover with: git -C '${_root}' checkout ${_ref} -- <path>"
430
+ return 0 # the work IS preserved; a logger that cannot write (it returns its own status) must not turn that into a failure
431
+ }
432
+
389
433
  ensure_story_branch() {
390
434
  local codeline_root="$1"
391
435
  local story_id="$2"
@@ -405,15 +449,16 @@ ensure_story_branch() {
405
449
  # constraint, so the default preserves today's plain "AI-<story_id>" shape.
406
450
  # ONE STORY BRANCH PER RELEASE LINE (lib/release-lines.sh): a line other than the codeline's own carries a
407
451
  # suffix, so the codeline's own story branch is never reset by another line's work.
408
- local _branch="${EPAM_BRANCH_PREFIX:-}AI-${story_id}${EPAM_BRANCH_SUFFIX:-}"
452
+ local _branch; _branch=$(story_branch_name "$story_id")
409
453
 
410
454
  # A STORY THAT ALREADY HAS ITS OWN WORK ON ITS BRANCH CONTINUES FROM IT (AMSD-2974 run 9, 2026-10-14). A self-heal retry
411
455
  # sets the story pending again and comes back here; the hard reset below moved the 7.x line's branch to v7.11.0 twice
412
456
  # in one run, discarding the approved full port (c25e819), and the writer re-wrote it from nothing — paid twice, weaker.
413
457
  # This run's own record of the story's commits (story-changes.jsonl) is what says the work is its own: a fresh run has
414
458
  # none, and a resume sent back to an earlier stage (EPAM_RESUME_FROM_STAGE) starts over as asked.
415
- if _story_branch_carries_own_work "$codeline_root" "$_branch" "$story_id"; then
459
+ if _story_branch_carries_own_work "$codeline_root" "$_branch" "$story_id" "$baseline_branch"; then
416
460
  if git -C "$codeline_root" checkout -q "$_branch" 2>/dev/null; then
461
+ _preserve_uncommitted "$codeline_root" "$story_id" || { error " [story-branch] $story_id: uncommitted work could not be preserved — refusing to reset it away"; return 1; }
417
462
  git -C "$codeline_root" reset --hard -q "$_branch" 2>/dev/null || true
418
463
  git -C "$codeline_root" clean -fd -e .epam -q 2>/dev/null || true
419
464
  _provision_epam_plugin_config "$codeline_root"
@@ -509,6 +554,8 @@ ensure_story_branch() {
509
554
  fi
510
555
  done
511
556
 
557
+ _preserve_uncommitted "$codeline_root" "$story_id" || { error " [story-branch] $story_id: uncommitted work could not be preserved — refusing to reset it away"; return 1; }
558
+
512
559
  if git -C "$codeline_root" checkout -B "$_branch" "$_base_ref" --quiet 2>/dev/null; then
513
560
  # checkout -B only forces tracked files that DIFFER between the old
514
561
  # branch tip and the new start-point — a tracked file whose content
@@ -0,0 +1,19 @@
1
+ #!/usr/bin/env bash
2
+ # pipeline_commit_flags — the flags every commit the PIPELINE makes passes to `git commit`, one per line.
3
+ #
4
+ # A codeline's hooks are written for its team's own checkout and machine. Operator decisions,
5
+ # 2026-10-05 (AMSD-2974): the pipeline's commits skip them — first inside worktrees, then in the
6
+ # checkout too. Proven on a copy of next.gotransit.com: its pre-commit hook (husky + lint-staged)
7
+ # type-checks the WHOLE project, which fails on the untouched checkout with 6 errors because the
8
+ # team's node_modules is older than its own develop branch expects (it needs an `npm install`, which
9
+ # the pipeline may not run) — so the hook refused every pipeline commit there, whatever it held, and
10
+ # the reproducing test of a correct fix was never committed. In worktrees the same hook also refused
11
+ # the branch name and sourced a helper git does not track.
12
+ #
13
+ # What the hook checks is still checked by the pipeline: the story's own verification, Step 19/20's
14
+ # type check judged against the phase baseline, the lint gate, review and the QA gates. The Step 17
15
+ # merge onto the story branch still runs the codeline's commit-msg hook (git merge has no --no-verify
16
+ # here), and its message is written to pass it.
17
+ pipeline_commit_flags() {
18
+ echo "--no-verify"
19
+ }
@@ -223,13 +223,32 @@ qa_phase_baseline_sha() {
223
223
  # phase-baseline-sha.txt: every pre-existing failure was charged to its writer, eight attempts in a
224
224
  # row (£0 multi-lane run, 2026-10-04). Nothing resolvable is said out loud, never written as a guess.
225
225
  qa_record_phase_baseline() {
226
- local _root="${1:-}" _branch="${2:-}" _log_dir="${3:-${LOG_DIR:-}}" _sha
226
+ local _root="${1:-}" _branch="${2:-}" _log_dir="${3:-${LOG_DIR:-}}" _sha _have _key
227
227
  [ -n "$_root" ] && [ -n "$_log_dir" ] || return 1
228
+ local _bf="$_log_dir/phase-baseline-sha.txt" _kf="$_log_dir/phase-baseline-key.txt"
229
+ # B77 (RS-4, = B73): THE BASELINE IS RECORDED ONCE for a run and phase. A resume starts the phase again; the baseline it had must not move to wherever HEAD
230
+ # is by then (that made "committed since the baseline" empty). A new phase, or a new run in the same log folder, has a different key and records its own.
231
+ _key="${PHASE:-main}|${ORCH_RUN_ID:-}"
232
+ if [ -f "$_bf" ] && [ -f "$_kf" ] && [ "$(cat "$_kf" 2>/dev/null)" = "$_key" ]; then
233
+ _have=$(tr -d '[:space:]' < "$_bf" 2>/dev/null)
234
+ if [ -n "$_have" ] && git -C "$_root" cat-file -e "${_have}^{commit}" 2>/dev/null; then
235
+ printf '%s' "$_have"
236
+ return 0
237
+ fi
238
+ warning "[orch] the recorded phase baseline (${_have:-empty}) is not a commit of $_root — recording it again" || true
239
+ fi
228
240
  _sha="$(qa_phase_baseline_sha "$_root" "$_branch" 2>/dev/null || echo "")"
229
241
  if [ -z "$_sha" ]; then
230
- warning "[orch] could not resolve a baseline commit in $_root — every diff-based gate will compare against nothing"
242
+ warning "[orch] could not resolve a baseline commit in $_root — every diff-based gate will compare against nothing" || true
231
243
  return 1
232
244
  fi
233
- printf '%s\n' "$_sha" > "$_log_dir/phase-baseline-sha.txt"
245
+ # the fallback is HEAD (the codeline has no such baseline branch — cx-shared has no develop): fine at the FIRST call, and said, because it is not a fork point
246
+ if [ -n "$_branch" ] && ! git -C "$_root" rev-parse --verify --quiet "origin/${_branch}^{commit}" >/dev/null 2>&1 \
247
+ && ! git -C "$_root" rev-parse --verify --quiet "${_branch}^{commit}" >/dev/null 2>&1; then
248
+ warning "[orch] baseline '${_branch}' did not resolve in $_root — the phase baseline is HEAD (${_sha:0:9}) as of now; it is recorded once and not moved" || true
249
+ fi
250
+ printf '%s\n' "$_sha" > "$_bf"
251
+ printf '%s\n' "$_key" > "$_kf"
234
252
  printf '%s' "$_sha"
253
+ return 0
235
254
  }
@@ -32,19 +32,35 @@ _run_release_lines() {
32
32
 
33
33
  # _return_from_release_lines <codeline-root> [name] — back on the branch the record names; the record is removed.
34
34
  _return_from_release_lines() {
35
- local _gd _home
35
+ local _gd _home _cur
36
36
  _gd=$(git -C "$1" rev-parse --absolute-git-dir 2>/dev/null) || return 0
37
37
  [ -f "$_gd/epam-release-home" ] || return 0
38
38
  _home=$(cat "$_gd/epam-release-home")
39
- if [ -n "$_home" ] && [ "$(git -C "$1" symbolic-ref -q --short HEAD 2>/dev/null)" != "$_home" ]; then
39
+ _cur=$(git -C "$1" symbolic-ref -q --short HEAD 2>/dev/null)
40
+ if [ -n "$_home" ] && [ "$_cur" != "$_home" ]; then
40
41
  if git -C "$1" checkout -q "$_home" 2>/dev/null; then
41
- log "[orch] Codeline '${2:-$1}': release lines done — back on ${_home}"
42
+ log "[orch] Codeline '${2:-$1}': release lines done — back on ${_home}" || true
42
43
  else
43
- warning "[orch] Codeline '${2:-$1}': could not return to ${_home} after its release lines"
44
- return 0
44
+ # B81 (RS-8): the usual reason is a dirty tree (a killed session). Whatever it holds is PRESERVED first (a rescue ref, git-ops.sh), then the copy is
45
+ # forced home; if it still cannot go home, that is an error (rc 1, the marker kept) — never a warning that leaves the copy on the line branch.
46
+ if [ -n "$(git -C "$1" status --porcelain --untracked-files=all 2>/dev/null)" ]; then
47
+ if declare -F _preserve_uncommitted >/dev/null 2>&1; then
48
+ _preserve_uncommitted "$1" "${_cur//\//-}" || { error "[orch] Codeline '${2:-$1}': could not return to ${_home} after its release lines — its uncommitted work could not be preserved, so nothing was forced"; return 1; }
49
+ else
50
+ error "[orch] Codeline '${2:-$1}': could not return to ${_home} after its release lines — the tree is dirty and this install cannot preserve it (needs amsd-pipeline-shared >= 0.5.2)"
51
+ return 1
52
+ fi
53
+ fi
54
+ if git -C "$1" checkout -q -f "$_home" 2>/dev/null; then
55
+ warning "[orch] Codeline '${2:-$1}': release lines done — back on ${_home} (its tree was not clean; the work it held is preserved)" || true
56
+ else
57
+ error "[orch] Codeline '${2:-$1}': could not return to ${_home} after its release lines (it is still on ${_cur:-a detached HEAD}; any uncommitted work is preserved on a rescue ref)"
58
+ return 1
59
+ fi
45
60
  fi
46
61
  fi
47
62
  rm -f "$_gd/epam-release-home"
63
+ return 0
48
64
  }
49
65
 
50
66
  # _return_codelines_from_release_lines <prd> — every codeline of the PRD a line pass left is put back before the
@@ -220,12 +220,16 @@ story_outputs_tests_for() {
220
220
  # the writer skips it and the review judges the committed change. A newer rejection leaves it for the writer.
221
221
  story_resume_completed_by_commit() {
222
222
  local project_root="$1" log_dir="$2" prd="$3"
223
- [ -n "$project_root" ] && [ -e "$project_root/.git" ] && [ -f "$prd" ] || return 0
224
- [ -f "$log_dir/phase-baseline-sha.txt" ] || return 0
223
+ [ -n "$project_root" ] && [ -e "$project_root/.git" ] && [ -f "$prd" ] || { echo "[resume] no story is completed by its commit: ${project_root:-no project root} is not a repository, or there is no PRD at ${prd:-none}" >&2; return 0; }
224
+ # B78 (RS-5): a precondition it cannot meet is SAID — the story is then left exactly as it was (the writer is invoked), and the operator can see why
225
+ [ -f "$log_dir/phase-baseline-sha.txt" ] || { echo "[resume] no phase baseline is recorded in ${log_dir} — cannot tell which commits are this run's; no story is completed by its commit" >&2; return 0; }
225
226
  local _base; _base=$(tr -d '[:space:]' < "$log_dir/phase-baseline-sha.txt" 2>/dev/null)
226
- [ -n "$_base" ] && git -C "$project_root" rev-parse --verify --quiet "$_base" >/dev/null 2>&1 || return 0
227
+ if [ -z "$_base" ] || ! git -C "$project_root" rev-parse --verify --quiet "${_base}^{commit}" >/dev/null 2>&1; then
228
+ echo "[resume] the recorded phase baseline (${_base:-empty}) is not a commit of ${project_root} — no story is completed by its commit" >&2
229
+ return 0
230
+ fi
227
231
  local _id _ct _fb _fbt
228
- for _id in $(jq -r '.stories[]? | select(.completed != true) | .id' "$prd" 2>/dev/null); do
232
+ for _id in $(jq -r '.stories[]? | select(.completed != true and .status != "deprecated") | .id' "$prd" 2>/dev/null); do
229
233
  _ct=$(git -C "$project_root" log "${_base}..HEAD" --grep="^${_id}:" -1 --format=%ct 2>/dev/null)
230
234
  [ -n "$_ct" ] || continue
231
235
  _fb="$log_dir/review-feedback-${_id}.json"
@@ -234,9 +238,9 @@ story_resume_completed_by_commit() {
234
238
  [ "${_fbt:-0}" -gt "$_ct" ] && continue
235
239
  fi
236
240
  local _tmp; _tmp=$(mktemp)
237
- jq --arg id "$_id" '(.stories[] | select(.id == $id)) |= (. + {completed: true})' "$prd" > "$_tmp" \
241
+ jq --arg id "$_id" '(.stories[] | select(.id == $id)) |= (. + {completed: true, status: "completed"})' "$prd" > "$_tmp" \
238
242
  && mv "$_tmp" "$prd" || rm -f "$_tmp"
239
- echo "[resume] ${_id}: its work is committed in this run ($(git -C "$project_root" log "${_base}..HEAD" --grep="^${_id}:" -1 --format=%h 2>/dev/null)) and no review rejection is newer — the writer is not invoked again; the review judges the committed change"
243
+ echo "[resume] ${_id}: its work is committed in this run ($(git -C "$project_root" log "${_base}..HEAD" --grep="^${_id}:" -1 --format=%h 2>/dev/null)) and no review rejection is newer — the writer is not invoked again; the review judges the committed change" >&2
240
244
  done
241
245
  return 0
242
246
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "amsd-pipeline-shared",
3
- "version": "0.5.1",
3
+ "version": "0.5.3",
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": {