@hegemonart/get-design-done 1.59.6 → 1.59.8

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.
Files changed (55) hide show
  1. package/.claude-plugin/marketplace.json +2 -2
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/CHANGELOG.md +55 -0
  4. package/README.md +4 -13
  5. package/SKILL.md +1 -1
  6. package/agents/design-authority-watcher.md +24 -5
  7. package/bin/gdd-graph +4 -1
  8. package/docs/i18n/README.de.md +210 -527
  9. package/docs/i18n/README.fr.md +201 -518
  10. package/docs/i18n/README.it.md +209 -526
  11. package/docs/i18n/README.ja.md +207 -524
  12. package/docs/i18n/README.ko.md +208 -525
  13. package/docs/i18n/README.zh-CN.md +213 -551
  14. package/hooks/_hook-emit.js +113 -29
  15. package/hooks/budget-enforcer.ts +44 -5
  16. package/hooks/gdd-mcp-circuit-breaker.js +72 -3
  17. package/hooks/gdd-sessionstart-recap.js +23 -14
  18. package/hooks/hooks.json +2 -2
  19. package/package.json +2 -2
  20. package/reference/bandit-integration.md +13 -2
  21. package/scripts/bootstrap.cjs +40 -8
  22. package/scripts/install.cjs +23 -1
  23. package/scripts/lib/bandit-router.cjs +47 -5
  24. package/scripts/lib/detect/cli.cjs +13 -3
  25. package/scripts/lib/install/converters/cursor.cjs +11 -19
  26. package/scripts/lib/install/doctor-codex-plugin.cjs +1 -1
  27. package/scripts/lib/install/doctor-cursor-marketplace.cjs +2 -2
  28. package/scripts/lib/install/installer.cjs +72 -21
  29. package/scripts/lib/install/merge.cjs +31 -3
  30. package/scripts/lib/install/runtime-artifact-layout.cjs +42 -8
  31. package/scripts/lib/manifest/harnesses.json +29 -1
  32. package/scripts/lib/manifest/skills.json +1 -1
  33. package/scripts/skill-templates/bandit-reset/SKILL.md +2 -0
  34. package/scripts/skill-templates/bandit-status/SKILL.md +4 -1
  35. package/scripts/skill-templates/darkmode/SKILL.md +1 -1
  36. package/scripts/skill-templates/graphify/SKILL.md +6 -6
  37. package/scripts/skill-templates/quick/SKILL.md +3 -1
  38. package/scripts/skill-templates/reflect/SKILL.md +1 -1
  39. package/scripts/skill-templates/router/SKILL.md +4 -2
  40. package/sdk/cli/index.js +114 -47
  41. package/sdk/dashboard/data/source.cjs +50 -4
  42. package/sdk/event-stream/writer.ts +112 -30
  43. package/sdk/mcp/gdd-mcp/server.js +49 -36
  44. package/sdk/mcp/gdd-mcp/tools/shared.ts +20 -2
  45. package/sdk/mcp/gdd-state/server.js +107 -41
  46. package/sdk/primitives/lockfile.cjs +26 -5
  47. package/sdk/state/index.ts +91 -17
  48. package/sdk/state/lockfile.ts +47 -8
  49. package/skills/bandit-reset/SKILL.md +2 -0
  50. package/skills/bandit-status/SKILL.md +4 -1
  51. package/skills/darkmode/SKILL.md +1 -1
  52. package/skills/graphify/SKILL.md +6 -6
  53. package/skills/quick/SKILL.md +3 -1
  54. package/skills/reflect/SKILL.md +1 -1
  55. package/skills/router/SKILL.md +4 -2
@@ -2034,7 +2034,7 @@ __export(server_exports, {
2034
2034
  });
2035
2035
  module.exports = __toCommonJS(server_exports);
2036
2036
  var import_node_fs5 = require("node:fs");
2037
- var import_node_path4 = require("node:path");
2037
+ var import_node_path5 = require("node:path");
2038
2038
  var import_server = require("@modelcontextprotocol/sdk/server/index.js");
2039
2039
  var import_stdio = require("@modelcontextprotocol/sdk/server/stdio.js");
2040
2040
  var import_types6 = require("@modelcontextprotocol/sdk/types.js");
@@ -2199,6 +2199,11 @@ function resolveProjectRoot(startCwd = process.cwd()) {
2199
2199
  if ((0, import_node_fs.existsSync)((0, import_node_path.join)(dir, ".design")) || (0, import_node_fs.existsSync)((0, import_node_path.join)(dir, ".planning")) || (0, import_node_fs.existsSync)((0, import_node_path.join)(dir, ".claude-plugin", "plugin.json"))) {
2200
2200
  return dir;
2201
2201
  }
2202
+ if ((0, import_node_fs.existsSync)((0, import_node_path.join)(dir, ".git"))) {
2203
+ throw new Error(
2204
+ `gdd project root not found: hit repo boundary at ${dir} (.git) before any GDD marker, starting from ${startCwd}`
2205
+ );
2206
+ }
2202
2207
  const parent = (0, import_node_path.dirname)(dir);
2203
2208
  if (parent === dir) {
2204
2209
  throw new Error(
@@ -2249,6 +2254,8 @@ __export(gdd_cycle_recap_exports, {
2249
2254
 
2250
2255
  // sdk/state/index.ts
2251
2256
  var import_node_fs2 = require("node:fs");
2257
+ var import_node_path2 = require("node:path");
2258
+ var import_node_module = require("node:module");
2252
2259
 
2253
2260
  // sdk/state/types.ts
2254
2261
  function isConnectionStatus(value) {
@@ -2950,6 +2957,8 @@ var GATES = Object.freeze({
2950
2957
  });
2951
2958
 
2952
2959
  // sdk/state/index.ts
2960
+ var _moduleDir = typeof __dirname !== "undefined" ? __dirname : (0, import_node_path2.dirname)(process.argv[1] || process.cwd());
2961
+ var _require = typeof require !== "undefined" ? require : (0, import_node_module.createRequire)(process.argv[1] || process.cwd());
2953
2962
  async function read(path) {
2954
2963
  const raw = (0, import_node_fs2.readFileSync)(path, "utf8");
2955
2964
  return parse(raw).state;
@@ -3017,51 +3026,55 @@ __export(gdd_events_tail_exports, {
3017
3026
 
3018
3027
  // sdk/event-stream/writer.ts
3019
3028
  var import_node_fs3 = require("node:fs");
3020
- var import_node_path2 = require("node:path");
3021
- var import_node_module = require("node:module");
3029
+ var import_node_path3 = require("node:path");
3030
+ var import_node_module2 = require("node:module");
3022
3031
  function _findRepoRoot() {
3023
- let dir = process.cwd();
3024
- for (let i = 0; i < 8; i++) {
3025
- if ((0, import_node_fs3.existsSync)((0, import_node_path2.join)(dir, "package.json"))) return dir;
3026
- const parent = (0, import_node_path2.dirname)(dir);
3032
+ return _walkToPackageJson(process.cwd());
3033
+ }
3034
+ function _walkToPackageJson(startDir) {
3035
+ let dir = startDir;
3036
+ for (let i = 0; i < 12; i++) {
3037
+ if ((0, import_node_fs3.existsSync)((0, import_node_path3.join)(dir, "package.json"))) return dir;
3038
+ const parent = (0, import_node_path3.dirname)(dir);
3027
3039
  if (parent === dir) break;
3028
3040
  dir = parent;
3029
3041
  }
3030
- return process.cwd();
3042
+ return startDir;
3031
3043
  }
3032
- var _redact;
3033
- try {
3034
- const _root = _findRepoRoot();
3035
- const _candidate = (0, import_node_path2.resolve)(_root, "scripts/lib/redact.cjs");
3036
- if ((0, import_node_fs3.existsSync)(_candidate)) {
3037
- const _redactRequire = (0, import_node_module.createRequire)((0, import_node_path2.join)(_root, "package.json"));
3038
- const _mod = _redactRequire(_candidate);
3039
- _redact = _mod.redact;
3040
- } else {
3041
- const _altRoot = (0, import_node_path2.resolve)(_root, "..", "..");
3042
- const _altCandidate = (0, import_node_path2.resolve)(_altRoot, "scripts/lib/redact.cjs");
3043
- if ((0, import_node_fs3.existsSync)(_altCandidate)) {
3044
- const _altRequire = (0, import_node_module.createRequire)((0, import_node_path2.join)(_altRoot, "package.json"));
3045
- const _altMod = _altRequire(_altCandidate);
3046
- _redact = _altMod.redact;
3047
- } else {
3048
- _redact = (v) => v;
3044
+ function _loadRedact() {
3045
+ const candidates = [];
3046
+ const entry = process.argv[1];
3047
+ if (typeof entry === "string" && entry.length > 0) {
3048
+ const entryAbs = (0, import_node_path3.isAbsolute)(entry) ? entry : (0, import_node_path3.resolve)(entry);
3049
+ const entryRoot = _walkToPackageJson((0, import_node_path3.dirname)(entryAbs));
3050
+ candidates.push((0, import_node_path3.resolve)(entryRoot, "scripts/lib/redact.cjs"));
3051
+ }
3052
+ const repoRoot = _findRepoRoot();
3053
+ candidates.push((0, import_node_path3.resolve)(repoRoot, "scripts/lib/redact.cjs"));
3054
+ candidates.push((0, import_node_path3.resolve)(repoRoot, "..", "..", "scripts/lib/redact.cjs"));
3055
+ for (const candidate of candidates) {
3056
+ try {
3057
+ if (!(0, import_node_fs3.existsSync)(candidate)) continue;
3058
+ const req = (0, import_node_module2.createRequire)(candidate);
3059
+ const mod = req(candidate);
3060
+ if (mod && typeof mod.redact === "function") return mod.redact;
3061
+ } catch {
3049
3062
  }
3050
3063
  }
3051
- } catch {
3052
- _redact = (v) => v;
3064
+ return null;
3053
3065
  }
3066
+ var _realRedact = _loadRedact();
3054
3067
  var DEFAULT_EVENTS_PATH = ".design/telemetry/events.jsonl";
3055
3068
  var DEFAULT_MAX_LINE_BYTES = 64 * 1024;
3056
3069
 
3057
3070
  // sdk/event-stream/reader.ts
3058
3071
  var import_node_fs4 = require("node:fs");
3059
- var import_node_path3 = require("node:path");
3072
+ var import_node_path4 = require("node:path");
3060
3073
  var import_node_readline = require("node:readline");
3061
3074
  function resolveReadPath(opts) {
3062
3075
  const raw = opts.path ?? DEFAULT_EVENTS_PATH;
3063
- if ((0, import_node_path3.isAbsolute)(raw)) return raw;
3064
- return (0, import_node_path3.resolve)(opts.baseDir ?? process.cwd(), raw);
3076
+ if ((0, import_node_path4.isAbsolute)(raw)) return raw;
3077
+ return (0, import_node_path4.resolve)(opts.baseDir ?? process.cwd(), raw);
3065
3078
  }
3066
3079
  async function* readEvents(opts = {}) {
3067
3080
  const path = resolveReadPath(opts);
@@ -3354,16 +3367,16 @@ if (TOOL_COUNT > 13) {
3354
3367
  var SERVER_NAME = "gdd-mcp";
3355
3368
  var SERVER_VERSION = "1.27.7";
3356
3369
  function here() {
3357
- const expectedRel = (0, import_node_path4.join)("sdk", "mcp", "gdd-mcp");
3370
+ const expectedRel = (0, import_node_path5.join)("sdk", "mcp", "gdd-mcp");
3358
3371
  const entry = process.argv[1];
3359
3372
  if (typeof entry === "string" && entry.length > 0) {
3360
- const entryDir = (0, import_node_path4.dirname)((0, import_node_path4.resolve)(entry));
3361
- if ((0, import_node_fs5.existsSync)((0, import_node_path4.join)(entryDir, "tools", "index.ts"))) {
3373
+ const entryDir = (0, import_node_path5.dirname)((0, import_node_path5.resolve)(entry));
3374
+ if ((0, import_node_fs5.existsSync)((0, import_node_path5.join)(entryDir, "tools", "index.ts"))) {
3362
3375
  return entryDir;
3363
3376
  }
3364
3377
  }
3365
- const candidate = (0, import_node_path4.resolve)(process.cwd(), expectedRel);
3366
- if ((0, import_node_fs5.existsSync)((0, import_node_path4.join)(candidate, "tools", "index.ts"))) {
3378
+ const candidate = (0, import_node_path5.resolve)(process.cwd(), expectedRel);
3379
+ if ((0, import_node_fs5.existsSync)((0, import_node_path5.join)(candidate, "tools", "index.ts"))) {
3367
3380
  return candidate;
3368
3381
  }
3369
3382
  return candidate;
@@ -3371,7 +3384,7 @@ function here() {
3371
3384
  function loadTools() {
3372
3385
  const baseDir = here();
3373
3386
  return TOOL_MODULES.map((m) => {
3374
- const absPath = (0, import_node_path4.join)(baseDir, "tools", m.schemaPath);
3387
+ const absPath = (0, import_node_path5.join)(baseDir, "tools", m.schemaPath);
3375
3388
  const raw = (0, import_node_fs5.readFileSync)(absPath, "utf8");
3376
3389
  const parsed = JSON.parse(raw);
3377
3390
  const rawInput = parsed.properties?.input;
@@ -103,9 +103,18 @@ export function resolveSnapshotsDir(): string {
103
103
  * is returned verbatim (after path resolution). This is useful for
104
104
  * tests and for users who want to pin a project root explicitly.
105
105
  *
106
+ * REPO-BOUNDARY GUARD (audit S8): the upward walk STOPS at the first `.git`
107
+ * directory it encounters. If a `.git` boundary is hit BEFORE any GDD marker
108
+ * is found, the walk does NOT continue into the parent repository — that
109
+ * would let a nested, unrelated checkout resolve to a PARENT repo's
110
+ * `.design/`/`.planning/`, leaking another project's state into this one
111
+ * (cross-project info bleed). At a `.git` boundary we check the boundary
112
+ * directory itself for a marker (a repo root legitimately holds `.design/`),
113
+ * then treat "no marker at or below this repo root" as not-found.
114
+ *
106
115
  * Throws `Error('gdd project root not found: ...')` when no marker is
107
- * found before the filesystem root. Callers in tool handlers should
108
- * catch and forward via `errorResponse()`.
116
+ * found before either the first `.git` boundary or the filesystem root.
117
+ * Callers in tool handlers should catch and forward via `errorResponse()`.
109
118
  */
110
119
  export function resolveProjectRoot(startCwd: string = process.cwd()): string {
111
120
  const override = process.env['GDD_PROJECT_ROOT'];
@@ -122,6 +131,15 @@ export function resolveProjectRoot(startCwd: string = process.cwd()): string {
122
131
  ) {
123
132
  return dir;
124
133
  }
134
+ // S8: a `.git` here marks a repository boundary. We already checked this
135
+ // directory for a marker above and found none, so do not walk PAST the
136
+ // repo root into a parent (possibly unrelated) project.
137
+ if (existsSync(join(dir, '.git'))) {
138
+ throw new Error(
139
+ `gdd project root not found: hit repo boundary at ${dir} ` +
140
+ `(.git) before any GDD marker, starting from ${startCwd}`,
141
+ );
142
+ }
125
143
  const parent = dirname(dir);
126
144
  if (parent === dir) {
127
145
  // Reached filesystem root — give up.
@@ -197,6 +197,7 @@ __export(get_exports, {
197
197
  // sdk/state/index.ts
198
198
  var import_node_fs2 = require("node:fs");
199
199
  var import_node_path = require("node:path");
200
+ var import_node_module = require("node:module");
200
201
 
201
202
  // sdk/state/lockfile.ts
202
203
  var import_node_fs = require("node:fs");
@@ -263,6 +264,14 @@ async function acquire(path2, opts = {}) {
263
264
  }
264
265
  const parsed = parseLock(existing);
265
266
  if (parsed !== null && isStale(parsed, staleMs)) {
267
+ const confirm = readLockSafe(lockPath);
268
+ if (confirm === null) {
269
+ continue;
270
+ }
271
+ if (confirm !== existing) {
272
+ await sleep(pollMs);
273
+ continue;
274
+ }
266
275
  try {
267
276
  (0, import_node_fs.unlinkSync)(lockPath);
268
277
  } catch (delErr) {
@@ -321,10 +330,14 @@ function parseLock(raw) {
321
330
  }
322
331
  }
323
332
  function isStale(payload, staleMs) {
324
- if (!isPidAlive(payload.pid, payload.host)) return true;
325
- const acquiredAt = Date.parse(payload.acquired_at);
326
- if (!Number.isFinite(acquiredAt)) return true;
327
- return Date.now() - acquiredAt > staleMs;
333
+ const pidRecorded = typeof payload.pid === "number" && Number.isInteger(payload.pid) && payload.pid > 0;
334
+ if (!pidRecorded) {
335
+ const acquiredAt = Date.parse(payload.acquired_at);
336
+ if (!Number.isFinite(acquiredAt)) return true;
337
+ return Date.now() - acquiredAt > staleMs;
338
+ }
339
+ if (isPidAlive(payload.pid, payload.host)) return false;
340
+ return true;
328
341
  }
329
342
  function isPidAlive(pid, host) {
330
343
  if (host !== (0, import_node_os.hostname)()) {
@@ -1731,6 +1744,8 @@ function gateFor(from, to) {
1731
1744
  }
1732
1745
 
1733
1746
  // sdk/state/index.ts
1747
+ var _moduleDir = typeof __dirname !== "undefined" ? __dirname : (0, import_node_path.dirname)(process.argv[1] || process.cwd());
1748
+ var _require = typeof require !== "undefined" ? require : (0, import_node_module.createRequire)(process.argv[1] || process.cwd());
1734
1749
  function _findPackageRoot(startDir) {
1735
1750
  let dir = (0, import_node_path.resolve)(startDir);
1736
1751
  let firstWithPkg = null;
@@ -1738,7 +1753,7 @@ function _findPackageRoot(startDir) {
1738
1753
  const pkgPath = (0, import_node_path.join)(dir, "package.json");
1739
1754
  if ((0, import_node_fs2.existsSync)(pkgPath)) {
1740
1755
  try {
1741
- const pkg = require(pkgPath);
1756
+ const pkg = _require(pkgPath);
1742
1757
  if (firstWithPkg === null) firstWithPkg = dir;
1743
1758
  if (pkg.name === "@hegemonart/get-design-done") return dir;
1744
1759
  } catch {
@@ -1755,7 +1770,7 @@ var _backendCache = null;
1755
1770
  function _loadBackend() {
1756
1771
  if (_backendCache !== null) return _backendCache === false ? null : _backendCache;
1757
1772
  try {
1758
- const pkgRoot = _findPackageRoot(__dirname);
1773
+ const pkgRoot = _findPackageRoot(_moduleDir);
1759
1774
  if (pkgRoot === null) {
1760
1775
  _backendCache = false;
1761
1776
  return null;
@@ -1765,7 +1780,7 @@ function _loadBackend() {
1765
1780
  _backendCache = false;
1766
1781
  return null;
1767
1782
  }
1768
- _backendCache = require(backendPath);
1783
+ _backendCache = _require(backendPath);
1769
1784
  return _backendCache;
1770
1785
  } catch {
1771
1786
  _backendCache = false;
@@ -1890,20 +1905,47 @@ async function transition(path2, toStage) {
1890
1905
  throw new TransitionGateFailed(toStage, gateResult.blockers);
1891
1906
  }
1892
1907
  const nowIso = (/* @__PURE__ */ new Date()).toISOString();
1893
- const nextState = await mutate(path2, (s) => {
1894
- s.frontmatter.stage = toStage;
1895
- s.frontmatter.last_checkpoint = nowIso;
1896
- s.position.stage = toStage;
1897
- s.timestamps[`${toStage}_started_at`] = nowIso;
1898
- return s;
1899
- });
1900
- return { pass: true, blockers: gateResult.blockers, state: nextState };
1908
+ let lockedFailure = null;
1909
+ let lockedBlockers = gateResult.blockers;
1910
+ try {
1911
+ const nextState = await mutate(path2, (s) => {
1912
+ const fromNow = s.position.stage;
1913
+ if (!isStage(fromNow)) {
1914
+ lockedFailure = new TransitionGateFailed(toStage, [
1915
+ `Invalid transition: from="${fromNow}" is not a recognized Stage (changed under lock)`
1916
+ ]);
1917
+ throw lockedFailure;
1918
+ }
1919
+ const gateNow = gateFor(fromNow, toStage);
1920
+ if (gateNow === null) {
1921
+ lockedFailure = new TransitionGateFailed(toStage, [
1922
+ `Invalid transition: ${fromNow} \u2192 ${toStage} (changed under lock)`
1923
+ ]);
1924
+ throw lockedFailure;
1925
+ }
1926
+ const resultNow = gateNow(s);
1927
+ if (!resultNow.pass) {
1928
+ lockedFailure = new TransitionGateFailed(toStage, resultNow.blockers);
1929
+ throw lockedFailure;
1930
+ }
1931
+ lockedBlockers = resultNow.blockers;
1932
+ s.frontmatter.stage = toStage;
1933
+ s.frontmatter.last_checkpoint = nowIso;
1934
+ s.position.stage = toStage;
1935
+ s.timestamps[`${toStage}_started_at`] = nowIso;
1936
+ return s;
1937
+ });
1938
+ return { pass: true, blockers: lockedBlockers, state: nextState };
1939
+ } catch (err) {
1940
+ if (lockedFailure !== null && err === lockedFailure) throw lockedFailure;
1941
+ throw err;
1942
+ }
1901
1943
  }
1902
1944
 
1903
1945
  // sdk/mcp/gdd-state/tools/shared.ts
1904
1946
  var import_node_path3 = __toESM(require("node:path"));
1905
1947
  var import_node_fs4 = require("node:fs");
1906
- var import_node_module2 = require("node:module");
1948
+ var import_node_module3 = require("node:module");
1907
1949
 
1908
1950
  // sdk/event-stream/index.ts
1909
1951
  var import_node_os2 = require("node:os");
@@ -1952,40 +1994,64 @@ var EventBus = class extends import_node_events.EventEmitter {
1952
1994
  // sdk/event-stream/writer.ts
1953
1995
  var import_node_fs3 = require("node:fs");
1954
1996
  var import_node_path2 = require("node:path");
1955
- var import_node_module = require("node:module");
1997
+ var import_node_module2 = require("node:module");
1956
1998
  function _findRepoRoot() {
1957
- let dir = process.cwd();
1958
- for (let i = 0; i < 8; i++) {
1999
+ return _walkToPackageJson(process.cwd());
2000
+ }
2001
+ function _walkToPackageJson(startDir) {
2002
+ let dir = startDir;
2003
+ for (let i = 0; i < 12; i++) {
1959
2004
  if ((0, import_node_fs3.existsSync)((0, import_node_path2.join)(dir, "package.json"))) return dir;
1960
2005
  const parent = (0, import_node_path2.dirname)(dir);
1961
2006
  if (parent === dir) break;
1962
2007
  dir = parent;
1963
2008
  }
1964
- return process.cwd();
2009
+ return startDir;
1965
2010
  }
1966
- var _redact;
1967
- try {
1968
- const _root = _findRepoRoot();
1969
- const _candidate = (0, import_node_path2.resolve)(_root, "scripts/lib/redact.cjs");
1970
- if ((0, import_node_fs3.existsSync)(_candidate)) {
1971
- const _redactRequire = (0, import_node_module.createRequire)((0, import_node_path2.join)(_root, "package.json"));
1972
- const _mod = _redactRequire(_candidate);
1973
- _redact = _mod.redact;
1974
- } else {
1975
- const _altRoot = (0, import_node_path2.resolve)(_root, "..", "..");
1976
- const _altCandidate = (0, import_node_path2.resolve)(_altRoot, "scripts/lib/redact.cjs");
1977
- if ((0, import_node_fs3.existsSync)(_altCandidate)) {
1978
- const _altRequire = (0, import_node_module.createRequire)((0, import_node_path2.join)(_altRoot, "package.json"));
1979
- const _altMod = _altRequire(_altCandidate);
1980
- _redact = _altMod.redact;
1981
- } else {
1982
- _redact = (v) => v;
2011
+ var _redactWarned = false;
2012
+ function _warnRedactUnavailable() {
2013
+ if (_redactWarned) return;
2014
+ _redactWarned = true;
2015
+ try {
2016
+ process.stderr.write(
2017
+ "[event-stream] WARNING: scripts/lib/redact.cjs could not be loaded \u2014 failing CLOSED: event payloads are dropped (envelope-only) to avoid writing unscrubbed secrets. Run the event writer from inside the plugin tree or set the redact lib on PATH to restore full payloads.\n"
2018
+ );
2019
+ } catch {
2020
+ }
2021
+ }
2022
+ function _loadRedact() {
2023
+ const candidates = [];
2024
+ const entry = process.argv[1];
2025
+ if (typeof entry === "string" && entry.length > 0) {
2026
+ const entryAbs = (0, import_node_path2.isAbsolute)(entry) ? entry : (0, import_node_path2.resolve)(entry);
2027
+ const entryRoot = _walkToPackageJson((0, import_node_path2.dirname)(entryAbs));
2028
+ candidates.push((0, import_node_path2.resolve)(entryRoot, "scripts/lib/redact.cjs"));
2029
+ }
2030
+ const repoRoot = _findRepoRoot();
2031
+ candidates.push((0, import_node_path2.resolve)(repoRoot, "scripts/lib/redact.cjs"));
2032
+ candidates.push((0, import_node_path2.resolve)(repoRoot, "..", "..", "scripts/lib/redact.cjs"));
2033
+ for (const candidate of candidates) {
2034
+ try {
2035
+ if (!(0, import_node_fs3.existsSync)(candidate)) continue;
2036
+ const req = (0, import_node_module2.createRequire)(candidate);
2037
+ const mod = req(candidate);
2038
+ if (mod && typeof mod.redact === "function") return mod.redact;
2039
+ } catch {
1983
2040
  }
1984
2041
  }
1985
- } catch {
1986
- _redact = (v) => v;
2042
+ return null;
1987
2043
  }
1988
- var redact = _redact;
2044
+ var _realRedact = _loadRedact();
2045
+ var redact = _realRedact !== null ? _realRedact : (v) => {
2046
+ _warnRedactUnavailable();
2047
+ if (v !== null && typeof v === "object") {
2048
+ const ev = v;
2049
+ const out = { ...ev };
2050
+ out["payload"] = { _redaction_unavailable: true };
2051
+ return out;
2052
+ }
2053
+ return { _redaction_unavailable: true };
2054
+ };
1989
2055
  var DEFAULT_EVENTS_PATH = ".design/telemetry/events.jsonl";
1990
2056
  var DEFAULT_MAX_LINE_BYTES = 64 * 1024;
1991
2057
  var EventWriter = class {
@@ -2132,7 +2198,7 @@ var _worktree = (() => {
2132
2198
  const root = _findRepoRoot2();
2133
2199
  const candidate = import_node_path3.default.resolve(root, "scripts/lib/worktree-resolve.cjs");
2134
2200
  if (!(0, import_node_fs4.existsSync)(candidate)) return null;
2135
- const req = (0, import_node_module2.createRequire)(import_node_path3.default.join(root, "package.json"));
2201
+ const req = (0, import_node_module3.createRequire)(import_node_path3.default.join(root, "package.json"));
2136
2202
  return req(candidate);
2137
2203
  } catch {
2138
2204
  return null;
@@ -82,8 +82,16 @@ async function acquire(path, opts) {
82
82
  // reads (EACCES/EPERM/EBUSY), and clearing under that condition
83
83
  // would let two writers race and lose increments.
84
84
  if (parsed !== null && isStale(parsed, staleMs)) {
85
+ // Audit D3 (TOCTOU): confirm the on-disk bytes STILL match the exact
86
+ // stale payload we just read before unlinking. If a different writer
87
+ // replaced the lock in the read→unlink window, do NOT unlink (that
88
+ // would steal a fresh lock); loop and re-evaluate the new holder.
89
+ const confirm = readLockSafe(lockPath);
90
+ if (confirm === null) continue; // already gone — race for wx-create
91
+ if (confirm !== existing) { await sleep(pollMs); continue; }
85
92
  // Clear stale lock; race-tolerant — if it's already gone we get
86
- // ENOENT, no-op.
93
+ // ENOENT, no-op. The wx-create below is atomic (O_CREAT|O_EXCL), so
94
+ // even if two waiters both unlink, only one wins the recreate.
87
95
  try { fs.unlinkSync(lockPath); } catch { /* ignore */ }
88
96
  continue;
89
97
  }
@@ -155,10 +163,23 @@ function parseLock(raw) {
155
163
  }
156
164
 
157
165
  function isStale(payload, staleMs) {
158
- if (!isPidAlive(payload.pid, payload.host)) return true;
159
- const t = Date.parse(payload.acquired_at);
160
- if (!Number.isFinite(t)) return true;
161
- return Date.now() - t > staleMs;
166
+ // Audit D3: PID-liveness is AUTHORITATIVE. A lock whose holder PID is still
167
+ // alive on this host is NEVER stale, regardless of age — a legitimate
168
+ // long-running mutation must not have its lock stolen. The age-based
169
+ // fallback applies ONLY when liveness cannot be confirmed: a dead PID, or a
170
+ // missing/invalid pid field. (isPidAlive conservatively reports alive for
171
+ // cross-host and unsignalable holders, so those are also never aged out.)
172
+ const pidRecorded =
173
+ typeof payload.pid === 'number' &&
174
+ Number.isInteger(payload.pid) &&
175
+ payload.pid > 0;
176
+ if (!pidRecorded) {
177
+ const t = Date.parse(payload.acquired_at);
178
+ if (!Number.isFinite(t)) return true;
179
+ return Date.now() - t > staleMs;
180
+ }
181
+ if (isPidAlive(payload.pid, payload.host)) return false;
182
+ return true;
162
183
  }
163
184
 
164
185
  function isPidAlive(pid, host) {
@@ -37,6 +37,33 @@ import {
37
37
  } from 'node:fs';
38
38
  import { dirname, join, resolve } from 'node:path';
39
39
  import { pathToFileURL } from 'node:url';
40
+ import { createRequire } from 'node:module';
41
+
42
+ // Audit D1: this .ts compiles to CommonJS via tsc (Node16 module mode), where
43
+ // `import.meta` is FORBIDDEN (error TS1470). But under Node's
44
+ // --experimental-strip-types the same file runs as ESM, where the CommonJS
45
+ // globals `require` and `__dirname` are UNDEFINED -- a bare reference to either
46
+ // THROWS a ReferenceError, so we cannot name them directly. We satisfy BOTH
47
+ // targets by probing with `typeof` (safe in ESM) and falling back to the entry
48
+ // script (`process.argv[1]`) when the CJS globals are absent. This mirrors the
49
+ // process.argv[1] anchoring used by sibling `sdk/event-stream/writer.ts` and
50
+ // avoids `import.meta` entirely.
51
+ // * `_require` -- a CJS-style require, used to load the optional .cjs backend
52
+ // (state-backend.cjs) and package.json files. In compiled CJS output the
53
+ // real `require` is used; under strip-types ESM we synthesize one anchored
54
+ // on the entry script via createRequire.
55
+ // * `_moduleDir` -- the walk-up anchor formerly spelled `__dirname`. In
56
+ // compiled CJS `__dirname` is used; under ESM we derive a directory from
57
+ // the entry script. Either way `_loadBackend` can resolve the optional
58
+ // native backend in BOTH compiled and source modes.
59
+ const _moduleDir: string =
60
+ typeof __dirname !== 'undefined'
61
+ ? __dirname
62
+ : dirname(process.argv[1] || process.cwd());
63
+ const _require =
64
+ typeof require !== 'undefined'
65
+ ? require
66
+ : createRequire(process.argv[1] || process.cwd());
40
67
 
41
68
  import { acquire, acquireSqliteLock } from './lockfile.ts';
42
69
  import { parse } from './parser.ts';
@@ -69,8 +96,8 @@ function _findPackageRoot(startDir: string): string | null {
69
96
  const pkgPath = join(dir, 'package.json');
70
97
  if (existsSync(pkgPath)) {
71
98
  try {
72
- // eslint-disable-next-line @typescript-eslint/no-require-imports
73
- const pkg = require(pkgPath) as { name?: string };
99
+ // D1: createRequire-bound require (bare `require` is undefined in ESM).
100
+ const pkg = _require(pkgPath) as { name?: string };
74
101
  if (firstWithPkg === null) firstWithPkg = dir;
75
102
  if (pkg.name === '@hegemonart/get-design-done') return dir;
76
103
  } catch {
@@ -85,9 +112,16 @@ function _findPackageRoot(startDir: string): string | null {
85
112
  }
86
113
 
87
114
  // ---------------------------------------------------------------------------
88
- // Phase 57: backend probe (loaded once via require, memoized).
89
- // state-backend.cjs is a CommonJS module; require() works from .ts under
90
- // Node 22 --experimental-strip-types (only type-erasable syntax is used).
115
+ // Phase 57: backend probe (loaded once via createRequire, memoized).
116
+ // state-backend.cjs is a CommonJS module. Audit D1 correction: a BARE
117
+ // `require()` does NOT work from this .ts under Node's --experimental-strip-
118
+ // types — this module is loaded as ESM, where `require` is undefined and a
119
+ // bare call throws ReferenceError (silently caught below, killing the backend
120
+ // path). We load via `_require` -- the real CJS `require` in compiled output,
121
+ // or a createRequire anchored on the entry script under strip-types ESM --
122
+ // which DOES resolve the optional .cjs backend in both modes. The graceful-null
123
+ // fallback is preserved for the genuinely-absent
124
+ // dependency (e.g. better-sqlite3 not installed).
91
125
  // ---------------------------------------------------------------------------
92
126
 
93
127
  interface StateBackendMod {
@@ -111,12 +145,13 @@ let _backendCache: StateBackendMod | null | false = null;
111
145
  function _loadBackend(): StateBackendMod | null {
112
146
  if (_backendCache !== null) return _backendCache === false ? null : _backendCache as StateBackendMod;
113
147
  try {
114
- const pkgRoot = _findPackageRoot(__dirname);
148
+ const pkgRoot = _findPackageRoot(_moduleDir);
115
149
  if (pkgRoot === null) { _backendCache = false; return null; }
116
150
  const backendPath = join(pkgRoot, 'scripts', 'lib', 'state', 'state-backend.cjs');
117
151
  if (!existsSync(backendPath)) { _backendCache = false; return null; }
118
- // eslint-disable-next-line @typescript-eslint/no-require-imports
119
- _backendCache = require(backendPath) as StateBackendMod;
152
+ // D1: createRequire-bound require so the optional native backend can
153
+ // actually load from this ESM (.ts strip-types) context.
154
+ _backendCache = _require(backendPath) as StateBackendMod;
120
155
  return _backendCache as StateBackendMod;
121
156
  } catch {
122
157
  _backendCache = false;
@@ -144,7 +179,7 @@ let _storeCache: StateStoreMod | null | false = null;
144
179
  async function _loadStore(): Promise<StateStoreMod | null> {
145
180
  if (_storeCache !== null) return _storeCache === false ? null : _storeCache as StateStoreMod;
146
181
  try {
147
- const pkgRoot = _findPackageRoot(__dirname);
182
+ const pkgRoot = _findPackageRoot(_moduleDir);
148
183
  if (pkgRoot === null) { _storeCache = false; return null; }
149
184
  const storePath = join(pkgRoot, 'scripts', 'lib', 'state', 'state-store.cjs');
150
185
  if (!existsSync(storePath)) { _storeCache = false; return null; }
@@ -420,12 +455,51 @@ export async function transition(
420
455
  throw new TransitionGateFailed(toStage, gateResult.blockers);
421
456
  }
422
457
  const nowIso: string = new Date().toISOString();
423
- const nextState = await mutate(path, (s): ParsedState => {
424
- s.frontmatter.stage = toStage;
425
- s.frontmatter.last_checkpoint = nowIso;
426
- s.position.stage = toStage;
427
- s.timestamps[`${toStage}_started_at`] = nowIso;
428
- return s;
429
- });
430
- return { pass: true, blockers: gateResult.blockers, state: nextState };
458
+ // Audit D4: the gate above was evaluated against a PRE-LOCK read. A
459
+ // concurrent stage change between that read and the locked mutate could make
460
+ // the transition invalid (e.g. another writer already advanced the stage, so
461
+ // `from` is no longer the current stage, or the gate's preconditions no
462
+ // longer hold). Re-evaluate the gate INSIDE the locked mutate against the
463
+ // freshly-read `s`, and abort the transition if it no longer holds. The
464
+ // mutate() lock serializes us against other writers, so this re-check is
465
+ // race-free: nothing can change `s` between this check and the write.
466
+ //
467
+ // We capture the locked re-check failure and re-throw it OUTSIDE mutate so
468
+ // the caller sees a TransitionGateFailed rather than a generic mutate error.
469
+ let lockedFailure: TransitionGateFailed | null = null;
470
+ let lockedBlockers: string[] = gateResult.blockers;
471
+ try {
472
+ const nextState = await mutate(path, (s): ParsedState => {
473
+ const fromNow: string = s.position.stage;
474
+ if (!isStage(fromNow)) {
475
+ lockedFailure = new TransitionGateFailed(toStage, [
476
+ `Invalid transition: from="${fromNow}" is not a recognized Stage (changed under lock)`,
477
+ ]);
478
+ throw lockedFailure;
479
+ }
480
+ const gateNow = gateFor(fromNow, toStage);
481
+ if (gateNow === null) {
482
+ lockedFailure = new TransitionGateFailed(toStage, [
483
+ `Invalid transition: ${fromNow} → ${toStage} (changed under lock)`,
484
+ ]);
485
+ throw lockedFailure;
486
+ }
487
+ const resultNow = gateNow(s);
488
+ if (!resultNow.pass) {
489
+ lockedFailure = new TransitionGateFailed(toStage, resultNow.blockers);
490
+ throw lockedFailure;
491
+ }
492
+ lockedBlockers = resultNow.blockers;
493
+ s.frontmatter.stage = toStage;
494
+ s.frontmatter.last_checkpoint = nowIso;
495
+ s.position.stage = toStage;
496
+ s.timestamps[`${toStage}_started_at`] = nowIso;
497
+ return s;
498
+ });
499
+ return { pass: true, blockers: lockedBlockers, state: nextState };
500
+ } catch (err) {
501
+ // If the in-lock re-check vetoed, surface the gate failure verbatim.
502
+ if (lockedFailure !== null && err === lockedFailure) throw lockedFailure;
503
+ throw err;
504
+ }
431
505
  }