@juicesharp/rpiv-pi 0.4.4 → 0.5.0
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/extensions/rpiv-core/agents.ts +0 -34
- package/extensions/rpiv-core/constants.ts +3 -0
- package/extensions/rpiv-core/guidance.ts +5 -4
- package/extensions/rpiv-core/index.ts +16 -267
- package/extensions/rpiv-core/package-checks.ts +12 -30
- package/extensions/rpiv-core/session-hooks.ts +120 -0
- package/extensions/rpiv-core/setup-command.ts +109 -0
- package/extensions/rpiv-core/siblings.ts +48 -0
- package/extensions/rpiv-core/update-agents-command.ts +42 -0
- package/package.json +2 -5
- package/skills/explore/SKILL.md +113 -36
- package/scripts/types.js +0 -1
|
@@ -57,12 +57,6 @@ export interface SyncResult {
|
|
|
57
57
|
pendingRemove: string[];
|
|
58
58
|
/** Per-file errors collected during sync. */
|
|
59
59
|
errors: SyncError[];
|
|
60
|
-
|
|
61
|
-
// -- Legacy aliases (backward compat for existing callers) --
|
|
62
|
-
/** Alias: added + updated (files written by this run). */
|
|
63
|
-
copied: string[];
|
|
64
|
-
/** Alias: unchanged + pendingUpdate + files that errored during read and were not written. */
|
|
65
|
-
skipped: string[];
|
|
66
60
|
}
|
|
67
61
|
|
|
68
62
|
/** Create an empty SyncResult with all arrays initialized. */
|
|
@@ -75,8 +69,6 @@ function emptySyncResult(): SyncResult {
|
|
|
75
69
|
pendingUpdate: [],
|
|
76
70
|
pendingRemove: [],
|
|
77
71
|
errors: [],
|
|
78
|
-
copied: [],
|
|
79
|
-
skipped: [],
|
|
80
72
|
};
|
|
81
73
|
}
|
|
82
74
|
|
|
@@ -219,7 +211,6 @@ export function syncBundledAgents(cwd: string, apply: boolean): SyncResult {
|
|
|
219
211
|
op: "read-src",
|
|
220
212
|
message: e instanceof Error ? e.message : String(e),
|
|
221
213
|
});
|
|
222
|
-
result.skipped.push(entry);
|
|
223
214
|
continue;
|
|
224
215
|
}
|
|
225
216
|
try {
|
|
@@ -230,18 +221,15 @@ export function syncBundledAgents(cwd: string, apply: boolean): SyncResult {
|
|
|
230
221
|
op: "read-dest",
|
|
231
222
|
message: e instanceof Error ? e.message : String(e),
|
|
232
223
|
});
|
|
233
|
-
result.skipped.push(entry);
|
|
234
224
|
continue;
|
|
235
225
|
}
|
|
236
226
|
|
|
237
227
|
if (Buffer.compare(srcContent, destContent) === 0) {
|
|
238
228
|
result.unchanged.push(entry);
|
|
239
|
-
result.skipped.push(entry);
|
|
240
229
|
} else if (apply) {
|
|
241
230
|
try {
|
|
242
231
|
copyFileSync(src, dest);
|
|
243
232
|
result.updated.push(entry);
|
|
244
|
-
result.copied.push(entry);
|
|
245
233
|
} catch (e) {
|
|
246
234
|
result.errors.push({
|
|
247
235
|
file: entry,
|
|
@@ -251,7 +239,6 @@ export function syncBundledAgents(cwd: string, apply: boolean): SyncResult {
|
|
|
251
239
|
}
|
|
252
240
|
} else {
|
|
253
241
|
result.pendingUpdate.push(entry);
|
|
254
|
-
result.skipped.push(entry);
|
|
255
242
|
}
|
|
256
243
|
}
|
|
257
244
|
|
|
@@ -287,26 +274,5 @@ export function syncBundledAgents(cwd: string, apply: boolean): SyncResult {
|
|
|
287
274
|
: [...sourceEntries, ...result.pendingRemove];
|
|
288
275
|
writeManifest(targetDir, manifestEntries);
|
|
289
276
|
|
|
290
|
-
// 6. Populate legacy `copied` alias (added + updated)
|
|
291
|
-
for (const name of result.added) {
|
|
292
|
-
result.copied.push(name);
|
|
293
|
-
}
|
|
294
|
-
// updated files were pushed to `copied` inline in the loop above
|
|
295
|
-
|
|
296
277
|
return result;
|
|
297
278
|
}
|
|
298
|
-
|
|
299
|
-
// ---------------------------------------------------------------------------
|
|
300
|
-
// Backward-compatible wrapper
|
|
301
|
-
// ---------------------------------------------------------------------------
|
|
302
|
-
|
|
303
|
-
/**
|
|
304
|
-
* Legacy entry point — delegates to syncBundledAgents.
|
|
305
|
-
* Kept for backward compatibility; prefer syncBundledAgents for new callers.
|
|
306
|
-
*/
|
|
307
|
-
export function copyBundledAgents(cwd: string, overwrite: boolean): {
|
|
308
|
-
copied: string[];
|
|
309
|
-
skipped: string[];
|
|
310
|
-
} {
|
|
311
|
-
return syncBundledAgents(cwd, overwrite);
|
|
312
|
-
}
|
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
import { existsSync, readFileSync } from "node:fs";
|
|
23
23
|
import { dirname, relative, sep, isAbsolute, join } from "node:path";
|
|
24
24
|
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
25
|
+
import { FLAG_DEBUG, MSG_TYPE_GUIDANCE } from "./constants.js";
|
|
25
26
|
|
|
26
27
|
// ---------------------------------------------------------------------------
|
|
27
28
|
// Guidance Resolution
|
|
@@ -145,9 +146,9 @@ export function injectRootGuidance(cwd: string, pi: ExtensionAPI): void {
|
|
|
145
146
|
kind: "architecture",
|
|
146
147
|
});
|
|
147
148
|
pi.sendMessage({
|
|
148
|
-
customType:
|
|
149
|
+
customType: MSG_TYPE_GUIDANCE,
|
|
149
150
|
content: `## Project Guidance: ${label}\n\n${content}`,
|
|
150
|
-
display:
|
|
151
|
+
display: !!pi.getFlag(FLAG_DEBUG),
|
|
151
152
|
});
|
|
152
153
|
}
|
|
153
154
|
|
|
@@ -185,9 +186,9 @@ export function handleToolCallGuidance(
|
|
|
185
186
|
);
|
|
186
187
|
|
|
187
188
|
pi.sendMessage({
|
|
188
|
-
customType:
|
|
189
|
+
customType: MSG_TYPE_GUIDANCE,
|
|
189
190
|
content: contextParts.join("\n\n---\n\n"),
|
|
190
|
-
display:
|
|
191
|
+
display: !!pi.getFlag(FLAG_DEBUG),
|
|
191
192
|
});
|
|
192
193
|
}
|
|
193
194
|
|
|
@@ -1,276 +1,25 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* rpiv-core —
|
|
2
|
+
* rpiv-core — Pure-orchestrator extension for rpiv-pi.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* - Git context injection (replaces !`git ...` shell evaluation in skills)
|
|
7
|
-
* - thoughts/ directory scaffolding on session start
|
|
8
|
-
* - Bundled-agent auto-copy into <cwd>/.pi/agents/
|
|
9
|
-
* - Aggregated session_start warning for missing sibling plugins
|
|
10
|
-
* - /rpiv-update-agents, /rpiv-setup slash commands
|
|
4
|
+
* Composes session hooks and the two slash commands. All logic lives in the
|
|
5
|
+
* registrar modules; this file is the table of contents.
|
|
11
6
|
*
|
|
12
|
-
* Tool-owning plugins are siblings
|
|
13
|
-
* @juicesharp/rpiv-todo, @juicesharp/rpiv-advisor, @juicesharp/rpiv-web-tools.
|
|
14
|
-
* Install via /rpiv-setup.
|
|
7
|
+
* Tool-owning plugins are siblings (see siblings.ts); install via /rpiv-setup.
|
|
15
8
|
*/
|
|
16
9
|
|
|
17
|
-
import {
|
|
18
|
-
import {
|
|
19
|
-
import {
|
|
20
|
-
import {
|
|
21
|
-
import {
|
|
22
|
-
clearGitContextCache,
|
|
23
|
-
isGitMutatingCommand,
|
|
24
|
-
resetInjectedMarker,
|
|
25
|
-
takeGitContextIfChanged,
|
|
26
|
-
} from "./git-context.js";
|
|
27
|
-
import { syncBundledAgents } from "./agents.js";
|
|
28
|
-
import { spawnPiInstall } from "./pi-installer.js";
|
|
29
|
-
import {
|
|
30
|
-
hasPiSubagentsInstalled,
|
|
31
|
-
hasRpivAskUserQuestionInstalled,
|
|
32
|
-
hasRpivTodoInstalled,
|
|
33
|
-
hasRpivAdvisorInstalled,
|
|
34
|
-
hasRpivWebToolsInstalled,
|
|
35
|
-
} from "./package-checks.js";
|
|
10
|
+
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
11
|
+
import { FLAG_DEBUG } from "./constants.js";
|
|
12
|
+
import { registerSessionHooks } from "./session-hooks.js";
|
|
13
|
+
import { registerSetupCommand } from "./setup-command.js";
|
|
14
|
+
import { registerUpdateAgentsCommand } from "./update-agents-command.js";
|
|
36
15
|
|
|
37
16
|
export default function (pi: ExtensionAPI) {
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
// Scaffold thoughts/ directory structure (artifact chain)
|
|
44
|
-
const dirs = [
|
|
45
|
-
"thoughts/shared/research",
|
|
46
|
-
"thoughts/shared/questions",
|
|
47
|
-
"thoughts/shared/designs",
|
|
48
|
-
"thoughts/shared/plans",
|
|
49
|
-
"thoughts/shared/handoffs",
|
|
50
|
-
];
|
|
51
|
-
for (const dir of dirs) {
|
|
52
|
-
mkdirSync(join(ctx.cwd, dir), { recursive: true });
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
// Inject git context once into the transcript
|
|
56
|
-
const gitMsg = await takeGitContextIfChanged(pi);
|
|
57
|
-
if (gitMsg) {
|
|
58
|
-
pi.sendMessage({ customType: "rpiv-git-context", content: gitMsg, display: false });
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
// Sync bundled agents into <cwd>/.pi/agents/
|
|
62
|
-
// Detect-only mode: adds new files, detects drift, does NOT overwrite or remove.
|
|
63
|
-
const agentResult = syncBundledAgents(ctx.cwd, false);
|
|
64
|
-
if (ctx.hasUI) {
|
|
65
|
-
if (agentResult.added.length > 0) {
|
|
66
|
-
ctx.ui.notify(
|
|
67
|
-
`Copied ${agentResult.added.length} rpiv-pi agent(s) to .pi/agents/`,
|
|
68
|
-
"info",
|
|
69
|
-
);
|
|
70
|
-
}
|
|
71
|
-
const driftCount = agentResult.pendingUpdate.length + agentResult.pendingRemove.length;
|
|
72
|
-
if (driftCount > 0) {
|
|
73
|
-
const parts: string[] = [];
|
|
74
|
-
if (agentResult.pendingUpdate.length > 0) {
|
|
75
|
-
parts.push(`${agentResult.pendingUpdate.length} outdated`);
|
|
76
|
-
}
|
|
77
|
-
if (agentResult.pendingRemove.length > 0) {
|
|
78
|
-
parts.push(`${agentResult.pendingRemove.length} removed from bundle`);
|
|
79
|
-
}
|
|
80
|
-
ctx.ui.notify(
|
|
81
|
-
`${parts.join(", ")} agent(s). Run /rpiv-update-agents to sync.`,
|
|
82
|
-
"info",
|
|
83
|
-
);
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
// Aggregated warning for any missing sibling plugins
|
|
88
|
-
if (ctx.hasUI) {
|
|
89
|
-
const missing: string[] = [];
|
|
90
|
-
if (!hasPiSubagentsInstalled()) missing.push("@tintinweb/pi-subagents");
|
|
91
|
-
if (!hasRpivAskUserQuestionInstalled()) missing.push("@juicesharp/rpiv-ask-user-question");
|
|
92
|
-
if (!hasRpivTodoInstalled()) missing.push("@juicesharp/rpiv-todo");
|
|
93
|
-
if (!hasRpivAdvisorInstalled()) missing.push("@juicesharp/rpiv-advisor");
|
|
94
|
-
if (!hasRpivWebToolsInstalled()) missing.push("@juicesharp/rpiv-web-tools");
|
|
95
|
-
if (missing.length > 0) {
|
|
96
|
-
ctx.ui.notify(
|
|
97
|
-
`rpiv-pi requires ${missing.length} sibling extension(s): ${missing.join(", ")}. Run /rpiv-setup to install them.`,
|
|
98
|
-
"warning",
|
|
99
|
-
);
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
});
|
|
103
|
-
|
|
104
|
-
// ── Session Compact — drop injection state, re-inject root guidance ────
|
|
105
|
-
pi.on("session_compact", async (_event, ctx) => {
|
|
106
|
-
clearInjectionState();
|
|
107
|
-
clearGitContextCache();
|
|
108
|
-
resetInjectedMarker();
|
|
109
|
-
injectRootGuidance(ctx.cwd, pi);
|
|
110
|
-
const gitMsg = await takeGitContextIfChanged(pi);
|
|
111
|
-
if (gitMsg) {
|
|
112
|
-
pi.sendMessage({ customType: "rpiv-git-context", content: gitMsg, display: false });
|
|
113
|
-
}
|
|
114
|
-
});
|
|
115
|
-
|
|
116
|
-
// ── Session Shutdown ───────────────────────────────────────────────────
|
|
117
|
-
pi.on("session_shutdown", async (_event, _ctx) => {
|
|
118
|
-
clearInjectionState();
|
|
119
|
-
clearGitContextCache();
|
|
120
|
-
resetInjectedMarker();
|
|
121
|
-
});
|
|
122
|
-
|
|
123
|
-
// ── Guidance Injection + Git Cache Invalidation ────────────────────────
|
|
124
|
-
pi.on("tool_call", async (event, ctx) => {
|
|
125
|
-
handleToolCallGuidance(event, ctx, pi);
|
|
126
|
-
if (isToolCallEventType("bash", event) && isGitMutatingCommand(event.input.command)) {
|
|
127
|
-
clearGitContextCache();
|
|
128
|
-
}
|
|
129
|
-
});
|
|
130
|
-
|
|
131
|
-
// ── Git Context Injection (only when cache diverges from transcript) ───
|
|
132
|
-
pi.on("before_agent_start", async (_event, _ctx) => {
|
|
133
|
-
const content = await takeGitContextIfChanged(pi);
|
|
134
|
-
if (!content) return;
|
|
135
|
-
return {
|
|
136
|
-
message: { customType: "rpiv-git-context", content, display: false },
|
|
137
|
-
};
|
|
138
|
-
});
|
|
139
|
-
|
|
140
|
-
// ── /rpiv-update-agents Command ────────────────────────────────────────
|
|
141
|
-
pi.registerCommand("rpiv-update-agents", {
|
|
142
|
-
description: "Sync rpiv-pi bundled agents into .pi/agents/: add new, update changed, remove stale",
|
|
143
|
-
handler: async (_args, ctx) => {
|
|
144
|
-
const result = syncBundledAgents(ctx.cwd, true);
|
|
145
|
-
if (!ctx.hasUI) return;
|
|
146
|
-
|
|
147
|
-
const totalSynced = result.added.length + result.updated.length + result.removed.length;
|
|
148
|
-
if (totalSynced === 0 && result.errors.length === 0) {
|
|
149
|
-
ctx.ui.notify("All agents already up-to-date.", "info");
|
|
150
|
-
return;
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
const parts: string[] = [];
|
|
154
|
-
if (result.added.length > 0) parts.push(`${result.added.length} added`);
|
|
155
|
-
if (result.updated.length > 0) parts.push(`${result.updated.length} updated`);
|
|
156
|
-
if (result.removed.length > 0) parts.push(`${result.removed.length} removed`);
|
|
157
|
-
|
|
158
|
-
const summary = parts.length > 0
|
|
159
|
-
? `Synced agents: ${parts.join(", ")}.`
|
|
160
|
-
: "No changes needed.";
|
|
161
|
-
|
|
162
|
-
if (result.errors.length > 0) {
|
|
163
|
-
ctx.ui.notify(
|
|
164
|
-
`${summary} ${result.errors.length} error(s): ${result.errors.map((e) => e.message).join("; ")}`,
|
|
165
|
-
"warning",
|
|
166
|
-
);
|
|
167
|
-
} else {
|
|
168
|
-
ctx.ui.notify(summary, "info");
|
|
169
|
-
}
|
|
170
|
-
},
|
|
171
|
-
});
|
|
172
|
-
|
|
173
|
-
// ── /rpiv-setup Command ────────────────────────────────────────────────
|
|
174
|
-
pi.registerCommand("rpiv-setup", {
|
|
175
|
-
description: "Install rpiv-pi's sibling extension plugins",
|
|
176
|
-
handler: async (_args, ctx) => {
|
|
177
|
-
if (!ctx.hasUI) {
|
|
178
|
-
ctx.ui.notify("/rpiv-setup requires interactive mode", "error");
|
|
179
|
-
return;
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
const missing: Array<{ pkg: string; reason: string }> = [];
|
|
183
|
-
if (!hasPiSubagentsInstalled()) {
|
|
184
|
-
missing.push({
|
|
185
|
-
pkg: "npm:@tintinweb/pi-subagents",
|
|
186
|
-
reason: "required — provides Agent / get_subagent_result / steer_subagent tools",
|
|
187
|
-
});
|
|
188
|
-
}
|
|
189
|
-
if (!hasRpivAskUserQuestionInstalled()) {
|
|
190
|
-
missing.push({
|
|
191
|
-
pkg: "npm:@juicesharp/rpiv-ask-user-question",
|
|
192
|
-
reason: "required — provides the ask_user_question tool",
|
|
193
|
-
});
|
|
194
|
-
}
|
|
195
|
-
if (!hasRpivTodoInstalled()) {
|
|
196
|
-
missing.push({
|
|
197
|
-
pkg: "npm:@juicesharp/rpiv-todo",
|
|
198
|
-
reason: "required — provides the todo tool + /todos command + overlay widget",
|
|
199
|
-
});
|
|
200
|
-
}
|
|
201
|
-
if (!hasRpivAdvisorInstalled()) {
|
|
202
|
-
missing.push({
|
|
203
|
-
pkg: "npm:@juicesharp/rpiv-advisor",
|
|
204
|
-
reason: "required — provides the advisor tool + /advisor command",
|
|
205
|
-
});
|
|
206
|
-
}
|
|
207
|
-
if (!hasRpivWebToolsInstalled()) {
|
|
208
|
-
missing.push({
|
|
209
|
-
pkg: "npm:@juicesharp/rpiv-web-tools",
|
|
210
|
-
reason: "required — provides web_search + web_fetch tools + /web-search-config",
|
|
211
|
-
});
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
if (missing.length === 0) {
|
|
215
|
-
ctx.ui.notify(
|
|
216
|
-
"All rpiv-pi sibling dependencies already installed.",
|
|
217
|
-
"info",
|
|
218
|
-
);
|
|
219
|
-
return;
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
const lines = [
|
|
223
|
-
"rpiv-pi will install the following Pi packages via `pi install`:",
|
|
224
|
-
"",
|
|
225
|
-
...missing.map((m) => ` • ${m.pkg} (${m.reason})`),
|
|
226
|
-
"",
|
|
227
|
-
"Each install is a separate `pi install <pkg>` invocation. Your",
|
|
228
|
-
"~/.pi/agent/settings.json will be updated. Proceed?",
|
|
229
|
-
];
|
|
230
|
-
|
|
231
|
-
const confirmed = await ctx.ui.confirm("Install rpiv-pi dependencies?", lines.join("\n"));
|
|
232
|
-
if (!confirmed) {
|
|
233
|
-
ctx.ui.notify("/rpiv-setup cancelled", "info");
|
|
234
|
-
return;
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
const succeeded: string[] = [];
|
|
238
|
-
const failed: Array<{ pkg: string; error: string }> = [];
|
|
239
|
-
for (const { pkg } of missing) {
|
|
240
|
-
ctx.ui.notify(`Installing ${pkg}…`, "info");
|
|
241
|
-
try {
|
|
242
|
-
const result = await spawnPiInstall(pkg, 120_000);
|
|
243
|
-
if (result.code === 0) {
|
|
244
|
-
succeeded.push(pkg);
|
|
245
|
-
} else {
|
|
246
|
-
failed.push({
|
|
247
|
-
pkg,
|
|
248
|
-
error: (result.stderr || result.stdout || `exit ${result.code}`).trim().slice(0, 300),
|
|
249
|
-
});
|
|
250
|
-
}
|
|
251
|
-
} catch (err) {
|
|
252
|
-
failed.push({
|
|
253
|
-
pkg,
|
|
254
|
-
error: err instanceof Error ? err.message : String(err),
|
|
255
|
-
});
|
|
256
|
-
}
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
const report: string[] = [];
|
|
260
|
-
if (succeeded.length > 0) {
|
|
261
|
-
report.push(`✓ Installed: ${succeeded.join(", ")}`);
|
|
262
|
-
}
|
|
263
|
-
if (failed.length > 0) {
|
|
264
|
-
report.push(`✗ Failed:`);
|
|
265
|
-
for (const { pkg, error } of failed) {
|
|
266
|
-
report.push(` ${pkg}: ${error}`);
|
|
267
|
-
}
|
|
268
|
-
}
|
|
269
|
-
if (succeeded.length > 0) {
|
|
270
|
-
report.push("");
|
|
271
|
-
report.push("Restart your Pi session to load the newly-installed extensions.");
|
|
272
|
-
}
|
|
273
|
-
ctx.ui.notify(report.join("\n"), failed.length > 0 ? "warning" : "info");
|
|
274
|
-
},
|
|
17
|
+
pi.registerFlag(FLAG_DEBUG, {
|
|
18
|
+
description: "Show injected guidance and git-context messages",
|
|
19
|
+
type: "boolean",
|
|
20
|
+
default: false,
|
|
275
21
|
});
|
|
22
|
+
registerSessionHooks(pi);
|
|
23
|
+
registerUpdateAgentsCommand(pi);
|
|
24
|
+
registerSetupCommand(pi);
|
|
276
25
|
}
|
|
@@ -1,24 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
* Pure utility. No ExtensionAPI interactions.
|
|
2
|
+
* Detect which SIBLINGS are installed by reading ~/.pi/agent/settings.json.
|
|
3
|
+
* Pure utility — no ExtensionAPI.
|
|
5
4
|
*/
|
|
6
5
|
|
|
7
6
|
import { existsSync, readFileSync } from "node:fs";
|
|
8
7
|
import { join } from "node:path";
|
|
9
8
|
import { homedir } from "node:os";
|
|
10
|
-
|
|
11
|
-
// ---------------------------------------------------------------------------
|
|
12
|
-
// Paths
|
|
13
|
-
// ---------------------------------------------------------------------------
|
|
9
|
+
import { SIBLINGS, type SiblingPlugin } from "./siblings.js";
|
|
14
10
|
|
|
15
11
|
const PI_AGENT_SETTINGS = join(homedir(), ".pi", "agent", "settings.json");
|
|
16
12
|
|
|
17
|
-
|
|
18
|
-
// Package Detection
|
|
19
|
-
// ---------------------------------------------------------------------------
|
|
20
|
-
|
|
21
|
-
export function readInstalledPackages(): string[] {
|
|
13
|
+
function readInstalledPackages(): string[] {
|
|
22
14
|
if (!existsSync(PI_AGENT_SETTINGS)) return [];
|
|
23
15
|
try {
|
|
24
16
|
const raw = readFileSync(PI_AGENT_SETTINGS, "utf-8");
|
|
@@ -30,22 +22,12 @@ export function readInstalledPackages(): string[] {
|
|
|
30
22
|
}
|
|
31
23
|
}
|
|
32
24
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
export function hasRpivTodoInstalled(): boolean {
|
|
42
|
-
return readInstalledPackages().some((entry) => /rpiv-todo/i.test(entry));
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
export function hasRpivAdvisorInstalled(): boolean {
|
|
46
|
-
return readInstalledPackages().some((entry) => /rpiv-advisor/i.test(entry));
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
export function hasRpivWebToolsInstalled(): boolean {
|
|
50
|
-
return readInstalledPackages().some((entry) => /rpiv-web-tools/i.test(entry));
|
|
25
|
+
/**
|
|
26
|
+
* Return the SIBLINGS not currently installed.
|
|
27
|
+
* Reads ~/.pi/agent/settings.json once per call — callers that need both the
|
|
28
|
+
* full snapshot and the missing subset should call this once and filter.
|
|
29
|
+
*/
|
|
30
|
+
export function findMissingSiblings(): SiblingPlugin[] {
|
|
31
|
+
const installed = readInstalledPackages();
|
|
32
|
+
return SIBLINGS.filter((s) => !installed.some((entry) => s.matches.test(entry)));
|
|
51
33
|
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session lifecycle wiring for rpiv-core.
|
|
3
|
+
*
|
|
4
|
+
* Each handler body is a named helper; pi.on(...) lines are pure wiring.
|
|
5
|
+
* Ordering and invariants preserved verbatim from the pre-refactor index.ts.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { mkdirSync } from "node:fs";
|
|
9
|
+
import { join } from "node:path";
|
|
10
|
+
import { isToolCallEventType, type ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
11
|
+
import { clearInjectionState, handleToolCallGuidance, injectRootGuidance } from "./guidance.js";
|
|
12
|
+
import {
|
|
13
|
+
clearGitContextCache,
|
|
14
|
+
isGitMutatingCommand,
|
|
15
|
+
resetInjectedMarker,
|
|
16
|
+
takeGitContextIfChanged,
|
|
17
|
+
} from "./git-context.js";
|
|
18
|
+
import { syncBundledAgents, type SyncResult } from "./agents.js";
|
|
19
|
+
import { FLAG_DEBUG, MSG_TYPE_GIT_CONTEXT } from "./constants.js";
|
|
20
|
+
import { findMissingSiblings } from "./package-checks.js";
|
|
21
|
+
|
|
22
|
+
const THOUGHTS_DIRS = [
|
|
23
|
+
"thoughts/shared/research",
|
|
24
|
+
"thoughts/shared/questions",
|
|
25
|
+
"thoughts/shared/designs",
|
|
26
|
+
"thoughts/shared/plans",
|
|
27
|
+
"thoughts/shared/handoffs",
|
|
28
|
+
] as const;
|
|
29
|
+
|
|
30
|
+
const msgAgentsAdded = (n: number) => `Copied ${n} rpiv-pi agent(s) to .pi/agents/`;
|
|
31
|
+
const msgAgentsDrift = (parts: string[]) =>
|
|
32
|
+
`${parts.join(", ")} agent(s). Run /rpiv-update-agents to sync.`;
|
|
33
|
+
const msgMissingSiblings = (n: number, list: string) =>
|
|
34
|
+
`rpiv-pi requires ${n} sibling extension(s): ${list}. Run /rpiv-setup to install them.`;
|
|
35
|
+
|
|
36
|
+
type UI = { notify: (msg: string, sev: "info" | "warning" | "error") => void };
|
|
37
|
+
|
|
38
|
+
export function registerSessionHooks(pi: ExtensionAPI): void {
|
|
39
|
+
pi.on("session_start", async (_event, ctx) => {
|
|
40
|
+
resetInjectionState();
|
|
41
|
+
injectRootGuidance(ctx.cwd, pi);
|
|
42
|
+
scaffoldThoughtsDirs(ctx.cwd);
|
|
43
|
+
await injectGitContext(pi, (msg) =>
|
|
44
|
+
pi.sendMessage({ customType: MSG_TYPE_GIT_CONTEXT, content: msg, display: !!pi.getFlag(FLAG_DEBUG) }),
|
|
45
|
+
);
|
|
46
|
+
const agents = syncBundledAgents(ctx.cwd, false);
|
|
47
|
+
if (ctx.hasUI) {
|
|
48
|
+
notifyAgentSyncDrift(ctx.ui, agents);
|
|
49
|
+
warnMissingSiblings(ctx.ui);
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
pi.on("session_compact", async (_event, ctx) => {
|
|
54
|
+
resetInjectionState();
|
|
55
|
+
clearGitContextCache();
|
|
56
|
+
resetInjectedMarker();
|
|
57
|
+
injectRootGuidance(ctx.cwd, pi);
|
|
58
|
+
await injectGitContext(pi, (msg) =>
|
|
59
|
+
pi.sendMessage({ customType: MSG_TYPE_GIT_CONTEXT, content: msg, display: !!pi.getFlag(FLAG_DEBUG) }),
|
|
60
|
+
);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
pi.on("session_shutdown", async () => {
|
|
64
|
+
resetInjectionState();
|
|
65
|
+
clearGitContextCache();
|
|
66
|
+
resetInjectedMarker();
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
pi.on("tool_call", async (event, ctx) => {
|
|
70
|
+
handleToolCallGuidance(event, ctx, pi);
|
|
71
|
+
if (isToolCallEventType("bash", event) && isGitMutatingCommand(event.input.command)) {
|
|
72
|
+
clearGitContextCache();
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
pi.on("before_agent_start", async () => {
|
|
77
|
+
const content = await takeGitContextIfChanged(pi);
|
|
78
|
+
if (!content) return;
|
|
79
|
+
return { message: { customType: MSG_TYPE_GIT_CONTEXT, content, display: !!pi.getFlag(FLAG_DEBUG) } };
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function resetInjectionState(): void {
|
|
84
|
+
clearInjectionState();
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function scaffoldThoughtsDirs(cwd: string): void {
|
|
88
|
+
for (const dir of THOUGHTS_DIRS) {
|
|
89
|
+
mkdirSync(join(cwd, dir), { recursive: true });
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
async function injectGitContext(
|
|
94
|
+
pi: ExtensionAPI,
|
|
95
|
+
send: (msg: string) => void,
|
|
96
|
+
): Promise<void> {
|
|
97
|
+
const msg = await takeGitContextIfChanged(pi);
|
|
98
|
+
if (msg) send(msg);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function notifyAgentSyncDrift(ui: UI, result: SyncResult): void {
|
|
102
|
+
if (result.added.length > 0) {
|
|
103
|
+
ui.notify(msgAgentsAdded(result.added.length), "info");
|
|
104
|
+
}
|
|
105
|
+
const parts: string[] = [];
|
|
106
|
+
if (result.pendingUpdate.length > 0) parts.push(`${result.pendingUpdate.length} outdated`);
|
|
107
|
+
if (result.pendingRemove.length > 0) parts.push(`${result.pendingRemove.length} removed from bundle`);
|
|
108
|
+
if (parts.length > 0) {
|
|
109
|
+
ui.notify(msgAgentsDrift(parts), "info");
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function warnMissingSiblings(ui: UI): void {
|
|
114
|
+
const missing = findMissingSiblings();
|
|
115
|
+
if (missing.length === 0) return;
|
|
116
|
+
ui.notify(
|
|
117
|
+
msgMissingSiblings(missing.length, missing.map((m) => m.pkg.replace(/^npm:/, "")).join(", ")),
|
|
118
|
+
"warning",
|
|
119
|
+
);
|
|
120
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* /rpiv-setup — installs any SIBLINGS not present in ~/.pi/agent/settings.json.
|
|
3
|
+
*
|
|
4
|
+
* Serial `pi install <pkg>` loop via spawnPiInstall (Windows-safe).
|
|
5
|
+
* Reports succeeded/failed split; prompts the user to restart Pi on success.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
9
|
+
import { findMissingSiblings } from "./package-checks.js";
|
|
10
|
+
import { spawnPiInstall } from "./pi-installer.js";
|
|
11
|
+
import { type SiblingPlugin } from "./siblings.js";
|
|
12
|
+
|
|
13
|
+
const INSTALL_TIMEOUT_MS = 120_000;
|
|
14
|
+
const STDERR_SNIPPET_CHARS = 300;
|
|
15
|
+
|
|
16
|
+
const MSG_INTERACTIVE_ONLY = "/rpiv-setup requires interactive mode";
|
|
17
|
+
const MSG_ALL_INSTALLED = "All rpiv-pi sibling dependencies already installed.";
|
|
18
|
+
const MSG_CANCELLED = "/rpiv-setup cancelled";
|
|
19
|
+
const MSG_CONFIRM_TITLE = "Install rpiv-pi dependencies?";
|
|
20
|
+
const MSG_RESTART = "Restart your Pi session to load the newly-installed extensions.";
|
|
21
|
+
|
|
22
|
+
const msgInstalling = (pkg: string) => `Installing ${pkg}…`;
|
|
23
|
+
const msgInstalledLine = (pkgs: string[]) => `✓ Installed: ${pkgs.join(", ")}`;
|
|
24
|
+
const msgFailedHeader = () => `✗ Failed:`;
|
|
25
|
+
const msgFailedLine = (pkg: string, err: string) => ` ${pkg}: ${err}`;
|
|
26
|
+
|
|
27
|
+
type UI = {
|
|
28
|
+
notify: (msg: string, sev: "info" | "warning" | "error") => void;
|
|
29
|
+
confirm: (title: string, body: string) => Promise<boolean>;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
function buildConfirmBody(missing: SiblingPlugin[]): string {
|
|
33
|
+
return [
|
|
34
|
+
"rpiv-pi will install the following Pi packages via `pi install`:",
|
|
35
|
+
"",
|
|
36
|
+
...missing.map((m) => ` • ${m.pkg} (required — provides ${m.provides})`),
|
|
37
|
+
"",
|
|
38
|
+
"Each install is a separate `pi install <pkg>` invocation. Your",
|
|
39
|
+
"~/.pi/agent/settings.json will be updated. Proceed?",
|
|
40
|
+
].join("\n");
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function registerSetupCommand(pi: ExtensionAPI): void {
|
|
44
|
+
pi.registerCommand("rpiv-setup", {
|
|
45
|
+
description: "Install rpiv-pi's sibling extension plugins",
|
|
46
|
+
handler: async (_args, ctx) => {
|
|
47
|
+
if (!ctx.hasUI) {
|
|
48
|
+
ctx.ui.notify(MSG_INTERACTIVE_ONLY, "error");
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const missing = findMissingSiblings();
|
|
53
|
+
if (missing.length === 0) {
|
|
54
|
+
ctx.ui.notify(MSG_ALL_INSTALLED, "info");
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const confirmed = await ctx.ui.confirm(MSG_CONFIRM_TITLE, buildConfirmBody(missing));
|
|
59
|
+
if (!confirmed) {
|
|
60
|
+
ctx.ui.notify(MSG_CANCELLED, "info");
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const { succeeded, failed } = await installMissing(ctx.ui, missing);
|
|
65
|
+
ctx.ui.notify(buildReport(succeeded, failed), failed.length > 0 ? "warning" : "info");
|
|
66
|
+
},
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async function installMissing(
|
|
71
|
+
ui: UI,
|
|
72
|
+
missing: SiblingPlugin[],
|
|
73
|
+
): Promise<{ succeeded: string[]; failed: Array<{ pkg: string; error: string }> }> {
|
|
74
|
+
const succeeded: string[] = [];
|
|
75
|
+
const failed: Array<{ pkg: string; error: string }> = [];
|
|
76
|
+
for (const { pkg } of missing) {
|
|
77
|
+
ui.notify(msgInstalling(pkg), "info");
|
|
78
|
+
try {
|
|
79
|
+
const result = await spawnPiInstall(pkg, INSTALL_TIMEOUT_MS);
|
|
80
|
+
if (result.code === 0) {
|
|
81
|
+
succeeded.push(pkg);
|
|
82
|
+
} else {
|
|
83
|
+
failed.push({
|
|
84
|
+
pkg,
|
|
85
|
+
error: (result.stderr || result.stdout || `exit ${result.code}`)
|
|
86
|
+
.trim()
|
|
87
|
+
.slice(0, STDERR_SNIPPET_CHARS),
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
} catch (err) {
|
|
91
|
+
failed.push({ pkg, error: err instanceof Error ? err.message : String(err) });
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return { succeeded, failed };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function buildReport(succeeded: string[], failed: Array<{ pkg: string; error: string }>): string {
|
|
98
|
+
const lines: string[] = [];
|
|
99
|
+
if (succeeded.length > 0) lines.push(msgInstalledLine(succeeded));
|
|
100
|
+
if (failed.length > 0) {
|
|
101
|
+
lines.push(msgFailedHeader());
|
|
102
|
+
for (const { pkg, error } of failed) lines.push(msgFailedLine(pkg, error));
|
|
103
|
+
}
|
|
104
|
+
if (succeeded.length > 0) {
|
|
105
|
+
lines.push("");
|
|
106
|
+
lines.push(MSG_RESTART);
|
|
107
|
+
}
|
|
108
|
+
return lines.join("\n");
|
|
109
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Declarative registry of rpiv-pi's sibling Pi plugins.
|
|
3
|
+
*
|
|
4
|
+
* Single source of truth for: presence detection (package-checks.ts),
|
|
5
|
+
* session_start "missing plugins" warning (session-hooks.ts), and
|
|
6
|
+
* /rpiv-setup installer (setup-command.ts). Add a sibling here and every
|
|
7
|
+
* consumer picks it up automatically.
|
|
8
|
+
*
|
|
9
|
+
* Detection is filesystem-based via a regex over ~/.pi/agent/settings.json
|
|
10
|
+
* — no runtime import of sibling packages (keeps rpiv-core pure-orchestrator).
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
export interface SiblingPlugin {
|
|
14
|
+
/** Install spec passed to `pi install`. Prefixed with `npm:` for Pi's installer. */
|
|
15
|
+
readonly pkg: string;
|
|
16
|
+
/** Case-insensitive regex that matches the package in ~/.pi/agent/settings.json. */
|
|
17
|
+
readonly matches: RegExp;
|
|
18
|
+
/** What the sibling provides — shown in /rpiv-setup confirmation and reports. */
|
|
19
|
+
readonly provides: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export const SIBLINGS: readonly SiblingPlugin[] = [
|
|
23
|
+
{
|
|
24
|
+
pkg: "npm:@tintinweb/pi-subagents",
|
|
25
|
+
matches: /@tintinweb\/pi-subagents/i,
|
|
26
|
+
provides: "Agent / get_subagent_result / steer_subagent tools",
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
pkg: "npm:@juicesharp/rpiv-ask-user-question",
|
|
30
|
+
matches: /rpiv-ask-user-question/i,
|
|
31
|
+
provides: "ask_user_question tool",
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
pkg: "npm:@juicesharp/rpiv-todo",
|
|
35
|
+
matches: /rpiv-todo/i,
|
|
36
|
+
provides: "todo tool + /todos command + overlay widget",
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
pkg: "npm:@juicesharp/rpiv-advisor",
|
|
40
|
+
matches: /rpiv-advisor/i,
|
|
41
|
+
provides: "advisor tool + /advisor command",
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
pkg: "npm:@juicesharp/rpiv-web-tools",
|
|
45
|
+
matches: /rpiv-web-tools/i,
|
|
46
|
+
provides: "web_search + web_fetch tools + /web-search-config",
|
|
47
|
+
},
|
|
48
|
+
];
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* /rpiv-update-agents — apply-mode sync of bundled agents into <cwd>/.pi/agents/.
|
|
3
|
+
* Adds new, overwrites changed managed files, removes stale managed files.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
7
|
+
import { syncBundledAgents, type SyncResult } from "./agents.js";
|
|
8
|
+
|
|
9
|
+
const MSG_UP_TO_DATE = "All agents already up-to-date.";
|
|
10
|
+
const MSG_NO_CHANGES = "No changes needed.";
|
|
11
|
+
|
|
12
|
+
const msgSynced = (parts: string[]) => `Synced agents: ${parts.join(", ")}.`;
|
|
13
|
+
const msgSyncedWithErrors = (summary: string, errors: string[]) =>
|
|
14
|
+
`${summary} ${errors.length} error(s): ${errors.join("; ")}`;
|
|
15
|
+
|
|
16
|
+
export function registerUpdateAgentsCommand(pi: ExtensionAPI): void {
|
|
17
|
+
pi.registerCommand("rpiv-update-agents", {
|
|
18
|
+
description:
|
|
19
|
+
"Sync rpiv-pi bundled agents into .pi/agents/: add new, update changed, remove stale",
|
|
20
|
+
handler: async (_args, ctx) => {
|
|
21
|
+
const result = syncBundledAgents(ctx.cwd, true);
|
|
22
|
+
if (!ctx.hasUI) return;
|
|
23
|
+
ctx.ui.notify(formatSyncReport(result), result.errors.length > 0 ? "warning" : "info");
|
|
24
|
+
},
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function formatSyncReport(result: SyncResult): string {
|
|
29
|
+
const totalSynced = result.added.length + result.updated.length + result.removed.length;
|
|
30
|
+
if (totalSynced === 0 && result.errors.length === 0) return MSG_UP_TO_DATE;
|
|
31
|
+
|
|
32
|
+
const parts: string[] = [];
|
|
33
|
+
if (result.added.length > 0) parts.push(`${result.added.length} added`);
|
|
34
|
+
if (result.updated.length > 0) parts.push(`${result.updated.length} updated`);
|
|
35
|
+
if (result.removed.length > 0) parts.push(`${result.removed.length} removed`);
|
|
36
|
+
|
|
37
|
+
const summary = parts.length > 0 ? msgSynced(parts) : MSG_NO_CHANGES;
|
|
38
|
+
if (result.errors.length > 0) {
|
|
39
|
+
return msgSyncedWithErrors(summary, result.errors.map((e) => e.message));
|
|
40
|
+
}
|
|
41
|
+
return summary;
|
|
42
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@juicesharp/rpiv-pi",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "Skill-based development workflow for Pi Agent — discover, research, design, plan, implement, validate",
|
|
5
5
|
"keywords": ["pi-package", "pi-extension", "rpiv", "skills", "workflow"],
|
|
6
6
|
"license": "MIT",
|
|
@@ -17,16 +17,13 @@
|
|
|
17
17
|
"publishConfig": {
|
|
18
18
|
"access": "public"
|
|
19
19
|
},
|
|
20
|
-
"files": ["extensions/", "skills/", "agents/", "scripts/", "README.md"],
|
|
20
|
+
"files": ["extensions/", "skills/", "agents/", "scripts/", "README.md", "LICENSE"],
|
|
21
21
|
"pi": {
|
|
22
22
|
"extensions": ["./extensions"],
|
|
23
23
|
"skills": ["./skills"]
|
|
24
24
|
},
|
|
25
25
|
"peerDependencies": {
|
|
26
|
-
"@mariozechner/pi-ai": "*",
|
|
27
26
|
"@mariozechner/pi-coding-agent": "*",
|
|
28
|
-
"@mariozechner/pi-tui": "*",
|
|
29
|
-
"@sinclair/typebox": "*",
|
|
30
27
|
"@tintinweb/pi-subagents": "*",
|
|
31
28
|
"@juicesharp/rpiv-ask-user-question": "*",
|
|
32
29
|
"@juicesharp/rpiv-todo": "*",
|
package/skills/explore/SKILL.md
CHANGED
|
@@ -31,35 +31,91 @@ Then wait for the user's request.
|
|
|
31
31
|
- Extract requirements, constraints, and goals
|
|
32
32
|
- Identify what problem we're solving
|
|
33
33
|
|
|
34
|
-
2. **
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
**
|
|
40
|
-
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
-
|
|
48
|
-
-
|
|
49
|
-
-
|
|
50
|
-
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
34
|
+
2. **Generate candidates and dimensions:**
|
|
35
|
+
|
|
36
|
+
**Generate 2–4 named candidates** from three sources, then merge into one shortlist:
|
|
37
|
+
|
|
38
|
+
- **Ecosystem scan** — spawn `web-search-researcher` for any topic where the candidate space includes external libraries, frameworks, or services. Prompt it to return 2–4 named options with one-line "what it is" + canonical doc link per option. Skip only when the topic is wholly internal (e.g., "how to organize this service layer") and the orchestrator's design-space enumeration plus the user shortlist already cover the space.
|
|
39
|
+
- **Design-space enumeration** — orchestrator names abstract shapes from first principles when applicable (pub/sub vs direct-call vs event-bus; sync vs async; manual mapping vs auto-mapper). One-line "what it is" per shape.
|
|
40
|
+
- **User shortlist** — if the user pre-named candidates in the entry prompt ("compare TanStack Query vs SWR"), include those verbatim.
|
|
41
|
+
|
|
42
|
+
Merge to 2–4 candidates total. Name each with a short noun phrase ("TanStack Query", "Direct event bus"). Deduplicate.
|
|
43
|
+
|
|
44
|
+
**Default dimension list** (presented at Step 2.5; developer may drop irrelevant ones):
|
|
45
|
+
|
|
46
|
+
- **approach-shape** (hybrid) — what category of solution the candidate is, what core moving parts it requires.
|
|
47
|
+
- **precedent-fit** (codebase-anchored) — does the existing code already use this pattern; how many call sites would adopt the new option.
|
|
48
|
+
- **integration-risk** (codebase-anchored) — which existing seams the candidate would touch; what breaks if it lands.
|
|
49
|
+
- **migration-cost** (external-anchored for libraries; codebase-anchored for in-house code) — work to introduce the candidate plus work to remove the incumbent if there is one.
|
|
50
|
+
- **verification-cost** (codebase-anchored) — test/CI surface needed to make the candidate safe to adopt.
|
|
51
|
+
- **novelty** (external-anchored) — how recently the candidate emerged, ecosystem momentum, deprecation risk.
|
|
52
|
+
|
|
53
|
+
Hold the candidate set and default dimension list in working state for the Step 2.5 checkpoint. Do not dispatch fit agents yet.
|
|
54
|
+
|
|
55
|
+
## Step 2.5: Candidate Checkpoint
|
|
56
|
+
|
|
57
|
+
Present the candidate set and default dimensions to the developer before per-candidate fit dispatch.
|
|
58
|
+
|
|
59
|
+
1. **Show candidates and dimensions:**
|
|
60
|
+
|
|
61
|
+
```
|
|
62
|
+
## Candidates for: [Topic]
|
|
63
|
+
|
|
64
|
+
1. [Candidate A] — [one-line what it is]
|
|
65
|
+
2. [Candidate B] — [one-line what it is]
|
|
66
|
+
...
|
|
67
|
+
|
|
68
|
+
Dimensions (default 6; drop any that don't apply):
|
|
69
|
+
- approach-shape · precedent-fit · integration-risk
|
|
70
|
+
- migration-cost · verification-cost · novelty
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
2. **Confirm via the `ask_user_question` tool with the following question:** "[N] candidates, [D] dimensions. Begin per-candidate fit dispatch?". Header: "Candidates". Options: "Proceed (Recommended)" (Begin per-candidate fit dispatch with all [N] candidates and all [D] dimensions); "Adjust candidates or dimensions" (Rename, add, or drop candidates; drop dimensions that don't apply); "Re-generate candidates" (Candidates look wrong — re-run Step 2 with adjusted scope).
|
|
74
|
+
|
|
75
|
+
3. **Handle developer input:**
|
|
76
|
+
|
|
77
|
+
**"Proceed"**: lock the candidate × dimension set; advance to Step 3.
|
|
78
|
+
|
|
79
|
+
**"Adjust candidates or dimensions"**: ask the follow-up free-text question with prefix `❓ Question:` — "Which candidates and dimensions should be added, dropped, or renamed?" — apply edits to the working set, re-present, and confirm again with the same three-option `ask_user_question`.
|
|
80
|
+
|
|
81
|
+
**"Re-generate candidates"**: ask the follow-up free-text question with prefix `❓ Question:` — "What should be different in candidate generation? (narrower/wider scope, different ecosystem, exclude approach X, …)" — return to Step 2 with the updated scope, then re-enter Step 2.5.
|
|
82
|
+
|
|
83
|
+
Loop until "Proceed" is selected.
|
|
84
|
+
|
|
85
|
+
3. **Per-candidate fit dispatch (parallel agents):**
|
|
86
|
+
|
|
87
|
+
For each confirmed candidate, dispatch up to two agents in parallel — total ≤ 2 × N agents:
|
|
88
|
+
|
|
89
|
+
- **One `codebase-analyzer` per candidate** — when ≥1 kept dimension is codebase-anchored (precedent-fit, integration-risk, often migration-cost and verification-cost). The agent scores the candidate on every kept codebase-anchored dimension in a single pass, returning evidence per dimension with `file:line` references.
|
|
90
|
+
- **One `web-search-researcher` per candidate** — when ≥1 kept dimension is external-anchored (novelty, often migration-cost for libraries, approach-shape for ecosystem options). The agent scores the candidate on every kept external-anchored dimension in a single pass, returning evidence per dimension with doc/source links.
|
|
91
|
+
|
|
92
|
+
Skip either agent for a candidate when no dimension of that anchor-type was kept. Hybrid dimension `approach-shape` is scored by the orchestrator after both agents return, by combining their per-candidate findings.
|
|
93
|
+
|
|
94
|
+
**Per-candidate prompt shape** (use the same outer template, fill in candidate name and kept dimensions):
|
|
95
|
+
|
|
96
|
+
```
|
|
97
|
+
Candidate: [name] — [one-line what it is]
|
|
98
|
+
Topic: [topic from Step 1]
|
|
99
|
+
|
|
100
|
+
Score this single candidate on the following dimensions, each with concrete evidence ([file:line] for codebase, doc/source link for external). Report findings as one section per dimension.
|
|
101
|
+
|
|
102
|
+
Dimensions for this run:
|
|
103
|
+
- [dimension name] — [one-line of what to look for]
|
|
104
|
+
- ...
|
|
105
|
+
|
|
106
|
+
Do NOT compare against other candidates; another agent handles each one separately. Focus on depth of evidence for THIS candidate.
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
Wait for ALL agents to complete before proceeding.
|
|
110
|
+
|
|
111
|
+
**Coverage check**: every (candidate × kept-dimension) cell is filled — by an agent's evidence or by an explicit `null` ("does not apply to this candidate"). Cells silently dropped indicate a missing dispatch — re-run that candidate's agent.
|
|
112
|
+
|
|
113
|
+
4. **Synthesize and recommend:**
|
|
114
|
+
|
|
115
|
+
- Cross-reference per-candidate findings — fill the candidate × dimension grid with evidence per cell.
|
|
116
|
+
- Apply the fit filter qualitatively per candidate: a candidate "clears" when no kept dimension surfaces a blocking concern (integration-risk that breaks load-bearing seams, migration-cost that exceeds the topic's scope, verification-cost with no path to coverage).
|
|
117
|
+
- **If ≥1 candidate clears the fit filter**: pick the strongest, document rationale with evidence, and explain why alternatives weren't chosen. Identify conditions that would change the recommendation.
|
|
118
|
+
- **If every candidate fails the fit filter**: produce a "no-fit" recommendation — list each candidate's blocking dimension with evidence, recommend re-scoping the question or expanding the candidate pool, and set Step 6 frontmatter `confidence: low` and `status: blocked`.
|
|
63
119
|
|
|
64
120
|
5. **Determine metadata and filename:**
|
|
65
121
|
- Filename format: `thoughts/shared/solutions/YYYY-MM-DD_HH-MM-SS_[topic].md`
|
|
@@ -166,6 +222,10 @@ Then wait for the user's request.
|
|
|
166
222
|
|
|
167
223
|
## Recommendation
|
|
168
224
|
|
|
225
|
+
<!-- Render exactly ONE of the two blocks below, based on Step 4's fit-filter outcome. -->
|
|
226
|
+
|
|
227
|
+
**(A) When ≥1 candidate clears the fit filter:**
|
|
228
|
+
|
|
169
229
|
**Selected:** [Option N]
|
|
170
230
|
|
|
171
231
|
**Rationale:**
|
|
@@ -193,6 +253,21 @@ Then wait for the user's request.
|
|
|
193
253
|
**Risks:**
|
|
194
254
|
- [Risk]: [Mitigation]
|
|
195
255
|
|
|
256
|
+
**(B) When every candidate fails the fit filter:**
|
|
257
|
+
|
|
258
|
+
**No-fit:** every candidate surfaced a blocking concern on at least one kept dimension.
|
|
259
|
+
|
|
260
|
+
**Per-candidate blockers:**
|
|
261
|
+
- [Option 1]: [blocking dimension] — [evidence with file:line or doc link]
|
|
262
|
+
- [Option 2]: [blocking dimension] — [evidence]
|
|
263
|
+
- ...
|
|
264
|
+
|
|
265
|
+
**Recommended next step:**
|
|
266
|
+
- [Re-scope the question] — [how the topic should narrow/widen so candidates can clear]
|
|
267
|
+
- OR [Expand the candidate pool] — [what new candidate sources to enumerate; e.g., named ecosystem option not surfaced by Step 2]
|
|
268
|
+
|
|
269
|
+
**Frontmatter overrides:** set `confidence: low` and `status: blocked`.
|
|
270
|
+
|
|
196
271
|
## Scope Boundaries
|
|
197
272
|
- [What we're building]
|
|
198
273
|
- [What we're NOT doing]
|
|
@@ -243,19 +318,21 @@ Then wait for the user's request.
|
|
|
243
318
|
- Always use parallel Agent tool calls to maximize efficiency and minimize context usage
|
|
244
319
|
- Always spawn fresh research to validate current state - never rely on old research docs as source of truth
|
|
245
320
|
- Old research documents can provide historical context but must be validated against current code
|
|
246
|
-
-
|
|
321
|
+
- Generate 2-4 named candidates in Step 2; confirm them with the developer at Step 2.5 before per-candidate fit dispatch
|
|
322
|
+
- Web-search-researcher is a first-class Step 2 agent for ecosystem candidate-source — skip only when the topic is wholly internal and design-space enumeration plus user shortlist cover the space
|
|
323
|
+
- Per-candidate fit dispatch caps at two agents per candidate (one codebase-analyzer, one web-search-researcher) — skip either when no dimension of its anchor-type was kept
|
|
247
324
|
- Solutions documents should be self-contained with all necessary context
|
|
248
|
-
- Each agent prompt should be specific and focused on
|
|
249
|
-
- Quantify pattern precedent
|
|
325
|
+
- Each agent prompt should be specific and focused on a single candidate scored on the kept dimensions
|
|
326
|
+
- Quantify pattern precedent — count usage in codebase, don't just say "follows pattern"
|
|
250
327
|
- Ground complexity estimates in actual similar work from git history
|
|
251
|
-
- Think like a software architect -
|
|
328
|
+
- Think like a software architect — option-shopping output is 2–4 comparable candidates plus an honest fit verdict
|
|
252
329
|
- Keep the main agent focused on synthesis and comparison, not deep implementation details
|
|
253
330
|
- Encourage agents to find existing patterns and examples, not just describe possibilities
|
|
254
|
-
- Resolve technical unknowns during research
|
|
331
|
+
- Resolve technical unknowns during research — don't leave critical questions for design
|
|
255
332
|
- **File reading**: Always read mentioned files FULLY (no limit/offset) before invoking skills
|
|
256
333
|
- **Critical ordering**: Follow the numbered steps exactly
|
|
257
334
|
- ALWAYS read mentioned files first before invoking skills (step 1)
|
|
258
|
-
- ALWAYS
|
|
259
|
-
- ALWAYS wait for all agents to complete before synthesizing (step 3)
|
|
335
|
+
- ALWAYS generate candidates and run the Step 2.5 checkpoint before per-candidate dispatch (steps 2 → 2.5 → 3)
|
|
336
|
+
- ALWAYS wait for all per-candidate agents to complete before synthesizing (step 3)
|
|
260
337
|
- ALWAYS gather metadata before writing the document (step 5 before step 6)
|
|
261
338
|
- NEVER write the solutions document with placeholder values
|
package/scripts/types.js
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|