@opengsd/gsd-core 1.17.0 → 1.18.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 (176) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/agents/gsd-advisor-researcher.compact.md +1 -1
  4. package/agents/gsd-advisor-researcher.md +1 -1
  5. package/agents/gsd-assumptions-analyzer.compact.md +1 -1
  6. package/agents/gsd-assumptions-analyzer.md +1 -1
  7. package/agents/gsd-code-fixer.compact.md +1 -1
  8. package/agents/gsd-code-fixer.md +1 -1
  9. package/agents/gsd-code-reviewer.compact.md +1 -1
  10. package/agents/gsd-code-reviewer.md +1 -1
  11. package/agents/gsd-codebase-mapper.compact.md +1 -1
  12. package/agents/gsd-codebase-mapper.md +1 -1
  13. package/agents/gsd-debugger.md +1 -1
  14. package/agents/gsd-doc-verifier.compact.md +4 -4
  15. package/agents/gsd-doc-verifier.md +4 -3
  16. package/agents/gsd-doc-writer.compact.md +23 -5
  17. package/agents/gsd-doc-writer.md +19 -6
  18. package/agents/gsd-eval-auditor.compact.md +1 -1
  19. package/agents/gsd-eval-auditor.md +1 -1
  20. package/agents/gsd-executor.md +7 -3
  21. package/agents/gsd-integration-checker.compact.md +1 -1
  22. package/agents/gsd-integration-checker.md +1 -1
  23. package/agents/gsd-nyquist-auditor.compact.md +1 -1
  24. package/agents/gsd-nyquist-auditor.md +1 -1
  25. package/agents/gsd-phase-researcher.md +1 -1
  26. package/agents/gsd-plan-checker.md +2 -1
  27. package/agents/gsd-planner.md +5 -4
  28. package/agents/gsd-project-researcher.compact.md +1 -1
  29. package/agents/gsd-project-researcher.md +1 -1
  30. package/agents/gsd-research-synthesizer.compact.md +5 -5
  31. package/agents/gsd-research-synthesizer.md +5 -5
  32. package/agents/gsd-roadmapper.compact.md +1 -1
  33. package/agents/gsd-roadmapper.md +1 -1
  34. package/agents/gsd-security-auditor.compact.md +1 -1
  35. package/agents/gsd-security-auditor.md +1 -1
  36. package/agents/gsd-ui-auditor.compact.md +1 -1
  37. package/agents/gsd-ui-auditor.md +1 -1
  38. package/agents/gsd-ui-checker.compact.md +1 -1
  39. package/agents/gsd-ui-checker.md +1 -1
  40. package/agents/gsd-ui-researcher.compact.md +13 -1
  41. package/agents/gsd-ui-researcher.md +28 -1
  42. package/agents/gsd-verifier.md +1 -1
  43. package/bin/install.js +38 -2
  44. package/commands/gsd/code-review.md +8 -0
  45. package/commands/gsd/pause-work.md +1 -1
  46. package/gsd-core/bin/gsd-tools.cjs +18 -11
  47. package/gsd-core/bin/lib/capability-ledger.cjs +25 -0
  48. package/gsd-core/bin/lib/capability-lock.cjs +29 -7
  49. package/gsd-core/bin/lib/capability-registry.cjs +70 -64
  50. package/gsd-core/bin/lib/capability-source.cjs +3 -1
  51. package/gsd-core/bin/lib/capability-state.cjs +6 -3
  52. package/gsd-core/bin/lib/capability-validator.cjs +6 -4
  53. package/gsd-core/bin/lib/commands.cjs +5 -1
  54. package/gsd-core/bin/lib/config-loader.cjs +3 -3
  55. package/gsd-core/bin/lib/decision-coverage-support.cjs +19 -0
  56. package/gsd-core/bin/lib/gate-decision-coverage-plan.cjs +15 -0
  57. package/gsd-core/bin/lib/gate-decision-coverage-verify.cjs +13 -0
  58. package/gsd-core/bin/lib/gate-predicate-evaluator.cjs +23 -3
  59. package/gsd-core/bin/lib/gate-predicate.cjs +8 -12
  60. package/gsd-core/bin/lib/gate-tdd-review-checkpoint.cjs +60 -5
  61. package/gsd-core/bin/lib/init.cjs +11 -4
  62. package/gsd-core/bin/lib/install-effort-resolver.cjs +2 -2
  63. package/gsd-core/bin/lib/install-model-override-resolver.cjs +4 -3
  64. package/gsd-core/bin/lib/install-profiles.cjs +5 -3
  65. package/gsd-core/bin/lib/phase.cjs +120 -11
  66. package/gsd-core/bin/lib/report-parser.cjs +57 -1
  67. package/gsd-core/bin/lib/review-lane-invocation.cjs +40 -0
  68. package/gsd-core/bin/lib/review-reviewer-selection.cjs +20 -1
  69. package/gsd-core/bin/lib/roadmap.cjs +56 -5
  70. package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +47 -2
  71. package/gsd-core/bin/lib/runtime-homes.cjs +18 -0
  72. package/gsd-core/bin/lib/shell-command-projection.cjs +2 -0
  73. package/gsd-core/bin/lib/smart-entry.cjs +24 -16
  74. package/gsd-core/bin/lib/state-document.cjs +37 -3
  75. package/gsd-core/bin/lib/state-transition.cjs +31 -10
  76. package/gsd-core/bin/lib/state.cjs +90 -60
  77. package/gsd-core/bin/lib/task-command-router.cjs +2 -3
  78. package/gsd-core/bin/lib/tdd-red-evidence.cjs +5 -0
  79. package/gsd-core/bin/lib/tdd-test-files.cjs +42 -0
  80. package/gsd-core/bin/lib/verify.cjs +36 -1
  81. package/gsd-core/references/agent-contracts.md +1 -1
  82. package/gsd-core/references/agent-skills-bootstrap.md +26 -7
  83. package/gsd-core/references/git-integration.md +3 -3
  84. package/gsd-core/references/planner-reversibility.md +18 -4
  85. package/gsd-core/references/planner-verify-command-grounding.md +18 -0
  86. package/gsd-core/references/tdd.md +4 -3
  87. package/gsd-core/references/verifier-phase-gates.md +8 -1
  88. package/gsd-core/workflows/add-backlog.md +10 -2
  89. package/gsd-core/workflows/add-phase.md +5 -1
  90. package/gsd-core/workflows/add-tests.md +5 -1
  91. package/gsd-core/workflows/add-todo.md +5 -1
  92. package/gsd-core/workflows/ai-integration-phase.md +5 -1
  93. package/gsd-core/workflows/audit-milestone.md +21 -9
  94. package/gsd-core/workflows/autonomous/templates/minimal-context.md +41 -0
  95. package/gsd-core/workflows/autonomous.md +52 -64
  96. package/gsd-core/workflows/check-todos.md +5 -1
  97. package/gsd-core/workflows/code-review-fix.md +5 -1
  98. package/gsd-core/workflows/code-review.md +5 -1
  99. package/gsd-core/workflows/complete-milestone/detail/elaboration.md +5 -1
  100. package/gsd-core/workflows/complete-milestone.md +25 -5
  101. package/gsd-core/workflows/debug.md +5 -1
  102. package/gsd-core/workflows/diagnose-issues.md +5 -1
  103. package/gsd-core/workflows/discuss-phase/modes/chain.md +5 -1
  104. package/gsd-core/workflows/discuss-phase-assumptions.md +11 -3
  105. package/gsd-core/workflows/discuss-phase.md +5 -1
  106. package/gsd-core/workflows/docs-update/detail/elaboration.md +7 -6
  107. package/gsd-core/workflows/docs-update.md +27 -5
  108. package/gsd-core/workflows/edit-phase.md +5 -1
  109. package/gsd-core/workflows/eval-review.md +11 -3
  110. package/gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md +5 -1
  111. package/gsd-core/workflows/execute-phase/steps/post-merge-gate.md +12 -6
  112. package/gsd-core/workflows/execute-phase/steps/regression-gate-run.md +19 -2
  113. package/gsd-core/workflows/execute-phase/steps/regression-gate.md +18 -1
  114. package/gsd-core/workflows/execute-phase/steps/verify-phase-goal.md +5 -1
  115. package/gsd-core/workflows/execute-phase.md +37 -6
  116. package/gsd-core/workflows/execute-plan.md +8 -4
  117. package/gsd-core/workflows/extract-learnings.md +6 -2
  118. package/gsd-core/workflows/forensics.md +5 -1
  119. package/gsd-core/workflows/import.md +5 -1
  120. package/gsd-core/workflows/insert-phase.md +5 -1
  121. package/gsd-core/workflows/manager.md +10 -2
  122. package/gsd-core/workflows/map-codebase.md +5 -1
  123. package/gsd-core/workflows/milestone-summary.md +10 -2
  124. package/gsd-core/workflows/new-milestone.md +13 -5
  125. package/gsd-core/workflows/new-project.md +9 -5
  126. package/gsd-core/workflows/next.md +8 -4
  127. package/gsd-core/workflows/pause-work.md +3 -3
  128. package/gsd-core/workflows/plan-phase/steps/auto-advance-check.md +70 -0
  129. package/gsd-core/workflows/plan-phase/steps/chunked-planning-mode.md +5 -1
  130. package/gsd-core/workflows/plan-phase/steps/closed-phase-gate.md +6 -2
  131. package/gsd-core/workflows/plan-phase.md +41 -73
  132. package/gsd-core/workflows/plan-review-convergence.md +17 -5
  133. package/gsd-core/workflows/plant-seed.md +8 -4
  134. package/gsd-core/workflows/progress.md +6 -2
  135. package/gsd-core/workflows/quick-batch.md +5 -1
  136. package/gsd-core/workflows/quick.md +22 -7
  137. package/gsd-core/workflows/remove-phase.md +5 -1
  138. package/gsd-core/workflows/resume-project.md +5 -1
  139. package/gsd-core/workflows/review.md +6 -2
  140. package/gsd-core/workflows/scan.md +5 -1
  141. package/gsd-core/workflows/secure-phase.md +6 -2
  142. package/gsd-core/workflows/settings.md +3 -3
  143. package/gsd-core/workflows/ship.md +13 -10
  144. package/gsd-core/workflows/thread.md +10 -2
  145. package/gsd-core/workflows/transition.md +5 -1
  146. package/gsd-core/workflows/ui-phase.md +5 -1
  147. package/gsd-core/workflows/ui-review.md +6 -2
  148. package/gsd-core/workflows/ultraplan-phase.md +5 -1
  149. package/gsd-core/workflows/update.md +5 -1
  150. package/gsd-core/workflows/validate-phase.md +6 -2
  151. package/gsd-core/workflows/verify-work/detail/elaboration.md +1 -1
  152. package/gsd-core/workflows/verify-work/steps/mvp-uat-framing.md +5 -1
  153. package/gsd-core/workflows/verify-work.md +11 -3
  154. package/hooks/dist/gsd-agent-isolation-guard.js +3 -1
  155. package/hooks/dist/gsd-cursor-subagent-start.js +2 -1
  156. package/hooks/dist/gsd-secret-read-guard.js +72 -14
  157. package/hooks/dist/gsd-validate-commit.sh +27 -5
  158. package/hooks/gsd-agent-isolation-guard.js +3 -1
  159. package/hooks/gsd-cursor-subagent-start.js +2 -1
  160. package/hooks/gsd-secret-read-guard.js +72 -14
  161. package/hooks/gsd-validate-commit.sh +27 -5
  162. package/package.json +1 -1
  163. package/scripts/audit-workflow-script-paths.cjs +1 -1
  164. package/scripts/diagnose-validate-commit-in-tier.cjs +270 -0
  165. package/scripts/diagnose-validate-commit-phases.cjs +348 -0
  166. package/scripts/diagnose-windows-spawn-watcher.cjs +317 -0
  167. package/scripts/docs-guard-registry.cjs +2 -0
  168. package/scripts/lib/macos-conformance-tier.generated.cjs +9 -0
  169. package/scripts/lib/platform-conformance-tier.generated.cjs +16 -0
  170. package/scripts/lint-phase-arg-assignment.cjs +2 -2
  171. package/scripts/lint-workflow-shellcheck-baseline.json +16 -16
  172. package/scripts/release-tarball-smoke.cjs +1 -0
  173. package/scripts/xtrace-phase-timing.cjs +205 -0
  174. package/skills/gsd-code-review/SKILL.md +8 -0
  175. package/skills/gsd-pause-work/SKILL.md +1 -1
  176. package/vscode/package.json +1 -1
@@ -47,6 +47,8 @@ const { loadConfig } = configLoaderMod;
47
47
  const installProfilesMod = require("./install-profiles.cjs");
48
48
  const { readActiveProfile, loadSkillsManifest, resolveProfile, parseRequires, parseCallsAgents, workflowAgentRefs } = installProfilesMod;
49
49
  // eslint-disable-next-line @typescript-eslint/no-require-imports
50
+ const ledgerMod = require("./capability-ledger.cjs");
51
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
50
52
  const surfaceMod = require("./surface.cjs");
51
53
  const { resolveSurface } = surfaceMod;
52
54
  // ─── Prototype-pollution guard (inline literal, CodeQL barrier) ───────────────
@@ -265,16 +267,17 @@ function _loadInstalledSkillsManifest(configDir) {
265
267
  // SKILL.md must NOT register the stem — otherwise the capability would be
266
268
  // wrongly reported surfaced/enabled and a verify:post hook would render
267
269
  // for a skill that cannot run.
268
- if (!node_fs_1.default.existsSync(skillMdPath))
269
- continue;
270
+ // #5121: bounded read — a missing, non-regular (FIFO/device) or oversized SKILL.md registers no stem.
270
271
  let content = '';
271
272
  try {
272
- content = node_fs_1.default.readFileSync(skillMdPath, 'utf8');
273
+ content = ledgerMod.readSkillFile(skillMdPath);
273
274
  }
274
275
  catch {
275
276
  // SKILL.md present but unreadable — register with no deps (parity with
276
277
  // loadSkillsManifest's readFileSync catch branch).
277
278
  }
279
+ if (content === null)
280
+ continue;
278
281
  // Parse requires: via the SAME shared parser loadSkillsManifest uses, so
279
282
  // installed-runtime dependency resolution can never silently diverge from
280
283
  // the source-tree path (single source of truth — no duplicated regex).
@@ -1899,6 +1899,10 @@ const REVIEWER_EFFORT_LEVELS = new Set(['minimal', 'low', 'medium', 'high', 'xhi
1899
1899
  * silently rot away from reality (DEFECT.GENERATIVE-FIX).
1900
1900
  */
1901
1901
  const KNOWN_HOST_BEHAVIORS = new Set([
1902
+ // #2586: declares runtime-level feature axes GSD does not/cannot support on
1903
+ // omitting the key on every other runtime carries no inverted meaning.
1904
+ // this host (e.g. Codex's `["context-warnings","phase-lifecycle-display"]`)
1905
+ // — present-and-populated / absent-is-unsupported-empty convention, so
1902
1906
  'agentFileExtension',
1903
1907
  'agentFrontmatterExtensions',
1904
1908
  'agentManifestStyle',
@@ -1916,6 +1920,7 @@ const KNOWN_HOST_BEHAVIORS = new Set([
1916
1920
  'combinedFamilyInstall',
1917
1921
  'commandBodyConverter',
1918
1922
  'contentRewriteProfile',
1923
+ 'defaultHomeIncludeRoot',
1919
1924
  'doneBannerStyle',
1920
1925
  'flatCommandDir',
1921
1926
  'frontmatterDialect',
@@ -1953,6 +1958,7 @@ const KNOWN_HOST_BEHAVIORS = new Set([
1953
1958
  'requiresSubagentPair',
1954
1959
  'restoreAtRefTildeInAgents',
1955
1960
  'restoreAtRefTildeInSpecTree',
1961
+ 'retargetsGsdCoreProsePath',
1956
1962
  'retiredArtifacts',
1957
1963
  'rewriteClaudeAtIncludes',
1958
1964
  'settingsFileByScope',
@@ -1972,10 +1978,6 @@ const KNOWN_HOST_BEHAVIORS = new Set([
1972
1978
  'specRootSkillPass',
1973
1979
  'tomlConfigInstall',
1974
1980
  'trackCategoryDescription',
1975
- // #2586: declares runtime-level feature axes GSD does not/cannot support on
1976
- // this host (e.g. Codex's `["context-warnings","phase-lifecycle-display"]`)
1977
- // — present-and-populated / absent-is-unsupported-empty convention, so
1978
- // omitting the key on every other runtime carries no inverted meaning.
1979
1981
  'unsupportedFeatures',
1980
1982
  'verificationStyle',
1981
1983
  'writeCategoryDescription',
@@ -2986,6 +2986,8 @@ async function cmdWebsearch(query, options, raw) {
2986
2986
  'Accept': 'application/json',
2987
2987
  'X-Subscription-Token': apiKey
2988
2988
  },
2989
+ // never follow redirects: X-Subscription-Token must not leave api.search.brave.com (#5409)
2990
+ redirect: 'manual',
2989
2991
  signal: ac.signal
2990
2992
  });
2991
2993
  }
@@ -3011,7 +3013,9 @@ async function cmdWebsearch(query, options, raw) {
3011
3013
  const status = response.status;
3012
3014
  const isRetryable = status === 429 || status >= 500;
3013
3015
  if (!isRetryable) {
3014
- // Non-retryable 4xx — fail immediately, no attempts field
3016
+ // Non-retryable 4xx and every 3xx (redirect: 'manual' hands them back as-is, #5409) —
3017
+ // fail immediately, no attempts field. Keep 3xx out of isRetryable.
3018
+ response.body?.cancel().catch(() => { });
3015
3019
  output({ available: false, error: `API error: ${status}` }, raw, '');
3016
3020
  return;
3017
3021
  }
@@ -25,11 +25,11 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
25
25
  return (mod && mod.__esModule) ? mod : { "default": mod };
26
26
  };
27
27
  const node_fs_1 = __importDefault(require("node:fs"));
28
- const node_os_1 = __importDefault(require("node:os"));
29
28
  const node_path_1 = __importDefault(require("node:path"));
30
29
  const shell_command_projection_cjs_1 = require("./shell-command-projection.cjs");
31
30
  // #4717: runtime-identity fill — env rung + per-install marker rung.
32
31
  const runtime_slash_cjs_1 = require("./runtime-slash.cjs");
32
+ const runtime_homes_cjs_1 = require("./runtime-homes.cjs");
33
33
  const runtime_name_policy_cjs_1 = require("./runtime-name-policy.cjs");
34
34
  // eslint-disable-next-line @typescript-eslint/no-require-imports
35
35
  const planningWorkspace = require("./planning-workspace.cjs");
@@ -935,7 +935,7 @@ function loadConfigResolvedInternal(cwd, options = {}) {
935
935
  // untouched. Faults in the global file stay silent in this branch (the
936
936
  // project config governs; the nearer file is the actionable one).
937
937
  try {
938
- const shadowHome = process.env['GSD_HOME'] || node_os_1.default.homedir();
938
+ const shadowHome = (0, runtime_homes_cjs_1.resolveGsdHome)();
939
939
  const shadowPath = node_path_1.default.join(shadowHome, '.gsd', 'defaults.json');
940
940
  const shadowRead = _readConfigFile(shadowPath);
941
941
  if (shadowRead.kind === 'ok') {
@@ -997,7 +997,7 @@ function loadConfigResolvedInternal(cwd, options = {}) {
997
997
  }
998
998
  // Branch D or E: no .planning/
999
999
  try {
1000
- const home = process.env['GSD_HOME'] || node_os_1.default.homedir();
1000
+ const home = (0, runtime_homes_cjs_1.resolveGsdHome)();
1001
1001
  const globalDefaultsPath = node_path_1.default.join(home, '.gsd', 'defaults.json');
1002
1002
  const globalRead = _readConfigFile(globalDefaultsPath);
1003
1003
  if (globalRead.kind === 'fault') {
@@ -21,6 +21,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
21
21
  };
22
22
  Object.defineProperty(exports, "__esModule", { value: true });
23
23
  exports.decisionMentioned = decisionMentioned;
24
+ exports.phaseDirProblem = phaseDirProblem;
24
25
  exports.loadPlanContents = loadPlanContents;
25
26
  exports.loadSummaryContents = loadSummaryContents;
26
27
  exports.loadDecisionExtraction = loadDecisionExtraction;
@@ -70,6 +71,24 @@ function decisionMentioned(haystack, decision) {
70
71
  return phrase ? normalizePhrase(haystack).includes(phrase) : false;
71
72
  }
72
73
  // ─── File reading ─────────────────────────────────────────────────────────────
74
+ // #4939: loadPlanContents answers no files for a path that is not there, and that reads
75
+ // as "no plan cites anything" — so a phase-dir argument that names no directory
76
+ // (the phase NUMBER in the phase-dir slot) must be told apart BEFORE the scan.
77
+ // Answers null for a directory, else what is wrong with the path and what to do
78
+ // about it. Like the plan gate's contextKind (#4794), the message names the real
79
+ // stat outcome: a permission or I/O failure (EACCES, ELOOP, EIO) is reported as
80
+ // unreadable, not as a missing directory. Only ENOENT/ENOTDIR mean the path is not there.
81
+ function phaseDirProblem(dirPath) {
82
+ const passDirectory = 'Pass the phase DIRECTORY (e.g. .planning/phases/01-slug), not the phase number.';
83
+ const st = (0, gate_evidence_cjs_1.statEvidence)(dirPath);
84
+ if (st.kind === 'none')
85
+ return { reason: 'phase directory not found', what: 'does not exist', hint: passDirectory, unreadable: false };
86
+ if (st.kind === 'unreadable')
87
+ return { reason: 'phase directory unreadable', what: `could not be read (${st.reason})`, hint: 'Check that the directory is readable.', unreadable: true };
88
+ if (st.value.isDirectory())
89
+ return null;
90
+ return { reason: 'phase path is not a directory', what: st.value.isFile() ? 'is a file, not a directory' : 'is not a directory', hint: passDirectory, unreadable: false };
91
+ }
73
92
  /**
74
93
  * The contents of the phase files `pick` selects (#5170, ADR-5057 §4). An ABSENT phase directory
75
94
  * is "no files" (`found []`); a directory or file that exists but cannot be read is `unreadable` —
@@ -145,9 +145,24 @@ function evaluateDecisionCoveragePlan(input) {
145
145
  if (decisions.length === 0) {
146
146
  return (0, gate_verdict_cjs_1.gateVerdict)('skip', false, { passed: true, skipped: true, reason: 'no trackable decisions', total: 0, covered: 0, uncovered: [], message: 'No trackable decisions in CONTEXT.md.' });
147
147
  }
148
+ // #4939: a phase-dir argument that was GIVEN but is not a directory is a caller
149
+ // error like #2770's empty context argument and #4794's non-file context path:
150
+ // no plan was scanned, so nothing was measured. Fail closed with total/covered
151
+ // null and `uncovered` omitted. An OMITTED phase dir is left to the scan below —
152
+ // the #4130 `--context <path>` alone tests pin that as a coverage gap.
153
+ const phaseDirIssue = phaseDir ? (0, decision_coverage_support_cjs_1.phaseDirProblem)(phaseDir) : null;
154
+ if (phaseDirIssue) {
155
+ const phaseDirResult = { passed: false, skipped: false, reason: phaseDirIssue.reason, total: null, covered: null, message: `Decision coverage gate: the phase directory "${positionals[0]}" ${phaseDirIssue.what}, so no plans were scanned. ${phaseDirIssue.hint}` };
156
+ return phaseDirIssue.unreadable ? (0, gate_verdict_cjs_1.gateUnreadable)(true, phaseDirResult) : (0, gate_verdict_cjs_1.gateVerdict)('block', true, phaseDirResult);
157
+ }
148
158
  const planContents = (0, decision_coverage_support_cjs_1.loadPlanContents)(phaseDir);
149
159
  if (planContents.kind === 'unreadable')
150
160
  return unreadableDecisionGate(`${planContents.span ?? phaseDir}: ${planContents.reason}`);
161
+ // #4939: a phase directory that holds no PLAN file is the same caller error: zero plans
162
+ // loaded is not zero coverage.
163
+ if (phaseDir && planContents.value.length === 0) {
164
+ return (0, gate_verdict_cjs_1.gateVerdict)('block', true, { passed: false, skipped: false, reason: 'phase directory holds no plans', total: null, covered: null, message: `Decision coverage gate: the phase directory "${positionals[0]}" holds no PLAN files, so no plans were scanned. Pass the phase DIRECTORY that holds the plans (e.g. .planning/phases/01-slug), not the phase number.` });
165
+ }
151
166
  const sections = planContents.value.map(decision_coverage_support_cjs_1.extractPlanDesignatedSections);
152
167
  const uncovered = [];
153
168
  let covered = 0;
@@ -90,6 +90,14 @@ function evaluateDecisionCoverageVerify(input) {
90
90
  if (decisions.length === 0) {
91
91
  return (0, gate_verdict_cjs_1.gateVerdict)('skip', false, { skipped: true, blocking: false, reason: 'no trackable decisions', total: 0, honored: 0, not_honored: [], message: 'No trackable decisions in CONTEXT.md.' });
92
92
  }
93
+ // #4939: mirror the plan gate. A phase-dir argument that names no directory
94
+ // means no plan or summary was scanned, so no decision can be reported
95
+ // not-honored. Warn (verify stays non-blocking) rather than list every decision.
96
+ const phaseDirIssue = phaseDir ? (0, decision_coverage_support_cjs_1.phaseDirProblem)(phaseDir) : null;
97
+ if (phaseDirIssue) {
98
+ const phaseDirResult = { skipped: false, blocking: false, reason: phaseDirIssue.reason, total: null, honored: null, not_honored: [], message: `Decision coverage verify (warning): the phase directory "${args[0]}" ${phaseDirIssue.what}, so no plans or summaries were scanned. ${phaseDirIssue.hint}` };
99
+ return phaseDirIssue.unreadable ? (0, gate_verdict_cjs_1.gateUnreadable)(false, phaseDirResult) : (0, gate_verdict_cjs_1.gateVerdict)('advisory', false, phaseDirResult);
100
+ }
93
101
  // Every file the haystack is built from is typed evidence (#5170): one that exists but cannot be
94
102
  // read is `unreadable`, never an empty slice of the haystack that makes a decision look unhonored.
95
103
  const planContents = (0, decision_coverage_support_cjs_1.loadPlanContents)(phaseDir);
@@ -98,6 +106,11 @@ function evaluateDecisionCoverageVerify(input) {
98
106
  const summaryParts = (0, decision_coverage_support_cjs_1.loadSummaryContents)(phaseDir);
99
107
  if (summaryParts.kind === 'unreadable')
100
108
  return unreadableVerify(`${summaryParts.span ?? phaseDir}: ${summaryParts.reason}`);
109
+ // #4939: mirror the plan gate. A phase directory with no PLAN and no SUMMARY file gave the
110
+ // verifier nothing to read, which is a caller error, not a phase whose decisions were all missed.
111
+ if (phaseDir && planContents.value.length === 0 && summaryParts.value.length === 0) {
112
+ return (0, gate_verdict_cjs_1.gateVerdict)('advisory', false, { skipped: false, blocking: false, reason: 'phase directory holds no plans', total: null, honored: null, not_honored: [], message: `Decision coverage verify (warning): the phase directory "${args[0]}" holds no PLAN or SUMMARY files, so nothing was scanned. Pass the phase DIRECTORY that holds the plans (e.g. .planning/phases/01-slug), not the phase number.` });
113
+ }
101
114
  const modifiedFiles = (0, decision_coverage_support_cjs_1.readModifiedFilesContent)(projectDir, summaryParts.value);
102
115
  if (modifiedFiles.kind === 'unreadable')
103
116
  return unreadableVerify(`${modifiedFiles.span ?? 'files_modified'}: ${modifiedFiles.reason}`);
@@ -26,6 +26,10 @@
26
26
  * This is a leaf pure module: no fs, no child_process, no config — the subprocess
27
27
  * seam is injected so the evaluator is trivially testable without spawning.
28
28
  */
29
+ var __importDefault = (this && this.__importDefault) || function (mod) {
30
+ return (mod && mod.__esModule) ? mod : { "default": mod };
31
+ };
32
+ const node_path_1 = __importDefault(require("node:path"));
29
33
  // ─── Public constants ─────────────────────────────────────────────────────────
30
34
  /** Default command timeout for `command-exit-zero` (30s). Matches execTool default. */
31
35
  const COMMAND_EXIT_ZERO_DEFAULT_TIMEOUT_MS = 30_000;
@@ -116,15 +120,31 @@ function evaluateArtifactFrontmatterEquals(predicate, ctx, deps) {
116
120
  throw new Error('artifact-frontmatter-equals predicate requires an "equals" key');
117
121
  }
118
122
  const targetDir = isNonEmptyString(ctx.phaseDir) ? ctx.phaseDir : ctx.cwd;
119
- const filePath = deps.findPhaseArtifact(targetDir, artifactSuffix);
120
- if (!filePath) {
123
+ // Plain `.sort()` (no locale) so the outcome never depends on directory listing order (#5313).
124
+ // Set: finders return realpaths, so a symlink to the same file is one artifact, not two.
125
+ const found = deps.findPhaseArtifact(targetDir, artifactSuffix);
126
+ // A pre-#5313 dep returned `string | null`; a silent null -> "not found" block or per-character spread would mislead.
127
+ if (!Array.isArray(found)) {
128
+ throw new Error('predicate deps "findPhaseArtifact" must return an array of paths');
129
+ }
130
+ const candidates = [...new Set(found)].sort();
131
+ if (candidates.length === 0) {
121
132
  return {
122
133
  block: true,
123
134
  message: `Artifact matching ${artifactSuffix} not found in ${targetDir}`,
124
135
  details: { kind: 'artifact-frontmatter-equals', artifactNotFound: true },
125
136
  };
126
137
  }
127
- const fm = deps.readFrontmatter(filePath);
138
+ // #5313: never pick one of several matches (listing order is OS-dependent); a throw would fail open via onError.
139
+ if (candidates.length > 1) {
140
+ const names = candidates.map((c) => node_path_1.default.relative(targetDir, c).split(node_path_1.default.sep).join('/')).join(', ');
141
+ return {
142
+ block: true,
143
+ message: `Multiple artifacts match ${artifactSuffix} in ${targetDir}: ${names}; remove or rename all but one`,
144
+ details: { kind: 'artifact-frontmatter-equals', ambiguous: true, candidates },
145
+ };
146
+ }
147
+ const fm = deps.readFrontmatter(candidates[0]);
128
148
  const actualValue = fm[field];
129
149
  // eslint-disable-next-line @typescript-eslint/no-base-to-string
130
150
  const expectedStr = typeof expectedValue === 'object' ? JSON.stringify(expectedValue) : String(expectedValue);
@@ -80,31 +80,27 @@ function buildPredicateDeps() {
80
80
  if (phaseDirStat.kind === 'unreadable')
81
81
  return unreadable(phaseDir, phaseDirStat.reason);
82
82
  if (phaseDirStat.kind === 'none')
83
- return null;
83
+ return [];
84
84
  if (artifactSuffix === '.' ||
85
85
  artifactSuffix === '..' ||
86
86
  artifactSuffix.includes('\0') ||
87
87
  node_path_1.default.basename(artifactSuffix) !== artifactSuffix ||
88
88
  node_path_1.default.win32.basename(artifactSuffix) !== artifactSuffix) {
89
- return null;
90
- }
91
- const directContained = (0, security_cjs_1.tryWithinRoot)(artifactSuffix, phaseDir);
92
- if (directContained !== null) {
93
- const direct = regularFileOrNull(directContained);
94
- if (direct !== null)
95
- return direct;
89
+ return [];
96
90
  }
91
+ // #5313: collect EVERY distinct match; the evaluator fails closed on >1 instead of trusting listing order.
92
+ const found = [];
97
93
  const planningContained = (0, security_cjs_1.tryWithinRoot)(node_path_1.default.join('.planning', artifactSuffix), phaseDir);
98
94
  if (planningContained !== null) {
99
95
  const planning = regularFileOrNull(planningContained);
100
96
  if (planning !== null)
101
- return planning;
97
+ found.push(planning);
102
98
  }
103
99
  const listing = (0, gate_evidence_cjs_1.readDirEvidence)(phaseDir);
104
100
  if (listing.kind === 'unreadable')
105
101
  return unreadable(phaseDir, listing.reason);
106
102
  if (listing.kind === 'none')
107
- return null;
103
+ return [];
108
104
  for (const f of listing.value) {
109
105
  if (f.endsWith('-' + artifactSuffix) || f === artifactSuffix) {
110
106
  const candidateContained = (0, security_cjs_1.tryWithinRoot)(f, phaseDir);
@@ -112,10 +108,10 @@ function buildPredicateDeps() {
112
108
  continue;
113
109
  const candidate = regularFileOrNull(candidateContained);
114
110
  if (candidate !== null)
115
- return candidate;
111
+ found.push(candidate);
116
112
  }
117
113
  }
118
- return null;
114
+ return found;
119
115
  },
120
116
  readFrontmatter(filePath) {
121
117
  const content = (0, shell_command_projection_cjs_1.platformReadSync)(filePath);
@@ -17,6 +17,12 @@
17
17
  * tolerant. A plan id that is not `<phase>-<plan>` (a plan named `.*-PLAN.md`) matches nothing,
18
18
  * and a git failure is "no commits" exactly as before.
19
19
  *
20
+ * Cross-plan GREEN (#4867): a gap closure is planned as a RED-only probe plan plus the plan that
21
+ * `depends_on` it and carries the fix. A `type: tdd` plan with its own RED but no GREEN is credited
22
+ * with a `feat(...)` commit under any plan that lists it in `depends_on`, directly or through a
23
+ * chain; the row carries `greenFrom` and the report names the supplier. Its own RED is still
24
+ * required, and a plan nothing depends on is scored on its own commits alone.
25
+ *
20
26
  * Argv after the verb: `<phase>` (a number; an unresolvable phase reports zero plans).
21
27
  */
22
28
  var __importDefault = (this && this.__importDefault) || function (mod) {
@@ -31,7 +37,7 @@ const gate_evidence_cjs_1 = require("./gate-evidence.cjs");
31
37
  const gate_evaluation_scope_cjs_1 = require("./gate-evaluation-scope.cjs");
32
38
  // eslint-disable-next-line @typescript-eslint/no-require-imports
33
39
  const frontmatterMod = require("./frontmatter.cjs");
34
- const { frontmatterKeyHasValue } = frontmatterMod;
40
+ const { frontmatterKeyHasValue, extractFrontmatter } = frontmatterMod;
35
41
  /**
36
42
  * True when the plan's frontmatter declares `type: tdd` (CRLF included, #2449). The block is the
37
43
  * one fence owner's, read as RAW text — not through the YAML parser — so a block the parser refuses
@@ -42,6 +48,35 @@ const { frontmatterKeyHasValue } = frontmatterMod;
42
48
  function isTddPlan(content) {
43
49
  return frontmatterKeyHasValue(content, 'type', 'tdd');
44
50
  }
51
+ /** A `depends_on` token as a plan id: `01.1-20`, `01.1-20-PLAN` and `01.1-20-PLAN.md` all name plan `01.1-20`. */
52
+ function toPlanId(token) {
53
+ return token.trim().replace(/-PLAN(?:\.md)?$/i, '');
54
+ }
55
+ /**
56
+ * The plan ids a plan lists in `depends_on` (#4867). A plan whose frontmatter does not parse, or has
57
+ * no `depends_on`, depends on nothing: the pairing below only ever ADDS credit, so an unreadable
58
+ * link can never turn a failing plan into a passing one.
59
+ */
60
+ function dependsOnOf(content) {
61
+ // `extractFrontmatter` does not throw: an unparseable block is an empty result, so `depends_on` is absent.
62
+ const value = extractFrontmatter(content)['depends_on'];
63
+ const tokens = Array.isArray(value) ? value.map(String) : typeof value === 'string' ? value.split(',') : [];
64
+ return tokens.map(toPlanId).filter((id) => id !== '');
65
+ }
66
+ /** Every plan that depends on `planId`, directly or through a chain (#4867); a cycle is walked once. */
67
+ function transitiveDependents(planId, dependents) {
68
+ const seen = new Set();
69
+ const queue = [planId];
70
+ while (queue.length > 0) {
71
+ for (const next of dependents.get(queue.shift()) ?? []) {
72
+ if (next !== planId && !seen.has(next)) {
73
+ seen.add(next);
74
+ queue.push(next);
75
+ }
76
+ }
77
+ }
78
+ return [...seen].sort();
79
+ }
45
80
  /**
46
81
  * The commit types (`test`, `feat`, `refactor`, …) among the plan's own commits — those that
47
82
  * touched at least one path (`pathspecs: ['.']`, as the old `git log -- .` lookup did). An
@@ -91,6 +126,9 @@ function evaluateTddReviewCheckpoint(input) {
91
126
  const phaseDir = located.kind === 'found' ? located.value : '';
92
127
  // Find all PLAN.md files with type: tdd in frontmatter
93
128
  const tddPlanFiles = [];
129
+ // #4867: plan id -> the plans that list it in `depends_on`, over EVERY plan of the phase (a
130
+ // dependent GREEN plan is usually `type: execute`, not `tdd`).
131
+ const dependents = new Map();
94
132
  const unreadable = [];
95
133
  if (phaseDir) {
96
134
  const entries = (0, gate_evidence_cjs_1.readDirEvidence)(phaseDir);
@@ -110,8 +148,13 @@ function evaluateTddReviewCheckpoint(input) {
110
148
  const plan = (0, gate_evidence_cjs_1.readTextEvidence)(planPath);
111
149
  if (plan.kind === 'unreadable')
112
150
  unreadable.push({ source: planPath, reason: plan.reason });
113
- else if (plan.kind === 'found' && isTddPlan(plan.value))
114
- tddPlanFiles.push(planPath);
151
+ else if (plan.kind === 'found') {
152
+ if (isTddPlan(plan.value))
153
+ tddPlanFiles.push(planPath);
154
+ const planId = node_path_1.default.basename(planPath, '-PLAN.md');
155
+ for (const dep of dependsOnOf(plan.value))
156
+ dependents.set(dep, [...(dependents.get(dep) ?? []), planId]);
157
+ }
115
158
  }
116
159
  }
117
160
  }
@@ -136,15 +179,24 @@ function evaluateTddReviewCheckpoint(input) {
136
179
  const planId = node_path_1.default.basename(planPath, '-PLAN.md');
137
180
  const kinds = planCommitKinds(projectDir, planId);
138
181
  const red = kinds.has('test');
139
- const green = kinds.has('feat');
140
182
  const refactor = kinds.has('refactor');
183
+ // #4867: a RED-only probe plan is satisfied by the GREEN of a plan that (transitively) declares
184
+ // `depends_on` it. Only a real `feat(...)` commit under such a plan counts; the plan's own RED is
185
+ // still required, and a plan nothing depends on is scored on its own commits alone.
186
+ const ownGreen = kinds.has('feat');
187
+ const greenFrom = ownGreen
188
+ ? []
189
+ : transitiveDependents(planId, dependents).filter((dependentId) => planCommitKinds(projectDir, dependentId).has('feat'));
190
+ const green = ownGreen || greenFrom.length > 0;
141
191
  const missing = [];
142
192
  if (!red)
143
193
  missing.push('RED');
144
194
  if (!green)
145
195
  missing.push('GREEN');
146
196
  const status = missing.length === 0 ? 'Pass' : 'FAIL';
147
- rows.push({ planId, red, green, refactor, status, missing });
197
+ rows.push(greenFrom.length > 0
198
+ ? { planId, red, green, refactor, status, missing, greenFrom }
199
+ : { planId, red, green, refactor, status, missing });
148
200
  }
149
201
  const violations = rows.filter(r => r.status === 'FAIL').length;
150
202
  // Build review table
@@ -160,6 +212,9 @@ function evaluateTddReviewCheckpoint(input) {
160
212
  tableDivider,
161
213
  ...tableRows,
162
214
  ].join('\n');
215
+ for (const r of rows.filter(row => row.greenFrom)) {
216
+ table += `\n\nPlan ${r.planId} GREEN is supplied by dependent plan ${(r.greenFrom ?? []).join(', ')}.`;
217
+ }
163
218
  if (violations > 0) {
164
219
  table += '\n\n⚠ Gate violations are advisory — review before advancing.';
165
220
  for (const r of rows.filter(row => row.status === 'FAIL')) {
@@ -20,6 +20,8 @@ const markdown_sectionizer_cjs_1 = require("./markdown-sectionizer.cjs");
20
20
  const io = require("./io.cjs");
21
21
  // eslint-disable-next-line @typescript-eslint/no-require-imports -- config-loader.cjs is an export= CommonJS module
22
22
  const configLoader = require("./config-loader.cjs");
23
+ // eslint-disable-next-line @typescript-eslint/no-require-imports -- capability-ledger.cjs is an export= CommonJS module
24
+ const capabilityLedger = require("./capability-ledger.cjs");
23
25
  const project_root_cjs_1 = require("./project-root.cjs");
24
26
  // eslint-disable-next-line @typescript-eslint/no-require-imports -- model-resolver.cjs is an export= CommonJS module
25
27
  const modelResolver = require("./model-resolver.cjs");
@@ -3773,7 +3775,7 @@ function buildAgentSkillsBlock(config, agentType, projectRoot, diagnostics) {
3773
3775
  }).join('\n');
3774
3776
  return `<agent_skills>\nRead these user-configured skills:\n${lines}\n</agent_skills>`;
3775
3777
  }
3776
- function cmdAgentSkills(cwd, agentType, raw, jsonMode) {
3778
+ function cmdAgentSkills(cwd, agentType, raw, jsonMode, skillsOnly = false) {
3777
3779
  if (!agentType) {
3778
3780
  output('', raw, '');
3779
3781
  return;
@@ -3804,8 +3806,13 @@ function cmdAgentSkills(cwd, agentType, raw, jsonMode) {
3804
3806
  // (a real exit code) instead of a prose config-get gate. Compact is tried
3805
3807
  // first when requested; a missing compact sibling falls back to canonical
3806
3808
  // with the fallback disclosed in the payload itself, never a silent switch.
3809
+ //
3810
+ // #5122: `skillsOnly` (the CLI's `--skills-only`) is the agent's own
3811
+ // self-load. It wants configured skills and nothing else, so it skips the
3812
+ // fallback on every runtime — otherwise a non-Claude agent reads its own
3813
+ // persona back into itself as if it were a skills block.
3807
3814
  let agentPayloadVariant = null;
3808
- if (!block) {
3815
+ if (!block && !skillsOnly) {
3809
3816
  const runtime = (config && config['runtime']) || process.env['GSD_RUNTIME'] || 'claude';
3810
3817
  if (runtime !== 'claude') {
3811
3818
  const agentCheck = checkAgentsInstalled(runtime, projectRoot);
@@ -4071,7 +4078,7 @@ function buildSkillManifest(cwd, skillsDir = null) {
4071
4078
  if (!entry.isDirectory())
4072
4079
  continue;
4073
4080
  const skillMdPath = node_path_1.default.join(rootPath, entry.name, 'SKILL.md');
4074
- const content = (0, shell_command_projection_cjs_1.platformReadSync)(skillMdPath);
4081
+ const content = capabilityLedger.readSkillFile(skillMdPath);
4075
4082
  if (content !== null) {
4076
4083
  if (pushSkillEntry(`${entry.name}/SKILL.md`, content, skillMdPath))
4077
4084
  skillCount++;
@@ -4096,7 +4103,7 @@ function buildSkillManifest(cwd, skillsDir = null) {
4096
4103
  if (!nested.isDirectory())
4097
4104
  continue;
4098
4105
  const nestedSkillMd = node_path_1.default.join(nestedSkillsDir, nested.name, 'SKILL.md');
4099
- const nestedContent = (0, shell_command_projection_cjs_1.platformReadSync)(nestedSkillMd);
4106
+ const nestedContent = capabilityLedger.readSkillFile(nestedSkillMd);
4100
4107
  if (nestedContent === null)
4101
4108
  continue;
4102
4109
  // Use forward-slash separator explicitly so manifest paths are posix-style
@@ -35,7 +35,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
35
35
  */
36
36
  const node_fs_1 = __importDefault(require("node:fs"));
37
37
  const node_path_1 = __importDefault(require("node:path"));
38
- const node_os_1 = __importDefault(require("node:os"));
38
+ const runtime_homes_cjs_1 = require("./runtime-homes.cjs");
39
39
  // eslint-disable-next-line @typescript-eslint/no-require-imports -- install-fs-adapter.cjs is an export= CommonJS module
40
40
  const installFsAdapter = require("./install-fs-adapter.cjs");
41
41
  const { installFs } = installFsAdapter;
@@ -154,7 +154,7 @@ function _findAncestorGsdConfigPath(targetDir) {
154
154
  * @param targetDir Runtime install root (walks up to find .planning/).
155
155
  */
156
156
  function readGsdEffectiveEffortConfig(targetDir = null) {
157
- const homeDefaults = _readGsdConfigFile(node_path_1.default.join(node_os_1.default.homedir(), '.gsd', 'defaults.json'), '~/.gsd/defaults.json');
157
+ const homeDefaults = _readGsdConfigFile(node_path_1.default.join((0, runtime_homes_cjs_1.resolveGsdHome)(), '.gsd', 'defaults.json'), '~/.gsd/defaults.json');
158
158
  let projectConfig = null;
159
159
  if (targetDir) {
160
160
  const candidate = _findAncestorGsdConfigPath(targetDir);
@@ -34,6 +34,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
34
34
  */
35
35
  const node_path_1 = __importDefault(require("node:path"));
36
36
  const node_os_1 = __importDefault(require("node:os"));
37
+ const runtime_homes_cjs_1 = require("./runtime-homes.cjs");
37
38
  // eslint-disable-next-line @typescript-eslint/no-require-imports -- install-fs-adapter.cjs is an export= CommonJS module
38
39
  const installFsAdapter = require("./install-fs-adapter.cjs");
39
40
  const { installFs } = installFsAdapter;
@@ -57,7 +58,7 @@ const { resolveTierEntry: gsdResolveTierEntry, resolveModelPolicy: gsdResolveMod
57
58
  */
58
59
  function readGsdGlobalModelOverrides(options = {}) {
59
60
  try {
60
- const home = options.homedir ? options.homedir() : node_os_1.default.homedir();
61
+ const home = (0, runtime_homes_cjs_1.resolveGsdHome)(options.homedir ? options.homedir() : node_os_1.default.homedir());
61
62
  const defaultsPath = node_path_1.default.join(home, '.gsd', 'defaults.json');
62
63
  if (!installFs().existsSync(defaultsPath))
63
64
  return null;
@@ -139,7 +140,7 @@ function readGsdAgentTools(config) {
139
140
  * model override resolver.
140
141
  */
141
142
  function readGsdEffectiveAgentTools(targetDir = null, options = {}) {
142
- const home = options.homedir ? options.homedir() : node_os_1.default.homedir();
143
+ const home = (0, runtime_homes_cjs_1.resolveGsdHome)(options.homedir ? options.homedir() : node_os_1.default.homedir());
143
144
  const globalConfig = _readGsdConfigFile(node_path_1.default.join(home, '.gsd', 'defaults.json'), 'global defaults');
144
145
  let projectConfig = null;
145
146
  if (targetDir) {
@@ -170,7 +171,7 @@ function readGsdEffectiveAgentTools(targetDir = null, options = {}) {
170
171
  * baked, letting the runtime's own default/session model govern).
171
172
  */
172
173
  function readGsdRuntimeProfileResolver(targetDir = null) {
173
- const homeDefaults = _readGsdConfigFile(node_path_1.default.join(node_os_1.default.homedir(), '.gsd', 'defaults.json'), '~/.gsd/defaults.json');
174
+ const homeDefaults = _readGsdConfigFile(node_path_1.default.join((0, runtime_homes_cjs_1.resolveGsdHome)(), '.gsd', 'defaults.json'), '~/.gsd/defaults.json');
174
175
  // #2875 defect fix (Generative Fix Divergence): same shared walk as
175
176
  // readGsdEffectiveModelOverrides above — see that call site's comment.
176
177
  let projectConfig = null;
@@ -31,6 +31,8 @@ const frontmatter_fence_cjs_1 = require("./frontmatter-fence.cjs");
31
31
  // #2322: reuse the existing pure path-containment seam (ADR-1239 Phase C-2)
32
32
  // instead of hand-rolling a new traversal check for capability skill stems.
33
33
  const external_descriptor_trust_cjs_1 = require("./external-descriptor-trust.cjs");
34
+ // eslint-disable-next-line @typescript-eslint/no-require-imports -- capability-ledger.cjs is an export= CommonJS module
35
+ const capabilityLedger = require("./capability-ledger.cjs");
34
36
  // eslint-disable-next-line @typescript-eslint/no-require-imports
35
37
  const conversionModule = require("./runtime-artifact-conversion.cjs");
36
38
  const { applyAgentPathRewrites: _applyAgentPathRewrites, processAttribution: _processAttribution, normalizeAgentBodyForRuntime: _normalizeAgentBodyForRuntime, readGsdCommandNames: _readGsdCommandNames, deriveAgentName: _deriveAgentName, applyAgentFrontmatterExtensions: _applyAgentFrontmatterExtensions, appendAgentTools: _appendAgentTools, } = conversionModule;
@@ -748,9 +750,9 @@ function readInstalledCapabilitySkill(stem, registry) {
748
750
  // installed as a capability skill (epic #4636).
749
751
  if (node_fs_1.default.lstatSync(skillPath).isSymbolicLink())
750
752
  return null;
751
- if (!node_fs_1.default.statSync(skillPath).isFile())
752
- return null;
753
- return { capId, content: node_fs_1.default.readFileSync(skillPath, 'utf8') };
753
+ // #5121: the bounded regular-file read — a FIFO/device/oversized SKILL.md is skipped like a missing one.
754
+ const content = capabilityLedger.readSkillFile(skillPath);
755
+ return content === null ? null : { capId, content };
754
756
  }
755
757
  catch {
756
758
  return null; // missing / unreadable / corrupt entry -> skip