@jiroamato/pstack 0.0.0-stage → 0.15.15

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 (225) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +78 -2
  3. package/bin/pstack.js +95 -0
  4. package/lib/install.js +103 -0
  5. package/lib/prompt.js +77 -0
  6. package/lib/targets.js +43 -0
  7. package/package.json +38 -5
  8. package/pstack/.claude-plugin/plugin.json +26 -0
  9. package/pstack/.codex-plugin/plugin.json +36 -0
  10. package/pstack/LICENSE +21 -0
  11. package/pstack/LICENSE-cursor-team-kit +21 -0
  12. package/pstack/NOTICE +8 -0
  13. package/pstack/README.md +300 -0
  14. package/pstack/agents/comment-sicko.md +34 -0
  15. package/pstack/agents/poteto-agent.md +10 -0
  16. package/pstack/automations/benny/FOR_AGENTS.md +92 -0
  17. package/pstack/automations/benny/README.md +28 -0
  18. package/pstack/automations/benny/skills/reproduce-and-fix-issues/SKILL.md +313 -0
  19. package/pstack/automations/benny/skills/reproduce-and-fix-issues/references/control-adapter.md +169 -0
  20. package/pstack/automations/benny/skills/reproduce-and-fix-issues/references/feature-map.example.md +205 -0
  21. package/pstack/automations/benny/skills/reproduce-and-fix-issues/references/verify-existing-fix.md +93 -0
  22. package/pstack/automations/benny/skills/setup-benny/SKILL.md +271 -0
  23. package/pstack/automations/benny/skills/triage-issue-reports/SKILL.md +240 -0
  24. package/pstack/automations/benny/skills/triage-issue-reports/references/routing.example.md +61 -0
  25. package/pstack/automations/benny/templates/configuration.example.yaml +84 -0
  26. package/pstack/automations/benny/templates/reproduce-automation-prompt.md +33 -0
  27. package/pstack/automations/benny/templates/triage-automation-prompt.md +39 -0
  28. package/pstack/codex/agents/comment-sicko.toml +36 -0
  29. package/pstack/codex/agents/poteto-agent.toml +11 -0
  30. package/pstack/docs/guide/01-setup.md +80 -0
  31. package/pstack/docs/guide/02-poteto-mode.md +131 -0
  32. package/pstack/docs/guide/03-understand.md +79 -0
  33. package/pstack/docs/guide/04-design.md +133 -0
  34. package/pstack/docs/guide/05-build-and-clean.md +83 -0
  35. package/pstack/docs/guide/06-verify-and-ship.md +130 -0
  36. package/pstack/docs/guide/07-overnight.md +120 -0
  37. package/pstack/docs/guide/08-principles.md +72 -0
  38. package/pstack/docs/guide/09-make-it-yours.md +100 -0
  39. package/pstack/docs/guide/10-recipes-and-pitfalls.md +156 -0
  40. package/pstack/docs/guide/README.md +38 -0
  41. package/pstack/skills/architect/SKILL.md +85 -0
  42. package/pstack/skills/architect/agents/openai.yaml +2 -0
  43. package/pstack/skills/architect/references/design-red-flags.md +57 -0
  44. package/pstack/skills/architect/references/rationale-template.md +35 -0
  45. package/pstack/skills/architect/references/runner-prompt.md +20 -0
  46. package/pstack/skills/arena/SKILL.md +75 -0
  47. package/pstack/skills/arena/agents/openai.yaml +2 -0
  48. package/pstack/skills/automate-me/SKILL.md +104 -0
  49. package/pstack/skills/automate-me/agents/openai.yaml +2 -0
  50. package/pstack/skills/benchmark-checklist/SKILL.md +39 -0
  51. package/pstack/skills/benchmark-checklist/agents/openai.yaml +2 -0
  52. package/pstack/skills/blast-radius/SKILL.md +52 -0
  53. package/pstack/skills/blast-radius/agents/openai.yaml +2 -0
  54. package/pstack/skills/bro/SKILL.md +7 -0
  55. package/pstack/skills/bro/agents/openai.yaml +2 -0
  56. package/pstack/skills/control-cli/SKILL.md +55 -0
  57. package/pstack/skills/control-cli/agents/openai.yaml +2 -0
  58. package/pstack/skills/control-ui/SKILL.md +72 -0
  59. package/pstack/skills/control-ui/agents/openai.yaml +2 -0
  60. package/pstack/skills/correct/SKILL.md +34 -0
  61. package/pstack/skills/correct/agents/openai.yaml +2 -0
  62. package/pstack/skills/create-verification-skill/SKILL.md +47 -0
  63. package/pstack/skills/create-verification-skill/agents/openai.yaml +2 -0
  64. package/pstack/skills/create-verification-skill/references/feature-map-example/README.md +47 -0
  65. package/pstack/skills/create-verification-skill/references/feature-map-example/create-note.md +39 -0
  66. package/pstack/skills/create-verification-skill/references/feature-map-example/search.md +45 -0
  67. package/pstack/skills/deslop/SKILL.md +30 -0
  68. package/pstack/skills/deslop/agents/openai.yaml +2 -0
  69. package/pstack/skills/figure-it-out/SKILL.md +55 -0
  70. package/pstack/skills/figure-it-out/agents/openai.yaml +2 -0
  71. package/pstack/skills/how/SKILL.md +58 -0
  72. package/pstack/skills/how/agents/openai.yaml +2 -0
  73. package/pstack/skills/how/references/explainer-prompt.md +55 -0
  74. package/pstack/skills/how/references/explorer-prompt.md +52 -0
  75. package/pstack/skills/interrogate/SKILL.md +111 -0
  76. package/pstack/skills/interrogate/agents/openai.yaml +2 -0
  77. package/pstack/skills/interrogate/references/code-quality-review.md +47 -0
  78. package/pstack/skills/interrogate/references/lead-judgment.md +58 -0
  79. package/pstack/skills/interrogate/references/reviewer-prompt.md +70 -0
  80. package/pstack/skills/interrogate/references/rubric.md +77 -0
  81. package/pstack/skills/maintain-verification-skill/SKILL.md +41 -0
  82. package/pstack/skills/maintain-verification-skill/agents/openai.yaml +2 -0
  83. package/pstack/skills/make-bot-ui/SKILL.md +289 -0
  84. package/pstack/skills/make-bot-ui/agents/openai.yaml +2 -0
  85. package/pstack/skills/no-comments/SKILL.md +24 -0
  86. package/pstack/skills/no-comments/agents/openai.yaml +2 -0
  87. package/pstack/skills/poteto-help/SKILL.md +156 -0
  88. package/pstack/skills/poteto-help/agents/openai.yaml +2 -0
  89. package/pstack/skills/poteto-help/references/prompting.md +51 -0
  90. package/pstack/skills/poteto-help/references/recipes.md +47 -0
  91. package/pstack/skills/poteto-mode/SKILL.md +143 -0
  92. package/pstack/skills/poteto-mode/agents/openai.yaml +2 -0
  93. package/pstack/skills/poteto-mode/playbooks/authoring-a-skill.md +12 -0
  94. package/pstack/skills/poteto-mode/playbooks/autonomous-run.md +13 -0
  95. package/pstack/skills/poteto-mode/playbooks/autopilot-full.md +13 -0
  96. package/pstack/skills/poteto-mode/playbooks/autopilot-stack.md +16 -0
  97. package/pstack/skills/poteto-mode/playbooks/babysit.md +29 -0
  98. package/pstack/skills/poteto-mode/playbooks/bug-fix.md +15 -0
  99. package/pstack/skills/poteto-mode/playbooks/eval.md +25 -0
  100. package/pstack/skills/poteto-mode/playbooks/feature.md +21 -0
  101. package/pstack/skills/poteto-mode/playbooks/hillclimb.md +21 -0
  102. package/pstack/skills/poteto-mode/playbooks/investigation.md +14 -0
  103. package/pstack/skills/poteto-mode/playbooks/multi-phase-plan.md +155 -0
  104. package/pstack/skills/poteto-mode/playbooks/opening-a-pr.md +38 -0
  105. package/pstack/skills/poteto-mode/playbooks/orchestrate.md +114 -0
  106. package/pstack/skills/poteto-mode/playbooks/pause-safely.md +10 -0
  107. package/pstack/skills/poteto-mode/playbooks/perf-issue.md +25 -0
  108. package/pstack/skills/poteto-mode/playbooks/prototype.md +14 -0
  109. package/pstack/skills/poteto-mode/playbooks/refactoring.md +16 -0
  110. package/pstack/skills/poteto-mode/playbooks/runtime-forensics.md +11 -0
  111. package/pstack/skills/poteto-mode/playbooks/session-pickup.md +11 -0
  112. package/pstack/skills/poteto-mode/playbooks/shipping.md +17 -0
  113. package/pstack/skills/poteto-mode/playbooks/trace-forensics.md +14 -0
  114. package/pstack/skills/poteto-mode/playbooks/visual-parity.md +11 -0
  115. package/pstack/skills/poteto-mode/playbooks/worktree-cleanup.md +14 -0
  116. package/pstack/skills/poteto-mode/references/bugbot-triage.md +142 -0
  117. package/pstack/skills/poteto-mode/scripts/bootstrap.ts +62 -0
  118. package/pstack/skills/poteto-mode/scripts/bun.lock +67 -0
  119. package/pstack/skills/poteto-mode/scripts/check-plan.mjs +185 -0
  120. package/pstack/skills/poteto-mode/scripts/orch/orch.test.ts +634 -0
  121. package/pstack/skills/poteto-mode/scripts/orch/orch.ts +578 -0
  122. package/pstack/skills/poteto-mode/scripts/orch/store.ts +1607 -0
  123. package/pstack/skills/poteto-mode/scripts/package.json +16 -0
  124. package/pstack/skills/poteto-mode/scripts/watch-pr/cli.test.ts +224 -0
  125. package/pstack/skills/poteto-mode/scripts/watch-pr/cli.ts +223 -0
  126. package/pstack/skills/poteto-mode/scripts/watch-pr/fakes.test-helper.ts +118 -0
  127. package/pstack/skills/poteto-mode/scripts/watch-pr/github.test.ts +306 -0
  128. package/pstack/skills/poteto-mode/scripts/watch-pr/github.ts +699 -0
  129. package/pstack/skills/poteto-mode/scripts/watch-pr/policy.test.ts +420 -0
  130. package/pstack/skills/poteto-mode/scripts/watch-pr/policy.ts +832 -0
  131. package/pstack/skills/poteto-mode/scripts/watch-pr/render.ts +169 -0
  132. package/pstack/skills/poteto-mode/scripts/watch-pr/tsconfig.json +13 -0
  133. package/pstack/skills/poteto-mode/scripts/watch-pr/types.compile.ts +93 -0
  134. package/pstack/skills/poteto-mode/scripts/watch-pr/types.ts +401 -0
  135. package/pstack/skills/poteto-mode/scripts/watch-pr/watch-pr +6 -0
  136. package/pstack/skills/poteto-mode/scripts/worktree-audit.sh +92 -0
  137. package/pstack/skills/principle-attack-the-premise/SKILL.md +23 -0
  138. package/pstack/skills/principle-attack-the-premise/agents/openai.yaml +2 -0
  139. package/pstack/skills/principle-boundary-discipline/SKILL.md +34 -0
  140. package/pstack/skills/principle-boundary-discipline/agents/openai.yaml +2 -0
  141. package/pstack/skills/principle-build-the-lever/SKILL.md +23 -0
  142. package/pstack/skills/principle-build-the-lever/agents/openai.yaml +2 -0
  143. package/pstack/skills/principle-encode-lessons-in-structure/SKILL.md +31 -0
  144. package/pstack/skills/principle-encode-lessons-in-structure/agents/openai.yaml +2 -0
  145. package/pstack/skills/principle-exhaust-the-design-space/SKILL.md +21 -0
  146. package/pstack/skills/principle-exhaust-the-design-space/agents/openai.yaml +2 -0
  147. package/pstack/skills/principle-experience-first/SKILL.md +19 -0
  148. package/pstack/skills/principle-experience-first/agents/openai.yaml +2 -0
  149. package/pstack/skills/principle-explain-the-number/SKILL.md +23 -0
  150. package/pstack/skills/principle-explain-the-number/agents/openai.yaml +2 -0
  151. package/pstack/skills/principle-fix-root-causes/SKILL.md +23 -0
  152. package/pstack/skills/principle-fix-root-causes/agents/openai.yaml +2 -0
  153. package/pstack/skills/principle-foundational-thinking/SKILL.md +21 -0
  154. package/pstack/skills/principle-foundational-thinking/agents/openai.yaml +2 -0
  155. package/pstack/skills/principle-guard-the-context-window/SKILL.md +16 -0
  156. package/pstack/skills/principle-guard-the-context-window/agents/openai.yaml +2 -0
  157. package/pstack/skills/principle-laziness-protocol/SKILL.md +18 -0
  158. package/pstack/skills/principle-laziness-protocol/agents/openai.yaml +2 -0
  159. package/pstack/skills/principle-make-operations-idempotent/SKILL.md +24 -0
  160. package/pstack/skills/principle-make-operations-idempotent/agents/openai.yaml +2 -0
  161. package/pstack/skills/principle-migrate-callers-then-delete-legacy-apis/SKILL.md +22 -0
  162. package/pstack/skills/principle-migrate-callers-then-delete-legacy-apis/agents/openai.yaml +2 -0
  163. package/pstack/skills/principle-minimize-reader-load/SKILL.md +23 -0
  164. package/pstack/skills/principle-minimize-reader-load/agents/openai.yaml +2 -0
  165. package/pstack/skills/principle-model-the-domain/SKILL.md +26 -0
  166. package/pstack/skills/principle-model-the-domain/agents/openai.yaml +2 -0
  167. package/pstack/skills/principle-never-block-on-the-human/SKILL.md +20 -0
  168. package/pstack/skills/principle-never-block-on-the-human/agents/openai.yaml +2 -0
  169. package/pstack/skills/principle-outcome-oriented-execution/SKILL.md +21 -0
  170. package/pstack/skills/principle-outcome-oriented-execution/agents/openai.yaml +2 -0
  171. package/pstack/skills/principle-prove-it-works/SKILL.md +22 -0
  172. package/pstack/skills/principle-prove-it-works/agents/openai.yaml +2 -0
  173. package/pstack/skills/principle-redesign-from-first-principles/SKILL.md +16 -0
  174. package/pstack/skills/principle-redesign-from-first-principles/agents/openai.yaml +2 -0
  175. package/pstack/skills/principle-separate-before-serializing-shared-state/SKILL.md +16 -0
  176. package/pstack/skills/principle-separate-before-serializing-shared-state/agents/openai.yaml +2 -0
  177. package/pstack/skills/principle-sequence-verifiable-units/SKILL.md +17 -0
  178. package/pstack/skills/principle-sequence-verifiable-units/agents/openai.yaml +2 -0
  179. package/pstack/skills/principle-subtract-before-you-add/SKILL.md +21 -0
  180. package/pstack/skills/principle-subtract-before-you-add/agents/openai.yaml +2 -0
  181. package/pstack/skills/principle-test-behavior-not-implementation/SKILL.md +25 -0
  182. package/pstack/skills/principle-test-behavior-not-implementation/agents/openai.yaml +2 -0
  183. package/pstack/skills/principle-type-system-discipline/SKILL.md +31 -0
  184. package/pstack/skills/principle-type-system-discipline/agents/openai.yaml +2 -0
  185. package/pstack/skills/pstack-harness/SKILL.md +67 -0
  186. package/pstack/skills/recall/SKILL.md +35 -0
  187. package/pstack/skills/recall/agents/openai.yaml +2 -0
  188. package/pstack/skills/reflect/SKILL.md +76 -0
  189. package/pstack/skills/reflect/agents/openai.yaml +2 -0
  190. package/pstack/skills/reflect/references/divergent-reviewer.md +43 -0
  191. package/pstack/skills/reflect/references/judgment-reviewer.md +42 -0
  192. package/pstack/skills/reflect/references/synthesizer.md +56 -0
  193. package/pstack/skills/reflect/references/tooling-reviewer.md +55 -0
  194. package/pstack/skills/setup-pstack/SKILL.md +110 -0
  195. package/pstack/skills/show-me-your-work/SKILL.md +82 -0
  196. package/pstack/skills/show-me-your-work/agents/openai.yaml +2 -0
  197. package/pstack/skills/show-me-your-work/references/decision-log-template.tsv +1 -0
  198. package/pstack/skills/show-me-your-work/scripts/log.sh +42 -0
  199. package/pstack/skills/swarm/SKILL.md +48 -0
  200. package/pstack/skills/swarm/agents/openai.yaml +2 -0
  201. package/pstack/skills/tdd/SKILL.md +44 -0
  202. package/pstack/skills/tdd/agents/openai.yaml +2 -0
  203. package/pstack/skills/teach/SKILL.md +21 -0
  204. package/pstack/skills/teach/agents/openai.yaml +2 -0
  205. package/pstack/skills/technical-writing/SKILL.md +106 -0
  206. package/pstack/skills/technical-writing/agents/openai.yaml +2 -0
  207. package/pstack/skills/typescript-best-practices/SKILL.md +31 -0
  208. package/pstack/skills/typescript-best-practices/agents/openai.yaml +2 -0
  209. package/pstack/skills/typescript-best-practices/references/patterns.md +324 -0
  210. package/pstack/skills/unslop/SKILL.md +67 -0
  211. package/pstack/skills/unslop/agents/openai.yaml +2 -0
  212. package/pstack/skills/why/SKILL.md +158 -0
  213. package/pstack/skills/why/agents/openai.yaml +2 -0
  214. package/pstack/skills/why/references/epistemics.md +144 -0
  215. package/pstack/skills/why/references/investigator-prompt.md +103 -0
  216. package/pstack/skills/why/references/source-playbook.md +17 -0
  217. package/pstack/skills/why/references/sources/code-archaeology.md +88 -0
  218. package/pstack/skills/why/references/sources/databricks.md +70 -0
  219. package/pstack/skills/why/references/sources/datadog.md +99 -0
  220. package/pstack/skills/why/references/sources/incident-postmortem.md +15 -0
  221. package/pstack/skills/why/references/sources/linear.md +48 -0
  222. package/pstack/skills/why/references/sources/notion.md +55 -0
  223. package/pstack/skills/why/references/sources/sentry.md +100 -0
  224. package/pstack/skills/why/references/sources/slack.md +54 -0
  225. package/pstack/skills/why/references/synthesizer-prompt.md +135 -0
@@ -0,0 +1,420 @@
1
+ import { describe, expect, it } from "bun:test";
2
+ import { WatcherQueryError } from "./github.ts";
3
+ import {
4
+ applyQueueSnapshot,
5
+ assessGitHubMerge,
6
+ classifyPr,
7
+ createQueueState,
8
+ evaluateQueue,
9
+ planQueue,
10
+ queryBackoffSeconds,
11
+ readSnapshot,
12
+ runQueued,
13
+ selectTierMajorStackDecision,
14
+ } from "./policy.ts";
15
+ import {
16
+ fakeReader,
17
+ failedCheck,
18
+ passingCheck,
19
+ pendingCheck,
20
+ } from "./fakes.test-helper.ts";
21
+ import type {
22
+ GitHubReader,
23
+ NonEmpty,
24
+ PollingOptions,
25
+ PrContext,
26
+ ProgressVerdict,
27
+ PullRequestFacts,
28
+ RollupState,
29
+ } from "./types.ts";
30
+ import { parsePrNumber } from "./types.ts";
31
+
32
+ const context = (number: number): PrContext => ({
33
+ owner: "owner",
34
+ repo: "repo",
35
+ number: parsePrNumber(number),
36
+ });
37
+ const options = {
38
+ interval: 10,
39
+ sweepInterval: 300,
40
+ timeout: 0,
41
+ maxQueryErrors: 5,
42
+ allowDraft: false,
43
+ } satisfies PollingOptions;
44
+
45
+ describe("readiness truth table", () => {
46
+ it("covers every specified row and every UNKNOWN rollup value", () => {
47
+ const cases: readonly [
48
+ PullRequestFacts["mergeStateStatus"],
49
+ RollupState,
50
+ "allowed" | "refused",
51
+ ][] = [
52
+ ["BLOCKED", "FAILURE", "refused"],
53
+ ["BLOCKED", "ERROR", "refused"],
54
+ ["BLOCKED", "PENDING", "allowed"],
55
+ ["UNSTABLE", "FAILURE", "allowed"],
56
+ ["UNKNOWN", "ERROR", "allowed"],
57
+ ["UNKNOWN", "EXPECTED", "allowed"],
58
+ ["UNKNOWN", "FAILURE", "allowed"],
59
+ ["UNKNOWN", "PENDING", "allowed"],
60
+ ["UNKNOWN", "SUCCESS", "allowed"],
61
+ ["UNKNOWN", null, "allowed"],
62
+ ["CLEAN", "SUCCESS", "allowed"],
63
+ ];
64
+ for (const [mergeStateStatus, headRollupState, expected] of cases) {
65
+ expect(
66
+ assessGitHubMerge({ mergeStateStatus, headRollupState }).kind
67
+ ).toBe(expected);
68
+ }
69
+ });
70
+
71
+ it("turns a clean visible list plus GitHub refusal into an explicit CI blocker", async () => {
72
+ const reader = fakeReader({
73
+ facts: { mergeStateStatus: "BLOCKED" },
74
+ fastPath: { kind: "checks", checks: [passingCheck()] },
75
+ commitRollups: [{ oid: "head", state: "FAILURE" }],
76
+ });
77
+ const snapshot = await readSnapshot({
78
+ reader,
79
+ context: context(1),
80
+ pendingHistory: "include",
81
+ allowDraft: false,
82
+ });
83
+ expect(snapshot.kind).toBe("open");
84
+ if (snapshot.kind !== "open") throw new Error("expected open snapshot");
85
+ expect(snapshot.ci.kind).toBe("ci-github-rejected");
86
+ expect(classifyPr(snapshot)).toMatchObject({
87
+ kind: "blocker",
88
+ blocker: { kind: "failing-checks" },
89
+ });
90
+ });
91
+ });
92
+
93
+ describe("snapshot query planning", () => {
94
+ it("does not query commit rollups while queued checks are pending", async () => {
95
+ const reader = fakeReader({
96
+ fastPath: { kind: "checks", checks: [pendingCheck()] },
97
+ });
98
+ const snapshot = await readSnapshot({
99
+ reader,
100
+ context: context(2),
101
+ pendingHistory: "omit",
102
+ allowDraft: false,
103
+ });
104
+ expect(snapshot.kind).toBe("open");
105
+ if (snapshot.kind !== "open") throw new Error("expected open snapshot");
106
+ expect(snapshot.ci.kind).toBe("ci-pending");
107
+ expect(reader.calls).toEqual([
108
+ "pullRequest",
109
+ "reviewThreads",
110
+ "checksFastPath",
111
+ ]);
112
+ });
113
+
114
+ it("queries rollups for settled and failed lists", async () => {
115
+ const settled = fakeReader();
116
+ await readSnapshot({
117
+ reader: settled,
118
+ context: context(3),
119
+ pendingHistory: "omit",
120
+ allowDraft: false,
121
+ });
122
+ expect(settled.calls).toContain("commitRollups");
123
+
124
+ const failed = fakeReader({
125
+ fastPath: { kind: "checks", checks: [failedCheck()] },
126
+ });
127
+ await readSnapshot({
128
+ reader: failed,
129
+ context: context(4),
130
+ pendingHistory: "omit",
131
+ allowDraft: false,
132
+ });
133
+ expect(failed.calls).toContain("commitRollups");
134
+ });
135
+
136
+ it("short-circuits merged rows before threads and checks", async () => {
137
+ const reader = fakeReader({
138
+ facts: { state: "MERGED", mergedAt: "2026-07-26T00:00:00Z" },
139
+ });
140
+ expect(
141
+ (
142
+ await readSnapshot({
143
+ reader,
144
+ context: context(5),
145
+ pendingHistory: "include",
146
+ allowDraft: false,
147
+ })
148
+ ).kind
149
+ ).toBe("merged");
150
+ expect(reader.calls).toEqual(["pullRequest"]);
151
+ });
152
+ });
153
+
154
+ it("scans stacks tier-major so an upstack conflict outranks frontier CI", async () => {
155
+ const frontier = await readSnapshot({
156
+ reader: fakeReader({
157
+ fastPath: { kind: "checks", checks: [failedCheck()] },
158
+ commitRollups: [{ oid: "head", state: "FAILURE" }],
159
+ }),
160
+ context: context(10),
161
+ pendingHistory: "omit",
162
+ allowDraft: false,
163
+ });
164
+ const upstack = await readSnapshot({
165
+ reader: fakeReader({ facts: { mergeable: "CONFLICTING" } }),
166
+ context: context(11),
167
+ pendingHistory: "omit",
168
+ allowDraft: false,
169
+ });
170
+ const decision = selectTierMajorStackDecision([frontier, upstack]);
171
+ expect(decision).toMatchObject({
172
+ kind: "blocker",
173
+ blocker: { kind: "merge-conflicts", pr: { number: 11 } },
174
+ });
175
+ });
176
+
177
+ it("attributes a stack wait to the PR whose checks are pending, not the bottom", async () => {
178
+ const readyBottom = await readSnapshot({
179
+ reader: fakeReader(),
180
+ context: context(20),
181
+ pendingHistory: "omit",
182
+ allowDraft: false,
183
+ });
184
+ const pendingUpstack = await readSnapshot({
185
+ reader: fakeReader({
186
+ fastPath: { kind: "checks", checks: [pendingCheck("upstack-build")] },
187
+ }),
188
+ context: context(21),
189
+ pendingHistory: "omit",
190
+ allowDraft: false,
191
+ });
192
+ const decision = selectTierMajorStackDecision([readyBottom, pendingUpstack]);
193
+ expect(decision).toMatchObject({
194
+ kind: "waiting",
195
+ frontier: { number: 21 },
196
+ pending: [{ name: "upstack-build" }],
197
+ });
198
+ });
199
+
200
+ it("waits on a draft while checks are pending, then reports the draft gate", async () => {
201
+ const pending = await readSnapshot({
202
+ reader: fakeReader({
203
+ facts: { isDraft: true },
204
+ fastPath: { kind: "checks", checks: [pendingCheck()] },
205
+ }),
206
+ context: context(12),
207
+ pendingHistory: "omit",
208
+ allowDraft: false,
209
+ });
210
+ expect(classifyPr(pending).kind).toBe("waiting");
211
+
212
+ const settled = await readSnapshot({
213
+ reader: fakeReader({ facts: { isDraft: true } }),
214
+ context: context(12),
215
+ pendingHistory: "omit",
216
+ allowDraft: false,
217
+ });
218
+ expect(classifyPr(settled)).toMatchObject({
219
+ kind: "blocker",
220
+ blocker: { kind: "merge-gate", reason: "draft-pr" },
221
+ });
222
+ });
223
+
224
+ describe("queued-stack cadence", () => {
225
+ async function openSnapshot(pr: PrContext) {
226
+ return readSnapshot({
227
+ reader: fakeReader(),
228
+ context: pr,
229
+ pendingHistory: "omit",
230
+ allowDraft: false,
231
+ });
232
+ }
233
+
234
+ it("drops a sweep head only after its snapshot succeeds", async () => {
235
+ const queue = [
236
+ context(20),
237
+ context(21),
238
+ context(22),
239
+ ] satisfies NonEmpty<PrContext>;
240
+ let state = createQueueState(queue, 0);
241
+ const first = await openSnapshot(queue[0]);
242
+ state = applyQueueSnapshot(state, first, 0, options).state;
243
+ expect(state.work).toMatchObject({
244
+ kind: "whole-stack-sweep",
245
+ remaining: [{ number: 21 }, { number: 22 }],
246
+ });
247
+ const second = await openSnapshot(queue[1]);
248
+ state = applyQueueSnapshot(state, second, 60, options).state;
249
+ expect(state.work).toMatchObject({
250
+ kind: "whole-stack-sweep",
251
+ remaining: [{ number: 22 }],
252
+ });
253
+ });
254
+
255
+ it("resumes the sweep at the PR whose read failed", async () => {
256
+ const middle = context(21);
257
+ const base = fakeReader();
258
+ let failNext = true;
259
+ const timeline: string[] = [];
260
+ const reader = {
261
+ ...base,
262
+ async pullRequest(pr: PrContext) {
263
+ if (pr.number === middle.number && failNext) {
264
+ failNext = false;
265
+ timeline.push(`fail:${pr.number}`);
266
+ throw new WatcherQueryError({
267
+ kind: "command-exit",
268
+ retryable: true,
269
+ detail: "rate limited",
270
+ code: 1,
271
+ });
272
+ }
273
+ timeline.push(`read:${pr.number}`);
274
+ return base.pullRequest(pr);
275
+ },
276
+ } satisfies GitHubReader;
277
+ let now = 0;
278
+ let sleeps = 0;
279
+ const running = runQueued({
280
+ dependencies: {
281
+ reader,
282
+ clock: {
283
+ now: () => now,
284
+ observedAt: () => "2026-07-26T00:00:00.000Z",
285
+ async sleep(seconds) {
286
+ timeline.push("sleep");
287
+ now += seconds;
288
+ sleeps += 1;
289
+ if (sleeps === 2) throw new Error("stop after resume proof");
290
+ },
291
+ },
292
+ emit(verdict) {
293
+ timeline.push(`emit:${verdict.kind}`);
294
+ },
295
+ },
296
+ contexts: [context(20), middle, context(22)],
297
+ options,
298
+ });
299
+ await expect(running).rejects.toThrow("stop after resume proof");
300
+ expect(timeline).toEqual([
301
+ "emit:QUEUE",
302
+ "read:20",
303
+ "fail:21",
304
+ "emit:RETRY",
305
+ "sleep",
306
+ "read:21",
307
+ "read:22",
308
+ "emit:STATUS",
309
+ "emit:WAITING",
310
+ "sleep",
311
+ ]);
312
+ });
313
+
314
+ it("emits a completed sweep only after its final successful snapshot", async () => {
315
+ const queue = [context(30), context(31)] satisfies NonEmpty<PrContext>;
316
+ let state = createQueueState(queue, 0);
317
+ const first = applyQueueSnapshot(
318
+ state,
319
+ await openSnapshot(queue[0]),
320
+ 0,
321
+ options
322
+ );
323
+ expect(first.completedSweepRows).toBeNull();
324
+ state = first.state;
325
+ const second = applyQueueSnapshot(
326
+ state,
327
+ await openSnapshot(queue[1]),
328
+ 5,
329
+ options
330
+ );
331
+ expect(
332
+ second.completedSweepRows?.map((row) => Number(row.context.number))
333
+ ).toEqual([30, 31]);
334
+ expect(second.state.nextSweepAt).toBe(305);
335
+ });
336
+
337
+ it("ADVANCE continues directly to the new frontier without sleeping", async () => {
338
+ const one = context(40);
339
+ const two = context(41);
340
+ const base = fakeReader();
341
+ const reads = new Map<number, number>();
342
+ const timeline: string[] = [];
343
+ const reader = {
344
+ ...base,
345
+ async pullRequest(pr: PrContext) {
346
+ timeline.push(`read:${pr.number}`);
347
+ const facts = await base.pullRequest(pr);
348
+ const count = (reads.get(pr.number) ?? 0) + 1;
349
+ reads.set(pr.number, count);
350
+ return pr.number === one.number && count > 1
351
+ ? {
352
+ ...facts,
353
+ state: "MERGED" as const,
354
+ mergedAt: "2026-07-26T00:00:00Z",
355
+ }
356
+ : facts;
357
+ },
358
+ } satisfies GitHubReader;
359
+ let now = 0;
360
+ let sleeps = 0;
361
+ const emitted: ProgressVerdict[] = [];
362
+ const running = runQueued({
363
+ dependencies: {
364
+ reader,
365
+ clock: {
366
+ now: () => now,
367
+ observedAt: () => "2026-07-26T00:00:00.000Z",
368
+ async sleep(seconds) {
369
+ timeline.push("sleep");
370
+ now += seconds;
371
+ sleeps += 1;
372
+ if (sleeps === 2) throw new Error("stop after advance proof");
373
+ },
374
+ },
375
+ emit(verdict) {
376
+ emitted.push(verdict);
377
+ timeline.push(`emit:${verdict.kind}`);
378
+ },
379
+ },
380
+ contexts: [one, two],
381
+ options,
382
+ });
383
+ await expect(running).rejects.toThrow("stop after advance proof");
384
+ expect(emitted.some((event) => event.kind === "ADVANCE")).toBe(true);
385
+ const firstSleep = timeline.indexOf("sleep");
386
+ expect(timeline.slice(firstSleep, firstSleep + 5)).toEqual([
387
+ "sleep",
388
+ "read:40",
389
+ "emit:ADVANCE",
390
+ "read:41",
391
+ "emit:WAITING",
392
+ ]);
393
+ });
394
+
395
+ it("deduplicates identical waits and schedules the next due sweep", async () => {
396
+ const queue = [context(50)] satisfies NonEmpty<PrContext>;
397
+ let state = createQueueState(queue, 0);
398
+ state = applyQueueSnapshot(
399
+ state,
400
+ await openSnapshot(queue[0]),
401
+ 0,
402
+ options
403
+ ).state;
404
+ const first = evaluateQueue(state, 0, options);
405
+ expect(first.kind).toBe("waiting");
406
+ if (first.kind !== "waiting") throw new Error("expected waiting");
407
+ expect(first.emit).toBe(true);
408
+ const second = evaluateQueue(first.state, 10, options);
409
+ expect(second.kind).toBe("waiting");
410
+ if (second.kind !== "waiting") throw new Error("expected waiting");
411
+ expect(second.emit).toBe(false);
412
+ expect(planQueue(second.state, 300).work?.kind).toBe("whole-stack-sweep");
413
+ });
414
+ });
415
+
416
+ it("uses the specified retry floor and cap", () => {
417
+ expect(queryBackoffSeconds(1, 1)).toBe(60);
418
+ expect(queryBackoffSeconds(1, 2)).toBe(120);
419
+ expect(queryBackoffSeconds(60, 4)).toBe(300);
420
+ });