@krtclcdy/tersio 2.25.7 → 2.25.8

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 (120) hide show
  1. package/README.md +17 -10
  2. package/dashboard/dist/index.html +1 -1
  3. package/dist/cli/common.js +25 -3
  4. package/dist/cli/dashboard.js +7 -2
  5. package/dist/cli/doctor-fix.js +12 -7
  6. package/dist/cli/doctor.js +16 -3
  7. package/dist/cli/install.js +5 -5
  8. package/dist/cli/profile.js +28 -3
  9. package/dist/cli/rtk-wiring.js +13 -10
  10. package/dist/cli/usage.js +5 -0
  11. package/dist/extensions/caveman/index.js +3 -6
  12. package/dist/extensions/combo/index.js +1 -6
  13. package/dist/extensions/commands/index.js +7 -2
  14. package/dist/extensions/pstack/index.js +2 -1
  15. package/dist/extensions/rtk/filter.js +45 -4
  16. package/dist/extensions/rtk/session.js +4 -2
  17. package/dist/extensions/server.js +44 -29
  18. package/dist/extensions/shared/agent-stats.js +21 -13
  19. package/dist/extensions/shared/pricing.js +5 -4
  20. package/dist/extensions/shared/rtk-eligible.js +7 -11
  21. package/dist/extensions/shared/rtk-gain.js +2 -6
  22. package/dist/extensions/shared/sqlite.js +1 -6
  23. package/dist/extensions/shared/usage-ledger.js +97 -12
  24. package/dist/extensions/shared/utils.js +33 -0
  25. package/dist/extensions/updater/index.js +15 -7
  26. package/extensions/caveman/index.ts +3 -6
  27. package/extensions/combo/index.ts +1 -6
  28. package/extensions/commands/index.ts +7 -2
  29. package/extensions/pstack/index.ts +2 -1
  30. package/extensions/pstack/playbooks/autopilot-full.md +1 -1
  31. package/extensions/pstack/playbooks/babysit.md +1 -1
  32. package/extensions/pstack/playbooks/eval.md +1 -1
  33. package/extensions/pstack/playbooks/multi-phase-plan.md +2 -2
  34. package/extensions/pstack/playbooks/orchestrate.md +3 -3
  35. package/extensions/pstack/playbooks/session-pickup.md +1 -1
  36. package/extensions/pstack/playbooks/shipping.md +2 -2
  37. package/extensions/pstack/playbooks/worktree-cleanup.md +1 -1
  38. package/extensions/pstack/skills/reflect/SKILL.md +2 -2
  39. package/extensions/pstack/skills/show-me-your-work/SKILL.md +2 -2
  40. package/extensions/rtk/filter.ts +51 -4
  41. package/extensions/rtk/session.ts +4 -2
  42. package/extensions/server.ts +31 -23
  43. package/extensions/shared/agent-stats.ts +21 -12
  44. package/extensions/shared/pricing.ts +7 -5
  45. package/extensions/shared/rtk-eligible.ts +7 -11
  46. package/extensions/shared/rtk-gain.ts +2 -6
  47. package/extensions/shared/sqlite.ts +1 -6
  48. package/extensions/shared/usage-ledger.ts +102 -14
  49. package/extensions/shared/utils.ts +29 -0
  50. package/extensions/updater/index.ts +18 -7
  51. package/package.json +3 -5
  52. package/dist/bench/code-out/pfull-0/debounce.js +0 -13
  53. package/dist/bench/code-out/pfull-0/deepClone.js +0 -18
  54. package/dist/bench/code-out/pfull-0/groupBy.js +0 -15
  55. package/dist/bench/code-out/pfull-1/debounce.js +0 -13
  56. package/dist/bench/code-out/pfull-1/deepClone.js +0 -17
  57. package/dist/bench/code-out/pfull-1/groupBy.js +0 -14
  58. package/dist/bench/code-out/pfull-2/debounce.js +0 -13
  59. package/dist/bench/code-out/pfull-2/deepClone.js +0 -18
  60. package/dist/bench/code-out/pfull-2/groupBy.js +0 -15
  61. package/dist/bench/code-out/pfull-3/debounce.js +0 -12
  62. package/dist/bench/code-out/pfull-3/deepClone.js +0 -17
  63. package/dist/bench/code-out/pfull-3/groupBy.js +0 -7
  64. package/dist/bench/code-out/pfull-4/debounce.js +0 -10
  65. package/dist/bench/code-out/pfull-4/deepClone.js +0 -17
  66. package/dist/bench/code-out/pfull-4/groupBy.js +0 -14
  67. package/dist/bench/code-out/plite-0/debounce.js +0 -13
  68. package/dist/bench/code-out/plite-0/deepClone.js +0 -18
  69. package/dist/bench/code-out/plite-0/groupBy.js +0 -15
  70. package/dist/bench/code-out/plite-1/debounce.js +0 -13
  71. package/dist/bench/code-out/plite-1/deepClone.js +0 -17
  72. package/dist/bench/code-out/plite-1/groupBy.js +0 -11
  73. package/dist/bench/code-out/plite-2/debounce.js +0 -12
  74. package/dist/bench/code-out/plite-2/deepClone.js +0 -17
  75. package/dist/bench/code-out/plite-2/groupBy.js +0 -14
  76. package/dist/bench/code-out/plite-3/debounce.js +0 -12
  77. package/dist/bench/code-out/plite-3/deepClone.js +0 -17
  78. package/dist/bench/code-out/plite-3/groupBy.js +0 -14
  79. package/dist/bench/code-out/plite-4/debounce.js +0 -12
  80. package/dist/bench/code-out/plite-4/deepClone.js +0 -17
  81. package/dist/bench/code-out/plite-4/groupBy.js +0 -14
  82. package/dist/bench/code-out/pultra-0/debounce.js +0 -12
  83. package/dist/bench/code-out/pultra-0/deepClone.js +0 -20
  84. package/dist/bench/code-out/pultra-0/groupBy.js +0 -14
  85. package/dist/bench/code-out/pultra-1/debounce.js +0 -10
  86. package/dist/bench/code-out/pultra-1/deepClone.js +0 -17
  87. package/dist/bench/code-out/pultra-1/groupBy.js +0 -7
  88. package/dist/bench/code-out/pultra-2/debounce.js +0 -13
  89. package/dist/bench/code-out/pultra-2/deepClone.js +0 -18
  90. package/dist/bench/code-out/pultra-2/groupBy.js +0 -15
  91. package/dist/bench/code-out/pultra-3/debounce.js +0 -13
  92. package/dist/bench/code-out/pultra-3/deepClone.js +0 -18
  93. package/dist/bench/code-out/pultra-3/groupBy.js +0 -15
  94. package/dist/bench/code-out/pultra-4/debounce.js +0 -13
  95. package/dist/bench/code-out/pultra-4/deepClone.js +0 -18
  96. package/dist/bench/code-out/pultra-4/groupBy.js +0 -15
  97. package/dist/bench/code-out/run-0/debounce.js +0 -12
  98. package/dist/bench/code-out/run-0/deepClone.js +0 -17
  99. package/dist/bench/code-out/run-0/groupBy.js +0 -14
  100. package/dist/bench/code-out/run-1/debounce.js +0 -13
  101. package/dist/bench/code-out/run-1/deepClone.js +0 -15
  102. package/dist/bench/code-out/run-1/groupBy.js +0 -13
  103. package/dist/bench/code-out/run-2/debounce.js +0 -12
  104. package/dist/bench/code-out/run-2/deepClone.js +0 -18
  105. package/dist/bench/code-out/run-2/groupBy.js +0 -13
  106. package/dist/bench/code-out/run-3/debounce.js +0 -12
  107. package/dist/bench/code-out/run-3/deepClone.js +0 -18
  108. package/dist/bench/code-out/run-3/groupBy.js +0 -14
  109. package/dist/bench/code-out/run-4/debounce.js +0 -12
  110. package/dist/bench/code-out/run-4/deepClone.js +0 -16
  111. package/dist/bench/code-out/run-4/groupBy.js +0 -13
  112. package/dist/extensions/caveman/rule-lite.md +0 -77
  113. package/dist/extensions/caveman/rule-megacave-lite.md +0 -59
  114. package/dist/extensions/caveman/rule-megacave-ultra.md +0 -59
  115. package/dist/extensions/caveman/rule-megacave.md +0 -65
  116. package/dist/extensions/caveman/rule-ultra.md +0 -63
  117. package/dist/extensions/caveman/rule.md +0 -89
  118. package/dist/extensions/pstack/rule.md +0 -141
  119. package/dist/probe-catalog.js +0 -48
  120. package/dist/probe-catalog188.js +0 -44
@@ -12,15 +12,15 @@ Three rules carry the rest.
12
12
 
13
13
  #### Roles and placement
14
14
 
15
- - **Coordinator (this chat).** Local. Frames, authors briefs, drains the inbox, owns the human report, makes judgment calls. It never authors or edits code. Conflicted merges, restacks, and code changes are always tasks. Mechanically landing a verified unit (fast-forward or clean cherry-pick of a worker's commit, then push) is bookkeeping the coordinator may do itself on repos where local git is cheap. Queueing finished work behind an idle stacker is how a deadline harvests nothing. The loop is agentic end to end. Agents are spawned, resumed, and drained only through the Task tool. State reads and writes go through `scripts/orch/orch.ts` at drain points, one command in and one line out. The CLI never spawns, waits, or wakes anything.
15
+ - **Coordinator (this chat).** Local. Frames, authors briefs, drains the inbox, owns the human report, makes judgment calls. It never authors or edits code. Conflicted merges, restacks, and code changes are always tasks. Mechanically landing a verified unit (fast-forward or clean cherry-pick of a worker's commit, then push) is bookkeeping the coordinator may do itself on repos where local git is cheap. Queueing finished work behind an idle stacker is how a deadline harvests nothing. The loop is agentic end to end. Agents are spawned, resumed, and drained only through the Task tool. State reads and writes go through the state store at drain points, one command in and one line out. The store never spawns, waits, or wakes anything.
16
16
  - **Sub-coordinator.** Always local, durable, one per track, and only when the program exceeds what one coordinator's drains can manage. A track the coordinator can drain itself needs no middle layer. Each nested layer re-pays a full orientation preamble, and a blocking sub-coordinator hides its children while the parent idles. Owns its track's units and boards, authors its workers' briefs, spawns its own workers and verifiers (nesting works to depth 3, and a nested spawn has the full Task schema including `environment`). Rolls up aggregates at wave boundaries. Never forwards raw child reports. Cap in-flight children at what one drain can process, roughly ten, as a rolling window. Never as blocking batches, which cost the slowest child of every batch.
17
- - **Worker / verifier.** Always unless the task needs this machine: `control-ui` or `control-cli` runtime verification. Reading local transcripts under `agent-transcripts/`. Simulators and local IDE state. Auth that exists only here. Cloud agents cannot read the local store, so their briefs inline what they need or point at repo paths. Prefer fewer, broader workers. One writer per worktree or branch (principle-separate-before-serializing-shared-state). Run a unit's verifier on a different model family from its worker.
17
+ - **Worker / verifier.** Always unless the task needs this machine: runtime verification on the real surface. Reading local transcripts under the workspace's transcript root. Simulators and local IDE state. Auth that exists only here. Cloud agents cannot read the local store, so their briefs inline what they need or point at repo paths. Prefer fewer, broader workers. One writer per worktree or branch (principle-separate-before-serializing-shared-state). Run a unit's verifier on a different model family from its worker.
18
18
 
19
19
  Depth stays at coordinator, track, worker. Author the track decomposition per project (build, landing, and verification are common cuts, not a required shape). Hard-coded swarm trees were tried and parked as too rigid.
20
20
 
21
21
  #### Store layout
22
22
 
23
- Create `orchestrate/<project-slug>/` in the current agent's store (path in the system prompt). Every file has exactly one writer. Owners publish facts, readers aggregate at read time. Use `bun scripts/orch/orch.ts` for bookkeeping, written below as `orch`, while its canonical plain TSV and JSON stay readable without the CLI.
23
+ Create `orchestrate/<project-slug>/` in the current agent's store (path in the system prompt). Every file has exactly one writer. Owners publish facts, readers aggregate at read time. Keep the bookkeeping in plain TSV and JSON, readable without any CLI.
24
24
 
25
25
  - `preferences.md` is the standing-orders register: numbered lines, one constraint each (model policy, stack shape and count, verification bar, forbidden paths, escalation policy). Paste it verbatim into every spawn and every resume. Directives decay across resumes, and each dropped one costs a human turn. When you catch yourself restating an instruction, append the line before you act (principle-encode-lessons-in-structure).
26
26
  - `overview.md` is the durable PR and issue DB. Append. Never rewrite wholesale per event.
@@ -2,7 +2,7 @@
2
2
 
3
3
  **You own the resume point. Read the prior trail, don't redo it.**
4
4
 
5
- 1. Locate the prior trail. A local transcript under the active workspace's `agent-transcripts/` directory (the system prompt names the path. Do not glob across every workspace's transcript root, that crosses workspace boundaries and reads private chats from unrelated projects), a cloud-agent URL, or a pushed branch. Read the metadata overview and last messages first, then scan back for the decision points. Parse a long transcript in a subagent and keep the reduced timeline in the main thread (the **principle-guard-the-context-window** skill).
5
+ 1. Locate the prior trail. A local transcript under the workspace's transcript root (the system prompt names the path. Do not glob across every workspace's transcript root, that crosses workspace boundaries and reads private chats from unrelated projects), a cloud-agent URL, or a pushed branch. Read the metadata overview and last messages first, then scan back for the decision points. Parse a long transcript in a subagent and keep the reduced timeline in the main thread (the **principle-guard-the-context-window** skill).
6
6
  2. Reconstruct operational state. The branch and worktree, what already landed (`git log`, `git diff` against the base), the open todos, the decisions made. The prior trail is authoritative input. Resist the bias to re-derive it.
7
7
  3. Diff done vs pending. Compare what shipped against what was planned, name the resume point, do not re-run the prior repro or redo completed work. A "let me verify from scratch" pass means you're treating the trail as untrustworthy when it's authoritative.
8
8
  4. Route the remaining work to the matching playbook and pick the verdict: continue the execution, ship a finished recommendation, ratify or override a prior conclusion, or postmortem a failed run. The pickup playbook ends here. The routed playbook owns the rest.
@@ -4,14 +4,14 @@
4
4
 
5
5
  This is the half after `playbooks/babysit.md`.
6
6
 
7
- 1. **Resolve the forge, then verify every PR independently.** GitHub CLI (`gh`) is the default. If `command -v origin` succeeds and Origin can resolve the repository, use `origin pr ...` for view, watch, edit, and merge operations. Otherwise stay on `gh` and record the fallback. Never require Graphite (`gt`). One subagent per PR, not batched, each exercising the real surface with the matching control skill (such as the `control-ui` or `control-cli` skill) against parent versus head. Each returns `PASS`, `PASS+NOTES` or `FAIL` and posts that verdict on its own PR. Safe means a verdict from an agent that did not write the code. CI green is not a verdict, and an approving bot review is not a verdict.
7
+ 1. **Resolve the forge, then verify every PR independently.** GitHub CLI (`gh`) is the default. If `command -v origin` succeeds and Origin can resolve the repository, use `origin pr ...` for view, watch, edit, and merge operations. Otherwise stay on `gh` and record the fallback. Never require Graphite (`gt`). One subagent per PR, not batched, each exercising the real surface through the repo's own control skill where one exists against parent versus head. Each returns `PASS`, `PASS+NOTES` or `FAIL` and posts that verdict on its own PR. Safe means a verdict from an agent that did not write the code. CI green is not a verdict, and an approving bot review is not a verdict.
8
8
  2. **Land only the contiguous verified run rooted at the bottom.** Walk up from the lowest unmerged PR and stop at the first one without a passing verdict, where both `PASS` and `PASS+NOTES` pass. A verified PR sitting above an unverified one is not landable. Report the ceiling as a PR number and say what breaks the chain.
9
9
  3. **Re-check that each verdict still describes the patch.** Record the verdict head SHA, base SHA, and stable `git patch-id` of that PR's base-to-head diff. A rebase or base retarget rewrites SHAs and can silently invalidate a verdict without touching a check. Before landing a PR, compare the recorded patch-id with its current base-to-head patch-id. When the two patches differ only in tests, docs, or lint config, build what each lane ran. Build it twice at the verdict SHA and once at the current head. A difference is noise if the two builds at the verdict SHA also show it, or if it is an embedded commit SHA. Judge each difference, not each file, and report each kind of noise with its files. If only noise differs, that lane's result stays valid, and checks and a review of the change run fresh. Do not reuse a lane result from a dev server or from anything else with no build output. Rerun that lane. Re-verify anything else when the patch changed. When it did not, keep the code verdict but re-run mergeability and CI at the current head. Never use matching commit messages or a green check from an older SHA as a substitute.
10
10
  4. **Prepare only the bottom PR.** Fetch current trunk. Rebase the lowest verified branch onto the exact trunk tip when needed, push it, and retarget only that PR to trunk with `origin pr edit <pr> --base <trunk>` or `gh pr edit <pr> --base <trunk>`. Re-run step 3 after the push. Do not retarget, arm, or merge descendants yet.
11
11
  5. **Land one PR at a time.** If the bottom PR is mergeable now, squash it with `origin pr merge <pr> --squash` or `gh pr merge <pr> --squash`. If requirements are still running and the user asked for merge-when-ready, arm only that PR with `origin pr merge <pr> --squash --auto` or `gh pr merge <pr> --squash --auto`. Origin's `--auto` is Origin merge-when-ready. GitHub's `--auto` is GitHub auto-merge. Wait for that PR to merge before preparing the next one.
12
12
  6. **Do not read GitHub `autoMergeRequest` as stack readiness.** At most it says GitHub auto-merge was requested for one GitHub PR. It does not prove Origin merge-when-ready is armed, that a descendant is queued, that a patch verdict is current, or that the contiguous stack is safe. Confirm the active forge's state for the current bottom PR, and say that the state is unknown if the active forge cannot report it.
13
13
  7. **Recompute after every merge.** Fetch trunk, confirm the merged SHA is present, drop the merged PR from the frozen bottom-to-top list, and inspect the new bottom PR's base, head, checks, and patch-id. A host may retarget a child automatically, but do not assume it did. Repeat steps 3 through 6 for that one PR. Independent work stays outside this chain and ships on its own.
14
- 8. **Watch the current frontier until it merges or fails. Do not mutate the queue around it.** With Origin, use `origin pr view <pr> --checks --comments` and `origin pr checks <pr> --watch`, then re-read the PR until it reports merged or blocked. With GitHub, use `scripts/watch-pr/watch-pr --queued-stack --stack-prs <bottom>` only as an event wake and poll `gh pr view <pr> --json state,mergedAt,mergeStateStatus,statusCheckRollup,autoMergeRequest` after each wake, ignoring `READY` until `mergedAt` is non-null or `state` is `MERGED`. Only then run step 7. Hard-fail only when `state` is `CLOSED` with no `mergedAt`, a required check concludes `FAILURE` or `CANCELLED` and blocks merge after auto-merge is no longer pending, or `mergeStateStatus` is `UNSTABLE` or `DIRTY` with no auto-merge pending. `BLOCKED` while checks are pending or auto-merge is armed is not failure. Do not use Babysit's queued `WAITING`/`merge-queue` stop condition here. Hold the watch under a repeating loop in dynamic mode. Report each merge and the new ceiling. If the queue stalls, diagnose before mutating.
14
+ 8. **Watch the current frontier until it merges or fails. Do not mutate the queue around it.** With Origin, use `origin pr view <pr> --checks --comments` and `origin pr checks <pr> --watch`, then re-read the PR until it reports merged or blocked. With GitHub, use an event wake if the repo has one, and poll `gh pr view <pr> --json state,mergedAt,mergeStateStatus,statusCheckRollup,autoMergeRequest` after each wake, ignoring `READY` until `mergedAt` is non-null or `state` is `MERGED`. Only then run step 7. Hard-fail only when `state` is `CLOSED` with no `mergedAt`, a required check concludes `FAILURE` or `CANCELLED` and blocks merge after auto-merge is no longer pending, or `mergeStateStatus` is `UNSTABLE` or `DIRTY` with no auto-merge pending. `BLOCKED` while checks are pending or auto-merge is armed is not failure. Do not use Babysit's queued `WAITING`/`merge-queue` stop condition here. Hold the watch under a repeating loop in dynamic mode. Report each merge and the new ceiling. If the queue stalls, diagnose before mutating.
15
15
  9. **Stop at the ceiling.** When the verified run is merged, report what landed, what the next unverified PR is, and what verifying it would take. Extending the run is a new pass through step 1.
16
16
 
17
17
  **Reply:** the verified run and its ceiling, each PR's verdict and who produced it, what you armed and how you confirmed it, what landed, and what the next gap needs.
@@ -1,6 +1,6 @@
1
1
  ### Worktree and simulator cleanup
2
2
 
3
- 1. Snapshot and audit. Record `df -h /`, then run `scripts/worktree-audit.sh [-b <pr-base-ref>] [repo-path]` (principle-build-the-lever). Without `-b` it checks `MERGED` against `origin/HEAD`, so pass `-b origin/<branch>` when PRs target another branch. It reads paths from `git worktree list`, never hand-typed, since a hand-typed `myrepo-worktrees/x` misses one that lives in a sibling worktrees directory (principle-encode-lessons-in-structure). It classifies each worktree by size, age, merge state, uncommitted work, PR state, and the newest chat that touched it, then suggests a bucket. The transcript scan is slow, so background it.
3
+ 1. Snapshot and audit. Record `df -h /`, then read paths from `git worktree list` and classify each worktree by size, age, merge state, uncommitted work, PR state, and the newest chat that touched it, then suggest a bucket. The transcript scan is slow, so background it.
4
4
  2. The bucket is advice, not permission. The pinned and active chats are the real artifact (principle-prove-it-works). Get that set from the user or sidebar and cross-check every candidate. The lever has marked `safe` a worktree the user had pinned, so the pinned set wins.
5
5
  3. Verify usage before deleting. For every `verify-recent-chat` row, or anything you doubt, fan subagents out to read the transcripts and report whether the chat is pinned or ongoing and which worktrees it touches (principle-guard-the-context-window, transcripts are bulk). A pinned chat spawns arena and repro trees into sibling worktrees via background subagents, and those are in use even when their names never hit the sidebar.
6
6
  4. Pause on irreversible loss. `wip:N` is N tracked uncommitted edits. Show the diff and get a decision first, since removing a clean worktree is recoverable from its branch but uncommitted work is gone. `scratch:N` is untracked throwaway, safe to drop, but name the files. Per Autonomy, clean and merged and not-in-use proceeds. `wip` and in-use pause.
@@ -11,10 +11,10 @@ Invoke when the user says "reflect" or "the reflect skill". Skip when the conver
11
11
 
12
12
  ## 1. Locate the active transcript
13
13
 
14
- Before fanning out, find this conversation's transcript in the `agent-transcripts/` directory the system prompt names. Do not glob across every workspace's transcript root. That crosses workspace boundaries and reads private chats from unrelated projects.
14
+ Before fanning out, find this conversation's transcript in the transcript root the system prompt names. Do not glob across every workspace's transcript root. That crosses workspace boundaries and reads private chats from unrelated projects.
15
15
 
16
16
  ```bash
17
- ls -t <agent-transcripts>/*.jsonl <agent-transcripts>/*/*.jsonl <agent-transcripts>/*/subagents/*.jsonl 2>/dev/null | head -10
17
+ ls -t <transcript-root>/*.jsonl <transcript-root>/*/*.jsonl <transcript-root>/*/subagents/*.jsonl 2>/dev/null | head -10
18
18
  ```
19
19
 
20
20
  That covers the legacy flat (`<id>.jsonl`), current nested (`<id>/<id>.jsonl`), and subagent (`<parent>/subagents/<child>.jsonl`) layouts. For each candidate, read the first JSONL line. Take the file whose `message.content[0].text` contains the conversation's opening user prompt. If none matches, pass a tight digest of the session instead.
@@ -34,7 +34,7 @@ ts phase decision why evidence result
34
34
 
35
35
  Write each entry the way you'd tell a teammate what you did. Plain words, concrete actions, no AI speak or abstract jargon (the **unslop** skill applies to log text too).
36
36
 
37
- Use the helper `scripts/log.sh <logfile> <phase> <decision> <why> <evidence> <result>`. It stamps `ts`, writes the header on first use, strips stray tabs/newlines, and prefixes any cell starting with `=`, `+`, `-`, or `@` with a single quote. A bare `printf` appending a row works too, but mind those same bytes if cells come from generated or user-supplied text.
37
+ Append a row with `printf`, minding those same bytes if cells come from generated or user-supplied text. Prefix any cell starting with `=`, `+`, `-`, or `@` with a single quote.
38
38
 
39
39
  Log decision points and checkpoints, not every action: a fork chosen, a unit completed with its verification result, a pivot or revert with its trigger, a blocker surfaced, a gate fixed. For loop runs, one row per iteration. Skip the trivial and self-evident.
40
40
 
@@ -53,7 +53,7 @@ Commit it only when the work is ambitious enough that a reviewer needs the trail
53
53
 
54
54
  ## Audit the log against the transcript
55
55
 
56
- At the end of the run, before handing back, check the log told the truth. Read this run's transcript under the active workspace's `agent-transcripts/` directory (the system prompt names the path). Don't glob across every workspace's transcript root. That reads unrelated private chats. Walk this run's rows against what actually happened. Each stretch of them begins at one of this run's `start` rows, or at the first row if this run created the log, and ends at the next `start` row of another run:
56
+ At the end of the run, before handing back, check the log told the truth. Read this run's transcript under the workspace's transcript root (the system prompt names the path). Don't glob across every workspace's transcript root. That reads unrelated private chats. Walk this run's rows against what actually happened. Each stretch of them begins at one of this run's `start` rows, or at the first row if this run created the log, and ends at the next `start` row of another run:
57
57
 
58
58
  - Check that every row maps to a real decision or action.
59
59
  - Check that each row's evidence resolves and shows what the row claims.
@@ -5,6 +5,8 @@ import { join } from 'node:path';
5
5
  import { resolveRtkBinary } from '../shared/utils.ts';
6
6
  import { readRtkDefault } from '../shared/plugin-settings.ts';
7
7
  import { getSharedComboState, lastCustomValue, sessionEntries } from '../shared/session-state.ts';
8
+ import { appendUsage } from '../shared/usage-ledger.ts';
9
+ import { isPiHost } from '../shared/host.ts';
8
10
  import type { ExtensionApi, ExtensionCtx, SessionEntry, ToolResultEvent } from '../shared/types.ts';
9
11
 
10
12
  /** Tool name to `rtk pipe --filter` name. Exported so the OpenCode host reuses one map. */
@@ -18,8 +20,13 @@ export const FILTER_BY_TOOL = new Map([
18
20
  export const MIN_FILTER_BYTES = 1200;
19
21
  const FILTER_TIMEOUT_MS = 5000;
20
22
 
21
- /** Absolute, so a minimal PATH in the host cannot break the spawn. */
22
- const SHELL = '/bin/sh';
23
+ // A shell that understands `<`, resolved per platform; separate flags, because Node quotes each argv element and cmd.exe rejects `"/d /s /c"`.
24
+ /** The pipe's shell, and whether the whole command needs an extra quote pair: `cmd.exe /s` strips the outer one, so a quoted binary and redirect path must be wrapped. */
25
+ export function filterShell(): { shell: string; flags: string[]; wrap: boolean } {
26
+ return process.platform === 'win32'
27
+ ? { shell: process.env.ComSpec || 'cmd.exe', flags: ['/d', '/s', '/c'], wrap: true }
28
+ : { shell: '/bin/sh', flags: ['-c'], wrap: false };
29
+ }
23
30
 
24
31
  export type FilterExec = (
25
32
  cmd: string,
@@ -58,8 +65,10 @@ export async function filterThroughRtk(
58
65
  dir = mkdtempSync(join(tmpdir(), 'tersio-rtk-'));
59
66
  const file = join(dir, 'in.txt');
60
67
  writeFileSync(file, text, 'utf8');
61
- // `rtk pipe` reads stdin only and execFile has no stdin, so one redirect.
62
- const result = await exec(SHELL, ['-c', `"${bin}" pipe -f ${filter} < "${file}"`], { signal, timeout: FILTER_TIMEOUT_MS });
68
+ // `rtk pipe` reads stdin only and execFile has no stdin, so one redirect through the platform shell.
69
+ const { shell, flags, wrap } = filterShell();
70
+ const line = `"${bin}" pipe -f ${filter} < "${file}"`;
71
+ const result = await exec(shell, [...flags, wrap ? `"${line}"` : line], { signal, timeout: FILTER_TIMEOUT_MS });
63
72
  if (result.code !== 0) return null;
64
73
  const out = result.stdout;
65
74
  if (!out.trim() || out.length >= text.length) return null;
@@ -71,10 +80,48 @@ export async function filterThroughRtk(
71
80
  }
72
81
  }
73
82
 
83
+ /** A bash tool call, before it runs. pi and omp hand the extension the mutable input. */
84
+ interface BashCallEvent {
85
+ toolName?: unknown;
86
+ input?: { command?: unknown };
87
+ }
88
+
89
+ /** Ask rtk for the rewritten command; null on any failure, so the original runs. */
90
+ async function rewriteCommand(
91
+ exec: FilterExec,
92
+ bin: string,
93
+ command: string,
94
+ signal?: AbortSignal,
95
+ ): Promise<string | null> {
96
+ try {
97
+ const result = await exec(bin, ['rewrite', command], { timeout: FILTER_TIMEOUT_MS, signal });
98
+ const out = result.stdout.trim();
99
+ return out || null;
100
+ } catch {
101
+ return null;
102
+ }
103
+ }
104
+
74
105
  export default function rtkFilterExtension(pi: ExtensionApi): void {
75
106
  const bin = resolveRtkBinary();
76
107
  const exec = pi.exec;
77
108
  if (!bin || !exec) return;
109
+ // omp records the post-hook command in its transcript, so only pi leaves an audit row here:
110
+ // opencode rewrites in its own plugin hook, and an omp row would double-count the transcript.
111
+ const auditHost = isPiHost(pi) ? 'pi' : null;
112
+ // Registered synchronously, unlike the rtk-init hook, whose async version probe can miss the first call of a fast session.
113
+ pi.on('tool_call', async (event, ctx: ExtensionCtx) => {
114
+ const call = (event ?? {}) as BashCallEvent;
115
+ const input = call.input;
116
+ if (call.toolName !== 'bash' || !input) return;
117
+ const command = input.command;
118
+ if (typeof command !== 'string' || !command.trim() || command.startsWith('rtk ')) return;
119
+ if (!rtkActive(sessionEntries(ctx))) return;
120
+ const rewritten = await rewriteCommand(exec, bin, command, ctx?.signal);
121
+ if (!rewritten || rewritten === command) return;
122
+ input.command = rewritten;
123
+ if (auditHost) appendUsage('rtk-audit', `${auditHost} ${command}`);
124
+ });
78
125
  pi.on<ToolResultEvent>('tool_result', async (event, ctx: ExtensionCtx) => {
79
126
  const filter = FILTER_BY_TOOL.get(String(event?.toolName ?? '').toLowerCase());
80
127
  if (!filter || event?.isError) return;
@@ -1,7 +1,7 @@
1
1
  import { getSharedComboState, hasModeState, isOmpSubagentPrompt, lastCustomValue, reconcileSharedComboEntries, sessionEntries, setSharedComboListener, setSharedComboMode, systemPromptIncludes } from '../shared/session-state.ts';
2
2
  import { injectPromptText, onHostEvent, setExtensionLabel, stringArrayToolParams } from '../shared/host.ts';
3
3
  import { readRtkDefault } from '../shared/plugin-settings.ts';
4
- import { resolveRtkBinary } from '../shared/utils.ts';
4
+ import { resolveRtkBinary, shimCommand } from '../shared/utils.ts';
5
5
  import { announceStatus } from '../shared/status.ts';
6
6
  import type { ExtensionApi, ExtensionCtx, SessionEntry, SystemPromptEvent } from '../shared/types.ts';
7
7
 
@@ -93,7 +93,9 @@ export default function rtkSessionExtension(pi: ExtensionApi): void {
93
93
  onUpdate?.({ content: [{ type: 'text', text: `${bin === null ? '' : 'rtk '}${argv.join(' ')}` }], details: { phase: 'start' } });
94
94
  try {
95
95
  // A GUI-launched host may lack ~/.bun/bin on PATH, so prefer the resolved path.
96
- const result = await pi.exec(bin ?? argv[0], argv, { signal, cwd });
96
+ // A `.cmd` shim cannot be spawned directly on Windows, so it runs through the shell.
97
+ const shim = shimCommand(bin ?? argv[0], argv);
98
+ const result = await pi.exec(shim.cmd, shim.args, { signal, cwd });
97
99
  const text = [result.stdout, result.stderr].filter(Boolean).join('\n');
98
100
  return {
99
101
  isError: result.code !== 0,
@@ -12,7 +12,8 @@ import comboToggleExtension from './combo/index.ts';
12
12
  import tersioCommandsExtension from './commands/index.ts';
13
13
  import { getSharedComboState } from './shared/session-state.ts';
14
14
  import { formatStatus } from './shared/status.ts';
15
- import { resolveRtkBinary } from './shared/utils.ts';
15
+ import { resolveRtkBinary, shimCommand } from './shared/utils.ts';
16
+ import { appendUsage } from './shared/usage-ledger.ts';
16
17
  import type { ExtensionApi, ExtensionCtx, InputEvent, SessionEntry, SystemPromptEvent } from './shared/types.ts';
17
18
 
18
19
  interface CommandConfig {
@@ -221,7 +222,11 @@ const plugin: Plugin.Plugin = {
221
222
  if (sid !== undefined) await ensureSession(sid);
222
223
  if (!rtkActive(sid === undefined ? undefined : sessionEntriesOf(sid))) return;
223
224
  const rewritten = await rewriteCommand(input.command);
224
- if (rewritten) input.command = rewritten;
225
+ if (!rewritten || rewritten === input.command) return;
226
+ const original = input.command;
227
+ input.command = rewritten;
228
+ // The transcript stores the pre-hook command, so the rewrite leaves its own row.
229
+ appendUsage('rtk-audit', `opencode ${original}`);
225
230
  });
226
231
 
227
232
  // Built-in grep and glob output never reaches the bash hook; mutate result.content in place.
@@ -250,15 +255,17 @@ const plugin: Plugin.Plugin = {
250
255
 
251
256
  /** execFile with the promise shape `filterThroughRtk` expects. */
252
257
  function execRtk(cmd: string, args: string[], opts?: { signal?: AbortSignal; timeout?: number }): Promise<{ stdout: string; code: number }> {
253
- return new Promise((resolve) => {
254
- execFile(cmd, args, { signal: opts?.signal, timeout: opts?.timeout }, (error, stdout) => {
255
- const out = stdout ?? '';
256
- if (!error) return resolve({ stdout: out, code: 0 });
257
- // A non-numeric code is a spawn failure (ENOENT, EACCES, killed) and must not read as success.
258
- const raw = (error as { code?: unknown }).code;
259
- resolve({ stdout: out, code: typeof raw === 'number' ? raw : 1 });
260
- });
258
+ const { promise, resolve } = Promise.withResolvers<{ stdout: string; code: number }>();
259
+ // cmd.exe parses its own quoting, so the command string must reach it verbatim: Node's default escaping turns the inner quotes into `\"` and the redirect then fails.
260
+ const shim = shimCommand(cmd, args);
261
+ execFile(shim.cmd, shim.args, { signal: opts?.signal, timeout: opts?.timeout, windowsVerbatimArguments: process.platform === 'win32' }, (error, stdout) => {
262
+ const out = stdout ?? '';
263
+ if (!error) { resolve({ stdout: out, code: 0 }); return; }
264
+ // A non-numeric code is a spawn failure (ENOENT, EACCES, killed) and must not read as success.
265
+ const raw = (error as { code?: unknown }).code;
266
+ resolve({ stdout: out, code: typeof raw === 'number' ? raw : 1 });
261
267
  });
268
+ return promise;
262
269
  }
263
270
 
264
271
  /** `rtk rewrite` exit codes: 0 rewrite found, 1 no equivalent, 3 advisory; the code is the contract. */
@@ -309,20 +316,21 @@ function bundledSkills(): ReturnType<typeof parseSkillFile>[] {
309
316
  function rewriteCommand(command: string): Promise<string | null> {
310
317
  const bin = resolveRtkBinary();
311
318
  if (!bin) return Promise.resolve(null);
312
- return new Promise((resolve) => {
313
- execFile(bin, ['rewrite', command], { timeout: 5000 }, (error, stdout) => {
314
- // A kill or timeout means untrustworthy output; pass the command through.
315
- const err = error as { killed?: boolean; signal?: unknown; code?: unknown } | null;
316
- if (err && (err.killed || err.signal)) return resolve(null);
317
- const code = typeof err?.code === 'number' ? err.code : RTK_REWRITE_FOUND;
318
- // Exit 1 is the only "no equivalent" answer, so anything else with output rewrites.
319
- if (code === 1) return resolve(null);
320
- const out = String(stdout).trim();
321
- if (!out || out === command) return resolve(null);
322
- if (code !== RTK_REWRITE_FOUND && code !== RTK_REWRITE_ADVISORY) return resolve(null);
323
- resolve(out);
324
- });
319
+ const { promise, resolve } = Promise.withResolvers<string | null>();
320
+ // A `.cmd` shim cannot be spawned directly on Windows, so it runs through the shell.
321
+ const shim = shimCommand(bin, ['rewrite', command]);
322
+ execFile(shim.cmd, shim.args, { timeout: 5000 }, (error, stdout) => {
323
+ // A kill or timeout means untrustworthy output; pass the command through.
324
+ const err = error as { killed?: boolean; signal?: unknown; code?: unknown } | null;
325
+ if (err && (err.killed || err.signal)) { resolve(null); return; }
326
+ const code = typeof err?.code === 'number' ? err.code : RTK_REWRITE_FOUND;
327
+ // Exit 1 is the only "no equivalent" answer, so anything else with output rewrites.
328
+ if (code === 1) { resolve(null); return; }
329
+ const out = String(stdout).trim();
330
+ if (!out || out === command || (code !== RTK_REWRITE_FOUND && code !== RTK_REWRITE_ADVISORY)) { resolve(null); return; }
331
+ resolve(out);
325
332
  });
333
+ return promise;
326
334
  }
327
335
 
328
336
  export default plugin;
@@ -3,6 +3,7 @@ import fs from 'node:fs';
3
3
  import os from 'node:os';
4
4
  import path from 'node:path';
5
5
  import { sqliteRead } from './sqlite.ts';
6
+ import { homeDir } from './utils.ts';
6
7
  import { usageDbPath } from './usage-store.ts';
7
8
 
8
9
  export type RangeKey = '1h' | '24h' | '7d' | '30d' | '90d' | 'all';
@@ -83,7 +84,7 @@ export function statsDbPath(): string {
83
84
 
84
85
  /** Quota snapshots. Only omp records these, so this stays omp's own database. */
85
86
  export function agentDbPath(): string {
86
- return process.env.TERSIO_AGENT_SESSIONS_DB ?? path.join(os.homedir(), '.omp', 'agent', 'agent.db');
87
+ return process.env.TERSIO_AGENT_SESSIONS_DB ?? path.join(homeDir(), '.omp', 'agent', 'agent.db');
87
88
  }
88
89
 
89
90
  /** The row shape every query groups by: a message joined to its file, since path and facts live apart. */
@@ -117,14 +118,19 @@ function providerExpr(model: string): string {
117
118
  return `CASE WHEN instr(${model}, '/') = 0 THEN '' ELSE CASE ${named} ${cases} ELSE ${named} END END`;
118
119
  }
119
120
 
120
- /** SQL: the project a transcript belongs to, named by its session folder. A flat session tree uses its data dir. */
121
+ /** SQL: the project from a transcript's session folder; the separator follows the stored path, since a Windows run stores `\`. */
121
122
  function projectExpr(file: string): string {
122
- const marker = `instr(${file}, '/sessions/')`;
123
- const seg = `substr(${file}, ${marker} + 10, instr(substr(${file}, ${marker} + 10) || '/', '/') - 1)`;
124
- const base = `replace(${file}, rtrim(${file}, replace(${file}, '/', '')), '')`;
123
+ // Position of the session marker under either separator; 0 when the path has none.
124
+ const posAt = `instr(${file}, '/sessions/')`;
125
+ const posWin = `instr(${file}, '\\sessions\\')`;
126
+ const at = `CASE WHEN ${posAt} > 0 THEN ${posAt} ELSE ${posWin} END`;
127
+ // Which separator to slice on: the one the matched marker used, else the one the path uses.
128
+ const sep = `CASE WHEN ${posAt} > 0 THEN '/' WHEN ${posWin} > 0 THEN '\\' WHEN instr(${file}, '\\') > 0 THEN '\\' ELSE '/' END`;
129
+ const seg = `substr(${file}, ${at} + 10, instr(substr(${file}, ${at} + 10) || ${sep}, ${sep}) - 1)`;
130
+ const base = `replace(${file}, rtrim(${file}, replace(${file}, ${sep}, '')), '')`;
125
131
  const dir = `substr(${file}, 1, length(${file}) - length(${base}) - 1)`;
126
- const dirBase = `replace(${dir}, rtrim(${dir}, replace(${dir}, '/', '')), '')`;
127
- return `CASE WHEN ${marker} > 0 THEN ${seg} ELSE ${dirBase} END`;
132
+ const dirBase = `replace(${dir}, rtrim(${dir}, replace(${dir}, ${sep}, '')), '')`;
133
+ return `CASE WHEN ${at} > 0 THEN ${seg} ELSE ${dirBase} END`;
128
134
  }
129
135
 
130
136
  /** Every column a query reads, mapped to the store's schema in one place. */
@@ -825,16 +831,18 @@ function foldTraces(rows: AgentTraceRow[]): AgentTraceRow[] {
825
831
  .toSorted((a, b) => b.lastTs - a.lastTs);
826
832
  }
827
833
 
828
- /** The root session file for a transcript, which is itself when it is already a root. */
834
+ /** The root session file for a transcript, which is itself when it is already a root.
835
+ * Splits on whichever separator the stored path uses: a Windows run writes `\`. */
829
836
  export function rootSessionFile(sessionFile: string): string {
830
- const marker = '/sessions/';
837
+ const marker = sessionFile.includes('/sessions/') ? '/sessions/' : '\\sessions\\';
831
838
  const at = sessionFile.indexOf(marker);
832
839
  if (at < 0) return sessionFile;
833
840
  const base = sessionFile.slice(0, at + marker.length);
834
- const parts = sessionFile.slice(at + marker.length).split('/');
841
+ const parts = sessionFile.slice(at + marker.length).split(marker[0]);
835
842
  // `<project>/<root>.jsonl` is a root; `<project>/<root>/<child>.jsonl` belongs to that root.
836
843
  if (parts.length < 3) return sessionFile;
837
- return `${base}${parts[0]}/${parts[1]}.jsonl`;
844
+ // Rejoined with the same separator, so a child's root matches its root file exactly.
845
+ return `${base}${parts[0]}${marker[0]}${parts[1]}.jsonl`;
838
846
  }
839
847
 
840
848
  /** Same normalization as the reference: ids and counters collapse, an HTTP status does not. */
@@ -1304,7 +1312,8 @@ const AGENT_TOOLS = new Set(['task', 'subagent']);
1304
1312
 
1305
1313
  /** One transcript as spans, tracks and markers: a model span runs `ts - duration` to `ts`. */
1306
1314
  export function readSessionTrace(sessionFile: string, limit = TRACE_SPAN_LIMIT): AgentSessionTrace {
1307
- const project = sessionFile.split('/sessions/')[1]?.split('/')[0] ?? '';
1315
+ // Either separator: a Windows host writes `<drive>\...\.omp\agent\sessions\<project>\<file>`.
1316
+ const project = sessionFile.split(/[/\\]sessions[/\\]/)[1]?.split(/[/\\]/)[0] ?? '';
1308
1317
  const empty: AgentSessionTrace = {
1309
1318
  sessionFile, project, title: '', cwd: project, startedAt: 0, endedAt: 0,
1310
1319
  tracks: [], markers: [], summary: emptyTraceSummary(), toolStats: [], truncated: false,
@@ -105,16 +105,18 @@ function asPrice(v: unknown): ModelPrice | null {
105
105
  return null;
106
106
  }
107
107
 
108
- let liveCache: { path: string; mtime: number; prices: LivePrices } | null = null;
108
+ let liveCache: { path: string; mtime: number; size: number; prices: LivePrices } | null = null;
109
109
 
110
110
  export function loadLivePrices(): LivePrices | null {
111
- // One stat per call beats one full parse: the recent-rows map prices thousands of rows.
111
+ // One stat per call beats a full parse; the key carries size and mtime, because Windows rounds a rapid rewrite's mtime to one value.
112
112
  try {
113
113
  const file = pricesCachePath();
114
- const mtime = fs.statSync(file).mtimeMs;
115
- if (liveCache && liveCache.path === file && liveCache.mtime === mtime) return liveCache.prices;
114
+ const stat = fs.statSync(file);
115
+ if (liveCache && liveCache.path === file && liveCache.mtime === stat.mtimeMs && liveCache.size === stat.size) {
116
+ return liveCache.prices;
117
+ }
116
118
  const prices = readLivePrices();
117
- if (prices) liveCache = { path: file, mtime, prices };
119
+ if (prices) liveCache = { path: file, mtime: stat.mtimeMs, size: stat.size, prices };
118
120
  return prices;
119
121
  } catch {
120
122
  return null;
@@ -1,11 +1,8 @@
1
- // Eligible command heads, taken from rtk's own contract rather than a hardcoded guess.
2
- // `rtk rewrite <probe>` prints a rewrite and exits 0 or 3 when rtk handles the command,
3
- // and exits 1 when no equivalent exists. Each probe below is a representative workload
4
- // command, because subcommand-scoped tools such as `gh` rewrite `gh pr list` but not `gh`.
1
+ // Eligible heads come from rtk's own contract: `rtk rewrite <probe>` exits 0 or 3 when rtk handles the command, 1 when it has no equivalent.
5
2
  import { execFileSync } from 'node:child_process';
6
3
  import fs from 'node:fs';
7
4
  import path from 'node:path';
8
- import { normalizeRtkVersion, tersioDataPath } from './utils.ts';
5
+ import { normalizeRtkVersion, shimCommand, tersioDataPath } from './utils.ts';
9
6
 
10
7
  /** Verified against rtk 1.0.1; the drift test fails when rtk's set moves. */
11
8
  export const FALLBACK_ELIGIBLE_HEADS: readonly string[] = [
@@ -33,7 +30,8 @@ const PROBES: Record<string, string> = {
33
30
 
34
31
  function probe(bin: string, command: string): boolean {
35
32
  try {
36
- const out = execFileSync(bin, ['rewrite', command], { encoding: 'utf8', timeout: 5000, windowsHide: true });
33
+ const shim = shimCommand(bin, ['rewrite', command]);
34
+ const out = execFileSync(shim.cmd, shim.args, { encoding: 'utf8', timeout: 5000, windowsHide: true });
37
35
  return out.trim().length > 0;
38
36
  } catch (e) {
39
37
  // Non-zero exit throws; exit 3 still prints a rewrite, exits 1 and 2 do not.
@@ -54,16 +52,14 @@ export function deriveEligibleHeads(bin: string): Set<string> {
54
52
 
55
53
  function rtkVersion(bin: string): string {
56
54
  try {
57
- return normalizeRtkVersion(execFileSync(bin, ['--version'], { encoding: 'utf8', timeout: 5000, windowsHide: true }));
55
+ const shim = shimCommand(bin, ['--version']);
56
+ return normalizeRtkVersion(execFileSync(shim.cmd, shim.args, { encoding: 'utf8', timeout: 5000, windowsHide: true }));
58
57
  } catch {
59
58
  return '';
60
59
  }
61
60
  }
62
61
 
63
- /**
64
- * Heads rtk rewrites, cached per binary version so an upgrade re-derives once.
65
- * Without a binary, or on any read failure, the verified fallback stands.
66
- */
62
+ /** Heads rtk rewrites, cached per binary version; without a binary, or on any read failure, the verified fallback stands. */
67
63
  export function eligibleHeads(bin: string | null): ReadonlySet<string> {
68
64
  if (!bin) return new Set(FALLBACK_ELIGIBLE_HEADS);
69
65
  const version = rtkVersion(bin);
@@ -1,7 +1,7 @@
1
1
  // Savings read from rtk history, never estimated; modes bench in docs/BENCHMARK_GUIDE.md.
2
2
  import fs from 'node:fs';
3
- import os from 'node:os';
4
3
  import path from 'node:path';
4
+ import { rtkDirs } from './utils.ts';
5
5
  import { sqliteRead } from './sqlite.ts';
6
6
 
7
7
  export interface RtkCommandRow {
@@ -32,11 +32,7 @@ const CMD_ONE_LINE = `replace(replace(replace(rtk_cmd, char(10), ' '), char(13),
32
32
  export function rtkDbPath(): string {
33
33
  const override = process.env.TERSIO_RTK_DB;
34
34
  if (override) return override;
35
- if (process.platform === 'darwin') {
36
- return path.join(os.homedir(), 'Library', 'Application Support', 'rtk', 'history.db');
37
- }
38
- const xdg = process.env.XDG_DATA_HOME || path.join(os.homedir(), '.local', 'share');
39
- return path.join(xdg, 'rtk', 'history.db');
35
+ return path.join(rtkDirs().data, 'history.db');
40
36
  }
41
37
 
42
38
  function query(db: string, sql: string): string[][] {
@@ -1,9 +1,4 @@
1
- // One place that runs SQL against a database file. Bun and Node 22.5+ carry SQLite in process, so the
2
- // `sqlite3` binary is a fallback: hosts without it kept silently reading nothing. Reads return the sqlite3
3
- // CLI shape (one row per line, cells tab-joined, NULL an empty cell) because every caller parses that.
4
- // The write path leaves the journal mode alone: the schema sets WAL so a reader never blocks a writer,
5
- // and the pragma needs exclusive access, so with a reader holding the file it fails after the caller's
6
- // rows have already committed (see docs/ERRORS.md).
1
+ // One place that runs SQL: Bun or Node 22.5+ in process, the `sqlite3` binary as the fallback, and rows in its CLI shape for every caller. The write path leaves the journal mode alone (see docs/ERRORS.md).
7
2
  import { execFileSync } from 'node:child_process';
8
3
  import fs from 'node:fs';
9
4
  import { createRequire } from 'node:module';