@bridge_gpt/mcp-server 0.2.69 → 0.2.70
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 +140 -292
- package/build/agent-launchers/claude-executor-adapter.js +17 -4
- package/build/agents.generated.js +4 -4
- package/build/artifact-diff.js +110 -0
- package/build/bin-entrypoint.js +36 -0
- package/build/bridge-permissions.js +23 -4
- package/build/claude-review-workflow-drift.js +3 -3
- package/build/codex-bridge-profile.js +1 -1
- package/build/commands.generated.js +6 -6
- package/build/conductor/bridge-api-client.js +598 -19
- package/build/conductor/bring-up-facts.js +9 -9
- package/build/conductor/doctor.js +466 -18
- package/build/conductor/epic-child-continuation.js +54 -3
- package/build/conductor/epic-implementer-cli.js +132 -34
- package/build/conductor/epic-runtime.js +2 -2
- package/build/conductor/epic-state.js +1 -1
- package/build/conductor/hook-runtime-qualification.js +1 -1
- package/build/conductor/install-doctor.js +4 -2
- package/build/conductor/launch-need-probes.js +337 -0
- package/build/conductor/local-merge.js +241 -56
- package/build/conductor/merge-credential.js +172 -0
- package/build/conductor/merge-execution-eligibility.js +1 -1
- package/build/conductor/merged-child-check.js +95 -0
- package/build/conductor/plan-trim.js +30 -0
- package/build/conductor/pr-ci-producer.js +1 -1
- package/build/conductor/readiness-cli.js +11 -6
- package/build/conductor/readiness-prerequisites.js +41 -0
- package/build/conductor/readiness-sections.js +345 -55
- package/build/conductor/readiness.js +99 -87
- package/build/conductor/recovery-cli.js +606 -21
- package/build/conductor/recovery-operations.js +306 -7
- package/build/conductor/repair-contract.js +47 -2
- package/build/conductor/repository-workflow-remedy.js +8 -6
- package/build/conductor/stored-merge-credential.js +76 -0
- package/build/conductor/stored-merge-token-probe.js +28 -0
- package/build/conductor/temporary-plan.js +50 -0
- package/build/conductor/wait-run-cli.js +759 -0
- package/build/conductor/worker-database-doctor-inspector.js +49 -0
- package/build/conductor-bin.js +2 -2
- package/build/conductor-claude-hook-bin.js +2 -2
- package/build/conductor-claude-hook-removed-stub-bin.js +2 -5
- package/build/conductor-removed-stub-bin.js +2 -5
- package/build/credential-store.js +356 -13
- package/build/docs.generated.js +26 -13
- package/build/doctor.js +94 -32
- package/build/drive-epic-help.js +212 -0
- package/build/drive-epic.js +1840 -373
- package/build/epic-ci-workflow-drift.js +77 -0
- package/build/epic-ci-workflow-install.js +183 -0
- package/build/epic-ci-workflow.js +157 -0
- package/build/epic-contract-context.js +326 -0
- package/build/epic-implementer-bin.js +57 -41
- package/build/epic-implementer-claude-hook-bin.js +2 -2
- package/build/escalation-reachability.js +134 -0
- package/build/executor/cleanup-report.js +99 -0
- package/build/executor/cli.js +12 -1
- package/build/executor/completion-contract.js +66 -11
- package/build/executor/completion-supervision.js +99 -0
- package/build/executor/delivery-outcome.js +179 -0
- package/build/executor/deps.js +3 -0
- package/build/executor/env.js +71 -0
- package/build/executor/failure-reason-codes.js +126 -0
- package/build/executor/heartbeat.js +38 -0
- package/build/executor/http-client.js +72 -0
- package/build/executor/integration-job.js +298 -5
- package/build/executor/job-errors.js +36 -6
- package/build/executor/job-runner.js +720 -36
- package/build/executor/likely-tests.js +36 -2
- package/build/executor/merge-job.js +145 -2
- package/build/executor/observation.js +73 -4
- package/build/executor/permissions.js +193 -97
- package/build/executor/preflight.js +14 -3
- package/build/executor/process-containment.js +28 -2
- package/build/executor/prompt-spec.js +50 -5
- package/build/executor/rebase-finalization.js +38 -3
- package/build/executor/reconcile-overlap-job.js +35 -7
- package/build/executor/required-work-ledger.js +236 -81
- package/build/executor/resume-pre-spawn.js +38 -1
- package/build/executor/runner.js +91 -3
- package/build/executor/test-clock.js +8 -0
- package/build/executor/types.js +37 -2
- package/build/executor/worker-cli-launcher.js +259 -0
- package/build/executor/worker-command.js +41 -4
- package/build/executor/worker-database-target.js +91 -0
- package/build/executor/worker-database.js +908 -0
- package/build/executor/worker-finalization.js +106 -0
- package/build/executor/worker-guard-hook.js +203 -0
- package/build/executor/worker-resources.js +47 -0
- package/build/executor/worker-verification-guidance.js +15 -2
- package/build/executor/worktree.js +303 -15
- package/build/executor-launch.js +11 -1
- package/build/generated-assets-doctor.js +9 -1
- package/build/generated-assets-health.js +13 -4
- package/build/generated-assets-legacy.js +39 -0
- package/build/generated-assets-paths.js +12 -1
- package/build/generated-assets-repair.js +4 -1
- package/build/generated-assets-test-support.js +12 -0
- package/build/github-remote.js +33 -0
- package/build/github-sign-in.js +62 -0
- package/build/index.js +1067 -536
- package/build/init.js +39 -1
- package/build/install-bridge-conductor-gate.js +78 -0
- package/build/install-bridge-conductor-setup.js +413 -0
- package/build/install-bridge-conductor.js +1226 -267
- package/build/install-bridge.js +376 -60
- package/build/mcp-install-state.js +8 -2
- package/build/merge-pull-request.js +6 -0
- package/build/merge-workflow-install.js +763 -345
- package/build/phase-result-artifacts.js +75 -20
- package/build/pipeline-orchestrator.js +247 -4
- package/build/pipelines.generated.js +35 -35
- package/build/plan-discovery.js +15 -2
- package/build/plan-execution-ledger.js +306 -17
- package/build/plan-phase-routing.js +48 -13
- package/build/plane/cli.js +105 -51
- package/build/plane/defaults.js +3 -0
- package/build/plane/plane-freshness.js +294 -0
- package/build/plane/schema-compatibility.js +137 -0
- package/build/plane/shutdown-deps.js +38 -0
- package/build/plane/shutdown.js +83 -8
- package/build/plane/terminal-run-cleanup.js +303 -0
- package/build/printed-remedy-commands.js +26 -4
- package/build/readiness-check.js +6 -2
- package/build/readiness-prerequisites.generated.js +532 -0
- package/build/readme.generated.js +1 -1
- package/build/setup-epic.js +400 -30
- package/build/shared-resources.generated.js +5 -0
- package/build/start-tickets.js +28 -15
- package/build/test-command-detection.js +247 -0
- package/build/test-command-validation.js +73 -0
- package/build/testing-config-client.js +150 -0
- package/build/ticket-delivery-artifacts.js +258 -0
- package/build/ticket-delivery-enrollment.js +306 -0
- package/build/ticket-delivery.js +205 -0
- package/build/ticket-review-summary.js +14 -0
- package/build/upgrade-cli.js +13 -0
- package/build/v2-readiness-requirements.js +552 -0
- package/build/version.generated.js +3 -3
- package/build/worker-guard-hook-bin.js +2 -2
- package/build/worktree-core.js +22 -7
- package/docs/BRIDGE-DIRECTORY-README.md +48 -0
- package/docs/EPIC-IMPLEMENTER-INSTALL.md +12 -867
- package/docs/EPIC-IMPLEMENTER.md +104 -63
- package/docs/epic-implementer/getting-started.md +131 -0
- package/docs/epic-implementer/self-hosting.md +133 -0
- package/package.json +3 -3
- package/pipelines/idea-to-ticket.json +1 -1
- package/pipelines/implement-ticket.json +8 -8
- package/pipelines/review-ticket.json +2 -2
- package/design-assets/favicon/favicon-32x32.png +0 -0
- package/design-assets/just-logo-rough-draft.png +0 -0
|
@@ -5,14 +5,22 @@
|
|
|
5
5
|
* Optional .conductor/recently-failed-tests.json is
|
|
6
6
|
* {version:1,tests:["most-recent-first reference"],truncated:false}.
|
|
7
7
|
* Child 3 writes recent failures under the worker worktree before each spawn.
|
|
8
|
-
*
|
|
9
|
-
*
|
|
8
|
+
* Optional declared touched files (raw JSON, supplied by the caller) admit the
|
|
9
|
+
* declared files that are tests and exist in the worktree.
|
|
10
|
+
* References are data only, never commands. Priority: recent failures, sorted
|
|
11
|
+
* declared tests, ordered pins, sorted inferred tracked tests; exact
|
|
12
|
+
* deduplication precedes the limit.
|
|
10
13
|
* Missing/malformed/unreadable/truncated sources degrade advisory coverage.
|
|
14
|
+
* Changed files come from the committed `<base>..HEAD` diff only; workers commit
|
|
15
|
+
* before discovery. BAPI-1479 confirmed this was not why workers saw no output
|
|
16
|
+
* (see `runLikelyTestsCommand` for the confirmed cause), so uncommitted changes
|
|
17
|
+
* stay out of scope.
|
|
11
18
|
*/
|
|
12
19
|
import path from "node:path";
|
|
13
20
|
import { constants } from "node:fs";
|
|
14
21
|
import { lstat, open } from "node:fs/promises";
|
|
15
22
|
import { execFile } from "node:child_process";
|
|
23
|
+
import { normalizeDeclaredTouchedFiles } from "../conductor/file-scope-guard.js";
|
|
16
24
|
export const RECENTLY_FAILED_TESTS_PATH = ".conductor/recently-failed-tests.json";
|
|
17
25
|
export const RECENTLY_FAILED_TESTS_MAX_COUNT = 20;
|
|
18
26
|
export const TEST_REFERENCE_MAX_LENGTH = 512;
|
|
@@ -213,6 +221,32 @@ export async function resolveLikelyTests(options) {
|
|
|
213
221
|
if (error.code !== "ENOENT")
|
|
214
222
|
diagnostics.add("data_unavailable");
|
|
215
223
|
}
|
|
224
|
+
// Declared test files sit right after recent failures and BEFORE any git diff
|
|
225
|
+
// or scan, so a later early return cannot bypass them.
|
|
226
|
+
if (options.declaredTouchedFilesJson !== undefined) {
|
|
227
|
+
let declared;
|
|
228
|
+
try {
|
|
229
|
+
const parsed = JSON.parse(options.declaredTouchedFilesJson);
|
|
230
|
+
if (Array.isArray(parsed))
|
|
231
|
+
declared = normalizeDeclaredTouchedFiles(parsed);
|
|
232
|
+
}
|
|
233
|
+
catch { /* malformed: reported below */ }
|
|
234
|
+
if (declared === undefined)
|
|
235
|
+
diagnostics.add("data_unavailable");
|
|
236
|
+
else {
|
|
237
|
+
const admitted = [];
|
|
238
|
+
for (const file of declared.filter(testCandidate)) {
|
|
239
|
+
try {
|
|
240
|
+
await read(root, file, MAX_SCAN_FILE_BYTES);
|
|
241
|
+
admitted.push(file);
|
|
242
|
+
}
|
|
243
|
+
catch {
|
|
244
|
+
diagnostics.add("skipped_entries");
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
add(admitted);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
216
250
|
let hints;
|
|
217
251
|
try {
|
|
218
252
|
hints = parseHints(JSON.parse(await read(root, ".bridge/verification-hints.json", MAX_OPTIONAL_JSON_BYTES)));
|
|
@@ -21,11 +21,23 @@
|
|
|
21
21
|
* {@link MERGE_FAILED} `error_kind` (via {@link classifyMergeFailureErrorKind})
|
|
22
22
|
* so the companion re-fire lane can route retry-vs-rebase without string parsing.
|
|
23
23
|
*/
|
|
24
|
-
import { makeLocalMergeExecutor, resolveLocalMergeCredential, resolveLocalMergeMethod, } from "../conductor/local-merge.js";
|
|
25
|
-
import { resolveConductorBridgeApiAccess, } from "../conductor/bridge-api-client.js";
|
|
24
|
+
import { classifyLocalGhPreflight, makeLocalMergeExecutor, probeLocalMergeRepositoryAccess, resolveLocalMergeCredential, resolveLocalMergeMethod, } from "../conductor/local-merge.js";
|
|
25
|
+
import { isProviderRepositoryIdentity, resolveConductorBridgeApiAccess, } from "../conductor/bridge-api-client.js";
|
|
26
26
|
import { createProductionEpicIntegrationGhRunner, ensureEpicIntegrationPullRequest, formatEpicIntegrationPullRequestOutcome, EPIC_BRANCH_PREFIX, } from "../epic-integration-pr.js";
|
|
27
|
+
import { resolveMergeCredentialAtAction } from "../conductor/stored-merge-credential.js";
|
|
27
28
|
import { secretFreeErrorMessage } from "./job-errors.js";
|
|
28
29
|
import { resolveExecutorJobBaseBranch } from "./base-branch.js";
|
|
30
|
+
import { GH_REPO_ACCESS_DENIED, failureReasonCode, isFailureReasonCode, } from "./failure-reason-codes.js";
|
|
31
|
+
/** Read the optional `payload.repository` against the provider-repository format. */
|
|
32
|
+
export function resolveMergeJobRepository(job) {
|
|
33
|
+
const payload = job.payload && typeof job.payload === "object" ? job.payload : {};
|
|
34
|
+
const value = payload.repository;
|
|
35
|
+
if (value === undefined || value === null)
|
|
36
|
+
return { kind: "absent" };
|
|
37
|
+
if (isProviderRepositoryIdentity(value))
|
|
38
|
+
return { kind: "valid", repository: value };
|
|
39
|
+
return { kind: "malformed" };
|
|
40
|
+
}
|
|
29
41
|
function positiveIntOrNull(value) {
|
|
30
42
|
return typeof value === "number" && Number.isInteger(value) && value > 0 ? value : null;
|
|
31
43
|
}
|
|
@@ -80,6 +92,7 @@ export function readMergeJobPayloadFields(job, repositoryBaseBranch) {
|
|
|
80
92
|
const actionKey = asString(payload.action_key) ?? `merge:pr-${prNumber}:${expectedHeadSha}`;
|
|
81
93
|
const ciWaitTimeoutMs = nonNegativeIntOrUndefined(payload.ci_wait_timeout_ms);
|
|
82
94
|
const ciWaitPollIntervalMs = nonNegativeIntOrUndefined(payload.ci_wait_poll_interval_ms);
|
|
95
|
+
const repository = resolveMergeJobRepository(job);
|
|
83
96
|
return {
|
|
84
97
|
ok: true,
|
|
85
98
|
fields: {
|
|
@@ -92,6 +105,7 @@ export function readMergeJobPayloadFields(job, repositoryBaseBranch) {
|
|
|
92
105
|
ciWaitPollIntervalMs,
|
|
93
106
|
expectedBaseBranch,
|
|
94
107
|
expectedHeadBranch,
|
|
108
|
+
...(repository.kind === "valid" ? { repository: repository.repository } : {}),
|
|
95
109
|
},
|
|
96
110
|
};
|
|
97
111
|
}
|
|
@@ -166,6 +180,16 @@ export const MERGE_RETRYABLE = "MergeRetryable";
|
|
|
166
180
|
export const MERGE_CONFLICT = "MergeConflict";
|
|
167
181
|
export const MERGE_FAILED = "MergeFailed";
|
|
168
182
|
export const MERGE_REF_MISMATCH = "MergeRefMismatch";
|
|
183
|
+
/**
|
|
184
|
+
* BAPI-1453 — a DELIBERATE, terminal refusal: after `gh pr view` failed, the
|
|
185
|
+
* permission probe PROVED the merge credential cannot access the repository (a
|
|
186
|
+
* 404, or a 200 without push). Emitted only with the `gh_repo_access_denied`
|
|
187
|
+
* reason code. Retrying with the same credential reproduces the identical
|
|
188
|
+
* failure, so — like {@link MERGE_REF_MISMATCH} — it is never retryable, never a
|
|
189
|
+
* rebase target, and reported as `clean_exit`. The server parks it as
|
|
190
|
+
* `merge_credential_no_access`.
|
|
191
|
+
*/
|
|
192
|
+
export const MERGE_CREDENTIAL_NO_ACCESS = "MergeCredentialNoAccess";
|
|
169
193
|
/**
|
|
170
194
|
* BAPI-1234 — the server's migration-graph merge guard refused the merge. Two
|
|
171
195
|
* kinds, kept distinct because they call for different human responses:
|
|
@@ -187,6 +211,7 @@ const MIGRATION_GRAPH_ERROR_KINDS = new Map([
|
|
|
187
211
|
/** Error kinds that are deliberate refusals rather than crashes. */
|
|
188
212
|
const DELIBERATE_REFUSAL_ERROR_KINDS = new Set([
|
|
189
213
|
MERGE_REF_MISMATCH,
|
|
214
|
+
MERGE_CREDENTIAL_NO_ACCESS,
|
|
190
215
|
MERGE_MIGRATION_GRAPH_INVALID,
|
|
191
216
|
MERGE_MIGRATION_GRAPH_UNREADABLE,
|
|
192
217
|
]);
|
|
@@ -222,10 +247,16 @@ const RETRYABLE_MERGE_REASONS = new Set([
|
|
|
222
247
|
* {@link RETRYABLE_MERGE_REASONS} — it is a terminal safety refusal, not a
|
|
223
248
|
* transient or conflict-shaped failure, and must not be re-firable by any
|
|
224
249
|
* future classification change.
|
|
250
|
+
*
|
|
251
|
+
* BAPI-1453: `gh_repo_access_denied` maps to {@link MERGE_CREDENTIAL_NO_ACCESS}
|
|
252
|
+
* in the same early position and for the same reason. Every other reason keeps
|
|
253
|
+
* its existing mapping.
|
|
225
254
|
*/
|
|
226
255
|
export function classifyMergeFailureErrorKind(response) {
|
|
227
256
|
const hasConflictEvent = response.ledger_events.some((ev) => ev.type === "merge.conflict");
|
|
228
257
|
const reason = asString(response.reason ?? undefined);
|
|
258
|
+
if (reason === GH_REPO_ACCESS_DENIED)
|
|
259
|
+
return MERGE_CREDENTIAL_NO_ACCESS;
|
|
229
260
|
if (reason === "ref_mismatch")
|
|
230
261
|
return MERGE_REF_MISMATCH;
|
|
231
262
|
const migrationGraphKind = reason ? MIGRATION_GRAPH_ERROR_KINDS.get(reason) : undefined;
|
|
@@ -251,6 +282,11 @@ export function classifyMergeFailureErrorKind(response) {
|
|
|
251
282
|
* refusal kinds to that set: a `crashed` classification would make the
|
|
252
283
|
* backend's merge re-fire lane retry a merge the server will refuse identically.
|
|
253
284
|
* Every other merge outcome keeps its existing `"crashed"` classification.
|
|
285
|
+
*
|
|
286
|
+
* BAPI-1453: also sets `telemetry.failure_reason_code` — the response's reason
|
|
287
|
+
* when it is in the closed vocabulary, else the fixed
|
|
288
|
+
* `merge_failure_unclassified`, so an arbitrary response string never reaches
|
|
289
|
+
* telemetry. The code is display-only; `error_kind` stays the routing signal.
|
|
254
290
|
*/
|
|
255
291
|
export function buildMergeJobFailure(response) {
|
|
256
292
|
const reason = asString(response.reason ?? undefined) ?? response.status;
|
|
@@ -259,8 +295,21 @@ export function buildMergeJobFailure(response) {
|
|
|
259
295
|
error_kind: errorKind,
|
|
260
296
|
error_message: secretFreeErrorMessage(new Error(`local merge ${response.status}: ${reason}`)),
|
|
261
297
|
classification: DELIBERATE_REFUSAL_ERROR_KINDS.has(errorKind) ? "clean_exit" : "crashed",
|
|
298
|
+
telemetry: { failure_reason_code: mergeResponseReasonCode(response) },
|
|
262
299
|
};
|
|
263
300
|
}
|
|
301
|
+
/** The response's reason as a vocabulary code, or the fixed unclassified code. */
|
|
302
|
+
function mergeResponseReasonCode(response) {
|
|
303
|
+
const reason = asString(response.reason ?? undefined);
|
|
304
|
+
return isFailureReasonCode(reason) ? reason : MERGE_FAILURE_UNCLASSIFIED;
|
|
305
|
+
}
|
|
306
|
+
/** BAPI-1453 — fixed codes for failures the executor's merge flow builds itself. */
|
|
307
|
+
export const MERGE_FAILURE_UNCLASSIFIED = failureReasonCode("merge_failure_unclassified");
|
|
308
|
+
export const MERGE_PAYLOAD_INVALID = failureReasonCode("merge_payload_invalid");
|
|
309
|
+
export const MERGE_ACCESS_UNAVAILABLE = failureReasonCode("merge_access_unavailable");
|
|
310
|
+
export const MERGE_FLOW_TIMEOUT = failureReasonCode("merge_flow_timeout");
|
|
311
|
+
export const MERGE_FLOW_ERROR = failureReasonCode("merge_flow_error");
|
|
312
|
+
export const MERGE_NO_OUTCOME = failureReasonCode("merge_no_outcome");
|
|
264
313
|
/**
|
|
265
314
|
* Post-merge draft epic-integration-PR retry (BAPI-951). Invoked ONLY after a
|
|
266
315
|
* successful ticket-bound merge whose child PR based on `epic/<EPIC>` — never
|
|
@@ -306,6 +355,7 @@ export async function runExecutorMergeJob(job, seams) {
|
|
|
306
355
|
error_kind: "ContractError.MergePayload",
|
|
307
356
|
error_message: resolution.error,
|
|
308
357
|
classification: "crashed",
|
|
358
|
+
telemetry: { failure_reason_code: MERGE_PAYLOAD_INVALID },
|
|
309
359
|
},
|
|
310
360
|
};
|
|
311
361
|
}
|
|
@@ -333,6 +383,7 @@ export async function runExecutorMergeJob(job, seams) {
|
|
|
333
383
|
error_kind: "MergeFailed",
|
|
334
384
|
error_message: secretFreeErrorMessage(err),
|
|
335
385
|
classification: "crashed",
|
|
386
|
+
telemetry: { failure_reason_code: MERGE_FLOW_ERROR },
|
|
336
387
|
},
|
|
337
388
|
};
|
|
338
389
|
}
|
|
@@ -343,8 +394,77 @@ export async function runExecutorMergeJob(job, seams) {
|
|
|
343
394
|
await tryEnsurePostMergeEpicIntegrationPr(seams, fields.expectedBaseBranch);
|
|
344
395
|
return { ok: true, result };
|
|
345
396
|
}
|
|
397
|
+
if (asString(response.reason ?? undefined) === "gh_pr_view_failed") {
|
|
398
|
+
response = await reclassifyPrViewFailure(job, response, seams);
|
|
399
|
+
}
|
|
346
400
|
return { ok: false, failure: buildMergeJobFailure(response) };
|
|
347
401
|
}
|
|
402
|
+
/** Emit one fixed probe diagnostic: the validated job id and a closed category. */
|
|
403
|
+
function logProbeDiagnostic(job, seams, category) {
|
|
404
|
+
const jobId = positiveIntOrNull(job.id);
|
|
405
|
+
try {
|
|
406
|
+
seams.diagnosticLog?.(`[executor] merge job ${jobId ?? "unknown"}: gh_pr_view_failed kept retryable (${category})`);
|
|
407
|
+
}
|
|
408
|
+
catch {
|
|
409
|
+
// A diagnostic sink can never change the merge outcome.
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
/**
|
|
413
|
+
* BAPI-1453 — after a `gh_pr_view_failed`, run Child 2's permission probe ONCE
|
|
414
|
+
* against the server-carried repository, with the credential, runner, and abort
|
|
415
|
+
* signal the merge itself used.
|
|
416
|
+
*
|
|
417
|
+
* Only a proven no-access verdict (a 404, or a 200 without push) turns the
|
|
418
|
+
* failure into `gh_repo_access_denied`. Every other outcome — access confirmed,
|
|
419
|
+
* probe unavailable (401, 403, 429, 5xx, transport, timeout, malformed), a
|
|
420
|
+
* thrown probe, a cancelled flow — returns the response UNCHANGED, so the
|
|
421
|
+
* failure stays a bare, retryable `gh_pr_view_failed`. HTTP statuses are never
|
|
422
|
+
* re-derived here; the probe's own verdict is the whole answer.
|
|
423
|
+
*
|
|
424
|
+
* With no valid `payload.repository` no probe runs at all: there is no
|
|
425
|
+
* `gh repo view` fallback on this path.
|
|
426
|
+
*/
|
|
427
|
+
async function reclassifyPrViewFailure(job, response, seams) {
|
|
428
|
+
const repository = resolveMergeJobRepository(job);
|
|
429
|
+
if (repository.kind !== "valid") {
|
|
430
|
+
logProbeDiagnostic(job, seams, repository.kind === "absent" ? "repository_absent" : "repository_malformed");
|
|
431
|
+
return response;
|
|
432
|
+
}
|
|
433
|
+
// An aborted flow (overall timeout or ownership loss) owns the outcome; a late
|
|
434
|
+
// probe must never be started for it.
|
|
435
|
+
if (seams.localMergeDeps.signal?.aborted) {
|
|
436
|
+
logProbeDiagnostic(job, seams, "probe_cancelled");
|
|
437
|
+
return response;
|
|
438
|
+
}
|
|
439
|
+
const probe = seams.probeRepositoryAccess ??
|
|
440
|
+
((repo) => probeLocalMergeRepositoryAccess(repo, seams.localMergeDeps));
|
|
441
|
+
let result;
|
|
442
|
+
try {
|
|
443
|
+
result = await probe(repository.repository);
|
|
444
|
+
}
|
|
445
|
+
catch {
|
|
446
|
+
// Exception text is never logged: it can carry subprocess detail.
|
|
447
|
+
logProbeDiagnostic(job, seams, "probe_failed");
|
|
448
|
+
return response;
|
|
449
|
+
}
|
|
450
|
+
if (seams.localMergeDeps.signal?.aborted) {
|
|
451
|
+
logProbeDiagnostic(job, seams, "probe_cancelled");
|
|
452
|
+
return response;
|
|
453
|
+
}
|
|
454
|
+
const verdict = classifyLocalGhPreflight(result);
|
|
455
|
+
if (verdict === "no_access") {
|
|
456
|
+
return { ...response, reason: GH_REPO_ACCESS_DENIED };
|
|
457
|
+
}
|
|
458
|
+
if (verdict === "passed")
|
|
459
|
+
return response;
|
|
460
|
+
if (verdict === "probe_unavailable") {
|
|
461
|
+
const detail = result.ok ? undefined : result.probe_unavailable_reason;
|
|
462
|
+
logProbeDiagnostic(job, seams, detail ?? "probe_response_malformed");
|
|
463
|
+
return response;
|
|
464
|
+
}
|
|
465
|
+
logProbeDiagnostic(job, seams, verdict);
|
|
466
|
+
return response;
|
|
467
|
+
}
|
|
348
468
|
/**
|
|
349
469
|
* Build the default local-merge deps for production wiring (no real I/O here).
|
|
350
470
|
* An optional {@link AbortSignal} is threaded through so the job-runner's overall
|
|
@@ -364,3 +484,26 @@ export function buildDefaultMergeLocalDeps(deps, signal) {
|
|
|
364
484
|
signal,
|
|
365
485
|
};
|
|
366
486
|
}
|
|
487
|
+
/**
|
|
488
|
+
* BAPI-1461 — the merge-job deps with the stored-entry fallback, resolved when
|
|
489
|
+
* the merge job RUNS. The store is read afresh each time, so an executor that
|
|
490
|
+
* started before the token was stored can still merge.
|
|
491
|
+
*
|
|
492
|
+
* `allowStoredEntry` comes from the executor's own mode declaration: it is
|
|
493
|
+
* `false` for a durable-host service executor (started with
|
|
494
|
+
* `--scrub-inherited-github-credentials`), which never reads a stored GitHub
|
|
495
|
+
* credential. The entry key comes from the job's server-carried repository.
|
|
496
|
+
*/
|
|
497
|
+
export async function buildMergeLocalDepsForJob(job, deps, options, signal) {
|
|
498
|
+
const repository = resolveMergeJobRepository(job);
|
|
499
|
+
const resolution = await resolveMergeCredentialAtAction(deps.env, "local", {
|
|
500
|
+
allowStoredEntry: options.allowStoredEntry,
|
|
501
|
+
providerRepository: repository.kind === "valid" ? repository.repository : null,
|
|
502
|
+
storeDeps: { env: deps.env, homedir: deps.homedir, readFile: deps.readFile },
|
|
503
|
+
});
|
|
504
|
+
return {
|
|
505
|
+
env: deps.env,
|
|
506
|
+
credential: resolution.status === "resolved" ? resolution.credential : null,
|
|
507
|
+
signal,
|
|
508
|
+
};
|
|
509
|
+
}
|
|
@@ -1,10 +1,16 @@
|
|
|
1
|
+
import { CONSUMER_GUIDE_POINTER } from "../printed-remedy-commands.js";
|
|
2
|
+
import { redactSecrets } from "../secret-redaction.js";
|
|
1
3
|
import { createWorkerDiagnosticsState } from "./worker-diagnostics.js";
|
|
4
|
+
/** BAPI-1453 — at most this many dirty paths are retained as evidence. */
|
|
5
|
+
export const MAX_DIRTY_PATHS = 20;
|
|
6
|
+
/** BAPI-1453 — a retained dirty path is at most this many characters. */
|
|
7
|
+
export const MAX_DIRTY_PATH_CHARS = 256;
|
|
2
8
|
/**
|
|
3
9
|
* Collect load-bearing git telemetry from a worktree. Individual git failures
|
|
4
10
|
* degrade the telemetry (fields omitted) rather than throwing — a degraded
|
|
5
11
|
* signal must never surface as a worker correctness failure.
|
|
6
12
|
*/
|
|
7
|
-
export async function collectGitTelemetry(deps, worktreePath, baseBranch) {
|
|
13
|
+
export async function collectGitTelemetry(deps, worktreePath, baseBranch, options = {}) {
|
|
8
14
|
const telemetry = {};
|
|
9
15
|
try {
|
|
10
16
|
const revList = await deps.runCommand("git", [
|
|
@@ -39,18 +45,27 @@ export async function collectGitTelemetry(deps, worktreePath, baseBranch) {
|
|
|
39
45
|
// `.git/index.lock`. This runs on every heartbeat against the LIVE worker's
|
|
40
46
|
// worktree, so without it a worker's own `git add`/`commit` can fail with
|
|
41
47
|
// "index.lock: File exists".
|
|
48
|
+
// BAPI-1453: `-z` only when paths are wanted. NUL-delimited records are the
|
|
49
|
+
// only form that survives a filename containing a newline or a quote.
|
|
42
50
|
const status = await deps.runCommand("git", [
|
|
43
51
|
"-C",
|
|
44
52
|
worktreePath,
|
|
45
53
|
"--no-optional-locks",
|
|
46
54
|
"status",
|
|
47
55
|
"--porcelain",
|
|
56
|
+
...(options.includeDirtyPaths === true ? ["-z"] : []),
|
|
48
57
|
]);
|
|
49
58
|
if (status.exitCode === 0) {
|
|
50
|
-
|
|
59
|
+
// The verdict never depends on path parsing: any output at all is dirty.
|
|
60
|
+
const dirty = status.stdout.replace(/\0/g, "").trim().length > 0;
|
|
51
61
|
telemetry.dirty = dirty;
|
|
52
62
|
if (dirty) {
|
|
53
63
|
telemetry.last_activity_at = new Date(deps.now()).toISOString();
|
|
64
|
+
if (options.includeDirtyPaths === true) {
|
|
65
|
+
const evidence = parseDirtyPathEvidence(status.stdout);
|
|
66
|
+
telemetry.dirty_paths = evidence.paths;
|
|
67
|
+
telemetry.dirty_paths_overflow_count = evidence.overflow;
|
|
68
|
+
}
|
|
54
69
|
}
|
|
55
70
|
}
|
|
56
71
|
}
|
|
@@ -59,6 +74,60 @@ export async function collectGitTelemetry(deps, worktreePath, baseBranch) {
|
|
|
59
74
|
}
|
|
60
75
|
return telemetry;
|
|
61
76
|
}
|
|
77
|
+
/** True for a path that is absolute, drive/UNC-rooted, or escapes the worktree. */
|
|
78
|
+
function isUnsafeRelativePath(path) {
|
|
79
|
+
if (path.startsWith("/") || path.startsWith("\\"))
|
|
80
|
+
return true;
|
|
81
|
+
if (/^[A-Za-z]:/.test(path))
|
|
82
|
+
return true;
|
|
83
|
+
return path.split(/[\\/]/).some((segment) => segment === "..");
|
|
84
|
+
}
|
|
85
|
+
/** Escape every control character so a filename can never forge a log line. */
|
|
86
|
+
function escapeControlCharacters(path) {
|
|
87
|
+
return path.replace(/[\x00-\x1f\x7f-\x9f]/g, (ch) => `\\x${ch.charCodeAt(0).toString(16).padStart(2, "0")}`);
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Reduce `git status --porcelain -z` output to at most {@link MAX_DIRTY_PATHS}
|
|
91
|
+
* repository-relative paths plus a count of everything omitted (BAPI-1453).
|
|
92
|
+
*
|
|
93
|
+
* Each record is `XY <path>`; a rename or copy (`R` or `C` in either column) is
|
|
94
|
+
* followed by one more NUL-terminated record holding the source path, which is
|
|
95
|
+
* consumed and not retained. An entry is omitted — and counted — when it is
|
|
96
|
+
* malformed, absolute, escapes the worktree, longer than
|
|
97
|
+
* {@link MAX_DIRTY_PATH_CHARS} once escaped, needs secret redaction, or arrives
|
|
98
|
+
* after the cap. Never throws; never retains file content.
|
|
99
|
+
*/
|
|
100
|
+
export function parseDirtyPathEvidence(stdout) {
|
|
101
|
+
const paths = [];
|
|
102
|
+
let overflow = 0;
|
|
103
|
+
const records = stdout.split("\0");
|
|
104
|
+
for (let i = 0; i < records.length; i++) {
|
|
105
|
+
const record = records[i];
|
|
106
|
+
if (record.length === 0)
|
|
107
|
+
continue;
|
|
108
|
+
const status = record.slice(0, 2);
|
|
109
|
+
if (/[RC]/.test(status))
|
|
110
|
+
i += 1; // the source path record of a rename/copy
|
|
111
|
+
if (record.length < 4 || record[2] !== " ") {
|
|
112
|
+
overflow += 1;
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
const path = record.slice(3);
|
|
116
|
+
if (paths.length >= MAX_DIRTY_PATHS) {
|
|
117
|
+
overflow += 1;
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
const escaped = escapeControlCharacters(path);
|
|
121
|
+
if (isUnsafeRelativePath(path) ||
|
|
122
|
+
escaped.length > MAX_DIRTY_PATH_CHARS ||
|
|
123
|
+
redactSecrets(escaped) !== escaped) {
|
|
124
|
+
overflow += 1;
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
paths.push(escaped);
|
|
128
|
+
}
|
|
129
|
+
return { paths, overflow };
|
|
130
|
+
}
|
|
62
131
|
/** A full 40-character hex object name. */
|
|
63
132
|
const FULL_SHA_PATTERN = /^[0-9a-f]{40}$/i;
|
|
64
133
|
/** Normalize a candidate SHA, or `null` when it is absent/malformed. */
|
|
@@ -298,8 +367,8 @@ const RATE_LIMIT_CONSEQUENCE_TEXT = "This warning is advisory only and triggers
|
|
|
298
367
|
"waiting happens only when the worker then fails with the supported terminal " +
|
|
299
368
|
"rejection and the server advertised capacity recovery for this job; otherwise " +
|
|
300
369
|
"the failure follows ordinary failure handling.";
|
|
301
|
-
/** Fixed
|
|
302
|
-
const RATE_LIMIT_RUNBOOK_REFERENCE =
|
|
370
|
+
/** Fixed guide reference appended to every rate-limit diagnostic. */
|
|
371
|
+
const RATE_LIMIT_RUNBOOK_REFERENCE = `See ${CONSUMER_GUIDE_POINTER}.`;
|
|
303
372
|
/**
|
|
304
373
|
* Render the operator-facing rate-limit line (BAPI-828, BAPI-898).
|
|
305
374
|
*
|