tickmarkr 2.6.5 → 2.6.6
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/README.md +7 -3
- package/dist/adapters/codex-commit-check.d.ts +15 -4
- package/dist/adapters/codex-commit-check.js +122 -25
- package/dist/adapters/codex.d.ts +1 -0
- package/dist/adapters/codex.js +12 -6
- package/dist/adapters/types.d.ts +4 -1
- package/dist/cli/commands/doctor.js +3 -1
- package/dist/cli/commands/fleet.js +318 -78
- package/dist/cli/commands/init.js +5 -2
- package/dist/cli/commands/plan.js +12 -1
- package/dist/cli/commands/verify.js +37 -9
- package/dist/config/fleet-overlay.d.ts +14 -0
- package/dist/config/fleet-overlay.js +154 -25
- package/dist/drivers/orca.d.ts +17 -3
- package/dist/drivers/orca.js +64 -20
- package/dist/gates/run-gates.d.ts +2 -1
- package/dist/gates/run-gates.js +21 -6
- package/dist/gates/test-manifest.d.ts +3 -0
- package/dist/gates/test-manifest.js +1 -1
- package/dist/gates/timeout-shaped.d.ts +3 -0
- package/dist/gates/timeout-shaped.js +10 -0
- package/dist/run/daemon.js +184 -119
- package/dist/tui/ink/fleet-app.d.ts +26 -5
- package/dist/tui/ink/fleet-app.js +50 -12
- package/package.json +1 -1
- package/skills/tickmarkr-overseer/scripts/classify-vitest-log.sh +58 -16
- package/skills/tickmarkr-overseer/scripts/grade-ci.sh +19 -6
- package/skills/tickmarkr-overseer/scripts/watch-parks.sh +121 -64
|
@@ -19,7 +19,8 @@ import { auditNamedTestOracles, listVitestTests } from "../../gates/acceptance.j
|
|
|
19
19
|
import { carriedAuthorVendors, gateReviewerFloor, modelId, modelProvider, pickReviewer } from "../../gates/review.js";
|
|
20
20
|
import { Journal, loadRoutingProfile, readProfileCursor, recordedGraphDefinitionHash, RUNS_WINDOW } from "../../run/journal.js";
|
|
21
21
|
import { harnessLine, resolveHarness } from "../harness.js";
|
|
22
|
-
import { codexCommitHeadline } from "../../adapters/codex-commit-check.js";
|
|
22
|
+
import { codexCommitHeadline, codexEscapeRoleLine } from "../../adapters/codex-commit-check.js";
|
|
23
|
+
import { codex } from "../../adapters/codex.js";
|
|
23
24
|
import { channelKey, shq } from "../../adapters/types.js";
|
|
24
25
|
import { shGit } from "../../run/git.js";
|
|
25
26
|
import { runLockRunId, runStatusLine } from "../../run/lock.js";
|
|
@@ -279,6 +280,16 @@ export async function plan(argv, cwd = process.cwd(), adapters = allAdapters(),
|
|
|
279
280
|
const entrySeats = routingEntrySeatLines(cfg);
|
|
280
281
|
if (entrySeats.length)
|
|
281
282
|
lines.push(...entrySeats, "");
|
|
283
|
+
// v2.6.6 T9 (K): Codex judge/review/consult seats launch with the worker grant (llm.ts, consult.ts), so a
|
|
284
|
+
// doctor-probed escape blocks them too; worker rows carry their own headline below.
|
|
285
|
+
const codexRoles = [
|
|
286
|
+
...(cfg.judge.adapter === codex.id ? ["judge"] : []),
|
|
287
|
+
...(pools.review.some((c) => c.adapter === codex.id) ? ["review"] : []),
|
|
288
|
+
...(cfg.consult.adapter === codex.id || pools.consult.some((c) => c.adapter === codex.id) ? ["consult"] : []),
|
|
289
|
+
];
|
|
290
|
+
const codexEscape = codexEscapeRoleLine(codexRoles, adapters, health);
|
|
291
|
+
if (codexEscape)
|
|
292
|
+
lines.push(codexEscape, "");
|
|
282
293
|
// OBS-1185: the floor route() resolved for THIS task — a task hint over the configured/mode floor.
|
|
283
294
|
const derivation = (t) => {
|
|
284
295
|
const floor = resolvedFloor(t, cfg);
|
|
@@ -102,6 +102,37 @@ export function excludeAuthorProvider(channels, author) {
|
|
|
102
102
|
const provider = modelProvider(author.model, author.vendor);
|
|
103
103
|
return [author, ...channels.filter((candidate) => candidate !== author && modelProvider(candidate.model, candidate.vendor) !== provider)];
|
|
104
104
|
}
|
|
105
|
+
/**
|
|
106
|
+
* G (D-874): `--author` names who wrote the diff so review can EXCLUDE it — it never asks for a seat. So
|
|
107
|
+
* it resolves against installed identities (an installed adapter's doctor-listed or configured model),
|
|
108
|
+
* not against the review pool, whose role allow/deny, model-auth and tier gates say nothing about
|
|
109
|
+
* authorship. A pool member resolves to itself, unchanged. Anything else is an author-only identity: its
|
|
110
|
+
* vendor is the configured declaration (the model's override, else the adapter entry's; the adapter's own
|
|
111
|
+
* only when config has no entry), never inferred, and an unseeded model carries no tier of its own — it
|
|
112
|
+
* holds the human sentinel's frontier floor, so the reviewer floor is never lowered. Same-vendor, so it
|
|
113
|
+
* can never be seated. An identity nothing declares refuses by name.
|
|
114
|
+
*/
|
|
115
|
+
function authorIdentity(claim, cfg, adapters, health, pool) {
|
|
116
|
+
const [adapter = "", ...rest] = claim.split(":");
|
|
117
|
+
const model = rest.join(":");
|
|
118
|
+
const member = pool.find((c) => c.adapter === adapter && c.model === model);
|
|
119
|
+
if (member)
|
|
120
|
+
return member;
|
|
121
|
+
const a = adapters.find((x) => x.id === adapter);
|
|
122
|
+
const h = health[adapter];
|
|
123
|
+
const seeded = a?.channels(cfg).find((c) => c.model === model);
|
|
124
|
+
const entry = cfg.tiers[adapter];
|
|
125
|
+
const vendor = seeded?.vendor ?? (entry ? entry.modelOverrides?.[model]?.vendor ?? entry.vendor : a?.vendor);
|
|
126
|
+
const why = !a ? `no registered adapter "${adapter}"`
|
|
127
|
+
: !h?.installed ? `doctor does not record ${adapter} as installed`
|
|
128
|
+
: !seeded && !h.models?.includes(model) ? `${adapter} neither lists "${model}" in doctor nor configures it`
|
|
129
|
+
: !vendor ? `no vendor is declared for ${adapter}:${model}`
|
|
130
|
+
: undefined;
|
|
131
|
+
if (why) {
|
|
132
|
+
throw new Error(`--author ${claim} does not name a discoverable author identity (${why}) — name an installed adapter's doctor-listed or configured model as <adapter>:<model>, or human`);
|
|
133
|
+
}
|
|
134
|
+
return seeded ?? { adapter, vendor: vendor, model, channel: entry?.modelOverrides?.[model]?.channel ?? entry?.channel ?? "sub", tier: "frontier" };
|
|
135
|
+
}
|
|
105
136
|
function recordedMerges(repoRoot) {
|
|
106
137
|
const runs = join(repoRoot, ".tickmarkr", "runs");
|
|
107
138
|
if (!existsSync(runs))
|
|
@@ -288,6 +319,9 @@ export async function verify(argv, cwd = process.cwd(), options = {}) {
|
|
|
288
319
|
const pools = rolePools(cfg, adapters, health);
|
|
289
320
|
judgeChannels = pools.judge;
|
|
290
321
|
channels = pools.review;
|
|
322
|
+
// Resolved before either branch so an undiscoverable claim refuses here, ahead of any capture —
|
|
323
|
+
// even where recorded authors bind and the claim is otherwise ignored.
|
|
324
|
+
const claimed = values.author && values.author !== "human" ? authorIdentity(values.author, cfg, adapters, health, channels) : undefined;
|
|
291
325
|
if (owed.length) {
|
|
292
326
|
// The run's lifetime patch authors bind the reviewer's exclusions; --author never overrides
|
|
293
327
|
// them. An author the pool cannot resolve leaves review unseatable, which fails closed.
|
|
@@ -298,15 +332,9 @@ export async function verify(argv, cwd = process.cwd(), options = {}) {
|
|
|
298
332
|
if (first)
|
|
299
333
|
author = { adapter: first.adapter, model: first.model, channel: first.channel, tier: first.tier };
|
|
300
334
|
}
|
|
301
|
-
else if (
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
const c = channels.find((ch) => ch.adapter === adapter && ch.model === model);
|
|
305
|
-
if (!c) {
|
|
306
|
-
throw new Error(`--author ${values.author} does not name a discoverable review channel — one of: ${channels.map(channelKey).join(", ") || "(none)"}`);
|
|
307
|
-
}
|
|
308
|
-
author = { adapter: c.adapter, model: c.model, channel: c.channel, tier: c.tier };
|
|
309
|
-
channels = excludeAuthorProvider(channels, c);
|
|
335
|
+
else if (claimed) {
|
|
336
|
+
author = { adapter: claimed.adapter, model: claimed.model, channel: claimed.channel, tier: claimed.tier };
|
|
337
|
+
channels = excludeAuthorProvider(channels, claimed);
|
|
310
338
|
}
|
|
311
339
|
else {
|
|
312
340
|
channels = [...channels, HUMAN_CHANNEL];
|
|
@@ -21,6 +21,14 @@ type FleetOverlayWriteFields = {
|
|
|
21
21
|
};
|
|
22
22
|
universe?: FleetUniverseRow[];
|
|
23
23
|
lowerMap?: Record<string, MapEntry>;
|
|
24
|
+
preservedAllow?: {
|
|
25
|
+
adapters?: string[];
|
|
26
|
+
models?: string[];
|
|
27
|
+
};
|
|
28
|
+
lowerAllow?: {
|
|
29
|
+
adapters?: string[];
|
|
30
|
+
models?: string[];
|
|
31
|
+
};
|
|
24
32
|
};
|
|
25
33
|
type EffortFree = FleetEditable & {
|
|
26
34
|
efforts?: undefined;
|
|
@@ -40,6 +48,12 @@ export declare function fleetFirstTouchProvenance(note: string, firstTouch: Flee
|
|
|
40
48
|
/** Apply only fields fleet authored to the parsed YAML document. Untouched nodes retain their
|
|
41
49
|
* keys, ordering, scalar style, and comments; fresh provenance is written directly on its tier node. */
|
|
42
50
|
export declare function renderFleetOverlayWrite(priorBytes: string, write: FleetOverlayWrite): string;
|
|
51
|
+
/** T10: the 1-based line of the key that holds `path` in these overlay bytes — the path's own key, the
|
|
52
|
+
* key whose `null` masks it from above, or a `<<` merge key on it (D-987: the write there refuses) — else
|
|
53
|
+
* undefined (the overlay neither declares nor masks it). */
|
|
54
|
+
export declare function overlayHoldingLine(bytes: string, path: readonly string[]): number | undefined;
|
|
55
|
+
/** T10: does `path` hold a mapping in these overlay bytes — neither absent nor a `null` mask? */
|
|
56
|
+
export declare function overlayHoldsMap(bytes: string, path: readonly string[]): boolean;
|
|
43
57
|
/** Build the repo overlay fragment fleet would write for edits since session start. */
|
|
44
58
|
export declare function fleetRepoOverlayFromDelta(initial: FleetEditable, edited: FleetEditable, existingRepo?: Record<string, unknown>, firstTouches?: Readonly<Record<string, FleetFirstTouch>>, universe?: FleetUniverseRow[]): Record<string, unknown>;
|
|
45
59
|
export declare function repoOverlayYaml(overlay: Record<string, unknown>): string;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// Fleet-overlay mutation, serialization, and diff rendering for the `tickmarkr fleet` write path.
|
|
2
|
-
import { isAlias, isMap, isNode, isScalar, isSeq, parseDocument, stringify, visit } from "yaml";
|
|
3
|
-
import { DENY_SCOPES, universeCovers, universeEntryMatches } from "./config.js";
|
|
2
|
+
import { isAlias, isCollection, isMap, isNode, isScalar, isSeq, parseDocument, stringify, visit } from "yaml";
|
|
3
|
+
import { ConfigError, DENY_SCOPES, universeCovers, universeEntryMatches } from "./config.js";
|
|
4
4
|
// OBS-1099 add.1: the writer ranges over the schema-derived scopes. The flat (all-seats) scopes
|
|
5
5
|
// take part in the membership/allow-form write; every nested scope (routing.deny.workers.*) is a
|
|
6
6
|
// literal deny list written on its own. ponytail: "flat" = directly under routing.deny. Read
|
|
@@ -64,9 +64,12 @@ function residualDeny(universe, entries) {
|
|
|
64
64
|
// list keeps its node. A staged entry the allow form cannot express as a membership key (outside the
|
|
65
65
|
// probe universe, or a bare-model/identity spelling) is written verbatim too; canonical keys the
|
|
66
66
|
// session added ride the allow form alone.
|
|
67
|
+
// T10 (D-973): read as the loader reads these bytes (readYaml parses with yaml's defaults, as toJS does
|
|
68
|
+
// here) — aliases at the leaf, its items and every ancestor resolve, and `<<` merges only in a document
|
|
69
|
+
// whose YAML version merges — so an aliased list's entries are authored entries too.
|
|
67
70
|
function authoredEntries(doc, path) {
|
|
68
|
-
const
|
|
69
|
-
return
|
|
71
|
+
const value = path.reduce((node, key) => (node !== null && typeof node === "object" ? node[key] : undefined), doc.toJS());
|
|
72
|
+
return Array.isArray(value) ? value.flatMap((item) => (item === null || typeof item !== "object" ? [String(item)] : [])) : [];
|
|
70
73
|
}
|
|
71
74
|
function flatDenyAfterWrite(doc, path, after, universe) {
|
|
72
75
|
const authored = authoredEntries(doc, path);
|
|
@@ -112,8 +115,10 @@ function unpackFleetProvenance(provenance) {
|
|
|
112
115
|
}
|
|
113
116
|
function setScalarPreservingComment(doc, path, value, authoredComment) {
|
|
114
117
|
const existing = doc.getIn(path, true);
|
|
115
|
-
if (isScalar(existing))
|
|
118
|
+
if (isScalar(existing)) {
|
|
119
|
+
releaseAnchors(doc, path);
|
|
116
120
|
existing.value = value;
|
|
121
|
+
}
|
|
117
122
|
else
|
|
118
123
|
setAt(doc, path, doc.createNode(value));
|
|
119
124
|
if (authoredComment !== undefined) {
|
|
@@ -146,6 +151,10 @@ function setStringSequencePreservingComments(doc, path, values) {
|
|
|
146
151
|
setAt(doc, path, doc.createNode(values));
|
|
147
152
|
return;
|
|
148
153
|
}
|
|
154
|
+
// T10 owed review F2: an unchanged list is not edited, so its anchor and every alias keep their bytes
|
|
155
|
+
if (existing.items.length === values.length && existing.items.every((item, at) => isScalar(item) && String(item.value) === values[at]))
|
|
156
|
+
return;
|
|
157
|
+
releaseAnchors(doc, path); // an anchored list is edited in place: its other aliases keep the original
|
|
149
158
|
const available = [...existing.items];
|
|
150
159
|
existing.items = values.map((value) => {
|
|
151
160
|
const at = available.findIndex((item) => isScalar(item) && String(item.value) === value);
|
|
@@ -175,6 +184,30 @@ function materializeAliases(doc, path) {
|
|
|
175
184
|
doc.setIn(path.slice(0, depth), detachedCopy(doc, node, target, new Set()));
|
|
176
185
|
}
|
|
177
186
|
}
|
|
187
|
+
// T10 (D-973): a write mutates every ancestor on its path and edits or replaces the leaf with all it
|
|
188
|
+
// holds; any of those nodes may be an anchor other aliases share. Each such alias first becomes a
|
|
189
|
+
// detached copy of what it resolved to, so the write never changes another consumer's value nor
|
|
190
|
+
// leaves its alias dangling.
|
|
191
|
+
function releaseAnchors(doc, path) {
|
|
192
|
+
const held = new Set();
|
|
193
|
+
for (let depth = 1; depth <= path.length; depth++) {
|
|
194
|
+
const node = doc.getIn(path.slice(0, depth), true);
|
|
195
|
+
if (!isNode(node))
|
|
196
|
+
break;
|
|
197
|
+
for (const inner of depth < path.length ? [node] : nodesOf(node)) {
|
|
198
|
+
if (!isAlias(inner) && inner.anchor)
|
|
199
|
+
held.add(inner);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
if (!held.size)
|
|
203
|
+
return;
|
|
204
|
+
visit(doc, {
|
|
205
|
+
Alias: (_key, alias) => {
|
|
206
|
+
const target = alias.resolve(doc);
|
|
207
|
+
return target !== undefined && held.has(target) ? detachedCopy(doc, alias, target, new Set()) : undefined;
|
|
208
|
+
},
|
|
209
|
+
});
|
|
210
|
+
}
|
|
178
211
|
const nodesOf = (root) => {
|
|
179
212
|
const out = [];
|
|
180
213
|
visit(root, (_key, node) => {
|
|
@@ -231,26 +264,56 @@ function deleteAt(doc, path) {
|
|
|
231
264
|
return;
|
|
232
265
|
}
|
|
233
266
|
// deleteIn throws on an empty document — only delete keys that exist.
|
|
234
|
-
if (doc.getIn(path)
|
|
235
|
-
|
|
267
|
+
if (doc.getIn(path) === undefined)
|
|
268
|
+
return;
|
|
269
|
+
releaseAnchors(doc, path);
|
|
270
|
+
doc.deleteIn(path);
|
|
236
271
|
}
|
|
237
272
|
function setAt(doc, path, value) {
|
|
238
273
|
materializeAliases(doc, path); // edit a copy of an aliased mapping, never drop what it resolved to
|
|
274
|
+
releaseAnchors(doc, path);
|
|
239
275
|
for (let depth = 1; depth < path.length; depth++) {
|
|
240
276
|
const node = doc.getIn(path.slice(0, depth), true);
|
|
241
277
|
if (node === undefined)
|
|
242
278
|
break;
|
|
243
279
|
if (!isMap(node)) {
|
|
244
|
-
|
|
280
|
+
const prefix = path.slice(0, depth);
|
|
281
|
+
doc.deleteIn(prefix);
|
|
282
|
+
// T10: a null mask over membership lists stays a mask for every deny leaf — and the routing.allow
|
|
283
|
+
// block — this write does not set, so materializing one leaf never resurrects a lower layer's
|
|
284
|
+
// other deny lists or its allow form (judge/review/consult reach)
|
|
285
|
+
if (isScalar(node) && node.value === null) {
|
|
286
|
+
const under = (inner, outer) => outer.every((key, at) => inner[at] === key);
|
|
287
|
+
for (const masked of [...DENY_SCOPES.map((scope) => scope.path), ["routing", "allow"]]) {
|
|
288
|
+
if (under(masked, prefix) && !under(masked, path) && !under(path, masked))
|
|
289
|
+
doc.setIn(masked, doc.createNode(null));
|
|
290
|
+
}
|
|
291
|
+
}
|
|
245
292
|
break;
|
|
246
293
|
}
|
|
247
294
|
}
|
|
248
295
|
doc.setIn(path, value);
|
|
249
296
|
}
|
|
297
|
+
// T10 (D-987): Fleet writes through no YAML merge key — a membership write whose edited path (the edited
|
|
298
|
+
// leaf or any map above it) takes keys through `<<` is refused, fail-closed, so nothing is published.
|
|
299
|
+
function refuseMerge(doc, priorBytes, path) {
|
|
300
|
+
let node = doc.contents;
|
|
301
|
+
for (let depth = 0; depth <= path.length && isMap(node); depth++) {
|
|
302
|
+
const merge = node.items.find((item) => isScalar(item.key) && typeof item.key.value === "symbol");
|
|
303
|
+
if (merge) {
|
|
304
|
+
const range = isNode(merge.value) ? merge.value.range : undefined;
|
|
305
|
+
const shape = range ? priorBytes.slice(range[0], range[1]).trim().replace(/\s+/g, " ") : "…";
|
|
306
|
+
throw new ConfigError(`${path.join(".")} takes keys through the YAML merge key \`<<: ${shape}\` in ${path.slice(0, depth).join(".") || "the document root"} — Fleet does not write through merge keys; inline the merged keys there by hand, then save again`);
|
|
307
|
+
}
|
|
308
|
+
node = depth < path.length ? node.get(path[depth], true) : undefined;
|
|
309
|
+
if (isAlias(node))
|
|
310
|
+
node = node.resolve(doc);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
250
313
|
function deleteEmptyMap(doc, path) {
|
|
251
314
|
const node = doc.getIn(path, true);
|
|
252
315
|
if (isMap(node) && node.items.length === 0)
|
|
253
|
-
doc
|
|
316
|
+
deleteAt(doc, path);
|
|
254
317
|
}
|
|
255
318
|
/** Apply only fields fleet authored to the parsed YAML document. Untouched nodes retain their
|
|
256
319
|
* keys, ordering, scalar style, and comments; fresh provenance is written directly on its tier node. */
|
|
@@ -266,24 +329,49 @@ export function renderFleetOverlayWrite(priorBytes, write) {
|
|
|
266
329
|
const flatChanged = new Map(FLAT_DENY_SCOPES.map((scope) => [scope.key, changed(...listsOf(write, scope))]));
|
|
267
330
|
if ([...flatChanged.values()].some(Boolean) || changed(initial.allowOut, edited.allowOut)) {
|
|
268
331
|
if (write.universe) {
|
|
332
|
+
refuseMerge(doc, priorBytes, ["routing", "allow"]);
|
|
269
333
|
// Membership write: the allow form IS the fleet; deny adapters/models scopes are tombstoned
|
|
270
334
|
// so a lower layer can never re-exclude behind the operator's back (workers untouched).
|
|
271
335
|
const form = allowFormFromExclusions(write.universe, edited);
|
|
336
|
+
const universe = write.universe;
|
|
337
|
+
const known = (entry) => universe.some((row) => entry === row.adapter || row.models.some((m) => universeEntryMatches(row, m, entry)));
|
|
338
|
+
const retainedAll = [];
|
|
339
|
+
for (const leaf of ["adapters", "models"]) {
|
|
340
|
+
const retained = [...(write.preservedAllow?.[leaf] ?? []), ...authoredEntries(doc, ["routing", "allow", leaf])]
|
|
341
|
+
.filter((entry) => !known(entry));
|
|
342
|
+
if (!retained.length)
|
|
343
|
+
continue;
|
|
344
|
+
retainedAll.push(...retained);
|
|
345
|
+
form[leaf] = sortedUnique([...form[leaf], ...retained]);
|
|
346
|
+
form.excluded = true;
|
|
347
|
+
}
|
|
348
|
+
// T10 review: a retained allow entry admits a channel the allow form cannot see, so a staged flat
|
|
349
|
+
// deny covering it stays in deny verbatim — folded into the allow form alone, the retained entry
|
|
350
|
+
// would re-admit what the operator just excluded (rail out · all over an unprobed fake:U).
|
|
351
|
+
// Either allow leaf admits a bare model (routing matches both leaves alike), so coverage never
|
|
352
|
+
// depends on which leaf retained the entry.
|
|
353
|
+
// ponytail: a bare retained entry's adapter is unknown, so any discovered adapter id covers it;
|
|
354
|
+
// an identity alias of an unprobed channel is unknowable and is matched by spelling only.
|
|
355
|
+
const coversRetained = (entry) => retainedAll.some((kept) => {
|
|
356
|
+
const colon = kept.indexOf(":");
|
|
357
|
+
if (colon !== -1) {
|
|
358
|
+
const model = kept.slice(colon + 1);
|
|
359
|
+
return universeEntryMatches({ adapter: kept.slice(0, colon), models: [model] }, model, entry);
|
|
360
|
+
}
|
|
361
|
+
return entry === kept || universe.some((row) => row.adapter === entry);
|
|
362
|
+
});
|
|
272
363
|
if (form.excluded) {
|
|
273
364
|
const allowNode = doc.getIn(["routing", "allow"], true);
|
|
274
365
|
if (allowNode !== undefined && !isMap(allowNode))
|
|
275
|
-
doc
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
}
|
|
285
|
-
else {
|
|
286
|
-
deleteAt(doc, ["routing", "allow", "models"]);
|
|
366
|
+
deleteAt(doc, ["routing", "allow"]);
|
|
367
|
+
for (const leaf of ["adapters", "models"]) {
|
|
368
|
+
const path = ["routing", "allow", leaf];
|
|
369
|
+
if (form[leaf].length)
|
|
370
|
+
setStringSequencePreservingComments(doc, path, form[leaf]);
|
|
371
|
+
else if (write.lowerAllow?.[leaf] != null)
|
|
372
|
+
setStringSequencePreservingComments(doc, path, null);
|
|
373
|
+
else
|
|
374
|
+
deleteAt(doc, path);
|
|
287
375
|
}
|
|
288
376
|
// Whole fleet out: allow stays present but empty — fail-closed, nothing admitted.
|
|
289
377
|
if (doc.getIn(["routing", "allow"]) === undefined) {
|
|
@@ -291,8 +379,12 @@ export function renderFleetOverlayWrite(priorBytes, write) {
|
|
|
291
379
|
}
|
|
292
380
|
}
|
|
293
381
|
else {
|
|
294
|
-
// Whole fleet in: no restriction to express — the allow block goes away entirely
|
|
295
|
-
|
|
382
|
+
// Whole fleet in: no restriction to express — the allow block goes away entirely, or is
|
|
383
|
+
// masked when a lower layer's allow would otherwise come back (T10 A/B resurrection).
|
|
384
|
+
if (write.lowerAllow)
|
|
385
|
+
setAt(doc, ["routing", "allow"], doc.createNode(null));
|
|
386
|
+
else
|
|
387
|
+
deleteAt(doc, ["routing", "allow"]);
|
|
296
388
|
}
|
|
297
389
|
// Authored and non-canonical entries stay in deny (LEG2-T3 finding 4, round 2 finding 2).
|
|
298
390
|
// OBS-1046: only an EDITED scope is rewritten, and only when its bytes must change — an
|
|
@@ -313,7 +405,8 @@ export function renderFleetOverlayWrite(priorBytes, write) {
|
|
|
313
405
|
const stale = authored.some((entry) => admitted.includes(entry));
|
|
314
406
|
if (!touched && !stale)
|
|
315
407
|
continue;
|
|
316
|
-
|
|
408
|
+
refuseMerge(doc, priorBytes, path);
|
|
409
|
+
const remaining = [...new Set([...flatDenyAfterWrite(doc, path, after, write.universe), ...after.filter(coversRetained)])];
|
|
317
410
|
if (remaining.length) {
|
|
318
411
|
if (remaining.join("\n") !== authored.join("\n"))
|
|
319
412
|
setStringSequencePreservingComments(doc, path, remaining);
|
|
@@ -327,6 +420,7 @@ export function renderFleetOverlayWrite(priorBytes, write) {
|
|
|
327
420
|
for (const scope of FLAT_DENY_SCOPES) {
|
|
328
421
|
if (!flatChanged.get(scope.key))
|
|
329
422
|
continue;
|
|
423
|
+
refuseMerge(doc, priorBytes, scope.path);
|
|
330
424
|
const after = edited[scope.key] ?? [];
|
|
331
425
|
setStringSequencePreservingComments(doc, scope.path, after.length ? sortedUnique(after) : null);
|
|
332
426
|
}
|
|
@@ -341,6 +435,7 @@ export function renderFleetOverlayWrite(priorBytes, write) {
|
|
|
341
435
|
const [before, after] = listsOf(write, scope);
|
|
342
436
|
if (!changed(before, after))
|
|
343
437
|
continue;
|
|
438
|
+
refuseMerge(doc, priorBytes, scope.path);
|
|
344
439
|
setStringSequencePreservingComments(doc, scope.path, after.length ? sortedUnique(after) : null);
|
|
345
440
|
}
|
|
346
441
|
for (const shape of new Set([...Object.keys(initial.map), ...Object.keys(edited.map)])) {
|
|
@@ -530,8 +625,9 @@ export function renderFleetOverlayWrite(priorBytes, write) {
|
|
|
530
625
|
// byte-identical to yaml's own stringifyComment. OBS-1046: a scalar parsed from the prior bytes
|
|
531
626
|
// keeps the exact whitespace it had before its hash sign (yaml itself emits one space, so the
|
|
532
627
|
// sentinel carries the rest); only a comment fleet authored gets the two-space style.
|
|
628
|
+
// T10 (D-936): a flow collection's trailing note (`[codex] # c`) is inline too, so it keeps its gap.
|
|
533
629
|
visit(doc, (_key, node) => {
|
|
534
|
-
if (isScalar(node) && typeof node.comment === "string" && !node.comment.includes("\n")) {
|
|
630
|
+
if ((isScalar(node) || (isCollection(node) && node.flow)) && typeof node.comment === "string" && !node.comment.includes("\n")) {
|
|
535
631
|
const tail = node.range ? priorBytes.slice(node.range[1], node.range[2]) : "";
|
|
536
632
|
const gap = /^([ \t]+)#/.exec(tail)?.[1] ?? " ";
|
|
537
633
|
node.comment = `${INLINE_COMMENT_SENTINEL}${gap.slice(1)}#${node.comment}`;
|
|
@@ -547,6 +643,39 @@ export function renderFleetOverlayWrite(priorBytes, write) {
|
|
|
547
643
|
flowCollectionPadding: false,
|
|
548
644
|
});
|
|
549
645
|
}
|
|
646
|
+
/** T10: the 1-based line of the key that holds `path` in these overlay bytes — the path's own key, the
|
|
647
|
+
* key whose `null` masks it from above, or a `<<` merge key on it (D-987: the write there refuses) — else
|
|
648
|
+
* undefined (the overlay neither declares nor masks it). */
|
|
649
|
+
export function overlayHoldingLine(bytes, path) {
|
|
650
|
+
const doc = parseDocument(bytes);
|
|
651
|
+
if (doc.errors.length)
|
|
652
|
+
return undefined;
|
|
653
|
+
const lineOf = (offset) => bytes.slice(0, offset).split("\n").length;
|
|
654
|
+
let node = doc.contents;
|
|
655
|
+
let line;
|
|
656
|
+
for (const key of path) {
|
|
657
|
+
if (isAlias(node))
|
|
658
|
+
node = node.resolve(doc);
|
|
659
|
+
if (line !== undefined && isScalar(node) && node.value === null)
|
|
660
|
+
return line; // a mask at or above
|
|
661
|
+
if (!isMap(node))
|
|
662
|
+
return undefined;
|
|
663
|
+
const merge = node.items.find((item) => isScalar(item.key) && typeof item.key.value === "symbol");
|
|
664
|
+
const pair = merge ?? node.items.find((item) => isScalar(item.key) && String(item.key.value) === key);
|
|
665
|
+
if (!pair || !isScalar(pair.key) || !pair.key.range)
|
|
666
|
+
return undefined;
|
|
667
|
+
line = lineOf(pair.key.range[0]);
|
|
668
|
+
if (merge)
|
|
669
|
+
return line;
|
|
670
|
+
node = pair.value;
|
|
671
|
+
}
|
|
672
|
+
return line;
|
|
673
|
+
}
|
|
674
|
+
/** T10: does `path` hold a mapping in these overlay bytes — neither absent nor a `null` mask? */
|
|
675
|
+
export function overlayHoldsMap(bytes, path) {
|
|
676
|
+
const doc = parseDocument(bytes);
|
|
677
|
+
return !doc.errors.length && isMap(doc.getIn(path, true));
|
|
678
|
+
}
|
|
550
679
|
/** Build the repo overlay fragment fleet would write for edits since session start. */
|
|
551
680
|
export function fleetRepoOverlayFromDelta(initial, edited, existingRepo = {}, firstTouches = {}, universe) {
|
|
552
681
|
if (fleetEditableEquals(initial, edited))
|
package/dist/drivers/orca.d.ts
CHANGED
|
@@ -5,7 +5,7 @@ import { type ExecutorDriver, type FocusTarget, type FocusResult, type NotifyOpt
|
|
|
5
5
|
/** The response families the ONE shared envelope parser serves. There is no second JSON seam. */
|
|
6
6
|
export declare const ORCA_RESPONSE_FAMILIES: readonly ["status", "create", "list", "read", "send", "wait", "show", "close", "worktree-current", "worktree-set", "hooks-status", "split"];
|
|
7
7
|
export type OrcaFamily = (typeof ORCA_RESPONSE_FAMILIES)[number];
|
|
8
|
-
export declare const ORCA_FIXTURE_VERSION = "1.4.
|
|
8
|
+
export declare const ORCA_FIXTURE_VERSION = "1.4.218";
|
|
9
9
|
export declare const ORCA_CLI_COMMAND_ENV = "ORCA_CLI_COMMAND";
|
|
10
10
|
export declare const STALE_HANDLE_CODE = "terminal_handle_stale";
|
|
11
11
|
export declare const TERMINAL_GONE_CODE = "terminal_gone";
|
|
@@ -176,6 +176,17 @@ export interface OrcaDriverOpts {
|
|
|
176
176
|
probeStalenessMs?: number;
|
|
177
177
|
launchingHandle?: string;
|
|
178
178
|
}
|
|
179
|
+
/**
|
|
180
|
+
* F (D-832): what one proof page's OWN metadata says about the scrollback it was read from — the closed
|
|
181
|
+
* seam checkoutProven decides paging and absence by. `complete`: explicit `limited:false` and
|
|
182
|
+
* `truncated:false` with cursors that agree — nothing lies past this page. `partial`: explicit booleans,
|
|
183
|
+
* one of them true, cursors that agree — more scrollback exists. `unread`: everything else — a flag
|
|
184
|
+
* missing or not a boolean, a cursor that is not a string, `limited:false` with a next cursor short of
|
|
185
|
+
* the latest one (the contradictory "last" page), a blind stream page
|
|
186
|
+
* (isBlindStreamPage), or a source that is not the stream (`screen-unavailable`). Missing metadata is
|
|
187
|
+
* never inferred complete: its page's bytes still count as read evidence, but no absence rests on it.
|
|
188
|
+
*/
|
|
189
|
+
export declare function classifyProofPage(term: Record<string, unknown>): "complete" | "partial" | "unread";
|
|
179
190
|
export declare class OrcaDriver implements ExecutorDriver {
|
|
180
191
|
id: string;
|
|
181
192
|
interactive: boolean;
|
|
@@ -265,8 +276,11 @@ export declare class OrcaDriver implements ExecutorDriver {
|
|
|
265
276
|
* and none carries a trace of the mark; `unread` — every other exit: PROOF_PAGES reached, a limited
|
|
266
277
|
* read with no (or the same) next cursor or a truncated anchor without one, a read that threw, a read
|
|
267
278
|
* the ceiling cut off or never let start (the anchor included), or a page that landed late (even one
|
|
268
|
-
* carrying the right proof), a malformed page (its tail
|
|
269
|
-
*
|
|
279
|
+
* carrying the right proof), a malformed page (its tail or cursors), a blind stream page
|
|
280
|
+
* (isBlindStreamPage), or — F (D-832) — any page read whose paging metadata classifyProofPage cannot
|
|
281
|
+
* call complete or partial (flags missing, not boolean or contradicted; even when they were omitted,
|
|
282
|
+
* that page's bytes stay evidence and its matching proof still proves). Unread output is unknown —
|
|
283
|
+
* never an empty page, never absence.
|
|
270
284
|
* Without a deadline (reconcile, recovery) only `proven` is consumed.
|
|
271
285
|
*/
|
|
272
286
|
private checkoutProven;
|
package/dist/drivers/orca.js
CHANGED
|
@@ -44,7 +44,7 @@ export const ORCA_RESPONSE_FAMILIES = [
|
|
|
44
44
|
"status", "create", "list", "read", "send", "wait", "show", "close",
|
|
45
45
|
"worktree-current", "worktree-set", "hooks-status", "split",
|
|
46
46
|
];
|
|
47
|
-
export const ORCA_FIXTURE_VERSION = "1.4.
|
|
47
|
+
export const ORCA_FIXTURE_VERSION = "1.4.218";
|
|
48
48
|
export const ORCA_CLI_COMMAND_ENV = "ORCA_CLI_COMMAND";
|
|
49
49
|
export const STALE_HANDLE_CODE = "terminal_handle_stale";
|
|
50
50
|
export const TERMINAL_GONE_CODE = "terminal_gone";
|
|
@@ -608,6 +608,31 @@ export function isBlindStreamPage(term) {
|
|
|
608
608
|
&& Array.isArray(term.tail) && term.tail.length === 0
|
|
609
609
|
&& (term.returnedLineCount === undefined || term.returnedLineCount === 0);
|
|
610
610
|
}
|
|
611
|
+
/**
|
|
612
|
+
* F (D-832): what one proof page's OWN metadata says about the scrollback it was read from — the closed
|
|
613
|
+
* seam checkoutProven decides paging and absence by. `complete`: explicit `limited:false` and
|
|
614
|
+
* `truncated:false` with cursors that agree — nothing lies past this page. `partial`: explicit booleans,
|
|
615
|
+
* one of them true, cursors that agree — more scrollback exists. `unread`: everything else — a flag
|
|
616
|
+
* missing or not a boolean, a cursor that is not a string, `limited:false` with a next cursor short of
|
|
617
|
+
* the latest one (the contradictory "last" page), a blind stream page
|
|
618
|
+
* (isBlindStreamPage), or a source that is not the stream (`screen-unavailable`). Missing metadata is
|
|
619
|
+
* never inferred complete: its page's bytes still count as read evidence, but no absence rests on it.
|
|
620
|
+
*/
|
|
621
|
+
export function classifyProofPage(term) {
|
|
622
|
+
const { limited, truncated } = term;
|
|
623
|
+
if (typeof limited !== "boolean" || typeof truncated !== "boolean")
|
|
624
|
+
return "unread";
|
|
625
|
+
if (isBlindStreamPage(term) || (term.source !== undefined && term.source !== "stream"))
|
|
626
|
+
return "unread";
|
|
627
|
+
const next = str(term.nextCursor), latest = str(term.latestCursor);
|
|
628
|
+
if ((term.nextCursor !== undefined && next === undefined) || (term.latestCursor !== undefined && latest === undefined))
|
|
629
|
+
return "unread";
|
|
630
|
+
// a page that says it is the last while its next cursor stops short of the latest one contradicts itself.
|
|
631
|
+
// (A limited page whose next cursor IS the latest is not: the read from that cursor settles it.)
|
|
632
|
+
if (!limited && next !== undefined && latest !== undefined && next !== latest)
|
|
633
|
+
return "unread";
|
|
634
|
+
return limited || truncated ? "partial" : "complete";
|
|
635
|
+
}
|
|
611
636
|
export class OrcaDriver {
|
|
612
637
|
id = "orca";
|
|
613
638
|
interactive = true; // a visible terminal the operator can watch and answer
|
|
@@ -1021,12 +1046,17 @@ export class OrcaDriver {
|
|
|
1021
1046
|
* and none carries a trace of the mark; `unread` — every other exit: PROOF_PAGES reached, a limited
|
|
1022
1047
|
* read with no (or the same) next cursor or a truncated anchor without one, a read that threw, a read
|
|
1023
1048
|
* the ceiling cut off or never let start (the anchor included), or a page that landed late (even one
|
|
1024
|
-
* carrying the right proof), a malformed page (its tail
|
|
1025
|
-
*
|
|
1049
|
+
* carrying the right proof), a malformed page (its tail or cursors), a blind stream page
|
|
1050
|
+
* (isBlindStreamPage), or — F (D-832) — any page read whose paging metadata classifyProofPage cannot
|
|
1051
|
+
* call complete or partial (flags missing, not boolean or contradicted; even when they were omitted,
|
|
1052
|
+
* that page's bytes stay evidence and its matching proof still proves). Unread output is unknown —
|
|
1053
|
+
* never an empty page, never absence.
|
|
1026
1054
|
* Without a deadline (reconcile, recovery) only `proven` is consumed.
|
|
1027
1055
|
*/
|
|
1028
1056
|
async checkoutProven(handle, checkout, from, runtimeId, deadline) {
|
|
1029
|
-
|
|
1057
|
+
// F (D-832): a page read whose own metadata does not say how complete it is (classifyProofPage):
|
|
1058
|
+
// its bytes are evidence, but the scrollback it came from is not known to be read whole.
|
|
1059
|
+
let late = false, unknown = false;
|
|
1030
1060
|
const page = async (cursor) => {
|
|
1031
1061
|
const budget = deadline === undefined ? undefined : deadline - this.time.now();
|
|
1032
1062
|
if (budget !== undefined && budget <= 0)
|
|
@@ -1052,23 +1082,24 @@ export class OrcaDriver {
|
|
|
1052
1082
|
throw new OrcaError("read", `proof page names terminal ${str(term.handle) ?? "none"}, not the candidate ${handle}`, env.raw);
|
|
1053
1083
|
}
|
|
1054
1084
|
// E1: only a well-formed page is read text. A tail that is missing, not an array or carries a
|
|
1055
|
-
// non-string row (tailText), a cursor that is not a non-empty string
|
|
1056
|
-
//
|
|
1057
|
-
// page that would read as an absent proof.
|
|
1085
|
+
// non-string row (tailText), or a cursor that is not a non-empty string is a malformed page: it
|
|
1086
|
+
// throws, so it is unread output — never filtered to an empty page that would read as an absent proof.
|
|
1058
1087
|
for (const key of ["oldestCursor", "nextCursor"]) {
|
|
1059
1088
|
if (term[key] !== undefined && str(term[key]) === undefined)
|
|
1060
1089
|
throw new OrcaError("read", `proof page ${key} is not a cursor`, env.raw);
|
|
1061
1090
|
}
|
|
1062
|
-
for (const key of ["limited", "truncated"]) {
|
|
1063
|
-
if (term[key] !== undefined && typeof term[key] !== "boolean")
|
|
1064
|
-
throw new OrcaError("read", `proof page ${key} is not a boolean`, env.raw);
|
|
1065
|
-
}
|
|
1066
1091
|
// E1: the OBS-1011 add.1 blind stream (exited, empty tail, zero lines returned) is the shape a
|
|
1067
1092
|
// live terminal's stream answers while its screen still paints — and a dead terminal's too. The
|
|
1068
1093
|
// stream alone cannot tell them apart, so its empty tail is unread scrollback, never absence.
|
|
1069
1094
|
if (isBlindStreamPage(term))
|
|
1070
1095
|
throw new OrcaError("read", "proof page is a blind stream page (status exited, empty tail): its scrollback is unread", env.raw);
|
|
1071
|
-
|
|
1096
|
+
const text = this.tailText("read", term, env.raw);
|
|
1097
|
+
// F: paging flags missing, not boolean or contradicted by the cursors leave the page's bytes read
|
|
1098
|
+
// but its completeness unknown — it ends paging, and the poll can never read as absent.
|
|
1099
|
+
const kind = classifyProofPage(term);
|
|
1100
|
+
if (kind === "unread")
|
|
1101
|
+
unknown = true;
|
|
1102
|
+
return { term, text, kind };
|
|
1072
1103
|
};
|
|
1073
1104
|
// Every validated read is evidence, whatever stops the next one: the anchor's tail and the pages
|
|
1074
1105
|
// read from the oldest cursor are inspected even when paging hits PROOF_PAGES or a read throws.
|
|
@@ -1093,12 +1124,13 @@ export class OrcaDriver {
|
|
|
1093
1124
|
tail = first.text;
|
|
1094
1125
|
let cursor = str(anchor.oldestCursor);
|
|
1095
1126
|
// why paging ended with scrollback still unread. Every exit is one of: complete (an anchor that
|
|
1096
|
-
// is the whole scrollback, or a page
|
|
1127
|
+
// is the whole scrollback, or a page whose metadata says it is the last), whole frames found, a
|
|
1128
|
+
// page whose completeness is unknown (`unknown`, classifyProofPage), or one of these.
|
|
1097
1129
|
let stop;
|
|
1098
1130
|
if (cursor === undefined) {
|
|
1099
1131
|
text = tail;
|
|
1100
1132
|
// A cursorless anchor that says it holds less than the scrollback leaves the rest unreadable.
|
|
1101
|
-
if (
|
|
1133
|
+
if (first.kind === "partial")
|
|
1102
1134
|
stop = "stalled";
|
|
1103
1135
|
}
|
|
1104
1136
|
for (let n = 0; cursor !== undefined; n++) {
|
|
@@ -1116,12 +1148,23 @@ export class OrcaDriver {
|
|
|
1116
1148
|
const frames = checkoutFrames(text);
|
|
1117
1149
|
if (frames.complete.length > 0 && !frames.incomplete)
|
|
1118
1150
|
break; // whole frames, nothing dangling
|
|
1119
|
-
if (
|
|
1151
|
+
if (got.kind === "complete")
|
|
1120
1152
|
break; // the last page: the scrollback was read to its end
|
|
1121
1153
|
const next = str(term.nextCursor);
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1154
|
+
const advancing = next !== undefined && next !== cursor;
|
|
1155
|
+
if (got.kind === "unread") {
|
|
1156
|
+
// F: no absence rests on this page (`unknown`), but a cursor that still advances toward more
|
|
1157
|
+
// scrollback — the page says limited, or its next cursor is short of the latest one — is
|
|
1158
|
+
// followed for positive proof, inside PROOF_PAGES and the ceiling.
|
|
1159
|
+
if (advancing && (term.limited === true || next !== str(term.latestCursor))) {
|
|
1160
|
+
cursor = next;
|
|
1161
|
+
continue;
|
|
1162
|
+
}
|
|
1163
|
+
break;
|
|
1164
|
+
}
|
|
1165
|
+
// A partial page that is not limited, or a limited one with no next cursor or the same one, says
|
|
1166
|
+
// more scrollback exists that no read can reach: unread, never absent.
|
|
1167
|
+
if (term.limited !== true || !advancing) {
|
|
1125
1168
|
stop = "stalled";
|
|
1126
1169
|
break;
|
|
1127
1170
|
}
|
|
@@ -1139,9 +1182,10 @@ export class OrcaDriver {
|
|
|
1139
1182
|
return { proven: true, reason: "" };
|
|
1140
1183
|
const why = `${late ? "read past the ceiling, " : ""}${stop === "cut" ? "the ceiling cut off paging, "
|
|
1141
1184
|
: stop === "exhausted" ? `paging stopped at ${PROOF_PAGES} pages with scrollback unread, `
|
|
1142
|
-
: stop === "stalled" ? "a limited read gave no next cursor with scrollback unread, " : ""}`;
|
|
1185
|
+
: stop === "stalled" ? "a limited read gave no next cursor with scrollback unread, " : ""}${unknown ? "a page carried missing, non-boolean or contradictory paging metadata, " : ""}`;
|
|
1143
1186
|
const read = described(why);
|
|
1144
|
-
|
|
1187
|
+
// F: absent only when every page read said, explicitly and consistently, how complete it was.
|
|
1188
|
+
return { ...read, seen: read.seen ?? (stop || late || unknown ? "unread" : "absent") };
|
|
1145
1189
|
}
|
|
1146
1190
|
catch (error) {
|
|
1147
1191
|
// A read that failed midway keeps what was already read as evidence; what it could not read is
|
|
@@ -109,7 +109,8 @@ export declare function screenCostRatio(baseline: Baseline, selected: readonly s
|
|
|
109
109
|
/** A retry base no runner invocation parses. evaluateManifestedTest builds its stranded single-fork
|
|
110
110
|
* retry from the base it is handed, and one it cannot parse throws before any spawn — so this base
|
|
111
111
|
* disables that inner recovery: a worker-RPC-stranded re-observation comes back infra (the caller
|
|
112
|
-
* parks it as ambiguous) instead of launching a second execution.
|
|
112
|
+
* parks it as ambiguous) instead of launching a second execution. runVitestManifestGate reads it as
|
|
113
|
+
* the mark of a diagnostic subset, which never buys the outer timeout-shaped remeasurement either. */
|
|
113
114
|
export declare const REOBSERVATION_RETRY_BASE = "tickmarkr-reobservation-refuses-stranded-retry";
|
|
114
115
|
/** OBS-1106 residual: ONE isolated re-observation of a timeout-shaped red's attributed failing files on
|
|
115
116
|
* the same checkout, narrowed exactly as a screen is. Never cached and never a verdict: the caller
|