vouchington-tooling 0.42.0 → 0.44.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (93) hide show
  1. package/README.md +45 -1
  2. package/dist/cli/commands/fresh-base.d.mts +6 -0
  3. package/dist/cli/commands/fresh-base.mjs +14 -0
  4. package/dist/cli/index.mjs +3 -0
  5. package/dist/cli/parse-fresh-base.d.mts +2 -0
  6. package/dist/cli/parse-fresh-base.mjs +15 -0
  7. package/dist/cli/parse.d.mts +5 -0
  8. package/dist/cli/parse.mjs +3 -0
  9. package/dist/cli/usage.d.mts +1 -1
  10. package/dist/cli/usage.mjs +3 -0
  11. package/dist/fresh-base/classify.d.mts +21 -0
  12. package/dist/fresh-base/classify.mjs +29 -0
  13. package/dist/fresh-base/index.d.mts +4 -0
  14. package/dist/fresh-base/index.mjs +2 -0
  15. package/dist/fresh-base/run.d.mts +25 -0
  16. package/dist/fresh-base/run.mjs +66 -0
  17. package/dist/gh-cli/closing-refs.d.mts +24 -0
  18. package/dist/gh-cli/closing-refs.mjs +37 -0
  19. package/dist/gh-cli/index.d.mts +3 -0
  20. package/dist/gh-cli/index.mjs +2 -0
  21. package/dist/gh-cli/non-closing-refs.d.mts +11 -0
  22. package/dist/gh-cli/non-closing-refs.mjs +24 -0
  23. package/dist/gh-cli/pr-create.d.mts +2 -0
  24. package/dist/gh-cli/pr-create.mjs +5 -1
  25. package/dist/index.d.mts +6 -0
  26. package/dist/index.mjs +3 -0
  27. package/dist/mcp-server/context.d.mts +11 -0
  28. package/dist/mcp-server/dispatch.d.mts +8 -0
  29. package/dist/mcp-server/dispatch.mjs +7 -0
  30. package/dist/mcp-server/handlers-pr.d.mts +4 -0
  31. package/dist/mcp-server/handlers-pr.mjs +143 -0
  32. package/dist/mcp-server/harness.test-helpers.d.mts +8 -1
  33. package/dist/mcp-server/harness.test-helpers.mjs +3 -1
  34. package/dist/mcp-server/pr-body-file.d.mts +4 -0
  35. package/dist/mcp-server/pr-body-file.mjs +22 -0
  36. package/dist/mcp-server/pr-body.d.mts +20 -0
  37. package/dist/mcp-server/pr-body.mjs +47 -0
  38. package/dist/mcp-server/pr-defaults.d.mts +16 -0
  39. package/dist/mcp-server/pr-defaults.mjs +35 -0
  40. package/dist/mcp-server/pr-gh.d.mts +16 -0
  41. package/dist/mcp-server/pr-gh.mjs +56 -0
  42. package/dist/mcp-server/pr-input.d.mts +26 -0
  43. package/dist/mcp-server/pr-input.mjs +62 -0
  44. package/dist/mcp-server/pr-journal.d.mts +12 -0
  45. package/dist/mcp-server/pr-journal.mjs +86 -0
  46. package/dist/mcp-server/pr-live.d.mts +24 -0
  47. package/dist/mcp-server/pr-live.mjs +49 -0
  48. package/dist/mcp-server/pr-tools.test-helpers.d.mts +54 -0
  49. package/dist/mcp-server/pr-tools.test-helpers.mjs +67 -0
  50. package/dist/mcp-server/tools-pr.d.mts +2 -0
  51. package/dist/mcp-server/tools-pr.mjs +69 -0
  52. package/dist/mcp-server/tools.mjs +2 -0
  53. package/dist/pull-request-diff/diff-size.d.mts +29 -0
  54. package/dist/pull-request-diff/diff-size.mjs +35 -0
  55. package/dist/pull-request-diff/index.d.mts +2 -0
  56. package/dist/pull-request-diff/index.mjs +1 -0
  57. package/dist/retrospective-distill/args.d.mts +13 -0
  58. package/dist/retrospective-distill/args.mjs +27 -0
  59. package/dist/retrospective-distill/checkpoint-entry.d.mts +2 -0
  60. package/dist/retrospective-distill/checkpoint-entry.mjs +11 -0
  61. package/dist/retrospective-distill/fixtures.test-helpers.d.mts +3 -0
  62. package/dist/retrospective-distill/fixtures.test-helpers.mjs +21 -0
  63. package/dist/retrospective-distill/flag-args.d.mts +5 -0
  64. package/dist/retrospective-distill/flag-args.mjs +31 -0
  65. package/dist/retrospective-distill/index.d.mts +10 -0
  66. package/dist/retrospective-distill/index.mjs +6 -0
  67. package/dist/retrospective-distill/partition-records.d.mts +6 -0
  68. package/dist/retrospective-distill/partition-records.mjs +49 -0
  69. package/dist/retrospective-distill/retrospective-entry.d.mts +3 -0
  70. package/dist/retrospective-distill/retrospective-entry.mjs +4 -0
  71. package/dist/retrospective-distill/run.d.mts +5 -0
  72. package/dist/retrospective-distill/run.mjs +28 -0
  73. package/dist/retrospective-distill/session-types.d.mts +17 -0
  74. package/dist/retrospective-distill/session-types.mjs +1 -0
  75. package/dist/retrospective-distill/shape.d.mts +29 -0
  76. package/dist/retrospective-distill/shape.mjs +111 -0
  77. package/docs/mcp-server.md +80 -6
  78. package/package.json +18 -3
  79. package/skills/agent-workflow/SKILL.md +20 -0
  80. package/skills/agent-workflow/references/four-refs.md +74 -0
  81. package/skills/blackboard/SKILL.md +21 -1
  82. package/skills/github-issue/SKILL.md +6 -0
  83. package/skills/manifest.json +11 -5
  84. package/skills/postgres-node-performance-tuning/SKILL.md +2 -0
  85. package/skills/postgres-node-performance-tuning/references/performance-patterns.md +76 -0
  86. package/skills/postgres-node-performance-tuning/references/reading-plans.md +111 -0
  87. package/skills/postgres-schema-design/references/relations.md +18 -0
  88. package/skills/pr-description/SKILL.md +8 -0
  89. package/skills/retrospective-distill/SKILL.md +67 -4
  90. package/skills/revisit-followups/SKILL.md +17 -0
  91. package/skills/stacked-prs/SKILL.md +47 -16
  92. package/skills/stacked-prs/references/drain.md +168 -0
  93. package/skills/stacked-prs/references/gh-stack.md +143 -0
@@ -0,0 +1,111 @@
1
+ # Reading plans
2
+
3
+ Run `EXPLAIN (ANALYZE, BUFFERS, VERBOSE, FORMAT JSON)` at representative cardinality, in a transaction you
4
+ roll back when the statement writes. `ANALYZE` executes the statement, and a rollback does not
5
+ undo non-transactional effects such as sequence advancement from `nextval()`. Calls that reach
6
+ outside the database (requests, messages) still happen even in a disposable database, so use plain
7
+ `EXPLAIN` for statements that trigger them, or disable the integrations first. A glance at the top node hides most of the problems below;
8
+ walk every node. `VERBOSE` adds each node's `Output` expressions, which is where a select-list
9
+ function call shows. In JSON, `Actual Rows`, `Actual Startup Time`, `Actual Total Time`,
10
+ `Rows Removed by Filter`, `Rows Removed by Join Filter`, and `Rows Removed by Index Recheck` are
11
+ per-loop averages, so multiply each by `Actual Loops` for the total.
12
+
13
+ ## Red flags
14
+
15
+ ### Rows read per row returned
16
+
17
+ A node that reads far more rows than it passes up is doing hidden work. Compare
18
+ `Actual Rows` x `Actual Loops` with the rows the parent keeps, and read `Rows Removed by Filter`
19
+ `Rows Removed by Join Filter`, and `Rows Removed by Index Recheck` (lossy GiST, BRIN, Bloom, or
20
+ bitmap pages; `Actual Rows` is counted after the recheck), each x `Actual Loops`. Millions removed to return tens means the predicate is not an
21
+ index condition, the index has the wrong leading columns, or a join filters after it multiplies.
22
+ Fix the index or the predicate shape, or filter before joining.
23
+
24
+ ### SubPlans whose loops equal the outer row count
25
+
26
+ A node with `"Parent Relationship": "SubPlan"` and `Actual Loops` equal to the outer row count
27
+ runs once per outer row. Correlated subqueries in the select list or in a `Filter` look like
28
+ this. The statements inside a non-inlined function do not appear as SubPlans; see opaque function
29
+ calls below. Rewrite as a join, a `LATERAL` with a limit, or a
30
+ set-based aggregate computed once. An uncorrelated `InitPlan` runs once, but check its own time, rows, and buffers: one large scan,
31
+ sort, or expensive call can still dominate the statement.
32
+
33
+ ### `Filter` where an `Index Cond` was expected
34
+
35
+ An `Index Cond` narrows the index scan; a `Filter` runs on every row the scan returns. If a
36
+ column you expected in `Index Cond` appears in `Filter`, check for a volatile function, a cast or
37
+ function wrapping the column, a type or collation mismatch, or a column outside the index's
38
+ leading keys. See
39
+ [volatile functions in predicates](performance-patterns.md#volatile-functions-in-predicates).
40
+
41
+ A partitioned parent whose plan lists every child and shows no `Subplans Removed` may not have
42
+ been pruned at plan or executor start. Pruning on an execution-time parameter, such as a value
43
+ from the outer side of a nested loop, keeps every child in the plan; check the children's loops,
44
+ where children pruned every time show `(never executed)`, and children visited only sometimes
45
+ show loops lower than the containing loop count. Constrain the partition key with a constant, a
46
+ parameter, or a once-per-statement stable expression; the bound must not be a volatile call.
47
+
48
+ ### Opaque function calls
49
+
50
+ A plan shows `fn_x(col)` in a `Filter` or output column and nothing about the queries the
51
+ function runs. A scalar SQL function whose body contains a subquery, such as `EXISTS`, is never inlined,
52
+ so those queries run per row and stay invisible. Count the calls in one transaction:
53
+
54
+ ```sql
55
+ BEGIN;
56
+ SET LOCAL track_functions = 'all'; -- needs superuser or a granted SET privilege
57
+ SELECT count(*) FROM orders WHERE fn_is_visible(orders.id);
58
+ SELECT funcname, calls FROM pg_stat_xact_user_functions;
59
+ ROLLBACK;
60
+ ```
61
+
62
+ A positive call count proves the function was not inlined. Inlined functions are not counted, but
63
+ a missing row is only suggestive: a query that returns no rows or short-circuits the predicate also
64
+ never calls the function, so confirm the function body shape before concluding it was inlined.
65
+ Tested on
66
+ PostgreSQL 18.6: an `EXISTS`-bodied function was counted 90 times for 90 rows, and a
67
+ plain-expression function was inlined and not counted. `track_functions = 'pl'` skips
68
+ SQL-language functions; use `'all'`. See
69
+ [function inlining](performance-patterns.md#function-inlining).
70
+
71
+ ### Text join and index conditions
72
+
73
+ `Hash Cond`, `Merge Cond`, `Join Filter`, or a join's `Index Cond` comparing `text` or `varchar`
74
+ columns (often shown with `::text` casts) means the key is compared under collation and stored in every
75
+ referencing row and index. Join on ids and keep the string in a unique column; see
76
+ [join on ids](../../postgres-schema-design/references/relations.md#join-on-ids-never-on-text).
77
+
78
+ ### Generic plans worse than custom plans
79
+
80
+ A prepared statement can switch to a generic plan after several executions, planned without the
81
+ parameter values. If a statement is fast in `psql` with literals but slow from the application,
82
+ compare `EXPLAIN (GENERIC_PLAN)` for the statement with the plan for a real value. Estimates alone
83
+ can hide the problem, and `GENERIC_PLAN` cannot be combined with `ANALYZE`; prepare the statement
84
+ and compare `EXPLAIN (ANALYZE, BUFFERS) EXECUTE` for the same skewed value under
85
+ `plan_cache_mode = force_generic_plan` and `force_custom_plan`. When the generic plan is much worse, fix the statistics or query shape first; set
86
+ `plan_cache_mode = force_custom_plan` for that statement or role only with measured evidence.
87
+
88
+ ### Large sequential scans and bad estimates
89
+
90
+ A `Seq Scan` over a large table that returns few rows and reads many buffers or removes many rows
91
+ by filter is usually a missing or unusable index. A sequential scan on a small table, one that
92
+ returns most rows, or one a parent `LIMIT` or `EXISTS` stops early (check the buffers) is fine. Compare `Plan Rows`
93
+ with `Actual Rows` on every executed node (`Actual Loops` above zero; a never-executed node has no
94
+ observed cardinality) that decides a join (scan inputs and join outputs): an estimate
95
+ off by 10x or more can pick the wrong join type or order. Run `ANALYZE`, then consider a higher
96
+ statistics target, or extended statistics for correlated columns within one table; extended
97
+ statistics do not improve join selectivity, so for a join misestimate look at the join keys'
98
+ statistics, the filters feeding the join, or the query shape before changing the query.
99
+
100
+ ## Checklist
101
+
102
+ 1. Total rows per node versus rows kept; `Rows Removed by ...`.
103
+ 2. Any SubPlan with loops equal to the outer rows.
104
+ 3. `Filter` that should be `Index Cond`; partition pruning (`Subplans Removed`).
105
+ 4. Function calls in `Filter` or output; count them with `pg_stat_xact_user_functions`.
106
+ 5. Text keys in join and index conditions.
107
+ 6. Generic versus custom plan for prepared statements.
108
+ 7. Estimate versus actual on join-deciding nodes.
109
+ 8. `Buffers` shared hit plus read totals against the rows returned; the split separates cache
110
+ pressure from reads that missed PostgreSQL's shared buffers; use OS or storage metrics
111
+ to judge physical I/O, since the OS cache may serve a shared read.
@@ -39,6 +39,24 @@ validation, then use URL FKs. Keep exact external protocol identifiers, capabili
39
39
  evidence snapshots unchanged when their contract requires it; document the exception.
40
40
  Do not duplicate a child collection into a parent column unless a trigger maintains the projection.
41
41
 
42
+ ## Join on ids, never on text
43
+
44
+ Keys are ids. A key compared as text runs under collation, and its string is copied into every
45
+ referencing row and index. Keep the string as a unique column on the owning table and reference
46
+ the row by id:
47
+
48
+ ```sql
49
+ -- avoid: orders.account_code text REFERENCES accounts (code)
50
+ CREATE TABLE accounts (id uuid PRIMARY KEY, code text NOT NULL UNIQUE);
51
+ CREATE TABLE orders (id uuid PRIMARY KEY, account_id uuid NOT NULL REFERENCES accounts (id));
52
+ ```
53
+
54
+ Resolve the string to an id once at the boundary, then join `orders.account_id = accounts.id`.
55
+ Values matched as text by nature, such as free-form tags, are the exception; document them. Text
56
+ in a join's `Hash Cond` or `Index Cond` is a plan red flag; the single lookup of the unique code
57
+ at the boundary is expected and fine. See
58
+ [reading plans](../../postgres-node-performance-tuning/references/reading-plans.md#text-join-and-index-conditions).
59
+
42
60
  Generic enforcement in `jonathanong/no-mistakes`: `postgres-column-naming`,
43
61
  `postgres-array-columns`, `postgres-table-shape`, `postgres-fk-index`, and
44
62
  `postgres-require-fk-on-delete`. These cover naming, configured shapes, and FK mechanics;
@@ -54,6 +54,14 @@ not evidence of a miss; classify genuine CI-only or external infrastructure fail
54
54
  Retain confirmed gap records after CI turns green. Diagnose the failure through the repository's
55
55
  CI investigation workflow; a failed status alone does not establish a gap.
56
56
 
57
+ Create, update, and validate pull requests through the `pr_create`, `pr_update`, and `pr_validate`
58
+ MCP tools with structured input, not by hand-writing a body: pass `title`, `summary`, `impact`,
59
+ `relatedIssues` (`closes` or `refs`, each with a reason), `details` (Markdown paragraphs or
60
+ bullets), `base`, `agent`, optional `model`, `mode`, and `workspaceSetup`. The server renders the
61
+ required headings and provenance lines, runs the repository's policy validation, and blocks on its
62
+ errors, so put the guidance above into `summary`, `impact`, and `details`. Run `pr_validate` first
63
+ to preview the rendered body. `pr_update` preserves the live Shepherd Journal.
64
+
57
65
  Use the repository's approved PR tooling and preserve sections managed by its review automation
58
66
  verbatim. Close only issues fully resolved and explain non-closing issue links. Apply this guidance
59
67
  to descriptions being created or updated, not a bulk rewrite of historical pull requests.
@@ -14,21 +14,25 @@ Use when completed retrospectives or journals should become durable follow-up wo
14
14
 
15
15
  1. Enumerate only completed, eligible session records with the `snapshot_export` tool of the
16
16
  `vouchington-tooling` MCP server, narrowing by `agent`, `version`, `parentSessionId`, exact
17
- `data` fields, `dataArrayContains`, and `inactiveForHours`. It exports non-archived sessions to a
17
+ `data` fields, and `dataArrayContains`. It exports non-archived sessions to a
18
18
  private local file and returns the path, counts, checksum, and manifest, never the records.
19
19
  Before exporting, call `outbox_flush` and then `outbox_status` for every worktree that
20
20
  `git worktree list` reports, passing each path as `worktree`, and stop if any reports a
21
21
  `worktreePendingCount` above 0. Use the worktree count, never `pendingCount` or `status`, which
22
22
  cover only the caller's own session: the sessions to distill belong to other agents, and an
23
23
  archived session refuses later delivery, so a retained record would never be distilled. Outboxes
24
- in removed worktrees or on other machines cannot be drained this way, so select with
25
- `inactiveForHours` to give them time to deliver. Leave in-progress sessions intact. If a search
24
+ in removed worktrees or on other machines cannot be drained this way, so apply the age cutoffs under
25
+ [session shapes](#session-shapes-and-eligibility) to give them time to deliver. Leave in-progress sessions intact. If a search
26
26
  for `snapshot_export` (the tool names carry a harness prefix, see the
27
27
  [blackboard skill](../blackboard/SKILL.md)) finds no server, stop and report it; do not fall back
28
28
  to a CLI command.
29
29
  2. Before using any record, verify the file against the returned checksum, counts, and terminal
30
30
  manifest with the consumer's approved snapshot reader, and stop on a mismatch rather than
31
- treating missing records as absent. Read full records, with their storage type, repository
31
+ treating missing records as absent. Require `manifest.schemaVersion` to be 1, `manifest.status`
32
+ to be `"complete"`, a nonempty `cleanupToken`, and manifest counts that exactly match the export
33
+ counts, including the terminal manifest record. Hand subagents only read-only partitions of the
34
+ verified file, never the original snapshot, and give each partition its own bounded session and
35
+ byte limits. Read full records, with their storage type, repository
32
36
  tags, source identity, coverage, and outcomes, from that file. Do not distill from
33
37
  `journal_entries`: it reads one session at a time and has no checksum or manifest to show that
34
38
  the read is complete. For repository-scoped work, filter sessions by
@@ -41,6 +45,8 @@ Use when completed retrospectives or journals should become durable follow-up wo
41
45
  by exact session/source identity and reject conflicting duplicate content. Records that differ
42
46
  only in their server-assigned `timestamp` are one event. Retain coverage,
43
47
  dropped counts, requested versus observed outcomes, and useful resolved findings.
48
+ Classify each session by [shape](#session-shapes-and-eligibility) and skip any that is not yet
49
+ eligible, with a reason.
44
50
  4. Cluster findings by root cause. Prefer a few broad actionable themes over many narrow issues.
45
51
  Treat a finding already linked to an open tracker as context, not a duplicate.
46
52
  5. Verify each candidate against the current base. Search existing issues and open changes before
@@ -63,6 +69,63 @@ Use when completed retrospectives or journals should become durable follow-up wo
63
69
  actionable dispositions with reasons. Keep the top five themes in human prose without dropping
64
70
  underlying findings or unresolved records.
65
71
 
72
+ ## Session shapes and eligibility
73
+
74
+ Classify by each entry's `type`, never by rendered text. Deduplicate by session and `sourceEventId`
75
+ first. Entries that differ only in the server-assigned `timestamp` are one event; any other
76
+ difference is a conflicting payload and stays quarantined.
77
+
78
+ | Shape | Definition |
79
+ | --------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
80
+ | Entry-type-unresolved | At least one entry has a missing type, a type other than `journal` or `retrospective`, or a feedback envelope that fails the shared validator, even if the session also has a retrospective. |
81
+ | Retrospective | Has a retrospective entry and no unresolved entry. |
82
+ | Checkpoint-only | Has entries, no retrospective, and every entry is a historic hook-written checkpoint, identified by a structural checkpoint field. |
83
+ | Journal-only | Has entries, no retrospective, and at least one entry is not a checkpoint. |
84
+ | Zero-entry child | No entries and a non-null `parentSessionId`. |
85
+ | Zero-entry root | No entries and a null `parentSessionId`. |
86
+
87
+ - The zero-entry split names session topology, not completion. A delegated child that died
88
+ mid-task looks identical to one that finished cleanly; never read it as "completed normally".
89
+ - Report entry-type-unresolved sessions separately. Give them no issue-filing pass or archival this
90
+ run, whatever their age; do not treat an untyped entry as journal evidence.
91
+ - Compute `retroAt` as the newest retrospective entry's `createdAt`, and `lastActive` as the newest
92
+ normalized entry's `createdAt`, falling back to the session's `createdAt` when it has no
93
+ entries. An identical replay does not advance activity; a new source event does.
94
+ - Compute `retroCutoff = now - retroDays` and `sessionCutoff = now - sessionDays` once, from
95
+ caller-supplied non-negative finite day counts, and share both with every inspector so none reads
96
+ its own clock. Reject a negative or non-numeric count before exporting: it makes the cutoff
97
+ lie in the future or every comparison false, and these values gate archival.
98
+ - Every shape except entry-type-unresolved is eligible when
99
+ `(has a retrospective AND retroAt < retroCutoff) OR (lastActive < sessionCutoff)`. Session age
100
+ alone makes a stale checkpoint-only, journal-only, or zero-entry session eligible, so a
101
+ retrospective-only gate cannot strand it. Everything else is not yet eligible; skip it with a
102
+ reason.
103
+ - Export unfiltered rather than with `inactiveForHours`: a zero-entry session has no last entry and
104
+ never matches that filter, which would hide the feedback gaps this pass exists to sweep up.
105
+ Derive age from the cutoffs above; they supply the delay for outboxes in removed worktrees or on
106
+ other machines.
107
+ - Journal-only and zero-entry evidence get full issue-filing rights, subject to the same duplicate,
108
+ current-state, and dependency checks as retrospective evidence, plus redaction: never quote a
109
+ journal entry's raw command string or stderr in an issue body, since a journal entry can carry
110
+ unredacted secrets.
111
+ - Skip archival for sessions that contributed to a theme whose issue mutation failed this run, and
112
+ for sessions whose only themes were deferred, since deferral promises re-evaluation. Keep a theme
113
+ tied to a closed-without-merge issue or change eligible; do not treat open state alone as
114
+ conclusive. Preserve each theme's contributing session IDs through merging and deduplication.
115
+ - Delegate partitions only to read-only inspectors, which must not call MCP, create issues, archive
116
+ sessions, edit files, or keep a copy of a partition. Have each return a compact disposition
117
+ summary: partition index, deduplicated themes with their session IDs, and each session's
118
+ disposition. Inspectors hand-read only journal-only and retrospective sessions; every other shape
119
+ is determined structurally.
120
+ - Keep the verified snapshot until the last `session_archive`, because the archival check reads it.
121
+ Then remove the snapshot with the cleanup capability returned at export, and any partitions with
122
+ the separate receipt returned when they were created (the export capability cannot remove them),
123
+ in a cleanup path that runs even when no session is archived.
124
+ - Report reviewed, fixed, duplicate, deferred, quarantined, actionable, created, updated,
125
+ not-yet-eligible, entry-type-unresolved, and archived counts with reasons, splitting eligible
126
+ sessions into retrospective and stale (checkpoint-only, journal-only, zero-entry child, and
127
+ zero-entry root).
128
+
66
129
  Use source records only for local verification and leave them in the repository's approved journal
67
130
  or retention system. Public issue bodies contain only the minimum bounded facts or redacted
68
131
  summaries needed to establish the problem, proposed work, relevant areas, and validation. Never
@@ -26,5 +26,22 @@ issue policy, and review-record conventions first.
26
26
  duplicate repository or label authorization. Then clean up temporary collection artifacts and
27
27
  report skipped, covered, and created items.
28
28
 
29
+ ## Scope and collection
30
+
31
+ - Accept a duration (`1w`, `2d`, `1m`) or ISO date. If absent, ask, proposing a one-week default,
32
+ then compute a date-only cutoff and fetch the base remote.
33
+ - Treat comments on changes and issues as opt-in sources; bodies are the default.
34
+ - Collect bodies first: for closed issues, unchecked boxes and partial numbered work lists are
35
+ candidates, and comments are fetched only for body candidates unless comments were requested. Do
36
+ not exclude a whole plan issue or rely on a fixed heading allowlist.
37
+ - Search merged and closed work with `--limit 500`. A full page is truncation, not exhaustion: split
38
+ the date range at its midpoint, retaining both bounds for interior chunks; only the top-level
39
+ final chunk may be open-ended.
40
+ - Keep splitting a full single day by timestamp (search dates accept ISO-8601 times); report
41
+ truncation only when the smallest timestamp bucket is still full.
42
+ - Use an OS-temp scratch directory with separate candidate and verdict files; each delegated
43
+ collector gets its own file, and never append concurrently. Do not print fetched bodies outside
44
+ scratch files, and remove all scratch files after collection.
45
+
29
46
  Consumer wrapper owns: review journal format, default lookback, hosting provider, repository,
30
47
  issue taxonomy, and confirmation mechanism.
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: stacked-prs
3
- description: Drain native GitHub stacks from the bottom ready layer; a ready lower layer does not freeze later work.
3
+ description: Work native GitHub stacks bottom-up and merge the ready prefix in one command; a ready lower layer does not freeze later work.
4
4
  ---
5
5
 
6
6
  # Stacked pull requests
@@ -13,6 +13,13 @@ Use when a change is a chain of native GitHub stacked pull requests. Read local
13
13
  `AGENTS.md` first. They own whether this repository stacks, which tool creates, rebases, and merges
14
14
  a stack, and who may authorize a merge.
15
15
 
16
+ - [gh-stack mechanics](references/gh-stack.md): commands, import, submit and verify, the forge as
17
+ the only truth.
18
+ - [Drain and keep track](references/drain.md): ownership, shepherding, ready-prefix merge checks,
19
+ and reconciling the ledger.
20
+
21
+ ## Shape
22
+
16
23
  - Each pull request except the bottom one targets the pull request below it. The bottom pull
17
24
  request targets the default branch.
18
25
  - GitHub merges the selected pull request and every unmerged pull request below it, bottom-up, as
@@ -25,31 +32,55 @@ a stack, and who may authorize a merge.
25
32
  report. Local stack metadata is stale after a rebase, relink, or manual recovery.
26
33
  - The bottom layer's base must be the default branch. Otherwise merge that base or re-root the
27
34
  stack onto the default branch before draining.
28
- - Merge the bottom-most ready layer as soon as it is ready. Do not wait for layers above it.
29
- Nothing in the stack merges on its own.
30
- - Name that bottom-most unmerged layer as the merge target. Naming a higher layer merges more than
31
- this step when everything below is also ready, and is unavailable when it is not.
35
+
36
+ ## Work bottom-up
37
+
38
+ - Work bottom-up. Write layers in stack order, and never start a layer before the one below it is
39
+ in decent shape.
40
+ - A lower layer is in decent shape when it is committed, its local checks have passed, its pull
41
+ request is open with an updated body, and shepherding has started. Decent shape does not mean
42
+ green CI, ready for review, or merged.
43
+ - Start the next dependent layer once the lower layer is in decent shape. Do not wait for merge,
44
+ a ready-delay, or green CI.
45
+ - On each shepherd result, act on the lowest owned layer that is not ready before any higher
46
+ layer. Put each fix on the lowest layer that owns it. A bottom layer that reaches ready early
47
+ frees the ready prefix above it to merge in one command.
48
+ - Before a command that adds layers to a stack, such as `gh stack add` or `gh stack link`, add the
49
+ forge's open-layer count, plus every locally tracked layer not yet submitted, to every layer the
50
+ command would add. `gh stack add` creates only a local branch, so repeated adds before a submit
51
+ are invisible to the forge count. Above the layer cap (default 10),
52
+ open independent work as its own pull request on the default branch and hold dependent work
53
+ until the bottom layers merge.
54
+ - A ready, mergeable, or queued lower layer blocks only an operation that would rewrite that
55
+ layer. Keep readying every other owned layer.
56
+
57
+ ## Merge the ready prefix
58
+
59
+ - Do not merge one layer at a time, and do not wait for the whole stack. Layers accumulate rebase
60
+ surface, CI cost, and review context, and drift against the default branch.
61
+ - The ready prefix is the bottom-most open layer plus every consecutive layer above it that is
62
+ also ready and inside the human's approval. As soon as the bottom layer is ready, merge the ready
63
+ prefix with one command whose target is the prefix's top layer. When every layer is ready and
64
+ approved, the target is the top of the stack.
65
+ - Nothing in the stack merges on its own. Run the merge checks in
66
+ [Drain and keep track](references/drain.md#merge-the-ready-prefix) before each merge.
32
67
  - After a merge, GitHub retargets the next unmerged layer onto the default branch. That layer is
33
68
  the new bottom.
34
- - On a stall, or when a layer closes without merging, stop if the bottom layer is otherwise ready.
35
- Report every layer and ask before merging that bottom layer.
69
+ - On a stall, or when a layer closes without merging, stop if the bottom layers are otherwise
70
+ ready. Report every layer and ask before merging that ready prefix.
36
71
  - A stall blocks only layers that depend on it. Ready the others. Pause the whole drain only when
37
72
  nothing further can be readied without the blocked layer.
73
+
74
+ ## Ownership and workers
75
+
38
76
  - Act only on layers assigned to you. Report the other layers; do not change them.
39
77
  - One worker per native stack implements that stack and runs its shepherd poll. The parent does
40
78
  not poll. It reconciles stacks, starts a worker, and carries merge questions to the human.
41
79
  - Start shepherding when the first owned pull request exists.
42
- - A lower layer is in decent shape when it is committed, its local checks have passed, its pull
43
- request is open with an updated body, and shepherding has started. Decent shape does not mean
44
- green CI, ready for review, or merged.
45
- - Start the next dependent layer once the lower layer is in decent shape. Do not wait for merge,
46
- a ready-delay, or green CI.
47
- - A ready, mergeable, or queued lower layer blocks only an operation that would rewrite that
48
- layer. Keep readying every other owned layer.
49
80
  - Independent stacks run at the same time, one worker each. Dependent unmerged work is another
50
81
  layer of the same stack, not a second stack whose base is an unmerged head.
51
82
  - At most one poll process per stack. The parent does not run a second poll for the same stack.
52
83
  - This skill does not grant a merge. Merge authorization stays with the consumer.
53
84
 
54
- Consumer wrapper owns: default branch, stacking tool and commands, merge-selector syntax, and
55
- merge authorization.
85
+ Consumer wrapper owns: default branch, stacking tool and commands, merge-selector syntax, layer cap,
86
+ pull request description tool, ownership stamp format, and merge authorization.
@@ -0,0 +1,168 @@
1
+ # Drain and keep track
2
+
3
+ Shepherding a layer to ready and merging are different acts. The first covers every pull request
4
+ the worker owns in the stack at once. The second lands the ready prefix in one command, and it is
5
+ never the agent's own decision.
6
+
7
+ ## Establish scope
8
+
9
+ 1. Resolve the stack from `pulls/<any-layer>.stack`, and record whether the field was present. A
10
+ stack object supplies its number; an explicit `null` means no registered stack. A missing
11
+ preview field: query `stacks?pull_request=<N>`, where one result is the stack and zero means
12
+ unstacked. Do not treat an empty `.stack.number` alone as proof
13
+ that a pull request is unstacked.
14
+ 2. Read topology from `repos/{owner}/{repo}/stacks/<n>`, ordered bottom to top. A
15
+ `closed && merged_at == null` layer below an open one means the stack is broken. Report it to
16
+ the human, and never merge across it.
17
+ 3. Partition by ownership, using that topology read plus each member's body from `pulls/<N>` (the
18
+ stack response has no bodies). Shepherd only owned pull requests. Never
19
+ poll, fix, rebase, or push the others, but list them in every report. Owned means this session
20
+ opened the pull request, or a human named it in this session.
21
+ - The opened-by-me half is a durable remote fact when the repository's pull request description
22
+ tool stamps each body with the authoring harness and session id. Compare the stamp with the
23
+ current session id.
24
+ - The named-by-a-human half is conversational and cannot be recovered remotely. That is
25
+ acceptable, because the human can restate it after a compaction.
26
+ - Degrade explicitly. A body without a matching stamp, or with a stamp naming no session id,
27
+ is not owned. Report it as "ownership unverifiable" and never poll it.
28
+ - A parent may assign its stack to one worker by listing the pull request numbers and the
29
+ session id stamped on them. The worker treats that list as assigned and still refuses any
30
+ layer outside it.
31
+
32
+ ## Shepherd the owned layers
33
+
34
+ Run one stack shepherd poll for the assigned stack, and follow its printed instructions.
35
+
36
+ - A shepherd's own "owned" mark can mean only that the author login equals the authenticated
37
+ viewer. Several sessions often share one account, so filter every printed row against the
38
+ ownership partition above before acting. Never start a per-pull-request session, fix, or push
39
+ for a row outside it, even when the shepherd marks it owned.
40
+ - Apply code fixes, rebases, pushes, and review mutations to owned layers only. Report work
41
+ surfaced on an unowned layer to the human.
42
+ - Fix on the owning layer. Commit there, rebase the layers above it (`gh stack rebase --upstack`),
43
+ `gh stack sync` when there are no local stack commits, `gh stack push` once, then rerun the
44
+ shepherd.
45
+ `rebase --upstack` and `push` act through the top of the stack. When an unowned layer sits above
46
+ an owned one, stop at that layer: do not rebase or push it, and hand the remaining rebase and
47
+ push to its owner.
48
+ - Start the shepherd with the first pull request. While the poll is in a pure wait and the
49
+ worktree is clean, commit the next layer once the lower layer is in decent shape. On a
50
+ fix-code result, finish it on the owning layer before more upper edits. A ready-delay schedules
51
+ the rerun: continue owned work on a later layer, and do not invent unrelated work.
52
+ - Never run a printed merge command unless the human's approval covers that layer and every open
53
+ layer below it, and the checks below pass. `gh stack merge <pr>` lands that pull request and
54
+ every layer below it.
55
+ - Mutating git does not parallelize. Stop the poll before a rebase, sync, or push in the stack's
56
+ shared worktree, because other layers' branches can be rewritten underneath it. Discard a poll
57
+ result that was in flight across the mutation, and start the poll again after the push lands.
58
+ Run at most one git mutation at a time.
59
+
60
+ ### Upper-only fast-forward
61
+
62
+ A queued lower layer, or any lower layer that must not be rewritten, does not stop an upper-only
63
+ fast-forward, or upper review and CI work that does not write git.
64
+
65
+ - Before that push, read the forge and capture the parent head, the parent base, the upper remote
66
+ head, and the stack order. Read all four again immediately before pushing.
67
+ - Push only when all four still match, the local upper commit fast-forwards that remote head, and
68
+ it still contains the parent. If anything differs, or the lower layer merges or leaves the queue
69
+ meanwhile, stop and read the forge again. Never push from a stale capture.
70
+ - Do not amend, force-push, `gh stack sync`, or `gh stack rebase` a queued parent: each rewrites
71
+ it. When the lower layer changes, re-read topology before branch work that depends on the new
72
+ parent.
73
+
74
+ ### Escalations persist to the pull request
75
+
76
+ When a layer needs a human, because it is ready and waiting on a merge decision or genuinely
77
+ blocked:
78
+
79
+ - Apply the repository's needs-human label and post one comment per escalation event, not per
80
+ poll, with the layer's position and what it needs.
81
+ - When a layer re-escalates, minimize the superseded comment (GraphQL `minimizeComment`,
82
+ `classifier: OUTDATED`). Clear the label once the layer no longer needs a human.
83
+ - A relayed merge command is for visibility only. The shepherd picks its target from ready
84
+ results alone and does not know what the human approved.
85
+
86
+ ## Report
87
+
88
+ When the owned set settles, present the stack bottom to top: each layer's number, owner, state,
89
+ and whether it is ready or blocked and why. Never leave a ready bottom layer under a blocked upper
90
+ layer without saying so.
91
+
92
+ ## Merge the ready prefix
93
+
94
+ The ready prefix is the bottom-most open layer plus every consecutive layer above it that is also
95
+ ready and inside the approval. Its top layer is the merge target. A shepherd that prints a merge
96
+ of its highest ready layer may name a layer above the approved range. Target the highest approved
97
+ layer of the prefix instead; that is still one command.
98
+
99
+ Merge only on explicit human approval. The approval names what it covers: one layer, a range, or
100
+ the whole stack. Approving the whole stack also covers the next ready prefix after an earlier
101
+ merge. It does not cover a layer added after the approval. Ask before merging any layer outside
102
+ it.
103
+
104
+ Immediately before each approved merge, run these checks in order:
105
+
106
+ 1. `gh api repos/{owner}/{repo}/stacks/<target>` must return 404. `gh stack merge` resolves a bare
107
+ number as a stack number first, then a pull request number. A 200 response means `--yes` would
108
+ merge every layer of that stack. Stop and ask.
109
+ 2. Re-read topology. Confirm every layer from the bottom-most open layer up to the target is open,
110
+ ready, and inside the approval, with nothing broken below.
111
+ 3. For every layer in the prefix, capture `pulls/<N>.head.sha` before the final poll and after it.
112
+ They must match. A mismatch means a push landed inside the evaluation window: discard the
113
+ verdict and poll again.
114
+ 4. `gh stack merge <target> --yes` with the repository's merge strategy. It lands the target and
115
+ every open layer below it, which is the ready prefix.
116
+ 5. Do not `gh stack sync` here. Return to step 2 of [Establish scope](#establish-scope) and
117
+ re-read topology remotely: rebasing right after a merge can replay already-landed commits from
118
+ a stale local base ref. Keep shepherding the layers above. When the new bottom is ready, merge
119
+ the next ready prefix the same way. Before any local edit, rebase, or push on a remaining
120
+ layer, rerun the fetch and ahead/behind check in
121
+ [Import a stack](gh-stack.md#import-a-stack-before-acting-on-it): GitHub rebases the next
122
+ layer remotely, so local upper branches are stale until fast-forwarded. The drain is complete when no unmerged owned layer remains.
123
+
124
+ The window between the last head read and the merge cannot be closed, because `gh stack merge`
125
+ has no head-SHA pin. Branch protection and rulesets are evaluated when the merge runs, and stacks
126
+ cannot bypass merge requirements. If the merge is rejected, re-read topology instead of retrying
127
+ blindly. Under a merge queue, a successful `gh stack merge` only enqueues: the prefix queues
128
+ together, and a failing layer is ejected along with the layers above it. The next topology
129
+ re-read handles that.
130
+
131
+ When you stop or are blocked (an unresolved escalation, an interrupt or pause, repeated fixes with
132
+ no progress, or an external blocker), report each layer's number and state. If the bottom layers
133
+ are ready, ask whether to merge that ready prefix before continuing.
134
+
135
+ ## Keep track: derive the ledger
136
+
137
+ A hand-kept list of "pull requests I opened" drifts, and a compaction or new session erases it.
138
+ The ownership stamp makes the ledger derivable, so state is re-read instead of recalled.
139
+
140
+ Reconcile broadly, partition narrowly. A new session opens a rival stack because it never looks for
141
+ the old one, so the reconcile query is deliberately unfiltered; only the partition is
142
+ session-filtered:
143
+
144
+ ```bash
145
+ # Reconcile, unfiltered: it finds stacks this session did not create.
146
+ gh api --paginate repos/{owner}/{repo}/stacks --jq '.[] | select(.open)' # 30 per page
147
+ gh api --paginate "repos/{owner}/{repo}/pulls?state=open&per_page=100" \
148
+ --jq '.[] | {number, title, user: .user.login, body}'
149
+ # Partition: apply the ownership check from Establish scope to that output.
150
+ ```
151
+
152
+ - Reconcile before starting anything new. At the start of work, and before opening any pull
153
+ request or stack, surface every open stack or unfinished layer, whoever made it.
154
+ - Close out; do not drift away. Close a layer that will not land. Ending or pausing a session with
155
+ owned open layers requires reporting them.
156
+ - Continue the stack instead of starting a parallel one. Dependent work for an owned open stack is
157
+ a new layer (`gh stack add`, after the layer cap check). For an unowned open stack, report it and
158
+ ask instead of adding to it or starting a rival.
159
+ - Derive state (which pull requests and stacks exist, their positions, their status) every time;
160
+ never cache it. Only intent, such as which stack is being built or what a planned layer is for,
161
+ belongs in the session's plan or journal, and it is a plan, not a record.
162
+
163
+ Known gaps:
164
+
165
+ - An ownership stamp is a body line any writer can edit. It is a signal, not an authorization
166
+ boundary, and that is sound only because every merge is human-gated.
167
+ - Stack and pull request numbers share no guaranteed disjointness, which is why merge check 1
168
+ runs before every merge.