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.
- 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 +3 -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/gh-cli/pr-create.d.mts +2 -0
- package/dist/gh-cli/pr-create.mjs +5 -1
- package/dist/index.d.mts +6 -0
- package/dist/index.mjs +3 -0
- package/dist/mcp-server/context.d.mts +11 -0
- package/dist/mcp-server/dispatch.d.mts +8 -0
- package/dist/mcp-server/dispatch.mjs +7 -0
- package/dist/mcp-server/handlers-pr.d.mts +4 -0
- package/dist/mcp-server/handlers-pr.mjs +143 -0
- package/dist/mcp-server/harness.test-helpers.d.mts +8 -1
- package/dist/mcp-server/harness.test-helpers.mjs +3 -1
- package/dist/mcp-server/pr-body-file.d.mts +4 -0
- package/dist/mcp-server/pr-body-file.mjs +22 -0
- package/dist/mcp-server/pr-body.d.mts +20 -0
- package/dist/mcp-server/pr-body.mjs +47 -0
- package/dist/mcp-server/pr-defaults.d.mts +16 -0
- package/dist/mcp-server/pr-defaults.mjs +35 -0
- package/dist/mcp-server/pr-gh.d.mts +16 -0
- package/dist/mcp-server/pr-gh.mjs +56 -0
- package/dist/mcp-server/pr-input.d.mts +26 -0
- package/dist/mcp-server/pr-input.mjs +62 -0
- package/dist/mcp-server/pr-journal.d.mts +12 -0
- package/dist/mcp-server/pr-journal.mjs +86 -0
- package/dist/mcp-server/pr-live.d.mts +24 -0
- package/dist/mcp-server/pr-live.mjs +49 -0
- package/dist/mcp-server/pr-tools.test-helpers.d.mts +54 -0
- package/dist/mcp-server/pr-tools.test-helpers.mjs +67 -0
- package/dist/mcp-server/tools-pr.d.mts +2 -0
- package/dist/mcp-server/tools-pr.mjs +69 -0
- package/dist/mcp-server/tools.mjs +2 -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 +80 -6
- 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/pr-description/SKILL.md +8 -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
|
@@ -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,
|
|
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.
|
|
@@ -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.
|