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,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
|
+
}
|