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
package/README.md CHANGED
@@ -455,6 +455,8 @@ Version one recognizes literal-key `apiResponse`, `apiNoContent`, `apiOpenApiRaw
455
455
  `ctx.pipeline`, `ctx.response.empty/buffer/xml`, `ctx.request.json/buffer`, `parseJsonBody`,
456
456
  `ctx.setStatus`, and static `Content-Type` setters. The input source files and TypeScript checker
457
457
  are the source representation; the adapter does not parse or discover a repository on its own.
458
+ Reading the handler context in another object's computed assignment key preserves its binding;
459
+ assignments or updates to the context itself, including side effects inside that key, still reject it.
458
460
 
459
461
  Error-only 405 routes require a terminal throw on the handler's own context. Named handlers and
460
462
  imported wrappers are followed through compiler declarations; conditional, caught, recursive,
@@ -506,6 +508,8 @@ A status setter must dominate each body emission, including separate branches; c
506
508
  aliases are followed, assigned mutable context aliases and object-literal context wrappers fail closed,
507
509
  and helper parameters must resolve to the registered caller's unchanged response context.
508
510
  Helper-owned contracts also account for response emissions in their proven caller scopes.
511
+ Feasible assignments or deletions of response methods through canonical context aliases invalidate
512
+ that dispatch evidence; dead, ignored, or unrelated method mutations preserve the contract.
509
513
  Indirect response `call`, `apply`, and `bind` forms fail closed, including borrowed context receivers.
510
514
  SSE frames and branded content variants reject body-forbidden 1xx, 204, 205, and 304 statuses.
511
515
  Literal bracket response methods have the same emission checks as property methods. Unrelated raw emissions
@@ -592,9 +596,11 @@ for minimizing summaries and excluding raw logs, transcripts, environment dumps,
592
596
  secrets. A bound violation fails visibly without clipping findings.
593
597
 
594
598
  `writeFeedback({identity, envelope, mode, ...})` accepts an exact caller-owned session identity and
595
- never manufactures native agent identities. Interactive mode requires an absolute private outbox
596
- directory. The writer persists before transport with atomic rename, file/directory fsync, a stable
597
- record path, and owner-only permissions. The outbox holds at most 128 records and 2 MB. Saturation,
599
+ never manufactures native agent identities. Interactive mode requires an absolute outbox
600
+ directory. Ancestor ownership and permissions are not checked; existing directory and record modes
601
+ are accepted. New directories and files default to `0700` and `0600`. The outbox itself must be a
602
+ real directory, and records must be regular files. The writer persists before transport with atomic
603
+ rename, file/directory fsync, and a stable record path. The outbox holds at most 128 records and 2 MB. Saturation,
598
604
  corruption, or persistence failure blocks capture and preserves unsent records. Delivered records
599
605
  are removed only after matching readback. Pending state includes count and a safe diagnostic;
600
606
  interactive CLI exit zero means durable retention, and callers must inspect the printed status to
@@ -0,0 +1,9 @@
1
+ // Plain JavaScript on purpose: the bin shims import this first, and it must parse and run on old
2
+ // Node releases so the version check can report a clear error before any TypeScript output loads.
3
+ const REQUIRED_MAJOR = 24
4
+
5
+ /** One stderr line when `found` is older than the supported Node, otherwise undefined. */
6
+ export function unsupportedNodeMessage(found = process.versions.node) {
7
+ if (Number(found.split('.')[0]) >= REQUIRED_MAJOR) return undefined
8
+ return `vouchington requires Node >=${REQUIRED_MAJOR} (found ${found})\n`
9
+ }
@@ -0,0 +1,20 @@
1
+ #!/usr/bin/env node
2
+ // The stable entry point of the MCP server: `<node> <install>/node_modules/vouchington-tooling/bin/
3
+ // vouchington-mcp.mjs`. It takes no arguments and skips CLI argument parsing, so a CLI refactor
4
+ // cannot move it. Plain JavaScript on purpose: the Node version check runs before anything else.
5
+ // ES2015 syntax only (no `??`, `?.`, or top-level await) so this file parses on every Node that can
6
+ // load a `.mjs` at all (12.17+), and the message prints there. Older Node cannot load `.mjs`.
7
+ import { unsupportedNodeMessage } from './node-guard.mjs'
8
+
9
+ const unsupported = unsupportedNodeMessage()
10
+ if (unsupported === undefined) {
11
+ import('../dist/mcp-server/main.mjs')
12
+ .then((main) => main.runMcpMain())
13
+ .catch((error) => {
14
+ process.stderr.write(String((error && error.stack) || error) + '\n')
15
+ process.exitCode = 1
16
+ })
17
+ } else {
18
+ process.stderr.write(unsupported)
19
+ process.exitCode = 1
20
+ }
@@ -0,0 +1,18 @@
1
+ #!/usr/bin/env node
2
+ // Plain JavaScript on purpose: the Node version check runs before any TypeScript output loads.
3
+ // ES2015 syntax only (no `??`, `?.`, or top-level await) so this file parses on every Node that can
4
+ // load a `.mjs` at all (12.17+), and the message prints there. Older Node cannot load `.mjs`.
5
+ import { unsupportedNodeMessage } from './node-guard.mjs'
6
+
7
+ const unsupported = unsupportedNodeMessage()
8
+ if (unsupported === undefined) {
9
+ import('../dist/cli/index.mjs')
10
+ .then((main) => main.runMain())
11
+ .catch((error) => {
12
+ process.stderr.write(String((error && error.stack) || error) + '\n')
13
+ process.exitCode = 1
14
+ })
15
+ } else {
16
+ process.stderr.write(unsupported)
17
+ process.exitCode = 1
18
+ }
@@ -0,0 +1,5 @@
1
+ /** Counts a session separately from all retry records in the shared worktree outbox. */
2
+ export declare function feedbackOutboxCounts(directory: string, sessionId: string): {
3
+ pendingCount: number;
4
+ worktreePendingCount: number;
5
+ };
@@ -0,0 +1,11 @@
1
+ import { assertSessionId } from './session-id.mjs';
2
+ import { readFeedbackOutbox } from './feedback-outbox.mjs';
3
+ /** Counts a session separately from all retry records in the shared worktree outbox. */
4
+ export function feedbackOutboxCounts(directory, sessionId) {
5
+ assertSessionId(sessionId);
6
+ const records = readFeedbackOutbox(directory);
7
+ return {
8
+ pendingCount: records.filter((record) => record.identity.sessionId === sessionId).length,
9
+ worktreePendingCount: records.length,
10
+ };
11
+ }
@@ -0,0 +1,2 @@
1
+ /** The outbox is temporary retry state; ancestor ownership and modes do not govern it. */
2
+ export declare function ensureOutboxDirectory(directory: string, create: boolean): boolean;
@@ -0,0 +1,16 @@
1
+ import { lstatSync, mkdirSync } from 'node:fs';
2
+ /** The outbox is temporary retry state; ancestor ownership and modes do not govern it. */
3
+ export function ensureOutboxDirectory(directory, create) {
4
+ if (create)
5
+ mkdirSync(directory, { recursive: true, mode: 0o700 });
6
+ try {
7
+ if (!lstatSync(directory).isDirectory())
8
+ throw new Error(`feedback outbox must be a directory: ${directory}`);
9
+ return true;
10
+ }
11
+ catch (error) {
12
+ if (error && typeof error === 'object' && 'code' in error && error.code === 'ENOENT')
13
+ return false;
14
+ throw error;
15
+ }
16
+ }
@@ -2,7 +2,7 @@ import { canonicalFeedback, canonicalFeedbackEvent } from './feedback-canonical.
2
2
  import { createHash, randomUUID } from 'node:crypto';
3
3
  import { closeSync, constants, fsyncSync, fstatSync, openSync, readdirSync, renameSync, unlinkSync, } from 'node:fs';
4
4
  import { isAbsolute, join, resolve } from 'node:path';
5
- import { ensurePrivateDirectory } from '../session-friction/directory.mjs';
5
+ import { ensureOutboxDirectory } from './feedback-outbox-directory.mjs';
6
6
  import { withFileLock } from '../session-friction/lock.mjs';
7
7
  import { openLogFile, readLogContent, writeAll } from '../session-friction/log-file.mjs';
8
8
  import { validateFeedbackEnvelope } from './feedback-codec.mjs';
@@ -13,9 +13,9 @@ const FEEDBACK_OUTBOX_MAX_BYTES = 2_000_000;
13
13
  const RECORD_MAX_BYTES = 18_000;
14
14
  function outboxDirectory(directory, create) {
15
15
  if (typeof directory !== 'string' || directory.length > 4096 || !isAbsolute(directory))
16
- throw new Error('feedback outbox requires an absolute private directory');
16
+ throw new Error('feedback outbox requires an absolute directory');
17
17
  const path = resolve(directory);
18
- ensurePrivateDirectory(path, create);
18
+ ensureOutboxDirectory(path, create);
19
19
  return path;
20
20
  }
21
21
  function filename(record) {
@@ -27,8 +27,6 @@ function readRecord(path) {
27
27
  const descriptor = openLogFile(path, constants.O_RDONLY);
28
28
  try {
29
29
  const status = fstatSync(descriptor);
30
- if ((status.mode & 0o777) !== 0o600 || status.uid !== process.geteuid?.())
31
- throw new Error('feedback outbox record must be private and owned by caller');
32
30
  const bytes = status.size;
33
31
  if (bytes > RECORD_MAX_BYTES)
34
32
  throw new Error('feedback outbox record is too large');
@@ -72,7 +70,7 @@ function syncDirectory(directory) {
72
70
  }
73
71
  export function feedbackOutboxStatus(directory) {
74
72
  const path = outboxDirectory(directory, false);
75
- if (!ensurePrivateDirectory(path, false))
73
+ if (!ensureOutboxDirectory(path, false))
76
74
  return { status: 'empty', pendingCount: 0 };
77
75
  return withFileLock(join(path, '.records'), () => {
78
76
  const pendingCount = records(path).length;
@@ -86,7 +84,7 @@ export function listFeedbackOutbox(directory) {
86
84
  /** Reads the retained records without creating the outbox directory. */
87
85
  export function readFeedbackOutbox(directory) {
88
86
  const path = outboxDirectory(directory, false);
89
- if (!ensurePrivateDirectory(path, false))
87
+ if (!ensureOutboxDirectory(path, false))
90
88
  return [];
91
89
  return withFileLock(join(path, '.records'), () => records(path).map((item) => item.record));
92
90
  }
@@ -24,3 +24,4 @@ export { composeRetrospective } from './feedback-compose.mts';
24
24
  export type { RetrospectiveCompositionInput, FeedbackAssessment } from './feedback-compose.mts';
25
25
  export type { JournalAuditOptions } from './feedback-journal-audit.mts';
26
26
  export type { JournalEntry, JournalLoader, JournalLoadResult } from '../session-friction/types.mts';
27
+ export { feedbackOutboxCounts } from './feedback-outbox-counts.mts';
@@ -63,3 +63,4 @@ export { createFeedbackEnvelope, validateFeedbackEnvelope } from './feedback-cod
63
63
  export { writeFeedback, verifyFreshFeedback, flushFeedbackOutbox, FeedbackDeliveryError, } from './feedback-delivery.mjs';
64
64
  export { feedbackOutboxStatus, readFeedbackOutbox } from './feedback-outbox.mjs';
65
65
  export { composeRetrospective } from './feedback-compose.mjs';
66
+ export { feedbackOutboxCounts } from './feedback-outbox-counts.mjs';
@@ -8,6 +8,7 @@ import { unsupportedContextAlias } from './protocol-context-alias.mjs';
8
8
  import { expressionReceiver } from './protocol-write-receiver.mjs';
9
9
  import { visit } from './response-contract-route-analysis.mjs';
10
10
  import { isSupportedProtocolCallback } from './protocol-callback-invocation.mjs';
11
+ import { mutatesHttpResponseMethod } from './protocol-http-method-mutations.mjs';
11
12
  import { opaqueProtocolCallbackPath } from './protocol-opaque-callback.mjs';
12
13
  const responseMethods = new Set([
13
14
  'setStatus',
@@ -26,6 +27,8 @@ export function associateHttpResponse(call, response, checker, siblings) {
26
27
  const emissions = new Map();
27
28
  for (const [scope, context] of httpContextScopes(handler, checker))
28
29
  visit(scope, (node) => {
30
+ if (mutatesHttpResponseMethod(node, context, checker))
31
+ throw new Error('HTTP response context method is mutated');
29
32
  if (ts.isCallExpression(node) &&
30
33
  executableProtocolPath(node, checker) &&
31
34
  responseMethods.has(indirectHttpResponseMethod(node, context, checker) ?? ''))
@@ -0,0 +1,3 @@
1
+ import ts from '../contract-schema/typescript-api.mts';
2
+ /** A feasible write through a canonical context alias invalidates response dispatch evidence. */
3
+ export declare function mutatesHttpResponseMethod(node: ts.Node, context: ts.Symbol, checker: ts.TypeChecker): boolean;
@@ -0,0 +1,31 @@
1
+ import ts from '../contract-schema/typescript-api.mjs';
2
+ import { contextResponseMethod } from './protocol-http-context.mjs';
3
+ import { unwrapExpression } from './protocol-marker-analysis.mjs';
4
+ import { executableProtocolPath } from './protocol-execution-path.mjs';
5
+ import { opaqueProtocolCallbackPath } from './protocol-opaque-callback.mjs';
6
+ const responseMethods = new Set([
7
+ 'setStatus',
8
+ 'pipeline',
9
+ 'json',
10
+ 'response',
11
+ 'response.buffer',
12
+ 'response.empty',
13
+ ]);
14
+ /** A feasible write through a canonical context alias invalidates response dispatch evidence. */
15
+ export function mutatesHttpResponseMethod(node, context, checker) {
16
+ let target;
17
+ if (ts.isBinaryExpression(node) &&
18
+ node.operatorToken.kind >= ts.SyntaxKind.FirstAssignment &&
19
+ node.operatorToken.kind <= ts.SyntaxKind.LastAssignment)
20
+ target = node.left;
21
+ if (ts.isDeleteExpression(node))
22
+ target = node.expression;
23
+ if ((ts.isPrefixUnaryExpression(node) || ts.isPostfixUnaryExpression(node)) &&
24
+ (node.operator === ts.SyntaxKind.PlusPlusToken ||
25
+ node.operator === ts.SyntaxKind.MinusMinusToken))
26
+ target = node.operand;
27
+ if (!target ||
28
+ !responseMethods.has(contextResponseMethod(unwrapExpression(target), context, checker, true) ?? ''))
29
+ return false;
30
+ return executableProtocolPath(node, checker) || opaqueProtocolCallbackPath(node, checker);
31
+ }
@@ -8,6 +8,14 @@ export function hasBindingWrite(declaration, checker) {
8
8
  const binding = checker.getSymbolAtLocation(declaration.name);
9
9
  let written = false;
10
10
  const referencesBinding = (node) => {
11
+ // Index expressions and computed property names read their keys, not write them.
12
+ // inspect still visits these expressions separately to detect nested side-effect writes.
13
+ if (ts.isComputedPropertyName(node))
14
+ return;
15
+ if (ts.isElementAccessExpression(node)) {
16
+ referencesBinding(node.expression);
17
+ return;
18
+ }
11
19
  if (ts.isIdentifier(node) && checker.getSymbolAtLocation(node) === binding)
12
20
  written = true;
13
21
  node.forEachChild(referencesBinding);
@@ -0,0 +1,4 @@
1
+ export declare function flagsToValues(flags: string[]): Record<string, string>;
2
+ export declare function assertAllowed(values: Record<string, string>, allowed: string[]): void;
3
+ export declare function required(values: Record<string, string>, key: string): string;
4
+ export declare function validatedSessionId(sessionId: string | undefined): string | undefined;
@@ -0,0 +1,34 @@
1
+ import { assertSessionId } from '../../agent-blackboard/session-id.mjs';
2
+ export function flagsToValues(flags) {
3
+ const values = {};
4
+ for (let index = 0; index < flags.length; index += 2) {
5
+ const flag = flags[index];
6
+ const value = flags[index + 1];
7
+ if (!flag?.startsWith('--') || value === undefined)
8
+ throw new Error(`invalid option: ${flag ?? ''}`);
9
+ const key = flag.slice(2);
10
+ if (key in values)
11
+ throw new Error(`duplicate option: ${flag}`);
12
+ values[key] = value;
13
+ }
14
+ return values;
15
+ }
16
+ export function assertAllowed(values, allowed) {
17
+ for (const key of Object.keys(values))
18
+ if (!allowed.includes(key))
19
+ throw new Error(`unknown option: --${key}`);
20
+ }
21
+ export function required(values, key) {
22
+ const value = values[key];
23
+ if (!value)
24
+ throw new Error(`--${key} is required`);
25
+ return value;
26
+ }
27
+ export function validatedSessionId(sessionId) {
28
+ if (sessionId !== undefined) {
29
+ assertSessionId(sessionId);
30
+ if (sessionId.length > 256)
31
+ throw new Error('session identity is too long');
32
+ }
33
+ return sessionId;
34
+ }
@@ -1,5 +1,7 @@
1
- import { appendJournal, flushFeedbackOutbox, formatJournalEntries, probeBlackboard, readJournal, } from '../../agent-blackboard/index.mjs';
1
+ import { assertAllowed, flagsToValues, required, validatedSessionId, } from './agent-blackboard-flags.mjs';
2
+ import { appendJournal, feedbackOutboxCounts, flushFeedbackOutbox, formatJournalEntries, probeBlackboard, readJournal, } from '../../agent-blackboard/index.mjs';
2
3
  import { cleanupSnapshotPartitions, partitionSnapshot } from '../../agent-blackboard/snapshot.mjs';
4
+ const dependencies = { resolveFrom: import.meta.url };
3
5
  let journalReader = readJournal;
4
6
  export function setJournalReaderForTest(reader) {
5
7
  journalReader = reader ?? readJournal;
@@ -8,14 +10,14 @@ export async function runAgentBlackboardCommand(args) {
8
10
  try {
9
11
  const [command, ...rest] = args;
10
12
  if (command === 'probe' && rest.length === 0) {
11
- await probeBlackboard();
13
+ await probeBlackboard(undefined, dependencies);
12
14
  return 0;
13
15
  }
14
16
  if (command === 'journal')
15
17
  return await runJournal(rest);
16
18
  if (command === 'snapshot')
17
19
  return await runSnapshot(rest);
18
- throw new Error('usage: agent-blackboard probe | journal append|entries|flush | snapshot partition|cleanup');
20
+ throw new Error('usage: agent-blackboard probe | journal append|entries|flush|status | snapshot partition|cleanup');
19
21
  }
20
22
  catch (error) {
21
23
  process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
@@ -53,10 +55,20 @@ async function runJournal(args) {
53
55
  const [action, ...flags] = args;
54
56
  const { repositories, remaining } = action === 'append' ? extractRepositories(flags) : { repositories: [], remaining: flags };
55
57
  const values = flagsToValues(remaining);
56
- if (action === 'flush') {
57
- assertAllowed(values, ['outbox-directory']);
58
- const result = await flushFeedbackOutbox({ directory: required(values, 'outbox-directory') });
59
- process.stdout.write(`${JSON.stringify(result)}\n`);
58
+ if (action === 'flush' || action === 'status') {
59
+ assertAllowed(values, ['outbox-directory', 'session-id']);
60
+ const directory = required(values, 'outbox-directory');
61
+ const sessionId = validatedSessionId(action === 'status' ? required(values, 'session-id') : values['session-id']);
62
+ const result = action === 'flush'
63
+ ? await flushFeedbackOutbox({ directory, dependencies })
64
+ : { pendingCount: 0 };
65
+ const counts = sessionId !== undefined ? feedbackOutboxCounts(directory, sessionId) : result;
66
+ process.stdout.write(`${JSON.stringify({
67
+ ...result,
68
+ ...counts,
69
+ sessionId,
70
+ status: counts.pendingCount ? 'pending' : 'empty',
71
+ })}\n`);
60
72
  return 0;
61
73
  }
62
74
  if (action === 'entries') {
@@ -64,7 +76,7 @@ async function runJournal(args) {
64
76
  const sessionId = required(values, 'session-id');
65
77
  let entries;
66
78
  try {
67
- entries = await journalReader(sessionId);
79
+ entries = await journalReader(sessionId, undefined, dependencies);
68
80
  }
69
81
  catch (error) {
70
82
  if (!isNotFound(error))
@@ -91,6 +103,7 @@ async function runJournal(args) {
91
103
  'outbox-directory',
92
104
  ]);
93
105
  const result = await appendJournal({
106
+ dependencies,
94
107
  mode: required(values, 'mode'),
95
108
  sourceEventId: required(values, 'source-event-id'),
96
109
  workOutcome: required(values, 'work-outcome'),
@@ -108,10 +121,14 @@ async function runJournal(args) {
108
121
  ...('timestamp' in values ? { timestamp: values.timestamp } : {}),
109
122
  parentSessionId: values['parent-session-id'] ?? null,
110
123
  });
111
- process.stdout.write(`${JSON.stringify(result)}\n`);
124
+ // Autonomous mode has no outbox, so its receipt must not depend on one.
125
+ const counts = values.mode === 'autonomous'
126
+ ? { pendingCount: 0, worktreePendingCount: 0 }
127
+ : feedbackOutboxCounts(required(values, 'outbox-directory'), required(values, 'session-id'));
128
+ process.stdout.write(`${JSON.stringify({ ...result, ...counts })}\n`);
112
129
  return 0;
113
130
  }
114
- throw new Error('usage: agent-blackboard journal append|entries|flush');
131
+ throw new Error('usage: agent-blackboard journal append|entries|flush|status');
115
132
  }
116
133
  function extractRepositories(flags) {
117
134
  const repositories = [];
@@ -133,31 +150,6 @@ function requiredRepositories(repositories) {
133
150
  throw new Error('--repository is required');
134
151
  return repositories;
135
152
  }
136
- function flagsToValues(flags) {
137
- const values = {};
138
- for (let index = 0; index < flags.length; index += 2) {
139
- const flag = flags[index];
140
- const value = flags[index + 1];
141
- if (!flag?.startsWith('--') || value === undefined)
142
- throw new Error(`invalid option: ${flag ?? ''}`);
143
- const key = flag.slice(2);
144
- if (key in values)
145
- throw new Error(`duplicate option: ${flag}`);
146
- values[key] = value;
147
- }
148
- return values;
149
- }
150
- function assertAllowed(values, allowed) {
151
- for (const key of Object.keys(values))
152
- if (!allowed.includes(key))
153
- throw new Error(`unknown option: --${key}`);
154
- }
155
- function required(values, key) {
156
- const value = values[key];
157
- if (!value)
158
- throw new Error(`--${key} is required`);
159
- return value;
160
- }
161
153
  function isNotFound(error) {
162
154
  return (typeof error === 'object' &&
163
155
  error !== null &&
@@ -1,3 +1,5 @@
1
1
  #!/usr/bin/env node
2
2
  export declare function runCli(argv?: readonly string[]): number | Promise<number>;
3
3
  export declare function isMainModule(metaUrl: string, argv1: string | undefined): boolean;
4
+ /** Runs the CLI and records its exit code. `bin/vouchington.mjs` calls this after its Node check. */
5
+ export declare function runMain(argv?: readonly string[], run?: typeof runCli): Promise<void>;
@@ -159,17 +159,17 @@ export function isMainModule(metaUrl, argv1) {
159
159
  return metaUrl === pathToFileURL(resolve(argv1)).href;
160
160
  }
161
161
  }
162
- /* v8 ignore next 8 */
163
- if (isMainModule(import.meta.url, process.argv[1])) {
164
- const result = runCli();
165
- if (typeof result === 'number')
166
- process.exitCode = result;
167
- else {
168
- result.then((code) => {
169
- process.exitCode = code;
170
- }, (error) => {
171
- process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
172
- process.exitCode = 1;
173
- });
162
+ /** Runs the CLI and records its exit code. `bin/vouchington.mjs` calls this after its Node check. */
163
+ export async function runMain(argv = process.argv, run = runCli) {
164
+ try {
165
+ process.exitCode = await run(argv);
166
+ }
167
+ catch (error) {
168
+ process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
169
+ process.exitCode = 1;
174
170
  }
175
171
  }
172
+ /* v8 ignore next 3 */
173
+ if (isMainModule(import.meta.url, process.argv[1])) {
174
+ void runMain();
175
+ }
@@ -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 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 gha-collaborator-trust Classify authors as write-access collaborators or not\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\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>...\ngha-collaborator-trust <owner/repo> [--allow-bot <login>]... < authors.json\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";
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 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 gha-collaborator-trust Classify authors as write-access collaborators or not\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\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>...\ngha-collaborator-trust <owner/repo> [--allow-bot <login>]... < authors.json\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 status --session-id UUID --outbox-directory PATH\nagent-blackboard journal flush --outbox-directory PATH [--session-id UUID]\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;
@@ -118,7 +118,8 @@ retrospective-facts (--pr NUMBER | --branch NAME | --no-pr) [--repo OWNER/NAME]
118
118
  agent-blackboard probe
119
119
  agent-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]
120
120
  agent-blackboard journal entries --session-id UUID
121
- agent-blackboard journal flush --outbox-directory PATH
121
+ agent-blackboard journal status --session-id UUID --outbox-directory PATH
122
+ agent-blackboard journal flush --outbox-directory PATH [--session-id UUID]
122
123
  Interactive journal append requires --outbox-directory PATH.
123
124
  agent-blackboard snapshot partition --snapshot PATH --checksum SHA256 --counts '{"sessions":N,"entries":N,"records":N,"bytes":N}'
124
125
  agent-blackboard snapshot cleanup [--snapshot PATH] [--partition-directory PATH --receipt JSON]
@@ -1,9 +1,12 @@
1
1
  import type { BlackboardClientDependencies } from '../agent-blackboard/client.mts';
2
2
  import type { RunTextCommand } from '../gh-cli/exec.mts';
3
3
  export type ServerEnvironment = {
4
- launchRoot: string;
4
+ /** The launch directory's worktree top level, or undefined when it is not inside one. */
5
+ launchRoot: string | undefined;
5
6
  env: NodeJS.ProcessEnv;
6
7
  runGit: RunTextCommand;
8
+ /** Test seam: where `agent-blackboard` resolves from. Defaults to this package's own module. */
9
+ resolveFrom?: string | URL;
7
10
  /** Test seam: replaces how the consumer's `agent-blackboard` client is loaded. */
8
11
  blackboard?: BlackboardClientDependencies;
9
12
  };
@@ -1,4 +1,3 @@
1
- import { join } from 'node:path';
2
1
  import { FeedbackDeliveryError } from '../agent-blackboard/index.mjs';
3
2
  import { optionalString, rejectUnknown, requireObject } from './args.mjs';
4
3
  import { readSessionId } from './context.mjs';
@@ -23,7 +22,7 @@ const DIAGNOSTIC_HINTS = {
23
22
  'authentication-rejected': 'the blackboard rejected AGENT_BLACKBOARD_TOKEN',
24
23
  'blackboard-unavailable': 'the blackboard could not be reached or returned an error',
25
24
  'configuration-invalid': 'AGENT_BLACKBOARD_URL or AGENT_BLACKBOARD_TOKEN is missing or invalid',
26
- 'client-unavailable': 'agent-blackboard is not installed in the worktree',
25
+ 'client-unavailable': 'agent-blackboard is not installed next to vouchington-tooling',
27
26
  'readback-unconfirmed': 'the write could not be confirmed; retry the same call (same sourceEventId and content)',
28
27
  'delivery-timeout': 'delivery timed out; retry the same call (same sourceEventId and content)',
29
28
  };
@@ -52,8 +51,11 @@ export async function callTool(name, rawArguments, environment) {
52
51
  sessionId,
53
52
  worktree,
54
53
  env: environment.env,
55
- // The client resolves from the validated worktree, never from the server's own location.
56
- dependencies: { ...environment.blackboard, resolveFrom: join(worktree, 'package.json') },
54
+ // The client resolves from this package's install, like the SDK, never from the worktree.
55
+ dependencies: {
56
+ ...environment.blackboard,
57
+ resolveFrom: environment.resolveFrom ?? import.meta.url,
58
+ },
57
59
  });
58
60
  return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
59
61
  }
@@ -1,16 +1,11 @@
1
- import { flushFeedbackOutbox, readFeedbackOutbox } from '../agent-blackboard/index.mjs';
1
+ import { flushFeedbackOutbox, feedbackOutboxCounts } from '../agent-blackboard/index.mjs';
2
2
  import { outboxDirectory } from './context.mjs';
3
3
  /**
4
4
  * The outbox belongs to the worktree, not to one session, so two counts are reported. `pendingCount`
5
5
  * is the caller's session only; `worktreePendingCount` is every session's records.
6
6
  */
7
7
  export function outboxCounts(worktree, sessionId) {
8
- // A read that never creates the directory, so asking about an empty outbox leaves no trace.
9
- const records = readFeedbackOutbox(outboxDirectory(worktree));
10
- return {
11
- pendingCount: records.filter((record) => record.identity.sessionId === sessionId).length,
12
- worktreePendingCount: records.length,
13
- };
8
+ return feedbackOutboxCounts(outboxDirectory(worktree), sessionId);
14
9
  }
15
10
  function sessionStatus(pendingCount) {
16
11
  return pendingCount === 0 ? 'empty' : 'pending';
@@ -11,6 +11,7 @@ export type Harness = {
11
11
  /** Calls tools against a repository fixture with the blackboard client faked. */
12
12
  export declare function harness(fixture: RepoFixture, options?: FakeOptions & {
13
13
  env?: NodeJS.ProcessEnv;
14
+ launchRoot?: string | undefined;
14
15
  }): Harness;
15
16
  export declare function textOf(result: ToolCallResult): string;
16
17
  export declare function jsonOf(result: ToolCallResult): Record<string, unknown>;
@@ -20,7 +20,7 @@ export function outboxPath(worktree) {
20
20
  export function harness(fixture, options = {}) {
21
21
  const fake = fakeBlackboard(options);
22
22
  const environment = {
23
- launchRoot: fixture.main,
23
+ launchRoot: 'launchRoot' in options ? options.launchRoot : fixture.main,
24
24
  env: options.env ?? BLACKBOARD_ENV,
25
25
  runGit: runIsolatedGit,
26
26
  blackboard: fake.dependencies,
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Entry of `bin/vouchington-mcp.mjs`: starts the server without parsing CLI arguments and records
3
+ * the exit code. The open stdin keeps the process alive after a successful start.
4
+ */
5
+ export declare function runMcpMain(): Promise<void>;
@@ -0,0 +1,8 @@
1
+ import { runMcpCommand } from '../cli/commands/mcp.mjs';
2
+ /**
3
+ * Entry of `bin/vouchington-mcp.mjs`: starts the server without parsing CLI arguments and records
4
+ * the exit code. The open stdin keeps the process alive after a successful start.
5
+ */
6
+ export async function runMcpMain() {
7
+ process.exitCode = await runMcpCommand([]);
8
+ }
@@ -10,6 +10,8 @@ export type RunMcpServerOptions = {
10
10
  runGit?: RunTextCommand;
11
11
  /** Defaults to stdio. Tests pass one half of an in-memory pair. */
12
12
  transport?: Transport;
13
+ /** Test seam: where the startup check looks for `agent-blackboard`. Defaults to this module. */
14
+ resolveFrom?: string | URL;
13
15
  /** Test seam: replaces how the consumer's `agent-blackboard` client is loaded. */
14
16
  blackboard?: BlackboardClientDependencies;
15
17
  };