@hegemonart/get-design-done 1.59.7 → 1.59.9

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 +59 -0
  4. package/README.md +2 -2
  5. package/SKILL.md +1 -1
  6. package/agents/design-authority-watcher.md +24 -5
  7. package/bin/gdd-graph +4 -1
  8. package/hooks/_hook-emit.js +113 -29
  9. package/hooks/budget-enforcer.ts +104 -5
  10. package/hooks/gdd-mcp-circuit-breaker.js +72 -3
  11. package/hooks/gdd-sessionstart-recap.js +23 -14
  12. package/hooks/hooks.json +2 -2
  13. package/package.json +2 -2
  14. package/reference/bandit-integration.md +13 -2
  15. package/reference/prices/claude.md +11 -0
  16. package/reference/runtime-models.md +9 -9
  17. package/reference/schemas/generated.d.ts +4 -0
  18. package/reference/schemas/runtime-models.schema.json +5 -0
  19. package/scripts/bootstrap.cjs +40 -8
  20. package/scripts/install.cjs +23 -1
  21. package/scripts/lib/bandit-router.cjs +47 -5
  22. package/scripts/lib/budget-enforcer.cjs +34 -5
  23. package/scripts/lib/detect/cli.cjs +13 -3
  24. package/scripts/lib/install/converters/cursor.cjs +11 -19
  25. package/scripts/lib/install/installer.cjs +72 -21
  26. package/scripts/lib/install/merge.cjs +31 -3
  27. package/scripts/lib/install/parse-runtime-models.cjs +9 -1
  28. package/scripts/lib/install/runtime-artifact-layout.cjs +42 -8
  29. package/scripts/lib/manifest/harnesses.json +29 -1
  30. package/scripts/lib/manifest/skills.json +1 -1
  31. package/scripts/lib/model-id.cjs +141 -0
  32. package/scripts/lib/session-runner/index.ts +87 -16
  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 +132 -55
  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
@@ -24,9 +24,10 @@
24
24
  * `degraded[]` (so gsd-health + the TUI can surface what is missing).
25
25
  * - Absent .design entirely -> every data section null/[] + degraded
26
26
  * populated, still no throw.
27
- * - Root resolution: opts.root || GDD_PROJECT_ROOT || package-root walk-up
28
- * || cwd. (Package-root walk-up resolves the GDD repo root, where
29
- * .design/.planning live.)
27
+ * - Root resolution: opts.root || GDD_PROJECT_ROOT || cwd marker walk-up
28
+ * (.design/.planning/.claude-plugin) || package-root walk-up. The cwd
29
+ * marker walk resolves the USER's project; package-root is a last-resort
30
+ * fallback (it would otherwise always pin the installed plugin's own dir).
30
31
  * - The .ts libs (sdk/state, sdk/event-stream) cannot be static-require()d
31
32
  * from a .cjs — they are loaded via dynamic import(pathToFileURL),
32
33
  * memoized once per process. The .cjs libs are require()d directly via the
@@ -100,15 +101,59 @@ function importEventStream() {
100
101
  // ---------------------------------------------------------------------------
101
102
  // Root resolution
102
103
  // ---------------------------------------------------------------------------
104
+ /**
105
+ * Walk UP from `startCwd` looking for a GDD project marker — `.design/` OR
106
+ * `.planning/` OR `.claude-plugin/plugin.json`. First match wins; returns the
107
+ * directory that holds the marker, or null if none is found before the
108
+ * filesystem root.
109
+ *
110
+ * This mirrors `resolveProjectRoot()` in
111
+ * `sdk/mcp/gdd-mcp/tools/shared.ts` (the canonical D-05 marker walk) so the
112
+ * dashboard resolves the USER's project, not the plugin's own install dir.
113
+ *
114
+ * @param {string} [startCwd]
115
+ * @returns {string|null}
116
+ */
117
+ function cwdMarkerWalk(startCwd = process.cwd()) {
118
+ let dir = path.resolve(startCwd);
119
+ // Bound the climb defensively (deep trees / odd mounts).
120
+ for (let i = 0; i < 64; i++) {
121
+ if (
122
+ fs.existsSync(path.join(dir, '.design')) ||
123
+ fs.existsSync(path.join(dir, '.planning')) ||
124
+ fs.existsSync(path.join(dir, '.claude-plugin', 'plugin.json'))
125
+ ) {
126
+ return dir;
127
+ }
128
+ const parent = path.dirname(dir);
129
+ if (parent === dir) return null; // reached filesystem root
130
+ dir = parent;
131
+ }
132
+ return null;
133
+ }
134
+
103
135
  /**
104
136
  * Resolve the project root the dashboard reads from:
105
- * opts.root || GDD_PROJECT_ROOT (env) || package-root walk-up || cwd.
137
+ * opts.root || GDD_PROJECT_ROOT (env) || cwd marker walk-up || package-root.
138
+ *
139
+ * D2 fix: `packageRoot()` ALWAYS succeeds (it walks up from __dirname and
140
+ * lands on the installed plugin's own package.json, e.g.
141
+ * node_modules/@hegemonart/get-design-done), so when it preceded the cwd
142
+ * resolution an installed `gdd-dashboard` always showed the PLUGIN's own
143
+ * (empty) data instead of the user's project. We now resolve the user's
144
+ * project FIRST via a cwd-upward marker walk (the same D-05 algorithm the
145
+ * gdd-mcp tools use), and only fall back to the package root as a last
146
+ * resort. Running the dashboard INSIDE the gdd repo still works — the marker
147
+ * walk finds the repo's own .design/.planning, which IS the project root.
148
+ *
106
149
  * @param {{root?: string}} [opts]
107
150
  * @returns {string}
108
151
  */
109
152
  function resolveRoot(opts = {}) {
110
153
  if (opts.root) return path.resolve(opts.root);
111
154
  if (process.env.GDD_PROJECT_ROOT) return path.resolve(process.env.GDD_PROJECT_ROOT);
155
+ const fromCwd = cwdMarkerWalk();
156
+ if (fromCwd) return fromCwd;
112
157
  try {
113
158
  return packageRoot();
114
159
  } catch {
@@ -610,6 +655,7 @@ module.exports = {
610
655
  loadDashboardModel,
611
656
  // Exposed for tests + sibling reuse (executors D/F may want the scrapers).
612
657
  resolveRoot,
658
+ cwdMarkerWalk,
613
659
  scrapeStateFile,
614
660
  scrapeEventsFile,
615
661
  };
@@ -38,46 +38,128 @@ import type { BaseEvent } from './types.ts';
38
38
  // anchor createRequire on the repo-root package.json discovered by
39
39
  // walking up from `process.cwd()`.
40
40
  function _findRepoRoot(): string {
41
- let dir = process.cwd();
42
- for (let i = 0; i < 8; i++) {
41
+ return _walkToPackageJson(process.cwd());
42
+ }
43
+
44
+ /**
45
+ * Walk up from `startDir` until a directory containing `package.json` is
46
+ * found; returns `startDir` itself if none is found within the bound.
47
+ */
48
+ function _walkToPackageJson(startDir: string): string {
49
+ let dir = startDir;
50
+ for (let i = 0; i < 12; i++) {
43
51
  if (existsSync(join(dir, 'package.json'))) return dir;
44
52
  const parent = dirname(dir);
45
53
  if (parent === dir) break;
46
54
  dir = parent;
47
55
  }
48
- return process.cwd();
56
+ return startDir;
57
+ }
58
+
59
+ // S2 fix: redaction now fails CLOSED. Previously, if redact.cjs could not be
60
+ // resolved from the runtime cwd, `_redact` fell through to the IDENTITY
61
+ // function and every event was written UNSCRUBBED — silently leaking secrets
62
+ // into events.jsonl whenever the writer ran outside the plugin tree (hook
63
+ // subprocesses, temp test dirs, unusual install layouts). That is a fail-open
64
+ // security hole.
65
+ //
66
+ // New contract:
67
+ // * redact.cjs loads → normal deep-walk scrubbing (unchanged behavior).
68
+ // * redact.cjs MISSING → fail closed: `redact` returns an envelope-only
69
+ // placeholder that DROPS the payload body (replacing it with
70
+ // `{ _redaction_unavailable: true }`) so no raw payload is ever persisted
71
+ // unscrubbed. A single visible stderr warning is emitted (once per
72
+ // process, guarded by `_redactWarned`) so the failure is observable.
73
+ //
74
+ // Resolution is also improved: we try createRequire anchored on THIS module
75
+ // (via the runtime-resolved module path) before the cwd-anchored walk, so it
76
+ // loads in more layouts.
77
+
78
+ /** Module-level guard so the fail-closed warning prints at most once. */
79
+ let _redactWarned = false;
80
+
81
+ /** Emit the one-time fail-closed warning to stderr (guarded, best-effort). */
82
+ function _warnRedactUnavailable(): void {
83
+ if (_redactWarned) return;
84
+ _redactWarned = true;
85
+ try {
86
+ process.stderr.write(
87
+ '[event-stream] WARNING: scripts/lib/redact.cjs could not be loaded — ' +
88
+ 'failing CLOSED: event payloads are dropped (envelope-only) to avoid ' +
89
+ 'writing unscrubbed secrets. Run the event writer from inside the ' +
90
+ 'plugin tree or set the redact lib on PATH to restore full payloads.\n',
91
+ );
92
+ } catch {
93
+ // If stderr itself is broken we have no recourse; swallow.
94
+ }
49
95
  }
50
96
 
51
- // Soft load: if redact.cjs is unreachable from the runtime cwd (e.g. a
52
- // hook subprocess running in a temp test dir three directories above
53
- // the plugin root), fall through to the identity function. The writer
54
- // keeps working — events just aren't scrubbed in that environment.
55
- // Production callers always run from inside the plugin tree.
56
- let _redact: (v: unknown) => unknown;
57
- try {
58
- const _root = _findRepoRoot();
59
- const _candidate = resolve(_root, 'scripts/lib/redact.cjs');
60
- if (existsSync(_candidate)) {
61
- const _redactRequire = createRequire(join(_root, 'package.json'));
62
- const _mod = _redactRequire(_candidate) as { redact: (v: unknown) => unknown };
63
- _redact = _mod.redact;
64
- } else {
65
- // Fallback: also try walking up from this source file's logical
66
- // position (3 dirs above writer.ts repo root).
67
- const _altRoot = resolve(_root, '..', '..');
68
- const _altCandidate = resolve(_altRoot, 'scripts/lib/redact.cjs');
69
- if (existsSync(_altCandidate)) {
70
- const _altRequire = createRequire(join(_altRoot, 'package.json'));
71
- const _altMod = _altRequire(_altCandidate) as { redact: (v: unknown) => unknown };
72
- _redact = _altMod.redact;
73
- } else {
74
- _redact = (v) => v;
97
+ /**
98
+ * Attempt to load redact.cjs from a set of candidate roots. Returns the
99
+ * real `redact` function on success, or `null` if no candidate resolves.
100
+ */
101
+ function _loadRedact(): ((v: unknown) => unknown) | null {
102
+ const candidates: string[] = [];
103
+
104
+ // We cannot use `import.meta.url` (tsc Node16 classifies this .ts as CJS for
105
+ // typecheck), so we probe several anchors so redact.cjs loads in as many
106
+ // layouts as possible BEFORE the fail-closed path engages:
107
+ //
108
+ // 1) The entry script (`process.argv[1]`). For hook subprocesses (e.g.
109
+ // budget-enforcer.ts spawned by the harness) the entry script lives
110
+ // INSIDE the plugin tree even when cwd is a detached temp dir — this is
111
+ // the same anchor the hook itself uses via resolveHookPath(). Walking up
112
+ // from the entry script to its package.json reliably lands on the plugin
113
+ // root regardless of cwd.
114
+ // 2) A cwd-walked repo root (works when cwd IS inside the plugin tree).
115
+ // 3) The source-relative layout (writer.ts → ../../scripts/lib/redact.cjs).
116
+ const entry = process.argv[1];
117
+ if (typeof entry === 'string' && entry.length > 0) {
118
+ const entryAbs = isAbsolute(entry) ? entry : resolve(entry);
119
+ const entryRoot = _walkToPackageJson(dirname(entryAbs));
120
+ candidates.push(resolve(entryRoot, 'scripts/lib/redact.cjs'));
121
+ }
122
+ const repoRoot = _findRepoRoot();
123
+ candidates.push(resolve(repoRoot, 'scripts/lib/redact.cjs'));
124
+ candidates.push(resolve(repoRoot, '..', '..', 'scripts/lib/redact.cjs'));
125
+
126
+ for (const candidate of candidates) {
127
+ try {
128
+ if (!existsSync(candidate)) continue;
129
+ // Anchor createRequire on the candidate file itself so resolution does
130
+ // not depend on a package.json being present at a particular ancestor.
131
+ const req = createRequire(candidate);
132
+ const mod = req(candidate) as { redact?: (v: unknown) => unknown };
133
+ if (mod && typeof mod.redact === 'function') return mod.redact;
134
+ } catch {
135
+ // Try the next candidate.
75
136
  }
76
137
  }
77
- } catch {
78
- _redact = (v) => v;
138
+ return null;
79
139
  }
80
- const redact = _redact;
140
+
141
+ const _realRedact = _loadRedact();
142
+
143
+ /**
144
+ * The redaction function used at the write boundary. When the real redactor
145
+ * loaded, this is it. When it did not, this is the FAIL-CLOSED shim: it warns
146
+ * once and returns an envelope-only object with the payload body dropped.
147
+ */
148
+ const redact: (v: unknown) => unknown =
149
+ _realRedact !== null
150
+ ? _realRedact
151
+ : (v: unknown): unknown => {
152
+ _warnRedactUnavailable();
153
+ if (v !== null && typeof v === 'object') {
154
+ // Preserve envelope metadata; drop the payload body entirely so no
155
+ // raw (potentially secret-bearing) content is persisted.
156
+ const ev = v as Record<string, unknown>;
157
+ const out: Record<string, unknown> = { ...ev };
158
+ out['payload'] = { _redaction_unavailable: true };
159
+ return out;
160
+ }
161
+ return { _redaction_unavailable: true };
162
+ };
81
163
 
82
164
  /** Default relative path for the persisted event stream. */
83
165
  export const DEFAULT_EVENTS_PATH = '.design/telemetry/events.jsonl';
@@ -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.