@sema-agent/server 7.43.0 → 7.44.0
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/USAGE.md +18 -0
- package/dist/adoption/plan.js +31 -192
- package/dist/adoption/quiesce.js +3 -86
- package/dist/adoption/runner.js +3 -140
- package/dist/adoption/sql.js +0 -74
- package/dist/adoption/wire.js +0 -80
- package/dist/approval-ask-machine.js +0 -75
- package/dist/approval-card.js +0 -323
- package/dist/approval-deny-reasons.js +0 -42
- package/dist/approval-hmac.js +0 -35
- package/dist/approval-reconciler.js +8 -166
- package/dist/approval.js +4 -67
- package/dist/audit.js +1 -44
- package/dist/auth-bridge.js +4 -47
- package/dist/auth-keys.js +0 -23
- package/dist/bake-runner/main.js +4 -65
- package/dist/bake-runner/protocol.js +3 -58
- package/dist/bake-runner/runner.js +5 -91
- package/dist/bench/l8/artifact.js +2 -61
- package/dist/bench/l8/escape.js +0 -25
- package/dist/bench/l8/index.js +0 -14
- package/dist/bench/l8/probes.js +0 -60
- package/dist/bench/l8/run-probes.js +2 -45
- package/dist/bench/s1/arms.js +14 -189
- package/dist/bench/s1/live-deps.js +11 -253
- package/dist/bench/s1/oracle.js +1 -36
- package/dist/bench/s1/repair-oracle-adapter.js +0 -30
- package/dist/bench/s1/reviewer.js +0 -34
- package/dist/bench/s1/row.js +0 -62
- package/dist/bench/s1/run-firm.js +4 -70
- package/dist/bench/s1/runner-ctx.js +0 -40
- package/dist/bench/s1/tasks.js +0 -115
- package/dist/boot/adoption.js +0 -20
- package/dist/boot/budget-tracing.js +3 -55
- package/dist/boot/config-center.js +43 -551
- package/dist/boot/coordinators.js +1 -69
- package/dist/boot/crash-last.js +0 -18
- package/dist/boot/deferred-sandbox-path-env.js +3 -119
- package/dist/boot/execution-env.js +5 -171
- package/dist/boot/governance-seams.js +0 -92
- package/dist/boot/leader.js +0 -69
- package/dist/boot/limit-sync.js +0 -2
- package/dist/boot/memory-boundary.js +3 -91
- package/dist/boot/org-memory.js +1 -25
- package/dist/boot/parked-revive-gate.js +0 -150
- package/dist/boot/permission-rules-audit.js +1 -56
- package/dist/boot/reapers.js +15 -273
- package/dist/boot/resolve-spec.js +9 -768
- package/dist/boot/retention-lane.js +4 -87
- package/dist/boot/runner-deps.js +3 -297
- package/dist/boot/runtime-caps.js +0 -40
- package/dist/boot/session-faces.js +2 -133
- package/dist/boot/shutdown.js +9 -83
- package/dist/boot/side-query-lane.js +2 -137
- package/dist/boot/stores.js +15 -215
- package/dist/boot/task-list-lane.js +0 -18
- package/dist/boot/webfetch-summarize-lane.js +0 -47
- package/dist/boot/workflow-orchestration.js +3 -99
- package/dist/boot-reclaim.js +0 -20
- package/dist/bounded-session-map.js +0 -19
- package/dist/brain.js +2 -139
- package/dist/budget.js +9 -244
- package/dist/capabilities/builtin-tools.js +0 -3
- package/dist/capabilities/center-plugins.js +6 -51
- package/dist/capabilities/center-prompts.js +6 -71
- package/dist/capabilities/code-review-council.js +7 -40
- package/dist/capabilities/collab-workflows.js +1 -44
- package/dist/capabilities/hands-lane.js +0 -65
- package/dist/capabilities/memory-notice.js +0 -70
- package/dist/capabilities/prompt.js +0 -14
- package/dist/capabilities/prompts/code-review.js +0 -14
- package/dist/capabilities/prompts/identity.js +0 -6
- package/dist/capabilities/prompts/team.js +0 -4
- package/dist/capabilities/repo-tools.js +0 -34
- package/dist/capabilities/sandbox-file-send.js +3 -92
- package/dist/capabilities/scenarios.d.ts +0 -1
- package/dist/capabilities/scenarios.js +18 -279
- package/dist/capabilities/select-environment-tool.js +0 -32
- package/dist/capabilities/send-user-file-tool.js +2 -66
- package/dist/capabilities/skills.d.ts +1 -2
- package/dist/capabilities/skills.js +3 -21
- package/dist/capabilities/team.d.ts +3 -11
- package/dist/capabilities/team.js +3 -51
- package/dist/capabilities/tool-defer.js +0 -3
- package/dist/config-center/apply-effective.js +15 -330
- package/dist/config-center/apply-ledger.js +5 -38
- package/dist/config-center/facade.js +0 -41
- package/dist/config-center/hot-keys-registry.js +0 -20
- package/dist/config-center/http-client.js +2 -125
- package/dist/config-center/mcp-revocation.js +2 -34
- package/dist/config-center/read-face.js +0 -59
- package/dist/config-center/restart-signal.js +1 -85
- package/dist/config-center/skills-mcp.d.ts +1 -1
- package/dist/config-center/skills-mcp.js +9 -76
- package/dist/config-center/stage-limits.js +8 -35
- package/dist/config-invariants.js +0 -16
- package/dist/config-lkg.js +0 -42
- package/dist/config-provider.js +3 -186
- package/dist/config-types.js +0 -5
- package/dist/config.js +107 -1145
- package/dist/degenerate-instrument.js +3 -67
- package/dist/deployment-governance.js +0 -124
- package/dist/digest-form.js +0 -12
- package/dist/elicitation.js +3 -86
- package/dist/env-facts.js +7 -75
- package/dist/fleet/fleet-bus.js +34 -507
- package/dist/fleet/fleet-reconciler.js +9 -149
- package/dist/fleet/fleet-terminal-window.js +10 -178
- package/dist/fleet/subagent-tail-bus.js +3 -72
- package/dist/fleet-client.js +10 -70
- package/dist/fleet-lease.js +5 -79
- package/dist/git-api-kind.js +0 -3
- package/dist/governance-ask-marks.js +2 -78
- package/dist/hooks/branch-transcript.js +0 -74
- package/dist/hooks/cc-agent-hook-prompt.js +0 -29
- package/dist/hooks/cc-stop-prompt.js +1 -46
- package/dist/hooks/hook-llm.js +1 -53
- package/dist/hooks/hook-runner.js +20 -414
- package/dist/http/active-run-conflict.js +4 -129
- package/dist/http/cursor-fingerprint.d.ts +5 -0
- package/dist/http/cursor-fingerprint.js +5 -0
- package/dist/http/idempotency.js +0 -37
- package/dist/http/principal-gate.js +3 -40
- package/dist/http/route-ctx.js +0 -9
- package/dist/http/routes/a2a-serve.js +7 -319
- package/dist/http/routes/admin-config-refresh.js +0 -4
- package/dist/http/routes/admin-drain.js +0 -10
- package/dist/http/routes/adoption.js +1 -29
- package/dist/http/routes/agents-roster.js +1 -47
- package/dist/http/routes/approvals-assistant.js +32 -395
- package/dist/http/routes/attachments.js +4 -22
- package/dist/http/routes/capabilities.js +3 -420
- package/dist/http/routes/diagnostics.js +1 -81
- package/dist/http/routes/fleet.js +9 -185
- package/dist/http/routes/images.js +14 -239
- package/dist/http/routes/leader.js +0 -13
- package/dist/http/routes/memory-bundle.js +1 -61
- package/dist/http/routes/memory-policy.js +9 -99
- package/dist/http/routes/notify-wake.js +3 -37
- package/dist/http/routes/observability.js +2 -19
- package/dist/http/routes/retention-ops.js +2 -34
- package/dist/http/routes/rules.js +1 -93
- package/dist/http/routes/runs.js +64 -873
- package/dist/http/routes/session-sync.js +19 -258
- package/dist/http/routes/sessions-list.js +17 -43
- package/dist/http/routes/sessions.js +34 -226
- package/dist/http/routes/shared-memory.js +5 -36
- package/dist/http/routes/side-query.js +1 -87
- package/dist/http/routes/tasks.js +52 -722
- package/dist/http/routes/trace-usage.js +32 -195
- package/dist/http/routes/workflows.js +19 -195
- package/dist/http/run-meta.js +0 -6
- package/dist/http/send.js +0 -32
- package/dist/http/server.js +92 -1597
- package/dist/http/sse-lifecycle.js +2 -13
- package/dist/http/sse-log.js +3 -48
- package/dist/http/tar.js +5 -21
- package/dist/http/verify-rounds.js +0 -5
- package/dist/http/wire-gate.js +0 -9
- package/dist/http/workspace-content.js +0 -10
- package/dist/images/bake-validate.js +1 -70
- package/dist/images/manifest.js +1 -6
- package/dist/index.js +0 -21
- package/dist/key-resolver.js +2 -17
- package/dist/leader/diffout.js +1 -20
- package/dist/leader/diffup.js +0 -47
- package/dist/leader/endpoint.js +2 -57
- package/dist/leader/fanout.js +3 -45
- package/dist/leader/grader-env-factory.js +3 -72
- package/dist/leader/leader.js +5 -156
- package/dist/leader/merge.js +8 -115
- package/dist/leader/planner.js +3 -54
- package/dist/leader/repair-oracle.js +1 -60
- package/dist/leader/repair-wire.js +2 -79
- package/dist/leader/wire.js +8 -307
- package/dist/lsp/e2b-bridge.js +4 -64
- package/dist/lsp/e2b-manager.js +6 -94
- package/dist/lsp/lsp-frames.js +0 -12
- package/dist/lsp/manager.js +4 -96
- package/dist/lsp/ws-transport.js +5 -55
- package/dist/lsp-evict.js +1 -15
- package/dist/main.js +57 -765
- package/dist/memory-bundle-engine.js +0 -55
- package/dist/memory-export.js +0 -4
- package/dist/memory-posture.js +1 -15
- package/dist/memory-scope.js +11 -148
- package/dist/memory-sync-client.js +2 -44
- package/dist/memory-sync.js +1 -80
- package/dist/model-select.js +3 -80
- package/dist/observability/cost-quota.js +1 -17
- package/dist/observability/cost-taxonomy.js +0 -34
- package/dist/observability/fail-open.js +7 -86
- package/dist/observability/logger.js +0 -6
- package/dist/observability/metrics.js +0 -94
- package/dist/observability/otel-exporter.js +3 -13
- package/dist/observability/principal-context.js +0 -9
- package/dist/observability/prompt-manifest.js +1 -37
- package/dist/observability/rate-limit.js +0 -4
- package/dist/observability/secret-env-scrub.js +2 -56
- package/dist/observability/tool-trace.js +1 -70
- package/dist/orchestration/hardened-vm-runner.js +4 -118
- package/dist/orchestration/hardened-vm-worker-runner.js +1 -26
- package/dist/orchestration/hardened-vm-worker.js +0 -27
- package/dist/orchestration/subagent-steer.js +1 -45
- package/dist/orchestration/workflow-agent-steer.js +1 -80
- package/dist/orchestration/workflow-completion-inbox.js +32 -285
- package/dist/orchestration/workflow-notify-journal.js +16 -259
- package/dist/org-memory-admission.js +3 -47
- package/dist/parent-watch.js +2 -48
- package/dist/parked-decide.js +1 -109
- package/dist/per-task-image.js +0 -57
- package/dist/plan-cache-probe.js +3 -27
- package/dist/plugins/adoption-log-sql.js +2 -119
- package/dist/plugins/approval-ask-store-memory.js +3 -38
- package/dist/plugins/approval-ask-store-sql.js +6 -188
- package/dist/plugins/approval-exemption-store.js +2 -28
- package/dist/plugins/background-agent-store-sql.js +4 -105
- package/dist/plugins/background-shell-support.js +14 -122
- package/dist/plugins/blob-backend.js +6 -169
- package/dist/plugins/breaker-state-sql.js +8 -46
- package/dist/plugins/caching-session-store.js +4 -106
- package/dist/plugins/checkpoint-store-sql.js +18 -505
- package/dist/plugins/e2b-orphan-reclaim.js +0 -45
- package/dist/plugins/file-outcome-sink.js +0 -9
- package/dist/plugins/file-resume-anchor-store.js +4 -43
- package/dist/plugins/file-run-store.js +26 -364
- package/dist/plugins/file-snapshot-store-sql.js +10 -181
- package/dist/plugins/fork-routing-session-store.js +8 -111
- package/dist/plugins/host-platform.js +2 -91
- package/dist/plugins/image-bake-store-sql.js +7 -250
- package/dist/plugins/image-index-sql.js +4 -123
- package/dist/plugins/k8s-bg-scripts.js +4 -88
- package/dist/plugins/k8s-exec-protocol.js +0 -41
- package/dist/plugins/leader-run-store-sql.js +0 -101
- package/dist/plugins/local-checkpoint-store.js +8 -128
- package/dist/plugins/local-session-store.js +34 -296
- package/dist/plugins/local-task-attachment-store.js +2 -16
- package/dist/plugins/mailbox-store-sql.js +8 -65
- package/dist/plugins/memory-embedder-fingerprint.js +5 -166
- package/dist/plugins/memory-embedder.js +4 -70
- package/dist/plugins/memory-engine-pg.js +6 -170
- package/dist/plugins/memory-engine-tidb.js +7 -155
- package/dist/plugins/memory-engine-vector-util.js +0 -10
- package/dist/plugins/memory-key-guards.js +0 -34
- package/dist/plugins/memory-origin-law.js +0 -187
- package/dist/plugins/memory-resume-anchor-store.js +0 -17
- package/dist/plugins/memory-run-store.js +12 -89
- package/dist/plugins/memory-session-policy-store.js +0 -17
- package/dist/plugins/memory-sync-store-pg.js +4 -49
- package/dist/plugins/memory-sync-store-tidb.js +3 -35
- package/dist/plugins/outcome-ledger-sql.js +3 -97
- package/dist/plugins/permission-rule-store-file.js +5 -133
- package/dist/plugins/permission-rule-store-sql.d.ts +3 -0
- package/dist/plugins/permission-rule-store-sql.js +50 -307
- package/dist/plugins/pg-cost-quota.js +0 -7
- package/dist/plugins/pg-pool.js +0 -92
- package/dist/plugins/pg-rate-limiter.js +2 -13
- package/dist/plugins/pg-safe-json.js +4 -40
- package/dist/plugins/pg-session-storage.js +25 -189
- package/dist/plugins/posix-shell-fs.js +1 -31
- package/dist/plugins/remote-env-adb.js +12 -101
- package/dist/plugins/remote-env-e2b.js +40 -371
- package/dist/plugins/remote-env-file-error.js +0 -37
- package/dist/plugins/remote-env-host.js +63 -483
- package/dist/plugins/remote-env-k8s.js +32 -326
- package/dist/plugins/remote-env-local-docker.js +24 -157
- package/dist/plugins/remote-env-ssh.js +20 -128
- package/dist/plugins/remote-scratchpad.js +2 -32
- package/dist/plugins/remote-shell.js +1 -32
- package/dist/plugins/resume-anchor-store-sql.js +0 -11
- package/dist/plugins/retention-lane-store-sql.js +0 -108
- package/dist/plugins/retention-store-sql.js +4 -383
- package/dist/plugins/roster-store-sql.js +0 -55
- package/dist/plugins/run-store-sql.js +14 -251
- package/dist/plugins/s3-presign.js +2 -49
- package/dist/plugins/scheduler-support.js +3 -80
- package/dist/plugins/send-file-ledger.js +4 -53
- package/dist/plugins/send-user-file.js +4 -94
- package/dist/plugins/session-placement.js +1 -89
- package/dist/plugins/session-policy-store-sql.js +3 -81
- package/dist/plugins/session-store.js +0 -57
- package/dist/plugins/shared-memory-store-sql.js +4 -167
- package/dist/plugins/sql-driver.js +0 -17
- package/dist/plugins/sql-errors.js +0 -7
- package/dist/plugins/sql-escape.js +0 -8
- package/dist/plugins/sql-row-helpers.js +0 -25
- package/dist/plugins/store-backend.js +42 -222
- package/dist/plugins/store-contracts.js +2 -46
- package/dist/plugins/task-attachment-store.js +3 -49
- package/dist/plugins/task-list-store-sql.js +0 -79
- package/dist/plugins/tidb-cost-quota.js +1 -4
- package/dist/plugins/tidb-pool.js +1 -207
- package/dist/plugins/tidb-rate-limiter.js +3 -9
- package/dist/plugins/tidb-session-storage.js +4 -70
- package/dist/plugins/tidb-session-store.js +19 -341
- package/dist/plugins/tool-result-store-sql.js +4 -179
- package/dist/plugins/usage-window-store-sql.js +0 -8
- package/dist/plugins/web-search.js +10 -132
- package/dist/plugins/workflow-journal-store-sql.js +2 -58
- package/dist/plugins/workflow-run-store-sql.js +6 -91
- package/dist/plugins/worktree-isolation.js +6 -126
- package/dist/plugins/write-behind-counter.js +16 -75
- package/dist/principal-jwt.js +5 -60
- package/dist/project-memory.js +15 -146
- package/dist/prompts-domain-validate.js +1 -51
- package/dist/question.js +2 -111
- package/dist/resource-suspend.js +0 -18
- package/dist/router/route-orchestration.js +0 -77
- package/dist/rules-consent.d.ts +44 -5
- package/dist/rules-consent.js +75 -215
- package/dist/run-local.js +14 -381
- package/dist/runs.js +29 -617
- package/dist/runtime-caps-resolver.js +6 -132
- package/dist/runtime-governance.js +1 -232
- package/dist/sandbox-pkg-source.js +0 -37
- package/dist/sealed-key.js +3 -68
- package/dist/security.js +6 -291
- package/dist/session-leaf-bus.js +0 -32
- package/dist/session-sync-content.js +1 -69
- package/dist/session-sync-kernel.js +3 -60
- package/dist/session-sync.js +3 -66
- package/dist/session-titler.js +7 -41
- package/dist/session-watch.js +9 -73
- package/dist/shared-memory-scope-authorizer.js +0 -18
- package/dist/sighup-idle.js +1 -12
- package/dist/spec-fields.js +4 -143
- package/dist/store-live-probe.js +3 -49
- package/dist/task-a2a.js +1 -126
- package/dist/task-cwd.js +1 -103
- package/dist/task-mcp.js +1 -89
- package/dist/task-settings.js +10 -300
- package/dist/task-workflow.js +8 -75
- package/dist/tool-approval.d.ts +28 -1
- package/dist/tool-approval.js +70 -1430
- package/dist/trace/artifacts.js +5 -20
- package/dist/trace/engine-notice-wire.js +6 -138
- package/dist/trace/ledger-sink.js +7 -96
- package/dist/trace/project.js +4 -448
- package/dist/trace/redact.js +9 -77
- package/dist/turn-activity.js +1 -27
- package/dist/usage-analytics.js +4 -34
- package/dist/wall-clock-jump-guard.js +1 -68
- package/package.json +3 -3
- package/dist/capabilities/scenario-alias.d.ts +0 -27
- package/dist/capabilities/scenario-alias.js +0 -61
package/dist/leader/merge.js
CHANGED
|
@@ -1,29 +1,18 @@
|
|
|
1
1
|
import { delimitUntrusted } from "@sema-agent/core";
|
|
2
2
|
import { applyAndGrade, changedFilesOutOfAllowlist } from "./grader-env-factory.js";
|
|
3
3
|
import { shellQuote as Q } from "../plugins/remote-shell.js";
|
|
4
|
-
/** Integrate completed workers' branches. Never throws — failures are typed MergeResult. */
|
|
5
4
|
export async function mergeBranches(reports, baseSha, deps) {
|
|
6
5
|
const repo = deps.repoDir ?? "/repo";
|
|
7
6
|
const mergeable = reports.filter((r) => r.status === "completed");
|
|
8
7
|
if (mergeable.length === 0) {
|
|
9
8
|
return { ok: false, phase: "no-workers", reason: "no completed workers to merge", quarantine: reports.map((r) => r.workerId) };
|
|
10
9
|
}
|
|
11
|
-
// The whole pipeline is wrapped so this function honours its "never throws" contract AND never leaks the
|
|
12
|
-
// integration sandbox: provisioning (which connects+seeds a billable VM) and pullWorkerDiff used to run
|
|
13
|
-
// OUTSIDE the try, so a throw there (e.g. seed failure) escaped as an exception and — worse — left a
|
|
14
|
-
// connected sandbox with no `destroy` ever called. `destroy` is captured the instant provisioning resolves,
|
|
15
|
-
// and the finally tears it down on every path (success / typed failure / thrown error).
|
|
16
10
|
let destroy;
|
|
17
|
-
// Track the current stage so a THROWN error is attributed to the right phase (not always "provision").
|
|
18
11
|
let phase = "diff-out";
|
|
19
|
-
|
|
20
|
-
// spend/repair/measure that already HAPPENED must ride the thrown-failure return too — the leader folds
|
|
21
|
-
// conflictResolverCostUsd into the next round's budget, and a throw doesn't un-spend it.
|
|
22
|
-
let conflictResolverCostUsd = 0; // 🔴 capture the bounded conflict-resolver agent's spend (was dropped → S1 TEAM-cost undercount)
|
|
12
|
+
let conflictResolverCostUsd = 0;
|
|
23
13
|
let repairMeta;
|
|
24
14
|
let measure;
|
|
25
15
|
try {
|
|
26
|
-
// 1. pull each worker's diff OUT of its container (M2). A failed diff quarantines that worker, aborts merge.
|
|
27
16
|
const patches = [];
|
|
28
17
|
for (const r of mergeable) {
|
|
29
18
|
const d = await deps.pullWorkerDiff(r);
|
|
@@ -32,17 +21,11 @@ export async function mergeBranches(reports, baseSha, deps) {
|
|
|
32
21
|
}
|
|
33
22
|
patches.push({ workerId: r.workerId, patch: d.patch });
|
|
34
23
|
}
|
|
35
|
-
// 2. ephemeral no-creds integration sandbox (destroyed in finally — council#15).
|
|
36
24
|
phase = "provision";
|
|
37
25
|
const integ = await deps.provisionIntegrationSandbox();
|
|
38
26
|
destroy = integ.destroy;
|
|
39
27
|
const { env } = integ;
|
|
40
|
-
// The sandbox's base commit ≠ the durable remote's `baseSha` (different repos). Capture the sandbox's
|
|
41
|
-
// pre-apply HEAD so `format-patch <integBase>..HEAD` captures the integration commit even though the
|
|
42
|
-
// commit advances the sandbox's branch (dogfood-surfaced: `main..HEAD` was empty). `baseSha` is used
|
|
43
|
-
// ONLY for the durable push CAS below.
|
|
44
28
|
const integBase = (await env.exec("git rev-parse HEAD", { cwd: repo })).stdout.trim();
|
|
45
|
-
// 3. apply each patch --3way (council#3: keeps conflict context; capture .rej on failure, quarantine).
|
|
46
29
|
phase = "apply";
|
|
47
30
|
const conflictsResolved = [];
|
|
48
31
|
for (const p of patches) {
|
|
@@ -51,15 +34,10 @@ export async function mergeBranches(reports, baseSha, deps) {
|
|
|
51
34
|
const ap = await env.exec(`git apply --3way --whitespace=nowarn ${Q(patchPath)}`, { cwd: repo });
|
|
52
35
|
if (ap.exitCode !== 0) {
|
|
53
36
|
const rej = await env.exec(`find . -name '*.rej' -print -exec cat {} +`, { cwd: repo }).catch(() => ({ stdout: "", stderr: "", exitCode: -1 }));
|
|
54
|
-
// Bounded conflict-resolution (search 2026-06-16): a --3way conflict left markers/.rej in the tree. Instead
|
|
55
|
-
// of discarding ALL fan-out work + redoing single (leader collapseToSingle), run a bounded agent IN this
|
|
56
|
-
// sandbox to integrate THIS worker's patch (siblings already applied clean), gated by a marker/.rej re-check
|
|
57
|
-
// and the downstream test gate. Symmetric with integration-repair; OFF unless a conflictResolver is wired;
|
|
58
|
-
// degrades to the original apply-failure (collapse) on any leftover/agent-error — never worse than baseline.
|
|
59
37
|
const cr = integ.conflictResolver && integ.conflictResolver.maxRounds > 0
|
|
60
38
|
? await resolveApplyConflict(env, repo, p, rej.stdout, ap.stderr, integ.conflictResolver, deps)
|
|
61
39
|
: { resolved: false, costUsd: 0 };
|
|
62
|
-
conflictResolverCostUsd += cr.costUsd;
|
|
40
|
+
conflictResolverCostUsd += cr.costUsd;
|
|
63
41
|
if (!cr.resolved) {
|
|
64
42
|
deps.logger?.warn?.("merge_apply_failed", { workerId: p.workerId, exitCode: ap.exitCode });
|
|
65
43
|
return { ok: false, phase: "apply", reason: `git apply --3way failed for ${p.workerId} (conflict)`, quarantine: [p.workerId], rej: rej.stdout, details: ap.stderr.slice(0, 800), ...(conflictResolverCostUsd > 0 ? { conflictResolverCostUsd } : {}) };
|
|
@@ -67,15 +45,8 @@ export async function mergeBranches(reports, baseSha, deps) {
|
|
|
67
45
|
conflictsResolved.push(p.workerId);
|
|
68
46
|
}
|
|
69
47
|
}
|
|
70
|
-
// 4. full test suite IN the sandbox = the merge gate (untrusted diff stays sandboxed — [26]§16).
|
|
71
|
-
// testCmd is a caller-provided COMMAND LINE (e.g. "cd x && pytest"), run verbatim by design — NOT a single
|
|
72
|
-
// arg, so it is intentionally not Q()-quoted (council round-2 noted the asymmetry). Blast radius = the
|
|
73
|
-
// ephemeral no-creds sandbox; the caller owns the command (trusted control-plane config).
|
|
74
48
|
phase = "test";
|
|
75
49
|
let test = await env.exec(deps.testCmd, { cwd: repo });
|
|
76
|
-
// Bounded integration-repair (search 2026-06-14): a clean-merge-but-failing-build used to be terminal.
|
|
77
|
-
// If the sandbox supplied a `repair`, run the repair agent IN this merged tree (it edits the working tree;
|
|
78
|
-
// the squash commit below captures worker patches + repair together), then re-run testCmd. Up to maxRounds.
|
|
79
50
|
if (test.exitCode !== 0 && integ.repair && integ.repair.maxRounds > 0) {
|
|
80
51
|
let cost = 0;
|
|
81
52
|
let round = 0;
|
|
@@ -88,7 +59,7 @@ export async function mergeBranches(reports, baseSha, deps) {
|
|
|
88
59
|
}
|
|
89
60
|
catch (e) {
|
|
90
61
|
deps.logger?.warn?.("integration_repair_error", { round: round + 1, err: e instanceof Error ? e.message : String(e) });
|
|
91
|
-
break;
|
|
62
|
+
break;
|
|
92
63
|
}
|
|
93
64
|
test = await env.exec(deps.testCmd, { cwd: repo });
|
|
94
65
|
}
|
|
@@ -98,28 +69,13 @@ export async function mergeBranches(reports, baseSha, deps) {
|
|
|
98
69
|
if (test.exitCode !== 0) {
|
|
99
70
|
return { ok: false, phase: "test", reason: `integration test failed (exit ${test.exitCode})`, details: (test.stdout + "\n" + test.stderr).slice(-2000), ...(repairMeta ? { repair: repairMeta } : {}), ...(conflictResolverCostUsd > 0 ? { conflictResolverCostUsd } : {}) };
|
|
100
71
|
}
|
|
101
|
-
// 4b. truecorrect MEASURE (design/59) — run the thorough hidden oracle IN this sandbox (E2B), where the merged
|
|
102
|
-
// code already lives, NOT on the CPU-contended control plane (fixes the frontier-multi oracle spawn-timeout
|
|
103
|
-
// that thinned n). Pure observation: never gates `ok`, just reports into MergeResult.measure.
|
|
104
72
|
if (deps.measureCmd) {
|
|
105
73
|
const mr = await env.exec(deps.measureCmd, { cwd: repo });
|
|
106
74
|
measure = mr.exitCode === 0 ? { pass: true } : { pass: false, reason: (mr.stdout + " " + mr.stderr).slice(-200) };
|
|
107
75
|
}
|
|
108
|
-
// 5. produce the integrated patch over base, hand to the Coordinator's credentialed push (council#1).
|
|
109
|
-
// Check the commit's exit code: if `git add -A`/commit fails, HEAD doesn't advance and `format-patch`
|
|
110
|
-
// still exits 0 with EMPTY output — pushing an empty patch would no-op or corrupt the merge silently.
|
|
111
76
|
phase = "push";
|
|
112
|
-
// If integration-repair ran, a repair agent may have created commits despite the instruction not to. Flatten
|
|
113
|
-
// any such commits back to the working tree (Codex review #2) so the single `commit` below squashes worker
|
|
114
|
-
// patches + repair edits into ONE integration commit — otherwise format-patch would emit the repair commit
|
|
115
|
-
// PLUS an empty leader commit, and the Coordinator's `git am` can choke on the empty patch. No-op when no
|
|
116
|
-
// repair commits exist (HEAD already == integBase).
|
|
117
77
|
if (repairMeta)
|
|
118
78
|
await env.exec(`git reset --soft ${Q(integBase)}`, { cwd: repo });
|
|
119
|
-
// Cost legs that MUST ride EVERY push-phase failure return (wq64gmm5e HIGH: TEAM C1 undercount). A
|
|
120
|
-
// conflictResolver and/or integration-repair may already have spent real tokens BEFORE any push-phase
|
|
121
|
-
// failure; dropping these on the commit/format-patch/empty/race/push-fail returns made captureTeamC1
|
|
122
|
-
// undercount the arm. Mirror the success/hold/test-fail returns exactly.
|
|
123
79
|
const costTail = { ...(repairMeta ? { repair: repairMeta } : {}), ...(conflictResolverCostUsd > 0 ? { conflictResolverCostUsd } : {}) };
|
|
124
80
|
const commit = await env.exec(`git add -A && git -c user.email=leader@local -c user.name=leader commit -m ${Q(`merge: integrate ${mergeable.map((r) => r.workerId).join("+")}`)} --allow-empty`, { cwd: repo });
|
|
125
81
|
if (commit.exitCode !== 0) {
|
|
@@ -132,17 +88,9 @@ export async function mergeBranches(reports, baseSha, deps) {
|
|
|
132
88
|
if (!integrated.stdout.trim()) {
|
|
133
89
|
return { ok: false, phase: "push", reason: "integrated patch is empty (nothing applied over base) — refusing to push", quarantine: mergeable.map((r) => r.workerId), ...costTail };
|
|
134
90
|
}
|
|
135
|
-
// ── §10.6 THE PUSH CHOKEPOINT + §10.3 push-hold-on-signal + §10.4 measure-drives-repair ──
|
|
136
|
-
// A single gate covering EVERY push path by construction (collapse `leader.ts:451` + N=1 `leader.ts:508`):
|
|
137
|
-
// the credentialed push fires ONLY when (a) no mergeable report carries a non-auto-accepting
|
|
138
|
-
// `repairTerminal` (§10.6 — the SAFE-tier `candidate_only` that the status collapse to "completed" would
|
|
139
|
-
// otherwise hide), AND (b) under `LEADER_MEASURE_GATES`, every configured strong oracle signal is GREEN
|
|
140
|
-
// (§10.3). When any of these HOLDS, the push is withheld and the held, DELIMITED candidate is surfaced for
|
|
141
|
-
// human acceptance (a candidate is never a silent push and never a silent drop — §5 invariant). With
|
|
142
|
-
// `measureGates` OFF and no report carrying a `repairTerminal`, this whole block is inert → byte-identical.
|
|
143
91
|
const evald = await evaluatePushHold(env, repo, mergeable, measure, integrated.stdout, deps);
|
|
144
92
|
if (evald.measure !== undefined)
|
|
145
|
-
measure = evald.measure;
|
|
93
|
+
measure = evald.measure;
|
|
146
94
|
if (evald.hold) {
|
|
147
95
|
const h = evald.hold;
|
|
148
96
|
deps.logger?.warn?.("merge_push_held", { terminal: h.terminal, reason: h.reason.slice(0, 200) });
|
|
@@ -165,9 +113,6 @@ export async function mergeBranches(reports, baseSha, deps) {
|
|
|
165
113
|
return { ok: true, ref: push.ref, merged: mergeable.map((r) => r.workerId), ...(measure ? { measure } : {}), ...(repairMeta ? { repair: repairMeta } : {}), ...(conflictsResolved.length > 0 ? { conflictsResolved } : {}), ...(conflictResolverCostUsd > 0 ? { conflictResolverCostUsd } : {}) };
|
|
166
114
|
}
|
|
167
115
|
catch (e) {
|
|
168
|
-
// Honour the "never throws" contract: a thrown pullWorkerDiff/provision/exec becomes a typed failure,
|
|
169
|
-
// attributed to the stage it threw in (not a blanket "provision"). [4953]②: spend/repair/measure that
|
|
170
|
-
// already happened before the throw ride this return too (same tail as every explicit failure return).
|
|
171
116
|
return {
|
|
172
117
|
ok: false, phase, reason: `merge aborted in ${phase}: ${e instanceof Error ? e.message : String(e)}`,
|
|
173
118
|
quarantine: mergeable.map((r) => r.workerId),
|
|
@@ -177,98 +122,48 @@ export async function mergeBranches(reports, baseSha, deps) {
|
|
|
177
122
|
};
|
|
178
123
|
}
|
|
179
124
|
finally {
|
|
180
|
-
// council#15: destroy the sandbox on EVERY exit path. Guarded because we may bail (diff-out) or throw
|
|
181
|
-
// BEFORE provisioning — `destroy` is only set once provisionIntegrationSandbox has resolved.
|
|
182
125
|
if (destroy)
|
|
183
126
|
await destroy().catch(() => { });
|
|
184
127
|
}
|
|
185
128
|
}
|
|
186
|
-
/**
|
|
187
|
-
* §10.6 + §10.3 + §10.4 — the push chokepoint decision (pure-ish: reads the reports + the optional measure grader;
|
|
188
|
-
* never throws — a grader error falls back to a HOLD, never a silent push). Returns `{ hold }` to WITHHOLD the
|
|
189
|
-
* credentialed push, surfacing the held candidate; `{}` to allow it. Three independent hold triggers:
|
|
190
|
-
*
|
|
191
|
-
* §10.6 — any mergeable report carries a `repairTerminal ∉ {undefined, "fixed"}`. A SAFE-tier `candidate_only`
|
|
192
|
-
* maps to core verdict "PASS" → the report status collapses to "completed", so WITHOUT this gate the
|
|
193
|
-
* merge would push `testCmd`-green-but-strong-oracle-UNCONFIRMED work. This fires regardless of
|
|
194
|
-
* `measureGates` (the single-agent repair loop is itself OFF-by-default behind LEADER_REPAIR_LOOP — once
|
|
195
|
-
* a report carries a repairTerminal, the operator opted in; the candidate MUST be held). Covers the
|
|
196
|
-
* collapse-to-single and N=1 leader paths by construction.
|
|
197
|
-
*
|
|
198
|
-
* §10.3 — under `measureGates` ONLY: a configured STRONG oracle that is not GREEN holds the push. The hold keys
|
|
199
|
-
* on the SIGNAL not a caller field: (`strongOracleSeeded`) OR (`measureCmd` present AND the measure did
|
|
200
|
-
* not pass). Absent BOTH strong signals → no §10.3 hold (legacy compile-only auto-push). With
|
|
201
|
-
* `measureGates` OFF this trigger is inert (today's observe-only measure).
|
|
202
|
-
*
|
|
203
|
-
* §10.4 — under `measureGates` AND `measureDrivesRepair`: re-seed the SINGLE out-of-process grader, mechanically
|
|
204
|
-
* check the integrated patch's changed files against the allowlist (out-of-allowlist / build-config →
|
|
205
|
-
* downgrade to `needs_human_oracle`), then re-measure THERE (never the repair-poisoned merge sandbox).
|
|
206
|
-
* A still-failing re-measure holds (`candidate_only`); a passing re-measure clears the §10.3 measure
|
|
207
|
-
* signal (so the §10.3 trigger no longer holds on it).
|
|
208
|
-
*/
|
|
209
129
|
async function evaluatePushHold(env, repo, mergeable, observeMeasure, integratedPatch, deps) {
|
|
210
|
-
// §10.7 — the integrated patch is worker-authored code reaching a human-read HTTP body / durable-approval row.
|
|
211
130
|
const heldCandidate = () => delimitUntrusted("CANDIDATE PATCH (untrusted worker code)", integratedPatch);
|
|
212
|
-
// §10.6 — a non-auto-accepting single-agent repair terminal rode in on a report (status collapsed to
|
|
213
|
-
// "completed"). The strongest non-fixed terminal wins the label (it's the WHY surfaced to the human).
|
|
214
131
|
const carried = mergeable.map((r) => r.repairTerminal).filter((t) => t !== undefined && t !== "fixed");
|
|
215
132
|
if (carried.length > 0) {
|
|
216
|
-
const terminal = carried[0];
|
|
133
|
+
const terminal = carried[0];
|
|
217
134
|
return { hold: { reason: `single-agent repair terminal '${terminal}' — strong oracle unconfirmed, push held for human acceptance (§10.6)`, terminal, candidatePatch: heldCandidate() } };
|
|
218
135
|
}
|
|
219
|
-
// §10.3/§10.4 are OFF unless the master measure-gates flag is on → byte-identical default path.
|
|
220
136
|
if (!deps.measureGates)
|
|
221
137
|
return {};
|
|
222
138
|
let measure = observeMeasure;
|
|
223
|
-
// §10.4 measure-drives-repair: re-measure the integrated patch in a FRESH-seeded out-of-process grader, with the
|
|
224
|
-
// mechanical changed-files allowlist guard. Runs only when configured AND there is a measure to re-confirm.
|
|
225
139
|
if (deps.measureDrivesRepair && deps.measureCmd) {
|
|
226
140
|
const mdr = deps.measureDrivesRepair;
|
|
227
141
|
try {
|
|
228
|
-
// §10.4(ii) — mechanical allowlist FIRST (a fresh sandbox is necessary-not-sufficient: poison can ride
|
|
229
|
-
// INSIDE the integrated patch). An out-of-allowlist / build-config touch → downgrade to needs_human_oracle.
|
|
230
142
|
const changed = await mdr.changedFiles(integratedPatch);
|
|
231
143
|
const offending = changedFilesOutOfAllowlist(changed, mdr.allowlist);
|
|
232
144
|
if (offending.length > 0) {
|
|
233
145
|
deps.logger?.warn?.("merge_measure_allowlist_denied", { offending: offending.slice(0, 20) });
|
|
234
146
|
return { hold: { reason: `integrated patch touches out-of-allowlist / build-config paths (${offending.slice(0, 5).join(", ")}) — needs_human_oracle (§10.4(ii))`, terminal: "needs_human_oracle", candidatePatch: heldCandidate() }, measure };
|
|
235
147
|
}
|
|
236
|
-
// Re-seed the SINGLE grader + re-measure THERE (never the repair-edited merge sandbox — §10.4(i)). The
|
|
237
|
-
// existing in-sandbox `repair` agent already ran in `mergeBranches`; the grader re-measure is the
|
|
238
|
-
// authoritative correctness signal. (Bounded measure-repair rounds beyond the existing integration-repair
|
|
239
|
-
// are a future enable; v1 re-measures once.)
|
|
240
148
|
const grade = await applyAndGrade(mdr.grader, integratedPatch, mdr.measureSteps);
|
|
241
149
|
measure = grade.passed ? { pass: true } : { pass: false, reason: (grade.trace ?? "measure failed in out-of-process grader").slice(-200) };
|
|
242
150
|
deps.logger?.info?.("merge_measure_grader", { passed: grade.passed, applied: grade.applied });
|
|
243
151
|
}
|
|
244
152
|
catch (e) {
|
|
245
|
-
// Fail-CLOSED (never a silent push): a grader transport/exec error holds the push as needs_human_oracle.
|
|
246
153
|
deps.logger?.warn?.("merge_measure_grader_error", { err: e instanceof Error ? e.message : String(e) });
|
|
247
|
-
// §10.7 — surfaced reason is a trusted constant; the untrusted error detail is in the warn log above.
|
|
248
154
|
return { hold: { reason: `measure grader errored — needs_human_oracle (§10.4)`, terminal: "needs_human_oracle", candidatePatch: heldCandidate() }, measure };
|
|
249
155
|
}
|
|
250
156
|
}
|
|
251
|
-
// §10.3 — push-hold on the SIGNAL. Auto-push only when every configured strong signal is GREEN.
|
|
252
157
|
const measureFailing = deps.measureCmd !== undefined && measure?.pass !== true;
|
|
253
|
-
const strongOracleUnconfirmed = deps.strongOracleSeeded === true;
|
|
158
|
+
const strongOracleUnconfirmed = deps.strongOracleSeeded === true;
|
|
254
159
|
if (measureFailing || strongOracleUnconfirmed) {
|
|
255
|
-
// §10.7 — the surfaced `reason` reaches a human-read HTTP body / durable-approval row, so it is built from
|
|
256
|
-
// TRUSTED constants ONLY; the untrusted measure output tail (`measure.reason`) stays in the logs, NEVER
|
|
257
|
-
// interpolated into a human-/LLM-reachable string.
|
|
258
160
|
const why = measureFailing ? "measure did not pass" : "hidden held-out oracle seeded but only the compile gate cleared";
|
|
259
161
|
deps.logger?.warn?.("merge_push_held_signal", { why, measureReason: (measure?.reason ?? "").slice(0, 200) });
|
|
260
162
|
return { hold: { reason: `strong oracle unconfirmed: ${why} — candidate_only, push held (§10.3)`, terminal: "candidate_only", candidatePatch: heldCandidate() }, measure };
|
|
261
163
|
}
|
|
262
164
|
return { measure };
|
|
263
165
|
}
|
|
264
|
-
/** Bounded resolution of a `git apply --3way` conflict (search 2026-06-16). The failed apply left conflict markers
|
|
265
|
-
* in-tree and/or `.rej` files. Run the resolver agent up to maxRounds, then VERIFY the tree is conflict-free (no
|
|
266
|
-
* `<<<<<<<`/`>>>>>>>` markers, no `*.rej`). Returns true iff clean → the patch is treated as applied and the merge
|
|
267
|
-
* continues, KEEPING every other worker's clean work. Any agent error / leftover marker → false → the caller falls
|
|
268
|
-
* back to today's apply-failure (collapse-to-single). Never worse than the baseline. */
|
|
269
166
|
async function resolveApplyConflict(env, repo, p, rejText, applyStderr, resolver, deps) {
|
|
270
|
-
// 🔴 Accumulate + RETURN the resolver's spend (it used to return only a boolean → the cost was dropped, the S1
|
|
271
|
-
// TEAM-cost-undercount bug). Counts every round, including the round that throws (the tokens were spent).
|
|
272
167
|
let costUsd = 0;
|
|
273
168
|
const conflictText = `git apply --3way could not cleanly apply worker '${p.workerId}'.\n--- apply stderr ---\n${applyStderr.slice(0, 1500)}\n--- reject hunks (*.rej) ---\n${rejText.slice(0, 3000)}`;
|
|
274
169
|
for (let round = 0; round < resolver.maxRounds; round++) {
|
|
@@ -279,10 +174,8 @@ async function resolveApplyConflict(env, repo, p, rejText, applyStderr, resolver
|
|
|
279
174
|
}
|
|
280
175
|
catch (e) {
|
|
281
176
|
deps.logger?.warn?.("merge_conflict_repair_error", { workerId: p.workerId, err: e instanceof Error ? e.message : String(e) });
|
|
282
|
-
return { resolved: false, costUsd };
|
|
177
|
+
return { resolved: false, costUsd };
|
|
283
178
|
}
|
|
284
|
-
// Resolved iff NO conflict markers and NO .rej files remain under the repo. `grep -rIlE` LEADS the command so a
|
|
285
|
-
// mock/impl can distinguish this verify from the `.rej` CAPTURE in mergeBranches; non-empty stdout = still dirty.
|
|
286
179
|
const check = await env
|
|
287
180
|
.exec(`grep -rIlE '^(<<<<<<<|>>>>>>>)' . 2>/dev/null; find . -name '*.rej' 2>/dev/null | head -20`, { cwd: repo })
|
|
288
181
|
.catch(() => ({ exitCode: -1, stdout: "DIRTY", stderr: "" }));
|
|
@@ -292,6 +185,6 @@ async function resolveApplyConflict(env, repo, p, rejText, applyStderr, resolver
|
|
|
292
185
|
return { resolved: true, costUsd };
|
|
293
186
|
}
|
|
294
187
|
}
|
|
295
|
-
return { resolved: false, costUsd };
|
|
188
|
+
return { resolved: false, costUsd };
|
|
296
189
|
}
|
|
297
190
|
//# sourceMappingURL=merge.js.map
|
package/dist/leader/planner.js
CHANGED
|
@@ -1,9 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Deterministic economic 防灾 guard (design/68 §7 / §11 D2, search [83] ruling (b)): the asymmetry is that a
|
|
3
|
-
* false fan-out is the -98% disaster while a false single merely runs slower — so every case decidable WITHOUT
|
|
4
|
-
* the model resolves to single, for free. Pure function (unit-testable); the semantic judgments (sequential/
|
|
5
|
-
* coupled/uncertain) stay with the router LLM, and "估不准回 single" is the collapse fallback (B3).
|
|
6
|
-
*/
|
|
7
1
|
export function economicGuard(objective, opts = {}) {
|
|
8
2
|
if (opts.fanoutEnabled === false)
|
|
9
3
|
return { single: true, reason: "fan-out disabled by deployment (A6)" };
|
|
@@ -14,12 +8,9 @@ export function economicGuard(objective, opts = {}) {
|
|
|
14
8
|
}
|
|
15
9
|
return { single: false };
|
|
16
10
|
}
|
|
17
|
-
/** The deterministic SINGLE route: one sub-task owning the whole objective (the same shape the router emits
|
|
18
|
-
* for its single decision — N=1 runs through the identical leader pipeline). */
|
|
19
11
|
export function singleRoute(objective) {
|
|
20
12
|
return [{ workerId: "main", branch: "worker/main", files: ["."], spec: { objective } }];
|
|
21
13
|
}
|
|
22
|
-
/** The planning prompt: asks for a strict JSON array of disjoint-file sub-tasks. */
|
|
23
14
|
export function planPrompt(objective, opts = {}) {
|
|
24
15
|
const max = opts.maxSubtasks ?? 6;
|
|
25
16
|
return [
|
|
@@ -33,7 +24,6 @@ export function planPrompt(objective, opts = {}) {
|
|
|
33
24
|
`Rules: workerId unique + url-safe; files non-empty + disjoint across ALL sub-tasks; objective self-contained.`,
|
|
34
25
|
].filter(Boolean).join(" ");
|
|
35
26
|
}
|
|
36
|
-
/** Extract a JSON array from a model completion (tolerates ```json fences / surrounding prose). */
|
|
37
27
|
function extractJsonArray(raw) {
|
|
38
28
|
const fenced = raw.match(/```(?:json)?\s*([\s\S]*?)```/i);
|
|
39
29
|
const candidate = (fenced ? fenced[1] : raw).trim();
|
|
@@ -48,20 +38,11 @@ function extractJsonArray(raw) {
|
|
|
48
38
|
throw new Error("planner output is not valid JSON");
|
|
49
39
|
}
|
|
50
40
|
}
|
|
51
|
-
/** Parse + VALIDATE a planner completion into SubtaskSpec[]. Throws on any violation (disjointness etc.). */
|
|
52
41
|
export function parsePlan(raw, opts = {}) {
|
|
53
42
|
return validateSubtasks(extractJsonArray(raw), opts);
|
|
54
43
|
}
|
|
55
|
-
/**
|
|
56
|
-
* Deterministically validate an array of sub-tasks (the trust boundary — see file header). Used both for the
|
|
57
|
-
* LLM planner output AND for caller-supplied `body.subtasks`, which would otherwise skip every invariant: the
|
|
58
|
-
* `branch` is interpolated into a worker-container shell command (`git checkout -b <branch>`), so an
|
|
59
|
-
* unvalidated branch (`x; curl …|sh`, `` `reboot` ``, `--upload-pack=…`) would inject; and the disjoint-file /
|
|
60
|
-
* unique-id invariants are what the Coordinator's parallel-merge depends on. Throws on any violation.
|
|
61
|
-
*/
|
|
62
44
|
export function validateSubtasks(arr, opts = {}) {
|
|
63
45
|
const max = opts.maxSubtasks ?? 6;
|
|
64
|
-
// "plan" not "planner" — this also validates caller-supplied body.subtasks, not just LLM output.
|
|
65
46
|
if (!Array.isArray(arr) || arr.length === 0)
|
|
66
47
|
throw new Error("plan must be a non-empty array of sub-tasks");
|
|
67
48
|
if (arr.length > max)
|
|
@@ -89,15 +70,6 @@ export function validateSubtasks(arr, opts = {}) {
|
|
|
89
70
|
seenFiles.add(norm);
|
|
90
71
|
}
|
|
91
72
|
const branch = typeof e.branch === "string" && e.branch.trim() ? e.branch.trim() : `worker/${workerId}`;
|
|
92
|
-
// Reject a leading "-" (would be parsed as an option by `git checkout -b <branch>`), ".." (ref escape), and
|
|
93
|
-
// an over-long ref (git rejects it anyway; cap keeps the value tidy). The char set is shell-safe.
|
|
94
|
-
//
|
|
95
|
-
// 🔗 近亲判据(2026-08-08,#176 二轮扫描 finding):`leader/diffout.ts` 的 `safeRev` 用**同一套**字符
|
|
96
|
-
// 类 `[A-Za-z0-9._/-]`、**同一个** 200 长度上限、**同样**拒前导 `-`。唯一的真差异在 `..`:safeRev 只拒
|
|
97
|
-
// `..--`(rev 要支持 `A..B` 区间形),这里全拒 `..`(branch **名**里 `..` 无合法用途,git 自己也拒)
|
|
98
|
-
// —— 是**有意收紧**,不是抄漏。两处此前零互指、无共享基元、无跨处钉;单边加一个字符类成员时另一边
|
|
99
|
-
// 不会有任何提示。是否抽一个共享基元(比如 `refToken(s, { allowRange })`)属结构面决定,候裁;本批
|
|
100
|
-
// 只把「近亲关系 + 差异所在」两侧成文。
|
|
101
73
|
if (branch.length > 200 || !/^[A-Za-z0-9._/-]+$/.test(branch) || branch.startsWith("-") || branch.includes("..")) {
|
|
102
74
|
throw new Error(`sub-task ${workerId}: invalid branch`);
|
|
103
75
|
}
|
|
@@ -106,19 +78,10 @@ export function validateSubtasks(arr, opts = {}) {
|
|
|
106
78
|
}
|
|
107
79
|
return subs;
|
|
108
80
|
}
|
|
109
|
-
/** Run the planner: one model completion → validated disjoint-file sub-tasks. */
|
|
110
81
|
export async function llmPlan(objective, runPlan, opts = {}) {
|
|
111
82
|
const raw = await runPlan(planPrompt(objective, opts));
|
|
112
83
|
return parsePlan(raw, opts);
|
|
113
84
|
}
|
|
114
|
-
/**
|
|
115
|
-
* The DIFFICULTY-ROUTER prompt (search [43], planner-light). Same disjoint-subtask contract as `planPrompt`,
|
|
116
|
-
* but the model FIRST routes: default to a SINGLE agent (one sub-task owning the whole task) and only fan out
|
|
117
|
-
* when the objective is CLEARLY + ECONOMICALLY decomposable (disjoint files, independent/non-sequential, each
|
|
118
|
-
* part buildable without most of the same context). The campaign proved forcing fan-out on a sequential/coupled
|
|
119
|
-
* task is a -98% disaster, so the bias is firmly toward single. The output is still a plain disjoint-file
|
|
120
|
-
* sub-task array — a 1-element array IS the single route (the leader runs N=1 through the same pipeline).
|
|
121
|
-
*/
|
|
122
85
|
export function routePrompt(objective, opts = {}) {
|
|
123
86
|
const max = opts.maxSubtasks ?? 6;
|
|
124
87
|
return [
|
|
@@ -141,39 +104,25 @@ export function routePrompt(objective, opts = {}) {
|
|
|
141
104
|
`Rules: always a JSON ARRAY (SINGLE = one element, not a bare object); workerId unique + url-safe; files non-empty + DISJOINT across ALL elements; each objective self-contained.`,
|
|
142
105
|
].filter(Boolean).join(" ");
|
|
143
106
|
}
|
|
144
|
-
/** Run the difficulty router: one model completion → a routed plan (1 sub-task = single route, N = fan-out),
|
|
145
|
-
* validated by `parsePlan` (the same disjointness/shape invariants the Coordinator depends on). */
|
|
146
107
|
export async function routePlan(objective, runRoute, opts = {}) {
|
|
147
108
|
if (opts.fanoutEnabled === false)
|
|
148
|
-
return singleRoute(objective);
|
|
109
|
+
return singleRoute(objective);
|
|
149
110
|
const raw = await runRoute(routePrompt(objective, opts));
|
|
150
111
|
return parsePlan(raw, opts);
|
|
151
112
|
}
|
|
152
|
-
/**
|
|
153
|
-
* Decomposer quality gate (design/68 §5/§11 B1-B3): run the router, and on a rejected/unparseable plan retry
|
|
154
|
-
* the LLM AT MOST ONCE, then COLLAPSE TO SINGLE (never fail the task on a bad plan — single is almost always
|
|
155
|
-
* the right fallback, and a false fan-out is the -98% disaster; a plan failure must not be louder than that).
|
|
156
|
-
* `collapsed` carries the reason (observability: a collapse is a router-quality signal, not silent).
|
|
157
|
-
*/
|
|
158
113
|
export async function routePlanWithFallback(objective, runRoute, opts = {}) {
|
|
159
|
-
const guard = economicGuard(objective, opts);
|
|
114
|
+
const guard = economicGuard(objective, opts);
|
|
160
115
|
if (guard.single)
|
|
161
116
|
return { subs: singleRoute(objective) };
|
|
162
117
|
let firstErr;
|
|
163
|
-
let firstRaw = "";
|
|
118
|
+
let firstRaw = "";
|
|
164
119
|
for (let attempt = 0; attempt < 2; attempt++) {
|
|
165
|
-
// §5: no more than ONE LLM re-plan
|
|
166
120
|
const raw = await runRoute(routePrompt(objective, opts));
|
|
167
121
|
try {
|
|
168
122
|
return { subs: parsePlan(raw, opts) };
|
|
169
123
|
}
|
|
170
124
|
catch (e) {
|
|
171
125
|
firstErr ??= e;
|
|
172
|
-
// Capturing the raw output on collapse is load-bearing for diagnosis: a collapse logs only "not valid
|
|
173
|
-
// JSON", and the router runs on an in-memory (non-durable) session, so the raw completion is otherwise
|
|
174
|
-
// GONE — task#2d's flaky collapse could not be root-caused because nothing recorded the bad output. Keep
|
|
175
|
-
// a bounded head+tail so the next collapse is diagnosable (truncated/unterminated vs unescaped-quote vs
|
|
176
|
-
// prose) without dumping a 32k-token runaway in full.
|
|
177
126
|
if (!firstRaw)
|
|
178
127
|
firstRaw = raw.length > 600 ? `${raw.slice(0, 300)}…[${raw.length}ch]…${raw.slice(-150)}` : raw;
|
|
179
128
|
}
|
|
@@ -1,37 +1,7 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The PROFILE-injected `RepairOracle` closure for the leader's single-agent self-repair loop
|
|
3
|
-
* (`runRepairLoop`, core design/78 / D1). See `sema-internal server/docs/LEADER-REPAIRLOOP-INTEGRATION.md` (§3.1 + the authoritative
|
|
4
|
-
* §10 corrected layer). The closure GRADES a worker's candidate in an ISOLATED grader env (the reused
|
|
5
|
-
* integration sandbox, distinct from the worker env) and maps the result to a core `OracleResult`. core only
|
|
6
|
-
* fixes the `OracleResult` shape + the read-only/identity contract; the composition lives HERE so the core type
|
|
7
|
-
* never grows a discriminated union.
|
|
8
|
-
*
|
|
9
|
-
* Hardened by the adversarial round (§10):
|
|
10
|
-
* - §10.1 HERMETIC grade: the grader tree is reset to a clean base (`git reset --hard <integBase> && git clean
|
|
11
|
-
* -fdx`) BEFORE applying the worker's LATEST full `base..HEAD` diff each call — so a 2nd/3rd attempt never
|
|
12
|
-
* applies cumulatively onto an already-applied tree (the BROKEN finding). The diff is recomputed fresh every
|
|
13
|
-
* call (never a stale cached diff — verify.ts:328 BUG5).
|
|
14
|
-
* - flaky: the gate is re-isolated K times on a green-looking verdict; a disagreement is reported `flaky:true`
|
|
15
|
-
* and core's `terminalForTier` provably refuses to project it to a passing terminal.
|
|
16
|
-
* - §10.9 DE-CORRELATION: the L3 judge can only contribute a CLEARING tier when the verifier model is PROVABLY
|
|
17
|
-
* distinct from the generator — and the closure CANNOT resolve roles→models itself (that happens inside
|
|
18
|
-
* `Runner.runTask` after the closure returns), so the WIRE resolves both ids and passes them in; the closure
|
|
19
|
-
* downgrades to `l3_judge_advisory` when they are equal. v1 ships L3 OFF (`cfg.l3` unset) → L2 `trusted_hidden`
|
|
20
|
-
* is the sole clearer and `terminalForTier` caps every PASS at `candidate_only`. Wiring the actual
|
|
21
|
-
* `verifyCompleted` invocation is a future enable: it needs the worker's `TaskResult` threaded to the oracle
|
|
22
|
-
* (core passes only the report `evidence` today), so it is deliberately NOT invoked here.
|
|
23
|
-
*
|
|
24
|
-
* The grader topology (graderEnv = the reused integration sandbox, oracle seeded ONLY there, worker env distinct)
|
|
25
|
-
* means the worker can never read the oracle; for SAFE-tier `candidate_only` the bare `graderEnv !== workerEnv`
|
|
26
|
-
* identity check is sufficient, so the caller OMITS `immutableOraclePaths` (the structural bash write-probe is
|
|
27
|
-
* vacuous across distinct sandboxes and matters only for the mandate-OFF auto-accept path — §10.2).
|
|
28
|
-
*/
|
|
29
1
|
import { runExecGate, } from "@sema-agent/core";
|
|
30
2
|
import { createHash } from "node:crypto";
|
|
31
3
|
import { shellQuote as sq } from "../plugins/remote-shell.js";
|
|
32
|
-
/** A short, stable content hash for the stale-diff guard (fork2.1) + the graded-bytes binding (fork2.2). */
|
|
33
4
|
const sha = (s) => createHash("sha256").update(s).digest("hex").slice(0, 16);
|
|
34
|
-
/** Parse the changed file paths from a unified diff — the `diff --git a/X b/X` headers (both old + new path). */
|
|
35
5
|
function changedFilesOf(patch) {
|
|
36
6
|
const out = new Set();
|
|
37
7
|
for (const m of patch.matchAll(/^diff --git a\/(.+?) b\/(.+)$/gm)) {
|
|
@@ -41,8 +11,6 @@ function changedFilesOf(patch) {
|
|
|
41
11
|
return [...out];
|
|
42
12
|
}
|
|
43
13
|
const normP = (p) => p.replace(/^\.?\/+/, "").replace(/\/+$/, "");
|
|
44
|
-
/** fork1.2 — the first changed file that touches a protected oracle path (exact, or a subtree either way), else
|
|
45
|
-
* undefined. A patch that edits the grader's hidden-oracle/test path is refused (test-tampering guard). */
|
|
46
14
|
function touchesOraclePath(changed, oraclePaths) {
|
|
47
15
|
const ops = oraclePaths.map(normP).filter((p) => p !== "");
|
|
48
16
|
for (const f of changed.map(normP)) {
|
|
@@ -53,35 +21,23 @@ function touchesOraclePath(changed, oraclePaths) {
|
|
|
53
21
|
}
|
|
54
22
|
return undefined;
|
|
55
23
|
}
|
|
56
|
-
/**
|
|
57
|
-
* Build the `RepairOracle` closure. The returned `(graderEnv, evidence) => OracleResult` is what the leader
|
|
58
|
-
* passes as `RepairLoopConfig.oracle`. `evidence` (the worker's report text) is intentionally unused — the
|
|
59
|
-
* closure recomputes the candidate diff from the worker env each call (§10.1).
|
|
60
|
-
*/
|
|
61
24
|
export function mkRepairOracle(cfg) {
|
|
62
25
|
const repo = cfg.graderRepoDir;
|
|
63
26
|
const K = Math.max(1, Math.floor(cfg.flakyK ?? 2));
|
|
64
|
-
let lastGradedHash;
|
|
27
|
+
let lastGradedHash;
|
|
65
28
|
return async (graderEnv, _evidence) => {
|
|
66
|
-
// §10.1 — materialize the worker's LATEST code in a CLEAN grader tree. Pull fresh (never a stale diff).
|
|
67
29
|
const diff = await cfg.pullWorkerDiff();
|
|
68
30
|
if (!diff.ok) {
|
|
69
31
|
cfg.log?.("repair_oracle_pull_failed", { error: diff.error });
|
|
70
32
|
return { tier: cfg.passTier, passed: false, flaky: false, retries: 0, trace: `pull worker diff failed: ${diff.error}` };
|
|
71
33
|
}
|
|
72
34
|
const diffHash = sha(diff.patch);
|
|
73
|
-
// fork1.2 — refuse a candidate that edits a seeded oracle/test path: applied into the grader it
|
|
74
|
-
// could tamper the very oracle that grades it. Surface as `needs_human_oracle` (tier "none" → terminalForTier),
|
|
75
|
-
// never a silent candidate_only. (The §10.2 factory lane OMITs the engine path-probe, so this is the fence.)
|
|
76
35
|
const tampered = cfg.oraclePaths && cfg.oraclePaths.length > 0 ? touchesOraclePath(changedFilesOf(diff.patch), cfg.oraclePaths) : undefined;
|
|
77
36
|
if (tampered) {
|
|
78
37
|
cfg.log?.("repair_oracle_tampering_guard", { path: tampered });
|
|
79
38
|
cfg.onGraded?.({ diff: diff.patch, hash: diffHash, passed: false });
|
|
80
39
|
return { tier: "none", passed: false, flaky: false, retries: 0, trace: `candidate patch touches oracle/test path '${tampered}' — refusing to grade (fork1.2 test-tampering guard)` };
|
|
81
40
|
}
|
|
82
|
-
// fork2.1 — a repair attempt that produced NO new diff (byte-identical to the prior graded one)
|
|
83
|
-
// made no progress → never grade stale bytes as a fresh PASS: report no-progress (the loop gives up rather
|
|
84
|
-
// than re-passing a stale artifact). With fork2.2 this guarantees the SURFACED candidate == the graded bytes.
|
|
85
41
|
if (lastGradedHash !== undefined && diffHash === lastGradedHash) {
|
|
86
42
|
cfg.log?.("repair_oracle_stale_diff", { hash: diffHash });
|
|
87
43
|
cfg.onGraded?.({ diff: diff.patch, hash: diffHash, passed: false });
|
|
@@ -90,8 +46,6 @@ export function mkRepairOracle(cfg) {
|
|
|
90
46
|
lastGradedHash = diffHash;
|
|
91
47
|
const patchPath = `/tmp/candidate-${Date.now()}.patch`;
|
|
92
48
|
await cfg.graderWriteFile(patchPath, diff.patch);
|
|
93
|
-
// Hermetic prep: reset the grader to its clean base, then apply the FULL base→latest diff. `--whitespace=nowarn`
|
|
94
|
-
// matches the merge path (merge.ts:144). A prep failure (e.g. an apply conflict) is a non-flaky failed grade.
|
|
95
49
|
const prep = await runExecGate(graderEnv, [
|
|
96
50
|
{ command: `git -C ${sq(repo)} reset --hard ${sq(cfg.integBase)} && git -C ${sq(repo)} clean -fdx`, label: "grader-reset" },
|
|
97
51
|
{ command: `git -C ${sq(repo)} apply --whitespace=nowarn ${sq(patchPath)}`, label: "grader-apply" },
|
|
@@ -102,15 +56,8 @@ export function mkRepairOracle(cfg) {
|
|
|
102
56
|
cfg.onGraded?.({ diff: diff.patch, hash: diffHash, passed: false });
|
|
103
57
|
return { tier: cfg.passTier, passed: false, flaky: false, retries: 0, trace: `grader prep failed:\n${trace}` };
|
|
104
58
|
}
|
|
105
|
-
// 🔴 council 2026-06-17 — the hermetic `clean -fdx` above DELETED the seeded hidden oracle (untracked/ignored,
|
|
106
|
-
// never committed at integBase). RE-INJECT it AFTER applying the worker patch, so (a) the gate runs against the
|
|
107
|
-
// REAL oracle the clean wiped (else `trusted_hidden` is a false label), and (b) any file the patch placed at an
|
|
108
|
-
// oracle path is OVERWRITTEN by the trusted content (defence-in-depth with the fork1.2 path guard above).
|
|
109
59
|
for (const f of cfg.oracleFiles ?? [])
|
|
110
60
|
await cfg.graderWriteFile(f.path, f.content);
|
|
111
|
-
// L2 mechanical gate (the trusted hidden oracle), re-isolated K times to settle flakiness. A clear FIRST
|
|
112
|
-
// failure short-circuits (no point re-running a definite fail); a green verdict is re-run to detect a flaky
|
|
113
|
-
// (nondeterministic) test — a disagreement is reported flaky and core refuses to project it to a pass.
|
|
114
61
|
const verdicts = [];
|
|
115
62
|
let lastTrace = "";
|
|
116
63
|
for (let k = 0; k < K; k++) {
|
|
@@ -123,18 +70,12 @@ export function mkRepairOracle(cfg) {
|
|
|
123
70
|
}
|
|
124
71
|
const passed = verdicts.length > 0 && verdicts.every(Boolean);
|
|
125
72
|
const flaky = new Set(verdicts).size > 1;
|
|
126
|
-
// §10.9 — a self-certifying verifier (no distinct model) cannot CLEAR: a PASS downgrades to advisory. v1 has
|
|
127
|
-
// `cfg.l3` unset (L3 off) → the L2 `passTier` stands and `terminalForTier` caps the PASS at candidate_only.
|
|
128
73
|
let tier = cfg.passTier;
|
|
129
74
|
if (cfg.l3 && passed && !flaky && cfg.l3.verifierModelId === cfg.l3.generatorModelId) {
|
|
130
75
|
cfg.log?.("repair_oracle_l3_decorrelation_failed", { model: cfg.l3.generatorModelId });
|
|
131
76
|
tier = "l3_judge_advisory";
|
|
132
77
|
}
|
|
133
|
-
// fork2.2 — bind the EXACT graded bytes (the diff that produced this verdict) so the caller surfaces the
|
|
134
|
-
// candidate the human reviews == the bytes that were graded (not a later re-pull that may differ).
|
|
135
78
|
cfg.onGraded?.({ diff: diff.patch, hash: diffHash, passed });
|
|
136
|
-
// [4953]① `retries` = how many re-isolations actually RAN (the cross-repo contract's wording) — the
|
|
137
|
-
// first-clear-fail short-circuit means this can be < K, and reporting the configured cap was a lie.
|
|
138
79
|
cfg.log?.("repair_oracle_verdict", { tier, passed, flaky, retries: verdicts.length });
|
|
139
80
|
return { tier, passed, flaky, retries: verdicts.length, ...(passed && !flaky ? {} : { trace: lastTrace || "oracle rejected the candidate" }) };
|
|
140
81
|
};
|