tickmarkr 1.97.0 → 2.1.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 (47) hide show
  1. package/README.md +19 -1
  2. package/dist/brand.d.ts +28 -0
  3. package/dist/brand.js +41 -0
  4. package/dist/cli/commands/compile.js +32 -1
  5. package/dist/cli/commands/doctor.d.ts +19 -0
  6. package/dist/cli/commands/doctor.js +59 -0
  7. package/dist/cli/commands/init.js +5 -2
  8. package/dist/cli/commands/resume.js +25 -8
  9. package/dist/cli/commands/run.d.ts +61 -1
  10. package/dist/cli/commands/run.js +372 -18
  11. package/dist/cli/commands/status.js +145 -28
  12. package/dist/cli/index.d.ts +1 -1
  13. package/dist/cli/index.js +1 -1
  14. package/dist/compile/collateral.d.ts +25 -0
  15. package/dist/compile/collateral.js +46 -11
  16. package/dist/compile/native.js +10 -0
  17. package/dist/config/config.d.ts +1 -0
  18. package/dist/config/config.js +2 -2
  19. package/dist/drivers/herdr.d.ts +19 -13
  20. package/dist/drivers/herdr.js +90 -26
  21. package/dist/drivers/index.d.ts +5 -1
  22. package/dist/drivers/index.js +16 -1
  23. package/dist/drivers/orca.d.ts +189 -0
  24. package/dist/drivers/orca.js +879 -0
  25. package/dist/drivers/types.d.ts +2 -0
  26. package/dist/gates/acceptance.js +17 -7
  27. package/dist/gates/llm.d.ts +19 -0
  28. package/dist/gates/llm.js +104 -6
  29. package/dist/gates/run-gates.d.ts +18 -0
  30. package/dist/gates/run-gates.js +195 -29
  31. package/dist/gates/scope.d.ts +9 -1
  32. package/dist/gates/scope.js +22 -2
  33. package/dist/graph/graph.d.ts +1 -0
  34. package/dist/graph/graph.js +19 -2
  35. package/dist/report/compare.js +17 -2
  36. package/dist/run/daemon.d.ts +1 -8
  37. package/dist/run/daemon.js +231 -246
  38. package/dist/run/environment.d.ts +18 -1
  39. package/dist/run/environment.js +19 -2
  40. package/dist/run/journal.d.ts +50 -3
  41. package/dist/run/journal.js +181 -5
  42. package/dist/run/protocol.d.ts +4 -4
  43. package/dist/run/stall.d.ts +30 -0
  44. package/dist/run/stall.js +173 -0
  45. package/dist/tui/ink/init-app.js +14 -4
  46. package/package.json +1 -1
  47. package/skills/tickmarkr-overseer/SKILL.md +27 -15
package/README.md CHANGED
@@ -13,7 +13,8 @@ tickmarkr is a spec-driven orchestration harness for AI coding agent CLIs. You w
13
13
  acceptance criteria; the engine routes tasks to the best installed agent CLI (claude-code, codex,
14
14
  cursor-agent, opencode, grok, pi, kimi) by cost and capability, dispatches work in git worktrees for
15
15
  change isolation — as interactive TUIs when running under [herdr](https://herdr.dev), headless
16
- subprocesses otherwise — and independently verifies each committed result by checking for no new
16
+ subprocesses otherwise, or in [Orca](https://onorca.dev) terminals when you name that driver
17
+ yourself — and independently verifies each committed result by checking for no new
17
18
  baseline failures per task, then strictly verifying the integration tip. Green tasks consolidate onto a
18
19
  `tickmarkr/<runId>` branch; merging to your mainline is always your call, never automated. Engage
19
20
  with full visibility into routing decisions, worker progress, and gate verdicts — or run headless
@@ -250,6 +251,23 @@ and first-attempt success rate. Cost reporting follows strict honesty rules and
250
251
  When running under [herdr](https://herdr.dev), tickmarkr creates a labeled pane-and-tab workspace
251
252
  for real-time visibility (optional — omit `--driver herdr` or run headless if preferred).
252
253
 
254
+ ### Orca: an explicit-selection execution surface
255
+
256
+ [Orca](https://onorca.dev) is the third execution surface, and the only one you must ask for by
257
+ name: `--driver orca` or `driver: orca` in config. `--driver auto` never selects it — auto picks
258
+ herdr when a herdr session is live and subprocess otherwise — so Orca is never inherited from an
259
+ ambient environment variable, and an Orca that is installed but unreachable is not silently
260
+ downgraded to a hidden subprocess worker either. Naming it is the whole gate; its runtime failures
261
+ stay Orca's, reported as failures.
262
+
263
+ What Orca supplies is terminals. What tickmarkr keeps is everything that decides whether work
264
+ ships: **it creates and owns the git worktree** for every task (Orca is told which checkout to bind
265
+ its terminal to, and never makes one), **it runs the full gate battery** — build, test, lint,
266
+ evidence, scope, acceptance, review — against the commits that land there, and **it holds merge
267
+ authority**, consolidating only green tasks onto the run's `tickmarkr/<runId>` integration branch.
268
+ Orca is given no say over any of the three. Merging that branch to your mainline remains your call,
269
+ exactly as with every other driver.
270
+
253
271
  tickmarkr borrows audit-firm vocabulary for its roles: **you** are the *Partner* (final sign-off),
254
272
  workers are the *field team*, the acceptance judge is the *EQR* (engagement quality reviewer), and
255
273
  the frontier-model consult is the *National Office*. The terms below use that vocabulary:
package/dist/brand.d.ts CHANGED
@@ -48,6 +48,34 @@ export declare const TOKENS: {
48
48
  readonly dim: (s: string) => string;
49
49
  readonly bold: (s: string) => string;
50
50
  };
51
+ /** Exact operator-approved live colours. Values stay hex so the authority is inspectable. */
52
+ export declare const LIVE_PALETTE: {
53
+ /** muted teal — brand identity and pass */
54
+ readonly brand: "#90C4A4";
55
+ /** cornflower blue — running state and information */
56
+ readonly running: "#5A76AE";
57
+ /** ice — primary text */
58
+ readonly text: "#E6FDFF";
59
+ /** cloud — chrome and secondary text */
60
+ readonly chrome: "#D9D7DD";
61
+ /** amethyst — attention and failure */
62
+ readonly attention: "#B07BAC";
63
+ };
64
+ export type LiveRole = keyof typeof LIVE_PALETTE;
65
+ /** Semantic live tokens. Aliases are intentional; no token introduces a sixth colour. */
66
+ export declare const LIVE: {
67
+ readonly brand: (s: string) => string;
68
+ readonly pass: (s: string) => string;
69
+ readonly running: (s: string) => string;
70
+ readonly information: (s: string) => string;
71
+ readonly text: (s: string) => string;
72
+ readonly primaryText: (s: string) => string;
73
+ readonly chrome: (s: string) => string;
74
+ readonly secondaryText: (s: string) => string;
75
+ readonly attention: (s: string) => string;
76
+ readonly failure: (s: string) => string;
77
+ readonly chip: (s: string) => string;
78
+ };
51
79
  /**
52
80
  * The glyph vocabulary — plain characters only; color layers on via tokens so
53
81
  * shape survives NO_COLOR. Bracket toggles ([x]/[ ]) are forbidden on every surface.
package/dist/brand.js CHANGED
@@ -122,6 +122,47 @@ export const dim = sgr("2");
122
122
  export const bold = sgr("1");
123
123
  /** Every semantic color token, for sweeps: each is TTY-gated and NO_COLOR-aware. */
124
124
  export const TOKENS = { brand, brandChip, ok, fail, warn, dim, bold };
125
+ // ── operator live-surface palette (v1.99) ───────────────────────────────────
126
+ // The tokens above remain the global design system for one-shot surfaces. LIVE is the closed
127
+ // five-colour system for long-running operator surfaces. Semantic aliases below deliberately share
128
+ // renderers: glyphs and words retain the distinction between pass/brand, running/information,
129
+ // chrome/secondary text, and attention/failure when colour is unavailable.
130
+ /** Exact operator-approved live colours. Values stay hex so the authority is inspectable. */
131
+ export const LIVE_PALETTE = {
132
+ /** muted teal — brand identity and pass */
133
+ brand: "#90C4A4",
134
+ /** cornflower blue — running state and information */
135
+ running: "#5A76AE",
136
+ /** ice — primary text */
137
+ text: "#E6FDFF",
138
+ /** cloud — chrome and secondary text */
139
+ chrome: "#D9D7DD",
140
+ /** amethyst — attention and failure */
141
+ attention: "#B07BAC",
142
+ };
143
+ const liveSgr = (hex) => {
144
+ const channels = hex.slice(1).match(/.{2}/gu).map((channel) => Number.parseInt(channel, 16));
145
+ return sgr(`38;2;${channels.join(";")}`);
146
+ };
147
+ const liveBrand = liveSgr(LIVE_PALETTE.brand);
148
+ const liveRunning = liveSgr(LIVE_PALETTE.running);
149
+ const liveText = liveSgr(LIVE_PALETTE.text);
150
+ const liveChrome = liveSgr(LIVE_PALETTE.chrome);
151
+ const liveAttention = liveSgr(LIVE_PALETTE.attention);
152
+ /** Semantic live tokens. Aliases are intentional; no token introduces a sixth colour. */
153
+ export const LIVE = {
154
+ brand: liveBrand,
155
+ pass: liveBrand,
156
+ running: liveRunning,
157
+ information: liveRunning,
158
+ text: liveText,
159
+ primaryText: liveText,
160
+ chrome: liveChrome,
161
+ secondaryText: liveChrome,
162
+ attention: liveAttention,
163
+ failure: liveAttention,
164
+ chip: liveBrand,
165
+ };
125
166
  /**
126
167
  * The glyph vocabulary — plain characters only; color layers on via tokens so
127
168
  * shape survives NO_COLOR. Bracket toggles ([x]/[ ]) are forbidden on every surface.
@@ -3,8 +3,28 @@ import { parseArgs } from "node:util";
3
3
  import { collateralLints, sourceScopeLints } from "../../compile/collateral.js";
4
4
  import { compileSource } from "../../compile/index.js";
5
5
  import { saveGraph, stateDirName } from "../../graph/graph.js";
6
+ import { formatPriorFindingEvidence, readPriorRunEvidence } from "../../run/journal.js";
7
+ import { shGit } from "../../run/git.js";
6
8
  import { acquireRunLock, releaseRunLock } from "../../run/lock.js";
7
9
  import { harnessLine, resolveHarness } from "../harness.js";
10
+ async function mergedPendingDiagnostics(cwd, pending, merges) {
11
+ const head = await shGit("git rev-parse HEAD", cwd);
12
+ const base = head.code === 0 ? head.stdout.trim() : "";
13
+ if (!/^[0-9a-f]{40}$/i.test(base))
14
+ return [];
15
+ const lines = [];
16
+ for (const taskId of pending) {
17
+ const candidates = merges.filter((merge) => merge.taskId === taskId).reverse();
18
+ for (const merge of candidates) {
19
+ const ancestor = await shGit(`git merge-base --is-ancestor ${merge.commit} ${base}`, cwd);
20
+ if (ancestor.code !== 0)
21
+ continue;
22
+ lines.push(`${taskId}: merged in run ${merge.runId}; compiles as pending (plan not marked done) — this dispatch rebuilds it`);
23
+ break; // one pure-information line per pending task, newest reachable merge wins
24
+ }
25
+ }
26
+ return lines;
27
+ }
8
28
  // v1.89 T4: harnessFrom is the resolver's INPUT (see plan.ts); the default is the INVOKED entrypoint
9
29
  // (`process.argv[1]`, the bin symlink), never this module's own url — that names an internal module.
10
30
  export async function compile(argv, cwd = process.cwd(), harnessFrom = process.argv[1]) {
@@ -22,6 +42,11 @@ export async function compile(argv, cwd = process.cwd(), harnessFrom = process.a
22
42
  // resolve against the target repo, not the process cwd (the CLI test passes a tmp repo)
23
43
  // Both modes reach the same pure compiler; --dry-run only removes the lock/write side effect below.
24
44
  const g = compileSource(isAbsolute(src) ? src : join(cwd, src), values.type, cwd);
45
+ // One bounded read supplies both cross-run surfaces: unresolved findings below and merge facts for
46
+ // the ancestry check. Neither fact mutates the compiled graph; status and every readiness predicate
47
+ // remain the source compiler's answer.
48
+ const prior = readPriorRunEvidence(cwd, g.tasks);
49
+ const mergedPending = await mergedPendingDiagnostics(cwd, new Set(g.tasks.filter((task) => task.status === "pending").map((task) => task.id)), prior.merges);
25
50
  const stateDir = stateDirName(cwd);
26
51
  if (!values["dry-run"]) {
27
52
  // HARD-01 / Sol #3: hold the same link(2) run lock as the daemon around saveGraph so compile
@@ -41,5 +66,11 @@ export async function compile(argv, cwd = process.cwd(), harnessFrom = process.a
41
66
  const diagnostics = scopeLints.length
42
67
  ? `\nscope lints:\n${scopeLints.map((lint) => ` ! ${lint}`).join("\n")}`
43
68
  : "";
44
- return `${harnessLine(resolveHarness(harnessFrom))}\n${summary}${diagnostics}`;
69
+ const priorFindings = prior.findings.length
70
+ ? `\nprior-run evidence:\n${prior.findings.map((finding) => ` ${formatPriorFindingEvidence(finding)}`).join("\n")}`
71
+ : "";
72
+ const mergeHistory = mergedPending.length
73
+ ? `\nmerge history:\n${mergedPending.map((line) => ` ${line}`).join("\n")}`
74
+ : "";
75
+ return `${harnessLine(resolveHarness(harnessFrom))}\n${summary}${diagnostics}${priorFindings}${mergeHistory}`;
45
76
  }
@@ -2,6 +2,7 @@ import { type ClaudeAlias } from "../../adapters/claude-code.js";
2
2
  import type { WorkerAdapter } from "../../adapters/types.js";
3
3
  import { type KimiDoctorTurnResult } from "../../adapters/kimi.js";
4
4
  import { type CatalogReadResult } from "../../adapters/catalog-remote.js";
5
+ import { type ShResult } from "../../run/git.js";
5
6
  /** Where a newer `table_<date>.csv` is discovered — the deployed site builds filenames by
6
7
  * concatenation and publishes no index, so the release listing is the only enumerable surface. */
7
8
  export declare const LIVEBENCH_RELEASES_URL = "https://api.github.com/repos/LiveBench/livebench.github.io/contents/public";
@@ -15,7 +16,24 @@ export type DoctorOpts = {
15
16
  /** init's between-acts surface: status rows only — the model matrix and inline drift stay
16
17
  * behind `tickmarkr doctor` (files are still written; only the RETURNED string shrinks). */
17
18
  compact?: boolean;
19
+ /** Test seam for the same `orca status --json` transport production invokes. */
20
+ orcaStatusProbe?: (cwd: string, binary: string) => Promise<ShResult>;
21
+ /** Test seam for shell-path discovery; absence remains a normal doctor row, never an exception. */
22
+ resolveOrcaBinary?: (cwd: string) => string | undefined;
18
23
  };
24
+ type OrcaCapability = {
25
+ verdict: "pass" | "fail";
26
+ detail: string;
27
+ };
28
+ /**
29
+ * Orca's status body is deliberately interpreted by T1's one shared envelope parser. Doctor owns
30
+ * only capability presentation: it may classify an absent executable, but it never invents a second
31
+ * permissive JSON reader for a malformed or refused status response.
32
+ *
33
+ * Capability-row `detail` stays hermetic — never `OrcaError.message`, which embeds volatile CLI
34
+ * stderr (Electron timestamps), so the row is byte-stable across runs.
35
+ */
36
+ export declare function probeOrcaCapability(cwd: string, opts?: Pick<DoctorOpts, "orcaStatusProbe" | "resolveOrcaBinary">): Promise<OrcaCapability>;
19
37
  export declare function runnerIgnoreFinding(cwd: string): {
20
38
  verdict: "pass" | "warn";
21
39
  detail: string;
@@ -71,3 +89,4 @@ export declare function selfShadowFinding(ownVersion: string, cwd?: string, reso
71
89
  */
72
90
  export declare function liveBenchStalenessFinding(now: Date): string | undefined;
73
91
  export declare function doctor(_argv: string[], cwd?: string, adapters?: WorkerAdapter[], opts?: DoctorOpts): Promise<string>;
92
+ export {};
@@ -6,14 +6,17 @@ import { detectPackageManager, turboContinueFindings } from "../../gates/baselin
6
6
  import { version } from "./version.js";
7
7
  import { allAdapters, binaryShadowWarnings, detectCandidateClis, flagDriftWarnings, modelAliasExclusions, modelAliasLine, probeAll, probeModels, resolveShellBinary, servableExclusions, servabilityLine, writeDoctor } from "../../adapters/registry.js";
8
8
  import { CLAUDE_ALIAS_IDENTITY_STAMPS, claudeCode, resolveClaudeAliasIdentity } from "../../adapters/claude-code.js";
9
+ import { shq } from "../../adapters/types.js";
9
10
  import { BANNER, compactTokens, dim, fail, kvRow, legend, ok, rule, statusRow, title } from "../../brand.js";
10
11
  import { tickmarkrDir, stateDirName } from "../../graph/graph.js";
11
12
  import { catalogModelAdvisory, catalogTierRanking, declaredModelWindow, hasWindowsConfig, modelLints, suggestOverlay, ttyVisual } from "../../adapters/model-lints.js";
12
13
  import { loadConfig, overlayPreferShapes } from "../../config/config.js";
13
14
  import { HerdrDriver } from "../../drivers/herdr.js";
15
+ import { parseEnvelope } from "../../drivers/orca.js";
14
16
  import { kimi, probeKimiDoctorTurn } from "../../adapters/kimi.js";
15
17
  import { denyPreferCollisionLine, denyPreferCollisions, disallowedBy, excludedChannels, exclusionLine, preferRanks } from "../../route/preference.js";
16
18
  import { LIVEBENCH_TABLE_DATE, readCachedCatalog, refreshCatalogCommand } from "../../adapters/catalog-remote.js";
19
+ import { sh } from "../../run/git.js";
17
20
  /** Where a newer `table_<date>.csv` is discovered — the deployed site builds filenames by
18
21
  * concatenation and publishes no index, so the release listing is the only enumerable surface. */
19
22
  export const LIVEBENCH_RELEASES_URL = "https://api.github.com/repos/LiveBench/livebench.github.io/contents/public";
@@ -21,6 +24,57 @@ export const LIVEBENCH_TABLE_MAX_AGE_DAYS = 90;
21
24
  const visual = () => process.stdout.isTTY === true && process.env.NO_COLOR === undefined;
22
25
  const alignedStatusRow = (verdict, key, value) => ` ${statusRow(verdict, kvRow(key, value).slice(2))}`;
23
26
  const attentionRow = (text) => ` ${statusRow("warn", text)}`;
27
+ /**
28
+ * Orca's status body is deliberately interpreted by T1's one shared envelope parser. Doctor owns
29
+ * only capability presentation: it may classify an absent executable, but it never invents a second
30
+ * permissive JSON reader for a malformed or refused status response.
31
+ *
32
+ * Capability-row `detail` stays hermetic — never `OrcaError.message`, which embeds volatile CLI
33
+ * stderr (Electron timestamps), so the row is byte-stable across runs.
34
+ */
35
+ export async function probeOrcaCapability(cwd, opts = {}) {
36
+ const binary = opts.resolveOrcaBinary ? opts.resolveOrcaBinary(cwd) : resolveShellBinary("orca", cwd).resolved;
37
+ if (!binary)
38
+ return { verdict: "fail", detail: "CLI not installed" };
39
+ let response;
40
+ try {
41
+ response = await (opts.orcaStatusProbe ?? ((probeCwd, executable) => sh(`${shq(executable)} status --json`, probeCwd, 10_000)))(cwd, binary);
42
+ }
43
+ catch {
44
+ // The seam (or the shell) never reaching a verdict is a failed probe, never an exception out of
45
+ // doctor: an absent or sick runtime must still render its row.
46
+ return { verdict: "fail", detail: "CLI installed but runtime probe failed" };
47
+ }
48
+ try {
49
+ // Do this before accepting the process result: Orca's documented refused transport is rc=1
50
+ // with an ok:false envelope on stdout, and parseEnvelope preserves that refusal fail-closed.
51
+ const envelope = parseEnvelope("status", response.stdout);
52
+ if (response.code !== 0 || response.timedOut) {
53
+ return {
54
+ verdict: "fail",
55
+ detail: `CLI installed but runtime probe failed — orca status exited ${response.code}${response.timedOut ? " after timeout" : ""}`,
56
+ };
57
+ }
58
+ const runtime = envelope.result.runtime;
59
+ const reachable = typeof runtime === "object" && runtime !== null && !Array.isArray(runtime)
60
+ ? runtime.reachable
61
+ : undefined;
62
+ if (reachable === true)
63
+ return { verdict: "pass", detail: `runtime reachable (${envelope.runtimeId})` };
64
+ if (reachable === false)
65
+ return { verdict: "fail", detail: "CLI installed but runtime unreachable" };
66
+ return { verdict: "fail", detail: "CLI installed but runtime probe failed — status carries no reachability proof" };
67
+ }
68
+ catch {
69
+ // Only the shared parser reads this body. Every shape it rejects — unparseable, non-object,
70
+ // ok:false refusal, no result, or absent/`none` `_meta.runtimeId` — is a FAILED probe, never
71
+ // an unreachable-runtime claim: a doctor that re-read those bytes with its own permissive
72
+ // reader would call malformed metadata "installed but unreachable". Unreachable is proven only
73
+ // by a well-formed envelope that says `reachable:false` (the shape a live orca CLI emits with
74
+ // its runtime down — tests/helpers/fake-orca.ts).
75
+ return { verdict: "fail", detail: "CLI installed but runtime probe failed" };
76
+ }
77
+ }
24
78
  function detectRunner(cwd) {
25
79
  const readIf = (p) => (existsSync(join(cwd, p)) ? readFileSync(join(cwd, p), "utf8") : "");
26
80
  let pkg = {};
@@ -373,6 +427,11 @@ export async function doctor(_argv, cwd = process.cwd(), adapters = allAdapters(
373
427
  }
374
428
  if (trustNa.length)
375
429
  rows.push(` ${dim("=")} ${dim(`n/a (${trustNa.length}): ${trustNa.join(", ")}`)}`);
430
+ // Orca is an explicit choice, so its health is capability information rather than an auto-routing
431
+ // input. A failed probe never changes pickDriver's auto ordering or substitutes subprocess.
432
+ const orca = await probeOrcaCapability(cwd, opts);
433
+ rows.push(legend("execution runtime:"));
434
+ rows.push(alignedStatusRow(orca.verdict, "orca", orca.detail));
376
435
  for (const [role, sel] of [["judge", cfg.judge], ["consult", cfg.consult]]) {
377
436
  if (!health[sel.adapter]?.installed) {
378
437
  rows.push(attentionRow(`${role} runs on ${sel.adapter}:${sel.model} — NOT installed; that gate will fail closed until you install it or remap cfg.${role}`));
@@ -12,11 +12,14 @@ import { Journal } from "../../run/journal.js";
12
12
  import { doctor } from "./doctor.js";
13
13
  import { assembleFleetEditor } from "./fleet.js";
14
14
  const SCAFFOLD_SPEC = "tickmarkr.spec.md";
15
- // Operator-approved (2026-07-17) environments footer — three rows; no npm install for herdr
16
- // (npm package "herdr" is a reserved 0.0.0 placeholder as of that date).
15
+ // Operator-approved (2026-07-17) environments footer — no npm install for herdr (npm package
16
+ // "herdr" is a reserved 0.0.0 placeholder as of that date). The orca row joins it with the v2.1
17
+ // driver: it is a THIRD execution surface an operator selects outright — `auto` still resolves
18
+ // herdr-else-subprocess and never picks it, so the footer names it beside the other two choices.
17
19
  const ENVIRONMENTS_FOOTER = [
18
20
  "environments:",
19
21
  " herdr — the full cockpit — every worker, judge, and consult is a visible pane you can watch and unblock · https://herdr.dev",
22
+ " orca — visible terminals in the Orca app — an explicit driver choice: set driver: orca (auto never picks it) · https://onorca.dev",
20
23
  " claude code — tickmarkr init --agent installs the /tkr skills + AGENTS.md so Claude Code (or any agent CLI) drives the loop natively",
21
24
  " anywhere — no herdr? same fail-closed gates, headless subprocess driver",
22
25
  ].join("\n");
@@ -1,20 +1,31 @@
1
+ import { parseArgs } from "node:util";
1
2
  import { loadConfig } from "../../config/config.js";
2
- import { pickDriver } from "../../drivers/index.js";
3
+ import { parseDriverOverride, pickDriver } from "../../drivers/index.js";
3
4
  import { loadGraph } from "../../graph/graph.js";
4
5
  import { formatSummary, runDaemon } from "../../run/daemon.js";
5
- import { formatJournalNarration } from "../../run/journal.js";
6
6
  import { denyPreferCollisionLine, denyPreferCollisions } from "../../route/preference.js";
7
+ import { narrationSink, bindNarration } from "./run.js";
7
8
  const summaryGreen = (s) => s.failed.length === 0 && s.human.length === 0 && s.blocked.length === 0 && s.pending.length === 0
8
9
  && s.tipVerify !== "failed";
9
10
  export async function resume(argv, cwd = process.cwd()) {
10
- const runId = argv[0];
11
+ const { values, positionals } = parseArgs({
12
+ args: argv,
13
+ options: {
14
+ "graph-changed": { type: "boolean" },
15
+ "retry-failed": { type: "boolean" },
16
+ driver: { type: "string" },
17
+ },
18
+ allowPositionals: true,
19
+ });
20
+ const runId = positionals[0];
11
21
  if (!runId)
12
- throw new Error("usage: tickmarkr resume <run-id> [--graph-changed] [--retry-failed]");
22
+ throw new Error("usage: tickmarkr resume <run-id> [--graph-changed] [--retry-failed] [--driver <auto|herdr|subprocess|orca>]");
23
+ const driverOverride = parseDriverOverride(values.driver);
13
24
  // T3: --graph-changed is the operator's audited release of the engagement-identity guard (Sol #2 /
14
25
  // Fable F2) — the daemon refuses a mismatched/unbound journal unless this is set, then journals a
15
26
  // graph-rehash event naming both hashes. Strip the flag before runId resolution so a bare id still wins.
16
- const graphChanged = argv.includes("--graph-changed");
17
- const retryFailed = argv.includes("--retry-failed");
27
+ const graphChanged = values["graph-changed"] ?? false;
28
+ const retryFailed = values["retry-failed"] ?? false;
18
29
  const cfg = loadConfig(cwd);
19
30
  // v1.87 T3 (OBS-162, twice-carried workaround): the preflight runs AFTER the graph is read and
20
31
  // sees only the shapes the resumed graph carries. A deny∩prefer collision on a shape no resumed
@@ -25,13 +36,19 @@ export async function resume(argv, cwd = process.cwd()) {
25
36
  if (collisions.length) {
26
37
  throw new Error(collisions.map(denyPreferCollisionLine).join("; "));
27
38
  }
39
+ const narrate = narrationSink(runId);
28
40
  const s = await runDaemon(cwd, {
29
41
  runId,
30
42
  resume: true,
31
43
  graphChanged,
32
44
  retryFailed,
33
- driver: pickDriver(cfg),
34
- narrate: (event) => console.log(formatJournalNarration(event)),
45
+ // bound to the same sink the daemon gets, so a driver-journaled recovery reaches this rail too
46
+ driver: bindNarration(pickDriver(cfg, driverOverride), narrate),
47
+ // v1.99 T2: the ONE narration sink — the quiet rail on a TTY, the raw journal formatter on a
48
+ // pipe. A resumed run meets the same surface a fresh one does; printing the raw formatter here
49
+ // would leave `resume` as the last place the old unfiltered dump survives. Bound to the run id
50
+ // the operator named, so a resumed run's lifecycle rows name THIS run and not a generic word.
51
+ narrate,
35
52
  });
36
53
  const out = `resumed ${s.runId} — ${formatSummary(s)}`;
37
54
  return { out, code: summaryGreen(s) ? 0 : 2 };
@@ -1,6 +1,66 @@
1
+ import type { ExecutorDriver } from "../../drivers/types.js";
1
2
  import { type JournalEvent } from "../../run/journal.js";
2
- export declare const narrationLine: (event: JournalEvent) => string;
3
+ /** Closed repetitive set the rail suppresses on a TTY. An ungated `phase-start` joins them below —
4
+ * it is a phase counter, while a phase-start CARRYING a gate is the gate start the rail draws. */
5
+ export declare const TTY_NOISE_EVENTS: readonly ["worker-contact", "worker-status"];
6
+ type RailTone = "pass" | "fail" | "attention" | "active" | "neutral";
7
+ /** Closed retained set: the short operator label and the row's default tone. Labels are the rail's
8
+ * own vocabulary — a raw journal event name is what this surface exists to stop printing. A `pass`
9
+ * or `ok` datum on the event overrides the default tone, so one gate row can read either way.
10
+ *
11
+ * MEMBERSHIP RULE — the daemon journals far more than this, and an allowlist built from whatever
12
+ * the tests happened to cover masks real events. An event earns a row when it changes what the run
13
+ * will DO next or reports an OUTCOME of it: a routing decision, a worker result, a gate start or
14
+ * verdict, a repair, an escalation, a merge, a run lifecycle step. Everything else — how the daemon
15
+ * got there (worktree setup, launch mechanics, baseline and routing lints) and every poll-time
16
+ * observation (contact reads, quota banners, held dead-verdicts, context samples) — stays off the
17
+ * rail and on the pipe, where it is byte-identical to the raw journal.
18
+ *
19
+ * Applying that rule is what put `graph-rehash` and the worker-nudge family here: a rehash is the
20
+ * operator's audited `--graph-changed` release, journaled by the resumed run through THIS sink, and
21
+ * a nudge is a decision the daemon takes on the operator's behalf (it contacts the worker and arms a
22
+ * grace deadline that force-concludes the wait), whose answered/failed/expired rows are that
23
+ * decision's outcome. Neither is mechanics, and both reach this process's narrate callback. */
24
+ export declare const RAIL_ROWS: Record<string, {
25
+ label: string;
26
+ tone: RailTone;
27
+ }>;
28
+ /**
29
+ * One rail row, or null when the TTY rail suppresses this event. Never wider than `columns`: the
30
+ * identity and the label are clipped SEPARATELY and the detail takes only what they leave, so a row
31
+ * can never wrap into a second line and can never lose its meaning to a long identity.
32
+ */
33
+ export declare function narrationRow(event: JournalEvent, runId: string, columns?: number): string | null;
34
+ /** The narration line for one event of the run named by `runId`: the raw journal formatter on a pipe
35
+ * (byte-identical, every event), the quiet rail on a TTY. Null means the rail suppressed it — the
36
+ * caller prints nothing. */
37
+ export declare const narrationLine: (event: JournalEvent, runId: string) => string | null;
38
+ /**
39
+ * The daemon's narration sink, BOUND TO THE RUN IT NARRATES: the rail on a TTY, the raw journal
40
+ * formatter on a pipe, and nothing at all for an event the rail suppressed. EVERY command that drives
41
+ * a daemon owes its narration to this sink - a second call site that prints `formatJournalNarration`
42
+ * itself is a surface where the rail does not exist, and the operator meets the old unfiltered dump
43
+ * under the newly stacked board.
44
+ *
45
+ * The binding is what puts a real identity on the run-scoped rows: the daemon's `narrate` callback is
46
+ * handed one event and nothing else, and the lifecycle events carry no run id of their own, so the
47
+ * run id can only come from the command that owns the run. Both daemon-driving commands supply it:
48
+ * `run` below mints the id it then passes to the daemon, and `resume` (src/cli/commands/resume.ts)
49
+ * carries the id the operator named.
50
+ */
51
+ export declare const narrationSink: (runId: string) => (event: JournalEvent) => void;
52
+ /**
53
+ * The run's driver, BOUND to the run's narration sink.
54
+ *
55
+ * A driver journals events of its own that the daemon never sees: `dispatch-retry` is appended by
56
+ * HerdrDriver from inside a pane recovery, through a Journal it opens itself (src/drivers/herdr.ts).
57
+ * Unbound, that event lands in the file and on the pipe while the rail — the operator's only live
58
+ * surface — stays silent about a redispatch that already happened. Both daemon-driving commands
59
+ * wrap their driver here so neither can forget the binding.
60
+ */
61
+ export declare function bindNarration<D extends ExecutorDriver>(driver: D, narrate: (event: JournalEvent) => void): D;
3
62
  export declare function run(argv: string[], cwd?: string): Promise<{
4
63
  out: string;
5
64
  code: number;
6
65
  }>;
66
+ export {};