@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.
Files changed (123) hide show
  1. package/CHANGELOG.md +46 -2
  2. package/README.md +51 -38
  3. package/dist/build-identity.json +2 -2
  4. package/dist/cli/completions.js +1 -2
  5. package/dist/cli/completions.js.map +1 -1
  6. package/dist/cli/dashboard.js +5 -4
  7. package/dist/cli/dashboard.js.map +1 -1
  8. package/dist/cli/help.js +7 -10
  9. package/dist/cli/help.js.map +1 -1
  10. package/dist/cli/index.d.ts +0 -1
  11. package/dist/cli/index.js +0 -10
  12. package/dist/cli/index.js.map +1 -1
  13. package/dist/cli/serve.js +7 -3
  14. package/dist/cli/serve.js.map +1 -1
  15. package/dist/cli/setup.d.ts +5 -3
  16. package/dist/cli/setup.js +14 -7
  17. package/dist/cli/setup.js.map +1 -1
  18. package/dist/cli/update.js +3 -1
  19. package/dist/cli/update.js.map +1 -1
  20. package/dist/core/daemon/activation-permit.d.ts +33 -268
  21. package/dist/core/daemon/activation-permit.js +106 -1111
  22. package/dist/core/daemon/activation-permit.js.map +1 -1
  23. package/dist/core/daemon/loop.d.ts +1 -13
  24. package/dist/core/daemon/loop.js +10 -113
  25. package/dist/core/daemon/loop.js.map +1 -1
  26. package/dist/core/daemon/service-install-authority.d.ts +5 -1
  27. package/dist/core/daemon/service-install-authority.js +7 -20
  28. package/dist/core/daemon/service-install-authority.js.map +1 -1
  29. package/dist/core/fleet/automerge-pass.js +28 -77
  30. package/dist/core/fleet/automerge-pass.js.map +1 -1
  31. package/dist/core/fleet/decisions-ledger.js +1 -1
  32. package/dist/core/fleet/decisions-ledger.js.map +1 -1
  33. package/dist/core/fleet/judge-trace.js +2 -1
  34. package/dist/core/fleet/judge-trace.js.map +1 -1
  35. package/dist/core/fleet/manager.d.ts +6 -0
  36. package/dist/core/fleet/manager.js +35 -17
  37. package/dist/core/fleet/manager.js.map +1 -1
  38. package/dist/core/fleet/post-merge-credit-label.d.ts +10 -0
  39. package/dist/core/fleet/post-merge-credit-label.js +13 -0
  40. package/dist/core/fleet/post-merge-credit-label.js.map +1 -0
  41. package/dist/core/fleet/post-merge-credit.d.ts +20 -38
  42. package/dist/core/fleet/post-merge-credit.js +38 -39
  43. package/dist/core/fleet/post-merge-credit.js.map +1 -1
  44. package/dist/core/fleet/self-improve.d.ts +7 -7
  45. package/dist/core/fleet/self-improve.js +104 -20
  46. package/dist/core/fleet/self-improve.js.map +1 -1
  47. package/dist/core/fleet/status.js +0 -1
  48. package/dist/core/fleet/status.js.map +1 -1
  49. package/dist/core/foundry/automerge-scope-policy.d.ts +2 -2
  50. package/dist/core/foundry/automerge-scope-policy.js +2 -9
  51. package/dist/core/foundry/automerge-scope-policy.js.map +1 -1
  52. package/dist/core/goals/store.js +60 -20
  53. package/dist/core/goals/store.js.map +1 -1
  54. package/dist/core/inbox/apply.js +3 -5
  55. package/dist/core/inbox/apply.js.map +1 -1
  56. package/dist/core/inbox/merge.d.ts +0 -84
  57. package/dist/core/inbox/merge.js +23 -277
  58. package/dist/core/inbox/merge.js.map +1 -1
  59. package/dist/core/learn/reflect.js +10 -0
  60. package/dist/core/learn/reflect.js.map +1 -1
  61. package/dist/core/onboard.d.ts +2 -0
  62. package/dist/core/onboard.js +9 -3
  63. package/dist/core/onboard.js.map +1 -1
  64. package/dist/core/run/orchestrator.js +2 -0
  65. package/dist/core/run/orchestrator.js.map +1 -1
  66. package/dist/core/types.d.ts +5 -4
  67. package/dist/core/types.js.map +1 -1
  68. package/dist/core/web/api.d.ts +6 -0
  69. package/dist/core/web/api.js +69 -11
  70. package/dist/core/web/api.js.map +1 -1
  71. package/dist/core/web/public/app.js +5 -1
  72. package/dist/core/web/public/next/assets/index-CDHQraxS.css +1 -0
  73. package/dist/core/web/public/next/assets/index-zVKD9krw.js +69 -0
  74. package/dist/core/web/public/next/assets/{index-Ct0mf8n4.js.map → index-zVKD9krw.js.map} +1 -1
  75. package/dist/core/web/public/next/index.html +2 -2
  76. package/dist/core/web/read-session-revocations.d.ts +26 -0
  77. package/dist/core/web/read-session-revocations.js +48 -0
  78. package/dist/core/web/read-session-revocations.js.map +1 -0
  79. package/dist/core/web/server.js +73 -27
  80. package/dist/core/web/server.js.map +1 -1
  81. package/dist/core/web/static.d.ts +1 -1
  82. package/dist/core/web/static.js +54 -20
  83. package/dist/core/web/static.js.map +1 -1
  84. package/dist/release-dependency-inventory.json +1 -1
  85. package/docs/RUNTIME_ACTIVATION_AUTHORITY.md +0 -11
  86. package/node_modules/fast-uri/index.js +201 -48
  87. package/node_modules/fast-uri/lib/schemes.js +9 -4
  88. package/node_modules/fast-uri/lib/utils.js +388 -91
  89. package/node_modules/fast-uri/package.json +1 -1
  90. package/node_modules/fast-uri/test/component-safe-serialization.test.js +105 -0
  91. package/node_modules/fast-uri/test/equal.test.js +31 -3
  92. package/node_modules/fast-uri/test/fixtures/uri-js-parse.json +2 -0
  93. package/node_modules/fast-uri/test/ipv6-canonical.test.js +34 -0
  94. package/node_modules/fast-uri/test/ipv6-validation.test.js +90 -0
  95. package/node_modules/fast-uri/test/malformed-percent.test.js +77 -0
  96. package/node_modules/fast-uri/test/malformed-urn.test.js +61 -0
  97. package/node_modules/fast-uri/test/parse.test.js +7 -3
  98. package/node_modules/fast-uri/test/query-fragment-normalization.test.js +33 -0
  99. package/node_modules/fast-uri/test/reserved-path-normalization.test.js +109 -0
  100. package/node_modules/fast-uri/test/scheme-validation.test.js +124 -0
  101. package/node_modules/fast-uri/test/security-normalization.test.js +101 -0
  102. package/node_modules/fast-uri/test/security.test.js +75 -3
  103. package/node_modules/fast-uri/test/urn-full-input.test.js +29 -0
  104. package/node_modules/fast-uri/test/websocket-query-preservation.test.js +24 -0
  105. package/node_modules/qs/.github/THREAT_MODEL.md +3 -3
  106. package/node_modules/qs/CHANGELOG.md +26 -0
  107. package/node_modules/qs/README.md +19 -1
  108. package/node_modules/qs/dist/qs.js +25 -25
  109. package/node_modules/qs/eslint.config.mjs +1 -1
  110. package/node_modules/qs/lib/parse.js +16 -6
  111. package/node_modules/qs/lib/stringify.js +23 -8
  112. package/node_modules/qs/lib/utils.js +57 -11
  113. package/node_modules/qs/package.json +8 -7
  114. package/node_modules/qs/test/parse.js +419 -0
  115. package/node_modules/qs/test/stringify.js +317 -0
  116. package/node_modules/qs/test/utils.js +206 -0
  117. package/package.json +5 -4
  118. package/schema/config.schema.json +7 -7
  119. package/dist/cli/activation.d.ts +0 -41
  120. package/dist/cli/activation.js +0 -485
  121. package/dist/cli/activation.js.map +0 -1
  122. package/dist/core/web/public/next/assets/index-CY0U241s.css +0 -1
  123. package/dist/core/web/public/next/assets/index-Ct0mf8n4.js +0 -69
@@ -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 { createHash, randomBytes } from 'node:crypto';
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 infraLike = category === 'tool' || category === 'timeout' || category === 'infra';
641
- const advisories = infraLike
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, !infraLike);
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
- // 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.
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', '--3way', '--index', patchFile]);
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 (3-way fallback also failed — likely a real conflict, not just a stale base): ${err instanceof Error ? err.message : String(err)}`,
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
- // --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]);
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 === 'parse' ? 'judge-parse-failure' : 'judge-network-failure')
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
- // 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.
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)`;