@opengsd/gsd-core 1.6.0 → 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 (68) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/agents/gsd-verifier.md +1 -0
  3. package/bin/gsd-mcp-server.js +31 -0
  4. package/bin/install.js +293 -1145
  5. package/commands/gsd/review.md +6 -0
  6. package/gemini-extension.json +1 -1
  7. package/gsd-core/bin/gsd-tools.cjs +116 -1
  8. package/gsd-core/bin/lib/adapter-declarative.cjs +35 -0
  9. package/gsd-core/bin/lib/adapter-imperative.cjs +52 -0
  10. package/gsd-core/bin/lib/assumption-delta.cjs +231 -0
  11. package/gsd-core/bin/lib/capability-lifecycle.cjs +7 -7
  12. package/gsd-core/bin/lib/capability-loader.cjs +18 -0
  13. package/gsd-core/bin/lib/capability-lock.cjs +2 -2
  14. package/gsd-core/bin/lib/capability-registry.cjs +889 -82
  15. package/gsd-core/bin/lib/capability-source.cjs +4 -4
  16. package/gsd-core/bin/lib/capability-validator.cjs +198 -0
  17. package/gsd-core/bin/lib/cli-skew-check.cjs +44 -0
  18. package/gsd-core/bin/lib/command-aliases.cjs +8 -0
  19. package/gsd-core/bin/lib/config.cjs +27 -0
  20. package/gsd-core/bin/lib/embedding-adapter.cjs +27 -0
  21. package/gsd-core/bin/lib/external-descriptor-trust.cjs +70 -0
  22. package/gsd-core/bin/lib/hook-bus.cjs +81 -0
  23. package/gsd-core/bin/lib/host-integration.cjs +408 -0
  24. package/gsd-core/bin/lib/init.cjs +1 -1
  25. package/gsd-core/bin/lib/install-engine.cjs +755 -0
  26. package/gsd-core/bin/lib/install-profiles.cjs +35 -4
  27. package/gsd-core/bin/lib/installer-migrations.cjs +1 -1
  28. package/gsd-core/bin/lib/mcp-server.cjs +194 -0
  29. package/gsd-core/bin/lib/milestone.cjs +27 -30
  30. package/gsd-core/bin/lib/model-adapter.cjs +50 -0
  31. package/gsd-core/bin/lib/phase.cjs +41 -72
  32. package/gsd-core/bin/lib/planning-workspace.cjs +1 -1
  33. package/gsd-core/bin/lib/probe-core.cjs +91 -1
  34. package/gsd-core/bin/lib/review-reviewer-selection.cjs +129 -13
  35. package/gsd-core/bin/lib/roadmap-upgrade.cjs +3 -2
  36. package/gsd-core/bin/lib/roadmap.cjs +17 -3
  37. package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +65 -9
  38. package/gsd-core/bin/lib/runtime-artifact-install-plan.cjs +54 -4
  39. package/gsd-core/bin/lib/runtime-artifact-layout.cjs +5 -2
  40. package/gsd-core/bin/lib/runtime-hooks-surface.cjs +1 -1
  41. package/gsd-core/bin/lib/runtime-name-policy.cjs +160 -30
  42. package/gsd-core/bin/lib/shell-command-projection.cjs +37 -1
  43. package/gsd-core/bin/lib/stale-bake-guard.cjs +254 -0
  44. package/gsd-core/bin/lib/state-command-router.cjs +4 -0
  45. package/gsd-core/bin/lib/state-io.cjs +55 -0
  46. package/gsd-core/bin/lib/state-transition.cjs +1588 -0
  47. package/gsd-core/bin/lib/state.cjs +306 -681
  48. package/gsd-core/bin/lib/surface.cjs +4 -1
  49. package/gsd-core/bin/lib/workstream.cjs +4 -4
  50. package/gsd-core/bin/shared/config-schema.manifest.json +9 -0
  51. package/gsd-core/references/honest-verifier.md +105 -0
  52. package/gsd-core/references/reviewer-instances.md +99 -0
  53. package/gsd-core/workflows/autonomous.md +9 -9
  54. package/gsd-core/workflows/manager.md +15 -15
  55. package/gsd-core/workflows/plan-phase.md +1 -1
  56. package/gsd-core/workflows/review.md +26 -0
  57. package/gsd-core/workflows/thread.md +4 -4
  58. package/gsd-core/workflows/verify-phase.md +11 -4
  59. package/hooks/dist/gsd-graphify-update.sh +7 -1
  60. package/hooks/gsd-graphify-update.sh +7 -1
  61. package/package.json +4 -4
  62. package/scripts/ci-test-scope.cjs +38 -9
  63. package/scripts/lint-allow-test-rule-refs.allowlist.json +0 -1
  64. package/scripts/lint-regression-test-names.allowlist.json +3 -0
  65. package/scripts/lint-test-file-count.allowlist.json +19 -5
  66. package/scripts/mutation-matrix.cjs +45 -3
  67. package/scripts/prompt-injection-scan.sh +8 -0
  68. 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
+ }
@@ -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": [
@@ -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
@@ -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,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
+ }
@@ -0,0 +1,81 @@
1
+ /**
2
+ * Hook-bus seam (ADR-1239 Phase C-1, AC4 / #1680).
3
+ *
4
+ * The lifecycle-hook ownership model, selected by the negotiated `hookBus`
5
+ * axis (host-integration.cts):
6
+ *
7
+ * - `engine` — GSD owns the bus internally (in-process pub/sub). Used by
8
+ * hosts that have no event bus (VS Code). Full subscribe + emit.
9
+ * - `host` — the host fires events; GSD subscribes. Handlers register
10
+ * locally for a Phase-5 host binding to dispatch to; `emit` delegates to a
11
+ * host-supplied emitter (fail-closed until bound — GSD does not drive a
12
+ * host-owned bus).
13
+ * - `none` — no bus (Cline-rules). Degrades to rule-text instructions;
14
+ * subscribe/emit are no-ops.
15
+ *
16
+ * Portable event floor — the "claude dialect" all hook-capable hosts share
17
+ * (sourced from src/runtime-hooks-surface.cts). Extended events are negotiated
18
+ * per-host (Phase 5).
19
+ *
20
+ * Minimal seam (per ADR-1239 open wire-shape question): the host-side dispatch
21
+ * wiring lands in Phase 5 (#1682). This slice ships the three ownership modes
22
+ * + the engine pub-sub + the fail-closed contract.
23
+ */
24
+ 'use strict';
25
+ Object.defineProperty(exports, "__esModule", { value: true });
26
+ exports.PORTABLE_EVENT_FLOOR = void 0;
27
+ exports.createHookBus = createHookBus;
28
+ exports.PORTABLE_EVENT_FLOOR = Object.freeze(['SessionStart', 'PreToolUse', 'PostToolUse', 'Stop', 'SessionEnd']);
29
+ function createHookBus({ bus }, options = {}) {
30
+ if (bus !== 'host' && bus !== 'engine' && bus !== 'none') {
31
+ throw new TypeError(`createHookBus: bus must be 'host' | 'engine' | 'none' (got ${JSON.stringify(bus)})`);
32
+ }
33
+ if (bus === 'none') {
34
+ return Object.freeze({
35
+ bus,
36
+ subscribe() { },
37
+ emit() { },
38
+ });
39
+ }
40
+ if (bus === 'engine') {
41
+ const subs = new Map();
42
+ return Object.freeze({
43
+ bus: 'engine',
44
+ subscribe(event, handler) {
45
+ const list = subs.get(event);
46
+ if (list)
47
+ list.push(handler);
48
+ else
49
+ subs.set(event, [handler]);
50
+ },
51
+ emit(event, payload) {
52
+ const list = subs.get(event);
53
+ if (!list)
54
+ return;
55
+ for (const h of list) {
56
+ // Handler errors are isolated — one throwing handler must not break the bus.
57
+ try {
58
+ h(payload);
59
+ }
60
+ catch { /* swallow; bus stays up */ }
61
+ }
62
+ },
63
+ });
64
+ }
65
+ // host: GSD subscribes; emits go to the host-supplied emitter (fail-closed until bound).
66
+ const hostEmit = options.hostEmit;
67
+ return Object.freeze({
68
+ bus: 'host',
69
+ subscribe(_event, _handler) {
70
+ // Host owns the bus; GSD's subscriptions are dispatched by a Phase-5 host
71
+ // binding that calls the registered handlers when the host fires events.
72
+ // Stored host-side; locally this is a seam until that binding lands.
73
+ },
74
+ emit(event, payload) {
75
+ if (typeof hostEmit !== 'function') {
76
+ throw new Error("host hook-bus emit: no host emitter bound — the 'host' bus requires a hostEmit primitive (Phase 5 wires the concrete host).");
77
+ }
78
+ hostEmit(event, payload);
79
+ },
80
+ });
81
+ }