@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,83 @@
1
+ {
2
+ "version": "7",
3
+ "dialect": "sqlite",
4
+ "entries": [
5
+ {
6
+ "idx": 0,
7
+ "version": "6",
8
+ "when": 1772820079904,
9
+ "tag": "0000_simple_surge",
10
+ "breakpoints": true
11
+ },
12
+ {
13
+ "idx": 1,
14
+ "version": "6",
15
+ "when": 1772820079905,
16
+ "tag": "0001_peaceful_marvel_apes",
17
+ "breakpoints": true
18
+ },
19
+ {
20
+ "idx": 2,
21
+ "version": "6",
22
+ "when": 1772820079906,
23
+ "tag": "0002_add_invocations_created_at",
24
+ "breakpoints": true
25
+ },
26
+ {
27
+ "idx": 3,
28
+ "version": "6",
29
+ "when": 1772820079907,
30
+ "tag": "0003_drop_integration_config",
31
+ "breakpoints": true
32
+ },
33
+ {
34
+ "idx": 4,
35
+ "version": "6",
36
+ "when": 1772868838854,
37
+ "tag": "0004_lucky_silverclaw",
38
+ "breakpoints": true
39
+ },
40
+ {
41
+ "idx": 5,
42
+ "version": "6",
43
+ "when": 1772894164983,
44
+ "tag": "0005_old_blue_shield",
45
+ "breakpoints": true
46
+ },
47
+ {
48
+ "idx": 6,
49
+ "version": "6",
50
+ "when": 1772893521314,
51
+ "tag": "0006_solid_magik",
52
+ "breakpoints": true
53
+ },
54
+ {
55
+ "idx": 7,
56
+ "version": "6",
57
+ "when": 1741305600000,
58
+ "tag": "0007_fancy_luke_cage",
59
+ "breakpoints": true
60
+ },
61
+ {
62
+ "idx": 8,
63
+ "version": "6",
64
+ "when": 1772909877406,
65
+ "tag": "0008_thick_dark_beast",
66
+ "breakpoints": true
67
+ },
68
+ {
69
+ "idx": 9,
70
+ "version": "6",
71
+ "when": 1772906724993,
72
+ "tag": "0009_brief_midnight",
73
+ "breakpoints": true
74
+ },
75
+ {
76
+ "idx": 10,
77
+ "version": "6",
78
+ "when": 1772906724994,
79
+ "tag": "0010_amusing_bastion",
80
+ "breakpoints": true
81
+ }
82
+ ]
83
+ }
package/gates/.gitkeep ADDED
File without changes
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Custom gate: checks whether all Linear blockers for an entity's issue
3
+ * have corresponding merged PRs on GitHub.
4
+ *
5
+ * Referenced by seed as: "gates/blocking-graph.ts:isUnblocked"
6
+ *
7
+ * NOTE: Function gates are not yet evaluated by the engine (gate-evaluator.ts
8
+ * throws for type "function"). This file exists as the implementation target
9
+ * for when function gate support lands.
10
+ *
11
+ * NOTE: This file is intentionally outside src/ — it is loaded dynamically at
12
+ * runtime, not compiled by the main build.
13
+ */
14
+ import type { Entity } from "../src/repositories/interfaces.js";
15
+ export interface BlockingGraphResult {
16
+ passed: boolean;
17
+ output: string;
18
+ }
19
+ /**
20
+ * Check if all blocking issues for the given entity have merged PRs.
21
+ *
22
+ * Expects entity.refs.linear.id to be the Linear issue ID.
23
+ * Uses LINEAR_API_KEY env var for authentication.
24
+ * Uses `gh` CLI to check PR merge status on GitHub.
25
+ */
26
+ export declare function isUnblocked(entity: Entity): Promise<BlockingGraphResult>;
@@ -0,0 +1,102 @@
1
+ /**
2
+ * Custom gate: checks whether all Linear blockers for an entity's issue
3
+ * have corresponding merged PRs on GitHub.
4
+ *
5
+ * Referenced by seed as: "gates/blocking-graph.ts:isUnblocked"
6
+ *
7
+ * NOTE: Function gates are not yet evaluated by the engine (gate-evaluator.ts
8
+ * throws for type "function"). This file exists as the implementation target
9
+ * for when function gate support lands.
10
+ *
11
+ * NOTE: This file is intentionally outside src/ — it is loaded dynamically at
12
+ * runtime, not compiled by the main build.
13
+ */
14
+ import { execFile } from "node:child_process";
15
+ import { promisify } from "node:util";
16
+ import { LinearClient } from "@linear/sdk";
17
+ const execFileAsync = promisify(execFile);
18
+ /**
19
+ * Check if all blocking issues for the given entity have merged PRs.
20
+ *
21
+ * Expects entity.refs.linear.id to be the Linear issue ID.
22
+ * Uses LINEAR_API_KEY env var for authentication.
23
+ * Uses `gh` CLI to check PR merge status on GitHub.
24
+ */
25
+ export async function isUnblocked(entity) {
26
+ const linearApiKey = process.env.LINEAR_API_KEY;
27
+ if (!linearApiKey) {
28
+ return { passed: false, output: "LINEAR_API_KEY not set" };
29
+ }
30
+ const issueId = entity.refs?.linear?.id;
31
+ if (!issueId) {
32
+ return { passed: false, output: "Entity has no linear ref" };
33
+ }
34
+ const client = new LinearClient({ apiKey: linearApiKey });
35
+ let issue;
36
+ let inverseRelations;
37
+ try {
38
+ issue = await client.issue(issueId);
39
+ inverseRelations = await issue.inverseRelations();
40
+ }
41
+ catch (err) {
42
+ const message = err instanceof Error ? err.message : String(err);
43
+ return { passed: false, output: `Linear API error: ${message}` };
44
+ }
45
+ const blockers = inverseRelations.nodes.filter((r) => r.type === "blocks");
46
+ if (blockers.length === 0) {
47
+ return { passed: true, output: "No blockers" };
48
+ }
49
+ const unmerged = [];
50
+ for (const relation of blockers) {
51
+ try {
52
+ const blockerIssue = await relation.issue;
53
+ if (!blockerIssue)
54
+ continue;
55
+ const identifier = blockerIssue.identifier;
56
+ // Resolve the PR via Linear attachment — the attachment URL tells us which repo
57
+ const attachments = await blockerIssue.attachments();
58
+ const prAttachment = attachments.nodes.find((a) => {
59
+ if (!a.url)
60
+ return false;
61
+ let hostname;
62
+ try {
63
+ hostname = new URL(a.url).hostname;
64
+ }
65
+ catch {
66
+ return false;
67
+ }
68
+ return hostname === "github.com" && a.url.includes("/pull/");
69
+ });
70
+ if (!prAttachment?.url) {
71
+ unmerged.push(`${identifier} (no PR found)`);
72
+ continue;
73
+ }
74
+ const match = prAttachment.url.match(/github\.com\/([^/]+\/[^/]+)\/pull\/(\d+)/);
75
+ if (!match) {
76
+ unmerged.push(`${identifier} (unrecognized PR URL)`);
77
+ continue;
78
+ }
79
+ const [, repo, prNum] = match;
80
+ try {
81
+ const { stdout } = await execFileAsync("gh", ["pr", "view", prNum, "--repo", repo, "--json", "state", "--jq", ".state"], { timeout: 10000 });
82
+ if (stdout.trim() !== "MERGED") {
83
+ unmerged.push(identifier);
84
+ }
85
+ }
86
+ catch {
87
+ unmerged.push(`${identifier} (gh check failed)`);
88
+ }
89
+ }
90
+ catch (err) {
91
+ const errMsg = err instanceof Error ? err.message : String(err);
92
+ unmerged.push(`(blocker check failed: ${errMsg})`);
93
+ }
94
+ }
95
+ if (unmerged.length > 0) {
96
+ return {
97
+ passed: false,
98
+ output: `Blocked by unmerged: ${unmerged.join(", ")}`,
99
+ };
100
+ }
101
+ return { passed: true, output: `All ${blockers.length} blockers merged` };
102
+ }
@@ -0,0 +1,121 @@
1
+ /**
2
+ * Custom gate: checks whether all Linear blockers for an entity's issue
3
+ * have corresponding merged PRs on GitHub.
4
+ *
5
+ * Referenced by seed as: "gates/blocking-graph.ts:isUnblocked"
6
+ *
7
+ * NOTE: Function gates are not yet evaluated by the engine (gate-evaluator.ts
8
+ * throws for type "function"). This file exists as the implementation target
9
+ * for when function gate support lands.
10
+ *
11
+ * NOTE: This file is intentionally outside src/ — it is loaded dynamically at
12
+ * runtime, not compiled by the main build.
13
+ */
14
+
15
+ import { execFile } from "node:child_process";
16
+ import { promisify } from "node:util";
17
+ import { LinearClient } from "@linear/sdk";
18
+ import type { Entity } from "../src/repositories/interfaces.js";
19
+
20
+ const execFileAsync = promisify(execFile);
21
+
22
+ export interface BlockingGraphResult {
23
+ passed: boolean;
24
+ output: string;
25
+ }
26
+
27
+ /**
28
+ * Check if all blocking issues for the given entity have merged PRs.
29
+ *
30
+ * Expects entity.refs.linear.id to be the Linear issue ID.
31
+ * Uses LINEAR_API_KEY env var for authentication.
32
+ * Uses `gh` CLI to check PR merge status on GitHub.
33
+ */
34
+ export async function isUnblocked(entity: Entity): Promise<BlockingGraphResult> {
35
+ const linearApiKey = process.env.LINEAR_API_KEY;
36
+ if (!linearApiKey) {
37
+ return { passed: false, output: "LINEAR_API_KEY not set" };
38
+ }
39
+
40
+ const issueId = entity.refs?.linear?.id as string | undefined;
41
+ if (!issueId) {
42
+ return { passed: false, output: "Entity has no linear ref" };
43
+ }
44
+
45
+ const client = new LinearClient({ apiKey: linearApiKey });
46
+ let issue: Awaited<ReturnType<LinearClient["issue"]>>;
47
+ let inverseRelations: Awaited<ReturnType<typeof issue.inverseRelations>>;
48
+ try {
49
+ issue = await client.issue(issueId);
50
+ inverseRelations = await issue.inverseRelations();
51
+ } catch (err) {
52
+ const message = err instanceof Error ? err.message : String(err);
53
+ return { passed: false, output: `Linear API error: ${message}` };
54
+ }
55
+
56
+ const blockers = inverseRelations.nodes.filter((r) => r.type === "blocks");
57
+
58
+ if (blockers.length === 0) {
59
+ return { passed: true, output: "No blockers" };
60
+ }
61
+
62
+ const unmerged: string[] = [];
63
+
64
+ for (const relation of blockers) {
65
+ try {
66
+ const blockerIssue = await relation.issue;
67
+ if (!blockerIssue) continue;
68
+
69
+ const identifier = blockerIssue.identifier;
70
+
71
+ // Resolve the PR via Linear attachment — the attachment URL tells us which repo
72
+ const attachments = await blockerIssue.attachments();
73
+ const prAttachment = attachments.nodes.find((a) => {
74
+ if (!a.url) return false;
75
+ let hostname: string;
76
+ try {
77
+ hostname = new URL(a.url).hostname;
78
+ } catch {
79
+ return false;
80
+ }
81
+ return hostname === "github.com" && a.url.includes("/pull/");
82
+ });
83
+ if (!prAttachment?.url) {
84
+ unmerged.push(`${identifier} (no PR found)`);
85
+ continue;
86
+ }
87
+ const match = prAttachment.url.match(
88
+ /github\.com\/([^/]+\/[^/]+)\/pull\/(\d+)/,
89
+ );
90
+ if (!match) {
91
+ unmerged.push(`${identifier} (unrecognized PR URL)`);
92
+ continue;
93
+ }
94
+ const [, repo, prNum] = match;
95
+ try {
96
+ const { stdout } = await execFileAsync(
97
+ "gh",
98
+ ["pr", "view", prNum, "--repo", repo, "--json", "state", "--jq", ".state"],
99
+ { timeout: 10000 },
100
+ );
101
+ if (stdout.trim() !== "MERGED") {
102
+ unmerged.push(identifier);
103
+ }
104
+ } catch {
105
+ unmerged.push(`${identifier} (gh check failed)`);
106
+ }
107
+ } catch (err) {
108
+ const errMsg = err instanceof Error ? err.message : String(err);
109
+ unmerged.push(`(blocker check failed: ${errMsg})`);
110
+ }
111
+ }
112
+
113
+ if (unmerged.length > 0) {
114
+ return {
115
+ passed: false,
116
+ output: `Blocked by unmerged: ${unmerged.join(", ")}`,
117
+ };
118
+ }
119
+
120
+ return { passed: true, output: `All ${blockers.length} blockers merged` };
121
+ }
@@ -0,0 +1,39 @@
1
+ #!/usr/bin/env bash
2
+ # Gate: check-design-posted — verify design spec comment on Linear issue
3
+ # Usage: gates/check-design-posted.sh <linear-issue-id>
4
+ set -euo pipefail
5
+
6
+ LINEAR_ID="${1:?Usage: check-design-posted.sh <linear-issue-id>}"
7
+ LINEAR_API_KEY="${LINEAR_API_KEY:?LINEAR_API_KEY env var is required}"
8
+
9
+ PAYLOAD=$(jq -n --arg id "$LINEAR_ID" '{
10
+ "query": "query($id: String!) { issue(id: $id) { comments { nodes { body } } } }",
11
+ "variables": {"id": $id}
12
+ }')
13
+ RESPONSE=$(curl -s -f -X POST "https://api.linear.app/graphql" \
14
+ -H "Authorization: Bearer ${LINEAR_API_KEY}" \
15
+ -H "Content-Type: application/json" \
16
+ -d "$PAYLOAD" 2>&1) || {
17
+ echo "Failed to query Linear API: $RESPONSE"
18
+ exit 1
19
+ }
20
+
21
+ if echo "$RESPONSE" | jq -e '.errors' > /dev/null 2>&1; then
22
+ echo "GraphQL error: $(echo "$RESPONSE" | jq -r '.errors[0].message // "unknown"')"
23
+ exit 1
24
+ fi
25
+
26
+ if [ "$(echo "$RESPONSE" | jq '.data.issue')" = "null" ]; then
27
+ echo "ERROR: Linear issue $LINEAR_ID not found"
28
+ exit 1
29
+ fi
30
+
31
+ COMMENTS=$(echo "$RESPONSE" | jq -r '.data.issue.comments.nodes[].body // empty')
32
+
33
+ if echo "$COMMENTS" | grep -qiE '(palette|typography|responsive|design spec|color scheme|breakpoint)'; then
34
+ echo "Design comment found on issue $LINEAR_ID"
35
+ exit 0
36
+ else
37
+ echo "No design comment found on issue $LINEAR_ID"
38
+ exit 1
39
+ fi
@@ -0,0 +1,51 @@
1
+ #!/usr/bin/env bash
2
+ # Gate: check-merge — poll PR until merged, fail on CI failure in merge queue
3
+ # Usage: gates/check-merge.sh <pr-number> <repo>
4
+ # Timeout is handled by defcon's gate evaluator (execFile timeout).
5
+ set -euo pipefail
6
+
7
+ PR="${1:?Usage: check-merge.sh <pr-number> <repo>}"
8
+ REPO="${2:?Usage: check-merge.sh <pr-number> <repo>}"
9
+
10
+ # Trap SIGTERM from gate evaluator timeout for clean exit
11
+ trap 'echo "Timed out waiting for PR #$PR to merge"; exit 1' TERM
12
+
13
+ while true; do
14
+ STATUS=$(gh pr view "$PR" --repo "$REPO" --json state,mergeStateStatus --jq '{state: .state, mergeStateStatus: .mergeStateStatus}' 2>/dev/null) || {
15
+ echo "Failed to query PR status"
16
+ sleep 30
17
+ continue
18
+ }
19
+ if ! echo "$STATUS" | jq . >/dev/null 2>&1; then
20
+ echo "ERROR: invalid JSON from gh pr view"
21
+ sleep 30
22
+ continue
23
+ fi
24
+
25
+ STATE=$(echo "$STATUS" | jq -r '.state')
26
+ MERGE_STATUS=$(echo "$STATUS" | jq -r '.mergeStateStatus')
27
+
28
+ case "$STATE" in
29
+ MERGED)
30
+ echo "PR #$PR merged in $REPO"
31
+ exit 0
32
+ ;;
33
+ CLOSED)
34
+ echo "PR #$PR was closed without merging in $REPO"
35
+ exit 1
36
+ ;;
37
+ esac
38
+
39
+ # Check for CI failure in merge queue
40
+ if [ "$MERGE_STATUS" = "DIRTY" ] || [ "$MERGE_STATUS" = "BLOCKED" ] || [ "$MERGE_STATUS" = "UNSTABLE" ]; then
41
+ RESULT=$(gh pr view "$PR" --repo "$REPO" --json state,mergeStateStatus,statusCheckRollup \
42
+ --jq '{state: .state, mergeStatus: .mergeStateStatus, failing: [.statusCheckRollup // [] | .[] | select(.conclusion == "FAILURE") | .name]}' 2>/dev/null) || continue
43
+ FAILING=$(echo "$RESULT" | jq -r '.failing[]' 2>/dev/null)
44
+ if [ -n "$FAILING" ]; then
45
+ echo "CI failing in merge queue: $FAILING"
46
+ exit 1
47
+ fi
48
+ fi
49
+
50
+ sleep 30
51
+ done
@@ -0,0 +1,17 @@
1
+ #!/usr/bin/env bash
2
+ # Gate: check-pr-capacity — exit 0 if open PR count < max
3
+ # Usage: gates/check-pr-capacity.sh <repo> [max]
4
+ set -euo pipefail
5
+
6
+ REPO="${1:?Usage: check-pr-capacity.sh <repo>}"
7
+ MAX="${2:-4}"
8
+
9
+ COUNT=$(gh pr list --repo "$REPO" --state open --json number --jq 'length')
10
+
11
+ if [ "$COUNT" -lt "$MAX" ]; then
12
+ echo "PR capacity OK: $COUNT open (max $MAX)"
13
+ exit 0
14
+ else
15
+ echo "PR capacity exceeded: $COUNT open (max $MAX)"
16
+ exit 1
17
+ fi
@@ -0,0 +1,47 @@
1
+ #!/usr/bin/env bash
2
+ # Gate: check-review-ready — CI green AND bot reviewers posted
3
+ # Usage: gates/check-review-ready.sh <pr-number> <repo>
4
+ set -euo pipefail
5
+
6
+ PR="${1:?Usage: check-review-ready.sh <pr-number> <repo>}"
7
+ REPO="${2:?Usage: check-review-ready.sh <pr-number> <repo>}"
8
+
9
+ # Check 1: All CI checks passing
10
+ echo "Checking CI status..."
11
+ CHECKS=$(gh pr view "$PR" --repo "$REPO" --json statusCheckRollup --jq '.statusCheckRollup' 2>/dev/null) || { echo "ERROR: failed to query statusCheckRollup for PR #$PR"; exit 1; }
12
+ FAILING=$(echo "$CHECKS" | jq -r '[.[] | select(.conclusion == "FAILURE") | .name] | .[]' 2>/dev/null)
13
+ if [ -n "$FAILING" ]; then
14
+ echo "CI checks failing for PR #$PR in $REPO: $FAILING"
15
+ exit 1
16
+ fi
17
+ PENDING=$(echo "$CHECKS" | jq -r '[.[] | select(.status == "IN_PROGRESS" or .status == "QUEUED" or .status == "PENDING") | .name] | .[]' 2>/dev/null)
18
+ if [ -n "$PENDING" ]; then
19
+ echo "CI checks still pending for PR #$PR in $REPO: $PENDING"
20
+ exit 1
21
+ fi
22
+ echo "CI checks passed"
23
+
24
+ # Check 2: Bot reviewers posted
25
+ echo "Checking for review bot comments..."
26
+ COMMENTS=$(gh api "repos/$REPO/issues/$PR/comments" --jq '.[].user.login' 2>/dev/null || echo "")
27
+ PR_COMMENTS=$(gh api "repos/$REPO/pulls/$PR/comments" --jq '.[].user.login' 2>/dev/null || echo "")
28
+ REVIEWS=$(gh api "repos/$REPO/pulls/$PR/reviews" --jq '.[].user.login' 2>/dev/null || echo "")
29
+ ALL_AUTHORS=$(printf '%s\n%s\n%s' "$COMMENTS" "$PR_COMMENTS" "$REVIEWS" | sort -u)
30
+
31
+ BOTS_FOUND=0
32
+ BOTS_MISSING=()
33
+ for BOT in "qodo-code-review[bot]" "coderabbitai[bot]" "sourcery-ai[bot]" "devin-ai-integration[bot]"; do
34
+ if echo "$ALL_AUTHORS" | grep -qF "$BOT"; then
35
+ BOTS_FOUND=$((BOTS_FOUND + 1))
36
+ else
37
+ BOTS_MISSING+=("$BOT")
38
+ fi
39
+ done
40
+
41
+ if [ "$BOTS_FOUND" -eq 0 ]; then
42
+ echo "No review bots have posted on PR #$PR: missing ${BOTS_MISSING[*]}"
43
+ exit 1
44
+ fi
45
+
46
+ echo "Review ready: CI green, $BOTS_FOUND bot(s) posted"
47
+ exit 0
@@ -0,0 +1,34 @@
1
+ #!/usr/bin/env bash
2
+ # Gate: check-spec-posted — verify implementation spec comment on Linear issue
3
+ # Usage: gates/check-spec-posted.sh <linear-issue-id>
4
+ set -euo pipefail
5
+
6
+ LINEAR_ID="${1:?Usage: check-spec-posted.sh <linear-issue-id>}"
7
+ LINEAR_API_KEY="${LINEAR_API_KEY:?LINEAR_API_KEY env var is required}"
8
+
9
+ PAYLOAD=$(jq -n --arg id "$LINEAR_ID" '{
10
+ "query": "query($id: String!) { issue(id: $id) { comments { nodes { body } } } }",
11
+ "variables": {"id": $id}
12
+ }')
13
+ RESPONSE=$(curl -s -f -X POST "https://api.linear.app/graphql" \
14
+ -H "Authorization: Bearer ${LINEAR_API_KEY}" \
15
+ -H "Content-Type: application/json" \
16
+ -d "$PAYLOAD" 2>&1) || {
17
+ echo "Failed to query Linear API: $RESPONSE"
18
+ exit 1
19
+ }
20
+
21
+ if echo "$RESPONSE" | jq -e '.errors' > /dev/null 2>&1; then
22
+ echo "GraphQL error: $(echo "$RESPONSE" | jq -r '.errors[0].message // "unknown"')"
23
+ exit 1
24
+ fi
25
+
26
+ COMMENTS=$(echo "$RESPONSE" | jq -r '.data.issue.comments.nodes[].body // empty')
27
+
28
+ if echo "$COMMENTS" | grep -qE '## (Implementation Spec|Implementation|File Changes)'; then
29
+ echo "Spec comment found on issue $LINEAR_ID"
30
+ exit 0
31
+ else
32
+ echo "No spec comment found on issue $LINEAR_ID"
33
+ exit 1
34
+ fi
@@ -0,0 +1,56 @@
1
+ #!/usr/bin/env bash
2
+ # Gate: check-unblocked — verify all blockedBy issues have merged PRs
3
+ # Usage: gates/check-unblocked.sh <linear-issue-id>
4
+ set -euo pipefail
5
+
6
+ LINEAR_ID="${1:?Usage: check-unblocked.sh <linear-issue-id>}"
7
+ LINEAR_API_KEY="${LINEAR_API_KEY:?LINEAR_API_KEY env var is required}"
8
+
9
+ # Fetch inverse relations (issues that block this one)
10
+ PAYLOAD=$(jq -n --arg id "$LINEAR_ID" '{
11
+ "query": "query($id: String!) { issue(id: $id) { relations { nodes { type relatedIssue { identifier attachments { nodes { url } } } } } } }",
12
+ "variables": {"id": $id}
13
+ }')
14
+
15
+ RESPONSE=$(curl -s -f -X POST "https://api.linear.app/graphql" \
16
+ -H "Authorization: Bearer ${LINEAR_API_KEY}" \
17
+ -H "Content-Type: application/json" \
18
+ -d "$PAYLOAD" 2>&1) || {
19
+ echo "Failed to query Linear API: $RESPONSE"
20
+ exit 1
21
+ }
22
+
23
+ if echo "$RESPONSE" | jq -e '.errors' > /dev/null 2>&1; then
24
+ echo "GraphQL error: $(echo "$RESPONSE" | jq -r '.errors[0].message // "unknown"')"
25
+ exit 1
26
+ fi
27
+
28
+ UNMERGED=()
29
+ while IFS= read -r blocker; do
30
+ IDENTIFIER=$(echo "$blocker" | jq -r '.identifier')
31
+ # Find GitHub PR URL in attachments
32
+ PR_URL=$(echo "$blocker" | jq -r '.attachments.nodes // [] | .[] | select(.url | test("github.com/.*/pull/")) | .url' | head -1)
33
+ if [ -z "$PR_URL" ]; then
34
+ UNMERGED+=("${IDENTIFIER} (no PR)")
35
+ continue
36
+ fi
37
+ # Extract repo and PR number from URL
38
+ REPO=$(echo "$PR_URL" | sed -n 's|.*github.com/\([^/]*/[^/]*\)/pull/.*|\1|p')
39
+ PR_NUM=$(echo "$PR_URL" | sed -n 's|.*/pull/\([0-9]*\).*|\1|p')
40
+ if [ -z "$REPO" ] || [ -z "$PR_NUM" ]; then
41
+ UNMERGED+=("${IDENTIFIER} (bad PR URL)")
42
+ continue
43
+ fi
44
+ STATE=$(gh pr view "$PR_NUM" --repo "$REPO" --json state --jq '.state' 2>/dev/null || echo "UNKNOWN")
45
+ if [ "$STATE" != "MERGED" ]; then
46
+ UNMERGED+=("${IDENTIFIER}")
47
+ fi
48
+ done < <(echo "$RESPONSE" | jq -c '.data.issue.relations.nodes[] | select(.type == "isBlockedBy") | .relatedIssue')
49
+
50
+ if [ ${#UNMERGED[@]} -gt 0 ]; then
51
+ echo "Blocked by unmerged: ${UNMERGED[*]}"
52
+ exit 1
53
+ fi
54
+
55
+ echo "All blockers merged"
56
+ exit 0
@@ -0,0 +1,9 @@
1
+ #!/usr/bin/env bash
2
+ # Gate: ci-green — wait for CI checks to pass on a PR
3
+ # Usage: gates/ci-green.sh <pr-number> <repo>
4
+ set -euo pipefail
5
+
6
+ PR="${1:?Usage: ci-green.sh <pr-number> <repo>}"
7
+ REPO="${2:?Usage: ci-green.sh <pr-number> <repo>}"
8
+
9
+ gh pr checks "$PR" --repo "$REPO" --watch --fail-fast 2>&1
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env bash
2
+ # Gate: merge-queue — watch PR through merge queue
3
+ # Usage: gates/merge-queue.sh <pr-number> <repo>
4
+ set -euo pipefail
5
+
6
+ PR="${1:?Usage: merge-queue.sh <pr-number> <repo>}"
7
+ REPO="${2:?Usage: merge-queue.sh <pr-number> <repo>}"
8
+
9
+ WATCH_SCRIPT="${WOPR_PR_WATCH_SCRIPT:-${WOPR_SCRIPTS_DIR:-$HOME}/wopr-pr-watch.sh}"
10
+ if [ ! -x "$WATCH_SCRIPT" ]; then
11
+ echo "ERROR: WOPR_PR_WATCH_SCRIPT not found or not executable: $WATCH_SCRIPT" >&2
12
+ exit 1
13
+ fi
14
+ "$WATCH_SCRIPT" "$PR" "$REPO" 2>&1
@@ -0,0 +1,9 @@
1
+ #!/usr/bin/env bash
2
+ # Gate: review-bots-ready — wait for review bots to post
3
+ # Usage: gates/review-bots-ready.sh <pr-number> <repo>
4
+ set -euo pipefail
5
+
6
+ PR="${1:?Usage: review-bots-ready.sh <pr-number> <repo>}"
7
+ REPO="${2:?Usage: review-bots-ready.sh <pr-number> <repo>}"
8
+
9
+ "${WOPR_AWAIT_REVIEWS_SCRIPT:-${WOPR_SCRIPTS_DIR:-$HOME}/wopr-await-reviews.sh}" "$PR" "$REPO" 2>&1
@@ -0,0 +1,31 @@
1
+ #!/usr/bin/env bash
2
+ # Gate: spec-posted — verify architect spec comment exists on Linear issue
3
+ # Usage: gates/spec-posted.sh <linear-issue-id>
4
+ set -euo pipefail
5
+
6
+ LINEAR_ID="${1:?Usage: spec-posted.sh <linear-issue-id>}"
7
+
8
+ # Query Linear API for comments on the issue containing "Implementation Spec"
9
+ LINEAR_API_KEY="${LINEAR_API_KEY:?LINEAR_API_KEY env var is required}"
10
+ PAYLOAD=$(jq -n --arg id "$LINEAR_ID" '{"query": "query { issue(id: \($id)) { comments { nodes { body } } } }"}')
11
+ RESPONSE=$(curl -s -f -X POST "https://api.linear.app/graphql" \
12
+ -H "Authorization: ${LINEAR_API_KEY}" \
13
+ -H "Content-Type: application/json" \
14
+ -d "$PAYLOAD" 2>&1) || {
15
+ echo "Failed to query Linear API: $RESPONSE"
16
+ exit 1
17
+ }
18
+ if echo "$RESPONSE" | jq -e '.errors' > /dev/null 2>&1; then
19
+ echo "GraphQL error from Linear API: $(echo "$RESPONSE" | jq -r '.errors[0].message // "unknown error"')"
20
+ exit 1
21
+ fi
22
+
23
+ COMMENTS=$(echo "$RESPONSE" | jq -r '.data.issue.comments.nodes[].body // empty')
24
+
25
+ if echo "$COMMENTS" | grep -q "Implementation Spec"; then
26
+ echo "Spec comment found on issue $LINEAR_ID"
27
+ exit 0
28
+ else
29
+ echo "No spec comment found on issue $LINEAR_ID"
30
+ exit 1
31
+ fi
@@ -0,0 +1 @@
1
+ export declare function check(): unknown;
@@ -0,0 +1,4 @@
1
+ // Fixture: returns wrong shape (no `passed` boolean) to test return-shape validation
2
+ export function check() {
3
+ return { status: "ok" }; // wrong shape — missing `passed`
4
+ }
@@ -0,0 +1,4 @@
1
+ // Fixture: returns wrong shape (no `passed` boolean) to test return-shape validation
2
+ export function check(): unknown {
3
+ return { status: "ok" }; // wrong shape — missing `passed`
4
+ }
@@ -0,0 +1,2 @@
1
+ import type { GateEvalResult } from "../../src/engine/gate-evaluator.js";
2
+ export declare function check(): GateEvalResult;