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,533 @@
1
+ /**
2
+ * CLI session directory — persist resume ids with lastSeen / status.
3
+ * Pattern aligned with t3code ProviderSessionDirectory (lighter, file-backed).
4
+ */
5
+
6
+ import {
7
+ existsSync,
8
+ mkdirSync,
9
+ readdirSync,
10
+ readFileSync,
11
+ unlinkSync,
12
+ writeFileSync,
13
+ } from "node:fs";
14
+ import { join } from "node:path";
15
+ import { homedir } from "node:os";
16
+ import { createHash, randomUUID } from "node:crypto";
17
+ import type { CliSessionKey, CliSessionRecord } from "./types.js";
18
+
19
+ export type { CliSessionKey, CliSessionRecord } from "./types.js";
20
+
21
+ /** Prefix for per-process ids used before the real pi session UUID is known. */
22
+ export const PROVISIONAL_PI_SESSION_PREFIX = "tmp-";
23
+
24
+ /** Unique per process/extension load — never shared across concurrent pi sessions. */
25
+ export function createProvisionalPiSessionId(): string {
26
+ return `${PROVISIONAL_PI_SESSION_PREFIX}${randomUUID()}`;
27
+ }
28
+
29
+ export function isProvisionalPiSessionId(
30
+ id: string | null | undefined,
31
+ ): boolean {
32
+ return typeof id === "string" && id.startsWith(PROVISIONAL_PI_SESSION_PREFIX);
33
+ }
34
+ function sessionsDir(): string {
35
+ return join(homedir(), ".pi", "cli-sessions");
36
+ }
37
+
38
+ function keyHash(key: CliSessionKey): string {
39
+ const raw = [
40
+ key.cwd,
41
+ key.agent,
42
+ key.model,
43
+ // Never coalesce unbound keys to a shared "default" here — callers must
44
+ // pass a real UUID or per-process provisional id (tmp-…).
45
+ key.piSessionId || "default",
46
+ ].join("\0");
47
+ return createHash("sha256").update(raw).digest("hex").slice(0, 24);
48
+ }
49
+
50
+ function sessionFile(key: CliSessionKey): string {
51
+ return join(sessionsDir(), `${keyHash(key)}.json`);
52
+ }
53
+
54
+ function nowIso(): string {
55
+ return new Date().toISOString();
56
+ }
57
+
58
+ function readRecord(path: string): CliSessionRecord | null {
59
+ if (!existsSync(path)) return null;
60
+ try {
61
+ const data = JSON.parse(readFileSync(path, "utf-8"));
62
+ if (typeof data?.sessionId !== "string" || !data.sessionId) return null;
63
+ return {
64
+ sessionId: data.sessionId,
65
+ agent: typeof data.agent === "string" ? data.agent : "",
66
+ model: typeof data.model === "string" ? data.model : "",
67
+ cwd: typeof data.cwd === "string" ? data.cwd : "",
68
+ piSessionId:
69
+ typeof data.piSessionId === "string" ? data.piSessionId : "default",
70
+ status: data.status === "stopped" ? "stopped" : "running",
71
+ createdAt:
72
+ typeof data.createdAt === "string" ? data.createdAt : nowIso(),
73
+ lastSeenAt:
74
+ typeof data.lastSeenAt === "string"
75
+ ? data.lastSeenAt
76
+ : typeof data.updatedAt === "string"
77
+ ? data.updatedAt
78
+ : nowIso(),
79
+ resumeFailures:
80
+ typeof data.resumeFailures === "number" ? data.resumeFailures : 0,
81
+ };
82
+ } catch {
83
+ return null;
84
+ }
85
+ }
86
+
87
+ /** Best-effort marker write. FS errors (e.g. cli-sessions is a file) → false. */
88
+ function writeRecord(key: CliSessionKey, record: CliSessionRecord): boolean {
89
+ try {
90
+ const dir = sessionsDir();
91
+ mkdirSync(dir, { recursive: true, mode: 0o700 });
92
+ writeFileSync(sessionFile(key), JSON.stringify(record, null, 0), {
93
+ mode: 0o600,
94
+ });
95
+ return true;
96
+ } catch {
97
+ return false;
98
+ }
99
+ }
100
+
101
+ export function loadCliSessionId(key: CliSessionKey): string | null {
102
+ const record = readRecord(sessionFile(key));
103
+ if (!record || record.status === "stopped") return null;
104
+ return record.sessionId;
105
+ }
106
+
107
+ export function loadCliSessionRecord(
108
+ key: CliSessionKey,
109
+ ): CliSessionRecord | null {
110
+ return readRecord(sessionFile(key));
111
+ }
112
+
113
+ /** Persist a running marker. Best-effort — returns false on FS errors. */
114
+ export function saveCliSessionId(key: CliSessionKey, sessionId: string): boolean {
115
+ if (!sessionId) return false;
116
+ const existing = readRecord(sessionFile(key));
117
+ const ts = nowIso();
118
+ return writeRecord(key, {
119
+ sessionId,
120
+ agent: key.agent,
121
+ model: key.model,
122
+ cwd: key.cwd,
123
+ piSessionId: key.piSessionId || "default",
124
+ status: "running",
125
+ createdAt: existing?.createdAt || ts,
126
+ lastSeenAt: ts,
127
+ resumeFailures: 0,
128
+ });
129
+ }
130
+
131
+ /** Bump lastSeenAt after a successful turn (t3 lastSeenAt). Best-effort. */
132
+ export function touchCliSession(key: CliSessionKey): boolean {
133
+ const existing = readRecord(sessionFile(key));
134
+ if (!existing) return false;
135
+ return writeRecord(key, {
136
+ ...existing,
137
+ status: "running",
138
+ lastSeenAt: nowIso(),
139
+ });
140
+ }
141
+
142
+ /**
143
+ * Update status / lastSeenAt on an existing marker without touching sessionId.
144
+ * Used by tests (and callers) that need idle/running fixtures without rewriting
145
+ * marker files by path.
146
+ */
147
+ export function setCliSessionMarkerMeta(
148
+ key: CliSessionKey,
149
+ opts: {
150
+ status?: "running" | "stopped";
151
+ /** Absolute ISO timestamp, or milliseconds ago from now when `idleMs` set. */
152
+ lastSeenAt?: string;
153
+ idleMs?: number;
154
+ },
155
+ ): boolean {
156
+ const existing = readRecord(sessionFile(key));
157
+ if (!existing) return false;
158
+ let lastSeenAt = opts.lastSeenAt ?? existing.lastSeenAt;
159
+ if (typeof opts.idleMs === "number") {
160
+ lastSeenAt = new Date(Date.now() - opts.idleMs).toISOString();
161
+ }
162
+ return writeRecord(key, {
163
+ ...existing,
164
+ status: opts.status ?? existing.status,
165
+ lastSeenAt,
166
+ });
167
+ }
168
+
169
+ export function markCliSessionStopped(key: CliSessionKey): boolean {
170
+ const existing = readRecord(sessionFile(key));
171
+ if (!existing) return false;
172
+ return writeRecord(key, {
173
+ ...existing,
174
+ status: "stopped",
175
+ lastSeenAt: nowIso(),
176
+ });
177
+ }
178
+
179
+ export function clearCliSessionId(key: CliSessionKey): void {
180
+ const path = sessionFile(key);
181
+ try {
182
+ if (existsSync(path)) unlinkSync(path);
183
+ } catch {
184
+ /* ignore */
185
+ }
186
+ }
187
+
188
+ /**
189
+ * Prefer source when dest is missing/invalid or source lastSeenAt is >= dest.
190
+ * Tie / newer source wins so a pre-bind first-turn provisional is not discarded
191
+ * in favor of an older real-sid marker.
192
+ */
193
+ export function preferCliSessionRecord(
194
+ source: CliSessionRecord,
195
+ dest: CliSessionRecord | null,
196
+ ): "source" | "dest" {
197
+ if (!dest) return "source";
198
+ const s = Date.parse(source.lastSeenAt);
199
+ const d = Date.parse(dest.lastSeenAt);
200
+ if (Number.isNaN(d)) return "source";
201
+ if (Number.isNaN(s)) return "dest";
202
+ return s >= d ? "source" : "dest";
203
+ }
204
+
205
+ /**
206
+ * Move a CLI resume marker from one piSessionId bucket onto `toKey`.
207
+ * Used when this process binds a real UUID after writing under provisional/tmp
208
+ * (or legacy `default`). On conflict, keeps the newer marker by lastSeenAt
209
+ * (provisional first-turn must not lose to a stale real-sid marker).
210
+ */
211
+ export function migrateCliSessionMarker(
212
+ toKey: CliSessionKey,
213
+ fromPiSessionId: string,
214
+ ): boolean {
215
+ if (!toKey.piSessionId || toKey.piSessionId === "" || !fromPiSessionId) {
216
+ return false;
217
+ }
218
+ if (toKey.piSessionId === fromPiSessionId) return false;
219
+ const fromKey: CliSessionKey = { ...toKey, piSessionId: fromPiSessionId };
220
+ const fromRec = loadCliSessionRecord(fromKey);
221
+ if (!fromRec) return false;
222
+ const toRec = loadCliSessionRecord(toKey);
223
+ if (preferCliSessionRecord(fromRec, toRec) === "source") {
224
+ const ok = saveCliSessionId(toKey, fromRec.sessionId);
225
+ if (!ok) return false;
226
+ clearCliSessionId(fromKey);
227
+ return true;
228
+ }
229
+ // Destination is newer — drop stale provisional/source only.
230
+ clearCliSessionId(fromKey);
231
+ return false;
232
+ }
233
+
234
+ /**
235
+ * Migrate every marker for `fromPiSessionId` in `cwd` onto `toPiSessionId`.
236
+ * On conflict, prefer the newer lastSeenAt (keeps pre-bind first-turn session).
237
+ */
238
+ export function migrateAllCliSessionMarkers(
239
+ cwd: string,
240
+ fromPiSessionId: string,
241
+ toPiSessionId: string,
242
+ ): number {
243
+ if (
244
+ !fromPiSessionId ||
245
+ !toPiSessionId ||
246
+ fromPiSessionId === toPiSessionId
247
+ ) {
248
+ return 0;
249
+ }
250
+ let n = 0;
251
+ for (const { path, record } of listCliSessions()) {
252
+ if (record.cwd !== cwd) continue;
253
+ if (record.piSessionId !== fromPiSessionId) continue;
254
+ const toKey: CliSessionKey = {
255
+ cwd: record.cwd,
256
+ agent: record.agent,
257
+ model: record.model,
258
+ piSessionId: toPiSessionId,
259
+ };
260
+ const toRec = loadCliSessionRecord(toKey);
261
+ if (preferCliSessionRecord(record, toRec) === "source") {
262
+ const ok = saveCliSessionId(toKey, record.sessionId);
263
+ if (!ok) continue;
264
+ try {
265
+ unlinkSync(path);
266
+ } catch {
267
+ /* ignore */
268
+ }
269
+ n += 1;
270
+ } else {
271
+ try {
272
+ unlinkSync(path);
273
+ } catch {
274
+ /* ignore */
275
+ }
276
+ }
277
+ }
278
+ return n;
279
+ }
280
+
281
+ /**
282
+ * If a real piSessionId key has no marker but the legacy `default` bucket does,
283
+ * move the CLI resume id onto the real key (avoids mid-session fork when
284
+ * activePiSessionId becomes available after the first turn).
285
+ * @deprecated Prefer migrateCliSessionMarker / migrateAllCliSessionMarkers.
286
+ */
287
+ export function adoptLegacyCliSessionMarker(key: CliSessionKey): boolean {
288
+ return migrateCliSessionMarker(key, "default");
289
+ }
290
+
291
+ /**
292
+ * Only migrate when *this process* previously keyed under provisional/tmp or
293
+ * legacy default. Never adopt a stale on-disk default from another session.
294
+ */
295
+ export function shouldAdoptLegacyCliMarker(
296
+ prevKey: CliSessionKey | null | undefined,
297
+ nextKey: CliSessionKey,
298
+ ): boolean {
299
+ if (!nextKey.piSessionId || nextKey.piSessionId === "default") return false;
300
+ if (isProvisionalPiSessionId(nextKey.piSessionId)) return false;
301
+ if (!prevKey?.piSessionId) return false;
302
+ const prev = prevKey.piSessionId;
303
+ return prev === "default" || isProvisionalPiSessionId(prev);
304
+ }
305
+
306
+ export function incrementResumeFailure(key: CliSessionKey): number {
307
+ const existing = readRecord(sessionFile(key));
308
+ if (!existing) return 1;
309
+ const n = (existing.resumeFailures || 0) + 1;
310
+ writeRecord(key, { ...existing, resumeFailures: n, lastSeenAt: nowIso() });
311
+ return n;
312
+ }
313
+
314
+ export interface ListedCliSession {
315
+ keyHash: string;
316
+ path: string;
317
+ record: CliSessionRecord;
318
+ }
319
+
320
+ /** List all persisted CLI session bindings (directory). Best-effort: FS errors → []. */
321
+ export function listCliSessions(): ListedCliSession[] {
322
+ const dir = sessionsDir();
323
+ if (!existsSync(dir)) return [];
324
+ const out: ListedCliSession[] = [];
325
+ let names: string[];
326
+ try {
327
+ names = readdirSync(dir);
328
+ } catch {
329
+ return [];
330
+ }
331
+ for (const name of names) {
332
+ if (!name.endsWith(".json")) continue;
333
+ const path = join(dir, name);
334
+ const record = readRecord(path);
335
+ if (!record) continue;
336
+ out.push({ keyHash: name.replace(/\.json$/, ""), path, record });
337
+ }
338
+ return out;
339
+ }
340
+
341
+ /**
342
+ * True when stderr/message indicates a dead/invalid resume id.
343
+ * Used to clear the marker and retry without --resume.
344
+ */
345
+ export function isCliResumeFailure(message: string): boolean {
346
+ const s = message.toLowerCase();
347
+ return (
348
+ /session.*(not found|expired|invalid|unknown|missing)/i.test(s) ||
349
+ /unknown session/i.test(s) ||
350
+ /no such session/i.test(s) ||
351
+ /cannot resume/i.test(s) ||
352
+ /failed to (load|resume|restore)/i.test(s) ||
353
+ /resume.*(fail|error|invalid)/i.test(s) ||
354
+ /invalid session (id|uuid)/i.test(s)
355
+ );
356
+ }
357
+
358
+ /** Exit codes that mean abort/kill — not a dead resume id (issue #661). */
359
+ const ABORT_EXIT_RE = /exited (130|137|143)\b/i;
360
+
361
+ /**
362
+ * Whether a failed turn that used --resume should clear the marker and retry.
363
+ * Explicit resume errors and empty non-zero exits (dead ids) retry.
364
+ * SIGINT(130) / SIGKILL(137) / SIGTERM(143) keep the marker — abort ≠ invalid id.
365
+ */
366
+ export function shouldRetryWithoutResume(message: string): boolean {
367
+ if (isCliResumeFailure(message)) return true;
368
+ if (ABORT_EXIT_RE.test(message)) return false;
369
+ if (/\b(aborted|sigterm|sigint|sigkill|cancelled|canceled)\b/i.test(message)) {
370
+ return false;
371
+ }
372
+ return /exited [1-9]\d*(?::\s*(?:no output)?\s*)?$/i.test(message.trim());
373
+ }
374
+
375
+ /**
376
+ * Decide whether this turn must seed pi history into a fresh CLI session.
377
+ */
378
+ export function resolveCliHistorySeed(opts: {
379
+ hasCliSession: boolean;
380
+ forceHistorySeed: boolean;
381
+ }): { useCliSession: boolean; seedHistory: boolean } {
382
+ if (opts.forceHistorySeed) {
383
+ return { useCliSession: false, seedHistory: true };
384
+ }
385
+ return {
386
+ useCliSession: opts.hasCliSession,
387
+ seedHistory: !opts.hasCliSession,
388
+ };
389
+ }
390
+
391
+ /**
392
+ * Prepend the once-per-CLI-session system prompt when starting a fresh chat
393
+ * (first turn or retry after failed --resume).
394
+ */
395
+ export function applySystemPromptToCliPrompt(
396
+ prompt: string,
397
+ systemPrompt?: string,
398
+ ): string {
399
+ if (!systemPrompt) return prompt;
400
+ return `${systemPrompt}\n\n---\n\n${prompt}`;
401
+ }
402
+
403
+ /**
404
+ * Decide CLI marker / history-seed action on pi `session_start` (issue #661).
405
+ * - reload → keep markers (CLI `--resume` continues)
406
+ * - resume / new / pi session id change → clear this session's markers + force re-seed
407
+ * - otherwise → bind id only
408
+ */
409
+ export function decideCliSessionStartReseed(opts: {
410
+ reason: string;
411
+ prevPiSessionId: string | null;
412
+ nextPiSessionId: string | null;
413
+ }): {
414
+ action: "keep" | "reseed" | "bind-only";
415
+ forceHistorySeed: boolean;
416
+ } {
417
+ if (opts.reason === "reload") {
418
+ return { action: "keep", forceHistorySeed: false };
419
+ }
420
+ const mustReseed =
421
+ opts.reason === "resume" ||
422
+ opts.reason === "new" ||
423
+ (Boolean(opts.prevPiSessionId) &&
424
+ Boolean(opts.nextPiSessionId) &&
425
+ opts.prevPiSessionId !== opts.nextPiSessionId);
426
+ if (mustReseed) {
427
+ return { action: "reseed", forceHistorySeed: true };
428
+ }
429
+ return { action: "bind-only", forceHistorySeed: false };
430
+ }
431
+
432
+ /**
433
+ * Resolve active pi session id on `session_start`.
434
+ * Unknown (no getter) must not null a previously bound UUID — that would flip
435
+ * marker keys back to the provisional tmp-* bucket and break `--resume`.
436
+ * Known empty from the getter still clears the binding (stale UUID avoid).
437
+ */
438
+ export function resolveActivePiSessionIdOnSessionStart(opts: {
439
+ prevPiSessionId: string | null;
440
+ hasSessionIdGetter: boolean;
441
+ readPiSessionId: string | null;
442
+ }): {
443
+ nextActivePiSessionId: string | null;
444
+ /** Sid for reseed decision / migrate / cleanup (prev when unknown). */
445
+ resolvedReadSid: string | null;
446
+ sidKnown: boolean;
447
+ } {
448
+ if (!opts.hasSessionIdGetter) {
449
+ return {
450
+ nextActivePiSessionId: opts.prevPiSessionId,
451
+ resolvedReadSid: opts.prevPiSessionId,
452
+ sidKnown: false,
453
+ };
454
+ }
455
+ return {
456
+ nextActivePiSessionId: opts.readPiSessionId,
457
+ resolvedReadSid: opts.readPiSessionId,
458
+ sidKnown: true,
459
+ };
460
+ }
461
+
462
+ /**
463
+ * Read pi session id from sessionManager.
464
+ * MUST call getSessionId on the manager object (not as an unbound method) —
465
+ * pi's impl reads `this.sessionId` and throws if `this` is lost (#664).
466
+ * Throws → treat as unknown (hasGetter false) and set readError for logging.
467
+ */
468
+ export function readPiSessionIdFromManager(sessionManager?: {
469
+ getSessionId?: () => string;
470
+ }): {
471
+ hasGetter: boolean;
472
+ readPiSessionId: string | null;
473
+ /** True when getSessionId existed but threw — callers should log. */
474
+ readError: boolean;
475
+ } {
476
+ if (typeof sessionManager?.getSessionId !== "function") {
477
+ return { hasGetter: false, readPiSessionId: null, readError: false };
478
+ }
479
+ try {
480
+ return {
481
+ hasGetter: true,
482
+ readPiSessionId: sessionManager.getSessionId() || null,
483
+ readError: false,
484
+ };
485
+ } catch {
486
+ return { hasGetter: false, readPiSessionId: null, readError: true };
487
+ }
488
+ }
489
+
490
+ /**
491
+ * Unlink CLI session markers for a cwd scoped to a pi session id.
492
+ * By default also clears legacy `default` markers for that cwd (migration).
493
+ * Does not touch other concurrent pi sessions sharing the same project.
494
+ */
495
+ export function clearCliSessionsForPiSession(
496
+ cwd: string,
497
+ piSessionId: string | null,
498
+ opts?: { includeLegacyDefault?: boolean },
499
+ ): number {
500
+ const includeLegacy = opts?.includeLegacyDefault ?? true;
501
+ const matchIds = new Set<string>();
502
+ if (piSessionId && piSessionId !== "") matchIds.add(piSessionId);
503
+ if (includeLegacy) matchIds.add("default");
504
+ if (matchIds.size === 0) return 0;
505
+
506
+ let n = 0;
507
+ for (const { path, record } of listCliSessions()) {
508
+ if (record.cwd !== cwd) continue;
509
+ if (!matchIds.has(record.piSessionId)) continue;
510
+ try {
511
+ unlinkSync(path);
512
+ n += 1;
513
+ } catch {
514
+ /* ignore */
515
+ }
516
+ }
517
+ return n;
518
+ }
519
+
520
+ /** @deprecated Prefer clearCliSessionsForPiSession — cwd-wide wipe hurts concurrent sessions. */
521
+ export function clearCliSessionsForCwd(cwd: string): number {
522
+ let n = 0;
523
+ for (const { path, record } of listCliSessions()) {
524
+ if (record.cwd !== cwd) continue;
525
+ try {
526
+ unlinkSync(path);
527
+ n += 1;
528
+ } catch {
529
+ /* ignore */
530
+ }
531
+ }
532
+ return n;
533
+ }