amsd-pipeline-shared 0.5.0 → 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.
- package/docs/gateway-baseline-2.0.80.md5 +47 -0
- package/docs/gateway-files.txt +47 -0
- package/orchestrations/config/repo-artifacts.json +23 -0
- package/orchestrations/config/resume-preserves.json +25 -0
- package/orchestrations/config/run-modes.json +47 -0
- package/orchestrations/scripts/lib/checkpoints.sh +175 -0
- package/orchestrations/scripts/lib/codeline-lanes.sh +1664 -0
- package/orchestrations/scripts/lib/codeline-packages.js +67 -0
- package/orchestrations/scripts/lib/codeline-write-perimeter.sh +318 -0
- package/orchestrations/scripts/lib/common.sh +66 -0
- package/orchestrations/scripts/lib/control-plane.sh +506 -0
- package/orchestrations/scripts/lib/ecosystem-registry.js +148 -0
- package/orchestrations/scripts/lib/engine-paths.sh +52 -0
- package/orchestrations/scripts/lib/flags.sh +38 -0
- package/orchestrations/scripts/lib/git-ops.sh +975 -0
- package/orchestrations/scripts/lib/halt-recovery.sh +56 -0
- package/orchestrations/scripts/lib/handlers/_read-input.js +88 -0
- package/orchestrations/scripts/lib/handlers/carry-plan.js +25 -0
- package/orchestrations/scripts/lib/handlers/cl-entries.js +10 -0
- package/orchestrations/scripts/lib/handlers/codeline-ecosystem.js +164 -0
- package/orchestrations/scripts/lib/handlers/codeline-manifests.js +148 -0
- package/orchestrations/scripts/lib/handlers/filtered-prd.js +85 -0
- package/orchestrations/scripts/lib/handlers/merge-lane-into-canonical.js +39 -0
- package/orchestrations/scripts/lib/handlers/package-engines-node.js +25 -0
- package/orchestrations/scripts/lib/handlers/prd-phases.js +46 -0
- package/orchestrations/scripts/lib/handlers/rebuild-cross-contract.js +7 -0
- package/orchestrations/scripts/lib/handlers/record-declared-pass.js +23 -0
- package/orchestrations/scripts/lib/handlers/release-line-prd.js +31 -0
- package/orchestrations/scripts/lib/handlers/release-lines.js +31 -0
- package/orchestrations/scripts/lib/handlers/repo-exclude-patterns.js +108 -0
- package/orchestrations/scripts/lib/handlers/resume-spec-output-present.js +16 -0
- package/orchestrations/scripts/lib/handlers/sibling-providers.js +43 -0
- package/orchestrations/scripts/lib/handlers/spanning-stories-incomplete.js +50 -0
- package/orchestrations/scripts/lib/handlers/spec-inputs-digest.js +48 -0
- package/orchestrations/scripts/lib/handlers/spec-predates-plan.js +40 -0
- package/orchestrations/scripts/lib/handlers/stories-lacking-spec.js +64 -0
- package/orchestrations/scripts/lib/handlers/write-missing-plans.js +92 -0
- package/orchestrations/scripts/lib/orchestration-resume.sh +74 -0
- package/orchestrations/scripts/lib/phase-exit.sh +90 -0
- package/orchestrations/scripts/lib/pipeline-commit.sh +19 -0
- package/orchestrations/scripts/lib/plan-hash.js +19 -0
- package/orchestrations/scripts/lib/plan-scope.js +73 -0
- package/orchestrations/scripts/lib/qa-gate-evidence.sh +235 -0
- package/orchestrations/scripts/lib/release-lines.sh +166 -0
- package/orchestrations/scripts/lib/resume-semantics.sh +31 -0
- package/orchestrations/scripts/lib/run-checkpoint.sh +842 -0
- package/orchestrations/scripts/lib/run-modes.sh +93 -0
- package/orchestrations/scripts/lib/story-outputs.sh +322 -0
- package/orchestrations/scripts/lib/story-retry-state.sh +355 -0
- package/package.json +1 -1
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# NAMED RUN MODES. "Resume the writer" is one word, not six variables to remember.
|
|
3
|
+
#
|
|
4
|
+
# On 2026-08-13 an operator asked for a writer resume. It was launched with three environment
|
|
5
|
+
# variables and needed six, so the CPA pre-pass, the pre-phase skill assessment and the regression
|
|
6
|
+
# guard ran anyway — each a separate default-ACTIVE step, none tied to the stated intent. The
|
|
7
|
+
# operator noticed four minutes in.
|
|
8
|
+
#
|
|
9
|
+
# THIS FILE KNOWS NO MODES. It reads orchestrations/config/run-modes.json. Adding a mode is a
|
|
10
|
+
# config edit; the engine never learns a mode's name.
|
|
11
|
+
#
|
|
12
|
+
# TWO RULES:
|
|
13
|
+
# FAILS CLOSED — an unknown mode, or a declaration file that will not parse, is a hard error.
|
|
14
|
+
# Silently ignoring either runs every gate the operator believed they had turned off.
|
|
15
|
+
# PRECEDENCE IS operator environment > mode > config-file default. A mode must beat a default
|
|
16
|
+
# (config.env carries SKIP_REGRESSION_GUARD=false, which silently defeated writer-only) while
|
|
17
|
+
# still yielding to a deliberate choice on the launch command — writer-only WITH the regression
|
|
18
|
+
# baseline is a legitimate thing to ask for. The two are only distinguishable before the config
|
|
19
|
+
# files load, which is what snapshot_operator_env captures.
|
|
20
|
+
|
|
21
|
+
RUN_MODES_FILE="${RUN_MODES_FILE:-$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/../../config/run-modes.json}"
|
|
22
|
+
|
|
23
|
+
# snapshot_operator_env — record which variables the OPERATOR set, before any config file is read.
|
|
24
|
+
#
|
|
25
|
+
# A mode must override a config-file DEFAULT (config.env carries SKIP_REGRESSION_GUARD=false, which
|
|
26
|
+
# silently defeated writer-only on 2026-08-13) while still yielding to a deliberate choice on the
|
|
27
|
+
# launch command. Those two are indistinguishable once both are in the environment — unless you
|
|
28
|
+
# look BEFORE the config files load, which is the one moment only the operator's values exist.
|
|
29
|
+
#
|
|
30
|
+
# Launchers call this before their first load_env_file_safe. A caller that never calls it gets the
|
|
31
|
+
# mode applied in full, which is the safe failure: a half-applied intent is worse than none.
|
|
32
|
+
snapshot_operator_env() {
|
|
33
|
+
EPAM_OPERATOR_SET_VARS=" $(compgen -e 2>/dev/null | tr '\n' ' ')"
|
|
34
|
+
export EPAM_OPERATOR_SET_VARS
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
# run_mode_env <mode> — print the KEY=VALUE lines a mode declares. Non-zero if unknown.
|
|
38
|
+
run_mode_env() {
|
|
39
|
+
local _mode="${1:-}"
|
|
40
|
+
[ -n "$_mode" ] || return 0
|
|
41
|
+
"${NODE_BIN:-node}" -e '
|
|
42
|
+
const fs = require("fs");
|
|
43
|
+
const [file, mode] = process.argv.slice(1);
|
|
44
|
+
let doc;
|
|
45
|
+
try {
|
|
46
|
+
doc = JSON.parse(fs.readFileSync(file, "utf8"));
|
|
47
|
+
} catch (e) {
|
|
48
|
+
process.stderr.write(`run-modes: cannot read ${file}: ${e.message}\n`);
|
|
49
|
+
process.exit(2);
|
|
50
|
+
}
|
|
51
|
+
const modes = doc.modes || {};
|
|
52
|
+
if (!Object.prototype.hasOwnProperty.call(modes, mode)) {
|
|
53
|
+
process.stderr.write(`run-modes: unknown mode "${mode}". Declared: ${Object.keys(modes).join(", ")}\n`);
|
|
54
|
+
process.exit(3);
|
|
55
|
+
}
|
|
56
|
+
const skips = modes[mode].skips || [];
|
|
57
|
+
process.stdout.write(skips.join("\n") + (skips.length ? "\n" : ""));
|
|
58
|
+
' "$RUN_MODES_FILE" "$_mode"
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
# apply_run_mode <mode> — export what the mode declares, leaving anything already set alone.
|
|
62
|
+
apply_run_mode() {
|
|
63
|
+
local _mode="${1:-}"
|
|
64
|
+
[ -n "$_mode" ] || return 0
|
|
65
|
+
|
|
66
|
+
local _env
|
|
67
|
+
_env=$(run_mode_env "$_mode") || {
|
|
68
|
+
if command -v error >/dev/null 2>&1; then
|
|
69
|
+
error "[run-mode] refusing to run: mode '$_mode' is not declared in $RUN_MODES_FILE"
|
|
70
|
+
else
|
|
71
|
+
printf '[run-mode] refusing to run: mode %s is not declared in %s\n' "$_mode" "$RUN_MODES_FILE" >&2
|
|
72
|
+
fi
|
|
73
|
+
return 1
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
local _line _key _val _already
|
|
77
|
+
while IFS= read -r _line; do
|
|
78
|
+
[ -n "$_line" ] || continue
|
|
79
|
+
_key="${_line%%=*}"
|
|
80
|
+
_val="${_line#*=}"
|
|
81
|
+
# Set by the OPERATOR — before any config file was read — so it wins. A value that only
|
|
82
|
+
# appeared once config.env loaded is a project default, and the mode is more specific than
|
|
83
|
+
# a default: the operator named this intent on the command line.
|
|
84
|
+
case "${EPAM_OPERATOR_SET_VARS:-}" in
|
|
85
|
+
*" ${_key} "*) continue ;;
|
|
86
|
+
esac
|
|
87
|
+
eval "export ${_key}=\"\$_val\""
|
|
88
|
+
done <<< "$_env"
|
|
89
|
+
|
|
90
|
+
RESOLVED_RUN_MODE="$_mode"
|
|
91
|
+
export RESOLVED_RUN_MODE
|
|
92
|
+
return 0
|
|
93
|
+
}
|
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# story-outputs.sh — one shared answer to "what did this run actually produce?"
|
|
3
|
+
#
|
|
4
|
+
# Step 20's lint gate was fixed by handing it the writers' output instead of
|
|
5
|
+
# letting it rediscover scope by linting the whole tree (see
|
|
6
|
+
# lib/eslint-baseline-gate.sh). The reviewers still inferred it, each in its own
|
|
7
|
+
# slightly different way — team-lead-review.sh, review-ranger and mutant-hunter
|
|
8
|
+
# all reimplemented `git diff --name-only <baseline>..HEAD`. Three copies of one
|
|
9
|
+
# idea is how they drift, and they already had:
|
|
10
|
+
#
|
|
11
|
+
# * `<baseline>..HEAD` is commit-to-commit, so writer output not yet committed
|
|
12
|
+
# is invisible to review. The repro-test-writer commits separately from the
|
|
13
|
+
# impl agent, so this is a normal state, not an edge case.
|
|
14
|
+
# * All three fell back to `HEAD~1` when the baseline SHA file was missing —
|
|
15
|
+
# silently. That is the review-oracle failure shape: the diff covers an
|
|
16
|
+
# arbitrary previous commit, the reviewers examine nothing this run
|
|
17
|
+
# produced, and the gate reports success.
|
|
18
|
+
#
|
|
19
|
+
# The producer is record_story_outputs() in claude.sh. This is the reader.
|
|
20
|
+
# STORY_OUTPUTS_SOURCE is set to manifest | baseline diff | none so callers can
|
|
21
|
+
# report which one they got — a gate that quietly changes how it computes its
|
|
22
|
+
# own scope is indistinguishable from one that is broken.
|
|
23
|
+
|
|
24
|
+
# Paths the pipeline writes into a client repo. Never story output.
|
|
25
|
+
# Was a two-entry regex ('^(\.codegraph/|\.epam/)') that missed orchestrations/,
|
|
26
|
+
# .deepeval/ and .contracts/ — which is how orchestrations/agents/KB.md was recorded as
|
|
27
|
+
# upexpress's writer output on run 20260804T225443Z. One shared definition now.
|
|
28
|
+
# shellcheck source=engine-paths.sh
|
|
29
|
+
. "$(dirname "${BASH_SOURCE[0]}")/engine-paths.sh"
|
|
30
|
+
|
|
31
|
+
# Test-file conventions. Deliberately broad: mutant-hunter used to look only for
|
|
32
|
+
# `*.test.ts` and therefore found nothing on the live metrolinx codeline, whose
|
|
33
|
+
# tests are all `.spec.ts` — it reported "(no test files found)" on a run that
|
|
34
|
+
# had just written a reproducing spec.
|
|
35
|
+
_STORY_OUTPUTS_TEST_RE='(\.|_)(spec|test)\.[A-Za-z0-9]+$|/__tests__/|(^|/)test_[^/]+$'
|
|
36
|
+
|
|
37
|
+
# story_outputs_record <project_root> <log_dir>
|
|
38
|
+
# Appends what this producer just put in the tree to the phase manifest.
|
|
39
|
+
#
|
|
40
|
+
# Shared because there is more than one producer and they finish at different
|
|
41
|
+
# times. Live metrolinx 2026-07-26: only the story loop recorded, at deliverable
|
|
42
|
+
# verification — but the repro-test-writer commits its .spec.ts AFTERWARDS, so
|
|
43
|
+
# the test never entered the manifest. mutant-hunter, freshly rewired to read
|
|
44
|
+
# its tests from that manifest, saw none, every mutant survived, it scored 0 and
|
|
45
|
+
# failed the gate — on a run whose test the repro gate had just proven fails on
|
|
46
|
+
# baseline and passes with the fix. The gate then ran its remediation pipeline
|
|
47
|
+
# against a finding that existed only because of the missing entry.
|
|
48
|
+
#
|
|
49
|
+
# Additive and idempotent: each producer records its own contribution and the
|
|
50
|
+
# list is the union. Writes nothing when there is no baseline to diff against
|
|
51
|
+
# (greenfield) — an ABSENT manifest means "fall back and say so", while an EMPTY
|
|
52
|
+
# one would assert the writers produced nothing. Never fails the caller.
|
|
53
|
+
story_outputs_record() {
|
|
54
|
+
local project_root="$1"
|
|
55
|
+
local log_dir="$2"
|
|
56
|
+
[ -n "$log_dir" ] || return 0
|
|
57
|
+
[ -n "$project_root" ] && [ -e "$project_root/.git" ] || return 0
|
|
58
|
+
|
|
59
|
+
# THE BASELINE SHA IS THE LANE'S IDENTITY. It resolves only in that lane's own repo
|
|
60
|
+
# (verified against all three live metrolinx lanes), so requiring it makes git's object
|
|
61
|
+
# database the identity oracle — no path comparison, no configuration.
|
|
62
|
+
#
|
|
63
|
+
# The `origin/${JIRA_BASELINE_BRANCH:-develop}` fallback this replaces is the silent
|
|
64
|
+
# wrong-scope shape this file's own header complains about: any repo that happens to
|
|
65
|
+
# have that ref satisfies it, including the engine's own.
|
|
66
|
+
local _ref
|
|
67
|
+
_ref=$(story_outputs_baseline_ref "$log_dir") || return 0
|
|
68
|
+
[ -n "$_ref" ] || return 0
|
|
69
|
+
git -C "$project_root" rev-parse --verify --quiet "${_ref}^{commit}" >/dev/null 2>&1 || return 0
|
|
70
|
+
|
|
71
|
+
local _manifest="$log_dir/story-outputs-${PHASE:-core}.txt"
|
|
72
|
+
# --diff-filter=ACMRT: Added/Copied/Modified/Renamed/Typechanged. DELETED paths are
|
|
73
|
+
# excluded deliberately — a path handed to a reviewer as "writer output" must exist, or
|
|
74
|
+
# story_outputs_tests nominates a test that cannot run. Deletions are reported
|
|
75
|
+
# separately by story_outputs_deleted, because a writer removing 10 tests is exactly
|
|
76
|
+
# what a gate needs to see and the old manifest lost it entirely.
|
|
77
|
+
local _produced
|
|
78
|
+
_produced=$( { git -C "$project_root" diff --name-only --diff-filter=ACMRT "$_ref" 2>/dev/null
|
|
79
|
+
git -C "$project_root" ls-files --others --exclude-standard 2>/dev/null; } | \
|
|
80
|
+
engine_paths_filter | grep -v '^$' | sort -u )
|
|
81
|
+
|
|
82
|
+
# OVERWRITE, never union. The previous form was
|
|
83
|
+
# { cat "$_manifest"; printf '%s' "$_produced"; } | sort -u
|
|
84
|
+
# which can only ever grow: a file the writer reverted, or one recorded when a wrong
|
|
85
|
+
# root was passed, stayed listed forever with no provenance to say when it entered.
|
|
86
|
+
# The union was also unnecessary — the diff is against the PHASE BASELINE, so work a
|
|
87
|
+
# LATE producer committed (the repro-test-writer commits its spec after the story loop)
|
|
88
|
+
# is already included by recomputing.
|
|
89
|
+
#
|
|
90
|
+
# Written even when empty: with a valid baseline, "nothing was produced" is a FACT and
|
|
91
|
+
# the manifest should say so. ABSENT still means "no baseline — fall back and say so".
|
|
92
|
+
printf '%s\n' "$_produced" | grep -v '^$' > "${_manifest}.tmp" 2>/dev/null || true
|
|
93
|
+
mv "${_manifest}.tmp" "$_manifest" 2>/dev/null || rm -f "${_manifest}.tmp"
|
|
94
|
+
return 0
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
# story_outputs_baseline_ref <log_dir> — the phase baseline SHA, or non-zero if unknown.
|
|
98
|
+
story_outputs_baseline_ref() {
|
|
99
|
+
local _log_dir="$1"
|
|
100
|
+
[ -f "$_log_dir/phase-baseline-sha.txt" ] || return 1
|
|
101
|
+
local _sha
|
|
102
|
+
_sha=$(tr -d '[:space:]' < "$_log_dir/phase-baseline-sha.txt" 2>/dev/null)
|
|
103
|
+
[ -n "$_sha" ] || return 1
|
|
104
|
+
printf '%s' "$_sha"
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
# story_outputs_deleted <project_root> <log_dir> — paths this phase REMOVED.
|
|
108
|
+
#
|
|
109
|
+
# The reviewer on run 20260804T225443Z reported the writer had deleted a 179-line test file
|
|
110
|
+
# containing 10 tests, with no replacement. No manifest showed it: the producer recorded
|
|
111
|
+
# deletions as though they were outputs, and every consumer then filtered them out as
|
|
112
|
+
# missing files. Test count going DOWN is a gate-relevant fact and now has a channel.
|
|
113
|
+
story_outputs_deleted() {
|
|
114
|
+
local project_root="$1"
|
|
115
|
+
local log_dir="$2"
|
|
116
|
+
[ -n "$project_root" ] && [ -e "$project_root/.git" ] || return 0
|
|
117
|
+
local _ref
|
|
118
|
+
_ref=$(story_outputs_baseline_ref "$log_dir") || return 0
|
|
119
|
+
git -C "$project_root" rev-parse --verify --quiet "${_ref}^{commit}" >/dev/null 2>&1 || return 0
|
|
120
|
+
git -C "$project_root" diff --name-only --diff-filter=D "$_ref" 2>/dev/null | \
|
|
121
|
+
engine_paths_filter | grep -v '^$' | sort -u
|
|
122
|
+
return 0
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
# story_outputs_files <project_root> <log_dir>
|
|
126
|
+
# Prints repo-relative paths, one per line. Sets STORY_OUTPUTS_SOURCE.
|
|
127
|
+
story_outputs_files() {
|
|
128
|
+
local project_root="$1"
|
|
129
|
+
local log_dir="$2"
|
|
130
|
+
STORY_OUTPUTS_SOURCE="none"
|
|
131
|
+
|
|
132
|
+
[ -n "$project_root" ] && [ -e "$project_root/.git" ] || return 0
|
|
133
|
+
|
|
134
|
+
local manifest="$log_dir/story-outputs-${PHASE:-core}.txt"
|
|
135
|
+
local raw=""
|
|
136
|
+
# -f, not -s: an EMPTY manifest is a real answer ("the writers produced nothing"),
|
|
137
|
+
# distinct from an ABSENT one ("no baseline — fall back and say so").
|
|
138
|
+
if [ -f "$manifest" ]; then
|
|
139
|
+
STORY_OUTPUTS_SOURCE="manifest"
|
|
140
|
+
raw=$(cat "$manifest" 2>/dev/null)
|
|
141
|
+
else
|
|
142
|
+
local baseline=""
|
|
143
|
+
if [ -f "$log_dir/phase-baseline-sha.txt" ]; then
|
|
144
|
+
baseline=$(tr -d '[:space:]' < "$log_dir/phase-baseline-sha.txt" 2>/dev/null)
|
|
145
|
+
fi
|
|
146
|
+
if [ -z "$baseline" ]; then
|
|
147
|
+
# No manifest and no baseline. Previously every caller substituted
|
|
148
|
+
# HEAD~1 here and reviewed an arbitrary commit. Returning an empty
|
|
149
|
+
# scope lets the caller say "nothing to review" truthfully instead.
|
|
150
|
+
return 0
|
|
151
|
+
fi
|
|
152
|
+
# TO STDERR: this function's stdout IS the scope a caller captures; a warning there was counted as a changed
|
|
153
|
+
# file and sent an unchanged story to review (rehearsal 2026-10-08).
|
|
154
|
+
command -v warning >/dev/null 2>&1 && \
|
|
155
|
+
warning " no writer-output manifest at $manifest — falling back to the baseline diff for scope" >&2
|
|
156
|
+
export STORY_OUTPUTS_SOURCE="baseline diff"
|
|
157
|
+
raw=$( { git -C "$project_root" diff --name-only --diff-filter=ACMRT "$baseline" 2>/dev/null
|
|
158
|
+
git -C "$project_root" ls-files --others --exclude-standard 2>/dev/null; } )
|
|
159
|
+
fi
|
|
160
|
+
|
|
161
|
+
printf '%s\n' "$raw" | grep -v '^$' | engine_paths_filter | sort -u
|
|
162
|
+
return 0
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
# story_outputs_tests <project_root> <log_dir> — test files only
|
|
166
|
+
story_outputs_tests() {
|
|
167
|
+
story_outputs_files "$1" "$2" | grep -E "$_STORY_OUTPUTS_TEST_RE" || true
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
# story_outputs_tests_for <project_root> <log_dir> <story_id> — THIS story's test files.
|
|
171
|
+
#
|
|
172
|
+
# story_outputs_tests reads the PHASE manifest, which carries no story attribution at all. So a
|
|
173
|
+
# caller looping over stories and taking `| head -1` gave every story in the phase the SAME test
|
|
174
|
+
# file — the first one written by anybody. Step 3.56 did exactly that, and reported story B's
|
|
175
|
+
# verification-criteria coverage against story A's test: a check that ran, produced a plausible
|
|
176
|
+
# artefact, and measured the wrong thing.
|
|
177
|
+
#
|
|
178
|
+
# The story's own commit is the attribution that does exist. commit_completed_story writes
|
|
179
|
+
# "<story_id>: story complete (N file(s))" and keeps that marker stable precisely so it can be
|
|
180
|
+
# grepped — reset-to-baseline.sh already keys off it.
|
|
181
|
+
#
|
|
182
|
+
# Prints nothing when the story has no commit. That is a real answer: no commit means no files,
|
|
183
|
+
# which the caller must report as "not checked", never as "covered".
|
|
184
|
+
story_outputs_tests_for() {
|
|
185
|
+
local project_root="$1" story_id="$3"
|
|
186
|
+
[ -n "$project_root" ] && [ -e "$project_root/.git" ] || return 0
|
|
187
|
+
[ -n "$story_id" ] || return 0
|
|
188
|
+
# EVERY COMMIT OF THE STORY, not the one that says "story complete".
|
|
189
|
+
#
|
|
190
|
+
# The writer no longer produces a single commit: the fix and the bug-reproducing test land
|
|
191
|
+
# separately, and the fix's subject is free-form. Measured 2026-08-28 on the first fully green
|
|
192
|
+
# paid run — MOCK3-1's fix was "Fix: correct fare boundary for riders aged exactly 65" with no
|
|
193
|
+
# marker, and MOCK3-2's marker commit carried only its source. Both stories reported "no test
|
|
194
|
+
# file in the writer manifest — coverage NOT checked" while their tests sat on the branch. The
|
|
195
|
+
# gate ran, warned, and measured nothing, on a run reported green.
|
|
196
|
+
#
|
|
197
|
+
# The story-id PREFIX is the attribution the commit convention already requires, and it keeps
|
|
198
|
+
# the property this function exists for: one story never sees another's test.
|
|
199
|
+
#
|
|
200
|
+
# Scoped to the phase baseline where one is recorded, so a previous run's commit for the same
|
|
201
|
+
# story id cannot be mistaken for this run's work.
|
|
202
|
+
local _range=""
|
|
203
|
+
if [ -n "${2:-}" ] && [ -f "$2/phase-baseline-sha.txt" ]; then
|
|
204
|
+
local _base; _base=$(tr -d '[:space:]' < "$2/phase-baseline-sha.txt" 2>/dev/null)
|
|
205
|
+
[ -n "$_base" ] && git -C "$project_root" rev-parse --verify --quiet "$_base" >/dev/null 2>&1 \
|
|
206
|
+
&& _range="${_base}..HEAD"
|
|
207
|
+
fi
|
|
208
|
+
git -C "$project_root" log ${_range:+"$_range"} --grep="^${story_id}:" \
|
|
209
|
+
--pretty=format: --name-only 2>/dev/null \
|
|
210
|
+
| grep -E "$_STORY_OUTPUTS_TEST_RE" 2>/dev/null | sort -u || true
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
# story_resume_completed_by_commit <project_root> <lane_log_dir> <lane_prd>
|
|
214
|
+
#
|
|
215
|
+
# A RESUME DOES NOT REWRITE COMMITTED WORK. A lane resumes from its pre-writer checkpoint, whose PRD predates the
|
|
216
|
+
# writer, so every story reads "not completed" and the writer is invoked again — AMSD-2974 run 9, resume 3: "I didn't
|
|
217
|
+
# need to change any code this turn: the earlier commit d429022 … already contains all three fixes", a paid call that
|
|
218
|
+
# changed nothing, then a full review. A story whose work is committed in THIS run (the "<story>:" convention, scoped
|
|
219
|
+
# to the lane's phase baseline) with no review rejection NEWER than that commit is marked completed in the lane PRD:
|
|
220
|
+
# the writer skips it and the review judges the committed change. A newer rejection leaves it for the writer.
|
|
221
|
+
story_resume_completed_by_commit() {
|
|
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
|
|
225
|
+
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
|
+
local _id _ct _fb _fbt
|
|
228
|
+
for _id in $(jq -r '.stories[]? | select(.completed != true) | .id' "$prd" 2>/dev/null); do
|
|
229
|
+
_ct=$(git -C "$project_root" log "${_base}..HEAD" --grep="^${_id}:" -1 --format=%ct 2>/dev/null)
|
|
230
|
+
[ -n "$_ct" ] || continue
|
|
231
|
+
_fb="$log_dir/review-feedback-${_id}.json"
|
|
232
|
+
if [ -f "$_fb" ]; then
|
|
233
|
+
_fbt=$(stat -c %Y "$_fb" 2>/dev/null || echo 0)
|
|
234
|
+
[ "${_fbt:-0}" -gt "$_ct" ] && continue
|
|
235
|
+
fi
|
|
236
|
+
local _tmp; _tmp=$(mktemp)
|
|
237
|
+
jq --arg id "$_id" '(.stories[] | select(.id == $id)) |= (. + {completed: true})' "$prd" > "$_tmp" \
|
|
238
|
+
&& 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"
|
|
240
|
+
done
|
|
241
|
+
return 0
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
# story_outputs_sources <project_root> <log_dir> — everything that is not a test
|
|
245
|
+
story_outputs_sources() {
|
|
246
|
+
story_outputs_files "$1" "$2" | grep -v -E "$_STORY_OUTPUTS_TEST_RE" || true
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
# ── THE RUNG THAT PRODUCED IT ────────────────────────────────────────────────
|
|
250
|
+
#
|
|
251
|
+
# Operator, 2026-08-22: "if writer's model moves up ladder, the reviewer must follow along and
|
|
252
|
+
# use the same rung EXACT setup the writer used to converge. No hard coding. No guards.
|
|
253
|
+
# Persistence."
|
|
254
|
+
#
|
|
255
|
+
# A RUNG IS NOT A MODEL. The writer's ladder moves model, provider, reasoning effort and
|
|
256
|
+
# temperature together — rung 1 sets effort medium / temp 0, rung 2 escalates the model, rung 3
|
|
257
|
+
# goes effort high / temp 0.7 — so a reviewer handed only the model still judges converged work
|
|
258
|
+
# on a configuration the writer never ran.
|
|
259
|
+
#
|
|
260
|
+
# WRITTEN ON EVERY ATTEMPT, at the point the rung is settled and before the writer is invoked
|
|
261
|
+
# (claude.sh, immediately after the escalation block closes). Deliberately not on a success
|
|
262
|
+
# path: the last write is by definition the rung that produced the final output, whatever
|
|
263
|
+
# happened on the way there, and a story that ends in rejection still has a real rung.
|
|
264
|
+
#
|
|
265
|
+
# Why persisted rather than passed in memory: run-agent-orchestration.sh spawns the writer as a
|
|
266
|
+
# CHILD PROCESS per story (:2333) and the reviewer as a separate process per cycle (:7909). No
|
|
267
|
+
# in-memory value crosses that. The reviewer's own ladder resume already works this way, for
|
|
268
|
+
# this same reason.
|
|
269
|
+
_story_rung_file() { echo "$1/story-rung/$2.json"; }
|
|
270
|
+
|
|
271
|
+
# story_rung_record <log_dir> <story_id>
|
|
272
|
+
# Captures the CURRENT rung from the writer's own variables. No arguments, on purpose: a
|
|
273
|
+
# parameter list is a second definition of what a rung is, and it drifts the first time
|
|
274
|
+
# claude.sh adds a knob to the ladder.
|
|
275
|
+
story_rung_record() {
|
|
276
|
+
local log_dir="$1" story_id="$2"
|
|
277
|
+
[ -n "$log_dir" ] && [ -n "$story_id" ] || return 0
|
|
278
|
+
mkdir -p "$log_dir/story-rung" 2>/dev/null || true
|
|
279
|
+
jq -n \
|
|
280
|
+
--arg model "${STORY_MODEL:-}" \
|
|
281
|
+
--arg provider "${STORY_PROVIDER:-}" \
|
|
282
|
+
--arg effort "${EPAM_REASONING_EFFORT:-}" \
|
|
283
|
+
--arg temperature "${EPAM_TEMPERATURE:-}" \
|
|
284
|
+
--arg iterations "${STORY_MAX_ITERATIONS:-}" \
|
|
285
|
+
--arg outTokens "${STORY_MAX_OUTPUT_TOKENS:-}" \
|
|
286
|
+
'{model:$model, provider:$provider, reasoningEffort:$effort,
|
|
287
|
+
temperature:$temperature, maxIterations:$iterations, maxOutputTokens:$outTokens}' \
|
|
288
|
+
> "$(_story_rung_file "$log_dir" "$story_id")" 2>/dev/null || true
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
# story_rung_get <log_dir> <story_id> <field>
|
|
292
|
+
# Empty when the story has no rung on record. ABSENT IS NOT A DEFAULT: a caller that substitutes
|
|
293
|
+
# something here makes a judge authoritative about work whose setup it never saw.
|
|
294
|
+
# A RUNG RECORD THAT IS GONE IS DERIVED FROM THE RUN'S OWN LEDGER — never guessed, never written by
|
|
295
|
+
# hand. regintel resume 3 (2026-09-21): the reset had wiped story-rung/ and the reviewer refused a
|
|
296
|
+
# story the run itself had completed. The cost ledger holds the completed attempt's resolvedModel
|
|
297
|
+
# and effort; the activity ledger the provider the story started on. What is derived is persisted,
|
|
298
|
+
# marked derivedFrom, so the next reader finds a record. A story the ledger never completed derives
|
|
299
|
+
# nothing, and the reviewer's refusal stands.
|
|
300
|
+
_story_rung_derive() {
|
|
301
|
+
local log_dir="$1" story_id="$2" f cost act model effort provider
|
|
302
|
+
cost="$log_dir/phase-cost.jsonl"; act="$log_dir/agent-activity.jsonl"
|
|
303
|
+
[ -f "$cost" ] || return 0
|
|
304
|
+
model=$(jq -r --arg s "$story_id" 'select(.story_id == $s and .status == "completed") | .resolvedModel // ""' "$cost" 2>/dev/null | grep -v '^$' | tail -1)
|
|
305
|
+
[ -n "$model" ] || return 0
|
|
306
|
+
effort=$(jq -r --arg s "$story_id" 'select(.story_id == $s and .status == "completed") | .effort // ""' "$cost" 2>/dev/null | tail -1)
|
|
307
|
+
provider=""
|
|
308
|
+
[ -f "$act" ] && provider=$(jq -r --arg s "$story_id" 'select((.storyId // .story_id) == $s and .provider != null) | .provider' "$act" 2>/dev/null | grep -v '^$' | tail -1)
|
|
309
|
+
f="$(_story_rung_file "$log_dir" "$story_id")"
|
|
310
|
+
mkdir -p "$(dirname "$f")" 2>/dev/null || true
|
|
311
|
+
jq -n --arg model "$model" --arg provider "$provider" --arg effort "$effort" \
|
|
312
|
+
'{model:$model, provider:$provider, reasoningEffort:$effort, temperature:"", maxIterations:"", maxOutputTokens:"",
|
|
313
|
+
derivedFrom:"ledger: phase-cost.jsonl (completed attempt) + agent-activity.jsonl (provider)"}' > "$f" 2>/dev/null || true
|
|
314
|
+
echo "[story-rung] $story_id: no record on disk — rung derived from this run's ledger (model=$model provider=${provider:-?})" >&2
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
story_rung_get() {
|
|
318
|
+
local f; f="$(_story_rung_file "$1" "$2")"
|
|
319
|
+
[ -f "$f" ] || _story_rung_derive "$1" "$2"
|
|
320
|
+
[ -f "$f" ] || return 0
|
|
321
|
+
jq -r --arg k "$3" '.[$k] // ""' "$f" 2>/dev/null || true
|
|
322
|
+
}
|