tickmarkr 1.37.0 → 1.40.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 (73) hide show
  1. package/README.md +25 -23
  2. package/dist/adapters/claude-code.js +4 -4
  3. package/dist/adapters/codex.js +1 -1
  4. package/dist/adapters/fake.d.ts +2 -2
  5. package/dist/adapters/fake.js +29 -11
  6. package/dist/adapters/grok.js +5 -5
  7. package/dist/adapters/model-lints.d.ts +4 -4
  8. package/dist/adapters/model-lints.js +1 -1
  9. package/dist/adapters/prompt.js +8 -13
  10. package/dist/adapters/registry.d.ts +8 -8
  11. package/dist/adapters/registry.js +17 -17
  12. package/dist/adapters/types.d.ts +4 -4
  13. package/dist/adapters/types.js +3 -2
  14. package/dist/brand.d.ts +2 -0
  15. package/dist/brand.js +17 -0
  16. package/dist/cli/commands/approve.js +3 -3
  17. package/dist/cli/commands/compile.js +1 -1
  18. package/dist/cli/commands/doctor.js +3 -3
  19. package/dist/cli/commands/init.js +57 -19
  20. package/dist/cli/commands/plan.js +12 -1
  21. package/dist/cli/commands/profile.js +6 -6
  22. package/dist/cli/commands/resume.d.ts +4 -1
  23. package/dist/cli/commands/resume.js +4 -1
  24. package/dist/cli/commands/run.d.ts +4 -1
  25. package/dist/cli/commands/run.js +9 -1
  26. package/dist/cli/commands/status.js +2 -1
  27. package/dist/cli/commands/unlock.js +1 -1
  28. package/dist/cli/commands/version.d.ts +1 -0
  29. package/dist/cli/commands/version.js +8 -0
  30. package/dist/cli/index.d.ts +6 -3
  31. package/dist/cli/index.js +13 -18
  32. package/dist/compile/gsd.js +1 -1
  33. package/dist/compile/index.js +9 -3
  34. package/dist/compile/native.d.ts +2 -0
  35. package/dist/compile/native.js +12 -4
  36. package/dist/config/config.d.ts +9 -6
  37. package/dist/config/config.js +15 -13
  38. package/dist/drivers/herdr.js +5 -5
  39. package/dist/drivers/index.d.ts +2 -2
  40. package/dist/drivers/subprocess.js +12 -5
  41. package/dist/drivers/types.js +4 -6
  42. package/dist/gates/acceptance.d.ts +1 -0
  43. package/dist/gates/acceptance.js +25 -5
  44. package/dist/gates/baseline.d.ts +2 -2
  45. package/dist/gates/llm.js +12 -9
  46. package/dist/gates/review.d.ts +2 -2
  47. package/dist/gates/review.js +27 -8
  48. package/dist/gates/run-gates.d.ts +2 -2
  49. package/dist/gates/run-gates.js +2 -2
  50. package/dist/graph/graph.d.ts +2 -2
  51. package/dist/graph/graph.js +4 -16
  52. package/dist/graph/schema.d.ts +2 -0
  53. package/dist/graph/schema.js +16 -1
  54. package/dist/plan/prompt.js +1 -1
  55. package/dist/plan/scope.d.ts +2 -2
  56. package/dist/plan/scope.js +5 -5
  57. package/dist/route/preference.d.ts +4 -4
  58. package/dist/route/router.d.ts +3 -3
  59. package/dist/run/consult.d.ts +5 -2
  60. package/dist/run/consult.js +31 -7
  61. package/dist/run/daemon.js +31 -18
  62. package/dist/run/git.d.ts +1 -1
  63. package/dist/run/git.js +5 -9
  64. package/dist/run/journal.d.ts +2 -2
  65. package/dist/run/journal.js +4 -4
  66. package/dist/run/lock.js +11 -11
  67. package/dist/run/merge.d.ts +2 -2
  68. package/dist/run/merge.js +3 -3
  69. package/dist/run/reconcile.js +1 -1
  70. package/package.json +2 -2
  71. package/schema/rungraph.schema.json +4 -0
  72. package/skills/tickmarkr-auto/SKILL.md +23 -2
  73. package/skills/tickmarkr-loop/SKILL.md +23 -2
package/README.md CHANGED
@@ -6,25 +6,25 @@
6
6
 
7
7
  tickmarkr is a spec-driven orchestration harness for AI coding agent CLIs. You write a spec with
8
8
  acceptance criteria; the engine routes tasks to the best installed agent CLI (claude-code, codex,
9
- cursor-agent, opencode, grok, pi) by cost and capability, dispatches work in isolated git worktrees
10
- — as interactive TUIs when running under [herdr](https://herdr.dev), headless subprocesses otherwise
11
- — and independently verifies every result before merge. Green tasks consolidate onto a
9
+ cursor-agent, opencode, grok, pi) by cost and capability, dispatches work in git worktrees for
10
+ change isolation — as interactive TUIs when running under [herdr](https://herdr.dev), headless
11
+ subprocesses otherwise — and independently verifies each committed result by checking for no new
12
+ baseline failures per task, then strictly verifying the integration tip. Green tasks consolidate onto a
12
13
  `tickmarkr/<runId>` branch; merging to your mainline is always your call, never automated. Engage
13
14
  with full visibility into routing decisions, worker progress, and gate verdicts — or run headless
14
- and review the audit trail afterward.
15
+ and review the local execution log afterward.
15
16
 
16
- <!-- tickmarkr:compat-legacy BEGIN: sanctioned naming residue, unchanged pending shim spec -->
17
- **Legacy naming (unchanged):** tickmarkr reads existing state from `.drovr/` directories and resumes runs with legacy `drovr/<runId>` branch names recorded in their journals; new engagements write to `tickmarkr/<runId>`. Environment variables `DROVR_*` remain unchanged (e.g., `DROVR_FAKE_SCRIPT`, `DROVR_E2E`). New specs use the `<!-- tickmarkr:spec -->` marker; the historical `<!-- drovr:spec -->` marker is still accepted. These names are pending formalization in a future shim spec; no action required — existing runs and configurations remain fully compatible.
18
- <!-- tickmarkr:compat-legacy END -->
17
+ Here, **isolated git worktrees** means change isolation: each task gets its own worktree and branch
18
+ so its diff is separated from sibling tasks. It is not a process sandbox or host-containment boundary.
19
19
 
20
20
  ## Invariants
21
21
 
22
22
  These are law; the codebase fails closed around them:
23
23
 
24
24
  - `acceptance[]` required on every task; compile fails without it
25
- - new engagements consolidate to a `tickmarkr/<runId>` branch — never main (resume of older
26
- engagements keeps the branch names recorded in their journal)
27
- - gates never trust worker claims; tickmarkr verifies everything itself
25
+ - new engagements consolidate to a `tickmarkr/<runId>` branch — never main
26
+ - gates never trust worker claims; each task is checked for no new baseline failures and the merged
27
+ integration tip is strictly re-verified
28
28
  - state is files + git only; no DB, no services
29
29
  - worker/judge/review/consult prompts end with machine-parseable trailers
30
30
  (structured JSON verdicts); parse defensively, fail closed
@@ -73,7 +73,8 @@ tickmarkr report <runId> --md # engagement record in Markdown
73
73
 
74
74
  That's the flow: `init` scaffolds config, you write tasks with `acceptance[]` criteria, `compile`
75
75
  validates and builds the graph, `plan` shows routing decisions and cost, `run` dispatches work to
76
- installed CLIs and independently verifies every result, and `report` documents the outcome. All
76
+ installed CLIs and checks each task for no new baseline failures before strictly verifying the
77
+ integration tip, and `report` documents the outcome. All
77
78
  green tasks land on `tickmarkr/<runId>` — merge to your mainline is your decision.
78
79
 
79
80
  ## Agent-ready repos: `tickmarkr init --agent`
@@ -103,7 +104,7 @@ Consent rules — every write is additive, never destructive:
103
104
 
104
105
  ```bash
105
106
  tickmarkr status # engagement state (--watch to follow live)
106
- tickmarkr resume <runId> # continue an engagement from the audit trail
107
+ tickmarkr resume <runId> # continue an engagement from the local execution log
107
108
  tickmarkr approve <runId> <taskId> # approve a parked task (--reason to document)
108
109
  tickmarkr report <runId> # cost/quality report
109
110
  tickmarkr profile # show the learned routing profile
@@ -165,11 +166,11 @@ routing:
165
166
  ```
166
167
  `tickmarkr plan` lints denied models identically to unauthed ones. Re-enable by deleting one line.
167
168
 
168
- ## Run output and audit trail
169
+ ## Run output and local execution log
169
170
 
170
- When you execute `tickmarkr run`, the daemon records every event in an append-only ledger (the audit trail):
171
+ When you execute `tickmarkr run`, the daemon records every event in an append-only local journal:
171
172
  task dispatch, gate verdicts, worker status, and merges. Narration streams to stdout; you can also
172
- `--watch` or tail the ledger directly. If interrupted, `tickmarkr resume <runId>` replays the ledger
173
+ `--watch` or tail the journal directly. If interrupted, `tickmarkr resume <runId>` replays the journal
173
174
  and continues from the last stable state — deterministic, not ephemeral.
174
175
 
175
176
  ## Usage and cost
@@ -198,7 +199,7 @@ and first-attempt success rate. Cost reporting follows strict honesty rules and
198
199
  - **Basis always shown**: every cost figure prints the token count, rate used, and date so estimates can be audited
199
200
  - **No network calls**: pricing config is operator-maintained locally, seeded with dated
200
201
  comments and LiteLLM's JSON file named as the copy-from source; tickmarkr never calls home to fetch rates
201
- - **Attribution from journal**: token counts come from tickmarkr's own telemetry spans in the audit trail, never from provider invoices or dashboards
202
+ - **Attribution from journal**: token counts come from tickmarkr's own telemetry spans in the local journal, never from provider invoices or dashboards
202
203
 
203
204
  ## Visibility: optional supervised workspace
204
205
 
@@ -253,7 +254,7 @@ Every pane and tab tickmarkr creates receives a **parseable ownership name** enc
253
254
 
254
255
  tickmarkr creates all owned panes only within the run's workspace; any tickmarkr-owned panes discovered outside the run's workspace (from prior runs or placement bugs) are reconciled and closed. Any pane not matching the ownership contract is **foreign** — created by you or another tool — and is never closed automatically.
255
256
 
256
- **Desired-state reconciliation**: A pure function computes the exact set of panes that should exist from the audit trail at any moment:
257
+ **Desired-state reconciliation**: A pure function computes the exact set of panes that should exist from the local journal at any moment:
257
258
  - Worker panes for all in-flight task attempts
258
259
  - Gate panes for unread judge/review/consult verdicts
259
260
  - The watch pane (if running)
@@ -269,11 +270,11 @@ Reconciliation failures (herdr unavailable, a pane vanished mid-sweep) never fai
269
270
 
270
271
  ### Workspace trust
271
272
 
272
- tickmarkr runs work in fresh git worktrees cloned from your repo. Before dispatch, workers check
273
- whether the repo is trusted by their CLI:
273
+ tickmarkr creates fresh git worktrees for change isolation. That is separate from each CLI's own
274
+ workspace-trust behavior:
274
275
 
275
- - `tickmarkr doctor` probes each CLI's trust status and auto-seeds trust where possible
276
- - Some CLIs show a "Workspace Trust" dialog; tickmarkr detects and auto-answers it once per task
276
+ - `tickmarkr doctor` invokes each installed adapter's trust hook where supported and seeds trust where possible
277
+ - Some CLIs show a "Workspace Trust" dialog; tickmarkr auto-answers only a recognized fingerprint once per slot
277
278
  - Other blocked dialogs page you for manual approval
278
279
 
279
280
  ## Spec formats
@@ -298,8 +299,9 @@ Every task requires explicit `acceptance[]` criteria; compile fails without them
298
299
  - **`test <name>`** — run a named test from your suite (must exist)
299
300
  - **`judge <rubric>`** — LLM verdict against your rubric; fail-closed, never overrides failed command/test
300
301
 
301
- At runtime, the **scope gate** verifies that workers only touched files declared in the spec. If a
302
- worker must change out-of-scope paths, the task pauses as a human gate — your approval is required.
302
+ At runtime, the **scope gate** derives `git diff --name-only` from the task base and compares it with
303
+ the spec-declared files. Out-of-scope edits fail unless the operator config explicitly allowlists
304
+ them; worker-declared deviations are recorded as notes, not authority.
303
305
 
304
306
  ## Claude Code integration
305
307
 
@@ -6,7 +6,7 @@ import { parseWorkerResult } from "./prompt.js";
6
6
  import { channelsFromConfig, shq, TokenUsageSchema } from "./types.js";
7
7
  // SPEND-01/SPEND-11: claude writes a per-session JSONL to ~/.claude/projects/<slug>/ where slug is the
8
8
  // realpath'd cwd with every non-alphanumeric char replaced by "-" (verified 114/114 — 36-DIAGNOSIS.md).
9
- // The old `/`-only formula missed the "." in `.drovr/worktrees/…` — ENOENT on every worktree dispatch.
9
+ // The old `/`-only formula missed the "." in `.tickmarkr/worktrees/…` — ENOENT on every worktree dispatch.
10
10
  // Each assistant message carries a per-record ISO `timestamp` and message.usage. We read it POST-HOC
11
11
  // (never the pane, never the trailer) and sum
12
12
  // tokens for records whose top-level cwd matches this task's worktree AND whose timestamp is at/after
@@ -43,9 +43,9 @@ export const claudeCode = {
43
43
  channels: (cfg) => channelsFromConfig("claude-code", cfg),
44
44
  // --strict-mcp-config --mcp-config '{"mcpServers":{}}': pin the MCP surface to empty so fresh-worktree
45
45
  // workers/gates don't load project .mcp.json servers (herdr scrapes dialogs as idle — v1.4 incident,
46
- // memory drovr-worker-mcp-dialog-stall). Live-verified 2026-07-10 on claude 2.1.205 (operator check):
46
+ // memory tickmarkr-worker-mcp-dialog-stall). Live-verified 2026-07-10 on claude 2.1.205 (operator check):
47
47
  // headless (-p) fully suppressed — exit 0, no dialog. Interactive TUI STILL shows the project
48
- // MCP-enable dialog (project trust/enablement, not config loading); Esc dismisses it, and drovr's
48
+ // MCP-enable dialog (project trust/enablement, not config loading); Esc dismisses it, and tickmarkr's
49
49
  // blocked/idle paging surfaces the pane to the operator (same path as cursor's trust dialog).
50
50
  // Gotchas (both bit the 2026-07-10 live check): bare '{}' is REJECTED ("mcpServers: expected record"),
51
51
  // and --mcp-config is VARIADIC — a positional after it is eaten as a config-file path, so another
@@ -53,7 +53,7 @@ export const claudeCode = {
53
53
  headlessCommand: (promptFile, model) => `claude -p "$(cat ${shq(promptFile)})" --model ${shq(model)} --permission-mode bypassPermissions --strict-mcp-config --mcp-config '{"mcpServers":{}}' --output-format text`,
54
54
  // HYG-03: the residual first-entry dialog on an interactive TUI is the workspace TRUST dialog (not MCP
55
55
  // config loading) — CLI-imposed, no flag to pre-accept, only store is claude's global last-writer-wins
56
- // ~/.claude.json keyed on the exact path. Closed WON'T-FIX (decision B, 2026-07-10): drovr writes nothing
56
+ // ~/.claude.json keyed on the exact path. Closed WON'T-FIX (decision B, 2026-07-10): tickmarkr writes nothing
57
57
  // to that file (a seed races claude's own writes, nondeterministically). Amortizes to one operator dismissal
58
58
  // per stable worktree path; blocked-pane paging surfaces it. Do NOT change this command to "fix" the dialog —
59
59
  // see .planning/REQUIREMENTS.md HYG-03 and 21-02-LIVE-CHECK.md. Revisit if upstream ships a --trust flag.
@@ -75,7 +75,7 @@ export function readCodexModelsCache(path) {
75
75
  return { models: [] };
76
76
  }
77
77
  }
78
- // drovr worktrees keep their gitdir under the MAIN repo's .git/worktrees/<name> — outside the
78
+ // tickmarkr worktrees keep their gitdir under the MAIN repo's .git/worktrees/<name> — outside the
79
79
  // workspace-write sandbox root — so git commit dies on index.lock (incident run-20260709-104447
80
80
  // P87-08: "Unable to create …/.git/worktrees/…/index.lock"). Expanded by the pane shell at the
81
81
  // worktree cwd; in a plain repo it resolves to ./.git (already writable, harmless).
@@ -1,4 +1,4 @@
1
- import type { DrovrConfig } from "../config/config.js";
1
+ import type { TickmarkrConfig } from "../config/config.js";
2
2
  import type { Task } from "../graph/schema.js";
3
3
  import { type Assignment, type AuthHealth, type BillingChannel, type ContextUsage, type Invocation, type SessionRef, type TokenUsage, type WorkerAdapter, type WorkerResult } from "./types.js";
4
4
  export interface FakeScript {
@@ -29,7 +29,7 @@ export declare class FakeAdapter implements WorkerAdapter {
29
29
  private attempts;
30
30
  constructor(scriptPath: string);
31
31
  probe(): Promise<AuthHealth>;
32
- channels(_cfg: DrovrConfig): BillingChannel[];
32
+ channels(_cfg: TickmarkrConfig): BillingChannel[];
33
33
  private judgeIdx;
34
34
  headlessCommand(promptFile: string, _model: string): string;
35
35
  resumeCommand(_sessionId: string, promptFile: string, model: string): string;
@@ -14,7 +14,13 @@ export class FakeAdapter {
14
14
  this.script = JSON.parse(readFileSync(scriptPath, "utf8"));
15
15
  }
16
16
  async probe() {
17
- return { installed: true, authed: true, version: "fake", models: ["fake-1", "fake-2"] };
17
+ return {
18
+ installed: true, authed: true, version: "fake", models: ["fake-1", "fake-2"],
19
+ modelAuth: {
20
+ "fake-1": { authed: true, probedAt: "1970-01-01T00:00:00.000Z" },
21
+ "fake-2": { authed: true, probedAt: "1970-01-01T00:00:00.000Z" },
22
+ },
23
+ };
18
24
  }
19
25
  channels(_cfg) {
20
26
  return [
@@ -22,14 +28,16 @@ export class FakeAdapter {
22
28
  { adapter: "fake", vendor: "fake-b", model: "fake-2", channel: "api", tier: "frontier" },
23
29
  ];
24
30
  }
25
- judgeIdx = 0; // Phase 47: per-instance judge verdict cursor — advances only on DROVR-JUDGE prompts
31
+ judgeIdx = 0; // Phase 47: per-instance judge verdict cursor — advances only on TICKMARKR-JUDGE prompts
26
32
  headlessCommand(promptFile, _model) {
27
33
  // Phase 47 (GATE-09): detect role at COMMAND-BUILD time. runHeadless/runViaDriver write the prompt
28
34
  // file BEFORE calling headlessCommand, so it exists here. The counter advances ONLY when the prompt
29
- // contains DROVR-JUDGE — a review/consult prompt never touches it (research Pitfall 3).
35
+ // contains TICKMARKR-JUDGE — a review/consult prompt never touches it (research Pitfall 3).
36
+ let prompt = "";
30
37
  let isJudge = false;
31
38
  try {
32
- isJudge = /DROVR-JUDGE/.test(readFileSync(promptFile, "utf8"));
39
+ prompt = readFileSync(promptFile, "utf8");
40
+ isJudge = /TICKMARKR-JUDGE/.test(prompt);
33
41
  }
34
42
  catch {
35
43
  // unreadable promptFile: can't detect role; serve static values (legacy headless-call behavior)
@@ -41,11 +49,19 @@ export class FakeAdapter {
41
49
  val = val[Math.min(this.judgeIdx, val.length - 1)];
42
50
  this.judgeIdx++;
43
51
  }
52
+ if (key === "judge" && isJudge && val && typeof val === "object" && !Array.isArray(val)) {
53
+ const verdict = val;
54
+ if (verdict.pass === true && Array.isArray(verdict.criteria) && verdict.criteria.length === 0) {
55
+ const items = prompt.match(/## Acceptance criteria \(judge\)\n([\s\S]*?)\n\n## Diff/)?.[1]
56
+ .split("\n").filter((line) => line.startsWith("- ")).map((line) => line.slice(2)) ?? [];
57
+ val = { ...verdict, criteria: items.map((criterion) => ({ criterion, met: true, reason: "scripted fake pass" })) };
58
+ }
59
+ }
44
60
  const p = join(dirname(this.scriptPath), `${key}.json`);
45
61
  writeFileSync(p, JSON.stringify(val ?? {}, null, 1));
46
62
  return p;
47
63
  };
48
- return `bash -c 'grep -q DROVR-JUDGE ${shq(promptFile)} && cat ${shq(serve("judge"))}; grep -q DROVR-REVIEW ${shq(promptFile)} && cat ${shq(serve("review"))}; grep -q DROVR-CONSULT ${shq(promptFile)} && cat ${shq(serve("consult"))}; true'`;
64
+ return `bash -c 'grep -q TICKMARKR-JUDGE ${shq(promptFile)} && cat ${shq(serve("judge"))}; grep -q TICKMARKR-REVIEW ${shq(promptFile)} && cat ${shq(serve("review"))}; grep -q TICKMARKR-CONSULT ${shq(promptFile)} && cat ${shq(serve("consult"))}; true'`;
49
65
  }
50
66
  resumeCommand(_sessionId, promptFile, model) {
51
67
  return this.interactiveCommand(promptFile, model) ?? this.headlessCommand(promptFile, model);
@@ -59,21 +75,23 @@ export class FakeAdapter {
59
75
  // SPEND-01: synthetic usage as a TIMESTAMPED disk record the fake's own reader picks up post-hoc —
60
76
  // never a trailer, never pane text. node stamps a real ms-precision ISO time (bash 3.2 on darwin has
61
77
  // no ms clock) so the record lands at/after this attempt's dispatch and clears the sinceMs cursor.
62
- // Written AFTER the step's `git add`/commit stays untracked invisible to the scope/evidence gates.
78
+ // Written BEFORE the trailer echo (same order real CLIs use: session-store rows land during work,
79
+ // completion marker last) so an interactive harvest on the trailer never races the usage writer.
80
+ // Still after the step's `git add`/commit ⇒ stays untracked ⇒ invisible to scope/evidence gates.
63
81
  const usageWrite = step.usage
64
- ? `; node -e ${shq(`require("fs").writeFileSync(".drovr-usage.json", JSON.stringify({timestamp:new Date().toISOString(), usage:${JSON.stringify(step.usage)}}))`)}`
82
+ ? `; node -e ${shq(`require("fs").writeFileSync(".tickmarkr-usage.json", JSON.stringify({timestamp:new Date().toISOString(), usage:${JSON.stringify(step.usage)}}))`)}`
65
83
  : "";
66
84
  const base = !step.result
67
85
  ? `bash -c ${shq(step.shell)}` // no trailer: scripted stall/quota
68
- : `bash -c ${shq(step.shell)}; echo ${shq(`DROVR_RESULT_${nonce} ` + JSON.stringify({ deviations: [], ...step.result }))}`;
69
- return base + usageWrite;
86
+ : `bash -c ${shq(step.shell)}${usageWrite}; echo ${shq(`TICKMARKR_RESULT_${nonce} ` + JSON.stringify({ deviations: [], ...step.result }))}`;
87
+ return base;
70
88
  }
71
89
  // SPEND-01: read the cwd-keyed usage record from DISK (never a pane), honoring the attempt cursor.
72
90
  // A record stamped before sinceMs, an unparseable stamp, or a bad usage shape all fail OPEN to
73
91
  // undefined ⇒ unmetered — never 0, never a thrown error that could fail a healthy task.
74
92
  collectUsage(cwd, sinceMs) {
75
93
  try {
76
- const rec = JSON.parse(readFileSync(join(cwd, ".drovr-usage.json"), "utf8"));
94
+ const rec = JSON.parse(readFileSync(join(cwd, ".tickmarkr-usage.json"), "utf8"));
77
95
  const ts = Date.parse(rec?.timestamp);
78
96
  if (!Number.isFinite(ts) || ts < sinceMs)
79
97
  return undefined; // cursor honored, fail-open
@@ -93,7 +111,7 @@ export class FakeAdapter {
93
111
  // parseWorkerResult(output, nonce) succeeds exactly as before the nonce existed
94
112
  nonceFor(promptFile) {
95
113
  try {
96
- return /DROVR_RESULT_([0-9a-z]+)/.exec(readFileSync(promptFile, "utf8"))?.[1] ?? "";
114
+ return /TICKMARKR_RESULT_([0-9a-z]+)/.exec(readFileSync(promptFile, "utf8"))?.[1] ?? "";
97
115
  }
98
116
  catch {
99
117
  return "";
@@ -19,11 +19,11 @@ import { channelsFromConfig, MODEL_ID_RE, shq } from "./types.js";
19
19
  // (4) `grok trace <id> --local` export and ~/.grok/logs/unified.jsonl (1,447 lines) — zero counters.
20
20
  // (5) --debug-file traces DO carry real per-turn input_tokens=… output_tokens=… cache_read_tokens=…
21
21
  // — a REAL source, REJECTED for stated reasons:
22
- // (a) it exists ONLY because drovr would inject --debug-file into every dispatch — a side-channel
23
- // drovr creates, not a record the CLI keeps (same class as cursor's rejected --print --trust
22
+ // (a) it exists ONLY because tickmarkr would inject --debug-file into every dispatch — a side-channel
23
+ // tickmarkr creates, not a record the CLI keeps (same class as cursor's rejected --print --trust
24
24
  // stdout tee, cursor-agent.ts:25-28);
25
25
  // (b) DECISIVE: at DEBUG level the log verbatim-dumps the full OIDC bearer JWT and every MCP server
26
- // env secret — an actual plaintext SUPABASE_ACCESS_TOKEN was observed — drovr forcing a
26
+ // env secret — an actual plaintext SUPABASE_ACCESS_TOKEN was observed — tickmarkr forcing a
27
27
  // per-attempt secret-spilling file onto disk to meter a sub channel fails its own security
28
28
  // posture;
29
29
  // (c) it is tracing diagnostics text with no format contract (xAI can reshape it in any patch).
@@ -102,11 +102,11 @@ export const grok = {
102
102
  // 2026-07-11 (40-RESEARCH F-3): trailer intact + unwrapped, exit 0. "$(cat file)" matches every
103
103
  // other adapter and is already quoting-proven. --permission-mode bypassPermissions is the
104
104
  // autonomous-worker analog of claude's --dangerously-skip-permissions. NEVER pass grok's
105
- // -w/--worktree flag — grok manages its OWN worktrees; a nested worktree inside drovr's worktree
105
+ // -w/--worktree flag — grok manages its OWN worktrees; a nested worktree inside tickmarkr's worktree
106
106
  // is scope-gate chaos (src/run/git.ts owns worktrees).
107
107
  headlessCommand: (promptFile, model) => `grok -p "$(cat ${shq(promptFile)})" --model ${shq(model)} --permission-mode bypassPermissions --output-format plain`,
108
108
  // GROK-02 interactive. Positional prompt seeds the TUI (40-CONTEXT D-01). Same permission flag;
109
- // NEVER the -w/--worktree flag (see headless comment above — drovr owns worktrees).
109
+ // NEVER the -w/--worktree flag (see headless comment above — tickmarkr owns worktrees).
110
110
  interactiveCommand: (promptFile, model) => `grok --model ${shq(model)} --permission-mode bypassPermissions "$(cat ${shq(promptFile)})"`,
111
111
  invoke(task, _cwd, a, ctx) {
112
112
  return { command: this.headlessCommand(ctx.promptFile, a.model) };
@@ -1,10 +1,10 @@
1
- import { type DrovrConfig } from "../config/config.js";
1
+ import { type TickmarkrConfig } from "../config/config.js";
2
2
  import { type AuthHealth, type WorkerAdapter } from "./types.js";
3
3
  export declare const SEED_STAMPED = "2026-07-09";
4
4
  export declare const MODEL_STALE_DAYS = 30;
5
5
  export declare const ttyVisual: () => boolean;
6
- export declare function seedPreferLints(cfg: DrovrConfig, health: Record<string, AuthHealth>, adapters: WorkerAdapter[], overlayPreferShapes?: ReadonlySet<string>): string[];
7
- export declare function modelLints(cfg: DrovrConfig, health: Record<string, AuthHealth>, adapters: WorkerAdapter[], opts?: {
6
+ export declare function seedPreferLints(cfg: TickmarkrConfig, health: Record<string, AuthHealth>, adapters: WorkerAdapter[], overlayPreferShapes?: ReadonlySet<string>): string[];
7
+ export declare function modelLints(cfg: TickmarkrConfig, health: Record<string, AuthHealth>, adapters: WorkerAdapter[], opts?: {
8
8
  tty?: boolean;
9
9
  stateDir?: string;
10
10
  overlayPreferShapes?: ReadonlySet<string>;
@@ -14,4 +14,4 @@ export declare function formatModelAuthLine(excluded: {
14
14
  reason: string;
15
15
  probedAt: string;
16
16
  }[], tty?: boolean, stateDir?: string): string;
17
- export declare function suggestOverlay(cfg: DrovrConfig, health: Record<string, AuthHealth>, adapters: WorkerAdapter[], stateDir?: string): string;
17
+ export declare function suggestOverlay(cfg: TickmarkrConfig, health: Record<string, AuthHealth>, adapters: WorkerAdapter[], stateDir?: string): string;
@@ -11,7 +11,7 @@ const DAY_MS = 86400000;
11
11
  const LINT_VARIANT_RE = /^auto$|-(fast|minimal|low|medium|high|xhigh)$/;
12
12
  const LINT_CAP = 5;
13
13
  const TTY_LINT_CAP = 3;
14
- const DEFAULT_STATE_DIR = ".drovr";
14
+ const DEFAULT_STATE_DIR = ".tickmarkr";
15
15
  const doctorJsonRef = (stateDir) => ` — see ${stateDir}/doctor.json`;
16
16
  export const ttyVisual = () => process.stdout.isTTY === true && process.env.NO_COLOR === undefined;
17
17
  const adapterHasAuthedChannel = (adapterId, shape, cfg, health, adapters) => {
@@ -18,7 +18,7 @@ ${task.files.length ? `\n## File scope — touch ONLY paths matching:\n${list(ta
18
18
  - Do not ask questions; you are unattended. Make the smallest correct change.
19
19
  ${feedback ? `\n## Previous attempt failed gates — fix these specifically\n${feedback}\n` : ""}
20
20
  When finished, end your final message with exactly one line (no code fence):
21
- DROVR_RESULT_${nonce} {"ok":true|false,"summary":"<one sentence>","deviations":["<path or reason>"]}
21
+ TICKMARKR_RESULT_${nonce} {"ok":true|false,"summary":"<one sentence>","deviations":["<path or reason>"]}
22
22
  `;
23
23
  }
24
24
  export function writePrompt(dir, task, attempt, feedback = "", nonce = "") {
@@ -31,26 +31,21 @@ export function writePrompt(dir, task, attempt, feedback = "", nonce = "") {
31
31
  // across lines by a TUI renderer ([\s\S] bridges newlines between tokens) — but never the template
32
32
  // line above, whose literal `true|false` is not a bool followed by , or }. v1.4: the run-tagged nonce
33
33
  // anchors it so displayed source/diffs (which can't know the nonce) can never premature-harvest.
34
- // v1.16 (REN-02): dual-parse — accepts both the new marker spelling and the legacy alternation, so a
35
- // worker/fixture still emitting the old marker keeps parsing through the rename.
36
34
  export function trailerPattern(nonce) {
37
- return `(?:DROVR|DROVER)_RESULT_${nonce} \\{[\\s\\S]{0,120}?"ok":\\s*(true|false)\\s*[,}]`;
35
+ return `TICKMARKR_RESULT_${nonce} \\{[\\s\\S]{0,120}?"ok":\\s*(true|false)\\s*[,}]`;
38
36
  }
39
- // all start positions of either marker spelling, ascending — merged so "last occurrence" and
40
- // "scan backward for the first parseable one" work identically across both spellings.
41
37
  function trailerTokenPositions(raw, nonce) {
38
+ const token = `TICKMARKR_RESULT_${nonce}`;
42
39
  const positions = [];
43
- for (const token of [`DROVR_RESULT_${nonce}`, `DROVER_RESULT_${nonce}`]) {
44
- for (let i = raw.indexOf(token); i !== -1; i = raw.indexOf(token, i + 1))
45
- positions.push(i);
46
- }
47
- return positions.sort((a, b) => a - b);
40
+ for (let i = raw.indexOf(token); i !== -1; i = raw.indexOf(token, i + 1))
41
+ positions.push(i);
42
+ return positions;
48
43
  }
49
44
  export function parseWorkerResult(raw, nonce) {
50
45
  const fail = (summary) => ({ ok: false, summary, deviations: [], raw });
51
46
  const positions = trailerTokenPositions(raw, nonce);
52
47
  if (positions.length === 0)
53
- return fail("worker produced no DROVR_RESULT trailer");
48
+ return fail("worker produced no TICKMARKR_RESULT trailer");
54
49
  // TUIs echo the prompt template, redraw lines, and HARD-wrap the JSON with per-line margins
55
50
  // (cursor does; recent-unwrapped can't rejoin hard newlines). Scan occurrences backward — last
56
51
  // parseable wins — joining wrapped lines, stripping margin/box chrome, and growing the candidate
@@ -80,5 +75,5 @@ export function parseWorkerResult(raw, nonce) {
80
75
  }
81
76
  }
82
77
  }
83
- return fail("unparseable DROVR_RESULT trailer");
78
+ return fail("unparseable TICKMARKR_RESULT trailer");
84
79
  }
@@ -1,4 +1,4 @@
1
- import { type DrovrConfig } from "../config/config.js";
1
+ import { type TickmarkrConfig } from "../config/config.js";
2
2
  import { type RoutingProfile } from "../route/profile.js";
3
3
  import { type AuthHealth, type BillingChannel, type WorkerAdapter } from "./types.js";
4
4
  export declare function allAdapters(opts?: {
@@ -14,20 +14,20 @@ export interface AutoPreferDoc {
14
14
  }
15
15
  export declare const pendingAutoPreferKey: unique symbol;
16
16
  export declare const DOCTOR_ROUTING_STALE_MS: number;
17
- export declare function deriveAutoPrefer(cfg: DrovrConfig, adapters: WorkerAdapter[], health: Record<string, AuthHealth>, profile?: RoutingProfile): AutoPreferDoc;
17
+ export declare function deriveAutoPrefer(cfg: TickmarkrConfig, adapters: WorkerAdapter[], health: Record<string, AuthHealth>, profile?: RoutingProfile): AutoPreferDoc;
18
18
  export declare function readAutoPrefer(repoRoot: string): AutoPreferDoc | null;
19
- export declare function routingPreferContext(repoRoot: string, cfg: DrovrConfig, opts?: {
19
+ export declare function routingPreferContext(repoRoot: string, cfg: TickmarkrConfig, opts?: {
20
20
  globalDir?: string;
21
21
  }): {
22
22
  autoPrefer?: AutoPreferDoc;
23
23
  doctorFresh: boolean;
24
24
  overlayPreferShapes: ReadonlySet<string>;
25
25
  };
26
- export declare function probeModels(cfg: DrovrConfig, repoRoot: string, adapters: WorkerAdapter[], health: Record<string, AuthHealth>, onProgress?: ProbeModelProgress): Promise<void>;
26
+ export declare function probeModels(cfg: TickmarkrConfig, repoRoot: string, adapters: WorkerAdapter[], health: Record<string, AuthHealth>, onProgress?: ProbeModelProgress): Promise<void>;
27
27
  export declare function writeDoctor(repoRoot: string, health: Record<string, AuthHealth>): void;
28
28
  export declare function readDoctor(repoRoot: string): Record<string, AuthHealth> | null;
29
- export declare function discoverChannels(cfg: DrovrConfig, adapters: WorkerAdapter[], health: Record<string, AuthHealth>): BillingChannel[];
30
- export declare function servableExclusions(cfg: DrovrConfig, adapters: WorkerAdapter[], health: Record<string, AuthHealth>): {
29
+ export declare function discoverChannels(cfg: TickmarkrConfig, adapters: WorkerAdapter[], health: Record<string, AuthHealth>): BillingChannel[];
30
+ export declare function servableExclusions(cfg: TickmarkrConfig, adapters: WorkerAdapter[], health: Record<string, AuthHealth>): {
31
31
  key: string;
32
32
  adapter: string;
33
33
  }[];
@@ -35,7 +35,7 @@ export declare function servabilityLine(excluded: {
35
35
  key: string;
36
36
  adapter: string;
37
37
  }[]): string;
38
- export declare function modelAuthExclusions(cfg: DrovrConfig, adapters: WorkerAdapter[], health: Record<string, AuthHealth>): {
38
+ export declare function modelAuthExclusions(cfg: TickmarkrConfig, adapters: WorkerAdapter[], health: Record<string, AuthHealth>): {
39
39
  key: string;
40
40
  adapter: string;
41
41
  reason: string;
@@ -54,6 +54,6 @@ export declare function initDoctorReuse(repoRoot: string, fresh: boolean): {
54
54
  ageMs: number | null;
55
55
  health: Record<string, AuthHealth> | null;
56
56
  };
57
- export declare function formatDoctorReport(cwd: string, cfg: DrovrConfig, health: Record<string, AuthHealth>, adapters: WorkerAdapter[], opts?: {
57
+ export declare function formatDoctorReport(cwd: string, cfg: TickmarkrConfig, health: Record<string, AuthHealth>, adapters: WorkerAdapter[], opts?: {
58
58
  wrote?: boolean;
59
59
  }): string;
@@ -4,7 +4,7 @@ import { join } from "node:path";
4
4
  import { overlayPreferShapes, TIER_RANK } from "../config/config.js";
5
5
  import { modelLints, suggestOverlay } from "./model-lints.js";
6
6
  import { HerdrDriver } from "../drivers/herdr.js";
7
- import { drovrDir, stateDirName } from "../graph/graph.js";
7
+ import { tickmarkrDir, stateDirName } from "../graph/graph.js";
8
8
  import { disallowedBy, excludedChannels, exclusionLine, preferRanks } from "../route/preference.js";
9
9
  import { learnedScore } from "../route/profile.js";
10
10
  import { marginalCostRank } from "../route/router.js";
@@ -22,7 +22,7 @@ export function allAdapters(opts = {}) {
22
22
  // keeps the Phase 6 shape→channel matrix byte-identical; inserting anywhere else silently
23
23
  // reassigns shapes on same-tier ties. grok is appended AFTER pi for the same reason.
24
24
  const real = [claudeCode, codex, cursorAgent, opencode, pi, grok];
25
- const fakePath = opts.fakeScriptPath ?? process.env.DROVR_FAKE_SCRIPT ?? process.env.DROVER_FAKE_SCRIPT;
25
+ const fakePath = opts.fakeScriptPath ?? process.env.TICKMARKR_FAKE_SCRIPT;
26
26
  return fakePath ? [new FakeAdapter(fakePath), ...real] : real;
27
27
  }
28
28
  export function getAdapter(id, adapters) {
@@ -131,7 +131,7 @@ export async function probeModels(cfg, repoRoot, adapters, health, onProgress) {
131
131
  verdict = { authed: false, reason: "headless probe unavailable", probedAt, durationMs: Date.now() - t0 };
132
132
  }
133
133
  else {
134
- const promptFile = join(mkdtempSync(join(tmpdir(), "drovr-auth-")), "probe.md");
134
+ const promptFile = join(mkdtempSync(join(tmpdir(), "tickmarkr-auth-")), "probe.md");
135
135
  writeFileSync(promptFile, MODEL_PROBE_PROMPT);
136
136
  const r = await sh(a.headlessCommand(promptFile, model), probeRoot, MODEL_PROBE_TIMEOUT_MS);
137
137
  if (r.timedOut && priorTimedOut) {
@@ -181,7 +181,7 @@ function readDoctorFile(repoRoot) {
181
181
  return existsSync(doctorPath(repoRoot)) ? JSON.parse(readFileSync(doctorPath(repoRoot), "utf8")) : null;
182
182
  }
183
183
  export function writeDoctor(repoRoot, health) {
184
- drovrDir(repoRoot);
184
+ tickmarkrDir(repoRoot);
185
185
  const pending = health[pendingAutoPreferKey];
186
186
  const payload = { ...health };
187
187
  delete payload[pendingAutoPreferKey];
@@ -202,10 +202,10 @@ export function discoverChannels(cfg, adapters, health) {
202
202
  .flatMap((a) => {
203
203
  const h = health[a.id];
204
204
  const s = h?.servable;
205
- // T2 (2026-07-13): a model doctor marked unauthed (modelAuth[model].authed===false) advertises NO
206
- // channel — routing a 403 into dispatch was the v1.10 regression. modelAuthed is true when no verdict
207
- // is recorded (absent modelAuth unknown ⇒ routable), so an all-authed/pre-v1.5 fleet is byte-identical.
208
- return a.channels(cfg).filter((c) => modelAuthed(h, c.model) && (!s || s.includes(c.model)));
205
+ // T2 (2026-07-13): only a model doctor marked authed (modelAuth[model].authed===true) advertises a
206
+ // channel — routing an unknown or 403 into dispatch is fail-closed. Operators with pre-v1.21 doctor.json
207
+ // can opt into the prior unknown-is-routable behavior with routing.allowUnverifiedModels.
208
+ return a.channels(cfg).filter((c) => modelAuthed(h, c.model, cfg.routing.allowUnverifiedModels) && (!s || s.includes(c.model)));
209
209
  });
210
210
  if (!cfg.routing.allow && !cfg.routing.deny)
211
211
  return base;
@@ -233,23 +233,23 @@ export function servabilityLine(excluded) {
233
233
  const parts = excluded.map(({ key, adapter }) => `${key} (not in ${adapter}'s served model list)`);
234
234
  return `servability: ${excluded.length} channel(s) unservable — ${parts.join(", ")}`;
235
235
  }
236
- // T2 (2026-07-13): channels discoverChannels dropped because doctor marked their model unauthed
237
- // (modelAuth[model].authed===false). Computed from the SAME inputs as the drop (a.channels(cfg)) so
238
- // attribution can never drift from behavior. installed+authed mirrors the discoverChannels adapter gate;
239
- // a missing modelAuth record ⇒ unknown ⇒ NOT excluded (compat with pre-v1.5 doctor.json + probeAll fallback).
236
+ // T2 (2026-07-13): channels discoverChannels dropped because doctor marked their model unauthed or left it
237
+ // unverified. Computed from the SAME inputs as the drop (a.channels(cfg)) so attribution can never drift from
238
+ // behavior. installed+authed mirrors the discoverChannels adapter gate.
240
239
  export function modelAuthExclusions(cfg, adapters, health) {
241
240
  const out = [];
242
241
  for (const a of adapters) {
243
242
  const h = health[a.id];
244
243
  if (!h?.installed || !h?.authed)
245
244
  continue;
246
- const verdicts = h.modelAuth;
247
- if (!verdicts)
248
- continue;
249
245
  for (const c of a.channels(cfg)) {
250
- const v = verdicts[c.model];
246
+ const v = h.modelAuth?.[c.model];
247
+ if (modelAuthed(h, c.model, cfg.routing.allowUnverifiedModels))
248
+ continue;
251
249
  if (v?.authed === false)
252
250
  out.push({ key: channelKey(c), adapter: a.id, reason: v.reason ?? "probe failed", probedAt: v.probedAt });
251
+ else
252
+ out.push({ key: channelKey(c), adapter: a.id, reason: "no model auth verdict — run tickmarkr doctor", probedAt: "not recorded" });
253
253
  }
254
254
  }
255
255
  return out;
@@ -327,7 +327,7 @@ export function formatDoctorReport(cwd, cfg, health, adapters, opts = {}) {
327
327
  let drift = "";
328
328
  if (frag) {
329
329
  if (visual) {
330
- const overlayPath = join(drovrDir(cwd), "doctor-overlay.yaml");
330
+ const overlayPath = join(tickmarkrDir(cwd), "doctor-overlay.yaml");
331
331
  writeFileSync(overlayPath, frag);
332
332
  drift = `\nmodel drift: unclassified models detected — paste-ready overlay written to ${overlayPath} (advisory; tickmarkr never applies it)`;
333
333
  }
@@ -1,5 +1,5 @@
1
1
  import { z } from "zod";
2
- import type { DrovrConfig, Tier } from "../config/config.js";
2
+ import type { TickmarkrConfig, Tier } from "../config/config.js";
3
3
  import type { Task } from "../graph/schema.js";
4
4
  export declare const TokenUsageSchema: z.ZodObject<{
5
5
  input: z.ZodNumber;
@@ -38,7 +38,7 @@ export interface AuthHealth {
38
38
  servable?: string[];
39
39
  modelAuth?: Record<string, ModelAuth>;
40
40
  }
41
- export declare function modelAuthed(health: AuthHealth | undefined, model: string): boolean;
41
+ export declare function modelAuthed(health: AuthHealth | undefined, model: string, allowUnverifiedModels?: boolean): boolean;
42
42
  export interface Invocation {
43
43
  command: string;
44
44
  }
@@ -74,7 +74,7 @@ export interface WorkerAdapter {
74
74
  vendor: string;
75
75
  probeCwd?: "repo" | "neutral";
76
76
  probe(): Promise<AuthHealth>;
77
- channels(cfg: DrovrConfig): BillingChannel[];
77
+ channels(cfg: TickmarkrConfig): BillingChannel[];
78
78
  headlessCommand(promptFile: string, model: string): string;
79
79
  interactiveCommand(promptFile: string, model: string): string | null;
80
80
  resumeCommand?(sessionId: string, promptFile: string, model: string): string;
@@ -89,7 +89,7 @@ export interface WorkerAdapter {
89
89
  trust?(repoRoot: string): TrustVerdict;
90
90
  trustDialog?: TrustDialog;
91
91
  }
92
- export declare function channelsFromConfig(adapterId: string, cfg: DrovrConfig): BillingChannel[];
92
+ export declare function channelsFromConfig(adapterId: string, cfg: TickmarkrConfig): BillingChannel[];
93
93
  export declare function channelKey(c: {
94
94
  adapter: string;
95
95
  model: string;
@@ -23,8 +23,9 @@ export function addUsage(a, b) {
23
23
  reasoning: add(a.reasoning, b.reasoning),
24
24
  };
25
25
  }
26
- export function modelAuthed(health, model) {
27
- return health?.modelAuth?.[model]?.authed !== false;
26
+ export function modelAuthed(health, model, allowUnverifiedModels = false) {
27
+ const authed = health?.modelAuth?.[model]?.authed;
28
+ return authed === true || (authed === undefined && allowUnverifiedModels);
28
29
  }
29
30
  export function matchesTrustDialog(paneText, dialog) {
30
31
  return paneText.includes(dialog.fingerprint);
@@ -0,0 +1,2 @@
1
+ export declare const BANNER: string;
2
+ export declare function bannerShell(): string;
package/dist/brand.js ADDED
@@ -0,0 +1,17 @@
1
+ // TTY-only pixel-tick logo (assets/mark.svg is the image twin — same block geometry).
2
+ // Never printed to pipes — the non-TTY stdout surface is byte-pinned by tests and consumed by machines.
3
+ const B = "\x1b[1m", R = "\x1b[0m";
4
+ const g = (n, s) => `\x1b[38;5;${n}m${s}${R}`; // 256-color green ramp, bright → deep
5
+ export const BANNER = [
6
+ ` ${g(84, "▄▄████")}`,
7
+ ` ${g(78, "▄▄████▀▀")}`,
8
+ `${g(41, "████▄▄▄▄████▀▀")} ${B}tickmarkr${R}`,
9
+ ` ${g(35, "▀▀████▀▀")} spec in, verified work out.`,
10
+ "",
11
+ ].join("\n");
12
+ // Shell one-liner that prints the banner inside a pane before a gate command runs. ESC bytes are
13
+ // carried as printf %b escapes (never raw control bytes in a command string crossing the herdr socket).
14
+ export function bannerShell() {
15
+ const printable = BANNER.replaceAll("\x1b", "\\033").replaceAll("\n", "\\n");
16
+ return `printf '%b\\n' '${printable}'`;
17
+ }
@@ -1,9 +1,9 @@
1
1
  import { userInfo } from "node:os";
2
2
  import { ATTEMPT_CAP_RELEASE, Journal } from "../../run/journal.js";
3
- // GATE-08 (v1.12): approve a parked human gate so the next `drovr resume <runId>` dispatches it.
3
+ // GATE-08 (v1.12): approve a parked human gate so the next `tickmarkr resume <runId>` dispatches it.
4
4
  //
5
5
  // The approval is a JOURNAL EVENT (task-approved) carrying who and when — it touches ONLY the
6
- // append-only journal. Writing it into drovr's compiled graph artifact would be silently erased by
6
+ // append-only journal. Writing it into tickmarkr's compiled graph artifact would be silently erased by
7
7
  // the next recompile (which re-emits humanGate:true from the plan frontmatter) — Phase 42 D-02.
8
8
  //
9
9
  // v1.24 OBS-18: when the park was attempt-cap (not a humanGate pre-dispatch park), the event also
@@ -13,7 +13,7 @@ import { ATTEMPT_CAP_RELEASE, Journal } from "../../run/journal.js";
13
13
  //
14
14
  // Fail-closed (D-05): unknown runId, unknown taskId, a not-parked task, and a double-approve are all
15
15
  // LOUD refusals that name the reason and append NO event — never a silent no-op. A handler throw
16
- // becomes `drovr approve: <message>` at exit 1 (src/cli/index.ts dispatch).
16
+ // becomes `tickmarkr approve: <message>` at exit 1 (src/cli/index.ts dispatch).
17
17
  //
18
18
  // Who/when is truthful, not dressed-up auth (D-03): default actor os.userInfo().username; --by overrides
19
19
  // for delegated approval; optional --reason; the event's ts (stamped by Journal.append) is the when.