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

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 (85) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/agents/gsd-advisor-researcher.md +1 -1
  3. package/agents/gsd-assumptions-analyzer.md +1 -1
  4. package/agents/gsd-code-fixer.md +1 -1
  5. package/agents/gsd-code-reviewer.md +1 -1
  6. package/agents/gsd-codebase-mapper.md +1 -1
  7. package/agents/gsd-debugger.md +1 -1
  8. package/agents/gsd-doc-writer.md +1 -1
  9. package/agents/gsd-eval-auditor.md +1 -1
  10. package/agents/gsd-executor.md +1 -1
  11. package/agents/gsd-integration-checker.md +1 -1
  12. package/agents/gsd-nyquist-auditor.md +1 -0
  13. package/agents/gsd-phase-researcher.md +1 -1
  14. package/agents/gsd-plan-checker.md +1 -1
  15. package/agents/gsd-planner.md +1 -1
  16. package/agents/gsd-project-researcher.md +1 -1
  17. package/agents/gsd-research-synthesizer.md +1 -1
  18. package/agents/gsd-roadmapper.md +55 -2
  19. package/agents/gsd-security-auditor.md +1 -0
  20. package/agents/gsd-ui-auditor.md +1 -1
  21. package/agents/gsd-ui-checker.md +1 -1
  22. package/agents/gsd-ui-researcher.md +1 -1
  23. package/agents/gsd-verifier.md +13 -2
  24. package/bin/install.js +36 -57
  25. package/commands/gsd/progress.md +2 -1
  26. package/gemini-extension.json +1 -1
  27. package/gsd-core/bin/gsd-tools.cjs +167 -3
  28. package/gsd-core/bin/lib/active-workstream-store.cjs +6 -0
  29. package/gsd-core/bin/lib/capability-state.cjs +97 -3
  30. package/gsd-core/bin/lib/capability-writer.cjs +354 -0
  31. package/gsd-core/bin/lib/config.cjs +80 -24
  32. package/gsd-core/bin/lib/edge-probe.cjs +25 -2
  33. package/gsd-core/bin/lib/frontmatter.cjs +53 -1
  34. package/gsd-core/bin/lib/git-base-branch.cjs +194 -0
  35. package/gsd-core/bin/lib/init.cjs +28 -6
  36. package/gsd-core/bin/lib/install-profiles.cjs +55 -0
  37. package/gsd-core/bin/lib/installer-migration-report.cjs +1 -0
  38. package/gsd-core/bin/lib/phase.cjs +28 -12
  39. package/gsd-core/bin/lib/plan-drift-guard.cjs +117 -0
  40. package/gsd-core/bin/lib/probe-core.cjs +117 -1
  41. package/gsd-core/bin/lib/roadmap-parser.cjs +13 -3
  42. package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +246 -0
  43. package/gsd-core/bin/lib/runtime-artifact-layout.cjs +34 -2
  44. package/gsd-core/bin/lib/state.cjs +240 -59
  45. package/gsd-core/bin/lib/verify.cjs +73 -4
  46. package/gsd-core/bin/lib/worktree-safety.cjs +2 -1
  47. package/gsd-core/references/edge-probe.md +11 -0
  48. package/gsd-core/references/prohibition-probe-fixtures/01-streak-reminder/expected.json +14 -0
  49. package/gsd-core/references/prohibition-probe-fixtures/02-clean-utility/expected.json +4 -0
  50. package/gsd-core/references/prohibition-probe-fixtures/03-multi-prohibition/expected.json +32 -0
  51. package/gsd-core/references/prohibition-probe.md +248 -0
  52. package/gsd-core/templates/spec.md +14 -0
  53. package/gsd-core/workflows/complete-milestone.md +1 -5
  54. package/gsd-core/workflows/execute-phase.md +4 -3
  55. package/gsd-core/workflows/execute-plan.md +21 -6
  56. package/gsd-core/workflows/help/modes/full.md +4 -0
  57. package/gsd-core/workflows/next.md +50 -2
  58. package/gsd-core/workflows/pause-work.md +7 -1
  59. package/gsd-core/workflows/plan-phase.md +2 -0
  60. package/gsd-core/workflows/plan-review-convergence.md +14 -4
  61. package/gsd-core/workflows/pr-branch.md +4 -2
  62. package/gsd-core/workflows/quick.md +3 -2
  63. package/gsd-core/workflows/resume-project.md +17 -1
  64. package/gsd-core/workflows/settings.md +27 -1
  65. package/gsd-core/workflows/ship.md +1 -5
  66. package/gsd-core/workflows/spec-phase.md +75 -0
  67. package/gsd-core/workflows/verify-phase.md +14 -4
  68. package/hooks/dist/gsd-ensure-canonical-path.js +305 -0
  69. package/hooks/dist/gsd-statusline.js +1 -1
  70. package/hooks/dist/managed-hooks-registry.cjs +1 -0
  71. package/hooks/gsd-ensure-canonical-path.js +305 -0
  72. package/hooks/gsd-statusline.js +1 -1
  73. package/hooks/hooks.json +1 -0
  74. package/hooks/managed-hooks-registry.cjs +1 -0
  75. package/package.json +3 -3
  76. package/scripts/build-hooks.js +7 -0
  77. package/scripts/changeset/new.cjs +17 -3
  78. package/scripts/fix-slash-commands.cjs +15 -3
  79. package/scripts/gen-capability-registry.cjs +14 -1
  80. package/scripts/lint-allow-test-rule-refs.allowlist.json +2 -1
  81. package/scripts/lint-test-file-count.allowlist.json +6 -0
  82. package/scripts/mutation-matrix.cjs +108 -7
  83. package/scripts/pr-target-policy.cjs +63 -0
  84. package/scripts/research-profiles.cjs +5 -5
  85. package/scripts/run-tests.cjs +107 -6
@@ -63,6 +63,13 @@
63
63
  * Returns JSON { valid, errors[], slots: {role,capability,outcome} | null }
64
64
  * --pick valid Emit bare boolean (for workflow boolean checks)
65
65
  *
66
+ * Drift Guard (ADR-22):
67
+ * drift-guard authority Resolve effective source-grounding authority
68
+ * (reads plan_review.source_grounding_authority + intel.enabled from config)
69
+ * drift-guard severity --status <S> Classify a symbol verdict into { severity, hardBlock }
70
+ * [--authority <A>] Status: VERIFIED|MISSING|AMBIGUOUS|UNCHECKABLE
71
+ * Authority: grep|intel|treesitter|lsp|scip (default: config-resolved)
72
+ *
66
73
  * Validation:
67
74
  * validate consistency Check phase numbering, disk/roadmap sync
68
75
  * validate health [--repair] Check .planning/ integrity, optionally repair
@@ -217,6 +224,7 @@ const verification = require('./lib/verification.cjs');
217
224
  const { routeInitCommand } = require('./lib/init-command-router.cjs');
218
225
  const loopResolver = require('./lib/loop-resolver.cjs');
219
226
  const capabilityState = require('./lib/capability-state.cjs');
227
+ const capabilityWriter = require('./lib/capability-writer.cjs');
220
228
  const { routePhaseCommand } = require('./lib/phase-command-router.cjs');
221
229
  const { routePhasesCommand } = require('./lib/phases-command-router.cjs');
222
230
  const { routeValidateCommand } = require('./lib/validate-command-router.cjs');
@@ -225,6 +233,8 @@ const { routeAgentCommand } = require('./lib/agent-command-router.cjs');
225
233
  const { routeCheckCommand } = require('./lib/check-command-router.cjs');
226
234
  const { routeTaskCommand } = require('./lib/task-command-router.cjs');
227
235
  const { parseNamedArgs, parseMultiwordArg } = require('./lib/command-arg-projection.cjs');
236
+ const { cmdGitBaseBranch } = require('./lib/git-base-branch.cjs');
237
+ const { getEffectiveAuthority, classifyDriftSeverity } = require('./lib/plan-drift-guard.cjs');
228
238
 
229
239
  // ─── Bridge collapsed (Phase 4) ────────────────────────────────────────────────
230
240
  // Non-family commands now run through their CJS handlers directly. Keep the
@@ -508,10 +518,10 @@ async function main() {
508
518
  const TOP_LEVEL_USAGE = 'Usage: gsd-tools <command> [args] [--raw] [--pick <field>] [--cwd <path>] [--ws <name>] [--json-errors]\n' +
509
519
  'Commands: agent, agent-skills, audit-open, audit-uat, check, check-commit, commit, commit-to-subrepo, ' +
510
520
  'config-ensure-section, config-get, config-new-project, config-path, config-set, migrate-config, ' +
511
- 'current-timestamp, detect-custom-files, docs-init, effort, extract-messages, find-phase, ' +
521
+ 'current-timestamp, detect-custom-files, docs-init, drift-guard, effort, extract-messages, find-phase, ' +
512
522
  'from-gsd2, frontmatter, gap-analysis, generate-claude-md, generate-claude-profile, ' +
513
523
  'generate-dev-preferences, generate-slug, graphify, history-digest, init, intel, ' +
514
- 'capability, classify-confidence, learnings, list-todos, loop, milestone, package-legitimacy, phase, phase-plan-index, phases, profile-questionnaire, ' +
524
+ 'capability, classify-confidence, git, learnings, list-todos, loop, milestone, package-legitimacy, phase, phase-plan-index, phases, profile-questionnaire, ' +
515
525
  'profile-sample, progress, prompt-budget, requirements, research-plan, research-store, resolve-granularity, resolve-model, roadmap, scaffold, state, ' +
516
526
  'task, template, user-story, validate, verify, verify-path-exists, verify-summary, workstream, worktree\n\n' +
517
527
  'Global flags:\n' +
@@ -1303,9 +1313,85 @@ async function runCommand(command, args, cwd, raw, defaultValue, originalCommand
1303
1313
  }
1304
1314
  const resolvedConfigDir = configDir ? path.resolve(configDir) : null;
1305
1315
  capabilityState.cmdCapabilityState(cwd, resolvedConfigDir, raw, {});
1316
+ } else if (capSubcommand === 'set') {
1317
+ // capability set <id> [--on|--off|--enable|--disable] [--gate <key>=<bool>]... [--config-dir <dir>] [--runtime <r>] [--scope <s>]
1318
+ const capId = args[2];
1319
+ if (!capId || capId.startsWith('--')) {
1320
+ error('Missing capability id for: capability set <id>', core.ERROR_REASON ? core.ERROR_REASON.USAGE : undefined);
1321
+ }
1322
+ // Parse --config-dir
1323
+ const setConfigDirIdx = args.indexOf('--config-dir');
1324
+ let setConfigDir = null;
1325
+ if (setConfigDirIdx !== -1) {
1326
+ const setConfigDirVal = args[setConfigDirIdx + 1];
1327
+ if (!setConfigDirVal || setConfigDirVal.startsWith('--')) {
1328
+ error('Missing value for --config-dir', core.ERROR_REASON ? core.ERROR_REASON.USAGE : undefined);
1329
+ }
1330
+ setConfigDir = setConfigDirVal;
1331
+ }
1332
+ const resolvedSetConfigDir = setConfigDir ? path.resolve(setConfigDir) : null;
1333
+ // Parse --on/--enable and --off/--disable (mutually exclusive)
1334
+ const hasOn = args.includes('--on') || args.includes('--enable');
1335
+ const hasOff = args.includes('--off') || args.includes('--disable');
1336
+ if (hasOn && hasOff) {
1337
+ error('Conflicting flags: --on/--enable and --off/--disable cannot both be present', core.ERROR_REASON ? core.ERROR_REASON.USAGE : undefined);
1338
+ }
1339
+ let setEnabled;
1340
+ if (hasOn) {
1341
+ setEnabled = true;
1342
+ } else if (hasOff) {
1343
+ setEnabled = false;
1344
+ }
1345
+ // Parse --gate <key>=<bool> (repeatable)
1346
+ const setGates = {};
1347
+ for (let gi = 0; gi < args.length; gi++) {
1348
+ if (args[gi] === '--gate') {
1349
+ const gateVal = args[gi + 1];
1350
+ if (!gateVal || gateVal.startsWith('--')) {
1351
+ error('Missing value for --gate (expected <key>=<true|false>)', core.ERROR_REASON ? core.ERROR_REASON.USAGE : undefined);
1352
+ }
1353
+ const eqIdx = gateVal.indexOf('=');
1354
+ if (eqIdx === -1) {
1355
+ error(`Malformed --gate value "${gateVal}": expected <key>=<true|false>`, core.ERROR_REASON ? core.ERROR_REASON.USAGE : undefined);
1356
+ }
1357
+ const gateKey = gateVal.slice(0, eqIdx);
1358
+ const gateBoolStr = gateVal.slice(eqIdx + 1);
1359
+ if (gateBoolStr !== 'true' && gateBoolStr !== 'false') {
1360
+ error(`Malformed --gate value "${gateVal}": bool must be true or false`, core.ERROR_REASON ? core.ERROR_REASON.USAGE : undefined);
1361
+ }
1362
+ setGates[gateKey] = gateBoolStr === 'true';
1363
+ gi++; // skip consumed value
1364
+ }
1365
+ }
1366
+ // Parse --runtime and --scope (validate that values are present and not flags)
1367
+ const runtimeIdx = args.indexOf('--runtime');
1368
+ let setRuntime;
1369
+ if (runtimeIdx !== -1) {
1370
+ const runtimeVal = args[runtimeIdx + 1];
1371
+ if (!runtimeVal || runtimeVal.startsWith('--')) {
1372
+ error('Missing value for --runtime', core.ERROR_REASON ? core.ERROR_REASON.USAGE : undefined);
1373
+ }
1374
+ setRuntime = runtimeVal;
1375
+ }
1376
+ const scopeIdx = args.indexOf('--scope');
1377
+ let setScope;
1378
+ if (scopeIdx !== -1) {
1379
+ const scopeVal = args[scopeIdx + 1];
1380
+ if (!scopeVal || scopeVal.startsWith('--')) {
1381
+ error('Missing value for --scope', core.ERROR_REASON ? core.ERROR_REASON.USAGE : undefined);
1382
+ }
1383
+ setScope = scopeVal;
1384
+ }
1385
+ capabilityWriter.cmdCapabilitySet(
1386
+ cwd,
1387
+ resolvedSetConfigDir,
1388
+ capId,
1389
+ { enabled: setEnabled, gates: Object.keys(setGates).length > 0 ? setGates : undefined, runtime: setRuntime, scope: setScope },
1390
+ raw,
1391
+ );
1306
1392
  } else {
1307
1393
  error(
1308
- `Unknown capability subcommand: ${capSubcommand}. Available: state`,
1394
+ `Unknown capability subcommand: ${capSubcommand}. Available: state, set`,
1309
1395
  core.ERROR_REASON ? core.ERROR_REASON.SDK_UNKNOWN_COMMAND : undefined,
1310
1396
  );
1311
1397
  }
@@ -1948,6 +2034,23 @@ async function runCommand(command, args, cwd, raw, defaultValue, originalCommand
1948
2034
  // Each slot must be non-empty and contain non-whitespace content.
1949
2035
  //
1950
2036
  // No .planning/ access needed — pure string validation.
2037
+
2038
+ // #1146: single base-branch resolver for all forking workflows.
2039
+ // Workflows call `gsd_run query git.base-branch` (dotted form normalised to
2040
+ // command='git', args=['git','base-branch']).
2041
+ case 'git': {
2042
+ const subcommand = args[1];
2043
+ if (subcommand !== 'base-branch') {
2044
+ error(
2045
+ `Unknown git subcommand: ${subcommand || '(none)'}. Available: base-branch`,
2046
+ ERROR_REASON.SDK_UNKNOWN_COMMAND,
2047
+ );
2048
+ break;
2049
+ }
2050
+ cmdGitBaseBranch(cwd, args.slice(2));
2051
+ break;
2052
+ }
2053
+
1951
2054
  case 'user-story': {
1952
2055
  const subcommand = args[1];
1953
2056
  if (subcommand !== 'validate') {
@@ -2002,6 +2105,67 @@ async function runCommand(command, args, cwd, raw, defaultValue, originalCommand
2002
2105
  break;
2003
2106
  }
2004
2107
 
2108
+ case 'drift-guard': {
2109
+ // ADR-22: deterministic authority resolution + severity classification.
2110
+ // Subcommands:
2111
+ // drift-guard authority → effective authority string
2112
+ // drift-guard severity --status <S> [--authority <A>] → {severity, hardBlock}
2113
+ const subcommand = args[1];
2114
+
2115
+ // Read config.json directly for both plan_review.source_grounding_authority
2116
+ // and intel.enabled. Neither key is in the config-loader.cjs whitelist that
2117
+ // core.loadConfig() returns; plan_review is only in config.cjs's private
2118
+ // buildConfig(), and intel is a federated capability config key.
2119
+ let configuredAuthority = 'grep';
2120
+ let intelEnabled = false;
2121
+ try {
2122
+ const { planningDir } = require('./lib/planning-workspace.cjs');
2123
+ const cfgPath = require('path').join(planningDir(cwd), 'config.json');
2124
+ if (require('fs').existsSync(cfgPath)) {
2125
+ const rawCfg = JSON.parse(require('fs').readFileSync(cfgPath, 'utf-8'));
2126
+ if (rawCfg && rawCfg.plan_review && rawCfg.plan_review.source_grounding_authority) {
2127
+ configuredAuthority = String(rawCfg.plan_review.source_grounding_authority);
2128
+ }
2129
+ if (rawCfg && rawCfg.intel && rawCfg.intel.enabled === true) {
2130
+ intelEnabled = true;
2131
+ }
2132
+ }
2133
+ } catch {
2134
+ // not fatal — defaults apply
2135
+ }
2136
+
2137
+ const effectiveAuthority = getEffectiveAuthority(configuredAuthority, intelEnabled);
2138
+
2139
+ if (subcommand === 'authority') {
2140
+ // Pass rawValue as 3rd arg so --raw returns unquoted string (not JSON)
2141
+ core.output(effectiveAuthority, raw, effectiveAuthority);
2142
+ break;
2143
+ }
2144
+
2145
+ if (subcommand === 'severity') {
2146
+ const statusIdx = args.indexOf('--status');
2147
+ const statusVal = statusIdx !== -1 ? args[statusIdx + 1] : undefined;
2148
+ if (!statusVal || statusVal.startsWith('--')) {
2149
+ error('drift-guard severity requires --status <VERIFIED|MISSING|AMBIGUOUS|UNCHECKABLE>', ERROR_REASON.SDK_UNKNOWN_COMMAND);
2150
+ break;
2151
+ }
2152
+ const authIdx = args.indexOf('--authority');
2153
+ const authVal = authIdx !== -1 ? args[authIdx + 1] : undefined;
2154
+ const authorityForClassify = (authVal && !authVal.startsWith('--'))
2155
+ ? authVal
2156
+ : effectiveAuthority;
2157
+ const result = classifyDriftSeverity({ status: statusVal, authority: authorityForClassify });
2158
+ core.output(result, raw);
2159
+ break;
2160
+ }
2161
+
2162
+ error(
2163
+ `Unknown drift-guard subcommand: ${subcommand || '(none)'}. Available: authority, severity`,
2164
+ ERROR_REASON.SDK_UNKNOWN_COMMAND,
2165
+ );
2166
+ break;
2167
+ }
2168
+
2005
2169
  default: {
2006
2170
  // ADR-959: try capability-registry dispatch before emitting the unknown-command error.
2007
2171
  // An unmigrated command still hits its hardcoded `case` above — untouched.
@@ -47,6 +47,11 @@ function sanitizeWorkstreamSessionToken(value) {
47
47
  const token = raw.trim().replace(/[^a-zA-Z0-9._-]+/g, '_').replace(/^_+|_+$/g, '');
48
48
  return token ? token.slice(0, 160) : null;
49
49
  }
50
+ /** Test-only seam: clear the memoized controlling-TTY probe cache (#1191). */
51
+ function _resetControllingTtyCacheForTests() {
52
+ cachedControllingTtyToken = null;
53
+ didProbeControllingTtyToken = false;
54
+ }
50
55
  function probeControllingTtyToken() {
51
56
  if (didProbeControllingTtyToken)
52
57
  return cachedControllingTtyToken;
@@ -288,4 +293,5 @@ module.exports = {
288
293
  parseCliWorkstream,
289
294
  resolveActiveWorkstream,
290
295
  applyResolvedWorkstreamEnv,
296
+ _resetControllingTtyCacheForTests,
291
297
  };
@@ -32,6 +32,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
32
32
  return (mod && mod.__esModule) ? mod : { "default": mod };
33
33
  };
34
34
  const node_path_1 = __importDefault(require("node:path"));
35
+ const node_fs_1 = __importDefault(require("node:fs"));
35
36
  // eslint-disable-next-line @typescript-eslint/no-require-imports
36
37
  const core = require("./core.cjs");
37
38
  const { output: coreOutput, error: coreError } = core;
@@ -43,7 +44,7 @@ const configLoaderMod = require("./config-loader.cjs");
43
44
  const { loadConfig } = configLoaderMod;
44
45
  // eslint-disable-next-line @typescript-eslint/no-require-imports
45
46
  const installProfilesMod = require("./install-profiles.cjs");
46
- const { readActiveProfile, loadSkillsManifest, resolveProfile } = installProfilesMod;
47
+ const { readActiveProfile, loadSkillsManifest, resolveProfile, parseRequires } = installProfilesMod;
47
48
  // eslint-disable-next-line @typescript-eslint/no-require-imports
48
49
  const surfaceMod = require("./surface.cjs");
49
50
  const { resolveSurface } = surfaceMod;
@@ -193,6 +194,93 @@ function _resolveCommandsGsdDir() {
193
194
  const repoRoot = node_path_1.default.resolve(__dirname, '..', '..', '..');
194
195
  return node_path_1.default.join(repoRoot, 'commands', 'gsd');
195
196
  }
197
+ /**
198
+ * Build a skill dependency manifest from an INSTALLED runtime's skills directory.
199
+ *
200
+ * In an installed runtime (e.g. Codex at ~/.codex), gsd skills live as
201
+ * configDir/skills/gsd-STEM/SKILL.md. There is no commands/gsd source tree.
202
+ * This function scans that installed layout and builds the same
203
+ * Map shape that loadSkillsManifest produces from sources.
204
+ *
205
+ * Stem extraction: a directory named gsd-secure-phase maps to stem secure-phase.
206
+ * Only directories whose names start with gsd- are included so user-created
207
+ * skills (without the gsd- prefix) are not accidentally pulled in.
208
+ *
209
+ * The requires: field is parsed via the shared parseRequires helper (the same
210
+ * parser loadSkillsManifest uses), so the two paths cannot drift.
211
+ *
212
+ * Returns an empty Map when the skills dir does not exist.
213
+ */
214
+ function _loadInstalledSkillsManifest(configDir) {
215
+ const manifest = new Map();
216
+ const skillsDir = node_path_1.default.join(configDir, 'skills');
217
+ if (!node_fs_1.default.existsSync(skillsDir))
218
+ return manifest;
219
+ let entries;
220
+ try {
221
+ entries = node_fs_1.default.readdirSync(skillsDir, { withFileTypes: true });
222
+ }
223
+ catch {
224
+ return manifest;
225
+ }
226
+ for (const entry of entries) {
227
+ if (!entry.isDirectory())
228
+ continue;
229
+ if (!entry.name.startsWith('gsd-'))
230
+ continue;
231
+ // Strip the 'gsd-' prefix to get the skill stem
232
+ const stem = entry.name.slice(4); // 'gsd-'.length === 4
233
+ if (!stem)
234
+ continue;
235
+ const skillMdPath = node_path_1.default.join(skillsDir, entry.name, 'SKILL.md');
236
+ // Parity with loadSkillsManifest: a stem exists only when its artifact
237
+ // file is present. loadSkillsManifest registers a stem per .md FILE (and
238
+ // tolerates an unreadable file as []), but never invents a stem for which
239
+ // no file exists. Mirror that here: a stale gsd-<stem>/ directory with no
240
+ // SKILL.md must NOT register the stem — otherwise the capability would be
241
+ // wrongly reported surfaced/enabled and a verify:post hook would render
242
+ // for a skill that cannot run.
243
+ if (!node_fs_1.default.existsSync(skillMdPath))
244
+ continue;
245
+ let content = '';
246
+ try {
247
+ content = node_fs_1.default.readFileSync(skillMdPath, 'utf8');
248
+ }
249
+ catch {
250
+ // SKILL.md present but unreadable — register with no deps (parity with
251
+ // loadSkillsManifest's readFileSync catch branch).
252
+ }
253
+ // Parse requires: via the SAME shared parser loadSkillsManifest uses, so
254
+ // installed-runtime dependency resolution can never silently diverge from
255
+ // the source-tree path (single source of truth — no duplicated regex).
256
+ manifest.set(stem, content ? parseRequires(content) : []);
257
+ // Mirror loadSkillsManifest's Map shape: it always sets a companion
258
+ // `_calls_agents_<stem>` key. Installed SKILL.md bodies carry no
259
+ // recoverable agent-call refs, so [] (the no-agents case) keeps the two
260
+ // manifest shapes identical and prevents undefined-vs-[] drift for any
261
+ // consumer that reads the agent-refs companion key.
262
+ manifest.set(`_calls_agents_${stem}`, []);
263
+ }
264
+ return manifest;
265
+ }
266
+ /**
267
+ * Resolve the skill dependency manifest for capability-state resolution.
268
+ *
269
+ * Resolution order (fixes #1160 — installed-runtime capability surface):
270
+ * 1. If commandsGsdDir exists, load from source (repo-checkout behavior).
271
+ * 2. Otherwise, fall back to installed skills at configDir/skills/gsd-[stem]/SKILL.md.
272
+ *
273
+ * In an installed runtime the commands/gsd source tree is absent; only the
274
+ * skills/ layout exists. Returning an empty manifest caused resolveSurface to
275
+ * materialise the full-sentinel to an empty Set, making every capability appear
276
+ * unsurfaced even when the skill was physically installed.
277
+ */
278
+ function _resolveManifest(commandsGsdDir, configDir) {
279
+ if (node_fs_1.default.existsSync(commandsGsdDir)) {
280
+ return loadSkillsManifest(commandsGsdDir);
281
+ }
282
+ return _loadInstalledSkillsManifest(configDir);
283
+ }
196
284
  /**
197
285
  * Command entry point: resolve install profile, surface, and config; compute
198
286
  * capability state; emit the envelope via core.output.
@@ -265,7 +353,9 @@ function resolveCapabilityRuntimeState(cwd, runtimeConfigDir) {
265
353
  let installedSkills;
266
354
  try {
267
355
  const commandsGsdDir = _resolveCommandsGsdDir();
268
- const manifest = loadSkillsManifest(commandsGsdDir);
356
+ // Fix #1160: use _resolveManifest so installed-runtime layouts (where
357
+ // commands/gsd is absent) fall back to <configDir>/skills/gsd-*/SKILL.md.
358
+ const manifest = _resolveManifest(commandsGsdDir, resolvedConfigDir);
269
359
  const profileName = readActiveProfile(resolvedConfigDir) ?? 'full';
270
360
  const resolvedInstall = resolveProfile({
271
361
  modes: profileName.split(',').map((s) => s.trim()),
@@ -285,7 +375,9 @@ function resolveCapabilityRuntimeState(cwd, runtimeConfigDir) {
285
375
  let surfacedSkills;
286
376
  try {
287
377
  const commandsGsdDir = _resolveCommandsGsdDir();
288
- const manifest = loadSkillsManifest(commandsGsdDir);
378
+ // Fix #1160: use _resolveManifest so installed-runtime layouts (where
379
+ // commands/gsd is absent) fall back to <configDir>/skills/gsd-*/SKILL.md.
380
+ const manifest = _resolveManifest(commandsGsdDir, resolvedConfigDir);
289
381
  const surfaceResult = resolveSurface(resolvedConfigDir, manifest, undefined, registry);
290
382
  // resolveSurface returns { name, skills: Set<string>, agents: Set<string> }
291
383
  // (always a concrete Set — full profile is materialized)
@@ -345,5 +437,7 @@ module.exports = {
345
437
  cmdCapabilityState,
346
438
  // Exported for tests
347
439
  _resolveCommandsGsdDir,
440
+ _loadInstalledSkillsManifest,
441
+ _resolveManifest,
348
442
  _isSafePropKey,
349
443
  };