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.
Files changed (123) hide show
  1. package/README.md +347 -0
  2. package/dist/adapters/claude-code.d.ts +4 -0
  3. package/dist/adapters/claude-code.js +191 -0
  4. package/dist/adapters/codex.d.ts +8 -0
  5. package/dist/adapters/codex.js +257 -0
  6. package/dist/adapters/cursor-agent.d.ts +4 -0
  7. package/dist/adapters/cursor-agent.js +62 -0
  8. package/dist/adapters/fake.d.ts +45 -0
  9. package/dist/adapters/fake.js +115 -0
  10. package/dist/adapters/grok.d.ts +4 -0
  11. package/dist/adapters/grok.js +122 -0
  12. package/dist/adapters/model-lints.d.ts +6 -0
  13. package/dist/adapters/model-lints.js +130 -0
  14. package/dist/adapters/opencode.d.ts +3 -0
  15. package/dist/adapters/opencode.js +108 -0
  16. package/dist/adapters/pi.d.ts +3 -0
  17. package/dist/adapters/pi.js +191 -0
  18. package/dist/adapters/prompt.d.ts +6 -0
  19. package/dist/adapters/prompt.js +84 -0
  20. package/dist/adapters/registry.d.ts +31 -0
  21. package/dist/adapters/registry.js +165 -0
  22. package/dist/adapters/types.d.ts +98 -0
  23. package/dist/adapters/types.js +56 -0
  24. package/dist/cli/commands/approve.d.ts +1 -0
  25. package/dist/cli/commands/approve.js +72 -0
  26. package/dist/cli/commands/compile.d.ts +1 -0
  27. package/dist/cli/commands/compile.js +23 -0
  28. package/dist/cli/commands/doctor.d.ts +2 -0
  29. package/dist/cli/commands/doctor.js +103 -0
  30. package/dist/cli/commands/init.d.ts +1 -0
  31. package/dist/cli/commands/init.js +32 -0
  32. package/dist/cli/commands/plan.d.ts +2 -0
  33. package/dist/cli/commands/plan.js +101 -0
  34. package/dist/cli/commands/profile.d.ts +1 -0
  35. package/dist/cli/commands/profile.js +56 -0
  36. package/dist/cli/commands/report.d.ts +4 -0
  37. package/dist/cli/commands/report.js +306 -0
  38. package/dist/cli/commands/resume.d.ts +1 -0
  39. package/dist/cli/commands/resume.js +11 -0
  40. package/dist/cli/commands/run.d.ts +1 -0
  41. package/dist/cli/commands/run.js +35 -0
  42. package/dist/cli/commands/scope.d.ts +3 -0
  43. package/dist/cli/commands/scope.js +22 -0
  44. package/dist/cli/commands/status.d.ts +9 -0
  45. package/dist/cli/commands/status.js +162 -0
  46. package/dist/cli/commands/unlock.d.ts +1 -0
  47. package/dist/cli/commands/unlock.js +11 -0
  48. package/dist/cli/index.d.ts +8 -0
  49. package/dist/cli/index.js +65 -0
  50. package/dist/compile/collateral.d.ts +6 -0
  51. package/dist/compile/collateral.js +128 -0
  52. package/dist/compile/common.d.ts +12 -0
  53. package/dist/compile/common.js +56 -0
  54. package/dist/compile/gsd.d.ts +3 -0
  55. package/dist/compile/gsd.js +222 -0
  56. package/dist/compile/index.d.ts +4 -0
  57. package/dist/compile/index.js +33 -0
  58. package/dist/compile/native.d.ts +4 -0
  59. package/dist/compile/native.js +196 -0
  60. package/dist/compile/prd.d.ts +2 -0
  61. package/dist/compile/prd.js +64 -0
  62. package/dist/compile/speckit.d.ts +2 -0
  63. package/dist/compile/speckit.js +66 -0
  64. package/dist/config/config.d.ts +184 -0
  65. package/dist/config/config.js +329 -0
  66. package/dist/drivers/herdr.d.ts +44 -0
  67. package/dist/drivers/herdr.js +394 -0
  68. package/dist/drivers/index.d.ts +3 -0
  69. package/dist/drivers/index.js +13 -0
  70. package/dist/drivers/subprocess.d.ts +25 -0
  71. package/dist/drivers/subprocess.js +106 -0
  72. package/dist/drivers/types.d.ts +61 -0
  73. package/dist/drivers/types.js +77 -0
  74. package/dist/gates/acceptance.d.ts +19 -0
  75. package/dist/gates/acceptance.js +92 -0
  76. package/dist/gates/baseline.d.ts +12 -0
  77. package/dist/gates/baseline.js +97 -0
  78. package/dist/gates/evidence.d.ts +4 -0
  79. package/dist/gates/evidence.js +12 -0
  80. package/dist/gates/llm.d.ts +25 -0
  81. package/dist/gates/llm.js +101 -0
  82. package/dist/gates/review.d.ts +12 -0
  83. package/dist/gates/review.js +80 -0
  84. package/dist/gates/run-gates.d.ts +34 -0
  85. package/dist/gates/run-gates.js +105 -0
  86. package/dist/gates/scope.d.ts +8 -0
  87. package/dist/gates/scope.js +32 -0
  88. package/dist/gates/types.d.ts +6 -0
  89. package/dist/gates/types.js +1 -0
  90. package/dist/graph/graph.d.ts +20 -0
  91. package/dist/graph/graph.js +148 -0
  92. package/dist/graph/schema.d.ts +185 -0
  93. package/dist/graph/schema.js +141 -0
  94. package/dist/index.d.ts +1 -0
  95. package/dist/index.js +1 -0
  96. package/dist/plan/prompt.d.ts +4 -0
  97. package/dist/plan/prompt.js +31 -0
  98. package/dist/plan/scope.d.ts +17 -0
  99. package/dist/plan/scope.js +141 -0
  100. package/dist/report/cost.d.ts +24 -0
  101. package/dist/report/cost.js +89 -0
  102. package/dist/route/preference.d.ts +27 -0
  103. package/dist/route/preference.js +49 -0
  104. package/dist/route/profile.d.ts +65 -0
  105. package/dist/route/profile.js +197 -0
  106. package/dist/route/router.d.ts +26 -0
  107. package/dist/route/router.js +200 -0
  108. package/dist/run/consult.d.ts +23 -0
  109. package/dist/run/consult.js +80 -0
  110. package/dist/run/daemon.d.ts +23 -0
  111. package/dist/run/daemon.js +728 -0
  112. package/dist/run/git.d.ts +12 -0
  113. package/dist/run/git.js +56 -0
  114. package/dist/run/journal.d.ts +91 -0
  115. package/dist/run/journal.js +287 -0
  116. package/dist/run/lock.d.ts +29 -0
  117. package/dist/run/lock.js +182 -0
  118. package/dist/run/merge.d.ts +12 -0
  119. package/dist/run/merge.js +44 -0
  120. package/dist/run/reconcile.d.ts +4 -0
  121. package/dist/run/reconcile.js +95 -0
  122. package/package.json +39 -0
  123. 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,4 @@
1
+ import { type AuthHealth, type WorkerAdapter } from "./types.js";
2
+ export declare function claudeSlug(real: string): string;
3
+ export declare function probeVersion(bin: string): AuthHealth;
4
+ export declare const claudeCode: WorkerAdapter;
@@ -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;