@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
package/build/init.js CHANGED
@@ -13,13 +13,9 @@ import { writeFile, mkdir, readFile, stat } from "fs/promises";
13
13
  import { execFile } from "child_process";
14
14
  import path from "path";
15
15
  import os from "os";
16
- import { COMMANDS } from "./commands.generated.js";
17
- import { renderCommandForHost } from "./command-host.js";
18
- import { CODEX_SKILLS_DIR, codexSkillAssets } from "./codex-skill-adapter.js";
19
- import { AGENTS } from "./agents.generated.js";
20
- import { DOCS } from "./docs.generated.js";
16
+ import { buildPackagedAssetCatalog } from "./generated-assets-catalog.js";
17
+ import { recordProvisionedAssets } from "./generated-assets-inventory.js";
21
18
  import { VERSION, LAUNCHER_ARGS } from "./version.generated.js";
22
- import { reconstructAgentMarkdown, translateAgentToCopilot, translateCommandToCopilot, } from "./agent-utils.js";
23
19
  import { BRIDGE_CONFIG_TEAMMATE_NOTICE_LINES, validateBridgeConfigRepoNameRepresentability, validateRepoName, resolveRepoNameForProjectRoot, } from "./bridge-config.js";
24
20
  import { ensureGitignored as ensureGitignoredShared } from "./git-ignore-utils.js";
25
21
  import { resolveBapiCredentials } from "./credential-store.js";
@@ -219,21 +215,23 @@ export async function resolveInitScaffoldAssets(options) {
219
215
  const ide = await detectInitIdes({ ...options, env });
220
216
  const pipelinesDir = resolveInitPipelinesDir(cwd, env);
221
217
  const instructionsDir = path.join(path.dirname(pipelinesDir), "instructions");
218
+ const catalog = buildPackagedAssetCatalog();
219
+ const hostKinds = [["claude", "command"], ...(ide.cursor ? [["cursor", "command"]] : []),
220
+ ["codex", "skill"], ...(ide.vscode ? [["copilot", "prompt"], ["copilot", "skill"]] : []),
221
+ ["claude", "agent"], ...(ide.vscode ? [["copilot", "agent"]] : [])];
222
+ const catalogDirectories = hostKinds.flatMap(([host, kind]) => {
223
+ const asset = catalog.assets.find(a => a.kind === kind && a.hosts.includes(host));
224
+ return asset ? [asset.destination.split("/").slice(0, 2).join("/")] : [];
225
+ });
222
226
  const commitSafePaths = [
223
227
  displayScaffoldPath(cwd, path.join(cwd, ".bridge", "config"), false),
224
228
  displayScaffoldPath(cwd, path.join(pipelinesDir, "README.md"), false),
225
229
  displayScaffoldPath(cwd, path.join(pipelinesDir, "example-pipeline.json"), false),
226
230
  displayScaffoldPath(cwd, instructionsDir, true),
227
- displayScaffoldPath(cwd, path.join(cwd, ".claude", "commands"), true),
228
- ...(ide.cursor ? [displayScaffoldPath(cwd, path.join(cwd, ".cursor", "commands"), true)] : []),
229
- displayScaffoldPath(cwd, path.join(cwd, CODEX_SKILLS_DIR), true),
230
- ...(ide.vscode ? [displayScaffoldPath(cwd, path.join(cwd, ".github", "prompts"), true)] : []),
231
- ...(ide.vscode ? [displayScaffoldPath(cwd, path.join(cwd, ".github", "skills"), true)] : []),
232
- displayScaffoldPath(cwd, path.join(cwd, ".claude", "agents"), true),
233
- ...(ide.vscode ? [displayScaffoldPath(cwd, path.join(cwd, ".github", "agents"), true)] : []),
231
+ ...catalogDirectories.map(directory => displayScaffoldPath(cwd, path.join(cwd, directory), true)),
234
232
  // Sourced from the bundled DOCS map, so a new scaffolded doc is covered
235
233
  // automatically rather than needing a second edit here.
236
- ...Object.keys(DOCS)
234
+ ...catalog.assets.filter(a => a.kind === "document").map(a => a.destination)
237
235
  .slice()
238
236
  .sort()
239
237
  .map((target) => displayScaffoldPath(cwd, path.join(cwd, target), false)),
@@ -844,173 +842,52 @@ export async function runInit(cwd, options = {}) {
844
842
  }
845
843
  warn(` Fix with: npx -y ${MCP_PACKAGE_NAME} install (or set BAPI_REPO_NAME and store a key with the install flow)`);
846
844
  }
847
- // ---- Phase 4: Scaffold slash command directories ----
848
- const commandDirs = [path.join(cwd, ".claude", "commands")];
849
- if (ideDetection.cursor) {
850
- commandDirs.push(path.join(cwd, ".cursor", "commands"));
851
- }
852
- const writtenFiles = new Set();
853
- const skippedFiles = new Set();
854
- const overwrittenFiles = new Set();
855
- const commandEntries = Object.entries(COMMANDS);
856
- for (const dir of commandDirs) {
857
- await mkdir(dir, { recursive: true });
858
- // Per-file read/compare/write runs concurrently; outcomes are applied to the
859
- // Sets afterward in entry order so the summary counts are unchanged.
860
- const outcomes = await Promise.all(commandEntries.map(([filename, content]) => scaffoldFile(path.join(dir, filename), renderCommandForHost(filename, content, dir === path.join(cwd, ".cursor", "commands") ? "cursor" : "claude-code"))));
861
- commandEntries.forEach(([filename], i) => {
862
- const outcome = outcomes[i];
863
- if (outcome === "skipped") {
864
- skippedFiles.add(filename);
865
- return;
866
- }
867
- if (outcome === "overwritten")
868
- overwrittenFiles.add(filename);
869
- writtenFiles.add(filename);
870
- });
871
- }
872
- // A file written in any directory takes priority over skipped
873
- for (const f of writtenFiles)
874
- skippedFiles.delete(f);
875
- const total = Object.keys(COMMANDS).length;
876
- const dirNames = commandDirs.map((d) => path.relative(cwd, d)).join(" and ");
877
- console.log(`\nSlash commands: scaffolded ${total} commands into ${commandDirs.length} director${commandDirs.length === 1 ? "y" : "ies"}`);
878
- if (writtenFiles.size > 0)
879
- console.log(` Written: ${writtenFiles.size}`);
880
- if (overwrittenFiles.size > 0)
881
- console.log(` Overwritten (content changed): ${overwrittenFiles.size}`);
882
- if (skippedFiles.size > 0)
883
- console.log(` Skipped (unchanged): ${skippedFiles.size}`);
884
- console.log(` ${dirNames}`);
885
- // ---- Phase 4a: Scaffold Codex-native repository skills ----
886
- // Codex skills are a project-local, commit-safe surface. They intentionally
887
- // do not participate in the global Codex MCP registration at
888
- // ~/.codex/config.toml; that registration is owned by install-bridge.
889
- const codexAssets = codexSkillAssets(COMMANDS);
890
- const codexOutcomes = await Promise.all(codexAssets.map((asset) => {
891
- const target = path.join(cwd, asset.relativePath);
892
- return scaffoldFile(target, asset.content, path.dirname(target));
893
- }));
894
- const codexWritten = codexOutcomes.filter((outcome) => outcome !== "skipped").length;
895
- const codexOverwritten = codexOutcomes.filter((outcome) => outcome === "overwritten").length;
896
- const codexSkipped = codexOutcomes.filter((outcome) => outcome === "skipped").length;
897
- console.log(`\nCodex skills: scaffolded ${codexAssets.length} skills into ${CODEX_SKILLS_DIR}/`);
898
- if (codexWritten > 0)
899
- console.log(` Written: ${codexWritten}`);
900
- if (codexOverwritten > 0)
901
- console.log(` Overwritten (content changed): ${codexOverwritten}`);
902
- if (codexSkipped > 0)
903
- console.log(` Skipped (unchanged): ${codexSkipped}`);
904
- // ---- Phase 4a: Scaffold GitHub Copilot command artifacts ----
905
- // VS Code Local agent discovers prompt files; Agent Host discovers project
906
- // skills. Both artifacts are rendered from COMMANDS above, never a second
907
- // Copilot-specific command catalog. Each representation reports its own
908
- // counts, so a written prompt never implies its skill exists (or vice versa).
909
- if (ideDetection.vscode) {
910
- const promptsDir = path.join(cwd, ".github", "prompts");
911
- const skillsDir = path.join(cwd, ".github", "skills");
912
- const artifacts = commandEntries.map(([filename, content]) => ({
913
- commandName: filename.replace(/\.md$/, ""),
914
- ...translateCommandToCopilot(filename, content),
845
+ // ---- Phases 4–5: Catalog-owned packaged assets ----
846
+ const catalog = buildPackagedAssetCatalog();
847
+ if (catalog.packageProblems.length)
848
+ throw new Error("Packaged asset catalog is incomplete; rebuild or reinstall the package.");
849
+ const hosts = ["claude", "codex"];
850
+ if (ideDetection.cursor)
851
+ hosts.push("cursor");
852
+ if (ideDetection.vscode)
853
+ hosts.push("copilot");
854
+ const provisioned = catalog.assets.filter(a => a.hosts.some(h => hosts.includes(h)));
855
+ const successful = [];
856
+ let scaffoldFailed = false;
857
+ const categories = [
858
+ ["Slash commands", provisioned.filter(a => a.kind === "command")],
859
+ ["Codex skills", provisioned.filter(a => a.kind === "skill" && a.hosts.includes("codex"))],
860
+ ["VS Code Local-agent prompt files", provisioned.filter(a => a.kind === "prompt")],
861
+ ["Agent Host skills", provisioned.filter(a => a.kind === "skill" && a.hosts.includes("copilot"))],
862
+ ["Documentation", provisioned.filter(a => a.kind === "document")],
863
+ ["Agents", provisioned.filter(a => a.kind === "agent")],
864
+ ];
865
+ for (const [label, assets] of categories) {
866
+ if (!assets.length)
867
+ continue;
868
+ const outcomes = await Promise.allSettled(assets.map(async (asset) => {
869
+ const target = path.join(cwd, asset.destination);
870
+ const outcome = await scaffoldFile(target, asset.content, path.dirname(target));
871
+ successful.push(asset);
872
+ return outcome;
915
873
  }));
916
- const promptOutcomes = await Promise.all(artifacts.map(({ commandName, prompt }) => scaffoldFile(path.join(promptsDir, `${commandName}.prompt.md`), prompt, promptsDir)));
917
- const skillOutcomes = await Promise.all(artifacts.map(({ commandName, skill }) => scaffoldFile(path.join(skillsDir, commandName, "SKILL.md"), skill, path.join(skillsDir, commandName))));
918
- const copilotReports = [
919
- ["VS Code Local-agent prompt files", ".github/prompts/", promptOutcomes],
920
- ["Agent Host skills", ".github/skills/", skillOutcomes],
921
- ];
922
- for (const [label, dir, outcomes] of copilotReports) {
923
- const written = outcomes.filter((outcome) => outcome !== "skipped").length;
924
- const overwritten = outcomes.filter((outcome) => outcome === "overwritten").length;
925
- const skipped = outcomes.filter((outcome) => outcome === "skipped").length;
926
- console.log(`\n${label}: scaffolded ${outcomes.length} into ${dir}`);
927
- if (written > 0)
928
- console.log(` Written: ${written}`);
929
- if (overwritten > 0)
930
- console.log(` Overwritten (content changed): ${overwritten}`);
931
- if (skipped > 0)
932
- console.log(` Skipped (unchanged): ${skipped}`);
933
- }
874
+ const completed = outcomes.flatMap(o => o.status === "fulfilled" ? [o.value] : []);
875
+ scaffoldFailed ||= completed.length !== assets.length;
876
+ const destinationLabel = assets[0]?.hosts.includes("copilot") && (assets[0].kind === "prompt" || assets[0].kind === "skill")
877
+ ? `into ${assets[0].destination.split("/").slice(0, 2).join("/")}/` : "assets";
878
+ console.log(`\n${label}: scaffolded ${completed.length} ${destinationLabel}`);
879
+ console.log(` Written: ${completed.filter(o => o !== "skipped").length}`);
880
+ console.log(` Overwritten (content changed): ${completed.filter(o => o === "overwritten").length}`);
881
+ console.log(` Skipped (unchanged): ${completed.filter(o => o === "skipped").length}`);
934
882
  }
935
- // ---- Phase 4b: Scaffold shipped documentation assets ----
936
- // Every doc asset the /install-bridge capability report points a human at,
937
- // so no pointer it prints dangles inside an installed project: the
938
- // capability catalog (docs/mcp-tool-integrations.md) that explains each
939
- // gate's human "why", and the SFCC integration guide
940
- // (docs/install/sfcc-integration.md) that the report's SFCC configure_in
941
- // pointer names by path. Keyed by target path RELATIVE to the consumer
942
- // project root; content is never secret.
943
- const docsWritten = new Set();
944
- const docsSkipped = new Set();
945
- const docEntries = Object.entries(DOCS);
946
- // Each doc target may live in a different subdir, so mkdir(dirname) happens
947
- // per-file inside scaffoldFile. Outcomes applied in entry order below preserve
948
- // the insertion order of the joined summary list.
949
- const docOutcomes = await Promise.all(docEntries.map(([relTarget, content]) => {
950
- const target = path.join(cwd, relTarget);
951
- return scaffoldFile(target, content, path.dirname(target));
952
- }));
953
- docEntries.forEach(([relTarget], i) => {
954
- // DOCS tracks only written vs skipped; an overwrite counts as written.
955
- if (docOutcomes[i] === "skipped")
956
- docsSkipped.add(relTarget);
957
- else
958
- docsWritten.add(relTarget);
959
- });
960
- const docsTotal = Object.keys(DOCS).length;
961
- if (docsTotal > 0) {
962
- console.log(`\nDocumentation: scaffolded ${docsTotal} doc asset${docsTotal === 1 ? "" : "s"}`);
963
- if (docsWritten.size > 0)
964
- console.log(` Written: ${[...docsWritten].join(", ")}`);
965
- if (docsSkipped.size > 0)
966
- console.log(` Skipped (unchanged): ${[...docsSkipped].join(", ")}`);
883
+ try {
884
+ await recordProvisionedAssets(cwd, successful, hosts);
967
885
  }
968
- // ---- Phase 5: Scaffold agent directories ----
969
- const agentWritten = new Set();
970
- const agentSkipped = new Set();
971
- const agentOverwritten = new Set();
972
- // Always scaffold to .claude/agents/
973
- const claudeAgentsDir = path.join(cwd, ".claude", "agents");
974
- await mkdir(claudeAgentsDir, { recursive: true });
975
- const agentEntries = Object.entries(AGENTS);
976
- // Apply a batch of ordered outcomes to the agent Sets (same branches as the
977
- // original per-file loop: skipped → skipped; overwritten → overwritten+written;
978
- // written → written).
979
- const applyAgentOutcomes = (filenames, outcomes) => {
980
- filenames.forEach((filename, i) => {
981
- const outcome = outcomes[i];
982
- if (outcome === "skipped") {
983
- agentSkipped.add(filename);
984
- return;
985
- }
986
- if (outcome === "overwritten")
987
- agentOverwritten.add(filename);
988
- agentWritten.add(filename);
989
- });
990
- };
991
- const claudeAgentFilenames = agentEntries.map(([key]) => `${key}.md`);
992
- const claudeAgentOutcomes = await Promise.all(agentEntries.map(([key, agent]) => scaffoldFile(path.join(claudeAgentsDir, `${key}.md`), reconstructAgentMarkdown(agent.frontmatter, agent.body))));
993
- applyAgentOutcomes(claudeAgentFilenames, claudeAgentOutcomes);
994
- // Scaffold to .github/agents/ for VS Code / Copilot
995
- if (ideDetection.vscode) {
996
- const copilotAgentsDir = path.join(cwd, ".github", "agents");
997
- await mkdir(copilotAgentsDir, { recursive: true });
998
- const copilotAgentFilenames = agentEntries.map(([key]) => `${key}.agent.md`);
999
- const copilotAgentOutcomes = await Promise.all(agentEntries.map(([key, agent]) => scaffoldFile(path.join(copilotAgentsDir, `${key}.agent.md`), translateAgentToCopilot(agent.frontmatter, agent.body))));
1000
- applyAgentOutcomes(copilotAgentFilenames, copilotAgentOutcomes);
886
+ catch {
887
+ warn("Packaged workflow inventory could not be safely recorded; discovery will remain conservative.");
1001
888
  }
1002
- // TODO: Add Cursor agent scaffolding when Cursor publishes a formal agent spec
1003
- // A file written in any directory takes priority over skipped
1004
- for (const f of agentWritten)
1005
- agentSkipped.delete(f);
1006
- const agentTotal = Object.keys(AGENTS).length;
1007
- console.log(`\nAgents: scaffolded ${agentTotal} agent${agentTotal === 1 ? "" : "s"}`);
1008
- if (agentWritten.size > 0)
1009
- console.log(` Written: ${agentWritten.size}`);
1010
- if (agentOverwritten.size > 0)
1011
- console.log(` Overwritten (content changed): ${agentOverwritten.size}`);
1012
- if (agentSkipped.size > 0)
1013
- console.log(` Skipped (unchanged): ${agentSkipped.size}`);
889
+ if (scaffoldFailed)
890
+ throw new Error("Some packaged workflow assets could not be scaffolded.");
1014
891
  // ---- Phase 6: Scaffold custom pipeline directories ----
1015
892
  // BAPI-708: shared with `resolveInitScaffoldAssets`, so a `BAPI_PIPELINES_DIR`
1016
893
  // override moves the scaffold and the commit-safe manifest in lockstep.