vouchington-tooling 0.24.1 → 0.26.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/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 +19 -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 +62 -0
  34. package/dist/mcp-server/fake-blackboard.test-helpers.d.mts +39 -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 +6 -0
  39. package/dist/mcp-server/handlers-journal.mjs +98 -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 +120 -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 +134 -0
  62. package/package.json +7 -2
  63. package/skills/blackboard/SKILL.md +59 -22
  64. package/skills/manifest.json +19 -19
  65. package/skills/retrospective-distill/SKILL.md +37 -13
@@ -0,0 +1,5 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { readPackageVersion } from '../package-version.mjs';
3
+ export function readInstalledVersion() {
4
+ return readPackageVersion(JSON.parse(readFileSync(new URL('../../package.json', import.meta.url), 'utf8')));
5
+ }
@@ -37,6 +37,9 @@ type ForwardedCommand = {
37
37
  } | {
38
38
  kind: 'agent-blackboard';
39
39
  args: string[];
40
+ } | {
41
+ kind: 'mcp';
42
+ args: string[];
40
43
  };
41
44
  export declare function parseForwardedCommand(command: string | undefined, args: string[]): ForwardedCommand | undefined;
42
45
  export {};
@@ -25,5 +25,7 @@ export function parseForwardedCommand(command, args) {
25
25
  return { kind: 'retrospective-facts', args };
26
26
  if (command === 'agent-blackboard')
27
27
  return { kind: 'agent-blackboard', args };
28
+ if (command === 'mcp')
29
+ return { kind: 'mcp', args };
28
30
  return undefined;
29
31
  }
@@ -66,6 +66,9 @@ export type ParsedCli = {
66
66
  } | {
67
67
  kind: 'agent-blackboard';
68
68
  args: string[];
69
+ } | {
70
+ kind: 'mcp';
71
+ args: string[];
69
72
  } | {
70
73
  kind: 'require-up-to-date';
71
74
  remote: string;
@@ -1,4 +1,4 @@
1
- export declare const USAGE = "Usage: vouchington <command> [options]\n\nCommands:\n runner-port-policy Print or validate a runner port policy\n with-host-lock Run a command under a host-wide lock\n agent-harness-config Apply classifier-auto + sandbox keys to agent harnesses\n gha-runtime-audit Audit successful GitHub Actions job runtimes\n require-up-to-date Require HEAD to include a fetched remote branch\n gitleaks-directory-scan Scan a directory with Gitleaks\n ast-grep-examples Run AST-grep rule examples\n ast-grep-pack Print shipped unconditional AST-grep pack paths as JSON\n gha-workspace-policy Check GitHub Actions workspace safety policy\n gha-output Write a collision-safe multiline GITHUB_OUTPUT record\n gha-needs-results Fail if required GitHub Actions job results failed\n download-with-diagnostics Download a URL and report HTTP status on failure\n download-optional-run-artifacts Download optional artifacts from the current run\n host-pressure-diagnostics Print a bounded host memory/OOM/PSI snapshot\n allocate-browser-safe-ports Allocate Fetch-safe localhost ports\n diagnose-port-collision Capture bounded localhost port diagnostics\n prepare-trivy-db Download the Trivy vulnerability database\n gha-artifacts-cleanup Delete classified GitHub Actions artifacts\n http-origin Validate an optional HTTP(S) origin\n vitest-blob-manifest Stamp a vitest-blob-manifest:v1 identity file\n vitest-report-attempt Write or read a Vitest report-attempt marker\n prepare-vitest-reports Validate and select Vitest report JSON files\n pnpm-install Install a pnpm workspace with retry and release-age fail-fast\n check-cache-size Measure a path and decide whether to save a GHA cache\n make-shard-matrix Emit a [1..N] GitHub Actions shard matrix\n load-runner-env Overlay a runner env file onto GITHUB_ENV with injection guards\n clean-workspace Reset a persistent-runner workspace with a fork-PR trust gate\n install-github-release Download a checksum-verified GitHub Release binary\n run-with-timeout Run a command with GNU timeout or a Perl fallback\n lint-links Two-pass lychee: internal links fail, external warn\n materialize-pr-context Dump PR title/body/files/diff/comments and #N crawl\n wait-for-apt-locks Wait until apt/dpkg lock files are free\n install-playwright-chromium-arm64 Install Playwright Chromium from browsers.json\n ghcr-package-retention Delete old GHCR package versions past KEEP_MIN\n harness-admission-lane Compute a GITHUB_RUN_ID admission lane for fleet fan-out\n harness-assert-gates Fail if any named HARNESS_*_ENABLED gate is enabled\n nuget-central-version Validate a Directory.Packages.props PackageVersion delta\n swift-semantic-equal Compare Swift sources ignoring comments and whitespace\n post-review Post one COMMENT review from a staged payload file\n stage-review-payload Validate a review payload file into a staging directory\n retrospective-transcript Format facts from Claude-compatible, Codex, or Grok transcripts\n link-skill Link one packaged skill into an explicit consumer directory\n retrospective-facts Gather immutable facts for a retrospective\n agent-blackboard Probe and journal an Agent Blackboard deployment\n\nOptions:\n -h, --help Show this help\n -v, --version Print the package version\n\nrunner-port-policy\n (no args) Print the shipped policy as JSON\n --file <path> Validate and print a policy file\n --reserved <port> Print true if the port is reserved\n\nwith-host-lock\n --name <family>\n [--slots <n>]\n --timeout-seconds <n>\n [--command-timeout-seconds <n>]\n [--failure-diagnostics <absolute-script>]\n [--on-acquire-timeout fail|run-unlocked]\n -- <command> [args...]\n\nagent-harness-config dump\nagent-harness-config check|apply [--global] [--repo PATH]... [--harness claude|codex|grok|cursor]...\n [--home PATH]\n\ngha-runtime-audit\n [--repository owner/name] Default GITHUB_REPOSITORY\n [--branch main]\n [--median-threshold-floor <seconds>] Off unless set. Must be below the ceiling\n [--median-threshold-ceiling <seconds>] Default 360. Flags a five-sample median above this\n --pr-workflow <name|/regex/> Repeatable\n --push-workflow <name|/regex/> Repeatable\n\nrequire-up-to-date --remote <name> --branch <name>\ngitleaks-directory-scan --config <path> [--directory <path>]\nast-grep-examples --rules <directory> --config <path>\nast-grep-pack\ngha-workspace-policy [--root <directory>] [--workflow-directory <directory>] [--action-directory <directory>]\n\ngha-output <name>\ngha-needs-results [label]\ndownload-with-diagnostics <url> <destination> [-- curl-args...]\ndownload-optional-run-artifacts (--name <name>... | --pattern <pattern>) --dir <directory>\nhost-pressure-diagnostics\nallocate-browser-safe-ports <count> [--policy path] [--forbidden-ports path]\ndiagnose-port-collision [--ports \"2200 2216\"] [--output-dir PATH]\nprepare-trivy-db\ngha-artifacts-cleanup run --run-id <id> [--keep-pattern glob] [--delete-pattern glob] [--patterns-file json]\ngha-artifacts-cleanup sweep --older-than-hours <n> [--keep-pattern glob] [--delete-pattern glob] [--patterns-file json]\nhttp-origin [--field NAME] [value]\nvitest-blob-manifest <suite> [reports-directory]\nvitest-report-attempt <write DIRECTORY SUITE|read ROOT>\nprepare-vitest-reports [primary-directory] [fallback-directory] [output-directory]\npnpm-install --runner-lifecycle persistent|ephemeral|ephemeral-full --install-scripts true|false\ncheck-cache-size <path> <max-bytes> <label>\nmake-shard-matrix <total>\nload-runner-env\nclean-workspace\ninstall-github-release --repo owner/name --version X --asset 'name-{platform}.tar.gz' --bin name [--tag-prefix PREFIX] [--expected-sha256 SHA256] [--no-checksum] [--checksums-asset NAME] [--version-flag FLAG] [--bin-dir DIR]\nrun-with-timeout <timeout-seconds> <kill-after-seconds> <command...>\nlint-links [--offline] [--config PATH] [--glob PATTERN] [files...]\nmaterialize-pr-context\nwait-for-apt-locks\ninstall-playwright-chromium-arm64 [name:archive...]\nghcr-package-retention <url-encoded-package>...\nharness-admission-lane <lanes>\nharness-assert-gates <gate>...\nnuget-central-version <trusted-props> <candidate-props> <metadata-json> <output-props>\nswift-semantic-equal <base> <head> <file.swift>\npost-review\nstage-review-payload optional|required <source> <destination>\nretrospective-transcript [--session-id ID] [--jsonl PATH] [--projects-dir PATH] [--codex-sessions-dir PATH] [--grok-sessions-dir PATH]\nlink-skill <name> --source-root <skills-dir> --target-root <consumer-skills-dir> Link a packaged or repository-local skill\nretrospective-facts (--pr NUMBER | --branch NAME | --no-pr) [--repo OWNER/NAME] [--raw]\nagent-blackboard probe\nagent-blackboard journal append --mode interactive|autonomous --source-event-id ID --work-outcome OUTCOME --coverage-status STATUS --session-id UUID --agent NAME --version VERSION --file PATH --repository OWNER/NAME [--repository OWNER/NAME ...] [--parent-session-id UUID] [--timestamp ISO8601] [--coverage-source SOURCE,...] [--dropped-count N] [--outbox-directory PATH]\nagent-blackboard journal entries --session-id UUID\nagent-blackboard journal flush --outbox-directory PATH\nInteractive journal append requires --outbox-directory PATH.\nagent-blackboard snapshot partition --snapshot PATH --checksum SHA256 --counts '{\"sessions\":N,\"entries\":N,\"records\":N,\"bytes\":N}'\nagent-blackboard snapshot cleanup [--snapshot PATH] [--partition-directory PATH --receipt JSON]\n";
1
+ export declare const USAGE = "Usage: vouchington <command> [options]\n\nCommands:\n runner-port-policy Print or validate a runner port policy\n with-host-lock Run a command under a host-wide lock\n agent-harness-config Apply classifier-auto + sandbox keys to agent harnesses\n gha-runtime-audit Audit successful GitHub Actions job runtimes\n require-up-to-date Require HEAD to include a fetched remote branch\n gitleaks-directory-scan Scan a directory with Gitleaks\n ast-grep-examples Run AST-grep rule examples\n ast-grep-pack Print shipped unconditional AST-grep pack paths as JSON\n gha-workspace-policy Check GitHub Actions workspace safety policy\n gha-output Write a collision-safe multiline GITHUB_OUTPUT record\n gha-needs-results Fail if required GitHub Actions job results failed\n download-with-diagnostics Download a URL and report HTTP status on failure\n download-optional-run-artifacts Download optional artifacts from the current run\n host-pressure-diagnostics Print a bounded host memory/OOM/PSI snapshot\n allocate-browser-safe-ports Allocate Fetch-safe localhost ports\n diagnose-port-collision Capture bounded localhost port diagnostics\n prepare-trivy-db Download the Trivy vulnerability database\n gha-artifacts-cleanup Delete classified GitHub Actions artifacts\n http-origin Validate an optional HTTP(S) origin\n vitest-blob-manifest Stamp a vitest-blob-manifest:v1 identity file\n vitest-report-attempt Write or read a Vitest report-attempt marker\n prepare-vitest-reports Validate and select Vitest report JSON files\n pnpm-install Install a pnpm workspace with retry and release-age fail-fast\n check-cache-size Measure a path and decide whether to save a GHA cache\n make-shard-matrix Emit a [1..N] GitHub Actions shard matrix\n load-runner-env Overlay a runner env file onto GITHUB_ENV with injection guards\n clean-workspace Reset a persistent-runner workspace with a fork-PR trust gate\n install-github-release Download a checksum-verified GitHub Release binary\n run-with-timeout Run a command with GNU timeout or a Perl fallback\n lint-links Two-pass lychee: internal links fail, external warn\n materialize-pr-context Dump PR title/body/files/diff/comments and #N crawl\n wait-for-apt-locks Wait until apt/dpkg lock files are free\n install-playwright-chromium-arm64 Install Playwright Chromium from browsers.json\n ghcr-package-retention Delete old GHCR package versions past KEEP_MIN\n harness-admission-lane Compute a GITHUB_RUN_ID admission lane for fleet fan-out\n harness-assert-gates Fail if any named HARNESS_*_ENABLED gate is enabled\n nuget-central-version Validate a Directory.Packages.props PackageVersion delta\n swift-semantic-equal Compare Swift sources ignoring comments and whitespace\n post-review Post one COMMENT review from a staged payload file\n stage-review-payload Validate a review payload file into a staging directory\n retrospective-transcript Format facts from Claude-compatible, Codex, or Grok transcripts\n link-skill Link one packaged skill into an explicit consumer directory\n retrospective-facts Gather immutable facts for a retrospective\n agent-blackboard Probe and journal an Agent Blackboard deployment\n mcp Serve the agent-blackboard journal tools over stdio (MCP)\n\nOptions:\n -h, --help Show this help\n -v, --version Print the package version\n\nrunner-port-policy\n (no args) Print the shipped policy as JSON\n --file <path> Validate and print a policy file\n --reserved <port> Print true if the port is reserved\n\nwith-host-lock\n --name <family>\n [--slots <n>]\n --timeout-seconds <n>\n [--command-timeout-seconds <n>]\n [--failure-diagnostics <absolute-script>]\n [--on-acquire-timeout fail|run-unlocked]\n -- <command> [args...]\n\nagent-harness-config dump\nagent-harness-config check|apply [--global] [--repo PATH]... [--harness claude|codex|grok|cursor]...\n [--home PATH]\n\ngha-runtime-audit\n [--repository owner/name] Default GITHUB_REPOSITORY\n [--branch main]\n [--median-threshold-floor <seconds>] Off unless set. Must be below the ceiling\n [--median-threshold-ceiling <seconds>] Default 360. Flags a five-sample median above this\n --pr-workflow <name|/regex/> Repeatable\n --push-workflow <name|/regex/> Repeatable\n\nrequire-up-to-date --remote <name> --branch <name>\ngitleaks-directory-scan --config <path> [--directory <path>]\nast-grep-examples --rules <directory> --config <path>\nast-grep-pack\ngha-workspace-policy [--root <directory>] [--workflow-directory <directory>] [--action-directory <directory>]\n\ngha-output <name>\ngha-needs-results [label]\ndownload-with-diagnostics <url> <destination> [-- curl-args...]\ndownload-optional-run-artifacts (--name <name>... | --pattern <pattern>) --dir <directory>\nhost-pressure-diagnostics\nallocate-browser-safe-ports <count> [--policy path] [--forbidden-ports path]\ndiagnose-port-collision [--ports \"2200 2216\"] [--output-dir PATH]\nprepare-trivy-db\ngha-artifacts-cleanup run --run-id <id> [--keep-pattern glob] [--delete-pattern glob] [--patterns-file json]\ngha-artifacts-cleanup sweep --older-than-hours <n> [--keep-pattern glob] [--delete-pattern glob] [--patterns-file json]\nhttp-origin [--field NAME] [value]\nvitest-blob-manifest <suite> [reports-directory]\nvitest-report-attempt <write DIRECTORY SUITE|read ROOT>\nprepare-vitest-reports [primary-directory] [fallback-directory] [output-directory]\npnpm-install --runner-lifecycle persistent|ephemeral|ephemeral-full --install-scripts true|false\ncheck-cache-size <path> <max-bytes> <label>\nmake-shard-matrix <total>\nload-runner-env\nclean-workspace\ninstall-github-release --repo owner/name --version X --asset 'name-{platform}.tar.gz' --bin name [--tag-prefix PREFIX] [--expected-sha256 SHA256] [--no-checksum] [--checksums-asset NAME] [--version-flag FLAG] [--bin-dir DIR]\nrun-with-timeout <timeout-seconds> <kill-after-seconds> <command...>\nlint-links [--offline] [--config PATH] [--glob PATTERN] [files...]\nmaterialize-pr-context\nwait-for-apt-locks\ninstall-playwright-chromium-arm64 [name:archive...]\nghcr-package-retention <url-encoded-package>...\nharness-admission-lane <lanes>\nharness-assert-gates <gate>...\nnuget-central-version <trusted-props> <candidate-props> <metadata-json> <output-props>\nswift-semantic-equal <base> <head> <file.swift>\npost-review\nstage-review-payload optional|required <source> <destination>\nretrospective-transcript [--session-id ID] [--jsonl PATH] [--projects-dir PATH] [--codex-sessions-dir PATH] [--grok-sessions-dir PATH]\nlink-skill <name> --source-root <skills-dir> --target-root <consumer-skills-dir> Link a packaged or repository-local skill\nretrospective-facts (--pr NUMBER | --branch NAME | --no-pr) [--repo OWNER/NAME] [--raw]\nagent-blackboard probe\nagent-blackboard journal append --mode interactive|autonomous --source-event-id ID --work-outcome OUTCOME --coverage-status STATUS --session-id UUID --agent NAME --version VERSION --file PATH --repository OWNER/NAME [--repository OWNER/NAME ...] [--parent-session-id UUID] [--timestamp ISO8601] [--coverage-source SOURCE,...] [--dropped-count N] [--outbox-directory PATH]\nagent-blackboard journal entries --session-id UUID\nagent-blackboard journal flush --outbox-directory PATH\nInteractive journal append requires --outbox-directory PATH.\nagent-blackboard snapshot partition --snapshot PATH --checksum SHA256 --counts '{\"sessions\":N,\"entries\":N,\"records\":N,\"bytes\":N}'\nagent-blackboard snapshot cleanup [--snapshot PATH] [--partition-directory PATH --receipt JSON]\nmcp\n (no args) Serve journal_append, journal_entries, outbox_status, outbox_flush,\n session_ensure, snapshot_export, and session_archive over stdio.\n Needs the optional @modelcontextprotocol/sdk peer (and zod).\n Launch it from inside the repository's git worktree.\n";
2
2
  export declare function printUsage(stream?: NodeJS.WritableStream): void;
3
3
  export declare function commandNames(usage?: string): string[];
4
4
  export declare function commandUsage(command: string, usage?: string): string;
@@ -45,6 +45,7 @@ Commands:
45
45
  link-skill Link one packaged skill into an explicit consumer directory
46
46
  retrospective-facts Gather immutable facts for a retrospective
47
47
  agent-blackboard Probe and journal an Agent Blackboard deployment
48
+ mcp Serve the agent-blackboard journal tools over stdio (MCP)
48
49
 
49
50
  Options:
50
51
  -h, --help Show this help
@@ -124,6 +125,11 @@ agent-blackboard journal flush --outbox-directory PATH
124
125
  Interactive journal append requires --outbox-directory PATH.
125
126
  agent-blackboard snapshot partition --snapshot PATH --checksum SHA256 --counts '{"sessions":N,"entries":N,"records":N,"bytes":N}'
126
127
  agent-blackboard snapshot cleanup [--snapshot PATH] [--partition-directory PATH --receipt JSON]
128
+ mcp
129
+ (no args) Serve journal_append, journal_entries, outbox_status, outbox_flush,
130
+ session_ensure, snapshot_export, and session_archive over stdio.
131
+ Needs the optional @modelcontextprotocol/sdk peer (and zod).
132
+ Launch it from inside the repository's git worktree.
127
133
  `;
128
134
  export function printUsage(stream = process.stdout) {
129
135
  stream.write(USAGE);
@@ -0,0 +1,9 @@
1
+ export type Args = Record<string, unknown>;
2
+ export declare function requireObject(value: unknown): Args;
3
+ export declare function rejectUnknown(args: Args, allowed: Iterable<string>): void;
4
+ export declare function requiredString(args: Args, name: string): string;
5
+ export declare function optionalString(args: Args, name: string): string | undefined;
6
+ export declare function requiredNullableString(args: Args, name: string): string | null;
7
+ export declare function requiredChoice<T extends string>(args: Args, name: string, choices: readonly T[]): T;
8
+ export declare function requiredStringArray(args: Args, name: string): string[];
9
+ export declare function optionalRecord(args: Args, name: string): Args | undefined;
@@ -0,0 +1,48 @@
1
+ export function requireObject(value) {
2
+ if (typeof value !== 'object' || value === null || Array.isArray(value))
3
+ throw new Error('tool arguments must be an object');
4
+ return value;
5
+ }
6
+ export function rejectUnknown(args, allowed) {
7
+ const known = new Set(allowed);
8
+ const extra = Object.keys(args).filter((key) => !known.has(key));
9
+ if (extra.length)
10
+ throw new Error(`unsupported argument(s): ${extra.join(', ')}`);
11
+ }
12
+ export function requiredString(args, name) {
13
+ const value = args[name];
14
+ if (typeof value !== 'string' || !value.trim())
15
+ throw new Error(`${name} is required and must be a non-empty string`);
16
+ return value;
17
+ }
18
+ export function optionalString(args, name) {
19
+ return args[name] === undefined ? undefined : requiredString(args, name);
20
+ }
21
+ export function requiredNullableString(args, name) {
22
+ if (args[name] === null)
23
+ return null;
24
+ const value = args[name];
25
+ if (typeof value !== 'string' || !value.trim())
26
+ throw new Error(`${name} is required: a non-empty string, or null for none`);
27
+ return value;
28
+ }
29
+ export function requiredChoice(args, name, choices) {
30
+ const value = args[name];
31
+ if (typeof value !== 'string' || !choices.includes(value))
32
+ throw new Error(`${name} is required and must be one of: ${choices.join(', ')}`);
33
+ return value;
34
+ }
35
+ export function requiredStringArray(args, name) {
36
+ const value = args[name];
37
+ if (!Array.isArray(value) || !value.length || value.some((item) => typeof item !== 'string'))
38
+ throw new Error(`${name} is required and must be a non-empty array of strings`);
39
+ return value;
40
+ }
41
+ export function optionalRecord(args, name) {
42
+ const value = args[name];
43
+ if (value === undefined)
44
+ return undefined;
45
+ if (typeof value !== 'object' || value === null || Array.isArray(value))
46
+ throw new Error(`${name} must be an object`);
47
+ return value;
48
+ }
@@ -0,0 +1,19 @@
1
+ import type { BlackboardClientDependencies } from '../agent-blackboard/client.mts';
2
+ import type { FeedbackIdentity } from '../agent-blackboard/feedback-types.mts';
3
+ import { type Args } from './args.mts';
4
+ /** What a handler may rely on: every field here was validated before the handler ran. */
5
+ type ToolContext = {
6
+ sessionId: string;
7
+ worktree: string;
8
+ env: NodeJS.ProcessEnv;
9
+ dependencies: BlackboardClientDependencies;
10
+ };
11
+ export type ToolHandler = (args: Args, context: ToolContext) => Promise<Record<string, unknown>>;
12
+ /**
13
+ * The consumer convention for the interactive outbox. It is derived, never accepted from the
14
+ * caller, so a credentialed, unsandboxed server cannot be pointed at an arbitrary directory.
15
+ */
16
+ export declare function outboxDirectory(worktree: string): string;
17
+ export declare function readSessionId(args: Args): string;
18
+ export declare function readIdentity(args: Args, sessionId: string): FeedbackIdentity;
19
+ export {};
@@ -0,0 +1,23 @@
1
+ import { join } from 'node:path';
2
+ import { assertSessionId } from '../agent-blackboard/session-id.mjs';
3
+ import { requiredNullableString, requiredString } from './args.mjs';
4
+ /**
5
+ * The consumer convention for the interactive outbox. It is derived, never accepted from the
6
+ * caller, so a credentialed, unsandboxed server cannot be pointed at an arbitrary directory.
7
+ */
8
+ export function outboxDirectory(worktree) {
9
+ return join(worktree, '.local', 'blackboard-outbox');
10
+ }
11
+ export function readSessionId(args) {
12
+ const value = requiredString(args, 'sessionId');
13
+ assertSessionId(value);
14
+ return value;
15
+ }
16
+ export function readIdentity(args, sessionId) {
17
+ return {
18
+ sessionId,
19
+ parentSessionId: requiredNullableString(args, 'parentSessionId'),
20
+ agent: requiredString(args, 'agent'),
21
+ version: requiredString(args, 'version'),
22
+ };
23
+ }
@@ -0,0 +1,18 @@
1
+ import type { BlackboardClientDependencies } from '../agent-blackboard/client.mts';
2
+ import type { RunTextCommand } from '../gh-cli/exec.mts';
3
+ export type ServerEnvironment = {
4
+ launchRoot: string;
5
+ env: NodeJS.ProcessEnv;
6
+ runGit: RunTextCommand;
7
+ /** Test seam: replaces how the consumer's `agent-blackboard` client is loaded. */
8
+ blackboard?: BlackboardClientDependencies;
9
+ };
10
+ export type ToolCallResult = {
11
+ content: Array<{
12
+ type: 'text';
13
+ text: string;
14
+ }>;
15
+ isError?: true;
16
+ };
17
+ /** Runs one tool call. Every failure becomes an MCP tool error; nothing here throws. */
18
+ export declare function callTool(name: string, rawArguments: unknown, environment: ServerEnvironment): Promise<ToolCallResult>;
@@ -0,0 +1,62 @@
1
+ import { join } from 'node:path';
2
+ import { FeedbackDeliveryError } from '../agent-blackboard/index.mjs';
3
+ import { optionalString, rejectUnknown, requireObject } from './args.mjs';
4
+ import { readSessionId } from './context.mjs';
5
+ import { journalAppend, journalEntries, outboxFlush, outboxStatus } from './handlers-journal.mjs';
6
+ import { sessionArchive, sessionEnsure, snapshotExport } from './handlers-session.mjs';
7
+ import { TOOLS } from './tools.mjs';
8
+ import { resolveWorktree } from './worktree.mjs';
9
+ const HANDLERS = {
10
+ journal_append: journalAppend,
11
+ journal_entries: journalEntries,
12
+ outbox_status: outboxStatus,
13
+ outbox_flush: outboxFlush,
14
+ session_ensure: sessionEnsure,
15
+ snapshot_export: snapshotExport,
16
+ session_archive: sessionArchive,
17
+ };
18
+ const DIAGNOSTIC_HINTS = {
19
+ 'identity-conflict': 'the session already exists with a different parent, agent, or version',
20
+ 'event-conflict': 'sourceEventId was already used with different content or timestamp; use a new sourceEventId',
21
+ 'archived-session': 'the session is archived; write to a new session',
22
+ 'authentication-rejected': 'the blackboard rejected AGENT_BLACKBOARD_TOKEN',
23
+ 'blackboard-unavailable': 'the blackboard could not be reached or returned an error',
24
+ 'configuration-invalid': 'AGENT_BLACKBOARD_URL or AGENT_BLACKBOARD_TOKEN is missing or invalid',
25
+ 'client-unavailable': 'agent-blackboard is not installed in the worktree',
26
+ 'readback-unconfirmed': 'the write could not be confirmed; retry with the same sourceEventId and timestamp',
27
+ 'delivery-timeout': 'delivery timed out; retry with the same sourceEventId and timestamp',
28
+ };
29
+ function describeError(error) {
30
+ if (error instanceof FeedbackDeliveryError)
31
+ return `${error.message} (${DIAGNOSTIC_HINTS[error.diagnostic]})`;
32
+ return error instanceof Error ? error.message : String(error);
33
+ }
34
+ /** Runs one tool call. Every failure becomes an MCP tool error; nothing here throws. */
35
+ export async function callTool(name, rawArguments, environment) {
36
+ try {
37
+ const definition = TOOLS.find((candidate) => candidate.name === name);
38
+ const handler = HANDLERS[name];
39
+ if (!definition || !handler)
40
+ throw new Error(`unknown tool ${name}; available: ${TOOLS.map((tool) => tool.name).join(', ')}`);
41
+ const args = requireObject(rawArguments ?? {});
42
+ rejectUnknown(args, Object.keys(definition.inputSchema.properties));
43
+ // Everything below this line may touch disk or the network, so identity comes first.
44
+ const sessionId = readSessionId(args);
45
+ const worktree = await resolveWorktree({
46
+ launchRoot: environment.launchRoot,
47
+ requested: optionalString(args, 'worktree'),
48
+ runGit: environment.runGit,
49
+ });
50
+ const result = await handler(args, {
51
+ sessionId,
52
+ worktree,
53
+ env: environment.env,
54
+ // The client resolves from the validated worktree, never from the server's own location.
55
+ dependencies: { ...environment.blackboard, resolveFrom: join(worktree, 'package.json') },
56
+ });
57
+ return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
58
+ }
59
+ catch (error) {
60
+ return { isError: true, content: [{ type: 'text', text: describeError(error) }] };
61
+ }
62
+ }
@@ -0,0 +1,39 @@
1
+ import type { BlackboardClientDependencies } from '../agent-blackboard/client.mts';
2
+ export declare const BLACKBOARD_ENV: {
3
+ AGENT_BLACKBOARD_URL: string;
4
+ AGENT_BLACKBOARD_TOKEN: string;
5
+ };
6
+ type StoredEntry = {
7
+ sessionId?: string;
8
+ createdAt: string;
9
+ data: unknown;
10
+ };
11
+ export type FakeOptions = {
12
+ archived?: boolean;
13
+ omitArchive?: boolean;
14
+ omitSnapshots?: boolean;
15
+ exportResult?: unknown;
16
+ /** Makes `ensure` fail and `get` report a session with this different identity. */
17
+ conflictingSession?: {
18
+ id: string;
19
+ parentSessionId: null;
20
+ agent: string;
21
+ version: string;
22
+ };
23
+ entries?: StoredEntry[];
24
+ entriesError?: unknown;
25
+ };
26
+ export type FakeBlackboard = {
27
+ dependencies: BlackboardClientDependencies;
28
+ calls: {
29
+ ensure: unknown[];
30
+ patch: unknown[];
31
+ append: unknown[];
32
+ archive: string[];
33
+ export: unknown[];
34
+ };
35
+ stored: StoredEntry[];
36
+ };
37
+ /** An in-memory blackboard client faked at the module boundary `loadClient` provides. */
38
+ export declare function fakeBlackboard(options?: FakeOptions): FakeBlackboard;
39
+ export {};
@@ -0,0 +1,75 @@
1
+ export const BLACKBOARD_ENV = {
2
+ AGENT_BLACKBOARD_URL: 'https://provider.test',
3
+ AGENT_BLACKBOARD_TOKEN: 'test-private-token',
4
+ };
5
+ const SNAPSHOT = {
6
+ path: '/private/snapshots/export.jsonl',
7
+ counts: { sessions: 1, entries: 2, records: 3, bytes: 4 },
8
+ checksum: { algorithm: 'sha256', value: 'abc' },
9
+ manifest: { schemaVersion: 1 },
10
+ cleanupToken: 'token',
11
+ };
12
+ /** An in-memory blackboard client faked at the module boundary `loadClient` provides. */
13
+ export function fakeBlackboard(options = {}) {
14
+ const calls = {
15
+ ensure: [],
16
+ patch: [],
17
+ append: [],
18
+ archive: [],
19
+ export: [],
20
+ };
21
+ const stored = options.entries ?? [];
22
+ const data = { repositories: [] };
23
+ class Sessions {
24
+ async ensure(input) {
25
+ calls.ensure.push(input);
26
+ if (options.conflictingSession)
27
+ throw new Error('session identity differs');
28
+ return {
29
+ status: 'created',
30
+ session: { data, archivedAt: options.archived ? '2026-01-01T00:00:00.000Z' : null },
31
+ };
32
+ }
33
+ async patch(input) {
34
+ calls.patch.push(input);
35
+ data.repositories = input.data.repositories;
36
+ }
37
+ async list() { }
38
+ async get() {
39
+ return options.conflictingSession;
40
+ }
41
+ async archive(sessionId) {
42
+ calls.archive.push(sessionId);
43
+ }
44
+ }
45
+ if (options.omitArchive)
46
+ delete Sessions.prototype.archive;
47
+ class Entries {
48
+ async append(input) {
49
+ calls.append.push(input);
50
+ const entry = {
51
+ createdAt: `2026-01-01T00:00:${String(stored.length).padStart(2, '0')}.000Z`,
52
+ data: input.data,
53
+ };
54
+ stored.push(entry);
55
+ return entry;
56
+ }
57
+ async *get() {
58
+ if (options.entriesError !== undefined)
59
+ throw options.entriesError;
60
+ yield* stored;
61
+ }
62
+ }
63
+ class Snapshots {
64
+ async export(input) {
65
+ calls.export.push(input);
66
+ return 'exportResult' in options ? options.exportResult : SNAPSHOT;
67
+ }
68
+ }
69
+ const module = {
70
+ Sessions,
71
+ Entries,
72
+ ...(options.omitSnapshots ? {} : { Snapshots }),
73
+ };
74
+ return { dependencies: { loadClient: async () => module }, calls, stored };
75
+ }
@@ -0,0 +1,16 @@
1
+ export type RepoFixture = {
2
+ /** Real path of the directory holding every repository below. */
3
+ root: string;
4
+ /** The primary worktree of the launch repository. */
5
+ main: string;
6
+ /** A linked worktree of the launch repository. */
7
+ linked: string;
8
+ /** A separate repository created with `git init`. */
9
+ outside: string;
10
+ /** A symlink that points at `linked`. */
11
+ alias: string;
12
+ git: (cwd: string, ...args: string[]) => string;
13
+ cleanup: () => void;
14
+ };
15
+ /** Two real repositories under the OS tmpdir: one with a linked worktree, one unrelated. */
16
+ export declare function createRepoFixture(): RepoFixture;
@@ -0,0 +1,35 @@
1
+ import { execFileSync } from 'node:child_process';
2
+ import { mkdirSync, mkdtempSync, realpathSync, rmSync, symlinkSync } from 'node:fs';
3
+ import { tmpdir } from 'node:os';
4
+ import { join } from 'node:path';
5
+ import { gitEnv } from '../shared-context/index.mjs';
6
+ const IDENTITY = ['-c', 'user.name=Test', '-c', 'user.email=test@example.test'];
7
+ /** Two real repositories under the OS tmpdir: one with a linked worktree, one unrelated. */
8
+ export function createRepoFixture() {
9
+ const root = realpathSync(mkdtempSync(join(tmpdir(), 'vouchington-mcp-')));
10
+ const git = (cwd, ...args) => execFileSync('git', [...IDENTITY, '-C', cwd, ...args], {
11
+ encoding: 'utf8',
12
+ env: gitEnv(),
13
+ stdio: ['ignore', 'pipe', 'pipe'],
14
+ });
15
+ const main = join(root, 'main');
16
+ const linked = join(root, 'linked');
17
+ const outside = join(root, 'outside');
18
+ const alias = join(root, 'alias');
19
+ mkdirSync(main);
20
+ git(main, 'init', '--quiet');
21
+ git(main, 'commit', '--quiet', '--allow-empty', '-m', 'initial');
22
+ git(main, 'worktree', 'add', '--quiet', '-b', 'linked', linked);
23
+ mkdirSync(outside);
24
+ git(outside, 'init', '--quiet');
25
+ symlinkSync(linked, alias);
26
+ return {
27
+ root,
28
+ main,
29
+ linked,
30
+ outside,
31
+ alias,
32
+ git,
33
+ cleanup: () => rmSync(root, { recursive: true, force: true }),
34
+ };
35
+ }
@@ -0,0 +1,6 @@
1
+ import { type ToolHandler } from './context.mts';
2
+ export declare const journalAppend: ToolHandler;
3
+ /** Every entry as the client returns it, oldest first. Nothing is filtered or rewritten. */
4
+ export declare const journalEntries: ToolHandler;
5
+ export declare const outboxStatus: ToolHandler;
6
+ export declare const outboxFlush: ToolHandler;
@@ -0,0 +1,98 @@
1
+ import { appendJournalMarkdown, feedbackOutboxStatus, flushFeedbackOutbox, readJournal, } from '../agent-blackboard/index.mjs';
2
+ import { optionalRecord, optionalString, requiredChoice, requiredString, requiredStringArray, rejectUnknown, } from './args.mjs';
3
+ import { outboxDirectory, readIdentity } from './context.mjs';
4
+ const MODES = ['interactive', 'autonomous'];
5
+ const OUTCOMES = [
6
+ 'in-progress',
7
+ 'success',
8
+ 'failure',
9
+ 'cancelled',
10
+ 'timed-out',
11
+ 'no-change',
12
+ 'policy-refusal',
13
+ 'unknown',
14
+ ];
15
+ const COVERAGE_STATUSES = [
16
+ 'complete',
17
+ 'partial',
18
+ 'unavailable',
19
+ 'not-assessed',
20
+ 'not-started',
21
+ ];
22
+ function readCoverage(args) {
23
+ const coverage = optionalRecord(args, 'feedbackCoverage');
24
+ if (coverage === undefined)
25
+ throw new Error('feedbackCoverage is required');
26
+ rejectUnknown(coverage, ['status', 'sources', 'droppedCount']);
27
+ const droppedCount = coverage.droppedCount ?? 0;
28
+ if (typeof droppedCount !== 'number' || !Number.isSafeInteger(droppedCount) || droppedCount < 0)
29
+ throw new Error('feedbackCoverage.droppedCount must be a non-negative integer');
30
+ const sources = coverage.sources ?? [];
31
+ if (!Array.isArray(sources) || sources.some((source) => typeof source !== 'string'))
32
+ throw new Error('feedbackCoverage.sources must be an array of strings');
33
+ return {
34
+ status: requiredChoice(coverage, 'status', COVERAGE_STATUSES),
35
+ sources: sources,
36
+ droppedCount,
37
+ };
38
+ }
39
+ export const journalAppend = async (args, context) => {
40
+ const identity = readIdentity(args, context.sessionId);
41
+ const mode = requiredChoice(args, 'mode', MODES);
42
+ // The envelope includes the timestamp, so a retry of one sourceEventId must reuse it. Fixing the
43
+ // default here and returning it lets the caller do that.
44
+ const timestamp = optionalString(args, 'timestamp') ?? new Date().toISOString();
45
+ const category = optionalString(args, 'category');
46
+ const result = await appendJournalMarkdown({
47
+ ...identity,
48
+ mode,
49
+ // Interactive delivery needs a durable outbox; autonomous delivery forbids one.
50
+ ...(mode === 'interactive' ? { outboxDirectory: outboxDirectory(context.worktree) } : {}),
51
+ markdown: requiredString(args, 'markdown'),
52
+ sourceEventId: requiredString(args, 'sourceEventId'),
53
+ workOutcome: requiredChoice(args, 'workOutcome', OUTCOMES),
54
+ repositories: requiredStringArray(args, 'repositories'),
55
+ feedbackCoverage: readCoverage(args),
56
+ timestamp,
57
+ ...(category === undefined ? {} : { category }),
58
+ env: context.env,
59
+ dependencies: context.dependencies,
60
+ });
61
+ return { sessionId: context.sessionId, timestamp, ...result };
62
+ };
63
+ // `Entries.get` documents no order, and `createdAt` is a service-generated ISO 8601 UTC time whose
64
+ // text order is time order. An entry without a usable `createdAt` sorts first rather than throwing.
65
+ function createdAtOf(entry) {
66
+ const createdAt = entry?.createdAt;
67
+ return typeof createdAt === 'string' ? createdAt : '';
68
+ }
69
+ function byCreatedAt(left, right) {
70
+ const leftAt = createdAtOf(left);
71
+ const rightAt = createdAtOf(right);
72
+ if (leftAt === rightAt)
73
+ return 0;
74
+ return leftAt < rightAt ? -1 : 1;
75
+ }
76
+ /** Every entry as the client returns it, oldest first. Nothing is filtered or rewritten. */
77
+ export const journalEntries = async (_args, context) => ({
78
+ sessionId: context.sessionId,
79
+ entries: (await readJournal(context.sessionId, context.env, context.dependencies)).toSorted(byCreatedAt),
80
+ });
81
+ export const outboxStatus = async (_args, context) => ({
82
+ sessionId: context.sessionId,
83
+ ...feedbackOutboxStatus(outboxDirectory(context.worktree)),
84
+ });
85
+ export const outboxFlush = async (_args, context) => {
86
+ const directory = outboxDirectory(context.worktree);
87
+ // Flushing creates the outbox directory, so an empty or absent outbox is reported as is.
88
+ if (feedbackOutboxStatus(directory).pendingCount === 0)
89
+ return { sessionId: context.sessionId, status: 'empty', pendingCount: 0, deliveredCount: 0 };
90
+ return {
91
+ sessionId: context.sessionId,
92
+ ...(await flushFeedbackOutbox({
93
+ directory,
94
+ env: context.env,
95
+ dependencies: context.dependencies,
96
+ })),
97
+ };
98
+ };
@@ -0,0 +1,4 @@
1
+ import { type ToolHandler } from './context.mts';
2
+ export declare const sessionEnsure: ToolHandler;
3
+ export declare const sessionArchive: ToolHandler;
4
+ export declare const snapshotExport: ToolHandler;
@@ -0,0 +1,47 @@
1
+ import { archiveBlackboardSession, ensureBlackboardSession, exportSnapshot, } from '../agent-blackboard/index.mjs';
2
+ import { optionalRecord, optionalString, requiredNullableString } from './args.mjs';
3
+ import { readIdentity } from './context.mjs';
4
+ function readSelection(args) {
5
+ const data = optionalRecord(args, 'data');
6
+ const arrays = optionalRecord(args, 'dataArrayContains');
7
+ const inactiveForHours = args.inactiveForHours;
8
+ if (inactiveForHours !== undefined &&
9
+ (typeof inactiveForHours !== 'number' || !(inactiveForHours > 0)))
10
+ throw new Error('inactiveForHours must be a positive number');
11
+ if (arrays !== undefined && Object.values(arrays).some((value) => typeof value !== 'string'))
12
+ throw new Error('dataArrayContains must contain only string values');
13
+ const agent = optionalString(args, 'agent');
14
+ const version = optionalString(args, 'version');
15
+ return {
16
+ ...(agent === undefined ? {} : { agent }),
17
+ ...(version === undefined ? {} : { version }),
18
+ ...(args.parentSessionId === undefined
19
+ ? {}
20
+ : { parentSessionId: requiredNullableString(args, 'parentSessionId') }),
21
+ ...(data === undefined ? {} : { data }),
22
+ ...(arrays === undefined ? {} : { dataArrayContains: arrays }),
23
+ ...(inactiveForHours === undefined ? {} : { inactiveForHours }),
24
+ };
25
+ }
26
+ export const sessionEnsure = async (args, context) => ensureBlackboardSession({
27
+ ...readIdentity(args, context.sessionId),
28
+ env: context.env,
29
+ dependencies: context.dependencies,
30
+ });
31
+ export const sessionArchive = async (_args, context) => archiveBlackboardSession({
32
+ sessionId: context.sessionId,
33
+ env: context.env,
34
+ dependencies: context.dependencies,
35
+ });
36
+ export const snapshotExport = async (args, context) => {
37
+ const selection = readSelection(args);
38
+ return {
39
+ sessionId: context.sessionId,
40
+ ...(await exportSnapshot({
41
+ // An empty selection means "everything"; leave it unset rather than send an empty filter.
42
+ ...(Object.keys(selection).length === 0 ? {} : { selection }),
43
+ env: context.env,
44
+ dependencies: context.dependencies,
45
+ })),
46
+ };
47
+ };
@@ -0,0 +1,33 @@
1
+ import { type ToolCallResult } from './dispatch.mts';
2
+ import { type FakeBlackboard, type FakeOptions } from './fake-blackboard.test-helpers.mts';
3
+ import { type RepoFixture } from './git-fixture.test-helpers.mts';
4
+ /** A fresh pair of real repositories for every test. */
5
+ export declare function useRepoFixture(): () => RepoFixture;
6
+ export declare function outboxPath(worktree: string): string;
7
+ export type Harness = {
8
+ fake: FakeBlackboard;
9
+ call: (name: string, args?: unknown) => Promise<ToolCallResult>;
10
+ };
11
+ /** Calls tools against a repository fixture with the blackboard client faked. */
12
+ export declare function harness(fixture: RepoFixture, options?: FakeOptions & {
13
+ env?: NodeJS.ProcessEnv;
14
+ }): Harness;
15
+ export declare function textOf(result: ToolCallResult): string;
16
+ export declare function jsonOf(result: ToolCallResult): Record<string, unknown>;
17
+ export declare const JOURNAL_ARGS: {
18
+ sessionId: string;
19
+ parentSessionId: null;
20
+ agent: string;
21
+ version: string;
22
+ mode: string;
23
+ markdown: string;
24
+ sourceEventId: string;
25
+ workOutcome: string;
26
+ repositories: string[];
27
+ feedbackCoverage: {
28
+ status: string;
29
+ sources: string[];
30
+ droppedCount: number;
31
+ };
32
+ timestamp: string;
33
+ };