bareloop 0.6.0 → 0.8.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 +690 -0
- package/README.md +30 -11
- package/bareloop.context.md +658 -38
- package/package.json +2 -2
- package/src/bridges.js +571 -0
- package/src/clock.js +57 -5
- package/src/exits.js +8 -2
- package/src/index.js +29 -0
- package/src/job.js +43 -6
- package/src/ladder.js +196 -0
- package/src/ledger.js +84 -9
- package/src/plan.js +106 -107
- package/src/planrun.js +1040 -168
- package/src/ralph.js +156 -29
- package/src/reuse.js +1665 -0
- package/src/run.js +92 -6
- package/src/selection.js +160 -0
- package/src/spine.js +7 -2
- package/src/trend.js +405 -0
- package/src/validate.js +121 -0
- package/types/bridges.d.ts +198 -0
- package/types/clock.d.ts +25 -3
- package/types/exits.d.ts +14 -0
- package/types/index.d.ts +3 -0
- package/types/job.d.ts +13 -2
- package/types/ladder.d.ts +86 -0
- package/types/plan.d.ts +41 -41
- package/types/planrun.d.ts +208 -9
- package/types/ralph.d.ts +98 -4
- package/types/reuse.d.ts +392 -0
- package/types/run.d.ts +63 -3
- package/types/selection.d.ts +46 -0
- package/types/spine.d.ts +8 -1
- package/types/trend.d.ts +108 -0
- package/types/validate.d.ts +50 -0
package/src/plan.js
CHANGED
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
// plain parsed data; every failure is a named {code, path, detail} red.
|
|
26
26
|
|
|
27
27
|
import { TOOL_MENU, LOCKED_TOOLS, WRITE_VERBS, checkMenu } from './job.js';
|
|
28
|
-
import { globToPrefix, scopeContained, isObj, isNonEmptyString, sweepSecretLiterals } from './validate.js';
|
|
28
|
+
import { globToPrefix, scopeContained, isObj, isNonEmptyString, sweepSecretLiterals, hasNestedQuantifier } from './validate.js';
|
|
29
29
|
|
|
30
30
|
/** the closed exit menu (PRD v1.12 §3 + decision 1's `check-passes`): the
|
|
31
31
|
* shell evaluates every form with its own fixed code, never a command — the
|
|
@@ -102,8 +102,28 @@ const STEP_FIELDS = ['id', 'action', 'tools', 'rounds', 'target', 'exit', 'model
|
|
|
102
102
|
* construction — the agent picks a tier, never names an arbitrary model string
|
|
103
103
|
* (the tier→model mapping and any effort params are the RUNNER's, per provider).
|
|
104
104
|
* opus is deliberately absent: reserved for fresh builds hamr explicitly assigns,
|
|
105
|
-
* never plan-selectable.
|
|
106
|
-
|
|
105
|
+
* never plan-selectable.
|
|
106
|
+
*
|
|
107
|
+
* 2026-08-06 — haiku dropped, on hamr's order: "haiku should be dropped to see if
|
|
108
|
+
* it passes, if it's agent poor work or harness, like always." This is a REVERSIBLE
|
|
109
|
+
* ATTRIBUTION PROBE, not a finding that haiku is incapable. What the $0 archive read
|
|
110
|
+
* (186 spines / 71 accepted plans / 255 steps) actually shows is a CONFOUND, in both
|
|
111
|
+
* directions: only 20 steps ever declared a tier at all — haiku 12 (9 of them on a
|
|
112
|
+
* REPLANNED plan, 6 the LAST step of their plan, bounds 3–12 rounds, 2 ever green)
|
|
113
|
+
* against explicitly-declared sonnet 8 (bounds 14–32, 5 green). The planner picks
|
|
114
|
+
* haiku for steps it judges mechanical, and replanned steps sit on runs already in
|
|
115
|
+
* trouble, so neither column is a controlled read. All three bareagent-u runs of
|
|
116
|
+
* 2026-08-06 tiered the REPLANNED step down (haiku/6, haiku/8, haiku/12) and all
|
|
117
|
+
* three failed, against sonnet initial steps at 30–40 rounds — a confound sitting
|
|
118
|
+
* on the programme's most recent capability read. Removing the tier is how it gets
|
|
119
|
+
* attributed: with only one tier expressible, a still-failing replan is agent or
|
|
120
|
+
* harness, not the tier.
|
|
121
|
+
*
|
|
122
|
+
* The `model` FIELD and its validator branch stay: a one-entry menu means a plan (or
|
|
123
|
+
* a stored bridge) declaring `model:"sonnet"` still validates, and restoring haiku is
|
|
124
|
+
* a one-token edit. This narrows only what the AGENT may express — `--model haiku`
|
|
125
|
+
* remains the OPERATOR's probe knob (PRD v1.36), untouched in the runner scripts. */
|
|
126
|
+
export const STEP_MODELS = Object.freeze(['sonnet']);
|
|
107
127
|
const EXIT_FIELDS = {
|
|
108
128
|
'artifact-written': ['type', 'path', 'pattern'],
|
|
109
129
|
'tree-changed': ['type', 'scope'],
|
|
@@ -112,103 +132,14 @@ const EXIT_FIELDS = {
|
|
|
112
132
|
};
|
|
113
133
|
const SLUG_RE = /^[a-z0-9][a-z0-9-]*$/;
|
|
114
134
|
|
|
115
|
-
/**
|
|
116
|
-
*
|
|
117
|
-
*
|
|
118
|
-
*
|
|
119
|
-
*
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
if (len && src[i + len] === '?') len++; // the lazy modifier is part of the same token
|
|
124
|
-
return len;
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
/**
|
|
128
|
-
* F49 — a heuristic reject for the catastrophic-backtracking footgun: an
|
|
129
|
-
* unbounded quantifier applied to a group whose body itself repeats unboundedly
|
|
130
|
-
* (`(a+)+`, `(\d*)*`, `([a-z]+){1,}`) — including through a redundant wrapping
|
|
131
|
-
* group (`((a+))+`, `(?:(a+))*`), which is the SAME exponential class as `(a+)+`
|
|
132
|
-
* and is caught by propagating the inner repeat up through the wrapper. Such a
|
|
133
|
-
* pattern can hang `RegExp.test` for
|
|
134
|
-
* seconds on a short crafted body, and `evalExits` runs the agent-authored
|
|
135
|
-
* `artifact-written` pattern with NO timeout. This is self-DoS only — the agent
|
|
136
|
-
* authors both the pattern and (via the worker) the artifact, so a hang burns
|
|
137
|
-
* only its own run's wall-clock; there is NO arbiter compromise (it cannot
|
|
138
|
-
* escape the fence, forge a green, or leak a secret). So the reject targets the
|
|
139
|
-
* dominant exponential class and fails it as a mechanical gap at the validation
|
|
140
|
-
* gate, before any tokens burn. A full ReDoS analyzer needs a real regex engine
|
|
141
|
-
* (an external native dep we do not take for a LOW issue); exotic
|
|
142
|
-
* overlapping-alternation blowup is out of scope by decision.
|
|
143
|
-
*
|
|
144
|
-
* SCOPE IS ASYMMETRIC, both directions named on purpose:
|
|
145
|
-
* - false NEGATIVE: overlapping-alternation blowup (`(a|ab)+`-class) is not
|
|
146
|
-
* detected — out of scope (self-DoS only, no arbiter compromise).
|
|
147
|
-
* - false POSITIVE: a group whose repetitions are disambiguated by a literal
|
|
148
|
-
* anchor/delimiter (`(?:^- .+$\n?)+`, `(?:CHANGELOG:.+\n)+`) is FLAGGED even
|
|
149
|
-
* though a real engine runs it linearly — the scan sees the nested-quantifier
|
|
150
|
-
* SHAPE, not the disambiguation. Rejecting it is the FAIL-SAFE direction (it
|
|
151
|
-
* never admits an unsafe pattern), and the cost is bounded: the plan drafter
|
|
152
|
-
* gets a mechanical gap and rewrites (drop the outer `+`, or match once).
|
|
153
|
-
* Detecting "safe because anchored" needs the same real engine we declined,
|
|
154
|
-
* and guessing it wrong would ADMIT an exponential pattern — so the shape
|
|
155
|
-
* reject stands, and the over-rejection is a named, accepted limitation.
|
|
156
|
-
*
|
|
157
|
-
* The input is guaranteed to compile as a RegExp (checked first by the caller),
|
|
158
|
-
* so this scan assumes valid, balanced JS regex syntax.
|
|
159
|
-
* @param {string} src a compiled regex source string
|
|
160
|
-
* @returns {boolean} true iff a nested unbounded quantifier is present
|
|
161
|
-
*/
|
|
162
|
-
export function hasNestedQuantifier(src) {
|
|
163
|
-
/** @type {{ quant: boolean }[]} */
|
|
164
|
-
const stack = [];
|
|
165
|
-
for (let i = 0; i < src.length; i++) {
|
|
166
|
-
const c = src[i];
|
|
167
|
-
if (c === '\\') { i++; continue; } // escaped atom — the next char is a literal
|
|
168
|
-
if (c === '[') { // character class: quantifier chars inside are literals
|
|
169
|
-
i++;
|
|
170
|
-
if (src[i] === '^') i++;
|
|
171
|
-
// NO POSIX leading-`]`-is-a-literal rule: JS is not POSIX here. `[]` is the
|
|
172
|
-
// EMPTY class (matches nothing) and `[^]` the any-char idiom — both close at
|
|
173
|
-
// that first `]`. Skipping it ran the scan past the class's real end and
|
|
174
|
-
// swallowed every quantifier after it, so `x[^](a+)+$` read as safe while
|
|
175
|
-
// RegExp.test on it does not finish in 15s on a 31-char body. A false
|
|
176
|
-
// NEGATIVE is the dangerous direction (F49), so the scan follows JS.
|
|
177
|
-
//
|
|
178
|
-
// Honest limit on the monotonicity claim: this is a PARSE correction, not
|
|
179
|
-
// the "add rejections only" tightening F49's rule contemplates, and it is
|
|
180
|
-
// not strictly rejection-monotonic. Measured over 14,862 compilable
|
|
181
|
-
// generated patterns (2,369 previously rejected): 2 rejections disappear,
|
|
182
|
-
// both pathological `[]`-bearing shapes whose old `true` came from the very
|
|
183
|
-
// misparse being fixed, and both verified to finish RegExp.test in ~0ms.
|
|
184
|
-
// Every entry of the checked-in REDOS_BAD/GOOD/OVERREJECTED corpus is
|
|
185
|
-
// unchanged. So no real hazard was un-rejected — but the claim is
|
|
186
|
-
// "measured, no hazard lost", never "monotonic by construction".
|
|
187
|
-
while (i < src.length && src[i] !== ']') { if (src[i] === '\\') i++; i++; }
|
|
188
|
-
continue;
|
|
189
|
-
}
|
|
190
|
-
if (c === '(') { stack.push({ quant: false }); continue; }
|
|
191
|
-
if (c === ')') {
|
|
192
|
-
const g = stack.pop();
|
|
193
|
-
const qlen = unboundedQuantLen(src, i + 1);
|
|
194
|
-
if (g && g.quant && qlen) return true; // group repeats unboundedly AND its body did too
|
|
195
|
-
// The group is an unbounded-repeated atom of its parent when it is directly
|
|
196
|
-
// re-quantified (qlen) OR its own body already repeats unboundedly (g.quant):
|
|
197
|
-
// a redundant wrapper — ((a+))+ , (?:(a+))* , (((\d*)))+ — is the SAME
|
|
198
|
-
// exponential class as (a+)+, so an inner repeat must propagate THROUGH the
|
|
199
|
-
// enclosing group or the outer quantifier's close never sees it. Propagation
|
|
200
|
-
// is MONOTONIC (it only ever SETS quant=true → only ever adds rejections): it
|
|
201
|
-
// can widen over-rejection — the named FAIL-SAFE direction — but can never
|
|
202
|
-
// introduce a false negative, which is the dangerous one (F49).
|
|
203
|
-
if ((qlen || (g && g.quant)) && stack.length) stack[stack.length - 1].quant = true;
|
|
204
|
-
i += qlen;
|
|
205
|
-
continue;
|
|
206
|
-
}
|
|
207
|
-
const qlen = unboundedQuantLen(src, i); // a quantifier applying to the preceding atom, inside the current group
|
|
208
|
-
if (qlen) { if (stack.length) stack[stack.length - 1].quant = true; i += qlen - 1; }
|
|
209
|
-
}
|
|
210
|
-
return false;
|
|
211
|
-
}
|
|
135
|
+
/** F49's catastrophic-backtracking reject. RE-EXPORTED from validate.js, never
|
|
136
|
+
* re-declared: THREE untimed regex fields are split across the two documents —
|
|
137
|
+
* this one (`artifact-written.pattern`, agent-authored) and the operator's
|
|
138
|
+
* `judged.pattern` + `gapKeep` in job.js. plan.js imports job.js, so job.js
|
|
139
|
+
* cannot import plan.js; housing the scan in the shared primitives module is
|
|
140
|
+
* what lets both reject through ONE inventory (the SECRET_PATTERNS precedent —
|
|
141
|
+
* a second copy is a shape one side rejects and the other admits). */
|
|
142
|
+
export { hasNestedQuantifier };
|
|
212
143
|
|
|
213
144
|
/**
|
|
214
145
|
* The close, as the ordered STAGE LIST the plan flow actually executes (PRD
|
|
@@ -246,7 +177,7 @@ export function stageClose(close) {
|
|
|
246
177
|
* job spec. Never throws on JSON text or plain parsed data; every failure is
|
|
247
178
|
* a named red. Returns the parsed plan on ok (single parse), null on any red.
|
|
248
179
|
* @param {object|string} input parsed plan, or raw JSON text (parse failures are a red)
|
|
249
|
-
* @param {{ job?: any, maxStepRounds?: number, scopes?: string[],
|
|
180
|
+
* @param {{ job?: any, maxStepRounds?: number, scopes?: string[], seedRed?: string[], priorChecks?: string[] }} [opts] `job`: the
|
|
250
181
|
* validateJob-GREEN four-field spec (the ceiling, the fence, and the checks
|
|
251
182
|
* menu all come from it — a missing or non-plan-shape job fails CLOSED);
|
|
252
183
|
* `maxStepRounds`: the shell's per-step rounds ceiling (interpret's
|
|
@@ -254,9 +185,23 @@ export function stageClose(close) {
|
|
|
254
185
|
* `scopes`: the offered `tree-changed` menu from `legalScopes` (the SAME array
|
|
255
186
|
* the drafting prompt enumerated). Omitted, it derives from the signed
|
|
256
187
|
* writeScope — never a free-text fallback (F50).
|
|
188
|
+
*
|
|
189
|
+
* The two shape-lottery gate rules (2026-08-04) — both keyed on RECORDED
|
|
190
|
+
* facts, never an LLM judgment of "is this job small", and both inactive
|
|
191
|
+
* when their fact is omitted (resume/reuse callers stay byte-identical):
|
|
192
|
+
* `seedRed`: check names whose PREFLIGHT verdict was needs_revision. Rule
|
|
193
|
+
* A-v2 — a seed-red check is the goal itself, not a milestone; it may only
|
|
194
|
+
* sit on the plan's FINAL write step (`check-placement`). The $0 archive
|
|
195
|
+
* sweep: seed-red-check-on-an-early-step has 0 honest greens ever; the one
|
|
196
|
+
* wide closing step greens 7/7. Green-at-seed checks stay free mid-plan
|
|
197
|
+
* (the 20 TESTGEN mid-plan greens — a guard, not a goal).
|
|
198
|
+
* `priorChecks`: the check names the PREDECESSOR plan carried (replan gate
|
|
199
|
+
* only). Rule B — a redraft may not drop one (`check-shed`): exits without
|
|
200
|
+
* the check verify FORM alone, so a shed lets the run "green" unearned
|
|
201
|
+
* (u-msdsmkid mechanism b; the 3 archived sheds were all on step-red runs).
|
|
257
202
|
* @returns {{ ok: boolean, reds: Red[], plan: object|null }}
|
|
258
203
|
*/
|
|
259
|
-
export function validatePlan(input, { job, maxStepRounds = 40, scopes,
|
|
204
|
+
export function validatePlan(input, { job, maxStepRounds = 40, scopes, seedRed, priorChecks } = {}) {
|
|
260
205
|
/** @type {Red[]} */
|
|
261
206
|
const reds = [];
|
|
262
207
|
/** @type {(code: string, path: string, detail?: string) => void} */
|
|
@@ -319,6 +264,14 @@ export function validatePlan(input, { job, maxStepRounds = 40, scopes, capRuns =
|
|
|
319
264
|
} else if (plan.steps.length > MAX_PLAN_STEPS) {
|
|
320
265
|
red('bounds', 'steps', `max ${MAX_PLAN_STEPS} steps — per-step rounds bound the spend, the step count bounds the claim on the wallet`);
|
|
321
266
|
} else {
|
|
267
|
+
// Rule A-v2's anchor: the index of the plan's FINAL write-granted step. Read
|
|
268
|
+
// off the RAW steps (a step whose grant didn't parse counts as non-write —
|
|
269
|
+
// its own missing-required/invalid-value red already fired; deriving a
|
|
270
|
+
// placement charge from the false default would be a second red for one
|
|
271
|
+
// defect). -1 when no step writes: every check then reds as a mailbox anyway.
|
|
272
|
+
const lastWriteIdx = plan.steps.reduce((/** @type {number} */ acc, /** @type {any} */ s, /** @type {number} */ i) => (
|
|
273
|
+
isObj(s) && Array.isArray(s.tools) && s.tools.some((/** @type {unknown} */ t) => WRITE_VERBS.includes(/** @type {string} */ (t))) ? i : acc), -1);
|
|
274
|
+
const seedRedNames = Array.isArray(seedRed) ? seedRed.filter(isNonEmptyString) : [];
|
|
322
275
|
const seen = new Set();
|
|
323
276
|
plan.steps.forEach((/** @type {any} */ s, /** @type {number} */ i) => {
|
|
324
277
|
const at = `steps.${i}`;
|
|
@@ -370,8 +323,24 @@ export function validatePlan(input, { job, maxStepRounds = 40, scopes, capRuns =
|
|
|
370
323
|
if (s.model !== undefined && !STEP_MODELS.includes(s.model)) {
|
|
371
324
|
red('invalid-value', `${at}.model`, `menu: ${STEP_MODELS.join('|')} — the tier menu is signed; the agent picks a tier, never names a model`);
|
|
372
325
|
}
|
|
373
|
-
|
|
374
|
-
|
|
326
|
+
// `attempts` is TOLERATED-INERT (step ladder, 2026-08-03). It once tightened a
|
|
327
|
+
// fixed per-step attempt count; that count is gone — a step's iterations are
|
|
328
|
+
// governed by the strike ladder, and a field that could only ever TIGHTEN is
|
|
329
|
+
// the wrong shape for a progress rule (drafters measurably tightened it to 3,
|
|
330
|
+
// then to 2 on replan, which is the opposite of the heal a converging step
|
|
331
|
+
// needs). It is no longer OFFERED in the drafting prompt, and the runner
|
|
332
|
+
// ignores it.
|
|
333
|
+
//
|
|
334
|
+
// It is not REJECTED, and that is the load-bearing half: the frozen bridge
|
|
335
|
+
// registry's stored plans carry `attempts`, and a bridge rides into the drafter
|
|
336
|
+
// as a starting draft after passing the ordinary validator. Redding it here
|
|
337
|
+
// would refuse every stored recipe minted before today and ruin a frozen
|
|
338
|
+
// contrast experiment. The SHAPE is still checked — a known field holding
|
|
339
|
+
// garbage stays a named red — but the upper bound is gone with the cap it
|
|
340
|
+
// named: keeping `<= capRuns` would make a plan stored under one runner's
|
|
341
|
+
// number invalid under another's, for a field that decides nothing.
|
|
342
|
+
if (s.attempts !== undefined && !(Number.isInteger(s.attempts) && s.attempts >= 1)) {
|
|
343
|
+
red('bounds', `${at}.attempts`, 'integer >= 1 — the field is accepted for stored-plan compatibility and bounds nothing (the step ladder governs iterations)');
|
|
375
344
|
}
|
|
376
345
|
if (s.scope !== undefined && !scopeMenu.includes(s.scope)) {
|
|
377
346
|
red('invalid-value', `${at}.scope`, `menu: [${scopeMenu.join(', ')}] — a per-step write scope narrows the fence from the same menu tree-changed uses`);
|
|
@@ -435,8 +404,26 @@ export function validatePlan(input, { job, maxStepRounds = 40, scopes, capRuns =
|
|
|
435
404
|
red('step-scope-escape', `${at}.target`, `"${s.target}" is inside the signed fence but outside this step's own scope "${stepScope}" — the step's gate is built from that narrowed prefix, so every write to this target would be denied`);
|
|
436
405
|
}
|
|
437
406
|
|
|
438
|
-
validateExit(s, at, red, { checkNames, fence: spec.writeScope, insideFence, writeStep, toolsParsed, scopeMenu, stepScope, overlapsStepScope });
|
|
407
|
+
validateExit(s, at, red, { checkNames, fence: spec.writeScope, insideFence, writeStep, toolsParsed, scopeMenu, stepScope, overlapsStepScope, seedRedNames, isFinalWrite: i === lastWriteIdx });
|
|
439
408
|
});
|
|
409
|
+
|
|
410
|
+
// Rule B (`check-shed`, replan gate): the caller hands over the check names
|
|
411
|
+
// the PREDECESSOR plan carried; a redraft that drops one is refused. Exits
|
|
412
|
+
// without the check verify FORM alone (tree-changed/artifact-written), so a
|
|
413
|
+
// shed lets a step "green" with the truth unjudged — u-msdsmkid's replan
|
|
414
|
+
// dropped check-passes from BOTH steps and greened unearned on form. Set
|
|
415
|
+
// semantics on NAMES: where the check sits may move (A-v2 forces it to the
|
|
416
|
+
// final write step); that it is judged at all may not. One red listing every
|
|
417
|
+
// dropped name — one defect (the shed), one red.
|
|
418
|
+
if (Array.isArray(priorChecks)) {
|
|
419
|
+
const carried = new Set(plan.steps.flatMap((/** @type {any} */ s) => (isObj(s) && Array.isArray(s.exit)
|
|
420
|
+
? s.exit.filter((/** @type {any} */ e) => isObj(e) && e.type === 'check-passes' && isNonEmptyString(e.name)).map((/** @type {any} */ e) => e.name)
|
|
421
|
+
: [])));
|
|
422
|
+
const dropped = priorChecks.filter(isNonEmptyString).filter((n) => !carried.has(n));
|
|
423
|
+
if (dropped.length) {
|
|
424
|
+
red('check-shed', 'steps', `the predecessor plan carried check-passes(${dropped.join(', ')}) and this redraft drops ${dropped.length === 1 ? 'it' : 'them'} — a replan may move a check, never shed it (the remaining exits verify form only, so a shed green would be unearned)`);
|
|
425
|
+
}
|
|
426
|
+
}
|
|
440
427
|
}
|
|
441
428
|
|
|
442
429
|
// 3. secrets sweep — the agent-authored document is the riskier entry point
|
|
@@ -452,13 +439,15 @@ export function validatePlan(input, { job, maxStepRounds = 40, scopes, capRuns =
|
|
|
452
439
|
* @param {Record<string, any>} s the step
|
|
453
440
|
* @param {string} at step path prefix
|
|
454
441
|
* @param {(code: string, path: string, detail?: string) => void} red
|
|
455
|
-
* @param {{ checkNames: string[], fence: string[], insideFence: (p: string) => boolean, writeStep: boolean, toolsParsed: boolean, scopeMenu: string[], stepScope: string|null, overlapsStepScope: (p: string) => boolean }} ctx
|
|
442
|
+
* @param {{ checkNames: string[], fence: string[], insideFence: (p: string) => boolean, writeStep: boolean, toolsParsed: boolean, scopeMenu: string[], stepScope: string|null, overlapsStepScope: (p: string) => boolean, seedRedNames: string[], isFinalWrite: boolean }} ctx
|
|
456
443
|
* `stepScope`/`overlapsStepScope` (W3): the step's OWN narrowed fence when it
|
|
457
444
|
* declared a legal `scope`, null/always-true otherwise. Exits are OBSERVATIONS,
|
|
458
445
|
* not writes, so only `tree-changed` is constrained by it and only against
|
|
459
446
|
* DISJOINTness — see the arm for the measured semantics.
|
|
447
|
+
* `seedRedNames`/`isFinalWrite` (Rule A-v2): the preflight-red check names and
|
|
448
|
+
* whether THIS step is the plan's final write step — see the check-passes arm.
|
|
460
449
|
*/
|
|
461
|
-
function validateExit(s, at, red, { checkNames, fence, insideFence, writeStep, toolsParsed, scopeMenu, stepScope, overlapsStepScope }) {
|
|
450
|
+
function validateExit(s, at, red, { checkNames, fence, insideFence, writeStep, toolsParsed, scopeMenu, stepScope, overlapsStepScope, seedRedNames, isFinalWrite }) {
|
|
462
451
|
if (!Array.isArray(s.exit) || s.exit.length === 0) {
|
|
463
452
|
red('missing-required', `${at}.exit`, `non-empty array from the closed menu ${EXIT_TYPES.join('|')} — ALL listed exits must pass (AND-only); a step without an exit has no progress gate`);
|
|
464
453
|
return;
|
|
@@ -485,6 +474,16 @@ function validateExit(s, at, red, { checkNames, fence, insideFence, writeStep, t
|
|
|
485
474
|
// decision 1: a check the spec doesn't sign DOES NOT EXIST — and the
|
|
486
475
|
// detail names the signed menu so the replan can aim, not guess
|
|
487
476
|
red('check-unknown', eAt, `"${e.name}" is not an offered close stage — the agent references stages of the operator's close, never authors a ruler; offered: [${checkNames.join(', ') || 'none'}]`);
|
|
477
|
+
} else if (seedRedNames.includes(e.name) && writeStep && !isFinalWrite) {
|
|
478
|
+
// Rule A-v2 (`check-placement`): a check that was RED at preflight is
|
|
479
|
+
// the GOAL itself, not a milestone — it judges the whole territory, so
|
|
480
|
+
// an earlier step scoped to a slice can never satisfy it without
|
|
481
|
+
// crossing its own boundary (the bareagent-u death shape: 0 honest
|
|
482
|
+
// greens in the whole archive; the wide closing step greens 7/7). Only
|
|
483
|
+
// on a WRITE step and only when the anchor exists elsewhere: a
|
|
484
|
+
// read-only step already redded as a mailbox (one defect, one red),
|
|
485
|
+
// and a green-at-seed check is a guard, free to sit anywhere.
|
|
486
|
+
red('check-placement', eAt, `"${e.name}" was RED at preflight — a failing check is the goal itself and may only gate the plan's FINAL write step (make that step wide: free to edit any file it reports, iterating until it passes); an earlier step scoped to a slice can never satisfy a whole-goal check`);
|
|
488
487
|
}
|
|
489
488
|
} else if (e.type === 'tree-changed') {
|
|
490
489
|
hasTreeChanged = true;
|