@bridge_gpt/mcp-server 0.2.14 → 0.2.18

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 (51) hide show
  1. package/README.md +2 -2
  2. package/build/agents.generated.js +19 -1
  3. package/build/commands.generated.js +6 -5
  4. package/build/conductor/bridge-api-client.js +191 -11
  5. package/build/conductor/claude-hook.js +22 -4
  6. package/build/conductor/cli.js +11 -13
  7. package/build/conductor/done-gate.js +5 -0
  8. package/build/conductor/epic-reconcile.js +62 -13
  9. package/build/conductor/epic-runtime.js +447 -35
  10. package/build/conductor/epic-state.js +517 -63
  11. package/build/conductor/errors.js +41 -0
  12. package/build/conductor/event-accessors.js +234 -0
  13. package/build/conductor/file-scope-guard.js +201 -0
  14. package/build/conductor/github-mergeability.js +85 -0
  15. package/build/conductor/local-merge.js +47 -1
  16. package/build/conductor/merge-identity.js +41 -0
  17. package/build/conductor/merge-ledger.js +13 -68
  18. package/build/conductor/plan.js +12 -2
  19. package/build/conductor/pr-discovery.js +11 -1
  20. package/build/conductor/supervisor-config.js +4 -39
  21. package/build/conductor/supervisor-escalation.js +10 -26
  22. package/build/conductor/supervisor-ledger.js +5 -12
  23. package/build/conductor/supervisor-message-relay.js +2 -5
  24. package/build/conductor/supervisor-notification.js +1 -1
  25. package/build/conductor/supervisor-runtime.js +12 -54
  26. package/build/conductor/supervisor-state.js +4 -18
  27. package/build/conductor/supervisor-types.js +2 -2
  28. package/build/conductor/taxonomy.js +4 -0
  29. package/build/conductor-bin.js +2333 -666
  30. package/build/conductor-claude-hook-bin.js +4 -2
  31. package/build/doctor.js +32 -0
  32. package/build/index.js +9985 -7579
  33. package/build/install-bridge.js +25 -8
  34. package/build/install-doctor.js +387 -0
  35. package/build/pipelines.generated.js +30 -5
  36. package/build/readme.generated.js +1 -1
  37. package/build/regression-check.js +872 -0
  38. package/build/review-tickets.js +175 -21
  39. package/build/sfcc/permissions.js +13 -1
  40. package/build/sfcc/reads-custom-object-def.js +56 -39
  41. package/build/sfcc/register.js +1 -1
  42. package/build/sfcc/tool-wrapper.js +5 -1
  43. package/build/start-tickets-conductor.js +22 -6
  44. package/build/start-tickets-prereqs.js +73 -0
  45. package/build/start-tickets.js +122 -22
  46. package/build/version.generated.js +1 -1
  47. package/package.json +5 -5
  48. package/pipelines/review-ticket.json +24 -2
  49. package/public/css/main.min.css +3272 -1
  50. package/public/css/main.min.css.map +1 -1
  51. package/smoke-test/SMOKE-TEST.md +4 -2
@@ -15,10 +15,12 @@
15
15
  * This path is worktree-free and base-branch-free; the human CLI path leaves
16
16
  * `epic` unset and retains byte-for-byte identical behavior.
17
17
  */
18
- import { createDefaultStartTicketsDeps, detectTerminal, runWithConcurrency, buildAgentInvocation, shSquoteInner, powershellSquote, isCommandOnPath, resolveFirstCommandOnPath, tmuxSessionNameForTicket, TICKET_KEY_PATTERN, DEFAULT_MAX_PARALLEL, } from "./start-tickets.js";
18
+ import { createDefaultStartTicketsDeps, detectTerminal, runWithConcurrency, buildAgentInvocation, shSquoteInner, powershellSquote, isCommandOnPath, resolveFirstCommandOnPath, tmuxSessionNameForTicket, TICKET_KEY_PATTERN, DEFAULT_MAX_PARALLEL, validateBranchName, fetchAndResolveBaseSha, fetchStartTicketsConfigField, resolveStartTicketsBridgeApiAccess, } from "./start-tickets.js";
19
19
  import { WINDOWS_TERMINAL_COMMAND, WINDOWS_POWERSHELL_CANDIDATES, TMUX_COMMAND, isSupportedStartTicketsPlatform, unsupportedPlatformMessage, } from "./start-tickets-prereqs.js";
20
20
  import { DEFAULT_AGENT_NAME, resolveAgentSpec, isAgentName, formatValidAgentNames, isValidModelAlias, } from "./agent-registry.js";
21
21
  import { injectConductorEnvIntoShellCommand, buildEpicIdentityEnv, mintStartTicketsRunId, } from "./start-tickets-conductor.js";
22
+ /** Default base context: no flags threaded through (matches pre-BAPI-474 behavior). */
23
+ const NO_BASE_CONTEXT = { noRefreshBase: false };
22
24
  // ---------------------------------------------------------------------------
23
25
  // Usage
24
26
  // ---------------------------------------------------------------------------
@@ -35,6 +37,8 @@ export function getReviewTicketsUsage() {
35
37
  " --model VALUE Model alias to pass as --model to the agent (optional passthrough)",
36
38
  " --max-parallel N Max tabs to spawn concurrently (default: 3)",
37
39
  " --dry-run Print intended commands; creates no tabs and opens no sessions",
40
+ " --base-branch BRANCH Base branch to fetch+pin for grounding (default: config base_branch, else \"main\")",
41
+ " --no-refresh-base Skip the parent-fetch-once base pin; restores git-free, in-place-grounded behavior",
38
42
  " -h, --help Show this help",
39
43
  "",
40
44
  "Examples:",
@@ -42,19 +46,107 @@ export function getReviewTicketsUsage() {
42
46
  " npx -y @bridge_gpt/mcp-server review-tickets --auto --rounds=1 BAPI-1 BAPI-2",
43
47
  " npx -y @bridge_gpt/mcp-server review-tickets --review BAPI-1=auto,rounds=1 --review BAPI-2=rounds=2 BAPI-1 BAPI-2",
44
48
  " npx -y @bridge_gpt/mcp-server review-tickets --dry-run BAPI-1 BAPI-2",
49
+ " npx -y @bridge_gpt/mcp-server review-tickets --base-branch=develop BAPI-1 BAPI-2",
50
+ " npx -y @bridge_gpt/mcp-server review-tickets --no-refresh-base BAPI-1 BAPI-2",
45
51
  "",
46
52
  "Each KEY must match [A-Z]+-[0-9]+ (e.g., BAPI-248).",
47
53
  "",
48
- "Prerequisites (terminal launcher only — no wt, git-wt, git, or Worktrunk required):",
49
- " macOS osascript",
50
- " Windows wt.exe or PowerShell",
51
- " Linux tmux",
54
+ "Prerequisites (terminal launcher, plus git — no wt, git-wt, or Worktrunk required):",
55
+ " macOS osascript, git",
56
+ " Windows wt.exe or PowerShell, git",
57
+ " Linux tmux, git",
52
58
  "",
53
- "review-tickets runs all tabs in the current repository cwd and creates no Worktrunk worktrees.",
54
- "--dry-run works on any platform, including unsupported ones.",
59
+ "review-tickets runs all tabs in the current repository cwd and creates no Worktrunk worktrees. It now",
60
+ "requires git on PATH (BAPI-474): before spawning tabs it fetches origin/<base_branch> once and pins a",
61
+ "single base_sha for the whole batch, so every spawned review grounds against the same base tree even if",
62
+ "origin advances mid-batch. Pass --no-refresh-base to skip this and restore the prior git-free behavior.",
63
+ "--dry-run works on any platform, including unsupported ones, and never performs the base-branch fetch.",
55
64
  ].join("\n");
56
65
  }
57
66
  // ---------------------------------------------------------------------------
67
+ // Fail-loud base-fetch diagnostic (BAPI-474 D-4)
68
+ // ---------------------------------------------------------------------------
69
+ const ANSI_BOLD = "\x1b[1m";
70
+ // Warm Coral — a high-contrast 256-color red/coral used exclusively for this
71
+ // fail-loud alert box so a base-branch fetch failure is unmistakable in a scan
72
+ // of scrollback, without hijacking the terminal's general error-red semantics.
73
+ const ANSI_WARM_CORAL = "\x1b[38;5;203m";
74
+ const ANSI_RESET = "\x1b[0m";
75
+ /**
76
+ * Render a bordered, high-contrast fail-loud alert box for a parent-fetch-once
77
+ * failure (BAPI-474 D-4 "Design Direction Fail-Loud"): an operation-failed
78
+ * header, the target base branch, and an actionable remediation line. Pure
79
+ * formatting (no I/O) so it stays unit-testable; the caller is responsible for
80
+ * writing it to stderr.
81
+ */
82
+ export function formatBaseFetchFailureBox(baseBranch, error) {
83
+ const title = "review-tickets: base-branch fetch failed";
84
+ // The box always shows a dedicated Remediation line below. fetchAndResolveBaseSha's
85
+ // own error text embeds the same remediation clause because it also has to stand
86
+ // alone when surfaced outside a box (e.g. raw JSON from materialize_fresh_base) —
87
+ // strip it here so the boxed rendering doesn't say it twice.
88
+ const reason = error
89
+ .replace(/\s*Check your network and 'git remote get-url origin', or pass --no-refresh-base to skip\.?\s*$/i, "")
90
+ .trim();
91
+ const bodyLines = [
92
+ `Target base branch: origin/${baseBranch}`,
93
+ `Reason: ${reason}`,
94
+ "Remediation: check your network and 'git remote get-url origin', or rerun with --no-refresh-base to evaluate in-place.",
95
+ ];
96
+ const contentWidth = Math.max(title.length + 2, ...bodyLines.map((l) => l.length)) + 2;
97
+ const horizontal = "─".repeat(contentWidth);
98
+ const pad = (s) => `│ ${s}${" ".repeat(contentWidth - s.length - 1)}│`;
99
+ const lines = [
100
+ `╭${horizontal}╮`,
101
+ pad(`✖ ${title}`),
102
+ `├${horizontal}┤`,
103
+ ...bodyLines.map((l) => pad(l)),
104
+ `╰${horizontal}╯`,
105
+ ];
106
+ return `${ANSI_BOLD}${ANSI_WARM_CORAL}${lines.join("\n")}${ANSI_RESET}`;
107
+ }
108
+ /**
109
+ * Resolve ONE base_branch/base_sha pin for the whole review-tickets batch
110
+ * BEFORE any tab is spawned (D-3), so a mid-batch `origin` advance can never
111
+ * mix bases within one run. Skipped entirely (never fetches) when
112
+ * `--no-refresh-base` was passed. base_branch precedence: `--base-branch` flag
113
+ * > config field `base_branch` (BAPI-313) > `"main"`. The config-field lookup
114
+ * is best-effort — any failure there falls through to `"main"`; only the git
115
+ * fetch/resolve itself is a hard failure (the caller applies the D-4 fail-loud
116
+ * response to that).
117
+ */
118
+ export async function resolveBatchBaseContext(deps, options) {
119
+ if (options.noRefreshBase) {
120
+ return { ok: true, context: { noRefreshBase: true } };
121
+ }
122
+ let effectiveBaseBranch = (options.baseBranch ?? "").trim();
123
+ if (effectiveBaseBranch.length === 0) {
124
+ try {
125
+ const accessResult = await resolveStartTicketsBridgeApiAccess(deps);
126
+ if (accessResult.ok) {
127
+ const configValue = await fetchStartTicketsConfigField(accessResult.access, "base_branch");
128
+ if (typeof configValue === "string" && configValue.trim().length > 0) {
129
+ effectiveBaseBranch = configValue.trim();
130
+ }
131
+ }
132
+ }
133
+ catch {
134
+ /* config-field lookup is best-effort; fall through to the "main" default */
135
+ }
136
+ }
137
+ if (effectiveBaseBranch.length === 0) {
138
+ effectiveBaseBranch = "main";
139
+ }
140
+ const fetchResult = await fetchAndResolveBaseSha(deps, effectiveBaseBranch);
141
+ if (!fetchResult.ok) {
142
+ return { ok: false, baseBranch: effectiveBaseBranch, error: fetchResult.error };
143
+ }
144
+ return {
145
+ ok: true,
146
+ context: { baseBranch: effectiveBaseBranch, baseSha: fetchResult.base_sha, noRefreshBase: false },
147
+ };
148
+ }
149
+ // ---------------------------------------------------------------------------
58
150
  // Argument parsing
59
151
  // ---------------------------------------------------------------------------
60
152
  export function parseReviewOverrideEntry(entry, requestedKeys) {
@@ -105,6 +197,8 @@ export function parseReviewTicketsArgs(argv) {
105
197
  let maxParallelRaw;
106
198
  let agentName = DEFAULT_AGENT_NAME;
107
199
  let modelAlias;
200
+ let noRefreshBase = false;
201
+ let baseBranch;
108
202
  const keys = [];
109
203
  const rawReviewEntries = [];
110
204
  for (let i = 0; i < argv.length; i++) {
@@ -123,6 +217,30 @@ export function parseReviewTicketsArgs(argv) {
123
217
  globalAuto = true;
124
218
  continue;
125
219
  }
220
+ if (arg === "--no-refresh-base") {
221
+ noRefreshBase = true;
222
+ continue;
223
+ }
224
+ if (arg === "--base-branch" || arg.startsWith("--base-branch=")) {
225
+ let value;
226
+ if (arg.startsWith("--base-branch=")) {
227
+ value = arg.slice("--base-branch=".length);
228
+ }
229
+ else {
230
+ const next = i + 1 < argv.length ? argv[i + 1] : undefined;
231
+ if (next === undefined || next.startsWith("-")) {
232
+ return { status: "error", message: "--base-branch requires a value (a branch name)." };
233
+ }
234
+ value = takeValue();
235
+ }
236
+ const trimmed = (value ?? "").trim();
237
+ const error = validateBranchName(trimmed);
238
+ if (error) {
239
+ return { status: "error", message: `Invalid --base-branch value: ${error}` };
240
+ }
241
+ baseBranch = trimmed;
242
+ continue;
243
+ }
126
244
  if (arg === "--rounds" || arg.startsWith("--rounds=")) {
127
245
  let value;
128
246
  if (arg.startsWith("--rounds=")) {
@@ -267,6 +385,8 @@ export function parseReviewTicketsArgs(argv) {
267
385
  auto: globalAuto,
268
386
  rounds: globalRounds,
269
387
  reviewOverrides,
388
+ noRefreshBase,
389
+ baseBranch,
270
390
  },
271
391
  };
272
392
  }
@@ -289,24 +409,27 @@ export function resolveEffectiveReviewMode(key, options) {
289
409
  // hard-wired to `/implement-ticket`. Review-aware builders are used instead so
290
410
  // the prompt uses `/review-ticket` and no directory change is prepended.
291
411
  // ---------------------------------------------------------------------------
292
- export function buildReviewTicketPrompt(key, mode) {
412
+ export function buildReviewTicketPrompt(key, mode, base = NO_BASE_CONTEXT) {
293
413
  const autoFlag = mode.auto ? " --auto" : "";
294
- return `/review-ticket ${key}${autoFlag} --rounds=${mode.rounds}`;
414
+ const baseBranchFlag = base.baseBranch ? ` --base-branch=${base.baseBranch}` : "";
415
+ const baseShaFlag = base.baseSha ? ` --base-sha=${base.baseSha}` : "";
416
+ const noRefreshFlag = base.noRefreshBase ? " --no-refresh-base" : "";
417
+ return `/review-ticket ${key}${autoFlag} --rounds=${mode.rounds}${baseBranchFlag}${baseShaFlag}${noRefreshFlag}`;
295
418
  }
296
- export function buildPosixReviewAgentShellCommand(agent, key, mode, cwd, modelAlias) {
297
- const prompt = buildReviewTicketPrompt(key, mode);
419
+ export function buildPosixReviewAgentShellCommand(agent, key, mode, cwd, modelAlias, base = NO_BASE_CONTEXT) {
420
+ const prompt = buildReviewTicketPrompt(key, mode, base);
298
421
  const invocation = buildAgentInvocation(agent, prompt, (p) => `'${shSquoteInner(p)}'`, modelAlias);
299
422
  return `cd '${shSquoteInner(cwd)}' && ${invocation}`;
300
423
  }
301
- export function buildPowerShellReviewAgentShellCommand(agent, key, mode, modelAlias) {
302
- const prompt = buildReviewTicketPrompt(key, mode);
424
+ export function buildPowerShellReviewAgentShellCommand(agent, key, mode, modelAlias, base = NO_BASE_CONTEXT) {
425
+ const prompt = buildReviewTicketPrompt(key, mode, base);
303
426
  return buildAgentInvocation(agent, prompt, powershellSquote, modelAlias);
304
427
  }
305
- export function buildReviewAgentShellCommand(agent, key, mode, platform, cwd, modelAlias) {
428
+ export function buildReviewAgentShellCommand(agent, key, mode, platform, cwd, modelAlias, base = NO_BASE_CONTEXT) {
306
429
  if (platform === "win32") {
307
- return buildPowerShellReviewAgentShellCommand(agent, key, mode, modelAlias);
430
+ return buildPowerShellReviewAgentShellCommand(agent, key, mode, modelAlias, base);
308
431
  }
309
- return buildPosixReviewAgentShellCommand(agent, key, mode, cwd, modelAlias);
432
+ return buildPosixReviewAgentShellCommand(agent, key, mode, cwd, modelAlias, base);
310
433
  }
311
434
  // ---------------------------------------------------------------------------
312
435
  // Unsupported platform message for review-tickets
@@ -325,6 +448,16 @@ export async function runReviewTicketsPreflight(deps, options) {
325
448
  if (!isSupportedStartTicketsPlatform(deps.platform)) {
326
449
  return { ok: false, error: unsupportedReviewTicketsPlatformMessage(deps.platform) };
327
450
  }
451
+ // BAPI-474: the parent-fetch-once base pin adds a git prerequisite unless the
452
+ // caller opted out via --no-refresh-base. Checked here (before any tab is
453
+ // spawned) so a missing git fails loud with a clear, actionable message.
454
+ if (!options.noRefreshBase && !(await isCommandOnPath(deps, "git"))) {
455
+ return {
456
+ ok: false,
457
+ error: "git is required for review-tickets' parent-fetch-once base-branch pin (BAPI-474) but was not " +
458
+ "found on PATH. Install git, or rerun with --no-refresh-base to skip the fetch and evaluate in-place.",
459
+ };
460
+ }
328
461
  if (deps.platform === "darwin") {
329
462
  if (await isCommandOnPath(deps, "osascript"))
330
463
  return { ok: true };
@@ -355,10 +488,10 @@ export async function runReviewTicketsPreflight(deps, options) {
355
488
  // ---------------------------------------------------------------------------
356
489
  // Plan rows + orchestration
357
490
  // ---------------------------------------------------------------------------
358
- export function buildReviewPlanRows(deps, options, agent, status, overrides = {}) {
491
+ export function buildReviewPlanRows(deps, options, agent, status, overrides = {}, baseContext = NO_BASE_CONTEXT) {
359
492
  return options.keys.map((key) => {
360
493
  const mode = resolveEffectiveReviewMode(key, options);
361
- const baseCommand = buildReviewAgentShellCommand(agent, key, mode, deps.platform, deps.cwd, options.modelAlias);
494
+ const baseCommand = buildReviewAgentShellCommand(agent, key, mode, deps.platform, deps.cwd, options.modelAlias, baseContext);
362
495
  let command = baseCommand;
363
496
  let runId;
364
497
  if (options.epic) {
@@ -379,6 +512,14 @@ export function buildReviewPlanRows(deps, options, agent, status, overrides = {}
379
512
  };
380
513
  });
381
514
  }
515
+ /** Build a dry-run preview base context from parsed CLI options only — never fetches. */
516
+ function previewBaseContextFromOptions(options) {
517
+ if (options.noRefreshBase)
518
+ return { noRefreshBase: true };
519
+ if (options.baseBranch)
520
+ return { baseBranch: options.baseBranch, noRefreshBase: false };
521
+ return NO_BASE_CONTEXT;
522
+ }
382
523
  export async function orchestrateReviewTickets(deps, options, overrides = {}) {
383
524
  const agent = resolveAgentSpec(options.agentName);
384
525
  if (!agent) {
@@ -395,7 +536,9 @@ export async function orchestrateReviewTickets(deps, options, overrides = {}) {
395
536
  "and would be re-claimed for each key in the batch. Call orchestrateReviewTickets once per ticket instead.",
396
537
  };
397
538
  }
398
- const rows = buildReviewPlanRows(deps, options, agent, "dry-run", overrides);
539
+ // --dry-run never performs the base-branch fetch (side-effect-free preview);
540
+ // it reflects only what the user explicitly passed on the CLI.
541
+ const rows = buildReviewPlanRows(deps, options, agent, "dry-run", overrides, previewBaseContextFromOptions(options));
399
542
  return { ok: true, rows };
400
543
  }
401
544
  const preflight = await runReviewTicketsPreflight(deps, options);
@@ -411,10 +554,21 @@ export async function orchestrateReviewTickets(deps, options, overrides = {}) {
411
554
  "and would be re-claimed for each key in the batch. Call orchestrateReviewTickets once per ticket instead.",
412
555
  };
413
556
  }
557
+ // BAPI-474 D-3: parent-fetch-once — resolve ONE base_branch/base_sha pin for
558
+ // the whole batch before any tab is spawned. D-4: on failure (and no
559
+ // --no-refresh-base), fail loud with a bordered diagnostic and halt the
560
+ // fan-out entirely rather than silently grounding in place.
561
+ const resolveContext = overrides.resolveBaseContext ?? resolveBatchBaseContext;
562
+ const baseContextResult = await resolveContext(deps, options);
563
+ if (!baseContextResult.ok) {
564
+ console.error(formatBaseFetchFailureBox(baseContextResult.baseBranch, baseContextResult.error));
565
+ return { ok: false, error: baseContextResult.error };
566
+ }
567
+ const baseContext = baseContextResult.context;
414
568
  const terminal = detectTerminal(undefined, deps.env);
415
569
  const rows = await runWithConcurrency(options.keys, options.maxParallel, async (key) => {
416
570
  const mode = resolveEffectiveReviewMode(key, options);
417
- const baseShellCommand = buildReviewAgentShellCommand(agent, key, mode, deps.platform, deps.cwd, options.modelAlias);
571
+ const baseShellCommand = buildReviewAgentShellCommand(agent, key, mode, deps.platform, deps.cwd, options.modelAlias, baseContext);
418
572
  let shellCommand = baseShellCommand;
419
573
  let runId;
420
574
  if (options.epic) {
@@ -565,7 +719,7 @@ export async function runReviewTicketsCli(argv, overrides = {}) {
565
719
  errorLog(`Error: Unknown agent: '${options.agentName}'. Valid agents: ${formatValidAgentNames()}.`);
566
720
  return 1;
567
721
  }
568
- const rows = buildReviewPlanRows(deps, options, agent, "dry-run", overrides.orchestrateOverrides);
722
+ const rows = buildReviewPlanRows(deps, options, agent, "dry-run", overrides.orchestrateOverrides, previewBaseContextFromOptions(options));
569
723
  for (const row of rows) {
570
724
  log(`DRY-RUN: ${row.key} -> ${row.command}`);
571
725
  }
@@ -27,7 +27,19 @@ const OCAPI_SETTINGS_READ_ONLY = (ocapiVersion) => JSON.stringify({
27
27
  },
28
28
  {
29
29
  resource_id: "/system_object_definitions/**",
30
- methods: ["get"],
30
+ methods: ["get", "post"],
31
+ read_attributes: "(**)",
32
+ write_attributes: "(**)",
33
+ },
34
+ {
35
+ resource_id: "/site_preferences/**",
36
+ methods: ["get", "post"],
37
+ read_attributes: "(**)",
38
+ write_attributes: "(**)",
39
+ },
40
+ {
41
+ resource_id: "/custom_object_definitions/**",
42
+ methods: ["get", "post"],
31
43
  read_attributes: "(**)",
32
44
  write_attributes: "(**)",
33
45
  },
@@ -2,18 +2,23 @@
2
2
  * SFCC custom-object-definition introspection read tools (BAPI-402, T4–T5).
3
3
  *
4
4
  * Implements:
5
- * custom_object_definition_list — GET /custom_object_definitions
6
- * custom_object_definition_get GET /custom_object_definitions/{type}
5
+ * custom_object_definition_attributes_get — GET /custom_object_definitions/{type}/attribute_definitions
6
+ * custom_object_definition_attribute_search POST /custom_object_definitions/{type}/attribute_definition_search
7
7
  *
8
8
  * Both tools are read-only, run behind the T1 call-time gate, and route
9
9
  * oversized payloads through the sfcc/output.ts truncate-and-save seam.
10
10
  *
11
+ * OCAPI's CustomObjectDefinitions resource only exposes operations scoped to
12
+ * a known object_type — there is no top-level list and no bare get-by-type
13
+ * (unlike SystemObjectDefinitions). A caller-supplied object_type is always
14
+ * required; OCAPI cannot enumerate custom object types.
15
+ *
11
16
  * Note: custom object TYPE creation/update is NOT available via OCAPI REST.
12
17
  * It is a v2 metadata-import capability (metadata XML → WebDAV → import job).
13
18
  */
14
19
  import path from "path";
15
20
  import { z } from "zod";
16
- import { ocapiGet } from "./client.js";
21
+ import { ocapiGet, ocapiPost } from "./client.js";
17
22
  import { withSfccGate } from "./tool-wrapper.js";
18
23
  import { normalizeOcapiBody } from "./ocapi-shape.js";
19
24
  import { truncateAndSaveIfNeeded } from "./output.js";
@@ -29,14 +34,22 @@ const READ_ANNOTATIONS = {
29
34
  // ---------------------------------------------------------------------------
30
35
  // Input schemas
31
36
  // ---------------------------------------------------------------------------
32
- const customObjectListInput = z.object({
33
- count: z.number().optional().describe("Maximum number of custom object types to return."),
34
- start: z.number().optional().describe("Zero-based offset for paging."),
37
+ const customObjectAttributesGetInput = z.object({
38
+ object_type: z
39
+ .string()
40
+ .describe('Known custom object type identifier, e.g. a custom type id starting with "c_". ' +
41
+ "OCAPI cannot enumerate custom object types — the type must already be known."),
35
42
  });
36
- const customObjectGetInput = z.object({
43
+ const customObjectAttributeSearchInput = z.object({
37
44
  object_type: z
38
45
  .string()
39
- .describe('Custom object type identifier, e.g. "GiftCertificate" or a custom type id starting with "c_".'),
46
+ .describe('Known custom object type to search within, e.g. a custom type id starting with "c_". ' +
47
+ "OCAPI cannot enumerate custom object types — the type must already be known."),
48
+ query: z.union([z.string(), z.record(z.string(), z.any())]).describe("Search query. Pass a plain string for a text search across id and display_name, " +
49
+ "or a structured OCAPI query object (term_query, filtered_query, etc.)."),
50
+ start: z.number().optional().describe("Zero-based offset for paging."),
51
+ count: z.number().optional().describe("Maximum number of results to return."),
52
+ sorts: z.array(z.any()).optional().describe("Array of OCAPI sort descriptors."),
40
53
  });
41
54
  // ---------------------------------------------------------------------------
42
55
  // Helpers
@@ -57,39 +70,43 @@ async function saveAndReturn(text, dir, filename) {
57
70
  // ---------------------------------------------------------------------------
58
71
  // Handlers
59
72
  // ---------------------------------------------------------------------------
60
- function buildCustomObjectListHandler(gateDeps, getDocsDir) {
73
+ function buildCustomObjectAttributesGetHandler(gateDeps, getDocsDir) {
61
74
  return withSfccGate(gateDeps, async (args, credentials) => {
62
- const { count, start } = customObjectListInput.parse(args);
63
- const queryParams = {};
64
- if (count !== undefined)
65
- queryParams.count = String(count);
66
- if (start !== undefined)
67
- queryParams.start = String(start);
68
- const queryStr = Object.keys(queryParams).length > 0
69
- ? "?" + new URLSearchParams(queryParams).toString()
70
- : "";
71
- const result = await ocapiGet(`/custom_object_definitions${queryStr}`, credentials);
75
+ const { object_type } = customObjectAttributesGetInput.parse(args);
76
+ const encodedType = encodeURIComponent(object_type);
77
+ const result = await ocapiGet(`/custom_object_definitions/${encodedType}/attribute_definitions`, credentials);
72
78
  if (!result.ok) {
73
79
  return textResult(JSON.stringify({ error: "OCAPI error", status: result.status, body: result.body }, null, 2));
74
80
  }
75
81
  const normalized = normalizeOcapiBody(result.body);
76
82
  const text = JSON.stringify(normalized, null, 2);
77
83
  const dir = path.join(await getDocsDir(), "sfcc");
78
- return saveAndReturn(text, dir, `custom-object-def-list-${safeTimestamp()}.json`);
84
+ return saveAndReturn(text, dir, `custom-object-def-attributes-${safeType(object_type)}-${safeTimestamp()}.json`);
79
85
  });
80
86
  }
81
- function buildCustomObjectGetHandler(gateDeps, getDocsDir) {
87
+ function buildCustomObjectAttributeSearchHandler(gateDeps, getDocsDir) {
82
88
  return withSfccGate(gateDeps, async (args, credentials) => {
83
- const { object_type } = customObjectGetInput.parse(args);
89
+ const { object_type, query, start, count, sorts } = customObjectAttributeSearchInput.parse(args);
84
90
  const encodedType = encodeURIComponent(object_type);
85
- const result = await ocapiGet(`/custom_object_definitions/${encodedType}`, credentials);
91
+ // Coerce plain string queries into OCAPI text_query shape
92
+ const resolvedQuery = typeof query === "string"
93
+ ? { text_query: { fields: ["id", "display_name"], search_phrase: query } }
94
+ : query;
95
+ const postBody = { query: resolvedQuery };
96
+ if (start !== undefined)
97
+ postBody.start = start;
98
+ if (count !== undefined)
99
+ postBody.count = count;
100
+ if (sorts !== undefined)
101
+ postBody.sorts = sorts;
102
+ const result = await ocapiPost(`/custom_object_definitions/${encodedType}/attribute_definition_search`, postBody, credentials);
86
103
  if (!result.ok) {
87
104
  return textResult(JSON.stringify({ error: "OCAPI error", status: result.status, body: result.body }, null, 2));
88
105
  }
89
106
  const normalized = normalizeOcapiBody(result.body);
90
107
  const text = JSON.stringify(normalized, null, 2);
91
108
  const dir = path.join(await getDocsDir(), "sfcc");
92
- return saveAndReturn(text, dir, `custom-object-def-${safeType(object_type)}-${safeTimestamp()}.json`);
109
+ return saveAndReturn(text, dir, `custom-object-def-search-${safeType(object_type)}-${safeTimestamp()}.json`);
93
110
  });
94
111
  }
95
112
  /**
@@ -99,21 +116,21 @@ function buildCustomObjectGetHandler(gateDeps, getDocsDir) {
99
116
  */
100
117
  export function registerSfccCustomObjectDefReadTools(registerTool, deps) {
101
118
  const { gateDeps, getDocsDir } = deps;
102
- registerTool("custom_object_definition_list", {
103
- description: "List all custom object type definitions from the developer sandbox via " +
104
- "GET /custom_object_definitions. Read-only introspection; type definitions cannot be created " +
105
- "or updated via OCAPI (v2 metadata-import only). " +
106
- "Optional `count` and `start` for paging. Oversized outputs auto-saved locally.",
107
- inputSchema: customObjectListInput,
119
+ registerTool("custom_object_definition_attributes_get", {
120
+ description: "Retrieve attribute definitions for a KNOWN custom object type via " +
121
+ "GET /custom_object_definitions/{type}/attribute_definitions. " +
122
+ "OCAPI cannot enumerate types object_type is required. " +
123
+ "Type creation is v2 metadata-import only, not OCAPI. Oversized payloads auto-saved locally.",
124
+ inputSchema: customObjectAttributesGetInput,
108
125
  annotations: READ_ANNOTATIONS,
109
- }, buildCustomObjectListHandler(gateDeps, getDocsDir));
110
- registerTool("custom_object_definition_get", {
111
- description: "Retrieve an EXISTING custom object type from the developer sandbox via " +
112
- "GET /custom_object_definitions/{type}. " +
113
- "Returns the type with key_definition and attribute_definitions/attribute_groups. " +
114
- "Read-only introspection; type definitions cannot be created or updated via OCAPI " +
115
- "(v2 metadata-import only). Oversized payloads auto-saved locally.",
116
- inputSchema: customObjectGetInput,
126
+ }, buildCustomObjectAttributesGetHandler(gateDeps, getDocsDir));
127
+ registerTool("custom_object_definition_attribute_search", {
128
+ description: "Search attribute definitions within a KNOWN custom object type via " +
129
+ "POST /custom_object_definitions/{type}/attribute_definition_search. " +
130
+ "OCAPI cannot enumerate types object_type is required. " +
131
+ "Pass a string or structured OCAPI query. Type creation is v2 metadata-import only. " +
132
+ "Oversized results auto-saved locally.",
133
+ inputSchema: customObjectAttributeSearchInput,
117
134
  annotations: READ_ANNOTATIONS,
118
- }, buildCustomObjectGetHandler(gateDeps, getDocsDir));
135
+ }, buildCustomObjectAttributeSearchHandler(gateDeps, getDocsDir));
119
136
  }
@@ -59,7 +59,7 @@ export function registerSfccTools(registerTool, deps) {
59
59
  gateDeps,
60
60
  getDocsDir: deps.getDocsDir,
61
61
  });
62
- // custom_object_definition_list, custom_object_definition_get (BAPI-402 T4–T5)
62
+ // custom_object_definition_attributes_get, custom_object_definition_attribute_search (BAPI-402 T4–T5)
63
63
  registerSfccCustomObjectDefReadTools(registerTool, {
64
64
  gateDeps,
65
65
  getDocsDir: deps.getDocsDir,
@@ -58,7 +58,11 @@ export function withSfccGate(deps, handler) {
58
58
  `Update the version field in your Bridge API project settings.`);
59
59
  }
60
60
  // (c) Resolve credentials from dw.json
61
- const explicitHostname = typeof args.instance === "string" ? args.instance : undefined;
61
+ // A hostname always contains a dot; the OCAPI instance-context enum
62
+ // (e.g. "sandbox") never does — only treat args.instance as an explicit
63
+ // hostname override when it looks like one.
64
+ const rawInstance = typeof args.instance === "string" ? args.instance : undefined;
65
+ const explicitHostname = rawInstance && rawInstance.includes(".") ? rawInstance : undefined;
62
66
  const credResult = await resolveSfccCredentials(explicitHostname);
63
67
  if (!credResult.ok) {
64
68
  return notConfigured("missing-dw-json", `SFCC credential resolution failed: ${credResult.error} ` +
@@ -22,6 +22,7 @@
22
22
  import { randomBytes } from "node:crypto";
23
23
  import path from "node:path";
24
24
  import { fileURLToPath } from "node:url";
25
+ import { normalizeDeclaredTouchedFiles } from "./conductor/file-scope-guard.js";
25
26
  import { resolveProfiles } from "./mcp-profile.js";
26
27
  import { resolveStartTicketsRepoName } from "./start-tickets-repo.js";
27
28
  // ---------------------------------------------------------------------------
@@ -54,15 +55,22 @@ export function mintStartTicketsWorkerId(ticketKey, agentName, fragment = random
54
55
  }
55
56
  /**
56
57
  * Build the secret-free epic identity env keys from an {@link EpicDispatchIdentity}.
57
- * Strict allowlist — constructs a FRESH object with exactly three named keys,
58
- * never copies arbitrary parent env so credentials cannot leak.
58
+ * Strict allowlist — constructs a FRESH object with only named conductor keys,
59
+ * never copies arbitrary parent env so credentials cannot leak. The declared
60
+ * touched-file set (BAPI-507 N-2) is included as compact JSON ONLY when it
61
+ * normalizes to a non-empty repo-relative set.
59
62
  */
60
63
  export function buildEpicIdentityEnv(epic) {
61
- return {
64
+ const env = {
62
65
  BAPI_CONDUCTOR_EPIC_KEY: epic.epic_key,
63
66
  BAPI_CONDUCTOR_EPIC_RUN_ID: epic.epic_run_id,
64
67
  BAPI_CONDUCTOR_PLAN_VERSION: String(epic.plan_version),
65
68
  };
69
+ const declared = normalizeDeclaredTouchedFiles(epic.declared_touched_files);
70
+ if (declared.length > 0) {
71
+ env.BAPI_CONDUCTOR_DECLARED_TOUCHED_FILES_JSON = JSON.stringify(declared);
72
+ }
73
+ return env;
66
74
  }
67
75
  // ---------------------------------------------------------------------------
68
76
  // Conductor context
@@ -182,11 +190,19 @@ export function buildConductorWorkerEnv(context, worker, parentEnv) {
182
190
  // ---------------------------------------------------------------------------
183
191
  // Claude hook settings merge / provisioning
184
192
  // ---------------------------------------------------------------------------
185
- /** Claude lifecycle events always registered for the conductor hook. */
193
+ /**
194
+ * Claude lifecycle events always registered for the conductor hook.
195
+ *
196
+ * BAPI-507 (N-1): `Stop` and `SubagentStop` are deliberately NOT registered.
197
+ * They are per-turn / per-subagent events (verified against the Claude Code
198
+ * lifecycle-hook contract), so registering them emitted a `run.stopped` on every
199
+ * turn and folded the ticket to `ready_for_review` while it was still being
200
+ * implemented. `SessionEnd` is the one true session-terminal event and is the
201
+ * only session-end signal registered here.
202
+ */
186
203
  export const CONDUCTOR_HOOK_LIFECYCLE_EVENTS = [
187
204
  "SessionStart",
188
- "Stop",
189
- "SubagentStop",
205
+ "SessionEnd",
190
206
  "Notification",
191
207
  ];
192
208
  /** Broad matcher used for `PreToolUse` when no existing matcher is present. */