@wrongstack/cli 0.7.4 → 0.7.6

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
@@ -9,7 +9,7 @@ import * as os6 from 'os';
9
9
  import os6__default from 'os';
10
10
  import * as crypto2 from 'crypto';
11
11
  import { randomUUID } from 'crypto';
12
- import { DefaultSecretVault as DefaultSecretVault$1, encryptConfigSecrets, decryptConfigSecrets as decryptConfigSecrets$1 } from '@wrongstack/core/security';
12
+ import { DefaultSecretVault as DefaultSecretVault$1, encryptConfigSecrets, isSecretField, decryptConfigSecrets as decryptConfigSecrets$1 } from '@wrongstack/core/security';
13
13
  import { WebSocketServer, WebSocket } from 'ws';
14
14
  import { MCPRegistry } from '@wrongstack/mcp';
15
15
  import { buildProviderFactoriesFromRegistry, makeProviderFromConfig, capabilitiesFor } from '@wrongstack/providers';
@@ -64,8 +64,10 @@ __export(sdd_exports, {
64
64
  getActiveSDDPhase: () => getActiveSDDPhase,
65
65
  getCurrentExecutingContext: () => getCurrentExecutingContext,
66
66
  getCurrentTask: () => getCurrentTask,
67
+ getTaskGraphId: () => getTaskGraphId,
67
68
  getTaskListText: () => getTaskListText,
68
69
  getTaskProgress: () => getTaskProgress,
70
+ getTaskTracker: () => getTaskTracker,
69
71
  markTaskCompleted: () => markTaskCompleted,
70
72
  renderTaskListWithProgress: () => renderTaskListWithProgress,
71
73
  trySaveImplementationPlan: () => trySaveImplementationPlan,
@@ -380,10 +382,9 @@ function buildSddCommand(opts) {
380
382
  name: "sdd",
381
383
  description: "AI-driven SDD: /sdd [new|approve|execute|cancel|status|list|show|templates]",
382
384
  async run(args) {
383
- const ctx = opts.context;
384
- const projectRoot = ctx?.projectRoot ?? process.cwd();
385
- const specsDir = path23.join(projectRoot, ".wrongstack", "specs");
386
- const graphsDir = path23.join(projectRoot, ".wrongstack", "task-graphs");
385
+ opts.context;
386
+ const specsDir = opts.paths.projectSpecs;
387
+ const graphsDir = opts.paths.projectTaskGraphs;
387
388
  const specStore = new SpecStore({ baseDir: specsDir });
388
389
  new TaskGraphStore({ baseDir: graphsDir });
389
390
  const versioning = sddState.getVersioning();
@@ -399,10 +400,10 @@ function buildSddCommand(opts) {
399
400
  const forceFlag = rest.includes("--force") || rest.includes("-f");
400
401
  const title = rest.filter((a) => !a.startsWith("-")).join(" ").trim() || "Untitled Feature";
401
402
  if (!sessionState.getBuilder() && !forceFlag) {
402
- const sessionPath = path23.join(projectRoot, ".wrongstack", "sdd-session.json");
403
+ const sessionPath = opts.paths.projectSddSession;
403
404
  try {
404
405
  await fsp2.access(sessionPath);
405
- const projectContext2 = await gatherProjectContext(projectRoot);
406
+ const projectContext2 = await gatherProjectContext(opts.context?.projectRoot ?? process.cwd());
406
407
  const tempBuilder = new AISpecBuilder({
407
408
  store: specStore,
408
409
  projectContext: projectContext2,
@@ -428,13 +429,13 @@ function buildSddCommand(opts) {
428
429
  }
429
430
  }
430
431
  sddState.clearTaskState();
431
- const projectContext = await gatherProjectContext(projectRoot);
432
+ const projectContext = await gatherProjectContext(opts.context?.projectRoot ?? process.cwd());
432
433
  sddState.setBuilder(new AISpecBuilder({
433
434
  store: specStore,
434
435
  projectContext,
435
436
  minQuestions: 2,
436
437
  maxQuestions: 10,
437
- sessionPath: path23.join(projectRoot, ".wrongstack", "sdd-session.json")
438
+ sessionPath: opts.paths.projectSddSession
438
439
  }));
439
440
  sddState.setSessionStartTime(Date.now());
440
441
  sddState.setPhaseStartTime(Date.now());
@@ -547,8 +548,16 @@ Start executing the tasks one by one.`
547
548
  };
548
549
  }
549
550
  // ── Task Execution ─────────────────────────────────────────────────
550
- case "execute":
551
- case "run": {
551
+ case "run":
552
+ case "execute": {
553
+ if (opts.onSddParallelRun) {
554
+ const slotsArg = restJoined.trim();
555
+ const slots = slotsArg ? Number.parseInt(slotsArg, 10) : void 0;
556
+ const message = await opts.onSddParallelRun(
557
+ slots && Number.isFinite(slots) ? { parallelSlots: Math.min(16, Math.max(1, slots)) } : {}
558
+ );
559
+ return { message };
560
+ }
552
561
  const runBuilder = sddState.getBuilder();
553
562
  if (!runBuilder) {
554
563
  return {
@@ -572,6 +581,21 @@ User message:
572
581
  Start executing the tasks one by one.`
573
582
  };
574
583
  }
584
+ case "parallel": {
585
+ if (!opts.onSddParallelRun) {
586
+ return { message: "SDD parallel run is not available in this session." };
587
+ }
588
+ const slotsArg = restJoined.trim();
589
+ const slots = slotsArg ? Number.parseInt(slotsArg, 10) : void 0;
590
+ const message = await opts.onSddParallelRun(
591
+ slots && Number.isFinite(slots) ? { parallelSlots: Math.min(16, Math.max(1, slots)) } : {}
592
+ );
593
+ return { message };
594
+ }
595
+ case "stop": {
596
+ opts.onSddParallelStop?.();
597
+ return { message: "SDD parallel run stopped." };
598
+ }
575
599
  case "plan":
576
600
  case "impl": {
577
601
  const planBuilder = sddState.getBuilder();
@@ -1001,7 +1025,7 @@ Start executing the tasks one by one.`
1001
1025
  return { message: lines2.join("\n") };
1002
1026
  }
1003
1027
  try {
1004
- const graphStore2 = new TaskGraphStore({ baseDir: path23.join(projectRoot, ".wrongstack", "task-graphs") });
1028
+ const graphStore2 = new TaskGraphStore({ baseDir: opts.paths.projectTaskGraphs });
1005
1029
  const stored = await graphStore2.load(graphId);
1006
1030
  if (stored) {
1007
1031
  return { message: renderTaskGraph(stored, { compact: false }) };
@@ -1026,7 +1050,7 @@ Start executing the tasks one by one.`
1026
1050
  return { message: lines.join("\n") };
1027
1051
  }
1028
1052
  case "cancel": {
1029
- const sessionPath = path23.join(projectRoot, ".wrongstack", "sdd-session.json");
1053
+ const sessionPath = opts.paths.projectSddSession;
1030
1054
  let deletedFromDisk = false;
1031
1055
  try {
1032
1056
  await fsp2.unlink(sessionPath);
@@ -1034,11 +1058,11 @@ Start executing the tasks one by one.`
1034
1058
  } catch {
1035
1059
  }
1036
1060
  try {
1037
- await fsp2.rm(path23.join(projectRoot, ".wrongstack", "specs"), { recursive: true, force: true });
1061
+ await fsp2.rm(opts.paths.projectSpecs, { recursive: true, force: true });
1038
1062
  } catch {
1039
1063
  }
1040
1064
  try {
1041
- await fsp2.rm(path23.join(projectRoot, ".wrongstack", "task-graphs"), { recursive: true, force: true });
1065
+ await fsp2.rm(opts.paths.projectTaskGraphs, { recursive: true, force: true });
1042
1066
  } catch {
1043
1067
  }
1044
1068
  const cancelBuilder = sddState.getBuilder();
@@ -1058,8 +1082,8 @@ Start executing the tasks one by one.`
1058
1082
  if (sddState.getBuilder()) {
1059
1083
  return { message: "An SDD session is already active. Use /sdd cancel first." };
1060
1084
  }
1061
- const sessionPath = path23.join(projectRoot, ".wrongstack", "sdd-session.json");
1062
- const projectContext = await gatherProjectContext(projectRoot);
1085
+ const sessionPath = opts.paths.projectSddSession;
1086
+ const projectContext = await gatherProjectContext(opts.context?.projectRoot ?? process.cwd());
1063
1087
  sddState.setBuilder(new AISpecBuilder({
1064
1088
  store: specStore,
1065
1089
  projectContext,
@@ -1222,7 +1246,7 @@ ${lines.join("\n")}`
1222
1246
  return { message: "No task graph found. Generate tasks first." };
1223
1247
  }
1224
1248
  try {
1225
- const graphStore2 = new TaskGraphStore({ baseDir: path23.join(projectRoot, ".wrongstack", "task-graphs") });
1249
+ const graphStore2 = new TaskGraphStore({ baseDir: opts.paths.projectTaskGraphs });
1226
1250
  const graph = await graphStore2.load(graphId);
1227
1251
  if (!graph) {
1228
1252
  return { message: "Could not load task graph." };
@@ -1416,6 +1440,12 @@ async function findSpec(store, idOrTitle) {
1416
1440
  if (match) return store.load(match.id);
1417
1441
  return null;
1418
1442
  }
1443
+ function getTaskGraphId() {
1444
+ return sddState.getTaskGraphId();
1445
+ }
1446
+ function getTaskTracker() {
1447
+ return sddState.getTaskTracker();
1448
+ }
1419
1449
  var SDD_META_KEY, SDDState, sddState;
1420
1450
  var init_sdd = __esm({
1421
1451
  "src/slash-commands/sdd.ts"() {
@@ -1821,7 +1851,21 @@ async function runWebUI(opts) {
1821
1851
  const client = { ws, sessionId: opts.session.id };
1822
1852
  clients.set(ws, client);
1823
1853
  console.log("[WebUI] Client connected");
1854
+ let msgCount = 0;
1855
+ let windowResetAt = Date.now() + 6e4;
1824
1856
  ws.on("message", async (data) => {
1857
+ const now = Date.now();
1858
+ if (now > windowResetAt) {
1859
+ msgCount = 0;
1860
+ windowResetAt = now + 6e4;
1861
+ }
1862
+ if (++msgCount > 60) {
1863
+ send(ws, {
1864
+ type: "error",
1865
+ payload: { phase: "rate_limit", message: "Too many messages. Please wait." }
1866
+ });
1867
+ return;
1868
+ }
1825
1869
  try {
1826
1870
  const msg = JSON.parse(data.toString());
1827
1871
  await handleMessage(ws, client, msg);
@@ -2743,7 +2787,7 @@ function maskConfigSecrets(cfg) {
2743
2787
  if (typeof cfg !== "object" || cfg === null) return {};
2744
2788
  const out = {};
2745
2789
  for (const [k, v] of Object.entries(cfg)) {
2746
- if (k === "apiKey" || k === "apiKeys" || k === "secret" || k === "secrets") {
2790
+ if (isSecretField(k)) {
2747
2791
  out[k] = "[REDACTED]";
2748
2792
  } else if (typeof v === "object" && v !== null && !Array.isArray(v)) {
2749
2793
  out[k] = maskConfigSecrets(v);
@@ -2760,7 +2804,7 @@ function diffSummary(oldCfg, newCfg) {
2760
2804
  const o = JSON.stringify(oldCfg[k]);
2761
2805
  const n = JSON.stringify(newCfg[k]);
2762
2806
  if (o !== n) {
2763
- if (k === "apiKey" || k === "apiKeys" || k === "secret") {
2807
+ if (isSecretField(k)) {
2764
2808
  changes.push(`${k}: [CHANGED]`);
2765
2809
  } else if (typeof newCfg[k] !== "object") {
2766
2810
  changes.push(`${k}: ${oldCfg[k] ?? "(unset)"} \u2192 ${newCfg[k]}`);
@@ -4583,7 +4627,7 @@ function buildPlanCommand(opts) {
4583
4627
  name: "plan",
4584
4628
  description: "Strategic plan board: /plan [show|add <title>|start <id|#>|done <id|#>|remove <id|#>|promote <id|#> [subtask ...]|derive <id|#>|template [list|use <name>]|clear]",
4585
4629
  async run(args) {
4586
- const planPath = opts.planPath;
4630
+ const planPath = opts.paths?.projectPlan ?? opts.planPath;
4587
4631
  if (!planPath) return { message: "Plan storage is not configured for this session." };
4588
4632
  const ctx = opts.context;
4589
4633
  const sessionId = ctx?.session.id ?? "unknown";
@@ -4825,11 +4869,27 @@ function buildSpawnCommand(opts) {
4825
4869
  function buildAgentsCommand(opts) {
4826
4870
  return {
4827
4871
  name: "agents",
4828
- description: "Show status of spawned subagents. /agents monitor opens the agents monitor overlay.",
4872
+ description: "Show status of spawned subagents. /agents monitor opens the agents monitor overlay. /agents on|off toggles the overlay.",
4873
+ help: [
4874
+ "Usage: /agents [monitor|on|off]",
4875
+ " /agents \u2014 show subagent status summary",
4876
+ " /agents monitor \u2014 open the agents monitor overlay",
4877
+ " /agents on \u2014 show the agents monitor overlay",
4878
+ " /agents off \u2014 hide the agents monitor overlay"
4879
+ ].join("\n"),
4829
4880
  async run(args) {
4881
+ const arg = args.trim().toLowerCase();
4882
+ if (arg === "monitor" || arg === "on") {
4883
+ opts.agentsMonitorController?.setVisible(true);
4884
+ return { message: "Agents monitor shown." };
4885
+ }
4886
+ if (arg === "off") {
4887
+ opts.agentsMonitorController?.setVisible(false);
4888
+ return { message: "Agents monitor hidden." };
4889
+ }
4830
4890
  if (!opts.onAgents) return { message: "Multi-agent is not enabled in this session." };
4831
- const subagentId = args.trim() || void 0;
4832
- return { message: await opts.onAgents(subagentId === "monitor" ? "monitor" : subagentId) };
4891
+ const subagentId = arg || void 0;
4892
+ return { message: await opts.onAgents(subagentId) };
4833
4893
  }
4834
4894
  };
4835
4895
  }
@@ -5164,7 +5224,7 @@ function buildGoalCommand(opts) {
5164
5224
  "Stage flow: decide \u2192 execute \u2192 reflect \u2192 sleep | paused | stopped",
5165
5225
  "Pausing stops after current iteration completes. Resume continues from next iteration.",
5166
5226
  "",
5167
- "Goals live in <projectRoot>/.wrongstack/goal.json and persist across sessions.",
5227
+ "Goals live in ~/.wrongstack/projects/<hash>/goal.json and persist across sessions.",
5168
5228
  "A goal is the prerequisite for /autonomy eternal \u2014 the engine consults it on",
5169
5229
  "every iteration to decide what to do next."
5170
5230
  ].join("\n"),
@@ -5173,7 +5233,7 @@ function buildGoalCommand(opts) {
5173
5233
  const [verbRaw, ...rest] = trimmed.split(/\s+/);
5174
5234
  const verb = (verbRaw ?? "").toLowerCase();
5175
5235
  const restJoined = rest.join(" ").trim();
5176
- const goalPath = goalFilePath(opts.projectRoot);
5236
+ const goalPath = opts.paths.projectGoal;
5177
5237
  const verbForDispatch = verb && !KNOWN_VERBS.has(verb) ? "set" : verb;
5178
5238
  const setText = verbForDispatch === "set" && !KNOWN_VERBS.has(verb) ? trimmed : restJoined;
5179
5239
  switch (verbForDispatch) {
@@ -8429,8 +8489,7 @@ var initCmd = async (_args, deps) => {
8429
8489
  await fsp2.mkdir(deps.paths.globalRoot, { recursive: true });
8430
8490
  const config = { version: 1, provider: providerId, model: modelId };
8431
8491
  if (apiKey) config.apiKey = apiKey;
8432
- const keyFile = path23.join(path23.dirname(deps.paths.globalConfig), ".key");
8433
- const vault = new DefaultSecretVault$1({ keyFile });
8492
+ const vault = new DefaultSecretVault$1({ keyFile: deps.paths.secretsKey });
8434
8493
  const encrypted = encryptConfigSecrets(config, vault);
8435
8494
  await atomicWrite(deps.paths.globalConfig, JSON.stringify(encrypted, null, 2));
8436
8495
  await fsp2.mkdir(path23.join(deps.projectRoot, ".wrongstack"), { recursive: true });
@@ -8880,14 +8939,12 @@ Cache age: ${isFinite(age) ? `${Math.round(age / 60)}m` : "never fetched"}. Run
8880
8939
  );
8881
8940
  return 0;
8882
8941
  };
8883
-
8884
- // src/subcommands/handlers/helpers.ts
8885
8942
  function redactKeys(obj) {
8886
8943
  if (!obj || typeof obj !== "object") return obj;
8887
8944
  if (Array.isArray(obj)) return obj.map(redactKeys);
8888
8945
  const out = {};
8889
8946
  for (const [k, v] of Object.entries(obj)) {
8890
- if (/api.?key|secret|token|pass/i.test(k) && typeof v === "string" && v.length > 0)
8947
+ if (isSecretField(k) && typeof v === "string" && v.length > 0)
8891
8948
  out[k] = "[REDACTED]";
8892
8949
  else out[k] = redactKeys(v);
8893
8950
  }
@@ -10420,6 +10477,7 @@ async function execute(deps) {
10420
10477
  fleetStreamController,
10421
10478
  statuslineHiddenItems,
10422
10479
  setStatuslineHiddenItems,
10480
+ agentsMonitorController,
10423
10481
  getYolo,
10424
10482
  getAutonomy,
10425
10483
  onAutonomy,
@@ -10577,6 +10635,7 @@ async function execute(deps) {
10577
10635
  fleetStreamController,
10578
10636
  statuslineHiddenItems,
10579
10637
  setStatuslineHiddenItems,
10638
+ agentsMonitorController,
10580
10639
  initialGoal: goalFlag,
10581
10640
  initialAsk: askFlag,
10582
10641
  projectRoot,
@@ -11945,7 +12004,7 @@ async function main(argv) {
11945
12004
  const skillLoader = container.resolve(TOKENS.SkillLoader);
11946
12005
  const sessionRef = {};
11947
12006
  const autonomyModeRef = { current: "off" };
11948
- const goalPathForPrompt = path23.join(projectRoot, ".wrongstack", "goal.json");
12007
+ const goalPathForPrompt = wpaths.projectGoal;
11949
12008
  container.bind(
11950
12009
  TOKENS.SystemPromptBuilder,
11951
12010
  () => new DefaultSystemPromptBuilder({
@@ -12238,12 +12297,20 @@ async function main(argv) {
12238
12297
  if (!hiddenItemsFromConfig[k]) hiddenItemsList.push(k);
12239
12298
  }
12240
12299
  const statuslineHiddenItems = hiddenItemsList;
12241
- [...statuslineHiddenItems];
12300
+ let currentHiddenItems = [...statuslineHiddenItems];
12242
12301
  const setStatuslineHiddenItems = (items) => {
12302
+ currentHiddenItems = items;
12303
+ };
12304
+ const agentsMonitorController = {
12305
+ visible: false,
12306
+ setVisible(visible) {
12307
+ this.visible = visible;
12308
+ }
12243
12309
  };
12244
12310
  const slashCmds = buildBuiltinSlashCommands({
12245
12311
  registry: slashRegistry,
12246
12312
  toolRegistry,
12313
+ paths: wpaths,
12247
12314
  compactor: container.resolve(TOKENS.Compactor),
12248
12315
  sessionStore,
12249
12316
  skillLoader,
@@ -12261,6 +12328,9 @@ async function main(argv) {
12261
12328
  llmProvider: provider,
12262
12329
  llmModel: config.model,
12263
12330
  statuslineConfig: statuslineConfigDeps,
12331
+ statuslineHiddenItems: [...currentHiddenItems],
12332
+ setStatuslineHiddenItems,
12333
+ agentsMonitorController,
12264
12334
  onSpawn: async (description, spawnOpts) => {
12265
12335
  const { subagentId, taskId } = await multiAgentHost.spawn(description, spawnOpts);
12266
12336
  const tags = [];
@@ -12800,13 +12870,62 @@ Restart WrongStack to load or unload plugin code in this session.`;
12800
12870
  onDispatchClassify: makeProviderClassifier(
12801
12871
  context.provider,
12802
12872
  context.model
12803
- )
12873
+ ),
12874
+ onSddParallelRun: async (opts) => {
12875
+ const { SddParallelRun } = await import('@wrongstack/core');
12876
+ const sdd = await Promise.resolve().then(() => (init_sdd(), sdd_exports));
12877
+ const tracker = sdd.getTaskTracker();
12878
+ const builder = sdd.getActiveBuilder();
12879
+ if (!tracker || !builder) {
12880
+ return "No active SDD session with tasks. Use /sdd new to start one.";
12881
+ }
12882
+ const session2 = builder.getSession();
12883
+ if (session2.phase !== "executing" && session2.phase !== "task_review") {
12884
+ return `Cannot run parallel in phase "${session2.phase}". Use /sdd approve first.`;
12885
+ }
12886
+ const graphId = sdd.getTaskGraphId();
12887
+ const graphStore = new (await import('@wrongstack/core')).TaskGraphStore({
12888
+ baseDir: wpaths.projectTaskGraphs
12889
+ });
12890
+ const graph = graphId ? await graphStore.load(graphId) : null;
12891
+ if (!graph) {
12892
+ return "No task graph found for the current SDD session.";
12893
+ }
12894
+ const subagentFactory = multiAgentHost.makeSubagentFactory(config);
12895
+ const run = new SddParallelRun({
12896
+ tracker,
12897
+ graph,
12898
+ agent,
12899
+ projectRoot,
12900
+ parallelSlots: opts?.parallelSlots,
12901
+ subagentFactory,
12902
+ onProgress: (p) => {
12903
+ renderer.write(` \u2591 wave ${p.wave + 1} \xB7 ${p.completed}/${p.total} tasks \xB7 ${p.percent}% done
12904
+ `);
12905
+ }
12906
+ });
12907
+ globalThis.__sddParallelRun = run;
12908
+ const result = await run.run();
12909
+ delete globalThis.__sddParallelRun;
12910
+ const lines = [
12911
+ `SDD parallel run complete:`,
12912
+ ` ${result.totalWaves} waves \xB7 ${result.totalCompleted} done \xB7 ${result.totalFailed} failed`,
12913
+ ` ${(result.totalDurationMs / 1e3).toFixed(1)}s total`
12914
+ ];
12915
+ if (result.deadlocked) lines.push(color.red(" \u26A0 deadlock \u2014 tasks blocked by failed tasks."));
12916
+ if (result.stopRequested) lines.push(color.yellow(" \u26A1 stopped by user."));
12917
+ return lines.join("\n");
12918
+ },
12919
+ onSddParallelStop: () => {
12920
+ const run = globalThis.__sddParallelRun;
12921
+ run?.stop();
12922
+ }
12804
12923
  });
12805
12924
  for (const cmd of slashCmds) slashRegistry.register(cmd);
12806
12925
  const eternalFlag = typeof flags["eternal"] === "string" ? flags["eternal"].trim() : "";
12807
12926
  if (eternalFlag.length > 0) {
12808
- const { saveGoal: saveGoal2, emptyGoal: emptyGoal2, goalFilePath: goalFilePath4, loadGoal: loadGoal4 } = await import('@wrongstack/core');
12809
- const goalPath = goalFilePath4(projectRoot);
12927
+ const { saveGoal: saveGoal2, emptyGoal: emptyGoal2, goalFilePath: goalFilePath3, loadGoal: loadGoal4 } = await import('@wrongstack/core');
12928
+ const goalPath = goalFilePath3(projectRoot);
12810
12929
  const prior = await loadGoal4(goalPath);
12811
12930
  const next = prior ? { ...prior, goal: eternalFlag, setAt: (/* @__PURE__ */ new Date()).toISOString(), lastActivityAt: (/* @__PURE__ */ new Date()).toISOString() } : emptyGoal2(eternalFlag);
12812
12931
  await saveGoal2(goalPath, next);