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/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, writeScope: spec.writeScope });
|
|
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
|
package/src/jobblock.js
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
// The "job" block's parser (P7, docs/product/PANEL-BUILD.md "Addendum 2026-10-10"): ONE owner of how the person's
|
|
2
|
+
// own multi-line words become numbered job lines and numbered inputs. The panel server AND the CLI interview both
|
|
3
|
+
// call these two functions, so the two doors can never read the same text two ways.
|
|
4
|
+
//
|
|
5
|
+
// Both are pure and $0: a malformed block is a named red `{ok: false, error}`, never a throw and never a guess.
|
|
6
|
+
|
|
7
|
+
/** @typedef {{verdict: 'PASS'|'FAIL', text: string}} JobExample a `~ PASS:` / `~ FAIL:` line (rubric only), kept with its job line */
|
|
8
|
+
/** @typedef {{n: number, text: string, rule: string, rules: string[], examples: JobExample[]}} JobLine one numbered job line; `rule` = its `~` lines joined with '; ' ('' when none), `rules` = those same `~` lines one by one (so an example's own '; ' survives) */
|
|
9
|
+
/** @typedef {{n: number, label: string, value: string}} JobInput one numbered `label: value` input line */
|
|
10
|
+
|
|
11
|
+
/** the joiner for consecutive `~` lines (fwd's joinGuardrails) */
|
|
12
|
+
export const RULE_JOINER = '; ';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Parse "The job" box. A plain line is a numbered job line (1, 2, 3...). A line starting `~` has no number and belongs
|
|
16
|
+
* to the numbered line above; consecutive `~` lines become ONE rule joined with `'; '`. A `~` line starting `PASS:` or
|
|
17
|
+
* `FAIL:` is also a judge example (kept with its line number); any other `~` line is a guardrail. Empty lines are
|
|
18
|
+
* ignored. A `~` before line 1 is a red.
|
|
19
|
+
* @param {unknown} text the box's raw text
|
|
20
|
+
* @returns {{ok: true, lines: JobLine[]}|{ok: false, error: string}}
|
|
21
|
+
*/
|
|
22
|
+
export function parseJobBlock(text) {
|
|
23
|
+
if (typeof text !== 'string') return { ok: false, error: 'The job is required' };
|
|
24
|
+
/** @type {JobLine[]} */
|
|
25
|
+
const lines = [];
|
|
26
|
+
/** @type {string[]} */
|
|
27
|
+
let rule = [];
|
|
28
|
+
const flush = () => { if (lines.length) { lines[lines.length - 1].rule = rule.join(RULE_JOINER); lines[lines.length - 1].rules = rule; } rule = []; };
|
|
29
|
+
let row = 0;
|
|
30
|
+
for (const raw of text.split(/\r?\n/)) {
|
|
31
|
+
const t = raw.trim();
|
|
32
|
+
if (t === '') continue;
|
|
33
|
+
row += 1;
|
|
34
|
+
if (t.startsWith('~')) {
|
|
35
|
+
if (lines.length === 0) return { ok: false, error: `The job, line ${row}: a "~" line belongs to the numbered line above it, and there is none yet` };
|
|
36
|
+
const body = t.slice(1).trim();
|
|
37
|
+
if (body === '') continue;
|
|
38
|
+
rule.push(body);
|
|
39
|
+
const m = /^(PASS|FAIL):\s*(.*)$/i.exec(body);
|
|
40
|
+
if (m) {
|
|
41
|
+
if (m[2].trim() === '') return { ok: false, error: `The job, line ${row}: "${m[1].toUpperCase()}:" needs an example after it` };
|
|
42
|
+
lines[lines.length - 1].examples.push({ verdict: /** @type {'PASS'|'FAIL'} */ (m[1].toUpperCase()), text: m[2].trim() });
|
|
43
|
+
}
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
flush();
|
|
47
|
+
lines.push({ n: lines.length + 1, text: t, rule: '', rules: [], examples: [] });
|
|
48
|
+
}
|
|
49
|
+
flush();
|
|
50
|
+
if (lines.length === 0) return { ok: false, error: 'The job is required' };
|
|
51
|
+
return { ok: true, lines };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** a label: a word or two, no colon */
|
|
55
|
+
const LABEL_RE = /^[A-Za-z][A-Za-z0-9_ -]{0,39}$/;
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Parse the "Inputs" box: one `label: value` per line, numbered 1, 2, 3 in the order written. Empty lines are ignored.
|
|
59
|
+
* This only checks the SHAPE — what a value must point at (line 1 an absolute repo, lines 2+ tracked files inside it)
|
|
60
|
+
* is proven by the panel at $0.
|
|
61
|
+
* @param {unknown} text
|
|
62
|
+
* @returns {{ok: true, inputs: JobInput[]}|{ok: false, error: string}}
|
|
63
|
+
*/
|
|
64
|
+
export function parseInputs(text) {
|
|
65
|
+
if (typeof text !== 'string') return { ok: false, error: 'Inputs is required' };
|
|
66
|
+
/** @type {JobInput[]} */
|
|
67
|
+
const inputs = [];
|
|
68
|
+
for (const raw of text.split(/\r?\n/)) {
|
|
69
|
+
const t = raw.trim();
|
|
70
|
+
if (t === '') continue;
|
|
71
|
+
const n = inputs.length + 1;
|
|
72
|
+
const i = t.indexOf(':');
|
|
73
|
+
const label = i < 0 ? '' : t.slice(0, i).trim();
|
|
74
|
+
const value = i < 0 ? '' : t.slice(i + 1).trim();
|
|
75
|
+
if (!LABEL_RE.test(label) || value === '') return { ok: false, error: `Inputs, line ${n}: write it as "label: value"` };
|
|
76
|
+
inputs.push({ n, label, value });
|
|
77
|
+
}
|
|
78
|
+
if (inputs.length === 0) return { ok: false, error: 'Inputs is required' };
|
|
79
|
+
return { ok: true, inputs };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* The person's job lines as the drafter reads them (answer 1 of the interview): each numbered line, then its rule
|
|
84
|
+
* (the `~` lines joined) indented under it. One spelling, so the number the person typed beside a line is the number a
|
|
85
|
+
* stage's `fromLine` names.
|
|
86
|
+
* @param {JobLine[]} lines
|
|
87
|
+
* @returns {string}
|
|
88
|
+
*/
|
|
89
|
+
export function renderJobLines(lines) {
|
|
90
|
+
return lines.map((l) => `${l.n}. ${l.text}${l.rule ? `\n rule: ${l.rule}` : ''}`).join('\n');
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* The PASS:/FAIL: examples the person wrote, each kept with its line number (answer 2 of a rubric interview — what the
|
|
95
|
+
* calibration compile reads). Empty string when there are none.
|
|
96
|
+
* @param {JobLine[]} lines
|
|
97
|
+
* @returns {string}
|
|
98
|
+
*/
|
|
99
|
+
export function renderExamples(lines) {
|
|
100
|
+
return lines.flatMap((l) => l.examples.map((e) => `line ${l.n} ${e.verdict}: ${e.text}`)).join('\n');
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* The inputs as the drafter is told about them: "the person pointed at these inputs: N label path".
|
|
105
|
+
* @param {JobInput[]} inputs
|
|
106
|
+
* @returns {string}
|
|
107
|
+
*/
|
|
108
|
+
export function renderInputs(inputs) {
|
|
109
|
+
return `the person pointed at these inputs: ${inputs.map((i) => `${i.n} ${i.label} ${i.value}`).join(' · ')}`;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* The two cross-checks between a parsed job block and the Check type: a RUBRIC job needs at least one `PASS:` and one
|
|
114
|
+
* `FAIL:` example (the calibration compile reads them); a DETERMINISTIC job has no judge, so an example line would be
|
|
115
|
+
* silently dropped — a red instead.
|
|
116
|
+
* @param {JobLine[]} lines
|
|
117
|
+
* @param {boolean} rubric
|
|
118
|
+
* @returns {string|null} the plain red, or null when the block fits the Check type
|
|
119
|
+
*/
|
|
120
|
+
export function jobBlockFitsCheck(lines, rubric) {
|
|
121
|
+
const all = lines.flatMap((l) => l.examples.map((e) => ({ n: l.n, ...e })));
|
|
122
|
+
if (!rubric) {
|
|
123
|
+
return all.length ? `The job, line ${all[0].n}: PASS:/FAIL: examples are for a Rubric check — a Deterministic job has no judge to read them` : null;
|
|
124
|
+
}
|
|
125
|
+
if (!all.some((e) => e.verdict === 'PASS') || !all.some((e) => e.verdict === 'FAIL')) {
|
|
126
|
+
return 'The job: a Rubric check needs at least one "~ PASS:" line and one "~ FAIL:" line under a job line';
|
|
127
|
+
}
|
|
128
|
+
return null;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* The signed `jobLines` field from parsed lines: the person's own words, `{n, text, rule, rules}` (examples stay inside the
|
|
133
|
+
* rule text, where the person wrote them). `rules` is the same `~` lines as an array, BESIDE the joined `rule` (never
|
|
134
|
+
* replacing it); it is omitted on a line with no `~` line, and an older spec lacks it entirely.
|
|
135
|
+
* @param {JobLine[]} lines
|
|
136
|
+
* @returns {{n: number, text: string, rule: string, rules?: string[]}[]}
|
|
137
|
+
*/
|
|
138
|
+
export function signedJobLines(lines) {
|
|
139
|
+
return lines.map((l) => ({ n: l.n, text: l.text, rule: l.rule, ...(l.rules.length ? { rules: [...l.rules] } : {}) }));
|
|
140
|
+
}
|
package/src/judged.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// THE JUDGED FLOOR'S CORE — softgreen module 1 (design record
|
|
2
|
-
// docs/
|
|
2
|
+
// docs/logs/2026-08-17-softgreen-review-door-design.md §4, plus the
|
|
3
3
|
// 2026-08-18 threshold addendum). POC: poc/softgreen-judge/poc.mjs — validated
|
|
4
4
|
// logic, rewritten here, never copied (graduation is a rewrite).
|
|
5
5
|
//
|
package/src/ledger.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// The upstream ledger (module 4) — reactive lib/primitive incident monitoring.
|
|
2
|
-
// Design record: docs/
|
|
2
|
+
// Design record: docs/logs/2026-07-11-upstream-ledger-design.md (adaptlearn
|
|
3
3
|
// POC'd spec) + its 2026-07-13 addendum: the bareloop event mapping. One
|
|
4
4
|
// append-only JSONL both the consumer (the panel, N6) and the maintainer read;
|
|
5
5
|
// the A1/A2/A3 upstream-ask flow, mechanized — evidence in, human judgment out.
|
|
@@ -23,6 +23,8 @@ import { openSync } from 'node:fs';
|
|
|
23
23
|
import { join } from 'node:path';
|
|
24
24
|
import { readFileSync } from 'node:fs';
|
|
25
25
|
import { createSession, validateJobCard, validateReuseCard, lockedFieldChanged, buildReuseSpec, cardFields, STEP_LABELS } from './authorsession.js';
|
|
26
|
+
import { parseInputs } from '../jobblock.js';
|
|
27
|
+
import { proveInputFiles } from '../source.js';
|
|
26
28
|
import { validateJob } from '../job.js';
|
|
27
29
|
import { checkMonthlyRoom, monthlyRefusalText } from '../monthly.js';
|
|
28
30
|
import { ConfigError } from '../config.js';
|
|
@@ -274,11 +276,19 @@ export function createAuthorRoutes(opts) {
|
|
|
274
276
|
const v = validateJobCard(card, { jobsDir: opts.jobsDir, rows });
|
|
275
277
|
if (!v.ok) { send(400, { ok: false, error: v.error }); return true; }
|
|
276
278
|
}
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
279
|
+
// P7: the Inputs are proven at $0 before a session (so before any token): line 1 the repo, lines 2+ tracked files inside it.
|
|
280
|
+
// A miss is a plain 400 naming the line, shown under the card exactly like every other refused start.
|
|
281
|
+
const ip = parseInputs(card.inputs);
|
|
282
|
+
if (!ip.ok) { send(400, { ok: false, error: ip.error }); return true; }
|
|
283
|
+
proveInputFiles(ip.inputs).then((proof) => {
|
|
284
|
+
if (!proof.ok) { send(400, { ok: false, error: proof.error }); return; }
|
|
285
|
+
if (hasLiveSession()) { send(409, { ok: false, error: 'an authoring session is already live — one at a time' }); return; }
|
|
286
|
+
const session = createSession(cardFields(card), {
|
|
287
|
+
env: envNow(), sessionsRoot: opts.sessionsRoot, ...(opts.home !== undefined ? { home: opts.home } : {}), ...(reuse ? { reuse } : {}),
|
|
288
|
+
});
|
|
289
|
+
sessions.set(session.id, session);
|
|
290
|
+
send(200, { ok: true, sessionId: session.id, reuse: reuse !== null, state: session.state });
|
|
291
|
+
}).catch((e) => { send(500, { ok: false, error: `could not check the inputs: ${/** @type {Error} */ (e)?.message ?? e}` }); });
|
|
282
292
|
return true;
|
|
283
293
|
}
|
|
284
294
|
|