@wrongstack/cli 0.4.1 → 0.5.2
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/dist/index.js +2070 -224
- package/dist/index.js.map +1 -1
- package/package.json +10 -10
package/dist/index.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import
|
|
2
|
+
import * as path18 from 'path';
|
|
3
|
+
import { color, allServers, DefaultPathResolver, TOKENS, DefaultSystemPromptBuilder, ToolRegistry, createContextManagerTool, EventBus, InMemoryMetricsSink, wireMetricsToEvents, DefaultHealthRegistry, startMetricsServer, SlashCommandRegistry, loadPlugins, createDelegateTool, FLEET_ROSTER, DefaultLogger, DefaultModelsRegistry, ProviderRegistry, RecoveryLock, DefaultAttachmentStore, QueueStore, Context, loadTodosCheckpoint, attachTodosCheckpoint, loadDirectorState, loadPlan, createDefaultPipelines, AutoCompactionMiddleware, estimateRequestTokens, Agent, makeDirectorSessionFactory, Director, DefaultMultiAgentCoordinator, makeAgentSubagentRunner, resolveWstackPaths, DefaultSecretVault, migratePlaintextSecrets, DefaultConfigLoader, DefaultSessionReader, DefaultSessionRewinder, DefaultSessionStore, atomicWrite, AutoApprovePermissionPolicy, formatContextWindowModeList, repairToolUseAdjacency, getContextWindowMode, resolveContextWindowPolicy, formatTodosList, emptyPlan, clearPlan, savePlan, removePlanItem, formatPlan, setPlanItemStatus, addPlanItem, SpecStore, TaskGraphStore, SpecVersioning, getTemplate, listTemplates, templateToMarkdown, SpecParser, renderSpecAnalysis, AISpecBuilder, DefaultTaskStore, TaskTracker, InputBuilder, projectHash, decryptConfigSecrets, encryptConfigSecrets as encryptConfigSecrets$1, DefaultPluginAPI } from '@wrongstack/core';
|
|
3
4
|
import * as crypto from 'crypto';
|
|
4
5
|
import { randomUUID } from 'crypto';
|
|
5
|
-
import * as
|
|
6
|
-
import
|
|
7
|
-
import { DefaultSecretVault as DefaultSecretVault$1, encryptConfigSecrets } from '@wrongstack/core/security';
|
|
6
|
+
import * as fs5 from 'fs/promises';
|
|
7
|
+
import { DefaultSecretVault as DefaultSecretVault$1, encryptConfigSecrets, decryptConfigSecrets as decryptConfigSecrets$1 } from '@wrongstack/core/security';
|
|
8
8
|
import { WebSocketServer, WebSocket } from 'ws';
|
|
9
9
|
import { writeFileSync } from 'fs';
|
|
10
10
|
import { createRequire } from 'module';
|
|
@@ -12,12 +12,16 @@ import { MCPRegistry } from '@wrongstack/mcp';
|
|
|
12
12
|
import { buildProviderFactoriesFromRegistry, makeProviderFromConfig, capabilitiesFor } from '@wrongstack/providers';
|
|
13
13
|
import { createDefaultContainer, routeImagesForModel, readClipboardImage } from '@wrongstack/runtime';
|
|
14
14
|
import { builtinToolsPack, rememberTool, forgetTool } from '@wrongstack/tools';
|
|
15
|
-
import * as
|
|
15
|
+
import * as os4 from 'os';
|
|
16
16
|
import * as readline from 'readline';
|
|
17
|
+
import { spawn } from 'child_process';
|
|
18
|
+
import { SkillInstaller } from '@wrongstack/core/skills';
|
|
17
19
|
import { createToolVisionAdapters } from '@wrongstack/runtime/vision';
|
|
18
20
|
|
|
19
21
|
var __defProp = Object.defineProperty;
|
|
22
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
20
23
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
24
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
21
25
|
var __esm = (fn, res) => function __init() {
|
|
22
26
|
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
23
27
|
};
|
|
@@ -25,6 +29,835 @@ var __export = (target, all) => {
|
|
|
25
29
|
for (var name in all)
|
|
26
30
|
__defProp(target, name, { get: all[name], enumerable: true });
|
|
27
31
|
};
|
|
32
|
+
var __copyProps = (to, from, except, desc) => {
|
|
33
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
34
|
+
for (let key of __getOwnPropNames(from))
|
|
35
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
36
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
37
|
+
}
|
|
38
|
+
return to;
|
|
39
|
+
};
|
|
40
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
41
|
+
|
|
42
|
+
// src/slash-commands/sdd.ts
|
|
43
|
+
var sdd_exports = {};
|
|
44
|
+
__export(sdd_exports, {
|
|
45
|
+
autoDetectTaskCompletion: () => autoDetectTaskCompletion,
|
|
46
|
+
buildSddCommand: () => buildSddCommand,
|
|
47
|
+
getActiveBuilder: () => getActiveBuilder,
|
|
48
|
+
getActiveSDDContext: () => getActiveSDDContext,
|
|
49
|
+
getActiveSDDPhase: () => getActiveSDDPhase,
|
|
50
|
+
getTaskListText: () => getTaskListText,
|
|
51
|
+
getTaskProgress: () => getTaskProgress,
|
|
52
|
+
markTaskCompleted: () => markTaskCompleted,
|
|
53
|
+
trySaveImplementationPlan: () => trySaveImplementationPlan,
|
|
54
|
+
trySaveSpecFromAIOutput: () => trySaveSpecFromAIOutput,
|
|
55
|
+
trySaveTasksFromAIOutput: () => trySaveTasksFromAIOutput
|
|
56
|
+
});
|
|
57
|
+
function getActiveSDDContext() {
|
|
58
|
+
if (!activeBuilder) return null;
|
|
59
|
+
const session = activeBuilder.getSession();
|
|
60
|
+
if (session.phase === "done") return null;
|
|
61
|
+
return activeBuilder.getAIPrompt();
|
|
62
|
+
}
|
|
63
|
+
function getActiveSDDPhase() {
|
|
64
|
+
if (!activeBuilder) return null;
|
|
65
|
+
return activeBuilder.getPhase();
|
|
66
|
+
}
|
|
67
|
+
async function trySaveSpecFromAIOutput(aiOutput) {
|
|
68
|
+
if (!activeBuilder) return false;
|
|
69
|
+
const spec = activeBuilder.tryParseSpecFromOutput(aiOutput);
|
|
70
|
+
if (!spec) return false;
|
|
71
|
+
activeBuilder.setSpec(spec);
|
|
72
|
+
return true;
|
|
73
|
+
}
|
|
74
|
+
async function trySaveTasksFromAIOutput(aiOutput) {
|
|
75
|
+
if (!activeBuilder) return false;
|
|
76
|
+
const session = activeBuilder.getSession();
|
|
77
|
+
if (!session.spec) return false;
|
|
78
|
+
const json = activeBuilder.extractJSONArray(aiOutput);
|
|
79
|
+
if (!json) return false;
|
|
80
|
+
let tasks;
|
|
81
|
+
try {
|
|
82
|
+
tasks = JSON.parse(json);
|
|
83
|
+
} catch {
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
if (!Array.isArray(tasks) || tasks.length === 0) return false;
|
|
87
|
+
const validTasks = tasks.filter((t) => t && typeof t === "object" && typeof t.title === "string" && t.title.length > 0);
|
|
88
|
+
if (validTasks.length === 0) return false;
|
|
89
|
+
const store = new DefaultTaskStore();
|
|
90
|
+
const tracker = new TaskTracker({ store });
|
|
91
|
+
const graph = await tracker.createGraph(session.spec.id, session.spec.title);
|
|
92
|
+
for (const task of validTasks) {
|
|
93
|
+
const title = String(task.title);
|
|
94
|
+
const description = String(task.description ?? "");
|
|
95
|
+
const type = ["feature", "bugfix", "refactor", "docs", "test", "chore"].includes(String(task.type)) ? String(task.type) : "feature";
|
|
96
|
+
const priority = ["critical", "high", "medium", "low"].includes(String(task.priority)) ? String(task.priority) : "medium";
|
|
97
|
+
const estimateHours = Number(task.estimateHours) || 2;
|
|
98
|
+
const tags = Array.isArray(task.tags) ? task.tags.map(String) : [];
|
|
99
|
+
tracker.addNode({
|
|
100
|
+
title,
|
|
101
|
+
description,
|
|
102
|
+
type,
|
|
103
|
+
priority,
|
|
104
|
+
status: "pending",
|
|
105
|
+
estimateHours,
|
|
106
|
+
tags
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
activeTaskStore = store;
|
|
110
|
+
activeTaskTracker = tracker;
|
|
111
|
+
activeTaskGraphId = graph.id;
|
|
112
|
+
activeBuilder.setTaskGraphId(graph.id);
|
|
113
|
+
return true;
|
|
114
|
+
}
|
|
115
|
+
function getTaskProgress() {
|
|
116
|
+
if (!activeTaskTracker) return null;
|
|
117
|
+
const progress = activeTaskTracker.getProgress();
|
|
118
|
+
return {
|
|
119
|
+
total: progress.total,
|
|
120
|
+
completed: progress.completed,
|
|
121
|
+
pending: progress.pending,
|
|
122
|
+
percent: progress.percentComplete
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
function getTaskListText() {
|
|
126
|
+
if (!activeTaskTracker) return null;
|
|
127
|
+
const nodes = activeTaskTracker.getAllNodes();
|
|
128
|
+
if (nodes.length === 0) return null;
|
|
129
|
+
const lines = nodes.map((n, i) => {
|
|
130
|
+
const status = n.status === "completed" ? "\u2705" : n.status === "in_progress" ? "\u{1F504}" : "\u23F3";
|
|
131
|
+
return `${i + 1}. ${status} [${n.priority}] ${n.title}`;
|
|
132
|
+
});
|
|
133
|
+
return lines.join("\n");
|
|
134
|
+
}
|
|
135
|
+
function markTaskCompleted(taskTitle) {
|
|
136
|
+
if (!activeTaskTracker) return false;
|
|
137
|
+
const nodes = activeTaskTracker.getAllNodes({ status: ["pending", "in_progress"] });
|
|
138
|
+
const match = nodes.find(
|
|
139
|
+
(n) => n.title.toLowerCase().includes(taskTitle.toLowerCase()) || taskTitle.toLowerCase().includes(n.title.toLowerCase())
|
|
140
|
+
);
|
|
141
|
+
if (!match) return false;
|
|
142
|
+
activeTaskTracker.updateNodeStatus(match.id, "completed");
|
|
143
|
+
return true;
|
|
144
|
+
}
|
|
145
|
+
function autoDetectTaskCompletion(aiOutput) {
|
|
146
|
+
if (!activeTaskTracker) return 0;
|
|
147
|
+
const pending = activeTaskTracker.getAllNodes({ status: ["pending", "in_progress"] });
|
|
148
|
+
if (pending.length === 0) return 0;
|
|
149
|
+
let completed = 0;
|
|
150
|
+
const lines = aiOutput.split("\n");
|
|
151
|
+
for (const line of lines) {
|
|
152
|
+
const trimmed = line.trim();
|
|
153
|
+
const sddDoneMatch = trimmed.match(/\/sdd\s+done\s+(.+)/i);
|
|
154
|
+
if (sddDoneMatch?.[1]) {
|
|
155
|
+
const target = sddDoneMatch[1].trim();
|
|
156
|
+
const num = Number(target);
|
|
157
|
+
if (!Number.isNaN(num) && num >= 1 && num <= pending.length) {
|
|
158
|
+
const node = pending[num - 1];
|
|
159
|
+
if (node && node.status !== "completed") {
|
|
160
|
+
activeTaskTracker.updateNodeStatus(node.id, "completed");
|
|
161
|
+
completed++;
|
|
162
|
+
}
|
|
163
|
+
} else {
|
|
164
|
+
const match = pending.find(
|
|
165
|
+
(n) => n.title.toLowerCase().includes(target.toLowerCase()) || target.toLowerCase().includes(n.title.toLowerCase())
|
|
166
|
+
);
|
|
167
|
+
if (match && match.status !== "completed") {
|
|
168
|
+
activeTaskTracker.updateNodeStatus(match.id, "completed");
|
|
169
|
+
completed++;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
const checkmarkMatch = trimmed.match(/^✅\s*(?:Task:\s*)?(.+)/i);
|
|
175
|
+
if (checkmarkMatch?.[1]) {
|
|
176
|
+
const title = checkmarkMatch[1].trim();
|
|
177
|
+
const match = pending.find(
|
|
178
|
+
(n) => n.title.toLowerCase().includes(title.toLowerCase()) || title.toLowerCase().includes(n.title.toLowerCase())
|
|
179
|
+
);
|
|
180
|
+
if (match && match.status !== "completed") {
|
|
181
|
+
activeTaskTracker.updateNodeStatus(match.id, "completed");
|
|
182
|
+
completed++;
|
|
183
|
+
}
|
|
184
|
+
continue;
|
|
185
|
+
}
|
|
186
|
+
const taskNumMatch = trimmed.match(/Task\s+(\d+)\s*[:]\s*(?:complete|done|finished)/i);
|
|
187
|
+
if (taskNumMatch?.[1]) {
|
|
188
|
+
const num = Number(taskNumMatch[1]);
|
|
189
|
+
if (num >= 1 && num <= pending.length) {
|
|
190
|
+
const node = pending[num - 1];
|
|
191
|
+
if (node && node.status !== "completed") {
|
|
192
|
+
activeTaskTracker.updateNodeStatus(node.id, "completed");
|
|
193
|
+
completed++;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
198
|
+
const completedMatch = trimmed.match(/^(?:Completed|Done|Finished)\s*[:]\s*(.+)/i);
|
|
199
|
+
if (completedMatch?.[1]) {
|
|
200
|
+
const title = completedMatch[1].trim();
|
|
201
|
+
const match = pending.find(
|
|
202
|
+
(n) => n.title.toLowerCase().includes(title.toLowerCase()) || title.toLowerCase().includes(n.title.toLowerCase())
|
|
203
|
+
);
|
|
204
|
+
if (match && match.status !== "completed") {
|
|
205
|
+
activeTaskTracker.updateNodeStatus(match.id, "completed");
|
|
206
|
+
completed++;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
return completed;
|
|
211
|
+
}
|
|
212
|
+
function trySaveImplementationPlan(aiOutput) {
|
|
213
|
+
if (!activeBuilder) return false;
|
|
214
|
+
const session = activeBuilder.getSession();
|
|
215
|
+
if (session.phase !== "implementation") return false;
|
|
216
|
+
const jsonMatch = aiOutput.match(/```json\s*\[/);
|
|
217
|
+
if (jsonMatch?.index && jsonMatch.index > 0) {
|
|
218
|
+
const plan = aiOutput.substring(0, jsonMatch.index).trim();
|
|
219
|
+
if (plan.length > 50) {
|
|
220
|
+
activeBuilder.setImplementation(plan);
|
|
221
|
+
return true;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
if (aiOutput.length > 100 && !aiOutput.includes("```json")) {
|
|
225
|
+
activeBuilder.setImplementation(aiOutput.trim());
|
|
226
|
+
return true;
|
|
227
|
+
}
|
|
228
|
+
return false;
|
|
229
|
+
}
|
|
230
|
+
function getActiveBuilder() {
|
|
231
|
+
return activeBuilder;
|
|
232
|
+
}
|
|
233
|
+
function buildSddCommand(opts) {
|
|
234
|
+
return {
|
|
235
|
+
name: "sdd",
|
|
236
|
+
description: "AI-driven SDD: /sdd [new|approve|execute|cancel|status|list|show|templates]",
|
|
237
|
+
async run(args) {
|
|
238
|
+
const ctx = opts.context;
|
|
239
|
+
const projectRoot = ctx?.projectRoot ?? process.cwd();
|
|
240
|
+
const specsDir = path18.join(projectRoot, ".wrongstack", "specs");
|
|
241
|
+
const graphsDir = path18.join(projectRoot, ".wrongstack", "task-graphs");
|
|
242
|
+
const specStore = new SpecStore({ baseDir: specsDir });
|
|
243
|
+
new TaskGraphStore({ baseDir: graphsDir });
|
|
244
|
+
const versioning = new SpecVersioning();
|
|
245
|
+
const [verb, ...rest] = args.trim().split(/\s+/);
|
|
246
|
+
const restJoined = rest.join(" ").trim();
|
|
247
|
+
switch (verb) {
|
|
248
|
+
case "":
|
|
249
|
+
case "help":
|
|
250
|
+
return { message: sddHelp() };
|
|
251
|
+
// ── AI-Driven Spec Session ─────────────────────────────────────────
|
|
252
|
+
case "new":
|
|
253
|
+
case "create": {
|
|
254
|
+
const forceFlag = rest.includes("--force") || rest.includes("-f");
|
|
255
|
+
const title = rest.filter((a) => !a.startsWith("-")).join(" ").trim() || "Untitled Feature";
|
|
256
|
+
if (!activeBuilder && !forceFlag) {
|
|
257
|
+
const sessionPath = path18.join(projectRoot, ".wrongstack", "sdd-session.json");
|
|
258
|
+
try {
|
|
259
|
+
const fsp = await import('fs/promises');
|
|
260
|
+
await fsp.access(sessionPath);
|
|
261
|
+
const projectContext2 = await gatherProjectContext(projectRoot);
|
|
262
|
+
const tempBuilder = new AISpecBuilder({
|
|
263
|
+
store: specStore,
|
|
264
|
+
projectContext: projectContext2,
|
|
265
|
+
sessionPath
|
|
266
|
+
});
|
|
267
|
+
const loaded = await tempBuilder.loadSession();
|
|
268
|
+
if (loaded) {
|
|
269
|
+
const existing = tempBuilder.getSession();
|
|
270
|
+
if (existing.phase !== "done") {
|
|
271
|
+
return {
|
|
272
|
+
message: [
|
|
273
|
+
`An existing SDD session was found:`,
|
|
274
|
+
` Feature: "${existing.title}"`,
|
|
275
|
+
` Phase: ${existing.phase}`,
|
|
276
|
+
` Questions: ${existing.questionCount}`,
|
|
277
|
+
"",
|
|
278
|
+
"Use /sdd resume to continue, or /sdd new --force to start fresh."
|
|
279
|
+
].join("\n")
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
} catch {
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
activeTaskStore = null;
|
|
287
|
+
activeTaskTracker = null;
|
|
288
|
+
activeTaskGraphId = null;
|
|
289
|
+
const projectContext = await gatherProjectContext(projectRoot);
|
|
290
|
+
activeBuilder = new AISpecBuilder({
|
|
291
|
+
store: specStore,
|
|
292
|
+
projectContext,
|
|
293
|
+
minQuestions: 2,
|
|
294
|
+
maxQuestions: 10,
|
|
295
|
+
sessionPath: path18.join(projectRoot, ".wrongstack", "sdd-session.json")
|
|
296
|
+
});
|
|
297
|
+
activeBuilder.startSession(title);
|
|
298
|
+
const aiPrompt = activeBuilder.getAIPrompt();
|
|
299
|
+
return {
|
|
300
|
+
message: [
|
|
301
|
+
`\u2554\u2550\u2550\u2550 SDD: AI Spec Builder \u2550\u2550\u2550\u2557`,
|
|
302
|
+
"",
|
|
303
|
+
`Feature: "${title}"`,
|
|
304
|
+
"",
|
|
305
|
+
"The AI will now ask you contextual questions.",
|
|
306
|
+
"Answer naturally \u2014 it will generate the spec when ready.",
|
|
307
|
+
"",
|
|
308
|
+
"Commands: /sdd approve \xB7 /sdd status \xB7 /sdd cancel"
|
|
309
|
+
].join("\n"),
|
|
310
|
+
runText: `[SDD SESSION ACTIVE]
|
|
311
|
+
${aiPrompt}
|
|
312
|
+
|
|
313
|
+
---
|
|
314
|
+
User message:
|
|
315
|
+
Start the specification interview for "${title}". Ask your first contextual question.`
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
// ── Phase Transitions ──────────────────────────────────────────────
|
|
319
|
+
case "approve":
|
|
320
|
+
case "ok":
|
|
321
|
+
case "confirm": {
|
|
322
|
+
if (!activeBuilder) {
|
|
323
|
+
return {
|
|
324
|
+
message: "No active SDD session. Use /sdd new to start one."
|
|
325
|
+
};
|
|
326
|
+
}
|
|
327
|
+
const phase = activeBuilder.getSession().phase;
|
|
328
|
+
if (phase === "questioning") {
|
|
329
|
+
const sddCtx = activeBuilder.getAIPrompt();
|
|
330
|
+
return {
|
|
331
|
+
message: "No spec generated yet. Generating now...",
|
|
332
|
+
runText: `[SDD SESSION ACTIVE]
|
|
333
|
+
${sddCtx}
|
|
334
|
+
|
|
335
|
+
---
|
|
336
|
+
User message:
|
|
337
|
+
Generate the complete specification now based on the conversation so far.`
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
if (phase === "spec_review") {
|
|
341
|
+
const spec = activeBuilder.getSession().spec;
|
|
342
|
+
if (!spec) {
|
|
343
|
+
return { message: "No spec to approve." };
|
|
344
|
+
}
|
|
345
|
+
await activeBuilder.saveSpec();
|
|
346
|
+
versioning.recordVersion(spec, "Initial spec approved");
|
|
347
|
+
activeBuilder.approve();
|
|
348
|
+
const implPrompt = activeBuilder.getAIPrompt();
|
|
349
|
+
return {
|
|
350
|
+
message: [
|
|
351
|
+
`\u2705 Spec "${spec.title}" approved and saved!`,
|
|
352
|
+
`ID: ${spec.id}`,
|
|
353
|
+
`Requirements: ${spec.requirements.length}`,
|
|
354
|
+
"",
|
|
355
|
+
"The AI will now generate an implementation plan and tasks."
|
|
356
|
+
].join("\n"),
|
|
357
|
+
runText: `[SDD SESSION ACTIVE]
|
|
358
|
+
${implPrompt}
|
|
359
|
+
|
|
360
|
+
---
|
|
361
|
+
User message:
|
|
362
|
+
Generate the implementation plan and tasks for the approved spec.`
|
|
363
|
+
};
|
|
364
|
+
}
|
|
365
|
+
if (phase === "task_review") {
|
|
366
|
+
activeBuilder.approve();
|
|
367
|
+
const execPrompt = activeBuilder.getAIPrompt();
|
|
368
|
+
return {
|
|
369
|
+
message: "\u2705 Tasks approved! The AI will now execute them one by one.",
|
|
370
|
+
runText: `[SDD SESSION ACTIVE]
|
|
371
|
+
${execPrompt}
|
|
372
|
+
|
|
373
|
+
---
|
|
374
|
+
User message:
|
|
375
|
+
Start executing the tasks one by one.`
|
|
376
|
+
};
|
|
377
|
+
}
|
|
378
|
+
return {
|
|
379
|
+
message: `Current phase is "${phase}". Use /sdd status to see details.`
|
|
380
|
+
};
|
|
381
|
+
}
|
|
382
|
+
// ── Task Execution ─────────────────────────────────────────────────
|
|
383
|
+
case "execute":
|
|
384
|
+
case "run": {
|
|
385
|
+
if (!activeBuilder) {
|
|
386
|
+
return {
|
|
387
|
+
message: "No active SDD session. Use /sdd new to start one."
|
|
388
|
+
};
|
|
389
|
+
}
|
|
390
|
+
const session = activeBuilder.getSession();
|
|
391
|
+
if (session.phase !== "executing" && session.phase !== "task_review") {
|
|
392
|
+
return {
|
|
393
|
+
message: `Cannot execute in phase "${session.phase}". Use /sdd approve first.`
|
|
394
|
+
};
|
|
395
|
+
}
|
|
396
|
+
const execPrompt = activeBuilder.getAIPrompt();
|
|
397
|
+
return {
|
|
398
|
+
message: "\u26A1 Starting task execution. The AI will execute tasks one by one.",
|
|
399
|
+
runText: `[SDD SESSION ACTIVE]
|
|
400
|
+
${execPrompt}
|
|
401
|
+
|
|
402
|
+
---
|
|
403
|
+
User message:
|
|
404
|
+
Start executing the tasks one by one.`
|
|
405
|
+
};
|
|
406
|
+
}
|
|
407
|
+
case "plan":
|
|
408
|
+
case "impl": {
|
|
409
|
+
if (!activeBuilder) {
|
|
410
|
+
return { message: "No active SDD session. Use /sdd new to start one." };
|
|
411
|
+
}
|
|
412
|
+
const session = activeBuilder.getSession();
|
|
413
|
+
if (!session.implementation) {
|
|
414
|
+
return {
|
|
415
|
+
message: session.phase === "implementation" ? "No implementation plan yet. The AI will generate it after /sdd approve." : "No implementation plan in this session."
|
|
416
|
+
};
|
|
417
|
+
}
|
|
418
|
+
return {
|
|
419
|
+
message: [
|
|
420
|
+
"\u2550\u2550\u2550 Implementation Plan \u2550\u2550\u2550",
|
|
421
|
+
"",
|
|
422
|
+
session.implementation
|
|
423
|
+
].join("\n")
|
|
424
|
+
};
|
|
425
|
+
}
|
|
426
|
+
case "spec": {
|
|
427
|
+
if (!activeBuilder) {
|
|
428
|
+
return { message: "No active SDD session. Use /sdd new to start one." };
|
|
429
|
+
}
|
|
430
|
+
const session = activeBuilder.getSession();
|
|
431
|
+
if (!session.spec) {
|
|
432
|
+
return {
|
|
433
|
+
message: session.phase === "questioning" ? "No spec generated yet. Keep answering the AI's questions." : "No spec in this session."
|
|
434
|
+
};
|
|
435
|
+
}
|
|
436
|
+
const spec = session.spec;
|
|
437
|
+
const lines = [
|
|
438
|
+
`\u2550\u2550\u2550 Current Spec \u2550\u2550\u2550`,
|
|
439
|
+
"",
|
|
440
|
+
`Title: ${spec.title}`,
|
|
441
|
+
`Version: ${spec.version}`,
|
|
442
|
+
`Status: ${spec.status}`,
|
|
443
|
+
"",
|
|
444
|
+
"## Overview",
|
|
445
|
+
spec.overview
|
|
446
|
+
];
|
|
447
|
+
if (spec.requirements.length > 0) {
|
|
448
|
+
lines.push("", `## Requirements (${spec.requirements.length})`);
|
|
449
|
+
for (const r of spec.requirements) {
|
|
450
|
+
const ac = r.acceptanceCriteria.length > 0 ? ` \u2192 ${r.acceptanceCriteria.join(", ")}` : "";
|
|
451
|
+
lines.push(` [${r.priority}] ${r.description}${ac}`);
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
return { message: lines.join("\n") };
|
|
455
|
+
}
|
|
456
|
+
case "tasks":
|
|
457
|
+
case "task": {
|
|
458
|
+
if (!activeTaskTracker) {
|
|
459
|
+
return { message: "No tasks generated yet. Use /sdd new to start." };
|
|
460
|
+
}
|
|
461
|
+
const nodes = activeTaskTracker.getAllNodes();
|
|
462
|
+
if (nodes.length === 0) {
|
|
463
|
+
return { message: "No tasks in the current graph." };
|
|
464
|
+
}
|
|
465
|
+
const progress = activeTaskTracker.getProgress();
|
|
466
|
+
const lines = [
|
|
467
|
+
`\u2550\u2550\u2550 Task List (${progress.completed}/${progress.total} done) \u2550\u2550\u2550`,
|
|
468
|
+
""
|
|
469
|
+
];
|
|
470
|
+
for (let i = 0; i < nodes.length; i++) {
|
|
471
|
+
const n = nodes[i];
|
|
472
|
+
const status = n.status === "completed" ? "\u2705" : n.status === "in_progress" ? "\u{1F504}" : n.status === "failed" ? "\u274C" : "\u23F3";
|
|
473
|
+
lines.push(`${i + 1}. ${status} [${n.priority}] ${n.title}`);
|
|
474
|
+
if (n.description) {
|
|
475
|
+
lines.push(` ${n.description.split("\n")[0]}`);
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
return { message: lines.join("\n") };
|
|
479
|
+
}
|
|
480
|
+
case "done":
|
|
481
|
+
case "complete": {
|
|
482
|
+
if (!activeTaskTracker) {
|
|
483
|
+
return { message: "No tasks to complete." };
|
|
484
|
+
}
|
|
485
|
+
if (!restJoined) {
|
|
486
|
+
return { message: "Usage: /sdd done <task title or number>" };
|
|
487
|
+
}
|
|
488
|
+
const nodes = activeTaskTracker.getAllNodes({ status: ["pending", "in_progress"] });
|
|
489
|
+
const num = Number(restJoined);
|
|
490
|
+
let matched = false;
|
|
491
|
+
if (!Number.isNaN(num) && num >= 1 && num <= nodes.length) {
|
|
492
|
+
const node = nodes[num - 1];
|
|
493
|
+
if (node) {
|
|
494
|
+
activeTaskTracker.updateNodeStatus(node.id, "completed");
|
|
495
|
+
matched = true;
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
if (!matched) {
|
|
499
|
+
const match = nodes.find(
|
|
500
|
+
(n) => n.title.toLowerCase().includes(restJoined.toLowerCase()) || restJoined.toLowerCase().includes(n.title.toLowerCase())
|
|
501
|
+
);
|
|
502
|
+
if (match) {
|
|
503
|
+
activeTaskTracker.updateNodeStatus(match.id, "completed");
|
|
504
|
+
matched = true;
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
if (!matched) {
|
|
508
|
+
return { message: `No pending task matching "${restJoined}".` };
|
|
509
|
+
}
|
|
510
|
+
const remaining = activeTaskTracker.getProgress();
|
|
511
|
+
return {
|
|
512
|
+
message: `\u2705 Task completed! ${remaining.completed}/${remaining.total} done (${remaining.percentComplete}%)`
|
|
513
|
+
};
|
|
514
|
+
}
|
|
515
|
+
// ── Session Management ─────────────────────────────────────────────
|
|
516
|
+
case "status": {
|
|
517
|
+
if (!activeBuilder) {
|
|
518
|
+
return { message: "No active SDD session." };
|
|
519
|
+
}
|
|
520
|
+
const session = activeBuilder.getSession();
|
|
521
|
+
const phaseEmoji = {
|
|
522
|
+
questioning: "\u2753",
|
|
523
|
+
spec_review: "\u{1F4CB}",
|
|
524
|
+
implementation: "\u{1F3D7}\uFE0F",
|
|
525
|
+
task_review: "\u{1F4DD}",
|
|
526
|
+
executing: "\u26A1",
|
|
527
|
+
done: "\u2705"
|
|
528
|
+
};
|
|
529
|
+
const progress = getTaskProgress();
|
|
530
|
+
const lines = [
|
|
531
|
+
"\u2550\u2550\u2550 SDD Session Status \u2550\u2550\u2550",
|
|
532
|
+
"",
|
|
533
|
+
`Feature: "${session.title}"`,
|
|
534
|
+
`Phase: ${phaseEmoji[session.phase]} ${session.phase}`,
|
|
535
|
+
`Questions asked: ${session.questionCount}`
|
|
536
|
+
];
|
|
537
|
+
if (session.spec) {
|
|
538
|
+
lines.push(`Spec: ${session.spec.title} (${session.spec.requirements.length} requirements)`);
|
|
539
|
+
lines.push(` Requirements: ${session.spec.requirements.map((r) => r.description).join(", ")}`);
|
|
540
|
+
}
|
|
541
|
+
if (session.implementation) {
|
|
542
|
+
const planPreview = session.implementation.split("\n").slice(0, 3).join(" ");
|
|
543
|
+
lines.push(`Implementation: ${planPreview}${session.implementation.length > 100 ? "..." : ""}`);
|
|
544
|
+
}
|
|
545
|
+
if (progress && progress.total > 0) {
|
|
546
|
+
lines.push(`Tasks: ${progress.completed}/${progress.total} (${progress.percent}%)`);
|
|
547
|
+
}
|
|
548
|
+
lines.push("", `Session ID: ${session.id}`);
|
|
549
|
+
lines.push("Commands: /sdd plan \xB7 /sdd tasks \xB7 /sdd approve \xB7 /sdd cancel");
|
|
550
|
+
return {
|
|
551
|
+
message: lines.join("\n")
|
|
552
|
+
};
|
|
553
|
+
}
|
|
554
|
+
case "cancel": {
|
|
555
|
+
const sessionPath = path18.join(projectRoot, ".wrongstack", "sdd-session.json");
|
|
556
|
+
let deletedFromDisk = false;
|
|
557
|
+
try {
|
|
558
|
+
const fsp = await import('fs/promises');
|
|
559
|
+
await fsp.unlink(sessionPath);
|
|
560
|
+
deletedFromDisk = true;
|
|
561
|
+
} catch {
|
|
562
|
+
}
|
|
563
|
+
if (activeBuilder) {
|
|
564
|
+
const title = activeBuilder.getSession().title;
|
|
565
|
+
await activeBuilder.deleteSession();
|
|
566
|
+
activeBuilder = null;
|
|
567
|
+
activeTaskStore = null;
|
|
568
|
+
activeTaskTracker = null;
|
|
569
|
+
activeTaskGraphId = null;
|
|
570
|
+
return { message: `SDD session for "${title}" cancelled.` };
|
|
571
|
+
}
|
|
572
|
+
if (deletedFromDisk) {
|
|
573
|
+
return { message: "Stale SDD session file deleted. You can now use /sdd new." };
|
|
574
|
+
}
|
|
575
|
+
return { message: "No active SDD session." };
|
|
576
|
+
}
|
|
577
|
+
case "resume": {
|
|
578
|
+
if (activeBuilder) {
|
|
579
|
+
return { message: "An SDD session is already active. Use /sdd cancel first." };
|
|
580
|
+
}
|
|
581
|
+
const sessionPath = path18.join(projectRoot, ".wrongstack", "sdd-session.json");
|
|
582
|
+
const projectContext = await gatherProjectContext(projectRoot);
|
|
583
|
+
activeBuilder = new AISpecBuilder({
|
|
584
|
+
store: specStore,
|
|
585
|
+
projectContext,
|
|
586
|
+
minQuestions: 2,
|
|
587
|
+
maxQuestions: 10,
|
|
588
|
+
sessionPath
|
|
589
|
+
});
|
|
590
|
+
const loaded = await activeBuilder.loadSession();
|
|
591
|
+
if (!loaded) {
|
|
592
|
+
activeBuilder = null;
|
|
593
|
+
return { message: "No saved SDD session found. Use /sdd new to start one." };
|
|
594
|
+
}
|
|
595
|
+
const session = activeBuilder.getSession();
|
|
596
|
+
let taskCount = 0;
|
|
597
|
+
let completedCount = 0;
|
|
598
|
+
const taskGraphId = activeBuilder.getTaskGraphId();
|
|
599
|
+
if (taskGraphId) {
|
|
600
|
+
try {
|
|
601
|
+
const store = new DefaultTaskStore();
|
|
602
|
+
const tracker = new TaskTracker({ store });
|
|
603
|
+
const graph = await tracker.loadGraph(taskGraphId);
|
|
604
|
+
if (graph) {
|
|
605
|
+
activeTaskStore = store;
|
|
606
|
+
activeTaskTracker = tracker;
|
|
607
|
+
activeTaskGraphId = taskGraphId;
|
|
608
|
+
const progress = tracker.getProgress();
|
|
609
|
+
taskCount = progress.total;
|
|
610
|
+
completedCount = progress.completed;
|
|
611
|
+
}
|
|
612
|
+
} catch {
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
const resumePrompt = activeBuilder.getAIPrompt();
|
|
616
|
+
return {
|
|
617
|
+
message: [
|
|
618
|
+
`\u2554\u2550\u2550\u2550 SDD Session Resumed \u2550\u2550\u2550\u2557`,
|
|
619
|
+
"",
|
|
620
|
+
`Feature: "${session.title}"`,
|
|
621
|
+
`Phase: ${session.phase}`,
|
|
622
|
+
`Questions asked: ${session.questionCount}`,
|
|
623
|
+
session.spec ? `Spec: ${session.spec.title}` : "",
|
|
624
|
+
taskCount > 0 ? `Tasks: ${completedCount}/${taskCount} completed` : "",
|
|
625
|
+
"",
|
|
626
|
+
"The AI will continue from where you left off."
|
|
627
|
+
].filter(Boolean).join("\n"),
|
|
628
|
+
runText: `[SDD SESSION ACTIVE]
|
|
629
|
+
${resumePrompt}
|
|
630
|
+
|
|
631
|
+
---
|
|
632
|
+
User message:
|
|
633
|
+
Continue from where we left off. Check the session status and proceed.`
|
|
634
|
+
};
|
|
635
|
+
}
|
|
636
|
+
// ── Spec Browsing ──────────────────────────────────────────────────
|
|
637
|
+
case "list":
|
|
638
|
+
case "ls": {
|
|
639
|
+
const entries = await specStore.list();
|
|
640
|
+
if (entries.length === 0) {
|
|
641
|
+
return { message: "No specs saved. Use /sdd new to create one." };
|
|
642
|
+
}
|
|
643
|
+
const lines = entries.map((e, i) => {
|
|
644
|
+
const status = e.status === "draft" ? "\u{1F4DD}" : e.status === "approved" ? "\u2705" : "\u{1F4CB}";
|
|
645
|
+
return `${i + 1}. ${status} ${e.title} (${e.version}) \u2014 ${e.id.slice(0, 8)}...`;
|
|
646
|
+
});
|
|
647
|
+
return { message: `Saved Specs:
|
|
648
|
+
${lines.join("\n")}` };
|
|
649
|
+
}
|
|
650
|
+
case "show":
|
|
651
|
+
case "view": {
|
|
652
|
+
const spec = await findSpec(specStore, restJoined);
|
|
653
|
+
if (!spec) return { message: `Spec "${restJoined}" not found.` };
|
|
654
|
+
const parser = new SpecParser();
|
|
655
|
+
const analysis = parser.analyze(spec);
|
|
656
|
+
return {
|
|
657
|
+
message: [
|
|
658
|
+
`# ${spec.title}`,
|
|
659
|
+
`Version: ${spec.version} | Status: ${spec.status}`,
|
|
660
|
+
"",
|
|
661
|
+
"## Overview",
|
|
662
|
+
spec.overview,
|
|
663
|
+
"",
|
|
664
|
+
`## Requirements (${spec.requirements.length})`,
|
|
665
|
+
...spec.requirements.map((r) => {
|
|
666
|
+
const tags = `[${r.type}][${r.priority}]`;
|
|
667
|
+
const ac = r.acceptanceCriteria.length > 0 ? `
|
|
668
|
+
AC: ${r.acceptanceCriteria.join(", ")}` : "";
|
|
669
|
+
return `- ${tags} ${r.description}${ac}`;
|
|
670
|
+
}),
|
|
671
|
+
"",
|
|
672
|
+
renderSpecAnalysis(spec, {
|
|
673
|
+
completeness: analysis.completeness,
|
|
674
|
+
gaps: analysis.gaps,
|
|
675
|
+
risks: analysis.risks.map((r) => r.risk),
|
|
676
|
+
suggestions: analysis.suggestions
|
|
677
|
+
})
|
|
678
|
+
].join("\n")
|
|
679
|
+
};
|
|
680
|
+
}
|
|
681
|
+
case "templates": {
|
|
682
|
+
const templates = listTemplates();
|
|
683
|
+
const lines = templates.map(
|
|
684
|
+
(t) => ` ${t.id}: ${t.name} \u2014 ${t.description}`
|
|
685
|
+
);
|
|
686
|
+
return {
|
|
687
|
+
message: `Available Templates:
|
|
688
|
+
${lines.join("\n")}`
|
|
689
|
+
};
|
|
690
|
+
}
|
|
691
|
+
case "from": {
|
|
692
|
+
const templateId = restJoined || "feature";
|
|
693
|
+
const template = getTemplate(templateId);
|
|
694
|
+
if (!template) {
|
|
695
|
+
return {
|
|
696
|
+
message: `Template "${templateId}" not found.
|
|
697
|
+
Available: ${listTemplates().map((t) => t.id).join(", ")}`
|
|
698
|
+
};
|
|
699
|
+
}
|
|
700
|
+
const skeleton = templateToMarkdown(template, "New Specification");
|
|
701
|
+
const spec = await specStore.createDraft("New Specification");
|
|
702
|
+
await specStore.update(spec.id, { sections: [] });
|
|
703
|
+
return {
|
|
704
|
+
message: [
|
|
705
|
+
`Created draft spec from template "${template.name}".`,
|
|
706
|
+
`ID: ${spec.id}`,
|
|
707
|
+
"",
|
|
708
|
+
"Edit the spec through the AI conversation or /sdd show to review.",
|
|
709
|
+
"",
|
|
710
|
+
skeleton
|
|
711
|
+
].join("\n")
|
|
712
|
+
};
|
|
713
|
+
}
|
|
714
|
+
case "version":
|
|
715
|
+
case "history": {
|
|
716
|
+
const spec = await findSpec(specStore, restJoined);
|
|
717
|
+
if (!spec)
|
|
718
|
+
return { message: `Spec "${restJoined}" not found.` };
|
|
719
|
+
const history = versioning.getHistory(spec.id);
|
|
720
|
+
if (history.length === 0) {
|
|
721
|
+
return {
|
|
722
|
+
message: `No version history for "${spec.title}".`
|
|
723
|
+
};
|
|
724
|
+
}
|
|
725
|
+
const lines = history.map(
|
|
726
|
+
(v, i) => `${i + 1}. v${v.version} \u2014 ${new Date(v.timestamp).toISOString()}${v.changeDescription ? ` (${v.changeDescription})` : ""}`
|
|
727
|
+
);
|
|
728
|
+
return {
|
|
729
|
+
message: `Version History for "${spec.title}":
|
|
730
|
+
${lines.join("\n")}`
|
|
731
|
+
};
|
|
732
|
+
}
|
|
733
|
+
default:
|
|
734
|
+
return {
|
|
735
|
+
message: `Unknown command "${verb}".
|
|
736
|
+
|
|
737
|
+
${sddHelp()}`
|
|
738
|
+
};
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
};
|
|
742
|
+
}
|
|
743
|
+
function sddHelp() {
|
|
744
|
+
return [
|
|
745
|
+
"",
|
|
746
|
+
"\u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557",
|
|
747
|
+
"\u2551 \u{1F680} SDD \u2014 AI-Driven Spec Builder \u2551",
|
|
748
|
+
"\u255A\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255D",
|
|
749
|
+
"",
|
|
750
|
+
" \u250C\u2500 \u{1F195} Start \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510",
|
|
751
|
+
" \u2502 /sdd new [title] Start a new spec session \u2502",
|
|
752
|
+
" \u2502 /sdd new --force Start fresh (skip resume check) \u2502",
|
|
753
|
+
" \u2502 /sdd resume Resume a saved session \u2502",
|
|
754
|
+
" \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518",
|
|
755
|
+
"",
|
|
756
|
+
" \u250C\u2500 \u{1F504} Flow \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510",
|
|
757
|
+
" \u2502 /sdd approve Approve current phase \u2502",
|
|
758
|
+
" \u2502 /sdd spec Show current session's spec \u2502",
|
|
759
|
+
" \u2502 /sdd plan Show implementation plan \u2502",
|
|
760
|
+
" \u2502 /sdd execute Execute generated tasks \u2502",
|
|
761
|
+
" \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518",
|
|
762
|
+
"",
|
|
763
|
+
" \u250C\u2500 \u{1F4CB} Task Management \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510",
|
|
764
|
+
" \u2502 /sdd tasks Show current task list \u2502",
|
|
765
|
+
" \u2502 /sdd done <N> Mark task complete (by # or name) \u2502",
|
|
766
|
+
" \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518",
|
|
767
|
+
"",
|
|
768
|
+
" \u250C\u2500 \u{1F4CA} Info \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510",
|
|
769
|
+
" \u2502 /sdd status Show session status \u2502",
|
|
770
|
+
" \u2502 /sdd cancel Cancel session \u2502",
|
|
771
|
+
" \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518",
|
|
772
|
+
"",
|
|
773
|
+
" \u250C\u2500 \u{1F4C1} Spec History \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510",
|
|
774
|
+
" \u2502 /sdd list List saved specs \u2502",
|
|
775
|
+
" \u2502 /sdd show <id> Show spec details \u2502",
|
|
776
|
+
" \u2502 /sdd templates List available templates \u2502",
|
|
777
|
+
" \u2502 /sdd from <tmpl> Create from template \u2502",
|
|
778
|
+
" \u2502 /sdd version <id> Show version history \u2502",
|
|
779
|
+
" \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518",
|
|
780
|
+
"",
|
|
781
|
+
" \u250C\u2500 \u{1F4A1} Quick Start \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510",
|
|
782
|
+
" \u2502 \u2502",
|
|
783
|
+
" \u2502 1. /sdd new Auth System \u2502",
|
|
784
|
+
" \u2502 \u2192 AI starts asking questions \u2502",
|
|
785
|
+
" \u2502 \u2502",
|
|
786
|
+
" \u2502 2. Just type your answers naturally \u2502",
|
|
787
|
+
" \u2502 \u2192 AI continues the interview \u2502",
|
|
788
|
+
" \u2502 \u2502",
|
|
789
|
+
" \u2502 3. AI generates spec (auto-detected) \u2502",
|
|
790
|
+
" \u2502 \u2192 /sdd approve \u2502",
|
|
791
|
+
" \u2502 \u2502",
|
|
792
|
+
" \u2502 3. AI generates implementation + tasks \u2502",
|
|
793
|
+
" \u2502 \u2192 /sdd approve \u2502",
|
|
794
|
+
" \u2502 \u2502",
|
|
795
|
+
" \u2502 4. AI executes tasks one by one \u2502",
|
|
796
|
+
" \u2502 \u2192 /sdd tasks (view progress) \u2502",
|
|
797
|
+
" \u2502 \u2192 /sdd done 1 (manual completion) \u2502",
|
|
798
|
+
" \u2502 \u2502",
|
|
799
|
+
" \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518",
|
|
800
|
+
""
|
|
801
|
+
].join("\n");
|
|
802
|
+
}
|
|
803
|
+
async function gatherProjectContext(projectRoot) {
|
|
804
|
+
const parts = [];
|
|
805
|
+
try {
|
|
806
|
+
const fsp = await import('fs/promises');
|
|
807
|
+
const pkgPath = path18.join(projectRoot, "package.json");
|
|
808
|
+
const pkgRaw = await fsp.readFile(pkgPath, "utf8");
|
|
809
|
+
const pkg = JSON.parse(pkgRaw);
|
|
810
|
+
parts.push(`Project: ${String(pkg.name ?? "unknown")}`);
|
|
811
|
+
parts.push(`Description: ${String(pkg.description ?? "none")}`);
|
|
812
|
+
if (pkg.dependencies) {
|
|
813
|
+
const deps = Object.keys(pkg.dependencies);
|
|
814
|
+
parts.push(`Dependencies: ${deps.slice(0, 20).join(", ")}${deps.length > 20 ? "..." : ""}`);
|
|
815
|
+
}
|
|
816
|
+
if (pkg.devDependencies) {
|
|
817
|
+
const devDeps = Object.keys(pkg.devDependencies);
|
|
818
|
+
parts.push(`Dev Dependencies: ${devDeps.slice(0, 15).join(", ")}${devDeps.length > 15 ? "..." : ""}`);
|
|
819
|
+
}
|
|
820
|
+
} catch {
|
|
821
|
+
}
|
|
822
|
+
try {
|
|
823
|
+
const fsp = await import('fs/promises');
|
|
824
|
+
const tsconfigPath = path18.join(projectRoot, "tsconfig.json");
|
|
825
|
+
await fsp.access(tsconfigPath);
|
|
826
|
+
parts.push("Language: TypeScript");
|
|
827
|
+
} catch {
|
|
828
|
+
}
|
|
829
|
+
try {
|
|
830
|
+
const fsp = await import('fs/promises');
|
|
831
|
+
const srcDir = path18.join(projectRoot, "src");
|
|
832
|
+
const entries = await fsp.readdir(srcDir, { withFileTypes: true });
|
|
833
|
+
const dirs = entries.filter((e) => e.isDirectory()).map((e) => e.name);
|
|
834
|
+
if (dirs.length > 0) {
|
|
835
|
+
parts.push(`Source structure: src/${dirs.join(", src/")}`);
|
|
836
|
+
}
|
|
837
|
+
} catch {
|
|
838
|
+
}
|
|
839
|
+
return parts.join("\n");
|
|
840
|
+
}
|
|
841
|
+
async function findSpec(store, idOrTitle) {
|
|
842
|
+
if (!idOrTitle) return null;
|
|
843
|
+
const byId = await store.load(idOrTitle);
|
|
844
|
+
if (byId) return byId;
|
|
845
|
+
const all = await store.list();
|
|
846
|
+
const match = all.find(
|
|
847
|
+
(e) => e.id.startsWith(idOrTitle) || e.title.toLowerCase().includes(idOrTitle.toLowerCase())
|
|
848
|
+
);
|
|
849
|
+
if (match) return store.load(match.id);
|
|
850
|
+
return null;
|
|
851
|
+
}
|
|
852
|
+
var activeBuilder, activeTaskStore, activeTaskTracker, activeTaskGraphId;
|
|
853
|
+
var init_sdd = __esm({
|
|
854
|
+
"src/slash-commands/sdd.ts"() {
|
|
855
|
+
activeBuilder = null;
|
|
856
|
+
activeTaskStore = null;
|
|
857
|
+
activeTaskTracker = null;
|
|
858
|
+
activeTaskGraphId = null;
|
|
859
|
+
}
|
|
860
|
+
});
|
|
28
861
|
function normalizeKeys(cfg) {
|
|
29
862
|
if (Array.isArray(cfg.apiKeys) && cfg.apiKeys.length > 0) {
|
|
30
863
|
return cfg.apiKeys.map((k) => ({ ...k }));
|
|
@@ -549,7 +1382,7 @@ async function runWebUI(opts) {
|
|
|
549
1382
|
if (!opts.globalConfigPath) return {};
|
|
550
1383
|
let raw;
|
|
551
1384
|
try {
|
|
552
|
-
raw = await
|
|
1385
|
+
raw = await fs5.readFile(opts.globalConfigPath, "utf8");
|
|
553
1386
|
} catch {
|
|
554
1387
|
return {};
|
|
555
1388
|
}
|
|
@@ -559,13 +1392,16 @@ async function runWebUI(opts) {
|
|
|
559
1392
|
} catch {
|
|
560
1393
|
return {};
|
|
561
1394
|
}
|
|
562
|
-
|
|
1395
|
+
if (!parsed.providers) return {};
|
|
1396
|
+
const keyFile = path18.join(path18.dirname(opts.globalConfigPath), ".key");
|
|
1397
|
+
const vault = new DefaultSecretVault$1({ keyFile });
|
|
1398
|
+
return decryptConfigSecrets$1(parsed.providers, vault);
|
|
563
1399
|
}
|
|
564
1400
|
async function saveProviders(providers) {
|
|
565
1401
|
if (!opts.globalConfigPath) return;
|
|
566
1402
|
let raw;
|
|
567
1403
|
try {
|
|
568
|
-
raw = await
|
|
1404
|
+
raw = await fs5.readFile(opts.globalConfigPath, "utf8");
|
|
569
1405
|
} catch {
|
|
570
1406
|
raw = "{}";
|
|
571
1407
|
}
|
|
@@ -576,7 +1412,7 @@ async function runWebUI(opts) {
|
|
|
576
1412
|
parsed = {};
|
|
577
1413
|
}
|
|
578
1414
|
parsed.providers = providers;
|
|
579
|
-
const keyFile =
|
|
1415
|
+
const keyFile = path18.join(path18.dirname(opts.globalConfigPath), ".key");
|
|
580
1416
|
const vault = new DefaultSecretVault$1({ keyFile });
|
|
581
1417
|
const encrypted = encryptConfigSecrets(parsed, vault);
|
|
582
1418
|
await atomicWrite(opts.globalConfigPath, JSON.stringify(encrypted, null, 2), { mode: 384 });
|
|
@@ -728,10 +1564,10 @@ function parseSpawnFlags(input) {
|
|
|
728
1564
|
return { description: rest.trim(), opts };
|
|
729
1565
|
}
|
|
730
1566
|
async function bootConfig(flags) {
|
|
731
|
-
const cwd = typeof flags["cwd"] === "string" ?
|
|
1567
|
+
const cwd = typeof flags["cwd"] === "string" ? path18.resolve(flags["cwd"]) : process.cwd();
|
|
732
1568
|
const pathResolver = new DefaultPathResolver(cwd);
|
|
733
1569
|
const projectRoot = pathResolver.projectRoot;
|
|
734
|
-
const userHome =
|
|
1570
|
+
const userHome = os4.homedir();
|
|
735
1571
|
const wpaths = resolveWstackPaths({ projectRoot, userHome });
|
|
736
1572
|
await ensureProjectMeta(wpaths, projectRoot);
|
|
737
1573
|
const vault = new DefaultSecretVault({ keyFile: wpaths.secretsKey });
|
|
@@ -779,13 +1615,13 @@ function flagsToConfigPatch(flags) {
|
|
|
779
1615
|
}
|
|
780
1616
|
async function ensureProjectMeta(paths, projectRoot) {
|
|
781
1617
|
try {
|
|
782
|
-
await
|
|
1618
|
+
await fs5.mkdir(paths.projectDir, { recursive: true });
|
|
783
1619
|
const meta = {
|
|
784
1620
|
hash: paths.projectHash,
|
|
785
1621
|
root: projectRoot,
|
|
786
1622
|
lastSeen: (/* @__PURE__ */ new Date()).toISOString()
|
|
787
1623
|
};
|
|
788
|
-
await
|
|
1624
|
+
await fs5.writeFile(paths.projectMeta, JSON.stringify(meta, null, 2));
|
|
789
1625
|
} catch {
|
|
790
1626
|
}
|
|
791
1627
|
}
|
|
@@ -795,11 +1631,11 @@ var ReadlineInputReader = class {
|
|
|
795
1631
|
history = [];
|
|
796
1632
|
pending = false;
|
|
797
1633
|
constructor(opts = {}) {
|
|
798
|
-
this.historyFile = opts.historyFile ??
|
|
1634
|
+
this.historyFile = opts.historyFile ?? path18.join(os4.homedir(), ".wrongstack", "history");
|
|
799
1635
|
}
|
|
800
1636
|
async loadHistory() {
|
|
801
1637
|
try {
|
|
802
|
-
const raw = await
|
|
1638
|
+
const raw = await fs5.readFile(this.historyFile, "utf8");
|
|
803
1639
|
this.history = raw.split("\n").filter(Boolean).slice(-1e3);
|
|
804
1640
|
} catch {
|
|
805
1641
|
this.history = [];
|
|
@@ -807,8 +1643,8 @@ var ReadlineInputReader = class {
|
|
|
807
1643
|
}
|
|
808
1644
|
async saveHistory() {
|
|
809
1645
|
try {
|
|
810
|
-
await
|
|
811
|
-
await
|
|
1646
|
+
await fs5.mkdir(path18.dirname(this.historyFile), { recursive: true });
|
|
1647
|
+
await fs5.writeFile(this.historyFile, this.history.slice(-1e3).join("\n"));
|
|
812
1648
|
} catch {
|
|
813
1649
|
}
|
|
814
1650
|
}
|
|
@@ -1278,7 +2114,7 @@ async function saveToGlobalConfig(configPath, provider, model) {
|
|
|
1278
2114
|
}
|
|
1279
2115
|
async function pathExists(file) {
|
|
1280
2116
|
try {
|
|
1281
|
-
await
|
|
2117
|
+
await fs5.access(file);
|
|
1282
2118
|
return true;
|
|
1283
2119
|
} catch {
|
|
1284
2120
|
return false;
|
|
@@ -1289,10 +2125,10 @@ async function detectPackageManager(root, declared) {
|
|
|
1289
2125
|
const name = declared.split("@")[0];
|
|
1290
2126
|
if (name) return name;
|
|
1291
2127
|
}
|
|
1292
|
-
if (await pathExists(
|
|
1293
|
-
if (await pathExists(
|
|
1294
|
-
if (await pathExists(
|
|
1295
|
-
if (await pathExists(
|
|
2128
|
+
if (await pathExists(path18.join(root, "pnpm-lock.yaml"))) return "pnpm";
|
|
2129
|
+
if (await pathExists(path18.join(root, "bun.lockb"))) return "bun";
|
|
2130
|
+
if (await pathExists(path18.join(root, "bun.lock"))) return "bun";
|
|
2131
|
+
if (await pathExists(path18.join(root, "yarn.lock"))) return "yarn";
|
|
1296
2132
|
return "npm";
|
|
1297
2133
|
}
|
|
1298
2134
|
function hasUsableScript(scripts, name) {
|
|
@@ -1313,7 +2149,7 @@ function parseMakeTargets(makefile) {
|
|
|
1313
2149
|
async function detectProjectFacts(root) {
|
|
1314
2150
|
const facts = { hints: [] };
|
|
1315
2151
|
try {
|
|
1316
|
-
const pkg = JSON.parse(await
|
|
2152
|
+
const pkg = JSON.parse(await fs5.readFile(path18.join(root, "package.json"), "utf8"));
|
|
1317
2153
|
const scripts = pkg.scripts ?? {};
|
|
1318
2154
|
const pm = await detectPackageManager(root, pkg.packageManager);
|
|
1319
2155
|
if (hasUsableScript(scripts, "build")) facts.build = `${pm} run build`;
|
|
@@ -1327,14 +2163,14 @@ async function detectProjectFacts(root) {
|
|
|
1327
2163
|
} catch {
|
|
1328
2164
|
}
|
|
1329
2165
|
try {
|
|
1330
|
-
if (!await pathExists(
|
|
2166
|
+
if (!await pathExists(path18.join(root, "pyproject.toml"))) throw new Error("not python");
|
|
1331
2167
|
facts.test ??= "pytest";
|
|
1332
2168
|
facts.lint ??= "ruff check .";
|
|
1333
2169
|
facts.hints.push("pyproject.toml");
|
|
1334
2170
|
} catch {
|
|
1335
2171
|
}
|
|
1336
2172
|
try {
|
|
1337
|
-
if (!await pathExists(
|
|
2173
|
+
if (!await pathExists(path18.join(root, "go.mod"))) throw new Error("not go");
|
|
1338
2174
|
facts.build ??= "go build ./...";
|
|
1339
2175
|
facts.test ??= "go test ./...";
|
|
1340
2176
|
facts.run ??= "go run .";
|
|
@@ -1342,7 +2178,7 @@ async function detectProjectFacts(root) {
|
|
|
1342
2178
|
} catch {
|
|
1343
2179
|
}
|
|
1344
2180
|
try {
|
|
1345
|
-
if (!await pathExists(
|
|
2181
|
+
if (!await pathExists(path18.join(root, "Cargo.toml"))) throw new Error("not rust");
|
|
1346
2182
|
facts.build ??= "cargo build";
|
|
1347
2183
|
facts.test ??= "cargo test";
|
|
1348
2184
|
facts.lint ??= "cargo clippy";
|
|
@@ -1351,7 +2187,7 @@ async function detectProjectFacts(root) {
|
|
|
1351
2187
|
} catch {
|
|
1352
2188
|
}
|
|
1353
2189
|
try {
|
|
1354
|
-
const makefile = await
|
|
2190
|
+
const makefile = await fs5.readFile(path18.join(root, "Makefile"), "utf8");
|
|
1355
2191
|
const targets = parseMakeTargets(makefile);
|
|
1356
2192
|
facts.build ??= targets.has("build") ? "make build" : "make";
|
|
1357
2193
|
if (targets.has("test")) facts.test ??= "make test";
|
|
@@ -1485,6 +2321,175 @@ function buildClearCommand(opts) {
|
|
|
1485
2321
|
}
|
|
1486
2322
|
};
|
|
1487
2323
|
}
|
|
2324
|
+
async function runGit(args, cwd) {
|
|
2325
|
+
return new Promise((resolve3) => {
|
|
2326
|
+
const child = spawn("git", args, {
|
|
2327
|
+
cwd,
|
|
2328
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
2329
|
+
});
|
|
2330
|
+
let stdout = "";
|
|
2331
|
+
let stderr = "";
|
|
2332
|
+
child.stdout?.on("data", (d) => stdout += d);
|
|
2333
|
+
child.stderr?.on("data", (d) => stderr += d);
|
|
2334
|
+
child.on("close", (code) => resolve3({ stdout, stderr, code: code ?? 0 }));
|
|
2335
|
+
});
|
|
2336
|
+
}
|
|
2337
|
+
function detectCommitType(stats) {
|
|
2338
|
+
const lines = stats.split("\n");
|
|
2339
|
+
const hasTestFiles = lines.some(
|
|
2340
|
+
(l) => l.includes("_test.") || l.includes(".test.") || l.includes(".spec.")
|
|
2341
|
+
);
|
|
2342
|
+
const hasDocs = lines.some(
|
|
2343
|
+
(l) => l.includes("README") || l.includes("CHANGELOG") || l.includes("docs/") || l.includes(".md")
|
|
2344
|
+
);
|
|
2345
|
+
const hasConfig = lines.some(
|
|
2346
|
+
(l) => l.includes("config") || l.includes("tsconfig") || l.includes(".json")
|
|
2347
|
+
);
|
|
2348
|
+
if (hasTestFiles) return "test";
|
|
2349
|
+
if (hasDocs) return "docs";
|
|
2350
|
+
if (hasConfig) return "chore";
|
|
2351
|
+
return "feat";
|
|
2352
|
+
}
|
|
2353
|
+
async function generateCommitMessage(cwd) {
|
|
2354
|
+
const statsResult = await runGit(["diff", "--stat"], cwd);
|
|
2355
|
+
if (statsResult.code !== 0) return "chore: update";
|
|
2356
|
+
const nameResult = await runGit(["diff", "--name-only"], cwd);
|
|
2357
|
+
const files = nameResult.stdout.split("\n").filter(Boolean);
|
|
2358
|
+
const commitType = detectCommitType(statsResult.stdout);
|
|
2359
|
+
let scope = "";
|
|
2360
|
+
if (files.length > 0) {
|
|
2361
|
+
const primary = files[0].split("/")[0];
|
|
2362
|
+
if (primary && primary !== "packages" && primary !== "apps" && primary !== "node_modules") {
|
|
2363
|
+
scope = `(${primary})`;
|
|
2364
|
+
}
|
|
2365
|
+
}
|
|
2366
|
+
if (files.length === 0) {
|
|
2367
|
+
return `${commitType}${scope}: update`;
|
|
2368
|
+
}
|
|
2369
|
+
if (files.length <= 3) {
|
|
2370
|
+
const summary2 = files.map((f) => f.split("/").pop()).join(", ");
|
|
2371
|
+
return `${commitType}${scope}: ${summary2}`;
|
|
2372
|
+
}
|
|
2373
|
+
const summary = files.slice(0, 3).map((f) => f.split("/").pop()).join(", ") + ` and ${files.length - 3} more`;
|
|
2374
|
+
return `${commitType}${scope}: ${summary}`;
|
|
2375
|
+
}
|
|
2376
|
+
async function hasUncommittedChanges(cwd) {
|
|
2377
|
+
const result = await runGit(["status", "--porcelain"], cwd);
|
|
2378
|
+
return result.stdout.trim().length > 0;
|
|
2379
|
+
}
|
|
2380
|
+
async function isGitRepo(cwd) {
|
|
2381
|
+
const result = await runGit(["rev-parse", "--git-dir"], cwd);
|
|
2382
|
+
return result.code === 0;
|
|
2383
|
+
}
|
|
2384
|
+
function buildCommitCommand(_opts) {
|
|
2385
|
+
return {
|
|
2386
|
+
name: "commit",
|
|
2387
|
+
description: "Stage all changes and commit with auto-generated message.",
|
|
2388
|
+
aliases: ["gc"],
|
|
2389
|
+
async run(args, ctx) {
|
|
2390
|
+
const cwd = ctx?.cwd ?? process.cwd();
|
|
2391
|
+
if (!await isGitRepo(cwd)) {
|
|
2392
|
+
return { message: "Not a git repository." };
|
|
2393
|
+
}
|
|
2394
|
+
if (!await hasUncommittedChanges(cwd)) {
|
|
2395
|
+
return { message: "Nothing to commit (working tree clean)." };
|
|
2396
|
+
}
|
|
2397
|
+
const dryRun = args.includes("--dry-run") || args.includes("-n");
|
|
2398
|
+
const message = await generateCommitMessage(cwd);
|
|
2399
|
+
if (dryRun) {
|
|
2400
|
+
return {
|
|
2401
|
+
message: `Would commit:
|
|
2402
|
+
|
|
2403
|
+
${color.green(message)}
|
|
2404
|
+
|
|
2405
|
+
${color.dim("(dry-run \u2014 no actual commit)")}`
|
|
2406
|
+
};
|
|
2407
|
+
}
|
|
2408
|
+
const stageResult = await runGit(["add", "."], cwd);
|
|
2409
|
+
if (stageResult.code !== 0) {
|
|
2410
|
+
return { message: `Stage failed: ${stageResult.stderr}` };
|
|
2411
|
+
}
|
|
2412
|
+
const commitResult = await runGit(["commit", "-m", message], cwd);
|
|
2413
|
+
if (commitResult.code !== 0) {
|
|
2414
|
+
return { message: `Commit failed: ${commitResult.stderr}` };
|
|
2415
|
+
}
|
|
2416
|
+
const hashResult = await runGit(["rev-parse", "--short", "HEAD"], cwd);
|
|
2417
|
+
const hash = hashResult.stdout.trim();
|
|
2418
|
+
const pushResult = await runGit(["remote"], cwd);
|
|
2419
|
+
const hasRemote = pushResult.stdout.trim().length > 0;
|
|
2420
|
+
let pushMsg = "";
|
|
2421
|
+
if (hasRemote) {
|
|
2422
|
+
pushMsg = `
|
|
2423
|
+
|
|
2424
|
+
${color.dim("Tip: Run /push to push to remote")}`;
|
|
2425
|
+
}
|
|
2426
|
+
return {
|
|
2427
|
+
message: `${color.green("\u2713")} Committed: ${color.bold(message)}
|
|
2428
|
+
${color.dim(hash)}${pushMsg}`
|
|
2429
|
+
};
|
|
2430
|
+
}
|
|
2431
|
+
};
|
|
2432
|
+
}
|
|
2433
|
+
function buildGitcheckCommand(_opts) {
|
|
2434
|
+
return {
|
|
2435
|
+
name: "gitcheck",
|
|
2436
|
+
description: "Check for uncommitted changes (for system prompt integration).",
|
|
2437
|
+
aliases: ["gcstatus"],
|
|
2438
|
+
async run(_args, ctx) {
|
|
2439
|
+
const cwd = ctx?.cwd ?? process.cwd();
|
|
2440
|
+
if (!await isGitRepo(cwd)) {
|
|
2441
|
+
return { message: "" };
|
|
2442
|
+
}
|
|
2443
|
+
if (!await hasUncommittedChanges(cwd)) {
|
|
2444
|
+
return { message: "" };
|
|
2445
|
+
}
|
|
2446
|
+
const statusResult = await runGit(["status", "--porcelain"], cwd);
|
|
2447
|
+
const lines = statusResult.stdout.split("\n").filter(Boolean);
|
|
2448
|
+
const count = lines.length;
|
|
2449
|
+
if (count === 0) return { message: "" };
|
|
2450
|
+
return {
|
|
2451
|
+
message: `\u26A0 ${color.yellow(`${count} uncommitted change${count > 1 ? "s" : ""}`)} \u2014 consider /commit`
|
|
2452
|
+
};
|
|
2453
|
+
}
|
|
2454
|
+
};
|
|
2455
|
+
}
|
|
2456
|
+
function buildPushCommand(_opts) {
|
|
2457
|
+
return {
|
|
2458
|
+
name: "push",
|
|
2459
|
+
description: "Push to remote after commit.",
|
|
2460
|
+
async run(args, ctx) {
|
|
2461
|
+
const cwd = ctx?.cwd ?? process.cwd();
|
|
2462
|
+
if (!await isGitRepo(cwd)) {
|
|
2463
|
+
return { message: "Not a git repository." };
|
|
2464
|
+
}
|
|
2465
|
+
const dryRun = args.includes("--dry-run") || args.includes("-n");
|
|
2466
|
+
const force = args.includes("--force") || args.includes("-f");
|
|
2467
|
+
const remoteResult = await runGit(["remote"], cwd);
|
|
2468
|
+
const remotes = remoteResult.stdout.split("\n").filter(Boolean);
|
|
2469
|
+
if (remotes.length === 0) {
|
|
2470
|
+
return { message: "No remote configured. Add one with: git remote add origin <url>" };
|
|
2471
|
+
}
|
|
2472
|
+
if (dryRun) {
|
|
2473
|
+
return {
|
|
2474
|
+
message: `Would push to ${remotes.join(", ")}${force ? " (force)" : ""}
|
|
2475
|
+
${color.dim("(dry-run)")}`
|
|
2476
|
+
};
|
|
2477
|
+
}
|
|
2478
|
+
const branchResult = await runGit(["rev-parse", "--abbrev-ref", "HEAD"], cwd);
|
|
2479
|
+
const branch = branchResult.stdout.trim() || "main";
|
|
2480
|
+
const pushArgs = ["push"];
|
|
2481
|
+
if (force) pushArgs.push("--force");
|
|
2482
|
+
pushArgs.push(...remotes, branch);
|
|
2483
|
+
const pushResult = await runGit(pushArgs, cwd);
|
|
2484
|
+
if (pushResult.code !== 0) {
|
|
2485
|
+
return { message: `Push failed: ${pushResult.stderr}` };
|
|
2486
|
+
}
|
|
2487
|
+
return {
|
|
2488
|
+
message: `${color.green("\u2713")} Pushed to ${remotes.join(", ")} (${branch})`
|
|
2489
|
+
};
|
|
2490
|
+
}
|
|
2491
|
+
};
|
|
2492
|
+
}
|
|
1488
2493
|
|
|
1489
2494
|
// src/slash-commands/compact.ts
|
|
1490
2495
|
function buildCompactCommand(opts) {
|
|
@@ -1814,10 +2819,10 @@ function buildInitCommand(opts) {
|
|
|
1814
2819
|
description: "Create .wrongstack/AGENTS.md project context for the system prompt.",
|
|
1815
2820
|
async run(args, ctx) {
|
|
1816
2821
|
const force = args.trim() === "--force";
|
|
1817
|
-
const dir =
|
|
1818
|
-
const file =
|
|
2822
|
+
const dir = path18.join(ctx.projectRoot, ".wrongstack");
|
|
2823
|
+
const file = path18.join(dir, "AGENTS.md");
|
|
1819
2824
|
try {
|
|
1820
|
-
await
|
|
2825
|
+
await fs5.access(file);
|
|
1821
2826
|
if (!force) {
|
|
1822
2827
|
const msg2 = `AGENTS.md already exists at ${file}. Use "/init --force" to overwrite.`;
|
|
1823
2828
|
opts.renderer.writeWarning(msg2);
|
|
@@ -1827,8 +2832,8 @@ function buildInitCommand(opts) {
|
|
|
1827
2832
|
}
|
|
1828
2833
|
const detected = await detectProjectFacts(ctx.projectRoot);
|
|
1829
2834
|
const body = renderAgentsTemplate(detected);
|
|
1830
|
-
await
|
|
1831
|
-
await
|
|
2835
|
+
await fs5.mkdir(dir, { recursive: true });
|
|
2836
|
+
await fs5.writeFile(file, body, "utf8");
|
|
1832
2837
|
if (detected.hints.length > 0) {
|
|
1833
2838
|
const msg2 = `Wrote ${file}
|
|
1834
2839
|
Pre-filled: ${detected.hints.join(", ")}. Edit the file with project context and instructions the system prompt should carry.`;
|
|
@@ -2057,6 +3062,13 @@ function buildExitCommand(opts) {
|
|
|
2057
3062
|
aliases: ["quit", "q"],
|
|
2058
3063
|
description: "Exit the REPL.",
|
|
2059
3064
|
async run() {
|
|
3065
|
+
if (opts.onBeforeExit) {
|
|
3066
|
+
const result = await opts.onBeforeExit();
|
|
3067
|
+
if (result?.abort) {
|
|
3068
|
+
opts.onExit?.();
|
|
3069
|
+
return { message: result.message ?? "", exit: true };
|
|
3070
|
+
}
|
|
3071
|
+
}
|
|
2060
3072
|
opts.onExit?.();
|
|
2061
3073
|
return { exit: true };
|
|
2062
3074
|
}
|
|
@@ -2065,7 +3077,7 @@ function buildExitCommand(opts) {
|
|
|
2065
3077
|
function buildSkillCommand(opts) {
|
|
2066
3078
|
return {
|
|
2067
3079
|
name: "skill",
|
|
2068
|
-
description: "Show skill details or list available skills.",
|
|
3080
|
+
description: "Show skill details or list available skills. Use /skill-gen to create new skills.",
|
|
2069
3081
|
async run(args) {
|
|
2070
3082
|
if (!opts.skillLoader) return { message: "No skill loader configured." };
|
|
2071
3083
|
if (!args.trim()) {
|
|
@@ -2130,76 +3142,462 @@ function buildDirectorCommand(opts) {
|
|
|
2130
3142
|
message: "Cannot promote to director mode: subagents have already been spawned. Promote before using /spawn, or restart with --director."
|
|
2131
3143
|
};
|
|
2132
3144
|
}
|
|
2133
|
-
return { message: result };
|
|
3145
|
+
return { message: result };
|
|
3146
|
+
}
|
|
3147
|
+
};
|
|
3148
|
+
}
|
|
3149
|
+
function buildTodosCommand(opts) {
|
|
3150
|
+
return {
|
|
3151
|
+
name: "todos",
|
|
3152
|
+
description: "Inspect or edit the live todo list: /todos [show|clear|add <text>|done <id|index>]",
|
|
3153
|
+
async run(args) {
|
|
3154
|
+
const ctx = opts.context;
|
|
3155
|
+
if (!ctx) return { message: "No active context." };
|
|
3156
|
+
const [verb, ...rest] = args.trim().split(/\s+/);
|
|
3157
|
+
const restJoined = rest.join(" ").trim();
|
|
3158
|
+
switch (verb) {
|
|
3159
|
+
case "":
|
|
3160
|
+
case "show":
|
|
3161
|
+
case "list": {
|
|
3162
|
+
return { message: formatTodosList(ctx.todos) };
|
|
3163
|
+
}
|
|
3164
|
+
case "clear": {
|
|
3165
|
+
const n = ctx.todos.length;
|
|
3166
|
+
ctx.todos.length = 0;
|
|
3167
|
+
return {
|
|
3168
|
+
message: n === 0 ? "Todos were already empty." : `Cleared ${n} todo${n === 1 ? "" : "s"}.`
|
|
3169
|
+
};
|
|
3170
|
+
}
|
|
3171
|
+
case "add": {
|
|
3172
|
+
if (!restJoined) return { message: "Usage: /todos add <text>" };
|
|
3173
|
+
ctx.todos.push({
|
|
3174
|
+
id: `todo_${Date.now()}_${randomUUID().slice(0, 7)}`,
|
|
3175
|
+
content: restJoined,
|
|
3176
|
+
status: "pending"
|
|
3177
|
+
});
|
|
3178
|
+
return { message: `Added: ${restJoined}` };
|
|
3179
|
+
}
|
|
3180
|
+
case "done":
|
|
3181
|
+
case "complete": {
|
|
3182
|
+
if (!restJoined) return { message: "Usage: /todos done <id|index>" };
|
|
3183
|
+
const asIndex = Number.parseInt(restJoined, 10);
|
|
3184
|
+
let target = !Number.isNaN(asIndex) ? ctx.todos[asIndex - 1] : ctx.todos.find((t) => t.id === restJoined);
|
|
3185
|
+
if (!target)
|
|
3186
|
+
target = ctx.todos.find(
|
|
3187
|
+
(t) => t.content.toLowerCase().includes(restJoined.toLowerCase())
|
|
3188
|
+
);
|
|
3189
|
+
if (!target) return { message: `No todo matched "${restJoined}".` };
|
|
3190
|
+
target.status = "completed";
|
|
3191
|
+
return { message: `Marked done: ${target.content}` };
|
|
3192
|
+
}
|
|
3193
|
+
default:
|
|
3194
|
+
return {
|
|
3195
|
+
message: `Unknown subcommand "${verb}". Try: show | clear | add <text> | done <id|index>`
|
|
3196
|
+
};
|
|
3197
|
+
}
|
|
3198
|
+
}
|
|
3199
|
+
};
|
|
3200
|
+
}
|
|
3201
|
+
function buildToolsCommand(opts) {
|
|
3202
|
+
return {
|
|
3203
|
+
name: "tools",
|
|
3204
|
+
description: "List registered tools.",
|
|
3205
|
+
async run() {
|
|
3206
|
+
const all = opts.toolRegistry.listWithOwner();
|
|
3207
|
+
const lines = all.map(
|
|
3208
|
+
({ tool, owner }) => ` ${tool.name.padEnd(28)} ${color.dim(`[${owner}]`)} ${tool.mutating ? color.yellow("mut") : color.cyan("ro")} ${color.dim(tool.permission)}`
|
|
3209
|
+
);
|
|
3210
|
+
const msg = `${color.bold("Tools")} (${all.length}):
|
|
3211
|
+
${lines.join("\n")}
|
|
3212
|
+
`;
|
|
3213
|
+
opts.renderer.write(msg);
|
|
3214
|
+
return { message: msg };
|
|
3215
|
+
}
|
|
3216
|
+
};
|
|
3217
|
+
}
|
|
3218
|
+
function buildYoloCommand(opts) {
|
|
3219
|
+
return {
|
|
3220
|
+
name: "yolo",
|
|
3221
|
+
description: "Toggle or query YOLO (auto-approve) mode.",
|
|
3222
|
+
help: [
|
|
3223
|
+
"Usage:",
|
|
3224
|
+
" /yolo Show current YOLO status",
|
|
3225
|
+
" /yolo on Enable YOLO mode (auto-approve every tool call)",
|
|
3226
|
+
" /yolo off Disable YOLO mode (restore permission prompts)",
|
|
3227
|
+
" /yolo toggle Toggle YOLO mode",
|
|
3228
|
+
"",
|
|
3229
|
+
"YOLO mode skips all permission prompts and auto-approves every tool call.",
|
|
3230
|
+
"Use with caution \u2014 the agent can execute any tool without asking."
|
|
3231
|
+
].join("\n"),
|
|
3232
|
+
async run(args) {
|
|
3233
|
+
const arg = args.trim().toLowerCase();
|
|
3234
|
+
if (!opts.onYolo) {
|
|
3235
|
+
const msg2 = "YOLO toggle is not available in this session.";
|
|
3236
|
+
opts.renderer.writeWarning(msg2);
|
|
3237
|
+
return { message: msg2 };
|
|
3238
|
+
}
|
|
3239
|
+
if (!arg) {
|
|
3240
|
+
const current = opts.onYolo();
|
|
3241
|
+
const status = current ? `${color.yellow("ON")} ${color.dim("(auto-approving all tool calls)")}` : `${color.green("OFF")} ${color.dim("(permission prompts active)")}`;
|
|
3242
|
+
const msg2 = `YOLO mode: ${status}`;
|
|
3243
|
+
opts.renderer.write(msg2);
|
|
3244
|
+
return { message: msg2 };
|
|
3245
|
+
}
|
|
3246
|
+
let newState;
|
|
3247
|
+
if (arg === "on" || arg === "enable" || arg === "true" || arg === "1") {
|
|
3248
|
+
newState = true;
|
|
3249
|
+
} else if (arg === "off" || arg === "disable" || arg === "false" || arg === "0") {
|
|
3250
|
+
newState = false;
|
|
3251
|
+
} else if (arg === "toggle") {
|
|
3252
|
+
newState = !opts.onYolo();
|
|
3253
|
+
} else {
|
|
3254
|
+
const msg2 = `Unknown argument: ${arg}. Use /yolo on, /yolo off, or /yolo toggle.`;
|
|
3255
|
+
opts.renderer.writeWarning(msg2);
|
|
3256
|
+
return { message: msg2 };
|
|
3257
|
+
}
|
|
3258
|
+
opts.onYolo(newState);
|
|
3259
|
+
const label = newState ? `${color.yellow("ENABLED")} \u2014 all tool calls will be auto-approved` : `${color.green("DISABLED")} \u2014 permission prompts are active`;
|
|
3260
|
+
const msg = `YOLO mode: ${label}`;
|
|
3261
|
+
opts.renderer.write(msg);
|
|
3262
|
+
return { message: msg };
|
|
3263
|
+
}
|
|
3264
|
+
};
|
|
3265
|
+
}
|
|
3266
|
+
function buildAutonomyCommand(opts) {
|
|
3267
|
+
return {
|
|
3268
|
+
name: "autonomy",
|
|
3269
|
+
description: "Toggle or query autonomy mode (self-driving agent).",
|
|
3270
|
+
help: [
|
|
3271
|
+
"Usage:",
|
|
3272
|
+
" /autonomy Show current autonomy status",
|
|
3273
|
+
" /autonomy off Disabled \u2014 agent stops after each turn (default)",
|
|
3274
|
+
" /autonomy suggest Show next-step suggestions after each turn",
|
|
3275
|
+
" /autonomy on Auto-continue \u2014 agent picks next step and proceeds",
|
|
3276
|
+
" /autonomy toggle Cycle: off \u2192 suggest \u2192 auto \u2192 off",
|
|
3277
|
+
"",
|
|
3278
|
+
"Modes:",
|
|
3279
|
+
" off \u2014 Normal interactive mode. Agent stops and waits.",
|
|
3280
|
+
" suggest \u2014 After each turn, agent suggests next steps. You pick.",
|
|
3281
|
+
" auto \u2014 After each turn, agent picks the best next step and continues.",
|
|
3282
|
+
" Runs indefinitely until you press Esc or Ctrl+C.",
|
|
3283
|
+
"",
|
|
3284
|
+
"In auto mode the agent works autonomously. Press Esc to redirect,",
|
|
3285
|
+
"Ctrl+C to stop. The agent suggests context-aware next steps based on",
|
|
3286
|
+
"the conversation history."
|
|
3287
|
+
].join("\n"),
|
|
3288
|
+
async run(args) {
|
|
3289
|
+
const arg = args.trim().toLowerCase();
|
|
3290
|
+
if (!opts.onAutonomy) {
|
|
3291
|
+
const msg2 = "Autonomy mode is not available in this session.";
|
|
3292
|
+
opts.renderer.writeWarning(msg2);
|
|
3293
|
+
return { message: msg2 };
|
|
3294
|
+
}
|
|
3295
|
+
if (!arg) {
|
|
3296
|
+
const current = opts.onAutonomy();
|
|
3297
|
+
const labels2 = {
|
|
3298
|
+
off: `${color.green("OFF")} ${color.dim("(agent stops after each turn)")}`,
|
|
3299
|
+
suggest: `${color.cyan("SUGGEST")} ${color.dim("(shows next-step suggestions)")}`,
|
|
3300
|
+
auto: `${color.yellow("AUTO")} ${color.dim("(self-driving \u2014 Esc to redirect, Ctrl+C to stop)")}`
|
|
3301
|
+
};
|
|
3302
|
+
const msg2 = `Autonomy mode: ${labels2[current]}`;
|
|
3303
|
+
opts.renderer.write(msg2);
|
|
3304
|
+
return { message: msg2 };
|
|
3305
|
+
}
|
|
3306
|
+
let newMode;
|
|
3307
|
+
if (arg === "on" || arg === "enable" || arg === "true" || arg === "auto") {
|
|
3308
|
+
newMode = "auto";
|
|
3309
|
+
} else if (arg === "off" || arg === "disable" || arg === "false") {
|
|
3310
|
+
newMode = "off";
|
|
3311
|
+
} else if (arg === "suggest" || arg === "suggestions") {
|
|
3312
|
+
newMode = "suggest";
|
|
3313
|
+
} else if (arg === "toggle" || arg === "cycle") {
|
|
3314
|
+
const current = opts.onAutonomy() ?? "off";
|
|
3315
|
+
const cycle = ["off", "suggest", "auto"];
|
|
3316
|
+
newMode = cycle[(cycle.indexOf(current) + 1) % cycle.length] ?? "off";
|
|
3317
|
+
} else {
|
|
3318
|
+
const msg2 = `Unknown argument: ${arg}. Use /autonomy on, /autonomy off, /autonomy suggest, or /autonomy toggle.`;
|
|
3319
|
+
opts.renderer.writeWarning(msg2);
|
|
3320
|
+
return { message: msg2 };
|
|
3321
|
+
}
|
|
3322
|
+
opts.onAutonomy(newMode);
|
|
3323
|
+
const labels = {
|
|
3324
|
+
off: `${color.green("OFF")} \u2014 agent stops after each turn`,
|
|
3325
|
+
suggest: `${color.cyan("SUGGEST")} \u2014 shows next-step suggestions after each turn`,
|
|
3326
|
+
auto: `${color.yellow("AUTO")} \u2014 self-driving, agent continues automatically`
|
|
3327
|
+
};
|
|
3328
|
+
const msg = `Autonomy mode: ${labels[newMode]}`;
|
|
3329
|
+
opts.renderer.write(msg);
|
|
3330
|
+
return { message: msg };
|
|
3331
|
+
}
|
|
3332
|
+
};
|
|
3333
|
+
}
|
|
3334
|
+
|
|
3335
|
+
// src/slash-commands/mode.ts
|
|
3336
|
+
function buildModeCommand(opts) {
|
|
3337
|
+
return {
|
|
3338
|
+
name: "mode",
|
|
3339
|
+
description: "Switch or view the current mode",
|
|
3340
|
+
help: [
|
|
3341
|
+
"Usage:",
|
|
3342
|
+
" /mode Show current mode and available modes",
|
|
3343
|
+
" /mode <id> Switch to a different mode",
|
|
3344
|
+
"",
|
|
3345
|
+
"Available modes:",
|
|
3346
|
+
" default General-purpose coding assistant",
|
|
3347
|
+
" brief Fast, no-nonsense \u2014 get to the point",
|
|
3348
|
+
" teach Mentor mode \u2014 explains why, not just what",
|
|
3349
|
+
" code-reviewer, code-auditor, architect, debugger, tester, devops, refactorer",
|
|
3350
|
+
"",
|
|
3351
|
+
"Example:",
|
|
3352
|
+
" /mode brief Switch to brief mode",
|
|
3353
|
+
" /mode teach Switch to teach mode"
|
|
3354
|
+
].join("\n"),
|
|
3355
|
+
async run(args) {
|
|
3356
|
+
const modeStore = opts.modeStore;
|
|
3357
|
+
if (!modeStore) {
|
|
3358
|
+
return { message: "Mode store not available in this context." };
|
|
3359
|
+
}
|
|
3360
|
+
const modes = await modeStore.listModes();
|
|
3361
|
+
const active = await modeStore.getActiveMode();
|
|
3362
|
+
if (!args.trim()) {
|
|
3363
|
+
const lines = [`Current mode: ${active?.name ?? "none"}`, "", "Available modes:"];
|
|
3364
|
+
for (const m of modes) {
|
|
3365
|
+
const mark = m.id === active?.id ? " [active]" : "";
|
|
3366
|
+
lines.push(` ${m.id} \u2014 ${m.description}${mark}`);
|
|
3367
|
+
}
|
|
3368
|
+
return { message: lines.join("\n") };
|
|
3369
|
+
}
|
|
3370
|
+
const target = args.trim().toLowerCase();
|
|
3371
|
+
const targetMode = modes.find((m) => m.id === target);
|
|
3372
|
+
if (!targetMode) {
|
|
3373
|
+
const available = modes.map((m) => m.id).join(", ");
|
|
3374
|
+
return { message: `Unknown mode "${target}". Available: ${available}` };
|
|
3375
|
+
}
|
|
3376
|
+
await modeStore.setActiveMode(targetMode.id);
|
|
3377
|
+
return {
|
|
3378
|
+
message: `Switched to "${targetMode.name}" mode.
|
|
3379
|
+
${targetMode.description}`
|
|
3380
|
+
};
|
|
3381
|
+
}
|
|
3382
|
+
};
|
|
3383
|
+
}
|
|
3384
|
+
|
|
3385
|
+
// src/slash-commands/index.ts
|
|
3386
|
+
init_sdd();
|
|
3387
|
+
|
|
3388
|
+
// src/slash-commands/skill-generator.ts
|
|
3389
|
+
function buildSkillGeneratorCommand(opts) {
|
|
3390
|
+
return {
|
|
3391
|
+
name: "skill-gen",
|
|
3392
|
+
description: "Create a new AI skill interactively. The AI will guide you.",
|
|
3393
|
+
help: [
|
|
3394
|
+
"\u2554\u2550\u2550\u2550 Skill Generator \u2550\u2550\u2550\u2557",
|
|
3395
|
+
"",
|
|
3396
|
+
"Create new AI skills with AI guidance.",
|
|
3397
|
+
"",
|
|
3398
|
+
"Usage:",
|
|
3399
|
+
" /skill-gen Start skill creation",
|
|
3400
|
+
" /skill-gen list List existing skills",
|
|
3401
|
+
" /skill-gen edit <name> View an existing skill",
|
|
3402
|
+
"",
|
|
3403
|
+
"The AI will ask you questions and create the skill file.",
|
|
3404
|
+
"Skills are saved to .wrongstack/skills/<name>/SKILL.md"
|
|
3405
|
+
].join("\n"),
|
|
3406
|
+
async run(args) {
|
|
3407
|
+
const trimmed = args.trim();
|
|
3408
|
+
if (trimmed === "list" || trimmed === "ls") {
|
|
3409
|
+
if (!opts.skillLoader) return { message: "No skill loader configured." };
|
|
3410
|
+
const entries = await opts.skillLoader.listEntries();
|
|
3411
|
+
if (entries.length === 0) return { message: "No skills found." };
|
|
3412
|
+
const lines = entries.map((e) => {
|
|
3413
|
+
const src = e.source === "project" ? "\u{1F4C1}" : e.source === "user" ? "\u{1F464}" : "\u{1F4E6}";
|
|
3414
|
+
return ` ${src} ${e.name}
|
|
3415
|
+
${e.trigger}`;
|
|
3416
|
+
});
|
|
3417
|
+
return { message: `Available Skills:
|
|
3418
|
+
${lines.join("\n\n")}
|
|
3419
|
+
` };
|
|
3420
|
+
}
|
|
3421
|
+
if (trimmed.startsWith("edit ")) {
|
|
3422
|
+
const skillName = trimmed.slice(5).trim();
|
|
3423
|
+
if (!opts.skillLoader) return { message: "No skill loader configured." };
|
|
3424
|
+
const skill = await opts.skillLoader.find(skillName);
|
|
3425
|
+
if (!skill) return { message: `Skill "${skillName}" not found.` };
|
|
3426
|
+
const body = await opts.skillLoader.readBody(skillName);
|
|
3427
|
+
return {
|
|
3428
|
+
message: [
|
|
3429
|
+
`Skill: ${skillName}`,
|
|
3430
|
+
`Path: ${skill.path}`,
|
|
3431
|
+
"",
|
|
3432
|
+
body
|
|
3433
|
+
].join("\n")
|
|
3434
|
+
};
|
|
3435
|
+
}
|
|
3436
|
+
return {
|
|
3437
|
+
message: "\u2554\u2550\u2550\u2550 Skill Generator \u2550\u2550\u2550\u2557\n\nThe AI will guide you through creating a new skill.\nAnswer its questions naturally.",
|
|
3438
|
+
runText: "I want to create a new AI skill. Read the skill-creator skill and guide me through the process. Ask me questions one at a time \u2014 name, description, what to cover \u2014 then create the SKILL.md file."
|
|
3439
|
+
};
|
|
3440
|
+
}
|
|
3441
|
+
};
|
|
3442
|
+
}
|
|
3443
|
+
function makeInstaller(opts, projectRoot, global) {
|
|
3444
|
+
const globalRoot = path18.join(os4.homedir(), ".wrongstack");
|
|
3445
|
+
return new SkillInstaller({
|
|
3446
|
+
manifestPath: path18.join(globalRoot, "installed-skills.json"),
|
|
3447
|
+
projectSkillsDir: path18.join(projectRoot, ".wrongstack", "skills"),
|
|
3448
|
+
globalSkillsDir: path18.join(globalRoot, "skills"),
|
|
3449
|
+
projectHash: projectHash(projectRoot),
|
|
3450
|
+
skillLoader: opts.skillLoader
|
|
3451
|
+
});
|
|
3452
|
+
}
|
|
3453
|
+
function buildSkillInstallCommand(opts) {
|
|
3454
|
+
return {
|
|
3455
|
+
name: "skill-install",
|
|
3456
|
+
description: "Install skills from a GitHub repository.",
|
|
3457
|
+
argsHint: "<user/repo[@ref]> [--global]",
|
|
3458
|
+
help: [
|
|
3459
|
+
"\u2554\u2550\u2550\u2550 Skill Install \u2550\u2550\u2550\u2557",
|
|
3460
|
+
"",
|
|
3461
|
+
"Install skills from a GitHub repository.",
|
|
3462
|
+
"",
|
|
3463
|
+
"Usage:",
|
|
3464
|
+
" /skill-install <user/repo> Install from default branch (main)",
|
|
3465
|
+
" /skill-install <user/repo@ref> Install specific tag/branch/commit",
|
|
3466
|
+
" /skill-install <user/repo> --global Install to user-global skills",
|
|
3467
|
+
"",
|
|
3468
|
+
"Supports both single-skill repos (SKILL.md at root)",
|
|
3469
|
+
"and multi-skill repos (skills/ subdirectory).",
|
|
3470
|
+
"",
|
|
3471
|
+
"Examples:",
|
|
3472
|
+
" /skill-install wrongstack/awesome-skills",
|
|
3473
|
+
" /skill-install wrongstack/skills@v1.0",
|
|
3474
|
+
" /skill-install user/my-skills --global"
|
|
3475
|
+
].join("\n"),
|
|
3476
|
+
async run(args, ctx) {
|
|
3477
|
+
const parts = args.trim().split(/\s+/);
|
|
3478
|
+
const ref = parts.find((p) => !p.startsWith("--"));
|
|
3479
|
+
const isGlobal = parts.includes("--global");
|
|
3480
|
+
if (!ref) {
|
|
3481
|
+
return { message: "Usage: /skill-install <user/repo[@ref]> [--global]" };
|
|
3482
|
+
}
|
|
3483
|
+
const installer = makeInstaller(opts, ctx.projectRoot);
|
|
3484
|
+
try {
|
|
3485
|
+
const results = await installer.install(ref, { global: isGlobal });
|
|
3486
|
+
if (results.length === 0) {
|
|
3487
|
+
return { message: "No skills found in the repository." };
|
|
3488
|
+
}
|
|
3489
|
+
const scope = isGlobal ? "user-global" : "project";
|
|
3490
|
+
const lines = [`Installed ${results.length} skill(s) [${scope}]:`];
|
|
3491
|
+
for (const r of results) {
|
|
3492
|
+
lines.push(` \u2713 ${r.name} (${r.source}@${r.ref})`);
|
|
3493
|
+
lines.push(` \u2192 ${r.path}`);
|
|
3494
|
+
}
|
|
3495
|
+
return { message: lines.join("\n") };
|
|
3496
|
+
} catch (err) {
|
|
3497
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
3498
|
+
opts.renderer.writeError(`Install failed: ${msg}`);
|
|
3499
|
+
return { message: `\u2717 Install failed: ${msg}` };
|
|
3500
|
+
}
|
|
2134
3501
|
}
|
|
2135
3502
|
};
|
|
2136
3503
|
}
|
|
2137
|
-
function
|
|
3504
|
+
function buildSkillUpdateCommand(opts) {
|
|
2138
3505
|
return {
|
|
2139
|
-
name: "
|
|
2140
|
-
description: "
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
|
|
3506
|
+
name: "skill-update",
|
|
3507
|
+
description: "Update installed skills from their GitHub source.",
|
|
3508
|
+
argsHint: "[name|ref] [--global]",
|
|
3509
|
+
help: [
|
|
3510
|
+
"\u2554\u2550\u2550\u2550 Skill Update \u2550\u2550\u2550\u2557",
|
|
3511
|
+
"",
|
|
3512
|
+
"Update installed skills from their GitHub source.",
|
|
3513
|
+
"",
|
|
3514
|
+
"Usage:",
|
|
3515
|
+
" /skill-update Update all installed skills",
|
|
3516
|
+
" /skill-update <name> Update a specific skill",
|
|
3517
|
+
" /skill-update <user/repo@ref> Update to a different ref",
|
|
3518
|
+
" /skill-update <name> --global Update a global skill"
|
|
3519
|
+
].join("\n"),
|
|
3520
|
+
async run(args, ctx) {
|
|
3521
|
+
const parts = args.trim().split(/\s+/);
|
|
3522
|
+
const nameOrRef = parts.find((p) => !p.startsWith("--"));
|
|
3523
|
+
const isGlobal = parts.includes("--global");
|
|
3524
|
+
const installer = makeInstaller(opts, ctx.projectRoot);
|
|
3525
|
+
try {
|
|
3526
|
+
const result = await installer.update(nameOrRef, { global: isGlobal });
|
|
3527
|
+
const lines = [];
|
|
3528
|
+
if (result.updated.length > 0) {
|
|
3529
|
+
lines.push(`Updated ${result.updated.length} skill(s):`);
|
|
3530
|
+
for (const u of result.updated) {
|
|
3531
|
+
if (u.oldRef !== u.newRef) {
|
|
3532
|
+
lines.push(` \u2713 ${u.name} (${u.oldRef} \u2192 ${u.newRef})`);
|
|
3533
|
+
} else {
|
|
3534
|
+
lines.push(` \u2713 ${u.name} (refreshed)`);
|
|
3535
|
+
}
|
|
3536
|
+
}
|
|
2151
3537
|
}
|
|
2152
|
-
|
|
2153
|
-
|
|
2154
|
-
ctx.todos.length = 0;
|
|
2155
|
-
return {
|
|
2156
|
-
message: n === 0 ? "Todos were already empty." : `Cleared ${n} todo${n === 1 ? "" : "s"}.`
|
|
2157
|
-
};
|
|
3538
|
+
if (result.unchanged.length > 0) {
|
|
3539
|
+
lines.push(`Up to date: ${result.unchanged.join(", ")}`);
|
|
2158
3540
|
}
|
|
2159
|
-
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
|
|
2163
|
-
content: restJoined,
|
|
2164
|
-
status: "pending"
|
|
2165
|
-
});
|
|
2166
|
-
return { message: `Added: ${restJoined}` };
|
|
3541
|
+
if (result.errors.length > 0) {
|
|
3542
|
+
for (const e of result.errors) {
|
|
3543
|
+
lines.push(` \u2717 ${e.name}: ${e.error}`);
|
|
3544
|
+
}
|
|
2167
3545
|
}
|
|
2168
|
-
|
|
2169
|
-
|
|
2170
|
-
if (!restJoined) return { message: "Usage: /todos done <id|index>" };
|
|
2171
|
-
const asIndex = Number.parseInt(restJoined, 10);
|
|
2172
|
-
let target = !Number.isNaN(asIndex) ? ctx.todos[asIndex - 1] : ctx.todos.find((t) => t.id === restJoined);
|
|
2173
|
-
if (!target)
|
|
2174
|
-
target = ctx.todos.find(
|
|
2175
|
-
(t) => t.content.toLowerCase().includes(restJoined.toLowerCase())
|
|
2176
|
-
);
|
|
2177
|
-
if (!target) return { message: `No todo matched "${restJoined}".` };
|
|
2178
|
-
target.status = "completed";
|
|
2179
|
-
return { message: `Marked done: ${target.content}` };
|
|
3546
|
+
if (lines.length === 0) {
|
|
3547
|
+
return { message: "No installed skills to update." };
|
|
2180
3548
|
}
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
|
|
2184
|
-
|
|
3549
|
+
return { message: lines.join("\n") };
|
|
3550
|
+
} catch (err) {
|
|
3551
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
3552
|
+
return { message: `\u2717 Update failed: ${msg}` };
|
|
2185
3553
|
}
|
|
2186
3554
|
}
|
|
2187
3555
|
};
|
|
2188
3556
|
}
|
|
2189
|
-
function
|
|
3557
|
+
function buildSkillUninstallCommand(opts) {
|
|
2190
3558
|
return {
|
|
2191
|
-
name: "
|
|
2192
|
-
description: "
|
|
2193
|
-
|
|
2194
|
-
|
|
2195
|
-
|
|
2196
|
-
|
|
2197
|
-
|
|
2198
|
-
|
|
2199
|
-
|
|
2200
|
-
|
|
2201
|
-
|
|
2202
|
-
|
|
3559
|
+
name: "skill-uninstall",
|
|
3560
|
+
description: "Remove an installed skill.",
|
|
3561
|
+
argsHint: "<name> [--global]",
|
|
3562
|
+
help: [
|
|
3563
|
+
"\u2554\u2550\u2550\u2550 Skill Uninstall \u2550\u2550\u2550\u2557",
|
|
3564
|
+
"",
|
|
3565
|
+
"Remove an installed skill and its files.",
|
|
3566
|
+
"",
|
|
3567
|
+
"Usage:",
|
|
3568
|
+
" /skill-uninstall <name> Remove from project skills",
|
|
3569
|
+
" /skill-uninstall <name> --global Remove from user-global skills"
|
|
3570
|
+
].join("\n"),
|
|
3571
|
+
async run(args, ctx) {
|
|
3572
|
+
const parts = args.trim().split(/\s+/);
|
|
3573
|
+
const name = parts.find((p) => !p.startsWith("--"));
|
|
3574
|
+
const isGlobal = parts.includes("--global");
|
|
3575
|
+
if (!name) {
|
|
3576
|
+
const installer2 = makeInstaller(opts, ctx.projectRoot);
|
|
3577
|
+
const installed = await installer2.listInstalled();
|
|
3578
|
+
if (installed.length === 0) {
|
|
3579
|
+
return { message: "No installed skills found." };
|
|
3580
|
+
}
|
|
3581
|
+
const scope = isGlobal ? "user" : "project";
|
|
3582
|
+
const filtered = installed.filter((s) => s.scope === scope);
|
|
3583
|
+
if (filtered.length === 0) {
|
|
3584
|
+
return { message: `No installed skills found (${scope} scope).` };
|
|
3585
|
+
}
|
|
3586
|
+
const lines = [`Installed skills (${scope}):`];
|
|
3587
|
+
for (const s of filtered) {
|
|
3588
|
+
lines.push(` ${s.name} ${s.source}@${s.ref} (${s.installedAt.slice(0, 10)})`);
|
|
3589
|
+
}
|
|
3590
|
+
lines.push("", "Use /skill-uninstall <name> to remove.");
|
|
3591
|
+
return { message: lines.join("\n") };
|
|
3592
|
+
}
|
|
3593
|
+
const installer = makeInstaller(opts, ctx.projectRoot);
|
|
3594
|
+
try {
|
|
3595
|
+
await installer.uninstall(name, { global: isGlobal });
|
|
3596
|
+
return { message: `\u2713 Skill "${name}" uninstalled.` };
|
|
3597
|
+
} catch (err) {
|
|
3598
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
3599
|
+
return { message: `\u2717 Uninstall failed: ${msg}` };
|
|
3600
|
+
}
|
|
2203
3601
|
}
|
|
2204
3602
|
};
|
|
2205
3603
|
}
|
|
@@ -2214,6 +3612,10 @@ function buildBuiltinSlashCommands(opts) {
|
|
|
2214
3612
|
buildContextCommand(opts),
|
|
2215
3613
|
buildToolsCommand(opts),
|
|
2216
3614
|
buildSkillCommand(opts),
|
|
3615
|
+
buildSkillGeneratorCommand(opts),
|
|
3616
|
+
buildSkillInstallCommand(opts),
|
|
3617
|
+
buildSkillUpdateCommand(opts),
|
|
3618
|
+
buildSkillUninstallCommand(opts),
|
|
2217
3619
|
buildPluginCommand(opts),
|
|
2218
3620
|
buildDiagCommand(opts),
|
|
2219
3621
|
buildStatsCommand(opts),
|
|
@@ -2226,9 +3628,16 @@ function buildBuiltinSlashCommands(opts) {
|
|
|
2226
3628
|
buildMemoryCommand(opts),
|
|
2227
3629
|
buildTodosCommand(opts),
|
|
2228
3630
|
buildPlanCommand(opts),
|
|
3631
|
+
buildSddCommand(opts),
|
|
2229
3632
|
buildSaveCommand(opts),
|
|
2230
3633
|
buildLoadCommand(opts),
|
|
2231
|
-
|
|
3634
|
+
buildYoloCommand(opts),
|
|
3635
|
+
buildAutonomyCommand(opts),
|
|
3636
|
+
buildModeCommand(opts),
|
|
3637
|
+
buildExitCommand(opts),
|
|
3638
|
+
buildCommitCommand(),
|
|
3639
|
+
buildGitcheckCommand(),
|
|
3640
|
+
buildPushCommand()
|
|
2232
3641
|
];
|
|
2233
3642
|
}
|
|
2234
3643
|
|
|
@@ -2247,13 +3656,13 @@ var MANIFESTS = [
|
|
|
2247
3656
|
];
|
|
2248
3657
|
async function detectProjectKind(projectRoot) {
|
|
2249
3658
|
try {
|
|
2250
|
-
await
|
|
3659
|
+
await fs5.access(path18.join(projectRoot, ".wrongstack", "AGENTS.md"));
|
|
2251
3660
|
return "initialized";
|
|
2252
3661
|
} catch {
|
|
2253
3662
|
}
|
|
2254
3663
|
for (const m of MANIFESTS) {
|
|
2255
3664
|
try {
|
|
2256
|
-
await
|
|
3665
|
+
await fs5.access(path18.join(projectRoot, m));
|
|
2257
3666
|
return "project";
|
|
2258
3667
|
} catch {
|
|
2259
3668
|
}
|
|
@@ -2261,12 +3670,12 @@ async function detectProjectKind(projectRoot) {
|
|
|
2261
3670
|
return "empty";
|
|
2262
3671
|
}
|
|
2263
3672
|
async function scaffoldAgentsMd(projectRoot) {
|
|
2264
|
-
const dir =
|
|
2265
|
-
const file =
|
|
3673
|
+
const dir = path18.join(projectRoot, ".wrongstack");
|
|
3674
|
+
const file = path18.join(dir, "AGENTS.md");
|
|
2266
3675
|
const facts = await detectProjectFacts(projectRoot);
|
|
2267
3676
|
const body = renderAgentsTemplate(facts);
|
|
2268
|
-
await
|
|
2269
|
-
await
|
|
3677
|
+
await fs5.mkdir(dir, { recursive: true });
|
|
3678
|
+
await fs5.writeFile(file, body, "utf8");
|
|
2270
3679
|
return file;
|
|
2271
3680
|
}
|
|
2272
3681
|
async function runProjectCheck(opts) {
|
|
@@ -2275,7 +3684,7 @@ async function runProjectCheck(opts) {
|
|
|
2275
3684
|
if (kind === "initialized") {
|
|
2276
3685
|
renderer.write(
|
|
2277
3686
|
`
|
|
2278
|
-
${color.green("\u2713")} Project initialized ${color.dim(`(${
|
|
3687
|
+
${color.green("\u2713")} Project initialized ${color.dim(`(${path18.join(projectRoot, ".wrongstack", "AGENTS.md")})`)}
|
|
2279
3688
|
`
|
|
2280
3689
|
);
|
|
2281
3690
|
return true;
|
|
@@ -2302,11 +3711,43 @@ async function runProjectCheck(opts) {
|
|
|
2302
3711
|
}
|
|
2303
3712
|
return true;
|
|
2304
3713
|
}
|
|
2305
|
-
|
|
2306
|
-
|
|
3714
|
+
const gitDir = path18.join(projectRoot, ".git");
|
|
3715
|
+
let hasGit = false;
|
|
3716
|
+
try {
|
|
3717
|
+
await fs5.access(gitDir);
|
|
3718
|
+
hasGit = true;
|
|
3719
|
+
} catch {
|
|
3720
|
+
}
|
|
3721
|
+
if (!hasGit) {
|
|
3722
|
+
renderer.write(
|
|
3723
|
+
`
|
|
2307
3724
|
${color.dim("\u25CB")} ${color.dim(`No project manifest in ${projectRoot} \u2014 running in a scratch directory.`)}
|
|
2308
3725
|
`
|
|
2309
|
-
|
|
3726
|
+
);
|
|
3727
|
+
const answer2 = (await reader.readLine(
|
|
3728
|
+
` ${color.amber("?")} No git repo found. ${color.bold("Initialize git?")} ${color.dim("[y/N]")} `
|
|
3729
|
+
)).trim().toLowerCase();
|
|
3730
|
+
if (answer2 === "y" || answer2 === "yes") {
|
|
3731
|
+
try {
|
|
3732
|
+
const { spawn: spawn2 } = await import('child_process');
|
|
3733
|
+
await new Promise((resolve3, reject) => {
|
|
3734
|
+
const child = spawn2("git", ["init"], { cwd: projectRoot });
|
|
3735
|
+
child.on("close", (code) => code === 0 ? resolve3() : reject(new Error(`git init failed with ${code}`)));
|
|
3736
|
+
});
|
|
3737
|
+
renderer.write(` ${color.green("\u2713")} Git repository initialized
|
|
3738
|
+
`);
|
|
3739
|
+
} catch (err) {
|
|
3740
|
+
renderer.writeError(`git init failed: ${err instanceof Error ? err.message : String(err)}
|
|
3741
|
+
`);
|
|
3742
|
+
}
|
|
3743
|
+
}
|
|
3744
|
+
} else {
|
|
3745
|
+
renderer.write(
|
|
3746
|
+
`
|
|
3747
|
+
${color.dim("\u25CB")} ${color.dim(`No project manifest in ${projectRoot} \u2014 running in a scratch directory.`)}
|
|
3748
|
+
`
|
|
3749
|
+
);
|
|
3750
|
+
}
|
|
2310
3751
|
const answer = (await reader.readLine(` ${color.amber("?")} Continue anyway? ${color.dim("[Y/n]")} `)).trim().toLowerCase();
|
|
2311
3752
|
if (answer === "n" || answer === "no") {
|
|
2312
3753
|
renderer.write(color.dim(" Cancelled.\n"));
|
|
@@ -2331,9 +3772,9 @@ async function runLaunchPrompts(opts) {
|
|
|
2331
3772
|
yolo = yoloPinned;
|
|
2332
3773
|
} else {
|
|
2333
3774
|
const answer = (await reader.readLine(
|
|
2334
|
-
` ${color.amber("?")} YOLO mode ${color.dim("(auto-approve every tool call)")} ${color.dim("[
|
|
3775
|
+
` ${color.amber("?")} YOLO mode ${color.dim("(auto-approve every tool call)")} ${color.dim("[Y/n]")} `
|
|
2335
3776
|
)).trim().toLowerCase();
|
|
2336
|
-
yolo = answer
|
|
3777
|
+
yolo = answer !== "n" && answer !== "no";
|
|
2337
3778
|
}
|
|
2338
3779
|
renderer.write(
|
|
2339
3780
|
`
|
|
@@ -2571,14 +4012,14 @@ function summarize(value, name) {
|
|
|
2571
4012
|
if (typeof v === "object" && v !== null) {
|
|
2572
4013
|
const o = v;
|
|
2573
4014
|
if (name === "edit") {
|
|
2574
|
-
const
|
|
4015
|
+
const path19 = typeof o["path"] === "string" ? o["path"] : "";
|
|
2575
4016
|
const reps = typeof o["replacements"] === "number" ? o["replacements"] : 0;
|
|
2576
|
-
return `${
|
|
4017
|
+
return `${path19} ${reps} replacement${reps === 1 ? "" : "s"}`.trim();
|
|
2577
4018
|
}
|
|
2578
4019
|
if (name === "write") {
|
|
2579
|
-
const
|
|
4020
|
+
const path19 = typeof o["path"] === "string" ? o["path"] : "";
|
|
2580
4021
|
const bytes = typeof o["bytes"] === "number" ? o["bytes"] : void 0;
|
|
2581
|
-
return bytes !== void 0 ? `${
|
|
4022
|
+
return bytes !== void 0 ? `${path19} ${bytes}B` : path19;
|
|
2582
4023
|
}
|
|
2583
4024
|
if (typeof o["count"] === "number") {
|
|
2584
4025
|
return `${o["count"]} match${o["count"] === 1 ? "" : "es"}`;
|
|
@@ -3174,7 +4615,7 @@ async function readKeyInput(deps, intent) {
|
|
|
3174
4615
|
async function loadProviders(deps) {
|
|
3175
4616
|
let raw;
|
|
3176
4617
|
try {
|
|
3177
|
-
raw = await
|
|
4618
|
+
raw = await fs5.readFile(deps.globalConfigPath, "utf8");
|
|
3178
4619
|
} catch {
|
|
3179
4620
|
return {};
|
|
3180
4621
|
}
|
|
@@ -3190,7 +4631,7 @@ async function loadProviders(deps) {
|
|
|
3190
4631
|
async function mutateProviders(deps, mutator) {
|
|
3191
4632
|
let raw;
|
|
3192
4633
|
try {
|
|
3193
|
-
raw = await
|
|
4634
|
+
raw = await fs5.readFile(deps.globalConfigPath, "utf8");
|
|
3194
4635
|
} catch {
|
|
3195
4636
|
raw = "{}";
|
|
3196
4637
|
}
|
|
@@ -3262,7 +4703,7 @@ var diagCmd = async (_args, deps) => {
|
|
|
3262
4703
|
` modelsCache: ${deps.paths.modelsCache}`,
|
|
3263
4704
|
` cacheAge: ${isFinite(age) ? `${Math.round(age / 60)}m` : "never"}`,
|
|
3264
4705
|
` node: ${process.version}`,
|
|
3265
|
-
` os: ${
|
|
4706
|
+
` os: ${os4.platform()} ${os4.release()}`,
|
|
3266
4707
|
` provider: ${cfg.provider ?? "<unset>"}`,
|
|
3267
4708
|
` model: ${cfg.model ?? "<unset>"}`,
|
|
3268
4709
|
` tools: ${deps.toolRegistry?.list().length ?? 0}`,
|
|
@@ -3330,7 +4771,7 @@ var doctorCmd = async (_args, deps) => {
|
|
|
3330
4771
|
});
|
|
3331
4772
|
}
|
|
3332
4773
|
try {
|
|
3333
|
-
await
|
|
4774
|
+
await fs5.access(deps.paths.secretsKey);
|
|
3334
4775
|
checks.push({ name: "secret vault", status: "ok", detail: deps.paths.secretsKey });
|
|
3335
4776
|
} catch {
|
|
3336
4777
|
checks.push({
|
|
@@ -3340,10 +4781,10 @@ var doctorCmd = async (_args, deps) => {
|
|
|
3340
4781
|
});
|
|
3341
4782
|
}
|
|
3342
4783
|
try {
|
|
3343
|
-
await
|
|
3344
|
-
const probe =
|
|
3345
|
-
await
|
|
3346
|
-
await
|
|
4784
|
+
await fs5.mkdir(deps.paths.projectSessions, { recursive: true });
|
|
4785
|
+
const probe = path18.join(deps.paths.projectSessions, `.probe-${Date.now()}`);
|
|
4786
|
+
await fs5.writeFile(probe, "");
|
|
4787
|
+
await fs5.unlink(probe);
|
|
3347
4788
|
checks.push({ name: "sessions writable", status: "ok", detail: deps.paths.projectSessions });
|
|
3348
4789
|
} catch (err) {
|
|
3349
4790
|
checks.push({
|
|
@@ -3444,8 +4885,8 @@ var exportCmd = async (args, deps) => {
|
|
|
3444
4885
|
return 1;
|
|
3445
4886
|
}
|
|
3446
4887
|
if (output) {
|
|
3447
|
-
await
|
|
3448
|
-
await
|
|
4888
|
+
await fs5.mkdir(path18.dirname(path18.resolve(deps.cwd, output)), { recursive: true });
|
|
4889
|
+
await fs5.writeFile(path18.resolve(deps.cwd, output), rendered, "utf8");
|
|
3449
4890
|
deps.renderer.write(`Wrote ${rendered.length} bytes to ${output}
|
|
3450
4891
|
`);
|
|
3451
4892
|
} else {
|
|
@@ -3502,17 +4943,17 @@ var initCmd = async (_args, deps) => {
|
|
|
3502
4943
|
} else {
|
|
3503
4944
|
deps.renderer.writeInfo(`Found API key in env (${provider.envVars.join(" / ")}).`);
|
|
3504
4945
|
}
|
|
3505
|
-
await
|
|
4946
|
+
await fs5.mkdir(deps.paths.globalRoot, { recursive: true });
|
|
3506
4947
|
const config = { version: 1, provider: providerId, model: modelId };
|
|
3507
4948
|
if (apiKey) config.apiKey = apiKey;
|
|
3508
|
-
const keyFile =
|
|
4949
|
+
const keyFile = path18.join(path18.dirname(deps.paths.globalConfig), ".key");
|
|
3509
4950
|
const vault = new DefaultSecretVault$1({ keyFile });
|
|
3510
4951
|
const encrypted = encryptConfigSecrets(config, vault);
|
|
3511
4952
|
await atomicWrite(deps.paths.globalConfig, JSON.stringify(encrypted, null, 2));
|
|
3512
|
-
await
|
|
3513
|
-
const agentsFile =
|
|
4953
|
+
await fs5.mkdir(path18.join(deps.projectRoot, ".wrongstack"), { recursive: true });
|
|
4954
|
+
const agentsFile = path18.join(deps.projectRoot, ".wrongstack", "AGENTS.md");
|
|
3514
4955
|
try {
|
|
3515
|
-
await
|
|
4956
|
+
await fs5.access(agentsFile);
|
|
3516
4957
|
} catch {
|
|
3517
4958
|
const detected2 = await detectProjectFacts(deps.projectRoot);
|
|
3518
4959
|
await atomicWrite(agentsFile, renderAgentsTemplate(detected2));
|
|
@@ -3588,7 +5029,7 @@ async function addMcpServer(args, deps) {
|
|
|
3588
5029
|
serverCfg.enabled = enable;
|
|
3589
5030
|
let existing = {};
|
|
3590
5031
|
try {
|
|
3591
|
-
existing = JSON.parse(await
|
|
5032
|
+
existing = JSON.parse(await fs5.readFile(deps.paths.globalConfig, "utf8"));
|
|
3592
5033
|
} catch {
|
|
3593
5034
|
}
|
|
3594
5035
|
const mcpServers = existing.mcpServers ?? {};
|
|
@@ -3608,7 +5049,7 @@ async function addMcpServer(args, deps) {
|
|
|
3608
5049
|
async function removeMcpServer(name, deps) {
|
|
3609
5050
|
let existing = {};
|
|
3610
5051
|
try {
|
|
3611
|
-
existing = JSON.parse(await
|
|
5052
|
+
existing = JSON.parse(await fs5.readFile(deps.paths.globalConfig, "utf8"));
|
|
3612
5053
|
} catch {
|
|
3613
5054
|
deps.renderer.writeError("No config file found.\n");
|
|
3614
5055
|
return 1;
|
|
@@ -3729,7 +5170,7 @@ function renderConfiguredPlugins(config) {
|
|
|
3729
5170
|
}
|
|
3730
5171
|
async function readConfig(file) {
|
|
3731
5172
|
try {
|
|
3732
|
-
return JSON.parse(await
|
|
5173
|
+
return JSON.parse(await fs5.readFile(file, "utf8"));
|
|
3733
5174
|
} catch {
|
|
3734
5175
|
return {};
|
|
3735
5176
|
}
|
|
@@ -3820,9 +5261,9 @@ var usageCmd = async (_args, deps) => {
|
|
|
3820
5261
|
return 0;
|
|
3821
5262
|
};
|
|
3822
5263
|
var projectsCmd = async (_args, deps) => {
|
|
3823
|
-
const projectsRoot =
|
|
5264
|
+
const projectsRoot = path18.join(deps.paths.globalRoot, "projects");
|
|
3824
5265
|
try {
|
|
3825
|
-
const entries = await
|
|
5266
|
+
const entries = await fs5.readdir(projectsRoot);
|
|
3826
5267
|
if (entries.length === 0) {
|
|
3827
5268
|
deps.renderer.write("No projects tracked.\n");
|
|
3828
5269
|
return 0;
|
|
@@ -3830,7 +5271,7 @@ var projectsCmd = async (_args, deps) => {
|
|
|
3830
5271
|
for (const hash of entries) {
|
|
3831
5272
|
try {
|
|
3832
5273
|
const meta = JSON.parse(
|
|
3833
|
-
await
|
|
5274
|
+
await fs5.readFile(path18.join(projectsRoot, hash, "meta.json"), "utf8")
|
|
3834
5275
|
);
|
|
3835
5276
|
deps.renderer.write(
|
|
3836
5277
|
` ${color.dim(hash)} ${color.dim(meta.lastSeen ?? "")} ${meta.root ?? "?"}
|
|
@@ -4007,6 +5448,131 @@ var configCmd = async (args, deps) => {
|
|
|
4007
5448
|
deps.renderer.writeError(`Unknown config subcommand: ${sub}`);
|
|
4008
5449
|
return 1;
|
|
4009
5450
|
};
|
|
5451
|
+
function parseRewindFlags(args) {
|
|
5452
|
+
const flags = {};
|
|
5453
|
+
for (let i = 0; i < args.length; i++) {
|
|
5454
|
+
const a = args[i];
|
|
5455
|
+
if (a === "--all") flags.all = true;
|
|
5456
|
+
else if (a === "--last") flags.last = args[++i] ?? "1";
|
|
5457
|
+
else if (a === "--to") flags.to = args[++i] ?? "";
|
|
5458
|
+
else if (a === "--list") flags.list = true;
|
|
5459
|
+
else if (a === "--resume") flags.resume = true;
|
|
5460
|
+
}
|
|
5461
|
+
return flags;
|
|
5462
|
+
}
|
|
5463
|
+
var rewindCmd = async (args, deps) => {
|
|
5464
|
+
const flags = parseRewindFlags(args);
|
|
5465
|
+
const wpaths = resolveWstackPaths({ projectRoot: deps.projectRoot });
|
|
5466
|
+
const sessionsDir = path18.join(wpaths.globalRoot, "sessions");
|
|
5467
|
+
const rewind = new DefaultSessionRewinder(sessionsDir);
|
|
5468
|
+
let sessionId = args.find((a) => !a.startsWith("--"));
|
|
5469
|
+
if (!sessionId) {
|
|
5470
|
+
if (!deps.sessionStore) {
|
|
5471
|
+
deps.renderer.writeError("No session store available.");
|
|
5472
|
+
return 1;
|
|
5473
|
+
}
|
|
5474
|
+
const sessions = await deps.sessionStore.list(1);
|
|
5475
|
+
if (sessions.length === 0) {
|
|
5476
|
+
deps.renderer.writeError("No sessions found.");
|
|
5477
|
+
return 1;
|
|
5478
|
+
}
|
|
5479
|
+
sessionId = sessions[0].id;
|
|
5480
|
+
}
|
|
5481
|
+
if (flags.list) {
|
|
5482
|
+
deps.renderer.write(`Session: ${color.bold(sessionId)}
|
|
5483
|
+
|
|
5484
|
+
`);
|
|
5485
|
+
const checkpoints = await rewind.listCheckpoints(sessionId);
|
|
5486
|
+
if (checkpoints.length === 0) {
|
|
5487
|
+
deps.renderer.write("No checkpoints in this session.\n");
|
|
5488
|
+
return 0;
|
|
5489
|
+
}
|
|
5490
|
+
for (const cp of checkpoints) {
|
|
5491
|
+
deps.renderer.write(
|
|
5492
|
+
` [${cp.promptIndex}] ${color.dim(cp.ts)} ${cp.promptPreview}${cp.fileCount > 0 ? color.dim(` (${cp.fileCount} file${cp.fileCount === 1 ? "" : "s"})`) : ""}
|
|
5493
|
+
`
|
|
5494
|
+
);
|
|
5495
|
+
}
|
|
5496
|
+
return 0;
|
|
5497
|
+
}
|
|
5498
|
+
try {
|
|
5499
|
+
let result;
|
|
5500
|
+
if (flags.all) {
|
|
5501
|
+
deps.renderer.write("Rewinding to session start...\n");
|
|
5502
|
+
result = await rewind.rewindToStart(sessionId);
|
|
5503
|
+
} else if (flags.last) {
|
|
5504
|
+
const n = parseInt(flags.last, 10);
|
|
5505
|
+
if (isNaN(n) || n < 1) {
|
|
5506
|
+
deps.renderer.writeError("--last requires a positive number");
|
|
5507
|
+
return 1;
|
|
5508
|
+
}
|
|
5509
|
+
deps.renderer.write(`Rewinding last ${n} prompt(s)...
|
|
5510
|
+
`);
|
|
5511
|
+
result = await rewind.rewindLastN(sessionId, n);
|
|
5512
|
+
} else if (flags.to) {
|
|
5513
|
+
const idx = parseInt(flags.to, 10);
|
|
5514
|
+
if (isNaN(idx) || idx < 0) {
|
|
5515
|
+
deps.renderer.writeError("--to requires a non-negative number");
|
|
5516
|
+
return 1;
|
|
5517
|
+
}
|
|
5518
|
+
deps.renderer.write(`Rewinding to checkpoint ${idx}...
|
|
5519
|
+
`);
|
|
5520
|
+
result = await rewind.rewindToCheckpoint(sessionId, idx);
|
|
5521
|
+
} else {
|
|
5522
|
+
deps.renderer.write("Usage: ws rewind --all | --last N | --to <index> [--list] [--resume]\n");
|
|
5523
|
+
deps.renderer.write(" --all Rewind to session start\n");
|
|
5524
|
+
deps.renderer.write(" --last N Rewind last N prompts\n");
|
|
5525
|
+
deps.renderer.write(" --to N Rewind to checkpoint N\n");
|
|
5526
|
+
deps.renderer.write(" --list List checkpoints\n");
|
|
5527
|
+
deps.renderer.write(" --resume After rewind, truncate session history at checkpoint\n");
|
|
5528
|
+
return 1;
|
|
5529
|
+
}
|
|
5530
|
+
if (result.revertedFiles.length === 0) {
|
|
5531
|
+
deps.renderer.write("No files to revert.\n");
|
|
5532
|
+
if (flags.resume) {
|
|
5533
|
+
const store = new DefaultSessionStore({ dir: sessionsDir });
|
|
5534
|
+
const resumed = await store.resume(sessionId);
|
|
5535
|
+
const toIdx = result.toPromptIndex;
|
|
5536
|
+
await resumed.writer.truncateToCheckpoint(toIdx);
|
|
5537
|
+
await resumed.writer.close();
|
|
5538
|
+
deps.renderer.write(` ${color.green("\u2713")} Session truncated at checkpoint ${toIdx}
|
|
5539
|
+
`);
|
|
5540
|
+
}
|
|
5541
|
+
return 0;
|
|
5542
|
+
}
|
|
5543
|
+
deps.renderer.write(`
|
|
5544
|
+
Reverted ${result.revertedFiles.length} file(s):
|
|
5545
|
+
`);
|
|
5546
|
+
for (const f of result.revertedFiles) {
|
|
5547
|
+
deps.renderer.write(` ${color.green("\u2713")} ${f}
|
|
5548
|
+
`);
|
|
5549
|
+
}
|
|
5550
|
+
if (flags.resume) {
|
|
5551
|
+
const store = new DefaultSessionStore({ dir: sessionsDir });
|
|
5552
|
+
const resumed = await store.resume(sessionId);
|
|
5553
|
+
const toIdx = result.toPromptIndex;
|
|
5554
|
+
const removed = await resumed.writer.truncateToCheckpoint(toIdx);
|
|
5555
|
+
await resumed.writer.close();
|
|
5556
|
+
deps.renderer.write(`
|
|
5557
|
+
${color.green("\u2713")} Session truncated \u2014 ${removed} event(s) removed
|
|
5558
|
+
`);
|
|
5559
|
+
}
|
|
5560
|
+
if (result.errors.length > 0) {
|
|
5561
|
+
deps.renderer.write(`
|
|
5562
|
+
${result.errors.length} error(s):
|
|
5563
|
+
`);
|
|
5564
|
+
for (const e of result.errors) {
|
|
5565
|
+
deps.renderer.write(` ${color.red("\u2717")} ${e}
|
|
5566
|
+
`);
|
|
5567
|
+
}
|
|
5568
|
+
return 1;
|
|
5569
|
+
}
|
|
5570
|
+
return 0;
|
|
5571
|
+
} catch (err) {
|
|
5572
|
+
deps.renderer.writeError(err instanceof Error ? err.message : String(err));
|
|
5573
|
+
return 1;
|
|
5574
|
+
}
|
|
5575
|
+
};
|
|
4010
5576
|
var toolsCmd = async (_args, deps) => {
|
|
4011
5577
|
const reg = deps.toolRegistry;
|
|
4012
5578
|
if (!reg) return 0;
|
|
@@ -4029,7 +5595,7 @@ var skillsCmd = async (_args, deps) => {
|
|
|
4029
5595
|
};
|
|
4030
5596
|
var versionCmd = async (_args, deps) => {
|
|
4031
5597
|
deps.renderer.write(
|
|
4032
|
-
`WrongStack ${CLI_VERSION} (apiVersion ${API_VERSION}, node ${process.version}, ${
|
|
5598
|
+
`WrongStack ${CLI_VERSION} (apiVersion ${API_VERSION}, node ${process.version}, ${os4.platform()})
|
|
4033
5599
|
`
|
|
4034
5600
|
);
|
|
4035
5601
|
return 0;
|
|
@@ -4069,6 +5635,7 @@ var subcommands = {
|
|
|
4069
5635
|
auth: authCmd,
|
|
4070
5636
|
sessions: sessionsCmd,
|
|
4071
5637
|
config: configCmd,
|
|
5638
|
+
rewind: rewindCmd,
|
|
4072
5639
|
tools: toolsCmd,
|
|
4073
5640
|
skills: skillsCmd,
|
|
4074
5641
|
providers: providersCmd,
|
|
@@ -4105,31 +5672,29 @@ function fmtDuration(ms) {
|
|
|
4105
5672
|
const remMin = m - h * 60;
|
|
4106
5673
|
return `${h}h${remMin}m`;
|
|
4107
5674
|
}
|
|
4108
|
-
function fmtTaskResultLine(r,
|
|
5675
|
+
function fmtTaskResultLine(r, color32) {
|
|
4109
5676
|
const stats = `${r.iterations}it ${r.toolCalls}tc ${fmtDuration(r.durationMs)}`;
|
|
4110
5677
|
const errMsg = typeof r.error === "string" ? r.error : r.error?.message;
|
|
4111
5678
|
const errKind = typeof r.error === "object" ? r.error?.kind : void 0;
|
|
4112
5679
|
const errTail = errMsg ? ` \u2014 ${errMsg.replace(/\s+/g, " ").slice(0, 80)}${errMsg.length > 80 ? "\u2026" : ""}` : "";
|
|
4113
|
-
const errKindChip = errKind ?
|
|
4114
|
-
const errSnip = errMsg || errKind ? `${errKindChip}${
|
|
5680
|
+
const errKindChip = errKind ? color32.dim(` [${errKind}]`) : "";
|
|
5681
|
+
const errSnip = errMsg || errKind ? `${errKindChip}${color32.dim(errTail)}` : "";
|
|
4115
5682
|
switch (r.status) {
|
|
4116
5683
|
case "success":
|
|
4117
|
-
return { mark:
|
|
5684
|
+
return { mark: color32.green("\u2713"), stats, tail: "" };
|
|
4118
5685
|
case "timeout":
|
|
4119
|
-
return { mark:
|
|
5686
|
+
return { mark: color32.yellow("\u23F1"), stats: `${color32.yellow("timeout")} ${stats}`, tail: errSnip };
|
|
4120
5687
|
case "stopped":
|
|
4121
|
-
return { mark:
|
|
5688
|
+
return { mark: color32.dim("\u2298"), stats: `${color32.dim("stopped")} ${stats}`, tail: errSnip };
|
|
4122
5689
|
case "failed":
|
|
4123
|
-
return { mark:
|
|
5690
|
+
return { mark: color32.red("\u2717"), stats: `${color32.red("failed")} ${stats}`, tail: errSnip };
|
|
4124
5691
|
}
|
|
4125
5692
|
}
|
|
4126
|
-
|
|
4127
|
-
// src/boot.ts
|
|
4128
5693
|
function resolveBundledSkillsDir() {
|
|
4129
5694
|
try {
|
|
4130
5695
|
const req2 = createRequire(import.meta.url);
|
|
4131
5696
|
const corePkg = req2.resolve("@wrongstack/core/package.json");
|
|
4132
|
-
return
|
|
5697
|
+
return path18.join(path18.dirname(corePkg), "skills");
|
|
4133
5698
|
} catch {
|
|
4134
5699
|
return void 0;
|
|
4135
5700
|
}
|
|
@@ -4160,11 +5725,15 @@ async function boot(argv) {
|
|
|
4160
5725
|
});
|
|
4161
5726
|
const first = positional[0];
|
|
4162
5727
|
if (first && subcommands[first]) {
|
|
4163
|
-
const
|
|
4164
|
-
|
|
4165
|
-
|
|
4166
|
-
|
|
5728
|
+
const container = createDefaultContainer({
|
|
5729
|
+
config,
|
|
5730
|
+
wpaths,
|
|
5731
|
+
logger,
|
|
5732
|
+
modelsRegistry,
|
|
5733
|
+
bundledSkillsDir: config.features.skills ? resolveBundledSkillsDir() : void 0
|
|
4167
5734
|
});
|
|
5735
|
+
const sessionStore = container.resolve(TOKENS.SessionStore);
|
|
5736
|
+
const skillLoader = container.resolve(TOKENS.SkillLoader);
|
|
4168
5737
|
const toolRegistryForSubcmd = new ToolRegistry();
|
|
4169
5738
|
toolRegistryForSubcmd.registerAllOrThrow(
|
|
4170
5739
|
[...builtinToolsPack.tools ?? []],
|
|
@@ -4265,6 +5834,9 @@ async function boot(argv) {
|
|
|
4265
5834
|
logger
|
|
4266
5835
|
};
|
|
4267
5836
|
}
|
|
5837
|
+
|
|
5838
|
+
// src/repl.ts
|
|
5839
|
+
init_sdd();
|
|
4268
5840
|
async function runRepl(opts) {
|
|
4269
5841
|
if (opts.banner !== false) printBanner(opts.renderer, opts.projectName);
|
|
4270
5842
|
let activeCtrl;
|
|
@@ -4308,6 +5880,58 @@ async function runRepl(opts) {
|
|
|
4308
5880
|
if (res?.message) opts.renderer.write(`${res.message}
|
|
4309
5881
|
`);
|
|
4310
5882
|
if (res?.exit) break;
|
|
5883
|
+
if (res?.runText) {
|
|
5884
|
+
const runBlocks = [{ type: "text", text: res.runText }];
|
|
5885
|
+
const runCtrl2 = new AbortController();
|
|
5886
|
+
activeCtrl = runCtrl2;
|
|
5887
|
+
try {
|
|
5888
|
+
const runResult = await opts.agent.run(runBlocks, { signal: runCtrl2.signal });
|
|
5889
|
+
if (runResult.status === "done" && runResult.finalText) {
|
|
5890
|
+
const specSaved = await trySaveSpecFromAIOutput(runResult.finalText);
|
|
5891
|
+
if (specSaved) {
|
|
5892
|
+
opts.renderer.write(
|
|
5893
|
+
`
|
|
5894
|
+
${color.cyan(" \u2713 Spec detected and saved! Use /sdd approve to continue.")}
|
|
5895
|
+
`
|
|
5896
|
+
);
|
|
5897
|
+
}
|
|
5898
|
+
const planSaved = trySaveImplementationPlan(runResult.finalText);
|
|
5899
|
+
if (planSaved) {
|
|
5900
|
+
opts.renderer.write(
|
|
5901
|
+
`
|
|
5902
|
+
${color.cyan(" \u2713 Implementation plan saved!")}
|
|
5903
|
+
`
|
|
5904
|
+
);
|
|
5905
|
+
}
|
|
5906
|
+
const tasksSaved = await trySaveTasksFromAIOutput(runResult.finalText);
|
|
5907
|
+
if (tasksSaved) {
|
|
5908
|
+
const progress = getTaskProgress();
|
|
5909
|
+
const count = progress?.total ?? 0;
|
|
5910
|
+
opts.renderer.write(
|
|
5911
|
+
`
|
|
5912
|
+
${color.cyan(` \u2713 ${count} tasks detected and saved! Use /sdd approve to execute.`)}
|
|
5913
|
+
`
|
|
5914
|
+
);
|
|
5915
|
+
}
|
|
5916
|
+
const sddPhase2 = getActiveSDDPhase();
|
|
5917
|
+
if (sddPhase2 === "executing") {
|
|
5918
|
+
const autoCompleted = autoDetectTaskCompletion(runResult.finalText);
|
|
5919
|
+
if (autoCompleted > 0) {
|
|
5920
|
+
const progress = getTaskProgress();
|
|
5921
|
+
if (progress) {
|
|
5922
|
+
opts.renderer.write(
|
|
5923
|
+
`
|
|
5924
|
+
${color.cyan(` \u2713 ${autoCompleted} task(s) auto-completed! Progress: ${progress.completed}/${progress.total} (${progress.percent}%)`)}
|
|
5925
|
+
`
|
|
5926
|
+
);
|
|
5927
|
+
}
|
|
5928
|
+
}
|
|
5929
|
+
}
|
|
5930
|
+
}
|
|
5931
|
+
} catch (runErr) {
|
|
5932
|
+
opts.renderer.writeWarning("AI auto-trigger failed. You can continue manually.");
|
|
5933
|
+
}
|
|
5934
|
+
}
|
|
4311
5935
|
} catch (err) {
|
|
4312
5936
|
opts.renderer.writeError(err instanceof Error ? err.message : String(err));
|
|
4313
5937
|
}
|
|
@@ -4320,20 +5944,47 @@ async function runRepl(opts) {
|
|
|
4320
5944
|
`));
|
|
4321
5945
|
}
|
|
4322
5946
|
const blocks = await builder.submit();
|
|
5947
|
+
const sddContext = getActiveSDDContext();
|
|
5948
|
+
const taskList = getTaskListText();
|
|
5949
|
+
const taskProgress = getTaskProgress();
|
|
5950
|
+
const sddPhase = getActiveSDDPhase();
|
|
5951
|
+
let sddPrefix = "";
|
|
5952
|
+
if (sddContext) {
|
|
5953
|
+
sddPrefix = `[SDD SESSION ACTIVE]
|
|
5954
|
+
${sddContext}`;
|
|
5955
|
+
if (taskList) {
|
|
5956
|
+
sddPrefix += `
|
|
5957
|
+
|
|
5958
|
+
**Current Task List:**
|
|
5959
|
+
${taskList}`;
|
|
5960
|
+
}
|
|
5961
|
+
if (taskProgress && taskProgress.total > 0) {
|
|
5962
|
+
sddPrefix += `
|
|
5963
|
+
**Progress:** ${taskProgress.completed}/${taskProgress.total} (${taskProgress.percent}%)`;
|
|
5964
|
+
}
|
|
5965
|
+
if (sddPhase === "executing" && taskProgress && taskProgress.percent === 100) {
|
|
5966
|
+
sddPrefix += "\n\n**All tasks completed! Provide a summary of everything implemented.**";
|
|
5967
|
+
}
|
|
5968
|
+
sddPrefix += "\n\n---\nUser message:\n";
|
|
5969
|
+
}
|
|
5970
|
+
const effectiveBlocks = sddPrefix ? [
|
|
5971
|
+
{ type: "text", text: sddPrefix },
|
|
5972
|
+
...blocks
|
|
5973
|
+
] : blocks;
|
|
4323
5974
|
const runCtrl = new AbortController();
|
|
4324
5975
|
activeCtrl = runCtrl;
|
|
4325
5976
|
try {
|
|
4326
5977
|
const startedAt = Date.now();
|
|
4327
5978
|
const before = opts.tokenCounter?.total();
|
|
4328
5979
|
const costBefore = opts.tokenCounter?.estimateCost().total ?? 0;
|
|
4329
|
-
const routed =
|
|
5980
|
+
const routed = effectiveBlocks.some((block) => block.type === "image") ? await routeImagesForModel(effectiveBlocks, {
|
|
4330
5981
|
supportsVision: opts.supportsVision ? await opts.supportsVision() : opts.agent.ctx.provider.capabilities.vision,
|
|
4331
5982
|
adapters: opts.visionAdapters ?? [],
|
|
4332
5983
|
ctx: opts.agent.ctx,
|
|
4333
5984
|
signal: runCtrl.signal,
|
|
4334
5985
|
providerId: opts.agent.ctx.provider.id,
|
|
4335
5986
|
model: opts.agent.ctx.model
|
|
4336
|
-
}) : { blocks, route: "none", convertedImages: 0 };
|
|
5987
|
+
}) : { blocks: effectiveBlocks, route: "none", convertedImages: 0 };
|
|
4337
5988
|
if (routed.route === "adapter") {
|
|
4338
5989
|
opts.renderer.write(
|
|
4339
5990
|
color.dim(
|
|
@@ -4356,6 +6007,55 @@ async function runRepl(opts) {
|
|
|
4356
6007
|
} else if (result.status === "max_iterations") {
|
|
4357
6008
|
opts.renderer.writeWarning(`Hit max iterations (${result.iterations}).`);
|
|
4358
6009
|
}
|
|
6010
|
+
if (result.status === "done" && result.finalText && sddContext) {
|
|
6011
|
+
const specSaved = await trySaveSpecFromAIOutput(result.finalText);
|
|
6012
|
+
if (specSaved) {
|
|
6013
|
+
opts.renderer.write(
|
|
6014
|
+
`
|
|
6015
|
+
${color.cyan(" \u2713 Spec detected and saved! Use /sdd approve to continue.")}
|
|
6016
|
+
`
|
|
6017
|
+
);
|
|
6018
|
+
}
|
|
6019
|
+
const planSaved = trySaveImplementationPlan(result.finalText);
|
|
6020
|
+
if (planSaved) {
|
|
6021
|
+
opts.renderer.write(
|
|
6022
|
+
`
|
|
6023
|
+
${color.cyan(" \u2713 Implementation plan saved!")}
|
|
6024
|
+
`
|
|
6025
|
+
);
|
|
6026
|
+
}
|
|
6027
|
+
const tasksSaved = await trySaveTasksFromAIOutput(result.finalText);
|
|
6028
|
+
if (tasksSaved) {
|
|
6029
|
+
const progress = getTaskProgress();
|
|
6030
|
+
const count = progress?.total ?? 0;
|
|
6031
|
+
opts.renderer.write(
|
|
6032
|
+
`
|
|
6033
|
+
${color.cyan(` \u2713 ${count} tasks detected and saved! Use /sdd approve to execute.`)}
|
|
6034
|
+
`
|
|
6035
|
+
);
|
|
6036
|
+
}
|
|
6037
|
+
const phase = getActiveSDDPhase();
|
|
6038
|
+
if (phase === "executing") {
|
|
6039
|
+
const autoCompleted = autoDetectTaskCompletion(result.finalText);
|
|
6040
|
+
if (autoCompleted > 0) {
|
|
6041
|
+
const progress = getTaskProgress();
|
|
6042
|
+
if (progress) {
|
|
6043
|
+
opts.renderer.write(
|
|
6044
|
+
`
|
|
6045
|
+
${color.cyan(` \u2713 ${autoCompleted} task(s) auto-completed! Progress: ${progress.completed}/${progress.total} (${progress.percent}%)`)}
|
|
6046
|
+
`
|
|
6047
|
+
);
|
|
6048
|
+
if (progress.percent === 100) {
|
|
6049
|
+
opts.renderer.write(
|
|
6050
|
+
`
|
|
6051
|
+
${color.green(" \u{1F389} All tasks completed! Use /sdd cancel to end the session.")}
|
|
6052
|
+
`
|
|
6053
|
+
);
|
|
6054
|
+
}
|
|
6055
|
+
}
|
|
6056
|
+
}
|
|
6057
|
+
}
|
|
6058
|
+
}
|
|
4359
6059
|
if (opts.tokenCounter && before) {
|
|
4360
6060
|
const after = opts.tokenCounter.total();
|
|
4361
6061
|
const costAfter = opts.tokenCounter.estimateCost().total;
|
|
@@ -4368,6 +6068,49 @@ ${color.dim(
|
|
|
4368
6068
|
`
|
|
4369
6069
|
);
|
|
4370
6070
|
}
|
|
6071
|
+
if (result.status === "done" && opts.getAutonomy) {
|
|
6072
|
+
const autonomy = opts.getAutonomy();
|
|
6073
|
+
if (autonomy === "auto") {
|
|
6074
|
+
const nextPrompt = 'Based on what you just did, what is the single most important next step? Just do it \u2014 execute the next logical step without asking for confirmation. If there is nothing meaningful left to do, say "DONE" and nothing else.';
|
|
6075
|
+
opts.renderer.write(color.dim("\n \u21B3 [autonomy] continuing\u2026\n"));
|
|
6076
|
+
const nextBlocks = [{ type: "text", text: nextPrompt }];
|
|
6077
|
+
const nextCtrl = new AbortController();
|
|
6078
|
+
activeCtrl = nextCtrl;
|
|
6079
|
+
try {
|
|
6080
|
+
const nextResult = await opts.agent.run(nextBlocks, { signal: nextCtrl.signal });
|
|
6081
|
+
if (nextResult.status === "done" && nextResult.finalText?.trim() === "DONE") {
|
|
6082
|
+
opts.renderer.write(color.dim("\n \u21B3 [autonomy] agent reports task complete.\n"));
|
|
6083
|
+
}
|
|
6084
|
+
if (opts.getAutonomy() === "auto" && nextResult.status === "done") {
|
|
6085
|
+
}
|
|
6086
|
+
} catch (err) {
|
|
6087
|
+
opts.renderer.writeError(
|
|
6088
|
+
`[autonomy] ${err instanceof Error ? err.message : String(err)}`
|
|
6089
|
+
);
|
|
6090
|
+
} finally {
|
|
6091
|
+
activeCtrl = void 0;
|
|
6092
|
+
}
|
|
6093
|
+
} else if (autonomy === "suggest") {
|
|
6094
|
+
const suggestPrompt = 'Based on what you just did, suggest 3 concrete next steps. Format: numbered list, one line each, no explanation. If there is nothing meaningful left, say "No further steps needed."';
|
|
6095
|
+
const suggestBlocks = [{ type: "text", text: suggestPrompt }];
|
|
6096
|
+
const suggestCtrl = new AbortController();
|
|
6097
|
+
activeCtrl = suggestCtrl;
|
|
6098
|
+
try {
|
|
6099
|
+
const suggestResult = await opts.agent.run(suggestBlocks, { signal: suggestCtrl.signal });
|
|
6100
|
+
if (suggestResult.status === "done" && suggestResult.finalText) {
|
|
6101
|
+
opts.renderer.write(
|
|
6102
|
+
`
|
|
6103
|
+
${color.cyan(" Suggested next steps:")}
|
|
6104
|
+
${suggestResult.finalText}
|
|
6105
|
+
`
|
|
6106
|
+
);
|
|
6107
|
+
}
|
|
6108
|
+
} catch {
|
|
6109
|
+
} finally {
|
|
6110
|
+
activeCtrl = void 0;
|
|
6111
|
+
}
|
|
6112
|
+
}
|
|
6113
|
+
}
|
|
4371
6114
|
} catch (err) {
|
|
4372
6115
|
opts.renderer.writeError(err instanceof Error ? err.message : String(err));
|
|
4373
6116
|
} finally {
|
|
@@ -4480,7 +6223,10 @@ async function execute(deps) {
|
|
|
4480
6223
|
switchProviderAndModel,
|
|
4481
6224
|
director,
|
|
4482
6225
|
fleetRoster,
|
|
4483
|
-
fleetStreamController
|
|
6226
|
+
fleetStreamController,
|
|
6227
|
+
getYolo,
|
|
6228
|
+
getAutonomy,
|
|
6229
|
+
skillLoader
|
|
4484
6230
|
} = deps;
|
|
4485
6231
|
let code = 0;
|
|
4486
6232
|
try {
|
|
@@ -4584,6 +6330,7 @@ async function execute(deps) {
|
|
|
4584
6330
|
banner: !flags["no-banner"],
|
|
4585
6331
|
queueStore,
|
|
4586
6332
|
yolo: !!config.yolo,
|
|
6333
|
+
getYolo,
|
|
4587
6334
|
appVersion: CLI_VERSION,
|
|
4588
6335
|
provider: config.provider,
|
|
4589
6336
|
family: banneredFamily,
|
|
@@ -4610,7 +6357,36 @@ async function execute(deps) {
|
|
|
4610
6357
|
},
|
|
4611
6358
|
fleetStreamController,
|
|
4612
6359
|
initialGoal: goalFlag,
|
|
4613
|
-
initialAsk: askFlag
|
|
6360
|
+
initialAsk: askFlag,
|
|
6361
|
+
getSDDContext: () => {
|
|
6362
|
+
const { getActiveSDDContext: getActiveSDDContext2 } = (init_sdd(), __toCommonJS(sdd_exports));
|
|
6363
|
+
return getActiveSDDContext2();
|
|
6364
|
+
},
|
|
6365
|
+
onSDDOutput: async (output) => {
|
|
6366
|
+
const { trySaveSpecFromAIOutput: trySaveSpecFromAIOutput2, trySaveImplementationPlan: trySaveImplementationPlan2, trySaveTasksFromAIOutput: trySaveTasksFromAIOutput2, autoDetectTaskCompletion: autoDetectTaskCompletion2, getTaskProgress: getTaskProgress2, getActiveSDDPhase: getActiveSDDPhase2 } = (init_sdd(), __toCommonJS(sdd_exports));
|
|
6367
|
+
const messages = [];
|
|
6368
|
+
const specSaved = await trySaveSpecFromAIOutput2(output);
|
|
6369
|
+
if (specSaved) messages.push("\u2713 Spec detected and saved! Use /sdd approve to continue.");
|
|
6370
|
+
const planSaved = trySaveImplementationPlan2(output);
|
|
6371
|
+
if (planSaved) messages.push("\u2713 Implementation plan saved!");
|
|
6372
|
+
const tasksSaved = await trySaveTasksFromAIOutput2(output);
|
|
6373
|
+
if (tasksSaved) {
|
|
6374
|
+
const progress = getTaskProgress2();
|
|
6375
|
+
const count = progress?.total ?? 0;
|
|
6376
|
+
messages.push(`\u2713 ${count} tasks detected and saved! Use /sdd approve to execute.`);
|
|
6377
|
+
}
|
|
6378
|
+
const sddPhase = getActiveSDDPhase2();
|
|
6379
|
+
if (sddPhase === "executing") {
|
|
6380
|
+
const autoCompleted = autoDetectTaskCompletion2(output);
|
|
6381
|
+
if (autoCompleted > 0) {
|
|
6382
|
+
const progress = getTaskProgress2();
|
|
6383
|
+
if (progress) {
|
|
6384
|
+
messages.push(`\u2713 ${autoCompleted} task(s) auto-completed! Progress: ${progress.completed}/${progress.total} (${progress.percent}%)`);
|
|
6385
|
+
}
|
|
6386
|
+
}
|
|
6387
|
+
}
|
|
6388
|
+
return messages;
|
|
6389
|
+
}
|
|
4614
6390
|
});
|
|
4615
6391
|
} finally {
|
|
4616
6392
|
renderer.setSilent(false);
|
|
@@ -4636,7 +6412,9 @@ async function execute(deps) {
|
|
|
4636
6412
|
supportsVision,
|
|
4637
6413
|
attachments,
|
|
4638
6414
|
effectiveMaxContext,
|
|
4639
|
-
projectName:
|
|
6415
|
+
projectName: path18.basename(projectRoot) || void 0,
|
|
6416
|
+
getAutonomy,
|
|
6417
|
+
skillLoader
|
|
4640
6418
|
});
|
|
4641
6419
|
} finally {
|
|
4642
6420
|
await webuiPromise.catch(() => void 0);
|
|
@@ -4652,7 +6430,9 @@ async function execute(deps) {
|
|
|
4652
6430
|
supportsVision,
|
|
4653
6431
|
attachments,
|
|
4654
6432
|
effectiveMaxContext,
|
|
4655
|
-
projectName:
|
|
6433
|
+
projectName: path18.basename(projectRoot) || void 0,
|
|
6434
|
+
getAutonomy,
|
|
6435
|
+
skillLoader
|
|
4656
6436
|
});
|
|
4657
6437
|
}
|
|
4658
6438
|
} finally {
|
|
@@ -4752,6 +6532,15 @@ var MultiAgentHost = class {
|
|
|
4752
6532
|
this.pending.delete(task.id);
|
|
4753
6533
|
this.emitLifecycleCompleted(task.id, result);
|
|
4754
6534
|
});
|
|
6535
|
+
this.director.fleet.filter("budget.threshold_reached", (e) => {
|
|
6536
|
+
const payload = e.payload;
|
|
6537
|
+
this.deps.events.emit("subagent.budget_warning", {
|
|
6538
|
+
subagentId: e.subagentId,
|
|
6539
|
+
kind: payload.kind,
|
|
6540
|
+
used: payload.used,
|
|
6541
|
+
limit: payload.limit
|
|
6542
|
+
});
|
|
6543
|
+
});
|
|
4755
6544
|
this.coordinator = this.director.coordinator;
|
|
4756
6545
|
} else {
|
|
4757
6546
|
this.coordinator = new DefaultMultiAgentCoordinator(coordinatorConfig, {});
|
|
@@ -4913,7 +6702,7 @@ var MultiAgentHost = class {
|
|
|
4913
6702
|
model: opts?.model,
|
|
4914
6703
|
tools: opts?.tools
|
|
4915
6704
|
};
|
|
4916
|
-
const transcriptPath = this.sessionFactory ?
|
|
6705
|
+
const transcriptPath = this.sessionFactory ? path18.join(this.sessionFactory.dir, `${subagentConfig.name}.jsonl`) : void 0;
|
|
4917
6706
|
if (this.director) {
|
|
4918
6707
|
const subagentId = await this.director.spawn(subagentConfig);
|
|
4919
6708
|
const taskId2 = randomUUID();
|
|
@@ -4982,8 +6771,16 @@ var MultiAgentHost = class {
|
|
|
4982
6771
|
description: v.description,
|
|
4983
6772
|
subagentId: v.subagentId
|
|
4984
6773
|
}));
|
|
4985
|
-
const
|
|
4986
|
-
|
|
6774
|
+
const live = [];
|
|
6775
|
+
if (this.coordinator) {
|
|
6776
|
+
const s = this.coordinator.getStatus();
|
|
6777
|
+
for (const a of s.subagents) {
|
|
6778
|
+
live.push({ subagentId: a.id, status: a.status, task: a.currentTask });
|
|
6779
|
+
}
|
|
6780
|
+
}
|
|
6781
|
+
const liveCount = live.filter((s) => s.status === "running" || s.status === "idle").length;
|
|
6782
|
+
const summary = !this.coordinator ? "No subagents have been spawned." : liveCount > 0 ? `${pending.length} pending, ${liveCount} active, ${this.results.length} completed.` : `${pending.length} pending, ${this.results.length} completed.`;
|
|
6783
|
+
return { pending, completed: this.results, live, summary };
|
|
4987
6784
|
}
|
|
4988
6785
|
/**
|
|
4989
6786
|
* Roll up per-subagent runtime cost from completed TaskResults. We don't
|
|
@@ -5065,16 +6862,16 @@ var MultiAgentHost = class {
|
|
|
5065
6862
|
}
|
|
5066
6863
|
this.opts.directorMode = true;
|
|
5067
6864
|
if (this.opts.fleetRoot && !this.opts.manifestPath) {
|
|
5068
|
-
this.opts.manifestPath =
|
|
6865
|
+
this.opts.manifestPath = path18.join(this.opts.fleetRoot, "fleet.json");
|
|
5069
6866
|
}
|
|
5070
6867
|
if (this.opts.fleetRoot && !this.opts.sharedScratchpadPath) {
|
|
5071
|
-
this.opts.sharedScratchpadPath =
|
|
6868
|
+
this.opts.sharedScratchpadPath = path18.join(this.opts.fleetRoot, "shared");
|
|
5072
6869
|
}
|
|
5073
6870
|
if (this.opts.fleetRoot && !this.opts.sessionsRoot) {
|
|
5074
|
-
this.opts.sessionsRoot =
|
|
6871
|
+
this.opts.sessionsRoot = path18.join(this.opts.fleetRoot, "subagents");
|
|
5075
6872
|
}
|
|
5076
6873
|
if (this.opts.fleetRoot && !this.opts.stateCheckpointPath) {
|
|
5077
|
-
this.opts.stateCheckpointPath =
|
|
6874
|
+
this.opts.stateCheckpointPath = path18.join(this.opts.fleetRoot, "director-state.json");
|
|
5078
6875
|
}
|
|
5079
6876
|
await this.ensureDirector();
|
|
5080
6877
|
return this.director ?? null;
|
|
@@ -5195,11 +6992,11 @@ var SessionStats = class {
|
|
|
5195
6992
|
if (e.name === "bash") this.bashCommands++;
|
|
5196
6993
|
else if (e.name === "fetch") this.fetches++;
|
|
5197
6994
|
if (!e.ok) return;
|
|
5198
|
-
const
|
|
5199
|
-
if (e.name === "read" &&
|
|
5200
|
-
else if (e.name === "edit" &&
|
|
5201
|
-
else if (e.name === "write" &&
|
|
5202
|
-
this.writtenPaths.add(
|
|
6995
|
+
const path19 = typeof input?.path === "string" ? input.path : void 0;
|
|
6996
|
+
if (e.name === "read" && path19) this.readPaths.add(path19);
|
|
6997
|
+
else if (e.name === "edit" && path19) this.editedPaths.add(path19);
|
|
6998
|
+
else if (e.name === "write" && path19) {
|
|
6999
|
+
this.writtenPaths.add(path19);
|
|
5203
7000
|
const content = typeof input?.content === "string" ? input.content : "";
|
|
5204
7001
|
this.bytesWritten += Buffer.byteLength(content, "utf8");
|
|
5205
7002
|
}
|
|
@@ -5405,36 +7202,14 @@ async function setupCompaction(params) {
|
|
|
5405
7202
|
const { compactor, events, modelsRegistry, context, config, provider, pipelines } = params;
|
|
5406
7203
|
const resolvedCaps = await capabilitiesFor(modelsRegistry, provider.id, context.model).catch(() => void 0);
|
|
5407
7204
|
const effectiveMaxContext = config.context.effectiveMaxContext ?? resolvedCaps?.maxContext ?? provider.capabilities.maxContext;
|
|
5408
|
-
|
|
5409
|
-
providerId: provider.id,
|
|
5410
|
-
model: context.model,
|
|
5411
|
-
resolvedCapsMaxContext: resolvedCaps?.maxContext,
|
|
5412
|
-
providerCapMaxContext: provider.capabilities.maxContext,
|
|
5413
|
-
configEffectiveMaxContext: config.context.effectiveMaxContext,
|
|
5414
|
-
effectiveMaxContext,
|
|
5415
|
-
resolvedCapsKeys: resolvedCaps ? Object.keys(resolvedCaps) : null
|
|
5416
|
-
});
|
|
7205
|
+
let autoCompactor;
|
|
5417
7206
|
if (config.context.autoCompact !== false) {
|
|
5418
|
-
|
|
7207
|
+
autoCompactor = new AutoCompactionMiddleware(
|
|
5419
7208
|
compactor,
|
|
5420
7209
|
effectiveMaxContext,
|
|
5421
|
-
|
|
5422
|
-
|
|
5423
|
-
|
|
5424
|
-
if (typeof m.content === "string") {
|
|
5425
|
-
total += Math.ceil(m.content.length / 4);
|
|
5426
|
-
} else if (Array.isArray(m.content)) {
|
|
5427
|
-
for (const b of m.content) {
|
|
5428
|
-
if (b.type === "text") {
|
|
5429
|
-
total += Math.ceil(b.text.length / 4);
|
|
5430
|
-
} else if (b.type === "tool_use" || b.type === "tool_result") {
|
|
5431
|
-
total += Math.ceil(JSON.stringify(b).length / 4);
|
|
5432
|
-
}
|
|
5433
|
-
}
|
|
5434
|
-
}
|
|
5435
|
-
}
|
|
5436
|
-
return total;
|
|
5437
|
-
},
|
|
7210
|
+
// Use the full API request estimator: messages + system prompt + tool definitions.
|
|
7211
|
+
// This matches what the provider actually counts as input tokens.
|
|
7212
|
+
(ctx) => estimateRequestTokens(ctx.messages, ctx.systemPrompt, ctx.tools ?? []).total,
|
|
5438
7213
|
{
|
|
5439
7214
|
warn: config.context.warnThreshold,
|
|
5440
7215
|
soft: config.context.softThreshold,
|
|
@@ -5444,7 +7219,7 @@ async function setupCompaction(params) {
|
|
|
5444
7219
|
);
|
|
5445
7220
|
pipelines.contextWindow.use({ name: "AutoCompaction", handler: autoCompactor.handler() });
|
|
5446
7221
|
}
|
|
5447
|
-
return effectiveMaxContext;
|
|
7222
|
+
return { effectiveMaxContext, autoCompactor };
|
|
5448
7223
|
}
|
|
5449
7224
|
function createAgent(params) {
|
|
5450
7225
|
return new Agent({
|
|
@@ -5552,12 +7327,12 @@ async function setupSession(params) {
|
|
|
5552
7327
|
}
|
|
5553
7328
|
const sessionRef = { current: session };
|
|
5554
7329
|
await recoveryLock.write(session.id).catch(() => void 0);
|
|
5555
|
-
const attachments = new DefaultAttachmentStore({ spoolDir:
|
|
5556
|
-
const queueStore = new QueueStore({ dir:
|
|
7330
|
+
const attachments = new DefaultAttachmentStore({ spoolDir: path18.join(wpaths.projectSessions, session.id, "attachments") });
|
|
7331
|
+
const queueStore = new QueueStore({ dir: path18.join(wpaths.projectSessions, session.id) });
|
|
5557
7332
|
const ctxSignal = new AbortController().signal;
|
|
5558
7333
|
const context = new Context({ systemPrompt, provider, session, signal: ctxSignal, tokenCounter, cwd, projectRoot, model: config.model });
|
|
5559
7334
|
if (restoredMessages.length > 0) context.state.replaceMessages(restoredMessages);
|
|
5560
|
-
const todosCheckpointPath =
|
|
7335
|
+
const todosCheckpointPath = path18.join(wpaths.projectSessions, `${session.id}.todos.json`);
|
|
5561
7336
|
if (resumeId) {
|
|
5562
7337
|
try {
|
|
5563
7338
|
const restoredTodos = await loadTodosCheckpoint(todosCheckpointPath);
|
|
@@ -5569,12 +7344,12 @@ async function setupSession(params) {
|
|
|
5569
7344
|
}
|
|
5570
7345
|
}
|
|
5571
7346
|
const detachTodosCheckpoint = attachTodosCheckpoint(context.state, todosCheckpointPath, session.id);
|
|
5572
|
-
const planPath =
|
|
7347
|
+
const planPath = path18.join(wpaths.projectSessions, `${session.id}.plan.json`);
|
|
5573
7348
|
context.state.setMeta("plan.path", planPath);
|
|
5574
7349
|
if (resumeId) {
|
|
5575
7350
|
try {
|
|
5576
|
-
const fleetRoot =
|
|
5577
|
-
const dirState = await loadDirectorState(
|
|
7351
|
+
const fleetRoot = path18.join(wpaths.projectSessions, session.id);
|
|
7352
|
+
const dirState = await loadDirectorState(path18.join(fleetRoot, "director-state.json"));
|
|
5578
7353
|
if (dirState) {
|
|
5579
7354
|
const tCounts = {};
|
|
5580
7355
|
for (const t of dirState.tasks) tCounts[t.status] = (tCounts[t.status] ?? 0) + 1;
|
|
@@ -5601,7 +7376,7 @@ function resolveBundledSkillsDir2() {
|
|
|
5601
7376
|
try {
|
|
5602
7377
|
const req2 = createRequire(import.meta.url);
|
|
5603
7378
|
const corePkg = req2.resolve("@wrongstack/core/package.json");
|
|
5604
|
-
return
|
|
7379
|
+
return path18.join(path18.dirname(corePkg), "skills");
|
|
5605
7380
|
} catch {
|
|
5606
7381
|
return void 0;
|
|
5607
7382
|
}
|
|
@@ -5688,7 +7463,7 @@ async function main(argv) {
|
|
|
5688
7463
|
modeId,
|
|
5689
7464
|
modePrompt,
|
|
5690
7465
|
modelCapabilities,
|
|
5691
|
-
planPath: () => sessionRef.current ?
|
|
7466
|
+
planPath: () => sessionRef.current ? path18.join(wpaths.projectSessions, `${sessionRef.current.id}.plan.json`) : void 0
|
|
5692
7467
|
})
|
|
5693
7468
|
);
|
|
5694
7469
|
const toolRegistry = new ToolRegistry();
|
|
@@ -5716,7 +7491,7 @@ async function main(argv) {
|
|
|
5716
7491
|
name: "session-store",
|
|
5717
7492
|
check: async () => {
|
|
5718
7493
|
try {
|
|
5719
|
-
await
|
|
7494
|
+
await fs5.access(wpaths.projectSessions);
|
|
5720
7495
|
return { status: "healthy" };
|
|
5721
7496
|
} catch (e) {
|
|
5722
7497
|
return { status: "unhealthy", detail: e instanceof Error ? e.message : "access denied" };
|
|
@@ -5733,7 +7508,7 @@ async function main(argv) {
|
|
|
5733
7508
|
const dumpMetrics = () => {
|
|
5734
7509
|
if (!metricsSink) return;
|
|
5735
7510
|
try {
|
|
5736
|
-
const out =
|
|
7511
|
+
const out = path18.join(wpaths.projectSessions, "metrics.json");
|
|
5737
7512
|
const snap = metricsSink.snapshot();
|
|
5738
7513
|
writeFileSync(out, JSON.stringify(snap, null, 2));
|
|
5739
7514
|
} catch {
|
|
@@ -5861,7 +7636,13 @@ async function main(argv) {
|
|
|
5861
7636
|
});
|
|
5862
7637
|
const pipelines = setupPipelines({ events, logger });
|
|
5863
7638
|
const compactor = container.resolve(TOKENS.Compactor);
|
|
5864
|
-
const effectiveMaxContext = await setupCompaction({ compactor, events, modelsRegistry, context, config, provider, pipelines });
|
|
7639
|
+
const { effectiveMaxContext, autoCompactor } = await setupCompaction({ compactor, events, modelsRegistry, context, config, provider, pipelines });
|
|
7640
|
+
const refreshMaxContext = async (providerId, modelId) => {
|
|
7641
|
+
if (!autoCompactor) return;
|
|
7642
|
+
const cap = await capabilitiesFor(modelsRegistry, providerId, modelId).catch(() => void 0);
|
|
7643
|
+
const mc = cap?.maxContext ?? config.context.effectiveMaxContext ?? 2e5;
|
|
7644
|
+
autoCompactor.setMaxContext(mc);
|
|
7645
|
+
};
|
|
5865
7646
|
const updateSpinnerContext = () => {
|
|
5866
7647
|
if (effectiveMaxContext > 0 && lastInputTokens > 0) {
|
|
5867
7648
|
spinner.setContext({ used: lastInputTokens, max: effectiveMaxContext });
|
|
@@ -5924,23 +7705,20 @@ async function main(argv) {
|
|
|
5924
7705
|
}
|
|
5925
7706
|
const switchProviderAndModel = (providerId, modelId) => {
|
|
5926
7707
|
try {
|
|
5927
|
-
console.error("[DEBUG] switchProviderAndModel called with:", { providerId, modelId });
|
|
5928
7708
|
const savedCfg = config.providers?.[providerId];
|
|
5929
7709
|
const resolvedProviderId = savedCfg?.type ?? providerId;
|
|
5930
|
-
console.error("[DEBUG] switchProviderAndModel: resolvedProviderId:", resolvedProviderId, "savedCfg.type:", savedCfg?.type);
|
|
5931
7710
|
const newCfg = savedCfg ?? {
|
|
5932
7711
|
type: providerId,
|
|
5933
7712
|
apiKey: config.apiKey,
|
|
5934
7713
|
baseUrl: config.baseUrl
|
|
5935
7714
|
};
|
|
5936
7715
|
const cfgWithType = { ...newCfg, type: resolvedProviderId };
|
|
5937
|
-
console.error("[DEBUG] switchProviderAndModel: cfgWithType:", cfgWithType);
|
|
5938
7716
|
const newProvider = config.features.modelsRegistry && providerRegistry.has(resolvedProviderId) ? providerRegistry.create(cfgWithType) : makeProviderFromConfig(resolvedProviderId, cfgWithType);
|
|
5939
|
-
console.error("[DEBUG] switchProviderAndModel: new provider id:", newProvider.id, "maxContext:", newProvider.capabilities.maxContext);
|
|
5940
7717
|
context.provider = newProvider;
|
|
5941
7718
|
context.model = modelId;
|
|
5942
7719
|
config = patchConfig(config, { provider: providerId, model: modelId });
|
|
5943
7720
|
configStore.update({ provider: providerId, model: modelId });
|
|
7721
|
+
void refreshMaxContext(resolvedProviderId, modelId);
|
|
5944
7722
|
return null;
|
|
5945
7723
|
} catch (err) {
|
|
5946
7724
|
return err instanceof Error ? err.message : String(err);
|
|
@@ -5948,12 +7726,13 @@ async function main(argv) {
|
|
|
5948
7726
|
};
|
|
5949
7727
|
const directorMode = flags["director"] === true;
|
|
5950
7728
|
let director = null;
|
|
5951
|
-
|
|
5952
|
-
const
|
|
5953
|
-
const
|
|
5954
|
-
const
|
|
5955
|
-
const
|
|
5956
|
-
const
|
|
7729
|
+
let autonomyMode = "off";
|
|
7730
|
+
const fleetRoot = directorMode ? path18.join(wpaths.projectSessions, session.id) : void 0;
|
|
7731
|
+
const manifestPath = directorMode ? typeof process.env["WRONGSTACK_FLEET_MANIFEST"] === "string" ? process.env["WRONGSTACK_FLEET_MANIFEST"] : path18.join(fleetRoot, "fleet.json") : void 0;
|
|
7732
|
+
const sharedScratchpadPath = directorMode ? path18.join(fleetRoot, "shared") : void 0;
|
|
7733
|
+
const subagentSessionsRoot = directorMode ? path18.join(fleetRoot, "subagents") : void 0;
|
|
7734
|
+
const stateCheckpointPath = directorMode ? path18.join(fleetRoot, "director-state.json") : void 0;
|
|
7735
|
+
const fleetRootForPromotion = path18.join(wpaths.projectSessions, session.id);
|
|
5957
7736
|
const multiAgentHost = new MultiAgentHost(
|
|
5958
7737
|
{
|
|
5959
7738
|
container,
|
|
@@ -6024,6 +7803,7 @@ async function main(argv) {
|
|
|
6024
7803
|
metricsSink,
|
|
6025
7804
|
healthRegistry,
|
|
6026
7805
|
planPath,
|
|
7806
|
+
modeStore,
|
|
6027
7807
|
fleetStreamController,
|
|
6028
7808
|
onSpawn: async (description, spawnOpts) => {
|
|
6029
7809
|
const { subagentId, taskId } = await multiAgentHost.spawn(description, spawnOpts);
|
|
@@ -6037,8 +7817,19 @@ async function main(argv) {
|
|
|
6037
7817
|
onAgents: () => {
|
|
6038
7818
|
const s = multiAgentHost.status();
|
|
6039
7819
|
const lines = [s.summary];
|
|
7820
|
+
const STATUS_ICON = {
|
|
7821
|
+
running: "\u25CF",
|
|
7822
|
+
idle: "\u25CB",
|
|
7823
|
+
stopped: "\u2298"
|
|
7824
|
+
};
|
|
7825
|
+
for (const a of s.live) {
|
|
7826
|
+
if (a.status === "running" || a.status === "idle") {
|
|
7827
|
+
const task = a.task ? ` \u2014 ${a.task.slice(0, 60)}` : "";
|
|
7828
|
+
lines.push(` ${STATUS_ICON[a.status] ?? "?"} ${a.subagentId.slice(0, 8)} ${a.status}${task}`);
|
|
7829
|
+
}
|
|
7830
|
+
}
|
|
6040
7831
|
for (const p of s.pending) {
|
|
6041
|
-
lines.push(` pending ${p.taskId.slice(0, 8)} \u2192 ${p.description.slice(0, 60)}`);
|
|
7832
|
+
lines.push(` \xB7 pending ${p.taskId.slice(0, 8)} \u2192 ${p.description.slice(0, 60)}`);
|
|
6042
7833
|
}
|
|
6043
7834
|
for (const r of s.completed) {
|
|
6044
7835
|
const fmt = fmtTaskResultLine(r, color);
|
|
@@ -6050,11 +7841,26 @@ async function main(argv) {
|
|
|
6050
7841
|
if (action === "status") {
|
|
6051
7842
|
const s = multiAgentHost.status();
|
|
6052
7843
|
const lines = [color.bold("Fleet status"), ` ${s.summary}`];
|
|
7844
|
+
const STATUS_ICON = {
|
|
7845
|
+
running: "\u25CF",
|
|
7846
|
+
idle: "\u25CB",
|
|
7847
|
+
stopped: "\u2298"
|
|
7848
|
+
};
|
|
7849
|
+
const liveActive = s.live.filter((a) => a.status === "running" || a.status === "idle");
|
|
7850
|
+
if (liveActive.length > 0) {
|
|
7851
|
+
lines.push("", color.dim(" Active"));
|
|
7852
|
+
for (const a of liveActive) {
|
|
7853
|
+
const task = a.task ? ` \xB7 ${a.task.slice(0, 50)}` : "";
|
|
7854
|
+
lines.push(
|
|
7855
|
+
` ${STATUS_ICON[a.status] ?? "?"} ${a.subagentId.slice(0, 8)} ${a.status}${task}`
|
|
7856
|
+
);
|
|
7857
|
+
}
|
|
7858
|
+
}
|
|
6053
7859
|
if (s.pending.length > 0) {
|
|
6054
7860
|
lines.push("", color.dim(" Pending"));
|
|
6055
7861
|
for (const p of s.pending) {
|
|
6056
7862
|
lines.push(
|
|
6057
|
-
` ${p.taskId.slice(0, 8)} \u2192 ${p.subagentId.slice(0, 8)} \xB7 ${p.description.slice(0, 60)}`
|
|
7863
|
+
` \xB7 ${p.taskId.slice(0, 8)} \u2192 ${p.subagentId.slice(0, 8)} \xB7 ${p.description.slice(0, 60)}`
|
|
6058
7864
|
);
|
|
6059
7865
|
}
|
|
6060
7866
|
}
|
|
@@ -6105,27 +7911,27 @@ async function main(argv) {
|
|
|
6105
7911
|
return `Unknown fleet action: ${action}`;
|
|
6106
7912
|
},
|
|
6107
7913
|
onFleetLog: async (subagentId, mode) => {
|
|
6108
|
-
const subagentsRoot =
|
|
7914
|
+
const subagentsRoot = path18.join(fleetRootForPromotion, "subagents");
|
|
6109
7915
|
let runDirs;
|
|
6110
7916
|
try {
|
|
6111
|
-
runDirs = await
|
|
7917
|
+
runDirs = await fs5.readdir(subagentsRoot);
|
|
6112
7918
|
} catch {
|
|
6113
7919
|
return "No fleet transcripts on disk \u2014 no subagents have been spawned for this session.";
|
|
6114
7920
|
}
|
|
6115
7921
|
const found = [];
|
|
6116
7922
|
for (const runId of runDirs) {
|
|
6117
|
-
const runDir =
|
|
7923
|
+
const runDir = path18.join(subagentsRoot, runId);
|
|
6118
7924
|
let files;
|
|
6119
7925
|
try {
|
|
6120
|
-
files = await
|
|
7926
|
+
files = await fs5.readdir(runDir);
|
|
6121
7927
|
} catch {
|
|
6122
7928
|
continue;
|
|
6123
7929
|
}
|
|
6124
7930
|
for (const f of files) {
|
|
6125
7931
|
if (!f.endsWith(".jsonl")) continue;
|
|
6126
|
-
const full =
|
|
7932
|
+
const full = path18.join(runDir, f);
|
|
6127
7933
|
try {
|
|
6128
|
-
const stat2 = await
|
|
7934
|
+
const stat2 = await fs5.stat(full);
|
|
6129
7935
|
found.push({
|
|
6130
7936
|
runId,
|
|
6131
7937
|
subagentId: f.replace(/\.jsonl$/, ""),
|
|
@@ -6164,7 +7970,7 @@ async function main(argv) {
|
|
|
6164
7970
|
].join("\n");
|
|
6165
7971
|
}
|
|
6166
7972
|
const t = matches[0];
|
|
6167
|
-
const raw = await
|
|
7973
|
+
const raw = await fs5.readFile(t.file, "utf8");
|
|
6168
7974
|
if (mode === "raw") return raw;
|
|
6169
7975
|
const lines = raw.split("\n").filter((l) => l.trim());
|
|
6170
7976
|
const counts = {};
|
|
@@ -6220,7 +8026,7 @@ async function main(argv) {
|
|
|
6220
8026
|
}
|
|
6221
8027
|
const dir = await multiAgentHost.ensureDirector();
|
|
6222
8028
|
if (!dir) return "Director is not available.";
|
|
6223
|
-
const dirStatePath =
|
|
8029
|
+
const dirStatePath = path18.join(fleetRootForPromotion, "director-state.json");
|
|
6224
8030
|
const prior = await loadDirectorState(dirStatePath);
|
|
6225
8031
|
if (!prior) {
|
|
6226
8032
|
return "No prior director-state.json found \u2014 nothing to retry.";
|
|
@@ -6291,9 +8097,9 @@ async function main(argv) {
|
|
|
6291
8097
|
for (const tool of director2.tools(FLEET_ROSTER)) {
|
|
6292
8098
|
toolRegistry.register(tool);
|
|
6293
8099
|
}
|
|
6294
|
-
const mp =
|
|
6295
|
-
const sp =
|
|
6296
|
-
const ss =
|
|
8100
|
+
const mp = path18.join(fleetRootForPromotion, "fleet.json");
|
|
8101
|
+
const sp = path18.join(fleetRootForPromotion, "shared");
|
|
8102
|
+
const ss = path18.join(fleetRootForPromotion, "subagents");
|
|
6297
8103
|
const lines = [
|
|
6298
8104
|
`${color.green("\u2713")} Promoted to director mode.`,
|
|
6299
8105
|
` Roster: ${Object.keys(FLEET_ROSTER).join(", ")}`,
|
|
@@ -6320,9 +8126,43 @@ Restart WrongStack to load or unload plugin code in this session.`;
|
|
|
6320
8126
|
}
|
|
6321
8127
|
return result.message;
|
|
6322
8128
|
},
|
|
8129
|
+
onYolo: (setTo) => {
|
|
8130
|
+
const policy = container.resolve(TOKENS.PermissionPolicy);
|
|
8131
|
+
if (setTo !== void 0) {
|
|
8132
|
+
policy.setYolo(setTo);
|
|
8133
|
+
config = patchConfig(config, { yolo: setTo });
|
|
8134
|
+
return setTo;
|
|
8135
|
+
}
|
|
8136
|
+
return policy.getYolo();
|
|
8137
|
+
},
|
|
8138
|
+
onAutonomy: (setTo) => {
|
|
8139
|
+
if (setTo !== void 0) {
|
|
8140
|
+
autonomyMode = setTo;
|
|
8141
|
+
return setTo;
|
|
8142
|
+
}
|
|
8143
|
+
return autonomyMode;
|
|
8144
|
+
},
|
|
6323
8145
|
onExit: () => {
|
|
6324
8146
|
void mcpRegistry.stopAll();
|
|
6325
8147
|
},
|
|
8148
|
+
onBeforeExit: async () => {
|
|
8149
|
+
const { spawn: spawn2 } = await import('child_process');
|
|
8150
|
+
const cwd2 = projectRoot;
|
|
8151
|
+
const statusResult = await new Promise((resolve3) => {
|
|
8152
|
+
const child = spawn2("git", ["status", "--porcelain"], { cwd: cwd2, stdio: ["ignore", "pipe", "pipe"] });
|
|
8153
|
+
let stdout = "";
|
|
8154
|
+
child.stdout?.on("data", (d) => stdout += d);
|
|
8155
|
+
child.on("close", (code) => resolve3({ stdout, code: code ?? 0 }));
|
|
8156
|
+
});
|
|
8157
|
+
if (statusResult.stdout.trim().length > 0) {
|
|
8158
|
+
const lines = statusResult.stdout.split("\n").filter(Boolean);
|
|
8159
|
+
return {
|
|
8160
|
+
abort: true,
|
|
8161
|
+
// signals there are uncommitted changes (used only for the message)
|
|
8162
|
+
message: `\u26A0 ${color.yellow(`${lines.length} uncommitted change${lines.length > 1 ? "s" : ""}`)} \u2014 session ended without commit`
|
|
8163
|
+
};
|
|
8164
|
+
}
|
|
8165
|
+
},
|
|
6326
8166
|
onClear: () => {
|
|
6327
8167
|
if (flags.tui && !flags["no-tui"]) return;
|
|
6328
8168
|
try {
|
|
@@ -6382,7 +8222,13 @@ Restart WrongStack to load or unload plugin code in this session.`;
|
|
|
6382
8222
|
switchProviderAndModel,
|
|
6383
8223
|
director: director ?? null,
|
|
6384
8224
|
fleetRoster: FLEET_ROSTER,
|
|
6385
|
-
fleetStreamController
|
|
8225
|
+
fleetStreamController,
|
|
8226
|
+
getYolo: () => {
|
|
8227
|
+
const policy = container.resolve(TOKENS.PermissionPolicy);
|
|
8228
|
+
return policy.getYolo();
|
|
8229
|
+
},
|
|
8230
|
+
getAutonomy: () => autonomyMode,
|
|
8231
|
+
skillLoader: config.features.skills ? skillLoader : void 0
|
|
6386
8232
|
});
|
|
6387
8233
|
}
|
|
6388
8234
|
async function promptRecovery(reader, renderer, abandoned, autoRecover) {
|