@ashlr/hub 3.3.0 → 3.3.2
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 +46 -2
- package/README.md +51 -38
- package/dist/build-identity.json +2 -2
- package/dist/cli/completions.js +1 -2
- package/dist/cli/completions.js.map +1 -1
- package/dist/cli/dashboard.js +5 -4
- package/dist/cli/dashboard.js.map +1 -1
- package/dist/cli/help.js +7 -10
- package/dist/cli/help.js.map +1 -1
- package/dist/cli/index.d.ts +0 -1
- package/dist/cli/index.js +0 -10
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/serve.js +7 -3
- package/dist/cli/serve.js.map +1 -1
- package/dist/cli/setup.d.ts +5 -3
- package/dist/cli/setup.js +14 -7
- package/dist/cli/setup.js.map +1 -1
- package/dist/cli/update.js +3 -1
- package/dist/cli/update.js.map +1 -1
- package/dist/core/daemon/activation-permit.d.ts +33 -268
- package/dist/core/daemon/activation-permit.js +106 -1111
- package/dist/core/daemon/activation-permit.js.map +1 -1
- package/dist/core/daemon/loop.d.ts +1 -13
- package/dist/core/daemon/loop.js +10 -113
- package/dist/core/daemon/loop.js.map +1 -1
- package/dist/core/daemon/service-install-authority.d.ts +5 -1
- package/dist/core/daemon/service-install-authority.js +7 -20
- package/dist/core/daemon/service-install-authority.js.map +1 -1
- package/dist/core/fleet/automerge-pass.js +28 -77
- package/dist/core/fleet/automerge-pass.js.map +1 -1
- package/dist/core/fleet/decisions-ledger.js +1 -1
- package/dist/core/fleet/decisions-ledger.js.map +1 -1
- package/dist/core/fleet/judge-trace.js +2 -1
- package/dist/core/fleet/judge-trace.js.map +1 -1
- package/dist/core/fleet/manager.d.ts +6 -0
- package/dist/core/fleet/manager.js +35 -17
- package/dist/core/fleet/manager.js.map +1 -1
- package/dist/core/fleet/post-merge-credit-label.d.ts +10 -0
- package/dist/core/fleet/post-merge-credit-label.js +13 -0
- package/dist/core/fleet/post-merge-credit-label.js.map +1 -0
- package/dist/core/fleet/post-merge-credit.d.ts +20 -38
- package/dist/core/fleet/post-merge-credit.js +38 -39
- package/dist/core/fleet/post-merge-credit.js.map +1 -1
- package/dist/core/fleet/self-improve.d.ts +7 -7
- package/dist/core/fleet/self-improve.js +104 -20
- package/dist/core/fleet/self-improve.js.map +1 -1
- package/dist/core/fleet/status.js +0 -1
- package/dist/core/fleet/status.js.map +1 -1
- package/dist/core/foundry/automerge-scope-policy.d.ts +2 -2
- package/dist/core/foundry/automerge-scope-policy.js +2 -9
- package/dist/core/foundry/automerge-scope-policy.js.map +1 -1
- package/dist/core/goals/store.js +60 -20
- package/dist/core/goals/store.js.map +1 -1
- package/dist/core/inbox/apply.js +3 -5
- package/dist/core/inbox/apply.js.map +1 -1
- package/dist/core/inbox/merge.d.ts +0 -84
- package/dist/core/inbox/merge.js +23 -277
- package/dist/core/inbox/merge.js.map +1 -1
- package/dist/core/learn/reflect.js +10 -0
- package/dist/core/learn/reflect.js.map +1 -1
- package/dist/core/onboard.d.ts +2 -0
- package/dist/core/onboard.js +9 -3
- package/dist/core/onboard.js.map +1 -1
- package/dist/core/run/orchestrator.js +2 -0
- package/dist/core/run/orchestrator.js.map +1 -1
- package/dist/core/types.d.ts +5 -4
- package/dist/core/types.js.map +1 -1
- package/dist/core/web/api.d.ts +6 -0
- package/dist/core/web/api.js +69 -11
- package/dist/core/web/api.js.map +1 -1
- package/dist/core/web/public/app.js +5 -1
- package/dist/core/web/public/next/assets/index-CDHQraxS.css +1 -0
- package/dist/core/web/public/next/assets/index-zVKD9krw.js +69 -0
- package/dist/core/web/public/next/assets/{index-Ct0mf8n4.js.map → index-zVKD9krw.js.map} +1 -1
- package/dist/core/web/public/next/index.html +2 -2
- package/dist/core/web/read-session-revocations.d.ts +26 -0
- package/dist/core/web/read-session-revocations.js +48 -0
- package/dist/core/web/read-session-revocations.js.map +1 -0
- package/dist/core/web/server.js +73 -27
- package/dist/core/web/server.js.map +1 -1
- package/dist/core/web/static.d.ts +1 -1
- package/dist/core/web/static.js +54 -20
- package/dist/core/web/static.js.map +1 -1
- package/dist/release-dependency-inventory.json +1 -1
- package/docs/RUNTIME_ACTIVATION_AUTHORITY.md +0 -11
- package/node_modules/fast-uri/index.js +201 -48
- package/node_modules/fast-uri/lib/schemes.js +9 -4
- package/node_modules/fast-uri/lib/utils.js +388 -91
- package/node_modules/fast-uri/package.json +1 -1
- package/node_modules/fast-uri/test/component-safe-serialization.test.js +105 -0
- package/node_modules/fast-uri/test/equal.test.js +31 -3
- package/node_modules/fast-uri/test/fixtures/uri-js-parse.json +2 -0
- package/node_modules/fast-uri/test/ipv6-canonical.test.js +34 -0
- package/node_modules/fast-uri/test/ipv6-validation.test.js +90 -0
- package/node_modules/fast-uri/test/malformed-percent.test.js +77 -0
- package/node_modules/fast-uri/test/malformed-urn.test.js +61 -0
- package/node_modules/fast-uri/test/parse.test.js +7 -3
- package/node_modules/fast-uri/test/query-fragment-normalization.test.js +33 -0
- package/node_modules/fast-uri/test/reserved-path-normalization.test.js +109 -0
- package/node_modules/fast-uri/test/scheme-validation.test.js +124 -0
- package/node_modules/fast-uri/test/security-normalization.test.js +101 -0
- package/node_modules/fast-uri/test/security.test.js +75 -3
- package/node_modules/fast-uri/test/urn-full-input.test.js +29 -0
- package/node_modules/fast-uri/test/websocket-query-preservation.test.js +24 -0
- package/node_modules/qs/.github/THREAT_MODEL.md +3 -3
- package/node_modules/qs/CHANGELOG.md +26 -0
- package/node_modules/qs/README.md +19 -1
- package/node_modules/qs/dist/qs.js +25 -25
- package/node_modules/qs/eslint.config.mjs +1 -1
- package/node_modules/qs/lib/parse.js +16 -6
- package/node_modules/qs/lib/stringify.js +23 -8
- package/node_modules/qs/lib/utils.js +57 -11
- package/node_modules/qs/package.json +8 -7
- package/node_modules/qs/test/parse.js +419 -0
- package/node_modules/qs/test/stringify.js +317 -0
- package/node_modules/qs/test/utils.js +206 -0
- package/package.json +5 -4
- package/schema/config.schema.json +7 -7
- package/dist/cli/activation.d.ts +0 -41
- package/dist/cli/activation.js +0 -485
- package/dist/cli/activation.js.map +0 -1
- package/dist/core/web/public/next/assets/index-CY0U241s.css +0 -1
- package/dist/core/web/public/next/assets/index-Ct0mf8n4.js +0 -69
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 {
|
|
101
|
+
import { 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,7 +128,6 @@ 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';
|
|
132
131
|
function auditAutoMergeCanaryFailure(repo, sandboxId) {
|
|
133
132
|
try {
|
|
134
133
|
audit({
|
|
@@ -637,13 +636,13 @@ export function evaluateAutoMergeReadinessPreflight(proposal, cfg) {
|
|
|
637
636
|
// autoArchive. An infra-flavored failure must not permanently kill a
|
|
638
637
|
// proposal whose only crime was that the verifier was momentarily
|
|
639
638
|
// unavailable — it stays pending and is retried next pass.
|
|
640
|
-
const
|
|
641
|
-
const advisories =
|
|
639
|
+
const explicitCodeFailure = category === 'code';
|
|
640
|
+
const advisories = !explicitCodeFailure
|
|
642
641
|
? [
|
|
643
642
|
`verifier-unavailable: verification could not run to completion (${category}) — not counted toward auto-archive; will retry`,
|
|
644
643
|
]
|
|
645
644
|
: [];
|
|
646
|
-
return block(`known verification failure: proposal.verifyResult.passed is false${failed ? ` (${failed})` : ''}`, advisories,
|
|
645
|
+
return block(`known verification failure: proposal.verifyResult.passed is false${failed ? ` (${failed})` : ''}`, advisories, explicitCodeFailure);
|
|
647
646
|
}
|
|
648
647
|
const trustBasis = cfg.foundry?.['autoMerge']
|
|
649
648
|
? cfg.foundry['autoMerge']?.['trustBasis'] ?? 'tier'
|
|
@@ -1718,24 +1717,17 @@ export async function verifyProposal(proposal, cfg) {
|
|
|
1718
1717
|
}
|
|
1719
1718
|
// Apply the diff to the worktree ONLY AFTER capturing the base-derived
|
|
1720
1719
|
// command list, then run those (immutable) commands against the patched tree.
|
|
1721
|
-
//
|
|
1722
|
-
//
|
|
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.
|
|
1720
|
+
// Apply exactly to the verified index. Context drift is a refusal, not
|
|
1721
|
+
// authority to synthesize a three-way result that was never proposed.
|
|
1730
1722
|
patchFile = writeTmpFile(diff);
|
|
1731
1723
|
try {
|
|
1732
|
-
gitRun(tmpDir, ['apply', '--
|
|
1724
|
+
gitRun(tmpDir, ['apply', '--index', patchFile]);
|
|
1733
1725
|
}
|
|
1734
1726
|
catch (err) {
|
|
1735
1727
|
return {
|
|
1736
1728
|
ok: false,
|
|
1737
1729
|
ran: [],
|
|
1738
|
-
detail: `git apply failed in verify worktree
|
|
1730
|
+
detail: `git apply failed in verify worktree: ${err instanceof Error ? err.message : String(err)}`,
|
|
1739
1731
|
baseBranch: base,
|
|
1740
1732
|
baseHead,
|
|
1741
1733
|
};
|
|
@@ -2012,223 +2004,6 @@ function reconcileLocalMergeIntent(id, cfg) {
|
|
|
2012
2004
|
});
|
|
2013
2005
|
return { ok: true, merged: true, reason };
|
|
2014
2006
|
}
|
|
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
|
-
}
|
|
2232
2007
|
/** Build the staging branch holding the proposal's diff, off the default branch.
|
|
2233
2008
|
* Returns the branch name on success, or null + detail on failure. */
|
|
2234
2009
|
function buildMergeBranch(repo, id, diff, base, expectedBaseHead, finalAuthority) {
|
|
@@ -2282,12 +2057,7 @@ function buildMergeBranch(repo, id, diff, base, expectedBaseHead, finalAuthority
|
|
|
2282
2057
|
const applyAuthorityFailure = finalAuthority?.();
|
|
2283
2058
|
if (applyAuthorityFailure)
|
|
2284
2059
|
throw new Error(applyAuthorityFailure);
|
|
2285
|
-
|
|
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]);
|
|
2060
|
+
gitRun(tmpDir, ['apply', '--index', patchFile]);
|
|
2291
2061
|
const commitAuthorityFailure = finalAuthority?.();
|
|
2292
2062
|
if (commitAuthorityFailure)
|
|
2293
2063
|
throw new Error(commitAuthorityFailure);
|
|
@@ -3038,7 +2808,7 @@ export async function autoMergeProposal(id, cfg, invocation = {}) {
|
|
|
3038
2808
|
let inlineAttestation;
|
|
3039
2809
|
const ts = new Date().toISOString();
|
|
3040
2810
|
const inlineReviewerIndependence = evaluateReviewerIndependence(proposal, inlineJudgeEngine);
|
|
3041
|
-
if (verdict.verdict === 'ship' && verdict.wouldMerge === true && isFrontierJudge(inlineJudgeEngine) &&
|
|
2811
|
+
if (verdict.considered === true && verdict.verdict === 'ship' && verdict.wouldMerge === true && isFrontierJudge(inlineJudgeEngine) &&
|
|
3042
2812
|
inlineReviewerIndependence.independent) {
|
|
3043
2813
|
try {
|
|
3044
2814
|
const { signJudgeAttestation: signAtt, hashDiff: hd } = await import('../foundry/provenance.js');
|
|
@@ -3069,15 +2839,15 @@ export async function autoMergeProposal(id, cfg, invocation = {}) {
|
|
|
3069
2839
|
action: 'judged',
|
|
3070
2840
|
engine: inlineJudgeEngine,
|
|
3071
2841
|
model: inlineJudgeEngine,
|
|
3072
|
-
verdict: verdict.verdict,
|
|
2842
|
+
verdict: verdict.considered === true ? verdict.verdict : 'review',
|
|
3073
2843
|
// M507: a judgeFailure verdict is a synthetic fail-closed fallback, not
|
|
3074
2844
|
// a considered judgment. It must be distinguishable in the ledger from a
|
|
3075
2845
|
// real 'review' — conflating them is what made ~22% of judge calls look
|
|
3076
2846
|
// like deliberate rejections. Mirrors automerge-pass.ts:243 so BOTH judge
|
|
3077
2847
|
// paths record the same way. `wouldMerge` is always false on a failure,
|
|
3078
2848
|
// so this never displaces the 'would-merge' marker.
|
|
3079
|
-
detail: verdict.judgeFailure
|
|
3080
|
-
? (verdict.judgeFailure === '
|
|
2849
|
+
detail: verdict.judgeFailure || verdict.considered !== true
|
|
2850
|
+
? (verdict.judgeFailure === 'network' ? 'judge-network-failure' : 'judge-parse-failure')
|
|
3081
2851
|
: (verdict.wouldMerge && inlineReviewerIndependence.independent ? 'would-merge' : ''),
|
|
3082
2852
|
...(verdict.semanticEvents ? { semanticEvents: verdict.semanticEvents } : {}),
|
|
3083
2853
|
...(inlineAttestation !== undefined ? { judgeAttestation: inlineAttestation } : {}),
|
|
@@ -3085,14 +2855,15 @@ export async function autoMergeProposal(id, cfg, invocation = {}) {
|
|
|
3085
2855
|
? { judgeAttestationIssuedAt: ts, judgeAttestationIntent: 'would-merge' }
|
|
3086
2856
|
: {}),
|
|
3087
2857
|
});
|
|
3088
|
-
if (verdict.verdict === 'ship' && verdict.wouldMerge === true &&
|
|
2858
|
+
if (verdict.considered === true && verdict.verdict === 'ship' && verdict.wouldMerge === true &&
|
|
3089
2859
|
!inlineReviewerIndependence.independent) {
|
|
3090
2860
|
return refuse(`manager quality gate: ${inlineReviewerIndependence.reason}`, repo);
|
|
3091
2861
|
}
|
|
2862
|
+
const consideredVerdict = verdict.considered === true;
|
|
3092
2863
|
managerVerdict = {
|
|
3093
|
-
verdict: verdict.verdict,
|
|
3094
|
-
wouldMerge: verdict.wouldMerge,
|
|
3095
|
-
reasonCode: judgeDecisionReasonCode(verdict.verdict, verdict.wouldMerge && inlineReviewerIndependence.independent),
|
|
2864
|
+
verdict: consideredVerdict ? verdict.verdict : 'review',
|
|
2865
|
+
wouldMerge: consideredVerdict ? verdict.wouldMerge : false,
|
|
2866
|
+
reasonCode: judgeDecisionReasonCode(consideredVerdict ? verdict.verdict : 'review', consideredVerdict && verdict.wouldMerge && inlineReviewerIndependence.independent),
|
|
3096
2867
|
};
|
|
3097
2868
|
}
|
|
3098
2869
|
catch {
|
|
@@ -3689,37 +3460,12 @@ export async function autoMergeProposal(id, cfg, invocation = {}) {
|
|
|
3689
3460
|
else if (!originStable) {
|
|
3690
3461
|
mergeNote = 'PR opened; host auto-merge refused because canonical GitHub origin changed';
|
|
3691
3462
|
}
|
|
3692
|
-
else if (!url) {
|
|
3693
|
-
mergeNote = 'PR opened; host auto-merge refused because no canonical PR URL is available';
|
|
3694
|
-
}
|
|
3695
3463
|
else {
|
|
3696
|
-
//
|
|
3697
|
-
//
|
|
3698
|
-
//
|
|
3699
|
-
|
|
3700
|
-
|
|
3701
|
-
finalProtectedHandoffConfirmed = true;
|
|
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
|
-
});
|
|
3464
|
+
// Protected PR handoff is the terminal autonomous action. A host
|
|
3465
|
+
// merge requires separate authority and remains intentionally
|
|
3466
|
+
// unavailable in this release.
|
|
3467
|
+
finalProtectedHandoffConfirmed = Boolean(url);
|
|
3468
|
+
mergeNote = 'PR opened; host auto-merge is disabled until durable revocation is available';
|
|
3723
3469
|
}
|
|
3724
3470
|
}
|
|
3725
3471
|
const durableReason = `${mergeNote}${url ? `: ${url}` : ''} (remote handoff; awaiting host merge)`;
|