@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
@@ -0,0 +1,290 @@
1
+ /**
2
+ * Local, read-only discovery. Optional .bridge/verification-hints.json is
3
+ * {version:1,pins:[{paths:["exact/file", "directory/"],tests:["reference"]}]}.
4
+ * Exact paths match exactly; directory prefixes match at directory boundaries.
5
+ * Optional .conductor/recently-failed-tests.json is
6
+ * {version:1,tests:["most-recent-first reference"],truncated:false}.
7
+ * Child 3 writes recent failures under the worker worktree before each spawn.
8
+ * References are data only, never commands. Priority: recent failures, ordered
9
+ * pins, sorted inferred tracked tests; exact deduplication precedes the limit.
10
+ * Missing/malformed/unreadable/truncated sources degrade advisory coverage.
11
+ */
12
+ import path from "node:path";
13
+ import { constants } from "node:fs";
14
+ import { lstat, open } from "node:fs/promises";
15
+ import { execFile } from "node:child_process";
16
+ export const RECENTLY_FAILED_TESTS_PATH = ".conductor/recently-failed-tests.json";
17
+ export const RECENTLY_FAILED_TESTS_MAX_COUNT = 20;
18
+ export const TEST_REFERENCE_MAX_LENGTH = 512;
19
+ export const MAX_OUTPUT_REFERENCES = 100;
20
+ export const MAX_SCAN_FILE_BYTES = 256 * 1024;
21
+ export const MAX_SCAN_TOTAL_BYTES = 16 * 1024 * 1024;
22
+ export const MAX_CANDIDATE_FILES = 10_000;
23
+ export const MAX_OPTIONAL_JSON_BYTES = 256 * 1024;
24
+ export const GIT_TIMEOUT_MS = 10_000;
25
+ export const GIT_MAX_STDOUT_BYTES = 4 * 1024 * 1024;
26
+ export const DISCOVERY_NOTES = {
27
+ invalid_arguments: "Likely-tests discovery unavailable: expected only --base and a full commit SHA.",
28
+ git_unavailable: "Likely-tests git discovery unavailable; retain available targeted verification.",
29
+ hints_missing: "Optional verification hints are absent.",
30
+ data_unavailable: "Optional discovery data is malformed or unreadable.",
31
+ skipped_entries: "Invalid discovery entries were skipped.",
32
+ scan_unavailable: "Some test candidates could not be read safely.",
33
+ truncated: "Likely-tests coverage is truncated by a resource bound.",
34
+ empty: "No likely tests discovered; retain targeted verification.",
35
+ };
36
+ export function isFullCommitSha(value) { return /^[a-fA-F0-9]{40}$/.test(value); }
37
+ function object(value) {
38
+ return value !== null && typeof value === "object" && !Array.isArray(value)
39
+ ? value : undefined;
40
+ }
41
+ export function validTestReference(value) {
42
+ return typeof value === "string" && value.trim().length > 0 &&
43
+ value.length <= TEST_REFERENCE_MAX_LENGTH && !/[\x00-\x1f\x7f-\x9f\u2028\u2029]/.test(value);
44
+ }
45
+ function validRepoPath(value, directory = false) {
46
+ const candidate = directory && value.endsWith("/") ? value.slice(0, -1) : value;
47
+ return !!candidate && !path.isAbsolute(candidate) && !/[:\\\x00-\x1f\x7f-\x9f\u2028\u2029]/.test(candidate) &&
48
+ candidate.split("/").every(p => p !== "" && p !== "." && p !== "..");
49
+ }
50
+ function validReference(value) {
51
+ if (!validTestReference(value))
52
+ return false;
53
+ const file = value.split("::")[0];
54
+ const fileShaped = value.includes("/") || /\.(?:py|[cm]?[jt]sx?)(?:::|$)/.test(value) || value.includes("::");
55
+ return !fileShaped || validRepoPath(file);
56
+ }
57
+ export function parseRecentlyFailedTests(value) {
58
+ const doc = object(value);
59
+ if (!doc || doc.version !== 1 || !Array.isArray(doc.tests) || typeof doc.truncated !== "boolean")
60
+ return { available: false };
61
+ const tests = [];
62
+ let skipped = false;
63
+ for (const test of doc.tests) {
64
+ if (!validReference(test)) {
65
+ skipped = true;
66
+ continue;
67
+ }
68
+ if (tests.length < RECENTLY_FAILED_TESTS_MAX_COUNT)
69
+ tests.push(test);
70
+ }
71
+ return { available: true, value: { version: 1, tests,
72
+ truncated: doc.truncated || doc.tests.length > RECENTLY_FAILED_TESTS_MAX_COUNT }, skipped };
73
+ }
74
+ function parseHints(value) {
75
+ const doc = object(value);
76
+ if (!doc || doc.version !== 1 || !Array.isArray(doc.pins))
77
+ return undefined;
78
+ const pins = [];
79
+ let skipped = false;
80
+ for (const entry of doc.pins) {
81
+ const pin = object(entry);
82
+ if (!pin || !Array.isArray(pin.paths) || !Array.isArray(pin.tests)) {
83
+ skipped = true;
84
+ continue;
85
+ }
86
+ const paths = pin.paths.filter((p) => typeof p === "string" && validRepoPath(p, true));
87
+ const tests = pin.tests.filter(validReference);
88
+ if (paths.length !== pin.paths.length || tests.length !== pin.tests.length)
89
+ skipped = true;
90
+ if (paths.length && tests.length)
91
+ pins.push({ paths, tests });
92
+ else
93
+ skipped = true;
94
+ }
95
+ return { pins, skipped };
96
+ }
97
+ /** No-follow and fstat protect the final component; lstat every ancestor first.
98
+ * Reads at most limit+1 bytes even if a file grows between stat and read.
99
+ */
100
+ async function containedText(root, relative, limit) {
101
+ if (!validRepoPath(relative))
102
+ throw new Error("invalid_path");
103
+ let current = root;
104
+ for (const part of relative.split("/")) {
105
+ current = path.join(current, part);
106
+ const info = await lstat(current);
107
+ if (info.isSymbolicLink())
108
+ throw new Error("symlink");
109
+ if (current !== path.join(root, relative) && !info.isDirectory())
110
+ throw new Error("ancestor");
111
+ }
112
+ const handle = await open(current, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0));
113
+ try {
114
+ const info = await handle.stat();
115
+ if (!info.isFile())
116
+ throw new Error("not_regular");
117
+ if (info.size > limit)
118
+ throw new Error("bound");
119
+ const buffer = Buffer.alloc(limit + 1);
120
+ const { bytesRead } = await handle.read(buffer, 0, buffer.length, 0);
121
+ if (bytesRead > limit)
122
+ throw new Error("bound");
123
+ return buffer.subarray(0, bytesRead).toString("utf8");
124
+ }
125
+ finally {
126
+ await handle.close();
127
+ }
128
+ }
129
+ async function boundedGit(cwd, argv) {
130
+ return new Promise((resolve, reject) => {
131
+ execFile("git", argv, { cwd, shell: false, timeout: GIT_TIMEOUT_MS,
132
+ maxBuffer: GIT_MAX_STDOUT_BYTES, encoding: "utf8" }, (error, stdout) => {
133
+ if (error)
134
+ reject(new Error("git_unavailable"));
135
+ else
136
+ resolve(stdout);
137
+ });
138
+ });
139
+ }
140
+ function nulPaths(output) {
141
+ if (output && !output.endsWith("\0"))
142
+ throw new Error("git_unavailable");
143
+ return output.split("\0").filter(Boolean);
144
+ }
145
+ const ordinal = (a, b) => a < b ? -1 : a > b ? 1 : 0;
146
+ function testCandidate(file) {
147
+ const base = path.posix.basename(file);
148
+ return /^test_.*\.py$|^.*_test\.py$|\.(?:test|spec)\.[cm]?[jt]sx?$/.test(base);
149
+ }
150
+ /** Discover declared Python source roots without importing any project code. */
151
+ async function pythonRoots(root, read) {
152
+ const roots = [""];
153
+ for (const file of ["pyproject.toml", "pytest.ini", "setup.cfg"]) {
154
+ try {
155
+ const text = await read(root, file, MAX_SCAN_FILE_BYTES);
156
+ for (const match of text.matchAll(/(?:pythonpath|package-dir|package_dir)\s*=\s*([^\n]+)/g)) {
157
+ for (const token of match[1].matchAll(/["']([^"']+)["']|\b([a-zA-Z][\w/-]*)\b/g)) {
158
+ const value = token[1] ?? token[2];
159
+ if (validRepoPath(value))
160
+ roots.push(value + "/");
161
+ }
162
+ }
163
+ }
164
+ catch { /* absent runner declarations are normal */ }
165
+ }
166
+ return [...new Set(roots)];
167
+ }
168
+ export async function resolveLikelyTests(options) {
169
+ const references = [];
170
+ const diagnostics = new Set();
171
+ const add = (items) => {
172
+ for (const item of items)
173
+ if (!references.includes(item))
174
+ references.push(item);
175
+ };
176
+ const finish = () => {
177
+ if (references.length > MAX_OUTPUT_REFERENCES)
178
+ diagnostics.add("truncated");
179
+ if (!references.length)
180
+ diagnostics.add("empty");
181
+ return { references: references.slice(0, MAX_OUTPUT_REFERENCES), diagnostics: [...diagnostics] };
182
+ };
183
+ if (!isFullCommitSha(options.comparisonSha)) {
184
+ diagnostics.add("invalid_arguments");
185
+ return finish();
186
+ }
187
+ const git = options.git ?? boundedGit;
188
+ const read = options.readText ?? containedText;
189
+ let root;
190
+ try {
191
+ root = (await git(options.cwd, ["rev-parse", "--show-toplevel"])).trim();
192
+ if (!path.isAbsolute(root))
193
+ throw new Error("root");
194
+ }
195
+ catch {
196
+ diagnostics.add("git_unavailable");
197
+ return finish();
198
+ }
199
+ try {
200
+ const raw = await read(root, RECENTLY_FAILED_TESTS_PATH, MAX_OPTIONAL_JSON_BYTES);
201
+ const recent = parseRecentlyFailedTests(JSON.parse(raw));
202
+ if (!recent.available)
203
+ diagnostics.add("data_unavailable");
204
+ else {
205
+ add(recent.value.tests);
206
+ if (recent.skipped)
207
+ diagnostics.add("skipped_entries");
208
+ if (recent.value.truncated)
209
+ diagnostics.add("truncated");
210
+ }
211
+ }
212
+ catch (error) {
213
+ if (error.code !== "ENOENT")
214
+ diagnostics.add("data_unavailable");
215
+ }
216
+ let hints;
217
+ try {
218
+ hints = parseHints(JSON.parse(await read(root, ".bridge/verification-hints.json", MAX_OPTIONAL_JSON_BYTES)));
219
+ if (!hints)
220
+ diagnostics.add("data_unavailable");
221
+ else if (hints.skipped)
222
+ diagnostics.add("skipped_entries");
223
+ }
224
+ catch (error) {
225
+ diagnostics.add(error.code === "ENOENT" ? "hints_missing" : "data_unavailable");
226
+ }
227
+ let changed;
228
+ try {
229
+ const verified = (await git(root, ["rev-parse", "--verify", `${options.comparisonSha}^{commit}`])).trim();
230
+ if (!isFullCommitSha(verified))
231
+ throw new Error("commit");
232
+ const paths = nulPaths(await git(root, ["diff", "--name-only", "-z", `${options.comparisonSha}..HEAD`]));
233
+ changed = paths.filter(p => validRepoPath(p));
234
+ if (changed.length !== paths.length)
235
+ diagnostics.add("skipped_entries");
236
+ }
237
+ catch {
238
+ diagnostics.add("git_unavailable");
239
+ return finish();
240
+ }
241
+ for (const pin of hints?.pins ?? []) {
242
+ if (pin.paths.some(p => changed.some(c => p.endsWith("/") ? c.startsWith(p) : c === p)))
243
+ add(pin.tests);
244
+ }
245
+ let tracked;
246
+ try {
247
+ const paths = nulPaths(await git(root, ["ls-files", "-z"]));
248
+ tracked = paths.filter(p => validRepoPath(p));
249
+ if (tracked.length !== paths.length)
250
+ diagnostics.add("skipped_entries");
251
+ }
252
+ catch {
253
+ diagnostics.add("git_unavailable");
254
+ return finish();
255
+ }
256
+ const candidates = tracked.filter(testCandidate).sort(ordinal);
257
+ if (candidates.length > MAX_CANDIDATE_FILES)
258
+ diagnostics.add("truncated");
259
+ const roots = await pythonRoots(root, read);
260
+ const literals = new Set(changed);
261
+ for (const file of changed) {
262
+ if (/\.[cm]?[jt]sx?$/.test(file)) {
263
+ literals.add(path.posix.basename(file));
264
+ literals.add(path.posix.basename(file).replace(/\.[cm]?[jt]sx?$/, ".js"));
265
+ }
266
+ if (file.endsWith(".py"))
267
+ for (const source of roots) {
268
+ if (file.startsWith(source))
269
+ literals.add(file.slice(source.length, -3).replaceAll("/", ".").replace(/\.__init__$/, ""));
270
+ }
271
+ }
272
+ let scanned = 0;
273
+ for (const candidate of candidates.slice(0, MAX_CANDIDATE_FILES)) {
274
+ try {
275
+ const text = await read(root, candidate, MAX_SCAN_FILE_BYTES);
276
+ const size = Buffer.byteLength(text);
277
+ if (scanned + size > MAX_SCAN_TOTAL_BYTES) {
278
+ diagnostics.add("truncated");
279
+ break;
280
+ }
281
+ scanned += size;
282
+ if ([...literals].some(literal => literal && text.includes(literal)))
283
+ add([candidate]);
284
+ }
285
+ catch (error) {
286
+ diagnostics.add(error.message === "bound" ? "truncated" : "scan_unavailable");
287
+ }
288
+ }
289
+ return finish();
290
+ }
@@ -43,7 +43,7 @@ export function createLiveWorkerRegistry() {
43
43
  return;
44
44
  alreadyRequested.add(controller);
45
45
  try {
46
- controller.requestTermination();
46
+ controller.requestTermination("executor_shutdown");
47
47
  }
48
48
  catch {
49
49
  // Fail-open, and deliberately silent. A controller that throws is a defect
@@ -1,3 +1,4 @@
1
+ import { createWorkerDiagnosticsState } from "./worker-diagnostics.js";
1
2
  /**
2
3
  * Collect load-bearing git telemetry from a worktree. Individual git failures
3
4
  * degrade the telemetry (fields omitted) rather than throwing — a degraded
@@ -34,9 +35,14 @@ export async function collectGitTelemetry(deps, worktreePath, baseBranch) {
34
35
  /* degraded: omit last_commit_sha */
35
36
  }
36
37
  try {
38
+ // `--no-optional-locks`: a plain `git status` refreshes the index and takes
39
+ // `.git/index.lock`. This runs on every heartbeat against the LIVE worker's
40
+ // worktree, so without it a worker's own `git add`/`commit` can fail with
41
+ // "index.lock: File exists".
37
42
  const status = await deps.runCommand("git", [
38
43
  "-C",
39
44
  worktreePath,
45
+ "--no-optional-locks",
40
46
  "status",
41
47
  "--porcelain",
42
48
  ]);
@@ -293,7 +299,7 @@ const RATE_LIMIT_CONSEQUENCE_TEXT = "This warning is advisory only and triggers
293
299
  "rejection and the server advertised capacity recovery for this job; otherwise " +
294
300
  "the failure follows ordinary failure handling.";
295
301
  /** Fixed runbook reference appended to every rate-limit diagnostic (BAPI-898). */
296
- const RATE_LIMIT_RUNBOOK_REFERENCE = "See docs/claude/epic-implementer-v2-operator-runbook.md §8.";
302
+ const RATE_LIMIT_RUNBOOK_REFERENCE = "See docs/epic-implementer/operator-runbook.md §8.";
297
303
  /**
298
304
  * Render the operator-facing rate-limit line (BAPI-828, BAPI-898).
299
305
  *
@@ -598,7 +604,10 @@ export const MAX_BUFFERED_ADVISORY_LINE_CHARS = 1_048_576;
598
604
  * on bad input, so ordinary success never depends on it either.
599
605
  */
600
606
  export function createObservationState(deps, options) {
607
+ const diagnostics = createWorkerDiagnosticsState();
601
608
  let git = {};
609
+ let publication;
610
+ let publicationInvalid = false;
602
611
  const advisory = {};
603
612
  let exitCode;
604
613
  let attemptStartSha;
@@ -676,6 +685,7 @@ export function createObservationState(deps, options) {
676
685
  }
677
686
  };
678
687
  const consumeLine = (line) => {
688
+ diagnostics.observeLine(line);
679
689
  if (options.advisoryParserEnabled)
680
690
  consumeAdvisoryLine(line);
681
691
  if (terminalCapacityParserEnabled)
@@ -684,8 +694,6 @@ export function createObservationState(deps, options) {
684
694
  return {
685
695
  recordStdout(chunk) {
686
696
  advisory.last_stdout_at = new Date(deps.now()).toISOString();
687
- if (!options.advisoryParserEnabled && !terminalCapacityParserEnabled)
688
- return;
689
697
  if (typeof chunk !== "string" || chunk.length === 0)
690
698
  return;
691
699
  // BAPI-828: line assembly replaces the previous per-chunk `split("\n")`.
@@ -748,8 +756,34 @@ export function createObservationState(deps, options) {
748
756
  git() {
749
757
  return git;
750
758
  },
759
+ allocateAttemptLog(identity, allocated) {
760
+ diagnostics.allocateLog(identity, allocated);
761
+ },
762
+ workerDiagnostics() { return diagnostics.snapshot(); },
763
+ recordPublication(fact) {
764
+ if (publicationInvalid)
765
+ return;
766
+ const valid = (sha) => /^(?:[0-9a-f]{40}|[0-9a-f]{64})$/i.test(sha);
767
+ if (!valid(fact.from_sha) || !valid(fact.to_sha) || fact.from_sha === fact.to_sha)
768
+ return;
769
+ if (!publication) {
770
+ publication = { ...fact };
771
+ return;
772
+ }
773
+ if (publication.jobId !== fact.jobId || publication.branch !== fact.branch ||
774
+ publication.to_sha !== fact.from_sha) {
775
+ publication = undefined;
776
+ publicationInvalid = true;
777
+ return;
778
+ }
779
+ publication = { ...publication, to_sha: fact.to_sha };
780
+ },
751
781
  snapshot() {
752
782
  const residue = { ...git };
783
+ if (publication) {
784
+ const observed = { from_sha: publication.from_sha, to_sha: publication.to_sha };
785
+ residue.published_head = observed;
786
+ }
753
787
  if (advisory.phase_hint)
754
788
  residue.phase_hint = advisory.phase_hint;
755
789
  if (advisory.last_stdout_at)
@@ -0,0 +1,106 @@
1
+ /**
2
+ * Packaged-recipe delivery for executor workers (BAPI-1392).
3
+ *
4
+ * Command provisioning is fill-only (BAPI-664): a command file the customer
5
+ * already committed under `.claude/commands/` is a customer asset and is never
6
+ * refreshed, so a worker that resolves `/implement-ticket` itself would run a
7
+ * stale committed copy. This module supplies the CURRENT packaged recipe instead,
8
+ * by expanding it into the worker prompt before the adapter builds the launch.
9
+ *
10
+ * DELIVERY PATH: prompt expansion. Both prompt transports (`-p <prompt>` argv and
11
+ * the held-stdin stream-json first message) carry exactly the prompt the runner
12
+ * hands the adapter, so one expansion reaches both with no launcher-option
13
+ * dependence. Nothing is written under the worker worktree and no Git index flag
14
+ * is touched; the worktree's command files keep the bytes provisioning left.
15
+ *
16
+ * WHAT IS RECOGNIZED (and nothing else):
17
+ * 1. A LEADING invocation — the prompt's first token is `/<name>` — which is how
18
+ * the synthesized `/implement-ticket <KEY> --auto` arrives. Its arguments are
19
+ * everything after the name, exactly as the slash command would receive them
20
+ * (so an appended PR-base instruction stays part of `$ARGUMENTS`). A BAPI-1367
21
+ * continuation notice may precede it; the notice's own mention of the command
22
+ * is never an invocation.
23
+ * 2. A NESTED invocation written as ``Run `/<name> <args>` `` — the shape the
24
+ * server-built `spec_review` prompt uses for `/review-ticket`. The span is
25
+ * replaced by a pointer and the recipe is appended once, below the prompt.
26
+ * Quoted ticket text, arbitrary slash-prefixed prose and a bare mention are left
27
+ * alone. A name is recognized only when `<name>.md` is a key of the packaged
28
+ * catalog, and never when the command is in the job's customized list (those keep
29
+ * the customer's committed copy). A prompt with no recognized invocation is
30
+ * returned byte-identical. Delivered recipes are NOT expanded recursively.
31
+ */
32
+ import { COMMANDS } from "../commands.generated.js";
33
+ /** Marker pair of the BAPI-1367 continuation notice (see `job-runner.ts`). */
34
+ const CONTINUATION_NOTICE_START = "CONTINUATION: this is a retry.";
35
+ const CONTINUATION_NOTICE_END = "The original instructions follow unchanged.\n\n";
36
+ const NAME = "[a-z][a-z0-9-]*";
37
+ const LEADING_RE = new RegExp(`^/(${NAME})(?=\\s|$)`);
38
+ const NESTED_RE = new RegExp("\\bRun `/(" + NAME + ")(?: ([^`\\n]*))?`", "g");
39
+ function isPackaged(name, catalog) {
40
+ return Object.prototype.hasOwnProperty.call(catalog, `${name}.md`);
41
+ }
42
+ /** Drop a leading `---` frontmatter block, as the slash-command loader does. */
43
+ function stripFrontmatter(text) {
44
+ const match = /^---\r?\n[\s\S]*?\r?\n---\r?\n?/.exec(text);
45
+ return match ? text.slice(match[0].length) : text;
46
+ }
47
+ /** `$ARGUMENTS` substitution with a function replacer so `$&`-style bytes survive. */
48
+ function renderRecipe(recipe, args) {
49
+ return stripFrontmatter(recipe).replace(/\$ARGUMENTS/g, () => args);
50
+ }
51
+ function undeliverable(reason, command) {
52
+ return {
53
+ ok: false,
54
+ reason,
55
+ command,
56
+ error: `packaged recipe for /${command} could not be delivered (reason=${reason})`,
57
+ };
58
+ }
59
+ export function resolvePackagedRecipes(prompt, customizedCommands, catalog = COMMANDS) {
60
+ const customized = new Set(customizedCommands);
61
+ const deliverable = (name) => isPackaged(name, catalog) && !customized.has(name);
62
+ // Offset of the "original instructions" after an optional continuation notice.
63
+ let offset = 0;
64
+ if (prompt.startsWith(CONTINUATION_NOTICE_START)) {
65
+ const end = prompt.indexOf(CONTINUATION_NOTICE_END);
66
+ if (end >= 0)
67
+ offset = end + CONTINUATION_NOTICE_END.length;
68
+ }
69
+ const head = prompt.slice(0, offset);
70
+ const body = prompt.slice(offset);
71
+ // 1. Leading invocation: the whole body becomes the recipe.
72
+ const leading = LEADING_RE.exec(body);
73
+ if (leading && deliverable(leading[1])) {
74
+ const name = leading[1];
75
+ const args = body.slice(leading[0].length).replace(/^\s+/, "");
76
+ const recipe = renderRecipe(catalog[`${name}.md`], args);
77
+ if (recipe.trim().length === 0)
78
+ return undeliverable("empty_recipe", name);
79
+ return { ok: true, prompt: `${head}${recipe}`, delivered: [name] };
80
+ }
81
+ // 2. Nested invocations: one appended block per command name.
82
+ const blocks = new Map(); // name -> args
83
+ for (const m of body.matchAll(NESTED_RE)) {
84
+ const name = m[1];
85
+ if (!deliverable(name))
86
+ continue;
87
+ const args = m[2] ?? "";
88
+ const seen = blocks.get(name);
89
+ if (seen !== undefined && seen !== args)
90
+ return undeliverable("conflicting_arguments", name);
91
+ blocks.set(name, args);
92
+ }
93
+ if (blocks.size === 0)
94
+ return { ok: true, prompt, delivered: [] };
95
+ const rewritten = body.replace(NESTED_RE, (whole, name, args) => blocks.has(name)
96
+ ? `Run the packaged /${name} recipe given below with arguments \`${args ?? ""}\``
97
+ : whole);
98
+ const appended = [];
99
+ for (const [name, args] of blocks) {
100
+ const recipe = renderRecipe(catalog[`${name}.md`], args);
101
+ if (recipe.trim().length === 0)
102
+ return undeliverable("empty_recipe", name);
103
+ appended.push(`\n\n=== PACKAGED RECIPE /${name} (arguments: ${args}) ===\n${recipe}\n=== END PACKAGED RECIPE /${name} ===`);
104
+ }
105
+ return { ok: true, prompt: `${head}${rewritten}${appended.join("")}`, delivered: [...blocks.keys()] };
106
+ }