@bridge_gpt/mcp-server 0.2.66 → 0.2.68

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 (171) hide show
  1. package/README.md +421 -83
  2. package/build/agent-capabilities/probes.js +15 -5
  3. package/build/agent-launchers/claude-executor-adapter.js +70 -16
  4. package/build/agent-launchers/executor-adapter.js +7 -4
  5. package/build/agents.generated.js +22 -2
  6. package/build/bitbucket-workspace-remote.js +136 -0
  7. package/build/bootstrap-project-identity.js +24 -0
  8. package/build/bounded-wait.js +67 -10
  9. package/build/bridge-permissions.js +829 -0
  10. package/build/claude-settings.js +18 -0
  11. package/build/cli-help.js +21 -0
  12. package/build/command-assets-doctor.js +24 -70
  13. package/build/command-provisioning.js +29 -5
  14. package/build/commands.generated.js +12 -12
  15. package/build/conductor/bridge-api-client.js +857 -1
  16. package/build/conductor/bring-up-facts.js +1 -1
  17. package/build/conductor/command-drift-production.js +92 -0
  18. package/build/conductor/command-drift.js +89 -0
  19. package/build/conductor/doctor.js +72 -29
  20. package/build/conductor/epic-branch-ci-inspection.js +1375 -0
  21. package/build/conductor/epic-implementer-cli.js +98 -28
  22. package/build/conductor/errors.js +4 -3
  23. package/build/conductor/gate-discovery.js +890 -0
  24. package/build/conductor/guided-repair.js +6 -2
  25. package/build/conductor/hook-runtime-qualification.js +1 -1
  26. package/build/conductor/install-doctor.js +15 -3
  27. package/build/conductor/json-output.js +63 -0
  28. package/build/conductor/merge-execution-contract.js +27 -8
  29. package/build/conductor/merge-execution-eligibility.js +3 -2
  30. package/build/conductor/pr-ci-producer.js +1 -1
  31. package/build/conductor/readiness-cli.js +113 -12
  32. package/build/conductor/readiness-sections.js +410 -9
  33. package/build/conductor/readiness.js +388 -33
  34. package/build/conductor/recovery-cli.js +310 -12
  35. package/build/conductor/recovery-operations.js +167 -3
  36. package/build/conductor/repair-contract.js +54 -3
  37. package/build/conductor/repository-workflow-remedy.js +125 -0
  38. package/build/conductor/tools.js +2 -2
  39. package/build/conductor-bundle-artifacts.js +8 -0
  40. package/build/connect-bitbucket-api.js +41 -6
  41. package/build/connect-bitbucket.js +106 -33
  42. package/build/credential-store.js +207 -47
  43. package/build/docs.generated.js +62 -3
  44. package/build/doctor.js +92 -1
  45. package/build/drive-epic-key.js +6 -5
  46. package/build/drive-epic.js +129 -61
  47. package/build/epic-coherence-input.js +2 -1
  48. package/build/epic-coherence-input.test-fixtures.js +4 -3
  49. package/build/epic-implementer-bin.js +1379 -28
  50. package/build/epic-implementer-bundle-cli.js +2 -2
  51. package/build/epic-workarounds-path.js +39 -0
  52. package/build/executor/cli.js +33 -2
  53. package/build/executor/completion-contract.js +66 -10
  54. package/build/executor/completion-supervision.js +4 -0
  55. package/build/executor/deps.js +4 -1
  56. package/build/executor/env.js +19 -0
  57. package/build/executor/gate-qualification-job.js +499 -0
  58. package/build/executor/guarded-fast-forward-push.js +129 -0
  59. package/build/executor/heartbeat.js +1 -1
  60. package/build/executor/http-client.js +20 -0
  61. package/build/executor/integration-job.js +122 -8
  62. package/build/executor/job-errors.js +33 -1
  63. package/build/executor/job-runner.js +813 -87
  64. package/build/executor/likely-tests.js +290 -0
  65. package/build/executor/live-worker-registry.js +1 -1
  66. package/build/executor/observation.js +37 -3
  67. package/build/executor/packaged-recipe.js +106 -0
  68. package/build/executor/permissions.js +115 -28
  69. package/build/executor/platform.js +1 -1
  70. package/build/executor/preflight.js +112 -26
  71. package/build/executor/process-containment.js +36 -12
  72. package/build/executor/process.js +15 -7
  73. package/build/executor/prompt-spec.js +33 -1
  74. package/build/executor/recently-failed-tests-file.js +71 -0
  75. package/build/executor/reconcile-overlap-job.js +41 -23
  76. package/build/executor/required-verification.js +1 -1
  77. package/build/executor/required-work-ledger.js +111 -18
  78. package/build/executor/resume-pre-spawn.js +2 -0
  79. package/build/executor/retirement.js +69 -0
  80. package/build/executor/runner.js +122 -13
  81. package/build/executor/service-unit.js +7 -1
  82. package/build/executor/test-clock.js +15 -0
  83. package/build/executor/types.js +32 -0
  84. package/build/executor/viewer-tabs.js +58 -8
  85. package/build/executor/worker-command.js +23 -7
  86. package/build/executor/worker-diagnostics.js +37 -0
  87. package/build/executor/worker-finalization.js +113 -34
  88. package/build/executor/worker-guard-hook.js +316 -58
  89. package/build/executor/worker-log.js +4 -3
  90. package/build/executor/worker-verification-guidance.js +37 -0
  91. package/build/executor/worktree.js +166 -8
  92. package/build/first-build-advisory.js +97 -0
  93. package/build/generated-assets-advisory.js +14 -0
  94. package/build/generated-assets-catalog.js +66 -0
  95. package/build/generated-assets-context.js +46 -0
  96. package/build/generated-assets-dependencies.js +26 -0
  97. package/build/generated-assets-doctor.js +50 -0
  98. package/build/generated-assets-health.js +65 -0
  99. package/build/generated-assets-host.js +17 -0
  100. package/build/generated-assets-inventory.js +17 -0
  101. package/build/generated-assets-paths.js +60 -0
  102. package/build/generated-assets-publisher.js +133 -0
  103. package/build/generated-assets-remedy.js +25 -0
  104. package/build/generated-assets-repair.js +72 -0
  105. package/build/generated-assets-session.js +71 -0
  106. package/build/generated-assets-test-support.js +35 -0
  107. package/build/index.js +1224 -1054
  108. package/build/init.js +54 -177
  109. package/build/install-bridge-conductor-gate.js +1372 -0
  110. package/build/install-bridge-conductor.js +296 -41
  111. package/build/install-bridge.js +547 -40
  112. package/build/mcp-install-state.js +50 -4
  113. package/build/merge-workflow-install.js +3 -4
  114. package/build/phase-context-git-probe.js +68 -0
  115. package/build/phase-result-artifacts.js +173 -5
  116. package/build/pipeline-utils.js +6 -2
  117. package/build/pipelines.generated.js +34 -160
  118. package/build/plan-execution-ledger.js +2 -32
  119. package/build/plane/alembic-head.js +12 -3
  120. package/build/plane/base-worktree-guard.js +147 -0
  121. package/build/plane/cli.js +217 -17
  122. package/build/plane/code-snapshot.js +468 -0
  123. package/build/plane/defaults.js +48 -3
  124. package/build/plane/endpoint.js +41 -0
  125. package/build/plane/manifest.js +136 -2
  126. package/build/plane/member-roster.js +94 -19
  127. package/build/plane/preflight.js +83 -2
  128. package/build/plane/process-cwd.js +67 -0
  129. package/build/plane/restart.js +542 -0
  130. package/build/plane/shutdown.js +31 -0
  131. package/build/plane/status.js +143 -8
  132. package/build/plane/supervisor.js +586 -179
  133. package/build/plane/types.js +54 -2
  134. package/build/printed-remedy-commands.js +52 -0
  135. package/build/readiness-check.js +1 -1
  136. package/build/readme.generated.js +1 -1
  137. package/build/repair-assets.js +93 -0
  138. package/build/review-generation.js +22 -0
  139. package/build/run-unit-tests-launcher.js +130 -49
  140. package/build/secret-redaction.js +33 -0
  141. package/build/setup-epic.js +152 -22
  142. package/build/sfcc/config.js +9 -0
  143. package/build/sfcc/credentials.js +100 -38
  144. package/build/sfcc/hostname.js +67 -0
  145. package/build/sfcc/permissions.js +17 -13
  146. package/build/sfcc/register.js +1 -1
  147. package/build/sfcc/tool-wrapper.js +39 -43
  148. package/build/sfcc/write-grants.js +17 -11
  149. package/build/sfcc/write-result.js +4 -3
  150. package/build/sfcc/writes-site-preference.js +20 -8
  151. package/build/start-tickets-prereqs.js +70 -16
  152. package/build/start-tickets.js +208 -36
  153. package/build/tool-error-envelope.js +61 -0
  154. package/build/tool-surface-gating.js +36 -0
  155. package/build/upgrade-cli.js +55 -1
  156. package/build/version.generated.js +3 -3
  157. package/build/visual-diff.js +22 -1
  158. package/build/worker-guard-hook-bin.js +4 -6
  159. package/build/worker-guard-hook-main.js +47 -0
  160. package/build/worktree-core.js +145 -6
  161. package/docs/EPIC-IMPLEMENTER-INSTALL.md +867 -0
  162. package/docs/EPIC-IMPLEMENTER.md +728 -0
  163. package/docs/install/mcp-tool-integrations.md +24 -15
  164. package/docs/install/sfcc-integration.md +12 -4
  165. package/package.json +3 -3
  166. package/pipelines/greenfield-setup.json +4 -128
  167. package/pipelines/implement-ticket.json +3 -2
  168. package/pipelines/plan-epic.json +0 -5
  169. package/pipelines/review-ticket.json +1 -1
  170. package/docs/CONDUCTOR.md +0 -605
  171. package/pipelines/idea-to-pr.json +0 -49
@@ -62,13 +62,54 @@ export const WORKER_GUARD_MALFORMED_LINE = `${WORKER_GUARD_REASON_PREFIX}: paylo
62
62
  * branch, or an exception could reach this string — the bound is structural rather
63
63
  * than a rule a caller is asked to remember.
64
64
  */
65
- export function renderWorkerGuardReason(category, family) {
65
+ export function renderWorkerGuardReason(category, family, form) {
66
66
  if (category === "git-ambiguous") {
67
67
  return (`${WORKER_GUARD_REASON_PREFIX}: denied ${family} (${category}). ` +
68
68
  "This shell form cannot be safely resolved: substitution, redirection, or " +
69
69
  "stdin-fed arguments hide what git will run. Use literal arguments instead; " +
70
70
  "for a commit message, write it to a file and run git commit -F <file>.");
71
71
  }
72
+ if (form === "shell_form") {
73
+ const pattern = category === "git-push"
74
+ ? "If the destination is your own branch and no force is used, run git push origin HEAD:<your work branch> alone."
75
+ : category === "git-reset-hard"
76
+ ? "If resetting your own branch, run git reset --hard origin/<your work branch> alone."
77
+ : category === "filesystem-clean"
78
+ ? "If all pathspecs are inside your worktree, run git clean with literal worktree-contained pathspecs alone."
79
+ : category === "filesystem-remove"
80
+ ? "If all paths are inside your worktree, run rm with literal worktree-contained paths alone."
81
+ : "If arguments are read-only, run literal read-only arguments alone, without redirects or $VAR.";
82
+ return `${WORKER_GUARD_REASON_PREFIX}: denied ${family} (${category}). ` +
83
+ "Shell form cannot be checked: redirects, pipes, substitution, or stdin-fed arguments. " + pattern;
84
+ }
85
+ const restriction = {
86
+ "git-push": "Push only to your own branch, without force. Use HEAD:<your work branch> as the destination.",
87
+ "git-reset-hard": "Hard reset only against your own branch ref.",
88
+ "filesystem-clean": "Clean only literal paths inside your worktree.",
89
+ "filesystem-remove": "Remove only literal paths inside your worktree.",
90
+ "database-destructive": "Destructive SQL is prohibited. Use read-only database arguments.",
91
+ "network-mutating": "Mutating requests are prohibited. Use read-only network arguments.",
92
+ };
93
+ if (restriction[category])
94
+ return `${WORKER_GUARD_REASON_PREFIX}: denied ${family} (${category}). ${restriction[category]}`;
95
+ if (category === "workflow-entry" ||
96
+ category === "workflow-launch-skill" ||
97
+ category === "workflow-launch-shell" ||
98
+ category === "workflow-planner-tool") {
99
+ return (`${WORKER_GUARD_REASON_PREFIX}: denied ${family} (${category}). ` +
100
+ "A correction worker must not start a full workflow. Reading or editing files " +
101
+ "that mention a workflow is allowed; make the focused correction directly.");
102
+ }
103
+ if (category === "correction-context-invalid") {
104
+ return (`${WORKER_GUARD_REASON_PREFIX}: denied ${family} (${category}). ` +
105
+ "The correction context for this worker is not valid, so no tool call can be " +
106
+ "checked. Stop and report the launch problem.");
107
+ }
108
+ if (category === "correction-payload-invalid") {
109
+ return (`${WORKER_GUARD_REASON_PREFIX}: denied ${family} (${category}). ` +
110
+ "The tool call could not be read as a tool name with a tool input, so it cannot " +
111
+ "be checked. Retry with a well-formed call.");
112
+ }
72
113
  return (`${WORKER_GUARD_REASON_PREFIX}: denied ${family} (${category}). ` +
73
114
  "This command family is restricted to the worker's own branch, worktree, and " +
74
115
  "read-only network access. Use a different approach and continue.");
@@ -192,6 +233,92 @@ export function tokenizeShellCommand(command) {
192
233
  endCommand();
193
234
  return commands;
194
235
  }
236
+ function diagnosticProjections(command) {
237
+ if (command.length > WORKER_GUARD_MAX_PAYLOAD_BYTES)
238
+ return [];
239
+ const result = [];
240
+ let start = 0;
241
+ let masked = "";
242
+ let quote = "";
243
+ let uncertain = false;
244
+ const finish = (end) => {
245
+ const original = tokenizeShellCommand(command.slice(start, end));
246
+ const projected = tokenizeShellCommand(masked);
247
+ const value = !uncertain && projected.length === 1
248
+ ? { argv: projected[0].argv, complete: !projected[0].unsafe } : undefined;
249
+ result.push(...original.map((_, i) => i === 0 ? value : undefined));
250
+ masked = "";
251
+ uncertain = false;
252
+ start = end + 1;
253
+ };
254
+ for (let i = 0; i < command.length; i += 1) {
255
+ const ch = command[i];
256
+ if (ch === "\\" && quote !== "'") {
257
+ masked += command.slice(i, i + 2);
258
+ i += 1;
259
+ continue;
260
+ }
261
+ if (quote) {
262
+ masked += ch;
263
+ if (ch === quote)
264
+ quote = "";
265
+ continue;
266
+ }
267
+ if (ch === "'" || ch === '"') {
268
+ quote = ch;
269
+ masked += ch;
270
+ continue;
271
+ }
272
+ if (ch === ">" || ch === "<") {
273
+ // A numeric fd is syntax only at a token boundary, not in a word.
274
+ masked = masked.replace(/(^|\s)\d+$/, "$1");
275
+ let j = i + 1;
276
+ if (command[j] === ch)
277
+ j += 1;
278
+ if (command[j] === "&")
279
+ j += 1;
280
+ while (/[ \t]/.test(command[j] ?? "") && j < command.length)
281
+ j += 1;
282
+ const operandStart = j;
283
+ let operandQuote = "";
284
+ while (j < command.length) {
285
+ const c = command[j];
286
+ if (c === "\\" && operandQuote !== "'") {
287
+ j += 2;
288
+ continue;
289
+ }
290
+ if (operandQuote) {
291
+ if (c === operandQuote)
292
+ operandQuote = "";
293
+ j += 1;
294
+ continue;
295
+ }
296
+ if (c === "'" || c === '"') {
297
+ operandQuote = c;
298
+ j += 1;
299
+ continue;
300
+ }
301
+ if (/[\s;|&<>]/.test(c))
302
+ break;
303
+ j += 1;
304
+ }
305
+ if (j === operandStart || operandQuote || command.slice(i, j).startsWith("<<"))
306
+ uncertain = true;
307
+ masked += " ";
308
+ i = j - 1;
309
+ continue;
310
+ }
311
+ if (SEPARATORS.has(ch) || ch === "\n") {
312
+ finish(i);
313
+ continue;
314
+ }
315
+ masked += ch;
316
+ }
317
+ if (quote)
318
+ uncertain = true;
319
+ finish(command.length);
320
+ return result;
321
+ }
195
322
  // ---------------------------------------------------------------------------
196
323
  // Payload shape
197
324
  // ---------------------------------------------------------------------------
@@ -643,18 +770,59 @@ function unsafeGitCategory(argv) {
643
770
  return "filesystem-clean";
644
771
  return "git-ambiguous";
645
772
  }
646
- /** The category a governed family denies with when its segment is unmodelable. */
647
- function unsafeSegmentDecision(family, argv) {
648
- switch (family) {
649
- case "git":
650
- return { kind: "deny", category: unsafeGitCategory(argv), family: "git" };
651
- case "rm":
652
- return { kind: "deny", category: "filesystem-remove", family: "rm" };
653
- case "database":
654
- return { kind: "deny", category: "database-destructive", family: "psql" };
655
- case "network":
656
- return { kind: "deny", category: "network-mutating", family: "curl" };
773
+ /** Diagnostic-only force policy; literal enforcement remains unchanged. */
774
+ function diagnosticPushForce(args) {
775
+ return args.some(t => t === "--force" || t === "-f" ||
776
+ t === "--force-with-lease" || t.startsWith("--force-with-lease=") || t.startsWith("+"));
777
+ }
778
+ function diagnosticViolation(family, diagnostic, ctx, cwd) {
779
+ const full = unwrapTransparentPrefixes(diagnostic.argv);
780
+ // A literal prefix may prove a violation even when later arguments are unknown.
781
+ // It is used only to establish a denial, never to assert a complete safe argv.
782
+ const unknown = diagnostic.complete ? -1 : full.findIndex(t => /[$`]/.test(t));
783
+ const argv = unknown < 0 ? full : full.slice(0, unknown);
784
+ if (family === "database")
785
+ return evaluateDatabase(argv).kind === "deny";
786
+ if (family === "network")
787
+ return evaluateNetwork(argv).kind === "deny";
788
+ // Never evaluate unknown/omitted targets as a complete safe command. Visible
789
+ // force/SQL/network restrictions can still be established independently.
790
+ const git = parseGitInvocation(argv, cwd);
791
+ if (family === "git" && git.subcommand === "push" && diagnosticPushForce(git.args))
792
+ return true;
793
+ if (!diagnostic.complete) {
794
+ if (family === "rm")
795
+ return argv.slice(1).some(t => !t.startsWith("-")) && evaluateRemove(argv, ctx, cwd).kind === "deny";
796
+ if (git.subcommand === "push")
797
+ return git.args.filter(t => !t.startsWith("-")).length >= 2 && evaluateGitPush(git.args, ctx).kind === "deny";
798
+ if (git.subcommand === "reset")
799
+ return git.args.some(t => !t.startsWith("-")) && evaluateGitReset(git.args, ctx).kind === "deny";
800
+ if (git.subcommand === "clean")
801
+ return evaluateGitClean(git.args, ctx, git.cwd).kind === "deny";
802
+ return false;
657
803
  }
804
+ if (family === "rm")
805
+ return evaluateRemove(argv, ctx, cwd).kind === "deny";
806
+ if (git.unsafe)
807
+ return true;
808
+ if (git.subcommand === "push")
809
+ return evaluateGitPush(git.args, ctx).kind === "deny";
810
+ if (git.subcommand === "reset")
811
+ return evaluateGitReset(git.args, ctx).kind === "deny";
812
+ if (git.subcommand === "clean")
813
+ return evaluateGitClean(git.args, ctx, git.cwd).kind === "deny";
814
+ return false;
815
+ }
816
+ /** Select metadata only, preserving the existing category and family. */
817
+ function unsafeSegmentDecision(family, argv, ctx, cwd, diagnostic) {
818
+ const category = family === "git" ? unsafeGitCategory(argv)
819
+ : family === "rm" ? "filesystem-remove"
820
+ : family === "database" ? "database-destructive" : "network-mutating";
821
+ const name = family === "database" ? "psql" : family === "network" ? "curl" : family;
822
+ const decision = { kind: "deny", category, family: name };
823
+ return diagnostic && category !== "git-ambiguous" &&
824
+ !diagnosticViolation(family, diagnostic, ctx, cwd)
825
+ ? { ...decision, form: "shell_form" } : decision;
658
826
  }
659
827
  /**
660
828
  * Transparent prefixes that run another command without changing what it does.
@@ -780,8 +948,54 @@ function stripStdinPrefixOptions(argv) {
780
948
  }
781
949
  return argv.slice(i);
782
950
  }
951
+ /** The workflow names a correction worker must never launch. */
952
+ const WORKFLOW_NAMES = "implement-ticket|plan-ticket|idea-to-pr|review-and-implement";
953
+ /**
954
+ * A slash-form workflow launch: the slash starts the text or follows whitespace, and
955
+ * the name ends at whitespace or the end. A file path such as
956
+ * `docs/claude/idea-to-pr.md` has neither, so it never matches.
957
+ */
958
+ const WORKFLOW_SLASH_LAUNCH = new RegExp(`(?:^|\\s)/(?:bridge-)?(?:${WORKFLOW_NAMES})(?=\\s|$)`, "i");
959
+ /** A bare skill or command name, with optional leading slash and `plugin:` prefix. */
960
+ const WORKFLOW_NAME_EXACT = new RegExp(`^/?(?:[A-Za-z0-9_-]+:)?(?:bridge-)?(?:${WORKFLOW_NAMES})$`, "i");
961
+ /** The token after `npx` that names the Claude CLI package. */
962
+ const CLAUDE_PACKAGE_TOKEN = /^(?:@anthropic-ai\/)?claude(?:-code)?(?:@\S+)?$/;
963
+ /** The argv after the Claude CLI name, or `undefined` when this is not a Claude launch. */
964
+ function claudeLaunchArgs(argv) {
965
+ const base = path.basename(argv[0] ?? "");
966
+ if (base === "claude")
967
+ return argv.slice(1);
968
+ if (base === "npx") {
969
+ const index = argv.findIndex((token, i) => i > 0 && CLAUDE_PACKAGE_TOKEN.test(token));
970
+ return index === -1 ? undefined : argv.slice(index + 1);
971
+ }
972
+ return undefined;
973
+ }
974
+ /**
975
+ * Is this a correction-mode Claude print run that launches a workflow (or that cannot
976
+ * be read well enough to rule one out)?
977
+ *
978
+ * Runs on tokenized, de-quoted argv, so a quoted prompt is one plain token. The whole
979
+ * argv is scanned for the print flag and for a workflow slash launch, because the
980
+ * prompt may sit anywhere among the options. A print run in a segment the tokenizer
981
+ * marked unsafe denies without trying to resolve the substitution.
982
+ */
983
+ function isClaudeWorkflowPrintLaunch(segment) {
984
+ const args = claudeLaunchArgs(segment.argv);
985
+ if (args === undefined)
986
+ return false;
987
+ const printing = args.some((token) => token === "-p" || token === "--print" || token.startsWith("--print="));
988
+ if (!printing)
989
+ return false;
990
+ if (segment.unsafe)
991
+ return true;
992
+ return args.some((token) => {
993
+ const value = token.startsWith("--print=") ? token.slice("--print=".length) : token;
994
+ return WORKFLOW_SLASH_LAUNCH.test(value);
995
+ });
996
+ }
783
997
  /** Evaluate one already-tokenized simple command. */
784
- function evaluateSimpleCommand(segment, ctx, payloadCwd, depth = 0, stdinFed = false) {
998
+ function evaluateSimpleCommand(segment, ctx, payloadCwd, depth = 0, stdinFed = false, diagnostic) {
785
999
  const argv = unwrapTransparentPrefixes(segment.argv);
786
1000
  const executable = argv[0];
787
1001
  if (executable === undefined)
@@ -794,8 +1008,9 @@ function evaluateSimpleCommand(segment, ctx, payloadCwd, depth = 0, stdinFed = f
794
1008
  const cIndex = argv.findIndex((t) => t === "-c");
795
1009
  const inner = cIndex === -1 ? undefined : argv[cIndex + 1];
796
1010
  if (inner !== undefined) {
797
- for (const nested of tokenizeShellCommand(inner)) {
798
- const decision = evaluateSimpleCommand(nested, ctx, payloadCwd, depth + 1, stdinFed);
1011
+ const projections = diagnostic?.complete ? diagnosticProjections(inner) : [];
1012
+ for (const [i, nested] of tokenizeShellCommand(inner).entries()) {
1013
+ const decision = evaluateSimpleCommand(nested, ctx, payloadCwd, depth + 1, stdinFed, projections[i]);
799
1014
  if (decision.kind === "deny")
800
1015
  return decision;
801
1016
  }
@@ -804,8 +1019,10 @@ function evaluateSimpleCommand(segment, ctx, payloadCwd, depth = 0, stdinFed = f
804
1019
  }
805
1020
  // `eval <words>` — join and re-tokenize, exactly as the shell would.
806
1021
  if (EVAL_BUILTINS.has(path.basename(executable)) && depth < 4) {
807
- for (const nested of tokenizeShellCommand(argv.slice(1).join(" "))) {
808
- const decision = evaluateSimpleCommand(nested, ctx, payloadCwd, depth + 1, stdinFed);
1022
+ const inner = argv.slice(1).join(" ");
1023
+ const projections = diagnostic?.complete ? diagnosticProjections(inner) : [];
1024
+ for (const [i, nested] of tokenizeShellCommand(inner).entries()) {
1025
+ const decision = evaluateSimpleCommand(nested, ctx, payloadCwd, depth + 1, stdinFed, projections[i]);
809
1026
  if (decision.kind === "deny")
810
1027
  return decision;
811
1028
  }
@@ -825,9 +1042,12 @@ function evaluateSimpleCommand(segment, ctx, payloadCwd, depth = 0, stdinFed = f
825
1042
  // `stdinFed` from here down: once arguments come from stdin, no containment or
826
1043
  // destination question about them can be answered, so reaching a governed
827
1044
  // family at any depth below denies.
828
- return evaluateSimpleCommand({ argv: rest, unsafe: segment.unsafe }, ctx, payloadCwd, depth + 1, true);
1045
+ return evaluateSimpleCommand({ argv: rest, unsafe: segment.unsafe }, ctx, payloadCwd, depth + 1, true, diagnostic ? { argv: stripStdinPrefixOptions(unwrapTransparentPrefixes(diagnostic.argv)), complete: false } : undefined);
829
1046
  }
830
1047
  segment = { argv, unsafe: segment.unsafe };
1048
+ if (ctx.correctionEnforced && isClaudeWorkflowPrintLaunch(segment)) {
1049
+ return { kind: "deny", category: "workflow-launch-shell", family: "workflow" };
1050
+ }
831
1051
  const family = classify(executable);
832
1052
  if (family === null)
833
1053
  return { kind: "allow" };
@@ -835,11 +1055,11 @@ function evaluateSimpleCommand(segment, ctx, payloadCwd, depth = 0, stdinFed = f
835
1055
  // under `stdinFed` can never be proven safe regardless of how its visible argv
836
1056
  // looks — the fail-CLOSED rule, applied after every wrapper has been resolved.
837
1057
  if (stdinFed)
838
- return unsafeSegmentDecision(family, segment.argv);
1058
+ return unsafeSegmentDecision(family, segment.argv, ctx, resolveEffectiveCwd(ctx, payloadCwd), diagnostic);
839
1059
  // A governed family inside a segment carrying substitution or redirection can
840
1060
  // never be proven safe — deny, per the fail-CLOSED half of the contract.
841
1061
  if (segment.unsafe)
842
- return unsafeSegmentDecision(family, segment.argv);
1062
+ return unsafeSegmentDecision(family, segment.argv, ctx, resolveEffectiveCwd(ctx, payloadCwd), diagnostic);
843
1063
  const cwd = resolveEffectiveCwd(ctx, payloadCwd);
844
1064
  if (family === "rm")
845
1065
  return evaluateRemove(segment.argv, ctx, cwd);
@@ -850,21 +1070,70 @@ function evaluateSimpleCommand(segment, ctx, payloadCwd, depth = 0, stdinFed = f
850
1070
  const git = parseGitInvocation(segment.argv, cwd);
851
1071
  if (git.subcommand === "push") {
852
1072
  return git.unsafe
853
- ? { kind: "deny", category: "git-push", family: "git" }
1073
+ ? unsafeSegmentDecision("git", segment.argv, ctx, cwd, diagnostic)
854
1074
  : evaluateGitPush(git.args, ctx);
855
1075
  }
856
1076
  if (git.subcommand === "reset") {
857
1077
  return git.unsafe
858
- ? { kind: "deny", category: "git-reset-hard", family: "git" }
1078
+ ? unsafeSegmentDecision("git", segment.argv, ctx, cwd, diagnostic)
859
1079
  : evaluateGitReset(git.args, ctx);
860
1080
  }
861
1081
  if (git.subcommand === "clean") {
862
1082
  return git.unsafe
863
- ? { kind: "deny", category: "filesystem-clean", family: "git" }
1083
+ ? unsafeSegmentDecision("git", segment.argv, ctx, cwd, diagnostic)
864
1084
  : evaluateGitClean(git.args, ctx, git.cwd);
865
1085
  }
866
1086
  return { kind: "allow" };
867
1087
  }
1088
+ function workflowDeny(category) {
1089
+ return { kind: "deny", category, family: "workflow" };
1090
+ }
1091
+ /**
1092
+ * Tools that read or change files. Their path and pattern fields name files, and a
1093
+ * file named after a workflow is not a workflow launch, so they are never matched.
1094
+ */
1095
+ const FILE_TOOLS = new Set(["Read", "Edit", "Write", "MultiEdit", "NotebookEdit", "Grep", "Glob"]);
1096
+ /** The fields of each invocation tool that can carry a workflow launch. */
1097
+ const INVOCATION_FIELDS = {
1098
+ Skill: ["skill", "args"],
1099
+ Task: ["prompt", "description"],
1100
+ Agent: ["prompt", "description"],
1101
+ SlashCommand: ["command"],
1102
+ };
1103
+ /** An `mcp__` tool whose name is a planner or recipe entry point. */
1104
+ const PLANNER_TOOL_NAME = /(?:request_plan_generation|get_pipeline_recipe|plan_ticket|implement_ticket)/i;
1105
+ /**
1106
+ * Correction-mode dispatch by tool role. Returns a decision for a file tool, an
1107
+ * invocation tool, a planner tool, or an unknown role, and `undefined` for `Bash`,
1108
+ * which the caller evaluates as a shell command.
1109
+ */
1110
+ function evaluateCorrectionToolRole(toolName, input) {
1111
+ if (FILE_TOOLS.has(toolName))
1112
+ return { kind: "allow" };
1113
+ if (toolName === "Bash")
1114
+ return undefined;
1115
+ const fields = INVOCATION_FIELDS[toolName];
1116
+ if (fields !== undefined) {
1117
+ for (const field of fields) {
1118
+ const value = input[field];
1119
+ if (value === undefined)
1120
+ continue;
1121
+ if (typeof value !== "string")
1122
+ return workflowDeny("workflow-launch-skill");
1123
+ const nameField = toolName === "Skill" ? field === "skill" : toolName === "SlashCommand";
1124
+ if (WORKFLOW_SLASH_LAUNCH.test(value) || (nameField && WORKFLOW_NAME_EXACT.test(value.trim().split(/\s+/)[0] ?? ""))) {
1125
+ return workflowDeny("workflow-launch-skill");
1126
+ }
1127
+ }
1128
+ return { kind: "allow" };
1129
+ }
1130
+ if (/^mcp__/i.test(toolName) && PLANNER_TOOL_NAME.test(toolName)) {
1131
+ return workflowDeny("workflow-planner-tool");
1132
+ }
1133
+ // Any other role: only an explicit workflow launch in a string argument denies.
1134
+ const args = Object.values(input).filter((v) => typeof v === "string").join(" ");
1135
+ return WORKFLOW_SLASH_LAUNCH.test(args) ? workflowDeny("workflow-entry") : undefined;
1136
+ }
868
1137
  /**
869
1138
  * Evaluate a parsed PreToolUse payload.
870
1139
  *
@@ -878,50 +1147,37 @@ function evaluateSimpleCommand(segment, ctx, payloadCwd, depth = 0, stdinFed = f
878
1147
  * governed here.
879
1148
  */
880
1149
  export function evaluateWorkerGuardPayload(payload, ctx) {
881
- const root = asRecord(payload);
882
1150
  if (ctx.correctionContextInvalid)
883
- return { kind: "deny", category: "workflow-entry", family: "workflow" };
884
- if (root === undefined)
885
- return ctx.correctionEnforced
886
- ? { kind: "deny", category: "workflow-entry", family: "workflow" }
887
- : { kind: "malformed" };
1151
+ return workflowDeny("correction-context-invalid");
1152
+ const root = asRecord(payload);
1153
+ if (root === undefined) {
1154
+ return ctx.correctionEnforced ? workflowDeny("correction-payload-invalid") : { kind: "malformed" };
1155
+ }
888
1156
  const toolName = root.tool_name ?? root.toolName;
889
- if (typeof toolName !== "string")
890
- return ctx.correctionEnforced
891
- ? { kind: "deny", category: "workflow-entry", family: "workflow" }
892
- : { kind: "malformed" };
1157
+ if (typeof toolName !== "string") {
1158
+ return ctx.correctionEnforced ? workflowDeny("correction-payload-invalid") : { kind: "malformed" };
1159
+ }
893
1160
  const input = asRecord(root.tool_input ?? root.toolInput);
894
1161
  if (ctx.correctionEnforced) {
895
1162
  if (input === undefined)
896
- return { kind: "deny", category: "workflow-entry", family: "workflow" };
897
- const args = Object.values(input).filter((v) => typeof v === "string").join(" ");
898
- const fullEntry = /(?:^|\s|\/)\/?(?:bridge-)?(?:implement-ticket|plan-ticket|idea-to-pr|review-and-implement)(?:\s|$|\.)/i;
899
- const recipePath = /(?:commands\/src|\.claude\/commands|\.cursor\/commands|pipelines)\/(?:implement-ticket|plan-ticket)(?:\.md|\.json)?/i;
900
- const plannerTool = /(?:request_plan_generation|get_pipeline_recipe|plan_ticket|implement_ticket)/i;
901
- if (recipePath.test(args) || fullEntry.test(args) ||
902
- (plannerTool.test(toolName) && toolName !== "Bash")) {
903
- return { kind: "deny", category: "workflow-entry", family: "workflow" };
904
- }
905
- if (/^(?:Task|Agent|Skill|SlashCommand|mcp__)/i.test(toolName) &&
906
- (fullEntry.test(args) || plannerTool.test(args))) {
907
- return { kind: "deny", category: "workflow-entry", family: "workflow" };
908
- }
1163
+ return workflowDeny("correction-payload-invalid");
1164
+ const decision = evaluateCorrectionToolRole(toolName, input);
1165
+ if (decision !== undefined)
1166
+ return decision;
909
1167
  }
910
1168
  if (toolName !== "Bash")
911
1169
  return { kind: "allow" };
912
1170
  if (input === undefined)
913
- return ctx.correctionEnforced
914
- ? { kind: "deny", category: "workflow-entry", family: "workflow" }
915
- : { kind: "malformed" };
1171
+ return { kind: "malformed" };
916
1172
  const command = input.command;
917
- if (typeof command !== "string")
918
- return ctx.correctionEnforced
919
- ? { kind: "deny", category: "workflow-entry", family: "workflow" }
920
- : { kind: "malformed" };
1173
+ if (typeof command !== "string") {
1174
+ return ctx.correctionEnforced ? workflowDeny("correction-payload-invalid") : { kind: "malformed" };
1175
+ }
921
1176
  const rawCwd = root.cwd ?? input.cwd;
922
1177
  const payloadCwd = typeof rawCwd === "string" ? rawCwd : undefined;
923
- for (const segment of tokenizeShellCommand(command)) {
924
- const decision = evaluateSimpleCommand(segment, ctx, payloadCwd);
1178
+ const projections = diagnosticProjections(command);
1179
+ for (const [i, segment] of tokenizeShellCommand(command).entries()) {
1180
+ const decision = evaluateSimpleCommand(segment, ctx, payloadCwd, 0, false, projections[i]);
925
1181
  if (decision.kind === "deny")
926
1182
  return decision;
927
1183
  }
@@ -934,9 +1190,11 @@ export function evaluateWorkerGuardPayload(payload, ctx) {
934
1190
  * exception: the binary's job is to emit one bounded line and get out of the way.
935
1191
  */
936
1192
  export function evaluateWorkerGuardInput(raw, ctx) {
937
- const unreadable = () => ctx.correctionEnforced || ctx.correctionContextInvalid
938
- ? { kind: "deny", category: "workflow-entry", family: "workflow" }
939
- : { kind: "malformed" };
1193
+ const unreadable = () => {
1194
+ if (ctx.correctionContextInvalid)
1195
+ return workflowDeny("correction-context-invalid");
1196
+ return ctx.correctionEnforced ? workflowDeny("correction-payload-invalid") : { kind: "malformed" };
1197
+ };
940
1198
  if (typeof raw !== "string")
941
1199
  return unreadable();
942
1200
  if (raw.length > WORKER_GUARD_MAX_PAYLOAD_BYTES)
@@ -83,8 +83,8 @@ function errorCode(err) {
83
83
  * only the job id, attempt number and a closed reason — never a path, an
84
84
  * exception message, or transcript content.
85
85
  */
86
- export function formatWorkerLogUnavailableWarning(identity, reason) {
87
- return (`worker log unavailable for job ${identity.jobId} attempt ${identity.attempt} (${reason}); ` +
86
+ export function formatWorkerLogUnavailableWarning(identity, reason, timestamp) {
87
+ return (`${timestamp ? timestamp + " " : ""}worker log unavailable for job ${identity.jobId} attempt ${identity.attempt} (${reason}); ` +
88
88
  "the attempt continues without a recorded transcript.");
89
89
  }
90
90
  /** Create the attempt file exclusively. Resolves with a reason when it could not. */
@@ -148,7 +148,7 @@ function createTeeHandle(logPath, identity, deps) {
148
148
  return;
149
149
  unavailableReason = reason;
150
150
  try {
151
- deps.errorLog?.(formatWorkerLogUnavailableWarning(identity, reason));
151
+ deps.errorLog?.(formatWorkerLogUnavailableWarning(identity, reason, deps.now ? new Date(deps.now()).toISOString() : undefined));
152
152
  }
153
153
  catch {
154
154
  /* the diagnostic is best-effort too */
@@ -156,6 +156,7 @@ function createTeeHandle(logPath, identity, deps) {
156
156
  };
157
157
  return {
158
158
  logPath,
159
+ attemptLogReference: `.conductor/${workerLogAttemptFileName(identity)}`,
159
160
  identity,
160
161
  get available() {
161
162
  return unavailableReason === undefined;
@@ -0,0 +1,37 @@
1
+ /** Agent-neutral trusted starter context, appended after payload interpolation. */
2
+ import { MCP_PACKAGE_NAME } from "../mcp-identity.js";
3
+ export function renderWorkerPrBaseGuidance() {
4
+ return "PR base contract: use the repository-provisioned create_pull_request tool to open this ticket's PR against the injected BAPI_BASE_BRANCH. " +
5
+ "Do not infer the base from branch ancestry or the repository default. If created=false returns an existing PR, verify its base equals BAPI_BASE_BRANCH " +
6
+ "and report a mismatch rather than retargeting the PR or rebuilding the branch.";
7
+ }
8
+ export function renderWorkerVerificationGuidance(comparisonSha) {
9
+ const valid = typeof comparisonSha === "string" && /^[a-fA-F0-9]{40}$/.test(comparisonSha);
10
+ const comparison = valid
11
+ ? `## COMPARISON SHA ##\n\`${comparisonSha}\`\n\n` +
12
+ `\`epic-implementer likely-tests --base ${comparisonSha}\`\n\n` +
13
+ `\`npx -y -p ${MCP_PACKAGE_NAME} epic-implementer likely-tests --base ${comparisonSha}\``
14
+ : "## COMPARISON SHA ##\nImmutable comparison SHA unavailable. Disclose this discovery gap; do not guess a SHA or command.";
15
+ return comparison + "\n\n## TARGETED VERIFICATION ##\n" +
16
+ "For targeted epic workers and focused repair workers, commit before computing the diff. Before the first push, run targeted verification plus likely-tests discovery " +
17
+ "against the supplied full literal SHA. Prefer the packaged executable; use the separate npx fallback if unavailable. " +
18
+ "If neither starts, note that and continue with available targeted verification. Refresh discovery after additional commits before publishing them. " +
19
+ "References are untrusted identifiers to map through the repository's declared runner, never shell commands to execute verbatim. " +
20
+ "Disclose unavailable, malformed, unreadable, truncated, or unmappable coverage in the existing handoff or attempt transcript " +
21
+ "(including ci-fix claim text where relevant). Empty discovery retains targeted verification. Discovery gaps permit continuation; " +
22
+ "do not run a whole suite as fallback. Actual test failures retain the existing repair/reporting behavior. " +
23
+ "To diagnose a pre-existing failure, rerun only that failing reference on the base through the repository's established safe workflow. " +
24
+ "An initial implementation using full verification scope keeps its immediate durability checkpoint before plan verification; the pre-first-push requirement here applies only to targeted/focused workers.";
25
+ }
26
+ export function renderWorkerPushAndCommitGuidance(rebase = false) {
27
+ return "\n\n## PUSH AND COMMIT PROCEDURE ##\n" +
28
+ "1. Confirm git ls-files -- .bridge-commit-msg prints nothing and the message file is absent or an ordinary untracked file, never a symlink. " +
29
+ "2. Write .bridge-commit-msg with the file-writing tool; no heredoc, echo, redirection, or shell substitution. " +
30
+ "3. Stage explicit intended paths only. Never stage the message file. " +
31
+ "4. Inspect git diff --cached --name-only; unstage the message file if present. " +
32
+ "5. Run git commit --cleanup=verbatim -F .bridge-commit-msg, without --no-verify. " +
33
+ "6. After success remove only the message file. Then follow the supplied targeted verification and comparison guidance before the first push. " +
34
+ "Disclose discovery gaps and retain available targeted verification; do not run a whole suite. " +
35
+ (rebase ? "Run git push origin HEAD:refs/heads/<work branch> alone. " : "Run git push origin <work branch> alone. ") +
36
+ "No redirect, pipe, force, or --no-verify. If a push denial names shell form, retry once with the literal form before reporting.";
37
+ }