pi-soly 1.16.4 → 2.1.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 CHANGED
@@ -27,7 +27,7 @@ pi install npm:pi-soly
27
27
  Restart pi (`/reload`), and you have:
28
28
 
29
29
  - **Project management** — plans, state, phases, decisions
30
- - **Workflow engine** — plain-text verbs: `soly discuss` · `plan` · `execute` · `verify` · `pause`/`resume`
30
+ - **Workflow engine** — runs inline (no subagent plugin). The model proposes the next step and drives it via the `soly_workflow` tool on your plain-language intent; the verbs `soly discuss` · `plan` · `execute` · `verify` · `pause`/`resume` still work as text
31
31
  - **Self-review loop** — `soly verify` re-reviews the work until "No issues found."
32
32
  - **Visual chrome** — native footer, equalizer working spinner with live telemetry, gradient welcome banner
33
33
  - **Rules & docs modal** — `/rules` and `/docs` open a fuzzy list + preview panel (no chat dumps)
@@ -37,7 +37,7 @@ Restart pi (`/reload`), and you have:
37
37
  - **HTML artifacts** — `html_artifact` tool serves self-contained HTML from a per-session browser gallery (live-updating, one stable URL)
38
38
  - **Skill-based execution** — LLM reads the `soly-framework` skill on demand
39
39
 
40
- The LLM drives execution; `plan`/`execute` delegate to a `worker` subagent when one is available (via pi-subagents), with first-party delegation on the roadmap. You focus on the work.
40
+ The LLM drives execution **inline, in the main session** — no external subagent plugin. Say what you want in plain language ("let's plan this", "start executing") and the model calls the first-party `soly_workflow` tool for you; the `soly <verb>` text form still works as a fallback. You focus on the work.
41
41
 
42
42
  ### Known install issue (upstream `pi install`)
43
43
 
@@ -82,7 +82,7 @@ Tracked upstream — fix is expected on the pi side, not here.
82
82
  soly new feat/auth-jwt # create branch + .agents/plans/<name>/ + stub PLAN.md
83
83
  soly discuss feat/auth-jwt # interactive discussion of the plan
84
84
  soly plan feat/auth-jwt # flesh out PLAN.md via ask_pro
85
- soly execute feat/auth-jwt # execute the plan in a subagent
85
+ soly execute feat/auth-jwt # execute the plan inline in this session
86
86
  soly done feat/auth-jwt # commit, push, open draft PR via gh
87
87
  soly verify # self-review loop until "No issues found." (soly verify stop to exit)
88
88
  soly pause # save a handoff; soly resume to pick it back up
package/config.ts CHANGED
@@ -49,13 +49,6 @@ export interface SolyConfig {
49
49
  * "off" — no gate.
50
50
  * Booleans accepted for back-compat: true = "scope", false = "off". */
51
51
  confirmBeforeCode: boolean | "off" | "ask" | "scope";
52
- /** Inject a compact list of rules applicable to the file being
53
- * edited/written, appended to the tool's result. Forces the LLM to
54
- * confirm in the next message which rules were applied — closes the
55
- * "rules read at start of turn, forgotten by edit time" gap.
56
- * Default on. Disable for verbose rule sets that would otherwise
57
- * flood the per-edit context. */
58
- preActionRuleReminder: boolean;
59
52
  };
60
53
  display: {
61
54
  /** Always show the recommended (⭐) option as the first row. */
@@ -148,7 +141,6 @@ export const DEFAULT_CONFIG: SolyConfig = {
148
141
  nudgeNotify: false,
149
142
  toolHints: true,
150
143
  confirmBeforeCode: "scope",
151
- preActionRuleReminder: true,
152
144
  },
153
145
  display: {
154
146
  defaultRecommendedFirst: true,
@@ -248,8 +240,6 @@ function deepMerge(base: SolyConfig, over: RawConfig): SolyConfig {
248
240
  over.agent.confirmBeforeCode === "scope"
249
241
  )
250
242
  merged.agent.confirmBeforeCode = over.agent.confirmBeforeCode;
251
- if (typeof over.agent.preActionRuleReminder === "boolean")
252
- merged.agent.preActionRuleReminder = over.agent.preActionRuleReminder;
253
243
  }
254
244
  if (over.display) {
255
245
  if (typeof over.display.defaultRecommendedFirst === "boolean")
package/core.ts CHANGED
@@ -571,68 +571,6 @@ ${blocks.join("\n\n---\n\n")}${skippedNote}
571
571
  return { section, loaded: applicable.map(ruleKey), interactive };
572
572
  }
573
573
 
574
- // ============================================================================
575
- // Per-file rule lookup (used by pre-action reminder in tool_result hook)
576
- // ============================================================================
577
- //
578
- // Returns the subset of `rules` that apply to a single file path. Mirrors the
579
- // glob logic in `buildRulesSection` (always/no-globs = universal; otherwise
580
- // match `filePath` against `meta.globs`). Excludes disabled rules. Use this
581
- // to feed the pre-action reminder — LLM should see applicable rules AT THE
582
- // MOMENT of edit, not just at the start of the turn (where they decay).
583
- export function getApplicableRulesForFile(
584
- filePath: string,
585
- rules: RuleFile[],
586
- ): RuleFile[] {
587
- return rules.filter((r) => {
588
- if (!r.enabled) return false;
589
- if (r.meta.always === true) return true;
590
- const globs = r.meta.globs;
591
- if (!globs || globs.length === 0) return true;
592
- return globs.some((g) => matchesGlob(filePath, g));
593
- });
594
- }
595
-
596
- // Formats a compact reminder block for the LLM to see right after editing
597
- // `filePath`. Sorted by priority (high → medium → low → undefined), capped
598
- // at `cap` rules (default 3) so the LLM isn't flooded. Returns "" when no
599
- // rules apply — caller should treat that as "don't inject anything".
600
- export function formatRuleReminder(
601
- rules: RuleFile[],
602
- filePath: string,
603
- cap = 3,
604
- ): string {
605
- if (rules.length === 0) return "";
606
-
607
- const priorityOrder: Record<string, number> = { high: 0, medium: 1, low: 2 };
608
- const sorted = [...rules].sort((a, b) => {
609
- const pa = priorityOrder[a.meta.priority ?? ""] ?? 1;
610
- const pb = priorityOrder[b.meta.priority ?? ""] ?? 1;
611
- if (pa !== pb) return pa - pb;
612
- return a.relPath.localeCompare(b.relPath);
613
- });
614
-
615
- const shown = sorted.slice(0, cap);
616
- const remaining = rules.length - shown.length;
617
-
618
- const lines: string[] = [];
619
- lines.push(`\n\n---\n\n📋 **Applicable rules for \`${filePath}\`:**\n`);
620
- shown.forEach((r, i) => {
621
- const desc = r.meta.description ? ` — ${r.meta.description}` : "";
622
- lines.push(`${i + 1}. [\`${r.relPath}\`]${desc}`);
623
- });
624
- if (remaining > 0) {
625
- lines.push(
626
- `\n_…and ${remaining} more — use \`/why ${filePath}\` to see all._`,
627
- );
628
- }
629
- lines.push(
630
- `\nIn your next message, briefly confirm which were applied (or note why not).`,
631
- );
632
-
633
- return lines.join("\n");
634
- }
635
-
636
574
  // ============================================================================
637
575
  // Phase-scoped rules loader
638
576
  // ============================================================================
package/index.ts CHANGED
@@ -32,9 +32,7 @@ import {
32
32
  CONTEXT_WINDOW_TOKENS,
33
33
  DEFAULT_PROGRESS,
34
34
  extractFilePathsFromPrompt,
35
- formatRuleReminder,
36
35
  formatTok,
37
- getApplicableRulesForFile,
38
36
  loadAllRules,
39
37
  loadPhaseRules,
40
38
  loadProjectState,
@@ -57,7 +55,8 @@ import {
57
55
  pruneOldIterations,
58
56
  type SolyConfig,
59
57
  } from "./config.ts";
60
- import { classifyTaskHeuristics, buildNudgeSection } from "./nudge.ts";
58
+ import { classifyTaskHeuristics, buildNudgeSection, buildSuggestionSection, type WorkflowSituation } from "./nudge.ts";
59
+ import { registerWorkflowTool } from "./workflows/llm-tools.ts";
61
60
  import { detectToolHints, buildToolHintSection } from "./tool-hints.ts";
62
61
  import { notifyNudge, notifyDeprecation } from "./notification.ts";
63
62
  import { registerCommands, type CommandUI } from "./commands.ts";
@@ -403,6 +402,24 @@ export default function solyExtension(pi: ExtensionAPI) {
403
402
  },
404
403
  });
405
404
 
405
+ // First-party LLM tool that drives the same workflow verbs — lets the model
406
+ // start plan/execute/etc. itself on the user's natural-language intent,
407
+ // with no external subagent plugin. Reuses the workflow builders.
408
+ registerWorkflowTool(pi, {
409
+ getState: () => state,
410
+ getInteractiveRules: () =>
411
+ combinedRules()
412
+ .filter((r) => r.interactiveOnly)
413
+ .map((r) => r.relPath),
414
+ getActiveTools: () => pi.getActiveTools(),
415
+ getConfig: getActiveConfig,
416
+ onWorkflowUsed: resetSolyDrift,
417
+ setVerbLabel: (verb) => {
418
+ chrome.data.verbLabel = verb;
419
+ chrome.poke();
420
+ },
421
+ });
422
+
406
423
  // ============================================================================
407
424
  // Events
408
425
  // ============================================================================
@@ -612,6 +629,44 @@ export default function solyExtension(pi: ExtensionAPI) {
612
629
  persistRuleMtimes();
613
630
  });
614
631
 
632
+ // Snapshot where the user is in the plan-branch workflow, for the proactive
633
+ // "suggested next step" section. Reads git branch (cached) + the plan dir on
634
+ // disk; the section builder itself is pure (nudge.ts).
635
+ const computeWorkflowSituation = (): WorkflowSituation => {
636
+ const branch = gitContext.branch;
637
+ const onPlanBranch =
638
+ !!branch && branch !== "master" && branch !== "main" && !branch.startsWith("HEAD");
639
+ const dirSlug = branch ? branch.replace(/\//g, "-") : null;
640
+ let planExists = false;
641
+ let planIsStub = false;
642
+ if (onPlanBranch && dirSlug && state.exists) {
643
+ const planFile = path.join(state.solyDir, "plans", dirSlug, "PLAN.md");
644
+ try {
645
+ const body = fs.readFileSync(planFile, "utf-8");
646
+ planExists = true;
647
+ planIsStub = body.includes("_Stub — fill in via");
648
+ } catch {
649
+ planExists = false;
650
+ }
651
+ }
652
+ const doneIds = new Set(state.tasks.filter((t) => t.status === "done").map((t) => t.id));
653
+ const readyTaskIds = state.tasks
654
+ .filter((t) => t.status === "ready" && t.dependsOn.every((d) => doneIds.has(d)))
655
+ .map((t) => t.id);
656
+ return {
657
+ hasProject: state.exists,
658
+ branch,
659
+ onPlanBranch,
660
+ // Target the branch name verbatim — the workflow builders accept
661
+ // `<prefix>/<slug>` and `<slug>` alike.
662
+ planSlug: onPlanBranch ? branch : null,
663
+ planExists,
664
+ planIsStub,
665
+ dirty: !!gitContext.statusShort && gitContext.statusShort.trim().length > 0,
666
+ readyTaskIds,
667
+ };
668
+ };
669
+
615
670
  pi.on("before_agent_start", async (event, ctx) => {
616
671
  // Keep the chrome (ctx%, model, phase) current for the upcoming turn.
617
672
  updateChromeData(ctx);
@@ -698,6 +753,14 @@ export default function solyExtension(pi: ExtensionAPI) {
698
753
  }),
699
754
  );
700
755
 
756
+ // 7.05 Proactive "suggested next step" — always on when a project exists.
757
+ // Lets the model OFFER the next workflow action and call `soly_workflow`
758
+ // itself, so the user never has to remember a verb.
759
+ if (state.exists) {
760
+ const suggestion = buildSuggestionSection(computeWorkflowSituation());
761
+ if (suggestion) sections.push(suggestion);
762
+ }
763
+
701
764
  // 7.1 Interactive-tool affordance hints (examples → html_artifact, options
702
765
  // → decision_deck, …) — only on turns whose wording mentions them.
703
766
  if (getActiveConfig().agent.toolHints) {
@@ -836,29 +899,6 @@ export default function solyExtension(pi: ExtensionAPI) {
836
899
  }
837
900
  });
838
901
 
839
- // ============================================================================
840
- // tool_result: inject a compact reminder of rules applicable to the file
841
- // just edited/written, so the LLM sees them AT THE MOMENT of action (not
842
- // 30 turns later when they've decayed in attention). The LLM is asked to
843
- // confirm in its next message which rules were applied — closing the gap
844
- // where rules loaded at the start of a turn are forgotten by the time the
845
- // agent actually edits something.
846
- // ============================================================================
847
- pi.on("tool_result", async (event, _ctx) => {
848
- if (!getActiveConfig().agent.preActionRuleReminder) return;
849
- if (event.toolName !== "edit" && event.toolName !== "write") return;
850
- if (event.isError) return; // tool failed — don't pile on
851
- const input = event.input as { path?: string };
852
- const filePath = input?.path;
853
- if (!filePath) return;
854
- const applicable = getApplicableRulesForFile(filePath, combinedRules());
855
- const reminder = formatRuleReminder(applicable, filePath);
856
- if (!reminder) return;
857
- return {
858
- content: [...event.content, { type: "text" as const, text: reminder }],
859
- };
860
- });
861
-
862
902
  // ============================================================================
863
903
  // Chrome: working-indicator telemetry lifecycle + reactive data refresh
864
904
  // ============================================================================
package/nudge.ts CHANGED
@@ -3,7 +3,8 @@
3
3
  // =============================================================================
4
4
  //
5
5
  // Goal: gently push the agent toward "ask before acting on non-trivial
6
- // tasks" and "use background subagents with fresh context for research".
6
+ // tasks", "scout with soly's own read tools", and "route project work through
7
+ // the soly workflow — proactively, without making the user memorize verbs".
7
8
  //
8
9
  // Implementation is prompt-only (no UI blocking) — the model reads the nudge
9
10
  // in its system prompt and follows it. Heuristics on the user prompt tell the
@@ -12,6 +13,11 @@
12
13
  //
13
14
  // The input event also surfaces a soft UI notify to the human, so they know
14
15
  // the model was told to pause and ask — but it never blocks input.
16
+ //
17
+ // `buildSuggestionSection` is a separate, always-on (when a project exists)
18
+ // block that surfaces soly's computed "suggested next step" so the model can
19
+ // proactively offer it and call the `soly_workflow` tool on the user's
20
+ // confirmation — no external subagent plugin involved.
15
21
  // =============================================================================
16
22
 
17
23
  // Words that suggest the user is asking for non-trivial changes.
@@ -157,17 +163,19 @@ export function buildNudgeSection(
157
163
  opts.hasProject && heuristics.nonTrivial
158
164
  ? `\n\n4. **Route project work through the soly plan workflow.** Each plan is a git branch with \`.agents/plans/<slug>/PLAN.md\` on it. Two parallel plans don't collide.
159
165
 
166
+ **Read the user's intent and act on it — don't make them memorize verbs.** When the user expresses a workflow intent in plain language — even loosely ("давай план", "let's plan this", "go", "start executing", "wrap it up") — call the \`soly_workflow\` tool with the matching action (\`new\` / \`plan\` / \`discuss\` / \`execute\` / \`done\`) instead of asking them to type a command. You propose the next step, they say what they want, you run it. Everything runs INLINE in this session — there is no external worker or subagent.
167
+
160
168
  **Branch naming convention for THIS project:** ${branchLine}
161
- \`soly new <slug>\` applies the project prefix automatically. \`soly new <prefix>/<slug>\` overrides it (use this when the work isn't a "feature" — e.g. \`soly new fix/login-redirect-bug\`).
169
+ \`soly_workflow({action:"new", target:"<slug>"})\` applies the project prefix automatically. A \`<prefix>/<slug>\` target overrides it (use this when the work isn't a "feature" — e.g. \`fix/login-redirect-bug\`).
162
170
 
163
- Lifecycle (text commands you type pi routes them through the workflow handlers):
164
- - \`soly new <slug-or-prefix>/<slug>\` — scaffold: branch + plan dir + stub PLAN.md + commit
165
- - \`soly discuss <slug>\` — interactive discussion of the plan (uses ask_pro)
166
- - \`soly plan <slug>\` — flesh out PLAN.md via ask_pro
167
- - \`soly execute <slug>\` — execute the plan in a subagent
168
- - \`soly done <slug>\` — commit + push + open draft PR via gh
169
- - \`soly verify\` — self-review loop until clean
170
- - \`soly status\` — current position + progress (no LLM round-trip)
171
+ Lifecycle call \`soly_workflow({action, target})\`, or the user can type the equivalent \`soly <verb>\` text:
172
+ - \`new\` — scaffold: branch + plan dir + stub PLAN.md + commit
173
+ - \`discuss\` — interactive discussion of the plan (uses ask_pro)
174
+ - \`plan\` — flesh out PLAN.md via ask_pro
175
+ - \`execute\` — execute the plan inline in THIS session
176
+ - \`done\` — commit + push + open draft PR via gh
177
+ - \`soly verify\` — self-review loop until clean (type this verb; it's a stateful loop)
178
+ - \`soly status\` — current position + progress (no LLM round-trip)
171
179
 
172
180
  **You may scaffold a new plan yourself** when the user asks for a new piece of work and the scope is clear (or the user just confirmed). Propose the slug AND the full branch name first via ask_pro — offer the default \`${prefix ? prefix + "/" : ""}<slug>\` plus the alternatives \`fix/<slug>\`, \`chore/<slug>\`, or just \`<slug>\` (no prefix). Pick whatever fits the work, then type \`soly new …\` in your next output. Don't ask for trivial one-liners or when the user already said "go". Skip only for a genuine one-off that doesn't deserve a plan branch.
173
181
 
@@ -214,10 +222,76 @@ The following are user-set defaults, not project rules. They tell you how the us
214
222
  1. **Pre-action gate.** Before starting non-trivial work, take a 10-second pause and decide: do I have enough to act, or should I ask? If the prompt has ambiguity, missing scope, or a hidden assumption, surface one short clarifying question (or a small set of multi-choice options) instead of starting to code. Skip the gate for trivial fixes ("rename X", "add log line", "fix typo") and for follow-up turns in an already-clarified task.${confirmBlock}
215
223
  ${triggerLine}${anglesBlock}
216
224
 
217
- 2. **Background subagents by default.** When you need to read unfamiliar code, scout a directory, gather external evidence, or run a multi-step review, prefer \`subagent(...)\` with \`async: true\` and \`context: "fresh"\` over doing it inline. Reserve your own context for the actual decision the user is paying you to make. If the work needs the parent conversation history, use \`context: "fork"\` instead. Do not silently block on long runs — launch async and continue with independent work.
225
+ 2. **Scout with soly's own read tools.** When you need to read unfamiliar code, map a directory, or gather context, use soly's read tools — \`soly_snippet(path, offset, limit)\`, \`soly_doc_search(query)\`, \`soly_read(...)\` plus \`grep\` / \`find\`. Prefer bounded snippets over reading whole files. soly does the work INLINE in this session; there is no separate worker or \`subagent(...)\` tool to delegate to (and none is required).
226
+
227
+ 3. **Reach for soly's interaction tools.** For structured questions use \`ask_pro\` (batched, multi-select, ⭐ recommended); for design/architecture forks where the choice hinges on the concrete code shape use \`decision_deck\`; for visual output (galleries, comparisons, diagrams) use \`html_artifact\`. Give each question a concrete recommended default + rationale — don't dump open-ended prompts.${workflowPoint}
228
+
229
+ Treat (1) and (2) as defaults, not laws. The user can always override per-task ("just do it", "ask me everything"). When overriding, briefly acknowledge it.
230
+ `;
231
+ }
232
+
233
+ // ---------------------------------------------------------------------------
234
+ // Proactive "suggested next step" section (pillar C)
235
+ // ---------------------------------------------------------------------------
236
+ //
237
+ // Always-on when a soly project exists (independent of the non-trivial
238
+ // heuristic). soly computes where the user is in the workflow and surfaces the
239
+ // single most useful next action so the model can OFFER it — and, on the
240
+ // user's confirmation, call the `soly_workflow` tool itself. The user never
241
+ // has to remember a verb.
242
+
243
+ /** Snapshot of where the user is in the plan-branch workflow. Computed by the
244
+ * caller (it needs git + fs); kept out of the pure section builder so the
245
+ * builder stays trivially testable. */
246
+ export interface WorkflowSituation {
247
+ /** A soly project (`.agents/`) exists in cwd. */
248
+ hasProject: boolean;
249
+ /** Current git branch, or null if not resolvable. */
250
+ branch: string | null;
251
+ /** Branch is a plan branch (not master/main, not detached). */
252
+ onPlanBranch: boolean;
253
+ /** Plan slug derived from the branch (drops any `<prefix>/`). */
254
+ planSlug: string | null;
255
+ /** `.agents/plans/<slug>/PLAN.md` exists on this branch. */
256
+ planExists: boolean;
257
+ /** The PLAN.md is still the scaffold stub (not fleshed out). */
258
+ planIsStub: boolean;
259
+ /** Working tree has uncommitted changes. */
260
+ dirty: boolean;
261
+ /** Task ids that are `status: ready` with all deps satisfied. */
262
+ readyTaskIds: string[];
263
+ }
264
+
265
+ /** Build the "suggested next step" system-prompt section. Pure. Returns "" when
266
+ * there's no project (nothing to suggest). */
267
+ export function buildSuggestionSection(sit: WorkflowSituation): string {
268
+ if (!sit.hasProject) return "";
269
+
270
+ // Decide the single best next action + how to invoke it.
271
+ let suggestion: string;
272
+ const slug = sit.planSlug ?? "<slug>";
273
+
274
+ if (sit.onPlanBranch && sit.planExists && sit.planIsStub) {
275
+ suggestion = `You're on plan branch \`${sit.branch}\` and its PLAN.md is still a scaffold stub. **Next:** flesh it out — \`soly_workflow({ action: "plan", target: "${slug}" })\` (or discuss first with \`action: "discuss"\`).`;
276
+ } else if (sit.onPlanBranch && sit.planExists && !sit.planIsStub) {
277
+ suggestion = sit.dirty
278
+ ? `You're on plan branch \`${sit.branch}\` with a fleshed-out PLAN.md and uncommitted changes. **Next:** finish the work, then \`soly_workflow({ action: "done", target: "${slug}" })\` to commit + push + open the PR.`
279
+ : `You're on plan branch \`${sit.branch}\` with a ready PLAN.md. **Next:** execute it — \`soly_workflow({ action: "execute", target: "${slug}" })\`.`;
280
+ } else if (sit.onPlanBranch && !sit.planExists) {
281
+ suggestion = `You're on plan branch \`${sit.branch}\` but there's no \`.agents/plans/${slug}/PLAN.md\`. **Next:** scaffold it — \`soly_workflow({ action: "new", target: "${slug}" })\`.`;
282
+ } else if (sit.readyTaskIds.length > 0) {
283
+ const ids = sit.readyTaskIds.slice(0, 5).join(", ");
284
+ suggestion = `There ${sit.readyTaskIds.length === 1 ? "is 1 ready task" : `are ${sit.readyTaskIds.length} ready tasks`} (${ids}${sit.readyTaskIds.length > 5 ? ", …" : ""}). **Next:** execute one — \`soly_workflow({ action: "execute", target: "<task-id>" })\` — or start fresh work with \`action: "new"\`.`;
285
+ } else {
286
+ suggestion = `On \`${sit.branch ?? "the current branch"}\`, no plan in flight. **Next:** when the user describes a piece of work, scaffold a plan — \`soly_workflow({ action: "new", target: "<slug>" })\` — instead of editing files ad-hoc.`;
287
+ }
288
+
289
+ return `
290
+
291
+ ## soly — suggested next step
218
292
 
219
- 3. **Subagent tool ergonomics.** When delegating: give the child a concrete role, scope, success criteria, hard constraints, and expected output. Do not pass vague instructions like "implement this" or "look into that". Async is the default; foreground is the explicit opt-out.${workflowPoint}
293
+ ${suggestion}
220
294
 
221
- Treat (1) and (2) as defaults, not laws. The user can always override per-task ("just do it", "ask me everything", "no subagents"). When overriding, briefly acknowledge it.
295
+ **You propose; the user confirms; you run it.** When the user expresses intent in plain language — even loosely ("давай план", "let's build it", "go", "start", "wrap it up") — call the \`soly_workflow\` tool with the matching action yourself. Do NOT make the user type \`soly <verb>\` (that text form still works, but it's a fallback, not a requirement). Everything runs inline in this session — no external subagent.
222
296
  `;
223
297
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "pi-soly",
3
- "version": "1.16.4",
4
- "description": "Project management + workflow framework for pi-coding-agent. Plans, state, MANDATORY rules, self-review, multi-question picker, native footer + welcome chrome — one npm install, zero config. The LLM drives execution and delegates to a worker subagent when available.",
3
+ "version": "2.1.0",
4
+ "description": "Project management + workflow framework for pi-coding-agent. Plans, state, MANDATORY rules, self-review, multi-question picker, native footer + welcome chrome — one npm install, zero config. The LLM drives the workflow inline via the soly_workflow tool — no external subagent plugin.",
5
5
  "type": "module",
6
6
  "main": "index.ts",
7
7
  "scripts": {
@@ -47,17 +47,28 @@ Workflow verbs are **plain text** — type `soly <verb>` (NOT a slash command):
47
47
  | `/why` | What rules + state grounded the last turn |
48
48
  | `/rulewizard` | Rule vs .editorconfig vs linter guide |
49
49
 
50
- ## Delegation
51
-
52
- `soly plan` and `soly execute` delegate the heavy work to a `worker` subagent
53
- (via the `subagent(...)` tool from pi-subagents); the parent session keeps the
54
- close-out (production commits `SUMMARY.md` `STATE.md` then `soly verify`).
55
- If the `subagent` tool is NOT installed they run **inline** in the main session
56
- instead `soly doctor` reports which mode is active. First-party delegation is
57
- on the roadmap. Other agents are read-only helpers: `oracle` (second opinion),
58
- `scout` (recon), `reviewer` (adversarial review). `soly discuss` is always
59
- interactive in the main session (not delegated). Rotors / `Ctrl+Tab` cycling
60
- were removed in 1.4.0.
50
+ ## How work runs (inline — no subagent plugin)
51
+
52
+ soly runs everything **inline, in the main session**. `soly plan` and
53
+ `soly execute` transform your request into a detailed instruction (the relevant
54
+ workflow markdown, the iteration context bundle, the close-out discipline) and
55
+ the model does the work itself production commits `SUMMARY.md` `STATE.md`
56
+ then `soly verify`. There is **no dependency on the external pi-subagents
57
+ plugin** (removed in 2.0.0); nothing breaks when that plugin changes.
58
+
59
+ **Two ways to drive the workflow both hit the same code:**
60
+
61
+ 1. **Let the model propose and run it (preferred).** soly injects a "suggested
62
+ next step" into the system prompt every turn. Just say what you want in
63
+ plain language ("let's plan this", "go", "start executing", "wrap it up")
64
+ and the model calls the `soly_workflow` tool for you — you never have to
65
+ memorize a verb.
66
+ 2. **Type the verb yourself.** `soly plan <slug>`, `soly execute <slug>`, etc.
67
+ still work as plain-text input (a fallback for power users).
68
+
69
+ `soly discuss` is always interactive in the main session. `soly verify` is a
70
+ stateful self-review loop you start by typing `soly verify` (it's not a
71
+ `soly_workflow` action).
61
72
 
62
73
  ## File structure
63
74
 
@@ -212,6 +223,7 @@ Once production commits exist, returning without a committed `SUMMARY.md` is an
212
223
 
213
224
  | Tool | Purpose |
214
225
  |---|---|
226
+ | `soly_workflow(action, target?)` | Drive the lifecycle inline: `new` / `discuss` / `plan` / `execute` / `done`. Call it on the user's natural-language intent instead of making them type `soly <verb>`. Returns the workflow instruction to follow in this session |
215
227
  | `soly_read(artifact, phase, taskId)` | Read soly artifacts: STATE, plan, context, research, ROADMAP, requirements, project, milestone, task |
216
228
  | `soly_log_decision(decision, rationale, phase)` | Append to STATE.md Decisions table |
217
229
  | `soly_list_phases()` | List all phases with plan counts, C/R markers |
@@ -296,6 +308,6 @@ Call `soly_read(artifact: "state")` and `soly_read(artifact: "roadmap")` first.
296
308
 
297
309
  - ❌ Edit `.agents/rules/` files you didn't write — those are project invariants
298
310
  - ❌ Skip the SUMMARY — illegal partial state
299
- - ❌ Spawn `soly-manager` / `soly-worker` / etc. there are no soly subagents (removed in 1.3.0). Use pi's built-in subagents via the parent LLM's `subagent(...)` call.
311
+ - ❌ Reach for a `subagent(...)` tool soly runs inline, in this session. There is no soly subagent and no dependency on pi-subagents (removed in 2.0.0). Use `soly_workflow` (or the plain `soly <verb>` text) instead.
300
312
  - ❌ Edit `.agents/phases/*/PLAN.md` after `status: in_progress` — create a new plan
301
313
  - ❌ Put intent docs anywhere other than `.agents/docs/`
@@ -3,17 +3,17 @@
3
3
  // =============================================================================
4
4
  //
5
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.
6
+ // and transforms it into a detailed LLM instruction that carries the soly
7
+ // execute workflow inline.
8
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).
9
+ // We use `action: "transform"` (not `action: "handled"`) — the LLM receives
10
+ // the request enriched with the full workflow context and executes it INLINE,
11
+ // in the same session, applying the SOLY-specific close-out discipline
12
+ // (commits, SUMMARY.md, STATE.md update).
13
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.
14
+ // No external subagent plugin is involved: the model does the work itself.
15
+ // The same instruction is emitted whether the request arrives as the plain
16
+ // "soly execute …" verb (input hook) or via the `soly_workflow` tool.
17
17
  // =============================================================================
18
18
 
19
19
  import * as fs from "node:fs";
@@ -67,7 +67,6 @@ export function buildExecuteTransform(
67
67
  cmd: SolyCommand,
68
68
  state: SolyState,
69
69
  interactiveRules: string[] = [],
70
- opts: { agent?: string } = {},
71
70
  ): ExecuteHandlerResult {
72
71
  if (!state.exists) {
73
72
  return {
@@ -165,7 +164,7 @@ export function buildExecuteTransform(
165
164
  });
166
165
  const inlineSummary = inlinePlanSummary(path.join(task.dir, "PLAN.md"));
167
166
 
168
- const instruction = `soly execute ${target.taskId} — launching worker for task.
167
+ const instruction = `soly execute ${target.taskId} — executing task.
169
168
 
170
169
  **Task:** ${task.id}
171
170
  **Feature:** ${task.feature}
@@ -177,20 +176,13 @@ export function buildExecuteTransform(
177
176
  **Dir:** ${task.dir}
178
177
 
179
178
  **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.
179
+ Read this file first — it contains intent, STATE, ROADMAP (n/a for tasks), the feature README, prior task SUMMARYs, and the current task PLAN.
181
180
 
182
- **0-POINT CHECK.** Worker must re-read .agents/docs/ (intent) and .agents/features/${task.feature}/README.md before implementing.
181
+ **0-POINT CHECK.** Re-read .agents/docs/ (intent) and .agents/features/${task.feature}/README.md before implementing.
183
182
 
184
- **STUDY THE REPO.** The worker MUST use \`soly_snippet(path, offset, limit)\`, \`soly_doc_search(query)\`, and \`## project layout\` from the system prompt to understand the area before changing any file. Read at least: the module being modified, one similar feature for the pattern, and the corresponding test file. Do NOT change code based on assumptions about how the existing code works.
183
+ **STUDY THE REPO.** Use \`soly_snippet(path, offset, limit)\`, \`soly_doc_search(query)\`, and \`## project layout\` from the system prompt to understand the area before changing any file. Read at least: the module being modified, one similar feature for the pattern, and the corresponding test file. Do NOT change code based on assumptions about how the existing code works.
185
184
 
186
- Launch a single subagent for this work. Do NOT do the work inline.
187
-
188
- subagent({
189
- agent: ${JSON.stringify(opts.agent ?? "worker")},
190
- context: "fresh",
191
- async: true,
192
- maxSubagentDepth: 1,
193
- task: \`You are soly-executor (single-task writer).
185
+ **Execute this now inline, in THIS session. You are soly-executor (single-task writer). Do the work directly; there is no separate worker to delegate to.**
194
186
 
195
187
  Your job: execute ONE task (atomic unit) and produce its SUMMARY.md.
196
188
 
@@ -227,15 +219,12 @@ ${workflow}
227
219
  Hard rules:
228
220
  - Do not skip the close-out order: production commits -> SUMMARY commit -> status: done.
229
221
  - Do not modify any .agents/rules/ files.
230
- - Do not run subagents yourself.
231
222
  - Do not start a task whose \`depends-on:\` lists tasks that are not \`done\`.
232
223
  - PATH DISCIPLINE: all files YOU create must live under \`.agents/\` (iteration, handoff, etc.) or under the project's source dirs. Never write to the project root.
233
- - Return: changed files, commands run with exit codes, validation evidence, surprises, decisions needing parent approval.
234
- - Interactive-only rules are NOT in scope for you: ${interactiveRules.length > 0 ? interactiveRules.join(", ") : "(none)"}.
235
- \`
236
- })
224
+ - Report as you go: changed files, commands run with exit codes, validation evidence, surprises, decisions needing user approval.
225
+ - Interactive-only rules are NOT in scope here: ${interactiveRules.length > 0 ? interactiveRules.join(", ") : "(none)"}.
237
226
 
238
- When the subagent completes, synthesize the result. Do not re-execute its work. Then suggest \`soly verify\` to self-review the change with fresh eyes before calling it done.`;
227
+ When done, confirm the SUMMARY.md was written and the task flipped to \`status: done\`. Then suggest \`soly verify\` to self-review the change with fresh eyes before calling it done.`;
239
228
  return { handled: true, transformedText: instruction };
240
229
  }
241
230
 
@@ -272,7 +261,7 @@ When the subagent completes, synthesize the result. Do not re-execute its work.
272
261
  `**v0.1 limitation:** tasks run sequentially, not in parallel. Parallel mode is v0.2.\n\n` +
273
262
  `Ready tasks (in suggested order):\n` +
274
263
  ready.map((t, i) => ` ${i + 1}. ${t.id} [${t.kind}] prio=${t.priority}`).join("\n") +
275
- `\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.`,
264
+ `\n\nExecute them one at a time in this order, inline in this session, using the task execution workflow (execute-task.md) per task.`,
276
265
  };
277
266
  }
278
267
 
@@ -318,7 +307,7 @@ When the subagent completes, synthesize the result. Do not re-execute its work.
318
307
  **PLAN.md:** ${planFile}
319
308
 
320
309
  **Iteration context file written:** \`${iter.relPath}\` (${iter.tokens} tokens)
321
- The worker reads this file first — it contains intent, STATE, ROADMAP, the
310
+ Read this file first — it contains intent, STATE, ROADMAP, the
322
311
  plan body inline, and any prior SUMMARYs for this plan (none on first run).
323
312
 
324
313
  **Inline plan body (so you have must-haves before reading the file):**
@@ -326,20 +315,13 @@ plan body inline, and any prior SUMMARYs for this plan (none on first run).
326
315
  ${planBody.slice(0, 4000)}${planBody.length > 4000 ? "\n…(truncated)" : ""}
327
316
  \`\`\`
328
317
 
329
- **0-POINT CHECK.** Worker must re-read .agents/docs/ (intent) before implementing.
318
+ **0-POINT CHECK.** Re-read .agents/docs/ (intent) before implementing.
330
319
 
331
- **STUDY THE REPO.** Worker MUST use \`soly_snippet(path, offset, limit)\`, \`soly_doc_search(query)\`, and \`## project layout\` from the system prompt to map the area before editing. Read at least: the module(s) the plan touches, one adjacent feature for the convention, and any test files in the same area. Do NOT edit code on assumptions about how existing code is structured.
320
+ **STUDY THE REPO.** Use \`soly_snippet(path, offset, limit)\`, \`soly_doc_search(query)\`, and \`## project layout\` from the system prompt to map the area before editing. Read at least: the module(s) the plan touches, one adjacent feature for the convention, and any test files in the same area. Do NOT edit code on assumptions about how existing code is structured.
332
321
 
333
322
  **Corporate reviewer — gap-hunt the plan first.** Re-read the entire PLAN.md end-to-end. Identify concrete gaps (file paths not named, error handling missing, boundary cases unmentioned, no test file cited, no migration plan for existing instances, etc.). If you find ANY material gap, use \`ask_pro\` to surface it to the user BEFORE editing a single file — phrase each question with a recommended default + 2-3 alternatives. Only after gaps are resolved (or explicitly accepted by the user) may implementation begin.
334
323
 
335
- Launch a single subagent to execute the plan. Do NOT do the work inline.
336
-
337
- subagent({
338
- agent: ${JSON.stringify(opts.agent ?? "worker")},
339
- context: "fresh",
340
- async: true,
341
- maxSubagentDepth: 1,
342
- task: \`You are soly-executor. Execute the plan at \`${planFile}\` end-to-end.
324
+ **Execute this now inline, in THIS session. You are soly-executor. Execute the plan at \`${planFile}\` end-to-end. Do the work directly.**
343
325
 
344
326
  **FIRST ACTION — read the iteration context file:**
345
327
  \`\`\`
@@ -364,8 +346,8 @@ Hard rules:
364
346
  - When the plan is fully executed and verified, write a SUMMARY.md next to
365
347
  PLAN.md summarizing what was done, what was deferred, and any deviations.
366
348
  - Do not commit unless the workflow tells you to; the user reviews and merges.
367
- \`)
368
- }`;
349
+
350
+ When done, suggest \`soly verify\` to self-review with fresh eyes, then \`soly done ${target.raw}\` to open the PR.`;
369
351
  return { handled: true, transformedText: instruction };
370
352
  }
371
353
 
@@ -451,26 +433,19 @@ The iteration context file lists all plans (their frontmatter) in section 6, gro
451
433
  ? `soly-executor (single-plan writer)`
452
434
  : `soly-executor (wave-based parallel phase executor)`;
453
435
 
454
- const instruction = `soly execute ${target.raw} — launching worker for ${targetDesc}.
436
+ const instruction = `soly execute ${target.raw} — executing ${targetDesc}.
455
437
 
456
438
  **Iteration context file written:** \`${iter.relPath}\` (${iter.tokens} tokens, ${iter.bytes} bytes)
457
- 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.
439
+ Read 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.
458
440
 
459
- **0-POINT CHECK — worker must read .agents/docs/ first.**
460
- 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 .agents/docs/ (and any intent docs linked from PLAN.md) before each plan.
441
+ **0-POINT CHECK — read .agents/docs/ first.**
442
+ These are the project's INTENT docs. You are about to implement tasks; if the implementation diverges from intent, it will be wrong even if the tests pass. Re-read .agents/docs/ (and any intent docs linked from PLAN.md) before each plan.
461
443
 
462
444
  ${scopeBlock}
463
445
 
464
- Launch a single subagent for this work. Do NOT do the work inline.
465
-
466
- subagent({
467
- agent: ${JSON.stringify(opts.agent ?? "worker")},
468
- context: "fresh",
469
- async: true,
470
- maxSubagentDepth: 1, // worker must not spawn sub-sub-agents
471
- task: \`You are ${childRole}.
446
+ **Execute this now inline, in THIS session. You are ${childRole}. Do the work directly; there is no separate worker to delegate to.**
472
447
 
473
- Your job: ${isPlanLevel ? "execute ONE plan and produce its SUMMARY.md" : useTasks ? "execute the phase's ready tasks in dependency order, each producing its SUMMARY.md" : "execute ALL plans in this phase using wave-based parallel execution"}.
448
+ Your job: ${isPlanLevel ? "execute ONE plan and produce its SUMMARY.md" : useTasks ? "execute the phase's ready tasks in dependency order, each producing its SUMMARY.md" : "execute ALL plans in this phase using wave-based sequential execution"}.
474
449
 
475
450
  **FIRST ACTION — read the iteration context file:**
476
451
  \`\`\`
@@ -499,14 +474,11 @@ ${workflow}
499
474
  Hard rules:
500
475
  - Do not skip the close-out order: production commits -> SUMMARY commit -> STATE/ROADMAP update.
501
476
  - Do not modify any .agents/rules/ files.
502
- - Do not run subagents yourself.
503
477
  - PATH DISCIPLINE: all files YOU create must live under \`.agents/\` (e.g. .agents/iterations/, .agents/phases/<slug>/, .agents/HANDOFF.json) or under the project's source dirs. Never write PLAN/SUMMARY/CONTEXT/RESEARCH/iteration files to the project root.
504
- - Return: changed files, commands run with exit codes, validation evidence, surprises, and any decisions needing parent approval.
505
- - 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.
506
- \`
507
- })
478
+ - Report as you go: changed files, commands run with exit codes, validation evidence, surprises, and any decisions needing user approval.
479
+ - Interactive-only rules are NOT in scope here: ${interactiveRules.length > 0 ? interactiveRules.join(", ") : "(none)"}. They describe how the user-facing conversation should go, not how to execute work.
508
480
 
509
- When the subagent completes, synthesize the result and confirm STATE.md was updated. Do not re-execute its work. Then suggest \`soly verify\` to self-review the work with fresh eyes before calling the phase done.`;
481
+ When done, confirm STATE.md was updated. Then suggest \`soly verify\` to self-review the work with fresh eyes before calling the phase done.`;
510
482
 
511
483
  return { handled: true, transformedText: instruction };
512
484
  }
@@ -33,8 +33,9 @@ import type { SolyConfig } from "../config.js";
33
33
 
34
34
  export interface WorkflowsDeps {
35
35
  getState: () => SolyState;
36
- /** List of rule relPaths marked `interactive: true` — passed to subagent
37
- * workers so they know which rules are explicitly out of scope. */
36
+ /** List of rule relPaths marked `interactive: true` — inlined into the
37
+ * execute instruction so the model knows which rules are explicitly out of
38
+ * scope for the execution work. */
38
39
  getInteractiveRules: () => string[];
39
40
  /** List of active tool names. Used to detect optional cross-extension
40
41
  * dependencies (e.g. `ask_pro` from the separate `pi-ask` extension). */
@@ -55,24 +56,6 @@ export interface WorkflowsDeps {
55
56
  /** Verbs that put the session into a visible "mode" (shown in the chrome top bar). */
56
57
  const MODE_VERBS: readonly string[] = ["execute", "plan", "discuss", "resume"];
57
58
 
58
- /**
59
- * execute/plan instructions tell the model to launch a `subagent(...)`. That
60
- * tool is provided by the external pi-subagents plugin. When it's absent the
61
- * instruction is impossible, so prepend an authoritative override telling the
62
- * model to do the work inline instead — removing the silent breakage until
63
- * soly ships first-party delegation.
64
- */
65
- export function withSubagentPreflight(text: string, activeTools: string[]): string {
66
- if (activeTools.includes("subagent")) return text;
67
- return (
68
- "> ⚠ The `subagent` tool is NOT installed in this session. Ignore any " +
69
- '"launch a subagent / do not work inline" instruction below — instead do this ' +
70
- "work yourself, inline, in THIS session, applying the same close-out discipline. " +
71
- "(Install pi-subagents for delegated/parallel execution.)\n\n" +
72
- text
73
- );
74
- }
75
-
76
59
  export function registerWorkflows(pi: ExtensionAPI, deps: WorkflowsDeps): void {
77
60
  const { getState, getInteractiveRules, getActiveTools, getConfig, onWorkflowUsed } = deps;
78
61
 
@@ -118,7 +101,7 @@ export function registerWorkflows(pi: ExtensionAPI, deps: WorkflowsDeps): void {
118
101
  if (cmd.verb === "execute") {
119
102
  const result = buildExecuteTransform(cmd, state, getInteractiveRules());
120
103
  if (!result.handled || !result.transformedText) return;
121
- return { action: "transform", text: withSubagentPreflight(result.transformedText, getActiveTools()) };
104
+ return { action: "transform", text: result.transformedText };
122
105
  }
123
106
 
124
107
  if (cmd.verb === "pause" || cmd.verb === "compact") {
@@ -137,7 +120,7 @@ export function registerWorkflows(pi: ExtensionAPI, deps: WorkflowsDeps): void {
137
120
  if (cmd.verb === "plan") {
138
121
  const result = buildPlanTransform(cmd, state);
139
122
  if (!result.handled || !result.transformedText) return;
140
- return { action: "transform", text: withSubagentPreflight(result.transformedText, getActiveTools()) };
123
+ return { action: "transform", text: result.transformedText };
141
124
  }
142
125
 
143
126
  if (cmd.verb === "discuss") {
@@ -200,15 +200,15 @@ export function showDoctor(_cmd: unknown, state: SolyState, ui: InspectUI, confi
200
200
  });
201
201
  }
202
202
 
203
- // 11. subagent tool (execute/plan delegate to it; without it they run inline)
203
+ // 11. execution model (always inline no external subagent plugin)
204
204
  if (state.exists) {
205
- const hasSubagent = activeTools.includes("subagent");
205
+ const hasWorkflowTool = activeTools.includes("soly_workflow");
206
206
  checks.push({
207
- name: "subagent tool (delegated execution)",
208
- status: hasSubagent ? "pass" : "info",
209
- detail: hasSubagent
210
- ? "subagent tool loaded — soly execute/plan delegate to a worker"
211
- : "not installedsoly execute/plan run inline in this session (install pi-subagents for delegated/parallel execution)",
207
+ name: "workflow execution (inline)",
208
+ status: hasWorkflowTool ? "pass" : "info",
209
+ detail: hasWorkflowTool
210
+ ? "soly_workflow tool loaded — the model drives plan/execute/etc. inline in this session (no subagent plugin needed)"
211
+ : "soly_workflow tool not detectedthe plain-text `soly <verb>` forms still run inline; reload if it's missing",
212
212
  });
213
213
  }
214
214
 
@@ -0,0 +1,123 @@
1
+ // =============================================================================
2
+ // workflows/llm-tools.ts — `soly_workflow` tool (first-party, no subagents)
3
+ // =============================================================================
4
+ //
5
+ // Exposes the soly lifecycle to the LLM as a single callable tool so the model
6
+ // can drive the workflow itself in response to the user's natural-language
7
+ // intent — instead of the user having to type `soly <verb>` by hand.
8
+ //
9
+ // soly_workflow({ action: "new" | "plan" | "discuss" | "execute" | "done",
10
+ // target?: "<slug>" | "<prefix>/<slug>" | "<N>" | "<task-id>" })
11
+ //
12
+ // The tool reuses the SAME builders the plain-text `soly <verb>` input hook
13
+ // uses (buildNewTransform / buildPlanTransform / …), so there is exactly one
14
+ // implementation of each verb. It returns the builder's instruction text as
15
+ // the tool result — which flows back into the model's context, and the model
16
+ // continues the work INLINE, in this session. No external subagent plugin.
17
+ //
18
+ // `verify` is intentionally NOT exposed here: it's a stateful self-review loop
19
+ // driven by the interactive input channel (the user types `soly verify`).
20
+ // Starting it mid-tool-call would fight the streaming turn.
21
+ // =============================================================================
22
+
23
+ import { Type } from "typebox";
24
+ import { StringEnum } from "@earendil-works/pi-ai";
25
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
26
+ import type { SolyCommand } from "./parser.ts";
27
+ import { buildExecuteTransform } from "./execute.ts";
28
+ import { buildPlanTransform, buildDiscussTransform } from "./planning.ts";
29
+ import { buildNewTransform } from "./new.ts";
30
+ import { buildDoneTransform } from "./done.ts";
31
+ import type { SolyState } from "../core.js";
32
+ import type { SolyConfig } from "../config.js";
33
+
34
+ /** Actions the LLM can drive through `soly_workflow`. Order matches lifecycle. */
35
+ export const WORKFLOW_ACTIONS = ["new", "discuss", "plan", "execute", "done"] as const;
36
+ export type WorkflowAction = (typeof WORKFLOW_ACTIONS)[number];
37
+
38
+ /** Actions that put the session into a visible "mode" (chrome top bar). */
39
+ const MODE_ACTIONS: readonly WorkflowAction[] = ["execute", "plan", "discuss"];
40
+
41
+ export interface WorkflowToolDeps {
42
+ getState: () => SolyState;
43
+ getInteractiveRules: () => string[];
44
+ getActiveTools: () => string[];
45
+ getConfig: () => SolyConfig;
46
+ /** Reset drift counters etc. (same hook the input handler fires). */
47
+ onWorkflowUsed?: () => void;
48
+ /** Set the active workflow-mode label shown in the chrome top bar. */
49
+ setVerbLabel?: (verb: string | null) => void;
50
+ }
51
+
52
+ /** Build the shared `{ handled, transformedText }` result for one action.
53
+ * Pure dispatch over the existing builders — no side effects beyond what the
54
+ * individual builders do (new/done touch git + ui.notify). */
55
+ function dispatch(
56
+ action: WorkflowAction,
57
+ cmd: SolyCommand,
58
+ deps: WorkflowToolDeps,
59
+ ui: { notify: (t: string, level?: "info" | "warning" | "error") => void },
60
+ cwd: string,
61
+ ): { handled: boolean; transformedText?: string } {
62
+ const state = deps.getState();
63
+ switch (action) {
64
+ case "new":
65
+ return buildNewTransform(cmd, state, ui, cwd, deps.getConfig().plan.defaultBranchPrefix);
66
+ case "done":
67
+ return buildDoneTransform(cmd, state, ui, cwd, {
68
+ defaultBranchPrefix: deps.getConfig().plan.defaultBranchPrefix,
69
+ });
70
+ case "plan":
71
+ return buildPlanTransform(cmd, state);
72
+ case "execute":
73
+ return buildExecuteTransform(cmd, state, deps.getInteractiveRules());
74
+ case "discuss":
75
+ return buildDiscussTransform(cmd, state, {
76
+ hasAskPro: deps.getActiveTools().includes("ask_pro"),
77
+ });
78
+ }
79
+ }
80
+
81
+ export function registerWorkflowTool(pi: ExtensionAPI, deps: WorkflowToolDeps): void {
82
+ pi.registerTool({
83
+ name: "soly_workflow",
84
+ label: "soly workflow",
85
+ description:
86
+ "Drive the soly plan lifecycle in THIS session (no subagent needed). Call this when the user expresses intent to plan or ship work — even loosely (\"let's plan this\", \"go\", \"start executing\", \"wrap it up\"). Actions: `new` (scaffold a plan branch + stub PLAN.md), `discuss` (interactive scoping via ask_pro), `plan` (flesh out PLAN.md), `execute` (implement the plan inline), `done` (commit + push + draft PR). `target` is the plan slug / `<prefix>/<slug>` / phase number / task id (required for new/done; optional for the phase-based forms). The result is the full workflow instruction — follow it inline; do not delegate. (For the `verify` self-review loop, tell the user to type `soly verify` — it's a stateful loop, not a one-shot action.)",
87
+ promptSnippet:
88
+ "Run a soly workflow step (new/discuss/plan/execute/done) when the user asks to plan or ship work",
89
+ promptGuidelines: [
90
+ "Use soly_workflow when the user expresses intent to plan, discuss, execute, or finish a piece of work — call it yourself instead of asking them to type a `soly <verb>` command.",
91
+ "After soly_workflow returns, follow the returned instruction inline in this session; there is no separate worker to hand off to.",
92
+ ],
93
+ parameters: Type.Object({
94
+ action: StringEnum([...WORKFLOW_ACTIONS] as [WorkflowAction, ...WorkflowAction[]]),
95
+ target: Type.Optional(
96
+ Type.String({
97
+ description:
98
+ "Plan slug (e.g. `auth-jwt`), `<prefix>/<slug>` (e.g. `fix/login-bug`), phase number (e.g. `3` or `3.02`), or task id. Required for new/done.",
99
+ }),
100
+ ),
101
+ }),
102
+ async execute(_id, params, _signal, _onUpdate, ctx) {
103
+ const action = params.action as WorkflowAction;
104
+ const target = (params.target ?? "").trim();
105
+ const cmd: SolyCommand = {
106
+ verb: action,
107
+ args: target ? target.split(/\s+/) : [],
108
+ raw: `soly ${action}${target ? ` ${target}` : ""}`,
109
+ };
110
+
111
+ // Same bookkeeping the input hook does for a recognized verb.
112
+ deps.onWorkflowUsed?.();
113
+ if (MODE_ACTIONS.includes(action)) deps.setVerbLabel?.(action);
114
+
115
+ const result = dispatch(action, cmd, deps, ctx.ui, ctx.cwd);
116
+ const text = result.transformedText ?? `soly_workflow: nothing to do for action "${action}".`;
117
+ return {
118
+ content: [{ type: "text", text }],
119
+ details: { action, target: target || null, handled: result.handled },
120
+ };
121
+ },
122
+ });
123
+ }
@@ -8,8 +8,9 @@
8
8
  // Convention:
9
9
  // - User types exactly "soly <verb> <args...>" (lowercase required for match)
10
10
  // - The extension intercepts via the `input` event (no slash-command needed)
11
- // - The handler transforms the input into a detailed LLM instruction that
12
- // delegates to the `subagent(...)` tool (provided by pi-subagents)
11
+ // - The handler transforms the input into a detailed LLM instruction the
12
+ // model follows INLINE, in this session (the same instruction the
13
+ // `soly_workflow` tool returns). No external subagent plugin.
13
14
  //
14
15
  // This module is pure parsing — no I/O, no extension state. Trivial to unit
15
16
  // test in isolation.
@@ -8,8 +8,8 @@
8
8
  // phase before any planning starts (phase-only in v0.2)
9
9
  //
10
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.
11
+ // markdown (plan-phase.md / plan-task.md / discuss-phase.md) inline — the
12
+ // model does the planning itself, in the same session. No subagent plugin.
13
13
  // =============================================================================
14
14
 
15
15
  import * as fs from "node:fs";
@@ -172,13 +172,9 @@ These documents hold the project's INTENT — business context, design vision, w
172
172
  Phase directory: ${phase.dir}
173
173
  Current state: planCount=${phase.planCount}, context=${phase.contextExists}, research=${phase.researchExists}
174
174
 
175
- Launch a subagent to produce the phase's tasks. Do NOT plan inline.
175
+ **Plan this now inline, in THIS session. You are the planner. Produce the phase's tasks directly.**
176
176
 
177
- subagent({
178
- agent: "worker",
179
- context: "fresh",
180
- async: true,
181
- 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.
177
+ 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.
182
178
 
183
179
  **FIRST ACTION — read the iteration context file:**
184
180
  \`\`\`
@@ -203,11 +199,8 @@ Hard rules:
203
199
  - Each task PLAN needs requirements, must_haves.truths, must_haves.artifacts, must_haves.key_links.
204
200
  - PATH DISCIPLINE: all task PLAN.md / phase CONTEXT.md / RESEARCH.md go under \`.agents/phases/<NN>-<slug>/\`. Never write to the project root.
205
201
  - Update .agents/STATE.md Current Position at the end.
206
- - Return: created task ids, the dependency order (which tasks are ready first), open questions.
207
- \`
208
- })
209
202
 
210
- When the subagent returns, summarize the tasks + their dependency order, then suggest \`soly execute ${target.phase}\` (or ask the user to confirm before execution).`;
203
+ When done, summarize the created task ids + their dependency order (which are ready first) and any open questions, then suggest \`soly execute ${target.phase}\` (or ask the user to confirm before execution).`;
211
204
  return { handled: true, transformedText: instruction };
212
205
  }
213
206
 
@@ -278,13 +271,7 @@ ${inlineSummary}
278
271
 
279
272
  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.
280
273
 
281
- Launch a single subagent to refine the plan:
282
-
283
- subagent({
284
- agent: "worker",
285
- context: "fresh",
286
- async: true,
287
- task: \`You are a planner. Refine PLAN.md for an existing task.
274
+ **Refine this now inline, in THIS session. You are the planner. Refine PLAN.md for an existing task, directly.**
288
275
 
289
276
  **FIRST ACTION — read the iteration context file:**
290
277
  \`\`\`
@@ -309,11 +296,8 @@ Hard rules:
309
296
  - If you change the plan body materially, commit it as \`chore(tasks): refine plan <task-id>\`.
310
297
  - If you only add small clarifications, no commit needed (or include in same commit).
311
298
  - PATH DISCIPLINE: PLAN.md lives at \`.agents/features/<feature>/tasks/<id>/PLAN.md\`. Never write to the project root.
312
- - Return: what changed, open questions, dependencies discovered.
313
- \`
314
- })
315
299
 
316
- When the subagent returns, summarize what was refined. Do not execute — planning only.`;
300
+ When done, summarize what was refined (what changed, open questions, dependencies discovered). Do not execute — planning only.`;
317
301
  return { handled: true, transformedText: instruction };
318
302
  }
319
303
 
@@ -402,13 +386,7 @@ depends-on: []
402
386
  [body produced by the planner workflow below]
403
387
  \`\`\`
404
388
 
405
- Launch a single subagent to flesh out the plan body:
406
-
407
- subagent({
408
- agent: "worker",
409
- context: "fresh",
410
- async: true,
411
- task: \`You are a planner. Create a new task dir + write PLAN.md with frontmatter.
389
+ **Do this now inline, in THIS session. You are the planner. Create a new task dir + write PLAN.md with frontmatter, directly.**
412
390
 
413
391
  Project root: ${projectRoot}
414
392
  Soly dir: ${state.solyDir}
@@ -428,11 +406,8 @@ Hard rules:
428
406
  - Pick a reasonable \`priority:\` (default: medium).
429
407
  - Leave \`depends-on:\` as \`[]\` unless you have a clear dep on an existing task.
430
408
  - Commit: \`chore(tasks): plan <id>\`.
431
- - Return: created path, task id, plan summary.
432
- \`
433
- })
434
409
 
435
- When the subagent returns, show the user the new task id + summary. They can then run \`soly execute <id>\`.`;
410
+ When done, show the user the created path, new task id + plan summary. They can then run \`soly execute <id>\`.`;
436
411
  return { handled: true, transformedText: instruction };
437
412
  }
438
413
 
@@ -479,7 +454,7 @@ When the subagent returns, show the user the new task id + summary. They can the
479
454
  `soly plan --feature ${target.feature}: ${ready.length} task(s) need planning.\n\n` +
480
455
  `Tasks:\n` +
481
456
  ready.map((t, i) => ` ${i + 1}. ${t.id} [${t.kind}] prio=${t.priority}`).join("\n") +
482
- `\n\nLaunch a single subagent to plan them in order. The subagent uses the plan-task.md workflow per task.`,
457
+ `\n\nPlan them in order, inline in this session, using the plan-task.md workflow per task.`,
483
458
  };
484
459
  }
485
460
 
@@ -614,7 +589,7 @@ Read it first — it contains intent, STATE, ROADMAP, and any existing phase art
614
589
 
615
590
  ${resumeBlock}
616
591
 
617
- **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.
592
+ **This is interactive.** Drive the discussion yourself, in this session, by asking the user a few questions one at a time.
618
593
 
619
594
  ---
620
595