@bridge_gpt/mcp-server 0.2.36 → 0.2.37

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 (90) hide show
  1. package/README.md +44 -6
  2. package/build/base-url.js +79 -0
  3. package/build/bridge-api-urls.js +9 -0
  4. package/build/chain-orchestrator.js +93 -15
  5. package/build/claude-user-config-doctor.js +317 -0
  6. package/build/commands.generated.js +2 -1
  7. package/build/conductor/bridge-api-client.js +178 -4
  8. package/build/conductor-bin.js +1 -1
  9. package/build/conductor-bundle-artifacts.js +7 -6
  10. package/build/credential-store.js +205 -4
  11. package/build/direct-ticket-tools.js +70 -0
  12. package/build/doctor.js +239 -80
  13. package/build/executor/cli.js +51 -1
  14. package/build/executor/credentials.js +1 -7
  15. package/build/executor/deps.js +18 -1
  16. package/build/executor/env.js +51 -25
  17. package/build/executor/heartbeat.js +138 -17
  18. package/build/executor/http-client.js +49 -8
  19. package/build/executor/job-errors.js +4 -0
  20. package/build/executor/job-runner.js +422 -22
  21. package/build/executor/observation.js +130 -0
  22. package/build/executor/permissions.js +104 -8
  23. package/build/executor/preflight.js +32 -0
  24. package/build/executor/runner.js +8 -0
  25. package/build/executor/test-clock.js +67 -3
  26. package/build/executor/types.js +4 -1
  27. package/build/executor/worker-command.js +11 -3
  28. package/build/executor/worker-config-isolation.js +287 -0
  29. package/build/executor/worker-finalization.js +68 -14
  30. package/build/executor/worktree.js +46 -4
  31. package/build/index.js +614 -244
  32. package/build/init.js +363 -73
  33. package/build/install-bridge.js +568 -80
  34. package/build/launcher-config-inspection.js +351 -0
  35. package/build/mcp-invoke.js +49 -6
  36. package/build/mcp-provisioning.js +30 -7
  37. package/build/mcp-registration-doctor.js +14 -5
  38. package/build/notifications.js +553 -0
  39. package/build/pipeline-orchestrator.js +146 -4
  40. package/build/pipeline-utils.js +3 -0
  41. package/build/pipelines.generated.js +22 -9
  42. package/build/plan-execution-ledger.js +550 -0
  43. package/build/plan-phase-routing.js +272 -0
  44. package/build/plane/alembic-head.js +110 -0
  45. package/build/plane/build-freshness.js +167 -0
  46. package/build/plane/cli.js +480 -0
  47. package/build/plane/defaults.js +266 -0
  48. package/build/plane/manifest.js +377 -0
  49. package/build/plane/member-logs.js +147 -0
  50. package/build/plane/member-roster.js +147 -0
  51. package/build/plane/preflight.js +289 -0
  52. package/build/plane/shutdown.js +195 -0
  53. package/build/plane/status.js +125 -0
  54. package/build/plane/supervisor.js +569 -0
  55. package/build/plane/test-fakes.js +156 -0
  56. package/build/plane/types.js +75 -0
  57. package/build/readme.generated.js +1 -1
  58. package/build/run-unit-tests-launcher.js +2 -0
  59. package/build/setup-epic.js +662 -27
  60. package/build/sfcc/log-gate.js +38 -11
  61. package/build/sfcc/log-query.js +55 -15
  62. package/build/sfcc/ocapi-shape.js +51 -14
  63. package/build/sfcc/output.js +41 -11
  64. package/build/sfcc/permissions.js +24 -2
  65. package/build/sfcc/read-projection.js +181 -0
  66. package/build/sfcc/read-result.js +158 -0
  67. package/build/sfcc/reads-custom-object-def.js +29 -18
  68. package/build/sfcc/reads-site-preference.js +75 -29
  69. package/build/sfcc/reads-system-object.js +40 -34
  70. package/build/sfcc/sfcc-result.js +106 -0
  71. package/build/sfcc/tool-wrapper.js +56 -13
  72. package/build/sfcc/write-grants.js +45 -22
  73. package/build/sfcc/write-guard.js +21 -13
  74. package/build/sfcc/write-result.js +61 -14
  75. package/build/sfcc/write-tool-common.js +126 -32
  76. package/build/sfcc/writes-system-object.js +11 -50
  77. package/build/start-tickets-prereqs.js +129 -0
  78. package/build/start-tickets.js +17 -13
  79. package/build/ticket-backend-metadata.js +59 -0
  80. package/build/ticket-key-utils.js +92 -0
  81. package/build/tool-error-envelope.js +71 -0
  82. package/build/tool-surface-gating.js +72 -0
  83. package/build/update-status.js +102 -0
  84. package/build/upgrade-advice.js +47 -0
  85. package/build/upgrade-cli.js +417 -101
  86. package/build/version.generated.js +1 -1
  87. package/build/worktree-core.js +73 -0
  88. package/docs/CONDUCTOR.md +23 -8
  89. package/package.json +3 -3
  90. package/pipelines/implement-ticket.json +15 -5
@@ -524,6 +524,38 @@ export const BOOTSTRAP_PENDING_FINGERPRINT_FIELD = "BOOTSTRAP_INVITE_FINGERPRINT
524
524
  * is NEVER logged, printed, or placed in an error string.
525
525
  */
526
526
  export const BOOTSTRAP_PENDING_REPLAY_TOKEN_FIELD = "BOOTSTRAP_SELF_SERVE_INVITE";
527
+ /**
528
+ * Signup-challenge metadata fields (BAPI-757).
529
+ *
530
+ * A self-serve run now passes through TWO durable pending shapes in sequence:
531
+ *
532
+ * 1. **Challenge-only** — these three fields and nothing else. Written after the
533
+ * server accepts the email and before the CLI prompts for the code, so a
534
+ * terminal closed mid-prompt can resume the SAME challenge instead of
535
+ * requesting a second email against the user's own per-address send budget.
536
+ * 2. **Exchange-resumable** — the pre-existing `key_secret` + fingerprint +
537
+ * replay-token shape, written once verification returns an invite token.
538
+ *
539
+ * The transition is a single atomic replacement: {@link buildPendingEntry} removes
540
+ * all three challenge fields when it writes shape 2, so a record is never both.
541
+ *
542
+ * **NONE of these is a secret, and the verification CODE is deliberately not among
543
+ * them.** The code is never written to disk, never passed to a writer, and has no
544
+ * field name here — on restart the CLI re-prompts for it rather than recovering it.
545
+ * That is what keeps a readable credential store from being enough to complete
546
+ * someone else's signup: the store holds the handle, the mailbox holds the code.
547
+ */
548
+ export const BOOTSTRAP_PENDING_SIGNUP_EMAIL_FIELD = "BOOTSTRAP_SIGNUP_EMAIL";
549
+ /** Public challenge handle. Lookup material, not an authorization. */
550
+ export const BOOTSTRAP_PENDING_CHALLENGE_HANDLE_FIELD = "BOOTSTRAP_CHALLENGE_HANDLE";
551
+ /** Server-supplied ISO-8601 challenge expiry, used only to decide resume vs. restart. */
552
+ export const BOOTSTRAP_PENDING_CHALLENGE_EXPIRES_FIELD = "BOOTSTRAP_CHALLENGE_EXPIRES_AT";
553
+ /** Every challenge-only field, for the removal in {@link buildPendingEntry}. */
554
+ const BOOTSTRAP_PENDING_CHALLENGE_FIELDS = [
555
+ BOOTSTRAP_PENDING_SIGNUP_EMAIL_FIELD,
556
+ BOOTSTRAP_PENDING_CHALLENGE_HANDLE_FIELD,
557
+ BOOTSTRAP_PENDING_CHALLENGE_EXPIRES_FIELD,
558
+ ];
527
559
  /** Pending target for a repo: `bootstrap-pending:<repo>`. */
528
560
  export function getBootstrapPendingTarget(repoName) {
529
561
  return `${BOOTSTRAP_PENDING_TARGET_PREFIX}${(repoName ?? "").trim()}`;
@@ -596,6 +628,15 @@ function buildPendingEntry(existing, keySecret, inviteFingerprint, replayToken)
596
628
  else {
597
629
  delete next[BOOTSTRAP_PENDING_REPLAY_TOKEN_FIELD];
598
630
  }
631
+ // BAPI-757: writing the exchange-resumable shape RETIRES the challenge-only one,
632
+ // in this same durable replacement. The challenge has been verified — its handle
633
+ // is spent and its expiry is meaningless — and leaving the fields behind would
634
+ // produce a record that reads as both shapes at once, which the lookup would
635
+ // then have to arbitrate. Removing them here means the two shapes are mutually
636
+ // exclusive by construction rather than by convention.
637
+ for (const field of BOOTSTRAP_PENDING_CHALLENGE_FIELDS) {
638
+ delete next[field];
639
+ }
599
640
  return next;
600
641
  }
601
642
  /**
@@ -767,6 +808,100 @@ export async function prepareBootstrapPendingCredential(params, deps) {
767
808
  return { ok: true, path: primaryPath, target, keySecret, reused: false };
768
809
  }, (error) => ({ ok: false, path: primaryPath, target, kind: "lock-error", error }));
769
810
  }
811
+ /**
812
+ * Durably persist signup-challenge metadata BEFORE the CLI prompts for the code
813
+ * (BAPI-757).
814
+ *
815
+ * The ordering is the point, and it mirrors the load-bearing
816
+ * persist-before-exchange rule one phase earlier: the server has already sent an
817
+ * email and already charged the user's per-address send budget by the time this
818
+ * runs, so the handle must reach disk before anything can interrupt the run. A
819
+ * terminal closed at the code prompt then resumes the SAME challenge, and the
820
+ * original emailed code still works.
821
+ *
822
+ * Written through {@link durablyReplaceCredentialStoreJson}, so it is fsync-backed
823
+ * and mode 0600 on POSIX exactly like every other record here — the write either
824
+ * lands or reports a failure; it never half-lands.
825
+ *
826
+ * **The verification code is not a parameter and has no field.** There is no way to
827
+ * pass one in, which is what makes "the code never touches disk" a property of the
828
+ * type rather than a rule someone has to remember.
829
+ *
830
+ * Refuses to clobber an existing pending record that belongs to something else —
831
+ * an ordinary `--invite` redemption or a completed self-serve exchange awaiting
832
+ * replay. That record may be the only proof that can recover a live admin key.
833
+ */
834
+ export async function prepareSelfServeBootstrapChallenge(params, deps) {
835
+ const primaryPath = getPrimaryCredentialStorePath(deps);
836
+ const repoName = (params.repoName ?? "").trim();
837
+ const signupEmail = (params.signupEmail ?? "").trim();
838
+ const challengeHandle = (params.challengeHandle ?? "").trim();
839
+ const challengeExpiresAt = (params.challengeExpiresAt ?? "").trim();
840
+ const target = getBootstrapPendingTarget(repoName);
841
+ if (repoName.length === 0) {
842
+ return {
843
+ ok: false,
844
+ path: primaryPath,
845
+ target,
846
+ kind: "invalid-repo",
847
+ error: "Cannot record a signup challenge: a non-empty repo name is required.",
848
+ };
849
+ }
850
+ // All three or none. A partial record is exactly the `pending-challenge-malformed`
851
+ // state the lookup fails closed on, so refusing to WRITE one keeps that state
852
+ // reachable only through external tampering. The rejected values never appear in
853
+ // the error — one of them is the invitee's address.
854
+ if (signupEmail.length === 0 ||
855
+ challengeHandle.length === 0 ||
856
+ challengeExpiresAt.length === 0) {
857
+ return {
858
+ ok: false,
859
+ path: primaryPath,
860
+ target,
861
+ kind: "invalid-challenge",
862
+ error: "Cannot record a signup challenge: the challenge metadata was incomplete.",
863
+ };
864
+ }
865
+ return withCredentialStoreLock(deps, async () => {
866
+ const loaded = await loadStoreForMutation(deps);
867
+ if (!loaded.ok) {
868
+ return { ok: false, path: primaryPath, target, kind: loaded.kind, error: loaded.error };
869
+ }
870
+ const base = loaded.base;
871
+ const existing = base[target];
872
+ // Never overwrite a record that holds a key_secret: that record is the only
873
+ // proof that can replay a redemption, and if its exchange already succeeded
874
+ // the admin key it created becomes unrecoverable. A challenge-only record
875
+ // (no key_secret) is ours to replace — that is the ordinary "requested a new
876
+ // challenge after the old one expired" path.
877
+ if (readPendingField(existing, BOOTSTRAP_PENDING_SECRET_FIELD).length > 0) {
878
+ return {
879
+ ok: false,
880
+ path: primaryPath,
881
+ target,
882
+ kind: "pending-conflict",
883
+ error: `A pending bootstrap credential already exists at ${target} in ${primaryPath}. ` +
884
+ "This run will not overwrite it. Finish or remove that setup attempt first.",
885
+ };
886
+ }
887
+ const next = {
888
+ ...base,
889
+ [target]: {
890
+ // Deliberately NOT spread from `existing`: a challenge-only record is a
891
+ // complete replacement, and inheriting stray fields from a previous
892
+ // attempt is how a record ends up describing two challenges at once.
893
+ [BOOTSTRAP_PENDING_SIGNUP_EMAIL_FIELD]: signupEmail,
894
+ [BOOTSTRAP_PENDING_CHALLENGE_HANDLE_FIELD]: challengeHandle,
895
+ [BOOTSTRAP_PENDING_CHALLENGE_EXPIRES_FIELD]: challengeExpiresAt,
896
+ },
897
+ };
898
+ const written = await durablyReplaceCredentialStoreJson(primaryPath, next, deps);
899
+ if (!written.ok) {
900
+ return { ok: false, path: primaryPath, target, kind: written.kind, error: written.error };
901
+ }
902
+ return { ok: true, path: primaryPath, target };
903
+ }, (error) => ({ ok: false, path: primaryPath, target, kind: "lock-error", error }));
904
+ }
770
905
  /**
771
906
  * Atomically move a pending record to a different repo name, preserving the exact
772
907
  * same `key_secret` — used when the server answers `409 repo_name_taken` and the
@@ -913,6 +1048,36 @@ export async function promoteBootstrapPendingCredential(params, deps) {
913
1048
  return { ok: true, path: primaryPath, target, action: hadKey ? "updated" : "created" };
914
1049
  }, (error) => ({ ok: false, path: primaryPath, target, kind: "lock-error", error }));
915
1050
  }
1051
+ /**
1052
+ * Classify a pending entry's signup-challenge fields (BAPI-757).
1053
+ *
1054
+ * All three present → `live` or `expired`. None present → `absent`. **Any other
1055
+ * combination is `malformed`, and that is a failure rather than a fallback**: a
1056
+ * partial record may correspond to a challenge whose email was already sent, and
1057
+ * silently treating it as absent would request another one.
1058
+ *
1059
+ * An unparseable expiry is `malformed` for the same reason. Guessing at it would
1060
+ * mean either discarding a live challenge or trusting a value nothing produced.
1061
+ */
1062
+ function classifySignupChallengeRecord(entry, now) {
1063
+ const signupEmail = readPendingField(entry, BOOTSTRAP_PENDING_SIGNUP_EMAIL_FIELD);
1064
+ const challengeHandle = readPendingField(entry, BOOTSTRAP_PENDING_CHALLENGE_HANDLE_FIELD);
1065
+ const challengeExpiresAt = readPendingField(entry, BOOTSTRAP_PENDING_CHALLENGE_EXPIRES_FIELD);
1066
+ const present = [signupEmail, challengeHandle, challengeExpiresAt].filter((value) => value.length > 0).length;
1067
+ if (present === 0)
1068
+ return { kind: "absent" };
1069
+ if (present < 3)
1070
+ return { kind: "malformed" };
1071
+ const expiresAtMs = Date.parse(challengeExpiresAt);
1072
+ if (Number.isNaN(expiresAtMs))
1073
+ return { kind: "malformed" };
1074
+ // Only a SUPPLIED clock can conclude expiry. Without one, the challenge is
1075
+ // reported live — see the `now` doc comment for why that direction is safer.
1076
+ if (typeof now === "number" && Number.isFinite(now) && expiresAtMs <= now) {
1077
+ return { kind: "expired" };
1078
+ }
1079
+ return { kind: "live", signupEmail, challengeHandle, challengeExpiresAt };
1080
+ }
916
1081
  /**
917
1082
  * Look up the SELF-SERVE pending record for a repo (BAPI-667) — the read that
918
1083
  * makes a self-serve retry resume instead of minting a second workspace.
@@ -924,8 +1089,13 @@ export async function promoteBootstrapPendingCredential(params, deps) {
924
1089
  * logical target and the store path and nothing else.
925
1090
  *
926
1091
  * States:
927
- * - `none` — no record, or a record with nothing at stake (no usable secret and
928
- * no replay material). The caller mints normally.
1092
+ * - `none` — no record, or a record with nothing at stake (no usable secret, no
1093
+ * replay material, and no challenge metadata). The caller requests normally.
1094
+ * - `challenge` — a LIVE signup challenge (BAPI-757). The caller resumes at the
1095
+ * code prompt and MUST NOT request or resend: the emailed code is still valid,
1096
+ * and requesting again would spend the user's own per-address send budget.
1097
+ * - `challenge-expired` — a signup challenge conclusively past its expiry against
1098
+ * the supplied clock. The caller may request a fresh one.
929
1099
  * - `resumable` — a complete self-serve record. The caller MUST reuse it and MUST
930
1100
  * NOT mint.
931
1101
  * - `pending-not-self-serve` — a real pending record with a `key_secret` but no
@@ -972,9 +1142,40 @@ export async function lookupSelfServeBootstrapPendingCredential(params, deps) {
972
1142
  const fingerprint = readPendingField(entry, BOOTSTRAP_PENDING_FINGERPRINT_FIELD);
973
1143
  const replayToken = readPendingField(entry, BOOTSTRAP_PENDING_REPLAY_TOKEN_FIELD);
974
1144
  if (replayToken.length === 0) {
975
- // No replay material: either nothing is stored, or an ordinary invite
976
- // redemption owns this target. Only the latter is a conflict.
1145
+ // No replay material: either nothing is stored, a signup challenge is in
1146
+ // flight, or an ordinary invite redemption owns this target.
977
1147
  if (keySecret.length === 0) {
1148
+ // BAPI-757: checked BEFORE returning `none`, because a challenge-only
1149
+ // record legitimately has no key_secret and no fingerprint. Treating it
1150
+ // as "nothing stored" would request a second verification email against
1151
+ // the user's own per-address budget and invalidate nothing — the classic
1152
+ // way a resumable state becomes a lockout.
1153
+ const challenge = classifySignupChallengeRecord(entry, params.now);
1154
+ if (challenge.kind === "malformed") {
1155
+ return {
1156
+ ok: false,
1157
+ path: primaryPath,
1158
+ target,
1159
+ kind: "pending-challenge-malformed",
1160
+ error: `The pending signup record at ${target} in ${primaryPath} is incomplete, so it ` +
1161
+ "cannot be resumed and will not be discarded automatically (a verification email " +
1162
+ "may already have been sent). Ask your Bridge API operator to recover it.",
1163
+ };
1164
+ }
1165
+ if (challenge.kind === "live") {
1166
+ return {
1167
+ ok: true,
1168
+ state: "challenge",
1169
+ path: primaryPath,
1170
+ target,
1171
+ signupEmail: challenge.signupEmail,
1172
+ challengeHandle: challenge.challengeHandle,
1173
+ challengeExpiresAt: challenge.challengeExpiresAt,
1174
+ };
1175
+ }
1176
+ if (challenge.kind === "expired") {
1177
+ return { ok: true, state: "challenge-expired", path: primaryPath, target };
1178
+ }
978
1179
  return { ok: true, state: "none", path: primaryPath, target };
979
1180
  }
980
1181
  return {
@@ -0,0 +1,70 @@
1
+ /**
2
+ * direct-ticket-tools — agent-facing entry points for the ticket updater and the
3
+ * single-ticket estimator (BAPI-711).
4
+ *
5
+ * Both flows were previously reachable only through a Jira-shaped webhook. In
6
+ * Jira mode Jira is the driver; in local mode the driver is the agent, which
7
+ * already talks to Bridge directly — so the fix is not a local event emitter, it
8
+ * is a direct entry point. These tools are that entry point.
9
+ *
10
+ * Two properties are load-bearing:
11
+ *
12
+ * - The repository is INJECTED from `BAPI_REPO_NAME`, never accepted from the
13
+ * agent. An agent-supplied repo would let a tool call address a repository
14
+ * the session was not configured for; the backend re-authorizes it anyway,
15
+ * but the schema should not invite the attempt.
16
+ * - Every outcome — success, duplicate-active, and backend error — resolves to
17
+ * a normal text content envelope. Throwing through the MCP transport turns a
18
+ * recoverable 429 into a dead session.
19
+ */
20
+ function text(value) {
21
+ return { content: [{ type: "text", text: value }] };
22
+ }
23
+ /**
24
+ * POST a direct-invocation request and render its outcome as normal content.
25
+ *
26
+ * A connection failure is reported as text rather than rethrown: the submission
27
+ * outcome is genuinely unknown at that point, and the recoverable action (poll
28
+ * `get_ticket_state`) is the same either way.
29
+ */
30
+ async function postDirectInvocation(deps, path, body, ticketNumber) {
31
+ let resp;
32
+ try {
33
+ resp = await (deps.fetchImpl ?? fetch)(deps.buildUrl(path), {
34
+ method: "POST",
35
+ headers: await deps.getPostHeaders(),
36
+ body: JSON.stringify({ ...body, repo_name: deps.repoName }),
37
+ });
38
+ }
39
+ catch {
40
+ return text(`The request could not be delivered, so it is unknown whether it was accepted. ` +
41
+ `Call get_ticket_state for ${ticketNumber} to check before retrying.`);
42
+ }
43
+ if (!resp.ok) {
44
+ return text(await deps.handleResponse(resp));
45
+ }
46
+ return text(await resp.text());
47
+ }
48
+ export async function runRequestTicketUpdate(args, deps) {
49
+ return postDirectInvocation(deps, `/ticket/${encodeURIComponent(args.ticket_number)}/generate-ticket-update`, {}, args.ticket_number);
50
+ }
51
+ export async function runRequestEstimate(args, deps) {
52
+ return postDirectInvocation(deps, `/ticket/${encodeURIComponent(args.ticket_number)}/generate-estimate`,
53
+ // Sent explicitly rather than omitted so the backend default and the tool
54
+ // default can never drift apart silently.
55
+ { recreate: args.recreate === true }, args.ticket_number);
56
+ }
57
+ export async function runGetTicketUpdateReview(args, deps) {
58
+ const url = deps.buildGetUrl(`/ticket/${encodeURIComponent(args.ticket_number)}/ticket-update-review`, { repo_name: deps.repoName });
59
+ let resp;
60
+ try {
61
+ resp = await (deps.fetchImpl ?? fetch)(url, { headers: await deps.getHeaders() });
62
+ }
63
+ catch {
64
+ return text(`The held-for-review proposal for ${args.ticket_number} could not be fetched. Retry shortly.`);
65
+ }
66
+ if (!resp.ok) {
67
+ return text(await deps.handleResponse(resp));
68
+ }
69
+ return text(await resp.text());
70
+ }