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/README.md +29 -15
- package/artifact/index.ts +3 -3
- package/artifact/render.ts +2 -2
- package/ask/README.md +3 -1
- package/ask/index.ts +16 -2
- package/codemap.ts +2 -2
- package/commands.ts +34 -63
- package/config.ts +20 -12
- package/core.ts +16 -18
- package/docs.ts +4 -4
- package/index.ts +20 -21
- package/init.ts +7 -7
- package/intent.ts +12 -12
- package/iteration.ts +14 -12
- package/notification.ts +1 -1
- package/notifications-log.ts +2 -2
- package/nudge.ts +41 -6
- package/package.json +1 -2
- package/skills/soly-framework/SKILL.md +34 -42
- package/status.ts +2 -2
- package/tools.ts +14 -14
- package/util.ts +2 -2
- package/visual/welcome.ts +3 -3
- package/workflows/done.ts +203 -0
- package/workflows/execute.ts +105 -16
- package/workflows/index.ts +25 -9
- package/workflows/inspect.ts +16 -15
- package/workflows/migrate.ts +150 -63
- package/workflows/new.ts +159 -0
- package/workflows/parser.ts +52 -1
- package/workflows/pause.ts +5 -5
- package/workflows/planning.ts +107 -22
- package/workflows/quick.ts +7 -7
- package/workflows/resume.ts +14 -14
- package/workflows-data/discuss-phase.md +9 -9
- package/workflows-data/execute-phase.md +4 -4
- package/workflows-data/execute-plan.md +8 -8
- package/workflows-data/execute-task.md +7 -7
- package/workflows-data/pause-work.md +18 -18
- package/workflows-data/plan-phase.md +7 -7
- package/workflows-data/plan-task.md +18 -18
- package/migrate.ts +0 -258
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/
|
|
60
|
-
lines.push(" → run /soly
|
|
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 .
|
|
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>/.
|
|
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 .
|
|
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 .
|
|
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 .
|
|
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/.
|
|
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=!.
|
|
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 `.
|
|
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 =
|
|
855
|
+
const solyDir = solyDirFor(ctx.cwd);
|
|
856
856
|
if (!fs.existsSync(solyDir)) {
|
|
857
857
|
return {
|
|
858
|
-
content: [{ type: "text", text: "soly_finish_discuss: no .
|
|
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 .
|
|
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 =
|
|
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 .
|
|
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 .
|
|
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 .
|
|
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
|
|
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
|
|
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
|
|
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
|
+
}
|
package/workflows/execute.ts
CHANGED
|
@@ -73,7 +73,7 @@ export function buildExecuteTransform(
|
|
|
73
73
|
return {
|
|
74
74
|
handled: true,
|
|
75
75
|
transformedText:
|
|
76
|
-
`soly: no .
|
|
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 .
|
|
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+ .
|
|
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 .
|
|
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 .
|
|
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 .
|
|
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 .
|
|
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 .
|
|
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 \`.
|
|
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 .
|
|
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 .
|
|
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 .
|
|
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 .
|
|
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 .
|
|
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 .
|
|
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 \`.
|
|
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
|
\`
|
package/workflows/index.ts
CHANGED
|
@@ -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 {
|
|
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 .
|
|
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 .
|
|
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 .
|
|
269
|
-
"and .
|
|
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");
|