@ashlr/hub 3.2.6 → 3.3.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/CHANGELOG.md +290 -0
- package/README.md +4 -1
- package/dist/build-identity.json +2 -2
- package/dist/cli/activation.d.ts +41 -0
- package/dist/cli/activation.js +485 -0
- package/dist/cli/activation.js.map +1 -0
- package/dist/cli/audit.js +18 -0
- package/dist/cli/audit.js.map +1 -1
- package/dist/cli/backlog.d.ts +0 -5
- package/dist/cli/backlog.js +18 -0
- package/dist/cli/backlog.js.map +1 -1
- package/dist/cli/comms.js +20 -0
- package/dist/cli/comms.js.map +1 -1
- package/dist/cli/completions.d.ts +6 -1
- package/dist/cli/completions.js +37 -11
- package/dist/cli/completions.js.map +1 -1
- package/dist/cli/conductor-permit.d.ts +1 -0
- package/dist/cli/conductor-permit.js +122 -0
- package/dist/cli/conductor-permit.js.map +1 -0
- package/dist/cli/daemon.js +10 -3
- package/dist/cli/daemon.js.map +1 -1
- package/dist/cli/demo.d.ts +0 -27
- package/dist/cli/demo.js +17 -0
- package/dist/cli/demo.js.map +1 -1
- package/dist/cli/goals.d.ts +2 -0
- package/dist/cli/goals.js +90 -5
- package/dist/cli/goals.js.map +1 -1
- package/dist/cli/help.js +21 -3
- package/dist/cli/help.js.map +1 -1
- package/dist/cli/inbox.d.ts +14 -2
- package/dist/cli/inbox.js +137 -23
- package/dist/cli/inbox.js.map +1 -1
- package/dist/cli/index.d.ts +1 -0
- package/dist/cli/index.js +16 -0
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/loop.js +26 -5
- package/dist/cli/loop.js.map +1 -1
- package/dist/cli/onboard.d.ts +0 -13
- package/dist/cli/onboard.js +20 -0
- package/dist/cli/onboard.js.map +1 -1
- package/dist/cli/resources.js +15 -0
- package/dist/cli/resources.js.map +1 -1
- package/dist/cli/run.js +1 -1
- package/dist/cli/run.js.map +1 -1
- package/dist/cli/sandbox.js +43 -0
- package/dist/cli/sandbox.js.map +1 -1
- package/dist/cli/tui.d.ts +0 -6
- package/dist/cli/tui.js +14 -0
- package/dist/cli/tui.js.map +1 -1
- package/dist/core/comms/elon-dialogue.js +16 -7
- package/dist/core/comms/elon-dialogue.js.map +1 -1
- package/dist/core/config.js +70 -0
- package/dist/core/config.js.map +1 -1
- package/dist/core/daemon/activation-permit.d.ts +357 -33
- package/dist/core/daemon/activation-permit.js +1585 -183
- package/dist/core/daemon/activation-permit.js.map +1 -1
- package/dist/core/daemon/goal-conductor-permit-operator.d.ts +75 -0
- package/dist/core/daemon/goal-conductor-permit-operator.js +842 -0
- package/dist/core/daemon/goal-conductor-permit-operator.js.map +1 -0
- package/dist/core/daemon/loop.d.ts +13 -1
- package/dist/core/daemon/loop.js +294 -29
- package/dist/core/daemon/loop.js.map +1 -1
- package/dist/core/daemon/runtime-activation-resident-start-protocol.d.ts +141 -0
- package/dist/core/daemon/runtime-activation-resident-start-protocol.js +462 -0
- package/dist/core/daemon/runtime-activation-resident-start-protocol.js.map +1 -0
- package/dist/core/daemon/runtime-activation-resident-start-runtime.d.ts +27 -0
- package/dist/core/daemon/runtime-activation-resident-start-runtime.js +28 -0
- package/dist/core/daemon/runtime-activation-resident-start-runtime.js.map +1 -0
- package/dist/core/daemon/runtime-activation-stopped-consumer.d.ts +139 -0
- package/dist/core/daemon/runtime-activation-stopped-consumer.js +995 -0
- package/dist/core/daemon/runtime-activation-stopped-consumer.js.map +1 -0
- package/dist/core/daemon/runtime-activation-stopped-runtime.d.ts +36 -0
- package/dist/core/daemon/runtime-activation-stopped-runtime.js +134 -0
- package/dist/core/daemon/runtime-activation-stopped-runtime.js.map +1 -0
- package/dist/core/daemon/runtime-activation-transaction.d.ts +41 -17
- package/dist/core/daemon/runtime-activation-transaction.js +80 -11
- package/dist/core/daemon/runtime-activation-transaction.js.map +1 -1
- package/dist/core/daemon/runtime-release-dependency-inventory.js +1 -0
- package/dist/core/daemon/runtime-release-dependency-inventory.js.map +1 -1
- package/dist/core/daemon/service-install-authority.d.ts +0 -4
- package/dist/core/daemon/service-install-authority.js +18 -3
- package/dist/core/daemon/service-install-authority.js.map +1 -1
- package/dist/core/dashboard.d.ts +10 -0
- package/dist/core/dashboard.js +103 -28
- package/dist/core/dashboard.js.map +1 -1
- package/dist/core/effective-config.js +7 -0
- package/dist/core/effective-config.js.map +1 -1
- package/dist/core/fleet/automerge-pass.js +33 -0
- package/dist/core/fleet/automerge-pass.js.map +1 -1
- package/dist/core/fleet/decisions-ledger.js +35 -12
- package/dist/core/fleet/decisions-ledger.js.map +1 -1
- package/dist/core/fleet/external-skill-git-capture.js +9 -1
- package/dist/core/fleet/external-skill-git-capture.js.map +1 -1
- package/dist/core/fleet/judge-decision-metadata.d.ts +8 -1
- package/dist/core/fleet/judge-decision-metadata.js +14 -1
- package/dist/core/fleet/judge-decision-metadata.js.map +1 -1
- package/dist/core/fleet/manager.d.ts +15 -1
- package/dist/core/fleet/manager.js +108 -30
- package/dist/core/fleet/manager.js.map +1 -1
- package/dist/core/fleet/post-merge-credit.d.ts +134 -9
- package/dist/core/fleet/post-merge-credit.js +350 -10
- package/dist/core/fleet/post-merge-credit.js.map +1 -1
- package/dist/core/fleet/quota.d.ts +74 -7
- package/dist/core/fleet/quota.js +688 -34
- package/dist/core/fleet/quota.js.map +1 -1
- package/dist/core/fleet/self-improve.d.ts +34 -1
- package/dist/core/fleet/self-improve.js +106 -1
- package/dist/core/fleet/self-improve.js.map +1 -1
- package/dist/core/fleet/skill-library.d.ts +21 -2
- package/dist/core/fleet/skill-library.js +54 -12
- package/dist/core/fleet/skill-library.js.map +1 -1
- package/dist/core/fleet/status.js +7 -5
- package/dist/core/fleet/status.js.map +1 -1
- package/dist/core/fleet/worked-ledger.d.ts +22 -0
- package/dist/core/fleet/worked-ledger.js +32 -0
- package/dist/core/fleet/worked-ledger.js.map +1 -1
- package/dist/core/foundry/automerge-scope-policy.d.ts +2 -2
- package/dist/core/foundry/automerge-scope-policy.js +9 -2
- package/dist/core/foundry/automerge-scope-policy.js.map +1 -1
- package/dist/core/goals/advance.js +34 -5
- package/dist/core/goals/advance.js.map +1 -1
- package/dist/core/goals/conductor-quota.d.ts +38 -0
- package/dist/core/goals/conductor-quota.js +110 -0
- package/dist/core/goals/conductor-quota.js.map +1 -0
- package/dist/core/goals/conductor-target.d.ts +24 -0
- package/dist/core/goals/conductor-target.js +35 -0
- package/dist/core/goals/conductor-target.js.map +1 -0
- package/dist/core/goals/conductor.d.ts +15 -0
- package/dist/core/goals/conductor.js +116 -9
- package/dist/core/goals/conductor.js.map +1 -1
- package/dist/core/goals/store.d.ts +29 -5
- package/dist/core/goals/store.js +197 -30
- package/dist/core/goals/store.js.map +1 -1
- package/dist/core/goals/timestamp-repair.d.ts +50 -0
- package/dist/core/goals/timestamp-repair.js +740 -0
- package/dist/core/goals/timestamp-repair.js.map +1 -0
- package/dist/core/inbox/apply.js +5 -2
- package/dist/core/inbox/apply.js.map +1 -1
- package/dist/core/inbox/merge.d.ts +94 -1
- package/dist/core/inbox/merge.js +349 -16
- package/dist/core/inbox/merge.js.map +1 -1
- package/dist/core/integrations/github.d.ts +2 -0
- package/dist/core/integrations/github.js +2 -1
- package/dist/core/integrations/github.js.map +1 -1
- package/dist/core/integrations/pulse-sync.js +13 -4
- package/dist/core/integrations/pulse-sync.js.map +1 -1
- package/dist/core/learn/reflect.d.ts +42 -4
- package/dist/core/learn/reflect.js +56 -5
- package/dist/core/learn/reflect.js.map +1 -1
- package/dist/core/run/best-of-n.d.ts +18 -0
- package/dist/core/run/best-of-n.js +163 -18
- package/dist/core/run/best-of-n.js.map +1 -1
- package/dist/core/run/engines.d.ts +8 -0
- package/dist/core/run/engines.js +9 -1
- package/dist/core/run/engines.js.map +1 -1
- package/dist/core/run/learned-router.js +28 -3
- package/dist/core/run/learned-router.js.map +1 -1
- package/dist/core/run/orchestrator.d.ts +7 -2
- package/dist/core/run/orchestrator.js +140 -60
- package/dist/core/run/orchestrator.js.map +1 -1
- package/dist/core/strategy/goal-planner.js +7 -1
- package/dist/core/strategy/goal-planner.js.map +1 -1
- package/dist/core/swarm/planner.d.ts +2 -2
- package/dist/core/swarm/planner.js +52 -5
- package/dist/core/swarm/planner.js.map +1 -1
- package/dist/core/swarm/rollback.d.ts +11 -0
- package/dist/core/swarm/rollback.js +54 -0
- package/dist/core/swarm/rollback.js.map +1 -1
- package/dist/core/swarm/runner.js +123 -11
- package/dist/core/swarm/runner.js.map +1 -1
- package/dist/core/types.d.ts +139 -1
- package/dist/core/types.js.map +1 -1
- package/dist/core/web/api.js +105 -7
- package/dist/core/web/api.js.map +1 -1
- package/dist/core/web/control.d.ts +20 -0
- package/dist/core/web/control.js +49 -34
- package/dist/core/web/control.js.map +1 -1
- package/dist/core/web/fleet-status-cache.d.ts +68 -0
- package/dist/core/web/fleet-status-cache.js +120 -0
- package/dist/core/web/fleet-status-cache.js.map +1 -0
- package/dist/core/web/public/next/assets/index-CY0U241s.css +1 -0
- package/dist/core/web/public/next/assets/index-Ct0mf8n4.js +69 -0
- package/dist/core/web/public/next/assets/index-Ct0mf8n4.js.map +1 -0
- package/dist/core/web/public/next/index.html +15 -0
- package/dist/release-dependency-inventory.json +1 -1
- package/dist/tui/app.js +2 -2
- package/dist/tui/app.js.map +1 -1
- package/dist/tui/render.d.ts +2 -0
- package/dist/tui/render.js +162 -4
- package/dist/tui/render.js.map +1 -1
- package/docs/RUNTIME_ACTIVATION_AUTHORITY.md +126 -21
- package/docs/contracts/CONTRACT-M521.md +102 -0
- package/package.json +20 -3
- package/schema/config.schema.json +800 -5
- package/node_modules/which/CHANGELOG.md +0 -166
package/dist/core/inbox/merge.js
CHANGED
|
@@ -98,7 +98,7 @@ import { execFileSync } from 'node:child_process';
|
|
|
98
98
|
import { existsSync, mkdirSync, writeFileSync, unlinkSync, symlinkSync, readFileSync } from 'node:fs';
|
|
99
99
|
import { homedir } from 'node:os';
|
|
100
100
|
import { join } from 'node:path';
|
|
101
|
-
import { randomBytes } from 'node:crypto';
|
|
101
|
+
import { createHash, randomBytes } from 'node:crypto';
|
|
102
102
|
import { isDeepStrictEqual } from 'node:util';
|
|
103
103
|
import { loadProposal, recordRealizedMerge, setStatus, updateProposalField } from './store.js';
|
|
104
104
|
import { canonicalModelTag } from '../run/model-catalog.js';
|
|
@@ -128,6 +128,7 @@ import { acquireProposalMutationLock, releaseProposalMutationLock } from './prop
|
|
|
128
128
|
import { acquireOutwardMutationFence, ownsOutwardMutationFence, releaseOutwardMutationFence, } from '../sandbox/mutation-fence.js';
|
|
129
129
|
import { isAuthenticatedAwaitingRemoteHandoff, isApprovedRemoteHandoffRetryCandidate, remoteAuthorityBinding, remoteAuthorityMatchesRepo, remoteIntentAuthorizationId, REMOTE_HANDOFF_RECOVERY_MARKER, } from './remote-handoff.js';
|
|
130
130
|
import { findLocalRealizedMergeDescendant } from './realized-merge.js';
|
|
131
|
+
import { prepareHostMergeRevocation, transitionHostMergeRevocation, readHostMergeRevocationState, } from '../autonomy/host-merge-revocation-protocol.js';
|
|
131
132
|
function auditAutoMergeCanaryFailure(repo, sandboxId) {
|
|
132
133
|
try {
|
|
133
134
|
audit({
|
|
@@ -159,6 +160,20 @@ function decisionReadIsHealthy(decisions) {
|
|
|
159
160
|
const MERGE_BRANCH_PREFIX = 'ashlr/merge/';
|
|
160
161
|
/** Timeout for git operations (ms). */
|
|
161
162
|
const GIT_TIMEOUT = 30_000;
|
|
163
|
+
/**
|
|
164
|
+
* Gate 6.5 (self-eval parity, M54/M86): how many times to run test:invariants
|
|
165
|
+
* per flag state before declaring it red. >1 tolerates a one-off transient
|
|
166
|
+
* blip (lock timing, momentary contention) without weakening the gate — a
|
|
167
|
+
* genuinely broken invariant still fails every attempt. Kept small (one
|
|
168
|
+
* retry, short delay): sustained multi-minute contention from a concurrent
|
|
169
|
+
* test run can't be "waited out" here anyway — cross-tick retry (the
|
|
170
|
+
* proposal simply stays pending and is re-evaluated next pass, see
|
|
171
|
+
* automerge-pass.ts) is what actually absorbs that, and this must stay
|
|
172
|
+
* short enough to never turn a fast fail-closed refusal into a slow one.
|
|
173
|
+
*/
|
|
174
|
+
const SELF_EVAL_PARITY_RETRY_ATTEMPTS = 2;
|
|
175
|
+
/** Delay between self-eval parity retry attempts (ms). */
|
|
176
|
+
const SELF_EVAL_PARITY_RETRY_DELAY_MS = 500;
|
|
162
177
|
// ---------------------------------------------------------------------------
|
|
163
178
|
// git helpers — arg arrays, no shell
|
|
164
179
|
// ---------------------------------------------------------------------------
|
|
@@ -612,7 +627,23 @@ export function evaluateAutoMergeReadinessPreflight(proposal, cfg) {
|
|
|
612
627
|
return block('proposal has no repo');
|
|
613
628
|
if (proposal.verifyResult?.passed === false) {
|
|
614
629
|
const failed = proposal.verifyResult.failed?.filter(Boolean).join('; ');
|
|
615
|
-
|
|
630
|
+
const category = proposal.verifyResult.failureCategory;
|
|
631
|
+
// A verifier that could NOT be executed (missing binary, exit 127, spawn
|
|
632
|
+
// ENOENT) or that hit a bounded timeout/infra hiccup is a DIFFERENT
|
|
633
|
+
// outcome from a verifier that ran and found the diff broken. Both stay
|
|
634
|
+
// fail-closed (ready:false — this NEVER makes an unverified change
|
|
635
|
+
// eligible to merge), but only a real 'code' failure (or an
|
|
636
|
+
// unclassified/legacy record) may accrue toward stuckPassCount/
|
|
637
|
+
// autoArchive. An infra-flavored failure must not permanently kill a
|
|
638
|
+
// proposal whose only crime was that the verifier was momentarily
|
|
639
|
+
// unavailable — it stays pending and is retried next pass.
|
|
640
|
+
const infraLike = category === 'tool' || category === 'timeout' || category === 'infra';
|
|
641
|
+
const advisories = infraLike
|
|
642
|
+
? [
|
|
643
|
+
`verifier-unavailable: verification could not run to completion (${category}) — not counted toward auto-archive; will retry`,
|
|
644
|
+
]
|
|
645
|
+
: [];
|
|
646
|
+
return block(`known verification failure: proposal.verifyResult.passed is false${failed ? ` (${failed})` : ''}`, advisories, !infraLike);
|
|
616
647
|
}
|
|
617
648
|
const trustBasis = cfg.foundry?.['autoMerge']
|
|
618
649
|
? cfg.foundry['autoMerge']?.['trustBasis'] ?? 'tier'
|
|
@@ -1417,6 +1448,7 @@ export function verifyResultFromProposalResult(result, source = 'auto-merge', ve
|
|
|
1417
1448
|
return {
|
|
1418
1449
|
passed: result.ok,
|
|
1419
1450
|
...(result.ok ? {} : { failed: [result.detail] }),
|
|
1451
|
+
...(result.ok || result.failureCategory === undefined ? {} : { failureCategory: result.failureCategory }),
|
|
1420
1452
|
detail: result.detail,
|
|
1421
1453
|
ran: [...result.ran],
|
|
1422
1454
|
...(result.browser ? { browser: result.browser } : {}),
|
|
@@ -1534,6 +1566,7 @@ function verifyResultFromStored(result) {
|
|
|
1534
1566
|
...(result.browser ? { browser: result.browser } : {}),
|
|
1535
1567
|
...(result.baseBranch ? { baseBranch: result.baseBranch } : {}),
|
|
1536
1568
|
...(result.baseHead ? { baseHead: result.baseHead } : {}),
|
|
1569
|
+
...(result.failureCategory ? { failureCategory: result.failureCategory } : {}),
|
|
1537
1570
|
};
|
|
1538
1571
|
}
|
|
1539
1572
|
function truncateEvidenceText(text, limit = 500) {
|
|
@@ -1685,15 +1718,24 @@ export async function verifyProposal(proposal, cfg) {
|
|
|
1685
1718
|
}
|
|
1686
1719
|
// Apply the diff to the worktree ONLY AFTER capturing the base-derived
|
|
1687
1720
|
// command list, then run those (immutable) commands against the patched tree.
|
|
1721
|
+
// M??? (merge-pipeline reliability): --3way lets git fall back to a
|
|
1722
|
+
// three-way merge (using the blob shas already embedded in `git diff`'s
|
|
1723
|
+
// extended headers) when the diff's ORIGINAL base has since moved but the
|
|
1724
|
+
// proposal's edits don't actually conflict with what changed. Plain
|
|
1725
|
+
// `apply` fails on any context drift even for benign staleness; `--3way`
|
|
1726
|
+
// only fails closed on a REAL textual conflict, which is the outcome we
|
|
1727
|
+
// actually want to distinguish from "diff is corrupt". Safe: this
|
|
1728
|
+
// worktree is always removed in the finally block below regardless of
|
|
1729
|
+
// outcome, so a conflicted intermediate state is never observed.
|
|
1688
1730
|
patchFile = writeTmpFile(diff);
|
|
1689
1731
|
try {
|
|
1690
|
-
gitRun(tmpDir, ['apply', '--index', patchFile]);
|
|
1732
|
+
gitRun(tmpDir, ['apply', '--3way', '--index', patchFile]);
|
|
1691
1733
|
}
|
|
1692
1734
|
catch (err) {
|
|
1693
1735
|
return {
|
|
1694
1736
|
ok: false,
|
|
1695
1737
|
ran: [],
|
|
1696
|
-
detail: `git apply failed in verify worktree: ${err instanceof Error ? err.message : String(err)}`,
|
|
1738
|
+
detail: `git apply failed in verify worktree (3-way fallback also failed — likely a real conflict, not just a stale base): ${err instanceof Error ? err.message : String(err)}`,
|
|
1697
1739
|
baseBranch: base,
|
|
1698
1740
|
baseHead,
|
|
1699
1741
|
};
|
|
@@ -1711,6 +1753,7 @@ export async function verifyProposal(proposal, cfg) {
|
|
|
1711
1753
|
detail: `required verify '${vc.kind}' failed (exit ${res.exitCode}): ${res.command}`,
|
|
1712
1754
|
baseBranch: base,
|
|
1713
1755
|
baseHead,
|
|
1756
|
+
...(res.failureCategory ? { failureCategory: res.failureCategory } : {}),
|
|
1714
1757
|
};
|
|
1715
1758
|
}
|
|
1716
1759
|
// M281: for test commands, only block if NEW failures were introduced.
|
|
@@ -1788,6 +1831,7 @@ export async function verifyProposal(proposal, cfg) {
|
|
|
1788
1831
|
detail: `verify '${vc.kind}' failed (exit ${res.exitCode}): ${res.command}`,
|
|
1789
1832
|
baseBranch: base,
|
|
1790
1833
|
baseHead,
|
|
1834
|
+
...(res.failureCategory ? { failureCategory: res.failureCategory } : {}),
|
|
1791
1835
|
};
|
|
1792
1836
|
}
|
|
1793
1837
|
}
|
|
@@ -1968,6 +2012,223 @@ function reconcileLocalMergeIntent(id, cfg) {
|
|
|
1968
2012
|
});
|
|
1969
2013
|
return { ok: true, merged: true, reason };
|
|
1970
2014
|
}
|
|
2015
|
+
// ---------------------------------------------------------------------------
|
|
2016
|
+
// Host merge — the last hop of the remote handoff path: the actual
|
|
2017
|
+
// `gh pr merge`. Gated by durable per-authority revocation
|
|
2018
|
+
// (autonomy/host-merge-revocation-protocol.ts) on top of every existing
|
|
2019
|
+
// mechanical/judge/self-target gate already run above. Default DISABLED via
|
|
2020
|
+
// cfg.foundry.autoMerge.hostAutoMerge (separate opt-in from pushToRemote —
|
|
2021
|
+
// opening a PR and actually merging it are different trust levels).
|
|
2022
|
+
// ---------------------------------------------------------------------------
|
|
2023
|
+
/** Bounded lifetime of a single host-merge authority window (< the protocol's 15m cap). */
|
|
2024
|
+
export const HOST_MERGE_AUTHORITY_TTL_MS = 10 * 60 * 1000;
|
|
2025
|
+
/** Timeout for the `gh pr merge` invocation itself (network op). */
|
|
2026
|
+
const HOST_MERGE_TIMEOUT_MS = 30_000;
|
|
2027
|
+
/**
|
|
2028
|
+
* Placeholder kill-switch epoch. ~/.ashlr/KILL has no versioning of its own
|
|
2029
|
+
* today; this binds every host-merge authority to a fixed protocol
|
|
2030
|
+
* generation so the identity schema is forward-compatible if a real epoch
|
|
2031
|
+
* counter is added later. It is NOT a substitute for the direct
|
|
2032
|
+
* killSwitchOn() checks below — those remain the live, load-bearing gate.
|
|
2033
|
+
*/
|
|
2034
|
+
export const HOST_MERGE_KILL_EPOCH_DIGEST = createHash('sha256')
|
|
2035
|
+
.update('ashlr.host-merge-revocation.kill-epoch.v1', 'utf8')
|
|
2036
|
+
.digest('hex');
|
|
2037
|
+
export function hostMergeOperationId(id, suffix) {
|
|
2038
|
+
const candidate = `${id}-${suffix}`;
|
|
2039
|
+
return /^[A-Za-z0-9][A-Za-z0-9_.:-]{0,159}$/.test(candidate) ? candidate : null;
|
|
2040
|
+
}
|
|
2041
|
+
/**
|
|
2042
|
+
* Digest binding the currently-configured auto-merge policy surface.
|
|
2043
|
+
* Changing any of these values between prepare and arm changes the
|
|
2044
|
+
* authority's identity (and therefore its authorityId), so a stale grant
|
|
2045
|
+
* prepared under an old policy can never silently carry forward under a
|
|
2046
|
+
* different one — transitionHostMergeRevocation would refuse it as
|
|
2047
|
+
* 'authority-missing' instead.
|
|
2048
|
+
*/
|
|
2049
|
+
export function hostMergePolicyEpoch(input) {
|
|
2050
|
+
return createHash('sha256').update(JSON.stringify(input), 'utf8').digest('hex');
|
|
2051
|
+
}
|
|
2052
|
+
export function buildHostMergeRevocationIdentity(input) {
|
|
2053
|
+
return {
|
|
2054
|
+
nameWithOwner: input.nameWithOwner,
|
|
2055
|
+
repositoryId: input.repositoryId,
|
|
2056
|
+
baseRef: input.base,
|
|
2057
|
+
baseOid: input.baseOid,
|
|
2058
|
+
headRef: input.branch,
|
|
2059
|
+
headOid: input.headOid,
|
|
2060
|
+
pullRequestId: input.pullRequestId,
|
|
2061
|
+
pullRequestNumber: input.pullRequestNumber,
|
|
2062
|
+
evidencePackDigest: input.evidencePackDigest,
|
|
2063
|
+
verifierManifestDigest: input.verifierManifestDigest,
|
|
2064
|
+
protectionPolicyDigest: input.protectionPolicyDigest,
|
|
2065
|
+
killEpoch: HOST_MERGE_KILL_EPOCH_DIGEST,
|
|
2066
|
+
policyEpoch: input.policyEpoch,
|
|
2067
|
+
expiresAt: new Date(input.now.getTime() + HOST_MERGE_AUTHORITY_TTL_MS).toISOString(),
|
|
2068
|
+
};
|
|
2069
|
+
}
|
|
2070
|
+
/** Run `gh pr merge` directly (never throws). */
|
|
2071
|
+
function hostMergeGhPrMerge(cwd, args) {
|
|
2072
|
+
try {
|
|
2073
|
+
const stdout = execFileSync('gh', args, {
|
|
2074
|
+
cwd,
|
|
2075
|
+
timeout: HOST_MERGE_TIMEOUT_MS,
|
|
2076
|
+
stdio: 'pipe',
|
|
2077
|
+
encoding: 'utf8',
|
|
2078
|
+
env: {
|
|
2079
|
+
...process.env,
|
|
2080
|
+
GH_HOST: 'github.com',
|
|
2081
|
+
GH_NO_UPDATE_NOTIFIER: '1',
|
|
2082
|
+
NO_COLOR: '1',
|
|
2083
|
+
},
|
|
2084
|
+
});
|
|
2085
|
+
return { ok: true, detail: (typeof stdout === 'string' ? stdout.trim() : '') || 'gh pr merge exited 0' };
|
|
2086
|
+
}
|
|
2087
|
+
catch (err) {
|
|
2088
|
+
const e = err;
|
|
2089
|
+
const stderr = typeof e.stderr === 'string'
|
|
2090
|
+
? e.stderr
|
|
2091
|
+
: e.stderr instanceof Buffer
|
|
2092
|
+
? e.stderr.toString('utf8')
|
|
2093
|
+
: undefined;
|
|
2094
|
+
return { ok: false, detail: (stderr || e.message || 'gh pr merge failed').trim().slice(0, 2000) };
|
|
2095
|
+
}
|
|
2096
|
+
}
|
|
2097
|
+
/**
|
|
2098
|
+
* Attempt the last hop of the remote handoff: the actual `gh pr merge`.
|
|
2099
|
+
* Returns a human-readable mergeNote describing the outcome; NEVER throws.
|
|
2100
|
+
* Fails closed on every ambiguity (kill switch, unreadable/degraded/missing
|
|
2101
|
+
* revocation state, missing identity inputs). The proposal always ends up in
|
|
2102
|
+
* the existing awaiting-host-merge state regardless of this function's
|
|
2103
|
+
* outcome — the pre-existing reconcileRemoteHandoffs() sweep is the durable
|
|
2104
|
+
* source of truth for whatever actually happened on the host, whether we
|
|
2105
|
+
* merged it just now or a human merges it later.
|
|
2106
|
+
*
|
|
2107
|
+
* Exported (with its identity-building helpers) so tests can drive it
|
|
2108
|
+
* directly, and so a test can independently prepare/arm/revoke the EXACT
|
|
2109
|
+
* authority this function will look up (same identity, same clock) to
|
|
2110
|
+
* exercise durable revocation without needing to fabricate an entire
|
|
2111
|
+
* end-to-end autoMergeProposal() gate-chain pass.
|
|
2112
|
+
*/
|
|
2113
|
+
export async function attemptHostAutoMerge(input) {
|
|
2114
|
+
const { repo, id, cfg, nameWithOwner, repositoryId, url, base, baseOid, branch, headOid, observedPr, evidencePackDigest, verifierManifestDigest, protectionPolicyDigest, maxRisk, managerGateEnabled, allowSelfMerge, maxFiles, maxLines, } = input;
|
|
2115
|
+
const hostAutoMergeEnabled = autoMergeConfigValue(cfg, 'hostAutoMerge') === true;
|
|
2116
|
+
if (!hostAutoMergeEnabled) {
|
|
2117
|
+
return 'PR opened; host auto-merge is disabled (cfg.foundry.autoMerge.hostAutoMerge !== true)';
|
|
2118
|
+
}
|
|
2119
|
+
if (!verifierManifestDigest) {
|
|
2120
|
+
return 'PR opened; host auto-merge refused because no required-verifier manifest digest is bound to this attempt';
|
|
2121
|
+
}
|
|
2122
|
+
if (killSwitchOn()) {
|
|
2123
|
+
return 'PR opened; host auto-merge refused because the autonomy kill switch is ON';
|
|
2124
|
+
}
|
|
2125
|
+
// Re-derive PR identity fresh — never trust an object handed in from an
|
|
2126
|
+
// earlier step of this same invocation. This is also what makes an
|
|
2127
|
+
// interrupted host merge reconcilable: if a prior, crashed attempt already
|
|
2128
|
+
// got the PR merged, we observe that here and never re-invoke gh pr merge.
|
|
2129
|
+
const freshBeforeAttempt = viewPr(repo, url, { repo: nameWithOwner });
|
|
2130
|
+
if (!freshBeforeAttempt) {
|
|
2131
|
+
return 'PR opened; host auto-merge refused because the PR could not be freshly re-read before merging';
|
|
2132
|
+
}
|
|
2133
|
+
if (freshBeforeAttempt.state === 'MERGED' || Boolean(freshBeforeAttempt.mergedAt)) {
|
|
2134
|
+
return 'PR opened; already merged on the host (observed before any merge attempt this pass)';
|
|
2135
|
+
}
|
|
2136
|
+
if (freshBeforeAttempt.state !== 'OPEN' ||
|
|
2137
|
+
!freshBeforeAttempt.headRefOid || freshBeforeAttempt.headRefOid.toLowerCase() !== headOid.toLowerCase() ||
|
|
2138
|
+
freshBeforeAttempt.baseRefName !== base) {
|
|
2139
|
+
return `PR opened; host auto-merge refused because the live PR state changed (state=${freshBeforeAttempt.state ?? 'unknown'})`;
|
|
2140
|
+
}
|
|
2141
|
+
const pullRequestId = observedPr?.id ?? freshBeforeAttempt.id;
|
|
2142
|
+
const pullRequestNumber = observedPr?.number ?? freshBeforeAttempt.number;
|
|
2143
|
+
if (!pullRequestId || typeof pullRequestNumber !== 'number' || !Number.isSafeInteger(pullRequestNumber)) {
|
|
2144
|
+
return 'PR opened; host auto-merge refused because the PR node id/number could not be resolved';
|
|
2145
|
+
}
|
|
2146
|
+
const policyEpoch = hostMergePolicyEpoch({
|
|
2147
|
+
maxRisk, managerGateEnabled, allowSelfMerge, hostAutoMergeEnabled, maxFiles, maxLines,
|
|
2148
|
+
});
|
|
2149
|
+
const now = input.now ?? new Date();
|
|
2150
|
+
const identity = buildHostMergeRevocationIdentity({
|
|
2151
|
+
nameWithOwner, repositoryId, base, baseOid, branch, headOid,
|
|
2152
|
+
pullRequestId, pullRequestNumber, evidencePackDigest, verifierManifestDigest,
|
|
2153
|
+
protectionPolicyDigest, policyEpoch, now,
|
|
2154
|
+
});
|
|
2155
|
+
const prepareOpId = hostMergeOperationId(id, 'host-merge-prepare');
|
|
2156
|
+
const armOpId = hostMergeOperationId(id, 'host-merge-arm');
|
|
2157
|
+
const consumeOpId = hostMergeOperationId(id, 'host-merge-consume');
|
|
2158
|
+
if (!prepareOpId || !armOpId || !consumeOpId) {
|
|
2159
|
+
return 'PR opened; host auto-merge refused because a durable operation id could not be derived';
|
|
2160
|
+
}
|
|
2161
|
+
const prepared = prepareHostMergeRevocation({ identity, operationId: prepareOpId, now });
|
|
2162
|
+
if (!('receipt' in prepared)) {
|
|
2163
|
+
return `PR opened; host auto-merge refused: revocation prepare ${prepared.status} (${prepared.reason})`;
|
|
2164
|
+
}
|
|
2165
|
+
const armed = transitionHostMergeRevocation({
|
|
2166
|
+
identity,
|
|
2167
|
+
action: 'arm',
|
|
2168
|
+
operationId: armOpId,
|
|
2169
|
+
expectedSequence: prepared.receipt.sequence,
|
|
2170
|
+
expectedReceiptDigest: prepared.receipt.receiptDigest,
|
|
2171
|
+
now,
|
|
2172
|
+
});
|
|
2173
|
+
if (!('receipt' in armed)) {
|
|
2174
|
+
return `PR opened; host auto-merge refused: revocation arm ${armed.status} (${armed.reason})`;
|
|
2175
|
+
}
|
|
2176
|
+
// Immediately before the irreversible act: re-check the kill switch AND do
|
|
2177
|
+
// a FRESH authenticated read of the durable revocation store — never trust
|
|
2178
|
+
// the in-memory `armed` result, since a concurrent revoker could have acted
|
|
2179
|
+
// in between.
|
|
2180
|
+
if (killSwitchOn()) {
|
|
2181
|
+
const killRevokeOpId = hostMergeOperationId(id, 'host-merge-revoke-kill');
|
|
2182
|
+
if (killRevokeOpId) {
|
|
2183
|
+
// Best-effort durable revoke — gives the kill switch real teeth against
|
|
2184
|
+
// this specific authority, even though we abort THIS invocation either way.
|
|
2185
|
+
transitionHostMergeRevocation({
|
|
2186
|
+
identity,
|
|
2187
|
+
action: 'revoke',
|
|
2188
|
+
operationId: killRevokeOpId,
|
|
2189
|
+
expectedSequence: armed.receipt.sequence,
|
|
2190
|
+
expectedReceiptDigest: armed.receipt.receiptDigest,
|
|
2191
|
+
now,
|
|
2192
|
+
});
|
|
2193
|
+
}
|
|
2194
|
+
return 'PR opened; host auto-merge refused because the autonomy kill switch is ON';
|
|
2195
|
+
}
|
|
2196
|
+
const preMergeState = readHostMergeRevocationState(identity);
|
|
2197
|
+
if (preMergeState.state !== 'healthy' || preMergeState.record.phase !== 'armed') {
|
|
2198
|
+
const detail = preMergeState.state === 'healthy'
|
|
2199
|
+
? `phase=${preMergeState.record.phase}`
|
|
2200
|
+
: preMergeState.state === 'degraded'
|
|
2201
|
+
? preMergeState.reason
|
|
2202
|
+
: 'authority missing';
|
|
2203
|
+
return `PR opened; host auto-merge refused: revocation authority is not armed (${detail})`;
|
|
2204
|
+
}
|
|
2205
|
+
const mergeMethodCfg = autoMergeConfigValue(cfg, 'mergeMethod');
|
|
2206
|
+
const methodFlag = mergeMethodCfg === 'merge' ? '--merge' : mergeMethodCfg === 'rebase' ? '--rebase' : '--squash';
|
|
2207
|
+
const attempt = hostMergeGhPrMerge(repo, ['pr', 'merge', url, '--repo', nameWithOwner, methodFlag]);
|
|
2208
|
+
const freshAfterAttempt = viewPr(repo, url, { repo: nameWithOwner });
|
|
2209
|
+
const confirmedMerged = Boolean(freshAfterAttempt &&
|
|
2210
|
+
(freshAfterAttempt.state === 'MERGED' || freshAfterAttempt.mergedAt) &&
|
|
2211
|
+
freshAfterAttempt.headRefOid?.toLowerCase() === headOid.toLowerCase());
|
|
2212
|
+
if (confirmedMerged) {
|
|
2213
|
+
transitionHostMergeRevocation({
|
|
2214
|
+
identity,
|
|
2215
|
+
action: 'consume',
|
|
2216
|
+
operationId: consumeOpId,
|
|
2217
|
+
expectedSequence: armed.receipt.sequence,
|
|
2218
|
+
expectedReceiptDigest: armed.receipt.receiptDigest,
|
|
2219
|
+
now,
|
|
2220
|
+
});
|
|
2221
|
+
return `PR opened and merged via host auto-merge (gh pr merge)${attempt.ok ? '' : `; gh reported: ${attempt.detail}`}`;
|
|
2222
|
+
}
|
|
2223
|
+
// Not confirmed merged — leave the authority 'armed' (never consumed, never
|
|
2224
|
+
// revoked) so a later retry via the existing remoteHandoff recovery path
|
|
2225
|
+
// replays the SAME authority instead of minting a new one, and so
|
|
2226
|
+
// reconcileRemoteHandoffs() remains the single source of truth if the host
|
|
2227
|
+
// actually completes the merge after our read.
|
|
2228
|
+
return attempt.ok
|
|
2229
|
+
? 'PR opened; host auto-merge invoked gh pr merge but the merge was not confirmed on re-read; will reconcile'
|
|
2230
|
+
: `PR opened; host auto-merge refused: ${attempt.detail}`;
|
|
2231
|
+
}
|
|
1971
2232
|
/** Build the staging branch holding the proposal's diff, off the default branch.
|
|
1972
2233
|
* Returns the branch name on success, or null + detail on failure. */
|
|
1973
2234
|
function buildMergeBranch(repo, id, diff, base, expectedBaseHead, finalAuthority) {
|
|
@@ -2021,7 +2282,12 @@ function buildMergeBranch(repo, id, diff, base, expectedBaseHead, finalAuthority
|
|
|
2021
2282
|
const applyAuthorityFailure = finalAuthority?.();
|
|
2022
2283
|
if (applyAuthorityFailure)
|
|
2023
2284
|
throw new Error(applyAuthorityFailure);
|
|
2024
|
-
|
|
2285
|
+
// --3way: see the matching comment in verifyProposal()'s apply site —
|
|
2286
|
+
// same rationale, harmless here since expectedBaseHead (when supplied)
|
|
2287
|
+
// already guarantees this worktree's base is byte-identical to what was
|
|
2288
|
+
// verified, so 3-way and plain apply agree; it only helps callers that
|
|
2289
|
+
// omit expectedBaseHead.
|
|
2290
|
+
gitRun(tmpDir, ['apply', '--3way', '--index', patchFile]);
|
|
2025
2291
|
const commitAuthorityFailure = finalAuthority?.();
|
|
2026
2292
|
if (commitAuthorityFailure)
|
|
2027
2293
|
throw new Error(commitAuthorityFailure);
|
|
@@ -2467,11 +2733,12 @@ export async function autoMergeProposal(id, cfg, invocation = {}) {
|
|
|
2467
2733
|
let maxLinesForEvidence = 0;
|
|
2468
2734
|
// ── Gate 5.5 (M86): scope cap — only small, fully-bounded diffs auto-merge
|
|
2469
2735
|
// to main. Defaults are conservative; bounded config keys allow opt-in relaxation.
|
|
2470
|
-
//
|
|
2471
|
-
// maxRisk
|
|
2472
|
-
//
|
|
2473
|
-
//
|
|
2474
|
-
//
|
|
2736
|
+
// NOTE (was stale): this used to hard-require risk==='low' regardless of
|
|
2737
|
+
// cfg.maxRisk, silently overriding an operator's higher maxRisk for the
|
|
2738
|
+
// scope check even though Gate 5 above had already cleared it. M295 (below)
|
|
2739
|
+
// fixed that — the size cap now applies up to the CONFIGURED maxRisk, so
|
|
2740
|
+
// the two gates agree. File + line counts reuse the same diff parser as
|
|
2741
|
+
// classifyRisk so the two gates are consistent.
|
|
2475
2742
|
{
|
|
2476
2743
|
const autoMergeCfg = cfg.foundry
|
|
2477
2744
|
?.autoMerge;
|
|
@@ -2593,12 +2860,32 @@ export async function autoMergeProposal(id, cfg, invocation = {}) {
|
|
|
2593
2860
|
: 'npm';
|
|
2594
2861
|
if (scriptsForParity['test:invariants']) {
|
|
2595
2862
|
// Fast path: run only the invariant suite.
|
|
2863
|
+
//
|
|
2864
|
+
// Bounded retry (merge-pipeline reliability): test:invariants
|
|
2865
|
+
// includes concurrency/timing/disk-cap tests (h3/h5) that are
|
|
2866
|
+
// legitimately flaky under host contention — a second live daemon
|
|
2867
|
+
// tick, another sandboxed agent's test run, or transient disk
|
|
2868
|
+
// pressure can make ONE pass momentarily red for reasons that have
|
|
2869
|
+
// nothing to do with the proposal's diff. Unlike the main verify
|
|
2870
|
+
// path (Gate 6, M281/M293) there is no per-proposal baseline to
|
|
2871
|
+
// diff against here — this check runs against the UNPATCHED repo
|
|
2872
|
+
// tree, so a single flaky run previously meant 3 non-parity
|
|
2873
|
+
// refusals permanently auto-archived an otherwise-good proposal.
|
|
2874
|
+
// A genuinely broken invariant fails EVERY attempt and is still
|
|
2875
|
+
// refused — this only tolerates transient noise, never a real break.
|
|
2596
2876
|
const invariantCmd = {
|
|
2597
2877
|
kind: 'test',
|
|
2598
2878
|
cmd: [pm, 'run', 'test:invariants'],
|
|
2599
2879
|
};
|
|
2600
|
-
|
|
2601
|
-
|
|
2880
|
+
for (let attempt = 1; attempt <= SELF_EVAL_PARITY_RETRY_ATTEMPTS; attempt++) {
|
|
2881
|
+
const res = await runVerifyCommandAsync(invariantCmd, repo, parityCfg);
|
|
2882
|
+
if (res.ok)
|
|
2883
|
+
return true;
|
|
2884
|
+
if (attempt < SELF_EVAL_PARITY_RETRY_ATTEMPTS) {
|
|
2885
|
+
await new Promise((resolveDelay) => setTimeout(resolveDelay, SELF_EVAL_PARITY_RETRY_DELAY_MS));
|
|
2886
|
+
}
|
|
2887
|
+
}
|
|
2888
|
+
return false;
|
|
2602
2889
|
}
|
|
2603
2890
|
// Fallback: no targeted script — run all detected verify commands.
|
|
2604
2891
|
const cmds = detectVerifyCommands(repo, 'merge');
|
|
@@ -2608,8 +2895,18 @@ export async function autoMergeProposal(id, cfg, invocation = {}) {
|
|
|
2608
2895
|
return true;
|
|
2609
2896
|
}
|
|
2610
2897
|
for (const vc of cmds) {
|
|
2611
|
-
const
|
|
2612
|
-
|
|
2898
|
+
const attempts = vc.kind === 'test' ? SELF_EVAL_PARITY_RETRY_ATTEMPTS : 1;
|
|
2899
|
+
let ok = false;
|
|
2900
|
+
for (let attempt = 1; attempt <= attempts; attempt++) {
|
|
2901
|
+
const res = await runVerifyCommandAsync(vc, repo, parityCfg);
|
|
2902
|
+
ok = res.ok;
|
|
2903
|
+
if (ok)
|
|
2904
|
+
break;
|
|
2905
|
+
if (attempt < attempts) {
|
|
2906
|
+
await new Promise((resolveDelay) => setTimeout(resolveDelay, SELF_EVAL_PARITY_RETRY_DELAY_MS));
|
|
2907
|
+
}
|
|
2908
|
+
}
|
|
2909
|
+
if (!ok && vc.required !== false)
|
|
2613
2910
|
return false;
|
|
2614
2911
|
}
|
|
2615
2912
|
return true;
|
|
@@ -2773,7 +3070,15 @@ export async function autoMergeProposal(id, cfg, invocation = {}) {
|
|
|
2773
3070
|
engine: inlineJudgeEngine,
|
|
2774
3071
|
model: inlineJudgeEngine,
|
|
2775
3072
|
verdict: verdict.verdict,
|
|
2776
|
-
|
|
3073
|
+
// M507: a judgeFailure verdict is a synthetic fail-closed fallback, not
|
|
3074
|
+
// a considered judgment. It must be distinguishable in the ledger from a
|
|
3075
|
+
// real 'review' — conflating them is what made ~22% of judge calls look
|
|
3076
|
+
// like deliberate rejections. Mirrors automerge-pass.ts:243 so BOTH judge
|
|
3077
|
+
// paths record the same way. `wouldMerge` is always false on a failure,
|
|
3078
|
+
// so this never displaces the 'would-merge' marker.
|
|
3079
|
+
detail: verdict.judgeFailure
|
|
3080
|
+
? (verdict.judgeFailure === 'parse' ? 'judge-parse-failure' : 'judge-network-failure')
|
|
3081
|
+
: (verdict.wouldMerge && inlineReviewerIndependence.independent ? 'would-merge' : ''),
|
|
2777
3082
|
...(verdict.semanticEvents ? { semanticEvents: verdict.semanticEvents } : {}),
|
|
2778
3083
|
...(inlineAttestation !== undefined ? { judgeAttestation: inlineAttestation } : {}),
|
|
2779
3084
|
...(inlineAttestation !== undefined
|
|
@@ -3384,9 +3689,37 @@ export async function autoMergeProposal(id, cfg, invocation = {}) {
|
|
|
3384
3689
|
else if (!originStable) {
|
|
3385
3690
|
mergeNote = 'PR opened; host auto-merge refused because canonical GitHub origin changed';
|
|
3386
3691
|
}
|
|
3692
|
+
else if (!url) {
|
|
3693
|
+
mergeNote = 'PR opened; host auto-merge refused because no canonical PR URL is available';
|
|
3694
|
+
}
|
|
3387
3695
|
else {
|
|
3696
|
+
// Reaching this branch means every protected-handoff precondition was
|
|
3697
|
+
// confirmed (PR identity, base OID, live protection, canonical origin,
|
|
3698
|
+
// canonical URL). Record that BEFORE attempting the host merge, so the
|
|
3699
|
+
// strictHandoffUncertain reconciliation path below stays correct
|
|
3700
|
+
// regardless of what the merge attempt itself returns.
|
|
3388
3701
|
finalProtectedHandoffConfirmed = true;
|
|
3389
|
-
mergeNote =
|
|
3702
|
+
mergeNote = await attemptHostAutoMerge({
|
|
3703
|
+
repo,
|
|
3704
|
+
id,
|
|
3705
|
+
cfg,
|
|
3706
|
+
nameWithOwner: githubOrigin.nameWithOwner,
|
|
3707
|
+
repositoryId: latestProtection.evidence.repositoryId,
|
|
3708
|
+
url,
|
|
3709
|
+
base,
|
|
3710
|
+
baseOid: boundBaseHead,
|
|
3711
|
+
branch,
|
|
3712
|
+
headOid: stagedHead,
|
|
3713
|
+
observedPr,
|
|
3714
|
+
evidencePackDigest,
|
|
3715
|
+
verifierManifestDigest: requiredVerifierManifest?.digest ?? null,
|
|
3716
|
+
protectionPolicyDigest: latestProtection.evidence.policyHash,
|
|
3717
|
+
maxRisk,
|
|
3718
|
+
managerGateEnabled,
|
|
3719
|
+
allowSelfMerge,
|
|
3720
|
+
maxFiles: maxFilesForEvidence,
|
|
3721
|
+
maxLines: maxLinesForEvidence,
|
|
3722
|
+
});
|
|
3390
3723
|
}
|
|
3391
3724
|
}
|
|
3392
3725
|
const durableReason = `${mergeNote}${url ? `: ${url}` : ''} (remote handoff; awaiting host merge)`;
|