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