pi-soly 1.11.2 → 1.12.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/README.md +60 -14
- package/artifact/index.ts +241 -0
- package/artifact/render.ts +239 -0
- package/artifact/server.ts +384 -0
- package/artifact/session.ts +368 -0
- package/ask/README.md +20 -8
- package/ask/index.ts +100 -36
- package/ask/picker.ts +345 -29
- package/commands.ts +246 -30
- package/config.ts +124 -8
- package/core.ts +48 -236
- package/deck/deck.ts +386 -0
- package/deck/index.ts +113 -0
- package/index.ts +153 -37
- package/iteration.ts +1 -9
- package/mcp/commands.ts +12 -0
- package/mcp/direct-tools.ts +19 -2
- package/mcp/index.ts +144 -2
- package/mcp/init.ts +11 -0
- package/mcp/lifecycle.ts +9 -2
- package/mcp/server-manager.ts +30 -18
- package/mcp/state.ts +7 -0
- package/mcp/tool-cache.ts +135 -0
- package/nudge.ts +20 -3
- package/package.json +10 -3
- package/skills/soly-framework/SKILL.md +64 -37
- package/tool-hints.ts +62 -0
- package/tools.ts +28 -100
- package/util.ts +250 -0
- package/visual/chrome.ts +166 -0
- package/visual/colors.ts +24 -0
- package/visual/data.ts +62 -0
- package/visual/footer.ts +129 -0
- package/visual/format.ts +73 -0
- package/visual/glyphs.ts +35 -0
- package/visual/gradient.ts +122 -0
- package/visual/index.ts +18 -0
- package/visual/list-panel.ts +207 -0
- package/visual/segments.ts +136 -0
- package/visual/style.ts +34 -0
- package/visual/topbar.ts +55 -0
- package/visual/welcome.ts +265 -0
- package/visual/working.ts +66 -0
- package/workflows/execute.ts +17 -7
- package/workflows/index.ts +91 -44
- package/workflows/inspect.ts +23 -26
- package/workflows/migrate.ts +85 -0
- package/workflows/parser.ts +4 -2
- package/workflows/planning.ts +9 -8
- package/workflows/verify.ts +194 -0
- package/workflows-data/discuss-phase.md +10 -6
- package/agents-install.ts +0 -106
- package/ask/prompt.ts +0 -41
- package/ask/tests/prompt.test.ts +0 -54
package/workflows/inspect.ts
CHANGED
|
@@ -199,33 +199,30 @@ export function showDoctor(_cmd: unknown, state: SolyState, ui: InspectUI, confi
|
|
|
199
199
|
});
|
|
200
200
|
}
|
|
201
201
|
|
|
202
|
-
// 11.
|
|
203
|
-
// Reports current state — not a failure if disabled.
|
|
202
|
+
// 11. subagent tool (execute/plan delegate to it; without it they run inline)
|
|
204
203
|
if (state.exists) {
|
|
205
|
-
const
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
});
|
|
228
|
-
}
|
|
204
|
+
const hasSubagent = activeTools.includes("subagent");
|
|
205
|
+
checks.push({
|
|
206
|
+
name: "subagent tool (delegated execution)",
|
|
207
|
+
status: hasSubagent ? "pass" : "info",
|
|
208
|
+
detail: hasSubagent
|
|
209
|
+
? "subagent tool loaded — soly execute/plan delegate to a worker"
|
|
210
|
+
: "not installed — soly execute/plan run inline in this session (install pi-subagents for delegated/parallel execution)",
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// 12. project layout (suggest migration to the unified phase=tasks model)
|
|
215
|
+
if (state.exists) {
|
|
216
|
+
const legacy =
|
|
217
|
+
state.phases.some((p) => p.plans.length > 0 && (!p.tasks || p.tasks.length === 0)) ||
|
|
218
|
+
state.features.length > 0;
|
|
219
|
+
checks.push({
|
|
220
|
+
name: "project layout",
|
|
221
|
+
status: legacy ? "info" : "pass",
|
|
222
|
+
detail: legacy
|
|
223
|
+
? "legacy plans/features detected — run `soly migrate` to move to phases/<N>/tasks/"
|
|
224
|
+
: "unified model (phase = group of tasks)",
|
|
225
|
+
});
|
|
229
226
|
}
|
|
230
227
|
|
|
231
228
|
// Render
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
// =============================================================================
|
|
2
|
+
// workflows/migrate.ts — `soly migrate`: legacy layout → unified (LLM-runnable)
|
|
3
|
+
// =============================================================================
|
|
4
|
+
//
|
|
5
|
+
// The unified model is `phases/<N>/tasks/<id>/PLAN.md`. Two legacy shapes need
|
|
6
|
+
// converting:
|
|
7
|
+
// - standalone plan files `phases/<N>/<NN-MM>-PLAN.md` (+ *-SUMMARY.md)
|
|
8
|
+
// - the separate features model `features/<f>/tasks/<id>/`
|
|
9
|
+
//
|
|
10
|
+
// Conversion is semantic (derive a task id/kind/deps from a plan), so we hand
|
|
11
|
+
// the model a careful, verify-before-delete protocol rather than doing blind
|
|
12
|
+
// file moves in the extension. Returns a transform the LLM executes inline.
|
|
13
|
+
// =============================================================================
|
|
14
|
+
|
|
15
|
+
import type { SolyState } from "../core.ts";
|
|
16
|
+
|
|
17
|
+
export type MigrateResult = { handled: boolean; transformedText?: string };
|
|
18
|
+
|
|
19
|
+
/** Phases that still use legacy standalone plan files and have no tasks/ yet. */
|
|
20
|
+
function legacyPhases(state: SolyState): string[] {
|
|
21
|
+
return state.phases
|
|
22
|
+
.filter((p) => p.plans.length > 0 && (!p.tasks || p.tasks.length === 0))
|
|
23
|
+
.map((p) => `${p.slug} (${p.plans.length} plan${p.plans.length === 1 ? "" : "s"})`);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function buildMigrateTransform(state: SolyState): MigrateResult {
|
|
27
|
+
if (!state.exists) {
|
|
28
|
+
return {
|
|
29
|
+
handled: true,
|
|
30
|
+
transformedText:
|
|
31
|
+
`soly migrate: no .soly/ project in this directory — nothing to migrate.`,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const phases = legacyPhases(state);
|
|
36
|
+
const features = state.features.map((f) => `${f.slug} (${f.taskCount} task${f.taskCount === 1 ? "" : "s"})`);
|
|
37
|
+
|
|
38
|
+
if (phases.length === 0 && features.length === 0) {
|
|
39
|
+
return {
|
|
40
|
+
handled: true,
|
|
41
|
+
transformedText:
|
|
42
|
+
`soly migrate: already on the unified model (phase = group of tasks). Nothing to migrate.`,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const detected: string[] = [];
|
|
47
|
+
if (phases.length > 0) detected.push(`- Legacy plan files in: ${phases.join(", ")}`);
|
|
48
|
+
if (features.length > 0) detected.push(`- Legacy \`features/\` dirs: ${features.join(", ")}`);
|
|
49
|
+
|
|
50
|
+
return {
|
|
51
|
+
handled: true,
|
|
52
|
+
transformedText: `soly migrate — convert the legacy layout to the unified model (phase = group of tasks).
|
|
53
|
+
|
|
54
|
+
Target layout:
|
|
55
|
+
.soly/phases/<N>/tasks/<task-id>/PLAN.md (frontmatter: id, kind, status, depends-on, feature?)
|
|
56
|
+
.soly/phases/<N>/tasks/<task-id>/SUMMARY.md
|
|
57
|
+
|
|
58
|
+
Detected legacy artifacts:
|
|
59
|
+
${detected.join("\n")}
|
|
60
|
+
|
|
61
|
+
Do the conversion YOURSELF, carefully, one item at a time. NEVER delete an old file until the new one is written and you've confirmed its contents. Use \`soly_read\` / \`soly_snippet\` to read, your file tools to write/move.
|
|
62
|
+
|
|
63
|
+
**A. For each legacy plan \`phases/<N>/<NN-MM>-PLAN.md\`:**
|
|
64
|
+
1. Read it. Derive a stable task id \`<short-slug>-<4hex>\` from the plan's title/intent (lowercase, hyphenated, e.g. \`auth-login-a3f9\`).
|
|
65
|
+
2. Write \`phases/<N>/tasks/<id>/PLAN.md\` with frontmatter, then the original body unchanged:
|
|
66
|
+
\`\`\`
|
|
67
|
+
---
|
|
68
|
+
id: <id>
|
|
69
|
+
kind: <be|fe|infra|docs|test> # infer from the plan
|
|
70
|
+
status: <ready|done> # done if a matching *-SUMMARY.md exists
|
|
71
|
+
depends-on: [] # fill if the plan depends on earlier plans/tasks
|
|
72
|
+
---
|
|
73
|
+
<original plan body>
|
|
74
|
+
\`\`\`
|
|
75
|
+
3. If a matching \`<NN-MM>-SUMMARY.md\` exists, move it to \`phases/<N>/tasks/<id>/SUMMARY.md\`.
|
|
76
|
+
4. Verify the new files exist and read correctly, THEN delete the old \`<NN-MM>-PLAN.md\` and its \`*-SUMMARY.md\`.
|
|
77
|
+
|
|
78
|
+
**B. For each \`features/<f>/tasks/<id>/\`:**
|
|
79
|
+
1. Decide which phase it belongs to (ask the user with \`ask_pro\` if it's not obvious from the task/feature).
|
|
80
|
+
2. Move the whole task dir to \`phases/<N>/tasks/<id>/\` and add \`feature: <f>\` to its PLAN.md frontmatter.
|
|
81
|
+
3. When a feature's tasks are all moved, remove the now-empty \`features/<f>/\`.
|
|
82
|
+
|
|
83
|
+
**Close out:** update any path references in STATE.md / ROADMAP.md, run \`soly status\` and \`soly doctor\` to confirm the new layout loads, then commit. Report a short summary of what moved (counts + any tasks you couldn't place).`,
|
|
84
|
+
};
|
|
85
|
+
}
|
package/workflows/parser.ts
CHANGED
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
/** Verbs currently supported by the workflow handlers. */
|
|
19
19
|
export type WorkflowVerb =
|
|
20
20
|
| "execute" | "pause" | "compact" | "resume" | "status" | "log" | "diff"
|
|
21
|
-
| "plan" | "discuss" | "help" | "doctor" | "iterations" | "phase" | "todos";
|
|
21
|
+
| "plan" | "discuss" | "help" | "doctor" | "iterations" | "phase" | "todos" | "verify" | "migrate";
|
|
22
22
|
|
|
23
23
|
export interface SolyCommand {
|
|
24
24
|
verb: WorkflowVerb;
|
|
@@ -66,7 +66,9 @@ export function parseSolyCommand(text: string): SolyCommand | null {
|
|
|
66
66
|
verb !== "doctor" &&
|
|
67
67
|
verb !== "iterations" &&
|
|
68
68
|
verb !== "phase" &&
|
|
69
|
-
verb !== "todos"
|
|
69
|
+
verb !== "todos" &&
|
|
70
|
+
verb !== "verify" &&
|
|
71
|
+
verb !== "migrate"
|
|
70
72
|
) {
|
|
71
73
|
return null;
|
|
72
74
|
}
|
package/workflows/planning.ts
CHANGED
|
@@ -129,13 +129,13 @@ These documents hold the project's INTENT — business context, design vision, w
|
|
|
129
129
|
Phase directory: ${phase.dir}
|
|
130
130
|
Current state: planCount=${phase.planCount}, context=${phase.contextExists}, research=${phase.researchExists}
|
|
131
131
|
|
|
132
|
-
Launch a subagent to produce the
|
|
132
|
+
Launch a subagent to produce the phase's tasks. Do NOT plan inline.
|
|
133
133
|
|
|
134
134
|
subagent({
|
|
135
135
|
agent: "worker",
|
|
136
136
|
context: "fresh",
|
|
137
137
|
async: true,
|
|
138
|
-
task: \`You are a planner.
|
|
138
|
+
task: \`You are a planner. Break phase ${target.phase} into discrete tasks (unified model): write one PLAN.md per task at phases/<NN>-slug/tasks/<task-id>/PLAN.md with frontmatter, plus ${phase.contextExists ? "" : "CONTEXT.md / "}RESEARCH.md if missing.
|
|
139
139
|
|
|
140
140
|
**FIRST ACTION — read the iteration context file:**
|
|
141
141
|
\`\`\`
|
|
@@ -155,15 +155,16 @@ ${workflow}
|
|
|
155
155
|
|
|
156
156
|
Hard rules:
|
|
157
157
|
- Do not write production code. Planning only.
|
|
158
|
-
-
|
|
159
|
-
- Each
|
|
160
|
-
-
|
|
158
|
+
- One task = one PLAN.md under .soly/phases/<NN>-<slug>/tasks/<task-id>/ (task id = <short-slug>-<4hex>).
|
|
159
|
+
- Each task PLAN.md starts with frontmatter: id, kind, status: ready, depends-on: [task ids], optional feature. The cross-task dependency graph must be acyclic.
|
|
160
|
+
- Each task PLAN needs requirements, must_haves.truths, must_haves.artifacts, must_haves.key_links.
|
|
161
|
+
- PATH DISCIPLINE: all task PLAN.md / phase CONTEXT.md / RESEARCH.md go under \`.soly/phases/<NN>-<slug>/\`. Never write to the project root.
|
|
161
162
|
- Update .soly/STATE.md Current Position at the end.
|
|
162
|
-
- Return: created
|
|
163
|
+
- Return: created task ids, the dependency order (which tasks are ready first), open questions.
|
|
163
164
|
\`
|
|
164
165
|
})
|
|
165
166
|
|
|
166
|
-
When the subagent returns, summarize the
|
|
167
|
+
When the subagent returns, summarize the tasks + their dependency order, then suggest \`soly execute ${target.phase}\` (or ask the user to confirm before execution).`;
|
|
167
168
|
return { handled: true, transformedText: instruction };
|
|
168
169
|
}
|
|
169
170
|
|
|
@@ -607,7 +608,7 @@ soly_ask_user({
|
|
|
607
608
|
- ${hasAskPro ? "`ask_pro` — multi-question tabbed picker (PREFERRED)" : "`soly_ask_user` — single-question picker (fallback)"}
|
|
608
609
|
- \`soly_save_discuss_checkpoint\` — save partial progress (use after each answer)
|
|
609
610
|
- \`soly_finish_discuss\` — finalize: writes CONTEXT.md, deletes checkpoint
|
|
610
|
-
- \`soly_read\`, \`soly_snippet\`, \`soly_doc_search
|
|
611
|
+
- \`soly_read\`, \`soly_snippet\`, \`soly_doc_search\` — read .soly/ artifacts as needed (intent docs are already in your system prompt)
|
|
611
612
|
- \`soly_log_decision\` — log to STATE.md Decisions table (use sparingly)
|
|
612
613
|
- Standard pi tools: \`read\`, \`bash\`, \`grep\`, \`find\` for codebase context
|
|
613
614
|
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
// =============================================================================
|
|
2
|
+
// workflows/verify.ts — self-review loop ("fresh eyes") for `soly verify`
|
|
3
|
+
// =============================================================================
|
|
4
|
+
//
|
|
5
|
+
// Repeatedly asks the model to review its own work until it reports nothing
|
|
6
|
+
// left to fix, or a max-iteration cap is hit. Driven by the `agent_end` event:
|
|
7
|
+
// after each turn we read the assistant's last message, decide exit vs. loop,
|
|
8
|
+
// and (if looping) re-inject the review prompt via sendUserMessage.
|
|
9
|
+
//
|
|
10
|
+
// Optional fresh-context mode strips prior review iterations from the model's
|
|
11
|
+
// view through the shared context-manager — the agent re-reviews with genuinely
|
|
12
|
+
// fresh eyes instead of through the lens of its earlier passes. We only ever
|
|
13
|
+
// drop real messages (slice), never fabricate; the "re-read the plan" nudge
|
|
14
|
+
// lives in the review prompt text, so no synthetic messages are needed.
|
|
15
|
+
//
|
|
16
|
+
// Pure helpers (exit detection, message slicing) are exported for unit tests.
|
|
17
|
+
// =============================================================================
|
|
18
|
+
|
|
19
|
+
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
20
|
+
import type { AgentMessageLike, ContextManager, ContextRewriter } from "../context-manager.ts";
|
|
21
|
+
|
|
22
|
+
/** Resolved verify settings (from soly config `verify`). */
|
|
23
|
+
export type VerifyConfig = {
|
|
24
|
+
maxIterations: number;
|
|
25
|
+
freshContext: boolean;
|
|
26
|
+
prompt: string;
|
|
27
|
+
exitPatterns: string[];
|
|
28
|
+
issuesFixedPatterns: string[];
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export type VerifyState = { active: boolean; iteration: number; max: number; fresh: boolean };
|
|
32
|
+
|
|
33
|
+
export type VerifyDeps = {
|
|
34
|
+
contextManager: ContextManager;
|
|
35
|
+
getConfig: () => VerifyConfig;
|
|
36
|
+
/** Notified whenever the loop state changes (drives the chrome top bar). */
|
|
37
|
+
onState: (state: VerifyState) => void;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export type VerifyStartOpts = { max?: number; fresh?: boolean };
|
|
41
|
+
|
|
42
|
+
export type VerifyLoop = {
|
|
43
|
+
isActive: () => boolean;
|
|
44
|
+
start: (ctx: ExtensionContext, opts?: VerifyStartOpts) => void;
|
|
45
|
+
stop: (ctx: ExtensionContext | undefined, reason: string) => void;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
// ---------------------------------------------------------------------------
|
|
49
|
+
// Pure helpers
|
|
50
|
+
// ---------------------------------------------------------------------------
|
|
51
|
+
|
|
52
|
+
/** Extract plain text from a message's content (string or text-block array). */
|
|
53
|
+
export function extractText(content: unknown): string {
|
|
54
|
+
if (typeof content === "string") return content;
|
|
55
|
+
if (!Array.isArray(content)) return "";
|
|
56
|
+
const parts: string[] = [];
|
|
57
|
+
for (const block of content) {
|
|
58
|
+
if (block && typeof block === "object" && (block as { type?: string }).type === "text") {
|
|
59
|
+
const t = (block as { text?: unknown }).text;
|
|
60
|
+
if (typeof t === "string") parts.push(t);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return parts.join("\n");
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Compile pattern strings into case-insensitive RegExps, skipping invalid ones. */
|
|
67
|
+
export function compilePatterns(patterns: string[]): RegExp[] {
|
|
68
|
+
const out: RegExp[] = [];
|
|
69
|
+
for (const p of patterns) {
|
|
70
|
+
try {
|
|
71
|
+
out.push(new RegExp(p, "i"));
|
|
72
|
+
} catch {
|
|
73
|
+
/* skip malformed pattern */
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return out;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Exit only when the text signals "done" AND does NOT also signal that issues
|
|
81
|
+
* were fixed — so "Fixed 3 issues. No issues found." keeps looping (the fix
|
|
82
|
+
* may have introduced new problems), but a clean "No issues found." exits.
|
|
83
|
+
*/
|
|
84
|
+
export function shouldExit(text: string, exit: RegExp[], fixed: RegExp[]): boolean {
|
|
85
|
+
const matched = (res: RegExp[]) => res.some((r) => r.test(text));
|
|
86
|
+
return matched(exit) && !matched(fixed);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** Index of the last user message, or -1. */
|
|
90
|
+
export function lastUserIndex(messages: AgentMessageLike[]): number {
|
|
91
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
92
|
+
if (messages[i]?.role === "user") return i;
|
|
93
|
+
}
|
|
94
|
+
return -1;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Fresh-eyes view: keep the conversation before the loop began (`boundary` =
|
|
99
|
+
* index of the first review prompt) plus the current review prompt onward,
|
|
100
|
+
* dropping the in-between prior iterations. Returns the input unchanged on the
|
|
101
|
+
* first pass (boundary === last user message).
|
|
102
|
+
*/
|
|
103
|
+
export function freshContextMessages<T extends AgentMessageLike>(messages: T[], boundary: number): T[] {
|
|
104
|
+
if (boundary < 0 || boundary >= messages.length) return messages;
|
|
105
|
+
const current = lastUserIndex(messages);
|
|
106
|
+
if (current <= boundary) return messages;
|
|
107
|
+
return [...messages.slice(0, boundary), ...messages.slice(current)];
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// ---------------------------------------------------------------------------
|
|
111
|
+
// Loop
|
|
112
|
+
// ---------------------------------------------------------------------------
|
|
113
|
+
|
|
114
|
+
/** Create the verify loop and register its `agent_end` + `input` handlers. */
|
|
115
|
+
export function createVerifyLoop(pi: ExtensionAPI, deps: VerifyDeps): VerifyLoop {
|
|
116
|
+
let active = false;
|
|
117
|
+
let iteration = 0;
|
|
118
|
+
let max = 0;
|
|
119
|
+
let fresh = false;
|
|
120
|
+
let boundary = -1;
|
|
121
|
+
let exitRes: RegExp[] = [];
|
|
122
|
+
let fixedRes: RegExp[] = [];
|
|
123
|
+
|
|
124
|
+
const emit = () => deps.onState({ active, iteration, max, fresh });
|
|
125
|
+
|
|
126
|
+
// Captures the boundary on the first call, then strips prior iterations.
|
|
127
|
+
const rewriter: ContextRewriter = (messages) => {
|
|
128
|
+
if (boundary < 0) {
|
|
129
|
+
boundary = lastUserIndex(messages);
|
|
130
|
+
return messages;
|
|
131
|
+
}
|
|
132
|
+
return freshContextMessages(messages, boundary);
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
const stop = (ctx: ExtensionContext | undefined, reason: string): void => {
|
|
136
|
+
if (!active) return;
|
|
137
|
+
active = false;
|
|
138
|
+
boundary = -1;
|
|
139
|
+
deps.contextManager.setRewriter(null);
|
|
140
|
+
emit();
|
|
141
|
+
ctx?.ui.notify(`soly verify: ${reason}`, "info");
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
pi.on("agent_end", async (event, ctx) => {
|
|
145
|
+
if (!active) return;
|
|
146
|
+
const last = [...event.messages].reverse().find((m) => m.role === "assistant");
|
|
147
|
+
const text = extractText((last as { content?: unknown } | undefined)?.content);
|
|
148
|
+
iteration++;
|
|
149
|
+
if (!text.trim()) {
|
|
150
|
+
stop(ctx, iteration === 1 ? "nothing to review" : "done");
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
if (shouldExit(text, exitRes, fixedRes)) {
|
|
154
|
+
stop(ctx, "no issues found");
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
if (iteration >= max) {
|
|
158
|
+
stop(ctx, `stopped after ${max} iterations`);
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
emit();
|
|
162
|
+
pi.sendUserMessage(deps.getConfig().prompt, { deliverAs: "followUp" });
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
// Any interactive input (other than re-invoking verify) breaks the loop.
|
|
166
|
+
pi.on("input", async (event, ctx) => {
|
|
167
|
+
if (!active || event.source !== "interactive") return;
|
|
168
|
+
if (event.text.trim().toLowerCase().startsWith("soly verify")) return;
|
|
169
|
+
stop(ctx, "interrupted");
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
return {
|
|
173
|
+
isActive: () => active,
|
|
174
|
+
start(ctx, opts = {}): void {
|
|
175
|
+
if (active) {
|
|
176
|
+
ctx.ui.notify("soly verify: already running", "info");
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
const cfg = deps.getConfig();
|
|
180
|
+
active = true;
|
|
181
|
+
iteration = 0;
|
|
182
|
+
boundary = -1;
|
|
183
|
+
max = Math.max(1, opts.max ?? cfg.maxIterations);
|
|
184
|
+
fresh = opts.fresh ?? cfg.freshContext;
|
|
185
|
+
exitRes = compilePatterns(cfg.exitPatterns);
|
|
186
|
+
fixedRes = compilePatterns(cfg.issuesFixedPatterns);
|
|
187
|
+
if (fresh) deps.contextManager.setRewriter(rewriter);
|
|
188
|
+
emit();
|
|
189
|
+
ctx.ui.notify(`soly verify: review mode on (max ${max}${fresh ? ", fresh context" : ""})`, "info");
|
|
190
|
+
pi.sendUserMessage(cfg.prompt);
|
|
191
|
+
},
|
|
192
|
+
stop,
|
|
193
|
+
};
|
|
194
|
+
}
|
|
@@ -2,8 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
<purpose>Reference document for the **interactive** `soly discuss <N>` flow. The
|
|
4
4
|
discussion is now driven by the interactive LLM (you) directly — no subagent.
|
|
5
|
-
You
|
|
6
|
-
|
|
5
|
+
You ask questions via a real UI picker — **`ask_pro` (preferred, one batched
|
|
6
|
+
call)**, falling back to `soly_ask_user` (deprecated, one question at a time)
|
|
7
|
+
when `ask_pro` isn't available — and `soly_finish_discuss` to write the
|
|
8
|
+
canonical CONTEXT.md. For design/architecture forks you may reach for
|
|
9
|
+
`decision_deck`, and for a rich visual comparison `html_artifact`. This
|
|
7
10
|
markdown is background context, not a strict protocol.</purpose>
|
|
8
11
|
|
|
9
12
|
<path_discipline>
|
|
@@ -20,8 +23,9 @@ The iteration context file (path given by the parent in the task prompt) is your
|
|
|
20
23
|
2. **Generate 3-5 phase-specific gray areas** grounded in the intent + ROADMAP row. For each, prepare a question with 2-3 CONCRETE options, ⭐ first = recommended answer, with 1-sentence rationale.
|
|
21
24
|
|
|
22
25
|
3. **Pick a picker** (the parent's prompt tells you which one is preferred):
|
|
23
|
-
- **`ask_pro`**
|
|
24
|
-
- **`soly_ask_user`** (
|
|
26
|
+
- **`ask_pro`** — multi-question tabbed picker. **PREFERRED**: call ONCE with all questions, returns all answers in one shot. Per question: `multiSelect` (+ `minSelect`/`maxSelect`), `allowOther` (free-text choice), `freeText` (typed answer, no options), per-option `preview` (side panel, code highlighted); the user can press `s` to skip.
|
|
27
|
+
- **`soly_ask_user`** (deprecated — fallback only when `ask_pro` is unavailable) — single-question picker. Call N times, one per question. No `allowOther` support.
|
|
28
|
+
- **`decision_deck`** (optional) — for an architecture/design fork where the choice hinges on the concrete code shape, present the options as full-screen cards (code snippet + pros/cons) instead of a flat list.
|
|
25
29
|
|
|
26
30
|
4. **Save a checkpoint after each answer** with `soly_save_discuss_checkpoint` so the user can quit and resume. The final `soly_finish_discuss` will delete the checkpoint and write CONTEXT.md.
|
|
27
31
|
|
|
@@ -165,9 +169,9 @@ Build a small `<codebase_context>` block (≤ 10 lines) of reusable assets/patte
|
|
|
165
169
|
|
|
166
170
|
**10. Discuss (interactive, one question at a time).**
|
|
167
171
|
|
|
168
|
-
**PREFERRED
|
|
172
|
+
**PREFERRED**: call `ask_pro` ONCE with all questions as tabs. Returns all answers in one shot. Per question: `header`, `question`, `options[2-4]`, optional `multiSelect` (+ `minSelect`/`maxSelect`), `allowOther` (text input for custom answer), or `freeText: true` for an open typed answer (no options). For a design/architecture fork where the choice turns on the concrete code shape, `decision_deck` (cards with code + pros/cons) reads better than a flat option list.
|
|
169
173
|
|
|
170
|
-
**FALLBACK** (if `ask_pro` is
|
|
174
|
+
**FALLBACK** (only if `ask_pro` is unavailable — it is deprecated): call `soly_ask_user` once per question, one at a time. Pattern:
|
|
171
175
|
|
|
172
176
|
```
|
|
173
177
|
soly_ask_user({
|
package/agents-install.ts
DELETED
|
@@ -1,106 +0,0 @@
|
|
|
1
|
-
// =============================================================================
|
|
2
|
-
// assets-install.ts — Idempotent install of soly-managed user assets
|
|
3
|
-
// =============================================================================
|
|
4
|
-
//
|
|
5
|
-
// Soly ships one kind of user-scope asset:
|
|
6
|
-
//
|
|
7
|
-
// Skills → `~/.pi/agent/skills/<name>/`
|
|
8
|
-
// The `soly-framework` skill — framework documentation the LLM
|
|
9
|
-
// loads on demand via the read tool. This is the LLM's only
|
|
10
|
-
// "helper" for soly — pi doesn't need a separate subagent layer
|
|
11
|
-
// for plan execution (the LLM in the main session does it).
|
|
12
|
-
//
|
|
13
|
-
// pi discovers the skill from `~/.pi/agent/`, so on first session_start
|
|
14
|
-
// we copy our shipped file there.
|
|
15
|
-
//
|
|
16
|
-
// IDEMPOTENT: if the target file already exists (user may have customized
|
|
17
|
-
// it), we do NOT overwrite. This is one-way "first install wins".
|
|
18
|
-
// =============================================================================
|
|
19
|
-
|
|
20
|
-
import * as fs from "node:fs";
|
|
21
|
-
import * as os from "node:os";
|
|
22
|
-
import * as path from "node:path";
|
|
23
|
-
|
|
24
|
-
/** soly skills bundled with the extension. Each entry is a directory
|
|
25
|
-
* under `skills/` containing a SKILL.md. */
|
|
26
|
-
const SHIPPED_SKILLS = [
|
|
27
|
-
"soly-framework",
|
|
28
|
-
] as const;
|
|
29
|
-
|
|
30
|
-
/** Where pi looks for user skills. Respects HOME/USERPROFILE for
|
|
31
|
-
* testability (otherwise we'd always write to the real user home). */
|
|
32
|
-
function userSkillsDir(): string {
|
|
33
|
-
const home = process.env.HOME || process.env.USERPROFILE || os.homedir();
|
|
34
|
-
return path.join(home, ".pi", "agent", "skills");
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
/** Where this soly extension's `skills/` directory lives. */
|
|
38
|
-
function shippedSkillsDir(extensionRoot: string): string {
|
|
39
|
-
return path.join(extensionRoot, "skills");
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
export interface InstallResult {
|
|
43
|
-
installed: string[];
|
|
44
|
-
skipped: string[];
|
|
45
|
-
errors: string[];
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
/** Copy a directory tree if destination doesn't exist. Idempotent. */
|
|
49
|
-
function copyDirIfMissing(from: string, to: string): "installed" | "skipped" | "error" {
|
|
50
|
-
if (!fs.existsSync(from)) return "error";
|
|
51
|
-
if (fs.existsSync(to)) return "skipped";
|
|
52
|
-
try {
|
|
53
|
-
fs.mkdirSync(path.dirname(to), { recursive: true });
|
|
54
|
-
fs.cpSync(from, to, { recursive: true });
|
|
55
|
-
return "installed";
|
|
56
|
-
} catch {
|
|
57
|
-
return "error";
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
/** Install shipped soly skills to `~/.pi/agent/skills/`. Idempotent. */
|
|
62
|
-
export function installSolySkills(extensionRoot: string): InstallResult {
|
|
63
|
-
const result: InstallResult = { installed: [], skipped: [], errors: [] };
|
|
64
|
-
const src = shippedSkillsDir(extensionRoot);
|
|
65
|
-
const dst = userSkillsDir();
|
|
66
|
-
|
|
67
|
-
if (!fs.existsSync(src)) return result; // dev mode no-op
|
|
68
|
-
|
|
69
|
-
for (const name of SHIPPED_SKILLS) {
|
|
70
|
-
const from = path.join(src, name);
|
|
71
|
-
const to = path.join(dst, name);
|
|
72
|
-
const r = copyDirIfMissing(from, to);
|
|
73
|
-
if (r === "installed") result.installed.push(name);
|
|
74
|
-
else if (r === "skipped") result.skipped.push(name);
|
|
75
|
-
else result.errors.push(`missing source: ${from}`);
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
return result;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
/** Install all soly assets (skills only — agents are not shipped). */
|
|
82
|
-
export function installSolyAssets(extensionRoot: string): {
|
|
83
|
-
skills: InstallResult;
|
|
84
|
-
} {
|
|
85
|
-
return {
|
|
86
|
-
skills: installSolySkills(extensionRoot),
|
|
87
|
-
};
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
/** Check which shipped soly skills are present in the user dir. */
|
|
91
|
-
export function checkSolySkillsInstalled(extensionRoot: string): {
|
|
92
|
-
installed: string[];
|
|
93
|
-
missing: string[];
|
|
94
|
-
} {
|
|
95
|
-
const dst = userSkillsDir();
|
|
96
|
-
const installed: string[] = [];
|
|
97
|
-
const missing: string[] = [];
|
|
98
|
-
for (const name of SHIPPED_SKILLS) {
|
|
99
|
-
if (fs.existsSync(path.join(dst, name, "SKILL.md"))) {
|
|
100
|
-
installed.push(name);
|
|
101
|
-
} else {
|
|
102
|
-
missing.push(name);
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
return { installed, missing };
|
|
106
|
-
}
|
package/ask/prompt.ts
DELETED
|
@@ -1,41 +0,0 @@
|
|
|
1
|
-
// =============================================================================
|
|
2
|
-
// prompt.ts — System-prompt section for the pi-ask extension
|
|
3
|
-
// =============================================================================
|
|
4
|
-
//
|
|
5
|
-
// Injected into the agent's system prompt via `before_agent_start` so the
|
|
6
|
-
// LLM knows `ask_pro` is available, when to use it, and when NOT to
|
|
7
|
-
// (anti-patterns matter more than use-cases here — the tool is easy to
|
|
8
|
-
// overuse for trivial questions).
|
|
9
|
-
//
|
|
10
|
-
// Kept short (~600 chars) so it doesn't bloat every turn's prompt. The
|
|
11
|
-
// tool's own `description` and parameter schema still carry the detailed
|
|
12
|
-
// contract; this section is the "when to reach for it" trigger.
|
|
13
|
-
// =============================================================================
|
|
14
|
-
|
|
15
|
-
/** Build the "when to use ask_pro" section. Pure function, easily testable. */
|
|
16
|
-
export function buildAskProSection(): string {
|
|
17
|
-
return `
|
|
18
|
-
|
|
19
|
-
## pi-ask — when to use \`ask_pro\`
|
|
20
|
-
|
|
21
|
-
\`ask_pro\` is a multi-question picker (tabbed, numbered, ⭐ recommended). Use it when:
|
|
22
|
-
- You need a focused choice between 2–4 options and a free-text question would be slower
|
|
23
|
-
- You have 2–6 related questions to ask in one batch (e.g. \`soly discuss\` scoping flow)
|
|
24
|
-
- The user must pick a single concrete answer to move forward
|
|
25
|
-
|
|
26
|
-
DON'T use it for:
|
|
27
|
-
- Simple yes/no — just ask in text
|
|
28
|
-
- Open-ended questions ("what do you want?") — free text is better
|
|
29
|
-
- More than 6 questions — tab-switching fatigue
|
|
30
|
-
- When the user already gave a clear answer — don't second-guess
|
|
31
|
-
- Trivial clarifications — use plain text first, escalate to \`ask_pro\` only if the answer matters
|
|
32
|
-
|
|
33
|
-
Keyboard in the picker: \`↑↓\` navigate, \`1-N\` instant-pick, \`Tab\` next question, \`Space\` toggle (multi-select only), \`Enter\` confirm/advance/submit, \`n\` add a free-text note to the current question, \`Esc\` cancel.
|
|
34
|
-
|
|
35
|
-
Schema reminder: \`questions: [{ header, question, options: [{label, description?, recommended?, preview?}], multiSelect? }]\`. Mark exactly one option \`recommended: true\` per question when you have a default.
|
|
36
|
-
|
|
37
|
-
**Option previews:** \`option.preview\` (markdown/plain string) shows in a side panel next to the option list while that option is focused. Use it when the question is about a code structure, API shape, or concrete example — show a small snippet of what each option entails so the user can decide without asking follow-ups. Example: when asking "how should we model auth?", each option's preview can show the relevant type signature.
|
|
38
|
-
|
|
39
|
-
**Notes:** the user can press \`n\` after picking an answer to attach a free-text note (edge cases, constraints, reasoning). The note is returned to you as \`// note: \"...\"\` next to the chosen answer. Treat it as a hard constraint.
|
|
40
|
-
`;
|
|
41
|
-
}
|
package/ask/tests/prompt.test.ts
DELETED
|
@@ -1,54 +0,0 @@
|
|
|
1
|
-
// =============================================================================
|
|
2
|
-
// tests/prompt.test.ts — Tests for the system-prompt section
|
|
3
|
-
// =============================================================================
|
|
4
|
-
|
|
5
|
-
/// <reference types="bun-types" />
|
|
6
|
-
import { describe, test, expect } from "bun:test";
|
|
7
|
-
import { buildAskProSection } from "../prompt.js";
|
|
8
|
-
|
|
9
|
-
describe("buildAskProSection", () => {
|
|
10
|
-
const s = buildAskProSection();
|
|
11
|
-
|
|
12
|
-
test("starts with a header", () => {
|
|
13
|
-
expect(s.trim().startsWith("## pi-ask")).toBe(true);
|
|
14
|
-
});
|
|
15
|
-
|
|
16
|
-
test("explains when to use ask_pro", () => {
|
|
17
|
-
expect(s).toContain("ask_pro");
|
|
18
|
-
expect(s).toContain("when to use");
|
|
19
|
-
// Use-case coverage
|
|
20
|
-
expect(s).toMatch(/focused choice/i);
|
|
21
|
-
expect(s).toMatch(/2.6 related questions/i);
|
|
22
|
-
});
|
|
23
|
-
|
|
24
|
-
test("explains when NOT to use it (anti-patterns)", () => {
|
|
25
|
-
expect(s).toContain("DON");
|
|
26
|
-
expect(s).toMatch(/yes\/no/i);
|
|
27
|
-
expect(s).toMatch(/open-ended/i);
|
|
28
|
-
expect(s).toMatch(/more than 6 questions/i);
|
|
29
|
-
});
|
|
30
|
-
|
|
31
|
-
test("documents keyboard shortcuts", () => {
|
|
32
|
-
expect(s).toContain("Space");
|
|
33
|
-
expect(s).toContain("Enter");
|
|
34
|
-
expect(s).toContain("Esc");
|
|
35
|
-
expect(s).toContain("Tab");
|
|
36
|
-
});
|
|
37
|
-
|
|
38
|
-
test("reminds about the schema", () => {
|
|
39
|
-
expect(s).toContain("header");
|
|
40
|
-
expect(s).toContain("question");
|
|
41
|
-
expect(s).toContain("options");
|
|
42
|
-
expect(s).toContain("recommended");
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
test("is reasonably short (< 2.5 KB) to not bloat every turn", () => {
|
|
46
|
-
// Sanity check — if this grows, consider trimming. The cost is
|
|
47
|
-
// paid on every turn (before_agent_start runs every prompt).
|
|
48
|
-
expect(s.length).toBeLessThan(2500);
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
test("is a pure function (same output across calls)", () => {
|
|
52
|
-
expect(buildAskProSection()).toBe(s);
|
|
53
|
-
});
|
|
54
|
-
});
|