@krtclcdy/tersio 2.25.6 → 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 (149) hide show
  1. package/README.md +43 -13
  2. package/dashboard/dist/index.html +1 -1
  3. package/dist/cli/banner.js +1 -2
  4. package/dist/cli/common.js +26 -4
  5. package/dist/cli/dashboard.js +7 -2
  6. package/dist/cli/doctor-fix.js +12 -7
  7. package/dist/cli/doctor.js +64 -15
  8. package/dist/cli/hosts.js +1 -2
  9. package/dist/cli/install.js +12 -7
  10. package/dist/cli/manifest.js +25 -5
  11. package/dist/cli/profile.js +28 -3
  12. package/dist/cli/rtk-wiring.js +13 -10
  13. package/dist/cli/uninstall.js +1 -1
  14. package/dist/cli/usage.js +5 -0
  15. package/dist/extensions/caveman/index.js +4 -3
  16. package/dist/extensions/combo/index.js +21 -7
  17. package/dist/extensions/commands/index.js +7 -2
  18. package/dist/extensions/pstack/index.js +5 -2
  19. package/dist/extensions/rtk/filter.js +45 -4
  20. package/dist/extensions/rtk/session.js +21 -11
  21. package/dist/extensions/server.js +44 -29
  22. package/dist/extensions/shared/agent-stats.js +21 -13
  23. package/dist/extensions/shared/pricing.js +5 -4
  24. package/dist/extensions/shared/rtk-eligible.js +80 -0
  25. package/dist/extensions/shared/rtk-gain.js +3 -7
  26. package/dist/extensions/shared/sqlite.js +2 -13
  27. package/dist/extensions/shared/status.js +1 -2
  28. package/dist/extensions/shared/usage-ledger.js +196 -29
  29. package/dist/extensions/shared/utils.js +33 -0
  30. package/dist/extensions/updater/index.js +15 -7
  31. package/extensions/caveman/index.ts +4 -3
  32. package/extensions/caveman/rule-lite.md +77 -0
  33. package/extensions/caveman/rule-megacave-lite.md +59 -0
  34. package/extensions/caveman/rule-megacave-ultra.md +59 -0
  35. package/extensions/caveman/rule.md +1 -1
  36. package/extensions/combo/index.ts +22 -7
  37. package/extensions/commands/index.ts +7 -2
  38. package/extensions/pstack/index.ts +5 -2
  39. package/extensions/pstack/playbooks/authoring-a-skill.md +12 -0
  40. package/extensions/pstack/playbooks/autonomous-run.md +13 -0
  41. package/extensions/pstack/playbooks/autopilot-full.md +13 -0
  42. package/extensions/pstack/playbooks/autopilot-stack.md +16 -0
  43. package/extensions/pstack/playbooks/babysit.md +23 -0
  44. package/extensions/pstack/playbooks/bug-fix.md +15 -0
  45. package/extensions/pstack/playbooks/eval.md +25 -0
  46. package/extensions/pstack/playbooks/feature.md +21 -0
  47. package/extensions/pstack/playbooks/hillclimb.md +21 -0
  48. package/extensions/pstack/playbooks/investigation.md +14 -0
  49. package/extensions/pstack/playbooks/multi-phase-plan.md +155 -0
  50. package/extensions/pstack/playbooks/opening-a-pr.md +34 -0
  51. package/extensions/pstack/playbooks/orchestrate.md +111 -0
  52. package/extensions/pstack/playbooks/pause-safely.md +10 -0
  53. package/extensions/pstack/playbooks/perf-issue.md +25 -0
  54. package/extensions/pstack/playbooks/prototype.md +14 -0
  55. package/extensions/pstack/playbooks/refactoring.md +16 -0
  56. package/extensions/pstack/playbooks/runtime-forensics.md +11 -0
  57. package/extensions/pstack/playbooks/session-pickup.md +11 -0
  58. package/extensions/pstack/playbooks/shipping.md +17 -0
  59. package/extensions/pstack/playbooks/trace-forensics.md +14 -0
  60. package/extensions/pstack/playbooks/visual-parity.md +11 -0
  61. package/extensions/pstack/playbooks/worktree-cleanup.md +12 -0
  62. package/extensions/pstack/principles/attack-the-premise.md +22 -0
  63. package/extensions/pstack/principles/boundary-discipline.md +33 -0
  64. package/extensions/pstack/principles/build-the-lever.md +22 -0
  65. package/extensions/pstack/principles/encode-lessons-in-structure.md +10 -0
  66. package/extensions/pstack/principles/exhaust-the-design-space.md +8 -0
  67. package/extensions/pstack/principles/experience-first.md +18 -0
  68. package/extensions/pstack/principles/explain-the-number.md +16 -0
  69. package/extensions/pstack/principles/fix-root-causes.md +23 -0
  70. package/extensions/pstack/principles/foundational-thinking.md +20 -0
  71. package/extensions/pstack/principles/guard-the-context-window.md +11 -0
  72. package/extensions/pstack/principles/laziness-protocol.md +9 -0
  73. package/extensions/pstack/principles/make-operations-idempotent.md +23 -0
  74. package/extensions/pstack/principles/migrate-callers-then-delete-legacy-apis.md +21 -0
  75. package/extensions/pstack/principles/minimize-reader-load.md +17 -0
  76. package/extensions/pstack/principles/model-the-domain.md +23 -0
  77. package/extensions/pstack/principles/never-block-on-the-human.md +14 -0
  78. package/extensions/pstack/principles/outcome-oriented-execution.md +20 -0
  79. package/extensions/pstack/principles/prove-it-works.md +21 -0
  80. package/extensions/pstack/principles/redesign-from-first-principles.md +15 -0
  81. package/extensions/pstack/principles/separate-before-serializing-shared-state.md +15 -0
  82. package/extensions/pstack/principles/sequence-verifiable-units.md +16 -0
  83. package/extensions/pstack/principles/subtract-before-you-add.md +17 -0
  84. package/extensions/pstack/principles/test-behavior-not-implementation.md +20 -0
  85. package/extensions/pstack/principles/type-system-discipline.md +17 -0
  86. package/extensions/pstack/references/bugbot-triage.md +142 -0
  87. package/extensions/pstack/rule.md +88 -12
  88. package/extensions/pstack/skills/architect/SKILL.md +78 -0
  89. package/extensions/pstack/skills/architect/references/design-red-flags.md +57 -0
  90. package/extensions/pstack/skills/architect/references/rationale-template.md +35 -0
  91. package/extensions/pstack/skills/architect/references/runner-prompt.md +20 -0
  92. package/extensions/pstack/skills/arena/SKILL.md +47 -0
  93. package/extensions/pstack/skills/benchmark-checklist/SKILL.md +38 -0
  94. package/extensions/pstack/skills/blast-radius/SKILL.md +49 -0
  95. package/extensions/pstack/skills/bro/SKILL.md +6 -0
  96. package/extensions/pstack/skills/correct/SKILL.md +31 -0
  97. package/extensions/pstack/skills/figure-it-out/SKILL.md +51 -0
  98. package/extensions/pstack/skills/how/SKILL.md +35 -0
  99. package/extensions/pstack/skills/how/references/explainer-prompt.md +55 -0
  100. package/extensions/pstack/skills/how/references/explorer-prompt.md +35 -0
  101. package/extensions/pstack/skills/interrogate/SKILL.md +73 -0
  102. package/extensions/pstack/skills/interrogate/references/code-quality-review.md +47 -0
  103. package/extensions/pstack/skills/interrogate/references/lead-judgment.md +58 -0
  104. package/extensions/pstack/skills/interrogate/references/reviewer-prompt.md +62 -0
  105. package/extensions/pstack/skills/interrogate/references/rubric.md +77 -0
  106. package/extensions/pstack/skills/no-comments/SKILL.md +23 -0
  107. package/extensions/pstack/skills/reflect/SKILL.md +66 -0
  108. package/extensions/pstack/skills/reflect/references/divergent-reviewer.md +38 -0
  109. package/extensions/pstack/skills/reflect/references/judgment-reviewer.md +37 -0
  110. package/extensions/pstack/skills/reflect/references/synthesizer.md +56 -0
  111. package/extensions/pstack/skills/reflect/references/tooling-reviewer.md +48 -0
  112. package/extensions/pstack/skills/show-me-your-work/SKILL.md +81 -0
  113. package/extensions/pstack/skills/swarm/SKILL.md +45 -0
  114. package/extensions/pstack/skills/tdd/SKILL.md +41 -0
  115. package/extensions/pstack/skills/technical-writing/SKILL.md +105 -0
  116. package/extensions/pstack/skills/unslop/SKILL.md +61 -0
  117. package/extensions/pstack/skills/why/SKILL.md +97 -0
  118. package/extensions/pstack/skills/why/references/epistemics.md +71 -0
  119. package/extensions/pstack/skills/why/references/investigator-prompt.md +84 -0
  120. package/extensions/pstack/skills/why/references/source-playbook.md +17 -0
  121. package/extensions/pstack/skills/why/references/sources/code-archaeology.md +51 -0
  122. package/extensions/pstack/skills/why/references/sources/databricks.md +70 -0
  123. package/extensions/pstack/skills/why/references/sources/datadog.md +97 -0
  124. package/extensions/pstack/skills/why/references/sources/incident-postmortem.md +5 -0
  125. package/extensions/pstack/skills/why/references/sources/linear.md +48 -0
  126. package/extensions/pstack/skills/why/references/sources/notion.md +55 -0
  127. package/extensions/pstack/skills/why/references/sources/sentry.md +66 -0
  128. package/extensions/pstack/skills/why/references/sources/slack.md +17 -0
  129. package/extensions/pstack/skills/why/references/synthesizer-prompt.md +129 -0
  130. package/extensions/rtk/filter.ts +51 -4
  131. package/extensions/rtk/session.ts +20 -10
  132. package/extensions/server.ts +31 -23
  133. package/extensions/shared/agent-stats.ts +21 -12
  134. package/extensions/shared/pricing.ts +7 -5
  135. package/extensions/shared/rtk-eligible.ts +79 -0
  136. package/extensions/shared/rtk-gain.ts +3 -7
  137. package/extensions/shared/sqlite.ts +2 -13
  138. package/extensions/shared/status.ts +1 -2
  139. package/extensions/shared/usage-ledger.ts +198 -32
  140. package/extensions/shared/utils.ts +29 -0
  141. package/extensions/timing/index.ts +1 -2
  142. package/extensions/updater/index.ts +18 -7
  143. package/package.json +8 -7
  144. package/dist/extensions/caveman/rule-megacave.md +0 -65
  145. package/dist/extensions/caveman/rule-ultra.md +0 -63
  146. package/dist/extensions/caveman/rule.md +0 -89
  147. package/dist/extensions/pstack/rule.md +0 -65
  148. package/dist/probe-catalog.js +0 -48
  149. package/dist/probe-catalog188.js +0 -44
@@ -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;
@@ -0,0 +1,79 @@
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.
2
+ import { execFileSync } from 'node:child_process';
3
+ import fs from 'node:fs';
4
+ import path from 'node:path';
5
+ import { normalizeRtkVersion, shimCommand, tersioDataPath } from './utils.ts';
6
+
7
+ /** Verified against rtk 1.0.1; the drift test fails when rtk's set moves. */
8
+ export const FALLBACK_ELIGIBLE_HEADS: readonly string[] = [
9
+ 'rtk', 'git', 'gh', 'glab', 'docker', 'kubectl', 'psql', 'aws', 'bunx', 'npx',
10
+ 'pytest', 'mypy', 'cat', 'grep', 'rg', 'find', 'tree', 'diff', 'ls', 'ast-grep',
11
+ 'wget', 'prettier', 'playwright', 'prisma', 'jest', 'vitest', 'ctest', 'lint',
12
+ 'wc', 'tsc', 'curl', 'cargo', 'npm', 'pnpm', 'bun', 'go', 'ruff', 'next',
13
+ 'dotnet', 'oc', 'jq', 'make',
14
+ ];
15
+
16
+ const PROBES: Record<string, string> = {
17
+ git: 'git log', gh: 'gh pr list', glab: 'glab mr list', docker: 'docker ps',
18
+ kubectl: 'kubectl get pods', psql: 'psql --version', aws: 'aws --version',
19
+ bunx: 'bunx --version', npx: 'npx --version', pytest: 'pytest --version',
20
+ mypy: 'mypy --version', cat: 'cat README.md', grep: 'grep rtk README.md',
21
+ rg: 'rg rtk README.md', find: 'find . -name x', tree: 'tree .', diff: 'diff a b',
22
+ ls: 'ls -la', 'ast-grep': 'ast-grep --version', wget: 'wget --version',
23
+ prettier: 'prettier --version', playwright: 'playwright --version', prisma: 'prisma --version',
24
+ jest: 'jest --version', vitest: 'vitest --version', ctest: 'ctest --version',
25
+ lint: 'lint .', wc: 'wc -l x', tsc: 'tsc --version', curl: 'curl --version',
26
+ cargo: 'cargo test', npm: 'npm run build', pnpm: 'pnpm run build', bun: 'bun run test',
27
+ go: 'go test ./...', ruff: 'ruff check .', next: 'next build', dotnet: 'dotnet build',
28
+ oc: 'oc get pods', jq: 'jq . x', make: 'make',
29
+ };
30
+
31
+ function probe(bin: string, command: string): boolean {
32
+ try {
33
+ const shim = shimCommand(bin, ['rewrite', command]);
34
+ const out = execFileSync(shim.cmd, shim.args, { encoding: 'utf8', timeout: 5000, windowsHide: true });
35
+ return out.trim().length > 0;
36
+ } catch (e) {
37
+ // Non-zero exit throws; exit 3 still prints a rewrite, exits 1 and 2 do not.
38
+ const err = e as { status?: number; stdout?: string | Buffer };
39
+ if (err.status !== 0 && err.status !== 3) return false;
40
+ return String(err.stdout ?? '').trim().length > 0;
41
+ }
42
+ }
43
+
44
+ /** One set of heads for this rtk build, read straight from its rewrite contract. */
45
+ export function deriveEligibleHeads(bin: string): Set<string> {
46
+ const heads = new Set<string>(['rtk']);
47
+ for (const [head, command] of Object.entries(PROBES)) {
48
+ if (probe(bin, command)) heads.add(head);
49
+ }
50
+ return heads;
51
+ }
52
+
53
+ function rtkVersion(bin: string): string {
54
+ try {
55
+ const shim = shimCommand(bin, ['--version']);
56
+ return normalizeRtkVersion(execFileSync(shim.cmd, shim.args, { encoding: 'utf8', timeout: 5000, windowsHide: true }));
57
+ } catch {
58
+ return '';
59
+ }
60
+ }
61
+
62
+ /** Heads rtk rewrites, cached per binary version; without a binary, or on any read failure, the verified fallback stands. */
63
+ export function eligibleHeads(bin: string | null): ReadonlySet<string> {
64
+ if (!bin) return new Set(FALLBACK_ELIGIBLE_HEADS);
65
+ const version = rtkVersion(bin);
66
+ const file = tersioDataPath('rtk-eligible.json', 'rtk-eligible.json');
67
+ try {
68
+ const cached = JSON.parse(fs.readFileSync(file, 'utf8')) as { version?: unknown; heads?: unknown };
69
+ if (cached.version === version && Array.isArray(cached.heads) && cached.heads.every((h) => typeof h === 'string')) {
70
+ return new Set(cached.heads as string[]);
71
+ }
72
+ } catch { /* derive below */ }
73
+ const heads = deriveEligibleHeads(bin);
74
+ try {
75
+ fs.mkdirSync(path.dirname(file), { recursive: true });
76
+ fs.writeFileSync(file, `${JSON.stringify({ version, heads: [...heads] })}\n`, 'utf8');
77
+ } catch { /* the cache is best-effort */ }
78
+ return heads;
79
+ }
@@ -1,7 +1,7 @@
1
- // Savings read from rtk history, never estimated; modes bench in docs/BENCHMARK.md.
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,14 +1,4 @@
1
- // One place that runs SQL against a database file.
2
- //
3
- // Bun and Node 22.5+ carry SQLite in process, so the `sqlite3` binary is a fallback rather than a
4
- // requirement: hosts without it kept silently reading nothing. Reads come back exactly as the sqlite3
5
- // CLI printed them -- rows on their own lines, cells joined by tabs, NULL an empty cell, one trailing
6
- // newline -- because every caller already parses that shape. The engine rungs take one statement per
7
- // read, so a caller with several queries calls once per query.
8
- //
9
- // The write path leaves the journal mode alone. The store's own schema sets WAL so a reader (the
10
- // dashboard) never blocks a writer, and changing the mode needs exclusive access: with a reader holding
11
- // the file the pragma fails, which is a failure reported after the caller's rows have already committed.
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).
12
2
  import { execFileSync } from 'node:child_process';
13
3
  import fs from 'node:fs';
14
4
  import { createRequire } from 'node:module';
@@ -65,8 +55,7 @@ function engine(): Engine | null {
65
55
  } catch { /* not Bun, or no SQLite in this build */ }
66
56
  try {
67
57
  const { DatabaseSync } = requireOptional('node:sqlite') as { DatabaseSync: new (db: string, opts?: { readOnly?: boolean }) => NodeDatabase };
68
- // Rows arrive keyed by column name unless the statement returns arrays. A duplicate name collapses
69
- // its key and shifts every later cell, so a Node that cannot return arrays is no engine at all.
58
+ // Rows arrive keyed by column name unless the statement returns arrays: a duplicate name collapses its key and shifts every later cell, so a Node that cannot return arrays is no engine.
70
59
  const probe = new DatabaseSync(':memory:');
71
60
  try {
72
61
  if (typeof probe.prepare('SELECT 1').setReturnArrays !== 'function') throw new Error('no positional rows');
@@ -38,8 +38,7 @@ let remembered: ExtensionCtx | undefined;
38
38
  let lastText: string | undefined;
39
39
  let lastUi: UiApi | undefined;
40
40
 
41
- // Paint the host footer bar; same text on the same UI is skipped, a new ctx repaints it.
42
- // One key only: the combo line already names the caveman mode, and a second key renders as a second footer line.
41
+ // Paint the host footer bar; same text on the same UI is skipped. One key only: the combo line already names the caveman mode, and a second key renders as a second footer line.
43
42
  export function announceStatus(ctx: ExtensionCtx | undefined): void {
44
43
  const state = getSharedComboState();
45
44
  const text = formatStatus(state);