bareloop 0.10.0 → 0.11.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 +351 -0
- package/README.md +17 -7
- package/bareloop.context.md +179 -16
- package/package.json +3 -3
- package/src/authorflow.js +114 -16
- package/src/authoring.js +100 -33
- package/src/authorjob.js +50 -6
- package/src/authorscout.js +13 -1
- package/src/bridges.js +1 -1
- package/src/clock.js +21 -3
- package/src/declaredclose.js +88 -11
- package/src/index.js +8 -1
- package/src/job.js +7 -2
- package/src/kinds.js +190 -9
- package/src/ledger.js +24 -1
- package/src/planrun.js +195 -5
- package/src/ralph.js +9 -0
- package/src/reuse.js +195 -7
- package/src/root.js +1 -1
- package/src/run.js +10 -4
- package/src/selection.js +1 -1
- package/types/authorflow.d.ts +86 -12
- package/types/authoring.d.ts +49 -35
- package/types/authorjob.d.ts +9 -1
- package/types/authorscout.d.ts +7 -1
- package/types/clock.d.ts +5 -2
- package/types/declaredclose.d.ts +64 -6
- package/types/index.d.ts +4 -4
- package/types/job.d.ts +5 -0
- package/types/kinds.d.ts +76 -4
- package/types/planrun.d.ts +18 -2
- package/types/reuse.d.ts +74 -0
- package/types/run.d.ts +12 -2
package/src/reuse.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// Layer 3 — the D7 ENVELOPE and the REUSE RUNNER (design record
|
|
2
|
-
// docs/
|
|
2
|
+
// docs/02-features/2026-08-01-layer-3-reuse-design.md, D3/D5/D6/D7 + R1).
|
|
3
3
|
//
|
|
4
4
|
// hamr's sentence is the whole specification of this file, verbatim:
|
|
5
5
|
//
|
|
@@ -52,6 +52,12 @@ import { closeStagesOf } from './plan.js';
|
|
|
52
52
|
import { MIN_WALL_MS, jobSpecHash } from './job.js';
|
|
53
53
|
import { isObj, isNonEmptyString } from './validate.js';
|
|
54
54
|
import { readGrade } from './trend.js';
|
|
55
|
+
|
|
56
|
+
/** the paused-run TERMINAL and its spine record share one name (`runPlan` returns
|
|
57
|
+
* it and `runJob` writes it onto `job-end`), spelled once in `declaredclose.js`
|
|
58
|
+
* beside the close-verdict word it translates, so the emitters, the checkpoint
|
|
59
|
+
* list, and the TTL gate below cannot drift from each other */
|
|
60
|
+
import { HITL_PAUSE, HUMAN_CHECKPOINTS } from './declaredclose.js';
|
|
55
61
|
import { extractArtifact, priceOf } from './text.js';
|
|
56
62
|
import { runJob as shippedRunJob } from './run.js';
|
|
57
63
|
|
|
@@ -84,6 +90,34 @@ const { Loop } = require('bare-agent');
|
|
|
84
90
|
*/
|
|
85
91
|
export const REUSE_GRADED_RED = Object.freeze(['escalated']);
|
|
86
92
|
|
|
93
|
+
/**
|
|
94
|
+
* WHICH TERMINALS ARE A CHECKPOINT — the list a runner hands `readResume` as
|
|
95
|
+
* `resumableOutcomes`, minted HERE rather than in whichever script happens to
|
|
96
|
+
* launch a run.
|
|
97
|
+
*
|
|
98
|
+
* `readResume` deliberately takes it as a parameter and defaults to empty, and
|
|
99
|
+
* that stays true: the reuse loop's own semantics depend on the empty default.
|
|
100
|
+
* What did not exist was a canonical ANSWER, so `scripts/run-u.mjs` kept its own
|
|
101
|
+
* copy — and the exported bundle (PRD v1.44 §2: a thin runner with bareloop as a
|
|
102
|
+
* dependency) would have had to keep a third. The same reasoning that put the
|
|
103
|
+
* pause TTL in the library (OPEN-2, hamr, 2026-08-13) puts this here.
|
|
104
|
+
*
|
|
105
|
+
* Every entry is a stop that left WORK ON DISK and an allowance unspent — money,
|
|
106
|
+
* time, a self-healed stall, and now a person who has not answered yet. A verdict
|
|
107
|
+
* already rendered is never on this list: `green`/`escalated` are graded rows, and
|
|
108
|
+
* re-buying one would pay twice for an answer already in hand. (The signer's own
|
|
109
|
+
* `hitl-cancel` terminal used to be the other example; the door was deleted on
|
|
110
|
+
* 2026-08-17 — a person who does not want to carry on pauses, and the checkpoint
|
|
111
|
+
* they keep is retired by the TTL rather than by a terminal.)
|
|
112
|
+
*
|
|
113
|
+
* The human half is SPLICED IN from `declaredclose.js` rather than spelled again,
|
|
114
|
+
* because the try loop below reads the two halves differently and a second copy is
|
|
115
|
+
* how they would come to disagree about which member is which: the first three are
|
|
116
|
+
* allowances the loop's remaining tries are meant to spend against, and the human
|
|
117
|
+
* ones are the stop no further try can change (see `hardStop`).
|
|
118
|
+
*/
|
|
119
|
+
export const CHECKPOINT_OUTCOMES = Object.freeze(['cap-halt', 'wall-halt', 'step-stalled', ...HUMAN_CHECKPOINTS]);
|
|
120
|
+
|
|
87
121
|
/**
|
|
88
122
|
* v1's definition of "the same KIND of recipe" — and it is the LOAD GATE's own: the
|
|
89
123
|
* close-stage names, in the same order (`bridges.loadGate` rule 2). One predicate,
|
|
@@ -418,13 +452,24 @@ function readTry(events) {
|
|
|
418
452
|
};
|
|
419
453
|
}
|
|
420
454
|
|
|
421
|
-
/** the
|
|
422
|
-
*
|
|
423
|
-
* try loop and the resume reader classify through this — two
|
|
424
|
-
* of answer was that" is how a casualty ends up demoting a
|
|
425
|
-
*
|
|
455
|
+
/** the verdict read of a runJob outcome, in ONE place: a green, the one GRADED red
|
|
456
|
+
* that demotes (D6), a CHECKPOINT waiting on a person, or a casualty that keeps its
|
|
457
|
+
* own name. Both the live try loop and the resume reader classify through this — two
|
|
458
|
+
* spellings of "what kind of answer was that" is how a casualty ends up demoting a
|
|
459
|
+
* recipe.
|
|
460
|
+
*
|
|
461
|
+
* `checkpoint` is its own name rather than a fourth flavour of casualty (2026-08-17).
|
|
462
|
+
* A casualty is a run that DIED — a transport fault, a governance cut, a broken close
|
|
463
|
+
* — and a reader that sees one is being told the attempt is over. A human checkpoint
|
|
464
|
+
* is the opposite fact in every respect: nothing failed, nothing is discarded, the
|
|
465
|
+
* work and the money are exactly where they were, and the allowance still unspent is
|
|
466
|
+
* a person's answer. It behaves like neither a red nor a casualty everywhere it
|
|
467
|
+
* matters already (`hardStop` ends the loop on it, the box is left untouched), so the
|
|
468
|
+
* ROW saying "casualty" was the one place the machinery still mis-described itself.
|
|
469
|
+
* @param {string} outcome @returns {'green'|'red'|'checkpoint'|'casualty'} */
|
|
426
470
|
function verdictClassOf(outcome) {
|
|
427
471
|
if (outcome === 'green' || outcome === 'already-green') return 'green';
|
|
472
|
+
if (HUMAN_CHECKPOINTS.includes(outcome)) return 'checkpoint';
|
|
428
473
|
return REUSE_GRADED_RED.includes(outcome) ? 'red' : 'casualty';
|
|
429
474
|
}
|
|
430
475
|
|
|
@@ -1023,6 +1068,81 @@ export function readResume(events, { deathAt = null, direct = false, resumableOu
|
|
|
1023
1068
|
};
|
|
1024
1069
|
}
|
|
1025
1070
|
|
|
1071
|
+
/**
|
|
1072
|
+
* THE PAUSE TTL (2026-08-12 §2): a hitl checkpoint is kept for 60 days.
|
|
1073
|
+
*
|
|
1074
|
+
* OPEN-2, RULED (hamr, in-turn, 2026-08-13): **the TTL lives in the LIBRARY**, so
|
|
1075
|
+
* the exported bundle (a headless runner with bareloop as a dependency, PRD v1.44
|
|
1076
|
+
* §2) inherits it instead of every runner re-implementing it. A script-only TTL
|
|
1077
|
+
* would vanish on export — the one place the rule is most needed and least
|
|
1078
|
+
* likely to be re-typed.
|
|
1079
|
+
*/
|
|
1080
|
+
export const PAUSE_TTL_MS = 60 * 24 * 60 * 60_000;
|
|
1081
|
+
|
|
1082
|
+
/**
|
|
1083
|
+
* The AGE gate for a paused checkpoint — the sibling of `resumeTreeGate`, and it
|
|
1084
|
+
* asks the other question a 60-day-old checkpoint raises. The tree gate asks
|
|
1085
|
+
* *is this still the tree the run left?*; this asks *is this still a decision
|
|
1086
|
+
* anyone is waiting on?*
|
|
1087
|
+
*
|
|
1088
|
+
* THREE things it deliberately does NOT do:
|
|
1089
|
+
*
|
|
1090
|
+
* - it does not widen any other resume rule. The 2026-08-12 ruling is about a
|
|
1091
|
+
* person deciding, so `applies: false` on every other terminal: a cap-halt
|
|
1092
|
+
* checkpoint is a different stop under a different allowance, and quietly
|
|
1093
|
+
* ageing it out would be a governance change nobody ruled.
|
|
1094
|
+
* - it does not treat an unreadable stamp as young (F6). A checkpoint that
|
|
1095
|
+
* cannot say WHEN it paused has an unknown age, and unknown is refused rather
|
|
1096
|
+
* than rounded down to fresh — the direction that fails safe.
|
|
1097
|
+
* - it does not skip silently. An expired checkpoint comes back as a refusal
|
|
1098
|
+
* that NAMES the age and the TTL, because the operator's next move (start
|
|
1099
|
+
* fresh) depends on knowing which of the two it hit.
|
|
1100
|
+
*
|
|
1101
|
+
* The clock is injected for the same reason `runPlan`'s is: a rule measured in
|
|
1102
|
+
* months cannot otherwise be tested at all.
|
|
1103
|
+
*
|
|
1104
|
+
* @param {any[]} events the paused run's own spine, parsed, in file order
|
|
1105
|
+
* @param {{now?: () => number, ttlMs?: number}} [opts]
|
|
1106
|
+
* @returns {{ok: boolean, applies: boolean, ageMs: number|null, ttlMs: number, pausedAt: string|null, detail: string|null}}
|
|
1107
|
+
*/
|
|
1108
|
+
export function checkpointAgeGate(events, { now = Date.now, ttlMs = PAUSE_TTL_MS } = {}) {
|
|
1109
|
+
const list = Array.isArray(events) ? events.filter(isObj) : [];
|
|
1110
|
+
const end = list.filter((e) => e.type === 'job-end').at(-1) ?? null;
|
|
1111
|
+
const base = { applies: false, ageMs: null, ttlMs, pausedAt: null, detail: null };
|
|
1112
|
+
if (end?.outcome !== HITL_PAUSE) return { ...base, ok: true };
|
|
1113
|
+
|
|
1114
|
+
// the PAUSE RECORD's own timestamp, and its terminal's as the fallback: the
|
|
1115
|
+
// pause is what the human is answering, so its stamp is when the waiting began
|
|
1116
|
+
const pause = list.filter((e) => e.type === HITL_PAUSE).at(-1) ?? end;
|
|
1117
|
+
const pausedAt = typeof pause.ts === 'string' ? pause.ts : null;
|
|
1118
|
+
const atMs = pausedAt === null ? NaN : Date.parse(pausedAt);
|
|
1119
|
+
if (!Number.isFinite(atMs)) {
|
|
1120
|
+
return {
|
|
1121
|
+
...base,
|
|
1122
|
+
ok: false,
|
|
1123
|
+
applies: true,
|
|
1124
|
+
detail: 'this checkpoint does not record when it paused, so its age is unknown — and an unknown age is not a '
|
|
1125
|
+
+ `young one (F6). The TTL is ${Math.round(ttlMs / 86_400_000)} days; start a fresh run rather than resuming a `
|
|
1126
|
+
+ 'checkpoint nothing can date.',
|
|
1127
|
+
};
|
|
1128
|
+
}
|
|
1129
|
+
const ageMs = Math.max(0, now() - atMs);
|
|
1130
|
+
const days = (/** @type {number} */ ms) => Math.floor(ms / 86_400_000);
|
|
1131
|
+
if (ageMs > ttlMs) {
|
|
1132
|
+
return {
|
|
1133
|
+
...base,
|
|
1134
|
+
ok: false,
|
|
1135
|
+
applies: true,
|
|
1136
|
+
ageMs,
|
|
1137
|
+
pausedAt,
|
|
1138
|
+
detail: `this run paused ${days(ageMs)} days ago and the pause TTL is ${days(ttlMs)} days — the tree, the `
|
|
1139
|
+
+ 'toolchain and the job itself have had two months to move, so the evidence the signer would be answering '
|
|
1140
|
+
+ 'about is no longer the evidence the run produced. Start a fresh run.',
|
|
1141
|
+
};
|
|
1142
|
+
}
|
|
1143
|
+
return { ...base, ok: true, applies: true, ageMs, pausedAt };
|
|
1144
|
+
}
|
|
1145
|
+
|
|
1026
1146
|
/**
|
|
1027
1147
|
* The PATIENT gate for a resume, which is deliberately NOT the fresh-launch one.
|
|
1028
1148
|
*
|
|
@@ -1375,7 +1495,30 @@ export async function runReuse(opts) {
|
|
|
1375
1495
|
costUsd: read.spentUsd, spendComplete: read.spendComplete, wallMs, rounds,
|
|
1376
1496
|
...(trySpecHash ? { specHash: trySpecHash } : {}),
|
|
1377
1497
|
};
|
|
1378
|
-
if (
|
|
1498
|
+
if (outcome === 'already-green') {
|
|
1499
|
+
// A GREEN THAT PREDATES THE RUN mints nothing, and the guard keys on the
|
|
1500
|
+
// TERMINAL rather than on the absence of a plan. `already-green` means the
|
|
1501
|
+
// close was satisfied at the precheck — before this leg wrote a byte — so
|
|
1502
|
+
// whatever plan is on the spine did not produce it, and minting a version
|
|
1503
|
+
// from one would be learning credit for work that did not happen.
|
|
1504
|
+
//
|
|
1505
|
+
// It used to be covered by accident: a cold already-green never drafts, so
|
|
1506
|
+
// `read.plan` was null and the branch below caught it. N4 broke that
|
|
1507
|
+
// accident — a hitl try that PAUSED and came back with the signer's
|
|
1508
|
+
// `accept` lands `already-green` with its predecessor's `plan-accepted`
|
|
1509
|
+
// sitting in the same try window. A rule with no wired detector is prose.
|
|
1510
|
+
bridgeWrites.push({
|
|
1511
|
+
name,
|
|
1512
|
+
action: 'none',
|
|
1513
|
+
file: null,
|
|
1514
|
+
reds: [{
|
|
1515
|
+
code: 'green-predates-run',
|
|
1516
|
+
path: 'outcome',
|
|
1517
|
+
detail: 'the close was already satisfied before this run did anything (already-green) — nothing here earned '
|
|
1518
|
+
+ 'a version, and a plan on the spine did not produce this green',
|
|
1519
|
+
}],
|
|
1520
|
+
});
|
|
1521
|
+
} else if (verdictClass === 'green' && read.plan === null) {
|
|
1379
1522
|
// a green with no plan on the spine cannot mint a version — the artifact that
|
|
1380
1523
|
// inherits is the one that RAN, and there is nothing here to inherit. Reported,
|
|
1381
1524
|
// never faked with a placeholder.
|
|
@@ -1389,6 +1532,26 @@ export async function runReuse(opts) {
|
|
|
1389
1532
|
toolsUsed: [...new Set((read.plan.steps ?? []).flatMap((/** @type {any} */ s) => s.tools ?? []))],
|
|
1390
1533
|
};
|
|
1391
1534
|
row.bridgeWrite = writeGreen(bridge, meta, { ...record, plan: read.plan });
|
|
1535
|
+
} else if (HUMAN_CHECKPOINTS.includes(outcome)) {
|
|
1536
|
+
// A PAUSE IS NOT A RESULT, so the box gets nothing — not a casualty row, not a red,
|
|
1537
|
+
// and the workflow is left exactly as it was. The recipe has not been judged: the
|
|
1538
|
+
// close is not FINISHED rendering its verdict, and the answer that would finish it
|
|
1539
|
+
// is a person's. Filing it as history would put a row on the entry that reads as a
|
|
1540
|
+
// run this workflow failed to carry, and the signer's `accept` moments later would
|
|
1541
|
+
// leave that row standing as evidence against a workflow that in fact greened.
|
|
1542
|
+
// Recorded as an explicit no-write for the already-green branch's reason: a reader
|
|
1543
|
+
// never has to branch on absence.
|
|
1544
|
+
bridgeWrites.push({
|
|
1545
|
+
name,
|
|
1546
|
+
action: 'none',
|
|
1547
|
+
file: null,
|
|
1548
|
+
reds: [{
|
|
1549
|
+
code: 'run-paused-for-decision',
|
|
1550
|
+
path: 'outcome',
|
|
1551
|
+
detail: `the run is waiting on a person (${outcome}) — nothing has been judged yet, so there is no result to `
|
|
1552
|
+
+ 'file against this workflow. The row is written by whatever the resumed run finally earns.',
|
|
1553
|
+
}],
|
|
1554
|
+
});
|
|
1392
1555
|
} else if (bridge) {
|
|
1393
1556
|
// R1: the recipe is NOT edited by a red. `outcome` rides in verbatim — a casualty
|
|
1394
1557
|
// keeps its own name, and only the literal 'red' demotes (D6).
|
|
@@ -1495,6 +1658,31 @@ export async function runReuse(opts) {
|
|
|
1495
1658
|
* @param {any} row @returns {any|null} the terminal, or null to carry on
|
|
1496
1659
|
*/
|
|
1497
1660
|
const hardStop = (row) => {
|
|
1661
|
+
// THE HUMAN HALF, read before the table and off the LIBRARY's own list: a run
|
|
1662
|
+
// waiting on a person is not one of the stops below, which are properties of the
|
|
1663
|
+
// run that would reproduce for $0. This one is the opposite — a try that did real
|
|
1664
|
+
// work and reached the one stage a machine cannot render. Buying the next try (and
|
|
1665
|
+
// then the cold leg) would spend the envelope re-asking a question already put to
|
|
1666
|
+
// someone who has not replied yet, and the second attempt could not be graded any
|
|
1667
|
+
// sooner than the first. The allowance still unspent is a person, so the loop stops
|
|
1668
|
+
// and hands the run back; the checkpoint on disk is untouched and resumable, which
|
|
1669
|
+
// is the runner script's half (N4 §1.6, PAUSE_TTL_MS).
|
|
1670
|
+
if (HUMAN_CHECKPOINTS.includes(row.runOutcome)) {
|
|
1671
|
+
return done(row.runOutcome, {
|
|
1672
|
+
category: row.runOutcome,
|
|
1673
|
+
decision: 'The run reached a stage only a person can render, and it is waiting on that answer. Every further '
|
|
1674
|
+
+ 'try and the cold leg would run the same job to the same question, so the loop stops here rather than '
|
|
1675
|
+
+ 'buying attempts nobody can grade yet. Nothing is lost: the run kept everything it did, and it resumes '
|
|
1676
|
+
+ 'from this checkpoint once the decision is in.',
|
|
1677
|
+
options: [
|
|
1678
|
+
'read the evidence package the pause recorded, then answer it and resume this run',
|
|
1679
|
+
'leave it: an unanswered checkpoint expires on its own once the pause TTL is up',
|
|
1680
|
+
],
|
|
1681
|
+
// NOT re-filed, for `interpreter-red`'s reason: the pause already escalated from
|
|
1682
|
+
// its own site inside the run, under this same name and with the evidence package
|
|
1683
|
+
// attached. A second copy here would count one person's pending decision twice.
|
|
1684
|
+
}, false);
|
|
1685
|
+
}
|
|
1498
1686
|
const decisions = {
|
|
1499
1687
|
'unapproved-spec': {
|
|
1500
1688
|
decision: `No approval record matches the per-try spec (hash ${trySpecHash}). Tightening the caps with an envelope makes a NEW spec version, and a new version is signed, not inherited — nothing was run.`,
|
package/src/root.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// Layer R — the root: the within-run ratchet (LAYERS.md item 2; design record
|
|
2
|
-
// docs/
|
|
2
|
+
// docs/02-features/2026-07-19-layer-r-design.md). One state that survives attempts
|
|
3
3
|
// inside ONE run so a never-green run stops repeating itself (F21). Role is
|
|
4
4
|
// CONTINUITY ONLY — F39 measured that hand-delivered state buys no conversion;
|
|
5
5
|
// the semantic converter is Layer 2's job, not this module's.
|
package/src/run.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// smoke, the ONE budget ledger (cap-not-estimate; unpriced is never free, F6),
|
|
4
4
|
// and the decision-ready stops. It composes the plan flow below it and
|
|
5
5
|
// interprets nothing itself.
|
|
6
|
-
// Design record: docs/
|
|
6
|
+
// Design record: docs/02-features/2026-07-12-n2-headless-loop-design.md.
|
|
7
7
|
//
|
|
8
8
|
// The legacy operator-authored `steps[]` path was DELETED 2026-07-26 (PRD
|
|
9
9
|
// v1.32): the drafting call, config-v1, the per-step interpret loops and the
|
|
@@ -140,12 +140,18 @@ async function primitiveSmoke(workdir) {
|
|
|
140
140
|
* a manufactured-fixation probe). `true` is the ON/experimental arm; the
|
|
141
141
|
* default is the OFF arm. (Shell-owned seam, same doctrine as the provider
|
|
142
142
|
* binding.)
|
|
143
|
+
* @param {{decision: string, text?: string}|null} [opts.humanRuling] N4 — the SIGNER's
|
|
144
|
+
* answer at a hitl pause (accept | rerun <text> | pause), carried by the leg that
|
|
145
|
+
* RESUMES a paused run and forwarded verbatim to the plan flow. Absent on every
|
|
146
|
+
* ordinary run; never authored and never defaulted (a defaulted answer to "is this
|
|
147
|
+
* done?" is the rubber-stamp the class exists to prevent, 2026-08-12 §4).
|
|
143
148
|
* @returns {Promise<string>} outcome: 'green' | 'already-green' | 'escalated' |
|
|
144
149
|
* 'unapproved-spec' | 'job-red' | 'smoke-red' | 'plan-red' | 'check-red' |
|
|
145
150
|
* 'close-red' | 'close-unsupported' | 'recipe-stale' | 'branch-red' | 'pricing-red' | 'provider-red' |
|
|
146
|
-
* 'interpreter-red' | 'cap-halt' | 'wall-halt' | 'step-stalled' |
|
|
151
|
+
* 'interpreter-red' | 'cap-halt' | 'wall-halt' | 'step-stalled' |
|
|
152
|
+
* 'hitl-pause' | 'hitl-decision-red' | `step-red:<id>`
|
|
147
153
|
*/
|
|
148
|
-
export async function runJob(rawSpec, { approvals, workdir, provider, nativeProvider, providerFor, emit, capRuns = 3, strikeLimit, shellCapUsd = 2, closeTimeoutMs, layerRoot = false, bridge = null, priorSpentUsd = 0, priorSpendComplete = true, priorWallMs = 0, resumeSeed = null, resumeGrades = [], resumeReplans = null, resumeBranch = null }) {
|
|
154
|
+
export async function runJob(rawSpec, { approvals, workdir, provider, nativeProvider, providerFor, emit, capRuns = 3, strikeLimit, shellCapUsd = 2, closeTimeoutMs, layerRoot = false, bridge = null, priorSpentUsd = 0, priorSpendComplete = true, priorWallMs = 0, resumeSeed = null, resumeGrades = [], resumeReplans = null, resumeBranch = null, humanRuling = null }) {
|
|
149
155
|
// 0. the ledger's counters, declared FIRST so that every job-end — including
|
|
150
156
|
// the pre-token reds below — can state a real figure. An omitted `spentUsd` is
|
|
151
157
|
// not a zero: a consumer reads `undefined` and either crashes or launders it
|
|
@@ -283,7 +289,7 @@ export async function runJob(rawSpec, { approvals, workdir, provider, nativeProv
|
|
|
283
289
|
// accounts it natively (F12) and the job-end money contract is unchanged.
|
|
284
290
|
{
|
|
285
291
|
const outcome = await runPlan(job, {
|
|
286
|
-
workdir, provider, nativeProvider, providerFor, emit: meter, capRuns, ...(strikeLimit !== undefined ? { strikeLimit } : {}), closeTimeoutMs, layerRoot, bridge, priorWallMs, resumeSeed, resumeGrades, resumeReplans, resumeBranch,
|
|
292
|
+
workdir, provider, nativeProvider, providerFor, emit: meter, capRuns, ...(strikeLimit !== undefined ? { strikeLimit } : {}), closeTimeoutMs, layerRoot, bridge, priorWallMs, resumeSeed, resumeGrades, resumeReplans, resumeBranch, humanRuling,
|
|
287
293
|
remainingUsd: () => Math.min(shellCapUsd, job.budgetUsd - spentUsd),
|
|
288
294
|
isUnpriced: () => unpriced, // F6: let the plan flow bail in-flight, not just after it returns
|
|
289
295
|
spendComplete, // …and let its money-halt readout say whether the remaining it quotes is exact
|
package/src/selection.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// Layer 3 — D3's DISPLAY half: the workflow LISTING, and the prompt that asks for a
|
|
2
|
-
// pick (design record docs/
|
|
2
|
+
// pick (design record docs/02-features/2026-08-01-layer-3-reuse-design.md, D3).
|
|
3
3
|
//
|
|
4
4
|
// hamr's ruling, verbatim: *"i think there should be a listing for workflows names that
|
|
5
5
|
// user can choose or auto-choose, or do we want llm to always decide (we still need to
|
package/types/authorflow.d.ts
CHANGED
|
@@ -346,9 +346,25 @@ export function resolveSourcePrefixes({ workdir, sourcePaths, seedFiles }: {
|
|
|
346
346
|
* run could not be read afterwards. A cost entry with no raw beside it is that
|
|
347
347
|
* run again.
|
|
348
348
|
*/
|
|
349
|
-
/**
|
|
350
|
-
|
|
349
|
+
/**
|
|
350
|
+
* @param {{ceilingUsd?: number|null,
|
|
351
|
+
* onCall?: (call: {label: string, costUsd: number|null, unpricedRounds: number}) => void}} [o]
|
|
352
|
+
* `ceilingUsd` is the OPERATOR's ceiling; `null`/absent is UNBOUNDED.
|
|
353
|
+
* `onCall` is a REPORTING seam fired as each metered call lands — the shell's
|
|
354
|
+
* only chance to know what has been spent while the run is still alive. It is
|
|
355
|
+
* never consulted and never governs: `capStop` below is still the one
|
|
356
|
+
* predicate, still asked between calls, and still reads the same `entries`.
|
|
357
|
+
* `absorb` deliberately does NOT fire it: those calls were metered (and
|
|
358
|
+
* reported) by whoever made them, and reporting them twice would double the
|
|
359
|
+
* scout's spend in every reader downstream.
|
|
360
|
+
*/
|
|
361
|
+
export function makeCostBook({ ceilingUsd, onCall }?: {
|
|
351
362
|
ceilingUsd?: number | null;
|
|
363
|
+
onCall?: (call: {
|
|
364
|
+
label: string;
|
|
365
|
+
costUsd: number | null;
|
|
366
|
+
unpricedRounds: number;
|
|
367
|
+
}) => void;
|
|
352
368
|
}): {
|
|
353
369
|
/** @param {string} label @param {any} r a bare-agent run result @param {number} [attempt] */
|
|
354
370
|
add(label: string, r: any, attempt?: number): {
|
|
@@ -450,10 +466,12 @@ export function makeLoopGenerate(provider: any, { system, maxTokens }?: {
|
|
|
450
466
|
* listing?: {stop: string|null, files: string[]|null, block: string|null, meta: any}|null,
|
|
451
467
|
* generate: Function, seedReadFn?: Function, closeCtx?: any,
|
|
452
468
|
* ceilingUsd?: number|null,
|
|
469
|
+
* onPhase?: (phase: string, data?: any) => void,
|
|
470
|
+
* onCall?: (call: {label: string, costUsd: number|null, unpricedRounds: number}) => void,
|
|
453
471
|
* maxRevisions?: number, structureRetries?: number,
|
|
454
472
|
* structuredMode?: 'tool'|'text', catalogue?: Record<string, any>}} o
|
|
455
473
|
*/
|
|
456
|
-
export function authorClose({ workdir, seedRef, lang, verdictType, answers, questions, scout, listing, generate, seedReadFn, closeCtx, ceilingUsd, maxRevisions, structureRetries, structuredMode, catalogue, }: {
|
|
474
|
+
export function authorClose({ workdir, seedRef, lang, verdictType, answers, questions, scout, listing, generate, seedReadFn, closeCtx, ceilingUsd, onPhase, onCall, maxRevisions, structureRetries, structuredMode, catalogue, }: {
|
|
457
475
|
workdir: string;
|
|
458
476
|
seedRef: string;
|
|
459
477
|
lang: string;
|
|
@@ -479,6 +497,12 @@ export function authorClose({ workdir, seedRef, lang, verdictType, answers, ques
|
|
|
479
497
|
seedReadFn?: Function;
|
|
480
498
|
closeCtx?: any;
|
|
481
499
|
ceilingUsd?: number | null;
|
|
500
|
+
onPhase?: (phase: string, data?: any) => void;
|
|
501
|
+
onCall?: (call: {
|
|
502
|
+
label: string;
|
|
503
|
+
costUsd: number | null;
|
|
504
|
+
unpricedRounds: number;
|
|
505
|
+
}) => void;
|
|
482
506
|
maxRevisions?: number;
|
|
483
507
|
structureRetries?: number;
|
|
484
508
|
structuredMode?: "tool" | "text";
|
|
@@ -529,21 +553,66 @@ export const STRUCTURE_INSTRUCTION_TEXT: string;
|
|
|
529
553
|
* cannot emit anything else (D2/D3), so a genre it has never seen is a
|
|
530
554
|
* composition problem, not a catalogue problem.
|
|
531
555
|
*
|
|
532
|
-
* The GREEN set
|
|
533
|
-
*
|
|
534
|
-
*
|
|
535
|
-
*
|
|
536
|
-
*
|
|
537
|
-
*
|
|
538
|
-
*
|
|
556
|
+
* The GREEN set descends from questions 1–6 of the frozen TYPES set (prereg §5).
|
|
557
|
+
* TWO slots have been deleted since, both because the interview was ASKING FOR
|
|
558
|
+
* SOMETHING IT ALREADY HAD or already knew:
|
|
559
|
+
*
|
|
560
|
+
* - D13's genre CONFIRM ("this looks like a type-fixing job, correct?") —
|
|
561
|
+
* superseded in full: the interview never asks about a genre again, and the
|
|
562
|
+
* refusal it carried MOVED to the composer, on the same counted
|
|
563
|
+
* `request-red` path (see `authorCloseForJob`);
|
|
564
|
+
* - "Is there a code repo I can look at? Where?" — DROPPED on hamr's ruling
|
|
565
|
+
* after he drove the terminal interview himself (2026-08-15, verbatim: *"drop
|
|
566
|
+
* Q6"*). The repository is a MANDATORY, STRUCTURED input already —
|
|
567
|
+
* `runInterview`'s own `repoPath`, which the runner takes from `--patient` and
|
|
568
|
+
* refuses to start without. Asking a person to re-type in prose a path the
|
|
569
|
+
* machine is holding in a variable is the SWE tax this product refuses, and
|
|
570
|
+
* it invites a second, drifting answer for the same fact (hamr's own answer
|
|
571
|
+
* on the live run was *"Yes — the patient itself."*). Nothing downstream lost
|
|
572
|
+
* anything: the scout reads `repoPath`, never the answer.
|
|
573
|
+
*
|
|
574
|
+
* Question 2 was WIDENED in the same ruling: it now asks for the read-or-draw-from
|
|
575
|
+
* files as well as the ones that change. The two halves belong in one answer
|
|
576
|
+
* because they are one decision for the person answering — what the work touches,
|
|
577
|
+
* and what it touches only to look at.
|
|
578
|
+
*
|
|
579
|
+
* The five that remain are genre-neutral, which is why every generalisation so far
|
|
580
|
+
* has deleted a slot rather than reworded one. They are NUMBERED CONTIGUOUSLY from
|
|
581
|
+
* 1: the number a person sees is the key their answer is filed under, and a gap
|
|
582
|
+
* would be a hole in a form nobody can explain.
|
|
539
583
|
*/
|
|
540
584
|
export const GREEN_QUESTIONS: Readonly<{
|
|
541
585
|
1: "What do you want done?";
|
|
542
|
-
2: "Which files or folders should change?";
|
|
586
|
+
2: "Which files or folders should change? And which files should the work read or draw from (they stay untouched)?";
|
|
587
|
+
3: "Is there anything that must not change?";
|
|
588
|
+
4: "How do you check today whether it's working?";
|
|
589
|
+
5: "What would make you say this came back worse than before?";
|
|
590
|
+
}>;
|
|
591
|
+
/**
|
|
592
|
+
* THE HITL SET (N4 slice 1) — the green questions BYTE FOR BYTE, plus one.
|
|
593
|
+
*
|
|
594
|
+
* The five are unchanged deliberately rather than economically: the
|
|
595
|
+
* mechanical-first composition law (2026-08-07) says every hitl close is
|
|
596
|
+
* *deterministic stages first, judge minimal, human last*, so a hitl job needs
|
|
597
|
+
* exactly the same mechanical facts a green one does — what changes, what must
|
|
598
|
+
* not, how it is checked today, what "worse" looks like. Rewording them would
|
|
599
|
+
* fork one interview into two that drift.
|
|
600
|
+
*
|
|
601
|
+
* The last one is the only thing the green set cannot supply, and it is not a
|
|
602
|
+
* flourish: `human-confirms` requires an `ask` — the question the signer answers
|
|
603
|
+
* at the end of the run — and nothing else in the interview names it. Without it
|
|
604
|
+
* the composer would have to invent what a person is deciding, which is the one
|
|
605
|
+
* thing a human stage must never have done for it. It is the SPREAD's next number
|
|
606
|
+
* rather than a fixed one, so a green-side deletion renumbers it instead of
|
|
607
|
+
* leaving a gap (it was 7 while the green set held six, and is 6 now it holds five).
|
|
608
|
+
*/
|
|
609
|
+
export const HITL_QUESTIONS: Readonly<{
|
|
610
|
+
6: "When you look at the finished result yourself, what are you deciding?";
|
|
611
|
+
1: "What do you want done?";
|
|
612
|
+
2: "Which files or folders should change? And which files should the work read or draw from (they stay untouched)?";
|
|
543
613
|
3: "Is there anything that must not change?";
|
|
544
614
|
4: "How do you check today whether it's working?";
|
|
545
615
|
5: "What would make you say this came back worse than before?";
|
|
546
|
-
6: "Is there a code repo I can look at? Where?";
|
|
547
616
|
}>;
|
|
548
617
|
/**
|
|
549
618
|
* The three sets. `questions: null` on a locked class is ABSENT, never `{}`:
|
|
@@ -754,6 +823,11 @@ export const PARAM_SCHEMAS: Readonly<{
|
|
|
754
823
|
requireNonEmpty: {
|
|
755
824
|
const: boolean;
|
|
756
825
|
};
|
|
826
|
+
ask: {
|
|
827
|
+
type: string;
|
|
828
|
+
minLength: number;
|
|
829
|
+
description: string;
|
|
830
|
+
};
|
|
757
831
|
}>;
|
|
758
832
|
export type Red = {
|
|
759
833
|
code: string;
|
package/types/authoring.d.ts
CHANGED
|
@@ -290,9 +290,11 @@ export function normalizeDeclaration(declaration: any): any;
|
|
|
290
290
|
/** @typedef {{required: readonly string[], optional: readonly string[], pathParams: readonly string[], locked?: true, verdictClass: string, shape: string, asserts: string}} KindSpec */
|
|
291
291
|
/** the whole menu, in ascending order of what it takes to render the verdict */
|
|
292
292
|
export const VERDICT_CLASSES: readonly string[];
|
|
293
|
-
/** declared-but-locked: selecting one is COUNTED demand, never an admission
|
|
293
|
+
/** declared-but-locked: selecting one is COUNTED demand, never an admission.
|
|
294
|
+
* `hitl` LEFT this list at N4 slice 1 (see `src/job.js`'s twin) — the class is
|
|
295
|
+
* built, and its battery is below. `soft-green` waits for the judged floor. */
|
|
294
296
|
export const LOCKED_CLASSES: readonly string[];
|
|
295
|
-
/** the classes v1 actually builds
|
|
297
|
+
/** the classes v1 actually builds */
|
|
296
298
|
export const LIVE_CLASSES: readonly string[];
|
|
297
299
|
/**
|
|
298
300
|
* The whole vocabulary, as DATA. `shape` and `asserts` are the strings the
|
|
@@ -508,40 +510,52 @@ export const TYPES_GENRE: Readonly<{
|
|
|
508
510
|
}>;
|
|
509
511
|
}>;
|
|
510
512
|
}>;
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
513
|
+
export const CLASS_BATTERIES: Readonly<{
|
|
514
|
+
green: Readonly<{
|
|
515
|
+
locked: false;
|
|
516
|
+
guards: readonly (Readonly<{
|
|
517
|
+
name: "changed-from-seed";
|
|
518
|
+
kind: "files-changed";
|
|
519
|
+
params: Readonly<{
|
|
520
|
+
requireNonEmpty: true;
|
|
521
|
+
}>;
|
|
522
|
+
compose: readonly never[];
|
|
523
|
+
fill: readonly string[];
|
|
524
|
+
}> | Readonly<{
|
|
525
|
+
name: "no-suppressions";
|
|
526
|
+
kind: "pattern-absent-in-diff";
|
|
527
|
+
params: Readonly<{}>;
|
|
528
|
+
compose: readonly string[];
|
|
529
|
+
fill: readonly never[];
|
|
530
|
+
}>)[];
|
|
531
|
+
}>;
|
|
532
|
+
'soft-green': Readonly<{
|
|
533
|
+
locked: true;
|
|
534
|
+
guards: null;
|
|
535
|
+
}>;
|
|
536
|
+
hitl: Readonly<{
|
|
537
|
+
locked: false;
|
|
538
|
+
guards: readonly (Readonly<{
|
|
539
|
+
name: "changed-from-seed";
|
|
540
|
+
kind: "files-changed";
|
|
541
|
+
params: Readonly<{
|
|
542
|
+
requireNonEmpty: true;
|
|
543
|
+
}>;
|
|
544
|
+
compose: readonly never[];
|
|
545
|
+
fill: readonly string[];
|
|
546
|
+
}> | Readonly<{
|
|
547
|
+
name: "no-suppressions";
|
|
548
|
+
kind: "pattern-absent-in-diff";
|
|
549
|
+
params: Readonly<{}>;
|
|
550
|
+
compose: readonly string[];
|
|
551
|
+
fill: readonly never[];
|
|
552
|
+
}>)[];
|
|
553
|
+
}>;
|
|
544
554
|
}>;
|
|
555
|
+
/** the kinds NO ONE runs as an in-run ruler — ruling 5's law, as data. A
|
|
556
|
+
* declaration that offers one is a red rather than a normalization: the signer
|
|
557
|
+
* must not read one thing and have the runner store another. */
|
|
558
|
+
export const NEVER_OFFERED_KINDS: readonly string[];
|
|
545
559
|
export type Red = {
|
|
546
560
|
code: string;
|
|
547
561
|
path: string;
|
package/types/authorjob.d.ts
CHANGED
|
@@ -82,13 +82,15 @@ export function refuseLockedKind(kind: string): Refusal;
|
|
|
82
82
|
* verdictType?: string|null,
|
|
83
83
|
* questions?: Record<string|number, string>|null, generate?: Function, provider?: any,
|
|
84
84
|
* seedRef?: string|null, scout?: any, listing?: any, ceilingUsd?: number|null,
|
|
85
|
+
* onPhase?: (phase: string, data?: any) => void,
|
|
86
|
+
* onCall?: (call: {label: string, costUsd: number|null, unpricedRounds: number}) => void,
|
|
85
87
|
* seedFn?: Function, scoutFn?: Function, listingFn?: Function,
|
|
86
88
|
* authorFn?: Function, authorOpts?: object}} o
|
|
87
89
|
* @returns {Promise<{ok: boolean, refusal: Refusal|null, verdictType: string|null,
|
|
88
90
|
* closeDecl: any, seedRef: string|null, authoring: any, reds: Red[], stop: string|null,
|
|
89
91
|
* cost: any, interview: any}>}
|
|
90
92
|
*/
|
|
91
|
-
export function authorCloseForJob({ answers, repoPath, lang, verdictType, questions, generate, provider, seedRef, scout, listing, ceilingUsd, seedFn, scoutFn, listingFn, authorFn, authorOpts, }: {
|
|
93
|
+
export function authorCloseForJob({ answers, repoPath, lang, verdictType, questions, generate, provider, seedRef, scout, listing, ceilingUsd, onPhase, onCall, seedFn, scoutFn, listingFn, authorFn, authorOpts, }: {
|
|
92
94
|
answers: Record<string | number, any>;
|
|
93
95
|
repoPath?: string | null;
|
|
94
96
|
lang: string;
|
|
@@ -100,6 +102,12 @@ export function authorCloseForJob({ answers, repoPath, lang, verdictType, questi
|
|
|
100
102
|
scout?: any;
|
|
101
103
|
listing?: any;
|
|
102
104
|
ceilingUsd?: number | null;
|
|
105
|
+
onPhase?: (phase: string, data?: any) => void;
|
|
106
|
+
onCall?: (call: {
|
|
107
|
+
label: string;
|
|
108
|
+
costUsd: number | null;
|
|
109
|
+
unpricedRounds: number;
|
|
110
|
+
}) => void;
|
|
103
111
|
seedFn?: Function;
|
|
104
112
|
scoutFn?: Function;
|
|
105
113
|
listingFn?: Function;
|
package/types/authorscout.d.ts
CHANGED
|
@@ -92,13 +92,14 @@ export function defaultSurveyor({ workdir, granted, auditPath, ctx }: {
|
|
|
92
92
|
* @param {{workdir: string, provider?: any,
|
|
93
93
|
* rounds?: number, minBytes?: number, blobMax?: number, maxTokens?: number, ctx?: boolean,
|
|
94
94
|
* attempts?: number, ceilingUsd?: number|null,
|
|
95
|
+
* onCall?: (call: {label: string, costUsd: number|null, unpricedRounds: number}) => void,
|
|
95
96
|
* createLoop?: (o: {system: string, policy: any, onLlmResult: any, provider: any}) => any,
|
|
96
97
|
* createSurveyor?: (o: {workdir: string, granted: readonly string[], ctx: boolean}) => Promise<any>}} o
|
|
97
98
|
* @returns {Promise<Survey & {raw: string, raws: ReturnType<typeof scrubRaw>[],
|
|
98
99
|
* calls: {label: string, costUsd: number|null, unpricedRounds: number}[],
|
|
99
100
|
* budgetStop: 'cap-halt'|'pricing-red'|null}>}
|
|
100
101
|
*/
|
|
101
|
-
export function runAuthorScout({ workdir, provider, rounds, minBytes, blobMax, maxTokens, ctx, attempts, ceilingUsd, createLoop, createSurveyor, }: {
|
|
102
|
+
export function runAuthorScout({ workdir, provider, rounds, minBytes, blobMax, maxTokens, ctx, attempts, ceilingUsd, onCall, createLoop, createSurveyor, }: {
|
|
102
103
|
workdir: string;
|
|
103
104
|
provider?: any;
|
|
104
105
|
rounds?: number;
|
|
@@ -108,6 +109,11 @@ export function runAuthorScout({ workdir, provider, rounds, minBytes, blobMax, m
|
|
|
108
109
|
ctx?: boolean;
|
|
109
110
|
attempts?: number;
|
|
110
111
|
ceilingUsd?: number | null;
|
|
112
|
+
onCall?: (call: {
|
|
113
|
+
label: string;
|
|
114
|
+
costUsd: number | null;
|
|
115
|
+
unpricedRounds: number;
|
|
116
|
+
}) => void;
|
|
111
117
|
createLoop?: (o: {
|
|
112
118
|
system: string;
|
|
113
119
|
policy: any;
|
package/types/clock.d.ts
CHANGED
|
@@ -53,8 +53,11 @@ export function isWallTimeout(err: any, clock: Clock): boolean;
|
|
|
53
53
|
* run. `closeStages`: how many stages the job's close runs (W5) — each one gets
|
|
54
54
|
* the FULL `closeTimeoutMs`, so this is the multiplier on the overshoot, not a
|
|
55
55
|
* decoration. Defaults to 1, the single-predicate close. `now`: injected for
|
|
56
|
-
* tests — the default is the real clock
|
|
57
|
-
*
|
|
56
|
+
* tests — the default is the real clock, and it stays injectable precisely so
|
|
57
|
+
* every test here can drive time deterministically without a sleep. That default
|
|
58
|
+
* is MONOTONIC (fact 4 above), not `Date.now`: a wall-clock jump — a suspend, an
|
|
59
|
+
* NTP step, a hand-set date — must not be able to reach a signed deadline.
|
|
60
|
+
* `priorElapsedMs`: time this attempt's PREDECESSOR already consumed (see below).
|
|
58
61
|
* @returns {Clock}
|
|
59
62
|
*/
|
|
60
63
|
export function createClock({ maxWallMs, closeStages, now, priorElapsedMs }?: {
|