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
|
@@ -1,36 +1,36 @@
|
|
|
1
1
|
# Pause Work
|
|
2
2
|
|
|
3
|
-
<purpose>Create `.
|
|
3
|
+
<purpose>Create `.agents/HANDOFF.json` (machine-readable) and a `.continue-here.md` (human-readable) to preserve work state across sessions. `soly resume` consumes both.</purpose>
|
|
4
4
|
|
|
5
|
-
<read_first>.
|
|
5
|
+
<read_first>.agents/STATE.md (current position) · the most recent `.agents/phases/*/SUMMARY.md` if any</read_first>
|
|
6
6
|
|
|
7
7
|
<process>
|
|
8
8
|
|
|
9
|
-
**1. Detect context.** First match wins; nothing detectable → `.
|
|
9
|
+
**1. Detect context.** First match wins; nothing detectable → `.agents/.continue-here.md` (note ambiguity in `<current_state>`).
|
|
10
10
|
|
|
11
11
|
```bash
|
|
12
|
-
phase=$(ls -t .
|
|
12
|
+
phase=$(ls -t .agents/phases/*/PLAN.md 2>/dev/null | head -1)
|
|
13
13
|
phase_slug=$(echo "$phase" | grep -oP 'phases/\K[^/]+')
|
|
14
|
-
spike=$(ls -td .
|
|
15
|
-
sketch=$(ls -td .
|
|
16
|
-
deliberation=$(ls .
|
|
14
|
+
spike=$(ls -td .agents/spikes/*/ 2>/dev/null | head -1)
|
|
15
|
+
sketch=$(ls -td .agents/sketches/*/ 2>/dev/null | head -1)
|
|
16
|
+
deliberation=$(ls .agents/deliberations/*.md 2>/dev/null | head -1)
|
|
17
17
|
```
|
|
18
18
|
|
|
19
19
|
| detected | handoff path |
|
|
20
20
|
|---|---|
|
|
21
|
-
| phase_slug | `.
|
|
22
|
-
| spike | `.
|
|
23
|
-
| sketch | `.
|
|
24
|
-
| deliberation | `.
|
|
25
|
-
| (notes only) | `.
|
|
21
|
+
| phase_slug | `.agents/phases/<phase_slug>/.continue-here.md` |
|
|
22
|
+
| spike | `.agents/spikes/<spike_slug>/.continue-here.md` |
|
|
23
|
+
| sketch | `.agents/sketches/<sketch_slug>/.continue-here.md` |
|
|
24
|
+
| deliberation | `.agents/deliberations/.continue-here.md` |
|
|
25
|
+
| (notes only) | `.agents/.continue-here.md` |
|
|
26
26
|
|
|
27
27
|
**2. Gather state.** Walk the conversation + recent diffs. Collect: current position (phase/plan/task + paths), work done this session (artifacts, not aspirations), work remaining, decisions (only those future-you must not re-litigate), blockers, human actions pending (API keys, approvals, manual tests), background processes, uncommitted files (`git status --porcelain`), and blocking constraints (anti-patterns discovered through actual failure — each tagged `blocking` or `advisory`).
|
|
28
28
|
|
|
29
29
|
If anything is ambiguous, return a `## Clarifications Needed` block — do not invent.
|
|
30
30
|
|
|
31
|
-
**3. Check for false completions.** `grep -l "To be filled\|placeholder\|TBD" .
|
|
31
|
+
**3. Check for false completions.** `grep -l "To be filled\|placeholder\|TBD" .agents/phases/*/*.md 2>/dev/null` — report matches as incomplete.
|
|
32
32
|
|
|
33
|
-
**4. Write `.
|
|
33
|
+
**4. Write `.agents/HANDOFF.json`.** Timestamp via `date -u +"%Y-%m-%dT%H:%M:%SZ"` (no SDK).
|
|
34
34
|
|
|
35
35
|
```json
|
|
36
36
|
{
|
|
@@ -98,7 +98,7 @@ _If none, remove this section._
|
|
|
98
98
|
|
|
99
99
|
## Required Reading (in order)
|
|
100
100
|
1. <doc> — <why>
|
|
101
|
-
2. `.
|
|
101
|
+
2. `.agents/METHODOLOGY.md` if it exists — project lenses
|
|
102
102
|
|
|
103
103
|
## Infrastructure State
|
|
104
104
|
- <service/env>: <state>
|
|
@@ -117,14 +117,14 @@ Specific enough that a fresh worker can resume without re-deriving from git hist
|
|
|
117
117
|
|
|
118
118
|
**6. Commit:**
|
|
119
119
|
```bash
|
|
120
|
-
git add .
|
|
120
|
+
git add .agents/HANDOFF.json <handoff-md-path>
|
|
121
121
|
git commit -m "chore(soly): pause work — create handoff"
|
|
122
122
|
```
|
|
123
123
|
|
|
124
124
|
**7. Return:**
|
|
125
125
|
```
|
|
126
126
|
Handoff created:
|
|
127
|
-
- .
|
|
127
|
+
- .agents/HANDOFF.json
|
|
128
128
|
- <handoff-md-path>
|
|
129
129
|
State: <context>, <location>, task <x>/<m>, in_progress, <n> blockers (<m> human)
|
|
130
130
|
Resume: `soly resume`
|
|
@@ -136,7 +136,7 @@ Resume: `soly resume`
|
|
|
136
136
|
- No production code. Handoff only.
|
|
137
137
|
- A summary with `TBD`/`placeholder`/`To be filled` is incomplete — report it.
|
|
138
138
|
- `null` is JSON `null`, not the string `"null"`.
|
|
139
|
-
- Do not modify `.
|
|
139
|
+
- Do not modify `.agents/rules/`. Do not run subagents (you ARE one).
|
|
140
140
|
- Commit message: `chore(soly): pause work — create handoff`.
|
|
141
141
|
- Return: paths, state summary, blocker count, `soly resume` command.
|
|
142
142
|
</hard_rules>
|
|
@@ -3,12 +3,12 @@
|
|
|
3
3
|
<purpose>Produce one or more PLAN.md files for a phase — each a self-contained contract (requirements, must_haves, tasks with runnable acceptance criteria) the executor can run standalone. Pre-compute wave numbers encoding the dependency graph. Update STATE.md.</purpose>
|
|
4
4
|
|
|
5
5
|
<path_discipline>
|
|
6
|
-
**All soly-managed files live under `.
|
|
6
|
+
**All soly-managed files live under `.agents/`.** Never write PLAN.md, CONTEXT.md, RESEARCH.md, SUMMARY.md, iteration files, or handoffs to the project root. All phase files go in `.agents/phases/<NN>-<slug>/`. Use absolute paths (or paths starting with `$SOLY_DIR`) — never bare relative names that could land in cwd.
|
|
7
7
|
|
|
8
8
|
The iteration context file (path given by the parent in the task prompt) is your single source of truth for intent, STATE, ROADMAP, and any existing phase artifacts. Read it FIRST, before any of your own `read` or `ls` calls.
|
|
9
9
|
</path_discipline>
|
|
10
10
|
|
|
11
|
-
<read_first>`.
|
|
11
|
+
<read_first>`.agents/docs/` (INTENT, 0-point) · `.agents/STATE.md` (current position) · `.agents/ROADMAP.md` (this phase's row) · `.agents/REQUIREMENTS.md` if exists · `<phase>-CONTEXT.md` if exists (user decisions from `soly discuss`) · `<phase>-RESEARCH.md` if exists (chosen libs/patterns) · up to 3 most recent prior `*-SUMMARY.md` (avoid re-implementing). If `.agents/` missing → stop + error.</read_first>
|
|
12
12
|
|
|
13
13
|
<git_branch_invariant>
|
|
14
14
|
**Do not create, rename, or switch git branches.** The branch was established at `soly discuss` and is owned by the user's git workflow. Verify the current branch matches expectations, but do NOT change it.
|
|
@@ -81,7 +81,7 @@ PHASE="$1"
|
|
|
81
81
|
# would fall back to the `write` tool with a relative path, polluting
|
|
82
82
|
# the project root).
|
|
83
83
|
PROJECT_ROOT="$(pwd)"
|
|
84
|
-
SOLY_DIR="$PROJECT_ROOT/.
|
|
84
|
+
SOLY_DIR="$PROJECT_ROOT/.agents"
|
|
85
85
|
PHASE_DIR=$(ls -d "$SOLY_DIR/phases/"*"-$PHASE-"* 2>/dev/null | head -1) || { echo "Phase $PHASE not found" >&2; exit 1; }
|
|
86
86
|
PADDED_PHASE=$(printf "%02d" "$(echo "$PHASE" | grep -oE '^[0-9]+' | sed 's/^0*//')")
|
|
87
87
|
PHASE_SLUG=$(basename "$PHASE_DIR")
|
|
@@ -103,8 +103,8 @@ HAS_SUMMARIES=$(ls "$PHASE_DIR"/${PADDED_PHASE}-*-SUMMARY.md 2>/dev/null | wc -l
|
|
|
103
103
|
**3. Read phase context** (also in the iteration file) in priority order:
|
|
104
104
|
1. `<phase>-CONTEXT.md` if exists — user decisions, honor them. Missing decision in an area you need to plan → surface in report.
|
|
105
105
|
2. `<phase>-RESEARCH.md` if exists — chosen libs/patterns. Pitfalls → Must Haves or task body.
|
|
106
|
-
3. `.
|
|
107
|
-
4. `.
|
|
106
|
+
3. `.agents/ROADMAP.md` — this phase's row: goal, requirements, deps on other phases.
|
|
107
|
+
4. `.agents/REQUIREMENTS.md` if exists — full text + acceptance criteria for each `phase_req_ids`. Every requirement → some plan's `requirements:` array.
|
|
108
108
|
5. Up to 3 most recent prior `*-SUMMARY.md` — what was already built.
|
|
109
109
|
|
|
110
110
|
**4. Decompose into plans.**
|
|
@@ -156,7 +156,7 @@ git commit -m "chore(${PADDED_PHASE}): plan phase <N> — <M> plan(s)"
|
|
|
156
156
|
- Phase status → `Planned`, `current_plan` → 1.
|
|
157
157
|
- `last_updated` → `date -u +"%Y-%m-%dT%H:%M:%SZ"`.
|
|
158
158
|
- If STATE.md has `progress:`, increment `total_plans` by new plan count.
|
|
159
|
-
- Keep < 150 lines — archive to `.
|
|
159
|
+
- Keep < 150 lines — archive to `.agents/DECISIONS-INDEX.md` if it grows.
|
|
160
160
|
|
|
161
161
|
**9. Report:**
|
|
162
162
|
|
|
@@ -188,7 +188,7 @@ Parent summarizes + asks confirmation. On confirm: `soly execute-plan <N>` (or `
|
|
|
188
188
|
|
|
189
189
|
<hard_rules>
|
|
190
190
|
- No production code. Planning only.
|
|
191
|
-
- No subagents (you ARE one). No `.
|
|
191
|
+
- No subagents (you ARE one). No `.agents/rules/` edits. No `.agents/docs/` edits without surfacing to parent.
|
|
192
192
|
- No git branch create/rename/switch.
|
|
193
193
|
- `must_haves` (truths/artifacts/key_links) is NOT optional — executor relies on it for self-verification.
|
|
194
194
|
- Every acceptance criterion is RUNNABLE — command/check/test, not "works correctly" or "feels right".
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
# Plan Task Workflow
|
|
2
2
|
|
|
3
|
-
# Всегда соблюдай правила проекта из .
|
|
3
|
+
# Всегда соблюдай правила проекта из .agents/rules/
|
|
4
4
|
# Не используй shortcuts ради скорости
|
|
5
5
|
|
|
6
6
|
<path_discipline>
|
|
7
|
-
**All soly-managed files live under `.
|
|
7
|
+
**All soly-managed files live under `.agents/`.** PLAN.md lives at `.agents/features/<feature>/tasks/<task-id>/PLAN.md`. Never write plan files to the project root. Use absolute paths.
|
|
8
8
|
|
|
9
9
|
The iteration context file (path given by the parent in the task prompt) is your single source of truth for intent, STATE, the feature README, prior task SUMMARYs, and the current task PLAN (for refinement). Read it FIRST.
|
|
10
10
|
</path_discipline>
|
|
@@ -18,14 +18,14 @@ executor can pick it up standalone.
|
|
|
18
18
|
|
|
19
19
|
<required_reading>
|
|
20
20
|
Before any planning, read:
|
|
21
|
-
1. .
|
|
22
|
-
2. .
|
|
23
|
-
3. .
|
|
24
|
-
4. .
|
|
25
|
-
5. .
|
|
21
|
+
1. .agents/docs/ — INTENT (0-point)
|
|
22
|
+
2. .agents/STATE.md — current state
|
|
23
|
+
3. .agents/ROADMAP.md — high-level requirements
|
|
24
|
+
4. .agents/features/<feature>/README.md — feature context
|
|
25
|
+
5. .agents/contracts/* — shared API schemas (if any)
|
|
26
26
|
6. The task's existing PLAN.md (if refining) or relevant sibling tasks (if creating)
|
|
27
27
|
|
|
28
|
-
If `.
|
|
28
|
+
If `.agents/features/` is empty for the target feature, error and stop.
|
|
29
29
|
Do not invent feature paths.
|
|
30
30
|
</required_reading>
|
|
31
31
|
|
|
@@ -36,7 +36,7 @@ PLAN.md starts with a YAML frontmatter block. The executor depends on it.
|
|
|
36
36
|
---
|
|
37
37
|
id: auth-be-login-a3f9 # slug-4hex, must match the directory name
|
|
38
38
|
kind: be # be | fe | infra | docs | integration
|
|
39
|
-
feature: auth # parent feature name (must match .
|
|
39
|
+
feature: auth # parent feature name (must match .agents/features/<feature>/)
|
|
40
40
|
status: ready # ready | in-progress | blocked | done
|
|
41
41
|
priority: high # high | medium | low
|
|
42
42
|
parallelizable: true # whether this task can run alongside other parallel tasks
|
|
@@ -68,7 +68,7 @@ If unclear, error and ask the parent.
|
|
|
68
68
|
</step>
|
|
69
69
|
|
|
70
70
|
<step name="read_intent" priority="first">
|
|
71
|
-
**0-POINT CHECK.** Re-read `.
|
|
71
|
+
**0-POINT CHECK.** Re-read `.agents/docs/` (intent docs) before any planning.
|
|
72
72
|
The task PLAN.md is the contract; intent docs are the WHY. If you find
|
|
73
73
|
a conflict, flag it instead of silently choosing.
|
|
74
74
|
|
|
@@ -79,13 +79,13 @@ read it directly from its path.
|
|
|
79
79
|
</step>
|
|
80
80
|
|
|
81
81
|
<step name="read_feature">
|
|
82
|
-
Read `.
|
|
82
|
+
Read `.agents/features/<feature>/README.md` for feature-level context.
|
|
83
83
|
If missing, note it but proceed (the parent may not have written one yet).
|
|
84
84
|
</step>
|
|
85
85
|
|
|
86
86
|
<step name="check_contracts">
|
|
87
87
|
If the task touches API surfaces (BE endpoints, FE request shapes, shared
|
|
88
|
-
types), read `.
|
|
88
|
+
types), read `.agents/contracts/<feature>.openapi.yaml` or similar. Contracts
|
|
89
89
|
are the synchronizing artifact between parallel BE and FE tasks.
|
|
90
90
|
</step>
|
|
91
91
|
|
|
@@ -137,13 +137,13 @@ For new-task, commit the new PLAN.md:
|
|
|
137
137
|
```
|
|
138
138
|
# Use absolute path with `cd "$PROJECT_ROOT"` so git doesn't interpret
|
|
139
139
|
# a bare relative path as cwd-relative.
|
|
140
|
-
cd "$PROJECT_ROOT" && git add .
|
|
140
|
+
cd "$PROJECT_ROOT" && git add .agents/features/<feature>/tasks/<id>/PLAN.md
|
|
141
141
|
git commit -m "chore(tasks): plan <id>"
|
|
142
142
|
```
|
|
143
143
|
|
|
144
144
|
For existing-task refinement, commit only if material change:
|
|
145
145
|
```
|
|
146
|
-
cd "$PROJECT_ROOT" && git add .
|
|
146
|
+
cd "$PROJECT_ROOT" && git add .agents/features/<feature>/tasks/<id>/PLAN.md
|
|
147
147
|
git commit -m "chore(tasks): refine plan <id>"
|
|
148
148
|
```
|
|
149
149
|
|
|
@@ -152,7 +152,7 @@ If nothing material changed, do not commit.
|
|
|
152
152
|
**Worker bash setup (when you do need it):**
|
|
153
153
|
```bash
|
|
154
154
|
PROJECT_ROOT="$(pwd)" # worker inherits parent cwd = project root
|
|
155
|
-
SOLY_DIR="$PROJECT_ROOT/.
|
|
155
|
+
SOLY_DIR="$PROJECT_ROOT/.agents"
|
|
156
156
|
```
|
|
157
157
|
</step>
|
|
158
158
|
|
|
@@ -172,14 +172,14 @@ Return to parent:
|
|
|
172
172
|
- Preserve existing frontmatter on refinement. Only update if you find a bug.
|
|
173
173
|
- For new-task, generate the id as `<slug>-<4hex>` (lowercase).
|
|
174
174
|
- Commit messages follow Conventional Commits 1.0.0 — `chore(tasks): ...`.
|
|
175
|
-
- Do not modify `.
|
|
175
|
+
- Do not modify `.agents/rules/`.
|
|
176
176
|
- Do not run subagents yourself.
|
|
177
|
-
- **PATH DISCIPLINE:** PLAN.md goes to `.
|
|
177
|
+
- **PATH DISCIPLINE:** PLAN.md goes to `.agents/features/<feature>/tasks/<id>/PLAN.md`. Never to the project root.
|
|
178
178
|
- Return: created/refined path, task id, plan summary, open questions.
|
|
179
179
|
</hard_rules>
|
|
180
180
|
|
|
181
181
|
<dual_mode_note>
|
|
182
182
|
Tasks are part of soly's dual-mode system. The project may also have
|
|
183
|
-
`.
|
|
183
|
+
`.agents/phases/` (phase-based layout, distinct from task-based). You are only planning a specific task.
|
|
184
184
|
Do not touch phases. Do not modify ROADMAP.md or STATE.md.
|
|
185
185
|
</dual_mode_note>
|
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
|
-
}
|