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,98 @@
1
+ import ts from '../contract-schema/typescript-api.mjs';
2
+ import { runtimeParameters } from './registered-route-runtime-parameters.mjs';
3
+ import { handlerNodes } from './registered-route-handler-analysis.mjs';
4
+ /** Proves a terminal 405 on the handler's own context, rather than finding a nested throw. */
5
+ export function isTerminal405Handler(node, checker, bindings = new Map()) {
6
+ if (!(ts.isArrowFunction(node) ||
7
+ ts.isFunctionExpression(node) ||
8
+ ts.isFunctionDeclaration(node) ||
9
+ ts.isMethodDeclaration(node)) ||
10
+ !node.body)
11
+ return false;
12
+ const context = runtimeParameters(node)[0]?.name;
13
+ const symbol = context && ts.isIdentifier(context) ? checker.getSymbolAtLocation(context) : undefined;
14
+ return !!symbol && terminalBody(node, symbol, checker, new Set(), true, bindings);
15
+ }
16
+ function terminalBody(node, context, checker, active, returned = true, bindings = new Map()) {
17
+ if (!node.body || node.asteriskToken || active.has(node))
18
+ return false;
19
+ if (node.parameters.some((parameter) => !ts.isIdentifier(parameter.name) ||
20
+ parameter.dotDotDotToken ||
21
+ (parameter.initializer && !ts.isLiteralExpression(parameter.initializer))))
22
+ return false;
23
+ const next = new Set(active).add(node);
24
+ if (!ts.isBlock(node.body))
25
+ return terminalCall(node.body, context, checker, next, returned, bindings);
26
+ const statements = node.body.statements;
27
+ const terminal = statements.at(-1);
28
+ if (!terminal ||
29
+ !statements.slice(0, -1).every((statement) => isInertStatement(statement, context, checker)))
30
+ return false;
31
+ if (ts.isExpressionStatement(terminal))
32
+ return terminalCall(terminal.expression, context, checker, next, false, bindings);
33
+ return (ts.isReturnStatement(terminal) &&
34
+ !!terminal.expression &&
35
+ terminalCall(terminal.expression, context, checker, next, returned, bindings));
36
+ }
37
+ function terminalCall(expression, context, checker, active, returned, bindings) {
38
+ if (ts.isAwaitExpression(expression))
39
+ return terminalCall(expression.expression, context, checker, active, true, bindings);
40
+ if (ts.isParenthesizedExpression(expression))
41
+ return terminalCall(expression.expression, context, checker, active, returned, bindings);
42
+ if (!ts.isCallExpression(expression) || expression.questionDotToken)
43
+ return false;
44
+ if (expression.arguments.some((argument) => !ts.isIdentifier(argument) && !ts.isLiteralExpression(argument)))
45
+ return false;
46
+ const callee = expression.expression;
47
+ if (ts.isPropertyAccessExpression(callee) && callee.name.text === 'throw') {
48
+ const status = expression.arguments[0];
49
+ return (!callee.questionDotToken &&
50
+ ts.isIdentifier(callee.expression) &&
51
+ checker.getSymbolAtLocation(callee.expression) === context &&
52
+ !!status &&
53
+ ts.isNumericLiteral(status) &&
54
+ Number(status.text) === 405);
55
+ }
56
+ if (!ts.isIdentifier(callee))
57
+ return false;
58
+ const implementations = [];
59
+ handlerNodes(callee, checker, bindings, new Set(), true, implementations);
60
+ if (!returned && checker.getPropertyOfType(checker.getTypeAtLocation(expression), 'then'))
61
+ return false;
62
+ return (implementations.length > 0 &&
63
+ implementations.every(({ node: implementation, bindings: captured }) => {
64
+ if (!returned &&
65
+ implementation.modifiers?.some((modifier) => modifier.kind === ts.SyntaxKind.AsyncKeyword))
66
+ return false;
67
+ const index = expression.arguments.findIndex((argument) => ts.isIdentifier(argument) && checker.getSymbolAtLocation(argument) === context);
68
+ const parameter = runtimeParameters(implementation)[index]?.name;
69
+ const symbol = parameter && ts.isIdentifier(parameter) ? checker.getSymbolAtLocation(parameter) : undefined;
70
+ return !!symbol && terminalBody(implementation, symbol, checker, active, returned, captured);
71
+ }));
72
+ }
73
+ function isInertStatement(statement, context, checker) {
74
+ if (ts.isEmptyStatement(statement))
75
+ return true;
76
+ if (ts.isExpressionStatement(statement)) {
77
+ const expression = statement.expression;
78
+ if (ts.isStringLiteral(expression))
79
+ return true;
80
+ if (!ts.isCallExpression(expression) || expression.arguments.length !== 2)
81
+ return false;
82
+ const callee = expression.expression;
83
+ const [header, value] = expression.arguments;
84
+ return (ts.isPropertyAccessExpression(callee) &&
85
+ callee.name.text === 'set' &&
86
+ ts.isIdentifier(callee.expression) &&
87
+ checker.getSymbolAtLocation(callee.expression) === context &&
88
+ ts.isStringLiteral(header) &&
89
+ header.text.toLowerCase() === 'allow' &&
90
+ ts.isStringLiteral(value) &&
91
+ /^[A-Z]+(?:, *[A-Z]+)*$/.test(value.text));
92
+ }
93
+ if (!ts.isVariableStatement(statement))
94
+ return false;
95
+ return statement.declarationList.declarations.every((declaration) => ts.isIdentifier(declaration.name) &&
96
+ checker.getSymbolAtLocation(declaration.name) !== context &&
97
+ (!declaration.initializer || ts.isLiteralExpression(declaration.initializer)));
98
+ }
@@ -1,4 +1,5 @@
1
1
  import ts from '../contract-schema/typescript-api.mjs';
2
+ import { registerPlatformCompilerLibraries } from './protocol-platform-callbacks.mjs';
2
3
  import { extractContractSchema } from '../contract-schema/index.mjs';
3
4
  import { registerRequestRouteContract, } from './request-contract-lenient.mjs';
4
5
  import { discoverImplicitRequestContract } from './request-contract-implicit.mjs';
@@ -14,6 +15,7 @@ import { collectHandlerBindings, enclosingRouteBinding, visit, } from './respons
14
15
  * `apiNoRequestBody` suppression) so Phase 2 knows to never harvest over it.
15
16
  */
16
17
  export function discoverApiRequestContracts(program, sourceFiles, requestedKeys, options) {
18
+ registerPlatformCompilerLibraries(program);
17
19
  const checker = program.getTypeChecker();
18
20
  const contracts = new Map();
19
21
  const markerKeys = new Set();
@@ -6,11 +6,34 @@ import { isXmlResponseCall, streamingTextMediaType } from './response-contract-m
6
6
  import { markBufferedRouteUnavailable, registerRouteContract, } from './response-contract-lenient.mjs';
7
7
  import { enclosingRouteBinding, isContextMethod, isContextResponseBufferCall, isContextResponseEmptyCall, requestedKeyForBinding, responseMarker, } from './response-contract-route-analysis.mjs';
8
8
  import { resolveEmissionStatus } from './response-contract-status.mjs';
9
+ import { opaqueHttpResponse, unsupportedContextResponse, supportedResponseContext, } from './protocol-http-association.mjs';
10
+ import { contextResponseMethod } from './protocol-http-context.mjs';
11
+ import { taintRouteKeys, markRouteTaint, isSseSetter } from './protocol-http-implicit-taint.mjs';
9
12
  export function discoverImplicitContract(call, checker, sourceFile, contracts, handlerBindings, requestedKeys, options) {
10
- const binding = enclosingRouteBinding(call, checker, handlerBindings);
13
+ const opaqueResponse = opaqueHttpResponse(call, checker);
14
+ const binding = enclosingRouteBinding(call, checker, handlerBindings, !opaqueResponse);
11
15
  if (!binding)
12
16
  return;
13
17
  const key = requestedKeyForBinding(binding, requestedKeys);
18
+ const taintKeys = taintRouteKeys(contracts, binding, key);
19
+ if (!taintKeys.length)
20
+ return;
21
+ const context = supportedResponseContext(call, checker);
22
+ const bracketResponse = context &&
23
+ (ts.isElementAccessExpression(call.expression) ||
24
+ (ts.isPropertyAccessExpression(call.expression) &&
25
+ ts.isElementAccessExpression(call.expression.expression))) &&
26
+ contextResponseMethod(call.expression, context, checker);
27
+ const mutableResponse = unsupportedContextResponse(call, checker);
28
+ const sseSetter = isSseSetter(contracts, binding, bracketResponse || undefined);
29
+ if (opaqueResponse ||
30
+ mutableResponse ||
31
+ (bracketResponse && !sseSetter && !containsResponseMarker(call))) {
32
+ markRouteTaint(contracts, taintKeys, binding, sourceLocation(sourceFile, call), !!mutableResponse &&
33
+ !!context &&
34
+ !!contextResponseMethod(call.expression, context, checker, true));
35
+ return;
36
+ }
14
37
  if (!key)
15
38
  return;
16
39
  const body = responseBodyExpression(call);
@@ -30,17 +53,13 @@ export function discoverImplicitContract(call, checker, sourceFile, contracts, h
30
53
  mediaTypeKnowledge: 'known',
31
54
  ...status,
32
55
  };
33
- // Primary body (bare key, or requested key in subset mode) uses the lenient path, so a
34
- // genuine single-variant route still reports a real `unavailableReason` on failure.
56
+ // Keep primary extraction failures unavailable in both full and subset discovery.
35
57
  if (requestedKeys || !contracts.has(key)) {
36
58
  registerRouteContract(contracts, key, binding, location, emission, extract, options);
37
59
  return;
38
60
  }
39
- // Full-scope discovery: a route can emit more than one distinct unmarked success body across
40
- // branches (e.g. a webhook's `{received}` vs `{ignored}`); register each under its own key —
41
- // `groupContractsByRoute` merges by method/routeTemplate, not this key. This route already has
42
- // a primary contract, so retain this one under a deterministic variant key. A failed secondary
43
- // extraction is registered as unavailable so the operation never silently omits a real branch.
61
+ // Preserve every additional unmarked body under a deterministic variant key, including
62
+ // unavailable secondary extractions. Route grouping merges these without hiding a branch.
44
63
  registerRouteContract(contracts, nextImplicitVariantKey(contracts, key), binding, location, emission, extract, options);
45
64
  return;
46
65
  }
@@ -72,8 +91,7 @@ export function discoverImplicitContract(call, checker, sourceFile, contracts, h
72
91
  }, () => extractContractSchema(checker.getStringType(), checker, location, options), options);
73
92
  return;
74
93
  }
75
- // Empty and raw branches can coexist; AST order must not let empty hide an unmarked raw body,
76
- // so every buffer or pipeline without an explicit response marker makes the route unavailable.
94
+ // Empty branches must not hide any unmarked buffer or pipeline body.
77
95
  if (isContextResponseBufferCall(call.expression) ||
78
96
  (isContextMethod(call.expression, 'pipeline') && !body && !containsResponseMarker(call))) {
79
97
  markBufferedRouteUnavailable(contracts, key, binding, sourceLocation(sourceFile, call));
@@ -1,13 +1,24 @@
1
1
  import ts from '../contract-schema/typescript-api.mjs';
2
+ import { registerPlatformCompilerLibraries } from './protocol-platform-callbacks.mjs';
3
+ import { requestedProtocolKey } from './protocol-requested-keys.mjs';
4
+ import { discoverProtocolContracts } from './protocol-contract-registry.mjs';
2
5
  import { discoverImplicitContract } from './response-contract-implicit.mjs';
3
6
  import { registerRouteContract, } from './response-contract-lenient.mjs';
4
7
  import { binaryContentContract, contractError, extractBodyContract, noContentContract, sourceLocation, } from './response-contract-registration.mjs';
5
8
  import { collectHandlerBindings, enclosingRouteBinding, isContextMethod, responseMarker, visit, } from './response-contract-route-analysis.mjs';
6
9
  import { resolveEmissionStatus } from './response-contract-status.mjs';
7
10
  export function discoverApiResponseContracts(program, sourceFiles, requestedKeys, options) {
11
+ registerPlatformCompilerLibraries(program);
8
12
  const checker = program.getTypeChecker();
9
13
  const contracts = new Map();
10
14
  const handlerBindings = collectHandlerBindings(sourceFiles, checker);
15
+ const protocolContracts = new Map();
16
+ const protocolEmissions = discoverProtocolContracts(sourceFiles, checker, handlerBindings, protocolContracts, options, requestedKeys);
17
+ for (const [key, contract] of protocolContracts) {
18
+ const requestedKey = requestedProtocolKey(key, contract, requestedKeys);
19
+ if (requestedKey)
20
+ contracts.set(requestedKey, contract);
21
+ }
11
22
  for (const sourceFile of sourceFiles) {
12
23
  visit(sourceFile, (node) => {
13
24
  if (!ts.isCallExpression(node))
@@ -47,7 +58,9 @@ export function discoverApiResponseContracts(program, sourceFiles, requestedKeys
47
58
  }
48
59
  for (const sourceFile of sourceFiles) {
49
60
  visit(sourceFile, (node) => {
50
- if (!ts.isCallExpression(node) || responseMarker(node.expression))
61
+ if (!ts.isCallExpression(node) ||
62
+ responseMarker(node.expression) ||
63
+ protocolEmissions.has(node))
51
64
  return;
52
65
  discoverImplicitContract(node, checker, sourceFile, contracts, handlerBindings, requestedKeys, options);
53
66
  });
@@ -6,7 +6,7 @@ export type RouteBinding = {
6
6
  };
7
7
  export type HandlerBindings = Map<ts.Symbol, RouteBinding>;
8
8
  export declare function requestedKeyForBinding(binding: RouteBinding, requestedKeys?: ReadonlySet<string>): string | undefined;
9
- export declare function enclosingRouteBinding(node: ts.Node, checker: ts.TypeChecker, handlerBindings: HandlerBindings): RouteBinding | undefined;
9
+ export declare function enclosingRouteBinding(node: ts.Node, checker: ts.TypeChecker, handlerBindings: HandlerBindings, proveCallbacks?: boolean): RouteBinding | undefined;
10
10
  export declare function collectHandlerBindings(sourceFiles: readonly ts.SourceFile[], checker: ts.TypeChecker): HandlerBindings;
11
11
  export declare function routeTemplateFromExpression(expression: ts.Expression): string | undefined;
12
12
  export declare function propertyName(expression: ts.Expression): string | undefined;
@@ -1,4 +1,5 @@
1
1
  import ts from '../contract-schema/typescript-api.mjs';
2
+ import { executableProtocolPath } from './protocol-execution-path.mjs';
2
3
  export { isContextMethod, isContextResponseBufferCall, isContextResponseEmptyCall, responseMarker, } from './response-contract-ctx-analysis.mjs';
3
4
  const HTTP_METHODS = new Set(['GET', 'POST', 'PUT', 'PATCH', 'DELETE']);
4
5
  export function requestedKeyForBinding(binding, requestedKeys) {
@@ -19,23 +20,38 @@ export function requestedKeyForBinding(binding, requestedKeys) {
19
20
  function routeShape(routeTemplate) {
20
21
  return routeTemplate.replace(/:[^/]+/g, ':');
21
22
  }
22
- export function enclosingRouteBinding(node, checker, handlerBindings) {
23
+ export function enclosingRouteBinding(node, checker, handlerBindings, proveCallbacks = true) {
23
24
  let current = node;
25
+ let insideHandlerFunction = false;
24
26
  while (current) {
27
+ if (ts.isFunctionLike(current))
28
+ insideHandlerFunction = true;
29
+ if (insideHandlerFunction && ts.isCallExpression(current)) {
30
+ const method = propertyName(current.expression)?.toUpperCase();
31
+ const routeTemplate = routeTemplateFromExpression(current.expression);
32
+ if (method && HTTP_METHODS.has(method) && routeTemplate)
33
+ return !proveCallbacks || executableProtocolPath(node, checker)
34
+ ? { method, routeTemplate }
35
+ : undefined;
36
+ }
25
37
  if (isFunctionLike(current) && ts.isCallExpression(current.parent)) {
26
38
  const handlerCall = current.parent;
27
39
  const method = propertyName(handlerCall.expression)?.toUpperCase();
28
40
  if (method && HTTP_METHODS.has(method)) {
29
41
  const routeTemplate = routeTemplateFromExpression(handlerCall.expression);
30
42
  if (routeTemplate)
31
- return { method, routeTemplate };
43
+ return !proveCallbacks || executableProtocolPath(node, checker)
44
+ ? { method, routeTemplate }
45
+ : undefined;
32
46
  }
33
47
  }
34
48
  const handlerSymbol = functionSymbol(current, checker);
35
49
  if (handlerSymbol) {
36
50
  const binding = handlerBindings.get(resolveSymbol(handlerSymbol, checker));
37
51
  if (binding)
38
- return binding;
52
+ return !proveCallbacks || executableProtocolPath(node, checker, current)
53
+ ? binding
54
+ : undefined;
39
55
  }
40
56
  current = current.parent;
41
57
  }
@@ -93,7 +109,9 @@ function handlerArgumentSymbols(node, checker) {
93
109
  }
94
110
  if (isFunctionLike(argument)) {
95
111
  visit(argument, (child) => {
96
- if (!ts.isCallExpression(child) || !ts.isIdentifier(child.expression))
112
+ if (!ts.isCallExpression(child) ||
113
+ !ts.isIdentifier(child.expression) ||
114
+ !executableProtocolPath(child, checker))
97
115
  return;
98
116
  const symbol = checker.getSymbolAtLocation(child.expression);
99
117
  if (symbol)
@@ -20,7 +20,7 @@ export declare function responseStatusCodesForContract(contract: BackendResponse
20
20
  * and the route-wide map (keyed only on `method:routeTemplate`) can't tell two markers in the
21
21
  * same route apart when they're preceded by different statuses.
22
22
  */
23
- export declare function resolveEmissionStatus(call: ts.CallExpression): {
23
+ export declare function resolveEmissionStatus(call: ts.CallExpression | ts.NewExpression, matchesContext?: (expression: ts.Expression) => boolean): {
24
24
  statusCodes?: readonly [number, ...number[]];
25
25
  statusKnowledge: 'default' | 'explicit' | 'unknown';
26
26
  unavailableReason?: string;
@@ -27,32 +27,34 @@ export function responseStatusCodesForContract(contract) {
27
27
  * and the route-wide map (keyed only on `method:routeTemplate`) can't tell two markers in the
28
28
  * same route apart when they're preceded by different statuses.
29
29
  */
30
- export function resolveEmissionStatus(call) {
31
- if (isContextMethod(call.expression, 'setStatus'))
30
+ export function resolveEmissionStatus(call, matchesContext = (expression) => isContextMethod(expression, 'setStatus')) {
31
+ if (ts.isCallExpression(call) && matchesContext(call.expression))
32
32
  return statusArgument(call.arguments[0]);
33
33
  let statement = call;
34
34
  while (!ts.isStatement(statement))
35
35
  statement = statement.parent;
36
- return precedingStatus(statement);
36
+ return precedingStatus(statement, matchesContext);
37
37
  }
38
- function precedingStatus(statement) {
38
+ function precedingStatus(statement, matchesContext) {
39
39
  const block = statement.parent;
40
40
  if (!ts.isBlock(block))
41
41
  return { statusKnowledge: 'default' };
42
42
  const index = block.statements.indexOf(statement);
43
43
  for (let i = index - 1; i >= 0; i--) {
44
- const status = statusStatement(block.statements[i]);
44
+ const status = statusStatement(block.statements[i], matchesContext);
45
45
  if (status)
46
46
  return status;
47
47
  }
48
48
  const enclosing = block.parent;
49
- return ts.isStatement(enclosing) ? precedingStatus(enclosing) : { statusKnowledge: 'default' };
49
+ return ts.isStatement(enclosing)
50
+ ? precedingStatus(enclosing, matchesContext)
51
+ : { statusKnowledge: 'default' };
50
52
  }
51
- function statusStatement(statement) {
53
+ function statusStatement(statement, matchesContext) {
52
54
  if (!ts.isExpressionStatement(statement))
53
55
  return undefined;
54
56
  const expression = statement.expression;
55
- if (!ts.isCallExpression(expression) || !isContextMethod(expression.expression, 'setStatus'))
57
+ if (!ts.isCallExpression(expression) || !matchesContext(expression.expression))
56
58
  return undefined;
57
59
  return statusArgument(expression.arguments[0]);
58
60
  }
@@ -11,6 +11,13 @@ export type BackendResponseContract = ExtractedResponseContract & {
11
11
  mediaType?: string;
12
12
  /** Raw buffers and arbitrary streams have an unknown media type; bodyless emissions have none. */
13
13
  mediaTypeKnowledge?: 'known' | 'none' | 'unknown';
14
+ /** Named SSE frames carried by the string stream body. */
15
+ sseEvents?: readonly {
16
+ eventName: string;
17
+ contract: ExtractedResponseContract;
18
+ }[];
19
+ /** A concrete error status can also be produced by the framework before dispatch. */
20
+ includeDefaultError?: boolean;
14
21
  /** Set when the route's response schema could not be extracted (lenient discovery mode). */
15
22
  unavailableReason?: string;
16
23
  };
@@ -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>;
@@ -2,7 +2,6 @@
2
2
  import { realpathSync } from 'node:fs';
3
3
  import { resolve } from 'node:path';
4
4
  import { pathToFileURL } from 'node:url';
5
- import { runAgentHarnessConfigCli } from '../agent-harness-config/cli.mjs';
6
5
  import { runPostReviewCli } from '../gha-post-review/cli.mjs';
7
6
  import { runStageReviewPayloadCli } from '../gha-review-payload/cli.mjs';
8
7
  import { runSwiftSemanticEqualCli } from '../swift-semantic-equal/cli.mjs';
@@ -98,8 +97,6 @@ export function runCli(argv = process.argv) {
98
97
  return runRunnerPortPolicy(parsed);
99
98
  case 'with-host-lock':
100
99
  return runWithHostLock(parsed.args);
101
- case 'agent-harness-config':
102
- return runAgentHarnessConfigCli(parsed.args);
103
100
  case 'gha-runtime-audit':
104
101
  return runGhaRuntimeAudit(parsed);
105
102
  case 'script': {
@@ -162,17 +159,17 @@ export function isMainModule(metaUrl, argv1) {
162
159
  return metaUrl === pathToFileURL(resolve(argv1)).href;
163
160
  }
164
161
  }
165
- /* v8 ignore next 8 */
166
- if (isMainModule(import.meta.url, process.argv[1])) {
167
- const result = runCli();
168
- if (typeof result === 'number')
169
- process.exitCode = result;
170
- else {
171
- result.then((code) => {
172
- process.exitCode = code;
173
- }, (error) => {
174
- process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
175
- process.exitCode = 1;
176
- });
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;
177
170
  }
178
171
  }
172
+ /* v8 ignore next 3 */
173
+ if (isMainModule(import.meta.url, process.argv[1])) {
174
+ void runMain();
175
+ }
@@ -1,9 +1,6 @@
1
1
  type ForwardedCommand = {
2
2
  kind: 'with-host-lock';
3
3
  args: string[];
4
- } | {
5
- kind: 'agent-harness-config';
6
- args: string[];
7
4
  } | {
8
5
  kind: 'pnpm-install';
9
6
  args: string[];
@@ -1,8 +1,6 @@
1
1
  export function parseForwardedCommand(command, args) {
2
2
  if (command === 'with-host-lock')
3
3
  return { kind: 'with-host-lock', args };
4
- if (command === 'agent-harness-config')
5
- return { kind: 'agent-harness-config', args };
6
4
  if (command === 'pnpm-install')
7
5
  return { kind: 'pnpm-install', args };
8
6
  if (command === 'vitest-blob-manifest')
@@ -17,9 +17,6 @@ export type ParsedCli = {
17
17
  } | {
18
18
  kind: 'with-host-lock';
19
19
  args: string[];
20
- } | {
21
- kind: 'agent-harness-config';
22
- args: string[];
23
20
  } | {
24
21
  kind: 'script';
25
22
  command: ScriptCommand;