@bridge_gpt/mcp-server 0.1.16 → 0.2.0

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 (50) hide show
  1. package/README.md +333 -162
  2. package/build/agent-capabilities/cli.js +152 -0
  3. package/build/agent-capabilities/default-deps.js +45 -0
  4. package/build/agent-capabilities/probe-context.js +111 -0
  5. package/build/agent-capabilities/probes.js +278 -0
  6. package/build/agent-capabilities/reporter.js +50 -0
  7. package/build/agent-capabilities/runner.js +56 -0
  8. package/build/agent-capabilities/types.js +10 -0
  9. package/build/agent-launchers/claude.js +85 -0
  10. package/build/agent-launchers/index.js +17 -0
  11. package/build/agent-launchers/types.js +1 -0
  12. package/build/agents.generated.js +1 -1
  13. package/build/brainstorm-files.js +89 -0
  14. package/build/bridge-config.js +404 -0
  15. package/build/chain-orchestrator.js +1364 -0
  16. package/build/chain-utils.js +68 -0
  17. package/build/commands.generated.js +5 -3
  18. package/build/credential-materialization.js +128 -0
  19. package/build/credential-store.js +232 -0
  20. package/build/decision-page-schema.js +39 -6
  21. package/build/decision-page-template.js +54 -18
  22. package/build/doctor.js +18 -2
  23. package/build/fetch-stub.js +139 -0
  24. package/build/git-ignore-utils.js +63 -0
  25. package/build/index.js +1623 -546
  26. package/build/mcp-invoke.js +417 -0
  27. package/build/mcp-provisioning.js +249 -0
  28. package/build/mcp-registration-doctor.js +96 -0
  29. package/build/pipeline-orchestrator.js +66 -1
  30. package/build/pipeline-utils.js +33 -0
  31. package/build/pipelines.generated.js +165 -5
  32. package/build/schedule-run.js +951 -0
  33. package/build/schedule-store.js +132 -0
  34. package/build/scheduler-backends/at-fallback.js +144 -0
  35. package/build/scheduler-backends/escaping.js +113 -0
  36. package/build/scheduler-backends/index.js +72 -0
  37. package/build/scheduler-backends/launchd.js +216 -0
  38. package/build/scheduler-backends/systemd-user.js +237 -0
  39. package/build/scheduler-backends/task-scheduler.js +219 -0
  40. package/build/scheduler-backends/types.js +23 -0
  41. package/build/start-tickets-prereqs.js +90 -1
  42. package/build/start-tickets.js +222 -70
  43. package/build/third-party-mcp-targets.js +75 -0
  44. package/build/version.generated.js +1 -1
  45. package/package.json +8 -8
  46. package/pipelines/full-automation.json +49 -0
  47. package/pipelines/idea-to-ticket.json +71 -0
  48. package/pipelines/implement-ticket.json +28 -2
  49. package/smoke-test/SMOKE-TEST.md +511 -0
  50. package/smoke-test/smoke-test-mcp.md +23 -0
@@ -0,0 +1,96 @@
1
+ /**
2
+ * Read-only worktree MCP registration inspection (doctor-only).
3
+ *
4
+ * Confirms that a worktree's generated `.mcp.json` / `.cursor/mcp.json` contains
5
+ * a valid Bridge API `mcp-invoke` shim entry pointing at THAT worktree — without
6
+ * launching `mcp-invoke`, `npx`, Node, or the MCP server. Pure read/parse only;
7
+ * the sole injected dependency is `readFile`.
8
+ */
9
+ import path from "path";
10
+ /**
11
+ * Read and parse a JSON file. A read failure (e.g. ENOENT) is `missing`; invalid
12
+ * JSON is `malformed` (carrying only the path, never the raw content); otherwise
13
+ * `present` with the parsed value.
14
+ */
15
+ export async function readJsonIfPresent(filePath, deps) {
16
+ let raw;
17
+ try {
18
+ raw = await deps.readFile(filePath);
19
+ }
20
+ catch {
21
+ return { state: "missing" };
22
+ }
23
+ try {
24
+ return { state: "present", value: JSON.parse(raw) };
25
+ }
26
+ catch {
27
+ return { state: "malformed", path: filePath };
28
+ }
29
+ }
30
+ /** Return the token immediately following `flag` in `args`, or undefined. */
31
+ function flagValue(args, flag) {
32
+ const index = args.indexOf(flag);
33
+ if (index >= 0 && index + 1 < args.length)
34
+ return args[index + 1];
35
+ return undefined;
36
+ }
37
+ /**
38
+ * True when `entry` is a secret-free Bridge API shim registration that launches
39
+ * `npx -y @bridge_gpt/mcp-server@<version> mcp-invoke --target bapi
40
+ * --project-root <worktreeRoot>`. The `--target` must be `bapi` and the
41
+ * `--project-root` must match the given worktree exactly.
42
+ */
43
+ export function isBridgeApiShimEntry(entry, worktreeRoot) {
44
+ if (!entry || typeof entry !== "object" || Array.isArray(entry))
45
+ return false;
46
+ const candidate = entry;
47
+ if (candidate.command !== "npx")
48
+ return false;
49
+ if (!Array.isArray(candidate.args))
50
+ return false;
51
+ const args = candidate.args.filter((a) => typeof a === "string");
52
+ if (!args.some((a) => a.startsWith("@bridge_gpt/mcp-server")))
53
+ return false;
54
+ if (!args.includes("mcp-invoke"))
55
+ return false;
56
+ if (flagValue(args, "--target") !== "bapi")
57
+ return false;
58
+ if (flagValue(args, "--project-root") !== worktreeRoot)
59
+ return false;
60
+ return true;
61
+ }
62
+ /**
63
+ * Inspect both `<worktreeRoot>/.mcp.json` and `<worktreeRoot>/.cursor/mcp.json`.
64
+ * Reports `found` when at least one registration file contains a valid
65
+ * `bridge-api` shim entry pointing at this worktree; otherwise `found: false`
66
+ * with an actionable, secret-free hint. Never spawns anything.
67
+ */
68
+ export async function probeWorktreeMcpRegistration(worktreeRoot, deps) {
69
+ const targets = [
70
+ path.join(worktreeRoot, ".mcp.json"),
71
+ path.join(worktreeRoot, ".cursor", "mcp.json"),
72
+ ];
73
+ for (const filePath of targets) {
74
+ const read = await readJsonIfPresent(filePath, deps);
75
+ if (read.state !== "present")
76
+ continue;
77
+ const doc = read.value;
78
+ if (!doc || typeof doc !== "object" || Array.isArray(doc))
79
+ continue;
80
+ const servers = doc.mcpServers;
81
+ if (!servers || typeof servers !== "object" || Array.isArray(servers))
82
+ continue;
83
+ const entry = servers["bridge-api"];
84
+ if (isBridgeApiShimEntry(entry, worktreeRoot)) {
85
+ return {
86
+ found: true,
87
+ detail: `bridge-api shim registered in ${path.basename(path.dirname(filePath)) === ".cursor" ? ".cursor/mcp.json" : ".mcp.json"}`,
88
+ };
89
+ }
90
+ }
91
+ return {
92
+ found: false,
93
+ detail: "No worktree .mcp.json or .cursor/mcp.json points at the bridge-api mcp-invoke shim. " +
94
+ "Re-run start-tickets to provision the worktree MCP registration.",
95
+ };
96
+ }
@@ -16,7 +16,21 @@
16
16
  * are inlined into the instruction string so the agent can review and
17
17
  * confirm before resuming. (BAPI-275 decisions E-4 / E-36.)
18
18
  */
19
+ import { createHash } from "node:crypto";
19
20
  import { resolveRecipe } from "./pipeline-utils.js";
21
+ /**
22
+ * Derive a STABLE, content-addressed hash of an idea — the basis for the
23
+ * cross-run idempotency label ``bapi-idea-hash-{idea_hash}``. Normalizes
24
+ * (trim + lowercase + collapse internal whitespace) so cosmetic edits don't
25
+ * change the hash, then takes the first 12 hex chars of the SHA-256. Unlike the
26
+ * per-run ``run_id`` label, this is identical every time the same idea is run,
27
+ * so a re-run of the same idea is reliably caught by label (not just fuzzy
28
+ * keyword search). Auto-injected as the ``idea_hash`` pipeline variable.
29
+ */
30
+ export function deriveIdeaHash(idea) {
31
+ const normalized = String(idea ?? "").trim().toLowerCase().replace(/\s+/g, " ");
32
+ return createHash("sha256").update(normalized).digest("hex").slice(0, 12);
33
+ }
20
34
  class PipelinePersistenceError extends Error {
21
35
  code;
22
36
  constructor(code, message) {
@@ -175,6 +189,10 @@ const ORCHESTRATION_TOOLS = new Set([
175
189
  "resume_pipeline",
176
190
  "list_pipeline_runs",
177
191
  "delete_pipeline_run",
192
+ // BAPI-326: chain orchestration tools must never be invoked from within a
193
+ // declarative pipeline step (avoids accidental orchestration recursion).
194
+ "run_full_automation",
195
+ "resume_full_automation",
178
196
  ]);
179
197
  // ---------------------------------------------------------------------------
180
198
  // runPipeline
@@ -196,6 +214,12 @@ export async function runPipeline(deps, input) {
196
214
  auto_approve: autoApprove ? "true" : "",
197
215
  ...(input.variables ?? {}),
198
216
  };
217
+ // Auto-inject the stable idea-hash (like docs_dir) whenever the pipeline was
218
+ // given an `idea`, so both the chain and standalone callers get the cross-run
219
+ // idempotency label without computing it themselves.
220
+ if ("idea" in mergedVariables) {
221
+ mergedVariables.idea_hash = deriveIdeaHash(mergedVariables.idea);
222
+ }
199
223
  let recipe;
200
224
  try {
201
225
  recipe = resolveRecipe(pipelineDef, deps.instructions, mergedVariables, undefined, autoApprove);
@@ -227,6 +251,39 @@ export async function runPipeline(deps, input) {
227
251
  }
228
252
  return continuePipelineExecution(deps, persistence, row, recipe, autoApprove);
229
253
  }
254
+ /**
255
+ * Read-only snapshot of a pipeline run's current persisted state — performs no
256
+ * execution and no state transition. Used by the chain orchestrator to decide,
257
+ * at a stage boundary, whether the inner pipeline still needs resuming
258
+ * (``paused``), has already reached a terminal state (``completed`` / ``failed``)
259
+ * so the stage can be advanced/failed idempotently, or is in an indeterminate
260
+ * state that cannot be safely resumed or recovered.
261
+ */
262
+ export async function peekPipelineRun(deps, pipeline_run_id) {
263
+ const persistence = createPipelinePersistenceClient({
264
+ baseUrl: deps.baseUrl,
265
+ apiKey: deps.apiKey,
266
+ repoName: deps.repoName,
267
+ });
268
+ let row;
269
+ try {
270
+ row = await persistence.getRun(pipeline_run_id);
271
+ }
272
+ catch (err) {
273
+ if (err instanceof PipelinePersistenceError) {
274
+ return failedEnvelope(err.code, err.message, { pipeline_run_id });
275
+ }
276
+ return failedEnvelope("TOOL_ERROR", err instanceof Error ? err.message : String(err), { pipeline_run_id });
277
+ }
278
+ return {
279
+ status: row.status,
280
+ pipeline_run_id: row.pipeline_run_id,
281
+ pipeline: row.pipeline_name,
282
+ total_steps: row.resolved_recipe.total_steps,
283
+ current_step_index: row.current_step_index,
284
+ results: row.results,
285
+ };
286
+ }
230
287
  // ---------------------------------------------------------------------------
231
288
  // resumePipeline
232
289
  // ---------------------------------------------------------------------------
@@ -291,7 +348,13 @@ export async function resumePipeline(deps, input) {
291
348
  updated = await persistence.patchRun(row.pipeline_run_id, {
292
349
  current_step_index: nextIndex,
293
350
  results: accumulatedResults,
294
- status: "running",
351
+ // Derive the persisted status from the resumed position. When the resumed
352
+ // step is the final ``agent_task``, ``nextIndex`` already equals
353
+ // ``recipe.total_steps``, so ``continuePipelineExecution``'s loop body never
354
+ // runs and it falls through to the completed envelope without issuing
355
+ // another patch. We must persist ``completed`` on THIS transition or the run
356
+ // is orphaned in ``running`` forever. Non-final resumes stay ``running``.
357
+ status: nextIndex >= recipe.total_steps ? "completed" : "running",
295
358
  expected_status: "paused",
296
359
  expected_current_step_index: stepIndex,
297
360
  });
@@ -583,6 +646,8 @@ async function executeMcpCallStep(deps, step) {
583
646
  error: err instanceof Error ? err.message : String(err),
584
647
  };
585
648
  }
649
+ // Non-text tool output (e.g. image content) intentionally degrades to "" for
650
+ // pipeline dispatch in v1 — pipeline steps only act on text envelopes.
586
651
  const text = toolResult?.content?.[0]?.type === "text"
587
652
  ? toolResult.content[0].text
588
653
  : "";
@@ -6,6 +6,35 @@
6
6
  import { readdir, readFile } from "fs/promises";
7
7
  import path from "path";
8
8
  // ---------------------------------------------------------------------------
9
+ // Tiered section-graph contract metadata (BAPI-345, Ticket 1)
10
+ // ---------------------------------------------------------------------------
11
+ //
12
+ // Static, server-owned metadata advertised on every resolved recipe envelope so
13
+ // MCP consumers can detect that the section-graph contract exists. Ticket 1 is
14
+ // contract-only: every execution-related capability is disabled, and the
15
+ // section graph itself is NEVER embedded in the recipe envelope (it lives on the
16
+ // plan-generation response, not here). This descriptor is NOT derived from the
17
+ // repo's `tiered_execution` config value.
18
+ export const PIPELINE_CONTRACT_VERSION = "2";
19
+ export const TIERED_SECTION_CAPABILITY_DESCRIPTOR = {
20
+ section_graph: {
21
+ supported: true,
22
+ execution_enabled: false,
23
+ payload_location: "plan_generation_response.sections",
24
+ },
25
+ tiers: ["cheap", "basic", "premium"],
26
+ risk_levels: ["low", "medium", "high"],
27
+ activities: ["implement", "test", "docs", "debug", "config"],
28
+ tiered_execution_modes: ["off", "claude_code_only", "all_capable"],
29
+ capabilities: {
30
+ // All Ticket 2 execution features are disabled / not implemented in Ticket 1.
31
+ escalation: false,
32
+ checkpoint: false,
33
+ rollback: false,
34
+ subagent_dispatch: false,
35
+ },
36
+ };
37
+ // ---------------------------------------------------------------------------
9
38
  // Schema Validation
10
39
  // ---------------------------------------------------------------------------
11
40
  export function validatePipelineSchema(json) {
@@ -212,6 +241,10 @@ export function resolveRecipe(pipeline, instructions, variables, skipSteps, auto
212
241
  agent_instructions: baseInstructions + autoApproveSuffix,
213
242
  auto_approve: !!autoApprove,
214
243
  steps: resolvedSteps,
244
+ // BAPI-345 (Ticket 1): additive static contract metadata. The section graph
245
+ // is NEVER embedded here — it is delivered on the plan-generation response.
246
+ contract_version: PIPELINE_CONTRACT_VERSION,
247
+ capability_descriptor: TIERED_SECTION_CAPABILITY_DESCRIPTOR,
215
248
  };
216
249
  }
217
250
  // ---------------------------------------------------------------------------