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.
- package/README.md +45 -1
- package/dist/cli/commands/fresh-base.d.mts +6 -0
- package/dist/cli/commands/fresh-base.mjs +14 -0
- package/dist/cli/index.mjs +3 -0
- package/dist/cli/parse-fresh-base.d.mts +2 -0
- package/dist/cli/parse-fresh-base.mjs +15 -0
- package/dist/cli/parse.d.mts +5 -0
- package/dist/cli/parse.mjs +3 -0
- package/dist/cli/usage.d.mts +1 -1
- package/dist/cli/usage.mjs +2 -0
- package/dist/fresh-base/classify.d.mts +21 -0
- package/dist/fresh-base/classify.mjs +29 -0
- package/dist/fresh-base/index.d.mts +4 -0
- package/dist/fresh-base/index.mjs +2 -0
- package/dist/fresh-base/run.d.mts +25 -0
- package/dist/fresh-base/run.mjs +66 -0
- package/dist/gh-cli/closing-refs.d.mts +24 -0
- package/dist/gh-cli/closing-refs.mjs +37 -0
- package/dist/gh-cli/index.d.mts +3 -0
- package/dist/gh-cli/index.mjs +2 -0
- package/dist/gh-cli/non-closing-refs.d.mts +11 -0
- package/dist/gh-cli/non-closing-refs.mjs +24 -0
- package/dist/index.d.mts +6 -0
- package/dist/index.mjs +3 -0
- package/dist/pull-request-diff/diff-size.d.mts +29 -0
- package/dist/pull-request-diff/diff-size.mjs +35 -0
- package/dist/pull-request-diff/index.d.mts +2 -0
- package/dist/pull-request-diff/index.mjs +1 -0
- package/dist/retrospective-distill/args.d.mts +13 -0
- package/dist/retrospective-distill/args.mjs +27 -0
- package/dist/retrospective-distill/checkpoint-entry.d.mts +2 -0
- package/dist/retrospective-distill/checkpoint-entry.mjs +11 -0
- package/dist/retrospective-distill/fixtures.test-helpers.d.mts +3 -0
- package/dist/retrospective-distill/fixtures.test-helpers.mjs +21 -0
- package/dist/retrospective-distill/flag-args.d.mts +5 -0
- package/dist/retrospective-distill/flag-args.mjs +31 -0
- package/dist/retrospective-distill/index.d.mts +10 -0
- package/dist/retrospective-distill/index.mjs +6 -0
- package/dist/retrospective-distill/partition-records.d.mts +6 -0
- package/dist/retrospective-distill/partition-records.mjs +49 -0
- package/dist/retrospective-distill/retrospective-entry.d.mts +3 -0
- package/dist/retrospective-distill/retrospective-entry.mjs +4 -0
- package/dist/retrospective-distill/run.d.mts +5 -0
- package/dist/retrospective-distill/run.mjs +28 -0
- package/dist/retrospective-distill/session-types.d.mts +17 -0
- package/dist/retrospective-distill/session-types.mjs +1 -0
- package/dist/retrospective-distill/shape.d.mts +29 -0
- package/dist/retrospective-distill/shape.mjs +111 -0
- package/docs/mcp-server.md +1 -1
- package/package.json +18 -3
- package/skills/agent-workflow/SKILL.md +20 -0
- package/skills/agent-workflow/references/four-refs.md +74 -0
- package/skills/blackboard/SKILL.md +21 -1
- package/skills/github-issue/SKILL.md +6 -0
- package/skills/manifest.json +11 -5
- package/skills/postgres-node-performance-tuning/SKILL.md +2 -0
- package/skills/postgres-node-performance-tuning/references/performance-patterns.md +76 -0
- package/skills/postgres-node-performance-tuning/references/reading-plans.md +111 -0
- package/skills/postgres-schema-design/references/relations.md +18 -0
- package/skills/retrospective-distill/SKILL.md +67 -4
- package/skills/revisit-followups/SKILL.md +17 -0
- package/skills/stacked-prs/SKILL.md +47 -16
- package/skills/stacked-prs/references/drain.md +168 -0
- package/skills/stacked-prs/references/gh-stack.md +143 -0
package/skills/manifest.json
CHANGED
|
@@ -6,7 +6,13 @@
|
|
|
6
6
|
"plugin": "vouchington-workflow",
|
|
7
7
|
"pluginVersion": "0.12.0",
|
|
8
8
|
"path": "agent-workflow/SKILL.md",
|
|
9
|
-
"prerequisites": [
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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,
|
|
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
|
|
25
|
-
|
|
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.
|
|
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:
|
|
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
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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
|
|
35
|
-
Report every layer and ask before merging that
|
|
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,
|
|
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.
|