@bridge_gpt/mcp-server 0.2.67 → 0.2.69

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 (127) hide show
  1. package/README.md +272 -51
  2. package/build/agent-launchers/claude-executor-adapter.js +27 -0
  3. package/build/agents.generated.js +22 -2
  4. package/build/bitbucket-workspace-remote.js +136 -0
  5. package/build/bootstrap-project-identity.js +24 -0
  6. package/build/bridge-permissions.js +1362 -0
  7. package/build/check-update-cli.js +74 -0
  8. package/build/claude-settings.js +102 -2
  9. package/build/cli-help.js +27 -0
  10. package/build/codex-home.js +9 -1
  11. package/build/codex-launcher-pin.js +598 -0
  12. package/build/command-assets-doctor.js +24 -70
  13. package/build/command-provisioning.js +29 -5
  14. package/build/commands.generated.js +9 -9
  15. package/build/conductor/bridge-api-client.js +714 -0
  16. package/build/conductor/command-drift-production.js +92 -0
  17. package/build/conductor/command-drift.js +89 -0
  18. package/build/conductor/doctor.js +69 -26
  19. package/build/conductor/epic-branch-ci-inspection.js +1375 -0
  20. package/build/conductor/epic-implementer-cli.js +76 -22
  21. package/build/conductor/gate-discovery.js +890 -0
  22. package/build/conductor/install-doctor.js +15 -3
  23. package/build/conductor/json-output.js +63 -0
  24. package/build/conductor/readiness-cli.js +95 -10
  25. package/build/conductor/readiness-sections.js +369 -3
  26. package/build/conductor/readiness.js +211 -8
  27. package/build/conductor/recovery-cli.js +110 -4
  28. package/build/conductor/recovery-operations.js +99 -1
  29. package/build/connect-bitbucket-api.js +41 -6
  30. package/build/connect-bitbucket.js +106 -33
  31. package/build/connect-github-api.js +33 -14
  32. package/build/credential-store.js +207 -47
  33. package/build/docs.generated.js +57 -3
  34. package/build/doctor.js +198 -4
  35. package/build/drive-epic.js +45 -8
  36. package/build/epic-coherence-input.test-fixtures.js +4 -3
  37. package/build/epic-implementer-bin.js +1369 -25
  38. package/build/epic-workarounds-path.js +39 -0
  39. package/build/executor/cli.js +30 -0
  40. package/build/executor/completion-contract.js +20 -5
  41. package/build/executor/deps.js +4 -1
  42. package/build/executor/gate-qualification-job.js +499 -0
  43. package/build/executor/guarded-fast-forward-push.js +129 -0
  44. package/build/executor/http-client.js +43 -4
  45. package/build/executor/integration-job.js +122 -8
  46. package/build/executor/job-errors.js +8 -1
  47. package/build/executor/job-runner.js +589 -47
  48. package/build/executor/likely-tests.js +290 -0
  49. package/build/executor/live-worker-registry.js +1 -1
  50. package/build/executor/observation.js +31 -2
  51. package/build/executor/packaged-recipe.js +106 -0
  52. package/build/executor/permissions.js +115 -28
  53. package/build/executor/platform.js +1 -1
  54. package/build/executor/preflight.js +61 -12
  55. package/build/executor/process-containment.js +36 -12
  56. package/build/executor/process.js +15 -7
  57. package/build/executor/prompt-spec.js +33 -1
  58. package/build/executor/recently-failed-tests-file.js +71 -0
  59. package/build/executor/reconcile-overlap-job.js +41 -23
  60. package/build/executor/required-verification.js +1 -1
  61. package/build/executor/required-work-ledger.js +105 -17
  62. package/build/executor/retirement.js +69 -0
  63. package/build/executor/runner.js +114 -11
  64. package/build/executor/test-clock.js +15 -0
  65. package/build/executor/types.js +32 -0
  66. package/build/executor/viewer-tabs.js +58 -8
  67. package/build/executor/worker-command.js +23 -7
  68. package/build/executor/worker-diagnostics.js +37 -0
  69. package/build/executor/worker-finalization.js +31 -5
  70. package/build/executor/worker-guard-hook.js +179 -25
  71. package/build/executor/worker-log.js +4 -3
  72. package/build/executor/worker-verification-guidance.js +37 -0
  73. package/build/executor/worktree.js +88 -11
  74. package/build/first-build-advisory.js +97 -0
  75. package/build/generated-assets-advisory.js +14 -0
  76. package/build/generated-assets-catalog.js +66 -0
  77. package/build/generated-assets-context.js +46 -0
  78. package/build/generated-assets-dependencies.js +26 -0
  79. package/build/generated-assets-doctor.js +50 -0
  80. package/build/generated-assets-health.js +65 -0
  81. package/build/generated-assets-host.js +17 -0
  82. package/build/generated-assets-inventory.js +17 -0
  83. package/build/generated-assets-paths.js +60 -0
  84. package/build/generated-assets-publisher.js +133 -0
  85. package/build/generated-assets-remedy.js +25 -0
  86. package/build/generated-assets-repair.js +72 -0
  87. package/build/generated-assets-session.js +71 -0
  88. package/build/generated-assets-test-support.js +35 -0
  89. package/build/index.js +1109 -695
  90. package/build/init.js +54 -177
  91. package/build/install-bridge-conductor-gate.js +1372 -0
  92. package/build/install-bridge-conductor.js +289 -36
  93. package/build/install-bridge.js +722 -252
  94. package/build/launcher-config-inspection.js +184 -19
  95. package/build/mcp-host-targets.js +4 -0
  96. package/build/mcp-identity.js +7 -0
  97. package/build/mcp-install-state.js +50 -4
  98. package/build/merge-workflow-install.js +3 -4
  99. package/build/pipeline-utils.js +6 -2
  100. package/build/pipelines.generated.js +22 -142
  101. package/build/plan-execution-ledger.js +2 -32
  102. package/build/plane/manifest.js +16 -1
  103. package/build/plane/preflight.js +54 -1
  104. package/build/printed-remedy-commands.js +16 -1
  105. package/build/readme.generated.js +1 -1
  106. package/build/repair-assets.js +93 -0
  107. package/build/run-unit-tests-launcher.js +130 -49
  108. package/build/secret-redaction.js +33 -0
  109. package/build/session-upgrade-notice.js +86 -0
  110. package/build/setup-epic.js +108 -3
  111. package/build/start-tickets-prereqs.js +70 -16
  112. package/build/start-tickets.js +44 -5
  113. package/build/tool-call-advisory.js +162 -0
  114. package/build/tool-surface-gating.js +32 -57
  115. package/build/update-check.js +97 -16
  116. package/build/update-status.js +61 -71
  117. package/build/upgrade-advice.js +15 -11
  118. package/build/upgrade-cli.js +398 -17
  119. package/build/version.generated.js +3 -3
  120. package/build/worker-guard-hook-bin.js +3 -2
  121. package/build/worker-guard-hook-main.js +1 -1
  122. package/build/worktree-core.js +15 -2
  123. package/docs/EPIC-IMPLEMENTER-INSTALL.md +30 -12
  124. package/docs/EPIC-IMPLEMENTER.md +68 -2
  125. package/package.json +3 -3
  126. package/pipelines/greenfield-setup.json +4 -128
  127. package/pipelines/implement-ticket.json +3 -2
@@ -22,20 +22,16 @@
22
22
  * carries config bodies, `env` maps, credential values, or raw error text.
23
23
  */
24
24
  import path from "path";
25
- import { getProjectJsonTargets, hostAdapterForTarget } from "./mcp-host-targets.js";
26
- import { LEGACY_SERVER_NAMES, MCP_SERVER_NAME, MCP_PACKAGE_NAME, resolveRegistrationKey, } from "./mcp-identity.js";
25
+ import { codexHome, isCodexHomeOverrideHonored } from "./codex-home.js";
26
+ import { inspectCodexLauncher } from "./codex-launcher-pin.js";
27
+ import { getProjectJsonTargets, hostAdapterForTarget, MCP_HOST_TARGETS, } from "./mcp-host-targets.js";
28
+ import { LEGACY_SERVER_NAMES, MCP_SERVER_NAME, MCP_PACKAGE_NAME, resolveRegistrationKey, WORKTREE_SHIM_TOKEN, } from "./mcp-identity.js";
27
29
  /**
28
30
  * The npm package every Bridge launcher spec names. Retained as a compatibility
29
31
  * alias — `doctor.ts` still imports this name — assigned from the shared
30
32
  * constant so there is no duplicate package-name literal.
31
33
  */
32
34
  export const BRIDGE_PACKAGE_NAME = MCP_PACKAGE_NAME;
33
- /**
34
- * The subcommand token that marks a worktree shim registration. A shim points at
35
- * a sibling worktree's server rather than a published package, so no pin applies
36
- * and no rewrite is ever safe (BAPI-714 / worktree provisioning).
37
- */
38
- const WORKTREE_SHIM_TOKEN = "mcp-invoke";
39
35
  /**
40
36
  * Every project-local MCP config Bridge registers into. Exported so `doctor`,
41
37
  * `init`, and `upgrade` can no longer drift apart on which paths exist or which
@@ -155,6 +151,10 @@ const REASON_TEXT = {
155
151
  "malformed-version": "the launcher pin is not a valid MAJOR.MINOR.PATCH release",
156
152
  "pinned-ahead-of-target": "the launcher pin is newer than the target version and must not be downgraded",
157
153
  "duplicate-registration": DUPLICATE_REGISTRATION_GUIDANCE,
154
+ // BAPI-1445: Codex's global TOML is read by a conservative scanner that refuses
155
+ // anything it cannot interpret with confidence. The wording is fixed so no file
156
+ // content, argument, or environment value can ever reach a diagnostic.
157
+ unlocatable: "the Codex Bridge launcher cannot be located safely; resolve its configuration manually, then re-run the upgrade",
158
158
  };
159
159
  /** Render a reason code as sanitized prose. */
160
160
  export function describeLauncherReason(reason) {
@@ -351,31 +351,38 @@ async function inspectOne(target, options) {
351
351
  reason: "no-package-token",
352
352
  };
353
353
  }
354
- const common = {
354
+ return {
355
355
  ...keyed,
356
356
  filePresent: true,
357
357
  bridgeEntryPresent: true,
358
358
  shape: "standard",
359
359
  spec: pin.spec,
360
+ ...classifyStandardPin(pin, targetVersion),
360
361
  };
362
+ }
363
+ /**
364
+ * Classify a parsed Bridge package token against the target version.
365
+ *
366
+ * Shared by the project JSON inspection and the global Codex inspection so a pin
367
+ * means the same thing wherever it was written. Order matters and is part of the
368
+ * contract: unpinned forms normalize, a non-exact specifier is refused, a
369
+ * non-release target is refused, a pin ahead of the target is never downgraded,
370
+ * and only then is the exact pin compared.
371
+ */
372
+ function classifyStandardPin(pin, targetVersion) {
361
373
  // Bare and explicit `@latest` are the two forms an exact-pin policy normalizes
362
374
  // rather than rejects — both mean "whatever npx resolves", which is exactly the
363
375
  // non-determinism BAPI-429 pinned away from.
364
376
  if (pin.specifier === null || pin.specifier === "latest") {
365
- return { ...common, action: "normalize-unpinned" };
377
+ return { action: "normalize-unpinned" };
366
378
  }
367
379
  if (!isExactVersion(pin.specifier)) {
368
- return {
369
- ...common,
370
- action: "unsupported",
371
- reason: classifyNonExactSpecifier(pin.specifier),
372
- };
380
+ return { action: "unsupported", reason: classifyNonExactSpecifier(pin.specifier) };
373
381
  }
374
382
  const comparison = compareExactVersions(pin.specifier, targetVersion);
375
383
  if (comparison === null) {
376
384
  // The TARGET is not an exact release. Refuse rather than guess.
377
385
  return {
378
- ...common,
379
386
  currentVersion: pin.specifier,
380
387
  action: "unsupported",
381
388
  reason: "malformed-version",
@@ -383,20 +390,18 @@ async function inspectOne(target, options) {
383
390
  }
384
391
  if (comparison > 0) {
385
392
  return {
386
- ...common,
387
393
  currentVersion: pin.specifier,
388
394
  action: "ahead",
389
395
  reason: "pinned-ahead-of-target",
390
396
  };
391
397
  }
392
398
  return {
393
- ...common,
394
399
  currentVersion: pin.specifier,
395
400
  action: comparison === 0 ? "already-current" : "repin",
396
401
  };
397
402
  }
398
403
  /** Classify why a non-exact specifier cannot be treated as a release pin. */
399
- function classifyNonExactSpecifier(specifier) {
404
+ export function classifyNonExactSpecifier(specifier) {
400
405
  if (/^[\^~]|^[<>=]/.test(specifier) || specifier.includes(" - ") || specifier.includes("||")) {
401
406
  return "version-range";
402
407
  }
@@ -410,3 +415,163 @@ function classifyNonExactSpecifier(specifier) {
410
415
  function isPlainObject(value) {
411
416
  return typeof value === "object" && value !== null && !Array.isArray(value);
412
417
  }
418
+ /**
419
+ * Resolve Codex's global config location through {@link codexHome}.
420
+ *
421
+ * `$CODEX_HOME/config.toml` is shown only when an absolute override was actually
422
+ * honored — even if it happens to equal the default location — so a relative or
423
+ * blank override falls back to the default display exactly as `codexHome` falls
424
+ * back to the default path.
425
+ */
426
+ export function resolveCodexConfigLocation(env, homedir) {
427
+ return {
428
+ filePath: path.join(codexHome(env, homedir), "config.toml"),
429
+ displayPath: isCodexHomeOverrideHonored(env)
430
+ ? "$CODEX_HOME/config.toml"
431
+ : MCP_HOST_TARGETS.codex.displayPath,
432
+ };
433
+ }
434
+ /**
435
+ * Inspect Codex's global Bridge launcher, read-only, with exactly one read.
436
+ *
437
+ * Only `ENOENT` is absence. Any other read failure is a blocking
438
+ * `file-unreadable`, and the exception text is discarded: it can carry an
439
+ * absolute path. After the scan, the launcher is classified with the same shared
440
+ * pin rules as the project JSON targets.
441
+ */
442
+ export async function inspectGlobalLauncherConfigs(options) {
443
+ const { targetVersion } = options;
444
+ const { filePath, displayPath } = resolveCodexConfigLocation(options.env, options.homedir);
445
+ const base = {
446
+ hostId: "codex",
447
+ scope: "global",
448
+ displayPath,
449
+ targetVersion,
450
+ spec: null,
451
+ currentVersion: null,
452
+ registrationKey: null,
453
+ legacyRegistration: false,
454
+ };
455
+ let raw;
456
+ try {
457
+ raw = await options.readFile(filePath);
458
+ }
459
+ catch (err) {
460
+ const code = err && typeof err === "object" ? err.code : undefined;
461
+ if (code === "ENOENT") {
462
+ return {
463
+ ...base,
464
+ filePresent: false,
465
+ bridgeEntryPresent: false,
466
+ shape: "none",
467
+ action: "skip-inactive",
468
+ };
469
+ }
470
+ return {
471
+ ...base,
472
+ filePresent: true,
473
+ bridgeEntryPresent: false,
474
+ shape: "none",
475
+ action: "invalid",
476
+ reason: "file-unreadable",
477
+ };
478
+ }
479
+ const found = inspectCodexLauncher(raw);
480
+ switch (found.kind) {
481
+ case "none":
482
+ // A readable file with no Bridge declaration: the user never configured
483
+ // Bridge in Codex, so it is not a participant.
484
+ return {
485
+ ...base,
486
+ filePresent: true,
487
+ bridgeEntryPresent: false,
488
+ shape: "none",
489
+ action: "skip-inactive",
490
+ };
491
+ case "duplicate":
492
+ return {
493
+ ...base,
494
+ filePresent: true,
495
+ bridgeEntryPresent: true,
496
+ shape: "custom",
497
+ action: "duplicate-registration",
498
+ reason: "duplicate-registration",
499
+ };
500
+ case "unlocatable":
501
+ return {
502
+ ...base,
503
+ filePresent: true,
504
+ bridgeEntryPresent: true,
505
+ shape: "custom",
506
+ action: "invalid",
507
+ reason: "unlocatable",
508
+ };
509
+ case "worktree-shim":
510
+ return {
511
+ ...base,
512
+ filePresent: true,
513
+ bridgeEntryPresent: true,
514
+ registrationKey: found.registrationName,
515
+ legacyRegistration: found.registrationName !== MCP_SERVER_NAME,
516
+ shape: "worktree-shim",
517
+ action: "skip-worktree-shim",
518
+ };
519
+ case "located": {
520
+ const keyed = {
521
+ ...base,
522
+ filePresent: true,
523
+ bridgeEntryPresent: true,
524
+ registrationKey: found.registrationName,
525
+ legacyRegistration: found.registrationName !== MCP_SERVER_NAME,
526
+ };
527
+ // Same order as the JSON targets: shim first (the parser already routed
528
+ // shims above), then the package token, then the shared pin rules.
529
+ const pin = parseLauncherPin(found.args);
530
+ if (!pin) {
531
+ return { ...keyed, shape: "custom", action: "invalid", reason: "unlocatable" };
532
+ }
533
+ const classified = classifyStandardPin(pin, targetVersion);
534
+ return {
535
+ ...keyed,
536
+ shape: "standard",
537
+ spec: pin.spec,
538
+ ...classified,
539
+ action: classified.action,
540
+ };
541
+ }
542
+ }
543
+ }
544
+ // ---------------------------------------------------------------------------
545
+ // Effective Claude Code pin (BAPI-1446)
546
+ // ---------------------------------------------------------------------------
547
+ /**
548
+ * The exact release the project's Claude Code launcher is pinned to, or null.
549
+ *
550
+ * Install uses this to pin the SessionStart upgrade-notice hook to the same build
551
+ * the project's MCP config launches. It answers null — never a guess from the
552
+ * running process, and never `@latest` — when the config is absent, unreadable,
553
+ * a worktree shim, a custom launcher, ambiguous (both registration keys), or
554
+ * pinned to anything other than one exact `MAJOR.MINOR.PATCH`. Read-only.
555
+ */
556
+ export async function discoverClaudeCodeEffectivePin(options) {
557
+ try {
558
+ const relPath = MCP_HOST_TARGETS["claude-code"].relPath;
559
+ const target = relPath ? launcherTargetFor(relPath) : undefined;
560
+ if (!target)
561
+ return null;
562
+ const [found] = await inspectLauncherConfigs({
563
+ cwd: options.cwd,
564
+ targetVersion: options.targetVersion,
565
+ readFile: options.readFile,
566
+ targets: [target],
567
+ });
568
+ if (!found || found.shape !== "standard" || !found.bridgeEntryPresent)
569
+ return null;
570
+ if (isPrerequisiteFailure(found))
571
+ return null;
572
+ return isExactVersion(found.currentVersion) ? found.currentVersion : null;
573
+ }
574
+ catch {
575
+ return null;
576
+ }
577
+ }
@@ -77,6 +77,7 @@ export const MCP_HOST_TARGETS = {
77
77
  launchAgent: "claude",
78
78
  capabilities: { mcpConfiguration: true, customCommands: true, worktreeProvisioning: true, sessionSpawning: true },
79
79
  writeStrategy: "vendor-first",
80
+ reloadHint: "run /mcp or restart Claude Code",
80
81
  // Claude Code is always a candidate default.
81
82
  detect: () => true,
82
83
  },
@@ -90,6 +91,7 @@ export const MCP_HOST_TARGETS = {
90
91
  launchAgent: "cursor-agent",
91
92
  capabilities: { mcpConfiguration: true, customCommands: true, worktreeProvisioning: true, sessionSpawning: true },
92
93
  writeStrategy: "direct",
94
+ reloadHint: "toggle bridge off and on in Cursor Settings → MCP, or reload the window",
93
95
  detect: (ctx) => ctx.exists(joinPath(ctx.cwd, ".cursor")) ||
94
96
  typeof ctx.env.CURSOR_TRACE_DIR === "string" &&
95
97
  ctx.env.CURSOR_TRACE_DIR.length > 0,
@@ -103,6 +105,7 @@ export const MCP_HOST_TARGETS = {
103
105
  hostKind: "vscode-json",
104
106
  capabilities: { mcpConfiguration: true, customCommands: true, worktreeProvisioning: false, sessionSpawning: false },
105
107
  writeStrategy: "direct",
108
+ reloadHint: "restart the bridge server (MCP: List Servers)",
106
109
  detect: (ctx) => ctx.exists(joinPath(ctx.cwd, ".vscode")),
107
110
  },
108
111
  codex: {
@@ -119,6 +122,7 @@ export const MCP_HOST_TARGETS = {
119
122
  // registration remains the ordinary interactive default.
120
123
  capabilities: { mcpConfiguration: true, customCommands: true, worktreeProvisioning: true, sessionSpawning: true },
121
124
  writeStrategy: "vendor-first",
125
+ reloadHint: "restart Codex",
122
126
  detect: (ctx) => ctx.exists(joinPath(ctx.homedir, ".codex")),
123
127
  },
124
128
  windsurf: {
@@ -18,6 +18,13 @@
18
18
  export const MCP_SERVER_NAME = "bridge";
19
19
  /** The current published npm package name for the MCP server. */
20
20
  export const MCP_PACKAGE_NAME = "@bridge_gpt/mcp-server";
21
+ /**
22
+ * The subcommand token that marks a worktree shim registration. A shim points at
23
+ * a sibling worktree's server rather than a published package, so no pin applies
24
+ * and no rewrite is ever safe (BAPI-714 / worktree provisioning). Shared by the
25
+ * JSON launcher inspection and the pure Codex TOML parser (BAPI-1445).
26
+ */
27
+ export const WORKTREE_SHIM_TOKEN = "mcp-invoke";
21
28
  /** The README resource URI served by the MCP server. */
22
29
  export const MCP_README_URI = `${MCP_SERVER_NAME}://readme`;
23
30
  /**
@@ -16,6 +16,7 @@
16
16
  * All filesystem access is through injected dependencies so the module is fully
17
17
  * unit-testable with in-memory fakes.
18
18
  */
19
+ import { validPackagedAssetInventory } from "./generated-assets-context.js";
19
20
  import { executorIdFromLaunchdFilename, executorIdFromSystemdFilename, } from "./executor/service-unit.js";
20
21
  import { HOST_PLATFORM_ORDER, isHostPlatformId, } from "./mcp-host-targets.js";
21
22
  /** Current on-disk schema version. */
@@ -217,8 +218,7 @@ export async function readMcpInstallState(cwd, deps) {
217
218
  catch (err) {
218
219
  if (isEnoent(err))
219
220
  return { status: "missing" };
220
- // A non-ENOENT read error is treated as missing state, not a crash.
221
- return { status: "missing" };
221
+ return { status: "unreadable" };
222
222
  }
223
223
  let parsed;
224
224
  try {
@@ -271,6 +271,9 @@ export async function readMcpInstallState(cwd, deps) {
271
271
  status: "valid",
272
272
  state: {
273
273
  version: MCP_INSTALL_STATE_VERSION,
274
+ ...(doc.packagedAssets === undefined ? {} : validPackagedAssetInventory(doc.packagedAssets)
275
+ ? { packagedAssets: doc.packagedAssets }
276
+ : { packagedAssetsStatus: (doc.packagedAssets && typeof doc.packagedAssets === "object" && doc.packagedAssets.version !== 1 ? "unsupported" : "invalid") }),
274
277
  selectedPlatforms: normalizePlatforms(rawPlatforms),
275
278
  projectConfigPaths: normalizeProjectPaths(projectConfigPaths),
276
279
  // Entries that are not generated-unit locators are DROPPED rather than
@@ -291,6 +294,7 @@ export function serializeMcpInstallState(state) {
291
294
  // Explicit key order — do not rely on object insertion order for stability.
292
295
  const ordered = {
293
296
  version: state.version,
297
+ ...(state.packagedAssets ? { packagedAssets: cleanAssetInventory(state.packagedAssets) } : {}),
294
298
  selectedPlatforms: state.selectedPlatforms,
295
299
  projectConfigPaths: state.projectConfigPaths,
296
300
  // Re-normalized at serialization time, not trusted from the input object:
@@ -315,10 +319,13 @@ export function serializeMcpInstallState(state) {
315
319
  */
316
320
  export async function writeMcpInstallState(cwd, input, deps) {
317
321
  const existing = await readMcpInstallState(cwd, deps);
322
+ if (!canPreserveAssetInventory(existing))
323
+ return { ok: false, error: "Existing installation state cannot be safely preserved." };
318
324
  const priorUnitPaths = existing.status === "valid" ? existing.state.executorServiceUnitPaths : [];
319
325
  const priorCapabilities = existing.status === "valid" ? (existing.state.capabilities ?? []) : [];
320
326
  const state = {
321
327
  version: MCP_INSTALL_STATE_VERSION,
328
+ ...(existing.status === "valid" && existing.state.packagedAssets ? { packagedAssets: existing.state.packagedAssets } : {}),
322
329
  selectedPlatforms: normalizePlatforms(input.selectedPlatforms),
323
330
  projectConfigPaths: normalizeProjectPaths(input.projectConfigPaths),
324
331
  executorServiceUnitPaths: normalizeExecutorServiceUnitPaths([
@@ -330,6 +337,11 @@ export async function writeMcpInstallState(cwd, input, deps) {
330
337
  // them (BAPI-775).
331
338
  capabilities: normalizeCapabilities(priorCapabilities),
332
339
  };
340
+ if (input.packagedAssets) {
341
+ if (!validPackagedAssetInventory(input.packagedAssets))
342
+ return { ok: false, error: "Invalid packaged-asset inventory." };
343
+ state.packagedAssets = cleanAssetInventory(input.packagedAssets);
344
+ }
333
345
  const finalPath = installStatePath(cwd);
334
346
  return persistMcpInstallState(cwd, state, finalPath, deps);
335
347
  }
@@ -351,8 +363,7 @@ async function persistMcpInstallState(cwd, state, finalPath, deps) {
351
363
  // Best-effort cleanup only; ignore secondary failures.
352
364
  }
353
365
  }
354
- const message = err instanceof Error ? err.message : String(err);
355
- return { ok: false, error: `failed to persist install state: ${message}` };
366
+ return { ok: false, error: "failed to persist install state" };
356
367
  }
357
368
  return { ok: true, path: finalPath, state };
358
369
  }
@@ -379,11 +390,14 @@ export async function recordInstalledProjectArtifact(cwd, relPath, deps) {
379
390
  };
380
391
  }
381
392
  const existing = await readMcpInstallState(cwd, deps);
393
+ if (!canPreserveAssetInventory(existing))
394
+ return { ok: false, error: "Existing installation state cannot be safely preserved." };
382
395
  const priorPlatforms = existing.status === "valid" ? existing.state.selectedPlatforms : [];
383
396
  const priorPaths = existing.status === "valid" ? existing.state.projectConfigPaths : [];
384
397
  const priorUnitPaths = existing.status === "valid" ? existing.state.executorServiceUnitPaths : [];
385
398
  const state = {
386
399
  version: MCP_INSTALL_STATE_VERSION,
400
+ ...(existing.status === "valid" && existing.state.packagedAssets ? { packagedAssets: existing.state.packagedAssets } : {}),
387
401
  selectedPlatforms: normalizePlatforms(priorPlatforms),
388
402
  projectConfigPaths: normalizeProjectPaths([...priorPaths, relPath]),
389
403
  // Preserved, not rebuilt: recording a workflow file must not drop the
@@ -417,11 +431,14 @@ export async function recordInstalledExecutorServiceUnit(cwd, unitPath, homeDir,
417
431
  };
418
432
  }
419
433
  const existing = await readMcpInstallState(cwd, deps);
434
+ if (!canPreserveAssetInventory(existing))
435
+ return { ok: false, error: "Existing installation state cannot be safely preserved." };
420
436
  const priorPlatforms = existing.status === "valid" ? existing.state.selectedPlatforms : [];
421
437
  const priorPaths = existing.status === "valid" ? existing.state.projectConfigPaths : [];
422
438
  const priorUnitPaths = existing.status === "valid" ? existing.state.executorServiceUnitPaths : [];
423
439
  const state = {
424
440
  version: MCP_INSTALL_STATE_VERSION,
441
+ ...(existing.status === "valid" && existing.state.packagedAssets ? { packagedAssets: existing.state.packagedAssets } : {}),
425
442
  selectedPlatforms: normalizePlatforms(priorPlatforms),
426
443
  projectConfigPaths: normalizeProjectPaths(priorPaths),
427
444
  executorServiceUnitPaths: normalizeExecutorServiceUnitPaths([...priorUnitPaths, locator]),
@@ -453,12 +470,15 @@ export async function recordInstalledConductorCapability(cwd, capability, deps)
453
470
  };
454
471
  }
455
472
  const existing = await readMcpInstallState(cwd, deps);
473
+ if (!canPreserveAssetInventory(existing))
474
+ return { ok: false, error: "Existing installation state cannot be safely preserved." };
456
475
  const priorPlatforms = existing.status === "valid" ? existing.state.selectedPlatforms : [];
457
476
  const priorPaths = existing.status === "valid" ? existing.state.projectConfigPaths : [];
458
477
  const priorUnitPaths = existing.status === "valid" ? existing.state.executorServiceUnitPaths : [];
459
478
  const priorCapabilities = existing.status === "valid" ? (existing.state.capabilities ?? []) : [];
460
479
  const state = {
461
480
  version: MCP_INSTALL_STATE_VERSION,
481
+ ...(existing.status === "valid" && existing.state.packagedAssets ? { packagedAssets: existing.state.packagedAssets } : {}),
462
482
  selectedPlatforms: normalizePlatforms(priorPlatforms),
463
483
  projectConfigPaths: normalizeProjectPaths(priorPaths),
464
484
  executorServiceUnitPaths: normalizeExecutorServiceUnitPaths(priorUnitPaths),
@@ -466,3 +486,29 @@ export async function recordInstalledConductorCapability(cwd, capability, deps)
466
486
  };
467
487
  return persistMcpInstallState(cwd, state, installStatePath(cwd), deps);
468
488
  }
489
+ function canPreserveAssetInventory(existing) {
490
+ return existing.status === "missing" || (existing.status === "valid" && !existing.state.packagedAssetsStatus);
491
+ }
492
+ function cleanAssetInventory(inventory) {
493
+ if (!validPackagedAssetInventory(inventory))
494
+ throw new Error("Invalid packaged-asset inventory.");
495
+ return { version: 1, packageVersion: inventory.packageVersion, rootBinding: inventory.rootBinding,
496
+ assets: inventory.assets.map(a => ({ id: a.id, destination: a.destination, host: a.host })) };
497
+ }
498
+ export async function recordPackagedAssetInventory(cwd, inventory, deps) {
499
+ if (!validPackagedAssetInventory(inventory))
500
+ return { ok: false, error: "Invalid packaged-asset inventory." };
501
+ const prior = await readMcpInstallState(cwd, deps);
502
+ if (!canPreserveAssetInventory(prior))
503
+ return { ok: false, error: "Existing installation state cannot be safely preserved." };
504
+ const state = prior.status === "valid" ? prior.state : undefined;
505
+ const records = new Map();
506
+ if (state?.packagedAssets?.rootBinding === inventory.rootBinding) {
507
+ for (const a of state.packagedAssets.assets)
508
+ records.set(`${a.host}:${a.id}`, a);
509
+ }
510
+ for (const a of inventory.assets)
511
+ records.set(`${a.host}:${a.id}`, a);
512
+ return writeMcpInstallState(cwd, { selectedPlatforms: state?.selectedPlatforms ?? [],
513
+ projectConfigPaths: state?.projectConfigPaths ?? [], packagedAssets: { ...inventory, assets: [...records.values()] } }, deps);
514
+ }
@@ -43,6 +43,7 @@
43
43
  * remediation sentence, because subprocess text is unbounded and can carry a
44
44
  * remote URL with an embedded token.
45
45
  */
46
+ import { resolve } from "node:path";
46
47
  import { MERGE_WORKFLOW_EXPECTED_VERSION, MERGE_WORKFLOW_PATH, } from "./conductor/merge-execution-contract.js";
47
48
  import { MERGE_WORKFLOW_API_KEY_SECRET_NAME, } from "./merge-workflow-config.js";
48
49
  import { MERGE_WORKFLOW_CLASSIFICATION_TEXT, classifyMergeWorkflowContent, } from "./merge-workflow-drift.js";
@@ -247,10 +248,8 @@ export async function runMergeWorkflowPreflight(deps, options) {
247
248
  if (!repositoryRoot)
248
249
  return { ok: false, result: fail("not_repository_root") };
249
250
  const normalizedCwd = deps.cwd.endsWith("/") ? deps.cwd.slice(0, -1) : deps.cwd;
250
- const normalizedRoot = repositoryRoot.endsWith("/")
251
- ? repositoryRoot.slice(0, -1)
252
- : repositoryRoot;
253
- if (normalizedRoot !== normalizedCwd) {
251
+ const normalizedRoot = repositoryRoot.endsWith("/") ? repositoryRoot.slice(0, -1) : repositoryRoot;
252
+ if (resolve(normalizedRoot) !== resolve(normalizedCwd)) {
254
253
  return { ok: false, result: fail("not_repository_root") };
255
254
  }
256
255
  deps.log(` repository root: ${normalizedRoot}`);
@@ -178,7 +178,7 @@ export const UPGRADE_ADVICE_SURFACING_INSTRUCTIONS = " Upgrade advice: when any
178
178
  export function resolveRecipe(pipeline, instructions, variables, skipSteps, autoApprove, options) {
179
179
  // Validate required variables are provided
180
180
  const declared = pipeline.variables ?? [];
181
- const missing = declared.filter((v) => !(v in variables));
181
+ const missing = declared.filter((v) => v !== "verification_scope" && !(v in variables));
182
182
  if (missing.length > 0) {
183
183
  throw new Error(`Missing required variable(s): ${missing.join(", ")}. ` +
184
184
  `Pipeline "${pipeline.name}" declares: [${declared.join(", ")}].`);
@@ -189,7 +189,11 @@ export function resolveRecipe(pipeline, instructions, variables, skipSteps, auto
189
189
  // `execution_mode` must not be able to make a phase believe an orchestrator is
190
190
  // watching when none is.
191
191
  const executionMode = options?.executionMode ?? "inline";
192
- variables = { ...variables, [EXECUTION_MODE_VARIABLE]: executionMode };
192
+ variables = {
193
+ ...variables,
194
+ verification_scope: variables.verification_scope === "targeted" ? "targeted" : "full",
195
+ [EXECUTION_MODE_VARIABLE]: executionMode,
196
+ };
193
197
  // Filter and resolve steps
194
198
  const resolvedSteps = [];
195
199
  let stepIndex = 1;