vouchington-tooling 0.26.0 → 0.28.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 (39) hide show
  1. package/README.md +5 -1
  2. package/dist/agent-blackboard/feedback-canonical.d.mts +7 -0
  3. package/dist/agent-blackboard/feedback-canonical.mjs +8 -0
  4. package/dist/agent-blackboard/feedback-delivery.mjs +8 -6
  5. package/dist/agent-blackboard/feedback-online.mjs +11 -4
  6. package/dist/agent-blackboard/feedback-outbox.d.mts +8 -1
  7. package/dist/agent-blackboard/feedback-outbox.mjs +17 -4
  8. package/dist/agent-blackboard/feedback-types.d.mts +4 -0
  9. package/dist/agent-blackboard/index.d.mts +1 -1
  10. package/dist/agent-blackboard/index.mjs +1 -1
  11. package/dist/cli/index.mjs +4 -0
  12. package/dist/cli/parse.d.mts +1 -1
  13. package/dist/cli/parse.mjs +1 -0
  14. package/dist/cli/usage.d.mts +1 -1
  15. package/dist/cli/usage.mjs +2 -0
  16. package/dist/mcp-server/dispatch.mjs +5 -4
  17. package/dist/mcp-server/fake-blackboard.test-helpers.d.mts +3 -0
  18. package/dist/mcp-server/fake-blackboard.test-helpers.mjs +7 -1
  19. package/dist/mcp-server/handlers-journal.d.mts +0 -2
  20. package/dist/mcp-server/handlers-journal.mjs +22 -30
  21. package/dist/mcp-server/handlers-outbox.d.mts +11 -0
  22. package/dist/mcp-server/handlers-outbox.mjs +45 -0
  23. package/dist/mcp-server/harness.test-helpers.d.mts +6 -1
  24. package/dist/mcp-server/harness.test-helpers.mjs +14 -2
  25. package/dist/mcp-server/server.mjs +5 -1
  26. package/dist/mcp-server/tools.mjs +12 -10
  27. package/dist/pnpm-install/pnpm-install-fake-pnpm.test-helpers.mjs +2 -2
  28. package/docs/mcp-server.md +58 -14
  29. package/package.json +3 -3
  30. package/scripts/gha/gha-collaborator-trust.sh +75 -0
  31. package/skills/blackboard/SKILL.md +27 -17
  32. package/skills/manifest.json +27 -21
  33. package/skills/postgres-node-performance-tuning/references/performance-patterns.md +22 -0
  34. package/skills/postgres-partitioning-uuid-v7/SKILL.md +3 -0
  35. package/skills/postgres-schema-design/SKILL.md +21 -0
  36. package/skills/postgres-schema-design/references/naming.md +34 -0
  37. package/skills/postgres-schema-design/references/relations.md +46 -0
  38. package/skills/postgres-schema-design/references/table-shapes.md +56 -0
  39. package/skills/retrospective-distill/SKILL.md +13 -8
@@ -59,7 +59,13 @@ const IDENTITY_REQUIRED = ['parentSessionId', 'agent', 'version'];
59
59
  export const TOOLS = [
60
60
  tool('journal_append', 'Appends one journal entry to the caller session and returns the verified read-back receipt. ' +
61
61
  'Nothing is written unless every required field validates. Interactive mode retains the ' +
62
- 'envelope in the worktree outbox when the blackboard is unreachable.', ADDITIVE, {
62
+ 'envelope in the worktree outbox when the blackboard is unreachable. The server owns the ' +
63
+ 'entry timestamp and returns it. An event is identified by sessionId, sourceEventId, and ' +
64
+ 'content, never by timestamp: to retry, repeat the identical call, and a retry of an ' +
65
+ 'event already stored or retained reports the stored timestamp instead of a new one. The ' +
66
+ 'same sourceEventId with different content is an event-conflict. pendingCount counts ' +
67
+ 'unsent records of the caller sessionId; worktreePendingCount counts those of every ' +
68
+ 'session in the worktree outbox (both 0 in autonomous mode, which never uses an outbox).', ADDITIVE, {
63
69
  ...IDENTITY,
64
70
  mode: { enum: ['interactive', 'autonomous'], description: 'Delivery mode.' },
65
71
  markdown: { type: 'string', description: 'Journal note as markdown text (max 12000 bytes).' },
@@ -82,11 +88,6 @@ export const TOOLS = [
82
88
  additionalProperties: false,
83
89
  description: 'Explicit coverage; sources default to [] and droppedCount to 0.',
84
90
  },
85
- timestamp: {
86
- type: 'string',
87
- description: 'Optional ISO 8601 time; defaults to now and is returned. Reuse the same sourceEventId ' +
88
- 'and timestamp when retrying, because the whole envelope must match.',
89
- },
90
91
  category: { type: 'string', description: 'Optional context; never changes entry type.' },
91
92
  }, [
92
93
  ...IDENTITY_REQUIRED,
@@ -100,10 +101,11 @@ export const TOOLS = [
100
101
  tool('journal_entries', 'Returns every entry of one session, oldest first, as JSON: { sessionId, entries }. Each ' +
101
102
  'entry is exactly as the blackboard returns it ({ sessionId, createdAt, data }), including ' +
102
103
  'non-journal types such as retrospective and legacy entries without a feedback envelope.', READ_ONLY),
103
- tool('outbox_status', 'Reports how many unsent journal records the worktree outbox retains. sessionId identifies ' +
104
- 'the caller; the count covers the whole outbox.', READ_ONLY),
105
- tool('outbox_flush', 'Delivers retained outbox records and returns the remaining count. sessionId identifies the ' +
106
- 'caller; the flush covers every record in the worktree outbox.', IDEMPOTENT_ADDITIVE),
104
+ tool('outbox_status', 'Reports unsent journal records in the worktree outbox. pendingCount and status (empty or ' +
105
+ 'pending) cover the caller sessionId only; worktreePendingCount covers every session.', READ_ONLY),
106
+ tool('outbox_flush', 'Delivers every retained record in the worktree outbox, whichever session wrote it, and ' +
107
+ 'returns deliveredCount for the whole flush. pendingCount and status cover the remaining ' +
108
+ 'records of the caller sessionId; worktreePendingCount covers every session.', IDEMPOTENT_ADDITIVE),
107
109
  tool('session_ensure', 'Idempotently creates a session, or verifies that an existing one has the same identity. ' +
108
110
  'Returns the session id and whether it was created.', IDEMPOTENT_ADDITIVE, IDENTITY, IDENTITY_REQUIRED),
109
111
  tool('snapshot_export', 'Exports non-archived sessions into a private local JSONL file chosen by the blackboard ' +
@@ -7,7 +7,7 @@ if [ "\${1:-}" = m ]; then
7
7
  exit 0
8
8
  fi
9
9
  if [ "\${1:-}" = --version ]; then
10
- printf '%s\\n' "\${PNPM_VERSION:-11.0.0}"
10
+ printf '%s\\n' "\${PNPM_VERSION:-1.0.0}"
11
11
  exit 0
12
12
  fi
13
13
  printf '%s\\n' "$*" >> "$PNPM_LOG"
@@ -23,7 +23,7 @@ write_modules() {
23
23
  print_release_age_violation() {
24
24
  printf '%s\\n' '✗ Lockfile failed supply-chain policy check (1 entries in 0.1s)'
25
25
  printf '%s\\n' '[ERR_PNPM_MINIMUM_RELEASE_AGE_VIOLATION] 1 lockfile entries failed verification:'
26
- printf '%s\\n' ' undici@8.10.0 was published at 2026-08-03T15:06:33.000Z, within the minimumReleaseAge cutoff (2026-08-02T04:48:10.357Z)'
26
+ printf '%s\\n' ' fixture-package@1.0.0 was published at 2026-08-03T15:06:33.000Z, within the minimumReleaseAge cutoff (2026-08-02T04:48:10.357Z)'
27
27
  }
28
28
  if [ "\${PNPM_FAIL_CALL:-0}" = "$calls" ]; then
29
29
  if [ -n "\${PNPM_FAIL_RELEASE_AGE:-}" ]; then print_release_age_violation; fi
@@ -42,22 +42,36 @@ Every tool takes an explicit `sessionId` (`^[A-Za-z0-9._:-]+$`, at most 256 char
42
42
  server never infers, and an optional `worktree`. Unknown arguments are rejected. Failures come back
43
43
  as MCP tool errors (`isError: true`) with an actionable message rather than crashing the server.
44
44
 
45
- | Tool | Arguments besides `sessionId` and `worktree` | Returns |
46
- | ----------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------- |
47
- | `journal_append` | `parentSessionId`, `agent`, `version`, `mode` (`interactive` or `autonomous`), `markdown`, `sourceEventId`, `workOutcome`, `repositories`, `feedbackCoverage`, optional `timestamp`, `category` | `sessionId`, `timestamp`, delivery `status` (`delivered` with a verified receipt, or `pending`) |
48
- | `journal_entries` | none | `sessionId` and `entries`, every entry oldest first |
49
- | `outbox_status` | none | `sessionId`, `status`, `pendingCount` |
50
- | `outbox_flush` | none | `sessionId`, `status`, `pendingCount`, `deliveredCount` |
51
- | `session_ensure` | `parentSessionId`, `agent`, `version` | `sessionId`, `status` (`created` or `exists`), `archived` |
52
- | `snapshot_export` | optional `agent`, `version`, `parentSessionId`, `data`, `dataArrayContains`, `inactiveForHours` | `sessionId`, `path`, `counts`, `checksum`, `manifest`, `cleanupToken` |
53
- | `session_archive` | none | `sessionId`, `archived: true` |
45
+ | Tool | Arguments besides `sessionId` and `worktree` | Returns |
46
+ | ----------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------- |
47
+ | `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` |
48
+ | `journal_entries` | none | `sessionId` and `entries`, every entry oldest first |
49
+ | `outbox_status` | none | `sessionId`, `status`, `pendingCount`, `worktreePendingCount` |
50
+ | `outbox_flush` | none | `sessionId`, `status`, `pendingCount`, `worktreePendingCount`, `deliveredCount` |
51
+ | `session_ensure` | `parentSessionId`, `agent`, `version` | `sessionId`, `status` (`created` or `exists`), `archived` |
52
+ | `snapshot_export` | optional `agent`, `version`, `parentSessionId`, `data`, `dataArrayContains`, `inactiveForHours` | `sessionId`, `path`, `counts`, `checksum`, `manifest`, `cleanupToken` |
53
+ | `session_archive` | none | `sessionId`, `archived: true` |
54
54
 
55
55
  `journal_append` validates the whole feedback envelope with `validateFeedbackEnvelope` before
56
56
  anything is written, so a missing required field writes nothing. It returns the verified read-back
57
- result of `writeFeedback`. Delivery is at least once. The envelope includes its timestamp, so when
58
- retrying the same `sourceEventId`, resend the same content and the `timestamp` the first call
59
- returned; the server fixes a default timestamp and returns it for that reason. Markdown is limited
60
- to 12000 bytes.
57
+ result of `writeFeedback`. Delivery is at least once. Markdown is limited to 12000 bytes.
58
+
59
+ The server owns the entry `timestamp`: the tool takes no `timestamp` argument and rejects one, so an
60
+ agent cannot fabricate it, and the result returns the timestamp it reports. An event is identified by
61
+ `sessionId`, `sourceEventId`, and content; the timestamp is not part of that identity. The server
62
+ stamps each attempt with the current time, and the delivery step compares the new attempt with what
63
+ the blackboard or the worktree outbox already holds while ignoring only `timestamp`. To retry, repeat
64
+ the identical call. A retry of an event that is already stored, or already retained in the outbox,
65
+ is the same event: it writes nothing, drops the duplicate retained record, and returns the stored
66
+ record's `timestamp` and `receipt`, so repeated retries report the same timestamp, including after a
67
+ server restart and when the provider was unreachable for the retry. An outbox record that was
68
+ delivered before the retry is recognized the same way when it is flushed, so it drains instead of
69
+ blocking. Any difference in the other envelope fields under the same `sessionId` and
70
+ `sourceEventId` is an `event-conflict` (or a conflict with the retained unsent record) and writes
71
+ nothing. If an autonomous write of the same event lands between the check and the append, both
72
+ entries exist, but the report names the earliest stored one and later retries report it again.
73
+ Admission freshness (`verifyFreshFeedback`) keeps its stricter rule: any earlier record of the event,
74
+ whatever its timestamp, is a conflict.
61
75
 
62
76
  `journal_entries` returns `{ sessionId, entries }`. Each entry is exactly what the `agent-blackboard`
63
77
  client returns (`sessionId`, `createdAt`, `data`), so `data` keeps its envelope fields such as
@@ -67,7 +81,13 @@ order, so they are sorted stably by `createdAt`, oldest first. An empty session
67
81
  `entries: []`. `vouchington agent-blackboard journal entries` still prints journal markdown only.
68
82
 
69
83
  `sessionId` on `outbox_status`, `outbox_flush`, and `snapshot_export` only identifies the caller.
70
- The outbox is per worktree and the status and flush cover every record in it. Flushing an empty
84
+ The outbox is per worktree, so the results name two counts apart. `pendingCount` is the number of
85
+ unsent records of the caller's `sessionId`, and `status` (`empty` or `pending`) follows it.
86
+ `worktreePendingCount` is the number of unsent records of every session in the worktree outbox. Both
87
+ are 0 in autonomous mode, which has no outbox. `outbox_flush` delivers every retained record in the
88
+ worktree, whichever session wrote it, so its `deliveredCount` covers the whole flush while
89
+ `pendingCount` and `worktreePendingCount` report what remains. A record that cannot be delivered, for
90
+ example after an `event-conflict`, stays in the count of the session that wrote it. Flushing an empty
71
91
  outbox does not create the directory. `snapshot_export` never accepts a destination: the
72
92
  `agent-blackboard` client chooses a private file and returns its path. `session_archive` is the one
73
93
  destructive tool; archived metadata is immutable, while entries stay appendable.
@@ -75,6 +95,22 @@ destructive tool; archived metadata is immutable, while entries stay appendable.
75
95
  The outbox lives at `<worktree>/.local/blackboard-outbox` and is used only in interactive mode.
76
96
  Autonomous mode forbids a filesystem outbox, so a failed delivery is an error.
77
97
 
98
+ ### Tool names by harness
99
+
100
+ Each harness names the same tools differently, and some load them on demand. The names in this page
101
+ are the bare tool names; find yours by searching for `journal_append` before concluding the server
102
+ is unavailable.
103
+
104
+ | Harness | `journal_append` is called |
105
+ | ----------- | ------------------------------------------------------------------------------------------------------------------------ |
106
+ | Claude Code | `mcp__vouchington-tooling__journal_append` |
107
+ | Codex | `mcp__vouchington_tooling__journal_append` (the server name's hyphen becomes an underscore) |
108
+ | Cursor | deferred: look it up with `GetDynamicTools` and call it with `CallDynamicTool` under the `vouchington-tooling` namespace |
109
+ | Grok | `vouchington-tooling__journal_append`, found with `search_tool` and called with `use_tool` |
110
+
111
+ The server sends the same list in its MCP `instructions`, so a harness that surfaces server
112
+ instructions shows it to the agent.
113
+
78
114
  ## Worktree validation
79
115
 
80
116
  `worktree` must be an absolute path that, after resolving symlinks, exactly matches an entry of
@@ -119,6 +155,11 @@ Approving every tool of the server at once:
119
155
  - Cursor permission rule: `Mcp(vouchington-tooling:*)`
120
156
  - Codex: `default_tools_approval_mode = "approve"` above
121
157
 
158
+ Cursor also asks each user to approve loading a project server, separately from the permission rule
159
+ above, and the rule does not replace it. Approve it once, interactively, with
160
+ `cursor-agent mcp enable vouchington-tooling`. Headless `cursor-agent -p` does not keep that
161
+ approval: pass `--approve-mcps` on every run.
162
+
122
163
  ## What is verified
123
164
 
124
165
  Covered by this package's tests: the tool list and schemas, every tool's success and rejection
@@ -126,6 +167,9 @@ paths against a faked blackboard client, the worktree gate against real temporar
126
167
  an in-process client and server round trip through the SDK's in-memory transport, and a spawned
127
168
  `vouchington mcp` process driven over real stdio.
128
169
 
170
+ The per-harness tool names and the Cursor approval steps come from headless smoke runs of the
171
+ server in each harness, not from this package's tests.
172
+
129
173
  Not verified, and tracked as the unchecked Phase 0 checklist in
130
174
  [#324](https://github.com/vouchington/vouchington-tooling/issues/324): each harness's approval
131
175
  syntax and whether the rules above take effect, the working directory after entering a worktree,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vouchington-tooling",
3
- "version": "0.26.0",
3
+ "version": "0.28.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": {
@@ -346,8 +346,8 @@
346
346
  "dependencies": {
347
347
  "@types/mdast": "4.0.4",
348
348
  "@types/unist": "3.0.3",
349
- "csv-parse": "7.0.2",
350
- "csv-stringify": "6.8.3",
349
+ "csv-parse": "7.0.3",
350
+ "csv-stringify": "6.9.0",
351
351
  "dockerfile-ast": "0.7.1",
352
352
  "picomatch": "4.0.7",
353
353
  "remark": "15.0.1",
@@ -0,0 +1,75 @@
1
+ #!/usr/bin/env bash
2
+ # Classify GitHub authors by their live permission on one repository.
3
+ #
4
+ # usage: gha-collaborator-trust.sh <owner/repo> [--allow-bot <login>]... < authors.json
5
+ # stdin: JSON array of {"login": string, "type": string} (GitHub user objects work as-is).
6
+ # stdout: {"trusted": [login...], "untrusted": [login...]}, each sorted and deduplicated.
7
+ #
8
+ # A user is trusted when the legacy `permission` field is admin or write (maintain maps to
9
+ # write). A GitHub App bot (type "Bot") is trusted only when its login is passed with
10
+ # --allow-bot, because an app with only issue or pull-request write access can still comment.
11
+ # A definitive 404 means no permission. Any other lookup failure exits non-zero so callers
12
+ # fail closed.
13
+ set -euo pipefail
14
+
15
+ usage() {
16
+ echo "usage: gha-collaborator-trust.sh <owner/repo> [--allow-bot <login>]... < authors.json" >&2
17
+ exit 2
18
+ }
19
+
20
+ [ "$#" -ge 1 ] || usage
21
+ repository="$1"
22
+ shift
23
+ [[ "$repository" =~ ^[A-Za-z0-9_-][A-Za-z0-9_.-]*/[A-Za-z0-9_-][A-Za-z0-9_.-]*$ ]] || usage
24
+
25
+ allowed_bots=$'\n'
26
+ while [ "$#" -gt 0 ]; do
27
+ if [ "$1" != "--allow-bot" ] || [ "$#" -lt 2 ] || ! [[ "$2" =~ ^[A-Za-z0-9-]+\[bot\]$ ]]; then
28
+ usage
29
+ fi
30
+ allowed_bots+="$2"$'\n'
31
+ shift 2
32
+ done
33
+
34
+ input="$(cat)"
35
+ if ! jq -e '
36
+ type == "array" and all(.[];
37
+ type == "object"
38
+ and (.login | type) == "string" and (.login | length) > 0
39
+ and ((.type | type) == "string" or .type == null))' >/dev/null 2>&1 <<<"$input"; then
40
+ echo "::error::gha-collaborator-trust: stdin must be a JSON array of {login, type}" >&2
41
+ exit 2
42
+ fi
43
+ rows="$(jq -r 'unique_by(.login) | .[] | [.login, (.type // "")] | @tsv' <<<"$input")"
44
+
45
+ results="$(mktemp)"
46
+ errors="$(mktemp)"
47
+ trap 'rm -f "$results" "$errors"' EXIT
48
+
49
+ while IFS=$'\t' read -r login type; do
50
+ [ -n "$login" ] || continue
51
+ verdict=untrusted
52
+ if [ "$type" = "Bot" ]; then
53
+ if [[ "$allowed_bots" == *$'\n'"$login"$'\n'* ]]; then
54
+ verdict=trusted
55
+ fi
56
+ elif [[ "$login" =~ ^[A-Za-z0-9-]+$ ]]; then
57
+ if permission="$(gh api "repos/$repository/collaborators/$login/permission" --jq '.permission' 2>"$errors")"; then
58
+ case "$permission" in
59
+ admin | write) verdict=trusted ;;
60
+ esac
61
+ elif ! grep -q 'HTTP 404' "$errors"; then
62
+ cat "$errors" >&2
63
+ echo "::error::gha-collaborator-trust: permission lookup failed for $login" >&2
64
+ exit 1
65
+ fi
66
+ fi
67
+ printf '%s\t%s\n' "$verdict" "$login" >>"$results"
68
+ done <<<"$rows"
69
+
70
+ jq -Rn '
71
+ [inputs | split("\t")] as $rows
72
+ | {
73
+ trusted: [$rows[] | select(.[0] == "trusted") | .[1]] | sort,
74
+ untrusted: [$rows[] | select(.[0] == "untrusted") | .[1]] | sort
75
+ }' <"$results"
@@ -15,21 +15,29 @@ subagent-identity rules.
15
15
 
16
16
  Journal operations are tools of the `vouchington-tooling` MCP server (`vouchington mcp`):
17
17
  `journal_append`, `journal_entries`, `outbox_status`, `outbox_flush`, `session_ensure`,
18
- `snapshot_export`, and `session_archive`. A harness may prefix the tool names with the server name.
18
+ `snapshot_export`, and `session_archive`. Harnesses name them differently, so match the bare tool
19
+ name: `mcp__vouchington-tooling__<tool>`, `mcp__vouchington_tooling__<tool>` (the hyphen becomes an
20
+ underscore), or `vouchington-tooling__<tool>` reached through a `search_tool` and `use_tool` pair,
21
+ or the `vouchington-tooling` namespace reached through `GetDynamicTools` and `CallDynamicTool`.
19
22
  Every call takes an exact `sessionId`, never inferred, whose meaning depends on the tool:
20
23
 
21
24
  - `journal_append` and `session_ensure`: the session written or ensured, which is the caller's own
22
25
  session unless a runner ensures a child identity.
23
26
  - `journal_entries` and `session_archive`: the session read or archived, which during distillation
24
27
  belongs to another agent.
25
- - `snapshot_export`, `outbox_status`, and `outbox_flush`: only the caller. The outbox tools cover
26
- every record in the worktree outbox, not only that session's.
28
+ - `snapshot_export`, `outbox_status`, and `outbox_flush`: only the caller. The outbox is per
29
+ worktree, so `outbox_status` and `outbox_flush` report two counts: `pendingCount` is the unsent
30
+ records of that `sessionId`, and `worktreePendingCount` is the unsent records of every session in
31
+ the worktree outbox. `outbox_flush` delivers every retained record in the worktree.
27
32
 
28
33
  Every call may take `worktree`, the absolute path of a worktree of the current repository (the
29
34
  launch worktree by default). Writing or retrying an entry needs no provider skill, temporary file,
30
35
  `--file` flag, or replay command.
31
- If these tools are not available because the server is not registered or not connected, stop and
32
- report that the journal server is unavailable. Do not fall back to a CLI command.
36
+ Search for `journal_append` (by bare name, so every prefix matches) before concluding the server is
37
+ unavailable; a deferred harness lists the tool only on request. If the search finds no such tool
38
+ because the server is not registered, not connected, or not approved, stop and report that the
39
+ journal server is unavailable, and name any per-user approval the harness still needs to load
40
+ it (the consumer wrapper documents it). Do not fall back to a CLI command.
33
41
 
34
42
  1. Capture consequential observations before filing an issue: failed or recovered checks, denied or
35
43
  approved permissions, repeated fixes, scope changes, first-party tool behavior, and architectural
@@ -40,11 +48,10 @@ report that the journal server is unavailable. Do not fall back to a CLI command
40
48
  2. Record each observation with one `journal_append` call and pass the note as its `markdown`
41
49
  argument (at most 12000 bytes). Required arguments are `sessionId`, `parentSessionId` (null for a
42
50
  root session), `agent`, `version`, `mode`, `markdown`, a stable `sourceEventId`, `workOutcome`,
43
- canonical exact `repositories`, and explicit `feedbackCoverage`. Also pass an ISO 8601
44
- `timestamp` fixed before the first attempt: the server otherwise generates one, and a failed or
45
- timed-out call returns no result to learn it from. Nothing is written unless the
46
- whole envelope validates. The tool writes journal entries; `category` is optional context and
47
- never changes storage type or distillation eligibility. Generate routine facts and metadata
51
+ canonical exact `repositories`, and explicit `feedbackCoverage`. Never pass a `timestamp`: the
52
+ server owns it, rejects the argument, and returns the timestamp it used. Nothing is written
53
+ unless the whole envelope validates. The tool writes journal entries; `category` is optional
54
+ context and never changes storage type or distillation eligibility. Generate routine facts and metadata
48
55
  through the approved composer and pass its output as `markdown` instead of hand-authoring
49
56
  factual markers. `journal_append` stores only journal entries: save a composed retrospective
50
57
  through the retrospective skill's writer, never through `journal_append`.
@@ -55,10 +62,11 @@ report that the journal server is unavailable. Do not fall back to a CLI command
55
62
  the exact identity only when a runner must confirm the session before work starts.
56
63
  4. Select the repository's trusted `mode` (`interactive` or `autonomous`) explicitly. In interactive
57
64
  mode an outage may retain the validated, sanitized record in the bounded durable outbox, and
58
- `journal_append` reports it as pending. Report the pending count from `outbox_status`, deliver
59
- with `outbox_flush` once the provider is reachable, and continue primary work only after
60
- persistence succeeds. A full, unsafe, or unwritable outbox blocks capture; never evict or
61
- silently discard an unsent record.
65
+ `journal_append` reports it as pending. Report `pendingCount` (this session's unsent records),
66
+ and `worktreePendingCount` when other sessions' records are also unsent, from `journal_append`
67
+ or `outbox_status`. Deliver with `outbox_flush` once the provider is reachable, and continue
68
+ primary work only after persistence succeeds. A full, unsafe, or unwritable outbox blocks
69
+ capture; never evict or silently discard an unsent record.
62
70
  5. Autonomous runners require online `session_ensure`, a fresh admission `journal_append`, and
63
71
  readback before launching an attempt. Give each attempt's admission entry a `sourceEventId`
64
72
  unique to that attempt: reusing an earlier one returns the earlier receipt, which does not
@@ -66,9 +74,11 @@ report that the journal server is unavailable. Do not fall back to a CLI command
66
74
  Terminal reporting preserves work outcome separately from feedback coverage and delivery. A
67
75
  filesystem outbox never grants admission or acknowledged completion.
68
76
  6. Use at-least-once delivery with consumer deduplication by exact session/source identity. To
69
- retry a pending or failed append, call `journal_append` again with the same `sourceEventId`,
70
- content, and `timestamp` from the first attempt: the whole envelope must match, and conflicting
71
- source reuse is rejected. A transport timeout can leave a late
77
+ retry a pending or failed append, repeat the identical `journal_append` call with the same
78
+ `sourceEventId` and content. The server's timestamp is not part of an event's identity, so a
79
+ retry of an event already stored or retained reports the stored timestamp, even after a restart
80
+ or an outage. The same `sourceEventId` with different content is rejected as a conflict, so use
81
+ a new `sourceEventId` for a changed note. A transport timeout can leave a late
72
82
  durable write; pending or blocked is never an acknowledged receipt.
73
83
  7. Read the session's entries oldest-first with `journal_entries` for retrospectives. It returns
74
84
  every entry with its envelope, including its type and repository tags, so a retrospective can
@@ -4,7 +4,7 @@
4
4
  {
5
5
  "name": "agent-workflow",
6
6
  "plugin": "vouchington-workflow",
7
- "pluginVersion": "0.10.0",
7
+ "pluginVersion": "0.11.0",
8
8
  "path": "agent-workflow/SKILL.md",
9
9
  "prerequisites": ["github-issue", "pr-description", "review-ci-logs"]
10
10
  },
@@ -18,13 +18,13 @@
18
18
  {
19
19
  "name": "blackboard",
20
20
  "plugin": "vouchington-workflow",
21
- "pluginVersion": "0.10.0",
21
+ "pluginVersion": "0.11.0",
22
22
  "path": "blackboard/SKILL.md"
23
23
  },
24
24
  {
25
25
  "name": "dependabot",
26
26
  "plugin": "vouchington-workflow",
27
- "pluginVersion": "0.10.0",
27
+ "pluginVersion": "0.11.0",
28
28
  "path": "dependabot/SKILL.md",
29
29
  "prerequisites": ["github-actions-checklist"]
30
30
  },
@@ -38,26 +38,26 @@
38
38
  {
39
39
  "name": "git-commit-checklist",
40
40
  "plugin": "vouchington-workflow",
41
- "pluginVersion": "0.10.0",
41
+ "pluginVersion": "0.11.0",
42
42
  "path": "git-commit-checklist/SKILL.md"
43
43
  },
44
44
  {
45
45
  "name": "github-actions-authoring",
46
46
  "plugin": "vouchington-workflow",
47
- "pluginVersion": "0.10.0",
47
+ "pluginVersion": "0.11.0",
48
48
  "path": "github-actions-authoring/SKILL.md",
49
49
  "prerequisites": ["github-actions-checklist"]
50
50
  },
51
51
  {
52
52
  "name": "github-actions-checklist",
53
53
  "plugin": "vouchington-workflow",
54
- "pluginVersion": "0.10.0",
54
+ "pluginVersion": "0.11.0",
55
55
  "path": "github-actions-checklist/SKILL.md"
56
56
  },
57
57
  {
58
58
  "name": "github-issue",
59
59
  "plugin": "vouchington-workflow",
60
- "pluginVersion": "0.10.0",
60
+ "pluginVersion": "0.11.0",
61
61
  "path": "github-issue/SKILL.md"
62
62
  },
63
63
  {
@@ -70,26 +70,26 @@
70
70
  {
71
71
  "name": "npm-publishing",
72
72
  "plugin": "vouchington-workflow",
73
- "pluginVersion": "0.10.0",
73
+ "pluginVersion": "0.11.0",
74
74
  "path": "npm-publishing/SKILL.md"
75
75
  },
76
76
  {
77
77
  "name": "organize-github-issues",
78
78
  "plugin": "vouchington-workflow",
79
- "pluginVersion": "0.10.0",
79
+ "pluginVersion": "0.11.0",
80
80
  "path": "organize-github-issues/SKILL.md",
81
81
  "prerequisites": ["github-issue"]
82
82
  },
83
83
  {
84
84
  "name": "package-json-checklist",
85
85
  "plugin": "vouchington-workflow",
86
- "pluginVersion": "0.10.0",
86
+ "pluginVersion": "0.11.0",
87
87
  "path": "package-json-checklist/SKILL.md"
88
88
  },
89
89
  {
90
90
  "name": "planning",
91
91
  "plugin": "vouchington-workflow",
92
- "pluginVersion": "0.10.0",
92
+ "pluginVersion": "0.11.0",
93
93
  "path": "planning/SKILL.md",
94
94
  "prerequisites": ["github-issue"]
95
95
  },
@@ -103,65 +103,71 @@
103
103
  {
104
104
  "name": "postgres-node-performance-tuning",
105
105
  "plugin": "vouchington-database",
106
- "pluginVersion": "0.2.0",
106
+ "pluginVersion": "0.3.0",
107
107
  "path": "postgres-node-performance-tuning/SKILL.md"
108
108
  },
109
109
  {
110
110
  "name": "postgres-partitioning-uuid-v7",
111
111
  "plugin": "vouchington-database",
112
- "pluginVersion": "0.2.0",
112
+ "pluginVersion": "0.3.0",
113
113
  "path": "postgres-partitioning-uuid-v7/SKILL.md"
114
114
  },
115
+ {
116
+ "name": "postgres-schema-design",
117
+ "plugin": "vouchington-database",
118
+ "pluginVersion": "0.3.0",
119
+ "path": "postgres-schema-design/SKILL.md"
120
+ },
115
121
  {
116
122
  "name": "pr-description",
117
123
  "plugin": "vouchington-workflow",
118
- "pluginVersion": "0.10.0",
124
+ "pluginVersion": "0.11.0",
119
125
  "path": "pr-description/SKILL.md"
120
126
  },
121
127
  {
122
128
  "name": "retrospective",
123
129
  "plugin": "vouchington-workflow",
124
- "pluginVersion": "0.10.0",
130
+ "pluginVersion": "0.11.0",
125
131
  "path": "retrospective/SKILL.md"
126
132
  },
127
133
  {
128
134
  "name": "retrospective-distill",
129
135
  "plugin": "vouchington-workflow",
130
- "pluginVersion": "0.10.0",
136
+ "pluginVersion": "0.11.0",
131
137
  "path": "retrospective-distill/SKILL.md",
132
138
  "prerequisites": ["github-issue"]
133
139
  },
134
140
  {
135
141
  "name": "review-ci-logs",
136
142
  "plugin": "vouchington-workflow",
137
- "pluginVersion": "0.10.0",
143
+ "pluginVersion": "0.11.0",
138
144
  "path": "review-ci-logs/SKILL.md",
139
145
  "prerequisites": ["github-actions-checklist", "pr-description"]
140
146
  },
141
147
  {
142
148
  "name": "review-github-issue-taxonomy",
143
149
  "plugin": "vouchington-workflow",
144
- "pluginVersion": "0.10.0",
150
+ "pluginVersion": "0.11.0",
145
151
  "path": "review-github-issue-taxonomy/SKILL.md",
146
152
  "prerequisites": ["github-issue"]
147
153
  },
148
154
  {
149
155
  "name": "revisit-followups",
150
156
  "plugin": "vouchington-workflow",
151
- "pluginVersion": "0.10.0",
157
+ "pluginVersion": "0.11.0",
152
158
  "path": "revisit-followups/SKILL.md",
153
159
  "prerequisites": ["github-issue"]
154
160
  },
155
161
  {
156
162
  "name": "stacked-prs",
157
163
  "plugin": "vouchington-workflow",
158
- "pluginVersion": "0.10.0",
164
+ "pluginVersion": "0.11.0",
159
165
  "path": "stacked-prs/SKILL.md"
160
166
  },
161
167
  {
162
168
  "name": "static-analysis-checklist",
163
169
  "plugin": "vouchington-workflow",
164
- "pluginVersion": "0.10.0",
170
+ "pluginVersion": "0.11.0",
165
171
  "path": "static-analysis-checklist/SKILL.md",
166
172
  "prerequisites": ["github-actions-checklist"]
167
173
  },
@@ -12,3 +12,25 @@ Check query predicates, joins, ordering, and selected columns against index shap
12
12
  evidence to confirm planner behavior. Add extended statistics only when observed estimates show a
13
13
  correlation problem; verify the statistics are collected and used. Partitioning can reduce scanned
14
14
  data, but it does not replace suitable local indexes or predicates that permit pruning.
15
+
16
+ ## Query shapes
17
+
18
+ Test existence with `EXISTS`, not `COUNT(*) > 0`. Use correlated `NOT EXISTS` rather than
19
+ `NOT IN (SELECT ...)`, whose NULL behavior can silently exclude expected rows. Filter and order
20
+ UUIDv7 rows by `id`, not generated `created_at`, so the key's index supports the query.
21
+ Read and return tables with explicit columns: no `SELECT *`, `alias.*`, or `RETURNING *`.
22
+ A view may use `*` when its reviewed column list is the reader contract.
23
+
24
+ Use keyset pagination, never `OFFSET`. An optimizer fence is a `MATERIALIZED` CTE, not
25
+ `OFFSET 0`. Constrain a partitioned table's partition key, or document the reason for a
26
+ cross-partition query. Choose the partition key from dominant access patterns; a hot reader
27
+ of `RANGE (id)` needs an id or time bound.
28
+
29
+ A UUIDv7 child cannot predate its parent. For a `RANGE (id)` child read by parent id, use a
30
+ shared lower-bound helper equivalent to
31
+ `id >= min_uuidv7(uuid_extract_timestamp(parent_id) - interval '1 hour')` to prune earlier
32
+ partitions; the overlap allows clock skew. Rate limits query an actor-keyed table rather than
33
+ recipient-keyed fan-out rows. Verify actual pruning with EXPLAIN.
34
+
35
+ Generic `jonathanong/no-mistakes` checks include `postgres-no-offset`; inspect available rules
36
+ and configure reviewed cross-partition exceptions rather than treating a rule as plan evidence.
@@ -9,6 +9,9 @@ Read applicable `AGENTS.md` and their declared adapter alias, otherwise
9
9
  `.agents/skills/postgres-partitioning-uuid-v7/SKILL.md` from the repository root if present.
10
10
  Local policy overrides these defaults; do not reload an already-read adapter or canonical skill.
11
11
 
12
+ For schema names, references, and canonical table shapes, read
13
+ [postgres-schema-design](../postgres-schema-design/SKILL.md).
14
+
12
15
  Partition only after lifecycle, retention, and query predicates show that it helps.
13
16
 
14
17
  - Treat a UUIDv7 as time ordered. Where the meaning matters, keep an explicit business timestamp.
@@ -0,0 +1,21 @@
1
+ ---
2
+ name: postgres-schema-design
3
+ description: Design PostgreSQL names, relations, history, work queues, and cursors.
4
+ ---
5
+
6
+ # PostgreSQL schema design
7
+
8
+ Read applicable `AGENTS.md` and the repository's local database adapter when present.
9
+ Local policy overrides these portable conventions.
10
+
11
+ - Names explain the stored thing; read [naming](references/naming.md) when naming schema objects.
12
+ - Columns say their type and FK target; finite sets use enums or lookup tables.
13
+ - Joined identities use foreign keys; read [relations](references/relations.md) before modeling references.
14
+ - Split differing shapes, share identical values, and keep history documents as JSON.
15
+ - Use canonical [table shapes](references/table-shapes.md) for revisions, changes, work, and cursors.
16
+ - Reuse parameterized trigger functions instead of copying bodies.
17
+ - Comment every table, column, and view; document reviewed exceptions explicitly.
18
+
19
+ For query shape, read [performance patterns](../postgres-node-performance-tuning/references/performance-patterns.md).
20
+ For partition lifecycle, use [UUIDv7 partitioning](../postgres-partitioning-uuid-v7/SKILL.md).
21
+ Consumer policy owns lifecycle, retention, migration strategy, and enforcement configuration.
@@ -0,0 +1,34 @@
1
+ # Naming
2
+
3
+ Use full words in snake_case. Tables read as `<owner>_<thing>s`, with only the last word
4
+ plural: `account_payment_methods`, not `accounts_payments_methods`. Core entities may use one
5
+ word (`accounts`, `orders`, `invoices`) when local policy permits it. Enum names follow the same
6
+ plural convention. Choose a shorter full-word name when a base identifier would exceed 63 bytes.
7
+
8
+ Indexes use `idx_<table>__<suffix>`; a unique index may use `uq_`. The suffix describes its
9
+ purpose or keys. The table portion may abbreviate only when it expands unambiguously to the
10
+ indexed table: preserve every word, its order, and separators. Each shortened word keeps its
11
+ first letter and at least three letters in their original order; words of three or fewer letters
12
+ stay whole. `idx_ord_line_itms__invoice_id` can name an index on `order_line_items`.
13
+ Initialisms and dropped words cannot. All identifiers stay within 63 bytes.
14
+
15
+ Column names and types imply each other:
16
+
17
+ - `timestamptz` ends in `_at`; dates use `day` or `_on`.
18
+ - Booleans contain a predicate prefix `is_`, `has_`, `can_`, or `should_` at a word boundary.
19
+ - FK columns end in the target's final word, singular: `invoice_id` references `invoices`.
20
+ Drop redundant owner words when the column's table already supplies them.
21
+ - A column referencing a natural key may use that key's name. A retained identity reference
22
+ keeps the entity's name even though the retained table ends in `identities`.
23
+ - Reserve `_user_id` and actor `_by_id` for references to users or retained user identities.
24
+ Adapt the actor convention to the repository's account model; never use an actor suffix for
25
+ an unrelated FK or an opaque token.
26
+
27
+ Trigger functions use `fn_<verb>_<what>` with this closed verb list:
28
+ `reject` (raise on invalid writes), `update` (set the current row), `project` (derived state in
29
+ other rows), `create` (companion rows), `lock`, `register`, and `ensure` (retained identity).
30
+ Ordinary functions use a name that explains their return value.
31
+
32
+ Generic enforcement: `postgres-object-naming`, `postgres-column-naming`, and
33
+ `postgres-identifier-length` in `jonathanong/no-mistakes`. Check the installed rule's supported options before enabling it;
34
+ an upstream rule name does not establish that the installed release includes it.