tickmarkr 2.5.4 → 2.5.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 +3 -1
- package/dist/adapters/registry.js +6 -1
- package/dist/adapters/types.d.ts +3 -0
- package/dist/cli/commands/approve.d.ts +1 -0
- package/dist/cli/commands/approve.js +118 -9
- package/dist/cli/commands/doctor.d.ts +10 -0
- package/dist/cli/commands/doctor.js +44 -29
- package/dist/cli/commands/fleet.js +199 -33
- package/dist/cli/commands/init.js +196 -6
- package/dist/cli/commands/plan.js +20 -4
- package/dist/cli/commands/resume.js +4 -2
- package/dist/cli/commands/run.js +11 -2
- package/dist/cli/commands/verify.js +102 -81
- package/dist/cli/help.d.ts +2 -0
- package/dist/cli/help.js +3 -1
- package/dist/config/config.d.ts +25 -8
- package/dist/config/config.js +41 -29
- package/dist/config/fleet-overlay.d.ts +3 -9
- package/dist/config/fleet-overlay.js +114 -19
- package/dist/config/fleet-why.d.ts +7 -0
- package/dist/config/fleet-why.js +5 -0
- package/dist/drivers/index.d.ts +15 -1
- package/dist/drivers/index.js +39 -10
- package/dist/drivers/orca.d.ts +119 -10
- package/dist/drivers/orca.js +781 -110
- package/dist/gates/baseline.d.ts +17 -3
- package/dist/gates/baseline.js +63 -15
- package/dist/gates/cache.d.ts +101 -0
- package/dist/gates/cache.js +401 -0
- package/dist/gates/llm.d.ts +3 -0
- package/dist/gates/llm.js +11 -0
- package/dist/gates/review.d.ts +28 -3
- package/dist/gates/review.js +106 -14
- package/dist/gates/run-gates.d.ts +9 -1
- package/dist/gates/run-gates.js +407 -102
- package/dist/gates/test-manifest.d.ts +128 -0
- package/dist/gates/test-manifest.js +463 -0
- package/dist/gates/test-reporter.d.ts +4 -0
- package/dist/gates/test-reporter.js +57 -0
- package/dist/graph/graph.d.ts +2 -0
- package/dist/graph/graph.js +45 -1
- package/dist/route/preference.d.ts +22 -1
- package/dist/route/preference.js +123 -25
- package/dist/route/router.js +31 -6
- package/dist/run/consult.js +5 -4
- package/dist/run/daemon.d.ts +15 -0
- package/dist/run/daemon.js +638 -167
- package/dist/run/execution-budget.d.ts +25 -0
- package/dist/run/execution-budget.js +142 -0
- package/dist/run/git.d.ts +50 -1
- package/dist/run/git.js +131 -12
- package/dist/run/journal.d.ts +16 -2
- package/dist/run/journal.js +115 -25
- package/dist/run/lease.d.ts +58 -0
- package/dist/run/lease.js +310 -0
- package/dist/run/merge.d.ts +2 -0
- package/dist/run/merge.js +91 -3
- package/dist/run/operator-state.d.ts +11 -0
- package/dist/run/operator-state.js +17 -3
- package/dist/run/recovery.d.ts +8 -0
- package/dist/run/recovery.js +25 -0
- package/dist/run/repair-selection.d.ts +12 -0
- package/dist/run/repair-selection.js +56 -0
- package/dist/run/stall.d.ts +6 -1
- package/dist/run/stall.js +60 -3
- package/dist/tui/cockpit/board.d.ts +96 -0
- package/dist/tui/cockpit/board.js +346 -0
- package/dist/tui/cockpit/decision-actions.js +2 -0
- package/dist/tui/cockpit/layout.d.ts +5 -1
- package/dist/tui/cockpit/layout.js +8 -3
- package/dist/tui/cockpit/live-runtime.js +71 -21
- package/dist/tui/cockpit/run-view.d.ts +7 -5
- package/dist/tui/cockpit/run-view.js +12 -11
- package/dist/tui/ink/fleet-app.d.ts +49 -27
- package/dist/tui/ink/fleet-app.js +229 -38
- package/package.json +2 -2
- package/skills/tickmarkr-overseer/SKILL.md +173 -113
- package/skills/tickmarkr-overseer/scripts/grade-ci.sh +29 -2
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// Fleet-overlay mutation, serialization, and diff rendering for the `tickmarkr fleet` write path.
|
|
2
2
|
import { isMap, isScalar, isSeq, parseDocument, stringify, visit } from "yaml";
|
|
3
|
-
import { universeCovers } from "./config.js";
|
|
3
|
+
import { universeCovers, universeEntryMatches } from "./config.js";
|
|
4
4
|
/** Fleet-owned overlay keys — the only config surface `tickmarkr fleet` may write. */
|
|
5
5
|
export const FLEET_OVERLAY_KEYS = ["routing", "tiers"];
|
|
6
6
|
function fleetSubset(obj) {
|
|
@@ -21,17 +21,19 @@ function allowFormFromExclusions(universe, edited) {
|
|
|
21
21
|
if (!universe.length) {
|
|
22
22
|
throw new Error("fleet write: universe is empty — no classified models to compute routing.allow from; classify models in `tickmarkr fleet` first");
|
|
23
23
|
}
|
|
24
|
-
|
|
25
|
-
|
|
24
|
+
// LEG2-T3 round 2 finding 1: every staged entry excludes what it NAMES — a bare model id every
|
|
25
|
+
// adapter serving it, an identity its alias — never only an adapter id or an adapter:model key.
|
|
26
|
+
// OBS-1046: the staged allow complement is a reason of its own, beside the authored deny lists.
|
|
27
|
+
const entries = [...edited.denyAdapters, ...edited.denyModels, ...(edited.allowOut ?? [])];
|
|
26
28
|
const adapters = [];
|
|
27
29
|
const models = [];
|
|
28
30
|
let excluded = false;
|
|
29
31
|
for (const row of universe) {
|
|
30
|
-
if (
|
|
32
|
+
if (entries.includes(row.adapter)) {
|
|
31
33
|
excluded = true;
|
|
32
34
|
continue;
|
|
33
35
|
}
|
|
34
|
-
const inFleet = row.models.filter((m) => !
|
|
36
|
+
const inFleet = row.models.filter((m) => !entries.some((entry) => universeEntryMatches(row, m, entry)));
|
|
35
37
|
if (inFleet.length === row.models.length) {
|
|
36
38
|
adapters.push(row.adapter);
|
|
37
39
|
}
|
|
@@ -49,6 +51,24 @@ function allowFormFromExclusions(universe, edited) {
|
|
|
49
51
|
function residualDeny(universe, entries) {
|
|
50
52
|
return sortedUnique(entries.filter((entry) => !universeCovers(universe, entry)));
|
|
51
53
|
}
|
|
54
|
+
// LEG2-T3 round 2 finding 2: the flat deny list a membership write leaves behind. Every entry the
|
|
55
|
+
// repo overlay authored in THIS list that is still staged stays verbatim, in its authored order and
|
|
56
|
+
// with its comments — one cleared reason never takes an independent one with it, and an untouched
|
|
57
|
+
// list keeps its node. A staged entry the allow form cannot express as a membership key (outside the
|
|
58
|
+
// probe universe, or a bare-model/identity spelling) is written verbatim too; canonical keys the
|
|
59
|
+
// session added ride the allow form alone.
|
|
60
|
+
function authoredEntries(doc, path) {
|
|
61
|
+
const node = doc.getIn(path, true);
|
|
62
|
+
return isSeq(node) ? node.items.flatMap((item) => (isScalar(item) ? [String(item.value)] : [])) : [];
|
|
63
|
+
}
|
|
64
|
+
function flatDenyAfterWrite(doc, path, after, universe) {
|
|
65
|
+
const authored = authoredEntries(doc, path);
|
|
66
|
+
const canonical = (entry) => universe.some((row) => entry === row.adapter || row.models.some((m) => entry === `${row.adapter}:${m}`));
|
|
67
|
+
const kept = authored.filter((entry) => after.includes(entry));
|
|
68
|
+
const verbatim = sortedUnique(after.filter((entry) => !kept.includes(entry)
|
|
69
|
+
&& (!universeCovers(universe, entry) || !canonical(entry))));
|
|
70
|
+
return [...new Set([...kept, ...verbatim])];
|
|
71
|
+
}
|
|
52
72
|
// fleet.ts deliberately remains the sole overlay builder and writer. Its established classification
|
|
53
73
|
// seam copies only `tier` and `note` into FleetEditable, so first-touch entry metadata rides inside a
|
|
54
74
|
// private provenance envelope until this module writes the YAML. The envelope never reaches disk.
|
|
@@ -167,9 +187,12 @@ export function renderFleetOverlayWrite(priorBytes, write) {
|
|
|
167
187
|
if (doc.errors.length)
|
|
168
188
|
throw doc.errors[0];
|
|
169
189
|
const { initial, edited } = write;
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
190
|
+
// OBS-1046: each flat scope (and the allow complement) is compared on its own — a scope the
|
|
191
|
+
// session never edited keeps its node byte for byte, whatever its sibling did.
|
|
192
|
+
const changed = (before = [], after = []) => sortedUnique(before).join() !== sortedUnique(after).join();
|
|
193
|
+
const adaptersChanged = changed(initial.denyAdapters, edited.denyAdapters);
|
|
194
|
+
const modelsChanged = changed(initial.denyModels, edited.denyModels);
|
|
195
|
+
if (adaptersChanged || modelsChanged || changed(initial.allowOut, edited.allowOut)) {
|
|
173
196
|
if (write.universe) {
|
|
174
197
|
// Membership write: the allow form IS the fleet; deny adapters/models scopes are tombstoned
|
|
175
198
|
// so a lower layer can never re-exclude behind the operator's back (workers untouched).
|
|
@@ -199,18 +222,61 @@ export function renderFleetOverlayWrite(priorBytes, write) {
|
|
|
199
222
|
// Whole fleet in: no restriction to express — the allow block goes away entirely.
|
|
200
223
|
deleteAt(doc, ["routing", "allow"]);
|
|
201
224
|
}
|
|
202
|
-
//
|
|
225
|
+
// Authored and non-canonical entries stay in deny (LEG2-T3 finding 4, round 2 finding 2).
|
|
226
|
+
// OBS-1046: only an EDITED scope is rewritten, and only when its bytes must change — an
|
|
227
|
+
// addition the allow form carries leaves the list (an explicit `[]` included) untouched; a
|
|
228
|
+
// scope the press CLEARED down to nothing is tombstoned so a lower layer can never
|
|
203
229
|
// re-exclude behind the operator's back (workers untouched).
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
230
|
+
// LEG2-T3 finding 4: an authored entry the edit admits (staged nowhere any more) must go
|
|
231
|
+
// even from a scope whose own set did not change, or the admitted channel stays excluded
|
|
232
|
+
// behind the preserved bytes.
|
|
233
|
+
const stagedAfter = new Set([...edited.denyAdapters, ...edited.denyModels, ...(edited.allowOut ?? [])]);
|
|
234
|
+
const admitted = [...initial.denyAdapters, ...initial.denyModels, ...(initial.allowOut ?? [])]
|
|
235
|
+
.filter((entry) => !stagedAfter.has(entry));
|
|
236
|
+
const scopes = [
|
|
237
|
+
["adapters", initial.denyAdapters, edited.denyAdapters, adaptersChanged],
|
|
238
|
+
["models", initial.denyModels, edited.denyModels, modelsChanged],
|
|
239
|
+
];
|
|
240
|
+
for (const [scope, before, after, touched] of scopes) {
|
|
241
|
+
const path = ["routing", "deny", scope];
|
|
242
|
+
const authored = authoredEntries(doc, path);
|
|
243
|
+
const stale = authored.some((entry) => admitted.includes(entry));
|
|
244
|
+
if (!touched && !stale)
|
|
245
|
+
continue;
|
|
246
|
+
const remaining = flatDenyAfterWrite(doc, path, after, write.universe);
|
|
247
|
+
if (remaining.length) {
|
|
248
|
+
if (remaining.join("\n") !== authored.join("\n"))
|
|
249
|
+
setStringSequencePreservingComments(doc, path, remaining);
|
|
250
|
+
}
|
|
251
|
+
else if (stale || before.some((entry) => !after.includes(entry))) {
|
|
252
|
+
setStringSequencePreservingComments(doc, path, null);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
208
255
|
}
|
|
209
256
|
else {
|
|
210
|
-
|
|
211
|
-
|
|
257
|
+
if (adaptersChanged) {
|
|
258
|
+
setStringSequencePreservingComments(doc, ["routing", "deny", "adapters"], edited.denyAdapters.length ? sortedUnique(edited.denyAdapters) : null);
|
|
259
|
+
}
|
|
260
|
+
if (modelsChanged) {
|
|
261
|
+
setStringSequencePreservingComments(doc, ["routing", "deny", "models"], edited.denyModels.length ? sortedUnique(edited.denyModels) : null);
|
|
262
|
+
}
|
|
212
263
|
}
|
|
213
264
|
}
|
|
265
|
+
// OBS-994/FL-1: routing.deny.workers is a literal deny list, never a universe-derived
|
|
266
|
+
// membership scope — it never routes through the allow-complement dance above, in either
|
|
267
|
+
// branch. Same tombstone/comment-preserving rules as the flat scopes.
|
|
268
|
+
const initialWorkersAdapters = initial.denyWorkersAdapters ?? [];
|
|
269
|
+
const editedWorkersAdapters = edited.denyWorkersAdapters ?? [];
|
|
270
|
+
const initialWorkersModels = initial.denyWorkersModels ?? [];
|
|
271
|
+
const editedWorkersModels = edited.denyWorkersModels ?? [];
|
|
272
|
+
// Each sub-path mutates independently — an untouched sibling must not be rewritten (a `null`
|
|
273
|
+
// tombstone over an absent/untouched sibling would mask a lower layer's own workers scope).
|
|
274
|
+
if (sortedUnique(initialWorkersAdapters).join() !== sortedUnique(editedWorkersAdapters).join()) {
|
|
275
|
+
setStringSequencePreservingComments(doc, ["routing", "deny", "workers", "adapters"], editedWorkersAdapters.length ? sortedUnique(editedWorkersAdapters) : null);
|
|
276
|
+
}
|
|
277
|
+
if (sortedUnique(initialWorkersModels).join() !== sortedUnique(editedWorkersModels).join()) {
|
|
278
|
+
setStringSequencePreservingComments(doc, ["routing", "deny", "workers", "models"], editedWorkersModels.length ? sortedUnique(editedWorkersModels) : null);
|
|
279
|
+
}
|
|
214
280
|
for (const shape of new Set([...Object.keys(initial.map), ...Object.keys(edited.map)])) {
|
|
215
281
|
const before = initial.map[shape];
|
|
216
282
|
const after = edited.map[shape];
|
|
@@ -327,15 +393,19 @@ export function renderFleetOverlayWrite(priorBytes, write) {
|
|
|
327
393
|
// operator reviews. commentString has no position context, but this writer owns the document:
|
|
328
394
|
// scalar-trailing single-line comments (the only inline form fleet emits) are marked with a
|
|
329
395
|
// private-use sentinel (the FIRST_TOUCH envelope precedent above), everything else renders
|
|
330
|
-
// byte-identical to yaml's own stringifyComment.
|
|
396
|
+
// byte-identical to yaml's own stringifyComment. OBS-1046: a scalar parsed from the prior bytes
|
|
397
|
+
// keeps the exact whitespace it had before its hash sign (yaml itself emits one space, so the
|
|
398
|
+
// sentinel carries the rest); only a comment fleet authored gets the two-space style.
|
|
331
399
|
visit(doc, (_key, node) => {
|
|
332
400
|
if (isScalar(node) && typeof node.comment === "string" && !node.comment.includes("\n")) {
|
|
333
|
-
node.
|
|
401
|
+
const tail = node.range ? priorBytes.slice(node.range[1], node.range[2]) : "";
|
|
402
|
+
const gap = /^([ \t]+)#/.exec(tail)?.[1] ?? " ";
|
|
403
|
+
node.comment = `${INLINE_COMMENT_SENTINEL}${gap.slice(1)}#${node.comment}`;
|
|
334
404
|
}
|
|
335
405
|
});
|
|
336
406
|
return doc.toString({
|
|
337
407
|
commentString: (comment) => comment.startsWith(INLINE_COMMENT_SENTINEL)
|
|
338
|
-
?
|
|
408
|
+
? comment.slice(1)
|
|
339
409
|
: comment.replace(/^(?!$)(?: $)?/gm, "#"),
|
|
340
410
|
// OBS-518: yaml's default pads flow collections (`[kimi]` → `[ kimi ]`), churning untouched
|
|
341
411
|
// lines on the one confirmation surface an operator reviews. Hand-written overlays use the
|
|
@@ -351,7 +421,8 @@ export function fleetRepoOverlayFromDelta(initial, edited, existingRepo = {}, fi
|
|
|
351
421
|
const routing = { ...out.routing };
|
|
352
422
|
let routingTouched = false;
|
|
353
423
|
const denyChanged = sortedUnique(initial.denyAdapters).join() !== sortedUnique(edited.denyAdapters).join()
|
|
354
|
-
|| sortedUnique(initial.denyModels).join() !== sortedUnique(edited.denyModels).join()
|
|
424
|
+
|| sortedUnique(initial.denyModels).join() !== sortedUnique(edited.denyModels).join()
|
|
425
|
+
|| sortedUnique(initial.allowOut ?? []).join() !== sortedUnique(edited.allowOut ?? []).join();
|
|
355
426
|
if (denyChanged) {
|
|
356
427
|
if (universe) {
|
|
357
428
|
const form = allowFormFromExclusions(universe, edited);
|
|
@@ -379,6 +450,30 @@ export function fleetRepoOverlayFromDelta(initial, edited, existingRepo = {}, fi
|
|
|
379
450
|
}
|
|
380
451
|
routingTouched = true;
|
|
381
452
|
}
|
|
453
|
+
// OBS-994/FL-1: workers deny is a literal list, independent of the universe/allow dance above.
|
|
454
|
+
// Each sub-path is included only when it actually changed — an untouched sibling must not be
|
|
455
|
+
// rewritten as a `null` tombstone over whatever the existing repo overlay already held.
|
|
456
|
+
const initialWorkersAdapters = initial.denyWorkersAdapters ?? [];
|
|
457
|
+
const editedWorkersAdapters = edited.denyWorkersAdapters ?? [];
|
|
458
|
+
const initialWorkersModels = initial.denyWorkersModels ?? [];
|
|
459
|
+
const editedWorkersModels = edited.denyWorkersModels ?? [];
|
|
460
|
+
const workersAdaptersChanged = sortedUnique(initialWorkersAdapters).join() !== sortedUnique(editedWorkersAdapters).join();
|
|
461
|
+
const workersModelsChanged = sortedUnique(initialWorkersModels).join() !== sortedUnique(editedWorkersModels).join();
|
|
462
|
+
if (workersAdaptersChanged || workersModelsChanged) {
|
|
463
|
+
routing.deny = {
|
|
464
|
+
...routing.deny,
|
|
465
|
+
workers: {
|
|
466
|
+
...(routing.deny?.workers),
|
|
467
|
+
...(workersAdaptersChanged
|
|
468
|
+
? { adapters: editedWorkersAdapters.length ? editedWorkersAdapters : null }
|
|
469
|
+
: {}),
|
|
470
|
+
...(workersModelsChanged
|
|
471
|
+
? { models: editedWorkersModels.length ? editedWorkersModels : null }
|
|
472
|
+
: {}),
|
|
473
|
+
},
|
|
474
|
+
};
|
|
475
|
+
routingTouched = true;
|
|
476
|
+
}
|
|
382
477
|
// pool widened to accept the null tombstone; MapEntry itself never carries null in memory.
|
|
383
478
|
const mapDelta = {};
|
|
384
479
|
for (const shape of new Set([...Object.keys(initial.map), ...Object.keys(edited.map)])) {
|
|
@@ -21,3 +21,10 @@ export type FleetWhyOptions = {
|
|
|
21
21
|
export declare function projectFleetWhy<Id extends string>(values: readonly FleetWhyValue<Id>[], options: FleetWhyOptions): FleetWhyRow<Id>[];
|
|
22
22
|
/** Plain line-mode twin of the Shapes rows; labels are projected, never reconstructed here. */
|
|
23
23
|
export declare function renderFleetWhy(rows: readonly FleetWhyRow[]): string;
|
|
24
|
+
/** LEG2-T3: one exclusion-collector scope as the reason a fleet row shows — its config path, then
|
|
25
|
+
* the entry that matched (a deny) or the fact the allowlist does not admit the channel. */
|
|
26
|
+
export declare function exclusionReason(scope: {
|
|
27
|
+
by: "deny" | "allow";
|
|
28
|
+
configPath: string;
|
|
29
|
+
entry: string;
|
|
30
|
+
}): string;
|
package/dist/config/fleet-why.js
CHANGED
|
@@ -40,3 +40,8 @@ export function projectFleetWhy(values, options) {
|
|
|
40
40
|
export function renderFleetWhy(rows) {
|
|
41
41
|
return ["tickmarkr fleet --why — effective shape routing", ...rows.map((row) => row.label)].join("\n");
|
|
42
42
|
}
|
|
43
|
+
/** LEG2-T3: one exclusion-collector scope as the reason a fleet row shows — its config path, then
|
|
44
|
+
* the entry that matched (a deny) or the fact the allowlist does not admit the channel. */
|
|
45
|
+
export function exclusionReason(scope) {
|
|
46
|
+
return scope.by === "allow" ? `${scope.configPath} (not admitted)` : `${scope.configPath} (${scope.entry})`;
|
|
47
|
+
}
|
package/dist/drivers/index.d.ts
CHANGED
|
@@ -2,6 +2,20 @@ import type { TickmarkrConfig } from "../config/config.js";
|
|
|
2
2
|
import type { ExecutorDriver } from "./types.js";
|
|
3
3
|
export declare const DRIVER_CHOICES: readonly ["auto", "herdr", "subprocess", "orca"];
|
|
4
4
|
export type DriverChoice = (typeof DRIVER_CHOICES)[number];
|
|
5
|
+
export type ClassifiedHost = "herdr" | "orca" | "none";
|
|
6
|
+
/**
|
|
7
|
+
* One host classifier beside the driver chooser reads the launching environment:
|
|
8
|
+
* herdr only when HERDR_ENV is exactly 1, orca only when both Orca markers are present,
|
|
9
|
+
* none otherwise. run and resume call it once at the refs-preflight point and thread the
|
|
10
|
+
* result into pickDriver; nothing downstream reads the markers again.
|
|
11
|
+
*/
|
|
12
|
+
export declare function classifyHost(env?: NodeJS.ProcessEnv): ClassifiedHost;
|
|
13
|
+
/**
|
|
14
|
+
* A config driver of herdr or orca whose host is not the classified one is refused naming the
|
|
15
|
+
* detected host, the config line and the --driver remedy. Any explicit --driver value bypasses
|
|
16
|
+
* this; auto and subprocess are never refused.
|
|
17
|
+
*/
|
|
18
|
+
export declare function preflightHostDriver(cfg: TickmarkrConfig, driverOverride: string | undefined, host: ClassifiedHost): void;
|
|
5
19
|
/**
|
|
6
20
|
* Orca authors both markers on every terminal it creates. Requiring the pair avoids treating an
|
|
7
21
|
* unrelated TERM_PROGRAM value or a copied terminal handle as host identity. This is deliberately
|
|
@@ -11,4 +25,4 @@ export declare function orcaHostDetected(env?: NodeJS.ProcessEnv): boolean;
|
|
|
11
25
|
/** Validate argv at the CLI boundary rather than casting an arbitrary string into a driver choice. */
|
|
12
26
|
export declare function parseDriverOverride(override?: string): DriverChoice | undefined;
|
|
13
27
|
export declare function driverEvidence(cfg: TickmarkrConfig, driver: ExecutorDriver, override?: string): string;
|
|
14
|
-
export declare function pickDriver(cfg: TickmarkrConfig, override?: string): ExecutorDriver;
|
|
28
|
+
export declare function pickDriver(cfg: TickmarkrConfig, override?: string, host?: ClassifiedHost): ExecutorDriver;
|
package/dist/drivers/index.js
CHANGED
|
@@ -3,13 +3,43 @@ import { OrcaDriver } from "./orca.js";
|
|
|
3
3
|
import { SubprocessDriver } from "./subprocess.js";
|
|
4
4
|
export const DRIVER_CHOICES = ["auto", "herdr", "subprocess", "orca"];
|
|
5
5
|
const overrideByDriver = new WeakMap();
|
|
6
|
+
// The host snapshot the driver was selected under: driverEvidence reads this, never process.env,
|
|
7
|
+
// so preflight, selection and the journal row all describe the same instant.
|
|
8
|
+
const hostByDriver = new WeakMap();
|
|
9
|
+
/**
|
|
10
|
+
* One host classifier beside the driver chooser reads the launching environment:
|
|
11
|
+
* herdr only when HERDR_ENV is exactly 1, orca only when both Orca markers are present,
|
|
12
|
+
* none otherwise. run and resume call it once at the refs-preflight point and thread the
|
|
13
|
+
* result into pickDriver; nothing downstream reads the markers again.
|
|
14
|
+
*/
|
|
15
|
+
export function classifyHost(env = process.env) {
|
|
16
|
+
if (env.HERDR_ENV === "1")
|
|
17
|
+
return "herdr";
|
|
18
|
+
// A whitespace-only handle is no handle: the narrator trims it and would refuse the split.
|
|
19
|
+
if (env.TERM_PROGRAM === "Orca" && (env.ORCA_TERMINAL_HANDLE ?? "").trim() !== "")
|
|
20
|
+
return "orca";
|
|
21
|
+
return "none";
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* A config driver of herdr or orca whose host is not the classified one is refused naming the
|
|
25
|
+
* detected host, the config line and the --driver remedy. Any explicit --driver value bypasses
|
|
26
|
+
* this; auto and subprocess are never refused.
|
|
27
|
+
*/
|
|
28
|
+
export function preflightHostDriver(cfg, driverOverride, host) {
|
|
29
|
+
if (driverOverride !== undefined)
|
|
30
|
+
return;
|
|
31
|
+
if ((cfg.driver === "herdr" || cfg.driver === "orca") && cfg.driver !== host) {
|
|
32
|
+
const remedy = host === "none" ? "subprocess" : host;
|
|
33
|
+
throw new Error(`refusing driver '${cfg.driver}' (config line 'driver: ${cfg.driver}'): detected host is ${host}; use --driver ${remedy} to override`);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
6
36
|
/**
|
|
7
37
|
* Orca authors both markers on every terminal it creates. Requiring the pair avoids treating an
|
|
8
38
|
* unrelated TERM_PROGRAM value or a copied terminal handle as host identity. This is deliberately
|
|
9
39
|
* environment-only: selection must not execute a binary or contact the Orca runtime.
|
|
10
40
|
*/
|
|
11
41
|
export function orcaHostDetected(env = process.env) {
|
|
12
|
-
return env
|
|
42
|
+
return classifyHost(env) === "orca";
|
|
13
43
|
}
|
|
14
44
|
/** Validate argv at the CLI boundary rather than casting an arbitrary string into a driver choice. */
|
|
15
45
|
export function parseDriverOverride(override) {
|
|
@@ -27,18 +57,16 @@ export function driverEvidence(cfg, driver, override) {
|
|
|
27
57
|
return `${driver.id} (--driver)`;
|
|
28
58
|
if (want !== "auto")
|
|
29
59
|
return `${driver.id} (config)`;
|
|
30
|
-
const
|
|
31
|
-
if (
|
|
60
|
+
const host = hostByDriver.get(driver);
|
|
61
|
+
if (host === "herdr" && driver.id === "herdr")
|
|
32
62
|
return "auto → herdr (HERDR_ENV=1)";
|
|
33
|
-
if (
|
|
63
|
+
if (host === "orca" && driver.id === "orca")
|
|
34
64
|
return "auto → orca (TERM_PROGRAM+ORCA_TERMINAL_HANDLE)";
|
|
35
|
-
|
|
36
|
-
if (!herdrAvailable && !orcaHostDetected() && driver.id === "subprocess") {
|
|
65
|
+
if (host === "none" && driver.id === "subprocess")
|
|
37
66
|
return "auto → subprocess (HERDR_ENV unset)";
|
|
38
|
-
}
|
|
39
67
|
return `auto → ${driver.id} (runtime)`;
|
|
40
68
|
}
|
|
41
|
-
export function pickDriver(cfg, override) {
|
|
69
|
+
export function pickDriver(cfg, override, host = classifyHost()) {
|
|
42
70
|
const selectedOverride = parseDriverOverride(override);
|
|
43
71
|
const want = selectedOverride ?? cfg.driver;
|
|
44
72
|
// VIS-09 item 2: plumb the per-tab cap into the HerdrDriver — the driver takes it as a constructor
|
|
@@ -49,10 +77,11 @@ export function pickDriver(cfg, override) {
|
|
|
49
77
|
// Orca is an operator-selected execution surface. Its runtime failure stays on Orca; selection
|
|
50
78
|
// must never substitute a hidden subprocess worker after an explicit or detected choice.
|
|
51
79
|
: want === "orca" ? new OrcaDriver()
|
|
52
|
-
:
|
|
53
|
-
:
|
|
80
|
+
: host === "herdr" ? new HerdrDriver("herdr", cfg.visibility.workersPerTab)
|
|
81
|
+
: host === "orca" ? new OrcaDriver()
|
|
54
82
|
: new SubprocessDriver();
|
|
55
83
|
if (selectedOverride !== undefined)
|
|
56
84
|
overrideByDriver.set(driver, selectedOverride);
|
|
85
|
+
hostByDriver.set(driver, host);
|
|
57
86
|
return driver;
|
|
58
87
|
}
|
package/dist/drivers/orca.d.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { type ShResult } from "../run/git.js";
|
|
2
2
|
import { type JournalEvent } from "../run/journal.js";
|
|
3
|
+
import { type WatchBoardOwner } from "../run/supervision.js";
|
|
3
4
|
import { type ExecutorDriver, type FocusTarget, type FocusResult, type NotifyOpts, type Slot, type SlotOpts } from "./types.js";
|
|
4
5
|
/** The response families the ONE shared envelope parser serves. There is no second JSON seam. */
|
|
5
|
-
export declare const ORCA_RESPONSE_FAMILIES: readonly ["status", "create", "list", "read", "send", "wait", "show", "close", "worktree-current", "worktree-set", "hooks-status"];
|
|
6
|
+
export declare const ORCA_RESPONSE_FAMILIES: readonly ["status", "create", "list", "read", "send", "wait", "show", "close", "worktree-current", "worktree-set", "hooks-status", "split"];
|
|
6
7
|
export type OrcaFamily = (typeof ORCA_RESPONSE_FAMILIES)[number];
|
|
7
8
|
export declare const ORCA_FIXTURE_VERSION = "1.4.195";
|
|
8
9
|
export declare const ORCA_CLI_COMMAND_ENV = "ORCA_CLI_COMMAND";
|
|
@@ -14,7 +15,6 @@ export declare const NOT_WRITABLE_CODE = "terminal_not_writable";
|
|
|
14
15
|
/** The ONLY terminal status that licenses reading a terminal's bytes or its agent state. */
|
|
15
16
|
export declare const RUNNING_STATUS = "running";
|
|
16
17
|
export declare const STATUS_GOVERNED_METHODS: readonly ["read", "waitOutput", "status", "waitAgentStatus"];
|
|
17
|
-
export declare const WORKTREE_ADOPTION_TIMEOUT_MS = 60000;
|
|
18
18
|
/** A missing slot gets the same bounded chance to appear as a reaped shell gets to settle. */
|
|
19
19
|
export declare const PENDING_PROJECT_GRACE_MS = 2000;
|
|
20
20
|
export interface OrcaExec {
|
|
@@ -81,11 +81,71 @@ export declare function terminalWorktree(term: Record<string, unknown>): string
|
|
|
81
81
|
* it keeps its resolved spelling: deterministic, and still comparable to another spelling of itself.
|
|
82
82
|
*/
|
|
83
83
|
export declare function canonicalWorktreePath(path: string): string;
|
|
84
|
+
/** The proof line a worker terminal prints first: recovery and focus read it back (FX-N01). */
|
|
85
|
+
export declare const CHECKOUT_MARK = "TICKMARKR_CHECKOUT";
|
|
86
|
+
/** The exact bytes the create command prints as its first line. */
|
|
87
|
+
export declare function checkoutProofLine(checkout: string): string;
|
|
88
|
+
/** Every complete frame in a scrollback, decoded and canonicalized; whether an incomplete one was seen. */
|
|
89
|
+
export declare function checkoutFrames(text: string): {
|
|
90
|
+
complete: string[];
|
|
91
|
+
incomplete: boolean;
|
|
92
|
+
};
|
|
93
|
+
/** Does a scrollback prove exactly `checkout`: at least one complete frame equals it, no complete
|
|
94
|
+
* frame names anything else, and no frame is incomplete. Full-path equality after canonicalization —
|
|
95
|
+
* never a prefix, a substring, or a whitespace-terminated fragment. */
|
|
96
|
+
export declare function provesCheckout(text: string, checkout: string): boolean;
|
|
97
|
+
/**
|
|
98
|
+
* Everything a terminal on the tracked worktree runs before the payload: enter the checkout (a
|
|
99
|
+
* failed cd stops the whole line — nothing of the payload ever runs in the enclosing path), print
|
|
100
|
+
* the proof line, then hand the WHOLE payload to one `sh -c` so a background list, a `;` list or a
|
|
101
|
+
* subshell inside it all start in the checkout and its exit status is the payload's (FX-N02).
|
|
102
|
+
*/
|
|
103
|
+
export declare function checkoutPrefix(checkout: string): string;
|
|
104
|
+
/** The command a terminal on the tracked worktree runs so that it executes INSIDE the checkout. */
|
|
105
|
+
export declare function inCheckout(checkout: string, cmd: string): string;
|
|
106
|
+
/** Every checkout the complete proof frames in a scrollback name, in order of appearance. */
|
|
107
|
+
export declare function checkoutsNamed(text: string): string[];
|
|
84
108
|
/** Conservative agent-state mapping over orca's ACTUAL surfaces: `blocked` only when the show
|
|
85
109
|
* record reports agentWait:true, `idle` only when the `terminal wait --for tui-idle` condition is
|
|
86
110
|
* satisfied. The recorded 1.4.186 show response carries NO agent field at all — an absent signal
|
|
87
111
|
* is "unknown", never a fabricated definite status. */
|
|
88
112
|
export declare function mapAgentState(term: Record<string, unknown>, tuiIdle: boolean): string;
|
|
113
|
+
/**
|
|
114
|
+
* The Orca board owner record is the board's ONE lifecycle, and it lives on disk: every step below is
|
|
115
|
+
* decided from the file (plus Orca's own terminal table), so a fresh OrcaDriver — a restarted daemon —
|
|
116
|
+
* reaches the same answer as the instance that placed the board. No instance map or set carries it.
|
|
117
|
+
*
|
|
118
|
+
* reserved pane "", no claim narrator, create-only, before the split can read its token
|
|
119
|
+
* claimed pid + armId, pane "" observer (observeNamedRun), written exactly once
|
|
120
|
+
* bound claim + the receipt's handle and the split envelope's runtimeId
|
|
121
|
+
* retired bound + retired:true tombstone, CAS on the bound bytes; never answered again
|
|
122
|
+
*
|
|
123
|
+
* A retired record is replaced by a new reservation (CAS on the tombstone bytes) only once its pane
|
|
124
|
+
* is proven gone. Everything else — a reservation or claim with no bound pane, a failed cleanup, a
|
|
125
|
+
* record another driver holds with a live observer — refuses and keeps the record exactly as it is.
|
|
126
|
+
*/
|
|
127
|
+
/** `placer` is the pid of the narrator that wrote the reservation: a reserved or claimed record is a
|
|
128
|
+
* normal intermediate state while that pid lives (another driver awaiting listing or bind) and a
|
|
129
|
+
* crash to recover only once it is dead. */
|
|
130
|
+
type BoardRecord = WatchBoardOwner & {
|
|
131
|
+
retired?: true;
|
|
132
|
+
runtimeId?: string;
|
|
133
|
+
placer?: number;
|
|
134
|
+
};
|
|
135
|
+
/**
|
|
136
|
+
* Whole-record compare-and-swap. The canonical path stays readable until commit: a mkdir lock
|
|
137
|
+
* excludes other writers, then create-only `link`s the new inode (fails if anything exists) or
|
|
138
|
+
* `rename`s the new file over the live path (POSIX atomic replace — readers see old or new, never
|
|
139
|
+
* absence). A crash that leaves a `.tmp` or `.lock` does not drop the previous record.
|
|
140
|
+
* Each lock generation publishes its pid and nonce atomically in a symlink target. A holder that died
|
|
141
|
+
* between taking and releasing it is recovered at once (its lock is taken over in place); a live
|
|
142
|
+
* holder bounds the wait on injected time and is then refused with the record untouched. A legacy
|
|
143
|
+
* pid-less or malformed generation is recoverable too: a contender atomically creates the next owner
|
|
144
|
+
* generation, and release removes the directory only while that exact generation is still current.
|
|
145
|
+
* ponytail: observeNamedRun (supervision.ts) renames without CAS. It cannot interleave with a swap
|
|
146
|
+
* because the narrator writes nothing between reserve and claim, and no transition here swaps one.
|
|
147
|
+
*/
|
|
148
|
+
export declare function casBoard(family: string, path: string, expected: string | undefined, next: BoardRecord, time?: OrcaTimeSource): Promise<string>;
|
|
89
149
|
/**
|
|
90
150
|
* The renderer hard-wraps long lines, paints margin chrome, and a cursor page boundary splits a
|
|
91
151
|
* marker exactly like a wrap does. `parseWorkerResult` (src/adapters/prompt.ts) already de-wraps
|
|
@@ -95,6 +155,8 @@ export declare function mapAgentState(term: Record<string, unknown>, tuiIdle: bo
|
|
|
95
155
|
* never depends on this.
|
|
96
156
|
*/
|
|
97
157
|
export declare function joinWrapped(raw: string): string;
|
|
158
|
+
/** The OBS-1011 add.1 capture: `status:"exited", tail:[], returnedLineCount 0` from a stream read. */
|
|
159
|
+
export declare function isBlindStreamPage(term: Record<string, unknown>): boolean;
|
|
98
160
|
export interface OrcaDriverOpts {
|
|
99
161
|
bin?: string;
|
|
100
162
|
env?: NodeJS.ProcessEnv | Record<string, string | undefined>;
|
|
@@ -105,6 +167,7 @@ export interface OrcaDriverOpts {
|
|
|
105
167
|
pollMs?: number;
|
|
106
168
|
/** Bounded, seam-adjustable staleness window for runtime probes before mutations. */
|
|
107
169
|
probeStalenessMs?: number;
|
|
170
|
+
launchingHandle?: string;
|
|
108
171
|
}
|
|
109
172
|
export declare class OrcaDriver implements ExecutorDriver {
|
|
110
173
|
id: string;
|
|
@@ -121,7 +184,12 @@ export declare class OrcaDriver implements ExecutorDriver {
|
|
|
121
184
|
private narrate?;
|
|
122
185
|
private hookCoverage?;
|
|
123
186
|
private taskWorktrees;
|
|
187
|
+
private trackedByCheckout;
|
|
124
188
|
private pendingProjects;
|
|
189
|
+
private env;
|
|
190
|
+
private launchingHandle?;
|
|
191
|
+
private serialQueue;
|
|
192
|
+
private serial;
|
|
125
193
|
constructor(opts?: OrcaDriverOpts);
|
|
126
194
|
private call;
|
|
127
195
|
/** The live runtime's identity, or an explicit failure. A missing or unreachable runtime is a
|
|
@@ -144,14 +212,13 @@ export declare class OrcaDriver implements ExecutorDriver {
|
|
|
144
212
|
private sendReceipt;
|
|
145
213
|
private create;
|
|
146
214
|
/**
|
|
147
|
-
*
|
|
148
|
-
*
|
|
149
|
-
*
|
|
150
|
-
*
|
|
215
|
+
* OBS-1004: the tracked worktree that encloses a checkout, asked ONCE of `worktree current` from
|
|
216
|
+
* inside that checkout. Orca answers the exact path when it tracks the checkout itself, the
|
|
217
|
+
* enclosing tracked clone for a git worktree the daemon added beneath it (1.4.200, verified from
|
|
218
|
+
* `.tickmarkr/worktrees.noindex/<task>`), and selector_not_found when nothing it tracks encloses
|
|
219
|
+
* the cwd — which is a driver failure, not something to wait out: Orca has no adopt verb.
|
|
151
220
|
*/
|
|
152
|
-
private
|
|
153
|
-
/** Same repo/run/narration path Herdr uses for its driver-owned dispatch-retry row. */
|
|
154
|
-
private appendAdoptionWait;
|
|
221
|
+
private trackedWorktree;
|
|
155
222
|
/**
|
|
156
223
|
* Every terminal-addressed call — read AND write — goes through here, and the runtime identity is
|
|
157
224
|
* established BEFORE the runtime-scoped handle goes on the wire. Discarding a lookalike's answer
|
|
@@ -161,6 +228,19 @@ export declare class OrcaDriver implements ExecutorDriver {
|
|
|
161
228
|
* the handle exactly once, then re-issues the operation against the replacement.
|
|
162
229
|
*/
|
|
163
230
|
private terminalOp;
|
|
231
|
+
/**
|
|
232
|
+
* FX-N01/N05/N06: under a shared enclosing worktree every task terminal lists with the same
|
|
233
|
+
* worktreePath, so the tracked path + owned title cannot tell two nested checkouts apart. The
|
|
234
|
+
* runtime's own proof is the terminal's earliest scrollback, where the create command printed a
|
|
235
|
+
* framed `TICKMARKR_CHECKOUT` line before its payload (checkoutProofLine). READ-only calls: the
|
|
236
|
+
* anchor (for `oldestCursor`), then pages from the oldest cursor until a frame is complete or the
|
|
237
|
+
* bound is hit. Every page is evidence only when the response's own identity is the candidate's:
|
|
238
|
+
* the terminal record must name `handle` and `_meta.runtimeId` must be `runtimeId` — the runtime
|
|
239
|
+
* that supplied the ownership listing — else another terminal's or another runtime's bytes were
|
|
240
|
+
* answered and nothing is proven. Proven means provesCheckout: exact canonical full-path equality
|
|
241
|
+
* of a complete frame, no other checkout named, no incomplete frame.
|
|
242
|
+
*/
|
|
243
|
+
private checkoutProven;
|
|
164
244
|
private recover;
|
|
165
245
|
/** Validated READ terminal record, or an explicit unavailable failure. Called BEFORE any caller
|
|
166
246
|
* looks at tail bytes — on every page, on every read-governed method. Read records are the one
|
|
@@ -173,6 +253,15 @@ export declare class OrcaDriver implements ExecutorDriver {
|
|
|
173
253
|
private liveShowTerm;
|
|
174
254
|
private tailText;
|
|
175
255
|
private readPage;
|
|
256
|
+
/**
|
|
257
|
+
* OBS-1011 add.1 / OBS-1016: the captured incident shape — a stream page answering status exited
|
|
258
|
+
* with an empty tail on a terminal that accepted a send seconds earlier — is BLIND, not dead, when
|
|
259
|
+
* the same handle's show record on the same runtime reports connected and not orphaned (show carries
|
|
260
|
+
* no status field; none is demanded) and its screen read reports running. Then the rendered frame is
|
|
261
|
+
* the terminal's bytes. Anything less — disconnected, orphaned, another handle or runtime, a screen
|
|
262
|
+
* that is unavailable or exited — is refused as unavailable, exactly as the dead record would be.
|
|
263
|
+
*/
|
|
264
|
+
private screenBehindBlindStream;
|
|
176
265
|
/** A rendered-frame liveness read. `--screen` and `--cursor` are mutually exclusive in Orca. */
|
|
177
266
|
private readScreen;
|
|
178
267
|
/** A single UNPAGED tail read — exactly what the caller asked for and nothing more. Markers split
|
|
@@ -200,7 +289,24 @@ export declare class OrcaDriver implements ExecutorDriver {
|
|
|
200
289
|
waitAgentStatus(slot: Slot, status: string, timeoutMs: number): Promise<boolean>;
|
|
201
290
|
sendKey(slot: Slot, key: string): Promise<void>;
|
|
202
291
|
nudge(slot: Slot, message: string): Promise<boolean>;
|
|
203
|
-
narrator(
|
|
292
|
+
narrator(cwd: string, command: string, runId?: string): Promise<Slot>;
|
|
293
|
+
private boardPath;
|
|
294
|
+
/** A recorded pane (a receipt's handle bound to the split envelope's runtime, never a guess) is
|
|
295
|
+
* gone when that runtime no longer lists it, or when a handle-bound close receipt names it.
|
|
296
|
+
* A handle listed by a different runtime is a different pane — not closed, treated as gone. */
|
|
297
|
+
private closeRecordedPane;
|
|
298
|
+
/** bound → retired, decided from the record alone: it must be this driver's board for exactly this
|
|
299
|
+
* slot's pane. Already retired is returned as it is. */
|
|
300
|
+
private retireBoard;
|
|
301
|
+
/** bound → retired first: whatever fails below, no later call answers this board again. A live
|
|
302
|
+
* observer is asked to stop and its acknowledgement awaited on injected time before the
|
|
303
|
+
* handle-bound close (timeout keeps the tombstone and the pane); a dead one never acknowledges,
|
|
304
|
+
* so it is only asked. */
|
|
305
|
+
private retireAndClose;
|
|
306
|
+
/** WB-1 seam: the daemon reports this board lost. "Lost" can be a stale beat or missing presence
|
|
307
|
+
* under a still-live owner pid, so it is not proof of a dead observer — retirement keeps close's
|
|
308
|
+
* acknowledgement discipline (Leg-2 T9 P1). */
|
|
309
|
+
retireLostWatch(slot: Slot): Promise<void>;
|
|
204
310
|
focus(target: FocusTarget): Promise<FocusResult>;
|
|
205
311
|
project(taskId: string, state: "in-progress" | "in-review" | "completed"): Promise<void>;
|
|
206
312
|
private setWorkspaceStatus;
|
|
@@ -243,5 +349,8 @@ export declare class OrcaDriver implements ExecutorDriver {
|
|
|
243
349
|
reconcile(desired: Set<string>, runId: string, opts?: {
|
|
244
350
|
spareLiveLlm?: boolean;
|
|
245
351
|
}): Promise<void>;
|
|
352
|
+
private isRecordedWatchHandle;
|
|
353
|
+
private isRecordedWorkerHandle;
|
|
246
354
|
worktree(repo: string, branch: string, baseRef: string): Promise<string>;
|
|
247
355
|
}
|
|
356
|
+
export {};
|