@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
|
@@ -69,6 +69,14 @@
|
|
|
69
69
|
* Both keep the `clean_exit` classification the process earned. No-expected-branch
|
|
70
70
|
* and absent-on-origin are still one cause, and may be refined later.
|
|
71
71
|
*
|
|
72
|
+
* BAPI-1426: a managed ticket may stop on purpose with pushed work and no pull
|
|
73
|
+
* request, because the server holds its delivery (blocked work, a failed
|
|
74
|
+
* required check, missing or stale evidence). When the caller passes a fresh,
|
|
75
|
+
* server-confirmed outcome whose preserved head equals both the worker HEAD and
|
|
76
|
+
* the origin tip, that exit is reported as an intentional hold under a
|
|
77
|
+
* completion-cause error kind — still a failure, never a success, and never a
|
|
78
|
+
* missing pull request. Without that confirmation every check below is unchanged.
|
|
79
|
+
*
|
|
72
80
|
* This check is deliberately fail-CLOSED, unlike the BAPI-586 base check it now
|
|
73
81
|
* shares a lookup with. "The PR could not be verified" and "there is no PR" are
|
|
74
82
|
* the same thing from the contract's point of view: neither is evidence that the
|
|
@@ -78,6 +86,7 @@
|
|
|
78
86
|
*/
|
|
79
87
|
import { ReconcileOverlapPushUnfinalized, WorkerFinalizationMissingRemoteBranchAndPr, WorkerFinalizationOriginUnverifiable, WorkerFinalizationPrBaseMismatch, WorkerFinalizationPrNotAttached, WorkerFinalizationPublicationUnverified, WorkerFinalizationSavedButUnfinalized, } from "./job-errors.js";
|
|
80
88
|
import { isImplementationStyleJobType } from "./job-types.js";
|
|
89
|
+
import { COMPLETION_CAUSE_ERROR_KINDS } from "./completion-supervision.js";
|
|
81
90
|
/** A full, lower-case 40-hex commit SHA. */
|
|
82
91
|
const FULL_SHA_RE = /^[0-9a-f]{40}$/;
|
|
83
92
|
/** The `runCommand` options for one bounded read; `{ cwd }` alone when unbounded. */
|
|
@@ -492,6 +501,82 @@ function reconcilePushUnfinalizedFailure(job, branch, originSha, pushedSha) {
|
|
|
492
501
|
last_commit_sha: originSha,
|
|
493
502
|
};
|
|
494
503
|
}
|
|
504
|
+
/**
|
|
505
|
+
* BAPI-1426: server outcomes that hold a bound attempt whatever the pull-request
|
|
506
|
+
* state. `awaiting_post_pr` is handled separately: it holds only while no pull
|
|
507
|
+
* request is attached, because its remaining work needs one.
|
|
508
|
+
*/
|
|
509
|
+
const DELIVERY_HOLD_OUTCOMES = new Set([
|
|
510
|
+
"blocked",
|
|
511
|
+
"verification_failed",
|
|
512
|
+
"evidence_missing_or_stale",
|
|
513
|
+
]);
|
|
514
|
+
const MAX_HOLD_BLOCKERS_IN_MESSAGE = 8;
|
|
515
|
+
/**
|
|
516
|
+
* BAPI-1426: the hold the server confirmed for THIS head, or null. Every
|
|
517
|
+
* condition must hold: a bound, available outcome; a full preserved head; a full
|
|
518
|
+
* worker HEAD equal to it; and an origin tip equal to both. Anything less is not
|
|
519
|
+
* server confirmation, and the caller falls through to its existing checks.
|
|
520
|
+
*/
|
|
521
|
+
function confirmedDeliveryHold(delivery, workerHeadSha, originSha) {
|
|
522
|
+
if (!delivery || delivery.bound !== true || delivery.unavailable === true)
|
|
523
|
+
return null;
|
|
524
|
+
if (typeof delivery.attemptId !== "string" || !delivery.attemptId)
|
|
525
|
+
return null;
|
|
526
|
+
if (typeof delivery.outcome !== "string")
|
|
527
|
+
return null;
|
|
528
|
+
const preserved = typeof delivery.preservedHeadSha === "string" ? delivery.preservedHeadSha.trim().toLowerCase() : "";
|
|
529
|
+
if (!FULL_SHA_RE.test(preserved) || !FULL_SHA_RE.test(workerHeadSha))
|
|
530
|
+
return null;
|
|
531
|
+
if (preserved !== workerHeadSha || originSha !== workerHeadSha)
|
|
532
|
+
return null;
|
|
533
|
+
return {
|
|
534
|
+
attempt_id: delivery.attemptId,
|
|
535
|
+
outcome: delivery.outcome,
|
|
536
|
+
preserved_head_sha: preserved,
|
|
537
|
+
blockers: (delivery.blockers ?? []).map((b) => typeof b.step_id === "number" ? { kind: b.kind, step_id: b.step_id } : { kind: b.kind }),
|
|
538
|
+
};
|
|
539
|
+
}
|
|
540
|
+
/**
|
|
541
|
+
* BAPI-1426: the intentional checkpoint a managed ticket ends with when the
|
|
542
|
+
* server holds its delivery. This is a `/fail`, never a success, and it never
|
|
543
|
+
* claims a pull request is missing. A failed required check is
|
|
544
|
+
* `RequiredCheckFailed`; every other hold is `RequiredWorkIncomplete`. A lookup
|
|
545
|
+
* or provider outage never reaches here, so it is never named a failed check.
|
|
546
|
+
*
|
|
547
|
+
* The message carries only public identifiers, the closed outcome token, a
|
|
548
|
+
* truncated head, and blocker kinds with step ids — never evidence text.
|
|
549
|
+
*/
|
|
550
|
+
function deliveryHoldFailure(job, branch, hold) {
|
|
551
|
+
const label = job.ticket_key ? `${job.ticket_key} (job ${job.id})` : `job ${job.id}`;
|
|
552
|
+
const shown = hold.blockers.slice(0, MAX_HOLD_BLOCKERS_IN_MESSAGE);
|
|
553
|
+
const blockerText = shown.length
|
|
554
|
+
? shown.map((b) => (typeof b.step_id === "number" ? `${b.kind}(step ${b.step_id})` : b.kind)).join(", ") +
|
|
555
|
+
(hold.blockers.length > shown.length ? `, +${hold.blockers.length - shown.length} more` : "")
|
|
556
|
+
: "none reported";
|
|
557
|
+
const telemetry = { delivery_hold: hold };
|
|
558
|
+
return {
|
|
559
|
+
error_kind: hold.outcome === "verification_failed"
|
|
560
|
+
? COMPLETION_CAUSE_ERROR_KINDS.required_check_failed
|
|
561
|
+
: COMPLETION_CAUSE_ERROR_KINDS.required_work_incomplete,
|
|
562
|
+
error_message: `${label}: delivery attempt ${hold.attempt_id} is held by the server at outcome ` +
|
|
563
|
+
`'${hold.outcome}'. Head ${hold.preserved_head_sha.slice(0, 12)} is durable on origin branch ` +
|
|
564
|
+
`'${branch}' and matches the head the server preserved. Remaining blockers: ${blockerText}. ` +
|
|
565
|
+
`This is an intentional checkpoint: the ticket is not delivered and no pull request was ` +
|
|
566
|
+
`required. Recovery: resolve the blockers and resume from the preserved head.`,
|
|
567
|
+
classification: "clean_exit",
|
|
568
|
+
last_commit_sha: hold.preserved_head_sha,
|
|
569
|
+
telemetry,
|
|
570
|
+
};
|
|
571
|
+
}
|
|
572
|
+
/**
|
|
573
|
+
* BAPI-1426: the delivery hold a finalization failure carries, or null. Read
|
|
574
|
+
* from the failure's own structured marker, never from its message.
|
|
575
|
+
*/
|
|
576
|
+
export function deliveryHoldOf(failure) {
|
|
577
|
+
const marker = failure.telemetry?.delivery_hold;
|
|
578
|
+
return marker && typeof marker === "object" && typeof marker.attempt_id === "string" ? marker : null;
|
|
579
|
+
}
|
|
495
580
|
/**
|
|
496
581
|
* Validate that an implementation-style job's clean exit actually published its
|
|
497
582
|
* work to origin AND left an attached pull request. Non-implementation-style jobs
|
|
@@ -656,12 +741,33 @@ export async function validateWorkerFinalization(input) {
|
|
|
656
741
|
if (refreshedBaseVerdict)
|
|
657
742
|
return { ok: false, failure: refreshedBaseVerdict };
|
|
658
743
|
}
|
|
744
|
+
// BAPI-1426: the work is provably on origin at the worker's HEAD. When the
|
|
745
|
+
// server confirms it holds this bound attempt at exactly that head, the exit is
|
|
746
|
+
// an intentional checkpoint — reported as a hold whether or not a pull request
|
|
747
|
+
// is attached, and before the attachment contract can call it missing.
|
|
748
|
+
// BAPI-1516: a `ci_fix` is never a delivery hold, even when an outcome reaches
|
|
749
|
+
// this seam directly; its success rests on the attachment contract below.
|
|
750
|
+
const hold = job.job_type === "ci_fix"
|
|
751
|
+
? undefined
|
|
752
|
+
: confirmedDeliveryHold(input.deliveryOutcome, trimmedHeadSha, remoteSha);
|
|
753
|
+
if (hold && DELIVERY_HOLD_OUTCOMES.has(hold.outcome)) {
|
|
754
|
+
return { ok: false, failure: deliveryHoldFailure(job, trimmedBranch, hold) };
|
|
755
|
+
}
|
|
659
756
|
let contract = evaluatePrContract(pr, trimmedHeadSha, covered);
|
|
660
757
|
for (let attempt = 1; attempt < attempts && !contract.ok && contract.retryable; attempt++) {
|
|
661
758
|
await sleep(retryDelayMs);
|
|
662
759
|
pr = await resolvePrForFinalization(runCommand, worktreePath, trimmedBranch, bound());
|
|
663
760
|
contract = evaluatePrContract(pr, trimmedHeadSha, covered);
|
|
664
761
|
}
|
|
762
|
+
// BAPI-1426: post-PR work needs a pull request. While neither the provider nor
|
|
763
|
+
// the server shows one, a confirmed `awaiting_post_pr` attempt is a hold too.
|
|
764
|
+
if (!contract.ok &&
|
|
765
|
+
contract.unverified !== true &&
|
|
766
|
+
!pr.found &&
|
|
767
|
+
hold?.outcome === "awaiting_post_pr" &&
|
|
768
|
+
input.deliveryOutcome?.prNumber == null) {
|
|
769
|
+
return { ok: false, failure: deliveryHoldFailure(job, trimmedBranch, hold) };
|
|
770
|
+
}
|
|
665
771
|
if (!contract.ok && contract.unverified === true) {
|
|
666
772
|
return {
|
|
667
773
|
ok: false,
|
|
@@ -105,6 +105,18 @@ export function renderWorkerGuardReason(category, family, form) {
|
|
|
105
105
|
"The correction context for this worker is not valid, so no tool call can be " +
|
|
106
106
|
"checked. Stop and report the launch problem.");
|
|
107
107
|
}
|
|
108
|
+
if (category === "operator-verb" && family === "regenerate-plan") {
|
|
109
|
+
return (`${WORKER_GUARD_REASON_PREFIX}: denied ${family} (${category}). ` +
|
|
110
|
+
"Choosing the route a failed plan is regenerated on is the run driver's decision, " +
|
|
111
|
+
"made outside the worker. Do not run regenerate-plan or call its route; record the " +
|
|
112
|
+
"step escalated with its reason and continue.");
|
|
113
|
+
}
|
|
114
|
+
if (category === "operator-verb") {
|
|
115
|
+
return (`${WORKER_GUARD_REASON_PREFIX}: denied ${family} (${category}). ` +
|
|
116
|
+
"Resolving a verification hand-off is the run owner's decision, made outside the " +
|
|
117
|
+
"worker. Do not run resolve-handoff or call its route; record the step escalated " +
|
|
118
|
+
"with its hand-off and continue.");
|
|
119
|
+
}
|
|
108
120
|
if (category === "correction-payload-invalid") {
|
|
109
121
|
return (`${WORKER_GUARD_REASON_PREFIX}: denied ${family} (${category}). ` +
|
|
110
122
|
"The tool call could not be read as a tool name with a tool input, so it cannot " +
|
|
@@ -994,6 +1006,192 @@ function isClaudeWorkflowPrintLaunch(segment) {
|
|
|
994
1006
|
return WORKFLOW_SLASH_LAUNCH.test(value);
|
|
995
1007
|
});
|
|
996
1008
|
}
|
|
1009
|
+
// ---------------------------------------------------------------------------
|
|
1010
|
+
// BAPI-1494 — operator-only verbs
|
|
1011
|
+
// ---------------------------------------------------------------------------
|
|
1012
|
+
/**
|
|
1013
|
+
* The operator-only subcommands a worker may never run, and their route segments.
|
|
1014
|
+
* BAPI-1532 adds `regenerate-plan` beside `resolve-handoff`, with its own family so
|
|
1015
|
+
* the denial names the verb that was attempted.
|
|
1016
|
+
*/
|
|
1017
|
+
const OPERATOR_ONLY_VERBS = ["resolve-handoff", "regenerate-plan"];
|
|
1018
|
+
/** The packaged MCP-server package token after `npx` (any version suffix). */
|
|
1019
|
+
const BRIDGE_PACKAGE_TOKEN = /^@bridge_gpt\/mcp-server(?:@\S+)?$/;
|
|
1020
|
+
/** Package runners whose package argument launches the packaged CLI. */
|
|
1021
|
+
const PACKAGE_RUNNERS = new Set(["npx", "pnpx", "bunx"]);
|
|
1022
|
+
/** Bins of the packaged MCP server that take `epic-implementer <verb>`. */
|
|
1023
|
+
const BRIDGE_SERVER_BINS = new Set(["mcp-server", "bridge-api-mcp-server"]);
|
|
1024
|
+
/**
|
|
1025
|
+
* Text tools that cannot send an HTTP request. A route segment in their
|
|
1026
|
+
* arguments (a grep pattern, a commit message, a sed edit) is text, not a call.
|
|
1027
|
+
* Every other executable naming the route segment is denied — whatever the HTTP
|
|
1028
|
+
* client, including `python -c` and `node -e` source text.
|
|
1029
|
+
*/
|
|
1030
|
+
const ROUTE_TEXT_ONLY_EXECUTABLES = new Set([
|
|
1031
|
+
"grep", "egrep", "fgrep", "rg", "ag", "cat", "echo", "printf", "head", "tail", "wc",
|
|
1032
|
+
"diff", "less", "more", "ls", "sort", "uniq", "tr", "cut", "jq",
|
|
1033
|
+
]);
|
|
1034
|
+
/**
|
|
1035
|
+
* `git` and `find` are text-only too, except in the spellings that run another
|
|
1036
|
+
* command: `git -c alias.<x>=!<cmd>` (a shell alias) and `find -exec|-execdir|-ok|-okdir`.
|
|
1037
|
+
* `sed` (GNU `e`) and `awk` (`system()`) can always run commands, so they are not
|
|
1038
|
+
* text-only at all.
|
|
1039
|
+
*/
|
|
1040
|
+
function isTextOnlyInvocation(argv) {
|
|
1041
|
+
const executable = path.basename(argv[0] ?? "");
|
|
1042
|
+
if (ROUTE_TEXT_ONLY_EXECUTABLES.has(executable))
|
|
1043
|
+
return true;
|
|
1044
|
+
if (executable === "git")
|
|
1045
|
+
return !argv.some((token) => /alias\.[^=]*=\s*!/i.test(token));
|
|
1046
|
+
if (executable === "find")
|
|
1047
|
+
return !argv.some((token) => ["-exec", "-execdir", "-ok", "-okdir"].includes(token));
|
|
1048
|
+
return false;
|
|
1049
|
+
}
|
|
1050
|
+
/** Which operator-only route segment this token names, in any case or percent-encoding. */
|
|
1051
|
+
function namedOperatorOnlyRoute(token) {
|
|
1052
|
+
let decoded = token;
|
|
1053
|
+
for (let i = 0; i < 3; i += 1) {
|
|
1054
|
+
let next;
|
|
1055
|
+
try {
|
|
1056
|
+
next = decodeURIComponent(decoded);
|
|
1057
|
+
}
|
|
1058
|
+
catch {
|
|
1059
|
+
break;
|
|
1060
|
+
}
|
|
1061
|
+
if (next === decoded)
|
|
1062
|
+
break;
|
|
1063
|
+
decoded = next;
|
|
1064
|
+
}
|
|
1065
|
+
return OPERATOR_ONLY_VERBS.find((verb) => decoded.toLowerCase().includes(`/${verb}`) || token.toLowerCase().includes(`/${verb}`));
|
|
1066
|
+
}
|
|
1067
|
+
/** Node options whose next token is a value, not the script. */
|
|
1068
|
+
const NODE_VALUE_OPTIONS = new Set([
|
|
1069
|
+
"-r", "--require", "--import", "--loader", "--experimental-loader", "-C", "--conditions",
|
|
1070
|
+
"--env-file", "--input-type", "--title", "--stack-size", "--max-old-space-size",
|
|
1071
|
+
]);
|
|
1072
|
+
/** npm subcommands that run a package bin like `npx`. */
|
|
1073
|
+
const NPM_EXEC_SUBCOMMANDS = new Set(["exec", "x"]);
|
|
1074
|
+
/** The package names a runner's argv requests through `-p`/`--package` or a bare operand. */
|
|
1075
|
+
function runnerPackageIndex(argv, start) {
|
|
1076
|
+
for (let i = start; i < argv.length; i += 1) {
|
|
1077
|
+
const token = argv[i];
|
|
1078
|
+
if (BRIDGE_PACKAGE_TOKEN.test(token))
|
|
1079
|
+
return { index: i, inline: false };
|
|
1080
|
+
const inline = /^(?:--package|-p)=(.*)$/.exec(token);
|
|
1081
|
+
if (inline && BRIDGE_PACKAGE_TOKEN.test(inline[1]))
|
|
1082
|
+
return { index: i, inline: true };
|
|
1083
|
+
}
|
|
1084
|
+
return undefined;
|
|
1085
|
+
}
|
|
1086
|
+
/** The first argument that is not an option, or `undefined`. */
|
|
1087
|
+
function firstOperand(args) {
|
|
1088
|
+
return args.find((token) => !token.startsWith("-"));
|
|
1089
|
+
}
|
|
1090
|
+
/**
|
|
1091
|
+
* The arguments the packaged `epic-implementer` CLI receives, or `undefined`
|
|
1092
|
+
* when this argv does not launch it. Covers the `epic-implementer` bin,
|
|
1093
|
+
* `npx [-y] [-p] @bridge_gpt/mcp-server[@<version>] epic-implementer`, the
|
|
1094
|
+
* `mcp-server` / `bridge-api-mcp-server` bins with `epic-implementer`, and
|
|
1095
|
+
* `node <path>/build/index.js epic-implementer` or
|
|
1096
|
+
* `node <path>/build/epic-implementer-bin.js`. An empty array means the CLI is
|
|
1097
|
+
* launched with no visible verb (its verb may arrive from stdin).
|
|
1098
|
+
*/
|
|
1099
|
+
function epicImplementerArgs(argv) {
|
|
1100
|
+
const base = path.basename(argv[0] ?? "");
|
|
1101
|
+
const afterEpicImplementer = (rest) => {
|
|
1102
|
+
if (rest.length === 0)
|
|
1103
|
+
return [];
|
|
1104
|
+
return rest[0] === "epic-implementer" ? rest.slice(1) : undefined;
|
|
1105
|
+
};
|
|
1106
|
+
if (base === "epic-implementer")
|
|
1107
|
+
return argv.slice(1);
|
|
1108
|
+
if (BRIDGE_SERVER_BINS.has(base))
|
|
1109
|
+
return afterEpicImplementer(argv.slice(1));
|
|
1110
|
+
const runnerStart = PACKAGE_RUNNERS.has(base)
|
|
1111
|
+
? 1
|
|
1112
|
+
: base === "npm" && NPM_EXEC_SUBCOMMANDS.has(argv[1] ?? "")
|
|
1113
|
+
? 2
|
|
1114
|
+
: -1;
|
|
1115
|
+
if (runnerStart !== -1) {
|
|
1116
|
+
// Options and `--` separators carry no verb; drop them from the remainder.
|
|
1117
|
+
const operands = (from) => argv.slice(from).filter((t) => t !== "--");
|
|
1118
|
+
const found = runnerPackageIndex(argv, runnerStart);
|
|
1119
|
+
if (found !== undefined) {
|
|
1120
|
+
// `npx [-p|--package[=]] <pkg> epic-implementer …` and `npx <pkg> epic-implementer …`.
|
|
1121
|
+
const rest = operands(found.index + 1);
|
|
1122
|
+
// Runner options may sit between the package and the command.
|
|
1123
|
+
const commandAt = rest.findIndex((t) => !t.startsWith("-"));
|
|
1124
|
+
return afterEpicImplementer(commandAt === -1 ? [] : rest.slice(commandAt));
|
|
1125
|
+
}
|
|
1126
|
+
// `npx epic-implementer …` — the package's bin, installed locally.
|
|
1127
|
+
const first = argv.slice(runnerStart).find((t) => !t.startsWith("-"));
|
|
1128
|
+
if (first === "epic-implementer") {
|
|
1129
|
+
return operands(argv.indexOf(first, runnerStart) + 1);
|
|
1130
|
+
}
|
|
1131
|
+
return undefined;
|
|
1132
|
+
}
|
|
1133
|
+
if (base === "node" || base === "nodejs") {
|
|
1134
|
+
let scriptIndex = -1;
|
|
1135
|
+
for (let i = 1; i < argv.length; i += 1) {
|
|
1136
|
+
const token = argv[i];
|
|
1137
|
+
if (NODE_VALUE_OPTIONS.has(token)) {
|
|
1138
|
+
i += 1;
|
|
1139
|
+
continue;
|
|
1140
|
+
}
|
|
1141
|
+
if (!token.startsWith("-")) {
|
|
1142
|
+
scriptIndex = i;
|
|
1143
|
+
break;
|
|
1144
|
+
}
|
|
1145
|
+
}
|
|
1146
|
+
if (scriptIndex === -1)
|
|
1147
|
+
return undefined;
|
|
1148
|
+
const script = argv[scriptIndex].replace(/\\/g, "/");
|
|
1149
|
+
const rest = argv.slice(scriptIndex + 1);
|
|
1150
|
+
if (path.posix.basename(script) === "epic-implementer-bin.js")
|
|
1151
|
+
return rest;
|
|
1152
|
+
if (/(?:^|\/)build\/index\.js$/.test(script))
|
|
1153
|
+
return afterEpicImplementer(rest);
|
|
1154
|
+
}
|
|
1155
|
+
return undefined;
|
|
1156
|
+
}
|
|
1157
|
+
/**
|
|
1158
|
+
* Deny a worker's `resolve-handoff` in any launcher spelling, or any direct call
|
|
1159
|
+
* naming its route. `null` when this simple command does neither.
|
|
1160
|
+
*
|
|
1161
|
+
* Runs on argv already unwrapped of transparent prefixes, and is reached again
|
|
1162
|
+
* inside the `sh -c`, `eval`, and `xargs` recursion, so wrapped spellings resolve
|
|
1163
|
+
* here too. Under `xargs` (stdin-fed arguments) a launcher whose verb is not
|
|
1164
|
+
* visible is denied: the verb may arrive from stdin.
|
|
1165
|
+
*/
|
|
1166
|
+
function operatorVerbDecision(segment, stdinFed) {
|
|
1167
|
+
const denyFor = (family) => ({
|
|
1168
|
+
kind: "deny",
|
|
1169
|
+
category: "operator-verb",
|
|
1170
|
+
family,
|
|
1171
|
+
});
|
|
1172
|
+
const argv = segment.argv;
|
|
1173
|
+
const cliArgs = epicImplementerArgs(argv);
|
|
1174
|
+
if (cliArgs !== undefined) {
|
|
1175
|
+
const verb = firstOperand(cliArgs);
|
|
1176
|
+
const named = OPERATOR_ONLY_VERBS.find((v) => verb === v || cliArgs.includes(v));
|
|
1177
|
+
if (named !== undefined)
|
|
1178
|
+
return denyFor(named);
|
|
1179
|
+
// The verb is not visible (stdin-fed or an unresolved expansion): the
|
|
1180
|
+
// original operator-only family is named, as before.
|
|
1181
|
+
if (verb === undefined && stdinFed)
|
|
1182
|
+
return denyFor("resolve-handoff");
|
|
1183
|
+
if (segment.unsafe && (verb === undefined || /[$`]/.test(verb)))
|
|
1184
|
+
return denyFor("resolve-handoff");
|
|
1185
|
+
}
|
|
1186
|
+
if (!isTextOnlyInvocation(argv)) {
|
|
1187
|
+
for (const token of argv) {
|
|
1188
|
+
const routeVerb = namedOperatorOnlyRoute(token);
|
|
1189
|
+
if (routeVerb !== undefined)
|
|
1190
|
+
return denyFor(routeVerb);
|
|
1191
|
+
}
|
|
1192
|
+
}
|
|
1193
|
+
return null;
|
|
1194
|
+
}
|
|
997
1195
|
/** Evaluate one already-tokenized simple command. */
|
|
998
1196
|
function evaluateSimpleCommand(segment, ctx, payloadCwd, depth = 0, stdinFed = false, diagnostic) {
|
|
999
1197
|
const argv = unwrapTransparentPrefixes(segment.argv);
|
|
@@ -1045,6 +1243,11 @@ function evaluateSimpleCommand(segment, ctx, payloadCwd, depth = 0, stdinFed = f
|
|
|
1045
1243
|
return evaluateSimpleCommand({ argv: rest, unsafe: segment.unsafe }, ctx, payloadCwd, depth + 1, true, diagnostic ? { argv: stripStdinPrefixOptions(unwrapTransparentPrefixes(diagnostic.argv)), complete: false } : undefined);
|
|
1046
1244
|
}
|
|
1047
1245
|
segment = { argv, unsafe: segment.unsafe };
|
|
1246
|
+
// BAPI-1494 — operator-only verbs, checked for every worker after every
|
|
1247
|
+
// wrapper has been resolved.
|
|
1248
|
+
const operatorVerb = operatorVerbDecision(segment, stdinFed);
|
|
1249
|
+
if (operatorVerb !== null)
|
|
1250
|
+
return operatorVerb;
|
|
1048
1251
|
if (ctx.correctionEnforced && isClaudeWorkflowPrintLaunch(segment)) {
|
|
1049
1252
|
return { kind: "deny", category: "workflow-launch-shell", family: "workflow" };
|
|
1050
1253
|
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-job worker resources the executor provisions before a supervised worker
|
|
3
|
+
* spawns (BAPI-1479): a disposable job database and an own-build
|
|
4
|
+
* `epic-implementer` launcher directory.
|
|
5
|
+
*
|
|
6
|
+
* This module is only the SEAM the job runner and the executor loop consume. The
|
|
7
|
+
* lifecycle itself lives in `worker-database.ts` and `worker-cli-launcher.ts`.
|
|
8
|
+
*
|
|
9
|
+
* The seam is OPT-IN on `ExecutorDeps.workerResources`: only the executor CLI
|
|
10
|
+
* (`cli.ts`) supplies it, for the real executor process. `createDefaultExecutorDeps`
|
|
11
|
+
* does not, because integration tests spread it. A deps object without it — every
|
|
12
|
+
* test fake, every embedded caller — provisions nothing, keeps the BAPI-1019 sentinel
|
|
13
|
+
* database, and leaves `PATH` as inherited. That is deliberate: a fake executor
|
|
14
|
+
* must never reach a real PostgreSQL server or write a launcher into a real state
|
|
15
|
+
* directory just because a test forgot to stub a seam.
|
|
16
|
+
*/
|
|
17
|
+
import os from "node:os";
|
|
18
|
+
import { resolveExecutorStateDir } from "./job-log-registry.js";
|
|
19
|
+
import { hostIdentityDigest } from "./process-containment.js";
|
|
20
|
+
import { createDefaultWorkerCliLauncherDeps, createWorkerCliLauncher, } from "./worker-cli-launcher.js";
|
|
21
|
+
import { createDefaultWorkerDatabaseDeps, dropWorkerDatabase, provisionWorkerDatabase, sweepOrphanedWorkerDatabases, } from "./worker-database.js";
|
|
22
|
+
/**
|
|
23
|
+
* The production worker resources, bound to this executor process: its pid, its
|
|
24
|
+
* host identity, its state directory, and the operator checkout it runs from
|
|
25
|
+
* (`cwd`) — whose `.venv` Alembic migrates each job database.
|
|
26
|
+
*/
|
|
27
|
+
export function createProductionWorkerResources(input) {
|
|
28
|
+
const stateDir = resolveExecutorStateDir({
|
|
29
|
+
env: input.env,
|
|
30
|
+
tmpdir: () => os.tmpdir(),
|
|
31
|
+
platform: input.platform,
|
|
32
|
+
});
|
|
33
|
+
const executorPid = input.executorPid ?? process.pid;
|
|
34
|
+
const hostDigest = hostIdentityDigest(input.hostname ?? os.hostname());
|
|
35
|
+
// Built lazily, once: constructing the deps touches nothing, but there is no
|
|
36
|
+
// reason to do even that for an executor that never claims a worker job.
|
|
37
|
+
let databaseDeps;
|
|
38
|
+
const dbDeps = () => (databaseDeps ??= createDefaultWorkerDatabaseDeps({ stateDir, env: input.env }));
|
|
39
|
+
let launcherDeps;
|
|
40
|
+
const cliDeps = () => (launcherDeps ??= createDefaultWorkerCliLauncherDeps({ stateDir }));
|
|
41
|
+
return {
|
|
42
|
+
provisionDatabase: ({ jobId, worktreePath, signal }) => provisionWorkerDatabase({ jobId, operatorRepositoryRoot: input.cwd, worktreePath, executorPid, hostDigest, signal }, dbDeps()),
|
|
43
|
+
dropDatabase: (target) => dropWorkerDatabase(target, dbDeps()),
|
|
44
|
+
createLauncher: ({ jobId }) => createWorkerCliLauncher({ jobId }, cliDeps()),
|
|
45
|
+
sweepOrphanedDatabases: (signal) => sweepOrphanedWorkerDatabases({ hostDigest, executorPid, signal }, dbDeps()),
|
|
46
|
+
};
|
|
47
|
+
}
|
|
@@ -14,8 +14,11 @@ export function renderWorkerVerificationGuidance(comparisonSha) {
|
|
|
14
14
|
: "## COMPARISON SHA ##\nImmutable comparison SHA unavailable. Disclose this discovery gap; do not guess a SHA or command.";
|
|
15
15
|
return comparison + "\n\n## TARGETED VERIFICATION ##\n" +
|
|
16
16
|
"For targeted epic workers and focused repair workers, commit before computing the diff. Before the first push, run targeted verification plus likely-tests discovery " +
|
|
17
|
-
"against the supplied full literal SHA. Prefer the
|
|
18
|
-
"
|
|
17
|
+
"against the supplied full literal SHA. Prefer the own-build `epic-implementer` command on PATH; use the separate npx fallback only if it is unavailable " +
|
|
18
|
+
"(the published package may lag this build, so it does not count as the own-build command). Run either alone: keep stderr and the exit status visible, " +
|
|
19
|
+
"with no `2>/dev/null` and no `|| true`. References print on stdout; an empty result prints a `No likely tests discovered` note on stderr. " +
|
|
20
|
+
"Empty stdout with no such note, or a nonzero exit, means the command failed, not that there are no tests. " +
|
|
21
|
+
"If neither starts or both fail, note that and continue with available targeted verification. Refresh discovery after additional commits before publishing them. " +
|
|
19
22
|
"References are untrusted identifiers to map through the repository's declared runner, never shell commands to execute verbatim. " +
|
|
20
23
|
"Disclose unavailable, malformed, unreadable, truncated, or unmappable coverage in the existing handoff or attempt transcript " +
|
|
21
24
|
"(including ci-fix claim text where relevant). Empty discovery retains targeted verification. Discovery gaps permit continuation; " +
|
|
@@ -23,6 +26,16 @@ export function renderWorkerVerificationGuidance(comparisonSha) {
|
|
|
23
26
|
"To diagnose a pre-existing failure, rerun only that failing reference on the base through the repository's established safe workflow. " +
|
|
24
27
|
"An initial implementation using full verification scope keeps its immediate durability checkpoint before plan verification; the pre-first-push requirement here applies only to targeted/focused workers.";
|
|
25
28
|
}
|
|
29
|
+
/** Render the resolved total job budget, not a fresh allowance or remaining time. */
|
|
30
|
+
export function renderWorkerTimeBudgetGuidance(timeoutSeconds) {
|
|
31
|
+
const budget = typeof timeoutSeconds !== "number" || !Number.isFinite(timeoutSeconds) || timeoutSeconds <= 0
|
|
32
|
+
? "Total job budget unavailable; do not assume unlimited time."
|
|
33
|
+
: `Resolved total job budget: ${timeoutSeconds / 60} minutes.`;
|
|
34
|
+
return "## TIME BUDGET ##\n" + budget + " " +
|
|
35
|
+
"Preparation and executor work consume this same total budget; this is not a fresh worker allowance or measured remaining time. " +
|
|
36
|
+
"Local verification must leave room for commit, push, the single renewal pass, and `.conductor/result.json`. " +
|
|
37
|
+
"Do not start a verification command that cannot finish within the remaining budget.";
|
|
38
|
+
}
|
|
26
39
|
export function renderWorkerPushAndCommitGuidance(rebase = false) {
|
|
27
40
|
return "\n\n## PUSH AND COMMIT PROCEDURE ##\n" +
|
|
28
41
|
"1. Confirm git ls-files -- .bridge-commit-msg prints nothing and the message file is absent or an ordinary untracked file, never a symlink. " +
|