pi-soly 1.13.5 → 1.14.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 +8 -9
- 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 +12 -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/execute.ts +16 -16
- package/workflows/index.ts +4 -14
- package/workflows/inspect.ts +16 -15
- package/workflows/parser.ts +2 -3
- package/workflows/pause.ts +5 -5
- package/workflows/planning.ts +18 -18
- 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/workflows/migrate.ts +0 -85
package/migrate.ts
DELETED
|
@@ -1,258 +0,0 @@
|
|
|
1
|
-
// =============================================================================
|
|
2
|
-
// migrate.ts — Atomic .soly/ → .agents/ migration
|
|
3
|
-
// =============================================================================
|
|
4
|
-
//
|
|
5
|
-
// Moves the legacy `.soly/` project state dir to the new vendor-neutral
|
|
6
|
-
// `.agents/` location. The move tries fs.rename first (atomic on the same
|
|
7
|
-
// filesystem). On Windows, rename often fails with EPERM because soly's
|
|
8
|
-
// hot-reload watcher (or an editor) holds open handles on .soly/ files.
|
|
9
|
-
// We retry the rename a few times, then fall back to recursive copy +
|
|
10
|
-
// delete if the handle won't release. Validates after move that key files
|
|
11
|
-
// made it.
|
|
12
|
-
//
|
|
13
|
-
// What gets moved (everything in `.soly/`):
|
|
14
|
-
// - ROADMAP.md
|
|
15
|
-
// - STATE.md
|
|
16
|
-
// - docs/ (intent docs)
|
|
17
|
-
// - rules/ (project rules)
|
|
18
|
-
// - phases/ (PLAN.md / SUMMARY.md / CONTEXT.md / RESEARCH.md)
|
|
19
|
-
// - iterations/ (per-execution context)
|
|
20
|
-
// - HANDOFF.json (pause snapshot)
|
|
21
|
-
// - .continue-here.md
|
|
22
|
-
// - config.json (per-project config — note: kept for backward compat
|
|
23
|
-
// reading; new writes go to .agents/config.json too)
|
|
24
|
-
//
|
|
25
|
-
// What does NOT get moved:
|
|
26
|
-
// - rotor persistence (no longer used in 1.4.0)
|
|
27
|
-
// - git history — `git mv` would preserve; fs.rename loses history. We
|
|
28
|
-
// suggest the user commit beforehand.
|
|
29
|
-
//
|
|
30
|
-
// Usage:
|
|
31
|
-
// /soly-migrate # confirm before doing
|
|
32
|
-
// /soly-migrate --yes # skip confirmation
|
|
33
|
-
// /soly-migrate --dry-run # show what would happen
|
|
34
|
-
// =============================================================================
|
|
35
|
-
|
|
36
|
-
import * as fs from "node:fs";
|
|
37
|
-
import * as path from "node:path";
|
|
38
|
-
import { promisify } from "node:util";
|
|
39
|
-
import { LEGACY_SOLY_DIRNAME, SOLY_DIRNAME } from "./core.js";
|
|
40
|
-
|
|
41
|
-
const renameAsync = promisify(fs.rename);
|
|
42
|
-
const rmAsync = promisify(fs.rm);
|
|
43
|
-
|
|
44
|
-
/**
|
|
45
|
-
* Cross-platform directory move.
|
|
46
|
-
*
|
|
47
|
-
* `fs.rename` is atomic on POSIX but on Windows it fails with EPERM if any
|
|
48
|
-
* file inside the source dir has an open handle (soly hot-reload watcher,
|
|
49
|
-
* editor, antivirus, the cwd itself). We:
|
|
50
|
-
*
|
|
51
|
-
* 1. Retry rename up to 5 times with 200ms backoff — most EPERM are
|
|
52
|
-
* transient (OS releasing a handle).
|
|
53
|
-
* 2. Fall back to recursive copy-then-delete if rename keeps failing.
|
|
54
|
-
* Slower but works when a handle is genuinely held for the duration.
|
|
55
|
-
*/
|
|
56
|
-
async function moveDir(from: string, to: string): Promise<void> {
|
|
57
|
-
let lastErr: unknown;
|
|
58
|
-
for (let attempt = 0; attempt < 5; attempt++) {
|
|
59
|
-
try {
|
|
60
|
-
await renameAsync(from, to);
|
|
61
|
-
return;
|
|
62
|
-
} catch (err) {
|
|
63
|
-
lastErr = err;
|
|
64
|
-
const code = (err as NodeJS.ErrnoException).code;
|
|
65
|
-
if (code !== "EPERM" && code !== "EBUSY" && code !== "EACCES") throw err;
|
|
66
|
-
await new Promise((r) => setTimeout(r, 200 * (attempt + 1)));
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
// Rename exhausted — fall back to copy + delete.
|
|
70
|
-
// This works even when handles are held because we open files read-only
|
|
71
|
-
// for the copy and the source is removed last.
|
|
72
|
-
await copyDirRecursive(from, to);
|
|
73
|
-
try {
|
|
74
|
-
await rmAsync(from, { recursive: true, force: true });
|
|
75
|
-
} catch (err) {
|
|
76
|
-
// Source dir couldn't be removed (handle still held). The migration
|
|
77
|
-
// itself succeeded — .agents/ has everything. Throw a soft warning so
|
|
78
|
-
// the caller can tell the user to delete .soly/ manually.
|
|
79
|
-
throw new Error(
|
|
80
|
-
`migrated to ${to} but could not remove ${from}: ${(err as Error).message}. ` +
|
|
81
|
-
`Delete ${from} manually after closing editors/pi.`,
|
|
82
|
-
);
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
function copyDirRecursive(src: string, dest: string): Promise<void> {
|
|
87
|
-
return new Promise((resolve, reject) => {
|
|
88
|
-
fs.mkdirSync(dest, { recursive: true });
|
|
89
|
-
const entries = fs.readdirSync(src, { withFileTypes: true });
|
|
90
|
-
let pending = entries.length;
|
|
91
|
-
if (pending === 0) return resolve();
|
|
92
|
-
let rejected = false;
|
|
93
|
-
const done = (err?: Error) => {
|
|
94
|
-
if (rejected) return;
|
|
95
|
-
if (err) {
|
|
96
|
-
rejected = true;
|
|
97
|
-
reject(err);
|
|
98
|
-
return;
|
|
99
|
-
}
|
|
100
|
-
pending -= 1;
|
|
101
|
-
if (pending === 0) resolve();
|
|
102
|
-
};
|
|
103
|
-
for (const entry of entries) {
|
|
104
|
-
const s = path.join(src, entry.name);
|
|
105
|
-
const d = path.join(dest, entry.name);
|
|
106
|
-
if (entry.isDirectory()) {
|
|
107
|
-
copyDirRecursive(s, d).then(() => done(), done);
|
|
108
|
-
} else if (entry.isSymbolicLink()) {
|
|
109
|
-
try {
|
|
110
|
-
fs.symlinkSync(fs.readlinkSync(s), d);
|
|
111
|
-
done();
|
|
112
|
-
} catch (e) {
|
|
113
|
-
done(e as Error);
|
|
114
|
-
}
|
|
115
|
-
} else {
|
|
116
|
-
fs.copyFile(s, d, (e) => done(e ? new Error(e.message) : undefined));
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
});
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
/** UI primitives needed by the migration. */
|
|
123
|
-
export interface MigrateUI {
|
|
124
|
-
notify: (text: string, level?: "info" | "warning" | "error") => void;
|
|
125
|
-
confirm: (title: string, message: string) => Promise<boolean>;
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
export interface MigrateOptions {
|
|
129
|
-
/** Skip the confirmation dialog. */
|
|
130
|
-
autoYes?: boolean;
|
|
131
|
-
/** Don't actually move; just report. */
|
|
132
|
-
dryRun?: boolean;
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
export interface MigrateResult {
|
|
136
|
-
/** Whether the move happened (false if cancelled or skipped). */
|
|
137
|
-
moved: boolean;
|
|
138
|
-
/** What was moved (file/dir names relative to cwd). */
|
|
139
|
-
relocated: string[];
|
|
140
|
-
/** Warnings during validation. */
|
|
141
|
-
warnings: string[];
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
/** What we expect to find in `.soly/`. Listed for dry-run reporting. */
|
|
145
|
-
const EXPECTED_ENTRIES = [
|
|
146
|
-
"ROADMAP.md",
|
|
147
|
-
"STATE.md",
|
|
148
|
-
"docs",
|
|
149
|
-
"rules",
|
|
150
|
-
"phases",
|
|
151
|
-
"iterations",
|
|
152
|
-
"HANDOFF.json",
|
|
153
|
-
".continue-here.md",
|
|
154
|
-
"config.json",
|
|
155
|
-
] as const;
|
|
156
|
-
|
|
157
|
-
/** Top-level move function. Returns a result so tests can assert. */
|
|
158
|
-
export async function migrateSolyDir(
|
|
159
|
-
cwd: string,
|
|
160
|
-
ui: MigrateUI,
|
|
161
|
-
options: MigrateOptions = {},
|
|
162
|
-
): Promise<MigrateResult> {
|
|
163
|
-
const from = path.join(cwd, LEGACY_SOLY_DIRNAME);
|
|
164
|
-
const to = path.join(cwd, SOLY_DIRNAME);
|
|
165
|
-
const result: MigrateResult = { moved: false, relocated: [], warnings: [] };
|
|
166
|
-
|
|
167
|
-
// Preconditions
|
|
168
|
-
if (!fs.existsSync(from)) {
|
|
169
|
-
ui.notify(`soly-migrate: no ${LEGACY_SOLY_DIRNAME}/ to migrate (already on ${SOLY_DIRNAME}/)`, "info");
|
|
170
|
-
return result;
|
|
171
|
-
}
|
|
172
|
-
if (fs.existsSync(to)) {
|
|
173
|
-
ui.notify(
|
|
174
|
-
`soly-migrate: both ${LEGACY_SOLY_DIRNAME}/ and ${SOLY_DIRNAME}/ exist. ` +
|
|
175
|
-
`Resolve manually before migrating.`,
|
|
176
|
-
"error",
|
|
177
|
-
);
|
|
178
|
-
return result;
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
// Inventory what's in .soly/
|
|
182
|
-
let entries: string[];
|
|
183
|
-
try {
|
|
184
|
-
entries = fs.readdirSync(from);
|
|
185
|
-
} catch (err) {
|
|
186
|
-
ui.notify(`soly-migrate: cannot read ${from}: ${(err as Error).message}`, "error");
|
|
187
|
-
return result;
|
|
188
|
-
}
|
|
189
|
-
const inventory = entries.filter((e) => !e.startsWith("."));
|
|
190
|
-
result.relocated = inventory;
|
|
191
|
-
|
|
192
|
-
// Dry-run: report and exit
|
|
193
|
-
if (options.dryRun) {
|
|
194
|
-
ui.notify(
|
|
195
|
-
`soly-migrate (dry run): would move ${inventory.length} entries from ` +
|
|
196
|
-
`${LEGACY_SOLY_DIRNAME}/ to ${SOLY_DIRNAME}/:\n - ${inventory.join("\n - ")}`,
|
|
197
|
-
"info",
|
|
198
|
-
);
|
|
199
|
-
return { ...result, relocated: inventory };
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
// Confirm with the user
|
|
203
|
-
if (!options.autoYes) {
|
|
204
|
-
const ok = await ui.confirm(
|
|
205
|
-
`soly migrate ${LEGACY_SOLY_DIRNAME}/ → ${SOLY_DIRNAME}/`,
|
|
206
|
-
`Move ${inventory.length} entries (${inventory.slice(0, 5).join(", ")}${inventory.length > 5 ? ", …" : ""})? ` +
|
|
207
|
-
`This is atomic on the same filesystem but does NOT preserve git history. ` +
|
|
208
|
-
`Recommend committing ${LEGACY_SOLY_DIRNAME}/ separately first if you care about history.`,
|
|
209
|
-
);
|
|
210
|
-
if (!ok) {
|
|
211
|
-
ui.notify("soly-migrate: cancelled", "info");
|
|
212
|
-
return result;
|
|
213
|
-
}
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
// Do the move (with Windows EPERM retry + copy fallback)
|
|
217
|
-
try {
|
|
218
|
-
await moveDir(from, to);
|
|
219
|
-
} catch (err) {
|
|
220
|
-
const msg = (err as Error).message;
|
|
221
|
-
const isPartial = msg.includes("Delete ") || msg.includes("could not remove");
|
|
222
|
-
ui.notify(
|
|
223
|
-
isPartial
|
|
224
|
-
? `soly-migrate: ${msg}`
|
|
225
|
-
: `soly-migrate: rename failed: ${msg}. Original .soly/ untouched.`,
|
|
226
|
-
isPartial ? "warning" : "error",
|
|
227
|
-
);
|
|
228
|
-
return result;
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
// Validate the result
|
|
232
|
-
if (!fs.existsSync(to)) {
|
|
233
|
-
ui.notify(`soly-migrate: post-rename check failed — ${SOLY_DIRNAME}/ missing`, "error");
|
|
234
|
-
return result;
|
|
235
|
-
}
|
|
236
|
-
for (const expected of EXPECTED_ENTRIES) {
|
|
237
|
-
const src = path.join(from, expected);
|
|
238
|
-
const dst = path.join(to, expected);
|
|
239
|
-
// Only check items that existed before; some are optional
|
|
240
|
-
if (fs.existsSync(src) && !fs.existsSync(dst)) {
|
|
241
|
-
result.warnings.push(`missing after move: ${expected}`);
|
|
242
|
-
}
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
result.moved = true;
|
|
246
|
-
|
|
247
|
-
// Success message + git hint
|
|
248
|
-
const warnPart = result.warnings.length > 0
|
|
249
|
-
? `\n\nWarnings:\n - ${result.warnings.join("\n - ")}`
|
|
250
|
-
: "";
|
|
251
|
-
ui.notify(
|
|
252
|
-
`soly-migrate: done. ${inventory.length} entries moved to ${SOLY_DIRNAME}/.${warnPart}\n\n` +
|
|
253
|
-
`Recommended next:\n` +
|
|
254
|
-
` git add -A && git commit -m "migrate soly state to .agents/"`,
|
|
255
|
-
"info",
|
|
256
|
-
);
|
|
257
|
-
return result;
|
|
258
|
-
}
|
package/workflows/migrate.ts
DELETED
|
@@ -1,85 +0,0 @@
|
|
|
1
|
-
// =============================================================================
|
|
2
|
-
// workflows/migrate.ts — `soly migrate`: legacy layout → unified (LLM-runnable)
|
|
3
|
-
// =============================================================================
|
|
4
|
-
//
|
|
5
|
-
// The unified model is `phases/<N>/tasks/<id>/PLAN.md`. Two legacy shapes need
|
|
6
|
-
// converting:
|
|
7
|
-
// - standalone plan files `phases/<N>/<NN-MM>-PLAN.md` (+ *-SUMMARY.md)
|
|
8
|
-
// - the separate features model `features/<f>/tasks/<id>/`
|
|
9
|
-
//
|
|
10
|
-
// Conversion is semantic (derive a task id/kind/deps from a plan), so we hand
|
|
11
|
-
// the model a careful, verify-before-delete protocol rather than doing blind
|
|
12
|
-
// file moves in the extension. Returns a transform the LLM executes inline.
|
|
13
|
-
// =============================================================================
|
|
14
|
-
|
|
15
|
-
import type { SolyState } from "../core.ts";
|
|
16
|
-
|
|
17
|
-
export type MigrateResult = { handled: boolean; transformedText?: string };
|
|
18
|
-
|
|
19
|
-
/** Phases that still use legacy standalone plan files and have no tasks/ yet. */
|
|
20
|
-
function legacyPhases(state: SolyState): string[] {
|
|
21
|
-
return state.phases
|
|
22
|
-
.filter((p) => p.plans.length > 0 && (!p.tasks || p.tasks.length === 0))
|
|
23
|
-
.map((p) => `${p.slug} (${p.plans.length} plan${p.plans.length === 1 ? "" : "s"})`);
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
export function buildMigrateTransform(state: SolyState): MigrateResult {
|
|
27
|
-
if (!state.exists) {
|
|
28
|
-
return {
|
|
29
|
-
handled: true,
|
|
30
|
-
transformedText:
|
|
31
|
-
`soly migrate: no .soly/ project in this directory — nothing to migrate.`,
|
|
32
|
-
};
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
const phases = legacyPhases(state);
|
|
36
|
-
const features = state.features.map((f) => `${f.slug} (${f.taskCount} task${f.taskCount === 1 ? "" : "s"})`);
|
|
37
|
-
|
|
38
|
-
if (phases.length === 0 && features.length === 0) {
|
|
39
|
-
return {
|
|
40
|
-
handled: true,
|
|
41
|
-
transformedText:
|
|
42
|
-
`soly migrate: already on the unified model (phase = group of tasks). Nothing to migrate.`,
|
|
43
|
-
};
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
const detected: string[] = [];
|
|
47
|
-
if (phases.length > 0) detected.push(`- Legacy plan files in: ${phases.join(", ")}`);
|
|
48
|
-
if (features.length > 0) detected.push(`- Legacy \`features/\` dirs: ${features.join(", ")}`);
|
|
49
|
-
|
|
50
|
-
return {
|
|
51
|
-
handled: true,
|
|
52
|
-
transformedText: `soly migrate — convert the legacy layout to the unified model (phase = group of tasks).
|
|
53
|
-
|
|
54
|
-
Target layout:
|
|
55
|
-
.soly/phases/<N>/tasks/<task-id>/PLAN.md (frontmatter: id, kind, status, depends-on, feature?)
|
|
56
|
-
.soly/phases/<N>/tasks/<task-id>/SUMMARY.md
|
|
57
|
-
|
|
58
|
-
Detected legacy artifacts:
|
|
59
|
-
${detected.join("\n")}
|
|
60
|
-
|
|
61
|
-
Do the conversion YOURSELF, carefully, one item at a time. NEVER delete an old file until the new one is written and you've confirmed its contents. Use \`soly_read\` / \`soly_snippet\` to read, your file tools to write/move.
|
|
62
|
-
|
|
63
|
-
**A. For each legacy plan \`phases/<N>/<NN-MM>-PLAN.md\`:**
|
|
64
|
-
1. Read it. Derive a stable task id \`<short-slug>-<4hex>\` from the plan's title/intent (lowercase, hyphenated, e.g. \`auth-login-a3f9\`).
|
|
65
|
-
2. Write \`phases/<N>/tasks/<id>/PLAN.md\` with frontmatter, then the original body unchanged:
|
|
66
|
-
\`\`\`
|
|
67
|
-
---
|
|
68
|
-
id: <id>
|
|
69
|
-
kind: <be|fe|infra|docs|test> # infer from the plan
|
|
70
|
-
status: <ready|done> # done if a matching *-SUMMARY.md exists
|
|
71
|
-
depends-on: [] # fill if the plan depends on earlier plans/tasks
|
|
72
|
-
---
|
|
73
|
-
<original plan body>
|
|
74
|
-
\`\`\`
|
|
75
|
-
3. If a matching \`<NN-MM>-SUMMARY.md\` exists, move it to \`phases/<N>/tasks/<id>/SUMMARY.md\`.
|
|
76
|
-
4. Verify the new files exist and read correctly, THEN delete the old \`<NN-MM>-PLAN.md\` and its \`*-SUMMARY.md\`.
|
|
77
|
-
|
|
78
|
-
**B. For each \`features/<f>/tasks/<id>/\`:**
|
|
79
|
-
1. Decide which phase it belongs to (ask the user with \`ask_pro\` if it's not obvious from the task/feature).
|
|
80
|
-
2. Move the whole task dir to \`phases/<N>/tasks/<id>/\` and add \`feature: <f>\` to its PLAN.md frontmatter.
|
|
81
|
-
3. When a feature's tasks are all moved, remove the now-empty \`features/<f>/\`.
|
|
82
|
-
|
|
83
|
-
**Close out:** update any path references in STATE.md / ROADMAP.md, run \`soly status\` and \`soly doctor\` to confirm the new layout loads, then commit. Report a short summary of what moved (counts + any tasks you couldn't place).`,
|
|
84
|
-
};
|
|
85
|
-
}
|