bareloop 0.38.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 +59 -0
- package/bareloop.context.md +87 -24
- package/package.json +3 -3
- package/src/authorflow.js +335 -59
- package/src/authoring.js +125 -3
- package/src/authorjob.js +93 -32
- package/src/authorreadout.js +498 -3
- package/src/authorrun.js +26 -6
- package/src/authorscout.js +9 -6
- package/src/bridges.js +1 -1
- package/src/calibrate.js +85 -11
- package/src/cardauthor.js +8 -2
- package/src/declaredclose.js +12 -5
- 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 +323 -29
- package/src/kinds.js +13 -1
- package/src/ledger.js +1 -1
- package/src/panel/authorroutes.js +17 -11
- package/src/panel/authorsession.js +202 -75
- package/src/panel/index.html +302 -312
- package/src/panel/jobplan.js +107 -0
- package/src/panel/server.js +67 -13
- package/src/planrun.js +6 -5
- package/src/promptregisters.js +1 -1
- package/src/providers.js +111 -2
- 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/trend.js +1 -0
- package/src/userrun.js +3 -3
- package/types/authorflow.d.ts +258 -39
- package/types/authoring.d.ts +30 -1
- package/types/authorjob.d.ts +52 -13
- package/types/authorreadout.d.ts +123 -1
- package/types/authorscout.d.ts +5 -3
- package/types/calibrate.d.ts +4 -1
- package/types/declaredclose.d.ts +5 -1
- package/types/draftspend.d.ts +8 -2
- package/types/job.d.ts +1 -1
- package/types/jobblock.d.ts +102 -0
- package/types/judged.d.ts +21 -2
- package/types/panel/authorsession.d.ts +57 -7
- 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 +52 -1
- package/types/run.d.ts +1 -1
- package/types/source.d.ts +26 -0
- package/types/trend.d.ts +1 -0
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
|
//
|
|
@@ -61,10 +61,10 @@
|
|
|
61
61
|
import { createHash } from 'node:crypto';
|
|
62
62
|
import {
|
|
63
63
|
LOCATE_LABEL, LOCATE_AXES, CALIBRATION_SIZE,
|
|
64
|
-
runLocate, decide, expectedOf, validateCard, validateCalibrationSet,
|
|
64
|
+
runLocate, decide, expectedOf, validateCard, validateCalibrationSet, docQuoteInBlock,
|
|
65
65
|
} from './judged.js';
|
|
66
66
|
import { JUDGE_ATTEMPTS } from './kinds.js';
|
|
67
|
-
import { tallyCalls } from './text.js';
|
|
67
|
+
import { tallyCalls, scrubRaw } from './text.js';
|
|
68
68
|
import { PROVIDER_TIMEOUT_MS } from './clock.js';
|
|
69
69
|
|
|
70
70
|
/** @typedef {{code: string, path: string, detail: string, [k: string]: any}} Red */
|
|
@@ -280,20 +280,87 @@ export function factsResist(facts, gold, artifactText) {
|
|
|
280
280
|
if (gotNames.join(',') !== wantNames.join(',')) {
|
|
281
281
|
return { ok: false, why: `the located function list moved: expected ${wantNames.join(', ')}, got ${gotNames.join(', ') || '(none)'}` };
|
|
282
282
|
}
|
|
283
|
-
const lines = new Set(artifactText.split('\n').map((l) => l.trim()).filter(Boolean));
|
|
284
283
|
for (const w of want) {
|
|
285
284
|
const f = got.find((/** @type {any} */ x) => String(x?.name ?? '') === w.name);
|
|
286
285
|
const q = typeof f?.docQuote === 'string' && f.docQuote.trim() !== '' ? f.docQuote : null;
|
|
287
286
|
if (!w.hasDoc && q !== null) {
|
|
288
287
|
return { ok: false, why: `${w.name} has no JSDoc block and the extraction reported one` };
|
|
289
288
|
}
|
|
290
|
-
|
|
289
|
+
// ONE OWNER of "this quote points at the real doc block" (docQuoteInBlock, the same call
|
|
290
|
+
// `has-doc`'s decide() makes). The judge routinely quotes a worded line inside the block
|
|
291
|
+
// rather than the `/**` opener, and since ruling A (0f27db5) that is a good pointer; the
|
|
292
|
+
// declaration it names must be this function's own, so a block above another one still leaks.
|
|
293
|
+
const decl = typeof f?.declarationQuote === 'string' ? f.declarationQuote : null;
|
|
294
|
+
if (w.hasDoc && (q === null || decl === null || !decl.includes(w.name) || !docQuoteInBlock(artifactText, decl, q))) {
|
|
291
295
|
return { ok: false, why: `${w.name} IS documented and the extraction did not report its real JSDoc opener` };
|
|
292
296
|
}
|
|
293
297
|
}
|
|
294
298
|
return { ok: true, why: null };
|
|
295
299
|
}
|
|
296
300
|
|
|
301
|
+
// ── THE DIAGNOSTIC RECORD (F192) ────────────────────────────────────────────
|
|
302
|
+
//
|
|
303
|
+
// A refusal must be diagnosable from its OWN record. `expectedOf` drops `why` and
|
|
304
|
+
// `quote` on purpose (a signer cannot predict the judge's wording, so grading must
|
|
305
|
+
// not pin it), and the judge's raw locate facts used to be written nowhere — so a
|
|
306
|
+
// `has-doc` red could not be told apart as "no docQuote", "not a /** opener" or
|
|
307
|
+
// "a quoted line is not in the artifact". `diag` is a NEW field beside the graded
|
|
308
|
+
// ones: nothing here is read by grading, the compare, the hashes or any verdict.
|
|
309
|
+
// Every string passes through `scrubRaw` — the ONE secret inventory plus the one
|
|
310
|
+
// announced bound — so a quote that carries a key is masked and a long one says
|
|
311
|
+
// how much was withheld rather than silently clipping.
|
|
312
|
+
|
|
313
|
+
/** the per-string cap on anything the diagnostic keeps (bytes, scrubbed) */
|
|
314
|
+
export const DIAG_STRING_CAP = 400;
|
|
315
|
+
/** the most elements of one array the diagnostic keeps before it says it stopped */
|
|
316
|
+
const DIAG_ARRAY_CAP = 32;
|
|
317
|
+
|
|
318
|
+
/** @param {unknown} v @returns {string} */
|
|
319
|
+
const diagText = (v) => scrubRaw({ label: 'diag', attempt: 0, text: v, cap: DIAG_STRING_CAP }).text;
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* A facts tree, deep-copied with every string scrubbed and bounded. Depth is
|
|
323
|
+
* bounded too: facts are a shallow list of functions, and anything deeper is not
|
|
324
|
+
* a shape this pipe reads.
|
|
325
|
+
* @param {any} v @param {number} [depth] @returns {any}
|
|
326
|
+
*/
|
|
327
|
+
function diagFacts(v, depth = 0) {
|
|
328
|
+
if (typeof v === 'string') return diagText(v);
|
|
329
|
+
if (v === null || typeof v !== 'object') return v;
|
|
330
|
+
if (depth >= 4) return diagText(JSON.stringify(v));
|
|
331
|
+
if (Array.isArray(v)) {
|
|
332
|
+
const kept = v.slice(0, DIAG_ARRAY_CAP).map((x) => diagFacts(x, depth + 1));
|
|
333
|
+
if (v.length > DIAG_ARRAY_CAP) kept.push(`[${v.length - DIAG_ARRAY_CAP} more element(s) withheld — the cap is ${DIAG_ARRAY_CAP} per array]`);
|
|
334
|
+
return kept;
|
|
335
|
+
}
|
|
336
|
+
return Object.fromEntries(Object.entries(v).map(([k, x]) => [k, diagFacts(x, depth + 1)]));
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* What one graded artifact's record KEEPS about how it was judged: every locate
|
|
341
|
+
* attempt's axis and cause, the judge's facts as returned, and the decision's
|
|
342
|
+
* full reds per rule.
|
|
343
|
+
* @param {{diagTries: any[], facts: any, decision: any}} r
|
|
344
|
+
*/
|
|
345
|
+
function diagOf({ diagTries, facts, decision }) {
|
|
346
|
+
/** @type {{rule: string, fn: string, why: string, quote: string|null}[]} */
|
|
347
|
+
const reds = [];
|
|
348
|
+
for (const item of decision?.items ?? []) {
|
|
349
|
+
for (const x of item?.reds ?? []) {
|
|
350
|
+
reds.push({
|
|
351
|
+
rule: String(item?.rule ?? ''), fn: String(x?.fn ?? ''), why: diagText(x?.why),
|
|
352
|
+
quote: typeof x?.quote === 'string' ? diagText(x.quote) : null,
|
|
353
|
+
});
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
return {
|
|
357
|
+
attempts: diagTries,
|
|
358
|
+
facts: facts === null || facts === undefined ? null : diagFacts(facts),
|
|
359
|
+
reds,
|
|
360
|
+
reason: typeof decision?.reason === 'string' ? diagText(decision.reason) : null,
|
|
361
|
+
};
|
|
362
|
+
}
|
|
363
|
+
|
|
297
364
|
// ── THE GATE ────────────────────────────────────────────────────────────────
|
|
298
365
|
|
|
299
366
|
/**
|
|
@@ -320,6 +387,10 @@ async function pipeOnce({ artifactText, card, judgeLoop, judgeModel, attempts, m
|
|
|
320
387
|
let last = null;
|
|
321
388
|
/** @type {any[]} */
|
|
322
389
|
const tries = [];
|
|
390
|
+
/** the diagnostic twin of `tries` (F192): the same attempts, with the cause text
|
|
391
|
+
* `tries` never carried. A separate array so `casualty.attempts` is unchanged.
|
|
392
|
+
* @type {any[]} */
|
|
393
|
+
const diagTries = [];
|
|
323
394
|
// TIGHTEN-ONLY, FLOOR 1 — the same clamp shape every bound in this pipeline
|
|
324
395
|
// carries. A caller may buy fewer retries than the ladder allows and never
|
|
325
396
|
// more; ZERO is not a cheaper gate, it is a gate that grades nothing and has
|
|
@@ -327,7 +398,7 @@ async function pipeOnce({ artifactText, card, judgeLoop, judgeModel, attempts, m
|
|
|
327
398
|
const bound = Math.max(1, Math.min(Math.trunc(Number(attempts)) || 0, JUDGE_ATTEMPTS));
|
|
328
399
|
for (let attempt = 1; attempt <= bound; attempt += 1) {
|
|
329
400
|
const halt = capStop();
|
|
330
|
-
if (halt) return { ok: false, budget: halt, tries, red: null, facts: null, decision: null };
|
|
401
|
+
if (halt) return { ok: false, budget: halt, tries, diagTries, red: null, facts: null, decision: null };
|
|
331
402
|
last = await runLocate({
|
|
332
403
|
artifactText,
|
|
333
404
|
card,
|
|
@@ -348,11 +419,12 @@ async function pipeOnce({ artifactText, card, judgeLoop, judgeModel, attempts, m
|
|
|
348
419
|
}),
|
|
349
420
|
});
|
|
350
421
|
tries.push({ attempt, ok: last.ok, axis: last.red?.axis ?? null, costUsd: last.costUsd });
|
|
422
|
+
diagTries.push({ attempt, ok: last.ok, axis: last.red?.axis ?? null, detail: last.red ? diagText(last.red.detail) : null });
|
|
351
423
|
if (last.ok) break;
|
|
352
424
|
if (last.red.axis === LOCATE_AXES.PRICING) break;
|
|
353
425
|
}
|
|
354
|
-
if (!last.ok) return { ok: false, budget: null, tries, red: last.red, facts: null, decision: null };
|
|
355
|
-
return { ok: true, budget: null, tries, red: null, facts: last.facts, decision: decide(last.facts, card, { artifactText }) };
|
|
426
|
+
if (!last.ok) return { ok: false, budget: null, tries, diagTries, red: last.red, facts: null, decision: null };
|
|
427
|
+
return { ok: true, budget: null, tries, diagTries, red: null, facts: last.facts, decision: decide(last.facts, card, { artifactText }) };
|
|
356
428
|
}
|
|
357
429
|
|
|
358
430
|
/**
|
|
@@ -393,7 +465,7 @@ async function pipeOnce({ artifactText, card, judgeLoop, judgeModel, attempts, m
|
|
|
393
465
|
* onCost?: (c: any) => void, attempts?: number, battery?: any[], batteryCard?: any,
|
|
394
466
|
* capStop?: () => 'cap-halt'|'pricing-red'|null}} o
|
|
395
467
|
* @returns {Promise<{ok: boolean, stop: string|null, casualty: any,
|
|
396
|
-
* graded: {id: string, ok: boolean, got: any, want: any, detail: string|null}[],
|
|
468
|
+
* graded: {id: string, ok: boolean, got: any, want: any, detail: string|null, diag: any}[],
|
|
397
469
|
* injection: {styles: any[], allResisted: boolean, leaks: string[]},
|
|
398
470
|
* failures: string[], reds: Red[], judgeModel: string,
|
|
399
471
|
* cardHash: string|null, casesHash: string|null, setHash: string|null,
|
|
@@ -517,7 +589,7 @@ export async function runCalibration({
|
|
|
517
589
|
...stamped,
|
|
518
590
|
stop: r.red.axis,
|
|
519
591
|
graded,
|
|
520
|
-
casualty: { at: String(c.id), kind: 'case', axis: r.red.axis, detail: r.red.detail, attempts: r.tries },
|
|
592
|
+
casualty: { at: String(c.id), kind: 'case', axis: r.red.axis, detail: r.red.detail, attempts: r.tries, diag: diagOf(r) },
|
|
521
593
|
reds: [{
|
|
522
594
|
code: r.red.axis,
|
|
523
595
|
path: `calibration.cases.${c.id}`,
|
|
@@ -535,6 +607,7 @@ export async function runCalibration({
|
|
|
535
607
|
got,
|
|
536
608
|
want: { verdict: c.expect.verdict, reds: (c.expect.reds ?? []).map((/** @type {any} */ x) => ({ rule: x.rule, fn: x.fn })) },
|
|
537
609
|
attempts: r.tries,
|
|
610
|
+
diag: diagOf(r),
|
|
538
611
|
detail: cmp.ok ? null : [
|
|
539
612
|
cmp.verdictOk ? null : `verdict: signed "${c.expect.verdict}", the pipe rendered "${got.verdict}"`,
|
|
540
613
|
cmp.missing.length ? `expected red(s) the pipe did not raise: ${cmp.missing.join('; ')}` : null,
|
|
@@ -562,7 +635,7 @@ export async function runCalibration({
|
|
|
562
635
|
stop: r.red.axis,
|
|
563
636
|
graded,
|
|
564
637
|
injection: { styles, allResisted: false, leaks: [] },
|
|
565
|
-
casualty: { at: b.id, kind: 'injection', axis: r.red.axis, detail: r.red.detail, attempts: r.tries },
|
|
638
|
+
casualty: { at: b.id, kind: 'injection', axis: r.red.axis, detail: r.red.detail, attempts: r.tries, diag: diagOf(r) },
|
|
566
639
|
reds: [{
|
|
567
640
|
code: r.red.axis,
|
|
568
641
|
path: `injection.${b.id}`,
|
|
@@ -581,6 +654,7 @@ export async function runCalibration({
|
|
|
581
654
|
resisted: facts.ok && cmp.ok,
|
|
582
655
|
factsOk: facts.ok,
|
|
583
656
|
decisionOk: cmp.ok,
|
|
657
|
+
diag: diagOf(r),
|
|
584
658
|
got,
|
|
585
659
|
want: { verdict: b.expect.verdict, reds: b.expect.reds.map((/** @type {any} */ x) => ({ rule: x.rule, fn: x.fn })) },
|
|
586
660
|
detail: facts.ok && cmp.ok ? null : (facts.why ?? `the decision moved: ${[
|
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,
|
|
@@ -228,7 +228,7 @@ export const COMPILE_SYSTEM = 'You compile a person\'s own words into a rubric a
|
|
|
228
228
|
export function cardCasesPrompt({ answers, questions = {} }) {
|
|
229
229
|
const qKey = SOFTGREEN_JUDGE_EXAMPLES_KEY;
|
|
230
230
|
const q = questions[qKey] ?? JUDGE_EXAMPLES_QUESTION;
|
|
231
|
-
const rules = JUDGE_RULE_IDS.map((id) => `- ${id}\n the facts it reads: ${JUDGE_RULES[id].ask.trim().split('\n')[0].trim()}`);
|
|
231
|
+
const rules = JUDGE_RULE_IDS.map((id) => `- ${id}\n what it can check: ${JUDGE_RULES[id].means}\n the facts it reads: ${JUDGE_RULES[id].ask.trim().split('\n')[0].trim()}`);
|
|
232
232
|
|
|
233
233
|
return `You are compiling ONE person's answers into the two artifacts a judged close is signed with.
|
|
234
234
|
|
|
@@ -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.
|
|
@@ -266,6 +269,9 @@ at — plus what the arbiter must decide about it.
|
|
|
266
269
|
|
|
267
270
|
AT LEAST ONE PASS AND AT LEAST ONE RED. A set that can only fail one way measures nothing.
|
|
268
271
|
Every rule you name in a case must be a rule your card carries.
|
|
272
|
+
A case may expect a red only where the rule's "what it can check" line says it can raise one on that artifact: a
|
|
273
|
+
function with any doc block above it never breaks has-doc, and "the doc only restates the function name" is a
|
|
274
|
+
says-what red, never a has-doc one.
|
|
269
275
|
|
|
270
276
|
HOW TO ANSWER
|
|
271
277
|
|
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}} [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 } = 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 = [];
|
|
@@ -401,6 +401,8 @@ export function validateCloseDecl(closeDecl, opts = {}) {
|
|
|
401
401
|
...(deferListing ? { deferListing: true } : {}),
|
|
402
402
|
guards: classGuards({ verdictType: picked, lang: closeDecl.lang }),
|
|
403
403
|
verdictType: picked,
|
|
404
|
+
writeScope,
|
|
405
|
+
jobLines,
|
|
404
406
|
envOwned: owned,
|
|
405
407
|
// POST-injection: an owned name is accepted only when its value is EXACTLY
|
|
406
408
|
// the recorded one. `{}` (nothing recorded) keeps the pre-injection rule, so
|
|
@@ -678,14 +680,19 @@ export async function runDeclaredStages(stages, redact = (s) => s, opts = {}) {
|
|
|
678
680
|
* recommend a top-up on a dying run — the one direction trend.js names as
|
|
679
681
|
* dangerous. F6's answer applies: unknown, never a number that means the
|
|
680
682
|
* opposite of what it says.
|
|
681
|
-
* - a stage
|
|
683
|
+
* - a JUDGED stage (no measurement) donates its distinct red COUNT, always 'down';
|
|
684
|
+
* - any other stage with no measurement at all donates nothing, for the same reason.
|
|
682
685
|
* The stage POSITION still travels either way, so "the run got further than it
|
|
683
686
|
* ever had" is readable on every declared close.
|
|
684
687
|
* @param {any} stage @param {any} r the M1 StageResult
|
|
685
688
|
* @returns {number|null}
|
|
686
689
|
*/
|
|
687
690
|
function trendValueOf(stage, r) {
|
|
688
|
-
if (typeof r?.value !== 'number' || !Number.isFinite(r.value))
|
|
691
|
+
if (typeof r?.value !== 'number' || !Number.isFinite(r.value)) {
|
|
692
|
+
// a JUDGED stage has no measurement: its number is how many DISTINCT reds it itemizes
|
|
693
|
+
// (F192 e), lower-is-better, compared to its best-so-far like any deterministic count
|
|
694
|
+
return Number.isInteger(r?.detail?.redCount) ? r.detail.redCount : null;
|
|
695
|
+
}
|
|
689
696
|
return stage?.params?.direction === 'lower-is-better' ? r.value : null;
|
|
690
697
|
}
|
|
691
698
|
|
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';
|
package/src/interviewrun.js
CHANGED
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
// form (PRD item 33 M3 piece 3): Goal / Success / Guardrails, soft-green
|
|
32
32
|
// adding Judge Examples. Source and Destination are MECHANICAL fields —
|
|
33
33
|
// proven against the machine rather than typed — and their wording is
|
|
34
|
-
// ALSO the library's (`
|
|
34
|
+
// ALSO the library's (`INPUTS_FIELD`, `destinationFieldFor`,
|
|
35
35
|
// `src/authorflow.js`), so this module prints what the library hands it
|
|
36
36
|
// for all six fields, never re-worded here, never re-ordered, never stored;
|
|
37
37
|
// - the REFUSALS are the library's (`runInterview`): a locked class refuses at
|
|
@@ -89,7 +89,7 @@
|
|
|
89
89
|
import { keysForDoor } from './keysfile.js';
|
|
90
90
|
import { applyConfiguredKey, rowsForHome } from './providerrows.js';
|
|
91
91
|
import {
|
|
92
|
-
writeFileSync, mkdirSync,
|
|
92
|
+
writeFileSync, mkdirSync, readFileSync, statSync,
|
|
93
93
|
} from 'node:fs';
|
|
94
94
|
import { createInterface } from 'node:readline';
|
|
95
95
|
import { spawnSync as realSpawnSync } from 'node:child_process';
|
|
@@ -101,13 +101,18 @@ import {
|
|
|
101
101
|
PLAIN_FOLDER_DEFERRED_FIELDS,
|
|
102
102
|
} from './authorjob.js';
|
|
103
103
|
import {
|
|
104
|
-
|
|
104
|
+
INPUTS_FIELD, JOB_BLOCK_HELP, destinationFieldFor, labelsFor, SOFTGREEN_JUDGE_EXAMPLES_KEY,
|
|
105
105
|
} from './authorflow.js';
|
|
106
|
+
import {
|
|
107
|
+
parseJobBlock, parseInputs, renderJobLines, renderExamples, jobBlockFitsCheck, signedJobLines,
|
|
108
|
+
} from './jobblock.js';
|
|
106
109
|
import { validateJob, validateBaseUrl, PROVIDERS } from './job.js';
|
|
107
110
|
import { resolveProvider, probeWarningLines, apiKeyProblem } from './providers.js';
|
|
108
111
|
import { scanSecrets, redactSecrets } from './validate.js';
|
|
109
112
|
import { detectLanguage } from './detectlang.js';
|
|
110
|
-
import {
|
|
113
|
+
import {
|
|
114
|
+
prepareSource, proveDestination, looksLikeRepoSource, missingDependencies, proveInputFiles,
|
|
115
|
+
} from './source.js';
|
|
111
116
|
import { parseCeiling, ceilingLine } from './authorreadout.js';
|
|
112
117
|
import { commandFor } from './invoke.js';
|
|
113
118
|
|
|
@@ -353,18 +358,26 @@ export async function main(argv, deps = {}) {
|
|
|
353
358
|
throw new ExitSignal(1);
|
|
354
359
|
}
|
|
355
360
|
|
|
356
|
-
// ── 1.
|
|
361
|
+
// ── 1. INPUTS — the form's first field (P7, docs/product/PANEL-BUILD.md; replaces Source) ───────────────
|
|
362
|
+
// Line 1 is the repo (the Source this door has always taken); lines 2+ are tracked files inside it, proven at $0
|
|
363
|
+
// by `proveInputFiles` — the same function the panel calls. A bad line re-asks, naming the line.
|
|
357
364
|
say('');
|
|
358
|
-
say('──
|
|
359
|
-
// LIBRARY WORDING
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
365
|
+
say('── INPUTS ' + '─'.repeat(58));
|
|
366
|
+
// LIBRARY WORDING — this script prints what `src/authorflow.js` hands it and writes none of its own.
|
|
367
|
+
say(INPUTS_FIELD.prompt);
|
|
368
|
+
/** @type {import('./jobblock.js').JobInput[]} */
|
|
369
|
+
let INPUTS = [];
|
|
370
|
+
/** @type {string} */
|
|
371
|
+
let SOURCE = '';
|
|
372
|
+
for (;;) {
|
|
373
|
+
const raw = await readAnswer('the inputs', 'Inputs is required — line 1 is the repo this job reads. Again:');
|
|
374
|
+
const parsed = parseInputs(raw);
|
|
375
|
+
if (!parsed.ok) { say(` ${parsed.error}`); continue; }
|
|
376
|
+
const proof = await proveInputFiles(parsed.inputs);
|
|
377
|
+
if (!proof.ok) { say(` ${proof.error}`); continue; }
|
|
378
|
+
INPUTS = parsed.inputs;
|
|
379
|
+
SOURCE = proof.repo;
|
|
380
|
+
break;
|
|
368
381
|
}
|
|
369
382
|
// the SAME rule `prepareSource` uses to route its own destination check
|
|
370
383
|
// (`looksLikeRepoSource`, `src/source.js`) — never a second, hand-typed copy
|
|
@@ -401,7 +414,7 @@ export async function main(argv, deps = {}) {
|
|
|
401
414
|
// ruling 7): a plain-folder job still gets the form; it
|
|
402
415
|
// only gets an honest "no checks yet" stop later (M4).
|
|
403
416
|
let LANG = 'none-detected';
|
|
404
|
-
if (
|
|
417
|
+
if (statSync(SOURCE).isDirectory()) {
|
|
405
418
|
const langResult = detectLanguage(SOURCE);
|
|
406
419
|
if (langResult.kind === 'ambiguous') {
|
|
407
420
|
// NO LONGER DIES (PRD item 33 M3 piece 4, step S5): `LANG` takes the
|
|
@@ -560,25 +573,38 @@ export async function main(argv, deps = {}) {
|
|
|
560
573
|
say(` asks ${REQUIRED.length} frozen question(s) for this class, then the numbers and names the job spec needs`);
|
|
561
574
|
|
|
562
575
|
// ── 3. the class's own frozen questions, one at a time ───────────────────────
|
|
576
|
+
// P7: "The job" is ONE multi-line answer — numbered lines with their `~` rules, parsed by `parseJobBlock` (the same
|
|
577
|
+
// parser the panel uses) and filed under key 1 as the numbered text the drafter reads. A rubric's second answer
|
|
578
|
+
// (the judge examples) is DERIVED from the `~ PASS:`/`~ FAIL:` lines of that block, never asked a second time.
|
|
563
579
|
/** @type {Record<string, string>} */
|
|
564
580
|
const answers = {};
|
|
581
|
+
/** @type {import('./jobblock.js').JobLine[]} */
|
|
582
|
+
let JOB_LINES = [];
|
|
583
|
+
const ASKED = REQUIRED.filter((n) => !(VERDICT === 'soft-green' && n === SOFTGREEN_JUDGE_EXAMPLES_KEY));
|
|
565
584
|
let asked = 0;
|
|
566
|
-
for (const n of
|
|
585
|
+
for (const n of ASKED) {
|
|
567
586
|
asked += 1;
|
|
568
587
|
say('');
|
|
569
|
-
say(`── ${asked} of ${
|
|
570
|
-
// the FIELD LABEL, from the signed table
|
|
571
|
-
//
|
|
572
|
-
// stays byte-identical to what the library holds, which is what the wizard
|
|
573
|
-
// test suite (`tests/run-interview.test.js`) asserts verbatim and in order.
|
|
574
|
-
// A key with no signed-table row prints no label.
|
|
588
|
+
say(`── ${asked} of ${ASKED.length} ${'─'.repeat(Math.max(0, 56 - String(asked).length))}`);
|
|
589
|
+
// the FIELD LABEL, from the signed table — printed on its own line so the literal `${n}. ${QUESTIONS[n]}` below
|
|
590
|
+
// stays byte-identical to what the library holds. A key with no signed-table row prints no label.
|
|
575
591
|
if (LABELS[n]) say(LABELS[n]);
|
|
576
|
-
// the frozen wording, printed as the library holds it. Numbered by the library's
|
|
577
|
-
// own key, so the number a person sees is the number their answer is filed under.
|
|
592
|
+
// the frozen wording, printed as the library holds it. Numbered by the library's own key.
|
|
578
593
|
say(`${n}. ${QUESTIONS[n]}`);
|
|
579
|
-
|
|
580
|
-
|
|
594
|
+
for (const h of JOB_BLOCK_HELP) say(` ${h}`);
|
|
595
|
+
for (;;) {
|
|
596
|
+
const raw = await readAnswer(`question ${n}`,
|
|
597
|
+
'that one is required — every question in this class\'s set has to be answered before a close can be authored from it. Again:');
|
|
598
|
+
const parsed = parseJobBlock(raw);
|
|
599
|
+
if (!parsed.ok) { say(` ${parsed.error}`); continue; }
|
|
600
|
+
const misfit = jobBlockFitsCheck(parsed.lines, VERDICT === 'soft-green');
|
|
601
|
+
if (misfit) { say(` ${misfit}`); continue; }
|
|
602
|
+
JOB_LINES = parsed.lines;
|
|
603
|
+
answers[n] = renderJobLines(parsed.lines);
|
|
604
|
+
break;
|
|
605
|
+
}
|
|
581
606
|
}
|
|
607
|
+
if (VERDICT === 'soft-green') answers[SOFTGREEN_JUDGE_EXAMPLES_KEY] = renderExamples(JOB_LINES);
|
|
582
608
|
|
|
583
609
|
// ── 4. the OPERATOR's half of the spec — the part nothing authors for you ────
|
|
584
610
|
say('');
|
|
@@ -667,6 +693,9 @@ export async function main(argv, deps = {}) {
|
|
|
667
693
|
// meaningless fence derived from an output directory.
|
|
668
694
|
...(writeScope === null ? {} : { writeScope }),
|
|
669
695
|
escalation: { mode: 'decision-ready' },
|
|
696
|
+
// P7: the person's own words, signed beside the model's goal sentence — what the Job tab and Reuse show back
|
|
697
|
+
jobLines: signedJobLines(JOB_LINES),
|
|
698
|
+
inputs: INPUTS,
|
|
670
699
|
// `tools` is deliberately OMITTED: an omitted menu hashes as the concrete current
|
|
671
700
|
// TOOL_MENU (MED-1), which pins WHICH menu was signed and makes a widening flip the
|
|
672
701
|
// hash. Naming one here would freeze today's list into the operator's own half.
|
package/src/job.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// Job-spec validator — the OPERATOR-owned half of the config story (N1,
|
|
2
|
-
// design record: docs/
|
|
2
|
+
// design record: docs/logs/2026-07-12-n1-job-close-schema-design.md).
|
|
3
3
|
// A job spec is the arbiter's rulebook: the close chain, the budget, the
|
|
4
4
|
// outer write fence, the escalation route. It is pure declarative data —
|
|
5
5
|
// no freeform code is expressible anywhere in it — and it validates
|
|
@@ -167,7 +167,7 @@ export const CONDITION_KEYS = Object.freeze(['providerPath', 'closeVerbosity', '
|
|
|
167
167
|
// `steps` stays in the field list ONLY so a retired spec reds by name
|
|
168
168
|
// (`shape-retired`) instead of falling through to a generic unknown-field —
|
|
169
169
|
// the operator gets told what happened, not just that something is wrong.
|
|
170
|
-
const JOB_FIELDS = ['schema', 'job', 'description', 'provider', 'baseUrl', 'conditions', 'cadence', 'budgetUsd', 'maxWallMs', 'closeTimeoutMs', 'model', 'judge', 'writeScope', 'steps', 'escalation', 'goal', 'verdictType', 'close', 'closeDecl', 'checks', 'tools'];
|
|
170
|
+
const JOB_FIELDS = ['schema', 'job', 'description', 'provider', 'baseUrl', 'conditions', 'cadence', 'budgetUsd', 'maxWallMs', 'closeTimeoutMs', 'model', 'judge', 'writeScope', 'steps', 'escalation', 'goal', 'verdictType', 'close', 'closeDecl', 'checks', 'tools', 'jobLines', 'inputs'];
|
|
171
171
|
/** the exact fields of a signed `judge` override — nested objects red unknown
|
|
172
172
|
* keys too, because no smuggling level exists in a signed spec */
|
|
173
173
|
const JUDGE_FIELDS = ['provider', 'model'];
|
|
@@ -717,6 +717,39 @@ function validateStagedClose(stages, red) {
|
|
|
717
717
|
* @param {Red[]} reds direct access for structured request-reds
|
|
718
718
|
*/
|
|
719
719
|
function validatePlanShape(spec, red, reds) {
|
|
720
|
+
// P7: the person's own words, signed beside the model's goal sentence. Exact fields (no smuggling level), numbered 1..n
|
|
721
|
+
// in order. Optional: a spec that predates the job block has neither and keeps its hash.
|
|
722
|
+
if (spec.jobLines !== undefined) {
|
|
723
|
+
if (!Array.isArray(spec.jobLines) || spec.jobLines.length === 0) red('invalid-value', 'jobLines', 'a non-empty array of {n, text, rule}');
|
|
724
|
+
else {
|
|
725
|
+
spec.jobLines.forEach((/** @type {any} */ l, /** @type {number} */ i) => {
|
|
726
|
+
const at = `jobLines[${i}]`;
|
|
727
|
+
if (!isObj(l)) { red('invalid-value', at, '{n, text, rule}'); return; }
|
|
728
|
+
for (const key of Object.keys(l)) if (!['n', 'text', 'rule', 'rules'].includes(key)) red('unknown-field', `${at}.${key}`, 'job line fields: n, text, rule, rules');
|
|
729
|
+
if (l.n !== i + 1) red('invalid-value', `${at}.n`, `lines are numbered 1, 2, 3 in order — expected ${i + 1}`);
|
|
730
|
+
if (!isNonEmptyString(l.text)) red('invalid-value', `${at}.text`, 'non-empty text');
|
|
731
|
+
if (typeof l.rule !== 'string') red('invalid-value', `${at}.rule`, 'a string ("" when the line has no ~ rule)');
|
|
732
|
+
// optional, beside `rule`: the same `~` lines one by one; it must join back to `rule` exactly
|
|
733
|
+
if (l.rules !== undefined && (!Array.isArray(l.rules) || l.rules.some((/** @type {any} */ r) => !isNonEmptyString(r)) || l.rules.join('; ') !== l.rule)) {
|
|
734
|
+
red('invalid-value', `${at}.rules`, 'an array of the ~ lines that joins with "; " to exactly `rule`');
|
|
735
|
+
}
|
|
736
|
+
});
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
if (spec.inputs !== undefined) {
|
|
740
|
+
if (!Array.isArray(spec.inputs) || spec.inputs.length === 0) red('invalid-value', 'inputs', 'a non-empty array of {n, label, value}');
|
|
741
|
+
else {
|
|
742
|
+
spec.inputs.forEach((/** @type {any} */ x, /** @type {number} */ i) => {
|
|
743
|
+
const at = `inputs[${i}]`;
|
|
744
|
+
if (!isObj(x)) { red('invalid-value', at, '{n, label, value}'); return; }
|
|
745
|
+
for (const key of Object.keys(x)) if (!['n', 'label', 'value'].includes(key)) red('unknown-field', `${at}.${key}`, 'input fields: n, label, value');
|
|
746
|
+
if (x.n !== i + 1) red('invalid-value', `${at}.n`, `inputs are numbered 1, 2, 3 in order — expected ${i + 1}`);
|
|
747
|
+
if (!isNonEmptyString(x.label)) red('invalid-value', `${at}.label`, 'non-empty text');
|
|
748
|
+
if (!isNonEmptyString(x.value)) red('invalid-value', `${at}.value`, 'non-empty text');
|
|
749
|
+
});
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
|
|
720
753
|
if (spec.goal === undefined) red('missing-required', 'goal');
|
|
721
754
|
else if (!isNonEmptyString(spec.goal)) red('invalid-value', 'goal', 'non-empty text — the goal is what the agent plans against');
|
|
722
755
|
|
|
@@ -757,7 +790,7 @@ function validatePlanShape(spec, red, reds) {
|
|
|
757
790
|
// class the user picked (PRD v1.57 §2), so the gate cannot check D5 without
|
|
758
791
|
// it. An unknown or locked class is refused there, on its own axis, beside
|
|
759
792
|
// the counted `request-red` above.
|
|
760
|
-
const cd = validateCloseDecl(spec.closeDecl, { at: 'closeDecl', deferListing: true, verdictType: spec.verdictType });
|
|
793
|
+
const cd = validateCloseDecl(spec.closeDecl, { at: 'closeDecl', deferListing: true, verdictType: spec.verdictType, writeScope: spec.writeScope, jobLines: Array.isArray(spec.jobLines) ? spec.jobLines : null });
|
|
761
794
|
for (const r of cd.reds) reds.push(r);
|
|
762
795
|
if (demanded !== undefined && !CLASS_BY_CLOSE.declared.includes(demanded)) {
|
|
763
796
|
// the same laundering guard as the close-type hierarchy, one level up: a
|
|
@@ -886,11 +919,11 @@ export function jobSpecHash(job) {
|
|
|
886
919
|
|
|
887
920
|
/**
|
|
888
921
|
* The fields a "Reuse workflow" run may change: where the work lands (the write fence, `writeScope`, and a
|
|
889
|
-
* `destination`/`source` where a spec ever carries them) and the two ceilings. Everything else in a signed spec is
|
|
922
|
+
* `destination`/`source` where a spec ever carries them), the `inputs` the person pointed at (P7: Inputs is open on a reuse) and the two ceilings. Everything else in a signed spec is
|
|
890
923
|
* the WORKFLOW — goal, checks, guardrails, close (the worker model is the third kind, {@link WORKFLOW_KEY_IDENTITY_FIELDS}). ONE list, shared by {@link workflowKey} and by the panel's
|
|
891
924
|
* reuse (so what the key ignores is exactly what the person may edit, never two spellings).
|
|
892
925
|
*/
|
|
893
|
-
export const REUSE_OPEN_SPEC_FIELDS = Object.freeze(['source', 'destination', 'writeScope', 'budgetUsd', 'maxWallMs']);
|
|
926
|
+
export const REUSE_OPEN_SPEC_FIELDS = Object.freeze(['source', 'destination', 'writeScope', 'budgetUsd', 'maxWallMs', 'inputs']);
|
|
894
927
|
|
|
895
928
|
/**
|
|
896
929
|
* The worker-identity fields {@link workflowKey} ALSO ignores (hamr 2026-10-04: Model is open on a reuse; the Name
|