@bridge_gpt/mcp-server 0.2.44 → 0.2.46
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 +8 -7
- package/build/agents.generated.js +1 -1
- package/build/commands.generated.js +4 -4
- package/build/conduct-epic/checkpoint-store.js +50 -4
- package/build/conduct-epic/cli.js +74 -6
- package/build/conductor/done-gate.js +39 -1
- package/build/conductor/git-ci-types.js +34 -0
- package/build/docs.generated.js +1 -1
- package/build/index.js +50 -15
- package/build/merge-pull-request.js +143 -16
- package/build/pipelines.generated.js +1 -1
- package/build/readme.generated.js +1 -1
- package/build/tool-surface-gating.js +9 -3
- package/build/update-status.js +15 -0
- package/build/version.generated.js +2 -2
- package/package.json +2 -2
|
@@ -75,6 +75,7 @@
|
|
|
75
75
|
* privileged write subtly wrong.
|
|
76
76
|
*/
|
|
77
77
|
import { DEFAULT_GATE_NAME, REQUIRED_CI_CHECKS_GREEN, REVIEW_STATE, normalizeCheckName, } from "./conductor/git-ci-types.js";
|
|
78
|
+
import { MERGE_REVIEW_WAIVED_BY_DEGRADATION_REASON, VERDICTLESS_DISPOSITION_FAIL_OPEN, VERDICTLESS_DISPOSITION_PARK, } from "./conductor/git-ci-types.js";
|
|
78
79
|
import { evaluateReviewCondition, normalizeReviewSnapshot, parseDoneGateConfig, } from "./conductor/done-gate.js";
|
|
79
80
|
import { buildGateIdentity, makeMergeActionKey } from "./conductor/merge-identity.js";
|
|
80
81
|
import { resolveLocalMergeMethod, runApprovedLocalMerge, } from "./conductor/local-merge.js";
|
|
@@ -135,6 +136,10 @@ function envelope(merged, outcome, reason, retryHint, evaluatedHeadSha, prNumber
|
|
|
135
136
|
result.http_status = diagnostics.http_status;
|
|
136
137
|
if (diagnostics.completion !== undefined)
|
|
137
138
|
result.completion = diagnostics.completion;
|
|
139
|
+
// BAPI-915: appended LAST, after every pre-existing optional diagnostic, so the
|
|
140
|
+
// serialized key order of every envelope that carries no waiver is unchanged.
|
|
141
|
+
if (diagnostics.review_waiver !== undefined)
|
|
142
|
+
result.review_waiver = diagnostics.review_waiver;
|
|
138
143
|
return result;
|
|
139
144
|
}
|
|
140
145
|
// ---------------------------------------------------------------------------
|
|
@@ -310,13 +315,94 @@ const SUPPORTED_REVIEW_SOURCES = new Set(["verdict_protocol", "native_review_dec
|
|
|
310
315
|
const REVIEW_UNAVAILABLE_REASON = "review_unavailable";
|
|
311
316
|
const REVIEW_SOURCE_UNSUPPORTED_REASON = "review_source_unsupported";
|
|
312
317
|
const HEAD_SHA_DRIFT_REASON = "head_sha_drift";
|
|
318
|
+
/** Wrap a refusal envelope as a pre-check decision. */
|
|
319
|
+
function refuse(env) {
|
|
320
|
+
return { kind: "refused", envelope: env };
|
|
321
|
+
}
|
|
322
|
+
/**
|
|
323
|
+
* Append the waiver marker to an envelope produced AFTER a waived pre-check.
|
|
324
|
+
*
|
|
325
|
+
* Applied at the handler's return seam rather than threaded through
|
|
326
|
+
* `interpretMergeResponse` and `executeApprovedLocalMerge`, both of which build
|
|
327
|
+
* envelopes on many internal branches — one seam is auditable, a dozen threaded
|
|
328
|
+
* parameters are not. Every envelope downstream of a waiver carries it,
|
|
329
|
+
* including one that a LATER guard refused: the waiver really was applied, and
|
|
330
|
+
* hiding it on the refusal would leave the operator reading a `ci_not_green`
|
|
331
|
+
* with no idea the review had been waived to get there. This mirrors the server
|
|
332
|
+
* service, which folds the same marker into every post-review evidence bundle.
|
|
333
|
+
*
|
|
334
|
+
* Key order is preserved: the spread keeps the existing keys in place and the
|
|
335
|
+
* new one lands last, exactly where `envelope()` would have put it.
|
|
336
|
+
*/
|
|
337
|
+
function withReviewWaiver(env, waiver) {
|
|
338
|
+
if (waiver === undefined)
|
|
339
|
+
return env;
|
|
340
|
+
return { ...env, review_waiver: waiver };
|
|
341
|
+
}
|
|
342
|
+
/**
|
|
343
|
+
* True only when the review carries NO verdict AT ALL for the configured source.
|
|
344
|
+
*
|
|
345
|
+
* This is narrower than "the evaluation did not pass", and the gap between the two
|
|
346
|
+
* is a real hole. Under `verdict_protocol` the server resolves the sticky comment
|
|
347
|
+
* to `null` when no comment was ever posted, but to the string `"unknown"` for
|
|
348
|
+
* FOUR different ambiguities: two same-head comments that contradict each other
|
|
349
|
+
* (BAPI-733), a stale `Reviewed-SHA`, a malformed one, and an unresolvable head.
|
|
350
|
+
* `evaluateReviewCondition` flattens all five into
|
|
351
|
+
* `{passed: false, changesRequested: false}`, so waiving on that result alone
|
|
352
|
+
* would waive a CONTRADICTION — and a contradiction can contain a
|
|
353
|
+
* changes-requested comment that merely lost the tie-break. That is precisely the
|
|
354
|
+
* human-escalation case, and waiving it would contradict this feature's own claim
|
|
355
|
+
* that an objection is never waivable.
|
|
356
|
+
*
|
|
357
|
+
* So the waiver mirrors the Python seam field for field: `api/library/vcs/epic_observers.py`
|
|
358
|
+
* routes `sticky is None` to `PENDING` (waivable) and EVERY `"unknown"` to
|
|
359
|
+
* `UNKNOWN` (never waivable), and `conductor_merge_service` admits only `PENDING`.
|
|
360
|
+
* No new server field is needed to do this — `sticky_verdict` already carries the
|
|
361
|
+
* distinction on the wire, and `normalizeReviewSnapshot` already preserves it.
|
|
362
|
+
*
|
|
363
|
+
* The raw body is consulted too, because the normalizer maps an UNRECOGNIZED
|
|
364
|
+
* `sticky_verdict` to `null` as well. A value outside the server's own closed
|
|
365
|
+
* vocabulary is not evidence that no verdict exists, so it does not get waived.
|
|
366
|
+
*
|
|
367
|
+
* `native_review_decision` has no ambiguity channel of its own — GitHub returns a
|
|
368
|
+
* single authoritative decision, and a non-approving, non-rejecting one genuinely
|
|
369
|
+
* means no decision was given — so it needs no extra guard.
|
|
370
|
+
*/
|
|
371
|
+
function verdictIsGenuinelyAbsent(condition, snapshot, rawBody) {
|
|
372
|
+
if (condition.source !== "verdict_protocol")
|
|
373
|
+
return true;
|
|
374
|
+
// `"unknown"` (and anything else non-null) is an unresolved verdict, not a
|
|
375
|
+
// missing one.
|
|
376
|
+
if (snapshot.sticky_verdict !== null)
|
|
377
|
+
return false;
|
|
378
|
+
const detail = isPlainObject(rawBody) && isPlainObject(rawBody.detail) ? rawBody.detail : null;
|
|
379
|
+
if (detail === null)
|
|
380
|
+
return false;
|
|
381
|
+
const raw = detail.sticky_verdict;
|
|
382
|
+
return raw === null || raw === undefined;
|
|
383
|
+
}
|
|
384
|
+
/**
|
|
385
|
+
* The condition's disposition, with the effective default applied.
|
|
386
|
+
*
|
|
387
|
+
* ABSENT means `park`, and the default is resolved HERE rather than materialized
|
|
388
|
+
* onto the parsed condition, so a config that never carried the field keeps its
|
|
389
|
+
* byte-identical `config_hash` and therefore its merge action key.
|
|
390
|
+
*/
|
|
391
|
+
function effectiveDisposition(condition) {
|
|
392
|
+
return condition.verdictless_disposition ?? VERDICTLESS_DISPOSITION_PARK;
|
|
393
|
+
}
|
|
313
394
|
/**
|
|
314
395
|
* Evaluate the configured `review_state` condition before the merge is sent.
|
|
315
396
|
*
|
|
316
|
-
* Returns a
|
|
317
|
-
*
|
|
318
|
-
*
|
|
319
|
-
* review that
|
|
397
|
+
* Returns a {@link ReviewPrecheckDecision}: a refusal, an ordinary continuation,
|
|
398
|
+
* or a continuation carrying the bounded waiver diagnostic. Every failure
|
|
399
|
+
* direction is contained here and every one of them is a refusal: a review this
|
|
400
|
+
* side cannot read, cannot parse, or cannot evaluate is not a review that
|
|
401
|
+
* approved anything — and BAPI-915 changed none of that. The single direction it
|
|
402
|
+
* added is a review that WAS read, at this head, under an explicitly configured
|
|
403
|
+
* `fail_open`, carrying no objection and NO VERDICT AT ALL — not merely a verdict
|
|
404
|
+
* this side could not resolve. See {@link verdictIsGenuinelyAbsent} for why that
|
|
405
|
+
* distinction is load-bearing rather than pedantic.
|
|
320
406
|
*
|
|
321
407
|
* `min_approvals` and `combination` are refused OUTRIGHT rather than approximated
|
|
322
408
|
* with a weaker rule. The server refuses them too, for the same reason: silently
|
|
@@ -325,9 +411,9 @@ const HEAD_SHA_DRIFT_REASON = "head_sha_drift";
|
|
|
325
411
|
*/
|
|
326
412
|
async function precheckReviewCondition(deps, condition, prNumber, expectedHeadSha) {
|
|
327
413
|
if (!SUPPORTED_REVIEW_SOURCES.has(condition.source)) {
|
|
328
|
-
return envelope(false, "review_source_unsupported", REVIEW_SOURCE_UNSUPPORTED_REASON, "needs_human", expectedHeadSha, prNumber);
|
|
414
|
+
return refuse(envelope(false, "review_source_unsupported", REVIEW_SOURCE_UNSUPPORTED_REASON, "needs_human", expectedHeadSha, prNumber));
|
|
329
415
|
}
|
|
330
|
-
const unavailable = () => envelope(false, "review_unavailable", REVIEW_UNAVAILABLE_REASON, "needs_human", expectedHeadSha, prNumber);
|
|
416
|
+
const unavailable = () => refuse(envelope(false, "review_unavailable", REVIEW_UNAVAILABLE_REASON, "needs_human", expectedHeadSha, prNumber));
|
|
331
417
|
// URL and header construction sit OUTSIDE the try, matching the required-check
|
|
332
418
|
// resolver: a failure there is a broken dependency, not an unreadable review,
|
|
333
419
|
// and it belongs to the handler's outer containment boundary. The repository
|
|
@@ -350,7 +436,12 @@ async function precheckReviewCondition(deps, condition, prNumber, expectedHeadSh
|
|
|
350
436
|
// `normalizeReviewSnapshot` returns null for `available: false` and for any
|
|
351
437
|
// body it cannot read as a review snapshot — the same answer, because both
|
|
352
438
|
// mean the same thing here.
|
|
353
|
-
|
|
439
|
+
//
|
|
440
|
+
// The raw body is kept as well: `verdictIsGenuinelyAbsent` below has to tell an
|
|
441
|
+
// ABSENT `sticky_verdict` from one the normalizer could not recognize, and the
|
|
442
|
+
// normalized snapshot maps both to `null`.
|
|
443
|
+
const reviewBody = await readJson(reviewResp);
|
|
444
|
+
const snapshot = normalizeReviewSnapshot(reviewBody);
|
|
354
445
|
if (snapshot === null)
|
|
355
446
|
return unavailable();
|
|
356
447
|
// Head equality is checked BEFORE the evaluator, and takes precedence over an
|
|
@@ -362,16 +453,42 @@ async function precheckReviewCondition(deps, condition, prNumber, expectedHeadSh
|
|
|
362
453
|
if (typeof snapshot.head_sha === "string" && snapshot.head_sha.length > 0) {
|
|
363
454
|
diagnostics.actual_head_sha = snapshot.head_sha;
|
|
364
455
|
}
|
|
365
|
-
return envelope(false, "refused", HEAD_SHA_DRIFT_REASON, "needs_human", expectedHeadSha, prNumber, diagnostics);
|
|
456
|
+
return refuse(envelope(false, "refused", HEAD_SHA_DRIFT_REASON, "needs_human", expectedHeadSha, prNumber, diagnostics));
|
|
366
457
|
}
|
|
367
458
|
const evaluation = evaluateReviewCondition(condition, snapshot);
|
|
368
459
|
if (!evaluation.passed) {
|
|
460
|
+
// BAPI-915 — the ONE waivable direction, and every guard that narrows it has
|
|
461
|
+
// already run above: the source is supported, the response was readable, the
|
|
462
|
+
// envelope was not `available: false`, and the snapshot is bound to THIS
|
|
463
|
+
// head. What remains is a review that was read and carries NO VERDICT AT
|
|
464
|
+
// ALL — the shape `claude-review` leaves behind when it dies during
|
|
465
|
+
// synthesis, having posted nothing.
|
|
466
|
+
//
|
|
467
|
+
// `changesRequested` is excluded explicitly: a human objected to this exact
|
|
468
|
+
// head, and no configuration waives an objection. Head drift is excluded by
|
|
469
|
+
// construction, because the head check above returns before the evaluator
|
|
470
|
+
// ever runs — that ordering is load-bearing and must not be folded in.
|
|
471
|
+
//
|
|
472
|
+
// `verdictIsGenuinelyAbsent` is the third guard and it is NOT redundant with
|
|
473
|
+
// the evaluator: the evaluator reports the same
|
|
474
|
+
// `{passed: false, changesRequested: false}` for "nobody posted anything" and
|
|
475
|
+
// for "two same-head comments contradict each other", and only the first may
|
|
476
|
+
// ever be waived.
|
|
477
|
+
//
|
|
478
|
+
// The disposition is compared for EXACT equality with `fail_open`. An absent
|
|
479
|
+
// field and an explicit `park` both fall through to the byte-identical
|
|
480
|
+
// pre-BAPI-915 refusal below.
|
|
481
|
+
if (effectiveDisposition(condition) === VERDICTLESS_DISPOSITION_FAIL_OPEN &&
|
|
482
|
+
evaluation.changesRequested === false &&
|
|
483
|
+
verdictIsGenuinelyAbsent(condition, snapshot, reviewBody)) {
|
|
484
|
+
return { kind: "waived", waiver: MERGE_REVIEW_WAIVED_BY_DEGRADATION_REASON };
|
|
485
|
+
}
|
|
369
486
|
// A wait, not a park: a verdict for THIS head may still be posted. The
|
|
370
487
|
// evaluator's own reason is forwarded verbatim rather than flattened, so the
|
|
371
488
|
// caller can tell "no verdict yet" from "changes requested".
|
|
372
|
-
return envelope(false, "review_not_approved", evaluation.reason, "retry_later", expectedHeadSha, prNumber);
|
|
489
|
+
return refuse(envelope(false, "review_not_approved", evaluation.reason, "retry_later", expectedHeadSha, prNumber));
|
|
373
490
|
}
|
|
374
|
-
return
|
|
491
|
+
return { kind: "proceed" };
|
|
375
492
|
}
|
|
376
493
|
// ---------------------------------------------------------------------------
|
|
377
494
|
// Server-response interpretation
|
|
@@ -691,10 +808,18 @@ export async function mergePullRequestHandler(deps, args) {
|
|
|
691
808
|
// carries a `review_state` condition, and it returns BEFORE the pre-POST
|
|
692
809
|
// section below — so a refusal here constructs no merge URL, no merge
|
|
693
810
|
// headers, no action key, and sends no merge request.
|
|
811
|
+
//
|
|
812
|
+
// BAPI-915: the decision is now three-valued. A refusal still returns before
|
|
813
|
+
// anything below runs; a waiver continues into the identical merge path and
|
|
814
|
+
// is carried through to the reported envelope so the operator can see that
|
|
815
|
+
// the merge advanced on CI evidence alone.
|
|
816
|
+
let reviewWaiver;
|
|
694
817
|
if (resolution.reviewCondition !== null) {
|
|
695
|
-
const
|
|
696
|
-
if (
|
|
697
|
-
return text(
|
|
818
|
+
const decision = await precheckReviewCondition(deps, resolution.reviewCondition, prNumber, expectedHeadSha);
|
|
819
|
+
if (decision.kind === "refused")
|
|
820
|
+
return text(decision.envelope);
|
|
821
|
+
if (decision.kind === "waived")
|
|
822
|
+
reviewWaiver = decision.waiver;
|
|
698
823
|
}
|
|
699
824
|
// --- pre-POST section ---------------------------------------------------
|
|
700
825
|
const gateIdentity = buildGateIdentity(DEFAULT_GATE_NAME, resolution.configHash);
|
|
@@ -729,6 +854,7 @@ export async function mergePullRequestHandler(deps, args) {
|
|
|
729
854
|
catch {
|
|
730
855
|
return text(envelope(false, "unknown", "merge_request_not_observed", "retry_later", expectedHeadSha, prNumber, {
|
|
731
856
|
hint: UNKNOWN_HINT,
|
|
857
|
+
review_waiver: reviewWaiver,
|
|
732
858
|
}));
|
|
733
859
|
}
|
|
734
860
|
if (!mergeResp.ok) {
|
|
@@ -736,10 +862,11 @@ export async function mergePullRequestHandler(deps, args) {
|
|
|
736
862
|
// status only — raw upstream error text is never forwarded to the agent.
|
|
737
863
|
await deps.handleResponse(mergeResp).catch(() => "");
|
|
738
864
|
if (mergeResp.status === 409) {
|
|
739
|
-
return text(envelope(false, "action_key_mismatch", "action_key_mismatch", "needs_human", expectedHeadSha, prNumber));
|
|
865
|
+
return text(envelope(false, "action_key_mismatch", "action_key_mismatch", "needs_human", expectedHeadSha, prNumber, { review_waiver: reviewWaiver }));
|
|
740
866
|
}
|
|
741
867
|
return text(envelope(false, "error", "merge_request_failed", "needs_human", expectedHeadSha, prNumber, {
|
|
742
868
|
http_status: mergeResp.status,
|
|
869
|
+
review_waiver: reviewWaiver,
|
|
743
870
|
}));
|
|
744
871
|
}
|
|
745
872
|
const mergeJson = await readJson(mergeResp);
|
|
@@ -749,9 +876,9 @@ export async function mergePullRequestHandler(deps, args) {
|
|
|
749
876
|
if (executionMode === "local" &&
|
|
750
877
|
isPlainObject(mergeJson) &&
|
|
751
878
|
mergeJson.status === LOCAL_APPROVAL_STATUS) {
|
|
752
|
-
return text(await executeApprovedLocalMerge(deps, mergeJson, prNumber, expectedHeadSha, actionKey));
|
|
879
|
+
return text(withReviewWaiver(await executeApprovedLocalMerge(deps, mergeJson, prNumber, expectedHeadSha, actionKey), reviewWaiver));
|
|
753
880
|
}
|
|
754
|
-
return text(interpretMergeResponse(mergeJson, expectedHeadSha, prNumber));
|
|
881
|
+
return text(withReviewWaiver(interpretMergeResponse(mergeJson, expectedHeadSha, prNumber), reviewWaiver));
|
|
755
882
|
}
|
|
756
883
|
catch {
|
|
757
884
|
// Containment: never throw through the MCP transport, and never serialize the
|
|
@@ -1023,5 +1023,5 @@ export const INSTRUCTIONS = {
|
|
|
1023
1023
|
"upload-and-track.md": "Step-10 umbrella upload instruction. Idempotently create the Jira ticket(s) for this run, attach the full draft(s), and call `track_ticket`.\n\n## Inputs\n\n- Run manifest: `{docs_dir}/idea-to-ticket/{slug}-{run_id}/run-manifest.json`.\n- Draft metadata: `{docs_dir}/idea-to-ticket/{slug}-{run_id}/draft-metadata.json`.\n- For epic runs, this instruction is also responsible for producing or refreshing `{docs_dir}/idea-to-ticket/{slug}-{run_id}/decomposition-plan.json` before any Jira mutation, by following `decompose-epic-candidate.md` (hard cap `{max_children}`).\n- Pipeline variable `auto_approve_external` controls whether the external-mutation pause is skipped (for this run, `auto_approve_external` = `{auto_approve_external}`). Treat the literal string `\"true\"` as skip; any other value (including `\"false\"`, missing, or empty) means pause and ask.\n\n## Instructions\n\n> **Orchestrator-directed step.** This agent task is part of the full-automation chain and is authorized to call `get_tickets`, `create_ticket`, `attachment` (operations: `upload`, `list`), `update_ticket_description`, `track_ticket`, and `add_comment`, and to execute the shared `gather-and-attach-materials.md` instruction, as directed below — performing orchestrator-directed tool calls is not \"re-orchestrating\".\n\n1. Read `run-manifest.json` and `draft-metadata.json`. Branch internally based on the manifest's `scope`:\n - `task` or `spike` → follow the **Single-ticket path** below.\n - `epic_candidate` → follow the **Epic path** below.\n The orchestrator does not support conditional steps; this branching lives in agent logic.\n\n2. External approval gate, applied before any mutating MCP tool call:\n - If `auto_approve_external` is `\"false\"` (or any non-`\"true\"` value), summarize the exact planned Jira mutations — list every `create_ticket`, `attachment` (operation: `\"upload\"`), and `track_ticket` call with its key arguments — and ask the user for explicit confirmation in this agent task before proceeding.\n - If `auto_approve_external` is `\"true\"`, proceed without the confirmation pause.\n\n3. **Single-ticket path** (`scope` is `task` or `spike`):\n 1. Idempotency lookup. Call `get_tickets` with its `labels` parameter set to both the per-run label `<idempotency_label>` and the stable `bapi-idea-hash-{idea_hash}` label from `draft-metadata.json` (comma-separated). If a match is found by either label, reuse that ticket key and skip `create_ticket`.\n 2. If no match was found, call `create_ticket` with `summary`, `slim_description` as the description, `issue_type`, and `labels` exactly as written in the metadata. Capture the returned `ticket_key`.\n 3. Upload the full markdown draft via `attachment` (operation: `\"upload\"`) using `attachment_path`.\n 4. **Gather and attach referenced materials.** Execute the shared `gather-and-attach-materials.md` instruction as an `agent_task`, passing `ticket_number` = the resolved ticket key, `draft_file_path` = `attachment_path`, and `auto_approve_external` = the inherited `{auto_approve_external}` value. It attaches phase-eligible local materials (Planning Assets, Downloadable Assets, and Planning & Downloadable Assets) and records external/auth-gated and binary/image materials per its own warn-not-halt rules. Any attach failure it reports is recorded (via `update_ticket_description`) as `partial_success` and never halts this step.\n 5. Call `track_ticket` with the resolved ticket key so Bridge API picks the new ticket up.\n 6. Write `{docs_dir}/idea-to-ticket/{slug}-{run_id}/upload-state.json` describing the final state.\n\n4. **Epic path** (`scope` is `epic_candidate`):\n 1. If `decomposition-plan.json` does not yet exist for this run, follow `decompose-epic-candidate.md` first to produce it (hard cap `{max_children}`). If it **does** already exist, it is the approved manifest and this step performs **no fresh decomposition** — read it and use it as written. Decomposition happens once; re-deriving the split immediately before upload is how children end up overlapping or contradicting the parent they are about to be created under.\n 2. Render bodies against the frozen manifest by following `render-ticket-manifest.md`: one `jira-ticket-writer` invocation per entry that lacks a draft on disk, each bound to its own entry's fixed boundary and size band, using the `draft_path` from the decomposition plan. A rendering invocation may not re-split, merge, reorder, renumber, or rescope. Its preflight is fail-closed — an unapproved manifest, a changed manifest identity, an XL epic child, or a missing writer draft stops the flow **before** any Jira mutation. After drafting, extend `draft-metadata.json` so `children[]` mirrors the final list from the decomposition plan.\n 3. Create only from writer-produced drafts. Every ticket body uploaded below came from `jira-ticket-writer`; nothing here composes a description inline.\n 4. Parent first. Look up the Epic parent by `bapi-idea-to-ticket-{run_id}-parent` via `get_tickets`. If found, reuse that key; otherwise call `create_ticket` with the parent's summary, slim description, issue type `Epic`, and parent labels. Attach the Epic draft via `attachment` (operation: `\"upload\"`) using `parent.attachment_path`. Then **gather and attach the Epic parent's referenced materials** by executing the shared `gather-and-attach-materials.md` instruction as an `agent_task`, passing `ticket_number` = the Epic key, `draft_file_path` = `parent.attachment_path`, and `auto_approve_external` = the inherited `{auto_approve_external}` value. Then call `track_ticket` for the Epic key.\n 5. Children next. For each child in order:\n - Look up by the child's `idempotency_label`. If found, reuse that key.\n - Otherwise call `create_ticket(parent_key=<epic_key>)` with the child's `summary`, `slim_description`, `issue_type`, and `labels`. The `parent_key` is required so Jira's modern parent linkage is set.\n - Upload the child draft via `attachment` (operation: `\"upload\"`) using `draft_path`.\n - **Gather and attach this child's referenced materials** by executing the shared `gather-and-attach-materials.md` instruction as an `agent_task`, passing `ticket_number` = the child key, `draft_file_path` = `draft_path`, and `auto_approve_external` = the inherited `{auto_approve_external}` value.\n - Call `track_ticket` for the child key.\n 6. After every parent or child mutation, write partial progress to `{docs_dir}/idea-to-ticket/{slug}-{run_id}/upload-state.json` so a later resume can pick up exactly where the run stopped.\n 7. **Recommended implementation order comment.** Once the Epic parent and all surviving children exist (real keys known), post a single comment on the Epic via `add_comment` with `ticket_number` set to the Epic key. The comment carries (a) a short System Goals / Non-Functional Requirements summary from `goals-and-nfrs.md`, and (b) the **Recommended Implementation Order** — the children in order, each referenced by its real Jira key, derived from the `depends_on` / `recommended_after` / `order_rationale` fields in `decomposition-plan.json`. State that this is recommended sequencing only — do **not** create Jira dependency links and do **not** attach a separate markdown doc. Skip this only if the run reused a pre-existing comment for the same run (idempotency); do not post duplicate order comments on resume.\n\n5. Required child label set whenever any child is created: `ai-generated`, `idea-to-ticket`, `idea-to-ticket-child`, and `bapi-idea-to-ticket-{run_id}-child-<N>` (1-based index from the decomposition plan).\n\n6. Partial-failure recovery rules:\n - If `create_ticket` succeeds but `attachment` (operation: `\"upload\"`) fails, record the outcome as `partial_success` in `upload-state.json` and continue with the next planned mutation; do not retry inside this step.\n - If the Epic parent is created successfully but one or more children fail, preserve the parent key and any completed child keys in `upload-state.json` before raising the failure.\n - On resume of any prior run, search by every relevant idempotency label first (`bapi-idea-to-ticket-{run_id}` for single tickets, `bapi-idea-to-ticket-{run_id}-parent`, and each `bapi-idea-to-ticket-{run_id}-child-<N>`) before considering any `create_ticket` call. Idempotency labels are how this pipeline avoids creating duplicate tickets across retries.\n\n## Return\n\nConfirm the run's final upload outcome: attachment results, `track_ticket` outcome, and any `partial_success` rows recorded in `upload-state.json`.\n\nThen, as the FINAL content of your reply, emit a fenced ```json block holding the authoritative payload for this run — and nothing else. The chain reads ONLY this final fenced JSON block to pick its review / start-tickets targets, so it must contain exactly the keys from `upload-state.json` and never any key you merely looked up during duplicate detection. Duplicate-detection / looked-up keys must not appear in this authoritative payload unless they are the final created/reused ticket for this run.\n\nThere are exactly two authoritative final payload shapes:\n\n- **Single-ticket path** (`scope` is `task` or `spike`): emit strictly `created_ticket_keys` containing **exactly one** implementable ticket key. `created_ticket_keys` is only for the single-ticket `task`/`spike` path and must contain exactly one implementable ticket key:\n\n ```json\n {\"created_ticket_keys\": [\"BAPI-331\"]}\n ```\n\n- **Epic path** (`scope` is `epic_candidate`): emit the Epic parent key separately as `epic_parent_key`, and the implementable children as `child_ticket_keys`:\n\n ```json\n {\"epic_parent_key\": \"BAPI-400\", \"child_ticket_keys\": [\"BAPI-401\", \"BAPI-402\"]}\n ```\n\n `child_ticket_keys` contains **only** implementable child Task/Spike ticket keys, listed in final decomposition order. `child_ticket_keys` must **never** include the Epic parent key.\n",
|
|
1024
1024
|
"upload-epic-hierarchy.md": "Standalone Epic upload protocol. Use as the detailed reference for the Epic path triggered from `upload-and-track.md`.\n\n## Inputs\n\n- Run manifest: `{docs_dir}/idea-to-ticket/{slug}-{run_id}/run-manifest.json` with `scope == \"epic_candidate\"`.\n- Draft metadata: `{docs_dir}/idea-to-ticket/{slug}-{run_id}/draft-metadata.json` with a populated `parent` and `children`.\n- Decomposition plan: `{docs_dir}/idea-to-ticket/{slug}-{run_id}/decomposition-plan.json`.\n- Pipeline variable `auto_approve_external` governs the external-mutation pause as in `upload-and-track.md` (for this run, `auto_approve_external` = `{auto_approve_external}`).\n\n## Instructions\n\n1. Parent idempotency lookup. Search Jira via `get_tickets` for issues carrying the label `bapi-idea-to-ticket-{run_id}-parent`. If a match exists, reuse that ticket key as the Epic parent and skip `create_ticket` for the parent. Otherwise call `create_ticket` with the parent's summary, slim description, `issue_type = \"Epic\"`, and labels including `ai-generated`, `idea-to-ticket`, and `bapi-idea-to-ticket-{run_id}-parent`. After creation or reuse, upload the Epic draft via `attachment` (operation: `\"upload\"`) and call `track_ticket`.\n\n2. Capture the resolved Epic key into a local variable `epic_key`. Every subsequent child mutation must reference this exact key.\n\n3. Per-child idempotency lookup. For each child in `decomposition-plan.json` (in order), search Jira by the child's `idempotency_label` (`bapi-idea-to-ticket-{run_id}-child-<N>`). If a match exists, reuse that key and skip `create_ticket` for that child. Otherwise call `create_ticket(parent_key=<epic_key>)` with:\n - `summary` — child summary.\n - `slim_description` — child slim description.\n - `issue_type` — typically `Task` (or `Spike` when the child is primarily discovery).\n - `labels` — `ai-generated`, `idea-to-ticket`, `idea-to-ticket-child`, and the child's own `bapi-idea-to-ticket-{run_id}-child-<N>` label.\n The `parent_key` argument is REQUIRED for every child `create_ticket` call so Jira sets the modern parent relationship; never omit it.\n\n4. After each child is created or reused, upload its draft via `attachment` (operation: `\"upload\"`) using the child's `draft_path`, then call `track_ticket` for that child key, then append the child outcome to `upload-state.json` in the run directory.\n\n5. On partial failure (e.g., parent succeeded, third child failed), preserve `epic_key` plus every completed child key in `upload-state.json`. The next run of this protocol must rediscover those keys via the idempotency-label lookups in steps 1 and 3 before considering any new `create_ticket` call.\n\n## Return\n\nConfirm the Epic key, the number of children created vs reused vs failed, and the path of the updated `upload-state.json`.\n",
|
|
1025
1025
|
"verify-plan.md": "Close the remaining plan gaps for ticket {ticket_key}, now that the pull request is open.\n\nThe plan's work has already been executed. The production phase authored the\nartifacts, the checkpoint pushed them, and the pre-PR verification phase ran the\nplan's review steps, test commands, and rendered-UI remediation — publishing each\nmaterial correction as it went. The pull request was then opened on top of all of it.\n\nThis phase exists for the narrow remainder: the plan obligations that genuinely could\n**not** be reached before a pull request existed, plus corrections attributable to this\nticket.\n\nTwo consequences follow, and both are deliberate:\n\n- **This phase does not re-run completed work.** A step the durable ledger records as\n `executed` or `adapted` stays settled unless a later correction invalidated its\n evidence. Re-running it duplicates work the pre-PR phase already did and burns the\n budget this protocol was reordered to protect.\n- **This phase never issues a verdict.** You report what you observed. The\n authoritative pass/fail belongs to the pipeline's `ci` and `code_review` gates,\n which the reconciler observes independently. Worker self-verification has\n demonstrably reported green while the full suite was red; that is exactly why the\n gates, not this phase, decide.\n\n**Tool and scope boundary.** Use only the tools this recipe names, the repo's own tooling, and MCP\ncapabilities provisioned for this repository. Keep all work confined to this worktree unless\nexplicitly told to do otherwise.\n\n**Execution mode for this run: `{execution_mode}`.** Under `orchestrated`\n(`run_pipeline` / `resume_pipeline`) orchestration appends the routed phase context and\nparses the fenced result envelope you return. Under `inline` (`get_pipeline_recipe`)\nthere is no orchestrator: the ledger is read with a tool call and written with one.\nFollow the branch that matches wherever the two are named.\n\n---\n\n## Step 1 — Establish that the durable artifact exists\n\nBefore running any check:\n\n1. Run `git branch --show-current` and `git rev-parse HEAD`, then verify the branch\n has been pushed and the local head is present on the remote (for example via\n `git status -sb` showing no unpushed ahead-count, or `git ls-remote origin <branch>`).\n2. Verify that a usable pull request URL was obtained by the preceding PR step —\n either a newly opened pull request or an already-open one on this head branch.\n\nIf the branch is not pushed, or no usable pull request URL exists, then\n**stop this phase** and report the missing prerequisite. This phase exists only to\nadd work on top of an open pull request.\n\n## Step 2 — Recover what remains from durable state\n\n1. Call the `get_plan` tool for `{ticket_key}`. The local copy at\n `{docs_dir}/plans/{ticket_key}-plan.md` may be used as a reference.\n2. Recover the durable ledger, by mode:\n - **orchestrated** — read the **Routed phase context** block appended to this\n instruction: `ledger` carries every disposition earlier phases recorded, and\n `ownedSteps` carries anything routed directly to this phase.\n - **inline** — call `get_phase_context` with `ticket_key` `{ticket_key}` and\n `phase` `post_pr_gap_close`. It returns the same `ownedSteps` plus the `ledger`\n merged from every earlier phase's artifact, `terminalStepIds` for what is\n already settled, and `unresolved` for the escalations that are this phase's\n actual subject.\n\nRecover the remaining work from that durable record, not from conversation. A\ncompaction or a resumed session loses the conversation; the ledger survives both,\nwhich is why it replaced the conversational hand-off.\n\n## Step 3 — Select only genuine gaps\n\nRun only:\n\n- Steps the ledger records as `escalated` **because a capability was unavailable\n before the pull request existed**, or **because the check genuinely requires an open pull request** — and\n which are now satisfiable.\n- Corrections clearly attributable to this ticket's change.\n\nReport — do not attempt to fix — failures that are unrelated to this ticket, flaky,\nenvironmental, pre-existing on the base branch, or outside the declared file scope.\nSpeculative edits made under budget pressure are how a correction round turns into a\nregression.\n\nApply the same adaptation boundary the earlier phases use: **locator-correction**,\n**repository-command-correction**, and **equivalent-implementation-recognized** are\nmechanical and may be applied; anything touching design, schema, public API,\ndependencies, or security escalates instead.\n\nBefore beginning any correction, apply the **low-budget guard**: only start if enough\nsession budget clearly remains to make the edit, commit it, *and* push it. Starting a\nfix you cannot finish and publish is strictly worse than reporting the finding and\nletting the CI and review gates handle it — an unpushed correction is invisible to\nthose gates.\n\n## Step 4 — Report what you observed, honestly\n\nFor every check you run, record the exact command, its observed result, and — on\nfailure — the relevant failure detail (the failing test names, the error output, the\ndiagnostic lines).\n\nReport all of it, including failures you did not fix. Never soften or omit a failing\nresult.\n\nDescribe only what you observed. Do not write that CI passed, that the gate is met,\nthat the review is approved, or any equivalent claim about the pipeline's verdict —\nthose states are decided by the `ci` and `code_review` gates and observed by the\nreconciler, never asserted by this phase.\n\n## Step 5 — Correct only what is clearly yours, and push it immediately\n\nFor each accepted correction:\n\n1. Make the edit.\n2. Stage the specific files, commit, and **push immediately** — the commit and its\n push are one consecutive sequence, never separated by another check. A local\n commit that is never pushed is not visible to the pull request, to CI, or to the\n reconciler.\n3. Run `git rev-parse HEAD` again and record the new pushed head as\n `last_commit_sha`.\n\n## Step 6 — Final git-state audit\n\nBefore returning, run `git status --porcelain` and resolve the working tree:\n\n- Legitimate corrections still uncommitted → commit and push them (Step 5's\n commit-then-push-immediately rule applies).\n- Accidental diagnostic edits — debug prints, scratch files, temporary config\n tweaks made while investigating a failure → revert them when it is safe to do so.\n- Anything you cannot safely resolve → leave it and **report it explicitly**,\n naming each remaining dirty path.\n\nNever return leaving unpushed commits unreported.\n\n## Step 7 — Hand unresolved findings forward\n\nAn unresolved local finding is normally **not** a reason to stop the pipeline. The\npull request is open and the authoritative gates will evaluate it. Report the\nfinding and let CI monitoring and code review take it from there.\n\nStop only when continuing would be unsafe or impossible — for example the durable\nartifact from Step 1 turned out to be missing, or the working tree is in a state you\ncannot resolve without risking the pushed branch.\n\n## Return\n\nReturn a summary containing:\n\n- the branch and the pull request URL,\n- the latest pushed `last_commit_sha`,\n- every gap-closing command run, with its observed outcome,\n- the correction commit, if one was made and pushed,\n- every unresolved finding and every unresolved dirty path.\n\nState these as worker observations. Do not include a pass/fail verdict for the `ci`\nor `code_review` gates.\n\nThen record the machine-readable phase result, by mode, so the durable ledger records\nhow the remaining gaps closed.\n\n### orchestrated\n\nEnd your result with the envelope in a fenced block tagged `bapi-phase-result`, which\norchestration parses, validates, and persists:\n\n```bapi-phase-result\n{\"version\":1,\"phase\":\"post_pr_gap_close\",\"lastCommitSha\":\"<sha>\",\"records\":[]}\n```\n\n### inline\n\nCall the `record_phase_result` tool with `ticket_key` `{ticket_key}` and\n`phase_result` set to that same envelope object; the tool validates and persists it.\n**Do not also emit a fenced `bapi-phase-result` block** — nothing parses one on this\npath.\n\nThe tool call is this phase's final action, **not the end of your turn.** When it\nreturns successfully, continue immediately with the next recipe step — the ticket\nstatus transition, the CI follow-up config, and CI monitoring. The pull request is\nopen and its gates are still pending; stopping here abandons the run before anything\nobserves them.\n\nIf the call fails, fix what it reports and call it again.\n",
|
|
1026
|
-
"write-epic-summary.md": "Synthesize all sub-task explorations into a final overview document.\n\n## Instructions\n\n1. First, use a terminal command or glob pattern to list all files in `{docs_dir}/epic-plans/{epic_slug}/explorations/`. Then read each file. Do not guess filenames — discover them dynamically.\n\n2. Also read:\n - `{docs_dir}/epic-plans/{epic_slug}/research-findings.md`\n - `{docs_dir}/epic-plans/{epic_slug}/epic-plan.md`\n - `{docs_dir}/epic-plans/{epic_slug}/goals-and-nfrs.md` (the goals/NFR framing; carry its System Goals, NFRs, and any Recommended Implementation Order through to the overview).\n\n3. Synthesize the information into an overview and write it to `{docs_dir}/epic-plans/{epic_slug}/overview.md` with the following required sections:\n\n```markdown\n# Epic Overview: {epic title derived from description}\n\n## Epic Description and Goals\n{Summary of the epic's purpose, scope, and desired outcomes. Lead with the business goal and desired end-state from goals-and-nfrs.md.}\n\n## Non-Functional Requirements\n{The classified NFRs from goals-and-nfrs.md — each with its category, requirement, implication, and final status (confirmed/assumed). Any NFRs the user clarified should now read as confirmed/assumed, not open.}\n\n## Research Summary\n{Key external findings that informed the decomposition. If no research was performed, state \"No external research was needed.\"}\n\n## Sub-task List\n{Numbered list of all sub-tasks with relative markdown links to their exploration docs.}\n1. [Sub-task title](explorations/01-subtask-slug.md) — one-line summary\n2. [Sub-task title](explorations/02-subtask-slug.md) — one-line summary\n...\n\n## Dependency Graph\n{Textual list showing execution ordering and dependencies between sub-tasks.}\n- Sub-task 1: No dependencies (start here)\n- Sub-task 2: Depends on Sub-task 1\n- Sub-task 3: Depends on Sub-task 1\n- Sub-task 4: Depends on Sub-tasks 2, 3\n...\n\n## Recommended Implementation Order\n{The recommended order in which to implement the sub-tasks, reconciling the provisional order from goals-and-nfrs.md with the approved decomposition. For each sub-task give the position, its hard prerequisites (depends on), any soft sequencing preferences (recommended after), and a one-line rationale. This is recommended sequencing only — no Jira dependency links are created.}\n\n## Next Steps\n{One-line summaries for each sub-task, specifically formatted so they can be
|
|
1026
|
+
"write-epic-summary.md": "Synthesize all sub-task explorations into a final overview document.\n\n## Instructions\n\n1. First, use a terminal command or glob pattern to list all files in `{docs_dir}/epic-plans/{epic_slug}/explorations/`. Then read each file. Do not guess filenames — discover them dynamically.\n\n2. Also read:\n - `{docs_dir}/epic-plans/{epic_slug}/research-findings.md`\n - `{docs_dir}/epic-plans/{epic_slug}/epic-plan.md`\n - `{docs_dir}/epic-plans/{epic_slug}/goals-and-nfrs.md` (the goals/NFR framing; carry its System Goals, NFRs, and any Recommended Implementation Order through to the overview).\n\n3. Synthesize the information into an overview and write it to `{docs_dir}/epic-plans/{epic_slug}/overview.md` with the following required sections:\n\n```markdown\n# Epic Overview: {epic title derived from description}\n\n## Epic Description and Goals\n{Summary of the epic's purpose, scope, and desired outcomes. Lead with the business goal and desired end-state from goals-and-nfrs.md.}\n\n## Non-Functional Requirements\n{The classified NFRs from goals-and-nfrs.md — each with its category, requirement, implication, and final status (confirmed/assumed). Any NFRs the user clarified should now read as confirmed/assumed, not open.}\n\n## Research Summary\n{Key external findings that informed the decomposition. If no research was performed, state \"No external research was needed.\"}\n\n## Sub-task List\n{Numbered list of all sub-tasks with relative markdown links to their exploration docs.}\n1. [Sub-task title](explorations/01-subtask-slug.md) — one-line summary\n2. [Sub-task title](explorations/02-subtask-slug.md) — one-line summary\n...\n\n## Dependency Graph\n{Textual list showing execution ordering and dependencies between sub-tasks.}\n- Sub-task 1: No dependencies (start here)\n- Sub-task 2: Depends on Sub-task 1\n- Sub-task 3: Depends on Sub-task 1\n- Sub-task 4: Depends on Sub-tasks 2, 3\n...\n\n## Recommended Implementation Order\n{The recommended order in which to implement the sub-tasks, reconciling the provisional order from goals-and-nfrs.md with the approved decomposition. For each sub-task give the position, its hard prerequisites (depends on), any soft sequencing preferences (recommended after), and a one-line rationale. This is recommended sequencing only — no Jira dependency links are created.}\n\n## Next Steps\n{One-line summaries for each sub-task, specifically formatted so they can be handed directly to the Jira Ticket Writer / ticket-authoring workflow as input. Each line should be a self-contained ticket description.}\n```\n\n4. After writing the overview, display the file path to the user and summarize the epic plan.\n\n5. **Push the goals/NFRs + recommended order into the Jira epic (only when `{epic_key}` is non-empty).** The `epic_key` is empty when this run was started from free-form text rather than an existing Epic; in that case skip this step. When `{epic_key}` is a real Jira key, post the System Goals, the final NFRs, and the Recommended Implementation Order as a **comment** on that epic by calling the `add_comment` MCP tool with `ticket_number` set to `{epic_key}` and a concise comment containing those three parts. Do not create Jira dependency links and do not attach a separate markdown doc — the comment is the delivery. Display: `\"Posted epic goals/NFRs and recommended implementation order to {epic_key}\"`.\n\n## Return\n\nConfirm the overview was written to `{docs_dir}/epic-plans/{epic_slug}/overview.md` and report the total sub-task count along with a one-line summary of the epic plan. State whether the goals/NFRs + recommended order were posted as a comment on `{epic_key}` or skipped because no epic key was provided.\n"
|
|
1027
1027
|
};
|