bareloop 0.35.0 → 0.36.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 +27 -0
- package/bareloop.context.md +36 -8
- package/package.json +1 -1
- package/src/authorrun.js +3 -2
- package/src/bundlerun.js +4 -8
- package/src/draftspend.js +116 -1
- package/src/index.js +3 -3
- package/src/legs.js +1 -1
- package/src/movefile.js +22 -0
- package/src/npminstall.js +55 -0
- package/src/panel/authorsession.js +87 -17
- package/src/panel/index.html +137 -43
- package/src/panel/server.js +130 -14
- package/src/panel/settingsroutes.js +52 -2
- package/src/ralph.js +1 -1
- package/src/source.js +43 -4
- package/src/u-patient.js +3 -2
- package/src/userrun.js +56 -10
- package/src/worktree.js +120 -0
- package/types/draftspend.d.ts +88 -0
- package/types/legs.d.ts +1 -1
- package/types/movefile.d.ts +9 -0
- package/types/npminstall.d.ts +30 -0
- package/types/panel/authorsession.d.ts +2 -1
- package/types/panel/server.d.ts +13 -2
- package/types/panel/settingsroutes.d.ts +6 -1
- package/types/ralph.d.ts +19 -0
- package/types/source.d.ts +13 -5
- package/types/userrun.d.ts +10 -0
- package/types/worktree.d.ts +48 -0
|
@@ -27,8 +27,11 @@ import {
|
|
|
27
27
|
} from 'node:fs';
|
|
28
28
|
import { join, resolve, dirname } from 'node:path';
|
|
29
29
|
import {
|
|
30
|
-
prepareSource, proveDestination, missingDependencies, looksLikeRepoSource,
|
|
30
|
+
prepareSource, proveDestination, missingDependencies, looksLikeRepoSource, nearestGitAncestor,
|
|
31
31
|
} from '../source.js';
|
|
32
|
+
import {
|
|
33
|
+
worktreePath, uncommittedCount, shortHead, removeWorktree,
|
|
34
|
+
} from '../worktree.js';
|
|
32
35
|
import { detectLanguage } from '../detectlang.js';
|
|
33
36
|
import {
|
|
34
37
|
validateJob, jobSpecHash, workflowKey, resolveWorkerModel,
|
|
@@ -48,7 +51,8 @@ import { resolveJobJudge, defaultJudgeLoop } from '../judged.js';
|
|
|
48
51
|
import { closeJudges } from '../kinds.js';
|
|
49
52
|
import { redactSecrets } from '../validate.js';
|
|
50
53
|
import { tallyCalls } from '../text.js';
|
|
51
|
-
import { writeDraftSpend } from '../draftspend.js';
|
|
54
|
+
import { writeDraftSpend, appendDraftLog } from '../draftspend.js';
|
|
55
|
+
import { runNpmCi, NPM_CI_LOCKS } from '../npminstall.js';
|
|
52
56
|
|
|
53
57
|
/**
|
|
54
58
|
* THE PROGRESS LIST's one table (hamr 2026-10-04, "one place showing all"): every pipeline step the left Chat panel
|
|
@@ -58,7 +62,7 @@ import { writeDraftSpend } from '../draftspend.js';
|
|
|
58
62
|
*/
|
|
59
63
|
export const STEP_LABELS = Object.freeze({
|
|
60
64
|
setup: 'checking setup',
|
|
61
|
-
copy: '
|
|
65
|
+
copy: 'making worktree',
|
|
62
66
|
check: 'checking source',
|
|
63
67
|
install: 'waiting on install',
|
|
64
68
|
reuse: 'reusing signed workflow',
|
|
@@ -315,7 +319,7 @@ export function validateJobCard(card, opts = {}) {
|
|
|
315
319
|
* @param {any} card the validated job card (see {@link validateJobCard})
|
|
316
320
|
* @param {{env?: Record<string,string|undefined>, sessionsRoot?: string, home?: string, timeoutMs?: number,
|
|
317
321
|
* scout?: any, generate?: Function, confirmGenerate?: Function, authorFn?: Function,
|
|
318
|
-
* prepareSigningFn?: Function, reuse?: {spec: any, workflowKey: string}}} [deps] `scout`..`prepareSigningFn` are
|
|
322
|
+
* prepareSigningFn?: Function, npmCiFn?: Function, reuse?: {spec: any, workflowKey: string}}} [deps] `scout`..`prepareSigningFn` are
|
|
319
323
|
* TEST SEAMS ONLY — see the note just above where each is read, below. `reuse` (Reuse workflow) is NOT a seam: the
|
|
320
324
|
* caller (`/api/author/start`) sets it only when the server's own same-job rule held.
|
|
321
325
|
* @returns {any} the session object
|
|
@@ -378,15 +382,31 @@ export function createSession(card, deps = {}) {
|
|
|
378
382
|
steps: /** @type {{id: string, label: string, status: 'running'|'done'|'failed', detail: string}[]} */ ([]),
|
|
379
383
|
};
|
|
380
384
|
|
|
385
|
+
// The drafting LOG's step events (src/draftspend.js, DRAFT_LOG_FILE): after every progress-list change, diff the list
|
|
386
|
+
// against what was already logged and append the start / end / re-open of each line — the ONE writer of step times.
|
|
387
|
+
/** @type {('running'|'ended')[]} */
|
|
388
|
+
const loggedSteps = [];
|
|
389
|
+
const logSteps = () => {
|
|
390
|
+
const at = new Date().toISOString();
|
|
391
|
+
state.steps.forEach((/** @type {any} */ x, no) => {
|
|
392
|
+
if (loggedSteps[no] === undefined) { appendDraftLog(outDir, { kind: 'step-start', no, id: x.id, label: x.label, at }); loggedSteps[no] = 'running'; }
|
|
393
|
+
if (x.status === 'running' && loggedSteps[no] === 'ended') { appendDraftLog(outDir, { kind: 'step-reopen', no, at }); loggedSteps[no] = 'running'; }
|
|
394
|
+
if (x.status !== 'running' && loggedSteps[no] === 'running') { appendDraftLog(outDir, { kind: 'step-end', no, status: x.status, label: x.label, at }); loggedSteps[no] = 'ended'; }
|
|
395
|
+
});
|
|
396
|
+
};
|
|
397
|
+
|
|
381
398
|
/** @param {string} id @param {string} [detail] (absent = keep what the step has) start (or restart) a step; whatever step was running is finished */
|
|
382
399
|
const stepStart = (id, detail) => {
|
|
383
400
|
advanceSteps(state.steps, id, detail);
|
|
384
401
|
state.progressLabel = STEP_LABELS[id] ?? id;
|
|
402
|
+
logSteps();
|
|
385
403
|
};
|
|
386
404
|
/** @param {string} id @param {string} detail set a step's latest line's detail without changing its status */
|
|
387
405
|
const stepDetail = (id, detail) => { const x = latestStep(state.steps, id); if (x) x.detail = detail; };
|
|
406
|
+
/** @param {string} id @param {string} label the latest line of `id` reads `label` instead of its table label (the install step: bareloop installing vs the person) */
|
|
407
|
+
const stepLabel = (id, label) => { const x = latestStep(state.steps, id); if (x) x.label = label; };
|
|
388
408
|
/** finish the running step as done (or the latest line of `id`, when given) */
|
|
389
|
-
const stepDone = (id) => { for (const x of state.steps) if (x.status === 'running' && (id === undefined || x.id === id)) x.status = 'done'; };
|
|
409
|
+
const stepDone = (id) => { for (const x of state.steps) if (x.status === 'running' && (id === undefined || x.id === id)) x.status = 'done'; logSteps(); };
|
|
390
410
|
/** @param {string} reason the running step (else the last one) fails with the code-owned reason on its own line */
|
|
391
411
|
const stepFail = (reason) => {
|
|
392
412
|
let x = state.steps.find((/** @type {any} */ y) => y.status === 'running');
|
|
@@ -396,6 +416,7 @@ export function createSession(card, deps = {}) {
|
|
|
396
416
|
}
|
|
397
417
|
x.status = 'failed';
|
|
398
418
|
x.detail = reason;
|
|
419
|
+
logSteps();
|
|
399
420
|
};
|
|
400
421
|
|
|
401
422
|
/** @param {'bot'|'you'|'system'} role @param {string} text */
|
|
@@ -498,6 +519,14 @@ export function createSession(card, deps = {}) {
|
|
|
498
519
|
// tally, so an abandoned / refused / restarted session's drafting spend still counts (src/draftspend.js)
|
|
499
520
|
const nowIso = new Date().toISOString();
|
|
500
521
|
draftStartedAt ??= nowIso;
|
|
522
|
+
// the same call, one line in the drafting LOG (the part a run's views show first): the step running right now,
|
|
523
|
+
// the call's label, the drafting model, its cost (null stays null: unpriced is never $0). One owner: this onCall.
|
|
524
|
+
const runningNo = state.steps.findLastIndex((/** @type {any} */ x) => x.status === 'running');
|
|
525
|
+
const callNo = runningNo === -1 ? state.steps.length - 1 : runningNo;
|
|
526
|
+
appendDraftLog(outDir, {
|
|
527
|
+
kind: 'call', no: callNo, step: state.steps[callNo]?.id ?? null, label: call.label, model: draftIdentity?.model ?? null,
|
|
528
|
+
costUsd: call.costUsd ?? null, unpricedRounds: call.unpricedRounds ?? 0, at: nowIso,
|
|
529
|
+
});
|
|
501
530
|
try {
|
|
502
531
|
writeDraftSpend(outDir, {
|
|
503
532
|
sessionId: id, spentUsd: t.knownUsd, spendComplete: t.spendComplete, provider: draftIdentity?.provider ?? null,
|
|
@@ -507,6 +536,15 @@ export function createSession(card, deps = {}) {
|
|
|
507
536
|
};
|
|
508
537
|
/** @type {string|null} */
|
|
509
538
|
let draftStartedAt = null;
|
|
539
|
+
/** P6 item 1: the worktree this session made in the person's repo (null until made, and again once removed) @type {{repo: string, dir: string}|null} */
|
|
540
|
+
let madeWorktree = null;
|
|
541
|
+
/** a session that ends WITHOUT ever running (refused, errored, abandoned) takes its worktree with it; a signed one keeps it for the run */
|
|
542
|
+
const dropWorktree = () => {
|
|
543
|
+
if (madeWorktree === null) return;
|
|
544
|
+
const w = madeWorktree;
|
|
545
|
+
madeWorktree = null;
|
|
546
|
+
removeWorktree(w.repo, w.dir);
|
|
547
|
+
};
|
|
510
548
|
/** @type {{provider: string, baseUrl: string|null, model: string}|null} the worker the drafting calls ran on, set once the Model Name resolves */
|
|
511
549
|
let draftIdentity = null;
|
|
512
550
|
|
|
@@ -515,6 +553,7 @@ export function createSession(card, deps = {}) {
|
|
|
515
553
|
/** @param {string} message @param {string} [phase] */
|
|
516
554
|
const refuse = (message, phase = 'refused') => {
|
|
517
555
|
if (abandoned) return;
|
|
556
|
+
dropWorktree();
|
|
518
557
|
state.phase = phase;
|
|
519
558
|
state.error = message;
|
|
520
559
|
stepFail(message);
|
|
@@ -556,6 +595,16 @@ export function createSession(card, deps = {}) {
|
|
|
556
595
|
|
|
557
596
|
state.phase = 'preparing-source';
|
|
558
597
|
stepStart('copy');
|
|
598
|
+
// P6 item 1 (hamr 2026-10-05, Q1 = A / Q3 = A): a REPO source gets a worktree in the person's own repo, made now at
|
|
599
|
+
// drafting start — `.bareloop/wt/<session id>` at their current commit. Edits they have not committed are not in it,
|
|
600
|
+
// and the progress line says so (a notice, never a refusal).
|
|
601
|
+
const repoRoot = isRepoLike ? nearestGitAncestor(resolve(card.source))?.dir ?? null : null;
|
|
602
|
+
if (repoRoot !== null) {
|
|
603
|
+
const dirty = uncommittedCount(repoRoot);
|
|
604
|
+
if (dirty > 0) {
|
|
605
|
+
stepDetail('copy', `${dirty} uncommitted change(s) in your repo are not in this job — it starts from commit ${shortHead(repoRoot) ?? 'HEAD'}.`);
|
|
606
|
+
}
|
|
607
|
+
}
|
|
559
608
|
// ONE rule for Destination (the same for a fresh card and a reuse card): for a REPO source it is the write fence
|
|
560
609
|
// (writeScope globs, relative to the repo — set into the spec below, never proven as a directory); for a FOLDER source
|
|
561
610
|
// it is an absolute output directory, proven by `prepareSource` -> `proveDestination`. The door routes on a peek at
|
|
@@ -565,7 +614,10 @@ export function createSession(card, deps = {}) {
|
|
|
565
614
|
const destIsDir = isAbsoluteDestination(card.destination);
|
|
566
615
|
const prep = await prepareSource({
|
|
567
616
|
source: card.source, into, ...(isRepoLike || destIsDir ? { destination: card.destination } : {}),
|
|
617
|
+
...(repoRoot === null ? {} : { worktree: worktreePath(repoRoot, id) }),
|
|
568
618
|
});
|
|
619
|
+
if (repoRoot !== null && prep.stop === null && prep.manifest?.worktree) madeWorktree = { repo: repoRoot, dir: prep.manifest.worktree };
|
|
620
|
+
if (abandoned) dropWorktree();
|
|
569
621
|
if (prep.stop !== null) {
|
|
570
622
|
// reaching the source and checking it are two lines: a scan/freeze refusal means the copy itself worked
|
|
571
623
|
if (SOURCE_CHECK_CODES.has(/** @type {any} */ (prep).code)) stepStart('check');
|
|
@@ -587,21 +639,38 @@ export function createSession(card, deps = {}) {
|
|
|
587
639
|
LANG = langResult.kind === 'resolved' ? langResult.lang : (langResult.kind === 'ambiguous' ? langResult.candidates[0] : 'none-detected');
|
|
588
640
|
|
|
589
641
|
// ── the install gap, WAITED FOR, never a dead end (build item 1,
|
|
590
|
-
// mirroring F182's fix in `src/interviewrun.js`) ── the
|
|
591
|
-
// session works from (`prep.tree`) is
|
|
592
|
-
//
|
|
593
|
-
//
|
|
594
|
-
//
|
|
595
|
-
//
|
|
596
|
-
//
|
|
597
|
-
//
|
|
598
|
-
// (
|
|
599
|
-
// `src/interviewrun.js` uses) on demand, via the "Check again" button
|
|
600
|
-
// (`checkDeps()`, below), instead of ending the session.
|
|
642
|
+
// mirroring F182's fix in `src/interviewrun.js`) ── the tree this
|
|
643
|
+
// session works from (`prep.tree`) is the job's own WORKTREE (P6 item 1:
|
|
644
|
+
// a detached checkout of the person's current commit, inside their
|
|
645
|
+
// repo), so a JS/TS repo's tree never carries `node_modules` until
|
|
646
|
+
// someone installs there. Refusing outright here was ITSELF the F182
|
|
647
|
+
// class of bug one layer up. bareloop still never runs an install
|
|
648
|
+
// itself — this only re-checks the SAME worktree on demand, via the
|
|
649
|
+
// "Check again" button (`checkDeps()`, below), instead of ending the
|
|
650
|
+
// session. (P6 item 3: for an npm lock file bareloop runs `npm ci` itself first, above.)
|
|
601
651
|
let depsGap = missingDependencies(prep.tree, prep.manifest.sourceSubdir ?? '');
|
|
652
|
+
// P6 item 3: a committed npm lock file -> bareloop runs `npm ci --ignore-scripts` itself, $0, before
|
|
653
|
+
// any model call. Any failure (or another lock file) falls through to the wait below, unchanged.
|
|
654
|
+
/** @type {string|null} */
|
|
655
|
+
let installFailed = null;
|
|
656
|
+
if (depsGap && depsGap.lockFile && NPM_CI_LOCKS.includes(depsGap.lockFile)) {
|
|
657
|
+
stepStart('install', 'Installing packages (npm ci)…');
|
|
658
|
+
stepLabel('install', 'installing packages');
|
|
659
|
+
state.progressLabel = 'installing packages';
|
|
660
|
+
const ci = await (deps.npmCiFn ?? runNpmCi)(depsGap.dir, {});
|
|
661
|
+
if (ci.ok) {
|
|
662
|
+
depsGap = missingDependencies(prep.tree, prep.manifest.sourceSubdir ?? '');
|
|
663
|
+
if (!depsGap) {
|
|
664
|
+
stepDetail('install', 'Installed packages (npm ci).');
|
|
665
|
+
stepDone('install');
|
|
666
|
+
stepStart('check');
|
|
667
|
+
} else installFailed = 'it ran but node_modules is still missing';
|
|
668
|
+
} else installFailed = ci.reason;
|
|
669
|
+
}
|
|
602
670
|
if (depsGap) {
|
|
603
671
|
state.phase = 'install-needed';
|
|
604
|
-
|
|
672
|
+
stepLabel('install', STEP_LABELS.install); // the person is the one installing now
|
|
673
|
+
stepStart('install', `${installFailed ? `Installing packages (npm ci) failed: ${redactSecrets(installFailed)}. ` : ''}Packages missing in the job's worktree. Run: cd ${prep.tree} && ${depsGap.command}`);
|
|
605
674
|
for (;;) {
|
|
606
675
|
state.pendingAsk = { kind: 'install-needed', tree: prep.tree, command: depsGap.command, reason: depsGap.reason };
|
|
607
676
|
// eslint-disable-next-line no-await-in-loop
|
|
@@ -834,6 +903,7 @@ export function createSession(card, deps = {}) {
|
|
|
834
903
|
if (['refused', 'abandoned', 'error', 'signed', 'signing-failed'].includes(state.phase)) return { ok: false, error: 'this session is not live — nothing to abandon' };
|
|
835
904
|
abandoned = true;
|
|
836
905
|
phaseValue = 'abandoned';
|
|
906
|
+
dropWorktree();
|
|
837
907
|
state.error = 'Abandoned by you — money already spent stays booked.';
|
|
838
908
|
state.pendingAsk = null;
|
|
839
909
|
if (resolvePending) { const r = resolvePending; resolvePending = null; r(null); }
|