pi-orchestrator-config 4.1.5

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 (175) hide show
  1. package/README.md +808 -0
  2. package/agents/api-documenter.md +32 -0
  3. package/agents/bash-expert.md +53 -0
  4. package/agents/code-reviewer-docs.md +145 -0
  5. package/agents/code-reviewer-guidelines.md +97 -0
  6. package/agents/code-reviewer-quality.md +142 -0
  7. package/agents/code-reviewer-security.md +107 -0
  8. package/agents/code-reviewer-spec.md +164 -0
  9. package/agents/debugger.md +63 -0
  10. package/agents/docker-expert.md +40 -0
  11. package/agents/docs-fetcher.md +48 -0
  12. package/agents/git-expert.md +63 -0
  13. package/agents/github-expert.md +83 -0
  14. package/agents/go-expert.md +62 -0
  15. package/agents/issue-reviewer-feasibility.md +60 -0
  16. package/agents/issue-reviewer-scope.md +63 -0
  17. package/agents/issue-reviewer-spec.md +77 -0
  18. package/agents/java-expert.md +31 -0
  19. package/agents/jenkins-expert.md +38 -0
  20. package/agents/kubernetes-expert.md +40 -0
  21. package/agents/planner.md +42 -0
  22. package/agents/python-expert.md +61 -0
  23. package/agents/reviewer.md +26 -0
  24. package/agents/scout.md +40 -0
  25. package/agents/security-auditor.md +254 -0
  26. package/agents/technical-documentation-writer.md +37 -0
  27. package/agents/test-automator.md +55 -0
  28. package/agents/test-runner.md +47 -0
  29. package/agents/ts-expert.md +23 -0
  30. package/agents/worker.md +30 -0
  31. package/extensions/acpx-provider/index.ts +104 -0
  32. package/extensions/acpx-provider/load-runtime.ts +99 -0
  33. package/extensions/acpx-provider/runtime-models.ts +40 -0
  34. package/extensions/cli-provider/agents/claude.ts +128 -0
  35. package/extensions/cli-provider/agents/cursor.ts +84 -0
  36. package/extensions/cli-provider/agents/gemini.ts +100 -0
  37. package/extensions/cli-provider/discover.ts +77 -0
  38. package/extensions/cli-provider/index.ts +57 -0
  39. package/extensions/cli-provider/parsers.ts +304 -0
  40. package/extensions/cli-provider/providers.ts +58 -0
  41. package/extensions/cli-provider/runner.ts +225 -0
  42. package/extensions/cli-provider/runtime-models.ts +32 -0
  43. package/extensions/cli-provider/session-reaper.ts +122 -0
  44. package/extensions/cli-provider/sessions.ts +533 -0
  45. package/extensions/cli-provider/shared/discover-cache.ts +235 -0
  46. package/extensions/cli-provider/types.ts +48 -0
  47. package/extensions/coms/coms-net-server.ts +1642 -0
  48. package/extensions/coms/coms-net-wrapper.ts +521 -0
  49. package/extensions/coms/coms-net.ts +1589 -0
  50. package/extensions/coms/coms-p2p.ts +1879 -0
  51. package/extensions/coms/coms-shared.ts +510 -0
  52. package/extensions/coms/coms-wrapper.ts +138 -0
  53. package/extensions/coms/index.ts +20 -0
  54. package/extensions/coms/themeMap.ts +147 -0
  55. package/extensions/image-gen/image-gen.ts +332 -0
  56. package/extensions/image-gen/index.ts +14 -0
  57. package/extensions/orchestrator/agents.ts +134 -0
  58. package/extensions/orchestrator/ask-user.ts +293 -0
  59. package/extensions/orchestrator/async-agents.ts +1037 -0
  60. package/extensions/orchestrator/async-capability.ts +129 -0
  61. package/extensions/orchestrator/async-runner.ts +324 -0
  62. package/extensions/orchestrator/async-status-parse.ts +31 -0
  63. package/extensions/orchestrator/async-status-ui.ts +201 -0
  64. package/extensions/orchestrator/async-wait.ts +36 -0
  65. package/extensions/orchestrator/btw.ts +210 -0
  66. package/extensions/orchestrator/cron-status-format.ts +126 -0
  67. package/extensions/orchestrator/cron-status-ui.ts +132 -0
  68. package/extensions/orchestrator/cron.ts +730 -0
  69. package/extensions/orchestrator/dreaming.ts +355 -0
  70. package/extensions/orchestrator/enforcement-helpers.ts +530 -0
  71. package/extensions/orchestrator/enforcement-rules.ts +329 -0
  72. package/extensions/orchestrator/enforcement.ts +828 -0
  73. package/extensions/orchestrator/extended-autocomplete.ts +593 -0
  74. package/extensions/orchestrator/git-helpers.ts +420 -0
  75. package/extensions/orchestrator/github-autocomplete.ts +250 -0
  76. package/extensions/orchestrator/icons.ts +17 -0
  77. package/extensions/orchestrator/index.ts +100 -0
  78. package/extensions/orchestrator/memory-embeddings.ts +281 -0
  79. package/extensions/orchestrator/memory-promotion.ts +449 -0
  80. package/extensions/orchestrator/memory-provenance.ts +80 -0
  81. package/extensions/orchestrator/memory-query-class.ts +90 -0
  82. package/extensions/orchestrator/memory-scoring.ts +434 -0
  83. package/extensions/orchestrator/memory-tools.ts +883 -0
  84. package/extensions/orchestrator/memory-tree.ts +212 -0
  85. package/extensions/orchestrator/nvim.ts +169 -0
  86. package/extensions/orchestrator/overlay-dashboard-utils.ts +25 -0
  87. package/extensions/orchestrator/overlay-dashboard.ts +512 -0
  88. package/extensions/orchestrator/pi-config-review-state.ts +303 -0
  89. package/extensions/orchestrator/preference-extractor.ts +106 -0
  90. package/extensions/orchestrator/project-settings.ts +468 -0
  91. package/extensions/orchestrator/promotion-queue.ts +311 -0
  92. package/extensions/orchestrator/resolve-agent-model.ts +32 -0
  93. package/extensions/orchestrator/review-ui.ts +204 -0
  94. package/extensions/orchestrator/rule-placeholders.ts +12 -0
  95. package/extensions/orchestrator/rules.ts +481 -0
  96. package/extensions/orchestrator/session-search.ts +243 -0
  97. package/extensions/orchestrator/session-validation.ts +374 -0
  98. package/extensions/orchestrator/situation-report.ts +370 -0
  99. package/extensions/orchestrator/status-bar.ts +31 -0
  100. package/extensions/orchestrator/status-line.ts +192 -0
  101. package/extensions/orchestrator/status.ts +110 -0
  102. package/extensions/orchestrator/subagent-tool.ts +1748 -0
  103. package/extensions/orchestrator/task-lifecycle.ts +88 -0
  104. package/extensions/orchestrator/utils.ts +227 -0
  105. package/extensions/pidash/index.ts +15 -0
  106. package/extensions/pidash/pidash.ts +941 -0
  107. package/extensions/pidiff/index.ts +13 -0
  108. package/extensions/pidiff/pidiff.ts +369 -0
  109. package/extensions/providers/acpx-driver.ts +479 -0
  110. package/extensions/providers/built-in-drivers.ts +52 -0
  111. package/extensions/providers/claude-driver.ts +319 -0
  112. package/extensions/providers/cursor-acpx-driver.ts +398 -0
  113. package/extensions/providers/cursor-cli-driver.ts +484 -0
  114. package/extensions/providers/gemini-driver.ts +302 -0
  115. package/extensions/providers/index.ts +513 -0
  116. package/extensions/shared/build-system-prompt.ts +57 -0
  117. package/extensions/shared/create-runtime-provider.ts +190 -0
  118. package/extensions/shared/daemon-manager.ts +248 -0
  119. package/extensions/shared/file-logger.ts +115 -0
  120. package/extensions/shared/managed-refresh.ts +211 -0
  121. package/extensions/shared/provider-driver.ts +295 -0
  122. package/extensions/shared/provider-errors.ts +305 -0
  123. package/extensions/shared/provider-registry.ts +388 -0
  124. package/extensions/shared/stream-builder.ts +275 -0
  125. package/extensions/shared/types.ts +20 -0
  126. package/extensions/shared/ui/README.md +10 -0
  127. package/extensions/shared/ui/badge.tsx +52 -0
  128. package/extensions/shared/ui/button.tsx +58 -0
  129. package/extensions/shared/ui/collapsible.tsx +21 -0
  130. package/extensions/shared/ui/dropdown-menu.tsx +266 -0
  131. package/extensions/shared/ui/input.tsx +20 -0
  132. package/extensions/shared/ui/popover.tsx +114 -0
  133. package/extensions/shared/ui/resizable.tsx +48 -0
  134. package/extensions/shared/ui/scroll-area.tsx +53 -0
  135. package/extensions/shared/ui/select.tsx +201 -0
  136. package/extensions/shared/ui/separator.tsx +25 -0
  137. package/extensions/shared/ui/tooltip.tsx +64 -0
  138. package/extensions/shared/ws-client.ts +74 -0
  139. package/package.json +59 -0
  140. package/prompts/coderabbit-rate-limit.md +95 -0
  141. package/prompts/create-coms-feature-manager.md +186 -0
  142. package/prompts/create-skill.md +128 -0
  143. package/prompts/domain-model.md +109 -0
  144. package/prompts/external-ai.md +665 -0
  145. package/prompts/implement-and-review.md +25 -0
  146. package/prompts/implement.md +26 -0
  147. package/prompts/issue-review.md +256 -0
  148. package/prompts/pr-review.md +753 -0
  149. package/prompts/query-db.md +154 -0
  150. package/prompts/refine-review.md +208 -0
  151. package/prompts/release.md +235 -0
  152. package/prompts/remember.md +33 -0
  153. package/prompts/review-handler-status.md +27 -0
  154. package/prompts/review-handler.md +677 -0
  155. package/prompts/review-local.md +159 -0
  156. package/prompts/scout-and-plan.md +21 -0
  157. package/rules/00-orchestrator-core.md +42 -0
  158. package/rules/05-issue-first-workflow.md +119 -0
  159. package/rules/10-agent-routing.md +58 -0
  160. package/rules/15-mcp-launchpad.md +46 -0
  161. package/rules/20-code-review-loop.md +234 -0
  162. package/rules/25-documentation-updates.md +15 -0
  163. package/rules/30-prompt-templates.md +19 -0
  164. package/rules/35-memory.md +240 -0
  165. package/rules/40-critical-rules.md +124 -0
  166. package/rules/45-file-preview.md +29 -0
  167. package/rules/50-agent-bug-reporting.md +90 -0
  168. package/rules/55-coms-protocol.md +63 -0
  169. package/rules/60-task-tracking.md +66 -0
  170. package/skills/conflict-resolver/SKILL.md +213 -0
  171. package/skills/skill-creator/SKILL.md +85 -0
  172. package/skills/systematic-debugging/SKILL.md +353 -0
  173. package/skills/test-driven-development/SKILL.md +335 -0
  174. package/skills/update-agents-md/SKILL.md +282 -0
  175. package/templates/coms-feature-manager-prompt.md +313 -0
@@ -0,0 +1,243 @@
1
+ /**
2
+ * Session Search — keyword search over past conversation summaries.
3
+ *
4
+ * Indexes compacted conversation summaries into a JSON file with simple
5
+ * keyword matching. Provides a tool for the LLM to recall past discussions
6
+ * at zero LLM cost.
7
+ *
8
+ * Inspired by Hermes Agent's session search layer.
9
+ * Uses JSON storage instead of SQLite for portability (no native deps).
10
+ */
11
+
12
+ import { existsSync, readFileSync, writeFileSync } from "node:fs";
13
+ import { join } from "node:path";
14
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
15
+ import { Type } from "typebox";
16
+ import { getProjectDataDir } from "./utils.js";
17
+
18
+ // ── Storage ────────────────────────────────────────────────────────────────
19
+
20
+ interface SessionEntry {
21
+ sessionId: string;
22
+ timestamp: string;
23
+ summary: string;
24
+ }
25
+
26
+ interface SessionStore {
27
+ entries: SessionEntry[];
28
+ }
29
+
30
+ function getStorePath(cwd: string): string {
31
+ return join(getProjectDataDir(cwd), "session-search.json");
32
+ }
33
+
34
+ function loadStore(cwd: string): SessionStore {
35
+ const storePath = getStorePath(cwd);
36
+ if (!existsSync(storePath)) return { entries: [] };
37
+ try {
38
+ const raw = JSON.parse(readFileSync(storePath, "utf-8"));
39
+ if (!raw || typeof raw !== "object" || !Array.isArray(raw.entries)) {
40
+ console.error("[session-search] corrupt store shape, resetting");
41
+ return { entries: [] };
42
+ }
43
+ // Filter to only valid entries
44
+ const entries = raw.entries.filter(
45
+ (e: unknown): e is SessionEntry =>
46
+ typeof e === "object" && e !== null &&
47
+ typeof (e as any).sessionId === "string" &&
48
+ typeof (e as any).timestamp === "string" &&
49
+ typeof (e as any).summary === "string"
50
+ );
51
+ return { entries };
52
+ } catch (err) {
53
+ console.error("[session-search] corrupt store, resetting:", err);
54
+ return { entries: [] };
55
+ }
56
+ }
57
+
58
+ function saveStore(cwd: string, store: SessionStore): void {
59
+ writeFileSync(getStorePath(cwd), JSON.stringify(store, null, 2), "utf-8");
60
+ }
61
+
62
+ // ── Index a session summary ────────────────────────────────────────────────
63
+
64
+ export function indexSessionSummary(
65
+ cwd: string,
66
+ sessionId: string,
67
+ summary: string,
68
+ ): void {
69
+ if (!summary || !summary.trim()) return;
70
+ const store = loadStore(cwd);
71
+
72
+ // Avoid duplicate entries for the same session
73
+ const exists = store.entries.some(
74
+ (e) => e.sessionId === sessionId && e.summary === summary.trim(),
75
+ );
76
+ if (exists) return;
77
+
78
+ store.entries.push({
79
+ sessionId,
80
+ timestamp: new Date().toISOString(),
81
+ summary: summary.trim(),
82
+ });
83
+
84
+ // Keep max 500 entries (oldest dropped)
85
+ if (store.entries.length > 500) {
86
+ store.entries = store.entries.slice(-500);
87
+ }
88
+
89
+ saveStore(cwd, store);
90
+ }
91
+
92
+ // ── Search sessions ────────────────────────────────────────────────────────
93
+
94
+ export interface SearchResult {
95
+ sessionId: string;
96
+ timestamp: string;
97
+ snippet: string;
98
+ score: number;
99
+ }
100
+
101
+ export function searchSessions(
102
+ cwd: string,
103
+ query: string,
104
+ limit: number = 10,
105
+ ): SearchResult[] {
106
+ const store = loadStore(cwd);
107
+ if (store.entries.length === 0) return [];
108
+
109
+ const queryWords = query.toLowerCase().split(/\s+/).filter(Boolean);
110
+ if (queryWords.length === 0) return [];
111
+
112
+ const results: SearchResult[] = [];
113
+
114
+ for (const entry of store.entries) {
115
+ const summaryLower = entry.summary.toLowerCase();
116
+
117
+ // Score: count how many query words match
118
+ let matchCount = 0;
119
+ for (const word of queryWords) {
120
+ if (summaryLower.includes(word)) matchCount++;
121
+ }
122
+
123
+ if (matchCount === 0) continue;
124
+
125
+ // Extract snippet around first match
126
+ const firstWord = queryWords.find((w) => summaryLower.includes(w))!;
127
+ const matchIdx = summaryLower.indexOf(firstWord);
128
+ const snippetStart = Math.max(0, matchIdx - 80);
129
+ const snippetEnd = Math.min(entry.summary.length, matchIdx + 120);
130
+ let snippet = entry.summary.slice(snippetStart, snippetEnd).trim();
131
+ if (snippetStart > 0) snippet = "..." + snippet;
132
+ if (snippetEnd < entry.summary.length) snippet = snippet + "...";
133
+
134
+ results.push({
135
+ sessionId: entry.sessionId,
136
+ timestamp: entry.timestamp,
137
+ snippet,
138
+ score: matchCount / queryWords.length,
139
+ });
140
+ }
141
+
142
+ // Sort by score (desc), then recency (desc)
143
+ results.sort((a, b) => {
144
+ if (b.score !== a.score) return b.score - a.score;
145
+ return b.timestamp.localeCompare(a.timestamp);
146
+ });
147
+
148
+ return results.slice(0, limit);
149
+ }
150
+
151
+
152
+
153
+ // ── Tool Registration ──────────────────────────────────────────────────────
154
+
155
+ export function registerSessionSearch(pi: ExtensionAPI): void {
156
+ // Only register in the orchestrator, not subagents
157
+ if (process.env.PI_SUBAGENT_CHILD === "1") return;
158
+
159
+ pi.registerTool({
160
+ name: "session_search",
161
+ label: "Session Search",
162
+ description:
163
+ "Search past conversation summaries by keyword. Use when the user references " +
164
+ "something from a past session, or when you need to recall what was discussed " +
165
+ "previously. Returns matching snippets from past sessions at zero LLM cost.",
166
+ parameters: Type.Object({
167
+ query: Type.String({
168
+ description: "Search query — keywords or phrases to find in past conversations",
169
+ }),
170
+ limit: Type.Optional(
171
+ Type.Number({
172
+ description: "Max results to return (default: 10)",
173
+ }),
174
+ ),
175
+ }),
176
+ async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
177
+ const results = searchSessions(ctx.cwd, params.query, params.limit ?? 10);
178
+
179
+ if (results.length === 0) {
180
+ return {
181
+ content: [{
182
+ type: "text",
183
+ text: `No past sessions found matching "${params.query}".`,
184
+ }],
185
+ };
186
+ }
187
+
188
+ const lines = results.map((r) =>
189
+ `**${r.timestamp.split("T")[0]}** (${r.sessionId.slice(0, 8)})\n${r.snippet}`
190
+ );
191
+
192
+ return {
193
+ content: [{
194
+ type: "text",
195
+ text: `Found ${results.length} past session(s) matching "${params.query}":\n\n${lines.join("\n\n---\n\n")}`,
196
+ }],
197
+ };
198
+ },
199
+ });
200
+
201
+ // Accumulate user messages for session-end indexing
202
+ let sessionMessages: string[] = [];
203
+ let sessionCwd = "";
204
+ let sessionId = "";
205
+
206
+ pi.on("session_start", (_event, ctx) => {
207
+ sessionMessages = [];
208
+ sessionCwd = ctx.cwd;
209
+ sessionId = ctx.sessionManager?.getSessionId?.() || `session-${Date.now()}`;
210
+ });
211
+
212
+ pi.on("input", (event, _ctx) => {
213
+ const text = event?.text;
214
+ if (!text || text.length < 15) return;
215
+ // Skip slash commands and system-injected text
216
+ if (text.startsWith("/") || text.startsWith("[IMPORTANT:") || text.startsWith("[SYSTEM:")) return;
217
+ sessionMessages.push(text.slice(0, 200));
218
+ });
219
+
220
+ // Index compaction summaries when context is compacted
221
+ pi.on("session_compact", (event, ctx) => {
222
+ try {
223
+ const summary = event?.compactionEntry?.summary;
224
+ if (!summary || summary.length < 50) return;
225
+ const cwd = sessionCwd || ctx.cwd;
226
+ const sid = sessionId || ctx.sessionManager?.getSessionId?.() || `session-${Date.now()}`;
227
+ indexSessionSummary(cwd, sid, summary);
228
+ } catch (err) {
229
+ console.error("[session-search] indexing failed on compact:", err);
230
+ }
231
+ });
232
+
233
+ // Index accumulated messages on session shutdown
234
+ pi.on("session_shutdown", () => {
235
+ try {
236
+ if (sessionMessages.length < 3 || !sessionCwd) return;
237
+ const summary = sessionMessages.join(" | ").slice(0, 1000);
238
+ indexSessionSummary(sessionCwd, sessionId, summary);
239
+ } catch (err) {
240
+ console.error("[session-search] indexing failed on shutdown:", err);
241
+ }
242
+ });
243
+ }
@@ -0,0 +1,374 @@
1
+ /**
2
+ * Session start validation — checks for required/optional CLI tools.
3
+ */
4
+
5
+ import { execSync, execFileSync } from "node:child_process";
6
+ import * as crypto from "node:crypto";
7
+ import * as fs from "node:fs";
8
+ import * as path from "node:path";
9
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
10
+ import { setUvAvailable, isUvAvailable } from "./enforcement-helpers.js";
11
+ import { checkMinPiVersion } from "./utils.js";
12
+
13
+ /** Check whether a CLI command is available on PATH. */
14
+ function hasCmd(cmd: string): boolean {
15
+ try {
16
+ execSync(`command -v ${cmd}`, {
17
+ timeout: 3000,
18
+ stdio: ["pipe", "pipe", "pipe"],
19
+ });
20
+ return true;
21
+ } catch {
22
+ return false;
23
+ }
24
+ }
25
+
26
+ /** Register the /repair command — fixes orphaned tool calls in session files. */
27
+ function registerRepairCommand(pi: ExtensionAPI): void {
28
+ pi.registerCommand("repair", {
29
+ description: "Repair session — fix orphaned tool calls that break API requests",
30
+ handler: async (_args, ctx) => {
31
+ const sessionFile = ctx.sessionManager.getSessionFile();
32
+ if (!sessionFile || !fs.existsSync(sessionFile)) {
33
+ ctx.ui.notify("No session file found", "warning");
34
+ return;
35
+ }
36
+
37
+ const raw = fs.readFileSync(sessionFile, "utf-8");
38
+ const lines = raw.split("\n").filter((l) => l.trim());
39
+
40
+ // Parse all entries and build index
41
+ const entries: any[] = [];
42
+ const byId = new Map<string, any>();
43
+ for (const line of lines) {
44
+ try {
45
+ const entry = JSON.parse(line);
46
+ if (entry.id) {
47
+ entries.push(entry);
48
+ byId.set(entry.id, entry);
49
+ }
50
+ } catch { /* skip malformed lines */ }
51
+ }
52
+
53
+ // Walk active branch (leaf to root) to find orphans on the ACTUAL path
54
+ // The flat file may have toolResults on dead branches that don't help.
55
+ const leaf = entries[entries.length - 1];
56
+ if (!leaf) {
57
+ ctx.ui.notify("Session file is empty", "warning");
58
+ return;
59
+ }
60
+
61
+ const branch: any[] = [];
62
+ let cur = leaf;
63
+ while (cur) {
64
+ branch.push(cur);
65
+ cur = cur.parentId ? byId.get(cur.parentId) : null;
66
+ }
67
+ branch.reverse();
68
+
69
+ // Collect toolCall IDs and toolResult IDs on the active branch
70
+ const toolCalls = new Map<string, { parentId: string; toolName: string }>();
71
+ const toolResults = new Set<string>();
72
+
73
+ for (const entry of branch) {
74
+ const msg = entry.message;
75
+ if (!msg) continue;
76
+ if (msg.role === "assistant" && Array.isArray(msg.content)) {
77
+ for (const item of msg.content) {
78
+ if (item.type === "toolCall" && item.id) {
79
+ toolCalls.set(item.id, {
80
+ parentId: entry.id,
81
+ toolName: item.toolName || item.name || "unknown",
82
+ });
83
+ }
84
+ }
85
+ } else if (msg.role === "toolResult" && msg.toolCallId) {
86
+ toolResults.add(msg.toolCallId);
87
+ }
88
+ }
89
+
90
+ // Find orphans on the active branch, and the next branch entry after each orphan parent
91
+ // so we can re-parent it through the synthetic toolResult.
92
+ const orphanGroups = new Map<string, {
93
+ calls: { callId: string; toolName: string }[];
94
+ nextBranchEntryId: string | null;
95
+ }>();
96
+ for (const [callId, meta] of toolCalls) {
97
+ if (!toolResults.has(callId)) {
98
+ if (!orphanGroups.has(meta.parentId)) {
99
+ // Find the next entry on the branch after this parent
100
+ const idx = branch.findIndex((e) => e.id === meta.parentId);
101
+ const nextEntry = idx >= 0 && idx + 1 < branch.length ? branch[idx + 1] : null;
102
+ orphanGroups.set(meta.parentId, {
103
+ calls: [],
104
+ nextBranchEntryId: nextEntry?.id ?? null,
105
+ });
106
+ }
107
+ orphanGroups.get(meta.parentId)!.calls.push({ callId, toolName: meta.toolName });
108
+ }
109
+ }
110
+
111
+ if (orphanGroups.size === 0) {
112
+ ctx.ui.notify("Session is clean \u2014 no orphaned tool calls found", "info");
113
+ return;
114
+ }
115
+
116
+ // Build synthetic IDs and re-parent map.
117
+ // Chain: assistant(parent) -> synthetic_toolResult(s) -> next_entry
118
+ // The LAST synthetic toolResult becomes the new parent for the next branch entry.
119
+ const reparentMap = new Map<string, string>(); // entryId -> newParentId
120
+ const syntheticsByParent = new Map<string, any[]>();
121
+ let totalFixed = 0;
122
+ const fixedDetails: string[] = [];
123
+
124
+ for (const [parentId, group] of orphanGroups) {
125
+ const synthetics: any[] = [];
126
+ let prevId = parentId;
127
+ for (const call of group.calls) {
128
+ const synId = crypto.randomBytes(4).toString("hex");
129
+ synthetics.push({
130
+ type: "message",
131
+ id: synId,
132
+ parentId: prevId,
133
+ timestamp: new Date().toISOString(),
134
+ message: {
135
+ role: "toolResult",
136
+ toolCallId: call.callId,
137
+ toolName: call.toolName,
138
+ content: [{ type: "text", text: "Error: session interrupted \u2014 tool call did not complete" }],
139
+ isError: true,
140
+ timestamp: Date.now(),
141
+ },
142
+ });
143
+ prevId = synId;
144
+ totalFixed++;
145
+ fixedDetails.push(` \u2022 ${call.toolName} (${call.callId})`);
146
+ }
147
+ syntheticsByParent.set(parentId, synthetics);
148
+ // Re-parent the next branch entry to point to the last synthetic
149
+ if (group.nextBranchEntryId) {
150
+ reparentMap.set(group.nextBranchEntryId, prevId);
151
+ }
152
+ }
153
+
154
+ // Rewrite the session file:
155
+ // 1. Insert synthetics after their parent
156
+ // 2. Re-parent entries that need it
157
+ const repairedLines: string[] = [];
158
+ for (const line of lines) {
159
+ let entry: any;
160
+ try { entry = JSON.parse(line); } catch {
161
+ repairedLines.push(line);
162
+ continue;
163
+ }
164
+ // Re-parent if needed
165
+ if (entry.id && reparentMap.has(entry.id)) {
166
+ entry.parentId = reparentMap.get(entry.id);
167
+ repairedLines.push(JSON.stringify(entry));
168
+ } else {
169
+ repairedLines.push(line);
170
+ }
171
+ // Insert synthetics after parent
172
+ if (entry.id && syntheticsByParent.has(entry.id)) {
173
+ for (const syn of syntheticsByParent.get(entry.id)!) {
174
+ repairedLines.push(JSON.stringify(syn));
175
+ }
176
+ }
177
+ }
178
+
179
+ fs.writeFileSync(sessionFile, repairedLines.join("\n") + "\n");
180
+
181
+ ctx.ui.notify(
182
+ `\ud83d\udd27 Repaired ${totalFixed} orphaned tool call${totalFixed > 1 ? "s" : ""}:\n${fixedDetails.join("\n")}\nReloading session...`,
183
+ "info",
184
+ );
185
+
186
+ // Force session re-read — switchSession to the same file rebuilds the tree
187
+ await ctx.switchSession(sessionFile);
188
+ },
189
+ });
190
+ }
191
+
192
+ /** Check for required/optional CLI tools and notify the user. */
193
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
194
+ async function checkSessionTools(ctx: any): Promise<void> {
195
+ // ── uv install suggestion (availability already set in session_start) ──
196
+ if (!isUvAvailable()) {
197
+ ctx.ui.notify(
198
+ "⚡ uv is not installed. Install it for better Python support and auto-fix for python/pip commands:\n" +
199
+ " • macOS/Linux: curl -LsSf https://astral.sh/uv/install.sh | sh\n" +
200
+ " • Windows: powershell -ExecutionPolicy ByPass -c \"irm https://astral.sh/uv/install.ps1 | iex\"\n" +
201
+ " • Docs: https://docs.astral.sh/uv/",
202
+ "warning",
203
+ );
204
+ }
205
+
206
+ const missing: string[] = [];
207
+ const optional: string[] = [];
208
+
209
+ // Optional
210
+ if (!hasCmd("gh"))
211
+ optional.push("gh — GitHub CLI. Install: https://cli.github.com/");
212
+ if (!hasCmd("mcpl"))
213
+ optional.push(
214
+ "mcpl — MCP Launchpad. Install: https://github.com/kenneth-liao/mcp-launchpad",
215
+ );
216
+ if (!hasCmd("myk-pi-tools"))
217
+ optional.push(
218
+ "myk-pi-tools — PR/release/review CLI. Install: uv tool install git+https://github.com/myk-org/pi-config",
219
+ );
220
+
221
+ // Check agent-browser skill
222
+ const agentBrowserPaths = [
223
+ path.join(process.env.HOME || "", ".agents", "skills", "agent-browser", "SKILL.md"),
224
+ path.join(process.env.HOME || "", ".pi", "agent", "skills", "agent-browser", "SKILL.md"),
225
+ ];
226
+ if (!agentBrowserPaths.some((p) => fs.existsSync(p))) {
227
+ optional.push(
228
+ "agent-browser skill — browser automation. Install: npx skills add vercel-labs/agent-browser@agent-browser -g -y",
229
+ );
230
+ }
231
+
232
+ // Check prek only if .pre-commit-config.yaml exists
233
+ try {
234
+ if (
235
+ fs.existsSync(path.join(ctx.cwd, ".pre-commit-config.yaml")) &&
236
+ !hasCmd("prek")
237
+ ) {
238
+ optional.push(
239
+ "prek — pre-commit wrapper (.pre-commit-config.yaml detected). Install: https://github.com/j178/prek",
240
+ );
241
+ }
242
+ } catch (e: any) { console.debug("[session-validation] tool detection failed:", e?.message || e); }
243
+
244
+ if (missing.length > 0 || optional.length > 0) {
245
+ const parts: string[] = [];
246
+ if (missing.length > 0)
247
+ parts.push(
248
+ `⚠️ CRITICAL missing:\n${missing.map((m) => ` • ${m}`).join("\n")}`,
249
+ );
250
+ if (optional.length > 0)
251
+ parts.push(
252
+ `Optional missing:\n${optional.map((m) => ` • ${m}`).join("\n")}`,
253
+ );
254
+ ctx.ui.notify(
255
+ parts.join("\n\n"),
256
+ missing.length > 0 ? "warning" : "info",
257
+ );
258
+ }
259
+ }
260
+
261
+ /** Check for pi-config version upgrades and show changelog notification. */
262
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
263
+ async function checkUpgradeChangelog(ctx: any): Promise<void> {
264
+ // Find pi-config package.json by walking up from this file's directory
265
+ try {
266
+ let searchDir = __dirname ?? path.dirname(new URL(import.meta.url).pathname);
267
+ let currentVersion: string | null = null;
268
+ for (let i = 0; i < 5; i++) {
269
+ const candidate = path.join(searchDir, "package.json");
270
+ if (fs.existsSync(candidate)) {
271
+ try {
272
+ const pkg = JSON.parse(fs.readFileSync(candidate, "utf-8"));
273
+ if (pkg.name === "pi-orchestrator-config" && pkg.version) {
274
+ currentVersion = pkg.version;
275
+ break;
276
+ }
277
+ } catch (e: any) { console.debug("[session-validation] package.json parse failed:", e?.message || e); }
278
+ }
279
+ searchDir = path.dirname(searchDir);
280
+ }
281
+ if (currentVersion) {
282
+ const versionFile = path.join(
283
+ process.env.HOME || "",
284
+ ".pi",
285
+ "pi-config-last-version",
286
+ );
287
+ let lastVersion: string | null = null;
288
+ try {
289
+ lastVersion = fs.readFileSync(versionFile, "utf-8").trim();
290
+ } catch (e: any) { console.debug("[session-validation] read last version failed:", e?.message || e); }
291
+
292
+ if (!lastVersion) {
293
+ // First run — just record the version, no notification
294
+ try {
295
+ fs.mkdirSync(path.dirname(versionFile), { recursive: true });
296
+ fs.writeFileSync(versionFile, currentVersion, "utf-8");
297
+ } catch (e: any) { console.debug("[session-validation] write version file failed:", e?.message || e); }
298
+ } else if (lastVersion !== currentVersion && hasCmd("gh")) {
299
+ // Version changed — fetch release notes (5s timeout, no shell)
300
+ const tag = `v${currentVersion}`;
301
+ const releaseUrl = `https://github.com/myk-org/pi-config/releases/tag/${tag}`;
302
+ let notified = false;
303
+ try {
304
+ const body = execFileSync(
305
+ "gh",
306
+ ["release", "view", "--repo", "myk-org/pi-config", tag, "--json", "body", "--jq", ".body"],
307
+ { timeout: 5000, stdio: ["pipe", "pipe", "pipe"] },
308
+ ).toString().trim();
309
+ if (body) {
310
+ const maxLen = 1500;
311
+ const truncated = body.length > maxLen
312
+ ? body.slice(0, maxLen) + `\n\n... [See full notes](${releaseUrl})`
313
+ : body;
314
+ ctx.ui.notify(
315
+ `🚀 pi-config updated: ${lastVersion} → ${currentVersion}\n\n${truncated}`,
316
+ "info",
317
+ );
318
+ notified = true;
319
+ }
320
+ } catch (e: any) { console.debug("[session-validation] release notes fetch failed:", e?.message || e); }
321
+ // Only update version file after successful notification
322
+ // so failed attempts retry on next session
323
+ if (notified) {
324
+ try {
325
+ fs.mkdirSync(path.dirname(versionFile), { recursive: true });
326
+ fs.writeFileSync(versionFile, currentVersion, "utf-8");
327
+ } catch (e: any) { console.debug("[session-validation] write version after notify failed:", e?.message || e); }
328
+ }
329
+ }
330
+ }
331
+ } catch (e: any) { console.debug("[session-validation] upgrade changelog failed:", e?.message || e); }
332
+ }
333
+
334
+ export function registerSessionValidation(pi: ExtensionAPI): void {
335
+ registerRepairCommand(pi);
336
+
337
+ pi.on("session_start", async (_event, ctx) => {
338
+ // Always check uv availability — even in headless mode.
339
+ // Enforcement depends on this being set correctly.
340
+ try {
341
+ const uvInstalled = hasCmd("uv");
342
+ setUvAvailable(uvInstalled);
343
+ } catch (e: any) {
344
+ console.debug("[session-validation] uv check failed:", e?.message || e);
345
+ }
346
+
347
+ if (!ctx.hasUI) return;
348
+
349
+ // Check minimum pi version
350
+ const versionCheck = checkMinPiVersion();
351
+ if (!versionCheck.ok) {
352
+ if (versionCheck.installed === null) {
353
+ ctx.ui.notify(
354
+ `⚠️ pi version could not be determined; CLI/ACPX providers require >= ${versionCheck.required} ` +
355
+ `and will still be attempted. Some features (--session-id for async agents, session_info_changed events) ` +
356
+ `may not work. Update pi if registration fails: npm install -g @earendil-works/pi-coding-agent`,
357
+ "warning",
358
+ );
359
+ } else {
360
+ ctx.ui.notify(
361
+ `⚠️ pi ${versionCheck.installed} is below minimum required version ${versionCheck.required}. ` +
362
+ `CLI/ACPX providers (createProvider) are disabled, and some features (--session-id for async agents, session_info_changed events) will not work. ` +
363
+ `Update pi: npm install -g @earendil-works/pi-coding-agent`,
364
+ "warning",
365
+ );
366
+ }
367
+ }
368
+
369
+ await checkSessionTools(ctx);
370
+
371
+ // ── Upgrade changelog ──────────────────────────────────────────────
372
+ await checkUpgradeChangelog(ctx);
373
+ });
374
+ }