vouchington-tooling 0.30.0 → 0.32.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 (42) hide show
  1. package/README.md +9 -3
  2. package/bin/node-guard.mjs +9 -0
  3. package/bin/vouchington-mcp.mjs +20 -0
  4. package/bin/vouchington.mjs +18 -0
  5. package/dist/agent-blackboard/feedback-outbox-counts.d.mts +5 -0
  6. package/dist/agent-blackboard/feedback-outbox-counts.mjs +11 -0
  7. package/dist/agent-blackboard/feedback-outbox-directory.d.mts +2 -0
  8. package/dist/agent-blackboard/feedback-outbox-directory.mjs +16 -0
  9. package/dist/agent-blackboard/feedback-outbox.mjs +5 -7
  10. package/dist/agent-blackboard/index.d.mts +1 -0
  11. package/dist/agent-blackboard/index.mjs +1 -0
  12. package/dist/api-contract-discovery/protocol-http-association.mjs +3 -0
  13. package/dist/api-contract-discovery/protocol-http-method-mutations.d.mts +3 -0
  14. package/dist/api-contract-discovery/protocol-http-method-mutations.mjs +31 -0
  15. package/dist/api-contract-discovery/registered-route-binding-writes.mjs +8 -0
  16. package/dist/cli/commands/agent-blackboard-flags.d.mts +4 -0
  17. package/dist/cli/commands/agent-blackboard-flags.mjs +34 -0
  18. package/dist/cli/commands/agent-blackboard.mjs +27 -35
  19. package/dist/cli/index.d.mts +2 -0
  20. package/dist/cli/index.mjs +12 -12
  21. package/dist/cli/usage.d.mts +1 -1
  22. package/dist/cli/usage.mjs +2 -1
  23. package/dist/mcp-server/dispatch.d.mts +4 -1
  24. package/dist/mcp-server/dispatch.mjs +6 -4
  25. package/dist/mcp-server/handlers-outbox.mjs +2 -7
  26. package/dist/mcp-server/harness.test-helpers.d.mts +1 -0
  27. package/dist/mcp-server/harness.test-helpers.mjs +1 -1
  28. package/dist/mcp-server/main.d.mts +5 -0
  29. package/dist/mcp-server/main.mjs +8 -0
  30. package/dist/mcp-server/run.d.mts +2 -0
  31. package/dist/mcp-server/run.mjs +4 -1
  32. package/dist/mcp-server/sdk.d.mts +11 -2
  33. package/dist/mcp-server/sdk.mjs +37 -5
  34. package/dist/mcp-server/server.mjs +3 -2
  35. package/dist/mcp-server/tools.mjs +3 -2
  36. package/dist/mcp-server/worktree.d.mts +17 -10
  37. package/dist/mcp-server/worktree.mjs +48 -31
  38. package/docs/mcp-server.md +83 -25
  39. package/package.json +9 -2
  40. package/scripts/build.mjs +2 -0
  41. package/skills/blackboard/SKILL.md +66 -4
  42. package/skills/manifest.json +19 -19
@@ -1,4 +1,4 @@
1
- import { loadMcpSdk } from './sdk.mjs';
1
+ import { assertBlackboardInstalled, loadMcpSdk } from './sdk.mjs';
2
2
  import { createMcpServer } from './server.mjs';
3
3
  import { launchWorktreeRoot, runIsolatedGit } from './worktree.mjs';
4
4
  /**
@@ -8,11 +8,14 @@ import { launchWorktreeRoot, runIsolatedGit } from './worktree.mjs';
8
8
  export async function runMcpServer(options) {
9
9
  const runGit = options.runGit ?? runIsolatedGit;
10
10
  const sdk = await loadMcpSdk(options.importSdk);
11
+ if (options.blackboard === undefined)
12
+ assertBlackboardInstalled(options.resolveFrom);
11
13
  const launchRoot = await launchWorktreeRoot(options.cwd, runGit);
12
14
  const environment = {
13
15
  launchRoot,
14
16
  env: options.env,
15
17
  runGit,
18
+ ...(options.resolveFrom === undefined ? {} : { resolveFrom: options.resolveFrom }),
16
19
  ...(options.blackboard === undefined ? {} : { blackboard: options.blackboard }),
17
20
  };
18
21
  const server = createMcpServer(sdk, environment, options.version);
@@ -9,9 +9,18 @@ export type McpSdk = {
9
9
  ListToolsRequestSchema: typeof ListToolsRequestSchema;
10
10
  };
11
11
  export type SdkImporter = (specifier: string) => Promise<unknown>;
12
- export declare const MISSING_SDK_MESSAGE: string;
12
+ /**
13
+ * The directory the server was installed into: the parent of the outermost `node_modules` above
14
+ * `from`, which is where `pnpm add` must run so the peers resolve from this package. Outside any
15
+ * `node_modules` (a source checkout) it is the module's own directory.
16
+ */
17
+ export declare function installDirectory(from?: string | URL): string;
18
+ export declare function missingSdkMessage(directory: string): string;
13
19
  /**
14
20
  * Imports the SDK lazily. A static import would make every `vouchington` command fail on a
15
21
  * consumer that never installed the optional peer.
16
22
  */
17
- export declare function loadMcpSdk(importSdk?: SdkImporter): Promise<McpSdk>;
23
+ export declare function loadMcpSdk(importSdk?: SdkImporter, directory?: string): Promise<McpSdk>;
24
+ export declare function missingBlackboardMessage(directory: string): string;
25
+ /** Fails fast at startup when `agent-blackboard` cannot be resolved from this package's install. */
26
+ export declare function assertBlackboardInstalled(resolveFrom?: string | URL): void;
@@ -6,17 +6,34 @@ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExte
6
6
  }
7
7
  return path;
8
8
  };
9
+ import { createRequire } from 'node:module';
10
+ import { dirname, sep } from 'node:path';
11
+ import { fileURLToPath } from 'node:url';
9
12
  import { isMissingModuleError } from '../agent-blackboard/client.mjs';
10
13
  const SDK_PACKAGE = '@modelcontextprotocol/sdk';
11
- export const MISSING_SDK_MESSAGE = `vouchington mcp needs ${SDK_PACKAGE} (and its zod peer), which is an optional peer ` +
12
- `dependency of vouchington-tooling and is not installed. Install it next to ` +
13
- `vouchington-tooling: pnpm add -D ${SDK_PACKAGE} zod`;
14
+ /**
15
+ * The directory the server was installed into: the parent of the outermost `node_modules` above
16
+ * `from`, which is where `pnpm add` must run so the peers resolve from this package. Outside any
17
+ * `node_modules` (a source checkout) it is the module's own directory.
18
+ */
19
+ export function installDirectory(from = import.meta.url) {
20
+ const file = typeof from === 'string' && !from.startsWith('file:') ? from : fileURLToPath(from);
21
+ const marker = `${sep}node_modules${sep}`;
22
+ const index = file.indexOf(marker);
23
+ return index === -1 ? dirname(file) : file.slice(0, index);
24
+ }
25
+ export function missingSdkMessage(directory) {
26
+ return (`vouchington mcp needs ${SDK_PACKAGE} (and its zod peer), which is an optional peer ` +
27
+ `dependency of vouchington-tooling and is not installed. Install it in the directory ` +
28
+ `vouchington-tooling is installed in, not in a project: cd ${directory} && ` +
29
+ `pnpm add ${SDK_PACKAGE} zod`);
30
+ }
14
31
  const defaultImporter = (specifier) => import(__rewriteRelativeImportExtension(specifier));
15
32
  /**
16
33
  * Imports the SDK lazily. A static import would make every `vouchington` command fail on a
17
34
  * consumer that never installed the optional peer.
18
35
  */
19
- export async function loadMcpSdk(importSdk = defaultImporter) {
36
+ export async function loadMcpSdk(importSdk = defaultImporter, directory = installDirectory()) {
20
37
  try {
21
38
  const [server, stdio, types] = await Promise.all([
22
39
  importSdk(`${SDK_PACKAGE}/server/mcp.js`),
@@ -27,7 +44,22 @@ export async function loadMcpSdk(importSdk = defaultImporter) {
27
44
  }
28
45
  catch (error) {
29
46
  if (isMissingModuleError(error))
30
- throw new Error(MISSING_SDK_MESSAGE, { cause: error });
47
+ throw new Error(missingSdkMessage(directory), { cause: error });
31
48
  throw error;
32
49
  }
33
50
  }
51
+ export function missingBlackboardMessage(directory) {
52
+ return ('vouchington mcp needs agent-blackboard, which is an optional peer dependency of ' +
53
+ 'vouchington-tooling and is not installed. Install it in the directory vouchington-tooling ' +
54
+ `is installed in, not in a project: cd ${directory} && pnpm add agent-blackboard`);
55
+ }
56
+ /** Fails fast at startup when `agent-blackboard` cannot be resolved from this package's install. */
57
+ export function assertBlackboardInstalled(resolveFrom = import.meta.url) {
58
+ try {
59
+ createRequire(resolveFrom).resolve('agent-blackboard');
60
+ }
61
+ catch (error) {
62
+ // `resolve` only fails when the package cannot be found from `resolveFrom`.
63
+ throw new Error(missingBlackboardMessage(installDirectory(resolveFrom)), { cause: error });
64
+ }
65
+ }
@@ -2,8 +2,9 @@ import { callTool } from './dispatch.mjs';
2
2
  import { TOOLS } from './tools.mjs';
3
3
  export const SERVER_NAME = 'vouchington-tooling';
4
4
  const INSTRUCTIONS = 'Journals agent-blackboard feedback for a vouchington consumer. Every tool takes an explicit ' +
5
- 'sessionId and an optional worktree (an absolute path listed by `git worktree list` for the ' +
6
- 'repository this server was launched from). Hosts prefix the tool names differently: Claude ' +
5
+ 'sessionId and an optional worktree (the absolute path of a git worktree top level; it ' +
6
+ 'defaults to the launch directory worktree and is required when the server was launched ' +
7
+ 'outside one). Hosts prefix the tool names differently: Claude ' +
7
8
  'Code mcp__vouchington-tooling__<tool>, Codex mcp__vouchington_tooling__<tool>, Grok ' +
8
9
  'vouchington-tooling__<tool> (through search_tool and use_tool), Cursor the vouchington-tooling ' +
9
10
  'namespace (through GetDynamicTools and CallDynamicTool). Search for journal_append before ' +
@@ -7,8 +7,9 @@ const SESSION_ID = {
7
7
  };
8
8
  const WORKTREE = {
9
9
  type: 'string',
10
- description: 'Absolute path of a worktree listed by `git worktree list` for the repository the server ' +
11
- 'was launched from. Defaults to the launch worktree.',
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.',
12
13
  };
13
14
  const PARENT_SESSION_ID = {
14
15
  type: ['string', 'null'],
@@ -1,18 +1,25 @@
1
1
  import { type RunTextCommand } from '../gh-cli/exec.mts';
2
- /** Runs git without inherited `GIT_*` variables so `GIT_DIR` cannot redirect the check. */
2
+ /**
3
+ * The environment for git: no inherited `GIT_*` variables, so `GIT_DIR` cannot redirect the check,
4
+ * and the C locale, so git's diagnostics are not translated and can be classified reliably.
5
+ */
6
+ export declare function isolatedGitEnv(): NodeJS.ProcessEnv;
3
7
  export declare const runIsolatedGit: RunTextCommand;
4
- /** The worktree root containing `cwd`, or a message that says how to fix the launch directory. */
5
- export declare function launchWorktreeRoot(cwd: string, runGit: RunTextCommand): Promise<string>;
6
- /** Paths of the non-bare entries in `git worktree list --porcelain` output. */
7
- export declare function parseWorktreeList(porcelain: string): string[];
8
8
  /**
9
- * Resolves the worktree a tool call may act on. The list is read from git on every call, so
10
- * worktrees created after the server started are accepted and removed ones are not. The
11
- * comparison is exact on real paths: a subdirectory of a worktree, or an unrelated repository,
12
- * is rejected.
9
+ * The worktree root containing `cwd`, or undefined when git reports `cwd` is not inside one (so
10
+ * the server may be launched from anywhere). Any other git failure, such as a missing git binary
11
+ * or an unsafe repository, is thrown with the cause rather than reported as "outside a worktree".
12
+ */
13
+ export declare function launchWorktreeRoot(cwd: string, runGit: RunTextCommand): Promise<string | undefined>;
14
+ /**
15
+ * 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.
13
20
  */
14
21
  export declare function resolveWorktree(input: {
15
- launchRoot: string;
22
+ launchRoot: string | undefined;
16
23
  requested?: string | undefined;
17
24
  runGit: RunTextCommand;
18
25
  }): Promise<string>;
@@ -5,8 +5,15 @@ import { promisify } from 'node:util';
5
5
  import { createCommandRunner } from '../gh-cli/exec.mjs';
6
6
  import { gitEnv } from '../shared-context/index.mjs';
7
7
  const execFileAsync = promisify(execFile);
8
- /** Runs git without inherited `GIT_*` variables so `GIT_DIR` cannot redirect the check. */
9
- export const runIsolatedGit = createCommandRunner('git', (command, args) => execFileAsync(command, args, { env: gitEnv() }));
8
+ /**
9
+ * The environment for git: no inherited `GIT_*` variables, so `GIT_DIR` cannot redirect the check,
10
+ * and the C locale, so git's diagnostics are not translated and can be classified reliably.
11
+ */
12
+ export function isolatedGitEnv() {
13
+ const { LANGUAGE: _language, ...env } = gitEnv();
14
+ return { ...env, LC_ALL: 'C' };
15
+ }
16
+ export const runIsolatedGit = createCommandRunner('git', (command, args) => execFileAsync(command, args, { env: isolatedGitEnv() }));
10
17
  async function canonical(path) {
11
18
  try {
12
19
  return await realpath(path);
@@ -15,44 +22,54 @@ async function canonical(path) {
15
22
  return undefined;
16
23
  }
17
24
  }
18
- /** The worktree root containing `cwd`, or a message that says how to fix the launch directory. */
25
+ /**
26
+ * git's two "no worktree here" outcomes: outside any repository, and inside a `.git` directory.
27
+ * The messages are English because `isolatedGitEnv` forces the C locale.
28
+ */
29
+ function isNotAGitRepository(error) {
30
+ if (typeof error !== 'object' || error === null)
31
+ return false;
32
+ const { stderr, message } = error;
33
+ return [stderr, message].some((text) => typeof text === 'string' && /not a git repository|must be run in a work tree/i.test(text));
34
+ }
35
+ /**
36
+ * The worktree root containing `cwd`, or undefined when git reports `cwd` is not inside one (so
37
+ * the server may be launched from anywhere). Any other git failure, such as a missing git binary
38
+ * or an unsafe repository, is thrown with the cause rather than reported as "outside a worktree".
39
+ */
19
40
  export async function launchWorktreeRoot(cwd, runGit) {
41
+ let output;
20
42
  try {
21
- return await realpath((await runGit(['-C', cwd, 'rev-parse', '--show-toplevel'])).trim());
43
+ output = await runGit(['-C', cwd, 'rev-parse', '--show-toplevel']);
22
44
  }
23
45
  catch (error) {
24
- throw new Error(`vouchington mcp must be launched inside a git worktree; ${cwd} is not one (${errorText(error)})`);
46
+ if (isNotAGitRepository(error))
47
+ return undefined;
48
+ const reason = error instanceof Error ? error.message : String(error);
49
+ throw new Error(`git could not determine the worktree of ${cwd}: ${reason}. Check that git is installed and ` +
50
+ 'on PATH, that the directory exists, and that the repository is readable by this user ' +
51
+ '(see safe.directory).', { cause: error });
25
52
  }
26
- }
27
- /** Paths of the non-bare entries in `git worktree list --porcelain` output. */
28
- export function parseWorktreeList(porcelain) {
29
- const paths = [];
30
- for (const block of porcelain.split(/\r?\n\r?\n/)) {
31
- const lines = block.split(/\r?\n/);
32
- const first = lines.find((line) => line.startsWith('worktree '));
33
- if (first !== undefined && !lines.includes('bare'))
34
- paths.push(first.slice('worktree '.length));
35
- }
36
- return paths;
53
+ return realpath(output.trim());
37
54
  }
38
55
  /**
39
- * Resolves the worktree a tool call may act on. The list is read from git on every call, so
40
- * worktrees created after the server started are accepted and removed ones are not. The
41
- * comparison is exact on real paths: a subdirectory of a worktree, or an unrelated repository,
42
- * is rejected.
56
+ * 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.
43
61
  */
44
62
  export async function resolveWorktree(input) {
45
63
  const { launchRoot, requested, runGit } = input;
46
- if (requested !== undefined && !isAbsolute(requested))
47
- throw new Error('worktree must be an absolute path to a git worktree of the launch repository');
48
- const target = requested === undefined ? launchRoot : await canonical(requested);
49
- const listing = await runGit(['-C', launchRoot, 'worktree', 'list', '--porcelain']);
50
- const known = (await Promise.all(parseWorktreeList(listing).map(canonical))).filter((path) => path !== undefined);
51
- if (target !== undefined && known.includes(target))
64
+ if (requested === undefined && launchRoot === undefined)
65
+ throw new Error('worktree is required: this server was launched outside a git worktree, so pass ' +
66
+ 'worktree as the absolute path of the git worktree to act on');
67
+ const path = requested ?? launchRoot;
68
+ if (!isAbsolute(path))
69
+ throw new Error('worktree must be an absolute path to a git worktree');
70
+ const target = await canonical(path);
71
+ if (target !== undefined && (await launchWorktreeRoot(target, runGit)) === target)
52
72
  return target;
53
- throw new Error(`worktree ${requested ?? launchRoot} is not a worktree of the repository this server was ` +
54
- `launched from (${launchRoot}). Registered worktrees: ${known.join(', ')}`);
55
- }
56
- function errorText(error) {
57
- return error instanceof Error ? error.message : String(error);
73
+ throw new Error(`worktree ${path} is not the top level of a git worktree: it must exist and be the ` +
74
+ 'directory `git rev-parse --show-toplevel` reports, not a subdirectory of it');
58
75
  }
@@ -11,30 +11,68 @@ are later phases.
11
11
 
12
12
  ## Install
13
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:
14
+ Install the server once per machine, in its own dedicated directory, not in a project or a
15
+ worktree: `pnpm add` installs into the directory it runs in. The server needs these packages
16
+ installed next to `vouchington-tooling`. `agent-blackboard` and `@modelcontextprotocol/sdk` are
17
+ optional peer dependencies, so nothing else in the CLI requires them. Pin exact versions; replace
18
+ the placeholders with the current releases:
16
19
 
17
20
  ```bash
18
- pnpm add -D vouchington-tooling agent-blackboard@^0.6.0
19
- pnpm add -D @modelcontextprotocol/sdk zod
21
+ mkdir -p ~/.local/share/vouchington-mcp
22
+ cd ~/.local/share/vouchington-mcp
23
+ pnpm init
24
+ pnpm add --save-exact vouchington-tooling@<version> agent-blackboard@<version>
25
+ pnpm add --save-exact @modelcontextprotocol/sdk@<version> zod@<version>
20
26
  ```
21
27
 
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.
28
+ Launch the server by the absolute path into that directory, for example
29
+ `/abs/node ~/.local/share/vouchington-mcp/node_modules/vouchington-tooling/bin/vouchington-mcp.mjs`
30
+ with `~` expanded to an absolute path (see [Launch](#launch)).
31
+
32
+ `zod` is required by the SDK. Both `agent-blackboard` and the SDK are resolved from the server's own
33
+ install, relative to this package's module, never from the worktree a tool call names, so
34
+ repositories do not need to depend on `agent-blackboard`. When either is missing, `vouchington mcp`
35
+ writes a message naming the package and the install directory to add it in to stderr and exits with
36
+ status 1.
25
37
 
26
38
  The connection comes from `AGENT_BLACKBOARD_URL` and `AGENT_BLACKBOARD_TOKEN` in the server's
27
39
  environment. The server never searches for, prints, or mints them.
28
40
 
41
+ ## CLI fallback
42
+
43
+ If MCP is unavailable, use `vouchington agent-blackboard journal append|entries|flush|status` as described
44
+ in the blackboard skill. Record the fallback and delivery status in the journal and tell the human. The CLI resolves its
45
+ client from its own installation, so install `agent-blackboard` alongside that trusted CLI.
46
+ Run the script with a verified absolute `node` and a cleared environment (`env -i`), never through
47
+ its `#!/usr/bin/env node` line, which takes `node` from a possibly repository-controlled `PATH`.
48
+ A missing MCP connection does not waive persistence or readback requirements.
49
+
29
50
  ## Launch
30
51
 
31
52
  ```bash
32
53
  vouchington mcp
33
54
  ```
34
55
 
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.
56
+ The stable entry point, for a harness that launches the server without a shell or a `PATH`
57
+ lookup, is this package-relative file:
58
+
59
+ ```text
60
+ node_modules/vouchington-tooling/bin/vouchington-mcp.mjs
61
+ ```
62
+
63
+ Run it with an absolute Node, for example
64
+ `/abs/node /abs/machine/node_modules/vouchington-tooling/bin/vouchington-mcp.mjs`. It is also the
65
+ `vouchington-mcp` bin (`node_modules/.bin/vouchington-mcp`) and the `vouchington-tooling/mcp-server/bin`
66
+ export. It takes no arguments, skips CLI argument parsing, and is a hand-written plain-JavaScript
67
+ file that a CLI refactor does not move; a test pins the path. Both it and `bin/vouchington.mjs` check
68
+ the Node version first: on Node older than 24 they print one line to stderr, such as
69
+ `vouchington requires Node >=24 (found 18.19.0)`, and exit with status 1.
70
+
71
+ The command takes no arguments and may be launched with any working directory, including one outside
72
+ every git repository (for example `~`). When the working directory is inside a git worktree, that
73
+ worktree's top level is the default for tools called without `worktree`. Otherwise those calls fail
74
+ with a tool error asking for an explicit `worktree`. stdout carries only protocol frames, and
75
+ diagnostics go to stderr.
38
76
 
39
77
  ## Tools
40
78
 
@@ -113,42 +151,62 @@ instructions shows it to the agent.
113
151
 
114
152
  ## Worktree validation
115
153
 
116
- `worktree` must be an absolute path that, after resolving symlinks, exactly matches an entry of
117
- `git worktree list --porcelain` for the repository the server was launched from. The list is read on
118
- every call, so worktrees created after launch are accepted and removed ones are refused. Anything
119
- else is rejected: a subdirectory of a worktree, a relative or nonexistent path, and a separate
120
- repository, including one made with `git init` elsewhere. It defaults to the launch worktree's root.
121
- Git runs without `GIT_*` variables, so `GIT_DIR` cannot redirect the check.
154
+ `worktree` is checked on every call. It must be an absolute path that, after resolving symlinks,
155
+ equals the top level of a git worktree, that is, what `git rev-parse --show-toplevel` prints when
156
+ run in it. Any git worktree on the machine is accepted, including one of a repository unrelated to
157
+ the launch directory, so the tools can act on every checkout the user can reach. Worktrees created
158
+ after launch are accepted and removed ones are refused. Rejected: a subdirectory of a worktree, a
159
+ relative or nonexistent path, and a directory that is not in a git worktree. Git runs without
160
+ `GIT_*` variables, so `GIT_DIR` cannot redirect the check, and with `LC_ALL=C` and no `LANGUAGE`, so
161
+ its "not a git repository" diagnostic is never translated.
162
+
163
+ `worktree` only chooses where the outbox lives. It is not a trust boundary for loading code: the
164
+ server never imports modules from it, and a tool that needs to must first get a repository allowlist
165
+ from the machine registration.
166
+
167
+ When `worktree` is omitted, the launch directory's worktree top level is used if the launch
168
+ directory is inside one, and it is validated like any explicit path. If the server was launched
169
+ outside a git worktree, the call fails with a tool error asking for an explicit `worktree`.
122
170
 
123
171
  ## Registering the server
124
172
 
125
- The commands below run `vouchington` from the repository's own `node_modules`, so a fresh worktree
126
- uses the version its lockfile pins. Set the blackboard credentials in the environment the harness
127
- gives the server.
173
+ Register the machine-wide installation from [Install](#install) at user level, with absolute paths.
174
+ Never check a registration into a repository: it names a path and a Node that exist only on your
175
+ machine, and every worktree inherits it from your user config. Replace `/abs/node` with the absolute
176
+ path of a Node >= 24 and `/home/you` with your home directory (neither file expands `~`). Set the
177
+ blackboard credentials in the environment you start the harness from.
128
178
 
129
- Claude Code, `.mcp.json`:
179
+ Claude Code, `mcpServers` in `~/.claude.json`:
130
180
 
131
181
  ```json
132
182
  {
133
183
  "mcpServers": {
134
184
  "vouchington-tooling": {
135
- "command": "bash",
136
- "args": ["-c", "exec \"$(git rev-parse --show-toplevel)/node_modules/.bin/vouchington\" mcp"]
185
+ "command": "/abs/node",
186
+ "args": [
187
+ "/home/you/.local/share/vouchington-mcp/node_modules/vouchington-tooling/bin/vouchington-mcp.mjs"
188
+ ]
137
189
  }
138
190
  }
139
191
  }
140
192
  ```
141
193
 
142
- Codex, `.codex/config.toml`:
194
+ Codex, `~/.codex/config.toml`:
143
195
 
144
196
  ```toml
145
197
  [mcp_servers.vouchington-tooling]
146
- command = "bash"
147
- args = ["-c", "exec \"$(git rev-parse --show-toplevel)/node_modules/.bin/vouchington\" mcp"]
198
+ command = "/abs/node"
199
+ args = [
200
+ "/home/you/.local/share/vouchington-mcp/node_modules/vouchington-tooling/bin/vouchington-mcp.mjs",
201
+ ]
202
+ env_vars = ["AGENT_BLACKBOARD_URL", "AGENT_BLACKBOARD_TOKEN"]
148
203
  default_tools_approval_mode = "approve"
149
204
  required = false
150
205
  ```
151
206
 
207
+ `env_vars` forwards those variables from Codex's own environment. Codex does not expand `${VAR}`
208
+ inside `env`, so do not write the credentials there.
209
+
152
210
  Approving every tool of the server at once:
153
211
 
154
212
  - Claude Code permission rule: `mcp__vouchington-tooling__*`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vouchington-tooling",
3
- "version": "0.30.0",
3
+ "version": "0.32.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": {
@@ -14,10 +14,12 @@
14
14
  "directory": "packages/vouchington-tooling"
15
15
  },
16
16
  "bin": {
17
- "vouchington": "dist/cli/index.mjs"
17
+ "vouchington": "bin/vouchington.mjs",
18
+ "vouchington-mcp": "bin/vouchington-mcp.mjs"
18
19
  },
19
20
  "files": [
20
21
  "ast-grep",
22
+ "bin",
21
23
  "dist",
22
24
  "docs",
23
25
  "skills",
@@ -68,6 +70,7 @@
68
70
  "import": "./dist/agent-blackboard/index.mjs",
69
71
  "default": "./dist/agent-blackboard/index.mjs"
70
72
  },
73
+ "./mcp-server/bin": "./bin/vouchington-mcp.mjs",
71
74
  "./session-friction": {
72
75
  "types": "./dist/session-friction/index.d.mts",
73
76
  "import": "./dist/session-friction/index.mjs",
@@ -360,10 +363,14 @@
360
363
  },
361
364
  "peerDependencies": {
362
365
  "@modelcontextprotocol/sdk": "^1.31.0",
366
+ "agent-blackboard": "^0.6.0",
363
367
  "typescript": ">=5",
364
368
  "vitest": ">=5"
365
369
  },
366
370
  "peerDependenciesMeta": {
371
+ "agent-blackboard": {
372
+ "optional": true
373
+ },
367
374
  "@modelcontextprotocol/sdk": {
368
375
  "optional": true
369
376
  },
package/scripts/build.mjs CHANGED
@@ -34,3 +34,5 @@ await copyFile(
34
34
  new URL('../skills/manifest.json', import.meta.url),
35
35
  )
36
36
  await chmod(new URL('../dist/cli/index.mjs', import.meta.url), 0o755)
37
+ await chmod(new URL('../bin/vouchington.mjs', import.meta.url), 0o755)
38
+ await chmod(new URL('../bin/vouchington-mcp.mjs', import.meta.url), 0o755)
@@ -35,9 +35,71 @@ launch worktree by default). Writing or retrying an entry needs no provider skil
35
35
  `--file` flag, or replay command.
36
36
  Search for `journal_append` (by bare name, so every prefix matches) before concluding the server is
37
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.
38
+ because the server is not registered or not connected, use the supported CLI fallback below.
39
+ Tell the human that MCP is unavailable and which CLI is being used. Record the fallback reason,
40
+ entrypoint, affected operations, and observed delivery status in the caller's journal before
41
+ continuing primary work. Do not treat a permission denial as tool unavailability or bypass a host
42
+ approval requirement through the CLI. If neither path persists the note, stop and report the blocker.
43
+
44
+ ## CLI fallback
45
+
46
+ Use a trusted machine-installed `vouchington` executable at a verified absolute path outside the
47
+ consumer worktree. Resolve its symlinks and verify the installation's source and version before
48
+ using credentials or requesting an unsandboxed run; its runtime dependencies must also belong to
49
+ that trusted installation. The CLI resolves its client relative to its own installation; installing
50
+ `agent-blackboard` only in the consumer worktree is insufficient for CLI fallback. Do not resolve the executable through the consumer's `node_modules/.bin`,
51
+ `pnpm exec`, or repository-controlled `PATH`. Set `BLACKBOARD_CLI` below to that verified absolute
52
+ script path, and bind any approval request to the same interpreter, script, and arguments.
53
+ Never execute the script directly: its `#!/usr/bin/env node` line would pick whichever `node` comes
54
+ first on `PATH`, which a consumer worktree can control. Set `BLACKBOARD_NODE` to a verified
55
+ absolute Node 24+ binary outside the consumer worktree, and run it with `env -i` so only `HOME`, a
56
+ fixed system `PATH`, and the two blackboard variables reach it. That also drops `NODE_OPTIONS` and
57
+ other loader variables. If no trusted CLI is
58
+ available, report the blocker; do not escalate a consumer-supplied executable. For local development,
59
+ the reviewed source CLI may run inside the sandbox, subject to the harness's credential policy.
60
+ `journal append` invokes the same validated writer as MCP, ensures the exact session identity,
61
+ patches its repository union, and verifies delivery. Use a temporary `0600` markdown input file,
62
+ remove it after the call, and inspect the JSON result: exit zero can mean retained pending feedback.
63
+ The input file is transport to the supported writer, never a replacement journal. Append returns
64
+ `pendingCount` for the caller and `worktreePendingCount` for the whole outbox. Use `journal status`
65
+ or pass `--session-id` to `journal flush` for the same two counts; an unfiltered legacy flush reports
66
+ only the whole-outbox count.
67
+
68
+ ```bash
69
+ blackboard() {
70
+ env -i HOME="$HOME" PATH=/usr/bin:/bin \
71
+ AGENT_BLACKBOARD_URL="$AGENT_BLACKBOARD_URL" \
72
+ AGENT_BLACKBOARD_TOKEN="$AGENT_BLACKBOARD_TOKEN" \
73
+ "$BLACKBOARD_NODE" "$BLACKBOARD_CLI" agent-blackboard "$@"
74
+ }
75
+ blackboard journal append \
76
+ --session-id SESSION --agent AGENT --version VERSION \
77
+ --mode interactive --source-event-id EVENT --work-outcome unknown \
78
+ --repository owner/name --coverage-status not-assessed --dropped-count 0 \
79
+ --outbox-directory "$PWD/.local/blackboard-outbox" --file NOTE
80
+ blackboard journal entries --session-id SESSION
81
+ blackboard journal status \
82
+ --session-id SESSION --outbox-directory "$PWD/.local/blackboard-outbox"
83
+ blackboard journal flush \
84
+ --session-id SESSION --outbox-directory "$PWD/.local/blackboard-outbox"
85
+ ```
86
+
87
+ Pass `--parent-session-id` for a child, repeat `--repository` for additional repositories, and use
88
+ explicit coverage sources when assessed. Omit `--timestamp` so the writer owns time. Autonomous
89
+ mode omits `--outbox-directory` and still requires fresh online admission plus readback.
90
+ For session-only or snapshot operations not exposed by this CLI, use the upstream
91
+ `agent-blackboard` CLI under its skill's operation contract; preserve exact identities and tags.
92
+ A sandboxed fallback may only write the outbox: the machine's harness sandbox can withhold
93
+ `AGENT_BLACKBOARD_TOKEN`, so it cannot deliver or read back. If delivery or readback needs the
94
+ credentials and the sandbox withholds them, run that one command unsandboxed through the harness's
95
+ normal approval path for running one command outside the sandbox, with each such run approved
96
+ individually. Never read, print, copy, or export the token
97
+ to work around the sandbox. If approval is denied, leave the record pending in the outbox, report
98
+ `pendingCount`, and tell the human.
99
+ Return to MCP when available and journal recovery. Never silently downgrade envelope validation,
100
+ retention, or delivery checks because the transport changed.
101
+
102
+ ## Journaling policy
41
103
 
42
104
  1. Capture consequential observations before filing an issue: failed or recovered checks, denied or
43
105
  approved permissions, repeated fixes, scope changes, first-party tool behavior, and architectural
@@ -65,7 +127,7 @@ it (the consumer wrapper documents it). Do not fall back to a CLI command.
65
127
  `journal_append` reports it as pending. Report `pendingCount` (this session's unsent records),
66
128
  and `worktreePendingCount` when other sessions' records are also unsent, from `journal_append`
67
129
  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
130
+ primary work only after persistence succeeds. A full, corrupt, or unwritable outbox blocks
69
131
  capture; never evict or silently discard an unsent record.
70
132
  5. Autonomous runners require online `session_ensure`, a fresh admission `journal_append`, and
71
133
  readback before launching an attempt. Give each attempt's admission entry a `sourceEventId`