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.
- package/CHANGELOG.md +27 -0
- package/bareloop.context.md +63 -11
- package/package.json +2 -2
- package/src/authorflow.js +333 -58
- package/src/authoring.js +65 -2
- package/src/authorjob.js +57 -9
- package/src/authorreadout.js +50 -2
- package/src/authorrun.js +5 -2
- package/src/authorscout.js +9 -6
- package/src/bridges.js +1 -1
- package/src/calibrate.js +1 -1
- package/src/cardauthor.js +4 -1
- package/src/declaredclose.js +4 -3
- package/src/draftspend.js +6 -1
- package/src/interviewrun.js +56 -27
- package/src/job.js +38 -5
- package/src/jobblock.js +140 -0
- package/src/judged.js +1 -1
- package/src/ledger.js +1 -1
- package/src/panel/authorroutes.js +15 -5
- package/src/panel/authorsession.js +158 -47
- package/src/panel/index.html +179 -70
- package/src/panel/jobplan.js +107 -0
- package/src/panel/server.js +67 -13
- package/src/planrun.js +4 -4
- package/src/promptregisters.js +1 -1
- package/src/providers.js +101 -19
- package/src/reuse.js +1 -1
- package/src/root.js +1 -1
- package/src/run.js +3 -3
- package/src/selection.js +1 -1
- package/src/source.js +64 -1
- package/src/text.js +13 -0
- package/src/transport.js +1 -1
- package/src/userrun.js +3 -3
- package/types/authorflow.d.ts +258 -39
- package/types/authoring.d.ts +7 -1
- package/types/authorjob.d.ts +35 -3
- package/types/authorreadout.d.ts +17 -1
- package/types/authorscout.d.ts +5 -3
- package/types/declaredclose.d.ts +4 -1
- package/types/draftspend.d.ts +8 -2
- package/types/job.d.ts +1 -1
- package/types/jobblock.d.ts +102 -0
- package/types/panel/authorsession.d.ts +50 -14
- package/types/panel/jobplan.d.ts +58 -0
- package/types/panel/server.d.ts +20 -0
- package/types/planrun.d.ts +1 -1
- package/types/providers.d.ts +47 -3
- package/types/run.d.ts +1 -1
- 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
|
-
|
|
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
|
-
|
|
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:
|
|
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:
|
|
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
|
package/src/authorreadout.js
CHANGED
|
@@ -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/
|
|
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/
|
|
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/
|
|
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,
|
package/src/authorscout.js
CHANGED
|
@@ -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
|
-
|
|
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/
|
|
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/
|
|
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/
|
|
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/declaredclose.js
CHANGED
|
@@ -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
|
-
* - `
|
|
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';
|