pm-github 2026.10.16 → 2026.10.23

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/README.md CHANGED
@@ -39,15 +39,26 @@ Only `https://github.com/owner/repo/blob/main/.agents/pm/<folder>/<id>.toon`
39
39
  links in PR bodies are recognized. Percent-encoded names and nested SDK tracker
40
40
  folders are supported; malformed encodings and traversal segments are ignored. Repository comparisons ignore case. Links to
41
41
  other repositories, unknown ids, closed/canceled items and `--skip` ids produce
42
- explicit skip rows. Bare ids and `pm:` trailers are not supported. If several
43
- selected PRs reference one item, the lowest PR number supplies its evidence and
44
- later references are skipped.
42
+ explicit skip rows. A whole-line `Closes <item-id>` or `pm-github: <item-id>`
43
+ declaration, a closing keyword before the canonical item link, or a structured
44
+ `PM item: <item-id-or-canonical-link>` line declares delivery. Spaces and tabs
45
+ are allowed; declarations cannot span lines. Matching uses the complete item ID,
46
+ so `Closes pm-x-extra` cannot deliver `pm-x`. Undeclared canonical links in the
47
+ PR body, including links in follow-up lists (`PM:`), can produce `not_delivery`
48
+ rows for otherwise eligible items with the reason "Mentioned, not delivered".
49
+ Tracker PR URL metadata with no body reference or declaration produces no row.
50
+
51
+ When several merged PRs declare delivery, closeout selects the latest merge with
52
+ complete merge facts and passing head checks. Equal merge times use the higher
53
+ PR number. Other qualifying claims receive `duplicate_selection`; mention-only
54
+ PRs retain `not_delivery`. All completion fields and the appended close event use
55
+ the selected PR's merge time, merge SHA, head SHA and check evidence.
45
56
 
46
57
  **A mention is not delivery.** PR bodies routinely link follow-ups, epics and
47
58
  related work for context, so a link alone never closes an item. An item is
48
59
  closed only when:
49
60
 
50
- - it is `in_progress` (claimed work), **or** its link is preceded on the same
61
+ - its delivery is explicitly declared; a canonical link may be preceded on the same
51
62
  line by a GitHub-style closing keyword: `Closes`, `Fixes`, `Resolves` or
52
63
  `Delivers` (and their tense forms), for example `- Closes [pm-x](…/pm-x.toon)`;
53
64
  - it is not an `Epic`; epics close when their children are done;
@@ -65,14 +76,21 @@ latest status. Check-run, status and ruleset pages are fully read. Rulesets are
65
76
  read through GitHub's [effective branch rules API](https://docs.github.com/en/rest/repos/rules#get-rules-for-a-branch).
66
77
  The check policy reflects the rules readable at invocation time.
67
78
 
68
- When a protection source is unreadable (HTTP 403/404), or no required contexts
69
- are configured, the report labels the **fallback** rule. Every readable required
70
- context still must conclude success, including its app binding. The fallback
71
- additionally requires at least one head check run, every run completed with
72
- `success`, `neutral` or `skipped`, and successful latest legacy head statuses.
73
- It never discards a known requirement because another source is unreadable.
74
- Missing, malformed or incomplete head evidence, malformed metadata and other
75
- metadata errors fail closed. The plan reports which rule was used.
79
+ When either protection source is unreadable (HTTP 403/404), the row is ineligible
80
+ with `required_checks_unverified`. Observed green runs cannot prove that every
81
+ required context ran. Supply `--required-checks $'test (22)\ntest (26)'` (Bash) to declare a
82
+ newline-separated set of required context names when metadata is unavailable.
83
+ Every declared context must conclude **success** on the exact head (or have a
84
+ successful latest legacy status); missing, pending, neutral or skipped contexts
85
+ refuse closure. Readable requirements remain enforced, including app bindings.
86
+ This **declared** policy does not require unrelated optional runs to succeed.
87
+ When both metadata sources are readable, declarations do not replace their policy.
88
+
89
+ With fully readable metadata and no required contexts configured, the report
90
+ labels the **fallback** rule: at least one head check run must exist, every run
91
+ must complete with `success`, `neutral` or `skipped`, and latest legacy statuses
92
+ must succeed. Missing, malformed or incomplete evidence and other metadata errors
93
+ fail closed. The plan reports which rule was used.
76
94
 
77
95
  TOON lists actionable `close`/`refuse` rows individually and summarizes routine skips
78
96
  as counts by stable code, for example `skipped: {already_terminal: 30,
@@ -83,7 +101,9 @@ plan row and selected PR check. Global `--json` always returns the full shape
83
101
  `{item, pr, action: "close" | "skip", code, reason}`. Codes distinguish
84
102
  `head_evidence_unavailable`, `incomplete_merge`, `head_checks_failed`, and
85
103
  `head_checks_passed` without parsing explanations. `checks` reports each PR's rule,
86
- verdict, explanation and successful check names. Dry-run rows describe intended
104
+ verdict, explanation and successful check names. `head_checks_passed` remains the success
105
+ code for all passing policies; `rule: "declared"` proves the caller-declared contexts,
106
+ while repository required-check metadata was unavailable. Dry-run rows describe intended
87
107
  closes; `closed` counts writes only with `--apply`. `--dry-run` overrides
88
108
  `--apply`. All API planning finishes before the first SDK close. Individual SDK
89
109
  refusals become skip rows containing a diagnostic code (or a generic failure
@@ -105,8 +125,11 @@ structured close fields it supports, without overriding tracker policy:
105
125
  otherwise `Delivered by the linked pull request, merged after its head check gate passed.`
106
126
  - `actual_result`: `Merged at <time>; required checks satisfied; successful head checks:
107
127
  <names>` when branch protection or rulesets name required checks (the list holds every
108
- successful head check, optional ones included). Under the fallback rule (no readable
109
- required contexts) it reads `Merged at <time>; no readable required checks, so every
128
+ successful head check, optional ones included). Under the declared policy it reads
129
+ `Merged at <time>; caller-declared checks concluded success; repository required-check
130
+ metadata was unavailable; successful head checks: <names>`. Under the fallback rule
131
+ (fully readable, empty policy) it reads
132
+ `Merged at <time>; no readable required checks, so every
110
133
  completed head check run had to succeed, be neutral or be skipped; successful checks:
111
134
  <names>`, or `…; no check reported success` when only neutral or skipped runs exist.
112
135
  Only the verified merge time, the applied check policy and sorted successful check
@@ -120,6 +143,39 @@ only the tracker; commit it with its history and regenerate the caller's
120
143
  changelog in a closeout PR. The [automated closeout workflow](#automated-closeout-workflow)
121
144
  performs that daily review-PR preparation for fleet repositories.
122
145
 
146
+ ## Authenticated webhook reconciliation
147
+
148
+ `pm github webhook receive <owner/repo> --payload delivery.json --delivery <id>
149
+ --event issues --signature <sha256=...>` accepts explicitly supplied deliveries.
150
+ Set `PM_GITHUB_WEBHOOK_SECRET` through your secret manager. Exact payload bytes
151
+ are authenticated before any write; a durable inbox deduplicates delivery IDs.
152
+
153
+ `pm github webhook reconcile <owner/repo> --limit 20` reads current GitHub state
154
+ for already-linked issues and returns compact proposals with lifecycle deltas.
155
+ It never applies the event's lifecycle or changes items. Out-of-order events
156
+ therefore cannot reopen a closed item. Output defaults to TOON; add `--json`
157
+ for JSON. Signature and storage refusals include typed codes and remedies.
158
+
159
+ Use `pm github refresh <owner/repo> --dry-run` to review full changes and
160
+ `--atomic` to apply a fresh polling read. Polling remains the recovery route
161
+ for missed deliveries or unavailable webhooks. Back up the ignored durable
162
+ `github-webhooks` directory inside your tracker. See the
163
+ [CLI and typed SDK example](docs/webhook-inbox.md) for deployment, crash recovery,
164
+ retention and supported-event details.
165
+
166
+ ## Without git
167
+
168
+ With an explicit repository, GitHub import, export, refresh, sync, project commands,
169
+ blocker checks, closeout and webhook receive/reconcile work in a plain pm tracker
170
+ without a git executable.
171
+ `pm github identity` also works there, entirely offline and without credentials.
172
+ The provider commands still require their normal GitHub access and credentials. Privacy scanning
173
+ with `github gate --diff <file>` also works there. Reading a proposed tracker diff
174
+ with `github gate` requires a git repository; otherwise pass an explicit diff.
175
+ Gated imports (`github import --gate`, including the scheduled sync workflow)
176
+ also require a git repository so the proposed publication can be checked. They
177
+ refuse before writing when the tracker is outside one; plain imports still work.
178
+
123
179
  ## Revalidate GitHub blockers
124
180
 
125
181
  **Compatibility:** pm-github now requires PM CLI 2026.10.5 or newer for complete
@@ -172,7 +228,7 @@ containing this command before adopting the updated workflow.
172
228
  |---|---|
173
229
  | `importers` | `pm github import <owner/repo>` — idempotent native import pipeline |
174
230
  | `importers` (exporter) | `pm github export` — pm items → GitHub issues (dry-run by default; upsert) |
175
- | `commands` | `pm gh-issues import` (legacy import alias), `pm github sync` (push status), `pm github gate` (fail-closed pre-push privacy gate), `pm github validate` (diagnostics), `pm github closeout` (verified merged-PR delivery), `pm github project list\|fields\|import\|sync` (Projects v2) |
231
+ | `commands` | `pm github refresh <owner/repo>` (refresh linked items only), `pm gh-issues import` (legacy import alias), `pm github sync` (push status), `pm github gate` (fail-closed pre-push privacy gate), `pm github validate` (diagnostics), `pm github closeout` (verified merged-PR delivery), `pm github project list\|fields\|import\|sync` (Projects v2) |
176
232
  | `schema` | declares `github_url`, `github_number`, `github_state`, `github_author`, `github_created_at`, `github_updated_at` item fields |
177
233
  | `hooks` | `afterCommand` — opt-in sync reminder (`PM_GITHUB_SYNC`) for linked items |
178
234
  | `preflight` | early warning when a mutating github command lacks a token |
@@ -209,6 +265,67 @@ checks.
209
265
 
210
266
  ## Import
211
267
 
268
+ ### Keeping a mirror current
269
+
270
+ Use `refresh` when your tracker deliberately mirrors only a few issues from a busy
271
+ repository. It selects existing `gh:owner/repo#N` tags or explicit `github_url`
272
+ links (also `github_repo` with `github_number`), fetches those numbers in GraphQL
273
+ batches of at most 100, and **never creates items**. Only `/issues/N` URLs are
274
+ refreshable links: the batch query reads issue nodes, so a `/pull/N` URL is not
275
+ selected. Ordinary references in text
276
+ are not links. Conflicting identities or multiple owners fail before any write.
277
+
278
+ ```sh
279
+ pm github refresh owner/repo --dry-run --json
280
+ pm github refresh owner/repo --atomic
281
+ pm github refresh owner/repo --since 7d --limit 50 --dry-run
282
+ ```
283
+
284
+ `--limit` bounds the selected issue numbers, in ascending order, before fetching.
285
+ `--since` filters by the fetched upstream update time, including closed issues;
286
+ filtered items are counted separately. Omit it for a complete refresh. A GitHub
287
+ token is required for GraphQL (`GITHUB_TOKEN`, `GH_TOKEN`, or `gh auth login`).
288
+ Closure evidence uses the importer's timeline reader; label pages are fetched in
289
+ full. API retries and rate limits use the shared transport.
290
+
291
+ Refresh reuses import's title, labels and source lifecycle mapping, including
292
+ real closure/completion dates and reopening. Body, local item type, assignee and
293
+ milestone are preserved. A `release` already present on the item is only replaced
294
+ when GitHub supplies calendar-tag attribution for a closure; open issues and
295
+ closures without a matching tag keep the existing release, so user- or tool-set
296
+ attribution survives. Existing cross-provider link-only owners retain their
297
+ native lifecycle and content, as on import. TOON (default) or `--json` returns a
298
+ `plan` of `{id, number, changes: [{field, old, new}]}` plus `updated`, `unchanged`,
299
+ `missing`, `filtered`, `selected`, `resumed` and `created: 0`. On dry-run, `updated` means
300
+ would update; nothing is written. Missing rows carry a typed `reason` and remain
301
+ unchanged: `not_found_or_inaccessible` (GitHub cannot distinguish deleted/hidden),
302
+ `inaccessible`, `transferred`, `api_error`, `incomplete_response`, or `invalid_issue`.
303
+ A request-wide failure aborts before new refresh writes. Recovery of unfinished
304
+ transactions happens first and may already have committed earlier writes when
305
+ a subsequent request fails.
306
+
307
+ Each changed item receives an audit comment. `--atomic` applies the batch in one
308
+ SDK workspace writer transaction with rollback; otherwise each changed item gets
309
+ its own transaction. Every update transaction persists its exact plan beside the
310
+ SDK journal before writing. If a run dies mid-transaction, the next refresh
311
+ detects unfinished journals for the selected items, validates every saved plan
312
+ before any replay, and refuses recovery from another repository. It replays the original
313
+ plan — committing the journal without duplicating the audit comment — before
314
+ unchanged items are skipped, and reports it in `resumed`. A preview
315
+ (`--dry-run`) refuses while such a journal is open; a missing plan fails closed
316
+ naming the transaction id instead of silently reporting a no-op. Review the preview before publishing tracker changes and
317
+ apply the existing privacy gate to any proposed commit.
318
+
319
+ For a weekly refresh, run this in a checkout with the extension installed, token
320
+ available and `PM_PATH` set to the intended tracker. For example, a cron entry:
321
+
322
+ ```cron
323
+ 0 6 * * 1 cd "$MIRROR_CHECKOUT" && pm github refresh owner/repo --atomic --json >> "$MIRROR_LOG" 2>&1
324
+ ```
325
+
326
+ Configure those environment variables in the crontab. The command updates the
327
+ tracker locally; review and commit changes using your normal workflow.
328
+
212
329
  ### `pm github import <owner/repo>` (or `pm gh-issues import`)
213
330
 
214
331
  ```bash
@@ -244,6 +361,7 @@ pm github import owner/repo --dry-run
244
361
  | `--with-comments` | boolean | Fetch issue comments and append them to the item body |
245
362
  | `--comments-mode <mode>` | `body`\|`annotations`\|`both` | How fetched GitHub comments are persisted (default `body`). `annotations` syncs comments into the pm item's native comments collection via the SDK; `both` writes the body section AND native comments. `annotations`/`both` are idempotent on re-import (dedupe by GitHub comment id) |
246
363
  | `--atomic` | boolean | Commit every create, update, close, and reopen in one workspace-writer-locked, crash-resumable transaction (requires pm CLI/SDK >=2026.7.20). Normal failure compensation restores updated/closed items and deletes newly created items; an incomplete compensation is reported explicitly for retry or repair. |
364
+ | `--adopt-cited` | boolean | Explicitly opt in to conservative citation adoption (default: off). Tag new items `gh:owner/repo#N` instead. Default imports only report citation candidates. |
247
365
  | `--link-deps` | boolean | After import, map dependency references in issue **bodies** (`Blocked by #N`, `Depends on owner/repo#N`, `Blocks #N`) to pm dependency edges between the linked items. Idempotent, best-effort, and re-runnable; see [Dependency linking](#dependency-linking---link-deps) below. |
248
366
  | `--gate` | boolean | Fail-closed privacy gate: verify plan completeness (every fetched issue accounted for exactly once) and provenance idempotency (every re-import lands on exactly one `gh:owner/repo#N`-tagged item) BEFORE the write, then scan the proposed tracker change with [`pm github gate`](#pm-github-gate) AFTER it. Any finding — or any unreadable scan input — exits non-zero so an automated pipeline can never commit or push the change. Embeds `gate` receipts in the result. |
249
367
  | `--dry-run` | boolean | Preview without writing |
@@ -251,6 +369,74 @@ pm github import owner/repo --dry-run
251
369
 
252
370
  Each imported item records GitHub provenance: the `gh:owner/repo#N` idempotency tag, a `github_author:<login>` tag, and an enriched description (`author @<login> · state reason <reason> · created <iso> · updated <iso>`). GitHub issues closed as `not_planned` import as pm `canceled` instead of `closed`, preserving the difference between completed work and deliberately dropped work. The integration declares the `github_url`, `github_number`, `github_state`, `github_author`, `github_created_at`, and `github_updated_at` schema fields.
253
371
 
372
+ The primary way to prevent duplicate imports is to tag an item
373
+ `gh:owner/repo#N` **when it is created**, for example
374
+ `pm create issue "Fix widget behavior" --tags "gh:acme/widgets#30"`.
375
+ For an existing item, use `pm update <id> --add-tags "gh:acme/widgets#30"`. A prose URL is context, not an
376
+ ownership declaration. Existing tag/structured URL/provider matching takes
377
+ precedence and is unchanged.
378
+
379
+ **Citation adoption is off by default.** Manual imports, the legacy alias and
380
+ scheduled gated syncs retain normal create behavior when explicit ownership is
381
+ absent. They report read-only `citation_candidates` so an agent can tag the
382
+ correct item, including when several candidates exist. No candidate is adopted
383
+ or ambiguity-blocked by default.
384
+
385
+ Use `--adopt-cited` on the CLI, `adoptCited: true` in SDK import options, or
386
+ `adopt-cited: true` on the reusable sync workflow to enable the fallback. The
387
+ fallback deliberately favors refusal over overwriting an unrelated item:
388
+
389
+ 1. **Normalize before checking.** Decode complete named/numeric HTML entities and
390
+ percent-encoded bytes repeatedly until stable, bounded to eight passes.
391
+ Apply NFKC and remove zero-width/bidi control characters. Unwrap Markdown
392
+ link destinations and reference definitions and join soft line breaks for
393
+ intent matching. Retain normalized source as well as the unwrapped view for
394
+ exclusion, so a link label cannot hide a second reference. Joined-line views
395
+ also expose split references. A normalization bound exhaustion is a refusal.
396
+ 2. **Allowlisted ownership.** A qualified target issue reference must appear in
397
+ the title, directly follow `Fix`, `Fixes`, `Fixed`, `Close`, `Closes`, `Closed`,
398
+ `Resolve`, `Resolves` or `Resolved` at the start of the description, or be a
399
+ URL on a whole body line `Issue: <url>` or `Tracks: <url>`. Matching ignores
400
+ case. Body fix phrases, `Related:` mentions, unanchored phrases and bare
401
+ numbers cannot authorize adoption.
402
+ 3. **Exclude every other reference.** Across all normalized fields, any other
403
+ `github.com` URL, foreign repository, different `#<digits>` or
404
+ `issues`/`pull` token followed by another number vetoes adoption. Unattributed
405
+ paths/bare hashes also refuse adoption: this item projection has no verified
406
+ owning-repository context. PR URLs, including a same-number PR URL, are other
407
+ URLs. The scan over-counts code and incidental text deliberately. These
408
+ checks reduce risk; they do not make arbitrary prose an ownership contract.
409
+
410
+ | Existing prose | Default | With `--adopt-cited` |
411
+ | --- | --- | --- |
412
+ | Description: `Fixes acme/widgets#30` | Create + `adoption_disabled` hint | Eligible if no other reference |
413
+ | Body line: `Issue: https://github.com/acme/widgets/issues/30` | Create + hint | Eligible if no other reference |
414
+ | Title: `Handle acme/widgets#30` | Create + hint | Eligible if no other reference |
415
+ | Body: `Fixes acme/widgets#30` | Create + `missing_intent` hint | Create + hint |
416
+ | Body: `Related: acme/widgets#30` | Create + `missing_intent` hint | Create + hint |
417
+ | Title cites issues 30 and 31 | Create + `multiple_issues` hint | Create + hint |
418
+ | Description fixes issue 30; body has `**https://github.com/other/widgets/issues/30**` | Create + hint | Create + `multiple_issues` hint |
419
+ | Description fixes issue 30; body has `[other](https://github.com/other/widgets/issues&sol;31)` or `issues%2F31` | Create + hint | Create + `multiple_issues` hint |
420
+
421
+ Plans include `citation_candidates: [{id, reason}]`; aggregate dry-run and write
422
+ receipts include `citation_candidates: [{issue, id, reason}]`. Reasons are
423
+ `adoption_disabled`, `missing_intent`, `multiple_issues`, `normalization_limit` or `structured_owner` (the item already carries a `github_*` field, a core `external_ref` or `source_owner`, a `linear_*`/`jira_*`/`ado_*` identity field, provider provenance, a GitHub issue URL in its links or a native declaration, so prose citing another issue never re-owns it).
424
+ Candidate reporting never changes the referenced item's status, content or
425
+ fields, including a closed item's completion state.
426
+
427
+ With explicit opt-in, one eligible untagged candidate produces an `adopt` plan:
428
+ its ID receives the provenance tag and `github_*` fields through normal upsert.
429
+ Closed/canceled owners remain eligible. Two or more eligible owners refuse the
430
+ batch with typed `ambiguous_citation`, IDs and a remedy to tag the correct item.
431
+ The default path never applies that fallback ambiguity refusal. Existing
432
+ structured-identity collision checks still apply in both modes.
433
+
434
+ Dry runs show `adopt` and `wouldAdopt`; write receipts report `adopted` separately
435
+ from `imported`/`updated`. Gated idempotency counts an opt-in adopt as one landing
436
+ item. Status push, export and Projects matching retain their scope. Citation
437
+ reporting and opt-in adoption work in non-git trackers; actual `--gate` writes
438
+ still require a Git worktree for the proposed-change privacy scan.
439
+
254
440
  `--issue` deduplicates references and fetches each selected issue directly, including closed issues, without listing the repository. Every selected number is fetched and checked before any tracker write. A missing number fails the whole import; `--dry-run` instead reports `missingIssues` and previews the available selection. Pull request numbers require `--include-prs`. Selection combines with comment modes, dependency linking, atomic imports, gates, dry runs, and custom item types, preserving provenance-based re-import idempotency.
255
441
 
256
442
  `--limit` bounds the matching list after PR, draft, and milestone filters. Pagination stops once one extra matching issue establishes truncation, so at most `n + 1` eligible issues are retained; GitHub pages may also contain excluded entries. Without a limit, existing list ordering is unchanged.
@@ -448,7 +634,83 @@ Unreadable input, malformed or truncated diffs, binary input, invalid UTF-8, Git
448
634
 
449
635
  Rendered values are also scanned in memory before mutation, so a known finding cannot be written to disk. After mutation, the importer verifies that every planned provenance tag exists exactly once, then scans the tracker diff. A completed SDK journal cannot substitute for missing item files.
450
636
 
451
- The `gate` receipt reports the verdict, scanned file count, findings, and allowlisted count. With `--json`, successful reports go to stdout; failures use the host CLI's JSON error envelope on stderr, with the redacted gate report serialized in `detail` (`jq ' .detail | fromjson ' gate-error.json`). Unreadable inputs produce a nonzero JSON refusal.
637
+ The `gate` receipt reports the verdict, scanned file count, findings, and allowlisted count. Each finding includes `match_offset` (the starting character offset of the hashed match within the scanned field value, or the serialized added line for tracker text) and `excerpt`. Host-path excerpts show only the root initial, such as `/h***/…`; other matches use `[redacted]`. Human-readable workflow diagnostics include the same evidence. Full paths and secrets never appear. Recognizable bare-root JavaScript regex literals immediately used with `.test()` or `.exec()` are distinguished from paths; quoted host paths and slash-separated host paths remain refused, including inside code and regex source. Ambiguous quote contexts remain refused. Whole JSON-serialized fields retain the same distinction after decoding; TOON wrappers are recognized only in tracker text.
638
+
639
+ With `--json`, successful reports go to stdout; failures use the host CLI's JSON error envelope on stderr, with the redacted gate report serialized in `detail` (`jq ' .detail | fromjson ' gate-error.json`). Unreadable inputs produce a nonzero JSON refusal.
640
+
641
+ ## Self-versioned workflows and Dependabot
642
+
643
+ Both reusable workflows resolve their own **exact** pm-github release version at
644
+ runtime and verify the installed package version. GitHub's documented
645
+ [`job.workflow_sha`](https://docs.github.com/en/actions/reference/workflows-and-actions/contexts#job-context)
646
+ is the commit of the workflow that defines the current job, including a reusable
647
+ workflow. `github.job_workflow_sha` is not a documented context property;
648
+ `job_workflow_sha` is an OIDC claim. The caller checkout and caller commit are
649
+ never used to discover the reusable workflow's version. These workflow identity
650
+ context properties are not available on GitHub Enterprise Server.
651
+
652
+ The resolver uses public `git ls-remote --tags https://github.com/unbraind/pm-github
653
+ 'refs/tags/v*'`. It matches lightweight tags directly and annotated tags by their
654
+ peeled `^{}` commit (never the tag object), strips `v`, and requires an exact
655
+ numeric version `N.N.N` or `N.N.N-N`. It reads all matching refs in one request,
656
+ without API pagination or new credentials. Lookup errors and multiple release
657
+ versions on one commit fail closed. Release version discovery needs no GitHub App, and no release-time workflow
658
+ stamping is required. The release workflow keeps its existing token behavior.
659
+
660
+ Pin **release commit SHAs**, with their matching tag in a same-line comment
661
+ (for example `uses: unbraind/pm-github/.github/workflows/pm-github-sync.yml@<40-character-release-sha> # v2026.10.16`).
662
+ New callers omit `pm-github-version`. If no release tag matches the workflow
663
+ commit, the job fails with instructions to pin a release SHA. For compatibility,
664
+ a nonempty deprecated input provides an exact-version fallback with a warning
665
+ only when no tag matches. If a tag matches and the input differs, the job warns
666
+ and uses the tag version. Untrusted input is passed through `env:`, never inserted
667
+ into shell source or warning text. No `latest`, range, or floating install tag is used.
668
+
669
+ GitHub documents [reusable-workflow updates](https://docs.github.com/en/code-security/how-tos/secure-your-supply-chain/secure-your-dependencies/auto-update-actions)
670
+ and [SHA pins and same-line version comments](https://docs.github.com/en/code-security/reference/supply-chain-security/supported-ecosystems-and-repositories#github-actions).
671
+ Dependabot updates SHA-pinned remote `uses:` references and their version comments
672
+ when the pinned commit has a matching release tag. GitHub warns that untagged
673
+ commits can instead advance to the latest commit, which may differ from the latest
674
+ release. A SHA update therefore selects the new runtime-resolved install version without
675
+ editing any caller input.
676
+
677
+ Merge this entry into the caller's `.github/dependabot.yml` (retain other ecosystems
678
+ and disjoint groups such as `codeql-action`):
679
+
680
+ ```yaml
681
+ version: 2
682
+ updates:
683
+ - package-ecosystem: "github-actions"
684
+ directory: "/"
685
+ schedule:
686
+ interval: "daily"
687
+ open-pull-requests-limit: 5
688
+ groups:
689
+ pm-toolchain:
690
+ patterns:
691
+ - "unbraind/pm-github"
692
+ # No update-types filter: calendar-version year changes must also update.
693
+ commit-message:
694
+ prefix: "chore"
695
+ include: "scope"
696
+ labels:
697
+ - "dependencies"
698
+ - "github_actions"
699
+ ```
700
+
701
+ The group pattern is the repository name, covering both reusable workflow paths.
702
+ Use exactly `pm-toolchain`: the fleet's existing
703
+ [dependabot-auto-merge.yml](.github/workflows/dependabot-auto-merge.yml) enables
704
+ `gh pr merge --auto --squash` for Dependabot PRs in that group, including calendar
705
+ version major changes. The workflow checks the Dependabot author and repository
706
+ owner, then the metadata group or minor/patch update type. Commit prefix and
707
+ labels do not control merge eligibility; the explicit values above keep PRs
708
+ consistent. Ensure the two labels exist (GitHub ignores unknown custom labels).
709
+ Repository auto-merge must be enabled and required branch-protection checks must
710
+ pass; this configuration does not bypass them. It uses the existing workflow's
711
+ `contents: write` and `pull-requests: write` token permissions. See GitHub's
712
+ [Dependabot option definitions](https://docs.github.com/en/code-security/reference/supply-chain-security/dependabot-options-reference)
713
+ for schedules, groups, commit messages and labels.
452
714
 
453
715
  ## Automated closeout workflow
454
716
 
@@ -456,10 +718,10 @@ The `gate` receipt reports the verdict, scanned file count, findings, and allowl
456
718
  is a reusable `workflow_call` that opens or updates **one** PR titled
457
719
  `chore(pm): close items delivered by merged pull requests` from `pm-github/closeout`.
458
720
  Copy [the caller template](docs/closeout-workflow-caller.yml) into the caller's
459
- workflows directory and replace `RELEASE_COMMIT_SHA` and `RELEASE_VERSION` with a
460
- reviewed workflow commit and an exact published pm-github version containing
721
+ workflows directory and replace `RELEASE_COMMIT_SHA` with a reviewed release
722
+ commit and `vRELEASE_VERSION` with its matching release tag containing
461
723
  `github closeout`. The template schedules a daily run and allows manual dispatch.
462
- Adopting callers is a separate change; the existing sync workflow is unchanged.
724
+ Adopting callers is a separate change.
463
725
 
464
726
  The workflow resolves the caller's default branch through the repository API,
465
727
  with event metadata as fallback, and refuses other run refs before checkout.
@@ -467,7 +729,25 @@ This works for scheduled payloads without a `repository` object and for branches
467
729
  such as `trunk` or `release/stable`. The caller's pinned PM CLI comes from
468
730
  `npm ci --ignore-scripts` in `npm-project-dir` (default `.`), which must resolve
469
731
  inside the workspace and contain `package.json`. The SDK resolves the tracker
470
- from the repository root. The exact `pm-github-version` is installed and verified.
732
+ from the repository root. The exact release version resolved from the workflow commit is installed and verified.
733
+
734
+ `required-checks` is an optional workflow input with an empty default. Set it to
735
+ all required context names, **one context name per line**, when the token cannot read
736
+ branch-protection or ruleset metadata, as is common with `GITHUB_TOKEN`:
737
+
738
+ ```yaml
739
+ with:
740
+ required-checks: |
741
+ test (22)
742
+ test (26)
743
+ ```
744
+
745
+ Each line is trimmed and empty lines are ignored. Spaces within names such as
746
+ `CI / build` are preserved; names are never split on spaces. The workflow passes
747
+ the newline-joined list as one quoted command argument for TypeScript to parse.
748
+ Without a declaration, unavailable
749
+ metadata refuses closeout and prevents publishing a branch or PR. Keep this list
750
+ aligned with the caller's required checks; it is a caller-owned policy declaration.
471
751
 
472
752
  Each run rebuilds the bot branch from the current default branch and invokes
473
753
  `pm github closeout --since 1970-01-01 --apply --json`. Scanning all merge history
@@ -488,23 +768,58 @@ The script must emit the root `CHANGELOG.md` and remain inside the publication s
488
768
  | Validation | Strict PM health; explicit item/history scope committed together |
489
769
  | Branch and PR | Explicit force-with-lease against the observed bot ref; push only bot branch; owned PR lookup excludes forks |
490
770
  | No-op | Zero closes or no tracker diff creates no remote branch or PR; caller-only drift unpublished; stale owned PR closed |
491
- | CI | Caller workflow dispatch after PR create/update; parked approval-required runs reported; no merge |
492
-
493
- `ci-workflows` defaults to `ci.yml`; use whitespace-separated filenames for every
494
- required caller CI workflow. Each must support `workflow_dispatch` and produce
495
- its required checks on that event. Missing files are skipped with a notice,
496
- matching sync; empty disables dispatch. CI dispatch starts checks; it does not
497
- certify that they passed. Review and required-check enforcement remain with the
498
- caller's protected branch. A repository concurrency group serializes closeout
499
- runs, and the explicit lease rejects competing writers.
500
-
501
- The caller grants only `contents: write`, `pull-requests: write`, `checks: read`
771
+ | CI and landing | Dispatch caller checks after publication and branch refresh; request protected squash auto-merge in App mode; report parked approval-required runs |
772
+
773
+ `ci-workflows` defaults to `ci.yml codeql.yml`, covering the fleet's CI and
774
+ CodeQL workflows without querying branch protection. An explicit list replaces
775
+ that inventory (use `ci.yaml` or `codeql.yaml` when appropriate). Missing files
776
+ emit a notice; existing files without a recognized `workflow_dispatch` trigger
777
+ emit a warning naming the one-line repair. Standard block triggers and compact
778
+ `on: workflow_dispatch` / `on: [pull_request, workflow_dispatch]` forms are
779
+ supported. Each dispatched workflow must emit its required context names on
780
+ that event. Zero dispatchable workflows fails; `""` deliberately disables both
781
+ dispatch and automatic landing. Dispatch errors fail the run.
782
+
783
+ GitHub does not evaluate `workflow_dispatch` job checks for PR branch rules,
784
+ even when those checks pass on the head SHA. For unattended landing, configure
785
+ `automation-app-client-id` and the optional `automation_app_private_key` secret.
786
+ After the local gates, the pinned official App-token action mints a token for
787
+ only the caller repository, with contents/pull-requests/actions write and
788
+ checks/statuses read. Publication and refresh use that token, so native
789
+ `pull_request` workflows run without the GITHUB_TOKEN approval prompt and
790
+ provide eligible required checks. Manual head dispatch remains additional
791
+ verification. Empty credentials retain GITHUB_TOKEN and emit a setup notice;
792
+ partial or failed App credentials stop publication. The private key is passed
793
+ only to the token action, after caller scripts have completed.
794
+
795
+ In App mode, after dispatch the workflow requests `gh pr merge --auto --squash`
796
+ with the expected head SHA. A repository that disallows auto-merge emits a notice.
797
+ Without an App, maintainers approve native PR runs, wait for required checks and
798
+ conversations, and merge with an authorized credential; the workflow does not arm
799
+ auto-merge.
800
+ GitHub enforces required checks and conversation resolution. The same run
801
+ polls once per minute: when the PR is `BEHIND`, it requests a branch update
802
+ against the expected head, then dispatches all selected workflows and, in App
803
+ mode, requests auto-merge again when the new head appears. App mode stops within
804
+ forty minutes, inside its token's one-hour lifetime; legacy mode has a two-hour bound. The
805
+ existing daily schedules and separate sync/closeout concurrency groups remain.
806
+ A base change after the monitor window waits for the next daily rebuild.
807
+ Unresolved conversations continue to block landing.
808
+
809
+ See [the caller audit and one-time setup](docs/bot-pr-automation.md) for the App
810
+ credential mapping, CodeQL trigger changes and explicit CI overrides. GitHub
811
+ documents [the token approval behavior](https://docs.github.com/en/actions/concepts/security/github_token)
812
+ and [why manual job checks do not satisfy PR branch rules](https://docs.github.com/en/pull-requests/how-tos/merge-and-close-pull-requests/troubleshooting-required-status-checks).
813
+
814
+ The caller template uses top-level `permissions: {}` and grants its
815
+ `gated-closeout` job only `contents: write`, `pull-requests: write`, `checks: read`
502
816
  and `actions: write`. Closeout uses `checks: read` instead of sync's `issues: read`
503
817
  to retrieve head-check evidence ([GitHub Checks API permissions](https://docs.github.com/en/rest/checks/runs#list-check-runs-for-a-git-reference)).
504
818
  The reusable workflow cannot elevate the caller's token
505
819
  permissions ([GitHub reusable workflow permissions](https://docs.github.com/en/actions/reference/workflows-and-actions/reusing-workflow-configurations)).
506
820
  Repository Actions settings must allow Actions to create PRs. Read denial stops
507
- closeout before push with a diagnostic, including unavailable head-check evidence
821
+ closeout before push with a diagnostic, including `required_checks_unverified` and
822
+ unavailable head-check evidence
508
823
  reported by the command as a skip when it blocks a local, existing, non-terminal,
509
824
  non-excluded tracker item. Unlinked PRs and foreign/terminal/excluded links do
510
825
  not fail the run for unavailable evidence. Incomplete merge facts use a separate
@@ -535,11 +850,13 @@ Optional inputs:
535
850
 
536
851
  | Input | Default | Behavior |
537
852
  | --- | --- | --- |
853
+ | `adopt-cited` | `false` | Explicitly enable conservative normalized citation adoption for preview, import and repeat verification. Primary prevention is creating items with `gh:owner/repo#N` tags. Leave false to retain normal creates with read-only candidate hints. |
538
854
  | `npm-project-dir` | `.` | Relative npm project directory for `npm ci --ignore-scripts`, the CLI `node_modules/.bin` PATH entry, and `post-import-script`. Must have `package.json`, contain no `..` segments, and resolve inside the workspace (including symlinks); invalid paths fail before caching/install. The tracker and publication scope stay at the repository root. |
539
855
  | `post-import-script` | `changelog:full` | Caller npm script name invoked with `npm run --if-present` after import and before the final scan. Override for a different package script, or set `""` to disable. Missing scripts are skipped. Raw shell commands are not accepted. Generated `CHANGELOG.md` is staged with the tracker. Changing any file outside the documented scope fails the run, whether staged, unstaged, or untracked. |
540
- | `ci-workflows` | `ci.yml` | Whitespace-separated filenames in the caller's `.github/workflows`, for example `ci.yml codeql.yml`. Missing files emit a notice and are skipped; set `""` to disable dispatch. Existing workflows must enable `workflow_dispatch`; dispatch errors fail the sync job. |
856
+ | `automation-app-client-id` | `""` | Optional GitHub App client ID, paired with the `automation_app_private_key` workflow-call secret. Enables eligible native PR runs through a fresh repo-scoped token; absent credentials retain GITHUB_TOKEN with a notice. |
857
+ | `ci-workflows` | `ci.yml codeql.yml` | Explicit whitespace-separated filename override in the caller's `.github/workflows`. Missing/non-dispatchable workflows are reported; zero dispatchable workflows fails. Empty disables dispatch and auto-merge. See the CI and landing contract above. |
541
858
 
542
- A fleet repo owns the schedule and calls it with the pinned versions (see [`docs/sync-workflow-caller.yml`](docs/sync-workflow-caller.yml)):
859
+ A fleet repo owns the schedule and calls it with a pinned release SHA (see [`docs/sync-workflow-caller.yml`](docs/sync-workflow-caller.yml)):
543
860
 
544
861
  ```yaml
545
862
  name: Sync GitHub issues into pm
@@ -556,12 +873,15 @@ permissions:
556
873
 
557
874
  jobs:
558
875
  gated-sync:
559
- uses: unbraind/pm-github/.github/workflows/pm-github-sync.yml@RELEASE_COMMIT_SHA
876
+ uses: unbraind/pm-github/.github/workflows/pm-github-sync.yml@RELEASE_COMMIT_SHA # vRELEASE_VERSION
560
877
  with:
561
- pm-github-version: "RELEASE_VERSION" # required: exact published version
878
+ automation-app-client-id: ${{ vars.PM_GITHUB_APP_CLIENT_ID }}
879
+ adopt-cited: false # Tag new items gh:owner/repo#N; opt in only for deliberate adoption.
880
+ secrets:
881
+ automation_app_private_key: ${{ secrets.PM_GITHUB_APP_PRIVATE_KEY }}
562
882
  ```
563
883
 
564
- Replace the two release placeholders with the release commit and package version containing this change; existing callers must repin both. The selected npm project needs a committed npm lockfile (`package-lock.json` or `npm-shrinkwrap.json`) and `@unbrained/pm-cli` as a devDependency (the workflow's `npm ci` provides the CLI) and its pm tracker at `.agents/pm` committed on the caller's default branch. Under **Settings → Actions → General → Workflow permissions** it must allow read and write token permissions and enable **Allow GitHub Actions to create and approve pull requests** (an organization can disable this), or the run pushes the sync branch and then fails to open the review PR. The caller must grant `actions: write`; a reusable workflow cannot elevate its caller's permissions.
884
+ Replace `RELEASE_COMMIT_SHA` with a reviewed release commit containing this change and `vRELEASE_VERSION` with its matching tag. Configure [Dependabot](#self-versioned-workflows-and-dependabot) to maintain future pins automatically. The selected npm project needs a committed npm lockfile (`package-lock.json` or `npm-shrinkwrap.json`) and `@unbrained/pm-cli` as a devDependency (the workflow's `npm ci` provides the CLI) and its pm tracker at `.agents/pm` committed on the caller's default branch. Under **Settings → Actions → General → Workflow permissions** it must allow read and write token permissions and enable **Allow GitHub Actions to create and approve pull requests** (an organization can disable this), or the run pushes the sync branch and then fails to open the review PR. The caller must grant `actions: write`; a reusable workflow cannot elevate its caller's permissions.
565
885
 
566
886
  For a caller such as `unbraind/pm-rust` with no root `package.json`, add
567
887
  `npm-project-dir: tests/oracle` under `with:`. Its `tests/oracle/package.json`
@@ -569,7 +889,16 @@ provides the CLI and post-import npm script; Git and pm tracker operations still
569
889
  run from the repository root. A script that regenerates the changelog must write
570
890
  the repository-root `CHANGELOG.md` to stay within the publication allowlist.
571
891
 
572
- Bot-created review PRs may have missing required checks or `pull_request` runs parked in `action_required`. After a successful push and PR creation/update, the reusable workflow uses the job's `GITHUB_TOKEN` to run `gh workflow run <file> --ref "$SYNC_BRANCH"` for each configured caller CI workflow. Explicit `workflow_dispatch` creates runs on the sync head, so callers must enable that trigger and ensure those runs produce their required check names (and do not restrict the required jobs to the `pull_request` event). It also lists `action_required` runs for the exact pushed SHA and prints their URLs as notices without failing because approval is needed. Missing workflows, disabled dispatch, or approval notices do not establish that required CI passed; inspect the dispatched checks. A no-change sync closes its stale own review PR and dispatches nothing. See GitHub's [GITHUB_TOKEN event rules](https://docs.github.com/en/actions/concepts/security/github_token) and [dispatch trigger and permission requirements](https://docs.github.com/en/rest/actions/workflows#create-a-workflow-dispatch-event).
892
+ Bot-created PRs using GITHUB_TOKEN may have `pull_request` runs parked in
893
+ `action_required`. The optional App token makes publication and branch refresh
894
+ start native eligible PR workflows automatically. Configured manual workflows
895
+ are also dispatched on the bot head; these jobs are additional verification,
896
+ because GitHub excludes manual job checks from PR branch-rule evaluation. Each
897
+ manual workflow must support `workflow_dispatch` and produce its intended check
898
+ names on that event. Parked-run notices include their URLs. A no-change sync
899
+ closes its stale owned review PR and dispatches nothing. See GitHub's
900
+ [token event rules](https://docs.github.com/en/actions/concepts/security/github_token)
901
+ and [required-check eligibility](https://docs.github.com/en/pull-requests/how-tos/merge-and-close-pull-requests/troubleshooting-required-status-checks).
573
902
 
574
903
  ### Installed CLI timeout acceptance
575
904
 
@@ -612,6 +941,10 @@ Release checks require type checking, docstrings, ESLint, zero source duplicatio
612
941
 
613
942
  `npm run changelog:full` reads the complete tracker through the SDK and feeds pm-changelog. Closed work supplies release history; the explicit `changelog-unreleased` tag includes a pending candidate without closing its PM item. Other open work stays excluded.
614
943
 
944
+ Daily Release measures the successful `test (22)` and `test (26)` jobs from the last 20 successful main push CI runs. Its merge wait is the nearest-rank p95 duration plus the larger of 15 minutes or 25%, bounded to 60–120 minutes. Missing history uses the 60-minute floor. The log and job summary record the samples, p95, margin and bounds; the job timeout is 360 minutes.
945
+
946
+ A main advance during preparation or waiting can update the release branch once. Every intervening commit must preserve `package.json` and lockfile release versions, `CHANGELOG.md`, `manifest.json`, and release notes, including changes later reverted. Dependency-only changes are eligible. A conflict, metadata change, non-ancestor main, or second advance aborts. A successful update uses a head-specific push lease, restarts the full measured wait, and requires protected checks on the new head. Publication still verifies the merged commit and runs the release gate again.
947
+
615
948
  ## Multi-agent merge safety
616
949
 
617
950
  This repo tracks its project management in `.agents/pm/` and ships a committed `.gitattributes`
@@ -628,3 +961,50 @@ itself lists each affected stream in its output; `pm history --verify <id>` spot
628
961
  content, so `reconcile` only re-greens the hash chain (no data loss) — see the authoritative
629
962
  [pm-cli merge-safety guide](https://github.com/unbraind/pm-cli/blob/main/docs/MERGE_SAFETY.md). The
630
963
  older blunt `pm history-repair --all` remains available as a lower-level primitive.
964
+
965
+ ## What an imported issue looks like
966
+
967
+ An issue owned by the GitHub importer keeps GitHub's creation and closure dates in the native lifecycle fields used by momentum and cycle-time readers. The import records a new, append-only before/after history event at the time of synchronization; it does not rewrite old events. The public SDK's high-level create/update commands reserve these dates, so finalization uses its public item lock, serialization and history primitives.
968
+
969
+ ```yaml
970
+ status: closed
971
+ created_at: "2026-09-02T10:00:00Z"
972
+ updated_at: "2026-10-16T12:00:00Z"
973
+ closed_at: "2026-09-04T12:00:00Z"
974
+ completed_at: "2026-09-04T12:00:00Z"
975
+ close_reason: 'Closed on GitHub as completed on 2026-09-04T12:00:00Z; closing PR https://github.com/example/project/pull/8 ("Restore the parser")'
976
+ release: v2026.9.5
977
+ github_url: https://github.com/example/project/issues/7
978
+ github_number: 7
979
+ github_state: closed
980
+ github_state_reason: completed
981
+ github_created_at: "2026-09-02T10:00:00Z"
982
+ github_updated_at: "2026-09-04T12:00:00Z"
983
+ github_closed_at: "2026-09-04T12:00:00Z"
984
+ github_release: v2026.9.5
985
+ github_link_only: false
986
+ ```
987
+
988
+ `completed` maps to `closed`; `not_planned` and `duplicate` map to `canceled`. An open or reopened issue maps to `open` and clears old closure evidence and import-assigned release attribution. A closure reason names the closing commit from the REST timeline or closing PR/commit identified by GraphQL `ClosedEvent`, when available. A `ConnectedEvent` supplies a quoted linked PR without claiming that the association caused the closure. Otherwise it says `Closed on GitHub as <reason> on <date>`. Closing a report alone does not establish that its described defect or privacy exposure was repaired.
989
+
990
+ Release attribution uses the actual dates of locally available calendar tags, including annotated tag dates. A closed issue belongs to the first tag at or after its closure: before the first tag goes to that first release, between two tags goes to the second, and after the latest tag remains pending. Fetch the complete tag history before importing; the reusable sync workflow already does this. Regenerate with pm-changelog's `--respect-item-release` so a historical import stays in its declared release window.
991
+
992
+ Before creating an item, import checks the complete tracker for its provenance tag, GitHub URLs in provenance or link metadata, and Linear/Jira/ADO links referenced by the GitHub issue body. A unique existing external owner receives the GitHub tag and source metadata; its title, body, provider identity, creation/completion dates and release remain intact. A persisted `github_link_only` marker preserves that ownership on subsequent syncs. Free-text descriptions and bodies do not declare GitHub ownership. Ambiguous matches refuse the import before writes with `--gate` or `--atomic`. A plain multi-issue import may write earlier issues before rejecting a later ambiguous match.
993
+
994
+ With `--gate` and a nonempty filtered issue list, both preview and apply results include a `plan` with source dates, closure reason, release, target identity, link/create/update action and state-conflict decision. An empty filtered issue list returns without a `plan`, including with `--gate --dry-run`. GitHub wins lifecycle conflicts for GitHub-owned imports. A canceled item that GitHub later reports as completed uses the SDK's explicit repeat-closure transition with the source reason. A linked external owner keeps its local status, and the plan records `preserve-provider-owner`; `github_state` still records the source state. Atomic transaction identity includes the latest external edit or different lifecycle generation in history. Local state edits and repeated state cycles force reconciliation, while staging and compensation keep crash retries on the same journal. Finalization runs before the SDK commits and is compensated with the batch on failure. It is idempotent and runs again after a recovered atomic journal, so an interrupted finalization can be repaired by retrying the same import before the gate permits a push.
995
+
996
+ SDK limitation: native `updated_at` remains the local mutation clock because the bulk SDK uses it to address rollback history. The GitHub update date is retained exactly in `github_updated_at`, and the gated plan discloses this policy. Backdating native `updated_at` safely requires an SDK import mutation whose version identity is independent of source time. Native creation, closure and completion dates still preserve GitHub lifetimes for momentum and cycle-time readers.
997
+
998
+ ## Provider identity collisions
999
+
1000
+ Use `pm github identity --json` for a bounded offline graph of conflicting
1001
+ `external_ref`, GitHub provenance tags and native/provider ownership. Copied
1002
+ prose appears as a weaker mention; genuine provider links retain distinct
1003
+ ownership evidence. Inspect one group with
1004
+ `pm github identity --identity 'gh:example/project#7'`.
1005
+
1006
+ Preview a narrowly scoped repair with `--identity`, `--owner` and `--detach`;
1007
+ apply only after reviewing its exact `--approve` digest. Ambiguous identities
1008
+ refuse with typed remedies. The SDK supplies complete reads, semantic preview,
1009
+ writer transactions, immutable audit history and idempotent replay. See the
1010
+ [worked example and SDK contracts](docs/identity-collisions.md).