@bridge_gpt/mcp-server 0.2.53 → 0.2.54
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 +86 -10
- package/build/agent-launchers/claude.js +3 -3
- package/build/agent-launchers/prompt.js +8 -11
- package/build/base-ref.js +33 -9
- package/build/bounded-wait.js +174 -0
- package/build/commands.generated.js +1 -1
- package/build/conductor/bridge-api-client.js +36 -8
- package/build/conductor/epic-runtime.js +133 -97
- package/build/conductor/readiness.js +85 -0
- package/build/conductor/run-branch.js +137 -0
- package/build/conductor/test-run-branch-vectors.js +165 -0
- package/build/conductor-bin.js +5 -5
- package/build/doctor.js +68 -1
- package/build/drive-epic.js +287 -51
- package/build/executor/claim-scope.js +104 -0
- package/build/executor/cli.js +14 -25
- package/build/executor/env-file-guard.js +82 -3
- package/build/executor/job-runner.js +60 -0
- package/build/index.js +128 -400
- package/build/local-artifact-storage.js +130 -0
- package/build/pipelines.generated.js +16 -9
- package/build/plane/cli.js +285 -36
- package/build/plane/manifest.js +209 -1
- package/build/plane/member-roster.js +70 -0
- package/build/plane/shutdown.js +14 -1
- package/build/plane/status.js +35 -1
- package/build/plane/supervisor.js +546 -164
- package/build/plane/types.js +25 -2
- package/build/polling-policy.js +72 -0
- package/build/readme.generated.js +1 -1
- package/build/review-generation.js +219 -0
- package/build/run-unit-tests-launcher.js +5 -0
- package/build/setup-epic.js +514 -23
- package/build/ticket-key-utils.js +4 -3
- package/build/ticket-review-artifact-gate.js +461 -0
- package/build/upgrade-cli.js +5 -26
- package/build/version.generated.js +3 -3
- package/docs/install/mcp-tool-integrations.md +23 -1
- package/package.json +1 -1
- package/pipelines/review-ticket.json +17 -4
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The executor CLAIM SCOPE: one vocabulary, one validator, two commands.
|
|
3
|
+
*
|
|
4
|
+
* BAPI-1026 (R54) made the claim scope mandatory and explicit: an executor
|
|
5
|
+
* started with neither `--epic-run-id` nor `--repo-wide` REFUSES TO START,
|
|
6
|
+
* because the server no longer treats an omitted scope as repository-wide and
|
|
7
|
+
* such an executor would poll forever and claim nothing. Refusing loudly at
|
|
8
|
+
* startup replaced that silent idle.
|
|
9
|
+
*
|
|
10
|
+
* BAPI-1102 gave `plane up` executor lanes of its own to spawn, which made the
|
|
11
|
+
* refusal reachable from a second command — and made a copied string literal the
|
|
12
|
+
* obvious hazard. A plane that re-worded the executor's refusal would tell an
|
|
13
|
+
* operator something subtly different about the same rule, and the two wordings
|
|
14
|
+
* would drift the first time either was edited. So the vocabulary and the rule
|
|
15
|
+
* move HERE, and both CLIs render what this module returns, verbatim.
|
|
16
|
+
*
|
|
17
|
+
* Deliberately free of process startup, filesystem access, network access, and
|
|
18
|
+
* CLI output. It is imported by `executor/cli.ts` and by `plane/`, and a shared
|
|
19
|
+
* module that reached for any of those would drag the whole executor runtime into
|
|
20
|
+
* the plane's import graph to validate two flags.
|
|
21
|
+
*/
|
|
22
|
+
/**
|
|
23
|
+
* The exact refusal for an invocation carrying BOTH forms of scope.
|
|
24
|
+
*
|
|
25
|
+
* Module-level constants, not inline literals, and exported so a test can assert
|
|
26
|
+
* that the plane renders the executor's own wording rather than a lookalike.
|
|
27
|
+
* Neither interpolates anything: these are fixed prose about flags.
|
|
28
|
+
*/
|
|
29
|
+
export const EXECUTOR_CLAIM_SCOPE_MUTUALLY_EXCLUSIVE_MESSAGE = "--repo-wide cannot be combined with --epic-run-id: pass one claim scope, not both";
|
|
30
|
+
/**
|
|
31
|
+
* The shape of a server-minted epic run id (BAPI-794).
|
|
32
|
+
*
|
|
33
|
+
* `epic_run_id` is always minted as `str(uuid.uuid4())` in
|
|
34
|
+
* `api/library/db/epic_runs.py`, so a standard UUID shape — any version — is a
|
|
35
|
+
* cheap, safe pre-validation: it rejects an obvious typo before any credential
|
|
36
|
+
* resolution or executor-loop startup, without over-constraining the CLI to a
|
|
37
|
+
* version the server does not itself enforce.
|
|
38
|
+
*
|
|
39
|
+
* It lives HERE, with the rest of the scope vocabulary, rather than in
|
|
40
|
+
* `executor/cli.ts` where it began. `plane up` accepted any non-blank id and the
|
|
41
|
+
* executor it spawns then refused the same value at startup — the operator saw a
|
|
42
|
+
* plane come up and a lane die, instead of one refusal at the point of entry
|
|
43
|
+
* (BAPI-1102 review).
|
|
44
|
+
*/
|
|
45
|
+
export const EXECUTOR_EPIC_RUN_ID_PATTERN = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/;
|
|
46
|
+
/** Is this a plausibly server-minted epic run id? */
|
|
47
|
+
export function isValidExecutorEpicRunId(value) {
|
|
48
|
+
return EXECUTOR_EPIC_RUN_ID_PATTERN.test(value.trim());
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* The refusal for an id that cannot be a server-minted run id. Interpolates the
|
|
52
|
+
* offending value, which is argv the operator just typed — never a credential.
|
|
53
|
+
*/
|
|
54
|
+
export function executorEpicRunIdRefusal(value) {
|
|
55
|
+
return `--epic-run-id has an invalid value: ${value}`;
|
|
56
|
+
}
|
|
57
|
+
/** The exact refusal for an invocation carrying NO scope at all. */
|
|
58
|
+
export const EXECUTOR_CLAIM_SCOPE_REQUIRED_MESSAGE = "a claim scope is required: pass --epic-run-id <id> (repeatable) to serve " +
|
|
59
|
+
"specific epic runs, or --repo-wide to deliberately claim repository-wide";
|
|
60
|
+
/**
|
|
61
|
+
* Validate a parsed scope selection.
|
|
62
|
+
*
|
|
63
|
+
* Order matters and is part of the contract: the mutual-exclusion refusal is
|
|
64
|
+
* reported BEFORE the missing-scope refusal, so an invocation that supplies both
|
|
65
|
+
* is told it supplied both rather than being told to supply one. One invocation
|
|
66
|
+
* therefore yields exactly one message, deterministically.
|
|
67
|
+
*
|
|
68
|
+
* Blank and whitespace-only ids are dropped before the emptiness test, so
|
|
69
|
+
* `--epic-run-id ""` is "no scope supplied" rather than a scope naming a run that
|
|
70
|
+
* cannot exist — the same reading the executor's own argv parse already takes.
|
|
71
|
+
*
|
|
72
|
+
* Each surviving id is then checked for the server-minted UUID shape, and that
|
|
73
|
+
* check runs LAST: an invocation with no scope at all is told to supply one
|
|
74
|
+
* rather than being told its zero ids are malformed.
|
|
75
|
+
*/
|
|
76
|
+
export function validateExecutorClaimScope(input) {
|
|
77
|
+
const epicRunIds = input.epicRunIds.map((id) => id.trim()).filter((id) => id.length > 0);
|
|
78
|
+
if (input.repoWide && epicRunIds.length > 0) {
|
|
79
|
+
return { ok: false, message: EXECUTOR_CLAIM_SCOPE_MUTUALLY_EXCLUSIVE_MESSAGE };
|
|
80
|
+
}
|
|
81
|
+
if (input.repoWide)
|
|
82
|
+
return { ok: true, scope: { kind: "repo-wide" } };
|
|
83
|
+
if (epicRunIds.length === 0) {
|
|
84
|
+
return { ok: false, message: EXECUTOR_CLAIM_SCOPE_REQUIRED_MESSAGE };
|
|
85
|
+
}
|
|
86
|
+
const malformed = epicRunIds.find((id) => !isValidExecutorEpicRunId(id));
|
|
87
|
+
if (malformed !== undefined) {
|
|
88
|
+
return { ok: false, message: executorEpicRunIdRefusal(malformed) };
|
|
89
|
+
}
|
|
90
|
+
return { ok: true, scope: { kind: "epic-runs", epicRunIds } };
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Render a validated scope as executor argv, in the executor's own flag spelling.
|
|
94
|
+
*
|
|
95
|
+
* The single place that knows how a scope becomes arguments, so the plane's lane
|
|
96
|
+
* roster cannot spell the flags differently from the executor that parses them.
|
|
97
|
+
* One `--epic-run-id <id>` PAIR per id, in the supplied order; `--repo-wide`
|
|
98
|
+
* exactly once and valueless.
|
|
99
|
+
*/
|
|
100
|
+
export function executorClaimScopeArgs(scope) {
|
|
101
|
+
if (scope.kind === "repo-wide")
|
|
102
|
+
return ["--repo-wide"];
|
|
103
|
+
return scope.epicRunIds.flatMap((id) => ["--epic-run-id", id]);
|
|
104
|
+
}
|
package/build/executor/cli.js
CHANGED
|
@@ -11,6 +11,7 @@ import { VERSION } from "../version.generated.js";
|
|
|
11
11
|
import { resolveWorktrunkBinary } from "../start-tickets-prereqs.js";
|
|
12
12
|
import { resolveStartTicketsRepoName } from "../start-tickets-repo.js";
|
|
13
13
|
import { DEFAULT_EXECUTOR_AGENT_ID } from "./agent-identity.js";
|
|
14
|
+
import { executorEpicRunIdRefusal, isValidExecutorEpicRunId, validateExecutorClaimScope, } from "./claim-scope.js";
|
|
14
15
|
import { createDefaultExecutorDeps } from "./deps.js";
|
|
15
16
|
import { resolveBaseUrl, resolveExecutorApiAccess, EXECUTOR_BASE_URL_REQUIRED_MESSAGE, } from "./credentials.js";
|
|
16
17
|
import { createExecutorHttpClient } from "./http-client.js";
|
|
@@ -72,17 +73,6 @@ function parseIntArg(value, flag) {
|
|
|
72
73
|
}
|
|
73
74
|
return { ok: true, value: n };
|
|
74
75
|
}
|
|
75
|
-
/**
|
|
76
|
-
* BAPI-794 — `epic_run_id` is always server-minted as `str(uuid.uuid4())`
|
|
77
|
-
* (`api/library/db/epic_runs.py`), so a standard UUID shape (any version) is a
|
|
78
|
-
* cheap, safe pre-validation: it rejects an obvious typo before any credential
|
|
79
|
-
* resolution or executor-loop startup, without over-constraining the CLI to a
|
|
80
|
-
* version the server does not itself enforce.
|
|
81
|
-
*/
|
|
82
|
-
const EPIC_RUN_ID_PATTERN = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/;
|
|
83
|
-
function isValidEpicRunId(value) {
|
|
84
|
-
return EPIC_RUN_ID_PATTERN.test(value);
|
|
85
|
-
}
|
|
86
76
|
/** Parse the executor CLI arguments into resolved options (pure, synchronous). */
|
|
87
77
|
export function parseExecutorArgs(argv, context) {
|
|
88
78
|
const repos = [];
|
|
@@ -121,8 +111,11 @@ export function parseExecutorArgs(argv, context) {
|
|
|
121
111
|
if (!v || !v.trim())
|
|
122
112
|
return { kind: "error", message: "--epic-run-id requires a value" };
|
|
123
113
|
const trimmed = v.trim();
|
|
124
|
-
|
|
125
|
-
|
|
114
|
+
// BAPI-1102 — the shape rule moved to the shared claim-scope module, so
|
|
115
|
+
// `plane up` refuses the same value at ITS entry point instead of coming
|
|
116
|
+
// up and letting the lane it spawned die on the same argument.
|
|
117
|
+
if (!isValidExecutorEpicRunId(trimmed)) {
|
|
118
|
+
return { kind: "error", message: executorEpicRunIdRefusal(trimmed) };
|
|
126
119
|
}
|
|
127
120
|
if (!epicRunIds.includes(trimmed))
|
|
128
121
|
epicRunIds.push(trimmed);
|
|
@@ -200,18 +193,14 @@ export function parseExecutorArgs(argv, context) {
|
|
|
200
193
|
// forever and claim nothing. Refusing to START is the loud failure that
|
|
201
194
|
// replaces that silent idle: a misconfiguration is a startup error, visible
|
|
202
195
|
// immediately, rather than an executor that looks healthy and does no work.
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
}
|
|
209
|
-
if (!
|
|
210
|
-
return {
|
|
211
|
-
kind: "error",
|
|
212
|
-
message: "a claim scope is required: pass --epic-run-id <id> (repeatable) to serve " +
|
|
213
|
-
"specific epic runs, or --repo-wide to deliberately claim repository-wide",
|
|
214
|
-
};
|
|
196
|
+
//
|
|
197
|
+
// BAPI-1102 moved the rule and both messages into `./claim-scope.js` so
|
|
198
|
+
// `plane up` — which now spawns executor lanes of its own — enforces the SAME
|
|
199
|
+
// rule and renders the SAME wording, rather than carrying a second copy that
|
|
200
|
+
// drifts. The returned message is rendered VERBATIM; nothing is re-worded here.
|
|
201
|
+
const scope = validateExecutorClaimScope({ epicRunIds, repoWide });
|
|
202
|
+
if (!scope.ok) {
|
|
203
|
+
return { kind: "error", message: scope.message };
|
|
215
204
|
}
|
|
216
205
|
const options = {
|
|
217
206
|
executorId: executorIdFinal,
|
|
@@ -31,6 +31,27 @@
|
|
|
31
31
|
* all produce `ok: false`; only a second listing that shows no matching entry
|
|
32
32
|
* produces `ok: true`.
|
|
33
33
|
*
|
|
34
|
+
* TRACKED TEMPLATES SURVIVE (BAPI-1104, R81). The `.env.` prefix rule below is
|
|
35
|
+
* deliberately wide, and it caught this repository's own git-TRACKED
|
|
36
|
+
* `.env.example` — deleting it from every worker worktree, which showed up as a
|
|
37
|
+
* spurious dirty tree in the BAPI-1061 run. A tracked file is not an operator's
|
|
38
|
+
* environment file: it is repository content the worker is entitled to see, and
|
|
39
|
+
* its contents are already public in the checkout. So a candidate that git
|
|
40
|
+
* reports as tracked is kept, reported as `kept_tracked`, and excluded from the
|
|
41
|
+
* confirming re-list below.
|
|
42
|
+
*
|
|
43
|
+
* The exemption does NOT widen this module's reach. The lookup is a narrow seam
|
|
44
|
+
* that takes basenames and returns basenames — it never resolves, reads, or stats
|
|
45
|
+
* a path, so the symlink guarantee above is untouched. And it is best-effort by
|
|
46
|
+
* design: when the lookup fails, NOTHING is exempt and every match is stripped
|
|
47
|
+
* exactly as before. That is strictly the safer direction. The guarantee this
|
|
48
|
+
* module makes is about what reaches the worker, and it is proven by the
|
|
49
|
+
* post-removal re-list, not by knowing which files were tracked — so a failed
|
|
50
|
+
* lookup leaves the proof exactly as strong as it was and costs only the
|
|
51
|
+
* convenience the exemption was added for. Refusing the spawn instead would
|
|
52
|
+
* convert a transient git hiccup into a dead job, adding a failure mode the guard
|
|
53
|
+
* never had.
|
|
54
|
+
*
|
|
34
55
|
* REDACTION. A failure returns a fixed CATEGORY and nothing else — no filename,
|
|
35
56
|
* no path, no resolved target, no exception text. This value reaches the job row
|
|
36
57
|
* and the executor's stderr, and a worktree path contains an operator's username.
|
|
@@ -53,6 +74,29 @@ import { pathApiForExecutorPlatform } from "./worktree-inspection.js";
|
|
|
53
74
|
export function isWorkerEnvFileName(name) {
|
|
54
75
|
return name === ".env" || name.startsWith(".env.");
|
|
55
76
|
}
|
|
77
|
+
/**
|
|
78
|
+
* True for a name safe to hand to a subprocess as a literal pathspec (BAPI-1104).
|
|
79
|
+
*
|
|
80
|
+
* `readdir` cannot produce a name containing a separator or a NUL, so in
|
|
81
|
+
* production this is already true of every candidate. It is checked anyway
|
|
82
|
+
* because the value crosses a process boundary: the property that matters —
|
|
83
|
+
* "only exact `.env`/`.env.*` root basenames ever reach git" — should be
|
|
84
|
+
* enforced at the boundary rather than inferred from what `readdir` happens to
|
|
85
|
+
* guarantee. A name that fails this is simply not exempted; it is still stripped.
|
|
86
|
+
*
|
|
87
|
+
* Shell metacharacters need no handling here and none is attempted:
|
|
88
|
+
* `deps.runCommand` is `execFile` with `shell: false`, so a name like
|
|
89
|
+
* `.env; rm -rf /` is delivered as one inert argv element and no shell ever
|
|
90
|
+
* parses it. The real (narrow) hazard is git reading a leading-dash argument as
|
|
91
|
+
* an OPTION, which the `--` pathspec separator at the call site closes — and
|
|
92
|
+
* which the `.env` prefix rule above already makes unreachable.
|
|
93
|
+
*/
|
|
94
|
+
function isSafePathspecBasename(name) {
|
|
95
|
+
return (isWorkerEnvFileName(name) &&
|
|
96
|
+
!name.includes("/") &&
|
|
97
|
+
!name.includes("\\") &&
|
|
98
|
+
!name.includes("\0"));
|
|
99
|
+
}
|
|
56
100
|
/**
|
|
57
101
|
* Strip every `.env` / `.env.*` entry from a worktree ROOT and prove it worked.
|
|
58
102
|
*
|
|
@@ -74,7 +118,37 @@ export async function stripWorkerEnvFiles(worktreePath, deps) {
|
|
|
74
118
|
// Sorted so a successful `removed` list is deterministic and two job rows for
|
|
75
119
|
// the same condition render identically.
|
|
76
120
|
const matches = entries.filter(isWorkerEnvFileName).sort();
|
|
77
|
-
|
|
121
|
+
// BAPI-1104 — which matches are git-TRACKED repository content rather than an
|
|
122
|
+
// operator's environment file. Asked ONCE for the whole candidate set: one
|
|
123
|
+
// bounded subprocess at the last step before every worker spawn is a cost worth
|
|
124
|
+
// paying; one per `.env*` entry is not.
|
|
125
|
+
//
|
|
126
|
+
// Every value that comes back is re-intersected against `matches`. The seam is
|
|
127
|
+
// narrow but it is still a seam, and a lookup that returned a path, a nested
|
|
128
|
+
// name, or a name that was never a candidate must not be able to exempt
|
|
129
|
+
// anything — so the guard trusts the set it computed itself, not the answer.
|
|
130
|
+
let keptTracked = [];
|
|
131
|
+
const lookupCandidates = matches.filter(isSafePathspecBasename);
|
|
132
|
+
if (deps.listTrackedNames && lookupCandidates.length > 0) {
|
|
133
|
+
try {
|
|
134
|
+
const reported = await deps.listTrackedNames(worktreePath, lookupCandidates);
|
|
135
|
+
if (Array.isArray(reported)) {
|
|
136
|
+
const candidateSet = new Set(lookupCandidates);
|
|
137
|
+
keptTracked = Array.from(new Set(reported.filter((name) => typeof name === "string" && candidateSet.has(name)))).sort();
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
catch {
|
|
141
|
+
// Exempt NOTHING and continue. The guarantee is proven by the re-list
|
|
142
|
+
// below, which a failed lookup does not weaken — so stripping the tracked
|
|
143
|
+
// file too is strictly safer than refusing a spawn the guard can still
|
|
144
|
+
// confirm clean. No category, no diagnostic: the caller that owns the
|
|
145
|
+
// subprocess owns the one bounded log line about it.
|
|
146
|
+
keptTracked = [];
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
const keptSet = new Set(keptTracked);
|
|
150
|
+
const removed = matches.filter((name) => !keptSet.has(name));
|
|
151
|
+
for (const name of removed) {
|
|
78
152
|
const target = pathApi.join(worktreePath, name);
|
|
79
153
|
try {
|
|
80
154
|
// Non-following metadata. The result is deliberately unused for a decision:
|
|
@@ -106,8 +180,13 @@ export async function stripWorkerEnvFiles(worktreePath, deps) {
|
|
|
106
180
|
catch {
|
|
107
181
|
return { ok: false, category: "verification_listing_failed" };
|
|
108
182
|
}
|
|
109
|
-
|
|
183
|
+
// A KEPT tracked name is an expected survivor, not a failure. Without this the
|
|
184
|
+
// exemption would be self-defeating: the re-list would see the `.env.example`
|
|
185
|
+
// it deliberately preserved and refuse the spawn with `entry_survived`.
|
|
186
|
+
if (remaining.some((name) => isWorkerEnvFileName(name) && !keptSet.has(name))) {
|
|
110
187
|
return { ok: false, category: "entry_survived" };
|
|
111
188
|
}
|
|
112
|
-
return
|
|
189
|
+
return keptTracked.length > 0
|
|
190
|
+
? { ok: true, removed, kept_tracked: keptTracked }
|
|
191
|
+
: { ok: true, removed };
|
|
113
192
|
}
|
|
@@ -48,6 +48,23 @@ import { buildPrBaseContractLaunchInstruction } from "../pr-base-contract.js";
|
|
|
48
48
|
import { resolveExecutorPrompt, resolveWorkerPermissionPosture, } from "./worker-command.js";
|
|
49
49
|
import { collectGitTelemetry, collectRemoteTrackingSha, createMcpSurfaceObserver, evaluatePreSpawnGitVerification, formatWorkerRateLimitAdvisory, normalizeMcpServerNames, } from "./observation.js";
|
|
50
50
|
import { isRecognizedServerName, MCP_SERVER_NAME } from "../mcp-identity.js";
|
|
51
|
+
/**
|
|
52
|
+
* Bound on the pre-spawn git tracked-file lookup (BAPI-1104, R81).
|
|
53
|
+
*
|
|
54
|
+
* This runs at the LAST step before every worker spawn, so a hung `git ls-files`
|
|
55
|
+
* would stall spawning rather than merely delay one exemption. Five seconds is
|
|
56
|
+
* far beyond what an index read of a handful of pathspecs takes, and a timeout
|
|
57
|
+
* degrades to "exempt nothing" rather than to a refusal.
|
|
58
|
+
*/
|
|
59
|
+
const ENV_FILE_TRACKED_LOOKUP_TIMEOUT_MS = 5_000;
|
|
60
|
+
/**
|
|
61
|
+
* The ONE fixed line emitted when that lookup fails. No path (a worktree path
|
|
62
|
+
* carries an operator's username), no filename, no errno, no command output, no
|
|
63
|
+
* exception text — the same bar `WORKER_ENV_FILE_PRESENT_MESSAGE` and the guard's
|
|
64
|
+
* closed categories hold. It is advisory: the spawn proceeds, with nothing
|
|
65
|
+
* exempted.
|
|
66
|
+
*/
|
|
67
|
+
const ENV_FILE_TRACKED_LOOKUP_FAILED_MESSAGE = "[executor] tracked env-file lookup unavailable; exempting nothing";
|
|
51
68
|
/** Default runtime for the no-op smoke process (ms). */
|
|
52
69
|
const DEFAULT_SMOKE_DURATION_MS = 100;
|
|
53
70
|
/** ProcessClassification maps 1:1 onto the wire ExecutorClassification. */
|
|
@@ -1599,6 +1616,49 @@ async function runPreparedSpawn(params) {
|
|
|
1599
1616
|
// enforcement: no `stat`, `readlink`, or `readFile` is reachable from here.
|
|
1600
1617
|
lstat: (target) => lstat(target),
|
|
1601
1618
|
unlink: (target) => unlink(target),
|
|
1619
|
+
// BAPI-1104 (R81): which candidates git reports as TRACKED. This repository's
|
|
1620
|
+
// own `.env.example` is tracked content, not an operator's environment file,
|
|
1621
|
+
// and the `.env.` prefix rule was deleting it from every worker worktree.
|
|
1622
|
+
//
|
|
1623
|
+
// `deps.runCommand` is `execFile` with `shell: false`, so the basenames are
|
|
1624
|
+
// delivered as inert argv elements and no shell parses them. `--` is
|
|
1625
|
+
// mandatory: it stops git reading any argument as an option (unreachable for
|
|
1626
|
+
// `.env*` names, but the separator is what makes that a property rather than
|
|
1627
|
+
// an argument). `--error-unmatch` is deliberately NOT used — it exits 1 and
|
|
1628
|
+
// prints nothing for the WHOLE invocation as soon as one pathspec is
|
|
1629
|
+
// untracked, so on the mixed tracked/untracked worktree this feature exists
|
|
1630
|
+
// for it would silently report "nothing is tracked" and delete the
|
|
1631
|
+
// `.env.example` anyway. Plain `ls-files` lists only tracked paths and exits
|
|
1632
|
+
// zero, so the tracked subset falls out of stdout directly.
|
|
1633
|
+
listTrackedNames: async (root, candidates) => {
|
|
1634
|
+
let result;
|
|
1635
|
+
try {
|
|
1636
|
+
result = await deps.runCommand("git", ["ls-files", "-z", "--", ...candidates], { cwd: root, timeoutMs: ENV_FILE_TRACKED_LOOKUP_TIMEOUT_MS });
|
|
1637
|
+
}
|
|
1638
|
+
catch {
|
|
1639
|
+
// `runCommand` surfaces a non-zero exit as `exitCode` rather than
|
|
1640
|
+
// throwing, so reaching here means the process could not be run at all.
|
|
1641
|
+
deps.errorLog(ENV_FILE_TRACKED_LOOKUP_FAILED_MESSAGE);
|
|
1642
|
+
return [];
|
|
1643
|
+
}
|
|
1644
|
+
if (result.exitCode !== 0 || typeof result.stdout !== "string") {
|
|
1645
|
+
// Fail-OPEN into "nothing is exempt": the guard then strips every match,
|
|
1646
|
+
// which is strictly safer than refusing a spawn it can still confirm
|
|
1647
|
+
// clean. One fixed line, and nothing from the command in it — stdout
|
|
1648
|
+
// would carry worktree-relative paths and stderr an errno.
|
|
1649
|
+
deps.errorLog(ENV_FILE_TRACKED_LOOKUP_FAILED_MESSAGE);
|
|
1650
|
+
return [];
|
|
1651
|
+
}
|
|
1652
|
+
// NUL-delimited, never logged. Filtered to the exact candidate set here as
|
|
1653
|
+
// well as in the guard: the seam's contract is basenames-in / basenames-out
|
|
1654
|
+
// restricted to what was asked about, and a producer that honors its own
|
|
1655
|
+
// contract is easier to reason about than one that relies on its consumer
|
|
1656
|
+
// to clean up after it. The guard's re-intersection stays the enforcement.
|
|
1657
|
+
const asked = new Set(candidates);
|
|
1658
|
+
return result.stdout
|
|
1659
|
+
.split("\0")
|
|
1660
|
+
.filter((name) => name.length > 0 && asked.has(name));
|
|
1661
|
+
},
|
|
1602
1662
|
platform: deps.platform,
|
|
1603
1663
|
});
|
|
1604
1664
|
if (!stripped.ok) {
|