bareloop 0.8.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/root.js CHANGED
@@ -30,6 +30,7 @@
30
30
  // UNRELIABLE red-set for comparison. Imported (never re-spelled) so the detector
31
31
  // keys off the exact string the shell emits — a magic string here would drift.
32
32
  import { GAP_KEEP_TRIM_MARKER } from './ralph.js';
33
+ import { GAP_TRIM_MARKER } from './kinds.js';
33
34
 
34
35
  /**
35
36
  * Strip a trailing duration stamp from a kept-failure line. Real `node --test`
@@ -107,7 +108,13 @@ export function createRoot({ gapKeep, redact = (s) => s, writesInformative = tru
107
108
  */
108
109
  const keptSet = (gap, pattern) => {
109
110
  const re = pattern ? new RegExp(pattern, 'm') : keepRe;
110
- if (!re || gap.includes(GAP_KEEP_TRIM_MARKER)) return null;
111
+ // BOTH trim announcements, because there are now two gap renderers: ralph's
112
+ // keep-block cap on a command close, and the kind executor's per-stage line
113
+ // cap on a DECLARED one. A trim the detector cannot see is worse than a
114
+ // missing detector — it makes the strong "reds unchanged" claim off a window
115
+ // that silently dropped failures (finding 5), which is the direction that
116
+ // manufactures a fixation reading out of an instrument that went blind.
117
+ if (!re || gap.includes(GAP_KEEP_TRIM_MARKER) || gap.includes(GAP_TRIM_MARKER)) return null;
111
118
  const lines = [...new Set(gap.split('\n').filter((l) => re.test(l)).map(normalizeRedLine))].sort();
112
119
  return lines.length ? lines : null;
113
120
  };
package/src/run.js CHANGED
@@ -125,6 +125,13 @@ async function primitiveSmoke(workdir) {
125
125
  * RUN's ("unlimited replanning launders thrash as adaptation"). It also rides onto this
126
126
  * run's `job-start` as `priorReplans`, the declared-fold precedent `priorSpentUsd`
127
127
  * already sets, so a resume OF a resume folds once. Absent is the cold path.
128
+ * @param {string|null} [opts.resumeBranch] RESUME — the WORK BRANCH the killed attempt
129
+ * created and recorded on its own spine (`readResume`'s `restart.branch`). The branch
130
+ * name is DERIVED from the signed spec and is therefore identical on every leg, so
131
+ * without this the restart would collide with its own predecessor's branch and the
132
+ * collision walk would mint a `-2` beside the work the resume exists to continue. The
133
+ * fold's precedent exactly: the chain's state is DECLARED, never re-derived. Forwarded
134
+ * verbatim to the plan flow; absent is the cold path.
128
135
  * @param {boolean} [opts.layerRoot=false] Layer R (within-run ratchet) — shell
129
136
  * territory, threaded to the plan flow. Defaults OFF
130
137
  * (decided 2026-07-21): fixation is extinct on every current job (F41), so
@@ -134,10 +141,10 @@ async function primitiveSmoke(workdir) {
134
141
  * binding.)
135
142
  * @returns {Promise<string>} outcome: 'green' | 'already-green' | 'escalated' |
136
143
  * 'unapproved-spec' | 'job-red' | 'smoke-red' | 'plan-red' | 'check-red' |
137
- * 'close-red' | 'close-unsupported' | 'recipe-stale' | 'pricing-red' | 'provider-red' |
144
+ * 'close-red' | 'close-unsupported' | 'recipe-stale' | 'branch-red' | 'pricing-red' | 'provider-red' |
138
145
  * 'interpreter-red' | 'cap-halt' | 'wall-halt' | 'step-stalled' | `step-red:<id>`
139
146
  */
140
- export async function runJob(rawSpec, { approvals, workdir, provider, nativeProvider, providerFor, emit, capRuns = 3, strikeLimit, shellCapUsd = 2, closeTimeoutMs, layerRoot = false, bridge = null, priorSpentUsd = 0, priorSpendComplete = true, priorWallMs = 0, resumeSeed = null, resumeGrades = [], resumeReplans = null }) {
147
+ export async function runJob(rawSpec, { approvals, workdir, provider, nativeProvider, providerFor, emit, capRuns = 3, strikeLimit, shellCapUsd = 2, closeTimeoutMs, layerRoot = false, bridge = null, priorSpentUsd = 0, priorSpendComplete = true, priorWallMs = 0, resumeSeed = null, resumeGrades = [], resumeReplans = null, resumeBranch = null }) {
141
148
  // 0. the ledger's counters, declared FIRST so that every job-end — including
142
149
  // the pre-token reds below — can state a real figure. An omitted `spentUsd` is
143
150
  // not a zero: a consumer reads `undefined` and either crashes or launders it
@@ -275,7 +282,7 @@ export async function runJob(rawSpec, { approvals, workdir, provider, nativeProv
275
282
  // accounts it natively (F12) and the job-end money contract is unchanged.
276
283
  {
277
284
  const outcome = await runPlan(job, {
278
- workdir, provider, nativeProvider, providerFor, emit: meter, capRuns, ...(strikeLimit !== undefined ? { strikeLimit } : {}), closeTimeoutMs, layerRoot, bridge, priorWallMs, resumeSeed, resumeGrades, resumeReplans,
285
+ workdir, provider, nativeProvider, providerFor, emit: meter, capRuns, ...(strikeLimit !== undefined ? { strikeLimit } : {}), closeTimeoutMs, layerRoot, bridge, priorWallMs, resumeSeed, resumeGrades, resumeReplans, resumeBranch,
279
286
  remainingUsd: () => Math.min(shellCapUsd, job.budgetUsd - spentUsd),
280
287
  isUnpriced: () => unpriced, // F6: let the plan flow bail in-flight, not just after it returns
281
288
  spendComplete, // …and let its money-halt readout say whether the remaining it quotes is exact
package/src/text.js CHANGED
@@ -4,6 +4,11 @@
4
4
  // are two instruments (F2/F21, the adaptlearn instrument caveat). It replaced
5
5
  // N0's stripFences when the fence-robust upgrade landed at N2. priceOf is the
6
6
  // ONE spelling of the honest-null cost read for the same reason (F6).
7
+ // `scrubRaw` is the third: the ONE way a model's raw output becomes a persisted
8
+ // record, for the same reason again — a second writer that forgets the scrub is
9
+ // a leak on the very trail the scrub exists to protect.
10
+
11
+ import { redactSecrets } from './validate.js';
7
12
 
8
13
  // A fence counts as the artifact's WRAPPER only when it opens within this many
9
14
  // lines of the response — the chatty-preamble shape ("Here's the fix:\n```…").
@@ -64,3 +69,118 @@ export function priceOf(r) {
64
69
  unpricedRounds: r?.metrics?.unpricedRounds ?? 0,
65
70
  };
66
71
  }
72
+
73
+ // ── THE RAW MODEL OUTPUT, AS A PERSISTED RECORD ─────────────────────────────
74
+ //
75
+ // Run mslhn707 died on a survey that broke JSON at position 1339 and left NO
76
+ // copy of what the model actually said: the red named the position, the process
77
+ // exited, and the only artifact that could have decided whether the malformation
78
+ // is recurring (and therefore whether the scout must move to a structure-forced
79
+ // tool call) went with it. A raw that is not written down is a diagnosis nobody
80
+ // can make twice.
81
+ //
82
+ // Everything a persisted raw needs is HERE rather than at each writer, and the
83
+ // reason is the `scrubRed` reason one layer up: the scrub, the bound and the
84
+ // announcement are properties of PERSISTING A RAW, not of any one caller
85
+ // remembering them. A future writer that reaches for a raw reaches for this.
86
+
87
+ /**
88
+ * The ceiling on ONE persisted raw. Sized to the survey's own prompt-ingredient
89
+ * cap (`AUTHOR_SCOUT_BLOB_MAX`, 8000) rather than picked: the largest raw this
90
+ * repository produces is a survey, the archive's real surveys ran 5991-8056
91
+ * bytes, and a record that cannot hold one whole specimen cannot answer the
92
+ * question it exists for. The two constants are deliberately independent
93
+ * numbers — one bounds a PROMPT, this one bounds a RECORD.
94
+ */
95
+ export const RAW_PERSIST_MAX = 8000;
96
+
97
+ /**
98
+ * The announced-trim marker for a persisted raw. A NEW spelling rather than a
99
+ * reuse of `GAP_TRIM_MARKER` ('gap trimmed:'), and that is a decision, not
100
+ * decoration: `src/root.js` READS `GAP_TRIM_MARKER` to mean *this gap was cut,
101
+ * so Layer R must not compare it*, so a second producer of those bytes in a
102
+ * different population is exactly the F90 hazard — a reader must know every
103
+ * producer of its marker. Nothing reads this one yet; it exists so the trim
104
+ * announces itself (F28) and so a future reader has an unambiguous key.
105
+ */
106
+ export const RAW_TRIM_MARKER = 'raw trimmed:';
107
+
108
+ /**
109
+ * ONE raw model emission, ready to persist.
110
+ *
111
+ * Three things happen here and NONE of them is optional at a call site:
112
+ * - the text goes through the ONE redactor (`redactSecrets`, over the ONE
113
+ * `SECRET_PATTERNS` inventory). A raw is the single most likely record in
114
+ * this system to carry a live credential: it is whatever a model read out
115
+ * of a repository, verbatim, and it lands in a file that outlives the run;
116
+ * - it is BOUNDED, and the bound ANNOUNCES itself with the full size (F28 —
117
+ * a bound eats detail, never evidence). The cut is decoded rather than
118
+ * sliced so a multi-byte character is never split in half;
119
+ * - it carries its own address: which call produced it (`label`, the same
120
+ * spelling the cost book meters under) and which attempt (`attempt`), plus
121
+ * the typed failure that describes it when there is one.
122
+ *
123
+ * `cause`/`reason` are `null` when the emission was fine — an ABSENT failure is
124
+ * absent, never an empty string that reads like a diagnosis nobody wrote.
125
+ * @param {{label: string, attempt: number, text: unknown, cause?: string|null,
126
+ * reason?: string|null, cap?: number}} o
127
+ * @returns {{label: string, attempt: number, bytes: number, trimmed: boolean,
128
+ * text: string, cause: string|null, reason: string|null}}
129
+ */
130
+ export function scrubRaw({ label, attempt, text, cause = null, reason = null, cap = RAW_PERSIST_MAX }) {
131
+ const scrubbed = redactSecrets(String(text ?? ''));
132
+ const buf = Buffer.from(scrubbed, 'utf8');
133
+ const trimmed = buf.length > cap;
134
+ // walk the cut BACK off any continuation byte (0b10xxxxxx): a byte-wise slice
135
+ // through a multi-byte character renders as a replacement char, and a record
136
+ // that garbles the evidence is a worse instrument than one that keeps a byte
137
+ // less of it
138
+ let end = cap;
139
+ while (trimmed && end > 0 && (buf[end] & 0xC0) === 0x80) end -= 1;
140
+ const kept = trimmed ? buf.subarray(0, end).toString('utf8') : scrubbed;
141
+ // the two diagnosis fields are rendered by `stampRaw` below, never here: the
142
+ // writer that stamps them LATER must go through the same one place, or the
143
+ // scrub becomes a property of when you knew the verdict
144
+ return stampRaw({
145
+ label,
146
+ attempt,
147
+ // the FULL size, always — the number a trim is measured against
148
+ bytes: buf.length,
149
+ trimmed,
150
+ // WITHHELD is measured off `end`, not off `cap`: the multi-byte walk-back above
151
+ // can cut up to three bytes short of the cap, and announcing `length - cap` would
152
+ // under-report the withheld bytes by exactly that much. An announced bound that
153
+ // states a number it did not withhold is the announcement failing at its one job.
154
+ text: trimmed
155
+ ? `${kept}\n[${RAW_TRIM_MARKER} ${buf.length - end} of ${buf.length} bytes withheld — the cap is ${cap} per raw]`
156
+ : kept,
157
+ cause: null,
158
+ reason: null,
159
+ }, { cause, reason });
160
+ }
161
+
162
+ /**
163
+ * Stamp a persisted raw with the verdict that DESCRIBES it — through the same
164
+ * boundary the raw's own text rode.
165
+ *
166
+ * It exists because the verdict is not known when the raw is created: a writer
167
+ * records the emission the moment the call returns (so a metered call can never
168
+ * be missing its output), and only classifies it afterwards. The obvious
169
+ * shape — assigning `cause`/`reason` onto the stored object at that later
170
+ * point — writes an UNSCRUBBED string into a record that is already past the
171
+ * one redactor, and `reason` is the worst field to do it to: it quotes the
172
+ * transport's own error prose, which nobody in this repo wrote and nobody
173
+ * bounds. So the stamp is a function, not an assignment, and `scrubRaw` renders
174
+ * its own two fields through it: ONE place decides how a diagnosis becomes
175
+ * persistable, exactly as ONE place decides it for the text.
176
+ *
177
+ * `cause` is enumerated (`SURVEY_CAUSES`) and passes through untouched; a mask
178
+ * over a closed set would only hide a bug in the set.
179
+ * @template {{cause: string|null, reason: string|null}} T
180
+ * @param {T} raw @param {{cause?: string|null, reason?: string|null}} o @returns {T}
181
+ */
182
+ export function stampRaw(raw, { cause = null, reason = null }) {
183
+ raw.cause = cause ?? null;
184
+ raw.reason = reason === null || reason === undefined ? null : redactSecrets(reason);
185
+ return raw;
186
+ }
@@ -0,0 +1,261 @@
1
+ // THE WORK BRANCH — runner mechanics, arbiter side. PRD Addendum v1.57 §3,
2
+ // hamr's ruling verbatim:
3
+ //
4
+ // "The agent creates a NEW BRANCH before it touches any code — a HARD RULE, no
5
+ // exceptions. Not a default, not a preference: no job edits the branch it was
6
+ // handed, and none edits `main`. Blast radius is bounded by where the work
7
+ // lands, not only by what the fence allows. Named with a meaningful slug —
8
+ // tool and problem, human-readable, of the form `bareloop-typecheck-fix`. A
9
+ // human scanning `git branch` must be able to tell what the run was for
10
+ // without opening the spec, which a run-id slug does not give them."
11
+ //
12
+ // WHAT IT IS AND IS NOT, stated here rather than left to be inferred (v1.57's own
13
+ // known-limitations discipline). bareloop never commits: the run leaves its work
14
+ // in the tree. So the guarantee this module actually delivers is precise —
15
+ //
16
+ // * the run's tree is standing on a branch NOBODY handed it, from the first
17
+ // moment a write-capable worker can exist to the end of the run, and
18
+ // * the handed ref (`main`, or whatever was checked out) is never moved,
19
+ // never written through, and never the thing a later commit attaches to.
20
+ //
21
+ // It is NOT containment: v1.41's local-trust model stands untouched, and a close
22
+ // or worker child can still reach anything the operator's OS user can reach. The
23
+ // branch bounds where the work LANDS. That is the whole claim.
24
+ //
25
+ // THE NAME IS OURS, NEVER THE MODEL'S. A branch name is arbiter bookkeeping: it
26
+ // says which history a run's blast radius belongs to. A model-authored one would
27
+ // be the agent authoring the arbiter's own books, so the name is DERIVED,
28
+ // mechanically and deterministically, from the SIGNED spec's own `job` field
29
+ // (see `workBranchName`).
30
+ //
31
+ // WHY THIS FILE OWNS ITS OWN git SEAM. `src/kinds.js` has one too, and its
32
+ // docstring says what it is: "git, read-only, in a repository" — the CLOSE's
33
+ // measuring instrument, whose faults are close faults. This one MUTATES the
34
+ // repository (it creates and checks out a ref) and its faults are runner
35
+ // instrument stops before a single token is bought. Two verbs, two failure
36
+ // vocabularies, two owners; the read-only one is not exported and could not be
37
+ // shared without widening it into a write seam.
38
+
39
+ import { execFile } from 'node:child_process';
40
+ import { promisify } from 'node:util';
41
+
42
+ const exec = promisify(execFile);
43
+
44
+ /** Every work branch carries it. It is what makes `main`, `master` and every
45
+ * branch a human could plausibly have handed the run INEXPRESSIBLE as a work
46
+ * branch name — a structural guarantee rather than a denylist somebody has to
47
+ * keep current. */
48
+ export const WORK_BRANCH_PREFIX = 'bareloop-';
49
+
50
+ /** The full legal shape of a work branch. Deliberately narrower than git's own
51
+ * `check-ref-format`: the kebab alphabet the job validator already holds every
52
+ * spec slug to (`src/job.js` SLUG_RE), plus the prefix. Everything git forbids —
53
+ * `..`, `@{`, a trailing `.lock`, a leading `-`, a `/` component, control
54
+ * characters — is unreachable inside it, so ref-safety is a property of the
55
+ * alphabet and never a sanitiser somebody has to remember to call. */
56
+ export const WORK_BRANCH_RE = /^bareloop-[a-z0-9][a-z0-9-]*$/;
57
+
58
+ /** The slug's ceiling, before any collision suffix. A ref component has a
59
+ * filesystem-length limit and a human scanning `git branch` has a much smaller
60
+ * one; 60 characters is longer than every job slug in `jobs/` and short enough
61
+ * to read in a column. */
62
+ export const MAX_SLUG_LEN = 60;
63
+
64
+ /** How far the collision walk goes before it STOPS and says so. A run that
65
+ * cannot find a free name in this many attempts is looking at a patient nobody
66
+ * has swept in a very long time, and silently walking to `-500` would be the
67
+ * runner papering over an operator problem. */
68
+ export const MAX_BRANCH_COLLISIONS = 50;
69
+
70
+ /** git, in a repository, WRITE-CLASS. A non-zero exit is a value, never a throw:
71
+ * every caller here decides for itself whether git refusing is a stop.
72
+ * @param {string} cwd @param {string[]} args
73
+ * @returns {Promise<{ok: boolean, out: string, err: string}>}
74
+ */
75
+ async function git(cwd, args) {
76
+ try {
77
+ const { stdout, stderr } = await exec('git', args, {
78
+ cwd, encoding: 'utf8', timeout: 120_000, maxBuffer: 1024 * 1024,
79
+ });
80
+ return { ok: true, out: String(stdout ?? ''), err: String(stderr ?? '') };
81
+ } catch (e) {
82
+ const err = /** @type {any} */ (e);
83
+ // A spawn failure (no git on PATH, cwd does not exist) has no streams at all;
84
+ // its `message` is the only thing there is to report, and reporting nothing
85
+ // would turn "git is not installed" into a mysterious silent refusal.
86
+ const detail = String(err?.stderr ?? '').trim() || String(err?.message ?? err).trim();
87
+ return { ok: false, out: String(err?.stdout ?? ''), err: detail };
88
+ }
89
+ }
90
+
91
+ /**
92
+ * The spec's own words, reduced to the kebab alphabet. Lossy on purpose: the
93
+ * output alphabet IS the ref-safety guarantee, so anything outside it collapses
94
+ * to a separator rather than being escaped into something clever.
95
+ * @param {unknown} s
96
+ * @returns {string} `''` when nothing usable survives
97
+ */
98
+ function slugify(s) {
99
+ return String(s ?? '')
100
+ .toLowerCase()
101
+ .replace(/[^a-z0-9]+/g, '-')
102
+ .replace(/^-+/, '')
103
+ .slice(0, MAX_SLUG_LEN)
104
+ .replace(/-+$/, '');
105
+ }
106
+
107
+ /**
108
+ * The branch name a job's run works on, DERIVED from the signed spec.
109
+ *
110
+ * **The source is `spec.job`, and the choice is load-bearing.** It is the one
111
+ * human-authored, REQUIRED, kebab-case name the spec carries; it is inside the
112
+ * spec hash, so the branch name cannot drift from what the operator signed; and
113
+ * it is stable across a resume and across a cold re-run of the same spec, which
114
+ * is precisely why the collision walk below has to exist. In practice it already
115
+ * reads as tool-and-problem (`bareagent-u-types`, `litectx-u-types`), which is
116
+ * the shape hamr's ruling asks for.
117
+ *
118
+ * **The goal prose was REJECTED as a source**, and not for tidiness: a goal is
119
+ * long, is edited freely between runs without changing what the job IS, and
120
+ * opens on boilerplate ("An existing pytest test suite for the SOAR
121
+ * orchestrator…"). Deriving from it would make the branch name unstable under
122
+ * edits that change nothing, and unreadable when it was stable. A run id was
123
+ * rejected by the ruling itself.
124
+ *
125
+ * Deterministic, pure, and total: any input yields a name matching
126
+ * `WORK_BRANCH_RE`, so a caller never has to check.
127
+ * @param {any} spec a job-v1 spec (validated or raw)
128
+ * @returns {string}
129
+ */
130
+ export function workBranchName(spec) {
131
+ const slug = slugify(spec?.job);
132
+ // `job` is REQUIRED and kebab-validated by `validateJob`, which runs before the
133
+ // runner ever reaches here — so the fallback is unreachable through `runJob`.
134
+ // It exists because this function is exported and total: a name is the wrong
135
+ // thing to throw about, and `bareloop-job` is honest ("a bareloop job") rather
136
+ // than invented.
137
+ return `${WORK_BRANCH_PREFIX}${slug || 'job'}`;
138
+ }
139
+
140
+ /** does a local branch of this name exist? @param {string} cwd @param {string} name */
141
+ async function branchExists(cwd, name) {
142
+ const r = await git(cwd, ['show-ref', '--verify', '--quiet', `refs/heads/${name}`]);
143
+ return r.ok;
144
+ }
145
+
146
+ /**
147
+ * Put the run on its OWN branch, before it can touch any code.
148
+ *
149
+ * The three shapes, and each one's honest answer:
150
+ *
151
+ * - **Cold.** `name` is free → create it at HEAD and switch. The handed branch
152
+ * keeps its ref and its history; a new branch at HEAD moves no commit, so this
153
+ * costs the patient nothing and changes nothing it can measure (the declared
154
+ * close's seed is HEAD either way).
155
+ * - **Cold, and the name is taken.** Suffix `-2`, `-3`… until one is free.
156
+ * Deterministic given the repository, and it NEVER reuses another run's
157
+ * branch — a second run of the same spec is a second blast radius.
158
+ * - **Resume.** `resume` names the branch the killed leg recorded on its own
159
+ * spine. It is checked out, never re-created and never suffixed: the work the
160
+ * operator already paid for is sitting on it, and minting `-2` beside it would
161
+ * strand exactly the progress a resume exists to keep. A `resume` branch that
162
+ * is GONE is a STOP, never a silent fresh start — the run would be grading a
163
+ * tree whose history it does not own.
164
+ *
165
+ * **Every failure is a stop with a name.** Not a git repository, no git on PATH,
166
+ * a checkout git refuses, a crowded namespace, a resume branch a human deleted:
167
+ * each comes back as `stop` and `branch: null`. There is no fallback to "just
168
+ * work on whatever was handed to us" anywhere in this function, because that
169
+ * fallback IS the thing the hard rule forbids.
170
+ *
171
+ * A DETACHED head is not a failure. Creating a branch from it is exactly the
172
+ * bounding the rule asks for (and it attaches a head that was floating), so it
173
+ * succeeds with `from: null` — there was no branch to be handed, and the record
174
+ * says so rather than inventing one.
175
+ *
176
+ * @param {string} workdir the patient (the fence's root)
177
+ * @param {object} o
178
+ * @param {string} o.name the derived name (`workBranchName`); refused unless it
179
+ * matches `WORK_BRANCH_RE`, which is what makes `main` unreachable even from a
180
+ * hand-passed argument
181
+ * @param {string|null} [o.resume] RESUME — the branch this run already owns
182
+ * @returns {Promise<{stop: string, branch: null}
183
+ * | {stop: null, branch: string, created: boolean, resumed: boolean, from: string|null, base: string|null,
184
+ * repo: string, collided: number}>}
185
+ */
186
+ export async function prepareWorkBranch(workdir, { name, resume = null }) {
187
+ const wanted = String(name ?? '');
188
+ if (!WORK_BRANCH_RE.test(wanted)) {
189
+ return { stop: `the work branch name ${JSON.stringify(wanted)} is not a bareloop work branch (${WORK_BRANCH_RE.source}) — a run never works on a branch it was handed`, branch: null };
190
+ }
191
+
192
+ // Is there a repository at all? Asked FIRST and by its own question, so "this
193
+ // patient is not a git checkout" reads as itself instead of arriving later
194
+ // disguised as a checkout failure.
195
+ //
196
+ // `--show-toplevel` rather than `--git-dir`, and the answer is REPORTED: git
197
+ // resolves upwards, so a workdir that is a SUBDIRECTORY of a repository gets a
198
+ // branch in that enclosing repository — a blast radius wider than the operator
199
+ // handed over. That is the pre-existing semantics of every other git read in
200
+ // the flow (`seedAtHead`, `changedSet`), so it is not changed here; what it must
201
+ // not be is invisible, and the record names the repository that got the branch.
202
+ const inside = await git(workdir, ['rev-parse', '--show-toplevel']);
203
+ if (!inside.ok) {
204
+ return { stop: `the work branch cannot be created in ${workdir}: ${inside.err || 'not a git repository'}`, branch: null };
205
+ }
206
+ const repo = inside.out.trim().split('\n')[0].trim() || workdir;
207
+
208
+ // WHERE the run was handed. A symbolic HEAD names the handed branch; a detached
209
+ // one names nothing, and `null` is the honest reading of that (never the string
210
+ // "HEAD", which a reader cannot tell from a branch called HEAD).
211
+ const sym = await git(workdir, ['symbolic-ref', '--quiet', '--short', 'HEAD']);
212
+ const from = sym.ok ? sym.out.trim() || null : null;
213
+ const rev = await git(workdir, ['rev-parse', 'HEAD']);
214
+ const base = rev.ok ? (rev.out.trim().split('\n')[0].trim() || null) : null;
215
+
216
+ if (resume !== null && resume !== undefined) {
217
+ const own = String(resume);
218
+ if (!WORK_BRANCH_RE.test(own)) {
219
+ return { stop: `the recorded work branch ${JSON.stringify(own)} is not a bareloop work branch (${WORK_BRANCH_RE.source}) — this resume would continue on a branch the run does not own`, branch: null };
220
+ }
221
+ if (!await branchExists(workdir, own)) {
222
+ return { stop: `the work branch ${JSON.stringify(own)} this run was killed on no longer exists in ${workdir} — the resume stops rather than starting a new branch beside work it cannot see`, branch: null };
223
+ }
224
+ // Already standing on it (the ordinary case — a kill leaves the tree where it
225
+ // was) is a NO-OP, deliberately: `git checkout` onto the current branch is
226
+ // harmless today, and not running it at all is what keeps a dirty in-flight
227
+ // tree out of git's hands entirely.
228
+ if (from !== own) {
229
+ const co = await git(workdir, ['checkout', own]);
230
+ if (!co.ok) return { stop: `could not return to the work branch ${JSON.stringify(own)} in ${workdir}: ${co.err}`, branch: null };
231
+ }
232
+ return { stop: null, branch: own, created: false, resumed: true, from, base, repo, collided: 0 };
233
+ }
234
+
235
+ // The collision walk. `-2` is the first suffix rather than `-1`, so the branch a
236
+ // human reads without a number is the first run and the numbers count runs the
237
+ // way a person does.
238
+ //
239
+ // CHECK-THEN-ACT, named rather than fixed: another process can create the
240
+ // candidate between the `branchExists` read and the `checkout -b` below. It is
241
+ // documented instead of closed because git already fails CLOSED on it — `git
242
+ // checkout -b` REFUSES an existing branch, so the race ends in the named stop
243
+ // right beneath this loop, never in two runs sharing one blast radius. Closing
244
+ // it properly means atomic ref creation, which is a wider change than the
245
+ // failure it would prevent.
246
+ let collided = 0;
247
+ let candidate = wanted;
248
+ while (await branchExists(workdir, candidate)) {
249
+ collided += 1;
250
+ if (collided >= MAX_BRANCH_COLLISIONS) {
251
+ return { stop: `every work-branch name from ${JSON.stringify(wanted)} through ${JSON.stringify(`${wanted}-${MAX_BRANCH_COLLISIONS}`)} is already taken in ${workdir} — sweep the patient's old work branches rather than letting the runner walk further`, branch: null };
252
+ }
253
+ candidate = `${wanted}-${collided + 1}`;
254
+ }
255
+
256
+ const co = await git(workdir, ['checkout', '-b', candidate]);
257
+ if (!co.ok) {
258
+ return { stop: `could not create the work branch ${JSON.stringify(candidate)} in ${workdir}: ${co.err}`, branch: null };
259
+ }
260
+ return { stop: null, branch: candidate, created: true, resumed: false, from, base, repo, collided };
261
+ }