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.
- package/README.md +112 -20
- 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/init.js +79 -8
- 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.d.ts +0 -4
- package/dist/cli/commands/status.js +116 -59
- package/dist/cli/index.d.ts +1 -1
- package/dist/cli/index.js +4 -4
- package/dist/compile/gsd.js +3 -3
- package/dist/compile/native.js +6 -6
- package/dist/compile/speckit.js +1 -1
- package/dist/config/config.js +5 -5
- package/dist/drivers/herdr.d.ts +5 -0
- package/dist/drivers/herdr.js +107 -16
- package/dist/drivers/subprocess.js +2 -2
- package/dist/drivers/types.js +4 -3
- package/dist/gates/baseline.js +6 -2
- package/dist/graph/graph.js +1 -1
- package/dist/plan/prompt.js +2 -2
- package/dist/plan/scope.js +4 -4
- package/dist/run/consult.js +1 -1
- package/dist/run/daemon.js +13 -21
- package/dist/run/git.js +26 -5
- package/dist/run/journal.d.ts +3 -1
- package/dist/run/journal.js +8 -2
- package/dist/run/reconcile.js +1 -0
- package/package.json +4 -4
- package/skills/tickmarkr-auto/SKILL.md +41 -0
- package/skills/tickmarkr-loop/SKILL.md +39 -0
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`
|
|
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,34 +52,76 @@ 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
|
-
|
|
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
|
-
|
|
59
|
-
`opencode`, `grok`, or `pi`) authenticated through its own login.
|
|
58
|
+
## Install
|
|
60
59
|
|
|
61
60
|
```bash
|
|
62
|
-
npm
|
|
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.
|
|
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.
|
|
74
|
+
|
|
67
75
|
Then verify your fleet:
|
|
68
76
|
|
|
69
77
|
```bash
|
|
70
78
|
tickmarkr doctor # probes installed adapters, herdr, auth; prints the capability matrix
|
|
71
79
|
```
|
|
72
80
|
|
|
73
|
-
|
|
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
|
+
|
|
86
|
+
## Quickstart (5 minutes)
|
|
74
87
|
|
|
75
88
|
```bash
|
|
76
|
-
tickmarkr init # guided setup + doctor; scaffolds .drovr/config.yaml and
|
|
77
|
-
# edit
|
|
78
|
-
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)
|
|
79
92
|
tickmarkr plan # dry-run routing table + cost estimate + floor lints
|
|
80
93
|
tickmarkr run # execute the graph (--concurrency N --driver herdr|subprocess --route-strict)
|
|
81
94
|
tickmarkr report <runId> --md # engagement record in Markdown
|
|
82
95
|
```
|
|
83
96
|
|
|
97
|
+
That's the whole loop: `init` scaffolds, you write the spec's tasks and `acceptance[]`, `compile`
|
|
98
|
+
turns it into a task graph, `plan` shows you the routing/cost dry run, `run` dispatches field teams
|
|
99
|
+
and gates every result, and `report` writes the engagement record. Nothing merges to your mainline
|
|
100
|
+
without your sign-off.
|
|
101
|
+
|
|
102
|
+
## Agent-ready repos: `tickmarkr init --agent`
|
|
103
|
+
|
|
104
|
+
`tickmarkr init --agent` composes with the base `init` above and additionally installs the
|
|
105
|
+
consumer-facing skills (`tickmarkr-loop`, `tickmarkr-auto`) that ship in the npm tarball, so a
|
|
106
|
+
coding agent working in your repo knows how to drive tickmarkr without reading its source:
|
|
107
|
+
|
|
108
|
+
```bash
|
|
109
|
+
tickmarkr init --agent # installs .claude/skills/tickmarkr-{loop,auto}, offers CLAUDE.md/AGENTS.md notes
|
|
110
|
+
tickmarkr init --agent --force # also overwrite skill files that already exist
|
|
111
|
+
tickmarkr init --agent --docs # also append the agent-docs section without an interactive prompt
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
Consent rules — every write is additive, never destructive:
|
|
115
|
+
|
|
116
|
+
- an existing `.claude/skills/tickmarkr-{loop,auto}/SKILL.md` is left untouched unless you answer
|
|
117
|
+
yes to a per-file prompt, or pass `--force`
|
|
118
|
+
- a short "tickmarkr" section (the loop commands + the invariants above) is appended to `CLAUDE.md`
|
|
119
|
+
— or `AGENTS.md` if that exists and `CLAUDE.md` doesn't — only after you say yes, or pass `--docs`;
|
|
120
|
+
it's wrapped in `<!-- tickmarkr:agent-docs begin/end -->` markers and never inserted twice
|
|
121
|
+
- declining a prompt still lets the rest of `init` complete
|
|
122
|
+
- non-interactive shells (no TTY) never prompt: missing skills are installed, everything else is
|
|
123
|
+
skipped, and the summary names exactly what was skipped and which flag would enable it
|
|
124
|
+
|
|
84
125
|
Supervise and finish:
|
|
85
126
|
|
|
86
127
|
```bash
|
|
@@ -282,11 +323,11 @@ produced the graph. Upstream, these front-ends exist:
|
|
|
282
323
|
|
|
283
324
|
| Spec source | Support | How |
|
|
284
325
|
|---|---|---|
|
|
285
|
-
| **
|
|
326
|
+
| **tickmarkr spec** (native format) | Native/default | `tickmarkr init` template with the `<!-- drovr:spec -->` marker |
|
|
286
327
|
| **Spec Kit** feature dir | Native | dir containing `tasks.md` |
|
|
287
328
|
| **GSD** phase dir | Native | dir containing `*-PLAN.md` |
|
|
288
329
|
| **Markdown PRD** | Native (universal adapter) | any `.md` with tasks + acceptance criteria |
|
|
289
|
-
| 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` |
|
|
290
331
|
|
|
291
332
|
Compile fails loudly on anything it can't recognize (`--type native|speckit|prd|gsd` to force).
|
|
292
333
|
|
|
@@ -323,8 +364,8 @@ Untyped acceptance strings in existing specs compile as `oracle: judge` + warnin
|
|
|
323
364
|
|
|
324
365
|
Cloning this repo gives Claude Code project skills under `.claude/skills/`:
|
|
325
366
|
|
|
326
|
-
- **`/
|
|
327
|
-
- **`/
|
|
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
|
|
328
369
|
executor (plans → compile → dry-run → run → merge → sync-back). Bindings are per-SDD by nature — the
|
|
329
370
|
sync-back writes that methodology's artifacts; other SDDs need their own thin binding, not this one.
|
|
330
371
|
- **`/overseer`** — supervise an autonomous tickmarkr engagement from a Herdr workspace: two-tier visible hierarchy,
|
|
@@ -332,14 +373,65 @@ Cloning this repo gives Claude Code project skills under `.claude/skills/`:
|
|
|
332
373
|
|
|
333
374
|
Repo-scoped: available automatically in Claude Code sessions inside this repo; not installed by npm.
|
|
334
375
|
|
|
335
|
-
##
|
|
376
|
+
## Contributing
|
|
377
|
+
|
|
378
|
+
### Dev setup
|
|
336
379
|
|
|
337
380
|
```bash
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
npm
|
|
381
|
+
git clone <your fork>
|
|
382
|
+
cd tickmarkr
|
|
383
|
+
npm install
|
|
384
|
+
npm run build # tsc → dist/; the only enforced static check
|
|
385
|
+
npm link # exposes the tickmarkr/tkr bins from your checkout
|
|
341
386
|
```
|
|
342
387
|
|
|
388
|
+
### Tests are zero-token by design
|
|
389
|
+
|
|
390
|
+
```bash
|
|
391
|
+
npm test # vitest unit+integration — fake adapter only, spends no tokens, ever
|
|
392
|
+
npm run test:coverage # same suite, coverage floors enforced (see below)
|
|
393
|
+
npm run e2e # opt-in real-CLI end-to-end — DOES spend tokens, needs ≥1 agent CLI installed
|
|
394
|
+
```
|
|
395
|
+
|
|
396
|
+
`npm test` and `npm run test:coverage` must never call a real agent CLI or spend tokens — that's
|
|
397
|
+
what `DROVR_FAKE_SCRIPT` and the `FakeAdapter` (`src/adapters/fake.ts`) exist for. If a change
|
|
398
|
+
needs a real CLI to verify, that test belongs under `npm run e2e` (`tests/e2e/`), gated behind
|
|
399
|
+
`DROVR_E2E=1`, never in the default suite.
|
|
400
|
+
|
|
401
|
+
Coverage floor: `src/{graph,route,gates,run}/**` must stay at lines 80% / functions 80% /
|
|
402
|
+
branches 70% — this is the CLAUDE.md invariant, enforced verbatim in `vitest.config.ts`.
|
|
403
|
+
`src/compile/**`, `src/adapters/**`, and `src/drivers/**` carry their own (higher) measured
|
|
404
|
+
thresholds in the same file.
|
|
405
|
+
|
|
406
|
+
### The spec-driven workflow — the gates are the review
|
|
407
|
+
|
|
408
|
+
Changes to tickmarkr itself land the same way tickmarkr expects consumer changes to land: write a
|
|
409
|
+
native spec (`specs/<version>-<name>.spec.md`, marked `<!-- tickmarkr:spec -->`), give every task
|
|
410
|
+
real `acceptance[]` criteria, then compile/plan/run it and let the gates (baseline, evidence, scope,
|
|
411
|
+
acceptance-judge, cross-vendor review) independently verify the diff before it merges. PRs are
|
|
412
|
+
welcome, but a PR that only "looks right" isn't the bar — a green, gated engagement is. If you're
|
|
413
|
+
proposing a non-trivial change, sketch it as a spec first; small fixes and docs can go straight to
|
|
414
|
+
a PR.
|
|
415
|
+
|
|
416
|
+
### Adding a worker adapter
|
|
417
|
+
|
|
418
|
+
Support for a new agent CLI is one new file implementing `WorkerAdapter`
|
|
419
|
+
(`src/adapters/types.ts`) — `id`, `vendor`, `probe()`, `channels()`, `headlessCommand()`,
|
|
420
|
+
`interactiveCommand()`, `invoke()`, `parse()`, plus the optional `listModels`/`resumeCommand`
|
|
421
|
+
capabilities other adapters use when the CLI supports them. Use an existing adapter as the
|
|
422
|
+
template (`src/adapters/codex.ts` is a good starting shape), then register it with one line in
|
|
423
|
+
`src/adapters/registry.ts`'s `allAdapters()` — append it, don't insert it in the middle, since
|
|
424
|
+
same-tier routing ties resolve by discovery order. No other file needs to know a new adapter
|
|
425
|
+
exists.
|
|
426
|
+
|
|
427
|
+
### Filing defects
|
|
428
|
+
|
|
429
|
+
Abnormalities found during a real engagement (not routine bugs — surprising or systemic
|
|
430
|
+
behavior) are logged as a numbered entry (`OBS-NN`) in `.planning/OBSERVATIONS.md`, with a status
|
|
431
|
+
(`OPEN` / `CLOSED-FIXED` / `CLOSED-MITIGATED`) and, once fixed, the spec/commit that closed it.
|
|
432
|
+
That file is the standing abnormality ledger for this repo — read it before assuming a rough edge
|
|
433
|
+
is new.
|
|
434
|
+
|
|
343
435
|
Design spec: `docs/superpowers/specs/2026-07-07-drover-design.md`.
|
|
344
436
|
|
|
345
437
|
## License
|
|
@@ -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 `
|
|
144
|
+
return stylize(`tickmarkr doctor — capability matrix:\n${rows.join("\n")}${modelSummary}${drift}\nwrote .drovr/doctor.json`);
|
|
103
145
|
}
|
|
@@ -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({
|
|
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, "
|
|
23
|
-
if (
|
|
24
|
-
|
|
25
|
-
|
|
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
|
-
|
|
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
|
|
102
|
+
return `${notes.join("\n")}\n${doc}\nnext: edit tickmarkr.spec.md, then tickmarkr compile tickmarkr.spec.md && tickmarkr plan && tickmarkr run`;
|
|
32
103
|
}
|
|
@@ -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, {
|
|
@@ -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>;
|