tickmarkr 2.4.2 → 2.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. package/README.md +110 -24
  2. package/dist/cli/commands/approve.d.ts +42 -0
  3. package/dist/cli/commands/approve.js +80 -13
  4. package/dist/cli/commands/doctor.d.ts +234 -0
  5. package/dist/cli/commands/doctor.js +139 -5
  6. package/dist/cli/commands/report.js +15 -44
  7. package/dist/cli/commands/scope.js +36 -6
  8. package/dist/cli/commands/stats.d.ts +2 -0
  9. package/dist/cli/commands/stats.js +42 -23
  10. package/dist/cli/commands/status.js +20 -4
  11. package/dist/cli/commands/ui.js +42 -53
  12. package/dist/cli/commands/unlock.d.ts +1 -1
  13. package/dist/cli/commands/unlock.js +59 -9
  14. package/dist/cli/help.d.ts +219 -0
  15. package/dist/cli/help.js +212 -0
  16. package/dist/cli/index.d.ts +42 -2
  17. package/dist/cli/index.js +23 -8
  18. package/dist/drivers/herdr.d.ts +7 -2
  19. package/dist/drivers/herdr.js +78 -48
  20. package/dist/drivers/orca.d.ts +3 -2
  21. package/dist/drivers/orca.js +43 -4
  22. package/dist/drivers/subprocess.d.ts +1 -0
  23. package/dist/drivers/subprocess.js +3 -0
  24. package/dist/drivers/types.d.ts +14 -0
  25. package/dist/gates/artifact-manifest.d.ts +50 -0
  26. package/dist/gates/artifact-manifest.js +23 -0
  27. package/dist/plan/scope.d.ts +25 -0
  28. package/dist/plan/scope.js +92 -12
  29. package/dist/report/operator-record.d.ts +49 -0
  30. package/dist/report/operator-record.js +137 -0
  31. package/dist/run/daemon.d.ts +3 -4
  32. package/dist/run/daemon.js +18 -10
  33. package/dist/run/lock.d.ts +59 -2
  34. package/dist/run/lock.js +184 -26
  35. package/dist/run/operator-state.d.ts +86 -0
  36. package/dist/run/operator-state.js +165 -0
  37. package/dist/run/supervision.d.ts +32 -0
  38. package/dist/run/supervision.js +138 -17
  39. package/dist/tui/cockpit/capture.d.ts +19 -0
  40. package/dist/tui/cockpit/capture.js +89 -1
  41. package/dist/tui/cockpit/components.d.ts +15 -1
  42. package/dist/tui/cockpit/components.js +79 -9
  43. package/dist/tui/cockpit/decision-actions.d.ts +147 -0
  44. package/dist/tui/cockpit/decision-actions.js +315 -0
  45. package/dist/tui/cockpit/derive.d.ts +1 -1
  46. package/dist/tui/cockpit/derive.js +2 -0
  47. package/dist/tui/cockpit/evidence-view.d.ts +119 -0
  48. package/dist/tui/cockpit/evidence-view.js +210 -0
  49. package/dist/tui/cockpit/home-view.d.ts +88 -0
  50. package/dist/tui/cockpit/home-view.js +240 -0
  51. package/dist/tui/cockpit/keys.d.ts +125 -0
  52. package/dist/tui/cockpit/keys.js +31 -0
  53. package/dist/tui/cockpit/layout.d.ts +14 -0
  54. package/dist/tui/cockpit/layout.js +15 -0
  55. package/dist/tui/cockpit/live-runtime.d.ts +46 -0
  56. package/dist/tui/cockpit/live-runtime.js +683 -0
  57. package/dist/tui/cockpit/live-store.d.ts +289 -0
  58. package/dist/tui/cockpit/live-store.js +308 -0
  59. package/dist/tui/cockpit/live.d.ts +21 -1
  60. package/dist/tui/cockpit/live.js +12 -1
  61. package/dist/tui/cockpit/run-view.d.ts +100 -0
  62. package/dist/tui/cockpit/run-view.js +202 -0
  63. package/dist/tui/cockpit/shell.d.ts +50 -0
  64. package/dist/tui/cockpit/shell.js +74 -0
  65. package/dist/tui/cockpit/theme.d.ts +27 -0
  66. package/dist/tui/cockpit/theme.js +21 -0
  67. package/package.json +1 -1
  68. package/skills/tickmarkr-auto/SKILL.md +10 -1
  69. package/skills/tickmarkr-loop/SKILL.md +72 -1
  70. package/skills/tickmarkr-overseer/SKILL.md +12 -0
@@ -1,5 +1,5 @@
1
1
  import { type JournalEvent } from "../run/journal.js";
2
- import { type ExecutorDriver, type NotifyOpts, type PanesToCloseOpts, type Slot, type SlotOpts } from "./types.js";
2
+ import { type ExecutorDriver, type FocusTarget, type FocusResult, type NotifyOpts, type PanesToCloseOpts, type Slot, type SlotOpts } from "./types.js";
3
3
  export declare const TRAILER_SAFE_FLOOR_COLS = 108;
4
4
  export declare const TRAILER_WIDTH_MARGIN = 2;
5
5
  export declare const DELIVERY_ATTEMPTS = 3;
@@ -69,6 +69,7 @@ export declare class HerdrDriver implements ExecutorDriver {
69
69
  private ws;
70
70
  private callerPane;
71
71
  private watches;
72
+ private watchTokens;
72
73
  constructor(bin?: string, workersPerTab?: number, time?: HerdrTimeSource, journal?: DriverJournal | undefined);
73
74
  private appendDispatchRetry;
74
75
  private appendPaneClose;
@@ -126,9 +127,13 @@ export declare class HerdrDriver implements ExecutorDriver {
126
127
  notify(msg: string, opts?: NotifyOpts): Promise<void>;
127
128
  close(slot: Slot): Promise<void>;
128
129
  private closeGrouped;
129
- private ownedWatchPanes;
130
+ private watchPanes;
131
+ /** Name collisions never confer repository ownership. Unknown boards stay protected. */
132
+ private retireWatch;
133
+ focus(target: FocusTarget): Promise<FocusResult>;
130
134
  private watchSlot;
131
135
  private discardSplit;
136
+ /** Only this repository's matching run may replace its acknowledged board. */
132
137
  narrator(cwd: string, command: string, runId?: string): Promise<Slot>;
133
138
  reconcile(desired: Set<string>, runId: string, opts?: PanesToCloseOpts): Promise<void>;
134
139
  worktree(repo: string, branch: string, baseRef: string): Promise<string>;
@@ -6,7 +6,7 @@ import { declaredInputBoxForWorkerName, matchesEmptyInputBox, matchesInputBox, m
6
6
  import { consumePaneLaunchIntent, PANE_IDENTITY_ENV, paneIdentityLine } from "../brand.js";
7
7
  import { createWorktree, sh } from "../run/git.js";
8
8
  import { Journal } from "../run/journal.js";
9
- import { readSupervision } from "../run/supervision.js";
9
+ import { readSupervision, readWatchBoard, reserveWatchBoard, stopWatchBoard, WATCH_OWNER_ENV } from "../run/supervision.js";
10
10
  import { herdrSealShellPrefix } from "./subprocess.js";
11
11
  import { canonicalizeLegacyName, formatOwnedName, panesToClose, parseOwnedName } from "./types.js";
12
12
  // VIS-09 P43-03: adopted safety floor from 43-MEASUREMENT.md (narrowest safe 53 → floor 108).
@@ -103,6 +103,7 @@ export function boardSplitPlan(_callerCols) {
103
103
  * `role:"other", taskId:"<the whole name>"` for any unrecognised string, so keying on taskId alone
104
104
  * makes EVERY one-off pane its own "task" and gives it a group tab. `watch` is excluded for the same
105
105
  * reason in the other direction — its taskId is the literal "run", which is a board, not a task. */
106
+ const shortTitle = (text) => [...text].slice(0, 20).join("");
106
107
  const TASK_TAB_ROLES = new Set(["worker", "judge", "review", "consult"]);
107
108
  export function taskGroupOf(name) {
108
109
  const { role, taskId } = canonicalizeLegacyName(name, "");
@@ -123,10 +124,10 @@ export function taskGroupOf(name) {
123
124
  export function tabLabelFor(name) {
124
125
  const { role, taskId, attempt } = canonicalizeLegacyName(name, "");
125
126
  if (!TASK_TAB_ROLES.has(role) || !taskId.trim())
126
- return name;
127
+ return shortTitle(name);
127
128
  if (role !== "worker")
128
- return `${role.toUpperCase()} ${taskId}`;
129
- return attempt > 0 ? `${taskId}↻${attempt}` : taskId;
129
+ return shortTitle(`${role.toUpperCase()} ${taskId}`);
130
+ return shortTitle(attempt > 0 ? `${taskId}↻${attempt}` : taskId);
130
131
  }
131
132
  /** Gate panes ride with the task they belong to and never consume the tab cap; everything else does.
132
133
  * Scoped to the three GATE roles deliberately — an earlier cut of this said "not a worker", which let
@@ -168,6 +169,7 @@ export class HerdrDriver {
168
169
  ws = process.env.HERDR_WORKSPACE_ID;
169
170
  callerPane = process.env.HERDR_PANE_ID;
170
171
  watches = new Map();
172
+ watchTokens = new WeakMap();
171
173
  constructor(bin = "herdr", workersPerTab = 3, time = SYSTEM_TIME, journal) {
172
174
  this.bin = bin;
173
175
  this.workersPerTab = workersPerTab;
@@ -469,7 +471,7 @@ export class HerdrDriver {
469
471
  // the worktree — that root pane IS the worker pane. The old one-shot `agent start … -- bash` verb
470
472
  // (which named a fresh bash pane) was removed; 0.7.5's `agent start` only ATTACHES to a DETECTED
471
473
  // agent CLI, so tickmarkr names the bash pane itself via `pane rename` and types the worker command in.
472
- const t = await this.herdr(`tab create --label ${shq(label)} --no-focus --workspace ${shq(this.ws)} --cwd ${shq(cwd)}`);
474
+ const t = await this.herdr(`tab create --label ${shq(shortTitle(label))} --no-focus --workspace ${shq(this.ws)} --cwd ${shq(cwd)}`);
473
475
  if (t.code !== 0)
474
476
  throw new Error(`herdr tab create failed (exit ${t.code}, refusing untargeted placement): ${t.stderr || t.stdout}`);
475
477
  let res;
@@ -597,7 +599,7 @@ export class HerdrDriver {
597
599
  const label = !token ? entry.label
598
600
  : entry.label === token ? `${token}${glyph}`
599
601
  : `${entry.label} · ${token}${glyph}`;
600
- const cmd = `tab rename ${shq(entry.tabId)} ${shq(label)}`;
602
+ const cmd = `tab rename ${shq(entry.tabId)} ${shq(shortTitle(label))}`;
601
603
  const ok = async () => (await this.herdr(cmd)).code === 0;
602
604
  if (await ok() || await ok())
603
605
  return;
@@ -1113,12 +1115,10 @@ export class HerdrDriver {
1113
1115
  await this.herdr(`notification show ${shq(msg)} --sound ${opts?.tier === "attention" ? "request" : opts?.sound ?? "request"}`);
1114
1116
  }
1115
1117
  async close(slot) {
1116
- if (this.watches.get(slot.name)?.id === slot.id) {
1118
+ if (parseOwnedName(slot.name)?.role === "watch") {
1119
+ await this.retireWatch(slot);
1117
1120
  this.watches.delete(slot.name);
1118
- const pane = await this.namedPaneId(slot.name);
1119
- if (pane)
1120
- await this.herdr(`pane close ${shq(pane)}`);
1121
- return; // run-end reconcile may already have reaped it; never close a compacted stale id
1121
+ return;
1122
1122
  }
1123
1123
  if (slot.group && this.groups.has(slot.group)) {
1124
1124
  return this.serial(() => this.closeGrouped(slot));
@@ -1158,29 +1158,63 @@ export class HerdrDriver {
1158
1158
  this.groups.delete(slot.group); // group dies when all generations gone
1159
1159
  }
1160
1160
  }
1161
- // Every surviving tickmarkr-owned board in this workspace — a PRIOR run's and one already wearing
1162
- // this run's own name alike. Both are retired before a new board opens (narrator): what a pane this
1163
- // process did not create is actually RUNNING cannot be read back, and the pre-v1.94 implementation
1164
- // launched a bare `tickmarkr status --watch`, which follows the newest journal.
1165
- async ownedWatchPanes() {
1161
+ async watchPanes(name) {
1166
1162
  if (!this.ws)
1167
1163
  throw new Error("herdr watch placement requires HERDR_WORKSPACE_ID — refusing unseeded pane");
1168
1164
  const list = await this.herdr("pane list");
1169
1165
  if (list.code !== 0)
1170
1166
  throw new Error(`herdr pane list failed: ${list.stderr || list.stdout}`);
1167
+ const panes = JSON.parse(list.stdout).result?.panes;
1168
+ if (!Array.isArray(panes))
1169
+ throw new Error("herdr pane list returned no panes");
1170
+ return panes.filter(p => p.workspace_id === this.ws && p.label === name);
1171
+ }
1172
+ /** Name collisions never confer repository ownership. Unknown boards stay protected. */
1173
+ async retireWatch(slot) {
1174
+ const runId = parseOwnedName(slot.name)?.runId;
1175
+ const owner = runId ? readWatchBoard(slot.cwd, runId) : undefined;
1176
+ const matches = await this.watchPanes(slot.name);
1177
+ if (matches.length === 0)
1178
+ throw new Error(`watch ${slot.name} closed without presence acknowledgement; cleanup unconfirmed`);
1179
+ if (!owner || owner.driver !== this.id || owner.name !== slot.name || owner.workspace !== this.ws ||
1180
+ (this.watchTokens.has(slot) && this.watchTokens.get(slot) !== owner.token) ||
1181
+ owner.pane !== slot.id || matches.length !== 1 || matches[0]?.pane_id !== owner.pane) {
1182
+ throw new Error(`watch ownership unknown or foreign for ${slot.name}; existing board protected`);
1183
+ }
1184
+ await stopWatchBoard(owner, this.time);
1185
+ const verified = await this.watchPanes(slot.name);
1186
+ if (verified.length !== 1 || verified[0]?.pane_id !== owner.pane)
1187
+ throw new Error("watch target changed after acknowledgement; pane protected");
1188
+ const closed = await this.herdr(`pane close ${shq(owner.pane)}`);
1189
+ if (closed.code !== 0 || await this.paneStillOpen(owner.pane))
1190
+ throw new Error(`watch ${slot.name} survived acknowledged close`);
1191
+ }
1192
+ async focus(target) {
1193
+ const { slot, runId, taskId, attempt } = target;
1194
+ if (slot.name !== formatOwnedName({ role: "worker", taskId, attempt, runId }) || !target.workspace || target.workspace !== this.ws) {
1195
+ return { status: "foreign", reason: "Recorded run/task/attempt or workspace does not match this driver" };
1196
+ }
1197
+ const r = await this.herdr("pane list", slot.cwd);
1198
+ if (r.code !== 0)
1199
+ return { status: "unsupported", reason: "Cannot verify the live pane list" };
1171
1200
  let panes;
1172
1201
  try {
1173
- panes = JSON.parse(list.stdout).result?.panes;
1202
+ panes = JSON.parse(r.stdout).result?.panes;
1174
1203
  }
1175
1204
  catch {
1176
- throw new Error(`herdr pane list returned unparseable JSON: ${list.stdout}`);
1205
+ return { status: "unsupported", reason: "Unreadable pane list" };
1177
1206
  }
1178
1207
  if (!Array.isArray(panes))
1179
- throw new Error(`herdr pane list returned no panes: ${list.stdout}`);
1180
- return panes.filter((p) => {
1181
- const owned = typeof p.label === "string" ? parseOwnedName(p.label) : null;
1182
- return p.workspace_id === this.ws && typeof p.pane_id === "string" && owned?.role === "watch" && owned.taskId === "run";
1183
- }).map((p) => p.pane_id);
1208
+ return { status: "unsupported", reason: "Missing pane list" };
1209
+ const named = panes.filter(p => p.label === slot.name && p.workspace_id === target.workspace);
1210
+ if (!named.length)
1211
+ return { status: panes.some(p => p.pane_id === slot.id) ? "foreign" : "closed", reason: "Recorded pane is no longer owned by this attempt; open task evidence" };
1212
+ if (named.length !== 1 || named[0]?.pane_id !== slot.id || (slot.tabId && named[0]?.tab_id !== slot.tabId)) {
1213
+ return { status: "foreign", reason: "Live pane identity differs from the recorded task attempt" };
1214
+ }
1215
+ const focused = await this.herdr(`pane focus ${shq(slot.id)}`, slot.cwd);
1216
+ return focused.code === 0 ? { status: "focused", reason: `Verified ${slot.name} in ${target.workspace}` }
1217
+ : { status: "unsupported", reason: focused.stderr || focused.stdout || "Host refused pane focus" };
1184
1218
  }
1185
1219
  // T2: the watch is a sibling of the daemon's own pane, never a separate tab — placed to the RIGHT
1186
1220
  // of it, always, whatever the terminal measures. Its durable owned name is how a
@@ -1243,38 +1277,34 @@ export class HerdrDriver {
1243
1277
  }
1244
1278
  throw new Error(orphan === null ? why : `${why} — and the split pane ${pane} survived its close (${orphan})`);
1245
1279
  }
1246
- // T6 narrator: the run's single live status surface, RUNNING THE COMMAND THIS CALL SUPPLIED. Only
1247
- // a board this driver instance itself opened is reused (this.watches); any other surviving board —
1248
- // a prior run's, or one already carrying this run's canonical name after a resume — is retired and
1249
- // re-split, because adoption cannot restart or even read the process inside it and a pre-v1.94 pane
1250
- // is running the bare `tickmarkr status --watch`, which narrates the newest journal instead of this
1251
- // run. The retirement is VERIFIED gone before the replacement splits: reconcile is no backstop here
1252
- // (panesToClose skips role "watch" by design, types.ts:92), so an unverified close would leave two
1253
- // boards bound to different runs. Failures propagate — the daemon swallows.
1280
+ /** Only this repository's matching run may replace its acknowledged board. */
1254
1281
  async narrator(cwd, command, runId) {
1255
- const name = runId ? formatOwnedName({ role: "watch", taskId: "run", attempt: 0, runId }) : `narrator-watch-${process.pid}`;
1282
+ if (!this.ws)
1283
+ throw new Error("herdr watch placement requires HERDR_WORKSPACE_ID");
1284
+ if (!runId)
1285
+ throw new Error("herdr narrator requires a run identity");
1286
+ const name = formatOwnedName({ role: "watch", taskId: "run", attempt: 0, runId });
1256
1287
  return this.serial(async () => {
1288
+ const matches = await this.watchPanes(name);
1257
1289
  const cached = this.watches.get(name);
1258
- if (cached)
1290
+ if (cached && matches.length === 1 && matches[0]?.pane_id === cached.id &&
1291
+ readWatchBoard(cwd, runId)?.token === this.watchTokens.get(cached))
1259
1292
  return cached;
1260
- const stale = await this.ownedWatchPanes();
1261
- for (const pane of stale)
1262
- await this.herdr(`pane close ${shq(pane)}`);
1263
- if (stale.length) {
1264
- const survived = (await this.ownedWatchPanes()).filter((p) => stale.includes(p));
1265
- if (survived.length)
1266
- throw new Error(`herdr watch retire failed: ${survived.join(", ")} survived close — refusing a second board`);
1267
- }
1268
- const s = await this.watchSlot(cwd, name);
1269
- this.watches.set(name, s);
1293
+ if (matches.length > 1)
1294
+ throw new Error(`watch ownership ambiguous for ${name}; existing boards protected`);
1295
+ if (matches.length === 1)
1296
+ await this.retireWatch({ id: matches[0].pane_id, name, cwd });
1297
+ const slot = await this.watchSlot(cwd, name);
1298
+ const owner = reserveWatchBoard({ repo: cwd, runId, driver: this.id, workspace: this.ws, pane: slot.id, name });
1299
+ this.watchTokens.set(slot, owner.token);
1270
1300
  try {
1271
- await this.deliverPersistentShellCommand(s, command);
1301
+ await this.deliverPersistentShellCommand(slot, `${WATCH_OWNER_ENV}=${shq(owner.token)} ${command}`);
1302
+ this.watches.set(name, slot);
1303
+ return slot;
1272
1304
  }
1273
- catch (err) {
1274
- this.watches.delete(name);
1275
- throw err;
1305
+ catch (error) {
1306
+ throw new Error(`watch launch unconfirmed for ${name}; pane protected: ${String(error)}`);
1276
1307
  }
1277
- return s;
1278
1308
  });
1279
1309
  }
1280
1310
  // OBS-17 T2 / v1.22b T1: close THIS RUN'S OWN tickmarkr-owned panes that should not exist
@@ -1,6 +1,6 @@
1
1
  import { type ShResult } from "../run/git.js";
2
2
  import { type JournalEvent } from "../run/journal.js";
3
- import { type ExecutorDriver, type NotifyOpts, type Slot, type SlotOpts } from "./types.js";
3
+ import { type ExecutorDriver, type FocusTarget, type FocusResult, type NotifyOpts, type Slot, type SlotOpts } from "./types.js";
4
4
  /** The response families the ONE shared envelope parser serves. There is no second JSON seam. */
5
5
  export declare const ORCA_RESPONSE_FAMILIES: readonly ["status", "create", "list", "read", "send", "wait", "show", "close", "worktree-current", "worktree-set", "hooks-status"];
6
6
  export type OrcaFamily = (typeof ORCA_RESPONSE_FAMILIES)[number];
@@ -200,7 +200,8 @@ export declare class OrcaDriver implements ExecutorDriver {
200
200
  waitAgentStatus(slot: Slot, status: string, timeoutMs: number): Promise<boolean>;
201
201
  sendKey(slot: Slot, key: string): Promise<void>;
202
202
  nudge(slot: Slot, message: string): Promise<boolean>;
203
- narrator(cwd: string, command: string, runId?: string): Promise<Slot>;
203
+ narrator(_cwd: string, _command: string, runId?: string): Promise<Slot>;
204
+ focus(target: FocusTarget): Promise<FocusResult>;
204
205
  project(taskId: string, state: "in-progress" | "in-review" | "completed"): Promise<void>;
205
206
  private setWorkspaceStatus;
206
207
  notify(msg: string, opts?: NotifyOpts): Promise<void>;
@@ -997,12 +997,51 @@ export class OrcaDriver {
997
997
  return false;
998
998
  }
999
999
  }
1000
- async narrator(cwd, command, runId) {
1000
+ async narrator(_cwd, _command, runId) {
1001
1001
  if (!runId)
1002
1002
  throw new OrcaError("create", "Orca narrator requires a run identity", "");
1003
- const slot = await this.slot(cwd, formatOwnedName({ role: "watch", taskId: "run", attempt: 0, runId }), { owned: { role: "watch", taskId: "run", attempt: 0, runId } });
1004
- await this.run(slot, command);
1005
- return slot;
1003
+ // The recorded Orca API can create a tab but provides no right/no-focus
1004
+ // placement receipt. Creating one would advertise a board we did not place.
1005
+ throw new OrcaError("create", "Orca narrator placement unsupported: right/no-focus board placement is not available", "");
1006
+ }
1007
+ async focus(target) {
1008
+ const { slot, runId, taskId, attempt } = target;
1009
+ const cwd = canonicalWorktreePath(slot.cwd);
1010
+ if (slot.name !== formatOwnedName({ role: "worker", taskId, attempt, runId })) {
1011
+ return { status: "foreign", reason: "Recorded run/task/attempt ownership does not match" };
1012
+ }
1013
+ try {
1014
+ const env = await this.listAll(cwd);
1015
+ const rows = env.result.terminals;
1016
+ const layouts = env.result.visualLayouts;
1017
+ if (!Array.isArray(rows) || !Array.isArray(layouts))
1018
+ return { status: "unsupported", reason: "Cannot verify Orca terminal ownership" };
1019
+ const handles = [];
1020
+ for (const layout of layouts) {
1021
+ if (typeof layout !== "object" || layout === null)
1022
+ continue;
1023
+ const lo = layout;
1024
+ if (!sameWorktree(terminalWorktree(lo), cwd))
1025
+ continue;
1026
+ const tabs = [];
1027
+ collectTabs(lo.root, tabs);
1028
+ for (const tab of tabs) {
1029
+ if (typeof tab === "object" && tab !== null && str(tab.title) === slot.name)
1030
+ collectPaneHandles(tab.panes, handles);
1031
+ }
1032
+ }
1033
+ if (handles.length !== 1)
1034
+ return { status: rows.length ? "foreign" : "closed", reason: "No unique owned terminal in the recorded worktree" };
1035
+ const matches = rows.filter(row => typeof row === "object" && row !== null && str(row.handle) === handles[0] && sameWorktree(terminalWorktree(row), cwd));
1036
+ if (matches.length !== 1)
1037
+ return { status: "foreign", reason: "Terminal worktree ownership is unverified" };
1038
+ if (matches[0].connected === false || matches[0].orphaned === true)
1039
+ return { status: "closed", reason: "Recorded terminal is no longer running; open task evidence" };
1040
+ return { status: "unsupported", reason: "Owned terminal verified; this Orca API has no focus operation. Open task evidence with Enter" };
1041
+ }
1042
+ catch (error) {
1043
+ return { status: "unsupported", reason: `Cannot verify Orca focus target: ${String(error)}` };
1044
+ }
1006
1045
  }
1007
1046
  async project(taskId, state) {
1008
1047
  const worktree = this.taskWorktrees.get(taskId);
@@ -24,6 +24,7 @@ export declare class SubprocessDriver implements ExecutorDriver {
24
24
  waitAgentStatus(slot: Slot, status: string, timeoutMs: number): Promise<boolean>;
25
25
  status(_slot: Slot): Promise<string>;
26
26
  read(slot: Slot, lines: number): Promise<string>;
27
+ focus(): Promise<import("./types.js").FocusResult>;
27
28
  notify(msg: string, _opts?: NotifyOpts): Promise<void>;
28
29
  close(slot: Slot): Promise<void>;
29
30
  worktree(repo: string, branch: string, baseRef: string): Promise<string>;
@@ -107,6 +107,9 @@ export class SubprocessDriver {
107
107
  async read(slot, lines) {
108
108
  return this.state(slot).buf.split("\n").slice(-lines).join("\n");
109
109
  }
110
+ async focus() {
111
+ return { status: "unsupported", reason: "Subprocess workers have no visible pane; open task evidence with Enter" };
112
+ }
110
113
  async notify(msg, _opts) {
111
114
  if (_opts?.tier === "routine")
112
115
  return;
@@ -66,6 +66,18 @@ export declare function panesToClose(agents: FleetAgent[], desired: Set<string>,
66
66
  tabId?: string;
67
67
  }[];
68
68
  export declare function canonicalizeLegacyName(name: string, runId: string): OwnedName;
69
+ export interface FocusTarget {
70
+ repo: string;
71
+ runId: string;
72
+ taskId: string;
73
+ attempt: number;
74
+ slot: Slot;
75
+ workspace?: string;
76
+ }
77
+ export type FocusResult = {
78
+ status: "focused" | "foreign" | "closed" | "unsupported";
79
+ reason: string;
80
+ };
69
81
  export interface SlotPlacement {
70
82
  surface?: string;
71
83
  hostPlatform?: string;
@@ -76,6 +88,8 @@ export interface ExecutorDriver {
76
88
  /** The exact terminal read surface used for liveness evidence. */
77
89
  readSource?: string;
78
90
  /** Placement facts returned by drivers whose terminal host exposes them. */
91
+ /** Verify the recorded identity against the live host before any focus mutation. */
92
+ focus?(target: FocusTarget): Promise<FocusResult>;
79
93
  describe?(slot: Slot): SlotPlacement | Promise<SlotPlacement> | undefined;
80
94
  slot(cwd: string, name: string, opts?: SlotOpts): Promise<Slot>;
81
95
  run(slot: Slot, cmd: string): Promise<void>;
@@ -23,6 +23,27 @@ export type CaptureArtifactManifest = {
23
23
  readonly artifacts: readonly CaptureArtifactManifestEntry[];
24
24
  };
25
25
  export declare const CAPTURE_PRODUCERS: readonly [{
26
+ readonly id: "screen-soak";
27
+ readonly provenance: {
28
+ readonly source: "tests/fixtures/screen-soak/soak.mjs";
29
+ readonly entrypoint: "sample";
30
+ readonly revision: "C6-four-hour-production-v1";
31
+ };
32
+ }, {
33
+ readonly id: "screen-soak-archive";
34
+ readonly provenance: {
35
+ readonly source: "tests/fixtures/screen-soak/archive.mjs";
36
+ readonly entrypoint: "archiveRecord";
37
+ readonly revision: "C6-lossless-gzip-v1";
38
+ };
39
+ }, {
40
+ readonly id: "cockpit-final-shell";
41
+ readonly provenance: {
42
+ readonly source: "src/tui/cockpit/capture.ts";
43
+ readonly entrypoint: "captureShellOutput";
44
+ readonly revision: "final-shell-v1";
45
+ };
46
+ }, {
26
47
  readonly id: "cockpit-golden-frames";
27
48
  readonly provenance: {
28
49
  readonly source: "src/tui/cockpit/capture.ts";
@@ -40,6 +61,27 @@ export declare const CAPTURE_PRODUCERS: readonly [{
40
61
  export declare const CAPTURE_ARTIFACT_MANIFEST: {
41
62
  readonly version: 1;
42
63
  readonly producers: readonly [{
64
+ readonly id: "screen-soak";
65
+ readonly provenance: {
66
+ readonly source: "tests/fixtures/screen-soak/soak.mjs";
67
+ readonly entrypoint: "sample";
68
+ readonly revision: "C6-four-hour-production-v1";
69
+ };
70
+ }, {
71
+ readonly id: "screen-soak-archive";
72
+ readonly provenance: {
73
+ readonly source: "tests/fixtures/screen-soak/archive.mjs";
74
+ readonly entrypoint: "archiveRecord";
75
+ readonly revision: "C6-lossless-gzip-v1";
76
+ };
77
+ }, {
78
+ readonly id: "cockpit-final-shell";
79
+ readonly provenance: {
80
+ readonly source: "src/tui/cockpit/capture.ts";
81
+ readonly entrypoint: "captureShellOutput";
82
+ readonly revision: "final-shell-v1";
83
+ };
84
+ }, {
43
85
  readonly id: "cockpit-golden-frames";
44
86
  readonly provenance: {
45
87
  readonly source: "src/tui/cockpit/capture.ts";
@@ -62,6 +104,14 @@ export declare const CAPTURE_ARTIFACT_MANIFEST: {
62
104
  path: string;
63
105
  producer: string;
64
106
  provenance: CaptureProducerProvenance;
107
+ } | {
108
+ path: string;
109
+ producer: string;
110
+ provenance: CaptureProducerProvenance;
111
+ } | {
112
+ path: string;
113
+ producer: string;
114
+ provenance: CaptureProducerProvenance;
65
115
  })[];
66
116
  };
67
117
  /** Compatibility name for the pre-manifest gate API; now derived from one manifest. */
@@ -13,7 +13,19 @@ const COLOUR_PROVENANCE = {
13
13
  entrypoint: "regenerateColourFrames",
14
14
  revision: "colour-frame-v1",
15
15
  };
16
+ const SHELL_PROVENANCE = {
17
+ source: "src/tui/cockpit/capture.ts", entrypoint: "captureShellOutput", revision: "final-shell-v1",
18
+ };
19
+ const SOAK_PROVENANCE = {
20
+ source: "tests/fixtures/screen-soak/soak.mjs", entrypoint: "sample", revision: "C6-four-hour-production-v1",
21
+ };
22
+ const SOAK_ARCHIVE_PROVENANCE = {
23
+ source: "tests/fixtures/screen-soak/archive.mjs", entrypoint: "archiveRecord", revision: "C6-lossless-gzip-v1",
24
+ };
16
25
  export const CAPTURE_PRODUCERS = [
26
+ { id: "screen-soak", provenance: SOAK_PROVENANCE },
27
+ { id: "screen-soak-archive", provenance: SOAK_ARCHIVE_PROVENANCE },
28
+ { id: "cockpit-final-shell", provenance: SHELL_PROVENANCE },
17
29
  { id: "cockpit-golden-frames", provenance: GOLDEN_PROVENANCE },
18
30
  { id: "cockpit-colour-frames", provenance: COLOUR_PROVENANCE },
19
31
  ];
@@ -49,6 +61,17 @@ export const CAPTURE_ARTIFACT_MANIFEST = {
49
61
  version: 1,
50
62
  producers: CAPTURE_PRODUCERS,
51
63
  artifacts: [
64
+ // Exact measured artifacts only. Neighbouring harnesses, notes and unknown
65
+ // records remain logic; duration evidence never grants a directory exemption.
66
+ ...["static", "growth", "cutover-static", "cutover-growth", "final-static", "final-growth", "retry-static", "retry-growth"].flatMap(attempt => ["build.json", "metadata.json", "samples.jsonl", "last-frame.ansi", "journal.jsonl.gz", "result.json.gz"].map(file => ({
67
+ path: `tests/fixtures/screen-soak/records/${attempt}/${file}`,
68
+ producer: file.endsWith(".gz") ? "screen-soak-archive" : "screen-soak",
69
+ provenance: provenanceCopy(file.endsWith(".gz") ? SOAK_ARCHIVE_PROVENANCE : SOAK_PROVENANCE),
70
+ }))),
71
+ ...["home", "run", "evidence"].flatMap(view => ["120x40", "80x24"].map(size => ({
72
+ path: `tests/fixtures/cockpit/final/${view}.${size}.txt`,
73
+ producer: "cockpit-final-shell", provenance: provenanceCopy(SHELL_PROVENANCE),
74
+ }))),
52
75
  ...goldenFrameNames.map((fixture) => ({
53
76
  path: `tests/fixtures/cockpit/frames/${fixture}`,
54
77
  producer: "cockpit-golden-frames",
@@ -1,12 +1,14 @@
1
1
  import type { WorkerAdapter } from "../adapters/types.js";
2
2
  import type { TickmarkrConfig } from "../config/config.js";
3
3
  import type { ExecutorDriver } from "../drivers/types.js";
4
+ export declare const MAX_SCOPE_ATTEMPTS = 3;
4
5
  export declare function clarificationGate(intent: string): string[];
5
6
  export interface ScopeOptions {
6
7
  cfg: TickmarkrConfig;
7
8
  adapters: WorkerAdapter[];
8
9
  driver?: ExecutorDriver;
9
10
  force?: boolean;
11
+ candidate?: ScopeCandidate;
10
12
  }
11
13
  export interface ScopeResult {
12
14
  specFile: string;
@@ -14,4 +16,27 @@ export interface ScopeResult {
14
16
  attempts: number;
15
17
  }
16
18
  export declare function specPathForIntent(intentFile: string): string;
19
+ export interface ScopeCandidate {
20
+ adapter: string;
21
+ model: string;
22
+ }
23
+ export interface ScopePreview {
24
+ intentFile: string;
25
+ specFile: string;
26
+ specExists: boolean;
27
+ cached: boolean;
28
+ candidate?: ScopeCandidate;
29
+ authoringBudget: number;
30
+ probeCalls: number;
31
+ }
32
+ /**
33
+ * R11/R45 (C10): local-only disclosure — intent/clarification checks and a candidate read off the
34
+ * doctor cache, never a fresh probe or a model turn. `readDoctor` and `discoverChannels`/`route` are
35
+ * pure reads over that cache, so this never touches an adapter.
36
+ */
37
+ export declare function previewScope(intentFile: string, repoRoot: string, options: {
38
+ cfg: TickmarkrConfig;
39
+ adapters: WorkerAdapter[];
40
+ }): ScopePreview;
41
+ export declare function formatScopePreview(preview: ScopePreview): string;
17
42
  export declare function scopeIntent(intentFile: string, repoRoot: string, options: ScopeOptions): Promise<ScopeResult>;