bareloop 0.6.0 → 0.7.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 +407 -0
- package/README.md +28 -11
- package/bareloop.context.md +510 -28
- package/package.json +2 -2
- package/src/bridges.js +571 -0
- package/src/clock.js +57 -5
- package/src/index.js +29 -0
- package/src/ladder.js +196 -0
- package/src/ledger.js +27 -0
- package/src/plan.js +75 -7
- package/src/planrun.js +762 -146
- package/src/ralph.js +148 -28
- package/src/reuse.js +1602 -0
- package/src/run.js +73 -6
- package/src/selection.js +160 -0
- package/src/spine.js +7 -2
- package/src/trend.js +405 -0
- package/types/bridges.d.ts +198 -0
- package/types/clock.d.ts +25 -3
- package/types/index.d.ts +3 -0
- package/types/ladder.d.ts +86 -0
- package/types/plan.d.ts +18 -3
- package/types/planrun.d.ts +108 -9
- package/types/ralph.d.ts +82 -4
- package/types/reuse.d.ts +388 -0
- package/types/run.d.ts +50 -3
- package/types/selection.d.ts +46 -0
- package/types/spine.d.ts +8 -1
- package/types/trend.d.ts +108 -0
package/src/run.js
CHANGED
|
@@ -76,9 +76,46 @@ async function primitiveSmoke(workdir) {
|
|
|
76
76
|
* metered claude-json text provider for the toolless drafter); ignored on every Loop-driven provider
|
|
77
77
|
* @param {(type: string, data?: object) => object} opts.emit spine emitter
|
|
78
78
|
* @param {(tier: string) => any} [opts.providerFor] P: per-step model-tier provider factory (forwarded to the plan flow)
|
|
79
|
-
* @param {number} [opts.capRuns] shell-owned
|
|
79
|
+
* @param {number} [opts.capRuns] shell-owned iteration cap for the plan flow's
|
|
80
|
+
* CLOSE-FIX loop (a plan STEP runs under the strike ladder instead)
|
|
81
|
+
* @param {number} [opts.strikeLimit] shell-owned strike ceiling for a plan step's ladder
|
|
80
82
|
* @param {number} [opts.shellCapUsd] the shell's hard USD ceiling
|
|
81
83
|
* @param {number} [opts.closeTimeoutMs] close wall-clock cap (shell territory)
|
|
84
|
+
* @param {unknown} [opts.bridge] Layer 3 — a bridge-v1 entry to reuse as the drafter's
|
|
85
|
+
* STARTING DRAFT (design record 2026-08-01, D4), forwarded to the plan flow. Omitted is
|
|
86
|
+
* the cold path and is byte-identical to a pre-Layer-3 run. WHICH entry (the listing, an
|
|
87
|
+
* operator pin, the LLM's pick) is the caller's decision, and so is what to do when the
|
|
88
|
+
* load gate refuses one: a refusal returns the distinct `recipe-stale` outcome with zero
|
|
89
|
+
* spend rather than silently drafting cold, because starting a paid run on a decision
|
|
90
|
+
* nobody made is the same class of error as widening a cap to manufacture a green.
|
|
91
|
+
* @param {number} [opts.priorSpentUsd=0] RESUME (module C) — money a PREVIOUS, killed
|
|
92
|
+
* attempt of this same try already spent. It seeds the ledger, so the restarted
|
|
93
|
+
* attempt runs under the REMAINDER of the signed `budgetUsd` and the terminal
|
|
94
|
+
* `job-end` states the try's WHOLE spend across both attempts. hamr's ruling: "a
|
|
95
|
+
* budget ceiling folds in prior spend so re-invoking cannot silently widen it" —
|
|
96
|
+
* the fold is the seam precisely because the CAP is in the spec hash, so
|
|
97
|
+
* tightening the cap instead would make a new spec version needing a signature
|
|
98
|
+
* nobody typed. The caller reconstructs the figure from the dead run's own spine.
|
|
99
|
+
* @param {boolean} [opts.priorSpendComplete=true] RESUME — was that fold EXACT?
|
|
100
|
+
* `readResume` reports false when any round inside the dead attempt came back
|
|
101
|
+
* unpriced, which makes the fold a FLOOR. The unknown does not heal by being carried
|
|
102
|
+
* forward, so it rides one-way onto every `job-end` this run emits: a total built on
|
|
103
|
+
* a floor is a floor, and reporting it as exact is F6 in a resume's coat.
|
|
104
|
+
* @param {number} [opts.priorWallMs=0] RESUME — the same ruling in a time coat: wall
|
|
105
|
+
* time the killed attempt already consumed, folded into the run clock so the
|
|
106
|
+
* restart gets the remainder of the SIGNED wall, never a fresh allotment.
|
|
107
|
+
* @param {any} [opts.resumeSeed] RESUME (module C v2) — WHERE inside the killed attempt
|
|
108
|
+
* the kill landed, read off its own spine by `readResume`: the plan it accepted and the
|
|
109
|
+
* steps that reached their exits. Forwarded verbatim to the plan flow, which reloads
|
|
110
|
+
* the plan instead of re-scouting and re-drafting, and skips the finished steps.
|
|
111
|
+
* hamr's ruling: *"start last step instead from the beginning, why would i want to
|
|
112
|
+
* waste more money on something i already started"*. Absent is the ordinary path.
|
|
113
|
+
* @param {{stage?: string|null, value?: number|null}[]} [opts.resumeGrades] RESUME — the
|
|
114
|
+
* close grades the killed leg recorded (`readResume`'s `restart.grades`), forwarded to
|
|
115
|
+
* the plan flow as the trend's BASELINES. A resumed run's halt readout judges the whole
|
|
116
|
+
* chain, not just the leg: without them a leg that re-grades an unchanged tree reports
|
|
117
|
+
* `flat` — "revise the goal" — on a run that was converging when the money ran out.
|
|
118
|
+
* Counts and stage names only; no close bytes cross this seam. Absent is the cold path.
|
|
82
119
|
* @param {boolean} [opts.layerRoot=false] Layer R (within-run ratchet) — shell
|
|
83
120
|
* territory, threaded to the plan flow. Defaults OFF
|
|
84
121
|
* (decided 2026-07-21): fixation is extinct on every current job (F41), so
|
|
@@ -88,16 +125,32 @@ async function primitiveSmoke(workdir) {
|
|
|
88
125
|
* binding.)
|
|
89
126
|
* @returns {Promise<string>} outcome: 'green' | 'already-green' | 'escalated' |
|
|
90
127
|
* 'unapproved-spec' | 'job-red' | 'smoke-red' | 'plan-red' | 'check-red' |
|
|
91
|
-
* 'close-red' | 'close-unsupported' | 'pricing-red' | 'provider-red' |
|
|
128
|
+
* 'close-red' | 'close-unsupported' | 'recipe-stale' | 'pricing-red' | 'provider-red' |
|
|
92
129
|
* 'interpreter-red' | 'cap-halt' | 'wall-halt' | 'step-stalled' | `step-red:<id>`
|
|
93
130
|
*/
|
|
94
|
-
export async function runJob(rawSpec, { approvals, workdir, provider, nativeProvider, providerFor, emit, capRuns = 3, shellCapUsd = 2, closeTimeoutMs, layerRoot = false }) {
|
|
131
|
+
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 = [] }) {
|
|
95
132
|
// 0. the ledger's counters, declared FIRST so that every job-end — including
|
|
96
133
|
// the pre-token reds below — can state a real figure. An omitted `spentUsd` is
|
|
97
134
|
// not a zero: a consumer reads `undefined` and either crashes or launders it
|
|
98
135
|
// into $0 (F12's class, at the terminal record instead of mid-attempt).
|
|
99
|
-
|
|
136
|
+
//
|
|
137
|
+
// It starts at the RESUME fold rather than at 0 when a killed attempt of this
|
|
138
|
+
// same try already spent money (see `priorSpentUsd`). Belted like every other
|
|
139
|
+
// number that enters the arbiter's arithmetic: a garbage fold is 0, never a NaN
|
|
140
|
+
// that would poison every later comparison into "no cap".
|
|
141
|
+
let spentUsd = typeof priorSpentUsd === 'number' && Number.isFinite(priorSpentUsd) && priorSpentUsd > 0 ? priorSpentUsd : 0;
|
|
100
142
|
let unpriced = false;
|
|
143
|
+
// RESUME (v1.46 §3): was the FOLD itself exact? `readResume` marks it false when any
|
|
144
|
+
// round inside the dead attempt came back unpriced, and that unknown does not heal by
|
|
145
|
+
// being carried forward — every round of THIS attempt being priced repairs nothing
|
|
146
|
+
// about the one before it. One-way, like `stalled` and `cutMidCall`, and read by
|
|
147
|
+
// `spend()`, so the terminal states a floor instead of an exact-looking total (F6).
|
|
148
|
+
// Read off the FLAG alone, never conjoined with the money: a leg killed with every
|
|
149
|
+
// round unpriced folds $0 and still says `false`, and that is a floor OF zero, not an
|
|
150
|
+
// exact zero. Gating on `spentUsd > 0` dropped exactly that predecessor's unknown and
|
|
151
|
+
// handed the resumed terminal an exact-looking total. The param defaults `true`, so a
|
|
152
|
+
// run nobody resumed is unchanged.
|
|
153
|
+
const priorFloor = priorSpendComplete === false;
|
|
101
154
|
// W1: did this run ABSORB a stall? The terminal `step-stalled` is the rare case;
|
|
102
155
|
// the common one self-heals (src/stall.js abandons the hung call and reissues),
|
|
103
156
|
// and an abandoned call may already have been billed — a reissue can pay twice
|
|
@@ -122,7 +175,10 @@ export async function runJob(rawSpec, { approvals, workdir, provider, nativeProv
|
|
|
122
175
|
// says so machine-readably instead of dressing a floor up as exact. Emitted on
|
|
123
176
|
// EVERY job-end (true when everything was priced) so no consumer ever has to
|
|
124
177
|
// branch on field presence.
|
|
125
|
-
|
|
178
|
+
// ONE spelling of the four causes, so the terminal and the in-flight money readout can
|
|
179
|
+
// never disagree about whether the same run's figure is exact.
|
|
180
|
+
const spendComplete = () => !unpriced && !stalled && !cutMidCall && !priorFloor;
|
|
181
|
+
const spend = () => ({ spentUsd, spendComplete: spendComplete() });
|
|
126
182
|
// 1. human-signs-always — before ANY provider call (N1 decision #1)
|
|
127
183
|
if (!checkApproval(rawSpec, approvals)) {
|
|
128
184
|
emit('job-end', { outcome: 'unapproved-spec', detail: 'no approval record matches this exact spec version', ...spend() });
|
|
@@ -138,6 +194,16 @@ export async function runJob(rawSpec, { approvals, workdir, provider, nativeProv
|
|
|
138
194
|
emit('job-start', {
|
|
139
195
|
job: job.job, specHash: jobSpecHash(job), budgetUsd: job.budgetUsd,
|
|
140
196
|
shape: 'plan', goal: job.goal,
|
|
197
|
+
// RESUME (v1.46 §3) — the DECLARED fold, the `try-start` precedent one level
|
|
198
|
+
// down. A reader reconstructing a chain of resumes must add only each attempt's
|
|
199
|
+
// OWN new rounds; without this record the second resume of a halted run would
|
|
200
|
+
// re-derive from its own file alone and silently widen the ceiling by everything
|
|
201
|
+
// spent before it. Emitted only when there IS a fold: an absent fold is absent,
|
|
202
|
+
// and a decorative `0` would be indistinguishable from a run nobody folded.
|
|
203
|
+
// A fold of $0 that is NOT exact is still a fold — the unknown is the whole of what
|
|
204
|
+
// it inherited, and it is the only field that can carry it forward.
|
|
205
|
+
...(spentUsd > 0 || priorFloor ? { priorSpentUsd: spentUsd, priorSpendComplete: !priorFloor } : {}),
|
|
206
|
+
...(typeof priorWallMs === 'number' && Number.isFinite(priorWallMs) && priorWallMs > 0 ? { priorWallMs } : {}),
|
|
141
207
|
});
|
|
142
208
|
|
|
143
209
|
// 2. known-answer smoke before tokens (A3: silent degradation throws nothing)
|
|
@@ -190,9 +256,10 @@ export async function runJob(rawSpec, { approvals, workdir, provider, nativeProv
|
|
|
190
256
|
// accounts it natively (F12) and the job-end money contract is unchanged.
|
|
191
257
|
{
|
|
192
258
|
const outcome = await runPlan(job, {
|
|
193
|
-
workdir, provider, nativeProvider, providerFor, emit: meter, capRuns, closeTimeoutMs, layerRoot,
|
|
259
|
+
workdir, provider, nativeProvider, providerFor, emit: meter, capRuns, ...(strikeLimit !== undefined ? { strikeLimit } : {}), closeTimeoutMs, layerRoot, bridge, priorWallMs, resumeSeed, resumeGrades,
|
|
194
260
|
remainingUsd: () => Math.min(shellCapUsd, job.budgetUsd - spentUsd),
|
|
195
261
|
isUnpriced: () => unpriced, // F6: let the plan flow bail in-flight, not just after it returns
|
|
262
|
+
spendComplete, // …and let its money-halt readout say whether the remaining it quotes is exact
|
|
196
263
|
});
|
|
197
264
|
if (unpriced) return pricingRed();
|
|
198
265
|
if (outcome.startsWith('step-red:')) {
|
package/src/selection.js
ADDED
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
// Layer 3 — D3's DISPLAY half: the workflow LISTING, and the prompt that asks for a
|
|
2
|
+
// pick (design record docs/plans/2026-08-01-layer-3-reuse-design.md, D3).
|
|
3
|
+
//
|
|
4
|
+
// hamr's ruling, verbatim: *"i think there should be a listing for workflows names that
|
|
5
|
+
// user can choose or auto-choose, or do we want llm to always decide (we still need to
|
|
6
|
+
// keep a way for user to inspect and view existing workflows … but llm can override and
|
|
7
|
+
// say none matches the ask and that agent has to create a new one)"*.
|
|
8
|
+
//
|
|
9
|
+
// **Both functions are pure text.** Nothing here reads a file, calls a model, or decides
|
|
10
|
+
// a run: the selection CALL, the operator's pin/shortlist/force-cold flow, and the parse
|
|
11
|
+
// of the answer are the runner's (modules 4/5). Choosing is not authoring — the user
|
|
12
|
+
// picks among workflows the agent authored and greens minted, and never writes a step, a
|
|
13
|
+
// check or a plan (PRD v1.28, unbroken).
|
|
14
|
+
//
|
|
15
|
+
// Two display rules are load-bearing rather than cosmetic:
|
|
16
|
+
//
|
|
17
|
+
// - **F6 — unpriced is never free.** An unknown cost or duration renders as UNKNOWN, and
|
|
18
|
+
// a band that skipped unknowns says how many it skipped. A `$0.00` where nothing was
|
|
19
|
+
// priced is a floor that reads as exact, which is the dishonesty F6 exists to stop; the
|
|
20
|
+
// same discipline extends to time.
|
|
21
|
+
// - **A listing shorter than its directory must say so.** `loadRegistry` skips-and-reports
|
|
22
|
+
// a malformed entry; a listing that quietly shows the survivors would leave the picker
|
|
23
|
+
// choosing from a set nobody told it was incomplete — the blind-instrument class.
|
|
24
|
+
//
|
|
25
|
+
// There is deliberately NO score and no ranking beyond name order (D6): n=1 is an anecdote
|
|
26
|
+
// in either direction, a percentage over 2–3 runs is fake precision, and if a score ever
|
|
27
|
+
// earns its way in its threshold is hamr's, from a measured base rate.
|
|
28
|
+
|
|
29
|
+
import { listingRow } from './bridges.js';
|
|
30
|
+
|
|
31
|
+
/** minutes, one decimal — the granularity a human picks a workflow at (the unit is
|
|
32
|
+
* written once, by the band) */
|
|
33
|
+
const minutes = (/** @type {number} */ ms) => (ms / 60_000).toFixed(1);
|
|
34
|
+
const usd = (/** @type {number} */ n) => `$${n.toFixed(2)}`;
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* One aggregate, rendered honestly (F6). `min`/`max` are null when NOTHING in the set was
|
|
38
|
+
* known — the answer is UNKNOWN, never a zero — and a partial set carries the count it
|
|
39
|
+
* could not read.
|
|
40
|
+
* @param {number|null} min @param {number|null} max
|
|
41
|
+
* @param {(n: number) => string} fmt
|
|
42
|
+
* @param {number} unknown how many of the greens carried no figure
|
|
43
|
+
* @param {number} total how many greens there are
|
|
44
|
+
* @param {string} word the honest name for a missing figure ('unpriced' | 'untimed')
|
|
45
|
+
* @param {string} [suffix] the unit, written ONCE after the range ("8.9–31.6 min", never
|
|
46
|
+
* "8.9 min–31.6 min")
|
|
47
|
+
*/
|
|
48
|
+
function band(min, max, fmt, unknown, total, word, suffix = '') {
|
|
49
|
+
if (min === null || max === null) return `UNKNOWN (${total || 'no'} green${total === 1 ? '' : 's'}, none ${word === 'unpriced' ? 'priced' : 'timed'})`;
|
|
50
|
+
const core = `${min === max ? fmt(min) : `${fmt(min)}–${fmt(max)}`}${suffix}`;
|
|
51
|
+
return unknown > 0 ? `${core} (${unknown} of ${total} ${word})` : core;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* D3's listing: what the operator inspects and what the selecting model reads — the SAME
|
|
56
|
+
* text, so the two can never be choosing from different sets.
|
|
57
|
+
*
|
|
58
|
+
* Per bridge, one compact block: name, the job sentence it greened, status, greens/reds
|
|
59
|
+
* with the last outcome, and the cost/time band of its greens.
|
|
60
|
+
*
|
|
61
|
+
* Total and never throws: a listing is a display surface, and one malformed entry must
|
|
62
|
+
* not take the whole listing down (`listingRow` holds the same contract). Deterministic
|
|
63
|
+
* by name, whatever order the entries arrived in — a set that reshuffles between calls
|
|
64
|
+
* would make one picker's answer unreadable against another's.
|
|
65
|
+
*
|
|
66
|
+
* @param {unknown} registry `loadRegistry`'s result (`{ok, reds, bridges}`) or a bare
|
|
67
|
+
* array of entries. The RESULT form is preferred: it carries the skip reds, and the
|
|
68
|
+
* skips are the half a bare array cannot tell you about.
|
|
69
|
+
* @returns {string}
|
|
70
|
+
*/
|
|
71
|
+
export function renderListing(registry) {
|
|
72
|
+
const r = /** @type {any} */ (registry);
|
|
73
|
+
const bridges = (Array.isArray(r) ? r : (Array.isArray(r?.bridges) ? r.bridges : [])).filter((b) => b && typeof b === 'object');
|
|
74
|
+
const reds = Array.isArray(r?.reds) ? r.reds : [];
|
|
75
|
+
const rows = bridges.map(listingRow).sort((a, b) => String(a.name).localeCompare(String(b.name)));
|
|
76
|
+
|
|
77
|
+
const skipped = reds.length
|
|
78
|
+
? [`\n\nNOTE: ${reds.length} registry entr${reds.length === 1 ? 'y' : 'ies'} could not be read and ${reds.length === 1 ? 'is' : 'are'} NOT listed — this listing is smaller than the directory:`,
|
|
79
|
+
...reds.map((/** @type {any} */ x) => ` ${x.path ?? '(unknown file)'}${x.detail ? ` — ${x.detail}` : ''}`)].join('\n')
|
|
80
|
+
: '';
|
|
81
|
+
|
|
82
|
+
if (!rows.length) {
|
|
83
|
+
return `WORKFLOWS ON RECORD — none.\nThere are no workflows to reuse; a plan has to be drafted from scratch.${skipped}`;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const blocks = rows.map((row) => {
|
|
87
|
+
const g = row.greenCost;
|
|
88
|
+
return [
|
|
89
|
+
`[${row.name ?? '(unnamed)'}] ${(row.status ?? 'no-green').toUpperCase()}`,
|
|
90
|
+
` goal ${row.goal ?? '(no goal recorded)'}`,
|
|
91
|
+
` runs ${row.greens} green · ${row.reds} red${row.lastOutcome ? ` (last: ${row.lastOutcome})` : ''}`,
|
|
92
|
+
` green cost ${band(g.minUsd, g.maxUsd, usd, g.unpricedCount, row.greens, 'unpriced')}`
|
|
93
|
+
+ ` · time ${band(g.minWallMs, g.maxWallMs, minutes, g.untimedCount, row.greens, 'untimed', ' min')}`,
|
|
94
|
+
].join('\n');
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
return `WORKFLOWS ON RECORD — ${rows.length}\nCANDIDATE = greened once · PROVEN = greened on two distinct repositories. There is deliberately no success score.\n\n${blocks.join('\n\n')}${skipped}`;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* The SELECT-WORKFLOW prompt: the text a later module sends to ask which workflow (if
|
|
102
|
+
* any) the run should start from. Pure — it sends nothing and parses nothing.
|
|
103
|
+
*
|
|
104
|
+
* D3's two standing rules are IN the prompt rather than enforced after the fact:
|
|
105
|
+
* - *"none matches"* is a first-class answer, and it means the agent drafts new. A picker
|
|
106
|
+
* with no way to decline would return the closest thing on the shelf every time, which
|
|
107
|
+
* is how a registry becomes a lookup table.
|
|
108
|
+
* - a PINNED workflow may be refused **only explicitly, with a reason** — never silently
|
|
109
|
+
* ignored or quietly substituted. The operator's pick is the operator's; overriding it
|
|
110
|
+
* invisibly is the one thing a picker must not be able to do.
|
|
111
|
+
*
|
|
112
|
+
* The output contract is stated so the answer is machine-readable, but the PARSE belongs
|
|
113
|
+
* to the runner that sends this (modules 4/5) — and the runner's parser and this shape are
|
|
114
|
+
* one contract in two places, which is why the shape is spelled out here rather than left
|
|
115
|
+
* to the model.
|
|
116
|
+
*
|
|
117
|
+
* @param {string} listing `renderListing`'s text — verbatim, so the picker sees exactly
|
|
118
|
+
* what the human inspecting the registry sees
|
|
119
|
+
* @param {string} ask the job's goal sentence, in the operator's own words
|
|
120
|
+
* @param {string|null} [pinned] the operator's PIN, stated IN the call. D3 rules that a
|
|
121
|
+
* pin is never bypassed and never silently overridden — so the pin does not skip the
|
|
122
|
+
* call, it enters it, and the model's only legal ways out are confirming it or
|
|
123
|
+
* refusing it by name with a reason. Stated here rather than assembled by the runner:
|
|
124
|
+
* a second spelling of the pin sentence is a second prompt, and the two would drift.
|
|
125
|
+
* @returns {string}
|
|
126
|
+
*/
|
|
127
|
+
export function selectionPrompt(listing, ask, pinned = null) {
|
|
128
|
+
const pin = pinned
|
|
129
|
+
? `\nThe operator has PINNED the workflow "${pinned}". Start from it unless it is genuinely the
|
|
130
|
+
wrong kind of recipe for this ask. If you cannot use it, you must say so by picking a
|
|
131
|
+
DIFFERENT name (or null) and giving the reason — the operator's pick is then handed back
|
|
132
|
+
to the operator, never quietly replaced by yours.\n`
|
|
133
|
+
: '';
|
|
134
|
+
return `SELECT-WORKFLOW
|
|
135
|
+
A previous run's workflow can be handed to the planner as a STARTING DRAFT for a new job —
|
|
136
|
+
it saves the planner starting from a blank page. Your job is to decide whether any workflow
|
|
137
|
+
on record is close enough to be worth starting from, or whether this ask needs a new plan.
|
|
138
|
+
|
|
139
|
+
The ask:
|
|
140
|
+
${ask}
|
|
141
|
+
|
|
142
|
+
${listing}
|
|
143
|
+
${pin}
|
|
144
|
+
Rules:
|
|
145
|
+
- Pick AT MOST ONE workflow, by its exact name, copied character for character from the listing.
|
|
146
|
+
- A workflow is worth reusing when it did the same KIND of work — the same sort of change,
|
|
147
|
+
judged the same sort of way. It does NOT have to name the same files: it ran on a different
|
|
148
|
+
repository, so its paths, scopes and targets are expected to be wrong here and the planner
|
|
149
|
+
will replace them.
|
|
150
|
+
- If none of them matches this ask, say so plainly and pick nothing.
|
|
151
|
+
The agent then drafts a new plan from scratch. "None matches" is a real answer, and a bad
|
|
152
|
+
starting draft costs more than a blank page.
|
|
153
|
+
- If the operator PINNED a workflow, you may refuse it — but only EXPLICITLY, and with a
|
|
154
|
+
reason. Never silently ignore the pin or quietly substitute another one.
|
|
155
|
+
- You are choosing, not authoring. You cannot edit, merge or invent a workflow here; the
|
|
156
|
+
planner does the tweaking afterwards, and the job's own verification is the only truth.
|
|
157
|
+
|
|
158
|
+
Output ONLY this JSON object, no fences and no commentary:
|
|
159
|
+
{"choice": "<exact name from the listing>" or null, "reason": "<one sentence>"}`;
|
|
160
|
+
}
|
package/src/spine.js
CHANGED
|
@@ -11,10 +11,15 @@ import { appendFileSync } from 'node:fs';
|
|
|
11
11
|
/**
|
|
12
12
|
* Create an emitter bound to one JSONL file.
|
|
13
13
|
* @param {string} file absolute path; created on first emit
|
|
14
|
+
* @param {{startSeq?: number}} [opts] `startSeq`: the last `seq` already IN the file, for a
|
|
15
|
+
* process that CONTINUES an existing spine (a resumed run appends to the dead run's own
|
|
16
|
+
* log — module C). `seq` is monotonic per SPINE, so a second series restarting at 1
|
|
17
|
+
* would give one append-only log two rows with the same number and silently break every
|
|
18
|
+
* reader that orders by it. Default 0: a fresh file, which is every other caller.
|
|
14
19
|
* @returns {(type: string, data?: object) => object} emit — returns the event as written
|
|
15
20
|
*/
|
|
16
|
-
export function makeSpine(file) {
|
|
17
|
-
let seq = 0;
|
|
21
|
+
export function makeSpine(file, { startSeq = 0 } = {}) {
|
|
22
|
+
let seq = typeof startSeq === 'number' && Number.isFinite(startSeq) && startSeq > 0 ? Math.floor(startSeq) : 0;
|
|
18
23
|
return function emit(type, data = {}) {
|
|
19
24
|
// type/seq/ts are the envelope's, by mechanism not comment: callers spread
|
|
20
25
|
// foreign objects into events, and a payload that grows one of these keys
|