tickmarkr 1.30.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +347 -0
- package/dist/adapters/claude-code.d.ts +4 -0
- package/dist/adapters/claude-code.js +191 -0
- package/dist/adapters/codex.d.ts +8 -0
- package/dist/adapters/codex.js +257 -0
- package/dist/adapters/cursor-agent.d.ts +4 -0
- package/dist/adapters/cursor-agent.js +62 -0
- package/dist/adapters/fake.d.ts +45 -0
- package/dist/adapters/fake.js +115 -0
- package/dist/adapters/grok.d.ts +4 -0
- package/dist/adapters/grok.js +122 -0
- package/dist/adapters/model-lints.d.ts +6 -0
- package/dist/adapters/model-lints.js +130 -0
- package/dist/adapters/opencode.d.ts +3 -0
- package/dist/adapters/opencode.js +108 -0
- package/dist/adapters/pi.d.ts +3 -0
- package/dist/adapters/pi.js +191 -0
- package/dist/adapters/prompt.d.ts +6 -0
- package/dist/adapters/prompt.js +84 -0
- package/dist/adapters/registry.d.ts +31 -0
- package/dist/adapters/registry.js +165 -0
- package/dist/adapters/types.d.ts +98 -0
- package/dist/adapters/types.js +56 -0
- package/dist/cli/commands/approve.d.ts +1 -0
- package/dist/cli/commands/approve.js +72 -0
- package/dist/cli/commands/compile.d.ts +1 -0
- package/dist/cli/commands/compile.js +23 -0
- package/dist/cli/commands/doctor.d.ts +2 -0
- package/dist/cli/commands/doctor.js +103 -0
- package/dist/cli/commands/init.d.ts +1 -0
- package/dist/cli/commands/init.js +32 -0
- package/dist/cli/commands/plan.d.ts +2 -0
- package/dist/cli/commands/plan.js +101 -0
- package/dist/cli/commands/profile.d.ts +1 -0
- package/dist/cli/commands/profile.js +56 -0
- package/dist/cli/commands/report.d.ts +4 -0
- package/dist/cli/commands/report.js +306 -0
- package/dist/cli/commands/resume.d.ts +1 -0
- package/dist/cli/commands/resume.js +11 -0
- package/dist/cli/commands/run.d.ts +1 -0
- package/dist/cli/commands/run.js +35 -0
- package/dist/cli/commands/scope.d.ts +3 -0
- package/dist/cli/commands/scope.js +22 -0
- package/dist/cli/commands/status.d.ts +9 -0
- package/dist/cli/commands/status.js +162 -0
- package/dist/cli/commands/unlock.d.ts +1 -0
- package/dist/cli/commands/unlock.js +11 -0
- package/dist/cli/index.d.ts +8 -0
- package/dist/cli/index.js +65 -0
- package/dist/compile/collateral.d.ts +6 -0
- package/dist/compile/collateral.js +128 -0
- package/dist/compile/common.d.ts +12 -0
- package/dist/compile/common.js +56 -0
- package/dist/compile/gsd.d.ts +3 -0
- package/dist/compile/gsd.js +222 -0
- package/dist/compile/index.d.ts +4 -0
- package/dist/compile/index.js +33 -0
- package/dist/compile/native.d.ts +4 -0
- package/dist/compile/native.js +196 -0
- package/dist/compile/prd.d.ts +2 -0
- package/dist/compile/prd.js +64 -0
- package/dist/compile/speckit.d.ts +2 -0
- package/dist/compile/speckit.js +66 -0
- package/dist/config/config.d.ts +184 -0
- package/dist/config/config.js +329 -0
- package/dist/drivers/herdr.d.ts +44 -0
- package/dist/drivers/herdr.js +394 -0
- package/dist/drivers/index.d.ts +3 -0
- package/dist/drivers/index.js +13 -0
- package/dist/drivers/subprocess.d.ts +25 -0
- package/dist/drivers/subprocess.js +106 -0
- package/dist/drivers/types.d.ts +61 -0
- package/dist/drivers/types.js +77 -0
- package/dist/gates/acceptance.d.ts +19 -0
- package/dist/gates/acceptance.js +92 -0
- package/dist/gates/baseline.d.ts +12 -0
- package/dist/gates/baseline.js +97 -0
- package/dist/gates/evidence.d.ts +4 -0
- package/dist/gates/evidence.js +12 -0
- package/dist/gates/llm.d.ts +25 -0
- package/dist/gates/llm.js +101 -0
- package/dist/gates/review.d.ts +12 -0
- package/dist/gates/review.js +80 -0
- package/dist/gates/run-gates.d.ts +34 -0
- package/dist/gates/run-gates.js +105 -0
- package/dist/gates/scope.d.ts +8 -0
- package/dist/gates/scope.js +32 -0
- package/dist/gates/types.d.ts +6 -0
- package/dist/gates/types.js +1 -0
- package/dist/graph/graph.d.ts +20 -0
- package/dist/graph/graph.js +148 -0
- package/dist/graph/schema.d.ts +185 -0
- package/dist/graph/schema.js +141 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/plan/prompt.d.ts +4 -0
- package/dist/plan/prompt.js +31 -0
- package/dist/plan/scope.d.ts +17 -0
- package/dist/plan/scope.js +141 -0
- package/dist/report/cost.d.ts +24 -0
- package/dist/report/cost.js +89 -0
- package/dist/route/preference.d.ts +27 -0
- package/dist/route/preference.js +49 -0
- package/dist/route/profile.d.ts +65 -0
- package/dist/route/profile.js +197 -0
- package/dist/route/router.d.ts +26 -0
- package/dist/route/router.js +200 -0
- package/dist/run/consult.d.ts +23 -0
- package/dist/run/consult.js +80 -0
- package/dist/run/daemon.d.ts +23 -0
- package/dist/run/daemon.js +728 -0
- package/dist/run/git.d.ts +12 -0
- package/dist/run/git.js +56 -0
- package/dist/run/journal.d.ts +91 -0
- package/dist/run/journal.js +287 -0
- package/dist/run/lock.d.ts +29 -0
- package/dist/run/lock.js +182 -0
- package/dist/run/merge.d.ts +12 -0
- package/dist/run/merge.js +44 -0
- package/dist/run/reconcile.d.ts +4 -0
- package/dist/run/reconcile.js +95 -0
- package/package.json +39 -0
- package/schema/rungraph.schema.json +306 -0
package/README.md
ADDED
|
@@ -0,0 +1,347 @@
|
|
|
1
|
+
# tickmarkr
|
|
2
|
+
|
|
3
|
+
**Assertions are free. Tickmarks are earned.**
|
|
4
|
+
|
|
5
|
+
tickmarkr is a spec-driven orchestration harness for AI coding agent CLIs — framed as an external
|
|
6
|
+
audit engagement. You scope assertions (acceptance criteria); the engine earns tickmarks (passed
|
|
7
|
+
gates) only after independent verification. An **engagement** (`tickmarkr run`) compiles a spec
|
|
8
|
+
into a task graph, routes work to the best installed agent CLI (claude-code, codex, cursor-agent,
|
|
9
|
+
opencode, grok, pi) by cost within capability floors, dispatches **field teams** in isolated git
|
|
10
|
+
worktrees — as real visible TUIs when running under [herdr](https://herdr.dev), headless
|
|
11
|
+
subprocesses otherwise — and records every event in the **audit trail** (`.drovr/runs/<runId>/journal.jsonl`).
|
|
12
|
+
Green tasks consolidate onto a `tickmarkr/<runId>` **consolidation** branch; **sign-off** (merge to
|
|
13
|
+
your mainline) is always the Partner's call, never the engine's. A fully green engagement is an
|
|
14
|
+
**unqualified opinion** — every assertion independently ticked.
|
|
15
|
+
|
|
16
|
+
> The package ships `tickmarkr` as the primary bin, plus `drovr` and a `drover` bin alias for
|
|
17
|
+
> backward compatibility — all three invoke the same entry. New installs should call `tickmarkr`.
|
|
18
|
+
|
|
19
|
+
## Vocabulary
|
|
20
|
+
|
|
21
|
+
Audit voice in docs and reports maps to engine concepts exactly once:
|
|
22
|
+
|
|
23
|
+
| Audit term | Engine concept |
|
|
24
|
+
|---|---|
|
|
25
|
+
| Partner | Human operator |
|
|
26
|
+
| Manager | Overseer |
|
|
27
|
+
| tickmarkr | Orchestrator daemon |
|
|
28
|
+
| field teams / staff | Workers (agent CLIs in worktrees) |
|
|
29
|
+
| seniors | Watchers (status monitor, gate panes) |
|
|
30
|
+
| engagement | Run (`tickmarkr run` / `resume`) |
|
|
31
|
+
| assertion | Acceptance criterion (`acceptance[]` entry) |
|
|
32
|
+
| tickmark | Passed gate |
|
|
33
|
+
| audit evidence | Evidence gate (commits/diff exist) |
|
|
34
|
+
| audit trail | Journal (`.drovr/runs/<runId>/journal.jsonl`) |
|
|
35
|
+
| the consolidation | Integration branch (`tickmarkr/<runId>`) |
|
|
36
|
+
| sign-off / issuance | Merge consolidation branch to main |
|
|
37
|
+
| unqualified opinion | Green engagement (all tasks merged) |
|
|
38
|
+
| qualified opinion | Failed or stalled engagement |
|
|
39
|
+
| EQR | Acceptance judge (LLM gate vs `acceptance[]`) |
|
|
40
|
+
| National Office | Consult (frontier-model escalation on deadlock) |
|
|
41
|
+
|
|
42
|
+
Commands below use the real CLI surface — audit terms never replace bin names in examples.
|
|
43
|
+
|
|
44
|
+
## Invariants
|
|
45
|
+
|
|
46
|
+
These are law; the codebase fails closed around them:
|
|
47
|
+
|
|
48
|
+
- `acceptance[]` required on every task; compile fails without it
|
|
49
|
+
- new engagements consolidate to a `tickmarkr/<runId>` branch — never main (resume of older
|
|
50
|
+
engagements keeps the branch names recorded in their journal, including legacy `drovr/<runId>`)
|
|
51
|
+
- gates never trust worker claims; tickmarkr verifies everything itself
|
|
52
|
+
- state is files + git only (`.drovr/`); no DB, no services
|
|
53
|
+
- worker/judge/review/consult prompts end with machine-parseable trailers
|
|
54
|
+
(`DROVR_RESULT` / JSON verdicts); parse defensively, fail closed
|
|
55
|
+
|
|
56
|
+
## Install
|
|
57
|
+
|
|
58
|
+
Requirements: Node ≥ 20, git, and at least one agent CLI on PATH (`claude`, `codex`, `cursor-agent`,
|
|
59
|
+
`opencode`, `grok`, or `pi`) authenticated through its own login.
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
npm install
|
|
63
|
+
npm run build
|
|
64
|
+
npm link # exposes the `tickmarkr` bin (and the `drovr` / `drover` compat alias)
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Then verify your fleet:
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
tickmarkr doctor # probes installed adapters, herdr, auth; prints the capability matrix
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## Quickstart
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
tickmarkr init # guided setup + doctor; scaffolds .drovr/config.yaml and drovr.spec.md
|
|
77
|
+
# edit drovr.spec.md # the native spec template, marked <!-- drovr:spec -->
|
|
78
|
+
tickmarkr compile drovr.spec.md # spec → .drovr/graph.json (fails without acceptance criteria)
|
|
79
|
+
tickmarkr plan # dry-run routing table + cost estimate + floor lints
|
|
80
|
+
tickmarkr run # execute the graph (--concurrency N --driver herdr|subprocess --route-strict)
|
|
81
|
+
tickmarkr report <runId> --md # engagement record in Markdown
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Supervise and finish:
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
tickmarkr status # live engagement state (--watch to follow)
|
|
88
|
+
tickmarkr resume <runId> # continue an engagement from its audit trail
|
|
89
|
+
tickmarkr approve <runId> <taskId> # Partner sign-off on a parked task (--by <name> --reason <text>)
|
|
90
|
+
tickmarkr report <runId> # cost/quality report
|
|
91
|
+
tickmarkr profile # show the learned routing profile
|
|
92
|
+
tickmarkr unlock # remove a stale/garbage run lock (refuses if the holder is alive)
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
Green tasks land on the engagement's `tickmarkr/<runId>` consolidation branch; sign-off to your
|
|
96
|
+
mainline is your call, never tickmarkr's.
|
|
97
|
+
|
|
98
|
+
### Escalation and National Office verdicts
|
|
99
|
+
|
|
100
|
+
When gates or field teams stall, tickmarkr escalates to a frontier-model **National Office** consult
|
|
101
|
+
for a structured verdict. The consult can recommend rerouting to a different adapter/model channel,
|
|
102
|
+
or human approval if deadlock persists. Reroute verdicts can target two scopes:
|
|
103
|
+
|
|
104
|
+
- **Channel-level reroute**: exclude a single channel (e.g., `claude-code:opus-4-8`) and try the next ranked channel
|
|
105
|
+
- **Adapter-level reroute**: exclude an entire adapter (e.g., `cursor-agent`) when the consult detects an environmental
|
|
106
|
+
CLI-level failure (blocked dialog, auth issue, timeout pattern). The daemon expands the task's exclusion list to cover
|
|
107
|
+
every available channel of that adapter, ensuring zero re-try of the same root cause
|
|
108
|
+
|
|
109
|
+
Both exclusion types persist across resume — a task that failed due to a consult reroute will never re-try excluded channels
|
|
110
|
+
or adapters in a subsequent `tickmarkr resume`.
|
|
111
|
+
|
|
112
|
+
### Approving tasks
|
|
113
|
+
|
|
114
|
+
`tickmarkr approve` unblocks two task states:
|
|
115
|
+
|
|
116
|
+
**Human gates** (attempt budget ≥ 1):
|
|
117
|
+
- The task finished with a result but gates require human judgment (`humanGate: true` in the spec)
|
|
118
|
+
- Approving records the Partner's verdict and the task proceeds to merge
|
|
119
|
+
|
|
120
|
+
**Attempt-cap parks** (attempt budget exhausted):
|
|
121
|
+
- The task has burned its full attempt budget without reaching a conclusive result
|
|
122
|
+
- All channels (including adapter exclusions from prior consults) remain excluded from retry
|
|
123
|
+
- Approving grants a **fresh attempt budget** with the same exclusion list — the task re-routes around all previously-burned channels
|
|
124
|
+
and adapters, giving it one more escalation ladder before exhaustion
|
|
125
|
+
|
|
126
|
+
Example: a task escalates away from cursor-agent due to a trust dialog (adapter-level exclusion), then away from all claude-code
|
|
127
|
+
models before hitting the attempt cap. `tickmarkr approve` grants a fresh ladder to try the frontier tier, still avoiding both
|
|
128
|
+
cursor-agent and claude-code entirely.
|
|
129
|
+
|
|
130
|
+
## Model scoping and auth detection
|
|
131
|
+
|
|
132
|
+
Each agent CLI exposes a list of available models. tickmarkr's routing works only with **classified** models — those you explicitly enter into `.drovr/config.yaml` under `tiers`. The `tickmarkr doctor` command probes these models to detect auth status and records the results in `.drovr/doctor.json`, which routing consumes to avoid 401/403 dispatch failures.
|
|
133
|
+
|
|
134
|
+
**Model terminology**:
|
|
135
|
+
|
|
136
|
+
- **Listed models**: All models a CLI advertises as available (e.g., pi advertises both `pi:glm-5.2` and `pi:anthropic-claude-opus-4-8`, but not all are authed)
|
|
137
|
+
- **Scoped models**: Listed models with an explicit CLI namespace prefix (e.g., `pi:glm-5.2`, `openai:gpt-4o`). The design rule is to classify scoped models primarily, not unscoped listed models
|
|
138
|
+
- **Classified models**: Models you've entered into `tiers` — the routing-eligible set, regardless of scope
|
|
139
|
+
|
|
140
|
+
When you run `tickmarkr doctor`, it:
|
|
141
|
+
1. Probes each classified model exactly once (one minimal headless API call per model per adapter)
|
|
142
|
+
2. Records results in `.drovr/doctor.json`: `authed: true` or `authed: false` with the failure reason and probe timestamp
|
|
143
|
+
3. Prints a model-status table for classified models only, showing tier, auth verdict, denial status, and prefer rank
|
|
144
|
+
|
|
145
|
+
**Routing and auth**:
|
|
146
|
+
- Routing discovers channels only from authed classified models (unauthed channels are dropped)
|
|
147
|
+
- `tickmarkr plan` lints each excluded model, naming the probe reason and timestamp
|
|
148
|
+
- If a task's floor can only be satisfied by unauthed models, the plan fails loudly — never a silent fallback
|
|
149
|
+
|
|
150
|
+
**When to re-run doctor**:
|
|
151
|
+
- After classifying new scoped models into tiers (e.g., onboarding a new vendor or aggregator CLI)
|
|
152
|
+
- Before a run, if credentials may have changed (API key rotation, quota reset, subscription renewal)
|
|
153
|
+
|
|
154
|
+
**Operator model control — deny.models**:
|
|
155
|
+
You can bench a classified model without removing it from tiers:
|
|
156
|
+
```yaml
|
|
157
|
+
routing:
|
|
158
|
+
deny:
|
|
159
|
+
models:
|
|
160
|
+
- pi:glm-5.2 # stays in tiers, never routes
|
|
161
|
+
- openai:gpt-4o # reason in git commit
|
|
162
|
+
```
|
|
163
|
+
`tickmarkr plan` lints denied models identically to unauthed ones. Re-enable by deleting one line.
|
|
164
|
+
|
|
165
|
+
## Run output and narration
|
|
166
|
+
|
|
167
|
+
When you execute `tickmarkr run`, the daemon streams live narration of each audit-trail event to stdout — task status changes,
|
|
168
|
+
gate results (tickmarks), worker dispatch, and merge commits. This narration is derived from the append-only `.drovr/runs/<runId>/journal.jsonl`
|
|
169
|
+
ledger, which remains the authoritative source of truth for resumability. If an engagement is interrupted, `tickmarkr resume <runId>`
|
|
170
|
+
replays the journal and picks up where it left off. You can also tail the journal directly for raw event inspection or
|
|
171
|
+
feed it to external tools; the narration is a convenience layer on top.
|
|
172
|
+
|
|
173
|
+
## Usage and cost
|
|
174
|
+
|
|
175
|
+
Every engagement record includes a **Usage & efficiency** section showing token/window consumption, cost estimates,
|
|
176
|
+
and first-attempt success rate. Cost reporting follows strict honesty rules and never infers absent data:
|
|
177
|
+
|
|
178
|
+
### Cost model — two channel economics
|
|
179
|
+
|
|
180
|
+
**API channels** (real marginal cost):
|
|
181
|
+
- Formula: `tokens × price`
|
|
182
|
+
- Pricing data sourced from [LiteLLM model prices](https://github.com/BerriAI/litellm/blob/main/litellm/model_prices_and_context_window.json)
|
|
183
|
+
- Each estimate carries its basis: input tokens, output tokens, cache reads (if applicable), rate per Mtok, and rate date
|
|
184
|
+
|
|
185
|
+
**Subscription channels** (flat monthly, no marginal token cost):
|
|
186
|
+
- Two metrics reported when computable:
|
|
187
|
+
- **Amortized window cost**: `plan monthly cost ÷ usable windows per month` — a range accounting for time-varying quotas
|
|
188
|
+
- **API-equivalent counterfactual**: `metered tokens × API price of the same/nearest model` — e.g., a $100/month Claude Max user
|
|
189
|
+
who consumes tokens worth $500 API-equivalent has paid $500 API-equivalent for flat $100
|
|
190
|
+
- Both metrics appear in the report alongside their sample count and range bounds
|
|
191
|
+
|
|
192
|
+
### Honesty rules (no guessing allowed)
|
|
193
|
+
|
|
194
|
+
- **Ranges, never single numbers**: quota multipliers and monthly-window variation make subscription costs a range, not a point estimate
|
|
195
|
+
- **"Not measurable" never becomes $0**: if a channel lacks pricing or metering data, the report explicitly states "not measurable"
|
|
196
|
+
- **Basis always shown**: every cost figure prints the token count, rate used, and date so estimates can be audited
|
|
197
|
+
- **No network calls**: pricing config is operator-maintained in `.drovr/config.yaml` (or `~/.config/drovr/config.yaml`), seeded with dated
|
|
198
|
+
comments and LiteLLM's JSON file named as the copy-from source; tickmarkr never calls home to fetch rates
|
|
199
|
+
- **Attribution from journal**: token counts come from tickmarkr's own telemetry spans in the audit trail, never from provider invoices or dashboards
|
|
200
|
+
|
|
201
|
+
## Visibility: supervised fleet UX
|
|
202
|
+
|
|
203
|
+
When running under [herdr](https://herdr.dev), tickmarkr creates a visible, labeled pane-and-tab workspace:
|
|
204
|
+
|
|
205
|
+
### Tab vocabulary
|
|
206
|
+
|
|
207
|
+
- **WORKERS**: the primary generation tab showing active field-team tasks. The tab header shows a live token (task ID) plus one state glyph:
|
|
208
|
+
- ↻ — the member is a retry attempt (attempt > 0)
|
|
209
|
+
- ✋ — the driver detected the member is blocked
|
|
210
|
+
- (bare token) — member is running normally
|
|
211
|
+
- **cleanup · <taskId>**: overflow/teardown generation tabs. When a new generation starts (on retry escalation), a new cleanup tab
|
|
212
|
+
opens labeled with the newest live member's task ID; it auto-closes when the generation completes
|
|
213
|
+
- **watch**: a single pane running `tickmarkr status --watch` — the senior's glanceable engagement monitor
|
|
214
|
+
|
|
215
|
+
### Pane naming (when visibility.llm = pane)
|
|
216
|
+
|
|
217
|
+
- **judge · <taskId>**: the EQR evaluating acceptance criteria
|
|
218
|
+
- **review · <taskId>**: the cross-vendor code review gate
|
|
219
|
+
- **consult · <taskId>**: National Office escalation on deadlock (gates or worker stall)
|
|
220
|
+
|
|
221
|
+
### Notification tiers
|
|
222
|
+
|
|
223
|
+
Only operator-decision events notify (with sound):
|
|
224
|
+
|
|
225
|
+
- ✓ Gate failure escalations (move to next channel, human approval needed)
|
|
226
|
+
- ✓ National Office verdicts (deadlock resolved or escalation recommended)
|
|
227
|
+
- ✓ Human gates (task parked, awaiting `tickmarkr approve`)
|
|
228
|
+
- ✓ Quota failover (channel exhausted, routed to next)
|
|
229
|
+
- ✓ Engagement end (unqualified or qualified opinion)
|
|
230
|
+
|
|
231
|
+
Routine events do NOT notify:
|
|
232
|
+
- ✗ Task dispatch (tab already shows progress)
|
|
233
|
+
- ✗ Task done (tab state updated, already visible)
|
|
234
|
+
|
|
235
|
+
This keeps the Partner focused on decisions that require attention, not noise.
|
|
236
|
+
|
|
237
|
+
### Reconciliation model: level-triggered pane lifecycle
|
|
238
|
+
|
|
239
|
+
tickmarkr closes exactly what it owns and no longer needs, no matter how any process died.
|
|
240
|
+
|
|
241
|
+
Every pane and tab tickmarkr creates receives a **parseable ownership name** encoding the pane's role, task, attempt, and run:
|
|
242
|
+
- `WORKERS` — active worker generation tab
|
|
243
|
+
- `cleanup · <taskId>` — teardown generation tab for overflow attempts
|
|
244
|
+
- `watch` — status monitor pane
|
|
245
|
+
- `<role> · <taskId> · A<attempt> · R<runId>` — judge, review, consult, and worker panes (formats like `judge · task-abc123 · A1 · Rrun-20260713-175532`)
|
|
246
|
+
|
|
247
|
+
tickmarkr creates all owned panes only within the run's workspace; any tickmarkr-owned panes discovered outside the run's workspace (from prior runs or placement bugs) are reconciled and closed along with their emptied tabs. Any pane not matching the ownership contract is **foreign** — created by the operator or another tool — and is never a candidate for closing.
|
|
248
|
+
|
|
249
|
+
**Desired-state reconciliation**: A pure function computes the exact set of panes that should exist from the audit trail at any moment:
|
|
250
|
+
- Worker panes for all in-flight task attempts
|
|
251
|
+
- Gate panes for unread judge/review/consult verdicts
|
|
252
|
+
- The watch pane (if running)
|
|
253
|
+
- Empty set (after engagement end)
|
|
254
|
+
|
|
255
|
+
The daemon reconciles at every safe point:
|
|
256
|
+
1. **Run start** — clean up any orphaned panes from crashed earlier runs of the same repo
|
|
257
|
+
2. **Resume** — reconcile the restarted journal state and close panes for superseded attempts
|
|
258
|
+
3. **After terminal events** (task done, failed, human gate) — close the corresponding worker/gate pane and its emptied tab
|
|
259
|
+
4. **At engagement end** — close all remaining owned panes and tabs
|
|
260
|
+
|
|
261
|
+
Reconciliation failures (herdr unavailable, a pane vanished mid-sweep) never fail the engagement — visibility is cosmetic, gates are law.
|
|
262
|
+
|
|
263
|
+
### Workspace-trust pre-flight
|
|
264
|
+
|
|
265
|
+
tickmarkr runs field teams in fresh git worktrees cloned from your repo. Before dispatch, every worker checks whether that repo is trusted by its CLI, fail-closed:
|
|
266
|
+
|
|
267
|
+
**Seeding trust at doctor time**:
|
|
268
|
+
- `tickmarkr doctor` probes each adapter's trust status for your repo root
|
|
269
|
+
- Adapters that can pre-seed trust (codex via `config.toml`, others if applicable) do so automatically; you see `seeded` in the doctor output
|
|
270
|
+
- Adapters requiring manual trust grant show `action-required` with the exact one-time command to run (e.g., `codex init /path/to/repo`)
|
|
271
|
+
- Adapters without a trust concept report `n/a`
|
|
272
|
+
|
|
273
|
+
**Auto-answering trust dialogs during runs**:
|
|
274
|
+
- Some CLIs (e.g., cursor-agent) cannot pre-seed trust and show an interactive "Workspace Trust Required" dialog the first time they touch the worktree
|
|
275
|
+
- For these CLIs, tickmarkr detects the trust dialog by its fingerprint and auto-answers it exactly once per slot — safe because tickmarkr created the worktree from your own repo
|
|
276
|
+
- Any other blocked dialog still pages the operator for manual approval
|
|
277
|
+
|
|
278
|
+
## Spec front-ends (what compiles today)
|
|
279
|
+
|
|
280
|
+
tickmarkr is SDD-agnostic downstream of `compile` — routing, gates, and merges never know which methodology
|
|
281
|
+
produced the graph. Upstream, these front-ends exist:
|
|
282
|
+
|
|
283
|
+
| Spec source | Support | How |
|
|
284
|
+
|---|---|---|
|
|
285
|
+
| **drovr spec** (native format) | Native/default | `tickmarkr init` template with the `<!-- drovr:spec -->` marker |
|
|
286
|
+
| **Spec Kit** feature dir | Native | dir containing `tasks.md` |
|
|
287
|
+
| **GSD** phase dir | Native | dir containing `*-PLAN.md` |
|
|
288
|
+
| **Markdown PRD** | Native (universal adapter) | any `.md` with tasks + acceptance criteria |
|
|
289
|
+
| OpenSpec, BMAD, Superpowers, others | Not yet | render to a drovr 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
|
+
|
|
291
|
+
Compile fails loudly on anything it can't recognize (`--type native|speckit|prd|gsd` to force).
|
|
292
|
+
|
|
293
|
+
## Planning layer: intent → spec → engagement
|
|
294
|
+
|
|
295
|
+
tickmarkr pairs a lightweight planning step (`tickmarkr scope`) with the existing compile/run flow. The flow is:
|
|
296
|
+
|
|
297
|
+
```
|
|
298
|
+
intent file (prose)
|
|
299
|
+
↓ [tickmarkr scope: 3-question gate]
|
|
300
|
+
↓ [clarification ≤3 blocking Qs]
|
|
301
|
+
native spec (REQ-nn → oracle → task)
|
|
302
|
+
↓ [tickmarkr compile]
|
|
303
|
+
↓ [fails without acceptance criteria]
|
|
304
|
+
task graph + routing table
|
|
305
|
+
↓ [tickmarkr run]
|
|
306
|
+
↓ [gates never trust workers]
|
|
307
|
+
merged green tasks + engagement record
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
**Clarification gate**: The `tickmarkr scope` CLI enforces ≤3 blocking questions. Drafting refuses until all are answered; residual uncertainty becomes explicit assumptions in the spec.
|
|
311
|
+
|
|
312
|
+
**Oracle types** — assertions are typed for deterministic verification:
|
|
313
|
+
|
|
314
|
+
- `oracle: command <cmd>` — exit code (0 = pass); most reliable
|
|
315
|
+
- `oracle: test <name>` — run a named test (must exist in the task's acceptance[] context)
|
|
316
|
+
- `oracle: judge <rubric>` — EQR verdict, fail-closed, never overrides failed command/test
|
|
317
|
+
|
|
318
|
+
Untyped acceptance strings in existing specs compile as `oracle: judge` + warning (read-old/write-new compat, same as the rename precedent).
|
|
319
|
+
|
|
320
|
+
**Scope semantics**: Plan-time blast radius is advisory + structured `{paths, confidence, reason}` — never authoritative. The run-time scope gate verifies actual file changes; if a worker requests widening, the daemon pauses as a designed human gate for operator approval.
|
|
321
|
+
|
|
322
|
+
## Claude Code skills (ship with the repo)
|
|
323
|
+
|
|
324
|
+
Cloning this repo gives Claude Code project skills under `.claude/skills/`:
|
|
325
|
+
|
|
326
|
+
- **`/drovr-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 `/drovr-auto`.
|
|
327
|
+
- **`/drovr-auto`** — the **GSD binding**: run remaining GSD milestone phases autonomously with tickmarkr as the
|
|
328
|
+
executor (plans → compile → dry-run → run → merge → sync-back). Bindings are per-SDD by nature — the
|
|
329
|
+
sync-back writes that methodology's artifacts; other SDDs need their own thin binding, not this one.
|
|
330
|
+
- **`/overseer`** — supervise an autonomous tickmarkr engagement from a Herdr workspace: two-tier visible hierarchy,
|
|
331
|
+
journal-based watchers, gate-failure triage, human-checkpoint routing. Requires `HERDR_ENV=1`.
|
|
332
|
+
|
|
333
|
+
Repo-scoped: available automatically in Claude Code sessions inside this repo; not installed by npm.
|
|
334
|
+
|
|
335
|
+
## Development
|
|
336
|
+
|
|
337
|
+
```bash
|
|
338
|
+
npm test # vitest unit+integration — zero tokens, fake adapter only
|
|
339
|
+
npm run test:coverage # coverage floors enforced on core dirs
|
|
340
|
+
npm run e2e # real-CLI end-to-end (spends tokens; needs ≥1 agent CLI)
|
|
341
|
+
```
|
|
342
|
+
|
|
343
|
+
Design spec: `docs/superpowers/specs/2026-07-07-drover-design.md`.
|
|
344
|
+
|
|
345
|
+
## License
|
|
346
|
+
|
|
347
|
+
MIT
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import { readdirSync, readFileSync, realpathSync, statSync } from "node:fs";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { parseWorkerResult } from "./prompt.js";
|
|
6
|
+
import { channelsFromConfig, shq, TokenUsageSchema } from "./types.js";
|
|
7
|
+
// SPEND-01/SPEND-11: claude writes a per-session JSONL to ~/.claude/projects/<slug>/ where slug is the
|
|
8
|
+
// realpath'd cwd with every non-alphanumeric char replaced by "-" (verified 114/114 — 36-DIAGNOSIS.md).
|
|
9
|
+
// The old `/`-only formula missed the "." in `.drovr/worktrees/…` — ENOENT on every worktree dispatch.
|
|
10
|
+
// Each assistant message carries a per-record ISO `timestamp` and message.usage. We read it POST-HOC
|
|
11
|
+
// (never the pane, never the trailer) and sum
|
|
12
|
+
// tokens for records whose top-level cwd matches this task's worktree AND whose timestamp is at/after
|
|
13
|
+
// `sinceMs` (this attempt's dispatch wall-clock, from the daemon). The cursor makes a per-attempt fold
|
|
14
|
+
// correct even though this store ACCUMULATES across attempts under a stable slug — without it, folding
|
|
15
|
+
// gives 3A+2B+C (checker blocker). Filter PER RECORD by the record's own timestamp, never file mtime.
|
|
16
|
+
// FAIL OPEN everywhere: any missing dir / unreadable file / torn line / no match / unparseable
|
|
17
|
+
// timestamp ⇒ undefined ⇒ unmetered. A metering read must NEVER throw — it must never fail a task.
|
|
18
|
+
// SPEND-06: we sum ONLY the four token counts. message.usage may also carry a costUSD — a NOTIONAL
|
|
19
|
+
// list price (0.74 for a two-token sub reply, LIVE-CHECK finding 3); we never read it. Money is
|
|
20
|
+
// Phase 18's operator-price × tokens derivation, not a CLI claim.
|
|
21
|
+
const MAX_SESSION_FILES = 20; // newest-first; a long-lived project dir can hold many sessions
|
|
22
|
+
const MAX_SESSION_BYTES = 8_000_000; // per-file cap; a runaway JSONL cannot make the read unbounded
|
|
23
|
+
export function claudeSlug(real) {
|
|
24
|
+
return real.replace(/[^A-Za-z0-9]/g, "-");
|
|
25
|
+
}
|
|
26
|
+
export function probeVersion(bin) {
|
|
27
|
+
const r = spawnSync(bin, ["--version"], { encoding: "utf8", timeout: 10000 });
|
|
28
|
+
if (r.error || r.status !== 0)
|
|
29
|
+
return { installed: false, authed: false, models: [] };
|
|
30
|
+
return {
|
|
31
|
+
installed: true,
|
|
32
|
+
authed: true,
|
|
33
|
+
version: (r.stdout || r.stderr).trim().split("\n")[0],
|
|
34
|
+
models: [],
|
|
35
|
+
note: "auth assumed; verified at dispatch (failover on auth/quota errors)",
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
export const claudeCode = {
|
|
39
|
+
id: "claude-code",
|
|
40
|
+
vendor: "anthropic",
|
|
41
|
+
probe: async () => probeVersion("claude"),
|
|
42
|
+
channels: (cfg) => channelsFromConfig("claude-code", cfg),
|
|
43
|
+
// --strict-mcp-config --mcp-config '{"mcpServers":{}}': pin the MCP surface to empty so fresh-worktree
|
|
44
|
+
// workers/gates don't load project .mcp.json servers (herdr scrapes dialogs as idle — v1.4 incident,
|
|
45
|
+
// memory drovr-worker-mcp-dialog-stall). Live-verified 2026-07-10 on claude 2.1.205 (operator check):
|
|
46
|
+
// headless (-p) fully suppressed — exit 0, no dialog. Interactive TUI STILL shows the project
|
|
47
|
+
// MCP-enable dialog (project trust/enablement, not config loading); Esc dismisses it, and drovr's
|
|
48
|
+
// blocked/idle paging surfaces the pane to the operator (same path as cursor's trust dialog).
|
|
49
|
+
// Gotchas (both bit the 2026-07-10 live check): bare '{}' is REJECTED ("mcpServers: expected record"),
|
|
50
|
+
// and --mcp-config is VARIADIC — a positional after it is eaten as a config-file path, so another
|
|
51
|
+
// flag must always follow the value, never the prompt.
|
|
52
|
+
headlessCommand: (promptFile, model) => `claude -p "$(cat ${shq(promptFile)})" --model ${shq(model)} --permission-mode acceptEdits --strict-mcp-config --mcp-config '{"mcpServers":{}}' --output-format text`,
|
|
53
|
+
// HYG-03: the residual first-entry dialog on an interactive TUI is the workspace TRUST dialog (not MCP
|
|
54
|
+
// config loading) — CLI-imposed, no flag to pre-accept, only store is claude's global last-writer-wins
|
|
55
|
+
// ~/.claude.json keyed on the exact path. Closed WON'T-FIX (decision B, 2026-07-10): drovr writes nothing
|
|
56
|
+
// to that file (a seed races claude's own writes, nondeterministically). Amortizes to one operator dismissal
|
|
57
|
+
// per stable worktree path; blocked-pane paging surfaces it. Do NOT change this command to "fix" the dialog —
|
|
58
|
+
// see .planning/REQUIREMENTS.md HYG-03 and 21-02-LIVE-CHECK.md. Revisit if upstream ships a --trust flag.
|
|
59
|
+
interactiveCommand: (promptFile, model) => `claude --model ${shq(model)} --strict-mcp-config --mcp-config '{"mcpServers":{}}' --permission-mode acceptEdits "$(cat ${shq(promptFile)})"`,
|
|
60
|
+
resumeCommand: (sessionId, promptFile, model) => `claude -r ${shq(sessionId)} --model ${shq(model)} --strict-mcp-config --mcp-config '{"mcpServers":{}}' --permission-mode acceptEdits "$(cat ${shq(promptFile)})"`,
|
|
61
|
+
invoke(task, _cwd, a, ctx) {
|
|
62
|
+
return { command: this.headlessCommand(ctx.promptFile, a.model) };
|
|
63
|
+
},
|
|
64
|
+
parse: parseWorkerResult,
|
|
65
|
+
collectUsage(cwd, sinceMs) {
|
|
66
|
+
try {
|
|
67
|
+
const real = realpathSync(cwd); // resolve symlinks (darwin /tmp → /private/tmp)
|
|
68
|
+
const slug = claudeSlug(real);
|
|
69
|
+
const dir = join(homedir(), ".claude", "projects", slug);
|
|
70
|
+
// newest-first by mtime, bounded — mtime picks WHICH files to scan, never a record's cursor.
|
|
71
|
+
const files = readdirSync(dir)
|
|
72
|
+
.filter((f) => f.endsWith(".jsonl"))
|
|
73
|
+
.map((f) => {
|
|
74
|
+
try {
|
|
75
|
+
return { f, m: statSync(join(dir, f)).mtimeMs };
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
return undefined; // a stat failure just drops that file
|
|
79
|
+
}
|
|
80
|
+
})
|
|
81
|
+
.filter((x) => x !== undefined)
|
|
82
|
+
.sort((a, b) => b.m - a.m)
|
|
83
|
+
.slice(0, MAX_SESSION_FILES);
|
|
84
|
+
let input = 0, output = 0, kept = false;
|
|
85
|
+
let cacheRead, cacheWrite;
|
|
86
|
+
const seen = new Set(); // message.id is globally unique across session files in one call
|
|
87
|
+
for (const { f } of files) {
|
|
88
|
+
let text;
|
|
89
|
+
try {
|
|
90
|
+
text = readFileSync(join(dir, f), "utf8").slice(0, MAX_SESSION_BYTES);
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
continue; // unreadable file ⇒ skip
|
|
94
|
+
}
|
|
95
|
+
for (const line of text.split("\n")) {
|
|
96
|
+
if (!line.trim())
|
|
97
|
+
continue;
|
|
98
|
+
let recRaw;
|
|
99
|
+
try {
|
|
100
|
+
recRaw = JSON.parse(line); // torn final line / garbage ⇒ dropped
|
|
101
|
+
}
|
|
102
|
+
catch {
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
const rec = recRaw;
|
|
106
|
+
if (rec.cwd !== real)
|
|
107
|
+
continue;
|
|
108
|
+
const u = rec.message?.usage;
|
|
109
|
+
if (!u || typeof u !== "object")
|
|
110
|
+
continue;
|
|
111
|
+
const ts = Date.parse(String(rec.timestamp));
|
|
112
|
+
if (!Number.isFinite(ts) || ts < sinceMs)
|
|
113
|
+
continue; // absent/unparseable/pre-cursor ⇒ skip
|
|
114
|
+
// claude splits one assistant response across N records (one per content block), each
|
|
115
|
+
// repeating the FULL message.usage; dedup after the cursor so a pre-sinceMs id cannot poison seen.
|
|
116
|
+
const id = rec.message?.id;
|
|
117
|
+
if (typeof id === "string") {
|
|
118
|
+
if (seen.has(id))
|
|
119
|
+
continue;
|
|
120
|
+
seen.add(id);
|
|
121
|
+
}
|
|
122
|
+
const uu = u;
|
|
123
|
+
const n = (k) => (typeof uu[k] === "number" ? uu[k] : 0);
|
|
124
|
+
input += n("input_tokens");
|
|
125
|
+
output += n("output_tokens");
|
|
126
|
+
if ("cache_read_input_tokens" in uu)
|
|
127
|
+
cacheRead = (cacheRead ?? 0) + n("cache_read_input_tokens");
|
|
128
|
+
if ("cache_creation_input_tokens" in uu)
|
|
129
|
+
cacheWrite = (cacheWrite ?? 0) + n("cache_creation_input_tokens");
|
|
130
|
+
kept = true;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
if (!kept)
|
|
134
|
+
return undefined; // nothing matched ⇒ unmetered, never {input:0,…}
|
|
135
|
+
const out = { input, output, ...(cacheRead !== undefined ? { cacheRead } : {}), ...(cacheWrite !== undefined ? { cacheWrite } : {}) };
|
|
136
|
+
const p = TokenUsageSchema.safeParse(out);
|
|
137
|
+
return p.success ? p.data : undefined;
|
|
138
|
+
}
|
|
139
|
+
catch {
|
|
140
|
+
return undefined; // missing dir / any throw ⇒ fail open
|
|
141
|
+
}
|
|
142
|
+
},
|
|
143
|
+
// v1.23 T1: last-turn context fill from ~/.claude/projects/<slug>/<sessionId>.jsonl ONLY.
|
|
144
|
+
// tokens = input_tokens + cache_creation_input_tokens + cache_read_input_tokens of the LAST
|
|
145
|
+
// assistant usage record (not a sum over turns — ctx-watch.sh class; overseer wake signal).
|
|
146
|
+
// Disk read only: no claude spawn, no pane, no network. null = unknown.
|
|
147
|
+
contextUsage(session) {
|
|
148
|
+
try {
|
|
149
|
+
const real = realpathSync(session.cwd);
|
|
150
|
+
const slug = claudeSlug(real);
|
|
151
|
+
// session id is a filename stem (herdr agent_session.value); refuse path traversal.
|
|
152
|
+
const sid = session.id.replace(/\.jsonl$/i, "");
|
|
153
|
+
if (!sid || sid.includes("/") || sid.includes("\\") || sid.includes(".."))
|
|
154
|
+
return null;
|
|
155
|
+
const file = join(homedir(), ".claude", "projects", slug, `${sid}.jsonl`);
|
|
156
|
+
let text;
|
|
157
|
+
try {
|
|
158
|
+
text = readFileSync(file, "utf8").slice(0, MAX_SESSION_BYTES);
|
|
159
|
+
}
|
|
160
|
+
catch {
|
|
161
|
+
return null;
|
|
162
|
+
}
|
|
163
|
+
let last;
|
|
164
|
+
for (const line of text.split("\n")) {
|
|
165
|
+
if (!line.trim())
|
|
166
|
+
continue;
|
|
167
|
+
let recRaw;
|
|
168
|
+
try {
|
|
169
|
+
recRaw = JSON.parse(line);
|
|
170
|
+
}
|
|
171
|
+
catch {
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
const rec = recRaw;
|
|
175
|
+
const u = rec.message?.usage;
|
|
176
|
+
if (!u || typeof u !== "object")
|
|
177
|
+
continue;
|
|
178
|
+
const uu = u;
|
|
179
|
+
const n = (k) => (typeof uu[k] === "number" ? uu[k] : 0);
|
|
180
|
+
// last turn wins — overwrite, never accumulate (the sum-over-turns bug this API exists to avoid)
|
|
181
|
+
last = n("input_tokens") + n("cache_creation_input_tokens") + n("cache_read_input_tokens");
|
|
182
|
+
}
|
|
183
|
+
if (last === undefined)
|
|
184
|
+
return null;
|
|
185
|
+
return { tokens: last };
|
|
186
|
+
}
|
|
187
|
+
catch {
|
|
188
|
+
return null;
|
|
189
|
+
}
|
|
190
|
+
},
|
|
191
|
+
};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { type TrustVerdict, type WorkerAdapter } from "./types.js";
|
|
2
|
+
export declare function readCodexModelsCache(path?: string): {
|
|
3
|
+
models: string[];
|
|
4
|
+
fetchedAt?: string;
|
|
5
|
+
};
|
|
6
|
+
export declare function seedCodexTrust(repoRoot: string, configPath?: string): TrustVerdict;
|
|
7
|
+
export declare function hasCodexTrustedProject(text: string, root: string): boolean;
|
|
8
|
+
export declare const codex: WorkerAdapter;
|