@bridge_gpt/mcp-server 0.2.52 → 0.2.54

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 (52) hide show
  1. package/README.md +121 -15
  2. package/build/agent-launchers/claude.js +3 -3
  3. package/build/agent-launchers/prompt.js +8 -11
  4. package/build/base-ref.js +33 -9
  5. package/build/bounded-wait.js +174 -0
  6. package/build/commands.generated.js +7 -5
  7. package/build/conductor/bridge-api-client.js +97 -8
  8. package/build/conductor/cli.js +23 -0
  9. package/build/conductor/doctor.js +428 -5
  10. package/build/conductor/epic-runtime.js +133 -97
  11. package/build/conductor/install-doctor.js +65 -656
  12. package/build/conductor/readiness-cli.js +152 -0
  13. package/build/conductor/readiness-sections.js +666 -0
  14. package/build/conductor/readiness.js +795 -0
  15. package/build/conductor/run-branch.js +137 -0
  16. package/build/conductor/test-run-branch-vectors.js +165 -0
  17. package/build/conductor/tools.js +56 -3
  18. package/build/conductor-bin.js +21 -17
  19. package/build/doctor.js +68 -1
  20. package/build/drive-epic.js +287 -51
  21. package/build/executor/claim-scope.js +104 -0
  22. package/build/executor/cli.js +14 -25
  23. package/build/executor/env-file-guard.js +82 -3
  24. package/build/executor/job-runner.js +60 -0
  25. package/build/index.js +4496 -4697
  26. package/build/install-doctor.js +154 -2
  27. package/build/local-artifact-storage.js +130 -0
  28. package/build/pipelines.generated.js +17 -10
  29. package/build/plane/alembic-head.js +40 -11
  30. package/build/plane/build-freshness.js +22 -11
  31. package/build/plane/cli.js +285 -36
  32. package/build/plane/manifest.js +209 -1
  33. package/build/plane/member-roster.js +70 -0
  34. package/build/plane/preflight.js +363 -48
  35. package/build/plane/shutdown.js +14 -1
  36. package/build/plane/status.js +35 -1
  37. package/build/plane/supervisor.js +546 -164
  38. package/build/plane/types.js +61 -2
  39. package/build/polling-policy.js +72 -0
  40. package/build/readiness-check.js +412 -0
  41. package/build/readme.generated.js +1 -1
  42. package/build/review-generation.js +219 -0
  43. package/build/run-unit-tests-launcher.js +5 -0
  44. package/build/setup-epic.js +514 -23
  45. package/build/ticket-key-utils.js +4 -3
  46. package/build/ticket-review-artifact-gate.js +461 -0
  47. package/build/upgrade-cli.js +5 -26
  48. package/build/version.generated.js +3 -3
  49. package/docs/install/mcp-tool-integrations.md +23 -1
  50. package/package.json +2 -2
  51. package/pipelines/{full-automation.json → idea-to-pr.json} +1 -1
  52. package/pipelines/review-ticket.json +17 -4
@@ -20,6 +20,7 @@
20
20
  * required prerequisites only.
21
21
  */
22
22
  import path from "path";
23
+ import { createReadinessCheck, createReadinessCheckSafely, } from "./readiness-check.js";
23
24
  import { resolveBapiCredentials } from "./credential-store.js";
24
25
  import { detectClaudeLogin, formatClaudeLoginAdvisory } from "./claude-login.js";
25
26
  import { getProjectJsonTargets, hostAdapterForTarget } from "./mcp-host-targets.js";
@@ -105,7 +106,16 @@ export async function resolveInstallDoctorTarget(deps) {
105
106
  }
106
107
  return { repoName, repoSource, baseUrl: baseUrl ?? DEFAULT_BASE_URL };
107
108
  }
108
- /** Read-only GET with a bounded timeout. Never throws. */
109
+ /** Classify a thrown probe failure into the closed category vocabulary. */
110
+ function classifyProbeFailure(error) {
111
+ // `AbortSignal.timeout` rejects with a DOMException named `TimeoutError`;
112
+ // fall back to the name check so a polyfilled or wrapped abort still reads as
113
+ // a timeout rather than as a generic failure.
114
+ const name = error?.name;
115
+ return name === "TimeoutError" || name === "AbortError"
116
+ ? "request timed out"
117
+ : "request unavailable";
118
+ }
109
119
  async function probeGet(deps, url, apiKey) {
110
120
  try {
111
121
  const resp = await deps.fetch(url, {
@@ -122,7 +132,7 @@ async function probeGet(deps, url, apiKey) {
122
132
  return { ok: true, status: resp.status, body };
123
133
  }
124
134
  catch (e) {
125
- return { ok: false, error: e instanceof Error ? e.message : String(e) };
135
+ return { ok: false, error: classifyProbeFailure(e) };
126
136
  }
127
137
  }
128
138
  /** Count set/total bootstrap fields from an install-manifest response body. */
@@ -300,6 +310,8 @@ export async function collectInstallStatusChecks(deps) {
300
310
  label: "Server connectivity",
301
311
  status: "WARN",
302
312
  detail: `unexpected HTTP ${ping.status} from /jira/ping`,
313
+ remediation: "the server answered but not with a status this check understands — re-run doctor; " +
314
+ "if it persists the Bridge API is unhealthy and no install fact below can be trusted.",
303
315
  });
304
316
  }
305
317
  // --- bootstrap fields ---
@@ -310,6 +322,7 @@ export async function collectInstallStatusChecks(deps) {
310
322
  label: "Bootstrap config fields",
311
323
  status: "WARN",
312
324
  detail: `manifest unreachable (${manifest.error})`,
325
+ remediation: `check ${target.baseUrl} and your network, then re-run doctor.`,
313
326
  });
314
327
  }
315
328
  else if (manifest.status === 404) {
@@ -329,6 +342,8 @@ export async function collectInstallStatusChecks(deps) {
329
342
  label: "Bootstrap config fields",
330
343
  status: "WARN",
331
344
  detail: "manifest response had an unexpected shape",
345
+ remediation: "the manifest could not be read, so no bootstrap field state is known — re-run doctor, " +
346
+ "then run /install-bridge to re-derive the project configuration.",
332
347
  });
333
348
  }
334
349
  else if (summary.unset.length === 0) {
@@ -355,6 +370,8 @@ export async function collectInstallStatusChecks(deps) {
355
370
  label: "Bootstrap config fields",
356
371
  status: "WARN",
357
372
  detail: `unexpected HTTP ${manifest.status} from the install manifest`,
373
+ remediation: "the manifest endpoint answered with an unrecognized status — re-run doctor; " +
374
+ "if it persists, re-run /install-bridge for this repository.",
358
375
  });
359
376
  }
360
377
  // --- integrations (from the same manifest response; presence booleans only) ---
@@ -509,3 +526,138 @@ export function formatInstallStatusFallbackReport() {
509
526
  },
510
527
  ]);
511
528
  }
529
+ // ---------------------------------------------------------------------------
530
+ // Canonical readiness projection (BAPI-1055)
531
+ // ---------------------------------------------------------------------------
532
+ /**
533
+ * The complete install-side prerequisite set, in render order.
534
+ *
535
+ * A STABLE descriptor set, not a projection of whatever the collector happened
536
+ * to return. `collectInstallStatusChecks` returns early on an unresolved
537
+ * identity and again on an unresolved credential, so the checks it omits simply
538
+ * never appear — and a locus that vanished reads as "nothing to report" rather
539
+ * than "this could not be established". The adapter walks this list instead, so
540
+ * every prerequisite is always represented.
541
+ */
542
+ export const INSTALL_READINESS_DESCRIPTORS = [
543
+ { id: "claude-login", label: "Claude login" },
544
+ { id: "identity", label: "Repository identity" },
545
+ { id: "credential", label: "Bridge API credential" },
546
+ { id: "connectivity", label: "Server connectivity" },
547
+ { id: "bootstrap", label: "Bootstrap config fields" },
548
+ { id: "integrations", label: "Integration credentials" },
549
+ { id: "github", label: "GitHub connection" },
550
+ { id: "indexing", label: "Repository indexing" },
551
+ ];
552
+ /** Fixed remediation for an install prerequisite whose identity never resolved. */
553
+ export const INSTALL_IDENTITY_REMEDIATION = "run /install-bridge (or set BAPI_REPO_NAME) to configure this project; without a repository " +
554
+ "identity no server-side conductor fact can be resolved.";
555
+ /** Fixed remediation for the advisory Claude-login gap. */
556
+ export const INSTALL_CLAUDE_LOGIN_REMEDIATION = "run `claude login` on the host that will run conductor workers; Bridge stores no Anthropic " +
557
+ "credential and cannot confirm this for you.";
558
+ /** Fixed detail for a prerequisite the checklist stopped short of collecting. */
559
+ const INSTALL_NOT_COLLECTED_DETAIL = "not collected — an earlier install prerequisite stopped the checklist";
560
+ /** Fixed detail for a prerequisite whose whole collector could not be run. */
561
+ const INSTALL_UNAVAILABLE_DETAIL = "unknown — the install-status checklist could not be collected at all";
562
+ /** Fixed remediation for a prerequisite whose whole collector could not be run. */
563
+ const INSTALL_UNAVAILABLE_REMEDIATION = "run `doctor` to collect the advisory install-status checklist directly; this prerequisite's " +
564
+ "state is unknown, not healthy.";
565
+ /**
566
+ * How each install check's non-canonical statuses project (BAPI-1055).
567
+ *
568
+ * `INFO` never becomes a canonical status, and `SKIP` is not automatically
569
+ * `skip`: an unresolved repository identity is reported as `SKIP` by the
570
+ * collector because the checklist stops there, but it is a genuine unmet
571
+ * prerequisite, so it maps to `fail`. The mapping is therefore stated per check
572
+ * id rather than derived from the source status alone.
573
+ */
574
+ function projectInstallStatus(check) {
575
+ if (check.status === "PASS")
576
+ return { status: "pass" };
577
+ if (check.status === "WARN") {
578
+ return { status: "warn", ...(check.remediation ? { remediation: check.remediation } : {}) };
579
+ }
580
+ if (check.status === "INFO") {
581
+ // `claude-login` INFO means "no local login marker found". That is an
582
+ // actionable conductor gap — a worker cannot run without it — so it warns.
583
+ if (check.id === "claude-login") {
584
+ return { status: "warn", remediation: INSTALL_CLAUDE_LOGIN_REMEDIATION };
585
+ }
586
+ // `indexing` INFO is genuinely informational in both of its branches (a
587
+ // parse running, or none running). Neither is a prerequisite gap, so the
588
+ // information rides on `detail` and the check passes.
589
+ return { status: "pass" };
590
+ }
591
+ // SKIP.
592
+ if (check.id === "identity") {
593
+ // Unknown identity is never a successful or guessed repository context.
594
+ return { status: "fail", remediation: check.remediation ?? INSTALL_IDENTITY_REMEDIATION };
595
+ }
596
+ return { status: "skip", ...(check.remediation ? { remediation: check.remediation } : {}) };
597
+ }
598
+ /**
599
+ * Project the install-status checklist into canonical readiness checks.
600
+ *
601
+ * PURE: no probe, no I/O, no re-derivation. Ids are namespaced `install.<id>`
602
+ * so provenance survives into a consolidated report without being encoded in
603
+ * display prose, and only the collector's own fixed labels, bounded details, and
604
+ * authored remediations are carried across.
605
+ */
606
+ export function mapInstallStatusChecksToReadinessChecks(checks,
607
+ /**
608
+ * True when the COLLECTOR itself could not run, as opposed to running and
609
+ * stopping early.
610
+ *
611
+ * `null` alone cannot tell those apart, and they are not the same fact. A
612
+ * checklist that ran and returned early leaves its later prerequisites
613
+ * genuinely dependent — a `skip`. A checklist that never ran leaves them
614
+ * simply unknown, and unknown must never read as healthy, so they fail with a
615
+ * restoring remediation instead.
616
+ */
617
+ collectorUnavailable = false) {
618
+ const byId = new Map();
619
+ for (const check of checks ?? []) {
620
+ // First writer wins: the collector emits each id once, and a duplicate from
621
+ // an injected fake must not silently replace the real finding.
622
+ if (!byId.has(check.id))
623
+ byId.set(check.id, check);
624
+ }
625
+ return INSTALL_READINESS_DESCRIPTORS.map(({ id, label }) => {
626
+ const source = byId.get(id);
627
+ if (!source) {
628
+ return collectorUnavailable
629
+ ? createReadinessCheck({
630
+ id: `install.${id}`,
631
+ source: "install",
632
+ label,
633
+ status: "fail",
634
+ detail: INSTALL_UNAVAILABLE_DETAIL,
635
+ remediation: INSTALL_UNAVAILABLE_REMEDIATION,
636
+ })
637
+ : // Absent because the checklist never got this far. A dependency case,
638
+ // so `skip` — the prerequisite that stopped the chain carries the fix.
639
+ createReadinessCheck({
640
+ id: `install.${id}`,
641
+ source: "install",
642
+ label,
643
+ status: "skip",
644
+ detail: INSTALL_NOT_COLLECTED_DETAIL,
645
+ });
646
+ }
647
+ const projected = projectInstallStatus(source);
648
+ return createReadinessCheckSafely({
649
+ id: `install.${id}`,
650
+ source: "install",
651
+ label: source.label || label,
652
+ status: projected.status,
653
+ ...(source.detail ? { detail: source.detail } : {}),
654
+ ...(projected.status === "pass"
655
+ ? {}
656
+ : projected.remediation
657
+ ? { remediation: projected.remediation }
658
+ : projected.status === "fail"
659
+ ? { remediation: INSTALL_IDENTITY_REMEDIATION }
660
+ : {}),
661
+ });
662
+ });
663
+ }
@@ -0,0 +1,130 @@
1
+ /**
2
+ * Structured local artifact persistence primitives (BAPI-1121).
3
+ *
4
+ * WHY THIS EXISTS. `wait_for_ticket_review`'s generation-aware artifact gate
5
+ * (`ticket-review-artifact-gate.ts`) needs a write/removal outcome it can
6
+ * branch on — did the accepted clarify leg actually land on disk, did the
7
+ * withheld critique file actually get removed — without parsing a
8
+ * human-readable note string. The pre-existing `saveLocally` in `index.ts`
9
+ * returns exactly that kind of note, and every existing artifact tool (the
10
+ * six single-artifact request/get tools, the BAPI-342 heavy-read saves, the
11
+ * combined ticket-review fan-out) depends on its exact rendered shape. This
12
+ * module extracts the underlying write mechanics into one place, so both the
13
+ * legacy byte-compatible `saveLocally` and the new structured primitives the
14
+ * gate uses share a single implementation rather than diverging.
15
+ *
16
+ * WHY A SEPARATE MODULE. `index.ts` is the file every other module is
17
+ * imported INTO; a helper defined there cannot be imported back into a module
18
+ * `index.ts` itself imports without an import cycle. This module has no
19
+ * dependency on `index.ts`, so `index.ts` imports `saveLocally` from here
20
+ * (BAPI-1121) instead of defining it locally, and the artifact gate imports
21
+ * the structured primitives directly.
22
+ */
23
+ import { writeFile, mkdir, rename, unlink } from "fs/promises";
24
+ import path from "path";
25
+ import { randomUUID } from "node:crypto";
26
+ /**
27
+ * Write `content` to `dir/filename` via per-file temporary staging followed by
28
+ * an atomic rename, so a concurrent reader can never observe a partially
29
+ * written file and a failed write never leaves a corrupt final artifact.
30
+ *
31
+ * The staging filename includes a random suffix so concurrent writes to the
32
+ * same final filename (unusual, but not prevented at this layer) cannot stage
33
+ * onto the same temporary path.
34
+ *
35
+ * Throws on failure — this is the shared low-level primitive both
36
+ * {@link saveLocally} (byte-compatible legacy notes) and
37
+ * {@link writeCanonicalArtifact} (sanitized structured outcome) wrap
38
+ * differently, because they render a failure to their callers in two
39
+ * deliberately different shapes.
40
+ */
41
+ async function writeFileAtomic(dir, filename, content) {
42
+ const finalPath = path.join(dir, filename);
43
+ const stagingPath = path.join(dir, `.${filename}.${randomUUID()}.tmp`);
44
+ await mkdir(dir, { recursive: true });
45
+ try {
46
+ await writeFile(stagingPath, content, "utf-8");
47
+ await rename(stagingPath, finalPath);
48
+ }
49
+ catch (err) {
50
+ // Best-effort cleanup of an orphaned staging file. Cleanup failure (or the
51
+ // staging file never having been created) does not change the outcome
52
+ // that matters: the original write/rename error propagates below.
53
+ try {
54
+ await unlink(stagingPath);
55
+ }
56
+ catch {
57
+ // Nothing to clean up, or cleanup itself failed — either way the
58
+ // original error is what the caller needs to see.
59
+ }
60
+ throw err;
61
+ }
62
+ return finalPath;
63
+ }
64
+ /**
65
+ * Byte-compatible replacement for the pre-BAPI-1121 `saveLocally` in
66
+ * `index.ts`. Preserves the exact call shape (`dir`, `filename`, `content` ->
67
+ * a rendered note string) and the exact rendered success/failure notes every
68
+ * existing artifact-tool caller depends on — including the raw error suffix
69
+ * on failure, which existing tests assert verbatim. The only behavioral
70
+ * change is that the write is now staged-then-renamed rather than written
71
+ * in place, which is externally invisible on success and, on failure, means
72
+ * no partial file is ever left at `dir/filename` (an improvement, not a
73
+ * compatibility break).
74
+ */
75
+ export async function saveLocally(dir, filename, content) {
76
+ try {
77
+ const finalPath = await writeFileAtomic(dir, filename, content);
78
+ return `\n\n---\nSaved to ${finalPath}`;
79
+ }
80
+ catch (writeErr) {
81
+ const filePath = path.join(dir, filename);
82
+ return `\n\n---\nNote: Failed to save file to ${filePath}: ${writeErr}`;
83
+ }
84
+ }
85
+ /**
86
+ * Structured canonical write for the generation-aware artifact gate. Returns
87
+ * `{ ok, path, message }` rather than throwing or requiring the caller to
88
+ * parse a note string. The public `message` is sanitized to the path and
89
+ * outcome only; any detailed local diagnostic (the underlying filesystem
90
+ * error) goes only to `console.error`, never through MCP.
91
+ */
92
+ export async function writeCanonicalArtifact(dir, filename, content) {
93
+ const finalPath = path.join(dir, filename);
94
+ try {
95
+ await writeFileAtomic(dir, filename, content);
96
+ return { ok: true, path: finalPath, message: `Saved to ${finalPath}` };
97
+ }
98
+ catch (err) {
99
+ console.error(`[local-artifact-storage] write failed for ${finalPath}:`, err);
100
+ return { ok: false, path: finalPath, message: `Failed to save file to ${finalPath}` };
101
+ }
102
+ }
103
+ /**
104
+ * Remove the exact canonical path for a withheld or stale artifact.
105
+ *
106
+ * Idempotent by design: an already-absent target (`ENOENT`) is reported as a
107
+ * SUCCESSFUL cleanup outcome, not an error — a retried removal, or a removal
108
+ * that runs after an earlier successful one, must never report failure. Only
109
+ * an actual removal failure (permissions, a directory at that path, etc.) is
110
+ * reported as `ok: false`.
111
+ *
112
+ * Removes exactly the given path — no globbing, no directory recursion, no
113
+ * inference of sibling files to clean up. The caller is responsible for
114
+ * passing the exact canonical target; this primitive never widens the blast
115
+ * radius of a removal on its own.
116
+ */
117
+ export async function removeCanonicalArtifact(targetPath) {
118
+ try {
119
+ await unlink(targetPath);
120
+ return { ok: true, path: targetPath, message: `Removed ${targetPath}` };
121
+ }
122
+ catch (err) {
123
+ const code = err?.code;
124
+ if (code === "ENOENT") {
125
+ return { ok: true, path: targetPath, message: `${targetPath} was already absent` };
126
+ }
127
+ console.error(`[local-artifact-storage] removal failed for ${targetPath}:`, err);
128
+ return { ok: false, path: targetPath, message: `Failed to remove ${targetPath}` };
129
+ }
130
+ }