pi-soly 0.2.1
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 +372 -0
- package/agents/soly-debugger.md +60 -0
- package/agents/soly-documenter.md +82 -0
- package/agents/soly-oracle.md +69 -0
- package/agents/soly-refactor.md +65 -0
- package/agents/soly-reviewer.md +107 -0
- package/agents/soly-tester.md +56 -0
- package/agents/soly-worker.md +84 -0
- package/agents-install.ts +105 -0
- package/commands.ts +778 -0
- package/config.ts +228 -0
- package/core.ts +1599 -0
- package/docs.ts +235 -0
- package/env.ts +196 -0
- package/git.ts +95 -0
- package/html.ts +157 -0
- package/index.ts +718 -0
- package/integrations.ts +64 -0
- package/intent.ts +303 -0
- package/iteration.ts +712 -0
- package/nudge.ts +123 -0
- package/package.json +66 -0
- package/scratchpad.ts +117 -0
- package/tools.ts +1132 -0
- package/workflows/execute.ts +401 -0
- package/workflows/index.ts +235 -0
- package/workflows/inspect.ts +492 -0
- package/workflows/parser.ts +268 -0
- package/workflows/pause.ts +150 -0
- package/workflows/planning.ts +624 -0
- package/workflows/quick.ts +258 -0
- package/workflows/resume.ts +201 -0
- package/workflows-data/discuss-phase.md +292 -0
- package/workflows-data/execute-phase.md +200 -0
- package/workflows-data/execute-plan.md +251 -0
- package/workflows-data/execute-task.md +116 -0
- package/workflows-data/pause-work.md +142 -0
- package/workflows-data/plan-phase.md +199 -0
- package/workflows-data/plan-task.md +185 -0
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
// =============================================================================
|
|
2
|
+
// workflows/quick.ts — Direct-response workflow handlers
|
|
3
|
+
// =============================================================================
|
|
4
|
+
//
|
|
5
|
+
// These verbs (status, log, diff) return immediate results without an LLM
|
|
6
|
+
// round-trip. The extension reads disk / runs git, formats a notification,
|
|
7
|
+
// and the LLM never sees the request.
|
|
8
|
+
//
|
|
9
|
+
// Why: these are common, low-judgment operations where the LLM would just
|
|
10
|
+
// regurgitate what the extension can compute more accurately and faster.
|
|
11
|
+
// =============================================================================
|
|
12
|
+
|
|
13
|
+
import { execFile } from "node:child_process";
|
|
14
|
+
import { promisify } from "node:util";
|
|
15
|
+
import * as fs from "node:fs";
|
|
16
|
+
import * as path from "node:path";
|
|
17
|
+
import { readIfExists, buildProgressBar, type SolyState } from "../core.js";
|
|
18
|
+
import type { SolyConfig } from "../config.js";
|
|
19
|
+
import type { SolyCommand } from "./parser.js";
|
|
20
|
+
|
|
21
|
+
const execFileAsync = promisify(execFile);
|
|
22
|
+
|
|
23
|
+
/** Minimum UI surface needed for the quick handlers. */
|
|
24
|
+
interface QuickUI {
|
|
25
|
+
notify: (text: string, kind?: "info" | "warning" | "error") => void;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// =============================================================================
|
|
29
|
+
// soly status
|
|
30
|
+
// =============================================================================
|
|
31
|
+
|
|
32
|
+
export function showStatus(
|
|
33
|
+
_cmd: SolyCommand,
|
|
34
|
+
state: SolyState,
|
|
35
|
+
ui: QuickUI,
|
|
36
|
+
config?: SolyConfig,
|
|
37
|
+
): void {
|
|
38
|
+
if (!state.exists) {
|
|
39
|
+
ui.notify("soly: no .soly/ directory in cwd", "error");
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
const maxPhases = config?.display.maxPhasesInStatus ?? 20;
|
|
43
|
+
|
|
44
|
+
const lines: string[] = [];
|
|
45
|
+
lines.push("=== soly status ===");
|
|
46
|
+
lines.push("");
|
|
47
|
+
lines.push(
|
|
48
|
+
`milestone: ${state.milestone}${state.milestoneName ? ` — ${state.milestoneName}` : ""}`,
|
|
49
|
+
);
|
|
50
|
+
lines.push(`status: ${state.status}`);
|
|
51
|
+
if (state.lastUpdated) lines.push(`updated: ${state.lastUpdated}`);
|
|
52
|
+
lines.push("");
|
|
53
|
+
|
|
54
|
+
if (state.position) {
|
|
55
|
+
lines.push(`position:`);
|
|
56
|
+
lines.push(` phase: ${state.position.phase}`);
|
|
57
|
+
lines.push(` plan: ${state.position.plan}`);
|
|
58
|
+
lines.push(` status: ${state.position.status}`);
|
|
59
|
+
} else {
|
|
60
|
+
lines.push("position: (none — run `soly plan <N>` to start a phase)");
|
|
61
|
+
}
|
|
62
|
+
lines.push("");
|
|
63
|
+
|
|
64
|
+
lines.push(
|
|
65
|
+
`progress: ${buildProgressBar(state.progress.percent, 30)} ${state.progress.percent}%`,
|
|
66
|
+
);
|
|
67
|
+
lines.push(` ${state.progress.completedPhases}/${state.progress.totalPhases} phases, ${state.progress.completedPlans}/${state.progress.totalPlans} plans`);
|
|
68
|
+
lines.push("");
|
|
69
|
+
|
|
70
|
+
if (state.phases.length > 0) {
|
|
71
|
+
lines.push("phases:");
|
|
72
|
+
const current = state.currentPhase?.number;
|
|
73
|
+
for (const p of state.phases.slice(0, maxPhases)) {
|
|
74
|
+
const marker = current === p.number ? "→" : " ";
|
|
75
|
+
const cr = (p.contextExists ? "C" : "·") + (p.researchExists ? "R" : "·");
|
|
76
|
+
lines.push(
|
|
77
|
+
` ${marker} ${String(p.number).padStart(2, "0")}. ${p.name.padEnd(28)} [${cr}] plans=${p.planCount}`,
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
if (state.phases.length > maxPhases) {
|
|
81
|
+
lines.push(` ... and ${state.phases.length - maxPhases} more (use config.display.maxPhasesInStatus to show more)`);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// NEW (A7): recent activity — last 3 iteration files
|
|
86
|
+
const iterDir = path.join(state.solyDir, "iterations");
|
|
87
|
+
if (fs.existsSync(iterDir)) {
|
|
88
|
+
const files = fs.readdirSync(iterDir)
|
|
89
|
+
.filter((f) => f.endsWith(".md"))
|
|
90
|
+
.map((f) => ({ f, stat: fs.statSync(path.join(iterDir, f)) }))
|
|
91
|
+
.sort((a, b) => b.stat.mtimeMs - a.stat.mtimeMs)
|
|
92
|
+
.slice(0, 3);
|
|
93
|
+
if (files.length > 0) {
|
|
94
|
+
lines.push("");
|
|
95
|
+
lines.push("recent iterations:");
|
|
96
|
+
for (const { f, stat } of files) {
|
|
97
|
+
const ago = humanizeAge(Date.now() - stat.mtimeMs);
|
|
98
|
+
lines.push(` ${f} (${ago})`);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
ui.notify(lines.join("\n"), "info");
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// =============================================================================
|
|
107
|
+
// soly log
|
|
108
|
+
// =============================================================================
|
|
109
|
+
|
|
110
|
+
function humanizeAge(ms: number): string {
|
|
111
|
+
if (ms < 60_000) return "just now";
|
|
112
|
+
if (ms < 3_600_000) return `${Math.round(ms / 60_000)}m ago`;
|
|
113
|
+
if (ms < 86_400_000) return `${Math.round(ms / 3_600_000)}h ago`;
|
|
114
|
+
if (ms < 30 * 86_400_000) return `${Math.round(ms / 86_400_000)}d ago`;
|
|
115
|
+
return `${Math.round(ms / (30 * 86_400_000))}mo ago`;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const DECISIONS_HEADER = /^##\s*Decisions\s*$/;
|
|
119
|
+
const DECISIONS_TABLE_HEADER = /^\|\s*Decision\s*\|/;
|
|
120
|
+
const DECISIONS_TABLE_SEPARATOR = /^\|[\s\-:|]+\|$/;
|
|
121
|
+
const DECISIONS_TABLE_ROW = /^\|\s*([^|]+)\s*\|\s*([^|]+)\s*\|\s*([^|]+)\s*\|\s*$/;
|
|
122
|
+
|
|
123
|
+
export function showLog(cmd: SolyCommand, state: SolyState, ui: QuickUI): void {
|
|
124
|
+
if (!state.exists) {
|
|
125
|
+
ui.notify("soly log: no .soly/ directory in cwd", "error");
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const statePath = path.join(state.solyDir, "STATE.md");
|
|
130
|
+
const raw = readIfExists(statePath);
|
|
131
|
+
if (!raw) {
|
|
132
|
+
ui.notify("soly log: STATE.md not found", "error");
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const lines = raw.split(/\r?\n/);
|
|
137
|
+
const decisionsIdx = lines.findIndex((l) => DECISIONS_HEADER.test(l));
|
|
138
|
+
if (decisionsIdx === -1) {
|
|
139
|
+
ui.notify("soly log: no Decisions table in STATE.md", "info");
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Collect rows from the table
|
|
144
|
+
const rows: Array<{ decision: string; rationale: string; phase: string }> = [];
|
|
145
|
+
for (let i = decisionsIdx + 1; i < lines.length; i++) {
|
|
146
|
+
const line = lines[i];
|
|
147
|
+
if (!line.startsWith("|")) break; // table ended
|
|
148
|
+
if (DECISIONS_TABLE_HEADER.test(line) || DECISIONS_TABLE_SEPARATOR.test(line)) continue;
|
|
149
|
+
const m = line.match(DECISIONS_TABLE_ROW);
|
|
150
|
+
if (m) rows.push({ decision: m[1].trim(), rationale: m[2].trim(), phase: m[3].trim() });
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (rows.length === 0) {
|
|
154
|
+
ui.notify("soly log: Decisions table is empty", "info");
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Optional limit: `soly log 5` — last 5 decisions
|
|
159
|
+
const limitArg = cmd.args[0]?.trim();
|
|
160
|
+
if (limitArg) {
|
|
161
|
+
const parsed = parseInt(limitArg, 10);
|
|
162
|
+
if (!Number.isFinite(parsed) || parsed <= 0) {
|
|
163
|
+
ui.notify(
|
|
164
|
+
`soly log: invalid limit "${limitArg}" (must be a positive integer)`,
|
|
165
|
+
"error",
|
|
166
|
+
);
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
const limit = limitArg ? parseInt(limitArg, 10) : 20;
|
|
171
|
+
const tail = Number.isFinite(limit) && limit > 0 ? rows.slice(-limit) : rows.slice(-20);
|
|
172
|
+
|
|
173
|
+
const out: string[] = [];
|
|
174
|
+
out.push(`=== last ${tail.length} of ${rows.length} decisions ===`);
|
|
175
|
+
out.push("");
|
|
176
|
+
for (const r of tail) {
|
|
177
|
+
out.push(` [${r.phase}] ${r.decision}`);
|
|
178
|
+
out.push(` ${r.rationale}`);
|
|
179
|
+
out.push("");
|
|
180
|
+
}
|
|
181
|
+
ui.notify(out.join("\n"), "info");
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// =============================================================================
|
|
185
|
+
// soly diff
|
|
186
|
+
// =============================================================================
|
|
187
|
+
|
|
188
|
+
interface DiffResult {
|
|
189
|
+
stdout: string;
|
|
190
|
+
stderr: string;
|
|
191
|
+
code: number;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
async function safeExec(file: string, args: string[], cwd: string): Promise<DiffResult> {
|
|
195
|
+
try {
|
|
196
|
+
const { stdout, stderr } = await execFileAsync(file, args, {
|
|
197
|
+
cwd,
|
|
198
|
+
maxBuffer: 4 * 1024 * 1024,
|
|
199
|
+
encoding: "utf-8",
|
|
200
|
+
});
|
|
201
|
+
return { stdout, stderr, code: 0 };
|
|
202
|
+
} catch (e) {
|
|
203
|
+
const err = e as { stdout?: string; stderr?: string; code?: number };
|
|
204
|
+
return {
|
|
205
|
+
stdout: err.stdout ?? "",
|
|
206
|
+
stderr: err.stderr ?? "",
|
|
207
|
+
code: typeof err.code === "number" ? err.code : 1,
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
export async function showDiff(
|
|
213
|
+
_cmd: SolyCommand,
|
|
214
|
+
state: SolyState,
|
|
215
|
+
ui: QuickUI,
|
|
216
|
+
): Promise<void> {
|
|
217
|
+
// Graceful without .soly/: use cwd as project root, skip the .soly/ filter
|
|
218
|
+
const projectRoot = state.solyDir ? path.dirname(state.solyDir) : process.cwd();
|
|
219
|
+
const solyDir = state.solyDir; // may be empty when run outside a soly project
|
|
220
|
+
|
|
221
|
+
// 1. git status (short)
|
|
222
|
+
const status = await safeExec("git", ["status", "--short", "--branch"], projectRoot);
|
|
223
|
+
// 2. git diff (tracked changes, no untracked)
|
|
224
|
+
const diff = await safeExec("git", ["diff", "--stat"], projectRoot);
|
|
225
|
+
// 3. uncommitted .soly/ file changes (since last commit)
|
|
226
|
+
const solyChanges = await safeExec(
|
|
227
|
+
"git",
|
|
228
|
+
["status", "--short", "--", solyDir],
|
|
229
|
+
projectRoot,
|
|
230
|
+
);
|
|
231
|
+
|
|
232
|
+
const out: string[] = [];
|
|
233
|
+
out.push(state.exists ? "=== soly diff ===" : "=== git diff (no .soly/ in cwd) ===");
|
|
234
|
+
out.push("");
|
|
235
|
+
|
|
236
|
+
if (status.code !== 0) {
|
|
237
|
+
out.push("(git not available or not a git repo)");
|
|
238
|
+
} else {
|
|
239
|
+
out.push("git status --short --branch:");
|
|
240
|
+
out.push(status.stdout.trim() || " (clean working tree)");
|
|
241
|
+
out.push("");
|
|
242
|
+
if (diff.stdout.trim()) {
|
|
243
|
+
out.push("git diff --stat:");
|
|
244
|
+
out.push(diff.stdout.trim());
|
|
245
|
+
out.push("");
|
|
246
|
+
}
|
|
247
|
+
if (solyDir) {
|
|
248
|
+
if (solyChanges.stdout.trim()) {
|
|
249
|
+
out.push("uncommitted .soly/ changes:");
|
|
250
|
+
out.push(solyChanges.stdout.trim());
|
|
251
|
+
} else {
|
|
252
|
+
out.push("uncommitted .soly/ changes: (none)");
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
ui.notify(out.join("\n"), "info");
|
|
258
|
+
}
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
// =============================================================================
|
|
2
|
+
// workflows/resume.ts — `soly resume [phase]` handler
|
|
3
|
+
// =============================================================================
|
|
4
|
+
//
|
|
5
|
+
// Intercepts "soly resume" (or "soly resume <N>" to scope to a specific phase)
|
|
6
|
+
// and transforms it into a kickoff prompt that re-establishes the full
|
|
7
|
+
// session context from the last handoff.
|
|
8
|
+
//
|
|
9
|
+
// Reads:
|
|
10
|
+
// - .soly/HANDOFF.json (machine-readable state, written by pause/compact)
|
|
11
|
+
// - .soly/.continue-here.md (human-readable context, written by pause/compact)
|
|
12
|
+
//
|
|
13
|
+
// If neither file exists, falls back to a "no prior handoff" message that
|
|
14
|
+
// tells the LLM to load context from .soly/STATE.md + ROADMAP.md normally.
|
|
15
|
+
// =============================================================================
|
|
16
|
+
|
|
17
|
+
import * as fs from "node:fs";
|
|
18
|
+
import * as path from "node:path";
|
|
19
|
+
import type { SolyCommand } from "./parser.js";
|
|
20
|
+
import type { SolyState } from "../core.js";
|
|
21
|
+
import { readIfExists } from "../core.js";
|
|
22
|
+
|
|
23
|
+
interface HandoffJson {
|
|
24
|
+
schema_version?: string;
|
|
25
|
+
generated_at?: string;
|
|
26
|
+
milestone?: string;
|
|
27
|
+
milestone_name?: string;
|
|
28
|
+
status?: string;
|
|
29
|
+
position?: { phase?: string; plan?: string; status?: string } | null;
|
|
30
|
+
current_phase?: {
|
|
31
|
+
number?: number;
|
|
32
|
+
name?: string;
|
|
33
|
+
slug?: string;
|
|
34
|
+
dir?: string;
|
|
35
|
+
plan_count?: number;
|
|
36
|
+
} | null;
|
|
37
|
+
progress?: {
|
|
38
|
+
total_phases?: number;
|
|
39
|
+
completed_phases?: number;
|
|
40
|
+
total_plans?: number;
|
|
41
|
+
completed_plans?: number;
|
|
42
|
+
percent?: number;
|
|
43
|
+
};
|
|
44
|
+
work_completed?: string[];
|
|
45
|
+
work_remaining?: string[];
|
|
46
|
+
decisions?: string[];
|
|
47
|
+
blockers?: string[];
|
|
48
|
+
human_actions_pending?: string[];
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface ResumeHandlerResult {
|
|
52
|
+
handled: boolean;
|
|
53
|
+
transformedText?: string;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function parseHandoff(solyDir: string): HandoffJson | null {
|
|
57
|
+
const handoffPath = path.join(solyDir, "HANDOFF.json");
|
|
58
|
+
const raw = readIfExists(handoffPath);
|
|
59
|
+
if (!raw) return null;
|
|
60
|
+
try {
|
|
61
|
+
const parsed = JSON.parse(raw);
|
|
62
|
+
return parsed as HandoffJson;
|
|
63
|
+
} catch {
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function formatListSection(label: string, items: string[] | undefined): string {
|
|
69
|
+
if (!items || items.length === 0) return ` (none)\n`;
|
|
70
|
+
return ` - ${items.join("\n - ")}\n`;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function formatHandoffSummary(h: HandoffJson): string {
|
|
74
|
+
const lines: string[] = [];
|
|
75
|
+
|
|
76
|
+
lines.push("=== From .soly/HANDOFF.json ===");
|
|
77
|
+
if (h.generated_at) lines.push(` generated_at: ${h.generated_at}`);
|
|
78
|
+
if (h.milestone) lines.push(` milestone: ${h.milestone}${h.milestone_name ? ` — ${h.milestone_name}` : ""}`);
|
|
79
|
+
if (h.status) lines.push(` status: ${h.status}`);
|
|
80
|
+
|
|
81
|
+
if (h.position) {
|
|
82
|
+
lines.push(` position: phase=${h.position.phase ?? "?"}, plan=${h.position.plan ?? "?"}, status=${h.position.status ?? "?"}`);
|
|
83
|
+
}
|
|
84
|
+
if (h.current_phase) {
|
|
85
|
+
lines.push(
|
|
86
|
+
` current_phase: #${h.current_phase.number ?? "?"} ${h.current_phase.name ?? ""} (${h.current_phase.plan_count ?? 0} plans)`,
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
if (h.progress) {
|
|
90
|
+
lines.push(
|
|
91
|
+
` progress: ${h.progress.completed_phases ?? 0}/${h.progress.total_phases ?? 0} phases, ${h.progress.completed_plans ?? 0}/${h.progress.total_plans ?? 0} plans, ${h.progress.percent ?? 0}%`,
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
lines.push("");
|
|
96
|
+
lines.push("Work completed (from last session):");
|
|
97
|
+
lines.push(formatListSection("", h.work_completed));
|
|
98
|
+
lines.push("Work remaining:");
|
|
99
|
+
lines.push(formatListSection("", h.work_remaining));
|
|
100
|
+
lines.push("Decisions logged:");
|
|
101
|
+
lines.push(formatListSection("", h.decisions));
|
|
102
|
+
lines.push("Blockers / open questions:");
|
|
103
|
+
lines.push(formatListSection("", h.blockers));
|
|
104
|
+
lines.push("Human actions pending:");
|
|
105
|
+
lines.push(formatListSection("", h.human_actions_pending));
|
|
106
|
+
|
|
107
|
+
return lines.join("\n");
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export function buildResumeTransform(cmd: SolyCommand, state: SolyState): ResumeHandlerResult {
|
|
111
|
+
if (!state.exists) {
|
|
112
|
+
return {
|
|
113
|
+
handled: true,
|
|
114
|
+
transformedText:
|
|
115
|
+
`soly resume: no .soly/ directory in cwd (${state.solyDir || "<cwd>"}) — nothing to resume.\n` +
|
|
116
|
+
`Initialize a soly project first, or run \`soly pause\` later to create handoff files.`,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const handoff = parseHandoff(state.solyDir);
|
|
121
|
+
const continueMd = readIfExists(path.join(state.solyDir, ".continue-here.md"));
|
|
122
|
+
|
|
123
|
+
// Optional phase scope: "soly resume 11" → focus kickoff on phase 11
|
|
124
|
+
const phaseArg = cmd.args[0]?.trim();
|
|
125
|
+
let phaseFilter: number | null = null;
|
|
126
|
+
let phaseFilterInvalid = false;
|
|
127
|
+
if (phaseArg) {
|
|
128
|
+
const m = phaseArg.match(/^(\d+)$/);
|
|
129
|
+
if (m) {
|
|
130
|
+
phaseFilter = parseInt(m[1], 10);
|
|
131
|
+
// Validate that the phase actually exists
|
|
132
|
+
if (state.phases.length > 0 && !state.phases.find((p) => p.number === phaseFilter)) {
|
|
133
|
+
phaseFilterInvalid = true;
|
|
134
|
+
}
|
|
135
|
+
} else {
|
|
136
|
+
phaseFilterInvalid = true;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
if (phaseFilterInvalid) {
|
|
140
|
+
const known = state.phases.map((p) => p.number).join(", ") || "(none)";
|
|
141
|
+
const argStr = cmd.args[0] ?? "";
|
|
142
|
+
return {
|
|
143
|
+
handled: true,
|
|
144
|
+
transformedText:
|
|
145
|
+
`soly resume: invalid or unknown phase "${argStr}".\n` +
|
|
146
|
+
`Usage: soly resume [N] (e.g. "soly resume 5")\n` +
|
|
147
|
+
`Known phases: ${known}`,
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (!handoff && !continueMd) {
|
|
152
|
+
// No prior handoff — fall back to loading from .soly/STATE.md directly.
|
|
153
|
+
const fallbackScope = phaseFilter != null
|
|
154
|
+
? `Focus: phase ${phaseFilter}.`
|
|
155
|
+
: "Scope: full project.";
|
|
156
|
+
return {
|
|
157
|
+
handled: true,
|
|
158
|
+
transformedText:
|
|
159
|
+
`soly resume: no handoff files found (looked for .soly/HANDOFF.json and .soly/.continue-here.md).\n` +
|
|
160
|
+
`No prior \`soly pause\` was run — loading context from .soly/STATE.md and .soly/ROADMAP.md directly.\n\n` +
|
|
161
|
+
`${fallbackScope}\n\n` +
|
|
162
|
+
`Read .soly/STATE.md (Current Position, Decisions, Blockers sections) and .soly/ROADMAP.md.\n` +
|
|
163
|
+
`Summarize: where the project is, what's next, what's blocking. Then ask the user what to focus on first.`,
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const handoffBlock = handoff ? formatHandoffSummary(handoff) : "(no HANDOFF.json found)";
|
|
168
|
+
const continueBlock = continueMd
|
|
169
|
+
? `\n=== From .soly/.continue-here.md ===\n${continueMd.trim()}\n`
|
|
170
|
+
: "";
|
|
171
|
+
|
|
172
|
+
const focusLine = phaseFilter != null
|
|
173
|
+
? `Focus: phase ${phaseFilter} only.`
|
|
174
|
+
: "Focus: pick up exactly where the last session left off.";
|
|
175
|
+
|
|
176
|
+
const instruction = `soly resume — restoring session context from last handoff.
|
|
177
|
+
|
|
178
|
+
${focusLine}
|
|
179
|
+
|
|
180
|
+
${handoffBlock}
|
|
181
|
+
${continueBlock}
|
|
182
|
+
|
|
183
|
+
=== Resume protocol ===
|
|
184
|
+
|
|
185
|
+
1. **Read .soly/docs/ first** — the project's 0-point intent. Pickup is meaningless without knowing what the user is building toward.
|
|
186
|
+
2. Read .soly/STATE.md to confirm current position (the handoff may be stale).
|
|
187
|
+
3. Read .soly/ROADMAP.md and any CONTEXT.md / RESEARCH.md for the active phase.
|
|
188
|
+
4. Compare handoff's "work remaining" with the actual repo state (git status, recent commits, .soly/ files).
|
|
189
|
+
5. Produce a one-screen "Where we are" summary:
|
|
190
|
+
- current phase + plan
|
|
191
|
+
- what's actually been done (verified via filesystem / git)
|
|
192
|
+
- what's still pending
|
|
193
|
+
- any new blockers discovered since handoff
|
|
194
|
+
6. Surface any stale handoff data — if the handoff says "remaining: X" but the repo shows X is done, say so.
|
|
195
|
+
7. Ask the user: "Pick up from <next concrete step> — confirm or change?" before doing any work.
|
|
196
|
+
|
|
197
|
+
Do NOT start coding. Resume is about restoring shared understanding, not action.
|
|
198
|
+
`;
|
|
199
|
+
|
|
200
|
+
return { handled: true, transformedText: instruction };
|
|
201
|
+
}
|