tickmarkr 2.4.1 → 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 (75) hide show
  1. package/README.md +110 -24
  2. package/dist/adapters/claude-code.js +7 -2
  3. package/dist/adapters/codex.d.ts +1 -0
  4. package/dist/adapters/codex.js +66 -5
  5. package/dist/adapters/types.d.ts +4 -0
  6. package/dist/adapters/types.js +33 -0
  7. package/dist/cli/commands/approve.d.ts +42 -0
  8. package/dist/cli/commands/approve.js +80 -13
  9. package/dist/cli/commands/doctor.d.ts +234 -0
  10. package/dist/cli/commands/doctor.js +139 -5
  11. package/dist/cli/commands/report.js +15 -44
  12. package/dist/cli/commands/scope.js +36 -6
  13. package/dist/cli/commands/stats.d.ts +2 -0
  14. package/dist/cli/commands/stats.js +42 -23
  15. package/dist/cli/commands/status.js +20 -4
  16. package/dist/cli/commands/ui.js +42 -53
  17. package/dist/cli/commands/unlock.d.ts +1 -1
  18. package/dist/cli/commands/unlock.js +59 -9
  19. package/dist/cli/help.d.ts +219 -0
  20. package/dist/cli/help.js +212 -0
  21. package/dist/cli/index.d.ts +42 -2
  22. package/dist/cli/index.js +23 -8
  23. package/dist/drivers/herdr.d.ts +7 -2
  24. package/dist/drivers/herdr.js +78 -48
  25. package/dist/drivers/orca.d.ts +3 -2
  26. package/dist/drivers/orca.js +43 -4
  27. package/dist/drivers/subprocess.d.ts +1 -0
  28. package/dist/drivers/subprocess.js +3 -0
  29. package/dist/drivers/types.d.ts +14 -0
  30. package/dist/gates/artifact-manifest.d.ts +50 -0
  31. package/dist/gates/artifact-manifest.js +23 -0
  32. package/dist/plan/scope.d.ts +25 -0
  33. package/dist/plan/scope.js +92 -12
  34. package/dist/report/operator-record.d.ts +49 -0
  35. package/dist/report/operator-record.js +137 -0
  36. package/dist/run/daemon.d.ts +3 -4
  37. package/dist/run/daemon.js +18 -10
  38. package/dist/run/lock.d.ts +59 -2
  39. package/dist/run/lock.js +184 -26
  40. package/dist/run/operator-state.d.ts +86 -0
  41. package/dist/run/operator-state.js +165 -0
  42. package/dist/run/supervision.d.ts +32 -0
  43. package/dist/run/supervision.js +138 -17
  44. package/dist/tui/cockpit/capture.d.ts +19 -0
  45. package/dist/tui/cockpit/capture.js +89 -1
  46. package/dist/tui/cockpit/components.d.ts +15 -1
  47. package/dist/tui/cockpit/components.js +79 -9
  48. package/dist/tui/cockpit/decision-actions.d.ts +147 -0
  49. package/dist/tui/cockpit/decision-actions.js +315 -0
  50. package/dist/tui/cockpit/derive.d.ts +1 -1
  51. package/dist/tui/cockpit/derive.js +2 -0
  52. package/dist/tui/cockpit/evidence-view.d.ts +119 -0
  53. package/dist/tui/cockpit/evidence-view.js +210 -0
  54. package/dist/tui/cockpit/home-view.d.ts +88 -0
  55. package/dist/tui/cockpit/home-view.js +240 -0
  56. package/dist/tui/cockpit/keys.d.ts +125 -0
  57. package/dist/tui/cockpit/keys.js +31 -0
  58. package/dist/tui/cockpit/layout.d.ts +14 -0
  59. package/dist/tui/cockpit/layout.js +15 -0
  60. package/dist/tui/cockpit/live-runtime.d.ts +46 -0
  61. package/dist/tui/cockpit/live-runtime.js +683 -0
  62. package/dist/tui/cockpit/live-store.d.ts +289 -0
  63. package/dist/tui/cockpit/live-store.js +308 -0
  64. package/dist/tui/cockpit/live.d.ts +21 -1
  65. package/dist/tui/cockpit/live.js +12 -1
  66. package/dist/tui/cockpit/run-view.d.ts +100 -0
  67. package/dist/tui/cockpit/run-view.js +202 -0
  68. package/dist/tui/cockpit/shell.d.ts +50 -0
  69. package/dist/tui/cockpit/shell.js +74 -0
  70. package/dist/tui/cockpit/theme.d.ts +27 -0
  71. package/dist/tui/cockpit/theme.js +21 -0
  72. package/package.json +1 -1
  73. package/skills/tickmarkr-auto/SKILL.md +10 -1
  74. package/skills/tickmarkr-loop/SKILL.md +72 -1
  75. package/skills/tickmarkr-overseer/SKILL.md +12 -0
@@ -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>;
@@ -1,13 +1,16 @@
1
1
  import { existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
2
2
  import { tmpdir } from "node:os";
3
3
  import { basename, dirname, extname, join } from "node:path";
4
- import { discoverChannels, getAdapter, probeAll } from "../adapters/registry.js";
4
+ import { discoverChannels, getAdapter, probeAll, readDoctor } from "../adapters/registry.js";
5
5
  import { compileNative, LEGACY_PREFIX } from "../compile/native.js";
6
6
  import { pickDriver } from "../drivers/index.js";
7
7
  import { extractJson, runLlm } from "../gates/llm.js";
8
8
  import { TaskSchema } from "../graph/schema.js";
9
9
  import { route } from "../route/router.js";
10
10
  import { scopePrompt } from "./prompt.js";
11
+ // R11/R45 (C10): the drafting loop's hard ceiling — shared by the loop itself and the preview
12
+ // disclosure so the two can never state different budgets.
13
+ export const MAX_SCOPE_ATTEMPTS = 3;
11
14
  const HEADING_RE = /^#{1,6}\s+(.+?)\s*$/;
12
15
  const ITEM_RE = /^\s*(?:[-*+]\s+|\d+[.)]\s+)(?:\[[ xX]\]\s*)?(?:[QA]\d*:\s*)?(.+?)\s*$/;
13
16
  function sectionItems(source, heading) {
@@ -95,7 +98,13 @@ function validateDraft(draft) {
95
98
  rmSync(dir, { recursive: true, force: true });
96
99
  }
97
100
  }
98
- export async function scopeIntent(intentFile, repoRoot, options) {
101
+ function scopePlanningTask() {
102
+ return TaskSchema.parse({
103
+ id: "SCOPE", title: "Draft native spec", goal: "Draft a compiled native spec", shape: "spec", complexity: 7,
104
+ acceptance: [{ oracle: "judge", text: "Every requirement maps to a task with typed acceptance oracles" }],
105
+ });
106
+ }
107
+ function localValidate(intentFile) {
99
108
  if (!existsSync(intentFile))
100
109
  throw new Error(`no such intent file: ${intentFile}`);
101
110
  const intent = readFileSync(intentFile, "utf8");
@@ -103,21 +112,92 @@ export async function scopeIntent(intentFile, repoRoot, options) {
103
112
  if (unanswered.length) {
104
113
  throw new Error(`unanswered blocking questions (${unanswered.length}):\n${unanswered.map((q, i) => `${i + 1}. ${q}`).join("\n")}`);
105
114
  }
106
- const specFile = specPathForIntent(intentFile);
115
+ return { intent, specFile: specPathForIntent(intentFile) };
116
+ }
117
+ /**
118
+ * R11/R45 (C10): local-only disclosure — intent/clarification checks and a candidate read off the
119
+ * doctor cache, never a fresh probe or a model turn. `readDoctor` and `discoverChannels`/`route` are
120
+ * pure reads over that cache, so this never touches an adapter.
121
+ */
122
+ export function previewScope(intentFile, repoRoot, options) {
123
+ const { specFile } = localValidate(intentFile);
124
+ const cachedHealth = readDoctor(repoRoot);
125
+ let candidate;
126
+ if (cachedHealth) {
127
+ const channels = discoverChannels(options.cfg, options.adapters, cachedHealth);
128
+ if (channels.length) {
129
+ try {
130
+ const assignment = route(scopePlanningTask(), options.cfg, channels).assignment;
131
+ // Review fix (finding 1): routing.allowUnverifiedModels lets discoverChannels/route pick a
132
+ // channel whose modelAuth is simply absent (unknown) — that is routing PERMISSION, not proof
133
+ // of health. Only disclose a candidate when the doctor cache actually marked this exact model
134
+ // authed; otherwise this stays the "unknown" case below, never "installed/authed".
135
+ if (cachedHealth[assignment.adapter]?.modelAuth?.[assignment.model]?.authed === true) {
136
+ candidate = { adapter: assignment.adapter, model: assignment.model };
137
+ }
138
+ }
139
+ catch {
140
+ // no eligible candidate in the cached snapshot — stays unknown, never "unreachable"
141
+ }
142
+ }
143
+ }
144
+ return {
145
+ intentFile, specFile, specExists: existsSync(specFile), cached: cachedHealth !== null,
146
+ candidate, authoringBudget: MAX_SCOPE_ATTEMPTS, probeCalls: options.adapters.length,
147
+ };
148
+ }
149
+ export function formatScopePreview(preview) {
150
+ const candidateLine = preview.candidate
151
+ ? `cached candidate: ${preview.candidate.adapter}:${preview.candidate.model} (installed/authed at last doctor run)`
152
+ : `cached candidate: unknown (${preview.cached ? "no eligible channel in the doctor cache" : "no doctor cache — run tickmarkr doctor"})`;
153
+ return [
154
+ `tickmarkr scope --preview ${preview.intentFile}:`,
155
+ candidateLine,
156
+ `output destination: ${preview.specFile}${preview.specExists ? " (exists — active scope needs --force)" : ""}`,
157
+ `authoring-call budget: up to ${preview.authoringBudget} call${preview.authoringBudget === 1 ? "" : "s"} if confirmed`,
158
+ `probe calls: ${preview.probeCalls} (disclosed separately — one per configured adapter, only on confirmed active scope)`,
159
+ "cache policy: read-only; no adapter was probed and no model was called",
160
+ ].join("\n");
161
+ }
162
+ // R11/R45 (C10 repair): a confirmed candidate is a promise made to the operator — find that exact
163
+ // adapter:model in the freshly probed channels, or fail loud. Never let a stale-cache candidate
164
+ // silently fall through to a fresh route() that could reroute to a different channel unconfirmed.
165
+ function bindCandidate(candidate, channels) {
166
+ const c = channels.find((ch) => ch.adapter === candidate.adapter && ch.model === candidate.model);
167
+ if (!c) {
168
+ throw new Error(`confirmed candidate ${candidate.adapter}:${candidate.model} is no longer available after a fresh probe ` +
169
+ `(doctor found: ${channels.map((ch) => `${ch.adapter}:${ch.model}`).join(", ") || "(nothing)"}) — ` +
170
+ "re-run scope --preview and confirm again");
171
+ }
172
+ return { adapter: c.adapter, model: c.model, channel: c.channel, tier: c.tier };
173
+ }
174
+ // Adapter-level probes establish the current installation/auth state, but shipped adapters do not
175
+ // return per-model verdicts from probe(). Preserve cached verdicts only where that fresh snapshot is
176
+ // silent; an explicit fresh per-model verdict still wins, as do fresh adapter-level failures.
177
+ function mergeCachedModelAuth(freshHealth, cachedHealth) {
178
+ if (!cachedHealth)
179
+ return freshHealth;
180
+ return Object.fromEntries(Object.entries(freshHealth).map(([adapter, fresh]) => {
181
+ const cachedModelAuth = cachedHealth[adapter]?.modelAuth;
182
+ if (!cachedModelAuth)
183
+ return [adapter, fresh];
184
+ return [adapter, { ...fresh, modelAuth: { ...cachedModelAuth, ...fresh.modelAuth } }];
185
+ }));
186
+ }
187
+ export async function scopeIntent(intentFile, repoRoot, options) {
188
+ const { intent, specFile } = localValidate(intentFile);
107
189
  if (existsSync(specFile) && !options.force)
108
190
  throw new Error(`${specFile} already exists; pass --force to overwrite it`);
109
- const health = await probeAll(options.adapters);
191
+ const health = mergeCachedModelAuth(await probeAll(options.adapters), readDoctor(repoRoot));
110
192
  const channels = discoverChannels(options.cfg, options.adapters, health);
111
- const planningTask = TaskSchema.parse({
112
- id: "SCOPE", title: "Draft native spec", goal: "Draft a compiled native spec", shape: "spec", complexity: 7,
113
- acceptance: [{ oracle: "judge", text: "Every requirement maps to a task with typed acceptance oracles" }],
114
- });
115
- const assignment = route(planningTask, options.cfg, channels).assignment;
193
+ const assignment = options.candidate
194
+ ? bindCandidate(options.candidate, channels)
195
+ : route(scopePlanningTask(), options.cfg, channels).assignment;
116
196
  const adapter = getAdapter(assignment.adapter, options.adapters);
117
197
  const driver = options.cfg.visibility.llm === "pane" ? options.driver ?? pickDriver(options.cfg) : undefined;
118
198
  const name = basename(specFile, ".spec.md");
119
199
  let prompt = scopePrompt(intent);
120
- for (let attempts = 1; attempts <= 3; attempts++) {
200
+ for (let attempts = 1; attempts <= MAX_SCOPE_ATTEMPTS; attempts++) {
121
201
  const via = driver ? {
122
202
  driver, name: `scope-${name}-${attempts}-${adapter.id}`, label: "SCOPE",
123
203
  keep: options.cfg.visibility.keepPanes === "forever",
@@ -129,8 +209,8 @@ export async function scopeIntent(intentFile, repoRoot, options) {
129
209
  }
130
210
  catch (error) {
131
211
  const message = error.message;
132
- if (attempts === 3)
133
- throw new Error(`scope draft failed after 2 repair retries:\n${message}`);
212
+ if (attempts === MAX_SCOPE_ATTEMPTS)
213
+ throw new Error(`scope draft failed after ${MAX_SCOPE_ATTEMPTS - 1} repair retries:\n${message}`);
134
214
  prompt = scopePrompt(intent, { draft, error: message });
135
215
  continue;
136
216
  }
@@ -0,0 +1,49 @@
1
+ import type { TokenUsage } from "../adapters/types.js";
2
+ import type { JournalEvent } from "../run/journal.js";
3
+ import type { ChannelCost } from "./cost.js";
4
+ export declare function formatTokenUsage(u: TokenUsage): string;
5
+ export declare function totalTokens(u: TokenUsage): number;
6
+ /** The channel a journal event's `assignment` names, or absent — never coalesced to a placeholder
7
+ * string here, so a caller decides its own absent-channel reading. */
8
+ export declare function assignmentChannel(data: Record<string, unknown>): string | undefined;
9
+ /** Worker token coverage: "unmetered" when the adapter reported no usage at all — distinct from a
10
+ * channel with no telemetry row whatsoever, which `labelChannelUsage` below calls "unknown". */
11
+ export declare function formatChannelTokens(row: ChannelCost): string;
12
+ export declare function formatRateBasis(row: ChannelCost): string | undefined;
13
+ /** Nonmeasurable money always says so explicitly (`row.reason`, or "not recorded") — never a $0. */
14
+ export declare function formatChannelMoney(row: ChannelCost): {
15
+ readonly prices: string[];
16
+ readonly bases: string[];
17
+ };
18
+ export interface ChannelUsageLabel {
19
+ readonly channel: string;
20
+ /** "unmetered" (metered run, adapter reported nothing), a real total, or "unknown" (no telemetry
21
+ * row exists for this channel at all — missing metadata, never invented). */
22
+ readonly tokens: string;
23
+ /** "not measurable" (metered but unpriced/partial), a real dollar figure, or "unknown" (no row). */
24
+ readonly money: string;
25
+ }
26
+ /** Labels one channel's metered/priced facts. `row` absent means journal evidence names this
27
+ * channel (e.g. a pre-telemetry run) but no telemetry row was ever recorded for it — missing
28
+ * metadata, distinct from a recorded-but-unpriced/unmetered row. */
29
+ export declare function labelChannelUsage(channel: string, row: ChannelCost | undefined): ChannelUsageLabel;
30
+ export interface ChannelRoleCounts {
31
+ readonly channel: string;
32
+ /** Dispatches recorded for this channel as the task's worker. */
33
+ readonly worker: number;
34
+ /** Review gate-results whose reviewer identity resolved to this channel. */
35
+ readonly review: number;
36
+ /** Consult verdicts recorded against the task this channel was last dispatched on. */
37
+ readonly consult: number;
38
+ }
39
+ /** Recorded worker/review/consult appearances per channel, read only from journal evidence — no
40
+ * extrapolation to an all-role invoice. A channel that only ever reviewed still gets a row with
41
+ * worker:0, never a fabricated dispatch. */
42
+ export declare function channelRoleCounts(events: readonly JournalEvent[]): ChannelRoleCounts[];
43
+ export interface OperatorRecordRow extends ChannelRoleCounts, Omit<ChannelUsageLabel, "channel"> {
44
+ }
45
+ /** The shared record: one row per channel journal evidence actually names (role counts), joined
46
+ * with that channel's metered/priced facts. A channel `costs` prices but journal evidence never
47
+ * names gets no row here — this never invents a role from cost data alone. */
48
+ export declare function buildOperatorRecord(events: readonly JournalEvent[], costs?: readonly ChannelCost[]): OperatorRecordRow[];
49
+ export declare function formatOperatorRecordRow(row: OperatorRecordRow): string;
@@ -0,0 +1,137 @@
1
+ const n = (x) => x.toLocaleString("en-US");
2
+ export function formatTokenUsage(u) {
3
+ const parts = [`in ${n(u.input)}`, `out ${n(u.output)}`];
4
+ if (u.cacheRead !== undefined && u.cacheWrite !== undefined)
5
+ parts.push(`cache r/w ${n(u.cacheRead)}/${n(u.cacheWrite)}`);
6
+ if (u.reasoning !== undefined)
7
+ parts.push(`reasoning ${n(u.reasoning)}`);
8
+ return parts.join(" ");
9
+ }
10
+ export function totalTokens(u) {
11
+ return [u.input, u.output, u.cacheRead, u.cacheWrite, u.reasoning].filter((x) => x !== undefined).reduce((a, b) => a + b, 0);
12
+ }
13
+ /** The channel a journal event's `assignment` names, or absent — never coalesced to a placeholder
14
+ * string here, so a caller decides its own absent-channel reading. */
15
+ export function assignmentChannel(data) {
16
+ const assignment = data.assignment;
17
+ if (!assignment || typeof assignment !== "object")
18
+ return undefined;
19
+ const { adapter, model } = assignment;
20
+ return typeof adapter === "string" && typeof model === "string" ? `${adapter}:${model}` : undefined;
21
+ }
22
+ /** Worker token coverage: "unmetered" when the adapter reported no usage at all — distinct from a
23
+ * channel with no telemetry row whatsoever, which `labelChannelUsage` below calls "unknown". */
24
+ export function formatChannelTokens(row) {
25
+ return row.tokens
26
+ ? `${row.partialMetering ? "≥ " : ""}${formatTokenUsage(row.tokens)} (${n(totalTokens(row.tokens))} tokens)`
27
+ : "unmetered";
28
+ }
29
+ export function formatRateBasis(row) {
30
+ if (!row.rate)
31
+ return undefined;
32
+ const cache = row.rate.cacheReadPerMtok === undefined ? "" : `; cache-read $${row.rate.cacheReadPerMtok}/Mtok`;
33
+ const date = row.rate.rateDate === undefined ? "" : `; rate date ${row.rate.rateDate}`;
34
+ return `in/out $${row.rate.inPerMtok}/$${row.rate.outPerMtok}/Mtok${cache}${date}`;
35
+ }
36
+ /** Nonmeasurable money always says so explicitly (`row.reason`, or "not recorded") — never a $0. */
37
+ export function formatChannelMoney(row) {
38
+ const prices = [];
39
+ const bases = [];
40
+ if (row.apiUsd !== undefined)
41
+ prices.push(`price: $${row.apiUsd.toFixed(6)}`);
42
+ if (row.amortizedUsd !== undefined && row.subPlan !== undefined) {
43
+ const [low, high] = row.amortizedUsd;
44
+ prices.push(`price: $${low.toFixed(6)}–$${high.toFixed(6)} amortized`);
45
+ bases.push(`${n(row.attempts)} windows × $${row.subPlan.planMonthly}/month ÷ ${row.subPlan.windowsPerMonthHigh}–${row.subPlan.windowsPerMonthLow} windows/month`);
46
+ }
47
+ if (row.counterfactualUsd !== undefined)
48
+ prices.push(`API-equivalent: $${row.counterfactualUsd.toFixed(6)}`);
49
+ const basis = formatRateBasis(row);
50
+ if (basis)
51
+ bases.push(basis);
52
+ if (!prices.length)
53
+ prices.push("price: not measurable");
54
+ if (!bases.length)
55
+ bases.push(row.reason || "not recorded");
56
+ return { prices, bases };
57
+ }
58
+ /** Labels one channel's metered/priced facts. `row` absent means journal evidence names this
59
+ * channel (e.g. a pre-telemetry run) but no telemetry row was ever recorded for it — missing
60
+ * metadata, distinct from a recorded-but-unpriced/unmetered row. */
61
+ export function labelChannelUsage(channel, row) {
62
+ if (!row)
63
+ return { channel, tokens: "unknown", money: "unknown" };
64
+ const { prices } = formatChannelMoney(row);
65
+ return { channel, tokens: formatChannelTokens(row), money: prices.join("; ") };
66
+ }
67
+ const REVIEWER_PATTERN = /reviewer\s+([^\s;()]+:[^\s;()]+)/i;
68
+ function reviewerChannel(data) {
69
+ if (typeof data.reviewer === "string" && data.reviewer.includes(":"))
70
+ return data.reviewer;
71
+ const meta = typeof data.meta === "object" && data.meta !== null ? data.meta : undefined;
72
+ if (meta && "reviewer" in meta && typeof meta.reviewer === "string" && meta.reviewer.includes(":"))
73
+ return meta.reviewer;
74
+ return typeof data.details === "string" ? REVIEWER_PATTERN.exec(data.details)?.[1] : undefined;
75
+ }
76
+ /** The channel a `consult-verdict` event names as ITS OWN consultant identity (daemon.ts always
77
+ * appends `adapter`/`model` on every verdict) — never the task's worker dispatch, which is a
78
+ * different channel a consult can (and typically does) disagree with. */
79
+ function consultChannel(data) {
80
+ const { adapter, model } = data;
81
+ return typeof adapter === "string" && typeof model === "string" ? `${adapter}:${model}` : undefined;
82
+ }
83
+ /** Recorded worker/review/consult appearances per channel, read only from journal evidence — no
84
+ * extrapolation to an all-role invoice. A channel that only ever reviewed still gets a row with
85
+ * worker:0, never a fabricated dispatch. */
86
+ export function channelRoleCounts(events) {
87
+ const counts = new Map();
88
+ const ensure = (channel) => {
89
+ let row = counts.get(channel);
90
+ if (!row) {
91
+ row = { worker: 0, review: 0, consult: 0 };
92
+ counts.set(channel, row);
93
+ }
94
+ return row;
95
+ };
96
+ for (const e of events) {
97
+ if (e.event === "task-dispatch") {
98
+ const channel = assignmentChannel(e.data);
99
+ if (channel)
100
+ ensure(channel).worker++;
101
+ continue;
102
+ }
103
+ if (e.event === "gate-result" && e.data.gate === "review") {
104
+ const channel = reviewerChannel(e.data);
105
+ if (channel)
106
+ ensure(channel).review++;
107
+ continue;
108
+ }
109
+ if (e.event === "review-leg2") {
110
+ const channel = reviewerChannel(e.data);
111
+ if (channel)
112
+ ensure(channel).review++;
113
+ continue;
114
+ }
115
+ if (e.event === "consult-verdict") {
116
+ const channel = consultChannel(e.data);
117
+ if (channel)
118
+ ensure(channel).consult++;
119
+ }
120
+ }
121
+ return [...counts.entries()]
122
+ .sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))
123
+ .map(([channel, role]) => ({ channel, ...role }));
124
+ }
125
+ /** The shared record: one row per channel journal evidence actually names (role counts), joined
126
+ * with that channel's metered/priced facts. A channel `costs` prices but journal evidence never
127
+ * names gets no row here — this never invents a role from cost data alone. */
128
+ export function buildOperatorRecord(events, costs = []) {
129
+ const byChannel = new Map(costs.map((c) => [`${c.adapter}:${c.model}`, c]));
130
+ return channelRoleCounts(events).map((role) => {
131
+ const usage = labelChannelUsage(role.channel, byChannel.get(role.channel));
132
+ return { ...role, tokens: usage.tokens, money: usage.money };
133
+ });
134
+ }
135
+ export function formatOperatorRecordRow(row) {
136
+ return `${row.channel} — worker: ${row.worker}, review: ${row.review}, consult: ${row.consult}; tokens: ${row.tokens}; money: ${row.money}`;
137
+ }
@@ -64,10 +64,9 @@ export interface RunSummary {
64
64
  */
65
65
  export declare function outstandingApprovals(events: JournalEvent[]): string[];
66
66
  export declare function formatSummary(s: RunSummary): string;
67
- /** The narrator's command, bound to THIS run. `status` takes exactly one positional and it is the
68
- * run id (cli/commands/status.ts positionalRunId), so naming it here is what stops the board from
69
- * following the newest journal in a repo that already carries a second, newer run — a board showing
70
- * the wrong run is a recorded incident (skills/tickmarkr-overseer/SKILL.md). */
67
+ /** The narrator enters the production Run cockpit for this exact run. The
68
+ * completed static/growing four-hour records precede this default cutover;
69
+ * an explicit ID prevents a newer journal from redirecting the owned board. */
71
70
  export declare const daemonEntrypoint: string;
72
71
  export declare const watchCommand: (runId: string) => string;
73
72
  /**