bareloop 0.38.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.
@@ -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
- /** the confirm turn's own fixed round cap (`src/authorflow.js`'s
136
- * `runConfirmTurn`: `for (let round = 1; round <= 2; round += 1)`) — named
137
- * here, once, rather than re-guessed at every `onPhase('confirm-round')`
138
- * below. Not itself exported from the library as a named constant (it is
139
- * the loop bound the confirm turn's own doc calls "D3" / "ruling 5"); this is
140
- * this file's one place that would need editing if that cap ever changed. */
141
- const CONFIRM_ROUND_CAP = 2;
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()`/`revise()`/
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 / revise-round /
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
- const p = step.plan ?? {};
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
@@ -764,9 +755,28 @@ export function createSession(card, deps = {}) {
764
755
  });
765
756
  const signingFile = join(outDir, 'signing.json');
766
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
+ }
767
767
 
768
768
  if (!signing.ok) {
769
- refuse(`signing gates failed — ${signing.reds.map((r) => r.code).join(', ') || 'no work red at seed'}`);
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
+ }));
770
780
  return;
771
781
  }
772
782
  state.specHash = signing.specHash;
@@ -840,7 +850,7 @@ export function createSession(card, deps = {}) {
840
850
  // TEST SEAMS (see the constructor's own note): a test overrides
841
851
  // `generate`/`confirmGenerate` directly (bypassing this real, constructed
842
852
  // `provider` for the model boundary) and/or `scout` (bypassing the real
843
- // paid scout) so it can drive the REAL confirm turn/ask()/revise/hash
853
+ // paid scout) so it can drive the REAL confirm turn/ask()/hash
844
854
  // machinery below with a deterministic fake, never a live provider call.
845
855
  // no NEW model call starts after an abandon; a call already in flight books its usage via onCall and is discarded
846
856
  const noCallAfterAbandon = (fn) => (...a) => { if (abandoned) throw new Error('abandoned — no new model call'); return fn(...a); };
@@ -865,6 +875,23 @@ export function createSession(card, deps = {}) {
865
875
  ...(authorFnOverride ? { authorFn: authorFnOverride } : {}),
866
876
  });
867
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
+ }
868
895
  if (!authored.ok) {
869
896
  refuse(`Stopped: ${authored.stop ?? 'authoring-failed'}${authored.refusal ? ` — ${authored.refusal.detail}` : ''}`, authored.stop === 'confirm-abandoned' ? 'abandoned' : 'refused');
870
897
  return;
@@ -888,7 +915,7 @@ export function createSession(card, deps = {}) {
888
915
  /** @param {string} text */
889
916
  send: (text) => {
890
917
  if (!state.pendingAsk) return { ok: false, error: 'nothing is being asked right now' };
891
- if (state.pendingAsk.kind === 'menu') return { ok: false, error: 'use Sign & run or Revise for a plan, never Send — the chat can never sign or pick a plan action' };
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' };
892
919
  if (state.pendingAsk.kind === 'install-needed') return { ok: false, error: 'install the packages, then click Check again — nothing to send here' };
893
920
  say('you', text);
894
921
  answer(text);
@@ -917,17 +944,6 @@ export function createSession(card, deps = {}) {
917
944
  r();
918
945
  return { ok: true };
919
946
  },
920
- /** @param {string} text */
921
- revise: async (text) => {
922
- if (!state.pendingAsk || state.pendingAsk.kind !== 'menu') return { ok: false, error: 'no plan is waiting for a Revise right now' };
923
- if (state.revisesLeft <= 0) return { ok: false, error: 'no revises left (D3: max 2 rounds)' };
924
- answer('fix');
925
- const reached = await waitForPendingKind('fix', 3000);
926
- if (!reached) return { ok: false, error: 'the confirm turn did not ask for a fix in time' };
927
- say('you', text);
928
- answer(text);
929
- return { ok: true };
930
- },
931
947
  /** click 1 — the confirm pick. Never signs on its own. */
932
948
  signPrepare: () => {
933
949
  if (!state.pendingAsk || state.pendingAsk.kind !== 'menu') return { ok: false, error: 'no plan is waiting for a decision right now' };