bareloop 0.32.0 → 0.33.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 +79 -0
- package/README.md +4 -1
- package/bareloop.context.md +150 -77
- package/package.json +1 -1
- package/src/authorflow.js +5 -3
- package/src/authorjob.js +3 -1
- package/src/authorrun.js +36 -8
- package/src/authorscout.js +10 -6
- package/src/bundlerun.js +266 -0
- package/src/cli.js +24 -352
- package/src/interviewrun.js +3 -2
- package/src/invoke.js +22 -0
- package/src/judged.js +5 -3
- package/src/monthly.js +92 -22
- package/src/panel/authorsession.js +24 -4
- package/src/planrun.js +7 -3
- package/src/providerrows.js +85 -1
- package/src/run.js +7 -2
- package/src/runlist.js +79 -7
- package/src/source.js +8 -3
- package/src/u-watchdog.mjs +296 -0
- package/src/userrun.js +186 -64
- package/types/authorflow.d.ts +8 -2
- package/types/authorjob.d.ts +6 -1
- package/types/authorscout.d.ts +11 -2
- package/types/bundlerun.d.ts +26 -0
- package/types/invoke.d.ts +8 -0
- package/types/judged.d.ts +5 -1
- package/types/monthly.d.ts +43 -8
- package/types/planrun.d.ts +13 -1
- package/types/providerrows.d.ts +65 -0
- package/types/run.d.ts +14 -1
- package/types/runlist.d.ts +65 -3
- package/types/source.d.ts +8 -3
- package/types/userrun.d.ts +49 -0
package/src/cli.js
CHANGED
|
@@ -3,37 +3,32 @@
|
|
|
3
3
|
// bareloop.mjs` is a ~10-line adapter that supplies the real `deps` and turns
|
|
4
4
|
// the returned exit code into `process.exitCode`.
|
|
5
5
|
//
|
|
6
|
-
// `
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
// questions and spend nothing (`bareloop run`, step 3).
|
|
6
|
+
// `bareloop run <bundle>` is a thin door (`src/bundlerun.js`) to the same engine `bareloop
|
|
7
|
+
// run-u` drives (`src/userrun.js`): providers, keys (the keys file / env, any provider the
|
|
8
|
+
// bundle's spec names), judge, monthly limit and readout are that engine's. The test seam is
|
|
9
|
+
// the engine's too: `deps.provider` (and `providerFor`/`judgeProvider`) supplies a scripted
|
|
10
|
+
// provider and skips the real-key check (same as `tests/planrun.test.js`'s pattern).
|
|
12
11
|
//
|
|
13
|
-
// Money/arbiter rules
|
|
14
|
-
//
|
|
15
|
-
//
|
|
16
|
-
//
|
|
17
|
-
//
|
|
18
|
-
//
|
|
19
|
-
// run, never the unresolved bundleHash. `history.jsonl` records both hashes
|
|
20
|
-
// side by side (POC fact 2) so the pairing can never drift silently.
|
|
12
|
+
// Money/arbiter rules the bundle door keeps: `--budget`/`--wall` only TIGHTEN a bundle's
|
|
13
|
+
// signed ceilings (`checkEnvelope`, M1); a bundle's `bundleHash` (the human-facing
|
|
14
|
+
// signature) never changes when the runner tightens — the SPEC that actually executes does,
|
|
15
|
+
// and the door hands the engine the hash of that tightened, `$BARELOOP_BUNDLE`-resolved
|
|
16
|
+
// spec. `history.jsonl` records both hashes side by side (POC fact 2) so the pairing can
|
|
17
|
+
// never drift silently.
|
|
21
18
|
|
|
22
19
|
import { createInterface } from 'node:readline/promises';
|
|
23
|
-
import { hostname } from 'node:os';
|
|
24
|
-
import { execFileSync } from 'node:child_process';
|
|
25
20
|
import {
|
|
26
|
-
existsSync,
|
|
21
|
+
existsSync, readFileSync,
|
|
27
22
|
} from 'node:fs';
|
|
28
23
|
import {
|
|
29
24
|
dirname, isAbsolute, join, resolve,
|
|
30
25
|
} from 'node:path';
|
|
31
26
|
|
|
32
27
|
import {
|
|
33
|
-
exportBundle,
|
|
34
|
-
runJob, makeSpine, loadRegistry, listingRow, jobSpecHash, resolveWorkerModel, resolveJudge,
|
|
28
|
+
exportBundle, loadRegistry, listingRow,
|
|
35
29
|
} from './index.js';
|
|
36
|
-
|
|
30
|
+
// One runner: `bareloop run <bundle>` is a thin door to the `run-u` engine (`src/userrun.js`).
|
|
31
|
+
import { bundleMain } from './bundlerun.js';
|
|
37
32
|
// PANEL-BUILD.md P0 task 2/4 — `bareloop run-u` wraps the person-path run
|
|
38
33
|
// flow's own argv-parsing entry (`src/userrun.js`'s `main(argv, deps)`,
|
|
39
34
|
// itself the lift target of P0 task 1). This is the SAME function
|
|
@@ -69,87 +64,11 @@ import { panelMain } from './panel/server.js';
|
|
|
69
64
|
// (list) and `bareloop runs backfill <dir>` (reconstruct rows from archived
|
|
70
65
|
// spines already on disk) are this rung's only new commands.
|
|
71
66
|
import { keysForDoor } from './keysfile.js';
|
|
72
|
-
import {
|
|
73
|
-
|
|
74
|
-
// The tier->model tables live in `src/providers.js` now (PRD item 28's
|
|
75
|
-
// factory) — one seam instead of a copy hardcoded in each runner. A
|
|
76
|
-
// `bareloop run` has no `--model` flag (not in the frozen spec), so
|
|
77
|
-
// `flagModel` is always undefined below and only the spec's own `model`
|
|
78
|
-
// (if signed) or the resolved provider's default tier is ever in play.
|
|
67
|
+
import { readRunList, backfillRuns, formatRunRow } from './runlist.js';
|
|
79
68
|
|
|
80
69
|
/** @param {unknown} v @returns {v is Record<string, any>} */
|
|
81
70
|
const isObj = (v) => typeof v === 'object' && v !== null;
|
|
82
71
|
|
|
83
|
-
/**
|
|
84
|
-
* Build the real providers from an API key, exactly the way `scripts/run-
|
|
85
|
-
* u.mjs` does (provider/providerFor/judgeProvider from `bare-agent/
|
|
86
|
-
* providers`'s `AnthropicProvider`). `AnthropicProvider` itself lives in
|
|
87
|
-
* `bare-agent` (the one production dependency already budgeted for provider
|
|
88
|
-
* calls); this function only wires it up the same way the u-runner does —
|
|
89
|
-
* it is not arbiter logic (no budget/verdict/merge decision lives here).
|
|
90
|
-
* @param {string} apiKey
|
|
91
|
-
* @param {any} spec the (already `$BARELOOP_BUNDLE`-resolved) bundle spec
|
|
92
|
-
*/
|
|
93
|
-
function buildProviders(apiKey, spec) {
|
|
94
|
-
// `bareloop run`'s frozen key contract (module header above) is
|
|
95
|
-
// ANTHROPIC_API_KEY only — this `apiKey` is always that key. A bundle
|
|
96
|
-
// whose spec names a provider that reads a DIFFERENT env key therefore
|
|
97
|
-
// has no seam into this flow yet, and the one thing this must never do is
|
|
98
|
-
// construct that provider with the Anthropic key: the run would fail at
|
|
99
|
-
// the first call with a vendor 401, which reads as a credential problem
|
|
100
|
-
// rather than as the unbuilt seam it actually is. So it REFUSES here, at
|
|
101
|
-
// $0, naming the key it would have needed. Widening the contract (a
|
|
102
|
-
// signed per-provider key story) is PRD item 28 part (2)/(3), the
|
|
103
|
-
// arbiter's, not this factory rewire's — the runner that actually fires
|
|
104
|
-
// openai-api jobs today is `scripts/run-u.mjs`, which IS key-aware.
|
|
105
|
-
const providerName = isObj(spec) && typeof spec.provider === 'string' ? spec.provider : 'anthropic-api';
|
|
106
|
-
const entry = resolveProvider(providerName);
|
|
107
|
-
if (entry.envKey !== 'ANTHROPIC_API_KEY') {
|
|
108
|
-
throw new Error(
|
|
109
|
-
`bareloop run: this bundle's spec names provider "${providerName}", which reads `
|
|
110
|
-
+ `${entry.envKey}. The bundle runner's key contract is ANTHROPIC_API_KEY only, so it `
|
|
111
|
-
+ 'refuses rather than calling that provider with the wrong key (PRD item 28 part 2 — '
|
|
112
|
-
+ 'a signed per-provider key story is not built). Run this job through scripts/run-u.mjs.',
|
|
113
|
-
);
|
|
114
|
-
}
|
|
115
|
-
const modelResolution = resolveWorkerModel({
|
|
116
|
-
specModel: isObj(spec) && typeof spec.model === 'string' ? spec.model : undefined,
|
|
117
|
-
flagModel: undefined,
|
|
118
|
-
defaultModel: entry.tiers.sonnet,
|
|
119
|
-
});
|
|
120
|
-
const MODEL = modelResolution.model;
|
|
121
|
-
const tierModels = modelResolution.source === 'spec' ? { ...entry.tiers, sonnet: MODEL } : entry.tiers;
|
|
122
|
-
const baseUrl = isObj(spec) && typeof spec.baseUrl === 'string' ? spec.baseUrl : undefined;
|
|
123
|
-
// THE JUDGE, RESOLVED RATHER THAN PINNED (PRD item 32.1/32.2). The bundle
|
|
124
|
-
// runner's key contract is `ANTHROPIC_API_KEY`-only and the refusal above has
|
|
125
|
-
// already established that the WORKER provider reads that key — so a judge
|
|
126
|
-
// defaulting to the worker's own identity reads the same key by construction,
|
|
127
|
-
// and a signed `judge` naming a different provider would need a key this
|
|
128
|
-
// runner does not have. That case refuses here, at $0, by name — the same
|
|
129
|
-
// shape and the same reason as the worker refusal above, never a call made
|
|
130
|
-
// with the wrong vendor's key.
|
|
131
|
-
const judge = resolveJudge({
|
|
132
|
-
specJudge: isObj(spec) ? spec.judge : undefined,
|
|
133
|
-
workerProvider: providerName,
|
|
134
|
-
workerModel: MODEL,
|
|
135
|
-
});
|
|
136
|
-
const judgeEntry = resolveProvider(judge.provider);
|
|
137
|
-
if (judgeEntry.envKey !== 'ANTHROPIC_API_KEY') {
|
|
138
|
-
throw new Error(
|
|
139
|
-
`bareloop run: this bundle's spec names judge provider "${judge.provider}", which reads `
|
|
140
|
-
+ `${judgeEntry.envKey}. The bundle runner's key contract is ANTHROPIC_API_KEY only, so it `
|
|
141
|
-
+ 'refuses rather than calling that provider with the wrong key. Run this job through scripts/run-u.mjs.',
|
|
142
|
-
);
|
|
143
|
-
}
|
|
144
|
-
return {
|
|
145
|
-
...buildRunnerProviders({
|
|
146
|
-
providerName, apiKey, model: MODEL, tierModels, baseUrl,
|
|
147
|
-
judgeApiKey: apiKey, judgeModel: judge.model, judgeProviderName: judge.provider, judgeBaseUrl: baseUrl,
|
|
148
|
-
}),
|
|
149
|
-
judgeModel: judge.model,
|
|
150
|
-
};
|
|
151
|
-
}
|
|
152
|
-
|
|
153
72
|
/** @param {string[]} args @returns {{ positional: string[], flags: Record<string, string|true> }} */
|
|
154
73
|
function parseFlags(args) {
|
|
155
74
|
/** @type {string[]} */
|
|
@@ -248,255 +167,6 @@ function doExport(args, { out, err, cwd }) {
|
|
|
248
167
|
return 0;
|
|
249
168
|
}
|
|
250
169
|
|
|
251
|
-
/**
|
|
252
|
-
* `bareloop run <bundleDir> --repo <path> [--budget N] [--wall MIN] [--approve <bundleHash>]`
|
|
253
|
-
* — the exact five-step order the frozen spec names; no step may be reordered.
|
|
254
|
-
* @param {string[]} args
|
|
255
|
-
* @param {{ out: (s: string) => void, err: (s: string) => void, cwd: string, env: Record<string,string|undefined>, now: () => number, deps: any }} ctx
|
|
256
|
-
*/
|
|
257
|
-
async function doRun(args, { out, err, cwd, env, now, deps }) {
|
|
258
|
-
const { positional, flags } = parseFlags(args);
|
|
259
|
-
const bundleDirArg = positional[0];
|
|
260
|
-
const repoArg = typeof flags.repo === 'string' ? flags.repo : undefined;
|
|
261
|
-
if (!bundleDirArg || !repoArg) {
|
|
262
|
-
err('usage: bareloop run <bundleDir> --repo <path> [--budget N] [--wall MIN] [--approve <bundleHash>]');
|
|
263
|
-
return 1;
|
|
264
|
-
}
|
|
265
|
-
const bundleDir = resolve(cwd, bundleDirArg);
|
|
266
|
-
const repo = resolve(cwd, repoArg);
|
|
267
|
-
|
|
268
|
-
// 1. readBundle — N4: this must complete, clean, before anything else
|
|
269
|
-
// touches the bundle or the repo. Any red (incl. bundle-tampered) stops here.
|
|
270
|
-
const bundle = readBundle(bundleDir);
|
|
271
|
-
if (!bundle.ok) { printReds(bundle.reds, err); return 1; }
|
|
272
|
-
|
|
273
|
-
// 1b. F128 — the bundle's OWN node_modules must carry `bareloop` before
|
|
274
|
-
// anything else runs. Left unchecked, the close crashes deep inside
|
|
275
|
-
// runJob's close-first precheck with a bare ERR_MODULE_NOT_FOUND, reported
|
|
276
|
-
// as an unhelpful generic close-red with no cure line.
|
|
277
|
-
const depsCheck = checkBundleDeps(bundleDir);
|
|
278
|
-
if (!depsCheck.ok) { printReds(depsCheck.reds, err); return 1; }
|
|
279
|
-
|
|
280
|
-
// 2. checkEnvelope — tighten-only, wall given in minutes -> ms.
|
|
281
|
-
/** @type {{ budgetUsd?: number, maxWallMs?: number }} */
|
|
282
|
-
const envelope = {};
|
|
283
|
-
if (flags.budget !== undefined) {
|
|
284
|
-
const n = Number(flags.budget);
|
|
285
|
-
if (!Number.isFinite(n)) { err(`--budget must be a number, got ${JSON.stringify(flags.budget)}`); return 1; }
|
|
286
|
-
envelope.budgetUsd = n;
|
|
287
|
-
}
|
|
288
|
-
if (flags.wall !== undefined) {
|
|
289
|
-
const n = Number(flags.wall);
|
|
290
|
-
if (!Number.isFinite(n)) { err(`--wall must be a number of minutes, got ${JSON.stringify(flags.wall)}`); return 1; }
|
|
291
|
-
envelope.maxWallMs = n * 60_000;
|
|
292
|
-
}
|
|
293
|
-
const envCheck = checkEnvelope(bundle.spec, envelope);
|
|
294
|
-
if (!envCheck.ok) { printReds(envCheck.reds, err); return 1; }
|
|
295
|
-
|
|
296
|
-
// 3. the key — ANTHROPIC_API_KEY only, UNLESS a caller already handed in a
|
|
297
|
-
// provider (the test seam). Absent and no injected provider: print the
|
|
298
|
-
// operator questions (the bundle's own README) and the hash, spend nothing.
|
|
299
|
-
let { provider, providerFor, judgeProvider } = deps;
|
|
300
|
-
// the judge IDENTITY travels with the judge PROVIDER (PRD item 32.1). A test
|
|
301
|
-
// injecting `deps.judgeProvider` may name it too; absent, it is resolved
|
|
302
|
-
// alongside the real providers below, and a judged stage with neither stops as
|
|
303
|
-
// a wiring gap rather than grading under a name nobody wrote.
|
|
304
|
-
let judgeModel = /** @type {string|null} */ (deps.judgeModel ?? null);
|
|
305
|
-
if (!provider) {
|
|
306
|
-
const apiKey = env.ANTHROPIC_API_KEY;
|
|
307
|
-
if (!apiKey) {
|
|
308
|
-
let readme = null;
|
|
309
|
-
try { readme = readFileSync(join(bundleDir, 'README.md'), 'utf8'); } catch { /* fall through to the hash alone */ }
|
|
310
|
-
if (readme) out(readme);
|
|
311
|
-
out(`bundleHash: ${bundle.manifest.bundleHash}`);
|
|
312
|
-
return 0;
|
|
313
|
-
}
|
|
314
|
-
// F181 — a key that reads as "set" above can still carry a line break/
|
|
315
|
-
// control char/stray whitespace and crash Node's own header-encode
|
|
316
|
-
// inside the paid span. Refused here, at the same door, before any
|
|
317
|
-
// provider is constructed; never echoes the value.
|
|
318
|
-
const keyProblem = apiKeyProblem(apiKey);
|
|
319
|
-
if (keyProblem) {
|
|
320
|
-
err(`ANTHROPIC_API_KEY ${keyProblem} — refusing rather than crashing mid-call (never trimmed or repaired; fix the value at its source)`);
|
|
321
|
-
return 1;
|
|
322
|
-
}
|
|
323
|
-
try {
|
|
324
|
-
({ provider, providerFor, judgeProvider, judgeModel } = buildProviders(apiKey, bundle.spec));
|
|
325
|
-
} catch (e) {
|
|
326
|
-
// a refusal from the key contract above: name it and spend nothing
|
|
327
|
-
err(String(/** @type {any} */ (e)?.message ?? e));
|
|
328
|
-
return 1;
|
|
329
|
-
}
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
// 4. blessing.
|
|
333
|
-
const { manifest } = bundle;
|
|
334
|
-
if (!bundle.blessing) {
|
|
335
|
-
// The runid that ACTUALLY minted this bundle's own signed spec — the
|
|
336
|
-
// bridge VERSION (base or shape fork; both ship into bridges/) whose
|
|
337
|
-
// recorded specHash equals the resolved bundle spec's jobSpecHash. NOT
|
|
338
|
-
// the bridge's first history row: a job can be re-exported/rebased many
|
|
339
|
-
// times, and only the version at THIS exact hash proves anything for
|
|
340
|
-
// THIS bundle.
|
|
341
|
-
const { approveHash: mintMatchHash } = resolveBundleSpec(bundle, bundleDir);
|
|
342
|
-
let mintRunid = null;
|
|
343
|
-
for (const b of bundle.bridges) {
|
|
344
|
-
const versions = Array.isArray(b.versions) ? b.versions : [];
|
|
345
|
-
const v = versions.find((/** @type {any} */ ver) => ver.specHash === mintMatchHash);
|
|
346
|
-
if (v) { mintRunid = v.runid; break; }
|
|
347
|
-
}
|
|
348
|
-
out('first run — this bundle has never been blessed on this machine.');
|
|
349
|
-
out(mintRunid ? `minting run: ${mintRunid} (spine not bundled in v1)` : 'no version at this hash');
|
|
350
|
-
out(`bundleHash: ${manifest.bundleHash}`);
|
|
351
|
-
if (flags.approve !== manifest.bundleHash) {
|
|
352
|
-
err(`--approve ${manifest.bundleHash} is required for the first run of an unblessed bundle`);
|
|
353
|
-
return 1;
|
|
354
|
-
}
|
|
355
|
-
} else {
|
|
356
|
-
const vb = verifyBlessing(bundle);
|
|
357
|
-
if (!vb.ok) { printReds(vb.reds, err); return 1; }
|
|
358
|
-
if (flags.approve) out('already blessed, --approve ignored');
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
// 5. the worktree — refuse a non-repo, a repo with no commit, or a collision;
|
|
362
|
-
// ALWAYS fresh, never reused (a reused worktree reads last run's edits as
|
|
363
|
-
// "already-green" — the negative POC's exact finding).
|
|
364
|
-
try {
|
|
365
|
-
execFileSync('git', ['-C', repo, 'rev-parse', 'HEAD'], { encoding: 'utf8' });
|
|
366
|
-
} catch (e) {
|
|
367
|
-
err(`--repo ${repoArg} must be a git repository with at least one commit: ${/** @type {Error} */ (e).message}`);
|
|
368
|
-
return 1;
|
|
369
|
-
}
|
|
370
|
-
const runid = now().toString(36);
|
|
371
|
-
const worktree = join(repo, '.bareloop', 'wt', runid);
|
|
372
|
-
if (existsSync(worktree)) { err(`worktree already exists: ${worktree}`); return 1; }
|
|
373
|
-
mkdirSync(dirname(worktree), { recursive: true });
|
|
374
|
-
try {
|
|
375
|
-
execFileSync('git', ['-C', repo, 'worktree', 'add', '--detach', worktree, 'HEAD'], { encoding: 'utf8' });
|
|
376
|
-
} catch (e) {
|
|
377
|
-
err(`git worktree add failed: ${/** @type {Error} */ (e).message}`);
|
|
378
|
-
return 1;
|
|
379
|
-
}
|
|
380
|
-
|
|
381
|
-
// 6. resolve (in-memory $BARELOOP_BUNDLE substitution) + tighten + run. The
|
|
382
|
-
// envelope tightens the ACTUAL spec that runs (budgetUsd/maxWallMs have no
|
|
383
|
-
// separate runJob override — the library reads them off the spec itself),
|
|
384
|
-
// so `approveHash` is recomputed AFTER tightening: the runner mints its own
|
|
385
|
-
// fresh approval over the exact spec it is about to hand to runJob, never
|
|
386
|
-
// over the untightened one `resolveBundleSpec` first returned. The
|
|
387
|
-
// bundleHash a human approved never changes; only the executed spec's own
|
|
388
|
-
// hash does, and that hash is minted here, not signed by a person.
|
|
389
|
-
const resolved = resolveBundleSpec(bundle, bundleDir);
|
|
390
|
-
const runSpec = resolved.spec;
|
|
391
|
-
if (envelope.budgetUsd !== undefined) runSpec.budgetUsd = envelope.budgetUsd;
|
|
392
|
-
if (envelope.maxWallMs !== undefined) runSpec.maxWallMs = envelope.maxWallMs;
|
|
393
|
-
const approveHash = jobSpecHash(runSpec);
|
|
394
|
-
|
|
395
|
-
const runsDir = join(bundleDir, 'runs', runid);
|
|
396
|
-
mkdirSync(runsDir, { recursive: true });
|
|
397
|
-
const spineFile = join(runsDir, 'spine.jsonl');
|
|
398
|
-
const emit = makeSpine(spineFile);
|
|
399
|
-
// PRD item 27/M3 Part B — the close's own books directory, inside the run's
|
|
400
|
-
// own directory so the bundle stays one self-contained unit (hamr,
|
|
401
|
-
// 2026-09-07: "export holds as one unit of itself"). `runJob` autosets the
|
|
402
|
-
// close timeout itself (no `closeTimeoutMs` passed — the bundle CLI has
|
|
403
|
-
// always run on the library default/autoset path, never run-u's operator
|
|
404
|
-
// knob).
|
|
405
|
-
const closeDir = join(runsDir, 'close');
|
|
406
|
-
mkdirSync(closeDir, { recursive: true });
|
|
407
|
-
|
|
408
|
-
// PANEL-BUILD.md P1 — one row in the run list, BEFORE the first paid call
|
|
409
|
-
// (runJob, right below). A list-append failure must never block a real,
|
|
410
|
-
// already-signed run: caught and named loudly, never rethrown.
|
|
411
|
-
// `deps.runlistHome` — the SAME test seam shape `deps.provider`/`deps.now`
|
|
412
|
-
// already are on this ctx (never a new env var, see src/runlist.js's own
|
|
413
|
-
// header comment): a caller that injects it (every test in
|
|
414
|
-
// tests/cli.test.js that reaches this line) writes its row under a temp
|
|
415
|
-
// home instead of the real `~/.config/bareloop`; production never sets it,
|
|
416
|
-
// so `appendRun` falls through to `runlistHome()`'s own `os.homedir()`.
|
|
417
|
-
try {
|
|
418
|
-
appendRun({
|
|
419
|
-
at: new Date(now()).toISOString(), runid, job: runSpec.job, spine: spineFile, patient: worktree, via: 'bundle',
|
|
420
|
-
}, { home: deps.runlistHome });
|
|
421
|
-
} catch (e) {
|
|
422
|
-
err(`WARNING: could not add this run to ~/.config/bareloop/runs.jsonl (${/** @type {Error} */ (e).message}) — the run continues; the panel's list will be missing this row.`);
|
|
423
|
-
}
|
|
424
|
-
|
|
425
|
-
let outcome;
|
|
426
|
-
try {
|
|
427
|
-
outcome = await runJob(runSpec, {
|
|
428
|
-
approvals: [{ specHash: approveHash, signer: 'bundle', ts: new Date(now()).toISOString() }],
|
|
429
|
-
workdir: worktree,
|
|
430
|
-
provider,
|
|
431
|
-
providerFor,
|
|
432
|
-
judgeProvider,
|
|
433
|
-
judgeModel,
|
|
434
|
-
emit,
|
|
435
|
-
closeDir,
|
|
436
|
-
shellCapUsd: runSpec.budgetUsd,
|
|
437
|
-
readShim: 'cap',
|
|
438
|
-
scout: true,
|
|
439
|
-
// F130/PRD item 27(c) — the bundle CLI has no `--resume` (v1); the
|
|
440
|
-
// honest tail says so instead of naming a flag that does not exist.
|
|
441
|
-
resumable: false,
|
|
442
|
-
});
|
|
443
|
-
} catch (e) {
|
|
444
|
-
err(`runJob crashed: ${/** @type {Error} */ (e).message}`);
|
|
445
|
-
return 1;
|
|
446
|
-
}
|
|
447
|
-
|
|
448
|
-
// the gate-audit relocation (POC fact 3, run-u.mjs:1293's move)
|
|
449
|
-
const auditSrc = join(worktree, 'gate-audit.jsonl');
|
|
450
|
-
if (existsSync(auditSrc)) renameSync(auditSrc, join(runsDir, 'gate-audit.jsonl'));
|
|
451
|
-
|
|
452
|
-
// the job-end record off THIS run's own spine — never fabricate a 0 for an
|
|
453
|
-
// unknown spend (F6/F12's class). PANEL-BUILD.md P0: read via the same
|
|
454
|
-
// named library reader `doHistory`/`doReplay` already use (`parseJsonl`,
|
|
455
|
-
// `src/replayio.js`) instead of hand-rolling the parse again here — it
|
|
456
|
-
// tolerates a malformed/truncated line (a process killed mid-append)
|
|
457
|
-
// instead of throwing uncaught and crashing this run's exit tail.
|
|
458
|
-
const { records: events } = parseJsonl(spineFile);
|
|
459
|
-
const je = events.findLast((/** @type {any} */ e) => e.type === 'job-end');
|
|
460
|
-
const spentUsd = je?.spentUsd ?? null;
|
|
461
|
-
const spendComplete = je?.spendComplete ?? null;
|
|
462
|
-
|
|
463
|
-
let branch = null;
|
|
464
|
-
try { branch = execFileSync('git', ['-C', worktree, 'rev-parse', '--abbrev-ref', 'HEAD'], { encoding: 'utf8' }).trim(); } catch { /* leave null — reported honestly below */ }
|
|
465
|
-
|
|
466
|
-
// 7. history + (on a first green) the blessing.
|
|
467
|
-
appendHistory(bundleDir, {
|
|
468
|
-
runid,
|
|
469
|
-
at: new Date(now()).toISOString(),
|
|
470
|
-
outcome,
|
|
471
|
-
spentUsd,
|
|
472
|
-
spendComplete,
|
|
473
|
-
budgetUsd: runSpec.budgetUsd,
|
|
474
|
-
maxWallMs: runSpec.maxWallMs ?? null,
|
|
475
|
-
worktree,
|
|
476
|
-
branch,
|
|
477
|
-
bundleHash: manifest.bundleHash,
|
|
478
|
-
approveHash,
|
|
479
|
-
});
|
|
480
|
-
if (outcome === 'green' && !bundle.blessing) {
|
|
481
|
-
bless(bundleDir, {
|
|
482
|
-
bundleHash: manifest.bundleHash, blessedAt: new Date(now()).toISOString(), runid, outcome, host: hostname(),
|
|
483
|
-
});
|
|
484
|
-
}
|
|
485
|
-
|
|
486
|
-
// 8. the tail.
|
|
487
|
-
out(`outcome ${outcome}`);
|
|
488
|
-
out(`spent ${spentUsd == null ? 'UNKNOWN' : `${spendComplete === false ? '≥' : ''}$${spentUsd.toFixed(4)}`} of $${runSpec.budgetUsd}`);
|
|
489
|
-
out(`branch ${branch ?? 'UNKNOWN (could not read the worktree branch)'}`);
|
|
490
|
-
out(`worktree ${worktree}`);
|
|
491
|
-
if (branch) out(`merge git merge ${branch} (merge stays human — this CLI never merges)`);
|
|
492
|
-
out(`the worktree is kept until you remove it: git worktree remove ${worktree}`);
|
|
493
|
-
// exit 0 ONLY for a real green — every other outcome (close-red, plan-red,
|
|
494
|
-
// escalated, cap/wall halts, worker-crash, provider-red, …) exits 1 so a
|
|
495
|
-
// caller scripting `bareloop run` off its exit code cannot mistake a red
|
|
496
|
-
// for a success. The tail print above is unchanged either way.
|
|
497
|
-
return outcome === 'green' || outcome === 'already-green' ? 0 : 1;
|
|
498
|
-
}
|
|
499
|
-
|
|
500
170
|
/**
|
|
501
171
|
* `bareloop history <bundleDir>`
|
|
502
172
|
* @param {string[]} args @param {{ out: (s: string) => void, err: (s: string) => void, cwd: string }} ctx
|
|
@@ -609,9 +279,11 @@ function doRuns(args, { out, err, cwd }) {
|
|
|
609
279
|
* inventing new interactive UX, not wiring, and that judgement stands. This
|
|
610
280
|
* menu is fixed instead: it now says those four commands exist and how to
|
|
611
281
|
* reach them, rather than implying only three commands do at all.
|
|
612
|
-
*
|
|
282
|
+
* `keysHome` is the home `main` already resolved (`keysForDoor`), handed on so choice 2 reads the
|
|
283
|
+
* SAME config.json (monthly limit, price, key row) as `bareloop run <bundle>` — never resolved twice.
|
|
284
|
+
* @param {any} deps @param {{ out: (s: string) => void, err: (s: string) => void, cwd: string, env: any, now: () => number }} ctx @param {string|undefined} keysHome
|
|
613
285
|
*/
|
|
614
|
-
async function runMenu(deps, ctx) {
|
|
286
|
+
async function runMenu(deps, ctx, keysHome) {
|
|
615
287
|
const stdin = deps.stdin;
|
|
616
288
|
const stdout = deps.stdout;
|
|
617
289
|
const rl = createInterface({ input: stdin, output: stdout });
|
|
@@ -636,7 +308,7 @@ async function runMenu(deps, ctx) {
|
|
|
636
308
|
if (budget) args.push('--budget', budget);
|
|
637
309
|
if (wall) args.push('--wall', wall);
|
|
638
310
|
if (approve) args.push('--approve', approve);
|
|
639
|
-
return await
|
|
311
|
+
return await bundleMain(args, { ...ctx, deps, keysHome, parseFlags, printReds });
|
|
640
312
|
}
|
|
641
313
|
if (choice === '3') {
|
|
642
314
|
const bundleDir = (await rl.question('bundle dir: ')).trim();
|
|
@@ -673,9 +345,9 @@ export async function main(argv, deps = {}) {
|
|
|
673
345
|
|
|
674
346
|
const [cmd, ...rest] = argv;
|
|
675
347
|
if (keys.warning && cmd && ['run', 'run-u', 'interview', 'author', 'panel'].includes(cmd)) err(`WARNING: ${keys.warning}`);
|
|
676
|
-
if (!cmd) return runMenu({ ...deps, stdin: deps.stdin ?? process.stdin, stdout }, ctx);
|
|
348
|
+
if (!cmd) return runMenu({ ...deps, stdin: deps.stdin ?? process.stdin, stdout }, ctx, keys.home);
|
|
677
349
|
if (cmd === 'export') return doExport(rest, ctx);
|
|
678
|
-
if (cmd === 'run') return
|
|
350
|
+
if (cmd === 'run') return bundleMain(rest, { ...ctx, keysHome: keys.home, parseFlags, printReds });
|
|
679
351
|
if (cmd === 'history') return doHistory(rest, ctx);
|
|
680
352
|
// `bareloop run-u` — the person-path run flow (PANEL-BUILD.md P0 task
|
|
681
353
|
// 2/4). `rest` is handed straight to `src/userrun.js`'s own `main(argv,
|
|
@@ -687,7 +359,7 @@ export async function main(argv, deps = {}) {
|
|
|
687
359
|
// then `env`/`out`/`err` are overridden to the SAME resolved values every
|
|
688
360
|
// other command here prints through, so `run-u`'s output lands on the
|
|
689
361
|
// `stdout`/`stderr` a caller of `main` actually passed.
|
|
690
|
-
if (cmd === 'run-u') return runUMain(rest, { ...deps, env, out, err, keysHome: keys.home });
|
|
362
|
+
if (cmd === 'run-u') return runUMain(rest, { ...deps, env, out, err, keysHome: keys.home, invokedAs: 'bareloop run-u' });
|
|
691
363
|
// `bareloop interview` — the close-authoring interview (PANEL-BUILD.md P0
|
|
692
364
|
// task 3/4). This flow reads a TTY (or a piped stdin) directly rather than
|
|
693
365
|
// through the `out`/`err` line-functions every other command here uses, so
|
package/src/interviewrun.js
CHANGED
|
@@ -109,6 +109,7 @@ import { scanSecrets, redactSecrets } from './validate.js';
|
|
|
109
109
|
import { detectLanguage } from './detectlang.js';
|
|
110
110
|
import { prepareSource, proveDestination, looksLikeRepoSource, missingDependencies } from './source.js';
|
|
111
111
|
import { parseCeiling, ceilingLine } from './authorreadout.js';
|
|
112
|
+
import { commandFor } from './invoke.js';
|
|
112
113
|
|
|
113
114
|
/** Thrown to unwind `main` to an exit code without ever calling
|
|
114
115
|
* `process.exit()` (constraint: a library function returns a code / throws,
|
|
@@ -157,7 +158,7 @@ export async function main(argv, deps = {}) {
|
|
|
157
158
|
const stderr = deps.stderr ?? process.stderr;
|
|
158
159
|
if (keys.warning && deps.env === undefined) stderr.write(`WARNING: ${keys.warning}\n`);
|
|
159
160
|
const spawnSyncFn = deps.spawnSync ?? realSpawnSync;
|
|
160
|
-
const invokedAs = deps.invokedAs ?? '
|
|
161
|
+
const invokedAs = deps.invokedAs ?? commandFor('interview', undefined);
|
|
161
162
|
const out = (/** @type {string} */ s = '') => { stdout.write(`${s}\n`); };
|
|
162
163
|
const err = (/** @type {string} */ s) => { stderr.write(`${s}\n`); };
|
|
163
164
|
const arg = (/** @type {string} */ n) => { const i = argv.indexOf(`--${n}`); return i === -1 ? null : (argv[i + 1] ?? ''); };
|
|
@@ -754,7 +755,7 @@ export async function main(argv, deps = {}) {
|
|
|
754
755
|
let providerEntry = null;
|
|
755
756
|
try { providerEntry = resolveProvider(PROVIDER); } catch { providerEntry = null; }
|
|
756
757
|
const providerEnvKey = providerEntry?.envKey ?? null;
|
|
757
|
-
say(` ${providerEnvKey ?? 'YOUR_PROVIDER_API_KEY'}=...
|
|
758
|
+
say(` ${providerEnvKey ?? 'YOUR_PROVIDER_API_KEY'}=... ${commandFor('author', deps.invokedAs)} ${childArgs.join(' ')}`);
|
|
758
759
|
// An `openai-api` + `baseUrl` draft (DeepSeek, today's one secondary, reached
|
|
759
760
|
// this way) still reads its key from `OPENAI_API_KEY` — there is no separate
|
|
760
761
|
// "DeepSeek key" env var, and a person pointed only at the command above could
|
package/src/invoke.js
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
// The one owner of how a printed re-invocation is spelled. The package ships `src/` and `bin/`
|
|
2
|
+
// only, so a person who reached a flow through the `bareloop` CLI must be told
|
|
3
|
+
// `bareloop run-u ...`, never `node scripts/run-u.mjs ...` (a path their install does not have).
|
|
4
|
+
// Each flow's `main(argv, deps)` takes `deps.invokedAs` — the command it was actually reached
|
|
5
|
+
// through (`src/cli.js` passes `'bareloop <name>'`; the `scripts/*.mjs` adapters pass none).
|
|
6
|
+
|
|
7
|
+
/** @typedef {'run-u' | 'interview' | 'author'} Flow */
|
|
8
|
+
|
|
9
|
+
/** the script adapter each flow keeps for a source-tree run */
|
|
10
|
+
const SCRIPT = /** @type {const} */ ({ 'run-u': 'run-u', interview: 'run-interview', author: 'run-author' });
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* The command that runs `flow`, spelled the way the caller reached this flow: `bareloop <flow>`
|
|
14
|
+
* when `invokedAs` names the CLI, else the source-tree script.
|
|
15
|
+
* @param {Flow} flow @param {string|undefined} invokedAs
|
|
16
|
+
* @returns {string}
|
|
17
|
+
*/
|
|
18
|
+
export function commandFor(flow, invokedAs) {
|
|
19
|
+
return invokedAs !== undefined && /^bareloop(\s|$)/.test(invokedAs)
|
|
20
|
+
? `bareloop ${flow}`
|
|
21
|
+
: `node scripts/${SCRIPT[flow]}.mjs`;
|
|
22
|
+
}
|
package/src/judged.js
CHANGED
|
@@ -791,10 +791,12 @@ function topLevelFunctionNames(artifactText) {
|
|
|
791
791
|
* A `Loop`, NOT `provider.generate()`: the POC measured that a bare provider
|
|
792
792
|
* result carries no `costUsd` at all, and under F6 an unpriced call is a red,
|
|
793
793
|
* so the priced seam is the only legal one.
|
|
794
|
-
*
|
|
795
|
-
|
|
794
|
+
* `rates` is the customer's own price for the judge's row (USD per 1K, `ratesFor`'s
|
|
795
|
+
* `rates`); null/absent = bare-agent's built-in guess, exactly as before.
|
|
796
|
+
* @param {{provider: any, system: string, rates?: {in: number, out: number}|null}} o */
|
|
797
|
+
export const defaultJudgeLoop = ({ provider, system, rates = null }) => {
|
|
796
798
|
const { Loop } = require('bare-agent');
|
|
797
|
-
return new Loop({ provider, system });
|
|
799
|
+
return new Loop({ provider, system, ...(rates ? { rates } : {}) });
|
|
798
800
|
};
|
|
799
801
|
|
|
800
802
|
// ── LOCATE: one attempt ─────────────────────────────────────────────────────
|