tickmarkr 2.4.1 → 2.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (75) hide show
  1. package/README.md +110 -24
  2. package/dist/adapters/claude-code.js +7 -2
  3. package/dist/adapters/codex.d.ts +1 -0
  4. package/dist/adapters/codex.js +66 -5
  5. package/dist/adapters/types.d.ts +4 -0
  6. package/dist/adapters/types.js +33 -0
  7. package/dist/cli/commands/approve.d.ts +42 -0
  8. package/dist/cli/commands/approve.js +80 -13
  9. package/dist/cli/commands/doctor.d.ts +234 -0
  10. package/dist/cli/commands/doctor.js +139 -5
  11. package/dist/cli/commands/report.js +15 -44
  12. package/dist/cli/commands/scope.js +36 -6
  13. package/dist/cli/commands/stats.d.ts +2 -0
  14. package/dist/cli/commands/stats.js +42 -23
  15. package/dist/cli/commands/status.js +20 -4
  16. package/dist/cli/commands/ui.js +42 -53
  17. package/dist/cli/commands/unlock.d.ts +1 -1
  18. package/dist/cli/commands/unlock.js +59 -9
  19. package/dist/cli/help.d.ts +219 -0
  20. package/dist/cli/help.js +212 -0
  21. package/dist/cli/index.d.ts +42 -2
  22. package/dist/cli/index.js +23 -8
  23. package/dist/drivers/herdr.d.ts +7 -2
  24. package/dist/drivers/herdr.js +78 -48
  25. package/dist/drivers/orca.d.ts +3 -2
  26. package/dist/drivers/orca.js +43 -4
  27. package/dist/drivers/subprocess.d.ts +1 -0
  28. package/dist/drivers/subprocess.js +3 -0
  29. package/dist/drivers/types.d.ts +14 -0
  30. package/dist/gates/artifact-manifest.d.ts +50 -0
  31. package/dist/gates/artifact-manifest.js +23 -0
  32. package/dist/plan/scope.d.ts +25 -0
  33. package/dist/plan/scope.js +92 -12
  34. package/dist/report/operator-record.d.ts +49 -0
  35. package/dist/report/operator-record.js +137 -0
  36. package/dist/run/daemon.d.ts +3 -4
  37. package/dist/run/daemon.js +18 -10
  38. package/dist/run/lock.d.ts +59 -2
  39. package/dist/run/lock.js +184 -26
  40. package/dist/run/operator-state.d.ts +86 -0
  41. package/dist/run/operator-state.js +165 -0
  42. package/dist/run/supervision.d.ts +32 -0
  43. package/dist/run/supervision.js +138 -17
  44. package/dist/tui/cockpit/capture.d.ts +19 -0
  45. package/dist/tui/cockpit/capture.js +89 -1
  46. package/dist/tui/cockpit/components.d.ts +15 -1
  47. package/dist/tui/cockpit/components.js +79 -9
  48. package/dist/tui/cockpit/decision-actions.d.ts +147 -0
  49. package/dist/tui/cockpit/decision-actions.js +315 -0
  50. package/dist/tui/cockpit/derive.d.ts +1 -1
  51. package/dist/tui/cockpit/derive.js +2 -0
  52. package/dist/tui/cockpit/evidence-view.d.ts +119 -0
  53. package/dist/tui/cockpit/evidence-view.js +210 -0
  54. package/dist/tui/cockpit/home-view.d.ts +88 -0
  55. package/dist/tui/cockpit/home-view.js +240 -0
  56. package/dist/tui/cockpit/keys.d.ts +125 -0
  57. package/dist/tui/cockpit/keys.js +31 -0
  58. package/dist/tui/cockpit/layout.d.ts +14 -0
  59. package/dist/tui/cockpit/layout.js +15 -0
  60. package/dist/tui/cockpit/live-runtime.d.ts +46 -0
  61. package/dist/tui/cockpit/live-runtime.js +683 -0
  62. package/dist/tui/cockpit/live-store.d.ts +289 -0
  63. package/dist/tui/cockpit/live-store.js +308 -0
  64. package/dist/tui/cockpit/live.d.ts +21 -1
  65. package/dist/tui/cockpit/live.js +12 -1
  66. package/dist/tui/cockpit/run-view.d.ts +100 -0
  67. package/dist/tui/cockpit/run-view.js +202 -0
  68. package/dist/tui/cockpit/shell.d.ts +50 -0
  69. package/dist/tui/cockpit/shell.js +74 -0
  70. package/dist/tui/cockpit/theme.d.ts +27 -0
  71. package/dist/tui/cockpit/theme.js +21 -0
  72. package/package.json +1 -1
  73. package/skills/tickmarkr-auto/SKILL.md +10 -1
  74. package/skills/tickmarkr-loop/SKILL.md +72 -1
  75. package/skills/tickmarkr-overseer/SKILL.md +12 -0
@@ -0,0 +1,212 @@
1
+ // Descriptions live beside dispatch, not in mutating command bodies. The parser/registry drift
2
+ // test checks these option names against production source; adding a command requires help here.
3
+ export const COMMAND_HELP = {
4
+ init: {
5
+ usage: "init [options]", description: "Set up repository/global config, a starter spec and adapter health.",
6
+ options: {
7
+ "--global-dir <path>": "Use this global configuration directory.",
8
+ "--agent": "Install agent skills; combine with --docs for guidance files.",
9
+ "--force": "Refresh existing managed agent skills/docs when installing them.",
10
+ "--docs": "Include agent guidance documents with --agent.",
11
+ "--fresh": "Force fresh adapter probes instead of reusing recent health.",
12
+ "--yes": "Skip the interactive setup wizard and use defaults.",
13
+ }, examples: ["init --yes", "init --agent --force --docs"],
14
+ },
15
+ doctor: {
16
+ usage: "doctor [options]", description: "Probe configured adapters and models and print diagnostics. Default probing may make model calls and refresh a stale catalog.",
17
+ options: {
18
+ "--models": "Include full per-model diagnostics.",
19
+ "--fix": "Run normal probes and repair the test-runner ignore when a safe edit exists.",
20
+ "--fix-only": "Repair and verify the test-runner ignore locally; skip model probes and catalog refresh.",
21
+ "--cached": "Read cached diagnostics without probing or refreshing the catalog.",
22
+ "--cached-only": "Alias for --cached.",
23
+ "--probe-preflight": "Disclose configured model/probe counts and affected files without probing.",
24
+ "--preflight": "Alias for --probe-preflight.",
25
+ "--refresh-catalog": "Refresh only the model catalog; use as the sole option.",
26
+ "--catalog-only": "Alias for --refresh-catalog; use as the sole option.",
27
+ }, examples: ["doctor --probe-preflight", "doctor --fix-only", "doctor --models", "doctor --refresh-catalog"],
28
+ },
29
+ fleet: {
30
+ usage: "fleet [options]", description: "Edit fleet configuration interactively, or print it for scripts.",
31
+ options: {
32
+ "--print": "Print fleet configuration without opening the editor.",
33
+ "--why": "Include routing eligibility explanations in printed output.",
34
+ "--global-dir <path>": "Use this global configuration directory.",
35
+ "--fresh": "Refresh adapter health; may make model calls.",
36
+ }, examples: ["fleet --print --why", "fleet"],
37
+ },
38
+ compile: {
39
+ usage: "compile <spec-dir-or-md> [options]", description: "Compile source into .tickmarkr/graph.json; acceptance criteria are required.",
40
+ options: {
41
+ "--type <speckit|prd|gsd|native>": "Select the source format explicitly.",
42
+ "--dry-run": "Validate and report without saving the graph.",
43
+ "--strict": "Treat all native authoring lint findings as blocking errors.",
44
+ }, examples: ["compile feature.spec.md --type native --dry-run", "compile feature.spec.md --strict"],
45
+ },
46
+ scope: {
47
+ usage: "scope <intent-file> [options]", description: "Draft a native spec beside an answered intent. Authoring requires TTY confirmation or --yes and may make model calls.",
48
+ options: {
49
+ "--preview": "Validate locally and show cached candidate, destination and call budget; no probes, model calls or writes.",
50
+ "--yes": "Confirm the disclosed authoring action without a TTY prompt.",
51
+ "--force": "Allow overwriting the destination spec after confirmation.",
52
+ }, examples: ["scope feature.intent.md --preview", "scope feature.intent.md --yes", "scope feature.intent.md --yes --force"],
53
+ },
54
+ plan: {
55
+ usage: "plan [options]", description: "Print the compiled graph's routing preview, cost estimate and floor lints without dispatching workers.",
56
+ options: {
57
+ "--mode <risk-based|partner-led|staff-led>": "Preview routing under the selected mode.",
58
+ "--driver <auto|herdr|subprocess|orca>": "Preview the execution driver; orca is explicit-only.",
59
+ }, examples: ["plan", "plan --mode risk-based --driver subprocess"],
60
+ },
61
+ run: {
62
+ usage: "run [options]", description: "Execute the compiled graph with production preflight, gates and run locking.",
63
+ options: {
64
+ "--concurrency <N>": "Set a positive integer worker concurrency.",
65
+ "--driver <auto|herdr|subprocess|orca>": "Choose the execution driver; orca is explicit-only.",
66
+ "--route-strict": "Refuse routing conflicts and lints before dispatch.",
67
+ "--no-explore": "Disable learned routing exploration for this run.",
68
+ "--mode <risk-based|partner-led|staff-led>": "Override the routing mode for this run.",
69
+ "--quality": "Compatibility alias for --mode partner-led; cannot combine with --mode.",
70
+ "--supersedes <run-id>": "Record that this engagement supersedes the named run.",
71
+ }, examples: ["run --concurrency 2 --driver subprocess --route-strict"],
72
+ },
73
+ status: {
74
+ usage: "status [<run-id>] [options]", description: "Show a named run or the latest run. With --watch --events, stdout is one JSON document per decision event and stderr carries keepalive lines. Do not merge stdout and stderr (2>&1 corrupts the JSON document stream).",
75
+ options: {
76
+ "--oneline": "Print a compact snapshot and exit.",
77
+ "--watch": "Follow updates; on a TTY open Run unless plain output or event streaming is selected.",
78
+ "--plain": "Use line-mode output with --watch, including on a TTY.",
79
+ "--events": "With --watch, replay and follow decision events as JSON documents on stdout.",
80
+ "--jsonl": "Alias for --events.",
81
+ "--decision-events": "Alias for --events.",
82
+ "--webhook <url>": "With --watch, POST decision events to this URL.",
83
+ }, examples: ["status --oneline", "status run-example --watch --plain", "status run-example --watch --events"],
84
+ },
85
+ stats: {
86
+ usage: "stats", description: "Print all-run channel delivery, failure, rescue, author and reviewer statistics. Takes no run ID.",
87
+ options: {}, examples: ["stats"],
88
+ },
89
+ resume: {
90
+ usage: "resume <run-id> [options]", description: "Continue a run from its journal through production preflight and gates.",
91
+ options: {
92
+ "--graph-changed": "Accept and journal a changed graph identity for this run.",
93
+ "--retry-failed": "Retry failed tasks.",
94
+ "--driver <auto|herdr|subprocess|orca>": "Choose the execution driver; orca is explicit-only.",
95
+ }, examples: ["resume run-example --driver subprocess", "resume run-example --retry-failed"],
96
+ },
97
+ report: {
98
+ usage: "report [<run-id>] [options]", description: "Report on a named or latest run. Markdown goes to stdout; redirect stdout to save an execution record beside the spec.",
99
+ options: {
100
+ "--md": "Write the Markdown execution record to stdout; does not create a Markdown file.",
101
+ "--compare <baseline-run-id>": "Include cost, gate and duration deltas with an environment comparability guard.",
102
+ "--bundle <path>": "Write a portable JSON proof bundle to this local path.",
103
+ }, examples: ["report run-example --md > feature.record.md", "report run-example --compare run-baseline", "report run-example --bundle proof.json"],
104
+ },
105
+ profile: {
106
+ usage: "profile [reset|discount <run-id> [<task-id>]|discounts] [options]",
107
+ description: "Show learned routing, reset the history cursor, append a discount or list discounts. Use profile <operation> --help for details.",
108
+ options: {
109
+ "--explain <shape> <adapter:model> [sub|api]": "Explain the learned score for a shape and channel (default billing: sub).",
110
+ "--weight <0|0.5>": "Required for discount: set the evidence weight.",
111
+ "--reason <text>": "Required for discount: explain the evidence claim.",
112
+ }, examples: ["profile", "profile --explain implement fake:fake-1 sub", "profile discount run-example T1 --weight 0.5 --reason 'infra incident'", "profile discounts", "profile reset"],
113
+ },
114
+ ui: {
115
+ usage: "ui [<run-id>] [options]", description: "Open the cockpit on a TTY. Delivered views: 1 Home, 4 Run, 5 Evidence. Fleet, Plan and Health remain CLI commands: tickmarkr fleet, tickmarkr plan, tickmarkr doctor. Without a TTY use tickmarkr fleet --print or tickmarkr status --watch.",
116
+ options: {
117
+ "--view <home|run|evidence>": "Choose a delivered view; default home (an empty repository opens Home).",
118
+ "--setup": "Open Run Parks for the positional run ID, or latest run; overrides --view.",
119
+ }, examples: ["ui", "ui run-example --view run", "ui run-example --view evidence", "ui --setup run-example"],
120
+ },
121
+ unlock: {
122
+ usage: "unlock <run-id> [--yes] | tickmarkr unlock --garbage [--yes]", description: "Remove only a matching, provably dead run lock after confirmation. Live, inaccessible or changed holders refuse; commit rechecks the preview.",
123
+ options: {
124
+ "--garbage": "Recover an unparseable lock identified by its bytes/inode; no run ID is invented.",
125
+ "--yes": "Confirm removal without a TTY prompt; all holder and race checks still apply.",
126
+ }, examples: ["unlock run-example --yes", "unlock --garbage --yes"],
127
+ },
128
+ approve: {
129
+ usage: "approve <run-id> <task-id> [options]", description: "Append a validated park decision. A running owner may enact it; a closed run requires a separate resume. Decisions cannot be undone.",
130
+ options: {
131
+ "--by <name>": "Name the actor (default: current OS user).",
132
+ "--reason <text>": "Record the decision reason.",
133
+ "--waive": "Waive only the identified failed gate.",
134
+ "--uphold": "Uphold a review failure and fund a fixed attempt.",
135
+ "--recheck": "Request rechecking an infra or failed-gate park; satisfies no gate.",
136
+ "--review-rounds <N>": "Set a positive integer review-round ceiling with the decision.",
137
+ }, examples: ["approve run-example T1 --by operator --reason 'ready to proceed'", "approve run-example T1 --recheck --reason 'infra recovered'"],
138
+ },
139
+ beat: {
140
+ usage: "beat <orchestrator|orchestrator-context|overseer|overseer-context|watch> --seat <identity> [options]",
141
+ description: "Write one supervision beat. The supervising seat's watcher loop repeats it; stopped beats become STALE.",
142
+ options: {
143
+ "--seat <identity>": "Required: identify the supervising pane or agent.",
144
+ "--arm-id <identity>": "Identify this supervision arm.",
145
+ "--pct <0..100>": "Report current context consumption percentage.",
146
+ "--threshold-pct <0..100>": "Set the context warning threshold (default 75).",
147
+ "--stand-down": "Record an explicit handoff and stand down this tier.",
148
+ }, examples: ["beat overseer --seat supervisor", "beat overseer --seat supervisor --stand-down"],
149
+ },
150
+ version: {
151
+ usage: "version [--dist]", description: "Print the installed package version. Top-level aliases: --version and -v.",
152
+ options: { "--dist": "Also print the resolved build directory and distribution fingerprint." },
153
+ examples: ["version", "version --dist"],
154
+ },
155
+ verify: {
156
+ usage: "verify [--base <ref>] [--criteria <file> | --task <id>] [options]",
157
+ description: "Verify the committed merge-base(base, HEAD)..HEAD diff without a daemon or retries. The verdict and JSON result are written to stdout; progress and diagnostics are written to stderr. Do not merge stdout and stderr (2>&1 corrupts the verdict stream).",
158
+ options: {
159
+ "--base <ref>": "Compare against merge-base(ref, HEAD); default main.",
160
+ "--criteria <file>": "Read acceptance criteria from a file; use this or --task.",
161
+ "--task <id>": "Read acceptance criteria and default file scope from a compiled task.",
162
+ "--files <glob>": "Declare file scope; repeat for multiple globs (overrides task scope).",
163
+ "--author <adapter:model>": "Identify the author channel for independent reviewer selection.",
164
+ "--baseline <path>": "Read an existing baseline JSON file instead of capturing one.",
165
+ "--record <run-id>": "Append verification results to the named run journal.",
166
+ "--json": "Print a machine-readable verdict on stdout.",
167
+ "--no-review": "Disable semantic review; deterministic gates remain mandatory.",
168
+ "--no-acceptance": "Disable semantic acceptance even when criteria are supplied.",
169
+ }, examples: ["verify --base main --task T1 --files 'src/**' --files 'tests/**' --author fake:fake-1 --json", "verify --base main --baseline baseline.json --record run-example --no-review --no-acceptance"],
170
+ },
171
+ eval: {
172
+ usage: "eval [<fixtures-root>]", description: "Discover and validate fixture start/solution directories, seed valid fixtures into temporary git repositories, then clean them up. Defaults to fixtures/eval in the current directory.",
173
+ options: {}, examples: ["eval", "eval ./fixtures", "eval -- --help"],
174
+ },
175
+ };
176
+ export const PROFILE_HELP = {
177
+ reset: {
178
+ usage: "profile reset", description: "Move the learned-history cursor to the latest run. Writes .tickmarkr/profile-since; preserves all telemetry. Help never resets it.",
179
+ options: {}, examples: ["profile reset"],
180
+ },
181
+ discount: {
182
+ usage: "profile discount <run-id> [<task-id>] --weight <0|0.5> --reason <text>", description: "Append an evidence discount to .tickmarkr/profile-discounts. Omitting the task ID discounts the entire run.",
183
+ options: { "--weight <0|0.5>": "Required evidence weight.", "--reason <text>": "Required nonempty reason for the evidence claim." },
184
+ examples: ["profile discount run-example T1 --weight 0.5 --reason 'infra incident'"],
185
+ },
186
+ discounts: {
187
+ usage: "profile discounts", description: "List recorded evidence discounts without modifying them.", options: {}, examples: ["profile discounts"],
188
+ },
189
+ };
190
+ export function hasHelpFlag(argv) {
191
+ for (const arg of argv) {
192
+ if (arg === "--")
193
+ break;
194
+ if (arg === "--help" || arg === "-h")
195
+ return true;
196
+ }
197
+ return false;
198
+ }
199
+ export function commandHelp(command, argv = []) {
200
+ const operation = argv.find((arg) => arg !== "--help" && arg !== "-h");
201
+ const nested = command === "profile" && operation && Object.hasOwn(PROFILE_HELP, operation) ? PROFILE_HELP[operation] : undefined;
202
+ const help = nested ?? (Object.hasOwn(COMMAND_HELP, command) ? COMMAND_HELP[command] : undefined);
203
+ if (!help)
204
+ return `usage: tickmarkr ${command}\n --help, -h Show help without running the command.`;
205
+ return [
206
+ `usage: tickmarkr ${help.usage}`, help.description, "", "Options:",
207
+ ...Object.entries(help.options).map(([flag, description]) => ` ${flag} ${description}`),
208
+ " --help, -h Show help without running the command.",
209
+ "", "Examples:", ...help.examples.map((example) => ` tickmarkr ${example}`),
210
+ "", "Help flags are recognized only before --; arguments after -- are literal data.",
211
+ ].join("\n");
212
+ }
@@ -1,11 +1,51 @@
1
1
  #!/usr/bin/env node
2
+ import { approve } from "./commands/approve.js";
3
+ import { beat } from "./commands/beat.js";
4
+ import { compile } from "./commands/compile.js";
5
+ import { doctor } from "./commands/doctor.js";
6
+ import { evalCommand } from "./commands/eval.js";
7
+ import { fleet } from "./commands/fleet.js";
8
+ import { init } from "./commands/init.js";
9
+ import { plan } from "./commands/plan.js";
10
+ import { profile } from "./commands/profile.js";
11
+ import { report } from "./commands/report.js";
12
+ import { resume } from "./commands/resume.js";
13
+ import { run } from "./commands/run.js";
14
+ import { scope } from "./commands/scope.js";
15
+ import { stats } from "./commands/stats.js";
16
+ import { status } from "./commands/status.js";
17
+ import { ui } from "./commands/ui.js";
18
+ import { unlock } from "./commands/unlock.js";
19
+ import { verify } from "./commands/verify.js";
20
+ import { version } from "./commands/version.js";
2
21
  export type CommandResult = string | {
3
22
  out: string;
4
23
  code: number;
5
24
  };
6
25
  export type CommandMap = Record<string, (argv: string[]) => Promise<CommandResult>>;
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 auto|herdr|subprocess|orca --route-strict; orca runs only when named)\n status live run state (--watch --events: JSON documents on stdout, keepalives on stderr; 2>&1 corrupts the stream)\n stats all-run channel delivery, red, rescue, author and reviewer statistics\n verify run the gate battery standalone against merge-base(--base, HEAD)..HEAD \u2014 verdict/JSON on stdout, progress on stderr; 2>&1 corrupts the verdict stream (--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|orchestrator-context|overseer|overseer-context|watch, --seat <identity> required (--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";
26
+ export declare const COMMANDS: {
27
+ init: typeof init;
28
+ doctor: typeof doctor;
29
+ fleet: typeof fleet;
30
+ compile: typeof compile;
31
+ scope: typeof scope;
32
+ plan: typeof plan;
33
+ run: typeof run;
34
+ status: typeof status;
35
+ stats: typeof stats;
36
+ resume: typeof resume;
37
+ report: typeof report;
38
+ profile: typeof profile;
39
+ ui: typeof ui;
40
+ unlock: typeof unlock;
41
+ approve: typeof approve;
42
+ beat: typeof beat;
43
+ version: typeof version;
44
+ verify: typeof verify;
45
+ eval: typeof evalCommand;
46
+ };
47
+ export type RegisteredCommand = keyof typeof COMMANDS;
48
+ 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> preview locally with --preview; draft after confirmation or --yes (--force to overwrite)\n plan dry-run routing table + cost estimate + floor lints\n eval discover and validate fixtures, seed isolated temp repos, then clean them up\n run execute the graph (--concurrency N --driver auto|herdr|subprocess|orca --route-strict; orca runs only when named)\n status live run state (--watch --events: JSON documents on stdout, keepalives on stderr; 2>&1 corrupts the stream)\n stats all-run channel delivery, red, rescue, author and reviewer statistics\n verify run the gate battery standalone against merge-base(--base, HEAD)..HEAD \u2014 verdict/JSON on stdout, progress on stderr; 2>&1 corrupts the verdict stream (--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 writes Markdown to stdout; redirect to save a record)\n profile show learned routing profile (profile reset = forget history via cursor, keeps telemetry)\n ui open Home, Run or Evidence (--view home|run|evidence; --setup <id> opens Run Parks)\n unlock remove a stale/garbage run lock (refuses if the holder is alive)\n beat <tier> record one supervision beat for orchestrator|orchestrator-context|overseer|overseer-context|watch, --seat <identity> required (--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\n version print the installed version (--dist adds build location and fingerprint)\n\nUse tickmarkr <command> --help (or -h) for all options and examples.\nUse tickmarkr profile <operation> --help for nested profile help.";
9
49
  export declare function dispatch(cmd: string | undefined, argv: string[], commands?: CommandMap): Promise<{
10
50
  out: string;
11
51
  code: number;
package/dist/cli/index.js CHANGED
@@ -20,6 +20,7 @@ import { ui } from "./commands/ui.js";
20
20
  import { unlock } from "./commands/unlock.js";
21
21
  import { verify } from "./commands/verify.js";
22
22
  import { version } from "./commands/version.js";
23
+ import { commandHelp, hasHelpFlag } from "./help.js";
23
24
  const normalize = (r) => typeof r === "string" ? { out: r, code: 0 } : r;
24
25
  export const COMMANDS = {
25
26
  init, doctor, fleet, compile, scope, plan, run, status, stats, resume, report, profile, ui, unlock, approve, beat, version, verify, eval: evalCommand,
@@ -33,33 +34,47 @@ usage: tickmarkr <command>
33
34
  doctor re-probe adapters, herdr, auth; print capability matrix (--fix writes the test-runner ignore when a safe edit exists)
34
35
  fleet interactive fleet editor (fleet --print for CI drift checks)
35
36
  compile <src> spec → .tickmarkr/graph.json (fails without acceptance criteria)
36
- scope <intent> draft a compiled native spec beside an answered intent (--force to overwrite)
37
+ scope <intent> preview locally with --preview; draft after confirmation or --yes (--force to overwrite)
37
38
  plan dry-run routing table + cost estimate + floor lints
38
- eval run checked-in fixtures against every channel in isolated temp repos
39
+ eval discover and validate fixtures, seed isolated temp repos, then clean them up
39
40
  run execute the graph (--concurrency N --driver auto|herdr|subprocess|orca --route-strict; orca runs only when named)
40
41
  status live run state (--watch --events: JSON documents on stdout, keepalives on stderr; 2>&1 corrupts the stream)
41
42
  stats all-run channel delivery, red, rescue, author and reviewer statistics
42
43
  verify run the gate battery standalone against merge-base(--base, HEAD)..HEAD — verdict/JSON on stdout, progress on stderr; 2>&1 corrupts the verdict stream (--base main --criteria <file> | --task <id> [--files <glob>] [--author adapter:model] [--no-review] [--json])
43
44
  resume <id> continue a run from its journal
44
- report <id> cost/quality report (--md for committable execution record)
45
+ report <id> cost/quality report (--md writes Markdown to stdout; redirect to save a record)
45
46
  profile show learned routing profile (profile reset = forget history via cursor, keeps telemetry)
46
- ui open the Fleet Studio TUI (full-screen tabbed cockpit)
47
+ ui open Home, Run or Evidence (--view home|run|evidence; --setup <id> opens Run Parks)
47
48
  unlock remove a stale/garbage run lock (refuses if the holder is alive)
48
49
  beat <tier> record one supervision beat for orchestrator|orchestrator-context|overseer|overseer-context|watch, --seat <identity> required (--stand-down to hand off); a supervising seat's own watcher loop calls it, and status reads the tier STALE once the beats stop
49
- approve <id> <task> release a park (--uphold sides with the reviewer and funds a fixed attempt; --by <name> --reason <text>); takes effect on resume`;
50
+ approve <id> <task> release a park (--uphold sides with the reviewer and funds a fixed attempt; --by <name> --reason <text>); takes effect on resume
51
+ version print the installed version (--dist adds build location and fingerprint)
52
+
53
+ Use tickmarkr <command> --help (or -h) for all options and examples.
54
+ Use tickmarkr profile <operation> --help for nested profile help.`;
50
55
  // pure, testable dispatcher: resolves a command, forwards argv, shapes the result — no side effects.
51
56
  // unknown/missing cmd → USAGE (exit 1 if a cmd was typed, 0 for bare `tickmarkr`); a handler throw becomes
52
57
  // a one-line `tickmarkr <cmd>: <message>` (never a raw stack) at exit 1.
53
58
  export async function dispatch(cmd, argv, commands = COMMANDS) {
54
- if (cmd && VERSION_FLAGS.has(cmd))
55
- return { out: await version(argv), code: 0 };
56
59
  const usage = process.stdout.isTTY ? BANNER + USAGE : USAGE;
57
60
  if (!cmd || HELP_CMDS.has(cmd))
58
61
  return { out: usage, code: 0 };
59
- const fn = commands[cmd];
62
+ if (VERSION_FLAGS.has(cmd))
63
+ cmd = "version";
64
+ const fn = Object.hasOwn(commands, cmd) ? commands[cmd] : undefined;
60
65
  if (!fn)
61
66
  return { out: usage, code: 1 };
67
+ if (hasHelpFlag(argv))
68
+ return { out: commandHelp(cmd, argv), code: 0 };
62
69
  try {
70
+ // These two legacy handlers scan the entire argv for help before parsing. Preserve their
71
+ // direct-call API, but never let a literal positional turn back into help at the CLI boundary.
72
+ // verify accepts no positionals; status accepts a run ID, which cannot start with a dash.
73
+ const separator = argv.indexOf("--");
74
+ const literalHelp = separator < 0 ? undefined : argv.slice(separator + 1).find((arg) => arg === "--help" || arg === "-h");
75
+ if (literalHelp && (fn === verify || fn === status)) {
76
+ throw new Error(`literal argument ${JSON.stringify(literalHelp)} after -- ${fn === verify ? "is not accepted: verify takes no positional arguments" : "is not a valid run ID"}`);
77
+ }
63
78
  return normalize(await fn(argv));
64
79
  }
65
80
  catch (err) {
@@ -1,5 +1,5 @@
1
1
  import { type JournalEvent } from "../run/journal.js";
2
- import { type ExecutorDriver, type NotifyOpts, type PanesToCloseOpts, type Slot, type SlotOpts } from "./types.js";
2
+ import { type ExecutorDriver, type FocusTarget, type FocusResult, type NotifyOpts, type PanesToCloseOpts, type Slot, type SlotOpts } from "./types.js";
3
3
  export declare const TRAILER_SAFE_FLOOR_COLS = 108;
4
4
  export declare const TRAILER_WIDTH_MARGIN = 2;
5
5
  export declare const DELIVERY_ATTEMPTS = 3;
@@ -69,6 +69,7 @@ export declare class HerdrDriver implements ExecutorDriver {
69
69
  private ws;
70
70
  private callerPane;
71
71
  private watches;
72
+ private watchTokens;
72
73
  constructor(bin?: string, workersPerTab?: number, time?: HerdrTimeSource, journal?: DriverJournal | undefined);
73
74
  private appendDispatchRetry;
74
75
  private appendPaneClose;
@@ -126,9 +127,13 @@ export declare class HerdrDriver implements ExecutorDriver {
126
127
  notify(msg: string, opts?: NotifyOpts): Promise<void>;
127
128
  close(slot: Slot): Promise<void>;
128
129
  private closeGrouped;
129
- private ownedWatchPanes;
130
+ private watchPanes;
131
+ /** Name collisions never confer repository ownership. Unknown boards stay protected. */
132
+ private retireWatch;
133
+ focus(target: FocusTarget): Promise<FocusResult>;
130
134
  private watchSlot;
131
135
  private discardSplit;
136
+ /** Only this repository's matching run may replace its acknowledged board. */
132
137
  narrator(cwd: string, command: string, runId?: string): Promise<Slot>;
133
138
  reconcile(desired: Set<string>, runId: string, opts?: PanesToCloseOpts): Promise<void>;
134
139
  worktree(repo: string, branch: string, baseRef: string): Promise<string>;
@@ -6,7 +6,7 @@ import { declaredInputBoxForWorkerName, matchesEmptyInputBox, matchesInputBox, m
6
6
  import { consumePaneLaunchIntent, PANE_IDENTITY_ENV, paneIdentityLine } from "../brand.js";
7
7
  import { createWorktree, sh } from "../run/git.js";
8
8
  import { Journal } from "../run/journal.js";
9
- import { readSupervision } from "../run/supervision.js";
9
+ import { readSupervision, readWatchBoard, reserveWatchBoard, stopWatchBoard, WATCH_OWNER_ENV } from "../run/supervision.js";
10
10
  import { herdrSealShellPrefix } from "./subprocess.js";
11
11
  import { canonicalizeLegacyName, formatOwnedName, panesToClose, parseOwnedName } from "./types.js";
12
12
  // VIS-09 P43-03: adopted safety floor from 43-MEASUREMENT.md (narrowest safe 53 → floor 108).
@@ -103,6 +103,7 @@ export function boardSplitPlan(_callerCols) {
103
103
  * `role:"other", taskId:"<the whole name>"` for any unrecognised string, so keying on taskId alone
104
104
  * makes EVERY one-off pane its own "task" and gives it a group tab. `watch` is excluded for the same
105
105
  * reason in the other direction — its taskId is the literal "run", which is a board, not a task. */
106
+ const shortTitle = (text) => [...text].slice(0, 20).join("");
106
107
  const TASK_TAB_ROLES = new Set(["worker", "judge", "review", "consult"]);
107
108
  export function taskGroupOf(name) {
108
109
  const { role, taskId } = canonicalizeLegacyName(name, "");
@@ -123,10 +124,10 @@ export function taskGroupOf(name) {
123
124
  export function tabLabelFor(name) {
124
125
  const { role, taskId, attempt } = canonicalizeLegacyName(name, "");
125
126
  if (!TASK_TAB_ROLES.has(role) || !taskId.trim())
126
- return name;
127
+ return shortTitle(name);
127
128
  if (role !== "worker")
128
- return `${role.toUpperCase()} ${taskId}`;
129
- return attempt > 0 ? `${taskId}↻${attempt}` : taskId;
129
+ return shortTitle(`${role.toUpperCase()} ${taskId}`);
130
+ return shortTitle(attempt > 0 ? `${taskId}↻${attempt}` : taskId);
130
131
  }
131
132
  /** Gate panes ride with the task they belong to and never consume the tab cap; everything else does.
132
133
  * Scoped to the three GATE roles deliberately — an earlier cut of this said "not a worker", which let
@@ -168,6 +169,7 @@ export class HerdrDriver {
168
169
  ws = process.env.HERDR_WORKSPACE_ID;
169
170
  callerPane = process.env.HERDR_PANE_ID;
170
171
  watches = new Map();
172
+ watchTokens = new WeakMap();
171
173
  constructor(bin = "herdr", workersPerTab = 3, time = SYSTEM_TIME, journal) {
172
174
  this.bin = bin;
173
175
  this.workersPerTab = workersPerTab;
@@ -469,7 +471,7 @@ export class HerdrDriver {
469
471
  // the worktree — that root pane IS the worker pane. The old one-shot `agent start … -- bash` verb
470
472
  // (which named a fresh bash pane) was removed; 0.7.5's `agent start` only ATTACHES to a DETECTED
471
473
  // agent CLI, so tickmarkr names the bash pane itself via `pane rename` and types the worker command in.
472
- const t = await this.herdr(`tab create --label ${shq(label)} --no-focus --workspace ${shq(this.ws)} --cwd ${shq(cwd)}`);
474
+ const t = await this.herdr(`tab create --label ${shq(shortTitle(label))} --no-focus --workspace ${shq(this.ws)} --cwd ${shq(cwd)}`);
473
475
  if (t.code !== 0)
474
476
  throw new Error(`herdr tab create failed (exit ${t.code}, refusing untargeted placement): ${t.stderr || t.stdout}`);
475
477
  let res;
@@ -597,7 +599,7 @@ export class HerdrDriver {
597
599
  const label = !token ? entry.label
598
600
  : entry.label === token ? `${token}${glyph}`
599
601
  : `${entry.label} · ${token}${glyph}`;
600
- const cmd = `tab rename ${shq(entry.tabId)} ${shq(label)}`;
602
+ const cmd = `tab rename ${shq(entry.tabId)} ${shq(shortTitle(label))}`;
601
603
  const ok = async () => (await this.herdr(cmd)).code === 0;
602
604
  if (await ok() || await ok())
603
605
  return;
@@ -1113,12 +1115,10 @@ export class HerdrDriver {
1113
1115
  await this.herdr(`notification show ${shq(msg)} --sound ${opts?.tier === "attention" ? "request" : opts?.sound ?? "request"}`);
1114
1116
  }
1115
1117
  async close(slot) {
1116
- if (this.watches.get(slot.name)?.id === slot.id) {
1118
+ if (parseOwnedName(slot.name)?.role === "watch") {
1119
+ await this.retireWatch(slot);
1117
1120
  this.watches.delete(slot.name);
1118
- const pane = await this.namedPaneId(slot.name);
1119
- if (pane)
1120
- await this.herdr(`pane close ${shq(pane)}`);
1121
- return; // run-end reconcile may already have reaped it; never close a compacted stale id
1121
+ return;
1122
1122
  }
1123
1123
  if (slot.group && this.groups.has(slot.group)) {
1124
1124
  return this.serial(() => this.closeGrouped(slot));
@@ -1158,29 +1158,63 @@ export class HerdrDriver {
1158
1158
  this.groups.delete(slot.group); // group dies when all generations gone
1159
1159
  }
1160
1160
  }
1161
- // Every surviving tickmarkr-owned board in this workspace — a PRIOR run's and one already wearing
1162
- // this run's own name alike. Both are retired before a new board opens (narrator): what a pane this
1163
- // process did not create is actually RUNNING cannot be read back, and the pre-v1.94 implementation
1164
- // launched a bare `tickmarkr status --watch`, which follows the newest journal.
1165
- async ownedWatchPanes() {
1161
+ async watchPanes(name) {
1166
1162
  if (!this.ws)
1167
1163
  throw new Error("herdr watch placement requires HERDR_WORKSPACE_ID — refusing unseeded pane");
1168
1164
  const list = await this.herdr("pane list");
1169
1165
  if (list.code !== 0)
1170
1166
  throw new Error(`herdr pane list failed: ${list.stderr || list.stdout}`);
1167
+ const panes = JSON.parse(list.stdout).result?.panes;
1168
+ if (!Array.isArray(panes))
1169
+ throw new Error("herdr pane list returned no panes");
1170
+ return panes.filter(p => p.workspace_id === this.ws && p.label === name);
1171
+ }
1172
+ /** Name collisions never confer repository ownership. Unknown boards stay protected. */
1173
+ async retireWatch(slot) {
1174
+ const runId = parseOwnedName(slot.name)?.runId;
1175
+ const owner = runId ? readWatchBoard(slot.cwd, runId) : undefined;
1176
+ const matches = await this.watchPanes(slot.name);
1177
+ if (matches.length === 0)
1178
+ throw new Error(`watch ${slot.name} closed without presence acknowledgement; cleanup unconfirmed`);
1179
+ if (!owner || owner.driver !== this.id || owner.name !== slot.name || owner.workspace !== this.ws ||
1180
+ (this.watchTokens.has(slot) && this.watchTokens.get(slot) !== owner.token) ||
1181
+ owner.pane !== slot.id || matches.length !== 1 || matches[0]?.pane_id !== owner.pane) {
1182
+ throw new Error(`watch ownership unknown or foreign for ${slot.name}; existing board protected`);
1183
+ }
1184
+ await stopWatchBoard(owner, this.time);
1185
+ const verified = await this.watchPanes(slot.name);
1186
+ if (verified.length !== 1 || verified[0]?.pane_id !== owner.pane)
1187
+ throw new Error("watch target changed after acknowledgement; pane protected");
1188
+ const closed = await this.herdr(`pane close ${shq(owner.pane)}`);
1189
+ if (closed.code !== 0 || await this.paneStillOpen(owner.pane))
1190
+ throw new Error(`watch ${slot.name} survived acknowledged close`);
1191
+ }
1192
+ async focus(target) {
1193
+ const { slot, runId, taskId, attempt } = target;
1194
+ if (slot.name !== formatOwnedName({ role: "worker", taskId, attempt, runId }) || !target.workspace || target.workspace !== this.ws) {
1195
+ return { status: "foreign", reason: "Recorded run/task/attempt or workspace does not match this driver" };
1196
+ }
1197
+ const r = await this.herdr("pane list", slot.cwd);
1198
+ if (r.code !== 0)
1199
+ return { status: "unsupported", reason: "Cannot verify the live pane list" };
1171
1200
  let panes;
1172
1201
  try {
1173
- panes = JSON.parse(list.stdout).result?.panes;
1202
+ panes = JSON.parse(r.stdout).result?.panes;
1174
1203
  }
1175
1204
  catch {
1176
- throw new Error(`herdr pane list returned unparseable JSON: ${list.stdout}`);
1205
+ return { status: "unsupported", reason: "Unreadable pane list" };
1177
1206
  }
1178
1207
  if (!Array.isArray(panes))
1179
- throw new Error(`herdr pane list returned no panes: ${list.stdout}`);
1180
- return panes.filter((p) => {
1181
- const owned = typeof p.label === "string" ? parseOwnedName(p.label) : null;
1182
- return p.workspace_id === this.ws && typeof p.pane_id === "string" && owned?.role === "watch" && owned.taskId === "run";
1183
- }).map((p) => p.pane_id);
1208
+ return { status: "unsupported", reason: "Missing pane list" };
1209
+ const named = panes.filter(p => p.label === slot.name && p.workspace_id === target.workspace);
1210
+ if (!named.length)
1211
+ return { status: panes.some(p => p.pane_id === slot.id) ? "foreign" : "closed", reason: "Recorded pane is no longer owned by this attempt; open task evidence" };
1212
+ if (named.length !== 1 || named[0]?.pane_id !== slot.id || (slot.tabId && named[0]?.tab_id !== slot.tabId)) {
1213
+ return { status: "foreign", reason: "Live pane identity differs from the recorded task attempt" };
1214
+ }
1215
+ const focused = await this.herdr(`pane focus ${shq(slot.id)}`, slot.cwd);
1216
+ return focused.code === 0 ? { status: "focused", reason: `Verified ${slot.name} in ${target.workspace}` }
1217
+ : { status: "unsupported", reason: focused.stderr || focused.stdout || "Host refused pane focus" };
1184
1218
  }
1185
1219
  // T2: the watch is a sibling of the daemon's own pane, never a separate tab — placed to the RIGHT
1186
1220
  // of it, always, whatever the terminal measures. Its durable owned name is how a
@@ -1243,38 +1277,34 @@ export class HerdrDriver {
1243
1277
  }
1244
1278
  throw new Error(orphan === null ? why : `${why} — and the split pane ${pane} survived its close (${orphan})`);
1245
1279
  }
1246
- // T6 narrator: the run's single live status surface, RUNNING THE COMMAND THIS CALL SUPPLIED. Only
1247
- // a board this driver instance itself opened is reused (this.watches); any other surviving board —
1248
- // a prior run's, or one already carrying this run's canonical name after a resume — is retired and
1249
- // re-split, because adoption cannot restart or even read the process inside it and a pre-v1.94 pane
1250
- // is running the bare `tickmarkr status --watch`, which narrates the newest journal instead of this
1251
- // run. The retirement is VERIFIED gone before the replacement splits: reconcile is no backstop here
1252
- // (panesToClose skips role "watch" by design, types.ts:92), so an unverified close would leave two
1253
- // boards bound to different runs. Failures propagate — the daemon swallows.
1280
+ /** Only this repository's matching run may replace its acknowledged board. */
1254
1281
  async narrator(cwd, command, runId) {
1255
- const name = runId ? formatOwnedName({ role: "watch", taskId: "run", attempt: 0, runId }) : `narrator-watch-${process.pid}`;
1282
+ if (!this.ws)
1283
+ throw new Error("herdr watch placement requires HERDR_WORKSPACE_ID");
1284
+ if (!runId)
1285
+ throw new Error("herdr narrator requires a run identity");
1286
+ const name = formatOwnedName({ role: "watch", taskId: "run", attempt: 0, runId });
1256
1287
  return this.serial(async () => {
1288
+ const matches = await this.watchPanes(name);
1257
1289
  const cached = this.watches.get(name);
1258
- if (cached)
1290
+ if (cached && matches.length === 1 && matches[0]?.pane_id === cached.id &&
1291
+ readWatchBoard(cwd, runId)?.token === this.watchTokens.get(cached))
1259
1292
  return cached;
1260
- const stale = await this.ownedWatchPanes();
1261
- for (const pane of stale)
1262
- await this.herdr(`pane close ${shq(pane)}`);
1263
- if (stale.length) {
1264
- const survived = (await this.ownedWatchPanes()).filter((p) => stale.includes(p));
1265
- if (survived.length)
1266
- throw new Error(`herdr watch retire failed: ${survived.join(", ")} survived close — refusing a second board`);
1267
- }
1268
- const s = await this.watchSlot(cwd, name);
1269
- this.watches.set(name, s);
1293
+ if (matches.length > 1)
1294
+ throw new Error(`watch ownership ambiguous for ${name}; existing boards protected`);
1295
+ if (matches.length === 1)
1296
+ await this.retireWatch({ id: matches[0].pane_id, name, cwd });
1297
+ const slot = await this.watchSlot(cwd, name);
1298
+ const owner = reserveWatchBoard({ repo: cwd, runId, driver: this.id, workspace: this.ws, pane: slot.id, name });
1299
+ this.watchTokens.set(slot, owner.token);
1270
1300
  try {
1271
- await this.deliverPersistentShellCommand(s, command);
1301
+ await this.deliverPersistentShellCommand(slot, `${WATCH_OWNER_ENV}=${shq(owner.token)} ${command}`);
1302
+ this.watches.set(name, slot);
1303
+ return slot;
1272
1304
  }
1273
- catch (err) {
1274
- this.watches.delete(name);
1275
- throw err;
1305
+ catch (error) {
1306
+ throw new Error(`watch launch unconfirmed for ${name}; pane protected: ${String(error)}`);
1276
1307
  }
1277
- return s;
1278
1308
  });
1279
1309
  }
1280
1310
  // OBS-17 T2 / v1.22b T1: close THIS RUN'S OWN tickmarkr-owned panes that should not exist
@@ -1,6 +1,6 @@
1
1
  import { type ShResult } from "../run/git.js";
2
2
  import { type JournalEvent } from "../run/journal.js";
3
- import { type ExecutorDriver, type NotifyOpts, type Slot, type SlotOpts } from "./types.js";
3
+ import { type ExecutorDriver, type FocusTarget, type FocusResult, type NotifyOpts, type Slot, type SlotOpts } from "./types.js";
4
4
  /** The response families the ONE shared envelope parser serves. There is no second JSON seam. */
5
5
  export declare const ORCA_RESPONSE_FAMILIES: readonly ["status", "create", "list", "read", "send", "wait", "show", "close", "worktree-current", "worktree-set", "hooks-status"];
6
6
  export type OrcaFamily = (typeof ORCA_RESPONSE_FAMILIES)[number];
@@ -200,7 +200,8 @@ export declare class OrcaDriver implements ExecutorDriver {
200
200
  waitAgentStatus(slot: Slot, status: string, timeoutMs: number): Promise<boolean>;
201
201
  sendKey(slot: Slot, key: string): Promise<void>;
202
202
  nudge(slot: Slot, message: string): Promise<boolean>;
203
- narrator(cwd: string, command: string, runId?: string): Promise<Slot>;
203
+ narrator(_cwd: string, _command: string, runId?: string): Promise<Slot>;
204
+ focus(target: FocusTarget): Promise<FocusResult>;
204
205
  project(taskId: string, state: "in-progress" | "in-review" | "completed"): Promise<void>;
205
206
  private setWorkspaceStatus;
206
207
  notify(msg: string, opts?: NotifyOpts): Promise<void>;