bareloop 0.43.0 → 0.44.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 +42 -0
- package/bareloop.context.md +52 -13
- package/package.json +2 -2
- package/src/authorflow.js +26 -11
- package/src/authoring.js +1 -1
- package/src/authorjob.js +27 -15
- package/src/authorreadout.js +7 -6
- package/src/declaredclose.js +4 -1
- package/src/draftspend.js +20 -2
- package/src/facts.js +54 -9
- package/src/factwords.js +58 -6
- package/src/inputlook.js +38 -3
- package/src/job.js +1 -1
- package/src/kinds.js +5 -0
- package/src/pagedropwords.js +64 -0
- package/src/pagemap.js +264 -0
- package/src/pagerules.js +319 -0
- package/src/panel/authorroutes.js +34 -30
- package/src/panel/authorsession.js +29 -5
- package/src/panel/index.html +465 -65
- package/src/panel/jobplan.js +4 -2
- package/src/panel/runroutes.js +46 -22
- package/src/panel/server.js +791 -28
- package/src/panel/status.js +33 -0
- package/src/panelmoney.js +29 -0
- package/src/planrun.js +53 -4
- package/src/promptregisters.js +4 -0
- package/src/replay.js +118 -7
- package/src/rubricstep.js +123 -11
- package/src/runlist.js +2 -1
- package/src/schedulewords.js +6 -3
- package/src/source.js +18 -0
- package/src/thinking.js +63 -2
- package/src/userrun.js +28 -14
- package/types/authorflow.d.ts +42 -6
- package/types/authorjob.d.ts +10 -2
- package/types/declaredclose.d.ts +2 -0
- package/types/draftspend.d.ts +27 -1
- package/types/facts.d.ts +15 -0
- package/types/factwords.d.ts +26 -3
- package/types/inputlook.d.ts +15 -0
- package/types/kinds.d.ts +5 -0
- package/types/pagedropwords.d.ts +26 -0
- package/types/pagemap.d.ts +154 -0
- package/types/pagerules.d.ts +86 -0
- package/types/panel/authorroutes.d.ts +17 -14
- package/types/panel/runroutes.d.ts +2 -2
- package/types/panel/server.d.ts +155 -2
- package/types/panel/status.d.ts +51 -0
- package/types/panelmoney.d.ts +5 -0
- package/types/replay.d.ts +21 -1
- package/types/rubricstep.d.ts +57 -1
- package/types/runlist.d.ts +4 -1
- package/types/schedulewords.d.ts +4 -2
- package/types/source.d.ts +11 -0
- package/types/thinking.d.ts +39 -0
- package/types/userrun.d.ts +4 -0
package/src/panel/status.js
CHANGED
|
@@ -72,3 +72,36 @@ export function partStatusFor(part) {
|
|
|
72
72
|
}
|
|
73
73
|
return statusFor({ outcome: 'green' });
|
|
74
74
|
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* PRD v1.106 Part 5 (C, choice 9): the review door's words, ONE table. The door is the only human step bareloop has; there is no "you
|
|
78
|
+
* cancelled" (cancel is deleted). `decision` is the spine's `door-decision.decision`, or null while nothing has been decided.
|
|
79
|
+
*/
|
|
80
|
+
export const DOOR_WORDS = Object.freeze({
|
|
81
|
+
waiting: Object.freeze({ head: 'waiting for you', row: 'paused for you', last: 'none yet' }),
|
|
82
|
+
accept: Object.freeze({ head: 'you accepted', row: 'you accepted', last: 'you accepted' }),
|
|
83
|
+
rerun: Object.freeze({ head: 'you said rerun', row: 'you said rerun', last: 'you said rerun' }),
|
|
84
|
+
pause: Object.freeze({ head: 'paused', row: 'paused', last: 'paused' }),
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* The words one door wears: header phrase, the `last answer:` line (with the time `HH:MM` once decided) and the table rows.
|
|
89
|
+
* @param {{at: string|null, paths: string[], decision: string|null, text: string|null, decidedAt: string|null}} door
|
|
90
|
+
* @returns {{ head: string, last: string, rows: Array<{path: string|null, text: string, time: string|null}> }}
|
|
91
|
+
*/
|
|
92
|
+
export function doorViewOf(door) {
|
|
93
|
+
const key = door.decision === 'accept' || door.decision === 'rerun' || door.decision === 'pause' ? door.decision : 'waiting';
|
|
94
|
+
const w = DOOR_WORDS[key];
|
|
95
|
+
const path = door.paths.length === 0 ? null : door.paths.join(', ');
|
|
96
|
+
const hhmm = (/** @type {string|null} */ iso) => (iso && /T(\d\d:\d\d)/.exec(iso)?.[1]) || null;
|
|
97
|
+
const said = key === 'rerun' && door.text ? `${w.row}: "${door.text}"` : w.row;
|
|
98
|
+
/** @type {Array<{path: string|null, text: string, time: string|null}>} */
|
|
99
|
+
const rows = [{ path, text: DOOR_WORDS.waiting.row, time: door.at }];
|
|
100
|
+
if (key !== 'waiting') rows.push({ path, text: said, time: door.decidedAt });
|
|
101
|
+
const when = hhmm(door.decidedAt);
|
|
102
|
+
const last = key === 'waiting' ? w.last : `${key === 'rerun' && door.text ? said : w.last}${when ? ` · ${when}` : ''}`;
|
|
103
|
+
return { head: w.head, last, rows };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** the marker a part wears for who did its work (PRD v1.106 Part 5 C/G): the model, or a person asked */
|
|
107
|
+
export const ACTOR_MARKER = Object.freeze({ model: '[model]', human: '[human · ask]' });
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
// The panel's ONE money spelling (hamr's ruling 2026-10-19, PRD "Part 6 rulings" (3)):
|
|
2
|
+
// - an amount of $0.01 or more: 2 decimals, rounded half-up ("$0.82", "$3.71");
|
|
3
|
+
// - a REAL positive amount under a cent: 4 decimals ("$0.0054"), never "<$0.01" and never "$0.00"
|
|
4
|
+
// (one too small for 4 decimals reads "<$0.0001"; one that rounds up to a cent reads "$0.01");
|
|
5
|
+
// - exact zero: "$0.00"; null/undefined/non-finite: "unknown" (never a fabricated $0).
|
|
6
|
+
// `src/panel/index.html` carries a browser-script copy of this same function (`panelMoney`, it
|
|
7
|
+
// cannot import a module); tests/panel-page.test.js pins the two to identical output. The CLI's own
|
|
8
|
+
// 4-decimal `money()` (src/replay.js) is a different, unchanged spelling.
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* @param {number|null|undefined} n a dollar amount
|
|
12
|
+
* @returns {string}
|
|
13
|
+
*/
|
|
14
|
+
export function panelMoney2(n) {
|
|
15
|
+
if (n === null || n === undefined) return 'unknown';
|
|
16
|
+
const v = Number(n);
|
|
17
|
+
if (!Number.isFinite(v)) return 'unknown';
|
|
18
|
+
// clean to 6-decimal precision first (the precision src/text.js sums to) so a float sitting just
|
|
19
|
+
// under an exact boundary is never mis-rounded
|
|
20
|
+
const clean = Math.round(v * 1e6) / 1e6;
|
|
21
|
+
if (clean > 0 && clean < 0.01) {
|
|
22
|
+
const units = Math.round(clean * 1e4 + 1e-6);
|
|
23
|
+
if (units <= 0) return '<$0.0001';
|
|
24
|
+
if (units >= 100) return '$0.01';
|
|
25
|
+
return `$${(units / 1e4).toFixed(4)}`;
|
|
26
|
+
}
|
|
27
|
+
const cents = Math.max(0, Math.round(clean * 100 + 1e-6));
|
|
28
|
+
return `$${(cents / 100).toFixed(2)}`;
|
|
29
|
+
}
|
package/src/planrun.js
CHANGED
|
@@ -42,10 +42,11 @@ import { globToPrefix, redactSecrets, SECRET_PATTERNS } from './validate.js';
|
|
|
42
42
|
import { validateBridge, loadGate, newestEligibleVersion, reuseEligibility, quarantinesCredit, QUARANTINED_CODE } from './bridges.js';
|
|
43
43
|
import { extractArtifact, rateSourceFields } from './text.js';
|
|
44
44
|
import { defaultJudgeLoop } from './judged.js';
|
|
45
|
+
import { workerThinkingMode, thinkingCallOptions } from './thinking.js';
|
|
45
46
|
export { rateSourceFields };
|
|
46
47
|
import { createClock, isWallTimeout } from './clock.js';
|
|
47
48
|
import { isDeclaredClose, runDeclaredStages, validateCloseDecl, closeGrade, HUMAN_PAUSE, HITL_PAUSE, HITL_DECISION_RED } from './declaredclose.js';
|
|
48
|
-
import { inputLabelsOf } from './rubricstep.js';
|
|
49
|
+
import { inputLabelsOf, outputJudgedRecords } from './rubricstep.js';
|
|
49
50
|
import {
|
|
50
51
|
seedAtHead, seedListing, changedSet, GAP_TRIM_MARKER, GATE_AUDIT_FILE, ARBITER_BOOK_STORES,
|
|
51
52
|
HUMAN_KIND, HUMAN_DECISIONS, normalizeHumanRuling, resolveHumanRuling,
|
|
@@ -1665,6 +1666,8 @@ export async function runPlan(job, { workdir, provider, nativeProvider, provider
|
|
|
1665
1666
|
* `null` for a command close, which has no baseline to measure against. */
|
|
1666
1667
|
/** @type {{seedRef: string}|null} */
|
|
1667
1668
|
let declaredCtx = null;
|
|
1669
|
+
/** @type {number|null} the step try whose checks are running right now (the `rubric-evidence` record's `iteration`); null outside a step's judge */
|
|
1670
|
+
let judgingIteration = null;
|
|
1668
1671
|
/**
|
|
1669
1672
|
* ONE close-execution seam, chosen once by which field the SIGNED spec carries.
|
|
1670
1673
|
* Both executors return `runClose`'s verdict shape, so everything downstream —
|
|
@@ -1703,6 +1706,20 @@ export async function runPlan(job, { workdir, provider, nativeProvider, provider
|
|
|
1703
1706
|
// handed through as the FUNCTION (not a snapshot) so a retried locate call
|
|
1704
1707
|
// re-reads the remaining time rather than reusing the first attempt's.
|
|
1705
1708
|
callBounds,
|
|
1709
|
+
// REPORT-ONLY: what a rubric-step stage's cited checks read (bareguard `full.evidence`), recorded as `rubric-evidence` for the Result
|
|
1710
|
+
// tab's highlights. No verdict, gap, governor or money path reads it. Masked HERE with the run's one `scrub`: the `text` is a quote from
|
|
1711
|
+
// the frozen page record, and the spine is append-only. Masking moves offsets, so an entry whose text changed keeps NO spans.
|
|
1712
|
+
onRubricEvidence: (/** @type {any} */ e) => emit('rubric-evidence', {
|
|
1713
|
+
step: e.step,
|
|
1714
|
+
...(judgingIteration !== null ? { iteration: judgingIteration } : {}),
|
|
1715
|
+
evidence: e.evidence.map((/** @type {any} */ x) => {
|
|
1716
|
+
if (typeof x?.text !== 'string') return x;
|
|
1717
|
+
const text = scrub(x.text);
|
|
1718
|
+
const spans = text === x.text && Array.isArray(x.spans) ? x.spans.map((/** @type {any} */ sp) => (typeof sp?.canon === 'string' ? { ...sp, canon: scrub(sp.canon) } : sp)) : [];
|
|
1719
|
+
return { ...x, text, spans };
|
|
1720
|
+
}),
|
|
1721
|
+
...(e.evidenceTotal !== undefined ? { evidenceTotal: e.evidenceTotal } : {}),
|
|
1722
|
+
}),
|
|
1706
1723
|
onJudgeCost: (/** @type {any} */ c) => emit('judge-round', {
|
|
1707
1724
|
stage: c.stage, path: c.path, attempt: c.attempt, label: c.label, model: c.model,
|
|
1708
1725
|
costUsd: c.costUsd, unpricedRounds: c.unpricedRounds,
|
|
@@ -2773,6 +2790,27 @@ export async function runPlan(job, { workdir, provider, nativeProvider, provider
|
|
|
2773
2790
|
self = new Loop({ provider: loopProvider, system, policy, onLlmResult: metered, onToolResult: onToolOutcome, ...(rates ? { rates } : {}) });
|
|
2774
2791
|
return self;
|
|
2775
2792
|
};
|
|
2793
|
+
/**
|
|
2794
|
+
* PRD v1.106 Part 1 — what THIS call sends for thinking. The decision is src/thinking.js `workerThinkingMode` (the one owner: phase,
|
|
2795
|
+
* attempt, the job's tick); the per-call option is `thinkingCallOptions` (support-aware: a model bare-agent says cannot switch, a test
|
|
2796
|
+
* double or a clipipe is sent nothing). The decision is written to the spine BEFORE the call. A step/fix iteration that is not a whole
|
|
2797
|
+
* number throws here as a plain operator error, outside any category map. A transport/rate-limit retry inside the call reuses these
|
|
2798
|
+
* very options (the Proxy retries `generate` with the same args), so it carries the same decision.
|
|
2799
|
+
* @returns {{thinkingMode: 'on'|'off'} | {}}
|
|
2800
|
+
*/
|
|
2801
|
+
const thinkingForCall = () => {
|
|
2802
|
+
const mode = workerThinkingMode({ phase, iteration: roundIteration, tick: job.thinking === true });
|
|
2803
|
+
emit('thinking-decision', { phase, iteration: roundIteration ?? null, mode });
|
|
2804
|
+
thinkingSent = mode;
|
|
2805
|
+
return thinkingCallOptions(workerProvider ?? provider, mode);
|
|
2806
|
+
};
|
|
2807
|
+
/** bare-agent's own per-call verdict (applied | unsupported | ignored | unknown), recorded as drafting rounds record it; absent when no switch was sent
|
|
2808
|
+
* @param {any} r a Loop.run() result */
|
|
2809
|
+
const noteThinkingStatus = (r) => {
|
|
2810
|
+
if (typeof r?.thinkingStatus === 'string') emit('thinking-status', { phase, iteration: roundIteration ?? null, mode: thinkingSent, thinkingStatus: r.thinkingStatus });
|
|
2811
|
+
};
|
|
2812
|
+
/** @type {'on'|'off'|null} */
|
|
2813
|
+
let thinkingSent = null;
|
|
2776
2814
|
/** @param {string} prompt @param {typeof toolDefs} [defs] */
|
|
2777
2815
|
/**
|
|
2778
2816
|
* F59 — continue an EXISTING conversation for exactly one TOOLLESS round.
|
|
@@ -2784,6 +2822,7 @@ export async function runPlan(job, { workdir, provider, nativeProvider, provider
|
|
|
2784
2822
|
* @param {string} prompt
|
|
2785
2823
|
*/
|
|
2786
2824
|
const askFrom = async (msgs, prompt) => {
|
|
2825
|
+
const thinkOpts = thinkingForCall();
|
|
2787
2826
|
try {
|
|
2788
2827
|
// `msgs` is a prior `loop.run()`'s returned transcript, which already has
|
|
2789
2828
|
// the Loop's `system` PREPENDED (bare-agent Loop.run, loop.js ~482-486).
|
|
@@ -2795,8 +2834,9 @@ export async function runPlan(job, { workdir, provider, nativeProvider, provider
|
|
|
2795
2834
|
// bare-agent's own Loop.chat() (loop.js ~1321) strips a leading system
|
|
2796
2835
|
// message before re-running for exactly this reason; do the same here.
|
|
2797
2836
|
const rest = msgs[0]?.role === 'system' ? msgs.slice(1) : msgs;
|
|
2798
|
-
const r = await stallWatch.watch((gen) => newLoop(gen).run([...rest, { role: 'user', content: prompt }], [], { cacheMessages: true, maxTokens: 32000, ...callBounds() }));
|
|
2837
|
+
const r = await stallWatch.watch((gen) => newLoop(gen).run([...rest, { role: 'user', content: prompt }], [], { cacheMessages: true, maxTokens: 32000, ...callBounds(), ...thinkOpts }));
|
|
2799
2838
|
throwIfStopped();
|
|
2839
|
+
noteThinkingStatus(r);
|
|
2800
2840
|
return r;
|
|
2801
2841
|
} catch (e) {
|
|
2802
2842
|
throw categorize(e).err;
|
|
@@ -2804,9 +2844,11 @@ export async function runPlan(job, { workdir, provider, nativeProvider, provider
|
|
|
2804
2844
|
};
|
|
2805
2845
|
const ask = async (prompt, defs = toolDefs) => {
|
|
2806
2846
|
let r;
|
|
2847
|
+
const thinkOpts = thinkingForCall();
|
|
2807
2848
|
try {
|
|
2808
|
-
r = await stallWatch.watch((gen) => newLoop(gen).run([{ role: 'user', content: prompt }], defs, { cacheMessages: true, maxTokens: 32000, ...callBounds() }));
|
|
2849
|
+
r = await stallWatch.watch((gen) => newLoop(gen).run([{ role: 'user', content: prompt }], defs, { cacheMessages: true, maxTokens: 32000, ...callBounds(), ...thinkOpts }));
|
|
2809
2850
|
throwIfStopped();
|
|
2851
|
+
noteThinkingStatus(r);
|
|
2810
2852
|
} catch (e) {
|
|
2811
2853
|
throw categorize(e).err;
|
|
2812
2854
|
}
|
|
@@ -3399,7 +3441,11 @@ export async function runPlan(job, { workdir, provider, nativeProvider, provider
|
|
|
3399
3441
|
};
|
|
3400
3442
|
let lastGap = '';
|
|
3401
3443
|
const judge = async () => {
|
|
3402
|
-
|
|
3444
|
+
judgingIteration = iterationNow;
|
|
3445
|
+
/** @type {any} */
|
|
3446
|
+
let evaled;
|
|
3447
|
+
try { evaled = await evalExits(step.exit, { dir: workdir, snapshot, runCheck }); } finally { judgingIteration = null; }
|
|
3448
|
+
const { pass, results: raw } = evaled;
|
|
3403
3449
|
// Scrubbed HERE, once, before the results are read by anyone. An exit detail is
|
|
3404
3450
|
// supposed to be names and counts, but `json-valid` embeds `JSON.parse`'s own
|
|
3405
3451
|
// message and V8 quotes a window of the SOURCE inside it (a body of 20 chars or
|
|
@@ -3414,6 +3460,9 @@ export async function runPlan(job, { workdir, provider, nativeProvider, provider
|
|
|
3414
3460
|
// results makes the leak inexpressible; scrubbing each consumer makes it
|
|
3415
3461
|
// something the next consumer has to remember.
|
|
3416
3462
|
const results = raw.map((r) => (typeof r.detail === 'string' ? { ...r, detail: scrub(r.detail) } : r));
|
|
3463
|
+
// PRD v1.106 Part 5: what the check just read (path, size, sha256 — never the content), recorded BEFORE the exit-eval because the next try
|
|
3464
|
+
// overwrites the file. The worker is idle between its turn and this judge, so these are the bytes the check read.
|
|
3465
|
+
for (const o of await outputJudgedRecords(job.closeDecl, workdir, step.exit)) emit('output-judged', { step: step.id, iteration: iterationNow, ...o });
|
|
3417
3466
|
emit('exit-eval', { step: step.id, iteration: iterationNow, results });
|
|
3418
3467
|
// an instrument fault rides out by its runClose verdict NAME: ralph
|
|
3419
3468
|
// escalates it through CLOSE_FAULTS (or F32-routes a crash after writes)
|
package/src/promptregisters.js
CHANGED
|
@@ -61,6 +61,10 @@ export const PROMPT_REGISTERS = Object.freeze([
|
|
|
61
61
|
Object.freeze({ file: 'src/cardauthor.js', name: 'COMPILE_SYSTEM' }),
|
|
62
62
|
Object.freeze({ file: 'src/cardauthor.js', name: 'cardCasesPrompt() (inline template, not a const)' }),
|
|
63
63
|
|
|
64
|
+
// src/pagemap.js - the page-word map call (idea A core): the AI names the page's own words for the person's job
|
|
65
|
+
Object.freeze({ file: 'src/pagemap.js', name: 'MAP_SYSTEM' }),
|
|
66
|
+
Object.freeze({ file: 'src/pagemap.js', name: 'mapPrompt() (inline template, not a const)' }),
|
|
67
|
+
|
|
64
68
|
// src/readshim.js — the capped/deduplicating read shim's worker-facing strategy
|
|
65
69
|
Object.freeze({ file: 'src/readshim.js', name: 'READ_SHIM_STRATEGY' }),
|
|
66
70
|
Object.freeze({ file: 'src/readshim.js', name: 'READ_SHIM_DIFF_STRATEGY' }),
|
package/src/replay.js
CHANGED
|
@@ -35,6 +35,8 @@
|
|
|
35
35
|
// `plan` / `fix` for the other worker phases — src/planrun.js:2735,2515,3359.
|
|
36
36
|
// `costUsd` is `arg?.costUsd ?? null` — a null is an HONEST unpriced round
|
|
37
37
|
// (F6), never summed as $0 by this module.
|
|
38
|
+
// rubric-evidence {step, iteration?, evidence, evidenceTotal?} REPORT-ONLY: what a rubric step's cited checks read (bareguard
|
|
39
|
+
// `full.evidence`); read by the Result tab only, by no verdict, governor or money path. src/planrun.js onRubricEvidence
|
|
38
40
|
// exit-eval {step, iteration, results: [{type, pass, detail?}]}
|
|
39
41
|
// src/planrun.js:2897
|
|
40
42
|
// each `results[i].type` is one of plan.js's EXIT_TYPES; `check-passes`
|
|
@@ -105,6 +107,7 @@ import { runBehaviour, formatBehaviour } from './behaviour.js';
|
|
|
105
107
|
import { shortSha } from './codeversion.js';
|
|
106
108
|
import { SPEND_RECORD_TYPES, chainSpend } from './ledger.js';
|
|
107
109
|
import { legsOf, capKindOf } from './legs.js';
|
|
110
|
+
import { thinkingWordsFrom } from './thinking.js';
|
|
108
111
|
|
|
109
112
|
/**
|
|
110
113
|
* @param {unknown} e
|
|
@@ -586,6 +589,12 @@ export function replayRun(spineEvents, auditEvents = [], { runId = null, auditAv
|
|
|
586
589
|
// re-spelled so the two can never drift apart.
|
|
587
590
|
const spendRecords = spine.filter((e) => SPEND_RECORD_TYPES.includes(e.type));
|
|
588
591
|
const exitEvals = spine.filter((e) => e.type === 'exit-eval');
|
|
592
|
+
// PRD v1.106 Part 5 (D1): which check each `check-passes` result was — the `check-run` record just before its exit-eval
|
|
593
|
+
const checkRuns = spine.filter((e) => e.type === 'check-run');
|
|
594
|
+
// the file each try's check read (new runs only; an old spine has none, and then no judged-file line is shown)
|
|
595
|
+
const outputJudgedRecs = spine.filter((e) => e.type === 'output-judged');
|
|
596
|
+
// D2: the iterations that began (a try with an iteration-start and no exit-eval was cut off before its checks ran)
|
|
597
|
+
const iterationStarts = spine.filter((e) => e.type === 'iteration-start');
|
|
589
598
|
|
|
590
599
|
// F119: every `transport-retry` record on this spine, and the (derived,
|
|
591
600
|
// never guessed) reason(s) behind a `spendComplete:false` job-end — see
|
|
@@ -800,7 +809,7 @@ export function replayRun(spineEvents, auditEvents = [], { runId = null, auditAv
|
|
|
800
809
|
// `/api/runs/:runid/rounds` endpoint (`src/panel/server.js`) to slice
|
|
801
810
|
// this attempt's own rounds/tool-calls out of the full spine+gate-audit,
|
|
802
811
|
// never recomputed a second way there either.
|
|
803
|
-
/** @type {Array<{n: number, iteration: number|null, outcome: 'green'|'red', startSeq: number, endSeq: number, startTs: number|null, endTs: number|null, exitEvalDetail: string|null}>} */
|
|
812
|
+
/** @type {Array<{n: number, iteration: number|null, outcome: 'green'|'red', startSeq: number, endSeq: number, startTs: number|null, endTs: number|null, exitEvalDetail: string|null, checks: Array<{type: string, pass: boolean, detail: string|null, name: string|null}>, outputJudged: Array<{path: string|null, bytes: number|null, sha256: string|null}>}>} */
|
|
804
813
|
const attemptWindows = [];
|
|
805
814
|
const stepExitEvals = exitEvals
|
|
806
815
|
.filter((ee) => ee.step === id && typeof ee.seq === 'number' && ee.seq > startSeq && ee.seq < endSeq)
|
|
@@ -827,6 +836,27 @@ export function replayRun(spineEvents, auditEvents = [], { runId = null, auditAv
|
|
|
827
836
|
iteration: typeof ee.iteration === 'number' ? ee.iteration : null,
|
|
828
837
|
outcome: allPass ? 'green' : 'red',
|
|
829
838
|
});
|
|
839
|
+
// the try's checks AS THEY RAN (D1): each exit-eval result in order; a `check-passes` result is named by the `check-run`
|
|
840
|
+
// record just before this exit-eval (same window), taken in order. `detail` is already scrubbed at capture.
|
|
841
|
+
const lo = prevBoundSeq;
|
|
842
|
+
const namesHere = checkRuns
|
|
843
|
+
.filter((c) => typeof c.seq === 'number' && c.seq > lo && c.seq < eeSeq && typeof c.name === 'string')
|
|
844
|
+
.sort((a, b) => a.seq - b.seq);
|
|
845
|
+
let nameAt = 0;
|
|
846
|
+
const tryChecks = results.map((r) => {
|
|
847
|
+
const type = typeof r.type === 'string' ? r.type : 'unknown';
|
|
848
|
+
const name = type === 'check-passes' ? (namesHere[nameAt++]?.name ?? null) : null;
|
|
849
|
+
return {
|
|
850
|
+
type, pass: r.pass === true, detail: typeof r.detail === 'string' ? r.detail : null, name,
|
|
851
|
+
};
|
|
852
|
+
});
|
|
853
|
+
const judgedHere = outputJudgedRecs
|
|
854
|
+
.filter((o) => o.step === id && (typeof ee.iteration !== 'number' || o.iteration === ee.iteration) && typeof o.seq === 'number' && o.seq > lo && o.seq < eeSeq)
|
|
855
|
+
.map((o) => ({
|
|
856
|
+
path: typeof o.path === 'string' ? o.path : null,
|
|
857
|
+
bytes: typeof o.bytes === 'number' ? o.bytes : null,
|
|
858
|
+
sha256: typeof o.sha256 === 'string' ? o.sha256 : null,
|
|
859
|
+
}));
|
|
830
860
|
attemptWindows.push({
|
|
831
861
|
n: attemptWindows.length + 1,
|
|
832
862
|
iteration: typeof ee.iteration === 'number' ? ee.iteration : null,
|
|
@@ -836,11 +866,30 @@ export function replayRun(spineEvents, auditEvents = [], { runId = null, auditAv
|
|
|
836
866
|
startTs: prevBoundTs,
|
|
837
867
|
endTs: eeTs,
|
|
838
868
|
exitEvalDetail: detailSource && typeof detailSource.detail === 'string' ? detailSource.detail : null,
|
|
869
|
+
checks: tryChecks,
|
|
870
|
+
outputJudged: judgedHere,
|
|
839
871
|
});
|
|
840
872
|
prevBoundSeq = eeSeq;
|
|
841
873
|
prevBoundTs = eeTs ?? prevBoundTs;
|
|
842
874
|
}
|
|
843
875
|
|
|
876
|
+
// D2: a try that BEGAN (an `iteration-start` after the last exit-eval) in a step that has ended, and never reached its checks, was cut off
|
|
877
|
+
// (the wall, a cap, a stop). A step still running has an in-flight try, not a cut-off one. Never a numbered attempt: it ran no check.
|
|
878
|
+
const endedHere = end !== null || stopReason !== null;
|
|
879
|
+
const lastIter = iterationStarts
|
|
880
|
+
.filter((it) => typeof it.seq === 'number' && it.seq > prevBoundSeq && it.seq < endSeq)
|
|
881
|
+
.sort((a, b) => b.seq - a.seq)[0] ?? null;
|
|
882
|
+
const cutOffTry = endedHere && lastIter
|
|
883
|
+
? {
|
|
884
|
+
n: attemptWindows.length + 1,
|
|
885
|
+
iteration: typeof lastIter.iteration === 'number' ? lastIter.iteration : null,
|
|
886
|
+
startSeq: prevBoundSeq,
|
|
887
|
+
endSeq: end && typeof end.seq === 'number' ? end.seq : (Number.isFinite(endSeq) ? endSeq : prevBoundSeq),
|
|
888
|
+
startTs: prevBoundTs,
|
|
889
|
+
endTs,
|
|
890
|
+
}
|
|
891
|
+
: null;
|
|
892
|
+
|
|
844
893
|
idOccurrence.set(id, (idOccurrence.get(id) ?? 0) + 1);
|
|
845
894
|
// the SAME step continuing across a leg boundary (the previous occurrence of this id was cut off by its leg's
|
|
846
895
|
// halt, and this one starts in a later leg) is "continued", never a retry; `tryNumber` counts real retries only
|
|
@@ -859,6 +908,7 @@ export function replayRun(spineEvents, auditEvents = [], { runId = null, auditAv
|
|
|
859
908
|
treeChanged,
|
|
860
909
|
attempts,
|
|
861
910
|
attemptWindows,
|
|
911
|
+
cutOffTry,
|
|
862
912
|
stepStartSeq: startSeq,
|
|
863
913
|
stepEndSeq: endSeq,
|
|
864
914
|
// `stepStartTs`/`stepEndTs` (panel item 2 follow-up, F195/F-audit-step):
|
|
@@ -877,7 +927,6 @@ export function replayRun(spineEvents, auditEvents = [], { runId = null, auditAv
|
|
|
877
927
|
// `phase` filter on the rounds: the seq window between one iteration-start
|
|
878
928
|
// and the next close-verdict/run-end/escalation already isolates that
|
|
879
929
|
// iteration's own rounds.
|
|
880
|
-
const iterationStarts = spine.filter((e) => e.type === 'iteration-start');
|
|
881
930
|
const timelineKind = /** @type {'steps'|'iterations'} */ (stepStarts.length > 0 ? 'steps' : (iterationStarts.length > 0 ? 'iterations' : 'steps'));
|
|
882
931
|
const boundaries = spine.filter((e) => e.type === 'close-verdict' || e.type === 'run-end' || e.type === 'escalation');
|
|
883
932
|
|
|
@@ -1320,7 +1369,7 @@ export function replayRun(spineEvents, auditEvents = [], { runId = null, auditAv
|
|
|
1320
1369
|
// same as a scout/plan/replan/judge/run part already does.
|
|
1321
1370
|
const stepAttempts = step.attemptWindows.length > 0
|
|
1322
1371
|
? step.attemptWindows.map((aw) => ({
|
|
1323
|
-
n: aw.n, outcome: aw.outcome, verdict: null, stages: null, detail: aw.exitEvalDetail, startSeq: aw.startSeq, endSeq: aw.endSeq, startTs: aw.startTs, endTs: aw.endTs,
|
|
1372
|
+
n: aw.n, outcome: aw.outcome, verdict: null, stages: null, detail: aw.exitEvalDetail, startSeq: aw.startSeq, endSeq: aw.endSeq, startTs: aw.startTs, endTs: aw.endTs, iteration: aw.iteration, checks: aw.checks, outputJudged: aw.outputJudged,
|
|
1324
1373
|
}))
|
|
1325
1374
|
: wholePartAttempt(step.stepStartSeq, step.stepStartTs, step.stepEndSeq, step.stepEndTs);
|
|
1326
1375
|
parts.push({
|
|
@@ -1340,6 +1389,7 @@ export function replayRun(spineEvents, auditEvents = [], { runId = null, auditAv
|
|
|
1340
1389
|
// P5-R (resumed runs only): why its leg's halt cut this step off, whether it is the same step continuing
|
|
1341
1390
|
// from the leg before, and its retry count not counting continuations
|
|
1342
1391
|
...(step.stopReason === undefined ? {} : { stopReason: step.stopReason, continued: step.continued, tryNumber: step.tryNumber }),
|
|
1392
|
+
...(step.cutOffTry ? { cutOffTry: step.cutOffTry } : {}),
|
|
1343
1393
|
attempts: step.stopReason ? stepAttempts.map((a) => (a.outcome === null || a.outcome === undefined ? { ...a, outcome: 'stopped' } : a)) : stepAttempts,
|
|
1344
1394
|
});
|
|
1345
1395
|
}
|
|
@@ -1420,6 +1470,63 @@ export function replayRun(spineEvents, auditEvents = [], { runId = null, auditAv
|
|
|
1420
1470
|
}
|
|
1421
1471
|
}
|
|
1422
1472
|
|
|
1473
|
+
// PRD v1.106 Part 5 (C, choice 9): a run that reached its review door gets one more part, the only human step bareloop has. `door` carries what
|
|
1474
|
+
// the spine recorded (when it opened, which files, what the person decided and when); the panel server words it (src/panel/status.js).
|
|
1475
|
+
const doorRec = spine.findLast((e) => e.type === 'review-door');
|
|
1476
|
+
if (doorRec) {
|
|
1477
|
+
const decided = spine.findLast((e) => e.type === 'door-decision' && e.honoured === true && typeof e.seq === 'number' && typeof doorRec.seq === 'number' && e.seq > doorRec.seq);
|
|
1478
|
+
const paths = Array.isArray(doorRec.changed?.paths) ? doorRec.changed.paths.filter((/** @type {any} */ x) => typeof x === 'string') : [];
|
|
1479
|
+
parts.push({
|
|
1480
|
+
id: 'door',
|
|
1481
|
+
kind: 'door',
|
|
1482
|
+
label: 'door',
|
|
1483
|
+
number: null,
|
|
1484
|
+
occurrence: null,
|
|
1485
|
+
rounds: 0,
|
|
1486
|
+
toolCalls: 0,
|
|
1487
|
+
byTool: null,
|
|
1488
|
+
blocked: 0,
|
|
1489
|
+
wallMs: null,
|
|
1490
|
+
spentUsd: 0,
|
|
1491
|
+
unpricedRounds: 0,
|
|
1492
|
+
outcome: null,
|
|
1493
|
+
attempts: [],
|
|
1494
|
+
door: {
|
|
1495
|
+
at: typeof doorRec.ts === 'string' ? doorRec.ts : null,
|
|
1496
|
+
paths,
|
|
1497
|
+
decision: decided && typeof decided.decision === 'string' ? decided.decision : null,
|
|
1498
|
+
text: decided && typeof decided.text === 'string' ? decided.text : null,
|
|
1499
|
+
decidedAt: decided && typeof decided.ts === 'string' ? decided.ts : null,
|
|
1500
|
+
},
|
|
1501
|
+
});
|
|
1502
|
+
}
|
|
1503
|
+
|
|
1504
|
+
// PRD v1.106 Part 3: the thinking each call was SENT, from the spine's own `thinking-decision` / `thinking-status` records, worded by
|
|
1505
|
+
// src/thinking.js. A part with one attempt (plan, scout, replan, a single-attempt step) carries it as `part.thinking`; an attempt that made a
|
|
1506
|
+
// worker call carries `attempt.thinking`. A spine with no such record gets no field at all (an older run shows no thinking word).
|
|
1507
|
+
if (spine.some((e) => e.type === 'thinking-decision')) {
|
|
1508
|
+
const firstStepSeq = stepStarts.reduce((m, ss) => (typeof ss.seq === 'number' && ss.seq < m ? ss.seq : m), Infinity);
|
|
1509
|
+
for (const p of parts) {
|
|
1510
|
+
const atts = Array.isArray(p.attempts) ? p.attempts : [];
|
|
1511
|
+
for (const a of atts) {
|
|
1512
|
+
let words = null;
|
|
1513
|
+
if (p.kind === 'step') words = thinkingWordsFrom(spine, { phase: `step:${p.label}`, lo: a.startSeq, hi: a.endSeq });
|
|
1514
|
+
else if (p.kind === 'fix') words = thinkingWordsFrom(spine, { phase: 'fix', lo: a.startSeq, hi: a.endSeq });
|
|
1515
|
+
if (words !== null) a.thinking = words;
|
|
1516
|
+
}
|
|
1517
|
+
if (p.kind === 'step' && p.cutOffTry) {
|
|
1518
|
+
const words = thinkingWordsFrom(spine, { phase: `step:${p.label}`, lo: p.cutOffTry.startSeq, hi: p.cutOffTry.endSeq });
|
|
1519
|
+
if (words !== null) p.cutOffTry.thinking = words;
|
|
1520
|
+
}
|
|
1521
|
+
let part = null;
|
|
1522
|
+
if (p.kind === 'scout') part = thinkingWordsFrom(spine, { phase: 'scout', hi: firstStepSeq });
|
|
1523
|
+
else if (p.kind === 'plan') part = thinkingWordsFrom(spine, { phase: 'plan', hi: firstStepSeq });
|
|
1524
|
+
else if (p.kind === 'replan' && atts[0]) part = thinkingWordsFrom(spine, { phase: 'plan', lo: atts[0].startSeq, hi: atts[0].endSeq });
|
|
1525
|
+
else if ((p.kind === 'step' || p.kind === 'fix') && atts.length === 1) part = atts[0].thinking ?? null;
|
|
1526
|
+
if (part !== null) p.thinking = part;
|
|
1527
|
+
}
|
|
1528
|
+
}
|
|
1529
|
+
|
|
1423
1530
|
// P5-R: which LEG each part belongs to, and where a resume picked up — only for a resumed run (a spine with
|
|
1424
1531
|
// no marker keeps its parts byte-for-byte). A step part is dated by its own step-start; a replan by its
|
|
1425
1532
|
// marker; every other part by the end of its last attempt (its own window's far edge), falling back to the
|
|
@@ -1587,16 +1694,20 @@ function money2(n) {
|
|
|
1587
1694
|
* @param {boolean|null} [draftSpendComplete] whether the drafting fold above was EXACT;
|
|
1588
1695
|
* `null`/`undefined`/`true` all read as exact (never a new enum value elsewhere) —
|
|
1589
1696
|
* only an explicit `false` triggers the floor wording.
|
|
1697
|
+
* @param {number|null} [runUsd] the run's own spend as a number (excluding drafting), so the sum can be written
|
|
1590
1698
|
* @returns {string}
|
|
1591
1699
|
*/
|
|
1592
|
-
export function moneyWithDraft(spendCore, draftSpentUsd, budgetUsd, draftSpendComplete) {
|
|
1700
|
+
export function moneyWithDraft(spendCore, draftSpentUsd, budgetUsd, draftSpendComplete, runUsd) {
|
|
1593
1701
|
const hasDraft = draftSpentUsd !== null && draftSpentUsd !== undefined;
|
|
1594
1702
|
const draftFloor = hasDraft && draftSpendComplete === false;
|
|
1595
1703
|
const coreAlreadyFloor = /^(at least |≥)/.test(spendCore);
|
|
1596
1704
|
const core = draftFloor && !coreAlreadyFloor ? `at least ${spendCore}` : spendCore;
|
|
1597
|
-
const draftPart = hasDraft ? ` (${draftFloor ? 'at least ' : ''}${money(draftSpentUsd)} drafting)` : '';
|
|
1598
1705
|
const capPart = budgetUsd === undefined ? '' : ` of ${money2(budgetUsd)}`;
|
|
1599
|
-
return `${core}${
|
|
1706
|
+
if (!hasDraft) return `${core}${capPart}`;
|
|
1707
|
+
// PRD v1.106 Part 5 Q4: the run's figure EXCLUDES drafting, so the line says so and adds the sum — "$0.0054 run + $0.0035 drafting =
|
|
1708
|
+
// $0.0089 of $1.00". A floor on either part makes the total a floor too ("at least"). Without the run's number the sum is left out.
|
|
1709
|
+
const total = typeof runUsd === 'number' && Number.isFinite(runUsd) ? ` = ${draftFloor || coreAlreadyFloor ? 'at least ' : ''}${money(runUsd + draftSpentUsd)}` : '';
|
|
1710
|
+
return `${core} run + ${draftFloor ? 'at least ' : ''}${money(draftSpentUsd)} drafting${total}${capPart}`;
|
|
1600
1711
|
}
|
|
1601
1712
|
|
|
1602
1713
|
/**
|
|
@@ -2045,7 +2156,7 @@ export function summarizeForAllLine(summary) {
|
|
|
2045
2156
|
// shown on a run with zero retries.
|
|
2046
2157
|
outcome: `${summary.outcome ?? 'unknown'}${summary.transportRetryCount > 0 ? ` ⟲${summary.transportRetryCount}` : ''}`,
|
|
2047
2158
|
hadTransportRetries: summary.transportRetryCount > 0,
|
|
2048
|
-
spend: moneyWithDraft(money(summary.spentUsd), summary.draftSpentUsd, undefined, summary.draftSpendComplete),
|
|
2159
|
+
spend: moneyWithDraft(money(summary.spentUsd), summary.draftSpentUsd, undefined, summary.draftSpendComplete, summary.spentUsd),
|
|
2049
2160
|
wall: duration(summary.wallMs),
|
|
2050
2161
|
steps: stepsCol,
|
|
2051
2162
|
reason,
|