Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
96 changes: 96 additions & 0 deletions .agents/pm/history/pm-github-closeout-attribution.jsonl

Large diffs are not rendered by default.

10 changes: 10 additions & 0 deletions .agents/pm/history/pm-github-vhi6.jsonl

Large diffs are not rendered by default.

180 changes: 180 additions & 0 deletions .agents/pm/issues/pm-github-closeout-attribution.toon

Large diffs are not rendered by default.

17 changes: 13 additions & 4 deletions .agents/pm/tasks/pm-github-vhi6.toon
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,24 @@ id: pm-github-vhi6
title: Bound coverage aggregation memory and compare default-heap CI behavior
description: "During the PR #122 privacy-scope regression work, an identity-isolated npm release:check completed 587/587 tests with zero failures, skips or cancellations, then c8 report aggregation exhausted Node's default 4 GiB heap. Native coverage receipts occupied approximately 2.9 GiB. The security fix is being validated with NODE_OPTIONS=--max-old-space-size=12288 and unchanged source inventory and four 100-percent thresholds. Compare default-heap CI behavior, investigate bounded report aggregation or documented resource requirements, and retain complete authored-source and real CLI coverage. Earlier local heap-exhaustion observations also appear in pm-github-fmrf; this task tracks current default-heap acceptance separately from the scope fix in pm-github-wrmt."
type: Task
status: open
status: in_progress
priority: 2
tags[3]: coverage,follow-up,gates
created_at: "2026-10-14T14:32:34.672Z"
updated_at: "2026-10-14T15:04:27.090Z"
updated_at: "2026-10-24T15:42:24.889Z"
author: codex-agent
comments[1]{created_at,author,text}:
comments[5]{created_at,author,text}:
"2026-10-14T15:04:27.090Z",codex-agent,"Confirmed resource workaround: complete identity-isolated npm and Bun release:check each passed 587/587 tests and all 11 checks with NODE_OPTIONS=--max-old-space-size=12288. Coverage remains exact 100/100/100/100 across 12 authored modules, without source exemptions or threshold changes. The earlier default-heap npm run passed all 587 tests before c8 aggregation exhausted the heap. Default-heap CI comparison and bounded aggregation remain follow-up work; the security change does not alter coverage implementation or CI memory settings."
files[2]{path,scope}:
"2026-10-24T14:34:24.911Z",codex-agent,"Additional measured receipt from pm-github-closeout-attribution / PR 174: CI https://github.com/unbraind/pm-github/actions/runs/40101464708 at 30b94f1 failed all four npm/Bun release jobs on Node 22/26 with JavaScript heap exhaustion during c8 aggregation at the existing 8192 MiB limit. The Node 26 test phases each passed 1337/1337, zero failures/cancellations/skips before aggregation failed. No release-gate pass is claimed. This round reduces repeated setup in the new 36-case workflow matrix (shared immutable Git/tracker baselines and only the actual closeout/publication steps); the broader bounded aggregation task remains open, with thresholds, source inventory and heap configuration unchanged."
"2026-10-24T15:10:00.744Z",codex-agent,"Second receipt: https://github.com/unbraind/pm-github/actions/runs/40103921945 at ad2e7cb passed 1337/1337 tests in every npm/Bun Node 22/26 job, then all four c8 aggregations exhausted the unchanged 8192 MiB heap. Sharing tracker callers alone was insufficient while package setup still repeated per cell. The next harness prepares package/scope setup once per immutable baseline; every one of the 36 cases still restores input, runs actual branch preparation, real CLI closeout, publication privacy/refusal gates and leased push, with fresh receipts and provenance/history assertions. No coverage resource or threshold change is used. The broader bounded aggregation task remains open."
"2026-10-24T15:38:04.069Z",codex-agent,"Third CI https://github.com/unbraind/pm-github/actions/runs/40106191758 at cff0f1c passed the complete npm release gate on Node 22 after preparing four baseline callers once. Both Node 26 jobs still passed 1337/1337 tests and exhausted c8 at 8192 MiB. Fixture reductions alone cannot establish Node 26 full-gate acceptance. Adjust only the c8 merge heap ceiling to 12288 MiB, leaving headroom on this public repository's documented 16 GB ubuntu-latest runners (https://docs.github.com/en/actions/reference/runners/github-hosted-runners#standard-github-hosted-runners-for-public-repositories). All four coverage thresholds remain 100, the complete 19-module inventory, zero-exclusion policy, test set and dependency pins stay unchanged. Earlier comments preserving the 8 GiB ceiling describe failed earlier attempts; measured Node 26 failures now justify this resource-only revision. General bounded aggregation remains unfinished and this item will not be closed."
"2026-10-24T15:42:23.333Z",codex-agent,"Resource-only local verification: PM-linked node --test test/coverage-gate.test.ts passes 9/9, zero failures/cancellations/skips. These tests run real c8 fixture reports and preserve threshold-refusal, missing-source, invalid-receipt and pre-entry heap-routing controls. Production typecheck, test typecheck, lint, docstrings and zero duplication pass. Only COVERAGE_HEAP_FLAG changes from 8192 to 12288 MiB, plus explanatory comments. Full CI is still required; this is not bounded-memory certification and the general task stays in progress for orchestrator follow-up."
files[3]{path,scope}:
scripts/coverage-gate.ts,project
test/closeout-workflow.test.ts,project
test/import-gate.test.ts,project
tests[1]{command,scope,timeout_seconds,provenance{author,created_at,source_kind,source_ref}}:
node --test test/coverage-gate.test.ts,project,300,codex-agent,"2026-10-24T15:38:03.298Z",local_mutation,fix/closeout-delivery-attribution
docs[1]{path,scope}:
docs/explicit-closeout-attribution.md,project
body: ""
67 changes: 64 additions & 3 deletions .github/workflows/pm-github-closeout.yml
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,25 @@ jobs:
CI_WORKFLOWS: ${{ inputs.ci-workflows }}
CALLING_REPOSITORY: ${{ github.repository }}
steps:
# Revoke a previous run's request before guards, installs, evidence reads or pushes.
- name: Disarm previous closeout auto-merge
env:
GH_TOKEN: ${{ github.token }}
shell: bash
run: |
set -euo pipefail
printf 'CLOSEOUT_AUTOMERGE_ALLOWED=false\n' >> "${GITHUB_ENV}"
# Use the caller identity and fixed branch, even when input validation fails.
# Filter same-repository heads; a fork using the same branch is never ours.
numbers="$(gh pr list -R "${CALLING_REPOSITORY}" --head pm-github/closeout --state open \
--json number,headRepository,headRepositoryOwner,isCrossRepository,autoMergeRequest \
| jq -r --arg repository "${CALLING_REPOSITORY}" \
'.[] | select(.isCrossRepository == false and (.headRepositoryOwner.login + "/" + .headRepository.name) == $repository and .autoMergeRequest != null) | .number')"
while IFS= read -r number; do
[[ -n "${number}" ]] || continue
gh pr merge "${number}" -R "${CALLING_REPOSITORY}" --disable-auto
done <<< "${numbers}"

- name: Require caller default branch
id: caller_branch
env:
Expand Down Expand Up @@ -372,6 +391,15 @@ jobs:
echo "::error::Closeout refused; incomplete merged-PR metadata blocks a linked local tracker item. No branch was published."
exit 1
fi
# Persist only safe attribution facts so an all-held run can still open a review PR.
review_file="${PM_SYNC_TRACKER}/closeout-review.json"
review_count="$(jq '[.plan[] | select(.action == "needs_review")] | length' "${RUNNER_TEMP}/pm-github-closeout.json")"
if [[ "${review_count}" != 0 ]]; then
jq '{needs_review: ([.plan[] | select(.action == "needs_review") | {item, action, candidates} + (if .missing_evidence then {missing_evidence} else {} end)] | unique_by(.item))}' "${RUNNER_TEMP}/pm-github-closeout.json" > "${review_file}"
elif [[ -f "${review_file}" ]]; then
rm -- "${review_file}"
fi
printf 'CLOSEOUT_REVIEW=%s\n' "${review_count}" >> "${GITHUB_ENV}"
printf 'CLOSEOUT_CLOSED=%s\n' "$(jq -r '.closed' "${RUNNER_TEMP}/pm-github-closeout.json")" >> "${GITHUB_ENV}"

- name: Run caller post-closeout script
Expand Down Expand Up @@ -474,7 +502,7 @@ jobs:
gh pr close "${number}" -R "${REPOSITORY}" --comment "No delivered tracker items remain to close; closing this review PR."
fi
}
if [[ "${CLOSEOUT_CLOSED}" == 0 ]] || git diff --cached --quiet -- "${sync_scope[0]}" "${sync_scope[@]:2}"; then
if [[ "${CLOSEOUT_CLOSED}" == 0 && "${CLOSEOUT_REVIEW:-0}" == 0 ]] || git diff --cached --quiet -- "${sync_scope[0]}" "${sync_scope[@]:2}"; then
# Changelog drift alone belongs to the caller's changelog check;
# publish regenerated output only alongside tracker changes.
if ! git diff --cached --quiet -- CHANGELOG.md; then
Expand Down Expand Up @@ -505,13 +533,16 @@ jobs:
printf -- '- [%s](https://github.com/%s/blob/%s/%s) ([proposed](https://github.com/%s/blob/%s/%s))\n' "${item_id}" "${REPOSITORY}" "${DEFAULT_BRANCH}" "${changed}" "${REPOSITORY}" "${SYNC_BRANCH}" "${changed}" >> "${links_file}"
fi
done < <(git diff --cached --name-only -- "${sync_scope[0]}" "${sync_scope[@]:2}")
if [[ "${CLOSEOUT_REVIEW:-0}" != 0 ]]; then
printf -- '- [Delivery attribution needing review](https://github.com/%s/blob/%s/%s/closeout-review.json)\n' "${REPOSITORY}" "${SYNC_BRANCH}" "${PM_SYNC_TRACKER}" >> "${links_file}"
fi
if [[ ! -s "${links_file}" ]]; then
echo "::error::Closeout changed tracker state but no item link could be generated."
exit 1
fi
# Publish only actionable decisions and counts, with no API prose.
summary_file="${RUNNER_TEMP}/pm-github-closeout-summary.json"
jq '{plan: [.plan[] | select(.code != "already_terminal" and .code != "other_repository" and .code != "duplicate_selection" and .code != "explicit_skip") | {item, pr, action: (if .action == "close" then "close" else "refuse" end), code}], skipped: ([.plan[] | select(.code == "already_terminal" or .code == "other_repository" or .code == "duplicate_selection" or .code == "explicit_skip")] | group_by(.code) | map({key: .[0].code, value: length}) | from_entries)}' "${RUNNER_TEMP}/pm-github-closeout.json" > "${summary_file}"
jq '{plan: [.plan[] | select(.code != "already_terminal" and .code != "other_repository" and .code != "duplicate_selection" and .code != "explicit_skip" and .code != "not_merged") | {item, pr, action: (if .action == "skip" and .code != "not_delivery" then "refuse" else .action end), code} + (if .candidates then {candidates} else {} end) + (if .missing_evidence then {missing_evidence} else {} end)], skipped: ([.plan[] | select(.code == "already_terminal" or .code == "other_repository" or .code == "duplicate_selection" or .code == "explicit_skip" or .code == "not_merged")] | group_by(.code) | map({key: .[0].code, value: length}) | from_entries)}' "${RUNNER_TEMP}/pm-github-closeout.json" > "${summary_file}"
# Scan the public summary as added diff content before any publication.
summary_patch="${RUNNER_TEMP}/pm-github-closeout-summary.patch"
{
Expand All @@ -532,7 +563,10 @@ jobs:
body_file="${RUNNER_TEMP}/pm-github-sync-pr.md"
{
printf '%s\n\n' "Automated pm-github merged-PR closeout for ${REPOSITORY}."
printf '%s\n\n' "The closeout command verified merged-PR delivery and head checks. The proposed tracker change and regenerated changelog passed the fail-closed privacy gate before anything was committed. \`${DEFAULT_BRANCH}\` is never pushed directly."
printf '%s\n\n' "The closeout command verified delivery and head checks for closed items; needs_review items remain open with every candidate listed below. The proposed tracker change and regenerated changelog passed the fail-closed privacy gate before anything was committed. \`${DEFAULT_BRANCH}\` is never pushed directly."
if [[ "${CLOSEOUT_REVIEW:-0}" != 0 ]]; then
printf '%s\n\n' "Automatic merge is disabled because delivery attribution needs_review; every candidate and evidence gap requires human review before landing. Any previous auto-merge request was explicitly disabled before this run."
fi
printf '%s\n\n' "Tracker items changed:"
cat "${links_file}"
printf '\n%s\n\n%s\n' 'Closeout plan (routine skips counted by reason):' '```json'
Expand Down Expand Up @@ -624,7 +658,14 @@ jobs:
}
head_sha="$(git rev-parse HEAD)"
dispatch_checks
if [[ "${CLOSEOUT_REVIEW:-0}" != 0 ]]; then
echo "::notice::Delivery attribution needs review; caller checks were dispatched and automatic landing is disabled."
exit 0
fi
enable_auto_merge
if [[ -n "${APP_DEADLINE:-}" ]]; then
printf 'CLOSEOUT_AUTOMERGE_ALLOWED=true\n' >> "${GITHUB_ENV}"
fi
parked_runs="$(gh run list -R "${REPOSITORY}" --commit "${head_sha}" --status action_required --limit 100 --json url --jq '.[].url')"
while IFS= read -r run_url; do
[[ -n "${run_url}" ]] || continue
Expand Down Expand Up @@ -681,3 +722,23 @@ jobs:
sleep 60
done
echo "::notice::Bot PR maintenance reached its configured bound; auto-merge remains enabled when allowed. The next scheduled run rebuilds from the current default branch. Unresolved conversations still require resolution."

# Also revoke on dispatch/maintenance failure, cancellation, disabled CI,
# no publication, or a hold. This runs even when an earlier step failed.
- name: Disarm closeout auto-merge on unsuccessful landing
if: ${{ always() && (failure() || cancelled() || env.CLOSEOUT_AUTOMERGE_ALLOWED != 'true') }}
env:
GH_TOKEN: ${{ steps.automation_token.outputs.token || github.token }}
shell: bash
run: |
set -euo pipefail
# Use the caller identity and fixed branch, even when input validation fails.
# Filter same-repository heads; a fork using the same branch is never ours.
numbers="$(gh pr list -R "${CALLING_REPOSITORY}" --head pm-github/closeout --state open \
--json number,headRepository,headRepositoryOwner,isCrossRepository,autoMergeRequest \
| jq -r --arg repository "${CALLING_REPOSITORY}" \
'.[] | select(.isCrossRepository == false and (.headRepositoryOwner.login + "/" + .headRepository.name) == $repository and .autoMergeRequest != null) | .number')"
while IFS= read -r number; do
[[ -n "${number}" ]] || continue
gh pr merge "${number}" -R "${CALLING_REPOSITORY}" --disable-auto
done <<< "${numbers}"
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# Changelog

## Unreleased

### Fixed

- Hold ambiguous merged-PR delivery until attribution is verified ([pm-github-closeout-attribution](https://github.com/unbraind/pm-github/blob/main/.agents/pm/issues/pm-github-closeout-attribution.toon))

## 2026.10.24 - 2026-10-24

### Fixed
Expand Down
36 changes: 30 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,17 +48,41 @@ PR body, including links in follow-up lists (`PM:`), can produce `not_delivery`
rows for otherwise eligible items with the reason "Mentioned, not delivered".
Tracker PR URL metadata with no body reference or declaration produces no row.

When several merged PRs declare delivery, closeout selects the latest merge with
complete merge facts and passing head checks. Equal merge times use the higher
PR number. Other qualifying claims receive `duplicate_selection`; mention-only
PRs retain `not_delivery`. All completion fields and the appended close event use
the selected PR's merge time, merge SHA, head SHA and check evidence.
A merged PR is a delivery candidate only when it claims the item through an explicit
`delivery-pr:` tag, a closing declaration, or a commit claim naming the item.
A fully inspected mention-only or link-only PR is context: it remains listed as
`skip` / `not_delivery`, never enters the candidate set, and its merge/check gaps
cannot hold another delivery. Unreadable PR or commit evidence remains unresolved
because it may hide a claim; a known candidate's incomplete evidence still holds.

Closeout never breaks a delivery tie by PR number or merge order. Prefer one explicit
item tag linking the delivery PR, for example:

```sh
pm update pm-x --add-tags delivery-pr:https://github.com/example/project/pull/19
```

The tagged PR must be in the selected PR set and pass the merge and head-check gates.
Ordinary `pr_url` metadata remains contextual. Without an explicit tag, exactly one
linked merged PR must declare the item in its description or commit messages and
merge strictly after the last acceptance-criteria edit in the item's SDK history.
Creation is the cutoff when no later criteria edit exists; comments and unrelated
updates do not move it. Missing or unverifiable history cannot establish chronology.
Unreadable commits on an otherwise qualifying mention also hold selection: they may hide another claim. A failed check on the unique PR never permits fallback to an earlier delivery.

Unresolved attribution produces `needs_review` rows listing every linked candidate
PR number, leaving the item and its history unchanged. Multiple or unmatched explicit
tags also require review. The reusable workflow includes these rows and candidates
in the closeout PR body. An all-held run proposes only the privacy-gated
`closeout-review.json` report in the tracker, so it can still open a review PR.
All completion fields and the appended close event use the selected PR's merge
time, merge SHA, head SHA and check evidence.

**A mention is not delivery.** PR bodies routinely link follow-ups, epics and
related work for context, so a link alone never closes an item. An item is
closed only when:

- its delivery is explicitly declared; a canonical link may be preceded on the same
- its delivery is selected by an explicit item tag or a unique claim after the last criteria edit; a canonical link may be preceded on the same
line by a GitHub-style closing keyword: `Closes`, `Fixes`, `Resolves` or
`Delivers` (and their tense forms), for example `- Closes [pm-x](…/pm-x.toon)`;
- it is not an `Epic`; epics close when their children are done;
Expand Down
Loading
Loading