tickmarkr 1.30.0 → 1.33.1

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.
@@ -24,7 +24,7 @@ export function drovrDir(repoRoot) {
24
24
  export function loadGraph(repoRoot) {
25
25
  const p = graphPath(repoRoot);
26
26
  if (!existsSync(p))
27
- throw new Error(`no graph at ${p} — run \`drovr compile <src>\` first`);
27
+ throw new Error(`no graph at ${p} — run \`tickmarkr compile <src>\` first`);
28
28
  return validateGraph(JSON.parse(readFileSync(p, "utf8")));
29
29
  }
30
30
  export function saveGraph(repoRoot, g) {
@@ -1,9 +1,9 @@
1
1
  export function scopePrompt(intent, repair) {
2
2
  return `DROVR-JUDGE
3
- You are drafting a drovr native spec from an answered intent. Return only the Markdown spec, with no commentary or code fence.
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:
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) ?? [])];
@@ -6,7 +6,7 @@ import { sh } from "./git.js";
6
6
  const ACTIONS = ["retry", "reroute", "decompose", "human"];
7
7
  export function buildDossierPrompt(d) {
8
8
  return `DROVR-CONSULT
9
- You are a senior engineering consult for the drovr orchestrator. A worker task hit trouble.
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
 
12
12
  ## Task: ${d.taskId} — trigger: ${d.trigger}
@@ -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, "tickmarkr 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;
@@ -139,7 +138,7 @@ export async function runDaemon(repoRoot, opts = {}) {
139
138
  // merges are serialized — two concurrent `git merge`s in one worktree would corrupt each other
140
139
  let mergeChain = Promise.resolve();
141
140
  const mergeSerial = (taskBranch, t, gated) => {
142
- const next = mergeChain.then(() => mergeTask(intWt, taskBranch, `drovr: merge ${t.id} ${t.title}`, gated));
141
+ const next = mergeChain.then(() => mergeTask(intWt, taskBranch, `tickmarkr: merge ${t.id} ${t.title}`, gated));
143
142
  mergeChain = next.catch(() => undefined);
144
143
  return next;
145
144
  };
@@ -153,7 +152,7 @@ export async function runDaemon(repoRoot, opts = {}) {
153
152
  journal.telemetry({ taskId: t.id, shape: t.shape, adapter: assignment.adapter, model: assignment.model, channel: assignment.channel, attempts, outcome: "human", durationMs: Date.now() - startMs, parkKind: kind ?? undefined, gateFails, consults, tokens, meteredAttempts: tokens ? metered : undefined, retryMode });
154
153
  }
155
154
  await reconcile({ spareLiveLlm: true }); // task-human is a terminal event — sweep, sparing sibling tasks' live LLM panes
156
- await driver.notify(`drovr ${runId}: ${t.id} needs a human — ${reason}`, { tier: "attention" });
155
+ await driver.notify(`tickmarkr ${runId}: ${t.id} needs a human — ${reason}`, { tier: "attention" });
157
156
  };
158
157
  const execTask = async (t) => {
159
158
  const startMs = Date.now();
@@ -252,7 +251,7 @@ export async function runDaemon(repoRoot, opts = {}) {
252
251
  action: v.action, notes: v.notes,
253
252
  ...(v.excludeAdapter ? { excludeAdapter: v.excludeAdapter } : {}),
254
253
  });
255
- await driver.notify(`drovr ${runId}: ${t.id} consult verdict: ${v.action}`, { tier: "attention" });
254
+ await driver.notify(`tickmarkr ${runId}: ${t.id} consult verdict: ${v.action}`, { tier: "attention" });
256
255
  if (v.action === "retry") {
257
256
  feedback = v.notes || feedback;
258
257
  return true;
@@ -386,7 +385,7 @@ export async function runDaemon(repoRoot, opts = {}) {
386
385
  threshold: cfg.contextWarnTokens,
387
386
  attempt,
388
387
  });
389
- await driver.notify(`drovr ${runId}: ${t.id} context ${usage.tokens} tokens ≥ ${cfg.contextWarnTokens}`, { tier: "attention" });
388
+ await driver.notify(`tickmarkr ${runId}: ${t.id} context ${usage.tokens} tokens ≥ ${cfg.contextWarnTokens}`, { tier: "attention" });
390
389
  };
391
390
  let finished;
392
391
  let output;
@@ -449,7 +448,7 @@ export async function runDaemon(repoRoot, opts = {}) {
449
448
  }
450
449
  paged = true; // page once — the visible pane is the operator's to unblock; task timeout is the backstop
451
450
  const why = st === "blocked" ? "is blocked on a prompt — approve in its pane" : "looks idle without finishing — check its pane";
452
- await driver.notify(`drovr ${runId}: ${slot.name} ${why}`, { tier: "attention" });
451
+ await driver.notify(`tickmarkr ${runId}: ${slot.name} ${why}`, { tier: "attention" });
453
452
  }
454
453
  // a dead pane or a false-positive marker display returns fast — sleep the unspent slice, never hot-spin
455
454
  const spent = Date.now() - sliceStart;
@@ -500,7 +499,7 @@ export async function runDaemon(repoRoot, opts = {}) {
500
499
  const next = failover("quota-failover");
501
500
  journal.append("quota-failover", t.id, { from: channelKey(assignment), to: next ? channelKey(next) : null });
502
501
  if (next) {
503
- await driver.notify(`drovr ${runId}: ${t.id} quota failover`, { tier: "attention" });
502
+ await driver.notify(`tickmarkr ${runId}: ${t.id} quota failover`, { tier: "attention" });
504
503
  // OBS-17 T2: the superseded slot's pane closes AT REROUTE TIME — it holds a throttled
505
504
  // dead-end, not failure context; the next safe-point reconcile catches a missed close.
506
505
  if (!keepForever) {
@@ -562,7 +561,7 @@ export async function runDaemon(repoRoot, opts = {}) {
562
561
  });
563
562
  }
564
563
  }
565
- journal.append("gate-result", t.id, { gate: g.gate, pass: g.pass, details: g.details });
564
+ journal.append("gate-result", t.id, { gate: g.gate, pass: g.pass, details: g.details, ...(g.meta?.skipped === true ? { skipped: true } : {}) });
566
565
  // v1.1 failover: never re-ask a reviewer channel that produced garbage for this task
567
566
  if (g.gate === "review" && !g.pass && /unparseable/.test(g.details) && typeof g.meta?.reviewer === "string") {
568
567
  badReviewers.push(g.meta.reviewer);
@@ -642,7 +641,7 @@ export async function runDaemon(repoRoot, opts = {}) {
642
641
  feedback = results.filter((g) => !g.pass).map((g) => `${g.gate}: ${g.details}`).join("\n\n");
643
642
  const step = r.ladder[Math.min(ladderIdx++, r.ladder.length - 1)];
644
643
  journal.append("escalation", t.id, { step, attempt: attempt + 1 });
645
- await driver.notify(`drovr ${runId}: ${t.id} escalation: ${step}`, { tier: "attention" });
644
+ await driver.notify(`tickmarkr ${runId}: ${t.id} escalation: ${step}`, { tier: "attention" });
646
645
  if (step === "retry")
647
646
  continue;
648
647
  if (step === "escalate") {
@@ -712,17 +711,10 @@ export async function runDaemon(repoRoot, opts = {}) {
712
711
  .sort(([a], [b]) => a.localeCompare(b))
713
712
  .map(([root, count]) => `${count} blocked behind ${root}`)
714
713
  .join(", ");
715
- await driver.notify(`drovr ${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" });
714
+ 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" });
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
  }
package/dist/run/git.js CHANGED
@@ -7,14 +7,35 @@ import { drovrDir } from "../graph/graph.js";
7
7
  // (pi -p / codex exec wait for stdin EOF). timedOut distinguishes SIGKILL-timeout from a real nonzero exit.
8
8
  export function sh(cmd, cwd, timeoutMs = 600000) {
9
9
  return new Promise((resolve) => {
10
- const p = spawn("bash", ["-lc", cmd], { cwd, stdio: ["ignore", "pipe", "pipe"] });
10
+ // detached: bash gets its own process group so a timeout can kill the whole tree —
11
+ // SIGKILLing bash alone orphans grandchildren (codex/pi) that hold the stdio pipes
12
+ // open, so "close" never fires and the promise wedges forever (v1.33.1 init hang).
13
+ const p = spawn("bash", ["-lc", cmd], { cwd, stdio: ["ignore", "pipe", "pipe"], detached: true });
11
14
  let stdout = "", stderr = "";
12
- let timedOut = false;
13
- const timer = setTimeout(() => { timedOut = true; p.kill("SIGKILL"); }, timeoutMs);
15
+ let timedOut = false, done = false;
16
+ const finish = (code, err) => {
17
+ if (done)
18
+ return;
19
+ done = true;
20
+ clearTimeout(timer);
21
+ resolve({ code, stdout, stderr: err ?? stderr, timedOut });
22
+ };
23
+ const timer = setTimeout(() => {
24
+ timedOut = true;
25
+ try {
26
+ process.kill(-p.pid, "SIGKILL");
27
+ }
28
+ catch {
29
+ p.kill("SIGKILL");
30
+ }
31
+ }, timeoutMs);
14
32
  p.stdout.on("data", (d) => (stdout += d));
15
33
  p.stderr.on("data", (d) => (stderr += d));
16
- p.on("error", (e) => { clearTimeout(timer); resolve({ code: 127, stdout, stderr: String(e), timedOut }); });
17
- p.on("close", (code) => { clearTimeout(timer); resolve({ code: code ?? 1, stdout, stderr, timedOut }); });
34
+ p.on("error", (e) => finish(127, String(e)));
35
+ p.on("close", (code) => finish(code ?? 1));
36
+ // "close" waits for stdio to drain; a surviving pipe-holder must not outlive the timeout
37
+ p.on("exit", (code) => { if (timedOut)
38
+ finish(code ?? 1); });
18
39
  });
19
40
  }
20
41
  export async function shOk(cmd, cwd) {
@@ -80,7 +80,9 @@ export declare class Journal {
80
80
  private constructor();
81
81
  static create(repoRoot: string, runId: string, narrate?: (event: JournalEvent) => void): Journal;
82
82
  static open(repoRoot: string, runId: string, narrate?: (event: JournalEvent) => void): Journal;
83
- static latestRunId(repoRoot: string): string | null;
83
+ static latestRunId(repoRoot: string, opts?: {
84
+ withJournal?: boolean;
85
+ }): string | null;
84
86
  private get journalPath();
85
87
  append(event: string, taskId?: string, data?: Record<string, unknown>): void;
86
88
  read(): JournalEvent[];
@@ -157,10 +157,16 @@ export class Journal {
157
157
  throw new Error(`no journal for ${runId} at ${dir}`);
158
158
  return new Journal(dir, runId, narrate);
159
159
  }
160
- static latestRunId(repoRoot) {
160
+ // withJournal: journal.jsonl appears at first append, after Journal.create mkdirs — a caller that
161
+ // will Journal.open the result (status, report) must fall back to the newest run that is actually
162
+ // readable, not throw on the mkdir-to-first-append window. Raw default stays for telemetry-scoped
163
+ // callers (profile cursor), where a journal-less run dir still counts.
164
+ static latestRunId(repoRoot, opts = {}) {
161
165
  if (!existsSync(runsDir(repoRoot)))
162
166
  return null;
163
- const ids = readdirSync(runsDir(repoRoot)).filter((d) => d.startsWith("run-")).sort();
167
+ const ids = readdirSync(runsDir(repoRoot))
168
+ .filter((d) => d.startsWith("run-") && (!opts.withJournal || existsSync(join(runsDir(repoRoot), d, "journal.jsonl"))))
169
+ .sort();
164
170
  return ids.at(-1) ?? null;
165
171
  }
166
172
  get journalPath() {
@@ -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.1",
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.