@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
@@ -562,13 +562,19 @@ function parseCoverageMatrix(text) {
562
562
  }
563
563
  return out;
564
564
  }
565
- // (2) markdown table — collect table rows whose decision column parses.
565
+ // (2) markdown table — collect rows from coverage matrix tables only (#2366).
566
+ // Track whether we are inside a recognized coverage matrix (after a header
567
+ // row, before a non-pipe line ends the table). This prevents summary tables
568
+ // elsewhere in the file from being parsed as data (#2366 bug 1) and allows
569
+ // multi-section matrices with repeated headers (#2366 bug 2).
566
570
  const lines = src.split('\n');
567
- let sawHeader = false;
571
+ let inMatrix = false;
568
572
  for (const line of lines) {
569
573
  const trimmed = line.trim();
570
- if (!trimmed.startsWith('|'))
574
+ if (!trimmed.startsWith('|')) {
575
+ inMatrix = false;
571
576
  continue;
577
+ }
572
578
  const cells = trimmed.slice(1, trimmed.endsWith('|') ? -1 : trimmed.length).split('|');
573
579
  if (cells.length < 2)
574
580
  continue;
@@ -577,13 +583,21 @@ function parseCoverageMatrix(text) {
577
583
  // is not mistaken for a separator.
578
584
  if (cleaned.every((c) => /^:?-{3,}:?$/.test(c)))
579
585
  continue;
580
- const decisionCell = (cleaned[1] || '').toUpperCase();
581
- // header detection
582
- if (!sawHeader && cleaned[0].toLowerCase() === 'capability') {
583
- sawHeader = true;
584
- out.format = 'table';
586
+ // Strip markdown emphasis (**, *, __, _, `) from the decision cell before
587
+ // comparison so **OPT-OUT** parses correctly (#2366 bug 3).
588
+ const decisionCell = (cleaned[1] || '').replace(/[*_`]/g, '').trim().toUpperCase();
589
+ // header detection — recognized by 'capability' in column 0; allows multiple
590
+ // headers for multi-section matrices (#2366 bug 2).
591
+ if (cleaned[0].toLowerCase() === 'capability') {
592
+ inMatrix = true;
593
+ if (out.format === 'none')
594
+ out.format = 'table';
585
595
  continue;
586
596
  }
597
+ // Only parse data rows from inside a recognized coverage matrix table.
598
+ // A pipe-table outside the matrix (e.g., a summary table) is ignored (#2366 bug 1).
599
+ if (!inMatrix)
600
+ continue;
587
601
  if (!VALID_DECISIONS.has(decisionCell)) {
588
602
  // A row that otherwise looks like data (≥3 cells, non-empty capability)
589
603
  // but carries a malformed decision is a real error, not a row to skip
@@ -66,7 +66,7 @@ function scanDebugSessions(planDir) {
66
66
  const content = (0, shell_command_projection_cjs_1.platformReadSync)(safeFilePath);
67
67
  if (content === null)
68
68
  continue;
69
- const fm = extractFrontmatter(content);
69
+ const fm = extractFrontmatter(content, safeFilePath);
70
70
  const status = (fm.status || 'unknown').toLowerCase();
71
71
  if (status === 'resolved' || status === 'complete')
72
72
  continue;
@@ -148,7 +148,7 @@ function scanQuickTasks(planDir) {
148
148
  status = 'unreadable';
149
149
  }
150
150
  else {
151
- const fm = extractFrontmatter(content);
151
+ const fm = extractFrontmatter(content, safeSum);
152
152
  status = (fm.status || 'unknown').toLowerCase();
153
153
  }
154
154
  }
@@ -205,7 +205,7 @@ function scanThreads(planDir) {
205
205
  const content = (0, shell_command_projection_cjs_1.platformReadSync)(safeFilePath);
206
206
  if (content === null)
207
207
  continue;
208
- const fm = extractFrontmatter(content);
208
+ const fm = extractFrontmatter(content, safeFilePath);
209
209
  let status = (fm.status || '').toLowerCase().trim();
210
210
  // Fall back to scanning body for ## Status: OPEN / IN PROGRESS
211
211
  if (!status) {
@@ -266,7 +266,7 @@ function scanTodos(planDir) {
266
266
  const content = (0, shell_command_projection_cjs_1.platformReadSync)(safeFilePath);
267
267
  if (content === null)
268
268
  continue;
269
- const fm = extractFrontmatter(content);
269
+ const fm = extractFrontmatter(content, safeFilePath);
270
270
  // Extract first line of body after frontmatter
271
271
  const bodyMatch = content.replace(/^---[\s\S]*?---\n?/, '');
272
272
  const firstLine = bodyMatch.trim().split('\n')[0] || '';
@@ -317,7 +317,7 @@ function scanSeeds(planDir) {
317
317
  const content = (0, shell_command_projection_cjs_1.platformReadSync)(safeFilePath);
318
318
  if (content === null)
319
319
  continue;
320
- const fm = extractFrontmatter(content);
320
+ const fm = extractFrontmatter(content, safeFilePath);
321
321
  const status = (fm.status || 'dormant').toLowerCase();
322
322
  if (!unimplementedStatuses.has(status))
323
323
  continue;
@@ -382,7 +382,7 @@ function scanUatGaps(planDir) {
382
382
  const content = (0, shell_command_projection_cjs_1.platformReadSync)(safeFilePath);
383
383
  if (content === null)
384
384
  continue;
385
- const fm = extractFrontmatter(content);
385
+ const fm = extractFrontmatter(content, safeFilePath);
386
386
  const status = (fm.status || 'unknown').toLowerCase();
387
387
  const result = (fm.result || '').toLowerCase();
388
388
  // Also accept `result: all_pass` as a fallback when status is absent
@@ -445,7 +445,7 @@ function scanVerificationGaps(planDir) {
445
445
  const content = (0, shell_command_projection_cjs_1.platformReadSync)(safeFilePath);
446
446
  if (content === null)
447
447
  continue;
448
- const fm = extractFrontmatter(content);
448
+ const fm = extractFrontmatter(content, safeFilePath);
449
449
  const status = (fm.status || 'unknown').toLowerCase();
450
450
  if (status !== 'gaps_found' && status !== 'human_needed')
451
451
  continue;
@@ -500,7 +500,7 @@ function scanContextQuestions(planDir) {
500
500
  const content = (0, shell_command_projection_cjs_1.platformReadSync)(safeFilePath);
501
501
  if (content === null)
502
502
  continue;
503
- const fm = extractFrontmatter(content);
503
+ const fm = extractFrontmatter(content, safeFilePath);
504
504
  // Check frontmatter open_questions field
505
505
  let questions = [];
506
506
  if (fm.open_questions) {
@@ -399,6 +399,10 @@ function isValidConsentRecord(rec) {
399
399
  return false;
400
400
  if (typeof r['consentedAt'] !== 'string' || !r['consentedAt'])
401
401
  return false;
402
+ // Optional (see ConsentRecord.reviewerHost): absent is valid and is the common case. Present but
403
+ // non-string is a corrupt record — reject rather than silently comparing against a non-host.
404
+ if (r['reviewerHost'] !== undefined && typeof r['reviewerHost'] !== 'string')
405
+ return false;
402
406
  return true;
403
407
  }
404
408
  /**
@@ -645,7 +649,7 @@ function writeConsentStore(gsdHome, store) {
645
649
  * written. (The OLD code returned a null handle and proceeded unlocked — that is the bug.)
646
650
  */
647
651
  function recordProjectConsent(args) {
648
- const { gsdHome, projectRoot, id, integrity, disclosureSignature, contentHash } = args;
652
+ const { gsdHome, projectRoot, id, integrity, disclosureSignature, contentHash, reviewerHost } = args;
649
653
  if (isUnsafeCapabilityId(id)) {
650
654
  throw new Error(`Invalid capability id "${String(id)}": must match /^[a-z][a-z0-9-]*$/ (kebab-case, lowercase). ` +
651
655
  `Unsafe or non-kebab ids are rejected to keep the consent store prototype-pollution-safe.`);
@@ -685,6 +689,9 @@ function recordProjectConsent(args) {
685
689
  disclosureSignature,
686
690
  contentHash,
687
691
  consentedAt: new Date().toISOString(),
692
+ // Written only when the capability actually declares an egress destination, so a spawn lane
693
+ // or a non-lane capability keeps a byte-identical record shape.
694
+ ...(typeof reviewerHost === 'string' && reviewerHost ? { reviewerHost } : {}),
688
695
  };
689
696
  writeConsentStore(gsdHome, store);
690
697
  }
@@ -730,6 +737,37 @@ function revokeProjectConsent(args) {
730
737
  releaseConsentLock(lock);
731
738
  }
732
739
  }
740
+ /**
741
+ * The egress destination a capability's consent was granted against, or `undefined`.
742
+ *
743
+ * ADR-2782 D5 rule 4 (#2799) — read on the INVOCATION path, not only at install, because
744
+ * `hostConfigKey` names a key in `.planning/config.json`: the one consent-bound value that lives
745
+ * outside the SHA-pinned bundle and can be changed by an ordinary pull request with no re-install
746
+ * and no integrity check.
747
+ *
748
+ * `undefined` means "nothing to compare", and the caller MUST treat that as allow, not deny. Two
749
+ * legitimate ways to get it: the capability has no consent record at all (first-party lanes ship
750
+ * inside the SHA-pinned distribution and are never consent-gated), or the record predates this
751
+ * field. Denying on absence would break every existing local-model user on upgrade.
752
+ *
753
+ * Non-throwing: a missing, corrupt, or oversized store yields `undefined` like any other absence.
754
+ */
755
+ function readConsentedReviewerHost(args) {
756
+ const { gsdHome, projectRoot, id } = args;
757
+ if (isUnsafeCapabilityId(id))
758
+ return undefined;
759
+ try {
760
+ const store = readConsentStore(gsdHome);
761
+ const key = consentKey(realpathProject(projectRoot), id);
762
+ if (!Object.prototype.hasOwnProperty.call(store.records, key))
763
+ return undefined;
764
+ const host = store.records[key].reviewerHost;
765
+ return typeof host === 'string' && host ? host : undefined;
766
+ }
767
+ catch {
768
+ return undefined;
769
+ }
770
+ }
733
771
  /**
734
772
  * #1459 finding 2 (round 6): TEST-ONLY — override the cumulative bundle entry-count cap and return a
735
773
  * restore() that resets it to the production default. Lets a test prove the streaming walk fails closed
@@ -744,6 +782,7 @@ module.exports = {
744
782
  consentStorePath,
745
783
  bundleContentHash,
746
784
  readConsentStore,
785
+ readConsentedReviewerHost,
747
786
  hasProjectConsent,
748
787
  recordProjectConsent,
749
788
  revokeProjectConsent,
@@ -672,6 +672,59 @@ function warnIfConsentSkipped(opts, id) {
672
672
  * Best-effort: a consent-store write failure must not turn a successful install/upgrade into a
673
673
  * failure (the bundle is already committed) — it is surfaced as a warning, not a throw.
674
674
  */
675
+ /**
676
+ * Resolve an `openai-http` reviewer lane's declared `hostConfigKey` to the destination it currently
677
+ * names, or `undefined` when this capability is not such a lane.
678
+ *
679
+ * Falls back to the lane's declared `defaultHost` when the key is unset, because that is exactly
680
+ * what the invocation path will do — binding the config value while the runtime uses the default
681
+ * would guarantee a mismatch on the very first review.
682
+ *
683
+ * Non-throwing: consent binding is best-effort and must never turn a successful install into a
684
+ * failure. An unresolvable host simply records nothing, which reads as "not bound" and allows.
685
+ */
686
+ function resolveReviewerEgressHost(opts, manifest) {
687
+ try {
688
+ const reviewer = manifest['reviewer'];
689
+ if (reviewer === null || typeof reviewer !== 'object' || Array.isArray(reviewer))
690
+ return undefined;
691
+ const r = reviewer;
692
+ if (r['transport'] !== 'openai-http')
693
+ return undefined;
694
+ const invoke = r['invoke'];
695
+ if (invoke === null || typeof invoke !== 'object')
696
+ return undefined;
697
+ const inv = invoke;
698
+ const key = typeof inv['hostConfigKey'] === 'string' ? inv['hostConfigKey'] : '';
699
+ const fallback = typeof inv['defaultHost'] === 'string' ? inv['defaultHost'] : '';
700
+ let configured = '';
701
+ if (key) {
702
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
703
+ const cfgLoader = require('./config-loader.cjs');
704
+ const root = projectRootMod.consentProjectRoot(opts.runtimeDir);
705
+ const cfg = cfgLoader.loadConfigResolved ? (cfgLoader.loadConfigResolved(root).config ?? {}) : {};
706
+ let cur = cfg;
707
+ for (const part of key.split('.')) {
708
+ if (cur === null || typeof cur !== 'object') {
709
+ cur = undefined;
710
+ break;
711
+ }
712
+ cur = Object.prototype.hasOwnProperty.call(cur, part)
713
+ ? cur[part]
714
+ : undefined;
715
+ }
716
+ if (typeof cur === 'string')
717
+ configured = cur.trim();
718
+ }
719
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
720
+ const { normalizeHost } = require('./review-lane-invocation.cjs');
721
+ const resolved = normalizeHost(configured || fallback);
722
+ return resolved || undefined;
723
+ }
724
+ catch {
725
+ return undefined;
726
+ }
727
+ }
675
728
  function bindProjectConsent(opts, id, integrity, manifest) {
676
729
  // #1459 IC-07: a project-scope op WITHOUT a consent store cannot bind — warn (then nothing to do).
677
730
  if (!shouldBindConsent(opts)) {
@@ -689,6 +742,11 @@ function bindProjectConsent(opts, id, integrity, manifest) {
689
742
  integrity,
690
743
  disclosureSignature: trustMod.signatureForManifest(manifest),
691
744
  contentHash: consentMod.bundleContentHash(capDir(opts.runtimeDir, id)),
745
+ // ADR-2782 D5 rule 1 (#2799): bind the RESOLVED egress destination, not merely the config key
746
+ // that names it. The key lives in `.planning/config.json`, outside the SHA-pinned bundle, so
747
+ // without this the user consents to "wherever that key points" — a promise the bundle hash
748
+ // cannot keep. Phase 5b re-resolves and compares at invocation (rule 4).
749
+ reviewerHost: resolveReviewerEgressHost(opts, manifest),
692
750
  });
693
751
  }
694
752
  catch (err) {
@@ -426,6 +426,8 @@ function loadRegistry(options = {}) {
426
426
  // consent the CLI records is the consent the loader checks. The consent store NEVER lives in a repo.
427
427
  const gsdHome = options.gsdHome || process.env['GSD_HOME'] || os.homedir();
428
428
  const warnings = [];
429
+ // ADR-2782 D4.3 — non-fatal notices for capabilities that are ACCEPTED (see OverlayMeta).
430
+ const diagnostics = [];
429
431
  const incompatibleGateCapIds = [];
430
432
  const blockedGates = [];
431
433
  const commandRoots = {};
@@ -719,6 +721,26 @@ function loadRegistry(options = {}) {
719
721
  }
720
722
  }
721
723
  // Accepted.
724
+ //
725
+ // ADR-2782 D4.3: an unknown field inside a `reviewer` body is IGNORED WITH A
726
+ // WARNING rather than failing validation, so a lane built for a newer GSD
727
+ // degrades to accepted-but-partially-understood instead of being rejected.
728
+ // That case validates cleanly, so without this call it would surface
729
+ // nowhere at runtime — the build-time generator only ever sees first-party
730
+ // in-repo manifests, never an installed third-party overlay. Guarded on
731
+ // presence so an older built validator without the function still loads,
732
+ // and wrapped because the never-crash contract (ADR-1244 D2) outranks a
733
+ // diagnostic: a throwing collector must not cost the user a working lane.
734
+ if (typeof validator.collectReviewerWarnings === 'function') {
735
+ try {
736
+ for (const w of validator.collectReviewerWarnings(cap) || []) {
737
+ diagnostics.push(`${root.scope}:${id}: ${w}`);
738
+ }
739
+ }
740
+ catch {
741
+ // A diagnostic that cannot be produced is not worth failing an install over.
742
+ }
743
+ }
722
744
  overlayCaps.push(cap);
723
745
  acceptedIds.add(id);
724
746
  for (const s of skills)
@@ -752,7 +774,7 @@ function loadRegistry(options = {}) {
752
774
  }
753
775
  }
754
776
  }
755
- const meta = { warnings, incompatibleGateCapIds, blockedGates, commandRoots };
777
+ const meta = { warnings, diagnostics, incompatibleGateCapIds, blockedGates, commandRoots };
756
778
  if (overlayCaps.length === 0) {
757
779
  // Nothing to compose. Return the frozen registry unchanged when there is
758
780
  // also nothing to report (identity-stable); otherwise attach diagnostics.