@wrongstack/sdd 1.0.2 → 1.0.3

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
@@ -521,7 +521,7 @@ function assertTaskGraphExecutionIntegrity(graph) {
521
521
  const indegree = new Map(Array.from(graph.nodes.keys(), (id) => [id, 0]));
522
522
  const outgoing = /* @__PURE__ */ new Map();
523
523
  for (const edge of dependencies) {
524
- indegree.set(edge.to, (indegree.get(edge.to) ?? 0) + 1);
524
+ indegree.set(edge.to, indegree.get(edge.to) + 1);
525
525
  outgoing.set(edge.from, [...outgoing.get(edge.from) ?? [], edge.to]);
526
526
  }
527
527
  const ready = Array.from(indegree.entries()).filter(([, degree]) => degree === 0).map(([id]) => id);
@@ -530,7 +530,7 @@ function assertTaskGraphExecutionIntegrity(graph) {
530
530
  const id = ready.shift();
531
531
  visited += 1;
532
532
  for (const dependent of outgoing.get(id) ?? []) {
533
- const next = (indegree.get(dependent) ?? 0) - 1;
533
+ const next = indegree.get(dependent) - 1;
534
534
  indegree.set(dependent, next);
535
535
  if (next === 0) ready.push(dependent);
536
536
  }
@@ -757,8 +757,9 @@ var SpecStore = class {
757
757
  await this.updateIndex(spec);
758
758
  }
759
759
  async load(id) {
760
+ const filePath = this.filePath(id);
760
761
  try {
761
- const raw = await fsp.readFile(this.filePath(id), "utf8");
762
+ const raw = await fsp.readFile(filePath, "utf8");
762
763
  return JSON.parse(raw);
763
764
  } catch {
764
765
  return null;
@@ -769,8 +770,9 @@ var SpecStore = class {
769
770
  return index.entries.sort((a, b) => b.updatedAt - a.updatedAt);
770
771
  }
771
772
  async delete(id) {
773
+ const filePath = this.filePath(id);
772
774
  try {
773
- await fsp.unlink(this.filePath(id));
775
+ await fsp.unlink(filePath);
774
776
  await this.removeFromIndex(id);
775
777
  return true;
776
778
  } catch {
@@ -816,8 +818,25 @@ var SpecStore = class {
816
818
  await this.save(updated);
817
819
  return updated;
818
820
  }
821
+ /**
822
+ * Resolve a spec id to its file, refusing anything that escapes `baseDir`.
823
+ * `id` arrives from a WebSocket frame (`specs-ws-handler.ts:96-97,152`,
824
+ * raw `as string` casts) and from any persisted spec JSON. A bare
825
+ * `path.join` was the traversal primitive: `id = "../../../secret"`
826
+ * resolved outside the store and `load()` returned its contents. Mirrors
827
+ * the same containment `task-graph-store.ts:134-145` applies to its ids.
828
+ */
819
829
  filePath(id) {
820
- return path.join(this.baseDir, `${id}.json`);
830
+ if (typeof id !== "string" || id.length === 0 || id.length > 200 || /[\0/\\]/.test(id)) {
831
+ throw new Error(`Invalid spec id: ${JSON.stringify(id)}`);
832
+ }
833
+ const dir = path.resolve(this.baseDir);
834
+ const resolved = path.resolve(dir, `${id}.json`);
835
+ const rel = path.relative(dir, resolved);
836
+ if (rel.startsWith("..") || path.isAbsolute(rel) || rel.includes(path.sep)) {
837
+ throw new Error(`Invalid spec id: ${JSON.stringify(id)}`);
838
+ }
839
+ return resolved;
821
840
  }
822
841
  async readIndex() {
823
842
  try {
@@ -3328,7 +3347,7 @@ var SddParallelRun = class {
3328
3347
  const sessionId = this.currentSessionId();
3329
3348
  this.events?.emit(
3330
3349
  event,
3331
- sessionId ? { ...payload, sessionId } : payload
3350
+ { ...payload, sessionId }
3332
3351
  );
3333
3352
  }
3334
3353
  currentSessionId() {
@@ -3342,7 +3361,7 @@ var SddParallelRun = class {
3342
3361
  /** Resolvers for tasks parked in `waitWhilePaused`, woken on resume/stop. */
3343
3362
  pausedWaiters = /* @__PURE__ */ new Set();
3344
3363
  notifyPausedWaiters() {
3345
- for (const resolve2 of this.pausedWaiters) resolve2();
3364
+ for (const resolve3 of this.pausedWaiters) resolve3();
3346
3365
  this.pausedWaiters.clear();
3347
3366
  }
3348
3367
  /** Trigger stop — causes run() to abort after the current wave. */
@@ -3515,10 +3534,10 @@ var SddParallelRun = class {
3515
3534
  }
3516
3535
  async waitWhilePaused() {
3517
3536
  while (this.paused && !this.stopRequested) {
3518
- await new Promise((resolve2) => {
3519
- this.pausedWaiters.add(resolve2);
3537
+ await new Promise((resolve3) => {
3538
+ this.pausedWaiters.add(resolve3);
3520
3539
  const safety = setTimeout(() => {
3521
- if (this.pausedWaiters.delete(resolve2)) resolve2();
3540
+ if (this.pausedWaiters.delete(resolve3)) resolve3();
3522
3541
  }, 1e3);
3523
3542
  safety.unref?.();
3524
3543
  });
@@ -4049,6 +4068,7 @@ function startSddRun(opts) {
4049
4068
  });
4050
4069
  const workflowId = kanbanWorkflowId("sdd", run.runId);
4051
4070
  const legacyControl = opts.controlTransport === "legacy-file";
4071
+ const controlTransportName = legacyControl ? "legacy-file" : "kanban";
4052
4072
  const legacyBoardState = opts.boardStateTransport === "legacy-file" || opts.boardStateTransport === void 0 && legacyControl;
4053
4073
  const boardPersistence = legacyBoardState ? opts.boardStore : {
4054
4074
  saveSnapshot: async (snapshot) => {
@@ -4110,7 +4130,7 @@ function startSddRun(opts) {
4110
4130
  event: "sdd.control_drain_failed",
4111
4131
  runId: run.runId,
4112
4132
  workflowId,
4113
- transport: legacyControl ? "legacy-file" : "kanban",
4133
+ transport: controlTransportName,
4114
4134
  message,
4115
4135
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
4116
4136
  })
@@ -4121,12 +4141,12 @@ function startSddRun(opts) {
4121
4141
  };
4122
4142
  if (!legacyControl) {
4123
4143
  void subscribeKanbanWorkflowCommands(opts.projectRoot, workflowId, () => {
4124
- void drainControl().catch(() => void 0);
4144
+ void drainControl();
4125
4145
  }).then((unsubscribe) => {
4126
4146
  if (controlDisposed) unsubscribe();
4127
4147
  else {
4128
4148
  unsubscribeControl = unsubscribe;
4129
- void drainControl().catch(() => void 0);
4149
+ void drainControl();
4130
4150
  }
4131
4151
  }).catch((error) => {
4132
4152
  console.warn(
@@ -4159,7 +4179,7 @@ function startSddRun(opts) {
4159
4179
  }
4160
4180
  const drainMs = opts.controlDrainMs ?? 500;
4161
4181
  const controlTimer = setInterval(() => {
4162
- void drainControl().catch(() => void 0);
4182
+ void drainControl();
4163
4183
  }, drainMs);
4164
4184
  controlTimer.unref?.();
4165
4185
  const completion = (async () => {
@@ -4522,7 +4542,7 @@ async function gatherProjectContext(projectRoot) {
4522
4542
  function deriveTitle(value) {
4523
4543
  const firstLine = value.split("\n").map((line) => line.trim()).find(Boolean);
4524
4544
  if (!firstLine) return "New SDD Project";
4525
- const sentence = firstLine.split(/(?<=[.!?])\s/)[0] ?? firstLine;
4545
+ const sentence = firstLine.split(/(?<=[.!?])\s/)[0];
4526
4546
  return sentence.length <= 64 ? sentence : `${sentence.slice(0, 63).trimEnd()}\u2026`;
4527
4547
  }
4528
4548
  function intakeToInterviewKickoff(record) {
@@ -5495,8 +5515,8 @@ function tokenizeCommand(command) {
5495
5515
  hasToken = true;
5496
5516
  }
5497
5517
  if (inSingle || inDouble) return void 0;
5498
- if (hasToken) argv.push(current);
5499
- return argv.length > 0 ? argv : void 0;
5518
+ argv.push(current);
5519
+ return argv;
5500
5520
  }
5501
5521
  function makeCompositeVerifier(parts) {
5502
5522
  return async function verifyTask(info) {
@@ -5569,7 +5589,7 @@ function makeCommandVerifier(options = {}) {
5569
5589
  return { ok: false, reason: `verification command is malformed: ${rawCommand}` };
5570
5590
  }
5571
5591
  const [executable, ...args] = argv;
5572
- return await new Promise((resolve2) => {
5592
+ return await new Promise((resolve3) => {
5573
5593
  const child = spawn(executable, args, {
5574
5594
  cwd: info.cwd,
5575
5595
  shell: false,
@@ -5580,18 +5600,18 @@ function makeCommandVerifier(options = {}) {
5580
5600
  const timer = setTimeout(() => {
5581
5601
  timedOut = true;
5582
5602
  child.kill();
5583
- resolve2({ ok: false, reason: `verification timed out: ${rawCommand}` });
5603
+ resolve3({ ok: false, reason: `verification timed out: ${rawCommand}` });
5584
5604
  }, timeoutMs);
5585
5605
  child.on("exit", (code) => {
5586
5606
  clearTimeout(timer);
5587
5607
  if (timedOut) return;
5588
- resolve2(
5608
+ resolve3(
5589
5609
  code === 0 ? { ok: true } : { ok: false, reason: `verification failed (exit ${code}): ${rawCommand}` }
5590
5610
  );
5591
5611
  });
5592
5612
  child.on("error", (err) => {
5593
5613
  clearTimeout(timer);
5594
- resolve2({ ok: false, reason: `verification spawn error: ${String(err)}` });
5614
+ resolve3({ ok: false, reason: `verification spawn error: ${String(err)}` });
5595
5615
  });
5596
5616
  });
5597
5617
  };
@@ -5750,7 +5770,7 @@ async function decomposeNonAtomicTasks(opts) {
5750
5770
 
5751
5771
  // src/conflict-resolver.ts
5752
5772
  import { readFile as readFile6, writeFile } from "node:fs/promises";
5753
- import { isAbsolute as isAbsolute2, join as join6 } from "node:path";
5773
+ import { isAbsolute as isAbsolute3, join as join6 } from "node:path";
5754
5774
  import { readBundledInstructionText as readBundledInstructionText2, renderInstructionTemplate as renderInstructionTemplate2 } from "@wrongstack/core/utils";
5755
5775
  var defaultFileIO = {
5756
5776
  read: (path6) => readFile6(path6, "utf8"),
@@ -5799,7 +5819,7 @@ function makePreferSideConflictResolver(side, io = defaultFileIO) {
5799
5819
  return async function conflictResolver(info) {
5800
5820
  if (info.conflictFiles.length === 0) return false;
5801
5821
  for (const rel of info.conflictFiles) {
5802
- const abs = isAbsolute2(rel) ? rel : join6(info.cwd, rel);
5822
+ const abs = isAbsolute3(rel) ? rel : join6(info.cwd, rel);
5803
5823
  let content;
5804
5824
  try {
5805
5825
  content = await io.read(abs);
@@ -5833,7 +5853,7 @@ function makeLlmConflictResolver(opts) {
5833
5853
  return async function conflictResolver(info) {
5834
5854
  if (info.conflictFiles.length === 0) return false;
5835
5855
  for (const rel of info.conflictFiles) {
5836
- const abs = isAbsolute2(rel) ? rel : join6(info.cwd, rel);
5856
+ const abs = isAbsolute3(rel) ? rel : join6(info.cwd, rel);
5837
5857
  let content;
5838
5858
  try {
5839
5859
  content = await io.read(abs);
@@ -28,6 +28,14 @@ export declare class SpecStore {
28
28
  createDraft(title: string, overview?: string): Promise<Specification>;
29
29
  /** Update spec fields and persist. */
30
30
  update(id: string, patch: Partial<Omit<Specification, 'id' | 'createdAt'>>): Promise<Specification | null>;
31
+ /**
32
+ * Resolve a spec id to its file, refusing anything that escapes `baseDir`.
33
+ * `id` arrives from a WebSocket frame (`specs-ws-handler.ts:96-97,152`,
34
+ * raw `as string` casts) and from any persisted spec JSON. A bare
35
+ * `path.join` was the traversal primitive: `id = "../../../secret"`
36
+ * resolved outside the store and `load()` returned its contents. Mirrors
37
+ * the same containment `task-graph-store.ts:134-145` applies to its ids.
38
+ */
31
39
  private filePath;
32
40
  private readIndex;
33
41
  private updateIndex;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wrongstack/sdd",
3
- "version": "1.0.2",
3
+ "version": "1.0.3",
4
4
  "license": "MIT",
5
5
  "description": "WrongStack Spec-Driven Development engine — standalone package extracted from @wrongstack/core. Task graph generation, tracking, execution, lifecycle management, and AI-driven spec building for SDD workflows.",
6
6
  "repository": {
@@ -27,10 +27,10 @@
27
27
  "!dist/**/*.map"
28
28
  ],
29
29
  "dependencies": {
30
- "@wrongstack/core": "1.0.2",
31
- "@wrongstack/kanban": "1.0.2",
32
- "@wrongstack/primitives": "1.0.2",
33
- "@wrongstack/requirement-intake": "1.0.2"
30
+ "@wrongstack/core": "1.0.3",
31
+ "@wrongstack/kanban": "1.0.3",
32
+ "@wrongstack/primitives": "1.0.3",
33
+ "@wrongstack/requirement-intake": "1.0.3"
34
34
  },
35
35
  "devDependencies": {
36
36
  "@types/node": "^26.2.0",