bareloop 0.41.0 → 0.43.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 +109 -1
- package/README.md +13 -21
- package/bareloop.context.md +97 -34
- package/package.json +4 -3
- package/src/authorflow.js +337 -29
- package/src/authoring.js +92 -7
- package/src/authorjob.js +84 -16
- package/src/authorreadout.js +89 -5
- package/src/authorrun.js +14 -5
- package/src/authorscout.js +108 -0
- package/src/declaredclose.js +25 -14
- package/src/facts.js +317 -0
- package/src/factwords.js +129 -0
- package/src/inputlook.js +301 -0
- package/src/interviewrun.js +0 -1
- package/src/job.js +40 -12
- package/src/jobblock.js +7 -2
- package/src/kinds.js +84 -3
- package/src/labelscan.js +170 -0
- package/src/pagefetch.js +125 -0
- package/src/panel/authorroutes.js +121 -34
- package/src/panel/authorsession.js +154 -45
- package/src/panel/index.html +845 -490
- package/src/panel/jobplan.js +34 -11
- package/src/panel/runroutes.js +163 -5
- package/src/panel/scheduler.js +156 -0
- package/src/panel/schedules.js +667 -0
- package/src/panel/schedulestart.js +243 -0
- package/src/panel/server.js +467 -48
- package/src/planrun.js +42 -9
- package/src/providerrows.js +3 -3
- package/src/reviewdoor.js +37 -0
- package/src/rubricstep.js +804 -0
- package/src/runlist.js +35 -0
- package/src/schedulewords.js +296 -0
- package/src/source.js +166 -1
- package/src/trigger.js +48 -0
- package/src/triggerclock.js +127 -0
- package/src/triggerfiles.js +215 -0
- package/src/triggerledger.js +79 -0
- package/src/u-readout.js +1 -1
- package/src/userrun.js +20 -9
- package/types/authorflow.d.ts +267 -13
- package/types/authoring.d.ts +29 -1
- package/types/authorjob.d.ts +7 -5
- package/types/authorreadout.d.ts +27 -2
- package/types/authorscout.d.ts +70 -0
- package/types/declaredclose.d.ts +5 -3
- package/types/facts.d.ts +71 -0
- package/types/factwords.d.ts +42 -0
- package/types/inputlook.d.ts +238 -0
- package/types/job.d.ts +8 -1
- package/types/jobblock.d.ts +8 -1
- package/types/labelscan.d.ts +38 -0
- package/types/pagefetch.d.ts +71 -0
- package/types/panel/authorroutes.d.ts +34 -1
- package/types/panel/authorsession.d.ts +55 -14
- package/types/panel/jobplan.d.ts +12 -4
- package/types/panel/runroutes.d.ts +32 -1
- package/types/panel/scheduler.d.ts +17 -0
- package/types/panel/schedules.d.ts +412 -0
- package/types/panel/schedulestart.d.ts +106 -0
- package/types/panel/server.d.ts +145 -14
- package/types/planrun.d.ts +2 -0
- package/types/providerrows.d.ts +2 -2
- package/types/reviewdoor.d.ts +27 -0
- package/types/rubricstep.d.ts +286 -0
- package/types/runlist.d.ts +20 -0
- package/types/schedulewords.d.ts +135 -0
- package/types/source.d.ts +94 -0
- package/types/trigger.d.ts +37 -0
- package/types/triggerclock.d.ts +52 -0
- package/types/triggerfiles.d.ts +93 -0
- package/types/triggerledger.d.ts +35 -0
- package/types/userrun.d.ts +11 -0
|
@@ -27,7 +27,7 @@ import {
|
|
|
27
27
|
} from 'node:fs';
|
|
28
28
|
import { join, resolve, dirname } from 'node:path';
|
|
29
29
|
import {
|
|
30
|
-
prepareSource, proveDestination, missingDependencies, looksLikeRepoSource, nearestGitAncestor, nonRepoSourceMessage,
|
|
30
|
+
prepareSource, proveDestination, proveRubricInputs, looksLikeRubricInput, missingDependencies, looksLikeRepoSource, nearestGitAncestor, nonRepoSourceMessage,
|
|
31
31
|
} from '../source.js';
|
|
32
32
|
import {
|
|
33
33
|
worktreePath, uncommittedCount, shortHead, removeWorktree,
|
|
@@ -50,10 +50,12 @@ import {
|
|
|
50
50
|
import { resolveJobJudge, defaultJudgeLoop } from '../judged.js';
|
|
51
51
|
import { assertThinkingMode, thinkingPlanFor, thinkingRowText } from '../thinking.js';
|
|
52
52
|
import { closeJudges } from '../kinds.js';
|
|
53
|
+
import { RUBRIC_LANG } from '../authoring.js';
|
|
54
|
+
import { rubricGoal, reconcileRubricPlan } from '../rubricstep.js';
|
|
53
55
|
import { redactSecrets } from '../validate.js';
|
|
54
56
|
import { tallyCalls } from '../text.js';
|
|
55
57
|
import { writeDraftSpend, appendDraftLog } from '../draftspend.js';
|
|
56
|
-
import { PLAIN_PROPOSAL_STOPS, proposalStopText, signingStopText, draftStopText, redsRecord, calibrationSummary } from '../authorreadout.js';
|
|
58
|
+
import { PLAIN_PROPOSAL_STOPS, proposalStopText, signingStopText, draftStopText, redsRecord, declarationRecord, calibrationSummary, tryRefusedText } from '../authorreadout.js';
|
|
57
59
|
import { runNpmCi, NPM_CI_LOCKS } from '../npminstall.js';
|
|
58
60
|
import {
|
|
59
61
|
parseJobBlock, parseInputs, jobBlockFitsCheck, renderJobLines, renderExamples, signedJobLines,
|
|
@@ -75,6 +77,7 @@ export const STEP_LABELS = Object.freeze({
|
|
|
75
77
|
reuse: 'reusing signed workflow',
|
|
76
78
|
read: 'reading repo',
|
|
77
79
|
scout: 'scouting repo',
|
|
80
|
+
look: 'looking at your input',
|
|
78
81
|
listing: 'listing files',
|
|
79
82
|
confirm: 'confirming plan',
|
|
80
83
|
draft: 'drafting',
|
|
@@ -84,6 +87,16 @@ export const STEP_LABELS = Object.freeze({
|
|
|
84
87
|
signed: 'signed hash',
|
|
85
88
|
});
|
|
86
89
|
|
|
90
|
+
/**
|
|
91
|
+
* The same steps, worded for a job on the person's own local FILES (Part 1a): there is no worktree and no repo to read.
|
|
92
|
+
* Code-owned, one table beside STEP_LABELS; a step with no entry keeps its STEP_LABELS word.
|
|
93
|
+
* @type {Readonly<Record<string, string>>}
|
|
94
|
+
*/
|
|
95
|
+
export const FILE_JOB_STEP_LABELS = Object.freeze({
|
|
96
|
+
copy: 'copying input files',
|
|
97
|
+
read: 'reading input files',
|
|
98
|
+
});
|
|
99
|
+
|
|
87
100
|
/**
|
|
88
101
|
* The library's own `onPhase(name, ...)` calls (`src/authorjob.js`/`src/authorflow.js`) -> the step each one advances.
|
|
89
102
|
* A phase with no entry leaves the current step as it was.
|
|
@@ -101,6 +114,8 @@ export const PHASE_STEP = Object.freeze({
|
|
|
101
114
|
seed: 'read',
|
|
102
115
|
scout: 'scout',
|
|
103
116
|
'scout-done': 'scout',
|
|
117
|
+
look: 'look',
|
|
118
|
+
'look-done': 'look',
|
|
104
119
|
confirm: 'confirm',
|
|
105
120
|
'confirm-turn-done': 'confirm',
|
|
106
121
|
listing: 'listing',
|
|
@@ -127,12 +142,13 @@ export function latestStep(steps, id) {
|
|
|
127
142
|
* earlier line keeps its status and detail). Existing lines are never moved. Re-starting the step that is still the
|
|
128
143
|
* LAST line just resumes that line.
|
|
129
144
|
* @param {StepLine[]} steps @param {string} id @param {string} [detail] (absent = keep what the line has)
|
|
145
|
+
* @param {Readonly<Record<string, string>>} [labels] the label table (a file job passes its own)
|
|
130
146
|
*/
|
|
131
|
-
export function advanceSteps(steps, id, detail) {
|
|
147
|
+
export function advanceSteps(steps, id, detail, labels = STEP_LABELS) {
|
|
132
148
|
for (const x of steps) if (x.status === 'running' && x.id !== id) x.status = 'done';
|
|
133
149
|
const last = steps.at(-1);
|
|
134
150
|
if (last && last.id === id) { last.status = 'running'; if (detail !== undefined) last.detail = detail; return; }
|
|
135
|
-
steps.push({ id, label:
|
|
151
|
+
steps.push({ id, label: labels[id] ?? id, status: 'running', detail: detail ?? '' });
|
|
136
152
|
}
|
|
137
153
|
|
|
138
154
|
/** source-door refusals that come from CHECKING the copied source (the scan/freeze), not from reaching it */
|
|
@@ -182,6 +198,25 @@ export function jobNameTaken(name, opts = {}) {
|
|
|
182
198
|
} catch { return false; }
|
|
183
199
|
}
|
|
184
200
|
|
|
201
|
+
/**
|
|
202
|
+
* The next free name for a NEW workflow made from `origin` (RULED 2026-10-15, v1.101): `<base>-N`, N the next free number from 2.
|
|
203
|
+
* Base: when the origin ends in `-<digits>` AND the part before it is itself a job (in the run list or jobs/), that part (so `x-2`
|
|
204
|
+
* suggests `x-3`, never `x-2-2`); else the origin's own name. Free = not a run-list job and not {@link jobNameTaken}.
|
|
205
|
+
* @param {string} origin
|
|
206
|
+
* @param {Iterable<string>} runJobs job names in the panel's run list
|
|
207
|
+
* @param {{jobsDir?: string}} [opts] test seam for {@link jobNameTaken}
|
|
208
|
+
* @returns {string}
|
|
209
|
+
*/
|
|
210
|
+
export function nextFreeJobName(origin, runJobs, opts = {}) {
|
|
211
|
+
const used = new Set(runJobs);
|
|
212
|
+
const m = /^(.+)-\d+$/.exec(origin);
|
|
213
|
+
const base = m && (used.has(m[1]) || jobNameTaken(m[1], opts)) ? m[1] : origin;
|
|
214
|
+
for (let n = 2; ; n += 1) {
|
|
215
|
+
const name = `${base}-${n}`;
|
|
216
|
+
if (!used.has(name) && !jobNameTaken(name, opts)) return name;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
185
220
|
/** the form's fields, in card order — what `card.json` stores, verbatim (P5 item 3) */
|
|
186
221
|
export const CARD_FIELDS = Object.freeze(['jobName', 'checkType', 'provider', 'model', 'thinking', 'jobText', 'inputs', 'destination', 'capUsd', 'maxWallMs']);
|
|
187
222
|
|
|
@@ -209,30 +244,25 @@ export function cardFields(card) {
|
|
|
209
244
|
}
|
|
210
245
|
|
|
211
246
|
/**
|
|
212
|
-
* REUSE WORKFLOW (
|
|
213
|
-
*
|
|
214
|
-
*
|
|
215
|
-
*
|
|
247
|
+
* REUSE WORKFLOW, ONE DOOR (RULED 2026-10-15, v1.101; replaces v1.88's five open boxes). Every box on the card is open. What decides
|
|
248
|
+
* the path is the CARD compared with the origin run's card, never the drafted spec: the JOB boxes are the job text (numbered lines,
|
|
249
|
+
* `~` rules, `~ PASS`/`~ FAIL`), the check type and the thinking tick. The RUN boxes (Inputs, Destination, Model, $ cap, Time cap)
|
|
250
|
+
* never decide it. A green origin with no job box changed reuses the signed plan verbatim (no drafting); anything else drafts.
|
|
216
251
|
*/
|
|
217
|
-
export const
|
|
218
|
-
/** every card field that is NOT open on a reuse card, in card order */
|
|
219
|
-
export const REUSE_LOCKED_FIELDS = Object.freeze(CARD_FIELDS.filter((f) => !REUSE_OPEN_FIELDS.includes(f)));
|
|
220
|
-
const REUSE_LABELS = Object.freeze(/** @type {Record<string, string>} */ ({
|
|
221
|
-
jobName: 'Job name', checkType: 'Check type', jobText: 'The job',
|
|
222
|
-
}));
|
|
252
|
+
export const JOB_BOXES = Object.freeze(['jobText', 'checkType', 'thinking']);
|
|
223
253
|
|
|
224
254
|
/**
|
|
225
|
-
*
|
|
226
|
-
*
|
|
255
|
+
* Did any JOB box on `card` change from what the origin run prefilled (`origin`)? Whitespace is not compared (a one-line input drops
|
|
256
|
+
* a text's newlines); an absent thinking tick reads as off.
|
|
227
257
|
* @param {Record<string, any>} card the form as it was submitted
|
|
228
258
|
* @param {Record<string, any>} origin the form as the server prefilled it
|
|
229
|
-
* @returns {
|
|
259
|
+
* @returns {boolean}
|
|
230
260
|
*/
|
|
231
|
-
export function
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
261
|
+
export function jobBoxesChanged(card, origin) {
|
|
262
|
+
const text = (/** @type {any} */ v) => (typeof v === 'string' ? v.replace(/\s+/g, '') : '');
|
|
263
|
+
return text(card?.jobText) !== text(origin?.jobText)
|
|
264
|
+
|| (card?.checkType === 'rubric') !== (origin?.checkType === 'rubric')
|
|
265
|
+
|| (card?.thinking === true) !== (origin?.thinking === true);
|
|
236
266
|
}
|
|
237
267
|
|
|
238
268
|
/**
|
|
@@ -251,7 +281,9 @@ export function lockedFieldChanged(card, origin) {
|
|
|
251
281
|
*/
|
|
252
282
|
export function buildReuseSpec(originSpec, card, rows) {
|
|
253
283
|
const spec = JSON.parse(JSON.stringify(originSpec));
|
|
254
|
-
|
|
284
|
+
// an ABSOLUTE Destination is a per-run output folder (a plain-files job): it is never part of the signed spec, and the spec's own
|
|
285
|
+
// fence (inside the run's tree) stays as signed. A repo job's Destination is the fence and is relative (an absolute one is refused).
|
|
286
|
+
if (!isAbsoluteDestination(String(card.destination ?? '').trim())) spec.writeScope = String(card.destination ?? '').split(/[,\n]/).map((x) => x.trim()).filter(Boolean);
|
|
255
287
|
const ip = parseInputs(card.inputs);
|
|
256
288
|
if (ip.ok) spec.inputs = ip.inputs; else delete spec.inputs;
|
|
257
289
|
spec.budgetUsd = card.capUsd;
|
|
@@ -294,6 +326,27 @@ const isAbsoluteDestination = (destination) => /^(\/|[a-zA-Z]:[\\/])/.test(desti
|
|
|
294
326
|
|
|
295
327
|
export { nonRepoSourceMessage };
|
|
296
328
|
|
|
329
|
+
/** the refusal for a plain-files rubric job whose Destination is not an absolute folder (ends with the words the session start shows) */
|
|
330
|
+
export const FILES_DESTINATION_WORDS = 'Destination must be the full path of a folder, like /home/me/out — the finished files are delivered there. Nothing spent.';
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* THE ONE Destination check of a session start, at $0, before any session, copy or model call. The session's own pipeline, the HTTP
|
|
334
|
+
* start (`startSession`), the schedule's two clicks and a scheduled fire's `wouldAccept` all call THIS (never a copy of it), so a start
|
|
335
|
+
* that would be refused for its Destination is refused before anything is made.
|
|
336
|
+
* @param {{inputs?: unknown, destination?: unknown, checkType?: unknown}} card the Inputs text (line 1 = the source), the Destination and the check type
|
|
337
|
+
* @returns {string|null} the plain refusal, or null when the Destination is fine
|
|
338
|
+
*/
|
|
339
|
+
export function startDestinationProblem(card) {
|
|
340
|
+
const ip = parseInputs(card?.inputs);
|
|
341
|
+
const source = ip.ok ? (ip.inputs[0]?.value ?? '') : '';
|
|
342
|
+
const destination = typeof card?.destination === 'string' ? card.destination : '';
|
|
343
|
+
const isRepoLike = looksLikeRepoSource(source);
|
|
344
|
+
const repo = repoDestinationProblem(destination, isRepoLike);
|
|
345
|
+
if (repo !== null) return repo;
|
|
346
|
+
if (card?.checkType === 'rubric' && !isRepoLike && !isAbsoluteDestination(destination)) return FILES_DESTINATION_WORDS;
|
|
347
|
+
return null;
|
|
348
|
+
}
|
|
349
|
+
|
|
297
350
|
/**
|
|
298
351
|
* The ONE Destination rule for a REPO source (fresh card and Reuse card alike): there Destination is the write fence,
|
|
299
352
|
* relative to the repo, so an absolute path is refused — at $0, before any copy or model call. A FOLDER source (or a
|
|
@@ -311,14 +364,14 @@ export function repoDestinationProblem(destination, isRepoLike) {
|
|
|
311
364
|
* creates a session or spends anything — never inside the async pipeline,
|
|
312
365
|
* so a bad card refuses synchronously, in the same HTTP response.
|
|
313
366
|
* @param {any} card
|
|
314
|
-
* @param {{jobsDir?: string, rows?: readonly import('../providerrows.js').KeyRow[]}} [opts] `rows` = the Settings rows (the Model menu); jobsDir is a server-side test seam — NEVER read off the client card
|
|
367
|
+
* @param {{jobsDir?: string, allowName?: string, rows?: readonly import('../providerrows.js').KeyRow[]}} [opts] `rows` = the Settings rows (the Model menu); jobsDir is a server-side test seam — NEVER read off the client card; `allowName` = the origin job's own name on an unchanged red Reuse (it is the same job, not a collision)
|
|
315
368
|
* @returns {{ok: true}|{ok: false, error: string}}
|
|
316
369
|
*/
|
|
317
370
|
export function validateJobCard(card, opts = {}) {
|
|
318
371
|
if (!card || typeof card !== 'object') return { ok: false, error: 'missing job card' };
|
|
319
372
|
const { jobName, checkType, model, destination, capUsd } = card;
|
|
320
373
|
if (!isSlug(jobName)) return { ok: false, error: 'Job name must be a kebab-case slug (letters, digits, dashes)' };
|
|
321
|
-
if (jobNameTaken(jobName, { jobsDir: opts.jobsDir })) {
|
|
374
|
+
if (jobName !== opts.allowName && jobNameTaken(jobName, { jobsDir: opts.jobsDir })) {
|
|
322
375
|
return { ok: false, error: `a job named "${jobName}" already exists — P3 is new jobs only (Q4=A)` };
|
|
323
376
|
}
|
|
324
377
|
if (checkType !== 'deterministic' && checkType !== 'rubric') return { ok: false, error: 'Check type must be deterministic or rubric' };
|
|
@@ -327,10 +380,13 @@ export function validateJobCard(card, opts = {}) {
|
|
|
327
380
|
if (card.thinking !== undefined && typeof card.thinking !== 'boolean') return { ok: false, error: 'Thinking on must be on or off' };
|
|
328
381
|
const jb = parseJobBlock(card.jobText);
|
|
329
382
|
if (!jb.ok) return { ok: false, error: jb.error };
|
|
330
|
-
const misfit = jobBlockFitsCheck(jb.lines, checkType === 'rubric');
|
|
331
|
-
if (misfit) return { ok: false, error: misfit };
|
|
332
383
|
const ib = parseInputs(card.inputs);
|
|
333
384
|
if (!ib.ok) return { ok: false, error: ib.error };
|
|
385
|
+
// P1=B: PASS/FAIL examples are required of a rubric job only when it holds a judged part. A rubric on the person's own
|
|
386
|
+
// local files (first input not a repo) holds none in this build; a rubric on a repo composes the judged floor.
|
|
387
|
+
const rubricFiles = checkType === 'rubric' && looksLikeRubricInput(ib.inputs[0].value);
|
|
388
|
+
const misfit = jobBlockFitsCheck(jb.lines, checkType === 'rubric', { judged: !rubricFiles });
|
|
389
|
+
if (misfit) return { ok: false, error: misfit };
|
|
334
390
|
if (typeof destination !== 'string' || destination.trim() === '') return { ok: false, error: 'Destination is required' };
|
|
335
391
|
if (typeof capUsd !== 'number' || !Number.isFinite(capUsd) || capUsd <= 0) {
|
|
336
392
|
return { ok: false, error: '$ cap is required and must be a positive number' };
|
|
@@ -346,7 +402,7 @@ export function validateJobCard(card, opts = {}) {
|
|
|
346
402
|
* @param {any} card the validated job card (see {@link validateJobCard})
|
|
347
403
|
* @param {{env?: Record<string,string|undefined>, sessionsRoot?: string, home?: string, timeoutMs?: number,
|
|
348
404
|
* scout?: any, generate?: Function, confirmGenerate?: Function, authorFn?: Function,
|
|
349
|
-
* prepareSigningFn?: Function, npmCiFn?: Function, draftThinking?: "on"|"off", reuse?: {spec: any, workflowKey: string}}} [deps] `scout`..`prepareSigningFn` are
|
|
405
|
+
* prepareSigningFn?: Function, npmCiFn?: Function, lookFn?: Function|null, fetchUrl?: (url: string) => Promise<{ok: true, text: string, url: string}|{ok: false, error: string}>, draftThinking?: "on"|"off", reuse?: {spec: any, workflowKey: string}}} [deps] `scout`..`prepareSigningFn` are
|
|
350
406
|
* TEST SEAMS ONLY — see the note just above where each is read, below. `reuse` (Reuse workflow) is NOT a seam: the
|
|
351
407
|
* caller (`/api/author/start`) sets it only when the server's own same-job rule held.
|
|
352
408
|
* @returns {any} the session object
|
|
@@ -432,10 +488,13 @@ export function createSession(card, deps = {}) {
|
|
|
432
488
|
});
|
|
433
489
|
};
|
|
434
490
|
|
|
491
|
+
/** a rubric job on local files words its copy/read steps for files (set once the card is known) */
|
|
492
|
+
let fileJob = card.checkType === 'rubric' && looksLikeRubricInput(parseInputs(card.inputs).ok ? (/** @type {any} */ (parseInputs(card.inputs))).inputs[0].value : '');
|
|
435
493
|
/** @param {string} id @param {string} [detail] (absent = keep what the step has) start (or restart) a step; whatever step was running is finished */
|
|
436
494
|
const stepStart = (id, detail) => {
|
|
437
|
-
|
|
438
|
-
state.
|
|
495
|
+
const labels = fileJob ? { ...STEP_LABELS, ...FILE_JOB_STEP_LABELS } : STEP_LABELS;
|
|
496
|
+
advanceSteps(state.steps, id, detail, labels);
|
|
497
|
+
state.progressLabel = labels[id] ?? id;
|
|
439
498
|
logSteps();
|
|
440
499
|
};
|
|
441
500
|
/** @param {string} id @param {string} detail set a step's latest line's detail without changing its status */
|
|
@@ -500,6 +559,7 @@ export function createSession(card, deps = {}) {
|
|
|
500
559
|
return true;
|
|
501
560
|
};
|
|
502
561
|
|
|
562
|
+
let lastTryRefused = false;
|
|
503
563
|
const onPhase = (name, data = {}) => {
|
|
504
564
|
// COLLAPSED INTO THE PROGRESS INDICATOR ONLY (build item 3) — this used
|
|
505
565
|
// to also `say('system', `… ${name}`)`, one jargon chat bubble per
|
|
@@ -510,10 +570,24 @@ export function createSession(card, deps = {}) {
|
|
|
510
570
|
// person needs to read; the step-by-step detail lives in one line.
|
|
511
571
|
// ONE owner of the drafting line's detail (model + cap): the drafting line is started here, by the real author phase
|
|
512
572
|
if (PHASE_STEP[name]) stepStart(PHASE_STEP[name], PHASE_STEP[name] === 'draft' ? `${card.model}, $${card.capUsd.toFixed(2)} cap` : undefined);
|
|
513
|
-
// a
|
|
514
|
-
//
|
|
573
|
+
// WHY a try was refused: the drafting log keeps it beside the call that made it (it used to say only "retry 3 of 3"), and the
|
|
574
|
+
// progress line shows it briefly, in code's words
|
|
575
|
+
if (name === 'author-try-failed') {
|
|
576
|
+
lastTryRefused = true;
|
|
577
|
+
const no = state.steps.findLastIndex((/** @type {any} */ x) => x.id === 'draft');
|
|
578
|
+
const reason = tryRefusedText(data);
|
|
579
|
+
appendDraftLog(outDir, {
|
|
580
|
+
kind: 'try-refused', no: no === -1 ? state.steps.length - 1 : no, call: String(data.call ?? ''), reason,
|
|
581
|
+
...(Array.isArray(data.reds) && data.reds.length ? { reds: redsRecord(data.reds) } : {}), ...declarationRecord(data.declaration), at: new Date().toISOString(),
|
|
582
|
+
});
|
|
583
|
+
stepDetail('draft', `${data.call}: refused — ${reason.length > 160 ? `${reason.slice(0, 160)}…` : reason}`);
|
|
584
|
+
}
|
|
585
|
+
// a drafting call after the first (`author-call` i >= 1, of up to `of + 1`) is relabelled in plain words, so a long silence on call 2
|
|
586
|
+
// of 3 does not read as a hang. The loop drafts again after a try that PASSED too (the drafter sees its own seed-read), so
|
|
587
|
+
// "retry" is said only when the previous try was actually refused; otherwise it is "look". The same one line, never a second one.
|
|
515
588
|
if (name === 'author-call' && Number.isInteger(data.i) && data.i >= 1 && Number.isInteger(data.of)) {
|
|
516
|
-
const label = `${STEP_LABELS.draft} (retry ${data.i + 1} of ${data.of + 1})`;
|
|
589
|
+
const label = `${STEP_LABELS.draft} (${lastTryRefused ? 'retry' : 'look'} ${data.i + 1} of ${data.of + 1})`;
|
|
590
|
+
lastTryRefused = false;
|
|
517
591
|
stepLabel('draft', label);
|
|
518
592
|
state.progressLabel = label;
|
|
519
593
|
}
|
|
@@ -644,8 +718,11 @@ export function createSession(card, deps = {}) {
|
|
|
644
718
|
const verdictType = card.checkType === 'rubric' ? 'soft-green' : 'green';
|
|
645
719
|
const into = join(outDir, 'source-seed');
|
|
646
720
|
const isRepoLike = looksLikeRepoSource(SOURCE);
|
|
721
|
+
// PART 1a: a RUBRIC job whose first input is not a repo works on the person's own local FILES — every numbered input
|
|
722
|
+
// line is one file, frozen by `proveRubricInputs`; there is no repo, no language and no scout (docs/product/ITEM33-BUILD.md)
|
|
723
|
+
const RUBRIC_FILES = card.checkType === 'rubric' && !isRepoLike;
|
|
647
724
|
// hamr's ruling B (2026-10-05): ONE rule for both a fresh card and a reuse card, at $0, before the copy and any model call
|
|
648
|
-
const destProblem =
|
|
725
|
+
const destProblem = startDestinationProblem(card);
|
|
649
726
|
if (destProblem !== null) { refuse(destProblem); return; }
|
|
650
727
|
|
|
651
728
|
state.phase = 'preparing-source';
|
|
@@ -667,10 +744,17 @@ export function createSession(card, deps = {}) {
|
|
|
667
744
|
// fence read as a directory and refused as "not absolute" — hiding the real problem with the Source. A scope-shaped
|
|
668
745
|
// (relative) Destination is therefore handed to the door only when the source is repo-like.
|
|
669
746
|
const destIsDir = isAbsoluteDestination(card.destination);
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
747
|
+
/** @type {any} */
|
|
748
|
+
let prep;
|
|
749
|
+
if (RUBRIC_FILES) {
|
|
750
|
+
const proved = await proveRubricInputs({ inputs: INPUTS, into, destination: card.destination, ...(deps.fetchUrl ? { fetchUrl: deps.fetchUrl } : {}) });
|
|
751
|
+
prep = proved.ok ? proved.prep : { stop: proved.error, code: proved.code };
|
|
752
|
+
} else {
|
|
753
|
+
prep = await prepareSource({
|
|
754
|
+
source: SOURCE, into, ...(isRepoLike || destIsDir ? { destination: card.destination } : {}),
|
|
755
|
+
...(repoRoot === null ? {} : { worktree: worktreePath(repoRoot, id) }),
|
|
756
|
+
});
|
|
757
|
+
}
|
|
674
758
|
if (repoRoot !== null && prep.stop === null && prep.manifest?.worktree) madeWorktree = { repo: repoRoot, dir: prep.manifest.worktree };
|
|
675
759
|
if (abandoned) dropWorktree();
|
|
676
760
|
if (prep.stop !== null) {
|
|
@@ -682,7 +766,7 @@ export function createSession(card, deps = {}) {
|
|
|
682
766
|
stepStart('check');
|
|
683
767
|
|
|
684
768
|
const IS_REPO_SOURCE = prep.manifest.kind === 'repo';
|
|
685
|
-
let LANG = 'none-detected';
|
|
769
|
+
let LANG = RUBRIC_FILES ? RUBRIC_LANG : 'none-detected';
|
|
686
770
|
/** @type {any} */
|
|
687
771
|
let langResult = null;
|
|
688
772
|
if (IS_REPO_SOURCE) {
|
|
@@ -741,7 +825,7 @@ export function createSession(card, deps = {}) {
|
|
|
741
825
|
state.phase = 'drafting';
|
|
742
826
|
}
|
|
743
827
|
}
|
|
744
|
-
if (!IS_REPO_SOURCE) {
|
|
828
|
+
if (!IS_REPO_SOURCE && !RUBRIC_FILES) {
|
|
745
829
|
refuse(nonRepoSourceMessage(prep.manifest.kind));
|
|
746
830
|
return;
|
|
747
831
|
}
|
|
@@ -874,7 +958,9 @@ export function createSession(card, deps = {}) {
|
|
|
874
958
|
return;
|
|
875
959
|
}
|
|
876
960
|
|
|
877
|
-
|
|
961
|
+
// a rubric job on files writes only inside the hidden tree's own `output/`; its Destination is a drop-off folder
|
|
962
|
+
// (proven above, per run), never a fence
|
|
963
|
+
const writeScope = RUBRIC_FILES ? ['output/'] : card.destination.split(/[,\n]/).map((s) => s.trim()).filter(Boolean);
|
|
878
964
|
/** @type {any} the confirm turn (below) fills `goal` in once accepted —
|
|
879
965
|
* declared loosely rather than typed field-by-field, since this object's
|
|
880
966
|
* own shape is a `job-v1` draft `validateJob` itself is the real
|
|
@@ -892,7 +978,6 @@ export function createSession(card, deps = {}) {
|
|
|
892
978
|
model: modelChoice.name,
|
|
893
979
|
// the "Thinking on" tick is signed ONLY when ticked (unticked writes no field: the hash stays what it would have been)
|
|
894
980
|
...(card.thinking === true && draftThinkingPlan?.support !== false ? { thinking: true } : {}),
|
|
895
|
-
cadence: { unit: 'day', every: 1 },
|
|
896
981
|
budgetUsd: card.capUsd,
|
|
897
982
|
...(typeof card.maxWallMs === 'number' ? { maxWallMs: card.maxWallMs } : {}),
|
|
898
983
|
writeScope,
|
|
@@ -909,7 +994,8 @@ export function createSession(card, deps = {}) {
|
|
|
909
994
|
/** @type {Record<string, string>} */
|
|
910
995
|
const answers = { 1: renderJobLines(jobLines) };
|
|
911
996
|
if (verdictType === 'soft-green') answers[SOFTGREEN_JUDGE_EXAMPLES_KEY] = renderExamples(jobLines);
|
|
912
|
-
|
|
997
|
+
// a rubric job on files holds no judged part in this build, so the PASS/FAIL examples are not required of it
|
|
998
|
+
const required = RUBRIC_FILES ? [1] : requiredAnswersFor(verdictType);
|
|
913
999
|
for (const n of required) {
|
|
914
1000
|
if (!answers[n]) { refuse(`the ${verdictType} check type needs ${required.length} answers and one is missing (key ${n})`); return; }
|
|
915
1001
|
}
|
|
@@ -973,8 +1059,10 @@ export function createSession(card, deps = {}) {
|
|
|
973
1059
|
rates: draftPrice?.rates ?? null,
|
|
974
1060
|
draftThinking,
|
|
975
1061
|
onPhase, onCall,
|
|
976
|
-
ask, answerQuestions, confirmGenerate, isRepo:
|
|
1062
|
+
ask, answerQuestions, confirmGenerate, isRepo: !RUBRIC_FILES, langResult,
|
|
977
1063
|
...(deps.scout ? { scout: deps.scout } : {}),
|
|
1064
|
+
// PART 1c: a test that replaces the model boundary (`generate`) must not reach the real paid look; `deps.lookFn` brings its own
|
|
1065
|
+
...(deps.lookFn !== undefined ? { lookFn: deps.lookFn } : (deps.generate ? { lookFn: null } : {})),
|
|
978
1066
|
...(authorFnOverride ? { authorFn: authorFnOverride } : {}),
|
|
979
1067
|
});
|
|
980
1068
|
|
|
@@ -1008,13 +1096,34 @@ export function createSession(card, deps = {}) {
|
|
|
1008
1096
|
: draftStopText({
|
|
1009
1097
|
stop: String(authored.stop ?? 'authoring-failed'), reds: stopReds, refusal: authored.refusal,
|
|
1010
1098
|
spend: { knownUsd: state.draftSpentUsd, spendComplete: state.draftSpendComplete },
|
|
1099
|
+
wholeJob: RUBRIC_FILES,
|
|
1011
1100
|
}), authored.stop === 'confirm-abandoned' ? 'abandoned' : 'refused');
|
|
1012
1101
|
return;
|
|
1013
1102
|
}
|
|
1014
1103
|
|
|
1015
|
-
if (
|
|
1104
|
+
if (RUBRIC_FILES) draft.goal = redactSecrets(rubricGoal({ jobLines, files: prep.manifest.inputs, confirmedGoal: authored.confirmed?.goal ?? null, closeDecl: authored.closeDecl }));
|
|
1105
|
+
else if (authored.confirmed?.goal) draft.goal = redactSecrets(String(authored.confirmed.goal));
|
|
1016
1106
|
const spec = assembleSpec(draft, { ...authored, verdictType: /** @type {string} */ (authored.verdictType) });
|
|
1107
|
+
// PART 1a (run mv5bk400): the plan the person confirmed named three checks for line 1; the composer wrote ONE stage and put
|
|
1108
|
+
// "lists 3 releases" in its notes, so the card promised a check the close never ran. The card AFTER drafting is now built
|
|
1109
|
+
// from the close itself, and every confirmed check no part carries out is moved to Not checked, in code, before signing.
|
|
1110
|
+
if (RUBRIC_FILES) {
|
|
1111
|
+
const rec = reconcileRubricPlan({
|
|
1112
|
+
confirmed: authored.confirmed?.checkItems ?? [], notChecked: authored.confirmed?.notChecked ?? [], closeDecl: spec.closeDecl, jobLines,
|
|
1113
|
+
});
|
|
1114
|
+
state.finalPlan = { goal: String(draft.goal ?? ''), checkItems: rec.checkItems, notChecked: rec.notChecked, protections: authored.confirmed?.protections ?? [], moved: rec.moved };
|
|
1115
|
+
}
|
|
1017
1116
|
await finish(spec, authored.seedRef);
|
|
1117
|
+
if (state.finalPlan && state.phase === 'prepared') {
|
|
1118
|
+
// The confirm turn's plan bubble (and a try-3 "answers are in" one) is the DRAFTER's wording of the checks, written before
|
|
1119
|
+
// any close existed. Once the close is signed-ready its card is the only plan the person sees: the earlier ones are
|
|
1120
|
+
// dropped, so drafter prose can never sit above (and be read instead of) the code's sentences (run mv5e1ktb).
|
|
1121
|
+
state.messages = state.messages.filter((/** @type {any} */ m) => m.kind !== 'plan');
|
|
1122
|
+
state.messages.push({
|
|
1123
|
+
...planMessage({ checkItems: state.finalPlan.checkItems, checks: state.finalPlan.checkItems.map((/** @type {any} */ c) => c.text), notChecked: state.finalPlan.notChecked, protections: state.finalPlan.protections, questions: [] }, jobLines),
|
|
1124
|
+
text: 'These are the checks the signed close will run, and what it will not check. Sign & run to accept them, or Revise to edit the boxes and draft again.',
|
|
1125
|
+
});
|
|
1126
|
+
}
|
|
1018
1127
|
}
|
|
1019
1128
|
|
|
1020
1129
|
// fire-and-forget — the caller (src/panel/server.js) never awaits this; it
|