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