vouchington-tooling 0.29.0 → 0.31.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 (174) hide show
  1. package/README.md +69 -19
  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/header-contract-registry.mjs +2 -0
  13. package/dist/api-contract-discovery/protocol-body-status.d.mts +2 -0
  14. package/dist/api-contract-discovery/protocol-body-status.mjs +4 -0
  15. package/dist/api-contract-discovery/protocol-callback-argument-bindings.d.mts +7 -0
  16. package/dist/api-contract-discovery/protocol-callback-argument-bindings.mjs +33 -0
  17. package/dist/api-contract-discovery/protocol-callback-invocation.d.mts +5 -0
  18. package/dist/api-contract-discovery/protocol-callback-invocation.mjs +103 -0
  19. package/dist/api-contract-discovery/protocol-callback-mutations.d.mts +5 -0
  20. package/dist/api-contract-discovery/protocol-callback-mutations.mjs +42 -0
  21. package/dist/api-contract-discovery/protocol-callback-registration.d.mts +3 -0
  22. package/dist/api-contract-discovery/protocol-callback-registration.mjs +27 -0
  23. package/dist/api-contract-discovery/protocol-callback-values.d.mts +17 -0
  24. package/dist/api-contract-discovery/protocol-callback-values.mjs +114 -0
  25. package/dist/api-contract-discovery/protocol-context-alias.d.mts +3 -0
  26. package/dist/api-contract-discovery/protocol-context-alias.mjs +20 -0
  27. package/dist/api-contract-discovery/protocol-contract-extraction.d.mts +9 -0
  28. package/dist/api-contract-discovery/protocol-contract-extraction.mjs +87 -0
  29. package/dist/api-contract-discovery/protocol-contract-keys.d.mts +1 -0
  30. package/dist/api-contract-discovery/protocol-contract-keys.mjs +8 -0
  31. package/dist/api-contract-discovery/protocol-contract-registry.d.mts +5 -0
  32. package/dist/api-contract-discovery/protocol-contract-registry.mjs +133 -0
  33. package/dist/api-contract-discovery/protocol-executable-path.d.mts +3 -0
  34. package/dist/api-contract-discovery/protocol-executable-path.mjs +64 -0
  35. package/dist/api-contract-discovery/protocol-execution-path.d.mts +3 -0
  36. package/dist/api-contract-discovery/protocol-execution-path.mjs +16 -0
  37. package/dist/api-contract-discovery/protocol-http-association.d.mts +7 -0
  38. package/dist/api-contract-discovery/protocol-http-association.mjs +97 -0
  39. package/dist/api-contract-discovery/protocol-http-body-kinds.d.mts +3 -0
  40. package/dist/api-contract-discovery/protocol-http-body-kinds.mjs +19 -0
  41. package/dist/api-contract-discovery/protocol-http-context-callers.d.mts +6 -0
  42. package/dist/api-contract-discovery/protocol-http-context-callers.mjs +34 -0
  43. package/dist/api-contract-discovery/protocol-http-context.d.mts +9 -0
  44. package/dist/api-contract-discovery/protocol-http-context.mjs +108 -0
  45. package/dist/api-contract-discovery/protocol-http-emission.d.mts +3 -0
  46. package/dist/api-contract-discovery/protocol-http-emission.mjs +76 -0
  47. package/dist/api-contract-discovery/protocol-http-implicit-taint.d.mts +7 -0
  48. package/dist/api-contract-discovery/protocol-http-implicit-taint.mjs +26 -0
  49. package/dist/api-contract-discovery/protocol-http-registration.d.mts +12 -0
  50. package/dist/api-contract-discovery/protocol-http-registration.mjs +53 -0
  51. package/dist/api-contract-discovery/protocol-marker-analysis.d.mts +7 -0
  52. package/dist/api-contract-discovery/protocol-marker-analysis.mjs +50 -0
  53. package/dist/api-contract-discovery/protocol-opaque-callback.d.mts +3 -0
  54. package/dist/api-contract-discovery/protocol-opaque-callback.mjs +107 -0
  55. package/dist/api-contract-discovery/protocol-platform-callbacks.d.mts +5 -0
  56. package/dist/api-contract-discovery/protocol-platform-callbacks.mjs +72 -0
  57. package/dist/api-contract-discovery/protocol-platform-mutation-targets.d.mts +5 -0
  58. package/dist/api-contract-discovery/protocol-platform-mutation-targets.mjs +89 -0
  59. package/dist/api-contract-discovery/protocol-platform-writes.d.mts +3 -0
  60. package/dist/api-contract-discovery/protocol-platform-writes.mjs +92 -0
  61. package/dist/api-contract-discovery/protocol-requested-keys.d.mts +5 -0
  62. package/dist/api-contract-discovery/protocol-requested-keys.mjs +45 -0
  63. package/dist/api-contract-discovery/protocol-sse-callers.d.mts +7 -0
  64. package/dist/api-contract-discovery/protocol-sse-callers.mjs +67 -0
  65. package/dist/api-contract-discovery/protocol-sse-emission.d.mts +11 -0
  66. package/dist/api-contract-discovery/protocol-sse-emission.mjs +61 -0
  67. package/dist/api-contract-discovery/protocol-sse-feasible-status.d.mts +3 -0
  68. package/dist/api-contract-discovery/protocol-sse-feasible-status.mjs +24 -0
  69. package/dist/api-contract-discovery/protocol-sse-raw-writes.d.mts +9 -0
  70. package/dist/api-contract-discovery/protocol-sse-raw-writes.mjs +70 -0
  71. package/dist/api-contract-discovery/protocol-sse-status.d.mts +3 -0
  72. package/dist/api-contract-discovery/protocol-sse-status.mjs +84 -0
  73. package/dist/api-contract-discovery/protocol-sse-write-access.d.mts +9 -0
  74. package/dist/api-contract-discovery/protocol-sse-write-access.mjs +42 -0
  75. package/dist/api-contract-discovery/protocol-sse-write-helpers.d.mts +6 -0
  76. package/dist/api-contract-discovery/protocol-sse-write-helpers.mjs +66 -0
  77. package/dist/api-contract-discovery/protocol-sse-write-mutations.d.mts +9 -0
  78. package/dist/api-contract-discovery/protocol-sse-write-mutations.mjs +36 -0
  79. package/dist/api-contract-discovery/protocol-sse-write-resolution.d.mts +14 -0
  80. package/dist/api-contract-discovery/protocol-sse-write-resolution.mjs +79 -0
  81. package/dist/api-contract-discovery/protocol-status-dominance.d.mts +5 -0
  82. package/dist/api-contract-discovery/protocol-status-dominance.mjs +35 -0
  83. package/dist/api-contract-discovery/protocol-write-receiver.d.mts +9 -0
  84. package/dist/api-contract-discovery/protocol-write-receiver.mjs +79 -0
  85. package/dist/api-contract-discovery/query-contract-registry.mjs +2 -0
  86. package/dist/api-contract-discovery/registered-route-binding-writes.d.mts +3 -0
  87. package/dist/api-contract-discovery/registered-route-binding-writes.mjs +30 -0
  88. package/dist/api-contract-discovery/registered-route-catalog.mjs +11 -7
  89. package/dist/api-contract-discovery/registered-route-factory-returns.d.mts +3 -0
  90. package/dist/api-contract-discovery/registered-route-factory-returns.mjs +29 -0
  91. package/dist/api-contract-discovery/registered-route-handler-analysis.d.mts +5 -1
  92. package/dist/api-contract-discovery/registered-route-handler-analysis.mjs +79 -35
  93. package/dist/api-contract-discovery/registered-route-runtime-parameters.d.mts +3 -0
  94. package/dist/api-contract-discovery/registered-route-runtime-parameters.mjs +5 -0
  95. package/dist/api-contract-discovery/registered-route-terminal-error.d.mts +3 -0
  96. package/dist/api-contract-discovery/registered-route-terminal-error.mjs +98 -0
  97. package/dist/api-contract-discovery/request-contract-registry.mjs +2 -0
  98. package/dist/api-contract-discovery/response-contract-implicit.mjs +28 -10
  99. package/dist/api-contract-discovery/response-contract-registry.mjs +14 -1
  100. package/dist/api-contract-discovery/response-contract-route-analysis.d.mts +1 -1
  101. package/dist/api-contract-discovery/response-contract-route-analysis.mjs +22 -4
  102. package/dist/api-contract-discovery/response-contract-status.d.mts +1 -1
  103. package/dist/api-contract-discovery/response-contract-status.mjs +10 -8
  104. package/dist/api-contract-discovery/response-contract-types.d.mts +7 -0
  105. package/dist/cli/commands/agent-blackboard-flags.d.mts +4 -0
  106. package/dist/cli/commands/agent-blackboard-flags.mjs +34 -0
  107. package/dist/cli/commands/agent-blackboard.mjs +27 -35
  108. package/dist/cli/index.d.mts +2 -0
  109. package/dist/cli/index.mjs +12 -15
  110. package/dist/cli/parse-forwarded-commands.d.mts +0 -3
  111. package/dist/cli/parse-forwarded-commands.mjs +0 -2
  112. package/dist/cli/parse.d.mts +0 -3
  113. package/dist/cli/usage.d.mts +1 -1
  114. package/dist/cli/usage.mjs +2 -6
  115. package/dist/contract-schema/contract-schema-bounded-array.d.mts +1 -3
  116. package/dist/contract-schema/contract-schema-bounded-array.mjs +5 -0
  117. package/dist/contract-schema/contract-schema-object-tuple.mjs +7 -5
  118. package/dist/contract-schema/contract-schema-type-extractor.mjs +13 -0
  119. package/dist/contract-schema/contract-schema-validator.mjs +2 -2
  120. package/dist/index.d.mts +0 -2
  121. package/dist/index.mjs +0 -1
  122. package/dist/mcp-server/dispatch.d.mts +4 -1
  123. package/dist/mcp-server/dispatch.mjs +6 -4
  124. package/dist/mcp-server/handlers-outbox.mjs +2 -7
  125. package/dist/mcp-server/harness.test-helpers.d.mts +1 -0
  126. package/dist/mcp-server/harness.test-helpers.mjs +1 -1
  127. package/dist/mcp-server/main.d.mts +5 -0
  128. package/dist/mcp-server/main.mjs +8 -0
  129. package/dist/mcp-server/run.d.mts +2 -0
  130. package/dist/mcp-server/run.mjs +4 -1
  131. package/dist/mcp-server/sdk.d.mts +11 -2
  132. package/dist/mcp-server/sdk.mjs +37 -5
  133. package/dist/mcp-server/server.mjs +3 -2
  134. package/dist/mcp-server/tools.mjs +3 -2
  135. package/dist/mcp-server/worktree.d.mts +17 -10
  136. package/dist/mcp-server/worktree.mjs +48 -31
  137. package/dist/openapi-document/build-openapi-catalog-sse.d.mts +4 -0
  138. package/dist/openapi-document/build-openapi-catalog-sse.mjs +27 -0
  139. package/dist/openapi-document/build-openapi-document.mjs +5 -2
  140. package/dist/openapi-document/build-openapi-response.d.mts +2 -2
  141. package/dist/openapi-document/build-openapi-response.mjs +33 -3
  142. package/dist/openapi-document/build-openapi-sse-events.d.mts +11 -0
  143. package/dist/openapi-document/build-openapi-sse-events.mjs +46 -0
  144. package/dist/openapi-document/contract-schema-intersection-merge.mjs +11 -11
  145. package/dist/openapi-document/contract-schema-to-openapi.mjs +3 -3
  146. package/dist/openapi-document/openapi-route-helpers.mjs +1 -3
  147. package/dist/openapi-document/openapi-types.d.mts +3 -0
  148. package/dist/openapi-document/operation-types.d.mts +7 -0
  149. package/docs/mcp-server.md +83 -25
  150. package/package.json +11 -9
  151. package/scripts/build.mjs +2 -0
  152. package/skills/blackboard/SKILL.md +66 -4
  153. package/skills/manifest.json +19 -19
  154. package/dist/agent-harness-config/apply.d.mts +0 -4
  155. package/dist/agent-harness-config/apply.mjs +0 -72
  156. package/dist/agent-harness-config/cli.d.mts +0 -10
  157. package/dist/agent-harness-config/cli.mjs +0 -100
  158. package/dist/agent-harness-config/index.d.mts +0 -4
  159. package/dist/agent-harness-config/index.mjs +0 -3
  160. package/dist/agent-harness-config/merge-json.d.mts +0 -5
  161. package/dist/agent-harness-config/merge-json.mjs +0 -62
  162. package/dist/agent-harness-config/merge-toml.d.mts +0 -6
  163. package/dist/agent-harness-config/merge-toml.mjs +0 -132
  164. package/dist/agent-harness-config/paths.d.mts +0 -3
  165. package/dist/agent-harness-config/paths.mjs +0 -117
  166. package/dist/agent-harness-config/policy.d.mts +0 -14
  167. package/dist/agent-harness-config/policy.mjs +0 -51
  168. package/dist/agent-harness-config/prerequisites.d.mts +0 -2
  169. package/dist/agent-harness-config/prerequisites.mjs +0 -67
  170. package/dist/agent-harness-config/toml-value.d.mts +0 -6
  171. package/dist/agent-harness-config/toml-value.mjs +0 -69
  172. package/dist/agent-harness-config/types.d.mts +0 -90
  173. package/dist/agent-harness-config/types.mjs +0 -2
  174. package/docs/agent-harness-config.md +0 -134
@@ -0,0 +1,27 @@
1
+ import { catalogResponse } from './openapi-route-helpers.mjs';
2
+ /** Keeps a cataloged stream unavailable until its extracted variants describe SSE. */
3
+ export function retainCatalogSseResponse(kind, variants, rendered) {
4
+ if (kind !== 'sse' ||
5
+ variants.some((variant) => variant.mediaType === 'text/event-stream' &&
6
+ variant.mediaTypeKnowledge !== 'unknown' &&
7
+ variant.bodyKind !== 'none'))
8
+ return rendered;
9
+ const reason = [rendered.unavailableReason, catalogResponse('sse').unavailableReason]
10
+ .filter(Boolean)
11
+ .join('; ');
12
+ const existing = rendered.responses[200];
13
+ return {
14
+ responses: {
15
+ ...rendered.responses,
16
+ 200: {
17
+ ...existing,
18
+ description: existing?.description ?? 'OK',
19
+ content: { ...existing?.content, 'text/event-stream': { schema: {} } },
20
+ 'x-schema-unavailable': true,
21
+ 'x-schema-unavailable-reason': reason,
22
+ },
23
+ },
24
+ unavailable: true,
25
+ unavailableReason: reason,
26
+ };
27
+ }
@@ -1,6 +1,7 @@
1
1
  import { queryParameters } from './build-openapi-query.mjs';
2
2
  import { buildOperationRequestBody } from './build-openapi-request.mjs';
3
3
  import { buildOperationResponse } from './build-openapi-response.mjs';
4
+ import { retainCatalogSseResponse } from './build-openapi-catalog-sse.mjs';
4
5
  import { createComponentRegistry } from './component-registry.mjs';
5
6
  import { responseStatusCodesForContract, routeShape, } from './operation-types.mjs';
6
7
  import { registerOpenApiErrorResponse } from './openapi-error-response.mjs';
@@ -112,10 +113,12 @@ function assertFixedNoContent(catalogRoute, variants, groupKey) {
112
113
  throw new Error(`OpenAPI fixed-no-content metadata conflicts with extracted response variants for ${groupKey}`);
113
114
  }
114
115
  function renderOperation(catalogRoute, variants, registry) {
115
- if (catalogRoute?.kind === 'sse' || catalogRoute?.kind === 'fixed-no-content')
116
+ if (catalogRoute?.kind === 'fixed-no-content')
116
117
  return catalogResponse(catalogRoute.kind, catalogRoute.fixedStatus);
117
118
  if (variants)
118
- return buildOperationResponse(variants, registry);
119
+ return retainCatalogSseResponse(catalogRoute?.kind, variants, buildOperationResponse(variants, registry));
120
+ if (catalogRoute?.kind === 'sse')
121
+ return catalogResponse('sse');
119
122
  if (catalogRoute?.kind === 'error-only')
120
123
  return catalogResponse('error-only');
121
124
  return catalogResponse('ordinary');
@@ -1,8 +1,8 @@
1
1
  import type { ComponentRegistry } from './component-registry.mts';
2
2
  import { type ResponseContract } from './operation-types.mts';
3
- import type { OpenApiResponseOrRef, OpenApiSchema } from './openapi-types.mts';
3
+ import type { OpenApiResponse, OpenApiSchema } from './openapi-types.mts';
4
4
  export declare function buildOperationResponse(variants: ResponseContract[], registry: ComponentRegistry): {
5
- responses: Record<number, OpenApiResponseOrRef>;
5
+ responses: Record<number, OpenApiResponse>;
6
6
  unavailable: boolean;
7
7
  unavailableReason?: string;
8
8
  };
@@ -1,4 +1,5 @@
1
1
  import { STATUS_CODES } from 'node:http';
2
+ import { collectSseEventSchemas, renderSseEvents, } from './build-openapi-sse-events.mjs';
2
3
  import { nodeToOpenApi } from './contract-schema-to-openapi.mjs';
3
4
  import { responseStatusCodesForContract } from './operation-types.mjs';
4
5
  export function buildOperationResponse(variants, registry) {
@@ -7,6 +8,8 @@ export function buildOperationResponse(variants, registry) {
7
8
  const operationFailures = [];
8
9
  for (const contract of variants) {
9
10
  const converted = convertContract(contract, registry);
11
+ const events = collectSseEventSchemas(contract, registry);
12
+ operationFailures.push(...events.failureReasons);
10
13
  if (converted.failureReason)
11
14
  operationFailures.push(converted.failureReason);
12
15
  if (contract.statusKnowledge === 'unknown' && !converted.failureReason)
@@ -17,6 +20,8 @@ export function buildOperationResponse(variants, registry) {
17
20
  if (converted.failureReason)
18
21
  reasons.push(converted.failureReason);
19
22
  bodyless.set(status, reasons);
23
+ if (contract.includeDefaultError)
24
+ addDefaultError(content, status, registry);
20
25
  continue;
21
26
  }
22
27
  const mediaType = mediaTypeFor(contract);
@@ -25,12 +30,21 @@ export function buildOperationResponse(variants, registry) {
25
30
  continue;
26
31
  }
27
32
  const byMedia = content.get(status) ?? new Map();
28
- const bucket = byMedia.get(mediaType) ?? { schemas: [], failureReasons: [] };
29
- bucket.schemas.push(converted.schema ?? {});
33
+ const bucket = byMedia.get(mediaType) ?? {
34
+ schemas: [],
35
+ failureReasons: [],
36
+ sseEvents: new Map(),
37
+ };
38
+ bucket.schemas.push(mediaType === 'text/event-stream' ? { type: 'string' } : (converted.schema ?? {}));
39
+ bucket.failureReasons.push(...events.failureReasons);
30
40
  if (converted.failureReason)
31
41
  bucket.failureReasons.push(converted.failureReason);
42
+ for (const [name, schemas] of events.schemas)
43
+ bucket.sseEvents.set(name, [...(bucket.sseEvents.get(name) ?? []), ...schemas]);
32
44
  byMedia.set(mediaType, bucket);
33
45
  content.set(status, byMedia);
46
+ if (contract.includeDefaultError)
47
+ addDefaultError(content, status, registry);
34
48
  }
35
49
  }
36
50
  const responses = {};
@@ -45,6 +59,17 @@ export function buildOperationResponse(variants, registry) {
45
59
  ...(reasons.length > 0 ? { unavailableReason: reasons.join('; ') } : {}),
46
60
  };
47
61
  }
62
+ function addDefaultError(content, status, registry) {
63
+ const byMedia = content.get(status) ?? new Map();
64
+ const bucket = byMedia.get('application/json') ?? {
65
+ schemas: [],
66
+ failureReasons: [],
67
+ sseEvents: new Map(),
68
+ };
69
+ bucket.schemas.push({ $ref: `#/components/schemas/${registry.refName('ErrorBody')}` });
70
+ byMedia.set('application/json', bucket);
71
+ content.set(status, byMedia);
72
+ }
48
73
  function renderStatusResponse(status, byMedia, bodyless, operationFailures) {
49
74
  const hasBodyless = bodyless.has(status);
50
75
  const conflictReason = byMedia && hasBodyless
@@ -67,7 +92,12 @@ function renderStatusResponse(status, byMedia, bodyless, operationFailures) {
67
92
  .toSorted(([left], [right]) => left.localeCompare(right))
68
93
  .map(([mediaType, bucket]) => [
69
94
  mediaType,
70
- { schema: mergeVariantSchemas(bucket.schemas) },
95
+ {
96
+ schema: mergeVariantSchemas(bucket.schemas),
97
+ ...(bucket.sseEvents.size
98
+ ? { 'x-sse-events': renderSseEvents(bucket.sseEvents) }
99
+ : {}),
100
+ },
71
101
  ])),
72
102
  }
73
103
  : {}),
@@ -0,0 +1,11 @@
1
+ import type { ComponentRegistry } from './component-registry.mts';
2
+ import type { ResponseContract } from './operation-types.mts';
3
+ import type { OpenApiSchema } from './openapi-types.mts';
4
+ export type SseEventSchemas = Map<string, OpenApiSchema[]>;
5
+ export declare function collectSseEventSchemas(contract: ResponseContract, registry: ComponentRegistry): {
6
+ schemas: SseEventSchemas;
7
+ failureReasons: string[];
8
+ };
9
+ export declare function renderSseEvents(schemas: SseEventSchemas): Record<string, {
10
+ dataSchema: OpenApiSchema;
11
+ }>;
@@ -0,0 +1,46 @@
1
+ import { nodeToOpenApi } from './contract-schema-to-openapi.mjs';
2
+ export function collectSseEventSchemas(contract, registry) {
3
+ const schemas = new Map();
4
+ const failureReasons = [];
5
+ if (contract.mediaType !== 'text/event-stream' && !contract.sseEvents)
6
+ return { schemas, failureReasons };
7
+ if (contract.mediaType !== 'text/event-stream' || contract.bodyKind === 'none')
8
+ return {
9
+ schemas,
10
+ failureReasons: ['SSE event metadata requires a text/event-stream response body'],
11
+ };
12
+ if (!contract.sseEvents?.length)
13
+ return { schemas, failureReasons: ['SSE event payload contracts are missing'] };
14
+ for (const { eventName, contract: event } of contract.sseEvents) {
15
+ if (!eventName.trim() || event.schema.root.type === 'unknown') {
16
+ failureReasons.push('SSE event payload or name is not statically known');
17
+ continue;
18
+ }
19
+ let schema;
20
+ try {
21
+ schema = nodeToOpenApi(event.schema.root, {
22
+ definitions: event.schema.definitions,
23
+ refName: registry.refName,
24
+ });
25
+ }
26
+ catch (error) {
27
+ failureReasons.push(String(error));
28
+ continue;
29
+ }
30
+ registry.register(event.source, event.schema.definitions);
31
+ const variants = schemas.get(eventName) ?? [];
32
+ variants.push(schema);
33
+ schemas.set(eventName, variants);
34
+ }
35
+ return { schemas, failureReasons };
36
+ }
37
+ export function renderSseEvents(schemas) {
38
+ return Object.fromEntries([...schemas.entries()]
39
+ .toSorted(([left], [right]) => left.localeCompare(right))
40
+ .map(([name, variants]) => {
41
+ const unique = [
42
+ ...new Map(variants.map((schema) => [JSON.stringify(schema), schema])).values(),
43
+ ].toSorted((left, right) => JSON.stringify(left).localeCompare(JSON.stringify(right)));
44
+ return [name, { dataSchema: unique.length === 1 ? unique[0] : { anyOf: unique } }];
45
+ }));
46
+ }
@@ -11,10 +11,10 @@ export function intersectionToOpenApi(node, ctx, nodeToOpenApi) {
11
11
  if (resolved.some((variant) => variant === undefined)) {
12
12
  return { allOf: node.variants.map((variant) => nodeToOpenApi(variant, ctx)) };
13
13
  }
14
- const properties = {};
14
+ const properties = new Map();
15
15
  for (const variant of resolved) {
16
16
  for (const [key, property] of Object.entries(variant.properties)) {
17
- const existing = properties[key];
17
+ const existing = properties.get(key);
18
18
  if (existing &&
19
19
  hashSchemaNode(existing.schema, ctx) !== hashSchemaNode(property.schema, ctx)) {
20
20
  // A member re-declaring a shared key with a strictly narrower type is a common, valid
@@ -23,33 +23,33 @@ export function intersectionToOpenApi(node, ctx, nodeToOpenApi) {
23
23
  // narrower type, not a conflict. Prefer whichever side is the narrower one; only two
24
24
  // genuinely incompatible schemas (neither a subset of the other) are a real conflict.
25
25
  if (isNarrowerVariant(property.schema, existing.schema, ctx)) {
26
- properties[key] = {
26
+ properties.set(key, {
27
27
  schema: property.schema,
28
28
  required: existing.required || property.required,
29
- };
29
+ });
30
30
  continue;
31
31
  }
32
32
  /* v8 ignore next 6 -- symmetric of the previous branch; covered by order-swapped fixtures */
33
33
  if (isNarrowerVariant(existing.schema, property.schema, ctx)) {
34
- properties[key] = {
34
+ properties.set(key, {
35
35
  schema: existing.schema,
36
36
  required: existing.required || property.required,
37
- };
37
+ });
38
38
  continue;
39
39
  }
40
40
  throw new Error(`Cannot merge intersection: property "${key}" has conflicting schemas across members`);
41
41
  }
42
- properties[key] = {
42
+ properties.set(key, {
43
43
  schema: property.schema,
44
44
  required: existing?.required || property.required,
45
- };
45
+ });
46
46
  }
47
47
  }
48
- const sortedKeys = Object.keys(properties).toSorted();
49
- const required = sortedKeys.filter((key) => properties[key].required);
48
+ const sortedKeys = [...properties.keys()].toSorted();
49
+ const required = sortedKeys.filter((key) => properties.get(key).required);
50
50
  return {
51
51
  type: 'object',
52
- properties: Object.fromEntries(sortedKeys.map((key) => [key, nodeToOpenApi(properties[key].schema, ctx)])),
52
+ properties: Object.fromEntries(sortedKeys.map((key) => [key, nodeToOpenApi(properties.get(key).schema, ctx)])),
53
53
  ...(required.length > 0 ? { required } : {}),
54
54
  additionalProperties: false,
55
55
  };
@@ -42,17 +42,17 @@ function tupleToOpenApi(node, ctx) {
42
42
  }
43
43
  function objectToOpenApi(node, ctx) {
44
44
  const keys = Object.keys(node.properties).toSorted();
45
- const properties = {};
45
+ const properties = new Map();
46
46
  const required = [];
47
47
  for (const key of keys) {
48
48
  const property = node.properties[key];
49
- properties[key] = nodeToOpenApi(property.schema, ctx);
49
+ properties.set(key, nodeToOpenApi(property.schema, ctx));
50
50
  if (property.required)
51
51
  required.push(key);
52
52
  }
53
53
  return {
54
54
  type: 'object',
55
- properties,
55
+ properties: Object.fromEntries(properties),
56
56
  ...(required.length > 0 ? { required } : {}),
57
57
  additionalProperties: node.additionalProperties === false ? false : nodeToOpenApi(node.additionalProperties, ctx),
58
58
  };
@@ -34,11 +34,9 @@ export function catalogResponse(kind, fixedStatus) {
34
34
  };
35
35
  }
36
36
  if (kind === 'error-only') {
37
- const reason = 'registered route has no success response';
38
37
  return {
39
38
  responses: { 405: { $ref: '#/components/responses/Error' } },
40
- unavailable: true,
41
- unavailableReason: reason,
39
+ unavailable: false,
42
40
  };
43
41
  }
44
42
  const reason = kind === 'sse'
@@ -39,6 +39,9 @@ export type OpenApiResponse = {
39
39
  description: string;
40
40
  content?: Record<string, {
41
41
  schema: OpenApiSchema;
42
+ 'x-sse-events'?: Record<string, {
43
+ dataSchema: OpenApiSchema;
44
+ }>;
42
45
  }>;
43
46
  'x-schema-unavailable'?: true;
44
47
  'x-schema-unavailable-reason'?: string;
@@ -1,3 +1,4 @@
1
+ import type { ExtractedResponseContract } from '../contract-schema/types.mts';
1
2
  import type { ContractSchema } from './contract-schema-types.mts';
2
3
  import type { OpenApiQueryContract } from './query-types.mts';
3
4
  export type ResponseContract = {
@@ -11,6 +12,12 @@ export type ResponseContract = {
11
12
  mediaType?: string;
12
13
  mediaTypeKnowledge?: 'known' | 'none' | 'unknown';
13
14
  unavailableReason?: string;
15
+ sseEvents?: readonly {
16
+ eventName: string;
17
+ contract: ExtractedResponseContract;
18
+ }[];
19
+ /** Merge the framework error body into this explicit response's JSON variants. */
20
+ includeDefaultError?: boolean;
14
21
  };
15
22
  export type RequestContract = {
16
23
  source: string;
@@ -11,30 +11,68 @@ are later phases.
11
11
 
12
12
  ## Install
13
13
 
14
- The server needs three packages next to `vouchington-tooling`. The last two are optional peer
15
- dependencies, so nothing else in the CLI requires them:
14
+ Install the server once per machine, in its own dedicated directory, not in a project or a
15
+ worktree: `pnpm add` installs into the directory it runs in. The server needs these packages
16
+ installed next to `vouchington-tooling`. `agent-blackboard` and `@modelcontextprotocol/sdk` are
17
+ optional peer dependencies, so nothing else in the CLI requires them. Pin exact versions; replace
18
+ the placeholders with the current releases:
16
19
 
17
20
  ```bash
18
- pnpm add -D vouchington-tooling agent-blackboard@^0.6.0
19
- pnpm add -D @modelcontextprotocol/sdk zod
21
+ mkdir -p ~/.local/share/vouchington-mcp
22
+ cd ~/.local/share/vouchington-mcp
23
+ pnpm init
24
+ pnpm add --save-exact vouchington-tooling@<version> agent-blackboard@<version>
25
+ pnpm add --save-exact @modelcontextprotocol/sdk@<version> zod@<version>
20
26
  ```
21
27
 
22
- `zod` is required by the SDK. When the SDK is missing, `vouchington mcp` writes a message naming the
23
- package to stderr and exits with status 1. `agent-blackboard` is resolved from the validated
24
- worktree at call time (`createRequire` on `<worktree>/package.json`), never from this package.
28
+ Launch the server by the absolute path into that directory, for example
29
+ `/abs/node ~/.local/share/vouchington-mcp/node_modules/vouchington-tooling/bin/vouchington-mcp.mjs`
30
+ with `~` expanded to an absolute path (see [Launch](#launch)).
31
+
32
+ `zod` is required by the SDK. Both `agent-blackboard` and the SDK are resolved from the server's own
33
+ install, relative to this package's module, never from the worktree a tool call names, so
34
+ repositories do not need to depend on `agent-blackboard`. When either is missing, `vouchington mcp`
35
+ writes a message naming the package and the install directory to add it in to stderr and exits with
36
+ status 1.
25
37
 
26
38
  The connection comes from `AGENT_BLACKBOARD_URL` and `AGENT_BLACKBOARD_TOKEN` in the server's
27
39
  environment. The server never searches for, prints, or mints them.
28
40
 
41
+ ## CLI fallback
42
+
43
+ If MCP is unavailable, use `vouchington agent-blackboard journal append|entries|flush|status` as described
44
+ in the blackboard skill. Record the fallback and delivery status in the journal and tell the human. The CLI resolves its
45
+ client from its own installation, so install `agent-blackboard` alongside that trusted CLI.
46
+ Run the script with a verified absolute `node` and a cleared environment (`env -i`), never through
47
+ its `#!/usr/bin/env node` line, which takes `node` from a possibly repository-controlled `PATH`.
48
+ A missing MCP connection does not waive persistence or readback requirements.
49
+
29
50
  ## Launch
30
51
 
31
52
  ```bash
32
53
  vouchington mcp
33
54
  ```
34
55
 
35
- The command takes no arguments. Start it inside the repository's git worktree: that repository
36
- defines which worktrees the tools accept. stdout carries only protocol frames, and diagnostics go
37
- to stderr.
56
+ The stable entry point, for a harness that launches the server without a shell or a `PATH`
57
+ lookup, is this package-relative file:
58
+
59
+ ```text
60
+ node_modules/vouchington-tooling/bin/vouchington-mcp.mjs
61
+ ```
62
+
63
+ Run it with an absolute Node, for example
64
+ `/abs/node /abs/machine/node_modules/vouchington-tooling/bin/vouchington-mcp.mjs`. It is also the
65
+ `vouchington-mcp` bin (`node_modules/.bin/vouchington-mcp`) and the `vouchington-tooling/mcp-server/bin`
66
+ export. It takes no arguments, skips CLI argument parsing, and is a hand-written plain-JavaScript
67
+ file that a CLI refactor does not move; a test pins the path. Both it and `bin/vouchington.mjs` check
68
+ the Node version first: on Node older than 24 they print one line to stderr, such as
69
+ `vouchington requires Node >=24 (found 18.19.0)`, and exit with status 1.
70
+
71
+ The command takes no arguments and may be launched with any working directory, including one outside
72
+ every git repository (for example `~`). When the working directory is inside a git worktree, that
73
+ worktree's top level is the default for tools called without `worktree`. Otherwise those calls fail
74
+ with a tool error asking for an explicit `worktree`. stdout carries only protocol frames, and
75
+ diagnostics go to stderr.
38
76
 
39
77
  ## Tools
40
78
 
@@ -113,42 +151,62 @@ instructions shows it to the agent.
113
151
 
114
152
  ## Worktree validation
115
153
 
116
- `worktree` must be an absolute path that, after resolving symlinks, exactly matches an entry of
117
- `git worktree list --porcelain` for the repository the server was launched from. The list is read on
118
- every call, so worktrees created after launch are accepted and removed ones are refused. Anything
119
- else is rejected: a subdirectory of a worktree, a relative or nonexistent path, and a separate
120
- repository, including one made with `git init` elsewhere. It defaults to the launch worktree's root.
121
- Git runs without `GIT_*` variables, so `GIT_DIR` cannot redirect the check.
154
+ `worktree` is checked on every call. It must be an absolute path that, after resolving symlinks,
155
+ equals the top level of a git worktree, that is, what `git rev-parse --show-toplevel` prints when
156
+ run in it. Any git worktree on the machine is accepted, including one of a repository unrelated to
157
+ the launch directory, so the tools can act on every checkout the user can reach. Worktrees created
158
+ after launch are accepted and removed ones are refused. Rejected: a subdirectory of a worktree, a
159
+ relative or nonexistent path, and a directory that is not in a git worktree. Git runs without
160
+ `GIT_*` variables, so `GIT_DIR` cannot redirect the check, and with `LC_ALL=C` and no `LANGUAGE`, so
161
+ its "not a git repository" diagnostic is never translated.
162
+
163
+ `worktree` only chooses where the outbox lives. It is not a trust boundary for loading code: the
164
+ server never imports modules from it, and a tool that needs to must first get a repository allowlist
165
+ from the machine registration.
166
+
167
+ When `worktree` is omitted, the launch directory's worktree top level is used if the launch
168
+ directory is inside one, and it is validated like any explicit path. If the server was launched
169
+ outside a git worktree, the call fails with a tool error asking for an explicit `worktree`.
122
170
 
123
171
  ## Registering the server
124
172
 
125
- The commands below run `vouchington` from the repository's own `node_modules`, so a fresh worktree
126
- uses the version its lockfile pins. Set the blackboard credentials in the environment the harness
127
- gives the server.
173
+ Register the machine-wide installation from [Install](#install) at user level, with absolute paths.
174
+ Never check a registration into a repository: it names a path and a Node that exist only on your
175
+ machine, and every worktree inherits it from your user config. Replace `/abs/node` with the absolute
176
+ path of a Node >= 24 and `/home/you` with your home directory (neither file expands `~`). Set the
177
+ blackboard credentials in the environment you start the harness from.
128
178
 
129
- Claude Code, `.mcp.json`:
179
+ Claude Code, `mcpServers` in `~/.claude.json`:
130
180
 
131
181
  ```json
132
182
  {
133
183
  "mcpServers": {
134
184
  "vouchington-tooling": {
135
- "command": "bash",
136
- "args": ["-c", "exec \"$(git rev-parse --show-toplevel)/node_modules/.bin/vouchington\" mcp"]
185
+ "command": "/abs/node",
186
+ "args": [
187
+ "/home/you/.local/share/vouchington-mcp/node_modules/vouchington-tooling/bin/vouchington-mcp.mjs"
188
+ ]
137
189
  }
138
190
  }
139
191
  }
140
192
  ```
141
193
 
142
- Codex, `.codex/config.toml`:
194
+ Codex, `~/.codex/config.toml`:
143
195
 
144
196
  ```toml
145
197
  [mcp_servers.vouchington-tooling]
146
- command = "bash"
147
- args = ["-c", "exec \"$(git rev-parse --show-toplevel)/node_modules/.bin/vouchington\" mcp"]
198
+ command = "/abs/node"
199
+ args = [
200
+ "/home/you/.local/share/vouchington-mcp/node_modules/vouchington-tooling/bin/vouchington-mcp.mjs",
201
+ ]
202
+ env_vars = ["AGENT_BLACKBOARD_URL", "AGENT_BLACKBOARD_TOKEN"]
148
203
  default_tools_approval_mode = "approve"
149
204
  required = false
150
205
  ```
151
206
 
207
+ `env_vars` forwards those variables from Codex's own environment. Codex does not expand `${VAR}`
208
+ inside `env`, so do not write the credentials there.
209
+
152
210
  Approving every tool of the server at once:
153
211
 
154
212
  - Claude Code permission rule: `mcp__vouchington-tooling__*`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vouchington-tooling",
3
- "version": "0.29.0",
3
+ "version": "0.31.0",
4
4
  "description": "Vouchington CLI and extractable tooling libraries.",
5
5
  "homepage": "https://github.com/vouchington/vouchington-tooling/tree/main/packages/vouchington-tooling#readme",
6
6
  "bugs": {
@@ -14,10 +14,12 @@
14
14
  "directory": "packages/vouchington-tooling"
15
15
  },
16
16
  "bin": {
17
- "vouchington": "dist/cli/index.mjs"
17
+ "vouchington": "bin/vouchington.mjs",
18
+ "vouchington-mcp": "bin/vouchington-mcp.mjs"
18
19
  },
19
20
  "files": [
20
21
  "ast-grep",
22
+ "bin",
21
23
  "dist",
22
24
  "docs",
23
25
  "skills",
@@ -68,6 +70,7 @@
68
70
  "import": "./dist/agent-blackboard/index.mjs",
69
71
  "default": "./dist/agent-blackboard/index.mjs"
70
72
  },
73
+ "./mcp-server/bin": "./bin/vouchington-mcp.mjs",
71
74
  "./session-friction": {
72
75
  "types": "./dist/session-friction/index.d.mts",
73
76
  "import": "./dist/session-friction/index.mjs",
@@ -113,11 +116,6 @@
113
116
  "import": "./dist/ast-grep-pack/index.mjs",
114
117
  "default": "./dist/ast-grep-pack/index.mjs"
115
118
  },
116
- "./agent-harness-config": {
117
- "types": "./dist/agent-harness-config/index.d.mts",
118
- "import": "./dist/agent-harness-config/index.mjs",
119
- "default": "./dist/agent-harness-config/index.mjs"
120
- },
121
119
  "./vitest-blob-manifest": {
122
120
  "types": "./dist/vitest-blob-manifest/index.d.mts",
123
121
  "import": "./dist/vitest-blob-manifest/index.mjs",
@@ -352,7 +350,6 @@
352
350
  "picomatch": "4.0.7",
353
351
  "remark": "15.0.1",
354
352
  "remark-gfm": "4.0.1",
355
- "smol-toml": "1.9.0",
356
353
  "spdx-expression-parse": "5.0.0",
357
354
  "yaml": "2.9.1"
358
355
  },
@@ -361,14 +358,19 @@
361
358
  "@modelcontextprotocol/sdk": "1.31.0",
362
359
  "@types/picomatch": "^4.0.3",
363
360
  "@types/spdx-expression-parse": "4.0.0",
364
- "agent-blackboard": "^0.6.0"
361
+ "agent-blackboard": "^0.6.0",
362
+ "smol-toml": "1.9.0"
365
363
  },
366
364
  "peerDependencies": {
367
365
  "@modelcontextprotocol/sdk": "^1.31.0",
366
+ "agent-blackboard": "^0.6.0",
368
367
  "typescript": ">=5",
369
368
  "vitest": ">=5"
370
369
  },
371
370
  "peerDependenciesMeta": {
371
+ "agent-blackboard": {
372
+ "optional": true
373
+ },
372
374
  "@modelcontextprotocol/sdk": {
373
375
  "optional": true
374
376
  },
package/scripts/build.mjs CHANGED
@@ -34,3 +34,5 @@ await copyFile(
34
34
  new URL('../skills/manifest.json', import.meta.url),
35
35
  )
36
36
  await chmod(new URL('../dist/cli/index.mjs', import.meta.url), 0o755)
37
+ await chmod(new URL('../bin/vouchington.mjs', import.meta.url), 0o755)
38
+ await chmod(new URL('../bin/vouchington-mcp.mjs', import.meta.url), 0o755)
@@ -35,9 +35,71 @@ launch worktree by default). Writing or retrying an entry needs no provider skil
35
35
  `--file` flag, or replay command.
36
36
  Search for `journal_append` (by bare name, so every prefix matches) before concluding the server is
37
37
  unavailable; a deferred harness lists the tool only on request. If the search finds no such tool
38
- because the server is not registered, not connected, or not approved, stop and report that the
39
- journal server is unavailable, and name any per-user approval the harness still needs to load
40
- it (the consumer wrapper documents it). Do not fall back to a CLI command.
38
+ because the server is not registered or not connected, use the supported CLI fallback below.
39
+ Tell the human that MCP is unavailable and which CLI is being used. Record the fallback reason,
40
+ entrypoint, affected operations, and observed delivery status in the caller's journal before
41
+ continuing primary work. Do not treat a permission denial as tool unavailability or bypass a host
42
+ approval requirement through the CLI. If neither path persists the note, stop and report the blocker.
43
+
44
+ ## CLI fallback
45
+
46
+ Use a trusted machine-installed `vouchington` executable at a verified absolute path outside the
47
+ consumer worktree. Resolve its symlinks and verify the installation's source and version before
48
+ using credentials or requesting an unsandboxed run; its runtime dependencies must also belong to
49
+ that trusted installation. The CLI resolves its client relative to its own installation; installing
50
+ `agent-blackboard` only in the consumer worktree is insufficient for CLI fallback. Do not resolve the executable through the consumer's `node_modules/.bin`,
51
+ `pnpm exec`, or repository-controlled `PATH`. Set `BLACKBOARD_CLI` below to that verified absolute
52
+ script path, and bind any approval request to the same interpreter, script, and arguments.
53
+ Never execute the script directly: its `#!/usr/bin/env node` line would pick whichever `node` comes
54
+ first on `PATH`, which a consumer worktree can control. Set `BLACKBOARD_NODE` to a verified
55
+ absolute Node 24+ binary outside the consumer worktree, and run it with `env -i` so only `HOME`, a
56
+ fixed system `PATH`, and the two blackboard variables reach it. That also drops `NODE_OPTIONS` and
57
+ other loader variables. If no trusted CLI is
58
+ available, report the blocker; do not escalate a consumer-supplied executable. For local development,
59
+ the reviewed source CLI may run inside the sandbox, subject to the harness's credential policy.
60
+ `journal append` invokes the same validated writer as MCP, ensures the exact session identity,
61
+ patches its repository union, and verifies delivery. Use a temporary `0600` markdown input file,
62
+ remove it after the call, and inspect the JSON result: exit zero can mean retained pending feedback.
63
+ The input file is transport to the supported writer, never a replacement journal. Append returns
64
+ `pendingCount` for the caller and `worktreePendingCount` for the whole outbox. Use `journal status`
65
+ or pass `--session-id` to `journal flush` for the same two counts; an unfiltered legacy flush reports
66
+ only the whole-outbox count.
67
+
68
+ ```bash
69
+ blackboard() {
70
+ env -i HOME="$HOME" PATH=/usr/bin:/bin \
71
+ AGENT_BLACKBOARD_URL="$AGENT_BLACKBOARD_URL" \
72
+ AGENT_BLACKBOARD_TOKEN="$AGENT_BLACKBOARD_TOKEN" \
73
+ "$BLACKBOARD_NODE" "$BLACKBOARD_CLI" agent-blackboard "$@"
74
+ }
75
+ blackboard journal append \
76
+ --session-id SESSION --agent AGENT --version VERSION \
77
+ --mode interactive --source-event-id EVENT --work-outcome unknown \
78
+ --repository owner/name --coverage-status not-assessed --dropped-count 0 \
79
+ --outbox-directory "$PWD/.local/blackboard-outbox" --file NOTE
80
+ blackboard journal entries --session-id SESSION
81
+ blackboard journal status \
82
+ --session-id SESSION --outbox-directory "$PWD/.local/blackboard-outbox"
83
+ blackboard journal flush \
84
+ --session-id SESSION --outbox-directory "$PWD/.local/blackboard-outbox"
85
+ ```
86
+
87
+ Pass `--parent-session-id` for a child, repeat `--repository` for additional repositories, and use
88
+ explicit coverage sources when assessed. Omit `--timestamp` so the writer owns time. Autonomous
89
+ mode omits `--outbox-directory` and still requires fresh online admission plus readback.
90
+ For session-only or snapshot operations not exposed by this CLI, use the upstream
91
+ `agent-blackboard` CLI under its skill's operation contract; preserve exact identities and tags.
92
+ A sandboxed fallback may only write the outbox: the machine's harness sandbox can withhold
93
+ `AGENT_BLACKBOARD_TOKEN`, so it cannot deliver or read back. If delivery or readback needs the
94
+ credentials and the sandbox withholds them, run that one command unsandboxed through the harness's
95
+ normal approval path for running one command outside the sandbox, with each such run approved
96
+ individually. Never read, print, copy, or export the token
97
+ to work around the sandbox. If approval is denied, leave the record pending in the outbox, report
98
+ `pendingCount`, and tell the human.
99
+ Return to MCP when available and journal recovery. Never silently downgrade envelope validation,
100
+ retention, or delivery checks because the transport changed.
101
+
102
+ ## Journaling policy
41
103
 
42
104
  1. Capture consequential observations before filing an issue: failed or recovered checks, denied or
43
105
  approved permissions, repeated fixes, scope changes, first-party tool behavior, and architectural
@@ -65,7 +127,7 @@ it (the consumer wrapper documents it). Do not fall back to a CLI command.
65
127
  `journal_append` reports it as pending. Report `pendingCount` (this session's unsent records),
66
128
  and `worktreePendingCount` when other sessions' records are also unsent, from `journal_append`
67
129
  or `outbox_status`. Deliver with `outbox_flush` once the provider is reachable, and continue
68
- primary work only after persistence succeeds. A full, unsafe, or unwritable outbox blocks
130
+ primary work only after persistence succeeds. A full, corrupt, or unwritable outbox blocks
69
131
  capture; never evict or silently discard an unsent record.
70
132
  5. Autonomous runners require online `session_ensure`, a fresh admission `journal_append`, and
71
133
  readback before launching an attempt. Give each attempt's admission entry a `sourceEventId`