@krtclcdy/tersio 2.25.5 → 2.25.7

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 (231) hide show
  1. package/README.md +42 -38
  2. package/dashboard/dist/index.html +15 -15
  3. package/dist/bench/code-out/pfull-0/debounce.js +13 -0
  4. package/dist/bench/code-out/pfull-0/deepClone.js +18 -0
  5. package/dist/bench/code-out/pfull-0/groupBy.js +15 -0
  6. package/dist/bench/code-out/pfull-1/debounce.js +13 -0
  7. package/dist/bench/code-out/pfull-1/deepClone.js +17 -0
  8. package/dist/bench/code-out/pfull-1/groupBy.js +14 -0
  9. package/dist/bench/code-out/pfull-2/debounce.js +13 -0
  10. package/dist/bench/code-out/pfull-2/deepClone.js +18 -0
  11. package/dist/bench/code-out/pfull-2/groupBy.js +15 -0
  12. package/dist/bench/code-out/pfull-3/debounce.js +12 -0
  13. package/dist/bench/code-out/pfull-3/deepClone.js +17 -0
  14. package/dist/bench/code-out/pfull-3/groupBy.js +7 -0
  15. package/dist/bench/code-out/pfull-4/debounce.js +10 -0
  16. package/dist/bench/code-out/pfull-4/deepClone.js +17 -0
  17. package/dist/bench/code-out/pfull-4/groupBy.js +14 -0
  18. package/dist/bench/code-out/plite-0/debounce.js +13 -0
  19. package/dist/bench/code-out/plite-0/deepClone.js +18 -0
  20. package/dist/bench/code-out/plite-0/groupBy.js +15 -0
  21. package/dist/bench/code-out/plite-1/debounce.js +13 -0
  22. package/dist/bench/code-out/plite-1/deepClone.js +17 -0
  23. package/dist/bench/code-out/plite-1/groupBy.js +11 -0
  24. package/dist/bench/code-out/plite-2/debounce.js +12 -0
  25. package/dist/bench/code-out/plite-2/deepClone.js +17 -0
  26. package/dist/bench/code-out/plite-2/groupBy.js +14 -0
  27. package/dist/bench/code-out/plite-3/debounce.js +12 -0
  28. package/dist/bench/code-out/plite-3/deepClone.js +17 -0
  29. package/dist/bench/code-out/plite-3/groupBy.js +14 -0
  30. package/dist/bench/code-out/plite-4/debounce.js +12 -0
  31. package/dist/bench/code-out/plite-4/deepClone.js +17 -0
  32. package/dist/bench/code-out/plite-4/groupBy.js +14 -0
  33. package/dist/bench/code-out/pultra-0/debounce.js +12 -0
  34. package/dist/bench/code-out/pultra-0/deepClone.js +20 -0
  35. package/dist/bench/code-out/pultra-0/groupBy.js +14 -0
  36. package/dist/bench/code-out/pultra-1/debounce.js +10 -0
  37. package/dist/bench/code-out/pultra-1/deepClone.js +17 -0
  38. package/dist/bench/code-out/pultra-1/groupBy.js +7 -0
  39. package/dist/bench/code-out/pultra-2/debounce.js +13 -0
  40. package/dist/bench/code-out/pultra-2/deepClone.js +18 -0
  41. package/dist/bench/code-out/pultra-2/groupBy.js +15 -0
  42. package/dist/bench/code-out/pultra-3/debounce.js +13 -0
  43. package/dist/bench/code-out/pultra-3/deepClone.js +18 -0
  44. package/dist/bench/code-out/pultra-3/groupBy.js +15 -0
  45. package/dist/bench/code-out/pultra-4/debounce.js +13 -0
  46. package/dist/bench/code-out/pultra-4/deepClone.js +18 -0
  47. package/dist/bench/code-out/pultra-4/groupBy.js +15 -0
  48. package/dist/bench/code-out/run-0/debounce.js +12 -0
  49. package/dist/bench/code-out/run-0/deepClone.js +17 -0
  50. package/dist/bench/code-out/run-0/groupBy.js +14 -0
  51. package/dist/bench/code-out/run-1/debounce.js +13 -0
  52. package/dist/bench/code-out/run-1/deepClone.js +15 -0
  53. package/dist/bench/code-out/run-1/groupBy.js +13 -0
  54. package/dist/bench/code-out/run-2/debounce.js +12 -0
  55. package/dist/bench/code-out/run-2/deepClone.js +18 -0
  56. package/dist/bench/code-out/run-2/groupBy.js +13 -0
  57. package/dist/bench/code-out/run-3/debounce.js +12 -0
  58. package/dist/bench/code-out/run-3/deepClone.js +18 -0
  59. package/dist/bench/code-out/run-3/groupBy.js +14 -0
  60. package/dist/bench/code-out/run-4/debounce.js +12 -0
  61. package/dist/bench/code-out/run-4/deepClone.js +16 -0
  62. package/dist/bench/code-out/run-4/groupBy.js +13 -0
  63. package/dist/cli/backups.js +7 -14
  64. package/dist/cli/banner.js +3 -3
  65. package/dist/cli/common.js +29 -13
  66. package/dist/cli/currency.js +1 -1
  67. package/dist/cli/dashboard.js +66 -35
  68. package/dist/cli/doctor-fix.js +204 -19
  69. package/dist/cli/doctor.js +263 -40
  70. package/dist/cli/hosts.js +25 -6
  71. package/dist/cli/install.js +150 -49
  72. package/dist/cli/interactive.js +3 -3
  73. package/dist/cli/manifest.js +109 -9
  74. package/dist/cli/profile.js +13 -8
  75. package/dist/cli/rtk-wiring.js +1 -1
  76. package/dist/cli/settings.js +20 -5
  77. package/dist/cli/uninstall.js +4 -4
  78. package/dist/cli/update.js +6 -7
  79. package/dist/cli/usage.js +6 -9
  80. package/dist/extensions/caveman/index.js +10 -4
  81. package/dist/extensions/caveman/rule-lite.md +77 -0
  82. package/dist/extensions/caveman/rule-megacave-lite.md +59 -0
  83. package/dist/extensions/caveman/rule-megacave-ultra.md +59 -0
  84. package/dist/extensions/caveman/rule-megacave.md +5 -3
  85. package/dist/extensions/caveman/rule-ultra.md +4 -4
  86. package/dist/extensions/caveman/rule.md +55 -45
  87. package/dist/extensions/combo/index.js +85 -25
  88. package/dist/extensions/commands/index.js +1 -1
  89. package/dist/extensions/pstack/index.js +121 -0
  90. package/dist/extensions/pstack/rule.md +141 -0
  91. package/dist/extensions/rtk/filter.js +0 -1
  92. package/dist/extensions/rtk/session.js +20 -12
  93. package/dist/extensions/server.js +90 -6
  94. package/dist/extensions/shared/agent-stats.js +214 -278
  95. package/dist/extensions/shared/brand-marks.js +33 -34
  96. package/dist/extensions/shared/carbon.js +1 -1
  97. package/dist/extensions/shared/plugin-settings.js +11 -6
  98. package/dist/extensions/shared/pricing.js +8 -16
  99. package/dist/extensions/shared/rtk-eligible.js +84 -0
  100. package/dist/extensions/shared/rtk-gain.js +5 -10
  101. package/dist/extensions/shared/session-state.js +15 -10
  102. package/dist/extensions/shared/sqlite.js +162 -0
  103. package/dist/extensions/shared/status.js +8 -9
  104. package/dist/extensions/shared/types.js +1 -1
  105. package/dist/extensions/shared/usage-ledger.js +155 -61
  106. package/dist/extensions/shared/usage-store.js +111 -52
  107. package/dist/extensions/shared/utils.js +5 -5
  108. package/dist/extensions/timing/index.js +31 -0
  109. package/dist/extensions/updater/index.js +3 -6
  110. package/dist/tersio.js +8 -3
  111. package/extensions/caveman/index.ts +10 -4
  112. package/extensions/caveman/rule-lite.md +77 -0
  113. package/extensions/caveman/rule-megacave-lite.md +59 -0
  114. package/extensions/caveman/rule-megacave-ultra.md +59 -0
  115. package/extensions/caveman/rule-megacave.md +5 -3
  116. package/extensions/caveman/rule-ultra.md +4 -4
  117. package/extensions/caveman/rule.md +55 -45
  118. package/extensions/combo/index.ts +86 -23
  119. package/extensions/commands/index.ts +1 -1
  120. package/extensions/pstack/index.ts +130 -0
  121. package/extensions/pstack/playbooks/authoring-a-skill.md +12 -0
  122. package/extensions/pstack/playbooks/autonomous-run.md +13 -0
  123. package/extensions/pstack/playbooks/autopilot-full.md +13 -0
  124. package/extensions/pstack/playbooks/autopilot-stack.md +16 -0
  125. package/extensions/pstack/playbooks/babysit.md +23 -0
  126. package/extensions/pstack/playbooks/bug-fix.md +15 -0
  127. package/extensions/pstack/playbooks/eval.md +25 -0
  128. package/extensions/pstack/playbooks/feature.md +21 -0
  129. package/extensions/pstack/playbooks/hillclimb.md +21 -0
  130. package/extensions/pstack/playbooks/investigation.md +14 -0
  131. package/extensions/pstack/playbooks/multi-phase-plan.md +155 -0
  132. package/extensions/pstack/playbooks/opening-a-pr.md +34 -0
  133. package/extensions/pstack/playbooks/orchestrate.md +111 -0
  134. package/extensions/pstack/playbooks/pause-safely.md +10 -0
  135. package/extensions/pstack/playbooks/perf-issue.md +25 -0
  136. package/extensions/pstack/playbooks/prototype.md +14 -0
  137. package/extensions/pstack/playbooks/refactoring.md +16 -0
  138. package/extensions/pstack/playbooks/runtime-forensics.md +11 -0
  139. package/extensions/pstack/playbooks/session-pickup.md +11 -0
  140. package/extensions/pstack/playbooks/shipping.md +17 -0
  141. package/extensions/pstack/playbooks/trace-forensics.md +14 -0
  142. package/extensions/pstack/playbooks/visual-parity.md +11 -0
  143. package/extensions/pstack/playbooks/worktree-cleanup.md +12 -0
  144. package/extensions/pstack/principles/attack-the-premise.md +22 -0
  145. package/extensions/pstack/principles/boundary-discipline.md +33 -0
  146. package/extensions/pstack/principles/build-the-lever.md +22 -0
  147. package/extensions/pstack/principles/encode-lessons-in-structure.md +10 -0
  148. package/extensions/pstack/principles/exhaust-the-design-space.md +8 -0
  149. package/extensions/pstack/principles/experience-first.md +18 -0
  150. package/extensions/pstack/principles/explain-the-number.md +16 -0
  151. package/extensions/pstack/principles/fix-root-causes.md +23 -0
  152. package/extensions/pstack/principles/foundational-thinking.md +20 -0
  153. package/extensions/pstack/principles/guard-the-context-window.md +11 -0
  154. package/extensions/pstack/principles/laziness-protocol.md +9 -0
  155. package/extensions/pstack/principles/make-operations-idempotent.md +23 -0
  156. package/extensions/pstack/principles/migrate-callers-then-delete-legacy-apis.md +21 -0
  157. package/extensions/pstack/principles/minimize-reader-load.md +17 -0
  158. package/extensions/pstack/principles/model-the-domain.md +23 -0
  159. package/extensions/pstack/principles/never-block-on-the-human.md +14 -0
  160. package/extensions/pstack/principles/outcome-oriented-execution.md +20 -0
  161. package/extensions/pstack/principles/prove-it-works.md +21 -0
  162. package/extensions/pstack/principles/redesign-from-first-principles.md +15 -0
  163. package/extensions/pstack/principles/separate-before-serializing-shared-state.md +15 -0
  164. package/extensions/pstack/principles/sequence-verifiable-units.md +16 -0
  165. package/extensions/pstack/principles/subtract-before-you-add.md +17 -0
  166. package/extensions/pstack/principles/test-behavior-not-implementation.md +20 -0
  167. package/extensions/pstack/principles/type-system-discipline.md +17 -0
  168. package/extensions/pstack/references/bugbot-triage.md +142 -0
  169. package/extensions/pstack/rule.md +141 -0
  170. package/extensions/pstack/skills/architect/SKILL.md +78 -0
  171. package/extensions/pstack/skills/architect/references/design-red-flags.md +57 -0
  172. package/extensions/pstack/skills/architect/references/rationale-template.md +35 -0
  173. package/extensions/pstack/skills/architect/references/runner-prompt.md +20 -0
  174. package/extensions/pstack/skills/arena/SKILL.md +47 -0
  175. package/extensions/pstack/skills/benchmark-checklist/SKILL.md +38 -0
  176. package/extensions/pstack/skills/blast-radius/SKILL.md +49 -0
  177. package/extensions/pstack/skills/bro/SKILL.md +6 -0
  178. package/extensions/pstack/skills/correct/SKILL.md +31 -0
  179. package/extensions/pstack/skills/figure-it-out/SKILL.md +51 -0
  180. package/extensions/pstack/skills/how/SKILL.md +35 -0
  181. package/extensions/pstack/skills/how/references/explainer-prompt.md +55 -0
  182. package/extensions/pstack/skills/how/references/explorer-prompt.md +35 -0
  183. package/extensions/pstack/skills/interrogate/SKILL.md +73 -0
  184. package/extensions/pstack/skills/interrogate/references/code-quality-review.md +47 -0
  185. package/extensions/pstack/skills/interrogate/references/lead-judgment.md +58 -0
  186. package/extensions/pstack/skills/interrogate/references/reviewer-prompt.md +62 -0
  187. package/extensions/pstack/skills/interrogate/references/rubric.md +77 -0
  188. package/extensions/pstack/skills/no-comments/SKILL.md +23 -0
  189. package/extensions/pstack/skills/reflect/SKILL.md +66 -0
  190. package/extensions/pstack/skills/reflect/references/divergent-reviewer.md +38 -0
  191. package/extensions/pstack/skills/reflect/references/judgment-reviewer.md +37 -0
  192. package/extensions/pstack/skills/reflect/references/synthesizer.md +56 -0
  193. package/extensions/pstack/skills/reflect/references/tooling-reviewer.md +48 -0
  194. package/extensions/pstack/skills/show-me-your-work/SKILL.md +81 -0
  195. package/extensions/pstack/skills/swarm/SKILL.md +45 -0
  196. package/extensions/pstack/skills/tdd/SKILL.md +41 -0
  197. package/extensions/pstack/skills/technical-writing/SKILL.md +105 -0
  198. package/extensions/pstack/skills/unslop/SKILL.md +61 -0
  199. package/extensions/pstack/skills/why/SKILL.md +97 -0
  200. package/extensions/pstack/skills/why/references/epistemics.md +71 -0
  201. package/extensions/pstack/skills/why/references/investigator-prompt.md +84 -0
  202. package/extensions/pstack/skills/why/references/source-playbook.md +17 -0
  203. package/extensions/pstack/skills/why/references/sources/code-archaeology.md +51 -0
  204. package/extensions/pstack/skills/why/references/sources/databricks.md +70 -0
  205. package/extensions/pstack/skills/why/references/sources/datadog.md +97 -0
  206. package/extensions/pstack/skills/why/references/sources/incident-postmortem.md +5 -0
  207. package/extensions/pstack/skills/why/references/sources/linear.md +48 -0
  208. package/extensions/pstack/skills/why/references/sources/notion.md +55 -0
  209. package/extensions/pstack/skills/why/references/sources/sentry.md +66 -0
  210. package/extensions/pstack/skills/why/references/sources/slack.md +17 -0
  211. package/extensions/pstack/skills/why/references/synthesizer-prompt.md +129 -0
  212. package/extensions/rtk/filter.ts +0 -1
  213. package/extensions/rtk/session.ts +19 -11
  214. package/extensions/server.ts +72 -7
  215. package/extensions/shared/agent-stats.ts +226 -278
  216. package/extensions/shared/brand-marks.ts +34 -34
  217. package/extensions/shared/carbon.ts +1 -1
  218. package/extensions/shared/plugin-settings.ts +12 -6
  219. package/extensions/shared/pricing.ts +10 -26
  220. package/extensions/shared/rtk-eligible.ts +83 -0
  221. package/extensions/shared/rtk-gain.ts +5 -10
  222. package/extensions/shared/session-state.ts +18 -13
  223. package/extensions/shared/sqlite.ts +162 -0
  224. package/extensions/shared/status.ts +9 -9
  225. package/extensions/shared/types.ts +2 -1
  226. package/extensions/shared/usage-ledger.ts +157 -66
  227. package/extensions/shared/usage-store.ts +120 -52
  228. package/extensions/shared/utils.ts +5 -5
  229. package/extensions/timing/index.ts +44 -0
  230. package/extensions/updater/index.ts +3 -8
  231. package/package.json +21 -9
@@ -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, cavemanDefaultFlag, comboDefaultFlag, command, dryRun, ponytailDefaultFlag, profileFlagsGiven, rtkDefaultFlag, verbose, yes, dashboardExport, dashboardPort, currency, debug, ensurePonytailConfigValue, execP, parseJsonObject, readPluginsPackage, writeIfChanged, } from './common.js';
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';
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';
@@ -17,7 +17,7 @@ import { CAVEMAN_REMOTE_ULTRA, CAVEMAN_REMOTE_MEGACAVE, RTK_RELEASE_API, fetchJs
17
17
  import { formatCliStatus, storedProfile, storedProfileSync, writePluginSettings } from './profile.js';
18
18
  import { runSettings } from './settings.js';
19
19
  import { tersioSettingsFile } from '../extensions/shared/plugin-settings.js';
20
- import { filesUnder, rootFiles, sourcePath } from './manifest.js';
20
+ import { PI_RTK_MANIFEST, PI_PONYTAIL_FILES, PONYTAIL_BUNDLE_FILES, filesUnder, rootFiles, sourcePath, syncPluginManifest, syncTreeFiles } from './manifest.js';
21
21
  import { detectHosts, hostHint, hostLabel, parseHostArg, piTersioSource } from './hosts.js';
22
22
  async function stepPonytail(pluginsDir, options) {
23
23
  if (!options.quiet)
@@ -25,22 +25,44 @@ async function stepPonytail(pluginsDir, options) {
25
25
  await fs.mkdir(pluginsDir, { recursive: true });
26
26
  const pkgPath = path.join(pluginsDir, 'package.json');
27
27
  const pkg = await readPluginsPackage(pkgPath);
28
- // Drop the legacy Ponytail plugin row; it is a tersio dependency now.
28
+ // npm resolves the *published* tersio, so a plain dependency entry lets npm nest the range's version.
29
29
  let migrated = false;
30
- if ('@dietrichgebert/ponytail' in pkg.dependencies) {
30
+ const localSpec = await bundledPonytailSpec();
31
+ const declared = pkg.dependencies['@dietrichgebert/ponytail'];
32
+ // Node resolves the nested copy first, so a dependency entry alone cannot hold the local version.
33
+ const rawOverrides = typeof pkg.overrides === 'object' && pkg.overrides !== null ? { ...pkg.overrides } : {};
34
+ const overrideNeeded = localSpec !== null && rawOverrides['@dietrichgebert/ponytail'] !== localSpec;
35
+ if (localSpec !== null && declared !== localSpec) {
36
+ pkg.dependencies['@dietrichgebert/ponytail'] = localSpec;
37
+ migrated = true;
38
+ }
39
+ else if (localSpec === null && declared) {
31
40
  delete pkg.dependencies['@dietrichgebert/ponytail'];
32
41
  migrated = true;
33
42
  }
43
+ if (localSpec === null) {
44
+ // No bundled spec means no tersio-owned row, so an override would resurrect the dependency.
45
+ delete rawOverrides['@dietrichgebert/ponytail'];
46
+ }
47
+ else if (overrideNeeded) {
48
+ rawOverrides['@dietrichgebert/ponytail'] = localSpec;
49
+ }
50
+ if (Object.keys(rawOverrides).length === 0)
51
+ delete pkg.overrides;
52
+ else if (JSON.stringify(rawOverrides) !== JSON.stringify(typeof pkg.overrides === 'object' && pkg.overrides !== null ? pkg.overrides : {})) {
53
+ pkg.overrides = rawOverrides;
54
+ migrated = true;
55
+ }
34
56
  if (options.dryRun) {
35
57
  if (verbose && !options.quiet)
36
58
  sayTagged(` [dry-run] would write ${pkgPath}`);
37
59
  if (migrated && verbose && !options.quiet)
38
- sayTagged(` [dry-run] would drop the separate @dietrichgebert/ponytail dependency (bundled with tersio)`);
60
+ sayTagged(` [dry-run] would set @dietrichgebert/ponytail → ${localSpec ?? 'this tree\x27s range'} (bundled with tersio)`);
39
61
  }
40
62
  else if (migrated) {
41
63
  await fs.writeFile(pkgPath, JSON.stringify(pkg, null, 2) + '\n');
42
64
  if (!options.quiet)
43
- console.log(' [migrate] dropped separate @dietrichgebert/ponytail dependency (now bundled with tersio)');
65
+ console.log(` [migrate] set @dietrichgebert/ponytail → ${pkg.dependencies['@dietrichgebert/ponytail'] ?? 'this tree\x27s range'} (bundled with tersio)`);
44
66
  }
45
67
  const ponytailExtPath = path.join(pluginsDir, 'node_modules', '@dietrichgebert', 'ponytail', 'pi-extension', 'index.js');
46
68
  const probeExt = async () => (await readTextIfExists(ponytailExtPath)) !== null;
@@ -50,7 +72,7 @@ async function stepPonytail(pluginsDir, options) {
50
72
  debug('Bundled Ponytail pi-extension already installed; skipping network refresh');
51
73
  }
52
74
  else if (options.dryRun) {
53
- // Dry runs preview the wiring below without touching the network.
75
+ // Pretend the extension is present so the wiring below still runs, without touching the network.
54
76
  if (verbose && !options.quiet)
55
77
  console.log(' [dry-run] would run: npm install --no-audit --no-fund (in plugins dir)');
56
78
  ponytailExtExists = true;
@@ -163,7 +185,7 @@ async function downloadRtkChecksums(release) {
163
185
  return null;
164
186
  }
165
187
  }
166
- // Fails closed: a missing checksum is worse than a failed install, since an unverifiable RTK binary runs with the user's privileges. `--allow-unverified` opts out.
188
+ // Fails closed: an unverifiable RTK binary runs with the user's privileges. `--allow-unverified` opts out.
167
189
  async function verifyRtkArchive(archivePath, assetName, checksumsText) {
168
190
  const unverified = (reason) => {
169
191
  if (allowUnverified) {
@@ -229,11 +251,7 @@ async function extractRtkArchive(archivePath, extractDir) {
229
251
  sayTagged(` [fail] Unknown archive format: ${archivePath}`);
230
252
  return false;
231
253
  }
232
- // The agent tree keeps only rtk's own wiring file. Every Tersio extension loads from the
233
- // OMP plugin manifest, which omp merges with the extension dir it discovers by default, so a
234
- // copy left in the agent tree loads a second time. `caveman-session` was never written here
235
- // after the plugin registration shipped, so an old install holds a set that cannot resolve
236
- // its own rule bodies.
254
+ // A rule body found in a retired copy still resolves and loads twice, so drop the agent tree's extras.
237
255
  const RETIRED_OMP_AGENT_DIRS = [
238
256
  'caveman-session',
239
257
  'rtk-session',
@@ -289,7 +307,7 @@ async function stepRtk(binDir, options, target = 'omp') {
289
307
  }
290
308
  if (!options.quiet)
291
309
  console.log(` RTK — download binary and wire into ${hostName}`);
292
- // A failed download must not skip wiring: a pre-existing binary serves the host hook just as well. Dry runs stay offline.
310
+ // A failed download must not skip wiring: a pre-existing binary serves the host hook. Dry runs stay offline.
293
311
  if (options.dryRun) {
294
312
  if (verbose && !options.quiet)
295
313
  sayTagged(` [dry-run] would download rtk binary and install to ${binDest}`);
@@ -300,7 +318,7 @@ async function stepRtk(binDir, options, target = 'omp') {
300
318
  const triple = resolveRtkTriple();
301
319
  if (!triple)
302
320
  return;
303
- // One task with streamed sub-steps: release lookup, download, checksum, extract, install. Non-TTY keeps the plain lines each step already prints.
321
+ // One task with streamed sub-steps; non-TTY keeps the plain lines each step already prints.
304
322
  await withInteractiveTask('Installing RTK', async (update) => {
305
323
  const release = await fetchJson(RTK_RELEASE_API);
306
324
  const asset = findRtkAsset(release, triple);
@@ -359,7 +377,7 @@ async function stepRtk(binDir, options, target = 'omp') {
359
377
  // Wire rtk's tool_call extension: the binary alone never meters a session.
360
378
  await bind(binDest);
361
379
  }
362
- // Copy sources into the target extension dir. First entry is required; the rest are optional companions.
380
+ // First entry is required; the rest are optional companions.
363
381
  async function copySources(extDir, files, skipLabel, options) {
364
382
  const src = await readTextIfExists(files[0][0]);
365
383
  if (!src) {
@@ -393,10 +411,12 @@ async function stepRtkFilter(extDir, options) {
393
411
  console.log(' RTK filter — install built-in tool output filtering');
394
412
  await copySources(extDir, filesUnder('rtk'), 'rtk/filter.ts', options);
395
413
  }
396
- // rule.md ships with the package; the other bodies track upstream skills.
397
- // rule.md is a deliberate fork of upstream skills/caveman/SKILL.md; a blind sync would drop the mode switch line and the STE100 rules.
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.
398
415
  const CAVEMAN_RULES = [
399
416
  ['rule.md', null],
417
+ ['rule-lite.md', null],
418
+ ['rule-megacave-lite.md', null],
419
+ ['rule-megacave-ultra.md', null],
400
420
  ['rule-ultra.md', CAVEMAN_REMOTE_ULTRA],
401
421
  ['rule-megacave.md', CAVEMAN_REMOTE_MEGACAVE],
402
422
  ];
@@ -449,17 +469,26 @@ async function stepTersioCommands(extDir, options) {
449
469
  async function stepUpdater(extDir, options) {
450
470
  await copySources(extDir, filesUnder('updater'), 'updater/index.ts', options);
451
471
  }
472
+ // omp loads no OpenCode entry, but its package ships one: the repair writes it, so the install does too.
473
+ async function stepPluginEntry(extDir, options) {
474
+ await copySources(extDir, rootFiles(), 'server.ts', options);
475
+ }
476
+ // pi records no first-token latency, so this stamps one into the row the dashboard reads.
477
+ async function stepTiming(extDir, options) {
478
+ await copySources(extDir, filesUnder('timing'), 'timing/index.ts', options);
479
+ }
452
480
  async function stepCombo(extDir, options) {
453
481
  if (!options.quiet)
454
482
  console.log(' Combo — install preset switch');
455
483
  await copySources(extDir, filesUnder('combo'), 'combo/index.ts', options);
456
484
  }
457
- // The tree ships no node_modules, so the full ruleset rides along as files.
458
- const PONYTAIL_BUNDLE_FILES = [
459
- 'hooks/ponytail-instructions.js',
460
- 'hooks/ponytail-config.js',
461
- 'skills/ponytail/SKILL.md',
462
- ];
485
+ // The body is a Tersio fork of an upstream skill, so the install copies it and never fetches it.
486
+ async function stepPstack(extDir, options) {
487
+ if (!options.quiet)
488
+ console.log(' pstack — install the rigor mode');
489
+ await copySources(extDir, filesUnder('pstack'), 'pstack/index.ts', options);
490
+ }
491
+ // The tree ships no node_modules, so the ruleset rides along as files. Both hosts need the same six.
463
492
  async function stepPonytailBundle(treeDir, options) {
464
493
  const root = findHoistedPackage('@dietrichgebert/ponytail', findPackageRoot(path.dirname(fileURLToPath(import.meta.url))));
465
494
  if (!root) {
@@ -487,11 +516,15 @@ async function stepOpencode(options) {
487
516
  const dir = path.join(HOME, '.config', 'opencode', 'plugins', 'tersio');
488
517
  if (!options.quiet)
489
518
  console.log(` OpenCode — write the plugin tree (${dir})`);
519
+ // Retire the `-session`/`-toggle` leftovers first: a rule body found in one still resolves.
520
+ await stepRetireAgentCopies(dir, options);
490
521
  await stepSharedSessionState(dir, options);
491
522
  await copySources(dir, filesUnder('rtk'), 'rtk/session.ts', options);
492
523
  await stepRtkFilter(dir, options);
524
+ await stepTiming(dir, options);
493
525
  await copySources(dir, filesUnder('caveman'), 'caveman/index.ts', options);
494
526
  await stepCombo(dir, options);
527
+ await stepPstack(dir, options);
495
528
  await copySources(dir, filesUnder('commands'), 'commands/index.ts', options);
496
529
  await stepUpdater(dir, options);
497
530
  await stepPonytailBundle(dir, options);
@@ -515,7 +548,8 @@ async function stepOpencode(options) {
515
548
  await fs.mkdir(path.dirname(configPath), { recursive: true });
516
549
  await fs.rm(path.join(HOME, '.config', 'opencode', 'plugins', 'rtk.ts'), { force: true }).catch(() => { });
517
550
  }
518
- 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 });
519
553
  }
520
554
  async function resolveProfile(opts = {}) {
521
555
  // Seed from the stored defaults so flag-less runs keep them.
@@ -547,6 +581,7 @@ async function resolveProfile(opts = {}) {
547
581
  profile.cavemanDefault = preset.caveman;
548
582
  profile.rtkDefault = preset.rtk;
549
583
  profile.ponytailDefault = preset.ponytail;
584
+ profile.pstackDefault = preset.pstack;
550
585
  }
551
586
  if (cavemanDefaultFlag !== undefined)
552
587
  profile.cavemanDefault = cavemanDefaultFlag;
@@ -554,8 +589,10 @@ async function resolveProfile(opts = {}) {
554
589
  profile.rtkDefault = rtkDefaultFlag === 'on';
555
590
  if (ponytailDefaultFlag !== undefined)
556
591
  profile.ponytailDefault = ponytailDefaultFlag;
592
+ if (pstackDefaultFlag !== undefined)
593
+ profile.pstackDefault = pstackDefaultFlag;
557
594
  if ((!opts.quiet && verbose) || dryRun)
558
- console.log(` Defaults: combo=${profile.comboDefault} (caveman=${profile.cavemanDefault} · rtk=${profile.rtkDefault ? 'on' : 'off'} · ponytail=${profile.ponytailDefault})`);
595
+ console.log(` Defaults: combo=${profile.comboDefault} (caveman=${profile.cavemanDefault} · rtk=${profile.rtkDefault ? 'on' : 'off'} · ponytail=${profile.ponytailDefault} · pstack=${profile.pstackDefault})`);
559
596
  return profile;
560
597
  }
561
598
  let updatePromptDone = false;
@@ -580,6 +617,7 @@ async function runCommandMenu() {
580
617
  updatePromptDone = true;
581
618
  const choice = await askInteractiveChoice(`Tersio — what next?\n${formatCliStatus(storedProfileSync())}`, [
582
619
  { value: 'install', label: 'Install add-ons', hint: 'user scope + combo defaults' },
620
+ { value: 'reinstall', label: 'Reinstall', hint: 're-install Tersio for the ai agents, then verify' },
583
621
  { value: 'update', label: 'Update', hint: 'CLI version check, then refresh add-ons (RTK, Caveman rule, Ponytail)' },
584
622
  { value: 'doctor', label: 'Doctor', hint: 'verify the installation' },
585
623
  { value: 'settings', label: 'Settings', hint: 'default combo mode, currency, diagnosis schedule' },
@@ -596,8 +634,11 @@ async function runCommandMenu() {
596
634
  case 'install':
597
635
  await runInstall();
598
636
  break;
637
+ case 'reinstall':
638
+ await runReinstall();
639
+ break;
599
640
  case 'update': {
600
- // One bound flow: the version check already ran above, so report it and offer the refresh in the same breath — no second "update" quiz.
641
+ // Version check already ran above, so report it and offer the refresh here — no second "update" quiz.
601
642
  if (typeof newer === 'string')
602
643
  console.log(` tersio ${newer} available (installed ${PACKAGE_VERSION})`);
603
644
  else
@@ -657,9 +698,57 @@ async function runCommandMenu() {
657
698
  break;
658
699
  }
659
700
  }
660
- // Which host this run targets. --host pins it; a TTY user picks; scripts, pipes, --yes, and --dry-run keep the pre-multi-host default of Oh My Pi.
701
+ // Which host this run targets. --host pins it; scripts and --yes keep the default of Oh My Pi.
661
702
  let targetHost = 'omp';
662
- // Hosts the menu advertises but cannot install yet. Listed disabled so the roadmap is visible without offering a target that does nothing.
703
+ // A reinstall re-runs the npm fast path and the Ponytail probe instead of reusing what is on disk.
704
+ let forceReinstall = false;
705
+ /** Reinstall one agent: pick it from what is installed now, then run the full install with a refresh. */
706
+ async function runReinstall() {
707
+ let pinned;
708
+ try {
709
+ pinned = parseHostArg(args);
710
+ }
711
+ catch (e) {
712
+ console.error(`[fail] ${e.message}`);
713
+ process.exit(1);
714
+ }
715
+ const installed = detectHosts().filter((host) => host.installed);
716
+ let host;
717
+ if (pinned)
718
+ host = pinned;
719
+ else if (installed.length === 0) {
720
+ console.log(' Nothing to reinstall — no agent has Tersio installed.\n Run `tersio install`, or pick it from the menu.');
721
+ closeRL();
722
+ return;
723
+ }
724
+ else if (installed.length === 1 || !tty() || yes || dryRun) {
725
+ host = installed[0].id;
726
+ }
727
+ else {
728
+ const options = installed.map((entry) => ({
729
+ value: entry.id,
730
+ label: hostLabel(entry),
731
+ hint: hostHint(entry),
732
+ }));
733
+ const picked = await askInteractiveChoice('Reinstall Tersio into which agent?', options, installed[0].id);
734
+ if (picked.status !== 'selected') {
735
+ closeRL();
736
+ process.exit(130);
737
+ }
738
+ host = picked.value;
739
+ }
740
+ forceReinstall = true;
741
+ printWelcome();
742
+ console.log(`Reinstall — Tersio v${PACKAGE_VERSION} into ${hostLabel(hostEntry(host))}`);
743
+ console.log(` ${resolvedHostPath(host)}`);
744
+ await runInstall(host);
745
+ if (!dryRun) {
746
+ const summary = await runDoctor();
747
+ if (summary.missing + summary.warn === 0)
748
+ console.log(`\n Reinstall verified — ${hostLabel(hostEntry(host))} is healthy.`);
749
+ }
750
+ }
751
+ // Hosts the menu advertises but cannot install yet; listed disabled so the roadmap stays visible.
663
752
  const SOON_HOSTS = [
664
753
  { value: 'claude', label: 'Claude Code', hint: 'Coming soon' },
665
754
  { value: 'codex', label: 'Codex', hint: 'Coming soon' },
@@ -684,7 +773,19 @@ async function chooseHost(title, only) {
684
773
  function hostEntry(id) {
685
774
  return detectHosts().find((host) => host.id === id);
686
775
  }
687
- // pi auto-discovers `<agent-dir>/extensions/**/index.ts`, so this route writes the same tree as OMP's and registers nothing.
776
+ /** pi discovers a subdirectory only through an index file or a package.json with `pi.extensions`, so rtk/ needs one or both files stay dead. */
777
+ async function stepPiRtkManifest(extDir, options) {
778
+ await syncTreeFiles(extDir, [PI_RTK_MANIFEST], options);
779
+ }
780
+ /** The directory a host's install writes, for the plan line a reinstall prints. */
781
+ function resolvedHostPath(host) {
782
+ if (host === 'pi')
783
+ return path.join(piAgentDir(), 'extensions');
784
+ if (host === 'opencode')
785
+ return path.join(HOME, '.config', 'opencode', 'plugins', 'tersio');
786
+ return path.join(OMP_PLUGINS_DIR, 'node_modules', ...PACKAGE_NAME.split('/'), 'extensions');
787
+ }
788
+ // pi auto-discovers `<agent-dir>/extensions/**/index.ts`, so this route writes the same tree and registers nothing.
688
789
  async function stepPiLayer(options) {
689
790
  const agentDir = piAgentDir();
690
791
  const extDir = path.join(agentDir, 'extensions');
@@ -697,20 +798,16 @@ async function stepPiLayer(options) {
697
798
  // OMP loads rtk/session.ts from its plugin manifest, so only the pi tree copies it.
698
799
  await copySources(extDir, filesUnder('rtk'), 'rtk/session.ts', options);
699
800
  await stepRtkFilter(extDir, options);
801
+ await stepPiRtkManifest(extDir, options);
802
+ await stepTiming(extDir, options);
700
803
  await stepCaveman(extDir, options);
701
804
  await stepCombo(extDir, options);
805
+ await stepPstack(extDir, options);
702
806
  await stepTersioCommands(extDir, options);
703
807
  await stepUpdater(extDir, options);
704
808
  await stepPonytailForPi(agentDir, options);
705
809
  }
706
810
  // A local dir, not an npm spec: pi only version-checks npm entries, so this avoids the update nag.
707
- const PI_PONYTAIL_FILES = [
708
- 'pi-extension/index.js',
709
- 'hooks/ponytail-instructions.js',
710
- 'hooks/ponytail-config.js',
711
- ...['ponytail', 'ponytail-audit', 'ponytail-debt', 'ponytail-gain', 'ponytail-help', 'ponytail-review']
712
- .map((skill) => `skills/${skill}/SKILL.md`),
713
- ];
714
811
  const PI_PONYTAIL_PACKAGE = `${JSON.stringify({ name: 'ponytail', version: '0.0.0', private: true, pi: { extensions: ['./pi-extension/index.js'], skills: ['./skills'] } }, null, 2)}\n`;
715
812
  async function stepPonytailForPi(agentDir, options) {
716
813
  if (!options.quiet)
@@ -736,8 +833,7 @@ async function stepPonytailForPi(agentDir, options) {
736
833
  }
737
834
  if (options.dryRun)
738
835
  return;
739
- // Only when pi still declares the npm spec: with nothing to remove, `pi remove` still
740
- // shells out to npm, which warns on stderr and exits non-zero.
836
+ // Only when pi still declares the npm spec: with nothing to remove, `pi remove` still warns and exits non-zero.
741
837
  const declared = await piDeclaredPackages(agentDir);
742
838
  if (declared.some(isNpmPonytailPackage)) {
743
839
  try {
@@ -783,8 +879,7 @@ async function registerPiLocalPonytail(agentDir, dest, options) {
783
879
  function comboHint(host, comboDefault) {
784
880
  return comboDefault === 'off' ? `\nDone — restart ${host}.` : `\nDone — restart ${host}, then /combo ${comboDefault}.`;
785
881
  }
786
- async function runInstall() {
787
- // Without this guard a picked command re-enters here with command === null and loops straight back into the picker.
882
+ async function runInstall(forcedHost) {
788
883
  if (command === null && tty() && !yes && !updatePromptDone) {
789
884
  await runCommandMenu();
790
885
  return;
@@ -798,7 +893,9 @@ async function runInstall() {
798
893
  console.error(`[fail] ${e.message}`);
799
894
  process.exit(1);
800
895
  }
801
- if (pinned)
896
+ if (forcedHost)
897
+ targetHost = forcedHost;
898
+ else if (pinned)
802
899
  targetHost = pinned;
803
900
  else if (tty() && !yes && !dryRun && !applyUpdate && command !== 'uninstall') {
804
901
  targetHost = await chooseHost('Install Tersio into which agent?');
@@ -816,7 +913,7 @@ async function runInstall() {
816
913
  if (tty() && !applyUpdate && command !== 'uninstall') {
817
914
  const newer = await checkForUpdate();
818
915
  if (typeof newer === 'string') {
819
- // Update pending: offer it here instead of burying it above the install prompts. Yes runs the full update and stops.
916
+ // Update pending: offer it here; yes runs the full update and stops.
820
917
  if (command === null && !dryRun && !yes && !updatePromptDone) {
821
918
  const answer = await askInteractiveConfirm(`tersio ${newer} is available (installed ${PACKAGE_VERSION}). Install it now?`);
822
919
  if (answer.status === 'confirmed' && answer.value) {
@@ -840,7 +937,7 @@ async function runInstall() {
840
937
  const userDir = OMP_AGENT_DIR;
841
938
  const userExtDir = path.join(userDir, 'extensions');
842
939
  // apply-update refreshes every add-on like a reinstall.
843
- const installOptions = { dryRun, verbose, yes, reinstall: applyUpdate, quiet };
940
+ const installOptions = { dryRun, verbose, yes, reinstall: applyUpdate || forceReinstall, quiet };
844
941
  // Point of no return: disk writes start here; pipes stay unattended.
845
942
  if (!yes && !dryRun && !applyUpdate && tty()) {
846
943
  const target = hostEntry(targetHost);
@@ -905,10 +1002,7 @@ async function runInstall() {
905
1002
  await capture('self-plugin', async () => { selfPlugin = await stepSelfPlugin(OMP_PLUGINS_DIR, installOptions); });
906
1003
  await capture('ponytail', () => stepPonytail(OMP_PLUGINS_DIR, installOptions));
907
1004
  await capture('rtk', () => stepRtk(BUN_BIN_DIR, installOptions));
908
- // A real install registers the plugin before it writes, so the OMP plugin manifest owns
909
- // every mode extension. A dry run previews that same destination. A registration that
910
- // failed is the one fallback: the agent tree then carries the tree so an offline install
911
- // still loads its add-ons, and the retired copies stay untouched.
1005
+ // The plugin manifest owns every mode extension; only a failed registration falls back to the agent tree.
912
1006
  const pluginExtDir = path.join(OMP_PLUGINS_DIR, 'node_modules', '@krtclcdy', 'tersio', 'extensions');
913
1007
  const ownsTree = selfPlugin || dryRun;
914
1008
  const extDir = ownsTree ? pluginExtDir : userExtDir;
@@ -917,10 +1011,17 @@ async function runInstall() {
917
1011
  await capture('shared', () => stepSharedSessionState(extDir, installOptions));
918
1012
  await capture('rtk session', () => stepRtkSession(extDir, installOptions));
919
1013
  await capture('rtk filter', () => stepRtkFilter(extDir, installOptions));
1014
+ await capture('timing', () => stepTiming(extDir, installOptions));
920
1015
  await capture('caveman', () => stepCaveman(extDir, installOptions));
921
1016
  await capture('combo', () => stepCombo(extDir, installOptions));
1017
+ await capture('pstack', () => stepPstack(extDir, installOptions));
922
1018
  await capture('commands', () => stepTersioCommands(extDir, installOptions));
923
1019
  await capture('updater', () => stepUpdater(extDir, installOptions));
1020
+ // One package, one file set: the repair writes every tree file here, so the install does too.
1021
+ if (ownsTree)
1022
+ await capture('plugin entry', () => stepPluginEntry(extDir, installOptions));
1023
+ if (ownsTree)
1024
+ await capture('plugin manifest', () => syncPluginManifest(path.dirname(pluginExtDir), installOptions));
924
1025
  if (ownsTree)
925
1026
  await capture('retired agent copies', () => stepRetireAgentCopies(userExtDir, installOptions));
926
1027
  if (selfPlugin)
@@ -934,4 +1035,4 @@ async function runInstall() {
934
1035
  }
935
1036
  closeRL();
936
1037
  }
937
- export { runInstall, verifyRtkArchive };
1038
+ export { runInstall, runReinstall, verifyRtkArchive };
@@ -30,7 +30,7 @@ let spinnerDepth = 0;
30
30
  async function withInteractiveSpinner(message, work) {
31
31
  if (!tty() || spinnerDepth > 0 || process.env.CI === 'true')
32
32
  return work(() => { });
33
- // Clack manages stdin itself. Close the legacy question interface before its first use; non-interactive callers never reach this branch.
33
+ // Clack manages stdin itself. Close the legacy question interface first; non-interactive callers skip it.
34
34
  closeRL();
35
35
  const active = clackSpinner({ indicator: 'timer' });
36
36
  active.start(message);
@@ -73,7 +73,7 @@ async function askInteractiveConfirm(message, initialValue = true) {
73
73
  finally {
74
74
  }
75
75
  }
76
- // Confirm a destructive run: a Clack dialog at a terminal, a plain prompt in a pipe or script. False means the user declined or aborted.
76
+ // Confirm a destructive run: a Clack dialog at a terminal, a plain prompt in a pipe. False means declined or aborted.
77
77
  async function confirmDestructive(message) {
78
78
  if (tty()) {
79
79
  closeRL();
@@ -98,7 +98,7 @@ async function confirmDestructive(message) {
98
98
  }
99
99
  return true;
100
100
  }
101
- // Run collecting work under one TTY-only Clack task. Callers print after the task completes, keeping normal output out of the spinner animation.
101
+ // Run collecting work under one TTY-only Clack task; callers print after it, so output stays out of the spinner.
102
102
  async function runInteractivePhase(title, collect) {
103
103
  if (!tty())
104
104
  return collect();
@@ -1,9 +1,28 @@
1
1
  // Single list of install writes, shared by installer, doctor, and tests.
2
2
  import path from 'node:path';
3
+ import { readdirSync, readFileSync } from 'node:fs';
3
4
  import { fileURLToPath } from 'node:url';
4
5
  import { findPackageRoot, readTextIfExists } from '../extensions/shared/utils.js';
5
6
  import { writeIfChanged } from './common.js';
6
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();
7
26
  /** Every file in the tree, as paths relative to the extensions directory. */
8
27
  export const TREE_FILES = [
9
28
  'shared/host.ts',
@@ -12,15 +31,24 @@ export const TREE_FILES = [
12
31
  'shared/types.ts',
13
32
  'shared/plugin-settings.ts',
14
33
  'shared/usage-ledger.ts',
34
+ 'shared/sqlite.ts',
15
35
  'shared/pricing.ts',
16
36
  'shared/carbon.ts',
17
37
  'shared/utils.ts',
38
+ 'shared/rtk-eligible.ts',
18
39
  'caveman/index.ts',
19
40
  'caveman/rule.md',
41
+ 'caveman/rule-lite.md',
42
+ 'caveman/rule-megacave-lite.md',
43
+ 'caveman/rule-megacave-ultra.md',
20
44
  'caveman/rule-ultra.md',
21
45
  'caveman/rule-megacave.md',
46
+ 'pstack/index.ts',
47
+ 'pstack/rule.md',
48
+ ...TREE_PSTACK_FILES,
22
49
  'rtk/session.ts',
23
50
  'rtk/filter.ts',
51
+ 'timing/index.ts',
24
52
  'combo/index.ts',
25
53
  'commands/index.ts',
26
54
  'updater/index.ts',
@@ -42,24 +70,96 @@ export function filesUnder(dir) {
42
70
  export function rootFiles() {
43
71
  return TREE_FILES.filter((file) => !file.includes('/')).map((file) => [sourcePath(file), file]);
44
72
  }
45
- /** The OpenCode plugin entry. It imports `@opencode/plugin`, which the omp and pi loaders lack, so it ships in the OpenCode tree alone. */
73
+ /** The OpenCode plugin entry: imports `@opencode/plugin`, absent from omp and pi loaders, so it ships there alone. */
46
74
  export const OPENCODE_ENTRY = 'server.ts';
75
+ /** pi's rtk entry manifest, generated rather than copied: no other host loads rtk that way. */
76
+ export const PI_RTK_MANIFEST = 'rtk/package.json';
77
+ /** pi's rtk entry manifest, generated rather than copied: no other host loads rtk that way. */
78
+ export const PI_RTK_MANIFEST_BODY = `${JSON.stringify({ pi: { extensions: ['./session.ts', './filter.ts'] } }, null, 2)}\n`;
79
+ /** Files the install generates, keyed by tree path, so one repair path handles copied and generated alike. */
80
+ export const GENERATED_TREE_FILES = {
81
+ [PI_RTK_MANIFEST]: PI_RTK_MANIFEST_BODY,
82
+ };
47
83
  /** The files one host's extension tree holds. The package tree holds all of them. */
48
84
  export function treeFilesFor(id) {
49
- return id === 'opencode' ? TREE_FILES : TREE_FILES.filter((file) => file !== OPENCODE_ENTRY);
85
+ if (id === 'opencode')
86
+ return TREE_FILES;
87
+ const files = TREE_FILES.filter((file) => file !== OPENCODE_ENTRY);
88
+ return id === 'pi' ? [...files, PI_RTK_MANIFEST] : files;
89
+ }
90
+ /** Obsolete install debris; explicit, not a sweep, because these trees are shared with tersio's other steps. */
91
+ const OBSOLETE_DIRS = ['caveman-session', 'rtk-filter', 'rtk-session', 'combo-toggle', 'tersio-commands', 'ai-addons-updater', 'lib', 'opencode'];
92
+ /** Debris test for a tree-relative path; one definition shared by detection and repair. */
93
+ export function isObsoleteTreeFile(rel, id) {
94
+ if (rel.endsWith('.bak') || rel === '.DS_Store')
95
+ return true;
96
+ if (OBSOLETE_DIRS.includes(rel.split('/')[0]))
97
+ return true;
98
+ return rel === 'server.ts' && id !== 'opencode';
99
+ }
100
+ /** A tree file's text: copied files come from the repo, generated ones from their own definition. */
101
+ export function treeFileText(file) {
102
+ const generated = GENERATED_TREE_FILES[file];
103
+ if (generated !== undefined)
104
+ return generated;
105
+ try {
106
+ return readFileSync(sourcePath(file), 'utf8');
107
+ }
108
+ catch {
109
+ return null;
110
+ }
50
111
  }
51
- /**
52
- * Copy every bundled file into `dest`, creating the directories it needs. A file the bundle does not
53
- * carry is reported and skipped rather than aborting, so one missing source cannot stop the rest.
54
- *
55
- * `skip` answers "leave this file alone", which is how the rule bodies survive a repair: an upstream
56
- * fetch can be newer than the bundled copy, and a re-fetch never overwrites it.
57
- */
112
+ /** The ponytail ruleset this build ships, copied to every host that cannot install the package. */
113
+ export const PONYTAIL_SKILL_NAMES = ['ponytail', 'ponytail-audit', 'ponytail-debt', 'ponytail-gain', 'ponytail-help', 'ponytail-review'];
114
+ /** The OpenCode bundle: no node_modules there, so the ruleset rides along as files. */
115
+ export const PONYTAIL_BUNDLE_FILES = [
116
+ 'hooks/ponytail-instructions.js',
117
+ 'hooks/ponytail-config.js',
118
+ ...PONYTAIL_SKILL_NAMES.map((skill) => `skills/${skill}/SKILL.md`),
119
+ ];
120
+ /** pi's local package: the same ruleset, plus the extension that injects it. */
121
+ export const PI_PONYTAIL_FILES = ['pi-extension/index.js', ...PONYTAIL_BUNDLE_FILES];
122
+ /** The shipped package manifest, one level above the extension tree. */
123
+ const MANIFEST_PATH = path.join(path.dirname(EXT_DIR), 'package.json');
124
+ /** The `omp` block a host reads for what to load: extensions, features, and the settings schema. */
125
+ export function shippedOmpManifest() {
126
+ try {
127
+ const pkg = JSON.parse(readFileSync(MANIFEST_PATH, 'utf8'));
128
+ return typeof pkg.omp === 'object' && pkg.omp !== null ? pkg.omp : null;
129
+ }
130
+ catch {
131
+ return null;
132
+ }
133
+ }
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.
135
+ export async function syncPluginManifest(pluginDir, options = {}) {
136
+ const omp = shippedOmpManifest();
137
+ if (omp === null)
138
+ return;
139
+ const pkgPath = path.join(pluginDir, 'package.json');
140
+ let pkg;
141
+ try {
142
+ pkg = JSON.parse(readFileSync(pkgPath, 'utf8'));
143
+ }
144
+ catch {
145
+ return;
146
+ }
147
+ if (JSON.stringify(pkg.omp) === JSON.stringify(omp))
148
+ return;
149
+ pkg.omp = omp;
150
+ await writeIfChanged(pkgPath, `${JSON.stringify(pkg, null, 2)}\n`, options);
151
+ }
152
+ /** Copy bundled files into `dest`; misses are reported and skipped, `skip` keeps newer fetched rule bodies. */
58
153
  export async function syncTreeFiles(dest, files, options, skip) {
59
154
  const missing = [];
60
155
  for (const file of files) {
61
156
  if (skip?.(file))
62
157
  continue;
158
+ const generated = GENERATED_TREE_FILES[file];
159
+ if (generated !== undefined) {
160
+ await writeIfChanged(path.join(dest, ...file.split('/')), generated, options);
161
+ continue;
162
+ }
63
163
  const from = sourcePath(file);
64
164
  const text = await readTextIfExists(from);
65
165
  if (text === null) {