@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
@@ -81,8 +81,7 @@ function printWelcome() {
81
81
  'tersio update refresh CLI + add-ons',
82
82
  'tersio doctor verify the install',
83
83
  ];
84
- // Below 26 rows the note scrolls off the top and slices the art, and the screen must also hold the
85
- // update prompt and every option, so the tips take one line there.
84
+ // Below 26 rows the tips scroll off the top and slice the art, so they take one line there.
86
85
  if (process.stdout.rows < 26) {
87
86
  clackNote(` ${tips.join(' · ')}`);
88
87
  return;
@@ -1,6 +1,6 @@
1
1
  // cli/common.ts — shared foundation: CLI flags, paths, exec, file/config helpers.
2
2
  import { execFile } from 'node:child_process';
3
- import { promises as fs } from 'node:fs';
3
+ import { existsSync, promises as fs } from 'node:fs';
4
4
  import path from 'node:path';
5
5
  import { promisify } from 'node:util';
6
6
  import { createRequire } from 'node:module';
@@ -14,6 +14,10 @@ const OMP_AGENT_DIR = path.join(HOME, '.omp', 'agent');
14
14
  const OMP_PLUGINS_DIR = path.join(HOME, '.omp', 'plugins');
15
15
  const BUN_BIN_DIR = path.join(HOME, '.bun', 'bin');
16
16
  const OMP_BIN = IS_WINDOWS ? 'omp.cmd' : 'omp';
17
+ // npm and bun are `.cmd` shims on Windows, and CreateProcess spawns neither a bare name nor a shim,
18
+ // so the spawn name itself carries the extension the way `omp.cmd` does.
19
+ const NPM_BIN = IS_WINDOWS ? 'npm.cmd' : 'npm';
20
+ const BUN_BIN = IS_WINDOWS ? 'bun.cmd' : 'bun';
17
21
  const PACKAGE_NAME = '@krtclcdy/tersio';
18
22
  const PACKAGE_BIN = 'tersio';
19
23
  const { version: PACKAGE_VERSION } = createRequire(import.meta.url)(path.join(findPackageRoot(path.dirname(fileURLToPath(import.meta.url))), 'package.json'));
@@ -112,8 +116,26 @@ function debug(...a) {
112
116
  console.log(' [debug]', ...a);
113
117
  }
114
118
  const execFileP = promisify(execFile);
119
+ /** Windows has no real `omp`, `npm`, or `pgrep`: `.cmd` shims throw EINVAL when spawned, so a bare name wraps through cmd.exe only when a `<name>.cmd` really sits on PATH. */
120
+ export function shimCommand(cmd, args) {
121
+ if (!IS_WINDOWS)
122
+ return { cmd, args };
123
+ if (/\.(?:cmd|bat)$/i.test(cmd)) {
124
+ return { cmd: process.env.ComSpec || 'cmd.exe', args: ['/d', '/s', '/c', cmd, ...args] };
125
+ }
126
+ if (hasCmdShim(cmd)) {
127
+ return { cmd: process.env.ComSpec || 'cmd.exe', args: ['/d', '/s', '/c', cmd, ...args] };
128
+ }
129
+ return { cmd, args };
130
+ }
131
+ function hasCmdShim(cmd) {
132
+ if (cmd.includes('/') || cmd.includes('\\'))
133
+ return false;
134
+ return (process.env.PATH || '').split(path.delimiter).some((dir) => dir && existsSync(path.join(dir, `${cmd}.cmd`)));
135
+ }
115
136
  async function execP(cmd, args, opts = {}) {
116
- return execFileP(cmd, args, {
137
+ const shim = shimCommand(cmd, args);
138
+ return execFileP(shim.cmd, shim.args, {
117
139
  timeout: opts.timeout || 120000,
118
140
  encoding: 'utf8',
119
141
  ...opts,
@@ -131,7 +153,7 @@ async function writeIfChanged(dest, content, options = {}) {
131
153
  return true;
132
154
  }
133
155
  await fs.mkdir(path.dirname(dest), { recursive: true });
134
- if (existing !== null) {
156
+ if (existing !== null && options.backup === true) {
135
157
  await fs.copyFile(dest, `${dest}.bak`);
136
158
  debug(`${path.basename(dest)} → ${path.basename(dest)}.bak`);
137
159
  }
@@ -316,4 +338,4 @@ function relTime(ageMs) {
316
338
  const weeks = Math.floor(days / 7);
317
339
  return `${weeks} week${weeks === 1 ? '' : 's'} ago`;
318
340
  }
319
- export { IS_WINDOWS, RTK_BINARY_NAME, HOME, OMP_AGENT_DIR, OMP_PLUGINS_DIR, BUN_BIN_DIR, OMP_BIN, PACKAGE_NAME, PACKAGE_BIN, PACKAGE_VERSION, CAVEMAN_DEFAULTS, PONYTAIL_DEFAULTS, PSTACK_DEFAULTS, RTK_DEFAULTS, DIAG_SCHEDULES, COMBO_PRESET_MODES, COMBO_DEFAULTS, parseEnum, flagValue, COMMANDS, commandArg, command, settingArg, unknownCommand, install, reinstall, update, showVersion, showHelp, applyUpdate, args, dryRun, yes, verbose, doctor, fix, uninstall, usage, dashboard, reset, settings, allowUnverified, dashboardPort, dashboardOpen, dashboardExport, currency, currencyGiven, removePonytail, keepPonytail, removeRtk, comboDefaultFlag, cavemanDefaultFlag, ponytailDefaultFlag, pstackDefaultFlag, rtkDefaultFlag, diagScheduleFlag, profileFlagsGiven, subagentMarkerFlags, subagentMarkersGiven, debug, execFileP, execP, writeIfChanged, normalizeExtensionsKey, writeConfigLines, ensureExtensionInConfig, removeExtensionFromConfig, readPonytailConfig, parseJsonObject, parsePonytailConfig, patchPonytailConfig, ensurePonytailConfigValue, readPluginsPackage, relTime, bundledPonytailSpec, ponytailOverride, };
341
+ export { IS_WINDOWS, RTK_BINARY_NAME, HOME, OMP_AGENT_DIR, OMP_PLUGINS_DIR, BUN_BIN_DIR, OMP_BIN, NPM_BIN, BUN_BIN, PACKAGE_NAME, PACKAGE_BIN, PACKAGE_VERSION, CAVEMAN_DEFAULTS, PONYTAIL_DEFAULTS, PSTACK_DEFAULTS, RTK_DEFAULTS, DIAG_SCHEDULES, COMBO_PRESET_MODES, COMBO_DEFAULTS, parseEnum, flagValue, COMMANDS, commandArg, command, settingArg, unknownCommand, install, reinstall, update, showVersion, showHelp, applyUpdate, args, dryRun, yes, verbose, doctor, fix, uninstall, usage, dashboard, reset, settings, allowUnverified, dashboardPort, dashboardOpen, dashboardExport, currency, currencyGiven, removePonytail, keepPonytail, removeRtk, comboDefaultFlag, cavemanDefaultFlag, ponytailDefaultFlag, pstackDefaultFlag, rtkDefaultFlag, diagScheduleFlag, profileFlagsGiven, subagentMarkerFlags, subagentMarkersGiven, debug, execFileP, execP, writeIfChanged, normalizeExtensionsKey, writeConfigLines, ensureExtensionInConfig, removeExtensionFromConfig, readPonytailConfig, parseJsonObject, parsePonytailConfig, patchPonytailConfig, ensurePonytailConfigValue, readPluginsPackage, relTime, bundledPonytailSpec, ponytailOverride, };
@@ -457,8 +457,13 @@ function readBody(req) {
457
457
  });
458
458
  }
459
459
  function openBrowser(url) {
460
- const cmd = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'start' : 'xdg-open';
461
- spawn(cmd, [url], { detached: true, stdio: 'ignore' }).unref();
460
+ // `start` is a cmd builtin, not an executable: spawning it by name fails with ENOENT.
461
+ const [cmd, args] = process.platform === 'darwin'
462
+ ? ['open', [url]]
463
+ : process.platform === 'win32'
464
+ ? [process.env.ComSpec || 'cmd.exe', ['/d', '/s', '/c', 'start', '', url]]
465
+ : ['xdg-open', [url]];
466
+ spawn(cmd, args, { detached: true, stdio: 'ignore' }).unref();
462
467
  }
463
468
  // Replacers throughout: session data holds `$'` (shell quoting) and `</script>`, which String.replace would mangle.
464
469
  function escapeInline(json) {
@@ -2,7 +2,7 @@ import { existsSync, promises as fs } from 'node:fs';
2
2
  import { spawnSync } from 'node:child_process';
3
3
  import os from 'node:os';
4
4
  import path from 'node:path';
5
- import { BUN_BIN_DIR, HOME, OMP_AGENT_DIR, OMP_PLUGINS_DIR, PACKAGE_NAME, PACKAGE_VERSION, RTK_BINARY_NAME, removeExtensionFromConfig, dryRun, verbose, debug, bundledPonytailSpec, execP, ponytailOverride, readPluginsPackage, } from './common.js';
5
+ import { BUN_BIN_DIR, HOME, NPM_BIN, OMP_AGENT_DIR, OMP_PLUGINS_DIR, PACKAGE_NAME, PACKAGE_VERSION, RTK_BINARY_NAME, removeExtensionFromConfig, dryRun, verbose, debug, bundledPonytailSpec, execP, ponytailOverride, readPluginsPackage, shimCommand, } from './common.js';
6
6
  import { execNetwork, sayTagged } from './interactive.js';
7
7
  import { wireRtkOmp, wireRtkOpencode, wireRtkPi, ensureRtkInConfig } from './rtk-wiring.js';
8
8
  import { RTK_RELEASE_API, fetchJson, findFile, httpsGet, httpsDownload, parseChecksum, readTextIfExists, resolveHostBinary, rtkPlatformSpec, sha256File, } from '../extensions/shared/utils.js';
@@ -14,18 +14,20 @@ export function serviceOrphanPids() {
14
14
  const bin = resolveHostBinary('opencode', path.join(HOME, '.opencode', 'bin'));
15
15
  if (!bin)
16
16
  return [];
17
+ const shim = shimCommand(bin, ['service', 'status']);
17
18
  let status = '';
18
19
  try {
19
- status = spawnSync(bin, ['service', 'status'], { encoding: 'utf8' }).stdout.trim().toLowerCase();
20
+ status = spawnSync(shim.cmd, shim.args, { encoding: 'utf8' }).stdout.trim().toLowerCase();
20
21
  }
21
22
  catch {
22
23
  return [];
23
24
  }
24
25
  if (serviceRunning(status))
25
26
  return [];
27
+ const grepShim = shimCommand('pgrep', ['-f', 'opencode serve --service']);
26
28
  let out = '';
27
29
  try {
28
- out = spawnSync('pgrep', ['-f', 'opencode serve --service'], { encoding: 'utf8' }).stdout ?? '';
30
+ out = spawnSync(grepShim.cmd, grepShim.args, { encoding: 'utf8' }).stdout ?? '';
29
31
  }
30
32
  catch {
31
33
  return [];
@@ -38,9 +40,10 @@ export function serviceState() {
38
40
  const bin = resolveHostBinary('opencode', path.join(HOME, '.opencode', 'bin'));
39
41
  if (!bin)
40
42
  return null;
43
+ const shim = shimCommand(bin, ['service', 'status']);
41
44
  let status = '';
42
45
  try {
43
- status = spawnSync(bin, ['service', 'status'], { encoding: 'utf8' }).stdout.trim().toLowerCase();
46
+ status = spawnSync(shim.cmd, shim.args, { encoding: 'utf8' }).stdout.trim().toLowerCase();
44
47
  }
45
48
  catch {
46
49
  return null;
@@ -49,7 +52,8 @@ export function serviceState() {
49
52
  return null;
50
53
  let live = false;
51
54
  try {
52
- const out = spawnSync('pgrep', ['-f', 'opencode serve --service'], { encoding: 'utf8' }).stdout ?? '';
55
+ const grepShim = shimCommand('pgrep', ['-f', 'opencode serve --service']);
56
+ const out = spawnSync(grepShim.cmd, grepShim.args, { encoding: 'utf8' }).stdout ?? '';
53
57
  live = out.split(/[\s,]+/).some((line) => /^\d+$/.test(line.trim()));
54
58
  }
55
59
  catch {
@@ -365,7 +369,7 @@ async function fixPonytail(pluginsDir) {
365
369
  return;
366
370
  }
367
371
  await fs.writeFile(pkgPath, JSON.stringify(pkg, null, 2) + '\n', 'utf8');
368
- await execNetwork('Restoring bundled Ponytail', 'npm', ['install', '--no-audit', '--no-fund'], { cwd: pluginsDir, timeout: 180000 }).catch((e) => {
372
+ await execNetwork('Restoring bundled Ponytail', NPM_BIN, ['install', '--no-audit', '--no-fund'], { cwd: pluginsDir, timeout: 180000 }).catch((e) => {
369
373
  throw new Error(`bundled ponytail restore failed: ${e.message}`);
370
374
  });
371
375
  }
@@ -405,7 +409,8 @@ async function fixOpencodeService() {
405
409
  sayTagged(' [warn] opencode not found on PATH; start it with `opencode service start`');
406
410
  return;
407
411
  }
408
- await execNetwork('Starting the OpenCode service', bin, ['service', 'start'], { timeout: 60000 }).catch((e) => {
412
+ const shim = shimCommand(bin, ['service', 'start']);
413
+ await execNetwork('Starting the OpenCode service', shim.cmd, shim.args, { timeout: 60000 }).catch((e) => {
409
414
  throw new Error(`service start failed: ${shortError(e)}`);
410
415
  });
411
416
  }
@@ -2,15 +2,16 @@
2
2
  import { execFileSync } from 'node:child_process';
3
3
  import { existsSync, readdirSync, readFileSync, statSync, promises as fs } from 'node:fs';
4
4
  import path from 'node:path';
5
- import { HOME, OMP_AGENT_DIR, OMP_PLUGINS_DIR, PACKAGE_NAME, PACKAGE_VERSION, args, dryRun, fix, yes, BUN_BIN_DIR, OMP_BIN, execP, parseJsonObject, relTime, } from './common.js';
5
+ import { HOME, OMP_AGENT_DIR, OMP_PLUGINS_DIR, PACKAGE_NAME, PACKAGE_VERSION, args, dryRun, fix, yes, BUN_BIN_DIR, OMP_BIN, execP, parseJsonObject, relTime, shimCommand, } from './common.js';
6
6
  import { detectHosts, hostExtensionsDir, ompPackageDir, piPonytailPackageDir } from './hosts.js';
7
+ import { GENERATED_TREE_FILES } from './manifest.js';
7
8
  import { askInteractiveChoice, askInteractiveConfirm, runInteractivePhase } from './interactive.js';
8
9
  import { usageDbPath } from '../extensions/shared/usage-store.js';
9
10
  import { pricesCachePath } from '../extensions/shared/pricing.js';
10
- import { findHoistedPackage, fetchJson, piAgentDir, readTextIfExists, resolveHostBinary, resolveRtkBinary, ruleBodyProblem } from '../extensions/shared/utils.js';
11
+ import { findHoistedPackage, fetchJson, piAgentDir, readTextIfExists, resolveHostBinary, resolveRtkBinary, rtkPlatformSpec, ruleBodyProblem, tersioHome } from '../extensions/shared/utils.js';
11
12
  import { sqliteScript, statsDbPath } from '../extensions/shared/agent-stats.js';
12
13
  import { OMP_SUBAGENT_MARKER } from '../extensions/shared/session-state.js';
13
- import { PI_PONYTAIL_FILES, PONYTAIL_BUNDLE_FILES, TREE_FILES, isObsoleteTreeFile, treeFileText, treeFilesFor } from './manifest.js';
14
+ import { PI_PONYTAIL_FILES, PONYTAIL_BUNDLE_FILES, TREE_FILES, TREE_PSTACK_FILES, isObsoleteTreeFile, treeFileText, treeFilesFor } from './manifest.js';
14
15
  import { serviceRunning, serviceState } from './doctor-fix.js';
15
16
  /** Bytes at the scale a transcript gap reads in: `5.6M`, not `5571992`. */
16
17
  function shortBytes(bytes) {
@@ -42,8 +43,7 @@ async function runDoctor(recheck = false) {
42
43
  const updaterIndex = path.join(tersioPluginDir, 'extensions', 'updater', 'index.ts');
43
44
  // Probes start concurrently and report in fixed order as data settles; each resolves instead of rejecting.
44
45
  const hosts = detectHosts();
45
- // Every host keeps a copy, and the copy omp loads is the reference itself, so only the hosts that get
46
- // their own copy are compared below. The package root, not the hook's directory, is what the paths hang off.
46
+ // Only hosts with their own copy are compared: omp's copy is the reference. Paths hang off the package root.
47
47
  const ponytailLoads = {
48
48
  pi: { dir: piPonytailPackageDir(piAgentDir()), files: PI_PONYTAIL_FILES },
49
49
  opencode: { dir: path.join(hostExtensionsDir('opencode'), 'ponytail-bundle'), files: PONYTAIL_BUNDLE_FILES },
@@ -70,9 +70,7 @@ async function runDoctor(recheck = false) {
70
70
  const treeOrphan = [];
71
71
  // The tree a host loads: omp reads the plugin package, every other host its own directory.
72
72
  const treeBaseFor = (host) => host.id === 'omp' ? path.join(tersioPluginDir, 'extensions') : hostExtensionsDir(host.id);
73
- // A tree holding at least one file of this build is compared even when the host reads as not
74
- // installed. pi auto-discovers every index.ts it finds, so a half-written tree needs reporting,
75
- // and `detectHosts` reads one as absent: it wants every file or a declared package.
73
+ // A tree holding any file of this build is compared even when the host reads as not installed: pi auto-discovers every index.ts it finds, so a half-written tree needs reporting.
76
74
  const treeHoldsOwnFiles = (host) => treeFilesFor(host.id === 'omp' ? 'opencode' : host.id).some((rel) => existsSync(path.join(treeBaseFor(host), ...rel.split('/'))));
77
75
  for (const host of hosts.filter((h) => h.installed || treeHoldsOwnFiles(h))) {
78
76
  const treeId = host.id === 'omp' ? 'opencode' : host.id;
@@ -103,6 +101,22 @@ async function runDoctor(recheck = false) {
103
101
  }
104
102
  }
105
103
  }
104
+ // A host reading as installed whose tree is short of this build's declared files: the pair loop above cannot see a file that is absent on both sides.
105
+ const unwrittenTreeFiles = async () => {
106
+ const out = [];
107
+ for (const host of hosts.filter((h) => h.installed)) {
108
+ const treeId = host.id === 'omp' ? 'opencode' : host.id;
109
+ const base = treeBaseFor(host);
110
+ for (const rel of treeFilesFor(treeId)) {
111
+ if (existsSync(path.join(base, ...rel.split('/'))))
112
+ continue;
113
+ if (host.id === 'omp' && GENERATED_TREE_FILES[rel] !== undefined)
114
+ continue;
115
+ out.push(`${host.id}/${rel}`);
116
+ }
117
+ }
118
+ return out;
119
+ };
106
120
  const probes = {
107
121
  ompPkgText: readTextIfExists(path.join(ompPackageDir(), 'package.json')),
108
122
  configText: readTextIfExists(path.join(OMP_AGENT_DIR, 'config.yml')),
@@ -110,9 +124,13 @@ async function runDoctor(recheck = false) {
110
124
  rtkBinText: rtkBin ? readTextIfExists(rtkBin) : Promise.resolve(null),
111
125
  cavemanIndexText: readTextIfExists(cavemanIndex),
112
126
  cavemanRuleText: readTextIfExists(cavemanRule('rule.md')),
127
+ cavemanLiteText: readTextIfExists(cavemanRule('rule-lite.md')),
128
+ cavemanMegacaveLiteText: readTextIfExists(cavemanRule('rule-megacave-lite.md')),
129
+ cavemanMegacaveUltraText: readTextIfExists(cavemanRule('rule-megacave-ultra.md')),
113
130
  cavemanUltraText: readTextIfExists(cavemanRule('rule-ultra.md')),
114
131
  cavemanMegacaveText: readTextIfExists(cavemanRule('rule-megacave.md')),
115
132
  pstackRuleText: readTextIfExists(pstackRule),
133
+ pstackTreeFiles: Promise.all(TREE_PSTACK_FILES.map((rel) => readTextIfExists(path.join(tersioPluginDir, "extensions", ...rel.split("/"))))),
116
134
  rtkIndexText: readTextIfExists(rtkIndex),
117
135
  updaterIndexText: readTextIfExists(updaterIndex),
118
136
  rtkMtime: rtkBin ? fs.stat(rtkBin).catch(() => null) : Promise.resolve(null),
@@ -128,7 +146,7 @@ async function runDoctor(recheck = false) {
128
146
  const opencodeServiceProbe = opencodeBin
129
147
  ? Promise.resolve(serviceState())
130
148
  : Promise.resolve(null);
131
- const [[ompPkgText, configText], [cavemanIndexText, rtkIndexText, updaterIndexText, ponytailPkgText], [cavemanRuleText, cavemanUltraText, cavemanMegacaveText, pstackRuleText, ruleMtime, rtkBinText, rtkMtime, rtkVersion, ponytailMtime, pricesMtime], [opencodeService]] = await runInteractivePhase('Checking installation', () => Promise.all([
149
+ const [[ompPkgText, configText], [cavemanIndexText, rtkIndexText, updaterIndexText, ponytailPkgText], [cavemanRuleText, cavemanLiteText, cavemanMegacaveLiteText, cavemanMegacaveUltraText, cavemanUltraText, cavemanMegacaveText, pstackRuleText, pstackTreeFiles, ruleMtime, rtkBinText, rtkMtime, rtkVersion, ponytailMtime, pricesMtime], [opencodeService]] = await runInteractivePhase('Checking installation', () => Promise.all([
132
150
  Promise.all([
133
151
  probes.ompPkgText,
134
152
  probes.configText,
@@ -141,9 +159,13 @@ async function runDoctor(recheck = false) {
141
159
  ]),
142
160
  Promise.all([
143
161
  probes.cavemanRuleText,
162
+ probes.cavemanLiteText,
163
+ probes.cavemanMegacaveLiteText,
164
+ probes.cavemanMegacaveUltraText,
144
165
  probes.cavemanUltraText,
145
166
  probes.cavemanMegacaveText,
146
167
  probes.pstackRuleText,
168
+ probes.pstackTreeFiles,
147
169
  probes.ruleMtime,
148
170
  probes.rtkBinText,
149
171
  probes.rtkMtime,
@@ -179,6 +201,18 @@ async function runDoctor(recheck = false) {
179
201
  function unused(label, detail) {
180
202
  console.log(` — ${label}: not installed ${detail}`);
181
203
  }
204
+ // A Windows failure names its cause here: several rows below silently degrade when the
205
+ // platform has no rtk build, and a wrong home would move every one of them.
206
+ section('Platform');
207
+ const rtkSpec = rtkPlatformSpec();
208
+ check('Platform', rtkSpec !== null, `${process.platform}/${process.arch}${rtkSpec ? ` · node ${process.version} · rtk ${rtkSpec.triple}` : ''}`);
209
+ if (rtkSpec === null) {
210
+ // Windows on ARM today: rtk publishes no build, so the RTK rows read as not installed.
211
+ console.log(` rtk publishes no build for ${process.platform}/${process.arch}; every RTK row below reads as missing`);
212
+ }
213
+ // Windows resolves data through the profile, so name the resolved home and data dir.
214
+ check('Home', HOME !== '', HOME);
215
+ check('Data', true, path.join(tersioHome(), 'usage.db'));
182
216
  section('Hosts');
183
217
  for (const host of hosts) {
184
218
  // pi can hold a working tree while reading as not installed; use the tree as ground truth.
@@ -201,8 +235,7 @@ async function runDoctor(recheck = false) {
201
235
  const kind = host.via === 'tree' ? 'extensions' : 'package';
202
236
  check(host.label, true, `${kind} ${PACKAGE_NAME}${host.version ? ` ${host.version}` : ''}`);
203
237
  }
204
- // A healthy service adds no row: the host row already names the install with its version, and
205
- // this row earns its place only when the manager's report and the live process disagree.
238
+ // A healthy service adds no row: the host row already names the install, and this row only shows when the manager's report and the live process disagree.
206
239
  if (opencodeService) {
207
240
  const running = serviceRunning(opencodeService.status);
208
241
  if (!running && opencodeService.live) {
@@ -236,7 +269,7 @@ async function runDoctor(recheck = false) {
236
269
  warnLine('omp stats index', indexLag);
237
270
  section('Add-ons');
238
271
  const ruleAge = ruleMtime ? `(updated ${relTime(Date.now() - ruleMtime.mtimeMs)} · ${absDate(ruleMtime.mtimeMs)})` : '';
239
- const cavemanRules = [['rule.md', cavemanRuleText], ['rule-ultra.md', cavemanUltraText], ['rule-megacave.md', cavemanMegacaveText]];
272
+ const cavemanRules = [['rule.md', cavemanRuleText], ['rule-lite.md', cavemanLiteText], ['rule-megacave-lite.md', cavemanMegacaveLiteText], ['rule-megacave-ultra.md', cavemanMegacaveUltraText], ['rule-ultra.md', cavemanUltraText], ['rule-megacave.md', cavemanMegacaveText]];
240
273
  const missingRules = cavemanRules.filter(([, text]) => text === null).map(([name]) => name);
241
274
  check('Caveman rule', missingRules.length === 0, missingRules.length ? `missing: ${missingRules.join(', ')}` : ruleAge);
242
275
  for (const [name, text] of cavemanRules) {
@@ -253,6 +286,16 @@ async function runDoctor(recheck = false) {
253
286
  if (problem)
254
287
  warnLine('pstack rule.md', `unusable body (${problem}) — run: tersio install`);
255
288
  }
289
+ // The index routes to files, so a tree with the rule but no leaves serves a mode that cannot apply.
290
+ const pstackOnDisk = {};
291
+ TREE_PSTACK_FILES.forEach((rel, i) => {
292
+ if (pstackTreeFiles[i] !== null)
293
+ pstackOnDisk[rel] = true;
294
+ });
295
+ const pstackMissing = TREE_PSTACK_FILES.filter((rel) => !pstackOnDisk[rel]).map((rel) => rel.replace(/^pstack\//, ''));
296
+ if (pstackMissing.length > 0) {
297
+ warnLine('pstack tree', `missing ${pstackMissing.length} file${pstackMissing.length === 1 ? '' : 's'}: ${pstackMissing.slice(0, 6).join(', ')}${pstackMissing.length > 6 ? ` +${pstackMissing.length - 6} more` : ''} — run: tersio install`);
298
+ }
256
299
  // A verbatim sentence read out of omp's own prompt, so a re-worded release stops Caveman reaching subagents.
257
300
  const subagentGap = subagentMarkerStatus(ompBinary());
258
301
  if (subagentGap)
@@ -270,6 +313,11 @@ async function runDoctor(recheck = false) {
270
313
  warnLine('Per-host add-on files', `orphaned in ${treeOrphan.length} file${treeOrphan.length === 1 ? '' : 's'}: ${treeOrphan.join(', ')}` +
271
314
  ` — run: tersio doctor --fix extensions`);
272
315
  }
316
+ // A count of pairs compared cannot show a file that is absent from disk and absent from the declaration, so assert every declared file landed.
317
+ const unwritten = await unwrittenTreeFiles();
318
+ if (unwritten.length > 0) {
319
+ check('Add-on tree content', false, `${unwritten.length} declared file${unwritten.length === 1 ? '' : 's'} not written for a host reading as installed: ${unwritten.slice(0, 6).join(', ')}${unwritten.length > 6 ? ` +${unwritten.length - 6} more` : ''} — run: tersio doctor --fix extensions`);
320
+ }
273
321
  const rtkAge = rtkMtime ? `(updated ${relTime(Date.now() - rtkMtime.mtimeMs)} · ${absDate(rtkMtime.mtimeMs)})` : '';
274
322
  check('RTK binary', rtkBin !== null, rtkBinText === null ? 'not found in PATH' : [rtkVersion, rtkAge].filter(Boolean).join(' '));
275
323
  // rtk_run needs a host exec API, which doctor cannot assume.
@@ -336,8 +384,8 @@ function ompBinary() {
336
384
  const managed = path.join(BUN_BIN_DIR, OMP_BIN);
337
385
  return existsSync(managed) ? managed : null;
338
386
  }
339
- // The rule bodies a per-host tree carries. rule.md is the bundled fork; the others track upstream.
340
- const CAVEMAN_BODIES = ['rule.md', 'rule-ultra.md', 'rule-megacave.md'];
387
+ // The rule bodies a per-host tree carries. rule.md and rule-lite.md are bundled forks; the others track upstream.
388
+ const CAVEMAN_BODIES = ['rule.md', 'rule-lite.md', 'rule-megacave-lite.md', 'rule-megacave-ultra.md', 'rule-ultra.md', 'rule-megacave.md'];
341
389
  /** `"installed < latest"` when npm answers, else null: silence means "no row", not "stale". */
342
390
  async function ponytailLatestGap(installedPkgText) {
343
391
  const local = parseJsonObject(installedPkgText)?.version;
@@ -384,7 +432,8 @@ function subagentMarkerStatus(ompBin) {
384
432
  return null;
385
433
  let haystack;
386
434
  try {
387
- haystack = execFileSync(ompBin, ['--version'], { encoding: 'utf8', timeout: 5000, stdio: ['ignore', 'pipe', 'pipe'] })
435
+ const shim = shimCommand(ompBin, ['--version']);
436
+ haystack = execFileSync(shim.cmd, shim.args, { encoding: 'utf8', timeout: 5000, stdio: ['ignore', 'pipe', 'pipe'] })
388
437
  + readFileSync(ompBin, 'utf8');
389
438
  }
390
439
  catch {
package/dist/cli/hosts.js CHANGED
@@ -86,8 +86,7 @@ function detect(agentDir = piAgentDir()) {
86
86
  return HOSTS.map((host) => {
87
87
  const pkgDir = host.id === 'omp' ? ompPackageDir() : host.id === 'pi' ? piPackageDir(agentDir) : null;
88
88
  const tree = hostExtensionsDir(host.id, agentDir);
89
- // A tree wins: it is what the installer writes, and it carries no package.json, so its version is ours.
90
- // OpenCode keeps no package to fall back to, so a present tree is the install; its drifted files get the per-host warning instead.
89
+ // A tree wins: the installer writes it and it carries no package.json, so its version is ours. OpenCode keeps no package to fall back to, so a present tree is the install and drift gets the per-host warning.
91
90
  const via = missingTreeFiles(host.id, agentDir).length === 0 || (host.id === 'opencode' && existsSync(tree))
92
91
  ? 'tree'
93
92
  : pkgDir && existsSync(pkgDir) ? 'package' : null;
@@ -3,7 +3,7 @@ import { existsSync, promises as fs } from 'node:fs';
3
3
  import os from 'node:os';
4
4
  import path from 'node:path';
5
5
  import { fileURLToPath } from 'node:url';
6
- import { BUN_BIN_DIR, COMBO_PRESET_MODES, HOME, IS_WINDOWS, OMP_AGENT_DIR, OMP_PLUGINS_DIR, OMP_BIN, PACKAGE_NAME, PACKAGE_VERSION, RTK_BINARY_NAME, args, allowUnverified, applyUpdate, bundledPonytailSpec, cavemanDefaultFlag, comboDefaultFlag, command, dryRun, ponytailDefaultFlag, pstackDefaultFlag, profileFlagsGiven, rtkDefaultFlag, verbose, yes, dashboardExport, dashboardPort, currency, debug, ensurePonytailConfigValue, execP, parseJsonObject, readPluginsPackage, writeIfChanged, } from './common.js';
6
+ import { BUN_BIN_DIR, BUN_BIN, COMBO_PRESET_MODES, HOME, IS_WINDOWS, NPM_BIN, OMP_AGENT_DIR, OMP_PLUGINS_DIR, OMP_BIN, PACKAGE_NAME, PACKAGE_VERSION, RTK_BINARY_NAME, args, allowUnverified, applyUpdate, bundledPonytailSpec, cavemanDefaultFlag, comboDefaultFlag, command, dryRun, ponytailDefaultFlag, pstackDefaultFlag, profileFlagsGiven, rtkDefaultFlag, verbose, yes, dashboardExport, dashboardPort, currency, debug, ensurePonytailConfigValue, execP, parseJsonObject, readPluginsPackage, writeIfChanged, } from './common.js';
7
7
  import { askInteractiveChoice, askInteractiveConfirm, closeRL, execNetwork, tty, withInteractiveSpinner, withInteractiveTask, sayTagged } from './interactive.js';
8
8
  import { printWelcome } from './banner.js';
9
9
  import { checkForUpdate, runLatestUpdate } from './update.js';
@@ -80,11 +80,11 @@ async function stepPonytail(pluginsDir, options) {
80
80
  else {
81
81
  // Reinstall only when the self-plugin step did not materialize it.
82
82
  try {
83
- await execNetwork('Installing bundled Ponytail', 'npm', ['install', '--no-audit', '--no-fund'], { cwd: pluginsDir, timeout: 180000 });
83
+ await execNetwork('Installing bundled Ponytail', NPM_BIN, ['install', '--no-audit', '--no-fund'], { cwd: pluginsDir, timeout: 180000 });
84
84
  }
85
85
  catch {
86
86
  try {
87
- await execNetwork('Installing bundled Ponytail', 'bun', ['install'], { cwd: pluginsDir, timeout: 180000 });
87
+ await execNetwork('Installing bundled Ponytail', BUN_BIN, ['install'], { cwd: pluginsDir, timeout: 180000 });
88
88
  }
89
89
  catch (e) {
90
90
  sayTagged(` [fail] Could not install bundled ponytail: ${e.message}`);
@@ -136,11 +136,11 @@ async function stepSelfPlugin(pluginsDir, options) {
136
136
  await fs.mkdir(pluginsDir, { recursive: true });
137
137
  await fs.writeFile(pkgPath, JSON.stringify(pkg, null, 2) + '\n', 'utf8');
138
138
  try {
139
- await execNetwork('Installing Tersio plugin dependencies', 'npm', ['install', '--no-audit', '--no-fund'], { cwd: pluginsDir, timeout: 180000 });
139
+ await execNetwork('Installing Tersio plugin dependencies', NPM_BIN, ['install', '--no-audit', '--no-fund'], { cwd: pluginsDir, timeout: 180000 });
140
140
  }
141
141
  catch {
142
142
  try {
143
- await execNetwork('Installing Tersio plugin dependencies', 'bun', ['install'], { cwd: pluginsDir, timeout: 180000 });
143
+ await execNetwork('Installing Tersio plugin dependencies', BUN_BIN, ['install'], { cwd: pluginsDir, timeout: 180000 });
144
144
  }
145
145
  catch (e) {
146
146
  sayTagged(` [fail] Could not install ${PACKAGE_NAME} into plugins dir: ${e.message}`);
@@ -411,9 +411,12 @@ async function stepRtkFilter(extDir, options) {
411
411
  console.log(' RTK filter — install built-in tool output filtering');
412
412
  await copySources(extDir, filesUnder('rtk'), 'rtk/filter.ts', options);
413
413
  }
414
- // rule.md is a deliberate fork of skills/caveman/SKILL.md: a blind sync would drop the mode switch line.
414
+ // rule.md and rule-lite.md are our forks and always ship bundled; the other two track upstream and keep a usable body when the fetch fails.
415
415
  const CAVEMAN_RULES = [
416
416
  ['rule.md', null],
417
+ ['rule-lite.md', null],
418
+ ['rule-megacave-lite.md', null],
419
+ ['rule-megacave-ultra.md', null],
417
420
  ['rule-ultra.md', CAVEMAN_REMOTE_ULTRA],
418
421
  ['rule-megacave.md', CAVEMAN_REMOTE_MEGACAVE],
419
422
  ];
@@ -545,7 +548,8 @@ async function stepOpencode(options) {
545
548
  await fs.mkdir(path.dirname(configPath), { recursive: true });
546
549
  await fs.rm(path.join(HOME, '.config', 'opencode', 'plugins', 'rtk.ts'), { force: true }).catch(() => { });
547
550
  }
548
- await writeIfChanged(configPath, `${JSON.stringify(config, null, 2)}\n`, options);
551
+ // The user's own config, so keep the previous copy beside it.
552
+ await writeIfChanged(configPath, `${JSON.stringify(config, null, 2)}\n`, { ...options, backup: true });
549
553
  }
550
554
  async function resolveProfile(opts = {}) {
551
555
  // Seed from the stored defaults so flag-less runs keep them.
@@ -1007,6 +1011,7 @@ async function runInstall(forcedHost) {
1007
1011
  await capture('shared', () => stepSharedSessionState(extDir, installOptions));
1008
1012
  await capture('rtk session', () => stepRtkSession(extDir, installOptions));
1009
1013
  await capture('rtk filter', () => stepRtkFilter(extDir, installOptions));
1014
+ await capture('timing', () => stepTiming(extDir, installOptions));
1010
1015
  await capture('caveman', () => stepCaveman(extDir, installOptions));
1011
1016
  await capture('combo', () => stepCombo(extDir, installOptions));
1012
1017
  await capture('pstack', () => stepPstack(extDir, installOptions));
@@ -1,10 +1,28 @@
1
1
  // Single list of install writes, shared by installer, doctor, and tests.
2
2
  import path from 'node:path';
3
- import { readFileSync } from 'node:fs';
3
+ import { readdirSync, readFileSync } from 'node:fs';
4
4
  import { fileURLToPath } from 'node:url';
5
5
  import { findPackageRoot, readTextIfExists } from '../extensions/shared/utils.js';
6
6
  import { writeIfChanged } from './common.js';
7
7
  export const EXT_DIR = path.join(findPackageRoot(path.dirname(fileURLToPath(import.meta.url))), 'extensions');
8
+ /** Every pstack leaf, playbook, and skill the index routes to. */
9
+ function pstackTreeFiles() {
10
+ const root = path.join(EXT_DIR, "pstack");
11
+ const out = [];
12
+ const walk = (dir, rel) => {
13
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
14
+ const child = path.join(dir, entry.name);
15
+ const relChild = rel ? `${rel}/${entry.name}` : entry.name;
16
+ if (entry.isDirectory())
17
+ walk(child, relChild);
18
+ else if (entry.name.endsWith(".md"))
19
+ out.push(`pstack/${relChild}`);
20
+ }
21
+ };
22
+ walk(root, "");
23
+ return out.toSorted();
24
+ }
25
+ export const TREE_PSTACK_FILES = pstackTreeFiles();
8
26
  /** Every file in the tree, as paths relative to the extensions directory. */
9
27
  export const TREE_FILES = [
10
28
  'shared/host.ts',
@@ -17,12 +35,17 @@ export const TREE_FILES = [
17
35
  'shared/pricing.ts',
18
36
  'shared/carbon.ts',
19
37
  'shared/utils.ts',
38
+ 'shared/rtk-eligible.ts',
20
39
  'caveman/index.ts',
21
40
  'caveman/rule.md',
41
+ 'caveman/rule-lite.md',
42
+ 'caveman/rule-megacave-lite.md',
43
+ 'caveman/rule-megacave-ultra.md',
22
44
  'caveman/rule-ultra.md',
23
45
  'caveman/rule-megacave.md',
24
46
  'pstack/index.ts',
25
47
  'pstack/rule.md',
48
+ ...TREE_PSTACK_FILES,
26
49
  'rtk/session.ts',
27
50
  'rtk/filter.ts',
28
51
  'timing/index.ts',
@@ -108,10 +131,7 @@ export function shippedOmpManifest() {
108
131
  return null;
109
132
  }
110
133
  }
111
- /**
112
- * Write the shipped `omp` block over an installed plugin's manifest. The tree files come from this
113
- * build, so an entry added here reaches the host only when its manifest names it.
114
- */
134
+ // Write the shipped `omp` block over an installed plugin's manifest: an entry added here reaches the host only when its manifest names it.
115
135
  export async function syncPluginManifest(pluginDir, options = {}) {
116
136
  const omp = shippedOmpManifest();
117
137
  if (omp === null)
@@ -62,11 +62,12 @@ async function storedProfile() {
62
62
  return storedProfileSync();
63
63
  }
64
64
  function legacyOmpLockPath() {
65
- const configHome = process.env.XDG_CONFIG_HOME || path.join(os.homedir(), '.config');
65
+ const home = process.env.HOME || process.env.USERPROFILE || os.homedir();
66
+ const configHome = process.env.XDG_CONFIG_HOME || path.join(home, '.config');
66
67
  return [
67
- path.join(os.homedir(), '.omp', 'plugins', 'omp-plugins.lock.json'),
68
+ path.join(home, '.omp', 'plugins', 'omp-plugins.lock.json'),
68
69
  path.join(configHome, 'omp', 'plugins', 'omp-plugins.lock.json'),
69
- ].find((p) => existsSync(p)) ?? path.join(os.homedir(), '.omp', 'plugins', 'omp-plugins.lock.json');
70
+ ].find((p) => existsSync(p)) ?? path.join(home, '.omp', 'plugins', 'omp-plugins.lock.json');
70
71
  }
71
72
  function parseStored(raw) {
72
73
  if (!raw)
@@ -83,6 +84,29 @@ function parseStored(raw) {
83
84
  return null;
84
85
  }
85
86
  }
87
+ // Upstream Ponytail's pi extension reads its own config for the session default, so the saved default
88
+ // is mirrored there. Without it a headless pi session starts at upstream's default and never injects.
89
+ function ponytailConfigFile() {
90
+ const base = (process.env.XDG_CONFIG_HOME || '').trim() || path.join(os.homedir(), '.config');
91
+ return path.join(base, 'ponytail', 'config.json');
92
+ }
93
+ async function mirrorPonytailDefault(mode, options) {
94
+ const file = ponytailConfigFile();
95
+ if (options.dryRun) {
96
+ if (verbose && !options.quiet)
97
+ console.log(` [dry-run] would write the Ponytail default to ${file}`);
98
+ return;
99
+ }
100
+ try {
101
+ const existing = JSON.parse(readTextIfExistsSync(file) ?? '{}');
102
+ if (existing.defaultMode === mode)
103
+ return;
104
+ await fs.mkdir(path.dirname(file), { recursive: true });
105
+ await fs.writeFile(file, `${JSON.stringify({ ...existing, defaultMode: mode }, null, 2)}\n`, 'utf8');
106
+ console.log(` [write] Ponytail default in ${file}`);
107
+ }
108
+ catch { /* upstream's config is best-effort; its own default stands */ }
109
+ }
86
110
  // Persist profile where every host reads it. `omp plugin config get` no longer reports it; `tersio settings` does.
87
111
  async function writePluginSettings(profile, options) {
88
112
  const file = tersioSettingsFile();
@@ -106,6 +130,7 @@ async function writePluginSettings(profile, options) {
106
130
  await fs.mkdir(path.dirname(file), { recursive: true });
107
131
  await fs.writeFile(file, `${JSON.stringify(values, null, 2)}\n`, 'utf8');
108
132
  console.log(` [write] Session defaults in ${file}`);
133
+ await mirrorPonytailDefault(profile.ponytailDefault, options);
109
134
  }
110
135
  function formatProfile(profile) {
111
136
  return `combo=${profile.comboDefault} (caveman=${profile.cavemanDefault} · rtk=${profile.rtkDefault ? 'on' : 'off'} · ponytail=${profile.ponytailDefault} · pstack=${profile.pstackDefault}) · currency=${profile.currency}`;
@@ -3,6 +3,7 @@ import { existsSync, promises as fs } from 'node:fs';
3
3
  import os from 'node:os';
4
4
  import path from 'node:path';
5
5
  import { execFile } from 'node:child_process';
6
+ import { shimCommand } from '../extensions/shared/utils.js';
6
7
  function debugWire(options, msg) {
7
8
  if (!options.quiet)
8
9
  console.log(` ${msg}`);
@@ -14,17 +15,19 @@ function shortWiringError(e) {
14
15
  return why.slice(0, 200);
15
16
  }
16
17
  function execFileP(cmd, args, timeout) {
17
- return new Promise((resolve, reject) => {
18
- execFile(cmd, args, { timeout, encoding: 'utf8' }, (err, stdout, stderr) => {
19
- if (err) {
20
- err.stderr = String(stderr || '');
21
- reject(err);
22
- }
23
- else {
24
- resolve({ stdout: String(stdout), stderr: String(stderr) });
25
- }
26
- });
18
+ const { promise, resolve, reject } = Promise.withResolvers();
19
+ // A `.cmd` shim cannot be spawned directly on Windows, so it runs through the shell.
20
+ const shim = shimCommand(cmd, args);
21
+ execFile(shim.cmd, shim.args, { timeout, encoding: 'utf8' }, (err, stdout, stderr) => {
22
+ if (err) {
23
+ err.stderr = String(stderr || '');
24
+ reject(err);
25
+ }
26
+ else {
27
+ resolve({ stdout: String(stdout), stderr: String(stderr) });
28
+ }
27
29
  });
30
+ return promise;
28
31
  }
29
32
  async function runWire(rtkBin, spec, options) {
30
33
  if (!options.dryRun)
@@ -306,7 +306,7 @@ async function runUninstall(options = {}) {
306
306
  }
307
307
  // Cleared last: writeConfigLines above would just make a fresh one.
308
308
  const cavemanDir = path.join(pluginsDir, 'node_modules', PACKAGE_NAME, 'extensions', 'caveman-session');
309
- for (const backup of [`${configPath}.bak`, path.join(cavemanDir, 'rule.md.bak'), path.join(cavemanDir, 'rule-ultra.md.bak'), path.join(cavemanDir, 'rule-megacave.md.bak')]) {
309
+ for (const backup of [`${configPath}.bak`, path.join(cavemanDir, 'rule.md.bak'), path.join(cavemanDir, 'rule-lite.md.bak'), path.join(cavemanDir, 'rule-megacave-lite.md.bak'), path.join(cavemanDir, 'rule-megacave-ultra.md.bak'), path.join(cavemanDir, 'rule-ultra.md.bak'), path.join(cavemanDir, 'rule-megacave.md.bak')]) {
310
310
  await removeUninstallTarget(backup, shouldDryRun, false);
311
311
  }
312
312
  // Remove the bundled Ponytail copy; its config.yml entry is already filtered.
package/dist/cli/usage.js CHANGED
@@ -215,6 +215,11 @@ function printReport(report) {
215
215
  ['Tokens', `${fmt(t.input)} in · ${fmt(t.output)} out · ${fmt(t.cacheRead)} cache read · ${fmt(t.cacheWrite)} cache write`],
216
216
  ['Cost', `${formatCurrency(report.usd, report.currency)}${report.priced ? '' : ' (includes default pricing)'} · ~${formatCurrency(report.savedUsd, report.currency)} cache-saved (est.) · ~${report.co2g.toFixed(1)}g CO2 (est.)${costNote}`],
217
217
  ['RTK adoption', `${fmt(a.rtkCalls)}/${fmt(a.eligibleCalls)} eligible Bash calls use RTK (${a.adoptionPct.toFixed(1)}%) · ${fmt(a.missedCalls)} missed · recall ${report.rtkRecall.available ? `${report.rtkRecall.mode} (${report.rtkRecall.entries})` : 'unavailable'}`],
218
+ // A blended number hides a quiet host, so each host reporting bash calls gets its own row.
219
+ ...Object.entries(a.byHost)
220
+ .filter(([, c]) => c.bashCalls > 0)
221
+ .sort((x, y) => y[1].eligibleCalls - x[1].eligibleCalls)
222
+ .map(([name, c]) => [`RTK — ${name}`, `${fmt(c.rtkCalls)}/${fmt(c.eligibleCalls)} eligible calls (${c.adoptionPct.toFixed(1)}%) · ${fmt(c.missedCalls)} missed · ${fmt(c.bashCalls)} bash calls`]),
218
223
  ], [false, false], 118))
219
224
  console.log(l);
220
225
  if (measuredCount > 0) {
@@ -5,9 +5,10 @@ import { dirname, join } from 'node:path';
5
5
  import { injectPromptText, onHostEvent, setExtensionLabel } from '../shared/host.js';
6
6
  import { announceStatus, formatCavemanStatus } from '../shared/status.js';
7
7
  import { readCavemanDefault } from '../shared/plugin-settings.js';
8
- const CAVERN_DIR = dirname(fileURLToPath(import.meta.url));
9
- // Upstream ships one skill per register: default, ultracave, megacave.
10
- const RULE_FILES = { lite: 'rule.md', full: 'rule.md', ultra: 'rule-ultra.md', 'megacave-lite': 'rule-megacave.md', 'megacave-full': 'rule-megacave.md', 'megacave-ultra': 'rule-megacave.md' };
8
+ import { ruleDir } from '../shared/utils.js';
9
+ const CAVERN_DIR = ruleDir('caveman', dirname(fileURLToPath(import.meta.url)));
10
+ // `rule.md`, `rule-ultra.md`, and `rule-megacave.md` track upstream and are refetched; `lite`, `megacave-lite`, and `megacave-ultra` are our forks and ship bundled.
11
+ const RULE_FILES = { lite: 'rule-lite.md', full: 'rule.md', ultra: 'rule-ultra.md', 'megacave-lite': 'rule-megacave-lite.md', 'megacave-full': 'rule-megacave.md', 'megacave-ultra': 'rule-megacave-ultra.md' };
11
12
  const DEFAULT_MODE = 'off';
12
13
  // A rule file is immutable for the process, so read each level once; reinstalling needs a restart.
13
14
  const instructionCache = new Map();