@wopr-network/defcon 0.2.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 (243) hide show
  1. package/README.md +274 -0
  2. package/dist/api/router.d.ts +24 -0
  3. package/dist/api/router.js +44 -0
  4. package/dist/api/server.d.ts +13 -0
  5. package/dist/api/server.js +280 -0
  6. package/dist/api/wire-types.d.ts +46 -0
  7. package/dist/api/wire-types.js +5 -0
  8. package/dist/config/db-path.d.ts +1 -0
  9. package/dist/config/db-path.js +1 -0
  10. package/dist/config/exporter.d.ts +3 -0
  11. package/dist/config/exporter.js +87 -0
  12. package/dist/config/index.d.ts +4 -0
  13. package/dist/config/index.js +4 -0
  14. package/dist/config/seed-loader.d.ts +10 -0
  15. package/dist/config/seed-loader.js +108 -0
  16. package/dist/config/zod-schemas.d.ts +165 -0
  17. package/dist/config/zod-schemas.js +283 -0
  18. package/dist/cors.d.ts +8 -0
  19. package/dist/cors.js +21 -0
  20. package/dist/engine/constants.d.ts +1 -0
  21. package/dist/engine/constants.js +1 -0
  22. package/dist/engine/engine.d.ts +69 -0
  23. package/dist/engine/engine.js +485 -0
  24. package/dist/engine/event-emitter.d.ts +9 -0
  25. package/dist/engine/event-emitter.js +19 -0
  26. package/dist/engine/event-types.d.ts +105 -0
  27. package/dist/engine/event-types.js +1 -0
  28. package/dist/engine/flow-spawner.d.ts +8 -0
  29. package/dist/engine/flow-spawner.js +28 -0
  30. package/dist/engine/gate-command-validator.d.ts +6 -0
  31. package/dist/engine/gate-command-validator.js +46 -0
  32. package/dist/engine/gate-evaluator.d.ts +12 -0
  33. package/dist/engine/gate-evaluator.js +233 -0
  34. package/dist/engine/handlebars.d.ts +9 -0
  35. package/dist/engine/handlebars.js +51 -0
  36. package/dist/engine/index.d.ts +12 -0
  37. package/dist/engine/index.js +7 -0
  38. package/dist/engine/invocation-builder.d.ts +18 -0
  39. package/dist/engine/invocation-builder.js +58 -0
  40. package/dist/engine/on-enter.d.ts +8 -0
  41. package/dist/engine/on-enter.js +102 -0
  42. package/dist/engine/ssrf-guard.d.ts +22 -0
  43. package/dist/engine/ssrf-guard.js +159 -0
  44. package/dist/engine/state-machine.d.ts +12 -0
  45. package/dist/engine/state-machine.js +74 -0
  46. package/dist/execution/active-runner.d.ts +45 -0
  47. package/dist/execution/active-runner.js +165 -0
  48. package/dist/execution/admin-schemas.d.ts +116 -0
  49. package/dist/execution/admin-schemas.js +125 -0
  50. package/dist/execution/cli.d.ts +57 -0
  51. package/dist/execution/cli.js +498 -0
  52. package/dist/execution/handlers/admin.d.ts +67 -0
  53. package/dist/execution/handlers/admin.js +200 -0
  54. package/dist/execution/handlers/flow.d.ts +25 -0
  55. package/dist/execution/handlers/flow.js +289 -0
  56. package/dist/execution/handlers/query.d.ts +31 -0
  57. package/dist/execution/handlers/query.js +64 -0
  58. package/dist/execution/index.d.ts +4 -0
  59. package/dist/execution/index.js +3 -0
  60. package/dist/execution/mcp-helpers.d.ts +42 -0
  61. package/dist/execution/mcp-helpers.js +23 -0
  62. package/dist/execution/mcp-server.d.ts +33 -0
  63. package/dist/execution/mcp-server.js +1020 -0
  64. package/dist/execution/provision-worktree.d.ts +16 -0
  65. package/dist/execution/provision-worktree.js +123 -0
  66. package/dist/execution/tool-schemas.d.ts +40 -0
  67. package/dist/execution/tool-schemas.js +44 -0
  68. package/dist/gates/blocking-graph.d.ts +26 -0
  69. package/dist/gates/blocking-graph.js +102 -0
  70. package/dist/gates/test/bad-return-gate.d.ts +1 -0
  71. package/dist/gates/test/bad-return-gate.js +4 -0
  72. package/dist/gates/test/passing-gate.d.ts +2 -0
  73. package/dist/gates/test/passing-gate.js +3 -0
  74. package/dist/gates/test/slow-gate.d.ts +2 -0
  75. package/dist/gates/test/slow-gate.js +5 -0
  76. package/dist/gates/test/throwing-gate.d.ts +1 -0
  77. package/dist/gates/test/throwing-gate.js +3 -0
  78. package/dist/logger.d.ts +8 -0
  79. package/dist/logger.js +12 -0
  80. package/dist/main.d.ts +14 -0
  81. package/dist/main.js +28 -0
  82. package/dist/repositories/drizzle/entity.repo.d.ts +27 -0
  83. package/dist/repositories/drizzle/entity.repo.js +190 -0
  84. package/dist/repositories/drizzle/event.repo.d.ts +12 -0
  85. package/dist/repositories/drizzle/event.repo.js +24 -0
  86. package/dist/repositories/drizzle/flow.repo.d.ts +22 -0
  87. package/dist/repositories/drizzle/flow.repo.js +364 -0
  88. package/dist/repositories/drizzle/gate.repo.d.ts +16 -0
  89. package/dist/repositories/drizzle/gate.repo.js +98 -0
  90. package/dist/repositories/drizzle/index.d.ts +6 -0
  91. package/dist/repositories/drizzle/index.js +7 -0
  92. package/dist/repositories/drizzle/invocation.repo.d.ts +23 -0
  93. package/dist/repositories/drizzle/invocation.repo.js +199 -0
  94. package/dist/repositories/drizzle/schema.d.ts +1932 -0
  95. package/dist/repositories/drizzle/schema.js +155 -0
  96. package/dist/repositories/drizzle/transition-log.repo.d.ts +11 -0
  97. package/dist/repositories/drizzle/transition-log.repo.js +42 -0
  98. package/dist/repositories/interfaces.d.ts +321 -0
  99. package/dist/repositories/interfaces.js +2 -0
  100. package/dist/src/api/router.d.ts +24 -0
  101. package/dist/src/api/router.js +44 -0
  102. package/dist/src/api/server.d.ts +13 -0
  103. package/dist/src/api/server.js +280 -0
  104. package/dist/src/api/wire-types.d.ts +46 -0
  105. package/dist/src/api/wire-types.js +5 -0
  106. package/dist/src/config/db-path.d.ts +1 -0
  107. package/dist/src/config/db-path.js +1 -0
  108. package/dist/src/config/exporter.d.ts +3 -0
  109. package/dist/src/config/exporter.js +87 -0
  110. package/dist/src/config/index.d.ts +4 -0
  111. package/dist/src/config/index.js +4 -0
  112. package/dist/src/config/seed-loader.d.ts +14 -0
  113. package/dist/src/config/seed-loader.js +131 -0
  114. package/dist/src/config/zod-schemas.d.ts +165 -0
  115. package/dist/src/config/zod-schemas.js +283 -0
  116. package/dist/src/cors.d.ts +8 -0
  117. package/dist/src/cors.js +21 -0
  118. package/dist/src/engine/constants.d.ts +1 -0
  119. package/dist/src/engine/constants.js +1 -0
  120. package/dist/src/engine/engine.d.ts +69 -0
  121. package/dist/src/engine/engine.js +485 -0
  122. package/dist/src/engine/event-emitter.d.ts +9 -0
  123. package/dist/src/engine/event-emitter.js +19 -0
  124. package/dist/src/engine/event-types.d.ts +105 -0
  125. package/dist/src/engine/event-types.js +1 -0
  126. package/dist/src/engine/flow-spawner.d.ts +8 -0
  127. package/dist/src/engine/flow-spawner.js +28 -0
  128. package/dist/src/engine/gate-command-validator.d.ts +6 -0
  129. package/dist/src/engine/gate-command-validator.js +46 -0
  130. package/dist/src/engine/gate-evaluator.d.ts +12 -0
  131. package/dist/src/engine/gate-evaluator.js +233 -0
  132. package/dist/src/engine/handlebars.d.ts +9 -0
  133. package/dist/src/engine/handlebars.js +51 -0
  134. package/dist/src/engine/index.d.ts +12 -0
  135. package/dist/src/engine/index.js +7 -0
  136. package/dist/src/engine/invocation-builder.d.ts +18 -0
  137. package/dist/src/engine/invocation-builder.js +58 -0
  138. package/dist/src/engine/on-enter.d.ts +8 -0
  139. package/dist/src/engine/on-enter.js +102 -0
  140. package/dist/src/engine/ssrf-guard.d.ts +22 -0
  141. package/dist/src/engine/ssrf-guard.js +159 -0
  142. package/dist/src/engine/state-machine.d.ts +12 -0
  143. package/dist/src/engine/state-machine.js +74 -0
  144. package/dist/src/execution/active-runner.d.ts +45 -0
  145. package/dist/src/execution/active-runner.js +165 -0
  146. package/dist/src/execution/admin-schemas.d.ts +116 -0
  147. package/dist/src/execution/admin-schemas.js +125 -0
  148. package/dist/src/execution/cli.d.ts +57 -0
  149. package/dist/src/execution/cli.js +501 -0
  150. package/dist/src/execution/handlers/admin.d.ts +67 -0
  151. package/dist/src/execution/handlers/admin.js +200 -0
  152. package/dist/src/execution/handlers/flow.d.ts +25 -0
  153. package/dist/src/execution/handlers/flow.js +289 -0
  154. package/dist/src/execution/handlers/query.d.ts +31 -0
  155. package/dist/src/execution/handlers/query.js +64 -0
  156. package/dist/src/execution/index.d.ts +4 -0
  157. package/dist/src/execution/index.js +3 -0
  158. package/dist/src/execution/mcp-helpers.d.ts +42 -0
  159. package/dist/src/execution/mcp-helpers.js +23 -0
  160. package/dist/src/execution/mcp-server.d.ts +33 -0
  161. package/dist/src/execution/mcp-server.js +1020 -0
  162. package/dist/src/execution/provision-worktree.d.ts +16 -0
  163. package/dist/src/execution/provision-worktree.js +123 -0
  164. package/dist/src/execution/tool-schemas.d.ts +40 -0
  165. package/dist/src/execution/tool-schemas.js +44 -0
  166. package/dist/src/logger.d.ts +8 -0
  167. package/dist/src/logger.js +12 -0
  168. package/dist/src/main.d.ts +14 -0
  169. package/dist/src/main.js +28 -0
  170. package/dist/src/repositories/drizzle/entity.repo.d.ts +27 -0
  171. package/dist/src/repositories/drizzle/entity.repo.js +190 -0
  172. package/dist/src/repositories/drizzle/event.repo.d.ts +12 -0
  173. package/dist/src/repositories/drizzle/event.repo.js +24 -0
  174. package/dist/src/repositories/drizzle/flow.repo.d.ts +22 -0
  175. package/dist/src/repositories/drizzle/flow.repo.js +364 -0
  176. package/dist/src/repositories/drizzle/gate.repo.d.ts +16 -0
  177. package/dist/src/repositories/drizzle/gate.repo.js +98 -0
  178. package/dist/src/repositories/drizzle/index.d.ts +6 -0
  179. package/dist/src/repositories/drizzle/index.js +7 -0
  180. package/dist/src/repositories/drizzle/invocation.repo.d.ts +23 -0
  181. package/dist/src/repositories/drizzle/invocation.repo.js +199 -0
  182. package/dist/src/repositories/drizzle/schema.d.ts +1932 -0
  183. package/dist/src/repositories/drizzle/schema.js +155 -0
  184. package/dist/src/repositories/drizzle/transition-log.repo.d.ts +11 -0
  185. package/dist/src/repositories/drizzle/transition-log.repo.js +42 -0
  186. package/dist/src/repositories/interfaces.d.ts +321 -0
  187. package/dist/src/repositories/interfaces.js +2 -0
  188. package/dist/src/utils/redact.d.ts +2 -0
  189. package/dist/src/utils/redact.js +62 -0
  190. package/dist/utils/redact.d.ts +2 -0
  191. package/dist/utils/redact.js +62 -0
  192. package/drizzle/.gitkeep +0 -0
  193. package/drizzle/0000_simple_surge.sql +144 -0
  194. package/drizzle/0001_peaceful_marvel_apes.sql +18 -0
  195. package/drizzle/0002_add_invocations_created_at.sql +1 -0
  196. package/drizzle/0003_drop_integration_config.sql +1 -0
  197. package/drizzle/0004_add_flow_discipline.sql +2 -0
  198. package/drizzle/0004_lucky_silverclaw.sql +5 -0
  199. package/drizzle/0005_old_blue_shield.sql +2 -0
  200. package/drizzle/0006_solid_magik.sql +2 -0
  201. package/drizzle/0007_fancy_luke_cage.sql +1 -0
  202. package/drizzle/0008_thick_dark_beast.sql +1 -0
  203. package/drizzle/0009_brief_midnight.sql +1 -0
  204. package/drizzle/0010_amusing_bastion.sql +1 -0
  205. package/drizzle/meta/0000_snapshot.json +996 -0
  206. package/drizzle/meta/0004_snapshot.json +1008 -0
  207. package/drizzle/meta/0005_snapshot.json +1023 -0
  208. package/drizzle/meta/0006_snapshot.json +1037 -0
  209. package/drizzle/meta/0007_snapshot.json +1044 -0
  210. package/drizzle/meta/0008_snapshot.json +1051 -0
  211. package/drizzle/meta/0009_snapshot.json +1058 -0
  212. package/drizzle/meta/0010_snapshot.json +1065 -0
  213. package/drizzle/meta/_journal.json +83 -0
  214. package/gates/.gitkeep +0 -0
  215. package/gates/blocking-graph.d.ts +26 -0
  216. package/gates/blocking-graph.js +102 -0
  217. package/gates/blocking-graph.ts +121 -0
  218. package/gates/check-design-posted.sh +39 -0
  219. package/gates/check-merge.sh +51 -0
  220. package/gates/check-pr-capacity.sh +17 -0
  221. package/gates/check-review-ready.sh +47 -0
  222. package/gates/check-spec-posted.sh +34 -0
  223. package/gates/check-unblocked.sh +56 -0
  224. package/gates/ci-green.sh +9 -0
  225. package/gates/merge-queue.sh +14 -0
  226. package/gates/review-bots-ready.sh +9 -0
  227. package/gates/spec-posted.sh +31 -0
  228. package/gates/test/bad-return-gate.d.ts +1 -0
  229. package/gates/test/bad-return-gate.js +4 -0
  230. package/gates/test/bad-return-gate.ts +4 -0
  231. package/gates/test/passing-gate.d.ts +2 -0
  232. package/gates/test/passing-gate.js +3 -0
  233. package/gates/test/passing-gate.ts +5 -0
  234. package/gates/test/slow-gate.d.ts +2 -0
  235. package/gates/test/slow-gate.js +5 -0
  236. package/gates/test/slow-gate.ts +7 -0
  237. package/gates/test/throwing-gate.d.ts +1 -0
  238. package/gates/test/throwing-gate.js +3 -0
  239. package/gates/test/throwing-gate.ts +3 -0
  240. package/gates/test-fail.sh +2 -0
  241. package/gates/test-pass.sh +2 -0
  242. package/gates/timeout-gate-script.sh +3 -0
  243. package/package.json +64 -0
@@ -0,0 +1,67 @@
1
+ import type { McpServerDeps } from "../mcp-helpers.js";
2
+ export declare function handleAdminEntityCreate(deps: McpServerDeps, args: Record<string, unknown>): Promise<{
3
+ content: {
4
+ type: "text";
5
+ text: string;
6
+ }[];
7
+ }>;
8
+ export declare function handleAdminFlowCreate(deps: McpServerDeps, args: Record<string, unknown>): Promise<{
9
+ content: {
10
+ type: "text";
11
+ text: string;
12
+ }[];
13
+ }>;
14
+ export declare function handleAdminFlowUpdate(deps: McpServerDeps, args: Record<string, unknown>): Promise<{
15
+ content: {
16
+ type: "text";
17
+ text: string;
18
+ }[];
19
+ }>;
20
+ export declare function handleAdminStateCreate(deps: McpServerDeps, args: Record<string, unknown>): Promise<{
21
+ content: {
22
+ type: "text";
23
+ text: string;
24
+ }[];
25
+ }>;
26
+ export declare function handleAdminStateUpdate(deps: McpServerDeps, args: Record<string, unknown>): Promise<{
27
+ content: {
28
+ type: "text";
29
+ text: string;
30
+ }[];
31
+ }>;
32
+ export declare function handleAdminTransitionCreate(deps: McpServerDeps, args: Record<string, unknown>): Promise<{
33
+ content: {
34
+ type: "text";
35
+ text: string;
36
+ }[];
37
+ }>;
38
+ export declare function handleAdminTransitionUpdate(deps: McpServerDeps, args: Record<string, unknown>): Promise<{
39
+ content: {
40
+ type: "text";
41
+ text: string;
42
+ }[];
43
+ }>;
44
+ export declare function handleAdminGateCreate(deps: McpServerDeps, args: Record<string, unknown>): Promise<{
45
+ content: {
46
+ type: "text";
47
+ text: string;
48
+ }[];
49
+ }>;
50
+ export declare function handleAdminGateAttach(deps: McpServerDeps, args: Record<string, unknown>): Promise<{
51
+ content: {
52
+ type: "text";
53
+ text: string;
54
+ }[];
55
+ }>;
56
+ export declare function handleAdminFlowSnapshot(deps: McpServerDeps, args: Record<string, unknown>): Promise<{
57
+ content: {
58
+ type: "text";
59
+ text: string;
60
+ }[];
61
+ }>;
62
+ export declare function handleAdminFlowRestore(deps: McpServerDeps, args: Record<string, unknown>): Promise<{
63
+ content: {
64
+ type: "text";
65
+ text: string;
66
+ }[];
67
+ }>;
@@ -0,0 +1,200 @@
1
+ import { isTerminal } from "../../engine/state-machine.js";
2
+ import { AdminFlowCreateSchema, AdminFlowRestoreSchema, AdminFlowSnapshotSchema, AdminFlowUpdateSchema, AdminGateAttachSchema, AdminGateCreateSchema, AdminStateCreateSchema, AdminStateUpdateSchema, AdminTransitionCreateSchema, AdminTransitionUpdateSchema, } from "../admin-schemas.js";
3
+ import { emitDefinitionChanged, errorResult, jsonResult, validateInput } from "../mcp-helpers.js";
4
+ import { FlowSeedSchema } from "../tool-schemas.js";
5
+ export async function handleAdminEntityCreate(deps, args) {
6
+ const v = validateInput(FlowSeedSchema, args);
7
+ if (!v.ok)
8
+ return v.result;
9
+ const { flow: flowName, refs } = v.data;
10
+ if (!deps.engine) {
11
+ return errorResult("Engine not available — MCP server started without engine dependency");
12
+ }
13
+ const entity = await deps.engine.createEntity(flowName, refs);
14
+ const invocations = await deps.invocations.findByEntity(entity.id);
15
+ const activeInvocation = invocations.find((inv) => !inv.completedAt && !inv.failedAt);
16
+ const result = { ...entity };
17
+ if (activeInvocation) {
18
+ result.invocation_id = activeInvocation.id;
19
+ }
20
+ return jsonResult(result);
21
+ }
22
+ export async function handleAdminFlowCreate(deps, args) {
23
+ const v = validateInput(AdminFlowCreateSchema, args);
24
+ if (!v.ok)
25
+ return v.result;
26
+ const { states, ...flowInput } = v.data;
27
+ if (states !== undefined) {
28
+ const stateNames = states.map((s) => s.name);
29
+ if (!stateNames.includes(flowInput.initialState)) {
30
+ return errorResult(`initialState '${flowInput.initialState}' must be included in the states array`);
31
+ }
32
+ }
33
+ const flow = await deps.flows.create(flowInput);
34
+ for (const stateDef of states ?? []) {
35
+ await deps.flows.addState(flow.id, stateDef);
36
+ }
37
+ const fullFlow = await deps.flows.get(flow.id);
38
+ emitDefinitionChanged(deps.eventRepo, flow.id, "admin.flow.create", { name: flow.name });
39
+ return jsonResult(fullFlow);
40
+ }
41
+ export async function handleAdminFlowUpdate(deps, args) {
42
+ const v = validateInput(AdminFlowUpdateSchema, args);
43
+ if (!v.ok)
44
+ return v.result;
45
+ const { flow_name, ...changes } = v.data;
46
+ const flow = await deps.flows.getByName(flow_name);
47
+ if (!flow)
48
+ return errorResult(`Flow not found: ${flow_name}`);
49
+ await deps.flows.snapshot(flow.id);
50
+ const updated = await deps.flows.update(flow.id, changes);
51
+ emitDefinitionChanged(deps.eventRepo, flow.id, "admin.flow.update", { name: flow_name, changes });
52
+ return jsonResult(updated);
53
+ }
54
+ export async function handleAdminStateCreate(deps, args) {
55
+ const v = validateInput(AdminStateCreateSchema, args);
56
+ if (!v.ok)
57
+ return v.result;
58
+ const { flow_name, ...stateInput } = v.data;
59
+ const flow = await deps.flows.getByName(flow_name);
60
+ if (!flow)
61
+ return errorResult(`Flow not found: ${flow_name}`);
62
+ await deps.flows.snapshot(flow.id);
63
+ const state = await deps.flows.addState(flow.id, stateInput);
64
+ emitDefinitionChanged(deps.eventRepo, flow.id, "admin.state.create", { name: state.name });
65
+ return jsonResult(state);
66
+ }
67
+ export async function handleAdminStateUpdate(deps, args) {
68
+ const v = validateInput(AdminStateUpdateSchema, args);
69
+ if (!v.ok)
70
+ return v.result;
71
+ const { flow_name, state_name, ...changes } = v.data;
72
+ const flow = await deps.flows.getByName(flow_name);
73
+ if (!flow)
74
+ return errorResult(`Flow not found: ${flow_name}`);
75
+ const stateDef = flow.states.find((s) => s.name === state_name);
76
+ if (!stateDef)
77
+ return errorResult(`State not found: ${state_name} in flow ${flow_name}`);
78
+ await deps.flows.snapshot(flow.id);
79
+ const updated = await deps.flows.updateState(stateDef.id, changes);
80
+ emitDefinitionChanged(deps.eventRepo, flow.id, "admin.state.update", { name: state_name, changes });
81
+ return jsonResult(updated);
82
+ }
83
+ export async function handleAdminTransitionCreate(deps, args) {
84
+ const v = validateInput(AdminTransitionCreateSchema, args);
85
+ if (!v.ok)
86
+ return v.result;
87
+ const { flow_name, gateName, ...transitionInput } = v.data;
88
+ const flow = await deps.flows.getByName(flow_name);
89
+ if (!flow)
90
+ return errorResult(`Flow not found: ${flow_name}`);
91
+ const stateNames = flow.states.map((s) => s.name);
92
+ if (!stateNames.includes(transitionInput.fromState)) {
93
+ return errorResult(`State not found: '${transitionInput.fromState}' in flow '${flow_name}'`);
94
+ }
95
+ if (!stateNames.includes(transitionInput.toState)) {
96
+ return errorResult(`State not found: '${transitionInput.toState}' in flow '${flow_name}'`);
97
+ }
98
+ await deps.flows.snapshot(flow.id);
99
+ let gateId;
100
+ if (gateName) {
101
+ const gate = await deps.gates.getByName(gateName);
102
+ if (!gate)
103
+ return errorResult(`Gate not found: ${gateName}`);
104
+ gateId = gate.id;
105
+ }
106
+ const transition = await deps.flows.addTransition(flow.id, { ...transitionInput, gateId });
107
+ emitDefinitionChanged(deps.eventRepo, flow.id, "admin.transition.create", {
108
+ fromState: transitionInput.fromState,
109
+ toState: transitionInput.toState,
110
+ trigger: transitionInput.trigger,
111
+ });
112
+ return jsonResult(transition);
113
+ }
114
+ export async function handleAdminTransitionUpdate(deps, args) {
115
+ const v = validateInput(AdminTransitionUpdateSchema, args);
116
+ if (!v.ok)
117
+ return v.result;
118
+ const { flow_name, transition_id, gateName, ...changes } = v.data;
119
+ const flow = await deps.flows.getByName(flow_name);
120
+ if (!flow)
121
+ return errorResult(`Flow not found: ${flow_name}`);
122
+ const existing = flow.transitions.find((t) => t.id === transition_id);
123
+ if (!existing)
124
+ return errorResult(`Transition not found: ${transition_id} in flow ${flow_name}`);
125
+ const stateNames = flow.states.map((s) => s.name);
126
+ if (changes.fromState !== undefined && !stateNames.includes(changes.fromState)) {
127
+ return errorResult(`State not found: '${changes.fromState}' in flow '${flow_name}'`);
128
+ }
129
+ if (changes.toState !== undefined && !stateNames.includes(changes.toState)) {
130
+ return errorResult(`State not found: '${changes.toState}' in flow '${flow_name}'`);
131
+ }
132
+ await deps.flows.snapshot(flow.id);
133
+ const updateChanges = { ...changes };
134
+ if (gateName === null) {
135
+ updateChanges.gateId = null;
136
+ }
137
+ else if (gateName !== undefined) {
138
+ const gate = await deps.gates.getByName(gateName);
139
+ if (!gate)
140
+ return errorResult(`Gate not found: ${gateName}`);
141
+ updateChanges.gateId = gate.id;
142
+ }
143
+ const updated = await deps.flows.updateTransition(transition_id, updateChanges);
144
+ emitDefinitionChanged(deps.eventRepo, flow.id, "admin.transition.update", { transition_id });
145
+ return jsonResult(updated);
146
+ }
147
+ export async function handleAdminGateCreate(deps, args) {
148
+ const v = validateInput(AdminGateCreateSchema, args);
149
+ if (!v.ok)
150
+ return v.result;
151
+ const gate = await deps.gates.create(v.data);
152
+ emitDefinitionChanged(deps.eventRepo, null, "admin.gate.create", { name: gate.name });
153
+ return jsonResult(gate);
154
+ }
155
+ export async function handleAdminGateAttach(deps, args) {
156
+ const v = validateInput(AdminGateAttachSchema, args);
157
+ if (!v.ok)
158
+ return v.result;
159
+ const { flow_name, transition_id, gate_name } = v.data;
160
+ const flow = await deps.flows.getByName(flow_name);
161
+ if (!flow)
162
+ return errorResult(`Flow not found: ${flow_name}`);
163
+ const existing = flow.transitions.find((t) => t.id === transition_id);
164
+ if (!existing)
165
+ return errorResult(`Transition not found: ${transition_id} in flow ${flow_name}`);
166
+ const gate = await deps.gates.getByName(gate_name);
167
+ if (!gate)
168
+ return errorResult(`Gate not found: ${gate_name}`);
169
+ await deps.flows.snapshot(flow.id);
170
+ const updated = await deps.flows.updateTransition(transition_id, { gateId: gate.id });
171
+ emitDefinitionChanged(deps.eventRepo, flow.id, "admin.gate.attach", { transition_id, gate_name });
172
+ return jsonResult(updated);
173
+ }
174
+ export async function handleAdminFlowSnapshot(deps, args) {
175
+ const v = validateInput(AdminFlowSnapshotSchema, args);
176
+ if (!v.ok)
177
+ return v.result;
178
+ const flow = await deps.flows.getByName(v.data.flow_name);
179
+ if (!flow)
180
+ return errorResult(`Flow not found: ${v.data.flow_name}`);
181
+ const version = await deps.flows.snapshot(flow.id);
182
+ return jsonResult(version);
183
+ }
184
+ export async function handleAdminFlowRestore(deps, args) {
185
+ const v = validateInput(AdminFlowRestoreSchema, args);
186
+ if (!v.ok)
187
+ return v.result;
188
+ const flow = await deps.flows.getByName(v.data.flow_name);
189
+ if (!flow)
190
+ return errorResult(`Flow not found: ${v.data.flow_name}`);
191
+ const nonTerminalStateNames = flow.states.map((s) => s.name).filter((name) => !isTerminal(flow, name));
192
+ const hasActive = await deps.entities.hasAnyInFlowAndState(flow.id, nonTerminalStateNames);
193
+ if (hasActive) {
194
+ return errorResult(`Cannot restore flow '${v.data.flow_name}': active entities exist. Complete or fail them first.`);
195
+ }
196
+ await deps.flows.snapshot(flow.id);
197
+ await deps.flows.restore(flow.id, v.data.version);
198
+ emitDefinitionChanged(deps.eventRepo, flow.id, "admin.flow.restore", { version: v.data.version });
199
+ return jsonResult({ restored: true, version: v.data.version });
200
+ }
@@ -0,0 +1,25 @@
1
+ import type { McpServerDeps } from "../mcp-helpers.js";
2
+ export declare function handleFlowClaim(deps: McpServerDeps, args: Record<string, unknown>): Promise<{
3
+ content: {
4
+ type: "text";
5
+ text: string;
6
+ }[];
7
+ }>;
8
+ export declare function handleFlowGetPrompt(deps: McpServerDeps, args: Record<string, unknown>): Promise<{
9
+ content: {
10
+ type: "text";
11
+ text: string;
12
+ }[];
13
+ }>;
14
+ export declare function handleFlowReport(deps: McpServerDeps, args: Record<string, unknown>): Promise<{
15
+ content: {
16
+ type: "text";
17
+ text: string;
18
+ }[];
19
+ }>;
20
+ export declare function handleFlowFail(deps: McpServerDeps, args: Record<string, unknown>): Promise<{
21
+ content: {
22
+ type: "text";
23
+ text: string;
24
+ }[];
25
+ }>;
@@ -0,0 +1,289 @@
1
+ import { DEFAULT_TIMEOUT_PROMPT } from "../../engine/constants.js";
2
+ import { errorResult, jsonResult, validateInput } from "../mcp-helpers.js";
3
+ import { FlowClaimSchema, FlowFailSchema, FlowGetPromptSchema, FlowReportSchema } from "../tool-schemas.js";
4
+ const RETRY_SHORT_MS = 30_000; // entities exist but all claimed
5
+ const RETRY_LONG_MS = 300_000; // backlog empty
6
+ function noWorkResult(retryAfterMs, role) {
7
+ return jsonResult({
8
+ next_action: "check_back",
9
+ retry_after_ms: retryAfterMs,
10
+ message: `No work available for role '${role}' right now. Call flow.claim again after the retry delay.`,
11
+ });
12
+ }
13
+ export async function handleFlowClaim(deps, args) {
14
+ const v = validateInput(FlowClaimSchema, args);
15
+ if (!v.ok)
16
+ return v.result;
17
+ const { worker_id, role, flow: flowName } = v.data;
18
+ // 1. Find candidate flows filtered by discipline
19
+ let candidateFlows = [];
20
+ if (flowName) {
21
+ const flow = await deps.flows.getByName(flowName);
22
+ if (!flow)
23
+ return errorResult(`Flow not found: ${flowName}`);
24
+ // Discipline must match — null discipline flows are claimable by any role
25
+ if (flow.discipline !== null && flow.discipline !== role)
26
+ return noWorkResult(RETRY_LONG_MS, role);
27
+ candidateFlows = [flow];
28
+ }
29
+ else {
30
+ const allFlows = await deps.flows.list();
31
+ candidateFlows = allFlows.filter((f) => f.discipline === null || f.discipline === role);
32
+ }
33
+ if (candidateFlows.length === 0)
34
+ return noWorkResult(RETRY_LONG_MS, role);
35
+ const allCandidates = [];
36
+ for (const flow of candidateFlows) {
37
+ const unclaimed = await deps.invocations.findUnclaimedByFlow(flow.id);
38
+ allCandidates.push(...unclaimed);
39
+ }
40
+ if (allCandidates.length === 0) {
41
+ // Determine if entities exist but are all claimed (short retry) vs empty backlog (long retry).
42
+ // Use hasAnyInFlowAndState (SELECT 1 LIMIT 1) to avoid loading full entity rows across all states.
43
+ let hasEntities = false;
44
+ for (const flow of candidateFlows) {
45
+ const stateNames = flow.states.filter((s) => s.promptTemplate !== null).map((s) => s.name);
46
+ if (await deps.entities.hasAnyInFlowAndState(flow.id, stateNames)) {
47
+ hasEntities = true;
48
+ break;
49
+ }
50
+ }
51
+ return noWorkResult(hasEntities ? RETRY_SHORT_MS : RETRY_LONG_MS, role);
52
+ }
53
+ // 3. Load entities for priority sorting
54
+ const entityMap = new Map();
55
+ const uniqueEntityIds = [...new Set(allCandidates.map((inv) => inv.entityId))];
56
+ await Promise.all(uniqueEntityIds.map(async (eid) => {
57
+ const entity = await deps.entities.get(eid);
58
+ if (entity)
59
+ entityMap.set(eid, entity);
60
+ }));
61
+ // 4. Build a flow lookup map (needed for affinity window below)
62
+ const flowById = new Map(candidateFlows.map((f) => [f.id, f]));
63
+ // 5. Check affinity for each entity using the flow's configured window
64
+ const affinitySet = new Set();
65
+ const now = Date.now();
66
+ if (worker_id) {
67
+ await Promise.all(uniqueEntityIds.map(async (eid) => {
68
+ const entity = entityMap.get(eid);
69
+ const flow = entity ? flowById.get(entity.flowId) : undefined;
70
+ const affinityWindowMs = flow?.affinityWindowMs ?? 300000;
71
+ const invocations = await deps.invocations.findByEntity(eid);
72
+ const lastCompleted = invocations
73
+ .filter((inv) => inv.completedAt !== null && inv.claimedBy === worker_id)
74
+ .sort((a, b) => (b.completedAt?.getTime() ?? 0) - (a.completedAt?.getTime() ?? 0));
75
+ if (lastCompleted.length > 0) {
76
+ const elapsed = now - (lastCompleted[0].completedAt?.getTime() ?? 0);
77
+ if (elapsed < affinityWindowMs) {
78
+ affinitySet.add(eid);
79
+ }
80
+ }
81
+ }));
82
+ }
83
+ // 6. Sort candidates by priority algorithm
84
+ allCandidates.sort((a, b) => {
85
+ const entityA = entityMap.get(a.entityId);
86
+ const entityB = entityMap.get(b.entityId);
87
+ // Tier 1: Affinity (has affinity sorts first)
88
+ const affinityA = affinitySet.has(a.entityId) ? 1 : 0;
89
+ const affinityB = affinitySet.has(b.entityId) ? 1 : 0;
90
+ if (affinityA !== affinityB)
91
+ return affinityB - affinityA;
92
+ // Tier 2: Entity priority (higher priority sorts first)
93
+ const priA = entityA?.priority ?? 0;
94
+ const priB = entityB?.priority ?? 0;
95
+ if (priA !== priB)
96
+ return priB - priA;
97
+ // Tier 3: Time in state (longest waiting sorts first — earlier createdAt as stable proxy)
98
+ const timeA = entityA?.createdAt?.getTime() ?? now;
99
+ const timeB = entityB?.createdAt?.getTime() ?? now;
100
+ return timeA - timeB;
101
+ });
102
+ // 7. Try claiming in priority order (handle race conditions)
103
+ for (const invocation of allCandidates) {
104
+ let claimed;
105
+ try {
106
+ claimed = await deps.invocations.claim(invocation.id, worker_id ?? `agent:${role}`);
107
+ }
108
+ catch (err) {
109
+ console.error(`Failed to claim invocation ${invocation.id}:`, err);
110
+ continue;
111
+ }
112
+ if (claimed) {
113
+ const entity = entityMap.get(claimed.entityId);
114
+ if (entity) {
115
+ let claimedEntity;
116
+ try {
117
+ claimedEntity = await deps.entities.claimById(entity.id, worker_id ?? `agent:${role}`);
118
+ }
119
+ catch (err) {
120
+ console.error(`Failed to claim entity ${entity.id}:`, err);
121
+ await deps.invocations.releaseClaim(claimed.id);
122
+ continue;
123
+ }
124
+ if (!claimedEntity) {
125
+ // Race condition: another worker claimed this entity first.
126
+ // Release the invocation claim so it can be picked up by another worker.
127
+ await deps.invocations.releaseClaim(claimed.id);
128
+ continue;
129
+ }
130
+ }
131
+ const flow = entity ? flowById.get(entity.flowId) : undefined;
132
+ // Record affinity for the claiming worker (best-effort; failure must not block the claim)
133
+ if (worker_id && entity && flow) {
134
+ try {
135
+ const windowMs = flow.affinityWindowMs ?? 300000;
136
+ await deps.entities.setAffinity(claimed.entityId, worker_id, role, new Date(Date.now() + windowMs));
137
+ }
138
+ catch (err) {
139
+ console.error(`Failed to set affinity for entity ${claimed.entityId}:`, err);
140
+ }
141
+ }
142
+ return jsonResult({
143
+ worker_id: worker_id,
144
+ entity_id: claimed.entityId,
145
+ invocation_id: claimed.id,
146
+ flow: flow?.name ?? null,
147
+ stage: claimed.stage,
148
+ prompt: claimed.prompt,
149
+ context: claimed.context,
150
+ });
151
+ }
152
+ }
153
+ return noWorkResult(RETRY_SHORT_MS, role);
154
+ }
155
+ export async function handleFlowGetPrompt(deps, args) {
156
+ const v = validateInput(FlowGetPromptSchema, args);
157
+ if (!v.ok)
158
+ return v.result;
159
+ const { entity_id: entityId } = v.data;
160
+ const entity = await deps.entities.get(entityId);
161
+ if (!entity)
162
+ return errorResult(`Entity not found: ${entityId}`);
163
+ const invocationList = await deps.invocations.findByEntity(entityId);
164
+ if (invocationList.length === 0) {
165
+ return errorResult(`No invocations found for entity: ${entityId}`);
166
+ }
167
+ // Return the active (claimed, not completed) invocation rather than the last by insertion order
168
+ const active = invocationList.find((inv) => inv.claimedAt !== null && inv.completedAt === null && inv.failedAt === null) ??
169
+ invocationList[invocationList.length - 1];
170
+ return jsonResult({
171
+ prompt: active.prompt,
172
+ context: active.context,
173
+ });
174
+ }
175
+ export async function handleFlowReport(deps, args) {
176
+ const v = validateInput(FlowReportSchema, args);
177
+ if (!v.ok)
178
+ return v.result;
179
+ const { entity_id: entityId, signal, artifacts, worker_id } = v.data;
180
+ const invocationList = await deps.invocations.findByEntity(entityId);
181
+ const activeInvocation = invocationList.find((inv) => inv.claimedAt !== null && inv.completedAt === null && inv.failedAt === null);
182
+ if (!activeInvocation) {
183
+ return errorResult(`No active invocation found for entity: ${entityId}`);
184
+ }
185
+ if (!deps.engine) {
186
+ return errorResult("Engine not available — MCP server started without engine dependency");
187
+ }
188
+ // Complete the current invocation BEFORE calling processSignal so the
189
+ // concurrency check inside the engine doesn't count it as still-active.
190
+ await deps.invocations.complete(activeInvocation.id, signal, artifacts);
191
+ // Delegate to the engine — it handles gate evaluation, transition, event
192
+ // emission, invocation creation, concurrency checks, and spawn logic.
193
+ let result;
194
+ try {
195
+ result = await deps.engine.processSignal(entityId, signal, artifacts, activeInvocation.id);
196
+ }
197
+ catch (err) {
198
+ const message = err instanceof Error ? err.message : String(err);
199
+ // processSignal failed after we already completed the invocation.
200
+ // Only recreate the replacement if the engine did NOT already advance the entity
201
+ // (i.e. the entity is still in the same state as when we started). If the engine
202
+ // mutated the entity mid-execution and then threw, recreating the old invocation
203
+ // would regress the entity back to a stale state.
204
+ const entityAfter = await deps.entities.get(entityId).catch(() => null);
205
+ if (!entityAfter || entityAfter.state === activeInvocation.stage) {
206
+ await deps.invocations.create(entityId, activeInvocation.stage, activeInvocation.prompt, activeInvocation.mode, undefined, activeInvocation.context ?? undefined);
207
+ }
208
+ return errorResult(message);
209
+ }
210
+ // Set affinity on completion for passive-mode invocations, after processSignal succeeds
211
+ if (worker_id && activeInvocation.mode === "passive") {
212
+ try {
213
+ const entity = await deps.entities.get(entityId);
214
+ if (entity) {
215
+ const flow = await deps.flows.get(entity.flowId);
216
+ const windowMs = flow?.affinityWindowMs ?? 300000;
217
+ const affinityRole = flow?.discipline;
218
+ if (affinityRole) {
219
+ await deps.entities.setAffinity(entityId, worker_id, affinityRole, new Date(Date.now() + windowMs));
220
+ }
221
+ }
222
+ }
223
+ catch (err) {
224
+ console.error(`Failed to set affinity for entity ${entityId} worker ${worker_id}:`, err);
225
+ }
226
+ }
227
+ // Gate blocked — create a replacement invocation so the entity can be reclaimed.
228
+ // Claim it immediately for the same worker so that a retry flow.report call (for
229
+ // check_back) finds an active invocation without requiring a round-trip through
230
+ // flow.claim. Workers on the "waiting" path will re-claim via flow.claim as usual.
231
+ if (result.gated) {
232
+ const replacement = await deps.invocations.create(entityId, activeInvocation.stage, activeInvocation.prompt, activeInvocation.mode, undefined, activeInvocation.context ?? undefined);
233
+ const claimedBy = activeInvocation.claimedBy;
234
+ if (claimedBy) {
235
+ await deps.invocations.claim(replacement.id, claimedBy).catch(() => {
236
+ // Best-effort: if claim fails the invocation remains unclaimed and the worker
237
+ // can re-claim it via flow.claim on the next attempt.
238
+ });
239
+ }
240
+ if (result.gateTimedOut) {
241
+ const renderedPrompt = result.timeoutPrompt ?? DEFAULT_TIMEOUT_PROMPT;
242
+ return jsonResult({
243
+ next_action: "check_back",
244
+ message: renderedPrompt,
245
+ retry_after_ms: 30000,
246
+ timeout_prompt: renderedPrompt,
247
+ });
248
+ }
249
+ return jsonResult({
250
+ new_state: null,
251
+ gated: true,
252
+ gate_output: result.gateOutput,
253
+ gateName: result.gateName,
254
+ next_action: "waiting",
255
+ failure_prompt: result.failurePrompt ?? null,
256
+ });
257
+ }
258
+ // If the engine created a next invocation, fetch its prompt
259
+ let nextPrompt = null;
260
+ let nextContext = null;
261
+ if (result.invocationId) {
262
+ const nextInvocation = await deps.invocations.get(result.invocationId);
263
+ if (nextInvocation) {
264
+ nextPrompt = nextInvocation.prompt;
265
+ nextContext = nextInvocation.context;
266
+ }
267
+ }
268
+ const nextAction = result.terminal ? "completed" : result.invocationId ? "continue" : "waiting";
269
+ return jsonResult({
270
+ new_state: result.newState,
271
+ gates_passed: result.gatesPassed,
272
+ next_action: nextAction,
273
+ prompt: nextPrompt,
274
+ context: nextContext,
275
+ });
276
+ }
277
+ export async function handleFlowFail(deps, args) {
278
+ const v = validateInput(FlowFailSchema, args);
279
+ if (!v.ok)
280
+ return v.result;
281
+ const { entity_id: entityId, error } = v.data;
282
+ const invocationList = await deps.invocations.findByEntity(entityId);
283
+ const activeInvocation = invocationList.find((inv) => inv.claimedAt !== null && inv.completedAt === null && inv.failedAt === null);
284
+ if (!activeInvocation) {
285
+ return errorResult(`No active invocation found for entity: ${entityId}`);
286
+ }
287
+ await deps.invocations.fail(activeInvocation.id, error);
288
+ return jsonResult({ acknowledged: true });
289
+ }
@@ -0,0 +1,31 @@
1
+ import type { McpServerDeps } from "../mcp-helpers.js";
2
+ export declare function handleQueryEntity(deps: McpServerDeps, args: Record<string, unknown>): Promise<{
3
+ content: {
4
+ type: "text";
5
+ text: string;
6
+ }[];
7
+ }>;
8
+ export declare function handleQueryEntities(deps: McpServerDeps, args: Record<string, unknown>): Promise<{
9
+ content: {
10
+ type: "text";
11
+ text: string;
12
+ }[];
13
+ }>;
14
+ export declare function handleQueryInvocations(deps: McpServerDeps, args: Record<string, unknown>): Promise<{
15
+ content: {
16
+ type: "text";
17
+ text: string;
18
+ }[];
19
+ }>;
20
+ export declare function handleQueryFlow(deps: McpServerDeps, args: Record<string, unknown>): Promise<{
21
+ content: {
22
+ type: "text";
23
+ text: string;
24
+ }[];
25
+ }>;
26
+ export declare function handleQueryFlows(deps: McpServerDeps): Promise<{
27
+ content: {
28
+ type: "text";
29
+ text: string;
30
+ }[];
31
+ }>;
@@ -0,0 +1,64 @@
1
+ import { errorResult, jsonResult, validateInput } from "../mcp-helpers.js";
2
+ import { QueryEntitiesSchema, QueryEntitySchema, QueryFlowSchema, QueryInvocationsSchema } from "../tool-schemas.js";
3
+ export async function handleQueryEntity(deps, args) {
4
+ const v = validateInput(QueryEntitySchema, args);
5
+ if (!v.ok)
6
+ return v.result;
7
+ const { id } = v.data;
8
+ const entity = await deps.entities.get(id);
9
+ if (!entity)
10
+ return errorResult(`Entity not found: ${id}`);
11
+ const history = await deps.transitions.historyFor(id);
12
+ return jsonResult({ ...entity, history });
13
+ }
14
+ export async function handleQueryEntities(deps, args) {
15
+ const v = validateInput(QueryEntitiesSchema, args);
16
+ if (!v.ok)
17
+ return v.result;
18
+ const { flow: flowName, state, limit } = v.data;
19
+ const effectiveLimit = limit ?? 50;
20
+ const flow = await deps.flows.getByName(flowName);
21
+ if (!flow)
22
+ return errorResult(`Flow not found: ${flowName}`);
23
+ const results = await deps.entities.findByFlowAndState(flow.id, state, effectiveLimit);
24
+ return jsonResult(results);
25
+ }
26
+ export async function handleQueryInvocations(deps, args) {
27
+ const v = validateInput(QueryInvocationsSchema, args);
28
+ if (!v.ok)
29
+ return v.result;
30
+ const { entity_id: entityId } = v.data;
31
+ const results = await deps.invocations.findByEntity(entityId);
32
+ return jsonResult(results);
33
+ }
34
+ export async function handleQueryFlow(deps, args) {
35
+ const v = validateInput(QueryFlowSchema, args);
36
+ if (!v.ok)
37
+ return v.result;
38
+ const { name } = v.data;
39
+ const flow = await deps.flows.getByName(name);
40
+ if (!flow)
41
+ return errorResult(`Flow not found: ${name}`);
42
+ return jsonResult(flow);
43
+ }
44
+ export async function handleQueryFlows(deps) {
45
+ const flows = await deps.flows.list();
46
+ return jsonResult(flows.map((f) => ({
47
+ id: f.id,
48
+ name: f.name,
49
+ description: f.description,
50
+ initialState: f.initialState,
51
+ discipline: f.discipline,
52
+ version: f.version,
53
+ states: f.states.map(({ id, flowId, name, modelTier, mode, constraints, onEnter }) => ({
54
+ id,
55
+ flowId,
56
+ name,
57
+ modelTier,
58
+ mode,
59
+ constraints,
60
+ onEnter,
61
+ })),
62
+ transitions: f.transitions,
63
+ })));
64
+ }
@@ -0,0 +1,4 @@
1
+ export type { ActiveRunnerDeps, ActiveRunnerRunOptions, IAIProviderAdapter } from "./active-runner.js";
2
+ export { ActiveRunner } from "./active-runner.js";
3
+ export type { McpServerDeps, McpServerOpts } from "./mcp-server.js";
4
+ export { createMcpServer, startStdioServer } from "./mcp-server.js";