amsd-pipeline-shared 0.5.22 → 0.5.23
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.
|
@@ -241,3 +241,6 @@ a7d52fab41432240f1e2dbffd6710ea5 scripts/lib/project-prompt-builder.js
|
|
|
241
241
|
dea5be1123db8ab5b054fef8cc1388fe scripts/mint-agents-step.js
|
|
242
242
|
eb4979b0f88ed8449b2e4702d0dff7a5 scripts/lib/prompt-variant.sh
|
|
243
243
|
d4992178558476af4c4c228e6aa63b2d scripts/lib/mint-and-spec.sh
|
|
244
|
+
fac1b26443bb52ba45ba072f4d654920 scripts/lib/story-guards.sh
|
|
245
|
+
ada237373615a26d924226184ed7614b scripts/lib/prd-change-review.sh
|
|
246
|
+
c2dfd2381db0242fdbc487a948fd7feb scripts/lib/agent-si.js
|
package/docs/gateway-files.txt
CHANGED
|
@@ -153,6 +153,7 @@ scripts/lib/agent-contract.js
|
|
|
153
153
|
scripts/lib/agent-ladder.sh
|
|
154
154
|
scripts/lib/agent-messages.js
|
|
155
155
|
scripts/lib/agent-si.js
|
|
156
|
+
scripts/lib/agent-si.js
|
|
156
157
|
scripts/lib/checkpoints.sh
|
|
157
158
|
scripts/lib/codeline-lanes.sh
|
|
158
159
|
scripts/lib/codeline-packages.js
|
|
@@ -222,6 +223,7 @@ scripts/lib/pipeline-tools.js
|
|
|
222
223
|
scripts/lib/plan-hash.js
|
|
223
224
|
scripts/lib/plan-of-record.sh
|
|
224
225
|
scripts/lib/plan-scope.js
|
|
226
|
+
scripts/lib/prd-change-review.sh
|
|
225
227
|
scripts/lib/preflight.sh
|
|
226
228
|
scripts/lib/prelaunch.js
|
|
227
229
|
scripts/lib/producers/fix-plan.js
|
|
@@ -244,6 +246,7 @@ scripts/lib/self-heal.js
|
|
|
244
246
|
scripts/lib/stage-coverage-gate.sh
|
|
245
247
|
scripts/lib/story-acs-block.sh
|
|
246
248
|
scripts/lib/story-attempt.sh
|
|
249
|
+
scripts/lib/story-guards.sh
|
|
247
250
|
scripts/lib/story-outputs.sh
|
|
248
251
|
scripts/lib/story-retry-state.sh
|
|
249
252
|
scripts/lib/tool-timeouts.js
|
|
@@ -303,6 +303,12 @@ _run_agent_mint() {
|
|
|
303
303
|
fi
|
|
304
304
|
fi
|
|
305
305
|
[ "${EPAM_SKIP_AGENT_MINT:-0}" = "1" ] || {
|
|
306
|
+
# A FULL MINT STARTS FROM A CLEAN SLATE (B49; operator, 2026-10-22: "Fix critical"): the roster it replaces is cleared first, as
|
|
307
|
+
# the deferred decision's own clean slate does, so the mint's own seams take their base SI (lib/agent-si.js: the base is for the
|
|
308
|
+
# seams that run before the roster exists). A fresh install's committed roster has no agent-mint entry, and a full mint
|
|
309
|
+
# (EPAM_REGENERATE_CODELINE_ASSETS=1) was refused at the hub. The codeline's recorded roster (.prompt-cache/<codeline>/minted) is
|
|
310
|
+
# not touched.
|
|
311
|
+
codeline_minted_clear
|
|
306
312
|
log "[mint] Minting project agents and assigning roles..."
|
|
307
313
|
"$NODE_BIN" "$SCRIPT_DIR/mint-agents-step.js" \
|
|
308
314
|
--prd "$_prd" \
|
|
@@ -0,0 +1,380 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# prd-change-review.sh — moved verbatim out of claude.sh by tools/split-main-into-modules.py
|
|
3
|
+
# (7 functions). Sourced by claude.sh; SCRIPT_DIR and the globals it sets
|
|
4
|
+
# are in scope exactly as they were. A move, not an edit: every body is byte-identical to
|
|
5
|
+
# the golden recorded at the move (see the identity test).
|
|
6
|
+
|
|
7
|
+
# run_prd_change_reviewer <story_id> <change_type> <before_json> <after_json>
|
|
8
|
+
# Validates a proposed PRD AC/TC or profiles.json change using the gate model.
|
|
9
|
+
# change_type: ac_patch | tc_patch | skill_note | profile_addendum
|
|
10
|
+
# Echoes "pass" or "fail"; caller decides whether to revert on fail.
|
|
11
|
+
# Silently returns "pass" if gate model is not configured (non-blocking).
|
|
12
|
+
run_prd_change_reviewer() {
|
|
13
|
+
local story_id="$1"
|
|
14
|
+
local change_type="$2"
|
|
15
|
+
local before_json="$3"
|
|
16
|
+
local after_json="$4"
|
|
17
|
+
|
|
18
|
+
local gate_provider="${ORCH_GATE_PROVIDER:-}"
|
|
19
|
+
if [ -z "$gate_provider" ]; then
|
|
20
|
+
# A GATE THAT CANNOT JUDGE DOES NOT PASS. This emitted a literal "pass", so with no gate
|
|
21
|
+
# provider configured every KB/PRD/profile write was AUTO-APPROVED and the caller read a
|
|
22
|
+
# manufactured verdict as a real one — indistinguishable from a review that ran and found
|
|
23
|
+
# nothing wrong.
|
|
24
|
+
#
|
|
25
|
+
# 'unreviewed' is the honest answer and callers already understand it:
|
|
26
|
+
# reviewOutcomeKeepsChange() accepts only an explicit pass, so the change reverts rather
|
|
27
|
+
# than standing on a judgement nobody made.
|
|
28
|
+
error " [PRD-ChangeReviewer] no gate provider configured — NOT reviewing; returning 'unreviewed' so the change is not kept on an unmade judgement" >&2
|
|
29
|
+
echo "unreviewed"
|
|
30
|
+
return 0
|
|
31
|
+
fi
|
|
32
|
+
# KB/PRD/profile writes are persistent and must be reviewed by the highest-quality
|
|
33
|
+
# model available, not the cheap gate model. Use ESCALATION_MODEL_HIGH with high
|
|
34
|
+
# reasoning so every persisted write is agentic-quality-reviewed.
|
|
35
|
+
# THE REVIEWER'S OWN SEAM. This was a run-wide "high" model behind a run-wide pin behind a
|
|
36
|
+
# vendor literal — three sources, none of them the ladder, and the literal always answered so
|
|
37
|
+
# the ladder never had to.
|
|
38
|
+
local gate_model
|
|
39
|
+
gate_model=$(seam_model_or_fail "prd-change-reviewer" 2>/dev/null || printf '')
|
|
40
|
+
|
|
41
|
+
# Select profile based on change type — KB entries use the stricter kb-change-reviewer
|
|
42
|
+
local _profile_key="prd-change-reviewer"
|
|
43
|
+
[ "$change_type" = "kb_entry" ] && _profile_key="kb-change-reviewer"
|
|
44
|
+
local reviewer_profile=""
|
|
45
|
+
if [ -f "$profiles_file" ]; then
|
|
46
|
+
reviewer_profile=$(require_profile "$_profile_key" "$profiles_file" || true)
|
|
47
|
+
fi
|
|
48
|
+
|
|
49
|
+
local review_prompt
|
|
50
|
+
# RENDERED FROM THE TEMPLATE LAYER. Values go via a FILE, never argv: before/after carry
|
|
51
|
+
# whole PRD fragments, and a value past ARG_MAX exits 126 with an empty result — which is
|
|
52
|
+
# how the FailureAnalyst died silently earlier today.
|
|
53
|
+
local _rv_vals; _rv_vals=$(mktemp "${TMPDIR:-/tmp}/prd-review-vals-XXXXXX.json")
|
|
54
|
+
jq_vals --arg profile "$reviewer_profile" --arg story "$story_id" --arg ct "$change_type" \
|
|
55
|
+
--rawfile before <(printf '%s' "$before_json") --rawfile after <(printf '%s' "$after_json") \
|
|
56
|
+
'{"__REVIEWER_PROFILE__":$profile,"__STORY_ID__":$story,"__CHANGE_TYPE__":$ct,"__BEFORE__":$before,"__AFTER__":$after}' \
|
|
57
|
+
> "$_rv_vals" 2>/dev/null
|
|
58
|
+
if ! review_prompt=$(render_engine_prompt prd-change-reviewer "$_rv_vals"); then
|
|
59
|
+
# >&2 REQUIRED: this function returns its verdict on STDOUT, so any log written to
|
|
60
|
+
# stdout is read as part of the verdict. There is a test for exactly this.
|
|
61
|
+
error " [PRD-Reviewer] cannot render its prompt — refusing to review with no instructions" >&2
|
|
62
|
+
# The honest verdict, as with no gate provider: nobody judged it. Returning nothing let every caller that
|
|
63
|
+
# tested only for "fail" keep the change — a self-heal tool was written with a .reviewed marker this way.
|
|
64
|
+
rm -f "$_rv_vals"; echo "unreviewed"; return 1
|
|
65
|
+
fi
|
|
66
|
+
rm -f "$_rv_vals"
|
|
67
|
+
|
|
68
|
+
local review_raw=""
|
|
69
|
+
# Full agent audit, 2026-07-31 (same class as HEAL-BLIND): several of this
|
|
70
|
+
# reviewer's own rejection rules require checking a claim against the real
|
|
71
|
+
# codebase (stack/tech usage, TC-fact verifiability), but this call had no
|
|
72
|
+
# tool access at all — a live incident already occurred (rejected correct
|
|
73
|
+
# Contentstack advice for Metrolinx with no way to check the real stack).
|
|
74
|
+
# Reuses the same shared, read-only allowlist every other gate agent draws
|
|
75
|
+
# from, bounded the same way.
|
|
76
|
+
review_raw=$(echo "$review_prompt" | \
|
|
77
|
+
EPAM_AGENT_NAME="prd-change-reviewer" EPAM_STORY_ID="${story_id}" \
|
|
78
|
+
AI_PROVIDER="$gate_provider" \
|
|
79
|
+
AI_MODEL="$gate_model" \
|
|
80
|
+
EPAM_CLI="${EPAM_CLI:-}" \
|
|
81
|
+
EPAM_REASONING_EFFORT="high" \
|
|
82
|
+
EPAM_TEMPERATURE="0.7" \
|
|
83
|
+
AI_GATE_ALLOW_TOOLS=1 \
|
|
84
|
+
EPAM_ALLOWED_TOOLS="${ORCH_GATE_ALLOWED_TOOLS:-}" \
|
|
85
|
+
EPAM_MAX_TOOL_CALLS="${PRD_CHANGE_REVIEWER_MAX_TOOL_CALLS:-24}" \
|
|
86
|
+
bash "${AI_RUNNER_CMD:-$SCRIPT_DIR/ai-run.sh}" --provider "$gate_provider" \
|
|
87
|
+
${gate_model:+--model "$gate_model"} \
|
|
88
|
+
2>/dev/null || echo '{"verdict":"fail","issues":["the reviewer could not be reached — the change was NOT reviewed"],"reason":"reviewer unavailable"}')
|
|
89
|
+
|
|
90
|
+
# ASKED AND GOT NO ANSWER IS NOT APPROVAL.
|
|
91
|
+
#
|
|
92
|
+
# These defaulted to 'pass' in four places, including an explicit
|
|
93
|
+
# {"verdict":"pass","reason":"reviewer unavailable"} above — a reviewer that could not be
|
|
94
|
+
# reached approved the change and said so in the reason field. This gate covers ac_patch and
|
|
95
|
+
# tc_patch, so that silently accepted edits to acceptance criteria, which are supposed to be
|
|
96
|
+
# immutable. code-review-cycle.sh settled the same question on 2026-07-23: 'SAFE default =
|
|
97
|
+
# BLOCK, never silently approve an unreviewed change'.
|
|
98
|
+
#
|
|
99
|
+
# The documented opt-out above is untouched: with NO gate model configured the reviewer is
|
|
100
|
+
# disabled and returns pass, because it was never asked. That is a different state from
|
|
101
|
+
# asking and getting nothing back.
|
|
102
|
+
local verdict=""
|
|
103
|
+
verdict=$(echo "$review_raw" | python3 "$SCRIPT_DIR/lib/handlers/prd-change-verdict.py" 2>/dev/null || echo "fail")
|
|
104
|
+
|
|
105
|
+
local issues=""
|
|
106
|
+
issues=$(echo "$review_raw" | python3 "$SCRIPT_DIR/lib/handlers/prd-change-issues.py" 2>/dev/null || echo "")
|
|
107
|
+
|
|
108
|
+
# CRITICAL: this function's return value is captured via command substitution
|
|
109
|
+
# ($(run_prd_change_reviewer ...)). warning()/log() write to stdout as well as
|
|
110
|
+
# the progress log, so any call to them here would pollute the captured verdict
|
|
111
|
+
# with extra lines — making `[ "$verdict" = "fail" ]` at every call site always
|
|
112
|
+
# false (the string would be "warning text\nfail", not "fail"), silently treating
|
|
113
|
+
# every rejection as an approval. Redirect to stderr so only the final echo
|
|
114
|
+
# reaches the caller.
|
|
115
|
+
# Exposes the rejection reason to callers (e.g. run_change_with_reviewer_retry).
|
|
116
|
+
# Every caller invokes this function via $(...) command substitution, which
|
|
117
|
+
# forks a subshell — a plain variable assignment here (PRD_REVIEW_ISSUES=...)
|
|
118
|
+
# would never be visible to the caller's shell. A file survives the subshell
|
|
119
|
+
# exit, so use that instead. $$ scopes the file to this process (worktree
|
|
120
|
+
# primary/independent run as separate PIDs, so no cross-process collision).
|
|
121
|
+
printf '%s' "$issues" > "${TMPDIR:-/tmp}/.prd-review-issues-$$" 2>/dev/null || true
|
|
122
|
+
|
|
123
|
+
if [ "$verdict" = "fail" ]; then
|
|
124
|
+
warning " [PRD-Reviewer] REJECTED ${change_type} for ${story_id}: ${issues:-no details}" >&2
|
|
125
|
+
echo "fail"
|
|
126
|
+
else
|
|
127
|
+
log " [PRD-Reviewer] APPROVED ${change_type} for ${story_id}" >&2
|
|
128
|
+
echo "pass"
|
|
129
|
+
fi
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
# run_prd_change_summarizer <story_id> <change_type> <issues> <rejected_text>
|
|
133
|
+
# Rewrites rejected self-heal text to address the reviewer's stated issues instead
|
|
134
|
+
# of discarding it outright. Most kb_entry/skill_note rejections are FORMAT problems
|
|
135
|
+
# (over 200 chars, wrong verb tense, references a specific story ID, truncated
|
|
136
|
+
# mid-sentence) — the underlying lesson is usually sound, only its shape is wrong.
|
|
137
|
+
# Prints the reformatted text to stdout (falls back to the original text if the
|
|
138
|
+
# gate model is unavailable or returns nothing usable).
|
|
139
|
+
run_prd_change_summarizer() {
|
|
140
|
+
local story_id="$1"
|
|
141
|
+
local change_type="$2"
|
|
142
|
+
local issues="$3"
|
|
143
|
+
local rejected_text="$4"
|
|
144
|
+
|
|
145
|
+
# THE SAME CHAIN ITS FAMILY USES. ac-gate.js, codeline-discovery.js and cpa-inference.js all
|
|
146
|
+
# consult EPAM_ORCHESTRATION_PROVIDER before giving up; this one read ORCH_GATE_PROVIDER alone,
|
|
147
|
+
# so a run that set the orchestration provider and not the gate provider lost EVERY rewrite.
|
|
148
|
+
local gate_provider="${ORCH_GATE_PROVIDER:-${EPAM_ORCHESTRATION_PROVIDER:-}}"
|
|
149
|
+
if [ -z "$gate_provider" ]; then
|
|
150
|
+
# DEGRADING IS FINE; DEGRADING IN SILENCE IS NOT. This returned the rejected text with no
|
|
151
|
+
# diagnostic and exit 0, and the caller assigns the result as the rewritten value
|
|
152
|
+
# (current=$(run_prd_change_summarizer ...)) — so the content a reviewer had just rejected
|
|
153
|
+
# flowed onward as though it had been fixed. The text is still returned, because the caller
|
|
154
|
+
# must not be left with nothing; what changes is that the skip is now visible.
|
|
155
|
+
error " [PRD-Summarizer] no provider resolved (ORCH_GATE_PROVIDER and EPAM_ORCHESTRATION_PROVIDER are both unset) — skipping the rewrite; the REJECTED text is being returned unchanged" >&2
|
|
156
|
+
printf '%s' "$rejected_text"
|
|
157
|
+
return 0
|
|
158
|
+
fi
|
|
159
|
+
# Summarizer rewrites rejected KB/PRD/profile writes — must use the same
|
|
160
|
+
# high-quality model as the reviewer so the rewrite is meaningfully better.
|
|
161
|
+
# THE REVIEWER'S OWN SEAM. This was a run-wide "high" model behind a run-wide pin behind a
|
|
162
|
+
# vendor literal — three sources, none of them the ladder, and the literal always answered so
|
|
163
|
+
# the ladder never had to.
|
|
164
|
+
local gate_model
|
|
165
|
+
gate_model=$(seam_model_or_fail "prd-change-reviewer" 2>/dev/null || printf '')
|
|
166
|
+
|
|
167
|
+
# tool_creation rewrites a bash script, not a short prose rule — the
|
|
168
|
+
# kb_entry/skill_note constraints (single line, under 200 chars, imperative
|
|
169
|
+
# verb) would corrupt working code. Branch the prompt AND the post-processing
|
|
170
|
+
# (no `tr -d '\n'` — a script needs its newlines) by change type.
|
|
171
|
+
local summarize_prompt output_cap _sum_template
|
|
172
|
+
if [ "$change_type" = "tool_creation" ]; then
|
|
173
|
+
_sum_template="prd-change-summarizer-tool"
|
|
174
|
+
output_cap=4000
|
|
175
|
+
else
|
|
176
|
+
_sum_template="prd-change-summarizer-text"
|
|
177
|
+
output_cap=400
|
|
178
|
+
fi
|
|
179
|
+
|
|
180
|
+
# One renderer for both variants; the branch above chose WHICH prompt, not its text.
|
|
181
|
+
local _sum_vals; _sum_vals=$(mktemp "${TMPDIR:-/tmp}/prd-sum-vals-XXXXXX.json")
|
|
182
|
+
# Each variant gets exactly the values ITS template uses. The renderer rejects a value
|
|
183
|
+
# nobody uses, deliberately: an extra value means the caller believes it supplied something
|
|
184
|
+
# the prompt never mentions, which is the same defect as a missing one seen from the other
|
|
185
|
+
# side. The tool variant carries no change type — a bash script is a bash script.
|
|
186
|
+
if [ "$_sum_template" = "prd-change-summarizer-tool" ]; then
|
|
187
|
+
jq_vals --arg story "$story_id" \
|
|
188
|
+
--rawfile issues <(printf '%s' "${issues:-no details}") \
|
|
189
|
+
--rawfile rejected <(printf '%s' "$rejected_text") \
|
|
190
|
+
'{"__STORY_ID__":$story,"__ISSUES__":$issues,"__REJECTED_TEXT__":$rejected}' \
|
|
191
|
+
> "$_sum_vals" 2>/dev/null
|
|
192
|
+
else
|
|
193
|
+
jq_vals --arg story "$story_id" --arg ct "$change_type" \
|
|
194
|
+
--rawfile issues <(printf '%s' "${issues:-no details}") \
|
|
195
|
+
--rawfile rejected <(printf '%s' "$rejected_text") \
|
|
196
|
+
'{"__STORY_ID__":$story,"__CHANGE_TYPE__":$ct,"__ISSUES__":$issues,"__REJECTED_TEXT__":$rejected}' \
|
|
197
|
+
> "$_sum_vals" 2>/dev/null
|
|
198
|
+
fi
|
|
199
|
+
if ! summarize_prompt=$(render_engine_prompt "$_sum_template" "$_sum_vals"); then
|
|
200
|
+
# >&2 REQUIRED: the caller captures this function with command substitution
|
|
201
|
+
# (current=$(run_prd_change_summarizer ...)), so a log on stdout becomes the rewritten
|
|
202
|
+
# text. Same hazard as the reviewer above.
|
|
203
|
+
error " [PRD-Summarizer] cannot render '$_sum_template' — refusing to rewrite with no instructions" >&2
|
|
204
|
+
rm -f "$_sum_vals"; return 1
|
|
205
|
+
fi
|
|
206
|
+
rm -f "$_sum_vals"
|
|
207
|
+
|
|
208
|
+
local summarized=""
|
|
209
|
+
summarized=$(echo "$summarize_prompt" | \
|
|
210
|
+
EPAM_AGENT_NAME="prd-change-summarizer" EPAM_STORY_ID="${story_id}" \
|
|
211
|
+
AI_PROVIDER="$gate_provider" \
|
|
212
|
+
AI_MODEL="$gate_model" \
|
|
213
|
+
EPAM_CLI="${EPAM_CLI:-}" \
|
|
214
|
+
EPAM_REASONING_EFFORT="high" \
|
|
215
|
+
EPAM_TEMPERATURE="0.7" \
|
|
216
|
+
bash "${AI_RUNNER_CMD:-$SCRIPT_DIR/ai-run.sh}" --provider "$gate_provider" \
|
|
217
|
+
${gate_model:+--model "$gate_model"} \
|
|
218
|
+
2>/dev/null | head -c "$output_cap" || echo "")
|
|
219
|
+
if [ "$change_type" = "tool_creation" ]; then
|
|
220
|
+
summarized="$(echo "$summarized" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')"
|
|
221
|
+
else
|
|
222
|
+
summarized="$(echo "$summarized" | tr -d '\n' | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')"
|
|
223
|
+
fi
|
|
224
|
+
|
|
225
|
+
if [ -n "$summarized" ]; then
|
|
226
|
+
printf '%s' "$summarized"
|
|
227
|
+
else
|
|
228
|
+
printf '%s' "$rejected_text"
|
|
229
|
+
fi
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
# _skill_note_format_ok <note> <story_id> <existing_profile_text>
|
|
233
|
+
# Deterministic pre-check for the OBJECTIVELY verifiable skill_note/kb_entry
|
|
234
|
+
# format rules already stated in profiles.json's prd-change-reviewer /
|
|
235
|
+
# kb-change-reviewer profiles: length cap, imperative opener, no story-ID
|
|
236
|
+
# reference, not a verbatim duplicate of an existing line in the same
|
|
237
|
+
# profile. These are checkable facts, not subjective judgment calls -- yet a
|
|
238
|
+
# live run (2026-07-07) observed the LLM reviewer reject notes that already
|
|
239
|
+
# satisfied every one of these rules (e.g. "Always end TypeScript statements
|
|
240
|
+
# with semicolons...", well under 200 chars, no story ID), 3/3 times, purely
|
|
241
|
+
# on flaky format judgment. That burns a gate round-trip per rejection for
|
|
242
|
+
# zero benefit. When a candidate passes this check, skip the LLM review
|
|
243
|
+
# entirely for format -- genuinely subjective calls (does this CONTRADICT an
|
|
244
|
+
# existing rule, is the underlying lesson actually sound) are out of scope
|
|
245
|
+
# here and still need a real reviewer, so this only short-circuits the
|
|
246
|
+
# specific failure mode that was observed wasting cost.
|
|
247
|
+
# The declared skill-note length limit. One source (config/self-heal.json), read by the checker
|
|
248
|
+
# here and handed to the failure analyst so it writes within it in the first place.
|
|
249
|
+
_skill_note_max_chars() {
|
|
250
|
+
local _cfg="${AUTOMATION_DIR:-$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)}/config/self-heal.json"
|
|
251
|
+
local _v=""
|
|
252
|
+
[ -f "$_cfg" ] && _v=$(jq -r '.skillNote.maxChars // empty' "$_cfg" 2>/dev/null)
|
|
253
|
+
case "$_v" in (''|*[!0-9]*)
|
|
254
|
+
echo "[skill-note] config/self-heal.json declares no numeric skillNote.maxChars — refusing to guess a limit" >&2
|
|
255
|
+
return 1 ;;
|
|
256
|
+
esac
|
|
257
|
+
printf '%s' "$_v"
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
_skill_note_format_ok() {
|
|
261
|
+
local note="$1"
|
|
262
|
+
local story_id="$2"
|
|
263
|
+
local existing_profile_text="${3:-}"
|
|
264
|
+
[ -z "$note" ] && return 1
|
|
265
|
+
# THE DECLARED LIMIT, NOT A NUMBER WRITTEN HERE. See config/self-heal.json: the producer is
|
|
266
|
+
# told the same value, so a note no longer has to be rejected and rewritten to discover it.
|
|
267
|
+
local _max; _max=$(_skill_note_max_chars)
|
|
268
|
+
[ "${#note}" -le "$_max" ] || return 1
|
|
269
|
+
echo "$note" | grep -Eiq "^(${SKILL_NOTE_IMPERATIVE_OPENERS})\\b" || return 1
|
|
270
|
+
if [ -n "$story_id" ] && echo "$note" | grep -qi "$story_id"; then
|
|
271
|
+
return 1
|
|
272
|
+
fi
|
|
273
|
+
if [ -n "$existing_profile_text" ] && echo "$existing_profile_text" | grep -qF "$note"; then
|
|
274
|
+
return 1
|
|
275
|
+
fi
|
|
276
|
+
return 0
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
# _ensure_imperative_opener <note>
|
|
280
|
+
# Deterministically normalizes a skill_note/kb_entry candidate to satisfy
|
|
281
|
+
# _skill_note_format_ok's imperative-opener check, WITHOUT ever needing an
|
|
282
|
+
# LLM rewrite round-trip. Closes a live gap (2026-07-12, tier3-travel-app
|
|
283
|
+
# run): SKY-002-impl's FailureAnalyst produced a genuinely correct, specific
|
|
284
|
+
# note -- "When converting an interface to Record<string, unknown>, ensure
|
|
285
|
+
# the interface has an index signature or use 'unknown' first to avoid
|
|
286
|
+
# TS2352 error." -- but it opens with a subordinate "When X, ..." clause,
|
|
287
|
+
# not an imperative, so it correctly failed the deterministic format check
|
|
288
|
+
# and went through the full LLM reviewer 3 times on this SAME fixable
|
|
289
|
+
# wording issue, then was persisted UNREVIEWED as a fallback. The lesson
|
|
290
|
+
# itself was fine; only the opening word was wrong -- a mechanically
|
|
291
|
+
# fixable defect, not a judgment call. Prepending
|
|
292
|
+
# SKILL_NOTE_NORMALIZATION_OPENER (a configurable var, checked against the
|
|
293
|
+
# SAME SKILL_NOTE_IMPERATIVE_OPENERS list _skill_note_format_ok uses -- no
|
|
294
|
+
# word list duplicated or hardcoded independently in either function) is a
|
|
295
|
+
# generic, content-preserving transform (not a rewrite) that reliably
|
|
296
|
+
# satisfies the imperative check for ANY note shape, so apply it BEFORE the
|
|
297
|
+
# note is ever handed to run_change_with_reviewer_retry, letting an
|
|
298
|
+
# otherwise-sound note skip the LLM reviewer entirely on the very first
|
|
299
|
+
# attempt.
|
|
300
|
+
_ensure_imperative_opener() {
|
|
301
|
+
local note="$1"
|
|
302
|
+
[ -z "$note" ] && { printf ''; return 0; }
|
|
303
|
+
if echo "$note" | grep -Eiq "^(${SKILL_NOTE_IMPERATIVE_OPENERS})\\b"; then
|
|
304
|
+
printf '%s' "$note"
|
|
305
|
+
return 0
|
|
306
|
+
fi
|
|
307
|
+
# Sanity-check the configured normalization opener is itself an accepted
|
|
308
|
+
# word (not assumed) -- if misconfigured, fall through without
|
|
309
|
+
# normalizing rather than prepend something that wouldn't pass the check
|
|
310
|
+
# anyway.
|
|
311
|
+
if ! echo "$SKILL_NOTE_NORMALIZATION_OPENER" | grep -Eiq "^(${SKILL_NOTE_IMPERATIVE_OPENERS})\\b"; then
|
|
312
|
+
printf '%s' "$note"
|
|
313
|
+
return 0
|
|
314
|
+
fi
|
|
315
|
+
# NO TRUNCATION. This function PREPENDS an opener, which makes the string LONGER;
|
|
316
|
+
# cutting the tail to compensate destroys the END of the instruction — where the fix
|
|
317
|
+
# lives. Live 2026-08-11, AMSD-2041/gotransit: the analyst correctly diagnosed a Jest
|
|
318
|
+
# ESM failure and this line delivered "...change the pattern to
|
|
319
|
+
# '/node_modules/(?!swiper|@azure|uu" to the writer. Told to change a regex, never told
|
|
320
|
+
# to what. Eight attempts, three ladder rungs, the run lost, on a one-line config fix.
|
|
321
|
+
# A severed instruction is not shorter guidance, it is confidently wrong guidance.
|
|
322
|
+
printf '%s' "${SKILL_NOTE_NORMALIZATION_OPENER}: ${note}"
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
run_change_with_reviewer_retry() {
|
|
326
|
+
local story_id="$1"
|
|
327
|
+
local change_type="$2"
|
|
328
|
+
local before="$3"
|
|
329
|
+
local candidate="$4"
|
|
330
|
+
local max_retries="${5:-3}"
|
|
331
|
+
|
|
332
|
+
# Same subshell-scope issue as PRD_REVIEW_ISSUES: this whole function is also
|
|
333
|
+
# invoked via $(...) by its callers, so a plain REVIEWER_RETRY_TEXT=... here
|
|
334
|
+
# would never reach them. File-based side channel again; callers read it
|
|
335
|
+
# right after the command substitution (see kb)/skill) cases in
|
|
336
|
+
# run_failure_analyst).
|
|
337
|
+
local _issues_file="${TMPDIR:-/tmp}/.prd-review-issues-$$"
|
|
338
|
+
local _retry_text_file="${TMPDIR:-/tmp}/.reviewer-retry-text-$$"
|
|
339
|
+
|
|
340
|
+
if { [ "$change_type" = "skill_note" ] || [ "$change_type" = "kb_entry" ]; } \
|
|
341
|
+
&& _skill_note_format_ok "$candidate" "$story_id" "$before"; then
|
|
342
|
+
printf '%s' "$candidate" > "$_retry_text_file" 2>/dev/null || true
|
|
343
|
+
log " [PRD-Reviewer] Skipped LLM review for ${change_type} (${story_id}) -- deterministic format check passed" >&2
|
|
344
|
+
echo "pass"
|
|
345
|
+
return 0
|
|
346
|
+
fi
|
|
347
|
+
|
|
348
|
+
local attempt=1
|
|
349
|
+
local current="$candidate"
|
|
350
|
+
local verdict="" review_issues=""
|
|
351
|
+
while [ "$attempt" -le "$max_retries" ]; do
|
|
352
|
+
verdict=$(run_prd_change_reviewer "$story_id" "$change_type" "$before" "$current")
|
|
353
|
+
if _review_kept "$verdict"; then
|
|
354
|
+
printf '%s' "$current" > "$_retry_text_file" 2>/dev/null || true
|
|
355
|
+
echo "pass"
|
|
356
|
+
return 0
|
|
357
|
+
fi
|
|
358
|
+
# Not judged (no provider, no prompt): no rewrite can earn a review that cannot run.
|
|
359
|
+
if [ "$verdict" != "fail" ]; then
|
|
360
|
+
error " [PRD-Reviewer] ${change_type} for ${story_id} was NOT reviewed (verdict '${verdict:-none}') — not kept" >&2
|
|
361
|
+
printf '%s' "$current" > "$_retry_text_file" 2>/dev/null || true
|
|
362
|
+
echo "fail"
|
|
363
|
+
return 1
|
|
364
|
+
fi
|
|
365
|
+
review_issues=$(cat "$_issues_file" 2>/dev/null || echo "")
|
|
366
|
+
if [ "$attempt" -lt "$max_retries" ]; then
|
|
367
|
+
log " [PRD-Summarizer] Rewriting rejected ${change_type} for ${story_id} (attempt ${attempt}/${max_retries}): ${review_issues:-no details}" >&2
|
|
368
|
+
current=$(run_prd_change_summarizer "$story_id" "$change_type" "$review_issues" "$current")
|
|
369
|
+
fi
|
|
370
|
+
attempt=$((attempt + 1))
|
|
371
|
+
done
|
|
372
|
+
printf '%s' "$current" > "$_retry_text_file" 2>/dev/null || true
|
|
373
|
+
echo "fail"
|
|
374
|
+
return 1
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
# run_pre_phase_assessment: ONE definition, lib/phase-assessment.sh (B48; operator, 2026-10-22: "Fix critical"). The older copy
|
|
378
|
+
# that stood here loaded after it (gates-testing.sh → reviewed-acs.sh → this file) and was the one in effect at Step 3: it handed
|
|
379
|
+
# the agent tools and told it to edit the PRD and orchestrations/agents/profiles.json relative to its working folder — the client's
|
|
380
|
+
# codeline. The newer one returns a decision that assessment_apply.py applies, judged by the change reviewer.
|