bareloop 0.37.0 → 0.39.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 +53 -0
- package/bareloop.context.md +48 -26
- package/package.json +3 -3
- package/src/authorflow.js +3 -2
- package/src/authoring.js +62 -3
- package/src/authorjob.js +37 -24
- package/src/authorreadout.js +449 -2
- package/src/authorrun.js +21 -4
- package/src/calibrate.js +84 -10
- package/src/cardauthor.js +4 -1
- package/src/declaredclose.js +10 -4
- package/src/job.js +1 -1
- package/src/judged.js +322 -28
- package/src/kinds.js +13 -1
- package/src/legs.js +15 -0
- package/src/panel/authorroutes.js +9 -6
- package/src/panel/authorsession.js +66 -49
- package/src/panel/index.html +321 -355
- package/src/panel/server.js +33 -13
- package/src/panel/status.js +5 -4
- package/src/planrun.js +4 -3
- package/src/providers.js +27 -0
- package/src/ralph.js +8 -2
- package/src/replay.js +23 -3
- package/src/trend.js +1 -0
- package/types/authoring.d.ts +24 -1
- package/types/authorjob.d.ts +17 -10
- package/types/authorreadout.d.ts +106 -0
- package/types/calibrate.d.ts +4 -1
- package/types/declaredclose.d.ts +2 -1
- package/types/judged.d.ts +21 -2
- package/types/legs.d.ts +12 -0
- package/types/panel/authorsession.d.ts +15 -1
- package/types/panel/server.d.ts +5 -4
- package/types/panel/status.d.ts +1 -1
- package/types/providers.d.ts +7 -0
- package/types/ralph.d.ts +6 -1
- package/types/trend.d.ts +1 -0
|
@@ -52,6 +52,7 @@ import { closeJudges } from '../kinds.js';
|
|
|
52
52
|
import { redactSecrets } from '../validate.js';
|
|
53
53
|
import { tallyCalls } from '../text.js';
|
|
54
54
|
import { writeDraftSpend, appendDraftLog } from '../draftspend.js';
|
|
55
|
+
import { PLAIN_PROPOSAL_STOPS, proposalStopText, signingStopText, redsRecord, calibrationSummary } from '../authorreadout.js';
|
|
55
56
|
import { runNpmCi, NPM_CI_LOCKS } from '../npminstall.js';
|
|
56
57
|
|
|
57
58
|
/**
|
|
@@ -132,13 +133,24 @@ const SOURCE_CHECK_CODES = new Set([
|
|
|
132
133
|
'source-file-oversize', 'source-changed-after-scan', 'source-untracked-in-repo',
|
|
133
134
|
]);
|
|
134
135
|
|
|
135
|
-
/**
|
|
136
|
-
*
|
|
137
|
-
*
|
|
138
|
-
*
|
|
139
|
-
*
|
|
140
|
-
*
|
|
141
|
-
|
|
136
|
+
/**
|
|
137
|
+
* The confirm turn's plan as the chat shows it: ONE owner of the text (hamr, 2026-10-09: the one-blob "Plan: ...
|
|
138
|
+
* Checks: ... Not checked: ..." was unreadable). Each section is a heading on its own line, its content on the next,
|
|
139
|
+
* and a blank line between sections. `#NOT CHECKED:` and `#QUESTIONS:` appear only when there is something to say.
|
|
140
|
+
* The CLI composes its own plan screen (src/authorrun.js) and shares no text with this one.
|
|
141
|
+
* @param {{goal?: string, checks?: string[], notChecked?: string[], questions?: string[]}} p
|
|
142
|
+
* @returns {string}
|
|
143
|
+
*/
|
|
144
|
+
export function planText(p) {
|
|
145
|
+
const sections = [
|
|
146
|
+
`#PLAN:\n${JSON.stringify(p.goal ?? '')}`,
|
|
147
|
+
`#CHECKS:\n${(p.checks ?? []).join(' · ') || '(none)'}`,
|
|
148
|
+
];
|
|
149
|
+
if ((p.notChecked ?? []).length) sections.push(`#NOT CHECKED:\n${(p.notChecked ?? []).join(' · ')}`);
|
|
150
|
+
if ((p.questions ?? []).length) sections.push(`#QUESTIONS:\n${(p.questions ?? []).join(' · ')}`);
|
|
151
|
+
sections.push('Sign & run to confirm this plan, or Revise to edit the boxes and draft again.');
|
|
152
|
+
return sections.join('\n\n');
|
|
153
|
+
}
|
|
142
154
|
|
|
143
155
|
/** @param {string} name @returns {boolean} kebab-case slug, same rule `job.js`'s `SLUG_RE` enforces */
|
|
144
156
|
function isSlug(name) {
|
|
@@ -304,7 +316,7 @@ export function validateJobCard(card, opts = {}) {
|
|
|
304
316
|
/**
|
|
305
317
|
* ONE session's engine — created already RUNNING (the caller awaits nothing;
|
|
306
318
|
* the pipeline drives itself, reporting through `state`). `state.pendingAsk`
|
|
307
|
-
* is the ONE thing a person answers next; `answer()`/`send()
|
|
319
|
+
* is the ONE thing a person answers next; `answer()`/`send()`/
|
|
308
320
|
* `signPrepare()` are the only ways in.
|
|
309
321
|
* @param {any} card the validated job card (see {@link validateJobCard})
|
|
310
322
|
* @param {{env?: Record<string,string|undefined>, sessionsRoot?: string, home?: string, timeoutMs?: number,
|
|
@@ -322,7 +334,7 @@ export function createSession(card, deps = {}) {
|
|
|
322
334
|
const timeoutMs = deps.timeoutMs ?? 300_000;
|
|
323
335
|
// TEST SEAMS ONLY (never set by `src/panel/authorroutes.js`'s real caller):
|
|
324
336
|
// override the declaration composer and/or `prepareSigning` itself so a
|
|
325
|
-
// test can drive the REAL confirm turn / ask() channel /
|
|
337
|
+
// test can drive the REAL confirm turn / ask() channel /
|
|
326
338
|
// hash-matching machinery this file owns, without also re-running (and
|
|
327
339
|
// re-proving) `authorClose`'s own composer ladder or `prepareSigning`'s own
|
|
328
340
|
// gates — both already have their own test suites. Absent, both default to
|
|
@@ -342,7 +354,6 @@ export function createSession(card, deps = {}) {
|
|
|
342
354
|
messages: /** @type {{role: string, text: string}[]} */ ([]),
|
|
343
355
|
pendingAsk: /** @type {any} */ (null),
|
|
344
356
|
cost: /** @type {any} */ (null),
|
|
345
|
-
revisesLeft: CONFIRM_ROUND_CAP,
|
|
346
357
|
specHash: /** @type {string|null} */ (null),
|
|
347
358
|
resolvedSpecPath: /** @type {string|null} */ (null),
|
|
348
359
|
error: /** @type {string|null} */ (null),
|
|
@@ -436,15 +447,9 @@ export function createSession(card, deps = {}) {
|
|
|
436
447
|
resolvePending = resolveFn;
|
|
437
448
|
if (step.kind === 'language') say('bot', `${step.field?.prompt ?? 'Which language is this job about?'} (${(step.candidates ?? []).join(', ')})`);
|
|
438
449
|
else if (step.kind === 'menu') {
|
|
439
|
-
|
|
440
|
-
const lines = [`Plan: ${JSON.stringify(p.goal ?? '')}`, `Checks: ${(p.checks ?? []).join(' · ') || '(none)'}`];
|
|
441
|
-
if ((p.notChecked ?? []).length) lines.push(`Not checked: ${p.notChecked.join(' · ')}`);
|
|
442
|
-
if ((p.questions ?? []).length) lines.push(`Questions: ${p.questions.join(' · ')}`);
|
|
443
|
-
lines.push('Sign & run to confirm this plan, or Revise to describe a change.');
|
|
444
|
-
say('bot', lines.join('\n'));
|
|
450
|
+
say('bot', planText(step.plan ?? {}));
|
|
445
451
|
} else if (step.kind === 'answer') say('bot', `A question the plan raised (${step.index} of ${step.total}): ${step.question}`);
|
|
446
452
|
else if (step.kind === 'goal') say('bot', 'Type the goal sentence yourself.');
|
|
447
|
-
else if (step.kind === 'fix') say('bot', 'What should change?');
|
|
448
453
|
});
|
|
449
454
|
|
|
450
455
|
/** @param {string|null} value @returns {boolean} */
|
|
@@ -457,21 +462,7 @@ export function createSession(card, deps = {}) {
|
|
|
457
462
|
return true;
|
|
458
463
|
};
|
|
459
464
|
|
|
460
|
-
/** @param {'menu'|'fix'|'answer'|'language'|'goal'} kind @param {number} timeoutMsInner */
|
|
461
|
-
const waitForPendingKind = async (kind, timeoutMsInner = 2000) => {
|
|
462
|
-
const start = Date.now();
|
|
463
|
-
while (Date.now() - start < timeoutMsInner) {
|
|
464
|
-
if (state.pendingAsk?.kind === kind) return true;
|
|
465
|
-
// eslint-disable-next-line no-await-in-loop
|
|
466
|
-
await new Promise((r) => { setTimeout(r, 10); });
|
|
467
|
-
}
|
|
468
|
-
return state.pendingAsk?.kind === kind;
|
|
469
|
-
};
|
|
470
|
-
|
|
471
465
|
const onPhase = (name, data = {}) => {
|
|
472
|
-
if (name === 'confirm-round' && typeof data.round === 'number') {
|
|
473
|
-
state.revisesLeft = Math.max(0, CONFIRM_ROUND_CAP - (data.round - 1));
|
|
474
|
-
}
|
|
475
466
|
// COLLAPSED INTO THE PROGRESS INDICATOR ONLY (build item 3) — this used
|
|
476
467
|
// to also `say('system', `… ${name}`)`, one jargon chat bubble per
|
|
477
468
|
// library phase (`… confirm-turn-done`, `… seed-read`, …), read back to
|
|
@@ -641,10 +632,11 @@ export function createSession(card, deps = {}) {
|
|
|
641
632
|
// a detached checkout of the person's current commit, inside their
|
|
642
633
|
// repo), so a JS/TS repo's tree never carries `node_modules` until
|
|
643
634
|
// someone installs there. Refusing outright here was ITSELF the F182
|
|
644
|
-
// class of bug one layer up.
|
|
645
|
-
//
|
|
646
|
-
//
|
|
647
|
-
//
|
|
635
|
+
// class of bug one layer up. For an npm lock file bareloop
|
|
636
|
+
// runs `npm ci --ignore-scripts` itself first (P6 item 3, below); any other
|
|
637
|
+
// gap (or a failed install) waits on the person: this re-checks the SAME
|
|
638
|
+
// worktree on demand, via the "Check again" button (`checkDeps()`, below),
|
|
639
|
+
// instead of ending the session.
|
|
648
640
|
let depsGap = missingDependencies(prep.tree, prep.manifest.sourceSubdir ?? '');
|
|
649
641
|
// P6 item 3: a committed npm lock file -> bareloop runs `npm ci --ignore-scripts` itself, $0, before
|
|
650
642
|
// any model call. Any failure (or another lock file) falls through to the wait below, unchanged.
|
|
@@ -763,9 +755,28 @@ export function createSession(card, deps = {}) {
|
|
|
763
755
|
});
|
|
764
756
|
const signingFile = join(outDir, 'signing.json');
|
|
765
757
|
writeFileSync(signingFile, `${JSON.stringify(signing, null, 2)}\n`);
|
|
758
|
+
// The `calibrating` line is the rubric phase the person watched; its ✓ was set long before the gate graded
|
|
759
|
+
// anything. Once the gate has a result, the line says THAT, on its own line (hamr, 2026-10-09).
|
|
760
|
+
const calSum = calibrationSummary(signing.gates?.calibration);
|
|
761
|
+
const calStep = latestStep(state.steps, 'calibrate');
|
|
762
|
+
if (calSum && calStep) {
|
|
763
|
+
calStep.status = calSum.ok ? 'done' : 'failed';
|
|
764
|
+
calStep.detail = calSum.note;
|
|
765
|
+
logSteps();
|
|
766
|
+
}
|
|
766
767
|
|
|
767
768
|
if (!signing.ok) {
|
|
768
|
-
|
|
769
|
+
// plain words for the person (src/authorreadout.js, the one owner the CLI prints too); the full reds go into
|
|
770
|
+
// the drafting log on the failed step, so the cause survives the session
|
|
771
|
+
const runningNo = state.steps.findLastIndex((/** @type {any} */ x) => x.status === 'running');
|
|
772
|
+
appendDraftLog(outDir, {
|
|
773
|
+
kind: 'step-reds', no: runningNo === -1 ? state.steps.length - 1 : runningNo, stop: 'signing-gates-failed',
|
|
774
|
+
reds: redsRecord(signing.reds ?? [], { signing: true }), at: new Date().toISOString(),
|
|
775
|
+
});
|
|
776
|
+
refuse(signingStopText({
|
|
777
|
+
reds: signing.reds ?? [], refusal: signing.refusal,
|
|
778
|
+
spend: { knownUsd: state.draftSpentUsd, spendComplete: state.draftSpendComplete },
|
|
779
|
+
}));
|
|
769
780
|
return;
|
|
770
781
|
}
|
|
771
782
|
state.specHash = signing.specHash;
|
|
@@ -839,7 +850,7 @@ export function createSession(card, deps = {}) {
|
|
|
839
850
|
// TEST SEAMS (see the constructor's own note): a test overrides
|
|
840
851
|
// `generate`/`confirmGenerate` directly (bypassing this real, constructed
|
|
841
852
|
// `provider` for the model boundary) and/or `scout` (bypassing the real
|
|
842
|
-
// paid scout) so it can drive the REAL confirm turn/ask()/
|
|
853
|
+
// paid scout) so it can drive the REAL confirm turn/ask()/hash
|
|
843
854
|
// machinery below with a deterministic fake, never a live provider call.
|
|
844
855
|
// no NEW model call starts after an abandon; a call already in flight books its usage via onCall and is discarded
|
|
845
856
|
const noCallAfterAbandon = (fn) => (...a) => { if (abandoned) throw new Error('abandoned — no new model call'); return fn(...a); };
|
|
@@ -864,6 +875,23 @@ export function createSession(card, deps = {}) {
|
|
|
864
875
|
...(authorFnOverride ? { authorFn: authorFnOverride } : {}),
|
|
865
876
|
});
|
|
866
877
|
|
|
878
|
+
if (!authored.ok && PLAIN_PROPOSAL_STOPS.includes(String(authored.stop))) {
|
|
879
|
+
// a refused rubric proposal: the person gets the reason in plain words (src/authorreadout.js, the one owner the
|
|
880
|
+
// CLI prints too), and the full reds go into the drafting log on the failed step so the cause survives the session
|
|
881
|
+
const cases = authored.judged?.signed?.cases ?? authored.judged?.proposal?.proposal?.cases ?? null;
|
|
882
|
+
const reds = authored.reds ?? [];
|
|
883
|
+
const runningNo = state.steps.findLastIndex((/** @type {any} */ x) => x.status === 'running');
|
|
884
|
+
appendDraftLog(outDir, {
|
|
885
|
+
kind: 'step-reds', no: runningNo === -1 ? state.steps.length - 1 : runningNo, stop: authored.stop,
|
|
886
|
+
reds: redsRecord(reds, { cases }), at: new Date().toISOString(),
|
|
887
|
+
});
|
|
888
|
+
refuse(proposalStopText({
|
|
889
|
+
stop: String(authored.stop), reds, cases,
|
|
890
|
+
spend: { knownUsd: state.draftSpentUsd, spendComplete: state.draftSpendComplete },
|
|
891
|
+
source: authored.judged?.source === 'signer' ? 'signer' : 'proposal',
|
|
892
|
+
}));
|
|
893
|
+
return;
|
|
894
|
+
}
|
|
867
895
|
if (!authored.ok) {
|
|
868
896
|
refuse(`Stopped: ${authored.stop ?? 'authoring-failed'}${authored.refusal ? ` — ${authored.refusal.detail}` : ''}`, authored.stop === 'confirm-abandoned' ? 'abandoned' : 'refused');
|
|
869
897
|
return;
|
|
@@ -887,7 +915,7 @@ export function createSession(card, deps = {}) {
|
|
|
887
915
|
/** @param {string} text */
|
|
888
916
|
send: (text) => {
|
|
889
917
|
if (!state.pendingAsk) return { ok: false, error: 'nothing is being asked right now' };
|
|
890
|
-
if (state.pendingAsk.kind === 'menu') return { ok: false, error: 'use Sign & run
|
|
918
|
+
if (state.pendingAsk.kind === 'menu') return { ok: false, error: 'use Sign & run for a plan, never Send — the chat can never sign or pick a plan action' };
|
|
891
919
|
if (state.pendingAsk.kind === 'install-needed') return { ok: false, error: 'install the packages, then click Check again — nothing to send here' };
|
|
892
920
|
say('you', text);
|
|
893
921
|
answer(text);
|
|
@@ -916,17 +944,6 @@ export function createSession(card, deps = {}) {
|
|
|
916
944
|
r();
|
|
917
945
|
return { ok: true };
|
|
918
946
|
},
|
|
919
|
-
/** @param {string} text */
|
|
920
|
-
revise: async (text) => {
|
|
921
|
-
if (!state.pendingAsk || state.pendingAsk.kind !== 'menu') return { ok: false, error: 'no plan is waiting for a Revise right now' };
|
|
922
|
-
if (state.revisesLeft <= 0) return { ok: false, error: 'no revises left (D3: max 2 rounds)' };
|
|
923
|
-
answer('fix');
|
|
924
|
-
const reached = await waitForPendingKind('fix', 3000);
|
|
925
|
-
if (!reached) return { ok: false, error: 'the confirm turn did not ask for a fix in time' };
|
|
926
|
-
say('you', text);
|
|
927
|
-
answer(text);
|
|
928
|
-
return { ok: true };
|
|
929
|
-
},
|
|
930
947
|
/** click 1 — the confirm pick. Never signs on its own. */
|
|
931
948
|
signPrepare: () => {
|
|
932
949
|
if (!state.pendingAsk || state.pendingAsk.kind !== 'menu') return { ok: false, error: 'no plan is waiting for a decision right now' };
|