vouchington-tooling 0.41.0 → 0.43.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 (65) hide show
  1. package/bin/policy-runner.mjs +26 -0
  2. package/dist/agent-blackboard/feedback-codec.d.mts +1 -0
  3. package/dist/agent-blackboard/feedback-codec.mjs +1 -1
  4. package/dist/cli/usage.d.mts +1 -1
  5. package/dist/cli/usage.mjs +3 -1
  6. package/dist/gh-cli/pr-create.d.mts +2 -0
  7. package/dist/gh-cli/pr-create.mjs +5 -1
  8. package/dist/mcp-server/context.d.mts +22 -2
  9. package/dist/mcp-server/context.mjs +14 -1
  10. package/dist/mcp-server/dispatch.d.mts +11 -0
  11. package/dist/mcp-server/dispatch.mjs +12 -0
  12. package/dist/mcp-server/handlers-journal.d.mts +14 -1
  13. package/dist/mcp-server/handlers-journal.mjs +14 -9
  14. package/dist/mcp-server/handlers-pr.d.mts +4 -0
  15. package/dist/mcp-server/handlers-pr.mjs +143 -0
  16. package/dist/mcp-server/handlers-retrospective.d.mts +12 -0
  17. package/dist/mcp-server/handlers-retrospective.mjs +154 -0
  18. package/dist/mcp-server/harness.test-helpers.d.mts +10 -1
  19. package/dist/mcp-server/harness.test-helpers.mjs +4 -1
  20. package/dist/mcp-server/journal-read.d.mts +9 -0
  21. package/dist/mcp-server/journal-read.mjs +33 -0
  22. package/dist/mcp-server/policy-config.d.mts +6 -0
  23. package/dist/mcp-server/policy-config.mjs +60 -0
  24. package/dist/mcp-server/policy-run.d.mts +40 -0
  25. package/dist/mcp-server/policy-run.mjs +116 -0
  26. package/dist/mcp-server/policy-spawn.d.mts +24 -0
  27. package/dist/mcp-server/policy-spawn.mjs +97 -0
  28. package/dist/mcp-server/pr-body-file.d.mts +4 -0
  29. package/dist/mcp-server/pr-body-file.mjs +22 -0
  30. package/dist/mcp-server/pr-body.d.mts +20 -0
  31. package/dist/mcp-server/pr-body.mjs +47 -0
  32. package/dist/mcp-server/pr-defaults.d.mts +16 -0
  33. package/dist/mcp-server/pr-defaults.mjs +35 -0
  34. package/dist/mcp-server/pr-gh.d.mts +16 -0
  35. package/dist/mcp-server/pr-gh.mjs +56 -0
  36. package/dist/mcp-server/pr-input.d.mts +26 -0
  37. package/dist/mcp-server/pr-input.mjs +62 -0
  38. package/dist/mcp-server/pr-journal.d.mts +12 -0
  39. package/dist/mcp-server/pr-journal.mjs +86 -0
  40. package/dist/mcp-server/pr-live.d.mts +24 -0
  41. package/dist/mcp-server/pr-live.mjs +49 -0
  42. package/dist/mcp-server/pr-tools.test-helpers.d.mts +54 -0
  43. package/dist/mcp-server/pr-tools.test-helpers.mjs +67 -0
  44. package/dist/mcp-server/retrospective-facts-input.d.mts +6 -0
  45. package/dist/mcp-server/retrospective-facts-input.mjs +25 -0
  46. package/dist/mcp-server/retrospective-front-matter.d.mts +22 -0
  47. package/dist/mcp-server/retrospective-front-matter.mjs +88 -0
  48. package/dist/mcp-server/retrospective-input.d.mts +29 -0
  49. package/dist/mcp-server/retrospective-input.mjs +145 -0
  50. package/dist/mcp-server/retrospective-resolve.d.mts +15 -0
  51. package/dist/mcp-server/retrospective-resolve.mjs +41 -0
  52. package/dist/mcp-server/tools-pr.d.mts +2 -0
  53. package/dist/mcp-server/tools-pr.mjs +69 -0
  54. package/dist/mcp-server/tools-retrospective.d.mts +2 -0
  55. package/dist/mcp-server/tools-retrospective.mjs +48 -0
  56. package/dist/mcp-server/tools-schema.d.mts +54 -0
  57. package/dist/mcp-server/tools-schema.mjs +70 -0
  58. package/dist/mcp-server/tools.d.mts +1 -18
  59. package/dist/mcp-server/tools.mjs +6 -70
  60. package/dist/mcp-server/worktree.d.mts +11 -4
  61. package/dist/mcp-server/worktree.mjs +31 -6
  62. package/docs/mcp-server.md +164 -16
  63. package/package.json +1 -1
  64. package/skills/pr-description/SKILL.md +8 -0
  65. package/skills/retrospective/SKILL.md +5 -2
@@ -1,62 +1,6 @@
1
- const ID_PATTERN = '^[A-Za-z0-9._:-]+$';
2
- const SESSION_ID = {
3
- type: 'string',
4
- pattern: ID_PATTERN,
5
- maxLength: 256,
6
- description: 'Explicit session id (URL-safe). Never inferred by the server.',
7
- };
8
- const WORKTREE = {
9
- type: 'string',
10
- description: 'Absolute path of the top level of a git worktree (`git rev-parse --show-toplevel`), not a ' +
11
- 'subdirectory. Defaults to the launch directory worktree; required when the server was ' +
12
- 'launched outside a git worktree.',
13
- };
14
- const PARENT_SESSION_ID = {
15
- type: ['string', 'null'],
16
- pattern: ID_PATTERN,
17
- maxLength: 256,
18
- description: 'Direct parent session id, or null for a root session.',
19
- };
20
- const AGENT = { type: 'string', maxLength: 128, description: 'Agent name.' };
21
- const VERSION = { type: 'string', maxLength: 128, description: 'Agent version.' };
22
- const OUTCOMES = [
23
- 'in-progress',
24
- 'success',
25
- 'failure',
26
- 'cancelled',
27
- 'timed-out',
28
- 'no-change',
29
- 'policy-refusal',
30
- 'unknown',
31
- ];
32
- const COVERAGE_STATUSES = ['complete', 'partial', 'unavailable', 'not-assessed', 'not-started'];
33
- const READ_ONLY = flags(true, false, true);
34
- const ADDITIVE = flags(false, false, false);
35
- const IDEMPOTENT_ADDITIVE = flags(false, false, true);
36
- const MUTATING = flags(false, true, true);
37
- function flags(readOnly, destructive, idempotent) {
38
- return {
39
- readOnlyHint: readOnly,
40
- destructiveHint: destructive,
41
- idempotentHint: idempotent,
42
- openWorldHint: false,
43
- };
44
- }
45
- function tool(name, description, annotations, properties = {}, required = []) {
46
- return {
47
- name,
48
- description,
49
- inputSchema: {
50
- type: 'object',
51
- properties: { sessionId: SESSION_ID, worktree: WORKTREE, ...properties },
52
- required: ['sessionId', ...required],
53
- additionalProperties: false,
54
- },
55
- annotations,
56
- };
57
- }
58
- const IDENTITY = { parentSessionId: PARENT_SESSION_ID, agent: AGENT, version: VERSION };
59
- const IDENTITY_REQUIRED = ['parentSessionId', 'agent', 'version'];
1
+ import { ADDITIVE, AGENT, COVERAGE, IDEMPOTENT_ADDITIVE, IDENTITY, IDENTITY_REQUIRED, ID_PATTERN, MUTATING, OUTCOMES, PARENT_SESSION_ID, READ_ONLY, VERSION, tool, } from "./tools-schema.mjs";
2
+ import { PR_TOOLS } from "./tools-pr.mjs";
3
+ import { RETROSPECTIVE_TOOLS } from "./tools-retrospective.mjs";
60
4
  export const TOOLS = [
61
5
  tool('journal_append', 'Appends one journal entry to the caller session and returns the verified read-back receipt. ' +
62
6
  'Nothing is written unless every required field validates. Interactive mode retains the ' +
@@ -78,17 +22,7 @@ export const TOOLS = [
78
22
  minItems: 1,
79
23
  description: 'Exact owner/name repositories this entry concerns.',
80
24
  },
81
- feedbackCoverage: {
82
- type: 'object',
83
- properties: {
84
- status: { enum: COVERAGE_STATUSES },
85
- sources: { type: 'array', items: { type: 'string' } },
86
- droppedCount: { type: 'integer', minimum: 0 },
87
- },
88
- required: ['status'],
89
- additionalProperties: false,
90
- description: 'Explicit coverage; sources default to [] and droppedCount to 0.',
91
- },
25
+ feedbackCoverage: COVERAGE,
92
26
  category: { type: 'string', description: 'Optional context; never changes entry type.' },
93
27
  }, [
94
28
  ...IDENTITY_REQUIRED,
@@ -112,6 +46,8 @@ export const TOOLS = [
112
46
  'as sourceEventId and diagnostic.', IDEMPOTENT_ADDITIVE),
113
47
  tool('session_ensure', 'Idempotently creates a session, or verifies that an existing one has the same identity. ' +
114
48
  'Returns the session id and whether it was created.', IDEMPOTENT_ADDITIVE, IDENTITY, IDENTITY_REQUIRED),
49
+ ...RETROSPECTIVE_TOOLS,
50
+ ...PR_TOOLS,
115
51
  tool('snapshot_export', 'Exports non-archived sessions into a private local JSONL file chosen by the blackboard ' +
116
52
  'client. Returns the path, counts, checksum, manifest, and cleanupToken; never the records. ' +
117
53
  'sessionId identifies the caller.', ADDITIVE, {
@@ -11,12 +11,19 @@ export declare const runIsolatedGit: RunTextCommand;
11
11
  * or an unsafe repository, is thrown with the cause rather than reported as "outside a worktree".
12
12
  */
13
13
  export declare function launchWorktreeRoot(cwd: string, runGit: RunTextCommand): Promise<string | undefined>;
14
+ /**
15
+ * The worktree paths in `git worktree list --porcelain -z` output: NUL-terminated fields, an empty
16
+ * field ending each record, so a path containing a newline cannot forge a record.
17
+ */
18
+ export declare function parseWorktreeList(output: string): string[];
14
19
  /**
15
20
  * Resolves the worktree a tool call may act on. It must be an absolute path that, after
16
- * resolving symlinks, is exactly the top level of a git worktree (of any repository on the
17
- * machine). A subdirectory, a relative or nonexistent path, and a non-git directory are rejected.
18
- * Git is asked on every call, so worktrees created after the server started are accepted and
19
- * removed ones are not. Without `requested`, the launch worktree is used when there is one.
21
+ * resolving symlinks, is exactly the top level of a git worktree. A subdirectory, a relative or
22
+ * nonexistent path, and a non-git directory are rejected. When the server was launched inside a
23
+ * repository, the path must also be one of that repository's worktrees (primary or linked), so an
24
+ * unrelated `git init` directory is refused. A server launched outside any repository accepts the
25
+ * top level of any worktree. Git is asked on every call, so worktrees created after the server
26
+ * started are accepted and removed ones are not. Without `requested`, the launch worktree is used.
20
27
  */
21
28
  export declare function resolveWorktree(input: {
22
29
  launchRoot: string | undefined;
@@ -52,12 +52,33 @@ export async function launchWorktreeRoot(cwd, runGit) {
52
52
  }
53
53
  return realpath(output.trim());
54
54
  }
55
+ /**
56
+ * The worktree paths in `git worktree list --porcelain -z` output: NUL-terminated fields, an empty
57
+ * field ending each record, so a path containing a newline cannot forge a record.
58
+ */
59
+ export function parseWorktreeList(output) {
60
+ return output
61
+ .split('\0')
62
+ .filter((field) => field.startsWith('worktree '))
63
+ .map((field) => field.slice('worktree '.length));
64
+ }
65
+ /**
66
+ * The real paths of every worktree of the repository containing `root` (the primary worktree and
67
+ * each linked one).
68
+ */
69
+ async function repositoryWorktrees(root, runGit) {
70
+ const output = await runGit(['-C', root, 'worktree', 'list', '--porcelain', '-z']);
71
+ const paths = parseWorktreeList(output).map(canonical);
72
+ return new Set((await Promise.all(paths)).filter((path) => path !== undefined));
73
+ }
55
74
  /**
56
75
  * Resolves the worktree a tool call may act on. It must be an absolute path that, after
57
- * resolving symlinks, is exactly the top level of a git worktree (of any repository on the
58
- * machine). A subdirectory, a relative or nonexistent path, and a non-git directory are rejected.
59
- * Git is asked on every call, so worktrees created after the server started are accepted and
60
- * removed ones are not. Without `requested`, the launch worktree is used when there is one.
76
+ * resolving symlinks, is exactly the top level of a git worktree. A subdirectory, a relative or
77
+ * nonexistent path, and a non-git directory are rejected. When the server was launched inside a
78
+ * repository, the path must also be one of that repository's worktrees (primary or linked), so an
79
+ * unrelated `git init` directory is refused. A server launched outside any repository accepts the
80
+ * top level of any worktree. Git is asked on every call, so worktrees created after the server
81
+ * started are accepted and removed ones are not. Without `requested`, the launch worktree is used.
61
82
  */
62
83
  export async function resolveWorktree(input) {
63
84
  const { launchRoot, requested, runGit } = input;
@@ -68,8 +89,12 @@ export async function resolveWorktree(input) {
68
89
  if (!isAbsolute(path))
69
90
  throw new Error('worktree must be an absolute path to a git worktree');
70
91
  const target = await canonical(path);
71
- if (target !== undefined && (await launchWorktreeRoot(target, runGit)) === target)
72
- return target;
92
+ if (target !== undefined && (await launchWorktreeRoot(target, runGit)) === target) {
93
+ if (launchRoot === undefined || (await repositoryWorktrees(launchRoot, runGit)).has(target))
94
+ return target;
95
+ throw new Error(`worktree ${path} is not a worktree of the repository this server was launched in ` +
96
+ `(${launchRoot}); \`git worktree list\` there names the worktrees it may act on`);
97
+ }
73
98
  throw new Error(`worktree ${path} is not the top level of a git worktree: it must exist and be the ` +
74
99
  'directory `git rev-parse --show-toplevel` reports, not a subdirectory of it');
75
100
  }
@@ -6,8 +6,8 @@ a tool argument; there is no temp file, `--file`, or replay command. The server
6
6
  existing `vouchington-tooling/agent-blackboard` exports and adds no provider protocol.
7
7
 
8
8
  Phase 1 of [vouchington-tooling#324](https://github.com/vouchington/vouchington-tooling/issues/324)
9
- covers the journal, outbox, session, and snapshot tools below. Retrospective and pull request tools
10
- are later phases.
9
+ covers the journal, outbox, session, and snapshot tools below. Phase 2 adds the retrospective tools
10
+ and the policy contract they run. Phase 3 adds the pull request tools.
11
11
 
12
12
  ## Install
13
13
 
@@ -82,15 +82,17 @@ 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
- | `snapshot_export` | optional `agent`, `version`, `parentSessionId`, `data`, `dataArrayContains`, `inactiveForHours` | `sessionId`, `path`, `counts`, `checksum`, `manifest`, `cleanupToken` |
93
- | `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` | 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` |
94
96
 
95
97
  `journal_append` validates the whole feedback envelope with `validateFeedbackEnvelope` before
96
98
  anything is written, so a missing required field writes nothing. It returns the verified read-back
@@ -163,20 +165,166 @@ is unavailable.
163
165
  The server sends the same list in its MCP `instructions`, so a harness that surfaces server
164
166
  instructions shows it to the agent.
165
167
 
168
+ ## Retrospective tools and the policy contract
169
+
170
+ `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
172
+ answers 404) reports `saved: false`.
173
+
174
+ `retrospective_save` builds a `retrospective` envelope and writes it exactly as `journal_append`
175
+ does (same receipt, `pendingCount`, `worktreePendingCount`, outbox rules, and server-owned
176
+ timestamp; a `timestamp` argument is rejected). The document comes from `markdown` or from
177
+ `compose`, the input of `composeRetrospective` (`narrative`, `date`, `issues`, `prs`,
178
+ `description`, `facts`, `transcript`, `tools`, `architecture`, optional `knownSensitiveValues`;
179
+ unknown keys are rejected, and the journal audit reads through the server's own client).
180
+ `compose.transcript` accepts only `{ sessionId?, harnessSessionId? }` (`sessionId` must equal the
181
+ call's `sessionId`) or `{ status, reason }` for an unassessed transcript. The transcript is found by
182
+ `harnessSessionId`, the harness's own session UUID, else by the call's `sessionId` when that is a
183
+ UUID; otherwise the call fails rather than reporting the transcript unavailable. When the server's environment names its harness session (`CLAUDE_CODE_SESSION_ID`, `CODEX_THREAD_ID`, and so on), only that session's transcript is read (after `/clear` or a thread switch, reconnect the server, or pass `{ status, reason }`); `jsonlPath`, `projectsDir`, `codexSessionsDir`,
184
+ `grokSessionsDir`, and `cwd` are rejected, and the server reads the transcript from the worktree
185
+ and its default session roots. The server's blackboard token, and any `knownSensitiveValues`, are
186
+ redacted from the document before the policy sees it or the envelope is stored. Facts commands
187
+ are killed after 120 seconds. `compose.facts` takes `pr`, `branch`, `noPr`, or `{ status, reason }`;
188
+ `repo` is rejected, so the server's GitHub token never queries a repository other than the
189
+ worktree's. Facts run `git` and `gh` in the worktree as the server's user, honoring its
190
+ repository configuration (which can select helpers that run code), so the worktree, including
191
+ `.git/config`, must be as trusted as the policy module. `dryRun`
192
+ runs every check and returns the document without writing.
193
+
194
+ The document must open with the front matter `composeRetrospective` writes: lines of
195
+ `key: <JSON value>` between two `---` lines. Recognized keys are `date`, `issues`, `prs` (required),
196
+ `session_id`, `description`, `work_outcome`, `feedback_coverage` (`status`, `sources`,
197
+ `droppedCount`), and `repositories`; anything else, a duplicate, or a malformed line is rejected.
198
+ `session_id`, `work_outcome`, `feedback_coverage`, and `repositories` must equal the arguments when
199
+ present. `repositories` resolves from the argument, else the front matter, else the policy's
200
+ `pullRequestDefaults()` result, else an error, and is stored sorted, deduplicated, and lowercase.
201
+ The default `sourceEventId` is `retrospective-<sessionId>`, or `retrospective-<sha256 hex of the
202
+ sessionId>` when that would exceed 256 bytes. Re-saving identical content returns the
203
+ stored receipt and timestamp. Changed content under the same `sourceEventId` is an
204
+ `event-conflict`; a revision needs a new `sourceEventId`. `compose` regenerates the document from
205
+ the live journal and repository, so repeating a `compose` call is not a retry: when an interactive
206
+ save is retained in the outbox, deliver it with `outbox_flush`.
207
+
208
+ ### Policy module
209
+
210
+ The consumer's root `package.json` may declare
211
+ `"vouchington-tooling": { "policy": "dev/agent-policy.mts" }`. The path must be relative and, after
212
+ resolving symlinks, stay inside the worktree. Without a declaration the policy checks are skipped.
213
+ Policy tools also require a server launched inside a repository worktree on macOS or Linux (they
214
+ refuse to run on Windows, where the server cannot reap a policy's descendants by process group),
215
+ and refuse to run when `AGENT_BLACKBOARD_TOKEN`, `GH_TOKEN`, or `GITHUB_TOKEN` is set to a value
216
+ shorter than four characters, which could not be redacted from policy output. Each call runs one named
217
+ export in a fresh child process (`bin/policy-runner.mjs`, started with the server's own Node), never
218
+ inside the server, with the worktree as its working directory and an environment of only `HOME`,
219
+ `PATH` (`/usr/bin`, `/bin`, `/usr/local/bin`, `/opt/homebrew/bin`, the Node directory, and the
220
+ directory of the first `gh` on the server's `PATH`, deduplicated), and `GH_TOKEN` / `GITHUB_TOKEN` when the server has
221
+ them. Blackboard credentials and `NODE_OPTIONS` are not passed. The child still runs as the
222
+ server's user, so this is not a sandbox: on Linux it could read the server's environment from
223
+ `/proc/<server pid>/environ`. Treat the policy module as trusted code. After the runner exits, its
224
+ whole process group is killed, so nothing it started outlives the call. The runner reads
225
+ `{ policyVersion: 1, export, input }` on stdin and writes `{ ok: true, result }` (or
226
+ `{ ok: false, error }`) on stdout; a missing export is skipped.
227
+
228
+ | Export | Input | Result | Timeout |
229
+ | ----------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------- | ------- |
230
+ | `validateRetrospective` | `sessionId`, `parentSessionId`, `mode`, `workOutcome`, `feedbackCoverage`, `repositories`, `markdown`, `sourceEventId`, `metadata` (`date`, `issues`, `prs`, `description`) | `{ errors: string[], warnings?: string[] }` | 30 s |
231
+ | `pullRequestDefaults` | `{}` 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
+
234
+ Any `errors` reject the save and write nothing. A non-zero exit, a timeout (reported as
235
+ `policy <export> timed out after <N>s`), output over 1 MB, malformed JSON, or a result of the wrong
236
+ shape is an error, never a pass.
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
+
166
312
  ## Worktree validation
167
313
 
168
314
  `worktree` is checked on every call. It must be an absolute path that, after resolving symlinks,
169
315
  equals the top level of a git worktree, that is, what `git rev-parse --show-toplevel` prints when
170
- run in it. Any git worktree on the machine is accepted, including one of a repository unrelated to
171
- the launch directory, so the tools can act on every checkout the user can reach. Worktrees created
172
- after launch are accepted and removed ones are refused. Rejected: a subdirectory of a worktree, a
316
+ run in it. When the server was launched inside a repository, the real path must also equal one of
317
+ the `worktree` entries of `git worktree list --porcelain` run in the launch root (the primary
318
+ worktree or a linked one), so an unrelated repository is refused. A server launched outside any
319
+ repository accepts the top level of any git worktree for the journal tools, but refuses the policy
320
+ tools. Worktrees created after launch are accepted and removed ones are refused. Rejected: a subdirectory of a worktree, a
173
321
  relative or nonexistent path, and a directory that is not in a git worktree. Git runs without
174
322
  `GIT_*` variables, so `GIT_DIR` cannot redirect the check, and with `LC_ALL=C` and no `LANGUAGE`, so
175
323
  its "not a git repository" diagnostic is never translated.
176
324
 
177
325
  `worktree` only chooses where the outbox lives. It is not a trust boundary for loading code: the
178
- server never imports modules from it, and a tool that needs to must first get a repository allowlist
179
- from the machine registration.
326
+ server never imports modules from it. The policy tools run the declared module only in a child
327
+ process without blackboard credentials, as described above.
180
328
 
181
329
  When `worktree` is omitted, the launch directory's worktree top level is used if the launch
182
330
  directory is inside one, and it is validated like any explicit path. If the server was launched
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vouchington-tooling",
3
- "version": "0.41.0",
3
+ "version": "0.43.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": {
@@ -54,6 +54,14 @@ not evidence of a miss; classify genuine CI-only or external infrastructure fail
54
54
  Retain confirmed gap records after CI turns green. Diagnose the failure through the repository's
55
55
  CI investigation workflow; a failed status alone does not establish a gap.
56
56
 
57
+ Create, update, and validate pull requests through the `pr_create`, `pr_update`, and `pr_validate`
58
+ MCP tools with structured input, not by hand-writing a body: pass `title`, `summary`, `impact`,
59
+ `relatedIssues` (`closes` or `refs`, each with a reason), `details` (Markdown paragraphs or
60
+ bullets), `base`, `agent`, optional `model`, `mode`, and `workspaceSetup`. The server renders the
61
+ required headings and provenance lines, runs the repository's policy validation, and blocks on its
62
+ errors, so put the guidance above into `summary`, `impact`, and `details`. Run `pr_validate` first
63
+ to preview the rendered body. `pr_update` preserves the live Shepherd Journal.
64
+
57
65
  Use the repository's approved PR tooling and preserve sections managed by its review automation
58
66
  verbatim. Close only issues fully resolved and explain non-closing issue links. Apply this guidance
59
67
  to descriptions being created or updated, not a bulk rewrite of historical pull requests.
@@ -14,7 +14,8 @@ local `AGENTS.md`, and journal guidance first.
14
14
 
15
15
  1. Read the agent's own journal oldest-first and check for an existing retrospective. Capture a
16
16
  material new observation before filing a follow-up issue. Preserve entry-level repository tags.
17
- 2. Generate routine repository and transcript facts with the approved composer. Keep exact factual
17
+ 2. Generate routine repository and transcript facts with the approved composer: pass `compose` to
18
+ the `retrospective_save` MCP tool and preview the composition with `dryRun`, never a CLI. Keep exact factual
18
19
  section markers generated, preserve unavailable counts, and never replace missing evidence with
19
20
  invented zero statistics. Do not paste raw collectors' output or manually reconstruct facts.
20
21
  3. Record work outcome, feedback coverage, and delivery independently. Success, failure,
@@ -30,7 +31,9 @@ local `AGENTS.md`, and journal guidance first.
30
31
  5. Keep the top five findings or improvements in the human prose. The bounded durable evidence can
31
32
  retain additional relevant findings; a prose limit is not permission to silently discard them.
32
33
  Separate recurring root causes from one-off noise and preserve existing issue references.
33
- 6. Save through the shared validated writer. Interactive outages use the approved bounded private
34
+ 6. Check for an existing retrospective with the `retrospective_check` MCP tool and save with
35
+ `retrospective_save`, never a CLI. A revision of changed content needs a new `sourceEventId`.
36
+ Interactive outages use the approved bounded private
34
37
  outbox with visible pending count; failed persistence blocks capture. Autonomous reporting stays
35
38
  online and never uses that outbox to authorize an attempt or completion. Report the acknowledged
36
39
  record/receipt, or the actual pending/blocked state and next replay action.