@bridge_gpt/mcp-server 0.2.18 → 0.2.20

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 (100) hide show
  1. package/CONDUCTOR.md +75 -0
  2. package/README.md +8 -5
  3. package/build/agent-capabilities/probe-context.js +13 -3
  4. package/build/agent-capabilities/probes.js +262 -11
  5. package/build/agent-capabilities/reporter.js +1 -0
  6. package/build/agents.generated.js +1 -1
  7. package/build/backend-warnings.js +44 -0
  8. package/build/claude-settings.js +129 -0
  9. package/build/commands.generated.js +5 -3
  10. package/build/conductor/bridge-api-client.js +7 -7
  11. package/build/conductor/cli.js +65 -12
  12. package/build/conductor/deny-enforcement-preflight.js +96 -0
  13. package/build/conductor/doctor.js +183 -2
  14. package/build/conductor/epic-reconcile.js +9 -1
  15. package/build/conductor/epic-runtime.js +403 -43
  16. package/build/conductor/epic-state.js +7 -0
  17. package/build/conductor/errors.js +115 -3
  18. package/build/conductor/event-accessors.js +28 -10
  19. package/build/conductor/local-merge.js +458 -95
  20. package/build/conductor/merge-ledger.js +6 -4
  21. package/build/conductor/pr-ci-producer.js +17 -2
  22. package/build/conductor/producer-ledger.js +1 -1
  23. package/build/conductor/store.js +161 -18
  24. package/build/conductor/supervisor-merge.js +32 -5
  25. package/build/conductor/taxonomy.js +8 -0
  26. package/build/conductor/tools.js +28 -6
  27. package/build/conductor/worker-ledger-cli.js +244 -0
  28. package/build/conductor-bin.js +1884 -6917
  29. package/build/doctor.js +8 -0
  30. package/build/estimate-epic.js +84 -0
  31. package/build/executor/cli.js +229 -0
  32. package/build/executor/credentials.js +65 -0
  33. package/build/executor/deps.js +117 -0
  34. package/build/executor/env.js +79 -0
  35. package/build/executor/heartbeat.js +59 -0
  36. package/build/executor/http-client.js +131 -0
  37. package/build/executor/index.js +10 -0
  38. package/build/executor/job-errors.js +55 -0
  39. package/build/executor/job-log-registry.js +110 -0
  40. package/build/executor/job-runner.js +822 -0
  41. package/build/executor/job-types.js +60 -0
  42. package/build/executor/merge-job.js +229 -0
  43. package/build/executor/observation.js +123 -0
  44. package/build/executor/permissions.js +79 -0
  45. package/build/executor/preflight.js +144 -0
  46. package/build/executor/process.js +81 -0
  47. package/build/executor/prompt-spec.js +235 -0
  48. package/build/executor/results.js +134 -0
  49. package/build/executor/resume-pre-spawn.js +179 -0
  50. package/build/executor/runner.js +98 -0
  51. package/build/executor/terminal-mutation.js +34 -0
  52. package/build/executor/test-clock.js +109 -0
  53. package/build/executor/types.js +18 -0
  54. package/build/executor/verdict-artifact.js +53 -0
  55. package/build/executor/viewer-tabs.js +78 -0
  56. package/build/executor/watch-cli.js +113 -0
  57. package/build/executor/worker-command.js +106 -0
  58. package/build/executor/worker-finalization.js +177 -0
  59. package/build/executor/worker-log.js +92 -0
  60. package/build/executor/worktree-gc.js +134 -0
  61. package/build/executor/worktree-inspection.js +86 -0
  62. package/build/executor/worktree.js +103 -0
  63. package/build/index.js +14534 -10416
  64. package/build/mcp-invoke.js +19 -3
  65. package/build/mcp-provisioning.js +31 -25
  66. package/build/mcp-registration-doctor.js +27 -7
  67. package/build/mcp-server-invocation.js +152 -0
  68. package/build/pipelines.generated.js +16 -20
  69. package/build/readme.generated.js +1 -1
  70. package/build/sfcc/client.js +192 -50
  71. package/build/sfcc/ocapi-write-faults.js +94 -0
  72. package/build/sfcc/permissions.js +7 -22
  73. package/build/sfcc/reads-site-preference.js +52 -19
  74. package/build/sfcc/register.js +9 -0
  75. package/build/sfcc/write-grants.js +80 -0
  76. package/build/sfcc/write-guard.js +39 -0
  77. package/build/sfcc/write-result.js +47 -0
  78. package/build/sfcc/write-tool-common.js +85 -0
  79. package/build/sfcc/writes-custom-object-def.js +141 -0
  80. package/build/sfcc/writes-object-attribute-payloads.js +97 -0
  81. package/build/sfcc/writes-site-preference-payloads.js +59 -0
  82. package/build/sfcc/writes-site-preference.js +96 -0
  83. package/build/sfcc/writes-system-object-payloads.js +213 -0
  84. package/build/sfcc/writes-system-object.js +348 -0
  85. package/build/sfcc/writes.js +66 -0
  86. package/build/start-tickets-conductor.js +25 -93
  87. package/build/start-tickets-prereqs.js +152 -1
  88. package/build/start-tickets.js +96 -158
  89. package/build/version.generated.js +1 -1
  90. package/build/visual-diff-worker.js +313 -0
  91. package/build/visual-diff.js +632 -0
  92. package/build/worktree-core.js +202 -0
  93. package/package.json +8 -4
  94. package/pipelines/idea-to-ticket.json +7 -0
  95. package/pipelines/review-ticket.json +5 -18
  96. package/public/css/main.min.css +1612 -107
  97. package/public/css/main.min.css.map +1 -1
  98. package/public/js/main.min.js +10267 -1
  99. package/public/js/main.min.js.map +1 -1
  100. package/smoke-test/SMOKE-TEST.md +2 -1
package/CONDUCTOR.md CHANGED
@@ -129,3 +129,78 @@ redeems the approval token and the server returns `merge.succeeded`.
129
129
  **`merge.succeeded` is the only terminal merge event.** The local SQLite conductor
130
130
  store uses schema version 5 (BAPI-413) to accommodate the `merge.pending_approval`
131
131
  type in the `events.type` CHECK constraint.
132
+
133
+ ## Durable execution environment hardening (BAPI-527)
134
+
135
+ Worker ledger tools are made **reliable again** — this is a robustness fix for the
136
+ worker-facing tools, **not** a new primary completion backstop. The done gate and
137
+ supervisor remain the authority; this change only stops the worker's own
138
+ `check_messages` / `wait_for_done_gate` calls from failing.
139
+
140
+ **The invariant.** Worker-facing ledger operations run through `conductor-bin.js`
141
+ executed **under the captured conductor Node** (`CONDUCTOR_NODE_PATH`). **The worker
142
+ Node must never load `better-sqlite3`.** Dispatch captures the conductor process's
143
+ own Node executable and threads it to each worker as `CONDUCTOR_NODE_PATH`, paired
144
+ with `BAPI_CONDUCTOR_CLI_FILE` (the packaged `conductor-bin.js` path). The
145
+ worker-side boundary (`src/conductor/worker-ledger-cli.ts`) shells out with a fixed
146
+ **list argv** via `execFile` (never `shell: true`, never a command string) and
147
+ passes event JSON over **stdin** (`--data-json-stdin`), so raw payloads/secrets
148
+ never enter the process argument list. Conductor-**owned** processes (the CLI,
149
+ supervisor, and epic runtimes) keep calling `store.ts` directly — they legitimately
150
+ own the native load.
151
+
152
+ **Fail loud, never fall back.** A missing/invalid `CONDUCTOR_NODE_PATH` or absent
153
+ `BAPI_CONDUCTOR_CLI_FILE` raises the typed `ConductorLedgerSubprocessRuntimeError`,
154
+ surfaced to callers as the `LEDGER_SUBPROCESS_RUNTIME_UNAVAILABLE` (503) envelope
155
+ with a fixed, path-free, secret-free `details: { env_var, reason }` bag
156
+ (`reason ∈ missing | invalid | cli_missing | spawn_failed | malformed_stdout`). The
157
+ boundary **never** falls back to the worker's `process.execPath` — doing so would
158
+ re-introduce the native load into the worker Node, the exact failure this removes.
159
+ `CONDUCTOR_NODE_PATH` is **non-secret operational metadata**: it is inherited from
160
+ the worker shell env injected at the spawn boundary and is **not** persisted into
161
+ `.mcp.json`, which stays intentionally env-free.
162
+
163
+ **Rejected alternatives.**
164
+
165
+ - **Local HTTP sidecar** (a long-lived ledger daemon the worker talks to over a
166
+ port) — rejected: port conflicts, zombie-process lifecycle management, capability-
167
+ token handling, and a large failure surface, all disproportionate for a
168
+ file-backed local ledger.
169
+ - **WASM / pure-JS SQLite in the worker** — rejected: it replaces the proven
170
+ `better-sqlite3` WAL / synchronous-performance behavior and is a poor fit for a
171
+ database file shared by multiple processes (the conductor Node and any workers).
172
+
173
+ ### Three protective guarantees (operator view)
174
+
175
+ BAPI-527 adds three independent reliability guarantees. Each is a protective
176
+ default; none changes the interactive (non-conductor) `start-tickets` contract.
177
+
178
+ 1. **Ledger operations are isolated from the worker path.** Worker `check_messages`
179
+ / `wait_for_done_gate` run the ledger op in a subprocess under
180
+ `CONDUCTOR_NODE_PATH` (details above), so a Node ABI mismatch degrades a worker
181
+ convenience, never the whole session.
182
+
183
+ 2. **Dispatch does not alter your branch.** Unattended/epic dispatch is
184
+ **non-mutating**: it resolves the base with a fetch-only
185
+ `origin/<base>` lookup and cuts new worktrees from that fetched SHA. It never
186
+ checks out, fast-forwards (`git merge --ff-only`), force-moves
187
+ (`git branch --force`), or stashes the operator's live checkout. A **live-source
188
+ guard** additionally refuses dispatch (or, with an explicit override, warns
189
+ loudly) when the conductor's base repo path equals the operator's live
190
+ dev-server checkout:
191
+ - `BAPI_CONDUCTOR_LIVE_SOURCE_PATH` — the operator's live dev-server checkout
192
+ path to protect.
193
+ - `BAPI_CONDUCTOR_ALLOW_LIVE_SOURCE_DISPATCH` — truthy (`1`/`true`/`yes`/`on`)
194
+ override that downgrades a collision from fatal to a loud warning.
195
+ The read-only `doctor` command surfaces the guard state
196
+ (`not-configured` / `safe` / `collision`) without mutating anything.
197
+
198
+ 3. **Parse runs in a separate process.** The post-merge repository re-index runs
199
+ out-of-process via `asyncio.create_subprocess_exec`, so the FastAPI event loop
200
+ stays responsive to concurrent dispatch calls while a CPU-bound parse runs. The
201
+ ticket enters **`parse_pending`** after merge and folds to **`done`** only when
202
+ `/parse-status` reports terminal success; a permanent parse failure (or the
203
+ `max_wait_ms` budget being exhausted) folds it to **`blocked`** with a sanitized
204
+ reason and never silently wedges the epic. Parse status values are `idle`,
205
+ `queued`, `in_progress`, `succeeded`, and `failed`; terminal failure reasons are
206
+ bounded and sanitized (no stderr, tracebacks, argv, or secrets).
package/README.md CHANGED
@@ -266,10 +266,10 @@ For invocation, prefer the slash command — it's deterministic. A free-text exa
266
266
  These features are useful for most tickets.
267
267
 
268
268
  **1. Review Ticket**
269
- - **What it does:** Runs a full quality review of a ticket: clarifying questions + critique (initial round), an automatic alternate-model second opinion, then evaluates findings and produces a decision page to accept/reject them. The second-opinion pass is included by default and can be skipped with `--rounds=1`.
269
+ - **What it does:** Runs a full quality review of a ticket: clarifying questions + critique plus an automatic alternate-model second opinion, then evaluates findings and produces a decision page to accept/reject them. The backend executor now owns all review round orchestration (including the second-opinion round) server-side; pass `--rounds=1` for a cheaper single-pass review, `--rounds=2` to force the full second-opinion review, or omit `--rounds` to let the backend's difficulty-adaptive review policy decide.
270
270
  - **When it's useful:** (Refinement) Right after a ticket is drafted, before anyone starts building — to surface gaps and tighten it.
271
271
  - **How to use it:** `/review-ticket BAPI-123` (command only — "review" as free text is easily mistaken for a freehand agent review).
272
- - **Flags:** `--auto` auto-accept findings / skip the approval gates · `--rounds=1` skip the automatic second-opinion review step while preserving downstream evaluation and decision-capture work (cheaper single-pass review). `--rounds=2` (default) runs the full two-round review.
272
+ - **Flags:** `--auto` auto-accept findings / skip the approval gates · `--rounds=1` request a cheaper single-pass review (no second-opinion round) while preserving downstream evaluation and decision-capture work · `--rounds=2` force the full second-opinion review · omit `--rounds` to let the backend's difficulty-adaptive review policy decide (it falls back to a full second-opinion review when adaptive routing is disabled or the ticket's difficulty cannot be resolved).
273
273
  - **Multi-ticket fan-out:** `/review-tickets BAPI-123 BAPI-456` opens one terminal tab per ticket for parallel review with no worktrees (terminal launcher only — no `wt`/`git`). All `/review-ticket` flags apply; `--review KEY=auto,rounds=N` sets per-ticket overrides. Packaged CLI: `npx -y @bridge_gpt/mcp-server review-tickets KEY [KEY ...]`.
274
274
 
275
275
  **2. Start Tickets**
@@ -497,10 +497,13 @@ All SFCC tools are read-only and target a developer sandbox. Oversized responses
497
497
  **Custom object definitions** (needs the `sfcc` profile)
498
498
  - `custom_object_definition_attributes_get` — fetch attribute definitions for a known custom object type. OCAPI cannot enumerate custom object types, so `object_type` must already be known.
499
499
  - `custom_object_definition_attribute_search` — search attribute definitions within a known custom object type. Read-only — creating a custom object *type* isn't possible via OCAPI; that's a future v2 metadata-import capability.
500
+ - `custom_object_definition_attribute_create` — **write** (sandbox only): create an attribute definition on a known custom object type via `PUT /custom_object_definitions/{type}/attribute_definitions/{id}`. TYPE creation is never attempted (the type must pre-exist). Echoes paste-ready OCAPI grant JSON on 403.
501
+ - `custom_object_definition_attribute_update` — **write** (sandbox only): update an attribute definition via an ETag-conditional `PATCH …/attribute_definitions/{id}`; surfaces 409/412 conflicts and echoes grant JSON on 403.
500
502
 
501
503
  **Site preferences** (needs the `sfcc` profile; sandbox only)
502
504
  - `site_preference_get` — read a preference group's effective preferences.
503
505
  - `site_preference_search` — search/filter preferences within a group.
506
+ - `site_preference_values_set` — **write** (sandbox only): set custom preference values via `PATCH /site_preferences/preference_groups/{group}/sandbox` with a flat map of `c_`-prefixed ids to string/number/boolean/string[] values. A bad group returns 404 `CustomPreferenceGroupNotFoundException`; echoes grant JSON on 403.
504
507
 
505
508
  ## CLI Subcommands
506
509
 
@@ -688,13 +691,13 @@ The full surface, for when you need the complete enumeration. Day-to-day, use [U
688
691
 
689
692
  ### MCP tools
690
693
 
691
- The server exposes **58 documented tools** (enumerated below). What's actually registered in a session depends on `BRIDGE_MCP_PROFILE`: the default `core` profile loads a trimmed subset, and the conductor/pipeline-authoring/SFCC tools are added only under their respective profiles (see [Environment Variables](#environment-variables)). Async AI tools follow a request/get pattern: call the `request_*` tool to kick off generation, then the matching `get_*` tool to retrieve the result (or pass `wait_for_result: true` to poll automatically).
694
+ The server exposes **59 documented tools** (enumerated below). What's actually registered in a session depends on `BRIDGE_MCP_PROFILE`: the default `core` profile loads a trimmed subset, and the conductor/pipeline-authoring/SFCC tools are added only under their respective profiles (see [Environment Variables](#environment-variables)). Async AI tools follow a request/get pattern: call the `request_*` tool to kick off generation, then the matching `get_*` tool to retrieve the result (or pass `wait_for_result: true` to poll automatically).
692
695
 
693
696
  - **Connectivity & identity** — `ping`, `get_my_role`, `get_docs_dir`
694
697
  - **Jira tickets** — `get_tickets`, `get_ticket`, `create_ticket`, `update_ticket_description`, `add_comment`, `get_comments`
695
698
  - **Attachments** — `attachment` (operations: `upload`, `download`, `list`)
696
699
  - **AI generation (request/get)** — `request_plan_generation`/`get_plan`, `request_architecture`/`get_architecture`, `create_doc`/`get_doc` (design docs by `doc_type`: tdd/fsd/prd), `request_prd`/`get_prd`, `request_clarifying_questions`/`get_clarifying_questions`, `request_ticket_critique`/`get_ticket_critique`, `request_ticket_review`, `request_reimplement_context`/`get_reimplement_context`, `request_brainstorm`/`get_brainstorm`, `request_deep_research`/`get_deep_research`
697
- - **Other AI** — `second_opinion`, `generate_image`, `generate_decision_page`
700
+ - **Other AI** — `second_opinion`, `generate_image`, `generate_decision_page`, `visual_diff` (deterministic pixel diff of a rendered URL vs a design comp)
698
701
  - **Ticket lifecycle** — `track_ticket`, `update_ticket_state`, `get_ticket_state`
699
702
  - **Jira status** — `get_jira_transitions`, `update_jira_status`, `resolve_target_status`
700
703
  - **Repository & CI** — `parse_repository`, `get_parse_status`, `regenerate_directory_map`, `create_pull_request`, `resolve_ci_checks`, `poll_ci_checks`
@@ -708,7 +711,7 @@ Pipelines are declarative, multi-step workflows your AI agent executes step-by-s
708
711
  | Pipeline | Description | Invoke with |
709
712
  |---|---|---|
710
713
  | `implement-ticket` | Generate a plan, execute the implementation, commit, open a PR, and monitor CI | `/implement-ticket PROJ-123` |
711
- | `review-ticket` | Full ticket quality review: initial clarifying questions + critique, automatic second-opinion pass (default), then evaluation and decision capture. Pass `--rounds=1` to skip the second-opinion step (`--rounds=2` is the default). | `/review-ticket PROJ-123` |
714
+ | `review-ticket` | Full ticket quality review: clarifying questions + critique plus an automatic second-opinion pass, then evaluation and decision capture. The backend owns round orchestration; pass `--rounds=1` for a single-pass review or `--rounds=2` to force the full second-opinion review, or omit `--rounds` to let the backend decide adaptively. | `/review-ticket PROJ-123` |
712
715
  | `idea-to-ticket` | Turn an idea into a Jira Task/Spike (or Epic + children) with research, dedup, and critique | `/idea-to-ticket "<idea>"` |
713
716
  | `plan-epic` | Decompose an epic into sub-tasks with a structured exploration doc for each | `/plan-epic "<epic>"` |
714
717
  | `full-automation` | Chain: idea → ticket(s) → review each → spawn worktrees to implement | `/full-automation "<idea>"` |
@@ -18,9 +18,12 @@ import { DEFAULT_PROBE_TIMEOUT_MS, } from "./types.js";
18
18
  *
19
19
  * - cursor-agent: `-p --output-format <fmt> --trust --workspace <cwd> <prompt>`.
20
20
  * `--trust` is MANDATORY headless or the workspace-trust prompt hangs; cursor has
21
- * its own `--workspace` cwd flag.
22
- * - claude: `-p [--output-format json] <prompt>`. Claude has NO working-dir flag —
23
- * cwd is set via the spawn options, never an argument.
21
+ * its own `--workspace` cwd flag. All `OutputFormat` values (including `stream-json`)
22
+ * pass through the generic `--output-format <fmt>` slot.
23
+ * - claude: `-p [--dangerously-skip-permissions] [--model <m>] [--output-format json|
24
+ * stream-json --verbose] <prompt>`. Claude has NO working-dir flag — cwd is set via
25
+ * the spawn options, never an argument. `stream-json` REQUIRES `--verbose`. The
26
+ * prompt is ALWAYS the final argv element so mocked prompt extraction stays valid.
24
27
  */
25
28
  export function buildHeadlessArgs(agentName, opts) {
26
29
  const fmt = opts.outputFormat ?? "text";
@@ -29,8 +32,15 @@ export function buildHeadlessArgs(agentName, opts) {
29
32
  }
30
33
  // claude (and any positional-prompt agent without a cwd flag)
31
34
  const args = ["-p"];
35
+ if (opts.skipPermissions === true)
36
+ args.push("--dangerously-skip-permissions");
37
+ if (typeof opts.model === "string" && opts.model.trim().length > 0) {
38
+ args.push("--model", opts.model);
39
+ }
32
40
  if (fmt === "json")
33
41
  args.push("--output-format", "json");
42
+ else if (fmt === "stream-json")
43
+ args.push("--output-format", "stream-json", "--verbose");
34
44
  args.push(opts.prompt);
35
45
  return args;
36
46
  }
@@ -15,6 +15,30 @@ function truncate(text) {
15
15
  const flat = text.replace(/\s+/g, " ").trim();
16
16
  return flat.length > EVIDENCE_MAX ? `${flat.slice(0, EVIDENCE_MAX)}…` : flat;
17
17
  }
18
+ /**
19
+ * Validate a `--output-format stream-json` stdout as a newline-delimited JSON
20
+ * envelope: split on newlines, discard blank lines, and require at least one
21
+ * non-empty line with every remaining line parsing as JSON. Advisory-only — the
22
+ * output-format probe uses this for telemetry validation, never executor gating.
23
+ */
24
+ function validateStreamJsonLines(stdout) {
25
+ const lines = stdout
26
+ .split("\n")
27
+ .map((line) => line.trim())
28
+ .filter((line) => line.length > 0);
29
+ if (lines.length === 0) {
30
+ return { ok: false, reason: "no non-empty stream-json lines were emitted" };
31
+ }
32
+ for (const line of lines) {
33
+ try {
34
+ JSON.parse(line);
35
+ }
36
+ catch {
37
+ return { ok: false, reason: "a stream-json line was not parseable JSON" };
38
+ }
39
+ }
40
+ return { ok: true };
41
+ }
18
42
  /** Verbatim body of the disposable `.claude/commands/echo-test.md` probe command. */
19
43
  function echoCommandBody(marker) {
20
44
  return [
@@ -190,8 +214,10 @@ const preambleMidPrompt = {
190
214
  };
191
215
  const outputFormat = {
192
216
  id: "output-format",
193
- title: "Honors --output-format text and json",
194
- description: "Text mode emits the plain marker; json mode emits a parseable JSON envelope.",
217
+ title: "Honors --output-format text, json, and stream-json",
218
+ description: "Text mode emits the plain marker; json mode emits a parseable JSON envelope; " +
219
+ "stream-json mode emits a parseable newline-delimited JSON line envelope. The " +
220
+ "stream-json check is advisory telemetry validation only — no executor behavior gates on it.",
195
221
  tier: "structural",
196
222
  appliesTo: ["claude", "cursor-agent"],
197
223
  spawnsAgent: true,
@@ -211,20 +237,46 @@ const outputFormat = {
211
237
  cwd: dir,
212
238
  outputFormat: "json",
213
239
  });
214
- const nonExited = nonExitedResult(jsonRun);
215
- if (nonExited)
216
- return nonExited;
217
- const { exitCode, stdout, elapsedMs } = jsonRun;
218
- if (exitCode !== 0) {
219
- return { status: "fail", detail: `json output-format exited ${exitCode}`, elapsedMs, evidence: truncate(stdout) };
240
+ const jsonNonExited = nonExitedResult(jsonRun);
241
+ if (jsonNonExited)
242
+ return jsonNonExited;
243
+ const jsonExited = jsonRun;
244
+ if (jsonExited.exitCode !== 0) {
245
+ return { status: "fail", detail: `json output-format exited ${jsonExited.exitCode}`, elapsedMs: jsonExited.elapsedMs, evidence: truncate(jsonExited.stdout) };
220
246
  }
221
247
  try {
222
- JSON.parse(stdout.trim());
248
+ JSON.parse(jsonExited.stdout.trim());
223
249
  }
224
250
  catch {
225
- return { status: "fail", detail: "json output-format did not emit parseable JSON", elapsedMs, evidence: truncate(stdout) };
251
+ return { status: "fail", detail: "json output-format did not emit parseable JSON", elapsedMs: jsonExited.elapsedMs, evidence: truncate(jsonExited.stdout) };
252
+ }
253
+ // Advisory stream-json validation (§15.2): parse the newline-delimited envelope.
254
+ const streamRun = await ctx.runHeadless({
255
+ prompt: `Do not use any tools. Output exactly the token ${marker}.`,
256
+ cwd: dir,
257
+ outputFormat: "stream-json",
258
+ });
259
+ const streamNonExited = nonExitedResult(streamRun);
260
+ if (streamNonExited)
261
+ return streamNonExited;
262
+ const streamExited = streamRun;
263
+ if (streamExited.exitCode !== 0) {
264
+ return { status: "fail", detail: `stream-json output-format exited ${streamExited.exitCode}`, elapsedMs: streamExited.elapsedMs, evidence: truncate(streamExited.stdout) };
265
+ }
266
+ const streamCheck = validateStreamJsonLines(streamExited.stdout);
267
+ if (!streamCheck.ok) {
268
+ return {
269
+ status: "fail",
270
+ detail: "stream-json output-format did not emit parseable newline-delimited JSON",
271
+ elapsedMs: streamExited.elapsedMs,
272
+ evidence: truncate(streamExited.stdout),
273
+ };
226
274
  }
227
- return { status: "pass", detail: "text marker present and json parseable", elapsedMs };
275
+ return {
276
+ status: "pass",
277
+ detail: "text marker present, json parseable, and stream-json line-envelope parseable",
278
+ elapsedMs: streamExited.elapsedMs,
279
+ };
228
280
  },
229
281
  };
230
282
  const workspaceFlag = {
@@ -261,6 +313,204 @@ const noCwdFlag = {
261
313
  return { status: "fail", detail: `unexpected cwd flag(s) in argv: ${offenders.join(", ")}` };
262
314
  },
263
315
  };
316
+ /** File the deny probe asks the agent to `cat`; its sole content is the allowed marker. */
317
+ const DENY_TARGET_FILE = "deny-probe-target.txt";
318
+ /** `permissions.deny` rule blocking the `cat` Bash command (the denied action). */
319
+ const DENY_BASH_RULE = "Bash(cat:*)";
320
+ /** PreToolUse matcher for the fallback hook — mirrors the conductor Bash-tool matcher style. */
321
+ const DENY_HOOK_MATCHER = "Bash";
322
+ /**
323
+ * Command body for the fallback `PreToolUse` deny hook. Emits the local block
324
+ * convention (`.claude/hooks/check_mutating_sql.py`): a `permissionDecision: "deny"`
325
+ * JSON object on stdout, exit 0. `printf` avoids echo's backslash portability
326
+ * differences; the JSON contains no single quotes so single-quoting is safe.
327
+ */
328
+ function denyHookCommand() {
329
+ const payload = JSON.stringify({
330
+ hookSpecificOutput: {
331
+ hookEventName: "PreToolUse",
332
+ permissionDecision: "deny",
333
+ permissionDecisionReason: "agent-capability deny-enforcement probe fallback: tool call denied.",
334
+ },
335
+ });
336
+ return `printf '%s' '${payload}'`;
337
+ }
338
+ /** Seed the `cat` target file whose only content is the unique allowed marker. */
339
+ async function seedDenyTargetFile(ctx, dir, marker) {
340
+ await ctx.deps.writeFile(join(dir, DENY_TARGET_FILE), `${marker}\n`);
341
+ }
342
+ /** Seed `<dir>/.claude/settings.local.json` with a minimal `permissions.deny` rule. */
343
+ async function seedDenySettings(ctx, dir, marker) {
344
+ await seedDenyTargetFile(ctx, dir, marker);
345
+ const claudeDir = join(dir, ".claude");
346
+ await ctx.deps.mkdir(claudeDir, { recursive: true });
347
+ const settings = { permissions: { deny: [DENY_BASH_RULE] } };
348
+ await ctx.deps.writeFile(join(claudeDir, "settings.local.json"), `${JSON.stringify(settings, null, 2)}\n`);
349
+ }
350
+ /** Seed `<dir>/.claude/settings.local.json` with the `PreToolUse` deny-hook fallback. */
351
+ async function seedDenyHook(ctx, dir, marker) {
352
+ await seedDenyTargetFile(ctx, dir, marker);
353
+ const claudeDir = join(dir, ".claude");
354
+ await ctx.deps.mkdir(claudeDir, { recursive: true });
355
+ const settings = {
356
+ hooks: {
357
+ PreToolUse: [
358
+ { matcher: DENY_HOOK_MATCHER, hooks: [{ type: "command", command: denyHookCommand() }] },
359
+ ],
360
+ },
361
+ };
362
+ await ctx.deps.writeFile(join(claudeDir, "settings.local.json"), `${JSON.stringify(settings, null, 2)}\n`);
363
+ }
364
+ /**
365
+ * Prompt instructing the agent to attempt the denied Bash call and echo its output.
366
+ * The allowed marker is deliberately NOT in the prompt — the agent can only produce
367
+ * it by actually running the (denied) Bash command, so the marker is un-fabricable.
368
+ */
369
+ function denyProbePrompt() {
370
+ return [
371
+ "This is an agent-capability probe of permission enforcement.",
372
+ `Use the Bash tool to run exactly this command: cat ${DENY_TARGET_FILE}`,
373
+ "Then output the command's exact stdout on its own line.",
374
+ "Use ONLY the Bash tool for this — do not use Read, Grep, or any other tool, and do not open the file yourself.",
375
+ "If the Bash command is blocked, denied, or errors, output the single word DENIED and nothing else.",
376
+ "Do not guess, fabricate, or infer the file contents — only report what the command actually returned.",
377
+ ].join("\n");
378
+ }
379
+ /** Attach a deny-enforcement layer to a probe result's metadata (bounded, secret-free). */
380
+ function withDenyLayer(result, layer) {
381
+ return { ...result, metadata: { ...(result.metadata ?? {}), layer } };
382
+ }
383
+ /**
384
+ * Classify one deny-probe headless run. Uses `nonExitedResult` first so hangs and
385
+ * spawn errors remain loud; a clean exit with the allowed marker means the call ran
386
+ * (allowed), and a clean exit without it means the call was rejected (blocked).
387
+ */
388
+ function evaluateDenyRun(run, allowedMarker) {
389
+ const nonExited = nonExitedResult(run);
390
+ if (nonExited)
391
+ return { kind: "nonexited", result: nonExited };
392
+ const { exitCode, stdout, stderr, elapsedMs } = run;
393
+ if (exitCode !== 0) {
394
+ return {
395
+ kind: "error",
396
+ result: {
397
+ status: "fail",
398
+ detail: `deny-enforcement probe agent exited ${exitCode}`,
399
+ elapsedMs,
400
+ evidence: truncate(stdout || stderr),
401
+ },
402
+ };
403
+ }
404
+ if (stdout.includes(allowedMarker)) {
405
+ return { kind: "allowed", evidence: truncate(stdout) };
406
+ }
407
+ return { kind: "blocked", evidence: truncate(stdout || stderr) };
408
+ }
409
+ /**
410
+ * Run the shared deny-enforcement check: a no-deny CONTROL run FIRST (proving the
411
+ * agent actually attempts the canary call — absence of the marker in a deny run is
412
+ * only evidence of enforcement once the control shows the marker), then settings
413
+ * `permissions.deny`, and the `PreToolUse` hook fallback ONLY when the settings run
414
+ * exits cleanly but allows the denied call. Hangs and spawn errors are returned
415
+ * loudly and never fall through to the next run. Returns the probe result plus the
416
+ * enforcing layer. Reused by the capability probe AND the executor preflight so both
417
+ * share identical behavior.
418
+ */
419
+ export async function runDenyEnforcementCheck(ctx, opts) {
420
+ const marker = ctx.marker("DENY_ALLOWED");
421
+ const prompt = denyProbePrompt();
422
+ const runOpts = { skipPermissions: true, model: opts?.model, timeoutMs: opts?.timeoutMs };
423
+ // 0) CONTROL: same temp-project shape and prompt, NO deny rule. The marker MUST
424
+ // surface here — otherwise the agent never attempted the canary Bash call and
425
+ // a marker-free deny run would be a refusal, not enforcement (NEVER a pass).
426
+ const controlDir = await ctx.makeTempProject((d) => seedDenyTargetFile(ctx, d, marker));
427
+ const controlRun = await ctx.runHeadless({ prompt, cwd: controlDir, ...runOpts });
428
+ const c = evaluateDenyRun(controlRun, marker);
429
+ if (c.kind === "nonexited")
430
+ return { result: withDenyLayer(c.result, "none"), layer: "none" };
431
+ if (c.kind === "error")
432
+ return { result: withDenyLayer(c.result, "none"), layer: "none" };
433
+ if (c.kind === "blocked") {
434
+ return {
435
+ result: withDenyLayer({
436
+ status: "fail",
437
+ detail: "control run (no deny rule) did not execute the canary call — the agent never " +
438
+ "attempted the Bash command, so deny enforcement cannot be verified " +
439
+ "(a refusal is NOT enforcement)",
440
+ elapsedMs: controlRun.elapsedMs,
441
+ evidence: c.evidence,
442
+ }, "none"),
443
+ layer: "none",
444
+ };
445
+ }
446
+ // c.kind === "allowed": the agent demonstrably attempts the call here — a
447
+ // marker-free clean exit in the runs below now genuinely means "rejected".
448
+ // 1) settings permissions.deny (preferred layer).
449
+ const denyDir = await ctx.makeTempProject((d) => seedDenySettings(ctx, d, marker));
450
+ const settingsRun = await ctx.runHeadless({ prompt, cwd: denyDir, ...runOpts });
451
+ const s = evaluateDenyRun(settingsRun, marker);
452
+ if (s.kind === "nonexited")
453
+ return { result: withDenyLayer(s.result, "none"), layer: "none" };
454
+ if (s.kind === "error")
455
+ return { result: withDenyLayer(s.result, "none"), layer: "none" };
456
+ if (s.kind === "blocked") {
457
+ return {
458
+ result: withDenyLayer({
459
+ status: "pass",
460
+ detail: "denied call rejected by settings permissions.deny",
461
+ elapsedMs: settingsRun.elapsedMs,
462
+ evidence: s.evidence,
463
+ }, "settings-deny"),
464
+ layer: "settings-deny",
465
+ };
466
+ }
467
+ // 2) settings deny did NOT reject — try the PreToolUse hook fallback.
468
+ const hookDir = await ctx.makeTempProject((d) => seedDenyHook(ctx, d, marker));
469
+ const hookRun = await ctx.runHeadless({ prompt, cwd: hookDir, ...runOpts });
470
+ const h = evaluateDenyRun(hookRun, marker);
471
+ if (h.kind === "nonexited")
472
+ return { result: withDenyLayer(h.result, "none"), layer: "none" };
473
+ if (h.kind === "error")
474
+ return { result: withDenyLayer(h.result, "none"), layer: "none" };
475
+ if (h.kind === "blocked") {
476
+ return {
477
+ result: withDenyLayer({
478
+ status: "pass",
479
+ detail: "settings deny did not reject; PreToolUse hook fallback rejected the call",
480
+ elapsedMs: hookRun.elapsedMs,
481
+ evidence: h.evidence,
482
+ }, "pretooluse-hook"),
483
+ layer: "pretooluse-hook",
484
+ };
485
+ }
486
+ // 3) allowed by BOTH — deny is not enforced on this installed CLI version.
487
+ return {
488
+ result: withDenyLayer({
489
+ status: "fail",
490
+ detail: "deny is NOT enforced for this installed Claude version under --dangerously-skip-permissions " +
491
+ "(neither settings permissions.deny nor a PreToolUse hook rejected the denied call)",
492
+ elapsedMs: hookRun.elapsedMs,
493
+ evidence: h.evidence,
494
+ }, "none"),
495
+ layer: "none",
496
+ };
497
+ }
498
+ const denyEnforcement = {
499
+ id: "deny-enforcement",
500
+ title: "Enforces a permissions.deny rule under --dangerously-skip-permissions",
501
+ description: "Runs a no-deny control run first (proving the agent actually attempts the canary call), then spawns " +
502
+ "claude headless with --dangerously-skip-permissions in a disposable temp project carrying a " +
503
+ "permissions.deny rule, attempts the denied call, and asserts it is rejected — reporting which layer " +
504
+ "enforced (settings-deny vs a PreToolUse hook fallback). A fail means deny is NOT enforced on the " +
505
+ "installed CLI (or cannot be verified) and is a load-bearing executor preflight signal.",
506
+ tier: "heavy",
507
+ appliesTo: ["claude"],
508
+ spawnsAgent: true,
509
+ async run(ctx) {
510
+ const { result } = await runDenyEnforcementCheck(ctx);
511
+ return result;
512
+ },
513
+ };
264
514
  /** All probes in deterministic report order. */
265
515
  export const ALL_PROBES = [
266
516
  binaryResolves,
@@ -269,6 +519,7 @@ export const ALL_PROBES = [
269
519
  claudeCommandsResolve,
270
520
  preambleMidPrompt,
271
521
  outputFormat,
522
+ denyEnforcement,
272
523
  workspaceFlag,
273
524
  noCwdFlag,
274
525
  ];
@@ -45,6 +45,7 @@ export function formatCapabilityJson(collection) {
45
45
  detail: r.result.detail,
46
46
  elapsedMs: r.result.elapsedMs ?? null,
47
47
  evidence: r.result.evidence ?? null,
48
+ metadata: r.result.metadata ?? null,
48
49
  })),
49
50
  }, null, 2);
50
51
  }
@@ -8,7 +8,7 @@ export const AGENTS = {
8
8
  "model": "opus",
9
9
  "color": "blue"
10
10
  },
11
- "body": "\nYou are an elite software engineering project manager and technical analyst with deep expertise in codebase archaeology and Jira ticket crafting. You excel at understanding complex codebases, identifying relevant existing code, and translating problem descriptions into precisely-scoped, actionable Jira tickets that engineers can pick up and execute with minimal ambiguity.\n\n## Your Mission\n\nGiven a problem description from the user, you will:\n1. Conduct thorough codebase research to understand the existing architecture, patterns, and relevant code\n2. Write a structured Jira ticket as a new markdown file that references specific files, functions, and patterns from the codebase\n\n## Phase 1: Deep Codebase Research\n\nThis is the most critical phase. You MUST spend significant time here before writing anything. Do NOT rush this phase.\n\n### Research Protocol\n\n1. **Understand the Problem Space**: Re-read the user's problem description carefully. Identify the domain, the affected areas, and the type of change needed (new feature, bug fix, refactor, enhancement).\n\n2. **Map the Relevant Architecture**: \n - Search for files, modules, and directories related to the problem domain\n - Read the key source files thoroughly — do not skim\n - Trace code paths: how does data flow through the relevant parts of the system?\n - Identify controller -> helper -> service -> model chains if applicable\n\n3. **Identify Extension Points**:\n - What existing code can be reused or extended?\n - What patterns does the codebase already use for similar functionality?\n - Are there helper functions, utilities, or base classes that should be leveraged?\n - Are there configuration files, metadata definitions, or templates that need modification?\n\n4. **Identify Constraints**:\n - What conventions does the project follow? (Check CLAUDE.md, README, existing patterns)\n - What testing patterns are used?\n - Are there ES5 limitations, specific framework patterns, or platform constraints?\n\n5. **Catalog Your Findings**: Keep mental notes of every relevant file path, function name, pattern, and architectural decision you discover. You will reference these in the ticket.\n\n### Research Depth Guidelines\n- Read at least 5-15 relevant source files in full, more if the problem is complex\n- Follow import chains to understand dependencies\n- Check test files to understand expected behaviors and testing patterns\n- Review configuration and metadata files if relevant\n- Search for TODO comments, known limitations, or related existing issues in the code\n\n## Phase 2: Write the Jira Ticket\n\nAfter completing research, create a new markdown file with the ticket. Use the naming convention `tickets/TICKET-<short-descriptive-name>.md`. If the `tickets/` directory does not exist, create it.\n\n### Ticket Structure\n\nThe markdown file MUST contain exactly these sections:\n\n```markdown\n# [Concise Title Describing the Task]\n\n## Summary\n\n[2-4 sentences describing what this task is about, why it matters, and the high-level approach. Be specific — reference the actual system components involved.]\n\n## Requirements\n\n[Numbered list of specific, actionable requirements. Each requirement should be a clear unit of work.]\n\n1. **[Requirement Title]**: [Description of what needs to be done.]\n - *Relevant code*: `path/to/file.js` — `functionName()` [brief note on how this code relates]\n - *Relevant code*: `path/to/other/file.js` — [brief note]\n\n2. **[Requirement Title]**: [Description]\n - *Relevant code*: ...\n\n[Continue for all requirements]\n\n## Acceptance Criteria\n\n[Bullet list. Each criterion is a testable, verifiable condition.]\n\n- [Specific, testable criterion]\n- [Another criterion]\n- [Continue as needed]\n\n## Materials & Access\n\n[Trailing audit-trail section — always the LAST section of the draft. Inventory every material the ticket references, grouped by source. Use monospace backticks for file paths and other technical provenance. Redact any embedded secrets.]\n\n### Reachable Local Files\n\n- `path/to/local/file.ext` — [what it is; will be gathered and attached post-create]\n\n### External/Auth-Gated Links\n\n- [Name or purpose] — `https://example.com/...` (record-only; external/auth-gated)\n\n### Binary/Image Materials (Record-Only)\n\n- `path/to/screenshot.png` — [sanitized location/access note; not attached]\n```\n\n### Writing Guidelines\n\n**Summary**:\n- Be concrete, not abstract. Name the actual components, cartridges, or subsystems involved.\n- State the \"why\" — what problem does this solve or what value does it add?\n- Mention the general technical approach if it's clear from the research.\n\n**Requirements**:\n- Each requirement should represent a logical unit of work\n- Order requirements in a logical implementation sequence when possible\n- ALWAYS cite relevant existing files and functions when they exist. Use exact file paths relative to the project root.\n- Explain HOW the existing code relates: \"extend this function\", \"follow this pattern\", \"reuse this helper\", \"modify this configuration\"\n- If a requirement involves creating new files, suggest where they should live based on existing project structure conventions\n- Be specific about what needs to change vs. what needs to be created new\n- Include requirements for tests, documentation, and configuration/metadata changes if applicable\n\n**Acceptance Criteria**:\n- Every criterion must be independently verifiable\n- Cover functional requirements, edge cases, testing, and non-functional requirements\n- Include criteria for backwards compatibility if relevant\n- Include criteria for test coverage\n- Use plain `-` bullets (Jira's ADF has no native checkbox, so `- [ ]` renders as literal text)\n\n**Materials Completeness Inventory**:\n- After the draft is written, INVENTORY every material the ticket references: local file paths, URLs/links, named docs/designs, screenshots, and specs. This pass only INVENTORIES and RECORDS — it does NOT attach anything. The actual attachment of reachable local files happens post-create (after the Jira `ticket_key` exists) via a separate gather-and-attach step.\n- Classify each material by source using a scheme-based rule (no network probe required):\n - **Local filesystem paths** named in the ticket body are the only **low-risk** materials — eligible to be gathered and attached post-create.\n - Every **`http(s)` URI is external/auth-gated** — regardless of whether the user explicitly linked it (an explicitly-linked Confluence or Google Doc URL is still external/auth-gated) — and is **record-only** here.\n - **Binary/image materials** (screenshots, PDFs, etc.) are **record-only** — document them with sanitized location/access notes; do NOT attempt to attach them.\n- Write the trailing `## Materials & Access` section (the LAST section of the draft) grouping items under the sub-headings *Reachable Local Files*, *External/Auth-Gated Links*, and *Binary/Image Materials (Record-Only)*, using bulleted lists. Use monospace formatting (backticks) for technical provenance such as file paths.\n- **Redact secrets before writing anything**: before writing any URL or access note, sanitize and redact embedded credentials, SAS tokens, API keys, and basic-auth secrets using a high-visibility placeholder such as `[REDACTED_TOKEN]`. A location/access note must NEVER expose a plaintext secret.\n\n### Regression Completeness Pass (Gated)\n\nAfter the draft (including its `## Materials & Access` section) is written, run this pass. It is a non-blocking, **warn-not-halt** completeness check — it never blocks or fails ticket creation, and it never modifies the Requirements or Acceptance Criteria text directly.\n\n1. **Check the gate first.** Call the `config_field` MCP tool with `operation` set to `\"get\"` and `field_name` set to `enable_regression_checks`. If the tool returns an error, `null`, or any value other than the literal string `\"true\"`, **skip this entire pass** — the draft is produced exactly as it would be without this section (byte-for-byte unchanged). The recommended default for this flag is OFF (unset) for safe rollout; only proceed past this step when it is explicitly `\"true\"`.\n\n2. **Derive the touched-symbol set.** From the draft's Requirements and *Relevant code* citations (or, if the ticket references an existing diff/PR, that diff/PR), extract the specific function/class/symbol names the proposed change touches.\n\n3. **Run the deterministic core.** Execute:\n ```bash\n npx -y @bridge_gpt/mcp-server regression-check --mode lightweight --json --symbols <derived,symbol,names>\n ```\n This is the same subcommand the standalone `regression-reviewer` agent and `regression-check` command use — do not hand-roll your own `ast-grep`/`ripgrep` discovery.\n\n4. **Fail-open on a degraded or failed run.** If the command errors, or `summary.degraded_flags` is non-empty, record that the pass ran degraded (or could not run) and proceed — the draft is still produced. Never halt ticket creation because this subcommand was unavailable.\n\n5. **Cross-check against Requirements + Acceptance Criteria.** Parse the JSON `findings` array (`symbol`, `call_sites.by_file`, `broad_mentions`). For each symbol, compare its real call-sites and broad mentions against what the draft's Requirements and Acceptance Criteria already cover. Flag any affected caller, migration, or contract (a file with a real call-site or an uninspected broad mention) that the criteria do NOT mention.\n\n6. **Record the flags — never rewrite Requirements/Acceptance Criteria.** Append a `[WARNING]` block immediately before the `## Materials & Access` section, listing each flagged item:\n ```markdown\n ## Regression Completeness Notes\n\n [WARNING] The following systems were not explicitly addressed in the Requirements or Acceptance Criteria above:\n - `path/to/affected_caller.py` — calls `changed_symbol` (N real call-sites); not mentioned in Requirements\n - `path/to/config.yml` — broad mention of `changed_symbol`; verify this reference is unaffected\n\n Degraded: [list summary.degraded_flags, or \"none — full structural analysis ran\"]\n ```\n If no flags were raised and the run was not degraded, write a single line instead: `Regression completeness pass: no unaddressed systems found.` If the run was degraded with zero findings either way, state that explicitly rather than implying a clean pass.\n\n### Output Formatting (Jira upload)\n\nThe ticket is uploaded to Jira, which converts the Markdown to Atlassian Document Format (ADF) and hard-caps the description at **32,767 characters**. Keep the output clean and within budget:\n\n- **Length**: aim for under ~30,000 characters. If the scope genuinely needs more, split into a parent ticket plus sub-tickets rather than one oversized ticket.\n- **Acceptance Criteria**: plain `-` bullets, not `- [ ]` (ADF has no native checkbox).\n- **No images**: do not embed images or use relative image links. This \"No images\" rule applies strictly to inline images in the description body; it does NOT restrict the attachments produced by the Materials Completeness Inventory / gather-and-attach pass.\n- **No empty headings**: every heading must have text on its line.\n- **Placeholders**: prefer `{placeholder}` over `<placeholder>`.\n\n## Quality Standards\n\n- **No vague language**: Replace \"should handle errors properly\" with \"should catch LLM provider timeouts and return a normalized error response with errorType 'TimeoutError'\"\n- **No assumptions without evidence**: Only reference code you actually read during research. If you're unsure about something, say so explicitly in the ticket.\n- **Appropriate scope**: The ticket should represent a coherent, deliverable unit of work. If the problem is too large, note that it may need to be broken into sub-tasks, but still write the parent ticket.\n- **Developer empathy**: Write as if the developer picking this up has general project knowledge but hasn't recently worked on this specific area. Give them enough context to get started quickly.\n\n## Important Reminders\n\n- Do NOT skip or abbreviate the research phase. The quality of the ticket depends entirely on the depth of your codebase understanding.\n- Do NOT make up file paths or function names. Only reference code you have actually found and read.\n- DO create the markdown file — do not just output the content to the chat. Write it to disk.\n- If the project has specific conventions (from CLAUDE.md or similar), ensure your ticket's requirements align with those conventions.\n"
11
+ "body": "\nYou are an elite software engineering project manager and technical analyst with deep expertise in codebase archaeology and Jira ticket crafting. You excel at understanding complex codebases, identifying relevant existing code, and translating problem descriptions into precisely-scoped, actionable Jira tickets that engineers can pick up and execute with minimal ambiguity.\n\n## Your Mission\n\nGiven a problem description from the user, you will:\n1. Conduct thorough codebase research to understand the existing architecture, patterns, and relevant code\n2. Write a structured Jira ticket as a new markdown file that references specific files, functions, and patterns from the codebase\n\n## Phase 1: Deep Codebase Research\n\nThis is the most critical phase. You MUST spend significant time here before writing anything. Do NOT rush this phase.\n\n### Research Protocol\n\n1. **Understand the Problem Space**: Re-read the user's problem description carefully. Identify the domain, the affected areas, and the type of change needed (new feature, bug fix, refactor, enhancement).\n\n2. **Map the Relevant Architecture**: \n - Search for files, modules, and directories related to the problem domain\n - Read the key source files thoroughly — do not skim\n - Trace code paths: how does data flow through the relevant parts of the system?\n - Identify controller -> helper -> service -> model chains if applicable\n\n3. **Identify Extension Points**:\n - What existing code can be reused or extended?\n - What patterns does the codebase already use for similar functionality?\n - Are there helper functions, utilities, or base classes that should be leveraged?\n - Are there configuration files, metadata definitions, or templates that need modification?\n\n4. **Identify Constraints**:\n - What conventions does the project follow? (Check CLAUDE.md, README, existing patterns)\n - What testing patterns are used?\n - Are there ES5 limitations, specific framework patterns, or platform constraints?\n\n5. **Catalog Your Findings**: Keep mental notes of every relevant file path, function name, pattern, and architectural decision you discover. You will reference these in the ticket.\n\n### Research Depth Guidelines\n- Read at least 5-15 relevant source files in full, more if the problem is complex\n- Follow import chains to understand dependencies\n- Check test files to understand expected behaviors and testing patterns\n- Review configuration and metadata files if relevant\n- Search for TODO comments, known limitations, or related existing issues in the code\n\n### Consuming a Comp→Codebase Map (optional upstream input)\n\nYou may be handed a precomputed comp→codebase map (`comp-analysis.json`) produced by an **upstream orchestrating vision step** (the recipe's `comp-analysis.md` step, or the `/write-ticket` Stage 0.5 pre-draft pass). That upstream step is a frontier vision model that already opened the design comp, classified it, and mapped its regions to concrete existing code. You remain **text-only**: you **must not open images**, embed images, download attachments, or perform any vision analysis yourself — you only read the JSON map as focused research input.\n\n- **When the map is missing or has `applicable: false`** (a backend-only request, a no-comp request, a non-design request, or a degraded/unreadable comp): **ignore the artifact entirely**. Do NOT mention comp analysis, design comps, visual fidelity, map artifacts, or image-derived requirements at all — unless the user's original request independently requires those materials. A backend-only or no-comp ticket must read exactly as it would with no map present.\n- **When the map has `applicable: true`**: read it in full before drafting and treat it as authoritative, focused research. Before citing any file the map names (component, template, token, or route), **inspect/read that concrete file yourself** — the standing rule that you do not make up file paths, function names, components, tokens, or routes still applies to map-sourced references.\n- **Class-appropriate depth** (mirror the map's `fidelity_classification.class`, the same shared taxonomy the downstream final plan reviewer uses):\n - `full comp` (confident) → you may write exact component/template/token/route Requirements.\n - `wireframe` → write layout/structure Requirements only; defer color, type, spacing, and component polish to the repo design system, not the wireframe.\n - `annotated-screenshot-of-existing-UI` → write delta-only Requirements (change only the annotated region; preserve the rest).\n - `unknown` / low confidence → use the design-system floor rather than pixel-exact Requirements.\n\n Hard rule: exact/strict mapping depth is used ONLY for a confidently-classified full comp. Fail toward the design system, never toward reproducing an ambiguous image.\n\n## Phase 2: Write the Jira Ticket\n\nAfter completing research, create a new markdown file with the ticket. Use the naming convention `tickets/TICKET-<short-descriptive-name>.md`. If the `tickets/` directory does not exist, create it.\n\n### Ticket Structure\n\nThe markdown file MUST contain exactly these sections:\n\n```markdown\n# [Concise Title Describing the Task]\n\n## Summary\n\n[2-4 sentences describing what this task is about, why it matters, and the high-level approach. Be specific — reference the actual system components involved.]\n\n## Requirements\n\n[Numbered list of specific, actionable requirements. Each requirement should be a clear unit of work.]\n\n1. **[Requirement Title]**: [Description of what needs to be done.]\n - *Relevant code*: `path/to/file.js` — `functionName()` [brief note on how this code relates]\n - *Relevant code*: `path/to/other/file.js` — [brief note]\n\n2. **[Requirement Title]**: [Description]\n - *Relevant code*: ...\n\n[Continue for all requirements]\n\n## Acceptance Criteria\n\n[Bullet list. Each criterion is a testable, verifiable condition.]\n\n- [Specific, testable criterion]\n- [Another criterion]\n- [Continue as needed]\n\n## Materials & Access\n\n[Trailing audit-trail section — always the LAST section of the draft. Inventory every material the ticket references, grouped by source. Use monospace backticks for file paths and other technical provenance. Redact any embedded secrets.]\n\n### Reachable Local Files\n\n[Only files NOT tracked in version control. Do NOT list version-controlled code or in-repo docs here — those are already in the repo and are cited inline as *Relevant code*.]\n\n- `path/to/local/file.ext` — [what it is; not in version control; will be gathered and attached post-create]\n\n### External/Auth-Gated Links\n\n- [Name or purpose] — `https://example.com/...` (record-only; external/auth-gated)\n\n### Design/UI Comps (Fetchable)\n\n- `attachment_id: 10421` — `checkout-comp.png` (`image/png`); fetch via the Jira attachment download capability into a worktree `file_path` at implementation time.\n\n### Binary/Image Materials (Record-Only)\n\n- `path/to/screenshot.png` — [sanitized location/access note; not attached]\n```\n\n### Writing Guidelines\n\n**Summary**:\n- Be concrete, not abstract. Name the actual components, cartridges, or subsystems involved.\n- State the \"why\" — what problem does this solve or what value does it add?\n- Mention the general technical approach if it's clear from the research.\n\n**Requirements**:\n- Each requirement should represent a logical unit of work\n- Order requirements in a logical implementation sequence when possible\n- ALWAYS cite relevant existing files and functions when they exist. Use exact file paths relative to the project root.\n- Explain HOW the existing code relates: \"extend this function\", \"follow this pattern\", \"reuse this helper\", \"modify this configuration\"\n- If a requirement involves creating new files, suggest where they should live based on existing project structure conventions\n- Be specific about what needs to change vs. what needs to be created new\n- Include requirements for tests, documentation, and configuration/metadata changes if applicable\n- **Design/UI Requirements (when an `applicable: true` comp→codebase map is provided)**: cite the mapped components, Jinja2 templates, CSS/SCSS tokens or design-system styles, and routes from the map with concrete phrasing — \"reuse `X` component\", \"extend template `Y`\", \"use token/style `Z`\", \"wire route `R`\" — so the ticket expresses HOW to realize the comp in code that already exists, not generic \"match the comp\" prose. Keep the depth class-appropriate per the map's classification.\n\n**Acceptance Criteria**:\n- Every criterion must be independently verifiable\n- Cover functional requirements, edge cases, testing, and non-functional requirements\n- Include criteria for backwards compatibility if relevant\n- Include criteria for test coverage\n- Use plain `-` bullets (Jira's ADF has no native checkbox, so `- [ ]` renders as literal text)\n- **Design/UI tickets**: whenever the ticket references or attaches a design comp (mockup, wireframe, or design/UI reference), ALWAYS include an explicit **visual-fidelity acceptance criterion**. Word it so the implementing agent must fetch/open the comp by its `attachment_id` or path and verify **class-appropriate** visual fidelity against it — strict pixel/visual match only for a full comp; layout-only for a wireframe; current-state-plus-delta for an annotated screenshot; the repo design-system floor otherwise. Do not settle for inert \"record-only\" prose that the implementing agent cannot act on. When an `applicable: true` comp→codebase map (`comp-analysis.json`) is available, the criterion should reference BOTH the concrete comp source AND the comp→codebase map, so the implementing agent verifies fidelity against the same components/tokens the Requirements already cite rather than a bare \"match the comp\".\n\n**Materials Completeness Inventory**:\n- After the draft is written, INVENTORY every material the ticket references: local file paths, URLs/links, named docs/designs, screenshots, and specs. This pass only INVENTORIES and RECORDS — it does NOT attach anything. The actual attachment of reachable local files happens post-create (after the Jira `ticket_key` exists) via a separate gather-and-attach step.\n- Classify each material by source using a scheme-based rule (no network probe required):\n - **Local filesystem paths** named in the ticket body are the only **low-risk** materials — but ONLY when the file is **not tracked in version control**. Before listing a local file as attachable, determine its VCS status by running `git ls-files --error-unmatch -- <path>` (exit code `0` means the file is tracked). A version-controlled file is **already available in the repository** — source code, in-repo docs, configs, and any other committed file — and **MUST NOT be attached**; it is cited inline as *Relevant code* in Requirements instead of being re-uploaded. Only local files that are **not tracked in version control** (external technical docs/specs, design comps, or generated artifacts a reviewer dropped locally — including files outside any repo, untracked, or gitignored) are eligible to be gathered and attached post-create. **Never upload code** or any file already in version control.\n - Every **`http(s)` URI is external/auth-gated** — regardless of whether the user explicitly linked it (an explicitly-linked Confluence or Google Doc URL is still external/auth-gated) — and is **record-only** here.\n - **Binary/image materials** (ordinary screenshots, PDFs, and unrelated binaries) are **record-only** — document them with sanitized location/access notes; do NOT attempt to attach them. This record-only rule does not apply to local design/UI comp images (see next bullet).\n - **Design/UI comps** (a mockup, wireframe, or design reference for a design/UI ticket) are the exception to record-only: when the comp has an `attachment_id`, local path, or other executable fetch path, record it as a **fetchable reference** so the implementing agent can download it into its worktree and open it. For a Jira attachment comp, record its `attachment_id`, filename, and MIME type when known, plus a note that the executor should use the Jira attachment download capability to save it to a worktree `file_path`. A **local design/UI comp image** — a reachable local file whose executable local path resolves and whose extension maps to an allowlisted image MIME type (`image/png`, `image/jpeg`, `image/webp`, `image/gif`) — is also recorded under *Design/UI Comps (Fetchable)* and is additionally eligible for post-create attachment through the allowlisted binary upload path (the gather-and-attach step uploads it, not just references it). An external/auth-gated design link or a Jira `attachment_id` reference on another ticket remains fetchable/reference material for implementation-time download, not a local re-upload target. Ordinary screenshots/PDFs/unrelated binaries with no fetch path and no design relevance stay record-only.\n - **Comp→codebase map** (`comp-analysis.json`): when the final ticket references the map, inventory it as a **reachable local text file** under *Reachable Local Files* (it is a low-risk local JSON text artifact, gatherable like any other local file — distinct from the design-comp material exception above, which governs the fetchable image itself). The version-control gate still applies: attach it only when it is **not tracked in version control** (a generated artifact normally is not); if it happens to be committed, it is already available in the repo and is not re-uploaded.\n- Write the trailing `## Materials & Access` section (the LAST section of the draft) grouping items under the sub-headings *Reachable Local Files*, *External/Auth-Gated Links*, *Design/UI Comps (Fetchable)* (only when a fetchable design/UI comp exists), and *Binary/Image Materials (Record-Only)*, using bulleted lists. Use monospace formatting (backticks) for technical provenance such as file paths.\n- **Redact secrets before writing anything**: before writing any URL or access note, sanitize and redact embedded credentials, SAS tokens, API keys, and basic-auth secrets using a high-visibility placeholder such as `[REDACTED_TOKEN]`. A location/access note must NEVER expose a plaintext secret.\n\n### Regression Completeness Pass (Gated)\n\nAfter the draft (including its `## Materials & Access` section) is written, run this pass. It is a non-blocking, **warn-not-halt** completeness check — it never blocks or fails ticket creation, and it never modifies the Requirements or Acceptance Criteria text directly.\n\n1. **Check the gate first.** Call the `config_field` MCP tool with `operation` set to `\"get\"` and `field_name` set to `enable_regression_checks`. If the tool returns an error, `null`, or any value other than the literal string `\"true\"`, **skip this entire pass** — the draft is produced exactly as it would be without this section (byte-for-byte unchanged). The recommended default for this flag is OFF (unset) for safe rollout; only proceed past this step when it is explicitly `\"true\"`.\n\n2. **Derive the touched-symbol set.** From the draft's Requirements and *Relevant code* citations (or, if the ticket references an existing diff/PR, that diff/PR), extract the specific function/class/symbol names the proposed change touches.\n\n3. **Run the deterministic core.** Execute:\n ```bash\n npx -y @bridge_gpt/mcp-server regression-check --mode lightweight --json --symbols <derived,symbol,names>\n ```\n This is the same subcommand the standalone `regression-reviewer` agent and `regression-check` command use — do not hand-roll your own `ast-grep`/`ripgrep` discovery.\n\n4. **Fail-open on a degraded or failed run.** If the command errors, or `summary.degraded_flags` is non-empty, record that the pass ran degraded (or could not run) and proceed — the draft is still produced. Never halt ticket creation because this subcommand was unavailable.\n\n5. **Cross-check against Requirements + Acceptance Criteria.** Parse the JSON `findings` array (`symbol`, `call_sites.by_file`, `broad_mentions`). For each symbol, compare its real call-sites and broad mentions against what the draft's Requirements and Acceptance Criteria already cover. Flag any affected caller, migration, or contract (a file with a real call-site or an uninspected broad mention) that the criteria do NOT mention.\n\n6. **Record the flags — never rewrite Requirements/Acceptance Criteria.** Append a `[WARNING]` block immediately before the `## Materials & Access` section, listing each flagged item:\n ```markdown\n ## Regression Completeness Notes\n\n [WARNING] The following systems were not explicitly addressed in the Requirements or Acceptance Criteria above:\n - `path/to/affected_caller.py` — calls `changed_symbol` (N real call-sites); not mentioned in Requirements\n - `path/to/config.yml` — broad mention of `changed_symbol`; verify this reference is unaffected\n\n Degraded: [list summary.degraded_flags, or \"none — full structural analysis ran\"]\n ```\n If no flags were raised and the run was not degraded, write a single line instead: `Regression completeness pass: no unaddressed systems found.` If the run was degraded with zero findings either way, state that explicitly rather than implying a clean pass.\n\n### Output Formatting (Jira upload)\n\nThe ticket is uploaded to Jira, which converts the Markdown to Atlassian Document Format (ADF) and hard-caps the description at **32,767 characters**. Keep the output clean and within budget:\n\n- **Length**: aim for under ~30,000 characters. If the scope genuinely needs more, split into a parent ticket plus sub-tickets rather than one oversized ticket.\n- **Acceptance Criteria**: plain `-` bullets, not `- [ ]` (ADF has no native checkbox).\n- **No images**: do not embed images or use relative image links. This \"No images\" rule applies strictly to inline images in the description body; it does NOT restrict the attachments produced by the Materials Completeness Inventory / gather-and-attach pass, nor does it forbid recording a fetchable design/UI comp reference (its `attachment_id` or path).\n- **No empty headings**: every heading must have text on its line.\n- **Placeholders**: prefer `{placeholder}` over `<placeholder>`.\n\n## Quality Standards\n\n- **No vague language**: Replace \"should handle errors properly\" with \"should catch LLM provider timeouts and return a normalized error response with errorType 'TimeoutError'\"\n- **No assumptions without evidence**: Only reference code you actually read during research. If you're unsure about something, say so explicitly in the ticket.\n- **Appropriate scope**: The ticket should represent a coherent, deliverable unit of work. If the problem is too large, note that it may need to be broken into sub-tasks, but still write the parent ticket.\n- **Developer empathy**: Write as if the developer picking this up has general project knowledge but hasn't recently worked on this specific area. Give them enough context to get started quickly.\n\n## Important Reminders\n\n- Do NOT skip or abbreviate the research phase. The quality of the ticket depends entirely on the depth of your codebase understanding.\n- Do NOT make up file paths or function names. Only reference code you have actually found and read.\n- DO create the markdown file — do not just output the content to the chat. Write it to disk.\n- If the project has specific conventions (from CLAUDE.md or similar), ensure your ticket's requirements align with those conventions.\n"
12
12
  },
13
13
  "refactor-reviewer": {
14
14
  "frontmatter": {
@@ -0,0 +1,44 @@
1
+ // Shared helper for surfacing non-blocking backend warnings (BAPI-545) as
2
+ // user-visible MCP tool text. The Tier-4 ticket-text routes (PRD, FSD,
3
+ // clarifying questions, ticket critique, ticket review, create_doc) attach a
4
+ // disclosure to their JSON response body when no VCS connection exists but
5
+ // still succeed — this never changes tool success/failure status, it only
6
+ // appends non-blocking text.
7
+ // Extracts warning strings from a parsed backend JSON response. Accepts the
8
+ // real backend shape (a singular `warning: string | null` field, as used by
9
+ // the Tier-4 trigger endpoints' MessageResponse) and, defensively, a
10
+ // `warnings` array of strings or `{ message: string }` objects (in case a
11
+ // future backend response adopts the list shape instead). Invalid/empty
12
+ // values are ignored; never throws.
13
+ export function extractBackendWarnings(body) {
14
+ if (body === null || typeof body !== "object") {
15
+ return [];
16
+ }
17
+ const record = body;
18
+ const warnings = [];
19
+ if (typeof record.warning === "string" && record.warning) {
20
+ warnings.push(record.warning);
21
+ }
22
+ if (Array.isArray(record.warnings)) {
23
+ for (const entry of record.warnings) {
24
+ if (typeof entry === "string" && entry) {
25
+ warnings.push(entry);
26
+ }
27
+ else if (entry !== null &&
28
+ typeof entry === "object" &&
29
+ typeof entry.message === "string" &&
30
+ entry.message) {
31
+ warnings.push(entry.message);
32
+ }
33
+ }
34
+ }
35
+ return warnings;
36
+ }
37
+ // Appends a non-blocking warning section to existing tool output text.
38
+ // Returns `text` unchanged (byte-for-byte) when `warnings` is empty.
39
+ export function appendBackendWarningsToText(text, warnings) {
40
+ if (!warnings.length) {
41
+ return text;
42
+ }
43
+ return `${text}\n\n**Warning:** ${warnings.join(" ")}`;
44
+ }