bareloop 0.29.0 → 0.31.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 +132 -0
- package/bareloop.context.md +147 -4
- package/package.json +1 -1
- package/src/authorflow.js +22 -42
- package/src/authorjob.js +5 -8
- package/src/authorrun.js +28 -12
- package/src/panel/authorroutes.js +283 -0
- package/src/panel/authorsession.js +557 -0
- package/src/panel/index.html +1233 -64
- package/src/panel/server.js +487 -40
- package/src/planrun.js +61 -5
- package/src/promptregisters.js +3 -0
- package/src/providers.js +82 -0
- package/src/replay.js +128 -4
- package/src/replayio.js +14 -3
- package/src/run.js +48 -2
- package/src/userrun.js +75 -8
- package/types/authorflow.d.ts +4 -21
- package/types/panel/authorroutes.d.ts +65 -0
- package/types/panel/authorsession.d.ts +61 -0
- package/types/panel/server.d.ts +33 -3
- package/types/providers.d.ts +34 -0
- package/types/replay.d.ts +31 -0
- package/types/replayio.d.ts +12 -1
- package/types/run.d.ts +26 -1
- package/types/userrun.d.ts +17 -0
package/src/planrun.js
CHANGED
|
@@ -3864,7 +3864,41 @@ export async function runPlan(job, { workdir, provider, nativeProvider, provider
|
|
|
3864
3864
|
blindCap: capRuns,
|
|
3865
3865
|
directions: stageDirections,
|
|
3866
3866
|
});
|
|
3867
|
-
|
|
3867
|
+
// F198: feed the STRUCTURED {stage, value} a declared close already produced
|
|
3868
|
+
// (`closeGrade`, src/declaredclose.js) rather than re-parsing `post.gap` through
|
|
3869
|
+
// `readGrade`'s `\bred\b` scan — a declared close's own gap prose never carries
|
|
3870
|
+
// that word, so the text path read every declared grade as an uncomparable
|
|
3871
|
+
// null. `post` IS `lastCloseVerdict` here (this is the opening grade, straight
|
|
3872
|
+
// off `judgeClose()`, before any crash-message substitution can occur), so
|
|
3873
|
+
// `closeGrade(post)` is exact. `readGrade` stays the fallback INSIDE
|
|
3874
|
+
// `trend.record` for a command close, where `closeGrade` returns `{gap}` only.
|
|
3875
|
+
fixTrend.record(closeGrade(post));
|
|
3876
|
+
// F198 item 2 — TRACE: before this, `middle` (below) handed the fix worker
|
|
3877
|
+
// ONLY the current gap (this iteration's raw close output, or the previous
|
|
3878
|
+
// attempt's — planrun.js's `await w.ask([...])` at the end of `middle`) and
|
|
3879
|
+
// never any history of prior grades. Nothing else about the close's own
|
|
3880
|
+
// numbers travelled between tries.
|
|
3881
|
+
//
|
|
3882
|
+
// The fix: a FACTS-ONLY number history, one entry per grade fixTrend has
|
|
3883
|
+
// already read (never advice, never model-generated text — a prompt
|
|
3884
|
+
// register change, `src/promptregisters.js`'s inventory, `src/planrun.js`
|
|
3885
|
+
// already listed there). `checksHistory` is the "N passed of M declared"
|
|
3886
|
+
// count per grade: N counts the satisfied stages on each declared verdict's
|
|
3887
|
+
// own `stages` list (first-red-wins, so that list holds only the stages that
|
|
3888
|
+
// RAN), and M is the DECLARED stage count (`stagedClose.length`, the same
|
|
3889
|
+
// one source `createTrend`'s stageOrder reads) — never the ran count, or a
|
|
3890
|
+
// 7-stage close red at stage 2 would read "1/2" instead of "1/7". Nothing a
|
|
3891
|
+
// command close's verdict carries, so it stays empty there and the line is
|
|
3892
|
+
// simply omitted. The per-stage number history rides on `fixTrend.report()`
|
|
3893
|
+
// already, so nothing new is stored for that half.
|
|
3894
|
+
/** @type {string[]} */
|
|
3895
|
+
const checksHistory = [];
|
|
3896
|
+
const pushChecks = (/** @type {any} */ v) => {
|
|
3897
|
+
if (Array.isArray(v?.stages)) {
|
|
3898
|
+
checksHistory.push(`${v.stages.filter((/** @type {any} */ s) => s.verdict === 'satisfied').length}/${stagedClose.length}`);
|
|
3899
|
+
}
|
|
3900
|
+
};
|
|
3901
|
+
pushChecks(post);
|
|
3868
3902
|
/** ralph's `ladder` seam, filled by the trend reader instead of the step
|
|
3869
3903
|
* ladder's repeat/write pair. ONE exhaustion terminal, two triggers (ralph's own
|
|
3870
3904
|
* rule): the category stays `cap-halt` and the outcome stays `escalated`, so the
|
|
@@ -3883,7 +3917,15 @@ export async function runPlan(job, { workdir, provider, nativeProvider, provider
|
|
|
3883
3917
|
if (lastCloseVerdict?.verdict === HUMAN_PAUSE) {
|
|
3884
3918
|
return { governor: 'close-trend', trend: 'unknown', reading: 'the close is waiting on a person — not a grade', iteration: o.iteration, paused: true };
|
|
3885
3919
|
}
|
|
3886
|
-
|
|
3920
|
+
// Same structured-first read as the seed above. `lastCloseVerdict` is THIS
|
|
3921
|
+
// iteration's verdict (judge() ran immediately before ralph calls this
|
|
3922
|
+
// record) UNLESS a worker-crash overwrote `gap` with a synthetic message
|
|
3923
|
+
// (src/ralph.js) that no longer matches `lastCloseVerdict.gap` — guarded by
|
|
3924
|
+
// the equality check so a crash still reads through the text fallback on
|
|
3925
|
+
// its own synthetic gap, exactly as before.
|
|
3926
|
+
const graded = lastCloseVerdict && lastCloseVerdict.gap === o.gap ? closeGrade(lastCloseVerdict) : { gap: o.gap };
|
|
3927
|
+
if (lastCloseVerdict && lastCloseVerdict.gap === o.gap) pushChecks(lastCloseVerdict);
|
|
3928
|
+
return { governor: 'close-trend', ...fixTrend.record(graded), iteration: o.iteration };
|
|
3887
3929
|
},
|
|
3888
3930
|
struckOut: fixTrend.struckOut,
|
|
3889
3931
|
report: fixTrend.report,
|
|
@@ -3897,9 +3939,12 @@ export async function runPlan(job, { workdir, provider, nativeProvider, provider
|
|
|
3897
3939
|
: `${rep.strikes}/${rep.limit} strikes — the fix loop stopped making progress against the close's own numbers (${t.reading}). Continue, change approach, or stop?`,
|
|
3898
3940
|
options: [
|
|
3899
3941
|
'revise the goal/spec so the work is reachable (a spec edit, so the new hash needs re-approval)',
|
|
3900
|
-
|
|
3901
|
-
|
|
3902
|
-
|
|
3942
|
+
// F197/mul5fofw — this terminal is ONE exhaustion terminal, and ralph.js
|
|
3943
|
+
// hardcodes its outcome as `escalated` (a GRADED RED, src/reuse.js's
|
|
3944
|
+
// `REUSE_GRADED_RED`: the close rendered a verdict and it was red).
|
|
3945
|
+
// `escalated` is a graded red, never a checkpoint (F197) — `--resume`
|
|
3946
|
+
// refuses it, so the sentence states that plainly.
|
|
3947
|
+
'this cannot be resumed — the close already rendered its verdict against the tree; revise the goal/spec and rerun fresh (a new hash needs re-approval)',
|
|
3903
3948
|
'abandon the task',
|
|
3904
3949
|
],
|
|
3905
3950
|
};
|
|
@@ -3996,12 +4041,23 @@ export async function runPlan(job, { workdir, provider, nativeProvider, provider
|
|
|
3996
4041
|
})
|
|
3997
4042
|
: null;
|
|
3998
4043
|
if (rootInj) emit('root-injected', { phase: 'fix', ...rootInj.event });
|
|
4044
|
+
// F198 item 2 — the facts-only history: JOINED numbers only, no prose the
|
|
4045
|
+
// model didn't already earn by writing the close's own output. `values`
|
|
4046
|
+
// joins with `→` (fixTrend's own vocabulary), stages join with `·`; a
|
|
4047
|
+
// stage this leg has read only once still shows its one number (no arrow),
|
|
4048
|
+
// which is still a fact the worker did not otherwise have.
|
|
4049
|
+
const checksFact = checksHistory.length > 0 ? `checks passed ${checksHistory.join(' → ')}` : null;
|
|
4050
|
+
const stageFacts = fixTrend.report().stages
|
|
4051
|
+
.filter((s) => s.values.length > 0)
|
|
4052
|
+
.map((s) => `${s.stage} ${s.values.join(' → ')}`);
|
|
4053
|
+
const factsLine = [checksFact, ...stageFacts].filter(Boolean).join(' · ');
|
|
3999
4054
|
await w.ask([
|
|
4000
4055
|
'The job\'s final verification is failing. Fix the repository so it passes.',
|
|
4001
4056
|
`Repository root (absolute): ${workdir}\nEvery path you pass to a tool MUST be absolute and inside this root.`,
|
|
4002
4057
|
artifacts.length > 0 && `Working context (read-only) — the plan's steps produced:\n${artifacts.map((a) => `[${a.id}] ${a.text}`).join('\n\n')}`,
|
|
4003
4058
|
!gap && post.gap && `The verification's output on the tree as it stands (not an attempt of yours):\n${post.gap}`,
|
|
4004
4059
|
gap && `Previous attempt failed the verification:\n${gap}`,
|
|
4060
|
+
factsLine && `The close's own numbers so far, oldest first (facts only — no advice):\n${factsLine}`,
|
|
4005
4061
|
rootInj && rootInj.note,
|
|
4006
4062
|
].filter(Boolean).join('\n\n'));
|
|
4007
4063
|
};
|
package/src/promptregisters.js
CHANGED
|
@@ -76,6 +76,9 @@ export const PROMPT_REGISTERS = Object.freeze([
|
|
|
76
76
|
|
|
77
77
|
// src/planrun.js — the native-surface read-truncation strategy line
|
|
78
78
|
Object.freeze({ file: 'src/planrun.js', name: 'NATIVE_READ_STRATEGY' }),
|
|
79
|
+
// src/planrun.js — the fix loop's facts-only close-grade history line
|
|
80
|
+
// (F198 item 2), built inline inside `middle` and appended to `w.ask([...])`
|
|
81
|
+
Object.freeze({ file: 'src/planrun.js', name: 'middle()\'s facts-only "The close\'s own numbers so far" block (inline template, not a const)' }),
|
|
79
82
|
|
|
80
83
|
// src/authoring.js — the types-genre close template, interpolated verbatim
|
|
81
84
|
// into the authoring prompt (src/authorflow.js's assembled system message,
|
package/src/providers.js
CHANGED
|
@@ -320,6 +320,88 @@ export function buildRunnerProviders({
|
|
|
320
320
|
return { provider, providerFor, judgeProvider };
|
|
321
321
|
}
|
|
322
322
|
|
|
323
|
+
/**
|
|
324
|
+
* A `providerName`'s own models-list GET endpoint — the shape this
|
|
325
|
+
* function's ONE caller (`checkProviderReachable`, below) needs and nothing
|
|
326
|
+
* else reads: `anthropic-api` and `openai-api` (the panel's Model select,
|
|
327
|
+
* PANEL-BUILD.md P3 build item 7) are the only two shapes this build knows
|
|
328
|
+
* how to ask, both a plain unauthenticated-body GET with the key in a
|
|
329
|
+
* header — never a completion/chat endpoint, so this never spends a token.
|
|
330
|
+
* @param {string} providerName
|
|
331
|
+
* @param {string} [baseUrl]
|
|
332
|
+
* @returns {{url: string, headers: Record<string,string>}|null} `null` when
|
|
333
|
+
* this provider's models-list shape is not known (never guessed at)
|
|
334
|
+
*/
|
|
335
|
+
function modelsListRequest(providerName, baseUrl) {
|
|
336
|
+
if (providerName === 'anthropic-api') {
|
|
337
|
+
return {
|
|
338
|
+
url: `${baseUrl || 'https://api.anthropic.com/v1'}/models`,
|
|
339
|
+
headers: { 'x-api-key': '', 'anthropic-version': '2023-06-01' },
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
if (providerName === 'openai-api') {
|
|
343
|
+
return { url: `${baseUrl || 'https://api.openai.com/v1'}/models`, headers: {} };
|
|
344
|
+
}
|
|
345
|
+
return null;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/**
|
|
349
|
+
* $0 reachability check: a GET against `providerName`'s own models-list
|
|
350
|
+
* endpoint, server-side, with the key in a header — NEVER a completion
|
|
351
|
+
* request, so it never spends a token (PANEL-BUILD.md P3 build item 7: "the
|
|
352
|
+
* person must get feedback BEFORE drafting if the chosen model can't
|
|
353
|
+
* work"). Reused, unmodified, by the panel's model-readiness route today
|
|
354
|
+
* (`src/panel/authorroutes.js`) and by the future Settings providers
|
|
355
|
+
* table's own "test" column (PANEL-BUILD.md P4) — ONE function, not two
|
|
356
|
+
* hand-typed copies of the same probe.
|
|
357
|
+
*
|
|
358
|
+
* NEVER returns the key, never logs it — the key only ever appears in a
|
|
359
|
+
* request header sent straight to the provider's own host.
|
|
360
|
+
* @param {object} o
|
|
361
|
+
* @param {string} o.providerName
|
|
362
|
+
* @param {string} o.apiKey
|
|
363
|
+
* @param {string} [o.model] when given, the response is also checked for this model id
|
|
364
|
+
* @param {string} [o.baseUrl]
|
|
365
|
+
* @param {typeof fetch} [o.fetchImpl] TEST SEAM — defaults to the real global `fetch`
|
|
366
|
+
* @param {number} [o.timeoutMs] defaults to 4000 — a flaky endpoint must never hang the caller
|
|
367
|
+
* @returns {Promise<{reachable: boolean, modelListed: boolean|null, status: string, note: string|null}>}
|
|
368
|
+
*/
|
|
369
|
+
export async function checkProviderReachable({
|
|
370
|
+
providerName, apiKey, model, baseUrl, fetchImpl = fetch, timeoutMs = 4000,
|
|
371
|
+
}) {
|
|
372
|
+
const req = modelsListRequest(providerName, baseUrl);
|
|
373
|
+
if (!req) {
|
|
374
|
+
return {
|
|
375
|
+
reachable: false, modelListed: null, status: 'unsupported-provider',
|
|
376
|
+
note: `no models-list check known for "${providerName}"`,
|
|
377
|
+
};
|
|
378
|
+
}
|
|
379
|
+
const headers = { ...req.headers };
|
|
380
|
+
if (providerName === 'anthropic-api') headers['x-api-key'] = apiKey;
|
|
381
|
+
else headers.Authorization = `Bearer ${apiKey}`;
|
|
382
|
+
|
|
383
|
+
const controller = new AbortController();
|
|
384
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
385
|
+
try {
|
|
386
|
+
// GET ONLY — no body, no completion/chat call, no tokens spent.
|
|
387
|
+
const res = await fetchImpl(req.url, { method: 'GET', headers, signal: controller.signal });
|
|
388
|
+
if (!res.ok) {
|
|
389
|
+
return { reachable: false, modelListed: null, status: `HTTP ${res.status}`, note: `models list returned HTTP ${res.status}` };
|
|
390
|
+
}
|
|
391
|
+
/** @type {any} */
|
|
392
|
+
let body = null;
|
|
393
|
+
try { body = await res.json(); } catch { body = null; }
|
|
394
|
+
const ids = Array.isArray(body?.data) ? body.data.map((/** @type {any} */ m) => m?.id).filter(Boolean) : [];
|
|
395
|
+
const modelListed = model ? ids.includes(model) : null;
|
|
396
|
+
return { reachable: true, modelListed, status: 'ok', note: null };
|
|
397
|
+
} catch (/** @type {any} */ e) {
|
|
398
|
+
const cls = e?.name === 'AbortError' ? 'timeout' : (e?.code || e?.name || 'network-error');
|
|
399
|
+
return { reachable: false, modelListed: null, status: String(cls), note: `request failed: ${cls}` };
|
|
400
|
+
} finally {
|
|
401
|
+
clearTimeout(timer);
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
|
|
323
405
|
/**
|
|
324
406
|
* The launch-time warning lines for an ADMITTED-PENDING-PROBE provider, or
|
|
325
407
|
* `null` when the provider has paid for its own end-to-end probe (and for an
|
package/src/replay.js
CHANGED
|
@@ -462,6 +462,24 @@ export function replayRun(spineEvents, auditEvents = [], { runId = null, auditAv
|
|
|
462
462
|
|
|
463
463
|
const spentUsd = jobEnd && typeof jobEnd.spentUsd === 'number' && Number.isFinite(jobEnd.spentUsd) ? jobEnd.spentUsd : null;
|
|
464
464
|
const spendComplete = jobEnd ? jobEnd.spendComplete === true && spentUsd !== null : false;
|
|
465
|
+
|
|
466
|
+
// draftSpentUsd (hamr's ruling 2026-09-28, "one cap covers drafting +
|
|
467
|
+
// run") — the AUTHORING pipeline's own spend on this job before it was
|
|
468
|
+
// signed, off `job-start`'s dedicated field (never `priorSpentUsd`, which
|
|
469
|
+
// is the RESUME fold `resumed` above keys on). `null` when the run never
|
|
470
|
+
// carried one (the common case, and every run predating this field) —
|
|
471
|
+
// never a decorative 0 a reader could mistake for "drafting spent
|
|
472
|
+
// nothing".
|
|
473
|
+
const draftSpentUsd = jobStart && typeof jobStart.draftSpentUsd === 'number' && Number.isFinite(jobStart.draftSpentUsd) && jobStart.draftSpentUsd > 0
|
|
474
|
+
? jobStart.draftSpentUsd : null;
|
|
475
|
+
// draftSpendComplete (hamr's ruling 2026-09-28) — was the drafting fold above
|
|
476
|
+
// EXACT? `null` when there is no drafting fold at all (the common case, and
|
|
477
|
+
// every run predating both fields) — never paired with a `draftSpentUsd` of
|
|
478
|
+
// `null`. An archive whose `job-start` carries `draftSpentUsd` but predates
|
|
479
|
+
// this companion field (src/run.js's own belt) defaults to `true` — it was
|
|
480
|
+
// exact then, the field simply didn't exist yet to say so.
|
|
481
|
+
const draftSpendComplete = draftSpentUsd === null ? null
|
|
482
|
+
: (jobStart.draftSpendComplete !== false);
|
|
465
483
|
const floorReasonList = floorReasons(spine, jobStart, spendComplete);
|
|
466
484
|
|
|
467
485
|
const wallMs = windowWallMs(parseTs(jobStart?.ts), parseTs(jobEnd?.ts));
|
|
@@ -799,8 +817,40 @@ export function replayRun(spineEvents, auditEvents = [], { runId = null, auditAv
|
|
|
799
817
|
// plan run whose close was satisfied on the first outer-close precheck
|
|
800
818
|
// never enters a fix loop, and this field must read exactly as it did
|
|
801
819
|
// before this addition for that run).
|
|
820
|
+
// F196 (2026-09-27, live-observed on mujjtrvd): a step still RUNNING (no
|
|
821
|
+
// `step-end` on the spine yet) has its own in-flight micro-loop
|
|
822
|
+
// (`ralph({judge,...})`, src/planrun.js) that emits its OWN
|
|
823
|
+
// `iteration-start` — e.g. step 2's attempt 1 on mujjtrvd (seq 71,
|
|
824
|
+
// `strict-clean-whole-src`'s own step-start at seq 69, no step-end). The
|
|
825
|
+
// old `lastStepEndSeq` (the LAST *ended* step's own boundary — seq 68,
|
|
826
|
+
// step 1 `type-checks-jsdoc`'s step-end) left that still-running step's
|
|
827
|
+
// seq range wide open, so its iteration-start leaked in below as a fake
|
|
828
|
+
// fix-loop attempt: the Run tab showed step 2 as `[done]` with 0
|
|
829
|
+
// calls/`?` (`end` never found for it in the `steps` map) plus a phantom
|
|
830
|
+
// `FIX [running]` box after it. An un-ended step is its own right
|
|
831
|
+
// boundary: the fix loop can only start once the run's LAST-STARTED step
|
|
832
|
+
// (`steps[steps.length-1]` — `steps` preserves spine order, so this is
|
|
833
|
+
// the step with the highest `stepStartSeq` across the whole file,
|
|
834
|
+
// including every concatenated leg of a resumed chain) has reached its
|
|
835
|
+
// own step-end.
|
|
836
|
+
//
|
|
837
|
+
// Deliberately checks only the LAST step, never "every step" (measured
|
|
838
|
+
// and rejected: a first pass checking `steps.some(s => s.stepEndSeq ===
|
|
839
|
+
// Infinity)` broke a genuinely FINISHED resumed-chain archive,
|
|
840
|
+
// litectx-u-bareloop/reuse-msc6w93z.jsonl — its first leg's
|
|
841
|
+
// `fix-index-js-strict` attempt (step-start seq 34) crashed with no
|
|
842
|
+
// step-end before the chain resumed and re-ran that same step from seq
|
|
843
|
+
// 77 onward to a real step-end at seq 115; the file's LAST step
|
|
844
|
+
// (`achieve-strict-typecheck`, step-end seq 611) did end, and this run's
|
|
845
|
+
// real fixLoop must still be read). Checking only the last-started step
|
|
846
|
+
// preserves that file's output unchanged while still nulling fixLoop on
|
|
847
|
+
// mujjtrvd (last step `strict-clean-whole-src` never step-ends in the
|
|
848
|
+
// mid-run slice) and on every other archived spine this fix was diffed
|
|
849
|
+
// against that turned out to be a truncated/killed file with no
|
|
850
|
+
// `job-end` at all (its last step-start likewise never step-ended) —
|
|
851
|
+
// see the F196 commit's report for the full before/after list.
|
|
802
852
|
let fixLoop = null;
|
|
803
|
-
if (timelineKind === 'steps' && stepStarts.length > 0) {
|
|
853
|
+
if (timelineKind === 'steps' && stepStarts.length > 0 && steps[steps.length - 1].stepEndSeq !== Infinity) {
|
|
804
854
|
const lastStepEndSeq = stepEnds.reduce((max, se) => (typeof se.seq === 'number' && se.seq > max ? se.seq : max), -Infinity);
|
|
805
855
|
const outerClosesAfter = spine
|
|
806
856
|
.filter((e) => e.type === 'outer-close' && typeof e.seq === 'number' && e.seq > lastStepEndSeq)
|
|
@@ -869,7 +919,21 @@ export function replayRun(spineEvents, auditEvents = [], { runId = null, auditAv
|
|
|
869
919
|
iteration: typeof is.iteration === 'number' ? is.iteration : null,
|
|
870
920
|
verdict,
|
|
871
921
|
stages: end && end.type === 'close-verdict' && Array.isArray(end.stages) ? end.stages : null,
|
|
872
|
-
|
|
922
|
+
// `end === null` means this iteration's own window hasn't closed
|
|
923
|
+
// yet (no close-verdict/run-end/escalation boundary has landed on
|
|
924
|
+
// the spine after its iteration-start) — a LIVE run reading this
|
|
925
|
+
// mid-iteration, never a finished one (a finished spine's last
|
|
926
|
+
// iteration always has a boundary). Fabricating 'red' here (as
|
|
927
|
+
// `verdict === null` used to fall through to) reads a still-running
|
|
928
|
+
// fix-loop attempt as an already-failed one; `null` (the same
|
|
929
|
+
// "unresolved" value every other in-progress outcome in this file
|
|
930
|
+
// uses) is the honest read. Fixed here, the one place this outcome
|
|
931
|
+
// is minted, rather than in the panel's `partBoxState` (which reads
|
|
932
|
+
// this field but has no ordering info to tell "genuinely red" from
|
|
933
|
+
// "still open" apart if this field itself already lies).
|
|
934
|
+
outcome: end
|
|
935
|
+
? /** @type {'green'|'red'} */ (verdict === 'satisfied' || verdict === 'green' || verdict === 'already-green' ? 'green' : 'red')
|
|
936
|
+
: /** @type {null} */ (null),
|
|
873
937
|
...buildOccurrenceMetrics(startSeq, startTs, endSeq, endTs, roundsInWindow),
|
|
874
938
|
windowStartSeq: startSeq,
|
|
875
939
|
windowEndSeq: endSeq,
|
|
@@ -1066,6 +1130,26 @@ export function replayRun(spineEvents, auditEvents = [], { runId = null, auditAv
|
|
|
1066
1130
|
if (planRoundsInitialArr.length > 0) {
|
|
1067
1131
|
const { spentUsd, unpricedRounds } = windowSpend(planRoundsInitialArr);
|
|
1068
1132
|
const b = partBehaviour(scoutTsHi, firstTs);
|
|
1133
|
+
// F: the plan part's own wall time (build item 8, 2026-09-28, run
|
|
1134
|
+
// mul5fofw: a $0.28/20623-token paid call read as "0.0s"). Root cause:
|
|
1135
|
+
// `scoutTsHi` is literally `planRoundsInitialArr[0].ts` — the plan
|
|
1136
|
+
// round's OWN completion timestamp (every round record is stamped
|
|
1137
|
+
// AFTER the call returns, never before) — so using it as plan's own
|
|
1138
|
+
// START boundary measures the call against ITSELF, leaving only the
|
|
1139
|
+
// few-ms gap to the next logged record (`firstTs`, the following
|
|
1140
|
+
// step-start) as the "duration". Measured on mul5fofw's real spine:
|
|
1141
|
+
// scout's own last round ts 11:15:00.335Z -> the plan round's ts
|
|
1142
|
+
// 11:16:22.667Z is 82.3s, the plan call's real wall time; the old
|
|
1143
|
+
// window (11:16:22.667Z -> 11:16:22.679Z, the step-start 12ms later)
|
|
1144
|
+
// could never show anything but ~0. Fixed HERE ONLY (the reader/
|
|
1145
|
+
// display side, never the seq-scoped `b`/`attempts` windows above,
|
|
1146
|
+
// which were already correctly bounded and are left untouched): the
|
|
1147
|
+
// wall window now runs from scout's own true end (its last round's
|
|
1148
|
+
// ts, or job-start when there was no scout) to plan's own true end
|
|
1149
|
+
// (its last round's ts) — never the NEXT phase's timestamps on either
|
|
1150
|
+
// side, which is what made this unmeasurable by construction.
|
|
1151
|
+
const scoutEndTs = scoutRoundsArr.length ? parseTs(scoutRoundsArr[scoutRoundsArr.length - 1].ts) : jobStartTs;
|
|
1152
|
+
const planEndTs = parseTs(planRoundsInitialArr[planRoundsInitialArr.length - 1].ts);
|
|
1069
1153
|
parts.push({
|
|
1070
1154
|
id: 'plan',
|
|
1071
1155
|
kind: 'plan',
|
|
@@ -1076,7 +1160,7 @@ export function replayRun(spineEvents, auditEvents = [], { runId = null, auditAv
|
|
|
1076
1160
|
toolCalls: b ? b.totalCalls : null,
|
|
1077
1161
|
byTool: b ? b.byTool : null,
|
|
1078
1162
|
blocked: b ? b.denied : null,
|
|
1079
|
-
wallMs: windowWallMs(
|
|
1163
|
+
wallMs: windowWallMs(scoutEndTs, planEndTs),
|
|
1080
1164
|
spentUsd,
|
|
1081
1165
|
unpricedRounds,
|
|
1082
1166
|
outcome: null,
|
|
@@ -1282,6 +1366,8 @@ export function replayRun(spineEvents, auditEvents = [], { runId = null, auditAv
|
|
|
1282
1366
|
stopReason,
|
|
1283
1367
|
spentUsd,
|
|
1284
1368
|
spendComplete,
|
|
1369
|
+
draftSpentUsd,
|
|
1370
|
+
draftSpendComplete,
|
|
1285
1371
|
floorReasons: floorReasonList,
|
|
1286
1372
|
transportRetryCount: transportRetries.length,
|
|
1287
1373
|
transportRetriesOutsideWindows,
|
|
@@ -1338,6 +1424,44 @@ function money2(n) {
|
|
|
1338
1424
|
return n === null ? 'unknown' : `$${n.toFixed(2)}`;
|
|
1339
1425
|
}
|
|
1340
1426
|
|
|
1427
|
+
/**
|
|
1428
|
+
* hamr's ruling 2026-09-28 ("one cap covers drafting + run") — the ONE
|
|
1429
|
+
* spend display for a run that may carry a drafting share, so every surface
|
|
1430
|
+
* that shows a run's money (the CLI's own `spent` line, the panel's Run tab
|
|
1431
|
+
* summary/counters, the Runs list row, the Audit header, the Job tab) reads
|
|
1432
|
+
* the same string rather than each hand-composing its own. `spendCore` is
|
|
1433
|
+
* already-formatted text (`money(...)`, or a `died`/floor line's "at least
|
|
1434
|
+
* $X") — this only decides whether to append the `(<draft> drafting)`
|
|
1435
|
+
* clause and the `of $<cap>` tail, never re-derives the money itself.
|
|
1436
|
+
* Unchanged (`spendCore` alone, or `spendCore of $cap`) when there is no
|
|
1437
|
+
* drafting share — a run without one prints exactly what it always did.
|
|
1438
|
+
*
|
|
1439
|
+
* hamr's ruling 2026-09-28 (2nd addendum) — an INCOMPLETE drafting fold
|
|
1440
|
+
* (`draftSpendComplete === false`) is a floor, not a total, and F6's rule
|
|
1441
|
+
* ("unpriced is never free, a floor never dresses as exact") applies here
|
|
1442
|
+
* exactly as it does to `spendCore` itself: the drafting clause reads `at
|
|
1443
|
+
* least $X drafting`, and because a reader summing the two figures would
|
|
1444
|
+
* otherwise take the LEADING number as exact too, `spendCore` itself also
|
|
1445
|
+
* gains the `at least ` prefix — unless it already carries one (a `died`
|
|
1446
|
+
* floor, or an already-prefixed caller), never doubled.
|
|
1447
|
+
* @param {string} spendCore already-formatted spend text (e.g. `$3.71` or `at least $2.00`)
|
|
1448
|
+
* @param {number|null} draftSpentUsd the drafting share, or null when there isn't one
|
|
1449
|
+
* @param {number|null} [budgetUsd] the signed cap, appended as `of $X.XX` when given
|
|
1450
|
+
* @param {boolean|null} [draftSpendComplete] whether the drafting fold above was EXACT;
|
|
1451
|
+
* `null`/`undefined`/`true` all read as exact (never a new enum value elsewhere) —
|
|
1452
|
+
* only an explicit `false` triggers the floor wording.
|
|
1453
|
+
* @returns {string}
|
|
1454
|
+
*/
|
|
1455
|
+
export function moneyWithDraft(spendCore, draftSpentUsd, budgetUsd, draftSpendComplete) {
|
|
1456
|
+
const hasDraft = draftSpentUsd !== null && draftSpentUsd !== undefined;
|
|
1457
|
+
const draftFloor = hasDraft && draftSpendComplete === false;
|
|
1458
|
+
const coreAlreadyFloor = /^(at least |≥)/.test(spendCore);
|
|
1459
|
+
const core = draftFloor && !coreAlreadyFloor ? `at least ${spendCore}` : spendCore;
|
|
1460
|
+
const draftPart = hasDraft ? ` (${draftFloor ? 'at least ' : ''}${money(draftSpentUsd)} drafting)` : '';
|
|
1461
|
+
const capPart = budgetUsd === undefined ? '' : ` of ${money2(budgetUsd)}`;
|
|
1462
|
+
return `${core}${draftPart}${capPart}`;
|
|
1463
|
+
}
|
|
1464
|
+
|
|
1341
1465
|
/**
|
|
1342
1466
|
* The `$`/`spend` column for one occurrence/iteration row: `unpricedRounds >
|
|
1343
1467
|
* 0` reads `unknown (N unpriced rounds)` — never `$0`, never a partial sum
|
|
@@ -1781,7 +1905,7 @@ export function summarizeForAllLine(summary) {
|
|
|
1781
1905
|
// shown on a run with zero retries.
|
|
1782
1906
|
outcome: `${summary.outcome ?? 'unknown'}${summary.transportRetryCount > 0 ? ` ⟲${summary.transportRetryCount}` : ''}`,
|
|
1783
1907
|
hadTransportRetries: summary.transportRetryCount > 0,
|
|
1784
|
-
spend: money(summary.spentUsd),
|
|
1908
|
+
spend: moneyWithDraft(money(summary.spentUsd), summary.draftSpentUsd, undefined, summary.draftSpendComplete),
|
|
1785
1909
|
wall: duration(summary.wallMs),
|
|
1786
1910
|
steps: stepsCol,
|
|
1787
1911
|
reason,
|
package/src/replayio.js
CHANGED
|
@@ -95,7 +95,8 @@ export function resolveSiblings(spinePath) {
|
|
|
95
95
|
* Read one run's spine (+ its gate-audit sidecar, when present) off disk and
|
|
96
96
|
* hand both to {@link replayRun}. Read-only, $0, mints no verdict.
|
|
97
97
|
* @param {string} spinePath absolute or cwd-relative path to a `.jsonl` spine
|
|
98
|
-
* @param {{preParsedSpine?: {records: any[], skipped: number}, skipAudit?: boolean
|
|
98
|
+
* @param {{preParsedSpine?: {records: any[], skipped: number}, skipAudit?: boolean,
|
|
99
|
+
* auditPathOverride?: string|null}} [opts]
|
|
99
100
|
* `preParsedSpine`: avoids a second parse of the same file when the caller
|
|
100
101
|
* ({@link listSpines}) already read it once to run {@link looksLikeSpine}.
|
|
101
102
|
* `skipAudit` (PR #23 review item 5, 2026-08-26, carried over): a
|
|
@@ -103,11 +104,21 @@ export function resolveSiblings(spinePath) {
|
|
|
103
104
|
* `summarizeForAllLine` reads none of the fields that sidecar feeds inside
|
|
104
105
|
* `replayRun`, so reading every run's sidecar in a directory listing was
|
|
105
106
|
* pure unused I/O, multiplied by every spine found.
|
|
107
|
+
* `auditPathOverride` (build item 7, 2026-09-28): when a caller has ALREADY
|
|
108
|
+
* resolved the real audit path through the one shared resolver
|
|
109
|
+
* (`src/panel/server.js`'s `resolveAuditPathForRow`, which also covers a
|
|
110
|
+
* still-LIVE run's during-run sidecar location — this function's own
|
|
111
|
+
* `resolveSiblings` call only ever finds the FINISHED-run convention), it
|
|
112
|
+
* passes that path here (`null` included) instead of letting this
|
|
113
|
+
* function re-derive a finished-only path on its own. `undefined` (the
|
|
114
|
+
* default) keeps the original resolveSiblings-only behaviour — every
|
|
115
|
+
* existing caller is unaffected.
|
|
106
116
|
* @returns {ReturnType<typeof replayRun>}
|
|
107
117
|
*/
|
|
108
118
|
export function replayOne(spinePath, opts = {}) {
|
|
109
|
-
const { preParsedSpine, skipAudit = false } = opts;
|
|
110
|
-
const { runId, auditPath } = resolveSiblings(spinePath);
|
|
119
|
+
const { preParsedSpine, skipAudit = false, auditPathOverride } = opts;
|
|
120
|
+
const { runId, auditPath: resolvedAuditPath } = resolveSiblings(spinePath);
|
|
121
|
+
const auditPath = auditPathOverride !== undefined ? auditPathOverride : resolvedAuditPath;
|
|
111
122
|
const spine = preParsedSpine ?? parseJsonl(spinePath);
|
|
112
123
|
const audit = (!skipAudit && auditPath) ? parseJsonl(auditPath) : { records: [], skipped: 0 };
|
|
113
124
|
// `auditAvailable`: true only when a sidecar was actually FOUND AND READ.
|
package/src/run.js
CHANGED
|
@@ -134,6 +134,29 @@ async function primitiveSmoke(workdir) {
|
|
|
134
134
|
* load gate refuses one: a refusal returns the distinct `recipe-stale` outcome with zero
|
|
135
135
|
* spend rather than silently drafting cold, because starting a paid run on a decision
|
|
136
136
|
* nobody made is the same class of error as widening a cap to manufacture a green.
|
|
137
|
+
* @param {number} [opts.draftSpentUsd=0] hamr's ruling 2026-09-28 ("one cap covers
|
|
138
|
+
* drafting + run") — money the AUTHORING pipeline (`authorCloseForJob`/
|
|
139
|
+
* `prepareSigning`, run under the SAME signed `budgetUsd` as its ceiling) already
|
|
140
|
+
* spent on THIS job before it was ever signed. The signed `budgetUsd` stays the
|
|
141
|
+
* one number in the spec hash; this shrinks the RUN's own enforced ceiling by
|
|
142
|
+
* what drafting already took, so the two phases never spend more than the one
|
|
143
|
+
* cap together. Deliberately NOT `priorSpentUsd` — that key means "a previous
|
|
144
|
+
* ATTEMPT of this same run died and folded its spend forward" and is read by
|
|
145
|
+
* `readResume`/`replay.js`'s `resumed` flag; a drafting spend is not a resume and
|
|
146
|
+
* must never make an ordinary first run of a job read as one. It is also never
|
|
147
|
+
* folded into `spentUsd` (the run's own ledger, cumulative-so-far and what a
|
|
148
|
+
* resume's `priorSpentUsd` seeds) or into `engagementSpentUsd` — drafting is a
|
|
149
|
+
* DIFFERENT phase's spend, reported beside the run's, never inside it. A garbage
|
|
150
|
+
* value (non-finite, negative) is read as 0, belted exactly like `priorSpentUsd`.
|
|
151
|
+
* @param {boolean} [opts.draftSpendComplete=true] hamr's ruling 2026-09-28 — was the
|
|
152
|
+
* `draftSpentUsd` fold above EXACT? The authoring pipeline's own metered-call list can
|
|
153
|
+
* go unpriced (F6) exactly like a resumed attempt's can, and the unknown does not heal
|
|
154
|
+
* by folding forward. A NEW field beside `draftSpentUsd`, never a new state inside
|
|
155
|
+
* `spendComplete`/`priorSpendComplete` — those answer "is THIS run's own ledger exact,"
|
|
156
|
+
* this answers "was the OTHER phase's ledger exact," and the two must never merge into
|
|
157
|
+
* one flag or a floor drafting spend could read as an exact run. Read only when
|
|
158
|
+
* `draftSpentUsd` itself is emitted; a garbage value is read as `true` (the same belt
|
|
159
|
+
* `priorSpendComplete` gets — an unreadable flag never manufactures a false floor).
|
|
137
160
|
* @param {number} [opts.priorSpentUsd=0] RESUME (module C) — money a PREVIOUS, killed
|
|
138
161
|
* attempt of this same try already spent. It seeds the ledger, so the restarted
|
|
139
162
|
* attempt runs under the REMAINDER of the signed `budgetUsd` and the terminal
|
|
@@ -242,7 +265,7 @@ async function primitiveSmoke(workdir) {
|
|
|
242
265
|
* deliberately NOT in the resumable/checkpoint set (`src/reuse.js`) — an
|
|
243
266
|
* unknown in-flight state is not a known-safe resume point.
|
|
244
267
|
*/
|
|
245
|
-
export async function runJob(rawSpec, { approvals, workdir, provider, nativeProvider, providerFor, judgeProvider = null, judgeModel = null, emit, capRuns = 3, strikeLimit, shellCapUsd = 2, closeTimeoutMs, closeDir = null, layerRoot = false, readShim = false, scout = true, bridge = null, priorSpentUsd = 0, priorSpendComplete = true, priorWallMs = 0, resumeSeed = null, resumeGrades = [], resumeReplans = null, resumeBranch = null, humanRuling = null, heldRuling = null, reviewDoor = null, doorRerun = null, resumable = true }) {
|
|
268
|
+
export async function runJob(rawSpec, { approvals, workdir, provider, nativeProvider, providerFor, judgeProvider = null, judgeModel = null, emit, capRuns = 3, strikeLimit, shellCapUsd = 2, closeTimeoutMs, closeDir = null, layerRoot = false, readShim = false, scout = true, bridge = null, draftSpentUsd = 0, draftSpendComplete = true, priorSpentUsd = 0, priorSpendComplete = true, priorWallMs = 0, resumeSeed = null, resumeGrades = [], resumeReplans = null, resumeBranch = null, humanRuling = null, heldRuling = null, reviewDoor = null, doorRerun = null, resumable = true }) {
|
|
246
269
|
// THE READ SHIM's ARM, resolved at the door — the FIRST thing this entry does,
|
|
247
270
|
// before the ledger, before the approval gate, before a byte of the spec is read.
|
|
248
271
|
// An unrecognised spelling throws here at zero cost instead of being coerced by
|
|
@@ -270,6 +293,18 @@ export async function runJob(rawSpec, { approvals, workdir, provider, nativeProv
|
|
|
270
293
|
* ENGAGEMENT's spend can be stated side by side on every terminal. Two things
|
|
271
294
|
* counted by one number will eventually be asked to disagree. */
|
|
272
295
|
const chainFoldUsd = spentUsd;
|
|
296
|
+
// hamr's ruling 2026-09-28 — the DRAFTING fold, kept apart from the RESUME
|
|
297
|
+
// fold above on purpose (see the param's own doc): it shrinks the run's
|
|
298
|
+
// enforced ceiling below but never seeds `spentUsd`/`chainFoldUsd`, so it
|
|
299
|
+
// can never be read as "a previous attempt of THIS run died" by `readResume`
|
|
300
|
+
// or by `replay.js`'s `resumed` flag (which keys on `priorSpentUsd`'s
|
|
301
|
+
// presence alone). Belted exactly like `priorSpentUsd`: a garbage value
|
|
302
|
+
// (non-finite, negative) reads as 0 rather than poisoning the ceiling
|
|
303
|
+
// arithmetic into "no cap" or "already over".
|
|
304
|
+
const draftFoldUsd = typeof draftSpentUsd === 'number' && Number.isFinite(draftSpentUsd) && draftSpentUsd > 0 ? draftSpentUsd : 0;
|
|
305
|
+
// Belted exactly like `priorSpendComplete`'s own read below: a garbage value
|
|
306
|
+
// (non-boolean) reads as `true` rather than manufacturing a false floor.
|
|
307
|
+
const draftFoldComplete = draftSpendComplete !== false;
|
|
273
308
|
let unpriced = false;
|
|
274
309
|
// RESUME (v1.46 §3): was the FOLD itself exact? `readResume` marks it false when any
|
|
275
310
|
// round inside the dead attempt came back unpriced, and that unknown does not heal by
|
|
@@ -379,6 +414,11 @@ export async function runJob(rawSpec, { approvals, workdir, provider, nativeProv
|
|
|
379
414
|
// A fold of $0 that is NOT exact is still a fold — the unknown is the whole of what
|
|
380
415
|
// it inherited, and it is the only field that can carry it forward.
|
|
381
416
|
...(spentUsd > 0 || priorFloor ? { priorSpentUsd: spentUsd, priorSpendComplete: !priorFloor } : {}),
|
|
417
|
+
// …and the DRAFTING fold, on its own dedicated field — never `priorSpentUsd`
|
|
418
|
+
// (see the param's own doc above: that key means "resumed", and this run
|
|
419
|
+
// was not). Emitted only when there is one, same "a decorative 0 is
|
|
420
|
+
// indistinguishable from none" rule every other fold field here follows.
|
|
421
|
+
...(draftFoldUsd > 0 ? { draftSpentUsd: draftFoldUsd, draftSpendComplete: draftFoldComplete } : {}),
|
|
382
422
|
// …and the TIME fold, which F103 splits in two. `priorWallMs` is what the next
|
|
383
423
|
// reader ADDS to (the bound), and it is engagement-scoped: a rerun opens a fresh
|
|
384
424
|
// engagement, so the minutes the person did not commission are not a bound on
|
|
@@ -493,7 +533,13 @@ export async function runJob(rawSpec, { approvals, workdir, provider, nativeProv
|
|
|
493
533
|
{
|
|
494
534
|
const outcome = await runPlan(job, {
|
|
495
535
|
workdir, provider, nativeProvider, providerFor, judgeProvider, judgeModel, emit: meter, capRuns, ...(strikeLimit !== undefined ? { strikeLimit } : {}), closeTimeoutMs, closeDir, layerRoot, readShim, scout, bridge, priorWallMs: chainWallMs, resumeSeed, resumeGrades, resumeReplans, resumeBranch, humanRuling, heldRuling, reviewDoor, doorRerun, priorSpentUsd: chainFoldUsd, resumable,
|
|
496
|
-
|
|
536
|
+
// hamr's ruling 2026-09-28 — ONE cap covers drafting + run: the run's
|
|
537
|
+
// enforced ceiling is the signed `budgetUsd` (unchanged, still in the
|
|
538
|
+
// spec hash) MINUS whatever drafting already spent on this job before
|
|
539
|
+
// signing. Advertised = enforced: this is the ONE place the ceiling
|
|
540
|
+
// arithmetic lives, so every readout of "what's left" is this same
|
|
541
|
+
// subtraction, never a second one computed elsewhere.
|
|
542
|
+
remainingUsd: () => Math.min(shellCapUsd, job.budgetUsd - draftFoldUsd - spentUsd),
|
|
497
543
|
isUnpriced: () => unpriced, // F6: let the plan flow bail in-flight, not just after it returns
|
|
498
544
|
spendComplete, // …and let its money-halt readout say whether the remaining it quotes is exact
|
|
499
545
|
});
|