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.
Files changed (78) hide show
  1. package/README.md +3 -1
  2. package/dist/adapters/registry.js +6 -1
  3. package/dist/adapters/types.d.ts +3 -0
  4. package/dist/cli/commands/approve.d.ts +1 -0
  5. package/dist/cli/commands/approve.js +118 -9
  6. package/dist/cli/commands/doctor.d.ts +10 -0
  7. package/dist/cli/commands/doctor.js +44 -29
  8. package/dist/cli/commands/fleet.js +199 -33
  9. package/dist/cli/commands/init.js +196 -6
  10. package/dist/cli/commands/plan.js +20 -4
  11. package/dist/cli/commands/resume.js +4 -2
  12. package/dist/cli/commands/run.js +11 -2
  13. package/dist/cli/commands/verify.js +102 -81
  14. package/dist/cli/help.d.ts +2 -0
  15. package/dist/cli/help.js +3 -1
  16. package/dist/config/config.d.ts +25 -8
  17. package/dist/config/config.js +41 -29
  18. package/dist/config/fleet-overlay.d.ts +3 -9
  19. package/dist/config/fleet-overlay.js +114 -19
  20. package/dist/config/fleet-why.d.ts +7 -0
  21. package/dist/config/fleet-why.js +5 -0
  22. package/dist/drivers/index.d.ts +15 -1
  23. package/dist/drivers/index.js +39 -10
  24. package/dist/drivers/orca.d.ts +119 -10
  25. package/dist/drivers/orca.js +781 -110
  26. package/dist/gates/baseline.d.ts +17 -3
  27. package/dist/gates/baseline.js +63 -15
  28. package/dist/gates/cache.d.ts +101 -0
  29. package/dist/gates/cache.js +401 -0
  30. package/dist/gates/llm.d.ts +3 -0
  31. package/dist/gates/llm.js +11 -0
  32. package/dist/gates/review.d.ts +28 -3
  33. package/dist/gates/review.js +106 -14
  34. package/dist/gates/run-gates.d.ts +9 -1
  35. package/dist/gates/run-gates.js +407 -102
  36. package/dist/gates/test-manifest.d.ts +128 -0
  37. package/dist/gates/test-manifest.js +463 -0
  38. package/dist/gates/test-reporter.d.ts +4 -0
  39. package/dist/gates/test-reporter.js +57 -0
  40. package/dist/graph/graph.d.ts +2 -0
  41. package/dist/graph/graph.js +45 -1
  42. package/dist/route/preference.d.ts +22 -1
  43. package/dist/route/preference.js +123 -25
  44. package/dist/route/router.js +31 -6
  45. package/dist/run/consult.js +5 -4
  46. package/dist/run/daemon.d.ts +15 -0
  47. package/dist/run/daemon.js +638 -167
  48. package/dist/run/execution-budget.d.ts +25 -0
  49. package/dist/run/execution-budget.js +142 -0
  50. package/dist/run/git.d.ts +50 -1
  51. package/dist/run/git.js +131 -12
  52. package/dist/run/journal.d.ts +16 -2
  53. package/dist/run/journal.js +115 -25
  54. package/dist/run/lease.d.ts +58 -0
  55. package/dist/run/lease.js +310 -0
  56. package/dist/run/merge.d.ts +2 -0
  57. package/dist/run/merge.js +91 -3
  58. package/dist/run/operator-state.d.ts +11 -0
  59. package/dist/run/operator-state.js +17 -3
  60. package/dist/run/recovery.d.ts +8 -0
  61. package/dist/run/recovery.js +25 -0
  62. package/dist/run/repair-selection.d.ts +12 -0
  63. package/dist/run/repair-selection.js +56 -0
  64. package/dist/run/stall.d.ts +6 -1
  65. package/dist/run/stall.js +60 -3
  66. package/dist/tui/cockpit/board.d.ts +96 -0
  67. package/dist/tui/cockpit/board.js +346 -0
  68. package/dist/tui/cockpit/decision-actions.js +2 -0
  69. package/dist/tui/cockpit/layout.d.ts +5 -1
  70. package/dist/tui/cockpit/layout.js +8 -3
  71. package/dist/tui/cockpit/live-runtime.js +71 -21
  72. package/dist/tui/cockpit/run-view.d.ts +7 -5
  73. package/dist/tui/cockpit/run-view.js +12 -11
  74. package/dist/tui/ink/fleet-app.d.ts +49 -27
  75. package/dist/tui/ink/fleet-app.js +229 -38
  76. package/package.json +2 -2
  77. package/skills/tickmarkr-overseer/SKILL.md +173 -113
  78. 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
- const denyAdapters = new Set(edited.denyAdapters);
25
- const denyModels = new Set(edited.denyModels);
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 (denyAdapters.has(row.adapter)) {
32
+ if (entries.includes(row.adapter)) {
31
33
  excluded = true;
32
34
  continue;
33
35
  }
34
- const inFleet = row.models.filter((m) => !denyModels.has(`${row.adapter}:${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
- const denyChanged = sortedUnique(initial.denyAdapters).join() !== sortedUnique(edited.denyAdapters).join()
171
- || sortedUnique(initial.denyModels).join() !== sortedUnique(edited.denyModels).join();
172
- if (denyChanged) {
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
- // Residuals stay in deny; covered scopes are tombstoned so a lower layer can never
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
- const residualAdapters = residualDeny(write.universe, edited.denyAdapters);
205
- const residualModels = residualDeny(write.universe, edited.denyModels);
206
- setStringSequencePreservingComments(doc, ["routing", "deny", "adapters"], residualAdapters.length ? residualAdapters : null);
207
- setStringSequencePreservingComments(doc, ["routing", "deny", "models"], residualModels.length ? residualModels : null);
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
- setStringSequencePreservingComments(doc, ["routing", "deny", "adapters"], edited.denyAdapters.length ? sortedUnique(edited.denyAdapters) : null);
211
- setStringSequencePreservingComments(doc, ["routing", "deny", "models"], edited.denyModels.length ? sortedUnique(edited.denyModels) : null);
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.comment = `${INLINE_COMMENT_SENTINEL}${node.comment}`;
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
- ? ` #${comment.slice(1)}`
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;
@@ -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
+ }
@@ -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;
@@ -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.TERM_PROGRAM === "Orca" && env.ORCA_TERMINAL_HANDLE !== undefined;
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 herdrAvailable = process.env.HERDR_ENV === "1";
31
- if (herdrAvailable && driver.id === "herdr")
60
+ const host = hostByDriver.get(driver);
61
+ if (host === "herdr" && driver.id === "herdr")
32
62
  return "auto → herdr (HERDR_ENV=1)";
33
- if (!herdrAvailable && orcaHostDetected() && driver.id === "orca") {
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
- : HerdrDriver.available() ? new HerdrDriver("herdr", cfg.visibility.workersPerTab)
53
- : orcaHostDetected() ? new OrcaDriver()
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
  }
@@ -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
- * A freshly-created git checkout does not become a valid Orca selector atomically. Ask
148
- * `worktree current` FROM that checkout until Orca itself resolves the exact filesystem identity;
149
- * an enclosing checkout is still not adoption. Only selector_not_found is a retryable refusal —
150
- * malformed envelopes and every other refusal remain explicit driver failures.
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 awaitWorktreeAdoption;
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(_cwd: string, _command: string, runId?: string): Promise<Slot>;
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 {};