bareloop 0.39.0 → 0.41.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.
Files changed (76) hide show
  1. package/CHANGELOG.md +60 -0
  2. package/bareloop.context.md +137 -112
  3. package/package.json +2 -2
  4. package/src/authorflow.js +333 -58
  5. package/src/authoring.js +65 -2
  6. package/src/authorjob.js +57 -9
  7. package/src/authorreadout.js +50 -2
  8. package/src/authorrun.js +30 -27
  9. package/src/authorscout.js +9 -6
  10. package/src/bridges.js +1 -1
  11. package/src/bundle.js +7 -4
  12. package/src/bundlerun.js +4 -1
  13. package/src/calibrate.js +1 -1
  14. package/src/cardauthor.js +4 -1
  15. package/src/cli.js +1 -1
  16. package/src/config.js +129 -29
  17. package/src/declaredclose.js +4 -3
  18. package/src/draftspend.js +6 -1
  19. package/src/index.js +2 -4
  20. package/src/interviewrun.js +73 -52
  21. package/src/job.js +50 -11
  22. package/src/jobblock.js +140 -0
  23. package/src/judged.js +12 -44
  24. package/src/keysfile.js +15 -7
  25. package/src/kinds.js +2 -2
  26. package/src/ledger.js +1 -1
  27. package/src/modellist.js +84 -0
  28. package/src/monthly.js +23 -43
  29. package/src/panel/authorroutes.js +34 -19
  30. package/src/panel/authorsession.js +187 -72
  31. package/src/panel/index.html +699 -215
  32. package/src/panel/jobplan.js +107 -0
  33. package/src/panel/runroutes.js +3 -0
  34. package/src/panel/server.js +102 -28
  35. package/src/panel/settingsroutes.js +232 -173
  36. package/src/planrun.js +6 -6
  37. package/src/promptregisters.js +1 -1
  38. package/src/providerrows.js +226 -141
  39. package/src/providers.js +58 -250
  40. package/src/providershapes.js +18 -0
  41. package/src/replay.js +1 -1
  42. package/src/reuse.js +1 -1
  43. package/src/root.js +1 -1
  44. package/src/run.js +3 -3
  45. package/src/selection.js +1 -1
  46. package/src/source.js +64 -1
  47. package/src/text.js +13 -0
  48. package/src/thinking.js +130 -0
  49. package/src/transport.js +1 -1
  50. package/src/userrun.js +53 -76
  51. package/types/authorflow.d.ts +258 -39
  52. package/types/authoring.d.ts +7 -1
  53. package/types/authorjob.d.ts +35 -3
  54. package/types/authorreadout.d.ts +17 -1
  55. package/types/authorscout.d.ts +5 -3
  56. package/types/config.d.ts +23 -6
  57. package/types/declaredclose.d.ts +4 -1
  58. package/types/draftspend.d.ts +8 -2
  59. package/types/index.d.ts +1 -1
  60. package/types/job.d.ts +6 -6
  61. package/types/jobblock.d.ts +102 -0
  62. package/types/judged.d.ts +5 -36
  63. package/types/keysfile.d.ts +15 -4
  64. package/types/modellist.d.ts +50 -0
  65. package/types/monthly.d.ts +4 -3
  66. package/types/panel/authorsession.d.ts +44 -15
  67. package/types/panel/jobplan.d.ts +58 -0
  68. package/types/panel/server.d.ts +34 -4
  69. package/types/planrun.d.ts +1 -1
  70. package/types/providerrows.d.ts +192 -99
  71. package/types/providers.d.ts +35 -116
  72. package/types/providershapes.d.ts +10 -0
  73. package/types/run.d.ts +1 -1
  74. package/types/source.d.ts +26 -0
  75. package/types/thinking.d.ts +90 -0
  76. package/types/userrun.d.ts +6 -1
package/src/authoring.js CHANGED
@@ -1199,7 +1199,10 @@ function readPath(p, dotted) {
1199
1199
  * @param {{catalogue?: Record<string, KindSpec>, listing?: string[]|null,
1200
1200
  * guards?: {name: string, kind: string, params: Record<string, any>, fill: string[]}[]|null,
1201
1201
  * envOwned?: string[]|null, envInjected?: Record<string, string>|null, deferListing?: boolean,
1202
- * verdictType?: string|null, writeScope?: string[]|null}} [opts]
1202
+ * verdictType?: string|null, writeScope?: string[]|null, jobLines?: {n: number}[]|null}} [opts]
1203
+ * `jobLines` — the person's numbered job lines (P7). Present, every stage must name the line it serves and every
1204
+ * line needs a stage or a refusal ({@link checkJobLines}); absent (every spec that predates the job block), the
1205
+ * rule is not run.
1203
1206
  * `writeScope` — the job's signed write fence. Every path a judged stage judges must sit inside it
1204
1207
  * ({@link judgedOutsideFence}); absent, the rule is not run (the plain-folder deferral has no fence).
1205
1208
  * `listing` — repo-relative paths at the seed (`git ls-tree -r --name-only`).
@@ -1218,7 +1221,7 @@ function readPath(p, dotted) {
1218
1221
  export function validateDeclaration(declaration, opts = {}) {
1219
1222
  const {
1220
1223
  catalogue = KIND_CATALOGUE, listing = null, guards = null, envOwned = null, envInjected = null,
1221
- verdictType = null, writeScope = null,
1224
+ verdictType = null, writeScope = null, jobLines = null,
1222
1225
  } = opts;
1223
1226
  const deferListing = opts.deferListing === true;
1224
1227
  /** @type {Red[]} */
@@ -1443,11 +1446,71 @@ export function validateDeclaration(declaration, opts = {}) {
1443
1446
  });
1444
1447
 
1445
1448
  if (haveGuards) checkGuards({ declaration, guards: /** @type {any[]} */ (guards), red });
1449
+ checkJobLines({ declaration, stages, jobLines, guards: haveGuards ? /** @type {any[]} */ (guards) : [], red });
1446
1450
 
1447
1451
  const ok = reds.length === 0;
1448
1452
  return { ok, reds, declaration: ok ? normalizeDeclaration(declaration) : null, grounded: haveListing, scoped, ceiling };
1449
1453
  }
1450
1454
 
1455
+ /**
1456
+ * P7 rules 1-3 of the job block, at $0, in the gate that can still revise: every `fromLine` is a non-empty, duplicate-free array of real job line numbers (one check may serve several lines);
1457
+ * every numbered job line has at least one stage OR a refusal reason in `refused` (a note in `notes` does NOT cover a
1458
+ * line); the mandatory guards serve no line and carry no `fromLine`. The SHAPE of `fromLine` / `refused` is checked
1459
+ * whenever they appear; the coverage half runs only when the person's `jobLines` were handed in.
1460
+ * @param {{declaration: any, stages: any[], jobLines: {n: number}[]|null|undefined,
1461
+ * guards: {name: string}[], red: (c: string, p: string, d: string, e?: object) => void}} o
1462
+ */
1463
+ function checkJobLines({ declaration, stages, jobLines, guards, red }) {
1464
+ const known = Array.isArray(jobLines) && jobLines.length > 0 ? jobLines.map((l) => l.n) : null;
1465
+ const guardNames = new Set(guards.map((g) => g.name));
1466
+ /** @type {Set<number>} */
1467
+ const served = new Set();
1468
+ stages.forEach((/** @type {any} */ s, /** @type {number} */ i) => {
1469
+ if (!isObj(s)) return;
1470
+ const at = `stages[${i}].fromLine`;
1471
+ const label = isNonEmptyString(s.name) ? s.name : `stages[${i}]`;
1472
+ const guard = isNonEmptyString(s.name) && guardNames.has(s.name);
1473
+ if (Object.hasOwn(s, 'fromLine')) {
1474
+ if (guard) {
1475
+ red('invalid-value', at, `stage "${label}" is a mandatory guard and serves no job line — remove its fromLine`);
1476
+ } else if (!Array.isArray(s.fromLine) || s.fromLine.length === 0 || !s.fromLine.every((/** @type {any} */ x) => Number.isInteger(x) && x >= 1)) {
1477
+ red('invalid-value', at, `stage "${label}" has fromLine ${JSON.stringify(s.fromLine)} — a non-empty array of job line numbers, e.g. [1] or [1, 3]`);
1478
+ } else if (new Set(s.fromLine).size !== s.fromLine.length) {
1479
+ red('invalid-value', at, `stage "${label}" lists a job line twice in fromLine ${JSON.stringify(s.fromLine)}`);
1480
+ } else if (known !== null && s.fromLine.some((/** @type {number} */ x) => !known.includes(x))) {
1481
+ red('invalid-value', at, `stage "${label}" has fromLine ${JSON.stringify(s.fromLine)}, but the job has lines ${known.join(', ')} only`);
1482
+ } else for (const x of s.fromLine) served.add(x);
1483
+ } else if (known !== null && !guard) {
1484
+ red('missing-field', at, `stage "${label}" does not say which job lines it serves — give it fromLine: [N, ...] (${known.join(', ')}); `
1485
+ + 'only a mandatory guard carries none');
1486
+ }
1487
+ });
1488
+ /** @type {Set<number>} */
1489
+ const refused = new Set();
1490
+ if (declaration.refused !== undefined) {
1491
+ if (!Array.isArray(declaration.refused)) {
1492
+ red('invalid-value', 'refused', 'an array of {line, reason} — the job lines no stage can check, each with a reason');
1493
+ } else {
1494
+ declaration.refused.forEach((/** @type {any} */ r, /** @type {number} */ j) => {
1495
+ const at = `refused[${j}]`;
1496
+ if (!isObj(r) || !Number.isInteger(r.line) || r.line < 1 || !isNonEmptyString(r.reason)) {
1497
+ red('invalid-value', at, '{line, reason}: a job line number and a plain-words reason');
1498
+ } else if (known !== null && !known.includes(r.line)) {
1499
+ red('invalid-value', `${at}.line`, `refused line ${r.line}, but the job has lines ${known.join(', ')} only`);
1500
+ } else refused.add(r.line);
1501
+ });
1502
+ }
1503
+ }
1504
+ if (known !== null) {
1505
+ for (const n of known) {
1506
+ if (!served.has(n) && !refused.has(n)) {
1507
+ red('job-line-uncovered', 'stages', `job line ${n} has no stage and no refusal. Give it a stage whose fromLine lists ${n}, or list `
1508
+ + `it in "refused" as {"line": ${n}, "reason": "..."} — a note does not cover a line`, { line: n });
1509
+ }
1510
+ }
1511
+ }
1512
+ }
1513
+
1451
1514
  /**
1452
1515
  * COMPLETENESS — the deletion half of the owned-env rule, and the half a
1453
1516
  * by-VALUE comparison structurally cannot cover.
package/src/authorjob.js CHANGED
@@ -113,6 +113,7 @@ import {
113
113
  QUESTION_SETS, questionsFor, requiredAnswersFor, authorClose, makeCostBook,
114
114
  runConfirmTurn, LANGUAGE_PICK_FIELD,
115
115
  } from './authorflow.js';
116
+ import { thinkingCallOptions, assertThinkingMode } from './thinking.js';
116
117
  import { runAuthorScout, buildSeedListing, SCOUT_ATTEMPTS } from './authorscout.js';
117
118
  import {
118
119
  DECLARED_GAP_PREFIX, DECLARED_GENRES, guardNames, isDeclaredClose, validateCloseDecl,
@@ -416,16 +417,19 @@ function composerRefusal(reds) {
416
417
  * one-genre-at-a-time law is untouched.
417
418
  *
418
419
  * @param {{answers: Record<string|number, any>, repoPath?: string|null, lang: string,
420
+ * jobLines?: {n: number, text: string}[]|null, inputs?: {n: number, label: string, value: string}[]|null,
419
421
  * verdictType?: string|null,
420
422
  * questions?: Record<string|number, string>|null, generate?: Function, provider?: any,
421
423
  * seedRef?: string|null, scout?: any, listing?: any, ceilingUsd?: number|null, judgeModel?: string|null,
422
- * rates?: {in: number, out: number}|null,
424
+ * rates?: {in: number, out: number}|null, draftThinking?: 'on'|'off',
423
425
  * onPhase?: (phase: string, data?: any) => void,
424
- * onCall?: (call: {label: string, costUsd: number|null, unpricedRounds: number}) => void,
426
+ * onCall?: (call: {label: string, costUsd: number|null, unpricedRounds: number, thinkingStatus?: string}) => void,
425
427
  * seedFn?: Function, scoutFn?: Function, listingFn?: Function,
426
428
  * authorFn?: Function, authorOpts?: object, writeScope?: string[]|null,
427
429
  * signerFix?: Function|any, proposeFn?: Function, compileOpts?: object,
428
430
  * ask?: ((step: {kind: string, [k: string]: any}) => Promise<string|null>)|null,
431
+ * answerQuestions?: ((o: {questions: {line: number, question: string}[], dropped: {reason: string, raw: any}[], confirmed: any}) =>
432
+ * Promise<{jobLines: {n: number, text: string, rule?: string}[], answers: Record<string|number, any>}|null>)|null,
429
433
  * confirmGenerate?: Function|null, isRepo?: boolean,
430
434
  * langResult?: {kind: string, candidates?: string[], [k: string]: any}|null}} o
431
435
  * @returns {Promise<{ok: boolean, refusal: Refusal|null, verdictType: string|null,
@@ -434,9 +438,15 @@ function composerRefusal(reds) {
434
438
  */
435
439
  export async function authorCloseForJob({
436
440
  answers, repoPath = null, lang, verdictType = null, questions = null,
441
+ // P7: the person's numbered job lines and numbered inputs (src/jobblock.js). Absent = every caller that predates the
442
+ // job block (no line tagging, no coverage rule).
443
+ jobLines = null, inputs = null,
437
444
  generate, provider = null, seedRef = null, scout = null, listing = null,
438
445
  ceilingUsd = null,
439
446
  rates = null,
447
+ // The drafter's THINKING knob ('on' | 'off', default 'off'; an operator param, never read from env here). It reaches the scout through
448
+ // `callOptions`; the `generate`/`confirmGenerate` the caller passes must be built with the SAME mode (`makeLoopGenerate`).
449
+ draftThinking = 'off',
440
450
  // Q2 IS GONE from the numbered interview (PRD item 33 M3 piece 3) — the
441
451
  // fence it used to describe in prose is now Destination's own proven
442
452
  // `writeScope`, and this is what carries it into the composer prompt
@@ -479,7 +489,14 @@ export async function authorCloseForJob({
479
489
  // shape matters here (a $0 language pick before the scout,
480
490
  // D7); every other kind leaves `lang` exactly as given.
481
491
  ask = null, confirmGenerate = null, isRepo = false, langResult = null,
492
+ // P7 item 5 (fwdloop am24/25): the seam that can really ASK the person. Present = the last drafting try, and only after
493
+ // the two before it both failed, may ask (`offerQuestions` below); the seam takes the machine-filtered questions, gets
494
+ // each answer (required, no skip), and hands back the job lines with the answers added as `~` rules plus the matching
495
+ // answers text. ABSENT (the CLI, every older caller) = nothing is ever offered and this file is byte-identical.
496
+ answerQuestions = null,
482
497
  }) {
498
+ // an operator-param guard: a typo never silently means "default" (throws before any work, even when the survey is supplied)
499
+ assertThinkingMode(draftThinking);
483
500
  /** @type {any} */
484
501
  const base = {
485
502
  ok: false, refusal: null, verdictType: null, closeDecl: null, seedRef: null,
@@ -580,7 +597,8 @@ export async function authorCloseForJob({
580
597
  // real tree. `attempts` is the ceiling the scout enforces on itself, so the
581
598
  // reader knows how many the line could repeat for.
582
599
  onPhase('scout', { attempts: SCOUT_ATTEMPTS });
583
- survey = await scoutFn({ workdir, provider, ceilingUsd, onCall, ...(rates ? { rates } : {}) });
600
+ const scoutCallOptions = thinkingCallOptions(provider, draftThinking);
601
+ survey = await scoutFn({ workdir, provider, ceilingUsd, onCall, ...(rates ? { rates } : {}), ...(Object.keys(scoutCallOptions).length ? { callOptions: scoutCallOptions } : {}) });
584
602
  // …and the survey's own VERDICT, said the moment it lands. `state` is what
585
603
  // every downstream refusal keys on (F59: absent is never "no facts needed"),
586
604
  // and the fact count is the one number that says whether the ladder came
@@ -627,7 +645,7 @@ export async function authorCloseForJob({
627
645
  onPhase('confirm', {});
628
646
  const confirm = await runConfirmTurn({
629
647
  verdictType: picked, answers: interview.answers, questions: questions ?? questionsFor(picked),
630
- facts: survey.facts, listing: null, writeScope, isRepo, lang, worseThanBefore,
648
+ facts: survey.facts, listing: null, writeScope, isRepo, lang, worseThanBefore, inputs,
631
649
  generate: confirmGenerate, book: confirmBook, ask, onPhase,
632
650
  });
633
651
  // DISTINCT from `runConfirmTurn`'s own per-round 'confirm-done' (fired once
@@ -671,13 +689,38 @@ export async function authorCloseForJob({
671
689
  // loop obeys — `author-call` carries the enforced cap instead, from where it
672
690
  // is computed.
673
691
  onPhase('author', {});
674
- const authored = await authorFn({
692
+ /** the answers text the later stages read; the try-3 redraft replaces it (the person's answers are new `~` rules) @type {Record<string|number, any>} */
693
+ let stageAnswers = interview.answers;
694
+ let authored = await authorFn({
675
695
  workdir, seedRef: seed, lang, verdictType: picked,
676
- answers: interview.answers, questions: questions ?? questionsFor(picked),
696
+ answers: stageAnswers, questions: questions ?? questionsFor(picked),
677
697
  scout: survey, listing: seeds, generate, ceilingUsd, onPhase, onCall, writeScope,
678
- priorCalls: confirmPriorCalls, priorRaws: confirmPriorRaws, confirmed,
698
+ priorCalls: confirmPriorCalls, priorRaws: confirmPriorRaws, confirmed, jobLines, inputs,
699
+ ...(answerQuestions ? { offerQuestions: true } : {}),
679
700
  ...authorOpts,
680
701
  });
702
+ // P7 item 5: the drafter, on the last try after two failed ones, asked. The person answers (required, no skip); each
703
+ // answer becomes a `~` rule on its job line; the draft then runs ONCE more over the same survey, listing and confirmed
704
+ // plan, and may not ask again. The first leg's calls are carried as prior spend so the one ceiling still binds.
705
+ if (!authored.ok && authored.stop === 'questions-open' && answerQuestions && Array.isArray(authored.questions) && authored.questions.length) {
706
+ const dropped = (authored.iterations ?? []).flatMap((/** @type {any} */ it) => it?.droppedQuestions ?? []);
707
+ const updated = await answerQuestions({ questions: authored.questions, dropped, confirmed });
708
+ if (updated === null) return { ...base, interview, authoring: authored, reds: [], stop: 'confirm-abandoned', cost: authored.cost };
709
+ const scoutCalls = (survey?.calls ?? []).length;
710
+ const scoutRaws = (survey?.raws ?? []).length;
711
+ jobLines = updated.jobLines;
712
+ stageAnswers = updated.answers;
713
+ onPhase('author', {});
714
+ authored = await authorFn({
715
+ workdir, seedRef: seed, lang, verdictType: picked,
716
+ answers: stageAnswers, questions: questions ?? questionsFor(picked),
717
+ scout: survey, listing: seeds, generate, ceilingUsd, onPhase, onCall, writeScope,
718
+ priorCalls: (authored.cost?.calls ?? []).slice(scoutCalls), priorRaws: (authored.raws ?? []).slice(scoutRaws),
719
+ confirmed, jobLines, inputs,
720
+ ...authorOpts,
721
+ offerQuestions: false,
722
+ });
723
+ }
681
724
 
682
725
  if (!authored.ok) {
683
726
  return {
@@ -724,6 +767,11 @@ export async function authorCloseForJob({
724
767
  ...(authored.declaration.notes?.length
725
768
  ? { notes: authored.declaration.notes.map((/** @type {any} */ n) => (typeof n === 'string' ? redactSecrets(n) : n)) }
726
769
  : {}),
770
+ // P7: the job lines the drafter could not turn into a stage, each with its reason — the Job tab shows them. Free
771
+ // text the model wrote, so it rides the same redactor `notes` does.
772
+ ...(authored.declaration.refused?.length
773
+ ? { refused: authored.declaration.refused.map((/** @type {any} */ r) => ({ line: r.line, reason: redactSecrets(String(r.reason)) })) }
774
+ : {}),
727
775
  };
728
776
 
729
777
  // ── THE TWO SIGNED ARTIFACTS (softgreen module 4, wired here by module 5) ──
@@ -750,7 +798,7 @@ export async function authorCloseForJob({
750
798
  book.absorb(authored.cost?.calls ?? []);
751
799
  onPhase('rubric', { size: CALIBRATION_SIZE });
752
800
  const proposed = await proposeFn({
753
- answers: interview.answers,
801
+ answers: stageAnswers,
754
802
  questions: questions ?? questionsFor(picked),
755
803
  generate,
756
804
  book,
@@ -1253,7 +1301,7 @@ export async function prepareSigning({
1253
1301
  }
1254
1302
  // the class rides from the SPEC (PRD v1.57 §2) — gate 1a already refused a spec
1255
1303
  // whose class is unknown or locked, so this is the class D5's battery hangs off
1256
- const dv = validateCloseDecl(spec.closeDecl, { at: 'closeDecl', listing: listed.files, verdictType: spec.verdictType, writeScope: spec.writeScope });
1304
+ const dv = validateCloseDecl(spec.closeDecl, { at: 'closeDecl', listing: listed.files, verdictType: spec.verdictType, writeScope: spec.writeScope, jobLines: Array.isArray(spec.jobLines) ? spec.jobLines : null });
1257
1305
  // the grounded gate's reds quote BOTH untrusted sources — the declaration, and
1258
1306
  // the seed listing itself (the listing rule names what really sits beside an
1259
1307
  // invented path). The listing half is a channel gate 1a structurally cannot
@@ -53,7 +53,7 @@ export function declarationLines({ goal, closeDecl }) {
53
53
  * F176'S FALLBACK, SAID OUT LOUD — the revise ladder kept the NEWEST SOUND
54
54
  * declaration rather than the last one it happened to try, because "kept the
55
55
  * last accepted revision" is not the same claim as "kept the best revision"
56
- * (docs/logs/FINDINGS.md F176). Absent on every run where the fallback never
56
+ * (docs/wiki/FINDINGS.md F176). Absent on every run where the fallback never
57
57
  * fired (the overwhelming majority), which is why this returns an EMPTY list
58
58
  * rather than a "no fallback" line — a green close has nothing to say here and
59
59
  * printing it every run would be noise, not information.
@@ -572,6 +572,54 @@ export function proposalStopText({ stop, reds, cases = null, spend = null, sourc
572
572
  return lines.join('\n');
573
573
  }
574
574
 
575
+ // ── ANY DRAFTING STOP, in plain words (P7 item 6, live session smv2bzdqmp0f8, 2026-10-10) ───────────────────────────
576
+ // A provider-red used to reach the panel as the bare line "Stopped: provider-red" — a code, no reason. Every stop of the
577
+ // drafting loop that is not a rubric-proposal or signing refusal (those have their own wording above and below) now says
578
+ // what happened, what it cost and what to do, in words. The stop's own reds are quoted (scrubbed, bounded), never dropped.
579
+
580
+ /** the reason inside a `scout-absent` red's detail: `the survey is ABSENT (<reason>) — an absent facts object ...` (src/authorflow.js) */
581
+ const SCOUT_REASON_RE = /^the survey is [^(]*\((.*)\) — an absent facts/s;
582
+
583
+ /**
584
+ * @param {{stop: string, reds?: PlainRed[]|null, refusal?: {detail?: string}|null,
585
+ * spend?: {knownUsd?: number|null, spendComplete?: boolean|null}|null}} o
586
+ * @returns {string}
587
+ */
588
+ export function draftStopText({ stop, reds = null, refusal = null, spend = null }) {
589
+ const spent = spendText(spend);
590
+ /** @type {Record<string, string>} */
591
+ const heads = {
592
+ 'provider-red': 'The AI provider failed before a usable plan came back — a connection or service fault, not a problem with your job.',
593
+ 'artifact-red': 'The AI answered, but its answer could not be used as a plan.',
594
+ 'cap-halt': 'Drafting stopped on your cost cap.',
595
+ 'pricing-red': 'Drafting stopped: the cost of a call could not be priced, so the cap cannot be enforced.',
596
+ 'max-revisions': 'The AI used all its tries without a plan that passed the checks.',
597
+ precheck: 'Drafting stopped before any call: something it needs was missing.',
598
+ unchanged: 'Drafting stopped: the AI sent the same plan twice.',
599
+ 'scout-absent': 'Drafting stopped: the first look at your repo did not complete.',
600
+ };
601
+ const redList = Array.isArray(reds) ? reds : [];
602
+ // F217: the first look at the repo failing (a dropped connection, a timeout) arrives as a `precheck` stop with a
603
+ // `scout-absent` red. That is NOT "something it needs was missing", and its reason sits past the 60-char quote bound
604
+ // behind a fixed lead-in, so the person saw neither. Word it as the scout's stop and quote the reason in full (scrubbed).
605
+ const scoutRed = stop === 'precheck' ? redList.find((r) => r?.code === 'scout-absent') : undefined;
606
+ const head = scoutRed ? heads['scout-absent'] : (heads[stop] ?? `Drafting stopped (${safeText(stop)}).`);
607
+ /** @type {string[]} */
608
+ const said = scoutRed
609
+ ? [SCOUT_REASON_RE.exec(String(scoutRed.detail ?? ''))?.[1]
610
+ ?.replace(/\s+/g, ' ').trim().slice(0, 240) ?? safeText(scoutRed.detail)].map((x) => redactSecrets(x)).filter(Boolean)
611
+ : [...new Set(redList.map((r) => safeText(r?.detail)).filter(Boolean))];
612
+ if (said.length === 0 && refusal?.detail) said.push(safeText(refusal.detail));
613
+ const lines = [`${head} Spent so far: ${spent ?? 'unknown'}.`];
614
+ if (said.length) {
615
+ lines.push('What it said:');
616
+ for (const x of said.slice(0, PLAIN_MAX_BULLETS)) lines.push(` • ${x}`);
617
+ if (said.length > PLAIN_MAX_BULLETS) lines.push(` • …and ${said.length - PLAIN_MAX_BULLETS} more.`);
618
+ }
619
+ lines.push(stop === 'cap-halt' || stop === 'pricing-red' ? 'What to do: raise the cap or fix the price in Settings, then draft again.' : 'What to do: draft again.');
620
+ return lines.join('\n');
621
+ }
622
+
575
623
  // ── THE SIGNING GATES' REFUSAL, in plain words (hamr, 2026-10-09) ──────────────────────────
576
624
  //
577
625
  // `prepareSigning` (src/authorjob.js) can refuse with a list of reds from four gates. The panel used to say
@@ -598,7 +646,7 @@ export function proposalStopText({ stop, reds, cases = null, spend = null, sourc
598
646
  export const DECLARATION_CODES = Object.freeze([
599
647
  'bounds', 'class-absent', 'class-battery-locked', 'class-ceiling', 'cmd-denied', 'duplicate-kind', 'duplicate-name',
600
648
  'env-ownership-absent', 'genre-env-missing', 'genre-env-ungrounded', 'genre-owned-env', 'guard-missing',
601
- 'guard-weakened', 'guards-absent', 'human-stage-not-last', 'human-stage-offered', 'invalid-value', 'judged-outside-write-scope', 'judged-stage-order',
649
+ 'guard-weakened', 'guards-absent', 'human-stage-not-last', 'human-stage-offered', 'invalid-value', 'job-line-uncovered', 'judged-outside-write-scope', 'judged-stage-order',
602
650
  'listing-absent', 'listing-conflict', 'locked-kind', 'missing-field', 'missing-required', 'one-population',
603
651
  'path-not-in-listing', 'secret-literal', 'unknown-field', 'unknown-kind',
604
652
  ]);
package/src/authorrun.js CHANGED
@@ -101,7 +101,7 @@
101
101
  // in; a draft missing one, or naming one the provider factory
102
102
  // does not know, dies here loud, listing the known table.
103
103
  import { keysForDoor } from './keysfile.js';
104
- import { applyConfiguredKey, judgeRatesFor, keyNameFor, ratesFor, rowsForHome } from './providerrows.js';
104
+ import { judgeRatesFor, judgeRowFor, keyNameFor, keyValueOf, ratesFor, ratesOfLine, resolveWorker, rowsForHome } from './providerrows.js';
105
105
  import { ConfigError } from './config.js';
106
106
  import {
107
107
  readFileSync, writeFileSync, appendFileSync, mkdirSync, existsSync, statSync,
@@ -466,13 +466,15 @@ export async function main(argv, deps = {}) {
466
466
  try { return resolveProvider(draft?.provider); } catch (e) { return die(/** @type {Error} */ (e).message); }
467
467
  })();
468
468
  const PROVIDER_NAME = /** @type {string} */ (draft.provider);
469
- // The same tier run-u drafts and works on. The declaration is authored by a
470
- // model, so the floor is the drafter floor (PRD v1.36: sonnet MINIMUM).
471
- const MODEL = /** @type {NonNullable<typeof providerEntry>} */ (providerEntry).tiers.sonnet;
472
- // spec.baseUrl (PRD item 28, ruling (d)) — the same forwarding rule run-u
473
- // applies: forwarded only when the draft names one, every provider
474
- // constructor defaults it on its own when absent.
475
- const baseUrl = typeof draft?.baseUrl === 'string' ? draft.baseUrl : undefined;
469
+ // The Settings LINE this authoring run works on (P8): the draft's own `model` on a ticked, priced line of a key row on the
470
+ // draft's API shape (and Base URL, when it names one). The row is the key, the Base URL and the old-max_tokens tick. Nothing is
471
+ // built in: a draft naming no model, or a model no row ticks, stops here at $0.
472
+ const keyCfg = rowsForHome(keys.home, env);
473
+ const W = resolveWorker(keyCfg, draft);
474
+ if (!W.ok) { err(W.say.includes('Nothing was spent') ? W.say : `${W.say} Nothing was spent.`); throw new ExitSignal(2); }
475
+ const MODEL = W.model;
476
+ const baseUrl = W.baseUrl;
477
+ const AUTHOR_ROW = W.row;
476
478
 
477
479
  // THE JUDGE THIS RUN'S OWN WORKER RESOLVES TO (PRD item 32.1) — resolved ONCE,
478
480
  // here, from the DRAFT, and reused for BOTH the compose-time stamp (the
@@ -508,12 +510,9 @@ export async function main(argv, deps = {}) {
508
510
 
509
511
  // Secrets load from the environment; they never enter argv (a command line is
510
512
  // world-readable on /proc) and they are never printed.
511
- // P4a item 4 — the key variable the person picked in Settings (config.json) stands in for
512
- // the provider's built-in one; none picked = the built-in. Names only.
513
- const keyCfg = rowsForHome(keys.home);
514
- const authorEnv = applyConfiguredKey(env, PROVIDER_NAME, baseUrl, keyCfg);
515
- const AUTHOR_ENV_KEY = keyNameFor(PROVIDER_NAME, baseUrl, keyCfg).name;
516
- const apiKey = authorEnv[/** @type {NonNullable<typeof providerEntry>} */ (providerEntry).envKey];
513
+ // The key is the chosen row's own variable (P8: no built-in name). Names only.
514
+ const AUTHOR_ENV_KEY = AUTHOR_ROW.envName;
515
+ const apiKey = keyValueOf(env, AUTHOR_ENV_KEY);
517
516
  if (!apiKey) { err(`${AUTHOR_ENV_KEY} not set (secrets load from the environment — never the tree, never argv)`); throw new ExitSignal(2); }
518
517
  // F181 — a key that carries a line break/control char/stray whitespace (a
519
518
  // two-line secret-store entry, e.g.) reads as "set" by the presence check
@@ -527,17 +526,17 @@ export async function main(argv, deps = {}) {
527
526
  // loop, confirm turn, calibration judge). A bad price refuses here; it never falls back to the guess.
528
527
  /** @type {ReturnType<typeof ratesFor>} */
529
528
  let authorPrice = null;
530
- try { authorPrice = ratesFor(PROVIDER_NAME, baseUrl, keyCfg); } catch (e) {
531
- if (!(e instanceof ConfigError)) throw e;
532
- err(`${e.message} — refusing to start rather than guess a price. Nothing spent.`);
533
- throw new ExitSignal(2);
529
+ {
530
+ const al = ratesOfLine(W.line);
531
+ authorPrice = al ? { ...al, envName: AUTHOR_ROW.envName } : null;
534
532
  }
535
533
  /** The judge's key follows the RESOLVED judge provider's own env var, with
536
534
  * `JUDGE_API_KEY` as the role-named override in front (PRD item 32.3) — the same
537
535
  * contract `scripts/run-u.mjs` keeps, so one story covers both surfaces. When the
538
536
  * judge IS the authoring provider this is the key already read above. */
537
+ const judgeRowOf = (/** @type {{provider: string, model?: string|null}} */ j) => judgeRowFor(keyCfg, j, PROVIDER_NAME, AUTHOR_ROW);
539
538
  const judgeKeyFor = (/** @type {string} */ providerName) => (
540
- env.JUDGE_API_KEY ?? (providerName === PROVIDER_NAME ? authorEnv : applyConfiguredKey(env, providerName, undefined, keyCfg))[resolveProvider(providerName).envKey]
539
+ env.JUDGE_API_KEY ?? (providerName === PROVIDER_NAME ? apiKey : keyValueOf(env, judgeRowFor(keyCfg, { provider: providerName }, PROVIDER_NAME, AUTHOR_ROW)?.envName))
541
540
  );
542
541
 
543
542
  /** F6 — an unpriced call makes the TOTAL unknown, and unknown is reported as
@@ -570,7 +569,7 @@ export async function main(argv, deps = {}) {
570
569
  out(` ${ceilingLine(CEILING_USD)}`);
571
570
  out(' stops at prepareSigning — this script NEVER signs and NEVER runs the job\n');
572
571
 
573
- // F190 (docs/logs/FINDINGS.md) — this used to be a hand-rolled `makeProvider`
572
+ // F190 (docs/wiki/FINDINGS.md) — this used to be a hand-rolled `makeProvider`
574
573
  // call, a duplicate of the SAME construction `buildRunnerProviders` already
575
574
  // owns for `src/cli.js`. Routed through the one owner now: the judge half of
576
575
  // this call is a THROWAWAY (`judgeProviderName`/`judgeModel`/`judgeBaseUrl` all
@@ -579,7 +578,7 @@ export async function main(argv, deps = {}) {
579
578
  // one) — the real judge identity is not resolved until after `validateJob`,
580
579
  // further down, where a second call takes only its `.judgeProvider`.
581
580
  const { provider } = buildRunnerProviders({
582
- providerName: PROVIDER_NAME, apiKey, model: MODEL, tierModels: providerEntry.tiers, baseUrl,
581
+ providerName: PROVIDER_NAME, apiKey, model: MODEL, baseUrl, legacyMaxTokens: AUTHOR_ROW.legacyMaxTokens,
583
582
  judgeApiKey: apiKey, judgeModel: MODEL, judgeProviderName: PROVIDER_NAME, judgeBaseUrl: baseUrl,
584
583
  });
585
584
  emit('author-start', { runid, source: SOURCE, lang: LANG, verdictType: VERDICT, provider: PROVIDER_NAME, model: MODEL, baseUrl: baseUrl ?? null, job: draft?.job ?? null, timeoutMs: TIMEOUT_MS, ceilingUsd: CEILING_USD });
@@ -818,7 +817,7 @@ export async function main(argv, deps = {}) {
818
817
  // piece 4, step S6, D5 amended 2026-09-21) ──────────────────────────────────
819
818
  // This USED TO run the whole confirm turn first (over a $0 manual listing,
820
819
  // via `runConfirmTurn`), THEN give this stop once the person confirmed a plan
821
- // — D5's original shape. Live run `mu4hc7sp` (docs/logs/FINDINGS.md F191)
820
+ // — D5's original shape. Live run `mu4hc7sp` (docs/wiki/FINDINGS.md F191)
822
821
  // crashed inside that confirm turn instead: `confirmProtections`
823
822
  // (`src/authorflow.js`) calls `classGuards` (`src/authoring.js`), which is
824
823
  // keyed by CODE LANGUAGE and THROWS on a plain folder's `lang:
@@ -879,6 +878,9 @@ export async function main(argv, deps = {}) {
879
878
  // fence is not this script's business to invent one for, so an absent or
880
879
  // malformed field travels as `null` and `authorPrompt` simply states nothing.
881
880
  writeScope: Array.isArray(draft.writeScope) ? draft.writeScope : null,
881
+ // P7: the person's numbered job lines and inputs ride the draft; the drafter tags every stage with a line
882
+ jobLines: Array.isArray(draft.jobLines) ? draft.jobLines : null,
883
+ inputs: Array.isArray(draft.inputs) ? draft.inputs : null,
882
884
  provider,
883
885
  generate: makeLoopGenerate(provider, { rates: authorPrice?.rates ?? null }),
884
886
  rates: authorPrice?.rates ?? null,
@@ -1112,7 +1114,7 @@ export async function main(argv, deps = {}) {
1112
1114
  const judgeKeyValue = judgeKeyFor(judge.provider);
1113
1115
  const judgeKeyProblem = judgeKeyValue ? apiKeyProblem(judgeKeyValue) : null;
1114
1116
  if (judgeKeyProblem) {
1115
- const judgeEnvName = env.JUDGE_API_KEY ? 'JUDGE_API_KEY' : resolveProvider(judge.provider).envKey;
1117
+ const judgeEnvName = env.JUDGE_API_KEY ? 'JUDGE_API_KEY' : (judgeRowOf(judge)?.envName ?? judge.provider);
1116
1118
  err(`${judgeEnvName} ${judgeKeyProblem} — refusing rather than crashing mid-call (never trimmed or repaired; fix the value at its source)`);
1117
1119
  // F186 — the scout above already ran and may have left its own gate
1118
1120
  // audit in the tree. Under `process.exit()` this path used to skip
@@ -1146,9 +1148,10 @@ export async function main(argv, deps = {}) {
1146
1148
  let judgeProvider = null;
1147
1149
  if (judge) {
1148
1150
  judgeProvider = buildRunnerProviders({
1149
- providerName: PROVIDER_NAME, apiKey, model: MODEL, tierModels: providerEntry.tiers, baseUrl,
1151
+ providerName: PROVIDER_NAME, apiKey, model: MODEL, baseUrl, legacyMaxTokens: AUTHOR_ROW.legacyMaxTokens,
1150
1152
  judgeApiKey: judgeKeyFor(judge.provider), judgeModel: judge.model, judgeProviderName: judge.provider,
1151
- judgeBaseUrl: judge.provider === PROVIDER_NAME ? baseUrl : undefined,
1153
+ judgeBaseUrl: judge.provider === PROVIDER_NAME ? baseUrl : (judgeRowOf(judge)?.baseUrl.trim() || undefined),
1154
+ judgeLegacyMaxTokens: judge.provider === PROVIDER_NAME ? AUTHOR_ROW.legacyMaxTokens : (judgeRowOf(judge)?.legacyMaxTokens ?? false),
1152
1155
  }).judgeProvider;
1153
1156
  }
1154
1157
  if (judge) {
@@ -1167,7 +1170,7 @@ export async function main(argv, deps = {}) {
1167
1170
  // number would silently widen it.
1168
1171
  ceilingUsd: CEILING_USD,
1169
1172
  priorCalls: [...metered],
1170
- judgeLoop: judgeProvider ? (o) => defaultJudgeLoop({ provider: judgeProvider, system: o.system, rates: judgePrice?.rates ?? null }) : null,
1173
+ judgeLoop: judgeProvider ? (o) => defaultJudgeLoop({ provider: judgeProvider, system: o.system, rates: judgePrice?.rates ?? null, thinking: spec.thinking === true }) : null,
1171
1174
  // the seam's other half (PRD item 32.1): the graded set is STAMPED with
1172
1175
  // this, and the close later refuses to grade under any other identity.
1173
1176
  judgeModel: judge?.model ?? null,
@@ -1294,7 +1297,7 @@ export async function main(argv, deps = {}) {
1294
1297
  + 'and run-u will enforce the remainder (Cap $ minus this) as ITS ceiling — pass --draft-spent-usd exactly as shown below, never a rounded or re-typed figure.');
1295
1298
  }
1296
1299
  out('\nTo run it (the same signature and gates as any other job — nothing here bypasses them):');
1297
- out(` ${keyNameFor(PROVIDER_NAME, baseUrl, keyCfg, spec.model).name}=... ${commandFor('run-u', deps.invokedAs)} --spec ${specFile} --approve ${hash}${draftSpentUsd > 0 ? ` --draft-spent-usd ${draftSpentUsd}${draftIncomplete ? ' --draft-spend-incomplete' : ''}` : ''}`);
1300
+ out(` ${AUTHOR_ENV_KEY}=... ${commandFor('run-u', deps.invokedAs)} --spec ${specFile} --approve ${hash}${draftSpentUsd > 0 ? ` --draft-spent-usd ${draftSpentUsd}${draftIncomplete ? ' --draft-spend-incomplete' : ''}` : ''}`);
1298
1301
  emit('author-end', { outcome: 'prepared', specHash: hash });
1299
1302
  }
1300
1303
  }
@@ -375,9 +375,9 @@ const defaultLoop = (/** @type {any} */ { system, policy, onLlmResult, provider,
375
375
  * @param {{workdir: string, provider?: any,
376
376
  * rounds?: number, minBytes?: number, blobMax?: number, maxTokens?: number, ctx?: boolean,
377
377
  * attempts?: number, ceilingUsd?: number|null, callTimeoutMs?: number,
378
- * onCall?: (call: {label: string, costUsd: number|null, unpricedRounds: number}) => void,
378
+ * onCall?: (call: {label: string, costUsd: number|null, unpricedRounds: number, thinkingStatus?: string}) => void,
379
379
  * createLoop?: (o: {system: string, policy: any, onLlmResult: any, provider: any, rates?: {in: number, out: number}}) => any,
380
- * rates?: {in: number, out: number}|null,
380
+ * rates?: {in: number, out: number}|null, callOptions?: Record<string, any>,
381
381
  * createSurveyor?: (o: {workdir: string, granted: readonly string[], ctx: boolean}) => Promise<any>}} o
382
382
  * @returns {Promise<Survey & {raw: string, raws: ReturnType<typeof scrubRaw>[],
383
383
  * calls: {label: string, costUsd: number|null, unpricedRounds: number}[],
@@ -400,6 +400,8 @@ export async function runAuthorScout({
400
400
  onCall = () => {},
401
401
  createLoop = defaultLoop,
402
402
  rates = null,
403
+ // the drafter's per-call model options (`thinkingCallOptions`, src/thinking.js; e.g. thinking off) — {} = nothing extra, byte-identical
404
+ callOptions = {},
403
405
  createSurveyor = defaultSurveyor,
404
406
  }) {
405
407
  const callTimeout = Number.isFinite(Number(callTimeoutMs)) && Number(callTimeoutMs) > 0
@@ -434,7 +436,8 @@ export async function runAuthorScout({
434
436
  // metered without the report going out: exactly the pairing invariant the
435
437
  // raw already rides. It REPORTS and decides nothing; the ceiling still binds
436
438
  // through `capStop` between calls, untouched.
437
- onCall({ ...call });
439
+ // bare-agent's per-run `thinkingStatus` rides the reporting seam only, never the stored entry (see makeCostBook.add)
440
+ onCall({ ...call, ...(r?.thinkingStatus === undefined ? {} : { thinkingStatus: r.thinkingStatus }) });
438
441
  return raws.push(scrubRaw({ label, attempt, text: r?.text })) - 1;
439
442
  };
440
443
  // TIGHTEN-ONLY: an operator may lower the ceiling, never raise it. A garbage
@@ -526,7 +529,7 @@ export async function runAuthorScout({
526
529
  loop = createLoop({ system, policy: surveyor.policy, onLlmResult: metered, provider, ...priced });
527
530
 
528
531
  const r = await settled(loop.run([{ role: 'user', content: scoutPrompt(workdir) }], surveyor.tools,
529
- { cacheMessages: true, maxTokens, timeoutMs: callTimeout }));
532
+ { cacheMessages: true, maxTokens, timeoutMs: callTimeout, ...callOptions }));
530
533
  verdictAt = record(label, attempt, r);
531
534
  blob = redactSecrets(String(r?.text ?? '')).slice(0, blobMax);
532
535
  lastMsgs = Array.isArray(r?.msgs) && r.msgs.length ? r.msgs : null;
@@ -572,7 +575,7 @@ export async function runAuthorScout({
572
575
  // backends 400 on it. Strip the leading one before re-running.
573
576
  const rMsgs = r.msgs[0]?.role === 'system' ? r.msgs.slice(1) : r.msgs;
574
577
  s2 = await settled(recovery.run([...rMsgs, { role: 'user', content: SCOUT_RECOVERY_PROMPT }], [],
575
- { cacheMessages: true, maxTokens, timeoutMs: callTimeout }));
578
+ { cacheMessages: true, maxTokens, timeoutMs: callTimeout, ...callOptions }));
576
579
  const recoveryAt = record('author-scout-recovery', attempt, s2);
577
580
  const t = redactSecrets(String(s2?.text ?? '')).slice(0, blobMax);
578
581
  // the verdict follows the blob that WON, never the call that came last
@@ -603,7 +606,7 @@ export async function runAuthorScout({
603
606
  const askMsgs = lastMsgs[0]?.role === 'system' ? lastMsgs.slice(1) : lastMsgs;
604
607
  const rr = await settled(reask.run(
605
608
  [...askMsgs, { role: 'user', content: scoutReaskTurn(/** @type {Survey} */ (survey).reason ?? '') }], [],
606
- { cacheMessages: true, maxTokens, timeoutMs: callTimeout },
609
+ { cacheMessages: true, maxTokens, timeoutMs: callTimeout, ...callOptions },
607
610
  ));
608
611
  verdictAt = record(label, attempt, rr);
609
612
  blob = redactSecrets(String(rr?.text ?? '')).slice(0, blobMax);
package/src/bridges.js CHANGED
@@ -1,5 +1,5 @@
1
1
  // Layer 3 — the REUSE registry (design record:
2
- // docs/product/2026-08-01-layer-3-reuse-design.md, D1/D2/D6, frozen 2026-08-01).
2
+ // docs/logs/2026-08-01-layer-3-reuse-design.md, D1/D2/D6, frozen 2026-08-01).
3
3
  //
4
4
  // A BRIDGE is the plan a green actually executed, kept so the next run of the same
5
5
  // SHAPE starts from it instead of cold. This module is the box and the label on it:
package/src/bundle.js CHANGED
@@ -459,13 +459,16 @@ export function exportBundle({ spec, closeScripts, registryDir, outDir, bareloop
459
459
  '',
460
460
  '## Running this bundle',
461
461
  '',
462
- ' ./node_modules/.bin/bareloop run . --repo <path> [--budget N] [--wall MIN] [--approve <bundleHash>]',
462
+ ' ./node_modules/.bin/bareloop run . --repo <path> [--model <id>] [--budget N] [--wall MIN] [--approve <bundleHash>]',
463
463
  '',
464
464
  'Answer these, in the order `bareloop run` asks them:',
465
465
  '',
466
- '1. Provider key — set ANTHROPIC_API_KEY in your environment (never on the',
467
- ' command line, never in a file). Absent, `bareloop run` prints these',
468
- ' questions and the bundleHash again, and spends nothing.',
466
+ "1. Provider key — put your provider's API key in your keys file (the NAME your",
467
+ ' Settings row uses), never on the command line. A bundle signed without a',
468
+ ' model needs `--model <id>` of a line ticked in Settings > Providers (with',
469
+ ' both prices), plus `--provider <KEY>` when two keys tick it. With neither,',
470
+ ' `bareloop run` prints these questions and the bundleHash again, and spends',
471
+ ' nothing.',
469
472
  `2. Approve — the FIRST run on this machine requires --approve ${hash}`,
470
473
  " (this bundle's own signed hash, shown above) to prove a human read and",
471
474
  ' accepted it before anything runs. Once that first run GREENS, the',
package/src/bundlerun.js CHANGED
@@ -204,13 +204,16 @@ export async function bundleMain(args, {
204
204
  workdir: worktree,
205
205
  seed,
206
206
  approve: approveHash,
207
+ // an exported bundle follows the run-u rule: a spec that names no model takes `--model <id>` (+ `--provider <KEY>` when two keys tick it)
208
+ ...(typeof flags.model === 'string' ? { model: flags.model } : {}),
209
+ ...(typeof flags.provider === 'string' ? { providerKey: flags.provider } : {}),
207
210
  deps: { ...deps, env, out, err, ...(keysHome === undefined ? {} : { keysHome }) },
208
211
  bundle: {
209
212
  runid,
210
213
  runDir,
211
214
  // the bare re-invocation prefix; the leg's OWN tightened ceilings (only those the person passed) ride as
212
215
  // DATA so the engine, which knows the halt kind and the spend, spells the printed line. Text only.
213
- invoke: `bareloop run ${bundleDirArg} --repo ${repo}`,
216
+ invoke: `bareloop run ${bundleDirArg} --repo ${repo}${typeof flags.model === 'string' ? ` --model ${flags.model}` : ''}${typeof flags.provider === 'string' ? ` --provider ${flags.provider}` : ''}`,
214
217
  tightened: {
215
218
  ...(envelope.budgetUsd === undefined ? {} : { budgetUsd: envelope.budgetUsd }),
216
219
  ...(flags.wall === undefined ? {} : { wallMin: Number(flags.wall) }),
package/src/calibrate.js CHANGED
@@ -1,5 +1,5 @@
1
1
  // THE CALIBRATION GATE — softgreen module 5 (design record
2
- // docs/product/2026-08-17-softgreen-review-door-design.md §4.4, the
2
+ // docs/logs/2026-08-17-softgreen-review-door-design.md §4.4, the
3
3
  // 2026-08-18 threshold addendum rulings 1–3, and the second same-day addendum's
4
4
  // rulings 1 and 3).
5
5
  //
package/src/cardauthor.js CHANGED
@@ -1,5 +1,5 @@
1
1
  // THE TWO SIGNED ARTIFACTS — softgreen module 4 (design record
2
- // docs/product/2026-08-17-softgreen-review-door-design.md §4.3 and §4.4,
2
+ // docs/logs/2026-08-17-softgreen-review-door-design.md §4.3 and §4.4,
3
3
  // plus the 2026-08-18 threshold addendum).
4
4
  //
5
5
  // Module 1 built the judged floor's core, module 2 wired the stage into a close,
@@ -240,6 +240,9 @@ THEIR ANSWER — verbatim, and the only input you have
240
240
  Q${qKey}. ${q}
241
241
  A${qKey}. ${answers?.[qKey] ?? '(no answer given)'}
242
242
 
243
+ Each example is kept with the number of the job line the person wrote it under. Never attach an example to a different
244
+ line than the one it names (run mv1j01sl guessed which example belonged to which part of the goal).
245
+
243
246
  ---
244
247
 
245
248
  THE RULEBOOK — the only rules that exist. You SELECT from it; a rule that is not here cannot be asked for.
package/src/cli.js CHANGED
@@ -7,7 +7,7 @@
7
7
  // run-u` drives (`src/userrun.js`): providers, keys (the keys file / env, any provider the
8
8
  // bundle's spec names), judge, monthly limit and readout are that engine's. The test seam is
9
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).
10
+ // provider and skips the real-key check (same as `tests/planrun-*.test.js`'s pattern).
11
11
  //
12
12
  // Money/arbiter rules the bundle door keeps: `--budget`/`--wall` only TIGHTEN a bundle's
13
13
  // signed ceilings (`checkEnvelope`, M1); a bundle's `bundleHash` (the human-facing