tickmarkr 2.0.0 → 2.1.1

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 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:
@@ -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,5 +1,6 @@
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
6
  import { denyPreferCollisionLine, denyPreferCollisions } from "../../route/preference.js";
@@ -7,14 +8,24 @@ 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
@@ -32,7 +43,7 @@ export async function resume(argv, cwd = process.cwd()) {
32
43
  graphChanged,
33
44
  retryFailed,
34
45
  // bound to the same sink the daemon gets, so a driver-journaled recovery reaches this rail too
35
- driver: bindNarration(pickDriver(cfg), narrate),
46
+ driver: bindNarration(pickDriver(cfg, driverOverride), narrate),
36
47
  // v1.99 T2: the ONE narration sink — the quiet rail on a TTY, the raw journal formatter on a
37
48
  // pipe. A resumed run meets the same surface a fresh one does; printing the raw formatter here
38
49
  // would leave `resume` as the last place the old unfiltered dump survives. Bound to the run id
@@ -1,7 +1,7 @@
1
1
  import { parseArgs } from "node:util";
2
2
  import { allAdapters, discoverChannels, probeAll, readDoctor } from "../../adapters/registry.js";
3
3
  import { ROUTING_MODES } from "../../config/config.js";
4
- import { pickDriver } from "../../drivers/index.js";
4
+ import { parseDriverOverride, pickDriver } from "../../drivers/index.js";
5
5
  import { loadGraph } from "../../graph/graph.js";
6
6
  import { formatSummary, resolveRunMode, runDaemon } from "../../run/daemon.js";
7
7
  import { isRunLockLive } from "../../run/lock.js";
@@ -393,6 +393,9 @@ export async function run(argv, cwd = process.cwd()) {
393
393
  if (!Number.isInteger(n) || n <= 0)
394
394
  throw new Error(`--concurrency must be a positive integer (got ${values.concurrency})`);
395
395
  }
396
+ // Keep invalid input at the argv boundary. In particular, do not cast a string into the closed
397
+ // driver union and accidentally turn an unknown explicit choice into auto-selection.
398
+ const driverOverride = parseDriverOverride(values.driver);
396
399
  if (values.quality && values.mode !== undefined) {
397
400
  throw new Error("--quality is a compatibility alias for --mode partner-led and cannot be combined with an explicit --mode — pass one or the other");
398
401
  }
@@ -443,7 +446,7 @@ export async function run(argv, cwd = process.cwd()) {
443
446
  const s = await runDaemon(cwd, {
444
447
  runId,
445
448
  concurrency: values.concurrency ? Number(values.concurrency) : undefined,
446
- driver: bindNarration(pickDriver(cfg, values.driver), narrate),
449
+ driver: bindNarration(pickDriver(cfg, driverOverride), narrate),
447
450
  mode: flagMode,
448
451
  supersedes: values.supersedes,
449
452
  narrate,
@@ -527,10 +527,34 @@ const foldTaskEffort = (tasks, events) => {
527
527
  return [...effort.values()].sort((left, right) => right.total - left.total
528
528
  || order.get(left.taskId) - order.get(right.taskId));
529
529
  };
530
- // Attention (review) and failure (park) wear the ONE amethyst, so the bar segments and their legend
531
- // markers carry the distinction in SHAPE: a full block for dispatch, light shade for a review round,
532
- // dark shade for a human park. Same display width, so the fitted bar keeps its measured columns.
533
- const EFFORT_GLYPH = { dispatch: "\u2588", review: "\u2592", park: "\u2593" };
530
+ // ONE glyph for every segment, distinction by COLOUR. This was shade-based — full block for dispatch,
531
+ // light shade for a review round, dark shade for a human park — because attention and failure share the
532
+ // ONE amethyst and shape was their only discriminator. OPERATOR 2026-08-25, with a screenshot: the rows
533
+ // rendered at DIFFERENT HEIGHTS. A row carrying the shade glyphs pulls a fallback font whose cell box is
534
+ // taller, so the whole line grows and its full blocks stretch with it — T1 (dispatch+review+park) stood
535
+ // visibly taller than T3 (dispatch only). Same display WIDTH was verified and preserved; nobody checked
536
+ // height, and the fitted-column arithmetic cannot see it.
537
+ //
538
+ // So review moves off amethyst onto `information` (cornflower) — an EXISTING token. Deliberately not a
539
+ // new amber: brand.ts calls the five live colours operator-approved and says "no token introduces a
540
+ // sixth colour", and a bar chart is not the place to spend that.
541
+ //
542
+ // The shape scheme existed so a reader WITHOUT colour could still separate a review round from a human
543
+ // park. That reader does not exist on this panel. `renderFrame` computes `const unicode = visual()`
544
+ // (`visual()` = `isTTY === true && NO_COLOR === undefined`) and, when it is false, EARLY-RETURNS the
545
+ // ASCII machine/CI surface at the `if (!unicode)` branch — which sits ABOVE the `effortPanel(...)` call
546
+ // below, so the panel is never built on that path. Measured before it was explained: a frame rendered
547
+ // with isTTY=true and NO_COLOR=1 contains no "WHERE THE EFFORT WENT" and no block glyph at all.
548
+ // (The `--watch` loop has its own `visual()` gate; that one is NOT what suppresses this panel, and the
549
+ // non-watch path reaches renderFrame unconditionally.) So the shades were paying an accessibility cost
550
+ // on a surface that cannot render colourless, and a conditional to restore them under NO_COLOR would
551
+ // have been an unreachable branch (the v1.80 injected-clock lesson: the fix for that is deletion).
552
+ //
553
+ // One glyph, three colours. Review moves off amethyst onto `information` (cornflower) — an EXISTING
554
+ // token, deliberately not a new amber: brand.ts calls the five live colours operator-approved and says
555
+ // "no token introduces a sixth colour". Every row now draws the same block, which is what makes the
556
+ // rows equal height.
557
+ const EFFORT_GLYPH = { dispatch: "\u2588", review: "\u2588", park: "\u2588" };
534
558
  /**
535
559
  * Prototype panel, fitted by the cockpit's display-cell authority before board-wide wrapping.
536
560
  *
@@ -557,7 +581,7 @@ const effortPanel = (tasks, events, columns) => {
557
581
  const reviewEnd = Math.round(((task.dispatches + task.reviews) / maxTotal) * barColumns);
558
582
  const parkEnd = Math.round((task.total / maxTotal) * barColumns);
559
583
  const stack = ok(EFFORT_GLYPH.dispatch.repeat(dispatchEnd))
560
- + warn(EFFORT_GLYPH.review.repeat(Math.max(0, reviewEnd - dispatchEnd)))
584
+ + information(EFFORT_GLYPH.review.repeat(Math.max(0, reviewEnd - dispatchEnd)))
561
585
  + fail(EFFORT_GLYPH.park.repeat(Math.max(0, parkEnd - reviewEnd)));
562
586
  return `${prefix}${fitCells(stack, barColumns)} ${counts}`;
563
587
  });
@@ -567,7 +591,7 @@ const effortPanel = (tasks, events, columns) => {
567
591
  "",
568
592
  ...rows,
569
593
  "",
570
- ` ${ok(EFFORT_GLYPH.dispatch)} ${dim("dispatch")} ${warn(EFFORT_GLYPH.review)} ${dim("review round")} ${fail(EFFORT_GLYPH.park)} ${dim("human park")}`,
594
+ ` ${ok(EFFORT_GLYPH.dispatch)} ${dim("dispatch")} ${information(EFFORT_GLYPH.review)} ${dim("review round")} ${fail(EFFORT_GLYPH.park)} ${dim("human park")}`,
571
595
  ];
572
596
  };
573
597
  // VIS-11 (v1.13): a liveness header for renderFrame — last journal event age + whether the recorded
@@ -5,7 +5,7 @@ export type CommandResult = string | {
5
5
  };
6
6
  export type CommandMap = Record<string, (argv: string[]) => Promise<CommandResult>>;
7
7
  export declare const COMMANDS: CommandMap;
8
- export declare const USAGE = "tickmarkr \u2014 spec-driven orchestration harness for AI coding agents\nusage: tickmarkr <command>\n init guided setup + doctor; init --agent [--force] [--docs] adds agent skills/docs\n doctor re-probe adapters, herdr, auth; print capability matrix (--fix writes the test-runner ignore when a safe edit exists)\n fleet interactive fleet editor (fleet --print for CI drift checks)\n compile <src> spec \u2192 .tickmarkr/graph.json (fails without acceptance criteria)\n scope <intent> draft a compiled native spec beside an answered intent (--force to overwrite)\n plan dry-run routing table + cost estimate + floor lints\n eval run checked-in fixtures against every channel in isolated temp repos\n run execute the graph (--concurrency N --driver herdr|subprocess --route-strict)\n status live run state\n verify run the gate battery standalone against merge-base(--base, HEAD)..HEAD \u2014 no daemon, one verdict (--base main --criteria <file> | --task <id> [--files <glob>] [--author adapter:model] [--no-review] [--json])\n resume <id> continue a run from its journal\n report <id> cost/quality report (--md for committable execution record)\n profile show learned routing profile (profile reset = forget history via cursor, keeps telemetry)\n ui open the Fleet Studio TUI (full-screen tabbed cockpit)\n unlock remove a stale/garbage run lock (refuses if the holder is alive)\n beat <tier> record one supervision beat for orchestrator|overseer|watch (--stand-down to hand off); a supervising seat's own watcher loop calls it, and status reads the tier STALE once the beats stop\n approve <id> <task> release a park (--uphold sides with the reviewer and funds a fixed attempt; --by <name> --reason <text>); takes effect on resume";
8
+ export declare const USAGE = "tickmarkr \u2014 spec-driven orchestration harness for AI coding agents\nusage: tickmarkr <command>\n init guided setup + doctor; init --agent [--force] [--docs] adds agent skills/docs\n doctor re-probe adapters, herdr, auth; print capability matrix (--fix writes the test-runner ignore when a safe edit exists)\n fleet interactive fleet editor (fleet --print for CI drift checks)\n compile <src> spec \u2192 .tickmarkr/graph.json (fails without acceptance criteria)\n scope <intent> draft a compiled native spec beside an answered intent (--force to overwrite)\n plan dry-run routing table + cost estimate + floor lints\n eval run checked-in fixtures against every channel in isolated temp repos\n run execute the graph (--concurrency N --driver auto|herdr|subprocess|orca --route-strict; orca runs only when named)\n status live run state\n verify run the gate battery standalone against merge-base(--base, HEAD)..HEAD \u2014 no daemon, one verdict (--base main --criteria <file> | --task <id> [--files <glob>] [--author adapter:model] [--no-review] [--json])\n resume <id> continue a run from its journal\n report <id> cost/quality report (--md for committable execution record)\n profile show learned routing profile (profile reset = forget history via cursor, keeps telemetry)\n ui open the Fleet Studio TUI (full-screen tabbed cockpit)\n unlock remove a stale/garbage run lock (refuses if the holder is alive)\n beat <tier> record one supervision beat for orchestrator|overseer|watch (--stand-down to hand off); a supervising seat's own watcher loop calls it, and status reads the tier STALE once the beats stop\n approve <id> <task> release a park (--uphold sides with the reviewer and funds a fixed attempt; --by <name> --reason <text>); takes effect on resume";
9
9
  export declare function dispatch(cmd: string | undefined, argv: string[], commands?: CommandMap): Promise<{
10
10
  out: string;
11
11
  code: number;
package/dist/cli/index.js CHANGED
@@ -35,7 +35,7 @@ usage: tickmarkr <command>
35
35
  scope <intent> draft a compiled native spec beside an answered intent (--force to overwrite)
36
36
  plan dry-run routing table + cost estimate + floor lints
37
37
  eval run checked-in fixtures against every channel in isolated temp repos
38
- run execute the graph (--concurrency N --driver herdr|subprocess --route-strict)
38
+ run execute the graph (--concurrency N --driver auto|herdr|subprocess|orca --route-strict; orca runs only when named)
39
39
  status live run state
40
40
  verify run the gate battery standalone against merge-base(--base, HEAD)..HEAD — no daemon, one verdict (--base main --criteria <file> | --task <id> [--files <glob>] [--author adapter:model] [--no-review] [--json])
41
41
  resume <id> continue a run from its journal
@@ -138,6 +138,7 @@ export declare const TickmarkrConfigSchema: z.ZodObject<{
138
138
  auto: "auto";
139
139
  herdr: "herdr";
140
140
  subprocess: "subprocess";
141
+ orca: "orca";
141
142
  }>;
142
143
  integrationBranchPrefix: z.ZodString;
143
144
  taskTimeoutMinutes: z.ZodNumber;
@@ -266,7 +266,7 @@ const ShapeGateParticipationSchema = z
266
266
  });
267
267
  export const TickmarkrConfigSchema = z.object({
268
268
  concurrency: z.number().int().positive(),
269
- driver: z.enum(["auto", "herdr", "subprocess"]),
269
+ driver: z.enum(["auto", "herdr", "subprocess", "orca"]),
270
270
  integrationBranchPrefix: z
271
271
  .string()
272
272
  .regex(/^[A-Za-z0-9][A-Za-z0-9._/-]*$/, "must be branch-safe (letters/digits/._/-, no spaces or shell metacharacters)")
@@ -713,7 +713,7 @@ export function overlayBytesLoadError(repoRoot, bytes, opts = {}) {
713
713
  export function configTemplate(overlay) {
714
714
  const base = `# tickmarkr config overlay — merges over built-in defaults (repo beats global beats defaults)
715
715
  # concurrency: 3
716
- # driver: auto # auto | herdr | subprocess
716
+ # driver: auto # auto | herdr | subprocess | orca
717
717
  # taskTimeoutMinutes: 30
718
718
  # contextWarnTokens: 170000 # v1.23: journal+notify once per attempt when live worker context crosses this (status shows the sample)
719
719
  # setup: npm ci --prefer-offline # run in each fresh task worktree before dispatch
@@ -23,17 +23,17 @@ export declare class DeliveryCorruptedError extends Error {
23
23
  export type DriverJournal = (event: string, slotName: string, data: Record<string, unknown>) => void;
24
24
  /** First-generation join direction from measured trailer-safe floor (43-MEASUREMENT.md). */
25
25
  export declare function workerSplitDirection(paneCols: number | null, safeFloor?: number, margin?: number): "right" | "down";
26
- export declare const BOARD_HEIGHT_SHARE = 0.72;
26
+ export declare const BOARD_WIDTH_SHARE = 0.5;
27
27
  export interface BoardPlacement {
28
- /** Always down: the split is vertical, so the board can own the caller's FULL width. */
29
- direction: "down";
30
- /** herdr's split ratio is the FIRST child's share, and a down split's first child is the TOP
31
- * region — the region the board occupies once it is swapped above the caller. */
28
+ /** Always right: the board sits BESIDE the caller, so the narration keeps a column of its own. */
29
+ direction: "right";
30
+ /** herdr's split ratio is the FIRST child's share, and a right split's first child is the LEFT
31
+ * region — the caller's, i.e. the narration. The board takes the remainder, on the right.
32
+ * Measured against a live herdr 2026-08-25: a 256-col caller split right at 0.5 leaves the caller
33
+ * at x=36 w=128 and puts the NEW pane at x=164 w=128. */
32
34
  ratio: number;
33
- /** The new pane is swapped ABOVE the caller; the split alone would leave the board underneath. */
34
- swap: "above";
35
35
  }
36
- /** The single approved vertical-stack record. The caller's columns are accepted and deliberately
36
+ /** The single approved side-by-side record. The caller's columns are accepted and deliberately
37
37
  * ignored: this signature is where width used to decide the arrangement, and the parameter stays
38
38
  * so that "the plan does not depend on it" is a property a caller (and a test) can exercise. */
39
39
  export declare function boardSplitPlan(_callerCols?: number | null): BoardPlacement;
@@ -73,15 +73,27 @@ export function workerSplitDirection(paneCols, safeFloor = TRAILER_SAFE_FLOOR_CO
73
73
  // placement any more. Every width-derived variant of this placement has been wrong in the operator's
74
74
  // tab: the halving floor sent a 189-column board below the seat (2026-08-18), and the width-first
75
75
  // side split that replaced it puts the board and the narration shoulder to shoulder when the board is
76
- // the surface the operator reads and the narration is the rail beneath it. The placement is now ONE
77
- // record — the board stacked ABOVE the caller at full width, taking 72% of the height — and it is
78
- // invariant: no terminal width, measured or unmeasurable, can select a different arrangement.
79
- export const BOARD_HEIGHT_SHARE = 0.72; // board 72 / narration 28, the operator's stack
80
- /** The single approved vertical-stack record. The caller's columns are accepted and deliberately
76
+ // the surface the operator reads and the narration is the rail beneath it. The placement is ONE
77
+ // record and it is invariant: no terminal width, measured or unmeasurable, can select a different
78
+ // arrangement. THAT invariance is the hard-won part and it is unchanged here.
79
+ //
80
+ // What the record SAYS changed on operator instruction (2026-08-25): the board sits to the RIGHT of
81
+ // the caller, not stacked above it. The vertical stack gave the board 72% of the HEIGHT at full
82
+ // width, and in the operator's own tab that was mostly empty board over a squeezed narration rail —
83
+ // a task table is a handful of rows, so height was the axis it did not need and the narration did.
84
+ // Side by side spends the axis the board actually uses.
85
+ //
86
+ // ⚠ This is NOT a return to the width-derived placement that was wrong twice. Those variants let the
87
+ // MEASURED width choose the arrangement, so the same run rendered differently in different terminals
88
+ // and neither operator nor test could name one expected geometry. This record is constant: every
89
+ // caller width gets `right`, and `boardSplitPlan` still ignores the columns it is handed. The
90
+ // invariance test's width-sensitive control still fails, which is the property that mattered.
91
+ export const BOARD_WIDTH_SHARE = 0.5; // narration 50 / board 50, side by side
92
+ /** The single approved side-by-side record. The caller's columns are accepted and deliberately
81
93
  * ignored: this signature is where width used to decide the arrangement, and the parameter stays
82
94
  * so that "the plan does not depend on it" is a property a caller (and a test) can exercise. */
83
95
  export function boardSplitPlan(_callerCols) {
84
- return { direction: "down", ratio: BOARD_HEIGHT_SHARE, swap: "above" };
96
+ return { direction: "right", ratio: BOARD_WIDTH_SHARE };
85
97
  }
86
98
  /** The tab a slot belongs to: its TASK — worker, judge, review and consult panes for one task share it.
87
99
  * Returns undefined for everything else, which keeps those on the dedicated-tab path.
@@ -1081,16 +1093,17 @@ export class HerdrDriver {
1081
1093
  return p.workspace_id === this.ws && typeof p.pane_id === "string" && owned?.role === "watch" && owned.taskId === "run";
1082
1094
  }).map((p) => p.pane_id);
1083
1095
  }
1084
- // T2: the watch is a sibling of the daemon's own pane, never a separate tab — stacked ABOVE it at
1085
- // the caller's full width, always, whatever the terminal measures. Its durable owned name is how a
1096
+ // T2: the watch is a sibling of the daemon's own pane, never a separate tab — placed to the RIGHT
1097
+ // of it, always, whatever the terminal measures. Its durable owned name is how a
1086
1098
  // later daemon RECOGNIZES the board it must retire, so a run never stacks a second one.
1087
1099
  async watchSlot(cwd, name) {
1088
1100
  if (!this.ws)
1089
1101
  throw new Error("herdr watch placement requires HERDR_WORKSPACE_ID — refusing unseeded pane");
1090
1102
  if (!this.callerPane)
1091
1103
  throw new Error("herdr watch placement requires HERDR_PANE_ID — refusing untargeted split");
1092
- // One invariant placement (boardSplitPlan): split the caller down, then swap the new pane above
1093
- // it. No layout read decides this — width chose the arrangement twice and was wrong twice.
1104
+ // One invariant placement (boardSplitPlan): split the caller RIGHT. No layout read decides this —
1105
+ // width chose the arrangement twice and was wrong twice. No swap: a right split already lands the
1106
+ // new pane — the board — beside the caller, so there is no second operation to verify.
1094
1107
  const plan = boardSplitPlan();
1095
1108
  const sp = await this.herdr(`pane split ${shq(this.callerPane)} --direction ${plan.direction} --ratio ${plan.ratio} --no-focus`);
1096
1109
  if (sp.code !== 0)
@@ -1104,28 +1117,12 @@ export class HerdrDriver {
1104
1117
  }
1105
1118
  if (typeof pane !== "string" || !pane)
1106
1119
  throw new Error(`herdr watch split returned no pane id: ${sp.stdout}`);
1107
- // The split leaves the board UNDER the caller; the swap is what makes the stack the requested
1108
- // one. Verified, not assumed: a swap that failed would leave a board below the narration while
1109
- // the daemon reported the geometry it asked for. Instead the split pane is closed and the failure
1110
- // propagates — the daemon swallows it and runs boardless, which is honest about what is on screen.
1111
- // `pane swap` answers a no-op with a ZERO exit and `changed:false` (herdr socket API: a swap it
1112
- // declined is a non-error response), so an exit code alone proves nothing about the geometry —
1113
- // that is exactly the path that would leave the board below the narration while the daemon
1114
- // reported the stack. The documented `changed` flag is the verification; anything else — a
1115
- // nonzero exit, `changed:false`, an unparseable result — fails closed.
1116
- const swapped = await this.herdr(`pane swap --source-pane ${shq(pane)} --target-pane ${shq(this.callerPane)}`);
1117
- let swapChanged;
1118
- try {
1119
- swapChanged = JSON.parse(swapped.stdout).result?.changed;
1120
- }
1121
- catch {
1122
- /* fail closed below */
1123
- }
1124
- if (swapped.code !== 0 || swapChanged !== true) {
1125
- await this.discardSplit(pane, `herdr watch swap ${plan.swap} failed: ${swapped.code !== 0
1126
- ? swapped.stderr || swapped.stdout
1127
- : `herdr reported no swap took place: ${swapped.stdout || swapped.stderr}`}`);
1128
- }
1120
+ // No swap step to verify any more. The stack needed one — the split landed the board UNDER the
1121
+ // caller and only the swap made the geometry the requested one, so a `pane swap` that no-opped
1122
+ // with a ZERO exit could leave a board below the narration while the daemon reported the stack;
1123
+ // the `changed` flag existed to catch exactly that. A right split places the board where it
1124
+ // belongs in ONE operation that either returns a pane id or throws above, so there is no
1125
+ // second-operation gap left to fail closed on.
1129
1126
  const renamed = await this.herdr(`pane rename ${shq(pane)} ${shq(name)}`);
1130
1127
  if (renamed.code !== 0 || await this.namedPaneId(name) !== pane) {
1131
1128
  await this.discardSplit(pane, `herdr watch rename failed: ${renamed.stderr || renamed.stdout}`);
@@ -1,3 +1,7 @@
1
1
  import type { TickmarkrConfig } from "../config/config.js";
2
2
  import type { ExecutorDriver } from "./types.js";
3
- export declare function pickDriver(cfg: TickmarkrConfig, override?: "auto" | "herdr" | "subprocess"): ExecutorDriver;
3
+ export declare const DRIVER_CHOICES: readonly ["auto", "herdr", "subprocess", "orca"];
4
+ export type DriverChoice = (typeof DRIVER_CHOICES)[number];
5
+ /** Validate argv at the CLI boundary rather than casting an arbitrary string into a driver choice. */
6
+ export declare function parseDriverOverride(override?: string): DriverChoice | undefined;
7
+ export declare function pickDriver(cfg: TickmarkrConfig, override?: string): ExecutorDriver;
@@ -1,7 +1,18 @@
1
1
  import { HerdrDriver } from "./herdr.js";
2
+ import { OrcaDriver } from "./orca.js";
2
3
  import { SubprocessDriver } from "./subprocess.js";
4
+ export const DRIVER_CHOICES = ["auto", "herdr", "subprocess", "orca"];
5
+ /** Validate argv at the CLI boundary rather than casting an arbitrary string into a driver choice. */
6
+ export function parseDriverOverride(override) {
7
+ if (override === undefined)
8
+ return undefined;
9
+ for (const choice of DRIVER_CHOICES)
10
+ if (override === choice)
11
+ return choice;
12
+ throw new Error(`usage: --driver must be one of ${DRIVER_CHOICES.join(" | ")} (got ${override})`);
13
+ }
3
14
  export function pickDriver(cfg, override) {
4
- const want = override ?? cfg.driver;
15
+ const want = parseDriverOverride(override) ?? cfg.driver;
5
16
  // VIS-09 item 2: plumb the per-tab cap into the HerdrDriver — the driver takes it as a constructor
6
17
  // param and never imports config (cfg is the only seam). Guaranteed present: DEFAULT_CONFIG seeds
7
18
  // workersPerTab:3 and deepMerge overlays on top, so a missing overlay key still resolves.
@@ -9,5 +20,9 @@ export function pickDriver(cfg, override) {
9
20
  return new HerdrDriver("herdr", cfg.visibility.workersPerTab);
10
21
  if (want === "subprocess")
11
22
  return new SubprocessDriver();
23
+ // Orca is an operator-selected execution surface. Its runtime failure stays on Orca; selection
24
+ // must never substitute a hidden subprocess worker after this explicit choice.
25
+ if (want === "orca")
26
+ return new OrcaDriver();
12
27
  return HerdrDriver.available() ? new HerdrDriver("herdr", cfg.visibility.workersPerTab) : new SubprocessDriver();
13
28
  }