@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.
- package/README.md +421 -83
- package/build/agent-capabilities/probes.js +15 -5
- package/build/agent-launchers/claude-executor-adapter.js +70 -16
- package/build/agent-launchers/executor-adapter.js +7 -4
- package/build/agents.generated.js +22 -2
- package/build/bitbucket-workspace-remote.js +136 -0
- package/build/bootstrap-project-identity.js +24 -0
- package/build/bounded-wait.js +67 -10
- package/build/bridge-permissions.js +829 -0
- package/build/claude-settings.js +18 -0
- package/build/cli-help.js +21 -0
- package/build/command-assets-doctor.js +24 -70
- package/build/command-provisioning.js +29 -5
- package/build/commands.generated.js +12 -12
- package/build/conductor/bridge-api-client.js +857 -1
- package/build/conductor/bring-up-facts.js +1 -1
- package/build/conductor/command-drift-production.js +92 -0
- package/build/conductor/command-drift.js +89 -0
- package/build/conductor/doctor.js +72 -29
- package/build/conductor/epic-branch-ci-inspection.js +1375 -0
- package/build/conductor/epic-implementer-cli.js +98 -28
- package/build/conductor/errors.js +4 -3
- package/build/conductor/gate-discovery.js +890 -0
- package/build/conductor/guided-repair.js +6 -2
- package/build/conductor/hook-runtime-qualification.js +1 -1
- package/build/conductor/install-doctor.js +15 -3
- package/build/conductor/json-output.js +63 -0
- package/build/conductor/merge-execution-contract.js +27 -8
- package/build/conductor/merge-execution-eligibility.js +3 -2
- package/build/conductor/pr-ci-producer.js +1 -1
- package/build/conductor/readiness-cli.js +113 -12
- package/build/conductor/readiness-sections.js +410 -9
- package/build/conductor/readiness.js +388 -33
- package/build/conductor/recovery-cli.js +310 -12
- package/build/conductor/recovery-operations.js +167 -3
- package/build/conductor/repair-contract.js +54 -3
- package/build/conductor/repository-workflow-remedy.js +125 -0
- package/build/conductor/tools.js +2 -2
- package/build/conductor-bundle-artifacts.js +8 -0
- package/build/connect-bitbucket-api.js +41 -6
- package/build/connect-bitbucket.js +106 -33
- package/build/credential-store.js +207 -47
- package/build/docs.generated.js +62 -3
- package/build/doctor.js +92 -1
- package/build/drive-epic-key.js +6 -5
- package/build/drive-epic.js +129 -61
- package/build/epic-coherence-input.js +2 -1
- package/build/epic-coherence-input.test-fixtures.js +4 -3
- package/build/epic-implementer-bin.js +1379 -28
- package/build/epic-implementer-bundle-cli.js +2 -2
- package/build/epic-workarounds-path.js +39 -0
- package/build/executor/cli.js +33 -2
- package/build/executor/completion-contract.js +66 -10
- package/build/executor/completion-supervision.js +4 -0
- package/build/executor/deps.js +4 -1
- package/build/executor/env.js +19 -0
- package/build/executor/gate-qualification-job.js +499 -0
- package/build/executor/guarded-fast-forward-push.js +129 -0
- package/build/executor/heartbeat.js +1 -1
- package/build/executor/http-client.js +20 -0
- package/build/executor/integration-job.js +122 -8
- package/build/executor/job-errors.js +33 -1
- package/build/executor/job-runner.js +813 -87
- package/build/executor/likely-tests.js +290 -0
- package/build/executor/live-worker-registry.js +1 -1
- package/build/executor/observation.js +37 -3
- package/build/executor/packaged-recipe.js +106 -0
- package/build/executor/permissions.js +115 -28
- package/build/executor/platform.js +1 -1
- package/build/executor/preflight.js +112 -26
- package/build/executor/process-containment.js +36 -12
- package/build/executor/process.js +15 -7
- package/build/executor/prompt-spec.js +33 -1
- package/build/executor/recently-failed-tests-file.js +71 -0
- package/build/executor/reconcile-overlap-job.js +41 -23
- package/build/executor/required-verification.js +1 -1
- package/build/executor/required-work-ledger.js +111 -18
- package/build/executor/resume-pre-spawn.js +2 -0
- package/build/executor/retirement.js +69 -0
- package/build/executor/runner.js +122 -13
- package/build/executor/service-unit.js +7 -1
- package/build/executor/test-clock.js +15 -0
- package/build/executor/types.js +32 -0
- package/build/executor/viewer-tabs.js +58 -8
- package/build/executor/worker-command.js +23 -7
- package/build/executor/worker-diagnostics.js +37 -0
- package/build/executor/worker-finalization.js +113 -34
- package/build/executor/worker-guard-hook.js +316 -58
- package/build/executor/worker-log.js +4 -3
- package/build/executor/worker-verification-guidance.js +37 -0
- package/build/executor/worktree.js +166 -8
- package/build/first-build-advisory.js +97 -0
- package/build/generated-assets-advisory.js +14 -0
- package/build/generated-assets-catalog.js +66 -0
- package/build/generated-assets-context.js +46 -0
- package/build/generated-assets-dependencies.js +26 -0
- package/build/generated-assets-doctor.js +50 -0
- package/build/generated-assets-health.js +65 -0
- package/build/generated-assets-host.js +17 -0
- package/build/generated-assets-inventory.js +17 -0
- package/build/generated-assets-paths.js +60 -0
- package/build/generated-assets-publisher.js +133 -0
- package/build/generated-assets-remedy.js +25 -0
- package/build/generated-assets-repair.js +72 -0
- package/build/generated-assets-session.js +71 -0
- package/build/generated-assets-test-support.js +35 -0
- package/build/index.js +1224 -1054
- package/build/init.js +54 -177
- package/build/install-bridge-conductor-gate.js +1372 -0
- package/build/install-bridge-conductor.js +296 -41
- package/build/install-bridge.js +547 -40
- package/build/mcp-install-state.js +50 -4
- package/build/merge-workflow-install.js +3 -4
- package/build/phase-context-git-probe.js +68 -0
- package/build/phase-result-artifacts.js +173 -5
- package/build/pipeline-utils.js +6 -2
- package/build/pipelines.generated.js +34 -160
- package/build/plan-execution-ledger.js +2 -32
- package/build/plane/alembic-head.js +12 -3
- package/build/plane/base-worktree-guard.js +147 -0
- package/build/plane/cli.js +217 -17
- package/build/plane/code-snapshot.js +468 -0
- package/build/plane/defaults.js +48 -3
- package/build/plane/endpoint.js +41 -0
- package/build/plane/manifest.js +136 -2
- package/build/plane/member-roster.js +94 -19
- package/build/plane/preflight.js +83 -2
- package/build/plane/process-cwd.js +67 -0
- package/build/plane/restart.js +542 -0
- package/build/plane/shutdown.js +31 -0
- package/build/plane/status.js +143 -8
- package/build/plane/supervisor.js +586 -179
- package/build/plane/types.js +54 -2
- package/build/printed-remedy-commands.js +52 -0
- package/build/readiness-check.js +1 -1
- package/build/readme.generated.js +1 -1
- package/build/repair-assets.js +93 -0
- package/build/review-generation.js +22 -0
- package/build/run-unit-tests-launcher.js +130 -49
- package/build/secret-redaction.js +33 -0
- package/build/setup-epic.js +152 -22
- package/build/sfcc/config.js +9 -0
- package/build/sfcc/credentials.js +100 -38
- package/build/sfcc/hostname.js +67 -0
- package/build/sfcc/permissions.js +17 -13
- package/build/sfcc/register.js +1 -1
- package/build/sfcc/tool-wrapper.js +39 -43
- package/build/sfcc/write-grants.js +17 -11
- package/build/sfcc/write-result.js +4 -3
- package/build/sfcc/writes-site-preference.js +20 -8
- package/build/start-tickets-prereqs.js +70 -16
- package/build/start-tickets.js +208 -36
- package/build/tool-error-envelope.js +61 -0
- package/build/tool-surface-gating.js +36 -0
- package/build/upgrade-cli.js +55 -1
- package/build/version.generated.js +3 -3
- package/build/visual-diff.js +22 -1
- package/build/worker-guard-hook-bin.js +4 -6
- package/build/worker-guard-hook-main.js +47 -0
- package/build/worktree-core.js +145 -6
- package/docs/EPIC-IMPLEMENTER-INSTALL.md +867 -0
- package/docs/EPIC-IMPLEMENTER.md +728 -0
- package/docs/install/mcp-tool-integrations.md +24 -15
- package/docs/install/sfcc-integration.md +12 -4
- package/package.json +3 -3
- package/pipelines/greenfield-setup.json +4 -128
- package/pipelines/implement-ticket.json +3 -2
- package/pipelines/plan-epic.json +0 -5
- package/pipelines/review-ticket.json +1 -1
- package/docs/CONDUCTOR.md +0 -605
- 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/
|
|
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
|
+
}
|