tickmarkr 1.30.0 → 1.33.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.
package/README.md CHANGED
@@ -13,8 +13,7 @@ Green tasks consolidate onto a `tickmarkr/<runId>` **consolidation** branch; **s
13
13
  your mainline) is always the Partner's call, never the engine's. A fully green engagement is an
14
14
  **unqualified opinion** — every assertion independently ticked.
15
15
 
16
- > The package ships `tickmarkr` as the primary bin, plus `drovr` and a `drover` bin alias for
17
- > backward compatibility — all three invoke the same entry. New installs should call `tickmarkr`.
16
+ > The package ships `tickmarkr` and `tkr` only; `tkr` is the short alias.
18
17
 
19
18
  ## Vocabulary
20
19
 
@@ -53,24 +52,27 @@ These are law; the codebase fails closed around them:
53
52
  - worker/judge/review/consult prompts end with machine-parseable trailers
54
53
  (`DROVR_RESULT` / JSON verdicts); parse defensively, fail closed
55
54
 
56
- ## Install
55
+ Any agent (or human) operating this repo must respect the same five rules above — they are not
56
+ merely internal implementation details, they are the contract the gates enforce.
57
57
 
58
- Requirements: Node ≥ 20, git, and at least one agent CLI on PATH (`claude`, `codex`, `cursor-agent`,
59
- `opencode`, `grok`, or `pi`) authenticated through its own login.
58
+ ## Install
60
59
 
61
60
  ```bash
62
- npm install
63
- npm run build
64
- npm link # exposes the `tickmarkr` bin (and the `drovr` / `drover` compat alias)
61
+ npm i -g tickmarkr
65
62
  ```
66
63
 
64
+ This installs two identical bins: `tickmarkr` and its short alias `tkr` — use whichever you
65
+ prefer, every example below works with either. Requirements: Node ≥ 20, git, and at least one
66
+ agent CLI on PATH (`claude`, `codex`, `cursor-agent`, `opencode`, `grok`, or `pi`) authenticated
67
+ through its own login.
68
+
67
69
  Then verify your fleet:
68
70
 
69
71
  ```bash
70
72
  tickmarkr doctor # probes installed adapters, herdr, auth; prints the capability matrix
71
73
  ```
72
74
 
73
- ## Quickstart
75
+ ## Quickstart (5 minutes)
74
76
 
75
77
  ```bash
76
78
  tickmarkr init # guided setup + doctor; scaffolds .drovr/config.yaml and drovr.spec.md
@@ -81,6 +83,34 @@ tickmarkr run # execute the graph (--concurrency N --driver her
81
83
  tickmarkr report <runId> --md # engagement record in Markdown
82
84
  ```
83
85
 
86
+ That's the whole loop: `init` scaffolds, you write the spec's tasks and `acceptance[]`, `compile`
87
+ turns it into a task graph, `plan` shows you the routing/cost dry run, `run` dispatches field teams
88
+ and gates every result, and `report` writes the engagement record. Nothing merges to your mainline
89
+ without your sign-off.
90
+
91
+ ## Agent-ready repos: `tickmarkr init --agent`
92
+
93
+ `tickmarkr init --agent` composes with the base `init` above and additionally installs the
94
+ consumer-facing skills (`tickmarkr-loop`, `tickmarkr-auto`) that ship in the npm tarball, so a
95
+ coding agent working in your repo knows how to drive tickmarkr without reading its source:
96
+
97
+ ```bash
98
+ tickmarkr init --agent # installs .claude/skills/tickmarkr-{loop,auto}, offers CLAUDE.md/AGENTS.md notes
99
+ tickmarkr init --agent --force # also overwrite skill files that already exist
100
+ tickmarkr init --agent --docs # also append the agent-docs section without an interactive prompt
101
+ ```
102
+
103
+ Consent rules — every write is additive, never destructive:
104
+
105
+ - an existing `.claude/skills/tickmarkr-{loop,auto}/SKILL.md` is left untouched unless you answer
106
+ yes to a per-file prompt, or pass `--force`
107
+ - a short "tickmarkr" section (the loop commands + the invariants above) is appended to `CLAUDE.md`
108
+ — or `AGENTS.md` if that exists and `CLAUDE.md` doesn't — only after you say yes, or pass `--docs`;
109
+ it's wrapped in `<!-- tickmarkr:agent-docs begin/end -->` markers and never inserted twice
110
+ - declining a prompt still lets the rest of `init` complete
111
+ - non-interactive shells (no TTY) never prompt: missing skills are installed, everything else is
112
+ skipped, and the summary names exactly what was skipped and which flag would enable it
113
+
84
114
  Supervise and finish:
85
115
 
86
116
  ```bash
@@ -332,14 +362,65 @@ Cloning this repo gives Claude Code project skills under `.claude/skills/`:
332
362
 
333
363
  Repo-scoped: available automatically in Claude Code sessions inside this repo; not installed by npm.
334
364
 
335
- ## Development
365
+ ## Contributing
366
+
367
+ ### Dev setup
336
368
 
337
369
  ```bash
338
- npm test # vitest unit+integration — zero tokens, fake adapter only
339
- npm run test:coverage # coverage floors enforced on core dirs
340
- npm run e2e # real-CLI end-to-end (spends tokens; needs ≥1 agent CLI)
370
+ git clone <your fork>
371
+ cd tickmarkr
372
+ npm install
373
+ npm run build # tsc → dist/; the only enforced static check
374
+ npm link # exposes the tickmarkr/tkr bins from your checkout
341
375
  ```
342
376
 
377
+ ### Tests are zero-token by design
378
+
379
+ ```bash
380
+ npm test # vitest unit+integration — fake adapter only, spends no tokens, ever
381
+ npm run test:coverage # same suite, coverage floors enforced (see below)
382
+ npm run e2e # opt-in real-CLI end-to-end — DOES spend tokens, needs ≥1 agent CLI installed
383
+ ```
384
+
385
+ `npm test` and `npm run test:coverage` must never call a real agent CLI or spend tokens — that's
386
+ what `DROVR_FAKE_SCRIPT` and the `FakeAdapter` (`src/adapters/fake.ts`) exist for. If a change
387
+ needs a real CLI to verify, that test belongs under `npm run e2e` (`tests/e2e/`), gated behind
388
+ `DROVR_E2E=1`, never in the default suite.
389
+
390
+ Coverage floor: `src/{graph,route,gates,run}/**` must stay at lines 80% / functions 80% /
391
+ branches 70% — this is the CLAUDE.md invariant, enforced verbatim in `vitest.config.ts`.
392
+ `src/compile/**`, `src/adapters/**`, and `src/drivers/**` carry their own (higher) measured
393
+ thresholds in the same file.
394
+
395
+ ### The spec-driven workflow — the gates are the review
396
+
397
+ Changes to tickmarkr itself land the same way tickmarkr expects consumer changes to land: write a
398
+ native spec (`specs/<version>-<name>.spec.md`, marked `<!-- tickmarkr:spec -->`), give every task
399
+ real `acceptance[]` criteria, then compile/plan/run it and let the gates (baseline, evidence, scope,
400
+ acceptance-judge, cross-vendor review) independently verify the diff before it merges. PRs are
401
+ welcome, but a PR that only "looks right" isn't the bar — a green, gated engagement is. If you're
402
+ proposing a non-trivial change, sketch it as a spec first; small fixes and docs can go straight to
403
+ a PR.
404
+
405
+ ### Adding a worker adapter
406
+
407
+ Support for a new agent CLI is one new file implementing `WorkerAdapter`
408
+ (`src/adapters/types.ts`) — `id`, `vendor`, `probe()`, `channels()`, `headlessCommand()`,
409
+ `interactiveCommand()`, `invoke()`, `parse()`, plus the optional `listModels`/`resumeCommand`
410
+ capabilities other adapters use when the CLI supports them. Use an existing adapter as the
411
+ template (`src/adapters/codex.ts` is a good starting shape), then register it with one line in
412
+ `src/adapters/registry.ts`'s `allAdapters()` — append it, don't insert it in the middle, since
413
+ same-tier routing ties resolve by discovery order. No other file needs to know a new adapter
414
+ exists.
415
+
416
+ ### Filing defects
417
+
418
+ Abnormalities found during a real engagement (not routine bugs — surprising or systemic
419
+ behavior) are logged as a numbered entry (`OBS-NN`) in `.planning/OBSERVATIONS.md`, with a status
420
+ (`OPEN` / `CLOSED-FIXED` / `CLOSED-MITIGATED`) and, once fixed, the spec/commit that closed it.
421
+ That file is the standing abnormality ledger for this repo — read it before assuming a rough edge
422
+ is new.
423
+
343
424
  Design spec: `docs/superpowers/specs/2026-07-07-drover-design.md`.
344
425
 
345
426
  ## License
@@ -68,7 +68,7 @@ export async function doctor(_argv, cwd = process.cwd(), adapters = allAdapters(
68
68
  rows.push(` ! ${servabilityLine(servable)}`);
69
69
  // MODEL-05/06: print-only drift fragment; advisory, whole-line-commented additions, drovr NEVER applies it.
70
70
  const frag = suggestOverlay(cfg, health, adapters);
71
- const drift = frag ? `\nmodel drift — paste-ready overlay (advisory; drovr never applies):\n${frag}` : "";
71
+ const drift = frag ? `\nmodel drift — paste-ready overlay (advisory; tickmarkr never applies):\n${frag}` : "";
72
72
  // T4: model-status table — one row per CLASSIFIED model (tiers config) with tier, auth verdict
73
73
  // (reason + date when unauthed), operator-deny flag, and prefer rank across the routing map.
74
74
  // Unclassified listed models (detected via listModels but never tiered) compress to one count line
@@ -99,5 +99,5 @@ export async function doctor(_argv, cwd = process.cwd(), adapters = allAdapters(
99
99
  return rows;
100
100
  });
101
101
  const modelSummary = modelStatus.length ? `\nmodel status:\n${modelStatus.join("\n")}` : "";
102
- return `drovr doctor — capability matrix:\n${rows.join("\n")}${modelSummary}${drift}\nwrote .drovr/doctor.json`;
102
+ return `tickmarkr doctor — capability matrix:\n${rows.join("\n")}${modelSummary}${drift}\nwrote .drovr/doctor.json`;
103
103
  }
@@ -1,12 +1,78 @@
1
- import { existsSync, mkdirSync, writeFileSync } from "node:fs";
1
+ import { appendFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
2
  import { join } from "node:path";
3
+ import { createInterface } from "node:readline/promises";
3
4
  import { parseArgs } from "node:util";
4
5
  import { configTemplate, globalConfigDir } from "../../config/config.js";
5
6
  import { specTemplate } from "../../compile/native.js";
6
7
  import { drovrDir } from "../../graph/graph.js";
7
8
  import { doctor } from "./doctor.js";
9
+ const AGENT_SKILLS = ["tickmarkr-loop", "tickmarkr-auto"];
10
+ const DOCS_BEGIN = "<!-- tickmarkr:agent-docs begin -->";
11
+ const DOCS_END = "<!-- tickmarkr:agent-docs end -->";
12
+ const AGENT_DOCS = `${DOCS_BEGIN}
13
+ ## tickmarkr
14
+
15
+ tickmarkr turns repository specs into isolated, independently verified agent work. Use \`/tickmarkr-loop\` for one spec or \`/tickmarkr-auto\` for several.
16
+
17
+ Loop: \`tickmarkr compile <spec>\` → \`tickmarkr plan\` → \`tickmarkr run\` → \`tickmarkr report <runId> --md\`.
18
+
19
+ - Never run two tickmarkr runs in the same repository concurrently.
20
+ - Never merge tickmarkr work directly to main; new runs consolidate on \`tickmarkr/<runId>\`.
21
+ - Fix source specs instead of editing generated graphs.
22
+ - Never trust a worker's completion claim; gates independently verify the work.
23
+ - Treat missing or unparseable results as failures.
24
+ ${DOCS_END}
25
+ `;
26
+ async function installAgentFiles(cwd, force, docs, notes) {
27
+ const interactive = process.stdin.isTTY === true && process.stdout.isTTY === true;
28
+ let prompt;
29
+ const confirm = async (question) => {
30
+ if (!interactive)
31
+ return false;
32
+ prompt ??= createInterface({ input: process.stdin, output: process.stdout });
33
+ return /^(?:y|yes)$/i.test((await prompt.question(`${question} [y/N] `)).trim());
34
+ };
35
+ try {
36
+ for (const skill of AGENT_SKILLS) {
37
+ const dest = join(cwd, ".claude", "skills", skill, "SKILL.md");
38
+ const exists = existsSync(dest);
39
+ if (exists && !force && !(await confirm(`Overwrite ${dest}?`))) {
40
+ notes.push(`skipped existing ${dest}; pass --force to overwrite it`);
41
+ continue;
42
+ }
43
+ mkdirSync(join(cwd, ".claude", "skills", skill), { recursive: true });
44
+ writeFileSync(dest, readFileSync(new URL(`../../../skills/${skill}/SKILL.md`, import.meta.url)), exists ? undefined : { flag: "wx" });
45
+ notes.push(`${exists ? "overwrote" : "wrote"} ${dest}`);
46
+ }
47
+ const claude = join(cwd, "CLAUDE.md");
48
+ const agents = join(cwd, "AGENTS.md");
49
+ const docPath = existsSync(claude) || !existsSync(agents) ? claude : agents;
50
+ const current = existsSync(docPath) ? readFileSync(docPath, "utf8") : "";
51
+ if (current.includes(DOCS_BEGIN) || current.includes(DOCS_END)) {
52
+ notes.push(`kept existing tickmarkr agent docs in ${docPath}`);
53
+ }
54
+ else if (docs || await confirm(`Append tickmarkr agent docs to ${docPath}?`)) {
55
+ appendFileSync(docPath, `${current ? current.endsWith("\n") ? "\n" : "\n\n" : ""}${AGENT_DOCS}`);
56
+ notes.push(`appended tickmarkr agent docs to ${docPath}`);
57
+ }
58
+ else {
59
+ notes.push(`skipped agent docs for ${docPath}; pass --docs to append them`);
60
+ }
61
+ }
62
+ finally {
63
+ prompt?.close();
64
+ }
65
+ }
8
66
  export async function init(argv, cwd = process.cwd()) {
9
- const { values } = parseArgs({ args: argv, options: { "global-dir": { type: "string" } } });
67
+ const { values } = parseArgs({
68
+ args: argv,
69
+ options: {
70
+ "global-dir": { type: "string" },
71
+ agent: { type: "boolean" },
72
+ force: { type: "boolean" },
73
+ docs: { type: "boolean" },
74
+ },
75
+ });
10
76
  const gdir = values["global-dir"] ?? globalConfigDir();
11
77
  mkdirSync(gdir, { recursive: true });
12
78
  const notes = [];
@@ -19,14 +85,19 @@ export async function init(argv, cwd = process.cwd()) {
19
85
  notes.push(`kept existing ${p}`);
20
86
  }
21
87
  }
22
- const specPath = join(cwd, "drovr.spec.md");
23
- if (!existsSync(specPath)) {
24
- writeFileSync(specPath, specTemplate());
25
- notes.push(`wrote ${specPath}`);
88
+ const specPath = join(cwd, "tickmarkr.spec.md");
89
+ if (existsSync(specPath)) {
90
+ notes.push(`kept existing ${specPath}`);
91
+ }
92
+ else if (existsSync(join(cwd, "drovr.spec.md"))) {
93
+ notes.push(`kept existing ${join(cwd, "drovr.spec.md")}`);
26
94
  }
27
95
  else {
28
- notes.push(`kept existing ${specPath}`);
96
+ writeFileSync(specPath, specTemplate());
97
+ notes.push(`wrote ${specPath}`);
29
98
  }
99
+ if (values.agent)
100
+ await installAgentFiles(cwd, values.force ?? false, values.docs ?? false, notes);
30
101
  const doc = await doctor([], cwd);
31
- return `${notes.join("\n")}\n${doc}\nnext: edit drovr.spec.md, then drovr compile drovr.spec.md && drovr plan && drovr run`;
102
+ return `${notes.join("\n")}\n${doc}\nnext: edit tickmarkr.spec.md, then tickmarkr compile tickmarkr.spec.md && tickmarkr plan && tickmarkr run`;
32
103
  }
@@ -1,9 +1,5 @@
1
- import { type Task } from "../../graph/schema.js";
2
- import { type JournalEvent } from "../../run/journal.js";
3
1
  export type StatusOpts = {
4
2
  iterations?: number;
5
3
  sleep?: (ms: number) => Promise<void>;
6
4
  };
7
- /** Derive the live phase column for one task. Counts only that task's events since its last dispatch/escalation. */
8
- export declare const derivePhase: (task: Task, events: JournalEvent[], channel?: string) => string;
9
5
  export declare function status(argv: string[], cwd?: string, opts?: StatusOpts): Promise<string>;
@@ -6,6 +6,7 @@ const REFRESH_MS = 2000;
6
6
  // The timer must keep the process ALIVE: an unref'd timer here let the event loop drain after the
7
7
  // first frame, so a live `--watch` printed once and exited 0 (OBS-11). Never unref this.
8
8
  const defaultSleep = (ms) => new Promise((r) => setTimeout(r, ms));
9
+ const GATE_KEYS = { build: "B", test: "T", lint: "L", evidence: "E", scope: "S", acceptance: "A", review: "R" };
9
10
  const attemptStartIdx = (events, taskId) => {
10
11
  let idx = -1;
11
12
  for (let i = 0; i < events.length; i++) {
@@ -15,39 +16,51 @@ const attemptStartIdx = (events, taskId) => {
15
16
  }
16
17
  return idx;
17
18
  };
18
- /** Derive the live phase column for one task. Counts only that task's events since its last dispatch/escalation. */
19
- export const derivePhase = (task, events, channel) => {
20
- const startIdx = attemptStartIdx(events, task.id);
21
- if (startIdx < 0)
22
- return "";
23
- // Scope to this task — concurrent tasks' events must not leak into the attempt window (D-03).
24
- const attempt = events.slice(startIdx).filter((e) => e.taskId === task.id);
25
- if (attempt.some((e) => e.event === "task-done" || e.event === "task-failed" || e.event === "task-human"))
26
- return "";
27
- if (!attempt.some((e) => e.event === "task-dispatch"))
28
- return "retrying"; // escalation before re-dispatch
29
- if (!attempt.some((e) => e.event === "worker-result"))
30
- return channel ? `worker · ${channel}` : "worker";
31
- const enabled = GATE_NAMES.filter((g) => task.gates.includes(g));
32
- const n = enabled.length;
33
- const gateResults = attempt.filter((e) => e.event === "gate-result");
34
- if (gateResults.length > 0) {
35
- const last = gateResults[gateResults.length - 1];
36
- if (!last.data.pass)
37
- return `gate ✗ ${last.data.gate}`;
19
+ const visual = () => process.stdout.isTTY === true && process.env.NO_COLOR === undefined;
20
+ const color = (text, code, enabled) => enabled ? `\x1b[${code}m${text}\x1b[0m` : text;
21
+ const taskBox = (status, unicode) => {
22
+ if (unicode) {
23
+ if (status === "done")
24
+ return "✓";
25
+ if (status === "failed")
26
+ return "";
27
+ if (status === "human")
28
+ return "";
29
+ return "";
38
30
  }
39
- const passed = new Set();
40
- for (const e of gateResults)
41
- if (e.data.pass)
42
- passed.add(String(e.data.gate));
43
- const k = passed.size;
44
- // n===0 (no enabled gates) → merge window immediately after worker-result
45
- if (n === 0 || k >= n) {
46
- if (!attempt.some((e) => e.event === "merge" || e.event === "task-done"))
47
- return "merging";
48
- return "";
31
+ if (status === "done")
32
+ return "[x]";
33
+ if (status === "failed" || status === "human")
34
+ return "[!]";
35
+ return "[ ]";
36
+ };
37
+ const gateBox = (state, unicode) => {
38
+ if (unicode)
39
+ return state === "pass" ? "✓" : state === "fail" ? "✗" : state === "skip" ? "·" : "☐";
40
+ return state === "pass" ? "[x]" : state === "fail" ? "[!]" : state === "skip" ? "." : "[ ]";
41
+ };
42
+ const gateChain = (task, events, unicode) => {
43
+ const outcomes = new Map();
44
+ const start = attemptStartIdx(events, task.id);
45
+ if (start >= 0) {
46
+ for (const e of events.slice(start)) {
47
+ if (e.taskId !== task.id || e.event !== "gate-result" || typeof e.data.gate !== "string")
48
+ continue;
49
+ if (e.data.pass === true)
50
+ outcomes.set(e.data.gate, "pass");
51
+ else if (e.data.pass === false)
52
+ outcomes.set(e.data.gate, "fail");
53
+ }
49
54
  }
50
- return `gate ${k + 1}/${n} ${enabled[k]}`;
55
+ return GATE_NAMES.map((gate) => `${GATE_KEYS[gate]}${gateBox(task.gates.includes(gate) ? outcomes.get(gate) ?? "open" : "skip", unicode)}`).join(" ");
56
+ };
57
+ const shortGoal = (goal, max) => {
58
+ const clause = goal.split(/[,;.?!]/, 1)[0].trim();
59
+ if (clause.length <= max)
60
+ return clause;
61
+ if (max <= 3)
62
+ return clause.slice(0, Math.max(0, max));
63
+ return `${clause.slice(0, max - 3).trimEnd()}...`;
51
64
  };
52
65
  // VIS-11 (v1.13): a liveness header for renderFrame — last journal event age + whether the recorded
53
66
  // daemon pid is still alive. Honest about unknowns: a pre-v1.13 journal with no pid renders "unknown",
@@ -72,14 +85,14 @@ const daemonPid = (events) => {
72
85
  }
73
86
  return undefined;
74
87
  };
75
- const livenessLine = (events) => {
88
+ const liveness = (events) => {
76
89
  const last = events.at(-1);
77
90
  if (!last)
78
- return null; // nothing to be honest about
91
+ return "last event unknown · daemon pid unknown";
79
92
  const age = fmtAge(Date.now() - Date.parse(last.ts));
80
93
  const pid = daemonPid(events);
81
94
  if (pid === undefined)
82
- return ` last event ${age} ago · daemon pid unknown`;
95
+ return `last event ${age} ago · daemon pid unknown`;
83
96
  let state;
84
97
  try {
85
98
  process.kill(pid, 0);
@@ -88,17 +101,14 @@ const livenessLine = (events) => {
88
101
  catch (k) {
89
102
  state = k.code === "ESRCH" ? "dead" : "alive";
90
103
  } // EPERM ⇒ alive
91
- return ` last event ${age} ago · daemon pid ${pid} ${state}`;
104
+ return `last event ${age} ago · daemon pid ${pid} ${state}`;
92
105
  };
93
106
  const renderFrame = (cwd) => {
94
107
  const g = loadGraph(cwd);
95
108
  const runId = Journal.latestRunId(cwd);
96
109
  const assignments = new Map();
97
- const gates = new Map();
98
110
  let replayed = null;
99
111
  let events = [];
100
- // v1.23 T2: latest known context tokens per task (from context-sample journal events). Informational
101
- // only — never feeds status/phase/gate classification (derivePhase and replayStatuses ignore them).
102
112
  const contexts = new Map();
103
113
  if (runId) {
104
114
  const j = Journal.open(cwd, runId);
@@ -107,11 +117,8 @@ const renderFrame = (cwd) => {
107
117
  for (const e of events) {
108
118
  if (e.event === "task-dispatch" && e.taskId) {
109
119
  const a = e.data.assignment;
110
- assignments.set(e.taskId, `${a.adapter}:${a.model}`);
111
- }
112
- if (e.event === "gate-result" && e.taskId) {
113
- const prev = gates.get(e.taskId) ?? "";
114
- gates.set(e.taskId, `${prev}${e.data.pass ? "✓" : "✗"}${e.data.gate} `);
120
+ if (typeof a.adapter === "string" && typeof a.model === "string")
121
+ assignments.set(e.taskId, `${a.adapter}:${a.model}`);
115
122
  }
116
123
  if (e.event === "context-sample" && e.taskId && typeof e.data.tokens === "number" && Number.isFinite(e.data.tokens)) {
117
124
  contexts.set(e.taskId, e.data.tokens); // last write wins
@@ -121,22 +128,26 @@ const renderFrame = (cwd) => {
121
128
  const effective = { ...g, tasks: g.tasks.map((t) => ({ ...t, status: replayed?.get(t.id) ?? t.status })) };
122
129
  const starved = new Set(blockedTasks(effective).map((t) => t.id));
123
130
  const waiting = new Set(pendingTasks(effective).map((t) => t.id));
131
+ const unicode = visual();
132
+ const divider = unicode ? " · " : " / ";
124
133
  const rows = g.tasks.map((t) => {
125
134
  const st = replayed?.get(t.id) ?? t.status;
126
135
  const label = starved.has(t.id) ? " starved" : waiting.has(t.id) ? " dep-waiting" : "";
127
- const phase = derivePhase(t, events, assignments.get(t.id)) || "-";
128
- const base = assignments.get(t.id) ?? "-";
129
- const ctx = contexts.get(t.id);
130
- // Informational suffix only when known never invents 0 from absence.
131
- const assignCol = ctx !== undefined ? `${base} · ctx ${ctx}` : base;
132
- return ` ${t.id.padEnd(6)} ${String(st).padEnd(8)} ${phase.padEnd(28)} ${assignCol.padEnd(28)} ${gates.get(t.id) ?? ""}${label}`;
136
+ const channel = assignments.get(t.id) ?? "-";
137
+ const assignCol = contexts.has(t.id) ? `${channel}${divider}ctx ${contexts.get(t.id)}` : channel;
138
+ const chain = gateChain(t, events, unicode);
139
+ const statusText = color(String(st), st === "done" ? 32 : st === "failed" ? 31 : st === "human" ? 33 : 36, unicode);
140
+ const suffix = ` ${chain} ${String(st)}${label} ${assignCol}`;
141
+ const width = process.stdout.columns ?? 120;
142
+ const prefix = ` ${taskBox(st, unicode)} ${t.id} `;
143
+ return `${prefix}${shortGoal(t.goal, Math.max(0, width - prefix.length - suffix.length))} ${chain} ${statusText}${label} ${assignCol}`;
133
144
  });
134
- return [
135
- `drovr status${runId ? ` — run ${runId}` : " — no runs yet"}`,
136
- ...(runId ? [livenessLine(events)].filter((l) => l !== null) : []),
137
- " task status phase assignment gates",
138
- ...rows,
139
- ].join("\n");
145
+ const done = effective.tasks.filter((t) => t.status === "done").length;
146
+ const header = runId
147
+ ? `tickmarkr status${divider}run ${runId}${divider}${liveness(events).replaceAll(" · ", divider)}${divider}${done}/${g.tasks.length} done`
148
+ : `tickmarkr status${divider}no runs yet${divider}${done}/${g.tasks.length} done`;
149
+ const legend = ` gates: ${GATE_NAMES.map((gate) => `${GATE_KEYS[gate]} ${gate}`).join(divider)}`;
150
+ return [header, legend, ...rows].join("\n");
140
151
  };
141
152
  export async function status(argv, cwd = process.cwd(), opts = {}) {
142
153
  if (!argv.includes("--watch"))
@@ -146,7 +157,7 @@ export async function status(argv, cwd = process.cwd(), opts = {}) {
146
157
  const bounded = Number.isFinite(iterations);
147
158
  const frames = [];
148
159
  const sep = "\n---\n";
149
- const tty = process.stdout.isTTY;
160
+ const tty = visual();
150
161
  for (let i = 0; i < iterations; i++) {
151
162
  const frame = renderFrame(cwd);
152
163
  if (tty)
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  export type CommandMap = Record<string, (argv: string[]) => Promise<string>>;
3
3
  export declare const COMMANDS: CommandMap;
4
- export declare const USAGE = "drovr \u2014 spec-driven orchestration harness for AI coding agents\nusage: drovr <command>\n init guided setup + doctor\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";
4
+ 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";
5
5
  export declare function dispatch(cmd: string | undefined, argv: string[], commands?: CommandMap): Promise<{
6
6
  out: string;
7
7
  code: number;
package/dist/cli/index.js CHANGED
@@ -16,9 +16,9 @@ import { unlock } from "./commands/unlock.js";
16
16
  export const COMMANDS = {
17
17
  init, doctor, compile, scope, plan, run, status, resume, report, profile, unlock, approve,
18
18
  };
19
- export const USAGE = `drovr — spec-driven orchestration harness for AI coding agents
20
- usage: drovr <command>
21
- init guided setup + doctor
19
+ export const USAGE = `tickmarkr — spec-driven orchestration harness for AI coding agents
20
+ usage: tickmarkr <command>
21
+ init guided setup + doctor; init --agent [--force] [--docs] adds agent skills/docs
22
22
  doctor re-probe adapters, herdr, auth; print capability matrix
23
23
  compile <src> spec → .drovr/graph.json (fails without acceptance criteria)
24
24
  scope <intent> draft a compiled native spec beside an answered intent (--force to overwrite)
@@ -41,7 +41,7 @@ export async function dispatch(cmd, argv, commands = COMMANDS) {
41
41
  return { out: await fn(argv), code: 0 };
42
42
  }
43
43
  catch (err) {
44
- return { out: `drovr ${cmd}: ${err.message}`, code: 1 };
44
+ return { out: `tickmarkr ${cmd}: ${err.message}`, code: 1 };
45
45
  }
46
46
  }
47
47
  /* v8 ignore start -- binary entry: printing + process.exit side effects, not unit-testable (ROADMAP crit 2) */
@@ -1,7 +1,7 @@
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*drovr:spec(?:\s+v1)?\s*-->\s*$/m;
4
+ export const NATIVE_MARKER = /^<!--\s*(?:tickmarkr|drovr):spec(?:\s+v1)?\s*-->\s*$/m;
5
5
  const HEAD_RE = /^## (T\d+):\s*(.+)$/;
6
6
  const FIELD_RE = /^- (\w+):\s*(.*)$/;
7
7
  const NESTED_RE = /^\s+- (.+)$/;
@@ -143,7 +143,7 @@ export function compileNative(file) {
143
143
  // parser ignores) so the template itself round-trips through compileSource() unchanged. Every field is
144
144
  // documented; the two example tasks illustrate plain vs deps+routing-hint, each with acceptance[].
145
145
  export function specTemplate() {
146
- return `<!-- drovr:spec -->
146
+ return `<!-- tickmarkr:spec -->
147
147
  # drovr native spec
148
148
 
149
149
  Your starting point for a drovr native spec. Edit this file, then run:
@@ -11,10 +11,13 @@ export declare class HerdrDriver implements ExecutorDriver {
11
11
  private groups;
12
12
  private groupSerial;
13
13
  private ws;
14
+ private callerPane;
15
+ private watches;
14
16
  constructor(bin?: string, workersPerTab?: number);
15
17
  private serial;
16
18
  static available(): boolean;
17
19
  private herdr;
20
+ private namedPaneId;
18
21
  private paneId;
19
22
  private paneWidth;
20
23
  slot(cwd: string, name: string, opts?: SlotOpts): Promise<Slot>;
@@ -36,6 +39,8 @@ export declare class HerdrDriver implements ExecutorDriver {
36
39
  notify(msg: string, opts?: NotifyOpts): Promise<void>;
37
40
  close(slot: Slot): Promise<void>;
38
41
  private closeGrouped;
42
+ private priorWatch;
43
+ private watchSlot;
39
44
  narrator(cwd: string, command: string, runId?: string): Promise<Slot>;
40
45
  reconcile(desired: Set<string>, runId: string, opts?: {
41
46
  spareLiveLlm?: boolean;
@@ -1,7 +1,7 @@
1
1
  import { shq } from "../adapters/types.js";
2
2
  import { createWorktree, sh } from "../run/git.js";
3
3
  import { herdrSealShellPrefix } from "./subprocess.js";
4
- import { canonicalizeLegacyName, formatOwnedName, panesToClose } from "./types.js";
4
+ import { canonicalizeLegacyName, formatOwnedName, panesToClose, parseOwnedName } from "./types.js";
5
5
  // VIS-09 P43-03: adopted safety floor from 43-MEASUREMENT.md (narrowest safe 53 → floor 108).
6
6
  export const TRAILER_SAFE_FLOOR_COLS = 108;
7
7
  export const TRAILER_WIDTH_MARGIN = 2; // cols below (floor + margin) refuse a rightward first split
@@ -24,6 +24,8 @@ export class HerdrDriver {
24
24
  // operator's env before the driver is built). Required at slot() time, never in the constructor —
25
25
  // pickDriver and its unit test construct HerdrDriver without env, so slot() is the trust gate.
26
26
  ws = process.env.HERDR_WORKSPACE_ID;
27
+ callerPane = process.env.HERDR_PANE_ID;
28
+ watches = new Map();
27
29
  constructor(bin = "herdr", workersPerTab = 3) {
28
30
  this.bin = bin;
29
31
  this.workersPerTab = workersPerTab;
@@ -39,18 +41,21 @@ export class HerdrDriver {
39
41
  herdr(args, cwd = process.cwd(), timeoutMs) {
40
42
  return sh(`${shq(this.bin)} ${args}`, cwd, timeoutMs);
41
43
  }
42
- // pane ids compact when panes close — resolve fresh via the durable agent name (spec §5)
43
- async paneId(slot) {
44
- const r = await this.herdr(`agent get ${shq(slot.name)}`);
44
+ async namedPaneId(name) {
45
+ const r = await this.herdr(`agent get ${shq(name)}`);
46
+ if (r.code !== 0)
47
+ return null;
45
48
  try {
46
49
  const id = JSON.parse(r.stdout).result?.agent?.pane_id;
47
- if (typeof id === "string" && id)
48
- return id;
50
+ return typeof id === "string" && id ? id : null;
49
51
  }
50
52
  catch {
51
- /* fall back to cached id */
53
+ return null;
52
54
  }
53
- return slot.id;
55
+ }
56
+ // pane ids compact when panes close — resolve fresh via the durable agent name (spec §5)
57
+ async paneId(slot) {
58
+ return await this.namedPaneId(slot.name) ?? slot.id;
54
59
  }
55
60
  // VIS-09 P43-03: runtime width for the layout gate (43-MEASUREMENT.md licensing condition 2).
56
61
  async paneWidth(paneId) {
@@ -310,6 +315,13 @@ export class HerdrDriver {
310
315
  await this.herdr(`notification show ${shq(msg)} --sound ${opts?.tier === "attention" ? "request" : opts?.sound ?? "request"}`);
311
316
  }
312
317
  async close(slot) {
318
+ if (this.watches.get(slot.name)?.id === slot.id) {
319
+ this.watches.delete(slot.name);
320
+ const pane = await this.namedPaneId(slot.name);
321
+ if (pane)
322
+ await this.herdr(`pane close ${shq(pane)}`);
323
+ return; // run-end reconcile may already have reaped it; never close a compacted stale id
324
+ }
313
325
  if (slot.group && this.groups.has(slot.group)) {
314
326
  return this.serial(() => this.closeGrouped(slot));
315
327
  }
@@ -342,16 +354,95 @@ export class HerdrDriver {
342
354
  this.groups.delete(slot.group); // group dies when all generations gone
343
355
  }
344
356
  }
345
- // T6 narrator: the run's single live surface — a drovr-created "watch" tab running the daemon's
346
- // status command. Reuses the dedicated-tab slot path (label → tabSlot); close() reaps the whole tab
347
- // at run end. One per run (the daemon calls this once). Failures propagate the daemon swallows.
348
- // T2: with a runId the pane is named canonically (drovr:watch:run:0:<runId>) so reconcile owns it;
349
- // the legacy `narrator-watch-<pid>` shape survives for runId-less callers (canonicalizeLegacyName).
357
+ async priorWatch(runId) {
358
+ if (!this.ws)
359
+ throw new Error("herdr watch placement requires HERDR_WORKSPACE_IDrefusing unseeded pane");
360
+ const list = await this.herdr("agent list");
361
+ if (list.code !== 0)
362
+ throw new Error(`herdr agent list failed: ${list.stderr || list.stdout}`);
363
+ let agents;
364
+ try {
365
+ agents = JSON.parse(list.stdout).result?.agents;
366
+ }
367
+ catch {
368
+ throw new Error(`herdr agent list returned unparseable JSON: ${list.stdout}`);
369
+ }
370
+ if (!Array.isArray(agents))
371
+ throw new Error(`herdr agent list returned no agents: ${list.stdout}`);
372
+ const prior = agents.find((a) => {
373
+ const owned = typeof a.name === "string" ? parseOwnedName(a.name) : null;
374
+ return a.workspace_id === this.ws && typeof a.pane_id === "string" && owned?.role === "watch" && owned.taskId === "run" && owned.runId !== runId;
375
+ });
376
+ return prior?.pane_id ?? null;
377
+ }
378
+ // T2: the watch is a rightward sibling of the daemon's own pane, never a separate tab. Its durable
379
+ // owned name lets a resumed daemon find an already-running watch instead of stacking another one.
380
+ async watchSlot(cwd, name) {
381
+ if (!this.ws)
382
+ throw new Error("herdr watch placement requires HERDR_WORKSPACE_ID — refusing unseeded pane");
383
+ if (!this.callerPane)
384
+ throw new Error("herdr watch placement requires HERDR_PANE_ID — refusing untargeted split");
385
+ const sp = await this.herdr(`pane split ${shq(this.callerPane)} --direction right --no-focus`);
386
+ if (sp.code !== 0)
387
+ throw new Error(`herdr watch split failed: ${sp.stderr || sp.stdout}`);
388
+ let pane;
389
+ try {
390
+ pane = JSON.parse(sp.stdout).result?.pane?.pane_id;
391
+ }
392
+ catch {
393
+ /* fail closed below */
394
+ }
395
+ if (typeof pane !== "string" || !pane)
396
+ throw new Error(`herdr watch split returned no pane id: ${sp.stdout}`);
397
+ const renamed = await this.herdr(`agent rename ${shq(pane)} ${shq(name)}`);
398
+ if (renamed.code !== 0 || await this.namedPaneId(name) !== pane) {
399
+ await this.herdr(`pane close ${shq(pane)}`);
400
+ throw new Error(`herdr watch rename failed: ${renamed.stderr || renamed.stdout}`);
401
+ }
402
+ const seed = await this.herdr(`pane run ${shq(pane)} ${shq(`cd ${shq(cwd)}; export HERDR_WORKSPACE_ID=${shq(this.ws)}; ${herdrSealShellPrefix()}`)}`, cwd);
403
+ if (seed.code !== 0) {
404
+ await this.herdr(`pane close ${shq(pane)}`);
405
+ throw new Error(`herdr watch seed failed: ${seed.stderr || seed.stdout}`);
406
+ }
407
+ return { id: pane, name, cwd };
408
+ }
409
+ // T6 narrator: the run's single live status surface. Reuse a local or already-running owned watch;
410
+ // a new run reowns its prior watch, and only a newly split pane receives the watch command. The
411
+ // status command reads the latest run every frame, so the renamed pane follows the new run without
412
+ // interrupting the operator's watch loop. Failures propagate — the daemon swallows.
350
413
  async narrator(cwd, command, runId) {
351
414
  const name = runId ? formatOwnedName({ role: "watch", taskId: "run", attempt: 0, runId }) : `narrator-watch-${process.pid}`;
352
- const s = await this.slot(cwd, name, { label: "watch" });
353
- await this.run(s, command);
354
- return s;
415
+ return this.serial(async () => {
416
+ const cached = this.watches.get(name);
417
+ if (cached)
418
+ return cached;
419
+ const existing = await this.namedPaneId(name);
420
+ if (existing) {
421
+ const s = { id: existing, name, cwd };
422
+ this.watches.set(name, s);
423
+ return s;
424
+ }
425
+ const prior = runId ? await this.priorWatch(runId) : null;
426
+ if (prior) {
427
+ const renamed = await this.herdr(`agent rename ${shq(prior)} ${shq(name)}`);
428
+ if (renamed.code !== 0 || await this.namedPaneId(name) !== prior) {
429
+ throw new Error(`herdr watch reclaim failed: ${renamed.stderr || renamed.stdout}`);
430
+ }
431
+ const s = { id: prior, name, cwd };
432
+ this.watches.set(name, s);
433
+ return s;
434
+ }
435
+ const s = await this.watchSlot(cwd, name);
436
+ this.watches.set(name, s);
437
+ try {
438
+ await this.run(s, command);
439
+ }
440
+ catch (err) {
441
+ this.watches.delete(name);
442
+ throw err;
443
+ }
444
+ return s;
445
+ });
355
446
  }
356
447
  // OBS-17 T2 / v1.22b T1: close every drovr-owned pane that should not exist (superseded attempts,
357
448
  // killed-daemon orphans, leftovers from OLDER runs) — in this run's workspace OR misplaced in any
@@ -20,19 +20,20 @@ export function parseOwnedName(name) {
20
20
  export function isForeignName(name) {
21
21
  return parseOwnedName(name) === null;
22
22
  }
23
- // v1.22b T1: workspace-aware fold over a fleet snapshot — decides which owned panes are garbage
23
+ // v1.22b T1: workspace-aware fold over a fleet snapshot — decides which owned task panes are garbage
24
24
  // right now. In-workspace: the existing desired-set/spareLiveLlm sweep (OBS-17 T2). Out-of-workspace:
25
25
  // an owned pane from a DIFFERENT run is a misplaced leftover (bug, foreign actor, pre-VIS-10 relic)
26
26
  // and closes regardless of `desired`; an owned pane from THIS run elsewhere is left alone — a live
27
27
  // run can legitimately hold panes across workspaces, so only run age marks a misplaced pane garbage.
28
- // Foreign names (parseOwnedName fails) are never candidates, in any workspace.
28
+ // Watch panes are operator-owned after run end and are reclaimed by the next run; foreign names
29
+ // (parseOwnedName fails) are never candidates, in any workspace.
29
30
  export function panesToClose(agents, desired, ws, runId, opts) {
30
31
  const out = [];
31
32
  for (const a of agents) {
32
33
  if (typeof a.name !== "string" || typeof a.paneId !== "string")
33
34
  continue;
34
35
  const owned = parseOwnedName(a.name);
35
- if (!owned)
36
+ if (!owned || owned.role === "watch")
36
37
  continue;
37
38
  if (a.workspaceId === ws) {
38
39
  if (desired.has(a.name))
@@ -3,7 +3,7 @@ export function scopePrompt(intent, repair) {
3
3
  You are drafting a drovr native spec from an answered intent. Return only the Markdown spec, with no commentary or code fence.
4
4
 
5
5
  The draft must:
6
- - start with <!-- drovr:spec -->
6
+ - start with <!-- tickmarkr:spec -->
7
7
  - include a Requirements section whose ids are sequential REQ-01, REQ-02, ...
8
8
  - include an Assumptions section that makes every residual uncertainty explicit
9
9
  - include a Traceability section mapping every REQ-nn to at least one Tn task
@@ -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 } from "../compile/native.js";
5
+ import { compileNative, NATIVE_MARKER } 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";
@@ -64,12 +64,12 @@ 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.indexOf("<!-- drovr:spec");
67
+ const marker = clean.search(/<!--\s*(?:tickmarkr|drovr):spec/);
68
68
  return (marker === -1 ? clean : clean.slice(marker)).trimEnd() + "\n";
69
69
  }
70
70
  function validateDraft(draft) {
71
- if (!/^<!--\s*drovr:spec(?:\s+v1)?\s*-->/m.test(draft))
72
- throw new Error("draft is missing the drovr native marker");
71
+ if (!NATIVE_MARKER.test(draft))
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) ?? [])];
@@ -38,9 +38,8 @@ export async function runDaemon(repoRoot, opts = {}) {
38
38
  // BEFORE loadGraph; release in the finally below (every exit path, incl. throws).
39
39
  const runId = opts.runId ?? newRunId();
40
40
  // T6 narrator: one live status surface per run (herdr only — driver.narrator is undefined on
41
- // subprocess, so the optional-chain open below is a no-op there). Declared out here so the finally
42
- // can close it on every exit path. Cosmetic-only: any failure is swallowed (never affects the run).
43
- let narrator = null;
41
+ // subprocess, so the optional-chain open below is a no-op there). Cosmetic-only: any failure is
42
+ // swallowed (never affects the run); the operator closes a surviving watch pane.
44
43
  const lock = acquireRunLock(repoRoot, runId);
45
44
  try {
46
45
  let graph = loadGraph(repoRoot);
@@ -91,10 +90,10 @@ export async function runDaemon(repoRoot, opts = {}) {
91
90
  // show. driver.narrator is undefined on subprocess → no-op (subprocess spawns nothing). Swallowed:
92
91
  // a failed-to-open or later-dead watch pane never affects the run.
93
92
  try {
94
- narrator = await driver.narrator?.(repoRoot, "drovr status --watch", runId) ?? null;
93
+ await driver.narrator?.(repoRoot, "drovr status --watch", runId);
95
94
  }
96
95
  catch {
97
- narrator = null; // cosmetic-only — the run proceeds without a live surface
96
+ /* cosmetic-only — the run proceeds without a live surface */
98
97
  }
99
98
  const intWt = await ensureIntegration(repoRoot, branch, baseRef);
100
99
  const concurrency = opts.concurrency ?? cfg.concurrency;
@@ -716,13 +715,6 @@ export async function runDaemon(repoRoot, opts = {}) {
716
715
  return summary;
717
716
  }
718
717
  finally {
719
- // T6: close the narrator at run end — cosmetic, swallowed (never blocks lock release)
720
- if (narrator) {
721
- try {
722
- await driver.close(narrator);
723
- }
724
- catch { /* cosmetic */ }
725
- }
726
718
  releaseRunLock(repoRoot);
727
719
  }
728
720
  }
@@ -88,6 +88,7 @@ export function desiredPanes(rows, runId) {
88
88
  break;
89
89
  case "run-end":
90
90
  desired.clear();
91
+ desired.add(formatOwnedName({ role: "watch", taskId: "run", attempt: 0, runId }));
91
92
  break;
92
93
  }
93
94
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tickmarkr",
3
- "version": "1.30.0",
3
+ "version": "1.33.0",
4
4
  "description": "Spec in, verified work out.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -9,12 +9,12 @@
9
9
  },
10
10
  "bin": {
11
11
  "tickmarkr": "dist/cli/index.js",
12
- "drovr": "dist/cli/index.js",
13
- "drover": "dist/cli/index.js"
12
+ "tkr": "dist/cli/index.js"
14
13
  },
15
14
  "files": [
16
15
  "dist",
17
- "schema"
16
+ "schema",
17
+ "skills"
18
18
  ],
19
19
  "scripts": {
20
20
  "build": "tsc -p tsconfig.json",
@@ -0,0 +1,41 @@
1
+ ---
2
+ name: tickmarkr-auto
3
+ description: 'Run a repository’s requested specs autonomously with tickmarkr. Triggers: "/tickmarkr-auto", "run these specs with tickmarkr", "autonomous tickmarkr run".'
4
+ argument-hint: "[spec-or-directory ...]"
5
+ ---
6
+
7
+ # tickmarkr-auto — run repository specs autonomously
8
+
9
+ Use this to execute a requested sequence of repository specs. It is SDD-agnostic: each target can be any file or directory accepted by `tickmarkr compile`.
10
+
11
+ ## Two-tier by default — role check before the loop
12
+
13
+ When working in a multi-agent terminal environment, decide your role before starting:
14
+
15
+ - **Orchestrator:** your session was started to execute the mission. Run the loop below.
16
+ - **Supervisor with a live orchestrator:** do not start a second run. Relay the mission to the existing orchestrator with a verified send, then supervise it.
17
+ - **Primary session without an orchestrator:** create one child orchestration session, give it the mission and these rules verbatim, then supervise it. Do not drive a duplicate single-tier run yourself.
18
+
19
+ Outside a multi-agent terminal environment, run the loop directly.
20
+
21
+ ## Invariants
22
+
23
+ - Never run two tickmarkr runs in the same repository concurrently.
24
+ - Never let tickmarkr merge work to the main branch. New work consolidates on `tickmarkr/<runId>`.
25
+ - Do not edit a generated graph to force an outcome; correct the source spec and compile again.
26
+ - Gates independently verify evidence, scope, acceptance criteria, and review. A worker's completion claim is not evidence.
27
+ - Treat missing or unparseable machine results and verdicts as failures. Never bypass a failed gate or merge partial work without a human decision.
28
+
29
+ ## Act by default
30
+
31
+ Run every requested target in order without seeking routine confirmation. Stop only for a blocked agent interaction, a genuinely unresolved stalled task, a designed human gate, or a failed run that needs a human decision. Diagnose from the journal and evidence first; self-release and resume only after fixing and verifying a harness defect.
32
+
33
+ ## Per-spec loop
34
+
35
+ 1. **Prepare** — confirm the target list, check `git status`, confirm no tickmarkr run is active, and work from a non-main branch.
36
+ 2. **Compile** — run `tickmarkr compile <spec-or-directory>`. Fix source-spec defects instead of editing the generated graph.
37
+ 3. **Plan** — run `tickmarkr plan`. Review routes, capability-floor warnings, and human gates before execution.
38
+ 4. **Run** — run `tickmarkr run`. Watch the run journal for its terminal event rather than polling agents. Resolve blocked interactions in the relevant agent session.
39
+ 5. **Verify and consolidate** — continue only after a green run. Tickmarkr consolidates accepted work on `tickmarkr/<runId>` and never signs off to the main branch. A human controls any later release merge.
40
+ 6. **Record** — write `tickmarkr report <runId> --md` beside the source spec and commit the execution record when the repository tracks those records.
41
+ 7. **Continue** — move to the next requested target. If a target fails or is parked, stop with the journal evidence rather than silently skipping it.
@@ -0,0 +1,39 @@
1
+ ---
2
+ name: tickmarkr-loop
3
+ description: 'Run one repository spec autonomously with tickmarkr. Triggers: "/tickmarkr-loop", "run this spec with tickmarkr", "tickmarkr the spec".'
4
+ ---
5
+
6
+ # tickmarkr-loop — run one spec autonomously
7
+
8
+ Use this for any spec that `tickmarkr compile` accepts. It is SDD-agnostic: use the repository's requested spec format and keep the execution record beside that source spec.
9
+
10
+ ## Two-tier by default — role check before the loop
11
+
12
+ When working in a multi-agent terminal environment, decide your role before starting:
13
+
14
+ - **Orchestrator:** your session was started to execute the mission. Run the loop below.
15
+ - **Supervisor with a live orchestrator:** do not start a second run. Relay the mission to the existing orchestrator with a verified send, then supervise it.
16
+ - **Primary session without an orchestrator:** create one child orchestration session, give it the mission and these rules verbatim, then supervise it. Do not drive a duplicate single-tier run yourself.
17
+
18
+ Outside a multi-agent terminal environment, run the loop directly.
19
+
20
+ ## Invariants
21
+
22
+ - Never run two tickmarkr runs in the same repository concurrently.
23
+ - Never let tickmarkr merge work to the main branch. New work consolidates on `tickmarkr/<runId>`.
24
+ - Do not edit the compiled graph to force an outcome; fix the source spec and compile again.
25
+ - Gates verify commits, diffs, acceptance criteria, and reviews independently. Never trust a worker's claim that work is complete.
26
+ - Treat missing or unparseable machine results and verdicts as failures. Do not release, resume, or merge around failed gates.
27
+
28
+ ## Act by default
29
+
30
+ Proceed through the loop without seeking routine confirmation. Stop only for a blocked agent interaction, a genuinely unresolved stalled task, or a designed human gate. Diagnose from the journal and available evidence before escalating; if a harness defect is fixed and verified, resume the run.
31
+
32
+ ## The loop
33
+
34
+ 1. **Prepare** — start from the requested spec. Check `git status`, confirm no tickmarkr run is active, and work from a non-main branch.
35
+ 2. **Compile** — run `tickmarkr compile <spec>`. Correct compilation errors in the spec, never in the generated graph.
36
+ 3. **Plan** — run `tickmarkr plan`. Review the routing table, capability-floor warnings, and every human gate, including work that each gate blocks.
37
+ 4. **Run** — run `tickmarkr run`. Watch the run journal for its terminal event rather than repeatedly polling agents. Resolve blocked interactions in the agent session; do not turn them into proxy questions.
38
+ 5. **Verify and consolidate** — accept only a green run. Tickmarkr consolidates accepted task work on `tickmarkr/<runId>`; it never signs off to the main branch. A human may later merge that integration branch through the repository's normal release process.
39
+ 6. **Record** — write `tickmarkr report <runId> --md` beside the source spec and commit the execution record when the repository tracks those records.