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
@@ -96,5 +96,239 @@ export declare function selfShadowFinding(ownVersion: string, cwd?: string, reso
96
96
  * check) reads it and bumps the constant.
97
97
  */
98
98
  export declare function liveBenchStalenessFinding(now: Date): string | undefined;
99
+ /**
100
+ * A no-call disclosure for the action that normal doctor performs. This is deliberately built
101
+ * from config alone: opening the disclosure must not turn a cache reader into a model purchase.
102
+ */
103
+ export declare function doctorProbePreflight(cwd?: string, cfg?: {
104
+ concurrency: number;
105
+ driver: "auto" | "herdr" | "subprocess" | "orca";
106
+ integrationBranchPrefix: string;
107
+ taskTimeoutMinutes: number;
108
+ contextWarnTokens: number;
109
+ routing: {
110
+ map: Record<string, {
111
+ pin?: {
112
+ via: string;
113
+ model: string;
114
+ } | undefined;
115
+ pool?: {
116
+ mode: "any" | "ordered";
117
+ channels: string[];
118
+ } | undefined;
119
+ tier?: undefined;
120
+ prefer?: string[] | undefined;
121
+ escalate?: boolean | undefined;
122
+ }>;
123
+ floors: Record<string, "cheap" | "mid" | "frontier">;
124
+ learned: "on" | "off";
125
+ allowUnverifiedModels: boolean;
126
+ mode?: "partner-led" | "risk-based" | "staff-led" | undefined;
127
+ learnedTuning?: {
128
+ halfLifeRuns?: number | undefined;
129
+ availWeight?: number | undefined;
130
+ } | undefined;
131
+ explore?: {
132
+ mode?: "on" | "off" | undefined;
133
+ excludeShapes?: string[] | undefined;
134
+ excludeComplexityAtOrAbove?: number | null | undefined;
135
+ cap?: number | undefined;
136
+ } | undefined;
137
+ sla?: Record<string, number> | undefined;
138
+ allow?: {
139
+ adapters?: string[] | undefined;
140
+ models?: string[] | undefined;
141
+ } | undefined;
142
+ deny?: {
143
+ adapters?: string[] | undefined;
144
+ models?: string[] | undefined;
145
+ workers?: {
146
+ adapters?: string[] | undefined;
147
+ models?: string[] | undefined;
148
+ } | undefined;
149
+ } | undefined;
150
+ };
151
+ tiers: Record<string, {
152
+ vendor: string | null;
153
+ channel: "sub" | "api";
154
+ models: Record<string, "cheap" | "mid" | "frontier">;
155
+ modelOverrides?: Record<string, {
156
+ vendor?: string | undefined;
157
+ channel?: "sub" | "api" | undefined;
158
+ }> | undefined;
159
+ windows?: Record<string, number> | undefined;
160
+ }>;
161
+ pricing: Record<string, number>;
162
+ gates: {
163
+ build?: string | undefined;
164
+ test?: string | undefined;
165
+ lint?: string | undefined;
166
+ diffCap?: number | undefined;
167
+ byShape?: Partial<Record<"plan" | "spec" | "implement" | "tests" | "docs" | "migration" | "ui" | "refactor" | "chore", {
168
+ [x: string]: unknown;
169
+ acceptance?: boolean | undefined;
170
+ review?: boolean | undefined;
171
+ }>> | undefined;
172
+ };
173
+ judge: {
174
+ adapter: string;
175
+ model: string;
176
+ };
177
+ review: {
178
+ complexityThreshold: number;
179
+ timeoutMs: number;
180
+ required: boolean;
181
+ prefer?: string[] | undefined;
182
+ policy?: "full" | "judge-only" | undefined;
183
+ criticalPaths?: string[] | undefined;
184
+ };
185
+ consult: {
186
+ adapter: string;
187
+ model: string;
188
+ stallMinutes: number;
189
+ prefer?: string[] | undefined;
190
+ };
191
+ visibility: {
192
+ llm: "pane" | "headless";
193
+ keepPanes: "run" | "attempt" | "forever";
194
+ worker: "interactive" | "print";
195
+ workersPerTab: number;
196
+ };
197
+ setup?: string | undefined;
198
+ cost?: {
199
+ models?: Record<string, {
200
+ inPerMtok: number;
201
+ outPerMtok: number;
202
+ cacheReadPerMtok?: number | undefined;
203
+ rateDate?: string | undefined;
204
+ }> | undefined;
205
+ subs?: Record<string, {
206
+ planMonthly: number;
207
+ windowsPerMonthLow: number;
208
+ windowsPerMonthHigh: number;
209
+ }> | undefined;
210
+ } | undefined;
211
+ scope?: {
212
+ allowDeviations?: string[] | undefined;
213
+ } | undefined;
214
+ }, adapters?: WorkerAdapter[]): string;
215
+ /**
216
+ * Health surfaces use this cache-only reader. It intentionally receives adapters only to render
217
+ * their configured identities; it never invokes adapter.probe(), listModels(), or a model command.
218
+ */
219
+ export declare function cachedDoctorDiagnostics(cwd?: string, adapters?: WorkerAdapter[], cfg?: {
220
+ concurrency: number;
221
+ driver: "auto" | "herdr" | "subprocess" | "orca";
222
+ integrationBranchPrefix: string;
223
+ taskTimeoutMinutes: number;
224
+ contextWarnTokens: number;
225
+ routing: {
226
+ map: Record<string, {
227
+ pin?: {
228
+ via: string;
229
+ model: string;
230
+ } | undefined;
231
+ pool?: {
232
+ mode: "any" | "ordered";
233
+ channels: string[];
234
+ } | undefined;
235
+ tier?: undefined;
236
+ prefer?: string[] | undefined;
237
+ escalate?: boolean | undefined;
238
+ }>;
239
+ floors: Record<string, "cheap" | "mid" | "frontier">;
240
+ learned: "on" | "off";
241
+ allowUnverifiedModels: boolean;
242
+ mode?: "partner-led" | "risk-based" | "staff-led" | undefined;
243
+ learnedTuning?: {
244
+ halfLifeRuns?: number | undefined;
245
+ availWeight?: number | undefined;
246
+ } | undefined;
247
+ explore?: {
248
+ mode?: "on" | "off" | undefined;
249
+ excludeShapes?: string[] | undefined;
250
+ excludeComplexityAtOrAbove?: number | null | undefined;
251
+ cap?: number | undefined;
252
+ } | undefined;
253
+ sla?: Record<string, number> | undefined;
254
+ allow?: {
255
+ adapters?: string[] | undefined;
256
+ models?: string[] | undefined;
257
+ } | undefined;
258
+ deny?: {
259
+ adapters?: string[] | undefined;
260
+ models?: string[] | undefined;
261
+ workers?: {
262
+ adapters?: string[] | undefined;
263
+ models?: string[] | undefined;
264
+ } | undefined;
265
+ } | undefined;
266
+ };
267
+ tiers: Record<string, {
268
+ vendor: string | null;
269
+ channel: "sub" | "api";
270
+ models: Record<string, "cheap" | "mid" | "frontier">;
271
+ modelOverrides?: Record<string, {
272
+ vendor?: string | undefined;
273
+ channel?: "sub" | "api" | undefined;
274
+ }> | undefined;
275
+ windows?: Record<string, number> | undefined;
276
+ }>;
277
+ pricing: Record<string, number>;
278
+ gates: {
279
+ build?: string | undefined;
280
+ test?: string | undefined;
281
+ lint?: string | undefined;
282
+ diffCap?: number | undefined;
283
+ byShape?: Partial<Record<"plan" | "spec" | "implement" | "tests" | "docs" | "migration" | "ui" | "refactor" | "chore", {
284
+ [x: string]: unknown;
285
+ acceptance?: boolean | undefined;
286
+ review?: boolean | undefined;
287
+ }>> | undefined;
288
+ };
289
+ judge: {
290
+ adapter: string;
291
+ model: string;
292
+ };
293
+ review: {
294
+ complexityThreshold: number;
295
+ timeoutMs: number;
296
+ required: boolean;
297
+ prefer?: string[] | undefined;
298
+ policy?: "full" | "judge-only" | undefined;
299
+ criticalPaths?: string[] | undefined;
300
+ };
301
+ consult: {
302
+ adapter: string;
303
+ model: string;
304
+ stallMinutes: number;
305
+ prefer?: string[] | undefined;
306
+ };
307
+ visibility: {
308
+ llm: "pane" | "headless";
309
+ keepPanes: "run" | "attempt" | "forever";
310
+ worker: "interactive" | "print";
311
+ workersPerTab: number;
312
+ };
313
+ setup?: string | undefined;
314
+ cost?: {
315
+ models?: Record<string, {
316
+ inPerMtok: number;
317
+ outPerMtok: number;
318
+ cacheReadPerMtok?: number | undefined;
319
+ rateDate?: string | undefined;
320
+ }> | undefined;
321
+ subs?: Record<string, {
322
+ planMonthly: number;
323
+ windowsPerMonthLow: number;
324
+ windowsPerMonthHigh: number;
325
+ }> | undefined;
326
+ } | undefined;
327
+ scope?: {
328
+ allowDeviations?: string[] | undefined;
329
+ } | undefined;
330
+ }): string;
331
+ /** Repair is an actuator, but never a reason to run the paid diagnostic sensor. */
332
+ export declare function doctorFixOnly(cwd?: string): string;
99
333
  export declare function doctor(_argv: string[], cwd?: string, adapters?: WorkerAdapter[], opts?: DoctorOpts): Promise<string>;
100
334
  export {};
@@ -4,7 +4,7 @@ import { existsSync, readFileSync } from "node:fs";
4
4
  import { join } from "node:path";
5
5
  import { detectPackageManager, turboContinueFindings } from "../../gates/baseline.js";
6
6
  import { version } from "./version.js";
7
- import { allAdapters, binaryShadowWarnings, detectCandidateClis, flagDriftWarnings, modelAliasExclusions, modelAliasLine, probeAll, probeModels, resolveShellBinary, servableExclusions, servabilityLine, writeDoctor } from "../../adapters/registry.js";
7
+ import { allAdapters, binaryShadowWarnings, detectCandidateClis, doctorAgeMs, flagDriftWarnings, modelAliasExclusions, modelAliasLine, probeAll, probeModels, readDoctor, resolveShellBinary, servableExclusions, servabilityLine, writeDoctor } from "../../adapters/registry.js";
8
8
  import { CLAUDE_ALIAS_IDENTITY_STAMPS, claudeCode, resolveClaudeAliasIdentity } from "../../adapters/claude-code.js";
9
9
  import { shq } from "../../adapters/types.js";
10
10
  import { BANNER, compactTokens, dim, fail, kvRow, legend, ok, rule, statusRow, title } from "../../brand.js";
@@ -347,8 +347,142 @@ export function liveBenchStalenessFinding(now) {
347
347
  return undefined;
348
348
  return `LiveBench table pinned at ${LIVEBENCH_TABLE_DATE} is ${days} days old (>${LIVEBENCH_TABLE_MAX_AGE_DAYS}d) — list ${LIVEBENCH_RELEASES_URL} for a newer table_<date>.csv and bump LIVEBENCH_TABLE_DATE (advisory — tickmarkr never fetches that listing)`;
349
349
  }
350
+ function configuredModels(cfg, adapters) {
351
+ const allowed = adapters ? new Set(adapters.map((adapter) => adapter.id)) : undefined;
352
+ return Object.entries(cfg.tiers).flatMap(([adapter, tier]) => allowed && !allowed.has(adapter) ? [] :
353
+ Object.keys(tier.models ?? {}).map((model) => ({ adapter, model })));
354
+ }
355
+ function cacheAge(ageMs) {
356
+ return ageMs === null ? "unavailable" : `${Math.floor(ageMs / 60_000)}m`;
357
+ }
358
+ /**
359
+ * A no-call disclosure for the action that normal doctor performs. This is deliberately built
360
+ * from config alone: opening the disclosure must not turn a cache reader into a model purchase.
361
+ */
362
+ export function doctorProbePreflight(cwd = process.cwd(), cfg = loadConfig(cwd), adapters = allAdapters()) {
363
+ const models = configuredModels(cfg, adapters);
364
+ const scope = models.length
365
+ ? models.map(({ adapter, model }) => `${adapter}:${model}`).join(", ")
366
+ : "none (no configured models)";
367
+ const state = stateDirName(cwd);
368
+ return [
369
+ "tickmarkr doctor — probe preflight:",
370
+ `configured model-call scope: ${models.length} model${models.length === 1 ? "" : "s"} — ${scope}`,
371
+ `cache policy: explicit probing replaces ${state}/doctor.json; each installed configured model receives one bounded call, with a serial retry only after a first failure (a prior timeout skips that retry)`,
372
+ `affected destinations: ${state}/doctor.json (adapter and model verdict cache); temporary probe prompts are removed`,
373
+ "catalog-only alternative: tickmarkr doctor --refresh-catalog refreshes the catalog cache without adapter or model probes",
374
+ ].join("\n");
375
+ }
376
+ /**
377
+ * Health surfaces use this cache-only reader. It intentionally receives adapters only to render
378
+ * their configured identities; it never invokes adapter.probe(), listModels(), or a model command.
379
+ */
380
+ export function cachedDoctorDiagnostics(cwd = process.cwd(), adapters = allAdapters(), cfg = loadConfig(cwd)) {
381
+ const health = readDoctor(cwd);
382
+ const age = doctorAgeMs(cwd);
383
+ const configured = configuredModels(cfg, adapters);
384
+ const state = stateDirName(cwd);
385
+ const knownAdapters = new Set(adapters.map((adapter) => adapter.id));
386
+ const rows = configured.map(({ adapter, model }) => {
387
+ const cachedAdapter = health?.[adapter];
388
+ const cachedModel = cachedAdapter?.modelAuth?.[model];
389
+ const status = !health
390
+ ? "unknown (cache unavailable)"
391
+ : !knownAdapters.has(adapter)
392
+ ? "unknown (adapter unavailable to this reader)"
393
+ : !cachedAdapter
394
+ ? "unknown (adapter absent from cache)"
395
+ : !cachedAdapter.installed
396
+ ? "unavailable (adapter not installed)"
397
+ : !cachedAdapter.authed
398
+ ? "unavailable (adapter not authenticated)"
399
+ : !cachedModel
400
+ ? "unknown (never probed)"
401
+ : cachedModel.authed
402
+ ? `passed (cached ${cachedModel.probedAt})`
403
+ : `failed (cached ${cachedModel.reason ?? "probe failed"})`;
404
+ return ` ${adapter}:${model} ${status}`;
405
+ });
406
+ return [
407
+ "tickmarkr doctor — cached diagnostics:",
408
+ `cache source: ${health ? `${state}/doctor.json` : "unavailable"}`,
409
+ `cache age: ${cacheAge(age)}`,
410
+ "cache policy: read-only; adapter and model probes were not called",
411
+ ...(rows.length ? ["configured model status:", ...rows] : ["configured model status: none (no configured models)"]),
412
+ ].join("\n");
413
+ }
414
+ const RUNNER_CONFIG_PATHS = [
415
+ "jest.config.js",
416
+ "jest.config.cjs",
417
+ "jest.config.mjs",
418
+ "jest.config.ts",
419
+ "jest.config.json",
420
+ "vitest.config.ts",
421
+ "vitest.config.js",
422
+ "vitest.config.mts",
423
+ "vitest.config.mjs",
424
+ "vitest.config.cts",
425
+ ];
426
+ function runnerConfigSnapshot(cwd) {
427
+ return new Map(RUNNER_CONFIG_PATHS.flatMap((path) => {
428
+ const absolute = join(cwd, path);
429
+ return existsSync(absolute) ? [[path, readFileSync(absolute, "utf8")]] : [];
430
+ }));
431
+ }
432
+ function actualTextDiff(path, before, after) {
433
+ const oldLines = before.split("\n");
434
+ const newLines = after.split("\n");
435
+ let start = 0;
436
+ while (start < oldLines.length && start < newLines.length && oldLines[start] === newLines[start])
437
+ start += 1;
438
+ let oldEnd = oldLines.length;
439
+ let newEnd = newLines.length;
440
+ while (oldEnd > start && newEnd > start && oldLines[oldEnd - 1] === newLines[newEnd - 1]) {
441
+ oldEnd -= 1;
442
+ newEnd -= 1;
443
+ }
444
+ return [
445
+ `--- ${path} (before)`,
446
+ `+++ ${path} (after)`,
447
+ ...oldLines.slice(start, oldEnd).map((line) => `-${line}`),
448
+ ...newLines.slice(start, newEnd).map((line) => `+${line}`),
449
+ ].join("\n");
450
+ }
451
+ /** Repair is an actuator, but never a reason to run the paid diagnostic sensor. */
452
+ export function doctorFixOnly(cwd = process.cwd()) {
453
+ const before = runnerConfigSnapshot(cwd);
454
+ const finding = runnerIgnoreFinding(cwd);
455
+ const repair = finding?.verdict === "warn"
456
+ ? applyRunnerIgnore(cwd)
457
+ : { action: "none", detail: finding?.detail ?? "no recognized test runner" };
458
+ const verified = runnerIgnoreFinding(cwd);
459
+ const diffs = [...before].flatMap(([path, original]) => {
460
+ const current = readFileSync(join(cwd, path), "utf8");
461
+ return current === original ? [] : [actualTextDiff(path, original, current)];
462
+ });
463
+ const result = repair.action === "wrote" && verified?.verdict !== "pass"
464
+ ? "invalid repair result (verification did not pass)"
465
+ : `${repair.action} — ${repair.detail}`;
466
+ return [
467
+ "tickmarkr doctor --fix-only — repair-only:",
468
+ "model probes: skipped (repair-only; adapters were not called)",
469
+ "catalog: untouched",
470
+ `repair result: ${result}`,
471
+ `repair verification: ${verified?.verdict ?? "unavailable"}`,
472
+ "repair diff:",
473
+ ...(diffs.length ? diffs : ["(no runner-config bytes changed)"]),
474
+ ].join("\n");
475
+ }
350
476
  export async function doctor(_argv, cwd = process.cwd(), adapters = allAdapters(), opts = {}) {
351
- if (_argv.length === 1 && _argv[0] === "--refresh-catalog") {
477
+ if (_argv.includes("--fix-only"))
478
+ return doctorFixOnly(cwd);
479
+ if (_argv.includes("--cached") || _argv.includes("--cached-only")) {
480
+ return cachedDoctorDiagnostics(cwd, adapters);
481
+ }
482
+ if (_argv.includes("--probe-preflight") || _argv.includes("--preflight")) {
483
+ return doctorProbePreflight(cwd, loadConfig(cwd), adapters);
484
+ }
485
+ if (_argv.length === 1 && (_argv[0] === "--refresh-catalog" || _argv[0] === "--catalog-only")) {
352
486
  const refreshed = await refreshCatalogCommand({ repoRoot: cwd, fetcher: opts.catalogFetcher, now: opts.catalogNow });
353
487
  return refreshed.updated
354
488
  ? `tickmarkr doctor --refresh-catalog: model catalog refreshed — ${formatCatalogRefreshLegs(refreshed.legs)}${refreshed.warning ? `; ${refreshed.warning}` : ""}`
@@ -378,9 +512,9 @@ export async function doctor(_argv, cwd = process.cwd(), adapters = allAdapters(
378
512
  // banner at START — the logo greets the operator before the ~60s probe wait, never trailing it (operator report 2026-07-17)
379
513
  if (opts.banner !== false && visual())
380
514
  process.stdout.write(BANNER);
381
- // stderr, live: auth probes are real LLM calls (up to 30s per configured model) and the CLI
382
- // otherwise prints nothing until the end — silence here reads as a hang (v1.33.1)
383
- console.error("probing installed agent CLIs — one short LLM call per configured model, may take a minute...");
515
+ // stderr, live: disclose the paid boundary before the first adapter/model call. The separate
516
+ // --probe-preflight route exposes these same bytes without crossing that boundary.
517
+ console.error(`${doctorProbePreflight(cwd, cfg, adapters)}\nprobing installed agent CLIs — one short LLM call per configured model, may take a minute...`);
384
518
  const probeProgressTTY = process.stderr.isTTY === true;
385
519
  const health = await probeAll(adapters, { cwd });
386
520
  const kimiAdapter = adapters.find((a) => a.id === kimi.id);
@@ -8,34 +8,19 @@ import { modelProvider } from "../../gates/review.js";
8
8
  import { buildProofBundle, gateDeclined } from "../../report/bundle.js";
9
9
  import { compareRuns } from "../../report/compare.js";
10
10
  import { estimateCosts } from "../../report/cost.js";
11
+ import { assignmentChannel, buildOperatorRecord, formatChannelMoney, formatChannelTokens, formatTokenUsage as fields, totalTokens as total, } from "../../report/operator-record.js";
11
12
  import { cellsOf, cellSummary } from "../../route/profile.js";
12
13
  import { Journal, loadRoutingProfile } from "../../run/journal.js";
13
14
  import { deriveRunCockpitData } from "../../tui/cockpit/derive.js";
14
15
  const n = (x) => x.toLocaleString("en-US"); // explicit locale — CI/darwin flake guard
15
16
  const EM = "—";
16
- // TokenUsage fields that are actually present — filtered, never coalesced to zero (absent ⇒ unmetered).
17
- const fields = (u) => {
18
- const parts = [`in ${n(u.input)}`, `out ${n(u.output)}`];
19
- if (u.cacheRead !== undefined && u.cacheWrite !== undefined)
20
- parts.push(`cache r/w ${n(u.cacheRead)}/${n(u.cacheWrite)}`);
21
- if (u.reasoning !== undefined)
22
- parts.push(`reasoning ${n(u.reasoning)}`);
23
- return parts.join(" ");
24
- };
25
- const total = (u) => [u.input, u.output, u.cacheRead, u.cacheWrite, u.reasoning].filter((x) => x !== undefined).reduce((a, b) => a + b, 0);
26
17
  const firstLine = (s) => {
27
18
  if (typeof s !== "string" || !s)
28
19
  return EM;
29
20
  const i = s.indexOf("\n");
30
21
  return (i < 0 ? s : s.slice(0, i)) || EM;
31
22
  };
32
- const channelLabel = (data) => {
33
- const a = data.assignment;
34
- if (!a || typeof a !== "object")
35
- return EM;
36
- const { adapter, model } = a;
37
- return typeof adapter === "string" && typeof model === "string" ? `${adapter}:${model}` : EM;
38
- };
23
+ const channelLabel = (data) => assignmentChannel(data) ?? EM;
39
24
  const modelFromChannel = (channel) => channel.slice(channel.indexOf(":") + 1);
40
25
  const reviewerIdentity = (data) => {
41
26
  const details = typeof data.details === "string" ? data.details : "";
@@ -52,35 +37,10 @@ const reviewerIdentity = (data) => {
52
37
  : modelProvider(modelFromChannel(reviewer), vendor ?? "unknown");
53
38
  return { reviewer, provider, ...(vendor ? { vendor } : {}) };
54
39
  };
55
- const rateBasis = (row) => {
56
- if (!row.rate)
57
- return EM;
58
- const cache = row.rate.cacheReadPerMtok === undefined ? "" : `; cache-read $${row.rate.cacheReadPerMtok}/Mtok`;
59
- const date = row.rate.rateDate === undefined ? "" : `; rate date ${row.rate.rateDate}`;
60
- return `in/out $${row.rate.inPerMtok}/$${row.rate.outPerMtok}/Mtok${cache}${date}`;
61
- };
62
40
  const priceLine = (row) => {
63
41
  const windows = row.channel === "sub" && row.subPlan ? `windows: ${n(row.attempts)}` : `attempts/windows: ${n(row.attempts)}`;
64
- const tokenText = row.tokens
65
- ? `tokens: ${row.partialMetering ? "≥ " : ""}${fields(row.tokens)} (${n(total(row.tokens))} tokens)`
66
- : "tokens: unmetered";
67
- const prices = [];
68
- const bases = [];
69
- if (row.apiUsd !== undefined)
70
- prices.push(`price: $${row.apiUsd.toFixed(6)}`);
71
- if (row.amortizedUsd !== undefined && row.subPlan !== undefined) {
72
- const [low, high] = row.amortizedUsd;
73
- prices.push(`price: $${low.toFixed(6)}–$${high.toFixed(6)} amortized`);
74
- bases.push(`${n(row.attempts)} windows × $${row.subPlan.planMonthly}/month ÷ ${row.subPlan.windowsPerMonthHigh}–${row.subPlan.windowsPerMonthLow} windows/month`);
75
- }
76
- if (row.counterfactualUsd !== undefined)
77
- prices.push(`API-equivalent: $${row.counterfactualUsd.toFixed(6)}`);
78
- if (row.rate)
79
- bases.push(rateBasis(row));
80
- if (!prices.length)
81
- prices.push("price: not measurable");
82
- if (!bases.length)
83
- bases.push(row.reason || "not recorded");
42
+ const tokenText = `tokens: ${formatChannelTokens(row)}`;
43
+ const { prices, bases } = formatChannelMoney(row);
84
44
  return `- **${row.adapter}:${row.model}** — ${windows}; ${tokenText}; ${prices.join("; ")}; basis: ${bases.join("; ")}`;
85
45
  };
86
46
  const journalUsage = (events, known) => {
@@ -257,6 +217,17 @@ export function renderMarkdownRecord(runId, events, prices = [], rows = []) {
257
217
  `- **escalations:** ${events.filter((e) => e.event === "escalation").length}`,
258
218
  "",
259
219
  ];
220
+ lines.push("## Channels", "");
221
+ const operatorRecords = buildOperatorRecord(events, prices);
222
+ if (operatorRecords.length) {
223
+ for (const row of operatorRecords) {
224
+ lines.push(`- **${row.channel}** — worker: ${row.worker}, review: ${row.review}, consult: ${row.consult}; tokens: ${row.tokens}; money: ${row.money}`);
225
+ }
226
+ }
227
+ else {
228
+ lines.push("- no channel activity recorded");
229
+ }
230
+ lines.push("");
260
231
  lines.push("## Audit trail", "");
261
232
  for (const taskId of taskIds(events)) {
262
233
  const dispatches = events.filter((e) => e.taskId === taskId && e.event === "task-dispatch");
@@ -1,21 +1,51 @@
1
+ import { createInterface } from "node:readline/promises";
1
2
  import { isAbsolute, join, relative } from "node:path";
2
3
  import { parseArgs } from "node:util";
3
4
  import { allAdapters } from "../../adapters/registry.js";
4
5
  import { loadConfig } from "../../config/config.js";
5
- import { scopeIntent } from "../../plan/scope.js";
6
+ import { formatScopePreview, previewScope, scopeIntent } from "../../plan/scope.js";
7
+ // R11/R16/R45 (C10): --preview is a read-only disclosure (cached candidate, destination, call
8
+ // budget) — it never probes, never calls a model, and never writes. Active scope reuses that same
9
+ // disclosure to build a confirmation question, then requires TTY "y" or --yes before it invokes the
10
+ // unchanged production authoring path (scopeIntent); a non-TTY shell without --yes refuses rather
11
+ // than silently proceeding (the unlock.ts confirm() convention).
6
12
  export async function scope(argv, cwd = process.cwd(), adapters = allAdapters(), driver) {
7
13
  const { values, positionals } = parseArgs({
8
14
  args: argv,
9
- options: { force: { type: "boolean" } },
15
+ options: { force: { type: "boolean" }, preview: { type: "boolean" }, yes: { type: "boolean" } },
10
16
  allowPositionals: true,
11
17
  });
12
18
  if (positionals.length !== 1)
13
- throw new Error("usage: tickmarkr scope <intent-file> [--force]");
19
+ throw new Error("usage: tickmarkr scope <intent-file> [--preview] [--yes] [--force]");
14
20
  const source = positionals[0];
15
21
  const intentFile = isAbsolute(source) ? source : join(cwd, source);
16
- const result = await scopeIntent(intentFile, cwd, {
17
- cfg: loadConfig(cwd), adapters, driver, force: values.force,
18
- });
22
+ const cfg = loadConfig(cwd);
23
+ const preview = previewScope(intentFile, cwd, { cfg, adapters });
24
+ if (values.preview)
25
+ return formatScopePreview(preview);
26
+ const confirm = async (question) => {
27
+ if (values.yes)
28
+ return true;
29
+ if (process.stdin.isTTY !== true || process.stdout.isTTY !== true)
30
+ return false;
31
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
32
+ try {
33
+ return /^(?:y|yes)$/i.test((await rl.question(`${question} [y/N] `)).trim());
34
+ }
35
+ finally {
36
+ rl.close();
37
+ }
38
+ };
39
+ const candidateLabel = preview.candidate
40
+ ? `${preview.candidate.adapter}:${preview.candidate.model}`
41
+ : `an unknown candidate (${preview.cached ? "no verified channel in the doctor cache" : "no doctor cache"})`;
42
+ const question = `Draft ${relative(cwd, preview.specFile)} via ${candidateLabel} using up to ${preview.authoringBudget} authoring call${preview.authoringBudget === 1 ? "" : "s"} plus ${preview.probeCalls} probe call${preview.probeCalls === 1 ? "" : "s"}?`;
43
+ if (!(await confirm(question))) {
44
+ throw new Error("scope: not confirmed (pass --yes or confirm at a TTY)");
45
+ }
46
+ // Bind dispatch to exactly the candidate just disclosed and confirmed (R11/R45 repair) — scopeIntent
47
+ // fails loud rather than silently authoring against a different channel a fresh probe turns up.
48
+ const result = await scopeIntent(intentFile, cwd, { cfg, adapters, driver, force: values.force, candidate: preview.candidate });
19
49
  const tasks = `${result.tasks} task${result.tasks === 1 ? "" : "s"}`;
20
50
  const calls = `${result.attempts} LLM call${result.attempts === 1 ? "" : "s"}`;
21
51
  return `scoped ${source} → ${relative(cwd, result.specFile)} (${tasks}, ${calls})`;
@@ -1,3 +1,4 @@
1
+ import { type OperatorRecordRow } from "../../report/operator-record.js";
1
2
  export declare const STATS_COLUMNS: readonly ["author", "reviewer", "dispatches", "deliveries", "delivery rate", "attempts to green", "real reds", "infra reds", "within-task rescues"];
2
3
  export interface ChannelStats {
3
4
  author: string;
@@ -14,6 +15,7 @@ export interface ChannelStats {
14
15
  export interface StatsReport {
15
16
  runs: number;
16
17
  channels: ChannelStats[];
18
+ operatorRecords?: OperatorRecordRow[];
17
19
  }
18
20
  /** Reduce every readable run journal under the repository's state directory. */
19
21
  export declare function collectChannelStats(cwd?: string): StatsReport;