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,420 @@
1
+ /**
2
+ * Git utility functions for enforcement and status line.
3
+ */
4
+
5
+ import { execFile, execSync } from "node:child_process";
6
+
7
+ export function runGit(
8
+ args: string[],
9
+ cwd?: string,
10
+ ): { stdout: string; code: number } {
11
+ try {
12
+ const stdout = execSync(`git --no-optional-locks ${args.join(" ")}`, {
13
+ cwd,
14
+ timeout: 5000,
15
+ encoding: "utf-8",
16
+ stdio: ["pipe", "pipe", "pipe"],
17
+ env: {
18
+ ...process.env,
19
+ GIT_TERMINAL_PROMPT: "0",
20
+ GCM_INTERACTIVE: "Never",
21
+ },
22
+ });
23
+ return { stdout: stdout.trim(), code: 0 };
24
+ } catch (e: any) {
25
+ return { stdout: (e.stdout || "").trim(), code: e.status || 1 };
26
+ }
27
+ }
28
+
29
+ export function getCurrentBranch(cwd?: string): string | null {
30
+ const r = runGit(["rev-parse", "--abbrev-ref", "HEAD"], cwd);
31
+ if (r.code === 0 && r.stdout && r.stdout !== "HEAD") return r.stdout;
32
+ const s = runGit(["symbolic-ref", "HEAD"], cwd);
33
+ if (s.code === 0 && s.stdout.startsWith("refs/heads/"))
34
+ return s.stdout.slice("refs/heads/".length);
35
+ return null;
36
+ }
37
+
38
+ export function getMainBranch(cwd?: string): string | null {
39
+ for (const b of ["main", "master"])
40
+ if (
41
+ runGit(["rev-parse", "--verify", "--end-of-options", b], cwd).code === 0
42
+ )
43
+ return b;
44
+ return null;
45
+ }
46
+
47
+ export function isGitRepo(cwd?: string): boolean {
48
+ return runGit(["rev-parse", "--git-dir"], cwd).code === 0;
49
+ }
50
+
51
+ export function isGithubRepo(cwd?: string): boolean {
52
+ const r = runGit(["remote", "get-url", "origin"], cwd);
53
+ return r.code === 0 && r.stdout.toLowerCase().includes("github.com");
54
+ }
55
+
56
+ export function isBranchMerged(branch: string, main: string, cwd?: string): boolean {
57
+ const u = runGit(["rev-list", "--count", `${main}..${branch}`], cwd);
58
+ if (u.code !== 0) return false;
59
+ const n = parseInt(u.stdout, 10);
60
+ if (isNaN(n) || n === 0) return false;
61
+ return runGit(["merge-base", "--is-ancestor", branch, main], cwd).code === 0;
62
+ }
63
+
64
+ export function isBranchAhead(cwd?: string): boolean {
65
+ if (
66
+ runGit(["rev-parse", "--abbrev-ref", "--symbolic-full-name", "@{u}"], cwd)
67
+ .code !== 0
68
+ )
69
+ return true;
70
+ const s = runGit(["status", "--short", "--branch"], cwd);
71
+ return s.code === 0 && s.stdout.includes("ahead");
72
+ }
73
+
74
+ export function getPrMergeStatus(
75
+ branch: string,
76
+ cwd?: string,
77
+ ): { merged: boolean | null; info: string | null } {
78
+ if (!isGithubRepo(cwd)) return { merged: false, info: null };
79
+ try {
80
+ const out = execSync(
81
+ `gh pr list --head "${branch}" --state merged --json number --limit 1`,
82
+ {
83
+ cwd,
84
+ timeout: 5000,
85
+ encoding: "utf-8",
86
+ stdio: ["pipe", "pipe", "pipe"],
87
+ },
88
+ );
89
+ const data = JSON.parse(out);
90
+ if (Array.isArray(data) && data.length > 0)
91
+ return { merged: true, info: String(data[0].number || "") };
92
+ return { merged: false, info: null };
93
+ } catch {
94
+ return { merged: null, info: "Could not check PR status" };
95
+ }
96
+ }
97
+
98
+ export type OpenPr = { number: number; url: string };
99
+
100
+ const OPEN_PR_TTL_MS = 30_000;
101
+ const OPEN_PR_CACHE_MAX = 50;
102
+ const SAFE_PR_URL = /^https:\/\/[^\x00-\x1f]+$/;
103
+
104
+ type OpenPrCacheEntry = { at: number; pr: OpenPr | null };
105
+ const openPrCache = new Map<string, OpenPrCacheEntry>();
106
+ const openPrInFlight = new Map<string, Promise<OpenPr | null>>();
107
+ /** Keys with an active scheduleOpenPrStatusRefresh .then subscription. */
108
+ const openPrSchedulePending = new Set<string>();
109
+
110
+ type GhPrViewRunner = (cwd?: string) => Promise<string>;
111
+
112
+ const defaultGhPrView: GhPrViewRunner = (cwd) =>
113
+ new Promise((resolve, reject) => {
114
+ execFile(
115
+ "gh",
116
+ ["pr", "view", "--json", "number,url,state"],
117
+ { cwd, timeout: 5000, encoding: "utf-8", maxBuffer: 1024 * 1024 },
118
+ (err, stdout) => {
119
+ if (err) reject(err);
120
+ else resolve(typeof stdout === "string" ? stdout : String(stdout));
121
+ },
122
+ );
123
+ });
124
+
125
+ let ghPrViewRunner: GhPrViewRunner = defaultGhPrView;
126
+
127
+ /** Override `gh pr view` runner (tests). Pass null to restore default. */
128
+ export function setGhPrViewRunner(runner: GhPrViewRunner | null): void {
129
+ ghPrViewRunner = runner ?? defaultGhPrView;
130
+ }
131
+
132
+ /** Parse `gh pr view --json number,url,state` output (OPEN only). */
133
+ export function parseOpenPrJson(out: string): OpenPr | null {
134
+ try {
135
+ const data = JSON.parse(out);
136
+ if (
137
+ data &&
138
+ Number.isInteger(data.number) &&
139
+ data.number > 0 &&
140
+ typeof data.url === "string" &&
141
+ SAFE_PR_URL.test(data.url) &&
142
+ data.state === "OPEN"
143
+ ) {
144
+ return { number: data.number, url: data.url };
145
+ }
146
+ } catch {
147
+ // invalid JSON
148
+ }
149
+ return null;
150
+ }
151
+
152
+ /** Clear open-PR cache and in-flight lookups (tests). */
153
+ export function clearOpenPrCache(): void {
154
+ openPrCache.clear();
155
+ openPrInFlight.clear();
156
+ openPrSchedulePending.clear();
157
+ }
158
+
159
+ /** Seed cache entry (tests) — use past `at` to exercise TTL / SWR. */
160
+ export function seedOpenPrCacheForTests(
161
+ cwd: string,
162
+ branch: string,
163
+ entry: { at: number; pr: OpenPr | null },
164
+ ): void {
165
+ openPrCache.set(openPrCacheKey(cwd, branch), entry);
166
+ }
167
+
168
+ /** Evict oldest keys when over max size (LRU via touch-on-hit). */
169
+ function enforceOpenPrCacheMax(): void {
170
+ while (openPrCache.size > OPEN_PR_CACHE_MAX) {
171
+ const oldest = openPrCache.keys().next().value;
172
+ if (oldest === undefined) break;
173
+ openPrCache.delete(oldest);
174
+ }
175
+ }
176
+
177
+ function openPrCacheKey(cwd: string | undefined, branch: string): string {
178
+ return `${cwd || process.cwd()}:${branch}`;
179
+ }
180
+
181
+ function touchOpenPrCache(key: string, entry: OpenPrCacheEntry): void {
182
+ openPrCache.delete(key);
183
+ openPrCache.set(key, entry);
184
+ enforceOpenPrCacheMax();
185
+ }
186
+
187
+ /**
188
+ * Cached open PR only — never calls `gh`.
189
+ * Returns stale entries past TTL (stale-while-revalidate); kick
190
+ * {@link refreshOpenPr} to refresh asynchronously.
191
+ * Pass `assumeGithub: true` when the caller already verified the remote.
192
+ */
193
+ export function getOpenPr(
194
+ cwd?: string,
195
+ branch?: string | null,
196
+ opts?: { assumeGithub?: boolean },
197
+ ): OpenPr | null {
198
+ const b = branch ?? getCurrentBranch(cwd);
199
+ if (!b) return null;
200
+ if (!opts?.assumeGithub && !isGithubRepo(cwd)) return null;
201
+
202
+ const key = openPrCacheKey(cwd, b);
203
+ const cached = openPrCache.get(key);
204
+ if (!cached) return null;
205
+ // Touch for LRU even on stale hits so hot keys survive eviction.
206
+ touchOpenPrCache(key, cached);
207
+ return cached.pr;
208
+ }
209
+
210
+ /**
211
+ * Async `gh pr view` for the current (or given) branch.
212
+ * Coalesces in-flight lookups per cwd+branch; caches result for 30s.
213
+ * Status-line callers must use {@link getOpenPr} synchronously and only
214
+ * await this to refresh — never block the update path on `gh`.
215
+ * Pass `assumeGithub: true` when the caller already verified the remote.
216
+ */
217
+ export function refreshOpenPr(
218
+ cwd?: string,
219
+ branch?: string | null,
220
+ opts?: { assumeGithub?: boolean },
221
+ ): Promise<OpenPr | null> {
222
+ const b = branch ?? getCurrentBranch(cwd);
223
+ if (!b) return Promise.resolve(null);
224
+ if (!opts?.assumeGithub && !isGithubRepo(cwd)) return Promise.resolve(null);
225
+
226
+ const now = Date.now();
227
+ const key = openPrCacheKey(cwd, b);
228
+ const cached = openPrCache.get(key);
229
+ if (cached && now - cached.at < OPEN_PR_TTL_MS) {
230
+ touchOpenPrCache(key, cached);
231
+ return Promise.resolve(cached.pr);
232
+ }
233
+
234
+ const inflight = openPrInFlight.get(key);
235
+ if (inflight) return inflight;
236
+
237
+ const pending = (async (): Promise<OpenPr | null> => {
238
+ let pr: OpenPr | null = null;
239
+ try {
240
+ const out = await ghPrViewRunner(cwd);
241
+ pr = parseOpenPrJson(out.trim());
242
+ } catch {
243
+ pr = null;
244
+ }
245
+ touchOpenPrCache(key, { at: Date.now(), pr });
246
+ return pr;
247
+ })().finally(() => {
248
+ openPrInFlight.delete(key);
249
+ });
250
+
251
+ openPrInFlight.set(key, pending);
252
+ return pending;
253
+ }
254
+
255
+ /** True when an async open-PR refresh still matches the active cwd/branch. */
256
+ export function shouldApplyOpenPrRefresh(
257
+ lastCtx: { cwd?: string } | null,
258
+ lastBranch: string | null,
259
+ refreshKey: string,
260
+ ): boolean {
261
+ if (!lastCtx || lastBranch == null) return false;
262
+ return `${lastCtx.cwd || ""}:${lastBranch}` === refreshKey;
263
+ }
264
+
265
+ export type OpenPrRefreshDecision = "skip" | "rerender";
266
+
267
+ /** Decide whether a finished refresh should re-run the status-line update. */
268
+ export function decideOpenPrRefreshRerender(args: {
269
+ lastCtx: { cwd?: string } | null;
270
+ lastBranch: string | null;
271
+ refreshKey: string;
272
+ shownKey: string;
273
+ fresh: OpenPr | null;
274
+ }): OpenPrRefreshDecision {
275
+ if (
276
+ !shouldApplyOpenPrRefresh(args.lastCtx, args.lastBranch, args.refreshKey)
277
+ ) {
278
+ return "skip";
279
+ }
280
+ const freshKey = args.fresh
281
+ ? `${args.fresh.number}\0${args.fresh.url}`
282
+ : "";
283
+ if (freshKey === args.shownKey) return "skip";
284
+ return "rerender";
285
+ }
286
+
287
+ /**
288
+ * Status-line open-PR refresh callback wiring (no TUI deps).
289
+ * Schedules refreshOpenPr and optionally re-renders when the result applies.
290
+ */
291
+ export function scheduleOpenPrStatusRefresh(opts: {
292
+ cwd?: string;
293
+ branch: string;
294
+ shownPr: OpenPr | null;
295
+ getState: () => {
296
+ lastCtx: { cwd?: string } | null;
297
+ lastBranch: string | null;
298
+ };
299
+ onRerender: (ctx: { cwd?: string }) => void;
300
+ assumeGithub?: boolean;
301
+ refresh?: typeof refreshOpenPr;
302
+ }): void {
303
+ const refreshKey = `${opts.cwd || ""}:${opts.branch}`;
304
+ // One .then per in-flight key — refreshOpenPr coalesces Promises but
305
+ // repeated schedule calls would otherwise all fire onRerender.
306
+ if (openPrSchedulePending.has(refreshKey)) return;
307
+ openPrSchedulePending.add(refreshKey);
308
+
309
+ const shownKey = opts.shownPr
310
+ ? `${opts.shownPr.number}\0${opts.shownPr.url}`
311
+ : "";
312
+ const refresh = opts.refresh ?? refreshOpenPr;
313
+ try {
314
+ void refresh(opts.cwd, opts.branch, {
315
+ assumeGithub: opts.assumeGithub,
316
+ })
317
+ .then((fresh) => {
318
+ const { lastCtx, lastBranch } = opts.getState();
319
+ if (
320
+ decideOpenPrRefreshRerender({
321
+ lastCtx,
322
+ lastBranch,
323
+ refreshKey,
324
+ shownKey,
325
+ fresh,
326
+ }) !== "rerender"
327
+ ) {
328
+ return;
329
+ }
330
+ if (!lastCtx) return;
331
+ try {
332
+ opts.onRerender(lastCtx);
333
+ } catch (e: any) {
334
+ console.debug(
335
+ "[status-line] open-PR refresh update failed:",
336
+ e?.message || e,
337
+ );
338
+ }
339
+ })
340
+ .finally(() => {
341
+ openPrSchedulePending.delete(refreshKey);
342
+ })
343
+ .catch((e: any) => {
344
+ console.debug(
345
+ "[status-line] open-PR refresh failed:",
346
+ refreshKey,
347
+ e?.message || e,
348
+ );
349
+ });
350
+ } catch {
351
+ openPrSchedulePending.delete(refreshKey);
352
+ }
353
+ }
354
+
355
+ // Cache protected branches per repo (fetched once per session)
356
+ const protectedBranchesCache = new Map<string, Set<string>>();
357
+
358
+ export function getProtectedBranches(cwd?: string): Set<string> {
359
+ const repoKey = cwd || process.cwd();
360
+ if (protectedBranchesCache.has(repoKey)) return protectedBranchesCache.get(repoKey)!;
361
+
362
+ const fallback = new Set(["main", "master"]);
363
+
364
+ if (!isGithubRepo(cwd)) {
365
+ protectedBranchesCache.set(repoKey, fallback);
366
+ return fallback;
367
+ }
368
+
369
+ // Get owner/repo from remote URL
370
+ const remote = runGit(["remote", "get-url", "origin"], cwd);
371
+ if (remote.code !== 0) {
372
+ protectedBranchesCache.set(repoKey, fallback);
373
+ return fallback;
374
+ }
375
+
376
+ const match = remote.stdout.match(/github\.com[:/]([^/]+\/[^/.]+)/);
377
+ if (!match) {
378
+ protectedBranchesCache.set(repoKey, fallback);
379
+ return fallback;
380
+ }
381
+
382
+ const repo = match[1];
383
+ try {
384
+ const out = execSync(
385
+ `gh api repos/${repo}/branches --paginate --jq '.[] | select(.protected==true) | .name'`,
386
+ { cwd, timeout: 10000, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] },
387
+ );
388
+ const branches = new Set(
389
+ out.split("\n").map((b) => b.trim()).filter(Boolean),
390
+ );
391
+ // Always include main/master as fallback
392
+ branches.add("main");
393
+ branches.add("master");
394
+ protectedBranchesCache.set(repoKey, branches);
395
+ return branches;
396
+ } catch {
397
+ protectedBranchesCache.set(repoKey, fallback);
398
+ return fallback;
399
+ }
400
+ }
401
+
402
+ export function hasGitSub(command: string, sub: string): boolean {
403
+ return new RegExp(
404
+ `\\bgit\\b(?:\\s+(?:-[a-zA-Z]\\s+\\S+|-\\S+))*\\s+${sub}\\b`,
405
+ ).test(command);
406
+ }
407
+
408
+ export const DANGEROUS = [
409
+ /\brm\s+(?:-[a-zA-Z]+\s+)*(-[a-zA-Z]*r[a-zA-Z]*|--recursive)/i,
410
+ /\bsudo\b/i,
411
+ /\b(chmod|chown)\b.*777/i,
412
+ /\bmkfs\b/i,
413
+ /\bdd\b.*\bof=\/dev\//i,
414
+ /\bgit\b[\s\S]*\breset\b[\s\S]*--hard\b/i,
415
+ /\bgit\b[\s\S]*\bclean\b[\s\S]*(?:--force|-\S*f)/i,
416
+ /\bfind\b[\s\S]*\s-delete\b/i,
417
+ /\bfind\b[\s\S]*-exec(?:dir)?\s+(?:\/\S+\/)?rm\b/i,
418
+ /\bxargs\s+(?:-\S+\s+)*(?:\/\S+\/)?rm\b/i,
419
+ /(?:^|[\s|])(ba|da|z|k|c|tc|fi)?sh\s*$/i,
420
+ ];
@@ -0,0 +1,250 @@
1
+ /**
2
+ * GitHub issue & PR autocomplete — type # to get suggestions from the current repo.
3
+ *
4
+ * Leverages pi 0.69.0's ctx.ui.addAutocompleteProvider() API.
5
+ * Lazy-loads on first # keystroke, caches for 5 minutes, includes both issues and PRs.
6
+ */
7
+
8
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
9
+ import {
10
+ type AutocompleteItem,
11
+ type AutocompleteProvider,
12
+ type AutocompleteSuggestions,
13
+ fuzzyFilter,
14
+ } from "@earendil-works/pi-tui";
15
+
16
+ type GitHubItem = {
17
+ number: number;
18
+ title: string;
19
+ state: string;
20
+ kind: "issue" | "pr";
21
+ url: string;
22
+ };
23
+
24
+ const MAX_ITEMS = 200;
25
+ const MAX_SUGGESTIONS = 20;
26
+ const CACHE_TTL_MS = 5 * 60 * 1000; // 5 minutes
27
+
28
+ function extractIssueToken(textBeforeCursor: string): string | undefined {
29
+ const match = textBeforeCursor.match(/(?:^|[ \t])#([^\s#]*)$/);
30
+ return match?.[1];
31
+ }
32
+
33
+ function parseGitHubRepo(remoteUrl: string): string | undefined {
34
+ const sshMatch = remoteUrl.match(
35
+ /^git@github\.com:([^/]+\/[^/]+?)(?:\.git)?$/,
36
+ );
37
+ if (sshMatch) return sshMatch[1];
38
+
39
+ const httpsMatch = remoteUrl.match(
40
+ /^https?:\/\/github\.com\/([^/]+\/[^/]+?)(?:\.git)?$/,
41
+ );
42
+ if (httpsMatch) return httpsMatch[1];
43
+
44
+ return undefined;
45
+ }
46
+
47
+ function formatItem(item: GitHubItem): AutocompleteItem {
48
+ const tag = item.kind === "pr" ? "pr" : "issue";
49
+ const stateIcon = item.state === "open" ? "🟢" : item.state === "merged" ? "🟣" : "🔴";
50
+ return {
51
+ value: `#${item.number} (${item.url})`,
52
+ label: `#${item.number}`,
53
+ description: `${stateIcon} [${tag}] ${item.title}`,
54
+ };
55
+ }
56
+
57
+ function filterItems(
58
+ items: GitHubItem[],
59
+ query: string,
60
+ ): AutocompleteItem[] {
61
+ if (!query.trim()) {
62
+ return items.slice(0, MAX_SUGGESTIONS).map(formatItem);
63
+ }
64
+
65
+ // Numeric prefix match first
66
+ if (/^\d+$/.test(query)) {
67
+ const numericMatches = items
68
+ .filter((item) => String(item.number).startsWith(query))
69
+ .slice(0, MAX_SUGGESTIONS)
70
+ .map(formatItem);
71
+ if (numericMatches.length > 0) return numericMatches;
72
+ }
73
+
74
+ // Fuzzy search on number + title
75
+ return fuzzyFilter(
76
+ items,
77
+ query,
78
+ (item) => `${item.number} ${item.title}`,
79
+ )
80
+ .slice(0, MAX_SUGGESTIONS)
81
+ .map(formatItem);
82
+ }
83
+
84
+ function createAutocompleteProvider(
85
+ current: AutocompleteProvider,
86
+ getItems: () => Promise<GitHubItem[] | undefined>,
87
+ ): AutocompleteProvider {
88
+ return {
89
+ async getSuggestions(
90
+ lines,
91
+ cursorLine,
92
+ cursorCol,
93
+ options,
94
+ ): Promise<AutocompleteSuggestions | null> {
95
+ const currentLine = lines[cursorLine] ?? "";
96
+ const textBeforeCursor = currentLine.slice(0, cursorCol);
97
+ const token = extractIssueToken(textBeforeCursor);
98
+
99
+ if (token === undefined) {
100
+ return current.getSuggestions(lines, cursorLine, cursorCol, options);
101
+ }
102
+
103
+ const items = await getItems();
104
+ if (options.signal.aborted || !items || items.length === 0) {
105
+ return current.getSuggestions(lines, cursorLine, cursorCol, options);
106
+ }
107
+
108
+ const suggestions = filterItems(items, token);
109
+ if (suggestions.length === 0) {
110
+ return current.getSuggestions(lines, cursorLine, cursorCol, options);
111
+ }
112
+
113
+ return { items: suggestions, prefix: `#${token}` };
114
+ },
115
+
116
+ applyCompletion(lines, cursorLine, cursorCol, item, prefix) {
117
+ return current.applyCompletion(
118
+ lines,
119
+ cursorLine,
120
+ cursorCol,
121
+ item,
122
+ prefix,
123
+ );
124
+ },
125
+
126
+ shouldTriggerFileCompletion(lines, cursorLine, cursorCol) {
127
+ return (
128
+ current.shouldTriggerFileCompletion?.(lines, cursorLine, cursorCol) ??
129
+ true
130
+ );
131
+ },
132
+ };
133
+ }
134
+
135
+ export function registerGithubAutocomplete(pi: ExtensionAPI): void {
136
+ // Skip in subagent children — they have no UI
137
+ if (process.env.PI_SUBAGENT_CHILD === "1") return;
138
+
139
+ pi.on("session_start", async (_event, ctx) => {
140
+ if (ctx.mode !== "tui") return;
141
+
142
+ // Resolve GitHub repo from git remote
143
+ const remoteResult = await pi.exec("git", ["remote", "-v"], {
144
+ cwd: ctx.cwd,
145
+ timeout: 5_000,
146
+ });
147
+ if (remoteResult.code !== 0) return; // Not a git repo
148
+
149
+ let repo: string | undefined;
150
+ for (const line of remoteResult.stdout.split("\n")) {
151
+ const columns = line.trim().split(/\s+/);
152
+ const remoteUrl = columns[1];
153
+ if (remoteUrl) {
154
+ repo = parseGitHubRepo(remoteUrl);
155
+ if (repo) break;
156
+ }
157
+ }
158
+ if (!repo) return; // Not a GitHub repo
159
+
160
+ // Cache state — lazy-loaded on first # keystroke, refreshed after TTL
161
+ let cachedItems: GitHubItem[] | undefined;
162
+ let cacheTimestamp = 0;
163
+ let fetchPromise: Promise<GitHubItem[] | undefined> | undefined;
164
+
165
+ async function fetchItems(): Promise<GitHubItem[] | undefined> {
166
+ // Fetch issues and PRs in parallel
167
+ const [issuesResult, prsResult] = await Promise.all([
168
+ pi.exec(
169
+ "gh",
170
+ [
171
+ "issue", "list",
172
+ "--repo", repo!,
173
+ "--state", "all",
174
+ "--limit", String(MAX_ITEMS),
175
+ "--json", "number,title,state,url",
176
+ ],
177
+ { cwd: ctx.cwd, timeout: 10_000 },
178
+ ),
179
+ pi.exec(
180
+ "gh",
181
+ [
182
+ "pr", "list",
183
+ "--repo", repo!,
184
+ "--state", "all",
185
+ "--limit", String(MAX_ITEMS),
186
+ "--json", "number,title,state,url",
187
+ ],
188
+ { cwd: ctx.cwd, timeout: 10_000 },
189
+ ),
190
+ ]);
191
+
192
+ const items: GitHubItem[] = [];
193
+
194
+ if (issuesResult.code === 0) {
195
+ try {
196
+ const issues = JSON.parse(issuesResult.stdout) as Array<{ number: number; title: string; state: string; url: string }>;
197
+ for (const issue of issues) {
198
+ items.push({ ...issue, state: issue.state.toLowerCase(), kind: "issue", url: issue.url || "" });
199
+ }
200
+ } catch (e: any) { console.debug("[github-autocomplete] issues JSON parse failed:", e?.message || e); }
201
+ }
202
+
203
+ if (prsResult.code === 0) {
204
+ try {
205
+ const prs = JSON.parse(prsResult.stdout) as Array<{ number: number; title: string; state: string; url: string }>;
206
+ for (const pr of prs) {
207
+ items.push({ ...pr, state: pr.state.toLowerCase(), kind: "pr", url: pr.url || "" });
208
+ }
209
+ } catch (e: any) { console.debug("[github-autocomplete] PRs JSON parse failed:", e?.message || e); }
210
+ }
211
+
212
+ if (items.length === 0) return undefined;
213
+
214
+ // Sort by number descending (newest first)
215
+ items.sort((a, b) => b.number - a.number);
216
+ return items;
217
+ }
218
+
219
+ const getItems = async (): Promise<GitHubItem[] | undefined> => {
220
+ const now = Date.now();
221
+
222
+ // Return cache if fresh
223
+ if (cachedItems && now - cacheTimestamp < CACHE_TTL_MS) {
224
+ return cachedItems;
225
+ }
226
+
227
+ // Deduplicate concurrent fetches
228
+ if (!fetchPromise) {
229
+ fetchPromise = fetchItems().then((result) => {
230
+ if (result) {
231
+ cachedItems = result;
232
+ cacheTimestamp = Date.now();
233
+ }
234
+ fetchPromise = undefined;
235
+ return cachedItems;
236
+ }).catch(() => {
237
+ fetchPromise = undefined;
238
+ return cachedItems;
239
+ });
240
+ }
241
+
242
+ return fetchPromise;
243
+ };
244
+
245
+ // Register autocomplete provider — fetches lazily on first # keystroke
246
+ ctx.ui.addAutocompleteProvider((current) =>
247
+ createAutocompleteProvider(current, getItems),
248
+ );
249
+ });
250
+ }
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Nerd font icons — single place to change all icons used across the extension.
3
+ * Browse icons at: https://www.nerdfonts.com/cheat-sheet
4
+ */
5
+
6
+ export const ICON_SEP = " ❱ ";
7
+ export const ICON_CONTAINER = "󰡨";
8
+ export const ICON_GIT_CLEAN = "";
9
+ export const ICON_GIT_DIRTY = "";
10
+ export const ICON_DIFF = "";
11
+ export const ICON_DREAM = "󰖔";
12
+
13
+ // Review loop status
14
+ export const ICON_REVIEW_CLEAN = "✓";
15
+ export const ICON_REVIEW_NEEDED = "";
16
+ export const ICON_REVIEW_PROGRESS = "";
17
+ export const ICON_REVIEW_FINDINGS = "";