bareloop 0.14.0 → 0.15.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/CHANGELOG.md +143 -0
- package/bareloop.context.md +312 -0
- package/package.json +2 -2
- package/src/codeversion.js +155 -0
- package/src/index.js +12 -0
- package/src/ledger.js +1 -1
- package/src/promptregisters.js +93 -0
- package/src/replay.js +1212 -0
- package/src/run.js +14 -0
- package/types/codeversion.d.ts +18 -0
- package/types/index.d.ts +2 -0
- package/types/ledger.d.ts +14 -0
- package/types/promptregisters.d.ts +31 -0
- package/types/replay.d.ts +195 -0
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,149 @@ feature lands, **patch** = docs, fixes, scaffolding.
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.15.0] — 2026-08-27
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- **Generic run replay** (PRD build-list #6): `replayRun`/`formatReplay` (`src/replay.js`)
|
|
15
|
+
reconstruct one run's whole story from its spine JSONL + gate-audit sidecar. The single-run
|
|
16
|
+
report is a signed layout (hamr, 2026-08-25): a header block (`goal:`/`shape:`/`signed:`/
|
|
17
|
+
`branch:`/`outcome:`/`spent:`, absent fields printing the literal `none recorded`, never
|
|
18
|
+
blank or a fabricated `0`), a `TIMELINE (steps)` or `TIMELINE (iterations — loop-shape run)`
|
|
19
|
+
section with its own column-header row and per-row time/cost (wall, `$` — a `null` `costUsd`
|
|
20
|
+
anywhere in a row's window makes that row's spend `unknown (N unpriced rounds)`, never `0` or
|
|
21
|
+
a partial sum stamped exact) and a `↳ tripped:`/`↳ close:` line under any row an escalation or
|
|
22
|
+
close fell inside, a `CLOSE` section (never omitted — `none — the run ended before any close
|
|
23
|
+
ran` when nothing ever closed; picks the run's REAL final close by seq, never by record type —
|
|
24
|
+
an early `outer-close` precheck reading red must not shadow a LATER fix-loop close-verdict that
|
|
25
|
+
went satisfied), `ENDING` (last record + 3 before it, plus an out-of-window escalation), the
|
|
26
|
+
`BEHAVIOUR` block, and a `MEMORY-CACHE` line always printed (`not armed on this run` when
|
|
27
|
+
absent — absence is never silent). Stop reason is `category — decision — detail` off the last
|
|
28
|
+
escalation, so a run's actual failure text (a TLS "bad record mac", a specific mypy error)
|
|
29
|
+
rides onto the report instead of just the generic per-category decision prose. A step id that
|
|
30
|
+
recurs after a replan is split per OCCURRENCE, never merged into one pooled row. `--all <dir>`
|
|
31
|
+
is name-agnostic (a spine is identified by a `job-start`/`run-start` record in its content,
|
|
32
|
+
never a filename convention) and prints a fixed-column, ALIGNED table (never CSV) — `id job
|
|
33
|
+
shape outcome spend wall steps reason`, `reason` built from `detail` only (never `decision`,
|
|
34
|
+
cut at 60 chars) — flagging any non-spine `.jsonl` as `<name> not-a-spine` rather than dropping
|
|
35
|
+
it. A RESUMED run's `job-end.spentUsd` is a CHAIN total (prior legs folded in) that can never
|
|
36
|
+
equal this one file's own rounds (measured: `u-msf70nei` header `$5.3389` vs its own 55 rounds
|
|
37
|
+
summing to `$1.2127`) — the header now prints both, each labelled, plus a `resumed:` line
|
|
38
|
+
(keyed on `job-start.priorSpentUsd`, never on the narrower `resume-seed` record alone — 4 real
|
|
39
|
+
archived runs carry a real fold with no resume-seed record at all). A non-resumed run's own
|
|
40
|
+
spend disagreeing with its summed rounds is a genuine finding and prints as a `MISMATCH`, never
|
|
41
|
+
hidden (measured: 0 of the archive's non-resumed runs trip it). `--all` marks a resumed row
|
|
42
|
+
with a trailing `*` and one footnote line when the listing holds any. Reuses `runBehaviour`/the
|
|
43
|
+
`memory-cache` readout rather than reimplementing either. Reads only, mints no verdict, writes
|
|
44
|
+
nothing to the spine. `job-start` (`src/run.js`) now also carries `verdictType` (the spec's
|
|
45
|
+
required verdict class) and `model` (when the shell-owned provider binding itself carries one)
|
|
46
|
+
— PRD TODO #20, blessed verbatim by hamr the same day. The header prints `class:`/`model:`
|
|
47
|
+
right after `shape:` (`not recorded (pre-F117 spine)` / `not recorded` on an older archive or
|
|
48
|
+
a provider with no `.model`), and `--all` gains `class`/`model` columns (`model` kept FULL,
|
|
49
|
+
never truncated).
|
|
50
|
+
- **Prompt-commit shape — a check, not convention** (PRD build-list #5, TODO #8, Q9 answered
|
|
51
|
+
by hamr 2026-08-25): a commit that changes a prompt register (a model-facing
|
|
52
|
+
system/strategy/instruction string a worker or judge reads) must now say, in its own
|
|
53
|
+
message, three labelled things — `Failure:` what caused the change, `Addresses:` what it
|
|
54
|
+
addresses, `Corrects:` what it corrects. `src/promptregisters.js` (`PROMPT_REGISTERS`,
|
|
55
|
+
`isPromptFile`, both exported from `bareloop`'s root) is the ONE inventory: 7 files carry
|
|
56
|
+
real prompt content (`src/authorscout.js`, `src/authorflow.js`, `src/judged.js`,
|
|
57
|
+
`src/cardauthor.js`, `src/readshim.js`, `src/tools.js`, `src/planrun.js`) — more than double
|
|
58
|
+
the 8 named consts a first grep found, once module-private consts and inline
|
|
59
|
+
template-literal prompt builders (`scoutPrompt()`, `cardCasesPrompt()`, `locatePrompt()`,
|
|
60
|
+
`strategyFor()`) are counted; the check is file-scoped precisely because a const-scoped one
|
|
61
|
+
would have silently missed those. Enforcement is LOCAL ONLY — wired into `npm test`
|
|
62
|
+
(`scripts/prompt-commit-check.mjs --range origin/main..HEAD`), never into any
|
|
63
|
+
`.github/workflows/*` file (ask-first, untouched); an unresolvable range (no `origin`, no
|
|
64
|
+
fetch) is a printed SKIP at exit 0, never a violation. Pure decision logic lives in
|
|
65
|
+
`scripts/promptcommitlib.mjs`, neither of which ships (`scripts/` is outside
|
|
66
|
+
`package.json`'s `files`). hamr's same-day addition: the `Failure:` line must also cite
|
|
67
|
+
the RUN that caused the change (`run <id>` / `run u-<id>`, `\brun\s+(u-)?[a-z0-9]{6,12}\b`
|
|
68
|
+
— alphabet/length measured against 231 real archived spine ids, every one exactly 8
|
|
69
|
+
lowercase-alnum chars), failing with a distinct message
|
|
70
|
+
(`FAILURE_NEEDS_RUN_REF`) when absent; format only, never checked against a real spine
|
|
71
|
+
file.
|
|
72
|
+
- **Run→code stamp** (F118, the parked half of the same day's prompt-commit build, closed
|
|
73
|
+
same day on hamr's go): the prompt-commit check closes commit→run; this closes run→code.
|
|
74
|
+
`src/codeversion.js`'s `codeVersion()` is a pure, $0, no-shell reader — `version` from
|
|
75
|
+
bareloop's own `package.json`, `sha` from `.git/HEAD` (loose ref, falling back to
|
|
76
|
+
`packed-refs`) ONLY when a `.git` directory exists at the package root (a dev checkout;
|
|
77
|
+
an npm install has none), `dirty` always `null` (uncommitted-changes state cannot be
|
|
78
|
+
known without a shell — reported as unknown, never faked as `false`). `job-start`
|
|
79
|
+
(`src/run.js`) now also carries `code: {version, sha}`. `replayRun`/`formatReplay`
|
|
80
|
+
(`src/replay.js`) read it: a `code:` line right after `model:` — `bareloop 0.14.0 @
|
|
81
|
+
43f2812` / `@ sha unknown` / `not recorded (pre-F118 spine)`; no new `--all` column
|
|
82
|
+
(hamr's index is already full). The library never grows a shell seam to answer this —
|
|
83
|
+
`run` stays the one locked verb.
|
|
84
|
+
- **Replay surfaces transport retries and floor reasons** (F119, live-proven the same day
|
|
85
|
+
a real run — `u-mt8yk53k` — fired the retry seam twice): `replayRun`/`formatReplay`
|
|
86
|
+
window `transport-retry` records into the step/iteration occurrence they fell inside
|
|
87
|
+
(same `seq` windowing as `worker-round`), printing `↳ transport retry ×N (recovered|not
|
|
88
|
+
recovered|partially recovered) — <error>` under that row; a retry outside every window
|
|
89
|
+
prints as `transport retries outside steps: N` under the `TIMELINE` header instead. The
|
|
90
|
+
header's `spent:` line, whenever `spendComplete:false`, now appends `· floor because:
|
|
91
|
+
<reason(s)>` — derived from the spine, never guessed: `transport retry ×N`, `unpriced
|
|
92
|
+
round(s)`, `cut mid-call`, `stall`, `prior leg floor`, every cause that shows real
|
|
93
|
+
evidence, or `floor (reason not in spine)` when none can be derived. `--all` adds no
|
|
94
|
+
column: a run carrying any retry gets ` ⟲N` appended to its outcome word (same posture
|
|
95
|
+
as the resumed `*`), with one footnote when the listing holds any. Report-only — reads
|
|
96
|
+
existing `transport-retry`/`wall-halt`/`stall`/`job-start` records, writes nothing.
|
|
97
|
+
|
|
98
|
+
### Fixed
|
|
99
|
+
|
|
100
|
+
- **Leaked tmp dirs from `npm test`** (same class that filled `/tmp` tmpfs on 2026-08-25 and
|
|
101
|
+
blocked a build): `tests/codeversion.test.js`'s real-worktree test tracked only the `wt`
|
|
102
|
+
subdirectory passed to `git worktree remove`, never the `mkdtemp`-created parent, so every run
|
|
103
|
+
left an empty `codeversion-worktree-*` dir behind; its `after()` now also prunes stale worktree
|
|
104
|
+
metadata when `remove` fails and `rmSync`s the tracked dir unconditionally, plus the mkdtemp
|
|
105
|
+
parent is now itself tracked and removed. `tests/ralph.test.js`'s cwd-threading test created a
|
|
106
|
+
`ralph-cwd-*` repo with no cleanup at all, and its cwd-relative-close test likewise leaked a
|
|
107
|
+
`close-cwd-*` and a `close-cwd-other-*` dir per run; added a per-test `after(() => rmSync(...))`
|
|
108
|
+
for each, matching the file's existing top-level cleanup pattern.
|
|
109
|
+
- **`codeversion.test.js` false-red on PR checkouts** (F118 addendum): the HEAD-sha test's own
|
|
110
|
+
precondition assumed a symbolic ref, but `actions/checkout` leaves a PR checkout's HEAD
|
|
111
|
+
DETACHED at the merge commit, so the precondition threw before the module under test ever
|
|
112
|
+
ran — `src/codeversion.js` itself already handled the detached case correctly. Test-only fix:
|
|
113
|
+
the precondition now resolves a bare 40-hex `.git/HEAD` the same way `codeVersion()` does, plus
|
|
114
|
+
a new deterministic fake-package-root test that exercises the detached case without depending
|
|
115
|
+
on the checkout's own HEAD state.
|
|
116
|
+
- **Review of PR #23 (2026-08-26), 7 items fixed**: (1) `replayRun`'s spend totals
|
|
117
|
+
(`src/replay.js`) counted only `worker-round`, silently dropping a close's own `judge-round`
|
|
118
|
+
spend and native `worker-turn` attribution out of every total — now filters on the canonical
|
|
119
|
+
`SPEND_RECORD_TYPES` (newly exported from `src/ledger.js`), including the per-step window,
|
|
120
|
+
which no longer requires a `.phase` match for records (like `judge-round`) that never carry
|
|
121
|
+
one. (2) `scripts/promptcommitlib.mjs`'s run-reference regex accepted plain prose ("run
|
|
122
|
+
failed" satisfied its `{6,12}` margin outright) — tightened to the measured exact shape
|
|
123
|
+
(`{8}`); a proposed additional per-id digit requirement was investigated and REJECTED (23 of
|
|
124
|
+
130 re-sampled real archived ids carry no digit, `msdsmkid` among them). (3)
|
|
125
|
+
`src/codeversion.js` read `sha: null` inside any real git worktree (`.git` there is a FILE,
|
|
126
|
+
`gitdir: <path>`, not a directory) — now resolves the pointer file and the worktree's
|
|
127
|
+
`commondir` to find HEAD/refs correctly. (4) `replayRun`'s `resumed` flag missed a genuine
|
|
128
|
+
$0-floor resume (`priorSpentUsd: 0` is still a real fold per `src/run.js:328`'s own doctrine)
|
|
129
|
+
— now keys on the field's presence, not `> 0`. (5) `scripts/run-replay.mjs --all` opened
|
|
130
|
+
every run's gate-audit sidecar despite never using the fields it feeds — no longer opens it
|
|
131
|
+
in `--all` mode (`--all` output on `aurora-u-bareloop` confirmed byte-identical before/after).
|
|
132
|
+
(6) the steps/iterations windowing logic in `replayRun` was duplicated inline twice — extracted
|
|
133
|
+
into one shared `buildOccurrenceMetrics` helper (replay output on `u-msdsmkid`, `u-msf70nei`,
|
|
134
|
+
`u-mt8yk53k` confirmed byte-identical before/after). (7) `npm test`'s prompt-commit check
|
|
135
|
+
(`origin/main..HEAD`) inspects NOTHING after a direct push to main, because the push itself
|
|
136
|
+
advances `origin/main` to match `HEAD` — `scripts/prompt-commit-check.mjs` now falls back to
|
|
137
|
+
`HEAD~1..HEAD` on that exact condition and prints that it did; a multi-commit direct push
|
|
138
|
+
still only re-covers the last commit (needs `github.event.before`, a `.github/` edit — out of
|
|
139
|
+
scope here, ask-first).
|
|
140
|
+
- **Release-gate findings (2026-08-27)**: `scripts/prompt-commit-check.mjs`'s `--range` value
|
|
141
|
+
was passed to `git rev-list` positionally with no way to disambiguate it from an option — an
|
|
142
|
+
option-shaped value (e.g. `--output=<path>`) was parsed by git as its own flag (verified: a
|
|
143
|
+
`--` separator does NOT fix `rev-list`, unlike `show`/`diff-tree`, since it treats everything
|
|
144
|
+
after it as a path filter, not a revision range); the script now rejects any `--range` value
|
|
145
|
+
starting with `-` up front, before any git call runs. Also corrected the fallback's printed
|
|
146
|
+
limitation and code comment: on a MERGE commit, `HEAD~1` is the first parent, so
|
|
147
|
+
`HEAD~1..HEAD` covers the merge PLUS every commit unique to the merged branch (measured on
|
|
148
|
+
this repo's own `4904d76`: 10 commits, not 1) — the prior wording ("only re-covers the last
|
|
149
|
+
commit") was only accurate for a LINEAR multi-commit push, which is now stated explicitly.
|
|
150
|
+
(`src/replay.js`'s phase-less step-window filter was also reviewed and confirmed correct by a
|
|
151
|
+
new adversarial synthetic test — no code change there.)
|
|
152
|
+
|
|
10
153
|
## [0.14.0] — 2026-08-25
|
|
11
154
|
|
|
12
155
|
### Added
|
package/bareloop.context.md
CHANGED
|
@@ -2142,6 +2142,318 @@ is not a spend record and no spend-slicing instrument needs to account for it.
|
|
|
2142
2142
|
(`no record` if the run ended before the summary could fire); `scripts/behaviour-readout.mjs`
|
|
2143
2143
|
prints the same line when handed the run's spine file as its optional third positional.
|
|
2144
2144
|
|
|
2145
|
+
|
|
2146
|
+
### `replayRun(spineEvents, auditEvents?, { runId? })` / `formatReplay(summary)` — `src/replay.js`
|
|
2147
|
+
|
|
2148
|
+
PRD build-list item 6: today no failed run can be reconstructed in under five minutes — every
|
|
2149
|
+
byte is on disk (the spine JSONL + its gate-audit sidecar) but only hand-slicing JSONL reads it.
|
|
2150
|
+
`replayRun` is a report-only reader over BOTH files: **reads only, mints no verdict, writes
|
|
2151
|
+
NOTHING to the spine**, same posture as `runBehaviour` above, which it reuses rather than
|
|
2152
|
+
reimplements for the tool-call breakdown. It returns one plain object:
|
|
2153
|
+
|
|
2154
|
+
```
|
|
2155
|
+
{ runId, job, goal, budgetUsd, specHash, branch, verdictType, model, code,
|
|
2156
|
+
outcome, stopReason, spentUsd, spendComplete, wallMs, chainClock,
|
|
2157
|
+
resumed, resumeSeed, thisFileSpend, spendMismatch,
|
|
2158
|
+
timelineKind: 'steps'|'iterations', replans, close,
|
|
2159
|
+
steps: [{ id, occurrence, outcome, rounds, toolCalls, checks: {passed, failed},
|
|
2160
|
+
treeChanged, wallMs, spentUsd, unpricedRounds, tripped }],
|
|
2161
|
+
iterations: [{ iteration, verdict, boundary, closeStage, rounds, toolCalls,
|
|
2162
|
+
wallMs, spentUsd, unpricedRounds, tripped }],
|
|
2163
|
+
ending: { record, before /* the 3 records before it */, escalation },
|
|
2164
|
+
lastEscalation, behaviour, memoryCache, skipped }
|
|
2165
|
+
```
|
|
2166
|
+
|
|
2167
|
+
**`verdictType`/`model`** (F117, PRD TODO #20 — hamr blessed the whole item verbatim, landed
|
|
2168
|
+
2026-08-25): `verdictType` reads `job-start.verdictType` (`src/run.js:303`) — a REQUIRED spec
|
|
2169
|
+
field (`src/job.js:552` reds `missing-required` before job-start ever fires), so `null` here
|
|
2170
|
+
means only "this spine predates the field" (measured: `u-msdsmkid`, an Aug-3 archived run),
|
|
2171
|
+
never "unset". `model` reads `job-start.model`, present only when the shell-owned provider
|
|
2172
|
+
binding itself carried a `.model` string at job-start time — `src/planrun.js:76`'s own comment
|
|
2173
|
+
documents bare-agent's Loop reading `baseProvider.model` directly off that same object, so it is
|
|
2174
|
+
a real, citable, synchronously-available property, not a guess; a native/clipipe call path (or
|
|
2175
|
+
any provider double with no such field — measured on `tests/helpers.js`'s `scriptedProvider`)
|
|
2176
|
+
legitimately has neither, and this reader reports that honestly rather than defaulting one in.
|
|
2177
|
+
`formatReplay` prints both right after `shape:` — `class: softgreen` / `class: not recorded
|
|
2178
|
+
(pre-F117 spine)`, `model: claude-sonnet-5` / `model: not recorded`
|
|
2179
|
+
(the two "not recorded" wordings are deliberately different: a missing `verdictType` is always
|
|
2180
|
+
an archive-age gap, a missing `model` can happen on any spine, old or new). `--all` gains a
|
|
2181
|
+
`class` column right after `shape` and a `model` column right after `class` (the verdict class /
|
|
2182
|
+
the model string, or `-`) — `model` is kept FULL, never truncated, unlike the compact `reason`
|
|
2183
|
+
column: a wrong or uncertain model reading is exactly the thing a directory-wide scan needs in
|
|
2184
|
+
full, not cut.
|
|
2185
|
+
|
|
2186
|
+
**`code`** (F118, the run→code direction — the commit→run direction is the prompt-commit
|
|
2187
|
+
check's `Failure:` run-reference rule; the two are companion halves of one loop, built the
|
|
2188
|
+
same day). `codeVersion()` (`src/codeversion.js`) is a pure, $0, no-shell reader: `version`
|
|
2189
|
+
is bareloop's own `package.json` version (resolved off `import.meta.url`, never a hardcoded
|
|
2190
|
+
path — works whether this module is imported from `src/` or from an installed
|
|
2191
|
+
`node_modules/bareloop`); `sha` is read straight off `.git/HEAD` (following a symbolic ref
|
|
2192
|
+
to its loose ref file, falling back to `packed-refs` when the branch has been packed) **only
|
|
2193
|
+
when `.git` is a real directory at the package root** — a dev checkout has one, an
|
|
2194
|
+
npm-installed copy does not, and that absence reads as `sha: null`, honestly, never a guess.
|
|
2195
|
+
`dirty` is always `null`: whether the tree has uncommitted changes cannot be known without a
|
|
2196
|
+
shell, and this module NEVER shells out — `run` is the one locked verb (hamr's law targets
|
|
2197
|
+
what the agent can DO, not how it's written), and a report-only field is not a license for
|
|
2198
|
+
the library to grow a shell seam. `null` is reported here rather than a defaulted `false`,
|
|
2199
|
+
because a faked "clean" reading is a worse lie than an honest "don't know."
|
|
2200
|
+
|
|
2201
|
+
`job-start` (`src/run.js:303`) carries `code: {version, sha}` alongside F117's
|
|
2202
|
+
`verdictType`/`model` (`dirty` is intentionally omitted from the spine — it is always
|
|
2203
|
+
`null`, and a spine field that can only ever hold one value is not worth a byte).
|
|
2204
|
+
`replayRun` reads it as `{version, sha}` or `null` (absent field, an archive older than this
|
|
2205
|
+
landing); `formatReplay` prints a `code:` line right after `model:` — `code: bareloop
|
|
2206
|
+
0.14.0 @ 43f2812` on a spine with a real sha, `code: bareloop 0.14.0 @ sha unknown` when
|
|
2207
|
+
`sha` is null (an npm-installed run), `code: not recorded (pre-F118 spine)` when the
|
|
2208
|
+
whole field is absent. `--all` gains no new column for this — the row is already at hamr's
|
|
2209
|
+
named limit.
|
|
2210
|
+
|
|
2211
|
+
**Unknown money/time is always `null`/`unknown` — never `0`, and a partial sum is never stamped
|
|
2212
|
+
exact** (F6's rule, carried through every level this module reports at): `spentUsd`/`wallMs` are
|
|
2213
|
+
`null` when the run's own `job-end` is absent or carries no figure; `spendComplete` reads `false`
|
|
2214
|
+
whenever `spentUsd` is `null`, regardless of what the record's own flag says. The SAME rule
|
|
2215
|
+
governs one occurrence/iteration's own `spentUsd`: it sums `worker-round.costUsd`
|
|
2216
|
+
(`src/planrun.js:2288-2297`) over records strictly inside that occurrence's own `seq` window —
|
|
2217
|
+
a single `null` `costUsd` anywhere in the window makes the WHOLE occurrence's `spentUsd` `null`
|
|
2218
|
+
(`unpricedRounds` names how many), never a partial sum. Zero rounds in the window is a real `$0`,
|
|
2219
|
+
not an unknown one.
|
|
2220
|
+
|
|
2221
|
+
`stopReason` is `null` on a green/already-green outcome; otherwise `category — decision —
|
|
2222
|
+
detail` from the last `escalation` record, every part present that record actually carries
|
|
2223
|
+
(`category` dropped when it duplicates the outcome already shown beside it; `detail` trimmed at
|
|
2224
|
+
400 chars with an explicit `…[+N chars]` marker, never silent) — `decision` alone is the generic
|
|
2225
|
+
one-line prose every category shares ("the provider path failed mid-run…" fires for every
|
|
2226
|
+
transport casualty); **`detail` is what actually killed THIS run** (a TLS "bad record mac", a
|
|
2227
|
+
specific mypy error) and is the reason a stop is explained, never the decision prose alone.
|
|
2228
|
+
Falls back to the bare outcome string for a pre-escalation red (`plan-red`/`job-red`/
|
|
2229
|
+
`smoke-red`). `ending.escalation` carries that same last escalation record whenever it falls
|
|
2230
|
+
outside the fixed 3-before window (common on a run that keeps emitting bookkeeping records after
|
|
2231
|
+
its real failure) — never silently dropped. `lastEscalation` is the same raw record exposed
|
|
2232
|
+
unconditionally (not gated on the window check) — `summarizeForAllLine`'s `reason` column reads
|
|
2233
|
+
it directly rather than re-deriving from `ending`.
|
|
2234
|
+
|
|
2235
|
+
**A step id can recur** (a replan re-executes the same step): each `step-start`..`step-end` pair
|
|
2236
|
+
is its own **occurrence** (`occurrence: 1, 2, …`), and every field on it — `rounds`, `toolCalls`,
|
|
2237
|
+
`checks`, `treeChanged`, `wallMs`, `spentUsd` — is scoped to records strictly between THAT pair's
|
|
2238
|
+
own `seq` (the spine's monotonic per-record counter, never `ts` — every record in one spine
|
|
2239
|
+
shares the same seq counter, so it is an exact ordering where two same-millisecond `ts` stamps
|
|
2240
|
+
are not), never pooled across every occurrence sharing the id. `rounds` counts real
|
|
2241
|
+
`worker-round` records (`phase === 'step:<id>'`), never the declared cap; `toolCalls` is the one
|
|
2242
|
+
thing still windowed by `ts` (the gate-audit carries no `step` or shared `seq`). `tripped` is the
|
|
2243
|
+
last `escalation` whose `seq` falls inside the occurrence's window — `{category, detail}`, with
|
|
2244
|
+
`detail` falling back to `decision` when the escalation site carries no `detail` at all (a
|
|
2245
|
+
ladder-exhaustion `cap-halt`, `src/ralph.js`, is the measured example — without the fallback the
|
|
2246
|
+
tripped line would print a bare category and hide the real reason).
|
|
2247
|
+
|
|
2248
|
+
**A run whose spine carries no `step-start` at all** — the plan's only step was
|
|
2249
|
+
already-green-skipped on a resumed leg, or a genuinely loop-only shape (measured: `u-msf70nei`,
|
|
2250
|
+
a resumed leg whose one step was already proven and only the CLOSE-FIX loop ran) — has
|
|
2251
|
+
`timelineKind: 'iterations'` and `iterations[]` populated from `iteration-start`
|
|
2252
|
+
(`src/ralph.js:583`) instead: each row runs from one `iteration-start` to the next
|
|
2253
|
+
`close-verdict`/`run-end`/`escalation` (`src/ralph.js:674,577/657/676/698`), whichever comes
|
|
2254
|
+
first by `seq`. `steps`/`iterations` are never both populated — exactly one is non-empty. Each
|
|
2255
|
+
iteration's own `closeStage` (`{verdict, stages}`) is set when a `close-verdict` ended it, distinct
|
|
2256
|
+
from the run's FINAL `close` (below).
|
|
2257
|
+
|
|
2258
|
+
**`close`** is the run's real, final close — resolved by picking the LAST (by `seq`) record
|
|
2259
|
+
among every `close-verdict` that carries an ARRAY `stages` field and every `outer-close`
|
|
2260
|
+
(`src/planrun.js:3296`). A `close-verdict` with NO `stages` came from a PLAN STEP's own
|
|
2261
|
+
micro-loop, judged by the exit evaluator instead of a command (`src/planrun.js:2907`) — never
|
|
2262
|
+
"the close". Picking by `seq` rather than preferring `outer-close`'s TYPE matters: `outer-close`
|
|
2263
|
+
is the PRECHECK against the real close, fired once before the fix loop even starts, so when it
|
|
2264
|
+
reads red the fix loop's own staged close-verdicts run strictly AFTER it — measured on
|
|
2265
|
+
`u-msf70nei`, whose `outer-close` (seq 14) reads `needs_revision` but whose fix loop's own final
|
|
2266
|
+
close-verdict (seq 97, LATER) reads `satisfied`, which is what the run actually ended on
|
|
2267
|
+
(`job-end.outcome:'green'`). A run that never reached ANY close (e.g. `u-msdsmkid`, stopped by a
|
|
2268
|
+
step-variance meter before `judgeClose()` ever fired) resolves `close: null`, printed as `none —
|
|
2269
|
+
the run ended before any close ran` — never omitted.
|
|
2270
|
+
|
|
2271
|
+
`replans` prefers `plan-executed.replans` (`src/planrun.js:2688`) and falls back to counting
|
|
2272
|
+
`materials` records stamped `phase:'replan'` (`src/planrun.js:2520`) when that field is absent —
|
|
2273
|
+
an older archived spine can predate the field (measured on `u-msdsmkid`, an Aug-3 run). `branch`
|
|
2274
|
+
reads the last `work-branch.branch` (`src/planrun.js:1589`) or `null` when absent — also measured
|
|
2275
|
+
missing on the same era of archive, before the work-branch rung landed.
|
|
2276
|
+
|
|
2277
|
+
`wallMs` is THIS FILE's own `job-start`→`job-end` span, labeled `(this file)` — on a RESUMED leg
|
|
2278
|
+
that is only the latest engagement, not the whole signed chain. `chainClock` is the other true
|
|
2279
|
+
number: the SIGNED wall read off the run's own clock record — `wall-halt` when present (the
|
|
2280
|
+
TERMINAL reading, taken at the moment the wall stopped the run; `src/planrun.js:1269`'s
|
|
2281
|
+
`emitWallHalt`), else the single `wall-clock` record every run emits near its start
|
|
2282
|
+
(`src/planrun.js:1203`) — both spread from `clock.report()` (`src/clock.js:235-247`: `bounded,
|
|
2283
|
+
requestedMs, closeStages, enforcedMs, elapsedMs, remainingMs`). `elapsedMs` on either is
|
|
2284
|
+
chain-scoped by construction (`createClock`'s `priorElapsedMs` fold, `src/clock.js:174`), so it
|
|
2285
|
+
can legitimately exceed this file's own `wallMs` on a resumed run. `wall-halt`'s reading is
|
|
2286
|
+
end-of-run-honest, so `formatReplay` states it plainly: `clock: 60m04s of 60m00s signed (chain,
|
|
2287
|
+
from wall-halt)`. `wall-clock`'s reading is taken at the TOP of the engagement (chain time
|
|
2288
|
+
already inherited, not this run's own duration) — printing that under the same template read as
|
|
2289
|
+
"this run took 0 seconds" on a cold run, so it gets its own honest wording instead: `clock: 30m00s
|
|
2290
|
+
signed · 0.0s inherited from prior legs at start (from wall-clock; no end-of-run reading in this
|
|
2291
|
+
file)`.
|
|
2292
|
+
|
|
2293
|
+
**Money carries the identical split** (hamr, 2026-08-25): a RESUMED run's `job-end.spentUsd` is
|
|
2294
|
+
the CHAIN total (prior legs folded in — `src/run.js:227`'s `chainFoldUsd`, seeded from
|
|
2295
|
+
`job-start.priorSpentUsd`), which can never equal this ONE file's own rounds — measured on
|
|
2296
|
+
`u-msf70nei`: header `spentUsd` reads `$5.3389` while its own 55 `worker-round`s sum to exactly
|
|
2297
|
+
`$1.2127`. `resumed` reads `job-start.priorSpentUsd > 0` — the SAME condition `src/run.js` itself
|
|
2298
|
+
folds on — deliberately NOT `resume-seed`'s presence: `resume-seed` (`src/planrun.js:2622`) is a
|
|
2299
|
+
NARROWER record, fired only when a plan is actually reloaded mid-flight, and its absence does not
|
|
2300
|
+
mean "not resumed" — measured directly on 4 real archived runs (`litectx-maintainer-bareloop`'s
|
|
2301
|
+
`msx7xoe0`/`msx87qqs`/`msxf9129`/`msxf2mwi`) that carry a real `priorSpentUsd` fold with NO
|
|
2302
|
+
resume-seed record at all. `resumeSeed` (when present) supplies the DETAIL —
|
|
2303
|
+
`{phase, completed, skipping, divergence}` — printed as `resumed: yes — phase close, 1 of 1
|
|
2304
|
+
completed step(s) skipped`; a resumed leg with no resume-seed record still reads `resumed: yes
|
|
2305
|
+
(no resume-seed detail recorded)`, honestly, rather than falling back to `no`. `thisFileSpend`
|
|
2306
|
+
prefers `job-end.engagementSpentUsd` (F103, `src/run.js:284` — this engagement's own fold-out
|
|
2307
|
+
spend) when the record carries it, falling back to summing this file's own `worker-round.costUsd`
|
|
2308
|
+
when it does not (measured absent on `u-msf70nei` itself); an unpriced round anywhere in THIS
|
|
2309
|
+
FILE makes the figure `null` regardless of source (F6, never a partial sum stamped exact) — both
|
|
2310
|
+
figures print on the `spent:` line, each labeled with its source, never one silently standing in
|
|
2311
|
+
for the other. `spendMismatch` is the same-class check for a NON-resumed run: there is no fold to
|
|
2312
|
+
explain a gap, so `job-end.spentUsd` disagreeing with this file's own summed rounds by more than
|
|
2313
|
+
$0.0001 is a genuine finding, printed as `MISMATCH vs job-end`, never hidden — measured across
|
|
2314
|
+
every non-resumed run in the available archive (aurora-u, bareagent-u, litectx-maintainer): zero
|
|
2315
|
+
trip it. `--all`'s spend column stays the chain total (what a directory scan asks first) but
|
|
2316
|
+
marks a resumed row with a trailing `*` on its id, with one footnote line
|
|
2317
|
+
(`* resumed run — spend is the chain total, see detail`) printed once, only when the listing
|
|
2318
|
+
holds at least one such row.
|
|
2319
|
+
|
|
2320
|
+
`formatReplay` renders the full page: a header block (`goal:`/`shape:`/`signed:`/`branch:`/
|
|
2321
|
+
`resumed:`/`outcome:`/`spent:`, every absent field printing the literal `none recorded` — never
|
|
2322
|
+
blank, never a fabricated `0`), a `TIMELINE (steps)` or `TIMELINE (iterations — loop-shape run)` section with
|
|
2323
|
+
its own column-header row (`# step result rounds tools checks tree wall spend`, or the
|
|
2324
|
+
iteration equivalent without `checks`/`tree`) and a `↳ tripped:`/`↳ close:` line under any row an
|
|
2325
|
+
escalation or close fell inside, a `CLOSE` section (never omitted — `none — the run ended before
|
|
2326
|
+
any close ran` when nothing ever closed; the failing stage, if any, prints FIRST with `✗`, the
|
|
2327
|
+
rest `name OK`), `ENDING` (last record + 3 before it, plus an out-of-window escalation), the
|
|
2328
|
+
`BEHAVIOUR` block, and a `MEMORY-CACHE` line **always printed** — the existing wording when the
|
|
2329
|
+
run carried a record, `not armed on this run` otherwise. An absent record is never silent.
|
|
2330
|
+
`RESULT` words are `GREEN`/`RED`/`UNKNOWN` — `satisfied`/`green`/`already-green` read GREEN,
|
|
2331
|
+
every other non-null value reads RED.
|
|
2332
|
+
|
|
2333
|
+
`summarizeForAllLine(summary)` builds one `--all` row's raw fields (`id, job, shape, outcome,
|
|
2334
|
+
spend, wall, steps, reason`) — `shape` is `plan`/`loop` (from `timelineKind`, never the
|
|
2335
|
+
always-literal-`'plan'` `job-start.shape` field); `steps` is a bare count for a plan-shape run,
|
|
2336
|
+
`"N it"` for a loop-shape one; `reason` is `category — <first clause of detail, cut at 60 chars,
|
|
2337
|
+
plain "…", no [+N] marker>` off `lastEscalation` — **`detail` only, never `decision`** for this
|
|
2338
|
+
column specifically (unlike the full report's `↳ tripped:` line, which does fall back to
|
|
2339
|
+
`decision` — this compact row has no room for both and the coordinator's own ruling keeps
|
|
2340
|
+
`decision`'s generic per-category prose out of it); `-` on a green row; the distinct, literal
|
|
2341
|
+
`no job-end (killed mid-run)` when the spine never reached a `job-end` at all (`outcome` itself
|
|
2342
|
+
is `null`) — not folded into the ordinary escalation case. `formatAllLines(entries)` renders every
|
|
2343
|
+
row as a FIXED-COLUMN, ALIGNED table with a header row (never CSV — columns are padded, not
|
|
2344
|
+
delimited; the header's own label lengths count toward the column widths) plus any
|
|
2345
|
+
`<name> not-a-spine` lines at their original position (unpadded — a filename carries no
|
|
2346
|
+
outcome/spend/wall of its own). No header prints when there are zero real spines in the listing.
|
|
2347
|
+
|
|
2348
|
+
`scripts/run-replay.mjs <spine.jsonl>` resolves the sibling `-gate-audit.jsonl` automatically and
|
|
2349
|
+
prints the full report; `--all <dir>` is **name-agnostic** — the patient corpus does not agree on
|
|
2350
|
+
one filename convention (`u-<id>.jsonl`, `battery-A1-<id>.jsonl`, `l2accept-L1-<id>.jsonl`,
|
|
2351
|
+
`reuse-<id>.jsonl`, `types-screen-C-<id>.jsonl`, …), so a spine is identified by CONTENT: any
|
|
2352
|
+
`.jsonl` that is not a sidecar by name (`*-gate-audit.jsonl`, `*.lag.jsonl`) and whose records
|
|
2353
|
+
include a `job-start` (present on every real spine checked, nested wrappers included) or
|
|
2354
|
+
`run-start` (present on most, absent on a run that died before ralph's first iteration). A
|
|
2355
|
+
`.jsonl` matching neither is printed as `<name> not-a-spine` — never silently dropped or
|
|
2356
|
+
misread as an empty run. Both modes mirror `scripts/behaviour-readout.mjs`'s own malformed-line
|
|
2357
|
+
handling (skip-and-count, never throw).
|
|
2358
|
+
|
|
2359
|
+
**F117's PRD TODO #20 is now landed in full**: both the verdict CLASS and the worker MODEL
|
|
2360
|
+
name are surfaced (see `verdictType`/`model` above) — hamr blessed the item verbatim
|
|
2361
|
+
("1") on 2026-08-25, so the spine-WRITER change to `job-start` (`src/run.js`) that a
|
|
2362
|
+
report-only reader would not otherwise make on its own authority went in with his explicit
|
|
2363
|
+
word. `judgedCount` on a close-verdict was never a reliable stand-in for the class either way
|
|
2364
|
+
(it appears on plain green closes too, not only softgreen ones) — this reads the real
|
|
2365
|
+
declared field instead.
|
|
2366
|
+
|
|
2367
|
+
**Transport retries and floor reasons (F119, live-proven 2026-08-25)**: every
|
|
2368
|
+
`transport-retry` record (`src/planrun.js:126`) whose `seq` falls inside a step's or
|
|
2369
|
+
iteration's own window prints a `↳ transport retry ×N (recovered|not recovered|partially
|
|
2370
|
+
recovered) — <first 80 chars of the error>` line under that row — the same open-interval
|
|
2371
|
+
`seq` windowing `windowSpend` already uses for rounds. A retry whose `seq` falls outside
|
|
2372
|
+
every occurrence window (e.g. during the scout/plan phase) is counted instead under the
|
|
2373
|
+
`TIMELINE` header as `transport retries outside steps: N`, printed only when that count is
|
|
2374
|
+
non-zero. The header's `spent:` line, whenever `spendComplete` is `false`, appends
|
|
2375
|
+
`· floor because: <reason(s)>` — derived from the spine, never guessed: `transport retry
|
|
2376
|
+
×N` (any `transport-retry` record), `unpriced round(s)` (a `worker-round`/`judge-round`
|
|
2377
|
+
with non-finite `costUsd`), `cut mid-call` (a `wall-halt` record with `cutMidCall:true`),
|
|
2378
|
+
`stall` (a `stall` record), `prior leg floor` (`job-start.priorSpendComplete === false`) —
|
|
2379
|
+
every cause that actually shows evidence prints, never just one; a floor with no derivable
|
|
2380
|
+
evidence on this spine (an older archive, or a resumed leg's floor inherited from a file not
|
|
2381
|
+
in hand) prints `· floor (reason not in spine)` instead. `--all` adds no new column: a run
|
|
2382
|
+
that carried any transport retry gets ` ⟲N` appended directly after its outcome word (same
|
|
2383
|
+
posture as the resumed `*` on `id`), with one footnote
|
|
2384
|
+
(`⟲N transport retry(ies), recovered inline — see the full replay for detail`) printed once
|
|
2385
|
+
when the listing holds at least one such row. Live-proven on `u-mt8yk53k` (F119): two
|
|
2386
|
+
recovered TLS `bad record mac` retries, both inside step `fix-mypy-strict`'s window —
|
|
2387
|
+
`floor because: transport retry ×2` and `green ⟲2` in `--all`, on a run that finished green.
|
|
2388
|
+
|
|
2389
|
+
```js
|
|
2390
|
+
import { replayRun, formatReplay } from 'bareloop';
|
|
2391
|
+
const spine = fs.readFileSync(spineFile, 'utf8').trim().split('\n').map(JSON.parse);
|
|
2392
|
+
const audit = fs.readFileSync(auditFile, 'utf8').trim().split('\n').map(JSON.parse);
|
|
2393
|
+
console.log(formatReplay(replayRun(spine, audit, { runId: 'mszcthk1' })));
|
|
2394
|
+
```
|
|
2395
|
+
|
|
2396
|
+
### `PROMPT_REGISTERS` / `isPromptFile(path)` — `src/promptregisters.js`, and the prompt-commit rule
|
|
2397
|
+
|
|
2398
|
+
PRD build-list item 5 (TODO #8), Q9 answered (hamr, 2026-08-25): "a check". A commit that
|
|
2399
|
+
changes a **prompt register** — one of the model-facing system/strategy/instruction
|
|
2400
|
+
strings a worker or judge actually reads (`SCOUT_SYSTEM`, `AUTHOR_SYSTEM`,
|
|
2401
|
+
`PERSONA_TOOLS`, `READ_SHIM_STRATEGY`, and their neighbors — the full, verified list is
|
|
2402
|
+
`PROMPT_REGISTERS` in `src/promptregisters.js`) — must say, in its own message, three
|
|
2403
|
+
things: what failure caused the change, what it addresses, and what it corrects.
|
|
2404
|
+
Convention-only was rejected: doctrine already on record is "a frozen rule without a
|
|
2405
|
+
wired detector is prose." `isPromptFile(path)` is the pure predicate the check runs
|
|
2406
|
+
against; both are exported from `bareloop`'s root so the inventory has exactly one home.
|
|
2407
|
+
|
|
2408
|
+
hamr's addition (2026-08-25, same day): the `Failure:` line must ALSO cite the **run**
|
|
2409
|
+
that caused the change — `run <id>` or `run u-<id>` (case-insensitive at the `run`
|
|
2410
|
+
keyword, multiple refs fine), matched by `\brun\s+(u-)?[a-z0-9]{8}\b` (tightened from an
|
|
2411
|
+
initial `{6,12}` margin after a PR #23 review found the margin loose enough to accept
|
|
2412
|
+
plain prose — "run failed" is 6 lowercase letters, satisfying `{6,12}` outright — to the
|
|
2413
|
+
measured exact shape: every real archived id is exactly 8 characters. A per-id digit
|
|
2414
|
+
requirement was proposed in that same review and rejected: 23 of 130 re-sampled real ids
|
|
2415
|
+
carry no digit at all, `msdsmkid` among them — a real archived run cited by name
|
|
2416
|
+
elsewhere in this repo — so requiring a digit would reject genuine citations). The commit
|
|
2417
|
+
points at the run; `replayRun`/`formatReplay` (above) close the loop the other way,
|
|
2418
|
+
turning that run id back into the whole story. A `Failure:` line with no run reference
|
|
2419
|
+
fails with a distinct message: `Failure: must cite the run that caused the change (e.g.
|
|
2420
|
+
"Failure: run mszcthk1 — ...")`. FORMAT ONLY — the check never verifies the cited run's
|
|
2421
|
+
spine file actually exists (a run's patient lives outside this repo entirely, so there is
|
|
2422
|
+
nothing here to check it against).
|
|
2423
|
+
|
|
2424
|
+
Enforcement is **local only** — wired into `npm test` (see `package.json`'s `test`
|
|
2425
|
+
script), never into `.github/workflows/*` (CI already runs `npm test`, so the rule is
|
|
2426
|
+
enforced there without an ask-first CI edit). The check itself lives outside the
|
|
2427
|
+
published package, at `scripts/prompt-commit-check.mjs` (pure decision logic in
|
|
2428
|
+
`scripts/promptcommitlib.mjs`, `scripts/` is not in `package.json`'s `files`, so none of
|
|
2429
|
+
this ships):
|
|
2430
|
+
|
|
2431
|
+
```
|
|
2432
|
+
node scripts/prompt-commit-check.mjs --range origin/main..HEAD # validates every commit
|
|
2433
|
+
# in the range that
|
|
2434
|
+
# touches a prompt file
|
|
2435
|
+
node scripts/prompt-commit-check.mjs --message-file .git/COMMIT_EDITMSG
|
|
2436
|
+
# commit-msg hook mode:
|
|
2437
|
+
# validates one message
|
|
2438
|
+
# against staged files
|
|
2439
|
+
```
|
|
2440
|
+
|
|
2441
|
+
A compliant message:
|
|
2442
|
+
|
|
2443
|
+
```
|
|
2444
|
+
fix: tighten PERSONA_TOOLS
|
|
2445
|
+
|
|
2446
|
+
Failure: run mszcthk1 — worker read the arbiter spine after being told it was denied
|
|
2447
|
+
Addresses: PERSONA_TOOLS did not name the spine file explicitly
|
|
2448
|
+
Corrects: spells the denied paths from ARBITER_BOOK_STORES
|
|
2449
|
+
```
|
|
2450
|
+
|
|
2451
|
+
A commit that touches no prompt-register file is passed without inspecting its message.
|
|
2452
|
+
An unresolvable `--range` (no such ref — a fresh clone with no `origin`, a detached
|
|
2453
|
+
checkout that never fetched) is a **skip**, printed and exited 0: a missing baseline is
|
|
2454
|
+
not a rule violation, never a red.
|
|
2455
|
+
|
|
2456
|
+
|
|
2145
2457
|
## Architecture
|
|
2146
2458
|
|
|
2147
2459
|
Three layers. An **outer shell** (dumb, permanent): per-run budget cap via bareguard,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "bareloop",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.15.0",
|
|
4
4
|
"description": "Workflows that earn their own design, with receipts — agent-authored workflows for repeated, long, verifiable jobs that improve across runs under an un-gameable gate.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/index.js",
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
"node": ">=20"
|
|
18
18
|
},
|
|
19
19
|
"scripts": {
|
|
20
|
-
"test": "node --test",
|
|
20
|
+
"test": "node --test && node scripts/prompt-commit-check.mjs --range origin/main..HEAD",
|
|
21
21
|
"typecheck": "tsc --noEmit",
|
|
22
22
|
"build:types": "tsc",
|
|
23
23
|
"prepublishOnly": "npm run build:types"
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
// F118 (parked half) — the run→code direction. The commit→run direction
|
|
2
|
+
// (prompt-commit-check's `Failure:` line citing a run id) landed same-day;
|
|
3
|
+
// this is the other half: a run's own spine never recorded which version of
|
|
4
|
+
// bareloop's code it ran under, so a replay could not tell which prompts
|
|
5
|
+
// were in force. Report-only, $0, no shell — same posture as the F117
|
|
6
|
+
// verdictType/model stamp this rides beside (src/run.js:303).
|
|
7
|
+
//
|
|
8
|
+
// NEVER shells out to git: `run` is the one locked verb (hamr's law), and a
|
|
9
|
+
// library growing a shell seam just to read a report field is exactly the
|
|
10
|
+
// kind of capability creep that doctrine exists to block. Everything below
|
|
11
|
+
// is a plain file read of `.git`'s own on-disk layout.
|
|
12
|
+
|
|
13
|
+
import { readFileSync, existsSync, statSync } from 'node:fs';
|
|
14
|
+
import { fileURLToPath } from 'node:url';
|
|
15
|
+
import { dirname, join } from 'node:path';
|
|
16
|
+
|
|
17
|
+
// this module lives at <pkgRoot>/src/codeversion.js
|
|
18
|
+
const PKG_ROOT = join(dirname(fileURLToPath(import.meta.url)), '..');
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* `.git` at a package root can be a FILE, not a directory — `git worktree
|
|
22
|
+
* add` makes every worktree's own `.git` a one-line pointer file
|
|
23
|
+
* (`gitdir: <path>`, absolute or relative to the worktree root) at the
|
|
24
|
+
* per-worktree git dir living under the main checkout's `.git/worktrees/
|
|
25
|
+
* <name>/` (measured directly: `git worktree add <tmp> HEAD --detach`
|
|
26
|
+
* against this repo). Resolves that file to the real per-worktree git dir;
|
|
27
|
+
* returns the plain `.git` directory path unchanged when it already is one,
|
|
28
|
+
* and `null` when `.git` is absent (an npm install) or the pointer file
|
|
29
|
+
* can't be read/parsed.
|
|
30
|
+
* @param {string} pkgRoot
|
|
31
|
+
* @returns {string|null}
|
|
32
|
+
*/
|
|
33
|
+
function resolveGitDir(pkgRoot) {
|
|
34
|
+
const dotGit = join(pkgRoot, '.git');
|
|
35
|
+
if (!existsSync(dotGit)) return null;
|
|
36
|
+
const st = statSync(dotGit);
|
|
37
|
+
if (st.isDirectory()) return dotGit;
|
|
38
|
+
if (!st.isFile()) return null;
|
|
39
|
+
const contents = readFileSync(dotGit, 'utf8').trim();
|
|
40
|
+
const m = /^gitdir:\s*(.+)$/.exec(contents);
|
|
41
|
+
if (!m) return null;
|
|
42
|
+
const target = m[1].trim();
|
|
43
|
+
return target.startsWith('/') ? target : join(pkgRoot, target);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* A worktree's own git dir (from {@link resolveGitDir}) holds HEAD but never
|
|
48
|
+
* refs — branches/`packed-refs` are shared, and live in the MAIN checkout's
|
|
49
|
+
* `.git`, whose path is this worktree git dir's own `commondir` file (one
|
|
50
|
+
* line, a path RELATIVE TO THIS GIT DIR — measured directly: this repo's own
|
|
51
|
+
* worktree fixture wrote `../..`, which from `.git/worktrees/<name>/`
|
|
52
|
+
* resolves back to the repo's real `.git`). A normal (non-worktree) git dir
|
|
53
|
+
* carries no `commondir` file at all and IS already the common dir.
|
|
54
|
+
* @param {string} gitDir
|
|
55
|
+
* @returns {string}
|
|
56
|
+
*/
|
|
57
|
+
function resolveCommonDir(gitDir) {
|
|
58
|
+
const commondirPath = join(gitDir, 'commondir');
|
|
59
|
+
if (!existsSync(commondirPath)) return gitDir;
|
|
60
|
+
try {
|
|
61
|
+
const rel = readFileSync(commondirPath, 'utf8').trim();
|
|
62
|
+
if (!rel) return gitDir;
|
|
63
|
+
return rel.startsWith('/') ? rel : join(gitDir, rel);
|
|
64
|
+
} catch {
|
|
65
|
+
return gitDir;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Resolve a ref name (e.g. "refs/heads/main") to a sha by reading the loose
|
|
71
|
+
* ref file, falling back to `packed-refs` when the branch has been packed
|
|
72
|
+
* (measured: this repo's own `.git/packed-refs` carries `main` but not every
|
|
73
|
+
* branch — a loose ref can legitimately be absent while the branch is real).
|
|
74
|
+
* @param {string} gitDir
|
|
75
|
+
* @param {string} refName
|
|
76
|
+
* @returns {string|null}
|
|
77
|
+
*/
|
|
78
|
+
function resolveRef(gitDir, refName) {
|
|
79
|
+
const loose = join(gitDir, refName);
|
|
80
|
+
if (existsSync(loose)) {
|
|
81
|
+
try {
|
|
82
|
+
const sha = readFileSync(loose, 'utf8').trim();
|
|
83
|
+
return /^[0-9a-f]{40}$/.test(sha) ? sha : null;
|
|
84
|
+
} catch {
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
const packed = join(gitDir, 'packed-refs');
|
|
89
|
+
if (!existsSync(packed)) return null;
|
|
90
|
+
try {
|
|
91
|
+
const lines = readFileSync(packed, 'utf8').split('\n');
|
|
92
|
+
for (const line of lines) {
|
|
93
|
+
if (line.startsWith('#')) continue;
|
|
94
|
+
const [sha, ref] = line.trim().split(' ');
|
|
95
|
+
if (ref === refName && sha && /^[0-9a-f]{40}$/.test(sha)) return sha;
|
|
96
|
+
}
|
|
97
|
+
} catch {
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Report-only stamp of the bareloop code this process is running as. Never
|
|
105
|
+
* throws — an unreadable package.json or absent `.git` reads as `null`,
|
|
106
|
+
* honestly, rather than crashing a caller over a report field.
|
|
107
|
+
* @returns {{ version: string|null, sha: string|null, dirty: null }}
|
|
108
|
+
*/
|
|
109
|
+
export function codeVersion() {
|
|
110
|
+
let version = null;
|
|
111
|
+
try {
|
|
112
|
+
const pkg = JSON.parse(readFileSync(join(PKG_ROOT, 'package.json'), 'utf8'));
|
|
113
|
+
version = typeof pkg.version === 'string' ? pkg.version : null;
|
|
114
|
+
} catch {
|
|
115
|
+
version = null;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
let sha = null;
|
|
119
|
+
try {
|
|
120
|
+
// only a dev checkout carries a `.git` at all (directory OR the
|
|
121
|
+
// `gitdir:` pointer file a worktree checkout leaves instead) — an
|
|
122
|
+
// npm-installed copy has neither, and this reads that absence as
|
|
123
|
+
// "unknown", never fakes one
|
|
124
|
+
const gitDir = resolveGitDir(PKG_ROOT);
|
|
125
|
+
if (gitDir !== null) {
|
|
126
|
+
const headPath = join(gitDir, 'HEAD');
|
|
127
|
+
const head = readFileSync(headPath, 'utf8').trim();
|
|
128
|
+
const m = /^ref:\s*(\S+)$/.exec(head);
|
|
129
|
+
if (m) {
|
|
130
|
+
// branches/packed-refs are shared across worktrees and live in the
|
|
131
|
+
// COMMON git dir, never the per-worktree one HEAD itself came from
|
|
132
|
+
sha = resolveRef(resolveCommonDir(gitDir), m[1]);
|
|
133
|
+
} else if (/^[0-9a-f]{40}$/.test(head)) {
|
|
134
|
+
// detached HEAD: a bare 40-hex sha directly in the file
|
|
135
|
+
sha = head;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
} catch {
|
|
139
|
+
sha = null;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// dirty: uncommitted-changes state cannot be known without a shell
|
|
143
|
+
// (`git status`/`git diff`) — reported as unknown, never faked as false.
|
|
144
|
+
return { version, sha, dirty: null };
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* First 8 characters of a sha, or null. Mirrors the short-sha convention
|
|
149
|
+
* `replay.js` already uses for `specHash` (`s.specHash.slice(0, 8)`).
|
|
150
|
+
* @param {string|null|undefined} sha
|
|
151
|
+
* @returns {string|null}
|
|
152
|
+
*/
|
|
153
|
+
export function shortSha(sha) {
|
|
154
|
+
return typeof sha === 'string' && sha.length > 0 ? sha.slice(0, 8) : null;
|
|
155
|
+
}
|