@krtclcdy/tersio 2.25.5 → 2.25.6

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 (73) hide show
  1. package/README.md +17 -36
  2. package/dashboard/dist/index.html +15 -15
  3. package/dist/cli/backups.js +7 -14
  4. package/dist/cli/banner.js +4 -3
  5. package/dist/cli/common.js +28 -12
  6. package/dist/cli/currency.js +1 -1
  7. package/dist/cli/dashboard.js +66 -35
  8. package/dist/cli/doctor-fix.js +204 -19
  9. package/dist/cli/doctor.js +226 -39
  10. package/dist/cli/hosts.js +25 -5
  11. package/dist/cli/install.js +144 -48
  12. package/dist/cli/interactive.js +3 -3
  13. package/dist/cli/manifest.js +87 -7
  14. package/dist/cli/profile.js +13 -8
  15. package/dist/cli/rtk-wiring.js +1 -1
  16. package/dist/cli/settings.js +20 -5
  17. package/dist/cli/uninstall.js +3 -3
  18. package/dist/cli/update.js +6 -7
  19. package/dist/cli/usage.js +6 -9
  20. package/dist/extensions/caveman/index.js +4 -2
  21. package/dist/extensions/caveman/rule-megacave.md +5 -3
  22. package/dist/extensions/caveman/rule-ultra.md +4 -4
  23. package/dist/extensions/caveman/rule.md +55 -45
  24. package/dist/extensions/combo/index.js +62 -21
  25. package/dist/extensions/commands/index.js +1 -1
  26. package/dist/extensions/pstack/index.js +119 -0
  27. package/dist/extensions/pstack/rule.md +65 -0
  28. package/dist/extensions/rtk/filter.js +0 -1
  29. package/dist/extensions/rtk/session.js +4 -4
  30. package/dist/extensions/server.js +90 -6
  31. package/dist/extensions/shared/agent-stats.js +214 -278
  32. package/dist/extensions/shared/brand-marks.js +33 -34
  33. package/dist/extensions/shared/carbon.js +1 -1
  34. package/dist/extensions/shared/plugin-settings.js +11 -6
  35. package/dist/extensions/shared/pricing.js +8 -16
  36. package/dist/extensions/shared/rtk-gain.js +4 -9
  37. package/dist/extensions/shared/session-state.js +15 -10
  38. package/dist/extensions/shared/sqlite.js +168 -0
  39. package/dist/extensions/shared/status.js +8 -8
  40. package/dist/extensions/shared/types.js +1 -1
  41. package/dist/extensions/shared/usage-ledger.js +53 -41
  42. package/dist/extensions/shared/usage-store.js +111 -52
  43. package/dist/extensions/shared/utils.js +5 -5
  44. package/dist/extensions/timing/index.js +31 -0
  45. package/dist/extensions/updater/index.js +3 -6
  46. package/dist/tersio.js +8 -3
  47. package/extensions/caveman/index.ts +4 -2
  48. package/extensions/caveman/rule-megacave.md +5 -3
  49. package/extensions/caveman/rule-ultra.md +4 -4
  50. package/extensions/caveman/rule.md +55 -45
  51. package/extensions/combo/index.ts +62 -19
  52. package/extensions/commands/index.ts +1 -1
  53. package/extensions/pstack/index.ts +128 -0
  54. package/extensions/pstack/rule.md +65 -0
  55. package/extensions/rtk/filter.ts +0 -1
  56. package/extensions/rtk/session.ts +4 -4
  57. package/extensions/server.ts +72 -7
  58. package/extensions/shared/agent-stats.ts +226 -278
  59. package/extensions/shared/brand-marks.ts +34 -34
  60. package/extensions/shared/carbon.ts +1 -1
  61. package/extensions/shared/plugin-settings.ts +12 -6
  62. package/extensions/shared/pricing.ts +10 -26
  63. package/extensions/shared/rtk-gain.ts +4 -9
  64. package/extensions/shared/session-state.ts +18 -13
  65. package/extensions/shared/sqlite.ts +168 -0
  66. package/extensions/shared/status.ts +9 -8
  67. package/extensions/shared/types.ts +2 -1
  68. package/extensions/shared/usage-ledger.ts +60 -47
  69. package/extensions/shared/usage-store.ts +120 -52
  70. package/extensions/shared/utils.ts +5 -5
  71. package/extensions/timing/index.ts +45 -0
  72. package/extensions/updater/index.ts +3 -8
  73. package/package.json +17 -8
@@ -1,4 +1,4 @@
1
- // Shared helpers for the installer and the AI add-ons updater. Node built-ins only — this module must stay dependency-free.
1
+ // Shared helpers for the installer and AI add-ons updater. Node built-ins only — must stay dependency-free.
2
2
  import { createHash } from 'node:crypto';
3
3
  import { accessSync, constants, createWriteStream, existsSync, mkdirSync, renameSync, statSync } from 'node:fs';
4
4
  import fs from 'node:fs/promises';
@@ -87,7 +87,7 @@ function redirectNext(res, url) {
87
87
  }
88
88
  return null;
89
89
  }
90
- // pi sets PI_CODING_AGENT; OMP does not. OMP does read PI_CODING_AGENT_DIR as its own relocation variable, so the override only counts under that marker.
90
+ // pi sets PI_CODING_AGENT; OMP does not. It reads PI_CODING_AGENT_DIR to relocate, so the override only counts there.
91
91
  export function isPiProcess() {
92
92
  return process.env.PI_CODING_AGENT === 'true';
93
93
  }
@@ -95,7 +95,7 @@ export function piAgentDir() {
95
95
  const override = process.env.PI_CODING_AGENT_DIR;
96
96
  return isPiProcess() && override ? override : path.join(homeDir(), '.pi', 'agent');
97
97
  }
98
- // Ponytail is a Tersio dependency that npm hoists, so it can sit in Tersio's node_modules, a host's plugin dir, or beside it. Walking up covers all.
98
+ // Ponytail is hoisted, so it sits in Tersio's node_modules, a host's plugin dir, or beside it; walking up covers all.
99
99
  export function findHoistedPackage(pkg, fromDir, ...relInside) {
100
100
  let dir = fromDir;
101
101
  for (;;) {
@@ -235,7 +235,7 @@ export async function readTextIfExists(p) {
235
235
  return null;
236
236
  }
237
237
  }
238
- // Chunked so a 200 MiB host binary is never held in memory. `tail` keeps the last needle.length-1 bytes of the previous chunk, because a match can begin near the end of one chunk and finish inside the next.
238
+ // Chunked: a 200 MiB binary never fits memory. `tail` keeps needle.length-1 bytes in case a match spans chunks.
239
239
  export async function fileContains(filePath, needleText) {
240
240
  const needle = Buffer.from(needleText, 'utf8');
241
241
  if (needle.length === 0)
@@ -266,7 +266,7 @@ export async function fileContains(filePath, needleText) {
266
266
  await handle?.close().catch(() => { });
267
267
  }
268
268
  }
269
- // Tersio-owned data home: ~/.tersio. First use moves legacy ~/.omp/plugins/tersio-* files over so existing installs keep their history.
269
+ // Tersio data home: ~/.tersio. First use moves legacy ~/.omp/plugins/tersio-* files over so installs keep history.
270
270
  const migratedTersioFiles = new Set();
271
271
  export function tersioHome() {
272
272
  // Matches the other TERSIO_* overrides so a test never reads the real home.
@@ -0,0 +1,31 @@
1
+ export default function timingExtension(pi) {
2
+ // First token per request, keyed by the request's own start stamp, which survives every stream event.
3
+ const firstTokenAt = new Map();
4
+ pi.on('message_update', (event) => {
5
+ const message = event.message;
6
+ if (!message || message.role !== 'assistant')
7
+ return;
8
+ const anchor = typeof message.timestamp === 'number' ? message.timestamp : 0;
9
+ if (anchor <= 0 || firstTokenAt.has(anchor))
10
+ return;
11
+ // `start` opens the stream with no token yet; the first delta is the first token.
12
+ if (!String(event.assistantMessageEvent?.type ?? '').endsWith('_delta'))
13
+ return;
14
+ firstTokenAt.set(anchor, Date.now());
15
+ });
16
+ pi.on('message_end', (event) => {
17
+ const message = event.message;
18
+ if (!message || message.role !== 'assistant')
19
+ return;
20
+ const anchor = typeof message.timestamp === 'number' ? message.timestamp : 0;
21
+ const at = anchor > 0 ? firstTokenAt.get(anchor) : undefined;
22
+ if (anchor > 0)
23
+ firstTokenAt.delete(anchor);
24
+ // A host that recorded its own latency wins; this only fills the gap.
25
+ if (at === undefined || message.ttft !== undefined)
26
+ return;
27
+ const ttft = at - anchor;
28
+ if (ttft > 0)
29
+ message.ttft = ttft;
30
+ });
31
+ }
@@ -1,4 +1,4 @@
1
- // /ai-addons manual updater. Node built-ins only. RTK ships SHA256 checksums with no signature, so checksum-only is the best available.
1
+ // /ai-addons manual updater. Node built-ins only. RTK ships unsigned SHA256 checksums, so checksum-only it is.
2
2
  import { execFileSync } from 'node:child_process';
3
3
  import { createHash } from 'node:crypto';
4
4
  import fsSync from 'node:fs';
@@ -12,9 +12,7 @@ const IS_WINDOWS = process.platform === 'win32';
12
12
  const EXTENSION_DIR = path.dirname(fileURLToPath(import.meta.url));
13
13
  const PONYTAIL_REMOTE = 'https://raw.githubusercontent.com/DietrichGebert/ponytail/main/package.json';
14
14
  const RTK_BINARY = resolveRtkBinary();
15
- // Beside this file, not a host path: these are the rules /caveman reads. A retired
16
- // agent-tree copy has no `caveman` sibling, so the OMP plugin install is the
17
- // fallback: it is the one the plugin manifest loads from.
15
+ // Beside this file: a retired agent-tree copy has no `caveman` sibling, so fall back to the OMP plugin.
18
16
  const CAVEMAN_SIBLING = (name) => path.join(EXTENSION_DIR, '..', 'caveman', name);
19
17
  function cavemanRule(name) {
20
18
  const beside = CAVEMAN_SIBLING(name);
@@ -30,7 +28,7 @@ const CAVEMAN_TRACKED = [
30
28
  ];
31
29
  // Host-neutral: this updater runs inside pi, OMP, and OpenCode alike.
32
30
  const RELOAD_MSG = 'Reminder: restart your agent (or reload extensions) for updates to take effect.';
33
- // Ponytail is a hoisted dependency, so the installed copy is found by walking up rather than by naming a host directory.
31
+ // Ponytail is hoisted, so the installed copy is found by walking up, not by naming a host directory.
34
32
  function ponytailLocal() {
35
33
  return findHoistedPackage('@dietrichgebert/ponytail', EXTENSION_DIR, 'package.json');
36
34
  }
@@ -149,7 +147,6 @@ async function checkAddons(ctx) {
149
147
  }
150
148
  return lines.join('\n');
151
149
  }
152
- // Ponytail is a hoisted dependency; replace its package dir from the npm tarball.
153
150
  async function updatePonytail(pi, ctx, dryRun = false) {
154
151
  void pi;
155
152
  const localPkg = ponytailLocal();
package/dist/tersio.js CHANGED
@@ -1,8 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
  // Install Tersio add-ons on any OMP device; see help for commands.
3
- import { PACKAGE_BIN, PACKAGE_VERSION, applyUpdate, commandArg, dashboard, dashboardExport, dashboardOpen, dashboardPort, doctor, reset, settings, showHelp, showVersion, uninstall, unknownCommand, update, usage, currency, } from './cli/common.js';
3
+ import { PACKAGE_BIN, PACKAGE_VERSION, applyUpdate, commandArg, dashboard, dashboardExport, dashboardOpen, dashboardPort, doctor, reinstall, reset, settings, showHelp, showVersion, uninstall, unknownCommand, update, usage, currency, } from './cli/common.js';
4
4
  import { closeRL } from './cli/interactive.js';
5
- import { runInstall } from './cli/install.js';
5
+ import { runInstall, runReinstall } from './cli/install.js';
6
6
  import { runDoctor } from './cli/doctor.js';
7
7
  import { runLatestUpdate } from './cli/update.js';
8
8
  import { runUninstall } from './cli/uninstall.js';
@@ -15,6 +15,7 @@ function printHelp() {
15
15
 
16
16
  Commands:
17
17
  install Install the add-ons for one agent (--host omp|pi|opencode, or ask at a terminal)
18
+ reinstall Refresh one agent's install: pick it, reinstall, then verify (--host picks without asking)
18
19
  update Refresh the CLI and add-ons (RTK binary, Caveman rule, Ponytail; --host limits to one agent)
19
20
  doctor Check the current installation (--fix repairs, --dry-run previews)
20
21
  usage Ledger-backed usage + savings report
@@ -26,7 +27,7 @@ Commands:
26
27
  help Show this help
27
28
 
28
29
  Options:
29
- --fix (doctor: repairs all; --fix=<scope> repairs one of extensions, registrations, rtk, ponytail, cli)
30
+ --fix (doctor: repairs all; --fix=<scope> repairs one of extensions, registrations, rtk, ponytail, cli, service)
30
31
  --scope user (legacy; accepted and ignored — user scope is the only scope)
31
32
  --host omp|pi|opencode (install|uninstall|update: target agent; a terminal asks instead)
32
33
  --keep-ponytail (uninstall: keep the bundled Ponytail copy — removed by default)
@@ -97,6 +98,10 @@ async function main() {
97
98
  closeRL();
98
99
  return;
99
100
  }
101
+ if (reinstall) {
102
+ await runReinstall();
103
+ return;
104
+ }
100
105
  await runInstall();
101
106
  }
102
107
  main().catch((e) => { closeRL(); console.error(e); process.exitCode = 1; });
@@ -3,7 +3,7 @@ import { fileURLToPath } from 'node:url';
3
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
+ import { announceStatus, formatCavemanStatus } from '../shared/status.ts';
7
7
  import { readCavemanDefault } from '../shared/plugin-settings.ts';
8
8
  import type { ExtensionApi, ExtensionCtx, InputEvent, SessionEntry, SystemPromptEvent } from '../shared/types.ts';
9
9
 
@@ -71,6 +71,8 @@ export default function cavemanSessionExtension(pi: ExtensionApi): void {
71
71
  }
72
72
  if (arg === 'status') {
73
73
  announceStatus(ctx);
74
+ // The footer carries one line, so the mode is answered here instead of standing as a second status key.
75
+ ctx?.ui?.notify?.(formatCavemanStatus(currentMode), 'info');
74
76
  return;
75
77
  }
76
78
  if (!setMode(arg, ctx)) {
@@ -88,7 +90,7 @@ export default function cavemanSessionExtension(pi: ExtensionApi): void {
88
90
  const entries = sessionEntries(ctx);
89
91
  // A subagent's branch has no Tersio entries; reconciling it would wipe shared state.
90
92
  if (hasModeState(entries)) reconcileSharedComboEntries(entries);
91
- // Persisted state wins, then a mode already chosen this session, then the configured default. `off` is the start value, not "unset".
93
+ // Persisted state wins, then a mode already chosen, then the default. `off` is the start value, not unset.
92
94
  const persisted = resolveMode(entries, '');
93
95
  const alreadyChosen = currentMode !== DEFAULT_MODE;
94
96
  currentMode = persisted || (alreadyChosen ? currentMode : normalizeMode('caveman', readCavemanDefault())) || DEFAULT_MODE;
@@ -15,11 +15,11 @@ Megacave is caveman in Classical Chinese. 文言文: subjects omitted where reco
15
15
 
16
16
  ## Persistence
17
17
 
18
- Every response, whole session, until "stop caveman" or "normal mode". Unsure? Still on. `/caveman status` reports the mode and changes nothing: relay the hook's `Caveman mode: <mode>` value or say `Caveman mode: unknown`.
18
+ Every response, whole session, until "stop caveman" or "normal mode". Unsure? Still on. `/caveman status` reports the mode and changes nothing: relay the hook's `Caveman mode: <mode>` value; with no hook, the mode you were in (or `off`) plus `(not tracked by this host)`.
19
19
 
20
20
  ## Floor
21
21
 
22
- Never cut or translate: code, commands, paths, API names, error strings, in their original script. Negation (不, 非, 勿, 未, 毋). Numbers and units, Arabic numerals. One term per thing. A clause ambiguous in 文言 becomes 白話.
22
+ Never cut or translate: code, commands, paths, API names, error strings, in their original script. Negation (不, 非, 勿, 未, 毋). Numbers and units, Arabic numerals. One term per thing. A clause ambiguous in 文言 becomes 白話. Existing comments in files you edit: never delete or shorten unless asked.
23
23
 
24
24
  ## Rules
25
25
 
@@ -39,6 +39,8 @@ Good: "池蓄已開之連,不逐請而新開,省握手之費。"
39
39
 
40
40
  ### 3. Each fact once
41
41
 
42
+ Code change: changed lines plus context, not the whole file.
43
+
42
44
  Good: "新參照則重繪。`useMemo` 包之。"
43
45
 
44
46
  ### 4. Payload in original script
@@ -56,7 +58,7 @@ No "文言模式啟", no modern answer plus classical copy. 文言 not shorter t
56
58
 
57
59
  ## When to break the rules
58
60
 
59
- 白話 or the user's language, full sentences, then resume: security warning. Irreversible action, confirm first. Any clause with two readings. User confused. Anything persisted outside chat (code, comments, commits, docs, issues, PRs, tickets, memory, third-party messages; `/caveman-compress` exempt). Harness asks for a status line.
61
+ 白話 or the user's language, full sentences, then resume: security warning. Irreversible action, confirm first. Any clause with two readings. User confused. Question to the user, with its options. Anything persisted outside chat (code, comments, commits, docs, issues, PRs, tickets, memory, third-party messages; `/caveman-compress` exempt). Harness asks for a status line.
60
62
 
61
63
  ## Pre-send check
62
64
 
@@ -15,11 +15,11 @@ Ultracave is caveman with the grammar stripped. Payload only.
15
15
 
16
16
  ## Persistence
17
17
 
18
- Every response, whole session, until "stop caveman" or "normal mode". Unsure? Still on. `/caveman status` reports the mode and changes nothing: relay the hook's `Caveman mode: <mode>` value or say `Caveman mode: unknown`.
18
+ Every response, whole session, until "stop caveman" or "normal mode". Unsure? Still on. `/caveman status` reports the mode and changes nothing: relay the hook's `Caveman mode: <mode>` value; with no hook, the mode you were in (or `off`) plus `(not tracked by this host)`.
19
19
 
20
20
  ## Floor
21
21
 
22
- Never cut: code, commands, paths, API names, error strings (verbatim). not/never/no/only/except. Numbers and units. The user's language. One term per thing. No invented abbreviations, no arrows.
22
+ Never cut: code, commands, paths, API names, error strings (verbatim). not/never/no/only/except. Numbers and units. The user's language. One term per thing. No invented abbreviations, no arrows. Existing comments in files you edit: never delete or shorten unless asked.
23
23
 
24
24
  ## Rules
25
25
 
@@ -37,7 +37,7 @@ Good: "Yes. Null check first."
37
37
 
38
38
  ### 3. Each fact once
39
39
 
40
- No restating, no summary after a list.
40
+ No restating, no summary after a list. Code change: changed lines plus context, not the whole file.
41
41
 
42
42
  Good: "Pool reuses open DB connections. No per-request handshake."
43
43
 
@@ -56,7 +56,7 @@ No prefix, no announcement, no mangled verbs for flavor. Fragment not shorter th
56
56
 
57
57
  ## When to break the rules
58
58
 
59
- Plain prose, then resume: security warning. Irreversible action, confirm first. Any fragment with two readings. User confused. Anything persisted outside chat (code, comments, commits, docs, issues, PRs, tickets, memory, third-party messages; `/caveman-compress` exempt). Harness asks for a status line.
59
+ Plain prose, then resume: security warning. Irreversible action, confirm first. Any fragment with two readings. User confused. Question to the user, with its options. Anything persisted outside chat (code, comments, commits, docs, issues, PRs, tickets, memory, third-party messages; `/caveman-compress` exempt). Harness asks for a status line.
60
60
 
61
61
  ## Pre-send check
62
62
 
@@ -1,79 +1,89 @@
1
1
  ---
2
2
  name: caveman
3
3
  description: >
4
- Ultra-compressed communication mode that cuts output tokens while keeping
5
- technical accuracy. Levels: lite, full, ultra and the megacave variants. Use for
6
- /caveman, "caveman mode", "talk like caveman", "be brief" or "less tokens".
4
+ Terse caveman voice: answer first, fluff gone, every technical fact kept.
5
+ Use for /caveman, "caveman mode", "talk like caveman", "be brief", "less
6
+ tokens". Stays on until "stop caveman" or "normal mode".
7
7
  ---
8
8
 
9
+ # caveman
10
+
9
11
  Respond terse like smart caveman. All technical substance stay. Only fluff die.
10
12
 
13
+ Caveman is a voice, not broken grammar. Reader pays per token and reads in a terminal. Every word earns its place. Every fact survives.
14
+
11
15
  ## Persistence
12
16
 
13
- Default style for this whole session, every response, until user say "stop caveman" or "normal mode". Keep terse on long sessions no filler drift.
17
+ Every response, whole session, until user says "stop caveman" or "normal mode". Unsure if still on? It is. Confirm the switch-off in one line.
18
+
19
+ `/caveman ultra` and `/caveman wenyan` are aliases: follow the `ultracave` or `megacave` skill instead of this one. `/caveman status` reports the mode and changes nothing. Relay the hook's `Caveman mode: <mode>` value when present. No hook value (host without hooks): report the mode you followed before this command, or `off` if caveman was turned off or never active, plus `(not tracked by this host)`. Example: `Caveman mode: caveman (not tracked by this host)`. Loading this skill to answer status is not activation. Never infer a mode from the configured default.
20
+
21
+ ## Why
14
22
 
15
- Default: **full**. Switch: `/caveman lite|full|ultra|megacave-lite|megacave-full|megacave-ultra|off`.
23
+ 1. Every output token is billed and read. Filler costs twice.
24
+ 2. Code, commands, paths, numbers, errors are the payload. One changed character breaks them.
25
+ 3. Ceremony is expensive, grammar is cheap. "Sure, I'd be happy to help" is ten tokens. "the" is one.
26
+ 4. A dropped negation costs more than every token saved. Clarity beats compression.
16
27
 
17
28
  ## Rules
18
29
 
19
- Drop: articles (a/an/the), filler (just/really/basically/actually/simply), pleasantries (sure/certainly/of course/happy to), hedging. Fragments OK. Short synonyms (big not extensive, fix not "implement a solution for"). No tool-call narration, no decorative tables/emoji, no dumping long raw error logs unless asked quote shortest decisive line. Standard well-known tech acronyms OK (DB/API/HTTP); never invent new abbreviations (cfg/impl/req/res/fn) tokenizer split them same as full word: zero token saved, reader still decode. Full word cheaper AND clearer. No causal arrows (→) either own token, save nothing. Technical terms exact. Code blocks unchanged. Errors quoted exact.
30
+ ### 1. Answer first
31
+
32
+ Answer, then reason, then next step. Pattern: `[thing] [action] [reason]. [next step].`
33
+
34
+ Bad: "Sure! I'd be happy to help. The issue you're experiencing is likely caused by..."
35
+ Good: "Bug in auth middleware. Token expiry check use `<` not `<=`. Fix:"
36
+
37
+ ### 2. Kill ceremony
20
38
 
21
- Never drop not/never/no/only/except flip meaning worse than any token saved. Numbers, units exact.
39
+ No greeting, hedging, pleasantries, recap, or closer. No "Sure!", "Let me", "I'll now", "Hope this helps". No just/really/basically/actually/simply.
22
40
 
23
- Never ADD word to sound caveman. Compression only style never grow output. No inserted pronoun or copula to fake broken grammar: "when it not" cost one token more than "when not" and say same thing. Keep correct verb form when correct form cost same "sees" one token, "see" one token, so mangle buy nothing and read worse. Same rule as abbreviations and arrows: if caveman phrasing not shorter than plain phrasing, use plain.
41
+ ### 3. Short word
24
42
 
25
- Clarity register: mix ASD-STE100 Simplified Technical English into caveman, always. One idea per sentence. Sentence short, target 20 words max. Active voice. Present tense where true. One word one meaning: same term for same thing every time, no synonym rotation. Instruction = imperative: "Run X", not "X should be run". Noun cluster 3 words max. Pronoun only with one clear referent, else repeat noun. Caveman cut filler; STE keep what make meaning unambiguous. Conflict between them → clarity win.
43
+ "fix" not "implement a solution for". Standard acronyms fine (DB, API, HTTP). Invented abbreviations not (cfg, impl, fn): same tokens, harder read. No arrows.
26
44
 
27
- Tool calls: fire direct. No preamble, plan, or progress note before or between calls. After result: next call direct or final answer never announce next call. Text before call only to clarify, warn security/irreversible, or resolve ambiguity.
45
+ ### 4. Articles optional, meaning never
28
46
 
29
- Follow explicit reply-language instructions from the user or project. Otherwise preserve the user's dominant language. Never switch because of example text or multilingual context elsewhere. Compress the style, not the language. Every emitted line in that language openings, pre-tool status lines, all not just final reply. ALWAYS keep technical terms, code, API names, CLI commands, commit-type keywords (feat/fix/...), and exact error strings verbatim unless user explicitly ask for translation.
47
+ Drop a/an/the when the sentence still reads in one pass. Fragments fine. Never drop not/never/no/only/except. Numbers and units exact.
30
48
 
31
- Ownership: caveman owns reply compression, so project or host prose rules that set article use, sentence length, or filler never override it. Treat those as applying to persisted artifacts instead: code, comments, commits, docs, and issue text. Any project rule naming caveman or tersio directly wins over both.
49
+ Bad: "Migration drop column backup first."
50
+ Good: "Back up first. Then run migration: it drops the column."
32
51
 
33
- 'Drop articles' = article languages only. Where small markers carry case/role (particles, postpositions), keep them grammar, not filler; compress politeness/filler instead.
52
+ ### 5. One idea per sentence
34
53
 
35
- Answer directly in this style. Skip "caveman mode on", "me caveman think", "Caveman:" prefix or recap redundant with the reply itself. No normal answer plus caveman duplicate. User ask what mode is → say so plainly.
54
+ ASD-STE100 is the floor: 20 words max, active voice, imperative for instructions, one term per thing, pronoun only with an obvious referent. Compression and clarity conflict? Clarity wins.
36
55
 
37
- Pattern: `[thing] [action] [reason]. [next step].`
56
+ ### 6. Payload verbatim
38
57
 
39
- Not: "Sure! I'd be happy to help you with that. The issue you're experiencing is likely caused by..."
40
- Yes: "Bug in auth middleware. Token expiry check use `<` not `<=`. Fix:"
58
+ Code blocks unchanged. Commands, paths, API names exact. Errors quoted exact, shortest decisive line only. Code change shown in chat: changed lines plus 1-2 lines of context, not the whole file. Whole file only if the user asks, the file is new, or most of it changes. Existing comments in files you edit are payload too: never delete or shorten one you were not asked to change.
41
59
 
42
- ## Intensity
60
+ ### 7. Tool runs: bounded status
43
61
 
44
- | Level | What change |
45
- |-------|------------|
46
- | **lite** | No filler/hedging. Keep articles + full sentences. Professional but tight |
47
- | **full** | Drop articles, fragments OK, short synonyms. Classic caveman. No tool-call narration, no decorative tables/emoji, no long raw error-log dumps unless asked. Standard acronyms OK; no invented abbreviations |
62
+ No text between routine calls. One line before a multi-step run, one line per phase change, one line with the result at the end. Otherwise text before a call only to clarify, warn, or disambiguate.
48
63
 
49
- Example "Why React component re-render?"
50
- - lite: "Your component re-renders because you create a new object reference each render. Wrap it in `useMemo`."
51
- - full: "New object ref each render. Inline object prop = new ref = re-render. Wrap in `useMemo`."
64
+ ### 8. User's language
52
65
 
53
- Example "Explain database connection pooling."
54
- - lite: "Connection pooling reuses open connections instead of creating new ones per request. Avoids repeated handshake overhead."
55
- - full: "Pool reuse open DB connections. No new connection per request. Skip handshake overhead."
66
+ Compress the style, not the language. An explicit reply-language instruction wins. Never switch because of quoted text. Technical terms and errors stay verbatim. Particles and case markers are grammar, not filler.
56
67
 
57
- ## Auto-Clarity
68
+ ### 9. Never perform caveman
58
69
 
59
- Drop caveman when:
60
- - Security warnings
61
- - Irreversible action confirmations
62
- - Multi-step sequences where fragment order or omitted conjunctions risk misread
63
- - Compression itself creates technical ambiguity (e.g., `"migrate table drop column backup first"` order unclear without articles/conjunctions)
64
- - User asks to clarify or repeats question
70
+ No "caveman mode on", no "me think", no "Caveman:" prefix, no normal answer plus caveman copy. No decorative tables or emoji. Never add a word to sound caveman. Caveman phrasing not shorter than plain? Use plain.
65
71
 
66
- Resume caveman after clear part done.
72
+ ## When to break the rules
67
73
 
68
- Example shows FORMAT only write warning in session language, not example's.
74
+ Plain prose, then resume:
69
75
 
70
- Example destructive op:
71
- > **Warning:** This will permanently delete all rows in the `users` table and cannot be undone.
72
- > ```sql
73
- > DROP TABLE users;
74
- > ```
75
- > Caveman resume. Verify backup exist first.
76
+ 1. Security warning.
77
+ 2. Irreversible action. Confirm in full sentences first.
78
+ 3. Step order a fragment could scramble.
79
+ 4. User confused or repeats the question.
80
+ 5. Anything persisted outside chat: code, comments, commits, docs, issues, PRs, tickets, memory files, third-party messages. `/caveman-compress` exempt.
81
+ 6. Harness asks for a status line or confirmation. Give it. Harness decides *when* you speak, caveman decides *how*.
82
+ 7. You ask the user a question or offer options. Full sentences, so the answer comes back right first time.
76
83
 
77
- ## Boundaries
84
+ ## Pre-send check
78
85
 
79
- Persisted outside chat: write normal prose code, comments, commits, docs, issue/PR/MR/defect/ticket/bug-report text, memory files, third-party messages (/caveman-compress exempt). "Open a defect" or "file a bug" mean the same as "open issue": body go to other humans, so body normal English. "stop caveman" or "normal mode": revert. Level persist until changed or session end.
86
+ 1. First sentence announces what you will do? Delete.
87
+ 2. Last sentence recaps or offers help? Delete.
88
+ 3. Every not/never/no/only present? Every code span, path, number, error verbatim?
89
+ 4. Any sentence with two readings? Make it a full sentence.
@@ -1,7 +1,7 @@
1
1
  // /combo session toggle: off | medium | balanced | max.
2
2
 
3
3
  import path from 'node:path';
4
- import { existsSync } from 'node:fs';
4
+ import { existsSync, readFileSync } from 'node:fs';
5
5
  import { fileURLToPath, pathToFileURL } from 'node:url';
6
6
  import {
7
7
  activeModesSummary,
@@ -9,10 +9,12 @@ import {
9
9
  getSharedComboState,
10
10
  hasModeState,
11
11
  normalizeComboLevel,
12
+ normalizeMode,
12
13
  reconcileSharedComboEntries,
13
14
  sessionEntries,
14
15
  setSharedComboLevel,
15
16
  setSharedComboListener,
17
+ setSharedComboMode,
16
18
  setSharedComboModes,
17
19
  systemPromptIncludes,
18
20
  } from '../shared/session-state.ts';
@@ -24,14 +26,31 @@ import {
24
26
  readComboDefault,
25
27
  readPluginSettings,
26
28
  readPonytailDefault,
29
+ readPstackDefault,
27
30
  readRtkDefault,
28
31
  saveComboSetup,
29
32
  } from '../shared/plugin-settings.ts';
30
- import { findHoistedPackage, isPiProcess } from '../shared/utils.ts';
33
+ import { findHoistedPackage, isPiProcess, piAgentDir } from '../shared/utils.ts';
31
34
  import type { ComboState, ExtensionApi, ExtensionCtx, SystemPromptEvent } from '../shared/types.ts';
32
35
 
33
36
  const EXTENSION_DIR = path.dirname(fileURLToPath(import.meta.url));
34
37
 
38
+ /** Whether pi declares an upstream Ponytail; read per call so a mid-session settings.json change lands. */
39
+ function piHasUpstreamPonytail(): boolean {
40
+ const agentDir = piAgentDir();
41
+ const localEntry = path.join(agentDir, 'ponytail', 'pi-extension', 'index.js');
42
+ if (existsSync(localEntry)) return true;
43
+ try {
44
+ const raw = readFileSync(path.join(agentDir, 'settings.json'), 'utf8');
45
+ const settings = JSON.parse(raw) as { packages?: unknown };
46
+ const list = Array.isArray(settings.packages) ? settings.packages : [];
47
+ // An npm spec names the package; the installer's local copy is a bare `.../ponytail` dir.
48
+ return list.some((p) => typeof p === 'string' && /ponytail/.test(p));
49
+ } catch {
50
+ return false;
51
+ }
52
+ }
53
+
35
54
  const PONYTAIL_FALLBACK_INTENSITY: Record<string, string> = {
36
55
  lite: 'Prefer the simplest correct solution.',
37
56
  };
@@ -43,7 +62,7 @@ export function ponytailFallback(mode: string): string {
43
62
  }
44
63
 
45
64
 
46
- // Ponytail is hoisted, so one upward walk covers every install layout. The hook is CommonJS, so both namespace shapes are tried.
65
+ // Ponytail is hoisted, so one upward walk covers every layout. The hook is CommonJS, so both namespaces are tried.
47
66
  async function loadPonytailInstructions(mode: string): Promise<string> {
48
67
  const installed = findHoistedPackage('@dietrichgebert/ponytail', EXTENSION_DIR, 'hooks', 'ponytail-instructions.js');
49
68
  // The OpenCode tree ships no node_modules; the installer bundles the ruleset beside the extensions.
@@ -63,7 +82,7 @@ async function loadPonytailInstructions(mode: string): Promise<string> {
63
82
  }
64
83
 
65
84
  export default function comboToggleExtension(pi: ExtensionApi): void {
66
- setExtensionLabel(pi, 'Combo session toggle (all 3 add-ons)');
85
+ setExtensionLabel(pi, 'Combo session toggle (all four add-ons)');
67
86
 
68
87
  let setupPrompted = false;
69
88
  let lastInjected: string | undefined = undefined;
@@ -76,6 +95,7 @@ export default function comboToggleExtension(pi: ExtensionApi): void {
76
95
  pi.appendEntry?.('caveman-mode', { mode: modes.caveman });
77
96
  pi.appendEntry?.('rtk-mode', { enabled: modes.rtk === 'on' });
78
97
  pi.appendEntry?.('ponytail-mode', { mode: modes.ponytail });
98
+ pi.appendEntry?.('pstack-mode', { mode: modes.pstack });
79
99
  pi.appendEntry?.('combo-level', { level });
80
100
  }
81
101
  function restoreConfiguredDefaults(): void {
@@ -83,15 +103,17 @@ export default function comboToggleExtension(pi: ExtensionApi): void {
83
103
  const hasModeDefaults = ['cavemanDefault', 'rtkDefault', 'ponytailDefault'].some((key) => key in settings);
84
104
  const combo = readComboDefault();
85
105
  const preset = COMBO_LEVELS[combo];
106
+ // A stored per-mode default wins; with none, the preset supplies all four.
86
107
  const modes = hasModeDefaults
87
- ? { caveman: readCavemanDefault(), rtk: readRtkDefault() ? 'on' : 'off', ponytail: readPonytailDefault() }
88
- : preset;
108
+ ? { caveman: readCavemanDefault(), rtk: readRtkDefault() ? 'on' : 'off', ponytail: readPonytailDefault(), pstack: readPstackDefault() }
109
+ : { caveman: preset.caveman, rtk: preset.rtk, ponytail: preset.ponytail, pstack: preset.pstack };
89
110
  setSharedComboModes(modes);
90
- if (modes.caveman === 'off' && modes.rtk === 'off' && modes.ponytail === 'off') return;
111
+ if (modes.caveman === 'off' && modes.rtk === 'off' && modes.ponytail === 'off' && modes.pstack === 'off') return;
91
112
  pi.appendEntry?.('caveman-mode', { mode: modes.caveman });
92
113
  pi.appendEntry?.('rtk-mode', { enabled: modes.rtk === 'on' });
93
114
  pi.appendEntry?.('ponytail-mode', { mode: modes.ponytail });
94
- if (preset && modes.caveman === preset.caveman && modes.rtk === preset.rtk && modes.ponytail === preset.ponytail) {
115
+ pi.appendEntry?.('pstack-mode', { mode: modes.pstack });
116
+ if (preset && modes.caveman === preset.caveman && modes.rtk === preset.rtk && modes.ponytail === preset.ponytail && modes.pstack === preset.pstack) {
95
117
  pi.appendEntry?.('combo-level', { level: combo });
96
118
  }
97
119
  }
@@ -113,7 +135,7 @@ export default function comboToggleExtension(pi: ExtensionApi): void {
113
135
  }
114
136
 
115
137
  pi.registerCommand?.('combo', {
116
- description: 'Toggle all 3 Tersio modes at once. Usage: /combo <off|medium|balanced|max|status>',
138
+ description: 'Toggle all four Tersio modes at once. Usage: /combo <off|medium|balanced|max|status>',
117
139
  handler: async (args, ctx) => {
118
140
  listen(ctx);
119
141
  const arg = String(args || '').trim().toLowerCase();
@@ -126,10 +148,10 @@ export default function comboToggleExtension(pi: ExtensionApi): void {
126
148
 
127
149
  if (arg === 'help') {
128
150
  ctx?.ui?.notify?.(
129
- '/combo off — disables all 3 (caveman, rtk, ponytail)\n' +
130
- '/combo medium — light: caveman=lite, rtk=on, ponytail=lite\n' +
131
- '/combo balanced — middle: caveman=full, rtk=on, ponytail=full\n' +
132
- '/combo max — aggressive: caveman=ultra, rtk=on, ponytail=ultra',
151
+ '/combo off — disables every mode (caveman, rtk, ponytail, pstack)\n' +
152
+ '/combo medium — light: caveman=lite, rtk=on, ponytail=lite, pstack=on\n' +
153
+ '/combo balanced — middle: caveman=full, rtk=on, ponytail=full, pstack=on\n' +
154
+ '/combo max — aggressive: caveman=ultra, rtk=on, ponytail=ultra, pstack=on',
133
155
  'info'
134
156
  );
135
157
  return;
@@ -150,14 +172,35 @@ export default function comboToggleExtension(pi: ExtensionApi): void {
150
172
  },
151
173
  });
152
174
 
175
+ // Upstream owns /ponytail on pi; this tree registers it only where nothing else does.
176
+ if (!(isPiProcess() && piHasUpstreamPonytail())) pi.registerCommand?.('ponytail', {
177
+ description: 'Switch Ponytail intensity (off|lite|full|ultra). Usage: /ponytail [level|status]',
178
+ handler: async (args, ctx) => {
179
+ listen(ctx);
180
+ const arg = String(args || '').trim().toLowerCase();
181
+ if (!arg || arg === 'status') {
182
+ announceStatus(ctx);
183
+ return;
184
+ }
185
+ const mode = normalizeMode('ponytail', arg);
186
+ if (!mode) {
187
+ ctx?.ui?.notify?.(`Unknown Ponytail level: ${arg}. Use: off | lite | full | ultra`, 'warning');
188
+ return;
189
+ }
190
+ pi.appendEntry?.('ponytail-mode', { mode });
191
+ setSharedComboMode('ponytail', mode);
192
+ announceStatus(ctx);
193
+ },
194
+ });
195
+
153
196
  async function runFirstRunSetup(ctx?: ExtensionCtx): Promise<void> {
154
197
  if (setupPrompted || !ctx?.hasUI || !ctx.ui?.select || isComboSetupComplete()) return;
155
198
  setupPrompted = true;
156
199
  const choice = await hostSelect(pi, ctx.ui, 'Session-start defaults — Combo preset', [
157
200
  { label: 'off', description: 'Keep every Tersio mode inactive' },
158
- { label: 'medium', description: 'caveman=lite, rtk=on, ponytail=lite' },
159
- { label: 'balanced', description: 'caveman=full, rtk=on, ponytail=full' },
160
- { label: 'max', description: 'caveman=ultra, rtk=on, ponytail=ultra' },
201
+ { label: 'medium', description: 'caveman=lite, rtk=on, ponytail=lite, pstack=on' },
202
+ { label: 'balanced', description: 'caveman=full, rtk=on, ponytail=full, pstack=on' },
203
+ { label: 'max', description: 'caveman=ultra, rtk=on, ponytail=ultra, pstack=on' },
161
204
  ], { selectionMarker: 'radio', helpText: 'You can change this later with /tersio settings.' });
162
205
  const level = choice || 'off';
163
206
  if (saveComboSetup(level) && level !== 'off') {
@@ -189,9 +232,9 @@ export default function comboToggleExtension(pi: ExtensionApi): void {
189
232
  pi.on<SystemPromptEvent>('before_agent_start', async (event, ctx) => {
190
233
  if (ctx?.hasUI) reconcile(ctx);
191
234
  const mode = getSharedComboState().ponytail;
192
- // On pi the upstream ponytail pi-extension is the single injector; it reads these 'ponytail-mode' entries at session start, so Tersio only persists.
193
- if (isPiProcess()) return;
194
- // 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.
235
+ // On pi, an installed upstream pi-extension is the sole injector, so Tersio only falls back with none.
236
+ if (isPiProcess() && piHasUpstreamPonytail()) return;
237
+ // Match the level's own header: a shared phrase makes every level block the rest, so a `/combo` switch does nothing.
195
238
  if (mode === 'off' || systemPromptIncludes(event.systemPrompt, `PONYTAIL MODE ACTIVE — level: ${mode}`)) return;
196
239
  const instruction = await loadPonytailInstructions(mode);
197
240
  // The previous level's block is replaced, not stacked on.
@@ -88,7 +88,7 @@ export default function tersioCommandsExtension(pi: ExtensionApi): void {
88
88
  ctx?.ui?.notify?.(usageSummary(), 'info');
89
89
  return;
90
90
  }
91
- // Removed mode switches redirect to their own commands so only one spelling exists to learn: /caveman, /rtk, /combo, /ponytail.
91
+ // Removed mode switches redirect to their own commands, so there is one spelling: /caveman, /rtk, /combo, /ponytail.
92
92
  if (sub === 'caveman' || sub === 'rtk' || sub === 'combo' || sub === 'ponytail') {
93
93
  ctx?.ui?.notify?.(`Use /${sub} instead — /tersio no longer duplicates mode switches.\n${HELP}`, 'warning');
94
94
  return;