pi-soly 0.2.1
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 +372 -0
- package/agents/soly-debugger.md +60 -0
- package/agents/soly-documenter.md +82 -0
- package/agents/soly-oracle.md +69 -0
- package/agents/soly-refactor.md +65 -0
- package/agents/soly-reviewer.md +107 -0
- package/agents/soly-tester.md +56 -0
- package/agents/soly-worker.md +84 -0
- package/agents-install.ts +105 -0
- package/commands.ts +778 -0
- package/config.ts +228 -0
- package/core.ts +1599 -0
- package/docs.ts +235 -0
- package/env.ts +196 -0
- package/git.ts +95 -0
- package/html.ts +157 -0
- package/index.ts +718 -0
- package/integrations.ts +64 -0
- package/intent.ts +303 -0
- package/iteration.ts +712 -0
- package/nudge.ts +123 -0
- package/package.json +66 -0
- package/scratchpad.ts +117 -0
- package/tools.ts +1132 -0
- package/workflows/execute.ts +401 -0
- package/workflows/index.ts +235 -0
- package/workflows/inspect.ts +492 -0
- package/workflows/parser.ts +268 -0
- package/workflows/pause.ts +150 -0
- package/workflows/planning.ts +624 -0
- package/workflows/quick.ts +258 -0
- package/workflows/resume.ts +201 -0
- package/workflows-data/discuss-phase.md +292 -0
- package/workflows-data/execute-phase.md +200 -0
- package/workflows-data/execute-plan.md +251 -0
- package/workflows-data/execute-task.md +116 -0
- package/workflows-data/pause-work.md +142 -0
- package/workflows-data/plan-phase.md +199 -0
- package/workflows-data/plan-task.md +185 -0
|
@@ -0,0 +1,624 @@
|
|
|
1
|
+
// =============================================================================
|
|
2
|
+
// workflows/planning.ts — `soly plan <target>` / `soly discuss <N>` handlers
|
|
3
|
+
// =============================================================================
|
|
4
|
+
//
|
|
5
|
+
// `soly plan <target>` — produce a PLAN.md for a phase or task.
|
|
6
|
+
// Dual-mode: phases and tasks live side by side.
|
|
7
|
+
// `soly discuss <N>` — discuss scope, requirements, and tradeoffs for a
|
|
8
|
+
// phase before any planning starts (phase-only in v0.2)
|
|
9
|
+
//
|
|
10
|
+
// Both transform into LLM instructions that load the relevant workflow
|
|
11
|
+
// markdown (plan-phase.md / plan-task.md / discuss-phase.md) and delegate
|
|
12
|
+
// to a subagent.
|
|
13
|
+
// =============================================================================
|
|
14
|
+
|
|
15
|
+
import * as fs from "node:fs";
|
|
16
|
+
import * as path from "node:path";
|
|
17
|
+
import { fileURLToPath } from "node:url";
|
|
18
|
+
import { describePlanTarget, type SolyCommand } from "./parser.js";
|
|
19
|
+
import type { SolyState } from "../core.js";
|
|
20
|
+
import {
|
|
21
|
+
extractPlanSummary,
|
|
22
|
+
renderPlanSummaryInline,
|
|
23
|
+
writeIterationContext,
|
|
24
|
+
} from "../iteration.js";
|
|
25
|
+
|
|
26
|
+
/** Build the inline plan summary block for the worker task (so it has the
|
|
27
|
+
* must_haves / wave / requirements even before reading the iteration file). */
|
|
28
|
+
function inlinePlanSummary(planFilePath: string): string {
|
|
29
|
+
const raw = fs.readFileSync(planFilePath, "utf-8");
|
|
30
|
+
const summary = extractPlanSummary(raw);
|
|
31
|
+
if (!summary) return "_(PLAN.md missing frontmatter or unparseable)_";
|
|
32
|
+
return renderPlanSummaryInline(summary);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Resolve <extension>/workflows-data/<name>.md regardless of cwd. */
|
|
36
|
+
function loadWorkflowMarkdown(name: string): string | null {
|
|
37
|
+
try {
|
|
38
|
+
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
39
|
+
const candidate = path.resolve(here, "..", "workflows-data", name);
|
|
40
|
+
if (fs.existsSync(candidate)) return fs.readFileSync(candidate, "utf-8");
|
|
41
|
+
} catch {
|
|
42
|
+
// fall through
|
|
43
|
+
}
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface PlanningHandlerResult {
|
|
48
|
+
handled: boolean;
|
|
49
|
+
transformedText?: string;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function getPhaseForDiscuss(
|
|
53
|
+
state: SolyState,
|
|
54
|
+
args: string[],
|
|
55
|
+
): { phase: number; raw: string } | null {
|
|
56
|
+
const raw = (args[0] ?? "").trim();
|
|
57
|
+
if (!raw) return null;
|
|
58
|
+
const m = raw.match(/^(\d+)$/);
|
|
59
|
+
if (!m) return null;
|
|
60
|
+
const n = parseInt(m[1], 10);
|
|
61
|
+
if (!state.phases.find((p) => p.number === n)) return null;
|
|
62
|
+
return { phase: n, raw };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function buildPlanTransform(cmd: SolyCommand, state: SolyState): PlanningHandlerResult {
|
|
66
|
+
if (!state.exists) {
|
|
67
|
+
return {
|
|
68
|
+
handled: true,
|
|
69
|
+
transformedText:
|
|
70
|
+
`soly plan: no .soly/ directory in cwd (${state.solyDir || "<cwd>"}) — cannot plan.\n` +
|
|
71
|
+
`Initialize a soly project first.`,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const projectRoot = path.dirname(state.solyDir);
|
|
76
|
+
const target = describePlanTarget(cmd.args);
|
|
77
|
+
if (!target) {
|
|
78
|
+
const knownPhases = state.phases.map((p) => p.number).join(", ") || "(none)";
|
|
79
|
+
const knownTasks = state.tasks.map((t) => t.id).join(", ") || "(none)";
|
|
80
|
+
return {
|
|
81
|
+
handled: true,
|
|
82
|
+
transformedText:
|
|
83
|
+
`soly plan: missing or malformed target.\n` +
|
|
84
|
+
`Usage:\n` +
|
|
85
|
+
` soly plan <N> — plan phase N\n` +
|
|
86
|
+
` soly plan <task-id> — plan existing task\n` +
|
|
87
|
+
` soly plan --new-task <slug> --feature <n> — create new task dir + PLAN.md\n` +
|
|
88
|
+
` soly plan --feature <n> — plan all ready tasks in a feature\n` +
|
|
89
|
+
`Known phases: ${knownPhases}\n` +
|
|
90
|
+
`Known tasks: ${knownTasks}`,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// === PHASE MODE ===
|
|
95
|
+
if (target.kind === "phase") {
|
|
96
|
+
const phase = state.phases.find((p) => p.number === target.phase);
|
|
97
|
+
if (!phase) {
|
|
98
|
+
return {
|
|
99
|
+
handled: true,
|
|
100
|
+
transformedText:
|
|
101
|
+
`soly plan: phase ${target.phase} not found.\n` +
|
|
102
|
+
`Known phases: ${state.phases.map((p) => p.number).join(", ") || "(none)"}`,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
const workflow = loadWorkflowMarkdown("plan-phase.md");
|
|
106
|
+
if (!workflow) {
|
|
107
|
+
return {
|
|
108
|
+
handled: true,
|
|
109
|
+
transformedText:
|
|
110
|
+
`soly plan: workflow markdown not found: workflows-data/plan-phase.md\n` +
|
|
111
|
+
`This is an extension installation issue — reinstall soly.`,
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
// Write per-iteration context bundle (B2).
|
|
115
|
+
const iter = writeIterationContext({
|
|
116
|
+
solyDir: state.solyDir,
|
|
117
|
+
projectRoot,
|
|
118
|
+
kind: "plan",
|
|
119
|
+
phaseNumber: target.phase,
|
|
120
|
+
});
|
|
121
|
+
const instruction = `soly plan ${target.raw} — planning phase ${target.phase} (${phase.name}).
|
|
122
|
+
|
|
123
|
+
**Iteration context file written:** \`${iter.relPath}\` (${iter.tokens} tokens, ${iter.bytes} bytes)
|
|
124
|
+
The planner reads this file first — it contains intent, STATE, ROADMAP row for this phase, phase CONTEXT, phase RESEARCH, and prior SUMMARYs.
|
|
125
|
+
|
|
126
|
+
**0-POINT CHECK — read .soly/docs/ first.**
|
|
127
|
+
These documents hold the project's INTENT — business context, design vision, what the user wants this app to be. Plans that ignore intent produce code that "works" but doesn't fit. If the plan would diverge from anything in .soly/docs/, surface that as a discussion point before committing to it.
|
|
128
|
+
|
|
129
|
+
Phase directory: ${phase.dir}
|
|
130
|
+
Current state: planCount=${phase.planCount}, context=${phase.contextExists}, research=${phase.researchExists}
|
|
131
|
+
|
|
132
|
+
Launch a subagent to produce the plan. Do NOT plan inline.
|
|
133
|
+
|
|
134
|
+
subagent({
|
|
135
|
+
agent: "worker",
|
|
136
|
+
context: "fresh",
|
|
137
|
+
async: true,
|
|
138
|
+
task: \`You are a planner. Produce PLAN.md (and ${phase.contextExists ? "" : "optionally CONTEXT.md / "}RESEARCH.md if missing) for phase ${target.phase}.
|
|
139
|
+
|
|
140
|
+
**FIRST ACTION — read the iteration context file:**
|
|
141
|
+
\`\`\`
|
|
142
|
+
${iter.relPath}
|
|
143
|
+
\`\`\`
|
|
144
|
+
It contains intent, STATE, ROADMAP row, phase CONTEXT, phase RESEARCH, and prior SUMMARYs.
|
|
145
|
+
|
|
146
|
+
Project root: ${projectRoot}
|
|
147
|
+
Soly dir: ${state.solyDir}
|
|
148
|
+
Phase dir: ${phase.dir}
|
|
149
|
+
|
|
150
|
+
Follow the workflow below VERBATIM.
|
|
151
|
+
|
|
152
|
+
=== WORKFLOW: plan-phase.md ===
|
|
153
|
+
${workflow}
|
|
154
|
+
=== END WORKFLOW ===
|
|
155
|
+
|
|
156
|
+
Hard rules:
|
|
157
|
+
- Do not write production code. Planning only.
|
|
158
|
+
- Wave numbers must be pre-computed; dependency graph must be acyclic.
|
|
159
|
+
- Each plan needs requirements, must_haves.truths, must_haves.artifacts, must_haves.key_links.
|
|
160
|
+
- PATH DISCIPLINE: all PLAN.md / CONTEXT.md / RESEARCH.md files go under \`.soly/phases/<NN>-<slug>/\`. Never write plan files to the project root.
|
|
161
|
+
- Update .soly/STATE.md Current Position at the end.
|
|
162
|
+
- Return: created files, plan count, wave breakdown, open questions.
|
|
163
|
+
\`
|
|
164
|
+
})
|
|
165
|
+
|
|
166
|
+
When the subagent returns, summarize the plan structure and ask the user to confirm before any execution.`;
|
|
167
|
+
return { handled: true, transformedText: instruction };
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// === TASK MODE (existing task — flesh out PLAN.md) ===
|
|
171
|
+
if (target.kind === "task") {
|
|
172
|
+
const task = state.tasks.find((t) => t.id === target.taskId);
|
|
173
|
+
if (!task) {
|
|
174
|
+
return {
|
|
175
|
+
handled: true,
|
|
176
|
+
transformedText:
|
|
177
|
+
`soly plan: task ${target.taskId} not found.\n` +
|
|
178
|
+
`Known tasks: ${state.tasks.map((t) => t.id).join(", ") || "(none)"}\n` +
|
|
179
|
+
`Tip: use the \`soly_list_tasks\` tool.`,
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
if (!task.planExists) {
|
|
183
|
+
return {
|
|
184
|
+
handled: true,
|
|
185
|
+
transformedText:
|
|
186
|
+
`soly plan: task ${target.taskId} has no PLAN.md at ${task.dir}/PLAN.md.\n` +
|
|
187
|
+
`Use \`soly plan --new-task <slug> --feature ${task.feature}\` to create a different task, or write PLAN.md manually.`,
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
if (task.status === "done") {
|
|
191
|
+
return {
|
|
192
|
+
handled: true,
|
|
193
|
+
transformedText:
|
|
194
|
+
`soly plan: task ${target.taskId} is already \`status: done\` (SUMMARY.md exists).\n` +
|
|
195
|
+
`To re-plan, change status to \`ready\` in PLAN.md frontmatter first.`,
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
const workflow = loadWorkflowMarkdown("plan-task.md");
|
|
199
|
+
if (!workflow) {
|
|
200
|
+
return {
|
|
201
|
+
handled: true,
|
|
202
|
+
transformedText:
|
|
203
|
+
`soly plan: workflow markdown not found: workflows-data/plan-task.md\n` +
|
|
204
|
+
`This is an extension installation issue — reinstall soly.`,
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
const featureDir = path.dirname(path.dirname(task.dir));
|
|
208
|
+
// Write per-iteration context bundle (B2).
|
|
209
|
+
const iter = writeIterationContext({
|
|
210
|
+
solyDir: state.solyDir,
|
|
211
|
+
projectRoot,
|
|
212
|
+
kind: "plan",
|
|
213
|
+
taskId: task.id,
|
|
214
|
+
feature: task.feature,
|
|
215
|
+
});
|
|
216
|
+
const planFile = path.join(task.dir, "PLAN.md");
|
|
217
|
+
const inlineSummary = inlinePlanSummary(planFile);
|
|
218
|
+
const instruction = `soly plan ${target.taskId} — fleshing out PLAN.md for existing task.
|
|
219
|
+
|
|
220
|
+
**Task:** ${task.id}
|
|
221
|
+
**Feature:** ${task.feature}
|
|
222
|
+
**Kind:** ${task.kind}
|
|
223
|
+
**Current status:** ${task.status}
|
|
224
|
+
**PLAN.md path:** ${task.dir}/PLAN.md
|
|
225
|
+
**Feature README:** ${featureDir}/README.md
|
|
226
|
+
|
|
227
|
+
**Iteration context file written:** \`${iter.relPath}\` (${iter.tokens} tokens)
|
|
228
|
+
The planner reads this file first — it contains intent, STATE, the feature README, prior task SUMMARYs, and the current task PLAN (refine, don't re-derive).
|
|
229
|
+
|
|
230
|
+
**Inline plan summary (so you have the must-haves even before reading the file):**
|
|
231
|
+
${inlineSummary}
|
|
232
|
+
|
|
233
|
+
**0-POINT CHECK.** Re-read .soly/docs/ (intent) and .soly/features/${task.feature}/README.md (feature context) before refining the plan.
|
|
234
|
+
|
|
235
|
+
This task already has PLAN.md. Your job is to flesh it out / improve it based on intent and feature context — not to start from scratch.
|
|
236
|
+
|
|
237
|
+
Launch a single subagent to refine the plan:
|
|
238
|
+
|
|
239
|
+
subagent({
|
|
240
|
+
agent: "worker",
|
|
241
|
+
context: "fresh",
|
|
242
|
+
async: true,
|
|
243
|
+
task: \`You are a planner. Refine PLAN.md for an existing task.
|
|
244
|
+
|
|
245
|
+
**FIRST ACTION — read the iteration context file:**
|
|
246
|
+
\`\`\`
|
|
247
|
+
${iter.relPath}
|
|
248
|
+
\`\`\`
|
|
249
|
+
It contains intent, STATE, feature README, prior task SUMMARYs, and the current task PLAN (refine, don't re-derive).
|
|
250
|
+
|
|
251
|
+
Project root: ${projectRoot}
|
|
252
|
+
Soly dir: ${state.solyDir}
|
|
253
|
+
Task dir: ${task.dir}
|
|
254
|
+
Feature dir: ${featureDir}
|
|
255
|
+
|
|
256
|
+
Follow the workflow below VERBATIM.
|
|
257
|
+
|
|
258
|
+
=== WORKFLOW: plan-task.md ===
|
|
259
|
+
${workflow}
|
|
260
|
+
=== END WORKFLOW ===
|
|
261
|
+
|
|
262
|
+
Hard rules:
|
|
263
|
+
- Do not write production code. Planning only.
|
|
264
|
+
- Preserve the existing frontmatter (id, kind, feature, status, etc.) — only update if you find a bug.
|
|
265
|
+
- If you change the plan body materially, commit it as \`chore(tasks): refine plan <task-id>\`.
|
|
266
|
+
- If you only add small clarifications, no commit needed (or include in same commit).
|
|
267
|
+
- PATH DISCIPLINE: PLAN.md lives at \`.soly/features/<feature>/tasks/<id>/PLAN.md\`. Never write to the project root.
|
|
268
|
+
- Return: what changed, open questions, dependencies discovered.
|
|
269
|
+
\`
|
|
270
|
+
})
|
|
271
|
+
|
|
272
|
+
When the subagent returns, summarize what was refined. Do not execute — planning only.`;
|
|
273
|
+
return { handled: true, transformedText: instruction };
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// === NEW-TASK MODE (create task dir + PLAN.md skeleton) ===
|
|
277
|
+
if (target.kind === "new-task") {
|
|
278
|
+
let feature = state.features.find((f) => f.name === target.feature);
|
|
279
|
+
if (!feature) {
|
|
280
|
+
// Auto-create the feature dir + README so the planner can immediately
|
|
281
|
+
// write a PLAN.md. Idempotent; safe to run repeatedly.
|
|
282
|
+
const featuresRoot = path.join(state.solyDir, "features");
|
|
283
|
+
const featureDir = path.join(featuresRoot, target.feature);
|
|
284
|
+
const featureReadme = path.join(featureDir, "README.md");
|
|
285
|
+
try {
|
|
286
|
+
fs.mkdirSync(path.join(featureDir, "tasks"), { recursive: true });
|
|
287
|
+
if (!fs.existsSync(featureReadme)) {
|
|
288
|
+
fs.writeFileSync(
|
|
289
|
+
featureReadme,
|
|
290
|
+
`# Feature: ${target.feature}\n\nDescribe the feature's purpose here.\n`,
|
|
291
|
+
"utf-8",
|
|
292
|
+
);
|
|
293
|
+
}
|
|
294
|
+
} catch (e) {
|
|
295
|
+
return {
|
|
296
|
+
handled: true,
|
|
297
|
+
transformedText:
|
|
298
|
+
`soly plan: could not auto-create .soly/features/${target.feature}/ (${(e as Error).message}). ` +
|
|
299
|
+
`Create it manually: \`mkdir -p .soly/features/${target.feature}/tasks/\``,
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
// Re-read state so the planner sees the new feature
|
|
303
|
+
feature = { name: target.feature, slug: target.feature, dir: featureDir, taskCount: 0, readmeExists: true, tasks: [] };
|
|
304
|
+
}
|
|
305
|
+
const workflow = loadWorkflowMarkdown("plan-task.md");
|
|
306
|
+
if (!workflow) {
|
|
307
|
+
return {
|
|
308
|
+
handled: true,
|
|
309
|
+
transformedText:
|
|
310
|
+
`soly plan: workflow markdown not found: workflows-data/plan-task.md\n` +
|
|
311
|
+
`This is an extension installation issue — reinstall soly.`,
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
const featureDir = feature.dir;
|
|
315
|
+
// For new-task mode, the PLAN.md doesn't exist yet, so no iteration
|
|
316
|
+
// bundle — the planner will write it as part of its work. We only
|
|
317
|
+
// pass the feature README + intent + state path hints.
|
|
318
|
+
const instruction = `soly plan --new-task ${target.slug} --feature ${target.feature} — creating new task.
|
|
319
|
+
|
|
320
|
+
**Feature:** ${target.feature}
|
|
321
|
+
**Slug:** ${target.slug}
|
|
322
|
+
**Feature README:** ${featureDir}/README.md
|
|
323
|
+
|
|
324
|
+
**0-POINT CHECK.** Re-read .soly/docs/ (intent) and .soly/features/${target.feature}/README.md (feature context) before planning.
|
|
325
|
+
|
|
326
|
+
**Step 1 — generate task ID.** The task ID is \`<slug>-<4hex>\` (e.g. \`${target.slug}-a3f9\`). Generate 4 lowercase hex chars (use \`crypto.randomBytes(2).toString('hex')\` in node, or any 4-char [0-9a-f]{4} string if you don't have a shell handy).
|
|
327
|
+
|
|
328
|
+
**Step 2 — create the dir:**
|
|
329
|
+
\`\`\`
|
|
330
|
+
mkdir -p .soly/features/${target.feature}/tasks/<id>
|
|
331
|
+
\`\`\`
|
|
332
|
+
|
|
333
|
+
**Step 3 — write PLAN.md** with the frontmatter below + the plan body.
|
|
334
|
+
|
|
335
|
+
**Frontmatter (REQUIRED):**
|
|
336
|
+
\`\`\`yaml
|
|
337
|
+
---
|
|
338
|
+
id: <id>
|
|
339
|
+
kind: <be|fe|infra|docs|integration>
|
|
340
|
+
feature: ${target.feature}
|
|
341
|
+
status: ready
|
|
342
|
+
priority: <high|medium|low>
|
|
343
|
+
parallelizable: <true|false>
|
|
344
|
+
depends-on: []
|
|
345
|
+
---
|
|
346
|
+
|
|
347
|
+
# Task: <title>
|
|
348
|
+
|
|
349
|
+
[body produced by the planner workflow below]
|
|
350
|
+
\`\`\`
|
|
351
|
+
|
|
352
|
+
Launch a single subagent to flesh out the plan body:
|
|
353
|
+
|
|
354
|
+
subagent({
|
|
355
|
+
agent: "worker",
|
|
356
|
+
context: "fresh",
|
|
357
|
+
async: true,
|
|
358
|
+
task: \`You are a planner. Create a new task dir + write PLAN.md with frontmatter.
|
|
359
|
+
|
|
360
|
+
Project root: ${projectRoot}
|
|
361
|
+
Soly dir: ${state.solyDir}
|
|
362
|
+
Feature dir: ${featureDir}
|
|
363
|
+
Target slug: ${target.slug}
|
|
364
|
+
|
|
365
|
+
=== WORKFLOW: plan-task.md ===
|
|
366
|
+
${workflow}
|
|
367
|
+
=== END WORKFLOW ===
|
|
368
|
+
|
|
369
|
+
Hard rules:
|
|
370
|
+
- Do not write production code. Planning only.
|
|
371
|
+
- Generate the task id as \`<slug>-<4hex>\` (e.g. \`${target.slug}-a3f9\`) — use 4 lowercase hex chars.
|
|
372
|
+
- Create the dir \`.soly/features/${target.feature}/tasks/<id>/\` first.
|
|
373
|
+
- Write PLAN.md with the frontmatter (id, kind, feature, status: ready, priority, parallelizable, depends-on).
|
|
374
|
+
- Pick a \`kind:\` value matching the work (be|fe|infra|docs|integration).
|
|
375
|
+
- Pick a reasonable \`priority:\` (default: medium).
|
|
376
|
+
- Leave \`depends-on:\` as \`[]\` unless you have a clear dep on an existing task.
|
|
377
|
+
- Commit: \`chore(tasks): plan <id>\`.
|
|
378
|
+
- Return: created path, task id, plan summary.
|
|
379
|
+
\`
|
|
380
|
+
})
|
|
381
|
+
|
|
382
|
+
When the subagent returns, show the user the new task id + summary. They can then run \`soly execute <id>\`.`;
|
|
383
|
+
return { handled: true, transformedText: instruction };
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// === FEATURE MODE (plan all ready tasks in a feature) ===
|
|
387
|
+
if (target.kind === "feature") {
|
|
388
|
+
const feature = state.features.find((f) => f.name === target.feature);
|
|
389
|
+
if (!feature) {
|
|
390
|
+
return {
|
|
391
|
+
handled: true,
|
|
392
|
+
transformedText:
|
|
393
|
+
`soly plan: feature "${target.feature}" not found.\n` +
|
|
394
|
+
`Known features: ${state.features.map((f) => f.name).join(", ") || "(none)"}`,
|
|
395
|
+
};
|
|
396
|
+
}
|
|
397
|
+
const featureTasks = state.tasks.filter((t) => t.feature === target.feature);
|
|
398
|
+
if (featureTasks.length === 0) {
|
|
399
|
+
return {
|
|
400
|
+
handled: true,
|
|
401
|
+
transformedText:
|
|
402
|
+
`soly plan: no tasks in feature "${target.feature}".\n` +
|
|
403
|
+
`Use \`soly plan --new-task <slug> --feature ${target.feature}\` to create one.`,
|
|
404
|
+
};
|
|
405
|
+
}
|
|
406
|
+
// For "plan all tasks in feature" mode: we don't know which task to
|
|
407
|
+
// bundle, so the planner will iterate per-task. The parent supplies
|
|
408
|
+
// the high-level feature README + task list. Per-task iteration
|
|
409
|
+
// bundles are written by the planner via the extension's write
|
|
410
|
+
// function (call from a child tool if needed).
|
|
411
|
+
const ready = featureTasks.filter((t) => t.status === "ready");
|
|
412
|
+
const done = featureTasks.filter((t) => t.status === "done");
|
|
413
|
+
const blocked = featureTasks.filter((t) => t.status === "blocked");
|
|
414
|
+
const inProgress = featureTasks.filter((t) => t.status === "in-progress");
|
|
415
|
+
if (ready.length === 0) {
|
|
416
|
+
return {
|
|
417
|
+
handled: true,
|
|
418
|
+
transformedText:
|
|
419
|
+
`soly plan --feature ${target.feature}: no tasks need planning.\n` +
|
|
420
|
+
`Tasks: ${featureTasks.length} total, ${done.length} done, ${inProgress.length} in-progress, ${blocked.length} blocked.`,
|
|
421
|
+
};
|
|
422
|
+
}
|
|
423
|
+
return {
|
|
424
|
+
handled: true,
|
|
425
|
+
transformedText:
|
|
426
|
+
`soly plan --feature ${target.feature}: ${ready.length} task(s) need planning.\n\n` +
|
|
427
|
+
`Tasks:\n` +
|
|
428
|
+
ready.map((t, i) => ` ${i + 1}. ${t.id} [${t.kind}] prio=${t.priority}`).join("\n") +
|
|
429
|
+
`\n\nLaunch a single subagent to plan them in order. The subagent uses the plan-task.md workflow per task.`,
|
|
430
|
+
};
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
// Unreachable (describePlanTarget only returns the 4 kinds above)
|
|
434
|
+
return {
|
|
435
|
+
handled: true,
|
|
436
|
+
transformedText: `soly plan: unknown target kind.`,
|
|
437
|
+
};
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
export function buildDiscussTransform(
|
|
441
|
+
cmd: SolyCommand,
|
|
442
|
+
state: SolyState,
|
|
443
|
+
opts: { hasAskPro?: boolean } = {},
|
|
444
|
+
): PlanningHandlerResult {
|
|
445
|
+
const hasAskPro = opts.hasAskPro ?? false;
|
|
446
|
+
if (!state.exists) {
|
|
447
|
+
return {
|
|
448
|
+
handled: true,
|
|
449
|
+
transformedText:
|
|
450
|
+
`soly discuss: no .soly/ directory in cwd (${state.solyDir || "<cwd>"}) — cannot discuss.\n` +
|
|
451
|
+
`Initialize a soly project first.`,
|
|
452
|
+
};
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
const projectRoot = path.dirname(state.solyDir);
|
|
456
|
+
const target = getPhaseForDiscuss(state, cmd.args);
|
|
457
|
+
if (!target) {
|
|
458
|
+
const known = state.phases.map((p) => p.number).join(", ") || "(none)";
|
|
459
|
+
return {
|
|
460
|
+
handled: true,
|
|
461
|
+
transformedText:
|
|
462
|
+
`soly discuss: phase argument required and must exist.\n` +
|
|
463
|
+
`Usage: soly discuss <N> (e.g. "soly discuss 11")\n` +
|
|
464
|
+
`Known phases: ${known}`,
|
|
465
|
+
};
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
const phase = state.phases.find((p) => p.number === target.phase)!;
|
|
469
|
+
|
|
470
|
+
// Write per-iteration context bundle (B2).
|
|
471
|
+
const iter = writeIterationContext({
|
|
472
|
+
solyDir: state.solyDir,
|
|
473
|
+
projectRoot,
|
|
474
|
+
kind: "discuss",
|
|
475
|
+
phaseNumber: target.phase,
|
|
476
|
+
});
|
|
477
|
+
|
|
478
|
+
const phaseDir = path.join(state.solyDir, "phases", phase.slug);
|
|
479
|
+
const padded = String(target.phase).padStart(2, "0");
|
|
480
|
+
const contextPath = path.join(phaseDir, `${padded}-CONTEXT.md`);
|
|
481
|
+
const checkpointPath = path.join(phaseDir, `${padded}-DISCUSS-CHECKPOINT.json`);
|
|
482
|
+
|
|
483
|
+
// Optional workflow reference (background only — not a strict protocol anymore)
|
|
484
|
+
const workflow = loadWorkflowMarkdown("discuss-phase.md");
|
|
485
|
+
|
|
486
|
+
// Resume / refine detection
|
|
487
|
+
let resumeBlock = "";
|
|
488
|
+
if (fs.existsSync(checkpointPath)) {
|
|
489
|
+
try {
|
|
490
|
+
const ck = JSON.parse(fs.readFileSync(checkpointPath, "utf-8")) as {
|
|
491
|
+
decisions?: Array<{ category: string; choice: string }>;
|
|
492
|
+
areas_total?: number;
|
|
493
|
+
areas_completed?: number[];
|
|
494
|
+
};
|
|
495
|
+
const decisions = ck.decisions ?? [];
|
|
496
|
+
resumeBlock = `\n**RESUME MODE** — found checkpoint at \`${path.relative(projectRoot, checkpointPath)}\`:\n${decisions
|
|
497
|
+
.map((d) => ` - ${d.category}: ${d.choice}`)
|
|
498
|
+
.join("\n")}\n\nAcknowledge these as **Decisions Locked** at the top of your output, then continue with the next un-answered gray area. Resume from where the prior session left off.`;
|
|
499
|
+
} catch {
|
|
500
|
+
resumeBlock = `\n**RESUME MODE** — checkpoint file existed but was malformed; ignoring it and starting fresh.`;
|
|
501
|
+
}
|
|
502
|
+
} else if (fs.existsSync(contextPath)) {
|
|
503
|
+
resumeBlock = `\n**REFINE MODE** — \`${padded}-CONTEXT.md\` already exists. Read it, list the existing decisions, and only ask about uncovered gray areas. Don't re-ask locked decisions.`;
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
const instruction = `soly discuss ${target.raw} — interactive discussion mode for phase ${target.phase} (${phase.name}).
|
|
507
|
+
|
|
508
|
+
**Iteration context file written:** \`${iter.relPath}\` (${iter.tokens} tokens, ${iter.bytes} bytes)
|
|
509
|
+
Read it first — it contains intent, STATE, ROADMAP, and any existing phase artifacts (CONTEXT, RESEARCH, prior SUMMARYs). It's your single source of truth.
|
|
510
|
+
|
|
511
|
+
${resumeBlock}
|
|
512
|
+
|
|
513
|
+
**This is NOT a subagent task.** You're running interactively. Drive the discussion yourself, in this session, by asking the user a few questions one at a time.
|
|
514
|
+
|
|
515
|
+
---
|
|
516
|
+
|
|
517
|
+
${
|
|
518
|
+
hasAskPro
|
|
519
|
+
? `**PREFERRED PICKER: \`ask_pro\` (from the \`pi-ask\` extension)** is available in this session.
|
|
520
|
+
|
|
521
|
+
This is a multi-question tabbed picker — one call shows all your questions as tabs, the user navigates with Tab/arrows and picks with 1-N. It returns all answers in one shot. This is much better UX than N separate \`soly_ask_user\` calls.
|
|
522
|
+
|
|
523
|
+
**Pattern:**
|
|
524
|
+
\`\`\`
|
|
525
|
+
ask_pro({
|
|
526
|
+
questions: [
|
|
527
|
+
{ header: "Auth", question: "Which auth approach?", options: [...], multiSelect: false },
|
|
528
|
+
{ header: "Tokens", question: "Where to store tokens?", options: [...], allowOther: true },
|
|
529
|
+
{ header: "Errors", question: "How to handle auth errors?", options: [...], multiSelect: true },
|
|
530
|
+
]
|
|
531
|
+
})
|
|
532
|
+
// → { answers: { 0: 1, 1: "Bearer in Authorization header", 2: [0, 2] } }
|
|
533
|
+
// or { cancelled: true }
|
|
534
|
+
\`\`\`
|
|
535
|
+
|
|
536
|
+
- Each option: \`{ label, description?, recommended? }\`. Mark the best with \`recommended: true\` (shown as ⭐ in the UI).
|
|
537
|
+
- For questions where the user might want a custom answer, add \`allowOther: true\` — the user gets a "Other…" option that opens a text input.
|
|
538
|
+
- For multi-select questions (checkboxes), set \`multiSelect: true\`. The answer is an array of option indices (and/or strings if \`allowOther\` is on).
|
|
539
|
+
- 2-4 options per question, max 6 questions per call.
|
|
540
|
+
- If the user cancels, you get \`{cancelled: true}\` — treat that as "deferred, ask differently" or just end the discuss.
|
|
541
|
+
- After getting answers, call \`soly_finish_discuss\` to write the canonical CONTEXT.md.
|
|
542
|
+
|
|
543
|
+
If \`ask_pro\` is not available (rare — would mean the user uninstalled the \`pi-ask\` extension), fall back to \`soly_ask_user\` (one call per question, see the fallback section below).`
|
|
544
|
+
: `**PICKER: \`soly_ask_user\`** is the available multi-choice picker in this session.
|
|
545
|
+
|
|
546
|
+
**Pattern (one call per question, one at a time):**
|
|
547
|
+
\`\`\`
|
|
548
|
+
soly_ask_user({
|
|
549
|
+
title: "Q1: <category>",
|
|
550
|
+
question: "<one short sentence>",
|
|
551
|
+
options: [
|
|
552
|
+
"⭐ <recommended option> — <1 sentence why>",
|
|
553
|
+
"<alternative 1>",
|
|
554
|
+
"<alternative 2>",
|
|
555
|
+
],
|
|
556
|
+
rationale: "<1–2 sentence note shown above the picker>",
|
|
557
|
+
})
|
|
558
|
+
\`\`\`
|
|
559
|
+
|
|
560
|
+
- Always include a recommended answer (⭐ first option) with 1-sentence rationale.
|
|
561
|
+
- After each answer, briefly acknowledge ("OK, locking X. Next:") and call \`soly_ask_user\` for the next question. **Do NOT dump all questions at once.**
|
|
562
|
+
- Never include "skip" / "you decide" as a default option. If a question is too hard, include a real option like \`"Defer — discuss in a future phase"\`.
|
|
563
|
+
- Note: \`soly_ask_user\` does NOT support \`allowOther\` (no text input). For questions that might need a custom answer, include a "Other (describe)" option that says the user can type a free-text answer in their next chat message.
|
|
564
|
+
|
|
565
|
+
**Tip:** the separate \`pi-ask\` extension provides a better UX (multi-question tabbed picker, \`allowOther\` text input). If it's not installed, \`soly_ask_user\` is the fallback.`
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
---
|
|
569
|
+
|
|
570
|
+
**Common flow (applies to both pickers):**
|
|
571
|
+
|
|
572
|
+
1. **Open with a 1–2 sentence framing** of what this phase delivers (grounded in ROADMAP + intent). No implementation details. Then show the locked decisions (if any from resume). Then say: "I have <N> questions about this phase. Let's go."
|
|
573
|
+
|
|
574
|
+
2. **Call the picker ONCE** (preferred) **or per-question** (fallback) as above. Always include a recommended answer (⭐ first option) with 1-sentence rationale.
|
|
575
|
+
|
|
576
|
+
3. **Save checkpoint after each answer** with \`soly_save_discuss_checkpoint({phase_number, decisions, areas_total, areas_completed})\` so the user can quit and resume. The final \`soly_finish_discuss\` will delete the checkpoint and write CONTEXT.md.
|
|
577
|
+
|
|
578
|
+
4. **After all questions captured, call \`soly_finish_discuss\`:**
|
|
579
|
+
\`\`\`
|
|
580
|
+
soly_finish_discuss({
|
|
581
|
+
phase_number: ${target.phase},
|
|
582
|
+
domain: "<1–2 paragraphs: what this phase delivers>",
|
|
583
|
+
decisions: [
|
|
584
|
+
{ category: "<cat>", choice: "<what was chosen>", rationale: "<why>" },
|
|
585
|
+
...
|
|
586
|
+
],
|
|
587
|
+
canonical_refs: [".soly/docs/<file>", ".soly/ROADMAP.md", ...],
|
|
588
|
+
deferred_ideas: ["<scope creep for future phase>", ...],
|
|
589
|
+
codebase_context: ["src/components/Card.tsx — has rounded/shadow variants, reuse", ...],
|
|
590
|
+
})
|
|
591
|
+
\`\`\`
|
|
592
|
+
|
|
593
|
+
5. **Tell the user the next step** after \`soly_finish_discuss\` returns: \`soly plan ${target.phase}\`.
|
|
594
|
+
|
|
595
|
+
---
|
|
596
|
+
|
|
597
|
+
**Available tools for this flow:**
|
|
598
|
+
- ${hasAskPro ? "`ask_pro` — multi-question tabbed picker (PREFERRED)" : "`soly_ask_user` — single-question picker (fallback)"}
|
|
599
|
+
- \`soly_save_discuss_checkpoint\` — save partial progress (use after each answer)
|
|
600
|
+
- \`soly_finish_discuss\` — finalize: writes CONTEXT.md, deletes checkpoint
|
|
601
|
+
- \`soly_read\`, \`soly_snippet\`, \`soly_doc_search\`, \`soly_intent\` — read .soly/ artifacts as needed
|
|
602
|
+
- \`soly_log_decision\` — log to STATE.md Decisions table (use sparingly)
|
|
603
|
+
- Standard pi tools: \`read\`, \`bash\`, \`grep\`, \`find\` for codebase context
|
|
604
|
+
|
|
605
|
+
**Workflow reference (background only — not a strict protocol anymore):**
|
|
606
|
+
${workflow ? "```\n" + workflow.slice(0, 1500) + "\n[...truncated, see .pi/agent/extensions/soly/workflows-data/discuss-phase.md for full reference]\n```" : "_(workflow markdown missing)_"}
|
|
607
|
+
|
|
608
|
+
---
|
|
609
|
+
|
|
610
|
+
**Hard rules:**
|
|
611
|
+
- **One picker call** (ask_pro) **or N calls** (soly_ask_user) — never dump all questions as text in your reply.
|
|
612
|
+
- Always include a recommended answer (⭐ first option) with 1-sentence rationale.
|
|
613
|
+
- Use \`soly_save_discuss_checkpoint\` after each answer (so resume works).
|
|
614
|
+
- Use \`soly_finish_discuss\` to finalize — don't just say "done".
|
|
615
|
+
- **No scope creep.** Defer scope-creep items to \`deferred_ideas\`.
|
|
616
|
+
- **No PLAN.md** from this flow — that's \`soly plan ${target.phase}\`.
|
|
617
|
+
- **No SUMMARY.md** — that's from \`soly execute\`.
|
|
618
|
+
- **No code edits.** Discussion only.
|
|
619
|
+
- If intent docs (0-point) are silent on a constraint, ask the user — don't assume.
|
|
620
|
+
|
|
621
|
+
Begin.`;
|
|
622
|
+
|
|
623
|
+
return { handled: true, transformedText: instruction };
|
|
624
|
+
}
|