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
@@ -18,7 +18,7 @@ export async function compile(argv, cwd = process.cwd()) {
18
18
  // as a second daemon. Read-only check — compile never acquires/holds (it's instantaneous).
19
19
  const stateDir = stateDirName(cwd);
20
20
  if (isRunLockLive(cwd))
21
- throw new Error(`${stateDir}/graph.lock is held by another drovr run, or is a stale/garbage lock — refusing to overwrite graph.json. If no run is active, run \`drovr unlock\`.`);
21
+ throw new Error(`${stateDir}/graph.lock is held by another tickmarkr run, or is a stale/garbage lock — refusing to overwrite graph.json. If no run is active, run \`tickmarkr unlock\`.`);
22
22
  saveGraph(cwd, g);
23
23
  return `compiled ${src} → ${stateDir}/graph.json (${g.tasks.length} tasks, source ${g.spec.source}, hash ${g.spec.hash.slice(0, 12)})`;
24
24
  }
@@ -1,7 +1,7 @@
1
1
  import { writeFileSync } from "node:fs";
2
2
  import { join } from "node:path";
3
3
  import { allAdapters, probeAll, probeModels, readAutoPrefer, servableExclusions, servabilityLine, writeDoctor } from "../../adapters/registry.js";
4
- import { drovrDir, stateDirName } from "../../graph/graph.js";
4
+ import { tickmarkrDir, stateDirName } from "../../graph/graph.js";
5
5
  import { modelLints, suggestOverlay, ttyVisual } from "../../adapters/model-lints.js";
6
6
  import { DEFAULT_CONFIG, loadConfig, overlayPreferShapes } from "../../config/config.js";
7
7
  import { HerdrDriver } from "../../drivers/herdr.js";
@@ -99,14 +99,14 @@ export async function doctor(_argv, cwd = process.cwd(), adapters = allAdapters(
99
99
  const servable = servableExclusions(cfg, adapters, health);
100
100
  if (servable.length)
101
101
  rows.push(` ! ${servabilityLine(servable)}`);
102
- // MODEL-05/06: print-only drift fragment; advisory, whole-line-commented additions, drovr NEVER applies it.
102
+ // MODEL-05/06: print-only drift fragment; advisory, whole-line-commented additions, tickmarkr NEVER applies it.
103
103
  // TTY gets a one-line summary + the fragment as a file (the full dump drowned everything else,
104
104
  // v1.33.1 onboarding); machine/CI surface keeps the inline dump — layout is pinned by tests.
105
105
  const frag = suggestOverlay(cfg, health, adapters, stateDirName(cwd));
106
106
  let drift = "";
107
107
  if (frag) {
108
108
  if (visual()) {
109
- const overlayPath = join(drovrDir(cwd), "doctor-overlay.yaml");
109
+ const overlayPath = join(tickmarkrDir(cwd), "doctor-overlay.yaml");
110
110
  writeFileSync(overlayPath, frag);
111
111
  drift = `\nmodel drift: unclassified models detected — paste-ready overlay written to ${overlayPath} (advisory; tickmarkr never applies it)`;
112
112
  }
@@ -4,8 +4,8 @@ import { createInterface } from "node:readline/promises";
4
4
  import { parseArgs } from "node:util";
5
5
  import { allAdapters, formatDoctorAgeForInit, formatDoctorReport, initDoctorReuse } from "../../adapters/registry.js";
6
6
  import { configTemplate, DEFAULT_CONFIG, globalConfigDir, loadConfig } from "../../config/config.js";
7
- import { specTemplate } from "../../compile/native.js";
8
- import { drovrDir } from "../../graph/graph.js";
7
+ import { LEGACY_PREFIX, specTemplate } from "../../compile/native.js";
8
+ import { tickmarkrDir } from "../../graph/graph.js";
9
9
  import { doctor } from "./doctor.js";
10
10
  const AGENT_SKILLS = ["tickmarkr-loop", "tickmarkr-auto"];
11
11
  const DOCS_BEGIN = "<!-- tickmarkr:agent-docs begin -->";
@@ -13,18 +13,51 @@ const DOCS_END = "<!-- tickmarkr:agent-docs end -->";
13
13
  const AGENT_DOCS = `${DOCS_BEGIN}
14
14
  ## tickmarkr
15
15
 
16
- tickmarkr turns repository specs into isolated, independently verified agent work. Use \`/tickmarkr-loop\` for one spec or \`/tickmarkr-auto\` for several.
16
+ tickmarkr compiles repository specs into isolated, independently verified agent work.
17
17
 
18
- Loop: \`tickmarkr compile <spec>\` → \`tickmarkr plan\` → \`tickmarkr run\` → \`tickmarkr report <runId> --md\`.
18
+ ### Invariants
19
19
 
20
20
  - Never run two tickmarkr runs in the same repository concurrently.
21
- - Never merge tickmarkr work directly to main; new runs consolidate on \`tickmarkr/<runId>\`.
22
- - Fix source specs instead of editing generated graphs.
23
- - Never trust a worker's completion claim; gates independently verify the work.
24
- - Treat missing or unparseable results as failures.
21
+ - Never let tickmarkr merge work to main; new runs consolidate on \`tickmarkr/<runId>\`.
22
+ - Do not edit compiled graphs to force outcomes; fix source specs and recompile.
23
+ - Gates verify commits, diffs, acceptance criteria, and reviews independently — never trust a worker's completion claim.
24
+ - Treat missing or unparseable machine results and verdicts as failures.
25
+
26
+ ### Commands
27
+
28
+ - \`tickmarkr compile <spec>\` — spec → RunGraph
29
+ - \`tickmarkr plan\` — routing table and human gates
30
+ - \`tickmarkr run\` — execute the graph
31
+ - \`tickmarkr status <runId>\` — run progress
32
+ - \`tickmarkr resume <runId>\` — continue a paused or failed run
33
+ - \`tickmarkr approve <runId> <taskId>\` — release a human gate
34
+ - \`tickmarkr report <runId> --md\` — execution record beside the spec
35
+
36
+ Loop: compile → plan → run → report. Watch the journal for run-end rather than polling workers.
37
+
38
+ ### Role check (multi-agent environments)
39
+
40
+ - **Orchestrator:** run the loop in your session; do not start a second run.
41
+ - **Supervisor with a live orchestrator:** relay the mission via verified handoff (below), then supervise — do not duplicate the loop.
42
+ - **Primary session without an orchestrator:** spawn one child orchestration session, give it the mission and these rules, then supervise.
43
+
44
+ Outside multi-agent environments, run the loop directly.
45
+
46
+ ### Version preflight
47
+
48
+ Before \`tickmarkr compile\` or \`tickmarkr run\`: run \`tickmarkr version\`, read \`package.json\` version, and if the binary is older on major.minor, stop and tell the operator to update. Never proceed on hope — stale binaries silently skip daemon gates.
49
+
50
+ ### Tip-verify-before-green
51
+
52
+ A run is green only when the run-end event exists in the journal AND tip verify is not "failed". Never report green to the operator, tab titles, or records until both hold.
53
+
54
+ ### Verified handoffs
55
+
56
+ When relaying missions between agents, never use bare send-text (\`herdr agent send\` / pane send-text) — it omits Enter. Use \`herdr pane run <pane> "<message>"\` or \`herdr notification show "<message>"\`. Confirm delivery by reading the target pane afterward; never report "relayed" without read-back.
25
57
  ${DOCS_END}
26
58
  `;
27
- const skillPath = (cwd, skill) => join(cwd, ".claude", "skills", skill, "SKILL.md");
59
+ const skillsRoot = (cwd) => existsSync(join(cwd, ".claude", "skills")) ? join(cwd, ".claude", "skills") : join(cwd, ".agents", "skills");
60
+ const skillPath = (cwd, skill) => join(skillsRoot(cwd), skill, "SKILL.md");
28
61
  const skillsInstalled = (cwd) => AGENT_SKILLS.every((s) => existsSync(skillPath(cwd, s)));
29
62
  const wizardDriverDefault = () => process.env.HERDR_ENV === "1" ? "herdr" : "auto";
30
63
  async function installAgentFiles(cwd, force, docs, notes) {
@@ -44,7 +77,7 @@ async function installAgentFiles(cwd, force, docs, notes) {
44
77
  notes.push(`skipped existing ${dest}; pass --force to overwrite it`);
45
78
  continue;
46
79
  }
47
- mkdirSync(join(cwd, ".claude", "skills", skill), { recursive: true });
80
+ mkdirSync(join(skillsRoot(cwd), skill), { recursive: true });
48
81
  writeFileSync(dest, readFileSync(new URL(`../../../skills/${skill}/SKILL.md`, import.meta.url)), exists ? undefined : { flag: "wx" });
49
82
  notes.push(`${exists ? "overwrote" : "wrote"} ${dest}`);
50
83
  }
@@ -52,7 +85,8 @@ async function installAgentFiles(cwd, force, docs, notes) {
52
85
  const agents = join(cwd, "AGENTS.md");
53
86
  const docPath = existsSync(claude) || !existsSync(agents) ? claude : agents;
54
87
  const current = existsSync(docPath) ? readFileSync(docPath, "utf8") : "";
55
- if (current.includes(DOCS_BEGIN) || current.includes(DOCS_END)) {
88
+ if (current.includes(DOCS_BEGIN) || current.includes(`<!-- ${LEGACY_PREFIX}:agent-docs begin -->`)
89
+ || current.includes(DOCS_END) || current.includes(`<!-- ${LEGACY_PREFIX}:agent-docs end -->`)) {
56
90
  notes.push(`kept existing tickmarkr agent docs in ${docPath}`);
57
91
  }
58
92
  else if (docs || await confirm(`Append tickmarkr agent docs to ${docPath}?`)) {
@@ -94,8 +128,8 @@ async function runInitWizard(cwd) {
94
128
  const llm = llmRaw === "pane" || llmRaw === "headless" ? llmRaw : llmDef;
95
129
  let installSkills = false;
96
130
  if (!skillsInstalled(cwd)) {
97
- const skillsDef = existsSync(join(cwd, ".claude")) && !skillsInstalled(cwd);
98
- installSkills = await askYesNo(rl, "Install agent skills (tickmarkr-loop/tickmarkr-auto) into .claude/skills?", skillsDef);
131
+ const skillsDef = existsSync(join(cwd, ".claude", "skills")) && !skillsInstalled(cwd);
132
+ installSkills = await askYesNo(rl, "Install agent skills (tickmarkr-loop/tickmarkr-auto)?", skillsDef);
99
133
  }
100
134
  return { overlay: { driver, concurrency, visibility: { llm } }, installSkills };
101
135
  }
@@ -126,7 +160,7 @@ export async function init(argv, cwd = process.cwd()) {
126
160
  else {
127
161
  notes.push(`kept existing ${globalPath}`);
128
162
  }
129
- const repoConfigPath = join(drovrDir(cwd), "config.yaml");
163
+ const repoConfigPath = join(tickmarkrDir(cwd), "config.yaml");
130
164
  const repoConfigExists = existsSync(repoConfigPath);
131
165
  if (repoConfigExists)
132
166
  notes.push(`kept existing ${repoConfigPath}`);
@@ -134,12 +168,16 @@ export async function init(argv, cwd = process.cwd()) {
134
168
  if (existsSync(specPath)) {
135
169
  notes.push(`kept existing ${specPath}`);
136
170
  }
137
- else if (existsSync(join(cwd, "drovr.spec.md"))) {
138
- notes.push(`kept existing ${join(cwd, "drovr.spec.md")}`);
139
- }
140
171
  else {
141
- writeFileSync(specPath, specTemplate());
142
- notes.push(`wrote ${specPath}`);
172
+ const legacySpec = join(cwd, `${LEGACY_PREFIX}.spec.md`);
173
+ if (existsSync(legacySpec)) {
174
+ writeFileSync(specPath, readFileSync(legacySpec, "utf8"));
175
+ notes.push(`wrote ${specPath}`);
176
+ }
177
+ else {
178
+ writeFileSync(specPath, specTemplate());
179
+ notes.push(`wrote ${specPath}`);
180
+ }
143
181
  }
144
182
  const fresh = values.fresh ?? false;
145
183
  const { reuse, ageMs, health } = initDoctorReuse(cwd, fresh);
@@ -5,7 +5,15 @@ import { loadConfig } from "../../config/config.js";
5
5
  import { loadGraph } from "../../graph/graph.js";
6
6
  import { excludedChannels, exclusionLine } from "../../route/preference.js";
7
7
  import { route, RoutingError } from "../../route/router.js";
8
+ import { modelId } from "../../gates/review.js";
8
9
  import { loadRoutingProfile } from "../../run/journal.js";
10
+ const fleetCanCrossVendorReview = (channels) => {
11
+ for (let i = 0; i < channels.length; i++)
12
+ for (let j = 0; j < channels.length; j++)
13
+ if (i !== j && channels[i].vendor !== channels[j].vendor && modelId(channels[i].model) !== modelId(channels[j].model))
14
+ return true;
15
+ return false;
16
+ };
9
17
  export async function plan(_argv, cwd = process.cwd(), adapters = allAdapters()) {
10
18
  // ponytail: hardcoded 24h TTL — promote to config when an operator asks. mtime is the signal because
11
19
  // doctor.json has no probe timestamp and a schema field would break the existing-files compat invariant.
@@ -59,6 +67,9 @@ export async function plan(_argv, cwd = process.cwd(), adapters = allAdapters())
59
67
  lints.push(`${role}: ${sel.adapter}:${sel.model} not installed — that gate/consult will fail closed`);
60
68
  }
61
69
  lints.push(...modelLints(cfg, health, adapters, { tty: ttyVisual() })); // health may be pre-v1.5/probeAll-fallback — no-detection branch covers both
70
+ if (cfg.review.required && channels.length && !fleetCanCrossVendorReview(channels)) {
71
+ lints.push("review: no cross-vendor reviewer pair in fleet — set review.required: false to waive");
72
+ }
62
73
  let cost = 0;
63
74
  for (const t of g.tasks) {
64
75
  try {
@@ -69,7 +80,7 @@ export async function plan(_argv, cwd = process.cwd(), adapters = allAdapters())
69
80
  }
70
81
  const est = r.assignment.channel === "sub" ? 0 : cfg.pricing[r.assignment.tier];
71
82
  cost += est;
72
- lines.push(` ${t.id.padEnd(6)} ${t.shape.padEnd(10)} c${String(t.complexity).padEnd(3)}→ ${r.assignment.adapter}:${r.assignment.model} [${r.assignment.channel}/${r.assignment.tier}]${t.humanGate ? " (human gate)" : ""}${est ? ` ~$${est.toFixed(2)}` : ""} — ${r.provenance}`);
83
+ lines.push(` ${t.id.padEnd(6)} ${t.shape.padEnd(10)} c${String(t.complexity).padEnd(3)}→ ${r.assignment.adapter}:${r.assignment.model} [${r.assignment.channel}/${r.assignment.tier}]${t.timeoutMinutes !== undefined ? ` (timeout ${t.timeoutMinutes}m)` : ""}${t.humanGate ? " (human gate)" : ""}${est ? ` ~$${est.toFixed(2)}` : ""} — ${r.provenance}`);
73
84
  if (r.deviation) {
74
85
  deviations++;
75
86
  const d = r.deviation;
@@ -1,22 +1,22 @@
1
1
  import { writeFileSync } from "node:fs";
2
2
  import { join } from "node:path";
3
3
  import { loadConfig } from "../../config/config.js";
4
- import { drovrDir, stateDirName } from "../../graph/graph.js";
4
+ import { tickmarkrDir, stateDirName } from "../../graph/graph.js";
5
5
  import { learnedScore, MIN_SAMPLES, cellsOf } from "../../route/profile.js";
6
6
  import { Journal, loadRoutingProfile, readProfileCursor, RUNS_WINDOW } from "../../run/journal.js";
7
- // drovr profile — inspect (show) and forget (reset) the derived learned-routing profile (VIS-03).
7
+ // tickmarkr profile — inspect (show) and forget (reset) the derived learned-routing profile (VIS-03).
8
8
  // Read-only class like plan/status/report: no run lock. `reset` writes ONE cursor scalar, nothing else.
9
9
  export async function profile(argv, cwd = process.cwd()) {
10
10
  return argv[0] === "reset" ? reset(cwd) : show(cwd);
11
11
  }
12
- // Non-destructive reset (T-13-06): a one-line runId cutoff at .drovr/profile-since. NEVER deletes,
12
+ // Non-destructive reset (T-13-06): a one-line runId cutoff at .tickmarkr/profile-since. NEVER deletes,
13
13
  // truncates, or rotates any telemetry — the profile is derived (PROF-01), so there is nothing to delete;
14
- // the cursor only bounds loadRoutingProfile's telemetry window. drovrDir guarantees .drovr exists and
14
+ // the cursor only bounds loadRoutingProfile's telemetry window. tickmarkrDir guarantees .tickmarkr exists and
15
15
  // blanket-gitignores it, so the cursor is never git-addable. Opaque string: used only in a runId > compare.
16
16
  function reset(cwd) {
17
17
  const cursor = Journal.latestRunId(cwd) ?? ""; // empty repo ⇒ empty cursor ⇒ readProfileCursor === undefined
18
18
  const stateDir = stateDirName(cwd);
19
- const path = join(drovrDir(cwd), "profile-since");
19
+ const path = join(tickmarkrDir(cwd), "profile-since");
20
20
  writeFileSync(path, cursor + "\n");
21
21
  return [
22
22
  cursor
@@ -27,7 +27,7 @@ function reset(cwd) {
27
27
  ].join("\n");
28
28
  }
29
29
  // Inspection surface: preview:true bypasses the routing.learned:off short-circuit so `show` renders the
30
- // profile even under the default off — same trust ramp as `drovr plan`. The routing path stays inert.
30
+ // profile even under the default off — same trust ramp as `tickmarkr plan`. The routing path stays inert.
31
31
  function show(cwd) {
32
32
  const cfg = loadConfig(cwd);
33
33
  const cursor = readProfileCursor(cwd);
@@ -1 +1,4 @@
1
- export declare function resume(argv: string[], cwd?: string): Promise<string>;
1
+ export declare function resume(argv: string[], cwd?: string): Promise<{
2
+ out: string;
3
+ code: number;
4
+ }>;
@@ -2,10 +2,13 @@ import { loadConfig } from "../../config/config.js";
2
2
  import { pickDriver } from "../../drivers/index.js";
3
3
  import { formatSummary, runDaemon } from "../../run/daemon.js";
4
4
  import { formatJournalNarration } from "../../run/journal.js";
5
+ const summaryGreen = (s) => s.failed.length === 0 && s.human.length === 0 && s.blocked.length === 0 && s.pending.length === 0
6
+ && s.tipVerify !== "failed";
5
7
  export async function resume(argv, cwd = process.cwd()) {
6
8
  const runId = argv[0];
7
9
  if (!runId)
8
10
  throw new Error("usage: tickmarkr resume <run-id>");
9
11
  const s = await runDaemon(cwd, { runId, resume: true, driver: pickDriver(loadConfig(cwd)), narrate: (event) => console.log(formatJournalNarration(event)) });
10
- return `resumed ${s.runId} — ${formatSummary(s)}`;
12
+ const out = `resumed ${s.runId} — ${formatSummary(s)}`;
13
+ return { out, code: summaryGreen(s) ? 0 : 2 };
11
14
  }
@@ -1 +1,4 @@
1
- export declare function run(argv: string[], cwd?: string): Promise<string>;
1
+ export declare function run(argv: string[], cwd?: string): Promise<{
2
+ out: string;
3
+ code: number;
4
+ }>;
@@ -6,6 +6,8 @@ import { loadGraph } from "../../graph/graph.js";
6
6
  import { formatSummary, runDaemon } from "../../run/daemon.js";
7
7
  import { route } from "../../route/router.js";
8
8
  import { formatJournalNarration, loadRoutingProfile } from "../../run/journal.js";
9
+ const summaryGreen = (s) => s.failed.length === 0 && s.human.length === 0 && s.blocked.length === 0 && s.pending.length === 0
10
+ && s.tipVerify !== "failed";
9
11
  export async function run(argv, cwd = process.cwd()) {
10
12
  const { values } = parseArgs({
11
13
  args: argv,
@@ -15,6 +17,11 @@ export async function run(argv, cwd = process.cwd()) {
15
17
  "route-strict": { type: "boolean" },
16
18
  },
17
19
  });
20
+ if (values.concurrency !== undefined) {
21
+ const n = Number(values.concurrency);
22
+ if (!Number.isInteger(n) || n <= 0)
23
+ throw new Error(`--concurrency must be a positive integer (got ${values.concurrency})`);
24
+ }
18
25
  const cfg = loadConfig(cwd);
19
26
  if (values["route-strict"]) {
20
27
  const adapters = allAdapters();
@@ -31,5 +38,6 @@ export async function run(argv, cwd = process.cwd()) {
31
38
  driver: pickDriver(cfg, values.driver),
32
39
  narrate: (event) => console.log(formatJournalNarration(event)),
33
40
  });
34
- return `run ${s.runId} finished — ${formatSummary(s)} (merge to main is a human decision)`;
41
+ const out = `run ${s.runId} finished — ${formatSummary(s)} (merge to main is a human decision)`;
42
+ return { out, code: summaryGreen(s) ? 0 : 2 };
35
43
  }
@@ -1,3 +1,4 @@
1
+ import { BANNER } from "../../brand.js";
1
2
  import { blockedTasks, loadGraph, pendingTasks } from "../../graph/graph.js";
2
3
  import { GATE_NAMES } from "../../graph/schema.js";
3
4
  import { Journal } from "../../run/journal.js";
@@ -211,7 +212,7 @@ export async function status(argv, cwd = process.cwd(), opts = {}) {
211
212
  for (let i = 0; i < iterations; i++) {
212
213
  const frame = renderFrame(cwd);
213
214
  if (tty)
214
- process.stdout.write(`\x1b[2J\x1b[H${frame}\n\x1b[2m watching · refresh ${REFRESH_MS / 1000}s · ^C to quit\x1b[0m`);
215
+ process.stdout.write(`\x1b[2J\x1b[H${BANNER}${frame}\n\x1b[2m watching · refresh ${REFRESH_MS / 1000}s · ^C to quit\x1b[0m`);
215
216
  else
216
217
  process.stdout.write(frame + sep);
217
218
  if (bounded)
@@ -1,6 +1,6 @@
1
1
  import { unlockRun } from "../../run/lock.js";
2
2
  // LOCK-03: thin formatter over unlockRun. A live-holder refusal propagates as the throw — the
3
- // dispatcher prints `drovr unlock: …` and exits 1 (src/cli/index.ts).
3
+ // dispatcher prints `tickmarkr unlock: …` and exits 1 (src/cli/index.ts).
4
4
  export async function unlock(_argv, cwd = process.cwd()) {
5
5
  const r = unlockRun(cwd);
6
6
  if (!r.held)
@@ -0,0 +1 @@
1
+ export declare function version(_argv?: string[]): Promise<string>;
@@ -0,0 +1,8 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { dirname, join } from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ const pkgPath = join(dirname(fileURLToPath(import.meta.url)), "../../../package.json");
5
+ export async function version(_argv = []) {
6
+ const { version: v } = JSON.parse(readFileSync(pkgPath, "utf8"));
7
+ return v;
8
+ }
@@ -1,8 +1,11 @@
1
1
  #!/usr/bin/env node
2
- export type CommandMap = Record<string, (argv: string[]) => Promise<string>>;
2
+ export type CommandResult = string | {
3
+ out: string;
4
+ code: number;
5
+ };
6
+ export type CommandMap = Record<string, (argv: string[]) => Promise<CommandResult>>;
3
7
  export declare const COMMANDS: CommandMap;
4
- export declare const BANNER: string;
5
- 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\n compile <src> spec \u2192 .drovr/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 run execute the graph (--concurrency N --driver herdr|subprocess --route-strict)\n status live run state\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 unlock remove a stale/garbage run lock (refuses if the holder is alive)\n approve <id> <task> approve a parked human gate (--by <name> --reason <text>); takes effect on resume";
8
+ export declare const USAGE = "tickmarkr \u2014 spec-driven orchestration harness for AI coding agents\nusage: tickmarkr <command>\n init guided setup + doctor; init --agent [--force] [--docs] adds agent skills/docs\n doctor re-probe adapters, herdr, auth; print capability matrix\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 run execute the graph (--concurrency N --driver herdr|subprocess --route-strict)\n status live run state\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 unlock remove a stale/garbage run lock (refuses if the holder is alive)\n approve <id> <task> approve a parked human gate (--by <name> --reason <text>); takes effect on resume";
6
9
  export declare function dispatch(cmd: string | undefined, argv: string[], commands?: CommandMap): Promise<{
7
10
  out: string;
8
11
  code: number;
package/dist/cli/index.js CHANGED
@@ -13,26 +13,19 @@ import { run } from "./commands/run.js";
13
13
  import { scope } from "./commands/scope.js";
14
14
  import { status } from "./commands/status.js";
15
15
  import { unlock } from "./commands/unlock.js";
16
+ import { version } from "./commands/version.js";
17
+ const normalize = (r) => typeof r === "string" ? { out: r, code: 0 } : r;
16
18
  export const COMMANDS = {
17
- init, doctor, compile, scope, plan, run, status, resume, report, profile, unlock, approve,
19
+ init, doctor, compile, scope, plan, run, status, resume, report, profile, unlock, approve, version,
18
20
  };
21
+ const VERSION_FLAGS = new Set(["version", "--version", "-v"]);
19
22
  const HELP_CMDS = new Set(["help", "-h", "--help"]);
20
- // TTY-only pixel-tick logo (assets/mark.svg is the image twin — same block geometry).
21
- // Never printed to pipes — the non-TTY stdout surface is byte-pinned by tests and consumed by machines.
22
- const B = "\x1b[1m", R = "\x1b[0m";
23
- const g = (n, s) => `\x1b[38;5;${n}m${s}${R}`; // 256-color green ramp, bright → deep
24
- export const BANNER = [
25
- ` ${g(84, "▄▄████")}`,
26
- ` ${g(78, "▄▄████▀▀")}`,
27
- `${g(41, "████▄▄▄▄████▀▀")} ${B}tickmarkr${R}`,
28
- ` ${g(35, "▀▀████▀▀")} spec in, verified work out.`,
29
- "",
30
- ].join("\n");
23
+ import { BANNER } from "../brand.js";
31
24
  export const USAGE = `tickmarkr — spec-driven orchestration harness for AI coding agents
32
25
  usage: tickmarkr <command>
33
26
  init guided setup + doctor; init --agent [--force] [--docs] adds agent skills/docs
34
27
  doctor re-probe adapters, herdr, auth; print capability matrix
35
- compile <src> spec → .drovr/graph.json (fails without acceptance criteria)
28
+ compile <src> spec → .tickmarkr/graph.json (fails without acceptance criteria)
36
29
  scope <intent> draft a compiled native spec beside an answered intent (--force to overwrite)
37
30
  plan dry-run routing table + cost estimate + floor lints
38
31
  run execute the graph (--concurrency N --driver herdr|subprocess --route-strict)
@@ -43,9 +36,11 @@ usage: tickmarkr <command>
43
36
  unlock remove a stale/garbage run lock (refuses if the holder is alive)
44
37
  approve <id> <task> approve a parked human gate (--by <name> --reason <text>); takes effect on resume`;
45
38
  // pure, testable dispatcher: resolves a command, forwards argv, shapes the result — no side effects.
46
- // unknown/missing cmd → USAGE (exit 1 if a cmd was typed, 0 for bare `drovr`); a handler throw becomes
47
- // a one-line `drovr <cmd>: <message>` (never a raw stack) at exit 1.
39
+ // unknown/missing cmd → USAGE (exit 1 if a cmd was typed, 0 for bare `tickmarkr`); a handler throw becomes
40
+ // a one-line `tickmarkr <cmd>: <message>` (never a raw stack) at exit 1.
48
41
  export async function dispatch(cmd, argv, commands = COMMANDS) {
42
+ if (cmd && VERSION_FLAGS.has(cmd))
43
+ return { out: await version(argv), code: 0 };
49
44
  const usage = process.stdout.isTTY ? BANNER + USAGE : USAGE;
50
45
  if (!cmd || HELP_CMDS.has(cmd))
51
46
  return { out: usage, code: 0 };
@@ -53,7 +48,7 @@ export async function dispatch(cmd, argv, commands = COMMANDS) {
53
48
  if (!fn)
54
49
  return { out: usage, code: 1 };
55
50
  try {
56
- return { out: await fn(argv), code: 0 };
51
+ return normalize(await fn(argv));
57
52
  }
58
53
  catch (err) {
59
54
  return { out: `tickmarkr ${cmd}: ${err.message}`, code: 1 };
@@ -61,7 +56,7 @@ export async function dispatch(cmd, argv, commands = COMMANDS) {
61
56
  }
62
57
  /* v8 ignore start -- binary entry: printing + process.exit side effects, not unit-testable (ROADMAP crit 2) */
63
58
  // node realpaths the main module (import.meta.url) but argv[1] keeps the symlink path —
64
- // a globally-linked `drovr` bin silently no-oped here (OBS-10); compare realpaths
59
+ // a globally-linked `tickmarkr` bin silently no-oped here (OBS-10); compare realpaths
65
60
  const argv1Real = (() => { try {
66
61
  return process.argv[1] ? realpathSync(process.argv[1]) : "";
67
62
  }
@@ -72,7 +67,7 @@ if (argv1Real && import.meta.url === pathToFileURL(argv1Real).href) {
72
67
  const [cmd, ...argv] = process.argv.slice(2);
73
68
  dispatch(cmd, argv).then(({ out, code }) => {
74
69
  // byte-identical streams: usage + success → stdout; a handler throw → stderr (original behavior)
75
- (out.endsWith(USAGE) || code === 0 ? console.log : console.error)(out);
70
+ (code === 1 && !out.endsWith(USAGE) ? console.error : console.log)(out);
76
71
  if (code !== 0)
77
72
  process.exit(code);
78
73
  });
@@ -3,7 +3,7 @@ import { basename, dirname, isAbsolute, join, relative } from "node:path";
3
3
  import { parse as parseYaml } from "yaml";
4
4
  import { TIERS, validateGraph } from "../graph/schema.js";
5
5
  import { CompileError, assertWriteScope, inferShape, sha256 } from "./common.js";
6
- // GSD artifact front-end (spec v1.3): one GSD *plan* is one drovr *task* — a plan is
6
+ // GSD artifact front-end (spec v1.3): one GSD *plan* is one tickmarkr *task* — a plan is
7
7
  // worktree-sized; its inner <task> steps stay in the worker prompt via context[0] = the plan file.
8
8
  // Artifact-level only: parses .planning/ markdown, never GSD repo/command internals.
9
9
  const PLAN_SUFFIX = "-PLAN.md";
@@ -2,7 +2,7 @@ import { existsSync, readFileSync, statSync } from "node:fs";
2
2
  import { join } from "node:path";
3
3
  import { CompileError } from "./common.js";
4
4
  import { compileGsd, isGsdPhaseDir } from "./gsd.js";
5
- import { compileNative, NATIVE_MARKER } from "./native.js";
5
+ import { compileNative, TICKMARKR_NATIVE_MARKER } from "./native.js";
6
6
  import { compilePrd } from "./prd.js";
7
7
  import { compileSpecKit } from "./speckit.js";
8
8
  function detect(src) {
@@ -15,8 +15,14 @@ function detect(src) {
15
15
  }
16
16
  if (src.endsWith("-PLAN.md"))
17
17
  return "gsd"; // before the generic .md → prd rule
18
- if (src.endsWith(".md"))
19
- return existsSync(src) && NATIVE_MARKER.test(readFileSync(src, "utf8")) ? "native" : "prd";
18
+ if (src.endsWith(".md")) {
19
+ if (!existsSync(src))
20
+ return "prd";
21
+ const content = readFileSync(src, "utf8");
22
+ if (TICKMARKR_NATIVE_MARKER.test(content))
23
+ return "native";
24
+ return "prd";
25
+ }
20
26
  return null;
21
27
  }
22
28
  export function compileSource(src, type, root) {
@@ -1,4 +1,6 @@
1
1
  import { type RunGraph } from "../graph/schema.js";
2
+ export declare const LEGACY_PREFIX: string;
3
+ export declare const TICKMARKR_NATIVE_MARKER: RegExp;
2
4
  export declare const NATIVE_MARKER: RegExp;
3
5
  export declare function compileNative(file: string): RunGraph;
4
6
  export declare function specTemplate(): string;
@@ -1,14 +1,16 @@
1
1
  import { existsSync, readFileSync } from "node:fs";
2
2
  import { GATE_NAMES, ORACLES, SHAPES, TIERS, validateGraph } from "../graph/schema.js";
3
3
  import { CompileError, inferShape, sha256 } from "./common.js";
4
- export const NATIVE_MARKER = /^<!--\s*(?:tickmarkr|drovr):spec(?:\s+v1)?\s*-->\s*$/m;
4
+ export const LEGACY_PREFIX = ["dro", "vr"].join("");
5
+ export const TICKMARKR_NATIVE_MARKER = /^<!--\s*tickmarkr:spec(?:\s+v1)?\s*-->\s*$/m;
6
+ export const NATIVE_MARKER = new RegExp(`^<!--\\s*(?:tickmarkr|${LEGACY_PREFIX}):spec(?:\\s+v1)?\\s*-->\\s*$`, "m");
5
7
  const HEAD_RE = /^## (T\d+):\s*(.+)$/;
6
8
  const FIELD_RE = /^- (\w+):\s*(.*)$/;
7
9
  const NESTED_RE = /^\s+- (.+)$/;
8
10
  // v1.19: a typed acceptance oracle line — "command: ...", "test: ...", or "judge: ...". Anything
9
11
  // without one of these prefixes is a plain-string judge criterion (compat path, emits a warning).
10
12
  const ORACLE_RE = new RegExp(`^(${ORACLES.join("|")}):\\s*(.*)$`);
11
- const FIELDS = new Set(["goal", "shape", "deps", "files", "context", "complexity", "humangate", "pin", "floor", "gates", "acceptance"]);
13
+ const FIELDS = new Set(["goal", "shape", "deps", "files", "context", "complexity", "humangate", "pin", "floor", "gates", "acceptance", "timeout"]);
12
14
  function invalid(task, field, detail) {
13
15
  throw new CompileError(`Task ${task} field "${field}" ${detail}`);
14
16
  }
@@ -108,6 +110,10 @@ export function compileNative(file) {
108
110
  const badGate = draft.gates.find((gate) => !GATE_NAMES.includes(gate));
109
111
  if (badGate)
110
112
  invalid(draft.id, "gates", `contains invalid gate "${badGate}"`);
113
+ const timeoutMinutes = fields.timeout === undefined ? undefined : Number(fields.timeout);
114
+ if (fields.timeout !== undefined && (!Number.isFinite(timeoutMinutes) || timeoutMinutes <= 0)) {
115
+ invalid(draft.id, "timeout", "must be a positive number");
116
+ }
111
117
  const routingHints = pin || fields.floor ? {
112
118
  ...(pin ? { pin: { via: pin[0], model: pin[1] } } : {}),
113
119
  ...(fields.floor ? { floor: fields.floor } : {}),
@@ -123,6 +129,7 @@ export function compileNative(file) {
123
129
  context: csv(fields.context),
124
130
  acceptance: draft.acceptance,
125
131
  ...(fields.humangate === "true" ? { humanGate: true } : {}),
132
+ ...(timeoutMinutes !== undefined ? { timeoutMinutes } : {}),
126
133
  ...(routingHints ? { routingHints } : {}),
127
134
  ...(draft.hasGates ? { gates: draft.gates } : {}),
128
135
  };
@@ -139,7 +146,7 @@ export function compileNative(file) {
139
146
  }
140
147
  return result;
141
148
  }
142
- // Commented native spec written to drovr.spec.md by `drovr init`. Documented via HTML comments (which the
149
+ // Commented native spec written to tickmarkr.spec.md by `tickmarkr init`. Documented via HTML comments (which the
143
150
  // parser ignores) so the template itself round-trips through compileSource() unchanged. Every field is
144
151
  // documented; the two example tasks illustrate plain vs deps+routing-hint, each with acceptance[].
145
152
  export function specTemplate() {
@@ -164,7 +171,8 @@ acceptance is required on every task (a nested list of observable outcomes).
164
171
  humanGate: true | false — pauses for a human review before merging this task
165
172
  pin: "via model" — pin an exact channel, e.g. "claude-code opus"
166
173
  floor: cheap | mid | frontier — minimum capability tier for routing
167
- gates: nested list, any of build | test | lint | evidence | scope | acceptance | review
174
+ gates: nested list; build | test | lint | evidence | scope are mandatory;
175
+ acceptance | review may be omitted
168
176
  acceptance: nested list (REQUIRED, non-empty). Each item is either a typed oracle or plain text:
169
177
  - command: <shell> (oracle: command — exit code)
170
178
  - test: <name> (oracle: test — named test)
@@ -6,6 +6,7 @@ declare const TierEnum: z.ZodEnum<{
6
6
  }>;
7
7
  export type Tier = z.infer<typeof TierEnum>;
8
8
  export declare const TIER_RANK: Record<Tier, number>;
9
+ export declare const DEFAULT_DIFF_CAP = 60000;
9
10
  export declare const MapEntrySchema: z.ZodObject<{
10
11
  pin: z.ZodOptional<z.ZodObject<{
11
12
  via: z.ZodString;
@@ -46,7 +47,7 @@ export declare const SubPricingSchema: z.ZodObject<{
46
47
  windowsPerMonthHigh: z.ZodNumber;
47
48
  }, z.core.$strip>;
48
49
  export type SubPricing = z.infer<typeof SubPricingSchema>;
49
- export declare const DrovrConfigSchema: z.ZodObject<{
50
+ export declare const TickmarkrConfigSchema: z.ZodObject<{
50
51
  concurrency: z.ZodNumber;
51
52
  driver: z.ZodEnum<{
52
53
  auto: "auto";
@@ -84,6 +85,7 @@ export declare const DrovrConfigSchema: z.ZodObject<{
84
85
  halfLifeRuns: z.ZodOptional<z.ZodNumber>;
85
86
  availWeight: z.ZodOptional<z.ZodNumber>;
86
87
  }, z.core.$strip>>;
88
+ allowUnverifiedModels: z.ZodBoolean;
87
89
  allow: z.ZodOptional<z.ZodObject<{
88
90
  adapters: z.ZodOptional<z.ZodArray<z.ZodString>>;
89
91
  models: z.ZodOptional<z.ZodArray<z.ZodString>>;
@@ -123,6 +125,7 @@ export declare const DrovrConfigSchema: z.ZodObject<{
123
125
  build: z.ZodOptional<z.ZodString>;
124
126
  test: z.ZodOptional<z.ZodString>;
125
127
  lint: z.ZodOptional<z.ZodString>;
128
+ diffCap: z.ZodOptional<z.ZodNumber>;
126
129
  byShape: z.ZodOptional<z.ZodOptional<z.ZodRecord<z.ZodEnum<{
127
130
  plan: "plan";
128
131
  spec: "spec";
@@ -171,23 +174,23 @@ export declare const DrovrConfigSchema: z.ZodObject<{
171
174
  workersPerTab: z.ZodNumber;
172
175
  }, z.core.$strip>;
173
176
  }, z.core.$strip>;
174
- export type DrovrConfig = z.infer<typeof DrovrConfigSchema>;
177
+ export type TickmarkrConfig = z.infer<typeof TickmarkrConfigSchema>;
175
178
  export declare class ConfigError extends Error {
176
179
  constructor(message: string);
177
180
  }
178
- export declare const DEFAULT_CONFIG: DrovrConfig;
181
+ export declare const DEFAULT_CONFIG: TickmarkrConfig;
179
182
  export declare function globalConfigDir(): string;
180
183
  export declare function overlayPreferShapes(repoRoot: string, opts?: {
181
184
  globalDir?: string;
182
185
  }): ReadonlySet<string>;
183
186
  export declare function loadConfig(repoRoot: string, opts?: {
184
187
  globalDir?: string;
185
- }): DrovrConfig;
188
+ }): TickmarkrConfig;
186
189
  export type InitConfigOverlay = {
187
190
  concurrency?: number;
188
- driver?: DrovrConfig["driver"];
191
+ driver?: TickmarkrConfig["driver"];
189
192
  visibility?: {
190
- llm?: DrovrConfig["visibility"]["llm"];
193
+ llm?: TickmarkrConfig["visibility"]["llm"];
191
194
  };
192
195
  };
193
196
  export declare function configTemplate(overlay?: InitConfigOverlay): string;