@bridge_gpt/mcp-server 0.2.51 → 0.2.52
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 +24 -8
- package/build/agent-capabilities/probe-context.js +15 -7
- package/build/agent-capabilities/probes.js +42 -6
- package/build/agent-launchers/claude-executor-adapter.js +98 -14
- package/build/commands.generated.js +1 -1
- package/build/conduct-epic/cut-protocol.js +17 -3
- package/build/conductor/bridge-api-client.js +171 -5
- package/build/conductor/deny-enforcement-preflight.js +107 -10
- package/build/conductor/local-merge.js +170 -11
- package/build/conductor-bin.js +2 -2
- package/build/connect-bitbucket-api.js +370 -0
- package/build/connect-bitbucket.js +437 -0
- package/build/docs.generated.js +1 -1
- package/build/doctor.js +40 -1
- package/build/drive-epic.js +423 -11
- package/build/env-file-link.js +164 -0
- package/build/epic-integration-pr.js +10 -0
- package/build/executor/cli.js +41 -6
- package/build/executor/deps.js +5 -1
- package/build/executor/env-file-guard.js +113 -0
- package/build/executor/env.js +78 -1
- package/build/executor/heartbeat.js +9 -0
- package/build/executor/http-client.js +90 -22
- package/build/executor/job-errors.js +43 -2
- package/build/executor/job-runner.js +130 -28
- package/build/executor/merge-job.js +67 -16
- package/build/executor/permissions.js +106 -0
- package/build/executor/preflight.js +38 -13
- package/build/executor/resume-pre-spawn.js +2 -1
- package/build/executor/runner.js +175 -4
- package/build/executor/service-unit.js +15 -0
- package/build/executor/terminal-mutation.js +22 -1
- package/build/executor/types.js +86 -0
- package/build/executor/worker-command.js +21 -5
- package/build/executor/worker-guard-hook.js +939 -0
- package/build/executor/worker-log.js +56 -0
- package/build/executor/worktree.js +11 -0
- package/build/git-reachability.js +147 -0
- package/build/index.js +514 -121
- package/build/install-bridge.js +95 -0
- package/build/pipelines.generated.js +5 -3
- package/build/plan-epic-conductor-eligibility.js +37 -7
- package/build/plane/cli.js +78 -15
- package/build/plane/defaults.js +165 -0
- package/build/plane/manifest.js +63 -8
- package/build/plane/member-logs.js +6 -0
- package/build/plane/member-roster.js +195 -11
- package/build/plane/preflight.js +43 -0
- package/build/plane/shutdown.js +25 -3
- package/build/plane/status.js +11 -0
- package/build/plane/supervisor.js +343 -14
- package/build/plane/test-fakes.js +43 -0
- package/build/plane/types.js +82 -11
- package/build/pr-base-contract.js +20 -0
- package/build/readme.generated.js +1 -1
- package/build/review-synthesis-config.js +60 -0
- package/build/scripts/executor-protocol-contract-driver.js +311 -0
- package/build/setup-epic.js +560 -139
- package/build/sfcc/log-query.js +2 -1
- package/build/start-tickets-conductor.js +11 -2
- package/build/start-tickets.js +69 -2
- package/build/version.generated.js +3 -3
- package/build/worker-containment-diagnostic.js +97 -0
- package/build/worker-guard-hook-bin.js +6 -0
- package/docs/CONDUCTOR.md +27 -0
- package/docs/install/mcp-tool-integrations.md +3 -2
- package/package.json +3 -2
|
@@ -26,11 +26,39 @@ export function buildWorkerLogPath(worktreePath, platform = process.platform) {
|
|
|
26
26
|
export function renderWorkerLogChunk(chunk, _stream = "stdout") {
|
|
27
27
|
return chunk;
|
|
28
28
|
}
|
|
29
|
+
/**
|
|
30
|
+
* True for a Node-`fs`-shaped "does not exist" rejection (`code: "ENOENT"`).
|
|
31
|
+
* The normal, expected outcome of probing a first-ever attempt's log path —
|
|
32
|
+
* never itself a reason to warn.
|
|
33
|
+
*/
|
|
34
|
+
function isMissingFileError(err) {
|
|
35
|
+
return typeof err === "object" && err !== null && err.code === "ENOENT";
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* The bounded, secret-free archive-preservation warning (BAPI-1021, AC-6).
|
|
39
|
+
* Carries ONLY the stable category and the current/archive path identifiers —
|
|
40
|
+
* never the caught exception's message or stack. Emitted at most once per tee
|
|
41
|
+
* creation, whether the existence probe or the rename itself failed.
|
|
42
|
+
*/
|
|
43
|
+
export function formatWorkerLogArchivePreservationWarning(currentLogPath, archivePath) {
|
|
44
|
+
return archivePath
|
|
45
|
+
? `worker log archive: could not preserve the prior attempt's log ` +
|
|
46
|
+
`(${currentLogPath} -> ${archivePath}); the new attempt continues.`
|
|
47
|
+
: `worker log archive: could not confirm whether a prior attempt's log exists ` +
|
|
48
|
+
`at ${currentLogPath}; the new attempt continues.`;
|
|
49
|
+
}
|
|
29
50
|
/**
|
|
30
51
|
* Create the `.conductor` directory, initialize an empty `worker.log`, and return
|
|
31
52
|
* a tee handle. When an `appendFile` boundary is supplied, chunks are appended
|
|
32
53
|
* incrementally; otherwise a buffered fallback re-writes the accumulated content
|
|
33
54
|
* via `writeFile` so all consumed chunks are preserved in order.
|
|
55
|
+
*
|
|
56
|
+
* BAPI-1021 (AC-6): before writing the new empty log, archives any PRIOR
|
|
57
|
+
* attempt's log under a timestamped filename in the same directory — so a
|
|
58
|
+
* retried job in the same worktree does not silently overwrite what the
|
|
59
|
+
* previous attempt wrote. Best-effort throughout: a missing prior log is the
|
|
60
|
+
* normal first-attempt case (no warning); a probe or rename failure degrades
|
|
61
|
+
* to exactly one bounded warning and tee creation still proceeds.
|
|
34
62
|
*/
|
|
35
63
|
export async function createWorkerLogTee(worktreePath, deps) {
|
|
36
64
|
const platform = deps.platform ?? process.platform;
|
|
@@ -38,7 +66,35 @@ export async function createWorkerLogTee(worktreePath, deps) {
|
|
|
38
66
|
const dir = pathApi.join(worktreePath, CONDUCTOR_WORKER_LOG_DIR);
|
|
39
67
|
const logPath = pathApi.join(dir, CONDUCTOR_WORKER_LOG_FILE);
|
|
40
68
|
await deps.mkdir(dir, { recursive: true });
|
|
69
|
+
if (deps.stat && deps.rename) {
|
|
70
|
+
try {
|
|
71
|
+
await deps.stat(logPath);
|
|
72
|
+
}
|
|
73
|
+
catch (err) {
|
|
74
|
+
if (!isMissingFileError(err)) {
|
|
75
|
+
deps.errorLog?.(formatWorkerLogArchivePreservationWarning(logPath));
|
|
76
|
+
}
|
|
77
|
+
// Missing (or unconfirmable) is the normal first-attempt case either
|
|
78
|
+
// way: fall through to the truncating write below without a rename.
|
|
79
|
+
await deps.writeFile(logPath, "");
|
|
80
|
+
return createTeeHandle(logPath, deps);
|
|
81
|
+
}
|
|
82
|
+
// The current log exists: archive it before truncating.
|
|
83
|
+
const clock = deps.now ?? Date.now;
|
|
84
|
+
const timestamp = new Date(clock()).toISOString().replace(/:/g, "-");
|
|
85
|
+
const archivePath = pathApi.join(dir, `worker.${timestamp}.log`);
|
|
86
|
+
try {
|
|
87
|
+
await deps.rename(logPath, archivePath);
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
deps.errorLog?.(formatWorkerLogArchivePreservationWarning(logPath, archivePath));
|
|
91
|
+
}
|
|
92
|
+
}
|
|
41
93
|
await deps.writeFile(logPath, "");
|
|
94
|
+
return createTeeHandle(logPath, deps);
|
|
95
|
+
}
|
|
96
|
+
/** Shared tee-handle construction, used by every `createWorkerLogTee` return path. */
|
|
97
|
+
function createTeeHandle(logPath, deps) {
|
|
42
98
|
let buffer = "";
|
|
43
99
|
let closed = false;
|
|
44
100
|
return {
|
|
@@ -126,6 +126,17 @@ async function resolveCommitSha(deps, worktreeCwd, ref) {
|
|
|
126
126
|
* `policy.reuseExistingBranch` selects between the two supported modes (see
|
|
127
127
|
* {@link EnsureExecutorWorktreePolicy}); it defaults to the fresh-off-base,
|
|
128
128
|
* F7-guarded behavior so callers that omit it are unchanged.
|
|
129
|
+
*
|
|
130
|
+
* BAPI-1019 — DO NOT provide operator environment files here. The interactive
|
|
131
|
+
* path (`createWorktrees` in `../start-tickets.ts`) calls `linkOperatorEnvFiles`
|
|
132
|
+
* after creating a worktree, because a human working in one needs `.env`. An
|
|
133
|
+
* EXECUTOR worktree must hold neither a copy nor a link: a link resolves to the
|
|
134
|
+
* operator's real `.env` exactly as a copy did, which is how Architecture Miss 28
|
|
135
|
+
* reached a live database. The executor's worktree is instead STRIPPED of
|
|
136
|
+
* `.env*` immediately before spawn (`env-file-guard.ts`), and the spawn is
|
|
137
|
+
* refused if that cannot be confirmed. Reusing the interactive linker at this
|
|
138
|
+
* seam — for symmetry, or to "fix" a worker that wants configuration — would
|
|
139
|
+
* reintroduce the miss.
|
|
129
140
|
*/
|
|
130
141
|
export async function ensureExecutorWorktree(job, options, deps, policy = {}) {
|
|
131
142
|
const branchResult = resolveExecutorBranch(job);
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Working-tree git trackedness oracle (BAPI-1027 / R10).
|
|
3
|
+
*
|
|
4
|
+
* The server-side plan linter owns the RULE — "a cited document that is not
|
|
5
|
+
* tracked is absent from the executor's worktree, so the citation resolves to
|
|
6
|
+
* nothing" — but it cannot own the FACT: the API server has no working tree.
|
|
7
|
+
* This helper supplies the fact and nothing else. It answers exactly one
|
|
8
|
+
* question, for exactly the paths it is handed:
|
|
9
|
+
*
|
|
10
|
+
* is this repository-relative path tracked in git?
|
|
11
|
+
*
|
|
12
|
+
* It deliberately contains NO lint logic. It does not extract citations from a
|
|
13
|
+
* plan, decide whether a path is documentation, assign a reason code, or
|
|
14
|
+
* reproduce any Python rule. Keeping the rule in one place and the git access in
|
|
15
|
+
* another is what lets the linter and A1b's R55 compiler share one rule set while
|
|
16
|
+
* each supplies its own oracle — the catalog's R-2 non-divergence constraint.
|
|
17
|
+
*
|
|
18
|
+
* Git runs through an injected argument-array seam modeled on
|
|
19
|
+
* `GitIgnoreRunCommand`/`GitIgnoreDeps` in `git-ignore-utils.ts`, so this is
|
|
20
|
+
* unit-testable without a real repository and can never be shell-interpolated.
|
|
21
|
+
*/
|
|
22
|
+
/** Max paths resolved in one invocation; mirrors the server's evidence bound. */
|
|
23
|
+
export const TRACKED_PATH_REQUEST_MAX_COUNT = 500;
|
|
24
|
+
/**
|
|
25
|
+
* Normalize one declared path exactly as the server's shared rules do.
|
|
26
|
+
*
|
|
27
|
+
* Mirrors `normalize_repo_relative_path` in `api/library/plan_lint_rules.py`:
|
|
28
|
+
* trim, `\` -> `/`, strip a single leading `./`. The two runtimes MUST agree on
|
|
29
|
+
* what makes two paths the same path — this helper answers questions about paths
|
|
30
|
+
* the server named, and a helper with its own notion of identity would answer
|
|
31
|
+
* about a different path than the one that was asked about.
|
|
32
|
+
*
|
|
33
|
+
* A genuinely unsafe path is REFUSED, never rewritten into a safe-looking one:
|
|
34
|
+
* silently normalizing `../../etc/passwd` would attribute an answer to a path the
|
|
35
|
+
* caller never asked about. A redundant `./` prefix is not that — it denotes the
|
|
36
|
+
* same path, and the server strips it too.
|
|
37
|
+
*/
|
|
38
|
+
export function normalizeRepoRelativePath(path) {
|
|
39
|
+
if (typeof path !== "string" || path.trim() === "") {
|
|
40
|
+
return { ok: false, reason: "must be a non-empty string" };
|
|
41
|
+
}
|
|
42
|
+
const candidate = path.trim();
|
|
43
|
+
if (candidate.includes("\0")) {
|
|
44
|
+
return { ok: false, reason: "must not contain a NUL character" };
|
|
45
|
+
}
|
|
46
|
+
let normalized = candidate.replace(/\\/g, "/");
|
|
47
|
+
if (normalized.startsWith("./"))
|
|
48
|
+
normalized = normalized.slice(2);
|
|
49
|
+
if (normalized === "") {
|
|
50
|
+
return { ok: false, reason: "must be a non-empty repository-relative path" };
|
|
51
|
+
}
|
|
52
|
+
if (normalized.startsWith("/")) {
|
|
53
|
+
return { ok: false, reason: "must be repository-relative, not absolute" };
|
|
54
|
+
}
|
|
55
|
+
if (normalized.length >= 2 && normalized[1] === ":") {
|
|
56
|
+
return { ok: false, reason: "must be repository-relative, not a drive-qualified path" };
|
|
57
|
+
}
|
|
58
|
+
if (normalized === ".." ||
|
|
59
|
+
normalized.startsWith("../") ||
|
|
60
|
+
normalized.includes("/../")) {
|
|
61
|
+
return { ok: false, reason: "must not contain a relative path segment" };
|
|
62
|
+
}
|
|
63
|
+
return { ok: true, path: normalized };
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Reject a path this helper must not hand to git.
|
|
67
|
+
*
|
|
68
|
+
* Returns a human-readable reason, or `null` when the path is acceptable. Thin
|
|
69
|
+
* wrapper over {@link normalizeRepoRelativePath} for callers that only need the
|
|
70
|
+
* verdict.
|
|
71
|
+
*/
|
|
72
|
+
export function rejectUnsafeRepoRelativePath(path) {
|
|
73
|
+
const result = normalizeRepoRelativePath(path);
|
|
74
|
+
return result.ok ? null : result.reason;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Resolve git trackedness for `paths` inside the working tree at `cwd`.
|
|
78
|
+
*
|
|
79
|
+
* `git ls-files` lists only the paths it considers TRACKED, so the answer is a
|
|
80
|
+
* set difference: a requested path git echoes back is tracked, and one it omits
|
|
81
|
+
* is not. `-z` makes the output NUL-delimited so a filename containing a newline
|
|
82
|
+
* cannot desynchronize parsing, `--full-name` makes every emitted path
|
|
83
|
+
* repository-root-relative regardless of `cwd`, and `--` terminates option
|
|
84
|
+
* parsing so a path beginning with `-` is never read as a flag.
|
|
85
|
+
*
|
|
86
|
+
* Every path is validated BEFORE the command runs; one bad path fails the whole
|
|
87
|
+
* call rather than being dropped, because a silently skipped path would come back
|
|
88
|
+
* as "no evidence" and read as an incomplete check rather than an authoring error.
|
|
89
|
+
*/
|
|
90
|
+
export async function resolveTrackedPaths(cwd, paths, deps) {
|
|
91
|
+
if (!Array.isArray(paths)) {
|
|
92
|
+
return { ok: false, error: "paths must be an array of repository-relative strings." };
|
|
93
|
+
}
|
|
94
|
+
// Deduplicate while preserving the caller's order, so one path asked for twice
|
|
95
|
+
// costs one argument and yields one fact.
|
|
96
|
+
const requested = [];
|
|
97
|
+
const seen = new Set();
|
|
98
|
+
for (const raw of paths) {
|
|
99
|
+
const normalization = normalizeRepoRelativePath(raw);
|
|
100
|
+
if (!normalization.ok) {
|
|
101
|
+
return {
|
|
102
|
+
ok: false,
|
|
103
|
+
error: `Cited document path ${JSON.stringify(raw)} ${normalization.reason}.`,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
if (seen.has(normalization.path))
|
|
107
|
+
continue;
|
|
108
|
+
seen.add(normalization.path);
|
|
109
|
+
requested.push(normalization.path);
|
|
110
|
+
}
|
|
111
|
+
if (requested.length === 0)
|
|
112
|
+
return { ok: true, facts: [] };
|
|
113
|
+
if (requested.length > TRACKED_PATH_REQUEST_MAX_COUNT) {
|
|
114
|
+
return {
|
|
115
|
+
ok: false,
|
|
116
|
+
error: `Refusing to resolve ${requested.length} cited document paths; the limit ` +
|
|
117
|
+
`is ${TRACKED_PATH_REQUEST_MAX_COUNT}.`,
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
let result;
|
|
121
|
+
try {
|
|
122
|
+
result = await deps.runCommand("git", ["ls-files", "-z", "--full-name", "--", ...requested], { cwd });
|
|
123
|
+
}
|
|
124
|
+
catch (err) {
|
|
125
|
+
return {
|
|
126
|
+
ok: false,
|
|
127
|
+
error: `git ls-files could not be run: ${err instanceof Error ? err.message : String(err)}`,
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
if (!result || typeof result.exitCode !== "number" || typeof result.stdout !== "string") {
|
|
131
|
+
return { ok: false, error: "git ls-files returned an unreadable result." };
|
|
132
|
+
}
|
|
133
|
+
if (result.exitCode !== 0) {
|
|
134
|
+
// Reported as an inability to check, with git's own stderr for diagnosis —
|
|
135
|
+
// never as "all of these are untracked".
|
|
136
|
+
const detail = (result.stderr || "").trim();
|
|
137
|
+
return {
|
|
138
|
+
ok: false,
|
|
139
|
+
error: `git ls-files exited ${result.exitCode}${detail ? `: ${detail}` : ""}`,
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
const trackedSet = new Set(result.stdout.split("\0").filter((entry) => entry !== ""));
|
|
143
|
+
return {
|
|
144
|
+
ok: true,
|
|
145
|
+
facts: requested.map((path) => ({ path, tracked: trackedSet.has(path) })),
|
|
146
|
+
};
|
|
147
|
+
}
|