@bridge_gpt/mcp-server 0.2.34 → 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.
- package/README.md +490 -366
- package/build/agent-capabilities/probe-context.js +8 -1
- package/build/agent-capabilities/probes.js +7 -1
- package/build/agents.generated.js +1 -1
- package/build/base-url.js +79 -0
- package/build/bridge-api-urls.js +9 -0
- package/build/chain-orchestrator.js +93 -15
- package/build/claude-review-workflow.js +264 -0
- package/build/claude-user-config-doctor.js +317 -0
- package/build/cli-release.js +53 -0
- package/build/commands.generated.js +6 -5
- package/build/conductor/bridge-api-client.js +393 -4
- package/build/conductor/deny-enforcement-preflight.js +1 -0
- package/build/conductor/done-gate.js +44 -5
- package/build/conductor/epic-reconcile.js +6 -0
- package/build/conductor/install-doctor.js +462 -0
- package/build/conductor-bin.js +3 -3
- package/build/conductor-bundle-artifacts.js +37 -15
- package/build/credential-store.js +205 -4
- package/build/direct-ticket-tools.js +70 -0
- package/build/doctor.js +473 -81
- package/build/executor/cli.js +83 -6
- package/build/executor/credentials.js +39 -11
- package/build/executor/deps.js +32 -1
- package/build/executor/env.js +71 -28
- package/build/executor/heartbeat.js +138 -17
- package/build/executor/http-client.js +49 -8
- package/build/executor/index.js +4 -0
- package/build/executor/job-errors.js +4 -0
- package/build/executor/job-runner.js +530 -20
- package/build/executor/observation.js +130 -0
- package/build/executor/permissions.js +114 -8
- package/build/executor/preflight.js +127 -8
- package/build/executor/prompt-spec.js +51 -0
- package/build/executor/runner.js +23 -2
- package/build/executor/service-unit.js +876 -0
- package/build/executor/test-clock.js +75 -3
- package/build/executor/types.js +4 -18
- package/build/executor/worker-command.js +73 -12
- package/build/executor/worker-config-isolation.js +287 -0
- package/build/executor/worker-finalization.js +68 -14
- package/build/executor/worktree.js +46 -4
- package/build/index.js +1039 -237
- package/build/init.js +503 -111
- package/build/install-bridge-conductor.js +491 -0
- package/build/install-bridge.js +1188 -247
- package/build/install-reexec.js +233 -0
- package/build/launcher-config-inspection.js +351 -0
- package/build/mcp-host-config.js +11 -1
- package/build/mcp-install-state.js +32 -0
- package/build/mcp-invoke.js +49 -6
- package/build/mcp-provisioning.js +52 -13
- package/build/mcp-registration-doctor.js +14 -5
- package/build/notifications.js +553 -0
- package/build/pipeline-orchestrator.js +146 -4
- package/build/pipeline-utils.js +3 -0
- package/build/pipelines.generated.js +32 -13
- package/build/plan-execution-ledger.js +550 -0
- package/build/plan-phase-routing.js +272 -0
- package/build/plane/alembic-head.js +110 -0
- package/build/plane/build-freshness.js +167 -0
- package/build/plane/cli.js +480 -0
- package/build/plane/defaults.js +266 -0
- package/build/plane/manifest.js +377 -0
- package/build/plane/member-logs.js +147 -0
- package/build/plane/member-roster.js +147 -0
- package/build/plane/preflight.js +289 -0
- package/build/plane/shutdown.js +195 -0
- package/build/plane/status.js +125 -0
- package/build/plane/supervisor.js +569 -0
- package/build/plane/test-fakes.js +156 -0
- package/build/plane/types.js +75 -0
- package/build/readme.generated.js +1 -1
- package/build/run-unit-tests-launcher.js +259 -0
- package/build/setup-epic.js +772 -28
- package/build/sfcc/log-gate.js +38 -11
- package/build/sfcc/log-query.js +55 -15
- package/build/sfcc/ocapi-shape.js +51 -14
- package/build/sfcc/output.js +41 -11
- package/build/sfcc/permissions.js +24 -2
- package/build/sfcc/read-projection.js +181 -0
- package/build/sfcc/read-result.js +158 -0
- package/build/sfcc/reads-custom-object-def.js +29 -18
- package/build/sfcc/reads-site-preference.js +75 -29
- package/build/sfcc/reads-system-object.js +40 -34
- package/build/sfcc/sfcc-result.js +106 -0
- package/build/sfcc/tool-wrapper.js +56 -13
- package/build/sfcc/write-grants.js +45 -22
- package/build/sfcc/write-guard.js +21 -13
- package/build/sfcc/write-result.js +61 -14
- package/build/sfcc/write-tool-common.js +126 -32
- package/build/sfcc/writes-system-object.js +11 -50
- package/build/start-tickets-prereqs.js +129 -0
- package/build/start-tickets.js +17 -13
- package/build/ticket-backend-metadata.js +59 -0
- package/build/ticket-key-utils.js +92 -0
- package/build/tool-error-envelope.js +71 -0
- package/build/tool-surface-gating.js +72 -0
- package/build/update-status.js +102 -0
- package/build/upgrade-advice.js +47 -0
- package/build/upgrade-cli.js +412 -110
- package/build/version.generated.js +1 -1
- package/build/worktree-core.js +73 -0
- package/docs/CONDUCTOR.md +132 -6
- package/docs/install/mcp-tool-integrations.md +29 -21
- package/package.json +9 -6
- package/pipelines/implement-ticket.json +19 -4
- package/build/conductor/supervisor-judgment-python.js +0 -141
- package/build/conductor/supervisor-judgment.js +0 -215
|
@@ -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
|
|
928
|
-
* no
|
|
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,
|
|
976
|
-
//
|
|
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
|
+
}
|