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,311 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Promotion queue — structured candidates for graduating memories into
|
|
3
|
+
* skills, enforcement, or project rules.
|
|
4
|
+
*
|
|
5
|
+
* Storage: `.pi/memory/promotions.md` (human + agent readable).
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
9
|
+
import { dirname, join } from "node:path";
|
|
10
|
+
import { createHash } from "node:crypto";
|
|
11
|
+
|
|
12
|
+
export type PromotionDestination =
|
|
13
|
+
| "memory"
|
|
14
|
+
| "skill"
|
|
15
|
+
| "enforcement"
|
|
16
|
+
| "project_rule"
|
|
17
|
+
| "discard";
|
|
18
|
+
|
|
19
|
+
export type PromotionStatus = "proposed" | "applied" | "rejected";
|
|
20
|
+
|
|
21
|
+
export interface PromotionCandidate {
|
|
22
|
+
id: string;
|
|
23
|
+
destination: PromotionDestination;
|
|
24
|
+
text: string;
|
|
25
|
+
category: string;
|
|
26
|
+
reason: string;
|
|
27
|
+
evidenceCount?: number;
|
|
28
|
+
status: PromotionStatus;
|
|
29
|
+
trigger?: string;
|
|
30
|
+
action?: string;
|
|
31
|
+
verifier?: string;
|
|
32
|
+
skillName?: string;
|
|
33
|
+
skillCreated?: boolean;
|
|
34
|
+
createdAt: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const DESTINATIONS: PromotionDestination[] = [
|
|
38
|
+
"memory",
|
|
39
|
+
"skill",
|
|
40
|
+
"enforcement",
|
|
41
|
+
"project_rule",
|
|
42
|
+
"discard",
|
|
43
|
+
];
|
|
44
|
+
|
|
45
|
+
const STATUSES: PromotionStatus[] = ["proposed", "applied", "rejected"];
|
|
46
|
+
|
|
47
|
+
export function getPromotionsPath(cwd: string): string {
|
|
48
|
+
return join(cwd, ".pi", "memory", "promotions.md");
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Stable id from destination + category + text (dedup key). */
|
|
52
|
+
export function promotionId(
|
|
53
|
+
destination: PromotionDestination,
|
|
54
|
+
category: string,
|
|
55
|
+
text: string,
|
|
56
|
+
): string {
|
|
57
|
+
return createHash("sha256")
|
|
58
|
+
.update(`${destination}|${category}|${text}`)
|
|
59
|
+
.digest("hex")
|
|
60
|
+
.slice(0, 12);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function formatPromotionBlock(c: PromotionCandidate): string {
|
|
64
|
+
const lines = [
|
|
65
|
+
`### ${c.id}`,
|
|
66
|
+
`- destination: ${c.destination}`,
|
|
67
|
+
`- status: ${c.status}`,
|
|
68
|
+
`- category: ${c.category}`,
|
|
69
|
+
`- text: ${c.text}`,
|
|
70
|
+
`- reason: ${c.reason}`,
|
|
71
|
+
`- created: ${c.createdAt}`,
|
|
72
|
+
];
|
|
73
|
+
if (c.evidenceCount !== undefined) {
|
|
74
|
+
lines.push(`- evidence_count: ${c.evidenceCount}`);
|
|
75
|
+
}
|
|
76
|
+
if (c.trigger) lines.push(`- trigger: ${c.trigger}`);
|
|
77
|
+
if (c.action) lines.push(`- action: ${c.action}`);
|
|
78
|
+
if (c.verifier) lines.push(`- verifier: ${c.verifier}`);
|
|
79
|
+
if (c.skillName) lines.push(`- skill_name: ${c.skillName}`);
|
|
80
|
+
if (c.skillCreated !== undefined) {
|
|
81
|
+
lines.push(`- skill_created: ${c.skillCreated ? "true" : "false"}`);
|
|
82
|
+
}
|
|
83
|
+
return lines.join("\n") + "\n";
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function parseField(block: string, key: string): string | undefined {
|
|
87
|
+
const re = new RegExp(`^- ${key}: (.+)$`, "m");
|
|
88
|
+
const m = block.match(re);
|
|
89
|
+
return m?.[1]?.trim();
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function parsePromotionsMarkdown(content: string): PromotionCandidate[] {
|
|
93
|
+
if (!content.trim()) return [];
|
|
94
|
+
|
|
95
|
+
const chunks = content.split(/^### /m).filter((c) => c.trim());
|
|
96
|
+
const results: PromotionCandidate[] = [];
|
|
97
|
+
|
|
98
|
+
for (const chunk of chunks) {
|
|
99
|
+
const lines = chunk.trim().split("\n");
|
|
100
|
+
const id = lines[0]?.trim();
|
|
101
|
+
if (!id) continue;
|
|
102
|
+
|
|
103
|
+
const body = chunk;
|
|
104
|
+
const destination = parseField(body, "destination") as PromotionDestination | undefined;
|
|
105
|
+
const status = parseField(body, "status") as PromotionStatus | undefined;
|
|
106
|
+
const category = parseField(body, "category");
|
|
107
|
+
const text = parseField(body, "text");
|
|
108
|
+
const reason = parseField(body, "reason");
|
|
109
|
+
const createdAt = parseField(body, "created");
|
|
110
|
+
|
|
111
|
+
if (!destination || !DESTINATIONS.includes(destination)) continue;
|
|
112
|
+
if (!status || !STATUSES.includes(status)) continue;
|
|
113
|
+
if (!category || !text || !reason || !createdAt) continue;
|
|
114
|
+
|
|
115
|
+
const evidenceRaw = parseField(body, "evidence_count");
|
|
116
|
+
const skillCreatedRaw = parseField(body, "skill_created");
|
|
117
|
+
|
|
118
|
+
results.push({
|
|
119
|
+
id,
|
|
120
|
+
destination,
|
|
121
|
+
status,
|
|
122
|
+
category,
|
|
123
|
+
text,
|
|
124
|
+
reason,
|
|
125
|
+
createdAt,
|
|
126
|
+
evidenceCount: evidenceRaw !== undefined ? Number(evidenceRaw) : undefined,
|
|
127
|
+
trigger: parseField(body, "trigger"),
|
|
128
|
+
action: parseField(body, "action"),
|
|
129
|
+
verifier: parseField(body, "verifier"),
|
|
130
|
+
skillName: parseField(body, "skill_name"),
|
|
131
|
+
skillCreated:
|
|
132
|
+
skillCreatedRaw === undefined
|
|
133
|
+
? undefined
|
|
134
|
+
: skillCreatedRaw === "true",
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return results;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export function loadPromotions(cwd: string): PromotionCandidate[] {
|
|
142
|
+
const path = getPromotionsPath(cwd);
|
|
143
|
+
if (!existsSync(path)) return [];
|
|
144
|
+
try {
|
|
145
|
+
return parsePromotionsMarkdown(readFileSync(path, "utf-8"));
|
|
146
|
+
} catch {
|
|
147
|
+
return [];
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export function writePromotions(cwd: string, candidates: PromotionCandidate[]): void {
|
|
152
|
+
const path = getPromotionsPath(cwd);
|
|
153
|
+
const dir = dirname(path);
|
|
154
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
155
|
+
|
|
156
|
+
const header =
|
|
157
|
+
"# Memory Promotion Queue\n\n" +
|
|
158
|
+
"Candidates for graduating memories into skills, enforcement, or project rules.\n" +
|
|
159
|
+
"Statuses: `proposed` → `applied` | `rejected`.\n" +
|
|
160
|
+
"Never auto-write package or project rules — `project_rule` stays proposed until a human applies it.\n\n";
|
|
161
|
+
|
|
162
|
+
const body = candidates.map(formatPromotionBlock).join("\n");
|
|
163
|
+
writeFileSync(path, header + body, "utf-8");
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Append new candidates, skipping ids that already exist.
|
|
168
|
+
* Returns how many were newly added.
|
|
169
|
+
*/
|
|
170
|
+
export function appendPromotions(
|
|
171
|
+
cwd: string,
|
|
172
|
+
incoming: PromotionCandidate[],
|
|
173
|
+
): number {
|
|
174
|
+
const existing = loadPromotions(cwd);
|
|
175
|
+
const seen = new Set(existing.map((c) => c.id));
|
|
176
|
+
let added = 0;
|
|
177
|
+
for (const c of incoming) {
|
|
178
|
+
if (seen.has(c.id)) continue;
|
|
179
|
+
existing.push(c);
|
|
180
|
+
seen.add(c.id);
|
|
181
|
+
added += 1;
|
|
182
|
+
}
|
|
183
|
+
if (added > 0) writePromotions(cwd, existing);
|
|
184
|
+
return added;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
export function updatePromotionStatus(
|
|
188
|
+
cwd: string,
|
|
189
|
+
id: string,
|
|
190
|
+
status: PromotionStatus,
|
|
191
|
+
): boolean {
|
|
192
|
+
return updatePromotionStatuses(cwd, [{ id, status }]) === 1;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/** Batch status updates — one load/write cycle. Returns how many ids were updated. */
|
|
196
|
+
export function updatePromotionStatuses(
|
|
197
|
+
cwd: string,
|
|
198
|
+
updates: { id: string; status: PromotionStatus }[],
|
|
199
|
+
): number {
|
|
200
|
+
return updatePromotionCandidates(
|
|
201
|
+
cwd,
|
|
202
|
+
updates.map((u) => ({ id: u.id, patch: { status: u.status } })),
|
|
203
|
+
);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/** Mutable fields only — identity keys cannot be patched. */
|
|
207
|
+
export type PromotionCandidatePatch = Partial<
|
|
208
|
+
Omit<PromotionCandidate, "id" | "destination" | "text" | "category" | "createdAt">
|
|
209
|
+
>;
|
|
210
|
+
|
|
211
|
+
const IMMUTABLE_PROMOTION_KEYS = new Set([
|
|
212
|
+
"id",
|
|
213
|
+
"destination",
|
|
214
|
+
"text",
|
|
215
|
+
"category",
|
|
216
|
+
"createdAt",
|
|
217
|
+
]);
|
|
218
|
+
|
|
219
|
+
const STRING_PATCH_KEYS = new Set([
|
|
220
|
+
"reason",
|
|
221
|
+
"trigger",
|
|
222
|
+
"action",
|
|
223
|
+
"verifier",
|
|
224
|
+
"skillName",
|
|
225
|
+
]);
|
|
226
|
+
|
|
227
|
+
/** Normalize one patch entry; returns undefined to leave the stored field unchanged. */
|
|
228
|
+
function normalizePatchValue(
|
|
229
|
+
key: string,
|
|
230
|
+
value: unknown,
|
|
231
|
+
): unknown | undefined {
|
|
232
|
+
if (value === undefined || value === null) return undefined;
|
|
233
|
+
if (key === "status") {
|
|
234
|
+
return typeof value === "string" && STATUSES.includes(value as PromotionStatus)
|
|
235
|
+
? value
|
|
236
|
+
: undefined;
|
|
237
|
+
}
|
|
238
|
+
if (key === "evidenceCount") {
|
|
239
|
+
return typeof value === "number" && Number.isFinite(value)
|
|
240
|
+
? value
|
|
241
|
+
: undefined;
|
|
242
|
+
}
|
|
243
|
+
if (key === "skillCreated") {
|
|
244
|
+
return typeof value === "boolean" ? value : undefined;
|
|
245
|
+
}
|
|
246
|
+
if (STRING_PATCH_KEYS.has(key)) {
|
|
247
|
+
return typeof value === "string" ? value : undefined;
|
|
248
|
+
}
|
|
249
|
+
return undefined;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/** Batch patch promotion candidates — one load/write. Returns how many ids were updated. */
|
|
253
|
+
export function updatePromotionCandidates(
|
|
254
|
+
cwd: string,
|
|
255
|
+
updates: { id: string; patch: PromotionCandidatePatch }[],
|
|
256
|
+
): number {
|
|
257
|
+
if (updates.length === 0) return 0;
|
|
258
|
+
const existing = loadPromotions(cwd);
|
|
259
|
+
const byId = new Map(existing.map((c) => [c.id, c]));
|
|
260
|
+
let n = 0;
|
|
261
|
+
for (const u of updates) {
|
|
262
|
+
const entry = byId.get(u.id);
|
|
263
|
+
if (!entry) continue;
|
|
264
|
+
const safePatch: PromotionCandidatePatch = {};
|
|
265
|
+
for (const [key, value] of Object.entries(u.patch)) {
|
|
266
|
+
if (IMMUTABLE_PROMOTION_KEYS.has(key)) continue;
|
|
267
|
+
const normalized = normalizePatchValue(key, value);
|
|
268
|
+
if (normalized === undefined) continue;
|
|
269
|
+
(safePatch as Record<string, unknown>)[key] = normalized;
|
|
270
|
+
}
|
|
271
|
+
if (Object.keys(safePatch).length === 0) continue;
|
|
272
|
+
Object.assign(entry, safePatch);
|
|
273
|
+
entry.id = u.id;
|
|
274
|
+
n += 1;
|
|
275
|
+
}
|
|
276
|
+
if (n > 0) writePromotions(cwd, [...byId.values()]);
|
|
277
|
+
return n;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
export function listProposedPromotions(cwd: string): PromotionCandidate[] {
|
|
281
|
+
return loadPromotions(cwd).filter((c) => c.status === "proposed");
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/** Compact section for situation-report injection (~150 tokens). */
|
|
285
|
+
export function formatPromotionsForReport(
|
|
286
|
+
cwd: string,
|
|
287
|
+
maxItems: number = 5,
|
|
288
|
+
): string {
|
|
289
|
+
const proposed = listProposedPromotions(cwd).slice(0, maxItems);
|
|
290
|
+
if (proposed.length === 0) return "";
|
|
291
|
+
|
|
292
|
+
const lines = proposed.map((c) => {
|
|
293
|
+
const dest =
|
|
294
|
+
c.destination === "project_rule"
|
|
295
|
+
? "project_rule (propose only)"
|
|
296
|
+
: c.destination;
|
|
297
|
+
return `- [${dest}] ${c.text}`;
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
const omitted = listProposedPromotions(cwd).length - proposed.length;
|
|
301
|
+
if (omitted > 0) {
|
|
302
|
+
lines.push(`- ... ${omitted} more proposed`);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
return (
|
|
306
|
+
`## Promotion Candidates\n` +
|
|
307
|
+
`Open promotions in \`.pi/memory/promotions.md\` — apply safe ones; never auto-write package rules.\n` +
|
|
308
|
+
lines.join("\n") +
|
|
309
|
+
"\n"
|
|
310
|
+
);
|
|
311
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolve effective model/provider for an agent.
|
|
3
|
+
* Priority: agent_overrides[name] > agent frontmatter > agent_provider/agent_model setting > parent
|
|
4
|
+
* null in overrides = skip settings, use parent directly.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { getSetting } from "./project-settings.js";
|
|
8
|
+
|
|
9
|
+
export function resolveAgentModelProvider(
|
|
10
|
+
agentName: string,
|
|
11
|
+
agent: { model?: string; provider?: string },
|
|
12
|
+
parentModelId: string | undefined,
|
|
13
|
+
parentProvider: string | undefined,
|
|
14
|
+
cwd: string
|
|
15
|
+
): { model: string | undefined; provider: string | undefined } {
|
|
16
|
+
const overrides = getSetting(cwd, "agent_overrides");
|
|
17
|
+
const override = overrides[agentName];
|
|
18
|
+
|
|
19
|
+
if (override) {
|
|
20
|
+
// null = explicitly use parent (skip agent frontmatter AND global settings)
|
|
21
|
+
// Model and provider resolve independently on purpose: a partial override
|
|
22
|
+
// (e.g. only model) still lets provider fall through frontmatter → settings → parent.
|
|
23
|
+
const model = override.model === null ? parentModelId : (override.model || agent.model || getSetting(cwd, "agent_model") || parentModelId);
|
|
24
|
+
const provider = override.provider === null ? parentProvider : (override.provider || agent.provider || getSetting(cwd, "agent_provider") || parentProvider);
|
|
25
|
+
return { model: model || undefined, provider: provider || undefined };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// No override: agent frontmatter > global setting > parent
|
|
29
|
+
const model = agent.model || getSetting(cwd, "agent_model") || parentModelId;
|
|
30
|
+
const provider = agent.provider || getSetting(cwd, "agent_provider") || parentProvider;
|
|
31
|
+
return { model: model || undefined, provider: provider || undefined };
|
|
32
|
+
}
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Review loop status — status bar indicator + transcript cards.
|
|
3
|
+
*
|
|
4
|
+
* Status bar: persistent "6-review" key (last in bar, after git).
|
|
5
|
+
* Cards: durable entry renderers via pi.appendEntry (persist across reload,
|
|
6
|
+
* no LLM context cost).
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
10
|
+
import { Box, Text } from "@earendil-works/pi-tui";
|
|
11
|
+
import { onStateTransition, readReviewState, type ReviewState } from "./pi-config-review-state.js";
|
|
12
|
+
import { ICON_REVIEW_CLEAN, ICON_REVIEW_NEEDED, ICON_REVIEW_PROGRESS, ICON_REVIEW_FINDINGS } from "./icons.js";
|
|
13
|
+
import { getSetting } from "./project-settings.js";
|
|
14
|
+
import { setSlot, clearSlot } from "./status-bar.js";
|
|
15
|
+
|
|
16
|
+
interface ReviewStatusData {
|
|
17
|
+
status: ReviewState["status"];
|
|
18
|
+
cycle: number;
|
|
19
|
+
findings_count: number;
|
|
20
|
+
tests_passed: boolean;
|
|
21
|
+
reviewers_pending: string[];
|
|
22
|
+
reviewers_total: number;
|
|
23
|
+
timestamp: number;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Deduplication — skip append if status+cycle hasn't changed
|
|
27
|
+
let lastAppendedKey = "";
|
|
28
|
+
|
|
29
|
+
/** Dedup key shared by status bar and transcript cards. */
|
|
30
|
+
function stateKey(s: { status: string; cycle: number; findings_count: number; tests_passed: boolean; reviewers_pending: { length: number } }): string {
|
|
31
|
+
return `${s.status}:${s.cycle}:${s.findings_count}:${s.tests_passed}:${s.reviewers_pending.length}`;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function registerReviewUI(pi: ExtensionAPI): void {
|
|
35
|
+
// Register the entry renderer for "review-status" custom type
|
|
36
|
+
pi.registerEntryRenderer<ReviewStatusData>("review-status", (entry, { expanded }, theme) => {
|
|
37
|
+
const data = entry.data ?? {
|
|
38
|
+
status: "none" as const,
|
|
39
|
+
cycle: 0,
|
|
40
|
+
findings_count: 0,
|
|
41
|
+
tests_passed: false,
|
|
42
|
+
reviewers_pending: [],
|
|
43
|
+
reviewers_total: 0,
|
|
44
|
+
timestamp: Date.now(),
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
if (data.status === "none") {
|
|
48
|
+
return new Box(0, 0, (text) => text);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const box = new Box(1, 1, (text) => theme.bg("customMessageBg", text));
|
|
52
|
+
|
|
53
|
+
let line: string;
|
|
54
|
+
switch (data.status) {
|
|
55
|
+
case "in_progress": {
|
|
56
|
+
const testsIcon = data.tests_passed ? "✅" : "⏳";
|
|
57
|
+
const waiting = data.reviewers_pending.length > 0
|
|
58
|
+
? ` — waiting: ${data.reviewers_pending.join(", ")}`
|
|
59
|
+
: "";
|
|
60
|
+
line = `🔄 Review cycle ${data.cycle} — ${data.findings_count} findings — tests ${testsIcon}${waiting}`;
|
|
61
|
+
break;
|
|
62
|
+
}
|
|
63
|
+
case "clean": {
|
|
64
|
+
const testsLabel = data.tests_passed ? "tests passed" : "tests pending";
|
|
65
|
+
line = `✅ Review clean — cycle ${data.cycle} — ${testsLabel} — ${data.tests_passed ? "ready to commit" : "awaiting tests"}`;
|
|
66
|
+
break;
|
|
67
|
+
}
|
|
68
|
+
case "needs_review":
|
|
69
|
+
line = `⚠️ Code changed — review needed`;
|
|
70
|
+
break;
|
|
71
|
+
case "has_findings":
|
|
72
|
+
line = `🔍 Review cycle ${data.cycle} — ${data.findings_count} finding${data.findings_count !== 1 ? "s" : ""} — needs fixes`;
|
|
73
|
+
break;
|
|
74
|
+
default:
|
|
75
|
+
line = `📋 Review: ${data.status}`;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const colorMap: Record<string, string> = {
|
|
79
|
+
in_progress: "accent",
|
|
80
|
+
clean: "success",
|
|
81
|
+
needs_review: "warning",
|
|
82
|
+
has_findings: "error",
|
|
83
|
+
};
|
|
84
|
+
const color = colorMap[data.status] || "dim";
|
|
85
|
+
box.addChild(new Text(theme.fg(color, line), 0, 0));
|
|
86
|
+
|
|
87
|
+
if (expanded && data.timestamp) {
|
|
88
|
+
box.addChild(new Text(theme.fg("dim", new Date(data.timestamp).toLocaleString()), 0, 1));
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return box;
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
// ── Status bar indicator ─────────────────────────────────────────────
|
|
95
|
+
|
|
96
|
+
let lastCtx: ExtensionContext | null = null;
|
|
97
|
+
let lastBarKey = "";
|
|
98
|
+
|
|
99
|
+
function updateStatusBar(state: ReviewState): void {
|
|
100
|
+
if (!lastCtx?.hasUI) return;
|
|
101
|
+
try {
|
|
102
|
+
const ctx = lastCtx;
|
|
103
|
+
|
|
104
|
+
let label: string;
|
|
105
|
+
let color: string;
|
|
106
|
+
switch (state.status) {
|
|
107
|
+
case "none":
|
|
108
|
+
clearSlot("review", ctx);
|
|
109
|
+
lastBarKey = "";
|
|
110
|
+
return;
|
|
111
|
+
case "needs_review":
|
|
112
|
+
label = `${ICON_REVIEW_NEEDED} review needed`;
|
|
113
|
+
color = "warning";
|
|
114
|
+
break;
|
|
115
|
+
case "in_progress": {
|
|
116
|
+
const pending = state.reviewers_pending.length;
|
|
117
|
+
const total = state.reviewers_total;
|
|
118
|
+
label = `${ICON_REVIEW_PROGRESS} review ${total - pending}/${total}`;
|
|
119
|
+
color = "accent";
|
|
120
|
+
break;
|
|
121
|
+
}
|
|
122
|
+
case "has_findings":
|
|
123
|
+
label = `${ICON_REVIEW_FINDINGS} ${state.findings_count} finding${state.findings_count !== 1 ? "s" : ""}`;
|
|
124
|
+
color = "error";
|
|
125
|
+
break;
|
|
126
|
+
case "clean":
|
|
127
|
+
label = state.tests_passed ? `${ICON_REVIEW_CLEAN} ready` : `${ICON_REVIEW_CLEAN} review clean`;
|
|
128
|
+
color = "success";
|
|
129
|
+
break;
|
|
130
|
+
default:
|
|
131
|
+
label = `${ICON_REVIEW_PROGRESS} ${state.status}`;
|
|
132
|
+
color = "dim";
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const barKey = stateKey(state);
|
|
136
|
+
if (barKey === lastBarKey) return;
|
|
137
|
+
lastBarKey = barKey;
|
|
138
|
+
setSlot("review", ctx.ui.theme.fg(color, label), ctx);
|
|
139
|
+
} catch { /* stale ctx after session replacement */ }
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Capture ctx and show current state on load
|
|
143
|
+
pi.on("session_start", (_event, ctx) => {
|
|
144
|
+
lastCtx = ctx;
|
|
145
|
+
if (getSetting(ctx.cwd, "review_loop_enforcement")) {
|
|
146
|
+
updateStatusBar(readReviewState(ctx.cwd));
|
|
147
|
+
} else if (ctx.hasUI) {
|
|
148
|
+
clearSlot("review", ctx);
|
|
149
|
+
lastBarKey = "";
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
pi.on("agent_end", (_event, ctx) => { lastCtx = ctx; });
|
|
153
|
+
|
|
154
|
+
// Poll pi-config-review-state.json for cross-process updates (subagents write state too)
|
|
155
|
+
const reviewPoller = setInterval(() => {
|
|
156
|
+
if (!lastCtx) return;
|
|
157
|
+
// Access a ctx property to detect stale/disposed context (throws if invalidated)
|
|
158
|
+
try { void lastCtx.ui.theme; } catch { lastCtx = null; return; }
|
|
159
|
+
try {
|
|
160
|
+
if (!getSetting(lastCtx.cwd, "review_loop_enforcement")) {
|
|
161
|
+
if (lastCtx.hasUI) clearSlot("review", lastCtx);
|
|
162
|
+
lastBarKey = "";
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
updateStatusBar(readReviewState(lastCtx.cwd));
|
|
166
|
+
} catch (e: any) {
|
|
167
|
+
console.debug("[review-ui] poller error:", e?.message);
|
|
168
|
+
}
|
|
169
|
+
}, 5000);
|
|
170
|
+
if (reviewPoller.unref) reviewPoller.unref();
|
|
171
|
+
|
|
172
|
+
pi.on("session_shutdown", () => {
|
|
173
|
+
clearInterval(reviewPoller);
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
// ── Hook into review state transitions ───────────────────────────────
|
|
177
|
+
|
|
178
|
+
onStateTransition((state: ReviewState) => {
|
|
179
|
+
// Always update status bar (even for "none" — clears it)
|
|
180
|
+
// Only show if enforcement is enabled
|
|
181
|
+
if (lastCtx && getSetting(lastCtx.cwd, "review_loop_enforcement")) {
|
|
182
|
+
updateStatusBar(state);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if (state.status === "none") return;
|
|
186
|
+
|
|
187
|
+
const data: ReviewStatusData = {
|
|
188
|
+
status: state.status,
|
|
189
|
+
cycle: state.cycle,
|
|
190
|
+
findings_count: state.findings_count,
|
|
191
|
+
tests_passed: state.tests_passed,
|
|
192
|
+
reviewers_pending: [...state.reviewers_pending],
|
|
193
|
+
reviewers_total: state.reviewers_total,
|
|
194
|
+
timestamp: Date.now(),
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
// Deduplicate transcript cards — skip if nothing meaningful changed
|
|
198
|
+
const key = stateKey(data);
|
|
199
|
+
if (key === lastAppendedKey) return;
|
|
200
|
+
lastAppendedKey = key;
|
|
201
|
+
|
|
202
|
+
pi.appendEntry<ReviewStatusData>("review-status", data);
|
|
203
|
+
});
|
|
204
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure placeholder substitution for rules/*.md prompt text.
|
|
3
|
+
*
|
|
4
|
+
* Kept in its own module (no heavy imports) so it can be unit tested without
|
|
5
|
+
* pulling in the full orchestrator dependency chain (e.g. rules.ts → async-agents.ts
|
|
6
|
+
* → @earendil-works/pi-tui, which is a runtime-only dependency not present in
|
|
7
|
+
* the test install).
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
export function substituteRulePlaceholders(text: string, values: { reviewLoopMaxCycles: number }): string {
|
|
11
|
+
return text.replaceAll("{{REVIEW_LOOP_MAX_CYCLES}}", String(values.reviewLoopMaxCycles));
|
|
12
|
+
}
|