From 692bb5963387f6302283249bebdd3431e327638d Mon Sep 17 00:00:00 2001 From: Codex Automation Date: Sat, 1 Aug 2026 18:10:50 -0500 Subject: [PATCH 1/3] feat(sync): centralize generated delivery handoff --- .../__tests__/sync-pr-merge-contract.test.js | 34 ++++++++ .github/scripts/sync_dependency_campaign.js | 2 + .github/scripts/sync_pr_merge_contract.js | 77 ++++++++++++++++- .github/workflows/maint-71-merge-sync-prs.yml | 82 ++++++++++--------- docs/ops/CONSUMER_REPO_MAINTENANCE.md | 8 ++ docs/ops/SYNC_DEPENDENCY_CAMPAIGN.md | 24 ++++++ 6 files changed, 187 insertions(+), 40 deletions(-) create mode 100644 docs/ops/SYNC_DEPENDENCY_CAMPAIGN.md diff --git a/.github/scripts/__tests__/sync-pr-merge-contract.test.js b/.github/scripts/__tests__/sync-pr-merge-contract.test.js index 5630f32cd..1b601cc59 100644 --- a/.github/scripts/__tests__/sync-pr-merge-contract.test.js +++ b/.github/scripts/__tests__/sync-pr-merge-contract.test.js @@ -5,9 +5,13 @@ const assert = require('node:assert/strict'); const { buildMarkdownSummary, + buildDeliveryHandoff, buildMergeReport, + classifyGeneratedPr, classifySyncPrChecks, collectDeletableSyncBranches, + generatedDeliveryLane, + isTrustedGeneratedDeliveryPr, isTrustedSyncPr, normalizeSyncHash, parseBooleanInput, @@ -68,6 +72,21 @@ test('isTrustedSyncPr requires the configured actor and sync branch', () => { assert.equal(isTrustedSyncPr({ ...trusted, user: { login: 'untrusted' } }, ['stranske']), false); }); +test('generated delivery classification gives sync and dev-tool lanes identical check and review dispositions', () => { + const record = ''; + const sync = { ...pr(1, 'sync/workflows-current', '2026-04-25T01:00:00Z'), body: record, user: { login: 'stranske' } }; + const devTool = { ...pr(2, 'deps/sync-dev-versions-20260801', '2026-04-25T01:00:00Z'), body: record, user: { login: 'stranske' } }; + + assert.equal(generatedDeliveryLane(sync.head.ref), 'sync'); + assert.equal(generatedDeliveryLane(devTool.head.ref), 'dev-tool-sync'); + assert.equal(isTrustedGeneratedDeliveryPr(devTool, ['stranske']), true); + for (const candidate of [sync, devTool]) { + assert.equal(classifyGeneratedPr({ pr: candidate, now: '2026-08-01T00:00:00Z' }).disposition, 'current'); + assert.equal(classifyGeneratedPr({ pr: candidate, activeReviewThreadCount: 1, now: '2026-08-01T00:00:00Z' }).disposition, 'review-blocked'); + assert.equal(classifyGeneratedPr({ pr: candidate, checkState: { status: 'checks_failed' }, now: '2026-08-01T00:00:00Z' }).disposition, 'repo-local-failure'); + } +}); + test('selectActiveSyncPr honors target hash instead of newest PR', () => { const selection = selectActiveSyncPr( [ @@ -135,6 +154,7 @@ test('buildMergeReport provides machine-readable summary counts', () => { branch_delete_failed: 0, checks_failed: 0, checks_pending: 0, + review_blocked: 0, ready: 0, dry_run_merge: 1, merge_blocked_runtime_ac: 0, @@ -143,6 +163,20 @@ test('buildMergeReport provides machine-readable summary counts', () => { delivery_contract_blocked: 0, error: 0, }); + assert.deepEqual(report.handoff_records, []); +}); + +test('buildDeliveryHandoff preserves the restart fields for a generated PR', () => { + assert.deepEqual(buildDeliveryHandoff({ + owner: 'stranske', repo: 'Ready', pr: 11, branch: 'deps/sync-dev-versions-20260801', + head_sha: 'abc', delivery_generation: 'g2', delivery_disposition: 'review-blocked', + blocker_owner: 'closer', next_command: 'resolve-active-review-threads', + }), { + schema: 'workflows-generated-delivery-handoff/v1', repository: 'stranske/Ready', pr: 11, + branch: 'deps/sync-dev-versions-20260801', head_sha: 'abc', delivery_generation: 'g2', + lane: 'dev-tool-sync', disposition: 'review-blocked', blocker_owner: 'closer', + next_command: 'resolve-active-review-threads', + }); }); test('collectDeletableSyncBranches keeps open PR branches and non-sync branches', () => { diff --git a/.github/scripts/sync_dependency_campaign.js b/.github/scripts/sync_dependency_campaign.js index f29094cd8..f5a22bfe7 100644 --- a/.github/scripts/sync_dependency_campaign.js +++ b/.github/scripts/sync_dependency_campaign.js @@ -14,6 +14,7 @@ const LABEL_CAMPAIGN = 'campaign:sync-dependabot'; const LABEL_ACTIVE = 'campaign:active'; const LABEL_NEEDS_LOCAL_CODEX = 'campaign:needs-local-codex'; const SYNC_BRANCH_PREFIX = 'sync/workflows-'; +const DEV_TOOL_SYNC_BRANCH_PREFIX = 'deps/sync-dev-versions-'; const DEFAULT_MAX_ATTEMPTS = 3; const DEFAULT_MAX_RETAINED_ITEMS = 120; const DEFAULT_MAX_SOURCE_REVIEW_HISTORY = 80; @@ -92,6 +93,7 @@ function isSyncPullRequest(pr = {}) { const labels = labelsForPullRequest(pr).map((label) => label.toLowerCase()); return ( headRef.startsWith(SYNC_BRANCH_PREFIX) || + headRef.startsWith(DEV_TOOL_SYNC_BRANCH_PREFIX) || title.startsWith('chore: sync workflow templates') || labels.includes('sync') || body.includes('workflows-sync-lifecycle') diff --git a/.github/scripts/sync_pr_merge_contract.js b/.github/scripts/sync_pr_merge_contract.js index 401a7fb86..6bbf422ad 100644 --- a/.github/scripts/sync_pr_merge_contract.js +++ b/.github/scripts/sync_pr_merge_contract.js @@ -2,6 +2,8 @@ const REPORT_SCHEMA = 'workflows-sync-pr-merge/v1'; const SYNC_BRANCH_PREFIX = 'sync/workflows-'; +const DEV_TOOL_SYNC_BRANCH_PREFIX = 'deps/sync-dev-versions-'; +const GENERATED_DELIVERY_BRANCH_PREFIXES = [SYNC_BRANCH_PREFIX, DEV_TOOL_SYNC_BRANCH_PREFIX]; const { parseDeliveryRecord, mergeEligibility } = require('./sync_pr_lease_contract'); function normalizeSyncHash(value) { @@ -28,11 +30,55 @@ function isSyncBranchName(value) { return branchNameFromRef(value).startsWith(SYNC_BRANCH_PREFIX); } +function generatedDeliveryLane(value) { + const branch = branchNameFromRef(value); + if (branch.startsWith(SYNC_BRANCH_PREFIX)) return 'sync'; + if (branch.startsWith(DEV_TOOL_SYNC_BRANCH_PREFIX)) return 'dev-tool-sync'; + return ''; +} + +function isGeneratedDeliveryBranchName(value) { + return Boolean(generatedDeliveryLane(value)); +} + function isTrustedSyncPr(pr, trustedActors = []) { const actor = String(pr?.user?.login || '').trim(); return isSyncBranchName(pr?.head?.ref) && new Set(trustedActors).has(actor); } +function isTrustedGeneratedDeliveryPr(pr, trustedActors = []) { + const actor = String(pr?.user?.login || '').trim(); + return isGeneratedDeliveryBranchName(pr?.head?.ref) && new Set(trustedActors).has(actor); +} + +function classifyGeneratedPr({ pr = {}, checkState = {}, activeReviewThreadCount = 0, now } = {}) { + const record = parseDeliveryRecord(pr.body || ''); + const lane = generatedDeliveryLane(pr?.head?.ref || pr?.headRefName); + if (!lane) return { disposition: 'owner-decision', blocker_owner: 'source', next_command: '' }; + if (!record) return { + disposition: 'owner-decision', + blocker_owner: 'source', + next_command: 'attach-or-infer-delivery-record', + }; + const eligibility = mergeEligibility(record, { now }); + if (eligibility.reason === 'lease_expired') { + return { disposition: 'expired', blocker_owner: 'maint-71', next_command: 'close-expired-delivery' }; + } + if (!eligibility.eligible) { + return { disposition: 'superseded', blocker_owner: 'maint-71', next_command: 'close-or-refresh-delivery' }; + } + if (Number(activeReviewThreadCount) > 0) { + return { disposition: 'review-blocked', blocker_owner: 'closer', next_command: 'resolve-active-review-threads' }; + } + if (checkState.status === 'checks_pending') { + return { disposition: 'awaiting-checks', blocker_owner: 'ci', next_command: 'await-required-checks' }; + } + if (checkState.status === 'checks_failed') { + return { disposition: 'repo-local-failure', blocker_owner: 'repo', next_command: 'repair-required-checks' }; + } + return { disposition: 'current', blocker_owner: 'maint-71', next_command: 'merge-current-delivery' }; +} + function parseBooleanInput(value, defaultValue = false) { if (value === undefined || value === null || String(value).trim() === '') { return Boolean(defaultValue); @@ -55,17 +101,17 @@ function collectDeletableSyncBranches({ const openBranches = new Set( (openPullRequests || []) .map((pr) => branchNameFromRef(pr?.head?.ref || pr?.headRefName || pr?.branch)) - .filter(isSyncBranchName), + .filter(isGeneratedDeliveryBranchName), ); const closedBranches = new Set( (closedPullRequests || []) .map((pr) => branchNameFromRef(pr?.head?.ref || pr?.headRefName || pr?.branch)) - .filter(isSyncBranchName), + .filter(isGeneratedDeliveryBranchName), ); return (branches || []) .map((branch) => branchNameFromRef(branch?.name || branch?.ref || branch)) - .filter(isSyncBranchName) + .filter(isGeneratedDeliveryBranchName) .filter((branch) => !openBranches.has(branch)) .filter((branch) => closedBranches.has(branch)) .sort(); @@ -203,6 +249,7 @@ function summarizeResults(results) { branch_delete_failed: 0, checks_failed: 0, checks_pending: 0, + review_blocked: 0, ready: 0, dry_run_merge: 0, merge_blocked_runtime_ac: 0, @@ -219,6 +266,22 @@ function summarizeResults(results) { return counts; } +function buildDeliveryHandoff(result = {}) { + if (!result.pr) return null; + return { + schema: 'workflows-generated-delivery-handoff/v1', + repository: `${result.owner || ''}/${result.repo || ''}`.replace(/^\//, ''), + pr: Number(result.pr), + branch: branchNameFromRef(result.branch), + head_sha: String(result.head_sha || ''), + delivery_generation: String(result.delivery_generation || ''), + lane: generatedDeliveryLane(result.branch), + disposition: String(result.delivery_disposition || result.status || ''), + blocker_owner: String(result.blocker_owner || ''), + next_command: String(result.next_command || ''), + }; +} + function buildMergeReport({ results = [], registeredRepos = [], @@ -244,6 +307,7 @@ function buildMergeReport({ }, summary: summarizeResults(results), results, + handoff_records: (results || []).map(buildDeliveryHandoff).filter(Boolean), }; } @@ -278,10 +342,16 @@ function buildMarkdownSummary(report) { module.exports = { REPORT_SCHEMA, SYNC_BRANCH_PREFIX, + DEV_TOOL_SYNC_BRANCH_PREFIX, + GENERATED_DELIVERY_BRANCH_PREFIXES, branchNameFromRef, + classifyGeneratedPr, classifySyncPrChecks, collectDeletableSyncBranches, + generatedDeliveryLane, + isGeneratedDeliveryBranchName, isSyncBranchName, + isTrustedGeneratedDeliveryPr, isTrustedSyncPr, normalizeSyncHash, syncBranchForHash, @@ -292,5 +362,6 @@ module.exports = { selectMergeEligibleSyncPr, summarizeResults, buildMergeReport, + buildDeliveryHandoff, buildMarkdownSummary, }; diff --git a/.github/workflows/maint-71-merge-sync-prs.yml b/.github/workflows/maint-71-merge-sync-prs.yml index fa3f22f0b..3f341a32e 100644 --- a/.github/workflows/maint-71-merge-sync-prs.yml +++ b/.github/workflows/maint-71-merge-sync-prs.yml @@ -134,8 +134,10 @@ jobs: const { buildMarkdownSummary, buildMergeReport, + classifyGeneratedPr, classifySyncPrChecks, collectDeletableSyncBranches, + generatedDeliveryLane, normalizeSyncHash, parseBooleanInput, isTrustedSyncPr, @@ -564,6 +566,29 @@ jobs: requiredContexts.size > 0 ? 'required-contexts' : 'denylist-fallback'; const failedChecks = classification.failed; const pendingChecks = classification.pending; + const activeReviewThreads = await activeReviewThreadCount( + owner, + repo, + pr.number, + ); + const deliveryState = classifyGeneratedPr({ + pr, + checkState: classification, + activeReviewThreadCount: activeReviewThreads, + now: new Date().toISOString(), + }); + const deliveryContext = { + owner, + repo, + pr: pr.number, + branch: pr.head.ref, + head_sha: pr.head.sha, + delivery_generation: selection.deliveryRecord?.generation || '', + delivery_lane: generatedDeliveryLane(pr.head.ref), + delivery_disposition: deliveryState.disposition, + blocker_owner: deliveryState.blocker_owner, + next_command: deliveryState.next_command, + }; if (metadata?.sync_phase === 'canary' && metadata?.plan_id) { canaryEvidence.push({ @@ -572,7 +597,7 @@ jobs: pr: pr.number, required_check_state: classification.status === 'ready' ? 'success' : classification.status, - active_review_thread_count: await activeReviewThreadCount(owner, repo, pr.number), + active_review_thread_count: activeReviewThreads, }); } @@ -581,14 +606,21 @@ jobs: `${failedChecks.length} failed, ${pendingChecks.length} pending`, ); + if (deliveryState.disposition === 'review-blocked') { + console.log(`Active review threads block merge: ${activeReviewThreads}`); + results.push({ + ...deliveryContext, + status: 'review_blocked', + active_review_thread_count: activeReviewThreads, + }); + continue; + } + if (classification.status === 'checks_failed') { console.log('Failed checks:'); failedChecks.forEach(c => console.log(` - ${c.name}: ${c.conclusion}`)); results.push({ - owner, - repo, - pr: pr.number, - branch: pr.head.ref, + ...deliveryContext, status: 'checks_failed', failed_checks: failedChecks.map((check) => ({ name: check.name, @@ -602,10 +634,7 @@ jobs: if (classification.status === 'checks_pending') { console.log('Waiting for checks to complete'); results.push({ - owner, - repo, - pr: pr.number, - branch: pr.head.ref, + ...deliveryContext, status: 'checks_pending', pending_checks: pendingChecks.map((check) => ({ name: check.name, @@ -620,10 +649,7 @@ jobs: if (!autoMerge) { console.log('✓ Ready to merge (auto-merge disabled)'); results.push({ - owner, - repo, - pr: pr.number, - branch: pr.head.ref, + ...deliveryContext, status: 'ready', }); continue; @@ -632,10 +658,7 @@ jobs: if (dryRun) { console.log('✓ Would merge (dry run)'); results.push({ - owner, - repo, - pr: pr.number, - branch: pr.head.ref, + ...deliveryContext, status: 'dry_run_merge', }); continue; @@ -656,10 +679,7 @@ jobs: const message = String(guardError?.message || guardError); console.log(`Runtime AC merge guard blocked PR #${pr.number}: ${message}`); results.push({ - owner, - repo, - pr: pr.number, - branch: pr.head.ref, + ...deliveryContext, status: 'merge_blocked_runtime_ac', error: message, }); @@ -709,38 +729,26 @@ jobs: })); console.log('✓ Branch deleted'); results.push({ - owner, - repo, - pr: pr.number, - branch: pr.head.ref, + ...deliveryContext, status: 'branch_deleted', }); } catch (e) { console.log(`⚠ Could not delete branch: ${e.message}`); results.push({ - owner, - repo, - pr: pr.number, - branch: pr.head.ref, + ...deliveryContext, status: 'branch_delete_failed', error: e.message, }); } results.push({ - owner, - repo, - pr: pr.number, - branch: pr.head.ref, + ...deliveryContext, status: 'merged', }); } catch (e) { console.log(`✗ Merge failed: ${e.message}`); results.push({ - owner, - repo, - pr: pr.number, - branch: pr.head.ref, + ...deliveryContext, status: 'merge_failed', error: e.message, }); diff --git a/docs/ops/CONSUMER_REPO_MAINTENANCE.md b/docs/ops/CONSUMER_REPO_MAINTENANCE.md index 868e1203c..40c3043ef 100644 --- a/docs/ops/CONSUMER_REPO_MAINTENANCE.md +++ b/docs/ops/CONSUMER_REPO_MAINTENANCE.md @@ -441,3 +441,11 @@ After fixing a template bug: | Date | Change | |------|--------| | 2025-12-27 | Initial document based on trip-planner/Manager-Database setup learnings | +# Generated delivery ownership + +For dependency and consumer-sync delivery, the campaign issue is durable and +each generated PR is a leased attempt. Maint 71 alone decides merge or close +disposition for `sync/workflows-*` and `deps/sync-dev-versions-*`; operators +and local watchers must consume its recorded owner/next-command handoff rather +than reimplementing that policy. See +[`SYNC_DEPENDENCY_CAMPAIGN.md`](SYNC_DEPENDENCY_CAMPAIGN.md). diff --git a/docs/ops/SYNC_DEPENDENCY_CAMPAIGN.md b/docs/ops/SYNC_DEPENDENCY_CAMPAIGN.md new file mode 100644 index 000000000..ac90276ec --- /dev/null +++ b/docs/ops/SYNC_DEPENDENCY_CAMPAIGN.md @@ -0,0 +1,24 @@ +# Sync/Dependency Campaign Contract + +The durable coordination surface for generated dependency and consumer-sync +work is the campaign issue, not an individual generated pull request. A pull +request is a leased delivery attempt and must carry the +`sync-pr-delivery-record/v1` marker with its durable issue, plan, generation, +repository, desired tree hash, source commit, expiry, and lineage. + +Maint 71 is the sole merge/close reconciler. It applies the same contract to +both `sync/workflows-*` consumer-sync branches and +`deps/sync-dev-versions-*` shared dev-tool propagation branches: + +- current attempts with passing required checks and no active review threads + may be merged; +- pending checks, active reviews, and repository-local failures retain a + precise owner and next command; +- expired or superseded attempts are closed rather than revived; and +- legacy attempts without a record require an explicit, one-time provenance + decision before any merge. + +Maint 82 owns the durable campaign state and only requests local agent work +when a materially changed exception fingerprint appears. Timestamps alone do +not constitute new work. Local watchers consume the normalized handoff record +and do not independently decide merge or close disposition. From 0c96e097f6b67bc54f0ec61ecdd26eba3eeec8aa Mon Sep 17 00:00:00 2001 From: Codex Automation Date: Sat, 1 Aug 2026 18:27:35 -0500 Subject: [PATCH 2/3] fix(sync): harden generated delivery reconciliation --- .../__tests__/sync-pr-merge-contract.test.js | 5 +++++ .github/scripts/sync_pr_merge_contract.js | 13 ++++++++++--- .github/workflows/maint-71-merge-sync-prs.yml | 4 ++-- docs/ops/CONSUMER_REPO_MAINTENANCE.md | 3 ++- docs/ops/SYNC_DEPENDENCY_CAMPAIGN.md | 8 ++++---- .../workflows/test_workflow_agents_consolidation.py | 3 +++ 6 files changed, 26 insertions(+), 10 deletions(-) diff --git a/.github/scripts/__tests__/sync-pr-merge-contract.test.js b/.github/scripts/__tests__/sync-pr-merge-contract.test.js index 1b601cc59..db98e370e 100644 --- a/.github/scripts/__tests__/sync-pr-merge-contract.test.js +++ b/.github/scripts/__tests__/sync-pr-merge-contract.test.js @@ -83,6 +83,7 @@ test('generated delivery classification gives sync and dev-tool lanes identical for (const candidate of [sync, devTool]) { assert.equal(classifyGeneratedPr({ pr: candidate, now: '2026-08-01T00:00:00Z' }).disposition, 'current'); assert.equal(classifyGeneratedPr({ pr: candidate, activeReviewThreadCount: 1, now: '2026-08-01T00:00:00Z' }).disposition, 'review-blocked'); + assert.equal(classifyGeneratedPr({ pr: candidate, activeReviewThreadCount: -1, now: '2026-08-01T00:00:00Z' }).next_command, 'retry-review-thread-query'); assert.equal(classifyGeneratedPr({ pr: candidate, checkState: { status: 'checks_failed' }, now: '2026-08-01T00:00:00Z' }).disposition, 'repo-local-failure'); } }); @@ -179,6 +180,10 @@ test('buildDeliveryHandoff preserves the restart fields for a generated PR', () }); }); +test('buildDeliveryHandoff rejects results that lack required restart fields', () => { + assert.equal(buildDeliveryHandoff({ owner: 'stranske', repo: 'Ready', pr: 11, branch: 'sync/workflows-current' }), null); +}); + test('collectDeletableSyncBranches keeps open PR branches and non-sync branches', () => { const branches = [ { name: 'sync/workflows-old' }, diff --git a/.github/scripts/sync_pr_merge_contract.js b/.github/scripts/sync_pr_merge_contract.js index 6bbf422ad..e4c6a42f6 100644 --- a/.github/scripts/sync_pr_merge_contract.js +++ b/.github/scripts/sync_pr_merge_contract.js @@ -67,7 +67,11 @@ function classifyGeneratedPr({ pr = {}, checkState = {}, activeReviewThreadCount if (!eligibility.eligible) { return { disposition: 'superseded', blocker_owner: 'maint-71', next_command: 'close-or-refresh-delivery' }; } - if (Number(activeReviewThreadCount) > 0) { + const reviewThreadCount = Number(activeReviewThreadCount); + if (!Number.isFinite(reviewThreadCount) || reviewThreadCount < 0) { + return { disposition: 'review-blocked', blocker_owner: 'closer', next_command: 'retry-review-thread-query' }; + } + if (reviewThreadCount > 0) { return { disposition: 'review-blocked', blocker_owner: 'closer', next_command: 'resolve-active-review-threads' }; } if (checkState.status === 'checks_pending') { @@ -268,13 +272,16 @@ function summarizeResults(results) { function buildDeliveryHandoff(result = {}) { if (!result.pr) return null; + const headSha = String(result.head_sha || ''); + const deliveryGeneration = String(result.delivery_generation || ''); + if (!headSha || !deliveryGeneration) return null; return { schema: 'workflows-generated-delivery-handoff/v1', repository: `${result.owner || ''}/${result.repo || ''}`.replace(/^\//, ''), pr: Number(result.pr), branch: branchNameFromRef(result.branch), - head_sha: String(result.head_sha || ''), - delivery_generation: String(result.delivery_generation || ''), + head_sha: headSha, + delivery_generation: deliveryGeneration, lane: generatedDeliveryLane(result.branch), disposition: String(result.delivery_disposition || result.status || ''), blocker_owner: String(result.blocker_owner || ''), diff --git a/.github/workflows/maint-71-merge-sync-prs.yml b/.github/workflows/maint-71-merge-sync-prs.yml index 3f341a32e..a438ffa8a 100644 --- a/.github/workflows/maint-71-merge-sync-prs.yml +++ b/.github/workflows/maint-71-merge-sync-prs.yml @@ -140,7 +140,7 @@ jobs: generatedDeliveryLane, normalizeSyncHash, parseBooleanInput, - isTrustedSyncPr, + isTrustedGeneratedDeliveryPr, selectMergeEligibleSyncPr, selectSyncPrGatingChecks, } = require('./.github/scripts/sync_pr_merge_contract.js'); @@ -332,7 +332,7 @@ jobs: per_page: 20 })); - const syncPRs = prs.filter((pr) => isTrustedSyncPr(pr, trustedSyncActors)); + const syncPRs = prs.filter((pr) => isTrustedGeneratedDeliveryPr(pr, trustedSyncActors)); if (cleanupBranches) { try { diff --git a/docs/ops/CONSUMER_REPO_MAINTENANCE.md b/docs/ops/CONSUMER_REPO_MAINTENANCE.md index 40c3043ef..c2be9bf51 100644 --- a/docs/ops/CONSUMER_REPO_MAINTENANCE.md +++ b/docs/ops/CONSUMER_REPO_MAINTENANCE.md @@ -441,7 +441,8 @@ After fixing a template bug: | Date | Change | |------|--------| | 2025-12-27 | Initial document based on trip-planner/Manager-Database setup learnings | -# Generated delivery ownership + +## Generated delivery ownership For dependency and consumer-sync delivery, the campaign issue is durable and each generated PR is a leased attempt. Maint 71 alone decides merge or close diff --git a/docs/ops/SYNC_DEPENDENCY_CAMPAIGN.md b/docs/ops/SYNC_DEPENDENCY_CAMPAIGN.md index ac90276ec..7b0a6fb83 100644 --- a/docs/ops/SYNC_DEPENDENCY_CAMPAIGN.md +++ b/docs/ops/SYNC_DEPENDENCY_CAMPAIGN.md @@ -3,7 +3,7 @@ The durable coordination surface for generated dependency and consumer-sync work is the campaign issue, not an individual generated pull request. A pull request is a leased delivery attempt and must carry the -`sync-pr-delivery-record/v1` marker with its durable issue, plan, generation, +`sync-pr-delivery-record:v1` marker (schema `sync-pr-delivery-record/v1`) with its durable issue, plan, generation, repository, desired tree hash, source commit, expiry, and lineage. Maint 71 is the sole merge/close reconciler. It applies the same contract to @@ -19,6 +19,6 @@ both `sync/workflows-*` consumer-sync branches and decision before any merge. Maint 82 owns the durable campaign state and only requests local agent work -when a materially changed exception fingerprint appears. Timestamps alone do -not constitute new work. Local watchers consume the normalized handoff record -and do not independently decide merge or close disposition. +when an actionable exception fingerprint materially changes. Timestamps alone +do not constitute new work. Local watchers consume the normalized handoff +record and do not independently decide merge or close disposition. diff --git a/tests/workflows/test_workflow_agents_consolidation.py b/tests/workflows/test_workflow_agents_consolidation.py index 318381f23..cf57cb070 100644 --- a/tests/workflows/test_workflow_agents_consolidation.py +++ b/tests/workflows/test_workflow_agents_consolidation.py @@ -312,6 +312,9 @@ def test_merge_sync_prs_uploads_machine_readable_report_and_hash_input(): assert ( "selectMergeEligibleSyncPr" in text ), "Maint 71 must select the active PR with the lease-aware merge contract" + assert ( + "isTrustedGeneratedDeliveryPr" in text + ), "Maint 71 must route both sync and dev-tool generated deliveries through the contract" assert "cleanup_branches:" in text, "Maint 71 must expose sync branch cleanup control" assert ( "collectDeletableSyncBranches" in text and "branch_delete_failed" in text From 0024aef4e4dfe4698e514cd2cee4c035bbe1914f Mon Sep 17 00:00:00 2001 From: Codex Automation Date: Sat, 1 Aug 2026 18:28:12 -0500 Subject: [PATCH 3/3] fix(sync): reconcile expired generated deliveries --- .github/workflows/maint-71-merge-sync-prs.yml | 40 ++++++++++++++++++- .../test_workflow_agents_consolidation.py | 1 + 2 files changed, 39 insertions(+), 2 deletions(-) diff --git a/.github/workflows/maint-71-merge-sync-prs.yml b/.github/workflows/maint-71-merge-sync-prs.yml index a438ffa8a..5616a914b 100644 --- a/.github/workflows/maint-71-merge-sync-prs.yml +++ b/.github/workflows/maint-71-merge-sync-prs.yml @@ -518,8 +518,44 @@ jobs: if (!selection.eligibility?.eligible) { const reason = selection.eligibility?.reason || 'missing_delivery_record'; console.log(`Delivery contract blocks merge: ${reason}`); - results.push({ owner, repo, pr: selection.active.number, branch: selection.active.head.ref, - status: 'delivery_contract_blocked', delivery_reason: reason }); + const deliveryDisposition = reason === 'lease_expired' + ? 'expired' + : selection.deliveryRecord + ? 'superseded' + : 'owner-decision'; + const deliveryContext = { + owner, + repo, + pr: selection.active.number, + branch: selection.active.head.ref, + head_sha: selection.active.head.sha, + delivery_generation: selection.deliveryRecord?.generation || '', + delivery_lane: generatedDeliveryLane(selection.active.head.ref), + delivery_disposition: deliveryDisposition, + blocker_owner: deliveryDisposition === 'owner-decision' ? 'source' : 'maint-71', + next_command: deliveryDisposition === 'expired' + ? 'close-expired-delivery' + : deliveryDisposition === 'superseded' + ? 'close-or-refresh-delivery' + : 'attach-or-infer-delivery-record', + }; + if (deliveryDisposition === 'expired' || deliveryDisposition === 'superseded') { + if (!dryRun) { + await withRetry((client) => client.rest.pulls.update({ + owner, + repo, + pull_number: selection.active.number, + state: 'closed', + })); + } + results.push({ ...deliveryContext, status: 'stale_closed', dry_run: dryRun }); + continue; + } + results.push({ + ...deliveryContext, + status: 'delivery_contract_blocked', + delivery_reason: reason, + }); continue; } diff --git a/tests/workflows/test_workflow_agents_consolidation.py b/tests/workflows/test_workflow_agents_consolidation.py index cf57cb070..794a2354c 100644 --- a/tests/workflows/test_workflow_agents_consolidation.py +++ b/tests/workflows/test_workflow_agents_consolidation.py @@ -315,6 +315,7 @@ def test_merge_sync_prs_uploads_machine_readable_report_and_hash_input(): assert ( "isTrustedGeneratedDeliveryPr" in text ), "Maint 71 must route both sync and dev-tool generated deliveries through the contract" + assert "close-expired-delivery" in text and "close-or-refresh-delivery" in text assert "cleanup_branches:" in text, "Maint 71 must expose sync branch cleanup control" assert ( "collectDeletableSyncBranches" in text and "branch_delete_failed" in text