tickmarkr 1.36.0 → 1.38.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 (63) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +83 -207
  3. package/dist/adapters/claude-code.js +4 -4
  4. package/dist/adapters/codex.js +1 -1
  5. package/dist/adapters/fake.d.ts +2 -2
  6. package/dist/adapters/fake.js +8 -8
  7. package/dist/adapters/grok.js +5 -5
  8. package/dist/adapters/model-lints.d.ts +4 -4
  9. package/dist/adapters/model-lints.js +1 -1
  10. package/dist/adapters/prompt.js +8 -13
  11. package/dist/adapters/registry.d.ts +8 -8
  12. package/dist/adapters/registry.js +5 -5
  13. package/dist/adapters/types.d.ts +3 -3
  14. package/dist/adapters/types.js +1 -1
  15. package/dist/brand.d.ts +2 -0
  16. package/dist/brand.js +17 -0
  17. package/dist/cli/commands/approve.js +3 -3
  18. package/dist/cli/commands/compile.js +1 -1
  19. package/dist/cli/commands/doctor.js +3 -3
  20. package/dist/cli/commands/init.js +14 -9
  21. package/dist/cli/commands/profile.js +6 -6
  22. package/dist/cli/commands/status.js +2 -1
  23. package/dist/cli/commands/unlock.js +1 -1
  24. package/dist/cli/index.d.ts +1 -1
  25. package/dist/cli/index.js +9 -7
  26. package/dist/compile/gsd.js +1 -1
  27. package/dist/compile/index.js +9 -3
  28. package/dist/compile/native.d.ts +2 -0
  29. package/dist/compile/native.js +4 -2
  30. package/dist/config/config.d.ts +6 -6
  31. package/dist/config/config.js +13 -12
  32. package/dist/drivers/herdr.js +5 -5
  33. package/dist/drivers/index.d.ts +2 -2
  34. package/dist/drivers/subprocess.js +3 -3
  35. package/dist/drivers/types.js +4 -6
  36. package/dist/gates/acceptance.js +1 -1
  37. package/dist/gates/baseline.d.ts +2 -2
  38. package/dist/gates/llm.js +12 -9
  39. package/dist/gates/review.d.ts +2 -2
  40. package/dist/gates/review.js +1 -1
  41. package/dist/gates/run-gates.d.ts +2 -2
  42. package/dist/graph/graph.d.ts +2 -2
  43. package/dist/graph/graph.js +4 -16
  44. package/dist/plan/prompt.js +1 -1
  45. package/dist/plan/scope.d.ts +2 -2
  46. package/dist/plan/scope.js +5 -5
  47. package/dist/route/preference.d.ts +4 -4
  48. package/dist/route/router.d.ts +3 -3
  49. package/dist/run/consult.d.ts +2 -2
  50. package/dist/run/consult.js +5 -5
  51. package/dist/run/daemon.d.ts +2 -0
  52. package/dist/run/daemon.js +48 -17
  53. package/dist/run/git.d.ts +1 -1
  54. package/dist/run/git.js +5 -9
  55. package/dist/run/journal.d.ts +2 -2
  56. package/dist/run/journal.js +9 -5
  57. package/dist/run/lock.js +11 -11
  58. package/dist/run/merge.d.ts +11 -2
  59. package/dist/run/merge.js +21 -3
  60. package/dist/run/reconcile.js +1 -1
  61. package/package.json +4 -2
  62. package/skills/tickmarkr-auto/SKILL.md +1 -1
  63. package/skills/tickmarkr-loop/SKILL.md +1 -1
@@ -74,7 +74,7 @@ export class HerdrDriver {
74
74
  }
75
75
  async slot(cwd, name, opts) {
76
76
  // T1 ownership contract: `opts.owned` (T2 call sites) names the pane canonically —
77
- // drovr:<role>:<taskId>:<attempt>:<runId>. Without it, `name` passes through byte-identical
77
+ // tickmarkr:<role>:<taskId>:<attempt>:<runId>. Without it, `name` passes through byte-identical
78
78
  // (today's legacy daemon/gates/consult shapes) — canonicalizeLegacyName (types.ts) is what lets
79
79
  // reconcile.ts and this driver's own renameGroupTab/glyphFor decode role/taskId/attempt from
80
80
  // those shapes without a call-site migration; T2 retires this branch by always passing `owned`.
@@ -92,7 +92,7 @@ export class HerdrDriver {
92
92
  // named after a dead cursor worker and the operator read it as a mislabeled agent)
93
93
  async tabSlot(cwd, name, label = name) {
94
94
  // tab-per-slot: concurrent agents in one tab split it into sliver columns — TUIs exit or
95
- // hard-wrap at COLUMNS≈2, shredding even the DROVR_RESULT marker (v1.4 phase-1 incident).
95
+ // hard-wrap at COLUMNS≈2, shredding even the TICKMARKR_RESULT marker (v1.4 phase-1 incident).
96
96
  // A dedicated named tab gives every agent a full-width pane; tab close() reaps it.
97
97
  // VIS-10 (operator ruling 2026-07-11): "pane placed by focus heuristic" is a DEFECT CLASS.
98
98
  // Fail closed at every step — env unset, tab-create non-zero, unparseable stdout, or a parsed
@@ -100,7 +100,7 @@ export class HerdrDriver {
100
100
  if (!this.ws)
101
101
  throw new Error("herdr placement requires HERDR_WORKSPACE_ID — refusing untargeted pane (VIS-10: fail closed, never place by focus)");
102
102
  // pin the tab to the RUN's workspace, UNCONDITIONALLY (inherited via HERDR_WORKSPACE_ID), never the
103
- // operator's focused one (Intl-Dossier run-20260709-104447 incident: worker tabs opened in the drovr repo workspace)
103
+ // operator's focused one (Intl-Dossier run-20260709-104447 incident: worker tabs opened in the tickmarkr repo workspace)
104
104
  const t = await this.herdr(`tab create --label ${shq(label)} --no-focus --workspace ${shq(this.ws)}`);
105
105
  if (t.code !== 0)
106
106
  throw new Error(`herdr tab create failed (exit ${t.code}, refusing untargeted placement): ${t.stderr || t.stdout}`);
@@ -139,7 +139,7 @@ export class HerdrDriver {
139
139
  if (typeof id !== "string" || !id)
140
140
  throw new Error(`herdr agent start returned no pane id: ${r.stdout}`);
141
141
  // tab create auto-spawns a root shell pane and agent start --tab adds the agent as a SECOND
142
- // pane — reap the idle shell so no tab shows a dead "drovr git:" prompt beside its agent
142
+ // pane — reap the idle shell so no tab shows a dead "tickmarkr git:" prompt beside its agent
143
143
  // (VIS-04 orphan fix). Best-effort: a failed reap costs cosmetics only.
144
144
  if (typeof rootPane === "string" && rootPane && rootPane !== id) {
145
145
  await this.herdr(`pane close ${shq(rootPane)}`);
@@ -444,7 +444,7 @@ export class HerdrDriver {
444
444
  return s;
445
445
  });
446
446
  }
447
- // OBS-17 T2 / v1.22b T1: close every drovr-owned pane that should not exist (superseded attempts,
447
+ // OBS-17 T2 / v1.22b T1: close every tickmarkr-owned pane that should not exist (superseded attempts,
448
448
  // killed-daemon orphans, leftovers from OLDER runs) — in this run's workspace OR misplaced in any
449
449
  // other one — then reap the tabs those closes emptied. Ownership is decided ONLY by parseOwnedName
450
450
  // (drivers/types.ts panesToClose) — foreign names never become candidates, in any workspace; a pane
@@ -1,3 +1,3 @@
1
- import type { DrovrConfig } from "../config/config.js";
1
+ import type { TickmarkrConfig } from "../config/config.js";
2
2
  import type { ExecutorDriver } from "./types.js";
3
- export declare function pickDriver(cfg: DrovrConfig, override?: "auto" | "herdr" | "subprocess"): ExecutorDriver;
3
+ export declare function pickDriver(cfg: TickmarkrConfig, override?: "auto" | "herdr" | "subprocess"): ExecutorDriver;
@@ -1,8 +1,8 @@
1
1
  import { spawn } from "node:child_process";
2
2
  import { createWorktree } from "../run/git.js";
3
- // HARD-03: cap retained worker output so a chatty worker can't grow drovr's heap unbounded.
3
+ // HARD-03: cap retained worker output so a chatty worker can't grow tickmarkr's heap unbounded.
4
4
  // 2MB is safe against BOTH consumers: the largest read() call site asks for 1000 lines
5
- // (daemon.ts:217/247), and every marker (DROVR_RESULT, DROVR_EXIT_<nonce>) is emitted at the
5
+ // (daemon.ts:217/247), and every marker (TICKMARKR_RESULT, TICKMARKR_EXIT_<nonce>) is emitted at the
6
6
  // END of output — a trailer would only be evicted if >2MB arrived AFTER it inside one 200ms
7
7
  // waitOutput poll (~10MB/s sustained, far beyond agent-CLI rates). Tail-truncate, never head:
8
8
  // consumers only ever tail-read.
@@ -43,7 +43,7 @@ export class SubprocessDriver {
43
43
  }
44
44
  async run(slot, cmd) {
45
45
  const s = this.state(slot);
46
- // HARD-05: interactive=false — no operator, so an open stdin pipe is a promise drovr can never
46
+ // HARD-05: interactive=false — no operator, so an open stdin pipe is a promise tickmarkr can never
47
47
  // keep; codex exec appends a piped stdin as a <stdin> block (`codex exec --help`) and blocks on a
48
48
  // read that never EOFs. One spawn site covers every adapter (D-06).
49
49
  // v1.22 T3: seal herdr control vars so worker/judge/review/consult children cannot reach the
@@ -1,15 +1,13 @@
1
1
  // ---- Ownership contract (T1: OBS-17 pane-hygiene) --------------------------------------------
2
- // Every pane/tab drovr creates is identified by exactly one parseable token:
3
- // drovr:<role>:<taskId>:<attempt>:<runId>
2
+ // Every pane/tab tickmarkr creates is identified by exactly one parseable token:
3
+ // tickmarkr:<role>:<taskId>:<attempt>:<runId>
4
4
  // role ∈ OWNED_ROLES; attempt is a non-negative integer; taskId/runId never contain ":" (task and
5
5
  // run ids are alphanumeric/dash by construction elsewhere). formatOwnedName/parseOwnedName round-trip
6
6
  // exactly. parseOwnedName (or isForeignName) is the ONLY way reconcile.ts may decide a live pane name
7
- // is drovr-owned — anything that doesn't parse is foreign and is never a candidate for closing.
7
+ // is tickmarkr-owned — anything that doesn't parse is foreign and is never a candidate for closing.
8
8
  export const OWNED_ROLES = ["worker", "judge", "review", "consult", "watch", "other"];
9
- // read-old/write-new: new panes are named tickmarkr:*, but reconcile still recognizes (and may
10
- // close/reuse) drovr:* panes left by pre-rename runs — resumed old runs keep working.
11
9
  const OWNED_PREFIX = "tickmarkr";
12
- const OWNED_RE = new RegExp(`^(?:${OWNED_PREFIX}|drovr):(${OWNED_ROLES.join("|")}):([^:]+):(\\d+):([^:]+)$`);
10
+ const OWNED_RE = new RegExp(`^${OWNED_PREFIX}:(${OWNED_ROLES.join("|")}):([^:]+):(\\d+):([^:]+)$`);
13
11
  export function formatOwnedName(o) {
14
12
  return `${OWNED_PREFIX}:${o.role}:${o.taskId}:${o.attempt}:${o.runId}`;
15
13
  }
@@ -58,7 +58,7 @@ export async function acceptanceGate(task, worktree, baseRef, judge, via, opts =
58
58
  ? "WARNING: only judge oracles — no deterministic command/test oracle guards this task (deterministic preferred, spec §2).\n"
59
59
  : "";
60
60
  const diff = (await shOk(`git diff '${baseRef}..HEAD'`, worktree)).slice(0, DIFF_CAP);
61
- const prompt = `DROVR-JUDGE
61
+ const prompt = `TICKMARKR-JUDGE
62
62
  You are a strict acceptance judge. Decide whether the diff satisfies EVERY acceptance criterion.
63
63
  Judge only what the diff proves — plausible-but-wrong must fail. Do not award partial credit.
64
64
  Deterministic command/test oracles have already passed mechanically; judge ONLY the rubric items below.
@@ -1,4 +1,4 @@
1
- import type { DrovrConfig } from "../config/config.js";
1
+ import type { TickmarkrConfig } from "../config/config.js";
2
2
  import type { GateResult } from "./types.js";
3
3
  export interface Baseline {
4
4
  commands: Record<string, {
@@ -7,6 +7,6 @@ export interface Baseline {
7
7
  }>;
8
8
  }
9
9
  export declare function fingerprint(output: string): string[];
10
- export declare function detectGateCommands(repoRoot: string, cfg: DrovrConfig): Record<string, string>;
10
+ export declare function detectGateCommands(repoRoot: string, cfg: TickmarkrConfig): Record<string, string>;
11
11
  export declare function captureBaseline(cwd: string, commands: Record<string, string>): Promise<Baseline>;
12
12
  export declare function compareToBaseline(cwd: string, commands: Record<string, string>, baseline: Baseline, enabled: string[]): Promise<GateResult[]>;
package/dist/gates/llm.js CHANGED
@@ -2,6 +2,7 @@ import { mkdtempSync, writeFileSync } from "node:fs";
2
2
  import { tmpdir } from "node:os";
3
3
  import { join } from "node:path";
4
4
  import { formatOwnedName, parseOwnedName } from "../drivers/types.js";
5
+ import { bannerShell } from "../brand.js";
5
6
  import { sh } from "../run/git.js";
6
7
  export const GATE_PANE_SEP = " · ";
7
8
  /** T8: role-first pane name for fleet visibility — judge · T4, review · T3, consult · T2. */
@@ -12,7 +13,7 @@ export function gatePaneName(role, taskId, suffix = "") {
12
13
  // via.name remains the fallback for non-gate runViaDriver callers and carries -r1 retry suffixes.
13
14
  // T2 ownership contract: a canonical owned fallback (the daemon's nameFor now emits one) passes
14
15
  // through untouched; run-gates' "-r1" judge-retry suffix becomes attempt+1 so the retry pane's name
15
- // stays contract-parseable (drovr:judge:<task>:1:<runId>) instead of a corrupted-runId shape.
16
+ // stays contract-parseable (tickmarkr:judge:<task>:1:<runId>) instead of a corrupted-runId shape.
16
17
  export function rolePaneNameFromPrompt(prompt, fallback) {
17
18
  const retry = fallback.endsWith("-r1");
18
19
  const base = retry ? fallback.slice(0, -3) : fallback;
@@ -22,14 +23,14 @@ export function rolePaneNameFromPrompt(prompt, fallback) {
22
23
  const id = prompt.match(/## Task ([^\n:]+):/)?.[1];
23
24
  if (!id)
24
25
  return fallback;
25
- if (prompt.startsWith("DROVR-JUDGE"))
26
+ if (prompt.startsWith("TICKMARKR-JUDGE"))
26
27
  return gatePaneName("judge", id, retry ? "-r1" : "");
27
- if (prompt.startsWith("DROVR-REVIEW"))
28
+ if (prompt.startsWith("TICKMARKR-REVIEW"))
28
29
  return gatePaneName("review", id, retry ? "-r1" : "");
29
30
  return fallback;
30
31
  }
31
32
  export async function runHeadless(adapter, model, prompt, cwd, timeoutMs = 300000) {
32
- const pf = join(mkdtempSync(join(tmpdir(), "drovr-llm-")), "prompt.md");
33
+ const pf = join(mkdtempSync(join(tmpdir(), "tickmarkr-llm-")), "prompt.md");
33
34
  writeFileSync(pf, prompt);
34
35
  const r = await sh(adapter.headlessCommand(pf, model), cwd, timeoutMs);
35
36
  return r.stdout + "\n" + r.stderr;
@@ -37,15 +38,17 @@ export async function runHeadless(adapter, model, prompt, cwd, timeoutMs = 30000
37
38
  // v1.1 default path: the same headless CLI call, but dispatched through the driver
38
39
  // as a visible named agent (herdr pane), with the quote-split completion wrapper.
39
40
  export async function runViaDriver(adapter, model, prompt, cwd, via, timeoutMs = 300000) {
40
- const pf = join(mkdtempSync(join(tmpdir(), "drovr-llm-")), "prompt.md");
41
+ const pf = join(mkdtempSync(join(tmpdir(), "tickmarkr-llm-")), "prompt.md");
41
42
  writeFileSync(pf, prompt);
42
43
  const slot = await via.driver.slot(cwd, rolePaneNameFromPrompt(prompt, via.name), via.label ? { label: via.label } : undefined);
43
44
  via.onSlot?.(slot);
44
- await via.driver.run(slot, `${adapter.headlessCommand(pf, model)}; printf '\\nDROVR_''EXIT:%s\\n' $?`);
45
- // wait for the digit-suffixed marker (a real $? exit code), never a bare "DROVR_EXIT:" that a
45
+ // brand banner at pane top — gate panes run headless-print (no alt screen), so it stays visible;
46
+ // extractJson is anchored to JSON braces and the exit marker to TICKMARKR_EXIT:\d, neither matches banner text
47
+ await via.driver.run(slot, `${bannerShell()}; ${adapter.headlessCommand(pf, model)}; printf '\\nTICKMARKR_''EXIT:%s\\n' $?`);
48
+ // wait for the digit-suffixed marker (a real $? exit code), never a bare "TICKMARKR_EXIT:" that a
46
49
  // self-referential diff under review merely DISPLAYS — same guard the worker path uses (daemon.ts:193).
47
- // Without it, a judge/review pane echoing drovr's own source false-completes before its verdict lands.
48
- await via.driver.waitOutput(slot, "DROVR_EXIT:\\d", timeoutMs, { regex: true });
50
+ // Without it, a judge/review pane echoing tickmarkr's own source false-completes before its verdict lands.
51
+ await via.driver.waitOutput(slot, "TICKMARKR_EXIT:\\d", timeoutMs, { regex: true });
49
52
  const out = await via.driver.read(slot, 400);
50
53
  if (!via.keep)
51
54
  await via.driver.close(slot);
@@ -1,5 +1,5 @@
1
1
  import { type Assignment, type BillingChannel, type WorkerAdapter } from "../adapters/types.js";
2
- import { type DrovrConfig } from "../config/config.js";
2
+ import { type TickmarkrConfig } from "../config/config.js";
3
3
  import { type Task } from "../graph/schema.js";
4
4
  import { type GateVia } from "./llm.js";
5
5
  import type { GateResult } from "./types.js";
@@ -9,4 +9,4 @@ export interface ReviewVerdict {
9
9
  }
10
10
  export declare function modelId(model: string): string;
11
11
  export declare function pickReviewer(author: Assignment, channels: BillingChannel[], exclude?: string[]): BillingChannel | null;
12
- export declare function reviewGate(task: Task, worktree: string, baseRef: string, author: Assignment, channels: BillingChannel[], adapters: WorkerAdapter[], cfg: DrovrConfig, via?: GateVia, excludeReviewers?: string[]): Promise<GateResult>;
12
+ export declare function reviewGate(task: Task, worktree: string, baseRef: string, author: Assignment, channels: BillingChannel[], adapters: WorkerAdapter[], cfg: TickmarkrConfig, via?: GateVia, excludeReviewers?: string[]): Promise<GateResult>;
@@ -39,7 +39,7 @@ export async function reviewGate(task, worktree, baseRef, author, channels, adap
39
39
  : { gate: "review", pass: true, details: "WARNING: no cross-vendor reviewer available — review waived by config" };
40
40
  }
41
41
  const diff = (await shOk(`git diff '${baseRef}..HEAD'`, worktree)).slice(0, DIFF_CAP);
42
- const prompt = `DROVR-REVIEW
42
+ const prompt = `TICKMARKR-REVIEW
43
43
  You are a skeptical cross-vendor code reviewer. Another agent (vendor: ${author.adapter}) authored this diff.
44
44
  Look for correctness bugs, security issues, and acceptance-criteria gaps. Approve only if you would merge it.
45
45
 
@@ -1,5 +1,5 @@
1
1
  import { type Assignment, type BillingChannel, type WorkerAdapter, type WorkerResult } from "../adapters/types.js";
2
- import { type DrovrConfig } from "../config/config.js";
2
+ import { type TickmarkrConfig } from "../config/config.js";
3
3
  import { type GateName, type Task } from "../graph/schema.js";
4
4
  import { type Baseline } from "./baseline.js";
5
5
  import type { GateVia } from "./llm.js";
@@ -23,7 +23,7 @@ export interface GateContext {
23
23
  baseline: Baseline;
24
24
  channels: BillingChannel[];
25
25
  adapters: WorkerAdapter[];
26
- cfg: DrovrConfig;
26
+ cfg: TickmarkrConfig;
27
27
  via?: GateVia;
28
28
  excludeReviewers?: string[];
29
29
  onGate?: (e: GateEvent) => void | Promise<void>;
@@ -1,7 +1,7 @@
1
1
  import { type RunGraph, type Task, type TaskStatus } from "./schema.js";
2
- export declare function stateDirName(repoRoot: string): string;
2
+ export declare function stateDirName(_repoRoot: string): string;
3
3
  export declare function graphPath(repoRoot: string): string;
4
- export declare function drovrDir(repoRoot: string): string;
4
+ export declare function tickmarkrDir(repoRoot: string): string;
5
5
  export declare function loadGraph(repoRoot: string): RunGraph;
6
6
  export declare function saveGraph(repoRoot: string, g: RunGraph): void;
7
7
  export declare function getTask(g: RunGraph, id: string): Task;
@@ -1,25 +1,13 @@
1
1
  import { existsSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
2
2
  import { join } from "node:path";
3
3
  import { validateGraph } from "./schema.js";
4
- const stateDirs = new Map();
5
- export function stateDirName(repoRoot) {
6
- const cached = stateDirs.get(repoRoot);
7
- if (cached)
8
- return cached;
9
- const name = existsSync(join(repoRoot, ".tickmarkr"))
10
- ? ".tickmarkr"
11
- : existsSync(join(repoRoot, ".drovr"))
12
- ? ".drovr"
13
- : existsSync(join(repoRoot, ".drover"))
14
- ? ".drover"
15
- : ".tickmarkr";
16
- stateDirs.set(repoRoot, name);
17
- return name;
4
+ export function stateDirName(_repoRoot) {
5
+ return ".tickmarkr";
18
6
  }
19
7
  export function graphPath(repoRoot) {
20
8
  return join(repoRoot, stateDirName(repoRoot), "graph.json");
21
9
  }
22
- export function drovrDir(repoRoot) {
10
+ export function tickmarkrDir(repoRoot) {
23
11
  const dir = join(repoRoot, stateDirName(repoRoot));
24
12
  mkdirSync(dir, { recursive: true });
25
13
  const gi = join(dir, ".gitignore");
@@ -34,7 +22,7 @@ export function loadGraph(repoRoot) {
34
22
  return validateGraph(JSON.parse(readFileSync(p, "utf8")));
35
23
  }
36
24
  export function saveGraph(repoRoot, g) {
37
- drovrDir(repoRoot);
25
+ tickmarkrDir(repoRoot);
38
26
  const p = graphPath(repoRoot);
39
27
  // Temp file MUST be a sibling of graph.json: rename(2) is atomic only within one filesystem
40
28
  // (never os.tmpdir()). pid-suffix so a racing writer can't clobber our in-flight temp (HARD-04).
@@ -1,5 +1,5 @@
1
1
  export function scopePrompt(intent, repair) {
2
- return `DROVR-JUDGE
2
+ return `TICKMARKR-JUDGE
3
3
  You are drafting a tickmarkr native spec from an answered intent. Return only the Markdown spec, with no commentary or code fence.
4
4
 
5
5
  The draft must:
@@ -1,9 +1,9 @@
1
1
  import type { WorkerAdapter } from "../adapters/types.js";
2
- import type { DrovrConfig } from "../config/config.js";
2
+ import type { TickmarkrConfig } from "../config/config.js";
3
3
  import type { ExecutorDriver } from "../drivers/types.js";
4
4
  export declare function clarificationGate(intent: string): string[];
5
5
  export interface ScopeOptions {
6
- cfg: DrovrConfig;
6
+ cfg: TickmarkrConfig;
7
7
  adapters: WorkerAdapter[];
8
8
  driver?: ExecutorDriver;
9
9
  force?: boolean;
@@ -2,7 +2,7 @@ import { existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "no
2
2
  import { tmpdir } from "node:os";
3
3
  import { basename, dirname, extname, join } from "node:path";
4
4
  import { discoverChannels, getAdapter, probeAll } from "../adapters/registry.js";
5
- import { compileNative, NATIVE_MARKER } from "../compile/native.js";
5
+ import { compileNative, LEGACY_PREFIX } from "../compile/native.js";
6
6
  import { pickDriver } from "../drivers/index.js";
7
7
  import { extractJson, runLlm } from "../gates/llm.js";
8
8
  import { TaskSchema } from "../graph/schema.js";
@@ -48,7 +48,7 @@ function section(source, name) {
48
48
  return lines.slice(start + 1, end === -1 ? undefined : end).join("\n");
49
49
  }
50
50
  function extractDraft(raw) {
51
- const clean = raw.replace(/\nDROVR_EXIT:\d[\s\S]*$/, "").trim();
51
+ const clean = raw.replace(/\nTICKMARKR_EXIT:\d[\s\S]*$/, "").trim();
52
52
  try {
53
53
  const value = JSON.parse(clean);
54
54
  if (typeof value === "string")
@@ -64,18 +64,18 @@ function extractDraft(raw) {
64
64
  const fenced = [...clean.matchAll(/```(?:markdown|md)?\s*\n([\s\S]*?)```/gi)].at(-1)?.[1];
65
65
  if (fenced)
66
66
  return fenced;
67
- const marker = clean.search(/<!--\s*(?:tickmarkr|drovr):spec/);
67
+ const marker = clean.search(new RegExp(`<!--\\s*(?:tickmarkr|${LEGACY_PREFIX}):spec`));
68
68
  return (marker === -1 ? clean : clean.slice(marker)).trimEnd() + "\n";
69
69
  }
70
70
  function validateDraft(draft) {
71
- if (!NATIVE_MARKER.test(draft))
71
+ if (!/^<!--\s*tickmarkr:spec/.test(draft))
72
72
  throw new Error("draft is missing the tickmarkr native marker");
73
73
  if (!section(draft, "Assumptions").trim())
74
74
  throw new Error("draft is missing explicit assumptions");
75
75
  const requirements = [...new Set(section(draft, "Requirements").match(/\bREQ-\d{2}\b/g) ?? [])];
76
76
  if (!requirements.length)
77
77
  throw new Error("draft has no REQ-nn requirements");
78
- const dir = mkdtempSync(join(tmpdir(), "drovr-scope-compile-"));
78
+ const dir = mkdtempSync(join(tmpdir(), "tickmarkr-scope-compile-"));
79
79
  const file = join(dir, "draft.spec.md");
80
80
  try {
81
81
  writeFileSync(file, draft);
@@ -1,10 +1,10 @@
1
1
  import { type AuthHealth } from "../adapters/types.js";
2
- import type { DrovrConfig } from "../config/config.js";
2
+ import type { TickmarkrConfig } from "../config/config.js";
3
3
  export interface Disallowed {
4
4
  by: "deny" | "allow";
5
5
  entry: string;
6
6
  }
7
- export declare function excludedChannels(cfg: DrovrConfig, adapters: {
7
+ export declare function excludedChannels(cfg: TickmarkrConfig, adapters: {
8
8
  id: string;
9
9
  }[] | string[], health: Record<string, AuthHealth>): {
10
10
  key: string;
@@ -17,11 +17,11 @@ export declare function exclusionLine(excluded: {
17
17
  export declare function preferRanks(c: {
18
18
  adapter: string;
19
19
  model: string;
20
- }, cfg: DrovrConfig): {
20
+ }, cfg: TickmarkrConfig): {
21
21
  shape: string;
22
22
  rank: number;
23
23
  }[];
24
24
  export declare function disallowedBy(c: {
25
25
  adapter: string;
26
26
  model: string;
27
- }, routing: DrovrConfig["routing"]): Disallowed | null;
27
+ }, routing: TickmarkrConfig["routing"]): Disallowed | null;
@@ -1,5 +1,5 @@
1
1
  import { type Assignment, type BillingChannel } from "../adapters/types.js";
2
- import { type DrovrConfig } from "../config/config.js";
2
+ import { type TickmarkrConfig } from "../config/config.js";
3
3
  import type { Task } from "../graph/schema.js";
4
4
  import { type RoutingProfile } from "./profile.js";
5
5
  export type LadderStep = "retry" | "escalate" | "consult" | "human";
@@ -30,5 +30,5 @@ export declare class RoutingError extends Error {
30
30
  constructor(msg: string);
31
31
  }
32
32
  export declare function marginalCostRank(c: BillingChannel): number;
33
- export declare function route(task: Task, cfg: DrovrConfig, channels: BillingChannel[], profile?: RoutingProfile, preferCtx?: RoutingPreferContext): Route;
34
- export declare function nextChannel(current: Assignment, task: Task, cfg: DrovrConfig, channels: BillingChannel[], tried: string[], profile?: RoutingProfile): Assignment | null;
33
+ export declare function route(task: Task, cfg: TickmarkrConfig, channels: BillingChannel[], profile?: RoutingProfile, preferCtx?: RoutingPreferContext): Route;
34
+ export declare function nextChannel(current: Assignment, task: Task, cfg: TickmarkrConfig, channels: BillingChannel[], tried: string[], profile?: RoutingProfile): Assignment | null;
@@ -1,5 +1,5 @@
1
1
  import type { WorkerAdapter } from "../adapters/types.js";
2
- import type { DrovrConfig } from "../config/config.js";
2
+ import type { TickmarkrConfig } from "../config/config.js";
3
3
  import type { ExecutorDriver, Slot } from "../drivers/types.js";
4
4
  import type { GateResult } from "../gates/types.js";
5
5
  export interface ConsultVerdict {
@@ -16,7 +16,7 @@ export interface Dossier {
16
16
  gates: GateResult[];
17
17
  }
18
18
  export declare function buildDossierPrompt(d: Dossier): string;
19
- export declare function consult(d: Dossier, cfg: DrovrConfig, adapters: WorkerAdapter[], driver: ExecutorDriver, cwd: string, runDir: string, opts?: {
19
+ export declare function consult(d: Dossier, cfg: TickmarkrConfig, adapters: WorkerAdapter[], driver: ExecutorDriver, cwd: string, runDir: string, opts?: {
20
20
  keep?: boolean;
21
21
  onSlot?: (slot: Slot) => void;
22
22
  runId?: string;
@@ -5,7 +5,7 @@ import { extractJson, gatePaneName } from "../gates/llm.js";
5
5
  import { sh } from "./git.js";
6
6
  const ACTIONS = ["retry", "reroute", "decompose", "human"];
7
7
  export function buildDossierPrompt(d) {
8
- return `DROVR-CONSULT
8
+ return `TICKMARKR-CONSULT
9
9
  You are a senior engineering consult for the tickmarkr orchestrator. A worker task hit trouble.
10
10
  Read the dossier and return a verdict. Be decisive; cost is the lowest priority, quality the highest.
11
11
 
@@ -37,7 +37,7 @@ Respond with ONLY this JSON:
37
37
  }
38
38
  let consultSeq = 0;
39
39
  export async function consult(d, cfg, adapters, driver, cwd, runDir,
40
- // T2 ownership contract: runId names the consult pane canonically (drovr:consult:<task>:0:<runId>)
40
+ // T2 ownership contract: runId names the consult pane canonically (tickmarkr:consult:<task>:0:<runId>)
41
41
  // via SlotOpts.owned; without it the legacy gatePaneName shape survives (non-daemon callers/tests).
42
42
  opts = {}) {
43
43
  const n = ++consultSeq;
@@ -58,10 +58,10 @@ opts = {}) {
58
58
  ...(opts.runId ? { owned: { role: "consult", taskId: d.taskId, attempt: 0, runId: opts.runId } } : {}),
59
59
  });
60
60
  opts.onSlot?.(slot);
61
- await driver.run(slot, `${adapter.headlessCommand(promptFile, cfg.consult.model)}; printf '\\nDROVR_''EXIT:%s\\n' $?`);
62
- // digit-suffixed marker only: a dossier quoting drovr's own "DROVR_EXIT:" literal must not
61
+ await driver.run(slot, `${adapter.headlessCommand(promptFile, cfg.consult.model)}; printf '\\nTICKMARKR_''EXIT:%s\\n' $?`);
62
+ // digit-suffixed marker only: a dossier quoting tickmarkr's own "TICKMARKR_EXIT:" literal must not
63
63
  // false-complete the consult pane before its verdict (same guard as daemon.ts:193 / llm.ts).
64
- await driver.waitOutput(slot, "DROVR_EXIT:\\d", cfg.consult.stallMinutes * 60_000, { regex: true });
64
+ await driver.waitOutput(slot, "TICKMARKR_EXIT:\\d", cfg.consult.stallMinutes * 60_000, { regex: true });
65
65
  out = await driver.read(slot, 300);
66
66
  if (!opts.keep)
67
67
  await driver.close(slot);
@@ -18,6 +18,8 @@ export interface RunSummary {
18
18
  human: string[];
19
19
  pending: string[];
20
20
  blocked: string[];
21
+ tipVerify?: "passed" | "failed";
22
+ lastMergedTask?: string;
21
23
  }
22
24
  export declare function formatSummary(s: RunSummary): string;
23
25
  export declare function runDaemon(repoRoot: string, opts?: RunOptions): Promise<RunSummary>;
@@ -14,13 +14,16 @@ import { consult } from "./consult.js";
14
14
  import { cleanupRunWorktrees, gitHead, sh } from "./git.js";
15
15
  import { Journal, loadRoutingProfile, newRunId } from "./journal.js";
16
16
  import { acquireRunLock, releaseRunLock } from "./lock.js";
17
- import { ensureIntegration, integrationBranch, integrationHead, mergeTask } from "./merge.js";
17
+ import { ensureIntegration, integrationBranch, integrationHead, mergeTask, verifyIntegrationTip } from "./merge.js";
18
18
  import { nextChannel, route } from "../route/router.js";
19
19
  import { desiredPanes } from "./reconcile.js";
20
20
  // VIS-01: one formatter, four readers (run-end journal event, run/resume CLI, run-end notify).
21
21
  // Parity by construction — every caller renders the same complete bucket line.
22
22
  export function formatSummary(s) {
23
- return `done: ${s.done.length}, failed: ${s.failed.length}, human: ${s.human.length}, blocked: ${s.blocked.length}, pending: ${s.pending.length}\nintegration branch: ${s.branch}`;
23
+ const tip = s.tipVerify === "failed"
24
+ ? `\ntip verify: FAILED${s.lastMergedTask ? ` (last merged: ${s.lastMergedTask})` : ""}`
25
+ : s.tipVerify === "passed" ? "\ntip verify: passed" : "";
26
+ return `done: ${s.done.length}, failed: ${s.failed.length}, human: ${s.human.length}, blocked: ${s.blocked.length}, pending: ${s.pending.length}\nintegration branch: ${s.branch}${tip}`;
24
27
  }
25
28
  const MAX_ATTEMPTS = 10; // ponytail: hard cap so a pathological ladder can never loop forever
26
29
  const BLOCKED_POLL_MS = 30_000; // between trailer-wait slices, check whether the pane is blocked on a prompt
@@ -50,7 +53,7 @@ export async function runDaemon(repoRoot, opts = {}) {
50
53
  const recordedBranch = typeof branchEvent?.data.branch === "string" ? branchEvent.data.branch : undefined;
51
54
  const branch = recordedBranch
52
55
  ? branchEvent.event === "merge" ? recordedBranch.slice(0, recordedBranch.lastIndexOf("--")) : recordedBranch
53
- : opts.resume ? `drovr/${runId}` : integrationBranch(cfg, runId);
56
+ : integrationBranch(cfg, runId);
54
57
  if (lock.reclaimed)
55
58
  journal.append("lock-reclaimed", undefined, lock.reclaimed); // HARD-02 audit trail
56
59
  // GATE-08 (v1.12): the humanGate guard consults this run's journaled approvals, not just the compiled
@@ -124,14 +127,14 @@ export async function runDaemon(repoRoot, opts = {}) {
124
127
  };
125
128
  await reconcile(); // run start/resume boundary: nothing in flight — full sweep, incl. older runs' leftovers
126
129
  // v1.4 self-reference guard: a random nonce on the worker trailer AND exit marker. Displayed
127
- // source/diffs (e.g. a worker editing drovr's own prompt.ts/daemon.ts) can't know it, so an echoed
128
- // DROVR_RESULT/DROVR_EXIT literal can never premature-harvest the worker. Quote-split keeps the
130
+ // source/diffs (e.g. a worker editing tickmarkr's own prompt.ts/daemon.ts) can't know it, so an echoed
131
+ // TICKMARKR_RESULT/TICKMARKR_EXIT literal can never premature-harvest the worker. Quote-split keeps the
129
132
  // echoed command line itself from matching the marker it prints.
130
133
  //
131
134
  // v1.13 (VIS-09 safety, 43-02): the nonce is per-ATTEMPT, declared at the top of the attempt loop
132
135
  // below — NOT here at run scope. A run-scoped nonce is a latent hazard: HerdrDriver.read() is
133
136
  // `pane read --lines 1000` over scrollback and SubprocessDriver never clears s.buf, so any transcript
134
- // retention across attempts would let attempt N harvest attempt N-1's DROVR_RESULT out of scrollback
137
+ // retention across attempts would let attempt N harvest attempt N-1's TICKMARKR_RESULT out of scrollback
135
138
  // as its OWN completion — silently LYING about a worker's outcome. Pinned by the stale-trailer oracle
136
139
  // in tests/run/daemon.test.ts ("a retained prior-attempt trailer cannot complete a retry"); a future
137
140
  // hoist back to run scope reddens it.
@@ -291,8 +294,8 @@ export async function runDaemon(repoRoot, opts = {}) {
291
294
  // v1.13 (VIS-09 safety): one FRESH nonce per attempt — see the run-scope comment above. A retained
292
295
  // prior-attempt trailer (herdr scrollback / subprocess buffer) must never satisfy this attempt.
293
296
  const nonce = randomBytes(4).toString("hex");
294
- const exitMarkerCmd = `printf '\\nDROVR_''EXIT_${nonce}:%s\\n' $?`;
295
- const exitRe = new RegExp(`DROVR_EXIT_${nonce}:(\\d+)`);
297
+ const exitMarkerCmd = `printf '\\nTICKMARKR_''EXIT_${nonce}:%s\\n' $?`;
298
+ const exitRe = new RegExp(`TICKMARKR_EXIT_${nonce}:(\\d+)`);
296
299
  if (attempt >= MAX_ATTEMPTS) {
297
300
  await park(t, `attempt cap (${MAX_ATTEMPTS}) reached`, "attempt-cap", assignment, attempt, startMs, gateFails, consults, tokens, metered, retryMode);
298
301
  return;
@@ -405,9 +408,9 @@ export async function runDaemon(repoRoot, opts = {}) {
405
408
  while (Date.now() < deadline) {
406
409
  const sliceStart = Date.now();
407
410
  const slice = Math.min(BLOCKED_POLL_MS, deadline - sliceStart);
408
- if (await driver.waitOutput(slot, `(${trailerPattern(nonce)})|DROVR_EXIT_${nonce}:\\d`, slice, { regex: true })) {
409
- // verify before accepting: a worker that merely DISPLAYS a marker (e.g. editing drovr's
410
- // own source, where "DROVR_EXIT:" is a string literal) must not end the wait. Only a
411
+ if (await driver.waitOutput(slot, `(${trailerPattern(nonce)})|TICKMARKR_EXIT_${nonce}:\\d`, slice, { regex: true })) {
412
+ // verify before accepting: a worker that merely DISPLAYS a marker (e.g. editing tickmarkr's
413
+ // own source, where "TICKMARKR_EXIT:" is a string literal) must not end the wait. Only a
411
414
  // parseable trailer or a digit-suffixed exit marker in the harvest is completion.
412
415
  output = await driver.read(slot, 1000); // TUI transcripts carry chrome — read deeper than print's 500
413
416
  finished = new RegExp(trailerPattern(nonce)).test(output);
@@ -426,7 +429,7 @@ export async function runDaemon(repoRoot, opts = {}) {
426
429
  const st = paged ? "" : await driver.status(slot);
427
430
  if (!paged && (st === "blocked" || st === "idle")) {
428
431
  // T5: once-per-slot auto-answer when the adapter declares a trust dialog and the pane
429
- // text matches. drovr created the worktree from the operator's own repo — safe by construction.
432
+ // text matches. tickmarkr created the worktree from the operator's own repo — safe by construction.
430
433
  if (!trustAnswered && adapter.trustDialog && driver.sendKey) {
431
434
  try {
432
435
  const paneText = await driver.read(slot, 80);
@@ -470,7 +473,7 @@ export async function runDaemon(repoRoot, opts = {}) {
470
473
  else {
471
474
  const inv = adapter.invoke(t, wt, assignment, { promptFile });
472
475
  await driver.run(slot, `${inv.command}; ${exitMarkerCmd}`);
473
- finished = await driver.waitOutput(slot, `DROVR_EXIT_${nonce}:\\d`, cfg.taskTimeoutMinutes * 60_000, { regex: true });
476
+ finished = await driver.waitOutput(slot, `TICKMARKR_EXIT_${nonce}:\\d`, cfg.taskTimeoutMinutes * 60_000, { regex: true });
474
477
  output = await driver.read(slot, 500);
475
478
  exitCode = Number(exitRe.exec(output)?.[1] ?? 1);
476
479
  }
@@ -545,7 +548,7 @@ export async function runDaemon(repoRoot, opts = {}) {
545
548
  return;
546
549
  const g = e.result;
547
550
  // GATE-09 (ROADMAP SC-4): journal every judge retry as an attributable event — which gate flaked,
548
- // which channel flaked, which channel retried — so `drovr journal`/report can distinguish "judge
551
+ // which channel flaked, which channel retried — so `tickmarkr journal`/report can distinguish "judge
549
552
  // flaked, retried" from "worker failed" (run-20260711-185020 P43-03 L70-72 billed a judge flake as
550
553
  // a worker attempt; 47-01 fixed WHO retries, this closes the audit-trail half). The condition is
551
554
  // META-ONLY (D-03): gate === "acceptance" + typeof-shape guards on meta.judgeRetry — never a
@@ -580,7 +583,7 @@ export async function runDaemon(repoRoot, opts = {}) {
580
583
  // D-07: judge/review panes self-clean when their verdict is read (keepLlm) — only "forever" keeps them.
581
584
  keep: keepLlm,
582
585
  onSlot: keepLlm ? (s) => keptSlots.push(s) : undefined,
583
- // T2 ownership contract: canonical names (drovr:<role>:<task>:0:<runId>) so reconcile
586
+ // T2 ownership contract: canonical names (tickmarkr:<role>:<task>:0:<runId>) so reconcile
584
587
  // owns judge/review panes; run-gates' -r1 retry suffix becomes attempt 1 in llm.ts.
585
588
  // Same-name reuse across worker attempts is safe: panes self-clean when read (keepLlm),
586
589
  // and herdr's DEFECT-01 reclaim covers a kept holdover under keepPanes:forever.
@@ -704,12 +707,37 @@ export async function runDaemon(repoRoot, opts = {}) {
704
707
  blocked: blockedTasks(graph).map((t) => t.id),
705
708
  pending: pendingTasks(graph).map((t) => t.id),
706
709
  };
710
+ // OBS-34: post-merge integration-tip verify — strict exit codes, no baseline forgiveness.
711
+ const lastMergedTask = [...journal.read()].reverse().find((e) => e.event === "merge" && e.taskId)?.taskId;
712
+ if (summary.done.length > 0 && Object.keys(commands).length > 0) {
713
+ const tipResults = await verifyIntegrationTip(intWt, commands);
714
+ let tipFailed = false;
715
+ for (const r of tipResults) {
716
+ if (r.pass) {
717
+ journal.append("tip-verify", undefined, { gate: r.gate, cmd: r.cmd, pass: true, exitCode: r.exitCode, details: r.details });
718
+ }
719
+ else {
720
+ journal.append("tip-verify-failed", undefined, {
721
+ gate: r.gate,
722
+ cmd: r.cmd,
723
+ exitCode: r.exitCode,
724
+ fingerprints: r.fingerprints,
725
+ lastMergedTask,
726
+ });
727
+ tipFailed = true;
728
+ }
729
+ }
730
+ summary.tipVerify = tipFailed ? "failed" : "passed";
731
+ if (tipFailed && lastMergedTask)
732
+ summary.lastMergedTask = lastMergedTask;
733
+ }
707
734
  journal.append("run-end", undefined, { ...summary });
708
735
  await reconcile(); // run-end boundary: nothing in flight — full sweep (empty desired set)
709
736
  // OBS-28: lingering worktrees starve CLI probes; keepPanes:forever is the debug override.
710
737
  if (!keepForever) {
711
738
  const green = summary.failed.length === 0 && summary.human.length === 0
712
- && summary.blocked.length === 0 && summary.pending.length === 0;
739
+ && summary.blocked.length === 0 && summary.pending.length === 0
740
+ && summary.tipVerify !== "failed";
713
741
  await cleanupRunWorktrees(repoRoot, branch, { removeIntegration: green, removeTaskIds: summary.done });
714
742
  }
715
743
  // VIS-02: name each blocked subtree by its nearest parked/failed root, e.g. "3 blocked behind P40-02".
@@ -717,7 +745,10 @@ export async function runDaemon(repoRoot, opts = {}) {
717
745
  .sort(([a], [b]) => a.localeCompare(b))
718
746
  .map(([root, count]) => `${count} blocked behind ${root}`)
719
747
  .join(", ");
720
- await driver.notify(`tickmarkr ${runId}: ${summary.done.length} done, ${summary.failed.length} failed, ${summary.human.length} awaiting human, ${summary.blocked.length} blocked, ${summary.pending.length} pending${attribution ? ` (${attribution})` : ""} — integration branch ${branch} (merge to main is yours)`, { tier: "attention" });
748
+ const tipFail = summary.tipVerify === "failed"
749
+ ? ` — TIP VERIFY FAILED on ${summary.lastMergedTask ? `last merge ${summary.lastMergedTask}` : "integration tip"}`
750
+ : "";
751
+ await driver.notify(`tickmarkr ${runId}: ${summary.done.length} done, ${summary.failed.length} failed, ${summary.human.length} awaiting human, ${summary.blocked.length} blocked, ${summary.pending.length} pending${attribution ? ` (${attribution})` : ""}${tipFail} — integration branch ${branch} (merge to main is yours)`, { tier: summary.tipVerify === "failed" ? "attention" : "attention" });
721
752
  return summary;
722
753
  }
723
754
  finally {
package/dist/run/git.d.ts CHANGED
@@ -14,6 +14,6 @@ export declare function cleanupRunWorktrees(repo: string, branch: string, opts:
14
14
  removeIntegration: boolean;
15
15
  removeTaskIds: string[];
16
16
  }): Promise<void>;
17
- export declare function resolveIntegrationBranch(repo: string, branch: string): Promise<string>;
17
+ export declare function resolveIntegrationBranch(_repo: string, branch: string): Promise<string>;
18
18
  export declare function createWorktree(repo: string, branch: string, baseRef: string): Promise<string>;
19
19
  export declare function removeWorktree(repo: string, dir: string): Promise<void>;