supipowers 1.2.6 → 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (137) hide show
  1. package/README.md +118 -56
  2. package/bin/install.ts +48 -128
  3. package/package.json +11 -3
  4. package/skills/code-review/SKILL.md +137 -40
  5. package/skills/context-mode/SKILL.md +67 -56
  6. package/skills/creating-supi-agents/SKILL.md +204 -0
  7. package/skills/debugging/SKILL.md +86 -40
  8. package/skills/fix-pr/SKILL.md +96 -65
  9. package/skills/planning/SKILL.md +103 -46
  10. package/skills/qa-strategy/SKILL.md +68 -46
  11. package/skills/receiving-code-review/SKILL.md +60 -53
  12. package/skills/release/SKILL.md +111 -39
  13. package/skills/tdd/SKILL.md +118 -67
  14. package/skills/verification/SKILL.md +71 -37
  15. package/src/bootstrap.ts +27 -5
  16. package/src/commands/agents.ts +249 -0
  17. package/src/commands/ai-review.ts +1113 -0
  18. package/src/commands/config.ts +224 -95
  19. package/src/commands/doctor.ts +19 -13
  20. package/src/commands/fix-pr.ts +8 -11
  21. package/src/commands/generate.ts +200 -0
  22. package/src/commands/model-picker.ts +5 -15
  23. package/src/commands/model.ts +4 -5
  24. package/src/commands/optimize-context.ts +202 -0
  25. package/src/commands/plan.ts +148 -92
  26. package/src/commands/qa.ts +14 -23
  27. package/src/commands/release.ts +504 -275
  28. package/src/commands/review.ts +643 -86
  29. package/src/commands/status.ts +44 -17
  30. package/src/commands/supi.ts +69 -41
  31. package/src/commands/update.ts +57 -2
  32. package/src/config/defaults.ts +6 -39
  33. package/src/config/loader.ts +388 -40
  34. package/src/config/model-resolver.ts +26 -22
  35. package/src/config/schema.ts +113 -48
  36. package/src/context/analyzer.ts +61 -2
  37. package/src/context/optimizer.ts +199 -0
  38. package/src/context-mode/compressor.ts +14 -11
  39. package/src/context-mode/detector.ts +16 -54
  40. package/src/context-mode/event-extractor.ts +45 -16
  41. package/src/context-mode/event-store.ts +225 -16
  42. package/src/context-mode/hooks.ts +195 -22
  43. package/src/context-mode/knowledge/chunker.ts +235 -0
  44. package/src/context-mode/knowledge/store.ts +187 -0
  45. package/src/context-mode/routing.ts +12 -23
  46. package/src/context-mode/sandbox/executor.ts +183 -0
  47. package/src/context-mode/sandbox/runners.ts +40 -0
  48. package/src/context-mode/snapshot-builder.ts +243 -7
  49. package/src/context-mode/tools.ts +440 -0
  50. package/src/context-mode/web/fetcher.ts +117 -0
  51. package/src/context-mode/web/html-to-md.ts +293 -0
  52. package/src/debug/logger.ts +107 -0
  53. package/src/deps/registry.ts +0 -20
  54. package/src/docs/drift.ts +454 -0
  55. package/src/fix-pr/fetch-comments.ts +66 -0
  56. package/src/git/commit-msg.ts +2 -1
  57. package/src/git/commit.ts +123 -141
  58. package/src/git/conventions.ts +2 -2
  59. package/src/git/status.ts +4 -1
  60. package/src/lsp/bridge.ts +138 -12
  61. package/src/planning/approval-flow.ts +125 -19
  62. package/src/planning/plan-writer-prompt.ts +4 -11
  63. package/src/planning/planning-ask-tool.ts +81 -0
  64. package/src/planning/prompt-builder.ts +9 -169
  65. package/src/planning/system-prompt.ts +290 -0
  66. package/src/platform/omp.ts +50 -4
  67. package/src/platform/progress.ts +182 -0
  68. package/src/platform/test-utils.ts +4 -1
  69. package/src/platform/tui-colors.ts +30 -0
  70. package/src/platform/types.ts +1 -0
  71. package/src/qa/detect-app-type.ts +102 -0
  72. package/src/qa/discover-routes.ts +353 -0
  73. package/src/quality/ai-session.ts +96 -0
  74. package/src/quality/ai-setup.ts +86 -0
  75. package/src/quality/gates/ai-review.ts +129 -0
  76. package/src/quality/gates/build.ts +8 -0
  77. package/src/quality/gates/command.ts +150 -0
  78. package/src/quality/gates/format.ts +28 -0
  79. package/src/quality/gates/lint.ts +22 -0
  80. package/src/quality/gates/lsp-diagnostics.ts +84 -0
  81. package/src/quality/gates/test-suite.ts +8 -0
  82. package/src/quality/gates/typecheck.ts +22 -0
  83. package/src/quality/registry.ts +25 -0
  84. package/src/quality/review-gates.ts +33 -0
  85. package/src/quality/runner.ts +268 -0
  86. package/src/quality/schemas.ts +48 -0
  87. package/src/quality/setup.ts +227 -0
  88. package/src/release/changelog.ts +7 -3
  89. package/src/release/channels/custom.ts +43 -0
  90. package/src/release/channels/gitea.ts +35 -0
  91. package/src/release/channels/github.ts +35 -0
  92. package/src/release/channels/gitlab.ts +35 -0
  93. package/src/release/channels/registry.ts +52 -0
  94. package/src/release/channels/types.ts +27 -0
  95. package/src/release/detector.ts +10 -63
  96. package/src/release/executor.ts +61 -51
  97. package/src/release/prompt.ts +38 -38
  98. package/src/release/version.ts +129 -10
  99. package/src/review/agent-loader.ts +331 -0
  100. package/src/review/consolidator.ts +180 -0
  101. package/src/review/default-agents/correctness.md +72 -0
  102. package/src/review/default-agents/maintainability.md +64 -0
  103. package/src/review/default-agents/security.md +67 -0
  104. package/src/review/fixer.ts +219 -0
  105. package/src/review/multi-agent-runner.ts +135 -0
  106. package/src/review/output.ts +147 -0
  107. package/src/review/prompts/agent-review-wrapper.md +36 -0
  108. package/src/review/prompts/fix-findings.md +32 -0
  109. package/src/review/prompts/fix-output-schema.md +18 -0
  110. package/src/review/prompts/invalid-output-retry.md +22 -0
  111. package/src/review/prompts/output-instructions.md +14 -0
  112. package/src/review/prompts/review-output-schema.md +38 -0
  113. package/src/review/prompts/single-review.md +53 -0
  114. package/src/review/prompts/validation-review.md +30 -0
  115. package/src/review/runner.ts +128 -0
  116. package/src/review/scope.ts +353 -0
  117. package/src/review/template.ts +15 -0
  118. package/src/review/types.ts +296 -0
  119. package/src/review/validator.ts +160 -0
  120. package/src/storage/plans.ts +5 -3
  121. package/src/storage/reports.ts +50 -7
  122. package/src/storage/review-sessions.ts +117 -0
  123. package/src/text.ts +19 -0
  124. package/src/types.ts +336 -26
  125. package/src/utils/paths.ts +39 -0
  126. package/src/visual/companion.ts +5 -3
  127. package/src/visual/start-server.ts +101 -0
  128. package/src/visual/stop-server.ts +39 -0
  129. package/bin/ctx-mode-wrapper.mjs +0 -66
  130. package/src/config/profiles.ts +0 -64
  131. package/src/context-mode/installer.ts +0 -38
  132. package/src/quality/ai-review-gate.ts +0 -43
  133. package/src/quality/gate-runner.ts +0 -67
  134. package/src/quality/lsp-gate.ts +0 -24
  135. package/src/quality/test-gate.ts +0 -39
  136. package/src/visual/scripts/start-server.sh +0 -98
  137. package/src/visual/scripts/stop-server.sh +0 -21
@@ -7,7 +7,13 @@ const HTTP_PATTERNS = [
7
7
  /^\s*wget\s/,
8
8
  /\bcurl\s+(-[a-zA-Z]*\s+)*https?:\/\//,
9
9
  /\bwget\s+(-[a-zA-Z]*\s+)*https?:\/\//,
10
- ];
10
+ // Inline HTTP patterns
11
+ /\bfetch\s*\(/,
12
+ /\brequests\.(get|post|put|delete|patch)\s*\(/,
13
+ /\bhttp\.(get|request)\s*\(/,
14
+ /\burllib\.request/,
15
+ /\bInvoke-WebRequest/,
16
+ ];
11
17
 
12
18
  /** Bash commands that are search/find operations */
13
19
  const BASH_SEARCH_PATTERNS = [
@@ -41,10 +47,10 @@ export function isBashSearchCommand(command: unknown): boolean {
41
47
  return BASH_SEARCH_PATTERNS.some((p) => p.test(command));
42
48
  }
43
49
 
44
- /** Check if a Read call is a full-file read (no limit/offset = likely analysis, not edit prep) */
50
+ /** Check if a Read call is a full-file read (no limit/offset/sel = likely analysis, not edit prep) */
45
51
  export function isFullFileRead(input: Record<string, unknown> | undefined): boolean {
46
52
  if (!input) return true;
47
- return input.limit == null && input.offset == null;
53
+ return input.limit == null && input.offset == null && input.sel == null;
48
54
  }
49
55
 
50
56
  /** Block result returned by routing functions */
@@ -57,14 +63,13 @@ export interface BlockResult {
57
63
  export function routeToolCall(
58
64
  toolName: string,
59
65
  input: Record<string, unknown> | undefined,
60
- status: ContextModeStatus,
66
+ _status: ContextModeStatus,
61
67
  options: { enforceRouting: boolean; blockHttpCommands: boolean },
62
68
  ): BlockResult | undefined {
63
- if (!status.available) return undefined;
69
+ // Native tools are always available no availability check needed.
64
70
 
65
71
  // Grep → block, redirect to ctx_search
66
72
  if (options.enforceRouting && toolName === "grep") {
67
- if (!status.tools.ctxSearch) return undefined;
68
73
  return {
69
74
  block: true,
70
75
  reason:
@@ -75,7 +80,6 @@ export function routeToolCall(
75
80
 
76
81
  // Find/Glob → block, redirect to ctx_execute or ctx_batch_execute
77
82
  if (options.enforceRouting && toolName === "find") {
78
- if (!status.tools.ctxExecute) return undefined;
79
83
  return {
80
84
  block: true,
81
85
  reason:
@@ -86,7 +90,6 @@ export function routeToolCall(
86
90
 
87
91
  // Fetch/WebFetch → block, redirect to ctx_fetch_and_index
88
92
  if (toolName === "fetch" || toolName === "web_fetch") {
89
- if (!status.tools.ctxFetchAndIndex) return undefined;
90
93
  return {
91
94
  block: true,
92
95
  reason:
@@ -95,25 +98,12 @@ export function routeToolCall(
95
98
  };
96
99
  }
97
100
 
98
- // Read (full-file, no limit/offset) → block, redirect to ctx_execute_file
99
- if (options.enforceRouting && toolName === "read") {
100
- if (!status.tools.ctxExecuteFile) return undefined;
101
- if (!isFullFileRead(input)) return undefined;
102
- return {
103
- block: true,
104
- reason:
105
- "Use ctx_execute_file(path, language, code) for file analysis instead of Read. " +
106
- "If you need to Read before editing, re-call with a limit parameter.",
107
- };
108
- }
109
-
110
101
  // Bash routing
111
102
  if (toolName === "bash") {
112
103
  const command = input?.command;
113
104
 
114
105
  // Bash search commands → block, redirect to ctx_execute
115
106
  if (options.enforceRouting && isBashSearchCommand(command)) {
116
- if (!status.tools.ctxExecute) return undefined;
117
107
  return {
118
108
  block: true,
119
109
  reason:
@@ -124,7 +114,6 @@ export function routeToolCall(
124
114
 
125
115
  // Bash HTTP commands → block, redirect to ctx_fetch_and_index
126
116
  if (options.blockHttpCommands && isHttpCommand(command)) {
127
- if (!status.tools.ctxFetchAndIndex) return undefined;
128
117
  return {
129
118
  block: true,
130
119
  reason:
@@ -135,4 +124,4 @@ export function routeToolCall(
135
124
  }
136
125
 
137
126
  return undefined;
138
- }
127
+ }
@@ -0,0 +1,183 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+ import fs from "node:fs";
5
+ import { getRunner } from "./runners.js";
6
+
7
+ export interface ExecuteOptions {
8
+ timeout?: number;
9
+ background?: boolean;
10
+ cwd?: string;
11
+ env?: Record<string, string>;
12
+ }
13
+
14
+ export interface ExecuteResult {
15
+ stdout: string;
16
+ stderr: string;
17
+ exitCode: number;
18
+ duration: number;
19
+ }
20
+
21
+ const DEFAULT_TIMEOUT = 30_000;
22
+
23
+ export async function executeCode(
24
+ language: string,
25
+ code: string,
26
+ options?: ExecuteOptions,
27
+ ): Promise<ExecuteResult> {
28
+ const runner = getRunner(language);
29
+ const opts = options ?? {};
30
+ const timeout = opts.timeout ?? DEFAULT_TIMEOUT;
31
+ const cwd = opts.cwd ?? process.cwd();
32
+
33
+ const id = randomUUID();
34
+ const srcPath = path.join(os.tmpdir(), `ctx-exec-${id}${runner.fileExt}`);
35
+ const outPath = runner.needsCompile
36
+ ? path.join(os.tmpdir(), `ctx-exec-${id}`)
37
+ : undefined;
38
+
39
+ fs.writeFileSync(srcPath, code);
40
+ const start = performance.now();
41
+
42
+ try {
43
+ // Compile step for compiled languages
44
+ if (runner.needsCompile && runner.compileCmd && outPath) {
45
+ const compileArgs = runner.compileCmd(srcPath, outPath);
46
+ const compileProc = Bun.spawn(compileArgs, {
47
+ stdout: "pipe",
48
+ stderr: "pipe",
49
+ cwd,
50
+ });
51
+ const [compStdout, compStderr] = await Promise.all([
52
+ new Response(compileProc.stdout).text(),
53
+ new Response(compileProc.stderr).text(),
54
+ ]);
55
+ const compileExit = await compileProc.exited;
56
+ if (compileExit !== 0) {
57
+ return {
58
+ stdout: compStdout,
59
+ stderr: compStderr,
60
+ exitCode: compileExit,
61
+ duration: performance.now() - start,
62
+ };
63
+ }
64
+ }
65
+
66
+ const execPath = outPath ?? srcPath;
67
+ const args = runner.needsCompile ? [execPath] : [...runner.binary, execPath];
68
+
69
+ const proc = Bun.spawn(args, {
70
+ stdout: "pipe",
71
+ stderr: "pipe",
72
+ cwd,
73
+ env: { ...process.env, ...opts.env },
74
+ });
75
+
76
+ if (opts.background) {
77
+ // Wait up to timeout collecting partial output, then return without killing
78
+ const partial = await collectWithTimeout(proc, timeout);
79
+ return {
80
+ stdout: partial.stdout,
81
+ stderr: partial.stderr,
82
+ exitCode: 0,
83
+ duration: performance.now() - start,
84
+ };
85
+ }
86
+
87
+ // Foreground: enforce hard timeout
88
+ const result = await raceTimeout(proc, timeout);
89
+ return {
90
+ stdout: result.stdout,
91
+ stderr: result.stderr,
92
+ exitCode: result.exitCode,
93
+ duration: performance.now() - start,
94
+ };
95
+ } finally {
96
+ cleanup(srcPath);
97
+ if (outPath) cleanup(outPath);
98
+ }
99
+ }
100
+
101
+ /** Race process completion against a timeout. Kills on timeout. */
102
+ async function raceTimeout(
103
+ proc: ReturnType<typeof Bun.spawn>,
104
+ timeout: number,
105
+ ): Promise<{ stdout: string; stderr: string; exitCode: number }> {
106
+ // Start reading streams immediately — they can only be consumed once
107
+ const stdoutP = new Response(proc.stdout as ReadableStream<Uint8Array> | undefined).text();
108
+ const stderrP = new Response(proc.stderr as ReadableStream<Uint8Array> | undefined).text();
109
+
110
+ const timedOut = await Promise.race([
111
+ proc.exited.then(() => false),
112
+ new Promise<boolean>((r) => setTimeout(() => r(true), timeout)),
113
+ ]);
114
+
115
+ if (timedOut) {
116
+ proc.kill("SIGKILL");
117
+ await proc.exited;
118
+ }
119
+
120
+ const [stdout, stderr] = await Promise.all([stdoutP, stderrP]);
121
+
122
+ if (timedOut) {
123
+ return {
124
+ stdout,
125
+ stderr: stderr + `\n[Timeout after ${timeout}ms]`,
126
+ exitCode: 124,
127
+ };
128
+ }
129
+
130
+ return { stdout, stderr, exitCode: await proc.exited };
131
+ }
132
+
133
+ /** Collect output for up to `ms` milliseconds without killing. */
134
+ async function collectWithTimeout(
135
+ proc: ReturnType<typeof Bun.spawn>,
136
+ ms: number,
137
+ ): Promise<{ stdout: string; stderr: string }> {
138
+ const chunks: { out: string[]; err: string[] } = { out: [], err: [] };
139
+ const decoder = new TextDecoder();
140
+
141
+ const readStream = async (
142
+ stream: ReadableStream<Uint8Array>,
143
+ bucket: string[],
144
+ ) => {
145
+ const reader = stream.getReader();
146
+ try {
147
+ while (true) {
148
+ const { done, value } = await reader.read();
149
+ if (done) break;
150
+ bucket.push(decoder.decode(value, { stream: true }));
151
+ }
152
+ } catch {
153
+ // Stream may error when we cancel — that's fine
154
+ } finally {
155
+ reader.releaseLock();
156
+ }
157
+ };
158
+
159
+ // Start reading both streams
160
+ const outP = readStream(proc.stdout as ReadableStream<Uint8Array>, chunks.out);
161
+ const errP = readStream(proc.stderr as ReadableStream<Uint8Array>, chunks.err);
162
+
163
+ // Wait for timeout, then return whatever we have
164
+ await Promise.race([
165
+ Promise.all([outP, errP]),
166
+ new Promise<void>((r) => setTimeout(r, ms)),
167
+ ]);
168
+
169
+ // proc.unref() is not available on Bun.spawn, but the process
170
+ // continues running since we never kill it.
171
+ return {
172
+ stdout: chunks.out.join(""),
173
+ stderr: chunks.err.join(""),
174
+ };
175
+ }
176
+
177
+ function cleanup(filePath: string): void {
178
+ try {
179
+ fs.unlinkSync(filePath);
180
+ } catch {
181
+ // Ignore — file may already be gone
182
+ }
183
+ }
@@ -0,0 +1,40 @@
1
+ export interface LanguageRunner {
2
+ binary: string[];
3
+ fileExt: string;
4
+ needsCompile?: boolean;
5
+ compileCmd?: (srcPath: string, outPath: string) => string[];
6
+ }
7
+
8
+ const RUNNERS: Record<string, LanguageRunner> = {
9
+ javascript: { binary: ["bun", "run"], fileExt: ".js" },
10
+ typescript: { binary: ["bun", "run"], fileExt: ".ts" },
11
+ python: { binary: ["python3"], fileExt: ".py" },
12
+ shell: { binary: ["bash"], fileExt: ".sh" },
13
+ ruby: { binary: ["ruby"], fileExt: ".rb" },
14
+ go: { binary: ["go", "run"], fileExt: ".go" },
15
+ rust: {
16
+ binary: ["rustc"],
17
+ fileExt: ".rs",
18
+ needsCompile: true,
19
+ compileCmd: (src, out) => ["rustc", src, "-o", out],
20
+ },
21
+ php: { binary: ["php"], fileExt: ".php" },
22
+ perl: { binary: ["perl"], fileExt: ".pl" },
23
+ r: { binary: ["Rscript"], fileExt: ".R" },
24
+ elixir: { binary: ["elixir"], fileExt: ".exs" },
25
+ };
26
+
27
+ export function getRunner(language: string): LanguageRunner {
28
+ const runner = RUNNERS[language];
29
+ if (!runner) {
30
+ const supported = getSupportedLanguages().join(", ");
31
+ throw new Error(
32
+ `Unsupported language: "${language}". Supported: ${supported}`,
33
+ );
34
+ }
35
+ return runner;
36
+ }
37
+
38
+ export function getSupportedLanguages(): string[] {
39
+ return Object.keys(RUNNERS).sort();
40
+ }
@@ -9,19 +9,255 @@ const CAPS = {
9
9
  git: 5,
10
10
  };
11
11
 
12
+ /** Escape all 5 XML special characters in user data */
13
+ function escapeXML(str: string): string {
14
+ return str
15
+ .replace(/&/g, "&amp;")
16
+ .replace(/</g, "&lt;")
17
+ .replace(/>/g, "&gt;")
18
+ .replace(/"/g, "&quot;")
19
+ .replace(/'/g, "&apos;");
20
+ }
21
+
22
+ interface SnapshotOpts {
23
+ compactCount?: number;
24
+ searchTool?: string;
25
+ searchAvailable?: boolean;
26
+ }
27
+
12
28
  /** Build a resume snapshot from tracked events for a session */
13
- export function buildResumeSnapshot(eventStore: EventStore, sessionId: string): string {
29
+ export function buildResumeSnapshot(
30
+ eventStore: EventStore,
31
+ sessionId: string,
32
+ opts?: SnapshotOpts,
33
+ ): string {
14
34
  const counts = eventStore.getEventCounts(sessionId);
15
35
  const hasAnyEvents = Object.values(counts).some((c) => c > 0);
16
36
  if (!hasAnyEvents) return "";
17
37
 
38
+ if (opts?.searchAvailable) {
39
+ return buildReferenceSnapshot(eventStore, sessionId, opts);
40
+ }
41
+ return buildFallbackSnapshot(eventStore, sessionId);
42
+ }
43
+
44
+ // ---------------------------------------------------------------------------
45
+ // Reference-based format (supi-context-mode MCP available)
46
+ // ---------------------------------------------------------------------------
47
+
48
+ function buildReferenceSnapshot(eventStore: EventStore, sessionId: string, opts: SnapshotOpts): string {
49
+ const compactCount = opts.compactCount ?? 0;
50
+ const now = new Date().toISOString();
51
+ const sections: string[] = [
52
+ `<session_knowledge compact_count="${compactCount}" generated_at="${escapeXML(now)}">`,
53
+ " <how_to_search>",
54
+ " Each section below contains a summary of prior work.",
55
+ " For FULL DETAILS, run the exact tool call shown under each section.",
56
+ " Do NOT ask the user to re-explain prior work. Search first.",
57
+ " </how_to_search>",
58
+ ];
59
+
60
+ let hasSections = false;
61
+
62
+ // --- rules ---
63
+ const ruleEvents = eventStore.getEvents(sessionId, { categories: ["rule"] });
64
+ if (ruleEvents.length > 0) {
65
+ const files = new Set<string>();
66
+ for (const r of ruleEvents) {
67
+ const data = safeParse(r.data);
68
+ const file = typeof data?.file === "string" ? data.file : typeof data?.path === "string" ? data.path : null;
69
+ if (file) files.add(file);
70
+ }
71
+ if (files.size > 0) {
72
+ const fileList = [...files];
73
+ sections.push("");
74
+ sections.push(` <rules>`);
75
+ sections.push(` Loaded ${fileList.length} project rule files: ${fileList.map(escapeXML).join(", ")}`);
76
+ sections.push(` For full details:`);
77
+ sections.push(` ctx_search(queries: [${fileList.map((f) => `"${escapeXML(f)}"`).join(", ")}], source: "session-events")`);
78
+ sections.push(` </rules>`);
79
+ hasSections = true;
80
+ }
81
+ }
82
+
83
+ // --- files ---
84
+ const fileEvents = eventStore.getEvents(sessionId, { categories: ["file"], limit: 200 });
85
+ if (fileEvents.length > 0) {
86
+ const edited = new Set<string>();
87
+ const read = new Set<string>();
88
+ for (const f of fileEvents) {
89
+ const data = safeParse(f.data);
90
+ const p = typeof data?.path === "string" ? data.path : null;
91
+ if (!p) continue;
92
+ if (data?.op === "edit" || data?.op === "write") edited.add(p);
93
+ else if (data?.op === "read") read.add(p);
94
+ }
95
+ if (edited.size > 0 || read.size > 0) {
96
+ sections.push("");
97
+ sections.push(` <files count="${edited.size + read.size}">`);
98
+ if (edited.size > 0) sections.push(` Edited: ${[...edited].map(escapeXML).join(", ")}`);
99
+ if (read.size > 0) sections.push(` Read: ${[...read].map(escapeXML).join(", ")}`);
100
+ const queryPaths = [...edited, ...read].slice(0, 5);
101
+ sections.push(` For full details:`);
102
+ sections.push(` ctx_search(queries: [${queryPaths.map((p) => `"${escapeXML(p)}"`).join(", ")}], source: "session-events")`);
103
+ sections.push(` </files>`);
104
+ hasSections = true;
105
+ }
106
+ }
107
+
108
+ // --- tasks ---
109
+ const tasks = eventStore.getEvents(sessionId, { categories: ["task"], limit: CAPS.tasks });
110
+ if (tasks.length > 0) {
111
+ const summaries: string[] = [];
112
+ for (const t of tasks) {
113
+ const data = safeParse(t.data);
114
+ const content = extractTaskContent(data);
115
+ if (content) summaries.push(escapeXML(content.slice(0, 100)));
116
+ }
117
+ if (summaries.length > 0) {
118
+ sections.push("");
119
+ sections.push(` <tasks>`);
120
+ for (const s of summaries) sections.push(` ${s}`);
121
+ sections.push(` For full details:`);
122
+ sections.push(` ctx_search(queries: ["task", "todo"], source: "session-events")`);
123
+ sections.push(` </tasks>`);
124
+ hasSections = true;
125
+ }
126
+ }
127
+
128
+ // --- decisions ---
129
+ const decisions = eventStore.getEvents(sessionId, { categories: ["decision"], limit: CAPS.decisions });
130
+ if (decisions.length > 0) {
131
+ const summaries: string[] = [];
132
+ for (const d of decisions) {
133
+ const data = safeParse(d.data);
134
+ const prompt = typeof data?.prompt === "string" ? data.prompt.slice(0, 100) : "";
135
+ if (prompt) summaries.push(escapeXML(prompt));
136
+ }
137
+ if (summaries.length > 0) {
138
+ sections.push("");
139
+ sections.push(` <decisions>`);
140
+ for (const s of summaries) sections.push(` ${s}`);
141
+ sections.push(` </decisions>`);
142
+ hasSections = true;
143
+ }
144
+ }
145
+
146
+ // --- errors ---
147
+ const errors = eventStore.getEvents(sessionId, { categories: ["error"], limit: CAPS.errors });
148
+ if (errors.length > 0) {
149
+ const summaries: string[] = [];
150
+ for (const e of errors) {
151
+ const data = safeParse(e.data);
152
+ const summary = formatErrorSummary(data);
153
+ if (summary) summaries.push(escapeXML(summary.slice(0, 150)));
154
+ }
155
+ if (summaries.length > 0) {
156
+ sections.push("");
157
+ sections.push(` <errors>`);
158
+ for (const s of summaries) sections.push(` ${s}`);
159
+ sections.push(` </errors>`);
160
+ hasSections = true;
161
+ }
162
+ }
163
+
164
+ // --- git ---
165
+ const gitEvents = eventStore.getEvents(sessionId, { categories: ["git"], limit: CAPS.git });
166
+ if (gitEvents.length > 0) {
167
+ const summaries: string[] = [];
168
+ for (const g of gitEvents) {
169
+ const data = safeParse(g.data);
170
+ const cmd = typeof data?.command === "string" ? data.command.slice(0, 100) : "";
171
+ if (cmd) summaries.push(escapeXML(cmd));
172
+ }
173
+ if (summaries.length > 0) {
174
+ sections.push("");
175
+ sections.push(` <git>`);
176
+ for (const s of summaries) sections.push(` ${s}`);
177
+ sections.push(` </git>`);
178
+ hasSections = true;
179
+ }
180
+ }
181
+
182
+ // --- skills ---
183
+ const skillEvents = eventStore.getEvents(sessionId, { categories: ["skill"] });
184
+ if (skillEvents.length > 0) {
185
+ const names = new Set<string>();
186
+ for (const s of skillEvents) {
187
+ const data = safeParse(s.data);
188
+ const name = typeof data?.name === "string" ? data.name : typeof data?.skill === "string" ? data.skill : null;
189
+ if (name) names.add(name);
190
+ }
191
+ if (names.size > 0) {
192
+ sections.push("");
193
+ sections.push(` <skills>`);
194
+ sections.push(` Activated: ${[...names].map(escapeXML).join(", ")}`);
195
+ sections.push(` </skills>`);
196
+ hasSections = true;
197
+ }
198
+ }
199
+
200
+ // --- intent ---
201
+ const intentEvents = eventStore.getEvents(sessionId, { categories: ["intent"], limit: 1 });
202
+ if (intentEvents.length > 0) {
203
+ const data = safeParse(intentEvents[0].data);
204
+ const mode = typeof data?.mode === "string" ? data.mode : typeof data?.intent === "string" ? data.intent : null;
205
+ if (mode) {
206
+ sections.push("");
207
+ sections.push(` <intent>Session mode: ${escapeXML(mode)}</intent>`);
208
+ hasSections = true;
209
+ }
210
+ }
211
+
212
+ // --- env ---
213
+ const envEvents = eventStore.getEvents(sessionId, { categories: ["env"] });
214
+ if (envEvents.length > 0) {
215
+ const details: string[] = [];
216
+ for (const e of envEvents) {
217
+ const data = safeParse(e.data);
218
+ const detail = typeof data?.detail === "string" ? data.detail : typeof data?.env === "string" ? data.env : null;
219
+ if (detail) details.push(escapeXML(detail.slice(0, 100)));
220
+ }
221
+ if (details.length > 0) {
222
+ sections.push("");
223
+ sections.push(` <env>`);
224
+ for (const d of details) sections.push(` ${d}`);
225
+ sections.push(` </env>`);
226
+ hasSections = true;
227
+ }
228
+ }
229
+
230
+ // --- cwd ---
231
+ const cwdEvents = eventStore.getEvents(sessionId, { categories: ["cwd"], limit: 1 });
232
+ if (cwdEvents.length > 0) {
233
+ const data = safeParse(cwdEvents[0].data);
234
+ const cwd = typeof data?.cwd === "string" ? data.cwd : typeof data?.path === "string" ? data.path : null;
235
+ if (cwd) {
236
+ sections.push("");
237
+ sections.push(` <cwd>${escapeXML(cwd)}</cwd>`);
238
+ hasSections = true;
239
+ }
240
+ }
241
+
242
+ sections.push("</session_knowledge>");
243
+
244
+ if (!hasSections) return "";
245
+
246
+ return sections.join("\n");
247
+ }
248
+
249
+ // ---------------------------------------------------------------------------
250
+ // Fallback inline-truncated format (no supi-context-mode MCP)
251
+ // ---------------------------------------------------------------------------
252
+
253
+ function buildFallbackSnapshot(eventStore: EventStore, sessionId: string): string {
18
254
  const sections: string[] = ["<session_knowledge>"];
19
255
 
20
256
  // Last request
21
257
  const prompts = eventStore.getEvents(sessionId, { categories: ["prompt"], limit: 1 });
22
258
  if (prompts.length > 0) {
23
259
  const data = safeParse(prompts[0].data);
24
- const prompt = typeof data?.prompt === "string" ? data.prompt.slice(0, 200) : "";
260
+ const prompt = typeof data?.prompt === "string" ? escapeXML(data.prompt.slice(0, 200)) : "";
25
261
  if (prompt) {
26
262
  sections.push(` <last_request>${prompt}</last_request>`);
27
263
  }
@@ -34,7 +270,7 @@ export function buildResumeSnapshot(eventStore: EventStore, sessionId: string):
34
270
  for (const t of tasks) {
35
271
  const data = safeParse(t.data);
36
272
  const content = extractTaskContent(data);
37
- if (content) sections.push(` - ${content.slice(0, 100)}`);
273
+ if (content) sections.push(` - ${escapeXML(content.slice(0, 100))}`);
38
274
  }
39
275
  sections.push(" </pending_tasks>");
40
276
  }
@@ -45,7 +281,7 @@ export function buildResumeSnapshot(eventStore: EventStore, sessionId: string):
45
281
  sections.push(" <key_decisions>");
46
282
  for (const d of decisions) {
47
283
  const data = safeParse(d.data);
48
- const prompt = typeof data?.prompt === "string" ? data.prompt.slice(0, 100) : "";
284
+ const prompt = typeof data?.prompt === "string" ? escapeXML(data.prompt.slice(0, 100)) : "";
49
285
  if (prompt) sections.push(` - ${prompt}`);
50
286
  }
51
287
  sections.push(" </key_decisions>");
@@ -63,7 +299,7 @@ export function buildResumeSnapshot(eventStore: EventStore, sessionId: string):
63
299
  if (modifiedPaths.size > 0) {
64
300
  sections.push(" <files_modified>");
65
301
  const paths = [...modifiedPaths].slice(0, CAPS.files);
66
- for (const p of paths) sections.push(` - ${p}`);
302
+ for (const p of paths) sections.push(` - ${escapeXML(p)}`);
67
303
  sections.push(" </files_modified>");
68
304
  }
69
305
 
@@ -74,7 +310,7 @@ export function buildResumeSnapshot(eventStore: EventStore, sessionId: string):
74
310
  for (const e of errors) {
75
311
  const data = safeParse(e.data);
76
312
  const summary = formatErrorSummary(data);
77
- if (summary) sections.push(` - ${summary.slice(0, 150)}`);
313
+ if (summary) sections.push(` - ${escapeXML(summary.slice(0, 150))}`);
78
314
  }
79
315
  sections.push(" </recent_errors>");
80
316
  }
@@ -85,7 +321,7 @@ export function buildResumeSnapshot(eventStore: EventStore, sessionId: string):
85
321
  sections.push(" <git_state>");
86
322
  for (const g of gitEvents) {
87
323
  const data = safeParse(g.data);
88
- const cmd = typeof data?.command === "string" ? data.command.slice(0, 100) : "";
324
+ const cmd = typeof data?.command === "string" ? escapeXML(data.command.slice(0, 100)) : "";
89
325
  if (cmd) sections.push(` - ${cmd}`);
90
326
  }
91
327
  sections.push(" </git_state>");