pi-soly 1.13.5 → 1.15.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/status.ts CHANGED
@@ -56,8 +56,8 @@ export function formatStatus(
56
56
  lines.push(" Project state");
57
57
  lines.push(" ─────────────");
58
58
  if (!state.exists) {
59
- lines.push(" no .agents/ or .soly/ found in cwd");
60
- lines.push(" → run /soly-init to scaffold");
59
+ lines.push(" no .agents/ found in cwd");
60
+ lines.push(" → run /soly init to scaffold");
61
61
  } else {
62
62
  lines.push(` milestone: ${state.milestone ?? "—"}`);
63
63
  lines.push(` current: ${state.currentPosition ?? "ready"}`);
package/tools.ts CHANGED
@@ -3,11 +3,11 @@
3
3
  // =============================================================================
4
4
  //
5
5
  // Registers three tools the LLM can call:
6
- // - soly_read — read any .soly/ artifact (state/plan/roadmap/...)
6
+ // - soly_read — read any .agents/ artifact (state/plan/roadmap/...)
7
7
  // - soly_log_decision — append a row to STATE.md Decisions table
8
8
  // - soly_list_phases — list all phases with markers
9
9
  //
10
- // All paths are relative to <cwd>/.soly/ (the soly layout — NOT .planning/).
10
+ // All paths are relative to <cwd>/.agents/ (the soly layout — NOT .planning/).
11
11
  // =============================================================================
12
12
 
13
13
  import * as fs from "node:fs";
@@ -17,7 +17,7 @@ import { promisify } from "node:util";
17
17
  import { Type } from "typebox";
18
18
  import { StringEnum } from "@earendil-works/pi-ai";
19
19
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
20
- import { readIfExists, splitFrontmatter, atomicWriteFileSync, type SolyState } from "./core.ts";
20
+ import { readIfExists, splitFrontmatter, atomicWriteFileSync, solyDirFor, type SolyState } from "./core.ts";
21
21
  import { detectEnv, type EnvSummary } from "./env.ts";
22
22
  import type { SolyConfig } from "./config.ts";
23
23
  import { buildDocIndex, searchDocs, readSnippet, stripHtml } from "./docs.ts";
@@ -66,7 +66,7 @@ export function registerTools(pi: ExtensionAPI, deps: ToolsDeps): void {
66
66
  name: "soly_read",
67
67
  label: "soly read",
68
68
  description:
69
- "Read a .soly/ artifact (state, plan, context, research, roadmap, requirements, project, milestone, task). `phase` targets a specific phase (default: current); `taskId` for the task artifact. Returns the file text.",
69
+ "Read a .agents/ artifact (state, plan, context, research, roadmap, requirements, project, milestone, task). `phase` targets a specific phase (default: current); `taskId` for the task artifact. Returns the file text.",
70
70
  parameters: Type.Object({
71
71
  artifact: StringEnum([
72
72
  "state",
@@ -129,7 +129,7 @@ export function registerTools(pi: ExtensionAPI, deps: ToolsDeps): void {
129
129
  if (!task) {
130
130
  return {
131
131
  content: [
132
- { type: "text", text: `soly: task ${taskId} not found in .soly/features/*/tasks/` },
132
+ { type: "text", text: `soly: task ${taskId} not found in .agents/features/*/tasks/` },
133
133
  ],
134
134
  details: { error: "task_not_found", taskId },
135
135
  };
@@ -189,7 +189,7 @@ export function registerTools(pi: ExtensionAPI, deps: ToolsDeps): void {
189
189
  name: "soly_log_decision",
190
190
  label: "soly log decision",
191
191
  description:
192
- "Append a one-line decision + rationale to the Decisions table in .soly/STATE.md (creates it if missing). For meaningful choices: scope cuts, library picks, trade-offs. `phase` defaults to current.",
192
+ "Append a one-line decision + rationale to the Decisions table in .agents/STATE.md (creates it if missing). For meaningful choices: scope cuts, library picks, trade-offs. `phase` defaults to current.",
193
193
  parameters: Type.Object({
194
194
  decision: Type.String({ description: "The decision made (one line)." }),
195
195
  rationale: Type.String({ description: "Why this decision was made (one line)." }),
@@ -329,7 +329,7 @@ export function registerTools(pi: ExtensionAPI, deps: ToolsDeps): void {
329
329
  name: "soly_todos",
330
330
  label: "soly todos",
331
331
  description:
332
- "Scan the tree for TODO/FIXME/HACK/XXX/NOTE comments, grouped by file (common source extensions; excludes node_modules/.git/dist/build/.soly). Needs ripgrep on PATH. `paths` overrides root, `limit` caps (default 200).",
332
+ "Scan the tree for TODO/FIXME/HACK/XXX/NOTE comments, grouped by file (common source extensions; excludes node_modules/.git/dist/build/.agents). Needs ripgrep on PATH. `paths` overrides root, `limit` caps (default 200).",
333
333
  parameters: Type.Object({
334
334
  paths: Type.Optional(
335
335
  Type.Array(Type.String(), {
@@ -356,7 +356,7 @@ export function registerTools(pi: ExtensionAPI, deps: ToolsDeps): void {
356
356
  "--glob=!node_modules/**",
357
357
  "--glob=!dist/**",
358
358
  "--glob=!build/**",
359
- "--glob=!.soly/**",
359
+ "--glob=!.agents/**",
360
360
  "--glob=!coverage/**",
361
361
  "-tts",
362
362
  "-tjs",
@@ -838,7 +838,7 @@ export function registerTools(pi: ExtensionAPI, deps: ToolsDeps): void {
838
838
  canonical_refs: Type.Optional(
839
839
  Type.Array(Type.String(), {
840
840
  description:
841
- "MANDATORY. Files the planner needs (intent docs, REQUIREMENTS, contracts), full paths from `.soly/`.",
841
+ "MANDATORY. Files the planner needs (intent docs, REQUIREMENTS, contracts), full paths from `.agents/`.",
842
842
  }),
843
843
  ),
844
844
  deferred_ideas: Type.Optional(
@@ -852,10 +852,10 @@ export function registerTools(pi: ExtensionAPI, deps: ToolsDeps): void {
852
852
  }),
853
853
  async execute(_id, params, _signal, _onUpdate, ctx) {
854
854
  // Locate phase dir
855
- const solyDir = path.join(ctx.cwd, ".soly");
855
+ const solyDir = solyDirFor(ctx.cwd);
856
856
  if (!fs.existsSync(solyDir)) {
857
857
  return {
858
- content: [{ type: "text", text: "soly_finish_discuss: no .soly/ in cwd" }],
858
+ content: [{ type: "text", text: "soly_finish_discuss: no .agents/ in cwd" }],
859
859
  details: { error: "no_soly" },
860
860
  };
861
861
  }
@@ -877,7 +877,7 @@ export function registerTools(pi: ExtensionAPI, deps: ToolsDeps): void {
877
877
  content: [
878
878
  {
879
879
  type: "text",
880
- text: `soly_finish_discuss: phase ${phaseNum} not found in .soly/phases/`,
880
+ text: `soly_finish_discuss: phase ${phaseNum} not found in .agents/phases/`,
881
881
  },
882
882
  ],
883
883
  details: { error: "no_phase", phase: phaseNum },
@@ -1021,11 +1021,11 @@ export function registerTools(pi: ExtensionAPI, deps: ToolsDeps): void {
1021
1021
  ),
1022
1022
  }),
1023
1023
  async execute(_id, params, _signal, _onUpdate, ctx) {
1024
- const solyDir = path.join(ctx.cwd, ".soly");
1024
+ const solyDir = solyDirFor(ctx.cwd);
1025
1025
  const phasesRoot = path.join(solyDir, "phases");
1026
1026
  if (!fs.existsSync(phasesRoot)) {
1027
1027
  return {
1028
- content: [{ type: "text", text: "soly_save_discuss_checkpoint: no .soly/phases/ in cwd" }],
1028
+ content: [{ type: "text", text: "soly_save_discuss_checkpoint: no .agents/phases/ in cwd" }],
1029
1029
  details: { error: "no_phases" },
1030
1030
  };
1031
1031
  }
package/util.ts CHANGED
@@ -16,7 +16,7 @@ import type { ProgressInfo, RuleFrontmatter } from "./core.ts";
16
16
  // Frontmatter parsers
17
17
  // ============================================================================
18
18
 
19
- // Simple parser for .soly/rules/ frontmatter.
19
+ // Simple parser for .agents/rules/ frontmatter.
20
20
  export function parseRuleFrontmatter(raw: string): {
21
21
  meta: RuleFrontmatter;
22
22
  body: string;
@@ -68,7 +68,7 @@ export function parseRuleFrontmatter(raw: string): {
68
68
  return { meta, body };
69
69
  }
70
70
 
71
- // YAML-ish parser for .soly/STATE.md. Handles 2-level nested objects (for `progress:`).
71
+ // YAML-ish parser for .agents/STATE.md. Handles 2-level nested objects (for `progress:`).
72
72
  export function parseStateFrontmatter(yaml: string): {
73
73
  meta: Record<string, unknown>;
74
74
  progress: ProgressInfo;
package/visual/welcome.ts CHANGED
@@ -10,7 +10,7 @@
10
10
  // soly adds plans · state · rules · workflows · ask_pro picker
11
11
  // project v1.12 · plan 2/5 — auth refactor · ≡ 4 rules · 2 docs
12
12
  // next → /execute
13
- // start here /soly-init · /plan · /soly · /why · /rules stats
13
+ // start here /soly init · /plan · /soly · /why · /rules stats
14
14
  // recent 1.11.2 … · 1.10.0 …
15
15
  //
16
16
  // pi is explicitly credited (engine) with soly as the framework on top. The
@@ -54,7 +54,7 @@ export const SOLY_ART: readonly string[] = [
54
54
  const TAGLINE = "the project framework · running on pi, the coding engine";
55
55
  const SOLY_ADDS = "plans · state · rules · workflows · ask_pro picker";
56
56
  const START_HERE: ReadonlyArray<[string, string]> = [
57
- ["/soly-init", "scaffold .agents/ in a new project"],
57
+ ["/soly init", "scaffold .agents/ in a new project"],
58
58
  ["/plan", "plan the current phase"],
59
59
  ["/soly", "state picker · /why · /rules stats"],
60
60
  ];
@@ -144,7 +144,7 @@ function bannerLines(opts: WelcomeOpts): string[] {
144
144
 
145
145
  /** The "project" row value, depending on whether a soly project exists. */
146
146
  function projectValue(input: WelcomeInput, styler: ChromeStyler): string {
147
- if (!input.hasProject) return styler.dim("no soly project here → /soly-init to scaffold");
147
+ if (!input.hasProject) return styler.dim("no soly project here → /soly init to scaffold");
148
148
  const bits = [styler.fg("accent", `v${input.version}`)];
149
149
  if (input.phaseLabel) bits.push(input.phaseLabel);
150
150
  if (input.rulesActive > 0) bits.push(`≡ ${input.rulesActive}`);
@@ -0,0 +1,203 @@
1
+ // =============================================================================
2
+ // workflows/done.ts — `soly done <type>/<name>` handler
3
+ // =============================================================================
4
+ //
5
+ // Direct workflow (no LLM transform): wraps up a plan on its branch.
6
+ // 1. Validates we're on the plan's branch (or any soly plan branch if
7
+ // `<type>/<name>` is omitted — see "implicit mode" below).
8
+ // 2. Commits any uncommitted changes (with a sensible Conventional
9
+ // Commits message if the user hasn't staged anything).
10
+ // 3. `git push -u origin <branch>` (or warn if no remote).
11
+ // 4. Tries `gh pr create --draft --fill` (or warns if `gh` not on PATH).
12
+ //
13
+ // STATE.md global sync (in main) is deferred to W4 — that's a separate
14
+ // concern about merge coordination.
15
+ // =============================================================================
16
+
17
+ import * as fs from "node:fs";
18
+ import { execFileSync } from "node:child_process";
19
+ import { parsePlanName, type SolyCommand } from "./parser.ts";
20
+ import type { SolyState } from "../core.js";
21
+
22
+ export interface DoneResult {
23
+ handled: boolean;
24
+ transformedText?: string;
25
+ completed?: {
26
+ branch: string;
27
+ commit: string;
28
+ pushed: boolean;
29
+ prUrl: string | null;
30
+ };
31
+ }
32
+
33
+ type Notifier = {
34
+ notify: (text: string, level?: "info" | "warning" | "error") => void;
35
+ };
36
+
37
+ function git(args: string[], opts: { cwd: string }): string {
38
+ try {
39
+ return execFileSync("git", args, {
40
+ cwd: opts.cwd,
41
+ encoding: "utf-8",
42
+ stdio: ["ignore", "pipe", "pipe"],
43
+ }).trim();
44
+ } catch (err) {
45
+ const e = err as { stderr?: Buffer | string; stdout?: Buffer | string; message?: string };
46
+ const stderr = e.stderr ? e.stderr.toString().trim() : "";
47
+ throw new Error(`git ${args.join(" ")} failed: ${stderr || e.message || "unknown error"}`);
48
+ }
49
+ }
50
+
51
+ /** Try `gh` and return stdout. Throws on failure. */
52
+ function gh(args: string[], opts: { cwd: string; ghPath?: string }): string {
53
+ const cmd = opts.ghPath ?? "gh";
54
+ try {
55
+ return execFileSync(cmd, args, {
56
+ cwd: opts.cwd,
57
+ encoding: "utf-8",
58
+ stdio: ["ignore", "pipe", "pipe"],
59
+ }).trim();
60
+ } catch (err) {
61
+ const e = err as { stderr?: Buffer | string; message?: string };
62
+ const stderr = e.stderr ? e.stderr.toString().trim() : "";
63
+ throw new Error(`gh ${args[0]} failed: ${stderr || e.message || "unknown error"}`);
64
+ }
65
+ }
66
+
67
+ /** ENOENT-style "command not found" -> throws a specific error. */
68
+ function ghAvailable(ghPath?: string): boolean {
69
+ const cmd = ghPath ?? "gh";
70
+ try {
71
+ execFileSync(cmd, ["--version"], { stdio: "ignore" });
72
+ return true;
73
+ } catch {
74
+ return false;
75
+ }
76
+ }
77
+
78
+ function reply(text: string): DoneResult {
79
+ return { handled: true, transformedText: text };
80
+ }
81
+
82
+ export function buildDoneTransform(
83
+ cmd: SolyCommand,
84
+ state: SolyState,
85
+ ui: Notifier,
86
+ projectRoot: string,
87
+ opts: { ghPath?: string } = {},
88
+ ): DoneResult {
89
+ if (!state.exists) {
90
+ return reply(`soly done: no .agents/ directory in cwd — run /soly init first.`);
91
+ }
92
+
93
+ const raw = cmd.args.join(" ").trim();
94
+ const parsed = parsePlanName(raw);
95
+ if ("error" in parsed) {
96
+ return reply(`soly done: ${parsed.error}\n\nUsage: soly done <type>/<name>`);
97
+ }
98
+ const { type, name } = parsed;
99
+ const branchName = `${type}/${name}`;
100
+
101
+ // 1. Preconditions
102
+ const currentBranch = git(["branch", "--show-current"], { cwd: projectRoot });
103
+ if (currentBranch !== branchName) {
104
+ return reply(
105
+ `soly done: currently on "${currentBranch}", not the plan branch "${branchName}".\n` +
106
+ `Switch first with \`git checkout ${branchName}\`.`,
107
+ );
108
+ }
109
+
110
+ // Check there's a remote
111
+ let hasRemote = false;
112
+ try {
113
+ git(["remote", "get-url", "origin"], { cwd: projectRoot });
114
+ hasRemote = true;
115
+ } catch {
116
+ // No remote — we'll skip push but still commit
117
+ }
118
+
119
+ // 2. Commit any pending changes (if anything to commit)
120
+ const statusBefore = git(["status", "--short"], { cwd: projectRoot });
121
+ let commitHash = git(["rev-parse", "HEAD"], { cwd: projectRoot });
122
+ if (statusBefore) {
123
+ // Auto-stage everything not under .agents/ (project files only)
124
+ try {
125
+ git(["add", "-A", "--", ":!.agents/"], { cwd: projectRoot });
126
+ // Re-check after add
127
+ const statusAfter = git(["status", "--short"], { cwd: projectRoot });
128
+ if (statusAfter) {
129
+ git(["commit", "-m", `${type}(${name}): wip`], { cwd: projectRoot });
130
+ }
131
+ } catch (err) {
132
+ const msg = err instanceof Error ? err.message : String(err);
133
+ return reply(`soly done: commit failed — ${msg}`);
134
+ }
135
+ commitHash = git(["rev-parse", "HEAD"], { cwd: projectRoot });
136
+ }
137
+
138
+ // 3. Push
139
+ let pushed = false;
140
+ if (hasRemote) {
141
+ try {
142
+ git(["push", "-u", "origin", branchName], { cwd: projectRoot });
143
+ pushed = true;
144
+ } catch (err) {
145
+ const msg = err instanceof Error ? err.message : String(err);
146
+ ui.notify(`soly done: push failed — ${msg}\nPlan was committed but not pushed. Push manually with \`git push -u origin ${branchName}\`.`, "warning");
147
+ return {
148
+ handled: true,
149
+ transformedText: `Plan ${branchName} committed locally (${commitHash.slice(0, 7)}), but push failed.\n${msg}`,
150
+ completed: { branch: branchName, commit: commitHash, pushed: false, prUrl: null },
151
+ };
152
+ }
153
+ } else {
154
+ ui.notify(`soly done: no 'origin' remote — committed locally only. Add a remote and \`git push\` when ready.`, "warning");
155
+ }
156
+
157
+ // 4. Draft PR via gh
158
+ let prUrl: string | null = null;
159
+ if (pushed && ghAvailable(opts.ghPath)) {
160
+ try {
161
+ prUrl = gh(
162
+ [
163
+ "pr",
164
+ "create",
165
+ "--draft",
166
+ "--fill",
167
+ "--head",
168
+ branchName,
169
+ ],
170
+ { cwd: projectRoot, ghPath: opts.ghPath },
171
+ );
172
+ } catch (err) {
173
+ // gh failed (maybe not authenticated, or PR already exists, etc.)
174
+ // Don't fail the whole workflow — just report.
175
+ const msg = err instanceof Error ? err.message : String(err);
176
+ ui.notify(
177
+ `soly done: pushed OK, but draft PR creation failed — ${msg}\n` +
178
+ `Run \`gh pr create --draft --fill\` manually.`,
179
+ "warning",
180
+ );
181
+ }
182
+ } else if (pushed) {
183
+ ui.notify(
184
+ `soly done: pushed OK, but \`gh\` CLI not found — draft PR not created.\n` +
185
+ `Install \`gh\` (https://cli.github.com) and run \`gh pr create --draft --fill\` manually.`,
186
+ "info",
187
+ );
188
+ }
189
+
190
+ // 5. Done — summarize
191
+ const prLine = prUrl ? `Draft PR: ${prUrl}` : "No draft PR (push or gh step skipped / failed).";
192
+ const notice =
193
+ `Plan ${branchName} done.\n` +
194
+ ` Commit: ${commitHash.slice(0, 7)}\n` +
195
+ ` Pushed: ${pushed ? "yes" : "no (no origin remote)"}\n` +
196
+ ` ${prLine}`;
197
+ ui.notify(notice, "info");
198
+ return {
199
+ handled: true,
200
+ transformedText: notice,
201
+ completed: { branch: branchName, commit: commitHash, pushed, prUrl },
202
+ };
203
+ }
@@ -73,7 +73,7 @@ export function buildExecuteTransform(
73
73
  return {
74
74
  handled: true,
75
75
  transformedText:
76
- `soly: no .soly/ directory found in cwd (${state.solyDir || "<cwd>"}) — cannot execute phase.\n` +
76
+ `soly: no .agents/ directory found in cwd (${state.solyDir || "<cwd>"}) — cannot execute phase.\n` +
77
77
  `Initialize a soly project first (see soly quickstart) before running "soly execute".`,
78
78
  };
79
79
  }
@@ -101,7 +101,7 @@ export function buildExecuteTransform(
101
101
  return {
102
102
  handled: true,
103
103
  transformedText:
104
- `soly execute: task ${target.taskId} not found in .soly/features/*/tasks/.\n` +
104
+ `soly execute: task ${target.taskId} not found in .agents/features/*/tasks/.\n` +
105
105
  `Known tasks: ${state.tasks.map((t) => t.id).join(", ") || "(none)"}\n` +
106
106
  `Tip: use the \`soly_list_tasks\` tool to see all available tasks.`,
107
107
  };
@@ -155,7 +155,7 @@ export function buildExecuteTransform(
155
155
  const featureDir = path.dirname(path.dirname(task.dir));
156
156
 
157
157
  // Write per-iteration context bundle (B2 of the soly design).
158
- // Worker reads this file first; no need to chase 6+ .soly/ files.
158
+ // Worker reads this file first; no need to chase 6+ .agents/ files.
159
159
  const iter = writeIterationContext({
160
160
  solyDir: state.solyDir,
161
161
  projectRoot,
@@ -179,7 +179,7 @@ export function buildExecuteTransform(
179
179
  **Iteration context file written:** \`${iter.relPath}\` (${iter.tokens} tokens, ${iter.bytes} bytes)
180
180
  The worker reads this file first — it contains intent, STATE, ROADMAP (n/a for tasks), the feature README, prior task SUMMARYs, and the current task PLAN.
181
181
 
182
- **0-POINT CHECK.** Worker must re-read .soly/docs/ (intent) and .soly/features/${task.feature}/README.md before implementing.
182
+ **0-POINT CHECK.** Worker must re-read .agents/docs/ (intent) and .agents/features/${task.feature}/README.md before implementing.
183
183
 
184
184
  Launch a single subagent for this work. Do NOT do the work inline.
185
185
 
@@ -206,12 +206,12 @@ Soly dir: ${state.solyDir}
206
206
  Feature dir: ${featureDir}
207
207
  Task dir: ${task.dir}
208
208
 
209
- **0-POINT CHECK — read .soly/docs/ first.**
209
+ **0-POINT CHECK — read .agents/docs/ first.**
210
210
  These are the project's INTENT (business context, design vision). Re-read them before implementing. If you find a conflict between intent and PLAN.md, flag it instead of silently choosing one.
211
211
 
212
- **Follow the worker self-audit gate (see .soly/rules/process/worker-audit.md):**
212
+ **Follow the worker self-audit gate (see .agents/rules/process/worker-audit.md):**
213
213
  1. Run \`dotnet build\` (or relevant build) — 0 warnings
214
- 2. Cross-check diff against .soly/rules/coding/*
214
+ 2. Cross-check diff against .agents/rules/coding/*
215
215
  3. Invoke \`analyzer-coach\` skill for any rule gaps
216
216
  4. Loop until clean (max 3 iterations)
217
217
  5. Commit (production-code commit(s))
@@ -224,10 +224,10 @@ ${workflow}
224
224
 
225
225
  Hard rules:
226
226
  - Do not skip the close-out order: production commits -> SUMMARY commit -> status: done.
227
- - Do not modify any .soly/rules/ files.
227
+ - Do not modify any .agents/rules/ files.
228
228
  - Do not run subagents yourself.
229
229
  - Do not start a task whose \`depends-on:\` lists tasks that are not \`done\`.
230
- - PATH DISCIPLINE: all files YOU create must live under \`.soly/\` (iteration, handoff, etc.) or under the project's source dirs. Never write to the project root.
230
+ - 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.
231
231
  - Return: changed files, commands run with exit codes, validation evidence, surprises, decisions needing parent approval.
232
232
  - Interactive-only rules are NOT in scope for you: ${interactiveRules.length > 0 ? interactiveRules.join(", ") : "(none)"}.
233
233
  \`
@@ -248,7 +248,7 @@ When the subagent completes, synthesize the result. Do not re-execute its work.
248
248
  handled: true,
249
249
  transformedText:
250
250
  target.kind === "all"
251
- ? `soly execute --all: no tasks found in .soly/features/*/tasks/.`
251
+ ? `soly execute --all: no tasks found in .agents/features/*/tasks/.`
252
252
  : `soly execute --feature ${target.feature}: no tasks found for that feature.`,
253
253
  };
254
254
  }
@@ -274,13 +274,102 @@ When the subagent completes, synthesize the result. Do not re-execute its work.
274
274
  };
275
275
  }
276
276
 
277
+ // === PLAN MODE (new dual-mode: `<type>/<name>` plans live under .agents/plans/<name>/) ===
278
+ if (target.kind === "plan") {
279
+ const planDirAbs = `${state.solyDir}/plans/${target.name}`;
280
+ const planFile = `${planDirAbs}/PLAN.md`;
281
+ let planBody: string;
282
+ try {
283
+ planBody = fs.readFileSync(planFile, "utf-8");
284
+ } catch {
285
+ return {
286
+ handled: true,
287
+ transformedText:
288
+ `soly execute: plan ${target.raw} has no PLAN.md at ${planFile}.\n` +
289
+ `Run \`soly plan ${target.raw}\` first to flesh it out.`,
290
+ };
291
+ }
292
+ const workflow = loadWorkflowMarkdown("execute-plan.md");
293
+ if (!workflow) {
294
+ return {
295
+ handled: true,
296
+ transformedText:
297
+ `soly execute: workflow markdown not found: workflows-data/execute-plan.md\n` +
298
+ `This is an extension installation issue — reinstall soly.`,
299
+ };
300
+ }
301
+ // Write per-iteration context bundle so the worker has intent + STATE
302
+ // + the plan body in one file.
303
+ const iter = writeIterationContext({
304
+ solyDir: state.solyDir,
305
+ projectRoot,
306
+ kind: "exec",
307
+ planName: target.name,
308
+ });
309
+ const instruction = `soly execute ${target.raw} — executing plan.
310
+
311
+ **Plan:** ${target.name}
312
+ **Branch:** ${target.raw}
313
+ **PLAN.md:** ${planFile}
314
+
315
+ **Iteration context file written:** \`${iter.relPath}\` (${iter.tokens} tokens)
316
+ The worker reads this file first — it contains intent, STATE, ROADMAP, the
317
+ plan body inline, and any prior SUMMARYs for this plan (none on first run).
318
+
319
+ **Inline plan body (so you have must-haves before reading the file):**
320
+ \`\`\`markdown
321
+ ${planBody.slice(0, 4000)}${planBody.length > 4000 ? "\n…(truncated)" : ""}
322
+ \`\`\`
323
+
324
+ **0-POINT CHECK.** Worker must re-read .agents/docs/ (intent) before implementing.
325
+
326
+ Launch a single subagent to execute the plan. Do NOT do the work inline.
327
+
328
+ subagent({
329
+ agent: ${JSON.stringify(opts.agent ?? "worker")},
330
+ context: "fresh",
331
+ async: true,
332
+ maxSubagentDepth: 1,
333
+ task: \`You are soly-executor. Execute the plan at \`${planFile}\` end-to-end.
334
+
335
+ **FIRST ACTION — read the iteration context file:**
336
+ \`\`\`
337
+ ${iter.relPath}
338
+ \`\`\`
339
+ It contains intent, STATE, ROADMAP, and the full plan body. Do NOT skip it.
340
+
341
+ Project root: ${projectRoot}
342
+ Soly dir: ${state.solyDir}
343
+ Plan dir: ${planDirAbs}
344
+
345
+ **0-POINT CHECK — read .agents/docs/ first.**
346
+
347
+ Follow the workflow below VERBATIM.
348
+
349
+ === WORKFLOW: execute-plan.md ===
350
+ ${workflow}
351
+ === END WORKFLOW ===
352
+
353
+ Hard rules:
354
+ - All work happens on branch \`${target.raw}\`. Do not switch branches.
355
+ - When the plan is fully executed and verified, write a SUMMARY.md next to
356
+ PLAN.md summarizing what was done, what was deferred, and any deviations.
357
+ - Do not commit unless the workflow tells you to; the user reviews and merges.
358
+ \`)
359
+ }`;
360
+ return { handled: true, transformedText: instruction };
361
+ }
362
+
277
363
  // === PHASE MODE ===
364
+ if (target.kind !== "phase") {
365
+ return { handled: false };
366
+ }
278
367
  const phase = state.phases.find((p) => p.number === target.phase);
279
368
  if (!phase) {
280
369
  return {
281
370
  handled: true,
282
371
  transformedText:
283
- `soly execute: phase ${target.phase} not found in .soly/phases/.\n` +
372
+ `soly execute: phase ${target.phase} not found in .agents/phases/.\n` +
284
373
  `Known phases: ${state.phases.map((p) => p.number).join(", ") || "(none)"}`,
285
374
  };
286
375
  }
@@ -358,8 +447,8 @@ The iteration context file lists all plans (their frontmatter) in section 6, gro
358
447
  **Iteration context file written:** \`${iter.relPath}\` (${iter.tokens} tokens, ${iter.bytes} bytes)
359
448
  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.
360
449
 
361
- **0-POINT CHECK — worker must read .soly/docs/ first.**
362
- These are the project's INTENT docs. The worker is about to implement tasks; if the implementation diverges from intent, it will be wrong even if the tests pass. Have the worker re-read .soly/docs/ (and any intent docs linked from PLAN.md) before each plan.
450
+ **0-POINT CHECK — worker must read .agents/docs/ first.**
451
+ 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.
363
452
 
364
453
  ${scopeBlock}
365
454
 
@@ -389,7 +478,7 @@ Project root: ${projectRoot}
389
478
  Soly dir: ${state.solyDir}
390
479
  Phase dir: ${phase.dir}
391
480
 
392
- **0-POINT CHECK — read .soly/docs/ first.**
481
+ **0-POINT CHECK — read .agents/docs/ first.**
393
482
  These are the project's INTENT (business context, design vision). Re-read them before implementing each plan. If you find a conflict between intent and PLAN.md, flag it instead of silently choosing one.
394
483
 
395
484
  Follow the workflow below VERBATIM — these are the user-approved soly instructions, not suggestions.
@@ -400,9 +489,9 @@ ${workflow}
400
489
 
401
490
  Hard rules:
402
491
  - Do not skip the close-out order: production commits -> SUMMARY commit -> STATE/ROADMAP update.
403
- - Do not modify any .soly/rules/ files.
492
+ - Do not modify any .agents/rules/ files.
404
493
  - Do not run subagents yourself.
405
- - PATH DISCIPLINE: all files YOU create must live under \`.soly/\` (e.g. .soly/iterations/, .soly/phases/<slug>/, .soly/HANDOFF.json) or under the project's source dirs. Never write PLAN/SUMMARY/CONTEXT/RESEARCH/iteration files to the project root.
494
+ - 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.
406
495
  - Return: changed files, commands run with exit codes, validation evidence, surprises, and any decisions needing parent approval.
407
496
  - 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.
408
497
  \`
@@ -23,8 +23,10 @@ import { buildResumeTransform } from "./resume.ts";
23
23
  import { showStatus, showLog, showDiff } from "./quick.ts";
24
24
  import { showDoctor, showIterations, showDiffIterations, showPhaseDelete, showTodos } from "./inspect.ts";
25
25
  import { buildPlanTransform, buildDiscussTransform } from "./planning.ts";
26
- import { createVerifyLoop, type VerifyState } from "./verify.ts";
26
+ import { buildNewTransform } from "./new.ts";
27
+ import { buildDoneTransform } from "./done.ts";
27
28
  import { buildMigrateTransform } from "./migrate.ts";
29
+ import { createVerifyLoop, type VerifyState } from "./verify.ts";
28
30
  import type { ContextManager } from "../context-manager.ts";
29
31
  import type { SolyState } from "../core.js";
30
32
  import type { SolyConfig } from "../config.js";
@@ -145,8 +147,25 @@ export function registerWorkflows(pi: ExtensionAPI, deps: WorkflowsDeps): void {
145
147
  return { action: "transform", text: result.transformedText };
146
148
  }
147
149
 
150
+ if (cmd.verb === "new") {
151
+ const result = buildNewTransform(cmd, state, ctx.ui, ctx.cwd);
152
+ if (!result.handled || !result.transformedText) return;
153
+ // Direct execution (the workflow already called ui.notify). The
154
+ // transformed text goes to the model so it can also tell the user
155
+ // in chat, but the action is "handled" (no LLM round-trip needed).
156
+ return { action: "transform", text: result.transformedText };
157
+ }
158
+
159
+ if (cmd.verb === "done") {
160
+ const result = buildDoneTransform(cmd, state, ctx.ui, ctx.cwd);
161
+ if (!result.handled || !result.transformedText) return;
162
+ // Direct execution — workflow already called ui.notify. The
163
+ // transformed text tells the LLM (and the user in chat) what happened.
164
+ return { action: "transform", text: result.transformedText };
165
+ }
166
+
148
167
  if (cmd.verb === "migrate") {
149
- const result = buildMigrateTransform(state);
168
+ const result = buildMigrateTransform(state, ctx.ui, ctx.cwd);
150
169
  if (!result.handled || !result.transformedText) return;
151
170
  return { action: "transform", text: result.transformedText };
152
171
  }
@@ -179,15 +198,12 @@ Lifecycle:
179
198
  Quick info (no LLM round-trip):
180
199
  status — position + progress + phases
181
200
  log [N] — last N decisions from STATE.md
182
- diff — git status + uncommitted .soly/ changes
201
+ diff — git status + uncommitted .agents/ changes
183
202
  doctor — health check (missing files, broken refs, stale iterations)
184
203
  iterations [N] — recent iteration bundles
185
204
  todos — pi-todo live list
186
205
  phase delete <N> — soft-delete a phase
187
206
 
188
- Maintenance:
189
- migrate — convert a legacy layout (NN-PLAN files / features/) to phases/<N>/tasks/
190
-
191
207
  State inspection lives on the slash form — \`/soly <sub>\`:
192
208
  position · state · plan · roadmap · progress · phases · tasks · task <id> ·
193
209
  features · milestone · context · research · config · reload
@@ -249,7 +265,7 @@ State inspection lives on the slash form — \`/soly <sub>\`:
249
265
  return {
250
266
  action: "transform",
251
267
  text: `soly phase — usage:
252
- soly phase delete <N> — soft-delete phase N (move to .soly/phases/.trash/)
268
+ soly phase delete <N> — soft-delete phase N (move to .agents/phases/.trash/)
253
269
  soly phase list — list all phases (same as /soly phases)
254
270
  soly phase <N> — alias for "soly plan <N>" (route through planner)`,
255
271
  };
@@ -265,8 +281,8 @@ State inspection lives on the slash form — \`/soly <sub>\`:
265
281
  pendingCompact = false;
266
282
  ctx.compact({
267
283
  customInstructions:
268
- "Session was paused via `soly compact`. Handoff files are in .soly/HANDOFF.json " +
269
- "and .soly/.continue-here.md. Preserve milestone/phase/plan position and key " +
284
+ "Session was paused via `soly compact`. Handoff files are in .agents/HANDOFF.json " +
285
+ "and .agents/.continue-here.md. Preserve milestone/phase/plan position and key " +
270
286
  "decisions in the summary. Drop implementation-detail noise.",
271
287
  onComplete: () => {
272
288
  ctx.ui.notify("soly: session compacted. Use `soly resume` to pick up.", "info");