@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
@@ -0,0 +1,342 @@
1
+ /**
2
+ * Worktree MCP registration provisioning.
3
+ *
4
+ * After `start-tickets` creates a worktree, this module writes secret-free MCP
5
+ * registrations into both Claude (`.mcp.json`) and Cursor (`.cursor/mcp.json`)
6
+ * so the configured MCP servers are reachable from either editor. Every
7
+ * generated entry contains NO `env` block — it points at the `mcp-invoke` shim
8
+ * with an absolute `--project-root` and the target name; credentials are
9
+ * resolved at runtime by the shim, never written into the worktree.
10
+ *
11
+ * Registrations are driven by `.bridge/config`: the `bapi` target is always
12
+ * provisioned when present, and every supported Tier-2 target (e.g. `sfcc`) is
13
+ * provisioned as its own shim entry. Unsupported or incomplete non-`bapi`
14
+ * targets produce a non-fatal, secret-free warning.
15
+ *
16
+ * All filesystem access is dependency-injected so this is unit-testable, and the
17
+ * platform path API is resolved locally (never imported from `start-tickets.ts`)
18
+ * to avoid a runtime import cycle.
19
+ */
20
+ import path from "path";
21
+ import { VERSION } from "./version.generated.js";
22
+ import { readBridgeConfig } from "./bridge-config.js";
23
+ import { getThirdPartyTargetDefinition, validateThirdPartyTargetManifestEntry, } from "./third-party-mcp-targets.js";
24
+ // ---------------------------------------------------------------------------
25
+ // Pure helpers
26
+ // ---------------------------------------------------------------------------
27
+ /**
28
+ * Resolve the path API for the target platform. Local (not imported from
29
+ * `start-tickets.ts`) to avoid a runtime cycle, since that module imports the
30
+ * provisioning entry point.
31
+ */
32
+ export function pathApiForProvisioningPlatform(platform) {
33
+ return platform === "win32" ? path.win32 : path.posix;
34
+ }
35
+ /**
36
+ * Normalize a worktree path to an absolute, platform-correct registration path.
37
+ * Absolute inputs are normalized; relative inputs are resolved against
38
+ * `deps.cwd`. A path that cannot be made absolute is a structured error.
39
+ */
40
+ export function normalizeWorktreePathForRegistration(worktreePath, deps) {
41
+ const api = pathApiForProvisioningPlatform(deps.platform);
42
+ if (typeof worktreePath !== "string" || worktreePath.trim().length === 0) {
43
+ return { ok: false, error: "worktree path is empty" };
44
+ }
45
+ const resolved = api.isAbsolute(worktreePath)
46
+ ? api.normalize(worktreePath)
47
+ : api.resolve(deps.cwd, worktreePath);
48
+ if (!api.isAbsolute(resolved)) {
49
+ return { ok: false, error: `unable to resolve an absolute worktree path from "${worktreePath}"` };
50
+ }
51
+ return { ok: true, path: resolved };
52
+ }
53
+ /** Map an MCP target to its registration server name (`bapi` -> `bridge-api`). */
54
+ export function serverNameForMcpTarget(target) {
55
+ return target === "bapi" ? "bridge-api" : target;
56
+ }
57
+ /**
58
+ * Build a secret-free shim entry for any target. `args` are version-pinned and
59
+ * carry the target name plus the absolute project root; there is intentionally
60
+ * no `env` block.
61
+ */
62
+ export function buildShimMcpServerEntry(target, absoluteWorktreePath) {
63
+ return {
64
+ command: "npx",
65
+ args: [
66
+ "-y",
67
+ `@bridge_gpt/mcp-server@${VERSION}`,
68
+ "mcp-invoke",
69
+ "--target",
70
+ target,
71
+ "--project-root",
72
+ absoluteWorktreePath,
73
+ ],
74
+ };
75
+ }
76
+ /** Back-compat wrapper around `buildShimMcpServerEntry("bapi", ...)`. */
77
+ export function buildBridgeApiShimMcpServerEntry(absoluteWorktreePath) {
78
+ return buildShimMcpServerEntry("bapi", absoluteWorktreePath);
79
+ }
80
+ /**
81
+ * Convert all supported manifest `mcp` entries into a map of server name -> shim
82
+ * entry. `bapi` is always supported; a Tier-2 target is supported only when it
83
+ * has a known target definition AND its manifest entry is complete. Unsupported
84
+ * or incomplete non-`bapi` targets are skipped with a secret-free warning (they
85
+ * never appear in the returned entries).
86
+ */
87
+ export function buildMcpServerEntriesForManifest(manifest, absoluteWorktreePath) {
88
+ const entries = {};
89
+ const warnings = [];
90
+ for (const mcp of manifest.mcp) {
91
+ if (mcp.target === "bapi") {
92
+ entries[serverNameForMcpTarget("bapi")] = buildShimMcpServerEntry("bapi", absoluteWorktreePath);
93
+ continue;
94
+ }
95
+ const definition = getThirdPartyTargetDefinition(mcp.target);
96
+ if (!definition) {
97
+ warnings.push(`MCP target '${mcp.target}' is not a supported third-party target; skipping its registration.`);
98
+ continue;
99
+ }
100
+ const validation = validateThirdPartyTargetManifestEntry(mcp);
101
+ if (!validation.ok) {
102
+ warnings.push(`MCP target '${mcp.target}' registration skipped: ${validation.error}.`);
103
+ continue;
104
+ }
105
+ entries[serverNameForMcpTarget(mcp.target)] = buildShimMcpServerEntry(mcp.target, absoluteWorktreePath);
106
+ }
107
+ return { entries, warnings };
108
+ }
109
+ /** Both registration files written for every provisioned worktree. */
110
+ export function getWorktreeMcpRegistrationTargets(worktreePath, platform) {
111
+ const api = pathApiForProvisioningPlatform(platform);
112
+ return [
113
+ { filePath: api.join(worktreePath, ".mcp.json"), topLevelKey: "mcpServers" },
114
+ { filePath: api.join(worktreePath, ".cursor", "mcp.json"), topLevelKey: "mcpServers" },
115
+ ];
116
+ }
117
+ /**
118
+ * Absolute path to the worktree's Claude local settings file
119
+ * (`.claude/settings.local.json`). This is where the server-trust pre-approval
120
+ * (`enabledMcpjsonServers`) is written so the just-registered `.mcp.json`
121
+ * servers don't trigger Claude Code's "use this MCP server?" prompt in the
122
+ * fresh worktree path. Deliberately NOT part of
123
+ * `getWorktreeMcpRegistrationTargets` — it is Claude-only and uses a different
124
+ * top-level key than an MCP registration.
125
+ */
126
+ export function claudeSettingsTargetForWorktree(worktreePath, platform) {
127
+ const api = pathApiForProvisioningPlatform(platform);
128
+ return api.join(worktreePath, ".claude", "settings.local.json");
129
+ }
130
+ /**
131
+ * Merge the given server names into an existing parsed settings document's
132
+ * `enabledMcpjsonServers` array. Unrelated top-level fields are preserved; the
133
+ * resulting array is the deduped union of any existing names plus the new ones,
134
+ * in stable order (existing names first, then newly-added names).
135
+ */
136
+ export function mergeEnabledMcpjsonServers(existing, serverNames) {
137
+ const result = existing && typeof existing === "object" && !Array.isArray(existing)
138
+ ? { ...existing }
139
+ : {};
140
+ const current = result.enabledMcpjsonServers;
141
+ const merged = Array.isArray(current)
142
+ ? current.filter((name) => typeof name === "string")
143
+ : [];
144
+ for (const name of serverNames) {
145
+ if (!merged.includes(name))
146
+ merged.push(name);
147
+ }
148
+ result.enabledMcpjsonServers = merged;
149
+ return result;
150
+ }
151
+ /**
152
+ * Merge multiple shim entries into an existing parsed registration document.
153
+ * Unrelated top-level fields and unrelated MCP servers are preserved; only the
154
+ * generated server names are replaced. Any legacy secret-embedded entry for a
155
+ * generated server name is force-upgraded to the secret-free shim shape.
156
+ */
157
+ export function mergeMcpRegistrations(existing, topLevelKey, entries) {
158
+ const result = existing && typeof existing === "object" && !Array.isArray(existing)
159
+ ? { ...existing }
160
+ : {};
161
+ const current = result[topLevelKey];
162
+ const servers = current && typeof current === "object" && !Array.isArray(current)
163
+ ? { ...current }
164
+ : {};
165
+ for (const [name, entry] of Object.entries(entries)) {
166
+ servers[name] = entry;
167
+ }
168
+ result[topLevelKey] = servers;
169
+ return result;
170
+ }
171
+ /**
172
+ * Back-compat single-entry merge. Sets only `mcpServers["bridge-api"]`; unrelated
173
+ * top-level fields and servers are preserved.
174
+ */
175
+ export function mergeBridgeApiMcpRegistration(existing, topLevelKey, entry) {
176
+ return mergeMcpRegistrations(existing, topLevelKey, { "bridge-api": entry });
177
+ }
178
+ // ---------------------------------------------------------------------------
179
+ // Filesystem writes
180
+ // ---------------------------------------------------------------------------
181
+ /**
182
+ * Write or merge a single registration file with the given entries. A missing
183
+ * file is created (with parent directories); an existing valid file is merged; an
184
+ * existing file with malformed JSON is left untouched and reported as a failure.
185
+ * JSON is written with two-space indentation and a trailing newline.
186
+ */
187
+ export async function writeMcpRegistrationFile(target, entries, deps) {
188
+ const api = pathApiForProvisioningPlatform(deps.platform);
189
+ let existing;
190
+ try {
191
+ const raw = await deps.readFile(target.filePath);
192
+ try {
193
+ existing = JSON.parse(raw);
194
+ }
195
+ catch {
196
+ return {
197
+ ok: false,
198
+ error: `existing ${target.filePath} contains malformed JSON; not overwriting`,
199
+ };
200
+ }
201
+ }
202
+ catch (err) {
203
+ const code = err && typeof err === "object" ? err.code : undefined;
204
+ if (code !== "ENOENT") {
205
+ return { ok: false, error: `unable to read ${target.filePath}` };
206
+ }
207
+ // ENOENT: create a fresh document below.
208
+ }
209
+ const merged = mergeMcpRegistrations(existing, target.topLevelKey, entries);
210
+ try {
211
+ await deps.mkdir(api.dirname(target.filePath), { recursive: true });
212
+ await deps.writeFile(target.filePath, `${JSON.stringify(merged, null, 2)}\n`);
213
+ }
214
+ catch {
215
+ return { ok: false, error: `failed to write ${target.filePath}` };
216
+ }
217
+ return { ok: true };
218
+ }
219
+ /**
220
+ * Write (or merge) the worktree's `.claude/settings.local.json` so the given
221
+ * `.mcp.json` server names are pre-approved via `enabledMcpjsonServers`. This
222
+ * suppresses Claude Code's per-project "use this MCP server?" trust prompt that
223
+ * would otherwise re-appear in every freshly-created worktree path.
224
+ *
225
+ * Mirrors `writeMcpRegistrationFile`'s read-merge-write contract, but is
226
+ * intentionally lenient: a missing file is created, an existing valid file is
227
+ * merged, and an existing file with malformed JSON is left untouched and
228
+ * reported as a failure so the caller can degrade to a warning (never a
229
+ * spawn-blocking error — trust pre-approval is convenience, not required).
230
+ */
231
+ export async function writeClaudeServerTrustSettings(worktreePath, serverNames, deps) {
232
+ const api = pathApiForProvisioningPlatform(deps.platform);
233
+ const filePath = claudeSettingsTargetForWorktree(worktreePath, deps.platform);
234
+ let existing;
235
+ try {
236
+ const raw = await deps.readFile(filePath);
237
+ try {
238
+ existing = JSON.parse(raw);
239
+ }
240
+ catch {
241
+ return {
242
+ ok: false,
243
+ error: `existing ${filePath} contains malformed JSON; not overwriting`,
244
+ };
245
+ }
246
+ }
247
+ catch (err) {
248
+ const code = err && typeof err === "object" ? err.code : undefined;
249
+ if (code !== "ENOENT") {
250
+ return { ok: false, error: `unable to read ${filePath}` };
251
+ }
252
+ // ENOENT: create a fresh document below.
253
+ }
254
+ const merged = mergeEnabledMcpjsonServers(existing, serverNames);
255
+ try {
256
+ await deps.mkdir(api.dirname(filePath), { recursive: true });
257
+ await deps.writeFile(filePath, `${JSON.stringify(merged, null, 2)}\n`);
258
+ }
259
+ catch {
260
+ return { ok: false, error: `failed to write ${filePath}` };
261
+ }
262
+ return { ok: true };
263
+ }
264
+ // ---------------------------------------------------------------------------
265
+ // Per-worktree orchestration
266
+ // ---------------------------------------------------------------------------
267
+ function withWarning(row, warning) {
268
+ return { ...row, warnings: [...(row.warnings ?? []), warning] };
269
+ }
270
+ function withWarnings(row, warnings) {
271
+ if (warnings.length === 0)
272
+ return row;
273
+ return { ...row, warnings: [...(row.warnings ?? []), ...warnings] };
274
+ }
275
+ /**
276
+ * Provision MCP registrations for one created worktree row.
277
+ *
278
+ * - Non-`created` rows and rows without a path are returned unchanged.
279
+ * - A missing / malformed manifest yields a non-fatal warning and leaves the row
280
+ * status unchanged.
281
+ * - A valid manifest writes BOTH Claude and Cursor registration files with a
282
+ * shim entry for every supported target (`bapi` + complete Tier-2 targets),
283
+ * regardless of the selected agent. Unsupported/incomplete non-`bapi` targets
284
+ * add a secret-free warning but never abort provisioning.
285
+ * - A required write failure (or a malformed existing registration file) marks
286
+ * only this row `spawn-failed` with a descriptive error; other rows continue.
287
+ * - After the registration files are written, the worktree's
288
+ * `.claude/settings.local.json` is updated to pre-approve those servers via
289
+ * `enabledMcpjsonServers` (suppressing Claude Code's per-project trust
290
+ * prompt). This step is best-effort: any failure degrades to a warning and
291
+ * never blocks the spawn.
292
+ */
293
+ export async function provisionMcpRegistrationForWorktree(row, deps) {
294
+ if (row.status !== "created" || !row.path) {
295
+ return row;
296
+ }
297
+ const read = await readBridgeConfig(row.path, { readFile: deps.readFile });
298
+ if (!read.ok) {
299
+ if (read.kind === "missing") {
300
+ return withWarning(row, "MCP provisioning skipped: .bridge/config is missing in the worktree.");
301
+ }
302
+ return withWarning(row, "MCP provisioning skipped: .bridge/config is malformed or invalid.");
303
+ }
304
+ const normalized = normalizeWorktreePathForRegistration(row.path, deps);
305
+ if (!normalized.ok) {
306
+ return { ...row, status: "spawn-failed", error: `MCP provisioning failed: ${normalized.error}` };
307
+ }
308
+ const built = buildMcpServerEntriesForManifest(read.manifest, normalized.path);
309
+ if (Object.keys(built.entries).length === 0) {
310
+ // Nothing supported to write (e.g. a manifest with no bapi target and no
311
+ // supported Tier-2 targets). Surface any warnings but leave status unchanged.
312
+ return withWarnings(withWarning(row, "MCP registration skipped: .bridge/config declares no supported MCP targets."), built.warnings);
313
+ }
314
+ const targets = getWorktreeMcpRegistrationTargets(normalized.path, deps.platform);
315
+ for (const target of targets) {
316
+ const result = await writeMcpRegistrationFile(target, built.entries, deps);
317
+ if (!result.ok) {
318
+ return { ...row, status: "spawn-failed", error: `MCP provisioning failed: ${result.error}` };
319
+ }
320
+ }
321
+ // Pre-approve the just-registered servers so Claude Code does not prompt to
322
+ // trust them in this fresh worktree path. Best-effort: a failure here only
323
+ // loses the convenience of skipping the prompt, so warn and still spawn.
324
+ let result = withWarnings(row, built.warnings);
325
+ const serverNames = Object.keys(built.entries);
326
+ const trust = await writeClaudeServerTrustSettings(normalized.path, serverNames, deps);
327
+ if (!trust.ok) {
328
+ result = withWarning(result, `Claude MCP trust pre-approval skipped: ${trust.error}`);
329
+ }
330
+ return result;
331
+ }
332
+ /**
333
+ * Provision MCP registrations for every created worktree row, in order.
334
+ * Per-row failures are isolated — a failed row never aborts later rows.
335
+ */
336
+ export async function provisionMcpRegistrationsForCreatedWorktrees(rows, deps) {
337
+ const out = [];
338
+ for (const row of rows) {
339
+ out.push(await provisionMcpRegistrationForWorktree(row, deps));
340
+ }
341
+ return out;
342
+ }
@@ -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
+ }
@@ -348,7 +348,13 @@ export async function resumePipeline(deps, input) {
348
348
  updated = await persistence.patchRun(row.pipeline_run_id, {
349
349
  current_step_index: nextIndex,
350
350
  results: accumulatedResults,
351
- 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",
352
358
  expected_status: "paused",
353
359
  expected_current_step_index: stepIndex,
354
360
  });
@@ -640,6 +646,8 @@ async function executeMcpCallStep(deps, step) {
640
646
  error: err instanceof Error ? err.message : String(err),
641
647
  };
642
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.
643
651
  const text = toolResult?.content?.[0]?.type === "text"
644
652
  ? toolResult.content[0].text
645
653
  : "";
@@ -588,13 +588,15 @@ export const CHAIN_RECIPES = {
588
588
  "auto_approve_external": "{auto_approve}"
589
589
  },
590
590
  "outputs": {
591
+ "child_ticket_keys": "child_ticket_keys",
592
+ "epic_parent_key": "epic_parent_key",
591
593
  "created_ticket_keys": "created_ticket_keys"
592
594
  }
593
595
  },
594
596
  {
595
597
  "pipeline_name": "review-ticket",
596
598
  "description": "Review each created ticket (one child pipeline run per ticket).",
597
- "fan_out_input": "created_ticket_keys",
599
+ "fan_out_input": "child_ticket_keys",
598
600
  "fan_out_variable": "ticket_key",
599
601
  "variables": {
600
602
  "ticket_key": "{ticket_key}"
@@ -619,7 +621,7 @@ export const INSTRUCTIONS = {
619
621
  "capture-review-decisions.md": "Capture user decisions on review findings for {ticket_key} using the HTML decision page, then interpretively rewrite the clarifying questions and critique docs and upload both to Jira.\n\n## Step 1: Read source documents\n\nRead the combined review-and-resolution file:\n- `{docs_dir}/review/{ticket_key}-review-and-resolution.md`\n\nIf the file does not exist or is unreadable, stop and report: \"Combined review-and-resolution file not found or unreadable. Run the earlier pipeline steps first.\"\n\nThe combined file existing but containing no actionable items (empty `Needs Scrutiny` and `Open Questions` sections) is **not** a failure condition — Step 4 handles the no-decisions-needed flow gracefully when `generate_decision_page` is called with empty `actionable_items`.\n\n## Step 2: Map evaluation items to decision page input\n\nTransform the combined review-and-resolution document into `generate_decision_page` JSON input using these mapping rules:\n\n| Evaluation Section | JSON Field | Mapping Rule |\n|---|---|---|\n| Open Questions | `actionable_items` | E-item title → `question`, `**Source**` → `source`, `**Original question**` → `original_question`, `**Why it matters**` → `why_it_matters`, decision tree branch labels → `options` (string array, labels only), `**Option consequences**` (parallel to branches) → `option_consequences`, `**Recommendation explanation**` → `recommendation_explanation`, combined `**Assessment**` paragraph and `**Codebase Evidence**` bullet list → `codebase_evidence`, `**Recommendation Index**` → `recommendation_index` |\n| Needs Scrutiny | `actionable_items` | E-item title → `question`, `**Source**` → `source`, `**Original question**` → `original_question`, `**Why it matters**` → `why_it_matters`, decision tree branch labels → `options` (string array, labels only), `**Option consequences**` (parallel to branches) → `option_consequences`, `**Recommendation explanation**` → `recommendation_explanation`, combined `**Assessment**` paragraph and `**Codebase Evidence**` bullet list → `codebase_evidence`, `**Recommendation Index**` → `recommendation_index` |\n| Confirmed Improvements | `clear_improvements` | E-item title → `title`, confidence tag → `confidence`, recommended action → `action`, `**Source**` from the combined file → `source` |\n\n**Important**: The `original_question`, `why_it_matters`, `option_consequences`, `recommendation_explanation`, and the collapsed `codebase_evidence` block together replace the old single `context` blob. Each clarity field guides a different facet of the user's decision: `original_question` reminds the reviewer what was asked, `why_it_matters` frames the impact, `option_consequences` describe the behavioral outcome of each branch, `recommendation_explanation` motivates the recommended branch, and the closed-by-default `codebase_evidence` block surfaces the Assessment + file:line citations on demand without overwhelming the card.\n\nFor each actionable item, the `options` array is a list of plain label strings extracted from the combined file's decision tree branches. The tool auto-generates value keys (`opt-0`, `opt-1`, etc.) and auto-appends a \"None of these\" option. Do not generate value keys yourself.\n\n## Step 2.5: Auto-approve fast path\n\nFor this run, `auto_approve` = `{auto_approve}`.\n\nIf `auto_approve` is `true` and Step 2 produced at least one actionable item, skip Steps 3–6 entirely and synthesize the commit JSON directly:\n\n- `ticket_key`: `{ticket_key}`\n- `general_comment`: `\"\"`\n- `decisions`: an object keyed by each `actionable_items[*].id` from Step 2's mapped input. For each item:\n - If `recommendation_index` is a non-negative integer within range of `options`: `choice = \"opt-\" + recommendation_index`, `chosen_label = options[recommendation_index]`, `comment = \"\"`, `source` copied from the item.\n - Otherwise (missing, null, or out of range): `choice = \"opt-0\"`, `chosen_label = options[0]`, `comment = \"\"`, `source` copied. Never emit `\"none\"` and never emit `\"ask\"`.\n\nPost a single chat acknowledgement listing each auto-approved item ID and chosen label, then proceed directly to Step 7 with the synthesized JSON. Step 7's \"Hard rule\" about resolving `ask` items does not apply because no item carries `choice === \"ask\"`.\n\nIf Step 2 produced zero actionable items, fall through to Step 3 — Step 4's existing `no_decisions_needed` branch handles the empty case correctly.\n\nOtherwise (any value of `auto_approve` other than the literal `true` — including empty, `false`, or missing), proceed to Step 3.\n\n## Step 3: Call the MCP tool\n\nCall `generate_decision_page` with:\n- `ticket_key`: `{ticket_key}`\n- `actionable_items`: combined mapped array from Step 2 (each item has `id`, `question`, `original_question`, `why_it_matters`, `options`, `option_consequences`, `recommendation_explanation`, `codebase_evidence`, `source`, and `recommendation_index`)\n- `clear_improvements`: mapped array from Step 2 (each item has `id`, `title`, `action`, `confidence`, `source`)\n\n## Step 4: Check tool response\n\nThe tool returns a JSON response with a `status` field:\n- If `status` is `\"no_decisions_needed\"`: skip Steps 5, 6, 7, and 8 entirely. Output a success message: \"No actionable review decisions needed — skipping doc rewrite and upload.\" This covers both the case where every item was confirmed as a Confirmed Improvement and the case where no items were emitted (e.g., both upstream source documents were absent).\n- If `status` is `\"decision_page_generated\"`: continue to Step 5. The response includes `file_path`.\n\n## Step 5: Direct user to the decision page\n\nTell the user to open the generated HTML file in their browser. Provide the `file_path` from the tool response. Then say to the user, verbatim: `Open the page. For any item you're unsure about, choose \"Ask about this\" — when you submit, I'll talk through those before we proceed. You can also ask me questions in chat before submitting if you prefer.`\n\nThis step only directs the user to the page and explains the two allowed next actions (submit selections, or ask questions first). Do not describe Step 7's rewrite semantics here; that belongs to the rewrite step.\n\n## Step 6: Q&A loop and commit signal\n\nEnter an open-ended Q&A loop. There is no turn cap — the user may ask any number of questions in any number of turns. Do not stop and wait silently; engage with each user message as either a commit signal or a discussion turn.\n\n### Proceed signal (commit)\n\nTrim the full user message and attempt to parse the entire trimmed message as JSON. The message is a commit only when the parsed value is an object with all three of these top-level fields:\n\n- `ticket_key` — must be a string\n- `decisions` — must be an object\n- `general_comment` — must be a string\n\nThe first valid commit-shaped JSON paste commits immediately. Proceed to Step 7 without prompting for additional confirmation. Any combination of `decisions` keys is accepted (the page may submit a partial set if the user only resolved some items conversationally). Do not over-validate the per-card fields beyond the top-level commit-shape check — the page guarantees the per-card schema, and over-validating risks rejecting valid pastes if the page schema evolves.\n\n### Discussion signal (Q&A turn)\n\nAnything that is not commit-shaped JSON is a discussion turn. This includes:\n\n- Freeform questions (with or without other text).\n- Questions pasted alongside other text or alongside JSON.\n- Malformed JSON (parse failure).\n- Well-formed JSON missing one or more of the required top-level keys (`ticket_key`, `decisions`, `general_comment`).\n\nFor JSON-shaped input that is missing required top-level fields, call this out in the reply — explain which fields are missing and ask whether the user intended to submit or share partial state — rather than silently treating it as a freeform question.\n\nAnswer discussion turns using these sources, in priority order:\n\n1. The combined `{ticket_key}-review-and-resolution.md` file already read in Step 1.\n2. The original `{ticket_key}-clarifying-questions.md` and `{ticket_key}-ticket-quality-critique.md` documents.\n3. Codebase lookups when the question requires verifying current code state.\n\nFallback: if running on a pre-PR1 branch where the combined review-and-resolution document does not exist, use the pre-PR1 `{ticket_key}-review-evaluation.md` and `{ticket_key}-resolution-guide.md` pair in its place.\n\nFor plain freeform questions, infer the item from chat context when possible.\n\n### In-flight decision state\n\nDuring the Q&A loop, maintain in-flight JSON state — agent-owned working memory representing the user's current intent for `decisions` and `general_comment`. This in-flight JSON state lives only in the agent's working memory for the duration of the loop; do not persist it server-side.\n\n- When the user clearly changes their mind about an item, chooses an option conversationally with reasonably explicit decision language (\"choose option B for E-3\", \"go with the configurable timeout\", \"change E-7 to None of these\"), or gives new overarching guidance, record that as an in-flight override.\n- Ambiguous preference language (\"I'm leaning toward...\", \"maybe option B is fine\") should be discussed but not recorded as an override unless the user gives reasonably explicit decision language.\n- `general_comment` may be updated in the in-flight state when the user gives overarching guidance during Q&A.\n- The page's general-comment textarea is preserved unchanged. Do not modify the page DOM during Q&A; the user can still fill the textarea before submitting if they prefer.\n\nOn the eventual JSON commit, the user-submitted JSON is the baseline and the recorded in-flight overrides take precedence over it. Before proceeding to Step 7, post a brief one-line acknowledgement in chat naming each overridden item ID and/or `general_comment`. The acknowledgement is mandatory (not optional) — it is the user's last chance to object before Step 7's document rewrite. The user does not need to re-open, edit, or re-submit the decision page after changing their mind in chat; they can submit the page as-is to provide the commit signal, and the in-flight state remains the source of truth for overrides.\n\n### Ask-about-this resolution\n\nAfter accepting a commit, scan `decisions` for any item where `choice === \"ask\"`. The user has signaled that they need more information before deciding on those items. For each such item:\n\n- If `comment` is non-empty, treat it as the user's specific question or stated uncertainty and answer that directly.\n- If `comment` is empty, proactively present the most relevant missing context — the item's `codebase_evidence`, related code lookups, prior-round answers — and lay out the trade-offs the user appears to need help weighing.\n- Continue the Q&A turn-by-turn until the user gives an explicit decision in chat for that item (\"go with option B\", \"none of these, because …\"). Record that decision as an in-flight override using the same override mechanism described above.\n\n**Hard rule.** Step 7 must not run while any `decisions[*].choice === \"ask\"` remains unresolved by an in-flight override. Do not honor \"just proceed\", \"skip those\", or any other instruction to defer resolution — every `ask` item must end with a recorded `opt-N` or `none` override before the rewrite step. The pre-Step-7 acknowledgement line lists every overridden item, including the ones resolved out of `ask`.\n\n## Step 7: Interpretively rewrite source documents\n\nThe pasted JSON contains a `decisions` object keyed by item ID. Each decision includes `source`, `choice`, `chosen_label`, and `comment`. Use these fields to locate and rewrite the corresponding sections in:\n- `{docs_dir}/clarifying-questions/{ticket_key}-clarifying-questions.md`\n- `{docs_dir}/ticket-critiques/{ticket_key}-ticket-quality-critique.md`\n\nAfter a second-opinion run, each document has this shape:\n\n- A top-level H1 (`# Ticket Analysis` or `# Ticket Quality Critique`) followed by an italic provider-attribution line `_This analysis was generated by GPT|Claude|Gemini._` naming the first-round LLM family. **Preserve this attribution line verbatim** — do not move, edit, or remove it during the rewrite step.\n- The first-round questions / critique items, exactly as written by the first-round model.\n- **Inline second-opinion blockquotes** (`> **Second opinion (<provider>) - concurrence|refinement|disagreement.** ... > *Citations: ...*`) nested directly under each prior item the second round addressed. The `(<provider>)` parenthetical is the second-round LLM family (`GPT|Claude|Gemini`). Items the second round did not comment on have no blockquote — that is the \"weak concurrence\" signal.\n- A **`## New in Second Opinion`** tail block listing items the second round added on top of the first round. Immediately under the H2 there is a second italic attribution line `_These additional points were raised by GPT|Claude|Gemini._` naming the second-round family — **also preserve this verbatim**. Then agent-specific sub-headings:\n - Clarifier docs: `### New Requirements Questions` / `### New Technical Questions` (numbering continues from the prior section).\n - Critique docs: `### New Requested Changes` / `### New Points to Consider` (numbering continues from the prior section).\n- A final **`## Second Opinion Summary`** footer (1-3 sentences). **This footer must be preserved verbatim** — it is the canonical record of the second round's overall position and should not be edited.\n\nThe `source` field on each decision tells you where the item lives:\n\n- `Clarifying Q3 (prior round, weak concurrence)` → the prior section, no inline blockquote. Rewrite the prior item's answer.\n- `Clarifying Q9 (prior round, concurrence inline)` → the prior section, prior item carries an explicit `concurrence` blockquote. Rewrite the prior answer; the blockquote can be removed once the answer absorbs the resolution.\n- `Clarifying Q3 (prior round, refinement inline)` / `(prior round, disagreement inline)` → the prior section, prior item carries an explicit `refinement` or `disagreement` blockquote. Rewrite the prior answer to reconcile the dispute, then handle the blockquote per the rule below.\n- `Clarifying Q11 (new in second opinion → New Requirements Questions)` → the `## New in Second Opinion > ### New Requirements Questions` sub-section. Rewrite the item in place inside that sub-section, not at the top of the prior analysis.\n- Equivalent forms for critique items: `Critique: Requested Change 2 (prior round, refinement inline)`, `Critique: Points to Consider N+1 (new in second opinion → New Points to Consider)`, etc.\n\n**Legacy fallback shape**: if the document instead ends with `\\n\\n---\\n\\n` followed by a `## Second Opinion` section (because the JSON pipeline fell back), apply decisions to the equivalent location: `### Response to Prior Items` for inline-style responses, `### Additional Points > New X` for tail-style new items. Preserve the `\\n\\n---\\n\\n` separator and the `## Second Opinion` heading verbatim.\n\nApply the decision to the item in its home location. Then apply the decision:\n\n### Actionable item decisions\n\n- **Selected option** (`choice` is `opt-N`): Add `**Review Decision**: Accepted. <chosen_label>.` to the corresponding section. Integrate the selected direction into the section text so it reads as a final recommendation or resolved answer.\n- **None of these** (`choice` is `none`): Add `**Review Decision**: Rejected — none of the proposed options accepted.` Include the user's `comment` explaining why. Rewrite the section to reflect this decision.\n\nFor actionable items sourced from clarifying questions, rewrite the question's best-guess answer so it reads as the final resolved direction chosen by the reviewer. Do not leave the item framed as an unresolved accept/reject/modify prompt.\n\nFor items sourced from `(prior round, refinement inline)` or `(prior round, disagreement inline)` — disputes of a prior-round item carried in an inline blockquote — the prior-round item is the canonical home: rewrite its answer to absorb the resolution. Then handle the blockquote in one of two ways: (a) remove the blockquote outright if the rewritten answer fully absorbs the second-opinion content, or (b) shorten the blockquote to a single sentence noting the resolution while preserving the `(<provider>)` attribution (e.g. `> **Second opinion (Claude) - refinement.** Resolved by reviewer decision E-N.`). Citations from the original blockquote may be promoted into the rewritten prior-item answer if useful — keep the strongest 1-2 grounding refs.\n\nFor items sourced from `(new in second opinion → ...)` — gap-captured items that received a decision — rewrite the item in place inside its tail-block sub-section (`## New in Second Opinion > ### New X`), not at the top of the prior analysis. Preserve the sub-section heading and continued numbering.\n\n### General comment handling\n\nTreat `general_comment` as overarching guidance that informs the tone and direction of both document rewrites. If it contains specific actionable feedback, weave it into the relevant sections. If it is broad or general, use it as context for how the rewrites should read. Do not create a separate \"General Comment\" or \"Reviewer Notes\" section — the goal is \"final draft\" form.\n\n### Rewrite principles\n\nThe goal is a **final draft** — the documents should read as if they were written with the decisions already made. Do not mechanically append decisions. Instead, lightly rewrite affected sections so they reflect the decisions naturally. Preserve all non-affected sections unchanged. The prior-round content should still read as coherent standalone analysis after integration. Preserve the `## New in Second Opinion` tail block intact for any items that weren't decided. **Always preserve the `## Second Opinion Summary` footer verbatim** — it is the canonical record of the second round's overall position and should not be edited even when individual items it references have been resolved.\n\n## Step 8: Upload to Jira\n\nUpload both updated documents to Jira using `upload_attachment`:\n\n1. Upload clarifying questions:\n - `ticket_number`: `{ticket_key}`\n - `file_path`: `{docs_dir}/clarifying-questions/{ticket_key}-clarifying-questions.md`\n - `link_type`: `clarifying-questions.md`\n\n2. Upload ticket quality critique:\n - `ticket_number`: `{ticket_key}`\n - `file_path`: `{docs_dir}/ticket-critiques/{ticket_key}-ticket-quality-critique.md`\n - `link_type`: `ticket-quality-critique.md`\n\n## Step 9: Complete\n\nConfirm: \"Review decisions captured and uploaded to {ticket_key}.\"\n\n## Return\n\nConfirm \"Review decisions captured and uploaded to {ticket_key}.\" and list the two attachments uploaded (`{ticket_key}-clarifying-questions.md` and `{ticket_key}-ticket-quality-critique.md`). Note any decisions that could not be applied.\n",
620
622
  "commit-and-push.md": "Stage, commit, and push implementation changes for ticket {ticket_key}.\n\nBefore executing, assess the git state and present a clear plan for user approval.\n\n## Step 1 — Assess Git State\n\nRun these commands and note the results:\n- `git branch --show-current` — record the current branch name\n- `git status --porcelain` — identify all modified, added, and untracked files\n\n## Step 2 — Determine Branch\n\nDecide the branching strategy and be prepared to state it explicitly. Cover:\n\n- Whether you will commit on the current branch, or create a new branch.\n- If creating a new branch: the exact new branch name, and which branch it will be created from (current branch vs. `main`).\n- If branching from `main`: whether `main` needs to be pulled/updated first, and the command you will run.\n- Whether the target branch already exists remotely (and if so, whether you will push to the existing remote branch).\n\nDefault rules:\n\n- If the current branch already contains `{ticket_key}` (case-insensitive), plan to commit on the current branch.\n- Otherwise, plan to create a new branch named `feature/{ticket_key}` from the current branch.\n\n## Step 3 — Prepare Commit Details\n\n- Separate implementation files from unrelated changes. Only stage files related to the ticket.\n- Compose a commit message: `{ticket_key}: <brief description of what was implemented>`\n\n## Step 4 — Present Plan for Approval (commit, push, and PR)\n\nFor this run, `auto_approve` = `{auto_approve}`.\n\n**Auto-approve mode.** If `auto_approve` is `true`, do NOT present the approval plan and do NOT wait for user input. Apply the default branching rule from Step 2 (commit on the current branch if it contains `{ticket_key}` case-insensitively; otherwise create `feature/{ticket_key}` from the current branch). Stage all files reported by `git status --porcelain` that you assess as related to the ticket per Step 3's \"Only stage files related to the ticket\" rule (when uncertain, prefer including over excluding — auto-approve trades caution for momentum, and the user has explicitly opted in). Use the commit-message format from Step 3. Skip directly to Step 5 and execute.\n\nOtherwise (any value of `auto_approve` other than the literal `true` — including empty, `false`, or missing), proceed with the existing approval flow below.\n\nPresent a single approval plan covering the commit, push, and pull request creation before proceeding:\n\n```\nCommit Plan for {ticket_key}\n─────────────────────────────\nCurrent branch: <current branch name>\nBranching: - <\"Commit on current branch\" | \"Create new branch `<name>` from `<source branch>`\">\n - <if branching from main: \"Pull latest main first via `git checkout main && git pull`\" | omit if N/A>\n - <\"Remote branch already exists — will push to existing\" | \"New remote branch — will push with -u\" | omit if N/A>\nFiles to stage: <count> files\n - path/to/file1.py\n - path/to/file2.py\nExcluded: <any unrelated changed files, or \"None\">\nCommit message: {ticket_key}: <description>\nPush to: origin/<target branch>\nPR title: <commit subject — derived automatically after commit>\nPR base: main\n```\n\nWait for the user to approve, request changes, or reject. The user may adjust the branch name, file inclusion, commit message, PR title, PR base, or give other instructions. The PR title defaults to the commit subject after the commit is made, and the PR base defaults to `main`.\n\nDo not proceed until the user explicitly approves.\n\n## Step 5 — Execute\n\n1. If creating a new branch, run `git checkout -b <branch name>`.\n2. Stage approved files with `git add <file1> <file2> ...` — do not use `git add -A` or `git add .`.\n3. Commit with the approved message.\n4. Push with `git push -u origin <branch>`.\n\n## Return\n\nConfirm the commit was made and pushed by reporting the branch name, the commit subject line, and the pushed remote (e.g. `origin/feature/{ticket_key}`). Note any files that were intentionally excluded from the commit.\n",
621
623
  "create-pr.md": "# Create a pull request for the just-pushed branch\n\nThe implementation has been committed and pushed. Open a PR against `main` for the current branch, with a descriptive title derived from the commit you just made.\n\n## Step 1 — Read the commit subject line\n\nRun `git log -1 --pretty=%s` to get the most recent commit subject. The implement-ticket pipeline asks the commit step to use the form `{ticket_key}: <description>`, so this line is normally already a good PR title.\n\n## Step 2 — Determine the head branch\n\nUse `git branch --show-current`. This is the head branch.\n\n## Step 3 — Call create_pull_request and report the PR URL\n\nCall the `create_pull_request` MCP tool directly with:\n\n- `head_branch`: value from `git branch --show-current`\n- `base_branch`: `\"main\"` — unless the user supplied a different PR base at the commit step, in which case use that value instead.\n- `title`: the commit subject from Step 1 (the derived PR title) — unless the user supplied a different PR title at the commit step, in which case use that value instead.\n\nHonor any PR title / PR base overrides the user gave at the commit step's plan; the commit step advertises those fields as adjustable, so any override the user gave there must carry forward into this tool call rather than being silently replaced by the defaults above.\n\nDo not pass a `body` parameter so the project's `.github/PULL_REQUEST_TEMPLATE.md` populates the description.\n\nReport the returned `pr_url` to the user.\n\n## Return\n\nReturn the URL of the created pull request.\n",
622
- "decompose-epic-candidate.md": "Decompose an Epic parent draft into ordered child tickets with idempotency and per-child duplicate checks.\n\n## Inputs\n\n- Epic parent draft: `{docs_dir}/tickets/EPIC-{slug}.md`.\n- Research pack: `{docs_dir}/idea-to-ticket/{slug}-{run_id}/research-pack.md` / `.json`.\n- Standards checklist: `{docs_dir}/idea-to-ticket/{slug}-{run_id}/standards-checklist.json`.\n- Resolved uncertainties: `{docs_dir}/idea-to-ticket/{slug}-{run_id}/resolved-uncertainties.md`.\n- Hard cap variable `{max_children}` (string integer; default `\"10\"` when not set by the caller).\n\n## Instructions\n\n1. Read the Epic parent draft, research pack, standards checklist, and resolved uncertainties. Use only this context plus optional narrow web search; do not call deep research from this step.\n\n2. Propose ordered child tickets that, together, fully implement the Epic. Each proposed child must include:\n - `summary` — Jira title.\n - `issue_type` — typically `Task`; use `Spike` only for primarily discovery children.\n - `rationale` — short explanation of why this child exists and what it produces.\n - `labels` — must include `ai-generated`, `idea-to-ticket`, `idea-to-ticket-child`, and the unique child idempotency label `bapi-idea-to-ticket-{run_id}-child-<N>` where `<N>` is the 1-based child index in the final ordered list.\n - `idempotency_label` — the same `bapi-idea-to-ticket-{run_id}-child-<N>` string.\n - `draft_path` — `{docs_dir}/tickets/TICKET-{slug}-child-<N>.md` (drafts written by `jira-ticket-writer` later).\n\n3. Hard cap enforcement. Count proposed children. If the count exceeds `{max_children}` (parsed as an integer), halt locally with a clear \"split first\" message: ask the user to split the idea into multiple smaller Epics or to raise `--max-children` deliberately. Do not silently truncate.\n\n4. Per-child duplicate lookup. For each proposed child (in order), call `get_tickets` once with a title/keyword search built from the child's summary. If a clear duplicate exists, drop that child from the plan and record the drop reason; never halt the whole run because a child has a duplicate. Re-number `<N>` only after all drops are finalized so child indexes are contiguous.\n\n5. Per-child research is restricted to the parent research pack plus optional narrow web search. Do not call deep research per child.\n\n6. Write `{docs_dir}/idea-to-ticket/{slug}-{run_id}/decomposition-plan.json` with at minimum:\n - `parent_summary` — copy from the parent draft.\n - `max_children` — the resolved integer value used for the cap.\n - `children` — ordered array of surviving children with all fields from step 2.\n - `dropped_children` — array of `{proposed_summary, reason}` for children removed by duplicate lookup.\n\n## Return\n\nConfirm `decomposition-plan.json` was written, report the final child count and the number of children dropped for duplicate reasons.\n",
624
+ "decompose-epic-candidate.md": "Decompose an Epic parent draft into ordered child tickets with idempotency and per-child duplicate checks.\n\n## Inputs\n\n- Epic parent draft: `{docs_dir}/tickets/EPIC-{slug}.md`.\n- Research pack: `{docs_dir}/idea-to-ticket/{slug}-{run_id}/research-pack.md` / `.json`.\n- Standards checklist: `{docs_dir}/idea-to-ticket/{slug}-{run_id}/standards-checklist.json`.\n- Resolved uncertainties: `{docs_dir}/idea-to-ticket/{slug}-{run_id}/resolved-uncertainties.md`.\n- Hard cap variable `{max_children}` (string integer; default `\"10\"` when not set by the caller). The default `\"10\"` is a **hard ceiling / upper bound, not a target** child count — it caps how many children are allowed, and is **not a goal to fill**. The normal target child count is smaller (fewer, larger M/L slices); see the sizing heuristics in step 2.\n\n## Instructions\n\n1. Read the Epic parent draft, research pack, standards checklist, and resolved uncertainties. Use only this context plus optional narrow web search; do not call deep research from this step.\n\n2. Propose ordered child tickets that, together, fully implement the Epic.\n\n **Sizing heuristics (maintainer-owned defaults).** Size each proposed child by its expected **file-touch breadth and depth plus rough lines of code (LOC) changed**, using these exact thresholds:\n - `S = 1–2 files / <~80 LOC`\n - `M = ~3–8 files / ~80–400 LOC (ideal target)`\n - `L = ~8–15 files / ~400–900 LOC (acceptable)`\n - `XL = >15 files / >~900 LOC → split further; never emit an XL child`\n\n Target size priority: `M (ideal) → L (acceptable) → S (only if unavoidable); never XL`.\n\n Bias the decomposition toward **fewer, larger, independently implementable vertical slices** rather than many tiny one-feature children. The Bridge implementation tooling works better on M–L vertical slices, and a swarm of tiny S children magnifies sibling merge risk under parallel execution. Each child should be an independently implementable vertical slice; if a proposed child would be XL, split it further until each piece is M or L.\n\n Each proposed child must include:\n - `summary` — Jira title.\n - `issue_type` — typically `Task`; use `Spike` only for primarily discovery children.\n - `rationale` — short explanation of why this child exists and what it produces. Include a brief size estimate inside this existing field (do **not** add a new `size` field), e.g. `Estimated size: M (~4 files / ~150 LOC)`.\n - `labels` — must include `ai-generated`, `idea-to-ticket`, `idea-to-ticket-child`, and the unique child idempotency label `bapi-idea-to-ticket-{run_id}-child-<N>` where `<N>` is the 1-based child index in the final ordered list.\n - `idempotency_label` — the same `bapi-idea-to-ticket-{run_id}-child-<N>` string.\n - `draft_path` — `{docs_dir}/tickets/TICKET-{slug}-child-<N>.md` (drafts written by `jira-ticket-writer` later).\n\n3. Hard cap enforcement. First attempt a normal, smaller M/L-biased decomposition per the step 2 sizing heuristics. Then count proposed children: `{max_children}` is a hard ceiling that **halts on exceed**, not a target to fill. If the count exceeds `{max_children}` (parsed as an integer), halt locally with a clear \"split first\" message: ask the user to split the idea into multiple smaller Epics or to raise `--max-children` deliberately. Do not silently truncate.\n\n4. Per-child duplicate lookup. For each proposed child (in order), call `get_tickets` once with a title/keyword search built from the child's summary. If a clear duplicate exists, drop that child from the plan and record the drop reason; never halt the whole run because a child has a duplicate. Re-number `<N>` only after all drops are finalized so child indexes are contiguous.\n\n5. Per-child research is restricted to the parent research pack plus optional narrow web search. Do not call deep research per child.\n\n6. Write `{docs_dir}/idea-to-ticket/{slug}-{run_id}/decomposition-plan.json` with at minimum:\n - `parent_summary` — copy from the parent draft.\n - `max_children` — the resolved integer value used for the cap.\n - `children` — ordered array of surviving children with all fields from step 2.\n - `dropped_children` — array of `{proposed_summary, reason}` for children removed by duplicate lookup.\n\n## Return\n\nConfirm `decomposition-plan.json` was written, report the final child count and the number of children dropped for duplicate reasons.\n",
623
625
  "decompose-epic.md": "Decompose the epic into manageable sub-tasks and get user approval.\n\n## Epic Description\n\n{epic_description}\n\n## Instructions\n\n1. Read the following artifacts to establish full context. If a file does not exist or is empty, proceed without it:\n - `{docs_dir}/epic-plans/{epic_slug}/research-findings.md`\n - `{docs_dir}/epic-plans/{epic_slug}/codebase-exploration.md`\n\n2. Reason about the epic and produce a decomposition. Consider:\n - Logical groupings of work that can be implemented and tested independently\n - Dependencies between sub-tasks (what must be built first)\n - A reasonable scope for each sub-task (each should be achievable in a single implementation session)\n\n3. Write the decomposition to `{docs_dir}/epic-plans/{epic_slug}/epic-plan.md` with this format:\n\n```markdown\n# Epic Decomposition\n\n## Sub-tasks\n\n### 1. {Sub-task title}\n- **Scope**: {What this sub-task covers}\n- **Key files/areas**: {Files and code areas involved}\n- **Dependencies**: {Other sub-task numbers this depends on, or \"None\"}\n\n### 2. {Sub-task title}\n...\n```\n\n4. **Soft limit check**: If the decomposition results in more than 8 sub-tasks, you must verbally warn the user: \"This decomposition has N sub-tasks, which exceeds the recommended limit of 8. Consider splitting this feature into multiple epics.\" Then proceed with the approval flow.\n\n5. Present the decomposition to the user and ask for their feedback. Explain the reasoning behind the breakdown and the dependency ordering.\n\n6. You MUST stop and wait for the user to respond. Do NOT assume approval. Do NOT proceed to the next step.\n\n7. If the user provides feedback or rejects the decomposition:\n - Incorporate their feedback\n - Rewrite `{docs_dir}/epic-plans/{epic_slug}/epic-plan.md` with the revised version\n - Present the revised decomposition and ask for approval again\n - Repeat until the user explicitly approves\n\n8. Only after explicit user approval, confirm: \"Decomposition approved. Proceeding to sub-task exploration.\"\n\n## Return\n\nConfirm \"Decomposition approved.\" and report the final sub-task count plus the path to `{docs_dir}/epic-plans/{epic_slug}/epic-plan.md`. Flag if the count exceeded the recommended limit of 8.\n",
624
626
  "draft-and-critique.md": "Draft the ticket(s) for this idea, run a BAPI-320 hygiene pass, and emit structured draft metadata.\n\n## Inputs\n\n- Run manifest: `{docs_dir}/idea-to-ticket/{slug}-{run_id}/run-manifest.json`.\n- Research pack: `{docs_dir}/idea-to-ticket/{slug}-{run_id}/research-pack.md` / `.json`.\n- Duplicate assessment: `{docs_dir}/idea-to-ticket/{slug}-{run_id}/duplicate-assessment.json`.\n- Standards checklist: `{docs_dir}/idea-to-ticket/{slug}-{run_id}/standards-checklist.json`.\n- Resolved uncertainties: `{docs_dir}/idea-to-ticket/{slug}-{run_id}/resolved-uncertainties.md`.\n\n## Instructions\n\n1. Read all five input artifacts in full before drafting. The manifest's `scope` (`task`, `spike`, or `epic_candidate`) determines the drafting path.\n\n2. Drafting path by scope:\n - **task** or **spike**:\n - Call the `jira-ticket-writer` sub-agent with an explicit output path of `{docs_dir}/tickets/TICKET-{slug}.md`. The sub-agent must write the full markdown draft to that exact file.\n - **epic_candidate**:\n - Call `jira-ticket-writer` to draft only the Epic parent. Use the explicit output path `{docs_dir}/tickets/EPIC-{slug}.md`. Child tickets are produced later by `decompose-epic-candidate.md`; do not draft them here.\n\n3. Issue type policy:\n - Default ambiguous ideas to `Task`.\n - Choose `Spike` only when the work is primarily discovery/research/learning with no clear acceptance criteria yet.\n - The Epic parent uses Jira issue type `Epic`.\n\n4. Hygiene pass (BAPI-320 forbidden tokens). After the sub-agent writes the draft, read it back and ensure none of these tokens are present:\n - markdown tables (any `|`-separated header row).\n - escaped pipe-table patterns (e.g. `\\|`).\n - task-list checkboxes such as `- [ ]` or `- [x]`.\n - angle-bracket placeholder tokens (any `<placeholder>` form, even inside backticks).\n - raw HTML blocks (`<div>`, `<br>`, `<table>`, etc.).\n When a forbidden token is found, rewrite the surrounding paragraph in plain prose or bullet form and save the cleaned draft over the same path.\n\n5. Write `{docs_dir}/idea-to-ticket/{slug}-{run_id}/draft-metadata.json` describing what Jira should later create.\n\n For **task** / **spike** scope, the metadata shape is:\n - `summary` — Jira ticket title.\n - `issue_type` — `Task` or `Spike`.\n - `labels` — array of Jira labels. Must include `ai-generated`, `idea-to-ticket`, the per-run label `bapi-idea-to-ticket-{run_id}`, and the stable idea-hash label `bapi-idea-hash-{idea_hash}` (so a future run of the same idea is caught by label).\n - `idempotency_label` — `bapi-idea-to-ticket-{run_id}` (matches the label used by the duplicate-and-context-scan step).\n - `slim_description` — short Jira-safe description (no forbidden tokens). The full draft is uploaded as an attachment.\n - `attachment_path` — `{docs_dir}/tickets/TICKET-{slug}.md` (or the equivalent path used above).\n\n For **epic_candidate** scope, the metadata shape is:\n - `parent.summary` — Epic title.\n - `parent.issue_type` — `Epic`.\n - `parent.labels` — must include `ai-generated`, `idea-to-ticket`, `bapi-idea-to-ticket-{run_id}-parent`, and the stable idea-hash label `bapi-idea-hash-{idea_hash}`.\n - `parent.idempotency_label` — `bapi-idea-to-ticket-{run_id}-parent`.\n - `parent.slim_description` — short Epic description.\n - `parent.attachment_path` — `{docs_dir}/tickets/EPIC-{slug}.md`.\n - `children` — placeholder array. Populated later by `decompose-epic-candidate.md`; leave as an empty array here.\n\n6. Save the metadata exactly once. Downstream steps read this file; do not move it.\n\n## Return\n\nConfirm the draft path, the metadata path, and the chosen scope (`task`, `spike`, or `epic_candidate`).\n",
625
627
  "duplicate-and-context-scan.md": "Detect existing Jira tickets that duplicate or relate to this idea before any Jira mutation.\n\n## Inputs\n\n- Run manifest: `{docs_dir}/idea-to-ticket/{slug}-{run_id}/run-manifest.json`.\n- Research pack: `{docs_dir}/idea-to-ticket/{slug}-{run_id}/research-pack.md` (if produced).\n- Pipeline variable `allow_duplicate` controls override behavior (for this run, `allow_duplicate` = `{allow_duplicate}`). Treat the literal string `\"true\"` as override; any other value (including `\"false\"`, missing, or empty) is non-override.\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` as directed below — performing an orchestrator-directed tool call is not \"re-orchestrating\".\n\n1. Build at least two Jira search queries from the manifest:\n - **Title/keyword query**: use the most salient nouns from `idea` and `slug` as title/text keywords. Prefer 2-4 concrete terms over long natural-language sentences. Run via `get_tickets`.\n - **Stable idea-hash query** (the reliable cross-run dedup): run `get_tickets` with its `labels` parameter set to `bapi-idea-hash-{idea_hash}`. This label is identical for every run of the same idea, so it catches a PRIOR run that already created a ticket for this idea — even one created days ago. A hit here is a strong `duplicate` signal.\n - **Idempotency-label query**: run `get_tickets` with its `labels` parameter set to `bapi-idea-to-ticket-{run_id}` (the tool builds the `labels in (...)` JQL for you — do not pass a raw JQL string). This per-run label only matches a partial run of THIS same run, so it supports resume behavior.\n\n2. For each returned ticket, capture: ticket key, summary, status, and a short reason it matched (which query, which keyword).\n\n3. Classify the overall verdict as one of:\n - `duplicate` — at least one returned ticket clearly describes the same work as `idea`.\n - `related` — returned tickets are adjacent or partial overlaps but not the same work.\n - `none_found` — no meaningful matches.\n - `unable_to_check` — the Jira search itself failed (network error, auth error, JQL rejection). Record the failure and pick this verdict.\n\n4. Write `{docs_dir}/idea-to-ticket/{slug}-{run_id}/duplicate-assessment.json` with at minimum:\n - `verdict` — one of the four values above.\n - `matches` — array of `{ticket_key, summary, status, reason}` objects (may be empty).\n - `queries_used` — array of the actual JQL/search strings sent.\n - `allow_duplicate` — the resolved value of `{allow_duplicate}` for this run.\n\n5. Halt behavior:\n - If `verdict` is `duplicate` and `allow_duplicate` is not `\"true\"`, halt locally. Do not continue the pipeline. Tell the user that the duplicate halt is strict and that re-running with `--allow-duplicate` overrides it.\n - If `verdict` is `duplicate` and `allow_duplicate` is `\"true\"`, continue the pipeline but keep the duplicate evidence in the assessment file so downstream steps can reference it (e.g., to add a \"supersedes\" note to the draft).\n - For `related`, `none_found`, and `unable_to_check`, continue without halting.\n\n## Return\n\nConfirm `duplicate-assessment.json` was written, report `verdict`, and report whether the run is halting or continuing.\n",
@@ -644,7 +646,7 @@ export const INSTRUCTIONS = {
644
646
  "research-decision.md": "Decide which research tools to run for this idea, biased toward cheap local research first.\n\n## Inputs\n\n- Run manifest: `{docs_dir}/idea-to-ticket/{slug}-{run_id}/run-manifest.json` (must already exist from the preflight step).\n\n## Instructions\n\n1. Read `{docs_dir}/idea-to-ticket/{slug}-{run_id}/run-manifest.json`. This is the source of truth for `idea`, `readiness`, `scope`, and `run_id`. If the file does not exist, halt locally — the preflight step did not complete.\n\n2. Decide which research tools should run for this idea, in roughly this priority order:\n - **Local codebase research first.** Inspect the working tree (search, grep, file reads) for prior art, related modules, and existing tests. Prefer this for anything that touches code you already own.\n - **Narrow web search second.** Use targeted web search for short factual lookups: a specific library API, a known external standard, a public spec.\n - **Deep research only when justified.** Deep research is expensive and slow; it must be earned by one of the rubric items below.\n\n3. Deep-research allowance rubric. Deep research is only allowed when at least one of these is true:\n - **blast radius**: the change spans many systems or has high reversibility cost (e.g., schema migrations, auth, billing, public APIs).\n - **unfamiliar external domain**: the idea depends on a third-party domain or specification the repository has no prior coverage of.\n - **compliance/security uncertainty**: there is real compliance or security uncertainty (SOC2, PII, secret handling, access control).\n - **cheaper research failed**: a cheaper round (local + narrow web search) already happened in this run and left blocking unknowns.\n - **explicit user request**: the user explicitly asked for deep research.\n\n4. Write `{docs_dir}/idea-to-ticket/{slug}-{run_id}/research-plan.json`. Required fields:\n - `selected_tools` — array of tool identifiers to run, drawn from at least `[\"codebase_search\", \"web_search\", \"deep_research\"]`. Empty array is allowed when no research is needed.\n - `rationale` — short string explaining the choice in terms of the rubric above.\n - `deep_research_query` — string. Required when `deep_research` is in `selected_tools`, otherwise empty string.\n - `web_search_topics` — array of strings; may be empty.\n - `codebase_search_topics` — array of strings; may be empty.\n - `expected_unknowns` — array of strings describing what the research is expected to resolve.\n\n5. Do not invoke any research tool from this step — that happens in `execute-research.md`. This step only writes the plan.\n\n## Return\n\nConfirm `research-plan.json` was written, list `selected_tools`, and quote the rationale.\n",
645
647
  "screen-and-resolve.md": "Apply project standards and the minimum-evidence gate before drafting.\n\n## Inputs\n\n- Run manifest: `{docs_dir}/idea-to-ticket/{slug}-{run_id}/run-manifest.json`.\n- Research pack: `{docs_dir}/idea-to-ticket/{slug}-{run_id}/research-pack.md` / `.json` (may be partial or absent).\n- Duplicate assessment: `{docs_dir}/idea-to-ticket/{slug}-{run_id}/duplicate-assessment.json`.\n- Project standards: the response from the earlier `get_project_standards` step in the same pipeline run. Treat the standards as unavailable when that response was an error envelope, a 404, or missing.\n\n## Instructions\n\n1. Produce three artifacts in the run directory:\n - `{docs_dir}/idea-to-ticket/{slug}-{run_id}/standards-checklist.json`\n - `{docs_dir}/idea-to-ticket/{slug}-{run_id}/open-questions.md`\n - `{docs_dir}/idea-to-ticket/{slug}-{run_id}/resolved-uncertainties.md`\n\n2. Standards checklist:\n - When `get_project_standards` returned a usable result, derive the checklist items from those standards. Each item is a `{requirement, satisfied, evidence}` triple where `evidence` either cites the research pack or notes \"deferred to draft\".\n - When standards are unavailable (404, error envelope, missing), generate a fallback baseline checklist covering at least: requirements clarity, acceptance criteria presence, testability, security/PII consideration, and rollback/observability when scope warrants it. Mark each as `satisfied: false` with `evidence: \"fallback baseline — no project standards available\"`.\n\n3. Open questions:\n - List every unresolved unknown that blocks drafting. Pull from the research pack's `unresolved_unknowns` and from your own reading of `idea`.\n - Each question gets its own bullet (no markdown tables, no `- [ ]` checkboxes — BAPI-320 hygiene).\n - If the question has a defensible best-guess answer, write it in `resolved-uncertainties.md` instead, with explicit assumption language (\"Assuming X because Y...\").\n\n4. Resolved uncertainties:\n - Mirror open questions that have defensible best-guess answers. Each entry must include `assumption`, `basis` (research-pack reference or codebase reference), and `confidence` (\"low\", \"medium\", \"high\").\n\n5. Minimum-evidence gate. Halt locally if ALL of the following are true:\n - The research pack contains no codebase references for the idea.\n - The standards checklist has zero items (even the fallback baseline is missing).\n - `resolved-uncertainties.md` records no explicit assumptions.\n When the gate fires, do not continue to drafting. Report the halt and direct the user to either run a smaller idea, run `--allow-duplicate` semantics for replays, or supply more context manually.\n\n## Return\n\nConfirm the three artifacts were written and whether the minimum-evidence gate fired.\n",
646
648
  "update-ticket-rewrite.md": "Rewrite the Jira ticket description for {ticket_key} using the generated clarifying questions and critique documents.\n\n1. Fetch the current ticket description using the `get_ticket` tool with ticket_number `{ticket_key}`.\n2. Read the clarifying questions from the local file saved by the previous step (check `{docs_dir}/clarifying-questions/` for `{ticket_key}-clarifying-questions.md`). For each best-guess answer, verify it against the codebase using file search and code grep. Accept verified answers, correct inaccurate ones with evidence, and let ambiguous ones stand.\n3. Read the critique from the local file saved by the previous step (check `{docs_dir}/ticket-critiques/` for `{ticket_key}-ticket-quality-critique.md`). Address all Requested Changes. Apply Points to Consider selectively — accept genuine improvements, skip stylistic preferences.\n4. Write the rewritten ticket in standard markdown format (not Jira wiki markup). Preserve the Summary, Requirements, and Acceptance Criteria structure.\n5. Save the output to `{docs_dir}/tickets/{ticket_key}.md`. Output only the clean rewritten ticket — no meta-commentary.\n\n## Return\n\nConfirm the rewritten ticket was saved to `{docs_dir}/tickets/{ticket_key}.md` and briefly note which clarifying-question answers were corrected against the codebase and which critique Requested Changes were addressed.\n",
647
- "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`, `upload_attachment`, and `track_ticket` 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`, `upload_attachment`, 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 `upload_attachment` using `attachment_path`.\n 4. Call `track_ticket` with the resolved ticket key so Bridge API picks the new ticket up.\n 5. 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}`).\n 2. Draft any surviving children that lack a draft on disk by calling `jira-ticket-writer` per child with the `draft_path` from the decomposition plan. After drafting, extend `draft-metadata.json` so `children[]` mirrors the final list from the decomposition plan.\n 3. 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 `upload_attachment` using `parent.attachment_path`, then call `track_ticket` for the Epic key.\n 4. 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 `upload_attachment` using `draft_path`.\n - Call `track_ticket` for the child key.\n 5. 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\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 `upload_attachment` 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 list of ticket key(s) this run created or reused — and nothing else. The chain reads ONLY this payload to pick its review / start-tickets targets, so it must contain exactly the keys from `upload-state.json` (the single `ticket_key`, or the Epic parent key followed by every child key) never any key you merely looked up during duplicate detection:\n\n```json\n{\"created_ticket_keys\": [\"BAPI-331\"]}\n```\n",
649
+ "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`, `upload_attachment`, and `track_ticket` 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`, `upload_attachment`, 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 `upload_attachment` using `attachment_path`.\n 4. Call `track_ticket` with the resolved ticket key so Bridge API picks the new ticket up.\n 5. 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}`).\n 2. Draft any surviving children that lack a draft on disk by calling `jira-ticket-writer` per child with the `draft_path` from the decomposition plan. After drafting, extend `draft-metadata.json` so `children[]` mirrors the final list from the decomposition plan.\n 3. 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 `upload_attachment` using `parent.attachment_path`, then call `track_ticket` for the Epic key.\n 4. 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 `upload_attachment` using `draft_path`.\n - Call `track_ticket` for the child key.\n 5. 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\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 `upload_attachment` 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",
648
650
  "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 `upload_attachment` 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 `upload_attachment` 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",
649
651
  "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\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.}\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## Next Steps\n{One-line summaries for each sub-task, specifically formatted so they can be copy-pasted directly into the `/write-ticket` command. 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\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.\n"
650
652
  };