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.
- package/README.md +808 -0
- package/agents/api-documenter.md +32 -0
- package/agents/bash-expert.md +53 -0
- package/agents/code-reviewer-docs.md +145 -0
- package/agents/code-reviewer-guidelines.md +97 -0
- package/agents/code-reviewer-quality.md +142 -0
- package/agents/code-reviewer-security.md +107 -0
- package/agents/code-reviewer-spec.md +164 -0
- package/agents/debugger.md +63 -0
- package/agents/docker-expert.md +40 -0
- package/agents/docs-fetcher.md +48 -0
- package/agents/git-expert.md +63 -0
- package/agents/github-expert.md +83 -0
- package/agents/go-expert.md +62 -0
- package/agents/issue-reviewer-feasibility.md +60 -0
- package/agents/issue-reviewer-scope.md +63 -0
- package/agents/issue-reviewer-spec.md +77 -0
- package/agents/java-expert.md +31 -0
- package/agents/jenkins-expert.md +38 -0
- package/agents/kubernetes-expert.md +40 -0
- package/agents/planner.md +42 -0
- package/agents/python-expert.md +61 -0
- package/agents/reviewer.md +26 -0
- package/agents/scout.md +40 -0
- package/agents/security-auditor.md +254 -0
- package/agents/technical-documentation-writer.md +37 -0
- package/agents/test-automator.md +55 -0
- package/agents/test-runner.md +47 -0
- package/agents/ts-expert.md +23 -0
- package/agents/worker.md +30 -0
- package/extensions/acpx-provider/index.ts +104 -0
- package/extensions/acpx-provider/load-runtime.ts +99 -0
- package/extensions/acpx-provider/runtime-models.ts +40 -0
- package/extensions/cli-provider/agents/claude.ts +128 -0
- package/extensions/cli-provider/agents/cursor.ts +84 -0
- package/extensions/cli-provider/agents/gemini.ts +100 -0
- package/extensions/cli-provider/discover.ts +77 -0
- package/extensions/cli-provider/index.ts +57 -0
- package/extensions/cli-provider/parsers.ts +304 -0
- package/extensions/cli-provider/providers.ts +58 -0
- package/extensions/cli-provider/runner.ts +225 -0
- package/extensions/cli-provider/runtime-models.ts +32 -0
- package/extensions/cli-provider/session-reaper.ts +122 -0
- package/extensions/cli-provider/sessions.ts +533 -0
- package/extensions/cli-provider/shared/discover-cache.ts +235 -0
- package/extensions/cli-provider/types.ts +48 -0
- package/extensions/coms/coms-net-server.ts +1642 -0
- package/extensions/coms/coms-net-wrapper.ts +521 -0
- package/extensions/coms/coms-net.ts +1589 -0
- package/extensions/coms/coms-p2p.ts +1879 -0
- package/extensions/coms/coms-shared.ts +510 -0
- package/extensions/coms/coms-wrapper.ts +138 -0
- package/extensions/coms/index.ts +20 -0
- package/extensions/coms/themeMap.ts +147 -0
- package/extensions/image-gen/image-gen.ts +332 -0
- package/extensions/image-gen/index.ts +14 -0
- package/extensions/orchestrator/agents.ts +134 -0
- package/extensions/orchestrator/ask-user.ts +293 -0
- package/extensions/orchestrator/async-agents.ts +1037 -0
- package/extensions/orchestrator/async-capability.ts +129 -0
- package/extensions/orchestrator/async-runner.ts +324 -0
- package/extensions/orchestrator/async-status-parse.ts +31 -0
- package/extensions/orchestrator/async-status-ui.ts +201 -0
- package/extensions/orchestrator/async-wait.ts +36 -0
- package/extensions/orchestrator/btw.ts +210 -0
- package/extensions/orchestrator/cron-status-format.ts +126 -0
- package/extensions/orchestrator/cron-status-ui.ts +132 -0
- package/extensions/orchestrator/cron.ts +730 -0
- package/extensions/orchestrator/dreaming.ts +355 -0
- package/extensions/orchestrator/enforcement-helpers.ts +530 -0
- package/extensions/orchestrator/enforcement-rules.ts +329 -0
- package/extensions/orchestrator/enforcement.ts +828 -0
- package/extensions/orchestrator/extended-autocomplete.ts +593 -0
- package/extensions/orchestrator/git-helpers.ts +420 -0
- package/extensions/orchestrator/github-autocomplete.ts +250 -0
- package/extensions/orchestrator/icons.ts +17 -0
- package/extensions/orchestrator/index.ts +100 -0
- package/extensions/orchestrator/memory-embeddings.ts +281 -0
- package/extensions/orchestrator/memory-promotion.ts +449 -0
- package/extensions/orchestrator/memory-provenance.ts +80 -0
- package/extensions/orchestrator/memory-query-class.ts +90 -0
- package/extensions/orchestrator/memory-scoring.ts +434 -0
- package/extensions/orchestrator/memory-tools.ts +883 -0
- package/extensions/orchestrator/memory-tree.ts +212 -0
- package/extensions/orchestrator/nvim.ts +169 -0
- package/extensions/orchestrator/overlay-dashboard-utils.ts +25 -0
- package/extensions/orchestrator/overlay-dashboard.ts +512 -0
- package/extensions/orchestrator/pi-config-review-state.ts +303 -0
- package/extensions/orchestrator/preference-extractor.ts +106 -0
- package/extensions/orchestrator/project-settings.ts +468 -0
- package/extensions/orchestrator/promotion-queue.ts +311 -0
- package/extensions/orchestrator/resolve-agent-model.ts +32 -0
- package/extensions/orchestrator/review-ui.ts +204 -0
- package/extensions/orchestrator/rule-placeholders.ts +12 -0
- package/extensions/orchestrator/rules.ts +481 -0
- package/extensions/orchestrator/session-search.ts +243 -0
- package/extensions/orchestrator/session-validation.ts +374 -0
- package/extensions/orchestrator/situation-report.ts +370 -0
- package/extensions/orchestrator/status-bar.ts +31 -0
- package/extensions/orchestrator/status-line.ts +192 -0
- package/extensions/orchestrator/status.ts +110 -0
- package/extensions/orchestrator/subagent-tool.ts +1748 -0
- package/extensions/orchestrator/task-lifecycle.ts +88 -0
- package/extensions/orchestrator/utils.ts +227 -0
- package/extensions/pidash/index.ts +15 -0
- package/extensions/pidash/pidash.ts +941 -0
- package/extensions/pidiff/index.ts +13 -0
- package/extensions/pidiff/pidiff.ts +369 -0
- package/extensions/providers/acpx-driver.ts +479 -0
- package/extensions/providers/built-in-drivers.ts +52 -0
- package/extensions/providers/claude-driver.ts +319 -0
- package/extensions/providers/cursor-acpx-driver.ts +398 -0
- package/extensions/providers/cursor-cli-driver.ts +484 -0
- package/extensions/providers/gemini-driver.ts +302 -0
- package/extensions/providers/index.ts +513 -0
- package/extensions/shared/build-system-prompt.ts +57 -0
- package/extensions/shared/create-runtime-provider.ts +190 -0
- package/extensions/shared/daemon-manager.ts +248 -0
- package/extensions/shared/file-logger.ts +115 -0
- package/extensions/shared/managed-refresh.ts +211 -0
- package/extensions/shared/provider-driver.ts +295 -0
- package/extensions/shared/provider-errors.ts +305 -0
- package/extensions/shared/provider-registry.ts +388 -0
- package/extensions/shared/stream-builder.ts +275 -0
- package/extensions/shared/types.ts +20 -0
- package/extensions/shared/ui/README.md +10 -0
- package/extensions/shared/ui/badge.tsx +52 -0
- package/extensions/shared/ui/button.tsx +58 -0
- package/extensions/shared/ui/collapsible.tsx +21 -0
- package/extensions/shared/ui/dropdown-menu.tsx +266 -0
- package/extensions/shared/ui/input.tsx +20 -0
- package/extensions/shared/ui/popover.tsx +114 -0
- package/extensions/shared/ui/resizable.tsx +48 -0
- package/extensions/shared/ui/scroll-area.tsx +53 -0
- package/extensions/shared/ui/select.tsx +201 -0
- package/extensions/shared/ui/separator.tsx +25 -0
- package/extensions/shared/ui/tooltip.tsx +64 -0
- package/extensions/shared/ws-client.ts +74 -0
- package/package.json +59 -0
- package/prompts/coderabbit-rate-limit.md +95 -0
- package/prompts/create-coms-feature-manager.md +186 -0
- package/prompts/create-skill.md +128 -0
- package/prompts/domain-model.md +109 -0
- package/prompts/external-ai.md +665 -0
- package/prompts/implement-and-review.md +25 -0
- package/prompts/implement.md +26 -0
- package/prompts/issue-review.md +256 -0
- package/prompts/pr-review.md +753 -0
- package/prompts/query-db.md +154 -0
- package/prompts/refine-review.md +208 -0
- package/prompts/release.md +235 -0
- package/prompts/remember.md +33 -0
- package/prompts/review-handler-status.md +27 -0
- package/prompts/review-handler.md +677 -0
- package/prompts/review-local.md +159 -0
- package/prompts/scout-and-plan.md +21 -0
- package/rules/00-orchestrator-core.md +42 -0
- package/rules/05-issue-first-workflow.md +119 -0
- package/rules/10-agent-routing.md +58 -0
- package/rules/15-mcp-launchpad.md +46 -0
- package/rules/20-code-review-loop.md +234 -0
- package/rules/25-documentation-updates.md +15 -0
- package/rules/30-prompt-templates.md +19 -0
- package/rules/35-memory.md +240 -0
- package/rules/40-critical-rules.md +124 -0
- package/rules/45-file-preview.md +29 -0
- package/rules/50-agent-bug-reporting.md +90 -0
- package/rules/55-coms-protocol.md +63 -0
- package/rules/60-task-tracking.md +66 -0
- package/skills/conflict-resolver/SKILL.md +213 -0
- package/skills/skill-creator/SKILL.md +85 -0
- package/skills/systematic-debugging/SKILL.md +353 -0
- package/skills/test-driven-development/SKILL.md +335 -0
- package/skills/update-agents-md/SKILL.md +282 -0
- 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 = "";
|