@bridge_gpt/mcp-server 0.1.17 → 0.2.1

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 (45) hide show
  1. package/README.md +334 -196
  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 +25 -17
  10. package/build/agent-launchers/cursor.js +65 -0
  11. package/build/agent-launchers/index.js +23 -8
  12. package/build/agent-registry.js +68 -0
  13. package/build/agents.generated.js +1 -1
  14. package/build/brainstorm-files.js +89 -0
  15. package/build/bridge-config.js +404 -0
  16. package/build/chain-orchestrator.js +247 -33
  17. package/build/command-catalog.js +376 -0
  18. package/build/commands.generated.js +10 -7
  19. package/build/credential-materialization.js +128 -0
  20. package/build/credential-store.js +232 -0
  21. package/build/decision-page-schema.js +39 -6
  22. package/build/decision-page-template.js +54 -18
  23. package/build/doctor.js +18 -2
  24. package/build/git-ignore-utils.js +63 -0
  25. package/build/index.js +1707 -557
  26. package/build/mcp-invoke.js +417 -0
  27. package/build/mcp-provisioning.js +342 -0
  28. package/build/mcp-registration-doctor.js +96 -0
  29. package/build/pipeline-orchestrator.js +9 -1
  30. package/build/pipelines.generated.js +5 -3
  31. package/build/schedule-run.js +440 -92
  32. package/build/schedule-store.js +41 -1
  33. package/build/scheduled-prompt.js +109 -0
  34. package/build/scheduler-backends/at-fallback.js +5 -10
  35. package/build/scheduler-backends/escaping.js +40 -10
  36. package/build/scheduler-backends/launchd.js +23 -14
  37. package/build/scheduler-backends/systemd-user.js +32 -19
  38. package/build/scheduler-backends/task-scheduler.js +8 -13
  39. package/build/start-tickets-prereqs.js +90 -1
  40. package/build/start-tickets.js +563 -42
  41. package/build/third-party-mcp-targets.js +75 -0
  42. package/build/version.generated.js +1 -1
  43. package/package.json +4 -3
  44. package/pipelines/full-automation.json +3 -1
  45. package/smoke-test/SMOKE-TEST.md +62 -17
@@ -15,10 +15,13 @@
15
15
  * - Stage 3 (start-tickets) is special-cased: the orchestrator does NOT call
16
16
  * ``runPipeline``; it emits an ``agent_task`` envelope naming the exact
17
17
  * ``/start-tickets ...`` command for the host agent to invoke in-session.
18
- * - Cross-stage variables (created_ticket_keys -> review fan-out ->
19
- * reviewed_ticket_keys -> start-tickets) are resolved server-side. Missing
20
- * cross-stage variables produce a typed ``VALIDATION`` failure never a
21
- * placeholder value.
18
+ * - Cross-stage variables (child_ticket_keys -> review fan-out ->
19
+ * reviewed_ticket_keys -> start-tickets) are resolved server-side. The
20
+ * idea-to-ticket stage emits a structured payload that keeps the Epic
21
+ * ``epic_parent_key`` separate from the implementable ``child_ticket_keys``,
22
+ * so the Epic parent is handed to NEITHER the review fan-out NOR
23
+ * start-tickets. Missing cross-stage variables produce a typed
24
+ * ``VALIDATION`` failure — never a placeholder value.
22
25
  */
23
26
  import { runPipeline, resumePipeline, peekPipelineRun } from "./pipeline-orchestrator.js";
24
27
  import { toStringVariable, substituteChainValue } from "./chain-utils.js";
@@ -174,6 +177,13 @@ const JIRA_KEY_EXACT = /^[A-Z][A-Z0-9]+-\d+$/;
174
177
  * keys merely *mentioned* by an earlier step (e.g. duplicate-detection search
175
178
  * hits) can never be mistaken for this run's creations. A string / no-field
176
179
  * input returns `[]`, which the caller treats as fail-closed.
180
+ *
181
+ * NOTE: This flat helper FLATTENS parent (`epic_key`) and `child_ticket_keys`
182
+ * into a single list. The full-automation chain fan-out must NOT use it — it
183
+ * would fold the Epic parent into the implementable list and hand the parent to
184
+ * review / start-tickets. The chain drives fan-out from the structured
185
+ * `extractCreatedTicketPayload` path instead; this helper remains only for
186
+ * non-chain compatibility callers that expect a flat single-ticket list.
177
187
  */
178
188
  export function extractCreatedTicketKeys(source) {
179
189
  const preferredKeys = [
@@ -267,24 +277,140 @@ function tryParseJson(raw) {
267
277
  return undefined;
268
278
  }
269
279
  }
280
+ function isValidJiraKey(value) {
281
+ return typeof value === "string" && JIRA_KEY_EXACT.test(value);
282
+ }
270
283
  /**
271
- * Derive the keys created by a completed idea-to-ticket child run AUTHORITATIVELY
272
- * from the upload step's structured output — never by scanning the whole
273
- * transcript. The upload step (`upload-and-track.md`) is the LAST step of the
274
- * idea-to-ticket pipeline and the chain never passes `skip_steps`, so it is the
275
- * final entry of `results`. Its `result` is parsed for a `{ created_ticket_keys }`
276
- * payload. Returns `[]` (→ caller fails closed) if the payload is absent or
277
- * malformed, so dedup-search hits from earlier steps can never become targets.
284
+ * Normalize a PARSED, STRUCTURED upload payload into a {@link CreatedTicketPayload}.
285
+ * Accepts only a parsed JSON object — never a string, array, null, number, or
286
+ * free text so Jira keys merely mentioned in prose can never become
287
+ * review/start-tickets targets. Two authoritative shapes are recognized:
288
+ *
289
+ * - Epic: ``{ epic_parent_key, child_ticket_keys }`` ``epic_parent_key`` only
290
+ * (the legacy ``epic_key`` alias is rejected for a clean break); at least one
291
+ * valid child key required; the parent must not appear in the children.
292
+ * - Single-ticket: ``{ created_ticket_keys: [key] }`` — exactly one valid key,
293
+ * normalized into ``child_ticket_keys`` (with ``created_ticket_keys`` kept as
294
+ * a compatibility alias). A multi-key flat ``created_ticket_keys`` WITHOUT
295
+ * ``epic_parent_key`` is treated as malformed rather than guessing the first
296
+ * key is the parent.
297
+ *
298
+ * Returns ``{ ok: false, error }`` on any malformed shape so the caller can fail
299
+ * the chain with a clear validation message.
278
300
  */
279
- export function extractCreatedKeysFromCompletedChild(childEnv) {
301
+ export function extractCreatedTicketPayload(source) {
302
+ if (source === null || typeof source !== "object" || Array.isArray(source)) {
303
+ return {
304
+ ok: false,
305
+ error: "Upload payload must be a structured JSON object; the chain never " +
306
+ "harvests Jira keys from free text, arrays, null, or numbers.",
307
+ };
308
+ }
309
+ const obj = source;
310
+ const hasEpicParent = "epic_parent_key" in obj;
311
+ const hasLegacyEpicKey = "epic_key" in obj;
312
+ const hasCreated = "created_ticket_keys" in obj;
313
+ // Epic path — driven exclusively by the explicit `epic_parent_key` field.
314
+ if (hasEpicParent) {
315
+ const parent = obj.epic_parent_key;
316
+ if (!isValidJiraKey(parent)) {
317
+ return {
318
+ ok: false,
319
+ error: `Epic payload "epic_parent_key" must be a valid Jira key; got ${JSON.stringify(parent)}.`,
320
+ };
321
+ }
322
+ const rawChildren = obj.child_ticket_keys;
323
+ if (!Array.isArray(rawChildren) || rawChildren.length === 0) {
324
+ return {
325
+ ok: false,
326
+ error: 'Epic payload requires "child_ticket_keys" with at least one ' +
327
+ "implementable child ticket key.",
328
+ };
329
+ }
330
+ const children = [];
331
+ for (const c of rawChildren) {
332
+ if (!isValidJiraKey(c)) {
333
+ return {
334
+ ok: false,
335
+ error: `Epic payload "child_ticket_keys" must contain only valid implementable Jira keys; got ${JSON.stringify(c)}.`,
336
+ };
337
+ }
338
+ if (c === parent) {
339
+ return {
340
+ ok: false,
341
+ error: `Epic parent ${parent} must not appear in "child_ticket_keys" (parent leakage).`,
342
+ };
343
+ }
344
+ children.push(c);
345
+ }
346
+ return { ok: true, payload: { epic_parent_key: parent, child_ticket_keys: children } };
347
+ }
348
+ // Reject the legacy `epic_key` alias explicitly: the chain accepts only
349
+ // `epic_parent_key` for the Epic parent (clean break, no alias recognition).
350
+ if (hasLegacyEpicKey) {
351
+ return {
352
+ ok: false,
353
+ error: 'Epic payload must use "epic_parent_key"; the legacy "epic_key" alias ' +
354
+ "is not accepted by the chain.",
355
+ };
356
+ }
357
+ // Single-ticket path — exactly one implementable key, normalized to children.
358
+ if (hasCreated) {
359
+ const raw = obj.created_ticket_keys;
360
+ if (!Array.isArray(raw) || raw.length !== 1 || !isValidJiraKey(raw[0])) {
361
+ return {
362
+ ok: false,
363
+ error: 'Single-ticket payload "created_ticket_keys" must contain exactly ' +
364
+ "one valid implementable Jira key. A multi-key list is malformed " +
365
+ '(the legacy "first key is the Epic parent" convention is not accepted).',
366
+ };
367
+ }
368
+ const key = raw[0];
369
+ return { ok: true, payload: { child_ticket_keys: [key], created_ticket_keys: [key] } };
370
+ }
371
+ return {
372
+ ok: false,
373
+ error: 'Upload payload has no recognized "epic_parent_key"/"child_ticket_keys" ' +
374
+ 'or single-ticket "created_ticket_keys" fields.',
375
+ };
376
+ }
377
+ /**
378
+ * Derive the structured {@link CreatedTicketPayload} created by a completed
379
+ * idea-to-ticket child run AUTHORITATIVELY from the upload step's structured
380
+ * output — never by scanning the whole transcript. The upload step
381
+ * (`upload-and-track.md`) is the LAST step of the idea-to-ticket pipeline and the
382
+ * chain never passes `skip_steps`, so it is the final entry of `results`. Its
383
+ * `result` is parsed (with `parseStructuredAgentResult` when it is a string) and
384
+ * passed to {@link extractCreatedTicketPayload}. Returns a validation error if
385
+ * the payload is absent or malformed, so dedup-search hits from earlier steps can
386
+ * never become targets.
387
+ */
388
+ export function extractCreatedTicketPayloadFromCompletedChild(childEnv) {
280
389
  const results = childEnv.results;
281
- if (!Array.isArray(results) || results.length === 0)
282
- return [];
390
+ if (!Array.isArray(results) || results.length === 0) {
391
+ return {
392
+ ok: false,
393
+ error: "idea-to-ticket child produced no results; cannot extract an upload payload.",
394
+ };
395
+ }
283
396
  const uploadResult = results[results.length - 1]?.result;
284
397
  const payload = typeof uploadResult === "string"
285
398
  ? parseStructuredAgentResult(uploadResult)
286
399
  : uploadResult;
287
- return extractCreatedTicketKeys(payload);
400
+ return extractCreatedTicketPayload(payload);
401
+ }
402
+ /**
403
+ * Compatibility wrapper over {@link extractCreatedTicketPayloadFromCompletedChild}
404
+ * that returns ONLY the normalized implementable `child_ticket_keys` (never the
405
+ * Epic parent) on success, and `[]` on any structured-extraction failure — so
406
+ * legacy callers keep their flat-array contract without ever guessing
407
+ * parent/child roles.
408
+ */
409
+ export function extractCreatedKeysFromCompletedChild(childEnv) {
410
+ const extraction = extractCreatedTicketPayloadFromCompletedChild(childEnv);
411
+ if (!extraction.ok)
412
+ return [];
413
+ return extraction.payload.child_ticket_keys;
288
414
  }
289
415
  /**
290
416
  * True when ``token`` appears in a failed child envelope's failure-text fields:
@@ -305,12 +431,29 @@ function failedEnvelopeMentions(env, token) {
305
431
  }
306
432
  return false;
307
433
  }
308
- /** Heuristic, no-LLM one-line stage summary. */
309
- export function summarizeStageCompletion(pipelineName, keys) {
310
- const joined = keys.join(", ");
434
+ /**
435
+ * Heuristic, no-LLM one-line stage summary. idea-to-ticket passes a structured
436
+ * {@link CreatedTicketPayload} so the summary distinguishes the Epic parent from
437
+ * implementable children; review / start-tickets pass a flat `string[]` of keys
438
+ * (wording preserved).
439
+ */
440
+ export function summarizeStageCompletion(pipelineName, keysOrPayload) {
311
441
  if (pipelineName === "idea-to-ticket") {
312
- return `Created tickets: ${joined}`;
442
+ if (!Array.isArray(keysOrPayload)) {
443
+ const payload = keysOrPayload;
444
+ const children = payload.child_ticket_keys.join(", ");
445
+ if (payload.epic_parent_key) {
446
+ return `Created Epic ${payload.epic_parent_key} and implementable children: ${children}`;
447
+ }
448
+ return `Created ticket: ${children}`;
449
+ }
450
+ // Backward-compatible flat-array form.
451
+ return `Created tickets: ${keysOrPayload.join(", ")}`;
313
452
  }
453
+ const keys = Array.isArray(keysOrPayload)
454
+ ? keysOrPayload
455
+ : keysOrPayload.child_ticket_keys;
456
+ const joined = keys.join(", ");
314
457
  if (pipelineName === "review-ticket") {
315
458
  return `Reviewed tickets: ${joined}`;
316
459
  }
@@ -504,7 +647,12 @@ function buildStageVariables(stage, args, extra = {}) {
504
647
  }
505
648
  return resolved;
506
649
  }
507
- /** Resolve a fan-out / cross-stage input list from prior stage outputs. */
650
+ /**
651
+ * Resolve a fan-out / cross-stage input list from prior stage outputs. This
652
+ * helper is intentionally KEY-SPECIFIC: it never searches alternate field names,
653
+ * so a recipe asking for `child_ticket_keys` will not silently fall back to
654
+ * `created_ticket_keys`.
655
+ */
508
656
  function resolveCrossStageList(row, stageIndex, key) {
509
657
  for (let i = stageIndex - 1; i >= 0; i--) {
510
658
  const outputs = row.stages[i]?.outputs;
@@ -514,6 +662,50 @@ function resolveCrossStageList(row, stageIndex, key) {
514
662
  }
515
663
  return null;
516
664
  }
665
+ /**
666
+ * Scan stage outputs STRICTLY BEFORE ``stageIndex`` and return the first valid
667
+ * string ``epic_parent_key``. Non-string / empty values are ignored. Used as a
668
+ * second-layer guard so the Epic parent can be filtered out of the start-tickets
669
+ * handoff even if an upstream persisted stage output is malformed.
670
+ */
671
+ export function resolvePriorEpicParentKey(row, stageIndex) {
672
+ for (let i = 0; i < stageIndex; i++) {
673
+ const candidate = row.stages[i]?.outputs?.epic_parent_key;
674
+ if (typeof candidate === "string" && candidate.trim() !== "") {
675
+ return candidate;
676
+ }
677
+ }
678
+ return null;
679
+ }
680
+ /**
681
+ * Resolve the start-tickets stage's configured fan-out input and defensively
682
+ * filter out any key equal to the prior ``epic_parent_key``. Fails closed (a
683
+ * validation-oriented message) when the configured input is missing/empty, or
684
+ * when excluding the Epic parent leaves no implementable keys. This is the
685
+ * belt-and-suspenders second layer; the primary fix excludes the parent before
686
+ * review via the structured extractor.
687
+ */
688
+ export function resolveStartTicketKeys(row, stageIndex, fanOutInput) {
689
+ const configured = resolveCrossStageList(row, stageIndex, fanOutInput);
690
+ if (!configured || configured.length === 0) {
691
+ return {
692
+ ok: false,
693
+ error: `Missing cross-stage variable "${fanOutInput}" for start-tickets.`,
694
+ };
695
+ }
696
+ const epicParent = resolvePriorEpicParentKey(row, stageIndex);
697
+ const filtered = epicParent
698
+ ? configured.filter((k) => k !== epicParent)
699
+ : configured;
700
+ if (filtered.length === 0) {
701
+ return {
702
+ ok: false,
703
+ error: `No implementable tickets remain for start-tickets after excluding the ` +
704
+ `Epic parent ${epicParent}.`,
705
+ };
706
+ }
707
+ return { ok: true, keys: filtered };
708
+ }
517
709
  // ---------------------------------------------------------------------------
518
710
  // Child-pipeline envelope handling
519
711
  // ---------------------------------------------------------------------------
@@ -633,22 +825,37 @@ async function handleChildPipelineEnvelope(persistence, recipe, row, childEnv, o
633
825
  }
634
826
  return { kind: "advanced", row: updated };
635
827
  }
636
- // Single-child stage (idea-to-ticket): finalize. Derive created keys ONLY from
637
- // the upload step's structured payload — never the whole transcript — so
638
- // duplicate-detection search hits cannot become review/start-tickets targets.
639
- const keys = extractCreatedKeysFromCompletedChild(childEnv);
640
- if (keys.length === 0) {
828
+ // Single-child stage (idea-to-ticket): finalize. Derive the STRUCTURED payload
829
+ // ONLY from the upload step's structured output — never the whole transcript —
830
+ // so duplicate-detection search hits cannot become review/start-tickets
831
+ // targets. The Epic parent (`epic_parent_key`) is kept SEPARATE from the
832
+ // implementable `child_ticket_keys` so it is handed to NEITHER review NOR
833
+ // start-tickets. `created_ticket_keys` is persisted as an implementable-only
834
+ // compatibility alias (= children) and never carries the Epic parent.
835
+ const extraction = extractCreatedTicketPayloadFromCompletedChild(childEnv);
836
+ if (!extraction.ok) {
837
+ return {
838
+ kind: "fail",
839
+ envelope: await failChainValidation(persistence, recipe, row, extraction.error),
840
+ };
841
+ }
842
+ const payload = extraction.payload;
843
+ if (payload.child_ticket_keys.length === 0) {
641
844
  return {
642
845
  kind: "fail",
643
- envelope: await failChainValidation(persistence, recipe, row, "idea-to-ticket produced no structured created_ticket_keys payload; cannot continue the chain."),
846
+ envelope: await failChainValidation(persistence, recipe, row, "idea-to-ticket produced no implementable child tickets; full automation cannot continue without implementable tickets."),
644
847
  };
645
848
  }
646
849
  const stages = cloneStages(row.stages);
647
850
  const stage = stages[idx];
648
851
  stage.status = "completed";
649
852
  stage.pipeline_run_id = null;
650
- stage.outputs = { created_ticket_keys: keys };
651
- stage.summary = summarizeStageCompletion(stageRecipe.pipeline_name, keys);
853
+ stage.outputs = {
854
+ child_ticket_keys: payload.child_ticket_keys,
855
+ created_ticket_keys: payload.child_ticket_keys,
856
+ ...(payload.epic_parent_key ? { epic_parent_key: payload.epic_parent_key } : {}),
857
+ };
858
+ stage.summary = summarizeStageCompletion(stageRecipe.pipeline_name, payload);
652
859
  let updated;
653
860
  try {
654
861
  updated = await persistence.patchRun(row.chain_run_id, {
@@ -815,14 +1022,18 @@ async function startStartTicketsStage(persistence, recipe, row) {
815
1022
  const stageRecipe = recipe.stages[idx];
816
1023
  const total = recipe.stages.length;
817
1024
  const fanOutInput = stageRecipe.fan_out_input ?? "reviewed_ticket_keys";
818
- const keys = resolveCrossStageList(row, idx, fanOutInput);
819
- if (!keys || keys.length === 0) {
820
- return failChainValidation(persistence, recipe, row, `Missing cross-stage variable "${fanOutInput}" for start-tickets.`);
1025
+ // Second-layer guard: filter the Epic parent out of the start keys even if an
1026
+ // upstream persisted stage output is malformed (the primary exclusion happens
1027
+ // before review via the structured extractor).
1028
+ const resolution = resolveStartTicketKeys(row, idx, fanOutInput);
1029
+ if (!resolution.ok) {
1030
+ return failChainValidation(persistence, recipe, row, resolution.error);
821
1031
  }
1032
+ const keys = resolution.keys;
822
1033
  // Hands-off chain runs (auto_approve=true, the default) spawn the
823
1034
  // implementation agents auto-approved too, so they don't stall on approval
824
1035
  // gates. A --require-approval run (auto_approve=false) spawns them interactive.
825
- const autoApproveFlag = row.args.auto_approve === true ? " --auto-approve" : "";
1036
+ const autoApproveFlag = row.args.auto_approve === true ? " --auto" : "";
826
1037
  const command = `/start-tickets ${keys.join(" ")}${autoApproveFlag}`;
827
1038
  const stages = cloneStages(row.stages);
828
1039
  stages[idx].status = "paused";
@@ -1018,7 +1229,10 @@ export async function resumeFullAutomation(deps, input) {
1018
1229
  chain_total: total,
1019
1230
  });
1020
1231
  }
1021
- const startedKeys = resolveCrossStageList(row, idx, stageRecipe.fan_out_input ?? "reviewed_ticket_keys") ?? [];
1232
+ // Persist started_ticket_keys WITHOUT the Epic parent, using the same
1233
+ // defensive filter as startStartTicketsStage.
1234
+ const startResolution = resolveStartTicketKeys(row, idx, stageRecipe.fan_out_input ?? "reviewed_ticket_keys");
1235
+ const startedKeys = startResolution.ok ? startResolution.keys : [];
1022
1236
  const stages = cloneStages(row.stages);
1023
1237
  stages[idx].status = "completed";
1024
1238
  stages[idx].pipeline_run_id = null;