vouchington-tooling 0.42.0 → 0.44.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (93) hide show
  1. package/README.md +45 -1
  2. package/dist/cli/commands/fresh-base.d.mts +6 -0
  3. package/dist/cli/commands/fresh-base.mjs +14 -0
  4. package/dist/cli/index.mjs +3 -0
  5. package/dist/cli/parse-fresh-base.d.mts +2 -0
  6. package/dist/cli/parse-fresh-base.mjs +15 -0
  7. package/dist/cli/parse.d.mts +5 -0
  8. package/dist/cli/parse.mjs +3 -0
  9. package/dist/cli/usage.d.mts +1 -1
  10. package/dist/cli/usage.mjs +3 -0
  11. package/dist/fresh-base/classify.d.mts +21 -0
  12. package/dist/fresh-base/classify.mjs +29 -0
  13. package/dist/fresh-base/index.d.mts +4 -0
  14. package/dist/fresh-base/index.mjs +2 -0
  15. package/dist/fresh-base/run.d.mts +25 -0
  16. package/dist/fresh-base/run.mjs +66 -0
  17. package/dist/gh-cli/closing-refs.d.mts +24 -0
  18. package/dist/gh-cli/closing-refs.mjs +37 -0
  19. package/dist/gh-cli/index.d.mts +3 -0
  20. package/dist/gh-cli/index.mjs +2 -0
  21. package/dist/gh-cli/non-closing-refs.d.mts +11 -0
  22. package/dist/gh-cli/non-closing-refs.mjs +24 -0
  23. package/dist/gh-cli/pr-create.d.mts +2 -0
  24. package/dist/gh-cli/pr-create.mjs +5 -1
  25. package/dist/index.d.mts +6 -0
  26. package/dist/index.mjs +3 -0
  27. package/dist/mcp-server/context.d.mts +11 -0
  28. package/dist/mcp-server/dispatch.d.mts +8 -0
  29. package/dist/mcp-server/dispatch.mjs +7 -0
  30. package/dist/mcp-server/handlers-pr.d.mts +4 -0
  31. package/dist/mcp-server/handlers-pr.mjs +143 -0
  32. package/dist/mcp-server/harness.test-helpers.d.mts +8 -1
  33. package/dist/mcp-server/harness.test-helpers.mjs +3 -1
  34. package/dist/mcp-server/pr-body-file.d.mts +4 -0
  35. package/dist/mcp-server/pr-body-file.mjs +22 -0
  36. package/dist/mcp-server/pr-body.d.mts +20 -0
  37. package/dist/mcp-server/pr-body.mjs +47 -0
  38. package/dist/mcp-server/pr-defaults.d.mts +16 -0
  39. package/dist/mcp-server/pr-defaults.mjs +35 -0
  40. package/dist/mcp-server/pr-gh.d.mts +16 -0
  41. package/dist/mcp-server/pr-gh.mjs +56 -0
  42. package/dist/mcp-server/pr-input.d.mts +26 -0
  43. package/dist/mcp-server/pr-input.mjs +62 -0
  44. package/dist/mcp-server/pr-journal.d.mts +12 -0
  45. package/dist/mcp-server/pr-journal.mjs +86 -0
  46. package/dist/mcp-server/pr-live.d.mts +24 -0
  47. package/dist/mcp-server/pr-live.mjs +49 -0
  48. package/dist/mcp-server/pr-tools.test-helpers.d.mts +54 -0
  49. package/dist/mcp-server/pr-tools.test-helpers.mjs +67 -0
  50. package/dist/mcp-server/tools-pr.d.mts +2 -0
  51. package/dist/mcp-server/tools-pr.mjs +69 -0
  52. package/dist/mcp-server/tools.mjs +2 -0
  53. package/dist/pull-request-diff/diff-size.d.mts +29 -0
  54. package/dist/pull-request-diff/diff-size.mjs +35 -0
  55. package/dist/pull-request-diff/index.d.mts +2 -0
  56. package/dist/pull-request-diff/index.mjs +1 -0
  57. package/dist/retrospective-distill/args.d.mts +13 -0
  58. package/dist/retrospective-distill/args.mjs +27 -0
  59. package/dist/retrospective-distill/checkpoint-entry.d.mts +2 -0
  60. package/dist/retrospective-distill/checkpoint-entry.mjs +11 -0
  61. package/dist/retrospective-distill/fixtures.test-helpers.d.mts +3 -0
  62. package/dist/retrospective-distill/fixtures.test-helpers.mjs +21 -0
  63. package/dist/retrospective-distill/flag-args.d.mts +5 -0
  64. package/dist/retrospective-distill/flag-args.mjs +31 -0
  65. package/dist/retrospective-distill/index.d.mts +10 -0
  66. package/dist/retrospective-distill/index.mjs +6 -0
  67. package/dist/retrospective-distill/partition-records.d.mts +6 -0
  68. package/dist/retrospective-distill/partition-records.mjs +49 -0
  69. package/dist/retrospective-distill/retrospective-entry.d.mts +3 -0
  70. package/dist/retrospective-distill/retrospective-entry.mjs +4 -0
  71. package/dist/retrospective-distill/run.d.mts +5 -0
  72. package/dist/retrospective-distill/run.mjs +28 -0
  73. package/dist/retrospective-distill/session-types.d.mts +17 -0
  74. package/dist/retrospective-distill/session-types.mjs +1 -0
  75. package/dist/retrospective-distill/shape.d.mts +29 -0
  76. package/dist/retrospective-distill/shape.mjs +111 -0
  77. package/docs/mcp-server.md +80 -6
  78. package/package.json +18 -3
  79. package/skills/agent-workflow/SKILL.md +20 -0
  80. package/skills/agent-workflow/references/four-refs.md +74 -0
  81. package/skills/blackboard/SKILL.md +21 -1
  82. package/skills/github-issue/SKILL.md +6 -0
  83. package/skills/manifest.json +11 -5
  84. package/skills/postgres-node-performance-tuning/SKILL.md +2 -0
  85. package/skills/postgres-node-performance-tuning/references/performance-patterns.md +76 -0
  86. package/skills/postgres-node-performance-tuning/references/reading-plans.md +111 -0
  87. package/skills/postgres-schema-design/references/relations.md +18 -0
  88. package/skills/pr-description/SKILL.md +8 -0
  89. package/skills/retrospective-distill/SKILL.md +67 -4
  90. package/skills/revisit-followups/SKILL.md +17 -0
  91. package/skills/stacked-prs/SKILL.md +47 -16
  92. package/skills/stacked-prs/references/drain.md +168 -0
  93. package/skills/stacked-prs/references/gh-stack.md +143 -0
@@ -0,0 +1,111 @@
1
+ import { isDeepStrictEqual } from 'node:util';
2
+ import { validateFeedbackEnvelope } from "../agent-blackboard/index.mjs";
3
+ import { isCheckpointEntry } from "./checkpoint-entry.mjs";
4
+ import { isRetrospectiveEntry } from "./retrospective-entry.mjs";
5
+ function quarantineReason(entry) {
6
+ if (entry.data.type !== 'journal' && entry.data.type !== 'retrospective')
7
+ return 'unknown-entry-type';
8
+ if (entry.data.schemaVersion !== undefined) {
9
+ try {
10
+ validateFeedbackEnvelope(entry.data);
11
+ }
12
+ catch {
13
+ return 'invalid-feedback-envelope';
14
+ }
15
+ }
16
+ return undefined;
17
+ }
18
+ function hasUnresolvedType(entry) {
19
+ return quarantineReason(entry) !== undefined;
20
+ }
21
+ function classifyShape(session, entries) {
22
+ if (entries.some(hasUnresolvedType))
23
+ return 'entry-type-unresolved';
24
+ if (entries.some(isRetrospectiveEntry))
25
+ return 'retrospective';
26
+ if (entries.length === 0) {
27
+ // Loose equality on purpose: a partition record missing `parentSessionId` entirely (the reader
28
+ // trusts the file on disk and does not re-validate every field against the Session interface —
29
+ // see partition-records.mts) must fall to zero-entry-root. This is a topology and capture-gap
30
+ // signal, never evidence of crash or clean completion. `=== null` would silently misroute
31
+ // `undefined` into the wrong, discarded bucket.
32
+ return session.parentSessionId == null ? 'zero-entry-root' : 'zero-entry-child';
33
+ }
34
+ return entries.every(isCheckpointEntry) ? 'checkpoint-only' : 'journal-only';
35
+ }
36
+ function newestRetrospectiveAt(entries) {
37
+ let newest;
38
+ for (const entry of entries) {
39
+ if (!isRetrospectiveEntry(entry))
40
+ continue;
41
+ if (newest === undefined || Date.parse(entry.createdAt) > Date.parse(newest))
42
+ newest = entry.createdAt;
43
+ }
44
+ return newest;
45
+ }
46
+ // Eligibility rule:
47
+ // (has a retrospective entry AND retroAt < retroCutoff) OR (lastActive < sessionCutoff).
48
+ // An entry-type-unresolved session gets no issue-filing or archival pass "this run, regardless of
49
+ // its age", so it is never eligible — that check short-circuits before the formula.
50
+ function isEligible(session, entries, shape, cutoffs) {
51
+ if (shape === 'entry-type-unresolved')
52
+ return false;
53
+ const retroAt = newestRetrospectiveAt(entries);
54
+ const staleRetro = retroAt !== undefined && Date.parse(retroAt) < Date.parse(cutoffs.retroCutoff);
55
+ const lastActive = entries.reduce((latest, entry) => Date.parse(entry.createdAt) > Date.parse(latest) ? entry.createdAt : latest, session.createdAt);
56
+ const staleSession = Date.parse(lastActive) < Date.parse(cutoffs.sessionCutoff);
57
+ return staleRetro || staleSession;
58
+ }
59
+ // An event is its session, `sourceEventId`, and content; the server assigns `timestamp` per attempt
60
+ // (a retry or a late write beside it can store the same event twice), so it is not identity.
61
+ function withoutTimestamp(data) {
62
+ return Object.fromEntries(Object.entries(data).filter(([key]) => key !== 'timestamp'));
63
+ }
64
+ function normalizeSourceEvents(entries) {
65
+ const events = new Map();
66
+ const unique = [];
67
+ let duplicateEntryCount = 0;
68
+ let conflictCount = 0;
69
+ for (const entry of entries) {
70
+ if (entry.data.schemaVersion !== 1 || quarantineReason(entry) !== undefined) {
71
+ unique.push(entry);
72
+ continue;
73
+ }
74
+ const key = `${entry.sessionId}:${String(entry.data.sourceEventId)}`;
75
+ const existing = events.get(key);
76
+ if (!existing) {
77
+ events.set(key, entry);
78
+ unique.push(entry);
79
+ continue;
80
+ }
81
+ if (!isDeepStrictEqual(withoutTimestamp(existing.data), withoutTimestamp(entry.data))) {
82
+ conflictCount++;
83
+ continue;
84
+ }
85
+ duplicateEntryCount++;
86
+ // At-least-once replay does not postpone a source event's distillation age.
87
+ if (Date.parse(entry.createdAt) < Date.parse(existing.createdAt)) {
88
+ unique[unique.indexOf(existing)] = entry;
89
+ events.set(key, entry);
90
+ }
91
+ }
92
+ return { unique, duplicateEntryCount, conflictCount };
93
+ }
94
+ export function classifySession(session, entries, cutoffs) {
95
+ const { unique, duplicateEntryCount, conflictCount } = normalizeSourceEvents(entries);
96
+ const shape = conflictCount ? 'entry-type-unresolved' : classifyShape(session, unique);
97
+ const reasons = entries
98
+ .map(quarantineReason)
99
+ .filter((reason) => reason !== undefined);
100
+ for (let index = 0; index < conflictCount; index++)
101
+ reasons.push('conflicting-source-event');
102
+ return {
103
+ sessionId: session.id,
104
+ shape,
105
+ eligible: isEligible(session, unique, shape, cutoffs),
106
+ ...(duplicateEntryCount ? { duplicateEntryCount } : {}),
107
+ ...(reasons.length === 0
108
+ ? {}
109
+ : { quarantine: { entryCount: reasons.length, reasons: [...new Set(reasons)].toSorted() } }),
110
+ };
111
+ }
@@ -7,7 +7,7 @@ existing `vouchington-tooling/agent-blackboard` exports and adds no provider pro
7
7
 
8
8
  Phase 1 of [vouchington-tooling#324](https://github.com/vouchington/vouchington-tooling/issues/324)
9
9
  covers the journal, outbox, session, and snapshot tools below. Phase 2 adds the retrospective tools
10
- and the policy contract they run. Pull request tools are a later phase.
10
+ and the policy contract they run. Phase 3 adds the pull request tools.
11
11
 
12
12
  ## Install
13
13
 
@@ -225,16 +225,90 @@ whole process group is killed, so nothing it started outlives the call. The runn
225
225
  `{ policyVersion: 1, export, input }` on stdin and writes `{ ok: true, result }` (or
226
226
  `{ ok: false, error }`) on stdout; a missing export is skipped.
227
227
 
228
- | Export | Input | Result | Timeout |
229
- | ----------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------- | ------- |
230
- | `validateRetrospective` | `sessionId`, `parentSessionId`, `mode`, `workOutcome`, `feedbackCoverage`, `repositories`, `markdown`, `sourceEventId`, `metadata` (`date`, `issues`, `prs`, `description`) | `{ errors: string[], warnings?: string[] }` | 30 s |
231
- | `pullRequestDefaults` | `{}` | `{ repositories: string[] }` | 120 s |
232
- | `validatePullRequest` | reserved for the pull request tools | `{ errors: string[], warnings?: string[] }` | 120 s |
228
+ | Export | Input | Result | Timeout |
229
+ | ----------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------- | ------- |
230
+ | `validateRetrospective` | `sessionId`, `parentSessionId`, `mode`, `workOutcome`, `feedbackCoverage`, `repositories`, `markdown`, `sourceEventId`, `metadata` (`date`, `issues`, `prs`, `description`) | `{ errors: string[], warnings?: string[] }` | 30 s |
231
+ | `pullRequestDefaults` | `{}` from `retrospective_save`; `{ title, base, relatedIssues }` from `pr_create` | `{ repositories?, labels?, milestone?, project? }` (an object) | 120 s |
232
+ | `validatePullRequest` | `title`, `body`, `base`, `head`, `mode`, and `pr` for an update | `{ errors: string[], warnings?: string[] }` | 120 s |
233
233
 
234
234
  Any `errors` reject the save and write nothing. A non-zero exit, a timeout (reported as
235
235
  `policy <export> timed out after <N>s`), output over 1 MB, malformed JSON, or a result of the wrong
236
236
  shape is an error, never a pass.
237
237
 
238
+ ## Pull request tools
239
+
240
+ `vouchington mcp` serves `pr_validate`, `pr_create` and `pr_update` besides the journal and
241
+ retrospective tools. All three take `sessionId` (required) and `worktree` (optional), need a server
242
+ launched inside a repository worktree (they run the checkout policy), and share this structured
243
+ input: `title` (single line, at most 256 characters), `summary`, `impact`, `relatedIssues`
244
+ (`[{ number, relation: "closes" | "refs", reason }]`, may be empty), `details` (string array),
245
+ `base` (default `main`), `agent`, optional `model`, `mode` (`interactive` | `autonomous`) and
246
+ `workspaceSetup`. `pr_update` also requires `pr`; `pr_validate` accepts an optional `pr`.
247
+
248
+ ### Rendering
249
+
250
+ ```
251
+ ## Summary
252
+
253
+ <summary>
254
+
255
+ ## Impact
256
+
257
+ <impact>
258
+
259
+ ## Related issues
260
+
261
+ - Closes #N — <reason> (or "Refs #N"; "- None" when empty)
262
+
263
+ ## Details
264
+
265
+ <details joined by blank lines>
266
+
267
+ Workspace setup: <workspaceSetup>
268
+ Agent: <agent>[ (<model>)] session <sessionId>
269
+ Device: <os user>@<lowercased hostname>
270
+ Worktree: <path after "/worktrees/", "main" for the main worktree, else the directory name>
271
+ ```
272
+
273
+ The head branch is the worktree's current branch; a detached HEAD, or a head equal to `base`, is
274
+ rejected. The body must stay within GitHub's 65536 characters. `pr_create` sees no stack membership,
275
+ so a consumer policy may reject any `base` other than the default branch; create stacked layers
276
+ with the repository's stack tooling instead.
277
+
278
+ ### Tools
279
+
280
+ - `pr_validate` (read-only): renders the body, runs the policy `validatePullRequest({ title, body,
281
+ base, head, mode, pr? })` export and returns `{ body, errors, warnings }`. With `pr`, the live
282
+ body is read (never written) so its Shepherd Journal is preserved as in `pr_update`.
283
+ - `pr_create` (additive): renders and validates (errors block), then calls the policy
284
+ `pullRequestDefaults({ title, base, relatedIssues })` export for optional `labels`, `milestone`
285
+ and `project`. It checks the head is pushed and current, writes the body to a mode 0600 temp file
286
+ (removed afterwards) and runs `gh pr create --draft` with the labels, `--milestone` and
287
+ `--project`. Returns `{ number, url, warnings }`.
288
+ - `pr_update` (mutating): reads `gh pr view <pr> --json body,headRefName,baseRefName,state`,
289
+ rejects a pull request that is not `OPEN` or whose head is not the worktree branch, and validates
290
+ against the live base (an explicit `base` must equal it). It renders, appends the live Shepherd
291
+ Journal (a `<details><summary>Shepherd Journal</summary>` block, or a legacy `## Shepherd Journal`
292
+ section to the next H2 or the end; examples in fenced code blocks are ignored), validates with
293
+ `pr` set (errors block), then runs `gh pr edit <pr> --title --body-file`. More than one journal
294
+ in the live body, one in the rendered fields, an unclosed journal, or rendered fields that leave
295
+ a code fence open are rejected rather than guessed. Just before the edit the live body is read
296
+ again, and the update aborts if its journal changed meanwhile. Nothing else is carried over from
297
+ the live body. Returns `{ number, url, warnings }`.
298
+
299
+ Every field is redacted of `AGENT_BLACKBOARD_TOKEN`, `GH_TOKEN`, and `GITHUB_TOKEN` before
300
+ rendering, the policy, or `gh`; a worktree branch name containing one is rejected. `title` and
301
+ `workspaceSetup` must be single lines, and the title is limited to 256 characters after redaction.
302
+
303
+ ### gh access
304
+
305
+ `gh` is found in the absolute directories of the server's `PATH` (relative entries are skipped,
306
+ since they would resolve inside the target worktree), else at `/opt/homebrew/bin/gh`, `/usr/local/bin/gh` or
307
+ `/usr/bin/gh`; otherwise the tool fails with "gh CLI not found on PATH; install gh or add it to the
308
+ server's PATH". It runs with the worktree as its working directory and the server environment
309
+ without `AGENT_BLACKBOARD_URL`, `AGENT_BLACKBOARD_TOKEN`, and `GH_REPO` (which would point it at
310
+ another repository), and each call is killed after 120 seconds.
311
+
238
312
  ## Worktree validation
239
313
 
240
314
  `worktree` is checked on every call. It must be an absolute path that, after resolving symlinks,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vouchington-tooling",
3
- "version": "0.42.0",
3
+ "version": "0.44.0",
4
4
  "description": "Vouchington CLI and extractable tooling libraries.",
5
5
  "homepage": "https://github.com/vouchington/vouchington-tooling/tree/main/packages/vouchington-tooling#readme",
6
6
  "bugs": {
@@ -101,6 +101,11 @@
101
101
  "import": "./dist/require-up-to-date/index.mjs",
102
102
  "default": "./dist/require-up-to-date/index.mjs"
103
103
  },
104
+ "./fresh-base": {
105
+ "types": "./dist/fresh-base/index.d.mts",
106
+ "import": "./dist/fresh-base/index.mjs",
107
+ "default": "./dist/fresh-base/index.mjs"
108
+ },
104
109
  "./gitleaks-directory-scan": {
105
110
  "types": "./dist/gitleaks-directory-scan/index.d.mts",
106
111
  "import": "./dist/gitleaks-directory-scan/index.mjs",
@@ -136,6 +141,11 @@
136
141
  "import": "./dist/shared-context/index.mjs",
137
142
  "default": "./dist/shared-context/index.mjs"
138
143
  },
144
+ "./retrospective-distill": {
145
+ "types": "./dist/retrospective-distill/index.d.mts",
146
+ "import": "./dist/retrospective-distill/index.mjs",
147
+ "default": "./dist/retrospective-distill/index.mjs"
148
+ },
139
149
  "./source-module-graph": {
140
150
  "types": "./dist/source-module-graph/index.d.mts",
141
151
  "import": "./dist/source-module-graph/index.mjs",
@@ -321,6 +331,11 @@
321
331
  "import": "./dist/compiler-build/index.mjs",
322
332
  "default": "./dist/compiler-build/index.mjs"
323
333
  },
334
+ "./pull-request-diff": {
335
+ "types": "./dist/pull-request-diff/index.d.mts",
336
+ "import": "./dist/pull-request-diff/index.mjs",
337
+ "default": "./dist/pull-request-diff/index.mjs"
338
+ },
324
339
  "./github-projects": {
325
340
  "types": "./dist/github-projects/index.d.mts",
326
341
  "import": "./dist/github-projects/index.mjs",
@@ -364,7 +379,7 @@
364
379
  "dependencies": {
365
380
  "@types/mdast": "4.0.4",
366
381
  "@types/unist": "3.0.3",
367
- "@typescript-eslint/parser": "^8.71.0",
382
+ "@typescript-eslint/parser": "^8.71.1",
368
383
  "csv-parse": "7.0.3",
369
384
  "csv-stringify": "6.9.0",
370
385
  "dockerfile-ast": "0.7.1",
@@ -377,7 +392,7 @@
377
392
  },
378
393
  "devDependencies": {
379
394
  "@ast-grep/cli": "0.45.3",
380
- "@modelcontextprotocol/sdk": "1.32.0",
395
+ "@modelcontextprotocol/sdk": "1.32.1",
381
396
  "@types/picomatch": "^4.0.3",
382
397
  "@types/spdx-expression-parse": "4.0.0",
383
398
  "agent-blackboard": "^0.6.1",
@@ -22,10 +22,30 @@ evidence record.
22
22
  - Before handoff, use [evidence sweep](references/evidence-sweep.md).
23
23
  - For review, use [review](references/review.md).
24
24
  - For surfaced feedback, use [review response](references/review-response.md).
25
+ - Before any rebase or lease push, use [four refs](references/four-refs.md).
25
26
 
26
27
  Load only the references required by the current phase. Keep scope changes explicit; a discovered
27
28
  blocker or follow-up does not authorize widening the task.
28
29
 
30
+ ## Close-out
31
+
32
+ This section is the single home for the hand-off retrospective and merge-note rules.
33
+
34
+ - Report actual validation, open pull requests, and remaining blockers.
35
+ - **Hand-off retrospective.** When you hand a pull request to the human as ready, write one
36
+ [retrospective](../retrospective/SKILL.md) from the session that did the work, which reads that
37
+ session's transcripts and journal. A native stack gets one retrospective when the stack is handed
38
+ off, covering every layer; never one per layer or for an intermediate commit.
39
+ - Give each hand-off its own `sourceEventId`, such as `handoff-pr<number>` (a stack uses its top
40
+ pull request's number), on both `retrospective_check` and `retrospective_save`. The default ID
41
+ fits only the first hand-off in a session; a second would be mistaken for it.
42
+ - **Merge note.** If you are still running when the merge happens and it changes the outcome, such
43
+ as a merge-queue ejection and its fix, a follow-up push, or review comments addressed after
44
+ hand-off, append one short note with the [blackboard skill](../blackboard/SKILL.md). Name the
45
+ pull request and the retrospective's source event, and give the note its own `sourceEventId` (for
46
+ example `merge-pr<number>`): the writer rejects the retrospective's ID reused for different
47
+ content. Never write a second retrospective.
48
+
29
49
  ## Parallel by default
30
50
 
31
51
  Split work into independent units and run them concurrently, one worker and worktree each, within
@@ -0,0 +1,74 @@
1
+ # Reading the four refs
2
+
3
+ Use before any rebase or lease push of a branch other agents or humans may also write. Publish this
4
+ branch; do not open a replacement pull request.
5
+
6
+ `origin` and `main` below stand for the consumer's remote and default branch; substitute the names
7
+ local policy declares.
8
+
9
+ ## Setup
10
+
11
+ - Run `git fetch origin main <branch>` as its own command. Never chain the fetch with a rebase,
12
+ reset, or merge.
13
+ - HEAD must be the branch being updated. A stack checkout can leave the top layer checked out, so
14
+ check out the layer first. A missing stack field means membership is unknown, not unstacked.
15
+ - A dirty worktree stops you before a fast-forward, reset, or rebase. Do not switch branches.
16
+ - Continue a conflicted rebase with `GIT_EDITOR=true git rebase --continue`.
17
+
18
+ ## Read the refs
19
+
20
+ - Print `git rev-parse` of HEAD, `origin/<branch>`, `origin/main`, and local `main` when it exists.
21
+ Local `main` is never the rebase target; when it exists, `git rev-list --left-right --count
22
+ main...origin/main` with a nonzero right count means local `main` is behind.
23
+ - Print `git rev-list --left-right --count HEAD...origin/<branch>` (left: commits only on the
24
+ local branch; right: commits only on `origin/<branch>`) and
25
+ `git log --oneline --left-right HEAD...origin/<branch>`.
26
+ - Print `git cherry -v origin/<branch> HEAD` and `git cherry -v HEAD origin/<branch>`. A leading `-`
27
+ is the same patch under another SHA; a leading `+` is a patch the other side lacks.
28
+ - Treat `git range-diff` as a reading aid, not the rebase cut, and read it from the base those
29
+ commits were built on. Never run it from a new stack parent: after a stack rebase, old parent
30
+ commits look like unique local work.
31
+ - Unmatched commits on `origin/<branch>` are new commits someone pushed.
32
+
33
+ ## Lease push
34
+
35
+ - Capture `<sha>` with `git rev-parse origin/<branch>` after the fetch and before any rewrite. Push
36
+ a non-fast-forward with `git push --force-with-lease=<branch>:<sha> origin <branch>`, always
37
+ naming the remote and branch so a configured push remote cannot change the destination. With concurrent writers,
38
+ capture right after the setup fetch and before any later fetch, so a later fetch cannot silently
39
+ widen the lease.
40
+ - If HEAD equals that SHA, do not push.
41
+ - If a lease or non-force push is rejected, fetch again and re-read. It is still this branch.
42
+
43
+ ## Unstacked
44
+
45
+ Before any move and after every move onto `origin/<branch>`, including when HEAD already equals it
46
+ (`0 0`) but `origin/main` has advanced, run `git merge-base --is-ancestor origin/main HEAD`. Exit 0
47
+ means the branch already contains `origin/main`; nonzero means rebase onto `origin/main`, then lease
48
+ push.
49
+
50
+ | Left/right | Action |
51
+ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
52
+ | `0 0`, `origin/main` already an ancestor | Stop. |
53
+ | `0 0`, `origin/main` not an ancestor | Rebase onto `origin/main`, then lease push. |
54
+ | `0 N` | `git merge --ff-only origin/<branch>`, then the ancestor check. |
55
+ | `N 0` | Read `git log --oneline origin/<branch>..HEAD`: those commits are what a push adds, including a remote rewind to an ancestor. Own unpushed work with `origin/main` an ancestor: `git push origin <branch>`. Otherwise rebase onto `origin/main`, then lease push. A rewind you did not make: stop and ask. |
56
+ | Diverged, local-only commits are rewritten copies (every local-only commit has cherry `-`; matching subjects alone do not prove equivalence), no unique local work | Clean tree, `git reset --hard origin/<branch>`, then the ancestor check. |
57
+ | Diverged, origin has new patches to keep and HEAD has unique local commits | `git rebase origin/<branch>` as its own command, then the ancestor check, then lease push whichever way the check exits (after another rebase onto `origin/main` if it is nonzero). This is the one added rebase target, not a license to rebase onto an arbitrary ref. If the rewrite changed patches and unique local work is mixed in, stop and ask. |
58
+
59
+ ## Stacked
60
+
61
+ A stack rebase is the usual reason `origin/<branch>` was rewritten, but another agent or a human can
62
+ rewrite an unstacked branch the same way.
63
+
64
+ - On a diverged layer, adopt `origin/<branch>` with `git reset --hard` when the layer is checked
65
+ out, or `git branch -f` when it is not, if the left-right log shows pre-rebase copies: the same
66
+ layer subjects on both sides and old parent subjects only on the left. Require `git cherry`
67
+ patch equivalence (`-`) for every local layer commit first; matching subjects alone do not
68
+ prove it, and a `+` commit is unique local work.
69
+ - Never `git rebase origin/<branch>` on a stack layer: it replays parent commits onto the layer.
70
+ - Never run the plain rebase-onto-main procedure on a mid-stack branch; use its stack-aware form.
71
+ - If the layer has unpushed commits that are not the old parent and not already on origin under a
72
+ new SHA, stop and ask.
73
+ - After adopting the rewritten tip, run the stack-aware rebase only when the stack still needs to
74
+ move onto `origin/main`.
@@ -50,7 +50,10 @@ human which case applies before continuing:
50
50
  registration and diagnosis steps.
51
51
  - Registered but disconnected: reconnect it through the harness's MCP controls, or restart the
52
52
  harness.
53
- - Automated runs with no human to ask: report "journaling unavailable" in the final report.
53
+ - Automated runs with no human to ask: an unattended `interactive` run continues its primary work,
54
+ while an `autonomous` run keeps its admission gate below. The final report states why journaling
55
+ was unavailable, the `pendingCount` and `worktreePendingCount` (or that none was written), and
56
+ each unjournaled finding.
54
57
 
55
58
  Prefer restoring MCP over the CLI fallback below, and use the fallback only when the human asks or
56
59
  MCP cannot be restored for this session. Harness sandboxes commonly withhold
@@ -176,5 +179,22 @@ bounded structured facts or sanitized summaries, never raw command output, trans
176
179
  environment dumps, provider payloads, or secrets. Known-secret redaction supplements the author's
177
180
  minimization responsibility; it cannot prove arbitrary prose contains no undisclosed secret.
178
181
 
182
+ ## Credential failures
183
+
184
+ - Never search shell profiles, `env`, or `.env*` files for a credential. Check presence only with
185
+ `[ -n "${VAR+x}" ] && echo SET || echo UNSET`; never copy the token around a sandbox denial.
186
+ - A missing or stale credential blocks hosted delivery. In interactive mode the outbox keeps the
187
+ record visibly pending: do not drop it or invent a fallback. Ask the human to refresh the
188
+ connection, then restart the MCP client or call `outbox_flush`; an automated run reports.
189
+
190
+ ## Child identity for spawned agents
191
+
192
+ - Pass the parent's own session id in the assignment prompt. The child resolves its own id from its
193
+ runtime through the consumer's identity policy, then calls `session_ensure` with it, the given
194
+ `parentSessionId`, its `agent`, and its version before other work. With no id or a failed ensure,
195
+ it stops and reports the blocker; never guess an id or pass a path, URL, or synthesized value.
196
+ - A subagent sharing its parent's id must not `session_ensure` it as a child. It asks the parent
197
+ to append, or skips this protocol.
198
+
179
199
  Consumer wrappers own service configuration, server registration and approval rules, trusted mode
180
200
  selection, issue routing, and archival authorization. The server fixes the outbox location.
@@ -99,5 +99,11 @@ For a batch, preflight every entry before writing any issue. One invalid entry f
99
99
  batch. A partial transport failure stops further creation and reports every issue already created.
100
100
  Do not retry an entry that succeeded.
101
101
 
102
+ ## Bounded output
103
+
104
+ - A search returns at most five `#N: title -- url` lines.
105
+ - A near-duplicate returns `Duplicate of #N: <url>`.
106
+ - A mutation returns the action, URL, labels, and verification, plus milestone and project when set.
107
+
102
108
  Consumer wrapper owns: repository allowlist, taxonomy, issue template, credential, and CLI wrapper.
103
109
  It may tighten this gate and may not weaken it.
@@ -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