@krtclcdy/tersio 2.24.0 → 2.25.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. package/README.md +28 -24
  2. package/cli/banner.js +7 -5
  3. package/cli/common.js +14 -13
  4. package/cli/currency.js +2 -7
  5. package/cli/dashboard.js +220 -13
  6. package/cli/doctor-fix.js +99 -29
  7. package/cli/doctor.js +32 -35
  8. package/cli/hosts.js +23 -26
  9. package/cli/install.js +228 -115
  10. package/cli/interactive.js +65 -21
  11. package/cli/manifest.js +8 -4
  12. package/cli/profile.js +49 -16
  13. package/cli/reset.js +3 -7
  14. package/cli/rtk-wiring.js +29 -32
  15. package/cli/settings.js +40 -16
  16. package/cli/uninstall.js +79 -29
  17. package/cli/update.js +27 -20
  18. package/cli/usage.js +54 -19
  19. package/dashboard/dist/index.html +18 -18
  20. package/extensions/ai-addons-updater/index.js +75 -20
  21. package/extensions/ai-addons-updater/index.ts +67 -23
  22. package/extensions/caveman-session/index.js +33 -75
  23. package/extensions/caveman-session/index.ts +35 -77
  24. package/extensions/combo-toggle/index.js +40 -62
  25. package/extensions/combo-toggle/index.ts +38 -72
  26. package/extensions/lib/utils.js +64 -15
  27. package/extensions/lib/utils.ts +62 -15
  28. package/extensions/opencode/index.js +104 -0
  29. package/extensions/opencode/server.js +190 -0
  30. package/extensions/opencode/server.ts +200 -0
  31. package/extensions/rtk-session/index.js +38 -57
  32. package/extensions/rtk-session/index.ts +36 -55
  33. package/extensions/shared/carbon.js +4 -10
  34. package/extensions/shared/carbon.ts +5 -10
  35. package/extensions/shared/host.js +95 -56
  36. package/extensions/shared/host.ts +106 -60
  37. package/extensions/shared/plugin-settings.js +6 -18
  38. package/extensions/shared/plugin-settings.ts +6 -18
  39. package/extensions/shared/pricing.js +79 -27
  40. package/extensions/shared/pricing.ts +80 -28
  41. package/extensions/shared/rtk-gain.js +4 -11
  42. package/extensions/shared/rtk-gain.ts +4 -11
  43. package/extensions/shared/session-state.js +28 -39
  44. package/extensions/shared/session-state.ts +29 -38
  45. package/extensions/shared/status.js +38 -0
  46. package/extensions/shared/status.ts +45 -0
  47. package/extensions/shared/types.js +1 -2
  48. package/extensions/shared/types.ts +2 -2
  49. package/extensions/shared/usage-ledger.js +312 -57
  50. package/extensions/shared/usage-ledger.ts +321 -61
  51. package/extensions/shared/usage-store.js +354 -38
  52. package/extensions/shared/usage-store.ts +360 -43
  53. package/extensions/tersio-commands/index.js +5 -14
  54. package/extensions/tersio-commands/index.ts +5 -14
  55. package/package.json +12 -7
  56. package/tersio.js +4 -6
  57. package/cli/agent-hosts.js +0 -42
  58. package/cli/agents.js +0 -266
  59. package/cli/layer.js +0 -24
  60. package/cli/omp-layer.js +0 -51
  61. package/cli/pi-layer.js +0 -37
  62. package/cli/pi-wiring.js +0 -92
  63. package/extensions/shared/omp-prompt.js +0 -12
@@ -1,45 +1,28 @@
1
1
  import { readFileSync } from 'node:fs';
2
2
  import { fileURLToPath } from 'node:url';
3
- import { activeModesSummary, getSharedComboState, isComboPresetActive, isOmpSubagentPrompt, lastCustomValue, normalizeInputCommand, normalizeMode, paintStatusBar, paintableCtx, reconcileSharedComboEntries, sessionEntries, setSharedComboListener, setSharedComboMode } from '../shared/session-state.ts';
3
+ import { getSharedComboState, hasModeState, isOmpSubagentPrompt, lastCustomValue, normalizeInputCommand, normalizeMode, reconcileSharedComboEntries, sessionEntries, setSharedComboListener, setSharedComboMode, systemPromptIncludes } from '../shared/session-state.ts';
4
4
  import { dirname, join } from 'node:path';
5
5
  import { injectPromptText, onHostEvent, setExtensionLabel } from '../shared/host.ts';
6
+ import { announceStatus } from '../shared/status.ts';
6
7
  import { readCavemanDefault } from '../shared/plugin-settings.ts';
7
8
  import type { ExtensionApi, ExtensionCtx, InputEvent, SessionEntry, SystemPromptEvent } from '../shared/types.ts';
8
9
 
9
10
  const CAVERN_DIR = dirname(fileURLToPath(import.meta.url));
10
11
  const RULE_PATH = join(CAVERN_DIR, 'rule.md');
11
12
 
12
- const FALLBACK_FULL_RULE = `Caveman full active for this session.
13
- Respond terse like smart caveman. Keep all technical substance. Drop filler, pleasantries, and hedging.
14
-
15
- Rules:
16
- - Drop articles where meaning stays clear. Keep fragments and short sentences.
17
- - Keep technical terms, code, commands, paths, API names, numbers, units, and errors exact.
18
- - Never invent abbreviations or causal arrows. Do not add words to sound caveman.
19
- - Use ASD-STE100 Simplified Technical English. Keep one idea per sentence, target 20 words, active voice, and consistent terms.
20
- - Make no tool-call narration. Use no decorative tables or emoji.
21
- - Preserve the user's reply language. Compress style, not language.
22
- - Drop caveman for security warnings, irreversible actions, ambiguous multi-step sequences, or requests to clarify.
23
- - Write code, comments, commits, docs, issues, pull requests, and third-party messages in normal prose.
24
-
25
- Default: **full**. Switch: \`/caveman lite|full|ultra|wenyan-lite|wenyan-full|wenyan-ultra|off\`. Stop: "stop caveman" or "normal mode".`;
26
-
27
- // ponytail: synchronous read each full-mode injection; ceiling = small file, cold session start. Upgrade path: cache file contents + mtime, invalidate on change.
13
+ // Fail loud: a wrong rule reads to the model as the real one.
28
14
  function readFullRule(): string {
29
- try { return readFileSync(RULE_PATH, 'utf8'); } catch { return FALLBACK_FULL_RULE; }
15
+ try { return readFileSync(RULE_PATH, 'utf8'); } catch {
16
+ return 'Caveman full requested, but rule.md is missing. Run: tersio doctor --fix extensions';
17
+ }
30
18
  }
31
19
 
32
20
  const DEFAULT_MODE = 'off';
33
- const INSTRUCTIONS: Record<string, string | (() => string)> = {
34
- lite: `Caveman lite active for this session.
35
- Respond concise. Drop pleasantries, filler, and hedging. Keep complete technical substance. Code, commands, paths, errors, commits, and PR text stay normal/exact.`,
36
- full: () => `Caveman full active for this session.\n${readFullRule()}`,
37
- ultra: `Caveman ultra active for this session.
38
- Maximum terse prose. Strip conjunctions only when meaning stays clear. State each fact once. Use no invented abbreviations or causal arrows. Keep code, commands, commits, PR text, paths, API names, numbers, units, and errors exact. Drop caveman for security warnings, irreversible actions, ambiguous multi-step sequences, or user confusion.`,
39
- 'wenyan-lite': `Caveman wenyan-lite active for this session. Use semi-classical terse prose. Keep grammar structure, user language, technical terms, code, commands, paths, API names, numbers, units, and errors exact. Drop caveman when clarity or safety needs normal prose.`,
40
- 'wenyan-full': `Caveman wenyan-full active for this session. Use maximum classical terseness where meaning stays clear. Use classical sentence patterns, verbs before objects, and optional omitted subjects. Keep user language, technical terms, code, commands, paths, API names, numbers, units, and errors exact. Drop caveman when clarity or safety needs normal prose.`,
41
- 'wenyan-ultra': `Caveman wenyan-ultra active for this session. Use extreme classical abbreviation while keeping a classical Chinese feel and meaning clear. Keep user language, technical terms, code, commands, paths, API names, numbers, units, and errors exact. Drop caveman when clarity or safety needs normal prose.`,
42
- };
21
+
22
+ // Upstream ships one SKILL.md; the level is a directive inside it, so swap only the header.
23
+ function buildInstruction(mode: string): string {
24
+ return `Caveman ${mode} active for this session.\n${readFullRule()}`;
25
+ }
43
26
 
44
27
  function resolveMode(entries: SessionEntry[] | null | undefined, fallback: string = DEFAULT_MODE): string {
45
28
  return lastCustomValue(entries, 'caveman-mode', (data) => normalizeMode('caveman', data?.mode)) ?? fallback;
@@ -52,20 +35,7 @@ function isOffCommand(text: unknown): boolean {
52
35
 
53
36
  export default function cavemanSessionExtension(pi: ExtensionApi): void {
54
37
  let currentMode = DEFAULT_MODE;
55
- let isActive = false;
56
- let lastCtx: ExtensionCtx | undefined = undefined;
57
-
58
- function syncStatus(ctx?: ExtensionCtx): void {
59
- lastCtx = paintableCtx(lastCtx, ctx);
60
- const c = lastCtx;
61
- if (!c?.ui?.setStatus) return;
62
- // Combo owns the bar when any preset is active; keep ours empty to avoid duplication.
63
- if (isComboPresetActive() || currentMode === 'off') {
64
- c.ui.setStatus('caveman', undefined);
65
- return;
66
- }
67
- paintStatusBar(c.ui, 'caveman', '🪨', `caveman: ${currentMode.toUpperCase()}`, isActive);
68
- }
38
+ let lastInjected: string | undefined = undefined;
69
39
 
70
40
  function setMode(mode: string, ctx?: ExtensionCtx): boolean {
71
41
  const normalized = normalizeMode('caveman', mode);
@@ -73,28 +43,18 @@ export default function cavemanSessionExtension(pi: ExtensionApi): void {
73
43
  currentMode = normalized;
74
44
  pi.appendEntry?.('caveman-mode', { mode: normalized });
75
45
  setSharedComboMode('caveman', normalized);
76
- syncStatus(ctx);
77
- const active = activeModesSummary(getSharedComboState());
78
- const msg = normalized === 'off'
79
- ? `Caveman off. Active: ${active}.`
80
- : `Caveman ${normalized} on — terse replies for this session. Active: ${active}.`;
81
- ctx?.ui?.notify?.(msg, 'info');
46
+ announceStatus(ctx);
82
47
  return true;
83
48
  }
84
49
 
85
50
  setExtensionLabel(pi, 'Caveman session toggle');
86
51
 
87
- // Live mirror: a /tersio or /combo switch publishes shared state — adopt
88
- // it at once so the next turn injects the new mode with no session reload.
89
- // Stable identity, so the bridge set dedupes across re-inits.
52
+ // Adopt a /tersio or /combo switch at once, so the next turn needs no reload.
90
53
  function syncFromShared(state: { caveman: string }): void {
91
54
  const mode = normalizeMode('caveman', state.caveman);
92
- if (mode) {
93
- currentMode = mode;
94
- syncStatus();
95
- }
55
+ if (mode) currentMode = mode;
96
56
  }
97
- setSharedComboListener(syncFromShared);
57
+ setSharedComboListener('caveman', syncFromShared);
98
58
  pi.registerCommand?.('caveman', {
99
59
  description: 'Toggle terse caveman replies for this session',
100
60
  handler: async (args, ctx) => {
@@ -104,7 +64,7 @@ export default function cavemanSessionExtension(pi: ExtensionApi): void {
104
64
  return;
105
65
  }
106
66
  if (arg === 'status') {
107
- ctx?.ui?.notify?.(`Caveman: ${currentMode}`, 'info');
67
+ announceStatus(ctx);
108
68
  return;
109
69
  }
110
70
  if (!setMode(arg, ctx)) {
@@ -120,21 +80,18 @@ export default function cavemanSessionExtension(pi: ExtensionApi): void {
120
80
 
121
81
  function restoreMode(ctx?: ExtensionCtx): void {
122
82
  const entries = sessionEntries(ctx);
123
- // Publish the persisted combo state (incl. combo-level) before painting:
124
- // bar suppression reads the in-process bridge, which is empty in a fresh
125
- // host until the combo extension reconciles — and its reconcile is
126
- // UI-gated. Deriving it here makes suppression independent of load order.
127
- reconcileSharedComboEntries(entries);
128
- // Persisted session state wins; a fresh session falls back to the
129
- // installer/user-configured default (off unless configured).
83
+ // A subagent's branch has no Tersio entries; reconciling it would wipe shared state.
84
+ if (hasModeState(entries)) reconcileSharedComboEntries(entries);
85
+ // Persisted state wins, then a mode already chosen this session, then the configured default. `off` is the start value, not "unset".
130
86
  const persisted = resolveMode(entries, '');
131
- currentMode = persisted || normalizeMode('caveman', readCavemanDefault()) || DEFAULT_MODE;
132
- syncStatus(ctx);
87
+ const alreadyChosen = currentMode !== DEFAULT_MODE;
88
+ currentMode = persisted || (alreadyChosen ? currentMode : normalizeMode('caveman', readCavemanDefault())) || DEFAULT_MODE;
89
+ announceStatus(ctx);
133
90
  }
134
91
 
92
+ // Start, resume, and branch all land here, so every session type shows it.
135
93
  pi.on('session_start', async (_event, ctx) => {
136
94
  restoreMode(ctx);
137
- ctx?.ui?.notify?.(`Caveman loaded: ${currentMode}`, 'info');
138
95
  });
139
96
 
140
97
  onHostEvent(pi, 'session_branch', async (_event, ctx) => {
@@ -145,22 +102,23 @@ export default function cavemanSessionExtension(pi: ExtensionApi): void {
145
102
  restoreMode(ctx);
146
103
  });
147
104
 
148
- pi.on('agent_start', async (_event, ctx) => {
149
- isActive = true;
150
- syncStatus(ctx);
105
+ onHostEvent(pi, 'session_switch', async (_event, ctx) => {
106
+ restoreMode(ctx);
151
107
  });
152
108
 
153
- pi.on('agent_end', async (_event, ctx) => {
154
- isActive = false;
155
- syncStatus(ctx);
109
+ onHostEvent(pi, 'session_compact', async (_event, ctx) => {
110
+ restoreMode(ctx);
156
111
  });
157
112
 
158
113
  pi.on('before_agent_start', async (event: SystemPromptEvent) => {
159
114
  const mode = isOmpSubagentPrompt(event.systemPrompt) ? getSharedComboState().caveman : currentMode;
160
115
  if (!mode || mode === 'off') return;
161
- const def = INSTRUCTIONS[mode];
162
- if (!def) return;
163
- const instruction = typeof def === 'function' ? def() : def;
164
- return injectPromptText(pi, event, instruction);
116
+ const instruction = buildInstruction(mode);
117
+ // Match the whole instruction so a re-presented prompt cannot stack it.
118
+ if (systemPromptIncludes(event.systemPrompt, instruction)) return;
119
+ // The previous level's block is replaced, not stacked on.
120
+ const stale = lastInjected;
121
+ lastInjected = instruction;
122
+ return injectPromptText(pi, event, instruction, stale);
165
123
  });
166
124
  }
@@ -8,11 +8,13 @@ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExte
8
8
  return path;
9
9
  };
10
10
  import path from 'node:path';
11
+ import { existsSync } from 'node:fs';
11
12
  import { fileURLToPath, pathToFileURL } from 'node:url';
12
- import { activeModesSummary, COMBO_LEVELS, getSharedComboState, isComboPresetActive, isOmpSubagentPrompt, normalizeComboLevel, paintableCtx, reconcileSharedComboEntries, sessionEntries, setSharedComboLevel, setSharedComboListener, setSharedComboMode, systemPromptIncludes, themeStatus, } from '../shared/session-state.js';
13
+ import { activeModesSummary, COMBO_LEVELS, getSharedComboState, hasModeState, normalizeComboLevel, reconcileSharedComboEntries, sessionEntries, setSharedComboLevel, setSharedComboListener, setSharedComboMode, systemPromptIncludes, } from '../shared/session-state.js';
13
14
  import { hostSelect, injectPromptText, onHostEvent, setExtensionLabel } from '../shared/host.js';
15
+ import { announceStatus } from '../shared/status.js';
14
16
  import { isComboSetupComplete, readComboDefault, readPonytailDefault, saveComboSetup, } from '../shared/plugin-settings.js';
15
- import { findHoistedPackage } from '../lib/utils.js';
17
+ import { findHoistedPackage, isPiProcess } from '../lib/utils.js';
16
18
  const EXTENSION_DIR = path.dirname(fileURLToPath(import.meta.url));
17
19
  const PONYTAIL_FALLBACK_INTENSITY = {
18
20
  lite: 'Prefer the simplest correct solution.',
@@ -22,16 +24,15 @@ export function ponytailFallback(mode) {
22
24
  const intensity = PONYTAIL_FALLBACK_INTENSITY[mode] ?? 'Use the minimum correct solution. Delete or reuse before adding.';
23
25
  return `🦥 PONYTAIL MODE ACTIVE — level: ${mode}\n${intensity} Understand the path first and fix root causes, not symptoms. Prefer the standard library and YAGNI. Avoid speculative abstractions and dependencies. Preserve correctness. Verify changed behavior.`;
24
26
  }
25
- function levelSummary(state) {
26
- return `caveman=${state.caveman} rtk=${state.rtk} ponytail=${state.ponytail}`;
27
- }
28
- // Ponytail is hoisted, so one upward walk covers every install layout. The hook
29
- // is CommonJS, so both namespace shapes are tried.
27
+ // Ponytail is hoisted, so one upward walk covers every install layout. The hook is CommonJS, so both namespace shapes are tried.
30
28
  async function loadPonytailInstructions(mode) {
31
29
  const installed = findHoistedPackage('@dietrichgebert/ponytail', EXTENSION_DIR, 'hooks', 'ponytail-instructions.js');
32
- if (installed) {
30
+ // The OpenCode tree ships no node_modules; the installer bundles the ruleset beside the extensions.
31
+ const bundled = path.join(EXTENSION_DIR, '..', 'ponytail-bundle', 'hooks', 'ponytail-instructions.js');
32
+ const treeLocal = installed ?? (existsSync(bundled) ? bundled : null);
33
+ if (treeLocal) {
33
34
  try {
34
- const loaded = await import(__rewriteRelativeImportExtension(pathToFileURL(installed).href));
35
+ const loaded = await import(__rewriteRelativeImportExtension(pathToFileURL(treeLocal).href));
35
36
  const build = loaded.getPonytailInstructions ?? loaded.default?.getPonytailInstructions;
36
37
  if (build)
37
38
  return build(mode);
@@ -42,30 +43,8 @@ async function loadPonytailInstructions(mode) {
42
43
  }
43
44
  export default function comboToggleExtension(pi) {
44
45
  setExtensionLabel(pi, 'Combo session toggle (all 3 add-ons)');
45
- let lastCtx = undefined;
46
46
  let setupPrompted = false;
47
- function syncStatus(ctx) {
48
- lastCtx = paintableCtx(lastCtx, ctx);
49
- const c = lastCtx;
50
- if (!c?.ui?.setStatus)
51
- return;
52
- // One unified bar replaces the per-extension bars while a preset is active.
53
- if (!isComboPresetActive()) {
54
- c.ui.setStatus('combo', undefined);
55
- return;
56
- }
57
- // Read the live bridge, not a cache: siblings reconcile it first.
58
- const state = getSharedComboState();
59
- const c0 = state.caveman.toUpperCase();
60
- const r = state.rtk.toUpperCase();
61
- const p = state.ponytail.toUpperCase();
62
- const lvl = state.level.toUpperCase();
63
- c.ui.setStatus('combo', themeStatus(c.ui, '🧩', `combo ${lvl}: 🪨caveman=${c0} ⚡rtk=${r} 🦥ponytail=${p}`, true));
64
- // Our bar is canonical for the preset, so clear any sibling bar.
65
- c.ui.setStatus('caveman', undefined);
66
- c.ui.setStatus('rtk', undefined);
67
- c.ui.setStatus('ponytail', undefined);
68
- }
47
+ let lastInjected = undefined;
69
48
  // Siblings restore from these entries, so the fallback must write them too.
70
49
  function persistPreset(level) {
71
50
  const modes = COMBO_LEVELS[level];
@@ -74,19 +53,15 @@ export default function comboToggleExtension(pi) {
74
53
  pi.appendEntry?.('ponytail-mode', { mode: modes.ponytail });
75
54
  pi.appendEntry?.('combo-level', { level });
76
55
  }
77
- function useState(state, ctx) {
78
- syncStatus(ctx);
79
- return state;
80
- }
81
56
  function reconcile(ctx) {
82
57
  if (!ctx?.hasUI)
83
58
  return getSharedComboState();
84
- return useState(reconcileSharedComboEntries(sessionEntries(ctx)), ctx);
59
+ return reconcileSharedComboEntries(sessionEntries(ctx));
85
60
  }
86
61
  function listen(ctx) {
87
- // Stable identity: the bridge set dedupes, so repeat calls register once.
88
- if (ctx?.hasUI)
89
- setSharedComboListener(useState);
62
+ if (!ctx?.hasUI)
63
+ return;
64
+ setSharedComboListener('combo', (state) => { announceStatus(ctx); return state; });
90
65
  }
91
66
  function track(ctx) {
92
67
  listen(ctx);
@@ -99,8 +74,8 @@ export default function comboToggleExtension(pi) {
99
74
  listen(ctx);
100
75
  const arg = String(args || '').trim().toLowerCase();
101
76
  if (!arg || arg === 'status') {
102
- const state = reconcile(ctx);
103
- ctx?.ui?.notify?.(`Combo: ${state.level === 'custom' ? 'INACTIVE' : state.level.toUpperCase()} (${levelSummary(state)})`, 'info');
77
+ reconcile(ctx);
78
+ announceStatus(ctx);
104
79
  return;
105
80
  }
106
81
  if (arg === 'help') {
@@ -116,9 +91,8 @@ export default function comboToggleExtension(pi) {
116
91
  return;
117
92
  }
118
93
  persistPreset(level);
119
- useState(setSharedComboLevel(level), ctx);
120
- const active = activeModesSummary(getSharedComboState());
121
- ctx?.ui?.notify?.(level === 'off' ? `Combo off — all tersio modes inactive for this session. Active: ${active}.` : `Combo ${level} on — ${active} active for this session.`, 'info');
94
+ setSharedComboLevel(level);
95
+ announceStatus(ctx);
122
96
  },
123
97
  });
124
98
  async function runFirstRunSetup(ctx) {
@@ -134,40 +108,39 @@ export default function comboToggleExtension(pi) {
134
108
  const level = choice || 'off';
135
109
  if (saveComboSetup(level) && level !== 'off') {
136
110
  persistPreset(level);
137
- useState(setSharedComboLevel(level), ctx);
111
+ setSharedComboLevel(level);
138
112
  ctx.ui.notify?.(`Combo default saved: ${level} — ${activeModesSummary(getSharedComboState())} will activate on fresh sessions.`, 'info');
139
113
  }
140
114
  }
141
115
  pi.on('session_start', async (_event, ctx) => {
142
116
  track(ctx);
143
- if (!ctx?.hasUI)
144
- syncStatus(ctx);
145
117
  await runFirstRunSetup(ctx);
146
118
  // The configured default applies only when no persisted mode state exists.
147
119
  const entries = sessionEntries(ctx);
148
- const hasModeState = entries.some((e) => e?.type === 'custom' && (e.customType === 'combo-level' || e.customType === 'caveman-mode' ||
149
- e.customType === 'rtk-mode' || e.customType === 'ponytail-mode'));
150
- if (getSharedComboState().level === 'off' && !hasModeState) {
120
+ const persisted = hasModeState(entries);
121
+ if (getSharedComboState().level === 'off' && !persisted) {
151
122
  const fallback = readComboDefault();
152
123
  if (fallback !== 'off') {
153
124
  persistPreset(fallback);
154
- useState(setSharedComboLevel(fallback), ctx);
155
- ctx?.ui?.notify?.(`Combo default applied: ${fallback} — ${activeModesSummary(getSharedComboState())} active for this session.`, 'info');
125
+ setSharedComboLevel(fallback);
156
126
  }
157
127
  }
158
- // No sibling restores a standalone ponytail default, so apply it here; skip
159
- // it when the active preset already writes the same mode.
128
+ // No sibling restores a standalone ponytail default, so apply it here; skip it when the active preset already writes the same mode.
160
129
  if (!entries.some((e) => e?.type === 'custom' && e.customType === 'ponytail-mode')) {
161
130
  const ponytailFallback = readPonytailDefault();
162
131
  if (ponytailFallback !== 'off' && ponytailFallback !== getSharedComboState().ponytail) {
163
132
  pi.appendEntry?.('ponytail-mode', { mode: ponytailFallback });
164
- useState(setSharedComboMode('ponytail', ponytailFallback), ctx);
133
+ setSharedComboMode('ponytail', ponytailFallback);
165
134
  }
166
135
  }
136
+ // Announced last, so any default above is reflected.
137
+ announceStatus(ctx);
167
138
  });
168
- for (const event of ['session_branch', 'session_tree', 'agent_start']) {
139
+ // Resume, branch, and compaction re-announce; dedupe keeps repeats silent.
140
+ for (const event of ['session_branch', 'session_tree', 'agent_start', 'session_switch', 'session_compact']) {
169
141
  onHostEvent(pi, event, async (_event, ctx) => {
170
142
  track(ctx);
143
+ announceStatus(ctx);
171
144
  if (event === 'agent_start')
172
145
  await runFirstRunSetup(ctx);
173
146
  });
@@ -175,13 +148,18 @@ export default function comboToggleExtension(pi) {
175
148
  pi.on('before_agent_start', async (event, ctx) => {
176
149
  if (ctx?.hasUI)
177
150
  reconcile(ctx);
178
- if (!isOmpSubagentPrompt(event.systemPrompt))
179
- return;
180
151
  const mode = getSharedComboState().ponytail;
181
- if (mode === 'off' || systemPromptIncludes(event.systemPrompt, 'PONYTAIL MODE ACTIVE'))
152
+ // On pi the upstream ponytail pi-extension is the single injector; it reads these 'ponytail-mode' entries at session start, so Tersio only persists.
153
+ if (isPiProcess())
154
+ return;
155
+ // Match the level's own header: the shared phrase alone makes every level block every other, so a `/combo` level switch is ignored all session.
156
+ if (mode === 'off' || systemPromptIncludes(event.systemPrompt, `PONYTAIL MODE ACTIVE — level: ${mode}`))
182
157
  return;
183
- const instructions = await loadPonytailInstructions(mode);
184
- return injectPromptText(pi, event, instructions);
158
+ const instruction = await loadPonytailInstructions(mode);
159
+ // The previous level's block is replaced, not stacked on.
160
+ const stale = lastInjected;
161
+ lastInjected = instruction;
162
+ return injectPromptText(pi, event, instruction, stale);
185
163
  });
186
164
  // Slash commands only; natural-language input caused accidental toggles and has no reload context.
187
165
  }
@@ -1,32 +1,30 @@
1
1
  // /combo session toggle: off | medium | balanced | max.
2
2
 
3
- import os from 'node:os';
4
3
  import path from 'node:path';
4
+ import { existsSync } from 'node:fs';
5
5
  import { fileURLToPath, pathToFileURL } from 'node:url';
6
6
  import {
7
7
  activeModesSummary,
8
8
  COMBO_LEVELS,
9
9
  getSharedComboState,
10
- isComboPresetActive,
11
- isOmpSubagentPrompt,
10
+ hasModeState,
12
11
  normalizeComboLevel,
13
- paintableCtx,
14
12
  reconcileSharedComboEntries,
15
13
  sessionEntries,
16
14
  setSharedComboLevel,
17
15
  setSharedComboListener,
18
16
  setSharedComboMode,
19
17
  systemPromptIncludes,
20
- themeStatus,
21
18
  } from '../shared/session-state.ts';
22
19
  import { hostSelect, injectPromptText, onHostEvent, setExtensionLabel } from '../shared/host.ts';
20
+ import { announceStatus } from '../shared/status.ts';
23
21
  import {
24
22
  isComboSetupComplete,
25
23
  readComboDefault,
26
24
  readPonytailDefault,
27
25
  saveComboSetup,
28
26
  } from '../shared/plugin-settings.ts';
29
- import { findHoistedPackage } from '../lib/utils.ts';
27
+ import { findHoistedPackage, isPiProcess } from '../lib/utils.ts';
30
28
  import type { ComboState, ExtensionApi, ExtensionCtx, SystemPromptEvent } from '../shared/types.ts';
31
29
 
32
30
  const EXTENSION_DIR = path.dirname(fileURLToPath(import.meta.url));
@@ -41,17 +39,16 @@ export function ponytailFallback(mode: string): string {
41
39
  return `🦥 PONYTAIL MODE ACTIVE — level: ${mode}\n${intensity} Understand the path first and fix root causes, not symptoms. Prefer the standard library and YAGNI. Avoid speculative abstractions and dependencies. Preserve correctness. Verify changed behavior.`;
42
40
  }
43
41
 
44
- function levelSummary(state: ComboState): string {
45
- return `caveman=${state.caveman} rtk=${state.rtk} ponytail=${state.ponytail}`;
46
- }
47
42
 
48
- // Ponytail is hoisted, so one upward walk covers every install layout. The hook
49
- // is CommonJS, so both namespace shapes are tried.
43
+ // Ponytail is hoisted, so one upward walk covers every install layout. The hook is CommonJS, so both namespace shapes are tried.
50
44
  async function loadPonytailInstructions(mode: string): Promise<string> {
51
45
  const installed = findHoistedPackage('@dietrichgebert/ponytail', EXTENSION_DIR, 'hooks', 'ponytail-instructions.js');
52
- if (installed) {
46
+ // The OpenCode tree ships no node_modules; the installer bundles the ruleset beside the extensions.
47
+ const bundled = path.join(EXTENSION_DIR, '..', 'ponytail-bundle', 'hooks', 'ponytail-instructions.js');
48
+ const treeLocal = installed ?? (existsSync(bundled) ? bundled : null);
49
+ if (treeLocal) {
53
50
  try {
54
- const loaded = await import(pathToFileURL(installed).href) as {
51
+ const loaded = await import(pathToFileURL(treeLocal).href) as {
55
52
  getPonytailInstructions?: (level: string) => string;
56
53
  default?: { getPonytailInstructions?: (level: string) => string };
57
54
  };
@@ -65,30 +62,9 @@ async function loadPonytailInstructions(mode: string): Promise<string> {
65
62
  export default function comboToggleExtension(pi: ExtensionApi): void {
66
63
  setExtensionLabel(pi, 'Combo session toggle (all 3 add-ons)');
67
64
 
68
- let lastCtx: ExtensionCtx | undefined = undefined;
69
65
  let setupPrompted = false;
66
+ let lastInjected: string | undefined = undefined;
70
67
 
71
- function syncStatus(ctx?: ExtensionCtx): void {
72
- lastCtx = paintableCtx(lastCtx, ctx);
73
- const c = lastCtx;
74
- if (!c?.ui?.setStatus) return;
75
- // One unified bar replaces the per-extension bars while a preset is active.
76
- if (!isComboPresetActive()) {
77
- c.ui.setStatus('combo', undefined);
78
- return;
79
- }
80
- // Read the live bridge, not a cache: siblings reconcile it first.
81
- const state = getSharedComboState();
82
- const c0 = state.caveman.toUpperCase();
83
- const r = state.rtk.toUpperCase();
84
- const p = state.ponytail.toUpperCase();
85
- const lvl = state.level.toUpperCase();
86
- c.ui.setStatus('combo', themeStatus(c.ui, '🧩', `combo ${lvl}: 🪨caveman=${c0} ⚡rtk=${r} 🦥ponytail=${p}`, true));
87
- // Our bar is canonical for the preset, so clear any sibling bar.
88
- c.ui.setStatus('caveman', undefined);
89
- c.ui.setStatus('rtk', undefined);
90
- c.ui.setStatus('ponytail', undefined);
91
- }
92
68
 
93
69
  // Siblings restore from these entries, so the fallback must write them too.
94
70
  function persistPreset(level: string): void {
@@ -99,18 +75,13 @@ export default function comboToggleExtension(pi: ExtensionApi): void {
99
75
  pi.appendEntry?.('combo-level', { level });
100
76
  }
101
77
 
102
- function useState(state: Readonly<ComboState>, ctx?: ExtensionCtx): Readonly<ComboState> {
103
- syncStatus(ctx);
104
- return state;
105
- }
106
-
107
78
  function reconcile(ctx?: ExtensionCtx): Readonly<ComboState> {
108
79
  if (!ctx?.hasUI) return getSharedComboState();
109
- return useState(reconcileSharedComboEntries(sessionEntries(ctx)), ctx);
80
+ return reconcileSharedComboEntries(sessionEntries(ctx));
110
81
  }
111
82
  function listen(ctx?: ExtensionCtx): void {
112
- // Stable identity: the bridge set dedupes, so repeat calls register once.
113
- if (ctx?.hasUI) setSharedComboListener(useState);
83
+ if (!ctx?.hasUI) return;
84
+ setSharedComboListener('combo', (state) => { announceStatus(ctx); return state; });
114
85
  }
115
86
 
116
87
  function track(ctx?: ExtensionCtx): void {
@@ -125,11 +96,8 @@ export default function comboToggleExtension(pi: ExtensionApi): void {
125
96
  const arg = String(args || '').trim().toLowerCase();
126
97
 
127
98
  if (!arg || arg === 'status') {
128
- const state = reconcile(ctx);
129
- ctx?.ui?.notify?.(
130
- `Combo: ${state.level === 'custom' ? 'INACTIVE' : state.level.toUpperCase()} (${levelSummary(state)})`,
131
- 'info'
132
- );
99
+ reconcile(ctx);
100
+ announceStatus(ctx);
133
101
  return;
134
102
  }
135
103
 
@@ -154,13 +122,8 @@ export default function comboToggleExtension(pi: ExtensionApi): void {
154
122
  }
155
123
 
156
124
  persistPreset(level);
157
- useState(setSharedComboLevel(level), ctx);
158
-
159
- const active = activeModesSummary(getSharedComboState());
160
- ctx?.ui?.notify?.(
161
- level === 'off' ? `Combo off — all tersio modes inactive for this session. Active: ${active}.` : `Combo ${level} on — ${active} active for this session.`,
162
- 'info'
163
- );
125
+ setSharedComboLevel(level);
126
+ announceStatus(ctx);
164
127
  },
165
128
  });
166
129
 
@@ -176,54 +139,57 @@ export default function comboToggleExtension(pi: ExtensionApi): void {
176
139
  const level = choice || 'off';
177
140
  if (saveComboSetup(level) && level !== 'off') {
178
141
  persistPreset(level);
179
- useState(setSharedComboLevel(level), ctx);
142
+ setSharedComboLevel(level);
180
143
  ctx.ui.notify?.(`Combo default saved: ${level} — ${activeModesSummary(getSharedComboState())} will activate on fresh sessions.`, 'info');
181
144
  }
182
145
  }
183
146
 
184
147
  pi.on('session_start', async (_event, ctx) => {
185
148
  track(ctx);
186
- if (!ctx?.hasUI) syncStatus(ctx);
187
149
  await runFirstRunSetup(ctx);
188
150
  // The configured default applies only when no persisted mode state exists.
189
151
  const entries = sessionEntries(ctx);
190
- const hasModeState = entries.some((e) => e?.type === 'custom' && (
191
- e.customType === 'combo-level' || e.customType === 'caveman-mode' ||
192
- e.customType === 'rtk-mode' || e.customType === 'ponytail-mode'));
193
- if (getSharedComboState().level === 'off' && !hasModeState) {
152
+ const persisted = hasModeState(entries);
153
+ if (getSharedComboState().level === 'off' && !persisted) {
194
154
  const fallback = readComboDefault();
195
155
  if (fallback !== 'off') {
196
156
  persistPreset(fallback);
197
- useState(setSharedComboLevel(fallback), ctx);
198
- ctx?.ui?.notify?.(`Combo default applied: ${fallback} — ${activeModesSummary(getSharedComboState())} active for this session.`, 'info');
157
+ setSharedComboLevel(fallback);
199
158
  }
200
159
  }
201
- // No sibling restores a standalone ponytail default, so apply it here; skip
202
- // it when the active preset already writes the same mode.
160
+ // No sibling restores a standalone ponytail default, so apply it here; skip it when the active preset already writes the same mode.
203
161
  if (!entries.some((e) => e?.type === 'custom' && e.customType === 'ponytail-mode')) {
204
162
  const ponytailFallback = readPonytailDefault();
205
163
  if (ponytailFallback !== 'off' && ponytailFallback !== getSharedComboState().ponytail) {
206
164
  pi.appendEntry?.('ponytail-mode', { mode: ponytailFallback });
207
- useState(setSharedComboMode('ponytail', ponytailFallback), ctx);
165
+ setSharedComboMode('ponytail', ponytailFallback);
208
166
  }
209
167
  }
168
+ // Announced last, so any default above is reflected.
169
+ announceStatus(ctx);
210
170
  });
211
171
 
212
- for (const event of ['session_branch', 'session_tree', 'agent_start']) {
172
+ // Resume, branch, and compaction re-announce; dedupe keeps repeats silent.
173
+ for (const event of ['session_branch', 'session_tree', 'agent_start', 'session_switch', 'session_compact']) {
213
174
  onHostEvent(pi, event, async (_event, ctx) => {
214
175
  track(ctx);
176
+ announceStatus(ctx);
215
177
  if (event === 'agent_start') await runFirstRunSetup(ctx);
216
178
  });
217
179
  }
218
180
 
219
181
  pi.on<SystemPromptEvent>('before_agent_start', async (event, ctx) => {
220
182
  if (ctx?.hasUI) reconcile(ctx);
221
- if (!isOmpSubagentPrompt(event.systemPrompt)) return;
222
-
223
183
  const mode = getSharedComboState().ponytail;
224
- if (mode === 'off' || systemPromptIncludes(event.systemPrompt, 'PONYTAIL MODE ACTIVE')) return;
225
- const instructions = await loadPonytailInstructions(mode);
226
- return injectPromptText(pi, event, instructions);
184
+ // On pi the upstream ponytail pi-extension is the single injector; it reads these 'ponytail-mode' entries at session start, so Tersio only persists.
185
+ if (isPiProcess()) return;
186
+ // Match the level's own header: the shared phrase alone makes every level block every other, so a `/combo` level switch is ignored all session.
187
+ if (mode === 'off' || systemPromptIncludes(event.systemPrompt, `PONYTAIL MODE ACTIVE — level: ${mode}`)) return;
188
+ const instruction = await loadPonytailInstructions(mode);
189
+ // The previous level's block is replaced, not stacked on.
190
+ const stale = lastInjected;
191
+ lastInjected = instruction;
192
+ return injectPromptText(pi, event, instruction, stale);
227
193
  });
228
194
 
229
195
  // Slash commands only; natural-language input caused accidental toggles and has no reload context.