@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.
- package/.claude-plugin/plugin.json +1 -1
- package/agents/gsd-verifier.md +1 -0
- package/bin/gsd-mcp-server.js +31 -0
- package/bin/install.js +293 -1145
- package/commands/gsd/review.md +6 -0
- package/gemini-extension.json +1 -1
- package/gsd-core/bin/gsd-tools.cjs +116 -1
- package/gsd-core/bin/lib/adapter-declarative.cjs +35 -0
- package/gsd-core/bin/lib/adapter-imperative.cjs +52 -0
- package/gsd-core/bin/lib/assumption-delta.cjs +231 -0
- package/gsd-core/bin/lib/capability-lifecycle.cjs +7 -7
- package/gsd-core/bin/lib/capability-loader.cjs +18 -0
- package/gsd-core/bin/lib/capability-lock.cjs +2 -2
- package/gsd-core/bin/lib/capability-registry.cjs +889 -82
- package/gsd-core/bin/lib/capability-source.cjs +4 -4
- package/gsd-core/bin/lib/capability-validator.cjs +198 -0
- package/gsd-core/bin/lib/cli-skew-check.cjs +44 -0
- package/gsd-core/bin/lib/command-aliases.cjs +8 -0
- package/gsd-core/bin/lib/config.cjs +27 -0
- package/gsd-core/bin/lib/embedding-adapter.cjs +27 -0
- package/gsd-core/bin/lib/external-descriptor-trust.cjs +70 -0
- package/gsd-core/bin/lib/hook-bus.cjs +81 -0
- package/gsd-core/bin/lib/host-integration.cjs +408 -0
- package/gsd-core/bin/lib/init.cjs +1 -1
- package/gsd-core/bin/lib/install-engine.cjs +755 -0
- package/gsd-core/bin/lib/install-profiles.cjs +35 -4
- package/gsd-core/bin/lib/installer-migrations.cjs +1 -1
- package/gsd-core/bin/lib/mcp-server.cjs +194 -0
- package/gsd-core/bin/lib/milestone.cjs +27 -30
- package/gsd-core/bin/lib/model-adapter.cjs +50 -0
- package/gsd-core/bin/lib/phase.cjs +41 -72
- package/gsd-core/bin/lib/planning-workspace.cjs +1 -1
- package/gsd-core/bin/lib/probe-core.cjs +91 -1
- package/gsd-core/bin/lib/review-reviewer-selection.cjs +129 -13
- package/gsd-core/bin/lib/roadmap-upgrade.cjs +3 -2
- package/gsd-core/bin/lib/roadmap.cjs +17 -3
- package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +65 -9
- package/gsd-core/bin/lib/runtime-artifact-install-plan.cjs +54 -4
- package/gsd-core/bin/lib/runtime-artifact-layout.cjs +5 -2
- package/gsd-core/bin/lib/runtime-hooks-surface.cjs +1 -1
- package/gsd-core/bin/lib/runtime-name-policy.cjs +160 -30
- package/gsd-core/bin/lib/shell-command-projection.cjs +37 -1
- package/gsd-core/bin/lib/stale-bake-guard.cjs +254 -0
- package/gsd-core/bin/lib/state-command-router.cjs +4 -0
- package/gsd-core/bin/lib/state-io.cjs +55 -0
- package/gsd-core/bin/lib/state-transition.cjs +1588 -0
- package/gsd-core/bin/lib/state.cjs +306 -681
- package/gsd-core/bin/lib/surface.cjs +4 -1
- package/gsd-core/bin/lib/workstream.cjs +4 -4
- package/gsd-core/bin/shared/config-schema.manifest.json +9 -0
- package/gsd-core/references/honest-verifier.md +105 -0
- package/gsd-core/references/reviewer-instances.md +99 -0
- package/gsd-core/workflows/autonomous.md +9 -9
- package/gsd-core/workflows/manager.md +15 -15
- package/gsd-core/workflows/plan-phase.md +1 -1
- package/gsd-core/workflows/review.md +26 -0
- package/gsd-core/workflows/thread.md +4 -4
- package/gsd-core/workflows/verify-phase.md +11 -4
- package/hooks/dist/gsd-graphify-update.sh +7 -1
- package/hooks/gsd-graphify-update.sh +7 -1
- package/package.json +4 -4
- package/scripts/ci-test-scope.cjs +38 -9
- package/scripts/lint-allow-test-rule-refs.allowlist.json +0 -1
- package/scripts/lint-regression-test-names.allowlist.json +3 -0
- package/scripts/lint-test-file-count.allowlist.json +19 -5
- package/scripts/mutation-matrix.cjs +45 -3
- package/scripts/prompt-injection-scan.sh +8 -0
- 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
|
-
|
|
597
|
+
shellSeam.retryRenameSync(finalDir, backupDir);
|
|
598
598
|
try {
|
|
599
|
-
|
|
599
|
+
shellSeam.retryRenameSync(stagingDir, finalDir);
|
|
600
600
|
}
|
|
601
601
|
catch (err) {
|
|
602
602
|
try {
|
|
603
|
-
|
|
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
|
-
|
|
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
|
+
}
|