bareloop 0.14.0 → 0.16.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 +218 -0
- package/bareloop.context.md +374 -49
- package/package.json +2 -2
- package/src/authorjob.js +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/reuse.js +11 -13
- 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/types/reuse.d.ts +10 -11
|
@@ -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
|
+
}
|
package/src/index.js
CHANGED
|
@@ -233,6 +233,12 @@ export { closeStagesOf } from './plan.js';
|
|
|
233
233
|
// writes nothing to the spine; `runBehaviour` computes, `formatBehaviour`
|
|
234
234
|
// renders the printable block.
|
|
235
235
|
export { runBehaviour, formatBehaviour } from './behaviour.js';
|
|
236
|
+
// PRD build-list item 6 — generic run replay: point it at a run's spine (+
|
|
237
|
+
// its gate-audit sidecar) and get the whole story back as one object
|
|
238
|
+
// (`replayRun`) or one printable page (`formatReplay`). Reads only, mints no
|
|
239
|
+
// verdict, writes nothing to the spine — reuses `runBehaviour` rather than
|
|
240
|
+
// re-implementing the tool-call count.
|
|
241
|
+
export { replayRun, formatReplay } from './replay.js';
|
|
236
242
|
// M4b — the interview, the refusal, the composition, and D9's three gates.
|
|
237
243
|
// `prepareSigning` returns the resolved spec's HASH and the seed evidence; it
|
|
238
244
|
// never signs — the approvals array and the human's word are unchanged.
|
|
@@ -240,3 +246,9 @@ export {
|
|
|
240
246
|
runInterview, authorCloseForJob, assembleSpec, prepareSigning, refusalEvents, refuseLockedKind,
|
|
241
247
|
GENRE, REFUSAL_LIB, REFUSAL_CATEGORY, AUTHORED_SPEC_FIELDS,
|
|
242
248
|
} from './authorjob.js';
|
|
249
|
+
// PRD build-list item 5 (TODO #8) — the prompt-register inventory: every file
|
|
250
|
+
// known to carry model-facing prompt/instruction text, and the predicate a
|
|
251
|
+
// commit-message check (`scripts/prompt-commit-check.mjs`) uses to decide
|
|
252
|
+
// whether a changed file is one. The ONE place this list lives; exported so
|
|
253
|
+
// bareloop.context.md can point an adopter here instead of re-deriving it.
|
|
254
|
+
export { PROMPT_REGISTERS, isPromptFile } from './promptregisters.js';
|
package/src/ledger.js
CHANGED
|
@@ -479,7 +479,7 @@ export function rateProvenance(record) {
|
|
|
479
479
|
* Re-spelled rather than imported (src/run.js imports this module's siblings and a cycle
|
|
480
480
|
* buys nothing here) — `tests/ledger.test.js` asserts the two spellings agree, because a
|
|
481
481
|
* spend-slicing instrument that misses a writer is the F45 class this list exists to avoid. */
|
|
482
|
-
const SPEND_RECORD_TYPES = Object.freeze(['worker-round', 'judge-round', 'worker-turn']);
|
|
482
|
+
export const SPEND_RECORD_TYPES = Object.freeze(['worker-round', 'judge-round', 'worker-turn']);
|
|
483
483
|
|
|
484
484
|
/** @typedef {{rounds: number, usd: number, unpricedRounds: number}} ProvenanceBucket */
|
|
485
485
|
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
// THE PROMPT REGISTER INVENTORY — build-list item 5 (PRD TODO #8), Q9 answered
|
|
2
|
+
// (hamr, 2026-08-25): "a check". A commit that changes a prompt register must
|
|
3
|
+
// say, in its message, the failure that caused the change, what it addresses,
|
|
4
|
+
// and what it corrects — doctrine already stated once ("a frozen rule without
|
|
5
|
+
// a wired detector is prose", docs/product/2026-08-23-agreed-build-list.md:187-189)
|
|
6
|
+
// so convention-only was rejected. This module is the ONE place that inventory
|
|
7
|
+
// lives: `scripts/prompt-commit-check.mjs` reads it to decide which commits the
|
|
8
|
+
// rule applies to, and this file's own doc-comment is where a human reads it.
|
|
9
|
+
//
|
|
10
|
+
// The inventory is FILE-scoped, not const-scoped. Several of the files below
|
|
11
|
+
// hold more than one named prompt const, and at least two (authorscout.js,
|
|
12
|
+
// cardauthor.js) also build a model-facing prompt INLINE inside a function
|
|
13
|
+
// (template literals, not a top-level const) rather than assembling one from a
|
|
14
|
+
// named export. A const-scoped check would silently miss those; a file-scoped
|
|
15
|
+
// one does not, because the whole file is in scope once it appears here. The
|
|
16
|
+
// `name` field on each entry is documentation only — grepped to the byte range
|
|
17
|
+
// that motivated it — and carries no weight in `isPromptFile`.
|
|
18
|
+
//
|
|
19
|
+
// Verified against source 2026-08-25 (grepped for exported/module-level string
|
|
20
|
+
// consts that read as model-facing instruction text, and separately for inline
|
|
21
|
+
// template-literal prompts built inside a function). Files checked and found to
|
|
22
|
+
// carry NO prompt content: src/plan.js, src/validate.js, src/kinds.js,
|
|
23
|
+
// src/text.js, src/declaredclose.js, src/calibrate.js, src/index.js (the last
|
|
24
|
+
// is the barrel export only).
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* One entry in the prompt-register inventory: a file known to carry
|
|
28
|
+
* model-facing prompt/instruction text, and the name(s) that live there for a
|
|
29
|
+
* human reading this file. `name` may point at a named const OR at a function
|
|
30
|
+
* that builds a prompt inline (e.g. `scoutPrompt()`) — the check only ever
|
|
31
|
+
* keys on `file`.
|
|
32
|
+
* @typedef {{file: string, name: string}} PromptRegisterEntry
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
/** @type {ReadonlyArray<PromptRegisterEntry>} */
|
|
36
|
+
export const PROMPT_REGISTERS = Object.freeze([
|
|
37
|
+
// src/authorscout.js — the read-only repo-survey scout that opens authoring
|
|
38
|
+
Object.freeze({ file: 'src/authorscout.js', name: 'SCOUT_SYSTEM' }),
|
|
39
|
+
Object.freeze({ file: 'src/authorscout.js', name: 'SCOUT_RECOVERY_PROMPT' }),
|
|
40
|
+
Object.freeze({ file: 'src/authorscout.js', name: 'scoutPrompt() (inline template, not a const)' }),
|
|
41
|
+
|
|
42
|
+
// src/authorflow.js — the declaration-authoring turn (system + revise/re-ask)
|
|
43
|
+
Object.freeze({ file: 'src/authorflow.js', name: 'AUTHOR_SYSTEM' }),
|
|
44
|
+
Object.freeze({ file: 'src/authorflow.js', name: 'REVISE_INSTRUCTION' }),
|
|
45
|
+
Object.freeze({ file: 'src/authorflow.js', name: 'STRUCTURE_INSTRUCTION_TOOL' }),
|
|
46
|
+
Object.freeze({ file: 'src/authorflow.js', name: 'STRUCTURE_INSTRUCTION_TEXT' }),
|
|
47
|
+
|
|
48
|
+
// src/judged.js — the softgreen judge's locate prompt (spine + per-rule asks)
|
|
49
|
+
Object.freeze({ file: 'src/judged.js', name: 'PROMPT_HEAD' }),
|
|
50
|
+
Object.freeze({ file: 'src/judged.js', name: 'PROMPT_TAIL' }),
|
|
51
|
+
Object.freeze({ file: 'src/judged.js', name: 'JUDGE_RULES[*].ask (per-rule fragments)' }),
|
|
52
|
+
Object.freeze({ file: 'src/judged.js', name: 'locatePrompt() (inline template, not a const)' }),
|
|
53
|
+
|
|
54
|
+
// src/cardauthor.js — compiles a person's own words into a judged rubric
|
|
55
|
+
Object.freeze({ file: 'src/cardauthor.js', name: 'COMPILE_SYSTEM' }),
|
|
56
|
+
Object.freeze({ file: 'src/cardauthor.js', name: 'cardCasesPrompt() (inline template, not a const)' }),
|
|
57
|
+
|
|
58
|
+
// src/readshim.js — the capped/deduplicating read shim's worker-facing strategy
|
|
59
|
+
Object.freeze({ file: 'src/readshim.js', name: 'READ_SHIM_STRATEGY' }),
|
|
60
|
+
Object.freeze({ file: 'src/readshim.js', name: 'READ_SHIM_DIFF_STRATEGY' }),
|
|
61
|
+
|
|
62
|
+
// src/tools.js — the worker tool menu: personas, retrieval/edit/component strategy
|
|
63
|
+
Object.freeze({ file: 'src/tools.js', name: 'PERSONA_TOOLS' }),
|
|
64
|
+
Object.freeze({ file: 'src/tools.js', name: 'RETRIEVAL_STRATEGY' }),
|
|
65
|
+
Object.freeze({ file: 'src/tools.js', name: 'EDIT_STRATEGY' }),
|
|
66
|
+
Object.freeze({ file: 'src/tools.js', name: 'COMPONENT_STRATEGIES' }),
|
|
67
|
+
Object.freeze({ file: 'src/tools.js', name: 'strategyFor() (assembles per-grant strategy prose)' }),
|
|
68
|
+
Object.freeze({ file: 'src/tools.js', name: 'LINE_SPACE (module-private, not exported)' }),
|
|
69
|
+
Object.freeze({ file: 'src/tools.js', name: 'PERSONA (module-private, not exported, currently unreferenced)' }),
|
|
70
|
+
|
|
71
|
+
// src/planrun.js — the native-surface read-truncation strategy line
|
|
72
|
+
Object.freeze({ file: 'src/planrun.js', name: 'NATIVE_READ_STRATEGY' }),
|
|
73
|
+
]);
|
|
74
|
+
|
|
75
|
+
/** The distinct file paths carrying prompt registers, repo-root-relative with
|
|
76
|
+
* forward slashes — derived from `PROMPT_REGISTERS` so the set can never drift
|
|
77
|
+
* from the entries above. @type {ReadonlySet<string>} */
|
|
78
|
+
const PROMPT_FILES = new Set(PROMPT_REGISTERS.map((e) => e.file));
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* True when `path` names one of the inventoried prompt-register files. Accepts
|
|
82
|
+
* either a repo-root-relative path (`src/tools.js`) or an absolute/relative
|
|
83
|
+
* path ending in one (`/home/x/bareloop/src/tools.js`, `./src/tools.js`) — git
|
|
84
|
+
* hands the check root-relative paths, but a caller working from an absolute
|
|
85
|
+
* path should not have to strip the prefix itself.
|
|
86
|
+
* @param {string} path
|
|
87
|
+
* @returns {boolean}
|
|
88
|
+
*/
|
|
89
|
+
export function isPromptFile(path) {
|
|
90
|
+
if (typeof path !== 'string' || path === '') return false;
|
|
91
|
+
const normalized = path.replace(/\\/g, '/');
|
|
92
|
+
return [...PROMPT_FILES].some((f) => normalized === f || normalized.endsWith(`/${f}`));
|
|
93
|
+
}
|