@sniper.ai/cli 2.0.0 → 3.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +28 -56
- package/dist/index.js +2799 -1258
- package/dist/index.js.map +1 -1
- package/package.json +5 -4
package/dist/index.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
// src/index.ts
|
|
4
4
|
import { createRequire as createRequire2 } from "module";
|
|
5
|
-
import { defineCommand as
|
|
5
|
+
import { defineCommand as defineCommand14, runMain } from "citty";
|
|
6
6
|
|
|
7
7
|
// src/commands/init.ts
|
|
8
8
|
import { defineCommand } from "citty";
|
|
@@ -13,15 +13,17 @@ import { readFile, writeFile, access } from "fs/promises";
|
|
|
13
13
|
import { join, dirname } from "path";
|
|
14
14
|
import { createRequire } from "module";
|
|
15
15
|
import YAML from "yaml";
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
16
|
+
function isV2Config(data) {
|
|
17
|
+
if (!data || typeof data !== "object") return false;
|
|
18
|
+
const cfg = data;
|
|
19
|
+
return "review_gates" in cfg || "agent_teams" in cfg || "domain_packs" in cfg;
|
|
20
|
+
}
|
|
21
|
+
function isV3Config(data) {
|
|
22
|
+
if (!data || typeof data !== "object") return false;
|
|
23
|
+
const cfg = data;
|
|
24
|
+
return "agents" in cfg && "routing" in cfg && "visibility" in cfg;
|
|
24
25
|
}
|
|
26
|
+
var CONFIG_PATH = ".sniper/config.yaml";
|
|
25
27
|
function assertField(obj, section, field, type) {
|
|
26
28
|
const val = obj[field];
|
|
27
29
|
if (typeof val !== type) {
|
|
@@ -30,18 +32,12 @@ function assertField(obj, section, field, type) {
|
|
|
30
32
|
);
|
|
31
33
|
}
|
|
32
34
|
}
|
|
33
|
-
function
|
|
35
|
+
function validateV3Config(data) {
|
|
34
36
|
if (!data || typeof data !== "object") {
|
|
35
37
|
throw new Error("Invalid config.yaml: expected an object");
|
|
36
38
|
}
|
|
37
39
|
const cfg = data;
|
|
38
|
-
for (const key of [
|
|
39
|
-
"project",
|
|
40
|
-
"stack",
|
|
41
|
-
"state",
|
|
42
|
-
"review_gates",
|
|
43
|
-
"agent_teams"
|
|
44
|
-
]) {
|
|
40
|
+
for (const key of ["project", "agents", "routing", "cost", "stack"]) {
|
|
45
41
|
if (!cfg[key] || typeof cfg[key] !== "object") {
|
|
46
42
|
throw new Error(`Invalid config.yaml: missing "${key}" section`);
|
|
47
43
|
}
|
|
@@ -49,37 +45,72 @@ function validateConfig(data) {
|
|
|
49
45
|
const project = cfg.project;
|
|
50
46
|
assertField(project, "project", "name", "string");
|
|
51
47
|
assertField(project, "project", "type", "string");
|
|
48
|
+
assertField(project, "project", "description", "string");
|
|
49
|
+
const agents = cfg.agents;
|
|
50
|
+
assertField(agents, "agents", "max_teammates", "number");
|
|
52
51
|
const stack = cfg.stack;
|
|
53
52
|
assertField(stack, "stack", "language", "string");
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
53
|
+
assertField(stack, "stack", "package_manager", "string");
|
|
54
|
+
if (!cfg.plugins || !Array.isArray(cfg.plugins)) {
|
|
55
|
+
cfg.plugins = [];
|
|
56
|
+
}
|
|
57
|
+
if (!cfg.visibility || typeof cfg.visibility !== "object") {
|
|
58
|
+
cfg.visibility = {
|
|
59
|
+
live_status: true,
|
|
60
|
+
checkpoints: true,
|
|
61
|
+
cost_tracking: true,
|
|
62
|
+
auto_retro: true
|
|
63
|
+
};
|
|
61
64
|
}
|
|
62
|
-
if (!
|
|
63
|
-
cfg.
|
|
65
|
+
if (!cfg.ownership || typeof cfg.ownership !== "object") {
|
|
66
|
+
cfg.ownership = {};
|
|
64
67
|
}
|
|
65
68
|
return data;
|
|
66
69
|
}
|
|
70
|
+
async function sniperConfigExists(cwd) {
|
|
71
|
+
try {
|
|
72
|
+
await access(join(cwd, CONFIG_PATH));
|
|
73
|
+
return true;
|
|
74
|
+
} catch {
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
67
78
|
async function readConfig(cwd) {
|
|
68
79
|
const raw = await readFile(join(cwd, CONFIG_PATH), "utf-8");
|
|
69
|
-
|
|
80
|
+
const data = YAML.parse(raw);
|
|
81
|
+
if (isV2Config(data)) {
|
|
82
|
+
throw new Error(
|
|
83
|
+
'This project uses SNIPER v2 config. Run "sniper migrate" to upgrade to v3.'
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
return validateV3Config(data);
|
|
87
|
+
}
|
|
88
|
+
async function readRawConfig(cwd) {
|
|
89
|
+
const raw = await readFile(join(cwd, CONFIG_PATH), "utf-8");
|
|
90
|
+
return YAML.parse(raw);
|
|
70
91
|
}
|
|
71
92
|
async function writeConfig(cwd, config) {
|
|
72
93
|
const content = YAML.stringify(config, { lineWidth: 0 });
|
|
73
94
|
await writeFile(join(cwd, CONFIG_PATH), content, "utf-8");
|
|
74
95
|
}
|
|
96
|
+
var DEFAULT_BUDGETS = Object.freeze({
|
|
97
|
+
full: 2e6,
|
|
98
|
+
feature: 8e5,
|
|
99
|
+
patch: 2e5,
|
|
100
|
+
ingest: 1e6,
|
|
101
|
+
explore: 5e5,
|
|
102
|
+
refactor: 6e5,
|
|
103
|
+
hotfix: 1e5
|
|
104
|
+
});
|
|
75
105
|
function getCorePath() {
|
|
76
106
|
const require3 = createRequire(import.meta.url);
|
|
77
107
|
try {
|
|
78
108
|
const corePkgPath = require3.resolve("@sniper.ai/core/package.json");
|
|
79
|
-
return
|
|
80
|
-
} catch {
|
|
109
|
+
return dirname(corePkgPath);
|
|
110
|
+
} catch (err) {
|
|
81
111
|
throw new Error(
|
|
82
|
-
'@sniper.ai/core is not installed. Run "pnpm add -D @sniper.ai/core" first.'
|
|
112
|
+
'@sniper.ai/core is not installed. Run "pnpm add -D @sniper.ai/core" first.',
|
|
113
|
+
{ cause: err }
|
|
83
114
|
);
|
|
84
115
|
}
|
|
85
116
|
}
|
|
@@ -95,119 +126,298 @@ import {
|
|
|
95
126
|
} from "fs/promises";
|
|
96
127
|
import { join as join2 } from "path";
|
|
97
128
|
import YAML2 from "yaml";
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
];
|
|
129
|
+
function assertSafeName(name, kind) {
|
|
130
|
+
if (!/^[a-z][a-z0-9-]*$/.test(name)) {
|
|
131
|
+
throw new Error(
|
|
132
|
+
`Invalid ${kind} name "${name}": must start with a letter and contain only lowercase letters, digits, and hyphens`
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
106
136
|
async function ensureDir(dir) {
|
|
107
137
|
await mkdir(dir, { recursive: true });
|
|
108
138
|
}
|
|
109
|
-
async function fileExists(
|
|
139
|
+
async function fileExists(p14) {
|
|
110
140
|
try {
|
|
111
|
-
await access2(
|
|
141
|
+
await access2(p14);
|
|
112
142
|
return true;
|
|
113
143
|
} catch {
|
|
114
144
|
return false;
|
|
115
145
|
}
|
|
116
146
|
}
|
|
147
|
+
async function composeMixin(basePath, mixinPaths) {
|
|
148
|
+
let content = await readFile2(basePath, "utf-8");
|
|
149
|
+
for (const mixinPath of mixinPaths) {
|
|
150
|
+
const mixin = await readFile2(mixinPath, "utf-8");
|
|
151
|
+
content += "\n\n---\n\n" + mixin;
|
|
152
|
+
}
|
|
153
|
+
return content;
|
|
154
|
+
}
|
|
155
|
+
function stableStringify(obj) {
|
|
156
|
+
if (obj === null || obj === void 0 || typeof obj !== "object") return JSON.stringify(obj ?? null);
|
|
157
|
+
if (Array.isArray(obj)) return "[" + obj.map(stableStringify).join(",") + "]";
|
|
158
|
+
const sorted = Object.keys(obj).sort();
|
|
159
|
+
return "{" + sorted.map((k) => JSON.stringify(k) + ":" + stableStringify(obj[k])).join(",") + "}";
|
|
160
|
+
}
|
|
161
|
+
function mergeHooks(base, ...sources) {
|
|
162
|
+
const result = { ...base };
|
|
163
|
+
if (!result.hooks || typeof result.hooks !== "object") {
|
|
164
|
+
result.hooks = {};
|
|
165
|
+
}
|
|
166
|
+
const hooks = result.hooks;
|
|
167
|
+
for (const source of sources) {
|
|
168
|
+
const sourceHooks = source.hooks || {};
|
|
169
|
+
for (const [event, entries] of Object.entries(sourceHooks)) {
|
|
170
|
+
if (!Array.isArray(entries)) continue;
|
|
171
|
+
if (!hooks[event]) hooks[event] = [];
|
|
172
|
+
for (const entry of entries) {
|
|
173
|
+
const typedEntry = entry;
|
|
174
|
+
const matcherKey = stableStringify(typedEntry.matcher || {});
|
|
175
|
+
const existing = hooks[event].find(
|
|
176
|
+
(h) => stableStringify(h.matcher || {}) === matcherKey
|
|
177
|
+
);
|
|
178
|
+
if (existing) {
|
|
179
|
+
const existingHooks = existing.hooks || [];
|
|
180
|
+
const newHooks = typedEntry.hooks || [];
|
|
181
|
+
for (const hook of newHooks) {
|
|
182
|
+
const alreadyExists = existingHooks.some((h) => h.description === hook.description);
|
|
183
|
+
if (!alreadyExists) {
|
|
184
|
+
existingHooks.push(hook);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
existing.hooks = existingHooks;
|
|
188
|
+
} else {
|
|
189
|
+
hooks[event].push(entry);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
return result;
|
|
195
|
+
}
|
|
117
196
|
async function scaffoldProject(cwd, config, options = {}) {
|
|
118
197
|
const corePath = getCorePath();
|
|
119
198
|
const sniperDir = join2(cwd, ".sniper");
|
|
120
|
-
const
|
|
199
|
+
const claudeDir = join2(cwd, ".claude");
|
|
200
|
+
const log14 = [];
|
|
121
201
|
const isUpdate = options.update === true;
|
|
122
202
|
await ensureDir(sniperDir);
|
|
123
|
-
for (const
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
}
|
|
203
|
+
for (const sub of [
|
|
204
|
+
"checkpoints",
|
|
205
|
+
"gates",
|
|
206
|
+
"retros",
|
|
207
|
+
"self-reviews",
|
|
208
|
+
"protocols",
|
|
209
|
+
"knowledge",
|
|
210
|
+
"memory/signals"
|
|
211
|
+
]) {
|
|
212
|
+
await ensureDir(join2(sniperDir, sub));
|
|
213
|
+
}
|
|
214
|
+
const checklistsSrc = join2(corePath, "checklists");
|
|
215
|
+
const checklistsDest = join2(sniperDir, "checklists");
|
|
216
|
+
await cp(checklistsSrc, checklistsDest, { recursive: true, force: true });
|
|
217
|
+
log14.push("Copied checklists/");
|
|
218
|
+
const manifestTemplate = join2(corePath, "templates", "knowledge-manifest.yaml");
|
|
219
|
+
const manifestDest = join2(sniperDir, "knowledge", "manifest.yaml");
|
|
220
|
+
if (await fileExists(manifestTemplate) && !await fileExists(manifestDest)) {
|
|
221
|
+
await cp(manifestTemplate, manifestDest);
|
|
222
|
+
log14.push("Created .sniper/knowledge/manifest.yaml");
|
|
144
223
|
}
|
|
145
|
-
log9.push("Created memory/ directory");
|
|
146
224
|
if (!isUpdate) {
|
|
147
225
|
const configContent = YAML2.stringify(config, { lineWidth: 0 });
|
|
148
226
|
await writeFile2(join2(sniperDir, "config.yaml"), configContent, "utf-8");
|
|
149
|
-
|
|
227
|
+
log14.push("Created .sniper/config.yaml");
|
|
228
|
+
}
|
|
229
|
+
await ensureDir(claudeDir);
|
|
230
|
+
await ensureDir(join2(claudeDir, "agents"));
|
|
231
|
+
const agentsSrc = join2(corePath, "agents");
|
|
232
|
+
for (const agentName of config.agents.base) {
|
|
233
|
+
assertSafeName(agentName, "agent");
|
|
234
|
+
const srcFile = join2(agentsSrc, `${agentName}.md`);
|
|
235
|
+
if (!await fileExists(srcFile)) continue;
|
|
236
|
+
const mixinNames = config.agents.mixins[agentName] || [];
|
|
237
|
+
if (mixinNames.length > 0) {
|
|
238
|
+
const mixinPaths = mixinNames.map((m) => {
|
|
239
|
+
assertSafeName(m, "mixin");
|
|
240
|
+
return join2(corePath, "personas", "cognitive", `${m}.md`);
|
|
241
|
+
});
|
|
242
|
+
const composed = await composeMixin(srcFile, mixinPaths);
|
|
243
|
+
await writeFile2(join2(claudeDir, "agents", `${agentName}.md`), composed, "utf-8");
|
|
244
|
+
} else {
|
|
245
|
+
await cp(srcFile, join2(claudeDir, "agents", `${agentName}.md`), { force: true });
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
log14.push("Scaffolded .claude/agents/");
|
|
249
|
+
const skillsSrc = join2(corePath, "skills");
|
|
250
|
+
const commandsDest = join2(claudeDir, "commands");
|
|
251
|
+
await ensureDir(commandsDest);
|
|
252
|
+
if (await fileExists(skillsSrc)) {
|
|
253
|
+
const skillDirs = await readdir(skillsSrc);
|
|
254
|
+
for (const skillDir of skillDirs) {
|
|
255
|
+
const skillFile = join2(skillsSrc, skillDir, "SKILL.md");
|
|
256
|
+
if (await fileExists(skillFile)) {
|
|
257
|
+
await cp(skillFile, join2(commandsDest, `${skillDir}.md`), { force: true });
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
log14.push("Copied skills to .claude/commands/");
|
|
262
|
+
const settingsPath = join2(claudeDir, "settings.json");
|
|
263
|
+
let settings = {};
|
|
264
|
+
if (isUpdate && await fileExists(settingsPath)) {
|
|
265
|
+
const raw = await readFile2(settingsPath, "utf-8");
|
|
266
|
+
try {
|
|
267
|
+
settings = JSON.parse(raw);
|
|
268
|
+
} catch {
|
|
269
|
+
log14.push("Warning: .claude/settings.json was invalid JSON; starting with empty settings");
|
|
270
|
+
settings = {};
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
const coreHooksPath = join2(corePath, "hooks", "settings-hooks.json");
|
|
274
|
+
if (await fileExists(coreHooksPath)) {
|
|
275
|
+
const coreHooks = JSON.parse(await readFile2(coreHooksPath, "utf-8"));
|
|
276
|
+
settings = mergeHooks(settings, coreHooks);
|
|
150
277
|
}
|
|
278
|
+
const signalHooksPath = join2(corePath, "hooks", "signal-hooks.json");
|
|
279
|
+
if (await fileExists(signalHooksPath)) {
|
|
280
|
+
const signalHooks = JSON.parse(await readFile2(signalHooksPath, "utf-8"));
|
|
281
|
+
settings = mergeHooks(settings, signalHooks);
|
|
282
|
+
}
|
|
283
|
+
if (config.plugins) {
|
|
284
|
+
for (const plugin of config.plugins) {
|
|
285
|
+
const pluginName = plugin.name;
|
|
286
|
+
const pluginYamlPath = join2(corePath, "..", "plugins", `plugin-${pluginName}`, "plugin.yaml");
|
|
287
|
+
if (!await fileExists(pluginYamlPath)) {
|
|
288
|
+
log14.push(`Warning: plugin "${pluginName}" not found at ${pluginYamlPath}`);
|
|
289
|
+
continue;
|
|
290
|
+
}
|
|
291
|
+
const pluginContent = YAML2.parse(await readFile2(pluginYamlPath, "utf-8"));
|
|
292
|
+
if (pluginContent?.hooks) {
|
|
293
|
+
const pluginHooksFormatted = {};
|
|
294
|
+
for (const [event, entries] of Object.entries(pluginContent.hooks)) {
|
|
295
|
+
if (!Array.isArray(entries)) continue;
|
|
296
|
+
pluginHooksFormatted[event] = entries.map((entry) => {
|
|
297
|
+
if (typeof entry === "object" && entry !== null && "matcher" in entry && typeof entry.matcher === "object" && "hooks" in entry && Array.isArray(entry.hooks)) {
|
|
298
|
+
return entry;
|
|
299
|
+
}
|
|
300
|
+
const cmd = String(entry);
|
|
301
|
+
return {
|
|
302
|
+
matcher: {},
|
|
303
|
+
hooks: [{
|
|
304
|
+
type: "command",
|
|
305
|
+
description: `${pluginName} plugin: ${cmd.split(" ")[0]}`,
|
|
306
|
+
command: cmd
|
|
307
|
+
}]
|
|
308
|
+
};
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
settings = mergeHooks(settings, { hooks: pluginHooksFormatted });
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
if (!settings.env || typeof settings.env !== "object") {
|
|
316
|
+
settings.env = {};
|
|
317
|
+
}
|
|
318
|
+
settings.env.CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS = "1";
|
|
319
|
+
const settingsExisted = isUpdate && await fileExists(settingsPath);
|
|
320
|
+
await writeFile2(settingsPath, JSON.stringify(settings, null, 2), "utf-8");
|
|
321
|
+
log14.push(settingsExisted ? "Updated .claude/settings.json hooks" : "Created .claude/settings.json");
|
|
151
322
|
if (!isUpdate || !await fileExists(join2(cwd, "CLAUDE.md"))) {
|
|
152
323
|
const claudeTemplate = await readFile2(
|
|
153
324
|
join2(corePath, "claude-md.template"),
|
|
154
325
|
"utf-8"
|
|
155
326
|
);
|
|
156
|
-
|
|
157
|
-
|
|
327
|
+
const claudeMd = claudeTemplate.replace("{{PROJECT_NAME}}", config.project.name).replace("{{CUSTOM_INSTRUCTIONS}}", "");
|
|
328
|
+
await writeFile2(join2(cwd, "CLAUDE.md"), claudeMd, "utf-8");
|
|
329
|
+
log14.push("Created CLAUDE.md");
|
|
158
330
|
} else {
|
|
159
|
-
|
|
331
|
+
log14.push("Skipped CLAUDE.md (preserved user customizations)");
|
|
160
332
|
}
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
join2(settingsDir, "settings.json"),
|
|
170
|
-
settingsTemplate,
|
|
171
|
-
"utf-8"
|
|
172
|
-
);
|
|
173
|
-
log9.push("Created .claude/settings.json");
|
|
174
|
-
} else {
|
|
175
|
-
log9.push("Skipped .claude/settings.json (preserved user customizations)");
|
|
333
|
+
await ensureDir(join2(cwd, "docs"));
|
|
334
|
+
const registryTemplate = join2(corePath, "templates", "registry.md");
|
|
335
|
+
const registryDest = join2(cwd, "docs", "registry.md");
|
|
336
|
+
if (await fileExists(registryTemplate) && !await fileExists(registryDest)) {
|
|
337
|
+
await cp(registryTemplate, registryDest);
|
|
338
|
+
log14.push(isUpdate ? "Created missing docs/registry.md" : "Created docs/ with registry.md");
|
|
339
|
+
} else if (!isUpdate) {
|
|
340
|
+
log14.push("Created docs/");
|
|
176
341
|
}
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
await writeFile2(join2(dir, ".gitkeep"), "", "utf-8");
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
log9.push("Created docs/ directory");
|
|
342
|
+
return log14;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// src/fs-utils.ts
|
|
346
|
+
import { mkdir as mkdir2, access as access3 } from "fs/promises";
|
|
347
|
+
async function ensureDir2(dir) {
|
|
348
|
+
await mkdir2(dir, { recursive: true });
|
|
349
|
+
}
|
|
350
|
+
async function pathExists(path) {
|
|
351
|
+
try {
|
|
352
|
+
await access3(path);
|
|
353
|
+
return true;
|
|
354
|
+
} catch {
|
|
355
|
+
return false;
|
|
195
356
|
}
|
|
196
|
-
return log9;
|
|
197
357
|
}
|
|
198
358
|
|
|
199
359
|
// src/commands/init.ts
|
|
360
|
+
import { join as join3, basename } from "path";
|
|
361
|
+
async function detectLanguage(cwd) {
|
|
362
|
+
const checks = [
|
|
363
|
+
[["tsconfig.json"], "typescript"],
|
|
364
|
+
[["pyproject.toml", "requirements.txt"], "python"],
|
|
365
|
+
[["go.mod"], "go"],
|
|
366
|
+
[["Cargo.toml"], "rust"],
|
|
367
|
+
[["pom.xml", "build.gradle"], "java"],
|
|
368
|
+
[["package.json"], "javascript"]
|
|
369
|
+
];
|
|
370
|
+
for (const [files, lang] of checks) {
|
|
371
|
+
for (const file of files) {
|
|
372
|
+
if (await pathExists(join3(cwd, file))) return lang;
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
return null;
|
|
376
|
+
}
|
|
377
|
+
async function detectPackageManager(cwd) {
|
|
378
|
+
const checks = [
|
|
379
|
+
["pnpm-lock.yaml", "pnpm"],
|
|
380
|
+
["yarn.lock", "yarn"],
|
|
381
|
+
["bun.lockb", "bun"],
|
|
382
|
+
["package-lock.json", "npm"],
|
|
383
|
+
["uv.lock", "uv"],
|
|
384
|
+
["poetry.lock", "poetry"]
|
|
385
|
+
];
|
|
386
|
+
for (const [file, pm] of checks) {
|
|
387
|
+
if (await pathExists(join3(cwd, file))) return pm;
|
|
388
|
+
}
|
|
389
|
+
return "npm";
|
|
390
|
+
}
|
|
391
|
+
async function detectTestRunner(cwd) {
|
|
392
|
+
const checks = [
|
|
393
|
+
[["vitest.config.ts", "vitest.config.js", "vitest.config.mts"], "vitest"],
|
|
394
|
+
[["jest.config.ts", "jest.config.js", "jest.config.mjs"], "jest"],
|
|
395
|
+
[["pytest.ini", "conftest.py", "pyproject.toml"], "pytest"]
|
|
396
|
+
];
|
|
397
|
+
for (const [files, runner] of checks) {
|
|
398
|
+
for (const file of files) {
|
|
399
|
+
if (await pathExists(join3(cwd, file))) return runner;
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
return null;
|
|
403
|
+
}
|
|
200
404
|
var initCommand = defineCommand({
|
|
201
405
|
meta: {
|
|
202
406
|
name: "init",
|
|
203
|
-
description: "Initialize
|
|
407
|
+
description: "Initialize SNIPER v3 in a project"
|
|
204
408
|
},
|
|
205
409
|
run: async () => {
|
|
206
410
|
const cwd = process.cwd();
|
|
207
|
-
p.intro("SNIPER \u2014 Project Initialization");
|
|
411
|
+
p.intro("SNIPER v3 \u2014 Project Initialization");
|
|
208
412
|
if (await sniperConfigExists(cwd)) {
|
|
413
|
+
const raw = await readRawConfig(cwd);
|
|
414
|
+
if (isV2Config(raw)) {
|
|
415
|
+
p.log.warning(
|
|
416
|
+
'Detected SNIPER v2 config. Run "sniper migrate" to upgrade, or reinitialize.'
|
|
417
|
+
);
|
|
418
|
+
}
|
|
209
419
|
const overwrite = await p.confirm({
|
|
210
|
-
message: "SNIPER is already initialized
|
|
420
|
+
message: "SNIPER is already initialized. Reinitialize?",
|
|
211
421
|
initialValue: false
|
|
212
422
|
});
|
|
213
423
|
if (p.isCancel(overwrite) || !overwrite) {
|
|
@@ -215,9 +425,14 @@ var initCommand = defineCommand({
|
|
|
215
425
|
process.exit(0);
|
|
216
426
|
}
|
|
217
427
|
}
|
|
428
|
+
const detectedLang = await detectLanguage(cwd);
|
|
429
|
+
const detectedPM = await detectPackageManager(cwd);
|
|
430
|
+
const detectedTestRunner = await detectTestRunner(cwd);
|
|
431
|
+
const dirName = basename(cwd);
|
|
218
432
|
const projectName = await p.text({
|
|
219
433
|
message: "Project name:",
|
|
220
|
-
placeholder:
|
|
434
|
+
placeholder: dirName,
|
|
435
|
+
initialValue: dirName,
|
|
221
436
|
validate: (v) => v.length === 0 ? "Project name is required" : void 0
|
|
222
437
|
});
|
|
223
438
|
if (p.isCancel(projectName)) {
|
|
@@ -240,17 +455,19 @@ var initCommand = defineCommand({
|
|
|
240
455
|
process.exit(0);
|
|
241
456
|
}
|
|
242
457
|
const description = await p.text({
|
|
243
|
-
message: "One-line
|
|
244
|
-
placeholder: "A brief description
|
|
458
|
+
message: "One-line description:",
|
|
459
|
+
placeholder: "A brief description"
|
|
245
460
|
});
|
|
246
461
|
if (p.isCancel(description)) {
|
|
247
462
|
p.cancel("Aborted.");
|
|
248
463
|
process.exit(0);
|
|
249
464
|
}
|
|
250
465
|
const language = await p.select({
|
|
251
|
-
message:
|
|
466
|
+
message: `Primary language${detectedLang ? ` (detected: ${detectedLang})` : ""}:`,
|
|
467
|
+
initialValue: detectedLang || "typescript",
|
|
252
468
|
options: [
|
|
253
469
|
{ value: "typescript", label: "TypeScript" },
|
|
470
|
+
{ value: "javascript", label: "JavaScript" },
|
|
254
471
|
{ value: "python", label: "Python" },
|
|
255
472
|
{ value: "go", label: "Go" },
|
|
256
473
|
{ value: "rust", label: "Rust" },
|
|
@@ -261,63 +478,6 @@ var initCommand = defineCommand({
|
|
|
261
478
|
p.cancel("Aborted.");
|
|
262
479
|
process.exit(0);
|
|
263
480
|
}
|
|
264
|
-
const frontend = await p.select({
|
|
265
|
-
message: "Frontend framework:",
|
|
266
|
-
options: [
|
|
267
|
-
{ value: "react", label: "React" },
|
|
268
|
-
{ value: "nextjs", label: "Next.js" },
|
|
269
|
-
{ value: "vue", label: "Vue" },
|
|
270
|
-
{ value: "svelte", label: "Svelte" },
|
|
271
|
-
{ value: "none", label: "None" }
|
|
272
|
-
]
|
|
273
|
-
});
|
|
274
|
-
if (p.isCancel(frontend)) {
|
|
275
|
-
p.cancel("Aborted.");
|
|
276
|
-
process.exit(0);
|
|
277
|
-
}
|
|
278
|
-
const backend = await p.select({
|
|
279
|
-
message: "Backend framework:",
|
|
280
|
-
options: [
|
|
281
|
-
{ value: "node-express", label: "Node + Express" },
|
|
282
|
-
{ value: "node-fastify", label: "Node + Fastify" },
|
|
283
|
-
{ value: "django", label: "Django" },
|
|
284
|
-
{ value: "fastapi", label: "FastAPI" },
|
|
285
|
-
{ value: "gin", label: "Go Gin" },
|
|
286
|
-
{ value: "none", label: "None" }
|
|
287
|
-
]
|
|
288
|
-
});
|
|
289
|
-
if (p.isCancel(backend)) {
|
|
290
|
-
p.cancel("Aborted.");
|
|
291
|
-
process.exit(0);
|
|
292
|
-
}
|
|
293
|
-
const database = await p.select({
|
|
294
|
-
message: "Primary database:",
|
|
295
|
-
options: [
|
|
296
|
-
{ value: "postgresql", label: "PostgreSQL" },
|
|
297
|
-
{ value: "mysql", label: "MySQL" },
|
|
298
|
-
{ value: "mongodb", label: "MongoDB" },
|
|
299
|
-
{ value: "sqlite", label: "SQLite" },
|
|
300
|
-
{ value: "none", label: "None" }
|
|
301
|
-
]
|
|
302
|
-
});
|
|
303
|
-
if (p.isCancel(database)) {
|
|
304
|
-
p.cancel("Aborted.");
|
|
305
|
-
process.exit(0);
|
|
306
|
-
}
|
|
307
|
-
const infrastructure = await p.select({
|
|
308
|
-
message: "Cloud infrastructure:",
|
|
309
|
-
options: [
|
|
310
|
-
{ value: "aws", label: "AWS" },
|
|
311
|
-
{ value: "gcp", label: "Google Cloud" },
|
|
312
|
-
{ value: "azure", label: "Azure" },
|
|
313
|
-
{ value: "vercel", label: "Vercel" },
|
|
314
|
-
{ value: "none", label: "None / Self-hosted" }
|
|
315
|
-
]
|
|
316
|
-
});
|
|
317
|
-
if (p.isCancel(infrastructure)) {
|
|
318
|
-
p.cancel("Aborted.");
|
|
319
|
-
process.exit(0);
|
|
320
|
-
}
|
|
321
481
|
const maxTeammates = await p.text({
|
|
322
482
|
message: "Max concurrent agent teammates:",
|
|
323
483
|
placeholder: "5",
|
|
@@ -338,82 +498,83 @@ var initCommand = defineCommand({
|
|
|
338
498
|
type: projectType,
|
|
339
499
|
description: description || ""
|
|
340
500
|
},
|
|
341
|
-
|
|
342
|
-
language,
|
|
343
|
-
frontend: frontend === "none" ? null : frontend,
|
|
344
|
-
backend: backend === "none" ? null : backend,
|
|
345
|
-
database: database === "none" ? null : database,
|
|
346
|
-
cache: null,
|
|
347
|
-
infrastructure: infrastructure === "none" ? null : infrastructure,
|
|
348
|
-
test_runner: null,
|
|
349
|
-
package_manager: "pnpm"
|
|
350
|
-
},
|
|
351
|
-
review_gates: {
|
|
352
|
-
after_discover: "flexible",
|
|
353
|
-
after_plan: "strict",
|
|
354
|
-
after_solve: "flexible",
|
|
355
|
-
after_sprint: "strict"
|
|
356
|
-
},
|
|
357
|
-
agent_teams: {
|
|
501
|
+
agents: {
|
|
358
502
|
max_teammates: parseInt(maxTeammates, 10),
|
|
359
|
-
default_model: "sonnet",
|
|
360
|
-
planning_model: "opus",
|
|
361
|
-
delegate_mode: true,
|
|
362
503
|
plan_approval: true,
|
|
363
|
-
coordination_timeout: 30
|
|
504
|
+
coordination_timeout: 30,
|
|
505
|
+
base: [
|
|
506
|
+
"lead-orchestrator",
|
|
507
|
+
"analyst",
|
|
508
|
+
"architect",
|
|
509
|
+
"product-manager",
|
|
510
|
+
"backend-dev",
|
|
511
|
+
"frontend-dev",
|
|
512
|
+
"qa-engineer",
|
|
513
|
+
"code-reviewer",
|
|
514
|
+
"gate-reviewer",
|
|
515
|
+
"retro-analyst"
|
|
516
|
+
],
|
|
517
|
+
mixins: {}
|
|
518
|
+
},
|
|
519
|
+
routing: {
|
|
520
|
+
auto_detect: {
|
|
521
|
+
patch_max_files: 5,
|
|
522
|
+
feature_max_files: 20
|
|
523
|
+
},
|
|
524
|
+
default: "feature",
|
|
525
|
+
budgets: { ...DEFAULT_BUDGETS }
|
|
526
|
+
},
|
|
527
|
+
cost: {
|
|
528
|
+
warn_threshold: 0.7,
|
|
529
|
+
soft_cap: 0.9,
|
|
530
|
+
hard_cap: 1
|
|
531
|
+
},
|
|
532
|
+
review: {
|
|
533
|
+
multi_model: false,
|
|
534
|
+
models: [],
|
|
535
|
+
require_consensus: true
|
|
364
536
|
},
|
|
365
|
-
domain_packs: [],
|
|
366
537
|
ownership: {
|
|
367
|
-
backend: [
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
"src/services/",
|
|
371
|
-
"src/db/",
|
|
372
|
-
"src/workers/"
|
|
373
|
-
],
|
|
374
|
-
frontend: [
|
|
375
|
-
"src/frontend/",
|
|
376
|
-
"src/components/",
|
|
377
|
-
"src/hooks/",
|
|
378
|
-
"src/styles/",
|
|
379
|
-
"src/pages/"
|
|
380
|
-
],
|
|
381
|
-
infrastructure: [
|
|
382
|
-
"docker/",
|
|
383
|
-
".github/",
|
|
384
|
-
"infra/",
|
|
385
|
-
"terraform/",
|
|
386
|
-
"scripts/"
|
|
387
|
-
],
|
|
538
|
+
backend: ["src/backend/", "src/api/", "src/services/", "src/db/"],
|
|
539
|
+
frontend: ["src/frontend/", "src/components/", "src/hooks/", "src/styles/", "src/pages/"],
|
|
540
|
+
infrastructure: ["docker/", ".github/", "infra/", "scripts/"],
|
|
388
541
|
tests: ["tests/", "__tests__/", "*.test.*", "*.spec.*"],
|
|
389
|
-
ai: ["src/ai/", "src/ml/", "src/pipeline/"],
|
|
390
542
|
docs: ["docs/"]
|
|
391
543
|
},
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
544
|
+
stack: {
|
|
545
|
+
language,
|
|
546
|
+
frontend: null,
|
|
547
|
+
backend: null,
|
|
548
|
+
database: null,
|
|
549
|
+
infrastructure: null,
|
|
550
|
+
test_runner: detectedTestRunner,
|
|
551
|
+
package_manager: detectedPM,
|
|
552
|
+
commands: {
|
|
553
|
+
test: "",
|
|
554
|
+
lint: "",
|
|
555
|
+
typecheck: "",
|
|
556
|
+
build: ""
|
|
404
557
|
}
|
|
558
|
+
},
|
|
559
|
+
plugins: [],
|
|
560
|
+
triggers: [],
|
|
561
|
+
visibility: {
|
|
562
|
+
live_status: true,
|
|
563
|
+
checkpoints: true,
|
|
564
|
+
cost_tracking: true,
|
|
565
|
+
auto_retro: true
|
|
405
566
|
}
|
|
406
567
|
};
|
|
407
568
|
const s = p.spinner();
|
|
408
|
-
s.start("Scaffolding SNIPER project...");
|
|
569
|
+
s.start("Scaffolding SNIPER v3 project...");
|
|
409
570
|
try {
|
|
410
|
-
const
|
|
571
|
+
const log14 = await scaffoldProject(cwd, config);
|
|
411
572
|
s.stop("Done!");
|
|
412
|
-
for (const entry of
|
|
573
|
+
for (const entry of log14) {
|
|
413
574
|
p.log.success(entry);
|
|
414
575
|
}
|
|
415
576
|
p.outro(
|
|
416
|
-
'SNIPER initialized. Run "sniper
|
|
577
|
+
'SNIPER v3 initialized. Run "/sniper-flow" to start your first protocol.'
|
|
417
578
|
);
|
|
418
579
|
} catch (err) {
|
|
419
580
|
s.stop("Failed!");
|
|
@@ -426,1302 +587,2682 @@ var initCommand = defineCommand({
|
|
|
426
587
|
// src/commands/status.ts
|
|
427
588
|
import { defineCommand as defineCommand2 } from "citty";
|
|
428
589
|
import * as p2 from "@clack/prompts";
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
};
|
|
590
|
+
import { readFile as readFile3 } from "fs/promises";
|
|
591
|
+
import { join as join4 } from "path";
|
|
592
|
+
import YAML3 from "yaml";
|
|
433
593
|
var statusCommand = defineCommand2({
|
|
434
594
|
meta: {
|
|
435
595
|
name: "status",
|
|
436
|
-
description: "Show SNIPER
|
|
596
|
+
description: "Show SNIPER v3 status and protocol progress"
|
|
437
597
|
},
|
|
438
598
|
run: async () => {
|
|
439
599
|
const cwd = process.cwd();
|
|
440
600
|
if (!await sniperConfigExists(cwd)) {
|
|
441
601
|
p2.log.error(
|
|
442
|
-
'SNIPER is not initialized
|
|
602
|
+
'SNIPER is not initialized. Run "sniper init" first.'
|
|
443
603
|
);
|
|
444
604
|
process.exit(1);
|
|
445
605
|
}
|
|
446
606
|
const config = await readConfig(cwd);
|
|
447
|
-
p2.intro("SNIPER Status");
|
|
607
|
+
p2.intro("SNIPER v3 Status");
|
|
448
608
|
p2.log.info(
|
|
449
609
|
`Project: ${config.project.name || "(unnamed)"} (${config.project.type})`
|
|
450
610
|
);
|
|
451
|
-
p2.log.info(
|
|
452
|
-
`Phase: ${config.state.current_phase || "not started"}`
|
|
453
|
-
);
|
|
454
|
-
if (config.state.current_sprint > 0) {
|
|
455
|
-
p2.log.info(`Sprint: ${config.state.current_sprint}`);
|
|
456
|
-
}
|
|
457
|
-
p2.log.step("Artifacts:");
|
|
458
|
-
const artifacts = config.state.artifacts;
|
|
459
|
-
for (const [name, status] of Object.entries(artifacts)) {
|
|
460
|
-
const icon = status ? ARTIFACT_ICONS[status] || "?" : "\u25CB";
|
|
461
|
-
const label = status || "\u2014";
|
|
462
|
-
console.log(` ${icon} ${name.padEnd(16)} ${label}`);
|
|
463
|
-
}
|
|
464
|
-
if (config.domain_packs && config.domain_packs.length > 0) {
|
|
465
|
-
const packNames = config.domain_packs.map((pk) => pk.name).join(", ");
|
|
466
|
-
p2.log.info(`
|
|
467
|
-
Packs: ${packNames}`);
|
|
468
|
-
}
|
|
469
|
-
const stack = config.stack;
|
|
470
611
|
const stackParts = [
|
|
471
|
-
stack.language,
|
|
472
|
-
stack.frontend,
|
|
473
|
-
stack.backend,
|
|
474
|
-
stack.database,
|
|
475
|
-
stack.infrastructure
|
|
612
|
+
config.stack.language,
|
|
613
|
+
config.stack.frontend,
|
|
614
|
+
config.stack.backend,
|
|
615
|
+
config.stack.database,
|
|
616
|
+
config.stack.infrastructure
|
|
476
617
|
].filter(Boolean);
|
|
477
618
|
p2.log.info(`Stack: ${stackParts.join(", ")}`);
|
|
619
|
+
p2.log.info(`Agents: ${config.agents.base.length} configured, max ${config.agents.max_teammates} concurrent`);
|
|
620
|
+
if (config.plugins.length > 0) {
|
|
621
|
+
const pluginNames = config.plugins.map((pk) => pk.name).join(", ");
|
|
622
|
+
p2.log.info(`Plugins: ${pluginNames}`);
|
|
623
|
+
}
|
|
624
|
+
const statusPath = join4(cwd, ".sniper", "live-status.yaml");
|
|
625
|
+
if (await pathExists(statusPath)) {
|
|
626
|
+
const raw = await readFile3(statusPath, "utf-8");
|
|
627
|
+
const liveStatus = YAML3.parse(raw);
|
|
628
|
+
if (liveStatus && liveStatus.protocol) {
|
|
629
|
+
p2.log.step("Active Protocol:");
|
|
630
|
+
console.log(` Protocol: ${liveStatus.protocol}`);
|
|
631
|
+
console.log(` Status: ${liveStatus.status}`);
|
|
632
|
+
if (liveStatus.current_phase) {
|
|
633
|
+
console.log(` Phase: ${liveStatus.current_phase}`);
|
|
634
|
+
}
|
|
635
|
+
if (Array.isArray(liveStatus.phases)) {
|
|
636
|
+
for (const phase of liveStatus.phases) {
|
|
637
|
+
const icon = phase.status === "completed" ? "\u2713" : phase.status === "in_progress" ? "\u25B6" : phase.status === "failed" ? "\u2717" : "\u25CB";
|
|
638
|
+
console.log(` ${icon} ${phase.name.padEnd(16)} ${phase.status}`);
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
if (liveStatus.cost && typeof liveStatus.cost.percent === "number" && typeof liveStatus.cost.tokens_used === "number" && typeof liveStatus.cost.budget === "number") {
|
|
642
|
+
const pct = Math.max(0, Math.min(100, Math.round(liveStatus.cost.percent * 100)));
|
|
643
|
+
const bar = "=".repeat(Math.floor(pct / 5)) + "-".repeat(20 - Math.floor(pct / 5));
|
|
644
|
+
console.log(`
|
|
645
|
+
Cost: ${(liveStatus.cost.tokens_used / 1e3).toFixed(0)}K / ${(liveStatus.cost.budget / 1e3).toFixed(0)}K tokens (${pct}%)`);
|
|
646
|
+
console.log(` [${bar}] ${pct}%`);
|
|
647
|
+
}
|
|
648
|
+
if (liveStatus.next_action) {
|
|
649
|
+
console.log(`
|
|
650
|
+
Next: ${liveStatus.next_action}`);
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
} else {
|
|
654
|
+
p2.log.info("No active protocol. Run /sniper-flow to start.");
|
|
655
|
+
}
|
|
656
|
+
p2.log.step("Protocol Routing:");
|
|
657
|
+
console.log(` Default: ${config.routing.default}`);
|
|
658
|
+
console.log(` Budgets: full=${(config.routing.budgets.full / 1e6).toFixed(1)}M, feature=${(config.routing.budgets.feature / 1e3).toFixed(0)}K, patch=${(config.routing.budgets.patch / 1e3).toFixed(0)}K`);
|
|
659
|
+
const velocityPath = join4(cwd, ".sniper", "memory", "velocity.yaml");
|
|
660
|
+
if (await pathExists(velocityPath)) {
|
|
661
|
+
const velRaw = await readFile3(velocityPath, "utf-8");
|
|
662
|
+
const velocity = YAML3.parse(velRaw);
|
|
663
|
+
if (velocity && velocity.calibrated_budgets && Object.keys(velocity.calibrated_budgets).length > 0) {
|
|
664
|
+
p2.log.step("Velocity (calibrated budgets):");
|
|
665
|
+
for (const [protocol, budget] of Object.entries(velocity.calibrated_budgets)) {
|
|
666
|
+
const configured = config.routing.budgets[protocol];
|
|
667
|
+
const calibrated = budget;
|
|
668
|
+
const avg = velocity.rolling_averages?.[protocol];
|
|
669
|
+
const avgStr = avg ? `${(avg / 1e3).toFixed(0)}K avg` : "";
|
|
670
|
+
const trend = configured && calibrated < configured * 0.9 ? "\u2193" : calibrated > configured * 1.1 ? "\u2191" : "\u2192";
|
|
671
|
+
console.log(` ${protocol}: ${avgStr} (calibrated: ${(calibrated / 1e3).toFixed(0)}K, configured: ${configured ? (configured / 1e3).toFixed(0) + "K" : "N/A"}) ${trend}`);
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
}
|
|
478
675
|
p2.outro("");
|
|
479
676
|
}
|
|
480
677
|
});
|
|
481
678
|
|
|
482
|
-
// src/commands/
|
|
679
|
+
// src/commands/migrate.ts
|
|
483
680
|
import { defineCommand as defineCommand3 } from "citty";
|
|
484
681
|
import * as p3 from "@clack/prompts";
|
|
682
|
+
import { writeFile as writeFile3, readFile as readFile4 } from "fs/promises";
|
|
683
|
+
import { join as join5 } from "path";
|
|
684
|
+
function migrateV2ToV3(v2) {
|
|
685
|
+
return {
|
|
686
|
+
project: {
|
|
687
|
+
name: v2.project.name,
|
|
688
|
+
type: v2.project.type,
|
|
689
|
+
description: v2.project.description || ""
|
|
690
|
+
},
|
|
691
|
+
agents: {
|
|
692
|
+
max_teammates: v2.agent_teams?.max_teammates || 5,
|
|
693
|
+
plan_approval: v2.agent_teams?.plan_approval ?? true,
|
|
694
|
+
coordination_timeout: v2.agent_teams?.coordination_timeout || 30,
|
|
695
|
+
base: [
|
|
696
|
+
"lead-orchestrator",
|
|
697
|
+
"analyst",
|
|
698
|
+
"architect",
|
|
699
|
+
"product-manager",
|
|
700
|
+
"backend-dev",
|
|
701
|
+
"frontend-dev",
|
|
702
|
+
"qa-engineer",
|
|
703
|
+
"code-reviewer",
|
|
704
|
+
"gate-reviewer",
|
|
705
|
+
"retro-analyst"
|
|
706
|
+
],
|
|
707
|
+
mixins: {}
|
|
708
|
+
},
|
|
709
|
+
routing: {
|
|
710
|
+
auto_detect: {
|
|
711
|
+
patch_max_files: 5,
|
|
712
|
+
feature_max_files: 20
|
|
713
|
+
},
|
|
714
|
+
default: "feature",
|
|
715
|
+
budgets: { ...DEFAULT_BUDGETS }
|
|
716
|
+
},
|
|
717
|
+
cost: {
|
|
718
|
+
warn_threshold: 0.7,
|
|
719
|
+
soft_cap: 0.9,
|
|
720
|
+
hard_cap: 1
|
|
721
|
+
},
|
|
722
|
+
review: {
|
|
723
|
+
multi_model: false,
|
|
724
|
+
models: [],
|
|
725
|
+
require_consensus: true
|
|
726
|
+
},
|
|
727
|
+
ownership: v2.ownership || {},
|
|
728
|
+
stack: {
|
|
729
|
+
language: v2.stack?.language || "",
|
|
730
|
+
frontend: v2.stack?.frontend || null,
|
|
731
|
+
backend: v2.stack?.backend || null,
|
|
732
|
+
database: v2.stack?.database || null,
|
|
733
|
+
infrastructure: v2.stack?.infrastructure || null,
|
|
734
|
+
test_runner: v2.stack?.test_runner || null,
|
|
735
|
+
package_manager: v2.stack?.package_manager || "npm",
|
|
736
|
+
commands: {
|
|
737
|
+
test: v2.stack?.commands?.test || "",
|
|
738
|
+
lint: v2.stack?.commands?.lint || "",
|
|
739
|
+
typecheck: v2.stack?.commands?.typecheck || "",
|
|
740
|
+
build: v2.stack?.commands?.build || ""
|
|
741
|
+
}
|
|
742
|
+
},
|
|
743
|
+
plugins: [],
|
|
744
|
+
triggers: [],
|
|
745
|
+
visibility: {
|
|
746
|
+
live_status: true,
|
|
747
|
+
checkpoints: true,
|
|
748
|
+
cost_tracking: true,
|
|
749
|
+
auto_retro: true
|
|
750
|
+
}
|
|
751
|
+
};
|
|
752
|
+
}
|
|
753
|
+
var migrateCommand = defineCommand3({
|
|
754
|
+
meta: {
|
|
755
|
+
name: "migrate",
|
|
756
|
+
description: "Migrate SNIPER v2 config to v3"
|
|
757
|
+
},
|
|
758
|
+
run: async () => {
|
|
759
|
+
const cwd = process.cwd();
|
|
760
|
+
p3.intro("SNIPER v2 \u2192 v3 Migration");
|
|
761
|
+
if (!await sniperConfigExists(cwd)) {
|
|
762
|
+
p3.log.error(
|
|
763
|
+
'No SNIPER config found. Run "sniper init" to initialize a new project.'
|
|
764
|
+
);
|
|
765
|
+
process.exit(1);
|
|
766
|
+
}
|
|
767
|
+
const raw = await readRawConfig(cwd);
|
|
768
|
+
if (isV3Config(raw)) {
|
|
769
|
+
p3.log.info("This project already uses SNIPER v3 config. No migration needed.");
|
|
770
|
+
p3.outro("");
|
|
771
|
+
return;
|
|
772
|
+
}
|
|
773
|
+
if (!isV2Config(raw)) {
|
|
774
|
+
p3.log.error("Unrecognized config format. Cannot migrate.");
|
|
775
|
+
process.exit(1);
|
|
776
|
+
}
|
|
777
|
+
const v2Config = raw;
|
|
778
|
+
p3.log.info(`Migrating project: ${v2Config.project.name}`);
|
|
779
|
+
const backupPath = join5(cwd, ".sniper", "config.v2.yaml");
|
|
780
|
+
const backupContent = await readFile4(join5(cwd, ".sniper", "config.yaml"), "utf-8");
|
|
781
|
+
await writeFile3(backupPath, backupContent, "utf-8");
|
|
782
|
+
p3.log.success("Backed up v2 config to .sniper/config.v2.yaml");
|
|
783
|
+
const v3Config = migrateV2ToV3(v2Config);
|
|
784
|
+
p3.log.step("Migration changes:");
|
|
785
|
+
console.log(" - review_gates \u2192 protocol-based gates");
|
|
786
|
+
console.log(" - agent_teams \u2192 agents (with base roster + mixins)");
|
|
787
|
+
console.log(" - domain_packs \u2192 plugins");
|
|
788
|
+
console.log(" - state tracking \u2192 checkpoint files");
|
|
789
|
+
console.log(" + routing (auto protocol selection)");
|
|
790
|
+
console.log(" + cost enforcement");
|
|
791
|
+
console.log(" + visibility settings");
|
|
792
|
+
const confirm6 = await p3.confirm({
|
|
793
|
+
message: "Apply migration and re-scaffold?",
|
|
794
|
+
initialValue: true
|
|
795
|
+
});
|
|
796
|
+
if (p3.isCancel(confirm6) || !confirm6) {
|
|
797
|
+
p3.cancel("Aborted. v2 config preserved.");
|
|
798
|
+
process.exit(0);
|
|
799
|
+
}
|
|
800
|
+
const s = p3.spinner();
|
|
801
|
+
s.start("Re-scaffolding with v3 structure...");
|
|
802
|
+
try {
|
|
803
|
+
const log14 = await scaffoldProject(cwd, v3Config, { update: true });
|
|
804
|
+
await writeConfig(cwd, v3Config);
|
|
805
|
+
s.stop("Done!");
|
|
806
|
+
p3.log.success("Wrote v3 config");
|
|
807
|
+
for (const entry of log14) {
|
|
808
|
+
p3.log.success(entry);
|
|
809
|
+
}
|
|
810
|
+
p3.log.warning(
|
|
811
|
+
"Review .sniper/config.yaml to configure stack commands (test, lint, build) and agent mixins."
|
|
812
|
+
);
|
|
813
|
+
p3.outro("Migration complete.");
|
|
814
|
+
} catch (err) {
|
|
815
|
+
s.stop("Failed!");
|
|
816
|
+
p3.log.error(`Migration failed: ${err}`);
|
|
817
|
+
p3.log.info("Your v2 config is preserved at .sniper/config.yaml (backup also at .sniper/config.v2.yaml)");
|
|
818
|
+
process.exit(1);
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
});
|
|
485
822
|
|
|
486
|
-
// src/
|
|
823
|
+
// src/commands/plugin.ts
|
|
824
|
+
import { defineCommand as defineCommand4 } from "citty";
|
|
825
|
+
import * as p4 from "@clack/prompts";
|
|
826
|
+
|
|
827
|
+
// src/plugin-manager.ts
|
|
487
828
|
import {
|
|
488
829
|
cp as cp2,
|
|
489
|
-
rm,
|
|
490
830
|
readdir as readdir2,
|
|
491
|
-
readFile as
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
mkdir as mkdir2
|
|
831
|
+
readFile as readFile5,
|
|
832
|
+
access as access4,
|
|
833
|
+
mkdir as mkdir3
|
|
495
834
|
} from "fs/promises";
|
|
496
|
-
import { join as
|
|
835
|
+
import { join as join6, resolve as resolve2, sep as sep2 } from "path";
|
|
497
836
|
import { execFileSync } from "child_process";
|
|
498
|
-
import
|
|
837
|
+
import YAML4 from "yaml";
|
|
838
|
+
function getPackageManagerCommand(config) {
|
|
839
|
+
return config?.stack?.package_manager || "pnpm";
|
|
840
|
+
}
|
|
499
841
|
function assertSafePath(base, untrusted) {
|
|
500
|
-
const full =
|
|
501
|
-
const safeBase =
|
|
502
|
-
if (!full.startsWith(safeBase) && full !==
|
|
842
|
+
const full = resolve2(base, untrusted);
|
|
843
|
+
const safeBase = resolve2(base) + sep2;
|
|
844
|
+
if (!full.startsWith(safeBase) && full !== resolve2(base)) {
|
|
503
845
|
throw new Error(
|
|
504
846
|
`Invalid name: path traversal detected in "${untrusted}"`
|
|
505
847
|
);
|
|
506
848
|
}
|
|
507
849
|
return full;
|
|
508
850
|
}
|
|
509
|
-
async function
|
|
851
|
+
async function pathExists2(p14) {
|
|
510
852
|
try {
|
|
511
|
-
await
|
|
853
|
+
await access4(p14);
|
|
512
854
|
return true;
|
|
513
855
|
} catch {
|
|
514
856
|
return false;
|
|
515
857
|
}
|
|
516
858
|
}
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
return JSON.parse(raw);
|
|
859
|
+
function getPackageDir(pkgName, cwd) {
|
|
860
|
+
return join6(cwd, "node_modules", ...pkgName.split("/"));
|
|
520
861
|
}
|
|
521
|
-
function
|
|
522
|
-
const
|
|
523
|
-
|
|
862
|
+
async function validatePluginYaml(pluginPath) {
|
|
863
|
+
const raw = await readFile5(pluginPath, "utf-8");
|
|
864
|
+
const manifest = YAML4.parse(raw);
|
|
865
|
+
if (!manifest.name || typeof manifest.name !== "string") {
|
|
866
|
+
throw new Error("Plugin manifest missing required 'name' field");
|
|
867
|
+
}
|
|
868
|
+
if (!manifest.version || typeof manifest.version !== "string") {
|
|
869
|
+
throw new Error("Plugin manifest missing required 'version' field");
|
|
870
|
+
}
|
|
871
|
+
return manifest;
|
|
524
872
|
}
|
|
525
|
-
async function
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
873
|
+
async function installPlugin(packageName, cwd) {
|
|
874
|
+
let projectConfig;
|
|
875
|
+
try {
|
|
876
|
+
projectConfig = await readConfig(cwd);
|
|
877
|
+
} catch {
|
|
878
|
+
}
|
|
879
|
+
const pm = getPackageManagerCommand(projectConfig);
|
|
880
|
+
execFileSync(pm, ["add", "-D", packageName], { cwd, stdio: "pipe" });
|
|
881
|
+
const pkgDir = getPackageDir(packageName, cwd);
|
|
882
|
+
const pkgJsonRaw = await readFile5(join6(pkgDir, "package.json"), "utf-8");
|
|
883
|
+
const pkgJson = JSON.parse(pkgJsonRaw);
|
|
884
|
+
const validTypes = ["plugin", "agent", "mixin", "pack"];
|
|
885
|
+
if (!pkgJson.sniper || !validTypes.includes(pkgJson.sniper.type)) {
|
|
886
|
+
execFileSync(pm, ["remove", packageName], { cwd, stdio: "pipe" });
|
|
531
887
|
throw new Error(
|
|
532
|
-
`${packageName} is not a valid SNIPER
|
|
888
|
+
`${packageName} is not a valid SNIPER package (missing sniper.type: one of ${validTypes.join(", ")})`
|
|
533
889
|
);
|
|
534
890
|
}
|
|
535
|
-
const
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
891
|
+
const sniperType = pkgJson.sniper.type;
|
|
892
|
+
if (sniperType === "agent") {
|
|
893
|
+
const agentsDir = join6(cwd, ".claude", "agents");
|
|
894
|
+
await mkdir3(agentsDir, { recursive: true });
|
|
895
|
+
const files = await readdir2(pkgDir);
|
|
896
|
+
for (const file of files) {
|
|
897
|
+
if (file.endsWith(".md") && file !== "README.md") {
|
|
898
|
+
const src = assertSafePath(pkgDir, file);
|
|
899
|
+
await cp2(src, join6(agentsDir, file), { force: true });
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
const config2 = await readConfig(cwd);
|
|
903
|
+
if (!config2.plugins.some((p14) => p14.name === pkgJson.name)) {
|
|
904
|
+
config2.plugins.push({ name: pkgJson.name, package: packageName });
|
|
905
|
+
}
|
|
906
|
+
await writeConfig(cwd, config2);
|
|
907
|
+
return { name: pkgJson.name, package: packageName, version: pkgJson.version };
|
|
908
|
+
}
|
|
909
|
+
if (sniperType === "mixin") {
|
|
910
|
+
const mixinsDir = join6(cwd, ".claude", "personas", "cognitive");
|
|
911
|
+
await mkdir3(mixinsDir, { recursive: true });
|
|
912
|
+
const files = await readdir2(pkgDir);
|
|
913
|
+
for (const file of files) {
|
|
914
|
+
if (file.endsWith(".md") && file !== "README.md") {
|
|
915
|
+
const src = assertSafePath(pkgDir, file);
|
|
916
|
+
await cp2(src, join6(mixinsDir, file), { force: true });
|
|
917
|
+
}
|
|
918
|
+
}
|
|
919
|
+
const config2 = await readConfig(cwd);
|
|
920
|
+
if (!config2.plugins.some((p14) => p14.name === pkgJson.name)) {
|
|
921
|
+
config2.plugins.push({ name: pkgJson.name, package: packageName });
|
|
922
|
+
}
|
|
923
|
+
await writeConfig(cwd, config2);
|
|
924
|
+
return { name: pkgJson.name, package: packageName, version: pkgJson.version };
|
|
925
|
+
}
|
|
926
|
+
if (sniperType === "pack") {
|
|
927
|
+
const sniperDir = join6(cwd, ".sniper");
|
|
928
|
+
const claudeDir = join6(cwd, ".claude");
|
|
929
|
+
const contentRoot = pkgJson.sniper?.packDir ? join6(pkgDir, pkgJson.sniper.packDir) : pkgDir;
|
|
930
|
+
if (pkgJson.sniper?.packDir) {
|
|
931
|
+
assertSafePath(pkgDir, pkgJson.sniper.packDir);
|
|
932
|
+
}
|
|
933
|
+
const knowledgeDir = join6(contentRoot, "knowledge");
|
|
934
|
+
if (await pathExists2(knowledgeDir)) {
|
|
935
|
+
const dest = join6(sniperDir, "knowledge");
|
|
936
|
+
await mkdir3(dest, { recursive: true });
|
|
937
|
+
await cp2(knowledgeDir, dest, { recursive: true, force: true });
|
|
938
|
+
}
|
|
939
|
+
const personasDir = join6(contentRoot, "personas");
|
|
940
|
+
if (await pathExists2(personasDir)) {
|
|
941
|
+
const dest = join6(claudeDir, "personas", "cognitive");
|
|
942
|
+
await mkdir3(dest, { recursive: true });
|
|
943
|
+
await cp2(personasDir, dest, { recursive: true, force: true });
|
|
944
|
+
}
|
|
945
|
+
const checklistsDir = join6(contentRoot, "checklists");
|
|
946
|
+
if (await pathExists2(checklistsDir)) {
|
|
947
|
+
const dest = join6(sniperDir, "checklists");
|
|
948
|
+
await mkdir3(dest, { recursive: true });
|
|
949
|
+
await cp2(checklistsDir, dest, { recursive: true, force: true });
|
|
950
|
+
}
|
|
951
|
+
const templatesDir = join6(contentRoot, "templates");
|
|
952
|
+
if (await pathExists2(templatesDir)) {
|
|
953
|
+
const dest = join6(sniperDir, "templates");
|
|
954
|
+
await mkdir3(dest, { recursive: true });
|
|
955
|
+
await cp2(templatesDir, dest, { recursive: true, force: true });
|
|
956
|
+
}
|
|
957
|
+
const config2 = await readConfig(cwd);
|
|
958
|
+
if (!config2.plugins.some((p14) => p14.name === pkgJson.name)) {
|
|
959
|
+
config2.plugins.push({ name: pkgJson.name, package: packageName });
|
|
960
|
+
}
|
|
961
|
+
await writeConfig(cwd, config2);
|
|
962
|
+
return { name: pkgJson.name, package: packageName, version: pkgJson.version };
|
|
963
|
+
}
|
|
964
|
+
const pluginYamlPath = join6(pkgDir, "plugin.yaml");
|
|
965
|
+
if (!await pathExists2(pluginYamlPath)) {
|
|
966
|
+
execFileSync(pm, ["remove", packageName], { cwd, stdio: "pipe" });
|
|
967
|
+
throw new Error(`${packageName} is missing plugin.yaml`);
|
|
968
|
+
}
|
|
969
|
+
const manifest = await validatePluginYaml(pluginYamlPath);
|
|
970
|
+
if (manifest.agent_mixins) {
|
|
971
|
+
const mixinsDir = join6(cwd, ".claude", "personas", "cognitive");
|
|
972
|
+
await mkdir3(mixinsDir, { recursive: true });
|
|
973
|
+
for (const [, mixinPaths] of Object.entries(manifest.agent_mixins)) {
|
|
974
|
+
for (const mixinPath of mixinPaths) {
|
|
975
|
+
const src = assertSafePath(pkgDir, mixinPath);
|
|
976
|
+
const parts = mixinPath.split("/");
|
|
977
|
+
const filename = parts[parts.length - 1];
|
|
978
|
+
const dest = join6(mixinsDir, filename);
|
|
979
|
+
await cp2(src, dest, { force: true });
|
|
980
|
+
}
|
|
981
|
+
}
|
|
546
982
|
}
|
|
547
983
|
const config = await readConfig(cwd);
|
|
548
|
-
if (!config.
|
|
549
|
-
|
|
550
|
-
config.domain_packs.push({ name: shortName, package: packageName });
|
|
984
|
+
if (!config.plugins.some((p14) => p14.name === manifest.name)) {
|
|
985
|
+
config.plugins.push({ name: manifest.name, package: packageName });
|
|
551
986
|
}
|
|
552
987
|
await writeConfig(cwd, config);
|
|
553
988
|
return {
|
|
554
|
-
name:
|
|
989
|
+
name: manifest.name,
|
|
555
990
|
package: packageName,
|
|
556
|
-
version:
|
|
557
|
-
contextCount
|
|
991
|
+
version: manifest.version
|
|
558
992
|
};
|
|
559
993
|
}
|
|
560
|
-
async function
|
|
994
|
+
async function removePlugin(pluginName, cwd) {
|
|
561
995
|
const config = await readConfig(cwd);
|
|
562
|
-
const
|
|
563
|
-
|
|
564
|
-
);
|
|
565
|
-
const packageName = packEntry?.package || `@sniper.ai/pack-${packName}`;
|
|
566
|
-
const domainPacksDir = join3(cwd, ".sniper", "domain-packs");
|
|
567
|
-
const packDir = assertSafePath(domainPacksDir, packName);
|
|
568
|
-
if (await pathExists(packDir)) {
|
|
569
|
-
await rm(packDir, { recursive: true, force: true });
|
|
570
|
-
}
|
|
996
|
+
const entry = config.plugins.find((p14) => p14.name === pluginName);
|
|
997
|
+
const packageName = entry?.package || `@sniper.ai/plugin-${pluginName}`;
|
|
998
|
+
const pm = getPackageManagerCommand(config);
|
|
571
999
|
try {
|
|
572
|
-
execFileSync(
|
|
1000
|
+
execFileSync(pm, ["remove", packageName], { cwd, stdio: "pipe" });
|
|
573
1001
|
} catch {
|
|
574
1002
|
}
|
|
575
|
-
config.
|
|
576
|
-
(p9) => p9.name !== packName
|
|
577
|
-
);
|
|
1003
|
+
config.plugins = config.plugins.filter((p14) => p14.name !== pluginName);
|
|
578
1004
|
await writeConfig(cwd, config);
|
|
579
1005
|
}
|
|
580
|
-
async function
|
|
581
|
-
const
|
|
582
|
-
|
|
583
|
-
const entries = await readdir2(packsDir);
|
|
584
|
-
const packs = [];
|
|
585
|
-
for (const entry of entries) {
|
|
586
|
-
const entryPath = join3(packsDir, entry);
|
|
587
|
-
const s = await stat(entryPath);
|
|
588
|
-
if (!s.isDirectory()) continue;
|
|
589
|
-
const packYaml = join3(entryPath, "pack.yaml");
|
|
590
|
-
if (await pathExists(packYaml)) {
|
|
591
|
-
const raw = await readFile3(packYaml, "utf-8");
|
|
592
|
-
const parsed = YAML3.parse(raw);
|
|
593
|
-
packs.push({ name: entry, version: parsed.version || "unknown" });
|
|
594
|
-
} else {
|
|
595
|
-
packs.push({ name: entry, version: "unknown" });
|
|
596
|
-
}
|
|
597
|
-
}
|
|
598
|
-
return packs;
|
|
599
|
-
}
|
|
600
|
-
async function searchRegistryPacks() {
|
|
601
|
-
try {
|
|
602
|
-
const result = execFileSync(
|
|
603
|
-
"npm",
|
|
604
|
-
["search", "@sniper.ai/pack-", "--json"],
|
|
605
|
-
{ encoding: "utf-8", timeout: 1e4, stdio: ["pipe", "pipe", "pipe"] }
|
|
606
|
-
).toString();
|
|
607
|
-
const packages = JSON.parse(result);
|
|
608
|
-
return packages.map(
|
|
609
|
-
(pkg) => ({
|
|
610
|
-
name: pkg.name,
|
|
611
|
-
version: pkg.version,
|
|
612
|
-
description: pkg.description || ""
|
|
613
|
-
})
|
|
614
|
-
);
|
|
615
|
-
} catch {
|
|
616
|
-
return [];
|
|
617
|
-
}
|
|
1006
|
+
async function listPlugins(cwd) {
|
|
1007
|
+
const config = await readConfig(cwd);
|
|
1008
|
+
return config.plugins;
|
|
618
1009
|
}
|
|
619
1010
|
|
|
620
|
-
// src/commands/
|
|
621
|
-
var
|
|
1011
|
+
// src/commands/plugin.ts
|
|
1012
|
+
var installSubcommand = defineCommand4({
|
|
622
1013
|
meta: {
|
|
623
|
-
name: "
|
|
624
|
-
description: "
|
|
1014
|
+
name: "install",
|
|
1015
|
+
description: "Install a SNIPER plugin"
|
|
625
1016
|
},
|
|
626
1017
|
args: {
|
|
627
|
-
|
|
1018
|
+
package: {
|
|
628
1019
|
type: "positional",
|
|
629
|
-
description: "
|
|
1020
|
+
description: "Plugin package name (e.g. @sniper.ai/plugin-typescript)",
|
|
630
1021
|
required: true
|
|
631
1022
|
}
|
|
632
1023
|
},
|
|
633
1024
|
run: async ({ args }) => {
|
|
634
1025
|
const cwd = process.cwd();
|
|
635
1026
|
if (!await sniperConfigExists(cwd)) {
|
|
636
|
-
|
|
637
|
-
'SNIPER is not initialized in this directory. Run "sniper init" first.'
|
|
638
|
-
);
|
|
1027
|
+
p4.log.error('SNIPER is not initialized. Run "sniper init" first.');
|
|
639
1028
|
process.exit(1);
|
|
640
1029
|
}
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
packageName = `@sniper.ai/pack-${packageName}`;
|
|
644
|
-
}
|
|
645
|
-
const s = p3.spinner();
|
|
646
|
-
s.start(`Installing ${packageName}...`);
|
|
1030
|
+
const s = p4.spinner();
|
|
1031
|
+
s.start(`Installing ${args.package}...`);
|
|
647
1032
|
try {
|
|
648
|
-
const result = await
|
|
1033
|
+
const result = await installPlugin(args.package, cwd);
|
|
649
1034
|
s.stop("Done!");
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
`Copied pack to .sniper/domain-packs/${result.name}/`
|
|
653
|
-
);
|
|
654
|
-
p3.log.success("Updated config.yaml with pack reference");
|
|
655
|
-
p3.log.info(
|
|
656
|
-
`
|
|
657
|
-
Pack "${result.name}" added. ${result.contextCount} context files available.`
|
|
658
|
-
);
|
|
1035
|
+
p4.log.success(`Installed plugin: ${result.name} v${result.version}`);
|
|
1036
|
+
p4.log.info("Plugin mixins and hooks have been merged into your project.");
|
|
659
1037
|
} catch (err) {
|
|
660
1038
|
s.stop("Failed!");
|
|
661
|
-
|
|
1039
|
+
p4.log.error(`Installation failed: ${err}`);
|
|
662
1040
|
process.exit(1);
|
|
663
1041
|
}
|
|
664
1042
|
}
|
|
665
1043
|
});
|
|
666
|
-
|
|
667
|
-
// src/commands/remove-pack.ts
|
|
668
|
-
import { defineCommand as defineCommand4 } from "citty";
|
|
669
|
-
import * as p4 from "@clack/prompts";
|
|
670
|
-
var removePackCommand = defineCommand4({
|
|
1044
|
+
var removeSubcommand = defineCommand4({
|
|
671
1045
|
meta: {
|
|
672
|
-
name: "remove
|
|
673
|
-
description: "Remove a
|
|
1046
|
+
name: "remove",
|
|
1047
|
+
description: "Remove a SNIPER plugin"
|
|
674
1048
|
},
|
|
675
1049
|
args: {
|
|
676
1050
|
name: {
|
|
677
1051
|
type: "positional",
|
|
678
|
-
description: "
|
|
1052
|
+
description: "Plugin name to remove",
|
|
679
1053
|
required: true
|
|
680
1054
|
}
|
|
681
1055
|
},
|
|
682
1056
|
run: async ({ args }) => {
|
|
683
1057
|
const cwd = process.cwd();
|
|
684
1058
|
if (!await sniperConfigExists(cwd)) {
|
|
685
|
-
p4.log.error(
|
|
686
|
-
'SNIPER is not initialized in this directory. Run "sniper init" first.'
|
|
687
|
-
);
|
|
1059
|
+
p4.log.error('SNIPER is not initialized. Run "sniper init" first.');
|
|
688
1060
|
process.exit(1);
|
|
689
1061
|
}
|
|
690
|
-
const confirm4 = await p4.confirm({
|
|
691
|
-
message: `Remove pack "${args.name}" and all its files?`
|
|
692
|
-
});
|
|
693
|
-
if (p4.isCancel(confirm4) || !confirm4) {
|
|
694
|
-
p4.cancel("Aborted.");
|
|
695
|
-
process.exit(0);
|
|
696
|
-
}
|
|
697
1062
|
const s = p4.spinner();
|
|
698
1063
|
s.start(`Removing ${args.name}...`);
|
|
699
1064
|
try {
|
|
700
|
-
await
|
|
1065
|
+
await removePlugin(args.name, cwd);
|
|
701
1066
|
s.stop("Done!");
|
|
702
|
-
p4.log.success(`Removed
|
|
703
|
-
p4.log.success("Updated config.yaml");
|
|
1067
|
+
p4.log.success(`Removed plugin: ${args.name}`);
|
|
704
1068
|
} catch (err) {
|
|
705
1069
|
s.stop("Failed!");
|
|
706
|
-
p4.log.error(
|
|
1070
|
+
p4.log.error(`Removal failed: ${err}`);
|
|
707
1071
|
process.exit(1);
|
|
708
1072
|
}
|
|
709
1073
|
}
|
|
710
1074
|
});
|
|
711
|
-
|
|
712
|
-
// src/commands/list-packs.ts
|
|
713
|
-
import { defineCommand as defineCommand5 } from "citty";
|
|
714
|
-
import * as p5 from "@clack/prompts";
|
|
715
|
-
var listPacksCommand = defineCommand5({
|
|
1075
|
+
var listSubcommand = defineCommand4({
|
|
716
1076
|
meta: {
|
|
717
|
-
name: "list
|
|
718
|
-
description: "List
|
|
1077
|
+
name: "list",
|
|
1078
|
+
description: "List installed SNIPER plugins"
|
|
719
1079
|
},
|
|
720
1080
|
run: async () => {
|
|
721
1081
|
const cwd = process.cwd();
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
1082
|
+
if (!await sniperConfigExists(cwd)) {
|
|
1083
|
+
p4.log.error('SNIPER is not initialized. Run "sniper init" first.');
|
|
1084
|
+
process.exit(1);
|
|
1085
|
+
}
|
|
1086
|
+
const plugins = await listPlugins(cwd);
|
|
1087
|
+
if (plugins.length === 0) {
|
|
1088
|
+
p4.log.info("No plugins installed.");
|
|
1089
|
+
return;
|
|
1090
|
+
}
|
|
1091
|
+
p4.log.step("Installed plugins:");
|
|
1092
|
+
for (const plugin of plugins) {
|
|
1093
|
+
console.log(` - ${plugin.name} (${plugin.package})`);
|
|
1094
|
+
}
|
|
1095
|
+
}
|
|
1096
|
+
});
|
|
1097
|
+
var pluginCommand = defineCommand4({
|
|
1098
|
+
meta: {
|
|
1099
|
+
name: "plugin",
|
|
1100
|
+
description: "Manage SNIPER plugins"
|
|
1101
|
+
},
|
|
1102
|
+
subCommands: {
|
|
1103
|
+
install: installSubcommand,
|
|
1104
|
+
remove: removeSubcommand,
|
|
1105
|
+
list: listSubcommand
|
|
1106
|
+
}
|
|
1107
|
+
});
|
|
1108
|
+
|
|
1109
|
+
// src/commands/protocol.ts
|
|
1110
|
+
import { defineCommand as defineCommand5 } from "citty";
|
|
1111
|
+
import * as p5 from "@clack/prompts";
|
|
1112
|
+
import { readdir as readdir3, readFile as readFile6, writeFile as writeFile4, mkdir as mkdir4, access as access5 } from "fs/promises";
|
|
1113
|
+
import { join as join7 } from "path";
|
|
1114
|
+
import YAML5 from "yaml";
|
|
1115
|
+
var CUSTOM_PROTOCOLS_DIR = ".sniper/protocols";
|
|
1116
|
+
function validateProtocol(data) {
|
|
1117
|
+
const errors = [];
|
|
1118
|
+
if (!data || typeof data !== "object") {
|
|
1119
|
+
errors.push({ path: "(root)", message: "Expected a YAML object" });
|
|
1120
|
+
return errors;
|
|
1121
|
+
}
|
|
1122
|
+
const proto = data;
|
|
1123
|
+
if (typeof proto.name !== "string" || proto.name.length === 0) {
|
|
1124
|
+
errors.push({ path: "name", message: "Required string field" });
|
|
1125
|
+
}
|
|
1126
|
+
if (typeof proto.description !== "string" || proto.description.length === 0) {
|
|
1127
|
+
errors.push({ path: "description", message: "Required string field" });
|
|
1128
|
+
}
|
|
1129
|
+
if (typeof proto.budget !== "number" || !Number.isInteger(proto.budget) || proto.budget < 1) {
|
|
1130
|
+
errors.push({ path: "budget", message: "Required positive integer" });
|
|
1131
|
+
}
|
|
1132
|
+
if (proto.auto_retro !== void 0 && typeof proto.auto_retro !== "boolean") {
|
|
1133
|
+
errors.push({ path: "auto_retro", message: "Must be a boolean" });
|
|
1134
|
+
}
|
|
1135
|
+
if (!Array.isArray(proto.phases) || proto.phases.length === 0) {
|
|
1136
|
+
errors.push({ path: "phases", message: "Required non-empty array" });
|
|
1137
|
+
return errors;
|
|
1138
|
+
}
|
|
1139
|
+
for (let i = 0; i < proto.phases.length; i++) {
|
|
1140
|
+
const phase = proto.phases[i];
|
|
1141
|
+
const prefix = `phases[${i}]`;
|
|
1142
|
+
if (!phase || typeof phase !== "object") {
|
|
1143
|
+
errors.push({ path: prefix, message: "Must be an object" });
|
|
1144
|
+
continue;
|
|
1145
|
+
}
|
|
1146
|
+
if (typeof phase.name !== "string" || phase.name.length === 0) {
|
|
1147
|
+
errors.push({ path: `${prefix}.name`, message: "Required string field" });
|
|
1148
|
+
}
|
|
1149
|
+
if (typeof phase.description !== "string" || phase.description.length === 0) {
|
|
1150
|
+
errors.push({ path: `${prefix}.description`, message: "Required string field" });
|
|
1151
|
+
}
|
|
1152
|
+
if (!Array.isArray(phase.agents) || phase.agents.length === 0) {
|
|
1153
|
+
errors.push({ path: `${prefix}.agents`, message: "Required non-empty array of strings" });
|
|
1154
|
+
} else {
|
|
1155
|
+
for (let j = 0; j < phase.agents.length; j++) {
|
|
1156
|
+
if (typeof phase.agents[j] !== "string") {
|
|
1157
|
+
errors.push({ path: `${prefix}.agents[${j}]`, message: "Must be a string" });
|
|
1158
|
+
}
|
|
1159
|
+
}
|
|
1160
|
+
}
|
|
1161
|
+
if (phase.spawn_strategy !== "single" && phase.spawn_strategy !== "team") {
|
|
1162
|
+
errors.push({ path: `${prefix}.spawn_strategy`, message: 'Must be "single" or "team"' });
|
|
1163
|
+
}
|
|
1164
|
+
if (phase.gate !== void 0) {
|
|
1165
|
+
if (!phase.gate || typeof phase.gate !== "object") {
|
|
1166
|
+
errors.push({ path: `${prefix}.gate`, message: "Must be an object" });
|
|
1167
|
+
} else {
|
|
1168
|
+
const gate = phase.gate;
|
|
1169
|
+
if (typeof gate.checklist !== "string") {
|
|
1170
|
+
errors.push({ path: `${prefix}.gate.checklist`, message: "Required string field" });
|
|
1171
|
+
}
|
|
1172
|
+
if (typeof gate.human_approval !== "boolean") {
|
|
1173
|
+
errors.push({ path: `${prefix}.gate.human_approval`, message: "Required boolean field" });
|
|
1174
|
+
}
|
|
1175
|
+
}
|
|
1176
|
+
}
|
|
1177
|
+
if (phase.plan_approval !== void 0 && typeof phase.plan_approval !== "boolean") {
|
|
1178
|
+
errors.push({ path: `${prefix}.plan_approval`, message: "Must be a boolean" });
|
|
1179
|
+
}
|
|
1180
|
+
if (phase.outputs !== void 0) {
|
|
1181
|
+
if (!Array.isArray(phase.outputs)) {
|
|
1182
|
+
errors.push({ path: `${prefix}.outputs`, message: "Must be an array of strings" });
|
|
1183
|
+
}
|
|
1184
|
+
}
|
|
1185
|
+
if (phase.coordination !== void 0) {
|
|
1186
|
+
if (!Array.isArray(phase.coordination)) {
|
|
1187
|
+
errors.push({ path: `${prefix}.coordination`, message: "Must be an array" });
|
|
1188
|
+
}
|
|
1189
|
+
}
|
|
1190
|
+
}
|
|
1191
|
+
return errors;
|
|
1192
|
+
}
|
|
1193
|
+
var createSubcommand = defineCommand5({
|
|
1194
|
+
meta: {
|
|
1195
|
+
name: "create",
|
|
1196
|
+
description: "Create a new custom protocol"
|
|
1197
|
+
},
|
|
1198
|
+
args: {
|
|
1199
|
+
name: {
|
|
1200
|
+
type: "positional",
|
|
1201
|
+
description: "Protocol name (e.g. my-workflow)",
|
|
1202
|
+
required: true
|
|
1203
|
+
}
|
|
1204
|
+
},
|
|
1205
|
+
run: async ({ args }) => {
|
|
1206
|
+
const cwd = process.cwd();
|
|
1207
|
+
if (!await sniperConfigExists(cwd)) {
|
|
1208
|
+
p5.log.error('SNIPER is not initialized. Run "sniper init" first.');
|
|
1209
|
+
process.exit(1);
|
|
1210
|
+
}
|
|
1211
|
+
const protocolName = args.name;
|
|
1212
|
+
if (!/^[a-z][a-z0-9-]*$/.test(protocolName)) {
|
|
1213
|
+
p5.log.error("Protocol name must start with a letter and contain only lowercase letters, digits, and hyphens.");
|
|
1214
|
+
process.exit(1);
|
|
1215
|
+
}
|
|
1216
|
+
const protocolsDir = join7(cwd, CUSTOM_PROTOCOLS_DIR);
|
|
1217
|
+
const targetPath = join7(protocolsDir, `${protocolName}.yaml`);
|
|
1218
|
+
try {
|
|
1219
|
+
await access5(targetPath);
|
|
1220
|
+
p5.log.error(`Protocol "${args.name}" already exists at ${CUSTOM_PROTOCOLS_DIR}/${args.name}.yaml`);
|
|
1221
|
+
process.exit(1);
|
|
1222
|
+
} catch {
|
|
1223
|
+
}
|
|
1224
|
+
const description = await p5.text({
|
|
1225
|
+
message: "Protocol description:",
|
|
1226
|
+
placeholder: "Describe the goal of your protocol",
|
|
1227
|
+
validate: (val) => {
|
|
1228
|
+
if (!val || val.trim().length === 0) return "Description is required";
|
|
1229
|
+
}
|
|
1230
|
+
});
|
|
1231
|
+
if (p5.isCancel(description)) {
|
|
1232
|
+
p5.cancel("Cancelled.");
|
|
1233
|
+
process.exit(0);
|
|
1234
|
+
}
|
|
1235
|
+
const budgetStr = await p5.text({
|
|
1236
|
+
message: "Token budget:",
|
|
1237
|
+
placeholder: "500000",
|
|
1238
|
+
initialValue: "500000",
|
|
1239
|
+
validate: (val) => {
|
|
1240
|
+
const n = Number(val);
|
|
1241
|
+
if (!Number.isInteger(n) || n < 1) return "Must be a positive integer";
|
|
1242
|
+
}
|
|
1243
|
+
});
|
|
1244
|
+
if (p5.isCancel(budgetStr)) {
|
|
1245
|
+
p5.cancel("Cancelled.");
|
|
1246
|
+
process.exit(0);
|
|
1247
|
+
}
|
|
1248
|
+
const budget = Number(budgetStr);
|
|
1249
|
+
let templateContent;
|
|
1250
|
+
try {
|
|
1251
|
+
const corePath = getCorePath();
|
|
1252
|
+
templateContent = await readFile6(
|
|
1253
|
+
join7(corePath, "templates", "custom-protocol.yaml"),
|
|
1254
|
+
"utf-8"
|
|
1255
|
+
);
|
|
1256
|
+
} catch {
|
|
1257
|
+
p5.log.warn("Could not read template from @sniper.ai/core. Using minimal template.");
|
|
1258
|
+
templateContent = YAML5.stringify({
|
|
1259
|
+
name: args.name,
|
|
1260
|
+
description,
|
|
1261
|
+
budget,
|
|
1262
|
+
phases: [
|
|
1263
|
+
{
|
|
1264
|
+
name: "implement",
|
|
1265
|
+
description: "Implementation phase",
|
|
1266
|
+
agents: ["fullstack-dev"],
|
|
1267
|
+
spawn_strategy: "single",
|
|
1268
|
+
gate: { checklist: "implement", human_approval: false },
|
|
1269
|
+
outputs: ["source code changes"]
|
|
1270
|
+
}
|
|
1271
|
+
],
|
|
1272
|
+
auto_retro: true
|
|
1273
|
+
});
|
|
1274
|
+
}
|
|
1275
|
+
let content;
|
|
1276
|
+
try {
|
|
1277
|
+
const parsed = YAML5.parse(templateContent);
|
|
1278
|
+
parsed.name = protocolName;
|
|
1279
|
+
parsed.description = description;
|
|
1280
|
+
parsed.budget = budget;
|
|
1281
|
+
content = YAML5.stringify(parsed, { lineWidth: 0 });
|
|
1282
|
+
} catch {
|
|
1283
|
+
content = templateContent.replace(/^name: .+$/m, `name: ${protocolName}`).replace(/^description: .+$/m, `description: ${YAML5.stringify(description).trim()}`).replace(/^budget: .+$/m, `budget: ${budget}`);
|
|
1284
|
+
}
|
|
1285
|
+
await mkdir4(protocolsDir, { recursive: true });
|
|
1286
|
+
await writeFile4(targetPath, content, "utf-8");
|
|
1287
|
+
p5.log.success(`Created custom protocol: ${CUSTOM_PROTOCOLS_DIR}/${protocolName}.yaml`);
|
|
1288
|
+
p5.log.info("Edit the file to customize phases, agents, and gates.");
|
|
1289
|
+
p5.log.info(`Run "sniper protocol validate ${protocolName}" to check your protocol.`);
|
|
1290
|
+
}
|
|
1291
|
+
});
|
|
1292
|
+
var listSubcommand2 = defineCommand5({
|
|
1293
|
+
meta: {
|
|
1294
|
+
name: "list",
|
|
1295
|
+
description: "List all available protocols (built-in and custom)"
|
|
1296
|
+
},
|
|
1297
|
+
run: async () => {
|
|
1298
|
+
const cwd = process.cwd();
|
|
1299
|
+
if (!await sniperConfigExists(cwd)) {
|
|
1300
|
+
p5.log.error('SNIPER is not initialized. Run "sniper init" first.');
|
|
1301
|
+
process.exit(1);
|
|
1302
|
+
}
|
|
1303
|
+
let builtInFiles = [];
|
|
1304
|
+
try {
|
|
1305
|
+
const corePath = getCorePath();
|
|
1306
|
+
const protocolsPath = join7(corePath, "protocols");
|
|
1307
|
+
const files = await readdir3(protocolsPath);
|
|
1308
|
+
builtInFiles = files.filter((f) => f.endsWith(".yaml"));
|
|
1309
|
+
} catch {
|
|
1310
|
+
p5.log.warn("Could not read built-in protocols from @sniper.ai/core.");
|
|
1311
|
+
}
|
|
1312
|
+
let customFiles = [];
|
|
1313
|
+
try {
|
|
1314
|
+
const customDir = join7(cwd, CUSTOM_PROTOCOLS_DIR);
|
|
1315
|
+
const files = await readdir3(customDir);
|
|
1316
|
+
customFiles = files.filter((f) => f.endsWith(".yaml"));
|
|
1317
|
+
} catch {
|
|
1318
|
+
}
|
|
1319
|
+
if (builtInFiles.length === 0 && customFiles.length === 0) {
|
|
1320
|
+
p5.log.info("No protocols found.");
|
|
1321
|
+
return;
|
|
1322
|
+
}
|
|
1323
|
+
if (builtInFiles.length > 0) {
|
|
1324
|
+
const corePathForList = getCorePath();
|
|
1325
|
+
p5.log.step("Built-in protocols:");
|
|
1326
|
+
for (const file of builtInFiles) {
|
|
1327
|
+
const name = file.replace(/\.yaml$/, "");
|
|
1328
|
+
try {
|
|
1329
|
+
const raw = await readFile6(join7(corePathForList, "protocols", file), "utf-8");
|
|
1330
|
+
const data = YAML5.parse(raw);
|
|
1331
|
+
console.log(` - ${name}: ${data.description || "(no description)"}`);
|
|
1332
|
+
} catch {
|
|
1333
|
+
console.log(` - ${name}`);
|
|
1334
|
+
}
|
|
1335
|
+
}
|
|
1336
|
+
}
|
|
1337
|
+
if (customFiles.length > 0) {
|
|
1338
|
+
p5.log.step("Custom protocols:");
|
|
1339
|
+
for (const file of customFiles) {
|
|
1340
|
+
const name = file.replace(/\.yaml$/, "");
|
|
1341
|
+
try {
|
|
1342
|
+
const raw = await readFile6(join7(cwd, CUSTOM_PROTOCOLS_DIR, file), "utf-8");
|
|
1343
|
+
const data = YAML5.parse(raw);
|
|
1344
|
+
console.log(` - ${name}: ${data.description || "(no description)"}`);
|
|
1345
|
+
} catch {
|
|
1346
|
+
console.log(` - ${name}`);
|
|
1347
|
+
}
|
|
1348
|
+
}
|
|
1349
|
+
}
|
|
1350
|
+
}
|
|
1351
|
+
});
|
|
1352
|
+
var validateSubcommand = defineCommand5({
|
|
1353
|
+
meta: {
|
|
1354
|
+
name: "validate",
|
|
1355
|
+
description: "Validate a custom protocol against the schema"
|
|
1356
|
+
},
|
|
1357
|
+
args: {
|
|
1358
|
+
name: {
|
|
1359
|
+
type: "positional",
|
|
1360
|
+
description: "Protocol name to validate",
|
|
1361
|
+
required: true
|
|
1362
|
+
}
|
|
1363
|
+
},
|
|
1364
|
+
run: async ({ args }) => {
|
|
1365
|
+
const cwd = process.cwd();
|
|
1366
|
+
if (!await sniperConfigExists(cwd)) {
|
|
1367
|
+
p5.log.error('SNIPER is not initialized. Run "sniper init" first.');
|
|
1368
|
+
process.exit(1);
|
|
1369
|
+
}
|
|
1370
|
+
if (!/^[a-z][a-z0-9-]*$/.test(args.name)) {
|
|
1371
|
+
p5.log.error("Protocol name must be lowercase alphanumeric with hyphens");
|
|
1372
|
+
process.exit(1);
|
|
1373
|
+
}
|
|
1374
|
+
const filePath = join7(cwd, CUSTOM_PROTOCOLS_DIR, `${args.name}.yaml`);
|
|
1375
|
+
let raw;
|
|
1376
|
+
try {
|
|
1377
|
+
raw = await readFile6(filePath, "utf-8");
|
|
1378
|
+
} catch {
|
|
1379
|
+
p5.log.error(`Protocol not found: ${CUSTOM_PROTOCOLS_DIR}/${args.name}.yaml`);
|
|
1380
|
+
process.exit(1);
|
|
1381
|
+
}
|
|
1382
|
+
let data;
|
|
1383
|
+
try {
|
|
1384
|
+
data = YAML5.parse(raw);
|
|
1385
|
+
} catch (err) {
|
|
1386
|
+
p5.log.error(`Invalid YAML: ${err}`);
|
|
1387
|
+
process.exit(1);
|
|
1388
|
+
}
|
|
1389
|
+
const errors = validateProtocol(data);
|
|
1390
|
+
if (errors.length === 0) {
|
|
1391
|
+
p5.log.success(`Protocol "${args.name}" is valid.`);
|
|
1392
|
+
} else {
|
|
1393
|
+
p5.log.error(`Protocol "${args.name}" has ${errors.length} error(s):`);
|
|
1394
|
+
for (const err of errors) {
|
|
1395
|
+
console.log(` ${err.path}: ${err.message}`);
|
|
1396
|
+
}
|
|
1397
|
+
process.exit(1);
|
|
1398
|
+
}
|
|
1399
|
+
}
|
|
1400
|
+
});
|
|
1401
|
+
var protocolCommand = defineCommand5({
|
|
1402
|
+
meta: {
|
|
1403
|
+
name: "protocol",
|
|
1404
|
+
description: "Manage SNIPER protocols"
|
|
1405
|
+
},
|
|
1406
|
+
subCommands: {
|
|
1407
|
+
create: createSubcommand,
|
|
1408
|
+
list: listSubcommand2,
|
|
1409
|
+
validate: validateSubcommand
|
|
1410
|
+
}
|
|
1411
|
+
});
|
|
1412
|
+
|
|
1413
|
+
// src/commands/dashboard.ts
|
|
1414
|
+
import { defineCommand as defineCommand6 } from "citty";
|
|
1415
|
+
import * as p6 from "@clack/prompts";
|
|
1416
|
+
import { readFile as readFile7, readdir as readdir4 } from "fs/promises";
|
|
1417
|
+
import { join as join8 } from "path";
|
|
1418
|
+
import YAML6 from "yaml";
|
|
1419
|
+
async function readYamlDir(dirPath) {
|
|
1420
|
+
if (!await pathExists(dirPath)) return [];
|
|
1421
|
+
const files = await readdir4(dirPath);
|
|
1422
|
+
const yamlFiles = files.filter((f) => f.endsWith(".yaml") || f.endsWith(".yml"));
|
|
1423
|
+
const results = [];
|
|
1424
|
+
for (const file of yamlFiles) {
|
|
1425
|
+
try {
|
|
1426
|
+
const raw = await readFile7(join8(dirPath, file), "utf-8");
|
|
1427
|
+
const parsed = YAML6.parse(raw);
|
|
1428
|
+
if (parsed) results.push(parsed);
|
|
1429
|
+
} catch {
|
|
1430
|
+
}
|
|
1431
|
+
}
|
|
1432
|
+
return results;
|
|
1433
|
+
}
|
|
1434
|
+
function formatTokens(n) {
|
|
1435
|
+
if (n >= 1e6) return `${(n / 1e6).toFixed(1)}M`;
|
|
1436
|
+
if (n >= 1e3) return `${(n / 1e3).toFixed(0)}K`;
|
|
1437
|
+
return String(n);
|
|
1438
|
+
}
|
|
1439
|
+
function aggregateData(checkpoints, gates, velocity, protocolFilter) {
|
|
1440
|
+
const filtered = protocolFilter ? checkpoints.filter((c) => c.protocol === protocolFilter) : checkpoints;
|
|
1441
|
+
const byProtocol = {};
|
|
1442
|
+
for (const cp3 of filtered) {
|
|
1443
|
+
if (!byProtocol[cp3.protocol]) {
|
|
1444
|
+
byProtocol[cp3.protocol] = { phase_tokens: 0, cumulative_tokens: 0, phases: [] };
|
|
1445
|
+
}
|
|
1446
|
+
const entry = byProtocol[cp3.protocol];
|
|
1447
|
+
entry.phase_tokens += cp3.token_usage?.phase_tokens ?? 0;
|
|
1448
|
+
if (cp3.token_usage?.cumulative_tokens && cp3.token_usage.cumulative_tokens > entry.cumulative_tokens) {
|
|
1449
|
+
entry.cumulative_tokens = cp3.token_usage.cumulative_tokens;
|
|
1450
|
+
}
|
|
1451
|
+
if (!entry.phases.includes(cp3.phase)) {
|
|
1452
|
+
entry.phases.push(cp3.phase);
|
|
1453
|
+
}
|
|
1454
|
+
}
|
|
1455
|
+
const byAgent = {};
|
|
1456
|
+
for (const cp3 of filtered) {
|
|
1457
|
+
const agentCount = cp3.agents?.length ?? 1;
|
|
1458
|
+
const tokensPerAgent = agentCount > 0 ? (cp3.token_usage?.phase_tokens ?? 0) / agentCount : 0;
|
|
1459
|
+
for (const agent of cp3.agents ?? []) {
|
|
1460
|
+
if (!byAgent[agent.name]) {
|
|
1461
|
+
byAgent[agent.name] = { tokens: 0, tasks_completed: 0, tasks_total: 0 };
|
|
1462
|
+
}
|
|
1463
|
+
byAgent[agent.name].tokens += tokensPerAgent;
|
|
1464
|
+
byAgent[agent.name].tasks_completed += agent.tasks_completed;
|
|
1465
|
+
byAgent[agent.name].tasks_total += agent.tasks_total;
|
|
1466
|
+
}
|
|
1467
|
+
}
|
|
1468
|
+
const filteredGates = protocolFilter ? gates.filter((g) => g.protocol === protocolFilter) : gates;
|
|
1469
|
+
const gateRates = {};
|
|
1470
|
+
for (const g of filteredGates) {
|
|
1471
|
+
const key = g.protocol ? `${g.protocol}/${g.gate}` : g.gate;
|
|
1472
|
+
if (!gateRates[key]) {
|
|
1473
|
+
gateRates[key] = { pass: 0, fail: 0, total_checks: 0 };
|
|
1474
|
+
}
|
|
1475
|
+
if (g.result === "pass") gateRates[key].pass++;
|
|
1476
|
+
else gateRates[key].fail++;
|
|
1477
|
+
gateRates[key].total_checks += g.total_checks;
|
|
1478
|
+
}
|
|
1479
|
+
const agentEfficiency = {};
|
|
1480
|
+
for (const [name, data] of Object.entries(byAgent)) {
|
|
1481
|
+
const totalTasks = data.tasks_completed || 1;
|
|
1482
|
+
agentEfficiency[name] = {
|
|
1483
|
+
tokens_per_task: Math.round(data.tokens / totalTasks),
|
|
1484
|
+
total_tokens: Math.round(data.tokens),
|
|
1485
|
+
total_tasks: data.tasks_completed
|
|
1486
|
+
};
|
|
1487
|
+
}
|
|
1488
|
+
const executions = velocity?.executions ?? [];
|
|
1489
|
+
const filteredExecs = protocolFilter ? executions.filter((e) => e.protocol === protocolFilter) : executions;
|
|
1490
|
+
const timeline = [];
|
|
1491
|
+
for (const cp3 of filtered) {
|
|
1492
|
+
timeline.push({
|
|
1493
|
+
timestamp: cp3.timestamp,
|
|
1494
|
+
type: "checkpoint",
|
|
1495
|
+
protocol: cp3.protocol,
|
|
1496
|
+
phase: cp3.phase,
|
|
1497
|
+
status: cp3.status
|
|
1498
|
+
});
|
|
1499
|
+
}
|
|
1500
|
+
for (const g of filteredGates) {
|
|
1501
|
+
timeline.push({
|
|
1502
|
+
timestamp: g.timestamp,
|
|
1503
|
+
type: "gate",
|
|
1504
|
+
protocol: g.protocol ?? "unknown",
|
|
1505
|
+
phase: g.gate,
|
|
1506
|
+
status: g.result
|
|
1507
|
+
});
|
|
1508
|
+
}
|
|
1509
|
+
timeline.sort((a, b) => b.timestamp.localeCompare(a.timestamp));
|
|
1510
|
+
return {
|
|
1511
|
+
cost_breakdown: { by_protocol: byProtocol, by_agent: byAgent },
|
|
1512
|
+
performance_trends: {
|
|
1513
|
+
executions: filteredExecs,
|
|
1514
|
+
calibrated_budgets: velocity?.calibrated_budgets ?? {},
|
|
1515
|
+
rolling_averages: velocity?.rolling_averages ?? {}
|
|
1516
|
+
},
|
|
1517
|
+
gate_pass_rates: gateRates,
|
|
1518
|
+
agent_efficiency: agentEfficiency,
|
|
1519
|
+
timeline: timeline.slice(0, 20)
|
|
1520
|
+
};
|
|
1521
|
+
}
|
|
1522
|
+
function renderDashboard(data, config) {
|
|
1523
|
+
p6.log.step("Cost Breakdown");
|
|
1524
|
+
const protocols = Object.entries(data.cost_breakdown.by_protocol);
|
|
1525
|
+
if (protocols.length === 0) {
|
|
1526
|
+
console.log(" No checkpoint data found.");
|
|
1527
|
+
} else {
|
|
1528
|
+
for (const [protocol, info] of protocols) {
|
|
1529
|
+
const budget = config.routing.budgets[protocol];
|
|
1530
|
+
const budgetStr = budget ? ` / ${formatTokens(budget)} budget` : "";
|
|
1531
|
+
console.log(` ${protocol}: ${formatTokens(info.cumulative_tokens)} cumulative${budgetStr}`);
|
|
1532
|
+
console.log(` Phase tokens: ${formatTokens(info.phase_tokens)} across ${info.phases.length} phase(s)`);
|
|
1533
|
+
}
|
|
1534
|
+
}
|
|
1535
|
+
const agents = Object.entries(data.cost_breakdown.by_agent);
|
|
1536
|
+
if (agents.length > 0) {
|
|
1537
|
+
console.log("");
|
|
1538
|
+
console.log(" By Agent:");
|
|
1539
|
+
for (const [name, info] of agents) {
|
|
1540
|
+
console.log(` ${name.padEnd(24)} ${formatTokens(info.tokens).padStart(8)} tokens (${info.tasks_completed}/${info.tasks_total} tasks)`);
|
|
1541
|
+
}
|
|
1542
|
+
}
|
|
1543
|
+
p6.log.step("Performance Trends");
|
|
1544
|
+
const execs = data.performance_trends.executions;
|
|
1545
|
+
if (execs.length === 0) {
|
|
1546
|
+
console.log(" No execution history found.");
|
|
1547
|
+
} else {
|
|
1548
|
+
const byProto = {};
|
|
1549
|
+
for (const e of execs) {
|
|
1550
|
+
if (!byProto[e.protocol]) byProto[e.protocol] = [];
|
|
1551
|
+
byProto[e.protocol].push(e);
|
|
1552
|
+
}
|
|
1553
|
+
for (const [proto, runs] of Object.entries(byProto)) {
|
|
1554
|
+
const avg = Math.round(runs.reduce((s, r) => s + r.tokens_used, 0) / runs.length);
|
|
1555
|
+
const calibrated = data.performance_trends.calibrated_budgets[proto];
|
|
1556
|
+
const rolling = data.performance_trends.rolling_averages[proto];
|
|
1557
|
+
console.log(` ${proto}: ${runs.length} execution(s), avg ${formatTokens(avg)} tokens`);
|
|
1558
|
+
if (rolling) console.log(` Rolling average: ${formatTokens(rolling)}`);
|
|
1559
|
+
if (calibrated) console.log(` Calibrated budget (p75): ${formatTokens(calibrated)}`);
|
|
1560
|
+
}
|
|
1561
|
+
}
|
|
1562
|
+
p6.log.step("Gate Pass Rates");
|
|
1563
|
+
const gateEntries = Object.entries(data.gate_pass_rates);
|
|
1564
|
+
if (gateEntries.length === 0) {
|
|
1565
|
+
console.log(" No gate results found.");
|
|
1566
|
+
} else {
|
|
1567
|
+
for (const [key, info] of gateEntries) {
|
|
1568
|
+
const total = info.pass + info.fail;
|
|
1569
|
+
const rate = total > 0 ? Math.round(info.pass / total * 100) : 0;
|
|
1570
|
+
const icon = rate === 100 ? "\u2713" : rate >= 50 ? "~" : "\u2717";
|
|
1571
|
+
console.log(` ${icon} ${key.padEnd(28)} ${rate}% pass (${info.pass}/${total}), ${info.total_checks} checks`);
|
|
1572
|
+
}
|
|
1573
|
+
}
|
|
1574
|
+
p6.log.step("Agent Efficiency");
|
|
1575
|
+
const effEntries = Object.entries(data.agent_efficiency);
|
|
1576
|
+
if (effEntries.length === 0) {
|
|
1577
|
+
console.log(" No agent data found.");
|
|
1578
|
+
} else {
|
|
1579
|
+
for (const [name, info] of effEntries) {
|
|
1580
|
+
console.log(` ${name.padEnd(24)} ${formatTokens(info.tokens_per_task).padStart(8)} tokens/task (${info.total_tasks} tasks, ${formatTokens(info.total_tokens)} total)`);
|
|
1581
|
+
}
|
|
1582
|
+
}
|
|
1583
|
+
p6.log.step("Timeline (recent)");
|
|
1584
|
+
if (data.timeline.length === 0) {
|
|
1585
|
+
console.log(" No recent activity.");
|
|
1586
|
+
} else {
|
|
1587
|
+
for (const entry of data.timeline.slice(0, 10)) {
|
|
1588
|
+
const ts = entry.timestamp.replace("T", " ").substring(0, 19);
|
|
1589
|
+
const icon = entry.type === "gate" ? entry.status === "pass" ? "\u2713" : "\u2717" : "\u25B6";
|
|
1590
|
+
const phaseStr = entry.phase ? `/${entry.phase}` : "";
|
|
1591
|
+
console.log(` ${ts} ${icon} ${entry.type.padEnd(12)} ${entry.protocol}${phaseStr} [${entry.status}]`);
|
|
1592
|
+
}
|
|
1593
|
+
}
|
|
1594
|
+
}
|
|
1595
|
+
var dashboardCommand = defineCommand6({
|
|
1596
|
+
meta: {
|
|
1597
|
+
name: "dashboard",
|
|
1598
|
+
description: "Show observability dashboard with cost, performance, gates, and agent metrics"
|
|
1599
|
+
},
|
|
1600
|
+
args: {
|
|
1601
|
+
protocol: {
|
|
1602
|
+
type: "string",
|
|
1603
|
+
description: "Filter by protocol name",
|
|
1604
|
+
required: false
|
|
1605
|
+
},
|
|
1606
|
+
json: {
|
|
1607
|
+
type: "boolean",
|
|
1608
|
+
description: "Output structured JSON instead of formatted text",
|
|
1609
|
+
required: false
|
|
1610
|
+
}
|
|
1611
|
+
},
|
|
1612
|
+
run: async ({ args }) => {
|
|
1613
|
+
const cwd = process.cwd();
|
|
1614
|
+
if (!await sniperConfigExists(cwd)) {
|
|
1615
|
+
p6.log.error('SNIPER is not initialized. Run "sniper init" first.');
|
|
1616
|
+
process.exit(1);
|
|
1617
|
+
}
|
|
1618
|
+
const config = await readConfig(cwd);
|
|
1619
|
+
const sniperDir = join8(cwd, ".sniper");
|
|
1620
|
+
const checkpoints = await readYamlDir(join8(sniperDir, "checkpoints"));
|
|
1621
|
+
const gates = await readYamlDir(join8(sniperDir, "gates"));
|
|
1622
|
+
let velocity = null;
|
|
1623
|
+
const velocityPath = join8(sniperDir, "memory", "velocity.yaml");
|
|
1624
|
+
if (await pathExists(velocityPath)) {
|
|
1625
|
+
try {
|
|
1626
|
+
const raw = await readFile7(velocityPath, "utf-8");
|
|
1627
|
+
velocity = YAML6.parse(raw);
|
|
1628
|
+
} catch {
|
|
1629
|
+
}
|
|
1630
|
+
}
|
|
1631
|
+
const protocolFilter = args.protocol || void 0;
|
|
1632
|
+
const data = aggregateData(checkpoints, gates, velocity, protocolFilter);
|
|
1633
|
+
if (args.json) {
|
|
1634
|
+
console.log(JSON.stringify(data, null, 2));
|
|
1635
|
+
return;
|
|
1636
|
+
}
|
|
1637
|
+
const title = protocolFilter ? `SNIPER Dashboard \u2014 ${protocolFilter}` : "SNIPER Dashboard";
|
|
1638
|
+
p6.intro(title);
|
|
1639
|
+
if (checkpoints.length === 0 && gates.length === 0 && !velocity) {
|
|
1640
|
+
p6.log.info("No observability data found yet. Run a protocol to generate metrics.");
|
|
1641
|
+
p6.outro("");
|
|
1642
|
+
return;
|
|
1643
|
+
}
|
|
1644
|
+
renderDashboard(data, config);
|
|
1645
|
+
p6.outro("");
|
|
1646
|
+
}
|
|
1647
|
+
});
|
|
1648
|
+
|
|
1649
|
+
// src/commands/workspace.ts
|
|
1650
|
+
import { defineCommand as defineCommand7 } from "citty";
|
|
1651
|
+
import * as p7 from "@clack/prompts";
|
|
1652
|
+
|
|
1653
|
+
// src/workspace-manager.ts
|
|
1654
|
+
import { readFile as readFile8, writeFile as writeFile5, access as access6, mkdir as mkdir5 } from "fs/promises";
|
|
1655
|
+
import { join as join9, resolve as resolve3, dirname as dirname2 } from "path";
|
|
1656
|
+
import YAML7 from "yaml";
|
|
1657
|
+
var WORKSPACE_DIR = ".sniper-workspace";
|
|
1658
|
+
var WORKSPACE_CONFIG = "config.yaml";
|
|
1659
|
+
async function pathExists3(p14) {
|
|
1660
|
+
try {
|
|
1661
|
+
await access6(p14);
|
|
1662
|
+
return true;
|
|
1663
|
+
} catch {
|
|
1664
|
+
return false;
|
|
1665
|
+
}
|
|
1666
|
+
}
|
|
1667
|
+
async function findWorkspaceRoot(cwd) {
|
|
1668
|
+
let dir = resolve3(cwd);
|
|
1669
|
+
while (true) {
|
|
1670
|
+
const configPath = join9(dir, WORKSPACE_DIR, WORKSPACE_CONFIG);
|
|
1671
|
+
if (await pathExists3(configPath)) {
|
|
1672
|
+
return dir;
|
|
1673
|
+
}
|
|
1674
|
+
const parent = dirname2(dir);
|
|
1675
|
+
if (parent === dir) break;
|
|
1676
|
+
dir = parent;
|
|
1677
|
+
}
|
|
1678
|
+
return null;
|
|
1679
|
+
}
|
|
1680
|
+
async function readWorkspaceConfig(workspaceRoot) {
|
|
1681
|
+
const configPath = join9(workspaceRoot, WORKSPACE_DIR, WORKSPACE_CONFIG);
|
|
1682
|
+
const raw = await readFile8(configPath, "utf-8");
|
|
1683
|
+
const data = YAML7.parse(raw);
|
|
1684
|
+
if (!data || typeof data !== "object") {
|
|
1685
|
+
throw new Error("Invalid workspace config: expected an object");
|
|
1686
|
+
}
|
|
1687
|
+
if (!data.name || typeof data.name !== "string") {
|
|
1688
|
+
throw new Error('Invalid workspace config: missing "name"');
|
|
1689
|
+
}
|
|
1690
|
+
if (!Array.isArray(data.projects)) {
|
|
1691
|
+
throw new Error('Invalid workspace config: missing "projects" array');
|
|
1692
|
+
}
|
|
1693
|
+
return data;
|
|
1694
|
+
}
|
|
1695
|
+
async function addProject(workspaceRoot, name, path) {
|
|
1696
|
+
if (path.includes("..")) {
|
|
1697
|
+
throw new Error(`Project path must not contain '..': ${path}`);
|
|
1698
|
+
}
|
|
1699
|
+
const config = await readWorkspaceConfig(workspaceRoot);
|
|
1700
|
+
if (config.projects.some((p14) => p14.name === name)) {
|
|
1701
|
+
throw new Error(`Project "${name}" already exists in workspace`);
|
|
1702
|
+
}
|
|
1703
|
+
config.projects.push({ name, path });
|
|
1704
|
+
const configPath = join9(workspaceRoot, WORKSPACE_DIR, WORKSPACE_CONFIG);
|
|
1705
|
+
await writeFile5(configPath, YAML7.stringify(config, { lineWidth: 0 }), "utf-8");
|
|
1706
|
+
}
|
|
1707
|
+
async function syncConventions(workspaceRoot) {
|
|
1708
|
+
const config = await readWorkspaceConfig(workspaceRoot);
|
|
1709
|
+
const candidates = [];
|
|
1710
|
+
for (const project of config.projects) {
|
|
1711
|
+
const projectConfigPath = join9(
|
|
1712
|
+
workspaceRoot,
|
|
1713
|
+
project.path,
|
|
1714
|
+
".sniper",
|
|
1715
|
+
"config.yaml"
|
|
1716
|
+
);
|
|
1717
|
+
if (await pathExists3(projectConfigPath)) {
|
|
1718
|
+
candidates.push(project.name);
|
|
1719
|
+
}
|
|
1720
|
+
}
|
|
1721
|
+
return candidates;
|
|
1722
|
+
}
|
|
1723
|
+
async function initWorkspace(cwd, name) {
|
|
1724
|
+
const wsDir = join9(cwd, WORKSPACE_DIR);
|
|
1725
|
+
const memoryDir = join9(wsDir, "memory");
|
|
1726
|
+
const locksDir2 = join9(wsDir, "locks");
|
|
1727
|
+
await mkdir5(wsDir, { recursive: true });
|
|
1728
|
+
await mkdir5(memoryDir, { recursive: true });
|
|
1729
|
+
await mkdir5(locksDir2, { recursive: true });
|
|
1730
|
+
const config = {
|
|
1731
|
+
name,
|
|
1732
|
+
projects: [],
|
|
1733
|
+
shared: {
|
|
1734
|
+
conventions: [],
|
|
1735
|
+
anti_patterns: [],
|
|
1736
|
+
architectural_decisions: []
|
|
1737
|
+
},
|
|
1738
|
+
memory: {
|
|
1739
|
+
directory: `${WORKSPACE_DIR}/memory`
|
|
1740
|
+
}
|
|
1741
|
+
};
|
|
1742
|
+
const configPath = join9(wsDir, WORKSPACE_CONFIG);
|
|
1743
|
+
await writeFile5(configPath, YAML7.stringify(config, { lineWidth: 0 }), "utf-8");
|
|
1744
|
+
return wsDir;
|
|
1745
|
+
}
|
|
1746
|
+
|
|
1747
|
+
// src/commands/workspace.ts
|
|
1748
|
+
var initSubcommand = defineCommand7({
|
|
1749
|
+
meta: {
|
|
1750
|
+
name: "init",
|
|
1751
|
+
description: "Initialize a SNIPER workspace for multi-project orchestration"
|
|
1752
|
+
},
|
|
1753
|
+
args: {
|
|
1754
|
+
name: {
|
|
1755
|
+
type: "string",
|
|
1756
|
+
description: "Workspace name",
|
|
1757
|
+
required: false
|
|
1758
|
+
}
|
|
1759
|
+
},
|
|
1760
|
+
run: async ({ args }) => {
|
|
1761
|
+
const cwd = process.cwd();
|
|
1762
|
+
p7.intro("SNIPER Workspace \u2014 Initialization");
|
|
1763
|
+
const existing = await findWorkspaceRoot(cwd);
|
|
1764
|
+
if (existing) {
|
|
1765
|
+
p7.log.warning(`Workspace already exists at ${existing}`);
|
|
1766
|
+
const overwrite = await p7.confirm({
|
|
1767
|
+
message: "Reinitialize workspace?",
|
|
1768
|
+
initialValue: false
|
|
1769
|
+
});
|
|
1770
|
+
if (p7.isCancel(overwrite) || !overwrite) {
|
|
1771
|
+
p7.cancel("Aborted.");
|
|
1772
|
+
process.exit(0);
|
|
1773
|
+
}
|
|
1774
|
+
}
|
|
1775
|
+
let name = args.name;
|
|
1776
|
+
if (!name) {
|
|
1777
|
+
const input = await p7.text({
|
|
1778
|
+
message: "Workspace name:",
|
|
1779
|
+
placeholder: "my-workspace",
|
|
1780
|
+
validate: (v) => v.length === 0 ? "Name is required" : void 0
|
|
1781
|
+
});
|
|
1782
|
+
if (p7.isCancel(input)) {
|
|
1783
|
+
p7.cancel("Aborted.");
|
|
1784
|
+
process.exit(0);
|
|
1785
|
+
}
|
|
1786
|
+
name = input;
|
|
1787
|
+
}
|
|
1788
|
+
const s = p7.spinner();
|
|
1789
|
+
s.start("Creating workspace...");
|
|
1790
|
+
try {
|
|
1791
|
+
const wsDir = await initWorkspace(cwd, name);
|
|
1792
|
+
s.stop("Done!");
|
|
1793
|
+
p7.log.success(`Workspace "${name}" created at ${wsDir}`);
|
|
1794
|
+
p7.log.info("Created: config.yaml, memory/, locks/");
|
|
1795
|
+
p7.outro('Add projects with "sniper workspace add <name> --path <dir>"');
|
|
1796
|
+
} catch (err) {
|
|
1797
|
+
s.stop("Failed!");
|
|
1798
|
+
p7.log.error(`Workspace init failed: ${err}`);
|
|
1799
|
+
process.exit(1);
|
|
1800
|
+
}
|
|
1801
|
+
}
|
|
1802
|
+
});
|
|
1803
|
+
var addSubcommand = defineCommand7({
|
|
1804
|
+
meta: {
|
|
1805
|
+
name: "add",
|
|
1806
|
+
description: "Add a project to the workspace"
|
|
1807
|
+
},
|
|
1808
|
+
args: {
|
|
1809
|
+
name: {
|
|
1810
|
+
type: "positional",
|
|
1811
|
+
description: "Project name",
|
|
1812
|
+
required: true
|
|
1813
|
+
},
|
|
1814
|
+
path: {
|
|
1815
|
+
type: "string",
|
|
1816
|
+
description: "Relative path to the project directory",
|
|
1817
|
+
required: true
|
|
1818
|
+
}
|
|
1819
|
+
},
|
|
1820
|
+
run: async ({ args }) => {
|
|
1821
|
+
const cwd = process.cwd();
|
|
1822
|
+
const wsRoot = await findWorkspaceRoot(cwd);
|
|
1823
|
+
if (!wsRoot) {
|
|
1824
|
+
p7.log.error('No workspace found. Run "sniper workspace init" first.');
|
|
1825
|
+
process.exit(1);
|
|
1826
|
+
}
|
|
1827
|
+
try {
|
|
1828
|
+
await addProject(wsRoot, args.name, args.path);
|
|
1829
|
+
p7.log.success(`Added project "${args.name}" (${args.path}) to workspace.`);
|
|
1830
|
+
} catch (err) {
|
|
1831
|
+
p7.log.error(`Failed to add project: ${err}`);
|
|
1832
|
+
process.exit(1);
|
|
1833
|
+
}
|
|
1834
|
+
}
|
|
1835
|
+
});
|
|
1836
|
+
var statusSubcommand = defineCommand7({
|
|
1837
|
+
meta: {
|
|
1838
|
+
name: "status",
|
|
1839
|
+
description: "Show workspace status"
|
|
1840
|
+
},
|
|
1841
|
+
run: async () => {
|
|
1842
|
+
const cwd = process.cwd();
|
|
1843
|
+
const wsRoot = await findWorkspaceRoot(cwd);
|
|
1844
|
+
if (!wsRoot) {
|
|
1845
|
+
p7.log.error('No workspace found. Run "sniper workspace init" first.');
|
|
1846
|
+
process.exit(1);
|
|
1847
|
+
}
|
|
1848
|
+
const config = await readWorkspaceConfig(wsRoot);
|
|
1849
|
+
p7.intro(`Workspace: ${config.name}`);
|
|
1850
|
+
p7.log.step("Projects:");
|
|
1851
|
+
if (config.projects.length === 0) {
|
|
1852
|
+
console.log(" (none)");
|
|
1853
|
+
} else {
|
|
1854
|
+
for (const proj of config.projects) {
|
|
1855
|
+
const typeLabel = proj.type ? ` (${proj.type})` : "";
|
|
1856
|
+
console.log(` - ${proj.name}: ${proj.path}${typeLabel}`);
|
|
1857
|
+
}
|
|
1858
|
+
}
|
|
1859
|
+
const conventions = config.shared?.conventions ?? [];
|
|
1860
|
+
p7.log.info(`Shared conventions: ${conventions.length}`);
|
|
1861
|
+
const antiPatterns = config.shared?.anti_patterns ?? [];
|
|
1862
|
+
p7.log.info(`Anti-patterns: ${antiPatterns.length}`);
|
|
1863
|
+
const adrs = config.shared?.architectural_decisions ?? [];
|
|
1864
|
+
p7.log.info(`Architectural decisions: ${adrs.length}`);
|
|
1865
|
+
const memDir = config.memory?.directory;
|
|
1866
|
+
p7.log.info(`Memory: ${memDir ? memDir : "not configured"}`);
|
|
1867
|
+
p7.outro("");
|
|
1868
|
+
}
|
|
1869
|
+
});
|
|
1870
|
+
var syncSubcommand = defineCommand7({
|
|
1871
|
+
meta: {
|
|
1872
|
+
name: "sync",
|
|
1873
|
+
description: "Sync shared conventions to workspace projects"
|
|
1874
|
+
},
|
|
1875
|
+
run: async () => {
|
|
1876
|
+
const cwd = process.cwd();
|
|
1877
|
+
const wsRoot = await findWorkspaceRoot(cwd);
|
|
1878
|
+
if (!wsRoot) {
|
|
1879
|
+
p7.log.error('No workspace found. Run "sniper workspace init" first.');
|
|
1880
|
+
process.exit(1);
|
|
1881
|
+
}
|
|
1882
|
+
const s = p7.spinner();
|
|
1883
|
+
s.start("Syncing conventions...");
|
|
1884
|
+
try {
|
|
1885
|
+
const synced = await syncConventions(wsRoot);
|
|
1886
|
+
s.stop("Done!");
|
|
1887
|
+
if (synced.length === 0) {
|
|
1888
|
+
p7.log.info("No projects with .sniper/config.yaml found to sync.");
|
|
1889
|
+
} else {
|
|
1890
|
+
for (const name of synced) {
|
|
1891
|
+
p7.log.success(`Checked: ${name}`);
|
|
1892
|
+
}
|
|
1893
|
+
}
|
|
1894
|
+
} catch (err) {
|
|
1895
|
+
s.stop("Failed!");
|
|
1896
|
+
p7.log.error(`Sync failed: ${err}`);
|
|
1897
|
+
process.exit(1);
|
|
1898
|
+
}
|
|
1899
|
+
}
|
|
1900
|
+
});
|
|
1901
|
+
var workspaceCommand = defineCommand7({
|
|
1902
|
+
meta: {
|
|
1903
|
+
name: "workspace",
|
|
1904
|
+
description: "Manage SNIPER workspaces for multi-project orchestration"
|
|
1905
|
+
},
|
|
1906
|
+
subCommands: {
|
|
1907
|
+
init: initSubcommand,
|
|
1908
|
+
add: addSubcommand,
|
|
1909
|
+
status: statusSubcommand,
|
|
1910
|
+
sync: syncSubcommand
|
|
1911
|
+
}
|
|
1912
|
+
});
|
|
1913
|
+
|
|
1914
|
+
// src/commands/revert.ts
|
|
1915
|
+
import { defineCommand as defineCommand8 } from "citty";
|
|
1916
|
+
import * as p8 from "@clack/prompts";
|
|
1917
|
+
import { readFile as readFile9, readdir as readdir5 } from "fs/promises";
|
|
1918
|
+
import { join as join10 } from "path";
|
|
1919
|
+
import { execFileSync as execFileSync2 } from "child_process";
|
|
1920
|
+
import YAML8 from "yaml";
|
|
1921
|
+
function isValidSha(sha) {
|
|
1922
|
+
return /^[0-9a-f]{7,40}$/i.test(sha);
|
|
1923
|
+
}
|
|
1924
|
+
var revertCommand = defineCommand8({
|
|
1925
|
+
meta: {
|
|
1926
|
+
name: "revert",
|
|
1927
|
+
description: "Logically revert a SNIPER protocol, phase, or checkpoint"
|
|
1928
|
+
},
|
|
1929
|
+
args: {
|
|
1930
|
+
protocol: {
|
|
1931
|
+
type: "string",
|
|
1932
|
+
description: "Protocol ID to revert"
|
|
1933
|
+
},
|
|
1934
|
+
phase: {
|
|
1935
|
+
type: "string",
|
|
1936
|
+
description: "Specific phase to revert"
|
|
1937
|
+
},
|
|
1938
|
+
checkpoint: {
|
|
1939
|
+
type: "string",
|
|
1940
|
+
description: "Specific checkpoint file to revert"
|
|
1941
|
+
},
|
|
1942
|
+
"dry-run": {
|
|
1943
|
+
type: "boolean",
|
|
1944
|
+
description: "Show what would be reverted without doing it",
|
|
1945
|
+
default: false
|
|
1946
|
+
},
|
|
1947
|
+
yes: {
|
|
1948
|
+
type: "boolean",
|
|
1949
|
+
description: "Skip confirmation prompt",
|
|
1950
|
+
default: false
|
|
1951
|
+
}
|
|
1952
|
+
},
|
|
1953
|
+
run: async ({ args }) => {
|
|
1954
|
+
const cwd = process.cwd();
|
|
1955
|
+
if (!await sniperConfigExists(cwd)) {
|
|
1956
|
+
p8.log.error(
|
|
1957
|
+
'SNIPER is not initialized. Run "sniper init" first.'
|
|
1958
|
+
);
|
|
1959
|
+
process.exit(1);
|
|
1960
|
+
}
|
|
1961
|
+
p8.intro("SNIPER v3 \u2014 Logical Revert");
|
|
1962
|
+
const checkpointsDir = join10(cwd, ".sniper", "checkpoints");
|
|
1963
|
+
if (!await pathExists(checkpointsDir)) {
|
|
1964
|
+
p8.log.error("No checkpoints found. Nothing to revert.");
|
|
1965
|
+
process.exit(1);
|
|
1966
|
+
}
|
|
1967
|
+
const files = await readdir5(checkpointsDir);
|
|
1968
|
+
const yamlFiles = files.filter(
|
|
1969
|
+
(f) => f.endsWith(".yaml") || f.endsWith(".yml")
|
|
1970
|
+
);
|
|
1971
|
+
if (yamlFiles.length === 0) {
|
|
1972
|
+
p8.log.error("No checkpoint files found. Nothing to revert.");
|
|
1973
|
+
process.exit(1);
|
|
1974
|
+
}
|
|
1975
|
+
const checkpoints = [];
|
|
1976
|
+
for (const file of yamlFiles) {
|
|
1977
|
+
const raw = await readFile9(join10(checkpointsDir, file), "utf-8");
|
|
1978
|
+
const data = YAML8.parse(raw);
|
|
1979
|
+
if (data) {
|
|
1980
|
+
checkpoints.push({ filename: file, data });
|
|
1981
|
+
}
|
|
1982
|
+
}
|
|
1983
|
+
let filtered = checkpoints;
|
|
1984
|
+
if (args.checkpoint) {
|
|
1985
|
+
filtered = filtered.filter((c) => c.filename === args.checkpoint);
|
|
1986
|
+
}
|
|
1987
|
+
if (args.protocol) {
|
|
1988
|
+
filtered = filtered.filter((c) => c.data.protocol === args.protocol);
|
|
1989
|
+
}
|
|
1990
|
+
if (args.phase) {
|
|
1991
|
+
filtered = filtered.filter((c) => c.data.phase === args.phase);
|
|
1992
|
+
}
|
|
1993
|
+
if (filtered.length === 0) {
|
|
1994
|
+
p8.log.error("No matching checkpoints found for the given filters.");
|
|
1995
|
+
process.exit(1);
|
|
1996
|
+
}
|
|
1997
|
+
const commits = [];
|
|
1998
|
+
for (const cp3 of filtered) {
|
|
1999
|
+
if (Array.isArray(cp3.data.commits)) {
|
|
2000
|
+
for (const commit of cp3.data.commits) {
|
|
2001
|
+
if (!commits.some((c) => c.sha === commit.sha)) {
|
|
2002
|
+
commits.push(commit);
|
|
2003
|
+
}
|
|
2004
|
+
}
|
|
735
2005
|
}
|
|
736
2006
|
}
|
|
737
|
-
if (
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
2007
|
+
if (commits.length === 0) {
|
|
2008
|
+
p8.log.error(
|
|
2009
|
+
"No commits found in matching checkpoints. Nothing to revert."
|
|
2010
|
+
);
|
|
2011
|
+
process.exit(1);
|
|
2012
|
+
}
|
|
2013
|
+
const protocolLabel = args.protocol || filtered[0].data.protocol;
|
|
2014
|
+
const phaseLabel = args.phase || "(all phases)";
|
|
2015
|
+
p8.log.step(
|
|
2016
|
+
`Revert plan: ${commits.length} commit(s) from protocol "${protocolLabel}" ${args.phase ? `phase "${phaseLabel}"` : ""}`
|
|
2017
|
+
);
|
|
2018
|
+
for (const commit of commits) {
|
|
2019
|
+
console.log(` ${commit.sha.substring(0, 8)} ${commit.message} (${commit.agent})`);
|
|
2020
|
+
}
|
|
2021
|
+
if (args["dry-run"]) {
|
|
2022
|
+
p8.log.info("Dry run complete. No changes were made.");
|
|
2023
|
+
p8.outro("");
|
|
2024
|
+
return;
|
|
2025
|
+
}
|
|
2026
|
+
if (!args.yes) {
|
|
2027
|
+
const confirmed = await p8.confirm({
|
|
2028
|
+
message: `Revert ${commits.length} commit(s)? A backup branch will be created.`,
|
|
2029
|
+
initialValue: false
|
|
2030
|
+
});
|
|
2031
|
+
if (p8.isCancel(confirmed) || !confirmed) {
|
|
2032
|
+
p8.cancel("Revert aborted.");
|
|
2033
|
+
process.exit(0);
|
|
2034
|
+
}
|
|
2035
|
+
}
|
|
2036
|
+
const timestamp = Date.now();
|
|
2037
|
+
const backupBranch = `sniper-revert-backup-${timestamp}`;
|
|
2038
|
+
try {
|
|
2039
|
+
execFileSync2("git", ["branch", backupBranch], { cwd });
|
|
2040
|
+
p8.log.success(`Created backup branch: ${backupBranch}`);
|
|
2041
|
+
} catch (err) {
|
|
2042
|
+
p8.log.error(`Failed to create backup branch: ${err}`);
|
|
2043
|
+
process.exit(1);
|
|
2044
|
+
}
|
|
2045
|
+
const s = p8.spinner();
|
|
2046
|
+
s.start("Reverting commits...");
|
|
2047
|
+
try {
|
|
2048
|
+
for (const commit of commits) {
|
|
2049
|
+
if (!isValidSha(commit.sha)) {
|
|
2050
|
+
throw new Error(`Invalid commit SHA: ${commit.sha}`);
|
|
743
2051
|
}
|
|
744
|
-
|
|
745
|
-
p5.log.info("\nNo packs installed.");
|
|
2052
|
+
execFileSync2("git", ["revert", "--no-commit", commit.sha], { cwd });
|
|
746
2053
|
}
|
|
2054
|
+
const revertMessage = `revert: undo ${commits.length} commit(s) from protocol "${protocolLabel}"${args.phase ? ` phase "${phaseLabel}"` : ""}
|
|
2055
|
+
|
|
2056
|
+
Reverted commits:
|
|
2057
|
+
${commits.map((c) => ` - ${c.sha.substring(0, 8)} ${c.message}`).join("\n")}
|
|
2058
|
+
|
|
2059
|
+
Backup branch: ${backupBranch}`;
|
|
2060
|
+
execFileSync2("git", ["commit", "-m", revertMessage], { cwd });
|
|
2061
|
+
s.stop("Revert complete!");
|
|
2062
|
+
p8.log.success(
|
|
2063
|
+
`Successfully reverted ${commits.length} commit(s). Backup branch: ${backupBranch}`
|
|
2064
|
+
);
|
|
2065
|
+
p8.outro("");
|
|
2066
|
+
} catch (err) {
|
|
2067
|
+
s.stop("Revert failed!");
|
|
2068
|
+
p8.log.error(`Revert failed: ${err}`);
|
|
2069
|
+
p8.log.info(
|
|
2070
|
+
`Your backup branch "${backupBranch}" is intact. You can run "git revert --abort" to undo the partial revert.`
|
|
2071
|
+
);
|
|
2072
|
+
process.exit(1);
|
|
747
2073
|
}
|
|
748
|
-
p5.outro("");
|
|
749
2074
|
}
|
|
750
2075
|
});
|
|
751
2076
|
|
|
752
|
-
// src/commands/
|
|
753
|
-
import { defineCommand as
|
|
754
|
-
import * as
|
|
755
|
-
|
|
2077
|
+
// src/commands/run.ts
|
|
2078
|
+
import { defineCommand as defineCommand9 } from "citty";
|
|
2079
|
+
import * as p9 from "@clack/prompts";
|
|
2080
|
+
|
|
2081
|
+
// src/headless.ts
|
|
2082
|
+
import { readFile as readFile10 } from "fs/promises";
|
|
2083
|
+
import { join as join11 } from "path";
|
|
2084
|
+
import YAML9 from "yaml";
|
|
2085
|
+
var BUILT_IN_PROTOCOLS = [
|
|
2086
|
+
"full",
|
|
2087
|
+
"feature",
|
|
2088
|
+
"patch",
|
|
2089
|
+
"ingest",
|
|
2090
|
+
"explore",
|
|
2091
|
+
"refactor",
|
|
2092
|
+
"hotfix"
|
|
2093
|
+
];
|
|
2094
|
+
var HeadlessRunner = class {
|
|
2095
|
+
cwd;
|
|
2096
|
+
options;
|
|
2097
|
+
constructor(cwd, options) {
|
|
2098
|
+
this.cwd = cwd;
|
|
2099
|
+
this.options = options;
|
|
2100
|
+
}
|
|
2101
|
+
async run() {
|
|
2102
|
+
const startTime = Date.now();
|
|
2103
|
+
const errors = [];
|
|
2104
|
+
let config;
|
|
2105
|
+
try {
|
|
2106
|
+
config = await readConfig(this.cwd);
|
|
2107
|
+
} catch (err) {
|
|
2108
|
+
return {
|
|
2109
|
+
exitCode: 4 /* ConfigError */,
|
|
2110
|
+
protocol: this.options.protocol,
|
|
2111
|
+
phases: [],
|
|
2112
|
+
totalTokens: 0,
|
|
2113
|
+
duration: Date.now() - startTime,
|
|
2114
|
+
errors: [
|
|
2115
|
+
`Config error: ${err instanceof Error ? err.message : String(err)}`
|
|
2116
|
+
]
|
|
2117
|
+
};
|
|
2118
|
+
}
|
|
2119
|
+
if (!/^[a-z][a-z0-9-]*$/.test(this.options.protocol)) {
|
|
2120
|
+
return {
|
|
2121
|
+
exitCode: 4 /* ConfigError */,
|
|
2122
|
+
protocol: this.options.protocol,
|
|
2123
|
+
phases: [],
|
|
2124
|
+
totalTokens: 0,
|
|
2125
|
+
duration: Date.now() - startTime,
|
|
2126
|
+
errors: [
|
|
2127
|
+
`Invalid protocol name: "${this.options.protocol}". Must be lowercase alphanumeric with hyphens.`
|
|
2128
|
+
]
|
|
2129
|
+
};
|
|
2130
|
+
}
|
|
2131
|
+
const isBuiltIn = BUILT_IN_PROTOCOLS.includes(this.options.protocol);
|
|
2132
|
+
let isCustom = false;
|
|
2133
|
+
if (!isBuiltIn) {
|
|
2134
|
+
try {
|
|
2135
|
+
const customPath = join11(
|
|
2136
|
+
this.cwd,
|
|
2137
|
+
".sniper",
|
|
2138
|
+
"protocols",
|
|
2139
|
+
`${this.options.protocol}.yaml`
|
|
2140
|
+
);
|
|
2141
|
+
await readFile10(customPath, "utf-8");
|
|
2142
|
+
isCustom = true;
|
|
2143
|
+
} catch {
|
|
2144
|
+
}
|
|
2145
|
+
}
|
|
2146
|
+
if (!isBuiltIn && !isCustom) {
|
|
2147
|
+
return {
|
|
2148
|
+
exitCode: 4 /* ConfigError */,
|
|
2149
|
+
protocol: this.options.protocol,
|
|
2150
|
+
phases: [],
|
|
2151
|
+
totalTokens: 0,
|
|
2152
|
+
duration: Date.now() - startTime,
|
|
2153
|
+
errors: [
|
|
2154
|
+
`Unknown protocol: "${this.options.protocol}". Available: ${BUILT_IN_PROTOCOLS.join(", ")} (or define a custom protocol in .sniper/protocols/)`
|
|
2155
|
+
]
|
|
2156
|
+
};
|
|
2157
|
+
}
|
|
2158
|
+
return {
|
|
2159
|
+
exitCode: 4 /* ConfigError */,
|
|
2160
|
+
protocol: this.options.protocol,
|
|
2161
|
+
phases: [],
|
|
2162
|
+
totalTokens: 0,
|
|
2163
|
+
duration: Date.now() - startTime,
|
|
2164
|
+
errors: [
|
|
2165
|
+
"Headless mode is not yet implemented. Protocol validation passed, but no execution occurred. Use /sniper-flow interactively instead."
|
|
2166
|
+
]
|
|
2167
|
+
};
|
|
2168
|
+
}
|
|
2169
|
+
formatOutput(result) {
|
|
2170
|
+
switch (this.options.outputFormat) {
|
|
2171
|
+
case "json":
|
|
2172
|
+
return JSON.stringify(
|
|
2173
|
+
{
|
|
2174
|
+
protocol: result.protocol,
|
|
2175
|
+
status: exitCodeToStatus(result.exitCode),
|
|
2176
|
+
phases: result.phases,
|
|
2177
|
+
total_tokens: result.totalTokens,
|
|
2178
|
+
duration_seconds: Math.round(result.duration / 1e3),
|
|
2179
|
+
errors: result.errors
|
|
2180
|
+
},
|
|
2181
|
+
null,
|
|
2182
|
+
2
|
|
2183
|
+
);
|
|
2184
|
+
case "yaml":
|
|
2185
|
+
return YAML9.stringify({
|
|
2186
|
+
protocol: result.protocol,
|
|
2187
|
+
status: exitCodeToStatus(result.exitCode),
|
|
2188
|
+
phases: result.phases,
|
|
2189
|
+
total_tokens: result.totalTokens,
|
|
2190
|
+
duration_seconds: Math.round(result.duration / 1e3),
|
|
2191
|
+
errors: result.errors
|
|
2192
|
+
});
|
|
2193
|
+
case "text":
|
|
2194
|
+
return formatTextTable(result);
|
|
2195
|
+
default:
|
|
2196
|
+
return JSON.stringify(
|
|
2197
|
+
{
|
|
2198
|
+
protocol: result.protocol,
|
|
2199
|
+
status: exitCodeToStatus(result.exitCode),
|
|
2200
|
+
phases: result.phases,
|
|
2201
|
+
total_tokens: result.totalTokens,
|
|
2202
|
+
duration_seconds: Math.round(result.duration / 1e3),
|
|
2203
|
+
errors: result.errors
|
|
2204
|
+
},
|
|
2205
|
+
null,
|
|
2206
|
+
2
|
|
2207
|
+
);
|
|
2208
|
+
}
|
|
2209
|
+
}
|
|
2210
|
+
};
|
|
2211
|
+
function exitCodeToStatus(code) {
|
|
2212
|
+
switch (code) {
|
|
2213
|
+
case 0 /* Success */:
|
|
2214
|
+
return "success";
|
|
2215
|
+
case 1 /* GateFail */:
|
|
2216
|
+
return "gate_fail";
|
|
2217
|
+
case 2 /* CostExceeded */:
|
|
2218
|
+
return "cost_exceeded";
|
|
2219
|
+
case 3 /* Timeout */:
|
|
2220
|
+
return "timeout";
|
|
2221
|
+
case 4 /* ConfigError */:
|
|
2222
|
+
return "config_error";
|
|
2223
|
+
}
|
|
2224
|
+
}
|
|
2225
|
+
function formatTextTable(result) {
|
|
2226
|
+
const lines = [];
|
|
2227
|
+
const status = exitCodeToStatus(result.exitCode);
|
|
2228
|
+
lines.push(`Protocol: ${result.protocol}`);
|
|
2229
|
+
lines.push(`Status: ${status}`);
|
|
2230
|
+
lines.push(`Duration: ${Math.round(result.duration / 1e3)}s`);
|
|
2231
|
+
lines.push(`Tokens: ${result.totalTokens}`);
|
|
2232
|
+
if (result.phases.length > 0) {
|
|
2233
|
+
lines.push("");
|
|
2234
|
+
lines.push("Phase Status Gate Tokens");
|
|
2235
|
+
lines.push("---------------- ----------- ---------------- ------");
|
|
2236
|
+
for (const phase of result.phases) {
|
|
2237
|
+
const name = phase.name.padEnd(16);
|
|
2238
|
+
const phaseStatus = phase.status.padEnd(11);
|
|
2239
|
+
const gate = (phase.gate_result ?? "-").padEnd(16);
|
|
2240
|
+
lines.push(`${name} ${phaseStatus} ${gate} ${phase.tokens}`);
|
|
2241
|
+
}
|
|
2242
|
+
}
|
|
2243
|
+
if (result.errors.length > 0) {
|
|
2244
|
+
lines.push("");
|
|
2245
|
+
lines.push("Errors:");
|
|
2246
|
+
for (const err of result.errors) {
|
|
2247
|
+
lines.push(` - ${err}`);
|
|
2248
|
+
}
|
|
2249
|
+
}
|
|
2250
|
+
return lines.join("\n");
|
|
2251
|
+
}
|
|
2252
|
+
|
|
2253
|
+
// src/commands/run.ts
|
|
2254
|
+
var runCommand = defineCommand9({
|
|
756
2255
|
meta: {
|
|
757
|
-
name: "
|
|
758
|
-
description: "
|
|
2256
|
+
name: "run",
|
|
2257
|
+
description: "Run a SNIPER protocol in headless mode (for CI/CD)"
|
|
759
2258
|
},
|
|
760
|
-
|
|
2259
|
+
args: {
|
|
2260
|
+
protocol: {
|
|
2261
|
+
type: "string",
|
|
2262
|
+
description: "Protocol to run (full, feature, patch, ingest, explore, refactor, hotfix)",
|
|
2263
|
+
required: true
|
|
2264
|
+
},
|
|
2265
|
+
ci: {
|
|
2266
|
+
type: "boolean",
|
|
2267
|
+
description: "CI mode: sets auto-approve, json output, warn-level logging",
|
|
2268
|
+
default: false
|
|
2269
|
+
},
|
|
2270
|
+
"auto-approve": {
|
|
2271
|
+
type: "boolean",
|
|
2272
|
+
description: "Auto-approve all gates",
|
|
2273
|
+
default: false
|
|
2274
|
+
},
|
|
2275
|
+
output: {
|
|
2276
|
+
type: "string",
|
|
2277
|
+
description: "Output format: json, yaml, text",
|
|
2278
|
+
default: "text"
|
|
2279
|
+
},
|
|
2280
|
+
timeout: {
|
|
2281
|
+
type: "string",
|
|
2282
|
+
description: "Timeout in minutes",
|
|
2283
|
+
default: "60"
|
|
2284
|
+
}
|
|
2285
|
+
},
|
|
2286
|
+
run: async ({ args }) => {
|
|
761
2287
|
const cwd = process.cwd();
|
|
762
2288
|
if (!await sniperConfigExists(cwd)) {
|
|
763
|
-
|
|
764
|
-
|
|
2289
|
+
p9.log.error('SNIPER is not initialized. Run "sniper init" first.');
|
|
2290
|
+
process.exit(4 /* ConfigError */);
|
|
2291
|
+
}
|
|
2292
|
+
const validFormats = ["json", "yaml", "text"];
|
|
2293
|
+
const outputFormat = args.ci ? "json" : args.output ?? "text";
|
|
2294
|
+
if (!validFormats.includes(outputFormat)) {
|
|
2295
|
+
p9.log.error(
|
|
2296
|
+
`Invalid output format: "${outputFormat}". Use: ${validFormats.join(", ")}`
|
|
2297
|
+
);
|
|
2298
|
+
process.exit(4 /* ConfigError */);
|
|
2299
|
+
}
|
|
2300
|
+
const options = {
|
|
2301
|
+
protocol: args.protocol,
|
|
2302
|
+
autoApproveGates: args.ci || args["auto-approve"] || false,
|
|
2303
|
+
outputFormat,
|
|
2304
|
+
logLevel: args.ci ? "warn" : "info",
|
|
2305
|
+
timeoutMinutes: parseInt(args.timeout, 10) || 60,
|
|
2306
|
+
failOnGateFailure: true
|
|
2307
|
+
};
|
|
2308
|
+
const runner = new HeadlessRunner(cwd, options);
|
|
2309
|
+
const result = await runner.run();
|
|
2310
|
+
const output = runner.formatOutput(result);
|
|
2311
|
+
if (result.exitCode === 0 /* Success */) {
|
|
2312
|
+
process.stdout.write(output + "\n");
|
|
2313
|
+
} else {
|
|
2314
|
+
if (result.errors.length > 0) {
|
|
2315
|
+
for (const err of result.errors) {
|
|
2316
|
+
process.stderr.write(`error: ${err}
|
|
2317
|
+
`);
|
|
2318
|
+
}
|
|
2319
|
+
}
|
|
2320
|
+
process.stdout.write(output + "\n");
|
|
2321
|
+
}
|
|
2322
|
+
process.exit(result.exitCode);
|
|
2323
|
+
}
|
|
2324
|
+
});
|
|
2325
|
+
|
|
2326
|
+
// src/commands/marketplace.ts
|
|
2327
|
+
import { defineCommand as defineCommand10 } from "citty";
|
|
2328
|
+
import * as p10 from "@clack/prompts";
|
|
2329
|
+
|
|
2330
|
+
// src/marketplace-client.ts
|
|
2331
|
+
import { readFile as readFile11 } from "fs/promises";
|
|
2332
|
+
import { join as join12 } from "path";
|
|
2333
|
+
function inferSniperType(sniper) {
|
|
2334
|
+
if (!sniper?.type) return null;
|
|
2335
|
+
const t = sniper.type;
|
|
2336
|
+
if (t === "plugin" || t === "agent" || t === "mixin" || t === "pack") {
|
|
2337
|
+
return t;
|
|
2338
|
+
}
|
|
2339
|
+
return null;
|
|
2340
|
+
}
|
|
2341
|
+
async function searchPackages(query, limit) {
|
|
2342
|
+
const size = limit || 20;
|
|
2343
|
+
const url = `https://registry.npmjs.org/-/v1/search?text=${encodeURIComponent(query)}+keywords:sniper&size=${size}`;
|
|
2344
|
+
const resp = await fetch(url);
|
|
2345
|
+
if (!resp.ok) {
|
|
2346
|
+
throw new Error(`npm registry search failed: ${resp.status}`);
|
|
2347
|
+
}
|
|
2348
|
+
const data = await resp.json();
|
|
2349
|
+
const results = await Promise.all(
|
|
2350
|
+
data.objects.map((obj) => getPackageInfo(obj.package.name))
|
|
2351
|
+
);
|
|
2352
|
+
const packages = results.filter(
|
|
2353
|
+
(info) => info !== null
|
|
2354
|
+
);
|
|
2355
|
+
return { packages, total: packages.length };
|
|
2356
|
+
}
|
|
2357
|
+
async function getPackageInfo(name) {
|
|
2358
|
+
const url = `https://registry.npmjs.org/${encodeURIComponent(name)}`;
|
|
2359
|
+
const resp = await fetch(url);
|
|
2360
|
+
if (!resp.ok) {
|
|
2361
|
+
if (resp.status === 404) return null;
|
|
2362
|
+
throw new Error(`npm registry fetch failed: ${resp.status}`);
|
|
2363
|
+
}
|
|
2364
|
+
const data = await resp.json();
|
|
2365
|
+
const latest = data["dist-tags"]?.latest;
|
|
2366
|
+
if (!latest || !data.versions?.[latest]) return null;
|
|
2367
|
+
const version2 = data.versions[latest];
|
|
2368
|
+
const sniperType = inferSniperType(version2.sniper);
|
|
2369
|
+
if (!sniperType) return null;
|
|
2370
|
+
return {
|
|
2371
|
+
name: data.name,
|
|
2372
|
+
version: latest,
|
|
2373
|
+
description: version2.description || "",
|
|
2374
|
+
sniperType,
|
|
2375
|
+
tags: version2.keywords || [],
|
|
2376
|
+
author: version2.author?.name
|
|
2377
|
+
};
|
|
2378
|
+
}
|
|
2379
|
+
async function validatePublishable(cwd) {
|
|
2380
|
+
const errors = [];
|
|
2381
|
+
let pkgJson;
|
|
2382
|
+
try {
|
|
2383
|
+
const raw = await readFile11(join12(cwd, "package.json"), "utf-8");
|
|
2384
|
+
pkgJson = JSON.parse(raw);
|
|
2385
|
+
} catch {
|
|
2386
|
+
return { valid: false, errors: ["No package.json found in current directory"] };
|
|
2387
|
+
}
|
|
2388
|
+
if (!pkgJson.name) {
|
|
2389
|
+
errors.push("package.json is missing a 'name' field");
|
|
2390
|
+
} else if (!pkgJson.name.startsWith("@sniper.ai/") && !pkgJson.name.startsWith("sniper-")) {
|
|
2391
|
+
errors.push(
|
|
2392
|
+
`Package name "${pkgJson.name}" must start with @sniper.ai/ or sniper-`
|
|
2393
|
+
);
|
|
2394
|
+
}
|
|
2395
|
+
if (!pkgJson.sniper?.type) {
|
|
2396
|
+
errors.push("package.json is missing 'sniper.type' field");
|
|
2397
|
+
}
|
|
2398
|
+
if (pkgJson.sniper?.type === "plugin") {
|
|
2399
|
+
try {
|
|
2400
|
+
await readFile11(join12(cwd, "plugin.yaml"), "utf-8");
|
|
2401
|
+
} catch {
|
|
2402
|
+
errors.push("Plugins require a plugin.yaml file in the package root");
|
|
2403
|
+
}
|
|
2404
|
+
}
|
|
2405
|
+
return { valid: errors.length === 0, errors };
|
|
2406
|
+
}
|
|
2407
|
+
|
|
2408
|
+
// src/commands/marketplace.ts
|
|
2409
|
+
var searchSubcommand = defineCommand10({
|
|
2410
|
+
meta: {
|
|
2411
|
+
name: "search",
|
|
2412
|
+
description: "Search the SNIPER marketplace for packages"
|
|
2413
|
+
},
|
|
2414
|
+
args: {
|
|
2415
|
+
query: {
|
|
2416
|
+
type: "positional",
|
|
2417
|
+
description: "Search query",
|
|
2418
|
+
required: true
|
|
2419
|
+
}
|
|
2420
|
+
},
|
|
2421
|
+
run: async ({ args }) => {
|
|
2422
|
+
const s = p10.spinner();
|
|
2423
|
+
s.start("Searching marketplace...");
|
|
2424
|
+
try {
|
|
2425
|
+
const result = await searchPackages(args.query);
|
|
2426
|
+
s.stop("Done!");
|
|
2427
|
+
if (result.packages.length === 0) {
|
|
2428
|
+
p10.log.info("No packages found.");
|
|
2429
|
+
return;
|
|
2430
|
+
}
|
|
2431
|
+
p10.log.step(`Found ${result.total} result(s):`);
|
|
2432
|
+
console.log(
|
|
2433
|
+
` ${"Name".padEnd(35)} ${"Version".padEnd(10)} ${"Type".padEnd(8)} Description`
|
|
765
2434
|
);
|
|
2435
|
+
console.log(` ${"\u2500".repeat(35)} ${"\u2500".repeat(10)} ${"\u2500".repeat(8)} ${"\u2500".repeat(30)}`);
|
|
2436
|
+
for (const pkg of result.packages) {
|
|
2437
|
+
const desc = pkg.description.length > 40 ? pkg.description.slice(0, 37) + "..." : pkg.description;
|
|
2438
|
+
console.log(
|
|
2439
|
+
` ${pkg.name.padEnd(35)} ${pkg.version.padEnd(10)} ${pkg.sniperType.padEnd(8)} ${desc}`
|
|
2440
|
+
);
|
|
2441
|
+
}
|
|
2442
|
+
} catch (err) {
|
|
2443
|
+
s.stop("Failed!");
|
|
2444
|
+
p10.log.error(`Search failed: ${err}`);
|
|
766
2445
|
process.exit(1);
|
|
767
2446
|
}
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
2447
|
+
}
|
|
2448
|
+
});
|
|
2449
|
+
var installSubcommand2 = defineCommand10({
|
|
2450
|
+
meta: {
|
|
2451
|
+
name: "install",
|
|
2452
|
+
description: "Install a package from the SNIPER marketplace"
|
|
2453
|
+
},
|
|
2454
|
+
args: {
|
|
2455
|
+
package: {
|
|
2456
|
+
type: "positional",
|
|
2457
|
+
description: "Package name to install",
|
|
2458
|
+
required: true
|
|
2459
|
+
}
|
|
2460
|
+
},
|
|
2461
|
+
run: async ({ args }) => {
|
|
2462
|
+
const cwd = process.cwd();
|
|
2463
|
+
if (!await sniperConfigExists(cwd)) {
|
|
2464
|
+
p10.log.error('SNIPER is not initialized. Run "sniper init" first.');
|
|
2465
|
+
process.exit(1);
|
|
2466
|
+
}
|
|
2467
|
+
const s = p10.spinner();
|
|
2468
|
+
s.start(`Checking ${args.package}...`);
|
|
2469
|
+
try {
|
|
2470
|
+
const info = await getPackageInfo(args.package);
|
|
2471
|
+
if (!info) {
|
|
2472
|
+
s.stop("Failed!");
|
|
2473
|
+
p10.log.error(
|
|
2474
|
+
`${args.package} is not a valid SNIPER package (not found or missing sniper metadata).`
|
|
2475
|
+
);
|
|
2476
|
+
process.exit(1);
|
|
2477
|
+
}
|
|
2478
|
+
s.message(`Installing ${args.package}...`);
|
|
2479
|
+
const result = await installPlugin(args.package, cwd);
|
|
2480
|
+
s.stop("Done!");
|
|
2481
|
+
p10.log.success(
|
|
2482
|
+
`Installed ${info.sniperType}: ${result.name} v${result.version}`
|
|
2483
|
+
);
|
|
2484
|
+
} catch (err) {
|
|
2485
|
+
s.stop("Failed!");
|
|
2486
|
+
p10.log.error(`Installation failed: ${err}`);
|
|
2487
|
+
process.exit(1);
|
|
2488
|
+
}
|
|
2489
|
+
}
|
|
2490
|
+
});
|
|
2491
|
+
var infoSubcommand = defineCommand10({
|
|
2492
|
+
meta: {
|
|
2493
|
+
name: "info",
|
|
2494
|
+
description: "Show details about a SNIPER marketplace package"
|
|
2495
|
+
},
|
|
2496
|
+
args: {
|
|
2497
|
+
package: {
|
|
2498
|
+
type: "positional",
|
|
2499
|
+
description: "Package name to inspect",
|
|
2500
|
+
required: true
|
|
2501
|
+
}
|
|
2502
|
+
},
|
|
2503
|
+
run: async ({ args }) => {
|
|
2504
|
+
const s = p10.spinner();
|
|
2505
|
+
s.start(`Fetching info for ${args.package}...`);
|
|
2506
|
+
try {
|
|
2507
|
+
const info = await getPackageInfo(args.package);
|
|
2508
|
+
s.stop("Done!");
|
|
2509
|
+
if (!info) {
|
|
2510
|
+
p10.log.error(
|
|
2511
|
+
`${args.package} not found or is not a SNIPER package.`
|
|
2512
|
+
);
|
|
2513
|
+
process.exit(1);
|
|
2514
|
+
}
|
|
2515
|
+
p10.log.step(`Package: ${info.name}`);
|
|
2516
|
+
console.log(` Version: ${info.version}`);
|
|
2517
|
+
console.log(` Type: ${info.sniperType}`);
|
|
2518
|
+
console.log(` Description: ${info.description || "(none)"}`);
|
|
2519
|
+
console.log(` Tags: ${info.tags.length > 0 ? info.tags.join(", ") : "(none)"}`);
|
|
2520
|
+
console.log(` Author: ${info.author || "(unknown)"}`);
|
|
2521
|
+
} catch (err) {
|
|
2522
|
+
s.stop("Failed!");
|
|
2523
|
+
p10.log.error(`Fetch failed: ${err}`);
|
|
2524
|
+
process.exit(1);
|
|
776
2525
|
}
|
|
777
|
-
|
|
778
|
-
|
|
2526
|
+
}
|
|
2527
|
+
});
|
|
2528
|
+
var publishSubcommand = defineCommand10({
|
|
2529
|
+
meta: {
|
|
2530
|
+
name: "publish",
|
|
2531
|
+
description: "Validate and guide publishing a SNIPER package"
|
|
2532
|
+
},
|
|
2533
|
+
run: async () => {
|
|
2534
|
+
const cwd = process.cwd();
|
|
2535
|
+
const s = p10.spinner();
|
|
2536
|
+
s.start("Validating package...");
|
|
779
2537
|
try {
|
|
780
|
-
const
|
|
2538
|
+
const result = await validatePublishable(cwd);
|
|
781
2539
|
s.stop("Done!");
|
|
782
|
-
|
|
783
|
-
|
|
2540
|
+
if (!result.valid) {
|
|
2541
|
+
p10.log.error("Package is not publishable:");
|
|
2542
|
+
for (const err of result.errors) {
|
|
2543
|
+
console.log(` - ${err}`);
|
|
2544
|
+
}
|
|
2545
|
+
process.exit(1);
|
|
784
2546
|
}
|
|
785
|
-
|
|
2547
|
+
p10.log.success("Package is valid and ready to publish.");
|
|
2548
|
+
p10.log.info('Run "npm publish" to publish your package to the marketplace.');
|
|
786
2549
|
} catch (err) {
|
|
787
2550
|
s.stop("Failed!");
|
|
788
|
-
|
|
2551
|
+
p10.log.error(`Validation failed: ${err}`);
|
|
789
2552
|
process.exit(1);
|
|
790
2553
|
}
|
|
791
2554
|
}
|
|
792
2555
|
});
|
|
2556
|
+
var marketplaceCommand = defineCommand10({
|
|
2557
|
+
meta: {
|
|
2558
|
+
name: "marketplace",
|
|
2559
|
+
description: "Browse and manage SNIPER marketplace packages"
|
|
2560
|
+
},
|
|
2561
|
+
subCommands: {
|
|
2562
|
+
search: searchSubcommand,
|
|
2563
|
+
install: installSubcommand2,
|
|
2564
|
+
info: infoSubcommand,
|
|
2565
|
+
publish: publishSubcommand
|
|
2566
|
+
}
|
|
2567
|
+
});
|
|
793
2568
|
|
|
794
|
-
// src/commands/
|
|
795
|
-
import {
|
|
796
|
-
import
|
|
797
|
-
import { defineCommand as defineCommand7 } from "citty";
|
|
798
|
-
import * as p7 from "@clack/prompts";
|
|
799
|
-
import YAML4 from "yaml";
|
|
2569
|
+
// src/commands/signal.ts
|
|
2570
|
+
import { defineCommand as defineCommand11 } from "citty";
|
|
2571
|
+
import * as p11 from "@clack/prompts";
|
|
800
2572
|
|
|
801
|
-
// src/
|
|
802
|
-
import {
|
|
803
|
-
|
|
804
|
-
|
|
2573
|
+
// src/signal-collector.ts
|
|
2574
|
+
import { readFile as readFile12, writeFile as writeFile6, readdir as readdir6, rm as rm2 } from "fs/promises";
|
|
2575
|
+
import { join as join13 } from "path";
|
|
2576
|
+
import { execFileSync as execFileSync3 } from "child_process";
|
|
2577
|
+
import YAML10 from "yaml";
|
|
2578
|
+
var SIGNAL_DIR = ".sniper/memory/signals";
|
|
2579
|
+
function getSignalDir(cwd) {
|
|
2580
|
+
return join13(cwd, SIGNAL_DIR);
|
|
2581
|
+
}
|
|
2582
|
+
function signalFilename(signal) {
|
|
2583
|
+
const date = new Date(signal.timestamp);
|
|
2584
|
+
const dateStr = date.toISOString().slice(0, 10).replace(/-/g, "");
|
|
2585
|
+
const ts = Math.floor(date.getTime() / 1e3);
|
|
2586
|
+
return `${dateStr}-${signal.type}-${ts}.yaml`;
|
|
805
2587
|
}
|
|
806
|
-
async function
|
|
2588
|
+
async function ingestSignal(cwd, signal) {
|
|
2589
|
+
const dir = getSignalDir(cwd);
|
|
2590
|
+
await ensureDir2(dir);
|
|
2591
|
+
const filename = signalFilename(signal);
|
|
2592
|
+
const filepath = join13(dir, filename);
|
|
2593
|
+
const content = YAML10.stringify(signal, { lineWidth: 0 });
|
|
2594
|
+
await writeFile6(filepath, content, "utf-8");
|
|
2595
|
+
return filename;
|
|
2596
|
+
}
|
|
2597
|
+
function assertGhAvailable() {
|
|
807
2598
|
try {
|
|
808
|
-
|
|
809
|
-
return true;
|
|
2599
|
+
execFileSync3("gh", ["--version"], { stdio: "pipe" });
|
|
810
2600
|
} catch {
|
|
811
|
-
|
|
2601
|
+
throw new Error(
|
|
2602
|
+
"GitHub CLI (gh) is not installed or not on PATH. Install it from https://cli.github.com/"
|
|
2603
|
+
);
|
|
812
2604
|
}
|
|
813
2605
|
}
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
2606
|
+
async function ingestFromPR(cwd, prNumber) {
|
|
2607
|
+
assertGhAvailable();
|
|
2608
|
+
const raw = execFileSync3("gh", [
|
|
2609
|
+
"pr",
|
|
2610
|
+
"view",
|
|
2611
|
+
prNumber.toString(),
|
|
2612
|
+
"--json",
|
|
2613
|
+
"comments,reviews,title"
|
|
2614
|
+
], { cwd, encoding: "utf-8" });
|
|
2615
|
+
const prData = JSON.parse(raw);
|
|
2616
|
+
const signals = [];
|
|
2617
|
+
for (const review of prData.reviews) {
|
|
2618
|
+
if (!review.body) continue;
|
|
2619
|
+
const signal = {
|
|
2620
|
+
type: "pr_review_comment",
|
|
2621
|
+
source: `pr-${prNumber}`,
|
|
2622
|
+
timestamp: review.submittedAt,
|
|
2623
|
+
summary: `PR #${prNumber} review (${review.state}) by ${review.author?.login ?? "unknown"}`,
|
|
2624
|
+
details: review.body,
|
|
2625
|
+
relevance_tags: ["pr-review", review.state.toLowerCase()]
|
|
2626
|
+
};
|
|
2627
|
+
const filename = await ingestSignal(cwd, signal);
|
|
2628
|
+
signals.push(signal);
|
|
2629
|
+
}
|
|
2630
|
+
for (const comment of prData.comments) {
|
|
2631
|
+
const signal = {
|
|
2632
|
+
type: "pr_review_comment",
|
|
2633
|
+
source: `pr-${prNumber}`,
|
|
2634
|
+
timestamp: comment.createdAt,
|
|
2635
|
+
summary: `PR #${prNumber} comment by ${comment.author?.login ?? "unknown"}`,
|
|
2636
|
+
details: comment.body,
|
|
2637
|
+
relevance_tags: ["pr-comment"]
|
|
2638
|
+
};
|
|
2639
|
+
await ingestSignal(cwd, signal);
|
|
2640
|
+
signals.push(signal);
|
|
2641
|
+
}
|
|
2642
|
+
return signals;
|
|
2643
|
+
}
|
|
2644
|
+
async function listSignals(cwd, options) {
|
|
2645
|
+
const dir = getSignalDir(cwd);
|
|
2646
|
+
if (!await pathExists(dir)) {
|
|
823
2647
|
return [];
|
|
824
2648
|
}
|
|
2649
|
+
const files = await readdir6(dir);
|
|
2650
|
+
const yamlFiles = files.filter((f) => f.endsWith(".yaml"));
|
|
2651
|
+
const signals = [];
|
|
2652
|
+
for (const file of yamlFiles) {
|
|
2653
|
+
const raw = await readFile12(join13(dir, file), "utf-8");
|
|
2654
|
+
const signal = YAML10.parse(raw);
|
|
2655
|
+
signals.push(signal);
|
|
2656
|
+
}
|
|
2657
|
+
let filtered = signals;
|
|
2658
|
+
if (options?.type) {
|
|
2659
|
+
filtered = filtered.filter((s) => s.type === options.type);
|
|
2660
|
+
}
|
|
2661
|
+
filtered.sort(
|
|
2662
|
+
(a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime()
|
|
2663
|
+
);
|
|
2664
|
+
if (options?.limit) {
|
|
2665
|
+
filtered = filtered.slice(0, options.limit);
|
|
2666
|
+
}
|
|
2667
|
+
return filtered;
|
|
825
2668
|
}
|
|
826
|
-
async function
|
|
827
|
-
const
|
|
828
|
-
await
|
|
2669
|
+
async function clearSignals(cwd) {
|
|
2670
|
+
const dir = getSignalDir(cwd);
|
|
2671
|
+
if (!await pathExists(dir)) {
|
|
2672
|
+
return 0;
|
|
2673
|
+
}
|
|
2674
|
+
const files = await readdir6(dir);
|
|
2675
|
+
const yamlFiles = files.filter((f) => f.endsWith(".yaml") || f.endsWith(".yml"));
|
|
2676
|
+
for (const file of yamlFiles) {
|
|
2677
|
+
await rm2(join13(dir, file));
|
|
2678
|
+
}
|
|
2679
|
+
return yamlFiles.length;
|
|
829
2680
|
}
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
2681
|
+
|
|
2682
|
+
// src/commands/signal.ts
|
|
2683
|
+
var SIGNAL_TYPES = [
|
|
2684
|
+
{ value: "ci_failure", label: "CI Failure" },
|
|
2685
|
+
{ value: "pr_review_comment", label: "PR Review Comment" },
|
|
2686
|
+
{ value: "production_error", label: "Production Error" },
|
|
2687
|
+
{ value: "manual", label: "Manual" }
|
|
2688
|
+
];
|
|
2689
|
+
var ingestSubcommand = defineCommand11({
|
|
2690
|
+
meta: {
|
|
2691
|
+
name: "ingest",
|
|
2692
|
+
description: "Interactively create a new signal record"
|
|
2693
|
+
},
|
|
2694
|
+
run: async () => {
|
|
2695
|
+
const cwd = process.cwd();
|
|
2696
|
+
if (!await sniperConfigExists(cwd)) {
|
|
2697
|
+
p11.log.error('SNIPER is not initialized. Run "sniper init" first.');
|
|
2698
|
+
process.exit(1);
|
|
2699
|
+
}
|
|
2700
|
+
const type = await p11.select({
|
|
2701
|
+
message: "Signal type:",
|
|
2702
|
+
options: SIGNAL_TYPES.map((t) => ({ value: t.value, label: t.label }))
|
|
2703
|
+
});
|
|
2704
|
+
if (p11.isCancel(type)) {
|
|
2705
|
+
p11.cancel("Cancelled.");
|
|
2706
|
+
process.exit(0);
|
|
2707
|
+
}
|
|
2708
|
+
const source = await p11.text({
|
|
2709
|
+
message: "Source (e.g., github-actions, pr-42, datadog):",
|
|
2710
|
+
validate: (v) => v.length === 0 ? "Source is required" : void 0
|
|
2711
|
+
});
|
|
2712
|
+
if (p11.isCancel(source)) {
|
|
2713
|
+
p11.cancel("Cancelled.");
|
|
2714
|
+
process.exit(0);
|
|
2715
|
+
}
|
|
2716
|
+
const summary = await p11.text({
|
|
2717
|
+
message: "Summary (one-line description):",
|
|
2718
|
+
validate: (v) => v.length === 0 ? "Summary is required" : void 0
|
|
2719
|
+
});
|
|
2720
|
+
if (p11.isCancel(summary)) {
|
|
2721
|
+
p11.cancel("Cancelled.");
|
|
2722
|
+
process.exit(0);
|
|
2723
|
+
}
|
|
2724
|
+
const details = await p11.text({
|
|
2725
|
+
message: "Details (optional \u2014 full error message or context):"
|
|
2726
|
+
});
|
|
2727
|
+
if (p11.isCancel(details)) {
|
|
2728
|
+
p11.cancel("Cancelled.");
|
|
2729
|
+
process.exit(0);
|
|
2730
|
+
}
|
|
2731
|
+
const filesInput = await p11.text({
|
|
2732
|
+
message: "Affected files (optional \u2014 comma-separated paths):"
|
|
2733
|
+
});
|
|
2734
|
+
if (p11.isCancel(filesInput)) {
|
|
2735
|
+
p11.cancel("Cancelled.");
|
|
2736
|
+
process.exit(0);
|
|
837
2737
|
}
|
|
2738
|
+
const signal = {
|
|
2739
|
+
type,
|
|
2740
|
+
source,
|
|
2741
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2742
|
+
summary
|
|
2743
|
+
};
|
|
2744
|
+
if (details && details.length > 0) {
|
|
2745
|
+
signal.details = details;
|
|
2746
|
+
}
|
|
2747
|
+
if (filesInput && filesInput.length > 0) {
|
|
2748
|
+
signal.affected_files = filesInput.split(",").map((f) => f.trim()).filter((f) => f.length > 0);
|
|
2749
|
+
}
|
|
2750
|
+
await ensureDir2(getSignalDir(cwd));
|
|
2751
|
+
const filename = await ingestSignal(cwd, signal);
|
|
2752
|
+
p11.log.success(`Signal captured: ${filename}`);
|
|
838
2753
|
}
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
var memoryCommand = defineCommand7({
|
|
2754
|
+
});
|
|
2755
|
+
var ingestPrSubcommand = defineCommand11({
|
|
842
2756
|
meta: {
|
|
843
|
-
name: "
|
|
844
|
-
description: "
|
|
2757
|
+
name: "ingest-pr",
|
|
2758
|
+
description: "Ingest signals from a GitHub PR's reviews and comments"
|
|
845
2759
|
},
|
|
846
2760
|
args: {
|
|
847
|
-
|
|
2761
|
+
"pr-number": {
|
|
848
2762
|
type: "positional",
|
|
849
|
-
description: "
|
|
850
|
-
required:
|
|
851
|
-
}
|
|
2763
|
+
description: "Pull request number",
|
|
2764
|
+
required: true
|
|
2765
|
+
}
|
|
2766
|
+
},
|
|
2767
|
+
run: async ({ args }) => {
|
|
2768
|
+
const cwd = process.cwd();
|
|
2769
|
+
if (!await sniperConfigExists(cwd)) {
|
|
2770
|
+
p11.log.error('SNIPER is not initialized. Run "sniper init" first.');
|
|
2771
|
+
process.exit(1);
|
|
2772
|
+
}
|
|
2773
|
+
const prNumber = parseInt(args["pr-number"], 10);
|
|
2774
|
+
if (isNaN(prNumber)) {
|
|
2775
|
+
p11.log.error("Invalid PR number.");
|
|
2776
|
+
process.exit(1);
|
|
2777
|
+
}
|
|
2778
|
+
const s = p11.spinner();
|
|
2779
|
+
s.start(`Ingesting signals from PR #${prNumber}...`);
|
|
2780
|
+
try {
|
|
2781
|
+
const signals = await ingestFromPR(cwd, prNumber);
|
|
2782
|
+
s.stop("Done!");
|
|
2783
|
+
p11.log.success(`Captured ${signals.length} signal(s) from PR #${prNumber}`);
|
|
2784
|
+
for (const signal of signals) {
|
|
2785
|
+
p11.log.info(` - ${signal.summary}`);
|
|
2786
|
+
}
|
|
2787
|
+
} catch (err) {
|
|
2788
|
+
s.stop("Failed!");
|
|
2789
|
+
p11.log.error(`Failed to ingest PR signals: ${err}`);
|
|
2790
|
+
process.exit(1);
|
|
2791
|
+
}
|
|
2792
|
+
}
|
|
2793
|
+
});
|
|
2794
|
+
var listSubcommand3 = defineCommand11({
|
|
2795
|
+
meta: {
|
|
2796
|
+
name: "list",
|
|
2797
|
+
description: "List captured signals"
|
|
2798
|
+
},
|
|
2799
|
+
args: {
|
|
852
2800
|
type: {
|
|
853
|
-
type: "
|
|
854
|
-
description: "
|
|
2801
|
+
type: "string",
|
|
2802
|
+
description: "Filter by signal type",
|
|
855
2803
|
required: false
|
|
856
2804
|
},
|
|
857
|
-
|
|
858
|
-
type: "
|
|
859
|
-
description: "
|
|
860
|
-
required: false
|
|
2805
|
+
limit: {
|
|
2806
|
+
type: "string",
|
|
2807
|
+
description: "Maximum number of signals to display",
|
|
2808
|
+
required: false,
|
|
2809
|
+
default: "20"
|
|
861
2810
|
}
|
|
862
2811
|
},
|
|
863
2812
|
run: async ({ args }) => {
|
|
864
2813
|
const cwd = process.cwd();
|
|
865
2814
|
if (!await sniperConfigExists(cwd)) {
|
|
866
|
-
|
|
867
|
-
'SNIPER is not initialized in this directory. Run "sniper init" first.'
|
|
868
|
-
);
|
|
2815
|
+
p11.log.error('SNIPER is not initialized. Run "sniper init" first.');
|
|
869
2816
|
process.exit(1);
|
|
870
2817
|
}
|
|
871
|
-
const
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
);
|
|
880
|
-
await writeFile3(
|
|
881
|
-
join4(memoryDir, "anti-patterns.yaml"),
|
|
882
|
-
"anti_patterns: []\n",
|
|
883
|
-
"utf-8"
|
|
884
|
-
);
|
|
885
|
-
await writeFile3(
|
|
886
|
-
join4(memoryDir, "decisions.yaml"),
|
|
887
|
-
"decisions: []\n",
|
|
888
|
-
"utf-8"
|
|
889
|
-
);
|
|
890
|
-
await writeFile3(
|
|
891
|
-
join4(memoryDir, "estimates.yaml"),
|
|
892
|
-
"calibration:\n velocity_factor: 1.0\n common_underestimates: []\n last_updated: null\n sprints_analyzed: 0\n",
|
|
893
|
-
"utf-8"
|
|
894
|
-
);
|
|
895
|
-
p7.log.info("Initialized .sniper/memory/ directory");
|
|
2818
|
+
const limit = parseInt(args.limit, 10) || 20;
|
|
2819
|
+
const signals = await listSignals(cwd, {
|
|
2820
|
+
type: args.type,
|
|
2821
|
+
limit
|
|
2822
|
+
});
|
|
2823
|
+
if (signals.length === 0) {
|
|
2824
|
+
p11.log.info("No signals found.");
|
|
2825
|
+
return;
|
|
896
2826
|
}
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
const antiPatterns = await readYamlArray(
|
|
902
|
-
join4(memoryDir, "anti-patterns.yaml"),
|
|
903
|
-
"anti_patterns"
|
|
2827
|
+
p11.log.step(`Signals (${signals.length}):`);
|
|
2828
|
+
console.log();
|
|
2829
|
+
console.log(
|
|
2830
|
+
" " + "Type".padEnd(22) + "Source".padEnd(20) + "Timestamp".padEnd(22) + "Summary"
|
|
904
2831
|
);
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
"
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
const retrosDir = join4(memoryDir, "retros");
|
|
911
|
-
if (await pathExists2(retrosDir)) {
|
|
912
|
-
const files = await readdir3(retrosDir);
|
|
913
|
-
retroCount = files.filter((f) => f.endsWith(".yaml")).length;
|
|
914
|
-
}
|
|
915
|
-
const action = args.action;
|
|
916
|
-
if (!action || action === "list") {
|
|
917
|
-
p7.intro("SNIPER Memory");
|
|
918
|
-
const confirmedConv = conventions.filter(
|
|
919
|
-
(c) => c.status !== "candidate"
|
|
920
|
-
).length;
|
|
921
|
-
const candidateConv = conventions.filter(
|
|
922
|
-
(c) => c.status === "candidate"
|
|
923
|
-
).length;
|
|
924
|
-
const confirmedAp = antiPatterns.filter(
|
|
925
|
-
(a) => a.status !== "candidate"
|
|
926
|
-
).length;
|
|
927
|
-
const candidateAp = antiPatterns.filter(
|
|
928
|
-
(a) => a.status === "candidate"
|
|
929
|
-
).length;
|
|
930
|
-
const activeDecisions = decisions.filter(
|
|
931
|
-
(d) => d.status === "active" || !d.status
|
|
932
|
-
).length;
|
|
933
|
-
const supersededDecisions = decisions.filter(
|
|
934
|
-
(d) => d.status === "superseded"
|
|
935
|
-
).length;
|
|
936
|
-
p7.log.info(
|
|
937
|
-
`Conventions: ${confirmedConv} confirmed, ${candidateConv} candidates`
|
|
938
|
-
);
|
|
939
|
-
p7.log.info(
|
|
940
|
-
`Anti-Patterns: ${confirmedAp} confirmed, ${candidateAp} candidates`
|
|
2832
|
+
console.log(" " + "-".repeat(90));
|
|
2833
|
+
for (const signal of signals) {
|
|
2834
|
+
const ts = new Date(signal.timestamp).toISOString().slice(0, 19).replace("T", " ");
|
|
2835
|
+
console.log(
|
|
2836
|
+
" " + signal.type.padEnd(22) + signal.source.padEnd(20) + ts.padEnd(22) + signal.summary.slice(0, 50)
|
|
941
2837
|
);
|
|
942
|
-
p7.log.info(
|
|
943
|
-
`Decisions: ${activeDecisions} active, ${supersededDecisions} superseded`
|
|
944
|
-
);
|
|
945
|
-
p7.log.info(`Retrospectives: ${retroCount}`);
|
|
946
|
-
const config = await readConfig(cwd);
|
|
947
|
-
if (config.workspace?.enabled && config.workspace.workspace_path) {
|
|
948
|
-
const wsMemory = join4(
|
|
949
|
-
cwd,
|
|
950
|
-
config.workspace.workspace_path,
|
|
951
|
-
"memory"
|
|
952
|
-
);
|
|
953
|
-
if (await pathExists2(wsMemory)) {
|
|
954
|
-
const wsConv = await readYamlArray(
|
|
955
|
-
join4(wsMemory, "conventions.yaml"),
|
|
956
|
-
"conventions"
|
|
957
|
-
);
|
|
958
|
-
const wsAp = await readYamlArray(
|
|
959
|
-
join4(wsMemory, "anti-patterns.yaml"),
|
|
960
|
-
"anti_patterns"
|
|
961
|
-
);
|
|
962
|
-
const wsDec = await readYamlArray(
|
|
963
|
-
join4(wsMemory, "decisions.yaml"),
|
|
964
|
-
"decisions"
|
|
965
|
-
);
|
|
966
|
-
p7.log.step("Workspace Memory:");
|
|
967
|
-
p7.log.info(` Conventions: ${wsConv.length}`);
|
|
968
|
-
p7.log.info(` Anti-Patterns: ${wsAp.length}`);
|
|
969
|
-
p7.log.info(` Decisions: ${wsDec.length}`);
|
|
970
|
-
}
|
|
971
|
-
}
|
|
972
|
-
p7.outro("");
|
|
973
|
-
return;
|
|
974
2838
|
}
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
conventions.push({
|
|
988
|
-
id,
|
|
989
|
-
rule: value,
|
|
990
|
-
rationale: "",
|
|
991
|
-
source: { type: "manual", ref: "user-added", date: today },
|
|
992
|
-
applies_to: [],
|
|
993
|
-
enforcement: "both",
|
|
994
|
-
scope: "project",
|
|
995
|
-
status: "confirmed",
|
|
996
|
-
examples: { positive: "", negative: "" }
|
|
997
|
-
});
|
|
998
|
-
await writeYamlArray(
|
|
999
|
-
join4(memoryDir, "conventions.yaml"),
|
|
1000
|
-
"conventions",
|
|
1001
|
-
conventions
|
|
1002
|
-
);
|
|
1003
|
-
p7.log.success(`Added convention ${id}: ${value}`);
|
|
1004
|
-
} else if (type === "anti-pattern") {
|
|
1005
|
-
const id = nextId(antiPatterns, "ap");
|
|
1006
|
-
antiPatterns.push({
|
|
1007
|
-
id,
|
|
1008
|
-
description: value,
|
|
1009
|
-
why_bad: "",
|
|
1010
|
-
fix_pattern: "",
|
|
1011
|
-
source: { type: "manual", ref: "user-added", date: today },
|
|
1012
|
-
detection_hint: "",
|
|
1013
|
-
applies_to: [],
|
|
1014
|
-
severity: "medium",
|
|
1015
|
-
status: "confirmed"
|
|
1016
|
-
});
|
|
1017
|
-
await writeYamlArray(
|
|
1018
|
-
join4(memoryDir, "anti-patterns.yaml"),
|
|
1019
|
-
"anti_patterns",
|
|
1020
|
-
antiPatterns
|
|
1021
|
-
);
|
|
1022
|
-
p7.log.success(`Added anti-pattern ${id}: ${value}`);
|
|
1023
|
-
} else if (type === "decision") {
|
|
1024
|
-
const id = nextId(decisions, "dec");
|
|
1025
|
-
decisions.push({
|
|
1026
|
-
id,
|
|
1027
|
-
title: value,
|
|
1028
|
-
context: "",
|
|
1029
|
-
decision: value,
|
|
1030
|
-
alternatives_considered: [],
|
|
1031
|
-
source: { type: "manual", ref: "user-added", date: today },
|
|
1032
|
-
applies_to: [],
|
|
1033
|
-
status: "active",
|
|
1034
|
-
superseded_by: null
|
|
1035
|
-
});
|
|
1036
|
-
await writeYamlArray(
|
|
1037
|
-
join4(memoryDir, "decisions.yaml"),
|
|
1038
|
-
"decisions",
|
|
1039
|
-
decisions
|
|
1040
|
-
);
|
|
1041
|
-
p7.log.success(`Added decision ${id}: ${value}`);
|
|
1042
|
-
} else {
|
|
1043
|
-
p7.log.error(
|
|
1044
|
-
`Unknown memory type "${type}". Use: convention, anti-pattern, decision`
|
|
1045
|
-
);
|
|
1046
|
-
process.exit(1);
|
|
1047
|
-
}
|
|
1048
|
-
return;
|
|
2839
|
+
}
|
|
2840
|
+
});
|
|
2841
|
+
var clearSubcommand = defineCommand11({
|
|
2842
|
+
meta: {
|
|
2843
|
+
name: "clear",
|
|
2844
|
+
description: "Delete all captured signals"
|
|
2845
|
+
},
|
|
2846
|
+
run: async () => {
|
|
2847
|
+
const cwd = process.cwd();
|
|
2848
|
+
if (!await sniperConfigExists(cwd)) {
|
|
2849
|
+
p11.log.error('SNIPER is not initialized. Run "sniper init" first.');
|
|
2850
|
+
process.exit(1);
|
|
1049
2851
|
}
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
let found = false;
|
|
1057
|
-
if (id.startsWith("conv-")) {
|
|
1058
|
-
const idx = conventions.findIndex((c) => c.id === id);
|
|
1059
|
-
if (idx >= 0) {
|
|
1060
|
-
conventions.splice(idx, 1);
|
|
1061
|
-
await writeYamlArray(
|
|
1062
|
-
join4(memoryDir, "conventions.yaml"),
|
|
1063
|
-
"conventions",
|
|
1064
|
-
conventions
|
|
1065
|
-
);
|
|
1066
|
-
found = true;
|
|
1067
|
-
}
|
|
1068
|
-
} else if (id.startsWith("ap-")) {
|
|
1069
|
-
const idx = antiPatterns.findIndex((a) => a.id === id);
|
|
1070
|
-
if (idx >= 0) {
|
|
1071
|
-
antiPatterns.splice(idx, 1);
|
|
1072
|
-
await writeYamlArray(
|
|
1073
|
-
join4(memoryDir, "anti-patterns.yaml"),
|
|
1074
|
-
"anti_patterns",
|
|
1075
|
-
antiPatterns
|
|
1076
|
-
);
|
|
1077
|
-
found = true;
|
|
1078
|
-
}
|
|
1079
|
-
} else if (id.startsWith("dec-")) {
|
|
1080
|
-
const idx = decisions.findIndex((d) => d.id === id);
|
|
1081
|
-
if (idx >= 0) {
|
|
1082
|
-
decisions.splice(idx, 1);
|
|
1083
|
-
await writeYamlArray(
|
|
1084
|
-
join4(memoryDir, "decisions.yaml"),
|
|
1085
|
-
"decisions",
|
|
1086
|
-
decisions
|
|
1087
|
-
);
|
|
1088
|
-
found = true;
|
|
1089
|
-
}
|
|
1090
|
-
}
|
|
1091
|
-
if (found) {
|
|
1092
|
-
p7.log.success(`Removed ${id}`);
|
|
1093
|
-
} else {
|
|
1094
|
-
p7.log.error(`Entry ${id} not found in memory.`);
|
|
1095
|
-
process.exit(1);
|
|
1096
|
-
}
|
|
1097
|
-
return;
|
|
2852
|
+
const confirm6 = await p11.confirm({
|
|
2853
|
+
message: "Delete all captured signals? This cannot be undone."
|
|
2854
|
+
});
|
|
2855
|
+
if (p11.isCancel(confirm6) || !confirm6) {
|
|
2856
|
+
p11.cancel("Cancelled.");
|
|
2857
|
+
process.exit(0);
|
|
1098
2858
|
}
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
join4(memoryDir, "decisions.yaml"),
|
|
1134
|
-
"decisions",
|
|
1135
|
-
decisions
|
|
1136
|
-
);
|
|
1137
|
-
found = true;
|
|
1138
|
-
}
|
|
1139
|
-
}
|
|
1140
|
-
if (found) {
|
|
1141
|
-
p7.log.success(`Promoted ${id} to confirmed/active`);
|
|
1142
|
-
} else {
|
|
1143
|
-
p7.log.error(
|
|
1144
|
-
`Entry ${id} not found or is not a candidate.`
|
|
1145
|
-
);
|
|
1146
|
-
process.exit(1);
|
|
1147
|
-
}
|
|
1148
|
-
return;
|
|
2859
|
+
const count = await clearSignals(cwd);
|
|
2860
|
+
p11.log.success(`Cleared ${count} signal(s).`);
|
|
2861
|
+
}
|
|
2862
|
+
});
|
|
2863
|
+
var signalCommand = defineCommand11({
|
|
2864
|
+
meta: {
|
|
2865
|
+
name: "signal",
|
|
2866
|
+
description: "Manage external signal learning"
|
|
2867
|
+
},
|
|
2868
|
+
subCommands: {
|
|
2869
|
+
ingest: ingestSubcommand,
|
|
2870
|
+
"ingest-pr": ingestPrSubcommand,
|
|
2871
|
+
list: listSubcommand3,
|
|
2872
|
+
clear: clearSubcommand
|
|
2873
|
+
}
|
|
2874
|
+
});
|
|
2875
|
+
|
|
2876
|
+
// src/commands/knowledge.ts
|
|
2877
|
+
import { defineCommand as defineCommand12 } from "citty";
|
|
2878
|
+
import * as p12 from "@clack/prompts";
|
|
2879
|
+
import { join as join14 } from "path";
|
|
2880
|
+
import { readFile as readFile13 } from "fs/promises";
|
|
2881
|
+
var KNOWLEDGE_DIR = ".sniper/knowledge";
|
|
2882
|
+
var INDEX_FILENAME = "knowledge-index.json";
|
|
2883
|
+
var indexSubcommand = defineCommand12({
|
|
2884
|
+
meta: {
|
|
2885
|
+
name: "index",
|
|
2886
|
+
description: "Index the SNIPER knowledge base"
|
|
2887
|
+
},
|
|
2888
|
+
run: async () => {
|
|
2889
|
+
const cwd = process.cwd();
|
|
2890
|
+
if (!await sniperConfigExists(cwd)) {
|
|
2891
|
+
p12.log.error('SNIPER is not initialized. Run "sniper init" first.');
|
|
2892
|
+
process.exit(1);
|
|
1149
2893
|
}
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
conventions: conventions.map(({ id: _id, source: _src, ...rest }) => rest),
|
|
1156
|
-
anti_patterns: antiPatterns.map(
|
|
1157
|
-
({ id: _id, source: _src, ...rest }) => rest
|
|
1158
|
-
),
|
|
1159
|
-
decisions: decisions.map(({ id: _id, source: _src, ...rest }) => rest)
|
|
1160
|
-
};
|
|
1161
|
-
const exportPath = join4(cwd, "sniper-memory-export.yaml");
|
|
1162
|
-
await writeFile3(
|
|
1163
|
-
exportPath,
|
|
1164
|
-
YAML4.stringify(exportData, { lineWidth: 0 }),
|
|
1165
|
-
"utf-8"
|
|
2894
|
+
const knowledgeDir = join14(cwd, KNOWLEDGE_DIR);
|
|
2895
|
+
if (!await pathExists(knowledgeDir)) {
|
|
2896
|
+
p12.log.error(
|
|
2897
|
+
`Knowledge directory not found: ${KNOWLEDGE_DIR}
|
|
2898
|
+
Create it and add .md files to index.`
|
|
1166
2899
|
);
|
|
1167
|
-
|
|
1168
|
-
`Exported ${conventions.length} conventions, ${antiPatterns.length} anti-patterns, ${decisions.length} decisions to sniper-memory-export.yaml`
|
|
1169
|
-
);
|
|
1170
|
-
return;
|
|
2900
|
+
process.exit(1);
|
|
1171
2901
|
}
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
(c) => c.rule === conv.rule
|
|
1188
|
-
);
|
|
1189
|
-
if (exists) {
|
|
1190
|
-
skipped++;
|
|
1191
|
-
continue;
|
|
1192
|
-
}
|
|
1193
|
-
conventions.push({
|
|
1194
|
-
...conv,
|
|
1195
|
-
id: nextId(conventions, "conv"),
|
|
1196
|
-
source: { type: "imported", ref: filePath, date: today },
|
|
1197
|
-
status: "candidate"
|
|
1198
|
-
});
|
|
1199
|
-
addedConv++;
|
|
1200
|
-
}
|
|
1201
|
-
await writeYamlArray(
|
|
1202
|
-
join4(memoryDir, "conventions.yaml"),
|
|
1203
|
-
"conventions",
|
|
1204
|
-
conventions
|
|
1205
|
-
);
|
|
1206
|
-
}
|
|
1207
|
-
if (Array.isArray(imported.anti_patterns)) {
|
|
1208
|
-
for (const ap of imported.anti_patterns) {
|
|
1209
|
-
const exists = antiPatterns.some(
|
|
1210
|
-
(a) => a.description === ap.description
|
|
1211
|
-
);
|
|
1212
|
-
if (exists) {
|
|
1213
|
-
skipped++;
|
|
1214
|
-
continue;
|
|
1215
|
-
}
|
|
1216
|
-
antiPatterns.push({
|
|
1217
|
-
...ap,
|
|
1218
|
-
id: nextId(antiPatterns, "ap"),
|
|
1219
|
-
source: { type: "imported", ref: filePath, date: today },
|
|
1220
|
-
status: "candidate"
|
|
1221
|
-
});
|
|
1222
|
-
addedAp++;
|
|
1223
|
-
}
|
|
1224
|
-
await writeYamlArray(
|
|
1225
|
-
join4(memoryDir, "anti-patterns.yaml"),
|
|
1226
|
-
"anti_patterns",
|
|
1227
|
-
antiPatterns
|
|
1228
|
-
);
|
|
1229
|
-
}
|
|
1230
|
-
p7.log.success(
|
|
1231
|
-
`Imported ${addedConv} conventions, ${addedAp} anti-patterns (${skipped} skipped as duplicates)`
|
|
1232
|
-
);
|
|
1233
|
-
return;
|
|
2902
|
+
const s = p12.spinner();
|
|
2903
|
+
s.start("Indexing knowledge base...");
|
|
2904
|
+
try {
|
|
2905
|
+
const { indexKnowledgeDir, writeIndex } = await import("@sniper.ai/mcp-knowledge/indexer");
|
|
2906
|
+
const index = await indexKnowledgeDir(knowledgeDir);
|
|
2907
|
+
const indexPath = join14(knowledgeDir, INDEX_FILENAME);
|
|
2908
|
+
await writeIndex(indexPath, index);
|
|
2909
|
+
s.stop("Done!");
|
|
2910
|
+
p12.log.success(`Indexed ${index.entries.length} entries`);
|
|
2911
|
+
p12.log.info(`Total tokens: ${index.total_tokens.toLocaleString()}`);
|
|
2912
|
+
p12.log.info(`Index written to: ${KNOWLEDGE_DIR}/${INDEX_FILENAME}`);
|
|
2913
|
+
} catch (err) {
|
|
2914
|
+
s.stop("Failed!");
|
|
2915
|
+
p12.log.error(`Indexing failed: ${err}`);
|
|
2916
|
+
process.exit(1);
|
|
1234
2917
|
}
|
|
1235
|
-
p7.log.error(
|
|
1236
|
-
`Unknown action "${action}". Use: list, add, remove, promote, export, import`
|
|
1237
|
-
);
|
|
1238
|
-
process.exit(1);
|
|
1239
2918
|
}
|
|
1240
2919
|
});
|
|
1241
|
-
|
|
1242
|
-
// src/commands/workspace.ts
|
|
1243
|
-
import {
|
|
1244
|
-
readFile as readFile5,
|
|
1245
|
-
writeFile as writeFile4,
|
|
1246
|
-
readdir as readdir4,
|
|
1247
|
-
stat as stat2,
|
|
1248
|
-
symlink
|
|
1249
|
-
} from "fs/promises";
|
|
1250
|
-
import { join as join5, relative, resolve as resolve2 } from "path";
|
|
1251
|
-
import { defineCommand as defineCommand8 } from "citty";
|
|
1252
|
-
import * as p8 from "@clack/prompts";
|
|
1253
|
-
import YAML5 from "yaml";
|
|
1254
|
-
var initSubCommand = defineCommand8({
|
|
2920
|
+
var statusSubcommand2 = defineCommand12({
|
|
1255
2921
|
meta: {
|
|
1256
|
-
name: "
|
|
1257
|
-
description: "
|
|
2922
|
+
name: "status",
|
|
2923
|
+
description: "Show knowledge base status"
|
|
1258
2924
|
},
|
|
1259
2925
|
run: async () => {
|
|
1260
2926
|
const cwd = process.cwd();
|
|
1261
|
-
if (await
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
p8.log.warn(
|
|
1265
|
-
`A workspace already exists: ${ws.name} (${ws.repositories.length} repos)`
|
|
1266
|
-
);
|
|
1267
|
-
p8.log.info("Use /sniper-workspace status to view details.");
|
|
1268
|
-
process.exit(0);
|
|
1269
|
-
}
|
|
1270
|
-
p8.intro("Initialize SNIPER Workspace");
|
|
1271
|
-
const name = await p8.text({
|
|
1272
|
-
message: "Workspace name:",
|
|
1273
|
-
placeholder: "my-saas-platform"
|
|
1274
|
-
});
|
|
1275
|
-
if (p8.isCancel(name)) {
|
|
1276
|
-
p8.cancel("Aborted.");
|
|
1277
|
-
process.exit(0);
|
|
2927
|
+
if (!await sniperConfigExists(cwd)) {
|
|
2928
|
+
p12.log.error('SNIPER is not initialized. Run "sniper init" first.');
|
|
2929
|
+
process.exit(1);
|
|
1278
2930
|
}
|
|
1279
|
-
const
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
process.exit(0);
|
|
2931
|
+
const indexPath = join14(cwd, KNOWLEDGE_DIR, INDEX_FILENAME);
|
|
2932
|
+
if (!await pathExists(indexPath)) {
|
|
2933
|
+
p12.log.warn(
|
|
2934
|
+
'Knowledge base has not been indexed yet. Run "sniper knowledge index" first.'
|
|
2935
|
+
);
|
|
2936
|
+
return;
|
|
1286
2937
|
}
|
|
1287
|
-
const s = p8.spinner();
|
|
1288
|
-
s.start("Scanning for SNIPER-enabled repositories...");
|
|
1289
|
-
const parentDir = resolve2(cwd, "..");
|
|
1290
|
-
const repos = [];
|
|
1291
2938
|
try {
|
|
1292
|
-
const
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
consumes: []
|
|
1311
|
-
});
|
|
1312
|
-
} catch {
|
|
1313
|
-
}
|
|
2939
|
+
const raw = await readFile13(indexPath, "utf-8");
|
|
2940
|
+
const index = JSON.parse(raw);
|
|
2941
|
+
const topics = [...new Set(index.entries.map((e) => e.topic))];
|
|
2942
|
+
p12.log.step("Knowledge Base Status:");
|
|
2943
|
+
console.log(` Entries: ${index.entries.length}`);
|
|
2944
|
+
console.log(` Topics: ${topics.length}`);
|
|
2945
|
+
console.log(
|
|
2946
|
+
` Total tokens: ${index.total_tokens.toLocaleString()}`
|
|
2947
|
+
);
|
|
2948
|
+
console.log(` Last indexed: ${index.indexed_at}`);
|
|
2949
|
+
if (topics.length > 0) {
|
|
2950
|
+
p12.log.step("Topics:");
|
|
2951
|
+
for (const topic of topics.slice(0, 20)) {
|
|
2952
|
+
const count = index.entries.filter((e) => e.topic === topic).length;
|
|
2953
|
+
console.log(` - ${topic} (${count} entries)`);
|
|
2954
|
+
}
|
|
2955
|
+
if (topics.length > 20) {
|
|
2956
|
+
console.log(` ... and ${topics.length - 20} more`);
|
|
1314
2957
|
}
|
|
1315
2958
|
}
|
|
1316
|
-
} catch {
|
|
2959
|
+
} catch (err) {
|
|
2960
|
+
p12.log.error(`Failed to read index: ${err}`);
|
|
2961
|
+
process.exit(1);
|
|
1317
2962
|
}
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
2963
|
+
}
|
|
2964
|
+
});
|
|
2965
|
+
var knowledgeCommand = defineCommand12({
|
|
2966
|
+
meta: {
|
|
2967
|
+
name: "knowledge",
|
|
2968
|
+
description: "Manage SNIPER knowledge base"
|
|
2969
|
+
},
|
|
2970
|
+
subCommands: {
|
|
2971
|
+
index: indexSubcommand,
|
|
2972
|
+
status: statusSubcommand2
|
|
2973
|
+
}
|
|
2974
|
+
});
|
|
2975
|
+
|
|
2976
|
+
// src/commands/sphere.ts
|
|
2977
|
+
import { defineCommand as defineCommand13 } from "citty";
|
|
2978
|
+
import * as p13 from "@clack/prompts";
|
|
2979
|
+
|
|
2980
|
+
// src/conflict-detector.ts
|
|
2981
|
+
import { readFile as readFile14, writeFile as writeFile7, readdir as readdir7, mkdir as mkdir6, rm as rm3 } from "fs/promises";
|
|
2982
|
+
import { join as join15 } from "path";
|
|
2983
|
+
import YAML11 from "yaml";
|
|
2984
|
+
var WORKSPACE_DIR2 = ".sniper-workspace";
|
|
2985
|
+
var LOCKS_DIR = "locks";
|
|
2986
|
+
function encodeLockFilename(file) {
|
|
2987
|
+
return file.replace(/%/g, "%25").replace(/\//g, "%2F").replace(/\\/g, "%5C") + ".yaml";
|
|
2988
|
+
}
|
|
2989
|
+
function locksDir(workspaceRoot) {
|
|
2990
|
+
return join15(workspaceRoot, WORKSPACE_DIR2, LOCKS_DIR);
|
|
2991
|
+
}
|
|
2992
|
+
async function createLock(workspaceRoot, file, project, agent, protocol, reason) {
|
|
2993
|
+
const dir = locksDir(workspaceRoot);
|
|
2994
|
+
await mkdir6(dir, { recursive: true });
|
|
2995
|
+
const lock = {
|
|
2996
|
+
file,
|
|
2997
|
+
locked_by: { project, agent, protocol },
|
|
2998
|
+
since: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2999
|
+
...reason ? { reason } : {}
|
|
3000
|
+
};
|
|
3001
|
+
const lockPath = join15(dir, encodeLockFilename(file));
|
|
3002
|
+
try {
|
|
3003
|
+
await writeFile7(lockPath, YAML11.stringify(lock, { lineWidth: 0 }), {
|
|
3004
|
+
encoding: "utf-8",
|
|
3005
|
+
flag: "wx"
|
|
3006
|
+
});
|
|
3007
|
+
} catch (err) {
|
|
3008
|
+
if (err && typeof err === "object" && "code" in err && err.code === "EEXIST") {
|
|
3009
|
+
throw new Error(
|
|
3010
|
+
`Lock already exists for "${file}". Another agent may be modifying this file.`
|
|
1325
3011
|
);
|
|
1326
|
-
} else {
|
|
1327
|
-
for (const repo of repos) {
|
|
1328
|
-
p8.log.info(` ${repo.name} (${repo.role}, ${repo.language}) ${repo.path}`);
|
|
1329
|
-
}
|
|
1330
3012
|
}
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
shared_domain_packs: [],
|
|
1345
|
-
memory: {
|
|
1346
|
-
workspace_conventions: true,
|
|
1347
|
-
auto_promote: false
|
|
1348
|
-
}
|
|
1349
|
-
},
|
|
1350
|
-
state: {
|
|
1351
|
-
feature_counter: 1,
|
|
1352
|
-
features: []
|
|
1353
|
-
}
|
|
1354
|
-
};
|
|
1355
|
-
await writeFile4(
|
|
1356
|
-
join5(cwd, "workspace.yaml"),
|
|
1357
|
-
YAML5.stringify(workspace, { lineWidth: 0 }),
|
|
1358
|
-
"utf-8"
|
|
1359
|
-
);
|
|
1360
|
-
await ensureDir2(join5(cwd, "memory"));
|
|
1361
|
-
await writeFile4(
|
|
1362
|
-
join5(cwd, "memory", "conventions.yaml"),
|
|
1363
|
-
"conventions: []\n",
|
|
1364
|
-
"utf-8"
|
|
1365
|
-
);
|
|
1366
|
-
await writeFile4(
|
|
1367
|
-
join5(cwd, "memory", "anti-patterns.yaml"),
|
|
1368
|
-
"anti_patterns: []\n",
|
|
1369
|
-
"utf-8"
|
|
1370
|
-
);
|
|
1371
|
-
await writeFile4(
|
|
1372
|
-
join5(cwd, "memory", "decisions.yaml"),
|
|
1373
|
-
"decisions: []\n",
|
|
1374
|
-
"utf-8"
|
|
1375
|
-
);
|
|
1376
|
-
await ensureDir2(join5(cwd, "contracts"));
|
|
1377
|
-
await writeFile4(join5(cwd, "contracts", ".gitkeep"), "", "utf-8");
|
|
1378
|
-
await ensureDir2(join5(cwd, "features"));
|
|
1379
|
-
await writeFile4(join5(cwd, "features", ".gitkeep"), "", "utf-8");
|
|
1380
|
-
if (repos.length > 0) {
|
|
1381
|
-
await ensureDir2(join5(cwd, "repositories"));
|
|
1382
|
-
for (const repo of repos) {
|
|
1383
|
-
const linkPath = join5(cwd, "repositories", repo.name);
|
|
1384
|
-
const targetPath = resolve2(cwd, repo.path);
|
|
1385
|
-
if (!await pathExists2(linkPath)) {
|
|
1386
|
-
try {
|
|
1387
|
-
await symlink(targetPath, linkPath);
|
|
1388
|
-
} catch {
|
|
1389
|
-
}
|
|
1390
|
-
}
|
|
3013
|
+
throw err;
|
|
3014
|
+
}
|
|
3015
|
+
}
|
|
3016
|
+
async function releaseLock(workspaceRoot, file, owner) {
|
|
3017
|
+
const lockPath = join15(locksDir(workspaceRoot), encodeLockFilename(file));
|
|
3018
|
+
if (await pathExists(lockPath)) {
|
|
3019
|
+
if (owner) {
|
|
3020
|
+
const raw = await readFile14(lockPath, "utf-8");
|
|
3021
|
+
const lock = YAML11.parse(raw);
|
|
3022
|
+
if (lock?.locked_by?.agent !== owner && lock?.locked_by?.project !== owner) {
|
|
3023
|
+
throw new Error(
|
|
3024
|
+
`Cannot release lock for "${file}": owned by agent "${lock?.locked_by?.agent}" / project "${lock?.locked_by?.project}", not "${owner}"`
|
|
3025
|
+
);
|
|
1391
3026
|
}
|
|
1392
3027
|
}
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
3028
|
+
await rm3(lockPath);
|
|
3029
|
+
return true;
|
|
3030
|
+
}
|
|
3031
|
+
return false;
|
|
3032
|
+
}
|
|
3033
|
+
async function readLocks(workspaceRoot) {
|
|
3034
|
+
const dir = locksDir(workspaceRoot);
|
|
3035
|
+
if (!await pathExists(dir)) {
|
|
3036
|
+
return [];
|
|
3037
|
+
}
|
|
3038
|
+
const files = await readdir7(dir);
|
|
3039
|
+
const yamlFiles = files.filter(
|
|
3040
|
+
(f) => f.endsWith(".yaml") || f.endsWith(".yml")
|
|
3041
|
+
);
|
|
3042
|
+
const locks = [];
|
|
3043
|
+
for (const file of yamlFiles) {
|
|
3044
|
+
const raw = await readFile14(join15(dir, file), "utf-8");
|
|
3045
|
+
const data = YAML11.parse(raw);
|
|
3046
|
+
if (data && data.file && data.locked_by) {
|
|
3047
|
+
locks.push(data);
|
|
1406
3048
|
}
|
|
1407
|
-
p8.log.success("Workspace initialized!");
|
|
1408
|
-
p8.log.info(` Location: ${cwd}`);
|
|
1409
|
-
p8.log.info(` Repos: ${repos.length}`);
|
|
1410
|
-
p8.log.info("");
|
|
1411
|
-
p8.log.info("Next steps:");
|
|
1412
|
-
p8.log.info(
|
|
1413
|
-
' /sniper-workspace feature "description" \u2014 Plan a cross-repo feature'
|
|
1414
|
-
);
|
|
1415
|
-
p8.log.info(
|
|
1416
|
-
" /sniper-workspace status \u2014 View workspace status"
|
|
1417
|
-
);
|
|
1418
|
-
p8.outro("");
|
|
1419
3049
|
}
|
|
1420
|
-
|
|
1421
|
-
|
|
3050
|
+
return locks;
|
|
3051
|
+
}
|
|
3052
|
+
async function checkConflicts(workspaceRoot, filesToModify, project) {
|
|
3053
|
+
const locks = await readLocks(workspaceRoot);
|
|
3054
|
+
const conflicts = [];
|
|
3055
|
+
const normalizedFiles = filesToModify.map((f) => f.replace(/\\/g, "/"));
|
|
3056
|
+
for (const lock of locks) {
|
|
3057
|
+
const normalizedLockFile = lock.file.replace(/\\/g, "/");
|
|
3058
|
+
if (normalizedFiles.includes(normalizedLockFile) && lock.locked_by.project !== project) {
|
|
3059
|
+
conflicts.push({
|
|
3060
|
+
file: lock.file,
|
|
3061
|
+
held_by: lock.locked_by,
|
|
3062
|
+
requested_by: {
|
|
3063
|
+
project,
|
|
3064
|
+
agent: "unknown",
|
|
3065
|
+
protocol: "unknown"
|
|
3066
|
+
}
|
|
3067
|
+
});
|
|
3068
|
+
}
|
|
3069
|
+
}
|
|
3070
|
+
return conflicts;
|
|
3071
|
+
}
|
|
3072
|
+
|
|
3073
|
+
// src/commands/sphere.ts
|
|
3074
|
+
import { execFileSync as execFileSync4 } from "child_process";
|
|
3075
|
+
import { basename as basename2 } from "path";
|
|
3076
|
+
var WORKSPACE_ERROR = "No workspace found. Sphere 7 requires a workspace. Run `sniper workspace init` first.";
|
|
3077
|
+
var statusSubcommand3 = defineCommand13({
|
|
1422
3078
|
meta: {
|
|
1423
3079
|
name: "status",
|
|
1424
|
-
description: "Show
|
|
3080
|
+
description: "Show active file locks and dependency graph summary"
|
|
1425
3081
|
},
|
|
1426
3082
|
run: async () => {
|
|
1427
3083
|
const cwd = process.cwd();
|
|
1428
|
-
const
|
|
1429
|
-
if (!
|
|
1430
|
-
|
|
1431
|
-
"No workspace found. Run /sniper-workspace init to create one."
|
|
1432
|
-
);
|
|
3084
|
+
const wsRoot = await findWorkspaceRoot(cwd);
|
|
3085
|
+
if (!wsRoot) {
|
|
3086
|
+
p13.log.error(WORKSPACE_ERROR);
|
|
1433
3087
|
process.exit(1);
|
|
1434
3088
|
}
|
|
1435
|
-
|
|
1436
|
-
const
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
for (const repo of ws.repositories) {
|
|
1441
|
-
const repoPath = resolve2(cwd, repo.path);
|
|
1442
|
-
const accessible = await pathExists2(repoPath);
|
|
1443
|
-
const icon = accessible ? "\u2713" : "\u2717";
|
|
1444
|
-
p8.log.info(
|
|
1445
|
-
` ${icon} ${repo.name.padEnd(20)} ${repo.role.padEnd(12)} ${repo.language}`
|
|
1446
|
-
);
|
|
1447
|
-
}
|
|
1448
|
-
const activeFeatures = ws.state.features.filter(
|
|
1449
|
-
(f) => f.phase !== "complete"
|
|
1450
|
-
);
|
|
1451
|
-
if (activeFeatures.length > 0) {
|
|
1452
|
-
p8.log.step("Active Features:");
|
|
1453
|
-
for (const f of activeFeatures) {
|
|
1454
|
-
p8.log.info(
|
|
1455
|
-
` ${f.id} "${f.title}" Phase: ${f.phase}${f.sprint_wave ? ` Wave: ${f.sprint_wave}` : ""}`
|
|
1456
|
-
);
|
|
1457
|
-
}
|
|
3089
|
+
p13.intro("Sphere 7 \u2014 Workspace Status");
|
|
3090
|
+
const locks = await readLocks(wsRoot);
|
|
3091
|
+
p13.log.step(`Active locks: ${locks.length}`);
|
|
3092
|
+
if (locks.length === 0) {
|
|
3093
|
+
console.log(" (none)");
|
|
1458
3094
|
} else {
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
);
|
|
1466
|
-
if (files.length > 0) {
|
|
1467
|
-
p8.log.step("Contracts:");
|
|
1468
|
-
for (const file of files) {
|
|
1469
|
-
try {
|
|
1470
|
-
const cRaw = await readFile5(join5(contractsDir, file), "utf-8");
|
|
1471
|
-
const contract = YAML5.parse(cRaw);
|
|
1472
|
-
const name = contract.contract?.name || file;
|
|
1473
|
-
const version2 = contract.contract?.version || "?";
|
|
1474
|
-
const between = contract.contract?.between?.join(" \u2194 ") || "?";
|
|
1475
|
-
p8.log.info(` ${name} v${version2} ${between}`);
|
|
1476
|
-
} catch {
|
|
1477
|
-
p8.log.info(` ${file} (parse error)`);
|
|
1478
|
-
}
|
|
1479
|
-
}
|
|
1480
|
-
} else {
|
|
1481
|
-
p8.log.step("No contracts defined.");
|
|
1482
|
-
}
|
|
1483
|
-
}
|
|
1484
|
-
const memDir = join5(cwd, "memory");
|
|
1485
|
-
if (await pathExists2(memDir)) {
|
|
1486
|
-
const convFile = join5(memDir, "conventions.yaml");
|
|
1487
|
-
const apFile = join5(memDir, "anti-patterns.yaml");
|
|
1488
|
-
const decFile = join5(memDir, "decisions.yaml");
|
|
1489
|
-
let convCount = 0;
|
|
1490
|
-
let apCount = 0;
|
|
1491
|
-
let decCount = 0;
|
|
1492
|
-
if (await pathExists2(convFile)) {
|
|
1493
|
-
try {
|
|
1494
|
-
const parsed = YAML5.parse(await readFile5(convFile, "utf-8"));
|
|
1495
|
-
convCount = Array.isArray(parsed?.conventions) ? parsed.conventions.length : 0;
|
|
1496
|
-
} catch {
|
|
1497
|
-
}
|
|
1498
|
-
}
|
|
1499
|
-
if (await pathExists2(apFile)) {
|
|
1500
|
-
try {
|
|
1501
|
-
const parsed = YAML5.parse(await readFile5(apFile, "utf-8"));
|
|
1502
|
-
apCount = Array.isArray(parsed?.anti_patterns) ? parsed.anti_patterns.length : 0;
|
|
1503
|
-
} catch {
|
|
1504
|
-
}
|
|
1505
|
-
}
|
|
1506
|
-
if (await pathExists2(decFile)) {
|
|
1507
|
-
try {
|
|
1508
|
-
const parsed = YAML5.parse(await readFile5(decFile, "utf-8"));
|
|
1509
|
-
decCount = Array.isArray(parsed?.decisions) ? parsed.decisions.length : 0;
|
|
1510
|
-
} catch {
|
|
1511
|
-
}
|
|
3095
|
+
for (const lock of locks) {
|
|
3096
|
+
const age = timeSince(lock.since);
|
|
3097
|
+
const reason = lock.reason ? ` \u2014 ${lock.reason}` : "";
|
|
3098
|
+
console.log(
|
|
3099
|
+
` ${lock.file} locked by ${lock.locked_by.project}/${lock.locked_by.agent} (${age})${reason}`
|
|
3100
|
+
);
|
|
1512
3101
|
}
|
|
1513
|
-
p8.log.step("Workspace Memory:");
|
|
1514
|
-
p8.log.info(` Conventions: ${convCount}`);
|
|
1515
|
-
p8.log.info(` Anti-Patterns: ${apCount}`);
|
|
1516
|
-
p8.log.info(` Decisions: ${decCount}`);
|
|
1517
3102
|
}
|
|
1518
|
-
|
|
3103
|
+
p13.outro("");
|
|
1519
3104
|
}
|
|
1520
3105
|
});
|
|
1521
|
-
var
|
|
3106
|
+
var lockSubcommand = defineCommand13({
|
|
1522
3107
|
meta: {
|
|
1523
|
-
name: "
|
|
1524
|
-
description: "
|
|
3108
|
+
name: "lock",
|
|
3109
|
+
description: "Acquire an advisory lock on a file"
|
|
1525
3110
|
},
|
|
1526
3111
|
args: {
|
|
1527
|
-
|
|
3112
|
+
file: {
|
|
1528
3113
|
type: "positional",
|
|
1529
|
-
description: "
|
|
3114
|
+
description: "File path to lock",
|
|
1530
3115
|
required: true
|
|
3116
|
+
},
|
|
3117
|
+
reason: {
|
|
3118
|
+
type: "string",
|
|
3119
|
+
description: "Reason for acquiring the lock",
|
|
3120
|
+
required: false
|
|
1531
3121
|
}
|
|
1532
3122
|
},
|
|
1533
3123
|
run: async ({ args }) => {
|
|
1534
3124
|
const cwd = process.cwd();
|
|
1535
|
-
const
|
|
1536
|
-
if (!
|
|
1537
|
-
|
|
3125
|
+
const wsRoot = await findWorkspaceRoot(cwd);
|
|
3126
|
+
if (!wsRoot) {
|
|
3127
|
+
p13.log.error(WORKSPACE_ERROR);
|
|
1538
3128
|
process.exit(1);
|
|
1539
3129
|
}
|
|
1540
|
-
const
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
3130
|
+
const project = basename2(cwd);
|
|
3131
|
+
try {
|
|
3132
|
+
await createLock(
|
|
3133
|
+
wsRoot,
|
|
3134
|
+
args.file,
|
|
3135
|
+
project,
|
|
3136
|
+
"cli",
|
|
3137
|
+
"manual",
|
|
3138
|
+
args.reason
|
|
1544
3139
|
);
|
|
3140
|
+
p13.log.success(`Locked: ${args.file}`);
|
|
3141
|
+
} catch (err) {
|
|
3142
|
+
p13.log.error(`Failed to acquire lock: ${err}`);
|
|
1545
3143
|
process.exit(1);
|
|
1546
3144
|
}
|
|
1547
|
-
const repoConfig = await readConfig(repoPath);
|
|
1548
|
-
const raw = await readFile5(wsPath, "utf-8");
|
|
1549
|
-
const ws = YAML5.parse(raw);
|
|
1550
|
-
const repoName = repoConfig.project.name;
|
|
1551
|
-
if (ws.repositories.some((r) => r.name === repoName)) {
|
|
1552
|
-
p8.log.warn(`Repository "${repoName}" is already in the workspace.`);
|
|
1553
|
-
process.exit(0);
|
|
1554
|
-
}
|
|
1555
|
-
ws.repositories.push({
|
|
1556
|
-
name: repoName,
|
|
1557
|
-
path: relative(cwd, repoPath),
|
|
1558
|
-
role: inferRole(repoConfig.project.type),
|
|
1559
|
-
language: repoConfig.stack.language,
|
|
1560
|
-
sniper_enabled: true,
|
|
1561
|
-
exposes: [],
|
|
1562
|
-
consumes: []
|
|
1563
|
-
});
|
|
1564
|
-
ws.dependency_graph[repoName] = [];
|
|
1565
|
-
await writeFile4(wsPath, YAML5.stringify(ws, { lineWidth: 0 }), "utf-8");
|
|
1566
|
-
repoConfig.workspace = {
|
|
1567
|
-
enabled: true,
|
|
1568
|
-
workspace_path: relative(repoPath, cwd),
|
|
1569
|
-
repo_name: repoName
|
|
1570
|
-
};
|
|
1571
|
-
await writeConfig(repoPath, repoConfig);
|
|
1572
|
-
p8.log.success(
|
|
1573
|
-
`Added ${repoName} (${repoConfig.project.type}, ${repoConfig.stack.language})`
|
|
1574
|
-
);
|
|
1575
3145
|
}
|
|
1576
3146
|
});
|
|
1577
|
-
var
|
|
3147
|
+
var unlockSubcommand = defineCommand13({
|
|
1578
3148
|
meta: {
|
|
1579
|
-
name: "
|
|
1580
|
-
description: "
|
|
3149
|
+
name: "unlock",
|
|
3150
|
+
description: "Release an advisory lock on a file"
|
|
1581
3151
|
},
|
|
1582
3152
|
args: {
|
|
1583
|
-
|
|
3153
|
+
file: {
|
|
1584
3154
|
type: "positional",
|
|
1585
|
-
description: "
|
|
3155
|
+
description: "File path to unlock",
|
|
1586
3156
|
required: true
|
|
1587
3157
|
}
|
|
1588
3158
|
},
|
|
1589
3159
|
run: async ({ args }) => {
|
|
1590
3160
|
const cwd = process.cwd();
|
|
1591
|
-
const
|
|
1592
|
-
if (!
|
|
1593
|
-
|
|
1594
|
-
process.exit(1);
|
|
1595
|
-
}
|
|
1596
|
-
const raw = await readFile5(wsPath, "utf-8");
|
|
1597
|
-
const ws = YAML5.parse(raw);
|
|
1598
|
-
const repoName = args.name;
|
|
1599
|
-
const idx = ws.repositories.findIndex((r) => r.name === repoName);
|
|
1600
|
-
if (idx < 0) {
|
|
1601
|
-
p8.log.error(`Repository "${repoName}" not found in workspace.`);
|
|
3161
|
+
const wsRoot = await findWorkspaceRoot(cwd);
|
|
3162
|
+
if (!wsRoot) {
|
|
3163
|
+
p13.log.error(WORKSPACE_ERROR);
|
|
1602
3164
|
process.exit(1);
|
|
1603
3165
|
}
|
|
1604
|
-
const
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
if (depIdx >= 0) deps.splice(depIdx, 1);
|
|
1610
|
-
}
|
|
1611
|
-
await writeFile4(wsPath, YAML5.stringify(ws, { lineWidth: 0 }), "utf-8");
|
|
1612
|
-
const repoPath = resolve2(cwd, repo.path);
|
|
1613
|
-
try {
|
|
1614
|
-
const repoConfig = await readConfig(repoPath);
|
|
1615
|
-
repoConfig.workspace = {
|
|
1616
|
-
enabled: false,
|
|
1617
|
-
workspace_path: null,
|
|
1618
|
-
repo_name: null
|
|
1619
|
-
};
|
|
1620
|
-
await writeConfig(repoPath, repoConfig);
|
|
1621
|
-
} catch {
|
|
3166
|
+
const released = await releaseLock(wsRoot, args.file);
|
|
3167
|
+
if (released) {
|
|
3168
|
+
p13.log.success(`Unlocked: ${args.file}`);
|
|
3169
|
+
} else {
|
|
3170
|
+
p13.log.warning(`No lock found for: ${args.file}`);
|
|
1622
3171
|
}
|
|
1623
|
-
p8.log.success(`Removed ${repoName} from workspace.`);
|
|
1624
3172
|
}
|
|
1625
3173
|
});
|
|
1626
|
-
var
|
|
3174
|
+
var conflictsSubcommand = defineCommand13({
|
|
1627
3175
|
meta: {
|
|
1628
|
-
name: "
|
|
1629
|
-
description: "
|
|
3176
|
+
name: "conflicts",
|
|
3177
|
+
description: "Detect file lock conflicts with current changes"
|
|
1630
3178
|
},
|
|
1631
3179
|
run: async () => {
|
|
1632
3180
|
const cwd = process.cwd();
|
|
1633
|
-
const
|
|
1634
|
-
if (!
|
|
1635
|
-
|
|
3181
|
+
const wsRoot = await findWorkspaceRoot(cwd);
|
|
3182
|
+
if (!wsRoot) {
|
|
3183
|
+
p13.log.error(WORKSPACE_ERROR);
|
|
1636
3184
|
process.exit(1);
|
|
1637
3185
|
}
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
3186
|
+
p13.intro("Sphere 7 \u2014 Conflict Detection");
|
|
3187
|
+
let changedFiles;
|
|
3188
|
+
try {
|
|
3189
|
+
const output = execFileSync4("git", ["diff", "--name-only"], {
|
|
3190
|
+
cwd,
|
|
3191
|
+
encoding: "utf-8"
|
|
3192
|
+
});
|
|
3193
|
+
changedFiles = output.split("\n").map((f) => f.trim()).filter(Boolean);
|
|
3194
|
+
} catch {
|
|
3195
|
+
p13.log.error("Failed to get changed files from git.");
|
|
1641
3196
|
process.exit(1);
|
|
1642
3197
|
}
|
|
1643
|
-
|
|
1644
|
-
(
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
p8.log.info("No contracts found. Create them with /sniper-workspace feature.");
|
|
1648
|
-
process.exit(0);
|
|
3198
|
+
if (changedFiles.length === 0) {
|
|
3199
|
+
p13.log.info("No changed files detected.");
|
|
3200
|
+
p13.outro("");
|
|
3201
|
+
return;
|
|
1649
3202
|
}
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
p8.log.info(
|
|
1661
|
-
`${name} v${version2}: ${endpoints} endpoints, ${types} types, ${events} events`
|
|
1662
|
-
);
|
|
1663
|
-
p8.log.info(
|
|
1664
|
-
" (Structural validation requires running /sniper-workspace validate as a slash command)"
|
|
3203
|
+
p13.log.step(`Changed files: ${changedFiles.length}`);
|
|
3204
|
+
const project = basename2(cwd);
|
|
3205
|
+
const conflicts = await checkConflicts(wsRoot, changedFiles, project);
|
|
3206
|
+
if (conflicts.length === 0) {
|
|
3207
|
+
p13.log.success("No conflicts detected.");
|
|
3208
|
+
} else {
|
|
3209
|
+
p13.log.warning(`${conflicts.length} conflict(s) detected:`);
|
|
3210
|
+
for (const conflict of conflicts) {
|
|
3211
|
+
console.log(
|
|
3212
|
+
` ${conflict.file} held by ${conflict.held_by.project}/${conflict.held_by.agent} (protocol: ${conflict.held_by.protocol})`
|
|
1665
3213
|
);
|
|
1666
|
-
} catch {
|
|
1667
|
-
p8.log.warn(` ${file}: parse error`);
|
|
1668
3214
|
}
|
|
1669
3215
|
}
|
|
1670
|
-
|
|
1671
|
-
"\nFull validation (endpoint/type/event checking) runs via the /sniper-workspace validate slash command."
|
|
1672
|
-
);
|
|
1673
|
-
p8.outro("");
|
|
3216
|
+
p13.outro("");
|
|
1674
3217
|
}
|
|
1675
3218
|
});
|
|
1676
|
-
|
|
1677
|
-
switch (projectType) {
|
|
1678
|
-
case "saas":
|
|
1679
|
-
case "web":
|
|
1680
|
-
case "mobile":
|
|
1681
|
-
return "frontend";
|
|
1682
|
-
case "api":
|
|
1683
|
-
return "backend";
|
|
1684
|
-
case "library":
|
|
1685
|
-
return "library";
|
|
1686
|
-
case "cli":
|
|
1687
|
-
case "monorepo":
|
|
1688
|
-
return "service";
|
|
1689
|
-
default:
|
|
1690
|
-
return "service";
|
|
1691
|
-
}
|
|
1692
|
-
}
|
|
1693
|
-
var workspaceCommand = defineCommand8({
|
|
3219
|
+
var sphereCommand = defineCommand13({
|
|
1694
3220
|
meta: {
|
|
1695
|
-
name: "
|
|
1696
|
-
description: "
|
|
3221
|
+
name: "sphere",
|
|
3222
|
+
description: "Sphere 7 \u2014 Cross-human workspace coordination"
|
|
1697
3223
|
},
|
|
1698
3224
|
subCommands: {
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
validate: validateSubCommand
|
|
3225
|
+
status: statusSubcommand3,
|
|
3226
|
+
lock: lockSubcommand,
|
|
3227
|
+
unlock: unlockSubcommand,
|
|
3228
|
+
conflicts: conflictsSubcommand
|
|
1704
3229
|
}
|
|
1705
3230
|
});
|
|
3231
|
+
function timeSince(isoDate) {
|
|
3232
|
+
const ms = Date.now() - new Date(isoDate).getTime();
|
|
3233
|
+
const seconds = Math.floor(ms / 1e3);
|
|
3234
|
+
if (seconds < 60) return `${seconds}s ago`;
|
|
3235
|
+
const minutes = Math.floor(seconds / 60);
|
|
3236
|
+
if (minutes < 60) return `${minutes}m ago`;
|
|
3237
|
+
const hours = Math.floor(minutes / 60);
|
|
3238
|
+
if (hours < 24) return `${hours}h ago`;
|
|
3239
|
+
const days = Math.floor(hours / 24);
|
|
3240
|
+
return `${days}d ago`;
|
|
3241
|
+
}
|
|
1706
3242
|
|
|
1707
3243
|
// src/index.ts
|
|
1708
3244
|
var require2 = createRequire2(import.meta.url);
|
|
1709
3245
|
var { version } = require2("../package.json");
|
|
1710
|
-
var main =
|
|
3246
|
+
var main = defineCommand14({
|
|
1711
3247
|
meta: {
|
|
1712
3248
|
name: "sniper",
|
|
1713
3249
|
version,
|
|
1714
|
-
description: "SNIPER \u2014
|
|
3250
|
+
description: "SNIPER v3 \u2014 AI-Powered Project Lifecycle Framework"
|
|
1715
3251
|
},
|
|
1716
3252
|
subCommands: {
|
|
1717
3253
|
init: initCommand,
|
|
1718
3254
|
status: statusCommand,
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
3255
|
+
migrate: migrateCommand,
|
|
3256
|
+
plugin: pluginCommand,
|
|
3257
|
+
protocol: protocolCommand,
|
|
3258
|
+
dashboard: dashboardCommand,
|
|
3259
|
+
workspace: workspaceCommand,
|
|
3260
|
+
revert: revertCommand,
|
|
3261
|
+
run: runCommand,
|
|
3262
|
+
marketplace: marketplaceCommand,
|
|
3263
|
+
signal: signalCommand,
|
|
3264
|
+
knowledge: knowledgeCommand,
|
|
3265
|
+
sphere: sphereCommand
|
|
1725
3266
|
}
|
|
1726
3267
|
});
|
|
1727
3268
|
runMain(main);
|