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 +3 -3
- package/config.ts +0 -10
- package/core.ts +0 -62
- package/index.ts +66 -26
- package/nudge.ts +87 -13
- package/package.json +2 -2
- package/skills/soly-framework/SKILL.md +24 -12
- package/workflows/execute.ts +33 -61
- package/workflows/index.ts +5 -22
- package/workflows/inspect.ts +7 -7
- package/workflows/llm-tools.ts +123 -0
- package/workflows/parser.ts +3 -2
- package/workflows/planning.ts +11 -36
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-
|
|
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
|
|
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
|
|
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"
|
|
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
|
-
\`
|
|
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
|
|
164
|
-
- \`
|
|
165
|
-
- \`
|
|
166
|
-
- \`
|
|
167
|
-
- \`
|
|
168
|
-
- \`
|
|
169
|
-
- \`soly verify\`
|
|
170
|
-
- \`soly status\`
|
|
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. **
|
|
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
|
-
|
|
293
|
+
${suggestion}
|
|
220
294
|
|
|
221
|
-
|
|
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.
|
|
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
|
|
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
|
-
##
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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
|
-
- ❌
|
|
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/`
|
package/workflows/execute.ts
CHANGED
|
@@ -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
|
|
7
|
-
//
|
|
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
|
|
10
|
-
//
|
|
11
|
-
// the
|
|
12
|
-
//
|
|
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
|
-
//
|
|
15
|
-
//
|
|
16
|
-
//
|
|
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} —
|
|
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
|
-
|
|
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.**
|
|
181
|
+
**0-POINT CHECK.** Re-read .agents/docs/ (intent) and .agents/features/${task.feature}/README.md before implementing.
|
|
183
182
|
|
|
184
|
-
**STUDY THE REPO.**
|
|
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
|
-
|
|
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
|
-
-
|
|
234
|
-
- Interactive-only rules are NOT in scope
|
|
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
|
|
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\
|
|
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
|
-
|
|
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.**
|
|
318
|
+
**0-POINT CHECK.** Re-read .agents/docs/ (intent) before implementing.
|
|
330
319
|
|
|
331
|
-
**STUDY THE REPO.**
|
|
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
|
-
|
|
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} —
|
|
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
|
-
|
|
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 —
|
|
460
|
-
These are the project's INTENT docs.
|
|
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
|
-
|
|
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
|
|
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
|
-
-
|
|
505
|
-
- Interactive-only rules are NOT in scope
|
|
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
|
|
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
|
}
|
package/workflows/index.ts
CHANGED
|
@@ -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` —
|
|
37
|
-
*
|
|
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:
|
|
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:
|
|
123
|
+
return { action: "transform", text: result.transformedText };
|
|
141
124
|
}
|
|
142
125
|
|
|
143
126
|
if (cmd.verb === "discuss") {
|
package/workflows/inspect.ts
CHANGED
|
@@ -200,15 +200,15 @@ export function showDoctor(_cmd: unknown, state: SolyState, ui: InspectUI, confi
|
|
|
200
200
|
});
|
|
201
201
|
}
|
|
202
202
|
|
|
203
|
-
// 11.
|
|
203
|
+
// 11. execution model (always inline — no external subagent plugin)
|
|
204
204
|
if (state.exists) {
|
|
205
|
-
const
|
|
205
|
+
const hasWorkflowTool = activeTools.includes("soly_workflow");
|
|
206
206
|
checks.push({
|
|
207
|
-
name: "
|
|
208
|
-
status:
|
|
209
|
-
detail:
|
|
210
|
-
? "
|
|
211
|
-
: "not
|
|
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 detected — the 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
|
+
}
|
package/workflows/parser.ts
CHANGED
|
@@ -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
|
|
12
|
-
//
|
|
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.
|
package/workflows/planning.ts
CHANGED
|
@@ -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)
|
|
12
|
-
//
|
|
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
|
-
|
|
175
|
+
**Plan this now — inline, in THIS session. You are the planner. Produce the phase's tasks directly.**
|
|
176
176
|
|
|
177
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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\
|
|
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
|
|
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
|
|