@krtclcdy/tersio 2.25.4 → 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 (77) hide show
  1. package/README.md +27 -63
  2. package/dashboard/dist/index.html +18 -16
  3. package/dist/cli/backups.js +187 -0
  4. package/dist/cli/banner.js +4 -3
  5. package/dist/cli/common.js +29 -13
  6. package/dist/cli/currency.js +1 -1
  7. package/dist/cli/dashboard.js +168 -110
  8. package/dist/cli/doctor-fix.js +232 -50
  9. package/dist/cli/doctor.js +281 -32
  10. package/dist/cli/hosts.js +29 -9
  11. package/dist/cli/install.js +204 -69
  12. package/dist/cli/interactive.js +3 -3
  13. package/dist/cli/manifest.js +125 -17
  14. package/dist/cli/profile.js +14 -9
  15. package/dist/cli/rtk-wiring.js +1 -1
  16. package/dist/cli/settings.js +20 -5
  17. package/dist/cli/uninstall.js +24 -8
  18. package/dist/cli/update.js +9 -10
  19. package/dist/cli/usage.js +75 -7
  20. package/dist/extensions/{caveman-session → caveman}/index.js +4 -2
  21. package/{extensions/caveman-session → dist/extensions/caveman}/rule-megacave.md +5 -3
  22. package/{extensions/caveman-session → dist/extensions/caveman}/rule-ultra.md +4 -4
  23. package/dist/extensions/caveman/rule.md +89 -0
  24. package/dist/extensions/{combo-toggle → combo}/index.js +62 -21
  25. package/dist/extensions/{tersio-commands → commands}/index.js +2 -2
  26. package/dist/extensions/pstack/index.js +119 -0
  27. package/dist/extensions/pstack/rule.md +65 -0
  28. package/dist/extensions/{rtk-filter/index.js → rtk/filter.js} +1 -2
  29. package/dist/extensions/{rtk-session/index.js → rtk/session.js} +5 -5
  30. package/dist/extensions/{opencode/server.js → server.js} +98 -14
  31. package/dist/extensions/shared/agent-stats.js +1198 -0
  32. package/dist/extensions/shared/brand-marks.js +153 -0
  33. package/dist/extensions/shared/carbon.js +1 -1
  34. package/dist/extensions/shared/plugin-settings.js +12 -7
  35. package/dist/extensions/shared/pricing.js +86 -30
  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 +113 -39
  42. package/dist/extensions/shared/usage-store.js +191 -48
  43. package/dist/extensions/{lib → shared}/utils.js +6 -6
  44. package/dist/extensions/timing/index.js +31 -0
  45. package/dist/extensions/{ai-addons-updater → updater}/index.js +13 -6
  46. package/dist/probe-catalog.js +48 -0
  47. package/dist/probe-catalog188.js +44 -0
  48. package/dist/tersio.js +9 -4
  49. package/extensions/{caveman-session → caveman}/index.ts +4 -2
  50. package/{dist/extensions/caveman-session → extensions/caveman}/rule-megacave.md +5 -3
  51. package/{dist/extensions/caveman-session → extensions/caveman}/rule-ultra.md +4 -4
  52. package/extensions/caveman/rule.md +89 -0
  53. package/extensions/{combo-toggle → combo}/index.ts +62 -19
  54. package/extensions/{tersio-commands → commands}/index.ts +2 -2
  55. package/extensions/pstack/index.ts +128 -0
  56. package/extensions/pstack/rule.md +65 -0
  57. package/extensions/{rtk-filter/index.ts → rtk/filter.ts} +1 -2
  58. package/extensions/{rtk-session/index.ts → rtk/session.ts} +5 -5
  59. package/extensions/{opencode/server.ts → server.ts} +81 -16
  60. package/extensions/shared/agent-stats.ts +1605 -0
  61. package/extensions/shared/brand-marks.ts +163 -0
  62. package/extensions/shared/carbon.ts +1 -1
  63. package/extensions/shared/plugin-settings.ts +13 -7
  64. package/extensions/shared/pricing.ts +95 -32
  65. package/extensions/shared/rtk-gain.ts +4 -9
  66. package/extensions/shared/session-state.ts +18 -13
  67. package/extensions/shared/sqlite.ts +168 -0
  68. package/extensions/shared/status.ts +9 -8
  69. package/extensions/shared/types.ts +2 -1
  70. package/extensions/shared/usage-ledger.ts +134 -46
  71. package/extensions/shared/usage-store.ts +202 -50
  72. package/extensions/{lib → shared}/utils.ts +6 -6
  73. package/extensions/timing/index.ts +45 -0
  74. package/extensions/{ai-addons-updater → updater}/index.ts +15 -9
  75. package/package.json +27 -18
  76. package/dist/extensions/caveman-session/rule.md +0 -79
  77. package/extensions/caveman-session/rule.md +0 -79
@@ -0,0 +1,65 @@
1
+ ---
2
+ name: pstack
3
+ description: >
4
+ Rigor mode: apply the named principle at the moment it fits, and verify
5
+ against the real artifact instead of a proxy. Use for /pstack, "pstack",
6
+ "poteto mode". Stays on until "/pstack off" or "normal mode".
7
+ ---
8
+
9
+ # pstack
10
+
11
+ Stay in this mode for every response of the session, until the user says `/pstack off` or `normal mode`.
12
+
13
+ This mode owns the work, not the voice: where an active style mode disagrees with these rules about the shape of the reply, the style mode wins.
14
+
15
+ Where ponytail disagrees with these rules about how much to change, ponytail wins. The change is the size the task requires, and no larger. These rules own the approach, the verification, and the design.
16
+
17
+ ## Non-negotiables
18
+
19
+ - About to ask the user to pick an approach → classify the fork first. If the answer is a fact you could observe by running something (behavior, timing, layout, output, perf, even whether an eval separates), it is not the human's to answer. Sketch it and let the result decide. Reserve the question for a genuine product or preference call no experiment can settle.
20
+ - Contested design → get an adversarial read before shipping. A second opinion is the same prompt against a different model.
21
+ - An automated review commented → skeptical posture. They catch real bugs and also file non-issues and nitpicks. Assess each on its merits and dismiss noise with a concrete reason instead of churning code.
22
+ - Asked to land or ship a green stack → green is not safe: nothing gets armed before an independent per-PR verdict.
23
+ - Broken skill or rule file mid-task → fix it in its own PR. Don't block. Don't silently work around it.
24
+
25
+ ## Principles
26
+
27
+ Apply the entry whose trigger matches the moment.
28
+
29
+ **Core**
30
+
31
+ - **Foundational Thinking.** Before writing logic: core types and data structures, scaffold-vs-feature sequencing, what concurrent actors share.
32
+ - **Attack the Premise.** Two or more fixes that share one premise have failed the same gate. Take a census of which actors hold the imbalance before the next fix, then question the premise instead of writing another fix that assumes it.
33
+ - **Minimize Reader Load.** Reviewing or shaping code that's hard to trace. Count layers and hidden state, collapse one-caller wrappers, shrink mutable scope.
34
+ - **Outcome-Oriented Execution.** Planned rewrites and migrations with explicit phase boundaries. Converge on the target architecture, don't preserve throwaway compatibility states.
35
+ - **Experience First.** Product, UX, or feature-scope tradeoffs. Choose user delight over implementation convenience.
36
+ - **Exhaust the Design Space.** A novel interaction or architectural decision with no precedent. Build 2-3 competing prototypes and compare before committing.
37
+
38
+ **Architecture**
39
+
40
+ - **Model the Domain.** Writing stateful logic, or code that branches a lot or repeats a shape assumption across files. Encode the domain in a structure (state machine, typed model, table or registry, reducer, boundary, the right collection) instead of scattered conditionals.
41
+ - **Type System Discipline.** Designing types or a signature in any typed language. Make illegal states unrepresentable, brand primitives, parse external data at boundaries.
42
+ - **Make Operations Idempotent.** Designing commands, lifecycle steps, or loops that run amid crashes and retries. Converge to the same end state.
43
+ - **Migrate Callers Then Delete Legacy APIs.** Introducing a new internal API while old callers exist. Migrate and delete in one wave.
44
+ - **Separate Before Serializing Shared State.** Concurrent actors might write the same file, branch, key, or object. Eliminate the sharing first.
45
+
46
+ **Verification**
47
+
48
+ - **Sequence Work into Verifiable Units.** Multi-step work (sweeps, migrations, runs of similar edits). Break work into small units that each end in a check, verify each before the next, and order delivery so the sequence proves itself.
49
+ - **Test Behavior, Not Implementation.** Writing, changing, or keeping a test. Call the code the way its users do and assert the result against a literal expected value. If the test would still pass when every imported function returns `undefined`, rewrite the assertion or delete the test.
50
+ - **Explain the Number.** Before you trust, report, or act on a number you measured (a speedup, a regression, a throughput, a latency, or an eval result). Find what limits it, and rule out that it measured something other than the work you think.
51
+
52
+ **Delegation**
53
+
54
+ - **Guard the Context Window.** Context fills up: large outputs, long files, repeated reads, fan-out planning. Route bulk to subagents, keep summaries in the main thread.
55
+ - **Never Block on the Human.** Tempted to ask "should I do X?" on reversible work. Proceed, present the result, let the human course-correct.
56
+
57
+ **Meta**
58
+
59
+ - **Encode Lessons in Structure.** You catch yourself writing the same instruction a second time. Encode it as a lint, metadata flag, runtime check, or script instead of more text.
60
+
61
+ ## Autonomy
62
+
63
+ **No is an acceptable answer.** Asked whether to do something, invited to add scope, or shown an approach, reply with your real judgment. Decline, push back, or say "this doesn't earn its place" when true. A recommendation is a judgment, not a validation. Agreement is not the default, candor over sycophancy.
64
+
65
+ Forked from pstack's poteto-mode skill by Lauren Tan, MIT Licence. Source: cursor/plugins at commit dbf49813a39c1a5389c5e28cb8e4c1c2379e2454, path pstack/skills/poteto-mode/SKILL.md. Every rule above is that skill's own text, trimmed. Omitted because Tersio's caveman and ponytail blocks already inject them: the Laziness Protocol, Subtract Before You Add, Boundary Discipline, Prove It Works, and Fix Root Causes entries, plus the Comments section. Omitted because they route to skills or playbooks Tersio does not ship: the Playbooks table, the Subagents section, and the remaining trigger entries. Omitted as reply-shape rules that fight caveman: Writing the reply, and the instruction to name each principle in the reply. Omitted because their triggers fire on ordinary work and ponytail owns the size of a change: the Redesign from First Principles and Build the Lever entries. The precedence lines are Tersio's own.
@@ -2,7 +2,7 @@
2
2
  import { mkdtempSync, rmSync, writeFileSync } from 'node:fs';
3
3
  import { tmpdir } from 'node:os';
4
4
  import { join } from 'node:path';
5
- import { resolveRtkBinary } from '../lib/utils.ts';
5
+ import { resolveRtkBinary } from '../shared/utils.ts';
6
6
  import { readRtkDefault } from '../shared/plugin-settings.ts';
7
7
  import { getSharedComboState, lastCustomValue, sessionEntries } from '../shared/session-state.ts';
8
8
  import type { ExtensionApi, ExtensionCtx, SessionEntry, ToolResultEvent } from '../shared/types.ts';
@@ -43,7 +43,6 @@ export function rtkActive(entries: SessionEntry[] | null | undefined): boolean {
43
43
  /** The text in a result's content parts, or null when the parts are not plain text. */
44
44
  export function filterableText(parts: unknown): string | null {
45
45
  if (!Array.isArray(parts) || parts.length === 0) return null;
46
- // An image or a mixed result is not text to filter.
47
46
  if (parts.some((part) => (part as { type?: unknown } | null)?.type !== 'text')) return null;
48
47
  return parts.map((part) => (part as { text?: unknown } | null)?.text ?? '').join('\n');
49
48
  }
@@ -1,7 +1,7 @@
1
1
  import { getSharedComboState, hasModeState, isOmpSubagentPrompt, lastCustomValue, reconcileSharedComboEntries, sessionEntries, setSharedComboListener, setSharedComboMode, systemPromptIncludes } from '../shared/session-state.ts';
2
2
  import { injectPromptText, onHostEvent, setExtensionLabel, stringArrayToolParams } from '../shared/host.ts';
3
3
  import { readRtkDefault } from '../shared/plugin-settings.ts';
4
- import { resolveRtkBinary } from '../lib/utils.ts';
4
+ import { resolveRtkBinary } from '../shared/utils.ts';
5
5
  import { announceStatus } from '../shared/status.ts';
6
6
  import type { ExtensionApi, ExtensionCtx, SessionEntry, SystemPromptEvent } from '../shared/types.ts';
7
7
 
@@ -23,8 +23,8 @@ function setRtkProcessEnabled(enabled: boolean): void {
23
23
  else process.env.RTK_DISABLED = '1';
24
24
  }
25
25
 
26
- // rtk's awareness contract: state the output contract, never tell the model to type `rtk`.
27
- const RTK_PROMPT = `RTK guidance active. Bash output, and large grep and glob results, are condensed to save tokens: every signal kept, costly noise dropped. Treat a condensed result as complete. Re-run something unfiltered only when its result is empty, garbled, or contradicts its exit code.`;
26
+ // rtk's contract: name the rewritten surface, never the binary — a hand-typed `rtk` fights the rewrite.
27
+ const RTK_PROMPT = `RTK guidance active. What you run is condensed automatically: searches (grep, rg), listings (ls, find, cat as read), package and test runs (bun, npm run, pnpm run), version control (git), and type and lint checks (tsc, eslint). Every signal kept, costly noise dropped. Treat a condensed result as complete. Re-run something unfiltered only when its result is empty, garbled, or contradicts its exit code. A command with no condensed form passes through unchanged.`;
28
28
 
29
29
  export default function rtkSessionExtension(pi: ExtensionApi): void {
30
30
  let enabled = DEFAULT_ENABLED;
@@ -44,7 +44,7 @@ export default function rtkSessionExtension(pi: ExtensionApi): void {
44
44
 
45
45
  setExtensionLabel(pi, 'RTK session toggle');
46
46
 
47
- // Live mirror: a /tersio or /combo switch publishes shared state — adopt it at once so the next turn and the rtk_run gate see it, no reload.
47
+ // Live mirror: a /tersio or /combo switch publishes shared state — adopt it once so the next turn and rtk_run see it.
48
48
  function syncFromShared(state: { rtk: string }): void {
49
49
  enabled = state.rtk === 'on';
50
50
  setRtkProcessEnabled(enabled);
@@ -102,7 +102,7 @@ export default function rtkSessionExtension(pi: ExtensionApi): void {
102
102
  const entries = sessionEntries(ctx);
103
103
  // A subagent's branch has no Tersio entries; reconciling it would wipe shared state.
104
104
  if (hasModeState(entries)) reconcileSharedComboEntries(entries);
105
- // Persisted session state wins; a fresh session falls back to the installer/user-configured default (off unless configured).
105
+ // Persisted session state wins; a fresh session falls back to the installer/user default (off unless configured).
106
106
  const persisted = resolveEnabled(entries);
107
107
  enabled = typeof persisted === 'boolean' ? persisted : enabled || readRtkDefault();
108
108
  setRtkProcessEnabled(enabled);
@@ -1,15 +1,19 @@
1
1
  // OpenCode plugin entry: maps its plugin API onto the shared ExtensionApi the mode extensions consume.
2
2
  import { execFile } from 'node:child_process';
3
+ import { readFileSync, readdirSync } from 'node:fs';
4
+ import { basename, dirname, join } from 'node:path';
5
+ import { fileURLToPath } from 'node:url';
3
6
  import type { Plugin } from '@opencode/plugin';
4
- import cavemanSessionExtension from '../caveman-session/index.ts';
5
- import rtkSessionExtension from '../rtk-session/index.ts';
6
- import rtkFilterExtension, { FILTER_BY_TOOL, MIN_FILTER_BYTES, filterThroughRtk, filterableText, rtkActive } from '../rtk-filter/index.ts';
7
- import comboToggleExtension from '../combo-toggle/index.ts';
8
- import tersioCommandsExtension from '../tersio-commands/index.ts';
9
- import { getSharedComboState } from '../shared/session-state.ts';
10
- import { formatStatus } from '../shared/status.ts';
11
- import { resolveRtkBinary } from '../lib/utils.ts';
12
- import type { ExtensionApi, ExtensionCtx, InputEvent, SessionEntry, SystemPromptEvent } from '../shared/types.ts';
7
+ import cavemanSessionExtension from './caveman/index.ts';
8
+ import pstackSessionExtension from './pstack/index.ts';
9
+ import rtkSessionExtension from './rtk/session.ts';
10
+ import rtkFilterExtension, { FILTER_BY_TOOL, MIN_FILTER_BYTES, filterThroughRtk, filterableText, rtkActive } from './rtk/filter.ts';
11
+ import comboToggleExtension from './combo/index.ts';
12
+ import tersioCommandsExtension from './commands/index.ts';
13
+ import { getSharedComboState } from './shared/session-state.ts';
14
+ import { formatStatus } from './shared/status.ts';
15
+ import { resolveRtkBinary } from './shared/utils.ts';
16
+ import type { ExtensionApi, ExtensionCtx, InputEvent, SessionEntry, SystemPromptEvent } from './shared/types.ts';
13
17
 
14
18
  interface CommandConfig {
15
19
  description: string;
@@ -40,8 +44,7 @@ const plugin: Plugin.Plugin = {
40
44
  const loadEntries = async (sid: string): Promise<SessionEntry[]> => {
41
45
  const own = ((await ctx.storage.get(`entries:${sid}`)) as SessionEntry[] | undefined) ?? [];
42
46
  if (own.length) return own;
43
- // Pre-scoped installs kept one global list; adopt it once, then diverge.
44
- // Clear it after adoption so it can never shadow saved defaults later.
47
+ // Pre-scoped installs kept one global list; adopt it once and clear it so it cannot shadow saved defaults later.
45
48
  const legacy = ((await ctx.storage.get('entries')) as SessionEntry[] | undefined) ?? [];
46
49
  if (legacy.length) void ctx.storage.set('entries', []).catch(() => {});
47
50
  return [...legacy];
@@ -122,7 +125,7 @@ const plugin: Plugin.Plugin = {
122
125
  },
123
126
  };
124
127
 
125
- for (const extension of [cavemanSessionExtension, rtkSessionExtension, rtkFilterExtension, comboToggleExtension, tersioCommandsExtension]) extension(host);
128
+ for (const extension of [cavemanSessionExtension, pstackSessionExtension, rtkSessionExtension, rtkFilterExtension, comboToggleExtension, tersioCommandsExtension]) extension(host);
126
129
 
127
130
  // The first hook of an unseen session runs its session_start handlers.
128
131
  const sidOf = (value: unknown): string | undefined =>
@@ -138,8 +141,7 @@ const plugin: Plugin.Plugin = {
138
141
  if (sid) await ensureSession(sid);
139
142
  const text = typeof input.prompt?.text === 'string' ? input.prompt.text : '';
140
143
  const args = text.includes(' ') ? text.slice(text.indexOf(' ') + 1).trim() : '';
141
- // The shared handlers report through ui.notify, which has no OpenCode
142
- // surface. Collect it and send it back so a command never answers blank.
144
+ // Shared handlers notify via ui.notify, which OpenCode lacks; echo it back so a command does not answer blank.
143
145
  const notes: string[] = [];
144
146
  const cmdCtx: ExtensionCtx = {
145
147
  ...stubCtx(sid),
@@ -175,6 +177,15 @@ const plugin: Plugin.Plugin = {
175
177
  }
176
178
  });
177
179
 
180
+ // The tree ships the ruleset beside the extensions, and OpenCode reads no node_modules from a plugin dir.
181
+ try {
182
+ await ctx.skill.transform((editor) => {
183
+ for (const skill of bundledSkills()) editor.add(skill as never);
184
+ });
185
+ } catch {
186
+ // Hosts without the skill transform keep the ruleset in the prompt alone.
187
+ }
188
+
178
189
  await ctx.session.hook('context', async (event) => {
179
190
  const sid = sidOf((event as { sessionID?: unknown }).sessionID);
180
191
  if (sid) await ensureSession(sid);
@@ -205,6 +216,10 @@ const plugin: Plugin.Plugin = {
205
216
  if (tool !== 'bash' && tool !== 'shell') return;
206
217
  const input = event.input as { command?: unknown } | null | undefined;
207
218
  if (typeof input?.command !== 'string' || !input.command) return;
219
+ // /rtk off must stop the rewrite too, or the mode switch only silences the prompt block.
220
+ const sid = typeof (event as { sessionID?: unknown }).sessionID === 'string' ? (event as { sessionID: string }).sessionID : activeSid;
221
+ if (sid !== undefined) await ensureSession(sid);
222
+ if (!rtkActive(sid === undefined ? undefined : sessionEntriesOf(sid))) return;
208
223
  const rewritten = await rewriteCommand(input.command);
209
224
  if (rewritten) input.command = rewritten;
210
225
  });
@@ -246,16 +261,66 @@ function execRtk(cmd: string, args: string[], opts?: { signal?: AbortSignal; tim
246
261
  });
247
262
  }
248
263
 
264
+ /** `rtk rewrite` exit codes: 0 rewrite found, 1 no equivalent, 3 advisory; the code is the contract. */
265
+ const RTK_REWRITE_FOUND = 0;
266
+ const RTK_REWRITE_ADVISORY = 3;
267
+
268
+ /** The bundled ruleset's skill dirs, beside the plugin entry: OpenCode reads no node_modules from a plugin. */
269
+ const PONYTAIL_BUNDLE_SKILLS = join(dirname(fileURLToPath(import.meta.url)), 'ponytail-bundle', 'skills');
270
+
271
+ /** Frontmatter a SKILL.md carries: flat keys only, the shape every bundled skill uses. */
272
+ function parseSkillFile(file: string): { id: string; name: string; description: string; path: string; content: string } | null {
273
+ let raw: string;
274
+ try { raw = readFileSync(file, 'utf8'); } catch { return null; }
275
+ const match = raw.match(/^---\r?\n([\s\S]*?)\r?\n---[^\S\n]*\r?\n?([\s\S]*)$/);
276
+ if (!match) return null;
277
+ const field = (key: string): string | undefined => {
278
+ const lines = match[1].split(/\r?\n/);
279
+ const at = lines.findIndex((line) => line.startsWith(`${key}:`));
280
+ if (at === -1) return undefined;
281
+ const value = lines[at].slice(key.length + 1).trim();
282
+ if (value[0] !== '>' && value[0] !== '|') return value;
283
+ // Folded (`>`) joins the indented lines; the next unindented key ends the block.
284
+ const block: string[] = [];
285
+ for (const line of lines.slice(at + 1)) {
286
+ if (line.trim() !== '' && !/^\s/.test(line)) break;
287
+ block.push(line.trim());
288
+ }
289
+ while (block.length > 0 && block[block.length - 1] === '') block.pop();
290
+ return block.join(value[0] === '|' ? '\n' : ' ');
291
+ };
292
+ const id = field('name') ?? basename(dirname(file));
293
+ return { id, name: id, description: field('description') ?? '', path: file, content: match[2] };
294
+ }
295
+
296
+ /** Every bundled skill, or an empty list when the installer has not bundled them. */
297
+ function bundledSkills(): ReturnType<typeof parseSkillFile>[] {
298
+ let entries;
299
+ try { entries = readdirSync(PONYTAIL_BUNDLE_SKILLS, { withFileTypes: true }); } catch { return []; }
300
+ const out: ReturnType<typeof parseSkillFile>[] = [];
301
+ for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) {
302
+ if (!entry.isDirectory()) continue;
303
+ const skill = parseSkillFile(join(PONYTAIL_BUNDLE_SKILLS, entry.name, 'SKILL.md'));
304
+ if (skill) out.push(skill);
305
+ }
306
+ return out;
307
+ }
308
+
249
309
  function rewriteCommand(command: string): Promise<string | null> {
250
310
  const bin = resolveRtkBinary();
251
311
  if (!bin) return Promise.resolve(null);
252
312
  return new Promise((resolve) => {
253
313
  execFile(bin, ['rewrite', command], { timeout: 5000 }, (error, stdout) => {
254
314
  // A kill or timeout means untrustworthy output; pass the command through.
255
- const err = error as { killed?: boolean; signal?: unknown } | null;
315
+ const err = error as { killed?: boolean; signal?: unknown; code?: unknown } | null;
256
316
  if (err && (err.killed || err.signal)) return resolve(null);
317
+ const code = typeof err?.code === 'number' ? err.code : RTK_REWRITE_FOUND;
318
+ // Exit 1 is the only "no equivalent" answer, so anything else with output rewrites.
319
+ if (code === 1) return resolve(null);
257
320
  const out = String(stdout).trim();
258
- resolve(out && out !== command ? out : null);
321
+ if (!out || out === command) return resolve(null);
322
+ if (code !== RTK_REWRITE_FOUND && code !== RTK_REWRITE_ADVISORY) return resolve(null);
323
+ resolve(out);
259
324
  });
260
325
  });
261
326
  }