tickmarkr 1.33.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.
- package/README.md +21 -10
- package/dist/adapters/cursor-agent.js +1 -1
- package/dist/adapters/model-lints.js +3 -3
- package/dist/adapters/prompt.js +1 -1
- package/dist/cli/commands/approve.js +4 -4
- package/dist/cli/commands/compile.js +1 -1
- package/dist/cli/commands/doctor.js +44 -2
- package/dist/cli/commands/plan.js +2 -2
- package/dist/cli/commands/profile.js +1 -1
- package/dist/cli/commands/report.js +2 -2
- package/dist/cli/commands/resume.js +1 -1
- package/dist/cli/commands/scope.js +1 -1
- package/dist/cli/commands/status.js +64 -18
- package/dist/compile/gsd.js +3 -3
- package/dist/compile/native.js +4 -4
- package/dist/compile/speckit.js +1 -1
- package/dist/config/config.js +5 -5
- package/dist/drivers/subprocess.js +2 -2
- package/dist/gates/baseline.js +6 -2
- package/dist/graph/graph.js +1 -1
- package/dist/plan/prompt.js +1 -1
- package/dist/run/consult.js +1 -1
- package/dist/run/daemon.js +10 -10
- package/dist/run/git.js +26 -5
- package/dist/run/journal.d.ts +3 -1
- package/dist/run/journal.js +8 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -62,9 +62,15 @@ npm i -g tickmarkr
|
|
|
62
62
|
```
|
|
63
63
|
|
|
64
64
|
This installs two identical bins: `tickmarkr` and its short alias `tkr` — use whichever you
|
|
65
|
-
prefer, every example below works with either.
|
|
66
|
-
|
|
67
|
-
|
|
65
|
+
prefer, every example below works with either.
|
|
66
|
+
|
|
67
|
+
**Requirements:**
|
|
68
|
+
|
|
69
|
+
- **macOS or Linux.** Every command shells out through `bash`, so native Windows is not
|
|
70
|
+
supported — use WSL (untested).
|
|
71
|
+
- Node ≥ 20 and `git` on PATH.
|
|
72
|
+
- At least one agent CLI on PATH (`claude`, `codex`, `cursor-agent`, `opencode`, `grok`, or
|
|
73
|
+
`pi`), authenticated through its own login. tickmarkr never handles vendor API keys itself.
|
|
68
74
|
|
|
69
75
|
Then verify your fleet:
|
|
70
76
|
|
|
@@ -72,12 +78,17 @@ Then verify your fleet:
|
|
|
72
78
|
tickmarkr doctor # probes installed adapters, herdr, auth; prints the capability matrix
|
|
73
79
|
```
|
|
74
80
|
|
|
81
|
+
**Expect `doctor` (and `init`, which runs it) to take a while on first run:** auth detection is
|
|
82
|
+
one real, short LLM call per configured model per installed CLI — honest, but not instant. A
|
|
83
|
+
machine with three CLIs and several models each can take a minute or more; each probe is capped
|
|
84
|
+
at 30s.
|
|
85
|
+
|
|
75
86
|
## Quickstart (5 minutes)
|
|
76
87
|
|
|
77
88
|
```bash
|
|
78
|
-
tickmarkr init # guided setup + doctor; scaffolds .drovr/config.yaml and
|
|
79
|
-
# edit
|
|
80
|
-
tickmarkr compile
|
|
89
|
+
tickmarkr init # guided setup + doctor; scaffolds .drovr/config.yaml and tickmarkr.spec.md
|
|
90
|
+
# edit tickmarkr.spec.md # the native spec template, marked <!-- drovr:spec -->
|
|
91
|
+
tickmarkr compile tickmarkr.spec.md # spec → .drovr/graph.json (fails without acceptance criteria)
|
|
81
92
|
tickmarkr plan # dry-run routing table + cost estimate + floor lints
|
|
82
93
|
tickmarkr run # execute the graph (--concurrency N --driver herdr|subprocess --route-strict)
|
|
83
94
|
tickmarkr report <runId> --md # engagement record in Markdown
|
|
@@ -312,11 +323,11 @@ produced the graph. Upstream, these front-ends exist:
|
|
|
312
323
|
|
|
313
324
|
| Spec source | Support | How |
|
|
314
325
|
|---|---|---|
|
|
315
|
-
| **
|
|
326
|
+
| **tickmarkr spec** (native format) | Native/default | `tickmarkr init` template with the `<!-- drovr:spec -->` marker |
|
|
316
327
|
| **Spec Kit** feature dir | Native | dir containing `tasks.md` |
|
|
317
328
|
| **GSD** phase dir | Native | dir containing `*-PLAN.md` |
|
|
318
329
|
| **Markdown PRD** | Native (universal adapter) | any `.md` with tasks + acceptance criteria |
|
|
319
|
-
| OpenSpec, BMAD, Superpowers, others | Not yet | render to a
|
|
330
|
+
| OpenSpec, BMAD, Superpowers, others | Not yet | render to a tickmarkr spec (the `<!-- drovr:spec -->` marker plus the full Task surface), or contribute a front-end: one parser module behind `src/compile/index.ts`, same pattern as `gsd.ts` |
|
|
320
331
|
|
|
321
332
|
Compile fails loudly on anything it can't recognize (`--type native|speckit|prd|gsd` to force).
|
|
322
333
|
|
|
@@ -353,8 +364,8 @@ Untyped acceptance strings in existing specs compile as `oracle: judge` + warnin
|
|
|
353
364
|
|
|
354
365
|
Cloning this repo gives Claude Code project skills under `.claude/skills/`:
|
|
355
366
|
|
|
356
|
-
- **`/
|
|
357
|
-
- **`/
|
|
367
|
+
- **`/tickmarkr-loop`** — the SDD-agnostic loop: compile one spec, read the plan, run from the journal, merge green work, and commit only the Markdown execution record beside that spec. For GSD bookkeeping, use `/tickmarkr-auto`.
|
|
368
|
+
- **`/tickmarkr-auto`** — the **GSD binding**: run remaining GSD milestone phases autonomously with tickmarkr as the
|
|
358
369
|
executor (plans → compile → dry-run → run → merge → sync-back). Bindings are per-SDD by nature — the
|
|
359
370
|
sync-back writes that methodology's artifacts; other SDDs need their own thin binding, not this one.
|
|
360
371
|
- **`/overseer`** — supervise an autonomous tickmarkr engagement from a Herdr workspace: two-tier visible hierarchy,
|
|
@@ -49,7 +49,7 @@ export const cursorAgent = {
|
|
|
49
49
|
// print-only). Doctor names the dialog; the daemon auto-answers it via trustDialog once per slot.
|
|
50
50
|
trust: () => ({
|
|
51
51
|
status: "action-required",
|
|
52
|
-
command: 'accept the cursor-agent "Workspace Trust Required" dialog (Enter) —
|
|
52
|
+
command: 'accept the cursor-agent "Workspace Trust Required" dialog (Enter) — tickmarkr auto-answers once per slot',
|
|
53
53
|
}),
|
|
54
54
|
trustDialog: CURSOR_TRUST_DIALOG,
|
|
55
55
|
// v1.5 MODEL-01: fail OPEN to [] (advisory detection, unlike gates). --list-models may touch the
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { MODEL_ID_RE } from "./types.js";
|
|
2
2
|
// date the tiers seeds were last live-verified (Phase 6/7 reseed) — surfaced when an adapter has no list surface.
|
|
3
3
|
export const SEED_STAMPED = "2026-07-09";
|
|
4
|
-
// knowledge past this age gets a "rerun
|
|
4
|
+
// knowledge past this age gets a "rerun tickmarkr doctor" nudge (BLOCKED_POLL_MS-style named constant).
|
|
5
5
|
export const MODEL_STALE_DAYS = 30;
|
|
6
6
|
const DAY_MS = 86400000;
|
|
7
7
|
// cursor-agent 2026.07.08 reports 193 mostly-parameterized ids (e.g. gpt-5.3-codex-high-fast); filter the `auto`
|
|
@@ -26,7 +26,7 @@ export function modelLints(cfg, health, adapters) {
|
|
|
26
26
|
const detected = h?.models ?? []; // MANDATORY default: pre-v1.5 files lack a populated models array
|
|
27
27
|
if (detected.length === 0) {
|
|
28
28
|
if (h?.installed)
|
|
29
|
-
lints.push(`${id}: no detection data — run
|
|
29
|
+
lints.push(`${id}: no detection data — run tickmarkr doctor`);
|
|
30
30
|
continue; // no data to diff or age
|
|
31
31
|
}
|
|
32
32
|
const configured = Object.keys(cfg.tiers[id].models);
|
|
@@ -45,7 +45,7 @@ export function modelLints(cfg, health, adapters) {
|
|
|
45
45
|
if (at) {
|
|
46
46
|
const days = Math.floor((Date.now() - Date.parse(at)) / DAY_MS); // completed days — never overstate age
|
|
47
47
|
if (days >= MODEL_STALE_DAYS)
|
|
48
|
-
lints.push(`${id}: model knowledge is ${days} days old — rerun
|
|
48
|
+
lints.push(`${id}: model knowledge is ${days} days old — rerun tickmarkr doctor`);
|
|
49
49
|
}
|
|
50
50
|
}
|
|
51
51
|
return lints;
|
package/dist/adapters/prompt.js
CHANGED
|
@@ -3,7 +3,7 @@ import { join } from "node:path";
|
|
|
3
3
|
import { renderAcceptanceItem } from "../graph/schema.js";
|
|
4
4
|
export function buildTaskPrompt(task, feedback = "", nonce = "") {
|
|
5
5
|
const list = (xs) => xs.map((x) => `- ${x}`).join("\n");
|
|
6
|
-
return `You are an autonomous coding worker dispatched by
|
|
6
|
+
return `You are an autonomous coding worker dispatched by tickmarkr into an isolated git worktree.
|
|
7
7
|
|
|
8
8
|
## Task ${task.id}: ${task.title}
|
|
9
9
|
Goal: ${task.goal}
|
|
@@ -40,7 +40,7 @@ export async function approve(argv, cwd = process.cwd()) {
|
|
|
40
40
|
via: "cli",
|
|
41
41
|
...(capPark ? { release: ATTEMPT_CAP_RELEASE } : {}),
|
|
42
42
|
});
|
|
43
|
-
return `approved ${taskId} in ${runId} — by ${by}; run \`
|
|
43
|
+
return `approved ${taskId} in ${runId} — by ${by}; run \`tickmarkr resume ${runId}\` to dispatch it`;
|
|
44
44
|
}
|
|
45
45
|
// hand-parsed argv — no CLI framework (house style). Flags --by <name> and --reason <text>; positionals
|
|
46
46
|
// are runId then taskId. Throws usage on missing positionals (mirrors resume.ts/unlock.ts).
|
|
@@ -53,12 +53,12 @@ function parseArgs(argv) {
|
|
|
53
53
|
if (a === "--by") {
|
|
54
54
|
by = argv[++i];
|
|
55
55
|
if (!by)
|
|
56
|
-
throw new Error("usage:
|
|
56
|
+
throw new Error("usage: tickmarkr approve <run-id> <task-id> [--by <name>] [--reason <text>]");
|
|
57
57
|
}
|
|
58
58
|
else if (a === "--reason") {
|
|
59
59
|
reason = argv[++i];
|
|
60
60
|
if (!reason)
|
|
61
|
-
throw new Error("usage:
|
|
61
|
+
throw new Error("usage: tickmarkr approve <run-id> <task-id> [--by <name>] [--reason <text>]");
|
|
62
62
|
}
|
|
63
63
|
else {
|
|
64
64
|
positionals.push(a);
|
|
@@ -66,7 +66,7 @@ function parseArgs(argv) {
|
|
|
66
66
|
}
|
|
67
67
|
const [runId, taskId] = positionals;
|
|
68
68
|
if (!runId || !taskId) {
|
|
69
|
-
throw new Error("usage:
|
|
69
|
+
throw new Error("usage: tickmarkr approve <run-id> <task-id> [--by <name>] [--reason <text>]");
|
|
70
70
|
}
|
|
71
71
|
return { runId, taskId, by: by ?? userInfo().username, reason };
|
|
72
72
|
}
|
|
@@ -11,7 +11,7 @@ export async function compile(argv, cwd = process.cwd()) {
|
|
|
11
11
|
});
|
|
12
12
|
const src = positionals[0];
|
|
13
13
|
if (!src)
|
|
14
|
-
throw new Error("usage:
|
|
14
|
+
throw new Error("usage: tickmarkr compile <spec-dir-or-md> [--type speckit|prd|gsd|native]");
|
|
15
15
|
// resolve against the target repo, not the process cwd (the CLI test passes a tmp repo)
|
|
16
16
|
const g = compileSource(isAbsolute(src) ? src : join(cwd, src), values.type, cwd);
|
|
17
17
|
// HARD-01: a compile clobbering a running daemon's graph.json is the same last-write-wins race
|
|
@@ -1,10 +1,40 @@
|
|
|
1
|
+
import { writeFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
1
3
|
import { allAdapters, probeAll, probeModels, servableExclusions, servabilityLine, writeDoctor } from "../../adapters/registry.js";
|
|
4
|
+
import { drovrDir } from "../../graph/graph.js";
|
|
2
5
|
import { modelLints, suggestOverlay } from "../../adapters/model-lints.js";
|
|
3
6
|
import { loadConfig } from "../../config/config.js";
|
|
4
7
|
import { HerdrDriver } from "../../drivers/herdr.js";
|
|
5
8
|
import { disallowedBy, excludedChannels, exclusionLine, preferRanks } from "../../route/preference.js";
|
|
9
|
+
// same gate + palette as status's audit-ledger frame: styled only on a real TTY, plain otherwise
|
|
10
|
+
const visual = () => process.stdout.isTTY === true && process.env.NO_COLOR === undefined;
|
|
11
|
+
const color = (text, code) => `\x1b[${code}m${text}\x1b[0m`;
|
|
12
|
+
// TTY-only line pass: color the verdict symbols, dim the chrome (section titles, parentheticals,
|
|
13
|
+
// n/a rows). Padding is untouched — every rewrite swaps equal-width text or wraps it in ANSI.
|
|
14
|
+
function stylize(out) {
|
|
15
|
+
if (!visual())
|
|
16
|
+
return out;
|
|
17
|
+
const rule = color("─".repeat(Math.min(process.stdout.columns ?? 80, 100)), 2);
|
|
18
|
+
return out.split("\n").map((line) => {
|
|
19
|
+
if (line.startsWith("tickmarkr doctor"))
|
|
20
|
+
return ` ${color("✓", 32)} ${color("tickmarkr", 1)} doctor ${color("· capability matrix", 2)}\n${rule}`;
|
|
21
|
+
if (/^(workspace trust|model status|model drift)/.test(line))
|
|
22
|
+
return color(line, 2);
|
|
23
|
+
return line
|
|
24
|
+
.replace(/^(\s+)✓ /, (_, s) => `${s}${color("✓", 32)} `)
|
|
25
|
+
.replace(/^(\s+)✗ /, (_, s) => `${s}${color("✗", 31)} `)
|
|
26
|
+
.replace(/^(\s+)! /, (_, s) => `${s}${color("!", 33)} `)
|
|
27
|
+
.replace(/^(\s+)= (.*)$/, (_, s, rest) => `${s}${color(`= ${rest}`, 2)}`)
|
|
28
|
+
.replace(/(\(auth assumed; verified at dispatch \(failover on auth\/quota errors\)\))/, (m) => color(m, 2))
|
|
29
|
+
.replace(/\bauthed\b(?!:)/, color("authed", 32))
|
|
30
|
+
.replace(/\bunauthed:/, color("unauthed:", 33));
|
|
31
|
+
}).join("\n");
|
|
32
|
+
}
|
|
6
33
|
export async function doctor(_argv, cwd = process.cwd(), adapters = allAdapters()) {
|
|
7
34
|
const cfg = loadConfig(cwd);
|
|
35
|
+
// stderr, live: auth probes are real LLM calls (up to 30s per configured model) and the CLI
|
|
36
|
+
// otherwise prints nothing until the end — silence here reads as a hang (v1.33.1)
|
|
37
|
+
console.error("probing installed agent CLIs — one short LLM call per configured model, may take a minute...");
|
|
8
38
|
const health = await probeAll(adapters);
|
|
9
39
|
// MODEL-02: detect models where the adapter exposes a list surface, BEFORE writing doctor.json (write once, below).
|
|
10
40
|
// Fail OPEN — the inverse of gates' fail-closed: detection is advisory, so a broken list surface NEVER fails doctor.
|
|
@@ -67,8 +97,20 @@ export async function doctor(_argv, cwd = process.cwd(), adapters = allAdapters(
|
|
|
67
97
|
if (servable.length)
|
|
68
98
|
rows.push(` ! ${servabilityLine(servable)}`);
|
|
69
99
|
// MODEL-05/06: print-only drift fragment; advisory, whole-line-commented additions, drovr NEVER applies it.
|
|
100
|
+
// TTY gets a one-line summary + the fragment as a file (the full dump drowned everything else,
|
|
101
|
+
// v1.33.1 onboarding); machine/CI surface keeps the inline dump — layout is pinned by tests.
|
|
70
102
|
const frag = suggestOverlay(cfg, health, adapters);
|
|
71
|
-
|
|
103
|
+
let drift = "";
|
|
104
|
+
if (frag) {
|
|
105
|
+
if (visual()) {
|
|
106
|
+
const overlayPath = join(drovrDir(cwd), "doctor-overlay.yaml");
|
|
107
|
+
writeFileSync(overlayPath, frag);
|
|
108
|
+
drift = `\nmodel drift: unclassified models detected — paste-ready overlay written to ${overlayPath} (advisory; tickmarkr never applies it)`;
|
|
109
|
+
}
|
|
110
|
+
else {
|
|
111
|
+
drift = `\nmodel drift — paste-ready overlay (advisory; tickmarkr never applies):\n${frag}`;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
72
114
|
// T4: model-status table — one row per CLASSIFIED model (tiers config) with tier, auth verdict
|
|
73
115
|
// (reason + date when unauthed), operator-deny flag, and prefer rank across the routing map.
|
|
74
116
|
// Unclassified listed models (detected via listModels but never tiered) compress to one count line
|
|
@@ -99,5 +141,5 @@ export async function doctor(_argv, cwd = process.cwd(), adapters = allAdapters(
|
|
|
99
141
|
return rows;
|
|
100
142
|
});
|
|
101
143
|
const modelSummary = modelStatus.length ? `\nmodel status:\n${modelStatus.join("\n")}` : "";
|
|
102
|
-
return `tickmarkr doctor — capability matrix:\n${rows.join("\n")}${modelSummary}${drift}\nwrote .drovr/doctor.json
|
|
144
|
+
return stylize(`tickmarkr doctor — capability matrix:\n${rows.join("\n")}${modelSummary}${drift}\nwrote .drovr/doctor.json`);
|
|
103
145
|
}
|
|
@@ -21,7 +21,7 @@ export async function plan(_argv, cwd = process.cwd(), adapters = allAdapters())
|
|
|
21
21
|
// output byte-identical to today.
|
|
22
22
|
const profile = loadRoutingProfile(cwd, cfg, { preview: true });
|
|
23
23
|
let deviations = 0;
|
|
24
|
-
const lines = [`
|
|
24
|
+
const lines = [`tickmarkr plan — dry run (${channels.length} channels available)`, ""];
|
|
25
25
|
const excluded = excludedChannels(cfg, adapters, health);
|
|
26
26
|
if (excluded.length)
|
|
27
27
|
lines.push(exclusionLine(excluded), "");
|
|
@@ -38,7 +38,7 @@ export async function plan(_argv, cwd = process.cwd(), adapters = allAdapters())
|
|
|
38
38
|
if (cached) {
|
|
39
39
|
const age = doctorAgeMs(cwd);
|
|
40
40
|
if (age !== null && age > DOCTOR_STALE_MS) {
|
|
41
|
-
lines.push(`doctor.json is ${Math.floor(age / 3_600_000)}h old — run '
|
|
41
|
+
lines.push(`doctor.json is ${Math.floor(age / 3_600_000)}h old — run 'tickmarkr doctor' to refresh (servability/auth may have changed)`, "");
|
|
42
42
|
}
|
|
43
43
|
}
|
|
44
44
|
// HYG-07(a)/T2 pin honesty: append the drop reason when a pin/miss message names a filtered channel —
|
|
@@ -32,7 +32,7 @@ function show(cwd) {
|
|
|
32
32
|
const cursor = readProfileCursor(cwd);
|
|
33
33
|
const p = loadRoutingProfile(cwd, cfg, { preview: true });
|
|
34
34
|
const header = [
|
|
35
|
-
`
|
|
35
|
+
`tickmarkr profile`,
|
|
36
36
|
` routing.learned: ${cfg.routing.learned}${cfg.routing.learned === "off" ? " (preview — routing is inert)" : ""}`,
|
|
37
37
|
` runs window: ${RUNS_WINDOW}`,
|
|
38
38
|
...(cursor ? [` reset cursor: ${cursor}`] : []),
|
|
@@ -293,9 +293,9 @@ export async function report(argv, cwd = process.cwd()) {
|
|
|
293
293
|
options: { md: { type: "boolean" } },
|
|
294
294
|
allowPositionals: true,
|
|
295
295
|
});
|
|
296
|
-
const runId = positionals[0] ?? Journal.latestRunId(cwd);
|
|
296
|
+
const runId = positionals[0] ?? Journal.latestRunId(cwd, { withJournal: true });
|
|
297
297
|
if (!runId)
|
|
298
|
-
throw new Error("no runs found — usage:
|
|
298
|
+
throw new Error("no runs found — usage: tickmarkr report <run-id> [--md]");
|
|
299
299
|
const j = Journal.open(cwd, runId);
|
|
300
300
|
const events = j.read();
|
|
301
301
|
if (values.md) {
|
|
@@ -5,7 +5,7 @@ import { formatJournalNarration } from "../../run/journal.js";
|
|
|
5
5
|
export async function resume(argv, cwd = process.cwd()) {
|
|
6
6
|
const runId = argv[0];
|
|
7
7
|
if (!runId)
|
|
8
|
-
throw new Error("usage:
|
|
8
|
+
throw new Error("usage: tickmarkr resume <run-id>");
|
|
9
9
|
const s = await runDaemon(cwd, { runId, resume: true, driver: pickDriver(loadConfig(cwd)), narrate: (event) => console.log(formatJournalNarration(event)) });
|
|
10
10
|
return `resumed ${s.runId} — ${formatSummary(s)}`;
|
|
11
11
|
}
|
|
@@ -10,7 +10,7 @@ export async function scope(argv, cwd = process.cwd(), adapters = allAdapters(),
|
|
|
10
10
|
allowPositionals: true,
|
|
11
11
|
});
|
|
12
12
|
if (positionals.length !== 1)
|
|
13
|
-
throw new Error("usage:
|
|
13
|
+
throw new Error("usage: tickmarkr scope <intent-file> [--force]");
|
|
14
14
|
const source = positionals[0];
|
|
15
15
|
const intentFile = isAbsolute(source) ? source : join(cwd, source);
|
|
16
16
|
const result = await scopeIntent(intentFile, cwd, {
|
|
@@ -39,21 +39,28 @@ const gateBox = (state, unicode) => {
|
|
|
39
39
|
return state === "pass" ? "✓" : state === "fail" ? "✗" : state === "skip" ? "·" : "☐";
|
|
40
40
|
return state === "pass" ? "[x]" : state === "fail" ? "[!]" : state === "skip" ? "." : "[ ]";
|
|
41
41
|
};
|
|
42
|
-
const
|
|
42
|
+
const gateStates = (task, events) => {
|
|
43
43
|
const outcomes = new Map();
|
|
44
44
|
const start = attemptStartIdx(events, task.id);
|
|
45
45
|
if (start >= 0) {
|
|
46
46
|
for (const e of events.slice(start)) {
|
|
47
47
|
if (e.taskId !== task.id || e.event !== "gate-result" || typeof e.data.gate !== "string")
|
|
48
48
|
continue;
|
|
49
|
-
if (e.data.
|
|
49
|
+
if (e.data.skipped === true)
|
|
50
|
+
outcomes.set(e.data.gate, "skip");
|
|
51
|
+
else if (e.data.pass === true)
|
|
50
52
|
outcomes.set(e.data.gate, "pass");
|
|
51
53
|
else if (e.data.pass === false)
|
|
52
54
|
outcomes.set(e.data.gate, "fail");
|
|
53
55
|
}
|
|
54
56
|
}
|
|
55
|
-
return GATE_NAMES.map((gate) =>
|
|
57
|
+
return GATE_NAMES.map((gate) => task.gates.includes(gate) ? outcomes.get(gate) ?? "open" : "skip");
|
|
56
58
|
};
|
|
59
|
+
// verdict semantics only: pass green, fail red, skip/open dim chrome — everything else stays quiet
|
|
60
|
+
const GATE_STATE_COLOR = { pass: 32, fail: 31, skip: 2, open: 2 };
|
|
61
|
+
const gateChain = (states, unicode) => GATE_NAMES.map((gate, i) => color(`${GATE_KEYS[gate]}${gateBox(states[i], unicode)}`, GATE_STATE_COLOR[states[i]], unicode)).join(" ");
|
|
62
|
+
// plain (uncolored) chip width for column math — ANSI codes have zero display width
|
|
63
|
+
const gateChainWidth = (unicode) => GATE_NAMES.reduce((w, gate) => w + GATE_KEYS[gate].length + (unicode ? 1 : 3) + 1, -1);
|
|
57
64
|
const shortGoal = (goal, max) => {
|
|
58
65
|
const clause = goal.split(/[,;.?!]/, 1)[0].trim();
|
|
59
66
|
if (clause.length <= max)
|
|
@@ -105,7 +112,7 @@ const liveness = (events) => {
|
|
|
105
112
|
};
|
|
106
113
|
const renderFrame = (cwd) => {
|
|
107
114
|
const g = loadGraph(cwd);
|
|
108
|
-
const runId = Journal.latestRunId(cwd);
|
|
115
|
+
const runId = Journal.latestRunId(cwd, { withJournal: true });
|
|
109
116
|
const assignments = new Map();
|
|
110
117
|
let replayed = null;
|
|
111
118
|
let events = [];
|
|
@@ -130,24 +137,63 @@ const renderFrame = (cwd) => {
|
|
|
130
137
|
const waiting = new Set(pendingTasks(effective).map((t) => t.id));
|
|
131
138
|
const unicode = visual();
|
|
132
139
|
const divider = unicode ? " · " : " / ";
|
|
133
|
-
const
|
|
140
|
+
const width = process.stdout.columns ?? 120;
|
|
141
|
+
const done = effective.tasks.filter((t) => t.status === "done").length;
|
|
142
|
+
const cells = g.tasks.map((t) => {
|
|
134
143
|
const st = replayed?.get(t.id) ?? t.status;
|
|
135
144
|
const label = starved.has(t.id) ? " starved" : waiting.has(t.id) ? " dep-waiting" : "";
|
|
136
145
|
const channel = assignments.get(t.id) ?? "-";
|
|
137
146
|
const assignCol = contexts.has(t.id) ? `${channel}${divider}ctx ${contexts.get(t.id)}` : channel;
|
|
138
|
-
|
|
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}`;
|
|
147
|
+
return { t, st, label, assignCol, states: gateStates(t, events) };
|
|
144
148
|
});
|
|
145
|
-
const
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
149
|
+
const statusColor = (st) => st === "done" ? 32 : st === "failed" ? 31 : st === "human" ? 33 : 36;
|
|
150
|
+
if (!unicode) {
|
|
151
|
+
// machine/CI surface — layout unchanged (pipes, greps, and the golden pins depend on it)
|
|
152
|
+
const rows = cells.map(({ t, st, label, assignCol, states }) => {
|
|
153
|
+
const chain = gateChain(states, false);
|
|
154
|
+
const prefix = ` ${taskBox(st, false)} ${t.id} `;
|
|
155
|
+
const suffix = ` ${chain} ${String(st)}${label} ${assignCol}`;
|
|
156
|
+
return `${prefix}${shortGoal(t.goal, Math.max(0, width - prefix.length - suffix.length))}${suffix}`;
|
|
157
|
+
});
|
|
158
|
+
const header = runId
|
|
159
|
+
? `tickmarkr status${divider}run ${runId}${divider}${liveness(events).replaceAll(" · ", divider)}${divider}${done}/${g.tasks.length} done`
|
|
160
|
+
: `tickmarkr status${divider}no runs yet${divider}${done}/${g.tasks.length} done`;
|
|
161
|
+
const legend = ` gates: ${GATE_NAMES.map((gate) => `${GATE_KEYS[gate]} ${gate}`).join(divider)}`;
|
|
162
|
+
return [header, legend, ...rows].join("\n");
|
|
163
|
+
}
|
|
164
|
+
// TTY: audit-ledger frame — bold brand mark, dim chrome, semantic color only on verdicts,
|
|
165
|
+
// completion gauge, and column-aligned rows (pad plain text FIRST, colorize after: ANSI has
|
|
166
|
+
// zero display width and would corrupt padEnd math)
|
|
167
|
+
const dim = (s) => color(s, 2, true);
|
|
168
|
+
const dot = dim(" · ");
|
|
169
|
+
const anyFailed = cells.some((c) => c.st === "failed");
|
|
170
|
+
const gaugeCells = 10;
|
|
171
|
+
const fill = g.tasks.length ? Math.round((done / g.tasks.length) * gaugeCells) : 0;
|
|
172
|
+
const gauge = (fill ? color("█".repeat(fill), anyFailed ? 31 : 32, true) : "") + (fill < gaugeCells ? dim("░".repeat(gaugeCells - fill)) : "");
|
|
173
|
+
const live = liveness(events)
|
|
174
|
+
.replace(/\bdead\b/, color("dead", 31, true))
|
|
175
|
+
.replace(/\balive\b/, color("alive", 32, true))
|
|
176
|
+
.replaceAll(" · ", dot);
|
|
177
|
+
const tally = `${done}/${g.tasks.length} done`;
|
|
178
|
+
const header = ` ${color("✓", 32, true)} ${color("tickmarkr", 1, true)}${dot}` +
|
|
179
|
+
(runId ? `run ${runId}${dot}${live}${dot}` : `no runs yet${dot}`) +
|
|
180
|
+
`${gauge} ${done === g.tasks.length && g.tasks.length > 0 ? color(tally, 32, true) : tally}`;
|
|
181
|
+
const rule = dim("─".repeat(Math.min(width, 100)));
|
|
182
|
+
const legend = dim(` gates: ${GATE_NAMES.map((gate) => `${GATE_KEYS[gate]} ${gate}`).join(" · ")}`);
|
|
183
|
+
const idW = Math.max(...cells.map((c) => c.t.id.length), 2);
|
|
184
|
+
const stW = Math.max(...cells.map((c) => (String(c.st) + c.label).length));
|
|
185
|
+
const chainW = gateChainWidth(true);
|
|
186
|
+
const assignW = Math.max(...cells.map((c) => c.assignCol.length));
|
|
187
|
+
const goalW = Math.max(8, width - (5 + idW) - 2 - chainW - 2 - stW - 2 - assignW);
|
|
188
|
+
const rows = cells.map(({ t, st, label, assignCol, states }) => {
|
|
189
|
+
const box = color(taskBox(st, true), st === "done" ? 32 : st === "failed" ? 31 : st === "human" ? 33 : 2, true);
|
|
190
|
+
const goal = shortGoal(t.goal, goalW).padEnd(goalW);
|
|
191
|
+
const statusCell = color(String(st), statusColor(st), true) +
|
|
192
|
+
(label ? (label === " starved" ? color(label, 31, true) : dim(label)) : "") +
|
|
193
|
+
" ".repeat(stW - (String(st) + label).length);
|
|
194
|
+
return ` ${box} ${t.id.padEnd(idW)} ${goal} ${gateChain(states, true)} ${statusCell} ${dim(assignCol)}`;
|
|
195
|
+
});
|
|
196
|
+
return [header, rule, legend, ...rows].join("\n");
|
|
151
197
|
};
|
|
152
198
|
export async function status(argv, cwd = process.cwd(), opts = {}) {
|
|
153
199
|
if (!argv.includes("--watch"))
|
|
@@ -161,7 +207,7 @@ export async function status(argv, cwd = process.cwd(), opts = {}) {
|
|
|
161
207
|
for (let i = 0; i < iterations; i++) {
|
|
162
208
|
const frame = renderFrame(cwd);
|
|
163
209
|
if (tty)
|
|
164
|
-
process.stdout.write(`\x1b[2J\x1b[H${frame}`);
|
|
210
|
+
process.stdout.write(`\x1b[2J\x1b[H${frame}\n\x1b[2m watching · refresh ${REFRESH_MS / 1000}s · ^C to quit\x1b[0m`);
|
|
165
211
|
else
|
|
166
212
|
process.stdout.write(frame + sep);
|
|
167
213
|
if (bounded)
|
package/dist/compile/gsd.js
CHANGED
|
@@ -92,7 +92,7 @@ function compileOne(file, storedPath) {
|
|
|
92
92
|
const truths = strings(fm.must_haves?.truths);
|
|
93
93
|
const acceptance = [...dones, ...truths].filter(Boolean);
|
|
94
94
|
if (!acceptance.length) {
|
|
95
|
-
throw new CompileError(`${file} has no acceptance criteria — every
|
|
95
|
+
throw new CompileError(`${file} has no acceptance criteria — every tickmarkr task needs them.\n` +
|
|
96
96
|
`GSD source: add <done> lines to the plan's tasks, or must_haves.truths in its frontmatter.`);
|
|
97
97
|
}
|
|
98
98
|
// @-refs from the <context> block only; keep repo-relative paths (no $VAR, ~, absolute, or ..-escape)
|
|
@@ -151,7 +151,7 @@ function compileOne(file, storedPath) {
|
|
|
151
151
|
}
|
|
152
152
|
export function compileGsd(src, root) {
|
|
153
153
|
if (!existsSync(src))
|
|
154
|
-
throw new CompileError(`${src} does not exist — point
|
|
154
|
+
throw new CompileError(`${src} does not exist — point tickmarkr compile at a GSD phase directory or a *-PLAN.md`);
|
|
155
155
|
const isDir = statSync(src).isDirectory();
|
|
156
156
|
const files = isDir
|
|
157
157
|
? readdirSync(src)
|
|
@@ -160,7 +160,7 @@ export function compileGsd(src, root) {
|
|
|
160
160
|
.map((f) => join(src, f))
|
|
161
161
|
: [src];
|
|
162
162
|
if (!files.length) {
|
|
163
|
-
throw new CompileError(`no *-PLAN.md files in ${src} — point
|
|
163
|
+
throw new CompileError(`no *-PLAN.md files in ${src} — point tickmarkr compile at a GSD phase directory`);
|
|
164
164
|
}
|
|
165
165
|
// context[0] must be readable from an isolated worktree: store repo-relative, never absolute
|
|
166
166
|
const rel = (f) => {
|
package/dist/compile/native.js
CHANGED
|
@@ -135,7 +135,7 @@ export function compileNative(file) {
|
|
|
135
135
|
// v1.19 read-old/write-new: a plain-string acceptance item compiles as a judge oracle. This is the
|
|
136
136
|
// one-time nudge toward typed oracles (command/test/judge); PRD/Spec Kit/GSD stay silent (untouched).
|
|
137
137
|
if (plainCount > 0) {
|
|
138
|
-
console.warn(`
|
|
138
|
+
console.warn(`tickmarkr: ${plainCount} acceptance item${plainCount === 1 ? "" : "s"} in ${file} ${plainCount === 1 ? "is a plain string" : "are plain strings"} — compiled as judge oracle${plainCount === 1 ? "" : "s"}. Prefix with command:/test:/judge: to make the oracle explicit.`);
|
|
139
139
|
}
|
|
140
140
|
return result;
|
|
141
141
|
}
|
|
@@ -144,10 +144,10 @@ export function compileNative(file) {
|
|
|
144
144
|
// documented; the two example tasks illustrate plain vs deps+routing-hint, each with acceptance[].
|
|
145
145
|
export function specTemplate() {
|
|
146
146
|
return `<!-- tickmarkr:spec -->
|
|
147
|
-
#
|
|
147
|
+
# tickmarkr native spec
|
|
148
148
|
|
|
149
|
-
Your starting point for a
|
|
150
|
-
|
|
149
|
+
Your starting point for a tickmarkr native spec. Edit this file, then run:
|
|
150
|
+
tickmarkr compile tickmarkr.spec.md && tickmarkr plan && tickmarkr run
|
|
151
151
|
|
|
152
152
|
Each task is a "## Tn: Title" heading with "- field: value" bullets.
|
|
153
153
|
acceptance is required on every task (a nested list of observable outcomes).
|
package/dist/compile/speckit.js
CHANGED
|
@@ -7,7 +7,7 @@ const SUB_RE = /^\s+- (\w+):\s*(.+)$/;
|
|
|
7
7
|
export function compileSpecKit(dir) {
|
|
8
8
|
const path = join(dir, "tasks.md");
|
|
9
9
|
if (!existsSync(path)) {
|
|
10
|
-
throw new CompileError(`no tasks.md in ${dir} — point
|
|
10
|
+
throw new CompileError(`no tasks.md in ${dir} — point tickmarkr compile at a Spec Kit feature directory`);
|
|
11
11
|
}
|
|
12
12
|
const content = readFileSync(path, "utf8");
|
|
13
13
|
const drafts = [];
|
package/dist/config/config.js
CHANGED
|
@@ -269,7 +269,7 @@ export function loadConfig(repoRoot, opts = {}) {
|
|
|
269
269
|
return r.data;
|
|
270
270
|
}
|
|
271
271
|
export function configTemplate() {
|
|
272
|
-
return `#
|
|
272
|
+
return `# tickmarkr config overlay — merges over built-in defaults (repo beats global beats defaults)
|
|
273
273
|
# concurrency: 3
|
|
274
274
|
# driver: auto # auto | herdr | subprocess
|
|
275
275
|
# taskTimeoutMinutes: 30
|
|
@@ -279,9 +279,9 @@ export function configTemplate() {
|
|
|
279
279
|
# map: # per-shape routing; pin an exact CLI+model, or tier+prefer
|
|
280
280
|
# implement: { tier: mid, prefer: [cursor-agent, codex] }
|
|
281
281
|
# migration: { pin: { via: claude-code, model: fable } }
|
|
282
|
-
# floors: # advisory minimum tiers; '
|
|
282
|
+
# floors: # advisory minimum tiers; 'tickmarkr plan' lints violations
|
|
283
283
|
# migration: frontier
|
|
284
|
-
# learned: on # default ON (ROUTE-14); cold profile = exact v1.5 static routing, warms per workspace. Set 'off' to pin static routing; preview with '
|
|
284
|
+
# learned: on # default ON (ROUTE-14); cold profile = exact v1.5 static routing, warms per workspace. Set 'off' to pin static routing; preview with 'tickmarkr plan'
|
|
285
285
|
# learnedTuning: { halfLifeRuns: 5, availWeight: 0.05 } # optional; defaults byte-identical
|
|
286
286
|
# allow: { adapters: [claude-code, codex] } # optional fleet allowlist; presence activates even if empty (fail-closed)
|
|
287
287
|
# deny: { models: [codex:gpt-5.5] } # optional fleet denylist; deny beats allow on conflict
|
|
@@ -321,9 +321,9 @@ export function configTemplate() {
|
|
|
321
321
|
# dialog stalling unattended HEADLESS runs (live-verified on claude 2.1.205). Interactive TUIs still
|
|
322
322
|
# show a first-entry dialog — but it is the workspace TRUST dialog, not MCP config loading (live-drilled
|
|
323
323
|
# claude 2.1.206, 2026-07-10). There is NO CLI flag to pre-accept trust; the only store is claude's global
|
|
324
|
-
# ~/.claude.json keyed on the EXACT worktree path.
|
|
324
|
+
# ~/.claude.json keyed on the EXACT worktree path. tickmarkr does NOT write it (HYG-03 won't-fix, decision B:
|
|
325
325
|
# claude's own last-writer-wins persistence races any seed). Cost is ~one dismissal per worktree path,
|
|
326
|
-
# ever —
|
|
326
|
+
# ever — tickmarkr reuses stable worktree paths so the accept persists across runs; blocked-pane paging
|
|
327
327
|
# surfaces each first-time dialog.
|
|
328
328
|
`;
|
|
329
329
|
}
|
|
@@ -57,7 +57,7 @@ export class SubprocessDriver {
|
|
|
57
57
|
p.stdout.on("data", (d) => (s.buf = (s.buf + d).slice(-MAX_BUF)));
|
|
58
58
|
p.stderr.on("data", (d) => (s.buf = (s.buf + d).slice(-MAX_BUF)));
|
|
59
59
|
p.on("close", () => (s.exited = true));
|
|
60
|
-
p.on("error", (e) => { s.buf = (s.buf + `\n[
|
|
60
|
+
p.on("error", (e) => { s.buf = (s.buf + `\n[tickmarkr subprocess error] ${e}\n`).slice(-MAX_BUF); s.exited = true; });
|
|
61
61
|
}
|
|
62
62
|
async waitOutput(slot, pattern, timeoutMs, opts) {
|
|
63
63
|
const s = this.state(slot);
|
|
@@ -92,7 +92,7 @@ export class SubprocessDriver {
|
|
|
92
92
|
async notify(msg, _opts) {
|
|
93
93
|
if (_opts?.tier === "routine")
|
|
94
94
|
return;
|
|
95
|
-
console.log(`[
|
|
95
|
+
console.log(`[tickmarkr] ${msg}`);
|
|
96
96
|
}
|
|
97
97
|
async close(slot) {
|
|
98
98
|
const s = this.slots.get(slot.id);
|
package/dist/gates/baseline.js
CHANGED
|
@@ -71,8 +71,12 @@ export async function compareToBaseline(cwd, commands, baseline, enabled) {
|
|
|
71
71
|
const results = [];
|
|
72
72
|
for (const name of enabled) {
|
|
73
73
|
const cmd = commands[name];
|
|
74
|
-
if (!cmd)
|
|
75
|
-
|
|
74
|
+
if (!cmd) {
|
|
75
|
+
// nothing detected for this gate in the target repo — journal an explicit skip instead of
|
|
76
|
+
// vanishing silently (a lint gate with no lint script rendered as forever-open in status)
|
|
77
|
+
results.push({ gate: name, pass: true, details: `no ${name} command detected — skipped`, meta: { skipped: true } });
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
76
80
|
const r = await sh(cmd, cwd);
|
|
77
81
|
if (r.code === 0) {
|
|
78
82
|
results.push({ gate: name, pass: true, details: "exit 0" });
|
package/dist/graph/graph.js
CHANGED
|
@@ -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 \`
|
|
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) {
|
package/dist/plan/prompt.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export function scopePrompt(intent, repair) {
|
|
2
2
|
return `DROVR-JUDGE
|
|
3
|
-
You are drafting a
|
|
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
6
|
- start with <!-- tickmarkr:spec -->
|
package/dist/run/consult.js
CHANGED
|
@@ -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
|
|
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}
|
package/dist/run/daemon.js
CHANGED
|
@@ -90,7 +90,7 @@ export async function runDaemon(repoRoot, opts = {}) {
|
|
|
90
90
|
// show. driver.narrator is undefined on subprocess → no-op (subprocess spawns nothing). Swallowed:
|
|
91
91
|
// a failed-to-open or later-dead watch pane never affects the run.
|
|
92
92
|
try {
|
|
93
|
-
await driver.narrator?.(repoRoot, "
|
|
93
|
+
await driver.narrator?.(repoRoot, "tickmarkr status --watch", runId);
|
|
94
94
|
}
|
|
95
95
|
catch {
|
|
96
96
|
/* cosmetic-only — the run proceeds without a live surface */
|
|
@@ -138,7 +138,7 @@ export async function runDaemon(repoRoot, opts = {}) {
|
|
|
138
138
|
// merges are serialized — two concurrent `git merge`s in one worktree would corrupt each other
|
|
139
139
|
let mergeChain = Promise.resolve();
|
|
140
140
|
const mergeSerial = (taskBranch, t, gated) => {
|
|
141
|
-
const next = mergeChain.then(() => mergeTask(intWt, taskBranch, `
|
|
141
|
+
const next = mergeChain.then(() => mergeTask(intWt, taskBranch, `tickmarkr: merge ${t.id} ${t.title}`, gated));
|
|
142
142
|
mergeChain = next.catch(() => undefined);
|
|
143
143
|
return next;
|
|
144
144
|
};
|
|
@@ -152,7 +152,7 @@ export async function runDaemon(repoRoot, opts = {}) {
|
|
|
152
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 });
|
|
153
153
|
}
|
|
154
154
|
await reconcile({ spareLiveLlm: true }); // task-human is a terminal event — sweep, sparing sibling tasks' live LLM panes
|
|
155
|
-
await driver.notify(`
|
|
155
|
+
await driver.notify(`tickmarkr ${runId}: ${t.id} needs a human — ${reason}`, { tier: "attention" });
|
|
156
156
|
};
|
|
157
157
|
const execTask = async (t) => {
|
|
158
158
|
const startMs = Date.now();
|
|
@@ -251,7 +251,7 @@ export async function runDaemon(repoRoot, opts = {}) {
|
|
|
251
251
|
action: v.action, notes: v.notes,
|
|
252
252
|
...(v.excludeAdapter ? { excludeAdapter: v.excludeAdapter } : {}),
|
|
253
253
|
});
|
|
254
|
-
await driver.notify(`
|
|
254
|
+
await driver.notify(`tickmarkr ${runId}: ${t.id} consult verdict: ${v.action}`, { tier: "attention" });
|
|
255
255
|
if (v.action === "retry") {
|
|
256
256
|
feedback = v.notes || feedback;
|
|
257
257
|
return true;
|
|
@@ -385,7 +385,7 @@ export async function runDaemon(repoRoot, opts = {}) {
|
|
|
385
385
|
threshold: cfg.contextWarnTokens,
|
|
386
386
|
attempt,
|
|
387
387
|
});
|
|
388
|
-
await driver.notify(`
|
|
388
|
+
await driver.notify(`tickmarkr ${runId}: ${t.id} context ${usage.tokens} tokens ≥ ${cfg.contextWarnTokens}`, { tier: "attention" });
|
|
389
389
|
};
|
|
390
390
|
let finished;
|
|
391
391
|
let output;
|
|
@@ -448,7 +448,7 @@ export async function runDaemon(repoRoot, opts = {}) {
|
|
|
448
448
|
}
|
|
449
449
|
paged = true; // page once — the visible pane is the operator's to unblock; task timeout is the backstop
|
|
450
450
|
const why = st === "blocked" ? "is blocked on a prompt — approve in its pane" : "looks idle without finishing — check its pane";
|
|
451
|
-
await driver.notify(`
|
|
451
|
+
await driver.notify(`tickmarkr ${runId}: ${slot.name} ${why}`, { tier: "attention" });
|
|
452
452
|
}
|
|
453
453
|
// a dead pane or a false-positive marker display returns fast — sleep the unspent slice, never hot-spin
|
|
454
454
|
const spent = Date.now() - sliceStart;
|
|
@@ -499,7 +499,7 @@ export async function runDaemon(repoRoot, opts = {}) {
|
|
|
499
499
|
const next = failover("quota-failover");
|
|
500
500
|
journal.append("quota-failover", t.id, { from: channelKey(assignment), to: next ? channelKey(next) : null });
|
|
501
501
|
if (next) {
|
|
502
|
-
await driver.notify(`
|
|
502
|
+
await driver.notify(`tickmarkr ${runId}: ${t.id} quota failover`, { tier: "attention" });
|
|
503
503
|
// OBS-17 T2: the superseded slot's pane closes AT REROUTE TIME — it holds a throttled
|
|
504
504
|
// dead-end, not failure context; the next safe-point reconcile catches a missed close.
|
|
505
505
|
if (!keepForever) {
|
|
@@ -561,7 +561,7 @@ export async function runDaemon(repoRoot, opts = {}) {
|
|
|
561
561
|
});
|
|
562
562
|
}
|
|
563
563
|
}
|
|
564
|
-
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 } : {}) });
|
|
565
565
|
// v1.1 failover: never re-ask a reviewer channel that produced garbage for this task
|
|
566
566
|
if (g.gate === "review" && !g.pass && /unparseable/.test(g.details) && typeof g.meta?.reviewer === "string") {
|
|
567
567
|
badReviewers.push(g.meta.reviewer);
|
|
@@ -641,7 +641,7 @@ export async function runDaemon(repoRoot, opts = {}) {
|
|
|
641
641
|
feedback = results.filter((g) => !g.pass).map((g) => `${g.gate}: ${g.details}`).join("\n\n");
|
|
642
642
|
const step = r.ladder[Math.min(ladderIdx++, r.ladder.length - 1)];
|
|
643
643
|
journal.append("escalation", t.id, { step, attempt: attempt + 1 });
|
|
644
|
-
await driver.notify(`
|
|
644
|
+
await driver.notify(`tickmarkr ${runId}: ${t.id} escalation: ${step}`, { tier: "attention" });
|
|
645
645
|
if (step === "retry")
|
|
646
646
|
continue;
|
|
647
647
|
if (step === "escalate") {
|
|
@@ -711,7 +711,7 @@ export async function runDaemon(repoRoot, opts = {}) {
|
|
|
711
711
|
.sort(([a], [b]) => a.localeCompare(b))
|
|
712
712
|
.map(([root, count]) => `${count} blocked behind ${root}`)
|
|
713
713
|
.join(", ");
|
|
714
|
-
await driver.notify(`
|
|
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" });
|
|
715
715
|
return summary;
|
|
716
716
|
}
|
|
717
717
|
finally {
|
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
|
-
|
|
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
|
|
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) =>
|
|
17
|
-
p.on("close", (code) =>
|
|
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) {
|
package/dist/run/journal.d.ts
CHANGED
|
@@ -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
|
|
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[];
|
package/dist/run/journal.js
CHANGED
|
@@ -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
|
-
|
|
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))
|
|
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() {
|