vouchington-tooling 0.43.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 (64) 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 +2 -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/index.d.mts +6 -0
  24. package/dist/index.mjs +3 -0
  25. package/dist/pull-request-diff/diff-size.d.mts +29 -0
  26. package/dist/pull-request-diff/diff-size.mjs +35 -0
  27. package/dist/pull-request-diff/index.d.mts +2 -0
  28. package/dist/pull-request-diff/index.mjs +1 -0
  29. package/dist/retrospective-distill/args.d.mts +13 -0
  30. package/dist/retrospective-distill/args.mjs +27 -0
  31. package/dist/retrospective-distill/checkpoint-entry.d.mts +2 -0
  32. package/dist/retrospective-distill/checkpoint-entry.mjs +11 -0
  33. package/dist/retrospective-distill/fixtures.test-helpers.d.mts +3 -0
  34. package/dist/retrospective-distill/fixtures.test-helpers.mjs +21 -0
  35. package/dist/retrospective-distill/flag-args.d.mts +5 -0
  36. package/dist/retrospective-distill/flag-args.mjs +31 -0
  37. package/dist/retrospective-distill/index.d.mts +10 -0
  38. package/dist/retrospective-distill/index.mjs +6 -0
  39. package/dist/retrospective-distill/partition-records.d.mts +6 -0
  40. package/dist/retrospective-distill/partition-records.mjs +49 -0
  41. package/dist/retrospective-distill/retrospective-entry.d.mts +3 -0
  42. package/dist/retrospective-distill/retrospective-entry.mjs +4 -0
  43. package/dist/retrospective-distill/run.d.mts +5 -0
  44. package/dist/retrospective-distill/run.mjs +28 -0
  45. package/dist/retrospective-distill/session-types.d.mts +17 -0
  46. package/dist/retrospective-distill/session-types.mjs +1 -0
  47. package/dist/retrospective-distill/shape.d.mts +29 -0
  48. package/dist/retrospective-distill/shape.mjs +111 -0
  49. package/docs/mcp-server.md +1 -1
  50. package/package.json +18 -3
  51. package/skills/agent-workflow/SKILL.md +20 -0
  52. package/skills/agent-workflow/references/four-refs.md +74 -0
  53. package/skills/blackboard/SKILL.md +21 -1
  54. package/skills/github-issue/SKILL.md +6 -0
  55. package/skills/manifest.json +11 -5
  56. package/skills/postgres-node-performance-tuning/SKILL.md +2 -0
  57. package/skills/postgres-node-performance-tuning/references/performance-patterns.md +76 -0
  58. package/skills/postgres-node-performance-tuning/references/reading-plans.md +111 -0
  59. package/skills/postgres-schema-design/references/relations.md +18 -0
  60. package/skills/retrospective-distill/SKILL.md +67 -4
  61. package/skills/revisit-followups/SKILL.md +17 -0
  62. package/skills/stacked-prs/SKILL.md +47 -16
  63. package/skills/stacked-prs/references/drain.md +168 -0
  64. package/skills/stacked-prs/references/gh-stack.md +143 -0
@@ -6,7 +6,13 @@
6
6
  "plugin": "vouchington-workflow",
7
7
  "pluginVersion": "0.12.0",
8
8
  "path": "agent-workflow/SKILL.md",
9
- "prerequisites": ["github-issue", "pr-description", "review-ci-logs"]
9
+ "prerequisites": [
10
+ "blackboard",
11
+ "github-issue",
12
+ "pr-description",
13
+ "retrospective",
14
+ "review-ci-logs"
15
+ ]
10
16
  },
11
17
  {
12
18
  "name": "backend-vitest-test-authoring",
@@ -24,7 +30,7 @@
24
30
  {
25
31
  "name": "bounded-iteration",
26
32
  "plugin": "vouchington-database",
27
- "pluginVersion": "0.6.0",
33
+ "pluginVersion": "0.7.0",
28
34
  "path": "bounded-iteration/SKILL.md"
29
35
  },
30
36
  {
@@ -109,19 +115,19 @@
109
115
  {
110
116
  "name": "postgres-node-performance-tuning",
111
117
  "plugin": "vouchington-database",
112
- "pluginVersion": "0.6.0",
118
+ "pluginVersion": "0.7.0",
113
119
  "path": "postgres-node-performance-tuning/SKILL.md"
114
120
  },
115
121
  {
116
122
  "name": "postgres-partitioning-uuid-v7",
117
123
  "plugin": "vouchington-database",
118
- "pluginVersion": "0.6.0",
124
+ "pluginVersion": "0.7.0",
119
125
  "path": "postgres-partitioning-uuid-v7/SKILL.md"
120
126
  },
121
127
  {
122
128
  "name": "postgres-schema-design",
123
129
  "plugin": "vouchington-database",
124
- "pluginVersion": "0.6.0",
130
+ "pluginVersion": "0.7.0",
125
131
  "path": "postgres-schema-design/SKILL.md"
126
132
  },
127
133
  {
@@ -20,6 +20,8 @@ Measure the query plan and the workload shape before changing code.
20
20
  connection pressure together.
21
21
  - Cut round trips: fold dependent reads, batch per-item lookups, and overlap independent reads; see
22
22
  [round trips](references/performance-patterns.md#round-trips).
23
+ - Read the plan for hidden per-row work before trusting a change; see
24
+ [reading plans](references/reading-plans.md).
23
25
  - Read [performance patterns](references/performance-patterns.md) before changing a high-volume
24
26
  path.
25
27
 
@@ -38,6 +38,82 @@ recipient-keyed fan-out rows. Verify actual pruning with EXPLAIN.
38
38
  Generic `jonathanong/no-mistakes` checks include `postgres-no-offset`; inspect available rules
39
39
  and configure reviewed cross-partition exceptions rather than treating a rule as plan evidence.
40
40
 
41
+ ## Volatile functions in predicates
42
+
43
+ `uuidv7(interval)` and `random()` are volatile: PostgreSQL evaluates them for every row, so
44
+ `WHERE id >= uuidv7(INTERVAL '-30 days')` is a `Filter`, cannot be an `Index Cond`, and blocks
45
+ partition pruning. Compute the bound once per statement and compare against that. A bind
46
+ parameter works, as does a stable expression such as `now() - INTERVAL '30 days'` fed to a stable
47
+ or immutable bound helper. A stable bound can be an index condition, and a partitioned table
48
+ prunes at executor start. Confirm `Index Cond` and `Subplans Removed` in the plan.
49
+
50
+ ```sql
51
+ -- evaluated per row
52
+ SELECT id FROM events WHERE id >= uuidv7(INTERVAL '-30 days');
53
+ -- bound computed once by the caller
54
+ SELECT id FROM events WHERE id >= $1;
55
+ ```
56
+
57
+ ## Function inlining
58
+
59
+ PostgreSQL inlines a simple scalar SQL function into the calling query, so its body is planned
60
+ with the outer query and its predicates can become index conditions. A SQL function whose body
61
+ contains a subquery (for example `EXISTS`) is never inlined as a scalar call; a set-returning SQL
62
+ function in `FROM` follows a separate inlining path. Functions with `SECURITY DEFINER` or
63
+ a `SET` clause, and PL/pgSQL functions, are not inlined either, nor is a body that is not a single
64
+ simple expression. When the function is not inlined, the plan shows only `fn_x(col)` in a `Filter`
65
+ or output, and none of the queries it runs per row.
66
+
67
+ ```sql
68
+ -- not inlined: the EXISTS runs once per orders row
69
+ CREATE FUNCTION fn_has_events(order_id uuid) RETURNS boolean
70
+ LANGUAGE sql STABLE AS $$ SELECT EXISTS (SELECT 1 FROM events e WHERE e.order_id = $1) $$;
71
+ SELECT id FROM orders WHERE fn_has_events(id);
72
+ -- plannable: write the semi-join in the query
73
+ SELECT o.id FROM orders o WHERE EXISTS (SELECT 1 FROM events e WHERE e.order_id = o.id);
74
+ ```
75
+
76
+ Tell from the plan: a function name in `Filter` or output with no matching scan nodes. Count calls
77
+ with `SET LOCAL track_functions = 'all'` and `pg_stat_xact_user_functions` in the same transaction;
78
+ inlined functions are not counted. See [reading plans](reading-plans.md#opaque-function-calls).
79
+ Put per-row logic in the query as a join or `EXISTS`, or keep the function a plain expression.
80
+
81
+ ## Reading views by key
82
+
83
+ PostgreSQL expands a view into the calling query, and a simple view lets the outer filter push
84
+ down into its base tables. A view that cannot be flattened (aggregation, `DISTINCT`, `LIMIT`,
85
+ set operations, window functions, volatile target-list expressions, a security barrier) is built
86
+ before a filter on a value it derives (such as an aggregate) applies, and before a sort, page, or
87
+ count over its output. A restriction on a grouping column can still be pushed below an aggregate,
88
+ so let the plan decide. Confirm from the
89
+ plan (large `Actual Rows` and `Actual Loops` inside the view's nodes), then choose the ids from the
90
+ base tables first and read the view by its key, as a correlated `LATERAL` lookup so the page
91
+ restriction can reach inside the view. This works for an aggregate or `GROUP BY` view whose key is
92
+ a grouping column; a `LIMIT` in the view blocks pushdown of any qual, so rewrite that view's query
93
+ against the base tables instead. A plain join to the page is a join clause, not a restriction
94
+ inside the view, so the planner can still build the whole aggregate first.
95
+
96
+ ```sql
97
+ -- order_summaries aggregates events, so event_count is built for every order, then filtered
98
+ SELECT * FROM order_summaries WHERE event_count > 3 ORDER BY id LIMIT 50;
99
+ -- choose ids from base tables with an equivalent predicate, then read the view by key
100
+ WITH page AS (
101
+ SELECT o.id FROM orders o
102
+ WHERE (SELECT count(*) FROM events e WHERE e.order_id = o.id) > 3
103
+ ORDER BY o.id LIMIT 50
104
+ )
105
+ SELECT s.id, s.total_cents, s.event_count
106
+ FROM page
107
+ CROSS JOIN LATERAL (
108
+ SELECT * FROM order_summaries WHERE id = page.id
109
+ ) s
110
+ ORDER BY s.id;
111
+ ```
112
+
113
+ Filter and sort on indexed base-table columns, and read only the view columns the page needs.
114
+ Confirm in the plan that the view's inner nodes run with `Actual Loops` equal to the page size and
115
+ read only that page's rows.
116
+
41
117
  ## Round trips
42
118
 
43
119
  Sequential queries can add network round trips when the client waits between statements, and planning
@@ -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;
@@ -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.