@bridge_gpt/mcp-server 0.2.66 → 0.2.68

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 (171) hide show
  1. package/README.md +421 -83
  2. package/build/agent-capabilities/probes.js +15 -5
  3. package/build/agent-launchers/claude-executor-adapter.js +70 -16
  4. package/build/agent-launchers/executor-adapter.js +7 -4
  5. package/build/agents.generated.js +22 -2
  6. package/build/bitbucket-workspace-remote.js +136 -0
  7. package/build/bootstrap-project-identity.js +24 -0
  8. package/build/bounded-wait.js +67 -10
  9. package/build/bridge-permissions.js +829 -0
  10. package/build/claude-settings.js +18 -0
  11. package/build/cli-help.js +21 -0
  12. package/build/command-assets-doctor.js +24 -70
  13. package/build/command-provisioning.js +29 -5
  14. package/build/commands.generated.js +12 -12
  15. package/build/conductor/bridge-api-client.js +857 -1
  16. package/build/conductor/bring-up-facts.js +1 -1
  17. package/build/conductor/command-drift-production.js +92 -0
  18. package/build/conductor/command-drift.js +89 -0
  19. package/build/conductor/doctor.js +72 -29
  20. package/build/conductor/epic-branch-ci-inspection.js +1375 -0
  21. package/build/conductor/epic-implementer-cli.js +98 -28
  22. package/build/conductor/errors.js +4 -3
  23. package/build/conductor/gate-discovery.js +890 -0
  24. package/build/conductor/guided-repair.js +6 -2
  25. package/build/conductor/hook-runtime-qualification.js +1 -1
  26. package/build/conductor/install-doctor.js +15 -3
  27. package/build/conductor/json-output.js +63 -0
  28. package/build/conductor/merge-execution-contract.js +27 -8
  29. package/build/conductor/merge-execution-eligibility.js +3 -2
  30. package/build/conductor/pr-ci-producer.js +1 -1
  31. package/build/conductor/readiness-cli.js +113 -12
  32. package/build/conductor/readiness-sections.js +410 -9
  33. package/build/conductor/readiness.js +388 -33
  34. package/build/conductor/recovery-cli.js +310 -12
  35. package/build/conductor/recovery-operations.js +167 -3
  36. package/build/conductor/repair-contract.js +54 -3
  37. package/build/conductor/repository-workflow-remedy.js +125 -0
  38. package/build/conductor/tools.js +2 -2
  39. package/build/conductor-bundle-artifacts.js +8 -0
  40. package/build/connect-bitbucket-api.js +41 -6
  41. package/build/connect-bitbucket.js +106 -33
  42. package/build/credential-store.js +207 -47
  43. package/build/docs.generated.js +62 -3
  44. package/build/doctor.js +92 -1
  45. package/build/drive-epic-key.js +6 -5
  46. package/build/drive-epic.js +129 -61
  47. package/build/epic-coherence-input.js +2 -1
  48. package/build/epic-coherence-input.test-fixtures.js +4 -3
  49. package/build/epic-implementer-bin.js +1379 -28
  50. package/build/epic-implementer-bundle-cli.js +2 -2
  51. package/build/epic-workarounds-path.js +39 -0
  52. package/build/executor/cli.js +33 -2
  53. package/build/executor/completion-contract.js +66 -10
  54. package/build/executor/completion-supervision.js +4 -0
  55. package/build/executor/deps.js +4 -1
  56. package/build/executor/env.js +19 -0
  57. package/build/executor/gate-qualification-job.js +499 -0
  58. package/build/executor/guarded-fast-forward-push.js +129 -0
  59. package/build/executor/heartbeat.js +1 -1
  60. package/build/executor/http-client.js +20 -0
  61. package/build/executor/integration-job.js +122 -8
  62. package/build/executor/job-errors.js +33 -1
  63. package/build/executor/job-runner.js +813 -87
  64. package/build/executor/likely-tests.js +290 -0
  65. package/build/executor/live-worker-registry.js +1 -1
  66. package/build/executor/observation.js +37 -3
  67. package/build/executor/packaged-recipe.js +106 -0
  68. package/build/executor/permissions.js +115 -28
  69. package/build/executor/platform.js +1 -1
  70. package/build/executor/preflight.js +112 -26
  71. package/build/executor/process-containment.js +36 -12
  72. package/build/executor/process.js +15 -7
  73. package/build/executor/prompt-spec.js +33 -1
  74. package/build/executor/recently-failed-tests-file.js +71 -0
  75. package/build/executor/reconcile-overlap-job.js +41 -23
  76. package/build/executor/required-verification.js +1 -1
  77. package/build/executor/required-work-ledger.js +111 -18
  78. package/build/executor/resume-pre-spawn.js +2 -0
  79. package/build/executor/retirement.js +69 -0
  80. package/build/executor/runner.js +122 -13
  81. package/build/executor/service-unit.js +7 -1
  82. package/build/executor/test-clock.js +15 -0
  83. package/build/executor/types.js +32 -0
  84. package/build/executor/viewer-tabs.js +58 -8
  85. package/build/executor/worker-command.js +23 -7
  86. package/build/executor/worker-diagnostics.js +37 -0
  87. package/build/executor/worker-finalization.js +113 -34
  88. package/build/executor/worker-guard-hook.js +316 -58
  89. package/build/executor/worker-log.js +4 -3
  90. package/build/executor/worker-verification-guidance.js +37 -0
  91. package/build/executor/worktree.js +166 -8
  92. package/build/first-build-advisory.js +97 -0
  93. package/build/generated-assets-advisory.js +14 -0
  94. package/build/generated-assets-catalog.js +66 -0
  95. package/build/generated-assets-context.js +46 -0
  96. package/build/generated-assets-dependencies.js +26 -0
  97. package/build/generated-assets-doctor.js +50 -0
  98. package/build/generated-assets-health.js +65 -0
  99. package/build/generated-assets-host.js +17 -0
  100. package/build/generated-assets-inventory.js +17 -0
  101. package/build/generated-assets-paths.js +60 -0
  102. package/build/generated-assets-publisher.js +133 -0
  103. package/build/generated-assets-remedy.js +25 -0
  104. package/build/generated-assets-repair.js +72 -0
  105. package/build/generated-assets-session.js +71 -0
  106. package/build/generated-assets-test-support.js +35 -0
  107. package/build/index.js +1224 -1054
  108. package/build/init.js +54 -177
  109. package/build/install-bridge-conductor-gate.js +1372 -0
  110. package/build/install-bridge-conductor.js +296 -41
  111. package/build/install-bridge.js +547 -40
  112. package/build/mcp-install-state.js +50 -4
  113. package/build/merge-workflow-install.js +3 -4
  114. package/build/phase-context-git-probe.js +68 -0
  115. package/build/phase-result-artifacts.js +173 -5
  116. package/build/pipeline-utils.js +6 -2
  117. package/build/pipelines.generated.js +34 -160
  118. package/build/plan-execution-ledger.js +2 -32
  119. package/build/plane/alembic-head.js +12 -3
  120. package/build/plane/base-worktree-guard.js +147 -0
  121. package/build/plane/cli.js +217 -17
  122. package/build/plane/code-snapshot.js +468 -0
  123. package/build/plane/defaults.js +48 -3
  124. package/build/plane/endpoint.js +41 -0
  125. package/build/plane/manifest.js +136 -2
  126. package/build/plane/member-roster.js +94 -19
  127. package/build/plane/preflight.js +83 -2
  128. package/build/plane/process-cwd.js +67 -0
  129. package/build/plane/restart.js +542 -0
  130. package/build/plane/shutdown.js +31 -0
  131. package/build/plane/status.js +143 -8
  132. package/build/plane/supervisor.js +586 -179
  133. package/build/plane/types.js +54 -2
  134. package/build/printed-remedy-commands.js +52 -0
  135. package/build/readiness-check.js +1 -1
  136. package/build/readme.generated.js +1 -1
  137. package/build/repair-assets.js +93 -0
  138. package/build/review-generation.js +22 -0
  139. package/build/run-unit-tests-launcher.js +130 -49
  140. package/build/secret-redaction.js +33 -0
  141. package/build/setup-epic.js +152 -22
  142. package/build/sfcc/config.js +9 -0
  143. package/build/sfcc/credentials.js +100 -38
  144. package/build/sfcc/hostname.js +67 -0
  145. package/build/sfcc/permissions.js +17 -13
  146. package/build/sfcc/register.js +1 -1
  147. package/build/sfcc/tool-wrapper.js +39 -43
  148. package/build/sfcc/write-grants.js +17 -11
  149. package/build/sfcc/write-result.js +4 -3
  150. package/build/sfcc/writes-site-preference.js +20 -8
  151. package/build/start-tickets-prereqs.js +70 -16
  152. package/build/start-tickets.js +208 -36
  153. package/build/tool-error-envelope.js +61 -0
  154. package/build/tool-surface-gating.js +36 -0
  155. package/build/upgrade-cli.js +55 -1
  156. package/build/version.generated.js +3 -3
  157. package/build/visual-diff.js +22 -1
  158. package/build/worker-guard-hook-bin.js +4 -6
  159. package/build/worker-guard-hook-main.js +47 -0
  160. package/build/worktree-core.js +145 -6
  161. package/docs/EPIC-IMPLEMENTER-INSTALL.md +867 -0
  162. package/docs/EPIC-IMPLEMENTER.md +728 -0
  163. package/docs/install/mcp-tool-integrations.md +24 -15
  164. package/docs/install/sfcc-integration.md +12 -4
  165. package/package.json +3 -3
  166. package/pipelines/greenfield-setup.json +4 -128
  167. package/pipelines/implement-ticket.json +3 -2
  168. package/pipelines/plan-epic.json +0 -5
  169. package/pipelines/review-ticket.json +1 -1
  170. package/docs/CONDUCTOR.md +0 -605
  171. package/pipelines/idea-to-pr.json +0 -49
@@ -1,3 +1,4 @@
1
+ import { guardedFastForwardPush, isSafePublicationBranch } from "./guarded-fast-forward-push.js";
1
2
  /**
2
3
  * Executor worktree ensure (BAPI-534, TDD §7, §12).
3
4
  *
@@ -10,7 +11,8 @@ import { fetchAndResolveBaseSha, probeClaudeReviewWorkflowDrift, resolveReposito
10
11
  // BAPI-941: the shared drift diagnostic formatter (a leaf module — no cycle).
11
12
  import { formatClaudeReviewWorkflowDriftDiagnostic } from "../claude-review-workflow-drift.js";
12
13
  import { commandSucceeded } from "../start-tickets-prereqs.js";
13
- import { createWorktreeForTicket } from "../worktree-core.js";
14
+ import { createWorktreeForTicket, provePriorHeadLineage } from "../worktree-core.js";
15
+ import { findWorktreeByBranch, listGitWorktrees } from "./worktree-inspection.js";
14
16
  import { provisionCommandsForWorktree } from "../command-provisioning.js";
15
17
  /**
16
18
  * Resolve the branch for a real spawn job: `expected_branch` wins; else
@@ -29,6 +31,45 @@ export function resolveExecutorBranch(job) {
29
31
  return { ok: true, branch: `feature/${ticketKey}` };
30
32
  return { ok: false, error: "no branch could be resolved (no expected_branch and no ticket_key)" };
31
33
  }
34
+ /** Configure only this branch; inherited routing must already be safe. */
35
+ export async function configureExecutorBranch(worktreePath, branch, deps) {
36
+ const failure = "executor push routing could not be confirmed";
37
+ if (!isSafePublicationBranch(branch))
38
+ return failure;
39
+ try {
40
+ const read = async (key) => {
41
+ const result = await deps.runCommand("git", ["config", "--get-all", key], { cwd: worktreePath });
42
+ if (result.exitCode === 1)
43
+ return [];
44
+ if (result.exitCode !== 0)
45
+ return null;
46
+ return result.stdout.trim().split("\n");
47
+ };
48
+ const allowed = async (key, values) => {
49
+ const actual = await read(key);
50
+ return actual !== null && actual.every(value => values.includes(value));
51
+ };
52
+ if (!await allowed("push.default", ["simple", "current", "upstream"]) ||
53
+ !await allowed("remote.pushDefault", ["origin"]) ||
54
+ !await allowed(`branch.${branch}.pushRemote`, ["origin"]) ||
55
+ !await allowed("remote.origin.push", []) ||
56
+ !await allowed("remote.origin.mirror", ["false"]))
57
+ return failure;
58
+ const urls = await read("remote.origin.url");
59
+ const pushUrls = await read("remote.origin.pushurl");
60
+ if (!urls || urls.length !== 1 || !pushUrls || pushUrls.length > 1 ||
61
+ (pushUrls.length === 1 && pushUrls[0] !== urls[0]))
62
+ return failure;
63
+ for (const [key, value] of [[`branch.${branch}.remote`, "origin"], [`branch.${branch}.merge`, `refs/heads/${branch}`]]) {
64
+ if ((await deps.runCommand("git", ["config", "--local", key, value], { cwd: worktreePath })).exitCode !== 0)
65
+ return failure;
66
+ }
67
+ return null;
68
+ }
69
+ catch {
70
+ return failure;
71
+ }
72
+ }
32
73
  /**
33
74
  * BAPI-941: emit the per-spawn `claude-review` workflow-drift advisory for an
34
75
  * executor worker. Returns `void` and swallows everything — a diagnostic that
@@ -70,6 +111,7 @@ function toWorktreeCoreDeps(deps) {
70
111
  /** Build the command-provisioning boundary from the executor deps (BAPI-664). */
71
112
  function toCommandProvisioningDeps(deps) {
72
113
  return {
114
+ recordAssets: deps.recordPackagedAssets,
73
115
  readFile: deps.readFile,
74
116
  writeFile: deps.writeFile,
75
117
  mkdir: deps.mkdir,
@@ -88,12 +130,12 @@ function toCommandProvisioningDeps(deps) {
88
130
  * preserved unchanged. Errors stay secret-free (no raw filesystem exception, Git
89
131
  * output, or command contents).
90
132
  */
91
- async function finalizeExecutorWorktree(worktreePath, branch, expectedHeadSha, deps) {
133
+ async function finalizeExecutorWorktree(worktreePath, branch, expectedHeadSha, deps, publication) {
92
134
  const provisioned = await provisionCommandsForWorktree(worktreePath, toCommandProvisioningDeps(deps));
93
135
  if (!provisioned.ok) {
94
- return { ok: false, error: provisioned.error };
136
+ return { ok: false, error: provisioned.error, ...(publication ? { publication } : {}) };
95
137
  }
96
- return { ok: true, worktreePath, branch, expectedHeadSha };
138
+ return { ok: true, worktreePath, branch, expectedHeadSha, ...(publication ? { publication } : {}) };
97
139
  }
98
140
  /**
99
141
  * BAPI-1300 — finish a RETAINED continuation worktree the same way a created one
@@ -156,6 +198,7 @@ export async function ensureExecutorWorktree(job, options, deps, policy = {}) {
156
198
  if (!branchResult.ok)
157
199
  return { ok: false, error: branchResult.error };
158
200
  const branch = branchResult.branch;
201
+ const postSwitch = policy.preparation ? (worktreePath) => configureExecutorBranch(worktreePath, branch, deps) : undefined;
159
202
  // The shared primitive resolves the branch from `key` via a per-key override,
160
203
  // so we pin the exact resolved branch through the override map.
161
204
  const key = typeof job.ticket_key === "string" && job.ticket_key.trim().length > 0
@@ -187,7 +230,7 @@ export async function ensureExecutorWorktree(job, options, deps, policy = {}) {
187
230
  const originRef = `origin/${branch}`;
188
231
  const fetch = await deps.runCommand("git", ["fetch", "origin", branch], { cwd: deps.cwd });
189
232
  if (!commandSucceeded(fetch)) {
190
- const reason = (fetch.stderr || fetch.stdout || "").trim();
233
+ const reason = "";
191
234
  return {
192
235
  ok: false,
193
236
  error: `git fetch origin ${branch} failed${reason ? `: ${reason}` : ""}`,
@@ -205,15 +248,49 @@ export async function ensureExecutorWorktree(job, options, deps, policy = {}) {
205
248
  error: `failed to resolve '${originRef}' to a commit after fetching branch '${branch}'`,
206
249
  };
207
250
  }
251
+ let publication;
252
+ let safeHead = expectedHeadSha;
253
+ const preReset = policy.preparation ? async (worktreePath, actualBranch) => {
254
+ const context = policy.preparation;
255
+ const refuse = (reason) => ({ ok: false, error: `retained work preserved: ${reason}` });
256
+ if (!context.mayMutate() || context.branch !== actualBranch)
257
+ return refuse("preparation unavailable");
258
+ const head = await resolveCommitSha(deps, worktreePath, "HEAD");
259
+ const checked = await deps.runCommand("git", ["symbolic-ref", "--quiet", "--short", "HEAD"], { cwd: worktreePath });
260
+ if (!head || checked.exitCode !== 0 || checked.stdout.trim() !== branch)
261
+ return refuse("branch or head unknown");
262
+ if (head === expectedHeadSha)
263
+ return { ok: true, target: expectedHeadSha };
264
+ const ancestry = await deps.runCommand("git", ["merge-base", "--is-ancestor", expectedHeadSha, "HEAD"], { cwd: worktreePath });
265
+ if (ancestry.exitCode === 1) {
266
+ try {
267
+ deps.errorLog?.(`[executor] job ${context.jobId} ${new Date(deps.now()).toISOString()}: branch ${branch}: non-descending retained head ${head.slice(0, 12)}; resetting to ${expectedHeadSha.slice(0, 12)}`);
268
+ }
269
+ catch { /* advisory */ }
270
+ return { ok: true, target: expectedHeadSha };
271
+ }
272
+ if (ancestry.exitCode !== 0)
273
+ return refuse("ancestry unknown");
274
+ const pushed = await guardedFastForwardPush({ branch, expectedOriginSha: expectedHeadSha,
275
+ candidateSha: head, worktreePath, runCommand: deps.runCommand, env: deps.env,
276
+ remainingMs: context.remainingMs, mayMutate: context.mayMutate });
277
+ if (pushed.status !== "published")
278
+ return refuse(pushed.reason);
279
+ publication = { jobId: context.jobId, preparationId: context.preparationId, branch,
280
+ from_sha: pushed.from_sha, to_sha: pushed.to_sha };
281
+ context.recordPublication(publication);
282
+ safeHead = pushed.to_sha;
283
+ return { ok: true, target: safeHead };
284
+ } : undefined;
208
285
  const row = await createWorktreeForTicket(toWorktreeCoreDeps(deps), key, { [key]: branch }, options.worktrunkBinary, baseStartPoint, guardStaleWorktree,
209
286
  // Hard-reset a reused existing local branch onto the freshly fetched
210
287
  // pushed head; a missing local branch is instead cut from `baseStartPoint`
211
288
  // (= `origin/<branch>`) inside the primitive, so both land at origin.
212
- { freshenFromOrigin: originRef });
289
+ { freshenFromOrigin: policy.preparation ? expectedHeadSha : originRef, ...(preReset ? { preReset } : {}), ...(postSwitch ? { postSwitch } : {}) });
213
290
  if (row.status === "created" && typeof row.path === "string") {
214
- return finalizeExecutorWorktree(row.path, branch, expectedHeadSha, deps);
291
+ return finalizeExecutorWorktree(row.path, branch, safeHead, deps, publication);
215
292
  }
216
- return { ok: false, error: row.error ?? `worktree creation failed for branch '${branch}'` };
293
+ return { ok: false, error: row.error ?? `worktree creation failed for branch '${branch}'`, ...(publication ? { publication } : {}) };
217
294
  }
218
295
  // BAPI-586 (fresh dispatch): a fresh implementation-style job must start from
219
296
  // the CURRENT remote base — never a stale local `main`, another local base
@@ -252,7 +329,15 @@ export async function ensureExecutorWorktree(job, options, deps, policy = {}) {
252
329
  // is neither), and it never contributes to the fail-CLOSED base-resolution
253
330
  // decision above.
254
331
  await emitExecutorWorkflowDriftAdvisory(deps, options.baseBranch);
332
+ // BAPI-1367: a retry of an `implement` job with a server-minted prior head
333
+ // resumes its own branch instead of being refused as a stale leftover. The
334
+ // gate lives HERE, not in the shared primitive, which learns no job types.
335
+ const priorHeadSha = eligiblePriorHead(job, policy);
336
+ if (priorHeadSha !== null) {
337
+ return ensurePriorAttemptWorktree(key, branch, priorHeadSha, baseSha, options, deps, postSwitch);
338
+ }
255
339
  const row = await createWorktreeForTicket(toWorktreeCoreDeps(deps), key, { [key]: branch }, options.worktrunkBinary, baseSha, guardStaleWorktree, {
340
+ ...(postSwitch ? { postSwitch } : {}),
256
341
  alignExistingBranchTo: baseSha,
257
342
  verifyHeadMatches: baseSha,
258
343
  // BAPI-862: the guard is ON for this fresh-dispatch path, and this is the
@@ -268,3 +353,76 @@ export async function ensureExecutorWorktree(job, options, deps, policy = {}) {
268
353
  }
269
354
  return { ok: false, error: row.error ?? `worktree creation failed for branch '${branch}'` };
270
355
  }
356
+ /**
357
+ * BAPI-1367: the prior head a worktree policy may act on, or `null`. Requires an
358
+ * `implement` job on a real retry and a syntactically valid full SHA; anything
359
+ * else keeps the ordinary fresh-dispatch behavior.
360
+ */
361
+ function eligiblePriorHead(job, policy) {
362
+ if (policy.reuseExistingBranch === true)
363
+ return null;
364
+ if (job.job_type !== "implement")
365
+ return null;
366
+ if (typeof job.attempt_count !== "number" || job.attempt_count <= 1)
367
+ return null;
368
+ const head = policy.priorAttemptHeadSha;
369
+ return typeof head === "string" && FULL_SHA_PATTERN.test(head) ? head : null;
370
+ }
371
+ /**
372
+ * BAPI-1367: prepare the worktree for a retry that resumes prior-attempt work.
373
+ *
374
+ * Non-destructive by construction: no `alignExistingBranchTo`, no
375
+ * `verifyHeadMatches: baseSha`, no reset-based freshen, no `git clean -fd`. The
376
+ * shared primitive proves lineage against the resolved base and refuses before
377
+ * any mutation otherwise.
378
+ *
379
+ * When the worktree is missing it is recreated from the pushed branch, so
380
+ * `origin/<branch>` is fetched first and its lineage is proven here as well as
381
+ * (when a local branch exists) the local tip's lineage in the primitive —
382
+ * recreation may only use a remote source that passes. A conflicting ref refuses
383
+ * without cleanup. The result says plainly whether prior uncommitted files could
384
+ * survive.
385
+ */
386
+ async function ensurePriorAttemptWorktree(key, branch, priorHeadSha, baseSha, options, deps, postSwitch) {
387
+ const entries = await listGitWorktrees(deps.runCommand, deps.cwd);
388
+ const retained = findWorktreeByBranch(entries, branch) !== undefined;
389
+ const coreDeps = toWorktreeCoreDeps(deps);
390
+ const policy = { priorHeadSha, baseRef: baseSha };
391
+ if (!retained) {
392
+ // Recreation reads origin/<branch>; make sure that ref is current and proven.
393
+ const fetch = await deps.runCommand("git", ["fetch", "origin", branch], { cwd: deps.cwd });
394
+ if (!commandSucceeded(fetch)) {
395
+ const reason = "";
396
+ return {
397
+ ok: false,
398
+ error: `cannot resume prior-attempt work on '${branch}': git fetch origin ${branch} failed` +
399
+ `${reason ? `: ${reason}` : ""}`,
400
+ };
401
+ }
402
+ const remote = await provePriorHeadLineage(coreDeps, branch, policy, "origin_branch");
403
+ if (!remote.ok) {
404
+ return {
405
+ ok: false,
406
+ error: `stale worktree guard: pushed branch 'origin/${branch}' cannot be resumed: ${remote.reason}`,
407
+ };
408
+ }
409
+ }
410
+ const row = await createWorktreeForTicket(coreDeps, key, { [key]: branch }, options.worktrunkBinary, baseSha, true, { allowedPriorHead: policy, ...(postSwitch ? { postSwitch } : {}) });
411
+ if (row.status !== "created" || typeof row.path !== "string") {
412
+ return { ok: false, error: row.error ?? `worktree creation failed for branch '${branch}'` };
413
+ }
414
+ const headSha = await resolveCommitSha(deps, row.path, "HEAD");
415
+ if (headSha === null) {
416
+ return { ok: false, error: `failed to resolve HEAD of the resumed worktree for '${branch}'` };
417
+ }
418
+ return {
419
+ ok: true,
420
+ worktreePath: row.path,
421
+ branch,
422
+ expectedHeadSha: headSha,
423
+ priorWork: {
424
+ kind: retained ? "retained_worktree" : "recreated_from_pushed_branch",
425
+ priorHeadSha,
426
+ },
427
+ };
428
+ }
@@ -0,0 +1,97 @@
1
+ /** Import-safe read-only installer guidance; never launcher admission. */
2
+ import { resolveAgentSpec } from "./agent-registry.js";
3
+ import { getPreflightPrereqDescriptors, getDoctorOnlyPrereqDescriptors, probePrerequisite, formatGitCompatibilityDiagnostic, START_TICKETS_DOCTOR_COMMAND, } from "./start-tickets-prereqs.js";
4
+ /** One ten-second budget across all tool probes, including unavailable runners. */
5
+ export async function collectFirstBuildAdvisory(options, deps) {
6
+ const preflight = getPreflightPrereqDescriptors(deps.platform, deps.env);
7
+ const base = { platform: deps.platform, configuredHosts: options.configuredHosts, required: [], recommended: [] };
8
+ if (!preflight.ok)
9
+ return { ...base, status: "unsupported" };
10
+ if (options.decision.kind !== "spawn")
11
+ return { ...base, status: "manual" };
12
+ const agent = resolveAgentSpec(options.decision.agent);
13
+ if (!agent)
14
+ return { ...base, status: "manual" };
15
+ const now = deps.now ?? Date.now;
16
+ const deadline = now() + 10000;
17
+ const controller = new AbortController();
18
+ const allowed = new Set(["uv", agent.command, "ast-grep", "rg", "lizard", "git-compatibility"]);
19
+ const required = preflight.descriptors.filter((d) => d.id !== "git-work-tree");
20
+ const recommended = getDoctorOnlyPrereqDescriptors(deps.platform, deps.env, agent).filter((d) => allowed.has(d.id));
21
+ async function probe(descriptor) {
22
+ let uncertain = false;
23
+ const remaining = Math.max(0, deadline - now());
24
+ const scoped = { ...deps, runCommand: async (file, args, commandOptions) => {
25
+ const budget = Math.max(0, deadline - now());
26
+ if (controller.signal.aborted || budget === 0) {
27
+ uncertain = true;
28
+ throw new Error("probe unavailable");
29
+ }
30
+ try {
31
+ const result = await deps.runCommand(file, args, {
32
+ ...commandOptions, timeoutMs: Math.min(commandOptions?.timeoutMs ?? budget, budget), signal: controller.signal,
33
+ });
34
+ if (result.unavailable || controller.signal.aborted || now() >= deadline || result.exitCode > 1) {
35
+ uncertain = true;
36
+ throw new Error("probe unavailable");
37
+ }
38
+ return result;
39
+ }
40
+ catch {
41
+ uncertain = true;
42
+ throw new Error("probe unavailable");
43
+ }
44
+ } };
45
+ let timer;
46
+ const unavailable = async () => {
47
+ const result = await probePrerequisite({ ...scoped, runCommand: async () => { throw new Error("probe unavailable"); } }, descriptor);
48
+ return { ...result, detail: undefined, availability: "unknown" };
49
+ };
50
+ if (remaining === 0 || controller.signal.aborted)
51
+ return unavailable();
52
+ try {
53
+ const result = await Promise.race([
54
+ probePrerequisite(scoped, descriptor),
55
+ new Promise((resolve) => { timer = setTimeout(() => { controller.abort(); resolve(null); }, remaining); }),
56
+ ]);
57
+ if (!result)
58
+ return unavailable();
59
+ return { ...result, detail: undefined, availability: uncertain ? "unknown" : result.found ? "found" : "missing" };
60
+ }
61
+ finally {
62
+ if (timer)
63
+ clearTimeout(timer);
64
+ }
65
+ }
66
+ const result = { ...base, status: "supported", agent: options.decision.agent, required: [], recommended: [] };
67
+ try {
68
+ for (const descriptor of required)
69
+ result.required.push(await probe(descriptor));
70
+ for (const descriptor of recommended)
71
+ result.recommended.push(await probe(descriptor));
72
+ return result;
73
+ }
74
+ finally {
75
+ controller.abort();
76
+ }
77
+ }
78
+ export function formatFirstBuildAdvisory(result) {
79
+ const lines = [`First-build prerequisites (${result.platform}${result.agent ? `; ${result.agent}` : ""})`];
80
+ if (result.status !== "supported")
81
+ lines.push(`${result.status}: continue manually in your configured host; launcher readiness was not checked.`);
82
+ else {
83
+ for (const [label, tools] of [["Required for launcher", result.required], ["Recommended diagnostics", result.recommended]]) {
84
+ lines.push(`${label}:`);
85
+ for (const tool of tools.filter((t) => t.id !== "git-compatibility")) {
86
+ lines.push(` ${tool.label}: ${tool.availability}${tool.availability === "missing" ? ` — ${tool.installHint}` : ""}`);
87
+ }
88
+ }
89
+ const git = result.recommended.find((t) => t.id === "git-compatibility");
90
+ lines.push(git?.gitCompatibility ? formatGitCompatibilityDiagnostic(git.gitCompatibility) : "Git compatibility advisory: unknown");
91
+ lines.push("Git work-tree context is checked by the launcher at launch time. Recommendations and Git compatibility add no admission gate.");
92
+ }
93
+ if (result.configuredHosts.length > 1)
94
+ lines.push(`Other configured hosts: ${result.configuredHosts.join(", ")}; tool checks above apply only to the selected agent.`);
95
+ lines.push(`Read-only follow-up: ${START_TICKETS_DOCTOR_COMMAND}${result.agent ? ` --agent ${result.agent}` : ""}`);
96
+ return lines.join("\n");
97
+ }
@@ -0,0 +1,14 @@
1
+ import { packagedAssetRemedy } from "./generated-assets-remedy.js";
2
+ export const RECOVERY_ADVISORY_META_KEY = "bridge-api/asset-recovery";
3
+ export function assetRecoveryAdvisory(result, root, host, platform = process.platform) {
4
+ if (!result.confirmed || !host)
5
+ return null;
6
+ const missing = result.findings.filter(f => f.status === "missing");
7
+ if (!missing.length)
8
+ return null;
9
+ const paths = missing.slice(0, 5).map(f => f.destination);
10
+ const capabilities = [...new Set(missing.flatMap(f => f.workflows))].slice(0, 5);
11
+ const remedy = packagedAssetRemedy(root, host, undefined, platform);
12
+ return { host, missingCount: missing.length, paths, capabilities, command: remedy.command,
13
+ text: `Bridge ${host} workflow files are missing (${missing.length}; ${paths.join(", ")}${missing.length > 5 ? ", …" : ""}). Affected capabilities: ${capabilities.join(", ") || "packaged workflow documentation"}. ${remedy.command}\n${remedy.guidance}` };
14
+ }
@@ -0,0 +1,66 @@
1
+ import { COMMANDS } from "./commands.generated.js";
2
+ import { AGENTS } from "./agents.generated.js";
3
+ import { DOCS, DOC_METADATA } from "./docs.generated.js";
4
+ import { renderCommandForHost } from "./command-host.js";
5
+ import { codexSkillAssets } from "./codex-skill-adapter.js";
6
+ import { reconstructAgentMarkdown, translateAgentToCopilot, translateCommandToCopilot } from "./agent-utils.js";
7
+ export const WORKFLOW_HOSTS = ["claude", "cursor", "codex", "copilot"];
8
+ /** Destination and rendered bytes have one owner, shared by all consumers. */
9
+ export function buildPackagedAssetCatalog() {
10
+ const assets = [];
11
+ const packageProblems = [];
12
+ const destinations = new Set();
13
+ const add = (destination, content, kind, hosts, workflows, classification = "entrypoint") => {
14
+ if (destinations.has(destination) || !validAssetDestination(destination) || !content) {
15
+ packageProblems.push("invalid-or-conflicting-packaged-asset");
16
+ return;
17
+ }
18
+ destinations.add(destination);
19
+ assets.push({ id: destination, destination, content, kind, hosts, workflows, classification });
20
+ };
21
+ for (const [filename, content] of Object.entries(COMMANDS)) {
22
+ const workflow = filename.replace(/\.md$/, "");
23
+ try {
24
+ add(`.claude/commands/${filename}`, renderCommandForHost(filename, content, "claude-code"), "command", ["claude"], [workflow]);
25
+ add(`.cursor/commands/${filename}`, renderCommandForHost(filename, content, "cursor"), "command", ["cursor"], [workflow]);
26
+ const copilot = translateCommandToCopilot(filename, content);
27
+ add(`.github/prompts/${workflow}.prompt.md`, copilot.prompt, "prompt", ["copilot"], [workflow]);
28
+ add(`.github/skills/${workflow}/SKILL.md`, copilot.skill, "skill", ["copilot"], [workflow]);
29
+ }
30
+ catch {
31
+ packageProblems.push("command-render-failed");
32
+ }
33
+ }
34
+ try {
35
+ for (const asset of codexSkillAssets(COMMANDS)) {
36
+ add(asset.relativePath, asset.content, "skill", ["codex"], [asset.relativePath.split("/")[2]]);
37
+ }
38
+ }
39
+ catch {
40
+ packageProblems.push("skill-render-failed");
41
+ }
42
+ for (const [key, agent] of Object.entries(AGENTS)) {
43
+ try {
44
+ add(`.claude/agents/${key}.md`, reconstructAgentMarkdown(agent.frontmatter, agent.body), "agent", ["claude"], [key]);
45
+ add(`.github/agents/${key}.agent.md`, translateAgentToCopilot(agent.frontmatter, agent.body), "agent", ["copilot"], [key]);
46
+ }
47
+ catch {
48
+ packageProblems.push("agent-render-failed");
49
+ }
50
+ }
51
+ for (const [destination, content] of Object.entries(DOCS)) {
52
+ const metadata = DOC_METADATA[destination];
53
+ add(destination, content, "document", WORKFLOW_HOSTS, metadata?.workflows ?? [], metadata?.classification ?? "reference");
54
+ }
55
+ if (!Object.keys(COMMANDS).length || !Object.keys(AGENTS).length || !Object.keys(DOCS).length) {
56
+ packageProblems.push("empty-required-bundle");
57
+ }
58
+ return { assets, packageProblems: [...new Set(packageProblems)].slice(0, 20) };
59
+ }
60
+ /** A bounded POSIX-relative destination, valid on every supported platform. */
61
+ export function validAssetDestination(value) {
62
+ if (typeof value !== "string" || !value || value.length > 512 || /[\x00-\x1f\x7f\\:<>"|?*]/.test(value))
63
+ return false;
64
+ return value.split("/").every(s => !!s && s !== "." && s !== ".." && !/[. ]$/.test(s)
65
+ && !/^(con|prn|aux|nul|com[1-9]|lpt[1-9])(\.|$)/i.test(s));
66
+ }
@@ -0,0 +1,46 @@
1
+ import { createHash } from "node:crypto";
2
+ import { validAssetDestination, WORKFLOW_HOSTS } from "./generated-assets-catalog.js";
3
+ /** Secret-free association token; no absolute customer path is persisted. */
4
+ export function assetRootBinding(canonicalRoot) {
5
+ return createHash("sha256").update(canonicalRoot).digest("hex");
6
+ }
7
+ export function validPackagedAssetInventory(value) {
8
+ if (!value || typeof value !== "object")
9
+ return false;
10
+ const v = value;
11
+ if (v.version !== 1 || typeof v.rootBinding !== "string" || !/^[0-9a-f]{64}$/.test(v.rootBinding)
12
+ || typeof v.packageVersion !== "string" || !/^\d+\.\d+\.\d+(?:[-+][a-zA-Z0-9.-]+)?$/.test(v.packageVersion)
13
+ || !Array.isArray(v.assets) || v.assets.length > 1000)
14
+ return false;
15
+ const seen = new Set();
16
+ for (const a of v.assets) {
17
+ if (!a || !validAssetDestination(a.id) || !validAssetDestination(a.destination)
18
+ || !WORKFLOW_HOSTS.includes(a.host) || seen.has(`${a.host}:${a.id}`))
19
+ return false;
20
+ seen.add(`${a.host}:${a.id}`);
21
+ }
22
+ return true;
23
+ }
24
+ export function assetContextConfidence(context) {
25
+ if (!context.initialized || !context.host)
26
+ return { confirmed: false, reason: "unknown-initialized-host" };
27
+ if (!context.corroborated)
28
+ return { confirmed: false, reason: "uncorroborated-project" };
29
+ if (!validPackagedAssetInventory(context.inventory))
30
+ return { confirmed: false, reason: "missing-or-invalid-inventory" };
31
+ if (context.inventory.rootBinding !== assetRootBinding(context.root))
32
+ return { confirmed: false, reason: "root-binding-mismatch" };
33
+ if (!context.inventory.assets.some(a => a.host === context.host))
34
+ return { confirmed: false, reason: "unprovisioned-host" };
35
+ return { confirmed: true, reason: "root-bound-initialized-workflow" };
36
+ }
37
+ /** Explicit root+host selection authorizes current-catalog provisioning. An
38
+ * implicit selection must instead be bound to an exact-cwd inventory. */
39
+ export function authorizeAssetRepair(root, hosts, explicitPair, inventory) {
40
+ if (!hosts.length || hosts.some(h => !WORKFLOW_HOSTS.includes(h)))
41
+ return false;
42
+ if (explicitPair)
43
+ return true;
44
+ return validPackagedAssetInventory(inventory) && inventory.rootBinding === assetRootBinding(root)
45
+ && hosts.every(host => inventory.assets.some(a => a.host === host));
46
+ }
@@ -0,0 +1,26 @@
1
+ import { buildPackagedAssetCatalog } from "./generated-assets-catalog.js";
2
+ import { inspectPackagedDependency } from "./generated-assets-health.js";
3
+ import { assetFindingRemedy } from "./generated-assets-remedy.js";
4
+ /** Used only by actual local-file consumers. Backend generation tools consume
5
+ * their bundled instructions, not customer slash-command files; they have no
6
+ * declared local packaged dependencies. Agent direct reads are covered in the
7
+ * narrowly scoped canonical command failure branches instead. */
8
+ export async function checkPackagedAssetDependency(root, dependency, fs) {
9
+ const asset = buildPackagedAssetCatalog().assets.find(a => a.id === dependency.assetId && a.hosts.includes(dependency.host));
10
+ if (!asset)
11
+ return { error: "PACKAGED_DEPENDENCY_UNAVAILABLE", status: 409,
12
+ message: "Required dependency is unavailable in the invoking bundle.", dependency: "[unavailable catalog asset]",
13
+ operation: dependency.operation, remedy: "Rebuild or reinstall the package; no consumer file can be reconstructed from this bundle." };
14
+ const finding = await inspectPackagedDependency(root, asset, fs);
15
+ if (finding.status === "present")
16
+ return null;
17
+ return { error: finding.status === "missing" ? "WORKFLOW_ASSET_MISSING" : "WORKFLOW_ASSET_UNUSABLE", status: 409,
18
+ asset_status: finding.status, reason: finding.reason, message: "The requested operation requires an unavailable local workflow asset.",
19
+ dependency: asset.destination, operation: dependency.operation, remedy: assetFindingRemedy(finding, root, dependency.host, fs.platform) };
20
+ }
21
+ export function reviewedRewriteFailure(dependency, reason) {
22
+ return { error: "BAD_REQUEST", status: 400, code: reason === "missing" ? "REVIEWED_FILE_MISSING" : reason === "unsafe" ? "REVIEWED_FILE_UNSAFE" : "REVIEWED_FILE_UNREADABLE",
23
+ asset_status: reason, dependency, operation: "publish_reviewed_attachment",
24
+ message: "The agent-reviewed rewrite is unavailable for publication.",
25
+ remedy: "Recreate the rewrite from eligible source material, review it, and save the reviewed Markdown file before publishing. Canonical retrieval is source material only; it cannot reconstruct a reviewed rewrite. Do not rerun paid generation automatically." };
26
+ }
@@ -0,0 +1,50 @@
1
+ import { buildPackagedAssetCatalog } from "./generated-assets-catalog.js";
2
+ import { inspectPackagedAssets, inspectPackagedDependency } from "./generated-assets-health.js";
3
+ import { assetRootBinding } from "./generated-assets-context.js";
4
+ import { readMcpInstallState } from "./mcp-install-state.js";
5
+ import { assetFindingRemedy } from "./generated-assets-remedy.js";
6
+ export async function collectPackagedAssetDoctor(deps) {
7
+ const lines = ["", "Packaged workflow assets (read-only advisory)"];
8
+ try {
9
+ const root = await deps.realpath(deps.cwd);
10
+ const state = await readMcpInstallState(root, deps);
11
+ const inventory = state.status === "valid" ? state.state.packagedAssets : undefined;
12
+ const catalog = buildPackagedAssetCatalog();
13
+ const health = await inspectPackagedAssets({ catalog, context: { root, host: null,
14
+ initialized: false, corroborated: false, inventory }, fs: deps });
15
+ lines.push(`Historical loss: indeterminate (${state.status === "valid" ? state.state.packagedAssetsStatus ?? health.reason : state.status}). Standalone doctor has no initialized host identity.`);
16
+ if (!inventory || inventory.rootBinding !== assetRootBinding(root)) {
17
+ lines.push(deps.platform === "linux" || (!deps.platform && process.platform === "linux")
18
+ ? "For explicit provisioning, select both --project-root and --host (claude, cursor, codex, copilot) with repair-assets; directory presence does not establish an installation."
19
+ : "In-place asset repair is not supported on this platform. Re-run start-tickets for worktrees or the existing Bridge --init/install workflow for project scaffolds; directory presence does not establish an installation.");
20
+ return lines.join("\n");
21
+ }
22
+ lines.push(`Invoking package ${health.invokingVersion}; recorded package ${inventory.packageVersion}.`);
23
+ const records = inventory.assets.slice(0, 1000);
24
+ const findings = [];
25
+ const deadline = Date.now() + 1000;
26
+ for (const record of records) {
27
+ if (Date.now() >= deadline) {
28
+ lines.push("Remaining inspection: indeterminate (time budget).");
29
+ break;
30
+ }
31
+ const asset = catalog.assets.find(a => a.id === record.id && a.destination === record.destination && a.hosts.includes(record.host));
32
+ if (!asset) {
33
+ lines.push("Package problem: recorded asset unavailable from invoking bundle.");
34
+ continue;
35
+ }
36
+ // Exact physical checks are observations, not claims about deletion history.
37
+ const result = await inspectPackagedDependency(root, asset, deps, deadline - Date.now());
38
+ findings.push(result);
39
+ if (result.status !== "present" && lines.length < 20) {
40
+ lines.push(`${record.host}: ${result.status} (${result.reason}) ${asset.destination}`);
41
+ lines.push(assetFindingRemedy(result, root, record.host, deps.platform));
42
+ }
43
+ }
44
+ lines.push(`Observed readable files: ${findings.filter(f => f.status === "present").length}; absent: ${findings.filter(f => f.status === "missing").length}; other unusable: ${findings.filter(f => f.status !== "present" && f.status !== "missing").length}.`);
45
+ return lines.join("\n");
46
+ }
47
+ catch {
48
+ return [...lines, "Inspection indeterminate: filesystem metadata or inventory unavailable."].join("\n");
49
+ }
50
+ }
@@ -0,0 +1,65 @@
1
+ import { VERSION } from "./version.generated.js";
2
+ import { assetContextConfidence } from "./generated-assets-context.js";
3
+ import { inspectAssetPath } from "./generated-assets-paths.js";
4
+ export const MAX_ASSET_INSPECTIONS = 1000;
5
+ /** Inspect only historically provisioned applicable assets; newer bundle entries
6
+ * are reported separately and never diagnosed as consumer loss. */
7
+ export async function inspectPackagedAssets(options) {
8
+ const { catalog, context, fs } = options;
9
+ const confidence = assetContextConfidence(context);
10
+ const result = { ...confidence, findings: [],
11
+ packageProblems: catalog.packageProblems.slice(0, 20), newlyIntroduced: [], invokingVersion: VERSION,
12
+ recordedVersion: context.inventory?.packageVersion };
13
+ if (!confidence.confirmed || !context.inventory)
14
+ return result;
15
+ const applicable = catalog.assets.filter(a => a.hosts.includes(context.host));
16
+ const byId = new Map(applicable.map(a => [a.id, a]));
17
+ const recorded = context.inventory.assets.filter(a => a.host === context.host);
18
+ if (recorded.length > MAX_ASSET_INSPECTIONS)
19
+ return { ...result, confirmed: false, reason: "inspection-limit" };
20
+ const recordedIds = new Set(recorded.map(a => a.id));
21
+ result.newlyIntroduced = applicable.filter(a => !recordedIds.has(a.id)).map(a => a.destination).slice(0, MAX_ASSET_INSPECTIONS);
22
+ const deadline = Date.now() + Math.min(1000, Math.max(1, options.budgetMs ?? 1000));
23
+ // Bounded serial probing; no unbounded Promise fan-out.
24
+ for (const record of recorded) {
25
+ const asset = byId.get(record.id);
26
+ if (!asset || record.destination !== asset.destination) {
27
+ result.packageProblems.push("recorded-asset-unavailable-in-bundle");
28
+ continue;
29
+ }
30
+ const remaining = deadline - Date.now();
31
+ if (remaining <= 0)
32
+ return { ...result, confirmed: false, reason: "inspection-timeout" };
33
+ let timer;
34
+ try {
35
+ const finding = await Promise.race([
36
+ inspectPackagedDependency(context.root, asset, fs),
37
+ new Promise(resolve => { timer = setTimeout(() => resolve(null), remaining); }),
38
+ ]);
39
+ if (!finding)
40
+ return { ...result, confirmed: false, reason: "inspection-timeout" };
41
+ result.findings.push(finding);
42
+ }
43
+ finally {
44
+ if (timer)
45
+ clearTimeout(timer);
46
+ }
47
+ }
48
+ result.packageProblems = [...new Set(result.packageProblems)].slice(0, 20);
49
+ return result;
50
+ }
51
+ /** Fresh exact dependency inspection is independent of historical confidence. */
52
+ export async function inspectPackagedDependency(root, asset, fs, budgetMs = 1000) {
53
+ let timer;
54
+ try {
55
+ const finding = await Promise.race([inspectAssetPath(root, asset.destination, fs),
56
+ new Promise(resolve => {
57
+ timer = setTimeout(() => resolve({ status: "indeterminate", reason: "metadata-timeout" }), Math.max(1, Math.min(1000, budgetMs)));
58
+ })]);
59
+ return { id: asset.id, destination: asset.destination, workflows: asset.workflows, ...finding };
60
+ }
61
+ finally {
62
+ if (timer)
63
+ clearTimeout(timer);
64
+ }
65
+ }
@@ -0,0 +1,17 @@
1
+ import { WORKFLOW_HOSTS } from "./generated-assets-catalog.js";
2
+ /** Exact identities with repository-owned handshake evidence. Cursor and
3
+ * Copilot VS Code remain unknown until an observed handshake is available.
4
+ * Copilot CLI evidence: learn-tool-gating.ts, CLI 1.0.71 probe (2026-08-26). */
5
+ const IDENTITIES = {
6
+ "claude-code": "claude", codex: "codex", "github-copilot-developer": "copilot",
7
+ };
8
+ export function isWorkflowHost(value) {
9
+ return typeof value === "string" && WORKFLOW_HOSTS.includes(value);
10
+ }
11
+ export function resolveWorkflowHost(clientName, marker) {
12
+ const host = typeof clientName === "string" && Object.hasOwn(IDENTITIES, clientName) ? IDENTITIES[clientName] : null;
13
+ const marked = marker === "claude_code" ? "claude" : marker;
14
+ if (marked != null && (!isWorkflowHost(marked) || (host && host !== marked)))
15
+ return { host: null, reason: "conflicting-or-invalid-identity" };
16
+ return { host, reason: host ? "initialized-client" : "unknown-client" };
17
+ }