@wrongstack/cli 0.7.5 → 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";
@@ -5180,7 +5224,7 @@ function buildGoalCommand(opts) {
5180
5224
  "Stage flow: decide \u2192 execute \u2192 reflect \u2192 sleep | paused | stopped",
5181
5225
  "Pausing stops after current iteration completes. Resume continues from next iteration.",
5182
5226
  "",
5183
- "Goals live in <projectRoot>/.wrongstack/goal.json and persist across sessions.",
5227
+ "Goals live in ~/.wrongstack/projects/<hash>/goal.json and persist across sessions.",
5184
5228
  "A goal is the prerequisite for /autonomy eternal \u2014 the engine consults it on",
5185
5229
  "every iteration to decide what to do next."
5186
5230
  ].join("\n"),
@@ -5189,7 +5233,7 @@ function buildGoalCommand(opts) {
5189
5233
  const [verbRaw, ...rest] = trimmed.split(/\s+/);
5190
5234
  const verb = (verbRaw ?? "").toLowerCase();
5191
5235
  const restJoined = rest.join(" ").trim();
5192
- const goalPath = goalFilePath(opts.projectRoot);
5236
+ const goalPath = opts.paths.projectGoal;
5193
5237
  const verbForDispatch = verb && !KNOWN_VERBS.has(verb) ? "set" : verb;
5194
5238
  const setText = verbForDispatch === "set" && !KNOWN_VERBS.has(verb) ? trimmed : restJoined;
5195
5239
  switch (verbForDispatch) {
@@ -8445,8 +8489,7 @@ var initCmd = async (_args, deps) => {
8445
8489
  await fsp2.mkdir(deps.paths.globalRoot, { recursive: true });
8446
8490
  const config = { version: 1, provider: providerId, model: modelId };
8447
8491
  if (apiKey) config.apiKey = apiKey;
8448
- const keyFile = path23.join(path23.dirname(deps.paths.globalConfig), ".key");
8449
- const vault = new DefaultSecretVault$1({ keyFile });
8492
+ const vault = new DefaultSecretVault$1({ keyFile: deps.paths.secretsKey });
8450
8493
  const encrypted = encryptConfigSecrets(config, vault);
8451
8494
  await atomicWrite(deps.paths.globalConfig, JSON.stringify(encrypted, null, 2));
8452
8495
  await fsp2.mkdir(path23.join(deps.projectRoot, ".wrongstack"), { recursive: true });
@@ -8896,14 +8939,12 @@ Cache age: ${isFinite(age) ? `${Math.round(age / 60)}m` : "never fetched"}. Run
8896
8939
  );
8897
8940
  return 0;
8898
8941
  };
8899
-
8900
- // src/subcommands/handlers/helpers.ts
8901
8942
  function redactKeys(obj) {
8902
8943
  if (!obj || typeof obj !== "object") return obj;
8903
8944
  if (Array.isArray(obj)) return obj.map(redactKeys);
8904
8945
  const out = {};
8905
8946
  for (const [k, v] of Object.entries(obj)) {
8906
- 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)
8907
8948
  out[k] = "[REDACTED]";
8908
8949
  else out[k] = redactKeys(v);
8909
8950
  }
@@ -11963,7 +12004,7 @@ async function main(argv) {
11963
12004
  const skillLoader = container.resolve(TOKENS.SkillLoader);
11964
12005
  const sessionRef = {};
11965
12006
  const autonomyModeRef = { current: "off" };
11966
- const goalPathForPrompt = path23.join(projectRoot, ".wrongstack", "goal.json");
12007
+ const goalPathForPrompt = wpaths.projectGoal;
11967
12008
  container.bind(
11968
12009
  TOKENS.SystemPromptBuilder,
11969
12010
  () => new DefaultSystemPromptBuilder({
@@ -12269,6 +12310,7 @@ async function main(argv) {
12269
12310
  const slashCmds = buildBuiltinSlashCommands({
12270
12311
  registry: slashRegistry,
12271
12312
  toolRegistry,
12313
+ paths: wpaths,
12272
12314
  compactor: container.resolve(TOKENS.Compactor),
12273
12315
  sessionStore,
12274
12316
  skillLoader,
@@ -12828,13 +12870,62 @@ Restart WrongStack to load or unload plugin code in this session.`;
12828
12870
  onDispatchClassify: makeProviderClassifier(
12829
12871
  context.provider,
12830
12872
  context.model
12831
- )
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
+ }
12832
12923
  });
12833
12924
  for (const cmd of slashCmds) slashRegistry.register(cmd);
12834
12925
  const eternalFlag = typeof flags["eternal"] === "string" ? flags["eternal"].trim() : "";
12835
12926
  if (eternalFlag.length > 0) {
12836
- const { saveGoal: saveGoal2, emptyGoal: emptyGoal2, goalFilePath: goalFilePath4, loadGoal: loadGoal4 } = await import('@wrongstack/core');
12837
- const goalPath = goalFilePath4(projectRoot);
12927
+ const { saveGoal: saveGoal2, emptyGoal: emptyGoal2, goalFilePath: goalFilePath3, loadGoal: loadGoal4 } = await import('@wrongstack/core');
12928
+ const goalPath = goalFilePath3(projectRoot);
12838
12929
  const prior = await loadGoal4(goalPath);
12839
12930
  const next = prior ? { ...prior, goal: eternalFlag, setAt: (/* @__PURE__ */ new Date()).toISOString(), lastActivityAt: (/* @__PURE__ */ new Date()).toISOString() } : emptyGoal2(eternalFlag);
12840
12931
  await saveGoal2(goalPath, next);