tickmarkr 2.6.4 → 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/catalog-remote.d.ts +23 -0
- package/dist/adapters/catalog-remote.js +38 -0
- package/dist/adapters/claude-code.d.ts +1 -1
- package/dist/adapters/claude-code.js +4 -1
- package/dist/adapters/codex-commit-check.d.ts +36 -0
- package/dist/adapters/codex-commit-check.js +205 -0
- package/dist/adapters/codex.d.ts +1 -0
- package/dist/adapters/codex.js +12 -6
- package/dist/adapters/model-discovery-check.d.ts +33 -0
- package/dist/adapters/model-discovery-check.js +123 -0
- package/dist/adapters/model-lints.d.ts +1 -1
- package/dist/adapters/model-lints.js +4 -0
- package/dist/adapters/prompt.js +3 -2
- package/dist/adapters/types.d.ts +10 -0
- package/dist/cli/commands/approve.js +10 -4
- package/dist/cli/commands/doctor.d.ts +2 -0
- package/dist/cli/commands/doctor.js +17 -3
- package/dist/cli/commands/fleet.d.ts +10 -1
- package/dist/cli/commands/fleet.js +363 -106
- package/dist/cli/commands/init.js +10 -3
- package/dist/cli/commands/plan.js +76 -4
- package/dist/cli/commands/verify.d.ts +1 -1
- package/dist/cli/commands/verify.js +65 -15
- package/dist/config/fleet-overlay.d.ts +16 -1
- package/dist/config/fleet-overlay.js +230 -28
- package/dist/drivers/orca.d.ts +34 -2
- package/dist/drivers/orca.js +206 -32
- package/dist/gates/cache.d.ts +4 -0
- package/dist/gates/cache.js +10 -2
- package/dist/gates/llm.d.ts +4 -1
- package/dist/gates/llm.js +6 -1
- package/dist/gates/review.js +8 -3
- package/dist/gates/run-gates.d.ts +5 -3
- package/dist/gates/run-gates.js +112 -56
- 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 +326 -130
- package/dist/run/git.js +9 -0
- package/dist/run/journal.d.ts +6 -2
- package/dist/run/journal.js +22 -7
- package/dist/run/launchservices-check.d.ts +56 -0
- package/dist/run/launchservices-check.js +117 -0
- package/dist/run/operator-state.d.ts +22 -2
- package/dist/run/operator-state.js +20 -3
- package/dist/run/title-environment.d.ts +45 -0
- package/dist/run/title-environment.js +127 -0
- package/dist/run/title-preload.cjs +38 -0
- package/dist/run/title-preload.d.cts +1 -0
- package/dist/tui/cockpit/board.js +12 -6
- package/dist/tui/cockpit/keys.d.ts +8 -0
- package/dist/tui/cockpit/keys.js +4 -1
- package/dist/tui/cockpit/layout.d.ts +2 -2
- package/dist/tui/cockpit/layout.js +2 -2
- package/dist/tui/cockpit/live-runtime.js +46 -20
- package/dist/tui/cockpit/live-store.d.ts +24 -1
- package/dist/tui/cockpit/live-store.js +99 -8
- package/dist/tui/cockpit/log-view.d.ts +28 -0
- package/dist/tui/cockpit/log-view.js +60 -0
- package/dist/tui/cockpit/run-view.js +4 -0
- package/dist/tui/cockpit/shell.d.ts +2 -1
- package/dist/tui/cockpit/shell.js +6 -4
- package/dist/tui/ink/fleet-app.d.ts +37 -10
- package/dist/tui/ink/fleet-app.js +65 -21
- package/package.json +1 -1
- package/skills/tickmarkr-auto/SKILL.md +32 -2
- package/skills/tickmarkr-loop/SKILL.md +46 -2
- package/skills/tickmarkr-overseer/SKILL.md +83 -8
- 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
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// Fleet-overlay mutation, serialization, and diff rendering for the `tickmarkr fleet` write path.
|
|
2
|
-
import { isMap, 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);
|
|
@@ -161,31 +170,150 @@ function setStringSequencePreservingComments(doc, path, values) {
|
|
|
161
170
|
// Total forms: a delete treats the masked subtree as absent (the operator's tombstone already
|
|
162
171
|
// denies it, and survives untouched); a set clears the scalar and rebuilds what fleet owns —
|
|
163
172
|
// the routing.allow precedent below.
|
|
173
|
+
// T8 repair: an alias is no tombstone — `spec: *empty` resolves to a mapping. Each alias on the path
|
|
174
|
+
// is replaced by an un-anchored copy of its target, so what it resolves to decides, and a write under
|
|
175
|
+
// it never edits the anchored node its other aliases share.
|
|
176
|
+
function materializeAliases(doc, path) {
|
|
177
|
+
for (let depth = 1; depth < path.length; depth++) {
|
|
178
|
+
const node = doc.getIn(path.slice(0, depth), true);
|
|
179
|
+
if (!isAlias(node))
|
|
180
|
+
continue;
|
|
181
|
+
const target = node.resolve(doc);
|
|
182
|
+
if (target === undefined)
|
|
183
|
+
return;
|
|
184
|
+
doc.setIn(path.slice(0, depth), detachedCopy(doc, node, target, new Set()));
|
|
185
|
+
}
|
|
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
|
+
}
|
|
211
|
+
const nodesOf = (root) => {
|
|
212
|
+
const out = [];
|
|
213
|
+
visit(root, (_key, node) => {
|
|
214
|
+
if (isNode(node))
|
|
215
|
+
out.push(node);
|
|
216
|
+
});
|
|
217
|
+
return out;
|
|
218
|
+
};
|
|
219
|
+
// D-821: a copy carries NO anchor (a nested `&empty` would rebind `plan: *empty` and every later
|
|
220
|
+
// alias to the edited copy), and each inner alias must still name what it named in the original —
|
|
221
|
+
// one whose anchor is redefined before `at` is replaced by a detached copy of its original target.
|
|
222
|
+
function detachedCopy(doc, at, target, expanding) {
|
|
223
|
+
if (expanding.has(target))
|
|
224
|
+
throw new Error(`fleet cannot detach the recursive YAML alias *${at.source}`);
|
|
225
|
+
const visible = new Map();
|
|
226
|
+
visit(doc, (_key, node) => {
|
|
227
|
+
if (node === at)
|
|
228
|
+
return visit.BREAK;
|
|
229
|
+
if (isNode(node) && !isAlias(node) && node.anchor)
|
|
230
|
+
visible.set(node.anchor, node);
|
|
231
|
+
});
|
|
232
|
+
const copy = target.clone();
|
|
233
|
+
const originals = nodesOf(target);
|
|
234
|
+
const replace = new Map();
|
|
235
|
+
nodesOf(copy).forEach((node, index) => {
|
|
236
|
+
if (!isAlias(node)) {
|
|
237
|
+
node.anchor = undefined;
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
const resolved = originals[index].resolve(doc);
|
|
241
|
+
if (resolved !== undefined && visible.get(node.source) !== resolved) {
|
|
242
|
+
replace.set(node, detachedCopy(doc, at, resolved, new Set([...expanding, target])));
|
|
243
|
+
}
|
|
244
|
+
});
|
|
245
|
+
if (!replace.size)
|
|
246
|
+
return copy;
|
|
247
|
+
visit(copy, (_key, node) => (isNode(node) ? replace.get(node) : undefined));
|
|
248
|
+
return copy;
|
|
249
|
+
}
|
|
250
|
+
function underScalar(doc, path) {
|
|
251
|
+
for (let depth = 1; depth < path.length; depth++) {
|
|
252
|
+
const node = doc.getIn(path.slice(0, depth), true);
|
|
253
|
+
if (node === undefined)
|
|
254
|
+
return false;
|
|
255
|
+
if (!isMap(node))
|
|
256
|
+
return true;
|
|
257
|
+
}
|
|
258
|
+
return false;
|
|
259
|
+
}
|
|
164
260
|
function deleteAt(doc, path) {
|
|
261
|
+
materializeAliases(doc, path); // an aliased mapping is no tombstone: Auto under `spec: *pinned` deletes the pin
|
|
165
262
|
for (let depth = 1; depth < path.length; depth++) {
|
|
166
263
|
if (!isMap(doc.getIn(path.slice(0, depth), true)))
|
|
167
264
|
return;
|
|
168
265
|
}
|
|
169
266
|
// deleteIn throws on an empty document — only delete keys that exist.
|
|
170
|
-
if (doc.getIn(path)
|
|
171
|
-
|
|
267
|
+
if (doc.getIn(path) === undefined)
|
|
268
|
+
return;
|
|
269
|
+
releaseAnchors(doc, path);
|
|
270
|
+
doc.deleteIn(path);
|
|
172
271
|
}
|
|
173
272
|
function setAt(doc, path, value) {
|
|
273
|
+
materializeAliases(doc, path); // edit a copy of an aliased mapping, never drop what it resolved to
|
|
274
|
+
releaseAnchors(doc, path);
|
|
174
275
|
for (let depth = 1; depth < path.length; depth++) {
|
|
175
276
|
const node = doc.getIn(path.slice(0, depth), true);
|
|
176
277
|
if (node === undefined)
|
|
177
278
|
break;
|
|
178
279
|
if (!isMap(node)) {
|
|
179
|
-
|
|
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
|
+
}
|
|
180
292
|
break;
|
|
181
293
|
}
|
|
182
294
|
}
|
|
183
295
|
doc.setIn(path, value);
|
|
184
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
|
+
}
|
|
185
313
|
function deleteEmptyMap(doc, path) {
|
|
186
314
|
const node = doc.getIn(path, true);
|
|
187
315
|
if (isMap(node) && node.items.length === 0)
|
|
188
|
-
doc
|
|
316
|
+
deleteAt(doc, path);
|
|
189
317
|
}
|
|
190
318
|
/** Apply only fields fleet authored to the parsed YAML document. Untouched nodes retain their
|
|
191
319
|
* keys, ordering, scalar style, and comments; fresh provenance is written directly on its tier node. */
|
|
@@ -201,24 +329,49 @@ export function renderFleetOverlayWrite(priorBytes, write) {
|
|
|
201
329
|
const flatChanged = new Map(FLAT_DENY_SCOPES.map((scope) => [scope.key, changed(...listsOf(write, scope))]));
|
|
202
330
|
if ([...flatChanged.values()].some(Boolean) || changed(initial.allowOut, edited.allowOut)) {
|
|
203
331
|
if (write.universe) {
|
|
332
|
+
refuseMerge(doc, priorBytes, ["routing", "allow"]);
|
|
204
333
|
// Membership write: the allow form IS the fleet; deny adapters/models scopes are tombstoned
|
|
205
334
|
// so a lower layer can never re-exclude behind the operator's back (workers untouched).
|
|
206
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
|
+
});
|
|
207
363
|
if (form.excluded) {
|
|
208
364
|
const allowNode = doc.getIn(["routing", "allow"], true);
|
|
209
365
|
if (allowNode !== undefined && !isMap(allowNode))
|
|
210
|
-
doc
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
}
|
|
220
|
-
else {
|
|
221
|
-
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);
|
|
222
375
|
}
|
|
223
376
|
// Whole fleet out: allow stays present but empty — fail-closed, nothing admitted.
|
|
224
377
|
if (doc.getIn(["routing", "allow"]) === undefined) {
|
|
@@ -226,8 +379,12 @@ export function renderFleetOverlayWrite(priorBytes, write) {
|
|
|
226
379
|
}
|
|
227
380
|
}
|
|
228
381
|
else {
|
|
229
|
-
// Whole fleet in: no restriction to express — the allow block goes away entirely
|
|
230
|
-
|
|
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"]);
|
|
231
388
|
}
|
|
232
389
|
// Authored and non-canonical entries stay in deny (LEG2-T3 finding 4, round 2 finding 2).
|
|
233
390
|
// OBS-1046: only an EDITED scope is rewritten, and only when its bytes must change — an
|
|
@@ -248,7 +405,8 @@ export function renderFleetOverlayWrite(priorBytes, write) {
|
|
|
248
405
|
const stale = authored.some((entry) => admitted.includes(entry));
|
|
249
406
|
if (!touched && !stale)
|
|
250
407
|
continue;
|
|
251
|
-
|
|
408
|
+
refuseMerge(doc, priorBytes, path);
|
|
409
|
+
const remaining = [...new Set([...flatDenyAfterWrite(doc, path, after, write.universe), ...after.filter(coversRetained)])];
|
|
252
410
|
if (remaining.length) {
|
|
253
411
|
if (remaining.join("\n") !== authored.join("\n"))
|
|
254
412
|
setStringSequencePreservingComments(doc, path, remaining);
|
|
@@ -262,6 +420,7 @@ export function renderFleetOverlayWrite(priorBytes, write) {
|
|
|
262
420
|
for (const scope of FLAT_DENY_SCOPES) {
|
|
263
421
|
if (!flatChanged.get(scope.key))
|
|
264
422
|
continue;
|
|
423
|
+
refuseMerge(doc, priorBytes, scope.path);
|
|
265
424
|
const after = edited[scope.key] ?? [];
|
|
266
425
|
setStringSequencePreservingComments(doc, scope.path, after.length ? sortedUnique(after) : null);
|
|
267
426
|
}
|
|
@@ -276,16 +435,25 @@ export function renderFleetOverlayWrite(priorBytes, write) {
|
|
|
276
435
|
const [before, after] = listsOf(write, scope);
|
|
277
436
|
if (!changed(before, after))
|
|
278
437
|
continue;
|
|
438
|
+
refuseMerge(doc, priorBytes, scope.path);
|
|
279
439
|
setStringSequencePreservingComments(doc, scope.path, after.length ? sortedUnique(after) : null);
|
|
280
440
|
}
|
|
281
441
|
for (const shape of new Set([...Object.keys(initial.map), ...Object.keys(edited.map)])) {
|
|
282
442
|
const before = initial.map[shape];
|
|
283
443
|
const after = edited.map[shape];
|
|
284
|
-
|
|
444
|
+
const pinPath = ["routing", "map", shape, "pin"];
|
|
445
|
+
const declares = after?.pin !== undefined || after?.pool !== undefined || after?.prefer !== undefined;
|
|
446
|
+
if (!declares && write.lowerMap?.[shape]?.pin !== undefined && JSON.stringify(before) !== JSON.stringify(after)) {
|
|
447
|
+
// pin: null tombstone — masks the lower-layer pin; a scalar tombstone above it already masks the entry
|
|
448
|
+
materializeAliases(doc, pinPath);
|
|
449
|
+
if (!underScalar(doc, pinPath))
|
|
450
|
+
setScalarPreservingComment(doc, pinPath, null);
|
|
451
|
+
}
|
|
452
|
+
else if (JSON.stringify(before?.pin) !== JSON.stringify(after?.pin)) {
|
|
285
453
|
if (after?.pin === undefined)
|
|
286
|
-
deleteAt(doc,
|
|
454
|
+
deleteAt(doc, pinPath);
|
|
287
455
|
else
|
|
288
|
-
setAt(doc,
|
|
456
|
+
setAt(doc, pinPath, doc.createNode(after.pin));
|
|
289
457
|
}
|
|
290
458
|
if (JSON.stringify(before?.pool) !== JSON.stringify(after?.pool)) {
|
|
291
459
|
const path = ["routing", "map", shape, "pool"];
|
|
@@ -457,8 +625,9 @@ export function renderFleetOverlayWrite(priorBytes, write) {
|
|
|
457
625
|
// byte-identical to yaml's own stringifyComment. OBS-1046: a scalar parsed from the prior bytes
|
|
458
626
|
// keeps the exact whitespace it had before its hash sign (yaml itself emits one space, so the
|
|
459
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.
|
|
460
629
|
visit(doc, (_key, node) => {
|
|
461
|
-
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")) {
|
|
462
631
|
const tail = node.range ? priorBytes.slice(node.range[1], node.range[2]) : "";
|
|
463
632
|
const gap = /^([ \t]+)#/.exec(tail)?.[1] ?? " ";
|
|
464
633
|
node.comment = `${INLINE_COMMENT_SENTINEL}${gap.slice(1)}#${node.comment}`;
|
|
@@ -474,6 +643,39 @@ export function renderFleetOverlayWrite(priorBytes, write) {
|
|
|
474
643
|
flowCollectionPadding: false,
|
|
475
644
|
});
|
|
476
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
|
+
}
|
|
477
679
|
/** Build the repo overlay fragment fleet would write for edits since session start. */
|
|
478
680
|
export function fleetRepoOverlayFromDelta(initial, edited, existingRepo = {}, firstTouches = {}, universe) {
|
|
479
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";
|
|
@@ -31,16 +31,18 @@ export declare class OrcaError extends Error {
|
|
|
31
31
|
readonly raw: string;
|
|
32
32
|
readonly code?: string;
|
|
33
33
|
readonly runtimeId?: string;
|
|
34
|
+
readonly launchCause?: SeatLaunchCause;
|
|
34
35
|
constructor(family: string, reason: string, raw: string, opts?: {
|
|
35
36
|
code?: string;
|
|
36
37
|
runtimeId?: string;
|
|
38
|
+
launchCause?: SeatLaunchCause;
|
|
37
39
|
});
|
|
38
40
|
}
|
|
39
41
|
/** The slot cannot be addressed: dead/unknown terminal record, or a handle that cannot be recovered
|
|
40
42
|
* to exactly one owned terminal in the slot's own worktree. Never a silent false or empty string. */
|
|
41
43
|
export declare class OrcaUnavailableError extends OrcaError {
|
|
42
44
|
readonly terminalStatus?: string | undefined;
|
|
43
|
-
constructor(family: string, reason: string, raw: string, terminalStatus?: string | undefined);
|
|
45
|
+
constructor(family: string, reason: string, raw: string, terminalStatus?: string | undefined, launchCause?: SeatLaunchCause);
|
|
44
46
|
}
|
|
45
47
|
export interface OrcaEnvelope {
|
|
46
48
|
result: Record<string, unknown>;
|
|
@@ -83,6 +85,10 @@ export declare function terminalWorktree(term: Record<string, unknown>): string
|
|
|
83
85
|
export declare function canonicalWorktreePath(path: string): string;
|
|
84
86
|
/** The proof line a worker terminal prints first: create, recovery and focus read it back. */
|
|
85
87
|
export declare const CHECKOUT_MARK = "TICKMARKR_CHECKOUT";
|
|
88
|
+
/** E1: the one typed launch cause a driver attaches to its launch failure. `checkout-proof-timeout`
|
|
89
|
+
* means the terminal printed NO proof frame by the ceiling — a slow host, not a misplaced seat. A
|
|
90
|
+
* foreign or malformed frame is placement evidence and carries no cause (never a timeout exemption). */
|
|
91
|
+
export type SeatLaunchCause = "checkout-proof-timeout";
|
|
86
92
|
/** The exact bytes the create command prints as its first line. */
|
|
87
93
|
export declare function checkoutProofLine(checkout: string): string;
|
|
88
94
|
/** Every complete frame in a scrollback, decoded and canonicalized; whether an incomplete one was seen. */
|
|
@@ -170,6 +176,17 @@ export interface OrcaDriverOpts {
|
|
|
170
176
|
probeStalenessMs?: number;
|
|
171
177
|
launchingHandle?: string;
|
|
172
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";
|
|
173
190
|
export declare class OrcaDriver implements ExecutorDriver {
|
|
174
191
|
id: string;
|
|
175
192
|
interactive: boolean;
|
|
@@ -250,6 +267,21 @@ export declare class OrcaDriver implements ExecutorDriver {
|
|
|
250
267
|
* that supplied the ownership listing — else another terminal's or another runtime's bytes were
|
|
251
268
|
* answered and nothing is proven. Proven means provesCheckout: exact canonical full-path equality
|
|
252
269
|
* of a complete frame, no other checkout named, no incomplete frame.
|
|
270
|
+
* E1: with a `deadline` (create's window) there is ONE absolute deadline: a read is issued only while
|
|
271
|
+
* budget is left and is handed exactly that budget, and a page that still lands past it is late —
|
|
272
|
+
* nothing late proves the checkout. `seen` is what the scrollback showed, for the typed timeout:
|
|
273
|
+
* `misplaced` — a frame naming another checkout, a cut frame or an unparseable mark in anything read
|
|
274
|
+
* (the anchor's tail and every page included, even when paging stopped or a later read threw);
|
|
275
|
+
* `absent` — ONLY when every page down to the start of the scrollback was read before the ceiling
|
|
276
|
+
* and none carries a trace of the mark; `unread` — every other exit: PROOF_PAGES reached, a limited
|
|
277
|
+
* read with no (or the same) next cursor or a truncated anchor without one, a read that threw, a read
|
|
278
|
+
* the ceiling cut off or never let start (the anchor included), or a page that landed late (even one
|
|
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.
|
|
284
|
+
* Without a deadline (reconcile, recovery) only `proven` is consumed.
|
|
253
285
|
*/
|
|
254
286
|
private checkoutProven;
|
|
255
287
|
private recover;
|