@opengsd/gsd-core 1.5.0 → 1.6.0-rc.1

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 (63) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/agents/gsd-plan-checker.md +34 -0
  3. package/agents/gsd-planner.md +2 -0
  4. package/bin/install.js +108 -34
  5. package/gemini-extension.json +1 -1
  6. package/gsd-core/bin/gsd-tools.cjs +677 -2
  7. package/gsd-core/bin/lib/adr-parser.cjs +24 -17
  8. package/gsd-core/bin/lib/audit.cjs +2 -2
  9. package/gsd-core/bin/lib/capability-consent.cjs +763 -0
  10. package/gsd-core/bin/lib/capability-ledger.cjs +831 -0
  11. package/gsd-core/bin/lib/capability-lifecycle.cjs +1551 -0
  12. package/gsd-core/bin/lib/capability-loader.cjs +764 -0
  13. package/gsd-core/bin/lib/capability-lock.cjs +553 -0
  14. package/gsd-core/bin/lib/capability-registry.cjs +198 -4
  15. package/gsd-core/bin/lib/capability-source.cjs +1242 -0
  16. package/gsd-core/bin/lib/capability-state.cjs +9 -6
  17. package/gsd-core/bin/lib/capability-trust.cjs +550 -0
  18. package/gsd-core/bin/lib/capability-validator.cjs +2066 -0
  19. package/gsd-core/bin/lib/capability-writer.cjs +14 -5
  20. package/gsd-core/bin/lib/check-command-router.cjs +69 -18
  21. package/gsd-core/bin/lib/command-aliases.cjs +8 -0
  22. package/gsd-core/bin/lib/config-loader.cjs +92 -84
  23. package/gsd-core/bin/lib/config-schema.cjs +26 -7
  24. package/gsd-core/bin/lib/config.cjs +1 -1
  25. package/gsd-core/bin/lib/decisions.cjs +149 -60
  26. package/gsd-core/bin/lib/gap-checker.cjs +126 -11
  27. package/gsd-core/bin/lib/init.cjs +91 -22
  28. package/gsd-core/bin/lib/legacy-cleanup.cjs +96 -0
  29. package/gsd-core/bin/lib/loop-resolver.cjs +26 -2
  30. package/gsd-core/bin/lib/markdown-sectionizer.cjs +471 -0
  31. package/gsd-core/bin/lib/milestone.cjs +41 -2
  32. package/gsd-core/bin/lib/phase-command-router.cjs +5 -0
  33. package/gsd-core/bin/lib/phase-lifecycle.cjs +14 -5
  34. package/gsd-core/bin/lib/phase.cjs +29 -0
  35. package/gsd-core/bin/lib/project-root.cjs +89 -2
  36. package/gsd-core/bin/lib/resolution.cjs +26 -0
  37. package/gsd-core/bin/lib/roadmap-parser.cjs +44 -98
  38. package/gsd-core/bin/lib/runtime-homes.cjs +53 -1
  39. package/gsd-core/bin/lib/semver-compare.cjs +127 -0
  40. package/gsd-core/bin/lib/state-document.cjs +4 -2
  41. package/gsd-core/bin/lib/state.cjs +317 -161
  42. package/gsd-core/bin/lib/uat-predicate.cjs +7 -47
  43. package/gsd-core/bin/lib/uat.cjs +39 -26
  44. package/gsd-core/bin/lib/verify.cjs +29 -13
  45. package/gsd-core/bin/shared/config-defaults.manifest.json +4 -0
  46. package/gsd-core/bin/shared/config-schema.manifest.json +4 -1
  47. package/gsd-core/references/execute-phase-between-wave-reset.md +43 -0
  48. package/gsd-core/references/execute-phase-wave-guard.md +33 -0
  49. package/gsd-core/references/planner-antipatterns.md +48 -0
  50. package/gsd-core/references/planning-config.md +3 -0
  51. package/gsd-core/references/scout-codebase.md +2 -2
  52. package/gsd-core/workflows/discuss-phase/templates/context.md +1 -1
  53. package/gsd-core/workflows/discuss-phase.md +1 -2
  54. package/gsd-core/workflows/execute-phase.md +4 -6
  55. package/package.json +3 -3
  56. package/scripts/gen-capability-matrix.cjs +284 -0
  57. package/scripts/gen-capability-registry.cjs +96 -1853
  58. package/scripts/lint-regression-test-names.allowlist.json +1 -0
  59. package/scripts/lint-resolution-provenance.allowlist.json +1 -0
  60. package/scripts/lint-resolution-provenance.cjs +192 -0
  61. package/scripts/lint-test-file-count.allowlist.json +9 -0
  62. package/scripts/run-tests.cjs +14 -0
  63. package/scripts/sync-manifest-versions.cjs +77 -5
@@ -17,6 +17,7 @@ const shell_command_projection_cjs_1 = require("./shell-command-projection.cjs")
17
17
  const io = require("./io.cjs");
18
18
  // eslint-disable-next-line @typescript-eslint/no-require-imports -- config-loader.cjs is an export= CommonJS module
19
19
  const configLoader = require("./config-loader.cjs");
20
+ const project_root_cjs_1 = require("./project-root.cjs");
20
21
  // eslint-disable-next-line @typescript-eslint/no-require-imports -- model-resolver.cjs is an export= CommonJS module
21
22
  const modelResolver = require("./model-resolver.cjs");
22
23
  // eslint-disable-next-line @typescript-eslint/no-require-imports -- phase-locator.cjs is an export= CommonJS module
@@ -48,8 +49,9 @@ const { checkAgentsInstalled } = agentInstallCheck;
48
49
  // eslint-disable-next-line @typescript-eslint/no-require-imports -- git-base-branch.cjs is an export= CommonJS module
49
50
  const gitBaseBranch = require("./git-base-branch.cjs");
50
51
  const { gitWorktreeInfoInternal } = gitBaseBranch;
52
+ const resolution_cjs_1 = require("./resolution.cjs");
51
53
  const { output, error } = io;
52
- const { loadConfig } = configLoader;
54
+ const { loadConfig, loadConfigResolved } = configLoader;
53
55
  const { resolveModelInternal, resolveGranularityInternal, assertValidGranularityOverride } = modelResolver;
54
56
  const { findPhaseInternal } = phaseLocator;
55
57
  const { getRoadmapPhaseInternal, getMilestoneInfo, getMilestonePhaseFilter, stripShippedMilestones, extractCurrentMilestone, } = roadmapParser;
@@ -1542,7 +1544,12 @@ function cmdInitRemoveWorkspace(cwd, name, raw) {
1542
1544
  };
1543
1545
  output(result, raw);
1544
1546
  }
1545
- function buildAgentSkillsBlock(config, agentType, projectRoot) {
1547
+ function buildAgentSkillsBlock(config, agentType, projectRoot, diagnostics) {
1548
+ const warn = (message) => {
1549
+ process.stderr.write(message);
1550
+ if (diagnostics)
1551
+ diagnostics.warnings.push(message.replace(/\n+$/, ''));
1552
+ };
1546
1553
  const runtime = (config && config['runtime']) || 'claude';
1547
1554
  const globalSkillsBase = (0, runtime_homes_cjs_1.getGlobalSkillsBase)(runtime);
1548
1555
  if (!config || !config['agent_skills'] || !agentType)
@@ -1552,7 +1559,11 @@ function buildAgentSkillsBlock(config, agentType, projectRoot) {
1552
1559
  return '';
1553
1560
  if (typeof skillPaths === 'string')
1554
1561
  skillPaths = [skillPaths];
1555
- if (!Array.isArray(skillPaths) || skillPaths.length === 0)
1562
+ if (!Array.isArray(skillPaths)) {
1563
+ warn(`[agent-skills] WARNING: Agent "${agentType}" has a malformed agent_skills value (expected string or array, got ${typeof skillPaths}) — ignoring\n`);
1564
+ return '';
1565
+ }
1566
+ if (skillPaths.length === 0)
1556
1567
  return '';
1557
1568
  // Hoist trusted roots computation before the loop: loadTrustedGlobalRoots does
1558
1569
  // realpathSync I/O and should run at most once per call, not once per failing skill.
@@ -1563,18 +1574,20 @@ function buildAgentSkillsBlock(config, agentType, projectRoot) {
1563
1574
  // Skill-tool directive ({ kind: 'directive', name }) for plugin-provided namespaced skills.
1564
1575
  const validEntries = [];
1565
1576
  for (const skillPath of skillPaths) {
1566
- if (typeof skillPath !== 'string')
1577
+ if (typeof skillPath !== 'string') {
1578
+ warn(`[agent-skills] WARNING: Ignoring non-string skill entry (${typeof skillPath}) — skipping\n`);
1567
1579
  continue;
1580
+ }
1568
1581
  if (skillPath.startsWith('global:')) {
1569
1582
  const skillName = skillPath.slice(7);
1570
1583
  if (!skillName) {
1571
- process.stderr.write(`[agent-skills] WARNING: "global:" prefix with empty skill name — skipping\n`);
1584
+ warn(`[agent-skills] WARNING: "global:" prefix with empty skill name — skipping\n`);
1572
1585
  continue;
1573
1586
  }
1574
1587
  // Accept: one or more [A-Za-z0-9_-]+ segments joined by single colons.
1575
1588
  // Rejects: empty segments (::), leading/trailing colon, dots, slashes, backslashes.
1576
1589
  if (!/^[A-Za-z0-9_-]+(:[A-Za-z0-9_-]+)*$/.test(skillName)) {
1577
- process.stderr.write(`[agent-skills] WARNING: Invalid global skill name "${skillName}" — skipping\n`);
1590
+ warn(`[agent-skills] WARNING: Invalid global skill name "${skillName}" — skipping\n`);
1578
1591
  continue;
1579
1592
  }
1580
1593
  const isNamespaced = skillName.includes(':');
@@ -1585,20 +1598,20 @@ function buildAgentSkillsBlock(config, agentType, projectRoot) {
1585
1598
  validEntries.push({ kind: 'directive', name: skillName });
1586
1599
  }
1587
1600
  else {
1588
- process.stderr.write(`[agent-skills] WARNING: Plugin-namespaced skill "global:${skillName}" requires a Skill-tool-capable runtime (claude) — skipping on runtime "${runtime}"\n`);
1601
+ warn(`[agent-skills] WARNING: Plugin-namespaced skill "global:${skillName}" requires a Skill-tool-capable runtime (claude) — skipping on runtime "${runtime}"\n`);
1589
1602
  }
1590
1603
  continue;
1591
1604
  }
1592
1605
  // Non-namespaced bare name: attempt filesystem resolution as before.
1593
1606
  if (globalSkillsBase === null) {
1594
- process.stderr.write(`[agent-skills] WARNING: Runtime "${runtime}" does not use a skills directory — "global:${skillName}" is not supported on this runtime\n`);
1607
+ warn(`[agent-skills] WARNING: Runtime "${runtime}" does not use a skills directory — "global:${skillName}" is not supported on this runtime\n`);
1595
1608
  continue;
1596
1609
  }
1597
1610
  const globalSkillDir = (0, runtime_homes_cjs_1.getGlobalSkillDir)(runtime, skillName);
1598
1611
  const globalSkillMd = node_path_1.default.join(globalSkillDir, 'SKILL.md');
1599
1612
  const displayPath = (0, runtime_homes_cjs_1.getGlobalSkillDisplayPath)(runtime, skillName);
1600
1613
  if (!node_fs_1.default.existsSync(globalSkillMd)) {
1601
- process.stderr.write(`[agent-skills] WARNING: Global skill not found at "${displayPath}/SKILL.md" — skipping\n`);
1614
+ warn(`[agent-skills] WARNING: Global skill not found at "${displayPath}/SKILL.md" — skipping\n`);
1602
1615
  continue;
1603
1616
  }
1604
1617
  const pathCheck = (0, security_cjs_1.validatePath)(globalSkillMd, globalSkillsBase, { allowAbsolute: true });
@@ -1608,9 +1621,11 @@ function buildAgentSkillsBlock(config, agentType, projectRoot) {
1608
1621
  return Boolean(rootCheck['safe']);
1609
1622
  });
1610
1623
  if (!acceptedViaTrustedRoot) {
1611
- process.stderr.write(`[agent-skills] WARNING: Global skill "${skillName}" failed path check (symlink escape?) — skipping\n`);
1624
+ warn(`[agent-skills] WARNING: Global skill "${skillName}" failed path check (symlink escape?) — skipping\n`);
1612
1625
  continue;
1613
1626
  }
1627
+ // Intentionally a direct stderr write, NOT warn(): this is an acceptance
1628
+ // trace, not a skip, so it must not land in the diagnostics warnings[].
1614
1629
  process.stderr.write(`[agent-skills] NOTE: Global skill "${skillName}" accepted via trusted_global_roots (resolves outside the default skills dir)\n`);
1615
1630
  }
1616
1631
  validEntries.push({ kind: 'include', ref: `${globalSkillDir}/SKILL.md`, display: displayPath });
@@ -1618,18 +1633,20 @@ function buildAgentSkillsBlock(config, agentType, projectRoot) {
1618
1633
  }
1619
1634
  const pathCheck = (0, security_cjs_1.validatePath)(skillPath, projectRoot);
1620
1635
  if (!pathCheck['safe']) {
1621
- process.stderr.write(`[agent-skills] WARNING: Skipping unsafe path "${skillPath}": ${pathCheck['error']}\n`);
1636
+ warn(`[agent-skills] WARNING: Skipping unsafe path "${skillPath}": ${pathCheck['error']}\n`);
1622
1637
  continue;
1623
1638
  }
1624
1639
  const skillMdPath = node_path_1.default.join(projectRoot, skillPath, 'SKILL.md');
1625
1640
  if (!node_fs_1.default.existsSync(skillMdPath)) {
1626
- process.stderr.write(`[agent-skills] WARNING: Skill not found at "${skillPath}/SKILL.md" — skipping\n`);
1641
+ warn(`[agent-skills] WARNING: Skill not found at "${skillPath}/SKILL.md" — skipping\n`);
1627
1642
  continue;
1628
1643
  }
1629
1644
  validEntries.push({ kind: 'include', ref: `${skillPath}/SKILL.md`, display: skillPath });
1630
1645
  }
1631
- if (validEntries.length === 0)
1646
+ if (validEntries.length === 0) {
1647
+ warn(`[agent-skills] WARNING: Agent "${agentType}" has ${skillPaths.length} configured skill path(s) but none resolved to a valid skill — all were skipped (see warnings above)\n`);
1632
1648
  return '';
1649
+ }
1633
1650
  const lines = validEntries.map((entry) => {
1634
1651
  if (entry.kind === 'directive') {
1635
1652
  return `- Load the \`${entry.name}\` skill via the Skill tool before proceeding (plugin-provided).`;
@@ -1643,16 +1660,68 @@ function cmdAgentSkills(cwd, agentType, raw, jsonMode) {
1643
1660
  output('', raw, '');
1644
1661
  return;
1645
1662
  }
1646
- const config = loadConfig(cwd);
1647
- const block = buildAgentSkillsBlock(config, agentType, cwd);
1663
+ // Anchor to project root before loading config (#1415/#1366 cwd-drift fix).
1664
+ const projectRoot = (0, project_root_cjs_1.findProjectRoot)(cwd);
1665
+ const { config, source, degraded } = loadConfigResolved(projectRoot);
1666
+ const diagnostics = { warnings: [] };
1667
+ const block = buildAgentSkillsBlock(config, agentType, projectRoot, diagnostics);
1668
+ // Compute configured + reason for diagnostic output.
1669
+ const agentSkillsMap = (config && config['agent_skills'] && typeof config['agent_skills'] === 'object')
1670
+ ? config['agent_skills']
1671
+ : {};
1672
+ const configured = Object.prototype.hasOwnProperty.call(agentSkillsMap, agentType);
1673
+ let reason;
1674
+ let skillPaths = configured ? agentSkillsMap[agentType] : [];
1675
+ if (!configured) {
1676
+ reason = 'not_configured';
1677
+ skillPaths = [];
1678
+ }
1679
+ else {
1680
+ // Normalize paths to array
1681
+ if (typeof skillPaths === 'string')
1682
+ skillPaths = [skillPaths];
1683
+ if (!Array.isArray(skillPaths))
1684
+ skillPaths = [];
1685
+ const pathsArr = skillPaths;
1686
+ // Fix 3: treat "" (empty string) as configured_empty — all-blank entries = no meaningful paths.
1687
+ // An array of all empty/blank strings has length > 0 but zero meaningful paths.
1688
+ const nonBlankPaths = pathsArr.filter(p => typeof p === 'string' && p.trim().length > 0);
1689
+ if (pathsArr.length === 0 || nonBlankPaths.length === 0) {
1690
+ // configured with empty array / "" / all-blank entries
1691
+ reason = 'configured_empty';
1692
+ // Reflect zero meaningful paths in the normalized array used for skills_count
1693
+ skillPaths = [];
1694
+ try {
1695
+ process.stderr.write(`[agent-skills] WARNING: Agent "${agentType}" is configured in agent_skills but has no skill paths — skills_count will be 0\n`);
1696
+ }
1697
+ catch { /* stderr might be closed */ }
1698
+ }
1699
+ else if (!block) {
1700
+ // configured with paths but all failed to resolve (warnings already emitted by buildAgentSkillsBlock)
1701
+ reason = 'configured_unresolved';
1702
+ }
1703
+ else {
1704
+ reason = 'resolved';
1705
+ }
1706
+ }
1707
+ const normalizedPaths = Array.isArray(skillPaths) ? skillPaths : [];
1648
1708
  if (jsonMode) {
1649
- const skillPaths = (config && config.agent_skills && config.agent_skills[agentType]) || [];
1650
- const normalizedPaths = Array.isArray(skillPaths)
1651
- ? skillPaths
1652
- : skillPaths
1653
- ? [skillPaths]
1654
- : [];
1655
- output({ agent_type: agentType, block: block || '', skills_count: normalizedPaths.length }, raw);
1709
+ // Build the Resolution<AgentSkillsValue> envelope and embed .value additively.
1710
+ // Flat fields are retained unchanged for back-compat; value formalises the
1711
+ // Resolution convention (ADR-1411 P3, #1416). source/degraded remain
1712
+ // config-provenance extras, outside the Resolution<T> envelope.
1713
+ const resolution = (0, resolution_cjs_1.makeResolution)({ block: block || '', skills_count: normalizedPaths.length }, { configured, reason, warnings: diagnostics.warnings });
1714
+ output({
1715
+ agent_type: agentType,
1716
+ block: block || '',
1717
+ skills_count: normalizedPaths.length,
1718
+ warnings: diagnostics.warnings,
1719
+ configured,
1720
+ reason,
1721
+ source,
1722
+ degraded,
1723
+ value: resolution.value,
1724
+ }, raw);
1656
1725
  return;
1657
1726
  }
1658
1727
  if (block) {
@@ -37,6 +37,33 @@ const OLD_PACKAGE_SIGNAL = 'gsd-core' + '-cc';
37
37
  */
38
38
  const GSD_MANAGED_SUBTREES = ['hooks', 'commands'];
39
39
 
40
+ /**
41
+ * Substring that identifies a skill file as referencing the pre-rename GSD
42
+ * runtime config subdirectory. Assembled from parts to avoid self-flagging.
43
+ *
44
+ * Old installs wrote skill bodies that embed the path to the GSD runtime
45
+ * directory — e.g. `@$HOME/.codex/get-shit-done/workflows/plan.md`. After // gsd-allow-legacy-name
46
+ * the rename to `gsd-core/` (#604), those embedded paths are stale and the
47
+ * skill file must be removed so the runtime does not pick up the wrong copy.
48
+ *
49
+ * Issue: #1453
50
+ */
51
+ const LEGACY_SKILL_PATH_SIGNAL = 'get-shit-done'; // gsd-allow-legacy-name
52
+
53
+ /**
54
+ * Prefix that identifies a skill directory as GSD-managed.
55
+ * Only `gsd-*` subdirectories under the `skills/` subtree are scanned; user
56
+ * skill directories with other prefixes are never touched.
57
+ */
58
+ const GSD_SKILL_DIR_PREFIX = 'gsd-';
59
+
60
+ /**
61
+ * File extensions eligible for the stale-skill-path scan.
62
+ * SKILL.md is the only file in a codex/cursor/kilo/etc skill directory that
63
+ * embeds an @-import path to the GSD runtime config tree.
64
+ */
65
+ const SKILL_MD_EXTENSIONS = new Set(['.md']);
66
+
40
67
  /**
41
68
  * Extensions eligible for the content-reference scan.
42
69
  *
@@ -113,6 +140,24 @@ function fileContainsOldPackageSignal(absPath, fsMod) {
113
140
  }
114
141
  }
115
142
 
143
+ /**
144
+ * Return true if the file at `absPath` contains the legacy skill path signal
145
+ * (`get-shit-done` as a path component inside an @-import or similar reference). // gsd-allow-legacy-name
146
+ * Skips unreadable files (returns false on any error).
147
+ *
148
+ * @param {string} absPath
149
+ * @param {object} fsMod
150
+ * @returns {boolean}
151
+ */
152
+ function fileContainsLegacySkillPathSignal(absPath, fsMod) {
153
+ try {
154
+ const content = fsMod.readFileSync(absPath, 'utf8');
155
+ return content.includes('/' + LEGACY_SKILL_PATH_SIGNAL + '/'); // gsd-allow-legacy-name
156
+ } catch {
157
+ return false;
158
+ }
159
+ }
160
+
116
161
  // ─── Public API ──────────────────────────────────────────────────────────────
117
162
 
118
163
  /**
@@ -122,6 +167,9 @@ function fileContainsOldPackageSignal(absPath, fsMod) {
122
167
  * Possible reasons in returned entries:
123
168
  * - 'content-references-old-package': a code file whose content contains
124
169
  * the old package name signal (hooks/ and commands/ subtrees only).
170
+ * - 'stale-get-shit-done-path': a skill markdown file whose content contains
171
+ * a path reference to the pre-rename `get-shit-done/` runtime directory // gsd-allow-legacy-name
172
+ * (skills/ subtree, gsd-* directories only). Issue #1453.
125
173
  * - 'legacy-shared-cache': the old package's shared update-check cache file.
126
174
  *
127
175
  * @param {string[]} configDirs - absolute paths to runtime config dirs to scan
@@ -164,6 +212,54 @@ function planLegacyCleanup(configDirs, opts = {}) {
164
212
  }
165
213
  }
166
214
  }
215
+
216
+ // #1453: Scan skills/gsd-* directories for stale get-shit-done path references. // gsd-allow-legacy-name
217
+ //
218
+ // Background: older GSD installs wrote SKILL.md files that embedded a path to
219
+ // the GSD runtime config directory, e.g.:
220
+ // @$HOME/.codex/get-shit-done/workflows/docs-update.md // gsd-allow-legacy-name
221
+ //
222
+ // After the rename to gsd-core/ (#604), the correct path is:
223
+ // @$HOME/.codex/gsd-core/workflows/docs-update.md
224
+ //
225
+ // When Codex upgrades to gsd-core 1.5.0 it writes fresh skill files to
226
+ // ~/.codex/skills/ but does NOT remove stale copies that an older install
227
+ // may have placed under OTHER discoverable skill roots (e.g. ~/.agents/skills/,
228
+ // ~/.config/agents/skills/). Codex can pick up either copy and the stale one
229
+ // breaks the session (#1453).
230
+ //
231
+ // This scan removes GSD-managed skill files (under gsd-* subdirs) that still
232
+ // reference the old path. Only .md files are scanned (SKILL.md is the sole
233
+ // embedded-path carrier in a skill dir). The skills/ dir itself is not deleted;
234
+ // user-owned non-gsd-* skill dirs are never touched.
235
+ const skillsDir = path.join(configDir, 'skills');
236
+ let skillDirEntries;
237
+ try {
238
+ skillDirEntries = fsMod.readdirSync(skillsDir, { withFileTypes: true });
239
+ } catch {
240
+ skillDirEntries = null;
241
+ }
242
+ if (skillDirEntries) {
243
+ for (const entry of skillDirEntries) {
244
+ // Only process gsd-* subdirectories (GSD-managed skill dirs).
245
+ if (!entry.isDirectory()) continue;
246
+ if (!entry.name.startsWith(GSD_SKILL_DIR_PREFIX)) continue;
247
+
248
+ const skillDir = path.join(skillsDir, entry.name);
249
+ const files = collectFilesUnder(skillDir, fsMod);
250
+
251
+ for (const absPath of files) {
252
+ // Never flag user-authored dev-preferences artifacts
253
+ if (isDevPreferencesPath(absPath)) continue;
254
+
255
+ // Only scan .md files for the stale path signal.
256
+ const ext = path.extname(absPath).toLowerCase();
257
+ if (SKILL_MD_EXTENSIONS.has(ext) && fileContainsLegacySkillPathSignal(absPath, fsMod)) {
258
+ addCandidate(absPath, 'stale-get-shit-done-path'); // gsd-allow-legacy-name
259
+ }
260
+ }
261
+ }
262
+ }
167
263
  }
168
264
 
169
265
  // Legacy shared cache (fixed name from the old package)
@@ -430,9 +430,13 @@ function cmdLoopRenderHooks(cwd, point, raw, options = {}) {
430
430
  config = {};
431
431
  }
432
432
  const state = resolveCapabilityRuntimeState(cwd, runtimeConfigDir, config);
433
- // Registry is the static generated module — same object capability-state uses internally.
433
+ // Load overlay-aware registry (ADR-1244 D2 wiring) so installed third-party
434
+ // capabilities are visible to loop rendering exactly like first-party ones.
434
435
  // eslint-disable-next-line @typescript-eslint/no-require-imports
435
- const registry = require('./capability-registry.cjs');
436
+ const { loadRegistry } = require('./capability-loader.cjs');
437
+ // #1459 IC-04: thread the consent home (process.env.GSD_HOME) EXPLICITLY so a consented project cap's
438
+ // loop surfaces (steps/gates/contributions) render here at the SAME home that gated its activation.
439
+ const registry = loadRegistry({ includeInstalled: true, cwd, gsdHome: process.env['GSD_HOME'] });
436
440
  const capabilityStatesById = new Map();
437
441
  for (const cap of state.capabilities || []) {
438
442
  capabilityStatesById.set(cap.id, cap);
@@ -446,6 +450,26 @@ function cmdLoopRenderHooks(cwd, point, raw, options = {}) {
446
450
  coreError(msg);
447
451
  return;
448
452
  }
453
+ // ── ADR-1244 D2 fail-closed gate injection ────────────────────────────────────
454
+ // For every skipped overlay capability that declared a gate at this point,
455
+ // inject a synthetic BLOCKING gate into the resolved output so the loop HALTS
456
+ // rather than silently proceeding as if the gate had passed. step/contribution
457
+ // overlays that were skipped are left open (skip-open is correct for them).
458
+ const overlayMeta = registry['_overlay'];
459
+ if (overlayMeta && Array.isArray(overlayMeta.blockedGates)) {
460
+ for (const blocked of overlayMeta.blockedGates) {
461
+ if (blocked.point === point) {
462
+ const syntheticGate = {
463
+ capId: blocked.capId,
464
+ kind: 'gate',
465
+ blocking: true,
466
+ onError: 'halt',
467
+ check: `capability "${blocked.capId}" was skipped at load (${blocked.reason}); its gate at ${point} cannot be evaluated — failing closed`,
468
+ };
469
+ resolved.activeHooks.push(syntheticGate);
470
+ }
471
+ }
472
+ }
449
473
  // --active-cap mode: print exactly 'true' or 'false' with no envelope
450
474
  if (activeCapId !== undefined) {
451
475
  const isActive = resolved.activeHooks.some((h) => h.capId === activeCapId);