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.
- package/README.md +110 -24
- package/dist/cli/commands/approve.d.ts +42 -0
- package/dist/cli/commands/approve.js +80 -13
- package/dist/cli/commands/doctor.d.ts +234 -0
- package/dist/cli/commands/doctor.js +139 -5
- package/dist/cli/commands/report.js +15 -44
- package/dist/cli/commands/scope.js +36 -6
- package/dist/cli/commands/stats.d.ts +2 -0
- package/dist/cli/commands/stats.js +42 -23
- package/dist/cli/commands/status.js +20 -4
- package/dist/cli/commands/ui.js +42 -53
- package/dist/cli/commands/unlock.d.ts +1 -1
- package/dist/cli/commands/unlock.js +59 -9
- package/dist/cli/help.d.ts +219 -0
- package/dist/cli/help.js +212 -0
- package/dist/cli/index.d.ts +42 -2
- package/dist/cli/index.js +23 -8
- package/dist/drivers/herdr.d.ts +7 -2
- package/dist/drivers/herdr.js +78 -48
- package/dist/drivers/orca.d.ts +3 -2
- package/dist/drivers/orca.js +43 -4
- package/dist/drivers/subprocess.d.ts +1 -0
- package/dist/drivers/subprocess.js +3 -0
- package/dist/drivers/types.d.ts +14 -0
- package/dist/gates/artifact-manifest.d.ts +50 -0
- package/dist/gates/artifact-manifest.js +23 -0
- package/dist/plan/scope.d.ts +25 -0
- package/dist/plan/scope.js +92 -12
- package/dist/report/operator-record.d.ts +49 -0
- package/dist/report/operator-record.js +137 -0
- package/dist/run/daemon.d.ts +3 -4
- package/dist/run/daemon.js +18 -10
- package/dist/run/lock.d.ts +59 -2
- package/dist/run/lock.js +184 -26
- package/dist/run/operator-state.d.ts +86 -0
- package/dist/run/operator-state.js +165 -0
- package/dist/run/supervision.d.ts +32 -0
- package/dist/run/supervision.js +138 -17
- package/dist/tui/cockpit/capture.d.ts +19 -0
- package/dist/tui/cockpit/capture.js +89 -1
- package/dist/tui/cockpit/components.d.ts +15 -1
- package/dist/tui/cockpit/components.js +79 -9
- package/dist/tui/cockpit/decision-actions.d.ts +147 -0
- package/dist/tui/cockpit/decision-actions.js +315 -0
- package/dist/tui/cockpit/derive.d.ts +1 -1
- package/dist/tui/cockpit/derive.js +2 -0
- package/dist/tui/cockpit/evidence-view.d.ts +119 -0
- package/dist/tui/cockpit/evidence-view.js +210 -0
- package/dist/tui/cockpit/home-view.d.ts +88 -0
- package/dist/tui/cockpit/home-view.js +240 -0
- package/dist/tui/cockpit/keys.d.ts +125 -0
- package/dist/tui/cockpit/keys.js +31 -0
- package/dist/tui/cockpit/layout.d.ts +14 -0
- package/dist/tui/cockpit/layout.js +15 -0
- package/dist/tui/cockpit/live-runtime.d.ts +46 -0
- package/dist/tui/cockpit/live-runtime.js +683 -0
- package/dist/tui/cockpit/live-store.d.ts +289 -0
- package/dist/tui/cockpit/live-store.js +308 -0
- package/dist/tui/cockpit/live.d.ts +21 -1
- package/dist/tui/cockpit/live.js +12 -1
- package/dist/tui/cockpit/run-view.d.ts +100 -0
- package/dist/tui/cockpit/run-view.js +202 -0
- package/dist/tui/cockpit/shell.d.ts +50 -0
- package/dist/tui/cockpit/shell.js +74 -0
- package/dist/tui/cockpit/theme.d.ts +27 -0
- package/dist/tui/cockpit/theme.js +21 -0
- package/package.json +1 -1
- package/skills/tickmarkr-auto/SKILL.md +10 -1
- package/skills/tickmarkr-loop/SKILL.md +72 -1
- package/skills/tickmarkr-overseer/SKILL.md +12 -0
|
@@ -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.
|
|
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:
|
|
382
|
-
//
|
|
383
|
-
console.error(
|
|
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
|
|
65
|
-
|
|
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
|
|
17
|
-
|
|
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;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { existsSync, readdirSync } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { stateDirName } from "../../graph/graph.js";
|
|
4
|
+
import { assignmentChannel, buildOperatorRecord, formatOperatorRecordRow } from "../../report/operator-record.js";
|
|
4
5
|
import { Journal } from "../../run/journal.js";
|
|
5
6
|
import { normalizeGateOutcome } from "../../run/outcome.js";
|
|
6
7
|
// The operator-facing schema is deliberately closed. Adding a fact to the table therefore requires
|
|
@@ -17,20 +18,15 @@ export const STATS_COLUMNS = [
|
|
|
17
18
|
"infra reds",
|
|
18
19
|
"within-task rescues",
|
|
19
20
|
];
|
|
20
|
-
const assignmentAuthor =
|
|
21
|
-
const assignment = data.assignment;
|
|
22
|
-
if (!assignment || typeof assignment !== "object")
|
|
23
|
-
return undefined;
|
|
24
|
-
const { adapter, model } = assignment;
|
|
25
|
-
return typeof adapter === "string" && typeof model === "string"
|
|
26
|
-
? `${adapter}:${model}`
|
|
27
|
-
: undefined;
|
|
28
|
-
};
|
|
21
|
+
const assignmentAuthor = assignmentChannel;
|
|
29
22
|
// Current journals write the reviewer into review prose; the structured field is also accepted so
|
|
30
23
|
// journals produced through gateResultJournalData retain their stronger identity representation.
|
|
31
24
|
const reviewerFrom = (data) => {
|
|
32
25
|
if (typeof data.reviewer === "string" && data.reviewer.trim())
|
|
33
26
|
return data.reviewer.trim();
|
|
27
|
+
const meta = typeof data.meta === "object" && data.meta !== null ? data.meta : undefined;
|
|
28
|
+
if (meta && "reviewer" in meta && typeof meta.reviewer === "string" && meta.reviewer.trim())
|
|
29
|
+
return meta.reviewer.trim();
|
|
34
30
|
if (typeof data.details !== "string")
|
|
35
31
|
return undefined;
|
|
36
32
|
return /\breviewer(?:\s+|:\s*)([\w@./+-]+:[\w@./+-]+)/iu.exec(data.details)?.[1];
|
|
@@ -79,8 +75,10 @@ export function collectChannelStats(cwd = process.cwd()) {
|
|
|
79
75
|
}
|
|
80
76
|
return channel;
|
|
81
77
|
};
|
|
78
|
+
const allEvents = [];
|
|
82
79
|
for (const runId of runIds) {
|
|
83
80
|
const events = Journal.open(cwd, runId).read();
|
|
81
|
+
allEvents.push(...events);
|
|
84
82
|
const tasks = new Map();
|
|
85
83
|
for (const [eventIndex, event] of events.entries()) {
|
|
86
84
|
if (!event.taskId)
|
|
@@ -124,6 +122,16 @@ export function collectChannelStats(cwd = process.cwd()) {
|
|
|
124
122
|
}
|
|
125
123
|
continue;
|
|
126
124
|
}
|
|
125
|
+
if (event.event === "review-leg2") {
|
|
126
|
+
const author = dispatchedAuthorFor(history, event.data)
|
|
127
|
+
?? (typeof event.data.author === "string" && event.data.author.includes(":") ? event.data.author : undefined);
|
|
128
|
+
if (author) {
|
|
129
|
+
const reviewer = reviewerFrom(event.data);
|
|
130
|
+
if (reviewer)
|
|
131
|
+
channelFor(author).reviewers.add(reviewer);
|
|
132
|
+
}
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
127
135
|
if (event.event !== "gate-result")
|
|
128
136
|
continue;
|
|
129
137
|
const author = dispatchedAuthorFor(history, event.data);
|
|
@@ -170,26 +178,37 @@ export function collectChannelStats(cwd = process.cwd()) {
|
|
|
170
178
|
infraReds: channel.infraReds,
|
|
171
179
|
rescues: [...channel.rescues].sort((a, b) => a.localeCompare(b, "en")),
|
|
172
180
|
})),
|
|
181
|
+
operatorRecords: buildOperatorRecord(allEvents),
|
|
173
182
|
};
|
|
174
183
|
}
|
|
175
184
|
const percent = (rate) => `${Number((rate * 100).toFixed(1))}%`;
|
|
176
185
|
export function renderStats(report) {
|
|
177
186
|
const lines = [`tickmarkr stats — ${report.runs} run${report.runs === 1 ? "" : "s"}`];
|
|
178
|
-
if (report.channels.length === 0)
|
|
187
|
+
if (report.channels.length === 0 && (!report.operatorRecords || report.operatorRecords.length === 0)) {
|
|
179
188
|
return [...lines, "no channels"].join("\n");
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
lines.push(
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
189
|
+
}
|
|
190
|
+
if (report.channels.length > 0) {
|
|
191
|
+
lines.push(STATS_COLUMNS.join(" | "));
|
|
192
|
+
for (const channel of report.channels) {
|
|
193
|
+
lines.push([
|
|
194
|
+
channel.author,
|
|
195
|
+
channel.reviewers.join(", ") || "—",
|
|
196
|
+
channel.dispatches,
|
|
197
|
+
channel.deliveries,
|
|
198
|
+
percent(channel.deliveryRate),
|
|
199
|
+
channel.attemptsToGreen === null ? "—" : Number(channel.attemptsToGreen.toFixed(2)),
|
|
200
|
+
channel.realReds,
|
|
201
|
+
channel.infraReds,
|
|
202
|
+
channel.rescues.join("; ") || "—",
|
|
203
|
+
].join(" | "));
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
if (report.operatorRecords && report.operatorRecords.length > 0) {
|
|
207
|
+
lines.push("");
|
|
208
|
+
lines.push("channels:");
|
|
209
|
+
for (const row of report.operatorRecords) {
|
|
210
|
+
lines.push(` ${formatOperatorRecordRow(row)}`);
|
|
211
|
+
}
|
|
193
212
|
}
|
|
194
213
|
return lines.join("\n");
|
|
195
214
|
}
|
|
@@ -13,9 +13,8 @@ import { isPidLive, runLockRunId, runStatusLine } from "../../run/lock.js";
|
|
|
13
13
|
import { normalizeGateOutcome } from "../../run/outcome.js";
|
|
14
14
|
import { desiredPanes } from "../../run/reconcile.js";
|
|
15
15
|
import { normalizeStallSnapshot } from "../../run/stall.js";
|
|
16
|
-
import { armWatchSupervision, readSupervision, supervisionText } from "../../run/supervision.js";
|
|
16
|
+
import { armWatchSupervision, observeNamedRun, WATCH_OWNER_ENV, readSupervision, supervisionText } from "../../run/supervision.js";
|
|
17
17
|
import { deriveRunCockpitData, } from "../../tui/cockpit/derive.js";
|
|
18
|
-
import { COCKPIT_COLUMN_FLOOR } from "../../tui/cockpit/layout.js";
|
|
19
18
|
import { cellWidth, fitCells, wrapCells } from "../../tui/cockpit/width.js";
|
|
20
19
|
import { version } from "./version.js";
|
|
21
20
|
// The board is a LIVE surface, so it draws through brand.ts's operator role map and never the vivid
|
|
@@ -1093,7 +1092,7 @@ binaryVersion, now = Date.now(), animationFrame = 0, workerLiveness = new Map(),
|
|
|
1093
1092
|
}
|
|
1094
1093
|
// TTY: the operator-approved task table. The daemon gives this surface a ~110-column pane first;
|
|
1095
1094
|
// narrower terminals keep the same facts in stacked rows. Every cut/wrap goes through width.ts.
|
|
1096
|
-
const boardColumns = Math.max(
|
|
1095
|
+
const boardColumns = Math.max(40, width);
|
|
1097
1096
|
const dot = dim(" · ");
|
|
1098
1097
|
const separator = ` ${dim("│")} `;
|
|
1099
1098
|
const tipFailed = tipPhase?.state === "failed";
|
|
@@ -1343,6 +1342,15 @@ export async function status(argv, cwd = process.cwd(), opts = {}) {
|
|
|
1343
1342
|
const sleep = opts.sleep ?? defaultSleep;
|
|
1344
1343
|
const now = opts.now ?? Date.now;
|
|
1345
1344
|
const bounded = Number.isFinite(iterations);
|
|
1345
|
+
// Bounded snapshots and printed twins never import Ink. Public interactive watch
|
|
1346
|
+
// selects its run once through the same launcher as manual UI.
|
|
1347
|
+
if (!bounded && !eventStream && !argv.includes("--plain") && process.stdin.isTTY === true && process.stdout.isTTY === true) {
|
|
1348
|
+
const { ui } = await import("./ui.js");
|
|
1349
|
+
const result = await ui([...(namedRunId ? [namedRunId] : []), "--view", "run"], {}, cwd);
|
|
1350
|
+
if (typeof result !== "string")
|
|
1351
|
+
throw new Error(result.out);
|
|
1352
|
+
return "";
|
|
1353
|
+
}
|
|
1346
1354
|
const frames = [];
|
|
1347
1355
|
const eventLines = [];
|
|
1348
1356
|
const sep = "\n---\n";
|
|
@@ -1455,7 +1463,8 @@ export async function status(argv, cwd = process.cwd(), opts = {}) {
|
|
|
1455
1463
|
// it INVERTS it: armSupervision's interval outlives the board in any host that outlives one board, so
|
|
1456
1464
|
// a dead board keeps writing and reads ARMED. That is the over-claiming direction, the one an operator
|
|
1457
1465
|
// acts on, and the one this instrument exists to close.
|
|
1458
|
-
const
|
|
1466
|
+
const boardObservation = !bounded && namedRunId && process.env[WATCH_OWNER_ENV] ? observeNamedRun(cwd, namedRunId) : undefined;
|
|
1467
|
+
const armed = bounded || boardObservation ? undefined : armWatchSupervision(cwd, opts.supervisionBeatMs);
|
|
1459
1468
|
let alternateScreen = false;
|
|
1460
1469
|
const leaveAlternateScreen = () => {
|
|
1461
1470
|
if (!alternateScreen)
|
|
@@ -1491,6 +1500,8 @@ export async function status(argv, cwd = process.cwd(), opts = {}) {
|
|
|
1491
1500
|
if (exitingFromSignal)
|
|
1492
1501
|
return;
|
|
1493
1502
|
exitingFromSignal = true;
|
|
1503
|
+
armed?.disarm();
|
|
1504
|
+
boardObservation?.close();
|
|
1494
1505
|
if (titleSaved) {
|
|
1495
1506
|
process.removeListener("exit", restoreTitle);
|
|
1496
1507
|
restoreTitle();
|
|
@@ -1507,6 +1518,8 @@ export async function status(argv, cwd = process.cwd(), opts = {}) {
|
|
|
1507
1518
|
process.stdout.write(ENTER_ALTERNATE_SCREEN);
|
|
1508
1519
|
alternateScreen = true;
|
|
1509
1520
|
process.once("exit", leaveAlternateScreen);
|
|
1521
|
+
}
|
|
1522
|
+
if (!bounded) {
|
|
1510
1523
|
for (const [signal, code] of [["SIGINT", 130], ["SIGTERM", 143]]) {
|
|
1511
1524
|
const handler = exitFromSignal(code);
|
|
1512
1525
|
signalHandlers.push([signal, handler]);
|
|
@@ -1515,6 +1528,8 @@ export async function status(argv, cwd = process.cwd(), opts = {}) {
|
|
|
1515
1528
|
}
|
|
1516
1529
|
try {
|
|
1517
1530
|
for (let i = 0; i < iterations; i++) {
|
|
1531
|
+
if (boardObservation?.stopRequested())
|
|
1532
|
+
break;
|
|
1518
1533
|
const nowMs = now();
|
|
1519
1534
|
const decisionEvents = eventStream || webhookUrl ? consumeDecisionEvents() : [];
|
|
1520
1535
|
let frame;
|
|
@@ -1558,6 +1573,7 @@ export async function status(argv, cwd = process.cwd(), opts = {}) {
|
|
|
1558
1573
|
// armSupervision's presence files). Otherwise the first pane closed would render the second
|
|
1559
1574
|
// pane's own tier down while it is drawing frames.
|
|
1560
1575
|
armed?.disarm();
|
|
1576
|
+
boardObservation?.close();
|
|
1561
1577
|
if (titleSaved) {
|
|
1562
1578
|
process.removeListener("exit", restoreTitle);
|
|
1563
1579
|
restoreTitle();
|
package/dist/cli/commands/ui.js
CHANGED
|
@@ -1,64 +1,53 @@
|
|
|
1
|
+
import { join } from "node:path";
|
|
2
|
+
import { Journal, parseRunId } from "../../run/journal.js";
|
|
3
|
+
import { stateDirName } from "../../graph/graph.js";
|
|
4
|
+
import { JournalTail } from "../../tui/cockpit/live-store.js";
|
|
1
5
|
const NON_TTY_MSG = "tickmarkr ui: the cockpit requires a TTY — use `tickmarkr fleet --print` or `tickmarkr status --watch` for line-mode output";
|
|
2
|
-
/**
|
|
3
|
-
* The setup surface's own flag. `tickmarkr ui --setup [run-id]` opens the one
|
|
4
|
-
* surface that writes — parked human decisions, approved or upheld through the
|
|
5
|
-
* production command path behind an explicit confirm.
|
|
6
|
-
*/
|
|
7
|
-
const SETUP_FLAG = "--setup";
|
|
8
6
|
export async function ui(argv, io = {}, cwd = process.cwd()) {
|
|
9
7
|
const input = io.input ?? process.stdin;
|
|
10
8
|
const output = io.output ?? process.stdout;
|
|
11
|
-
if (input.isTTY !== true || output.isTTY !== true)
|
|
9
|
+
if (input.isTTY !== true || output.isTTY !== true)
|
|
12
10
|
return { out: NON_TTY_MSG, code: 1 };
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
let runId;
|
|
11
|
+
let explicit;
|
|
12
|
+
let view = "home";
|
|
13
|
+
let setup = false;
|
|
14
|
+
for (let i = 0; i < argv.length; i++) {
|
|
15
|
+
const arg = argv[i];
|
|
16
|
+
if (arg === "--setup")
|
|
17
|
+
setup = true;
|
|
18
|
+
else if (arg === "--view") {
|
|
19
|
+
const value = argv[++i];
|
|
20
|
+
if (value !== "home" && value !== "run" && value !== "evidence")
|
|
21
|
+
return { out: "tickmarkr ui: --view must be home, run or evidence", code: 1 };
|
|
22
|
+
view = value;
|
|
23
|
+
}
|
|
24
|
+
else if (arg.startsWith("-"))
|
|
25
|
+
return { out: `tickmarkr ui: unknown flag ${arg}`, code: 1 };
|
|
26
|
+
else if (explicit !== undefined)
|
|
27
|
+
return { out: "tickmarkr ui: expected at most one run ID", code: 1 };
|
|
28
|
+
else
|
|
29
|
+
explicit = arg;
|
|
30
|
+
}
|
|
31
|
+
let runId = null;
|
|
34
32
|
try {
|
|
35
|
-
runId =
|
|
33
|
+
runId = explicit === undefined ? Journal.latestRunId(cwd, { withJournal: true }) : parseRunId(explicit);
|
|
34
|
+
// Refuse an invalid explicit target before importing or borrowing the terminal.
|
|
35
|
+
if (runId) {
|
|
36
|
+
const initial = new JournalTail(join(cwd, stateDirName(cwd), "runs", runId, "journal.jsonl")).poll();
|
|
37
|
+
if (initial.status === "unreadable" || initial.status === "corrupt")
|
|
38
|
+
throw new Error(initial.error?.error ?? initial.errors[0]?.error ?? "journal corrupt");
|
|
39
|
+
}
|
|
36
40
|
}
|
|
37
41
|
catch (error) {
|
|
38
|
-
return { out: `tickmarkr ui: ${error.message}`, code: 1 };
|
|
39
|
-
}
|
|
40
|
-
if (runId === null) {
|
|
41
|
-
return {
|
|
42
|
-
out: "tickmarkr ui: no engagement found — start one with `tickmarkr run` first",
|
|
43
|
-
code: 1,
|
|
44
|
-
};
|
|
42
|
+
return { out: `tickmarkr ui: cannot read engagement ${runId ?? explicit ?? "latest"}: ${error.message}`, code: 1 };
|
|
45
43
|
}
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
code: 1,
|
|
55
|
-
};
|
|
56
|
-
}
|
|
57
|
-
if (setup) {
|
|
58
|
-
const { runSetupDecisionsCockpit } = await import("../../tui/cockpit/setup-cockpit.js");
|
|
59
|
-
await runSetupDecisionsCockpit({ input, output, cwd, runId });
|
|
60
|
-
return "ui: closed";
|
|
61
|
-
}
|
|
62
|
-
await live.runLiveCockpit({ input, output, cwd, runId, binaryVersion });
|
|
44
|
+
const { runLiveCockpit } = await import("../../tui/cockpit/live.js");
|
|
45
|
+
const { version } = await import("./version.js");
|
|
46
|
+
const options = {
|
|
47
|
+
input, output, cwd, runId: runId ?? "run-no-run", binaryVersion: await version(),
|
|
48
|
+
observeRun: runId !== null, initialView: runId === null ? "home" : setup ? "run" : view,
|
|
49
|
+
initialParks: setup,
|
|
50
|
+
};
|
|
51
|
+
await runLiveCockpit(options);
|
|
63
52
|
return "ui: closed";
|
|
64
53
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare function unlock(
|
|
1
|
+
export declare function unlock(argv: string[], cwd?: string): Promise<string>;
|