ramstack 0.1.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 (215) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +44 -0
  3. package/agents/comment-sicko.md +32 -0
  4. package/agents/poteto-agent.md +9 -0
  5. package/cli/rstack.mjs +539 -0
  6. package/package.json +29 -0
  7. package/skills/architect/README.md +25 -0
  8. package/skills/architect/SKILL.md +83 -0
  9. package/skills/architect/agents/openai.yaml +5 -0
  10. package/skills/architect/references/design-red-flags.md +33 -0
  11. package/skills/architect/references/rationale-template.md +35 -0
  12. package/skills/architect/references/runner-prompt.md +20 -0
  13. package/skills/arena/README.md +21 -0
  14. package/skills/arena/SKILL.md +71 -0
  15. package/skills/arena/agents/openai.yaml +5 -0
  16. package/skills/automate-me/README.md +21 -0
  17. package/skills/automate-me/SKILL.md +109 -0
  18. package/skills/automate-me/agents/openai.yaml +5 -0
  19. package/skills/blast-radius/README.md +21 -0
  20. package/skills/blast-radius/SKILL.md +50 -0
  21. package/skills/blast-radius/agents/openai.yaml +5 -0
  22. package/skills/bro/README.md +16 -0
  23. package/skills/bro/SKILL.md +7 -0
  24. package/skills/bro/agents/openai.yaml +5 -0
  25. package/skills/create-verification-skill/README.md +22 -0
  26. package/skills/create-verification-skill/SKILL.md +44 -0
  27. package/skills/create-verification-skill/agents/openai.yaml +5 -0
  28. package/skills/create-verification-skill/references/feature-map-example/README.md +47 -0
  29. package/skills/create-verification-skill/references/feature-map-example/create-note.md +39 -0
  30. package/skills/create-verification-skill/references/feature-map-example/search.md +45 -0
  31. package/skills/figure-it-out/README.md +22 -0
  32. package/skills/figure-it-out/SKILL.md +55 -0
  33. package/skills/figure-it-out/agents/openai.yaml +5 -0
  34. package/skills/how/README.md +25 -0
  35. package/skills/how/SKILL.md +134 -0
  36. package/skills/how/agents/openai.yaml +3 -0
  37. package/skills/how/references/critic-prompt.md +59 -0
  38. package/skills/how/references/critique-rubric.md +58 -0
  39. package/skills/how/references/explainer-prompt.md +55 -0
  40. package/skills/how/references/explorer-prompt.md +52 -0
  41. package/skills/interrogate/README.md +21 -0
  42. package/skills/interrogate/SKILL.md +112 -0
  43. package/skills/interrogate/agents/openai.yaml +5 -0
  44. package/skills/interrogate/references/code-quality-review.md +47 -0
  45. package/skills/interrogate/references/lead-judgment.md +58 -0
  46. package/skills/interrogate/references/reviewer-prompt.md +72 -0
  47. package/skills/interrogate/references/rubric.md +77 -0
  48. package/skills/maintain-verification-skill/README.md +21 -0
  49. package/skills/maintain-verification-skill/SKILL.md +39 -0
  50. package/skills/maintain-verification-skill/agents/openai.yaml +5 -0
  51. package/skills/no-comments/README.md +22 -0
  52. package/skills/no-comments/SKILL.md +24 -0
  53. package/skills/no-comments/agents/openai.yaml +5 -0
  54. package/skills/poteto-mode/README.md +26 -0
  55. package/skills/poteto-mode/SKILL.md +140 -0
  56. package/skills/poteto-mode/agents/openai.yaml +5 -0
  57. package/skills/poteto-mode/playbooks/authoring-a-skill.md +13 -0
  58. package/skills/poteto-mode/playbooks/autonomous-run.md +13 -0
  59. package/skills/poteto-mode/playbooks/autopilot-full.md +13 -0
  60. package/skills/poteto-mode/playbooks/autopilot-stack.md +16 -0
  61. package/skills/poteto-mode/playbooks/babysit.md +27 -0
  62. package/skills/poteto-mode/playbooks/bug-fix.md +17 -0
  63. package/skills/poteto-mode/playbooks/eval.md +27 -0
  64. package/skills/poteto-mode/playbooks/feature.md +21 -0
  65. package/skills/poteto-mode/playbooks/hillclimb.md +21 -0
  66. package/skills/poteto-mode/playbooks/investigation.md +14 -0
  67. package/skills/poteto-mode/playbooks/multi-phase-plan.md +3 -0
  68. package/skills/poteto-mode/playbooks/opening-a-pr.md +11 -0
  69. package/skills/poteto-mode/playbooks/orchestrate.md +113 -0
  70. package/skills/poteto-mode/playbooks/pause-safely.md +10 -0
  71. package/skills/poteto-mode/playbooks/perf-issue.md +24 -0
  72. package/skills/poteto-mode/playbooks/prototype.md +14 -0
  73. package/skills/poteto-mode/playbooks/refactoring.md +16 -0
  74. package/skills/poteto-mode/playbooks/runtime-forensics.md +11 -0
  75. package/skills/poteto-mode/playbooks/session-pickup.md +13 -0
  76. package/skills/poteto-mode/playbooks/shipping.md +20 -0
  77. package/skills/poteto-mode/playbooks/trace-forensics.md +14 -0
  78. package/skills/poteto-mode/playbooks/visual-parity.md +11 -0
  79. package/skills/poteto-mode/playbooks/worktree-cleanup.md +14 -0
  80. package/skills/poteto-mode/references/bugbot-triage.md +142 -0
  81. package/skills/poteto-mode/references/plan.md +105 -0
  82. package/skills/poteto-mode/scripts/bootstrap.ts +62 -0
  83. package/skills/poteto-mode/scripts/bun.lock +67 -0
  84. package/skills/poteto-mode/scripts/orch/orch.test.ts +634 -0
  85. package/skills/poteto-mode/scripts/orch/orch.ts +578 -0
  86. package/skills/poteto-mode/scripts/orch/store.ts +1607 -0
  87. package/skills/poteto-mode/scripts/package.json +16 -0
  88. package/skills/poteto-mode/scripts/watch-pr/cli.test.ts +224 -0
  89. package/skills/poteto-mode/scripts/watch-pr/cli.ts +223 -0
  90. package/skills/poteto-mode/scripts/watch-pr/fakes.test-helper.ts +118 -0
  91. package/skills/poteto-mode/scripts/watch-pr/github.test.ts +306 -0
  92. package/skills/poteto-mode/scripts/watch-pr/github.ts +699 -0
  93. package/skills/poteto-mode/scripts/watch-pr/policy.test.ts +420 -0
  94. package/skills/poteto-mode/scripts/watch-pr/policy.ts +832 -0
  95. package/skills/poteto-mode/scripts/watch-pr/render.ts +169 -0
  96. package/skills/poteto-mode/scripts/watch-pr/tsconfig.json +13 -0
  97. package/skills/poteto-mode/scripts/watch-pr/types.compile.ts +93 -0
  98. package/skills/poteto-mode/scripts/watch-pr/types.ts +401 -0
  99. package/skills/poteto-mode/scripts/watch-pr/watch-pr +6 -0
  100. package/skills/poteto-mode/scripts/worktree-audit.sh +86 -0
  101. package/skills/principle-boundary-discipline/README.md +17 -0
  102. package/skills/principle-boundary-discipline/SKILL.md +34 -0
  103. package/skills/principle-boundary-discipline/agents/openai.yaml +5 -0
  104. package/skills/principle-build-the-lever/README.md +17 -0
  105. package/skills/principle-build-the-lever/SKILL.md +23 -0
  106. package/skills/principle-build-the-lever/agents/openai.yaml +5 -0
  107. package/skills/principle-encode-lessons-in-structure/README.md +17 -0
  108. package/skills/principle-encode-lessons-in-structure/SKILL.md +31 -0
  109. package/skills/principle-encode-lessons-in-structure/agents/openai.yaml +5 -0
  110. package/skills/principle-exhaust-the-design-space/README.md +17 -0
  111. package/skills/principle-exhaust-the-design-space/SKILL.md +21 -0
  112. package/skills/principle-exhaust-the-design-space/agents/openai.yaml +5 -0
  113. package/skills/principle-experience-first/README.md +17 -0
  114. package/skills/principle-experience-first/SKILL.md +19 -0
  115. package/skills/principle-experience-first/agents/openai.yaml +5 -0
  116. package/skills/principle-fix-root-causes/README.md +17 -0
  117. package/skills/principle-fix-root-causes/SKILL.md +23 -0
  118. package/skills/principle-fix-root-causes/agents/openai.yaml +5 -0
  119. package/skills/principle-foundational-thinking/README.md +17 -0
  120. package/skills/principle-foundational-thinking/SKILL.md +21 -0
  121. package/skills/principle-foundational-thinking/agents/openai.yaml +5 -0
  122. package/skills/principle-guard-the-context-window/README.md +17 -0
  123. package/skills/principle-guard-the-context-window/SKILL.md +17 -0
  124. package/skills/principle-guard-the-context-window/agents/openai.yaml +5 -0
  125. package/skills/principle-laziness-protocol/README.md +17 -0
  126. package/skills/principle-laziness-protocol/SKILL.md +18 -0
  127. package/skills/principle-laziness-protocol/agents/openai.yaml +5 -0
  128. package/skills/principle-make-operations-idempotent/README.md +17 -0
  129. package/skills/principle-make-operations-idempotent/SKILL.md +24 -0
  130. package/skills/principle-make-operations-idempotent/agents/openai.yaml +5 -0
  131. package/skills/principle-migrate-callers-then-delete-legacy-apis/README.md +17 -0
  132. package/skills/principle-migrate-callers-then-delete-legacy-apis/SKILL.md +22 -0
  133. package/skills/principle-migrate-callers-then-delete-legacy-apis/agents/openai.yaml +5 -0
  134. package/skills/principle-minimize-reader-load/README.md +17 -0
  135. package/skills/principle-minimize-reader-load/SKILL.md +23 -0
  136. package/skills/principle-minimize-reader-load/agents/openai.yaml +5 -0
  137. package/skills/principle-model-the-domain/README.md +17 -0
  138. package/skills/principle-model-the-domain/SKILL.md +26 -0
  139. package/skills/principle-model-the-domain/agents/openai.yaml +5 -0
  140. package/skills/principle-never-block-on-the-human/README.md +16 -0
  141. package/skills/principle-never-block-on-the-human/SKILL.md +23 -0
  142. package/skills/principle-never-block-on-the-human/agents/openai.yaml +5 -0
  143. package/skills/principle-outcome-oriented-execution/README.md +16 -0
  144. package/skills/principle-outcome-oriented-execution/SKILL.md +22 -0
  145. package/skills/principle-outcome-oriented-execution/agents/openai.yaml +5 -0
  146. package/skills/principle-prove-it-works/README.md +18 -0
  147. package/skills/principle-prove-it-works/SKILL.md +33 -0
  148. package/skills/principle-prove-it-works/agents/openai.yaml +5 -0
  149. package/skills/principle-redesign-from-first-principles/README.md +16 -0
  150. package/skills/principle-redesign-from-first-principles/SKILL.md +16 -0
  151. package/skills/principle-redesign-from-first-principles/agents/openai.yaml +5 -0
  152. package/skills/principle-separate-before-serializing-shared-state/README.md +16 -0
  153. package/skills/principle-separate-before-serializing-shared-state/SKILL.md +16 -0
  154. package/skills/principle-separate-before-serializing-shared-state/agents/openai.yaml +5 -0
  155. package/skills/principle-sequence-verifiable-units/README.md +18 -0
  156. package/skills/principle-sequence-verifiable-units/SKILL.md +22 -0
  157. package/skills/principle-sequence-verifiable-units/agents/openai.yaml +5 -0
  158. package/skills/principle-subtract-before-you-add/README.md +17 -0
  159. package/skills/principle-subtract-before-you-add/SKILL.md +22 -0
  160. package/skills/principle-subtract-before-you-add/agents/openai.yaml +5 -0
  161. package/skills/principle-type-system-discipline/README.md +19 -0
  162. package/skills/principle-type-system-discipline/SKILL.md +31 -0
  163. package/skills/principle-type-system-discipline/agents/openai.yaml +5 -0
  164. package/skills/recall/README.md +23 -0
  165. package/skills/recall/SKILL.md +35 -0
  166. package/skills/recall/agents/openai.yaml +5 -0
  167. package/skills/reflect/README.md +24 -0
  168. package/skills/reflect/SKILL.md +77 -0
  169. package/skills/reflect/agents/openai.yaml +5 -0
  170. package/skills/reflect/references/divergent-reviewer.md +43 -0
  171. package/skills/reflect/references/judgment-reviewer.md +42 -0
  172. package/skills/reflect/references/synthesizer.md +56 -0
  173. package/skills/reflect/references/tooling-reviewer.md +57 -0
  174. package/skills/setup-rstack/README.md +25 -0
  175. package/skills/setup-rstack/SKILL.md +81 -0
  176. package/skills/setup-rstack/agents/openai.yaml +3 -0
  177. package/skills/show-me-your-work/README.md +23 -0
  178. package/skills/show-me-your-work/SKILL.md +82 -0
  179. package/skills/show-me-your-work/agents/openai.yaml +5 -0
  180. package/skills/show-me-your-work/references/decision-log-template.tsv +1 -0
  181. package/skills/show-me-your-work/scripts/log.sh +40 -0
  182. package/skills/swarm/README.md +21 -0
  183. package/skills/swarm/SKILL.md +46 -0
  184. package/skills/swarm/agents/openai.yaml +5 -0
  185. package/skills/tdd/README.md +21 -0
  186. package/skills/tdd/SKILL.md +44 -0
  187. package/skills/tdd/agents/openai.yaml +5 -0
  188. package/skills/teach/README.md +22 -0
  189. package/skills/teach/SKILL.md +21 -0
  190. package/skills/teach/agents/openai.yaml +5 -0
  191. package/skills/technical-writing/README.md +23 -0
  192. package/skills/technical-writing/SKILL.md +130 -0
  193. package/skills/technical-writing/agents/openai.yaml +5 -0
  194. package/skills/typescript-best-practices/README.md +22 -0
  195. package/skills/typescript-best-practices/SKILL.md +28 -0
  196. package/skills/typescript-best-practices/agents/openai.yaml +3 -0
  197. package/skills/typescript-best-practices/references/patterns.md +292 -0
  198. package/skills/unslop/README.md +21 -0
  199. package/skills/unslop/SKILL.md +80 -0
  200. package/skills/unslop/agents/openai.yaml +3 -0
  201. package/skills/why/README.md +26 -0
  202. package/skills/why/SKILL.md +229 -0
  203. package/skills/why/agents/openai.yaml +3 -0
  204. package/skills/why/references/epistemics.md +144 -0
  205. package/skills/why/references/investigator-prompt.md +103 -0
  206. package/skills/why/references/source-playbook.md +17 -0
  207. package/skills/why/references/sources/code-archaeology.md +88 -0
  208. package/skills/why/references/sources/databricks.md +70 -0
  209. package/skills/why/references/sources/datadog.md +99 -0
  210. package/skills/why/references/sources/incident-postmortem.md +15 -0
  211. package/skills/why/references/sources/linear.md +48 -0
  212. package/skills/why/references/sources/notion.md +55 -0
  213. package/skills/why/references/sources/sentry.md +100 -0
  214. package/skills/why/references/sources/slack.md +54 -0
  215. package/skills/why/references/synthesizer-prompt.md +135 -0
@@ -0,0 +1,169 @@
1
+ import type * as T from "./types.ts";
2
+ export const renderJson = (verdict: T.WatcherVerdict): string =>
3
+ `${JSON.stringify(verdict)}\n`;
4
+ function ciCell(row: T.PrSnapshot): string {
5
+ if (row.kind !== "open") return "\u2014";
6
+ const was = row.ci.hadPreviousPassingCi ? ", was ✅" : "";
7
+ switch (row.ci.kind) {
8
+ case "ci-clean":
9
+ return "✅";
10
+ case "ci-pending":
11
+ return `⏳ ${row.ci.pending.length} pending${was}`;
12
+ case "ci-failing":
13
+ return `❌ ${row.ci.failed.length} failed${row.ci.pending.length ? `, ${row.ci.pending.length} pending` : ""}${was}`;
14
+ case "ci-github-rejected":
15
+ return `❌ GitHub reports failing checks${was}`;
16
+ default: {
17
+ const exhaustive: never = row.ci;
18
+ return exhaustive;
19
+ }
20
+ }
21
+ }
22
+ function reviewCell(row: T.PrSnapshot): string {
23
+ if (row.kind !== "open") return "\u2014";
24
+ const open = row.threads.length;
25
+ return row.reviewAutomationRunning
26
+ ? open
27
+ ? `🤖 running, ${open} open`
28
+ : "🤖 running"
29
+ : open
30
+ ? `📝 ${open} open`
31
+ : "✅";
32
+ }
33
+ function mergeCell(row: T.PrSnapshot): string {
34
+ if (row.kind === "merged") return "✅ merged";
35
+ if (row.kind === "closed") return "❌ closed";
36
+ if (row.facts.isDraft) return "⏸ draft";
37
+ if (row.facts.reviewDecision === "CHANGES_REQUESTED")
38
+ return "⚠️ changes requested";
39
+ return row.facts.mergeable === "CONFLICTING" ||
40
+ row.facts.mergeStateStatus === "DIRTY" ||
41
+ row.facts.mergeStateStatus === "CONFLICTING"
42
+ ? "⚠️ conflict"
43
+ : "✅";
44
+ }
45
+ export function renderStatusTable(rows: T.NonEmpty<T.PrSnapshot>): string {
46
+ const lines = ["| PR | CI | Review | Merge |", "| --- | --- | --- | --- |"];
47
+ for (const row of rows) {
48
+ const url = `https://github.com/${row.context.owner}/${row.context.repo}/pull/${row.context.number}`;
49
+ lines.push(
50
+ `| [#${row.context.number}](${url}) | ${ciCell(row)} | ${reviewCell(row)} | ${mergeCell(row)} |`
51
+ );
52
+ }
53
+ return `${lines.join("\n")}\n`;
54
+ }
55
+ function threadLine(thread: T.ReviewThread): string {
56
+ const comment = thread.firstComment;
57
+ return [
58
+ thread.id,
59
+ comment?.path ?? "None",
60
+ comment?.line ?? "None",
61
+ comment?.authorLogin ?? "None",
62
+ `isBugBot=${thread.isBugbot}`,
63
+ `bugbotReviewPasses=${thread.bugbotReviewPasses}`,
64
+ (comment?.body ?? "").split(/\r?\n/, 1)[0]?.slice(0, 180) ?? "",
65
+ ].join(" ");
66
+ }
67
+ type StatusQueryBlocker = {
68
+ readonly kind: "status-query";
69
+ readonly failures: number;
70
+ readonly failure: { readonly detail: string };
71
+ };
72
+ function renderBlocker(blocker: T.MergeBlocker | StatusQueryBlocker): string {
73
+ switch (blocker.kind) {
74
+ case "merge-conflicts":
75
+ return [
76
+ "BLOCKER: merge-conflicts",
77
+ `pr=${blocker.pr.number}`,
78
+ `mergeable=${blocker.facts.mergeable}`,
79
+ `mergeStateStatus=${blocker.facts.mergeStateStatus}`,
80
+ "action=resolve merge conflicts before waiting for CI",
81
+ ].join("\n");
82
+ case "review-threads":
83
+ return [
84
+ "BLOCKER: review-threads",
85
+ `pr=${blocker.pr.number}`,
86
+ `unresolved=${blocker.threads.length}`,
87
+ ...blocker.threads.map(threadLine),
88
+ ].join("\n");
89
+ case "failing-checks": {
90
+ const failed = blocker.ci.kind === "ci-failing" ? blocker.ci.failed : [];
91
+ const details = failed.map(
92
+ (check) =>
93
+ `${check.name} ${check.reportedState} ${check.description} ${check.link}`
94
+ );
95
+ if (blocker.ci.kind === "ci-github-rejected")
96
+ details.push(
97
+ `mergeStateStatus=${blocker.ci.github.mergeStateStatus}`,
98
+ `headRollupState=${blocker.ci.github.headRollupState}`
99
+ );
100
+ return [
101
+ "BLOCKER: failing-checks",
102
+ `pr=${blocker.pr.number}`,
103
+ `failed=${failed.length}`,
104
+ ...details,
105
+ ].join("\n");
106
+ }
107
+ case "merge-gate": {
108
+ const action =
109
+ blocker.reason === "closed-without-merge"
110
+ ? "restore or remove the closed PR from the queued stack"
111
+ : blocker.reason === "draft-pr"
112
+ ? "mark the PR ready for review before waiting for the merge queue"
113
+ : "resolve the changes-requested review before waiting for the merge queue";
114
+ return [
115
+ `BLOCKER: ${blocker.reason}`,
116
+ `pr=${blocker.pr.number}`,
117
+ `action=${action}`,
118
+ ].join("\n");
119
+ }
120
+ case "status-query":
121
+ return [
122
+ "BLOCKER: status-query",
123
+ `failures=${blocker.failures}`,
124
+ `detail=${blocker.failure.detail}`,
125
+ "action=verify current PR context, GitHub authentication, and API availability, then rearm",
126
+ ].join("\n");
127
+ default: {
128
+ const exhaustive: never = blocker;
129
+ return exhaustive;
130
+ }
131
+ }
132
+ }
133
+ export function renderPretty(verdict: T.WatcherVerdict): string {
134
+ switch (verdict.kind) {
135
+ case "QUEUE":
136
+ return `QUEUE: captured ${verdict.queue.length} PR${verdict.queue.length === 1 ? "" : "s"} bottom-to-top: ${verdict.queue.map((pr) => `#${pr.number}`).join(",")}\n`;
137
+ case "STATUS":
138
+ return renderStatusTable(verdict.rows);
139
+ case "WAITING":
140
+ return verdict.reason.kind === "pending-checks"
141
+ ? `WAITING: frontier=#${verdict.frontier.number}; ${verdict.reason.pending.length} check${verdict.reason.pending.length === 1 ? "" : "s"} pending\n`
142
+ : `WAITING: frontier=#${verdict.frontier.number} is blocker-free; waiting for merge queue (${verdict.reason.unmergedCount} PR${verdict.reason.unmergedCount === 1 ? "" : "s"} unmerged)\n`;
143
+ case "ADVANCE":
144
+ return `ADVANCE: merged #${verdict.merged.number}; next=#${verdict.frontier.number}; remaining=${verdict.remaining}\n`;
145
+ case "RETRY":
146
+ return `RETRY: GitHub status query failed; retrying in ${verdict.retryInSeconds}s\ndetail=${verdict.failure.detail}\n`;
147
+ case "BLOCKER":
148
+ return `${renderBlocker(verdict.blocker)}\n`;
149
+ case "READY": {
150
+ const detail =
151
+ verdict.scope.kind === "single" && verdict.scope.pr.kind === "ready-pr"
152
+ ? `\nmergeStateStatus=${verdict.scope.pr.proof.ci.github.mergeStateStatus}\nreviewDecision=${verdict.scope.pr.proof.gate.reviewDecision}\nisDraft=${verdict.scope.pr.proof.gate.draft === "draft-allowed"}${verdict.scope.pr.proof.gate.draft === "draft-allowed" ? "\nnote=draft allowed (--allow-draft); leave draft \u2014 do not mark ready" : ""}`
153
+ : "";
154
+ return `READY: no merge conflicts, no unresolved review threads, no failing or pending checks${detail}\n`;
155
+ }
156
+ case "COMPLETE":
157
+ return `COMPLETE: queued stack merged (${verdict.queue.length} PR${verdict.queue.length === 1 ? "" : "s"})\n`;
158
+ case "TIMEOUT":
159
+ if (verdict.reason.kind === "pending-checks")
160
+ return "TIMEOUT: checks still pending\n";
161
+ if (verdict.reason.kind === "status-unavailable")
162
+ return "TIMEOUT: GitHub status remained unavailable\n";
163
+ return `TIMEOUT: queued stack still has ${verdict.reason.unmergedCount} PR${verdict.reason.unmergedCount === 1 ? "" : "s"} unmerged; frontier=#${verdict.reason.frontier.number}\n`;
164
+ default: {
165
+ const exhaustive: never = verdict;
166
+ return exhaustive;
167
+ }
168
+ }
169
+ }
@@ -0,0 +1,13 @@
1
+ {
2
+ "compilerOptions": {
3
+ "allowImportingTsExtensions": true,
4
+ "module": "esnext",
5
+ "moduleResolution": "bundler",
6
+ "noEmit": true,
7
+ "skipLibCheck": true,
8
+ "strict": true,
9
+ "target": "esnext",
10
+ "types": ["bun-types"]
11
+ },
12
+ "include": ["*.ts"]
13
+ }
@@ -0,0 +1,93 @@
1
+ import { parsePrNumber } from "./types.ts";
2
+ import type {
3
+ CiClean,
4
+ GitHubMergeAllowed,
5
+ PrContext,
6
+ ReadyPr,
7
+ TerminalVerdict,
8
+ } from "./types.ts";
9
+
10
+ type ReadyVerdict = Extract<TerminalVerdict, { readonly kind: "READY" }>;
11
+
12
+ const context = {
13
+ owner: "octocat",
14
+ repo: "hello-world",
15
+ number: parsePrNumber(123),
16
+ } satisfies PrContext;
17
+ const cleanCi = {
18
+ kind: "ci-clean",
19
+ source: "gh-pr-checks",
20
+ all: [
21
+ {
22
+ kind: "passed",
23
+ name: "ci",
24
+ reportedState: "SUCCESS",
25
+ description: "",
26
+ link: "",
27
+ workflow: "",
28
+ },
29
+ ],
30
+ failed: [],
31
+ pending: [],
32
+ hadPreviousPassingCi: false,
33
+ github: {
34
+ kind: "allowed",
35
+ basis: "merge-state",
36
+ mergeStateStatus: "CLEAN",
37
+ headRollupState: "SUCCESS",
38
+ },
39
+ } satisfies CiClean;
40
+ const readyPr = {
41
+ kind: "ready-pr",
42
+ context,
43
+ proof: {
44
+ mergeability: "clear",
45
+ threads: [],
46
+ ci: cleanCi,
47
+ gate: {
48
+ state: "OPEN",
49
+ reviewDecision: "APPROVED",
50
+ draft: "not-draft",
51
+ },
52
+ },
53
+ } satisfies ReadyPr;
54
+ const ready = {
55
+ schemaVersion: 1,
56
+ sequence: 1,
57
+ observedAt: "2026-07-26T00:00:00.000Z",
58
+ mode: "single",
59
+ kind: "READY",
60
+ terminal: true,
61
+ exitCode: 0,
62
+ scope: { kind: "single", pr: readyPr },
63
+ } satisfies ReadyVerdict;
64
+
65
+ void ready;
66
+
67
+ // PR 179929's shape. Each assertion below stays a single short statement so a
68
+ // reformat cannot drift the directive away from the line that actually errors.
69
+ const refused = {
70
+ kind: "allowed",
71
+ basis: "rollup",
72
+ mergeStateStatus: "BLOCKED",
73
+ headRollupState: "FAILURE",
74
+ } as const;
75
+
76
+ // @ts-expect-error BLOCKED with a failing rollup is a refusal, not an allowance.
77
+ const refusalIsNotAllowed: GitHubMergeAllowed = refused;
78
+
79
+ // @ts-expect-error CI cannot be clean while GitHub refuses the merge.
80
+ const refusalIsNotClean: CiClean = { ...cleanCi, github: refused };
81
+
82
+ // @ts-expect-error READY cannot carry the failing-checks exit code.
83
+ const readyWithBlockerExit: ReadyVerdict = { ...ready, exitCode: 4 };
84
+
85
+ const unprovenPr = { kind: "ready-pr", context } as const;
86
+
87
+ // @ts-expect-error An open READY row must carry positive readiness proof.
88
+ const readyWithoutProof: ReadyPr = unprovenPr;
89
+
90
+ void refusalIsNotAllowed;
91
+ void refusalIsNotClean;
92
+ void readyWithBlockerExit;
93
+ void readyWithoutProof;
@@ -0,0 +1,401 @@
1
+ declare const prNumberBrand: unique symbol;
2
+ export type PrNumber = number & { readonly [prNumberBrand]: "PrNumber" };
3
+ export type NonEmpty<T> = readonly [T, ...T[]];
4
+ export function nonEmpty<T>(items: readonly T[]): NonEmpty<T> | null {
5
+ return items.length === 0 ? null : [items[0], ...items.slice(1)];
6
+ }
7
+ export function parsePrNumber(value: unknown, label = "PR number"): PrNumber {
8
+ if (typeof value !== "number" || !Number.isInteger(value) || value <= 0)
9
+ throw new Error(`${label} must be a positive integer`);
10
+ return value as PrNumber;
11
+ }
12
+ export interface Repository {
13
+ readonly owner: string;
14
+ readonly repo: string;
15
+ }
16
+ export interface PrContext extends Repository {
17
+ readonly number: PrNumber;
18
+ }
19
+ export type MergeStateStatus =
20
+ | "BEHIND"
21
+ | "BLOCKED"
22
+ | "CLEAN"
23
+ | "CONFLICTING"
24
+ | "DIRTY"
25
+ | "DRAFT"
26
+ | "HAS_HOOKS"
27
+ | "UNKNOWN"
28
+ | "UNSTABLE";
29
+ export type RollupState =
30
+ | "ERROR"
31
+ | "EXPECTED"
32
+ | "FAILURE"
33
+ | "PENDING"
34
+ | "SUCCESS"
35
+ | null;
36
+ export type ReviewDecision =
37
+ | "APPROVED"
38
+ | "CHANGES_REQUESTED"
39
+ | "REVIEW_REQUIRED"
40
+ | null;
41
+ export interface PullRequestFacts {
42
+ readonly context: PrContext;
43
+ readonly mergeable: "MERGEABLE" | "CONFLICTING" | "UNKNOWN";
44
+ readonly mergeStateStatus: MergeStateStatus;
45
+ readonly reviewDecision: ReviewDecision;
46
+ readonly headRefOid: string | null;
47
+ readonly headRefName: string;
48
+ readonly baseRefName: string;
49
+ readonly state: "OPEN" | "CLOSED" | "MERGED";
50
+ readonly mergedAt: string | null;
51
+ readonly isDraft: boolean;
52
+ }
53
+ export interface OpenPullRequest {
54
+ readonly number: PrNumber;
55
+ readonly headRefName: string;
56
+ readonly baseRefName: string;
57
+ }
58
+ export interface ReviewComment {
59
+ readonly authorLogin: string | null;
60
+ readonly body: string;
61
+ readonly path: string | null;
62
+ readonly line: number | null;
63
+ readonly createdAt: string;
64
+ }
65
+ export interface ReviewThread {
66
+ readonly id: string;
67
+ readonly firstComment: ReviewComment | null;
68
+ readonly isBugbot: boolean;
69
+ readonly bugbotReviewPasses: number;
70
+ }
71
+ interface CheckDetails {
72
+ readonly name: string;
73
+ readonly reportedState: string;
74
+ readonly description: string;
75
+ readonly link: string;
76
+ readonly workflow: string;
77
+ }
78
+ export type Check =
79
+ | (CheckDetails & { readonly kind: "passed" })
80
+ | (CheckDetails & { readonly kind: "skipped" })
81
+ | (CheckDetails & { readonly kind: "failed" })
82
+ | (CheckDetails & { readonly kind: "pending" })
83
+ | (CheckDetails & {
84
+ readonly kind: "code-review-gate";
85
+ readonly name: "Code Review Gate";
86
+ });
87
+ export type FailedCheck = Extract<Check, { readonly kind: "failed" }>;
88
+ export type PendingCheck = Extract<Check, { readonly kind: "pending" }>;
89
+ export interface CheckRead {
90
+ readonly source: "gh-pr-checks" | "graphql-rollup";
91
+ readonly checks: NonEmpty<Check>;
92
+ }
93
+ export interface CommitRollup {
94
+ readonly oid: string;
95
+ readonly state: RollupState;
96
+ }
97
+ export interface GitHubMergeRefusal {
98
+ readonly kind: "refused";
99
+ readonly mergeStateStatus: "BLOCKED";
100
+ readonly headRollupState: "ERROR" | "FAILURE";
101
+ }
102
+ export type GitHubMergeAllowed =
103
+ | {
104
+ readonly kind: "allowed";
105
+ readonly basis: "merge-state";
106
+ readonly mergeStateStatus: Exclude<MergeStateStatus, "BLOCKED">;
107
+ readonly headRollupState: RollupState;
108
+ }
109
+ | {
110
+ readonly kind: "allowed";
111
+ readonly basis: "rollup";
112
+ readonly mergeStateStatus: "BLOCKED";
113
+ readonly headRollupState: Exclude<RollupState, "ERROR" | "FAILURE">;
114
+ };
115
+ export type GitHubMergeAssessment = GitHubMergeAllowed | GitHubMergeRefusal;
116
+ interface CiBase {
117
+ readonly source: CheckRead["source"];
118
+ readonly all: NonEmpty<Check>;
119
+ readonly hadPreviousPassingCi: boolean;
120
+ }
121
+ export type CiFailing = CiBase & {
122
+ readonly kind: "ci-failing";
123
+ readonly failed: NonEmpty<FailedCheck>;
124
+ readonly pending: readonly PendingCheck[];
125
+ readonly github: GitHubMergeAssessment;
126
+ };
127
+ export type CiGithubRejected = CiBase & {
128
+ readonly kind: "ci-github-rejected";
129
+ readonly failed: readonly [];
130
+ readonly pending: readonly PendingCheck[];
131
+ readonly github: GitHubMergeRefusal;
132
+ };
133
+ export type CiPending = CiBase & {
134
+ readonly kind: "ci-pending";
135
+ readonly failed: readonly [];
136
+ readonly pending: NonEmpty<PendingCheck>;
137
+ };
138
+ export type CiClean = CiBase & {
139
+ readonly kind: "ci-clean";
140
+ readonly failed: readonly [];
141
+ readonly pending: readonly [];
142
+ readonly github: GitHubMergeAllowed;
143
+ };
144
+ export type CiState = CiFailing | CiGithubRejected | CiPending | CiClean;
145
+ export type PrSnapshot =
146
+ | {
147
+ readonly kind: "merged" | "closed";
148
+ readonly context: PrContext;
149
+ readonly facts: PullRequestFacts;
150
+ }
151
+ | {
152
+ readonly kind: "open";
153
+ readonly context: PrContext;
154
+ readonly facts: PullRequestFacts;
155
+ readonly threads: readonly ReviewThread[];
156
+ readonly ci: CiState;
157
+ readonly reviewAutomationRunning: boolean;
158
+ };
159
+ export interface ReadyPr {
160
+ readonly kind: "ready-pr";
161
+ readonly context: PrContext;
162
+ readonly proof: {
163
+ readonly mergeability: "clear";
164
+ readonly threads: readonly [];
165
+ readonly ci: CiClean;
166
+ readonly gate: {
167
+ readonly state: "OPEN";
168
+ readonly reviewDecision: Exclude<ReviewDecision, "CHANGES_REQUESTED">;
169
+ readonly draft: "not-draft" | "draft-allowed";
170
+ };
171
+ };
172
+ }
173
+ export interface MergedPr {
174
+ readonly kind: "merged-pr";
175
+ readonly context: PrContext;
176
+ readonly mergedAt: string | null;
177
+ }
178
+ export type MergeGateReason =
179
+ | "closed-without-merge"
180
+ | "draft-pr"
181
+ | "changes-requested";
182
+ export type MergeBlocker =
183
+ | {
184
+ readonly kind: "merge-conflicts";
185
+ readonly pr: PrContext;
186
+ readonly facts: PullRequestFacts;
187
+ }
188
+ | {
189
+ readonly kind: "review-threads";
190
+ readonly pr: PrContext;
191
+ readonly threads: NonEmpty<ReviewThread>;
192
+ }
193
+ | {
194
+ readonly kind: "failing-checks";
195
+ readonly pr: PrContext;
196
+ readonly ci: CiFailing | CiGithubRejected;
197
+ }
198
+ | {
199
+ readonly kind: "merge-gate";
200
+ readonly pr: PrContext;
201
+ readonly reason: MergeGateReason;
202
+ };
203
+ export type QueryFailure =
204
+ | {
205
+ readonly kind: "json-parse";
206
+ readonly retryable: true;
207
+ readonly detail: string;
208
+ }
209
+ | {
210
+ readonly kind: "missing-key";
211
+ readonly retryable: true;
212
+ readonly detail: string;
213
+ readonly rawValue?: string;
214
+ }
215
+ | {
216
+ readonly kind: "command-exit";
217
+ readonly retryable: true;
218
+ readonly detail: string;
219
+ readonly code: number;
220
+ }
221
+ | {
222
+ readonly kind: "checks-unavailable";
223
+ readonly retryable: true;
224
+ readonly detail: string;
225
+ }
226
+ | {
227
+ readonly kind: "invalid-context-url";
228
+ readonly retryable: false;
229
+ readonly detail: string;
230
+ readonly rawValue: string;
231
+ };
232
+ /**
233
+ * `frontier` names the lowest unmerged PR that is actually waiting, and
234
+ * `pending` is that PR's checks only. Pooling every row's pending under the
235
+ * bottom PR's number misattributed upstack waits to the frontier.
236
+ *
237
+ * This decision serves single and `--stack` mode. Queued mode deliberately
238
+ * reports its own merge frontier instead: when that PR is blocker-free it
239
+ * emits a merge-queue wait that ignores upstack pending, because upstack
240
+ * checks do not block the frontier's merge. That is the Python watcher's
241
+ * contract, not an attribution bug.
242
+ */
243
+ export interface WaitingDecision {
244
+ readonly kind: "waiting";
245
+ readonly frontier: PrContext;
246
+ readonly pending: NonEmpty<PendingCheck>;
247
+ }
248
+ export type PrDecision =
249
+ | { readonly kind: "blocker"; readonly blocker: MergeBlocker }
250
+ | WaitingDecision
251
+ | { readonly kind: "ready"; readonly pr: ReadyPr }
252
+ | { readonly kind: "merged"; readonly pr: MergedPr };
253
+ export type StackDecision =
254
+ | { readonly kind: "blocker"; readonly blocker: MergeBlocker }
255
+ | WaitingDecision
256
+ | { readonly kind: "clear"; readonly prs: NonEmpty<ReadyPr | MergedPr> };
257
+ export type WatchMode = "single" | "stack" | "queued-stack";
258
+ interface EventBase<K extends string, M extends WatchMode = WatchMode> {
259
+ readonly schemaVersion: 1;
260
+ readonly sequence: number;
261
+ readonly observedAt: string;
262
+ readonly mode: M;
263
+ readonly kind: K;
264
+ }
265
+ interface Progress<K extends string, M extends WatchMode = WatchMode>
266
+ extends EventBase<K, M> {
267
+ readonly terminal: false;
268
+ }
269
+ interface Terminal<
270
+ K extends string,
271
+ C extends number,
272
+ M extends WatchMode = WatchMode,
273
+ > extends EventBase<K, M> {
274
+ readonly terminal: true;
275
+ readonly exitCode: C;
276
+ }
277
+ export type ProgressVerdict =
278
+ | (Progress<"QUEUE", "queued-stack"> & {
279
+ readonly queue: NonEmpty<PrContext>;
280
+ })
281
+ | (Progress<"STATUS", "stack" | "queued-stack"> & {
282
+ readonly reason: "poll" | "whole-stack-sweep";
283
+ readonly rows: NonEmpty<PrSnapshot>;
284
+ })
285
+ | (Progress<"WAITING"> & {
286
+ readonly frontier: PrContext;
287
+ readonly reason:
288
+ | {
289
+ readonly kind: "pending-checks";
290
+ readonly pending: NonEmpty<PendingCheck>;
291
+ }
292
+ | { readonly kind: "merge-queue"; readonly unmergedCount: number };
293
+ })
294
+ | (Progress<"ADVANCE", "queued-stack"> & {
295
+ readonly merged: PrContext;
296
+ readonly frontier: PrContext;
297
+ readonly remaining: number;
298
+ })
299
+ | (Progress<"RETRY"> & {
300
+ readonly failure: QueryFailure;
301
+ readonly consecutiveFailures: number;
302
+ readonly retryInSeconds: number;
303
+ });
304
+ export type BlockerVerdict =
305
+ | (Terminal<"BLOCKER", 2> & {
306
+ readonly blocker: Extract<
307
+ MergeBlocker,
308
+ { readonly kind: "merge-conflicts" }
309
+ >;
310
+ })
311
+ | (Terminal<"BLOCKER", 3> & {
312
+ readonly blocker: Extract<
313
+ MergeBlocker,
314
+ { readonly kind: "review-threads" }
315
+ >;
316
+ })
317
+ | (Terminal<"BLOCKER", 4> & {
318
+ readonly blocker: Extract<
319
+ MergeBlocker,
320
+ { readonly kind: "failing-checks" }
321
+ >;
322
+ })
323
+ | (Terminal<"BLOCKER", 6> & {
324
+ readonly blocker: Extract<MergeBlocker, { readonly kind: "merge-gate" }>;
325
+ })
326
+ | (Terminal<"BLOCKER", 7> & {
327
+ readonly blocker: {
328
+ readonly kind: "status-query";
329
+ readonly failures: number;
330
+ readonly failure: QueryFailure;
331
+ };
332
+ });
333
+ export type TimeoutVerdict = Terminal<"TIMEOUT", 5> & {
334
+ readonly reason:
335
+ | {
336
+ readonly kind: "pending-checks";
337
+ readonly pending: NonEmpty<PendingCheck>;
338
+ }
339
+ | { readonly kind: "status-unavailable"; readonly failure: QueryFailure }
340
+ | {
341
+ readonly kind: "queued-stack";
342
+ readonly frontier: PrContext;
343
+ readonly unmergedCount: number;
344
+ };
345
+ };
346
+ export type TerminalVerdict =
347
+ | (Terminal<"STATUS", 0> & {
348
+ readonly reason: "status-only";
349
+ readonly rows: NonEmpty<PrSnapshot>;
350
+ })
351
+ | (Terminal<"READY", 0, "single" | "stack"> & {
352
+ readonly scope:
353
+ | { readonly kind: "single"; readonly pr: ReadyPr | MergedPr }
354
+ | {
355
+ readonly kind: "stack";
356
+ readonly prs: NonEmpty<ReadyPr | MergedPr>;
357
+ };
358
+ })
359
+ | (Terminal<"COMPLETE", 0, "queued-stack"> & {
360
+ readonly queue: NonEmpty<PrContext>;
361
+ readonly merged: NonEmpty<MergedPr>;
362
+ })
363
+ | BlockerVerdict
364
+ | TimeoutVerdict;
365
+ export type WatcherVerdict = ProgressVerdict | TerminalVerdict;
366
+ export type ExitCode = TerminalVerdict["exitCode"];
367
+ export type QueueTerminalVerdict =
368
+ | Extract<TerminalVerdict, { readonly kind: "COMPLETE" }>
369
+ | BlockerVerdict
370
+ | TimeoutVerdict;
371
+ export type ChecksFastPath =
372
+ | { readonly kind: "checks"; readonly checks: readonly Check[] }
373
+ | {
374
+ readonly kind: "unusable";
375
+ readonly exitCode: number;
376
+ readonly stderr: string;
377
+ };
378
+ export interface RollupPage {
379
+ readonly checks: readonly Check[];
380
+ readonly endCursor: string | null;
381
+ }
382
+ export interface GitHubReader {
383
+ originRepo(): Promise<Repository | null>;
384
+ currentPr(pr: PrNumber | null): Promise<PrContext>;
385
+ pullRequest(context: PrContext): Promise<PullRequestFacts>;
386
+ openPullRequests(repository: Repository): Promise<readonly OpenPullRequest[]>;
387
+ checksFastPath(context: PrContext): Promise<ChecksFastPath>;
388
+ checkRollupPage(
389
+ context: PrContext,
390
+ after: string | null
391
+ ): Promise<RollupPage>;
392
+ reviewThreads(context: PrContext): Promise<readonly ReviewThread[]>;
393
+ commitRollups(context: PrContext): Promise<readonly CommitRollup[]>;
394
+ }
395
+ export interface PollingOptions {
396
+ readonly interval: number;
397
+ readonly sweepInterval: number;
398
+ readonly timeout: number;
399
+ readonly maxQueryErrors: number;
400
+ readonly allowDraft: boolean;
401
+ }
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env bun
2
+ import { ensureDependenciesInstalled } from "../bootstrap.ts";
3
+
4
+ ensureDependenciesInstalled();
5
+ const { main } = await import("./cli.ts");
6
+ process.exitCode = await main(process.argv.slice(2));