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.
@@ -74,7 +74,7 @@ export const RUNID_RE = /^[A-Za-z0-9._~-]+$/;
74
74
  export const DEFAULT_PORT = 4700;
75
75
 
76
76
  /**
77
- * `[✓]`/`[✗]`/`[▶]` — the ONLY vocabulary a result is ever rendered in
77
+ * `[✓]`/`[✗]`/`[■]`/`[▶]` — the ONLY vocabulary a result is ever rendered in
78
78
  * (auto-memory `ui-verdict-words.md`: never the words green/red/soft-green
79
79
  * anywhere in the page). `null` (no `job-end` reached — a killed-mid-run or
80
80
  * still-running spine) reads `▶` — the same "in progress / unresolved" glyph
@@ -82,10 +82,10 @@ export const DEFAULT_PORT = 4700;
82
82
  * running attempt and an archived spine that never reached its own end are
83
83
  * the same fact from this read-only side: no verdict has been recorded yet.
84
84
  * @param {string|null} outcome
85
- * @returns {'✓'|'✗'|'▶'}
85
+ * @returns {'✓'|'✗'|'■'|'▶'}
86
86
  */
87
87
  export function glyphForOutcome(outcome) {
88
- return /** @type {'✓'|'✗'|'▶'} */ (statusFor({ outcome }).sign);
88
+ return /** @type {'✓'|'✗'|'■'|'▶'} */ (statusFor({ outcome }).sign);
89
89
  }
90
90
 
91
91
  /**
@@ -417,7 +417,7 @@ const GOAL_NOT_MET = new Set(['plan-red', 'check-red', 'step-red', 'escalated'])
417
417
  * @returns {{reason: string, next: string, line: string, actions: {id: string, label: string}[]}|null}
418
418
  */
419
419
  export function endedFor(summary, death, o = {}) {
420
- const base = endedBase(summary, death, o);
420
+ const base = withEditAction(endedBase(summary, death, o));
421
421
  const wt = o.worktree;
422
422
  if (!base || !wt) return base;
423
423
  // P6 item 1 — the run worked on a worktree in the person's own repo. GREEN: the branch carries the work (the folder is
@@ -432,21 +432,31 @@ export function endedFor(summary, death, o = {}) {
432
432
  return wt.exists ? { ...base, next: `${base.next} Your work is in ${wt.folder}.` } : base;
433
433
  }
434
434
 
435
+ /**
436
+ * "Edit in chat" (hamr 2026-10-07): offered on EVERY non-green ending (failed, capped, stopped, died, every other red),
437
+ * after Resume when both show; a green ending keeps Reuse workflow only. ONE place, so no ending branch can forget it.
438
+ * @param {{reason: string, next: string, line: string, actions: {id: string, label: string}[]}|null} ended
439
+ */
440
+ function withEditAction(ended) {
441
+ if (!ended || ended.actions.some((a) => a.id === 'reuse')) return ended;
442
+ return { ...ended, actions: [...ended.actions, { id: 'edit', label: 'Edit in chat' }] };
443
+ }
444
+
435
445
  /** @param {any} summary @param {any} death @param {any} o */
436
446
  function endedBase(summary, death, o) {
437
447
  const resumeOk = !!(o.resume && o.resume.ok);
438
448
  /** @type {{id: string, label: string}[]} */
439
449
  const RESUME = [{ id: 'resume', label: 'Resume' }];
440
450
  /** Reuse workflow (replaces P5 item 3's Start from this, hamr 2026-10-03): the same signed job on a new source — a NEW
441
- * run, never a rerun. Offered on GREEN rows only; every red row says "Change the job: Clear the card and draft a new one" instead. */
451
+ * run, never a rerun. Offered on GREEN rows only; every red row says "Change the job: press Edit in chat" instead. */
442
452
  const REUSE = [{ id: 'reuse', label: 'Reuse workflow' }];
443
- const CHANGE = 'Change the job: Clear the card and draft a new one.';
453
+ const CHANGE = 'Change the job: press Edit in chat.';
444
454
  const detailOf = (/** @type {string|null} */ s) => {
445
455
  if (typeof s !== 'string' || s.length === 0) return '';
446
456
  return s.length > 120 ? `${s.slice(0, 120)}…` : s;
447
457
  };
448
458
  /** a resumable ending whose Resume the engine would refuse: say so, never offer the button */
449
- const resumeOr = (/** @type {string} */ okNext) => (resumeOk ? okNext : `Resume is not available for this run${o.resume && o.resume.why ? ` (${o.resume.why})` : ''}.`);
459
+ const resumeOr = (/** @type {string} */ okNext) => (resumeOk ? okNext : `Resume is not available for this run${o.resume && o.resume.why ? ` (${o.resume.why})` : ''}. ${CHANGE}`);
450
460
 
451
461
  if (death.died) {
452
462
  return {
@@ -497,13 +507,20 @@ function endedBase(summary, death, o) {
497
507
 
498
508
  if (outcome === 'green' || outcome === 'already-green' || outcome === 'satisfied') {
499
509
  if (o.destinationRefused) {
510
+ // Reuse workflow only on a run that earned its green (hamr 2026-10-08, as below); the rest take Edit in chat
511
+ // (withEditAction adds it) and are told to change the job, not to reuse it
512
+ const earned = outcome === 'green';
500
513
  return {
501
514
  reason: 'Goal met, but the output could not be delivered.',
502
- next: 'Fix the destination, then Reuse workflow.',
515
+ next: earned ? 'Fix the destination, then Reuse workflow.' : CHANGE,
503
516
  line: `${GOAL_MET_LINE} — not delivered`,
504
- actions: REUSE,
517
+ actions: earned ? REUSE : [],
505
518
  };
506
519
  }
520
+ // hamr 2026-10-08: already-green (the check passed before any work) is not a reusable job — only a run that
521
+ // earned its green offers Reuse workflow; the rest take Edit in chat (withEditAction adds it). `satisfied` is a
522
+ // close VERDICT, never a run outcome (ralph and the job-end emitters write `green`), so it rides with already-green.
523
+ if (outcome !== 'green') return { reason: 'Goal met.', next: CHANGE, line: GOAL_MET_LINE, actions: [] };
507
524
  return { reason: 'Goal met.', next: 'Nothing to do.', line: GOAL_MET_LINE, actions: REUSE };
508
525
  }
509
526
  if (outcome === 'cap-halt') {
@@ -545,7 +562,7 @@ function endedBase(summary, death, o) {
545
562
  if (outcome === 'step-stalled') {
546
563
  return {
547
564
  reason: 'A step stopped making progress.',
548
- next: resumeOr('Resume, or change the job: Clear the card and draft a new one.'),
565
+ next: resumeOr('Resume, or change the job: press Edit in chat.'),
549
566
  line: resumeOk ? 'step stalled — resume' : 'step stalled',
550
567
  actions: resumeOk ? RESUME : [],
551
568
  };
@@ -2408,7 +2425,7 @@ export function reuseLine(record) {
2408
2425
  * to copy: change the job: Clear the card). `null` when the runid is not listed.
2409
2426
  * @param {string} runid
2410
2427
  * @param {{ home?: string, model?: string }} [opts] `model` = the Model box's current Name (the estimate counts only runs on that worker); absent = the card's own Model
2411
- * @returns {{ok: true, origin: {runid: string, job: string}, card: Record<string, any>, from: 'signed job',
2428
+ * @returns {{ok: true, origin: {runid: string, job: string}, outcome: string|null, card: Record<string, any>, from: 'signed job',
2412
2429
  * locked: readonly string[], open: readonly string[], note: string|null,
2413
2430
  * specHash: string, workflowKey: string, spec: any, specPath: string,
2414
2431
  * trackRecord: ReturnType<typeof trackRecordFor>, line: string}|{ok: false, error: string}|null}
@@ -2447,6 +2464,8 @@ export function getStartFrom(runid, opts = {}) {
2447
2464
  return {
2448
2465
  ok: true,
2449
2466
  origin: { runid, job: row.job },
2467
+ // the run's terminal outcome — the Reuse start (POST /api/author/start) refuses anything but a real green
2468
+ outcome: legsOf(records).at(-1)?.outcome ?? null,
2450
2469
  card,
2451
2470
  from: 'signed job',
2452
2471
  locked: REUSE_LOCKED_FIELDS,
@@ -2480,8 +2499,9 @@ export function listReuseJobs(opts = {}) {
2480
2499
  let records;
2481
2500
  try { records = parseJsonl(row.spine).records; } catch { continue; }
2482
2501
  const outcome = legsOf(records).at(-1)?.outcome ?? null;
2483
- // the same endings the Run tab's own Reuse button is offered on (`endedBase`)
2484
- if (outcome !== 'green' && outcome !== 'already-green' && outcome !== 'satisfied') continue;
2502
+ // only a REAL green (hamr 2026-10-08): `already-green` means the check passed before any work, so there is nothing to reuse
2503
+ // (src/reuse.js 'green-predates-run'). Narrower than the Run tab's Reuse button on purpose.
2504
+ if (outcome !== 'green') continue;
2485
2505
  const signed = signedSpecForRun(row, records);
2486
2506
  if (!signed) continue;
2487
2507
  const key = workflowKey(signed.spec);
@@ -2,7 +2,7 @@
2
2
  // hamr's ruling 2026-10-04 (option A): every surface (run cards, expanded run rows, the right-side header, imported
3
3
  // rows) reads `status` from the server; the page never maps an outcome to a word itself.
4
4
  //
5
- // [▶] running · [·] waiting · [✓] passed · [✗] failed · [✗] capped · [✗] stopped · [?] died
5
+ // [▶] running · [·] waiting · [✓] passed · [✗] failed · [✗] capped · [■] stopped · [?] died
6
6
  //
7
7
  // Outcome -> word (every outcome the panel knows; anything not listed is `failed`):
8
8
  // green, already-green, satisfied -> passed
@@ -15,6 +15,8 @@
15
15
  // and every other outcome (refusals, instrument stops) -> failed
16
16
  // `waiting` is the step-level sign for a step that has not started; no run outcome maps to it.
17
17
 
18
+ import { capKindOf } from '../legs.js';
19
+
18
20
  /** the sentence a passed run carries as its reason (the Ended block's `line`) */
19
21
  export const GOAL_MET_LINE = 'goal met';
20
22
 
@@ -27,7 +29,7 @@ export const STATUS = Object.freeze({
27
29
  passed: Object.freeze({ sign: '✓', word: 'passed' }),
28
30
  failed: Object.freeze({ sign: '✗', word: 'failed' }),
29
31
  capped: Object.freeze({ sign: '✗', word: 'capped' }),
30
- stopped: Object.freeze({ sign: '✗', word: 'stopped' }),
32
+ stopped: Object.freeze({ sign: '■', word: 'stopped' }),
31
33
  died: Object.freeze({ sign: '?', word: 'died' }),
32
34
  });
33
35
 
@@ -44,9 +46,8 @@ export function statusFor(o) {
44
46
  if (o.died) key = 'died';
45
47
  else if (out === null || out === undefined) key = 'running';
46
48
  else if (out === 'green' || out === 'already-green' || out === 'satisfied') key = 'passed';
47
- else if (out === 'cap-halt' || out === 'wall-halt') key = 'capped';
49
+ else if (capKindOf(o) !== null) key = 'capped';
48
50
  else if (out === 'stopped') key = 'stopped';
49
- else if (out === 'escalated' && ((o.category === 'cap-halt' && o.moneyHalt) || o.category === 'wall-halt')) key = 'capped';
50
51
  return { key, ...STATUS[key] };
51
52
  }
52
53
 
package/src/planrun.js CHANGED
@@ -36,6 +36,7 @@ import { createLadder, STRIKE_LIMIT } from './ladder.js';
36
36
  import { createTrend, FIX_STRIKE_LIMIT } from './trend.js';
37
37
  import { TOOL_MENU, STORE_VERBS, checkMenu } from './job.js';
38
38
  import { TOOL_BY_VERB, CTX_TOOLS, createCtxTools, toolAction, PERSONA_TOOLS, strategyFor } from './tools.js';
39
+ import { anySuppressionWarning } from './authoring.js';
39
40
  import { createReadShim, readShimArm, readShimStrategy } from './readshim.js';
40
41
  import { globToPrefix, redactSecrets, SECRET_PATTERNS } from './validate.js';
41
42
  import { validateBridge, loadGate, newestEligibleVersion, reuseEligibility, quarantinesCredit, QUARANTINED_CODE } from './bridges.js';
@@ -2035,7 +2036,7 @@ export async function runPlan(job, { workdir, provider, nativeProvider, provider
2035
2036
  }
2036
2037
  const listed = await seedListing(workdir, s.seedRef);
2037
2038
  const v = listed.stop === null
2038
- ? validateCloseDecl(job.closeDecl, { at: 'closeDecl', listing: listed.files, verdictType: job.verdictType })
2039
+ ? validateCloseDecl(job.closeDecl, { at: 'closeDecl', listing: listed.files, verdictType: job.verdictType, writeScope: job.writeScope })
2039
2040
  : { ok: false, grounded: false, reds: [{ code: 'listing-unreadable', path: 'closeDecl', detail: listed.stop }] };
2040
2041
  emit('close-decl', {
2041
2042
  genre: job.closeDecl.genre,
@@ -2546,7 +2547,7 @@ export async function runPlan(job, { workdir, provider, nativeProvider, provider
2546
2547
  const readLines = grantedNames.has(TOOL_BY_VERB.read)
2547
2548
  ? (native && !shimArm.cap ? NATIVE_READ_STRATEGY : '') + readShimStrategy(shimArm)
2548
2549
  : '';
2549
- const system = PERSONA_TOOLS + strategyFor(granted) + readLines;
2550
+ const system = PERSONA_TOOLS + strategyFor(granted) + readLines + anySuppressionWarning(closeStagesOf(job));
2550
2551
  /** @param {any} u @returns {{inputTokens: number, outputTokens: number, cacheReadTokens: number, cacheCreationTokens: number}} */
2551
2552
  const usageOf = (u) => ({ inputTokens: u?.inputTokens ?? 0, outputTokens: u?.outputTokens ?? 0, cacheReadTokens: u?.cacheReadTokens ?? 0, cacheCreationTokens: u?.cacheCreationTokens ?? 0 });
2552
2553
 
@@ -4108,7 +4109,7 @@ export async function runPlan(job, { workdir, provider, nativeProvider, provider
4108
4109
  // is not passed — it survives only inside the governor, as the bound for the
4109
4110
  // case the trend instrument is blind.
4110
4111
  middle, judge: async () => judgeClose(), ladder: fixGovernor, emit: emitL, redact: scrub,
4111
- closeTimeoutMs, cwd: workdir, workerWrites: w.workerWrites, closeDir,
4112
+ closeTimeoutMs, cwd: workdir, workerWrites: w.workerWrites, closeDir, phase: 'fix',
4112
4113
  });
4113
4114
  } catch (e) {
4114
4115
  planExecuted();
package/src/providers.js CHANGED
@@ -115,6 +115,33 @@ const OPENAI_MODEL_OPTIONS = Object.freeze({
115
115
  'deepseek-flash': Object.freeze({ legacyMaxTokens: true }),
116
116
  });
117
117
 
118
+ /**
119
+ * Per-MODEL JUDGE-ONLY request options (F192, hamr ruling A 2026-10-09). deepseek-flash
120
+ * thinks by default and bills the reasoning as output: run mv117wde's rubric judge locate
121
+ * truncated at JUDGE_MAX_TOKENS=4000 four times, and a probe measured 1835 of 2409
122
+ * completion tokens as reasoning. The judge extracts quotes and decides nothing, so it
123
+ * runs with thinking off. These are PER-CALL options (bare-agent >=0.49 forwards
124
+ * `thinking` verbatim to `body.thinking`), never constructor options: the judge may be
125
+ * the very same provider instance as the worker (`buildRunnerProviders` reuses it), and
126
+ * the worker is deliberately left as it was. A model with no entry gets nothing; the
127
+ * field is never sent to a model not listed here (OpenAI/Anthropic would reject or
128
+ * misread it). Read through `judgeCallOptions` only — one owner.
129
+ */
130
+ const OPENAI_JUDGE_CALL_OPTIONS = Object.freeze({
131
+ 'deepseek-flash': Object.freeze({ thinking: Object.freeze({ type: 'disabled' }) }),
132
+ });
133
+
134
+ /**
135
+ * The per-call options the rubric JUDGE's model calls carry for this provider
136
+ * instance, from the allow-list above. Empty for any provider/model not listed.
137
+ * @param {any} provider a constructed bare-agent provider
138
+ * @returns {Record<string, any>}
139
+ */
140
+ export function judgeCallOptions(provider) {
141
+ if (!(provider instanceof OpenAIProvider)) return {};
142
+ return OPENAI_JUDGE_CALL_OPTIONS[/** @type {keyof typeof OPENAI_JUDGE_CALL_OPTIONS} */ (provider.model)] ?? {};
143
+ }
144
+
118
145
  /**
119
146
  * @typedef {object} ProviderTableEntry
120
147
  * @property {new (options: any) => any} ctor the bare-agent provider class
package/src/ralph.js CHANGED
@@ -562,9 +562,13 @@ export const CLOSE_FAULTS = Object.freeze({
562
562
  * (PRD item 27/M3 Part B), forwarded to the raw `close` path's `runClose`
563
563
  * call unchanged; a caller supplying `judge` instead already closed over
564
564
  * its own `closeDir` and this param does nothing for it.
565
+ * @param {string} [opts.phase] the phase this loop runs in, when it is not the step loop
566
+ * (the fix loop passes `'fix'`): the person's-stop text then names it ("during the fix
567
+ * phase") instead of saying a resume re-enters "this step", which only the step loop has.
568
+ * Text only — it changes no category, outcome or option.
565
569
  * @returns {Promise<'green'|'escalated'>}
566
570
  */
567
- export async function ralph({ middle, close, judge, capRuns, ladder, emit, redact, closeTimeoutMs, cwd, expect, judged, gapKeep, workerWrites, closeDir }) {
571
+ export async function ralph({ middle, close, judge, capRuns, ladder, emit, redact, closeTimeoutMs, cwd, expect, judged, gapKeep, workerWrites, closeDir, phase }) {
568
572
  // PARAM GUARD (the BA-4 class — a caller's malformed argument, which is the one
569
573
  // thing this shell throws for; a WORKER's bad reach comes back as a refusal
570
574
  // RESULT instead, and that rule is about feedback, not about the API contract).
@@ -654,7 +658,9 @@ export async function ralph({ middle, close, judge, capRuns, ladder, emit, redac
654
658
  'revise the goal/spec so the work fits the time (same re-approval)',
655
659
  'abandon the task']],
656
660
  // P5 item 5 — the PERSON asked to stop at a round boundary: nothing failed, nothing is discarded.
657
- 'stopped': ['You stopped the run. Nothing is discarded: the work on disk stands, and a resume re-enters this step.',
661
+ 'stopped': [phase
662
+ ? `You stopped the run during the ${phase} phase. Nothing is discarded: the work on disk stands, and a resume continues the run.`
663
+ : 'You stopped the run. Nothing is discarded: the work on disk stands, and a resume re-enters this step.',
658
664
  ['resume (the same run continues)', 'abandon the task']],
659
665
  // F66 — the STALL WATCHDOG gave up: no round completed for the stall window,
660
666
  // three times over, and reissuing the call did not recover it. It borrows both
package/src/replay.js CHANGED
@@ -104,7 +104,7 @@
104
104
  import { runBehaviour, formatBehaviour } from './behaviour.js';
105
105
  import { shortSha } from './codeversion.js';
106
106
  import { SPEND_RECORD_TYPES, chainSpend } from './ledger.js';
107
- import { legsOf } from './legs.js';
107
+ import { legsOf, capKindOf } from './legs.js';
108
108
 
109
109
  /**
110
110
  * @param {unknown} e
@@ -234,6 +234,16 @@ const ALL_REASON_MAX = 60;
234
234
  * record fired) — printed literally, never blank, never a fabricated 0. */
235
235
  const NONE_RECORDED = 'none recorded';
236
236
 
237
+ /**
238
+ * Did this leg end on a cap? Reads the leg's own last escalation and `money-halt` record.
239
+ * @param {import('./legs.js').Leg} leg
240
+ * @returns {'money'|'time'|null}
241
+ */
242
+ function legCapKind(leg) {
243
+ const esc = leg.outcome === 'escalated' ? leg.records.findLast((r) => r && r.type === 'escalation') : null;
244
+ return capKindOf({ outcome: leg.outcome, category: esc?.category ?? null, moneyHalt: leg.records.some((r) => r && r.type === 'money-halt') });
245
+ }
246
+
237
247
  /**
238
248
  * `stopReason` for a non-green outcome: `category — decision — detail`, every
239
249
  * part present that the escalation actually carries (see the field-site
@@ -737,11 +747,21 @@ export function replayRun(spineEvents, auditEvents = [], { runId = null, auditAv
737
747
  // why this leg stopped the step (code-owned words); null for a step that ended itself or is still running
738
748
  /** @type {string|null} */
739
749
  let stopReason = null;
750
+ // a single-leg run has no leg boundary to cut a step, but a cap still ends it: its open step reads capped too
751
+ // (hamr 2026-10-08), from the same rule (`capKindOf`) the run's own status word uses
752
+ if (!end && myLeg === null) {
753
+ const only = legs[0];
754
+ const capKind = only ? legCapKind(only) : null;
755
+ if (capKind !== null) stopReason = capKind === 'money' ? 'money cap' : 'time cap';
756
+ }
740
757
  if (!end && myLeg !== null) {
741
758
  const o = myLeg.outcome;
742
759
  const isLastLeg = myLegIdx === legs.length - 1;
743
760
  if (cutByLeg || (isLastLeg && o !== null && o !== 'green' && o !== 'already-green')) {
744
- stopReason = o === 'cap-halt' ? 'money cap' : o === 'wall-halt' ? 'time cap' : o === 'provider-red' ? 'provider failed'
761
+ // hamr 2026-10-08: a leg that ended `escalated` on a money-halt / wall-halt reads as the cap, exactly as the
762
+ // run's own status word does (src/panel/status.js statusFor) — derived HERE so every surface follows
763
+ const capKind = legCapKind(myLeg);
764
+ stopReason = capKind === 'money' ? 'money cap' : capKind === 'time' ? 'time cap' : o === 'provider-red' ? 'provider failed'
745
765
  : o === 'step-stalled' ? 'step stalled' : o === 'stopped' ? 'you stopped it' : o === null ? 'died' : o;
746
766
  }
747
767
  }
@@ -832,7 +852,7 @@ export function replayRun(spineEvents, auditEvents = [], { runId = null, auditAv
832
852
  return {
833
853
  id,
834
854
  occurrence: idOccurrence.get(id),
835
- ...(myLeg === null ? {} : { stopReason, continued, tryNumber }),
855
+ ...(myLeg === null && stopReason === null ? {} : { stopReason, continued, tryNumber }),
836
856
  outcome: end ? (end.outcome ?? null) : null,
837
857
  ...buildOccurrenceMetrics(startSeq, startTs, endSeq, endTs, roundsInWindow),
838
858
  checks: { passed, failed },
package/src/trend.js CHANGED
@@ -272,6 +272,7 @@ export function createTrend({ stageOrder = [], limit = FIX_STRIKE_LIMIT, blindCa
272
272
  * @param {{gap?: string, stage?: string|null, value?: number|null}} o either a
273
273
  * raw gap (parsed here) or an already-read `{stage, value}` — never both
274
274
  * spellings of the same reading in one call.
275
+ * (A judged stage's value is its distinct red COUNT, supplied by the caller.)
275
276
  * @returns {{iteration: number, stage: string, value: number|null, improved: boolean,
276
277
  * comparable: boolean, noProgress: number, limit: number, stageIndex: number}}
277
278
  */
@@ -80,6 +80,14 @@ export function envCapableKind(kind: string, catalogue?: Record<string, KindSpec
80
80
  * @param {string} cmd @returns {string|null}
81
81
  */
82
82
  export function deniedCommandReason(cmd: string): string | null;
83
+ /**
84
+ * The warning line for a job whose ACTUAL stages carry the js `any` pattern, else
85
+ * ''. Derived from the stages' patterns (never goal prose), so a job without the
86
+ * guard is told nothing about it.
87
+ * @param {any[]|null|undefined} stages
88
+ * @returns {string}
89
+ */
90
+ export function anySuppressionWarning(stages: any[] | null | undefined): string;
83
91
  /**
84
92
  * The picked class's mandatory guards, FULLY ENUMERATED (D13 forward-compat
85
93
  * point 3: an omittable-with-a-default field is the omitted-`tools` shape, where
@@ -219,6 +227,16 @@ export function ungroundedGenreEnv(lang: string, recorded: Record<string, string
219
227
  element: string;
220
228
  resolved: string;
221
229
  }[];
230
+ /**
231
+ * Which of a judged stage's paths lie OUTSIDE the job's write fence? One owner of the rule "a check must be
232
+ * free to edit every file it can report on", called from every door that validates a declaration. It uses
233
+ * the shipped fence spelling (`globToPrefix`, the same containment reading plan.js's `insideFence` has) and
234
+ * returns `[]` when there is no fence to compare against or the paths are not a string list (shape is the
235
+ * schema walk's business).
236
+ * @param {unknown} paths @param {string[]|null|undefined} writeScope
237
+ * @returns {string[]}
238
+ */
239
+ export function judgedOutsideFence(paths: unknown, writeScope: string[] | null | undefined): string[];
222
240
  /**
223
241
  * Validate an authored close DECLARATION. Never throws on model output — every
224
242
  * failure is a named red, in the shipped `{ok, reds, <parsed>}` shape, and the
@@ -256,7 +274,9 @@ export function ungroundedGenreEnv(lang: string, recorded: Record<string, string
256
274
  * @param {{catalogue?: Record<string, KindSpec>, listing?: string[]|null,
257
275
  * guards?: {name: string, kind: string, params: Record<string, any>, fill: string[]}[]|null,
258
276
  * envOwned?: string[]|null, envInjected?: Record<string, string>|null, deferListing?: boolean,
259
- * verdictType?: string|null}} [opts]
277
+ * verdictType?: string|null, writeScope?: string[]|null}} [opts]
278
+ * `writeScope` — the job's signed write fence. Every path a judged stage judges must sit inside it
279
+ * ({@link judgedOutsideFence}); absent, the rule is not run (the plain-folder deferral has no fence).
260
280
  * `listing` — repo-relative paths at the seed (`git ls-tree -r --name-only`).
261
281
  * `guards` — the picked class's injected guards (`classGuards`).
262
282
  * `verdictType` — the class the USER picked (`VERDICT_CLASSES`).
@@ -283,6 +303,7 @@ export function validateDeclaration(declaration: any, opts?: {
283
303
  envInjected?: Record<string, string> | null;
284
304
  deferListing?: boolean;
285
305
  verdictType?: string | null;
306
+ writeScope?: string[] | null;
286
307
  }): {
287
308
  ok: boolean;
288
309
  reds: Red[];
@@ -455,6 +476,8 @@ export const DENIED_COMMANDS: readonly string[];
455
476
  */
456
477
  export const TYPES_GENRE_TEMPLATE: "1. The STRICT form of the language's type checker is used only when a confirmed check \u2014 or,\n absent a confirm turn, the goal and interview answers \u2014 asks for strict typing; otherwise the\n checker runs at whatever strictness the repo's own scripts already use.\n2. Tools are invoked so their binaries actually resolve \u2014 through the project's own package\n runner or language module runner, never a bare binary name.\n3. The WORK stages \u2014 typecheck (error count IN the target scope, baseline 0, lower-is-better),\n typecheck-outside (error count OUTSIDE the target scope, baseline measured at seed, a\n ceiling \u2014 required whenever a typecheck stage is scoped to a subset of the tree), tests-kept\n (a floor on tests that actually EXECUTED, baseline at seed, higher-is-better; a skipped or\n deselected test did not run and must not count), and suite-green (the suite exits clean AND\n reports zero failing tests \u2014 two assertions) \u2014 are composed ONLY for the checks a confirmed\n plan asked for (or, absent a confirm turn, the goal and interview answers). The GUARDS \u2014\n changed-from-seed and no-suppressions \u2014 are composed always, first-red-wins:\n changed-from-seed before every WORK stage, no-suppressions last.\n4. One population per stage \u2014 two structurally different counts never share a stage.\n5. The checker must judge the PATIENT's own tree: if imports could resolve to an installed or\n editable copy elsewhere, the environment is set so they resolve inside the patient.\n6. A number the tool did not report is unknown, never zero.";
457
478
  export const GENRE_LANGUAGES: readonly string[];
479
+ /** The worker-facing line that rides with the js `any` guard (run mv13ery3). */
480
+ export const ANY_SUPPRESSION_WARNING: string;
458
481
  /**
459
482
  * THE GENRE, as data. The suppression batteries are the hand-written closes'
460
483
  * own `SUPPRESSIONS` tables (`scripts/u-pulselog-close.mjs:35`,
@@ -185,6 +185,14 @@ export function assembleSpec(specDraft: any, { closeDecl, verdictType }: {
185
185
  closeDecl: any;
186
186
  verdictType: string;
187
187
  }): any;
188
+ /**
189
+ * THE one spelling of "a passed calibration gate stands in for the seed red": the
190
+ * close carries a judged stage AND the gate ran and passed. A failed or missing
191
+ * calibration never satisfies it, and a close with no judged stage never reaches it.
192
+ * @param {any[]} judged `judgedStages(closeDecl)` @param {any} calibration `gates.calibration`
193
+ * @returns {boolean}
194
+ */
195
+ export function seedProofIsCalibration(judged: any[], calibration: any): boolean;
188
196
  /**
189
197
  * D9, in code: *nothing JUDGES the close; three mechanical gates plus a
190
198
  * signature.* No LLM validates another LLM's close, here or anywhere.
@@ -206,16 +214,15 @@ export function assembleSpec(specDraft: any, { closeDecl, verdictType }: {
206
214
  * has already had its chance, and nobody pays to discover a broken
207
215
  * declaration.
208
216
  *
209
- * D9.3 AND THE JUDGED-ONLY CLOSE (2026-08-18 second addendum, ruling 3; hamr:
210
- * *"fix it now, we are delivering softgreen, isn't that the whole point?"*). Gate
211
- * 3 protects against a close that cannot fail — one that scans nothing and reads
212
- * green at seed against a true red. A judged stage SKIPS the seed read by ruling
213
- * 8, so a close whose only work stage is judged has no seed red to show and used
214
- * to be unsignable: the one job shape softgreen exists for. For that shape, and
215
- * ONLY that shape, a PASSED calibration gate plays the seed-red role — it is that
216
- * close's own proof of failability, held to the same all-or-nothing bar. A close
217
- * with a mechanical work stage is UNCHANGED: it still needs a seed red, whatever
218
- * its calibration says.
217
+ * D9.3 AND THE JUDGED CLOSE (2026-08-18 second addendum, ruling 3; hamr: *"fix it now,
218
+ * we are delivering softgreen, isn't that the whole point?"*; widened 2026-10-09 to mixed
219
+ * closes, hamr: *"a rubric job should check if judge can judge (calibration) and judge
220
+ * rules can apply properly to pass ... we can't close everything upfront"*). Gate 3
221
+ * protects against a close that cannot fail. A judged stage SKIPS the seed read by ruling
222
+ * 8, so it can never show red there. For ANY close carrying a judged stage (pure or
223
+ * mixed with mechanical stages), a PASSED calibration gate plays the seed-red role
224
+ * (`seedProofIsCalibration`). A close with NO judged stage is UNCHANGED: some work stage
225
+ * must be red at the seed. A failed or missing calibration never satisfies it.
219
226
  *
220
227
  * Gates 2 and 3 share ONE execution, and that is stated rather than hidden: at
221
228
  * job creation the tree IS the seed, so "does every stage run against the real
@@ -87,6 +87,20 @@ export function answeredQuestionLines(confirmed: {
87
87
  * @returns {string[]}
88
88
  */
89
89
  export function calibrationLines(calibration: any): string[];
90
+ /**
91
+ * The calibration gate's real result as one line a person reads next to the step (hamr, 2026-10-09: the panel
92
+ * showed `calibrating ✓` over a gate that graded 8 of 10 and let 2 attack styles leak). PASS IS THE GATE'S OWN:
93
+ * `ok` is `calibration.ok` as `runCalibration` decided it (every case right as a SET of reds, every style
94
+ * resisted), never recomputed here, so there is no second spelling of the floor. The counts are read from the
95
+ * record (`graded[].ok` is `compareExpectation`, which compares reds as sets); the denominators are the live
96
+ * constants (CALIBRATION_SIZE, the battery length), so a gate that stopped part-way reads "x of 10", not "x of x".
97
+ * @param {any} calibration `signing.gates.calibration`
98
+ * @returns {{ok: boolean, note: string}|null} null when the gate was not reached
99
+ */
100
+ export function calibrationSummary(calibration: any): {
101
+ ok: boolean;
102
+ note: string;
103
+ } | null;
90
104
  /**
91
105
  * THE AUTHORING CEILING, parsed from `--budget`. Housed here for the same reason
92
106
  * the readout above is: the runner is a script, and a rule no test can reach is a
@@ -169,4 +183,96 @@ export function crashRecord(err: unknown): {
169
183
  code: string | null;
170
184
  raw: ReturnType<typeof scrubRaw>;
171
185
  };
186
+ /**
187
+ * Every plain kind the table can emit — the list the test enumerates against.
188
+ * @returns {string[]}
189
+ */
190
+ export function plainKinds(): string[];
191
+ /**
192
+ * Map the technical reds of a refused proposal to plain sentences, one per red.
193
+ * An unknown code is NEVER dropped: it becomes a generic sentence that still
194
+ * quotes the code and path, under the kind `unmapped`.
195
+ * @param {PlainRed[]} reds
196
+ * @param {{cases?: any}} [o] the proposed cases, to name a case by its own id
197
+ * @returns {PlainReason[]}
198
+ */
199
+ export function plainReasons(reds: PlainRed[], { cases }?: {
200
+ cases?: any;
201
+ }): PlainReason[];
202
+ /**
203
+ * THE MESSAGE a person reads when the rubric proposal was refused. Both doors
204
+ * (the panel's failed step and the CLI) print exactly this.
205
+ * @param {{stop: string, reds: PlainRed[], cases?: any, spend?: {knownUsd?: number|null, spendComplete?: boolean|null}|null,
206
+ * source?: 'proposal'|'signer'}} o
207
+ * @returns {string}
208
+ */
209
+ export function proposalStopText({ stop, reds, cases, spend, source }: {
210
+ stop: string;
211
+ reds: PlainRed[];
212
+ cases?: any;
213
+ spend?: {
214
+ knownUsd?: number | null;
215
+ spendComplete?: boolean | null;
216
+ } | null;
217
+ source?: "proposal" | "signer";
218
+ }): string;
219
+ /**
220
+ * Plain sentences for the reds of a refused signing, one or more per red. Card and practice-case reds reuse the
221
+ * proposal table; an unknown code is a generic sentence that quotes the code.
222
+ * @param {PlainRed[]} reds @param {{cases?: any}} [o]
223
+ * @returns {{kind: string, sentence: string, todo: string, red: PlainRed}[]}
224
+ */
225
+ export function signingReasons(reds: PlainRed[], { cases }?: {
226
+ cases?: any;
227
+ }): {
228
+ kind: string;
229
+ sentence: string;
230
+ todo: string;
231
+ red: PlainRed;
232
+ }[];
233
+ /** every plain kind `signingReasons` can emit besides the proposal table's — what the test enumerates
234
+ * @returns {string[]} */
235
+ export function signingKinds(): string[];
236
+ /**
237
+ * THE MESSAGE a person reads when the signing gates refuse. The panel's failed step and the CLI print exactly this.
238
+ * @param {{reds: PlainRed[], refusal?: {detail?: string}|null, cases?: any,
239
+ * spend?: {knownUsd?: number|null, spendComplete?: boolean|null}|null}} o
240
+ * @returns {string}
241
+ */
242
+ export function signingStopText({ reds, refusal, cases, spend }: {
243
+ reds: PlainRed[];
244
+ refusal?: {
245
+ detail?: string;
246
+ } | null;
247
+ cases?: any;
248
+ spend?: {
249
+ knownUsd?: number | null;
250
+ spendComplete?: boolean | null;
251
+ } | null;
252
+ }): string;
253
+ /**
254
+ * The reds as the drafting log keeps them: every structured field, the plain
255
+ * sentence beside it, every string secret-scrubbed and bounded, the list capped.
256
+ * @param {PlainRed[]} reds @param {{cases?: any, signing?: boolean}} [o] `signing`: the reds are the signing gates', worded by signingReasons
257
+ * @returns {Record<string, any>[]}
258
+ */
259
+ export function redsRecord(reds: PlainRed[], { cases, signing }?: {
260
+ cases?: any;
261
+ signing?: boolean;
262
+ }): Record<string, any>[];
263
+ /** the stops this module explains in plain words */
264
+ export const PLAIN_PROPOSAL_STOPS: readonly string[];
265
+ /** codes the declaration gates (validateJob, validateCloseDecl) raise: one plain sentence covers the family */
266
+ export const DECLARATION_CODES: readonly string[];
267
+ export type PlainRed = {
268
+ code: string;
269
+ path: string;
270
+ detail: string;
271
+ [k: string]: any;
272
+ };
273
+ export type PlainReason = {
274
+ kind: string;
275
+ sentence: string;
276
+ red: PlainRed;
277
+ };
172
278
  import { scrubRaw } from './text.js';
@@ -90,7 +90,7 @@ export function factsResist(facts: any, gold: any, artifactText: string): {
90
90
  * onCost?: (c: any) => void, attempts?: number, battery?: any[], batteryCard?: any,
91
91
  * capStop?: () => 'cap-halt'|'pricing-red'|null}} o
92
92
  * @returns {Promise<{ok: boolean, stop: string|null, casualty: any,
93
- * graded: {id: string, ok: boolean, got: any, want: any, detail: string|null}[],
93
+ * graded: {id: string, ok: boolean, got: any, want: any, detail: string|null, diag: any}[],
94
94
  * injection: {styles: any[], allResisted: boolean, leaks: string[]},
95
95
  * failures: string[], reds: Red[], judgeModel: string,
96
96
  * cardHash: string|null, casesHash: string|null, setHash: string|null,
@@ -116,6 +116,7 @@ export function runCalibration({ cases, card, judgeLoop, judgeModel, onCost, att
116
116
  got: any;
117
117
  want: any;
118
118
  detail: string | null;
119
+ diag: any;
119
120
  }[];
120
121
  injection: {
121
122
  styles: any[];
@@ -190,6 +191,8 @@ ${string}`;
190
191
  }>[];
191
192
  }>;
192
193
  }>[];
194
+ /** the per-string cap on anything the diagnostic keeps (bytes, scrubbed) */
195
+ export const DIAG_STRING_CAP: 400;
193
196
  export type Red = {
194
197
  code: string;
195
198
  path: string;
@@ -53,7 +53,7 @@ export function declaredStages(closeDecl: any): any[] | null;
53
53
  *
54
54
  * @param {any} closeDecl
55
55
  * @param {{at?: string, listing?: string[]|null, deferListing?: boolean,
56
- * catalogue?: Record<string, any>, verdictType?: string|null}} [opts]
56
+ * catalogue?: Record<string, any>, verdictType?: string|null, writeScope?: string[]|null}} [opts]
57
57
  * @returns {{ok: boolean, reds: Red[], closeDecl: any, grounded: boolean,
58
58
  * scoped: {scoped: boolean, via: string|null},
59
59
  * ceiling: {class: string, kind: string, stage: string|null, locked: boolean}|null}}
@@ -64,6 +64,7 @@ export function validateCloseDecl(closeDecl: any, opts?: {
64
64
  deferListing?: boolean;
65
65
  catalogue?: Record<string, any>;
66
66
  verdictType?: string | null;
67
+ writeScope?: string[] | null;
67
68
  }): {
68
69
  ok: boolean;
69
70
  reds: Red[];
package/types/judged.d.ts CHANGED
@@ -97,6 +97,15 @@ export function resolveJobJudge(source: {
97
97
  provider: string;
98
98
  model: string;
99
99
  };
100
+ /**
101
+ * THE ONE ACCEPTANCE for "this quote points at the JSDoc block of this declaration":
102
+ * every non-empty line of `docQuote` is a line inside the block directly above the
103
+ * declaration. `has-doc`'s check and the calibration gate's injection facts check
104
+ * both call this, so the two can never disagree about what a good pointer is (F192).
105
+ * @param {string} text the artifact @param {string|null} declQuote @param {string} docQuote
106
+ * @returns {boolean}
107
+ */
108
+ export function docQuoteInBlock(text: string, declQuote: string | null, docQuote: string): boolean;
100
109
  /**
101
110
  * The rubric card: Q6's answer compiled into enumerated items, signed by the
102
111
  * person and enumerated in the spec hash. `rule` selects from `JUDGE_RULES`;
@@ -359,15 +368,25 @@ export const LOCATE_AXES: Readonly<{
359
368
  PROVIDER: "provider-red";
360
369
  ARTIFACT: "artifact-red";
361
370
  }>;
371
+ /**
372
+ * Function words ONLY: articles, prepositions, conjunctions. No judgement words
373
+ * ("returns", "gets", "function"): a description that adds one of those has added
374
+ * something, and deciding whether it is MEANINGFUL is exactly what this rule
375
+ * refuses to do.
376
+ */
377
+ export const SAYS_WHAT_STOPWORDS: readonly string[];
362
378
  /**
363
379
  * THE OWNED RULE TABLE. Frozen: the rulebook is the arbiter's, and a caller that
364
380
  * could add a rule at runtime would be authoring the arbiter.
365
- * @type {Readonly<Record<string, {id: string, ask: string, check: (fn: any, lines: Set<string>|null) => RuleRed[]}>>}
381
+ * `means` is the rule's plain-words reach: what it CAN check, and what it cannot. The case-proposal
382
+ * prompt renders it, so a proposed case never expects a red the rule cannot raise.
383
+ * @type {Readonly<Record<string, {id: string, means: string, ask: string, check: (fn: any, text: string|null) => RuleRed[]}>>}
366
384
  */
367
385
  export const JUDGE_RULES: Readonly<Record<string, {
368
386
  id: string;
387
+ means: string;
369
388
  ask: string;
370
- check: (fn: any, lines: Set<string> | null) => RuleRed[];
389
+ check: (fn: any, text: string | null) => RuleRed[];
371
390
  }>>;
372
391
  /** the enumerated set a card may name — handed over verbatim in every refusal */
373
392
  export const JUDGE_RULE_IDS: readonly string[];