@opengsd/gsd-core 1.5.0-rc.4 → 1.5.0

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 (89) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/LICENSE +1 -1
  3. package/agents/gsd-executor.md +22 -0
  4. package/agents/gsd-phase-researcher.md +1 -1
  5. package/agents/gsd-planner.md +4 -0
  6. package/agents/gsd-project-researcher.md +1 -1
  7. package/agents/gsd-verifier.md +45 -13
  8. package/bin/install.js +56 -100
  9. package/commands/gsd/autonomous.md +1 -1
  10. package/commands/gsd/execute-phase.md +1 -1
  11. package/commands/gsd/plan-phase.md +1 -1
  12. package/gemini-extension.json +1 -1
  13. package/gsd-core/bin/gsd-tools.cjs +85 -53
  14. package/gsd-core/bin/lib/agent-command-router.cjs +2 -2
  15. package/gsd-core/bin/lib/agent-install-check.cjs +143 -0
  16. package/gsd-core/bin/lib/audit-command-router.cjs +4 -4
  17. package/gsd-core/bin/lib/capability-activation.cjs +37 -10
  18. package/gsd-core/bin/lib/capability-registry.cjs +2 -0
  19. package/gsd-core/bin/lib/capability-state.cjs +80 -30
  20. package/gsd-core/bin/lib/capability-writer.cjs +5 -4
  21. package/gsd-core/bin/lib/check-command-router.cjs +50 -51
  22. package/gsd-core/bin/lib/commands.cjs +20 -2
  23. package/gsd-core/bin/lib/config-loader.cjs +3 -4
  24. package/gsd-core/bin/lib/config-schema.cjs +1 -1
  25. package/gsd-core/bin/lib/config-types.cjs +2 -1
  26. package/gsd-core/bin/lib/config.cjs +5 -2
  27. package/gsd-core/bin/lib/decisions.cjs +19 -1
  28. package/gsd-core/bin/lib/docs.cjs +14 -2
  29. package/gsd-core/bin/lib/frontmatter.cjs +2 -2
  30. package/gsd-core/bin/lib/gap-checker.cjs +5 -2
  31. package/gsd-core/bin/lib/git-base-branch.cjs +27 -1
  32. package/gsd-core/bin/lib/graphify-command-router.cjs +6 -8
  33. package/gsd-core/bin/lib/graphify.cjs +7 -31
  34. package/gsd-core/bin/lib/gsd2-import.cjs +2 -2
  35. package/gsd-core/bin/lib/init.cjs +30 -4
  36. package/gsd-core/bin/lib/intel-command-router.cjs +6 -3
  37. package/gsd-core/bin/lib/intel.cjs +28 -34
  38. package/gsd-core/bin/lib/io.cjs +2 -4
  39. package/gsd-core/bin/lib/learnings.cjs +2 -2
  40. package/gsd-core/bin/lib/loop-resolver.cjs +45 -167
  41. package/gsd-core/bin/lib/milestone.cjs +13 -5
  42. package/gsd-core/bin/lib/model-resolver.cjs +3 -4
  43. package/gsd-core/bin/lib/phase-id.cjs +3 -5
  44. package/gsd-core/bin/lib/phase-locator.cjs +3 -6
  45. package/gsd-core/bin/lib/phase.cjs +59 -13
  46. package/gsd-core/bin/lib/probe-core.cjs +40 -11
  47. package/gsd-core/bin/lib/profile-output.cjs +5 -2
  48. package/gsd-core/bin/lib/prohibition-enforcement.cjs +660 -0
  49. package/gsd-core/bin/lib/roadmap-command-router.cjs +2 -2
  50. package/gsd-core/bin/lib/roadmap-parser.cjs +28 -25
  51. package/gsd-core/bin/lib/roadmap.cjs +9 -4
  52. package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +4 -1
  53. package/gsd-core/bin/lib/runtime-hooks-surface.cjs +24 -3
  54. package/gsd-core/bin/lib/state.cjs +75 -17
  55. package/gsd-core/bin/lib/task-command-router.cjs +2 -2
  56. package/gsd-core/bin/lib/teams-status.cjs +74 -0
  57. package/gsd-core/bin/lib/template.cjs +11 -2
  58. package/gsd-core/bin/lib/uat.cjs +64 -2
  59. package/gsd-core/bin/lib/verification.cjs +8 -5
  60. package/gsd-core/bin/lib/verify.cjs +311 -4
  61. package/gsd-core/bin/lib/workstream-inventory.cjs +2 -2
  62. package/gsd-core/bin/lib/workstream.cjs +8 -2
  63. package/gsd-core/bin/lib/worktree-safety.cjs +44 -3
  64. package/gsd-core/bin/shared/config-schema.manifest.json +2 -0
  65. package/gsd-core/references/planner-antipatterns.md +46 -0
  66. package/gsd-core/references/planning-config.md +5 -1
  67. package/gsd-core/references/prohibition-probe.md +80 -2
  68. package/gsd-core/references/worktree-branch-check.md +11 -5
  69. package/gsd-core/templates/verification-report.md +16 -3
  70. package/gsd-core/workflows/docs-update.md +23 -31
  71. package/gsd-core/workflows/execute-phase/steps/worktree-recovery-policy.md +9 -0
  72. package/gsd-core/workflows/execute-phase.md +7 -2
  73. package/gsd-core/workflows/map-codebase.md +8 -10
  74. package/gsd-core/workflows/plan-phase.md +6 -0
  75. package/gsd-core/workflows/quick.md +26 -2
  76. package/gsd-core/workflows/settings-advanced.md +5 -5
  77. package/gsd-core/workflows/settings-integrations.md +5 -5
  78. package/gsd-core/workflows/spec-phase.md +30 -2
  79. package/gsd-core/workflows/verify-phase.md +22 -7
  80. package/hooks/dist/gsd-worktree-path-guard.js +34 -18
  81. package/hooks/gsd-worktree-path-guard.js +34 -18
  82. package/package.json +3 -3
  83. package/scripts/ci-prepare-test-scope.cjs +56 -14
  84. package/scripts/diff-touches-shipped-paths.cjs +5 -11
  85. package/scripts/gen-capability-registry.cjs +27 -1
  86. package/scripts/lint-allow-test-rule-refs.allowlist.json +0 -2
  87. package/scripts/research-profiles.cjs +2 -2
  88. package/scripts/run-tests.cjs +1 -0
  89. package/gsd-core/bin/lib/core.cjs +0 -345
@@ -55,32 +55,59 @@ function _readRawConfigKey(filePath, dotKey) {
55
55
  return { found: false, value: undefined };
56
56
  }
57
57
  }
58
- function _resolveActivationValue(dotKey, config, cwd, registry) {
58
+ /**
59
+ * Resolve the raw value for a dotted config key using the four-level precedence
60
+ * walk. Returns { found, value } with the RAW value (not coerced to boolean),
61
+ * so callers can decide how to interpret the value (boolean gate vs. raw config
62
+ * value for numeric/string settings like security_asvs_level).
63
+ *
64
+ * Precedence (mirrors _resolveActivationValue):
65
+ * 1. loadConfig result (config arg) — guarded nested-lookup.
66
+ * 2. Workstream config.json at planningDir(cwd)/config.json.
67
+ * 3. Root config.json at planningRoot(cwd)/config.json (only if path differs).
68
+ * 4. registry.configSchema[dotKey].default — schema default.
69
+ * 5. Absent → { found: false, value: undefined }.
70
+ */
71
+ function resolveConfigKey(dotKey, opts) {
72
+ const { config, cwd, registry } = opts;
73
+ // Level 1: loadConfig result
59
74
  const fromConfig = _getNestedConfigValue(config, dotKey);
60
75
  if (fromConfig.found)
61
- return Boolean(fromConfig.value);
76
+ return { found: true, value: fromConfig.value };
77
+ // Level 2 + 3: raw config.json files (only when cwd is available)
62
78
  if (cwd) {
63
79
  const wsConfigPath = node_path_1.default.join(planningDir(cwd), 'config.json');
64
80
  const rootConfigPath = node_path_1.default.join(planningRoot(cwd), 'config.json');
65
81
  const fromWs = _readRawConfigKey(wsConfigPath, dotKey);
66
82
  if (fromWs.found)
67
- return Boolean(fromWs.value);
83
+ return { found: true, value: fromWs.value };
68
84
  if (wsConfigPath !== rootConfigPath) {
69
85
  const fromRoot = _readRawConfigKey(rootConfigPath, dotKey);
70
86
  if (fromRoot.found)
71
- return Boolean(fromRoot.value);
87
+ return { found: true, value: fromRoot.value };
72
88
  }
73
89
  }
74
- const schemaEntry = registry['configSchema']?.[dotKey];
75
- if (schemaEntry && typeof schemaEntry === 'object' && schemaEntry !== null) {
76
- const def = schemaEntry['default'];
77
- if (def !== undefined)
78
- return Boolean(def);
90
+ // Level 4: registry configSchema default
91
+ const schemaMap = registry['configSchema'];
92
+ if (schemaMap && typeof schemaMap === 'object' && !Array.isArray(schemaMap)
93
+ && Object.prototype.hasOwnProperty.call(schemaMap, dotKey)) {
94
+ const schemaEntry = schemaMap[dotKey];
95
+ if (schemaEntry && typeof schemaEntry === 'object' && schemaEntry !== null) {
96
+ const def = schemaEntry['default'];
97
+ if (def !== undefined)
98
+ return { found: true, value: def };
99
+ }
79
100
  }
80
- return false;
101
+ // Level 5: absent
102
+ return { found: false, value: undefined };
103
+ }
104
+ function _resolveActivationValue(dotKey, config, cwd, registry) {
105
+ const r = resolveConfigKey(dotKey, { config, cwd, registry });
106
+ return r.found ? Boolean(r.value) : false;
81
107
  }
82
108
  module.exports = {
83
109
  _getNestedConfigValue,
84
110
  _readRawConfigKey,
85
111
  _resolveActivationValue,
112
+ resolveConfigKey,
86
113
  };
@@ -762,6 +762,7 @@ const capabilities = {
762
762
  "graphify"
763
763
  ],
764
764
  "agents": [],
765
+ "activationKey": "graphify.enabled",
765
766
  "config": {
766
767
  "graphify.enabled": {
767
768
  "type": "boolean",
@@ -845,6 +846,7 @@ const capabilities = {
845
846
  },
846
847
  "skills": [],
847
848
  "agents": [],
849
+ "activationKey": "intel.enabled",
848
850
  "config": {
849
851
  "intel.enabled": {
850
852
  "type": "boolean",
@@ -18,14 +18,15 @@
18
18
  * pure, config-only resolution with no filesystem I/O.
19
19
  * cmdCapabilityState is the I/O handler.
20
20
  *
21
- * Dependencies (leaf modules only — no core.cjs circular risk):
21
+ * Dependencies (leaf modules only — no circular risk):
22
22
  * - node:path
23
- * - ./core.cjs (output, error)
23
+ * - ./io.cjs (output, error)
24
24
  * - ./capability-activation.cjs (_resolveActivationValue)
25
25
  * - ./install-profiles.cjs (readActiveProfile, loadSkillsManifest, resolveProfile)
26
26
  * - ./surface.cjs (resolveSurface)
27
27
  * - ./config-loader.cjs (loadConfig)
28
28
  * - ./runtime-homes.cjs (getGlobalConfigDir — for runtimeConfigDir auto-detection)
29
+ * - ./runtime-slash.cjs (resolveRuntime — GSD_RUNTIME > config.runtime > 'claude' precedence)
29
30
  * - capability-registry.cjs (loaded at call time)
30
31
  */
31
32
  var __importDefault = (this && this.__importDefault) || function (mod) {
@@ -34,8 +35,8 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
34
35
  const node_path_1 = __importDefault(require("node:path"));
35
36
  const node_fs_1 = __importDefault(require("node:fs"));
36
37
  // eslint-disable-next-line @typescript-eslint/no-require-imports
37
- const core = require("./core.cjs");
38
- const { output: coreOutput, error: coreError } = core;
38
+ const ioMod = require("./io.cjs");
39
+ const { output: coreOutput, error: coreError } = ioMod;
39
40
  // eslint-disable-next-line @typescript-eslint/no-require-imports
40
41
  const activationMod = require("./capability-activation.cjs");
41
42
  const { _resolveActivationValue } = activationMod;
@@ -135,6 +136,19 @@ function resolveCapabilityState(input) {
135
136
  surfaced = skills.every((s) => surfacedSkills.has(s));
136
137
  }
137
138
  const enabled = installed && surfaced;
139
+ // ── per-capability config activation ──────────────────────────────────────
140
+ // Resolve the capability's own activationKey (if present). This is the
141
+ // config-level toggle that gates the whole capability — separate from the
142
+ // per-hook `when` keys that gate individual hooks. When activationKey is
143
+ // absent, configActivation defaults to true (no config gate on the cap).
144
+ // active = enabled && configActivation (enabled unchanged: installed && surfaced)
145
+ const activationKey = typeof capObj['activationKey'] === 'string' && capObj['activationKey'].length > 0
146
+ ? capObj['activationKey']
147
+ : undefined;
148
+ const configActivation = activationKey !== undefined
149
+ ? _resolveActivationValue(activationKey, config, cwd, registry)
150
+ : true;
151
+ const active = enabled && configActivation;
138
152
  // ── hooks ──────────────────────────────────────────────────────────────────
139
153
  // Collect from steps, gates, contributions. Each may have a `when` key.
140
154
  // Activation semantics (mirrors loop-resolver.isActive exactly):
@@ -165,7 +179,12 @@ function resolveCapabilityState(input) {
165
179
  // (mirrors loop-resolver.isActive: `typeof when !== 'string' || when.length === 0` → false)
166
180
  configured = false;
167
181
  }
168
- hooks.push({ point, kind, when: whenRaw, configured, active: enabled && configured });
182
+ // Hook active = capability-level active AND hook's own config gate.
183
+ // The capability's `active` constant (= enabled && configActivation) is
184
+ // used here so that a config-disabled capability (active=false) cannot
185
+ // produce active hooks even when the hook's own `when` is unconditional
186
+ // (configured=true). The capability gate cascades to all its hooks.
187
+ hooks.push({ point, kind, when: whenRaw, configured, active: active && configured });
169
188
  }
170
189
  }
171
190
  const stepsRaw = capObj['steps'];
@@ -174,7 +193,7 @@ function resolveCapabilityState(input) {
174
193
  processHooks(Array.isArray(stepsRaw) ? stepsRaw : [], 'step');
175
194
  processHooks(Array.isArray(gatesRaw) ? gatesRaw : [], 'gate');
176
195
  processHooks(Array.isArray(contributionsRaw) ? contributionsRaw : [], 'contribution');
177
- results.push({ id: capId, tier, skills, installed, surfaced, enabled, hooks });
196
+ results.push({ id: capId, tier, skills, installed, surfaced, enabled, active, hooks });
178
197
  }
179
198
  // Deterministic sort by id for stable output across calls
180
199
  results.sort((a, b) => a.id < b.id ? -1 : a.id > b.id ? 1 : 0);
@@ -283,16 +302,20 @@ function _resolveManifest(commandsGsdDir, configDir) {
283
302
  }
284
303
  /**
285
304
  * Command entry point: resolve install profile, surface, and config; compute
286
- * capability state; emit the envelope via core.output.
305
+ * capability state; emit the envelope via io.output.
287
306
  *
288
307
  * Envelope: { runtimeConfigDir, warnings?: string[], capabilities: CapabilityStateEntry[] }
289
308
  *
290
309
  * runtimeConfigDir resolution (when not provided or empty):
291
- * Uses the canonical getGlobalConfigDir from runtime-homes.cjs to detect the
292
- * active runtime's config dir — the same resolver used by install.js. This
293
- * correctly handles all supported runtimes (claude, codex, cursor, gemini,
294
- * opencode, grok, etc.) and their env-var overrides. Defaults to claude
295
- * (falls back to ~/.claude) if the resolver throws.
310
+ * Detects the active runtime via the canonical precedence:
311
+ * process.env.GSD_RUNTIME → config.runtime → 'claude'
312
+ * (using resolveRuntime() from runtime-slash.cjs, the same precedence used
313
+ * by profile-output.cjs and the rest of the runtime resolution chain).
314
+ * Then calls getGlobalConfigDir(detectedRuntime) from runtime-homes.cjs —
315
+ * the same resolver used by install.js. This correctly handles all supported
316
+ * runtimes (claude, codex, cursor, gemini, opencode, grok, etc.) and their
317
+ * env-var overrides (CLAUDE_CONFIG_DIR, CODEX_HOME, CURSOR_CONFIG_DIR, …).
318
+ * Defaults to ~/.claude if either resolver throws.
296
319
  *
297
320
  * Failure surfacing: genuine resolution failures (manifest/profile/surface
298
321
  * errors) are reported in the `warnings` array in the envelope. The output
@@ -308,15 +331,18 @@ function _resolveManifest(commandsGsdDir, configDir) {
308
331
  * Providing a value without a next token (e.g. the flag
309
332
  * is last in argv with no following value) should be
310
333
  * caught by the caller before invoking this function.
311
- * @param raw Whether to emit raw JSON (core.output raw mode)
334
+ * @param raw Whether to emit raw JSON (io.output raw mode)
312
335
  * @param _options Reserved for future use
313
336
  */
314
- function resolveCapabilityRuntimeState(cwd, runtimeConfigDir) {
337
+ function resolveCapabilityRuntimeState(cwd, runtimeConfigDir, configOverride) {
315
338
  const warnings = [];
316
339
  // Resolve runtimeConfigDir using the canonical runtime-homes resolver.
317
- // When not provided, getGlobalConfigDir(runtime) is called with 'claude'
318
- // as the default runtime — the same fallback as install.js. The canonical
319
- // resolver handles all env-var overrides (CLAUDE_CONFIG_DIR, CODEX_HOME,
340
+ // When not provided, the active runtime is detected via the canonical
341
+ // precedence: process.env.GSD_RUNTIME → config.runtime → 'claude'
342
+ // (mirrors resolveRuntime() from runtime-slash.cjs and the precedence used
343
+ // by profile-output.cjs and the rest of the runtime resolution chain).
344
+ // getGlobalConfigDir(detectedRuntime) is then called, which honours the
345
+ // runtime-specific env-var override (CLAUDE_CONFIG_DIR, CODEX_HOME,
320
346
  // CURSOR_CONFIG_DIR, GROK_AGENTS_HOME, etc.) correctly and without
321
347
  // fabricating env vars that don't exist upstream.
322
348
  let resolvedConfigDir = runtimeConfigDir || '';
@@ -324,13 +350,13 @@ function resolveCapabilityRuntimeState(cwd, runtimeConfigDir) {
324
350
  try {
325
351
  // eslint-disable-next-line @typescript-eslint/no-require-imports
326
352
  const runtimeHomes = require('./runtime-homes.cjs');
327
- // Delegate runtime detection entirely to getGlobalConfigDir: calling it
328
- // with 'claude' causes it to check CLAUDE_CONFIG_DIR first, falling back
329
- // to ~/.claude. The canonical resolver already encodes the correct env-var
330
- // precedence for each runtime — we do not re-implement that logic here.
331
- // For non-claude runtimes, the caller should pass --config-dir explicitly
332
- // (or set the runtime-specific env var, which getGlobalConfigDir honors).
333
- resolvedConfigDir = runtimeHomes.getGlobalConfigDir('claude');
353
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
354
+ const runtimeSlash = require('./runtime-slash.cjs');
355
+ // Detect the active runtime via GSD_RUNTIME → config.runtime → 'claude'.
356
+ // resolveRuntime reads config.json directly (no side effects) and returns
357
+ // a lowercased canonical runtime name.
358
+ const detectedRuntime = runtimeSlash.resolveRuntime(cwd);
359
+ resolvedConfigDir = runtimeHomes.getGlobalConfigDir(detectedRuntime);
334
360
  }
335
361
  catch {
336
362
  // Defensive fallback: use ~/.claude if the canonical resolver throws.
@@ -392,12 +418,20 @@ function resolveCapabilityRuntimeState(cwd, runtimeConfigDir) {
392
418
  surfacedSkills = new Set();
393
419
  }
394
420
  // ── Load config ───────────────────────────────────────────────────────────────
421
+ // When the caller already holds a loadConfig snapshot (e.g. cmdLoopRenderHooks),
422
+ // accept it via configOverride so capability `active` and hook resolution
423
+ // share the SAME config object — single snapshot, no TOCTOU window.
395
424
  let config;
396
- try {
397
- config = loadConfig(cwd);
425
+ if (configOverride !== undefined) {
426
+ config = configOverride;
398
427
  }
399
- catch {
400
- config = {};
428
+ else {
429
+ try {
430
+ config = loadConfig(cwd);
431
+ }
432
+ catch {
433
+ config = {};
434
+ }
401
435
  }
402
436
  // ── Resolve state ────────────────────────────────────────────────────────────
403
437
  const result = resolveCapabilityState({
@@ -410,8 +444,6 @@ function resolveCapabilityRuntimeState(cwd, runtimeConfigDir) {
410
444
  return {
411
445
  runtimeConfigDir: resolvedConfigDir,
412
446
  warnings,
413
- registry,
414
- config,
415
447
  capabilities: result.capabilities,
416
448
  };
417
449
  }
@@ -431,9 +463,27 @@ function cmdCapabilityState(cwd, runtimeConfigDir, raw, _options = {}) {
431
463
  }
432
464
  coreOutput(envelope, raw);
433
465
  }
466
+ /**
467
+ * Convenience predicate: returns true if the capability identified by `capId`
468
+ * is active (installed && surfaced && config-enabled) in the current runtime
469
+ * environment at `cwd`.
470
+ *
471
+ * Internally calls `resolveCapabilityRuntimeState(cwd, undefined)` and returns
472
+ * the `active` field of the matching CapabilityStateEntry.
473
+ * Returns `false` when the capability is not found in the registry.
474
+ *
475
+ * @param capId Capability identifier (e.g. 'graphify', 'intel')
476
+ * @param cwd Project root directory for config resolution
477
+ */
478
+ function isCapabilityActive(capId, cwd) {
479
+ const result = resolveCapabilityRuntimeState(cwd, undefined);
480
+ const entry = result.capabilities.find((c) => c.id === capId);
481
+ return entry !== undefined ? entry.active : false;
482
+ }
434
483
  module.exports = {
435
484
  resolveCapabilityState,
436
485
  resolveCapabilityRuntimeState,
486
+ isCapabilityActive,
437
487
  cmdCapabilityState,
438
488
  // Exported for tests
439
489
  _resolveCommandsGsdDir,
@@ -14,7 +14,7 @@
14
14
  * - re-resolve: always calls resolveCapabilityRuntimeState for the return value
15
15
  *
16
16
  * Dependencies (leaf modules only — no circular risk):
17
- * - ./core.cjs (output, error)
17
+ * - ./io.cjs (output, error)
18
18
  * - ./capability-state.cjs (resolveCapabilityRuntimeState, _resolveManifest, _resolveCommandsGsdDir)
19
19
  * - ./surface.cjs (readSurface, writeSurface, applySurface)
20
20
  * - ./install-profiles.cjs (readActiveProfile)
@@ -23,8 +23,8 @@
23
23
  * - capability-registry.cjs (loaded at call time)
24
24
  */
25
25
  // eslint-disable-next-line @typescript-eslint/no-require-imports
26
- const core = require("./core.cjs");
27
- const { output: coreOutput, error: coreError } = core;
26
+ const ioMod = require("./io.cjs");
27
+ const { output: coreOutput, error: coreError } = ioMod;
28
28
  // eslint-disable-next-line @typescript-eslint/no-require-imports
29
29
  const capabilityStateMod = require("./capability-state.cjs");
30
30
  const { resolveCapabilityRuntimeState, _resolveManifest, _resolveCommandsGsdDir } = capabilityStateMod;
@@ -71,7 +71,8 @@ function setCapabilityState(cwd, runtimeConfigDir, desired, opts) {
71
71
  const before = resolveCapabilityRuntimeState(cwd, runtimeConfigDir);
72
72
  const resolvedConfigDir = before.runtimeConfigDir;
73
73
  // ── Load registry ─────────────────────────────────────────────────────────
74
- const registry = before.registry;
74
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
75
+ const registry = require('./capability-registry.cjs');
75
76
  const capabilitiesMap = (registry['capabilities'] && typeof registry['capabilities'] === 'object' && !Array.isArray(registry['capabilities'])
76
77
  ? registry['capabilities']
77
78
  : {});
@@ -13,8 +13,14 @@ const node_fs_1 = __importDefault(require("node:fs"));
13
13
  const node_path_1 = __importDefault(require("node:path"));
14
14
  const node_child_process_1 = require("node:child_process");
15
15
  // eslint-disable-next-line @typescript-eslint/no-require-imports
16
- const core = require("./core.cjs");
17
- const { output, error, ERROR_REASON } = core;
16
+ const io = require("./io.cjs");
17
+ const { output, error, ERROR_REASON } = io;
18
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
19
+ const planningWorkspaceMod = require("./planning-workspace.cjs");
20
+ const { planningDir } = planningWorkspaceMod;
21
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
22
+ const phaseLocatorMod = require("./phase-locator.cjs");
23
+ const { findPhaseInternal } = phaseLocatorMod;
18
24
  const decisions_cjs_1 = require("./decisions.cjs");
19
25
  const ui_safety_gate_cjs_1 = require("./ui-safety-gate.cjs");
20
26
  // eslint-disable-next-line @typescript-eslint/no-require-imports
@@ -26,6 +32,7 @@ const { getRoadmapPhaseWithFallback } = roadmapModule;
26
32
  // eslint-disable-next-line @typescript-eslint/no-require-imports
27
33
  const gapCheckerModule = require("./gap-checker.cjs");
28
34
  const { runGapAnalysis } = gapCheckerModule;
35
+ const prohibition_enforcement_cjs_1 = require("./prohibition-enforcement.cjs");
29
36
  // ─── Helpers ──────────────────────────────────────────────────────────────────
30
37
  function normalizePhrase(text) {
31
38
  // eslint-disable-next-line @typescript-eslint/no-base-to-string
@@ -341,7 +348,7 @@ function cmdDecisionCoverageVerify(projectDir, args, raw) {
341
348
  * Invocable as: gsd_run check ui-plan-gate <phase>
342
349
  *
343
350
  * Uses checkUiPresence from ui-safety-gate.cjs — does NOT reimplement frontend detection.
344
- * Uses getRoadmapPhaseInternal + findPhaseInternal from core.cjs for phase data.
351
+ * Uses getRoadmapPhaseWithFallback + findPhaseInternal from leaf modules for phase data.
345
352
  */
346
353
  function findUiSpecInDir(phaseDir) {
347
354
  if (!phaseDir || !node_fs_1.default.existsSync(phaseDir))
@@ -368,7 +375,7 @@ function findUiSpecInDir(phaseDir) {
368
375
  * surface the miss — we do NOT silently degrade to frontend:false if the roadmap
369
376
  * exists but the phase header is absent.
370
377
  * (b) Runs checkUiPresence (frontend detection) — no reimplementation.
371
- * (c) Resolves the phase directory via core.findPhaseInternal; checks for *-UI-SPEC.md.
378
+ * (c) Resolves the phase directory via findPhaseInternal (phase-locator.cjs); checks for *-UI-SPEC.md.
372
379
  *
373
380
  * Returns: { frontend, hasUiSpec, block, uiSpecPath, phaseLookupFailed }
374
381
  * block = frontend && !hasUiSpec
@@ -385,10 +392,8 @@ function computeUiPlanGate(projectDir, phase) {
385
392
  const section = getRoadmapPhaseWithFallback(projectDir, phase);
386
393
  if (section === null) {
387
394
  // Distinguish: ROADMAP.md missing (no-roadmap project) vs phase not found in ROADMAP.
388
- // core.planningDir(cwd) resolves the .planning/ root for workstream-aware paths.
389
- const planDir = typeof core['planningDir'] === 'function'
390
- ? core['planningDir'](projectDir)
391
- : node_path_1.default.join(projectDir, '.planning');
395
+ // planningDir(cwd) resolves the .planning/ root for workstream-aware paths.
396
+ const planDir = planningDir(projectDir);
392
397
  const roadmapPath = node_path_1.default.join(planDir, 'ROADMAP.md');
393
398
  if (node_fs_1.default.existsSync(roadmapPath)) {
394
399
  // ROADMAP.md exists but phase was not found → surface the miss
@@ -405,24 +410,20 @@ function computeUiPlanGate(projectDir, phase) {
405
410
  const presenceResult = (0, ui_safety_gate_cjs_1.checkUiPresence)(phaseSection);
406
411
  const frontend = presenceResult.hasUI;
407
412
  // (c) Resolve phase directory via findPhaseInternal and check for *-UI-SPEC.md
408
- const coreModule = core;
409
413
  let phaseDir = '';
410
414
  try {
411
- const findPhase = coreModule['findPhaseInternal'];
412
- if (typeof findPhase === 'function') {
413
- const result = findPhase(projectDir, phase);
414
- if (result && typeof result === 'object') {
415
- // findPhaseInternal returns { directory: '<relative-posix-path>', ... }
416
- // directory is relative to cwd — resolve it to absolute.
417
- const relDir = typeof result['directory'] === 'string' ? result['directory'] : '';
418
- if (relDir) {
419
- phaseDir = node_path_1.default.resolve(projectDir, relDir);
420
- }
421
- }
422
- else if (typeof result === 'string') {
423
- phaseDir = result;
415
+ const result = findPhaseInternal(projectDir, phase);
416
+ if (result && typeof result === 'object') {
417
+ // findPhaseInternal returns { directory: '<relative-posix-path>', ... }
418
+ // directory is relative to cwd — resolve it to absolute.
419
+ const relDir = typeof result['directory'] === 'string' ? result['directory'] : '';
420
+ if (relDir) {
421
+ phaseDir = node_path_1.default.resolve(projectDir, relDir);
424
422
  }
425
423
  }
424
+ else if (typeof result === 'string') {
425
+ phaseDir = result;
426
+ }
426
427
  }
427
428
  catch { /* phase dir lookup failure → hasUiSpec=false */ }
428
429
  const uiSpecPath = findUiSpecInDir(phaseDir);
@@ -475,7 +476,7 @@ const UI_PATH_PATTERNS_RE = /\/(components|pages|views|screens|layouts|ui|fronte
475
476
  * same lookup as computeUiPlanGate — to determine if this is a frontend phase.
476
477
  * (b) Runs checkUiPresence (frontend detection) — no reimplementation.
477
478
  * (c) Checks git diff HEAD~1..HEAD for UI file changes in the current worktree.
478
- * (d) Resolves the phase directory via core.findPhaseInternal; checks for *-UI-SPEC.md.
479
+ * (d) Resolves the phase directory via findPhaseInternal (phase-locator.cjs); checks for *-UI-SPEC.md.
479
480
  *
480
481
  * Returns: { frontend, hasUiFiles, hasUiSpec, block, message?, phaseLookupFailed? }
481
482
  * block = frontend && hasUiFiles && !hasUiSpec
@@ -488,9 +489,7 @@ function computeUiSafetyGate(projectDir, phase) {
488
489
  try {
489
490
  const section = getRoadmapPhaseWithFallback(projectDir, phase);
490
491
  if (section === null) {
491
- const planDir = typeof core['planningDir'] === 'function'
492
- ? core['planningDir'](projectDir)
493
- : node_path_1.default.join(projectDir, '.planning');
492
+ const planDir = planningDir(projectDir);
494
493
  const roadmapPath = node_path_1.default.join(planDir, 'ROADMAP.md');
495
494
  if (node_fs_1.default.existsSync(roadmapPath)) {
496
495
  phaseLookupFailed = true;
@@ -519,22 +518,18 @@ function computeUiSafetyGate(projectDir, phase) {
519
518
  }
520
519
  catch { /* git unavailable or no prior commit — treat as no UI files changed */ }
521
520
  // (d) Resolve phase directory and check for *-UI-SPEC.md (same as computeUiPlanGate)
522
- const coreModule = core;
523
521
  let phaseDir = '';
524
522
  try {
525
- const findPhase = coreModule['findPhaseInternal'];
526
- if (typeof findPhase === 'function') {
527
- const result = findPhase(projectDir, phase);
528
- if (result && typeof result === 'object') {
529
- const relDir = typeof result['directory'] === 'string' ? result['directory'] : '';
530
- if (relDir) {
531
- phaseDir = node_path_1.default.resolve(projectDir, relDir);
532
- }
533
- }
534
- else if (typeof result === 'string') {
535
- phaseDir = result;
523
+ const result = findPhaseInternal(projectDir, phase);
524
+ if (result && typeof result === 'object') {
525
+ const relDir = typeof result['directory'] === 'string' ? result['directory'] : '';
526
+ if (relDir) {
527
+ phaseDir = node_path_1.default.resolve(projectDir, relDir);
536
528
  }
537
529
  }
530
+ else if (typeof result === 'string') {
531
+ phaseDir = result;
532
+ }
538
533
  }
539
534
  catch { /* phase dir lookup failure → hasUiSpec=false */ }
540
535
  const uiSpecPath = findUiSpecInDir(phaseDir);
@@ -567,20 +562,16 @@ function cmdTddReviewCheckpoint(projectDir, args, raw) {
567
562
  return;
568
563
  }
569
564
  // Resolve phase directory
570
- const coreModule = core;
571
565
  let phaseDir = '';
572
566
  try {
573
- const findPhase = coreModule['findPhaseInternal'];
574
- if (typeof findPhase === 'function') {
575
- const result = findPhase(projectDir, phase);
576
- if (result && typeof result === 'object') {
577
- const relDir = typeof result['directory'] === 'string' ? result['directory'] : '';
578
- if (relDir)
579
- phaseDir = node_path_1.default.resolve(projectDir, relDir);
580
- }
581
- else if (typeof result === 'string') {
582
- phaseDir = result;
583
- }
567
+ const result = findPhaseInternal(projectDir, phase);
568
+ if (result && typeof result === 'object') {
569
+ const relDir = typeof result['directory'] === 'string' ? result['directory'] : '';
570
+ if (relDir)
571
+ phaseDir = node_path_1.default.resolve(projectDir, relDir);
572
+ }
573
+ else if (typeof result === 'string') {
574
+ phaseDir = result;
584
575
  }
585
576
  }
586
577
  catch { /* phase dir lookup failure */ }
@@ -788,7 +779,15 @@ function routeCheckCommand({ args, cwd, raw }) {
788
779
  cmdVerifyCodebaseDrift(cwd, raw);
789
780
  return;
790
781
  }
791
- error('Unknown check subcommand. Available: auto-mode, decision-coverage-plan, decision-coverage-verify, gap-analysis-plan-post, tdd-review-checkpoint, ui-plan-gate, ui-safety-gate, verify-schema-drift, verify-codebase-drift', ERROR_REASON.SDK_UNKNOWN_COMMAND);
782
+ if (subcommand === 'prohibition-enforcement') {
783
+ // The deterministic test-tier prohibition PRODUCER/gate (#1259, ADR-550 D5d). Locates the
784
+ // wired mechanical check (node-test or lint-rule), confirms fail-first, runs it, builds
785
+ // enforcementEvidence, and emits the dispositionForProhibition verdict. Invocable as
786
+ // `gsd_run check prohibition-enforcement <request.json>`.
787
+ (0, prohibition_enforcement_cjs_1.routeProhibitionEnforcement)(args, raw);
788
+ return;
789
+ }
790
+ error('Unknown check subcommand. Available: auto-mode, decision-coverage-plan, decision-coverage-verify, gap-analysis-plan-post, prohibition-enforcement, tdd-review-checkpoint, ui-plan-gate, ui-safety-gate, verify-schema-drift, verify-codebase-drift', ERROR_REASON.SDK_UNKNOWN_COMMAND);
792
791
  }
793
792
  module.exports = {
794
793
  routeCheckCommand,
@@ -13,8 +13,26 @@ const node_fs_1 = __importDefault(require("node:fs"));
13
13
  const node_path_1 = __importDefault(require("node:path"));
14
14
  const shell_command_projection_cjs_1 = require("./shell-command-projection.cjs");
15
15
  // eslint-disable-next-line @typescript-eslint/no-require-imports
16
- const core = require("./core.cjs");
17
- const { loadConfig, isGitIgnored, normalizePhaseName, comparePhaseNum, getArchivedPhaseDirs, generateSlugInternal, getMilestoneInfo, getMilestonePhaseFilter, resolveModelInternal, resolveEffortInternal, resolveFastModeInternal, resolveEffortForTier, stripShippedMilestones: _stripShippedMilestones, extractCurrentMilestone, toPosixPath, output, error, findPhaseInternal, extractOneLinerFromBody, getRoadmapPhaseInternal, extractPhaseToken, resolveGranularityInternal, assertValidGranularityOverride, } = core;
16
+ const ioMod = require("./io.cjs");
17
+ const { output, error } = ioMod;
18
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
19
+ const configLoaderMod = require("./config-loader.cjs");
20
+ const { loadConfig, isGitIgnored } = configLoaderMod;
21
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
22
+ const coreUtilsMod = require("./core-utils.cjs");
23
+ const { toPosixPath, generateSlugInternal, extractOneLinerFromBody } = coreUtilsMod;
24
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
25
+ const phaseIdMod = require("./phase-id.cjs");
26
+ const { normalizePhaseName, comparePhaseNum, extractPhaseToken } = phaseIdMod;
27
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
28
+ const phaseLocatorMod = require("./phase-locator.cjs");
29
+ const { getArchivedPhaseDirs, findPhaseInternal } = phaseLocatorMod;
30
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
31
+ const roadmapParserMod = require("./roadmap-parser.cjs");
32
+ const { extractCurrentMilestone, stripShippedMilestones: _stripShippedMilestones, getMilestoneInfo, getMilestonePhaseFilter, getRoadmapPhaseInternal } = roadmapParserMod;
33
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
34
+ const modelResolverMod = require("./model-resolver.cjs");
35
+ const { resolveModelInternal, resolveEffortInternal, resolveFastModeInternal, resolveEffortForTier, resolveGranularityInternal, assertValidGranularityOverride } = modelResolverMod;
18
36
  const model_catalog_cjs_1 = require("./model-catalog.cjs");
19
37
  // eslint-disable-next-line @typescript-eslint/no-require-imports
20
38
  const planningWorkspace = require("./planning-workspace.cjs");
@@ -8,11 +8,10 @@
8
8
  * normalizes legacy keys, applies the active-workstream overlay, validates
9
9
  * against the config schema, and warns on unknown keys/profile overrides.
10
10
  * Behaviour is preserved byte-for-behaviour from the prior location; only
11
- * the module boundary moved. core.cjs re-exports `loadConfig` for back-compat.
11
+ * the module boundary moved. The core.cjs re-export spine was retired in
12
+ * epic #1267; callers import loadConfig from config-loader.cjs directly.
12
13
  *
13
- * New imports should pull loadConfig from config-loader.cjs directly.
14
- *
15
- * Dependencies (leaf modules only — no core.cjs):
14
+ * Dependencies (leaf modules only):
16
15
  * - node:fs / node:os / node:path (stdlib)
17
16
  * - ./configuration.cjs (normalizeLegacyKeys, CONFIG_DEFAULTS as CANONICAL_CONFIG_DEFAULTS)
18
17
  * - ./config-schema.cjs (VALID_CONFIG_KEYS, DYNAMIC_KEY_PATTERNS)
@@ -6,8 +6,8 @@
6
6
  *
7
7
  * Imported by:
8
8
  * - config.cjs (isValidConfigKey validator)
9
- * - core.cjs
10
9
  * - many tests (config-schema.property.test.cjs, bug-*, feat-*, etc.)
10
+ * (core.cjs re-export spine retired in epic #1267)
11
11
  *
12
12
  * See Phase 2 Cycle 5 (#3536) — schema manifest migration.
13
13
  *
@@ -3,7 +3,8 @@
3
3
  * TypeScript type definitions for GSD project config — model_policy block.
4
4
  *
5
5
  * These types reflect the model_policy config shape consumed by
6
- * resolveModelPolicy in core.cjs and validated by config-schema.cjs.
6
+ * resolveModelPolicy in model-resolver.cjs and validated by config-schema.cjs.
7
+ * (core.cjs re-export spine retired in epic #1267)
7
8
  *
8
9
  * See feat #49 (model_policy presets) and config-schema.manifest.json.
9
10
  * Added under ADR-457: TS sources in src/ compile to CJS artifacts in
@@ -13,8 +13,11 @@ const node_fs_1 = __importDefault(require("node:fs"));
13
13
  const node_path_1 = __importDefault(require("node:path"));
14
14
  const node_os_1 = __importDefault(require("node:os"));
15
15
  // eslint-disable-next-line @typescript-eslint/no-require-imports
16
- const core = require("./core.cjs");
17
- const { output, error, ERROR_REASON, CONFIG_DEFAULTS } = core;
16
+ const io = require("./io.cjs");
17
+ const { output, error, ERROR_REASON } = io;
18
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
19
+ const configLoader = require("./config-loader.cjs");
20
+ const { CONFIG_DEFAULTS } = configLoader;
18
21
  const shell_command_projection_cjs_1 = require("./shell-command-projection.cjs");
19
22
  // eslint-disable-next-line @typescript-eslint/no-require-imports
20
23
  const planningWorkspace = require("./planning-workspace.cjs");
@@ -61,7 +61,13 @@ function parseDecisions(content) {
61
61
  // in addition to numeric-only IDs (D-42). The first character after `D-` must
62
62
  // be alphanumeric, so malformed shapes like `D--foo` or `D-_bar` are rejected.
63
63
  // CJS callers consume {id, text} and ignore the optional extras.
64
- const bulletRe = /^\s*-\s+\*\*D-([A-Za-z0-9][A-Za-z0-9_-]*)(?:\s*\[([^\]]+)\])?\s*:\*\*\s*(.*)$/;
64
+ // #1343: `[^:*]*` replaces the old `\s*` before `:**` so that a freeform run
65
+ // such as `(parenthetical)`, an em-dash, or other prose between the optional
66
+ // bracket-tag group and the closing `:**` is tolerated rather than silently
67
+ // dropping the whole decision. `[^:*]*` subsumes plain whitespace and stops
68
+ // correctly at `:**`. Capture groups 1 (id), 2 (bracket tags), 3 (text) are
69
+ // unchanged.
70
+ const bulletRe = /^\s*-\s+\*\*D-([A-Za-z0-9][A-Za-z0-9_-]*)(?:\s*\[([^\]]+)\])?[^:*]*:\*\*\s*(.*)$/;
65
71
  let current = null;
66
72
  const flush = () => {
67
73
  if (current) {
@@ -102,6 +108,18 @@ function parseDecisions(content) {
102
108
  current = { id, text: bulletMatch[3], category, tags, trackable };
103
109
  continue;
104
110
  }
111
+ // Parse-miss guard (#1343): a line that looks like a `D-NN` decision bullet
112
+ // but failed `bulletRe` (e.g. a `:` or `*` inside the pre-colon run) must NOT
113
+ // be silently dropped — a narrowed trackable set lets a blocking coverage gate
114
+ // report a false pass. Surface it loudly instead.
115
+ if (/^\s*-\s+\*\*D-/.test(line)) {
116
+ // A malformed D-bullet still starts a (failed) new decision, so it ends the
117
+ // previous one — flush before warning so a following continuation line cannot
118
+ // be mis-appended to the prior valid decision.
119
+ flush();
120
+ console.warn(`parseDecisions: ignored unparseable decision bullet: ${trimmed}`);
121
+ continue;
122
+ }
105
123
  // Continuation line for current decision (indented with space OR tab,
106
124
  // non-bullet, non-empty) — tab indentation must work too (review F12).
107
125
  if (current && trimmed !== '' && !trimmed.startsWith('-') && /^[ \t]/.test(line)) {