@krtclcdy/tersio 2.25.6 → 2.25.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (149) hide show
  1. package/README.md +43 -13
  2. package/dashboard/dist/index.html +1 -1
  3. package/dist/cli/banner.js +1 -2
  4. package/dist/cli/common.js +26 -4
  5. package/dist/cli/dashboard.js +7 -2
  6. package/dist/cli/doctor-fix.js +12 -7
  7. package/dist/cli/doctor.js +64 -15
  8. package/dist/cli/hosts.js +1 -2
  9. package/dist/cli/install.js +12 -7
  10. package/dist/cli/manifest.js +25 -5
  11. package/dist/cli/profile.js +28 -3
  12. package/dist/cli/rtk-wiring.js +13 -10
  13. package/dist/cli/uninstall.js +1 -1
  14. package/dist/cli/usage.js +5 -0
  15. package/dist/extensions/caveman/index.js +4 -3
  16. package/dist/extensions/combo/index.js +21 -7
  17. package/dist/extensions/commands/index.js +7 -2
  18. package/dist/extensions/pstack/index.js +5 -2
  19. package/dist/extensions/rtk/filter.js +45 -4
  20. package/dist/extensions/rtk/session.js +21 -11
  21. package/dist/extensions/server.js +44 -29
  22. package/dist/extensions/shared/agent-stats.js +21 -13
  23. package/dist/extensions/shared/pricing.js +5 -4
  24. package/dist/extensions/shared/rtk-eligible.js +80 -0
  25. package/dist/extensions/shared/rtk-gain.js +3 -7
  26. package/dist/extensions/shared/sqlite.js +2 -13
  27. package/dist/extensions/shared/status.js +1 -2
  28. package/dist/extensions/shared/usage-ledger.js +196 -29
  29. package/dist/extensions/shared/utils.js +33 -0
  30. package/dist/extensions/updater/index.js +15 -7
  31. package/extensions/caveman/index.ts +4 -3
  32. package/extensions/caveman/rule-lite.md +77 -0
  33. package/extensions/caveman/rule-megacave-lite.md +59 -0
  34. package/extensions/caveman/rule-megacave-ultra.md +59 -0
  35. package/extensions/caveman/rule.md +1 -1
  36. package/extensions/combo/index.ts +22 -7
  37. package/extensions/commands/index.ts +7 -2
  38. package/extensions/pstack/index.ts +5 -2
  39. package/extensions/pstack/playbooks/authoring-a-skill.md +12 -0
  40. package/extensions/pstack/playbooks/autonomous-run.md +13 -0
  41. package/extensions/pstack/playbooks/autopilot-full.md +13 -0
  42. package/extensions/pstack/playbooks/autopilot-stack.md +16 -0
  43. package/extensions/pstack/playbooks/babysit.md +23 -0
  44. package/extensions/pstack/playbooks/bug-fix.md +15 -0
  45. package/extensions/pstack/playbooks/eval.md +25 -0
  46. package/extensions/pstack/playbooks/feature.md +21 -0
  47. package/extensions/pstack/playbooks/hillclimb.md +21 -0
  48. package/extensions/pstack/playbooks/investigation.md +14 -0
  49. package/extensions/pstack/playbooks/multi-phase-plan.md +155 -0
  50. package/extensions/pstack/playbooks/opening-a-pr.md +34 -0
  51. package/extensions/pstack/playbooks/orchestrate.md +111 -0
  52. package/extensions/pstack/playbooks/pause-safely.md +10 -0
  53. package/extensions/pstack/playbooks/perf-issue.md +25 -0
  54. package/extensions/pstack/playbooks/prototype.md +14 -0
  55. package/extensions/pstack/playbooks/refactoring.md +16 -0
  56. package/extensions/pstack/playbooks/runtime-forensics.md +11 -0
  57. package/extensions/pstack/playbooks/session-pickup.md +11 -0
  58. package/extensions/pstack/playbooks/shipping.md +17 -0
  59. package/extensions/pstack/playbooks/trace-forensics.md +14 -0
  60. package/extensions/pstack/playbooks/visual-parity.md +11 -0
  61. package/extensions/pstack/playbooks/worktree-cleanup.md +12 -0
  62. package/extensions/pstack/principles/attack-the-premise.md +22 -0
  63. package/extensions/pstack/principles/boundary-discipline.md +33 -0
  64. package/extensions/pstack/principles/build-the-lever.md +22 -0
  65. package/extensions/pstack/principles/encode-lessons-in-structure.md +10 -0
  66. package/extensions/pstack/principles/exhaust-the-design-space.md +8 -0
  67. package/extensions/pstack/principles/experience-first.md +18 -0
  68. package/extensions/pstack/principles/explain-the-number.md +16 -0
  69. package/extensions/pstack/principles/fix-root-causes.md +23 -0
  70. package/extensions/pstack/principles/foundational-thinking.md +20 -0
  71. package/extensions/pstack/principles/guard-the-context-window.md +11 -0
  72. package/extensions/pstack/principles/laziness-protocol.md +9 -0
  73. package/extensions/pstack/principles/make-operations-idempotent.md +23 -0
  74. package/extensions/pstack/principles/migrate-callers-then-delete-legacy-apis.md +21 -0
  75. package/extensions/pstack/principles/minimize-reader-load.md +17 -0
  76. package/extensions/pstack/principles/model-the-domain.md +23 -0
  77. package/extensions/pstack/principles/never-block-on-the-human.md +14 -0
  78. package/extensions/pstack/principles/outcome-oriented-execution.md +20 -0
  79. package/extensions/pstack/principles/prove-it-works.md +21 -0
  80. package/extensions/pstack/principles/redesign-from-first-principles.md +15 -0
  81. package/extensions/pstack/principles/separate-before-serializing-shared-state.md +15 -0
  82. package/extensions/pstack/principles/sequence-verifiable-units.md +16 -0
  83. package/extensions/pstack/principles/subtract-before-you-add.md +17 -0
  84. package/extensions/pstack/principles/test-behavior-not-implementation.md +20 -0
  85. package/extensions/pstack/principles/type-system-discipline.md +17 -0
  86. package/extensions/pstack/references/bugbot-triage.md +142 -0
  87. package/extensions/pstack/rule.md +88 -12
  88. package/extensions/pstack/skills/architect/SKILL.md +78 -0
  89. package/extensions/pstack/skills/architect/references/design-red-flags.md +57 -0
  90. package/extensions/pstack/skills/architect/references/rationale-template.md +35 -0
  91. package/extensions/pstack/skills/architect/references/runner-prompt.md +20 -0
  92. package/extensions/pstack/skills/arena/SKILL.md +47 -0
  93. package/extensions/pstack/skills/benchmark-checklist/SKILL.md +38 -0
  94. package/extensions/pstack/skills/blast-radius/SKILL.md +49 -0
  95. package/extensions/pstack/skills/bro/SKILL.md +6 -0
  96. package/extensions/pstack/skills/correct/SKILL.md +31 -0
  97. package/extensions/pstack/skills/figure-it-out/SKILL.md +51 -0
  98. package/extensions/pstack/skills/how/SKILL.md +35 -0
  99. package/extensions/pstack/skills/how/references/explainer-prompt.md +55 -0
  100. package/extensions/pstack/skills/how/references/explorer-prompt.md +35 -0
  101. package/extensions/pstack/skills/interrogate/SKILL.md +73 -0
  102. package/extensions/pstack/skills/interrogate/references/code-quality-review.md +47 -0
  103. package/extensions/pstack/skills/interrogate/references/lead-judgment.md +58 -0
  104. package/extensions/pstack/skills/interrogate/references/reviewer-prompt.md +62 -0
  105. package/extensions/pstack/skills/interrogate/references/rubric.md +77 -0
  106. package/extensions/pstack/skills/no-comments/SKILL.md +23 -0
  107. package/extensions/pstack/skills/reflect/SKILL.md +66 -0
  108. package/extensions/pstack/skills/reflect/references/divergent-reviewer.md +38 -0
  109. package/extensions/pstack/skills/reflect/references/judgment-reviewer.md +37 -0
  110. package/extensions/pstack/skills/reflect/references/synthesizer.md +56 -0
  111. package/extensions/pstack/skills/reflect/references/tooling-reviewer.md +48 -0
  112. package/extensions/pstack/skills/show-me-your-work/SKILL.md +81 -0
  113. package/extensions/pstack/skills/swarm/SKILL.md +45 -0
  114. package/extensions/pstack/skills/tdd/SKILL.md +41 -0
  115. package/extensions/pstack/skills/technical-writing/SKILL.md +105 -0
  116. package/extensions/pstack/skills/unslop/SKILL.md +61 -0
  117. package/extensions/pstack/skills/why/SKILL.md +97 -0
  118. package/extensions/pstack/skills/why/references/epistemics.md +71 -0
  119. package/extensions/pstack/skills/why/references/investigator-prompt.md +84 -0
  120. package/extensions/pstack/skills/why/references/source-playbook.md +17 -0
  121. package/extensions/pstack/skills/why/references/sources/code-archaeology.md +51 -0
  122. package/extensions/pstack/skills/why/references/sources/databricks.md +70 -0
  123. package/extensions/pstack/skills/why/references/sources/datadog.md +97 -0
  124. package/extensions/pstack/skills/why/references/sources/incident-postmortem.md +5 -0
  125. package/extensions/pstack/skills/why/references/sources/linear.md +48 -0
  126. package/extensions/pstack/skills/why/references/sources/notion.md +55 -0
  127. package/extensions/pstack/skills/why/references/sources/sentry.md +66 -0
  128. package/extensions/pstack/skills/why/references/sources/slack.md +17 -0
  129. package/extensions/pstack/skills/why/references/synthesizer-prompt.md +129 -0
  130. package/extensions/rtk/filter.ts +51 -4
  131. package/extensions/rtk/session.ts +20 -10
  132. package/extensions/server.ts +31 -23
  133. package/extensions/shared/agent-stats.ts +21 -12
  134. package/extensions/shared/pricing.ts +7 -5
  135. package/extensions/shared/rtk-eligible.ts +79 -0
  136. package/extensions/shared/rtk-gain.ts +3 -7
  137. package/extensions/shared/sqlite.ts +2 -13
  138. package/extensions/shared/status.ts +1 -2
  139. package/extensions/shared/usage-ledger.ts +198 -32
  140. package/extensions/shared/utils.ts +29 -0
  141. package/extensions/timing/index.ts +1 -2
  142. package/extensions/updater/index.ts +18 -7
  143. package/package.json +8 -7
  144. package/dist/extensions/caveman/rule-megacave.md +0 -65
  145. package/dist/extensions/caveman/rule-ultra.md +0 -63
  146. package/dist/extensions/caveman/rule.md +0 -89
  147. package/dist/extensions/pstack/rule.md +0 -65
  148. package/dist/probe-catalog.js +0 -48
  149. package/dist/probe-catalog188.js +0 -44
@@ -2,7 +2,8 @@
2
2
  import fs from 'node:fs';
3
3
  import path from 'node:path';
4
4
  import { execFileSync } from 'node:child_process';
5
- import { homeDir, isPiProcess, piAgentDir, resolveRtkBinary, tersioDataPath } from './utils.js';
5
+ import { homeDir, isPiProcess, piAgentDir, resolveRtkBinary, rtkDirs, shimCommand, tersioDataPath } from './utils.js';
6
+ import { eligibleHeads } from './rtk-eligible.js';
6
7
  import { sqliteRead } from './sqlite.js';
7
8
  export function ledgerPath() {
8
9
  const override = process.env.TERSIO_USAGE_FILE;
@@ -196,6 +197,60 @@ export function dayKey(ts) {
196
197
  const p = (n) => String(n).padStart(2, '0');
197
198
  return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())}`;
198
199
  }
200
+ // OpenCode tool parts carry the command in the message blob; sqliteRead is line-based, so newlines are flattened in SQL.
201
+ const OC_BASH_CMD = "replace(replace(json_extract(j.value,'$.state.input.command'), char(10), ' '), char(13), ' ')";
202
+ const OC_BASH_TOOL = "json_extract(j.value,'$.type')='tool' AND (json_extract(j.value,'$.name')='bash' OR json_extract(j.value,'$.name')='shell')";
203
+ const ocBashLegacy = `SELECT message.id, json_extract(message.data,'$.sessionID'), ${OC_BASH_CMD} FROM message, json_each(message.data,'$.content') j WHERE json_extract(message.data,'$.role')='assistant' AND ${OC_BASH_TOOL}`;
204
+ const ocBashCurrent = `SELECT session_message.id, session_message.session_id, ${OC_BASH_CMD} FROM session_message, json_each(session_message.data,'$.content') j WHERE session_message.type='assistant' AND ${OC_BASH_TOOL}`;
205
+ /** Every executed bash command in an OpenCode db, v2 first and the legacy table second. */
206
+ export function readOpencodeBashRows(db) {
207
+ const rows = [];
208
+ try {
209
+ if (!fs.existsSync(db))
210
+ return rows;
211
+ let out = '';
212
+ try {
213
+ out += sqliteRead(db, ocBashCurrent);
214
+ }
215
+ catch { /* no v2 table */ }
216
+ try {
217
+ out += sqliteRead(db, ocBashLegacy);
218
+ }
219
+ catch { /* no legacy table */ }
220
+ for (const line of out.split('\n')) {
221
+ if (!line.trim())
222
+ continue;
223
+ const [id, sessionId, command] = line.split('\t');
224
+ if (typeof command !== 'string' || !command.trim())
225
+ continue;
226
+ rows.push({ id: id ?? '', sessionId: sessionId ?? '', command });
227
+ }
228
+ }
229
+ catch { /* an unreadable db is a skip, not a failure */ }
230
+ return rows;
231
+ }
232
+ // Pre-v2 OpenCode stores one message per JSON file; the same tool-part shape holds the command.
233
+ function opencodeFileBash(file) {
234
+ try {
235
+ const message = JSON.parse(fs.readFileSync(file, 'utf8'));
236
+ const parts = Array.isArray(message.content) ? message.content : [];
237
+ const commands = [];
238
+ for (const part of parts) {
239
+ const p = part;
240
+ if (p?.type !== 'tool')
241
+ continue;
242
+ if (p.name !== 'bash' && p.name !== 'shell')
243
+ continue;
244
+ const command = p.state?.input?.command;
245
+ if (typeof command === 'string' && command)
246
+ commands.push(command);
247
+ }
248
+ return { commands, sessionId: typeof message.sessionID === 'string' ? message.sessionID : '' };
249
+ }
250
+ catch {
251
+ return { commands: [], sessionId: '' };
252
+ }
253
+ }
199
254
  export function walkJsonl(dir, out, cap, ext = '.jsonl') {
200
255
  let entries;
201
256
  try {
@@ -217,11 +272,6 @@ export function walkJsonl(dir, out, cap, ext = '.jsonl') {
217
272
  // Recent rows are a newest-first window over lifetime history; aggregates keep everything.
218
273
  export const RECENT_LIMIT = 2000;
219
274
  const FREE_SUFFIX = /(?::free|-free)$/i;
220
- const RTK_ELIGIBLE_HEADS = new Set([
221
- 'rtk', 'git', 'grep', 'rg', 'find', 'cat', 'ls', 'tree', 'diff', 'log',
222
- 'npm', 'npx', 'pnpm', 'bun', 'bunx', 'cargo', 'go', 'python', 'pytest',
223
- 'ruff', 'mypy', 'docker', 'kubectl', 'psql', 'aws', 'gh', 'glab', 'wc',
224
- ]);
225
275
  export function newSessionAccum() {
226
276
  return { byModel: {}, byDay: {}, byDayModel: {}, byTool: {}, byModelMessages: {}, byHost: {}, byAgent: {}, byAgentMessages: {}, byModelMeasured: {}, byModelCostMessages: {}, totals: zeroBreakdown(), messages: 0, costMeasured: 0, recent: [], recentFloor: Number.POSITIVE_INFINITY };
227
277
  }
@@ -569,7 +619,17 @@ export function agentOfSessionText(text) {
569
619
  return 'main';
570
620
  }
571
621
  const adoptionFileCache = new Map();
572
- function parseAdoptionFile(file) {
622
+ function countBashCommands(commands, eligible, counts) {
623
+ for (const command of commands) {
624
+ counts.bashCalls += 1;
625
+ const head = leadBinary(command);
626
+ if (eligible.has(head))
627
+ counts.eligibleCalls += 1;
628
+ if (head === 'rtk')
629
+ counts.rtkCalls += 1;
630
+ }
631
+ }
632
+ function parseAdoptionFile(file, eligible) {
573
633
  const counts = { bashCalls: 0, eligibleCalls: 0, rtkCalls: 0 };
574
634
  let text;
575
635
  try {
@@ -578,33 +638,87 @@ function parseAdoptionFile(file) {
578
638
  catch {
579
639
  return { counts, sessions: 0 };
580
640
  }
581
- let sawBash = false;
641
+ // omp emits a custom row per tool start; pi records the same call inside the assistant message.
642
+ // A host that writes both shapes would double every call, so the custom ids gate the message pass.
643
+ const commands = [];
644
+ const customIds = new Set();
645
+ const messageCalls = [];
582
646
  for (const line of text.split('\n')) {
583
- if (!line.includes('tool_execution_start'))
647
+ if (!line.includes('tool_execution_start') && !line.includes('"toolCall"'))
584
648
  continue;
585
649
  try {
586
650
  const row = JSON.parse(line);
587
- if (row.type !== 'custom' || row.customType !== 'tool_execution_start' || row.data?.toolName !== 'bash')
651
+ if (row.type === 'custom' && row.customType === 'tool_execution_start' && row.data?.toolName === 'bash') {
652
+ const command = row.data.args?.command;
653
+ if (typeof command !== 'string')
654
+ continue;
655
+ if (typeof row.data.toolCallId === 'string')
656
+ customIds.add(row.data.toolCallId);
657
+ commands.push(command);
588
658
  continue;
589
- const command = row.data.args?.command;
590
- if (typeof command !== 'string')
659
+ }
660
+ const content = row.message?.content;
661
+ if (row.type !== 'message' || !Array.isArray(content))
591
662
  continue;
592
- sawBash = true;
593
- counts.bashCalls += 1;
594
- const head = leadBinary(command);
595
- if (RTK_ELIGIBLE_HEADS.has(head))
596
- counts.eligibleCalls += 1;
597
- if (head === 'rtk')
598
- counts.rtkCalls += 1;
663
+ for (const part of content) {
664
+ if (typeof part !== 'object' || part === null)
665
+ continue;
666
+ const call = part;
667
+ if (call.type !== 'toolCall' || call.name !== 'bash')
668
+ continue;
669
+ const args = call.arguments;
670
+ if (typeof args !== 'object' || args === null || !('command' in args))
671
+ continue;
672
+ const command = args.command;
673
+ if (typeof command !== 'string')
674
+ continue;
675
+ messageCalls.push({ id: typeof call.id === 'string' ? call.id : undefined, command });
676
+ }
599
677
  }
600
678
  catch { /* skip corrupt line */ }
601
679
  }
602
- return { counts, sessions: sawBash ? 1 : 0 };
680
+ for (const call of messageCalls) {
681
+ if (call.id !== undefined && customIds.has(call.id))
682
+ continue;
683
+ commands.push(call.command);
684
+ }
685
+ countBashCommands(commands, eligible, counts);
686
+ return { counts, sessions: commands.length > 0 ? 1 : 0 };
603
687
  }
604
688
  export function clearRtkAdoptionCache() {
605
689
  adoptionFileCache.clear();
606
690
  }
691
+ /** Rewrites the hook performed on hosts whose transcript records the pre-hook command, by host. */
692
+ function countRtkAuditRows() {
693
+ const byHost = {};
694
+ let text;
695
+ try {
696
+ text = fs.readFileSync(ledgerPath(), 'utf8');
697
+ }
698
+ catch {
699
+ return byHost;
700
+ }
701
+ for (const line of text.split('\n')) {
702
+ if (!line.includes('rtk-audit'))
703
+ continue;
704
+ try {
705
+ const row = JSON.parse(line);
706
+ if (row.kind !== 'rtk-audit' || typeof row.detail !== 'string')
707
+ continue;
708
+ const host = row.detail.split(' ')[0];
709
+ if (!host)
710
+ continue;
711
+ byHost[host] = (byHost[host] ?? 0) + 1;
712
+ }
713
+ catch { /* skip corrupt line */ }
714
+ }
715
+ return byHost;
716
+ }
607
717
  export function readRtkAdoption() {
718
+ // The eligible set comes from rtk's rewrite contract, cached per binary version.
719
+ const eligible = eligibleHeads(resolveRtkBinary());
720
+ // A cached count is only valid for the head set that produced it.
721
+ const headsKey = [...eligible].sort().join(',');
608
722
  const files = [];
609
723
  for (const dir of sessionsDirs())
610
724
  walkJsonl(dir, files, 2000);
@@ -617,6 +731,14 @@ export function readRtkAdoption() {
617
731
  let eligibleCalls = 0;
618
732
  let rtkCalls = 0;
619
733
  let sessions = 0;
734
+ const byHost = {};
735
+ const hostCounts = (host, counts) => {
736
+ const into = byHost[host] ?? { bashCalls: 0, eligibleCalls: 0, rtkCalls: 0 };
737
+ into.bashCalls += counts.bashCalls;
738
+ into.eligibleCalls += counts.eligibleCalls;
739
+ into.rtkCalls += counts.rtkCalls;
740
+ byHost[host] = into;
741
+ };
620
742
  for (const file of files) {
621
743
  let stat;
622
744
  try {
@@ -626,9 +748,9 @@ export function readRtkAdoption() {
626
748
  continue;
627
749
  }
628
750
  let entry = adoptionFileCache.get(file);
629
- if (!entry || entry.size !== stat.size || entry.mtimeMs !== stat.mtimeMs) {
630
- const parsed = parseAdoptionFile(file);
631
- entry = { size: stat.size, mtimeMs: stat.mtimeMs, counts: parsed.counts };
751
+ if (!entry || entry.size !== stat.size || entry.mtimeMs !== stat.mtimeMs || entry.heads !== headsKey) {
752
+ const parsed = parseAdoptionFile(file, eligible);
753
+ entry = { size: stat.size, mtimeMs: stat.mtimeMs, heads: headsKey, counts: parsed.counts };
632
754
  adoptionFileCache.set(file, entry);
633
755
  sessions += parsed.sessions;
634
756
  }
@@ -638,9 +760,56 @@ export function readRtkAdoption() {
638
760
  bashCalls += entry.counts.bashCalls;
639
761
  eligibleCalls += entry.counts.eligibleCalls;
640
762
  rtkCalls += entry.counts.rtkCalls;
763
+ hostCounts(hostOfSessionFile(file), entry.counts);
764
+ }
765
+ // OpenCode keeps its own store: a v2 sqlite db, or one JSON file per message before v2.
766
+ const opencode = { bashCalls: 0, eligibleCalls: 0, rtkCalls: 0 };
767
+ const opencodeSessions = new Set();
768
+ if (process.env.TERSIO_SESSIONS_DIR === undefined || process.env.TERSIO_OPENCODE_DB !== undefined) {
769
+ for (const row of readOpencodeBashRows(opencodeDbPath())) {
770
+ countBashCommands([row.command], eligible, opencode);
771
+ if (row.sessionId)
772
+ opencodeSessions.add(row.sessionId);
773
+ }
774
+ }
775
+ const ocFiles = [];
776
+ if (process.env.TERSIO_SESSIONS_DIR === undefined || process.env.TERSIO_OPENCODE_DIR !== undefined) {
777
+ walkJsonl(opencodeSessionsDir(), ocFiles, 5000, '.json');
778
+ for (const file of ocFiles) {
779
+ const parsed = opencodeFileBash(file);
780
+ countBashCommands(parsed.commands, eligible, opencode);
781
+ if (parsed.sessionId)
782
+ opencodeSessions.add(parsed.sessionId);
783
+ }
784
+ }
785
+ bashCalls += opencode.bashCalls;
786
+ eligibleCalls += opencode.eligibleCalls;
787
+ rtkCalls += opencode.rtkCalls;
788
+ sessions += opencodeSessions.size;
789
+ if (opencode.bashCalls > 0)
790
+ hostCounts('opencode', opencode);
791
+ // The hook audit carries rewrites on pi and opencode, whose transcripts record the pre-hook command.
792
+ for (const [host, rows] of Object.entries(countRtkAuditRows())) {
793
+ rtkCalls += rows;
794
+ hostCounts(host, { bashCalls: 0, eligibleCalls: 0, rtkCalls: rows });
641
795
  }
642
796
  const missedCalls = Math.max(0, eligibleCalls - rtkCalls);
643
- return { sessions, bashCalls, eligibleCalls, rtkCalls, missedCalls, adoptionPct: eligibleCalls ? (rtkCalls / eligibleCalls) * 100 : 0 };
797
+ const host = (counts) => ({
798
+ bashCalls: counts.bashCalls,
799
+ eligibleCalls: counts.eligibleCalls,
800
+ rtkCalls: counts.rtkCalls,
801
+ missedCalls: Math.max(0, counts.eligibleCalls - counts.rtkCalls),
802
+ adoptionPct: counts.eligibleCalls ? (counts.rtkCalls / counts.eligibleCalls) * 100 : 0,
803
+ });
804
+ return {
805
+ sessions,
806
+ bashCalls,
807
+ eligibleCalls,
808
+ rtkCalls,
809
+ missedCalls,
810
+ adoptionPct: eligibleCalls ? (rtkCalls / eligibleCalls) * 100 : 0,
811
+ byHost: Object.fromEntries(Object.entries(byHost).map(([name, counts]) => [name, host(counts)])),
812
+ };
644
813
  }
645
814
  export function processSessionText(accum, state, text, host, agent) {
646
815
  for (const line of text.split('\n')) {
@@ -678,10 +847,7 @@ export function readRtkRecallDiagnostics(binary = resolveRtkBinary()) {
678
847
  if (!binary)
679
848
  return { mode: 'unknown', entries: 0, available: false };
680
849
  try {
681
- const configPath = process.env.RTK_CONFIG
682
- || (process.platform === 'darwin'
683
- ? path.join(homeDir(), 'Library', 'Application Support', 'rtk', 'config.toml')
684
- : path.join(process.env.XDG_DATA_HOME || path.join(homeDir(), '.local', 'share'), 'rtk', 'config.toml'));
850
+ const configPath = process.env.RTK_CONFIG || path.join(rtkDirs().config, 'config.toml');
685
851
  let config = '';
686
852
  try {
687
853
  config = fs.readFileSync(configPath, 'utf8');
@@ -689,7 +855,8 @@ export function readRtkRecallDiagnostics(binary = resolveRtkBinary()) {
689
855
  catch { /* use RTK CLI fallback */ }
690
856
  const modeMatch = /^\s*mode\s*=\s*"(sqlite|tee|disabled)"/m.exec(config);
691
857
  const mode = (modeMatch?.[1] ?? 'unknown');
692
- const output = execFileSync(binary, ['recall', '--list'], { encoding: 'utf8', timeout: 5000, windowsHide: true });
858
+ const shim = shimCommand(binary, ['recall', '--list']);
859
+ const output = execFileSync(shim.cmd, shim.args, { encoding: 'utf8', timeout: 5000, windowsHide: true });
693
860
  const entries = /\((\d+)\s+(?:entries?|stored)\)/i.exec(output)?.[1] ?? /^\s*(\d+)\s+entries?/im.exec(output)?.[1] ?? '0';
694
861
  return { mode, entries: Number(entries) || 0, available: true };
695
862
  }
@@ -17,9 +17,31 @@ export function homeDir() {
17
17
  const home = process.env.HOME || process.env.USERPROFILE;
18
18
  return home && home.trim() !== '' ? home : os.homedir();
19
19
  }
20
+ /** Windows cannot run a `.cmd` or `.bat` directly, so a shim path goes through the shell; a real `.exe` or POSIX binary passes through. */
21
+ export function shimCommand(cmd, args) {
22
+ if (process.platform !== 'win32' || !/\.(?:cmd|bat)$/i.test(cmd))
23
+ return { cmd, args };
24
+ return { cmd: process.env.ComSpec || 'cmd.exe', args: ['/d', '/s', '/c', cmd, ...args] };
25
+ }
26
+ /** rtk's own dirs: `config_dir()` for config.toml and `data_local_dir()` for history.db, which differ per platform; a guess here empties the Gain charts. */
27
+ export function rtkDirs() {
28
+ if (process.platform === 'darwin') {
29
+ const base = path.join(homeDir(), 'Library', 'Application Support', 'rtk');
30
+ return { data: base, config: base };
31
+ }
32
+ if (process.platform === 'win32') {
33
+ const local = process.env.LOCALAPPDATA || path.join(homeDir(), 'AppData', 'Local');
34
+ const roaming = process.env.APPDATA || path.join(homeDir(), 'AppData', 'Roaming');
35
+ return { data: path.join(local, 'rtk'), config: path.join(roaming, 'rtk') };
36
+ }
37
+ const xdgData = process.env.XDG_DATA_HOME || path.join(homeDir(), '.local', 'share');
38
+ const xdgConfig = process.env.XDG_CONFIG_HOME || path.join(homeDir(), '.config');
39
+ return { data: path.join(xdgData, 'rtk'), config: path.join(xdgConfig, 'rtk') };
40
+ }
20
41
  export const RTK_RELEASE_API = 'https://api.github.com/repos/rtk-ai/rtk/releases/latest';
21
42
  export const CAVEMAN_REMOTE_ULTRA = 'https://raw.githubusercontent.com/JuliusBrussee/caveman/main/skills/ultracave/SKILL.md';
22
43
  export const CAVEMAN_REMOTE_MEGACAVE = 'https://raw.githubusercontent.com/JuliusBrussee/caveman/main/skills/megacave/SKILL.md';
44
+ export const CAVEMAN_REMOTE_FULL = 'https://raw.githubusercontent.com/JuliusBrussee/caveman/main/skills/caveman/SKILL.md';
23
45
  // A fetch that returns an error page or a truncated body must not reach the extension tree.
24
46
  export function ruleBodyProblem(text) {
25
47
  const trimmed = text.trim();
@@ -80,6 +102,17 @@ export function findPackageRoot(fromDir) {
80
102
  dir = parent;
81
103
  }
82
104
  }
105
+ /** Where a mode's rule bodies sit: beside the module, else the package's own `extensions/<name>`. */
106
+ export function ruleDir(name, ownDir) {
107
+ if (existsSync(path.join(ownDir, 'rule.md')))
108
+ return ownDir;
109
+ try {
110
+ return path.join(findPackageRoot(ownDir), 'extensions', name);
111
+ }
112
+ catch {
113
+ return ownDir;
114
+ }
115
+ }
83
116
  // The 3xx+location redirect rule shared by httpsGet/httpsDownload.
84
117
  function redirectNext(res, url) {
85
118
  if (res.statusCode !== undefined && res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
@@ -7,7 +7,7 @@ import os from 'node:os';
7
7
  import path from 'node:path';
8
8
  import { fileURLToPath } from 'node:url';
9
9
  import { setExtensionLabel } from '../shared/host.js';
10
- import { CAVEMAN_REMOTE_ULTRA, CAVEMAN_REMOTE_MEGACAVE, RTK_RELEASE_API, fetchJson, findFile, findHoistedPackage, httpsGet, httpsDownload, sha256Hex, parseChecksum, normalizeRtkVersion, readTextIfExists, rtkPlatformSpec, resolveRtkBinary, } from '../shared/utils.js';
10
+ import { CAVEMAN_REMOTE_ULTRA, CAVEMAN_REMOTE_MEGACAVE, CAVEMAN_REMOTE_FULL, RTK_RELEASE_API, fetchJson, findFile, findHoistedPackage, homeDir, httpsGet, httpsDownload, sha256Hex, parseChecksum, normalizeRtkVersion, readTextIfExists, rtkPlatformSpec, resolveRtkBinary, } from '../shared/utils.js';
11
11
  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';
@@ -18,14 +18,18 @@ function cavemanRule(name) {
18
18
  const beside = CAVEMAN_SIBLING(name);
19
19
  if (fsSync.existsSync(beside))
20
20
  return beside;
21
- const plugin = path.join(os.homedir(), '.omp', 'plugins', 'node_modules', '@krtclcdy', 'tersio', 'extensions', 'caveman', name);
21
+ const plugin = path.join(homeDir(), '.omp', 'plugins', 'node_modules', '@krtclcdy', 'tersio', 'extensions', 'caveman', name);
22
22
  return fsSync.existsSync(plugin) ? plugin : beside;
23
23
  }
24
24
  const CAVEMAN_LOCAL = cavemanRule('rule.md');
25
+ // `rule.md` is upstream's body with one adapted line, so a refresh re-applies the adaptation.
26
+ const adaptCavemanFull = (text) => text.replace('`/caveman-compress` exempt.', '`/caveman-compress`, where the host offers it, is exempt.');
25
27
  const CAVEMAN_TRACKED = [
28
+ { file: CAVEMAN_LOCAL, remote: CAVEMAN_REMOTE_FULL, adapt: adaptCavemanFull },
26
29
  { file: cavemanRule('rule-ultra.md'), remote: CAVEMAN_REMOTE_ULTRA },
27
30
  { file: cavemanRule('rule-megacave.md'), remote: CAVEMAN_REMOTE_MEGACAVE },
28
31
  ];
32
+ export { adaptCavemanFull };
29
33
  // Host-neutral: this updater runs inside pi, OMP, and OpenCode alike.
30
34
  const RELOAD_MSG = 'Reminder: restart your agent (or reload extensions) for updates to take effect.';
31
35
  // Ponytail is hoisted, so the installed copy is found by walking up, not by naming a host directory.
@@ -98,11 +102,13 @@ function checkRtk() {
98
102
  function checkCaveman() {
99
103
  return runCheck('Caveman', async () => {
100
104
  const parts = [(await readTextIfExists(CAVEMAN_LOCAL)) ? 'rule.md ok' : 'rule.md missing'];
101
- for (const { file, remote } of CAVEMAN_TRACKED) {
105
+ for (const { file, remote, adapt } of CAVEMAN_TRACKED) {
102
106
  const name = path.basename(file);
103
107
  try {
104
108
  const [remoteText, localText] = await Promise.all([httpsGet(remote), readTextIfExists(file)]);
105
- parts.push(!localText ? `${name} missing` : shortHash(localText) === shortHash(remoteText) ? `${name} up to date` : `${name} update available`);
109
+ // Compare the text a refresh would write, so the adapted line never reads as drift.
110
+ const nextHash = shortHash(adapt ? adapt(remoteText) : remoteText);
111
+ parts.push(!localText ? `${name} missing` : shortHash(localText) === nextHash ? `${name} up to date` : `${name} update available`);
106
112
  }
107
113
  catch {
108
114
  parts.push(`${name} check skipped`);
@@ -333,7 +339,7 @@ async function updateRtk(ctx, dryRun = false) {
333
339
  }
334
340
  async function updateCaveman(ctx, dryRun = false) {
335
341
  const results = [];
336
- for (const { file, remote } of CAVEMAN_TRACKED) {
342
+ for (const { file, remote, adapt } of CAVEMAN_TRACKED) {
337
343
  const name = path.basename(file);
338
344
  let remoteText;
339
345
  try {
@@ -343,7 +349,9 @@ async function updateCaveman(ctx, dryRun = false) {
343
349
  results.push(report(ctx, `Caveman ${name} update failed: ${e.message}`, 'warning'));
344
350
  continue;
345
351
  }
346
- const remoteHash = shortHash(remoteText);
352
+ // A tracked fork compares against the text it would write, so an adapted body still reads up to date.
353
+ const nextText = adapt ? adapt(remoteText) : remoteText;
354
+ const remoteHash = shortHash(nextText);
347
355
  const oldLocal = await readTextIfExists(file);
348
356
  const oldHash = oldLocal ? shortHash(oldLocal) : null;
349
357
  if (oldHash === remoteHash) {
@@ -359,7 +367,7 @@ async function updateCaveman(ctx, dryRun = false) {
359
367
  const backupPath = `${file}.bak`;
360
368
  if (oldLocal !== null)
361
369
  await fs.writeFile(backupPath, oldLocal, 'utf8');
362
- await fs.writeFile(file, remoteText, 'utf8');
370
+ await fs.writeFile(file, nextText, 'utf8');
363
371
  const writtenHash = shortHash(await fs.readFile(file, 'utf8'));
364
372
  if (writtenHash !== remoteHash) {
365
373
  if (oldLocal !== null)
@@ -5,11 +5,12 @@ import { dirname, join } from 'node:path';
5
5
  import { injectPromptText, onHostEvent, setExtensionLabel } from '../shared/host.ts';
6
6
  import { announceStatus, formatCavemanStatus } from '../shared/status.ts';
7
7
  import { readCavemanDefault } from '../shared/plugin-settings.ts';
8
+ import { ruleDir } from '../shared/utils.ts';
8
9
  import type { ExtensionApi, ExtensionCtx, InputEvent, SessionEntry, SystemPromptEvent } from '../shared/types.ts';
9
10
 
10
- const CAVERN_DIR = dirname(fileURLToPath(import.meta.url));
11
- // Upstream ships one skill per register: default, ultracave, megacave.
12
- const RULE_FILES = { lite: 'rule.md', full: 'rule.md', ultra: 'rule-ultra.md', 'megacave-lite': 'rule-megacave.md', 'megacave-full': 'rule-megacave.md', 'megacave-ultra': 'rule-megacave.md' };
11
+ const CAVERN_DIR = ruleDir('caveman', dirname(fileURLToPath(import.meta.url)));
12
+ // `rule.md`, `rule-ultra.md`, and `rule-megacave.md` track upstream and are refetched; `lite`, `megacave-lite`, and `megacave-ultra` are our forks and ship bundled.
13
+ const RULE_FILES = { lite: 'rule-lite.md', full: 'rule.md', ultra: 'rule-ultra.md', 'megacave-lite': 'rule-megacave-lite.md', 'megacave-full': 'rule-megacave.md', 'megacave-ultra': 'rule-megacave-ultra.md' };
13
14
 
14
15
  const DEFAULT_MODE = 'off';
15
16
 
@@ -0,0 +1,77 @@
1
+ ---
2
+ name: lightcave
3
+ description: >
4
+ Caveman at its lightest register: no filler and no hedging, with articles and
5
+ full sentences kept. Professional but tight. Use for /caveman lite, or the lite
6
+ part of a Combo preset. Stays on until "stop caveman" or "normal mode".
7
+ source: >
8
+ Upstream JuliusBrussee/caveman, skills/caveman/SKILL.md at 84cc3c1, the "Intensity" table, row `lite`.
9
+ The row reads: "No filler/hedging. Keep articles + full sentences. Professional but tight"
10
+ The worked example under it is this register's model answer. The floor, the break-the-rules list,
11
+ and the pre-send check come from the same body. Adapted for Tersio's hosts and hook; no rule here
12
+ is Tersio's own.
13
+ ---
14
+
15
+ # lightcave
16
+
17
+ Respond terse like smart caveman. All technical substance stay. Only fluff die. This is the light register: sentences stay sentences and articles stay, and the words that carry nothing go.
18
+
19
+ ## Persistence
20
+
21
+ Every response, whole session, until "stop caveman" or "normal mode". Unsure if still on? It is. `/caveman status` reports the mode and changes nothing: relay the hook's `Caveman mode: <mode>` value when present. With no hook, report the mode you followed, or `off` if caveman was turned off or never active, plus `(not tracked by this host)`.
22
+
23
+ ## What changes at lite
24
+
25
+ Drop filler, hedging, pleasantries, recap, and closers. Keep articles, keep full sentences, keep punctuation. Professional but tight.
26
+
27
+ Bad: "Sure! I'd be happy to help. The issue you're experiencing is likely caused by an inline object prop."
28
+ Good: "Your component re-renders because you create a new object reference on each render. Wrap it in `useMemo`."
29
+
30
+ ## Floor
31
+
32
+ Never cut: code blocks, 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 one you were not asked to change.
33
+
34
+ ## Rules
35
+
36
+ ### 1. Answer first
37
+
38
+ Answer, then reason, then next step. Pattern: `[thing] [action] [reason]. [next step].`
39
+
40
+ ### 2. Kill ceremony
41
+
42
+ No greeting, hedging, pleasantries, recap, or closer. No "Sure!", "Let me", "I'll now", "Hope this helps". No just/really/basically/actually/simply.
43
+
44
+ ### 3. Short word
45
+
46
+ "fix" not "implement a solution for". Standard acronyms fine (DB, API, HTTP). Invented abbreviations not (cfg, impl, fn): same tokens, harder read. No arrows.
47
+
48
+ ### 4. One idea per sentence
49
+
50
+ 20 words max, active voice, imperative for instructions, pronoun only with an obvious referent. Compression and clarity conflict? Clarity wins.
51
+
52
+ ### 5. Payload verbatim
53
+
54
+ Code blocks unchanged. Commands, paths, API names exact. Errors quoted exact, shortest decisive line only. A code change in chat shows the changed lines plus 1-2 lines of context, not the whole file, unless the user asks for the whole file or most of it changes.
55
+
56
+ ### 6. Tool runs: bounded status
57
+
58
+ 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.
59
+
60
+ ### 7. User's language
61
+
62
+ Compress the style, not the language. An explicit reply-language instruction wins. Never switch because of quoted text. Technical terms and errors stay verbatim.
63
+
64
+ ### 8. Never perform caveman
65
+
66
+ No "caveman mode on", no "me think", no "Caveman:" prefix, no normal answer plus caveman copy. No decorative tables or emoji.
67
+
68
+ ## When to break the rules
69
+
70
+ Plain prose, then resume: security warning. Irreversible action, confirmed first. Step order a sentence could scramble. User confused or repeats the question. Anything persisted outside chat — code, comments, commits, docs, issues, PRs, tickets, memory files, third-party messages — with `/caveman-compress`, where the host offers it, exempt. A status line or confirmation the harness asks for. A question you ask the user, with its options.
71
+
72
+ ## Pre-send check
73
+
74
+ 1. First sentence announces what you will do? Delete.
75
+ 2. Last sentence recaps or offers help? Delete.
76
+ 3. Any filler or hedging left? Cut it.
77
+ 4. Every code span, path, number, and error verbatim? Any sentence with two readings is a full sentence.
@@ -0,0 +1,59 @@
1
+ ---
2
+ name: megacave-lite
3
+ description: >
4
+ Megacave at its lightest register: semi-classical 文言文 flavour with grammar
5
+ structure kept, filler and hedging dropped. Use for /caveman megacave-lite. Stays
6
+ on until "stop caveman" or "normal mode".
7
+ source: >
8
+ Upstream JuliusBrussee/caveman, skills/caveman/SKILL.md at 84cc3c1, the "Intensity" table, row `wenyan-lite`.
9
+ The row reads: "Semi-classical. Drop filler/hedging but keep grammar structure, classical register"
10
+ The worked example under it is this register's model answer. The Floor comes from upstream
11
+ skills/megacave/SKILL.md at main. Adapted for Tersio's hosts and hook; no rule here is Tersio's own.
12
+ ---
13
+
14
+ # megacave-lite
15
+
16
+ 以文言答,而語法具存。技術之實皆存,唯贅言與游移去之。
17
+
18
+ Semi-classical register with grammar kept. Drop filler, hedging, pleasantries, recap, and closers. Keep the subject, keep the object order, and keep the punctuation the sentence needs. Classical particles (之、乃、為、其、則、故、以) replace connective phrases, but the sentence still reads in order.
19
+
20
+ Bad: 「當然,我很樂意協助您。您遇到的問題大概是因為每次渲染都建立了新的物件參照。」
21
+ Good: 「組件頻重繪,以每繪新生對象參照故。以 `useMemo` 包之。」
22
+
23
+ ## Floor
24
+
25
+ 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.
26
+
27
+ ## Rules
28
+
29
+ ### 1. Classical flavour, modern grammar
30
+
31
+ 半文言, not 白話. Subject, verb, and object stay where a reader expects them. One particle, one relation.
32
+
33
+ ### 2. Drop filler and hedging only
34
+
35
+ No 當然, 我想, 可能, 基本上, 其實. No pleasantries, no recap, no closer. Articles and sentence structure stay.
36
+
37
+ ### 3. One idea per sentence
38
+
39
+ Short sentences, one relation each. Any clause with two readings becomes a full sentence.
40
+
41
+ ### 4. Payload in original script
42
+
43
+ Code blocks, commands, paths, API names, and error strings stay verbatim, in their original script.
44
+
45
+ ### 5. Tool runs: bounded status
46
+
47
+ One line in, one out. Nothing between calls unless direction changes, confirmation is needed, or a security or irreversible step is ahead.
48
+
49
+ ### 6. Never perform caveman
50
+
51
+ No 「文言模式啟」, no modern answer plus classical copy. 半文言 not shorter than 白話 for a sentence? Use 白話.
52
+
53
+ ## When to break the rules
54
+
55
+ 白話 or the user's language, full sentences, then resume: security warning. Irreversible action, confirmed 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 — with `/caveman-compress`, where the host offers it, exempt. A status line or confirmation the harness asks for.
56
+
57
+ ## Pre-send check
58
+
59
+ Identifiers verbatim, original script? Negations present? Two readings? 白話. Flavour word left? Cut.
@@ -0,0 +1,59 @@
1
+ ---
2
+ name: megacave-ultra
3
+ description: >
4
+ Megacave at maximum compression: extreme abbreviation while keeping the
5
+ classical Chinese feel. Use for /caveman megacave-ultra. Stays on until
6
+ "stop caveman" or "normal mode".
7
+ source: >
8
+ Upstream JuliusBrussee/caveman, skills/caveman/SKILL.md at 84cc3c1, the "Intensity" table, row `wenyan-ultra`.
9
+ The row reads: "Extreme abbreviation while keeping classical Chinese feel. Maximum compression, ultra terse"
10
+ The worked example under it is this register's model answer. The Floor comes from upstream
11
+ skills/megacave/SKILL.md at main. Adapted for Tersio's hosts and hook; no rule here is Tersio's own.
12
+ ---
13
+
14
+ # megacave-ultra
15
+
16
+ 以文言答,極簡其辭。技術之實皆存,唯字去之。
17
+
18
+ Extreme abbreviation, classical feel. Subjects, copulas, and connectives all go; one mark carries one relation. Arrows for causality. Maximum compression, ultra terse.
19
+
20
+ Bad: 「因為每次渲染都建立了新的物件參照,所以組件會重新渲染。請用 useMemo 包起來。」
21
+ Good: 「新參照→重繪。`useMemo` Wrap。」
22
+
23
+ ## Floor
24
+
25
+ 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.
26
+
27
+ ## Rules
28
+
29
+ ### 1. Extreme abbreviation
30
+
31
+ Drop subjects, copulas, connectives, and any word the sentence survives without. One mark per relation: → for cause, 、 for a list, 。 to stop.
32
+
33
+ ### 2. Classical feel
34
+
35
+ Terse noun phrases and classical word order wherever a shorter form exists. A particle only when no shorter mark can carry the relation.
36
+
37
+ ### 3. Each fact once
38
+
39
+ No restating, no summary after a list. Code change: changed lines plus context, not the whole file.
40
+
41
+ ### 4. Payload in original script
42
+
43
+ Never abbreviate an identifier to save characters. Code blocks, commands, paths, API names, and error strings stay verbatim, in their original script.
44
+
45
+ ### 5. Tool runs
46
+
47
+ One line in, one out. Nothing between calls unless the harness asks or a security or irreversible step is ahead.
48
+
49
+ ### 6. Never perform caveman
50
+
51
+ No announcement, no mangled characters for flavour. Abbreviation not shorter than the sentence? Use the sentence.
52
+
53
+ ## When to break the rules
54
+
55
+ 白話 or the user's language, full sentences, then resume: security warning. Irreversible action, confirmed 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 — with `/caveman-compress`, where the host offers it, exempt. A status line or confirmation the harness asks for.
56
+
57
+ ## Pre-send check
58
+
59
+ Identifiers verbatim, original script? Negations present? Two readings? 白話. Flavour character? Cut.
@@ -77,7 +77,7 @@ Plain prose, then resume:
77
77
  2. Irreversible action. Confirm in full sentences first.
78
78
  3. Step order a fragment could scramble.
79
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.
80
+ 5. Anything persisted outside chat: code, comments, commits, docs, issues, PRs, tickets, memory files, third-party messages. `/caveman-compress`, where the host offers it, is exempt.
81
81
  6. Harness asks for a status line or confirmation. Give it. Harness decides *when* you speak, caveman decides *how*.
82
82
  7. You ask the user a question or offer options. Full sentences, so the answer comes back right first time.
83
83