bareloop 0.39.0 → 0.40.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 (51) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/bareloop.context.md +63 -11
  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 +5 -2
  9. package/src/authorscout.js +9 -6
  10. package/src/bridges.js +1 -1
  11. package/src/calibrate.js +1 -1
  12. package/src/cardauthor.js +4 -1
  13. package/src/declaredclose.js +4 -3
  14. package/src/draftspend.js +6 -1
  15. package/src/interviewrun.js +56 -27
  16. package/src/job.js +38 -5
  17. package/src/jobblock.js +140 -0
  18. package/src/judged.js +1 -1
  19. package/src/ledger.js +1 -1
  20. package/src/panel/authorroutes.js +15 -5
  21. package/src/panel/authorsession.js +158 -47
  22. package/src/panel/index.html +179 -70
  23. package/src/panel/jobplan.js +107 -0
  24. package/src/panel/server.js +67 -13
  25. package/src/planrun.js +4 -4
  26. package/src/promptregisters.js +1 -1
  27. package/src/providers.js +101 -19
  28. package/src/reuse.js +1 -1
  29. package/src/root.js +1 -1
  30. package/src/run.js +3 -3
  31. package/src/selection.js +1 -1
  32. package/src/source.js +64 -1
  33. package/src/text.js +13 -0
  34. package/src/transport.js +1 -1
  35. package/src/userrun.js +3 -3
  36. package/types/authorflow.d.ts +258 -39
  37. package/types/authoring.d.ts +7 -1
  38. package/types/authorjob.d.ts +35 -3
  39. package/types/authorreadout.d.ts +17 -1
  40. package/types/authorscout.d.ts +5 -3
  41. package/types/declaredclose.d.ts +4 -1
  42. package/types/draftspend.d.ts +8 -2
  43. package/types/job.d.ts +1 -1
  44. package/types/jobblock.d.ts +102 -0
  45. package/types/panel/authorsession.d.ts +50 -14
  46. package/types/panel/jobplan.d.ts +58 -0
  47. package/types/panel/server.d.ts +20 -0
  48. package/types/planrun.d.ts +1 -1
  49. package/types/providers.d.ts +47 -3
  50. package/types/run.d.ts +1 -1
  51. package/types/source.d.ts +26 -0
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 { draftCallOptions, assertDraftThinking } from './providers.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, thinkingApplied?: boolean|null}) => 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
+ assertDraftThinking(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 = draftCallOptions(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
@@ -570,7 +570,7 @@ export async function main(argv, deps = {}) {
570
570
  out(` ${ceilingLine(CEILING_USD)}`);
571
571
  out(' stops at prepareSigning — this script NEVER signs and NEVER runs the job\n');
572
572
 
573
- // F190 (docs/logs/FINDINGS.md) — this used to be a hand-rolled `makeProvider`
573
+ // F190 (docs/wiki/FINDINGS.md) — this used to be a hand-rolled `makeProvider`
574
574
  // call, a duplicate of the SAME construction `buildRunnerProviders` already
575
575
  // owns for `src/cli.js`. Routed through the one owner now: the judge half of
576
576
  // this call is a THROWAWAY (`judgeProviderName`/`judgeModel`/`judgeBaseUrl` all
@@ -818,7 +818,7 @@ export async function main(argv, deps = {}) {
818
818
  // piece 4, step S6, D5 amended 2026-09-21) ──────────────────────────────────
819
819
  // This USED TO run the whole confirm turn first (over a $0 manual listing,
820
820
  // 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)
821
+ // — D5's original shape. Live run `mu4hc7sp` (docs/wiki/FINDINGS.md F191)
822
822
  // crashed inside that confirm turn instead: `confirmProtections`
823
823
  // (`src/authorflow.js`) calls `classGuards` (`src/authoring.js`), which is
824
824
  // keyed by CODE LANGUAGE and THROWS on a plain folder's `lang:
@@ -879,6 +879,9 @@ export async function main(argv, deps = {}) {
879
879
  // fence is not this script's business to invent one for, so an absent or
880
880
  // malformed field travels as `null` and `authorPrompt` simply states nothing.
881
881
  writeScope: Array.isArray(draft.writeScope) ? draft.writeScope : null,
882
+ // P7: the person's numbered job lines and inputs ride the draft; the drafter tags every stage with a line
883
+ jobLines: Array.isArray(draft.jobLines) ? draft.jobLines : null,
884
+ inputs: Array.isArray(draft.inputs) ? draft.inputs : null,
882
885
  provider,
883
886
  generate: makeLoopGenerate(provider, { rates: authorPrice?.rates ?? null }),
884
887
  rates: authorPrice?.rates ?? null,
@@ -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, thinkingApplied?: boolean|null}) => 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 (`draftCallOptions`, src/providers.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 `thinkingApplied` rides the reporting seam only, never the stored entry (see makeCostBook.add)
440
+ onCall({ ...call, ...(r?.thinkingApplied === undefined ? {} : { thinkingApplied: r.thinkingApplied }) });
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/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.
@@ -102,7 +102,7 @@ export const DECLARED_GENRES = Object.freeze([TYPES_GENRE.name]);
102
102
  * constrained to the names the genre OWNS, so it can never become an environment
103
103
  * channel of its own.
104
104
  */
105
- export const CLOSE_DECL_FIELDS = Object.freeze(['genre', 'lang', 'stages', 'notes', 'genreEnv', 'calibration']);
105
+ export const CLOSE_DECL_FIELDS = Object.freeze(['genre', 'lang', 'stages', 'notes', 'refused', 'genreEnv', 'calibration']);
106
106
 
107
107
  /** what a stored calibration set carries, ENUMERATED: the frozen cases and the judge
108
108
  * tier that graded them. Both are inside the spec, so `jobSpecHash` covers both — which
@@ -272,13 +272,13 @@ export function declaredStages(closeDecl) {
272
272
  *
273
273
  * @param {any} closeDecl
274
274
  * @param {{at?: string, listing?: string[]|null, deferListing?: boolean,
275
- * catalogue?: Record<string, any>, verdictType?: string|null, writeScope?: string[]|null}} [opts]
275
+ * catalogue?: Record<string, any>, verdictType?: string|null, writeScope?: string[]|null, jobLines?: {n: number}[]|null}} [opts]
276
276
  * @returns {{ok: boolean, reds: Red[], closeDecl: any, grounded: boolean,
277
277
  * scoped: {scoped: boolean, via: string|null},
278
278
  * ceiling: {class: string, kind: string, stage: string|null, locked: boolean}|null}}
279
279
  */
280
280
  export function validateCloseDecl(closeDecl, opts = {}) {
281
- const { at = 'closeDecl', listing = null, catalogue, verdictType = null, writeScope = null } = opts;
281
+ const { at = 'closeDecl', listing = null, catalogue, verdictType = null, writeScope = null, jobLines = null } = opts;
282
282
  const deferListing = opts.deferListing === true;
283
283
  /** @type {Red[]} */
284
284
  const reds = [];
@@ -402,6 +402,7 @@ export function validateCloseDecl(closeDecl, opts = {}) {
402
402
  guards: classGuards({ verdictType: picked, lang: closeDecl.lang }),
403
403
  verdictType: picked,
404
404
  writeScope,
405
+ jobLines,
405
406
  envOwned: owned,
406
407
  // POST-injection: an owned name is accepted only when its value is EXACTLY
407
408
  // the recorded one. `{}` (nothing recorded) keeps the pre-injection rule, so
package/src/draftspend.js CHANGED
@@ -24,7 +24,10 @@ export const DRAFT_LOG_FILE = 'draft-log.jsonl';
24
24
  * One line of the drafting log. `no` = the step's position in the session's progress list (a step id can repeat).
25
25
  * - `step-start` {no, id, label, at}
26
26
  * - `step-end` {no, status: 'done'|'failed', label, at} (a re-opened step writes `step-reopen` {no, at} first)
27
- * - `call` {no, step, label, model, costUsd (null stays null), unpricedRounds, at}: one METERED model call
27
+ * - `step-label` {no, label, at} (the step reads `label` from now on, status and times untouched: the thinking row's "the model thought anyway")
28
+ * - `call` {no, step, label, model, thinking, thinkingApplied?, costUsd (null stays null), unpricedRounds, at}: one METERED model call
29
+ * (`thinking` = what was asked and sent: off | on | not-switchable | unverified; `thinkingApplied` = bare-agent's own verdict for the
30
+ * call's run (true | false | null), present only when thinkingMode was sent)
28
31
  * (no token counts: the call path does not carry them without reworking the library, so none are recorded)
29
32
  * @typedef {{kind: string, no: number, at: string, [k: string]: any}} DraftEvent
30
33
  */
@@ -148,6 +151,8 @@ export function readDraftSteps(dir) {
148
151
  x.status = ev.status === 'failed' ? 'failed' : 'done';
149
152
  x.endedAt = ev.at;
150
153
  if (typeof ev.label === 'string') x.label = ev.label;
154
+ } else if (ev.kind === 'step-label' && byNo.has(ev.no)) {
155
+ if (typeof ev.label === 'string') byNo.get(ev.no).label = ev.label;
151
156
  } else if (ev.kind === 'step-reopen' && byNo.has(ev.no)) {
152
157
  const x = byNo.get(ev.no);
153
158
  x.status = 'running';