vouchington-tooling 0.24.0 → 0.25.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 (63) hide show
  1. package/README.md +43 -0
  2. package/dist/agent-blackboard/client.d.mts +5 -0
  3. package/dist/agent-blackboard/client.mjs +1 -1
  4. package/dist/agent-blackboard/feedback-audit-source.d.mts +13 -0
  5. package/dist/agent-blackboard/feedback-audit-source.mjs +30 -0
  6. package/dist/agent-blackboard/feedback-compose.d.mts +5 -1
  7. package/dist/agent-blackboard/feedback-compose.mjs +8 -18
  8. package/dist/agent-blackboard/feedback-journal-audit.d.mts +20 -0
  9. package/dist/agent-blackboard/feedback-journal-audit.mjs +29 -0
  10. package/dist/agent-blackboard/feedback-journal-sandbox.d.mts +5 -0
  11. package/dist/agent-blackboard/feedback-journal-sandbox.mjs +40 -0
  12. package/dist/agent-blackboard/index.d.mts +9 -8
  13. package/dist/agent-blackboard/index.mjs +7 -34
  14. package/dist/agent-blackboard/journal.d.mts +21 -0
  15. package/dist/agent-blackboard/journal.mjs +45 -0
  16. package/dist/agent-blackboard/session-tools.d.mts +36 -0
  17. package/dist/agent-blackboard/session-tools.mjs +41 -0
  18. package/dist/cli/commands/mcp.d.mts +5 -0
  19. package/dist/cli/commands/mcp.mjs +18 -0
  20. package/dist/cli/index.mjs +5 -5
  21. package/dist/cli/installed-version.d.mts +1 -0
  22. package/dist/cli/installed-version.mjs +5 -0
  23. package/dist/cli/parse-forwarded-commands.d.mts +3 -0
  24. package/dist/cli/parse-forwarded-commands.mjs +2 -0
  25. package/dist/cli/parse.d.mts +3 -0
  26. package/dist/cli/usage.d.mts +1 -1
  27. package/dist/cli/usage.mjs +6 -0
  28. package/dist/mcp-server/args.d.mts +9 -0
  29. package/dist/mcp-server/args.mjs +48 -0
  30. package/dist/mcp-server/context.d.mts +20 -0
  31. package/dist/mcp-server/context.mjs +23 -0
  32. package/dist/mcp-server/dispatch.d.mts +18 -0
  33. package/dist/mcp-server/dispatch.mjs +69 -0
  34. package/dist/mcp-server/fake-blackboard.test-helpers.d.mts +38 -0
  35. package/dist/mcp-server/fake-blackboard.test-helpers.mjs +75 -0
  36. package/dist/mcp-server/git-fixture.test-helpers.d.mts +16 -0
  37. package/dist/mcp-server/git-fixture.test-helpers.mjs +35 -0
  38. package/dist/mcp-server/handlers-journal.d.mts +5 -0
  39. package/dist/mcp-server/handlers-journal.mjs +81 -0
  40. package/dist/mcp-server/handlers-session.d.mts +4 -0
  41. package/dist/mcp-server/handlers-session.mjs +47 -0
  42. package/dist/mcp-server/harness.test-helpers.d.mts +33 -0
  43. package/dist/mcp-server/harness.test-helpers.mjs +48 -0
  44. package/dist/mcp-server/run.d.mts +20 -0
  45. package/dist/mcp-server/run.mjs +21 -0
  46. package/dist/mcp-server/sdk.d.mts +17 -0
  47. package/dist/mcp-server/sdk.mjs +33 -0
  48. package/dist/mcp-server/server.d.mts +9 -0
  49. package/dist/mcp-server/server.mjs +17 -0
  50. package/dist/mcp-server/tools.d.mts +19 -0
  51. package/dist/mcp-server/tools.mjs +118 -0
  52. package/dist/mcp-server/worktree.d.mts +18 -0
  53. package/dist/mcp-server/worktree.mjs +58 -0
  54. package/dist/session-friction/audit-block.d.mts +4 -0
  55. package/dist/session-friction/audit-block.mjs +45 -0
  56. package/dist/session-friction/ci-failures.d.mts +2 -1
  57. package/dist/session-friction/ci-failures.mjs +10 -46
  58. package/dist/session-friction/journal.d.mts +14 -0
  59. package/dist/session-friction/journal.mjs +66 -0
  60. package/dist/session-friction/report.mjs +10 -74
  61. package/docs/mcp-server.md +127 -0
  62. package/package.json +7 -2
  63. package/skills/stacked-prs/SKILL.md +15 -1
@@ -1,61 +1,8 @@
1
1
  import { eventLimit, readFrictionLog, requireDirectory } from './log.mjs';
2
- import { buildCiFailuresSection, getConformingGroups } from './ci-failures.mjs';
2
+ import { buildCiFailuresSection, getConformingGroups, incompleteCiSection } from './ci-failures.mjs';
3
+ import { errorMessage, scanJournal } from './journal.mjs';
3
4
  import { buildSandboxSection } from './sandbox.mjs';
4
5
  import { validateSessionId } from './session-id.mjs';
5
- const JOURNAL_ENTRY_MAX_COUNT = 500;
6
- const JOURNAL_MARKDOWN_MAX_BYTES = 10_000;
7
- const JOURNAL_TOTAL_MAX_BYTES = 1_000_000;
8
- function journalData(entry) {
9
- try {
10
- const data = entry?.data;
11
- return data ? { type: data.type, markdown: data.markdown } : undefined;
12
- }
13
- catch {
14
- return 'unreadable';
15
- }
16
- }
17
- async function collectEntries(entries) {
18
- const result = [];
19
- let consumed = 0;
20
- let inspectedBytes = 0;
21
- let retainedBytes = 0;
22
- let truncated = false;
23
- for await (const entry of entries) {
24
- consumed++;
25
- const scanned = journalData(entry);
26
- if (scanned === 'unreadable')
27
- truncated = true;
28
- const data = scanned === 'unreadable' ? undefined : scanned;
29
- const markdown = data?.markdown;
30
- if (data?.type === 'journal' && typeof markdown !== 'string')
31
- truncated = true;
32
- if (data?.type === 'journal' && typeof markdown === 'string') {
33
- const bytes = Buffer.byteLength(markdown);
34
- inspectedBytes += bytes;
35
- if (bytes > JOURNAL_MARKDOWN_MAX_BYTES)
36
- truncated = true;
37
- if (bytes <= JOURNAL_MARKDOWN_MAX_BYTES && retainedBytes + bytes <= JOURNAL_TOTAL_MAX_BYTES) {
38
- result.push({ data: { type: 'journal', markdown } });
39
- retainedBytes += bytes;
40
- }
41
- }
42
- if (consumed >= JOURNAL_ENTRY_MAX_COUNT ||
43
- inspectedBytes >= JOURNAL_TOTAL_MAX_BYTES ||
44
- retainedBytes >= JOURNAL_TOTAL_MAX_BYTES) {
45
- truncated = true;
46
- break;
47
- }
48
- }
49
- return { entries: result, truncated };
50
- }
51
- function errorMessage(error) {
52
- try {
53
- return error instanceof Error ? error.message : String(error);
54
- }
55
- catch {
56
- return '[unprintable error]';
57
- }
58
- }
59
6
  function sandboxMarkdown(friction) {
60
7
  if (friction.status === 'events')
61
8
  return `${friction.truncated ? `## Capture Coverage\nStatus: partial\nDropped records: ${friction.droppedCount}\n\n` : ''}${buildSandboxSection(friction.events)}`;
@@ -64,9 +11,6 @@ function sandboxMarkdown(friction) {
64
11
  const status = friction.status === 'empty' ? 'none observed' : 'unavailable (no friction log)';
65
12
  return `## Sandbox & Permission Audit\nStatus: ${status}`;
66
13
  }
67
- function incompleteCiSection(markdownBlocks) {
68
- return ['## CI Failures\nStatus: unavailable (journal scan incomplete)', ...markdownBlocks].join('\n\n');
69
- }
70
14
  function reportFromLog(sessionId, logOptions, journal) {
71
15
  try {
72
16
  const friction = readFrictionLog(sessionId, logOptions);
@@ -113,24 +57,16 @@ export async function buildSessionFrictionReport(sessionId, options) {
113
57
  validateSessionId(sessionId);
114
58
  requireDirectory(options.directory);
115
59
  eventLimit(options.maxEvents);
116
- let journal;
117
- try {
118
- const loaded = await options.journalLoader(sessionId);
119
- if (loaded.status === 'not-found')
120
- journal = { status: 'ok', markdownBlocks: [], truncated: false };
121
- else {
122
- const collected = await collectEntries(loaded.entries);
123
- journal = {
60
+ const scanned = await scanJournal(sessionId, options.journalLoader);
61
+ const journal = scanned.status === 'unreachable'
62
+ ? scanned
63
+ : scanned.status === 'not-found'
64
+ ? { status: 'ok', markdownBlocks: [], truncated: false }
65
+ : {
124
66
  status: 'ok',
125
- markdownBlocks: getConformingGroups(collected.entries),
126
- truncated: collected.truncated,
67
+ markdownBlocks: getConformingGroups(scanned.entries),
68
+ truncated: scanned.truncated,
127
69
  };
128
- }
129
- }
130
- catch (error) {
131
- const diagnostic = errorMessage(error);
132
- journal = { status: 'unreachable', diagnostic };
133
- }
134
70
  const report = reportFromLog(sessionId, options, journal);
135
71
  if (journal.status === 'unreachable') {
136
72
  const diagnostic = report.diagnostic
@@ -0,0 +1,127 @@
1
+ # MCP server
2
+
3
+ `vouchington mcp` serves the [agent-blackboard](../README.md#validated-feedback-and-delivery)
4
+ journal operations to an agent over stdio (Model Context Protocol). An agent passes the markdown as
5
+ a tool argument; there is no temp file, `--file`, or replay command. The server builds on the
6
+ existing `vouchington-tooling/agent-blackboard` exports and adds no provider protocol.
7
+
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.
11
+
12
+ ## Install
13
+
14
+ The server needs three packages next to `vouchington-tooling`. The last two are optional peer
15
+ dependencies, so nothing else in the CLI requires them:
16
+
17
+ ```bash
18
+ pnpm add -D vouchington-tooling agent-blackboard@^0.6.0
19
+ pnpm add -D @modelcontextprotocol/sdk zod
20
+ ```
21
+
22
+ `zod` is required by the SDK. When the SDK is missing, `vouchington mcp` writes a message naming the
23
+ package to stderr and exits with status 1. `agent-blackboard` is resolved from the validated
24
+ worktree at call time (`createRequire` on `<worktree>/package.json`), never from this package.
25
+
26
+ The connection comes from `AGENT_BLACKBOARD_URL` and `AGENT_BLACKBOARD_TOKEN` in the server's
27
+ environment. The server never searches for, prints, or mints them.
28
+
29
+ ## Launch
30
+
31
+ ```bash
32
+ vouchington mcp
33
+ ```
34
+
35
+ The command takes no arguments. Start it inside the repository's git worktree: that repository
36
+ defines which worktrees the tools accept. stdout carries only protocol frames, and diagnostics go
37
+ to stderr.
38
+
39
+ ## Tools
40
+
41
+ Every tool takes an explicit `sessionId` (`^[A-Za-z0-9._:-]+$`, at most 256 characters) that the
42
+ server never infers, and an optional `worktree`. Unknown arguments are rejected. Failures come back
43
+ as MCP tool errors (`isError: true`) with an actionable message rather than crashing the server.
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 | the session's entries as markdown |
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` |
54
+
55
+ `journal_append` validates the whole feedback envelope with `validateFeedbackEnvelope` before
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.
61
+
62
+ `sessionId` on `outbox_status`, `outbox_flush`, and `snapshot_export` only identifies the caller.
63
+ The outbox is per worktree and the status and flush cover every record in it. Flushing an empty
64
+ outbox does not create the directory. `snapshot_export` never accepts a destination: the
65
+ `agent-blackboard` client chooses a private file and returns its path. `session_archive` is the one
66
+ destructive tool; archived metadata is immutable, while entries stay appendable.
67
+
68
+ The outbox lives at `<worktree>/.local/blackboard-outbox` and is used only in interactive mode.
69
+ Autonomous mode forbids a filesystem outbox, so a failed delivery is an error.
70
+
71
+ ## Worktree validation
72
+
73
+ `worktree` must be an absolute path that, after resolving symlinks, exactly matches an entry of
74
+ `git worktree list --porcelain` for the repository the server was launched from. The list is read on
75
+ every call, so worktrees created after launch are accepted and removed ones are refused. Anything
76
+ else is rejected: a subdirectory of a worktree, a relative or nonexistent path, and a separate
77
+ repository, including one made with `git init` elsewhere. It defaults to the launch worktree's root.
78
+ Git runs without `GIT_*` variables, so `GIT_DIR` cannot redirect the check.
79
+
80
+ ## Registering the server
81
+
82
+ The commands below run `vouchington` from the repository's own `node_modules`, so a fresh worktree
83
+ uses the version its lockfile pins. Set the blackboard credentials in the environment the harness
84
+ gives the server.
85
+
86
+ Claude Code, `.mcp.json`:
87
+
88
+ ```json
89
+ {
90
+ "mcpServers": {
91
+ "vouchington-tooling": {
92
+ "command": "bash",
93
+ "args": ["-c", "exec \"$(git rev-parse --show-toplevel)/node_modules/.bin/vouchington\" mcp"]
94
+ }
95
+ }
96
+ }
97
+ ```
98
+
99
+ Codex, `.codex/config.toml`:
100
+
101
+ ```toml
102
+ [mcp_servers.vouchington-tooling]
103
+ command = "bash"
104
+ args = ["-c", "exec \"$(git rev-parse --show-toplevel)/node_modules/.bin/vouchington\" mcp"]
105
+ default_tools_approval_mode = "approve"
106
+ required = false
107
+ ```
108
+
109
+ Approving every tool of the server at once:
110
+
111
+ - Claude Code permission rule: `mcp__vouchington-tooling__*`
112
+ - Cursor permission rule: `Mcp(vouchington-tooling:*)`
113
+ - Codex: `default_tools_approval_mode = "approve"` above
114
+
115
+ ## What is verified
116
+
117
+ Covered by this package's tests: the tool list and schemas, every tool's success and rejection
118
+ paths against a faked blackboard client, the worktree gate against real temporary git repositories,
119
+ an in-process client and server round trip through the SDK's in-memory transport, and a spawned
120
+ `vouchington mcp` process driven over real stdio.
121
+
122
+ Not verified, and tracked as the unchecked Phase 0 checklist in
123
+ [#324](https://github.com/vouchington/vouchington-tooling/issues/324): each harness's approval
124
+ syntax and whether the rules above take effect, the working directory after entering a worktree,
125
+ environment interpolation and credential passing, `gh` authentication, behavior in a freshly
126
+ created worktree, and connectivity from automation. Treat the registration snippets and permission
127
+ rules as untested per harness until that checklist is complete.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vouchington-tooling",
3
- "version": "0.24.0",
3
+ "version": "0.25.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": {
@@ -352,21 +352,26 @@
352
352
  "picomatch": "4.0.7",
353
353
  "remark": "15.0.1",
354
354
  "remark-gfm": "4.0.1",
355
- "smol-toml": "1.8.0",
355
+ "smol-toml": "1.9.0",
356
356
  "spdx-expression-parse": "5.0.0",
357
357
  "yaml": "2.9.1"
358
358
  },
359
359
  "devDependencies": {
360
360
  "@ast-grep/cli": "0.45.3",
361
+ "@modelcontextprotocol/sdk": "1.31.0",
361
362
  "@types/picomatch": "^4.0.3",
362
363
  "@types/spdx-expression-parse": "4.0.0",
363
364
  "agent-blackboard": "^0.6.0"
364
365
  },
365
366
  "peerDependencies": {
367
+ "@modelcontextprotocol/sdk": "^1.31.0",
366
368
  "typescript": ">=5",
367
369
  "vitest": ">=5"
368
370
  },
369
371
  "peerDependenciesMeta": {
372
+ "@modelcontextprotocol/sdk": {
373
+ "optional": true
374
+ },
370
375
  "typescript": {
371
376
  "optional": true
372
377
  },
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: stacked-prs
3
- description: Drain native GitHub stacks from the bottom ready layer.
3
+ description: Drain native GitHub stacks from the bottom ready layer; a ready lower layer does not freeze later work.
4
4
  ---
5
5
 
6
6
  # Stacked pull requests
@@ -36,6 +36,20 @@ a stack, and who may authorize a merge.
36
36
  - A stall blocks only layers that depend on it. Ready the others. Pause the whole drain only when
37
37
  nothing further can be readied without the blocked layer.
38
38
  - Act only on layers assigned to you. Report the other layers; do not change them.
39
+ - One worker per native stack implements that stack and runs its shepherd poll. The parent does
40
+ not poll. It reconciles stacks, starts a worker, and carries merge questions to the human.
41
+ - Start shepherding when the first owned pull request exists.
42
+ - A lower layer is in decent shape when it is committed, its local checks have passed, its pull
43
+ request is open with an updated body, and shepherding has started. Decent shape does not mean
44
+ green CI, ready for review, or merged.
45
+ - Start the next dependent layer once the lower layer is in decent shape. Do not wait for merge,
46
+ a ready-delay, or green CI.
47
+ - A ready, mergeable, or queued lower layer blocks only an operation that would rewrite that
48
+ layer. Keep readying every other owned layer.
49
+ - Independent stacks run at the same time, one worker each. Dependent unmerged work is another
50
+ layer of the same stack, not a second stack whose base is an unmerged head.
51
+ - At most one poll process per stack. The parent does not run a second poll for the same stack.
52
+ - This skill does not grant a merge. Merge authorization stays with the consumer.
39
53
 
40
54
  Consumer wrapper owns: default branch, stacking tool and commands, merge-selector syntax, and
41
55
  merge authorization.