@opengsd/gsd-core 1.8.0 → 1.9.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 (174) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/.opencode/plugins/gsd-core.js +31 -1
  4. package/agents/gsd-code-fixer.md +1 -1
  5. package/agents/gsd-codebase-mapper.md +1 -1
  6. package/agents/gsd-debug-session-manager.md +36 -0
  7. package/agents/gsd-executor.md +20 -7
  8. package/agents/gsd-intel-updater.md +3 -3
  9. package/agents/gsd-phase-researcher.md +4 -2
  10. package/agents/gsd-plan-checker.md +20 -0
  11. package/agents/gsd-planner.md +15 -23
  12. package/agents/gsd-project-researcher.md +2 -2
  13. package/agents/gsd-ui-auditor.md +0 -40
  14. package/bin/install.js +186 -55
  15. package/commands/gsd/plan-review-convergence.md +5 -1
  16. package/gsd-core/bin/gsd-tools.cjs +849 -2
  17. package/gsd-core/bin/lib/api-coverage.cjs +22 -8
  18. package/gsd-core/bin/lib/audit.cjs +8 -8
  19. package/gsd-core/bin/lib/capability-consent.cjs +40 -1
  20. package/gsd-core/bin/lib/capability-lifecycle.cjs +58 -0
  21. package/gsd-core/bin/lib/capability-loader.cjs +23 -1
  22. package/gsd-core/bin/lib/capability-registry.cjs +1353 -132
  23. package/gsd-core/bin/lib/capability-trust.cjs +468 -33
  24. package/gsd-core/bin/lib/capability-validator.cjs +882 -6
  25. package/gsd-core/bin/lib/check-command-router.cjs +12 -2
  26. package/gsd-core/bin/lib/cjs-command-router-adapter.cjs +15 -0
  27. package/gsd-core/bin/lib/claude-orchestration-command-router.cjs +102 -12
  28. package/gsd-core/bin/lib/claude-orchestration.cjs +125 -22
  29. package/gsd-core/bin/lib/commands.cjs +246 -18
  30. package/gsd-core/bin/lib/config-loader.cjs +200 -28
  31. package/gsd-core/bin/lib/config.cjs +90 -5
  32. package/gsd-core/bin/lib/estimate-cli.cjs +336 -0
  33. package/gsd-core/bin/lib/frontmatter.cjs +125 -15
  34. package/gsd-core/bin/lib/host-integration.cjs +215 -8
  35. package/gsd-core/bin/lib/init.cjs +44 -19
  36. package/gsd-core/bin/lib/install-engine.cjs +1 -0
  37. package/gsd-core/bin/lib/milestone.cjs +5 -5
  38. package/gsd-core/bin/lib/model-catalog.cjs +51 -1
  39. package/gsd-core/bin/lib/observability/logger.cjs +7 -2
  40. package/gsd-core/bin/lib/phase-command-router.cjs +10 -1
  41. package/gsd-core/bin/lib/phase-estimation.cjs +398 -0
  42. package/gsd-core/bin/lib/phase-id.cjs +278 -5
  43. package/gsd-core/bin/lib/phase.cjs +57 -5
  44. package/gsd-core/bin/lib/plan-drift-guard.cjs +1 -1
  45. package/gsd-core/bin/lib/plan-scan.cjs +1 -1
  46. package/gsd-core/bin/lib/planning-workspace.cjs +9 -2
  47. package/gsd-core/bin/lib/profile-output.cjs +34 -8
  48. package/gsd-core/bin/lib/review-lane-descriptor.cjs +927 -0
  49. package/gsd-core/bin/lib/review-lane-invocation.cjs +348 -0
  50. package/gsd-core/bin/lib/review-lane-runner.cjs +594 -0
  51. package/gsd-core/bin/lib/review-reviewer-selection.cjs +114 -32
  52. package/gsd-core/bin/lib/roadmap-parser.cjs +54 -6
  53. package/gsd-core/bin/lib/roadmap.cjs +10 -4
  54. package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +31 -4
  55. package/gsd-core/bin/lib/runtime-artifact-layout.cjs +1 -1
  56. package/gsd-core/bin/lib/runtime-hooks-surface.cjs +140 -0
  57. package/gsd-core/bin/lib/runtime-name-policy.cjs +15 -2
  58. package/gsd-core/bin/lib/smart-entry.cjs +1 -1
  59. package/gsd-core/bin/lib/state-document.cjs +164 -20
  60. package/gsd-core/bin/lib/state-transition.cjs +28 -10
  61. package/gsd-core/bin/lib/state.cjs +141 -21
  62. package/gsd-core/bin/lib/uat-predicate.cjs +6 -4
  63. package/gsd-core/bin/lib/uat.cjs +9 -7
  64. package/gsd-core/bin/lib/ui-consideration-probe.cjs +2 -2
  65. package/gsd-core/bin/lib/unusable-input.cjs +216 -0
  66. package/gsd-core/bin/lib/validate.cjs +32 -0
  67. package/gsd-core/bin/lib/verification.cjs +51 -14
  68. package/gsd-core/bin/lib/verify.cjs +128 -20
  69. package/gsd-core/bin/lib/worktree-safety.cjs +360 -15
  70. package/gsd-core/bin/shared/config-defaults.manifest.json +1 -0
  71. package/gsd-core/bin/shared/config-schema.manifest.json +1 -13
  72. package/gsd-core/bin/shared/model-catalog.json +5 -0
  73. package/gsd-core/bin/shared/runtime-aliases.manifest.json +5 -0
  74. package/gsd-core/references/context-budget.md +40 -0
  75. package/gsd-core/references/gate-prompts.md +6 -3
  76. package/gsd-core/references/model-profile-resolution.md +64 -13
  77. package/gsd-core/references/offer-next.md +88 -0
  78. package/gsd-core/references/planning-config.md +2 -1
  79. package/gsd-core/references/reviewer-instances.md +28 -21
  80. package/gsd-core/references/runtime-aware-dispatch.md +42 -0
  81. package/gsd-core/references/ui-consideration-probe.md +2 -2
  82. package/gsd-core/references/worktree-branch-check.md +4 -4
  83. package/gsd-core/templates/summary-minimal.md +4 -0
  84. package/gsd-core/templates/summary-standard.md +4 -0
  85. package/gsd-core/templates/summary.md +7 -0
  86. package/gsd-core/workflows/ai-integration-phase.md +4 -4
  87. package/gsd-core/workflows/audit-fix.md +4 -0
  88. package/gsd-core/workflows/audit-milestone.md +8 -0
  89. package/gsd-core/workflows/autonomous.md +19 -15
  90. package/gsd-core/workflows/check-todos.md +2 -2
  91. package/gsd-core/workflows/code-review-fix.md +14 -6
  92. package/gsd-core/workflows/code-review.md +76 -19
  93. package/gsd-core/workflows/debug.md +10 -2
  94. package/gsd-core/workflows/diagnose-issues.md +4 -0
  95. package/gsd-core/workflows/discuss-phase/modes/advisor.md +2 -4
  96. package/gsd-core/workflows/discuss-phase/modes/auto.md +0 -6
  97. package/gsd-core/workflows/discuss-phase-assumptions.md +15 -9
  98. package/gsd-core/workflows/discuss-phase.md +2 -2
  99. package/gsd-core/workflows/docs-update.md +8 -0
  100. package/gsd-core/workflows/eval-review.md +1 -1
  101. package/gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md +4 -0
  102. package/gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md +160 -0
  103. package/gsd-core/workflows/execute-phase.md +85 -115
  104. package/gsd-core/workflows/execute-plan.md +5 -4
  105. package/gsd-core/workflows/explore.md +4 -0
  106. package/gsd-core/workflows/extract-learnings.md +21 -0
  107. package/gsd-core/workflows/help/modes/full.md +3 -3
  108. package/gsd-core/workflows/import.md +4 -1
  109. package/gsd-core/workflows/ingest-docs.md +4 -0
  110. package/gsd-core/workflows/map-codebase.md +13 -6
  111. package/gsd-core/workflows/new-milestone.md +10 -2
  112. package/gsd-core/workflows/new-project.md +11 -4
  113. package/gsd-core/workflows/next.md +5 -2
  114. package/gsd-core/workflows/plan-phase.md +42 -46
  115. package/gsd-core/workflows/plan-review-convergence.md +18 -14
  116. package/gsd-core/workflows/progress.md +1 -1
  117. package/gsd-core/workflows/quick.md +14 -3
  118. package/gsd-core/workflows/review.md +146 -575
  119. package/gsd-core/workflows/scan.md +9 -1
  120. package/gsd-core/workflows/secure-phase.md +10 -2
  121. package/gsd-core/workflows/ship.md +41 -11
  122. package/gsd-core/workflows/smart-entry.md +1 -1
  123. package/gsd-core/workflows/ui-phase.md +8 -1
  124. package/gsd-core/workflows/ui-review.md +8 -1
  125. package/gsd-core/workflows/update.md +104 -5
  126. package/gsd-core/workflows/validate-phase.md +10 -2
  127. package/gsd-core/workflows/verify-work.md +8 -1
  128. package/hooks/dist/gsd-cursor-session-start.js +6 -2
  129. package/hooks/dist/gsd-cursor-stop.js +6 -2
  130. package/hooks/dist/gsd-cursor-subagent-start.js +6 -2
  131. package/hooks/dist/gsd-graphify-update.sh +9 -0
  132. package/hooks/dist/gsd-phase-boundary.sh +14 -2
  133. package/hooks/dist/gsd-prompt-guard.js +101 -2
  134. package/hooks/dist/gsd-read-guard.js +100 -2
  135. package/hooks/dist/gsd-read-injection-scanner.js +109 -2
  136. package/hooks/dist/gsd-statusline.js +9 -6
  137. package/hooks/dist/gsd-workflow-guard.js +110 -6
  138. package/hooks/dist/gsd-worktree-path-guard.js +132 -8
  139. package/hooks/dist/lib/cursor-workspace.js +74 -0
  140. package/hooks/gsd-cursor-session-start.js +6 -2
  141. package/hooks/gsd-cursor-stop.js +6 -2
  142. package/hooks/gsd-cursor-subagent-start.js +6 -2
  143. package/hooks/gsd-graphify-update.sh +9 -0
  144. package/hooks/gsd-phase-boundary.sh +14 -2
  145. package/hooks/gsd-prompt-guard.js +101 -2
  146. package/hooks/gsd-read-guard.js +100 -2
  147. package/hooks/gsd-read-injection-scanner.js +109 -2
  148. package/hooks/gsd-statusline.js +9 -6
  149. package/hooks/gsd-workflow-guard.js +110 -6
  150. package/hooks/gsd-worktree-path-guard.js +132 -8
  151. package/hooks/lib/cursor-workspace.js +74 -0
  152. package/package.json +7 -7
  153. package/pi/gsd.cjs +26 -1
  154. package/scripts/check-coverage-gate.cjs +51 -0
  155. package/scripts/check-glossary-refs.cjs +24 -0
  156. package/scripts/ci-test-scope.cjs +67 -17
  157. package/scripts/gen-adr-index.cjs +6 -4
  158. package/scripts/gen-capability-matrix.cjs +26 -2
  159. package/scripts/gen-capability-registry.cjs +132 -34
  160. package/scripts/gen-emitted-baseline.cjs +145 -0
  161. package/scripts/lint-compiled-artifact-sync.cjs +146 -0
  162. package/scripts/lint-emitted-drift-ack.cjs +149 -0
  163. package/scripts/lint-fix-has-regression-test.cjs +131 -0
  164. package/scripts/lint-resolution-provenance.cjs +9 -0
  165. package/scripts/mutation-matrix.cjs +4 -0
  166. package/scripts/prompt-injection-scan.sh +6 -0
  167. package/scripts/registry-schema.cjs +57 -8
  168. package/scripts/release-notes/conventional-title.cjs +19 -1
  169. package/scripts/release-notes/format-github-release-notes.cjs +7 -3
  170. package/scripts/workflow-size.cjs +16 -8
  171. package/skills/gsd-plan-review-convergence/SKILL.md +5 -1
  172. package/vscode/package.json +1 -1
  173. package/scripts/gen-golden-install-parity-zcode.cjs +0 -77
  174. package/scripts/update-size-baseline.cjs +0 -68
package/pi/gsd.cjs CHANGED
@@ -197,10 +197,35 @@ function buildBeforeProviderRequestHandler({ tier = 'sonnet' } = {}) {
197
197
  return async function onBeforeProviderRequest(event, ctx) {
198
198
  try {
199
199
  const effectiveCwd = (ctx && ctx.cwd) || process.cwd();
200
- const { resolveTierEntry } = require(path.join(GSD_CORE, 'bin', 'lib', 'model-resolver.cjs'));
201
200
  const { loadConfig } = require(path.join(GSD_CORE, 'bin', 'lib', 'config-loader.cjs'));
202
201
  const config = loadConfig(effectiveCwd);
203
202
  const overrides = (config && config.model_profile_overrides) || undefined;
203
+
204
+ // #2460: FAIL-OPEN when the user has not explicitly configured
205
+ // model_profile_overrides[runtime][tier]. pi is provider-agnostic
206
+ // (kimi-coding, zai, openrouter, openai-codex, minimax, anthropic, …);
207
+ // the built-in RUNTIME_PROFILE_MAP.pi.sonnet default is `claude-sonnet-5`,
208
+ // an Anthropic-ecosystem assumption that is wrong for every non-Anthropic
209
+ // provider. Returning the built-in default unconditionally rewrote every
210
+ // outgoing request to a model the active provider did not know.
211
+ //
212
+ // resolveTierEntry falls back to the built-in catalog when no override is
213
+ // present, so we cannot call it to detect "did the user opt in?" — we
214
+ // inspect the override map directly. Only when the user has explicitly
215
+ // set model_profile_overrides[runtime][tier] do we steer; otherwise we
216
+ // return undefined and pi's chosen model flows through untouched.
217
+ const userRaw = overrides && overrides.pi ? overrides.pi[tier] : undefined;
218
+ // `''` is treated the same as null/undefined: an explicit empty-string
219
+ // override is the user clearing a previously-set value, NOT opting in to
220
+ // steering. Without this guard, `resolveTierEntry`'s falsy `if (userRaw)`
221
+ // check would silently fall back to the built-in catalog and rewrite
222
+ // payload.model to claude-sonnet-5 — re-introducing the exact bug this
223
+ // handler exists to prevent.
224
+ if (userRaw === undefined || userRaw === null || userRaw === '') {
225
+ return undefined; // fail-open — leave pi's model untouched
226
+ }
227
+
228
+ const { resolveTierEntry } = require(path.join(GSD_CORE, 'bin', 'lib', 'model-resolver.cjs'));
204
229
  const entry = resolveTierEntry({ runtime: 'pi', tier, overrides });
205
230
  const modelId = entry && typeof entry.model === 'string' && entry.model.length > 0 ? entry.model : null;
206
231
  if (!modelId) return undefined; // fail-open — leave pi's model untouched
@@ -0,0 +1,51 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ const fs = require('fs');
5
+ const path = require('path');
6
+
7
+ const summaryPath = path.join(process.cwd(), 'coverage', 'coverage-summary.json');
8
+ if (!fs.existsSync(summaryPath)) {
9
+ console.error('check-coverage-gate: coverage/coverage-summary.json not found.');
10
+ console.error('Ensure c8 runs with --reporter json-summary before this script.');
11
+ process.exitCode = 1;
12
+ return;
13
+ }
14
+
15
+ const summary = JSON.parse(fs.readFileSync(summaryPath, 'utf8'));
16
+ const errors = [];
17
+
18
+ const OVERALL_LINES = 70;
19
+ const OVERALL_BRANCHES = 60;
20
+ const PER_FILE_BRANCHES = 70;
21
+ const PER_FILE_FILES = [
22
+ 'gsd-core/bin/lib/state.cjs',
23
+ 'gsd-core/bin/lib/phase.cjs',
24
+ 'gsd-core/bin/lib/verify.cjs',
25
+ 'gsd-core/bin/lib/init.cjs',
26
+ ];
27
+
28
+ const overall = summary.total;
29
+ if (overall.lines.pct < OVERALL_LINES) {
30
+ errors.push(`Overall lines ${overall.lines.pct}% < ${OVERALL_LINES}%`);
31
+ }
32
+ if (overall.branches.pct < OVERALL_BRANCHES) {
33
+ errors.push(`Overall branches ${overall.branches.pct}% < ${OVERALL_BRANCHES}%`);
34
+ }
35
+
36
+ for (const file of PER_FILE_FILES) {
37
+ const key = Object.keys(summary).find((k) => k.endsWith(file));
38
+ if (!key) {
39
+ errors.push(`${file}: not found in coverage summary`);
40
+ continue;
41
+ }
42
+ const pct = summary[key].branches.pct;
43
+ if (pct < PER_FILE_BRANCHES) {
44
+ errors.push(`${file}: branches ${pct}% < ${PER_FILE_BRANCHES}%`);
45
+ }
46
+ }
47
+
48
+ if (errors.length > 0) {
49
+ for (const e of errors) console.error(`ERROR: ${e}`);
50
+ process.exitCode = 1;
51
+ }
@@ -64,6 +64,29 @@ const TRACKED_PREFIXES = [
64
64
  /** The only bare (no-prefix-match) tokens this gate checks by exact name. */
65
65
  const TRACKED_EXACT = new Set(['bin/install.js', 'package.json']);
66
66
 
67
+ /**
68
+ * Paths CONTEXT.md documents whose ABSENCE is the healthy steady state (#2778).
69
+ *
70
+ * `TRACKED_PREFIXES` skips claims this gate *cannot* check. This is the narrower
71
+ * third case: a claim it must not check, because "does not exist" is the correct
72
+ * state rather than drift.
73
+ *
74
+ * `tests/emitted-drift-ack.json` is the acknowledgment file from ADR-2719 §3. Its
75
+ * whole design property is that it appears in the changed-files list ONLY when
76
+ * something rippled unexpectedly — "touching it IS the alarm". A permanently
77
+ * committed copy would signal nothing, which is exactly why the ADR rejected
78
+ * shipping an empty stub. So the file is absent on a healthy `next` and present
79
+ * only inside a PR that needs it, and asserting either way is wrong.
80
+ *
81
+ * Until #2778 this passed only by accident: CONTEXT.md's `RULESET.` entries are
82
+ * themselves backtick-wrapped and contain backticks, so the sequential pairing in
83
+ * `extractTrackedRefs` happened to leave this token outside a code span. Any edit
84
+ * that shifted the parity — such as #2778's own — exposed it. A gate that passes
85
+ * by luck is not passing; naming the exemption makes the intent explicit and
86
+ * survives the next edit.
87
+ */
88
+ const INTENTIONALLY_ABSENT = new Set(['tests/emitted-drift-ack.json']);
89
+
67
90
  /**
68
91
  * Shape a backticked token must have to even be considered a path candidate:
69
92
  * one or more `/`-separated segments of word/dot/dash characters, with an
@@ -75,6 +98,7 @@ const PATH_TOKEN_RE = /^[\w.-]+(?:\/[\w.-]+)*(?::\d+)?$/;
75
98
 
76
99
  /** Whether `token` (line-suffix already stripped) is one this gate checks. */
77
100
  function isTracked(token) {
101
+ if (INTENTIONALLY_ABSENT.has(token)) return false;
78
102
  return TRACKED_EXACT.has(token) || TRACKED_PREFIXES.some((prefix) => token.startsWith(prefix));
79
103
  }
80
104
 
@@ -1,6 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  'use strict';
3
3
 
4
+ const path = require('path');
4
5
  const { execFileSync } = require('child_process');
5
6
  const { existsSync, readdirSync, appendFileSync } = require('fs');
6
7
 
@@ -105,9 +106,8 @@ const RULES = [
105
106
  fullMatrix: true,
106
107
  tests: [
107
108
  'tests/check-env.test.cjs',
108
- 'tests/npm-integrity-gate.test.cjs',
109
+ 'tests/npm-integrity-gate.test.cjs', // #2758: absorbs the former tests/bug-3588-npm-audit-clean.test.cjs (folded into it by consolidation epic #1969 B6 #1975; the stale filename here was a silent coverage hole this rule never actually re-selected)
109
110
  'tests/package-manifest.test.cjs',
110
- 'tests/bug-3588-npm-audit-clean.test.cjs',
111
111
  ],
112
112
  },
113
113
  {
@@ -116,9 +116,9 @@ const RULES = [
116
116
  // still trigger the migrated module's tests (otherwise CI silently skips them).
117
117
  match: path => path.startsWith('src/') || path === 'tsconfig.build.json',
118
118
  tests: [
119
- 'tests/semver-compare.test.cjs',
120
- 'tests/bug-10-semver-policy-consolidation.test.cjs',
121
- 'tests/golden-install-parity.test.cjs', // any src/installer change can alter emitted install artifacts → re-verify golden install parity (drift guard)
119
+ 'tests/semver-compare.test.cjs', // #2758: absorbs the former tests/bug-10-semver-policy-consolidation.test.cjs (folded into it by consolidation epic #1969 B3 #1972; the stale filename here was a silent coverage hole this rule never actually re-selected)
120
+ 'tests/emitted-provenance.test.cjs', // any src/installer change can alter emitted install artifacts → re-verify provenance totality (#2724: golden-install-parity retired, this is the sole gate)
121
+ 'tests/emitted-attribution.test.cjs',
122
122
  ],
123
123
  },
124
124
  {
@@ -142,19 +142,23 @@ const RULES = [
142
142
  // SCOPED_LANE_EXCLUDE guard below, which also drops it when it is itself a
143
143
  // changed test file.
144
144
  'tests/runtime-artifact-layout.test.cjs',
145
- 'tests/golden-install-parity.test.cjs', // any src/installer change can alter emitted install artifacts → re-verify golden install parity (drift guard)
145
+ 'tests/emitted-provenance.test.cjs', // any src/installer change can alter emitted install artifacts → re-verify provenance totality (#2724: golden-install-parity retired, this is the sole gate)
146
+ 'tests/emitted-attribution.test.cjs',
146
147
  ],
147
148
  },
148
149
  {
149
- name: 'shipped install content (golden-parity drift guard, #2267)',
150
+ name: 'shipped install content (emitted-attribution drift guard, #2267/#2724)',
150
151
  // Every source file the installer EMITS into a runtime layout is captured by
151
- // golden-install-parity + the install-tree snapshot. A source edit here that
152
- // changes emitted output MUST re-verify the fixtures — otherwise stale golden
153
- // fixtures merge silently (#2266: a hooks/gsd-statusline.js edit changed
154
- // installed output but no rule selected golden-parity, so stale fixtures
155
- // shipped to next undetected). Union semantics: this ADDS the parity guard on
156
- // top of each path's existing content-specific tests. Targeted lane only (the
157
- // golden test skips win32 by design), no fullMatrix.
152
+ // the emitted-attribution differential + the install-tree snapshot. A source
153
+ // edit here that changes emitted output MUST re-verify (#2266: a
154
+ // hooks/gsd-statusline.js edit changed installed output but no rule selected
155
+ // the drift guard, so a stale emitted state shipped to next undetected).
156
+ // Union semantics: this ADDS the drift guard on top of each path's existing
157
+ // content-specific tests. Targeted lane only (the real-tree test skips win32
158
+ // by design), no fullMatrix.
159
+ // #2724: golden-install-parity.test.cjs is retired (ADR-2719 Phase 4); the
160
+ // emitted differential (ADR-2719 Phase 2/3) is now the sole gate for a PR
161
+ // editing only shipped content, the archetypal emitted-ripple case.
158
162
  // NOTE: intentionally NOT a blanket 'gsd-core/' prefix, for two reasons:
159
163
  // (1) gsd-core/bin/** is tsc-compiled runtime output — EXCLUDED_PREFIXES-
160
164
  // excluded from both manifests, and already covered by the 'installer and
@@ -172,8 +176,9 @@ const RULES = [
172
176
  (path.startsWith('gsd-core/bin/shared/') && path.endsWith('.json')) ||
173
177
  ['scripts/fix-slash-commands.cjs', 'scripts/gen-capability-registry.cjs', 'scripts/gen-loop-host-contract.cjs'].includes(path),
174
178
  tests: [
175
- 'tests/golden-install-parity.test.cjs',
176
179
  'tests/golden-install-tree.test.cjs',
180
+ 'tests/emitted-provenance.test.cjs',
181
+ 'tests/emitted-attribution.test.cjs',
177
182
  ],
178
183
  },
179
184
  {
@@ -236,7 +241,13 @@ const RULES = [
236
241
  'tests/workflow-size-budget.test.cjs',
237
242
  'tests/workflow-guard-registration.test.cjs',
238
243
  'tests/commands.test.cjs',
239
- 'tests/bug-3683-workflow-colon-namespace-leak.test.cjs',
244
+ // #2758: was 'tests/bug-3683-workflow-colon-namespace-leak.test.cjs', deleted by
245
+ // consolidation epic #1969 (B6 #1975) and folded into slash-command-namespace.test.cjs
246
+ // ("folded:bug-3683-workflow-colon-namespace-leak" describe block). The stale filename
247
+ // here was itself an instance of this issue's defect class — silently dropped by
248
+ // existingTests() below, so gsd-core/workflows/ changes stopped re-running this
249
+ // regression's coverage with nothing signaling it.
250
+ 'tests/slash-command-namespace.test.cjs',
240
251
  ],
241
252
  },
242
253
  {
@@ -291,6 +302,41 @@ const RULES = [
291
302
  },
292
303
  ];
293
304
 
305
+ /**
306
+ * Every RULES[].tests entry (deduped, across every rule) that does NOT exist on
307
+ * disk. #2758: a rule naming a test file that no longer exists is not merely
308
+ * inert — existingTests() below silently drops it out of targeted_tests, with
309
+ * nothing in the CI output signaling why. Phase 4 (#2724) deletes
310
+ * tests/golden-install-parity.test.cjs; without this check, any rule still
311
+ * naming it would stop selecting the guard entirely and CI would stay green
312
+ * throughout. Pure and independent of which rule / which file: it catches ANY
313
+ * phantom entry, not only the two names this issue is about.
314
+ * Paths resolve relative to the repo root (this file's parent directory), not
315
+ * the caller's cwd, so the check behaves identically whether invoked as the CLI
316
+ * (`node scripts/ci-test-scope.cjs ...`, cwd == repo root by convention) or
317
+ * required directly by a test.
318
+ */
319
+ function missingRuleTestFiles(rules) {
320
+ const referenced = new Set();
321
+ for (const rule of rules) {
322
+ for (const f of rule.tests) referenced.add(f);
323
+ }
324
+ return [...referenced].filter(f => !existsSync(path.join(__dirname, '..', f))).sort();
325
+ }
326
+
327
+ // Fail loudly at module load, mirroring the PROTECTED_WORKFLOWS check above —
328
+ // this fires on EVERY invocation of the CLI (including the real `changes` job
329
+ // in .github/workflows/test.yml), not only when a test suite happens to run.
330
+ {
331
+ const missing = missingRuleTestFiles(RULES);
332
+ if (missing.length > 0) {
333
+ throw new Error(
334
+ `ci-test-scope: RULES reference test file(s) that do not exist on disk ` +
335
+ `(silent coverage hole — see #2758):\n ${missing.join('\n ')}`,
336
+ );
337
+ }
338
+ }
339
+
294
340
  function usage() {
295
341
  return [
296
342
  'Usage:',
@@ -523,4 +569,8 @@ function main() {
523
569
  }
524
570
  }
525
571
 
526
- runMain(main);
572
+ if (require.main === module) {
573
+ runMain(main);
574
+ }
575
+
576
+ module.exports = { RULES, missingRuleTestFiles };
@@ -347,10 +347,12 @@ function validate({ adrs, byFile, byId, nonConforming }) {
347
347
  //
348
348
  // Only a RATIFIED (Accepted) claimant is owed the back-link. A Proposed ADR's
349
349
  // supersession claim is prospective — it has not taken effect, so stamping its
350
- // target as superseded would assert something untrue (ADR-857 is Proposed and
351
- // claims to generalize ADR-0011/ADR-58, both of which are Accepted and live).
352
- // When such an ADR is ratified to Accepted, this check starts demanding the
353
- // back-links at exactly the right moment.
350
+ // target as superseded would assert something untrue. When such an ADR is
351
+ // ratified to Accepted, this check starts demanding the back-links at exactly
352
+ // the right moment — as ADR-857 shows: it was Proposed when this guard was
353
+ // written, was ratified to Accepted on 2026-07-17 (its claim over ADR-0011 /
354
+ // ADR-58 restated as "Subsumes", since both remain Accepted and live), and
355
+ // both targets now carry the reciprocal "Subsumed by" back-link this demands.
354
356
  const OPPOSITE = { out: 'in', in: 'out' };
355
357
  for (const a of adrs) {
356
358
  for (const kind of Object.keys(RELATION_SPEC)) {
@@ -114,8 +114,15 @@ function buildMatrix(registry) {
114
114
  const extByCap = extensionsByCapability(registry);
115
115
  const featureTable = renderTable(caps, 'feature', extByCap);
116
116
  const runtimeTable = renderTable(caps, 'runtime', extByCap);
117
+ // ADR-2782 D3 added a third role. Rendering only feature+runtime silently
118
+ // DROPPED every role:"reviewer" capability from the catalogue — and because
119
+ // `--check` compares generated output against the committed file, both omitted
120
+ // them identically, so the drift guard reported "up to date" while five shipped
121
+ // capabilities were invisible. A guard blind to a whole role is not guarding.
122
+ const reviewerTable = renderTable(caps, 'reviewer', extByCap);
117
123
  const featureCount = caps.filter((c) => c.role === 'feature').length;
118
124
  const runtimeCount = caps.filter((c) => c.role === 'runtime').length;
125
+ const reviewerCount = caps.filter((c) => c.role === 'reviewer').length;
119
126
 
120
127
  return `# Capability matrix reference
121
128
 
@@ -138,7 +145,7 @@ See also: [ADR-1244](../adr/1244-capability-ecosystem.md) —
138
145
  | Column | Description |
139
146
  |---|---|
140
147
  | **id** | Canonical capability identifier; unique across first- and third-party capabilities. Reserved prefixes: \`gsd-\`, \`gsd-core-\`, \`anthropic-\`. |
141
- | **role** | \`feature\` — extends what the loop does; \`runtime\` — adapts GSD to a specific AI runtime/IDE. |
148
+ | **role** | \`feature\` — extends what the loop does; \`runtime\` — adapts GSD to a specific AI runtime/IDE; \`reviewer\` — declares a cross-AI reviewer lane (ADR-2782). A capability may be both a runtime and a reviewer. |
142
149
  | **tier** | \`core\` — always active; \`standard\` — active when the runtime supports it; \`full\` — opt-in or runtime-specific. |
143
150
  | **engines.gsd** | Semver RANGE expressing host-version compatibility. A hard gate at install and at load. \`—\` means the capability declares no range. |
144
151
  | **extension points** | The loop points this capability registers hooks into (from the registry's \`byLoopPoint\` index). \`—\` means it registers none (typical for runtime capabilities, whose job is surface emission). |
@@ -180,6 +187,23 @@ emission), so their extension-point and hook-kind cells are \`—\`.
180
187
 
181
188
  ${runtimeTable}
182
189
 
190
+ ### Reviewer capabilities (role: reviewer) — ${reviewerCount}
191
+
192
+ Reviewer capabilities declare a cross-AI **reviewer lane** — one external CLI or
193
+ model endpoint \`/gsd:review\` hands a plan to (ADR-2782 D3). They are not install
194
+ targets: they emit no skills, agents, hooks or surface files, so their
195
+ extension-point and hook-kind cells are \`—\`. A host that is *also* a reviewer
196
+ (Claude, Codex, Cursor, OpenCode, Qwen, Antigravity) keeps one manifest and
197
+ appears under **runtime** above, carrying its lane alongside its runtime body;
198
+ only lanes that GSD never installs into appear here.
199
+
200
+ Because a lane receives the plan text, requirements, research findings and
201
+ \`CONTEXT.md\` decisions, it is a disclosed executable surface and is consent-gated
202
+ at install like any other — see
203
+ [the trust model](../explanation/capability-trust-model.md).
204
+
205
+ ${reviewerTable}
206
+
183
207
  ---
184
208
 
185
209
  ## Third-party capabilities
@@ -199,7 +223,7 @@ fields described below, with \`source\` = \`third-party\`.
199
223
  | Column | Value |
200
224
  |---|---|
201
225
  | **id** | As declared in \`capability.json\`. Must not use reserved prefixes (\`gsd-\`, \`gsd-core-\`, \`anthropic-\`). |
202
- | **role** | \`feature\` or \`runtime\`, as declared. |
226
+ | **role** | \`feature\`, \`runtime\`, or \`reviewer\`, as declared. |
203
227
  | **tier** | \`core\`, \`standard\`, or \`full\`, as declared. |
204
228
  | **engines.gsd** | Range from \`capability.json\`; verified at install and at each load. |
205
229
  | **extension points** | The loop points the capability registers into, validated against the known 12 identifiers. |
@@ -71,6 +71,7 @@ const {
71
71
  validateArtifactKindEntry,
72
72
  validateArtifactLayout,
73
73
  validateRuntimeBody,
74
+ collectReviewerWarnings,
74
75
  materializeHookFragments,
75
76
  validateAgainstContract,
76
77
  validateConsumesGlobal,
@@ -128,6 +129,71 @@ function loadCentralConfigKeys(schemaPath = CONFIG_SCHEMA_PATH) {
128
129
  return new Set(Array.isArray(manifest.validKeys) ? manifest.validKeys : []);
129
130
  }
130
131
 
132
+ /**
133
+ * Loads the central config-schema's DYNAMIC key patterns (#2797).
134
+ *
135
+ * `loadCentralConfigKeys` above reads `validKeys` only, so a federated key
136
+ * claimed by a central *pattern* was invisible to the exclusivity check. That is
137
+ * not a cosmetic gap: `isCentralConfigKey` consults these same patterns, and
138
+ * `mergeFederatedConfig` skips every key for which it returns true — so an
139
+ * overlapping slice is inert while the build stays green.
140
+ *
141
+ * The patterns are read from the SAME manifest the runtime reads and compiled
142
+ * with the same `source`, rather than re-implementing a matcher here, so the two
143
+ * cannot drift.
144
+ *
145
+ * Failure contract MATCHES `loadCentralConfigKeys` above deliberately — the two
146
+ * read the same file and must not disagree about what a broken one means:
147
+ * - ENOENT → empty list. Legitimately absent.
148
+ * - Any other read error, or a JSON parse error → prominent stderr + throw.
149
+ *
150
+ * An earlier revision swallowed the parse error and returned []. That is
151
+ * fail-OPEN on the gate this function exists to feed: with zero patterns,
152
+ * `validateCrossCapability`'s pattern-collision check silently passes and an
153
+ * inert federated slice ships green. It was masked in the one production call
154
+ * site only because `loadCentralConfigKeys` runs first against the same path and
155
+ * throws — a coincidence of ordering, not a guarantee, and this function is
156
+ * exported and called standalone.
157
+ *
158
+ * A single unparseable PATTERN is still skipped rather than fatal: that is a
159
+ * per-entry defect the central schema's own tests own, and skipping one pattern
160
+ * degrades to "checked less" rather than blocking every build.
161
+ */
162
+ function loadCentralConfigPatterns(schemaPath = CONFIG_SCHEMA_PATH) {
163
+ let raw;
164
+ try {
165
+ raw = fs.readFileSync(schemaPath, 'utf8');
166
+ } catch (err) {
167
+ if (err && err.code === 'ENOENT') return [];
168
+ process.stderr.write(
169
+ ' ERROR Failed to read config-schema manifest at ' + schemaPath + ': ' + err.message + '\n',
170
+ );
171
+ throw new ExitError(1, 'could not read config-schema manifest');
172
+ }
173
+
174
+ let manifest;
175
+ try {
176
+ manifest = JSON.parse(raw);
177
+ } catch (err) {
178
+ process.stderr.write(
179
+ ' ERROR Config-schema manifest at ' + schemaPath + ' is broken (JSON parse error): ' + err.message + '\n',
180
+ );
181
+ throw new ExitError(1, 'config-schema manifest JSON is malformed');
182
+ }
183
+ const declared = Array.isArray(manifest.dynamicKeyPatterns) ? manifest.dynamicKeyPatterns : [];
184
+ const out = [];
185
+ for (const entry of declared) {
186
+ const src = entry && typeof entry.source === 'string' ? entry.source : null;
187
+ if (!src) continue;
188
+ try {
189
+ out.push(new RegExp(src));
190
+ } catch {
191
+ // Unparseable pattern — the central schema's own tests own that failure.
192
+ }
193
+ }
194
+ return out;
195
+ }
196
+
131
197
  // ─── ADR-857 Phase 4a: Derived views ─────────────────────────────────────────
132
198
 
133
199
  // (Config-slice validation, per-capability validators, contract validators,
@@ -335,14 +401,21 @@ function runConsistencyGate(capabilityClusters, profileMembership, capMap) {
335
401
  * (used during 3a-impl while migration is in-progress).
336
402
  * @param {string} [capabilitiesDir] Override capabilities dir (for testing with fixtures).
337
403
  */
338
- function loadAndValidate(centralKeys, capabilitiesDir) {
404
+ function loadAndValidate(centralKeys, capabilitiesDir, centralPatterns) {
339
405
  const resolvedCentralKeys = centralKeys !== undefined ? centralKeys : loadCentralConfigKeys();
406
+ // #2797: patterns default to the real manifest unless a caller passes its own
407
+ // (tests pass [] to isolate the exact-key path).
408
+ const resolvedCentralPatterns = centralPatterns !== undefined ? centralPatterns : loadCentralConfigPatterns();
340
409
  const resolvedCapDir = capabilitiesDir !== undefined ? capabilitiesDir : CAPABILITIES_DIR;
341
410
  const errors = [];
342
411
  const capMap = new Map();
412
+ // ADR-2782 D4 — non-fatal diagnostics (e.g. an unknown field inside a reviewer
413
+ // body). These NEVER fail the build; they surface on stderr so a forward-built
414
+ // manifest degrades visibly instead of silently.
415
+ const warnings = [];
343
416
 
344
417
  if (!fs.existsSync(resolvedCapDir)) {
345
- return { capMap, errors };
418
+ return { capMap, errors, warnings };
346
419
  }
347
420
 
348
421
  // Compute wired points ONCE before iterating capabilities so the filesystem
@@ -366,6 +439,10 @@ function loadAndValidate(centralKeys, capabilitiesDir) {
366
439
  continue;
367
440
  }
368
441
 
442
+ // Collected BEFORE the error short-circuit below so a manifest that is both
443
+ // forward-built and invalid still reports why it looked unfamiliar.
444
+ for (const w of collectReviewerWarnings(cap)) warnings.push(folderId + '/capability.json: ' + w);
445
+
369
446
  const capErrors = validateCapability(cap, folderId);
370
447
  if (capErrors.length > 0) {
371
448
  for (const e of capErrors) errors.push(folderId + '/capability.json: ' + e);
@@ -397,7 +474,7 @@ function loadAndValidate(centralKeys, capabilitiesDir) {
397
474
  }
398
475
 
399
476
  // Cross-capability invariants — capMap contains only fully-valid capabilities at this point.
400
- const crossErrors = validateCrossCapability(capMap, resolvedCentralKeys);
477
+ const crossErrors = validateCrossCapability(capMap, resolvedCentralKeys, resolvedCentralPatterns);
401
478
  errors.push(...crossErrors);
402
479
 
403
480
  // C2: Global consumes-satisfiability — runs after capMap is fully built so cross-capability
@@ -406,7 +483,7 @@ function loadAndValidate(centralKeys, capabilitiesDir) {
406
483
  const consumesErrors = validateConsumesGlobal(capMap);
407
484
  errors.push(...consumesErrors);
408
485
 
409
- return { capMap, errors };
486
+ return { capMap, errors, warnings };
410
487
  }
411
488
 
412
489
  /**
@@ -446,6 +523,46 @@ function buildRegistry(capMap) {
446
523
  if (capId === '__proto__' || capId === 'constructor' || capId === 'prototype') continue;
447
524
  capabilities[capId] = cap;
448
525
 
526
+ // Federated config slice — harvested from ANY role that declares one.
527
+ //
528
+ // ADR-2782 D1/D9: this loop was nested inside the `role === 'feature'` branch,
529
+ // so a `role: "runtime"` capability's `config` was read by nothing and dropped
530
+ // in silence — the actual reason reviewer config keys are stranded in the
531
+ // central schema. (The often-cited reason, that the runtime body forbids
532
+ // feature-only fields, does not apply: `config` is NOT in
533
+ // FEATURE_FIELDS_FORBIDDEN_ON_RUNTIME.) Owning a config slice is a property of
534
+ // DECLARING one, not of being a feature. Verified inert at introduction — no
535
+ // shipped capability declares `config` on a non-feature role — so this changes
536
+ // no existing key; it stops a latent silent drop and unblocks Phase 4 (#2797).
537
+ for (const key of Object.keys(cap.config || {})) {
538
+ // S2b: inline literal guard at each write site (CodeQL barrier)
539
+ if (key === '__proto__' || key === 'constructor' || key === 'prototype') continue;
540
+ configKeys[key] = capId;
541
+
542
+ // Build configSchema entry — validate the slice first (throw on violation)
543
+ const slice = (cap.config || {})[key];
544
+ const sliceErrors = validateConfigSliceEntry(capId, key, slice);
545
+ if (sliceErrors.length > 0) {
546
+ throw new Error(
547
+ 'configSchema validation failed during registry build:\n' +
548
+ sliceErrors.map((e) => ' ' + e).join('\n'),
549
+ );
550
+ }
551
+ // S2b: inline literal guard for configSchema write site
552
+ if (key !== '__proto__' && key !== 'constructor' && key !== 'prototype') {
553
+ configSchema[key] = {
554
+ owner: capId,
555
+ type: slice.type,
556
+ default: slice.default,
557
+ description: slice.description,
558
+ };
559
+ // Preserve values array for enum types if present
560
+ if (slice.type === 'enum' && Array.isArray(slice.values)) {
561
+ configSchema[key].values = slice.values;
562
+ }
563
+ }
564
+ }
565
+
449
566
  if (cap.role === 'feature') {
450
567
  for (const skill of (cap.skills || [])) {
451
568
  // S2b: inline literal guard at each write site (CodeQL barrier)
@@ -457,34 +574,6 @@ function buildRegistry(capMap) {
457
574
  if (agent === '__proto__' || agent === 'constructor' || agent === 'prototype') continue;
458
575
  byAgent[agent] = capId;
459
576
  }
460
- for (const key of Object.keys(cap.config || {})) {
461
- // S2b: inline literal guard at each write site (CodeQL barrier)
462
- if (key === '__proto__' || key === 'constructor' || key === 'prototype') continue;
463
- configKeys[key] = capId;
464
-
465
- // Build configSchema entry — validate the slice first (throw on violation)
466
- const slice = (cap.config || {})[key];
467
- const sliceErrors = validateConfigSliceEntry(capId, key, slice);
468
- if (sliceErrors.length > 0) {
469
- throw new Error(
470
- 'configSchema validation failed during registry build:\n' +
471
- sliceErrors.map((e) => ' ' + e).join('\n'),
472
- );
473
- }
474
- // S2b: inline literal guard for configSchema write site
475
- if (key !== '__proto__' && key !== 'constructor' && key !== 'prototype') {
476
- configSchema[key] = {
477
- owner: capId,
478
- type: slice.type,
479
- default: slice.default,
480
- description: slice.description,
481
- };
482
- // Preserve values array for enum types if present
483
- if (slice.type === 'enum' && Array.isArray(slice.values)) {
484
- configSchema[key].values = slice.values;
485
- }
486
- }
487
- }
488
577
 
489
578
  for (const step of (cap.steps || [])) {
490
579
  if (VALID_LOOP_POINTS.has(step.point)) {
@@ -741,7 +830,11 @@ function main() {
741
830
  if (flag === '--check') {
742
831
  // Fix #3: read the REAL central config keys so collision detection fires and is visible.
743
832
  const centralKeys = loadCentralConfigKeys();
744
- const { capMap, errors } = loadAndValidate(centralKeys);
833
+ const { capMap, errors, warnings } = loadAndValidate(centralKeys);
834
+
835
+ // ADR-2782 D4 — non-fatal manifest diagnostics. Emitted BEFORE the hard-error
836
+ // exit so a forward-built manifest still explains itself on a failing build.
837
+ for (const w of warnings) process.stderr.write(w + '\n');
745
838
 
746
839
  // Separate pending-migration warnings from hard errors
747
840
  const { hardErrors, pendingMigrationWarnings } = classifyCrossErrors(errors);
@@ -778,7 +871,11 @@ function main() {
778
871
  } else if (flag === '--write') {
779
872
  // Fix #3: read the REAL central config keys so collision detection fires and is visible.
780
873
  const centralKeys = loadCentralConfigKeys();
781
- const { capMap, errors } = loadAndValidate(centralKeys);
874
+ const { capMap, errors, warnings } = loadAndValidate(centralKeys);
875
+
876
+ // ADR-2782 D4 — non-fatal manifest diagnostics. Emitted BEFORE the hard-error
877
+ // exit so a forward-built manifest still explains itself on a failing build.
878
+ for (const w of warnings) process.stderr.write(w + '\n');
782
879
 
783
880
  // Separate pending-migration warnings from hard errors
784
881
  const { hardErrors, pendingMigrationWarnings } = classifyCrossErrors(errors);
@@ -828,6 +925,7 @@ module.exports = {
828
925
  validateCrossCapability,
829
926
  classifyCrossErrors,
830
927
  loadCentralConfigKeys,
928
+ loadCentralConfigPatterns,
831
929
  loadAndValidate,
832
930
  buildRegistry,
833
931
  serializeRegistry,