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.
Files changed (50) hide show
  1. package/docs/gateway-baseline-2.0.80.md5 +47 -0
  2. package/docs/gateway-files.txt +47 -0
  3. package/orchestrations/config/repo-artifacts.json +23 -0
  4. package/orchestrations/config/resume-preserves.json +25 -0
  5. package/orchestrations/config/run-modes.json +47 -0
  6. package/orchestrations/scripts/lib/checkpoints.sh +175 -0
  7. package/orchestrations/scripts/lib/codeline-lanes.sh +1664 -0
  8. package/orchestrations/scripts/lib/codeline-packages.js +67 -0
  9. package/orchestrations/scripts/lib/codeline-write-perimeter.sh +318 -0
  10. package/orchestrations/scripts/lib/common.sh +66 -0
  11. package/orchestrations/scripts/lib/control-plane.sh +506 -0
  12. package/orchestrations/scripts/lib/ecosystem-registry.js +148 -0
  13. package/orchestrations/scripts/lib/engine-paths.sh +52 -0
  14. package/orchestrations/scripts/lib/flags.sh +38 -0
  15. package/orchestrations/scripts/lib/git-ops.sh +975 -0
  16. package/orchestrations/scripts/lib/halt-recovery.sh +56 -0
  17. package/orchestrations/scripts/lib/handlers/_read-input.js +88 -0
  18. package/orchestrations/scripts/lib/handlers/carry-plan.js +25 -0
  19. package/orchestrations/scripts/lib/handlers/cl-entries.js +10 -0
  20. package/orchestrations/scripts/lib/handlers/codeline-ecosystem.js +164 -0
  21. package/orchestrations/scripts/lib/handlers/codeline-manifests.js +148 -0
  22. package/orchestrations/scripts/lib/handlers/filtered-prd.js +85 -0
  23. package/orchestrations/scripts/lib/handlers/merge-lane-into-canonical.js +39 -0
  24. package/orchestrations/scripts/lib/handlers/package-engines-node.js +25 -0
  25. package/orchestrations/scripts/lib/handlers/prd-phases.js +46 -0
  26. package/orchestrations/scripts/lib/handlers/rebuild-cross-contract.js +7 -0
  27. package/orchestrations/scripts/lib/handlers/record-declared-pass.js +23 -0
  28. package/orchestrations/scripts/lib/handlers/release-line-prd.js +31 -0
  29. package/orchestrations/scripts/lib/handlers/release-lines.js +31 -0
  30. package/orchestrations/scripts/lib/handlers/repo-exclude-patterns.js +108 -0
  31. package/orchestrations/scripts/lib/handlers/resume-spec-output-present.js +16 -0
  32. package/orchestrations/scripts/lib/handlers/sibling-providers.js +43 -0
  33. package/orchestrations/scripts/lib/handlers/spanning-stories-incomplete.js +50 -0
  34. package/orchestrations/scripts/lib/handlers/spec-inputs-digest.js +48 -0
  35. package/orchestrations/scripts/lib/handlers/spec-predates-plan.js +40 -0
  36. package/orchestrations/scripts/lib/handlers/stories-lacking-spec.js +64 -0
  37. package/orchestrations/scripts/lib/handlers/write-missing-plans.js +92 -0
  38. package/orchestrations/scripts/lib/orchestration-resume.sh +74 -0
  39. package/orchestrations/scripts/lib/phase-exit.sh +90 -0
  40. package/orchestrations/scripts/lib/pipeline-commit.sh +19 -0
  41. package/orchestrations/scripts/lib/plan-hash.js +19 -0
  42. package/orchestrations/scripts/lib/plan-scope.js +73 -0
  43. package/orchestrations/scripts/lib/qa-gate-evidence.sh +235 -0
  44. package/orchestrations/scripts/lib/release-lines.sh +166 -0
  45. package/orchestrations/scripts/lib/resume-semantics.sh +31 -0
  46. package/orchestrations/scripts/lib/run-checkpoint.sh +842 -0
  47. package/orchestrations/scripts/lib/run-modes.sh +93 -0
  48. package/orchestrations/scripts/lib/story-outputs.sh +322 -0
  49. package/orchestrations/scripts/lib/story-retry-state.sh +355 -0
  50. package/package.json +1 -1
@@ -0,0 +1,67 @@
1
+ 'use strict';
2
+ /**
3
+ * WHAT EACH CODELINE OF A STORY PROVIDES, AND WHICH RANGE EACH DECLARES FOR WHAT ITS SIBLINGS PROVIDE.
4
+ *
5
+ * Live 2026-10-02, AMSD-2974: whether a site's declared range takes a shared library's change was
6
+ * left to the library lane's detective choosing to report the version; twice it did not, and one
7
+ * site then assumed its ^7 range would take an 8.x change. These are facts on disk. Each manifest is
8
+ * read by the ecosystem provider that owns it (ecosystems/*.js, through lib/ecosystem-registry.js):
9
+ * describe() for name and version, depRanges() for declared ranges. A codeline whose folder or
10
+ * manifest cannot be read is reported with nothing known, never guessed. What the facts MEAN — does
11
+ * this range take that change — is the detective's judgement, not this module's.
12
+ */
13
+ const fs = require('fs');
14
+ const path = require('path');
15
+ const { allManifests } = require('./ecosystem-registry.js');
16
+
17
+ function readManifest(root) {
18
+ if (!root) return null;
19
+ for (const eco of allManifests()) {
20
+ const file = path.join(root, eco.file);
21
+ if (!fs.existsSync(file)) continue;
22
+ try { return { eco, text: fs.readFileSync(file, 'utf8') }; } catch { return null; }
23
+ }
24
+ return null;
25
+ }
26
+
27
+ /**
28
+ * @param {object} prd the lane's PRD (project.outputDirs: [{codeline, path}])
29
+ * @param {string[]} codelines the story's codelines
30
+ * @returns {Array<{codeline, provides: {name, version}|null, declares: object}>}
31
+ */
32
+ function codelinePackages(prd, codelines) {
33
+ const dirs = (prd && prd.project && Array.isArray(prd.project.outputDirs)) ? prd.project.outputDirs : [];
34
+ const rootOf = (cl) => (dirs.find((d) => d && d.codeline === cl) || {}).path || '';
35
+ const read = (codelines || []).map((cl) => {
36
+ const m = readManifest(rootOf(cl));
37
+ let provides = null;
38
+ let ranges = {};
39
+ if (m) {
40
+ try {
41
+ const d = typeof m.eco.describe === 'function' ? m.eco.describe(m.text) : null;
42
+ if (d && d.packageName) provides = { name: d.packageName, version: d.version || '' };
43
+ } catch { provides = null; }
44
+ try { ranges = (typeof m.eco.depRanges === 'function' && m.eco.depRanges(m.text)) || {}; } catch { ranges = {}; }
45
+ }
46
+ // ITS RELEASES, from its own history: a consumer locked to an older line receives a change only if
47
+ // the provider ships it there too, and the base of that line is the newest release on it (2026-10-06).
48
+ let releases = [];
49
+ if (provides) {
50
+ try {
51
+ releases = require('child_process').execFileSync('git', ['-C', rootOf(cl), 'tag', '--sort=-v:refname'], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] })
52
+ .split('\n').map((t) => t.trim()).filter(Boolean);
53
+ } catch { releases = []; }
54
+ }
55
+ return { codeline: cl, provides, ranges, releases };
56
+ });
57
+ return read.map((r) => {
58
+ const declares = {};
59
+ for (const o of read) {
60
+ if (!o.provides) continue;
61
+ if (Object.prototype.hasOwnProperty.call(r.ranges, o.provides.name)) declares[o.provides.name] = String(r.ranges[o.provides.name]);
62
+ }
63
+ return { codeline: r.codeline, provides: r.provides, declares, releases: r.releases };
64
+ });
65
+ }
66
+
67
+ module.exports = { codelinePackages };
@@ -0,0 +1,318 @@
1
+ #!/usr/bin/env bash
2
+ # codeline-write-perimeter.sh — OS-level write perimeter for client codelines.
3
+ #
4
+ # THE HOLE THIS CLOSES
5
+ # --------------------
6
+ # WriteFile.ts has a scope guard that blocks writes outside a story's declared
7
+ # files. Bash.ts has nothing: no cwd restriction, no command filtering, no deny
8
+ # list. It takes an arbitrary cwd and runs anything. Six agents hold `bash`
9
+ # against the client repo — code-graph-detective, team-lead-review,
10
+ # failure-analyst, prd-change-reviewer, plan-reviewer, lint-fix — and
11
+ # AI_GATE_ALLOW_TOOLS=1 also sets EPAM_DANGEROUS_SKIP_APPROVAL=1, so nothing
12
+ # prompts. Any of them can overwrite client source, and one did: run
13
+ # 20260806T113101Z lost ~1050 lines across five files during the spec pass,
14
+ # before the writer had run at all.
15
+ #
16
+ # A tool-level fix cannot hold — it has to be re-applied to every tool anyone
17
+ # adds. This enforces at the filesystem, where no tool can reach around it.
18
+ # Same reasoning, and the same mechanism, as _vendor_lock() in claude.sh, whose
19
+ # own comment says it exists because "an agent can reach for Bash and bypass it".
20
+ #
21
+ # THE RULE
22
+ # baseline branch in the main checkout -> READ-ONLY. Agents never write here.
23
+ # a git worktree, or a story branch -> writable. Edits land here.
24
+ # .git -> always writable (commits, resets,
25
+ # git ls-files, index updates).
26
+ #
27
+ # Locking is decided by asking git where we are, not by a path list — so a new
28
+ # codeline is covered the moment discovery finds it, with no configuration.
29
+ #
30
+ # Usage:
31
+ # . lib/codeline-write-perimeter.sh
32
+ # perimeter_apply <repo> # lock iff repo is on the baseline branch
33
+ # perimeter_seal <repo> # RUN START: lock whatever branch it is on
34
+ # perimeter_unlock <repo> # reopen (writer step, teardown, reset)
35
+ # perimeter_is_write_allowed <repo> # exit 0 if writes are permitted here
36
+
37
+ # NO `set -uo pipefail` here. This file is SOURCED, so any shell option it sets
38
+ # leaks into every caller — git-ops.sh is itself sourced by claude.sh,
39
+ # run-agent-orchestration.sh and codemie-claude.sh, so a stray `set -u` here
40
+ # changes the behaviour of three top-level scripts and broke a commit path the
41
+ # first time this was written. A library configures nothing about its host.
42
+
43
+ _perim_log() { echo "[write-perimeter] $*"; }
44
+
45
+ # The branch this project treats as its integration line. Configured per project
46
+ # (JIRA_BASELINE_BRANCH); no branch name is hardcoded here.
47
+ _perimeter_baseline_branch() {
48
+ echo "${JIRA_BASELINE_BRANCH:-${EPAM_BASELINE_BRANCH:-main}}"
49
+ }
50
+
51
+ # A linked worktree has .git as a FILE (a gitdir pointer); a main checkout has
52
+ # it as a directory. That is git's own distinction, so it needs no path list.
53
+ perimeter_is_worktree() {
54
+ local repo="${1:-}"
55
+ [ -n "$repo" ] && [ -f "$repo/.git" ]
56
+ }
57
+
58
+ # Writes are allowed in a worktree, or on any branch that is not the baseline.
59
+ # Detached HEAD counts as "not a story branch" and stays locked — nothing should
60
+ # be authored there.
61
+ perimeter_is_write_allowed() {
62
+ local repo="${1:-}"
63
+ [ -n "$repo" ] && [ -e "$repo/.git" ] || return 0 # not a repo: not ours to lock
64
+ perimeter_is_worktree "$repo" && return 0
65
+ local branch
66
+ branch="$(git -C "$repo" rev-parse --abbrev-ref HEAD 2>/dev/null || echo HEAD)"
67
+ [ "$branch" = "HEAD" ] && return 1 # detached: locked
68
+ [ "$branch" = "$(_perimeter_baseline_branch)" ] && return 1
69
+ return 0
70
+ }
71
+
72
+ # WHAT GETS LOCKED: the client's own tracked source, and nothing else.
73
+ #
74
+ # `git ls-files` is the source of truth, so this assumes nothing about the
75
+ # project's language, build directory or vendor convention — and it already
76
+ # excludes .git, node_modules, .venv, dist and anything else the repo ignores,
77
+ # because none of those are tracked.
78
+ #
79
+ # ENGINE-INTERNAL PATHS ARE NEVER LOCKED. The pipeline writes its own state
80
+ # inside the codeline while a run is in flight — .epam/ (settings.json,
81
+ # codeline-facts.json, dependency-check.json, the vendor-lock and
82
+ # write-perimeter markers) and .codegraph/ (the index, rebuilt on commit).
83
+ # Those are engine artefacts, not client source; locking them would break the
84
+ # run rather than protect anything. They are normally untracked and so absent
85
+ # from ls-files anyway — filtered explicitly so that a project which happens to
86
+ # commit one of them cannot silently freeze the engine's own scratch space.
87
+ # Extendable per project via EPAM_PERIMETER_EXCLUDE (comma-separated prefixes).
88
+ _perimeter_targets() {
89
+ local repo="$1"
90
+ local extra="${EPAM_PERIMETER_EXCLUDE:-}"
91
+ git -C "$repo" ls-files -z 2>/dev/null | while IFS= read -r -d '' f; do
92
+ case "$f" in
93
+ .epam/*|.codegraph/*|.epam|.codegraph) continue ;;
94
+ esac
95
+ local skip=0 pfx
96
+ if [ -n "$extra" ]; then
97
+ IFS=',' read -ra _pfxs <<< "$extra"
98
+ for pfx in "${_pfxs[@]}"; do
99
+ pfx="$(echo "$pfx" | tr -d '[:space:]')"
100
+ [ -n "$pfx" ] && case "$f" in "$pfx"*) skip=1 ;; esac
101
+ done
102
+ fi
103
+ [ "$skip" -eq 1 ] && continue
104
+ printf '%s\0' "$f"
105
+ done
106
+ }
107
+
108
+ perimeter_lock() {
109
+ local repo="${1:-}"
110
+ [ -n "$repo" ] && [ -d "$repo" ] || return 0
111
+ local n=0
112
+ while IFS= read -r -d '' f; do
113
+ [ -f "$repo/$f" ] || continue
114
+ chmod a-w "$repo/$f" 2>/dev/null && n=$((n + 1))
115
+ done < <(_perimeter_targets "$repo")
116
+ if [ "$n" -gt 0 ]; then
117
+ mkdir -p "$repo/.epam" 2>/dev/null || true
118
+ date -u +%Y-%m-%dT%H:%M:%SZ > "$repo/.epam/.write-perimeter-lock" 2>/dev/null || true
119
+ _perim_log "LOCKED $n tracked file(s) read-only in $repo (on baseline branch — agents may not write here)"
120
+ fi
121
+ return 0
122
+ }
123
+
124
+ perimeter_unlock() {
125
+ local repo="${1:-}"
126
+ [ -n "$repo" ] && [ -d "$repo" ] || return 0
127
+ local n=0
128
+ while IFS= read -r -d '' f; do
129
+ [ -f "$repo/$f" ] || continue
130
+ chmod u+w "$repo/$f" 2>/dev/null && n=$((n + 1))
131
+ done < <(_perimeter_targets "$repo")
132
+ rm -f "$repo/.epam/.write-perimeter-lock" 2>/dev/null || true
133
+ [ "$n" -gt 0 ] && _perim_log "unlocked $n tracked file(s) in $repo"
134
+ return 0
135
+ }
136
+
137
+ # WHICH AGENTS MAY AUTHOR CODE AT ALL.
138
+ #
139
+ # Being on a story branch says WHERE writes may land; this says WHO may make
140
+ # them. Only agents whose job is to author files are listed. Everything else —
141
+ # code-graph-detective, team-lead-review, failure-analyst, prd-change-reviewer,
142
+ # plan-reviewer, the spec agents — reads code to form a judgement and has no
143
+ # reason to modify it. All six of those hold `bash` today, which is how ~1050
144
+ # lines of client source were rewritten during a spec pass with no writer
145
+ # running.
146
+ #
147
+ # The reviewer is deliberately NOT here: it reads the diff and the repo to
148
+ # decide, and returns a verdict. If a reviewer needs to change code, that is a
149
+ # writer re-implementation cycle, which is exactly what Step 3.6 already does.
150
+ #
151
+ # Configurable per project, never hardcoded to one pipeline's role names.
152
+ #
153
+ # This list USED TO name them: "writer,typescript-engineer,test-engineer,repro-test-writer,
154
+ # lint-fix". Two of those five are epam-cli's own agents, carried from its first commit, and a
155
+ # role minted for some other project was in none of them. It would be proposed, briefed, wired,
156
+ # given inputs and tools, assigned a story — and then be unable to write a byte, because this
157
+ # gate is enforced with chmod below the tool layer. Every attempt fails, the ladder climbs, the
158
+ # budget exhausts, and nothing in the logs says the agent was never allowed to write.
159
+ #
160
+ # What remains here is the AUTHORING SEAMS: pipeline stages that write, which are not roster
161
+ # roles at all. Project roles are derived from the roster instead (see below), so a role minted
162
+ # tomorrow is covered without editing this file.
163
+ _PERIMETER_AUTHORING_SEAMS="writer,repro-test-writer,lint-fix"
164
+
165
+ # _perimeter_project_roles — this project's implementation roles, from the mint's registry.
166
+ #
167
+ # NOT derived as "the roster minus the canonical core". That was tried and it is wrong: of 38
168
+ # non-canonical roles in a live roster, only about nine implement anything — the rest is engine
169
+ # machinery (doc-*, failure-analyst, the vocabulary agents, code-graph-detective). Deriving from
170
+ # that set handed write access to the DETECTIVE, the exact agent whose lack of it is why this
171
+ # perimeter exists. The existing perimeter suite caught it.
172
+ #
173
+ # So the roster states what each agent IS, and this reads the `kind` field. One fact, one place:
174
+ # authorship is a property of an agent, not a separate registry that can disagree with it.
175
+ #
176
+ # THE ENGINE-LEVEL FALLBACK IS GONE. The previous resolver tried EPAM_PROJECT_ROLES_FILE, then the
177
+ # project's own project-roles.json, then agents/project-roles.json — so a client codeline whose
178
+ # project registry was missing inherited THIS repository's implementation roles.
179
+ #
180
+ # Fails CLOSED. No roster, unreadable roster, no project declared → contributes nothing, and only
181
+ # the authoring seams may write. A perimeter that fails open is not a perimeter; one that fails
182
+ # open quietly is worse, so the refusal is stated on stderr.
183
+ _perimeter_project_roles() {
184
+ if [ -n "${_PERIM_PROJECT_ROLES_CACHE+x}" ]; then
185
+ printf '%s' "$_PERIM_PROJECT_ROLES_CACHE"
186
+ return 0
187
+ fi
188
+ local _lib_dir _out
189
+ _lib_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" 2>/dev/null && pwd)"
190
+ _out=""
191
+ if [ -n "${EPAM_PROJECT_CONFIG_DIR:-}" ]; then
192
+ # Through the library, so "which agents may author" has ONE definition — the same one the
193
+ # producer validates against. A jq reimplementation here is a second answer to the
194
+ # question, and the two drift the first time the roster's shape moves.
195
+ _out=$("${NODE_BIN:-node}" -e '
196
+ const m = require(process.argv[1]);
197
+ try { process.stdout.write(m.agentsOfKind("implementer", process.argv[2]).join(",")); }
198
+ catch (e) { process.stderr.write("[perimeter] " + (e && e.message) + "\n"); process.exit(1); }
199
+ ' "${_lib_dir}/project-roster.js" "$EPAM_PROJECT_CONFIG_DIR" 2>/dev/null) || _out=""
200
+ else
201
+ echo "[perimeter] no project declared — no agent may author, only the authoring seams." >&2
202
+ fi
203
+ _PERIM_PROJECT_ROLES_CACHE="$_out"
204
+ printf '%s' "$_out"
205
+ }
206
+
207
+ perimeter_role_may_write() {
208
+ local role="${1:-${EPAM_AGENT_NAME:-}}"
209
+ [ -n "$role" ] || return 1 # unknown caller: no writes
210
+ local allowed
211
+ if [ -n "${EPAM_PERIMETER_WRITE_ROLES:-}" ]; then
212
+ # An explicit operator override replaces the rule entirely — it is the escape hatch,
213
+ # so it must not be silently unioned with anything derived.
214
+ allowed="$EPAM_PERIMETER_WRITE_ROLES"
215
+ else
216
+ allowed="${_PERIMETER_AUTHORING_SEAMS},$(_perimeter_project_roles)"
217
+ fi
218
+ local r
219
+ IFS=',' read -ra _roles <<< "$allowed"
220
+ for r in "${_roles[@]}"; do
221
+ r="$(echo "$r" | tr -d '[:space:]')"
222
+ [ -n "$r" ] && [ "${role%%:*}" = "$r" ] && return 0 # strip any ":plan" suffix
223
+ done
224
+ return 1
225
+ }
226
+
227
+ # Lock or leave open according to the rule. Safe to call repeatedly.
228
+ perimeter_apply() {
229
+ local repo="${1:-}"
230
+ [ -n "$repo" ] && [ -d "$repo" ] || return 0
231
+ if perimeter_is_write_allowed "$repo"; then
232
+ _perim_log "$repo is a worktree or story branch — writes permitted, not locking"
233
+ perimeter_unlock "$repo" >/dev/null
234
+ else
235
+ perimeter_lock "$repo"
236
+ fi
237
+ return 0
238
+ }
239
+
240
+ # perimeter_seal <repo>
241
+ #
242
+ # THE RUN-START OPERATION. Locks a real checkout whatever branch it is on.
243
+ #
244
+ # perimeter_apply answers a different question — "is this repo in its write window?" — and
245
+ # unlocks whenever the checkout is on a branch other than the baseline, because that is how
246
+ # ensure_story_branch opens the window after resetting and creating the story branch. Calling
247
+ # it at RUN START reads a LEFTOVER branch as authorisation, and at run start no branch this run
248
+ # created can exist yet.
249
+ #
250
+ # Live 2026-08-09: next.metrolinx.com sat on a story branch left by a killed run, which
251
+ # survived the preflight reset. Run start logged "writes permitted, not locking", the repo
252
+ # stayed writable for the entire run, and a client source file was rewritten into an
253
+ # incompatible component during the SPEC PASS — in a run that paused before the writer and
254
+ # never started one. The two codelines on the baseline were sealed and untouched.
255
+ #
256
+ # The writer is unaffected: ensure_story_branch still calls perimeter_apply after it creates
257
+ # the branch, and that reopens the repo exactly as before.
258
+ #
259
+ # A linked worktree is exempt in both, because git's own structure makes it per-story rather
260
+ # than a name anyone can leave lying around.
261
+ perimeter_seal() {
262
+ local repo="${1:-}"
263
+ [ -n "$repo" ] && [ -d "$repo" ] || return 0
264
+ [ -e "$repo/.git" ] || return 0 # not a repo: not ours to lock
265
+ if perimeter_is_worktree "$repo"; then
266
+ _perim_log "$repo is a linked worktree — per-story by construction, not sealing"
267
+ return 0
268
+ fi
269
+ perimeter_lock "$repo"
270
+ }
271
+
272
+ # perimeter_seal_all <codeline-root>
273
+ #
274
+ # Seal every repository under the root. The MIRROR of perimeter_release_all, and the reason
275
+ # this exists as a function at all: sealing used to be an inline loop in one launcher while
276
+ # releasing was generic in the engine, so seven of eight projects ran with no perimeter and
277
+ # nothing said so — releasing a repo that was never locked logs nothing.
278
+ #
279
+ # The defect that split cost: live 2026-08-17 run 20260817T231306Z, mock-a's src/fares.ts and
280
+ # test/fares.test.ts were rewritten during the SPEC PASS, before the writer stage was reached,
281
+ # on a project whose launcher never sealed. The same thing had already been recorded on the
282
+ # client estate — ~1050 lines across five files, same stage, which is why this library exists.
283
+ #
284
+ # Idempotent, and never fails the caller: a locked repo re-locks to the same state.
285
+ perimeter_seal_all() {
286
+ local root="${1:-}"
287
+ [ -n "$root" ] && [ -d "$root" ] || return 0
288
+ local _n=0 _cl
289
+ for _cl in "$root"/*/; do
290
+ [ -e "${_cl}.git" ] || continue
291
+ perimeter_seal "${_cl%/}" && _n=$((_n + 1))
292
+ done
293
+ [ "$_n" -gt 0 ] && _perim_log "sealed $_n codeline(s) — agents may not write until a story branch exists"
294
+ return 0
295
+ }
296
+
297
+ # perimeter_release_all <codeline-root>
298
+ #
299
+ # Give every repository under the root back to the operator. Called when the RUN ends —
300
+ # including the successful ending, which for these runs is the pause before the writer.
301
+ #
302
+ # Nothing did this. perimeter_apply locked at run start, ensure_story_branch reopened a repo
303
+ # that reached a story branch, and the rest stayed read-only forever. Observed twice on
304
+ # 2026-08-06: after a run paused, 23 of the operator's repositories were still locked and
305
+ # nothing said so. The kill path has the same gap.
306
+ #
307
+ # Idempotent, and never fails the caller: it restores permission, it does not touch content.
308
+ perimeter_release_all() {
309
+ local root="${1:-}"
310
+ [ -n "$root" ] && [ -d "$root" ] || return 0
311
+ local _n=0 _cl
312
+ for _cl in "$root"/*/; do
313
+ [ -e "${_cl}.git" ] || continue
314
+ perimeter_unlock "${_cl%/}" >/dev/null 2>&1 && _n=$((_n + 1))
315
+ done
316
+ [ "$_n" -gt 0 ] && _perim_log "released $_n codeline(s) — repositories are writable again"
317
+ return 0
318
+ }
@@ -0,0 +1,66 @@
1
+ #!/usr/bin/env bash
2
+ # common.sh — moved verbatim out of claude.sh by tools/split-main-into-modules.py
3
+ # (6 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
+ # KB IS KEYED BY CODELINE, NOT BY AGENT ROLE.
8
+ #
9
+ # The roster is ephemeral by design — regenerated every run, no aggregation — and the mint
10
+ # invents a new NAME each run for what is essentially the same agent. KB-<role>.md therefore
11
+ # named an address that changed every run: 32 files accumulated, each holding what one run
12
+ # learned, none reachable by any later run. The store persisted; the key did not.
13
+ #
14
+ # A codeline is stable, discovered rather than invented, already the investigator key, and the
15
+ # subject of most durable learning — where the SDK is initialised here, how this repository
16
+ # names its tests. Project-wide lessons that belong to no codeline go to the shared file.
17
+ # _resolved_baseline_ref [repo] — the ref every diff in this file compares against.
18
+ #
19
+ # NINE SITES SPELLED THIS `origin/${JIRA_BASELINE_BRANCH:-develop}`. "develop" is a fact of some
20
+ # projects and not of others, so on a codeline whose trunk is named anything else every one of
21
+ # those diffs resolved nothing — and a diff against a ref that does not exist is empty, which
22
+ # reads downstream exactly like "this story changed nothing".
23
+ #
24
+ # The project declares it; otherwise take the repository's OWN checked-out branch, which is at
25
+ # least true. Prefer origin/<branch> when that ref exists, because these are baseline comparisons
26
+ # and the remote is the shared baseline; fall back to the local branch when there is no remote.
27
+ # Prints nothing when nothing resolves, so a caller can refuse rather than diff against a name.
28
+ _resolved_baseline_ref() {
29
+ local _repo="${1:-${PROJECT_ROOT:-.}}"
30
+ local _branch="${JIRA_BASELINE_BRANCH:-}"
31
+ if [ -z "$_branch" ]; then
32
+ _branch="$(git -C "$_repo" rev-parse --abbrev-ref HEAD 2>/dev/null || echo "")"
33
+ [ "$_branch" = "HEAD" ] && _branch=""
34
+ fi
35
+ [ -n "$_branch" ] || return 0
36
+ if git -C "$_repo" rev-parse --verify --quiet "origin/${_branch}" >/dev/null 2>&1; then
37
+ printf 'origin/%s' "$_branch"
38
+ else
39
+ printf '%s' "$_branch"
40
+ fi
41
+ }
42
+
43
+ # Logging functions
44
+ log() {
45
+ echo -e "${BLUE}[$(date +'%Y-%m-%d %H:%M:%S')]${NC} $1" >&2
46
+ echo "[$(date +'%Y-%m-%d %H:%M:%S')] $1" >> "$PROGRESS_LOG"
47
+ }
48
+
49
+ error() {
50
+ echo -e "${RED}[ERROR]${NC} $1" >&2
51
+ echo "[ERROR] [$(date +'%Y-%m-%d %H:%M:%S')] $1" >> "$PROGRESS_LOG"
52
+ }
53
+
54
+ success() {
55
+ echo -e "${GREEN}[SUCCESS]${NC} $1" >&2
56
+ echo "[SUCCESS] [$(date +'%Y-%m-%d %H:%M:%S')] $1" >> "$PROGRESS_LOG"
57
+ }
58
+
59
+ warning() {
60
+ echo -e "${YELLOW}[WARNING]${NC} $1" >&2
61
+ echo "[WARNING] [$(date +'%Y-%m-%d %H:%M:%S')] $1" >> "$PROGRESS_LOG"
62
+ }
63
+
64
+ info() {
65
+ echo -e "${CYAN}[INFO]${NC} $1" >&2
66
+ }