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,401 @@
|
|
|
1
|
+
// =============================================================================
|
|
2
|
+
// workflows/execute.ts — `soly execute <N>` / `soly execute <N.MM>` handler
|
|
3
|
+
// =============================================================================
|
|
4
|
+
//
|
|
5
|
+
// Intercepts "soly execute 11" (phase) or "soly execute 11.02" (specific plan)
|
|
6
|
+
// and transforms it into a detailed LLM instruction that launches a worker
|
|
7
|
+
// subagent with the soly execute workflow loaded into its system prompt.
|
|
8
|
+
//
|
|
9
|
+
// We use `action: "transform"` (not `action: "handled"`) — the LLM still
|
|
10
|
+
// receives the request, but with the full workflow context, so it can call
|
|
11
|
+
// the `subagent(...)` tool itself and apply the SOLY-specific close-out
|
|
12
|
+
// discipline (commits, SUMMARY.md, STATE.md update).
|
|
13
|
+
//
|
|
14
|
+
// We do NOT spawn the subagent directly from the extension — `subagent(...)`
|
|
15
|
+
// is a tool only available to the LLM (via pi-subagents), and the parent
|
|
16
|
+
// session needs to keep ownership of the close-out loop.
|
|
17
|
+
// =============================================================================
|
|
18
|
+
|
|
19
|
+
import * as fs from "node:fs";
|
|
20
|
+
import * as path from "node:path";
|
|
21
|
+
import { fileURLToPath } from "node:url";
|
|
22
|
+
import { describeExecuteTarget, type SolyCommand } from "./parser.js";
|
|
23
|
+
import type { SolyState } from "../core.js";
|
|
24
|
+
import {
|
|
25
|
+
extractPlanSummary,
|
|
26
|
+
renderPlanSummaryInline,
|
|
27
|
+
writeIterationContext,
|
|
28
|
+
} from "../iteration.js";
|
|
29
|
+
|
|
30
|
+
/** Resolve <extension>/workflows-data/<name>.md regardless of cwd. */
|
|
31
|
+
function loadWorkflowMarkdown(name: string): string | null {
|
|
32
|
+
try {
|
|
33
|
+
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
34
|
+
// workflows-data is sibling of workflows/
|
|
35
|
+
const candidate = path.resolve(here, "..", "workflows-data", name);
|
|
36
|
+
if (fs.existsSync(candidate)) return fs.readFileSync(candidate, "utf-8");
|
|
37
|
+
} catch {
|
|
38
|
+
// fileURLToPath may fail in some runtimes; fall through.
|
|
39
|
+
}
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Build the inline plan summary block for the worker task (so it has the
|
|
44
|
+
* must_haves / wave / requirements even before reading the iteration file). */
|
|
45
|
+
function inlinePlanSummary(planFilePath: string | null): string {
|
|
46
|
+
if (!planFilePath) return "_(no PLAN.md located)_";
|
|
47
|
+
const raw = fs.readFileSync(planFilePath, "utf-8");
|
|
48
|
+
const summary = extractPlanSummary(raw);
|
|
49
|
+
if (!summary) return "_(PLAN.md missing frontmatter or unparseable)_";
|
|
50
|
+
return renderPlanSummaryInline(summary);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface ExecuteHandlerResult {
|
|
54
|
+
handled: boolean;
|
|
55
|
+
transformedText?: string;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Build the transformed LLM instruction for a `soly execute ...` command.
|
|
60
|
+
* Returns { handled: false } if the args are malformed.
|
|
61
|
+
*
|
|
62
|
+
* `interactiveRules` is the list of relPaths marked `interactive: true`
|
|
63
|
+
* — passed to the worker so it knows which rules are explicitly OUT of
|
|
64
|
+
* scope (they describe the user-facing conversation, not the work).
|
|
65
|
+
*/
|
|
66
|
+
export function buildExecuteTransform(
|
|
67
|
+
cmd: SolyCommand,
|
|
68
|
+
state: SolyState,
|
|
69
|
+
interactiveRules: string[] = [],
|
|
70
|
+
opts: { agent?: string; useSolyWorker?: boolean } = {},
|
|
71
|
+
): ExecuteHandlerResult {
|
|
72
|
+
if (!state.exists) {
|
|
73
|
+
return {
|
|
74
|
+
handled: true,
|
|
75
|
+
transformedText:
|
|
76
|
+
`soly: no .soly/ directory found in cwd (${state.solyDir || "<cwd>"}) — cannot execute phase.\n` +
|
|
77
|
+
`Initialize a soly project first (see soly quickstart) before running "soly execute".`,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const projectRoot = path.dirname(state.solyDir);
|
|
82
|
+
const target = describeExecuteTarget(cmd.args);
|
|
83
|
+
if (!target) {
|
|
84
|
+
return {
|
|
85
|
+
handled: true,
|
|
86
|
+
transformedText:
|
|
87
|
+
`soly execute: missing or malformed target.\n` +
|
|
88
|
+
`Usage:\n` +
|
|
89
|
+
` soly execute <N> — execute all plans in phase N\n` +
|
|
90
|
+
` soly execute <N.MM> — execute a specific plan\n` +
|
|
91
|
+
` soly execute <task-id> — execute a specific task (new dual-mode)\n` +
|
|
92
|
+
` soly execute --all — execute all ready tasks (sequential in v0.1)\n` +
|
|
93
|
+
` soly execute --feature <n> — execute all tasks in a feature (sequential in v0.1)`,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// === TASK MODE (new dual-mode) ===
|
|
98
|
+
if (target.kind === "task") {
|
|
99
|
+
const task = state.tasks.find((t) => t.id === target.taskId);
|
|
100
|
+
if (!task) {
|
|
101
|
+
return {
|
|
102
|
+
handled: true,
|
|
103
|
+
transformedText:
|
|
104
|
+
`soly execute: task ${target.taskId} not found in .soly/features/*/tasks/.\n` +
|
|
105
|
+
`Known tasks: ${state.tasks.map((t) => t.id).join(", ") || "(none)"}\n` +
|
|
106
|
+
`Tip: use the \`soly_list_tasks\` tool to see all available tasks.`,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
if (!task.planExists) {
|
|
110
|
+
return {
|
|
111
|
+
handled: true,
|
|
112
|
+
transformedText:
|
|
113
|
+
`soly execute: task ${target.taskId} has no PLAN.md at ${task.dir}/PLAN.md.\n` +
|
|
114
|
+
`Create a PLAN.md with frontmatter (id, kind, feature, status, depends-on) before executing.`,
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
if (task.status === "blocked") {
|
|
118
|
+
return {
|
|
119
|
+
handled: true,
|
|
120
|
+
transformedText:
|
|
121
|
+
`soly execute: task ${target.taskId} is \`status: blocked\`.\n` +
|
|
122
|
+
`Resolve blockers in PLAN.md before executing.`,
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
if (task.status === "done") {
|
|
126
|
+
return {
|
|
127
|
+
handled: true,
|
|
128
|
+
transformedText:
|
|
129
|
+
`soly execute: task ${target.taskId} is already \`status: done\`.\n` +
|
|
130
|
+
`SUMMARY.md exists at ${task.dir}/SUMMARY.md. To re-run, change status to \`ready\` in PLAN.md frontmatter.`,
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
// Check deps
|
|
134
|
+
const unmetDeps = task.dependsOn.filter((depId) => {
|
|
135
|
+
const dep = state.tasks.find((t) => t.id === depId);
|
|
136
|
+
return !dep || dep.status !== "done";
|
|
137
|
+
});
|
|
138
|
+
if (unmetDeps.length > 0) {
|
|
139
|
+
return {
|
|
140
|
+
handled: true,
|
|
141
|
+
transformedText:
|
|
142
|
+
`soly execute: task ${target.taskId} has unmet dependencies: [${unmetDeps.join(", ")}].\n` +
|
|
143
|
+
`Tasks must be \`status: done\` (have a SUMMARY.md) before their dependents can run.`,
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
const workflow = loadWorkflowMarkdown("execute-task.md");
|
|
147
|
+
if (!workflow) {
|
|
148
|
+
return {
|
|
149
|
+
handled: true,
|
|
150
|
+
transformedText:
|
|
151
|
+
`soly execute: workflow markdown not found: workflows-data/execute-task.md\n` +
|
|
152
|
+
`This is an extension installation issue — reinstall soly.`,
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
const featureDir = path.dirname(path.dirname(task.dir));
|
|
156
|
+
|
|
157
|
+
// Write per-iteration context bundle (B2 of the soly design).
|
|
158
|
+
// Worker reads this file first; no need to chase 6+ .soly/ files.
|
|
159
|
+
const iter = writeIterationContext({
|
|
160
|
+
solyDir: state.solyDir,
|
|
161
|
+
projectRoot,
|
|
162
|
+
kind: "exec",
|
|
163
|
+
taskId: task.id,
|
|
164
|
+
feature: task.feature,
|
|
165
|
+
});
|
|
166
|
+
const inlineSummary = inlinePlanSummary(path.join(task.dir, "PLAN.md"));
|
|
167
|
+
|
|
168
|
+
const instruction = `soly execute ${target.taskId} — launching worker for task.
|
|
169
|
+
|
|
170
|
+
**Task:** ${task.id}
|
|
171
|
+
**Feature:** ${task.feature}
|
|
172
|
+
**Kind:** ${task.kind}
|
|
173
|
+
**Status:** ${task.status}
|
|
174
|
+
**Priority:** ${task.priority}
|
|
175
|
+
**Depends-on:** [${task.dependsOn.join(", ") || "none"}]
|
|
176
|
+
**Parallelizable:** ${task.parallelizable}
|
|
177
|
+
**Dir:** ${task.dir}
|
|
178
|
+
|
|
179
|
+
**Iteration context file written:** \`${iter.relPath}\` (${iter.tokens} tokens, ${iter.bytes} bytes)
|
|
180
|
+
The worker reads this file first — it contains intent, STATE, ROADMAP (n/a for tasks), the feature README, prior task SUMMARYs, and the current task PLAN.
|
|
181
|
+
|
|
182
|
+
**0-POINT CHECK.** Worker must re-read .soly/docs/ (intent) and .soly/features/${task.feature}/README.md before implementing.
|
|
183
|
+
|
|
184
|
+
Launch a single subagent for this work. Do NOT do the work inline.
|
|
185
|
+
|
|
186
|
+
subagent({
|
|
187
|
+
agent: ${JSON.stringify(opts.agent ?? "worker")},
|
|
188
|
+
context: "fresh",
|
|
189
|
+
async: true,
|
|
190
|
+
maxSubagentDepth: 1,
|
|
191
|
+
task: \`You are soly-executor (single-task writer).
|
|
192
|
+
|
|
193
|
+
Your job: execute ONE task (atomic unit) and produce its SUMMARY.md.
|
|
194
|
+
|
|
195
|
+
**FIRST ACTION — read the iteration context file:**
|
|
196
|
+
\`\`\`
|
|
197
|
+
${iter.relPath}
|
|
198
|
+
\`\`\`
|
|
199
|
+
It contains intent, STATE, feature README, prior task SUMMARYs, and the current PLAN. Do NOT skip it. The must-haves below are also inlined so you have them even before reading the file.
|
|
200
|
+
|
|
201
|
+
**Inline plan summary (from PLAN.md frontmatter + Must Haves):**
|
|
202
|
+
${inlineSummary}
|
|
203
|
+
|
|
204
|
+
Project root: ${projectRoot}
|
|
205
|
+
Soly dir: ${state.solyDir}
|
|
206
|
+
Feature dir: ${featureDir}
|
|
207
|
+
Task dir: ${task.dir}
|
|
208
|
+
|
|
209
|
+
**0-POINT CHECK — read .soly/docs/ first.**
|
|
210
|
+
These are the project's INTENT (business context, design vision). Re-read them before implementing. If you find a conflict between intent and PLAN.md, flag it instead of silently choosing one.
|
|
211
|
+
|
|
212
|
+
**Follow the worker self-audit gate (see .soly/rules/process/worker-audit.md):**
|
|
213
|
+
1. Run \`dotnet build\` (or relevant build) — 0 warnings
|
|
214
|
+
2. Cross-check diff against .soly/rules/coding/*
|
|
215
|
+
3. Invoke \`analyzer-coach\` skill for any rule gaps
|
|
216
|
+
4. Loop until clean (max 3 iterations)
|
|
217
|
+
5. Commit (production-code commit(s))
|
|
218
|
+
6. Write SUMMARY.md, commit it
|
|
219
|
+
7. Update PLAN.md frontmatter: \`status: done\`
|
|
220
|
+
|
|
221
|
+
=== WORKFLOW: execute-task.md ===
|
|
222
|
+
${workflow}
|
|
223
|
+
=== END WORKFLOW ===
|
|
224
|
+
|
|
225
|
+
Hard rules:
|
|
226
|
+
- Do not skip the close-out order: production commits -> SUMMARY commit -> status: done.
|
|
227
|
+
- Do not modify any .soly/rules/ files.
|
|
228
|
+
- Do not run subagents yourself.
|
|
229
|
+
- Do not start a task whose \`depends-on:\` lists tasks that are not \`done\`.
|
|
230
|
+
- PATH DISCIPLINE: all files YOU create must live under \`.soly/\` (iteration, handoff, etc.) or under the project's source dirs. Never write to the project root.
|
|
231
|
+
- Return: changed files, commands run with exit codes, validation evidence, surprises, decisions needing parent approval.
|
|
232
|
+
- Interactive-only rules are NOT in scope for you: ${interactiveRules.length > 0 ? interactiveRules.join(", ") : "(none)"}.
|
|
233
|
+
\`
|
|
234
|
+
})
|
|
235
|
+
|
|
236
|
+
When the subagent completes, synthesize the result. Do not re-execute its work.`;
|
|
237
|
+
return { handled: true, transformedText: instruction };
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// === ALL / FEATURE (new dual-mode, sequential in v0.1) ===
|
|
241
|
+
if (target.kind === "all" || target.kind === "feature") {
|
|
242
|
+
const allTasks =
|
|
243
|
+
target.kind === "all"
|
|
244
|
+
? state.tasks
|
|
245
|
+
: state.tasks.filter((t) => t.feature === target.feature);
|
|
246
|
+
if (allTasks.length === 0) {
|
|
247
|
+
return {
|
|
248
|
+
handled: true,
|
|
249
|
+
transformedText:
|
|
250
|
+
target.kind === "all"
|
|
251
|
+
? `soly execute --all: no tasks found in .soly/features/*/tasks/.`
|
|
252
|
+
: `soly execute --feature ${target.feature}: no tasks found for that feature.`,
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
const ready = allTasks.filter((t) => t.status === "ready");
|
|
256
|
+
const blocked = allTasks.filter((t) => t.status === "blocked");
|
|
257
|
+
const done = allTasks.filter((t) => t.status === "done");
|
|
258
|
+
if (ready.length === 0) {
|
|
259
|
+
return {
|
|
260
|
+
handled: true,
|
|
261
|
+
transformedText:
|
|
262
|
+
`soly execute: no ready tasks in scope.\n` +
|
|
263
|
+
`Tasks: ${allTasks.length} total, ${done.length} done, ${blocked.length} blocked.`,
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
return {
|
|
267
|
+
handled: true,
|
|
268
|
+
transformedText:
|
|
269
|
+
`soly execute ${target.kind === "all" ? "--all" : `--feature ${target.feature}`}: ${ready.length} task(s) ready.\n\n` +
|
|
270
|
+
`**v0.1 limitation:** tasks run sequentially, not in parallel. Parallel mode is v0.2.\n\n` +
|
|
271
|
+
`Ready tasks (in suggested order):\n` +
|
|
272
|
+
ready.map((t, i) => ` ${i + 1}. ${t.id} [${t.kind}] prio=${t.priority}`).join("\n") +
|
|
273
|
+
`\n\nLaunch a single subagent to execute them one at a time in this order. The subagent uses the task execution workflow (execute-task.md) per task.`,
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// === PHASE MODE ===
|
|
278
|
+
const phase = state.phases.find((p) => p.number === target.phase);
|
|
279
|
+
if (!phase) {
|
|
280
|
+
return {
|
|
281
|
+
handled: true,
|
|
282
|
+
transformedText:
|
|
283
|
+
`soly execute: phase ${target.phase} not found in .soly/phases/.\n` +
|
|
284
|
+
`Known phases: ${state.phases.map((p) => p.number).join(", ") || "(none)"}`,
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
const isPlanLevel = target.plan != null;
|
|
289
|
+
const workflowName = isPlanLevel ? "execute-plan.md" : "execute-phase.md";
|
|
290
|
+
const workflow = loadWorkflowMarkdown(workflowName);
|
|
291
|
+
if (!workflow) {
|
|
292
|
+
return {
|
|
293
|
+
handled: true,
|
|
294
|
+
transformedText:
|
|
295
|
+
`soly execute: workflow markdown not found: workflows-data/${workflowName}\n` +
|
|
296
|
+
`This is an extension installation issue — reinstall soly.`,
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// Write per-iteration context bundle (B2 of the soly design).
|
|
301
|
+
// For plan-level execution: bundle includes the specific PLAN.md.
|
|
302
|
+
// For phase-level execution: bundle includes all plan frontmatter summaries
|
|
303
|
+
// (the phase workflow iterates waves on its own).
|
|
304
|
+
const iter = writeIterationContext({
|
|
305
|
+
solyDir: state.solyDir,
|
|
306
|
+
projectRoot,
|
|
307
|
+
kind: "exec",
|
|
308
|
+
phaseNumber: target.phase,
|
|
309
|
+
planNumber: isPlanLevel ? (target.plan ?? undefined) : undefined,
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
// For plan-level: also pull out the must_haves summary for inline cache.
|
|
313
|
+
const planFile = isPlanLevel
|
|
314
|
+
? path.join(phase.dir, `${phase.slug}-${String(target.plan).padStart(2, "0")}-${phase.slug.split("-").slice(1).join("-") || "plan"}-PLAN.md`)
|
|
315
|
+
: null;
|
|
316
|
+
// Fall back to findPlanFile if the conventional name didn't match.
|
|
317
|
+
let planFileResolved = planFile;
|
|
318
|
+
if (isPlanLevel && planFile && !fs.existsSync(planFile)) {
|
|
319
|
+
const padded = String(target.plan).padStart(2, "0");
|
|
320
|
+
const candidates = fs.readdirSync(phase.dir).filter((f) =>
|
|
321
|
+
new RegExp(`^\\d+-${padded}-.+-PLAN\\.md$`).test(f),
|
|
322
|
+
);
|
|
323
|
+
if (candidates.length > 0) planFileResolved = path.join(phase.dir, candidates[0]!);
|
|
324
|
+
}
|
|
325
|
+
const inlineSummary = isPlanLevel ? inlinePlanSummary(planFileResolved) : "_(phase-level exec — iterate all PLAN.md files; each iteration has its own bundle)_";
|
|
326
|
+
|
|
327
|
+
// Build the LLM instruction. Keep it terse at the top, then dump the
|
|
328
|
+
// workflow markdown verbatim so the LLM has full context.
|
|
329
|
+
const targetDesc = isPlanLevel
|
|
330
|
+
? `phase ${target.phase} plan ${String(target.plan).padStart(2, "0")}`
|
|
331
|
+
: `phase ${target.phase} (${phase.name}) — all ${phase.planCount} plan(s)`;
|
|
332
|
+
|
|
333
|
+
const scopeBlock = isPlanLevel
|
|
334
|
+
? `Target: ONE plan = ${targetDesc}.
|
|
335
|
+
The iteration context file lists the specific plan at section 6.`
|
|
336
|
+
: `Target: ${targetDesc}.
|
|
337
|
+
The iteration context file lists all plans (their frontmatter) in section 6, grouped by wave.`;
|
|
338
|
+
|
|
339
|
+
const childRole = isPlanLevel
|
|
340
|
+
? `soly-executor (single-plan writer)`
|
|
341
|
+
: `soly-executor (wave-based parallel phase executor)`;
|
|
342
|
+
|
|
343
|
+
const instruction = `soly execute ${target.raw} — launching worker for ${targetDesc}.
|
|
344
|
+
|
|
345
|
+
**Iteration context file written:** \`${iter.relPath}\` (${iter.tokens} tokens, ${iter.bytes} bytes)
|
|
346
|
+
The worker reads this file first — it contains intent, STATE, ROADMAP row for this phase, phase CONTEXT, phase RESEARCH, prior SUMMARYs, ${isPlanLevel ? "and the current PLAN" : "and all PLAN frontmatter summaries"}, and (for exec) the Critical Anti-Patterns from .continue-here.md.
|
|
347
|
+
|
|
348
|
+
**0-POINT CHECK — worker must read .soly/docs/ first.**
|
|
349
|
+
These are the project's INTENT docs. The worker is about to implement tasks; if the implementation diverges from intent, it will be wrong even if the tests pass. Have the worker re-read .soly/docs/ (and any intent docs linked from PLAN.md) before each plan.
|
|
350
|
+
|
|
351
|
+
${scopeBlock}
|
|
352
|
+
|
|
353
|
+
Launch a single subagent for this work. Do NOT do the work inline.
|
|
354
|
+
|
|
355
|
+
subagent({
|
|
356
|
+
agent: ${JSON.stringify(opts.agent ?? "worker")},
|
|
357
|
+
context: "fresh",
|
|
358
|
+
async: true,
|
|
359
|
+
maxSubagentDepth: 1, // worker must not spawn sub-sub-agents
|
|
360
|
+
task: \`You are ${childRole}.
|
|
361
|
+
|
|
362
|
+
Your job: ${isPlanLevel ? "execute ONE plan and produce its SUMMARY.md" : "execute ALL plans in this phase using wave-based parallel execution"}.
|
|
363
|
+
|
|
364
|
+
**FIRST ACTION — read the iteration context file:**
|
|
365
|
+
\`\`\`
|
|
366
|
+
${iter.relPath}
|
|
367
|
+
\`\`\`
|
|
368
|
+
It contains intent, STATE, ROADMAP, phase CONTEXT, phase RESEARCH, prior SUMMARYs, ${isPlanLevel ? "and the current PLAN" : "and the wave-grouped plan index"}, and (for exec) the Critical Anti-Patterns.
|
|
369
|
+
|
|
370
|
+
${isPlanLevel
|
|
371
|
+
? `**Inline plan summary (so you have must-haves even before reading the file):**
|
|
372
|
+
${inlineSummary}`
|
|
373
|
+
: `**Note for phase-level exec:** for each plan you execute, you may write a new bundle via the extension (the parent will regenerate; you do not need to). Or, simpler: read the plan file directly from the source path listed in section 6. The must-haves of the current plan are in section 6 too.`}
|
|
374
|
+
|
|
375
|
+
Project root: ${projectRoot}
|
|
376
|
+
Soly dir: ${state.solyDir}
|
|
377
|
+
Phase dir: ${phase.dir}
|
|
378
|
+
|
|
379
|
+
**0-POINT CHECK — read .soly/docs/ first.**
|
|
380
|
+
These are the project's INTENT (business context, design vision). Re-read them before implementing each plan. If you find a conflict between intent and PLAN.md, flag it instead of silently choosing one.
|
|
381
|
+
|
|
382
|
+
Follow the workflow below VERBATIM — these are the user-approved soly instructions, not suggestions.
|
|
383
|
+
|
|
384
|
+
=== WORKFLOW: ${workflowName} ===
|
|
385
|
+
${workflow}
|
|
386
|
+
=== END WORKFLOW ===
|
|
387
|
+
|
|
388
|
+
Hard rules:
|
|
389
|
+
- Do not skip the close-out order: production commits -> SUMMARY commit -> STATE/ROADMAP update.
|
|
390
|
+
- Do not modify any .soly/rules/ files.
|
|
391
|
+
- Do not run subagents yourself.
|
|
392
|
+
- PATH DISCIPLINE: all files YOU create must live under \`.soly/\` (e.g. .soly/iterations/, .soly/phases/<slug>/, .soly/HANDOFF.json) or under the project's source dirs. Never write PLAN/SUMMARY/CONTEXT/RESEARCH/iteration files to the project root.
|
|
393
|
+
- Return: changed files, commands run with exit codes, validation evidence, surprises, and any decisions needing parent approval.
|
|
394
|
+
- Interactive-only rules are NOT in scope for you: ${interactiveRules.length > 0 ? interactiveRules.join(", ") : "(none)"}. They describe how the user-facing conversation should go, not how to execute work.
|
|
395
|
+
\`
|
|
396
|
+
})
|
|
397
|
+
|
|
398
|
+
When the subagent completes, synthesize the result and confirm STATE.md was updated. Do not re-execute its work.`;
|
|
399
|
+
|
|
400
|
+
return { handled: true, transformedText: instruction };
|
|
401
|
+
}
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
// =============================================================================
|
|
2
|
+
// workflows/index.ts — Single `input` event hook for all soly workflow verbs
|
|
3
|
+
// =============================================================================
|
|
4
|
+
//
|
|
5
|
+
// Intercepts plain-text "soly <verb> ..." user input (NOT /soly slash commands
|
|
6
|
+
// — those still go through pi.registerCommand in commands.ts).
|
|
7
|
+
//
|
|
8
|
+
// For each verb, the handler either:
|
|
9
|
+
// - transforms the input into a detailed LLM instruction (execute / pause /
|
|
10
|
+
// compact / resume / plan / discuss) — LLM drives the heavy lifting
|
|
11
|
+
// - shows a direct response (status / log / diff) — extension computes
|
|
12
|
+
// immediately, no LLM round-trip needed
|
|
13
|
+
//
|
|
14
|
+
// "Direct response" verbs (status/log/diff) return action: "handled" — the
|
|
15
|
+
// LLM never sees them, and the user gets an immediate UI notification.
|
|
16
|
+
// =============================================================================
|
|
17
|
+
|
|
18
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
19
|
+
import { parseSolyCommand, type SolyCommand } from "./parser.js";
|
|
20
|
+
import { buildExecuteTransform } from "./execute.js";
|
|
21
|
+
import { buildPauseTransform } from "./pause.js";
|
|
22
|
+
import { buildResumeTransform } from "./resume.js";
|
|
23
|
+
import { showStatus, showLog, showDiff } from "./quick.js";
|
|
24
|
+
import { showDoctor, showIterations, showDiffIterations, showPhaseDelete, showTodos } from "./inspect.js";
|
|
25
|
+
import { buildPlanTransform, buildDiscussTransform } from "./planning.js";
|
|
26
|
+
import type { SolyState } from "../core.js";
|
|
27
|
+
import type { SolyConfig } from "../config.js";
|
|
28
|
+
|
|
29
|
+
export interface WorkflowsDeps {
|
|
30
|
+
getState: () => SolyState;
|
|
31
|
+
/** List of rule relPaths marked `interactive: true` — passed to subagent
|
|
32
|
+
* workers so they know which rules are explicitly out of scope. */
|
|
33
|
+
getInteractiveRules: () => string[];
|
|
34
|
+
/** List of active tool names. Used to detect optional cross-extension
|
|
35
|
+
* dependencies (e.g. `ask_pro` from the separate `pi-ask` extension). */
|
|
36
|
+
getActiveTools: () => string[];
|
|
37
|
+
/** Current merged config (per-project + global + defaults). */
|
|
38
|
+
getConfig: () => SolyConfig;
|
|
39
|
+
/** Fired when a recognized soly verb is parsed (handled OR transformed).
|
|
40
|
+
* Used by the parent extension to reset drift counters etc. */
|
|
41
|
+
onWorkflowUsed?: () => void;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function registerWorkflows(pi: ExtensionAPI, deps: WorkflowsDeps): void {
|
|
45
|
+
const { getState, getInteractiveRules, getActiveTools, getConfig, onWorkflowUsed } = deps;
|
|
46
|
+
// The current agent is owned by the separate `pi-switch` extension.
|
|
47
|
+
// It writes `globalThis.__PI_SWITCH_AGENT__` (in-process) and
|
|
48
|
+
// `.soly/agent` (persisted). We read the in-process value first (fresh);
|
|
49
|
+
// fall back to "worker" if pi-switch isn't installed.
|
|
50
|
+
const getCurrentAgent = (): string => {
|
|
51
|
+
return (globalThis as { __PI_SWITCH_AGENT__?: string }).__PI_SWITCH_AGENT__ ?? "worker";
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
// Track whether we need to fire ctx.compact() at the end of the upcoming
|
|
55
|
+
// turn. Reset on every user input — only set if the user types
|
|
56
|
+
// "soly compact" (which expands to a handoff + compact request).
|
|
57
|
+
let pendingCompact = false;
|
|
58
|
+
|
|
59
|
+
pi.on("input", async (event, ctx) => {
|
|
60
|
+
// Only handle plain interactive text. Skip:
|
|
61
|
+
// - extension-injected messages (recursion guard)
|
|
62
|
+
// - RPC / programmatic sources (we want explicit user intent)
|
|
63
|
+
// - slash commands ("/soly ...") — those go through pi's command
|
|
64
|
+
// handler in commands.ts, not here
|
|
65
|
+
if (event.source !== "interactive") return;
|
|
66
|
+
if (event.text.trim().startsWith("/")) return;
|
|
67
|
+
|
|
68
|
+
const cmd = parseSolyCommand(event.text);
|
|
69
|
+
if (!cmd) return;
|
|
70
|
+
|
|
71
|
+
// Notify the parent extension that a soly verb was used
|
|
72
|
+
// (resets the drift counter, etc.). Fires for BOTH "handled"
|
|
73
|
+
// and "transform" actions — both are real workflow usage.
|
|
74
|
+
onWorkflowUsed?.();
|
|
75
|
+
|
|
76
|
+
const state = getState();
|
|
77
|
+
|
|
78
|
+
// ----- LLM-driven transforms -----
|
|
79
|
+
|
|
80
|
+
if (cmd.verb === "execute") {
|
|
81
|
+
const result = buildExecuteTransform(cmd, state, getInteractiveRules(), {
|
|
82
|
+
useSolyWorker: getConfig().agent.useSolyWorkerSubagents,
|
|
83
|
+
});
|
|
84
|
+
if (!result.handled || !result.transformedText) return;
|
|
85
|
+
return { action: "transform", text: result.transformedText };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (cmd.verb === "pause" || cmd.verb === "compact") {
|
|
89
|
+
const result = buildPauseTransform(cmd, state);
|
|
90
|
+
if (!result.handled || !result.transformedText) return;
|
|
91
|
+
if (result.triggerCompact) pendingCompact = true;
|
|
92
|
+
return { action: "transform", text: result.transformedText };
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (cmd.verb === "resume") {
|
|
96
|
+
const result = buildResumeTransform(cmd, state);
|
|
97
|
+
if (!result.handled || !result.transformedText) return;
|
|
98
|
+
return { action: "transform", text: result.transformedText };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (cmd.verb === "plan") {
|
|
102
|
+
const result = buildPlanTransform(cmd, state);
|
|
103
|
+
if (!result.handled || !result.transformedText) return;
|
|
104
|
+
return { action: "transform", text: result.transformedText };
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (cmd.verb === "discuss") {
|
|
108
|
+
const hasAskPro = getActiveTools().includes("ask_pro");
|
|
109
|
+
const result = buildDiscussTransform(cmd, state, { hasAskPro });
|
|
110
|
+
if (!result.handled || !result.transformedText) return;
|
|
111
|
+
return { action: "transform", text: result.transformedText };
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (cmd.verb === "help") {
|
|
115
|
+
return {
|
|
116
|
+
action: "transform",
|
|
117
|
+
text: `soly subcommand picker (esc to cancel).
|
|
118
|
+
|
|
119
|
+
Available verbs (all start with \`soly <verb>\` or use \`/soly <verb>\` in slash form):
|
|
120
|
+
|
|
121
|
+
position — one-screen current position summary
|
|
122
|
+
state — full STATE.md body
|
|
123
|
+
plan — current PLAN.md body
|
|
124
|
+
context — current phase CONTEXT.md
|
|
125
|
+
research — current phase RESEARCH.md
|
|
126
|
+
roadmap — ROADMAP.md body
|
|
127
|
+
progress — progress bar + counts
|
|
128
|
+
phases — list all phases with C/R markers
|
|
129
|
+
tasks — list all tasks grouped by feature (new in v2)
|
|
130
|
+
task <id> — show one task's PLAN + SUMMARY
|
|
131
|
+
features — list all features (new in v2)
|
|
132
|
+
milestone — show the active milestone document
|
|
133
|
+
log [N] — last N (default 20) decisions from STATE.md
|
|
134
|
+
diff — git status + uncommitted .soly/ changes
|
|
135
|
+
doctor — health check: missing files, broken refs, stale iterations
|
|
136
|
+
iterations [N] — list recent iteration files
|
|
137
|
+
todos — show pi-todo live list (.soly/todos.json or .pi-todos.json)
|
|
138
|
+
phase delete <N> — soft-delete a phase
|
|
139
|
+
reload — re-read project state from disk
|
|
140
|
+
plan <N> — produce PLAN.md for phase N
|
|
141
|
+
discuss <N> — interactive discussion of phase N
|
|
142
|
+
execute <N> — execute all plans in phase N (or \`execute N.MM\` for one plan)
|
|
143
|
+
pause — write HANDOFF.json + .continue-here.md
|
|
144
|
+
compact — pause + auto-compact session
|
|
145
|
+
resume [N] — restore from handoff (scoped to phase N if given)
|
|
146
|
+
help — this picker
|
|
147
|
+
|
|
148
|
+
Unknown / missing verb? Use \`/soly\` (slash) for the picker.`,
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// ----- Direct responses (no LLM round-trip) -----
|
|
153
|
+
|
|
154
|
+
if (cmd.verb === "status") {
|
|
155
|
+
showStatus(cmd, state, ctx.ui, getConfig());
|
|
156
|
+
return { action: "handled" };
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (cmd.verb === "log") {
|
|
160
|
+
showLog(cmd, state, ctx.ui);
|
|
161
|
+
return { action: "handled" };
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (cmd.verb === "diff") {
|
|
165
|
+
// Subverb: "soly diff iterations <a> <b>" — compare two iteration files
|
|
166
|
+
if (cmd.args[0] === "iterations" || cmd.args[0] === "iter") {
|
|
167
|
+
showDiffIterations(
|
|
168
|
+
{ verb: "diff", args: cmd.args.slice(1), raw: cmd.raw },
|
|
169
|
+
state,
|
|
170
|
+
ctx.ui,
|
|
171
|
+
);
|
|
172
|
+
} else {
|
|
173
|
+
await showDiff(cmd, state, ctx.ui);
|
|
174
|
+
}
|
|
175
|
+
return { action: "handled" };
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
if (cmd.verb === "doctor") {
|
|
179
|
+
showDoctor(cmd, state, ctx.ui, getConfig(), getActiveTools());
|
|
180
|
+
return { action: "handled" };
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if (cmd.verb === "iterations") {
|
|
184
|
+
showIterations(cmd, state, ctx.ui);
|
|
185
|
+
return { action: "handled" };
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
if (cmd.verb === "todos") {
|
|
189
|
+
showTodos(cmd, state, ctx.ui);
|
|
190
|
+
return { action: "handled" };
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
if (cmd.verb === "phase") {
|
|
194
|
+
// "soly phase delete <N>" — soft delete
|
|
195
|
+
if (cmd.args[0] === "delete" || cmd.args[0] === "rm") {
|
|
196
|
+
showPhaseDelete(
|
|
197
|
+
{ verb: "phase", args: cmd.args.slice(1), raw: cmd.raw },
|
|
198
|
+
state,
|
|
199
|
+
ctx.ui,
|
|
200
|
+
);
|
|
201
|
+
} else {
|
|
202
|
+
return {
|
|
203
|
+
action: "transform",
|
|
204
|
+
text: `soly phase — usage:
|
|
205
|
+
soly phase delete <N> — soft-delete phase N (move to .soly/phases/.trash/)
|
|
206
|
+
soly phase list — list all phases (same as /soly phases)
|
|
207
|
+
soly phase <N> — alias for "soly plan <N>" (route through planner)`,
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
return { action: "handled" };
|
|
211
|
+
}
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
// After the LLM finishes a turn that was triggered by "soly compact",
|
|
215
|
+
// fire ctx.compact() to actually compress the session.
|
|
216
|
+
pi.on("agent_end", async (_event, ctx) => {
|
|
217
|
+
if (!pendingCompact) return;
|
|
218
|
+
pendingCompact = false;
|
|
219
|
+
ctx.compact({
|
|
220
|
+
customInstructions:
|
|
221
|
+
"Session was paused via `soly compact`. Handoff files are in .soly/HANDOFF.json " +
|
|
222
|
+
"and .soly/.continue-here.md. Preserve milestone/phase/plan position and key " +
|
|
223
|
+
"decisions in the summary. Drop implementation-detail noise.",
|
|
224
|
+
onComplete: () => {
|
|
225
|
+
ctx.ui.notify("soly: session compacted. Use `soly resume` to pick up.", "info");
|
|
226
|
+
},
|
|
227
|
+
onError: (err) => {
|
|
228
|
+
ctx.ui.notify(`soly: compact failed — ${err.message}`, "error");
|
|
229
|
+
},
|
|
230
|
+
});
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/** Re-export for callers that want to inspect the parsed command. */
|
|
235
|
+
export type { SolyCommand };
|