@opengsd/gsd-core 1.6.0-rc.3 → 1.7.0-rc.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (105) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/agents/gsd-advisor-researcher.md +2 -0
  3. package/agents/gsd-ai-researcher.md +2 -0
  4. package/agents/gsd-assumptions-analyzer.md +2 -0
  5. package/agents/gsd-doc-classifier.md +2 -0
  6. package/agents/gsd-doc-synthesizer.md +2 -0
  7. package/agents/gsd-domain-researcher.md +2 -0
  8. package/agents/gsd-eval-auditor.md +6 -9
  9. package/agents/gsd-phase-researcher.md +2 -0
  10. package/agents/gsd-project-researcher.md +2 -0
  11. package/agents/gsd-research-synthesizer.md +2 -0
  12. package/agents/gsd-ui-researcher.md +2 -0
  13. package/agents/gsd-verifier.md +1 -0
  14. package/bin/gsd-mcp-server.js +31 -0
  15. package/bin/install.js +512 -1150
  16. package/commands/gsd/review.md +6 -0
  17. package/gemini-extension.json +1 -1
  18. package/gsd-core/bin/gsd-tools.cjs +127 -2
  19. package/gsd-core/bin/lib/adapter-declarative.cjs +35 -0
  20. package/gsd-core/bin/lib/adapter-imperative.cjs +52 -0
  21. package/gsd-core/bin/lib/assumption-delta.cjs +231 -0
  22. package/gsd-core/bin/lib/capability-lifecycle.cjs +7 -7
  23. package/gsd-core/bin/lib/capability-loader.cjs +18 -0
  24. package/gsd-core/bin/lib/capability-lock.cjs +2 -2
  25. package/gsd-core/bin/lib/capability-registry.cjs +889 -82
  26. package/gsd-core/bin/lib/capability-source.cjs +4 -4
  27. package/gsd-core/bin/lib/capability-validator.cjs +198 -0
  28. package/gsd-core/bin/lib/cli-skew-check.cjs +44 -0
  29. package/gsd-core/bin/lib/command-aliases.cjs +18 -1
  30. package/gsd-core/bin/lib/config.cjs +27 -0
  31. package/gsd-core/bin/lib/decisions.cjs +27 -0
  32. package/gsd-core/bin/lib/embedding-adapter.cjs +27 -0
  33. package/gsd-core/bin/lib/eval-command-router.cjs +21 -0
  34. package/gsd-core/bin/lib/eval.cjs +60 -0
  35. package/gsd-core/bin/lib/external-descriptor-trust.cjs +70 -0
  36. package/gsd-core/bin/lib/frontmatter.cjs +132 -13
  37. package/gsd-core/bin/lib/hook-bus.cjs +81 -0
  38. package/gsd-core/bin/lib/host-integration.cjs +408 -0
  39. package/gsd-core/bin/lib/init.cjs +132 -27
  40. package/gsd-core/bin/lib/install-engine.cjs +755 -0
  41. package/gsd-core/bin/lib/install-profiles.cjs +35 -4
  42. package/gsd-core/bin/lib/installer-migrations.cjs +1 -1
  43. package/gsd-core/bin/lib/io.cjs +1 -0
  44. package/gsd-core/bin/lib/mcp-server.cjs +194 -0
  45. package/gsd-core/bin/lib/milestone.cjs +27 -30
  46. package/gsd-core/bin/lib/model-adapter.cjs +50 -0
  47. package/gsd-core/bin/lib/phase.cjs +57 -74
  48. package/gsd-core/bin/lib/plan-scan.cjs +2 -2
  49. package/gsd-core/bin/lib/planning-workspace.cjs +1 -1
  50. package/gsd-core/bin/lib/probe-core.cjs +91 -1
  51. package/gsd-core/bin/lib/review-reviewer-selection.cjs +129 -13
  52. package/gsd-core/bin/lib/roadmap-upgrade.cjs +3 -2
  53. package/gsd-core/bin/lib/roadmap.cjs +17 -3
  54. package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +65 -9
  55. package/gsd-core/bin/lib/runtime-artifact-install-plan.cjs +54 -4
  56. package/gsd-core/bin/lib/runtime-artifact-layout.cjs +5 -2
  57. package/gsd-core/bin/lib/runtime-hooks-surface.cjs +1 -1
  58. package/gsd-core/bin/lib/runtime-name-policy.cjs +160 -30
  59. package/gsd-core/bin/lib/shell-command-projection.cjs +47 -1
  60. package/gsd-core/bin/lib/stale-bake-guard.cjs +254 -0
  61. package/gsd-core/bin/lib/state-command-router.cjs +4 -0
  62. package/gsd-core/bin/lib/state-io.cjs +55 -0
  63. package/gsd-core/bin/lib/state-transition.cjs +1588 -0
  64. package/gsd-core/bin/lib/state.cjs +340 -689
  65. package/gsd-core/bin/lib/surface.cjs +4 -1
  66. package/gsd-core/bin/lib/uat-predicate.cjs +13 -6
  67. package/gsd-core/bin/lib/verification.cjs +67 -6
  68. package/gsd-core/bin/lib/verify.cjs +8 -1
  69. package/gsd-core/bin/lib/workstream.cjs +4 -4
  70. package/gsd-core/bin/shared/config-defaults.manifest.json +3 -0
  71. package/gsd-core/bin/shared/config-schema.manifest.json +11 -1
  72. package/gsd-core/references/honest-verifier.md +105 -0
  73. package/gsd-core/references/reviewer-instances.md +99 -0
  74. package/gsd-core/references/untrusted-input-boundary.md +13 -0
  75. package/gsd-core/workflows/autonomous.md +61 -54
  76. package/gsd-core/workflows/complete-milestone.md +27 -8
  77. package/gsd-core/workflows/execute-phase.md +1 -1
  78. package/gsd-core/workflows/manager.md +30 -20
  79. package/gsd-core/workflows/new-project.md +78 -12
  80. package/gsd-core/workflows/plan-phase.md +1 -1
  81. package/gsd-core/workflows/profile-user.md +6 -2
  82. package/gsd-core/workflows/progress.md +37 -4
  83. package/gsd-core/workflows/quick.md +3 -1
  84. package/gsd-core/workflows/review.md +26 -0
  85. package/gsd-core/workflows/ship.md +3 -1
  86. package/gsd-core/workflows/spec-phase.md +3 -1
  87. package/gsd-core/workflows/thread.md +4 -4
  88. package/gsd-core/workflows/transition.md +14 -12
  89. package/gsd-core/workflows/ui-review.md +2 -6
  90. package/gsd-core/workflows/verify-phase.md +11 -4
  91. package/gsd-core/workflows/verify-work.md +44 -0
  92. package/hooks/dist/gsd-graphify-update.sh +7 -1
  93. package/hooks/dist/gsd-read-injection-scanner.js +49 -25
  94. package/hooks/gsd-graphify-update.sh +7 -1
  95. package/hooks/gsd-read-injection-scanner.js +49 -25
  96. package/hooks/hooks.json +1 -1
  97. package/package.json +4 -4
  98. package/scripts/check-alias-drift.cjs +5 -0
  99. package/scripts/ci-test-scope.cjs +38 -9
  100. package/scripts/lint-allow-test-rule-refs.allowlist.json +0 -1
  101. package/scripts/lint-regression-test-names.allowlist.json +3 -0
  102. package/scripts/lint-test-file-count.allowlist.json +19 -5
  103. package/scripts/mutation-matrix.cjs +45 -3
  104. package/scripts/prompt-injection-scan.sh +13 -0
  105. package/scripts/lint-windows-test-portability.cjs +0 -178
@@ -594,13 +594,13 @@ function stageValidated(opts) {
594
594
  // lives in capability-lifecycle.cjs and uses promote:false above.)
595
595
  if (node_fs_1.default.existsSync(finalDir)) {
596
596
  const backupDir = `${finalDir}.old-${process.pid}-${Date.now()}`;
597
- node_fs_1.default.renameSync(finalDir, backupDir);
597
+ shellSeam.retryRenameSync(finalDir, backupDir);
598
598
  try {
599
- node_fs_1.default.renameSync(stagingDir, finalDir);
599
+ shellSeam.retryRenameSync(stagingDir, finalDir);
600
600
  }
601
601
  catch (err) {
602
602
  try {
603
- node_fs_1.default.renameSync(backupDir, finalDir);
603
+ shellSeam.retryRenameSync(backupDir, finalDir);
604
604
  }
605
605
  catch { /* best-effort restore */ }
606
606
  throw err;
@@ -611,7 +611,7 @@ function stageValidated(opts) {
611
611
  catch { /* best-effort */ }
612
612
  }
613
613
  else {
614
- node_fs_1.default.renameSync(stagingDir, finalDir);
614
+ shellSeam.retryRenameSync(stagingDir, finalDir);
615
615
  }
616
616
  const version = typeof cap['version'] === 'string' ? cap['version'] : '';
617
617
  return { id, version, stagedDir: finalDir, integrity, source };
@@ -713,6 +713,16 @@ const VALID_INSTALL_SURFACES = new Set(['settings-json', 'codex-toml', 'copilot-
713
713
  const VALID_PERMISSION_WRITERS = new Set(['opencode', 'kilo']);
714
714
  const VALID_EXTENDED_HOOK_EVENTS = new Set(['SubagentStop', 'Stop', 'PreCompact', 'FileChanged', 'BeforeAgent', 'AfterAgent', 'BeforeModel']);
715
715
 
716
+ // ADR-1239 Phase A: hostIntegration axes (MUST stay parity-identical to HOST_INTEGRATION_AXES in src/host-integration.cts)
717
+ const VALID_EMBEDDING_MODES = new Set(['imperative', 'declarative']);
718
+ const VALID_COMMAND_SURFACES = new Set(['slash-file', 'slash-programmatic', 'slash-toml', 'palette', 'prose-only']);
719
+ const VALID_MODEL_MODES = new Set(['active', 'passive']);
720
+ const VALID_HOOK_BUSES = new Set(['host', 'engine', 'none']);
721
+ const VALID_STATE_IO = new Set(['filesystem', 'sandboxed-storage', 'session-log-append']);
722
+ const VALID_TRANSPORTS = new Set(['mcp', 'native-extension']);
723
+ const VALID_HOST_RUNTIMES = new Set(['node', 'bun', 'sandboxed-web', 'python', 'go', 'rust', 'electron', 'other']);
724
+ const VALID_SUBAGENT_TOOLKITS = new Set(['full', 'read-only']);
725
+
716
726
  // GATE A: installSurface → allowed hooksSurface values (DEFECT.GENERATIVE-FIX: parity invariant)
717
727
  // Derived from the actual pairings in the 16 real runtime descriptors.
718
728
  const INSTALL_SURFACE_TO_ALLOWED_HOOKS_SURFACES = new Map([
@@ -1020,6 +1030,20 @@ function validateRuntimeBody(cap) {
1020
1030
  );
1021
1031
  }
1022
1032
 
1033
+ // localConfigDir — REQUIRED non-empty dot-dir string (ADR-1239 Phase B #1679)
1034
+ // Must start with '.' (e.g. ".claude", ".cursor"). Validated here so the registry
1035
+ // generator catches any descriptor missing the field before regenerating.
1036
+ if (typeof r.localConfigDir !== 'string' || r.localConfigDir.length === 0) {
1037
+ errors.push(
1038
+ 'runtime.localConfigDir is required and must be a non-empty string (e.g. ".claude"); ' +
1039
+ 'got: ' + JSON.stringify(r.localConfigDir),
1040
+ );
1041
+ } else if (!r.localConfigDir.startsWith('.')) {
1042
+ errors.push(
1043
+ 'runtime.localConfigDir must start with "." (a dot-dir); got: ' + JSON.stringify(r.localConfigDir),
1044
+ );
1045
+ }
1046
+
1023
1047
  // extendedHookEvents — required array; every element must be in closed enum
1024
1048
  if (!Array.isArray(r.extendedHookEvents)) {
1025
1049
  errors.push(
@@ -1037,6 +1061,162 @@ function validateRuntimeBody(cap) {
1037
1061
  }
1038
1062
  }
1039
1063
 
1064
+ // hostIntegration — ADR-1239 Phase A: required object with closed-enum axes
1065
+ if (typeof r.hostIntegration !== 'object' || r.hostIntegration === null || Array.isArray(r.hostIntegration)) {
1066
+ errors.push('runtime.hostIntegration is required and must be an object');
1067
+ } else {
1068
+ const hi = r.hostIntegration;
1069
+
1070
+ // S2b: reserved-OWN-KEY guard on hostIntegration (CodeQL barrier — inline literal comparisons)
1071
+ if (Object.prototype.hasOwnProperty.call(hi, '__proto__')) {
1072
+ errors.push('runtime.hostIntegration must not contain reserved key "__proto__"');
1073
+ }
1074
+ if (Object.prototype.hasOwnProperty.call(hi, 'constructor')) {
1075
+ errors.push('runtime.hostIntegration must not contain reserved key "constructor"');
1076
+ }
1077
+ if (Object.prototype.hasOwnProperty.call(hi, 'prototype')) {
1078
+ errors.push('runtime.hostIntegration must not contain reserved key "prototype"');
1079
+ }
1080
+
1081
+ // embeddingMode
1082
+ if (hi.embeddingMode === '__proto__' || hi.embeddingMode === 'constructor' || hi.embeddingMode === 'prototype') {
1083
+ errors.push('runtime.hostIntegration.embeddingMode "' + hi.embeddingMode + '" is a reserved name');
1084
+ } else if (hi.embeddingMode !== 'undocumented' && !VALID_EMBEDDING_MODES.has(hi.embeddingMode)) {
1085
+ errors.push(
1086
+ 'runtime.hostIntegration.embeddingMode must be one of: ' + [...VALID_EMBEDDING_MODES].join(', ') +
1087
+ ' (or "undocumented") (got: ' + JSON.stringify(hi.embeddingMode) + ')',
1088
+ );
1089
+ }
1090
+
1091
+ // commandSurface
1092
+ if (hi.commandSurface === '__proto__' || hi.commandSurface === 'constructor' || hi.commandSurface === 'prototype') {
1093
+ errors.push('runtime.hostIntegration.commandSurface "' + hi.commandSurface + '" is a reserved name');
1094
+ } else if (hi.commandSurface !== 'undocumented' && !VALID_COMMAND_SURFACES.has(hi.commandSurface)) {
1095
+ errors.push(
1096
+ 'runtime.hostIntegration.commandSurface must be one of: ' + [...VALID_COMMAND_SURFACES].join(', ') +
1097
+ ' (or "undocumented") (got: ' + JSON.stringify(hi.commandSurface) + ')',
1098
+ );
1099
+ }
1100
+
1101
+ // modelMode
1102
+ if (hi.modelMode === '__proto__' || hi.modelMode === 'constructor' || hi.modelMode === 'prototype') {
1103
+ errors.push('runtime.hostIntegration.modelMode "' + hi.modelMode + '" is a reserved name');
1104
+ } else if (hi.modelMode !== 'undocumented' && !VALID_MODEL_MODES.has(hi.modelMode)) {
1105
+ errors.push(
1106
+ 'runtime.hostIntegration.modelMode must be one of: ' + [...VALID_MODEL_MODES].join(', ') +
1107
+ ' (or "undocumented") (got: ' + JSON.stringify(hi.modelMode) + ')',
1108
+ );
1109
+ }
1110
+
1111
+ // hookBus
1112
+ if (hi.hookBus === '__proto__' || hi.hookBus === 'constructor' || hi.hookBus === 'prototype') {
1113
+ errors.push('runtime.hostIntegration.hookBus "' + hi.hookBus + '" is a reserved name');
1114
+ } else if (hi.hookBus !== 'undocumented' && !VALID_HOOK_BUSES.has(hi.hookBus)) {
1115
+ errors.push(
1116
+ 'runtime.hostIntegration.hookBus must be one of: ' + [...VALID_HOOK_BUSES].join(', ') +
1117
+ ' (or "undocumented") (got: ' + JSON.stringify(hi.hookBus) + ')',
1118
+ );
1119
+ }
1120
+
1121
+ // stateIO
1122
+ if (hi.stateIO === '__proto__' || hi.stateIO === 'constructor' || hi.stateIO === 'prototype') {
1123
+ errors.push('runtime.hostIntegration.stateIO "' + hi.stateIO + '" is a reserved name');
1124
+ } else if (hi.stateIO !== 'undocumented' && !VALID_STATE_IO.has(hi.stateIO)) {
1125
+ errors.push(
1126
+ 'runtime.hostIntegration.stateIO must be one of: ' + [...VALID_STATE_IO].join(', ') +
1127
+ ' (or "undocumented") (got: ' + JSON.stringify(hi.stateIO) + ')',
1128
+ );
1129
+ }
1130
+
1131
+ // transport
1132
+ if (hi.transport === '__proto__' || hi.transport === 'constructor' || hi.transport === 'prototype') {
1133
+ errors.push('runtime.hostIntegration.transport "' + hi.transport + '" is a reserved name');
1134
+ } else if (hi.transport !== 'undocumented' && !VALID_TRANSPORTS.has(hi.transport)) {
1135
+ errors.push(
1136
+ 'runtime.hostIntegration.transport must be one of: ' + [...VALID_TRANSPORTS].join(', ') +
1137
+ ' (or "undocumented") (got: ' + JSON.stringify(hi.transport) + ')',
1138
+ );
1139
+ }
1140
+
1141
+ // runtime (axis)
1142
+ if (hi.runtime === '__proto__' || hi.runtime === 'constructor' || hi.runtime === 'prototype') {
1143
+ errors.push('runtime.hostIntegration.runtime "' + hi.runtime + '" is a reserved name');
1144
+ } else if (hi.runtime !== 'undocumented' && !VALID_HOST_RUNTIMES.has(hi.runtime)) {
1145
+ errors.push(
1146
+ 'runtime.hostIntegration.runtime must be one of: ' + [...VALID_HOST_RUNTIMES].join(', ') +
1147
+ ' (or "undocumented") (got: ' + JSON.stringify(hi.runtime) + ')',
1148
+ );
1149
+ }
1150
+
1151
+ // dispatch — required object
1152
+ if (typeof hi.dispatch !== 'object' || hi.dispatch === null || Array.isArray(hi.dispatch)) {
1153
+ errors.push('runtime.hostIntegration.dispatch must be an object');
1154
+ } else {
1155
+ const d = hi.dispatch;
1156
+
1157
+ // S2b: reserved-OWN-KEY guard on dispatch (CodeQL barrier — inline literal comparisons)
1158
+ if (Object.prototype.hasOwnProperty.call(d, '__proto__')) {
1159
+ errors.push('runtime.hostIntegration.dispatch must not contain reserved key "__proto__"');
1160
+ }
1161
+ if (Object.prototype.hasOwnProperty.call(d, 'constructor')) {
1162
+ errors.push('runtime.hostIntegration.dispatch must not contain reserved key "constructor"');
1163
+ }
1164
+ if (Object.prototype.hasOwnProperty.call(d, 'prototype')) {
1165
+ errors.push('runtime.hostIntegration.dispatch must not contain reserved key "prototype"');
1166
+ }
1167
+
1168
+ // namedDispatch — boolean or 'undocumented'
1169
+ if (typeof d.namedDispatch !== 'boolean' && d.namedDispatch !== 'undocumented') {
1170
+ errors.push(
1171
+ 'runtime.hostIntegration.dispatch.namedDispatch must be a boolean or "undocumented" (got: ' + JSON.stringify(d.namedDispatch) + ')',
1172
+ );
1173
+ }
1174
+
1175
+ // nested — boolean or 'undocumented'
1176
+ if (typeof d.nested !== 'boolean' && d.nested !== 'undocumented') {
1177
+ errors.push(
1178
+ 'runtime.hostIntegration.dispatch.nested must be a boolean or "undocumented" (got: ' + JSON.stringify(d.nested) + ')',
1179
+ );
1180
+ }
1181
+
1182
+ // background — boolean or 'undocumented'
1183
+ if (typeof d.background !== 'boolean' && d.background !== 'undocumented') {
1184
+ errors.push(
1185
+ 'runtime.hostIntegration.dispatch.background must be a boolean or "undocumented" (got: ' + JSON.stringify(d.background) + ')',
1186
+ );
1187
+ }
1188
+
1189
+ // subagentToolkit — closed enum or 'undocumented'
1190
+ if (d.subagentToolkit === '__proto__' || d.subagentToolkit === 'constructor' || d.subagentToolkit === 'prototype') {
1191
+ errors.push('runtime.hostIntegration.dispatch.subagentToolkit "' + d.subagentToolkit + '" is a reserved name');
1192
+ } else if (d.subagentToolkit !== 'undocumented' && !VALID_SUBAGENT_TOOLKITS.has(d.subagentToolkit)) {
1193
+ errors.push(
1194
+ 'runtime.hostIntegration.dispatch.subagentToolkit must be one of: ' + [...VALID_SUBAGENT_TOOLKITS].join(', ') +
1195
+ ' (or "undocumented") (got: ' + JSON.stringify(d.subagentToolkit) + ')',
1196
+ );
1197
+ }
1198
+
1199
+ // maxDepth — integer >= -1 or 'undocumented'
1200
+ if (d.maxDepth !== 'undocumented' && (!Number.isInteger(d.maxDepth) || d.maxDepth < -1)) {
1201
+ errors.push(
1202
+ 'runtime.hostIntegration.dispatch.maxDepth must be an integer >= -1 or "undocumented" (got: ' + JSON.stringify(d.maxDepth) + ')',
1203
+ );
1204
+ }
1205
+
1206
+ // backgroundDispatch — REQUIRED (all 16 runtime descriptors carry it, matching the sibling fields
1207
+ // namedDispatch/nested/background/subagentToolkit/maxDepth which are all required).
1208
+ if (!Object.prototype.hasOwnProperty.call(d, 'backgroundDispatch')) {
1209
+ errors.push(
1210
+ 'runtime.hostIntegration.dispatch.backgroundDispatch is required (must be a boolean or "undocumented")',
1211
+ );
1212
+ } else if (typeof d.backgroundDispatch !== 'boolean' && d.backgroundDispatch !== 'undocumented') {
1213
+ errors.push(
1214
+ 'runtime.hostIntegration.dispatch.backgroundDispatch must be a boolean or "undocumented" (got: ' + JSON.stringify(d.backgroundDispatch) + ')',
1215
+ );
1216
+ }
1217
+ }
1218
+ }
1219
+
1040
1220
  // GATE A: installSurface ↔ hooksSurface consistency (DEFECT.GENERATIVE-FIX)
1041
1221
  // Only check if both fields are valid strings (individual field validators above report type errors).
1042
1222
  if (typeof r.installSurface === 'string' && typeof r.hooksSurface === 'string') {
@@ -2052,6 +2232,24 @@ module.exports = {
2052
2232
  VALID_INSTALL_SURFACES,
2053
2233
  VALID_PERMISSION_WRITERS,
2054
2234
  VALID_EXTENDED_HOOK_EVENTS,
2235
+ VALID_EMBEDDING_MODES,
2236
+ VALID_COMMAND_SURFACES,
2237
+ VALID_MODEL_MODES,
2238
+ VALID_HOOK_BUSES,
2239
+ VALID_STATE_IO,
2240
+ VALID_TRANSPORTS,
2241
+ VALID_HOST_RUNTIMES,
2242
+ VALID_SUBAGENT_TOOLKITS,
2243
+ _HOST_INTEGRATION_VOCAB: {
2244
+ embeddingMode: [...VALID_EMBEDDING_MODES],
2245
+ commandSurface: [...VALID_COMMAND_SURFACES],
2246
+ modelMode: [...VALID_MODEL_MODES],
2247
+ hookBus: [...VALID_HOOK_BUSES],
2248
+ stateIO: [...VALID_STATE_IO],
2249
+ transport: [...VALID_TRANSPORTS],
2250
+ runtime: [...VALID_HOST_RUNTIMES],
2251
+ subagentToolkit: [...VALID_SUBAGENT_TOOLKITS],
2252
+ },
2055
2253
  INSTALL_SURFACE_TO_ALLOWED_HOOKS_SURFACES,
2056
2254
  GEMINI_AGENT_EVENTS,
2057
2255
  CLAUDE_FAMILY_EVENTS,
@@ -0,0 +1,44 @@
1
+ 'use strict';
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.checkCliSkew = checkCliSkew;
7
+ /**
8
+ * cli-skew-check.cts — CLI version-skew detection (#1754).
9
+ *
10
+ * Pure function: compares the resolved gsd-tools.cjs path to the project root.
11
+ * If the resolved CLI is OUTSIDE the project root while a project-local install
12
+ * EXISTS, returns a warning string (the caller writes it to stderr). Non-blocking.
13
+ *
14
+ * Catches the shadowing scenario from #1748: a stale global canary CLI (e.g.
15
+ * from the retired @gsd-build/sdk) shadowing the project-local GSD install.
16
+ *
17
+ * The function is PURE (no I/O) — the caller provides the resolved path, the
18
+ * project root, and whether a project-local install exists. This makes it
19
+ * trivially testable without filesystem setup.
20
+ */
21
+ const node_path_1 = __importDefault(require("node:path"));
22
+ /**
23
+ * Check for CLI version skew.
24
+ *
25
+ * @param opts.resolvedPath - The absolute path of the running gsd-tools.cjs (__filename).
26
+ * @param opts.projectRoot - The project root (from findProjectRoot), or null if no project.
27
+ * @param opts.projectLocalExists - Whether a project-local gsd-tools.cjs exists.
28
+ * @returns A warning string if skew is detected, or null if no skew.
29
+ */
30
+ function checkCliSkew(opts) {
31
+ const { resolvedPath, projectRoot, projectLocalExists } = opts;
32
+ // No project context or no project-local install → no skew possible.
33
+ if (!projectRoot || !projectLocalExists)
34
+ return null;
35
+ // If the resolved CLI is under the project root, it IS a project-local install.
36
+ const rel = node_path_1.default.relative(projectRoot, resolvedPath);
37
+ if (!rel.startsWith('..'))
38
+ return null;
39
+ // Resolved CLI is outside project root while a project-local install exists → SKEW.
40
+ const hint = resolvedPath.includes('@gsd-build')
41
+ ? ' If @gsd-build/sdk: npm uninstall -g @gsd-build/sdk'
42
+ : '';
43
+ return `⚠ GSD: ${resolvedPath} may shadow project-local GSD.${hint}`;
44
+ }
@@ -7,7 +7,7 @@
7
7
  * from the prior hand-written .cjs; only types are added.
8
8
  */
9
9
  Object.defineProperty(exports, "__esModule", { value: true });
10
- exports.ROADMAP_SUBCOMMANDS = exports.VALIDATE_SUBCOMMANDS = exports.PHASES_SUBCOMMANDS = exports.PHASE_SUBCOMMANDS = exports.INIT_SUBCOMMANDS = exports.VERIFY_SUBCOMMANDS = exports.STATE_SUBCOMMANDS = exports.NON_FAMILY_COMMAND_ALIASES = exports.ROADMAP_COMMAND_ALIASES = exports.VALIDATE_COMMAND_ALIASES = exports.PHASES_COMMAND_ALIASES = exports.PHASE_COMMAND_ALIASES = exports.INIT_COMMAND_ALIASES = exports.VERIFY_COMMAND_ALIASES = exports.STATE_COMMAND_ALIASES = void 0;
10
+ exports.EVAL_SUBCOMMANDS = exports.EVAL_COMMAND_ALIASES = exports.ROADMAP_SUBCOMMANDS = exports.VALIDATE_SUBCOMMANDS = exports.PHASES_SUBCOMMANDS = exports.PHASE_SUBCOMMANDS = exports.INIT_SUBCOMMANDS = exports.VERIFY_SUBCOMMANDS = exports.STATE_SUBCOMMANDS = exports.NON_FAMILY_COMMAND_ALIASES = exports.ROADMAP_COMMAND_ALIASES = exports.VALIDATE_COMMAND_ALIASES = exports.PHASES_COMMAND_ALIASES = exports.PHASE_COMMAND_ALIASES = exports.INIT_COMMAND_ALIASES = exports.VERIFY_COMMAND_ALIASES = exports.STATE_COMMAND_ALIASES = void 0;
11
11
  exports.STATE_COMMAND_ALIASES = [
12
12
  {
13
13
  "canonical": "state.load",
@@ -159,6 +159,14 @@ exports.STATE_COMMAND_ALIASES = [
159
159
  "subcommand": "prune",
160
160
  "mutation": true
161
161
  },
162
+ {
163
+ "canonical": "state.rebuild",
164
+ "aliases": [
165
+ "state rebuild"
166
+ ],
167
+ "subcommand": "rebuild",
168
+ "mutation": true
169
+ },
162
170
  {
163
171
  "canonical": "state.milestone-switch",
164
172
  "aliases": [
@@ -815,3 +823,12 @@ exports.PHASE_SUBCOMMANDS = exports.PHASE_COMMAND_ALIASES.map((entry) => entry.s
815
823
  exports.PHASES_SUBCOMMANDS = exports.PHASES_COMMAND_ALIASES.map((entry) => entry.subcommand);
816
824
  exports.VALIDATE_SUBCOMMANDS = exports.VALIDATE_COMMAND_ALIASES.map((entry) => entry.subcommand);
817
825
  exports.ROADMAP_SUBCOMMANDS = exports.ROADMAP_COMMAND_ALIASES.map((entry) => entry.subcommand);
826
+ exports.EVAL_COMMAND_ALIASES = [
827
+ {
828
+ "canonical": "eval.score",
829
+ "aliases": ["eval score"],
830
+ "subcommand": "score",
831
+ "mutation": false
832
+ }
833
+ ];
834
+ exports.EVAL_SUBCOMMANDS = exports.EVAL_COMMAND_ALIASES.map((entry) => entry.subcommand);
@@ -637,6 +637,33 @@ function cmdConfigSet(cwd, keyPath, value, raw) {
637
637
  }
638
638
  parsedValue = normalized.values;
639
639
  }
640
+ // #1517: validate review.reviewer_instances.<name>.<field> leaves at the
641
+ // invocation boundary (Postel/Kerckhoffs — strict at accept). The config
642
+ // schema dynamic pattern admits the path; this block validates the name + the
643
+ // field value so a misconfigured instance is rejected at config-set time, not
644
+ // silently at review time. Single-source validators live in
645
+ // review-reviewer-selection.cjs (INSTANCE_NAME_PATTERN, KNOWN_REVIEWER_SLUGS).
646
+ const instanceLeaf = kp.match(/^review\.reviewer_instances\.([a-zA-Z0-9_-]+)\.(cli|model|agent)$/);
647
+ if (instanceLeaf) {
648
+ const [, instanceName, field] = instanceLeaf;
649
+ if (!review_reviewer_selection_cjs_1.INSTANCE_NAME_PATTERN.test(instanceName)) {
650
+ error(`Invalid reviewer instance name '${instanceName}'. Must match ^[a-z0-9][a-z0-9-]*$.`);
651
+ }
652
+ if (review_reviewer_selection_cjs_1.KNOWN_REVIEWER_SLUGS.includes(instanceName)) {
653
+ error(`Reviewer instance name '${instanceName}' must not equal a built-in reviewer slug.`);
654
+ }
655
+ if (field === 'cli') {
656
+ if (typeof parsedValue !== 'string' || !review_reviewer_selection_cjs_1.KNOWN_REVIEWER_SLUGS.includes(parsedValue)) {
657
+ error(`Invalid reviewer_instances.${instanceName}.cli '${val}'. Must be a known reviewer adapter: ${review_reviewer_selection_cjs_1.KNOWN_REVIEWER_SLUGS.join(', ')}.`);
658
+ }
659
+ }
660
+ else {
661
+ // model | agent — opaque pass-through strings (never interpolated into shell).
662
+ if (typeof parsedValue !== 'string') {
663
+ error(`Invalid reviewer_instances.${instanceName}.${field} '${val}'. Must be a string.`);
664
+ }
665
+ }
666
+ }
640
667
  const setConfigValueResult = setConfigValue(cwd, kp, parsedValue);
641
668
  // Mask secrets in both JSON and text output. The plaintext is written
642
669
  // to config.json (that's where secrets live on disk); the CLI output
@@ -42,6 +42,18 @@ const bulletColonRe = /^\s*-\s+\*\*D-([A-Za-z0-9][A-Za-z0-9_-]*)(?:\s*\[([^\]]+)
42
42
  * Accepts both U+2014 em-dash (—) and U+2013 en-dash (–) for robustness.
43
43
  */
44
44
  const bulletEmDashRe = /^\s*-\s+\*\*D-([A-Za-z0-9][A-Za-z0-9_-]*)(?:\s*\[([^\]]+)\])?[^*]*[—–][^*]*\*\*\s*(.*)$/;
45
+ /**
46
+ * Titled-colon form: `- **D-NN[ [tags]]: Title.** body`
47
+ * A title sits between the colon and the closing `**` (so the `:**` anchor of
48
+ * bulletColonRe fails, and there is no em-dash for bulletEmDashRe). This is a strict
49
+ * superset of the colon-immediate form, so it MUST be checked AFTER bulletColonRe and
50
+ * bulletEmDashRe — it only catches bullets those two miss. The title run is `[^:*]*` (no
51
+ * colon, no `*`) so a genuinely-malformed bullet with a colon in the pre-separator run
52
+ * (e.g. `D-07 ratio 3:1:**`) still fails the anchor and falls through to the parse-miss
53
+ * guard — matching bulletColonRe's `[^:*]*` discipline that the separator colon is the
54
+ * only colon permitted before `**`. (#1639)
55
+ */
56
+ const bulletTitledColonRe = /^\s*-\s+\*\*D-([A-Za-z0-9][A-Za-z0-9_-]*)(?:\s*\[([^\]]+)\])?[^:*]*:[^:*]*\*\*\s*(.*)$/;
45
57
  /**
46
58
  * Parse decision lines from a block of text (the inner text of a <decisions>
47
59
  * or markdown-header section body). Returns the extracted decisions and a count
@@ -110,6 +122,21 @@ function parseDecisionLines(block) {
110
122
  current = { id, text: emDashMatch[3] || '', category, tags, trackable };
111
123
  continue;
112
124
  }
125
+ // Titled-colon form: `- **D-NN[ [tags]]: Title.** body` (#1639). Checked LAST — it is
126
+ // a strict superset of bulletColonRe, so it only catches bullets the colon-immediate
127
+ // and em-dash forms missed (minimal blast radius). id + [tags] trackability honored;
128
+ // the body after the closing bold run is reported as text.
129
+ const titledColonMatch = line.match(bulletTitledColonRe);
130
+ if (titledColonMatch) {
131
+ flush();
132
+ const id = `D-${titledColonMatch[1]}`;
133
+ const tags = titledColonMatch[2]
134
+ ? titledColonMatch[2].split(',').map((t) => t.trim().toLowerCase()).filter(Boolean)
135
+ : [];
136
+ const trackable = !inDiscretion && !tags.some((t) => NON_TRACKABLE_TAGS.has(t));
137
+ current = { id, text: titledColonMatch[3] || '', category, tags, trackable };
138
+ continue;
139
+ }
113
140
  // Parse-miss guard (FIX B + #1343): a line that looks like a `D-NN` decision
114
141
  // bullet but failed both patterns — flush, warn, and record the miss.
115
142
  // parseMisses > 0 forces could-not-parse even when other decisions parsed.
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Embedding adapter contract — the common `HostIntegrationInterface` both
3
+ * embedding adapters satisfy (ADR-1239 Phase C-1, #1680).
4
+ *
5
+ * INTENTIONALLY MINIMAL (Phase 3 slice 1). The full six-interface-point binding
6
+ * surface (command / dispatch / model / hooks / state / artifact) is DEFERRED
7
+ * until the imperative adapter (AC2) provides a real consumer that fixes the
8
+ * shape — ADR-1239 lists the wire-shape as an open question:
9
+ * "Exact wire-shape of the initialize handshake … Where precisely to cut the
10
+ * engine↔host boundary"
11
+ * (docs/adr/1239-gsd-embeddable-orchestration-engine.md#open-questions-narrowed-by-the-research).
12
+ * Freezing a 6-point contract before the imperative adapter exists would risk
13
+ * rework across Phases 3-6. This slice ships only what the declarative adapter
14
+ * (AC1) needs: the kind discriminator + runtime + install/uninstall entry.
15
+ *
16
+ * Both adapters bind the SAME engine (install-engine.cjs / the loop resolver);
17
+ * they differ in HOW — declarative projects files (lossy: drops loop
18
+ * orchestration), imperative drives host primitives in-process. See ADR-1239
19
+ * "How a capability reaches a host (two adapters, one engine)".
20
+ */
21
+ 'use strict';
22
+ Object.defineProperty(exports, "__esModule", { value: true });
23
+ exports.ADAPTER_KINDS = void 0;
24
+ // ---------------------------------------------------------------------------
25
+ // Kinds
26
+ // ---------------------------------------------------------------------------
27
+ exports.ADAPTER_KINDS = Object.freeze(['declarative', 'imperative']);
@@ -0,0 +1,21 @@
1
+ "use strict";
2
+ /**
3
+ * Manifest-backed eval subcommand router (#10).
4
+ */
5
+ const command_aliases_cjs_1 = require("./command-aliases.cjs");
6
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
7
+ const cjsCommandRouterAdapter = require("./cjs-command-router-adapter.cjs");
8
+ const { routeCjsCommandFamily } = cjsCommandRouterAdapter;
9
+ function routeEvalCommand({ evalMod, args, cwd, raw, error }) {
10
+ routeCjsCommandFamily({
11
+ args,
12
+ subcommands: command_aliases_cjs_1.EVAL_SUBCOMMANDS,
13
+ unsupported: {},
14
+ error,
15
+ unknownMessage: (_s, available) => `Unknown eval subcommand. Available: ${available.join(', ')}`,
16
+ handlers: {
17
+ score: () => evalMod.cmdEvalScore(cwd, args, raw),
18
+ },
19
+ });
20
+ }
21
+ module.exports = { routeEvalCommand };
@@ -0,0 +1,60 @@
1
+ "use strict";
2
+ /**
3
+ * Deterministic eval scoring verb (#10).
4
+ * Moves coverage/infra/overall arithmetic out of the gsd-eval-auditor prompt
5
+ * into code, per the framework's code-delegation discipline.
6
+ */
7
+ function parseFlag(args, flag) {
8
+ const i = args.indexOf(flag);
9
+ return i >= 0 && i + 1 < args.length ? args[i + 1] : undefined;
10
+ }
11
+ const INFRA_VALUE = { ok: 1, partial: 0.5, missing: 0 };
12
+ const INFRA_TOKENS = new Set(Object.keys(INFRA_VALUE));
13
+ function computeEvalScore(covered, total, infra) {
14
+ const coverage = total > 0 ? (covered / total) * 100 : 0;
15
+ // unknown/typo tokens are treated as `missing` (score 0) by design — upstream agent only passes ok|partial|missing
16
+ const infraSum = infra.reduce((acc, s) => acc + (INFRA_VALUE[s.trim().toLowerCase()] ?? 0), 0);
17
+ const infraScore = (infraSum / 5) * 100;
18
+ const overall = coverage * 0.6 + infraScore * 0.4;
19
+ const round = (n) => Math.round(n * 100) / 100;
20
+ const o = round(overall);
21
+ const verdict = o >= 80 ? 'PRODUCTION READY' :
22
+ o >= 60 ? 'NEEDS WORK' :
23
+ o >= 40 ? 'SIGNIFICANT GAPS' : 'NOT IMPLEMENTED';
24
+ return { coverage_score: round(coverage), infra_score: round(infraScore), overall_score: o, verdict };
25
+ }
26
+ function cmdEvalScore(_cwd, args, raw) {
27
+ const coveredRaw = parseFlag(args, '--covered');
28
+ const totalRaw = parseFlag(args, '--total');
29
+ const infraRaw = parseFlag(args, '--infra') || '';
30
+ const infra = infraRaw ? infraRaw.split(',').map((s) => s.trim().toLowerCase()) : [];
31
+ const covered = Number(coveredRaw);
32
+ const total = Number(totalRaw);
33
+ if (coveredRaw === undefined || coveredRaw.trim() === '' ||
34
+ totalRaw === undefined || totalRaw.trim() === '' ||
35
+ !Number.isFinite(covered) || !Number.isFinite(total) ||
36
+ infra.length !== 5) {
37
+ process.stderr.write('Usage: gsd-tools query eval.score --covered N --total N --infra a,b,c,d,e (each ok|partial|missing)\n');
38
+ process.exitCode = 1;
39
+ return;
40
+ }
41
+ // Domain validation: this is a public CLI verb, so reject out-of-domain inputs
42
+ // rather than emit nonsense (covered>total -> coverage_score>100; negatives ->
43
+ // negative scores). Counts must be non-negative integers and covered cannot
44
+ // exceed total; infra tokens must match the documented ok|partial|missing set.
45
+ if (!Number.isInteger(covered) || !Number.isInteger(total) || covered < 0 || total < 0 || covered > total) {
46
+ process.stderr.write('Invalid eval.score domain: require integer counts with 0 <= covered <= total.\n');
47
+ process.exitCode = 1;
48
+ return;
49
+ }
50
+ const invalidInfra = infra.find((s) => !INFRA_TOKENS.has(s));
51
+ if (invalidInfra !== undefined) {
52
+ process.stderr.write(`Invalid eval.score infra token: ${invalidInfra || '<empty>'}. Expected ok|partial|missing.\n`);
53
+ process.exitCode = 1;
54
+ return;
55
+ }
56
+ const result = computeEvalScore(covered, total, infra);
57
+ process.stdout.write(raw ? JSON.stringify(result) : JSON.stringify(result, null, 2));
58
+ process.stdout.write('\n');
59
+ }
60
+ module.exports = { cmdEvalScore, computeEvalScore };
@@ -0,0 +1,70 @@
1
+ /**
2
+ * External-descriptor trust gate (ADR-1239 Phase C-2, #1681).
3
+ *
4
+ * Load-time `configHome` write-confinement for installed third-party host-plugin
5
+ * descriptors. The opt-in loader (`loadRegistry({includeInstalled:true})`) already
6
+ * applies schema validation + consent + first-party-wins + fail-closed gates;
7
+ * this adds defense-in-depth: **before** a third-party descriptor's install plan
8
+ * is ever executed, assert every destSubpath it declares resolves within the
9
+ * user-approved `configHome`. A path-escaping or malformed descriptor is
10
+ * rejected fail-closed.
11
+ *
12
+ * This is the load-time twin of Phase 2's install-time gate
13
+ * (`assertDestWithinConfigHome` in runtime-artifact-install-plan.cts, #1679 AC3).
14
+ * The two are defense-in-depth: load-time rejects malformed descriptors early
15
+ * (before consent even matters); install-time bounds the actual writes.
16
+ *
17
+ * Do NOT conflate with ADR-1577's prompt-injection circuit-breaker — separate
18
+ * concern sharing the word "trust".
19
+ */
20
+ 'use strict';
21
+ var __importDefault = (this && this.__importDefault) || function (mod) {
22
+ return (mod && mod.__esModule) ? mod : { "default": mod };
23
+ };
24
+ Object.defineProperty(exports, "__esModule", { value: true });
25
+ exports.isPathConfined = isPathConfined;
26
+ exports.assertDescriptorConfined = assertDescriptorConfined;
27
+ const node_path_1 = __importDefault(require("node:path"));
28
+ /**
29
+ * Pure path-containment check (cross-platform). `target` is confined to `root`
30
+ * iff resolving it relative to `root` yields a path equal to or under `root`.
31
+ * Absolute paths outside `root` and `..`-escapes return false.
32
+ */
33
+ function isPathConfined(target, root) {
34
+ if (typeof target !== 'string' || typeof root !== 'string' || target.length === 0 || root.length === 0) {
35
+ return false;
36
+ }
37
+ const rootResolved = node_path_1.default.resolve(root);
38
+ const targetResolved = node_path_1.default.resolve(root, target);
39
+ const prefix = rootResolved + node_path_1.default.sep;
40
+ return targetResolved === rootResolved || targetResolved.startsWith(prefix);
41
+ }
42
+ /**
43
+ * Assert every destSubpath the descriptor declares (global + local artifact
44
+ * layout) resolves within `configHome`. Throws fail-closed naming the offending
45
+ * descriptor + path on the first escape. A descriptor with no artifact layout
46
+ * passes (nothing to confine).
47
+ */
48
+ function assertDescriptorConfined(descriptor, configHome) {
49
+ if (!descriptor || typeof descriptor !== 'object')
50
+ return;
51
+ const id = typeof descriptor.id === 'string' ? descriptor.id : '<unknown>';
52
+ const layout = descriptor.runtime?.artifactLayout;
53
+ if (!layout || typeof layout !== 'object')
54
+ return;
55
+ const check = (scope, kinds) => {
56
+ if (!Array.isArray(kinds))
57
+ return;
58
+ for (const kind of kinds) {
59
+ const dest = kind?.destSubpath;
60
+ if (typeof dest !== 'string' || dest.length === 0)
61
+ continue;
62
+ if (!isPathConfined(dest, configHome)) {
63
+ throw new Error(`external-descriptor-trust: descriptor '${id}' declares an unconfined ${scope} destSubpath ` +
64
+ `${JSON.stringify(dest)} (resolves outside configHome ${JSON.stringify(configHome)}) — rejected fail-closed.`);
65
+ }
66
+ }
67
+ };
68
+ check('global', layout.global);
69
+ check('local', layout.local);
70
+ }