@bridge_gpt/mcp-server 0.2.68 → 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 +226 -320
- 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 +650 -98
- package/build/check-update-cli.js +74 -0
- package/build/claude-review-workflow-drift.js +3 -3
- package/build/claude-settings.js +84 -2
- package/build/cli-help.js +7 -1
- package/build/codex-bridge-profile.js +1 -1
- package/build/codex-home.js +9 -1
- package/build/codex-launcher-pin.js +598 -0
- package/build/commands.generated.js +8 -8
- 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/connect-github-api.js +33 -14
- package/build/credential-store.js +356 -13
- package/build/docs.generated.js +27 -14
- package/build/doctor.js +203 -38
- 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 +131 -120
- 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 +68 -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 +115 -4
- 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 +1214 -465
- 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 +629 -350
- package/build/launcher-config-inspection.js +184 -19
- package/build/mcp-host-targets.js +4 -0
- package/build/mcp-identity.js +7 -0
- 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/session-upgrade-notice.js +86 -0
- package/build/setup-epic.js +400 -30
- package/build/shared-resources.generated.js +5 -0
- package/build/start-tickets.js +43 -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/tool-call-advisory.js +162 -0
- package/build/tool-surface-gating.js +28 -89
- package/build/update-check.js +97 -16
- package/build/update-status.js +61 -71
- package/build/upgrade-advice.js +15 -11
- package/build/upgrade-cli.js +360 -20
- 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
|
@@ -35,6 +35,7 @@
|
|
|
35
35
|
* (claude 2.1.201), so plain `permissions.deny` is provisioned; the PreToolUse
|
|
36
36
|
* fallback is available for CLIs where settings-deny is not enforced.
|
|
37
37
|
*/
|
|
38
|
+
import { randomUUID } from "node:crypto";
|
|
38
39
|
import path from "node:path";
|
|
39
40
|
import { mergeClaudeSettingsWithAutoMemoryDisabled, mergeClaudeSettingsWithCommandHook, provisionClaudeSettingsForWorktree, DEFAULT_PRE_TOOL_USE_MATCHER, } from "../claude-settings.js";
|
|
40
41
|
/**
|
|
@@ -276,139 +277,234 @@ function excludeWarning(worktreePath, category) {
|
|
|
276
277
|
`${EXECUTOR_EXCLUDE_LINES.join(" and ")} are not protected from worker commits on this ` +
|
|
277
278
|
"worktree; inspect it before trusting the worktree's staged changes; continuing fail-open.");
|
|
278
279
|
}
|
|
280
|
+
const EXCLUDE_UNRESOLVED_CATEGORY = {
|
|
281
|
+
no_gitdir_pointer: "git metadata carries no gitdir pointer",
|
|
282
|
+
metadata_unreadable: "git metadata could not be read",
|
|
283
|
+
metadata_invalid: "git metadata does not reference a usable git directory",
|
|
284
|
+
commondir_unusable: "the git common-directory pointer is unusable",
|
|
285
|
+
exclude_unreadable: "the exclude file could not be read",
|
|
286
|
+
writability_unknown: "exclude writability could not be determined",
|
|
287
|
+
};
|
|
288
|
+
function errnoCode(err) {
|
|
289
|
+
return err && typeof err === "object" ? err.code : undefined;
|
|
290
|
+
}
|
|
291
|
+
const S_IFMT = 0o170000;
|
|
292
|
+
const S_IFDIR = 0o040000;
|
|
293
|
+
/** `true` / `false` for a directory / non-directory; `null` when it is absent. */
|
|
294
|
+
async function directoryState(deps, dirPath) {
|
|
295
|
+
try {
|
|
296
|
+
return ((await deps.stat(dirPath)).mode & S_IFMT) === S_IFDIR;
|
|
297
|
+
}
|
|
298
|
+
catch (err) {
|
|
299
|
+
if (errnoCode(err) === "ENOENT")
|
|
300
|
+
return null;
|
|
301
|
+
throw err;
|
|
302
|
+
}
|
|
303
|
+
}
|
|
279
304
|
/**
|
|
280
|
-
*
|
|
281
|
-
*
|
|
282
|
-
*
|
|
283
|
-
* operator's global git excludes (`~/.config/git/ignore`) no longer apply inside
|
|
284
|
-
* a worker — empirically, a worker `git add -A` commits the deny-layer file this
|
|
285
|
-
* module just wrote. A per-worktree `.git/info/exclude` entry is invisible to
|
|
286
|
-
* the diff and travels with the gitdir, so it closes that without touching the
|
|
287
|
-
* worktree's tracked `.gitignore`.
|
|
288
|
-
*
|
|
289
|
-
* A linked worktree's `.git` is a FILE containing `gitdir: <path>` pointing at
|
|
290
|
-
* the main repository's per-worktree gitdir; that indirection is resolved rather
|
|
291
|
-
* than assuming a directory (`EISDIR` on the read identifies a real main-checkout
|
|
292
|
-
* `.git` directory). The per-worktree gitdir is then resolved onward to the
|
|
293
|
-
* COMMON dir, because that — not the per-worktree dir — is where git reads
|
|
294
|
-
* `info/exclude` from; see the comment at the resolution site. Idempotent: the
|
|
295
|
-
* line is appended once, never duplicated.
|
|
305
|
+
* Locate the COMMON `info/exclude` for a checkout, with filesystem reads only (no
|
|
306
|
+
* Git subprocess, no writes). Shared by the writer and the read-only resolver so
|
|
307
|
+
* they can never disagree about the destination.
|
|
296
308
|
*
|
|
297
|
-
*
|
|
298
|
-
*
|
|
299
|
-
*
|
|
300
|
-
*
|
|
301
|
-
*
|
|
302
|
-
*
|
|
303
|
-
*
|
|
304
|
-
* pointer, or cannot be read) — non-blocking, but WARNED. Git metadata is
|
|
305
|
-
* there and the executor could not use it, which means the settings file is
|
|
306
|
-
* unprotected on this worktree and a human should look.
|
|
307
|
-
*
|
|
308
|
-
* That distinction is the whole fix: a total failure to ever write was invisible
|
|
309
|
-
* precisely because "could not resolve" and "nothing to do" returned the same
|
|
310
|
-
* silent success.
|
|
311
|
-
*
|
|
312
|
-
* Warnings are bounded to a failure category plus the affected worktree path.
|
|
313
|
-
* They never carry `.git` contents, exception text, environment values, or
|
|
314
|
-
* settings-file content.
|
|
309
|
+
* `info/` is a COMMON-dir file: git reads exclusions from `<common>/info/exclude`,
|
|
310
|
+
* and for a linked worktree the common dir is the MAIN repository's `.git`, not
|
|
311
|
+
* `<main>/.git/worktrees/<name>` (BAPI-815/R2 — writing to the per-worktree gitdir
|
|
312
|
+
* made the whole mechanism a no-op). `<gitdir>/commondir` holds the pointer. Its
|
|
313
|
+
* ABSENCE (`ENOENT`) is the only case that falls back to the git dir itself (a main
|
|
314
|
+
* checkout); any other read error, or a pointer that does not name a directory, is
|
|
315
|
+
* unresolved rather than a guess.
|
|
315
316
|
*/
|
|
316
|
-
async function
|
|
317
|
+
async function resolveWorktreeExcludePath(worktreePath, deps) {
|
|
317
318
|
const gitPath = path.join(worktreePath, ".git");
|
|
318
319
|
let gitDir;
|
|
319
320
|
try {
|
|
320
321
|
const raw = await deps.readFile(gitPath);
|
|
321
322
|
const match = /^gitdir:\s*(.+?)\s*$/m.exec(raw);
|
|
322
|
-
if (!match)
|
|
323
|
-
|
|
324
|
-
// function understands; do not guess at a location to write into. Present
|
|
325
|
-
// but unusable — degrade loudly rather than reporting a clean success.
|
|
326
|
-
return {
|
|
327
|
-
ok: true,
|
|
328
|
-
warning: excludeWarning(worktreePath, "git metadata carries no gitdir pointer"),
|
|
329
|
-
};
|
|
330
|
-
}
|
|
323
|
+
if (!match)
|
|
324
|
+
return { kind: "unresolved", reason: "no_gitdir_pointer" };
|
|
331
325
|
const target = match[1];
|
|
332
326
|
gitDir = path.isAbsolute(target) ? target : path.resolve(worktreePath, target);
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
// Main-checkout shape: `.git` is a real directory.
|
|
338
|
-
gitDir = gitPath;
|
|
339
|
-
}
|
|
340
|
-
else if (code === "ENOENT") {
|
|
341
|
-
// No git checkout here at all — nothing to protect, nothing to report.
|
|
342
|
-
return { ok: true };
|
|
327
|
+
try {
|
|
328
|
+
const isDir = await directoryState(deps, gitDir);
|
|
329
|
+
if (isDir !== true)
|
|
330
|
+
return { kind: "unresolved", reason: "metadata_invalid" };
|
|
343
331
|
}
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
// error CODE is a bounded category; the exception itself never escapes.
|
|
347
|
-
return {
|
|
348
|
-
ok: true,
|
|
349
|
-
warning: excludeWarning(worktreePath, "git metadata could not be read"),
|
|
350
|
-
};
|
|
332
|
+
catch {
|
|
333
|
+
return { kind: "unresolved", reason: "metadata_unreadable" };
|
|
351
334
|
}
|
|
352
335
|
}
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
// operator's `~/.config/git/ignore` carried the same rule — the protection
|
|
363
|
-
// built to be machine-independent was supplied entirely by machine-specific
|
|
364
|
-
// config, and the day that global ignore is absent a worker commits the file.
|
|
365
|
-
//
|
|
366
|
-
// `<gitdir>/commondir` holds the pointer (usually the relative `../..`), and
|
|
367
|
-
// it is a plain file, so this needs no new command-runner dependency. Its
|
|
368
|
-
// absence means this IS the common dir (a main checkout), which is why the
|
|
369
|
-
// fallback is `gitDir` itself rather than an error.
|
|
336
|
+
catch (err) {
|
|
337
|
+
const code = errnoCode(err);
|
|
338
|
+
if (code === "EISDIR")
|
|
339
|
+
gitDir = gitPath; // main-checkout shape
|
|
340
|
+
else if (code === "ENOENT")
|
|
341
|
+
return { kind: "unresolved", reason: "no_checkout" };
|
|
342
|
+
else
|
|
343
|
+
return { kind: "unresolved", reason: "metadata_unreadable" };
|
|
344
|
+
}
|
|
370
345
|
let commonDir = gitDir;
|
|
371
346
|
try {
|
|
372
347
|
const pointer = (await deps.readFile(path.join(gitDir, "commondir"))).trim();
|
|
373
|
-
if (pointer.length
|
|
374
|
-
|
|
348
|
+
if (pointer.length === 0)
|
|
349
|
+
return { kind: "unresolved", reason: "commondir_unusable" };
|
|
350
|
+
commonDir = path.isAbsolute(pointer) ? pointer : path.resolve(gitDir, pointer);
|
|
351
|
+
try {
|
|
352
|
+
if ((await directoryState(deps, commonDir)) !== true) {
|
|
353
|
+
return { kind: "unresolved", reason: "commondir_unusable" };
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
catch {
|
|
357
|
+
return { kind: "unresolved", reason: "commondir_unusable" };
|
|
375
358
|
}
|
|
376
359
|
}
|
|
377
|
-
catch {
|
|
360
|
+
catch (err) {
|
|
361
|
+
if (errnoCode(err) !== "ENOENT")
|
|
362
|
+
return { kind: "unresolved", reason: "commondir_unusable" };
|
|
378
363
|
/* no commondir pointer: `gitDir` is already the common dir */
|
|
379
364
|
}
|
|
365
|
+
const infoDir = path.join(commonDir, "info");
|
|
366
|
+
return { kind: "resolved", excludePath: path.join(infoDir, "exclude"), infoDir };
|
|
367
|
+
}
|
|
368
|
+
/**
|
|
369
|
+
* Report the local exclude readiness for a checkout WITHOUT changing anything.
|
|
370
|
+
*
|
|
371
|
+
* `present` means the exclude file is readable and already contains every
|
|
372
|
+
* canonical {@link EXECUTOR_EXCLUDE_LINES} entry as an exact line — not merely that
|
|
373
|
+
* a file exists. An absent file or a missing entry is not present. `writable` comes
|
|
374
|
+
* from non-writing access checks: the existing file when there is one, otherwise
|
|
375
|
+
* the nearest existing ancestor that would have to receive the new file.
|
|
376
|
+
*
|
|
377
|
+
* This is a local filesystem snapshot. It performs no `mkdir`, write, rename, or
|
|
378
|
+
* remediation, and it does not verify worker protection or detect remote executor
|
|
379
|
+
* clones.
|
|
380
|
+
*/
|
|
381
|
+
export async function resolveWorktreeSettingsExclude(worktreePath, deps) {
|
|
382
|
+
const resolved = await resolveWorktreeExcludePath(worktreePath, deps);
|
|
383
|
+
if (resolved.kind === "unresolved")
|
|
384
|
+
return resolved;
|
|
385
|
+
let content = null;
|
|
386
|
+
try {
|
|
387
|
+
content = await deps.readFile(resolved.excludePath);
|
|
388
|
+
}
|
|
389
|
+
catch (err) {
|
|
390
|
+
if (errnoCode(err) !== "ENOENT")
|
|
391
|
+
return { kind: "unresolved", reason: "exclude_unreadable" };
|
|
392
|
+
}
|
|
393
|
+
const lines = new Set((content ?? "").split(/\r?\n/));
|
|
394
|
+
const present = content !== null && EXECUTOR_EXCLUDE_LINES.every((line) => lines.has(line));
|
|
395
|
+
let probe = content !== null ? resolved.excludePath : resolved.infoDir;
|
|
396
|
+
if (content === null) {
|
|
397
|
+
// Nearest existing ancestor that would have to receive the new file.
|
|
398
|
+
for (;;) {
|
|
399
|
+
let exists;
|
|
400
|
+
try {
|
|
401
|
+
exists = (await directoryState(deps, probe)) !== null;
|
|
402
|
+
}
|
|
403
|
+
catch {
|
|
404
|
+
return { kind: "unresolved", reason: "writability_unknown" };
|
|
405
|
+
}
|
|
406
|
+
if (exists)
|
|
407
|
+
break;
|
|
408
|
+
const parent = path.dirname(probe);
|
|
409
|
+
if (parent === probe)
|
|
410
|
+
return { kind: "unresolved", reason: "writability_unknown" };
|
|
411
|
+
probe = parent;
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
let writable = true;
|
|
415
|
+
try {
|
|
416
|
+
await deps.accessWritable(probe);
|
|
417
|
+
}
|
|
418
|
+
catch (err) {
|
|
419
|
+
const code = errnoCode(err);
|
|
420
|
+
if (code === "EACCES" || code === "EPERM" || code === "EROFS")
|
|
421
|
+
writable = false;
|
|
422
|
+
else
|
|
423
|
+
return { kind: "unresolved", reason: "writability_unknown" };
|
|
424
|
+
}
|
|
425
|
+
return { kind: "resolved", excludePath: resolved.excludePath, present, writable };
|
|
426
|
+
}
|
|
427
|
+
/**
|
|
428
|
+
* Keep the executor-owned `.claude/settings.local.json` and the conductor's
|
|
429
|
+
* `.conductor/` artifacts out of worker commits (BAPI-740/F2, BAPI-814/D2).
|
|
430
|
+
* Worker-config isolation redirects `XDG_CONFIG_HOME`, so the
|
|
431
|
+
* operator's global git excludes (`~/.config/git/ignore`) no longer apply inside
|
|
432
|
+
* a worker — empirically, a worker `git add -A` commits the deny-layer file this
|
|
433
|
+
* module just wrote. A per-worktree `.git/info/exclude` entry is invisible to
|
|
434
|
+
* the diff and travels with the gitdir, so it closes that without touching the
|
|
435
|
+
* worktree's tracked `.gitignore`.
|
|
436
|
+
*
|
|
437
|
+
* The destination is the COMMON `info/exclude`, located by
|
|
438
|
+
* {@link resolveWorktreeExcludePath}. Fail-open at every step, but NOT silent
|
|
439
|
+
* (BAPI-815/R2): no checkout (`ENOENT` on `.git`) is a benign silent success;
|
|
440
|
+
* present-but-unresolvable metadata is non-blocking but WARNED; a failed read or
|
|
441
|
+
* publish keeps `ok: false`.
|
|
442
|
+
*
|
|
443
|
+
* Publication contract: the file is re-read immediately before composing the
|
|
444
|
+
* replacement (only `ENOENT` counts as empty), existing content is kept verbatim —
|
|
445
|
+
* ordering, comments, line endings, and duplicates — and only absent canonical
|
|
446
|
+
* lines are appended. The result is written to a uniquely named temporary file in
|
|
447
|
+
* the same `info/` directory and renamed into place; no lockfile is used, and
|
|
448
|
+
* nothing is published when no canonical line is missing. Competing executor
|
|
449
|
+
* writers add the same canonical entries, so fresh reads plus atomic publication
|
|
450
|
+
* prevent torn files and executor-created duplicates. This does not serialize
|
|
451
|
+
* against unrelated external editors.
|
|
452
|
+
*
|
|
453
|
+
* Warnings are bounded to a failure category plus the affected worktree path.
|
|
454
|
+
* They never carry `.git` contents, exception text, environment values, or
|
|
455
|
+
* settings-file content.
|
|
456
|
+
*/
|
|
457
|
+
export async function provisionWorktreeSettingsExclude(worktreePath, deps) {
|
|
458
|
+
const resolved = await resolveWorktreeExcludePath(worktreePath, deps);
|
|
459
|
+
if (resolved.kind === "unresolved") {
|
|
460
|
+
if (resolved.reason === "no_checkout")
|
|
461
|
+
return { ok: true };
|
|
462
|
+
return {
|
|
463
|
+
ok: true,
|
|
464
|
+
warning: excludeWarning(worktreePath, EXCLUDE_UNRESOLVED_CATEGORY[resolved.reason]),
|
|
465
|
+
};
|
|
466
|
+
}
|
|
467
|
+
const { excludePath, infoDir } = resolved;
|
|
468
|
+
let tempPath;
|
|
380
469
|
try {
|
|
381
|
-
const infoDir = path.join(commonDir, "info");
|
|
382
470
|
await deps.mkdir(infoDir, { recursive: true });
|
|
383
|
-
const excludePath = path.join(infoDir, "exclude");
|
|
384
471
|
let existing = "";
|
|
385
472
|
try {
|
|
386
473
|
existing = await deps.readFile(excludePath);
|
|
387
474
|
}
|
|
388
|
-
catch {
|
|
389
|
-
|
|
475
|
+
catch (err) {
|
|
476
|
+
if (errnoCode(err) !== "ENOENT") {
|
|
477
|
+
return {
|
|
478
|
+
ok: false,
|
|
479
|
+
warning: excludeWarning(worktreePath, "the exclude file could not be read"),
|
|
480
|
+
};
|
|
481
|
+
}
|
|
390
482
|
}
|
|
391
483
|
// Idempotent per LINE, not per file: the settings entry predates the
|
|
392
484
|
// `.conductor/` one, so a worktree provisioned by an earlier executor build
|
|
393
|
-
// already contains the first and still needs the second.
|
|
394
|
-
// "does the file mention anything of ours?" check would skip it forever.
|
|
485
|
+
// already contains the first and still needs the second.
|
|
395
486
|
const present = new Set(existing.split(/\r?\n/));
|
|
396
487
|
const missing = EXECUTOR_EXCLUDE_LINES.filter((line) => !present.has(line));
|
|
397
|
-
if (missing.length === 0)
|
|
488
|
+
if (missing.length === 0)
|
|
398
489
|
return { ok: true };
|
|
399
|
-
}
|
|
400
|
-
// One write. Unrelated user-authored entries are preserved verbatim, and a
|
|
401
|
-
// file that did not end in a newline gets one first so the appended lines
|
|
402
|
-
// cannot fuse onto the operator's last entry.
|
|
403
490
|
const prefix = existing.length === 0 || existing.endsWith("\n") ? existing : `${existing}\n`;
|
|
404
|
-
|
|
491
|
+
tempPath = path.join(infoDir, `exclude.${process.pid}.${randomUUID()}.tmp`);
|
|
492
|
+
await deps.writeFile(tempPath, `${prefix}${missing.map((line) => `${line}\n`).join("")}`);
|
|
493
|
+
await deps.rename(tempPath, excludePath);
|
|
405
494
|
return { ok: true };
|
|
406
495
|
}
|
|
407
496
|
catch {
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
497
|
+
if (tempPath !== undefined) {
|
|
498
|
+
try {
|
|
499
|
+
await deps.removeFile(tempPath);
|
|
500
|
+
}
|
|
501
|
+
catch {
|
|
502
|
+
/* best effort: only this writer's own temporary file */
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
// The gitdir resolved but the exclude could not be published. This keeps
|
|
506
|
+
// `ok: false`, because the caller's established contract treats a failed
|
|
507
|
+
// WRITE as the degraded case worth aggregating into the deny layer's result.
|
|
412
508
|
return {
|
|
413
509
|
ok: false,
|
|
414
510
|
warning: excludeWarning(worktreePath, "the exclude file could not be written"),
|
|
@@ -2,10 +2,10 @@ import { DEFAULT_PROBE_TIMEOUT_MS } from "../agent-capabilities/types.js";
|
|
|
2
2
|
import { inspectExecutorAgentAdapter, inspectionForFailedResolution, } from "../agent-launchers/executor-adapter-inspection.js";
|
|
3
3
|
import { resolveExecutorAgentAdapter } from "../agent-launchers/executor-adapter-registry.js";
|
|
4
4
|
import { resolveCompletionEvidenceCapability } from "../agent-launchers/executor-adapter.js";
|
|
5
|
-
import { COMPLETION_EVIDENCE_CAPABILITY, REQUIRED_VERIFICATION_CAPABILITY, cliVersionAtLeast, normalizeCliVersion, parseCliVersion, runtimeMatchesSupported, } from "./completion-contract.js";
|
|
5
|
+
import { CLEANUP_REPORT_CAPABILITY, COMPLETION_EVIDENCE_CAPABILITY, REQUIRED_VERIFICATION_CAPABILITY, REQUIRED_VERIFICATION_CI_DEFERRAL_CAPABILITY, cliVersionAtLeast, normalizeCliVersion, parseCliVersion, runtimeMatchesSupported, } from "./completion-contract.js";
|
|
6
6
|
import { buildHeadlessAgentEnv } from "./env.js";
|
|
7
7
|
import { resolveExecutorAgentId } from "./agent-identity.js";
|
|
8
|
-
import { mkdtemp, rm } from "node:fs/promises";
|
|
8
|
+
import { mkdtemp, rm, rename as fsRename } from "node:fs/promises";
|
|
9
9
|
import { tmpdir } from "node:os";
|
|
10
10
|
import { join } from "node:path";
|
|
11
11
|
import { spawnSync } from "node:child_process";
|
|
@@ -291,6 +291,9 @@ export async function collectExecutorPreflight(options, deps, seams = {}) {
|
|
|
291
291
|
denyProvisioningDeps: {
|
|
292
292
|
readFile: deps.readFile, writeFile: deps.writeFile,
|
|
293
293
|
mkdir: deps.mkdir, homedir: deps.homedir,
|
|
294
|
+
stat: deps.stat,
|
|
295
|
+
rename: (from, to) => fsRename(from, to),
|
|
296
|
+
removeFile: (filePath) => rm(filePath, { force: true }),
|
|
294
297
|
},
|
|
295
298
|
} : {}),
|
|
296
299
|
},
|
|
@@ -646,7 +649,15 @@ export function buildClaimManifest(report, options, freeSlots) {
|
|
|
646
649
|
// server drops the unknown token (`normalize_declared_capabilities`).
|
|
647
650
|
capabilities: [
|
|
648
651
|
...(report.completionEvidenceQualified === true
|
|
649
|
-
? [
|
|
652
|
+
? [
|
|
653
|
+
COMPLETION_EVIDENCE_CAPABILITY,
|
|
654
|
+
REQUIRED_VERIFICATION_CAPABILITY,
|
|
655
|
+
REQUIRED_VERIFICATION_CI_DEFERRAL_CAPABILITY,
|
|
656
|
+
// BAPI-1504: this executor persists ownership before spawning a
|
|
657
|
+
// covered worker and delivers cleanup reports, so it may claim a job
|
|
658
|
+
// that requires the capability. An older server drops the token.
|
|
659
|
+
CLEANUP_REPORT_CAPABILITY,
|
|
660
|
+
]
|
|
650
661
|
: []),
|
|
651
662
|
FOCUSED_REBASE_CAPABILITY,
|
|
652
663
|
SCOPE_WAIT_CAPABILITY,
|
|
@@ -773,8 +773,34 @@ export async function recoverInterruptedOwnership(options) {
|
|
|
773
773
|
catch {
|
|
774
774
|
/* logging is best-effort */
|
|
775
775
|
}
|
|
776
|
-
|
|
777
|
-
|
|
776
|
+
// A malformed record proved nothing and has no token to report: it is removed
|
|
777
|
+
// exactly as before. A real record is reported only after a CONFIRMED sweep.
|
|
778
|
+
let removeRecord = true;
|
|
779
|
+
if (options.reportCleanup !== undefined && record !== null) {
|
|
780
|
+
removeRecord = false;
|
|
781
|
+
if (verdict.verdict === "confirmed" && verdict.survivors === 0) {
|
|
782
|
+
try {
|
|
783
|
+
const outcome = await options.reportCleanup({
|
|
784
|
+
jobId: record.jobId,
|
|
785
|
+
repoName: record.repoName,
|
|
786
|
+
token: record.token,
|
|
787
|
+
});
|
|
788
|
+
removeRecord = outcome.kind === "accepted" || outcome.kind === "not_applicable";
|
|
789
|
+
try {
|
|
790
|
+
log(`[executor] BAPI-1504 cleanup report job=${fileJobId} outcome=${outcome.kind === "retained" ? `retained:${outcome.reason}` : outcome.kind}`);
|
|
791
|
+
}
|
|
792
|
+
catch {
|
|
793
|
+
/* logging is best-effort */
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
catch {
|
|
797
|
+
removeRecord = false;
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
if (removeRecord)
|
|
802
|
+
await removeOwnershipRecord(stateDir, fileJobId, fsDeps);
|
|
803
|
+
results.push({ jobId: fileJobId, verdict, recordRemoved: removeRecord });
|
|
778
804
|
}
|
|
779
805
|
return results;
|
|
780
806
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { renderWorkerVerificationGuidance } from "./worker-verification-guidance.js";
|
|
1
|
+
import { renderWorkerVerificationGuidance, renderWorkerTimeBudgetGuidance } from "./worker-verification-guidance.js";
|
|
2
2
|
/**
|
|
3
3
|
* Executor-side structured prompt-spec renderer (BAPI-550, TDD §6/§7).
|
|
4
4
|
*
|
|
@@ -32,6 +32,18 @@ import { EPIC_WORKAROUNDS_DIR, epicWorkaroundRecordPath } from "../epic-workarou
|
|
|
32
32
|
* work unfinished, which parks the ticket as `required_work_incomplete` through
|
|
33
33
|
* the unchanged completion contract.
|
|
34
34
|
*
|
|
35
|
+
* BAPI-1454 adds planned hand-offs: a capability needed only to VERIFY an
|
|
36
|
+
* acceptance criterion (a running app or database, a toolchain version, live
|
|
37
|
+
* credentials) is recorded as a hand-off, never as a core gap, and the worker
|
|
38
|
+
* continues without reporting that criterion as passed.
|
|
39
|
+
*
|
|
40
|
+
* BAPI-1492 adds the verification hand-off order for an E2E or rendered-UI step:
|
|
41
|
+
* run it locally (the disposable job database when one is provided), defer to
|
|
42
|
+
* `remote_ci` only when a named required workflow, job, or command runs it, and
|
|
43
|
+
* otherwise record it escalated with a `handoff` object carrying the observed
|
|
44
|
+
* failure. Review steps always run and are recorded executed. The server alone
|
|
45
|
+
* decides whether a hand-off counts, from persisted run state.
|
|
46
|
+
*
|
|
35
47
|
* The worker decides from the ticket and the code alone; no supervisor scan
|
|
36
48
|
* result is injected here. Kept SINGLE-LINE and free of `;`, quotes, and
|
|
37
49
|
* apostrophes, like `buildPrBaseContractLaunchInstruction`, because it travels
|
|
@@ -46,7 +58,24 @@ export function buildEpicWorkaroundWorkerInstruction(ticketKey) {
|
|
|
46
58
|
"only that feature), keep the core path working, and write " + record + " naming the requirement, " +
|
|
47
59
|
"the guarded substitute you delivered, and the step a human must still take. Commit that record on " +
|
|
48
60
|
"this ticket branch with git add -f " + record + ". For a core gap, never write a workaround or a " +
|
|
49
|
-
"record: leave the required work unfinished so the run parks this ticket for a human."
|
|
61
|
+
"record: leave the required work unfinished so the run parks this ticket for a human. Planned " +
|
|
62
|
+
"hand-offs: if a running app or database, a toolchain version, or live credentials are needed only " +
|
|
63
|
+
"to verify an acceptance criterion and not to implement it safely, that is a planned hand-off and " +
|
|
64
|
+
"not a core gap. Record the affected acceptance criterion and the human verification step in " + record +
|
|
65
|
+
" and continue. Never report an unverified acceptance criterion as passed. A dependency needed to " +
|
|
66
|
+
"implement safely keeps ordinary core gap handling. " +
|
|
67
|
+
"Verification hand-off for an e2e_tests or rendered_ui_review step: first run it locally, using the " +
|
|
68
|
+
"disposable job database when one is provided. Record it escalated with prDependency remote_ci only " +
|
|
69
|
+
"when a named required CI workflow, job, or command runs that check. Otherwise, if a missing live " +
|
|
70
|
+
"server, database, toolchain, or credential stopped the check before it produced any result, record " +
|
|
71
|
+
"the step escalated with a handoff object giving the cause, the affected criterion, an observation " +
|
|
72
|
+
"naming the command you ran and the missing capability it hit, the head SHA it ran against, and the " +
|
|
73
|
+
"next human action. Never put credentials or secrets in the observation. A failed assertion, an " +
|
|
74
|
+
"application error from a check that did run, and unfinished implementation or test authoring stay " +
|
|
75
|
+
"blockers, and an unclear failure stays an ordinary escalation. Review steps (test_gap_review, " +
|
|
76
|
+
"final_plan_review, and combined_review_test) always run, acknowledge the handed-off criterion from " +
|
|
77
|
+
"that escalated record and the workaround record written in the same job, and are recorded executed, " +
|
|
78
|
+
"never handed off. A new head needs a fresh handoff observation.");
|
|
50
79
|
}
|
|
51
80
|
/** Stable named-error kind used for every prompt-spec contract failure. */
|
|
52
81
|
const PROMPT_CONTRACT_ERROR = "ContractError.Prompt";
|
|
@@ -391,19 +420,33 @@ export function readCiFixDispatchContext(job) {
|
|
|
391
420
|
if (repoName !== job.repo_name || (jobExpectedBranch && jobExpectedBranch !== expectedBranch)) {
|
|
392
421
|
throw ciFixContextError(job, "identity_mismatch");
|
|
393
422
|
}
|
|
394
|
-
const
|
|
423
|
+
const candidateAnchor = typeof raw.failing_head_sha === "string"
|
|
424
|
+
? raw.failing_head_sha.trim().toLowerCase() : "";
|
|
425
|
+
const failingHeadSha = CI_FIX_HEAD_SHA_RE.test(candidateAnchor) ? candidateAnchor : undefined;
|
|
426
|
+
// BAPI-1514: a refreshed payload re-reads CI at the bound head and says so with
|
|
427
|
+
// `evidence_head_sha === head_sha`; its checks, snapshot, and excerpts are then
|
|
428
|
+
// bound to that head, not to the original failing anchor. A legacy payload
|
|
429
|
+
// (no or mismatched `evidence_head_sha`) keeps the anchor-bound evidence.
|
|
430
|
+
const candidateEvidence = typeof raw.evidence_head_sha === "string"
|
|
431
|
+
? raw.evidence_head_sha.trim().toLowerCase() : "";
|
|
432
|
+
const evidenceHeadSha = /^[0-9a-f]{40}$/.test(candidateEvidence) && candidateEvidence === headSha
|
|
433
|
+
? candidateEvidence : undefined;
|
|
434
|
+
const evidenceHead = evidenceHeadSha ?? failingHeadSha ?? headSha;
|
|
435
|
+
const logExcerpts = readLogExcerpts(raw.log_excerpts, evidenceHead, seen);
|
|
395
436
|
if (logExcerpts === null) {
|
|
396
437
|
throw ciFixContextError(job, "log_excerpts_invalid");
|
|
397
438
|
}
|
|
398
439
|
return {
|
|
399
440
|
headSha,
|
|
441
|
+
...(failingHeadSha ? { failingHeadSha } : {}),
|
|
442
|
+
...(evidenceHeadSha ? { evidenceHeadSha } : {}),
|
|
400
443
|
repoName,
|
|
401
444
|
expectedBranch,
|
|
402
445
|
failingChecks: failing,
|
|
403
446
|
missingChecks: missing,
|
|
404
447
|
requiredClaimChecks: required,
|
|
405
448
|
logExcerpts,
|
|
406
|
-
evidence: readCiEvidence(raw.evidence_snapshot,
|
|
449
|
+
evidence: readCiEvidence(raw.evidence_snapshot, evidenceHead, new Set(failing)),
|
|
407
450
|
};
|
|
408
451
|
}
|
|
409
452
|
function formatCheckList(names, none) {
|
|
@@ -679,5 +722,7 @@ export async function renderPromptSpecPrompt(job, context) {
|
|
|
679
722
|
const userPrompt = buildPromptSpecUserPrompt(spec, values);
|
|
680
723
|
return `${systemPrompt}\n\n${userPrompt}` +
|
|
681
724
|
(!context.deferComparisonGuidance && ["implement", "resume", "ci_fix"].includes(job.job_type)
|
|
682
|
-
? "\n\n" + renderWorkerVerificationGuidance(context.comparisonSha) : "")
|
|
725
|
+
? "\n\n" + renderWorkerVerificationGuidance(context.comparisonSha) : "") +
|
|
726
|
+
(!context.deferComparisonGuidance && job.job_type === "ci_fix"
|
|
727
|
+
? "\n\n" + renderWorkerTimeBudgetGuidance(context.timeoutSeconds) : "");
|
|
683
728
|
}
|
|
@@ -27,8 +27,10 @@
|
|
|
27
27
|
* rule 3).
|
|
28
28
|
*/
|
|
29
29
|
import { ExecutorNamedError, FocusedRebaseContext } from "./job-errors.js";
|
|
30
|
+
import { readRequiredVerification } from "./completion-supervision.js";
|
|
30
31
|
import { allRequiredChecksGreen, anyRequiredCheckFailed, ProviderChecksUnobservable, } from "./provider-checks.js";
|
|
31
32
|
import { FOCUSED_REBASE_CAPABILITY, FOCUSED_REBASE_CONTEXT_VERSION, } from "./types.js";
|
|
33
|
+
import { deliveryRenewedWithRemoteCiDeferrals } from "./delivery-outcome.js";
|
|
32
34
|
/** The job type the focused context is valid on. */
|
|
33
35
|
export const FOCUSED_REBASE_JOB_TYPE = "rebase";
|
|
34
36
|
/** The payload key the server writes the context under. */
|
|
@@ -120,6 +122,12 @@ export function validateFocusedRebaseContext(job) {
|
|
|
120
122
|
const required = Array.isArray(job.required_capabilities) ? job.required_capabilities : [];
|
|
121
123
|
if (!required.includes(FOCUSED_REBASE_CAPABILITY))
|
|
122
124
|
return fail("capability_not_required");
|
|
125
|
+
if (required.includes("required_verification.ci_deferral.v2")) {
|
|
126
|
+
const declaration = readRequiredVerification(payload);
|
|
127
|
+
if (ctx.verification_policy_version !== 2 || !["renewal", "conflict"].includes(String(ctx.mode)) ||
|
|
128
|
+
declaration.kind !== "ok" || declaration.declaration.policyVersion !== 2)
|
|
129
|
+
return fail("version_unsupported");
|
|
130
|
+
}
|
|
123
131
|
const prompt = payload.prompt;
|
|
124
132
|
if (typeof prompt !== "string" || prompt.trim().length === 0)
|
|
125
133
|
return fail("prompt_missing");
|
|
@@ -233,6 +241,11 @@ function boundedRace(work, ms, signal) {
|
|
|
233
241
|
* explicitly empty server-derived check set is satisfied vacuously, but only once
|
|
234
242
|
* every pull-request condition holds.
|
|
235
243
|
*
|
|
244
|
+
* BAPI-1473: while checks are pending with none failed, `readDelivery` (when
|
|
245
|
+
* given) is consulted; a server decision of `awaiting_post_pr` at exactly the
|
|
246
|
+
* pushed head with only `remote_ci` deferrals also ends `ready` (`ciDeferred`).
|
|
247
|
+
* Missing renewal, mixed dependencies, or an unavailable read stay pending.
|
|
248
|
+
*
|
|
236
249
|
* Proven failures stop at once: a required check concluded red, the pull request
|
|
237
250
|
* closed or reads `CONFLICTING`, or its number, head, head branch, or base moved.
|
|
238
251
|
* Running or missing checks, `UNKNOWN` mergeability, and unavailable reads stay
|
|
@@ -297,6 +310,19 @@ export async function waitForFocusedRebase(input) {
|
|
|
297
310
|
return { kind: "stopped", reason: "required_check_failed" };
|
|
298
311
|
if (allRequiredChecksGreen(poll, ctx.requiredChecks))
|
|
299
312
|
return { kind: "ready", ciVacuous: false };
|
|
313
|
+
// BAPI-1473: checks are pending and none failed. A rebase that renewed its
|
|
314
|
+
// evidence and deferred CI-only steps may end here, once the server confirms
|
|
315
|
+
// it at exactly the pushed head; the server's completion contract re-checks.
|
|
316
|
+
if (input.readDelivery && budget() > 0) {
|
|
317
|
+
const delivery = await boundedRace(input.readDelivery(), Math.min(readMax, budget()), input.signal);
|
|
318
|
+
if (delivery === "aborted")
|
|
319
|
+
return "aborted";
|
|
320
|
+
if (delivery !== "timeout" &&
|
|
321
|
+
!(delivery instanceof ProviderChecksUnobservable) &&
|
|
322
|
+
deliveryRenewedWithRemoteCiDeferrals(delivery, pushed)) {
|
|
323
|
+
return { kind: "ready", ciVacuous: false, ciDeferred: true };
|
|
324
|
+
}
|
|
325
|
+
}
|
|
300
326
|
return { kind: "pending", reason: "checks_pending" };
|
|
301
327
|
};
|
|
302
328
|
const unverifiedReason = () => {
|
|
@@ -321,10 +347,19 @@ export async function waitForFocusedRebase(input) {
|
|
|
321
347
|
if (reading === "timeout")
|
|
322
348
|
continue;
|
|
323
349
|
if (reading.kind === "ready") {
|
|
350
|
+
const checksText = reading.ciDeferred
|
|
351
|
+
? "required checks pending; renewed evidence with CI deferrals"
|
|
352
|
+
: reading.ciVacuous
|
|
353
|
+
? "no required checks are configured (vacuous CI)"
|
|
354
|
+
: "every required check is green";
|
|
324
355
|
input.errorLog(`[executor] Ready: focused rebase evidence current — pushed work at the pull request head is mergeable ` +
|
|
325
|
-
`and ${
|
|
326
|
-
|
|
327
|
-
|
|
356
|
+
`and ${checksText}; submitting completion once; ${ids}; elapsed=${secs()}`);
|
|
357
|
+
return {
|
|
358
|
+
kind: "ready",
|
|
359
|
+
ciVacuous: reading.ciVacuous,
|
|
360
|
+
...(reading.ciDeferred ? { ciDeferred: true } : {}),
|
|
361
|
+
elapsedMs: elapsed(),
|
|
362
|
+
};
|
|
328
363
|
}
|
|
329
364
|
if (reading.kind === "stopped") {
|
|
330
365
|
input.errorLog(`[executor] Stopped: ${reading.reason} — pushed work preserved, no further repair launched; ${ids}; elapsed=${secs()}`);
|