amsd-pipeline-shared 0.2.0 → 0.4.0

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.
@@ -0,0 +1,36 @@
1
+ #!/usr/bin/env bash
2
+ # preflight-prd-integrity.sh — exhaustive PRD integrity gate.
3
+ #
4
+ # Catches every category of PRD drift that has caused run failures:
5
+ # - Stale runtime artifacts (BUG- stories, numbered bug-fix split suffixes)
6
+ # - Provider/model misalignment
7
+ # - Phase structure corruption
8
+ # - Dirty pre-run state (non-pending active stories)
9
+ # - Path drift (outputDir mismatch, /tmp/ stragglers)
10
+ # - AC quality issues (phantom imports, oversized)
11
+ #
12
+ # Usage:
13
+ # bash orchestrations/scripts/preflight-prd-integrity.sh --prd <path>
14
+ #
15
+ # Exit 0 = all checks passed. Exit 1 = one or more HARD failures.
16
+ # Warnings (⚠) are printed but do not fail the gate.
17
+
18
+ set -euo pipefail
19
+
20
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
21
+
22
+ PRD_FILE=""
23
+ PHASE_ARG=""
24
+ while [[ $# -gt 0 ]]; do
25
+ case $1 in
26
+ --prd) PRD_FILE="$2"; shift 2 ;;
27
+ --phase) PHASE_ARG="$2"; shift 2 ;;
28
+ *) echo "Unknown arg: $1" >&2; exit 1 ;;
29
+ esac
30
+ done
31
+
32
+ if [[ -z "$PRD_FILE" || ! -f "$PRD_FILE" ]]; then
33
+ echo " ✗ PRD file not found: $PRD_FILE" >&2; exit 1
34
+ fi
35
+
36
+ python3 "$SCRIPT_DIR/lib/handlers/prd-integrity-audit.py" "$PHASE_ARG" "$PRD_FILE" "$SCRIPT_DIR/../config/providers.json"
@@ -0,0 +1,251 @@
1
+ #!/usr/bin/env bash
2
+ # PRE-FLIGHT — everything that can be known without spending a run.
3
+ #
4
+ # Every live failure on 2026-08-19/20 was catchable offline in under a minute. None needed a model
5
+ # call, a codeline, or a dollar. This runs the checks that would have caught them.
6
+ #
7
+ # Exit 0 = safe to launch. Non-zero = a defect that WILL surface in a run.
8
+
9
+ # THE COVERAGE GATE IS NOT HERE. This file is a STATIC SOURCE AUDIT — it reads code and reports
10
+ # findings, and it is run at a desk as often as in a pipeline. Halting it on a coverage measurement
11
+ # meant it printed nothing at all whenever coverage was stale, so every check it performs vanished
12
+ # and the operator saw an empty report rather than a reason.
13
+ #
14
+ # The whole-map gate belongs where money moves: the tier3 launchers call require_all_stage_coverage
15
+ # before they spend, which is also what marks the run gated for the per-stage gates.
16
+
17
+ set -uo pipefail
18
+ # NEITHER OF THESE IS WRITTEN DOWN.
19
+ #
20
+ # This defaulted to one developer's absolute home path, and pinned node to one nvm install of
21
+ # one version. On any other machine — CI, a second checkout, a colleague — the default ROOT
22
+ # pointed at a directory that does not exist, and $NODE at an interpreter that does not exist,
23
+ # so a pre-flight whose entire purpose is "catch it before spending a run" could not run at all.
24
+ #
25
+ # The repo root is where this script lives, two levels up. The interpreter is resolved by
26
+ # lib/node-bin.sh, which reads the requirement from package.json engines.node and finds an
27
+ # interpreter that satisfies it — the library that exists for exactly this, and that ten other
28
+ # sites already use.
29
+ _SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
30
+ ROOT="${1:-$(cd "$_SCRIPT_DIR/../.." && pwd)}"
31
+ # shellcheck source=lib/node-bin.sh
32
+ . "$_SCRIPT_DIR/lib/node-bin.sh"
33
+ NODE="$(resolve_node_bin "$ROOT")" || { echo "[preflight] no usable node interpreter" >&2; exit 2; }
34
+ cd "$ROOT" || exit 2
35
+ FAILED=0
36
+ report() { printf '%-34s %s\n' "$1" "$2"; }
37
+ fail() { FAILED=$((FAILED+1)); report "$1" "FAIL $2"; }
38
+ pass() { report "$1" "ok $2"; }
39
+
40
+ # ── 1. SHELL SCOPE/RUNTIME ERRORS ────────────────────────────────────────────
41
+ # bash -n is a SYNTAX check and cannot see `local` outside a function. That one word broke the only
42
+ # reviewer the pipeline runs, produced NO VERDICT eight times, and cost a whole run.
43
+ n=0
44
+ while read -r f; do
45
+ out=$(shellcheck -S error "$f" 2>/dev/null | grep -c 'SC[0-9]* (error)' || true)
46
+ [ "${out:-0}" -gt 0 ] && { n=$((n+out)); echo " $f"; shellcheck -S error "$f" 2>/dev/null | grep 'SC[0-9]* (error)' | sed 's/^/ /'; }
47
+ done < <(ls orchestrations/scripts/*.sh orchestrations/scripts/lib/*.sh 2>/dev/null)
48
+ [ "$n" -eq 0 ] && pass "shell runtime errors" "0" || fail "shell runtime errors" "$n found"
49
+
50
+ # ── 1b. WHAT ACTUALLY RUNS IS CURRENT WITH ITS SOURCE ────────────────────────
51
+ # Every check in this file reads SOURCE. `epam` runs dist/epam.js. A change written, tested and
52
+ # never built passes everything here and reaches a run as the old behaviour — 2026-08-09 (tool-use
53
+ # logging, nine passing tests, zero events) and 2026-08-20 (plugin strictness, caught only by one
54
+ # launcher's own gate). Shared with the launchers: lib/build-freshness.sh.
55
+ # shellcheck source=lib/build-freshness.sh
56
+ source "$ROOT/orchestrations/scripts/lib/build-freshness.sh" 2>/dev/null || true
57
+ if command -v build_is_current >/dev/null 2>&1; then
58
+ _bf=$(build_is_current "$ROOT" 2>&1) && pass "build current with src" "ok" \
59
+ || { fail "build current with src" "stale"; echo "$_bf" | sed 's/^/ /'; }
60
+ else
61
+ fail "build current with src" "lib/build-freshness.sh did not load"
62
+ fi
63
+
64
+ # ── 2. JS / PYTHON PARSE ─────────────────────────────────────────────────────
65
+ n=0
66
+ while read -r f; do "$NODE" --check "$f" >/dev/null 2>&1 || { n=$((n+1)); echo " $f"; }; done \
67
+ < <(ls orchestrations/scripts/*.js orchestrations/scripts/lib/*.js orchestrations/plugins/*.js 2>/dev/null)
68
+ [ "$n" -eq 0 ] && pass "js parse" "0" || fail "js parse" "$n"
69
+ n=0
70
+ while read -r f; do python3 -c "import ast,sys;ast.parse(open(sys.argv[1]).read())" "$f" >/dev/null 2>&1 || { n=$((n+1)); echo " $f"; }; done \
71
+ < <(ls orchestrations/scripts/lib/handlers/*.py orchestrations/scripts/lib/*.py 2>/dev/null)
72
+ [ "$n" -eq 0 ] && pass "python parse" "0" || fail "python parse" "$n"
73
+
74
+ # ── 3. PROVISIONED PLUGINS ACTUALLY LOAD ─────────────────────────────────────
75
+ # verification-plugin.js is provisioned by the codeline and rejected at load for a missing `name`.
76
+ # A warning, so nothing ever surfaced it: the tools simply never reached the agent.
77
+ out=$("$NODE" -e '
78
+ const fs=require("fs"),path=require("path");
79
+ // THE REAL CONTRACT: src/tools/PluginLoader.ts:79-80 requires name AND execute on each TOOL.
80
+ // A first version of this check tested the MODULE for .name and flagged all six plugins — a false
81
+ // positive rate of 5/6. Verified against the loader before being trusted.
82
+ let bad=[];
83
+ for (const f of fs.readdirSync("orchestrations/plugins").filter(x=>x.endsWith(".js"))) {
84
+ let m; try { m=require(path.resolve("orchestrations/plugins",f)); }
85
+ catch(e){ bad.push(f+" (load error: "+e.message.slice(0,50)+")"); continue; }
86
+ if (!Array.isArray(m.tools)) continue; // not a tool plugin
87
+ m.tools.forEach((t,i)=>{
88
+ if (!t || !t.name) bad.push(f+" tool["+i+"] missing required field: name");
89
+ else if (typeof t.execute !== "function") bad.push(f+" tool["+i+"] ("+t.name+") missing execute");
90
+ });
91
+ }
92
+ console.log(bad.join("\n"));' 2>&1)
93
+ [ -z "$out" ] && pass "plugins load" "all" || { fail "plugins load" "$(echo "$out"|wc -l) rejected"; echo "$out" | sed 's/^/ /'; }
94
+
95
+ # ── 4. EVERY TEMPLATE'S PLACEHOLDERS ARE SUPPLIED ────────────────────────────
96
+ # A producer that omits a declared value makes the renderer throw AT RUN TIME. Two of these fired
97
+ # live on consecutive runs (skill-assessment-postphase, plan-corrective).
98
+ out=$("$NODE" -e '
99
+ const fs=require("fs"),path=require("path");
100
+ const SH=["orchestrations/scripts","orchestrations/scripts/lib"].flatMap(d=>fs.readdirSync(d).filter(f=>f.endsWith(".sh")).map(f=>path.join(d,f)));
101
+ const T={};for(const f of fs.readdirSync("orchestrations/prompts/templates").filter(f=>f.endsWith(".json"))){const j=JSON.parse(fs.readFileSync(path.join("orchestrations/prompts/templates",f),"utf8"));T[j.id||f.replace(/\.json$/,"")]=j;}
102
+ // THE RENDERER RULE, not a second copy of it. NO APOSTROPHES IN THIS BLOCK: it is single-quoted
103
+ // shell. This line had its own regex, anchored so that __A____B__ read as ONE placeholder, while
104
+ // engine-prompt.js matches non-greedily precisely because templates put placeholders ADJACENT.
105
+ // The tc-writer body opens __CORRECTIVE_NOTE____TC_WRITER_PROFILE__, renders correctly, and
106
+ // failed pre-flight as MISSING a placeholder that does not exist.
107
+ const {placeholdersIn:ph}=require(path.resolve("orchestrations/scripts/lib/engine-prompt.js"));
108
+ const bad=[];
109
+ for(const f of SH){const L=fs.readFileSync(f,"utf8").split("\n");
110
+ for(let i=0;i<L.length;i++){const m=/render_(?:engine_prompt|or_keep)\s+([a-z0-9-]+)\s+"?\$?\{?([A-Za-z_]*)\}?"?\s*("?([a-z_]+)"?)?/.exec(L[i]);
111
+ if(!m)continue;const j=T[m[1]];if(!j)continue;const key=m[4];let body;
112
+ if(j.bodies){body=key&&j.bodies[key]!==undefined?j.bodies[key]:null;if(body===null)continue;}else body=j.body||"";
113
+ // Values arrive two ways: named in the jq_vals block, or supplied by THE RENDERER. A checker
114
+ // that modelled only the first would report a site as broken while it works — silencing it by
115
+ // "fixing" working code is worse than the miss.
116
+ //
117
+ // The renderer is the supplier now, unconditionally. engine-prompt.js adds exactly the stack
118
+ // placeholders a template DECLARES (`stackDeclared`). This used to credit merge_stack_facts
119
+ // instead, and only when that call appeared near the jq_vals block — a second implementation
120
+ // of the same idea that merged ALL SEVEN keys, so the renderer threw "was given values it does
121
+ // not use" on every template declaring fewer, and four seams could not render at all.
122
+ const STACK_FACT_KEYS=["__STACK__","__MANIFEST_FILE__","__TEST_COMMAND__","__TEST_FILE_CONVENTIONS__","__PROTECTED_FILES__","__IMPL_ROLE__","__TEST_ROLE__"];
123
+ let sup=null;for(let k=i;k>=0&&k>i-30;k--){if(/jq_vals|jq -n/.test(L[k])){sup=ph(L.slice(k,i).join("\n"));
124
+ sup=sup.concat(STACK_FACT_KEYS);
125
+ break;}}
126
+ if(!sup)continue;const miss=ph(body).filter(p=>!sup.includes(p));
127
+ if(miss.length)bad.push(f.replace("orchestrations/scripts/","")+":"+(i+1)+" "+m[1]+" MISSING "+miss.join(", "));}}
128
+ console.log(bad.join("\n"));' 2>&1)
129
+ [ -z "$out" ] && pass "template values supplied" "all" || { fail "template values supplied" "$(echo "$out"|wc -l) mismatched"; echo "$out" | sed 's/^/ /'; }
130
+
131
+ # ── 5. HANDLERS PRODUCE OUTPUT FOR THE REAL STORY ────────────────────────────
132
+ # tc-story-context.py returned EMPTY for a brownfield story, so the TC writer was invoked three
133
+ # times per run with an empty brief and wrote nothing — while its gate reported PASSED.
134
+ #
135
+ # THE CHECK ITSELF HAD THE SAME DEFECT IT WAS WRITTEN TO CATCH. It took stories[0], ran the handler
136
+ # and called an empty answer a failure. But the handler SKIPS a story that already has
137
+ # testCriteria.facts — correctly, there is nothing to brief — so once AMSD-2041 had its 21 facts
138
+ # the check failed on the handler doing exactly the right thing. A check whose scope does not match
139
+ # its claim reports the wrong thing in both directions.
140
+ #
141
+ # It now asks the handler about a story that ACTUALLY needs context, and says so when none does.
142
+ # shellcheck source=lib/project-config.sh
143
+ . "$ROOT/orchestrations/scripts/lib/project-config.sh"
144
+ PRD=$(ls -t "$(projects_root "$ROOT")"/*/runs/*/work/*-prd.json 2>/dev/null | head -1)
145
+ if [ -n "$PRD" ]; then
146
+ # The selector is a handler, not an inline program, so the same question this check asks can be
147
+ # asked by a test — a check that can only ever be exercised by running the whole pre-flight
148
+ # against whatever PRD happens to be on disk is a check nobody can prove.
149
+ SID=$("$NODE" "orchestrations/scripts/lib/handlers/tc-story-needing-context.js" "$PRD" core 2>/dev/null || echo "")
150
+ OUT_DIR=$(dirname "$(dirname "$PRD")")
151
+ if [ -z "$SID" ]; then
152
+ report "tc story context" "skip (no story in this PRD needs criteria — nothing to brief)"
153
+ else
154
+ ctx=$(python3 orchestrations/scripts/lib/handlers/tc-story-context.py "$OUT_DIR" "$PRD" core "$SID" 2>/dev/null | wc -c)
155
+ [ "${ctx:-0}" -gt 20 ] && pass "tc story context ($SID)" "${ctx} bytes" \
156
+ || fail "tc story context ($SID)" "EMPTY — the TC writer would be given nothing"
157
+ fi
158
+ else
159
+ report "tc story context" "skip (no run PRD on disk)"
160
+ fi
161
+
162
+ # ── 6. DETERMINISTIC CHECKS ARE READ, NOT CALLED BARE ────────────────────────
163
+ # A check whose exit status is discarded is not a gate. lockfile-sync blocked four times live and
164
+ # the story completed anyway.
165
+ out=$("$NODE" -e '
166
+ const fs=require("fs");const s=fs.readFileSync("orchestrations/scripts/claude.sh","utf8");
167
+ const f=s.slice(s.indexOf("run_external_verification() {"));const b=f.slice(0,f.indexOf("\n}\n"));
168
+ const bare=b.split("\n").map(l=>l.trim()).filter(l=>/^run_[a-z_]+_check "/.test(l)).filter(l=>!/^if ! /.test(l)&&!/\|\||&&/.test(l));
169
+ console.log(bare.join("\n"));' 2>&1)
170
+ [ -z "$out" ] && pass "gate verdicts read" "all" || { fail "gate verdicts read" "$(echo "$out"|wc -l) bare"; echo "$out" | sed 's/^/ /'; }
171
+
172
+ # ── 7. RATCHETS — THE DEBT MAY ONLY SHRINK ───────────────────────────────────
173
+ # Eight of the ten defects confirmed on 2026-08-20 were hardcoding, and every one of them was
174
+ # added by a change that passed every check above. Counting is not a fix; it is the only thing
175
+ # that stops the number growing while the fixes are worked through one at a time.
176
+ #
177
+ # A count BELOW its baseline lowers the baseline in place — today's number becomes tomorrow's
178
+ # ceiling. A count ABOVE it fails. Raising a baseline by hand is an operator decision.
179
+ BASELINES="orchestrations/config/preflight-baselines.json"
180
+ ratchet() {
181
+ local _label="$1" _key="$2" _scanner="$3"
182
+ local _out _n _base _rc
183
+ # An empty scan is not automatically a pass: a scanner that cannot run reports nothing, which
184
+ # is the exact shape of every defect this file exists to catch. rc is captured on the SAME
185
+ # logical step as the call — a check that reads $? a line later reads whatever ran in between.
186
+ _out=$("$NODE" "orchestrations/scripts/lib/handlers/$_scanner" "$ROOT" 2>&1); _rc=$?
187
+ if [ "$_rc" -ne 0 ]; then fail "$_label" "scanner did not run"; printf '%s\n' "$_out" | sed 's/^/ /'; return; fi
188
+ _n=$(printf '%s' "$_out" | grep -c . || true)
189
+ _base=$("$NODE" -e 'try{const j=require(process.argv[1]);const v=j[process.argv[2]];console.log(Number.isFinite(v)?v:"")}catch{console.log("")}' \
190
+ "$ROOT/$BASELINES" "$_key" 2>/dev/null)
191
+ if [ -z "$_base" ]; then fail "$_label" "no baseline for $_key in $BASELINES"; return; fi
192
+ if [ "$_n" -gt "$_base" ]; then
193
+ fail "$_label" "$_n > baseline $_base — $(($_n - _base)) new"
194
+ printf '%s\n' "$_out" | sed 's/^/ /'
195
+ elif [ "$_n" -lt "$_base" ]; then
196
+ "$NODE" -e '
197
+ const fs=require("fs");const p=process.argv[1];const j=JSON.parse(fs.readFileSync(p,"utf8"));
198
+ j[process.argv[2]]=Number(process.argv[3]);fs.writeFileSync(p,JSON.stringify(j,null,2)+"\n");' \
199
+ "$ROOT/$BASELINES" "$_key" "$_n" 2>/dev/null
200
+ pass "$_label" "$_n (improved from $_base — baseline tightened)"
201
+ else
202
+ pass "$_label" "$_n (at baseline)"
203
+ fi
204
+ }
205
+ ratchet "literal ratchet" "duplicatedLiterals" "scan-duplicated-literals.js"
206
+ ratchet "guard calibration" "uncalibratedGuards" "scan-uncalibrated-guards.js"
207
+ # THE SHELL ITSELF, READ STATICALLY. Coverage says how much of the engine a test has executed;
208
+ # this says how much of it is wrong on its face, across every .sh file, needing no test written
209
+ # first. The classes it counts have each already cost a run: an export masking a command status,
210
+ # `A && B || C` read as if-then-else, a redirection with no command, a subshell losing an assignment.
211
+ ratchet "shell defects" "shellDefects" "scan-shell-defects.js"
212
+
213
+ # ── THE HARDCODING AUDIT CAN STILL SEE ───────────────────────────────────────
214
+ # Not its count — that is a research number nobody should gate on, and the file says so itself.
215
+ # This asks the prior question: does each category still detect the defect it claims to cover?
216
+ #
217
+ # Every hardcoding defect found on 2026-08-23 was invisible to that audit — it scanned .sh/.js/.ts
218
+ # and declared config exempt, so RELOCATING a literal into orchestrations/config counted as
219
+ # repair; its numeric category needed a named knob, so topN = 8 matched nothing. And nothing ran
220
+ # it, so the blindness was never observed. --calibrate runs every pattern against a fixture of
221
+ # known-bad lines and fails on any that has gone blind. It found a real gap on its first run: the
222
+ # branch pattern required a character AFTER the branch name, so `git checkout develop` at the end
223
+ # of a line matched nothing.
224
+ _hc_out=$(bash orchestrations/scripts/hardcoding-audit.sh --calibrate 2>&1); _hc_rc=$?
225
+ if [ "$_hc_rc" -eq 0 ]; then
226
+ pass "hardcoding audit sees" "$(printf '%s' "$_hc_out" | grep -c 'sees its example') categories"
227
+ else
228
+ fail "hardcoding audit sees" "a category has gone BLIND"
229
+ printf '%s\n' "$_hc_out" | grep -E 'BLIND' | sed 's/^/ /'
230
+ fi
231
+
232
+ # ── 8. THE OPERATOR'S OWN DECISIONS ──────────────────────────────────────────
233
+ # orchestrations/config/remediation-register.json is DATA the operator owns: literals that must be
234
+ # gone, and guards judged useless. Marking one enforce:true makes it fatal here — no code change,
235
+ # no negotiation with this file. An unreadable register FAILS rather than reporting nothing.
236
+ out=$("$NODE" "orchestrations/scripts/lib/handlers/check-remediation-register.js" "$ROOT" 2>&1)
237
+ rc=$?
238
+ if [ "$rc" -eq 2 ]; then
239
+ fail "remediation register" "unreadable — decisions cannot be enforced"
240
+ echo "$out" | sed 's/^/ /'
241
+ elif [ "$rc" -ne 0 ]; then
242
+ fail "remediation register" "$(printf '%s' "$out" | grep -c . || true) enforced item(s) still present"
243
+ echo "$out" | sed 's/^/ /'
244
+ else
245
+ pass "remediation register" "no enforced violations"
246
+ fi
247
+
248
+ echo
249
+ [ "$FAILED" -eq 0 ] && echo "PRE-FLIGHT PASS — nothing here will surface in a run" \
250
+ || echo "PRE-FLIGHT FAIL — $FAILED check(s); each one WILL surface in a run"
251
+ exit "$FAILED"
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "amsd-pipeline-shared",
3
- "version": "0.2.0",
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 \u2014 retry, ladder, self-heal, cost) and the libraries it runs.",
3
+ "version": "0.4.0",
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": {
7
7
  "amsd-pipeline-shared": "bin/amsd-pipeline-shared.js"