@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,404 @@
1
+ /**
2
+ * Reader and validator for the committed, secret-free `.bridge/config` manifest.
3
+ *
4
+ * The manifest is a tiny, hand-written TOML subset — `repo_name` plus repeated
5
+ * `[[mcp]]` target blocks — committed at the repo root and inherited by every
6
+ * git worktree. It NEVER contains credentials; this module only resolves repo
7
+ * identity and which MCP targets a worktree should be provisioned for.
8
+ *
9
+ * Everything is dependency-free (no TOML runtime dependency) and dependency
10
+ * injected (`readFile`, optional `runCommand`) so it is unit-testable without
11
+ * touching the filesystem or invoking real `git`.
12
+ */
13
+ import path from "path";
14
+ /**
15
+ * True if a string contains any ASCII control character (0x00-0x1f or 0x7f).
16
+ * Keeps identifiers path-safe and free of anything that could smuggle into a
17
+ * shell argument or filename. Implemented by char-code scan to avoid a
18
+ * control-character regex literal in source.
19
+ */
20
+ function hasControlChars(value) {
21
+ for (let i = 0; i < value.length; i++) {
22
+ const code = value.charCodeAt(i);
23
+ if (code <= 0x1f || code === 0x7f)
24
+ return true;
25
+ }
26
+ return false;
27
+ }
28
+ // ---------------------------------------------------------------------------
29
+ // Identifier validation
30
+ // ---------------------------------------------------------------------------
31
+ /**
32
+ * Validate a repo-name identifier. Requires a trimmed, non-empty string with no
33
+ * path separators or ASCII control characters. Returns a structured result with
34
+ * the trimmed value rather than throwing.
35
+ */
36
+ export function validateRepoName(raw) {
37
+ if (typeof raw !== "string") {
38
+ return { ok: false, error: "repo_name must be a string" };
39
+ }
40
+ const value = raw.trim();
41
+ if (value.length === 0) {
42
+ return { ok: false, error: "repo_name must be a non-empty string" };
43
+ }
44
+ if (value.includes("/") || value.includes("\\")) {
45
+ return { ok: false, error: "repo_name must not contain path separators" };
46
+ }
47
+ if (hasControlChars(value)) {
48
+ return { ok: false, error: "repo_name must not contain control characters" };
49
+ }
50
+ return { ok: true, value };
51
+ }
52
+ /**
53
+ * Validate an MCP target identifier. Same path-safety rules as repo names.
54
+ * Non-`bapi` targets validate successfully (parsing is not hard-coded to
55
+ * `bapi`); only `bapi` is acted on elsewhere for this MVP.
56
+ */
57
+ export function validateMcpTarget(raw) {
58
+ if (typeof raw !== "string") {
59
+ return { ok: false, error: "mcp target must be a string" };
60
+ }
61
+ const value = raw.trim();
62
+ if (value.length === 0) {
63
+ return { ok: false, error: "mcp target must be a non-empty string" };
64
+ }
65
+ if (value.includes("/") || value.includes("\\")) {
66
+ return { ok: false, error: "mcp target must not contain path separators" };
67
+ }
68
+ if (hasControlChars(value)) {
69
+ return { ok: false, error: "mcp target must not contain control characters" };
70
+ }
71
+ return { ok: true, value };
72
+ }
73
+ // ---------------------------------------------------------------------------
74
+ // TOML subset parser
75
+ // ---------------------------------------------------------------------------
76
+ /** Extract the inner content of a `"..."` double-quoted string, or null. */
77
+ function parseQuotedString(value) {
78
+ if (value.length < 2)
79
+ return null;
80
+ if (value[0] !== '"' || value[value.length - 1] !== '"')
81
+ return null;
82
+ const inner = value.slice(1, -1);
83
+ // The supported subset does not include escapes or embedded quotes.
84
+ if (inner.includes('"'))
85
+ return null;
86
+ return inner;
87
+ }
88
+ /**
89
+ * Parse a minimal, dependency-free TOML string array: `[ "a", "b" ]`. Elements
90
+ * are double-quoted, comma-free strings; whitespace around brackets, commas, and
91
+ * elements is ignored. An empty `[]` yields `[]`. Returns null on any malformed
92
+ * input (so callers can emit a secret-safe error that never echoes the value).
93
+ */
94
+ function parseStringArray(value) {
95
+ const trimmed = value.trim();
96
+ if (trimmed.length < 2 || trimmed[0] !== "[" || trimmed[trimmed.length - 1] !== "]") {
97
+ return null;
98
+ }
99
+ const inner = trimmed.slice(1, -1).trim();
100
+ if (inner.length === 0)
101
+ return [];
102
+ const parts = inner.split(",");
103
+ const out = [];
104
+ for (const part of parts) {
105
+ const element = parseQuotedString(part.trim());
106
+ if (element === null)
107
+ return null;
108
+ out.push(element);
109
+ }
110
+ return out;
111
+ }
112
+ /**
113
+ * Parse the supported `.bridge/config` TOML subset:
114
+ * - a single top-level `repo_name = "..."`
115
+ * - zero or more `[[mcp]]` sections, each with `target = "..."`
116
+ * - `#` comments, blank lines, and surrounding whitespace
117
+ *
118
+ * Anything outside this subset (other keys, single-bracket tables, unquoted or
119
+ * unterminated strings, duplicate `repo_name`, `target` outside `[[mcp]]`) is a
120
+ * structured error. Error messages reference line numbers and key names only —
121
+ * never the raw manifest content or a value — so secrets can never leak through
122
+ * a misplaced line.
123
+ */
124
+ export function parseBridgeConfigToml(text) {
125
+ const lines = text.split("\n");
126
+ let repoName;
127
+ let sawRepoName = false;
128
+ const mcp = [];
129
+ // null = top-level scope; otherwise points at the in-progress [[mcp]] block.
130
+ let currentMcp = null;
131
+ for (let i = 0; i < lines.length; i++) {
132
+ const lineNo = i + 1;
133
+ const line = lines[i].trim();
134
+ if (line.length === 0 || line.startsWith("#"))
135
+ continue;
136
+ if (line === "[[mcp]]") {
137
+ currentMcp = { headerLine: lineNo };
138
+ mcp.push(currentMcp);
139
+ continue;
140
+ }
141
+ // Any other bracketed table header is unsupported.
142
+ if (line.startsWith("[")) {
143
+ return {
144
+ ok: false,
145
+ kind: "parse-error",
146
+ error: `Unsupported table header on line ${lineNo}; only [[mcp]] is allowed`,
147
+ };
148
+ }
149
+ const eq = line.indexOf("=");
150
+ if (eq === -1) {
151
+ return {
152
+ ok: false,
153
+ kind: "parse-error",
154
+ error: `Malformed line ${lineNo}; expected key = "value"`,
155
+ };
156
+ }
157
+ const key = line.slice(0, eq).trim();
158
+ const rawValue = line.slice(eq + 1).trim();
159
+ if (key.length === 0) {
160
+ return {
161
+ ok: false,
162
+ kind: "parse-error",
163
+ error: `Malformed line ${lineNo}; missing key`,
164
+ };
165
+ }
166
+ if (currentMcp === null) {
167
+ // Top-level scope: only repo_name is allowed.
168
+ if (key === "repo_name") {
169
+ if (sawRepoName) {
170
+ return {
171
+ ok: false,
172
+ kind: "parse-error",
173
+ error: `Duplicate repo_name on line ${lineNo}`,
174
+ };
175
+ }
176
+ sawRepoName = true;
177
+ const stringValue = parseQuotedString(rawValue);
178
+ if (stringValue === null) {
179
+ return {
180
+ ok: false,
181
+ kind: "parse-error",
182
+ error: `Expected a double-quoted string for '${key}' on line ${lineNo}`,
183
+ };
184
+ }
185
+ const validated = validateRepoName(stringValue);
186
+ if (!validated.ok) {
187
+ return { ok: false, kind: "validation-error", error: validated.error };
188
+ }
189
+ repoName = validated.value;
190
+ continue;
191
+ }
192
+ if (key === "target") {
193
+ return {
194
+ ok: false,
195
+ kind: "parse-error",
196
+ error: `target on line ${lineNo} must appear inside an [[mcp]] section`,
197
+ };
198
+ }
199
+ return {
200
+ ok: false,
201
+ kind: "parse-error",
202
+ error: `Unsupported key '${key}' on line ${lineNo}`,
203
+ };
204
+ }
205
+ // Inside an [[mcp]] block: target / command / args / secret_bundle.
206
+ if (key === "args") {
207
+ if (currentMcp.args !== undefined) {
208
+ return { ok: false, kind: "parse-error", error: `Duplicate args on line ${lineNo}` };
209
+ }
210
+ const arr = parseStringArray(rawValue);
211
+ if (arr === null) {
212
+ return {
213
+ ok: false,
214
+ kind: "parse-error",
215
+ error: `Expected a string array for 'args' on line ${lineNo}`,
216
+ };
217
+ }
218
+ currentMcp.args = arr;
219
+ continue;
220
+ }
221
+ if (key === "target" || key === "command" || key === "secret_bundle") {
222
+ const stringValue = parseQuotedString(rawValue);
223
+ if (stringValue === null) {
224
+ return {
225
+ ok: false,
226
+ kind: "parse-error",
227
+ error: `Expected a double-quoted string for '${key}' on line ${lineNo}`,
228
+ };
229
+ }
230
+ if (key === "target") {
231
+ if (currentMcp.target !== undefined) {
232
+ return { ok: false, kind: "parse-error", error: `Duplicate target on line ${lineNo}` };
233
+ }
234
+ const validated = validateMcpTarget(stringValue);
235
+ if (!validated.ok) {
236
+ return { ok: false, kind: "validation-error", error: validated.error };
237
+ }
238
+ currentMcp.target = validated.value;
239
+ continue;
240
+ }
241
+ if (key === "command") {
242
+ if (currentMcp.command !== undefined) {
243
+ return { ok: false, kind: "parse-error", error: `Duplicate command on line ${lineNo}` };
244
+ }
245
+ currentMcp.command = stringValue;
246
+ continue;
247
+ }
248
+ // secret_bundle
249
+ if (currentMcp.secretBundle !== undefined) {
250
+ return { ok: false, kind: "parse-error", error: `Duplicate secret_bundle on line ${lineNo}` };
251
+ }
252
+ currentMcp.secretBundle = stringValue;
253
+ continue;
254
+ }
255
+ return {
256
+ ok: false,
257
+ kind: "parse-error",
258
+ error: `Unsupported key '${key}' inside [[mcp]] on line ${lineNo}`,
259
+ };
260
+ }
261
+ if (!sawRepoName || repoName === undefined) {
262
+ return { ok: false, kind: "validation-error", error: "Missing required repo_name" };
263
+ }
264
+ const cleaned = [];
265
+ for (const entry of mcp) {
266
+ if (entry.target === undefined) {
267
+ return {
268
+ ok: false,
269
+ kind: "validation-error",
270
+ error: `An [[mcp]] section on line ${entry.headerLine} is missing its target`,
271
+ };
272
+ }
273
+ if (entry.target !== "bapi") {
274
+ // Tier-2 targets must declare a launch command, an args array, and the
275
+ // store bundle that provides their secrets.
276
+ if (entry.command === undefined || entry.command.trim().length === 0) {
277
+ return {
278
+ ok: false,
279
+ kind: "validation-error",
280
+ error: `[[mcp]] target '${entry.target}' on line ${entry.headerLine} requires a non-empty command`,
281
+ };
282
+ }
283
+ if (entry.args === undefined) {
284
+ return {
285
+ ok: false,
286
+ kind: "validation-error",
287
+ error: `[[mcp]] target '${entry.target}' on line ${entry.headerLine} requires an args array`,
288
+ };
289
+ }
290
+ if (entry.secretBundle === undefined || entry.secretBundle.trim().length === 0) {
291
+ return {
292
+ ok: false,
293
+ kind: "validation-error",
294
+ error: `[[mcp]] target '${entry.target}' on line ${entry.headerLine} requires a non-empty secret_bundle`,
295
+ };
296
+ }
297
+ }
298
+ const clean = { target: entry.target };
299
+ if (entry.command !== undefined)
300
+ clean.command = entry.command;
301
+ if (entry.args !== undefined)
302
+ clean.args = entry.args;
303
+ if (entry.secretBundle !== undefined)
304
+ clean.secretBundle = entry.secretBundle;
305
+ cleaned.push(clean);
306
+ }
307
+ return { ok: true, manifest: { repoName, mcp: cleaned } };
308
+ }
309
+ // ---------------------------------------------------------------------------
310
+ // Reading + identity resolution
311
+ // ---------------------------------------------------------------------------
312
+ /** Absolute path to a project root's committed manifest. */
313
+ export function bridgeConfigPath(projectRoot) {
314
+ return path.join(projectRoot, ".bridge", "config");
315
+ }
316
+ /**
317
+ * Read and parse `<projectRoot>/.bridge/config`. A missing file is the common,
318
+ * non-fatal case (`kind: "missing"`); malformed/invalid content surfaces as a
319
+ * structured error. Raw manifest content is never echoed back.
320
+ */
321
+ export async function readBridgeConfig(projectRoot, deps) {
322
+ const filePath = bridgeConfigPath(projectRoot);
323
+ let raw;
324
+ try {
325
+ raw = await deps.readFile(filePath);
326
+ }
327
+ catch (err) {
328
+ if (err && typeof err === "object" && err.code === "ENOENT") {
329
+ return { ok: false, kind: "missing" };
330
+ }
331
+ return { ok: false, kind: "parse-error", error: "Unable to read .bridge/config" };
332
+ }
333
+ return parseBridgeConfigToml(raw);
334
+ }
335
+ /** True only when the manifest contains an exact target match (e.g. `"bapi"`). */
336
+ export function hasMcpTarget(manifest, target) {
337
+ return manifest.mcp.some((entry) => entry.target === target);
338
+ }
339
+ /**
340
+ * Derive the repo name from `git rev-parse --git-common-dir`, run with the
341
+ * passed `projectRoot` as `cwd` (so a worktree resolves to its parent repo's
342
+ * identity). The common dir is the directory immediately before the `.git`
343
+ * path segment, e.g. `<repo>/bapi/.git` and `<repo>/bapi/.git/worktrees/X` both
344
+ * derive `bapi`.
345
+ */
346
+ export async function deriveRepoNameFromGitCommonDir(projectRoot, deps) {
347
+ if (!deps.runCommand) {
348
+ return { ok: false, error: "Cannot derive repo name: no command runner available" };
349
+ }
350
+ let result;
351
+ try {
352
+ result = await deps.runCommand("git", ["rev-parse", "--git-common-dir"], {
353
+ cwd: projectRoot,
354
+ });
355
+ }
356
+ catch (err) {
357
+ const message = err instanceof Error ? err.message : String(err);
358
+ return { ok: false, error: `git rev-parse --git-common-dir failed: ${message}` };
359
+ }
360
+ if (result.exitCode !== 0) {
361
+ const reason = (result.stderr || result.stdout || "").trim();
362
+ return {
363
+ ok: false,
364
+ error: `git rev-parse --git-common-dir failed${reason ? `: ${reason}` : ""}`,
365
+ };
366
+ }
367
+ const commonDir = result.stdout.trim();
368
+ if (commonDir.length === 0) {
369
+ return { ok: false, error: "git rev-parse --git-common-dir returned no output" };
370
+ }
371
+ const resolved = path.isAbsolute(commonDir)
372
+ ? commonDir
373
+ : path.resolve(projectRoot, commonDir);
374
+ const segments = resolved.split(/[\\/]+/).filter((s) => s.length > 0);
375
+ const gitIndex = segments.lastIndexOf(".git");
376
+ if (gitIndex < 1) {
377
+ return {
378
+ ok: false,
379
+ error: "Unable to derive repo name from git common dir",
380
+ };
381
+ }
382
+ const derived = segments[gitIndex - 1];
383
+ const validated = validateRepoName(derived);
384
+ if (!validated.ok) {
385
+ return { ok: false, error: `Derived repo name is invalid: ${validated.error}` };
386
+ }
387
+ return { ok: true, value: validated.value };
388
+ }
389
+ /**
390
+ * Resolve the repo name for a project root. A valid manifest `repo_name` wins;
391
+ * a *missing* manifest falls back to the git-common-dir derivation; a malformed
392
+ * or invalid manifest returns a structured error rather than silently falling
393
+ * back (so a broken committed file is never papered over by git guessing).
394
+ */
395
+ export async function resolveRepoNameForProjectRoot(projectRoot, deps) {
396
+ const read = await readBridgeConfig(projectRoot, deps);
397
+ if (read.ok) {
398
+ return { ok: true, value: read.manifest.repoName };
399
+ }
400
+ if (read.kind === "missing") {
401
+ return deriveRepoNameFromGitCommonDir(projectRoot, deps);
402
+ }
403
+ return { ok: false, error: read.error };
404
+ }