@opengsd/gsd-core 1.6.1 → 1.7.0-rc.2

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 (119) hide show
  1. package/.claude-plugin/marketplace.json +20 -0
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/.opencode/plugins/gsd-core.js +711 -0
  4. package/agents/gsd-advisor-researcher.md +2 -0
  5. package/agents/gsd-ai-researcher.md +1 -1
  6. package/agents/gsd-assumptions-analyzer.md +2 -0
  7. package/agents/gsd-code-fixer.md +2 -0
  8. package/agents/gsd-code-reviewer.md +2 -0
  9. package/agents/gsd-codebase-mapper.md +2 -0
  10. package/agents/gsd-debugger.md +2 -0
  11. package/agents/gsd-doc-writer.md +2 -0
  12. package/agents/gsd-eval-auditor.md +2 -0
  13. package/agents/gsd-executor.md +9 -6
  14. package/agents/gsd-integration-checker.md +2 -0
  15. package/agents/gsd-nyquist-auditor.md +2 -0
  16. package/agents/gsd-phase-researcher.md +2 -0
  17. package/agents/gsd-plan-checker.md +2 -0
  18. package/agents/gsd-planner.md +2 -0
  19. package/agents/gsd-project-researcher.md +2 -0
  20. package/agents/gsd-research-synthesizer.md +2 -0
  21. package/agents/gsd-roadmapper.md +2 -0
  22. package/agents/gsd-security-auditor.md +2 -0
  23. package/agents/gsd-ui-auditor.md +2 -0
  24. package/agents/gsd-ui-checker.md +2 -0
  25. package/agents/gsd-ui-researcher.md +2 -0
  26. package/agents/gsd-verifier.md +5 -2
  27. package/bin/gsd-mcp-server.js +31 -0
  28. package/bin/install.js +411 -1146
  29. package/commands/gsd/review.md +6 -0
  30. package/gemini-extension.json +1 -1
  31. package/gsd-core/bin/gsd-tools.cjs +134 -8
  32. package/gsd-core/bin/lib/adapter-declarative.cjs +35 -0
  33. package/gsd-core/bin/lib/adapter-imperative.cjs +52 -0
  34. package/gsd-core/bin/lib/assumption-delta.cjs +231 -0
  35. package/gsd-core/bin/lib/capability-lifecycle.cjs +7 -7
  36. package/gsd-core/bin/lib/capability-loader.cjs +45 -9
  37. package/gsd-core/bin/lib/capability-lock.cjs +2 -2
  38. package/gsd-core/bin/lib/capability-registry.cjs +891 -82
  39. package/gsd-core/bin/lib/capability-source.cjs +26 -11
  40. package/gsd-core/bin/lib/capability-validator.cjs +222 -2
  41. package/gsd-core/bin/lib/cli-skew-check.cjs +44 -0
  42. package/gsd-core/bin/lib/command-aliases.cjs +8 -0
  43. package/gsd-core/bin/lib/commands.cjs +2 -1
  44. package/gsd-core/bin/lib/config.cjs +27 -0
  45. package/gsd-core/bin/lib/embedding-adapter.cjs +27 -0
  46. package/gsd-core/bin/lib/external-descriptor-trust.cjs +70 -0
  47. package/gsd-core/bin/lib/frontmatter.cjs +53 -6
  48. package/gsd-core/bin/lib/handshake-serialized.cjs +70 -0
  49. package/gsd-core/bin/lib/hook-bus.cjs +81 -0
  50. package/gsd-core/bin/lib/host-integration-sdk.cjs +53 -0
  51. package/gsd-core/bin/lib/host-integration.cjs +469 -0
  52. package/gsd-core/bin/lib/init.cjs +35 -7
  53. package/gsd-core/bin/lib/install-engine.cjs +755 -0
  54. package/gsd-core/bin/lib/install-profiles.cjs +35 -4
  55. package/gsd-core/bin/lib/installer-migrations.cjs +1 -1
  56. package/gsd-core/bin/lib/mcp-server.cjs +194 -0
  57. package/gsd-core/bin/lib/milestone.cjs +68 -40
  58. package/gsd-core/bin/lib/model-adapter.cjs +50 -0
  59. package/gsd-core/bin/lib/phase-id.cjs +18 -0
  60. package/gsd-core/bin/lib/phase.cjs +57 -90
  61. package/gsd-core/bin/lib/phases-command-router.cjs +4 -3
  62. package/gsd-core/bin/lib/planning-workspace.cjs +1 -1
  63. package/gsd-core/bin/lib/probe-core.cjs +132 -2
  64. package/gsd-core/bin/lib/review-reviewer-selection.cjs +129 -13
  65. package/gsd-core/bin/lib/roadmap-command-router.cjs +3 -2
  66. package/gsd-core/bin/lib/roadmap-parser.cjs +21 -11
  67. package/gsd-core/bin/lib/roadmap-upgrade.cjs +3 -2
  68. package/gsd-core/bin/lib/roadmap.cjs +33 -22
  69. package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +65 -9
  70. package/gsd-core/bin/lib/runtime-artifact-install-plan.cjs +54 -4
  71. package/gsd-core/bin/lib/runtime-artifact-layout.cjs +5 -2
  72. package/gsd-core/bin/lib/runtime-hooks-surface.cjs +1 -1
  73. package/gsd-core/bin/lib/runtime-name-policy.cjs +160 -30
  74. package/gsd-core/bin/lib/shell-command-projection.cjs +16 -0
  75. package/gsd-core/bin/lib/stale-bake-guard.cjs +254 -0
  76. package/gsd-core/bin/lib/state-command-router.cjs +4 -0
  77. package/gsd-core/bin/lib/state-io.cjs +55 -0
  78. package/gsd-core/bin/lib/state-transition.cjs +1603 -0
  79. package/gsd-core/bin/lib/state.cjs +327 -683
  80. package/gsd-core/bin/lib/surface.cjs +4 -1
  81. package/gsd-core/bin/lib/validate.cjs +2 -1
  82. package/gsd-core/bin/lib/verify.cjs +6 -4
  83. package/gsd-core/bin/lib/workstream-inventory-builder.cjs +12 -2
  84. package/gsd-core/bin/lib/workstream-inventory.cjs +28 -0
  85. package/gsd-core/bin/lib/workstream.cjs +4 -4
  86. package/gsd-core/bin/shared/config-schema.manifest.json +9 -0
  87. package/gsd-core/references/agent-skills-bootstrap.md +60 -0
  88. package/gsd-core/references/honest-verifier.md +105 -0
  89. package/gsd-core/references/model-profiles.md +27 -0
  90. package/gsd-core/references/reviewer-instances.md +99 -0
  91. package/gsd-core/workflows/autonomous.md +30 -32
  92. package/gsd-core/workflows/complete-milestone.md +6 -10
  93. package/gsd-core/workflows/execute-phase.md +1 -1
  94. package/gsd-core/workflows/forensics.md +3 -3
  95. package/gsd-core/workflows/help/modes/full.md +1 -1
  96. package/gsd-core/workflows/manager.md +15 -15
  97. package/gsd-core/workflows/milestone-summary.md +3 -3
  98. package/gsd-core/workflows/new-milestone.md +6 -0
  99. package/gsd-core/workflows/plan-phase/steps/closed-phase-gate.md +42 -0
  100. package/gsd-core/workflows/plan-phase/steps/prd-express-path.md +102 -0
  101. package/gsd-core/workflows/plan-phase/steps/windows-troubleshooting.md +23 -0
  102. package/gsd-core/workflows/plan-phase.md +4 -159
  103. package/gsd-core/workflows/review.md +33 -2
  104. package/gsd-core/workflows/thread.md +4 -4
  105. package/gsd-core/workflows/verify-phase.md +11 -4
  106. package/gsd-core/workflows/verify-work.md +1 -2
  107. package/hooks/dist/gsd-graphify-update.sh +7 -1
  108. package/hooks/gsd-graphify-update.sh +7 -1
  109. package/package.json +6 -4
  110. package/scripts/ci-test-scope.cjs +38 -9
  111. package/scripts/lint-allow-test-rule-refs.allowlist.json +0 -1
  112. package/scripts/lint-regression-test-names.allowlist.json +3 -0
  113. package/scripts/lint-test-file-count.allowlist.json +19 -5
  114. package/scripts/mutation-matrix.cjs +45 -3
  115. package/scripts/prompt-injection-scan.sh +8 -0
  116. package/scripts/run-tests.cjs +51 -1
  117. package/scripts/sync-manifest-versions.cjs +66 -14
  118. package/skills/gsd-review/SKILL.md +6 -0
  119. package/scripts/lint-windows-test-portability.cjs +0 -178
@@ -153,16 +153,31 @@ function _setCapabilitySourceHttpGet(fn) {
153
153
  // ---------------------------------------------------------------------------
154
154
  // Helpers
155
155
  // ---------------------------------------------------------------------------
156
- /** Resolve the running GSD version; fail-closed to '0.0.0'. */
157
- function readHostVersion() {
156
+ /**
157
+ * Resolve the running GSD version; fail-closed to '0.0.0'.
158
+ *
159
+ * Prefer the authoritative `gsd-core/VERSION` the installer writes for EVERY runtime
160
+ * (libDir = gsd-core/bin/lib/, so `../../VERSION` = gsd-core/VERSION), so installed
161
+ * layouts report the true version even when the walked-up `../../../package.json` is
162
+ * absent or belongs to the user's own project (#1920). Fall back to the runtime-root
163
+ * package.json (dev/source tree), then fail-closed. Mirrors resolveVersionFrom() (#1383).
164
+ */
165
+ function readHostVersion(libDir = __dirname) {
166
+ const SEMVER_PREFIX = /^\d+\.\d+\.\d+/;
158
167
  try {
159
- // eslint-disable-next-line @typescript-eslint/no-require-imports
160
- const pkg = require('../../../package.json');
161
- return typeof pkg.version === 'string' && pkg.version ? pkg.version : '0.0.0';
168
+ const v = node_fs_1.default.readFileSync(node_path_1.default.join(libDir, '..', '..', 'VERSION'), 'utf8').trim();
169
+ if (SEMVER_PREFIX.test(v))
170
+ return v;
162
171
  }
163
- catch {
164
- return '0.0.0';
172
+ catch { /* not an installed tree (no gsd-core/VERSION) */ }
173
+ try {
174
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
175
+ const pkg = require(node_path_1.default.join(libDir, '..', '..', '..', 'package.json'));
176
+ if (pkg && typeof pkg.version === 'string' && SEMVER_PREFIX.test(pkg.version))
177
+ return pkg.version;
165
178
  }
179
+ catch { /* runtime root has no package.json */ }
180
+ return '0.0.0';
166
181
  }
167
182
  /** Compute sha512-<base64> integrity over a buffer. */
168
183
  function computeIntegrity(buf) {
@@ -594,13 +609,13 @@ function stageValidated(opts) {
594
609
  // lives in capability-lifecycle.cjs and uses promote:false above.)
595
610
  if (node_fs_1.default.existsSync(finalDir)) {
596
611
  const backupDir = `${finalDir}.old-${process.pid}-${Date.now()}`;
597
- node_fs_1.default.renameSync(finalDir, backupDir);
612
+ shellSeam.retryRenameSync(finalDir, backupDir);
598
613
  try {
599
- node_fs_1.default.renameSync(stagingDir, finalDir);
614
+ shellSeam.retryRenameSync(stagingDir, finalDir);
600
615
  }
601
616
  catch (err) {
602
617
  try {
603
- node_fs_1.default.renameSync(backupDir, finalDir);
618
+ shellSeam.retryRenameSync(backupDir, finalDir);
604
619
  }
605
620
  catch { /* best-effort restore */ }
606
621
  throw err;
@@ -611,7 +626,7 @@ function stageValidated(opts) {
611
626
  catch { /* best-effort */ }
612
627
  }
613
628
  else {
614
- node_fs_1.default.renameSync(stagingDir, finalDir);
629
+ shellSeam.retryRenameSync(stagingDir, finalDir);
615
630
  }
616
631
  const version = typeof cap['version'] === 'string' ? cap['version'] : '';
617
632
  return { id, version, stagedDir: finalDir, integrity, source };
@@ -704,7 +704,12 @@ const VALID_CONFIG_FORMATS = new Set(['settings-json', 'toml', 'markdown', 'mark
704
704
  const VALID_CONFIG_HOME_KINDS = new Set(['dot-home', 'dot-home-nested', 'xdg', 'generic-agents-root']);
705
705
  const VALID_COMMAND_STYLES = new Set(['slash-hyphen', 'shell-var']);
706
706
  const VALID_HOOKS_SURFACES = new Set(['settings-json', 'codex-hooks-json', 'cursor-hooks-json', 'copilot-inline', 'cline-rules', 'none']);
707
- const VALID_HOOK_EVENTS = new Set(['claude', 'gemini', 'opencode-subset']);
707
+ const VALID_HOOK_EVENTS = new Set(['claude', 'gemini']);
708
+ // extensionEvents — the plugin/extension-system event dialect (ADR-1239 amendment / #1943).
709
+ // DISTINCT from hookEvents (managed-hook dialect): extensionEvents describes the
710
+ // plugin-owned event subset imperative hosts expose (opencode / pi); 'none' = the
711
+ // host exposes no extension surface (engine owns the bus, e.g. VS Code).
712
+ const VALID_EXTENSION_EVENTS = new Set(['opencode', 'pi', 'none']);
708
713
  const VALID_SANDBOX_TIERS = new Set(['none', 'codex-agent-sandbox']);
709
714
  const VALID_ARTIFACT_KIND_NAMES = new Set(['commands', 'agents', 'skills', 'kimi-agents']);
710
715
  const VALID_ARTIFACT_NESTINGS = new Set(['flat', 'nested']);
@@ -713,6 +718,16 @@ const VALID_INSTALL_SURFACES = new Set(['settings-json', 'codex-toml', 'copilot-
713
718
  const VALID_PERMISSION_WRITERS = new Set(['opencode', 'kilo']);
714
719
  const VALID_EXTENDED_HOOK_EVENTS = new Set(['SubagentStop', 'Stop', 'PreCompact', 'FileChanged', 'BeforeAgent', 'AfterAgent', 'BeforeModel']);
715
720
 
721
+ // ADR-1239 Phase A: hostIntegration axes (MUST stay parity-identical to HOST_INTEGRATION_AXES in src/host-integration.cts)
722
+ const VALID_EMBEDDING_MODES = new Set(['imperative', 'declarative']);
723
+ const VALID_COMMAND_SURFACES = new Set(['slash-file', 'slash-programmatic', 'slash-toml', 'palette', 'prose-only']);
724
+ const VALID_MODEL_MODES = new Set(['active', 'passive']);
725
+ const VALID_HOOK_BUSES = new Set(['host', 'engine', 'none']);
726
+ const VALID_STATE_IO = new Set(['filesystem', 'sandboxed-storage', 'session-log-append']);
727
+ const VALID_TRANSPORTS = new Set(['mcp', 'native-extension']);
728
+ const VALID_HOST_RUNTIMES = new Set(['node', 'bun', 'sandboxed-web', 'python', 'go', 'rust', 'electron', 'other']);
729
+ const VALID_SUBAGENT_TOOLKITS = new Set(['full', 'read-only']);
730
+
716
731
  // GATE A: installSurface → allowed hooksSurface values (DEFECT.GENERATIVE-FIX: parity invariant)
717
732
  // Derived from the actual pairings in the 16 real runtime descriptors.
718
733
  const INSTALL_SURFACE_TO_ALLOWED_HOOKS_SURFACES = new Map([
@@ -968,7 +983,9 @@ function validateRuntimeBody(cap) {
968
983
  );
969
984
  }
970
985
 
971
- // hookEvents — optional; if present must be in closed 3-enum (ADR-1016 Decision 5)
986
+ // hookEvents — optional; if present must be in closed enum (ADR-1016 Decision 5).
987
+ // Managed-hook dialect only (claude/gemini). The OpenCode extension-system event
988
+ // subset is NOT a hookEvents value — it is `extensionEvents` (ADR-1239 / #1943).
972
989
  if (r.hookEvents !== undefined) {
973
990
  if (r.hookEvents === '__proto__' || r.hookEvents === 'constructor' || r.hookEvents === 'prototype') {
974
991
  errors.push('runtime.hookEvents "' + r.hookEvents + '" is a reserved name');
@@ -980,6 +997,20 @@ function validateRuntimeBody(cap) {
980
997
  }
981
998
  }
982
999
 
1000
+ // extensionEvents — optional; the extension-system event dialect (ADR-1239 amendment / #1943).
1001
+ // Distinct from hookEvents. Only imperative-embedding hosts (with a plugin/extension
1002
+ // API) set it; declarative hosts do not.
1003
+ if (r.extensionEvents !== undefined) {
1004
+ if (r.extensionEvents === '__proto__' || r.extensionEvents === 'constructor' || r.extensionEvents === 'prototype') {
1005
+ errors.push('runtime.extensionEvents "' + r.extensionEvents + '" is a reserved name');
1006
+ } else if (!VALID_EXTENSION_EVENTS.has(r.extensionEvents)) {
1007
+ errors.push(
1008
+ 'runtime.extensionEvents must be one of: ' + [...VALID_EXTENSION_EVENTS].join(', ') +
1009
+ ' (got: ' + JSON.stringify(r.extensionEvents) + ')',
1010
+ );
1011
+ }
1012
+ }
1013
+
983
1014
  // sandboxTier — closed 2-enum (ADR-1016 Decision 6); inline literal guard (CodeQL barrier)
984
1015
  if (r.sandboxTier === '__proto__' || r.sandboxTier === 'constructor' || r.sandboxTier === 'prototype') {
985
1016
  errors.push('runtime.sandboxTier "' + r.sandboxTier + '" is a reserved name');
@@ -1020,6 +1051,20 @@ function validateRuntimeBody(cap) {
1020
1051
  );
1021
1052
  }
1022
1053
 
1054
+ // localConfigDir — REQUIRED non-empty dot-dir string (ADR-1239 Phase B #1679)
1055
+ // Must start with '.' (e.g. ".claude", ".cursor"). Validated here so the registry
1056
+ // generator catches any descriptor missing the field before regenerating.
1057
+ if (typeof r.localConfigDir !== 'string' || r.localConfigDir.length === 0) {
1058
+ errors.push(
1059
+ 'runtime.localConfigDir is required and must be a non-empty string (e.g. ".claude"); ' +
1060
+ 'got: ' + JSON.stringify(r.localConfigDir),
1061
+ );
1062
+ } else if (!r.localConfigDir.startsWith('.')) {
1063
+ errors.push(
1064
+ 'runtime.localConfigDir must start with "." (a dot-dir); got: ' + JSON.stringify(r.localConfigDir),
1065
+ );
1066
+ }
1067
+
1023
1068
  // extendedHookEvents — required array; every element must be in closed enum
1024
1069
  if (!Array.isArray(r.extendedHookEvents)) {
1025
1070
  errors.push(
@@ -1037,6 +1082,162 @@ function validateRuntimeBody(cap) {
1037
1082
  }
1038
1083
  }
1039
1084
 
1085
+ // hostIntegration — ADR-1239 Phase A: required object with closed-enum axes
1086
+ if (typeof r.hostIntegration !== 'object' || r.hostIntegration === null || Array.isArray(r.hostIntegration)) {
1087
+ errors.push('runtime.hostIntegration is required and must be an object');
1088
+ } else {
1089
+ const hi = r.hostIntegration;
1090
+
1091
+ // S2b: reserved-OWN-KEY guard on hostIntegration (CodeQL barrier — inline literal comparisons)
1092
+ if (Object.prototype.hasOwnProperty.call(hi, '__proto__')) {
1093
+ errors.push('runtime.hostIntegration must not contain reserved key "__proto__"');
1094
+ }
1095
+ if (Object.prototype.hasOwnProperty.call(hi, 'constructor')) {
1096
+ errors.push('runtime.hostIntegration must not contain reserved key "constructor"');
1097
+ }
1098
+ if (Object.prototype.hasOwnProperty.call(hi, 'prototype')) {
1099
+ errors.push('runtime.hostIntegration must not contain reserved key "prototype"');
1100
+ }
1101
+
1102
+ // embeddingMode
1103
+ if (hi.embeddingMode === '__proto__' || hi.embeddingMode === 'constructor' || hi.embeddingMode === 'prototype') {
1104
+ errors.push('runtime.hostIntegration.embeddingMode "' + hi.embeddingMode + '" is a reserved name');
1105
+ } else if (hi.embeddingMode !== 'undocumented' && !VALID_EMBEDDING_MODES.has(hi.embeddingMode)) {
1106
+ errors.push(
1107
+ 'runtime.hostIntegration.embeddingMode must be one of: ' + [...VALID_EMBEDDING_MODES].join(', ') +
1108
+ ' (or "undocumented") (got: ' + JSON.stringify(hi.embeddingMode) + ')',
1109
+ );
1110
+ }
1111
+
1112
+ // commandSurface
1113
+ if (hi.commandSurface === '__proto__' || hi.commandSurface === 'constructor' || hi.commandSurface === 'prototype') {
1114
+ errors.push('runtime.hostIntegration.commandSurface "' + hi.commandSurface + '" is a reserved name');
1115
+ } else if (hi.commandSurface !== 'undocumented' && !VALID_COMMAND_SURFACES.has(hi.commandSurface)) {
1116
+ errors.push(
1117
+ 'runtime.hostIntegration.commandSurface must be one of: ' + [...VALID_COMMAND_SURFACES].join(', ') +
1118
+ ' (or "undocumented") (got: ' + JSON.stringify(hi.commandSurface) + ')',
1119
+ );
1120
+ }
1121
+
1122
+ // modelMode
1123
+ if (hi.modelMode === '__proto__' || hi.modelMode === 'constructor' || hi.modelMode === 'prototype') {
1124
+ errors.push('runtime.hostIntegration.modelMode "' + hi.modelMode + '" is a reserved name');
1125
+ } else if (hi.modelMode !== 'undocumented' && !VALID_MODEL_MODES.has(hi.modelMode)) {
1126
+ errors.push(
1127
+ 'runtime.hostIntegration.modelMode must be one of: ' + [...VALID_MODEL_MODES].join(', ') +
1128
+ ' (or "undocumented") (got: ' + JSON.stringify(hi.modelMode) + ')',
1129
+ );
1130
+ }
1131
+
1132
+ // hookBus
1133
+ if (hi.hookBus === '__proto__' || hi.hookBus === 'constructor' || hi.hookBus === 'prototype') {
1134
+ errors.push('runtime.hostIntegration.hookBus "' + hi.hookBus + '" is a reserved name');
1135
+ } else if (hi.hookBus !== 'undocumented' && !VALID_HOOK_BUSES.has(hi.hookBus)) {
1136
+ errors.push(
1137
+ 'runtime.hostIntegration.hookBus must be one of: ' + [...VALID_HOOK_BUSES].join(', ') +
1138
+ ' (or "undocumented") (got: ' + JSON.stringify(hi.hookBus) + ')',
1139
+ );
1140
+ }
1141
+
1142
+ // stateIO
1143
+ if (hi.stateIO === '__proto__' || hi.stateIO === 'constructor' || hi.stateIO === 'prototype') {
1144
+ errors.push('runtime.hostIntegration.stateIO "' + hi.stateIO + '" is a reserved name');
1145
+ } else if (hi.stateIO !== 'undocumented' && !VALID_STATE_IO.has(hi.stateIO)) {
1146
+ errors.push(
1147
+ 'runtime.hostIntegration.stateIO must be one of: ' + [...VALID_STATE_IO].join(', ') +
1148
+ ' (or "undocumented") (got: ' + JSON.stringify(hi.stateIO) + ')',
1149
+ );
1150
+ }
1151
+
1152
+ // transport
1153
+ if (hi.transport === '__proto__' || hi.transport === 'constructor' || hi.transport === 'prototype') {
1154
+ errors.push('runtime.hostIntegration.transport "' + hi.transport + '" is a reserved name');
1155
+ } else if (hi.transport !== 'undocumented' && !VALID_TRANSPORTS.has(hi.transport)) {
1156
+ errors.push(
1157
+ 'runtime.hostIntegration.transport must be one of: ' + [...VALID_TRANSPORTS].join(', ') +
1158
+ ' (or "undocumented") (got: ' + JSON.stringify(hi.transport) + ')',
1159
+ );
1160
+ }
1161
+
1162
+ // runtime (axis)
1163
+ if (hi.runtime === '__proto__' || hi.runtime === 'constructor' || hi.runtime === 'prototype') {
1164
+ errors.push('runtime.hostIntegration.runtime "' + hi.runtime + '" is a reserved name');
1165
+ } else if (hi.runtime !== 'undocumented' && !VALID_HOST_RUNTIMES.has(hi.runtime)) {
1166
+ errors.push(
1167
+ 'runtime.hostIntegration.runtime must be one of: ' + [...VALID_HOST_RUNTIMES].join(', ') +
1168
+ ' (or "undocumented") (got: ' + JSON.stringify(hi.runtime) + ')',
1169
+ );
1170
+ }
1171
+
1172
+ // dispatch — required object
1173
+ if (typeof hi.dispatch !== 'object' || hi.dispatch === null || Array.isArray(hi.dispatch)) {
1174
+ errors.push('runtime.hostIntegration.dispatch must be an object');
1175
+ } else {
1176
+ const d = hi.dispatch;
1177
+
1178
+ // S2b: reserved-OWN-KEY guard on dispatch (CodeQL barrier — inline literal comparisons)
1179
+ if (Object.prototype.hasOwnProperty.call(d, '__proto__')) {
1180
+ errors.push('runtime.hostIntegration.dispatch must not contain reserved key "__proto__"');
1181
+ }
1182
+ if (Object.prototype.hasOwnProperty.call(d, 'constructor')) {
1183
+ errors.push('runtime.hostIntegration.dispatch must not contain reserved key "constructor"');
1184
+ }
1185
+ if (Object.prototype.hasOwnProperty.call(d, 'prototype')) {
1186
+ errors.push('runtime.hostIntegration.dispatch must not contain reserved key "prototype"');
1187
+ }
1188
+
1189
+ // namedDispatch — boolean or 'undocumented'
1190
+ if (typeof d.namedDispatch !== 'boolean' && d.namedDispatch !== 'undocumented') {
1191
+ errors.push(
1192
+ 'runtime.hostIntegration.dispatch.namedDispatch must be a boolean or "undocumented" (got: ' + JSON.stringify(d.namedDispatch) + ')',
1193
+ );
1194
+ }
1195
+
1196
+ // nested — boolean or 'undocumented'
1197
+ if (typeof d.nested !== 'boolean' && d.nested !== 'undocumented') {
1198
+ errors.push(
1199
+ 'runtime.hostIntegration.dispatch.nested must be a boolean or "undocumented" (got: ' + JSON.stringify(d.nested) + ')',
1200
+ );
1201
+ }
1202
+
1203
+ // background — boolean or 'undocumented'
1204
+ if (typeof d.background !== 'boolean' && d.background !== 'undocumented') {
1205
+ errors.push(
1206
+ 'runtime.hostIntegration.dispatch.background must be a boolean or "undocumented" (got: ' + JSON.stringify(d.background) + ')',
1207
+ );
1208
+ }
1209
+
1210
+ // subagentToolkit — closed enum or 'undocumented'
1211
+ if (d.subagentToolkit === '__proto__' || d.subagentToolkit === 'constructor' || d.subagentToolkit === 'prototype') {
1212
+ errors.push('runtime.hostIntegration.dispatch.subagentToolkit "' + d.subagentToolkit + '" is a reserved name');
1213
+ } else if (d.subagentToolkit !== 'undocumented' && !VALID_SUBAGENT_TOOLKITS.has(d.subagentToolkit)) {
1214
+ errors.push(
1215
+ 'runtime.hostIntegration.dispatch.subagentToolkit must be one of: ' + [...VALID_SUBAGENT_TOOLKITS].join(', ') +
1216
+ ' (or "undocumented") (got: ' + JSON.stringify(d.subagentToolkit) + ')',
1217
+ );
1218
+ }
1219
+
1220
+ // maxDepth — integer >= -1 or 'undocumented'
1221
+ if (d.maxDepth !== 'undocumented' && (!Number.isInteger(d.maxDepth) || d.maxDepth < -1)) {
1222
+ errors.push(
1223
+ 'runtime.hostIntegration.dispatch.maxDepth must be an integer >= -1 or "undocumented" (got: ' + JSON.stringify(d.maxDepth) + ')',
1224
+ );
1225
+ }
1226
+
1227
+ // backgroundDispatch — REQUIRED (all 16 runtime descriptors carry it, matching the sibling fields
1228
+ // namedDispatch/nested/background/subagentToolkit/maxDepth which are all required).
1229
+ if (!Object.prototype.hasOwnProperty.call(d, 'backgroundDispatch')) {
1230
+ errors.push(
1231
+ 'runtime.hostIntegration.dispatch.backgroundDispatch is required (must be a boolean or "undocumented")',
1232
+ );
1233
+ } else if (typeof d.backgroundDispatch !== 'boolean' && d.backgroundDispatch !== 'undocumented') {
1234
+ errors.push(
1235
+ 'runtime.hostIntegration.dispatch.backgroundDispatch must be a boolean or "undocumented" (got: ' + JSON.stringify(d.backgroundDispatch) + ')',
1236
+ );
1237
+ }
1238
+ }
1239
+ }
1240
+
1040
1241
  // GATE A: installSurface ↔ hooksSurface consistency (DEFECT.GENERATIVE-FIX)
1041
1242
  // Only check if both fields are valid strings (individual field validators above report type errors).
1042
1243
  if (typeof r.installSurface === 'string' && typeof r.hooksSurface === 'string') {
@@ -2045,6 +2246,7 @@ module.exports = {
2045
2246
  VALID_COMMAND_STYLES,
2046
2247
  VALID_HOOKS_SURFACES,
2047
2248
  VALID_HOOK_EVENTS,
2249
+ VALID_EXTENSION_EVENTS,
2048
2250
  VALID_SANDBOX_TIERS,
2049
2251
  VALID_ARTIFACT_KIND_NAMES,
2050
2252
  VALID_ARTIFACT_NESTINGS,
@@ -2052,6 +2254,24 @@ module.exports = {
2052
2254
  VALID_INSTALL_SURFACES,
2053
2255
  VALID_PERMISSION_WRITERS,
2054
2256
  VALID_EXTENDED_HOOK_EVENTS,
2257
+ VALID_EMBEDDING_MODES,
2258
+ VALID_COMMAND_SURFACES,
2259
+ VALID_MODEL_MODES,
2260
+ VALID_HOOK_BUSES,
2261
+ VALID_STATE_IO,
2262
+ VALID_TRANSPORTS,
2263
+ VALID_HOST_RUNTIMES,
2264
+ VALID_SUBAGENT_TOOLKITS,
2265
+ _HOST_INTEGRATION_VOCAB: {
2266
+ embeddingMode: [...VALID_EMBEDDING_MODES],
2267
+ commandSurface: [...VALID_COMMAND_SURFACES],
2268
+ modelMode: [...VALID_MODEL_MODES],
2269
+ hookBus: [...VALID_HOOK_BUSES],
2270
+ stateIO: [...VALID_STATE_IO],
2271
+ transport: [...VALID_TRANSPORTS],
2272
+ runtime: [...VALID_HOST_RUNTIMES],
2273
+ subagentToolkit: [...VALID_SUBAGENT_TOOLKITS],
2274
+ },
2055
2275
  INSTALL_SURFACE_TO_ALLOWED_HOOKS_SURFACES,
2056
2276
  GEMINI_AGENT_EVENTS,
2057
2277
  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": [
@@ -1302,7 +1302,8 @@ function cmdStats(cwd, format, raw) {
1302
1302
  const roadmapContent = extractCurrentMilestone(roadmapRaw, cwd);
1303
1303
  // Matches both plain numeric (Phase 1:) and milestone-prefixed (Phase 2-01:) headings.
1304
1304
  // Also tolerates optional [bracket-token] scope prefix on phase headings.
1305
- const headingPattern = /#{2,4}\s*(?:\[[^\]]+\]\s*)?Phase\s+([\w][\w.-]*)\s*:\s*([^\n]+)/gi;
1305
+ // #1729: `(?:\s*\([^)\n]*\))?` tolerates a pre-colon ( ) tag (literal mirror of OPTIONAL_PHASE_TAG_SOURCE).
1306
+ const headingPattern = /#{2,4}\s*(?:\[[^\]]+\]\s*)?Phase\s+([\w][\w.-]*)(?:\s*\([^)\n]*\))?\s*:\s*([^\n]+)/gi;
1306
1307
  let match;
1307
1308
  while ((match = headingPattern.exec(roadmapContent)) !== null) {
1308
1309
  const key = normalizePhaseName(match[1]);
@@ -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
+ }
@@ -126,6 +126,49 @@ function extractFrontmatter(content) {
126
126
  }
127
127
  return frontmatter;
128
128
  }
129
+ /**
130
+ * Escape a string for emission inside a YAML double-quoted scalar (#1779).
131
+ * Backslash must be escaped first so the backslashes added for embedded quotes
132
+ * (and control chars) are not themselves doubled. Without this, a value
133
+ * carrying an indicator (`:`/`#`) that also contains a literal `"` serializes
134
+ * to invalid YAML, e.g. `upstream: "https://x (Tom; "Git. Ship. Done")"`. A
135
+ * literal newline/tab/control char inside the quotes likewise breaks (or
136
+ * silently alters) the scalar, so those are escaped to their YAML forms too.
137
+ */
138
+ function escapeDoubleQuoted(s) {
139
+ return s
140
+ .replace(/\\/g, '\\\\')
141
+ .replace(/"/g, '\\"')
142
+ .replace(/\n/g, '\\n')
143
+ .replace(/\t/g, '\\t')
144
+ .replace(/\r/g, '\\r')
145
+ // Remaining C0 controls + DEL → \xHH (a valid YAML double-quoted escape).
146
+ .replace(/[\u0000-\u001f\u007f]/g, (c) => `\\x${c.charCodeAt(0).toString(16).padStart(2, '0')}`);
147
+ }
148
+ /**
149
+ * A plain (unquoted) scalar that would mis-parse or round-trip lossily when
150
+ * emitted bare must instead go through the double-quoted + escaped form
151
+ * (#1779): the empty string (bare `k:` reloads as null), an embedded `"`/`\`
152
+ * or control char, a leading YAML indicator (quote, `&`/`*`/`!` anchor/alias/
153
+ * tag, `|`/`>` block scalar, flow `[]{},`, `#`, reserved `%`/`@`/backtick, or
154
+ * `-`/`?`/`:` before a space), or leading/trailing whitespace. This helper is
155
+ * the correctness complement of `escapeDoubleQuoted`: it broadens the *trigger*
156
+ * for quoting without broadening the lossy object-list handling deferred to
157
+ * #1572/#1660.
158
+ */
159
+ function scalarNeedsDoubleQuoting(s) {
160
+ if (s === '')
161
+ return true;
162
+ if (/["\\\u0000-\u001f\u007f]/.test(s))
163
+ return true;
164
+ // Always-unsafe leading indicators, or leading/trailing whitespace.
165
+ if (/^[,[\]{}#&*!|>'"%@`]/.test(s) || /^\s|\s$/.test(s))
166
+ return true;
167
+ // `-` `?` `:` only start a plain scalar safely when NOT followed by a space.
168
+ if (/^[-?:](\s|$)/.test(s))
169
+ return true;
170
+ return false;
171
+ }
129
172
  function reconstructFrontmatter(obj) {
130
173
  const lines = [];
131
174
  for (const [key, value] of Object.entries(obj)) {
@@ -141,7 +184,7 @@ function reconstructFrontmatter(obj) {
141
184
  else {
142
185
  lines.push(`${key}:`);
143
186
  for (const item of value) {
144
- lines.push(` - ${typeof item === 'string' && (item.includes(':') || item.includes('#')) ? `"${item}"` : item}`);
187
+ lines.push(` - ${typeof item === 'string' && (item.includes(':') || item.includes('#') || scalarNeedsDoubleQuoting(item)) ? `"${escapeDoubleQuoted(item)}"` : item}`);
145
188
  }
146
189
  }
147
190
  }
@@ -160,7 +203,7 @@ function reconstructFrontmatter(obj) {
160
203
  else {
161
204
  lines.push(` ${subkey}:`);
162
205
  for (const item of subval) {
163
- lines.push(` - ${typeof item === 'string' && (item.includes(':') || item.includes('#')) ? `"${item}"` : item}`);
206
+ lines.push(` - ${typeof item === 'string' && (item.includes(':') || item.includes('#') || scalarNeedsDoubleQuoting(item)) ? `"${escapeDoubleQuoted(item)}"` : item}`);
164
207
  }
165
208
  }
166
209
  }
@@ -189,14 +232,14 @@ function reconstructFrontmatter(obj) {
189
232
  else {
190
233
  // eslint-disable-next-line @typescript-eslint/no-base-to-string
191
234
  const sv = String(subval);
192
- lines.push(` ${subkey}: ${sv.includes(':') || sv.includes('#') ? `"${sv}"` : sv}`);
235
+ lines.push(` ${subkey}: ${sv.includes(':') || sv.includes('#') || scalarNeedsDoubleQuoting(sv) ? `"${escapeDoubleQuoted(sv)}"` : sv}`);
193
236
  }
194
237
  }
195
238
  }
196
239
  else {
197
240
  const sv = String(value);
198
- if (sv.includes(':') || sv.includes('#') || sv.startsWith('[') || sv.startsWith('{')) {
199
- lines.push(`${key}: "${sv}"`);
241
+ if (sv.includes(':') || sv.includes('#') || sv.startsWith('[') || sv.startsWith('{') || scalarNeedsDoubleQuoting(sv)) {
242
+ lines.push(`${key}: "${escapeDoubleQuoted(sv)}"`);
200
243
  }
201
244
  else {
202
245
  lines.push(`${key}: ${sv}`);
@@ -448,7 +491,11 @@ function parseMustHavesBlock(content, blockName) {
448
491
  else {
449
492
  const kvMatch = trimmed.match(/^(\w+):\s*"?([^"]*)"?\s*$/);
450
493
  if (kvMatch) {
451
- const val = kvMatch[2];
494
+ // Trim: a quoted value like `"backstop "` captures the inner trailing space in group 2.
495
+ // Left untrimmed, a hand-authored `must_haves` marker degrades (a `backstop` truth silently
496
+ // grades green instead of abstaining — #1905, the #1154 false-pass; also the sibling
497
+ // check_target/violationFixture path). Whitespace is never semantic in a scalar KV value.
498
+ const val = kvMatch[2].trim();
452
499
  // Try to parse as number
453
500
  (current)[kvMatch[1]] = /^\d+$/.test(val) ? parseInt(val, 10) : val;
454
501
  }