vouchington-tooling 0.43.0 → 0.45.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 (110) hide show
  1. package/README.md +45 -1
  2. package/dist/agent-blackboard/client.d.mts +6 -0
  3. package/dist/cli/commands/fresh-base.d.mts +6 -0
  4. package/dist/cli/commands/fresh-base.mjs +14 -0
  5. package/dist/cli/index.mjs +3 -0
  6. package/dist/cli/parse-fresh-base.d.mts +2 -0
  7. package/dist/cli/parse-fresh-base.mjs +15 -0
  8. package/dist/cli/parse.d.mts +5 -0
  9. package/dist/cli/parse.mjs +3 -0
  10. package/dist/cli/usage.d.mts +1 -1
  11. package/dist/cli/usage.mjs +3 -1
  12. package/dist/fresh-base/classify.d.mts +21 -0
  13. package/dist/fresh-base/classify.mjs +29 -0
  14. package/dist/fresh-base/index.d.mts +4 -0
  15. package/dist/fresh-base/index.mjs +2 -0
  16. package/dist/fresh-base/run.d.mts +25 -0
  17. package/dist/fresh-base/run.mjs +66 -0
  18. package/dist/gh-cli/closing-refs.d.mts +24 -0
  19. package/dist/gh-cli/closing-refs.mjs +37 -0
  20. package/dist/gh-cli/index.d.mts +3 -0
  21. package/dist/gh-cli/index.mjs +2 -0
  22. package/dist/gh-cli/non-closing-refs.d.mts +11 -0
  23. package/dist/gh-cli/non-closing-refs.mjs +24 -0
  24. package/dist/index.d.mts +6 -0
  25. package/dist/index.mjs +3 -0
  26. package/dist/mcp-server/context.d.mts +3 -0
  27. package/dist/mcp-server/dispatch.d.mts +3 -0
  28. package/dist/mcp-server/dispatch.mjs +7 -1
  29. package/dist/mcp-server/fake-blackboard.test-helpers.d.mts +2 -0
  30. package/dist/mcp-server/fake-blackboard.test-helpers.mjs +3 -0
  31. package/dist/mcp-server/handlers-pr.d.mts +1 -1
  32. package/dist/mcp-server/handlers-pr.mjs +6 -97
  33. package/dist/mcp-server/handlers-retrospective-save.d.mts +10 -0
  34. package/dist/mcp-server/handlers-retrospective-save.mjs +138 -0
  35. package/dist/mcp-server/handlers-retrospective.d.mts +1 -12
  36. package/dist/mcp-server/handlers-retrospective.mjs +1 -154
  37. package/dist/mcp-server/handlers-session.mjs +25 -6
  38. package/dist/mcp-server/handlers-snapshot-cleanup.d.mts +7 -0
  39. package/dist/mcp-server/handlers-snapshot-cleanup.mjs +36 -0
  40. package/dist/mcp-server/harness.test-helpers.d.mts +2 -0
  41. package/dist/mcp-server/harness.test-helpers.mjs +1 -0
  42. package/dist/mcp-server/policy-run.d.mts +1 -0
  43. package/dist/mcp-server/policy-run.mjs +1 -0
  44. package/dist/mcp-server/pr-body.d.mts +4 -2
  45. package/dist/mcp-server/pr-body.mjs +15 -3
  46. package/dist/mcp-server/pr-defaults.d.mts +3 -0
  47. package/dist/mcp-server/pr-defaults.mjs +7 -2
  48. package/dist/mcp-server/pr-input.d.mts +12 -5
  49. package/dist/mcp-server/pr-input.mjs +21 -4
  50. package/dist/mcp-server/pr-live.d.mts +3 -1
  51. package/dist/mcp-server/pr-live.mjs +7 -2
  52. package/dist/mcp-server/pr-mode.d.mts +7 -0
  53. package/dist/mcp-server/pr-mode.mjs +34 -0
  54. package/dist/mcp-server/pr-no-source.d.mts +9 -0
  55. package/dist/mcp-server/pr-no-source.mjs +22 -0
  56. package/dist/mcp-server/pr-prepare.d.mts +25 -0
  57. package/dist/mcp-server/pr-prepare.mjs +103 -0
  58. package/dist/mcp-server/preview-registry.d.mts +25 -0
  59. package/dist/mcp-server/preview-registry.mjs +38 -0
  60. package/dist/mcp-server/retrospective-check.d.mts +3 -0
  61. package/dist/mcp-server/retrospective-check.mjs +38 -0
  62. package/dist/mcp-server/retrospective-input.mjs +2 -2
  63. package/dist/mcp-server/retrospective-preview.d.mts +17 -0
  64. package/dist/mcp-server/retrospective-preview.mjs +60 -0
  65. package/dist/mcp-server/snapshot-registry.d.mts +25 -0
  66. package/dist/mcp-server/snapshot-registry.mjs +32 -0
  67. package/dist/mcp-server/tools-pr.mjs +20 -4
  68. package/dist/mcp-server/tools-retrospective.mjs +25 -5
  69. package/dist/mcp-server/tools.mjs +6 -0
  70. package/dist/pull-request-diff/diff-size.d.mts +29 -0
  71. package/dist/pull-request-diff/diff-size.mjs +35 -0
  72. package/dist/pull-request-diff/index.d.mts +2 -0
  73. package/dist/pull-request-diff/index.mjs +1 -0
  74. package/dist/retrospective-distill/args.d.mts +13 -0
  75. package/dist/retrospective-distill/args.mjs +27 -0
  76. package/dist/retrospective-distill/checkpoint-entry.d.mts +2 -0
  77. package/dist/retrospective-distill/checkpoint-entry.mjs +11 -0
  78. package/dist/retrospective-distill/fixtures.test-helpers.d.mts +3 -0
  79. package/dist/retrospective-distill/fixtures.test-helpers.mjs +21 -0
  80. package/dist/retrospective-distill/flag-args.d.mts +5 -0
  81. package/dist/retrospective-distill/flag-args.mjs +31 -0
  82. package/dist/retrospective-distill/index.d.mts +10 -0
  83. package/dist/retrospective-distill/index.mjs +6 -0
  84. package/dist/retrospective-distill/partition-records.d.mts +6 -0
  85. package/dist/retrospective-distill/partition-records.mjs +49 -0
  86. package/dist/retrospective-distill/retrospective-entry.d.mts +3 -0
  87. package/dist/retrospective-distill/retrospective-entry.mjs +4 -0
  88. package/dist/retrospective-distill/run.d.mts +5 -0
  89. package/dist/retrospective-distill/run.mjs +28 -0
  90. package/dist/retrospective-distill/session-types.d.mts +17 -0
  91. package/dist/retrospective-distill/session-types.mjs +1 -0
  92. package/dist/retrospective-distill/shape.d.mts +29 -0
  93. package/dist/retrospective-distill/shape.mjs +111 -0
  94. package/docs/mcp-server.md +95 -23
  95. package/package.json +20 -5
  96. package/skills/agent-workflow/SKILL.md +20 -0
  97. package/skills/agent-workflow/references/four-refs.md +74 -0
  98. package/skills/blackboard/SKILL.md +21 -1
  99. package/skills/github-issue/SKILL.md +6 -0
  100. package/skills/manifest.json +11 -5
  101. package/skills/postgres-node-performance-tuning/SKILL.md +2 -0
  102. package/skills/postgres-node-performance-tuning/references/performance-patterns.md +76 -0
  103. package/skills/postgres-node-performance-tuning/references/reading-plans.md +111 -0
  104. package/skills/postgres-schema-design/references/relations.md +18 -0
  105. package/skills/retrospective/SKILL.md +3 -1
  106. package/skills/retrospective-distill/SKILL.md +72 -4
  107. package/skills/revisit-followups/SKILL.md +17 -0
  108. package/skills/stacked-prs/SKILL.md +47 -16
  109. package/skills/stacked-prs/references/drain.md +168 -0
  110. package/skills/stacked-prs/references/gh-stack.md +143 -0
@@ -82,17 +82,18 @@ Every tool takes an explicit `sessionId` (`^[A-Za-z0-9._:-]+$`, at most 256 char
82
82
  server never infers, and an optional `worktree`. Unknown arguments are rejected. Failures come back
83
83
  as MCP tool errors (`isError: true`) with an actionable message rather than crashing the server.
84
84
 
85
- | Tool | Arguments besides `sessionId` and `worktree` | Returns |
86
- | --------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ |
87
- | `journal_append` | `parentSessionId`, `agent`, `version`, `mode` (`interactive` or `autonomous`), `markdown`, `sourceEventId`, `workOutcome`, `repositories`, `feedbackCoverage`, optional `category` | `sessionId`, `timestamp`, delivery `status` (`delivered` with a verified receipt, or `pending`), `pendingCount`, `worktreePendingCount` |
88
- | `journal_entries` | none | `sessionId` and `entries`, every entry oldest first |
89
- | `outbox_status` | none | `sessionId`, `status`, `pendingCount`, `worktreePendingCount`, `rejectedCount`, `worktreeRejectedCount` |
90
- | `outbox_flush` | none | `sessionId`, `status`, `pendingCount`, `worktreePendingCount`, `rejectedCount`, `worktreeRejectedCount`, `deliveredCount`, `rejected` (when any) |
91
- | `session_ensure` | `parentSessionId`, `agent`, `version` | `sessionId`, `status` (`created` or `exists`), `archived` |
92
- | `retrospective_check` | optional `sourceEventId` | `sessionId`, `saved`, `entries` (`sourceEventId`, `timestamp`, `issues`, `prs` of each retrospective, oldest first) |
93
- | `retrospective_save` | `parentSessionId`, `agent`, `version`, `mode`, `workOutcome`, `feedbackCoverage`, exactly one of `markdown` or `compose`, optional `repositories`, `sourceEventId`, `category`, `dryRun` | as `journal_append` plus `policy.warnings`; with `dryRun`, `markdown` and `policy.warnings` |
94
- | `snapshot_export` | optional `agent`, `version`, `parentSessionId`, `data`, `dataArrayContains`, `inactiveForHours` | `sessionId`, `path`, `counts`, `checksum`, `manifest`, `cleanupToken` |
95
- | `session_archive` | none | `sessionId`, `archived: true` |
85
+ | Tool | Arguments besides `sessionId` and `worktree` | Returns |
86
+ | --------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ |
87
+ | `journal_append` | `parentSessionId`, `agent`, `version`, `mode` (`interactive` or `autonomous`), `markdown`, `sourceEventId`, `workOutcome`, `repositories`, `feedbackCoverage`, optional `category` | `sessionId`, `timestamp`, delivery `status` (`delivered` with a verified receipt, or `pending`), `pendingCount`, `worktreePendingCount` |
88
+ | `journal_entries` | none | `sessionId` and `entries`, every entry oldest first |
89
+ | `outbox_status` | none | `sessionId`, `status`, `pendingCount`, `worktreePendingCount`, `rejectedCount`, `worktreeRejectedCount` |
90
+ | `outbox_flush` | none | `sessionId`, `status`, `pendingCount`, `worktreePendingCount`, `rejectedCount`, `worktreeRejectedCount`, `deliveredCount`, `rejected` (when any) |
91
+ | `session_ensure` | `parentSessionId`, `agent`, `version` | `sessionId`, `status` (`created` or `exists`), `archived` |
92
+ | `retrospective_check` | optional `sourceEventId` | `sessionId`, `saved`, `entries` (`sourceEventId`, `timestamp`, `issues`, `prs` of each retrospective, oldest first) |
93
+ | `retrospective_save` | `parentSessionId`, `agent`, `version`, `mode`, `workOutcome`, `feedbackCoverage`, exactly one of `markdown` or `compose`, optional `repositories`, `sourceEventId`, `category`, `dryRun`, `knownSensitiveValues` (markdown only); or `previewToken` with identity, `mode` and `sourceEventId` | as `journal_append` plus `policy.warnings`; with `dryRun`, `markdown`, `previewToken` and `policy.warnings` |
94
+ | `snapshot_export` | optional `agent`, `version`, `parentSessionId`, `data`, `dataArrayContains`, `inactiveForHours` | `sessionId`, `path`, `counts`, `checksum`, `manifest`, `cleanupToken` |
95
+ | `snapshot_cleanup` | `cleanupToken` | `sessionId`, `cleaned: true` |
96
+ | `session_archive` | none | `sessionId`, `archived: true` |
96
97
 
97
98
  `journal_append` validates the whole feedback envelope with `validateFeedbackEnvelope` before
98
99
  anything is written, so a missing required field writes nothing. It returns the verified read-back
@@ -146,6 +147,35 @@ outbox does not create the directory. `snapshot_export` never accepts a destinat
146
147
  `agent-blackboard` client chooses a private file and returns its path. `session_archive` is the one
147
148
  destructive tool; archived metadata is immutable, while entries stay appendable.
148
149
 
150
+ ### Snapshot cleanup
151
+
152
+ `snapshot_export` writes a private file in the OS temp directory, and `snapshot_cleanup` is the only
153
+ way to delete it through the server. It was chosen over cleanup inside `session_archive` because
154
+ the exporting session and the archived sessions differ during distillation, and cleanup must run
155
+ whether or not archiving happened. Call it as a finally step after processing, on success and on
156
+ failure.
157
+
158
+ - **No caller-chosen path.** The tool takes only `cleanupToken` (`path`, `directory`, and `receipt`
159
+ are unsupported arguments). The server records the export's file path itself, plus a partition
160
+ directory and its signed receipt when the client reports both, and cleans exactly that record.
161
+ When the client's export carried its own cleanup capability, the server keeps it (never
162
+ returning it) and cleans through the client's `cleanupSnapshotPartitions`, so the client's
163
+ ownership marker goes with the file; otherwise it uses the in-tree `cleanupSnapshotPartitions`.
164
+ - **Server-minted, bound token.** `snapshot_export` replaces the client's `cleanupToken` with 32
165
+ random bytes the server generates, recorded with the caller's `sessionId` and resolved `worktree`.
166
+ The binding is checked before any filesystem call.
167
+ - **Single use.** A successful cleanup spends the token; a repeat fails as unknown and deletes
168
+ nothing. The record is taken synchronously, so concurrent calls cannot both clean.
169
+ - **Failure modes.** An unknown, forged (including a path used as a token), spent, other-session, or
170
+ other-worktree token fails with one message and deletes nothing; a mismatched caller does not spend
171
+ the owner's token. If the cleanup itself fails, the token stays valid and the error carries the
172
+ reason, including the retained tombstone path when a partition directory was half removed, so the
173
+ same call can retry. A file that is already gone counts as cleaned.
174
+ - **Server restart.** Bindings are in memory only. After a restart every earlier token is unknown, so
175
+ the file is left for the operating system to reap from its temp directory; run the distill pass
176
+ again to export a fresh snapshot. The registry also remembers at most 256 exports and forgets the
177
+ oldest first.
178
+
149
179
  The outbox lives at `<worktree>/.local/blackboard-outbox` and is used only in interactive mode.
150
180
  Autonomous mode forbids a filesystem outbox, so a failed delivery is an error.
151
181
 
@@ -168,7 +198,9 @@ instructions shows it to the agent.
168
198
  ## Retrospective tools and the policy contract
169
199
 
170
200
  `retrospective_check` reads the session through the same client as `journal_entries` and reports the
171
- entries whose envelope `type` is `retrospective`. A session that was never created (the blackboard
201
+ entries whose envelope `type` is `retrospective`. A schema-versioned entry counts only when its
202
+ envelope passes `validateFeedbackEnvelope`; a malformed one is not saved, while an unversioned legacy
203
+ entry is still accepted. A session that was never created (the blackboard
172
204
  answers 404) reports `saved: false`.
173
205
 
174
206
  `retrospective_save` builds a `retrospective` envelope and writes it exactly as `journal_append`
@@ -189,7 +221,20 @@ are killed after 120 seconds. `compose.facts` takes `pr`, `branch`, `noPr`, or `
189
221
  worktree's. Facts run `git` and `gh` in the worktree as the server's user, honoring its
190
222
  repository configuration (which can select helpers that run code), so the worktree, including
191
223
  `.git/config`, must be as trusted as the policy module. `dryRun`
192
- runs every check and returns the document without writing.
224
+ runs every check and returns the document without writing, plus a `previewToken`.
225
+
226
+ `knownSensitiveValues` (each at least 4 characters) is also accepted beside `markdown`, and
227
+ redacts the markdown, `category` and `feedbackCoverage` just as `compose.knownSensitiveValues`
228
+ does; with `compose`, pass it inside `compose`. The `previewToken` is 32 random bytes (base64url),
229
+ opaque, single use, and held only in server memory: a restart drops every token, and the registry
230
+ keeps the 32 most recent. It is bound to the `sessionId`, `parentSessionId`, `mode`, the resolved worktree, the `sourceEventId`
231
+ and the exact redacted content. Save it by calling `retrospective_save` again with the identity,
232
+ the same `parentSessionId`, `mode`, `sourceEventId` and `previewToken`, and none of `markdown`, `compose`,
233
+ `workOutcome`, `feedbackCoverage`, `repositories`, `category`, `knownSensitiveValues` or `dryRun`.
234
+ The server stores exactly the previewed content (so a sensitive value never has to be sent
235
+ again), mints a new timestamp, and does not run the policy again. An unknown, spent, restarted or
236
+ mismatched token is one error and the dry run must be repeated; a mismatch does not spend the
237
+ token, and neither does a failed write.
193
238
 
194
239
  The document must open with the front matter `composeRetrospective` writes: lines of
195
240
  `key: <JSON value>` between two `---` lines. Recognized keys are `date`, `issues`, `prs` (required),
@@ -229,7 +274,7 @@ whole process group is killed, so nothing it started outlives the call. The runn
229
274
  | ----------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------- | ------- |
230
275
  | `validateRetrospective` | `sessionId`, `parentSessionId`, `mode`, `workOutcome`, `feedbackCoverage`, `repositories`, `markdown`, `sourceEventId`, `metadata` (`date`, `issues`, `prs`, `description`) | `{ errors: string[], warnings?: string[] }` | 30 s |
231
276
  | `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 |
277
+ | `validatePullRequest` | `title`, `body`, `base`, `head`, the trusted `mode`, `noSourceIssue` when given, and `pr` when set | `{ errors: string[], warnings?: string[] }` | 120 s |
233
278
 
234
279
  Any `errors` reject the save and write nothing. A non-zero exit, a timeout (reported as
235
280
  `policy <export> timed out after <N>s`), output over 1 MB, malformed JSON, or a result of the wrong
@@ -241,9 +286,33 @@ shape is an error, never a pass.
241
286
  retrospective tools. All three take `sessionId` (required) and `worktree` (optional), need a server
242
287
  launched inside a repository worktree (they run the checkout policy), and share this structured
243
288
  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`.
289
+ (`[{ number, relation: "closes" | "refs", reason }]`, may be empty), optional `noSourceIssue`
290
+ (`{ reason }`, see below), `details` (string array), `base` (default `main`), `agent`, optional
291
+ `model`, optional `mode` (`interactive` | `autonomous`) and `workspaceSetup`. `pr_update` also
292
+ requires `pr`; `pr_validate` accepts an optional `pr`.
293
+
294
+ ### Trusted mode
295
+
296
+ The mode the policy sees is derived by the server from its own launch environment, never from the
297
+ caller. It fails closed: when any automation marker is set to something other than an empty
298
+ string, `0` or `false` (`CI`, `GITHUB_ACTIONS`, `GITLAB_CI`, `BUILDKITE`, `CIRCLECI`,
299
+ `JENKINS_URL`, `TF_BUILD`, `CODEBUILD_BUILD_ID`, `VOUCHINGTON_AUTOMATION`), the mode is
300
+ `autonomous` whatever the caller passes. `CLAUDE_CODE_SESSION_ATTENDED` set to exactly `0` (Claude
301
+ Code's marker for an unattended `claude -p` run, inherited by MCP servers) also means `autonomous`;
302
+ a missing value or `1` never does. This list is a superset of the downstream check it replaces,
303
+ which was exactly `GITHUB_ACTIONS === 'true' || CI === 'true'`. Otherwise the mode is `interactive`. The optional `mode`
304
+ argument can only narrow `interactive` to `autonomous` (it never widens), so an autonomous run
305
+ cannot unlock an interactive-only policy exception. The policy child receives no env markers, so it
306
+ relies on this value.
307
+
308
+ ### Pull requests with no source issue
309
+
310
+ Pass `noSourceIssue: { reason }` (a single line) with an empty `relatedIssues`; a non-empty
311
+ `relatedIssues` beside it is rejected. The Related issues section then holds the generic statement
312
+ `- No source issue — <reason>` instead of `- None`. A policy may replace the statement by
313
+ exporting `renderNoSourceIssue({ reason })`, returning a non-empty string (30 s). The field also
314
+ reaches `validatePullRequest` (with the trusted `mode`, so the policy can apply an interactive-only
315
+ rule) and `pullRequestDefaults`.
247
316
 
248
317
  ### Rendering
249
318
 
@@ -258,7 +327,8 @@ input: `title` (single line, at most 256 characters), `summary`, `impact`, `rela
258
327
 
259
328
  ## Related issues
260
329
 
261
- - Closes #N — <reason> (or "Refs #N"; "- None" when empty)
330
+ - Closes #N — <reason> (or "Refs #N"; "- None" when empty;
331
+ "- No source issue — <reason>" for noSourceIssue)
262
332
 
263
333
  ## Details
264
334
 
@@ -270,18 +340,20 @@ Device: <os user>@<lowercased hostname>
270
340
  Worktree: <path after "/worktrees/", "main" for the main worktree, else the directory name>
271
341
  ```
272
342
 
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,
343
+ The head branch is the worktree's current branch (for `pr_validate` with `pr`, the live pull
344
+ request's head, which may equal `base` for a fork); otherwise a detached HEAD, or a head equal to `base`, is rejected. The body must stay within GitHub's 65536 characters. `pr_create` sees no stack membership,
275
345
  so a consumer policy may reject any `base` other than the default branch; create stacked layers
276
346
  with the repository's stack tooling instead.
277
347
 
278
348
  ### Tools
279
349
 
280
350
  - `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`.
351
+ base, head, mode, noSourceIssue?, pr? })` export and returns `{ body, errors, warnings }`. With
352
+ `pr`, the live body is read (never written) so its Shepherd Journal is preserved as in
353
+ `pr_update`. The liveness checks of `pr_update` do not apply: any state (merged or closed
354
+ included) and any head are accepted, and the policy sees the pull request's own head and base.
283
355
  - `pr_create` (additive): renders and validates (errors block), then calls the policy
284
- `pullRequestDefaults({ title, base, relatedIssues })` export for optional `labels`, `milestone`
356
+ `pullRequestDefaults({ title, base, relatedIssues, noSourceIssue? })` export for optional `labels`, `milestone`
285
357
  and `project`. It checks the head is pushed and current, writes the body to a mode 0600 temp file
286
358
  (removed afterwards) and runs `gh pr create --draft` with the labels, `--milestone` and
287
359
  `--project`. Returns `{ number, url, warnings }`.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vouchington-tooling",
3
- "version": "0.43.0",
3
+ "version": "0.45.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",
@@ -376,13 +391,13 @@
376
391
  "yaml": "2.9.1"
377
392
  },
378
393
  "devDependencies": {
379
- "@ast-grep/cli": "0.45.3",
380
- "@modelcontextprotocol/sdk": "1.32.0",
394
+ "@ast-grep/cli": "0.50.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",
384
399
  "no-mistakes": "0.83.0",
385
- "smol-toml": "1.9.0",
400
+ "smol-toml": "1.9.1",
386
401
  "typescript5": "npm:typescript@5.9.3"
387
402
  },
388
403
  "peerDependencies": {
@@ -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