@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
@@ -35,6 +35,12 @@ Phase number: extracted from $ARGUMENTS (required)
35
35
  - `--cursor` — Include Cursor agent review
36
36
  - `--agy` / `--antigravity` — Include Antigravity CLI review
37
37
  - `--all` — Include all available CLIs
38
+
39
+ **No flags** — if `review.default_reviewers` is set, review with only those configured
40
+ reviewers that are detected; otherwise review with all available CLIs. Configured
41
+ `review.reviewer_instances` names may appear in `review.default_reviewers`; each runs as an
42
+ independent reviewer identity backed by its configured adapter+model (see
43
+ `docs/CONFIGURATION.md`). Instance names are not valid as flags.
38
44
  </context>
39
45
 
40
46
  <process>
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gsd-core",
3
- "version": "1.6.1",
3
+ "version": "1.7.0-rc.2",
4
4
  "description": "GSD Core — a meta-prompting, context engineering, and spec-driven development system for AI coding agents. Loads gsd's operating context into every Gemini CLI session.",
5
5
  "contextFileName": "GEMINI.md"
6
6
  }
@@ -57,7 +57,7 @@
57
57
  * Milestone Operations:
58
58
  * milestone complete <version> Archive milestone, create MILESTONES.md
59
59
  * [--name <name>]
60
- * [--archive-phases] Move phase dirs to milestones/vX.Y-phases/
60
+ * [--no-archive-phases] Skip moving phase dirs to milestones/vX.Y-phases/ (archived by default)
61
61
  *
62
62
  * User Story Validation:
63
63
  * user-story validate --story "..." Validate "As a / I want to / so that" format
@@ -204,11 +204,31 @@ const projectRoot = require('./lib/project-root.cjs');
204
204
  // against any require/load-ordering edge where the export isn't bound yet
205
205
  // when this entrypoint is first required (#604).
206
206
  const findProjectRoot = (...args) => projectRoot.findProjectRoot(...args);
207
+
208
+ // #1754: CLI skew detection — warn (stderr, non-blocking) if this gsd-tools.cjs
209
+ // is NOT the project-local install while a project-local install exists. Catches
210
+ // the shadowing scenario from #1748 (stale global canary shadowing project-local).
211
+ try {
212
+ const _skew = require('./lib/cli-skew-check.cjs');
213
+ const _skewRoot = findProjectRoot(process.cwd());
214
+ if (_skewRoot) {
215
+ const _skewLocal = path.join(_skewRoot, '.claude', 'gsd-core', 'bin', 'gsd-tools.cjs');
216
+ const _skewWarn = _skew.checkCliSkew({
217
+ resolvedPath: path.resolve(__filename),
218
+ projectRoot: _skewRoot,
219
+ projectLocalExists: fs.existsSync(_skewLocal),
220
+ });
221
+ if (_skewWarn) process.stderr.write(_skewWarn + '\n');
222
+ }
223
+ } catch { /* advisory — never block */ }
224
+
207
225
  const { getActiveWorkstream } = require('./lib/planning-workspace.cjs');
208
226
  const { resolveActiveWorkstream, applyResolvedWorkstreamEnv } = require('./lib/active-workstream-store.cjs');
209
227
  const state = require('./lib/state.cjs');
210
228
  const phase = require('./lib/phase.cjs');
211
229
  const roadmap = require('./lib/roadmap.cjs');
230
+ // #1561 — assumption-delta advisory checkpoint detector (pure function).
231
+ const { detectAssumptionDelta } = require('./lib/assumption-delta.cjs');
212
232
  const verify = require('./lib/verify.cjs');
213
233
  const config = require('./lib/config.cjs');
214
234
  const template = require('./lib/template.cjs');
@@ -227,6 +247,10 @@ const evalMod = require('./lib/eval.cjs');
227
247
  const { routeVerificationCommand } = require('./lib/verification-command-router.cjs');
228
248
  const verification = require('./lib/verification.cjs');
229
249
  const { routeInitCommand } = require('./lib/init-command-router.cjs');
250
+ // Stale-bake guard (#1688): warns once when model config changed since agents
251
+ // were last baked on static-frontmatter runtimes (codex/opencode). Lazy-required
252
+ // here, invoked from case 'init' below.
253
+ const { warnIfStaleBake } = require('./lib/stale-bake-guard.cjs');
230
254
  const loopResolver = require('./lib/loop-resolver.cjs');
231
255
  const capabilityState = require('./lib/capability-state.cjs');
232
256
  const capabilityWriter = require('./lib/capability-writer.cjs');
@@ -635,7 +659,7 @@ async function main() {
635
659
  // discovery; previously it was a partial subset that didn't include
636
660
  // phase / roadmap / milestone / progress / etc.
637
661
  const TOP_LEVEL_USAGE = 'Usage: gsd-tools <command> [args] [--raw] [--pick <field>] [--cwd <path>] [--ws <name>] [--json-errors]\n' +
638
- 'Commands: agent, agent-skills, audit-open, audit-uat, check, check-commit, commit, commit-to-subrepo, pr-subrepo, ' +
662
+ 'Commands: agent, agent-skills, assumption-delta, audit-open, audit-uat, check, check-commit, commit, commit-to-subrepo, pr-subrepo, ' +
639
663
  'config-ensure-section, config-get, config-new-project, config-path, config-set, migrate-config, ' +
640
664
  'current-timestamp, detect-custom-files, docs-init, drift-guard, effort, extract-messages, find-phase, ' +
641
665
  'from-gsd2, frontmatter, gap-analysis, generate-claude-md, generate-claude-profile, ' +
@@ -1234,6 +1258,56 @@ async function runCommand(command, args, cwd, raw, defaultValue, originalCommand
1234
1258
  break;
1235
1259
  }
1236
1260
 
1261
+ case 'dispatch-should-flatten': {
1262
+ // #1708 / #853: typed query replacing the `RUNTIME === 'codex'` prose rule.
1263
+ //
1264
+ // Resolves the current runtime (GSD_RUNTIME > config.runtime > 'claude'),
1265
+ // looks up registry.runtimes[id].runtime.hostIntegration.dispatch, and
1266
+ // calls shouldFlattenDispatch(dispatch) from host-integration.cjs.
1267
+ //
1268
+ // Fail-closed: any unknown runtime, missing dispatch, or thrown error
1269
+ // yields `true` (inline — the always-safe default).
1270
+ //
1271
+ // Output:
1272
+ // --raw → prints exactly `true` or `false`
1273
+ // --json → prints { runtime, shouldFlatten, dispatch }
1274
+ // default → same as --raw
1275
+ try {
1276
+ // Resolve runtime using the same precedence as `config-get runtime`.
1277
+ const { resolveRuntime } = require('./lib/runtime-slash.cjs');
1278
+ const runtimeId = resolveRuntime(cwd);
1279
+
1280
+ // Look up dispatch from the capability registry.
1281
+ const registry = require('./lib/capability-registry.cjs');
1282
+ const runtimeEntry = registry.runtimes != null
1283
+ ? registry.runtimes[runtimeId]
1284
+ : null;
1285
+ const dispatch = runtimeEntry?.runtime?.hostIntegration?.dispatch ?? null;
1286
+
1287
+ // Call shouldFlattenDispatch from host-integration.cjs.
1288
+ const hostIntegration = require('./lib/host-integration.cjs');
1289
+ const shouldFlat = dispatch !== null
1290
+ ? hostIntegration.shouldFlattenDispatch(dispatch)
1291
+ : true; // fail-closed: unknown runtime → inline
1292
+
1293
+ const jsonIdx = args.indexOf('--json');
1294
+ if (jsonIdx !== -1) {
1295
+ output({
1296
+ runtime: runtimeId,
1297
+ shouldFlatten: shouldFlat,
1298
+ dispatch: dispatch,
1299
+ }, raw);
1300
+ } else {
1301
+ // --raw or default: print exactly true or false
1302
+ process.stdout.write(shouldFlat ? 'true' : 'false');
1303
+ }
1304
+ } catch {
1305
+ // Fail-closed on any error: inline is always safe.
1306
+ process.stdout.write('true');
1307
+ }
1308
+ break;
1309
+ }
1310
+
1237
1311
  case 'config-new-project': {
1238
1312
  // Phase 6 (#3575): dispatch via SDK executeForCjs when available.
1239
1313
  const handled = _dispatchNonFamily({
@@ -1310,6 +1384,43 @@ async function runCommand(command, args, cwd, raw, defaultValue, originalCommand
1310
1384
  break;
1311
1385
  }
1312
1386
 
1387
+ case 'assumption-delta': {
1388
+ // #1561 — advisory architecture checkpoint. `scan <phase>` reads the
1389
+ // phase section via the same resolver as roadmap.get-phase and runs the
1390
+ // deterministic detectAssumptionDelta, emitting the typed IR as JSON.
1391
+ const sub = args[1];
1392
+ if (sub === 'scan') {
1393
+ const phaseNum = args[2];
1394
+ // Reject missing or flag-shaped phase values (QA matrix: values that
1395
+ // look like flags). `scan --json` must not treat "--json" as a phase.
1396
+ if (!phaseNum || phaseNum.startsWith('-')) {
1397
+ error('Usage: assumption-delta scan <phase> [--terms <csv>]', ERROR_REASON.SDK_UNKNOWN_COMMAND);
1398
+ break;
1399
+ }
1400
+ // Optional --terms <csv> override (replaces the pluralization cues;
1401
+ // optional/chosen keep defaults). An EMPTY value ("") or a flag-shaped
1402
+ // value restores the curated defaults (does NOT disable pluralization).
1403
+ // Terms are normalized (deduped, alphanumeric-only, capped) by
1404
+ // detectAssumptionDelta's resolveTerms.
1405
+ let termsOverride;
1406
+ const termsIdx = args.indexOf('--terms');
1407
+ const termsVal = termsIdx !== -1 ? args[termsIdx + 1] : undefined;
1408
+ if (typeof termsVal === 'string' && !termsVal.startsWith('-')) {
1409
+ const list = termsVal
1410
+ .split(',')
1411
+ .map((t) => t.trim().toLowerCase())
1412
+ .filter((t) => t.length > 0);
1413
+ termsOverride = list.length > 0 ? { pluralization: list } : undefined;
1414
+ }
1415
+ const section = roadmap.getRoadmapPhaseWithFallback(cwd, phaseNum);
1416
+ const result = detectAssumptionDelta(section ?? '', termsOverride);
1417
+ output(result, raw);
1418
+ break;
1419
+ }
1420
+ error(`Unknown assumption-delta subcommand: ${sub}. Available: scan`, ERROR_REASON.SDK_UNKNOWN_COMMAND);
1421
+ break;
1422
+ }
1423
+
1313
1424
  case 'requirements': {
1314
1425
  const subcommand = args[1];
1315
1426
  if (subcommand === 'mark-complete') {
@@ -1342,7 +1453,9 @@ async function runCommand(command, args, cwd, raw, defaultValue, originalCommand
1342
1453
  const subcommand = args[1];
1343
1454
  if (subcommand === 'complete') {
1344
1455
  const milestoneName = parseMultiwordArg(args, 'name');
1345
- const archivePhases = args.includes('--archive-phases');
1456
+ // #1871: archive phase dirs by default on milestone complete so the next
1457
+ // new-milestone never inherits un-archived dirs. --no-archive-phases opts out.
1458
+ const archivePhases = !args.includes('--no-archive-phases');
1346
1459
  const force = args.includes('--force');
1347
1460
  milestone.cmdMilestoneComplete(cwd, args[2], { name: milestoneName, archivePhases, force }, raw);
1348
1461
  } else {
@@ -1414,6 +1527,10 @@ async function runCommand(command, args, cwd, raw, defaultValue, originalCommand
1414
1527
  }
1415
1528
 
1416
1529
  case 'init': {
1530
+ // #1688: warn (at most once per process) if the user edited model_overrides
1531
+ // without re-running `gsd install <runtime>` on a static-frontmatter runtime.
1532
+ // Best-effort, stderr-only, swallowed errors — never blocks the command.
1533
+ try { warnIfStaleBake(cwd); } catch { /* guard must never break init */ }
1417
1534
  routeInitCommand({
1418
1535
  init,
1419
1536
  args,
@@ -1550,13 +1667,22 @@ async function runCommand(command, args, cwd, raw, defaultValue, originalCommand
1550
1667
  return undefined;
1551
1668
  };
1552
1669
  // Running GSD version (hard gate for engines.gsd at install/load); fail-closed to 0.0.0.
1670
+ // #1920: prefer the authoritative gsd-core/VERSION the installer writes for EVERY runtime
1671
+ // (gsd-core/bin/ -> ../VERSION), so installed layouts report the true version even when the
1672
+ // walked-up ../../package.json is the versionless CommonJS marker or the user's own project.
1673
+ // Fall back to the runtime-root package.json (dev/source tree), then fail-closed. Mirrors
1674
+ // readHostVersion() in capability-loader.cts.
1553
1675
  const capHostVersion = () => {
1676
+ const SEMVER_PREFIX = /^\d+\.\d+\.\d+/;
1554
1677
  try {
1555
- const pkg = require('../../package.json'); // gsd-core/bin/ -> repo root is two up
1556
- return typeof pkg.version === 'string' && pkg.version ? pkg.version : '0.0.0';
1557
- } catch {
1558
- return '0.0.0';
1559
- }
1678
+ const v = fs.readFileSync(path.join(__dirname, '..', 'VERSION'), 'utf8').trim();
1679
+ if (SEMVER_PREFIX.test(v)) return v;
1680
+ } catch { /* not an installed tree (no gsd-core/VERSION) */ }
1681
+ try {
1682
+ const pkg = require(path.join(__dirname, '..', '..', 'package.json')); // gsd-core/bin/ -> repo root is two up
1683
+ if (pkg && typeof pkg.version === 'string' && SEMVER_PREFIX.test(pkg.version)) return pkg.version;
1684
+ } catch { /* runtime root has no package.json */ }
1685
+ return '0.0.0';
1560
1686
  };
1561
1687
  // #1459: the USER-OWNED consent home (GSD_HOME||homedir()) where project-scope consent records
1562
1688
  // live — OUTSIDE any repo. SAME rule as the loader/consent-store path resolution so a record
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Declarative embedding adapter (ADR-1239 Phase C-1, AC1 / #1680).
3
+ *
4
+ * NAMES + BOUNDS today's projection path (file emission via install-engine)
5
+ * behind `HostIntegrationInterface`. The declarative adapter is lossy by
6
+ * design: it projects skills/agents/commands and does NOT drive the loop
7
+ * orchestration (that is the imperative adapter's job, AC2 / a later slice).
8
+ *
9
+ * `install`/`uninstall` delegate IN-PROCESS to install-engine's
10
+ * `installRuntimeArtifacts` / `uninstallRuntimeArtifacts` — the SAME engine
11
+ * functions `bin/install.js` uses — so the adapter's output is byte-identical to
12
+ * today's install (the link is gated by `tests/golden-install-parity.test.cjs`).
13
+ * The module-ref call style (`installEngine.fn`) keeps it monkeypatch-friendly
14
+ * for tests, mirroring the install-engine.cts:31-38 pattern.
15
+ */
16
+ 'use strict';
17
+ Object.defineProperty(exports, "__esModule", { value: true });
18
+ exports.createDeclarativeAdapter = createDeclarativeAdapter;
19
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
20
+ const installEngine = require("./install-engine.cjs");
21
+ function createDeclarativeAdapter({ runtime }) {
22
+ if (!runtime || typeof runtime !== 'string') {
23
+ throw new TypeError('createDeclarativeAdapter: runtime is required (non-empty string)');
24
+ }
25
+ return Object.freeze({
26
+ kind: 'declarative',
27
+ runtime,
28
+ install(intent) {
29
+ installEngine.installRuntimeArtifacts(runtime, intent.configDir, intent.scope, intent.resolvedProfile, intent.resolveAttribution);
30
+ },
31
+ uninstall(intent) {
32
+ installEngine.uninstallRuntimeArtifacts(runtime, intent.configDir, intent.scope);
33
+ },
34
+ });
35
+ }
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Imperative embedding adapter (ADR-1239 Phase C-1, AC2 / #1680).
3
+ *
4
+ * The engine-as-library path: an in-process host plugin calls
5
+ * `createImperativeAdapter({runtime})`, which composes the capability registry
6
+ * via `loadRegistry({includeInstalled:true})` (first-party-wins + consent +
7
+ * fail-closed gates) and binds the engine surface behind the SAME
8
+ * `HostIntegrationInterface` the declarative adapter satisfies. The adapter
9
+ * stays thin — it does NOT reimplement the loop resolver; it delegates to the
10
+ * engine + exposes the composed registry so a host (Phase 5) can bind its
11
+ * primitives (command/dispatch/model/hooks/state/artifact) to the registry's
12
+ * declared capability set.
13
+ *
14
+ * Concrete host binding (OpenCode/VS Code/pi) is deferred to Phase 5 (#1682,
15
+ * D15/D18). This slice ships the adapter + the composed-registry seam.
16
+ *
17
+ * Minimal interface (per ADR-1239 open wire-shape question): satisfies the
18
+ * same `{kind, runtime, install, uninstall}` shape as the declarative adapter,
19
+ * plus an imperative-specific `registry` accessor (the composed loadRegistry
20
+ * result). The full 6-point binding surface grows when a real host consumer
21
+ * fixes the shape.
22
+ */
23
+ 'use strict';
24
+ Object.defineProperty(exports, "__esModule", { value: true });
25
+ exports.createImperativeAdapter = createImperativeAdapter;
26
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
27
+ const installEngine = require("./install-engine.cjs");
28
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
29
+ const capabilityLoader = require("./capability-loader.cjs");
30
+ function createImperativeAdapter({ runtime }, options = {}) {
31
+ if (!runtime || typeof runtime !== 'string') {
32
+ throw new TypeError('createImperativeAdapter: runtime is required (non-empty string)');
33
+ }
34
+ // Compose first-party ∪ installed capability overlays with the SAME
35
+ // precedence, consent, and fail-closed-gate guarantees the CLI enforces —
36
+ // an in-process host gets identical trust semantics, not a parallel path.
37
+ const registry = capabilityLoader.loadRegistry({
38
+ includeInstalled: true,
39
+ ...(options.loadOptions ?? {}),
40
+ });
41
+ return Object.freeze({
42
+ kind: 'imperative',
43
+ runtime,
44
+ registry,
45
+ install(intent) {
46
+ installEngine.installRuntimeArtifacts(runtime, intent.configDir, intent.scope, intent.resolvedProfile, intent.resolveAttribution);
47
+ },
48
+ uninstall(intent) {
49
+ installEngine.uninstallRuntimeArtifacts(runtime, intent.configDir, intent.scope);
50
+ },
51
+ });
52
+ }
@@ -0,0 +1,231 @@
1
+ "use strict";
2
+ /**
3
+ * Assumption-Delta detector (#1561).
4
+ *
5
+ * A rarely-firing, advisory architecture checkpoint. When a phase makes
6
+ * something PLURAL / OPTIONAL / CHOSEN that used to be SINGULAR / REQUIRED /
7
+ * DERIVED, the primary key / identity model may silently stop matching the
8
+ * generalized intent. This detector scans phase-scope prose for the linguistic
9
+ * signals of that transition so the plan:pre capability hook (see
10
+ * capabilities/assumption-delta/) can surface ONE identity-model question.
11
+ *
12
+ * Design notes (rubber-duck'd):
13
+ * - DETERMINISTIC + TYPED IR. The "does it fire?" decision is a pure function
14
+ * returning { detected, signals, terms }, not an LLM judgment — so the
15
+ * low-false-positive guarantee (acceptance criterion #2) is testable.
16
+ * - BARE "or" IS INTENTIONALLY EXCLUDED from the default pluralization cues.
17
+ * The issue lists "or" as a tell, but bare "or" is extremely common in
18
+ * English prose and would make the gate fire on nearly every phase
19
+ * description. Pluralization requires a stronger second-case cue
20
+ * (second / alternative / fallback / additional / ...). The vocabulary is
21
+ * tunable (config + the `terms` parameter) so teams can widen it.
22
+ * - FENCED CODE BLOCKS ARE STRIPPED first (via the markdown-sectionizer seam)
23
+ * so a trigger term that appears only inside a code snippet does not fire.
24
+ * - Mirrors ui-safety-gate.cts: a pure function + a STDIN-reading CLI whose
25
+ * exit codes mirror grep (0 = signal found, 1 = none, 2 = usage error).
26
+ *
27
+ * Public API:
28
+ * detectAssumptionDelta(text, terms?) -> { detected, signals, terms }
29
+ * DEFAULT_ASSUMPTION_DELTA_TERMS
30
+ *
31
+ * CLI:
32
+ * echo "$PHASE_SECTION" | node gsd-core/bin/lib/assumption-delta.cjs [--json]
33
+ * exit 0 = signal detected, 1 = none, 2 = startup error
34
+ * --json additionally prints the typed IR on stdout
35
+ */
36
+ Object.defineProperty(exports, "__esModule", { value: true });
37
+ exports.DEFAULT_ASSUMPTION_DELTA_TERMS = void 0;
38
+ exports.detectAssumptionDelta = detectAssumptionDelta;
39
+ const markdown_sectionizer_cjs_1 = require("./markdown-sectionizer.cjs");
40
+ /**
41
+ * Curated default trigger vocabulary. Each kind lists cue terms that signal a
42
+ * core-assumption monopoly has been lost. ADDITIVE-ONLY (Hyrum's Law: once
43
+ * shipped, this set is a depended-upon interface). Tunable via the `terms`
44
+ * parameter or the capability's config slice.
45
+ */
46
+ exports.DEFAULT_ASSUMPTION_DELTA_TERMS = {
47
+ // Primary trigger — a second X where there was one.
48
+ // Bare "or" excluded (prose-frequency false positives).
49
+ pluralization: [
50
+ 'second',
51
+ 'alternative',
52
+ 'alternate',
53
+ 'fallback',
54
+ 'also',
55
+ 'additional',
56
+ 'another',
57
+ 'supplementary',
58
+ 'alongside',
59
+ 'multiple',
60
+ 'plural',
61
+ '2nd',
62
+ ],
63
+ // required / `only` -> optional
64
+ optional: ['optional', 'optionally'],
65
+ // derived -> chosen / constant -> parameter
66
+ chosen: [
67
+ 'chosen',
68
+ 'choose',
69
+ 'selectable',
70
+ 'configurable',
71
+ 'parameterized',
72
+ 'parameterised',
73
+ 'parameterize',
74
+ 'parameterise',
75
+ 'custom',
76
+ ],
77
+ };
78
+ /** Hardening caps for the tunable term vocabulary (Codex review finding). */
79
+ const MAX_TERMS_PER_KIND = 200;
80
+ const MAX_TERM_LEN = 32;
81
+ /**
82
+ * Normalize a caller-provided term list: trim, lowercase, reject empties and
83
+ * punctuation-only terms (e.g. "-"), dedupe (preserve order), and cap the
84
+ * count/length so a huge or hostile `--terms` value cannot build a giant
85
+ * alternation regex or echo a massive payload. Defaults are already clean, so
86
+ * this is a no-op on them.
87
+ */
88
+ function normalizeTerms(list) {
89
+ if (!Array.isArray(list))
90
+ return [];
91
+ const seen = new Set();
92
+ const out = [];
93
+ for (const raw of list) {
94
+ if (typeof raw !== 'string')
95
+ continue;
96
+ const t = raw.trim().toLowerCase().slice(0, MAX_TERM_LEN);
97
+ // Require at least one alphanumeric char so punctuation-only terms like
98
+ // "-" cannot match prose punctuation as a "signal".
99
+ if (!t || !/[a-z0-9]/.test(t))
100
+ continue;
101
+ if (seen.has(t))
102
+ continue;
103
+ seen.add(t);
104
+ out.push(t);
105
+ if (out.length >= MAX_TERMS_PER_KIND)
106
+ break;
107
+ }
108
+ return out;
109
+ }
110
+ /**
111
+ * Resolve the effective term set: per-kind override. An explicitly-provided
112
+ * non-empty array for a kind REPLACES that kind's defaults (then normalized);
113
+ * an absent kind KEEPS its defaults. An explicitly-empty array disables that
114
+ * kind (override present, normalized to []). This lets a caller narrow one axis
115
+ * without re-declaring the others.
116
+ */
117
+ function resolveTerms(terms) {
118
+ const merge = (key) => {
119
+ const t = terms && terms[key];
120
+ return Array.isArray(t) ? normalizeTerms(t) : [...exports.DEFAULT_ASSUMPTION_DELTA_TERMS[key]];
121
+ };
122
+ return {
123
+ pluralization: merge('pluralization'),
124
+ optional: merge('optional'),
125
+ chosen: merge('chosen'),
126
+ };
127
+ }
128
+ /** Trim + collapse + truncate a context window around a match for the snippet. */
129
+ function makeSnippet(line, term) {
130
+ const cleaned = line.replace(/\s+/g, ' ').trim();
131
+ if (cleaned.length <= 120)
132
+ return cleaned;
133
+ // Centre the window on the matched term when the line is long.
134
+ const idx = cleaned.toLowerCase().indexOf(term);
135
+ if (idx < 0)
136
+ return cleaned.slice(0, 120);
137
+ const start = Math.max(0, idx - 50);
138
+ const end = Math.min(cleaned.length, idx + term.length + 50);
139
+ const prefix = start > 0 ? '…' : '';
140
+ const suffix = end < cleaned.length ? '…' : '';
141
+ return `${prefix}${cleaned.slice(start, end)}${suffix}`;
142
+ }
143
+ /**
144
+ * Detect assumption-delta signals in phase-scope prose.
145
+ *
146
+ * @param text - Roadmap phase section / scope prose. Non-string inputs degrade
147
+ * to `{ detected: false }` without throwing.
148
+ * @param terms - Optional per-kind override (see resolveTerms).
149
+ * @returns typed IR: { detected, signals[], terms }. `terms` is the effective
150
+ * (merged) set actually used, so callers/tests can audit what fired.
151
+ */
152
+ function detectAssumptionDelta(text, terms) {
153
+ if (typeof text !== 'string') {
154
+ return { detected: false, signals: [], terms: resolveTerms(terms) };
155
+ }
156
+ const effective = resolveTerms(terms);
157
+ // Strip fenced code blocks so trigger terms inside code snippets do not fire.
158
+ // stripFencedCode is CommonMark-correct and CRLF-safe.
159
+ const stripped = (0, markdown_sectionizer_cjs_1.stripFencedCode)(text.replace(/\r\n/g, '\n')).text;
160
+ if (stripped.trim().length === 0) {
161
+ return { detected: false, signals: [], terms: effective };
162
+ }
163
+ const signals = [];
164
+ const kinds = ['pluralization', 'optional', 'chosen'];
165
+ for (const kind of kinds) {
166
+ const cueTerms = effective[kind];
167
+ if (cueTerms.length === 0)
168
+ continue;
169
+ // Word-boundary anchored, case-insensitive — same shape as ui-safety-gate.
170
+ // (^|[^a-zA-Z0-9])(TERM)([^a-zA-Z0-9]|$) prevents interior-substring matches.
171
+ const escaped = cueTerms.map(escapeRegex).join('|');
172
+ const pattern = new RegExp('(^|[^a-zA-Z0-9])(' + escaped + ')([^a-zA-Z0-9]|$)', 'gi');
173
+ const seen = new Set();
174
+ for (const line of stripped.split('\n')) {
175
+ pattern.lastIndex = 0;
176
+ for (const m of line.matchAll(pattern)) {
177
+ const raw = m[2];
178
+ if (!raw)
179
+ continue;
180
+ const matched = raw.toLowerCase();
181
+ const key = `${kind}:${matched}`;
182
+ if (seen.has(key))
183
+ continue;
184
+ seen.add(key);
185
+ signals.push({ kind, term: matched, snippet: makeSnippet(line, matched) });
186
+ }
187
+ }
188
+ }
189
+ return { detected: signals.length > 0, signals, terms: effective };
190
+ }
191
+ function escapeRegex(s) {
192
+ return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
193
+ }
194
+ // ── CLI entry point ──────────────────────────────────────────────────────────
195
+ // Reads phase-section text from STDIN (not argv) to avoid OS ARG_MAX limits.
196
+ // Invoked by workflow bash as: echo "$PHASE_SECTION" | node .../assumption-delta.cjs [--json]
197
+ // Exit 0 = signal detected, 1 = none, 2 = startup error. Mirrors ui-safety-gate.
198
+ if (require.main === module) {
199
+ const argv = process.argv.slice(2);
200
+ const wantJson = argv.includes('--json');
201
+ // --terms <csv>: config-tunable vocabulary override. Replaces the
202
+ // pluralization cues (the primary trigger); optional/chosen keep defaults.
203
+ // An EMPTY value ("") or a flag-shaped value restores the curated defaults
204
+ // (does NOT disable pluralization). Terms are normalized (deduped, etc.) by
205
+ // detectAssumptionDelta's resolveTerms.
206
+ let termsOverride;
207
+ const termsIdx = argv.indexOf('--terms');
208
+ const termsVal = termsIdx !== -1 ? argv[termsIdx + 1] : undefined;
209
+ if (typeof termsVal === 'string' && !termsVal.startsWith('-')) {
210
+ const list = termsVal
211
+ .split(',')
212
+ .map((t) => t.trim().toLowerCase())
213
+ .filter((t) => t.length > 0);
214
+ termsOverride = list.length > 0 ? { pluralization: list } : undefined;
215
+ }
216
+ const chunks = [];
217
+ process.stdin.setEncoding('utf-8');
218
+ process.stdin.on('data', (chunk) => chunks.push(chunk));
219
+ process.stdin.on('end', () => {
220
+ const input = chunks.join('');
221
+ const result = detectAssumptionDelta(input, termsOverride);
222
+ if (wantJson) {
223
+ process.stdout.write(JSON.stringify(result) + '\n');
224
+ }
225
+ process.exit(result.detected ? 0 : 1);
226
+ });
227
+ process.stdin.on('error', (err) => {
228
+ process.stderr.write(`ERROR: assumption-delta.cjs stdin read failed: ${err.message}\n`);
229
+ process.exit(2);
230
+ });
231
+ }
@@ -33,7 +33,7 @@ const consentMod = require('./capability-consent.cjs');
33
33
  const projectRootMod = require('./project-root.cjs');
34
34
  // #1459 finding 4: the SHARED hardened lock primitive (single source of truth for lifecycle + consent).
35
35
  const lockMod = require('./capability-lock.cjs');
36
- const { platformWriteSync } = require('./shell-command-projection.cjs');
36
+ const { platformWriteSync, retryRenameSync } = require('./shell-command-projection.cjs');
37
37
  // #1463: numeric major.minor.patch comparison for the outdated check (the SAME compare the resolver
38
38
  // and capability list use). -1 (a<b), 0 (equal), 1 (a>b).
39
39
  const semverMod = require('./semver-compare.cjs');
@@ -369,16 +369,16 @@ function promoteStagingToFinal(stagingDir, finalDir, backupName) {
369
369
  ? node_path_1.default.join(parent, backupName)
370
370
  // CONC-3: a random nonce in the unnamed-branch backup name prevents same-ms cross-process collision.
371
371
  : node_path_1.default.join(parent, newBackupName(node_path_1.default.basename(finalDir)));
372
- node_fs_1.default.renameSync(finalDir, backupDir);
372
+ retryRenameSync(finalDir, backupDir);
373
373
  // DUR-3: fsync the parent dir so the old→backup rename is durable BEFORE the second rename —
374
374
  // a crash here must not lose the backup (the only recovery path for reconcile).
375
375
  fsyncDir(parent);
376
376
  try {
377
- node_fs_1.default.renameSync(stagingDir, finalDir);
377
+ retryRenameSync(stagingDir, finalDir);
378
378
  }
379
379
  catch (err) {
380
380
  try {
381
- node_fs_1.default.renameSync(backupDir, finalDir);
381
+ retryRenameSync(backupDir, finalDir);
382
382
  }
383
383
  catch { /* best-effort restore */ }
384
384
  throw err;
@@ -388,7 +388,7 @@ function promoteStagingToFinal(stagingDir, finalDir, backupName) {
388
388
  return { backupDir };
389
389
  }
390
390
  node_fs_1.default.mkdirSync(parent, { recursive: true });
391
- node_fs_1.default.renameSync(stagingDir, finalDir);
391
+ retryRenameSync(stagingDir, finalDir);
392
392
  fsyncDir(parent); // DUR-3: durable fresh-install promotion.
393
393
  return { backupDir: null };
394
394
  }
@@ -1330,8 +1330,8 @@ function reconcileCapabilities(opts) {
1330
1330
  // - crash after step (b): old bundle live at finalDir; only the aside copy leaks → swept.
1331
1331
  const discard = `${finalDir}.discard-${process.pid}-${Date.now()}-${node_crypto_1.default.randomBytes(4).toString('hex')}`;
1332
1332
  if (node_fs_1.default.existsSync(finalDir))
1333
- node_fs_1.default.renameSync(finalDir, discard); // (a) set the new dir aside
1334
- node_fs_1.default.renameSync(backupDir, finalDir); // (b) restore the old bundle
1333
+ retryRenameSync(finalDir, discard); // (a) set the new dir aside
1334
+ retryRenameSync(backupDir, finalDir); // (b) restore the old bundle
1335
1335
  fsyncDir(root); // make the restore durable
1336
1336
  try {
1337
1337
  node_fs_1.default.rmSync(discard, { recursive: true, force: true });