amsd-pipeline-shared 0.5.2 → 0.5.4
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 +4 -0
- package/docs/gateway-files.txt +4 -0
- package/orchestrations/prompts/templates/criterion-origin.json +15 -0
- package/orchestrations/scripts/lib/codeline-lanes.sh +24 -7
- package/orchestrations/scripts/lib/plan-of-record.sh +43 -0
- package/orchestrations/scripts/lib/qa-gate-evidence.sh +22 -3
- package/orchestrations/scripts/lib/release-lines.sh +21 -5
- package/orchestrations/scripts/lib/render-engine-prompt.sh +89 -0
- package/orchestrations/scripts/lib/story-acs-block.sh +72 -0
- package/orchestrations/scripts/lib/story-outputs.sh +10 -6
- package/package.json +1 -1
|
@@ -88,3 +88,7 @@ ac166207d8a1d93a4c50a88568ef6e5c scripts/lib/handlers/write-missing-plans.js
|
|
|
88
88
|
ee4366721c38cbc8cd72ee0f779d22a8 scripts/lib/plan-scope.js
|
|
89
89
|
aab7063cf68a9ecd54f6ef735adf895e scripts/lib/engine-paths.sh
|
|
90
90
|
1d0346da09c13150e511809034396949 scripts/lib/pipeline-commit.sh
|
|
91
|
+
a01137cb36b65a3b80a8327404f4ed7a scripts/lib/story-acs-block.sh
|
|
92
|
+
043b6c85d1505ebc5da6c6b158000330 scripts/lib/plan-of-record.sh
|
|
93
|
+
1cc1978d614fca875ad1117e9ac42f93 scripts/lib/render-engine-prompt.sh
|
|
94
|
+
bad91b18bf546145a58423c770ba1a3a prompts/templates/criterion-origin.json
|
package/docs/gateway-files.txt
CHANGED
|
@@ -6,6 +6,7 @@ config/resume-preserves.json
|
|
|
6
6
|
config/run-modes.json
|
|
7
7
|
prompts/doctor-repair.md
|
|
8
8
|
prompts/templates/codeline-discovery.json
|
|
9
|
+
prompts/templates/criterion-origin.json
|
|
9
10
|
scripts/agent-attempt-analyst.sh
|
|
10
11
|
scripts/lib/agent-contract.js
|
|
11
12
|
scripts/lib/agent-ladder.sh
|
|
@@ -77,12 +78,14 @@ scripts/lib/phase-exit.sh
|
|
|
77
78
|
scripts/lib/pipeline-commit.sh
|
|
78
79
|
scripts/lib/pipeline-tools.js
|
|
79
80
|
scripts/lib/plan-hash.js
|
|
81
|
+
scripts/lib/plan-of-record.sh
|
|
80
82
|
scripts/lib/plan-scope.js
|
|
81
83
|
scripts/lib/preflight.sh
|
|
82
84
|
scripts/lib/prelaunch.js
|
|
83
85
|
scripts/lib/qa-gate-evidence.sh
|
|
84
86
|
scripts/lib/relative-paths.js
|
|
85
87
|
scripts/lib/release-lines.sh
|
|
88
|
+
scripts/lib/render-engine-prompt.sh
|
|
86
89
|
scripts/lib/render-prompt-section.js
|
|
87
90
|
scripts/lib/resume-semantics.sh
|
|
88
91
|
scripts/lib/run-checkpoint.sh
|
|
@@ -91,6 +94,7 @@ scripts/lib/seam-invocation.js
|
|
|
91
94
|
scripts/lib/seam-ladder.sh
|
|
92
95
|
scripts/lib/self-heal.js
|
|
93
96
|
scripts/lib/stage-coverage-gate.sh
|
|
97
|
+
scripts/lib/story-acs-block.sh
|
|
94
98
|
scripts/lib/story-outputs.sh
|
|
95
99
|
scripts/lib/story-retry-state.sh
|
|
96
100
|
scripts/lib/tool-timeouts.js
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$why": [
|
|
3
|
+
"2026-10-14 (operator: \"You are not permitted to hard code engine prose\"). The words that mark a criterion the",
|
|
4
|
+
"pipeline's remediator added — so the reviewer may retire it when it cannot be met, and never one of the plan's —",
|
|
5
|
+
"were a string in lib/story-acs-block.sh. One definition: the criteria block renders it beside such a criterion,",
|
|
6
|
+
"the reviewer's contract (review-finding-routing) names it, and retire-criteria.py accepts a name copied with it."
|
|
7
|
+
],
|
|
8
|
+
"description": "How a criterion the remediator added is marked wherever criteria are shown to an agent.",
|
|
9
|
+
"id": "criterion-origin",
|
|
10
|
+
"version": 1,
|
|
11
|
+
"placeholders": [],
|
|
12
|
+
"bodies": {
|
|
13
|
+
"remediation": "[added by remediation — not the plan]"
|
|
14
|
+
}
|
|
15
|
+
}
|
|
@@ -603,25 +603,42 @@ 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
|
-
[ -
|
|
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
|
-
#
|
|
613
|
-
|
|
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
|
+
# B80 fix (run 11 leg 3): a RESUME's lane state is its own. Without a timestamp id there is no way to tell the run's earlier legs' files from an earlier run's, and
|
|
618
|
+
# the run's first legs may predate the marker — so a resume never archives (the released 0.5.3 moved 113 files of the run's own state and the Writer re-ran).
|
|
619
|
+
local _start _how=""
|
|
614
620
|
_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
|
-
[ -
|
|
621
|
+
if [ -z "$_start" ]; then
|
|
622
|
+
if [ -n "${EPAM_RESUME_RUN:-}" ]; then
|
|
623
|
+
log "[orch] the lane state of '${_run}' is kept: this is a resume, and its earlier legs' files are its own (the run id carries no start time to tell them from an earlier run's)" || true
|
|
624
|
+
return 0
|
|
625
|
+
fi
|
|
626
|
+
if [ -z "$_marker" ]; then
|
|
627
|
+
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
|
|
628
|
+
return 0
|
|
629
|
+
fi
|
|
630
|
+
_start=$(date -u '+%Y-%m-%d %H:%M:%S.%N UTC'); _how=" (run '${_run}' carries no start time: it starts now)"
|
|
631
|
+
fi
|
|
616
632
|
local _ref; _ref=$(mktemp "${TMPDIR:-/tmp}/run-start-XXXXXX") || return 0
|
|
617
633
|
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
|
|
634
|
+
local _dest="${LOG_DIR}/archive/lanes-before-${_run}" _n=0 _failed=0 _f _rel
|
|
619
635
|
while IFS= read -r -d '' _f; do
|
|
620
636
|
_rel="${_f#"$_lanes"/}"
|
|
621
|
-
mkdir -p "$_dest/$(dirname "$_rel")" 2>/dev/null && mv "$_f" "$_dest/$_rel" 2>/dev/null
|
|
637
|
+
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
638
|
done < <(find "$_lanes" -type f ! -newer "$_ref" -print0 2>/dev/null)
|
|
623
639
|
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)"
|
|
640
|
+
[ "$_n" -gt 0 ] && { log "[orch] archived ${_n} file(s) of earlier runs' lane state → ${_dest} (this run starts with none of it)${_how}" || true; }
|
|
641
|
+
[ "$_failed" -gt 0 ] && { warning "[orch] ${_failed} file(s) of earlier runs' lane state could not be archived — they stay in ${_lanes}" || true; }
|
|
625
642
|
[ -n "$_marker" ] && { mkdir -p "${_marker%/*}" 2>/dev/null; date -u +%Y-%m-%dT%H:%M:%SZ > "$_marker"; }
|
|
626
643
|
return 0
|
|
627
644
|
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# plan-of-record.sh — THE STORY'S PLAN AS APPROVED AT PAUSE 2, WRITTEN ONCE, NEVER OVERWRITTEN.
|
|
3
|
+
#
|
|
4
|
+
# Operator, 2026-10-14 (option 3): which criteria are the plan's is decided by the plan approved at pause 2 — not by a
|
|
5
|
+
# criterion's shape. The old rule ("exactly {text, status} means a remediator added it") was hard coding. The plan of
|
|
6
|
+
# record lives beside the lane's pre-writer checkpoint; a release line reads its lane's (the line's plan is the lane's).
|
|
7
|
+
# A run paused before this existed derives it once from that checkpoint — never from an earlier stage, which is not
|
|
8
|
+
# the approved plan.
|
|
9
|
+
|
|
10
|
+
# shellcheck source=run-checkpoint.sh
|
|
11
|
+
declare -F checkpoint_dir >/dev/null 2>&1 || . "$(dirname "${BASH_SOURCE[0]}")/run-checkpoint.sh"
|
|
12
|
+
|
|
13
|
+
# plan_of_record_file [run-id] — the path (it may not exist yet).
|
|
14
|
+
plan_of_record_file() {
|
|
15
|
+
local _d; _d=$(EPAM_RELEASE_LINE='' checkpoint_dir "${1:-}" 2>/dev/null) || return 1
|
|
16
|
+
[ -n "$_d" ] || return 1
|
|
17
|
+
printf '%s/plan-of-record.json' "$_d"
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
# record_plan_of_record <prd> — write the plan of record from <prd>, ONCE: an existing one is never replaced.
|
|
21
|
+
record_plan_of_record() {
|
|
22
|
+
local _src="${1:-}" _f
|
|
23
|
+
_f=$(plan_of_record_file) || return 0
|
|
24
|
+
[ -s "$_f" ] && return 0
|
|
25
|
+
[ -n "$_src" ] && [ -f "$_src" ] || return 0
|
|
26
|
+
mkdir -p "$(dirname "$_f")" 2>/dev/null || true
|
|
27
|
+
jq '{stories: [(.stories // [])[] | {id, acceptanceCriteria: (.acceptanceCriteria // [])}]}' "$_src" > "$_f.tmp" 2>/dev/null \
|
|
28
|
+
&& mv "$_f.tmp" "$_f" || rm -f "$_f.tmp"
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
# plan_of_record_ensured — the plan of record's path when there is one: written now from the lane's pre-writer checkpoint
|
|
32
|
+
# if this run paused before plans of record were kept; nothing when no pause-2 checkpoint exists.
|
|
33
|
+
plan_of_record_ensured() {
|
|
34
|
+
local _f _d
|
|
35
|
+
_f=$(plan_of_record_file) || return 0
|
|
36
|
+
if [ ! -s "$_f" ]; then
|
|
37
|
+
_d=$(dirname "$_f")
|
|
38
|
+
[ -f "$_d/checkpoint.json" ] && [ -f "$_d/prd.json" ] || return 0
|
|
39
|
+
[ "$(_stage_rank "$(jq -r '.stage // empty' "$_d/checkpoint.json" 2>/dev/null)")" -ge "$(_stage_rank pre-writer)" ] || return 0
|
|
40
|
+
record_plan_of_record "$_d/prd.json"
|
|
41
|
+
fi
|
|
42
|
+
[ -s "$_f" ] && printf '%s' "$_f"
|
|
43
|
+
}
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
44
|
-
|
|
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
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
3
|
+
# render-engine-prompt.sh — render a template-layer prompt from shell.
|
|
4
|
+
#
|
|
5
|
+
# Several seams that still carry embedded prompts are shell, not JavaScript, so
|
|
6
|
+
# they need the same renderer the JS side uses. This is a thin wrapper over
|
|
7
|
+
# lib/engine-prompt.js: one renderer, one strictness, one set of rules.
|
|
8
|
+
#
|
|
9
|
+
# render_engine_prompt <template-id> <values-json-file>
|
|
10
|
+
#
|
|
11
|
+
# VALUES ARRIVE VIA A FILE, NEVER ARGV.
|
|
12
|
+
# Prompt values routinely carry a whole diff, a suite dump or a PRD fragment.
|
|
13
|
+
# ARG_MAX is 2 MiB, and passing a value that exceeds it makes the command exit
|
|
14
|
+
# 126 with an empty result — which is exactly how the FailureAnalyst died on
|
|
15
|
+
# 2026-08-15, silently, because the failure looked like a parse error three
|
|
16
|
+
# steps later. A file has no such limit.
|
|
17
|
+
#
|
|
18
|
+
# Exit status is the contract: non-zero means nothing was rendered, and the
|
|
19
|
+
# caller must refuse to invoke an agent rather than send it an empty prompt.
|
|
20
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
21
|
+
|
|
22
|
+
# shellcheck disable=SC2148
|
|
23
|
+
_REP_LIB_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
24
|
+
|
|
25
|
+
render_engine_prompt() {
|
|
26
|
+
local _id="$1" _values_file="$2" _body_key="${3:-}"
|
|
27
|
+
|
|
28
|
+
if [ -z "$_id" ] || [ -z "$_values_file" ]; then
|
|
29
|
+
echo "[render-engine-prompt] usage: render_engine_prompt <template-id> <values-json-file>" >&2
|
|
30
|
+
return 2
|
|
31
|
+
fi
|
|
32
|
+
if [ ! -f "$_values_file" ]; then
|
|
33
|
+
echo "[render-engine-prompt] values file not found: $_values_file" >&2
|
|
34
|
+
return 2
|
|
35
|
+
fi
|
|
36
|
+
|
|
37
|
+
local _out _err
|
|
38
|
+
_err=$(mktemp "${TMPDIR:-/tmp}/render-prompt-err-XXXXXX")
|
|
39
|
+
# The values file path is the only thing on argv; the values themselves are read
|
|
40
|
+
# from disk by node.
|
|
41
|
+
_out=$("${NODE_BIN:-node}" -e '
|
|
42
|
+
const fs = require("fs");
|
|
43
|
+
const { renderEngineTemplate } = require(process.argv[1]);
|
|
44
|
+
const values = JSON.parse(fs.readFileSync(process.argv[3], "utf8"));
|
|
45
|
+
process.stdout.write(renderEngineTemplate(process.argv[2], values, process.argv[4] || undefined));
|
|
46
|
+
' "$_REP_LIB_DIR/engine-prompt.js" "$_id" "$_values_file" "$_body_key" 2>"$_err")
|
|
47
|
+
local _rc=$?
|
|
48
|
+
|
|
49
|
+
if [ "$_rc" -ne 0 ] || [ -z "$_out" ]; then
|
|
50
|
+
# Loud, and naming the template. An agent invoked with an empty prompt answers
|
|
51
|
+
# from nothing and the answer looks like every other answer.
|
|
52
|
+
echo "[render-engine-prompt] FAILED to render '$_id': $(cat "$_err" 2>/dev/null)" >&2
|
|
53
|
+
rm -f "$_err"
|
|
54
|
+
return 1
|
|
55
|
+
fi
|
|
56
|
+
rm -f "$_err"
|
|
57
|
+
printf '%s' "$_out"
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
# render_or_keep <template-id> <values-json-file> [<body-key>]
|
|
61
|
+
#
|
|
62
|
+
# Renders, or prints NOTHING and returns non-zero — so the caller's variable keeps whatever it
|
|
63
|
+
# already held instead of being blanked:
|
|
64
|
+
#
|
|
65
|
+
# _r="$(render_or_keep writer-plan-section "$_vals" execution_plan)" && prompt="$_r"
|
|
66
|
+
#
|
|
67
|
+
# WHY. `prompt="$(render_engine_prompt ...)"` discards the exit status, so a dead render assigns
|
|
68
|
+
# the empty string. Live metrolinx AMSD-2041, 2026-08-19: jq hit the 128KB argv cap, both the
|
|
69
|
+
# writer prompt and the retry amendment silently became empty, and the run climbed the entire model
|
|
70
|
+
# ladder invoking agents with nothing in hand — visible as attempts returning in 0.01 min having
|
|
71
|
+
# consumed zero tokens. The header of this file already stated the rule the callers broke: exit
|
|
72
|
+
# status is the contract, and a caller must refuse rather than send an empty prompt.
|
|
73
|
+
#
|
|
74
|
+
# AN EMPTY SUCCESSFUL RENDER IS ALSO A FAILURE here. At the call site the two are the same event —
|
|
75
|
+
# an agent handed nothing — and only one of them was ever going to be noticed.
|
|
76
|
+
render_or_keep() {
|
|
77
|
+
local _out _rc
|
|
78
|
+
_out=$(render_engine_prompt "$@")
|
|
79
|
+
_rc=$?
|
|
80
|
+
if [ "$_rc" -ne 0 ] || [ -z "$_out" ]; then
|
|
81
|
+
if command -v warning >/dev/null 2>&1; then
|
|
82
|
+
warning " [render] '${1:-?}' produced nothing (exit ${_rc}) — keeping the previous value rather than blanking it"
|
|
83
|
+
else
|
|
84
|
+
echo "[render-engine-prompt] '${1:-?}' produced nothing (exit ${_rc}) — caller keeps its previous value" >&2
|
|
85
|
+
fi
|
|
86
|
+
return 1
|
|
87
|
+
fi
|
|
88
|
+
printf '%s' "$_out"
|
|
89
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# story-acs-block.sh — the acceptance-criteria BLOCK a seam prompt receives, or nothing.
|
|
3
|
+
#
|
|
4
|
+
# Brownfield judges on verification criteria: a brownfield story has no acceptance criteria, and an
|
|
5
|
+
# empty "ACCEPTANCE CRITERIA:" heading over nothing is worse than no heading (operator, 2026-09-01,
|
|
6
|
+
# ea920ee7). Greenfield is authored WITH acceptance criteria and they are in scope (operator,
|
|
7
|
+
# 2026-09-11). Both hold when the heading travels with the criteria: present, the block is the
|
|
8
|
+
# heading and the numbered list; absent, the block is empty and the template's __STORY_ACS__
|
|
9
|
+
# (declared mayBeEmpty) renders as nothing. Four seams read it — failure-analyst, team-lead-review,
|
|
10
|
+
# code-review-cycle, code-graph-detective (spec-mode-runner.js builds the same shape in JS) — so
|
|
11
|
+
# the shape is defined here once.
|
|
12
|
+
#
|
|
13
|
+
# story_acs_block <prd_file> <story_id> [heading]
|
|
14
|
+
# stdout: "<heading>\nAC1: ...\nAC2: ...\n\n" when the story declares acceptance criteria,
|
|
15
|
+
# "" otherwise. Default heading "ACCEPTANCE CRITERIA:".
|
|
16
|
+
# A CRITERION THE REMEDIATOR ADDED SAYS SO (operator, 2026-10-14: a reviewer may retire one that cannot be met — never
|
|
17
|
+
# one of the plan's). What the plan is comes from the plan of record (lib/plan-of-record.sh); the remediators also stamp
|
|
18
|
+
# what they add {origin: "remediation"}.
|
|
19
|
+
# THE MARKER IS TEMPLATE PROSE (prompts/templates/criterion-origin.json) — never a literal here. The criteria block renders
|
|
20
|
+
# it, the reviewer's contract names it (story_criterion_mark_values), and retire-criteria.py accepts a name copied with it.
|
|
21
|
+
# shellcheck source=plan-of-record.sh
|
|
22
|
+
declare -F plan_of_record_ensured >/dev/null 2>&1 || . "$(dirname "${BASH_SOURCE[0]}")/plan-of-record.sh"
|
|
23
|
+
# shellcheck source=render-engine-prompt.sh
|
|
24
|
+
declare -F render_engine_prompt >/dev/null 2>&1 || . "$(dirname "${BASH_SOURCE[0]}")/render-engine-prompt.sh"
|
|
25
|
+
story_criterion_mark() {
|
|
26
|
+
local _v _m; _v=$(mktemp "${TMPDIR:-/tmp}/criterion-origin-XXXXXX.json"); echo '{}' > "$_v"
|
|
27
|
+
_m=$(render_engine_prompt criterion-origin "$_v" remediation); rm -f "$_v"; printf '%s' "$_m"
|
|
28
|
+
}
|
|
29
|
+
# story_criterion_mark_values — the mark as a values object for a template that names it (__REMEDIATION_MARK__).
|
|
30
|
+
story_criterion_mark_values() {
|
|
31
|
+
jq -cn --arg m "$(story_criterion_mark)" '{"__REMEDIATION_MARK__": $m}'
|
|
32
|
+
}
|
|
33
|
+
# B84 (finding RS-18): WHAT IS LOST IS SAID. The block stays what callers expect (stdout empty, status 0 — nothing is added to or removed from a prompt),
|
|
34
|
+
# but a PRD that cannot be read, a PRD without the story, or a plan of record that cannot be read is named on stderr: the reviewer would otherwise judge without
|
|
35
|
+
# criteria, or with the remediator's criteria unmarked, and nothing would say so. A story that simply declares no criteria (brownfield) is silent.
|
|
36
|
+
_story_acs_said() { printf '[story-acs-block] %s\n' "$*" >&2; }
|
|
37
|
+
story_acs_block() {
|
|
38
|
+
local _prd="${1:-}" _id="${2:-}" _heading="${3:-ACCEPTANCE CRITERIA:}"
|
|
39
|
+
[ -n "$_prd" ] && [ -n "$_id" ] || return 0
|
|
40
|
+
if [ ! -f "$_prd" ]; then _story_acs_said "the PRD $_prd was not found — no acceptance-criteria block for $_id"; return 0; fi
|
|
41
|
+
# WHAT THE PLAN IS — the plan of record (lib/plan-of-record.sh), never a criterion's shape. A criterion it does not
|
|
42
|
+
# list, or one the remediator stamped, is marked. With no plan of record only the stamp marks.
|
|
43
|
+
local _plan _list _plan_json=null _err
|
|
44
|
+
_plan=$(plan_of_record_ensured 2>/dev/null || true)
|
|
45
|
+
if [ -n "$_plan" ]; then
|
|
46
|
+
_plan_json=$(jq -c --arg id "$_id" '[.stories[] | select(.id == $id) | .acceptanceCriteria[]? | if type == "object" then (.text // "") else tostring end]' "$_plan" 2>/dev/null) \
|
|
47
|
+
|| { _story_acs_said "the plan of record $_plan cannot be read — the criteria of $_id are shown but not marked against the plan"; _plan_json=null; }
|
|
48
|
+
[ -n "$_plan_json" ] || _plan_json=null
|
|
49
|
+
fi
|
|
50
|
+
_err=$(mktemp "${TMPDIR:-/tmp}/story-acs-err-XXXXXX")
|
|
51
|
+
_list=$(jq -r --arg id "$_id" --arg mark "$(story_criterion_mark)" --argjson plan "$_plan_json" \
|
|
52
|
+
'.stories[] | select(.id == $id) | .acceptanceCriteria // [] | map(select(. != null and . != "")) | to_entries
|
|
53
|
+
| map(.value as $v | ($v | if type == "object" then (.text // tostring) else tostring end) as $t
|
|
54
|
+
| "AC\(.key+1): " + $t
|
|
55
|
+
+ (if (($v | type) == "object" and (($v.origin // "") == "remediation"))
|
|
56
|
+
or ($plan != null and (($plan | index($t)) == null))
|
|
57
|
+
then " " + $mark else "" end))
|
|
58
|
+
| join("\n")' \
|
|
59
|
+
"$_prd" 2>"$_err")
|
|
60
|
+
local _rc=$?
|
|
61
|
+
if [ "$_rc" -ne 0 ]; then
|
|
62
|
+
_story_acs_said "the PRD $_prd is not readable for $_id ($(head -c 200 "$_err" | tr '\n' ' ')) — no acceptance-criteria block"
|
|
63
|
+
rm -f "$_err"; return 0
|
|
64
|
+
fi
|
|
65
|
+
rm -f "$_err"
|
|
66
|
+
if [ -z "$_list" ] && ! jq -e --arg id "$_id" '.stories[]? | select(.id == $id)' "$_prd" >/dev/null 2>&1; then
|
|
67
|
+
_story_acs_said "story $_id is not in the PRD $_prd — no acceptance-criteria block"
|
|
68
|
+
return 0
|
|
69
|
+
fi
|
|
70
|
+
[ -n "$_list" ] || return 0
|
|
71
|
+
printf '%s\n%s\n\n' "$_heading" "$_list"
|
|
72
|
+
}
|
|
@@ -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
|
-
|
|
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
|
-
[ -
|
|
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.
|
|
3
|
+
"version": "0.5.4",
|
|
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": {
|