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.
- package/README.md +43 -0
- package/dist/agent-blackboard/client.d.mts +5 -0
- package/dist/agent-blackboard/client.mjs +1 -1
- package/dist/agent-blackboard/feedback-audit-source.d.mts +13 -0
- package/dist/agent-blackboard/feedback-audit-source.mjs +30 -0
- package/dist/agent-blackboard/feedback-compose.d.mts +5 -1
- package/dist/agent-blackboard/feedback-compose.mjs +8 -18
- package/dist/agent-blackboard/feedback-journal-audit.d.mts +20 -0
- package/dist/agent-blackboard/feedback-journal-audit.mjs +29 -0
- package/dist/agent-blackboard/feedback-journal-sandbox.d.mts +5 -0
- package/dist/agent-blackboard/feedback-journal-sandbox.mjs +40 -0
- package/dist/agent-blackboard/index.d.mts +9 -8
- package/dist/agent-blackboard/index.mjs +7 -34
- package/dist/agent-blackboard/journal.d.mts +21 -0
- package/dist/agent-blackboard/journal.mjs +45 -0
- package/dist/agent-blackboard/session-tools.d.mts +36 -0
- package/dist/agent-blackboard/session-tools.mjs +41 -0
- package/dist/cli/commands/mcp.d.mts +5 -0
- package/dist/cli/commands/mcp.mjs +18 -0
- package/dist/cli/index.mjs +5 -5
- package/dist/cli/installed-version.d.mts +1 -0
- package/dist/cli/installed-version.mjs +5 -0
- package/dist/cli/parse-forwarded-commands.d.mts +3 -0
- package/dist/cli/parse-forwarded-commands.mjs +2 -0
- package/dist/cli/parse.d.mts +3 -0
- package/dist/cli/usage.d.mts +1 -1
- package/dist/cli/usage.mjs +6 -0
- package/dist/mcp-server/args.d.mts +9 -0
- package/dist/mcp-server/args.mjs +48 -0
- package/dist/mcp-server/context.d.mts +20 -0
- package/dist/mcp-server/context.mjs +23 -0
- package/dist/mcp-server/dispatch.d.mts +18 -0
- package/dist/mcp-server/dispatch.mjs +69 -0
- package/dist/mcp-server/fake-blackboard.test-helpers.d.mts +38 -0
- package/dist/mcp-server/fake-blackboard.test-helpers.mjs +75 -0
- package/dist/mcp-server/git-fixture.test-helpers.d.mts +16 -0
- package/dist/mcp-server/git-fixture.test-helpers.mjs +35 -0
- package/dist/mcp-server/handlers-journal.d.mts +5 -0
- package/dist/mcp-server/handlers-journal.mjs +81 -0
- package/dist/mcp-server/handlers-session.d.mts +4 -0
- package/dist/mcp-server/handlers-session.mjs +47 -0
- package/dist/mcp-server/harness.test-helpers.d.mts +33 -0
- package/dist/mcp-server/harness.test-helpers.mjs +48 -0
- package/dist/mcp-server/run.d.mts +20 -0
- package/dist/mcp-server/run.mjs +21 -0
- package/dist/mcp-server/sdk.d.mts +17 -0
- package/dist/mcp-server/sdk.mjs +33 -0
- package/dist/mcp-server/server.d.mts +9 -0
- package/dist/mcp-server/server.mjs +17 -0
- package/dist/mcp-server/tools.d.mts +19 -0
- package/dist/mcp-server/tools.mjs +118 -0
- package/dist/mcp-server/worktree.d.mts +18 -0
- package/dist/mcp-server/worktree.mjs +58 -0
- package/dist/session-friction/audit-block.d.mts +4 -0
- package/dist/session-friction/audit-block.mjs +45 -0
- package/dist/session-friction/ci-failures.d.mts +2 -1
- package/dist/session-friction/ci-failures.mjs +10 -46
- package/dist/session-friction/journal.d.mts +14 -0
- package/dist/session-friction/journal.mjs +66 -0
- package/dist/session-friction/report.mjs +10 -74
- package/docs/mcp-server.md +127 -0
- package/package.json +7 -2
- 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
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
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(
|
|
126
|
-
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.
|
|
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.
|
|
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.
|