amsd-pipeline-shared 0.5.1 → 0.5.2

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
@@ -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
+ }
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.2",
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": {