@zenspc/pi-pstack 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 (128) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +49 -0
  3. package/agents/comment-sicko.md +34 -0
  4. package/agents/poteto-agent.md +13 -0
  5. package/extensions/pstack/config.ts +228 -0
  6. package/extensions/pstack/index.ts +211 -0
  7. package/package.json +57 -0
  8. package/skills/architect/SKILL.md +82 -0
  9. package/skills/architect/references/design-red-flags.md +33 -0
  10. package/skills/architect/references/rationale-template.md +35 -0
  11. package/skills/architect/references/runner-prompt.md +20 -0
  12. package/skills/arena/SKILL.md +70 -0
  13. package/skills/automate-me/SKILL.md +114 -0
  14. package/skills/blast-radius/SKILL.md +49 -0
  15. package/skills/bro/SKILL.md +6 -0
  16. package/skills/create-verification-skill/SKILL.md +43 -0
  17. package/skills/create-verification-skill/references/feature-map-example/README.md +47 -0
  18. package/skills/create-verification-skill/references/feature-map-example/create-note.md +39 -0
  19. package/skills/create-verification-skill/references/feature-map-example/search.md +45 -0
  20. package/skills/figure-it-out/SKILL.md +54 -0
  21. package/skills/how/SKILL.md +134 -0
  22. package/skills/how/references/critic-prompt.md +59 -0
  23. package/skills/how/references/critique-rubric.md +58 -0
  24. package/skills/how/references/explainer-prompt.md +55 -0
  25. package/skills/how/references/explorer-prompt.md +52 -0
  26. package/skills/interrogate/SKILL.md +109 -0
  27. package/skills/interrogate/references/code-quality-review.md +47 -0
  28. package/skills/interrogate/references/lead-judgment.md +58 -0
  29. package/skills/interrogate/references/reviewer-prompt.md +72 -0
  30. package/skills/interrogate/references/rubric.md +77 -0
  31. package/skills/maintain-verification-skill/SKILL.md +38 -0
  32. package/skills/no-comments/SKILL.md +23 -0
  33. package/skills/poteto-mode/SKILL.md +140 -0
  34. package/skills/poteto-mode/playbooks/authoring-a-skill.md +12 -0
  35. package/skills/poteto-mode/playbooks/autonomous-run.md +13 -0
  36. package/skills/poteto-mode/playbooks/autopilot-full.md +13 -0
  37. package/skills/poteto-mode/playbooks/autopilot-stack.md +31 -0
  38. package/skills/poteto-mode/playbooks/babysit.md +27 -0
  39. package/skills/poteto-mode/playbooks/bug-fix.md +17 -0
  40. package/skills/poteto-mode/playbooks/eval.md +27 -0
  41. package/skills/poteto-mode/playbooks/feature.md +21 -0
  42. package/skills/poteto-mode/playbooks/hillclimb.md +21 -0
  43. package/skills/poteto-mode/playbooks/investigation.md +14 -0
  44. package/skills/poteto-mode/playbooks/multi-phase-plan.md +3 -0
  45. package/skills/poteto-mode/playbooks/opening-a-pr.md +35 -0
  46. package/skills/poteto-mode/playbooks/orchestrate.md +113 -0
  47. package/skills/poteto-mode/playbooks/pause-safely.md +10 -0
  48. package/skills/poteto-mode/playbooks/perf-issue.md +24 -0
  49. package/skills/poteto-mode/playbooks/prototype.md +14 -0
  50. package/skills/poteto-mode/playbooks/refactoring.md +16 -0
  51. package/skills/poteto-mode/playbooks/runtime-forensics.md +11 -0
  52. package/skills/poteto-mode/playbooks/session-pickup.md +13 -0
  53. package/skills/poteto-mode/playbooks/shipping.md +40 -0
  54. package/skills/poteto-mode/playbooks/trace-forensics.md +14 -0
  55. package/skills/poteto-mode/playbooks/visual-parity.md +11 -0
  56. package/skills/poteto-mode/playbooks/worktree-cleanup.md +14 -0
  57. package/skills/poteto-mode/references/bugbot-triage.md +142 -0
  58. package/skills/poteto-mode/references/plan.md +105 -0
  59. package/skills/poteto-mode/scripts/bootstrap.ts +62 -0
  60. package/skills/poteto-mode/scripts/bun.lock +67 -0
  61. package/skills/poteto-mode/scripts/orch/orch.test.ts +634 -0
  62. package/skills/poteto-mode/scripts/orch/orch.ts +578 -0
  63. package/skills/poteto-mode/scripts/orch/store.ts +1607 -0
  64. package/skills/poteto-mode/scripts/package.json +16 -0
  65. package/skills/poteto-mode/scripts/watch-pr/cli.test.ts +224 -0
  66. package/skills/poteto-mode/scripts/watch-pr/cli.ts +223 -0
  67. package/skills/poteto-mode/scripts/watch-pr/fakes.test-helper.ts +118 -0
  68. package/skills/poteto-mode/scripts/watch-pr/github.test.ts +306 -0
  69. package/skills/poteto-mode/scripts/watch-pr/github.ts +699 -0
  70. package/skills/poteto-mode/scripts/watch-pr/policy.test.ts +420 -0
  71. package/skills/poteto-mode/scripts/watch-pr/policy.ts +832 -0
  72. package/skills/poteto-mode/scripts/watch-pr/render.ts +169 -0
  73. package/skills/poteto-mode/scripts/watch-pr/tsconfig.json +13 -0
  74. package/skills/poteto-mode/scripts/watch-pr/types.compile.ts +93 -0
  75. package/skills/poteto-mode/scripts/watch-pr/types.ts +401 -0
  76. package/skills/poteto-mode/scripts/watch-pr/watch-pr +6 -0
  77. package/skills/poteto-mode/scripts/worktree-audit.sh +85 -0
  78. package/skills/principle-boundary-discipline/SKILL.md +33 -0
  79. package/skills/principle-build-the-lever/SKILL.md +22 -0
  80. package/skills/principle-encode-lessons-in-structure/SKILL.md +30 -0
  81. package/skills/principle-exhaust-the-design-space/SKILL.md +20 -0
  82. package/skills/principle-experience-first/SKILL.md +18 -0
  83. package/skills/principle-fix-root-causes/SKILL.md +22 -0
  84. package/skills/principle-foundational-thinking/SKILL.md +20 -0
  85. package/skills/principle-guard-the-context-window/SKILL.md +16 -0
  86. package/skills/principle-laziness-protocol/SKILL.md +17 -0
  87. package/skills/principle-make-operations-idempotent/SKILL.md +23 -0
  88. package/skills/principle-migrate-callers-then-delete-legacy-apis/SKILL.md +21 -0
  89. package/skills/principle-minimize-reader-load/SKILL.md +22 -0
  90. package/skills/principle-model-the-domain/SKILL.md +25 -0
  91. package/skills/principle-never-block-on-the-human/SKILL.md +22 -0
  92. package/skills/principle-outcome-oriented-execution/SKILL.md +21 -0
  93. package/skills/principle-prove-it-works/SKILL.md +32 -0
  94. package/skills/principle-redesign-from-first-principles/SKILL.md +15 -0
  95. package/skills/principle-separate-before-serializing-shared-state/SKILL.md +15 -0
  96. package/skills/principle-sequence-verifiable-units/SKILL.md +21 -0
  97. package/skills/principle-subtract-before-you-add/SKILL.md +21 -0
  98. package/skills/principle-type-system-discipline/SKILL.md +30 -0
  99. package/skills/recall/SKILL.md +40 -0
  100. package/skills/reflect/SKILL.md +77 -0
  101. package/skills/reflect/references/divergent-reviewer.md +43 -0
  102. package/skills/reflect/references/judgment-reviewer.md +42 -0
  103. package/skills/reflect/references/synthesizer.md +56 -0
  104. package/skills/reflect/references/tooling-reviewer.md +57 -0
  105. package/skills/setup-pstack/SKILL.md +28 -0
  106. package/skills/show-me-your-work/SKILL.md +89 -0
  107. package/skills/show-me-your-work/references/decision-log-template.tsv +1 -0
  108. package/skills/show-me-your-work/scripts/log.sh +40 -0
  109. package/skills/swarm/SKILL.md +45 -0
  110. package/skills/tdd/SKILL.md +43 -0
  111. package/skills/teach/SKILL.md +20 -0
  112. package/skills/technical-writing/SKILL.md +129 -0
  113. package/skills/typescript-best-practices/SKILL.md +28 -0
  114. package/skills/typescript-best-practices/references/patterns.md +292 -0
  115. package/skills/unslop/SKILL.md +80 -0
  116. package/skills/why/SKILL.md +229 -0
  117. package/skills/why/references/epistemics.md +144 -0
  118. package/skills/why/references/investigator-prompt.md +103 -0
  119. package/skills/why/references/source-playbook.md +17 -0
  120. package/skills/why/references/sources/code-archaeology.md +88 -0
  121. package/skills/why/references/sources/databricks.md +70 -0
  122. package/skills/why/references/sources/datadog.md +99 -0
  123. package/skills/why/references/sources/incident-postmortem.md +15 -0
  124. package/skills/why/references/sources/linear.md +48 -0
  125. package/skills/why/references/sources/notion.md +55 -0
  126. package/skills/why/references/sources/sentry.md +100 -0
  127. package/skills/why/references/sources/slack.md +54 -0
  128. package/skills/why/references/synthesizer-prompt.md +135 -0
@@ -0,0 +1,16 @@
1
+ {
2
+ "name": "@zenspc/pi-pstack-tools",
3
+ "private": true,
4
+ "type": "module",
5
+ "scripts": {
6
+ "test": "bun test orch watch-pr",
7
+ "typecheck": "tsc --project watch-pr/tsconfig.json --noEmit --strict"
8
+ },
9
+ "dependencies": {
10
+ "commander": "14.0.0"
11
+ },
12
+ "devDependencies": {
13
+ "bun-types": "latest",
14
+ "typescript": "latest"
15
+ }
16
+ }
@@ -0,0 +1,224 @@
1
+ import { describe, expect, it } from "bun:test";
2
+ import { type CliRuntime, main, parseArgs } from "./cli.ts";
3
+ import { fakeReader, passingCheck } from "./fakes.test-helper.ts";
4
+ import { renderJson, renderPretty } from "./render.ts";
5
+ import type { GitHubReader, WatcherVerdict } from "./types.ts";
6
+ import { parsePrNumber } from "./types.ts";
7
+
8
+ const silentIo = { stdout: () => {}, stderr: () => {} };
9
+
10
+ function testRuntime(reader: GitHubReader): {
11
+ readonly runtime: CliRuntime;
12
+ readonly stdout: string[];
13
+ readonly stderr: string[];
14
+ } {
15
+ const stdout: string[] = [];
16
+ const stderr: string[] = [];
17
+ return {
18
+ stdout,
19
+ stderr,
20
+ runtime: {
21
+ reader,
22
+ clock: {
23
+ now: () => 0,
24
+ observedAt: () => "2026-07-26T00:00:00.000Z",
25
+ async sleep() {
26
+ throw new Error("test unexpectedly slept");
27
+ },
28
+ },
29
+ stdout: (value) => stdout.push(value),
30
+ stderr: (value) => stderr.push(value),
31
+ },
32
+ };
33
+ }
34
+
35
+ describe("parseArgs", () => {
36
+ it("uses the specified defaults", () => {
37
+ expect(parseArgs([], silentIo)).toMatchObject({
38
+ owner: null,
39
+ repo: null,
40
+ pr: null,
41
+ mode: "single",
42
+ stackPrs: [],
43
+ statusOnly: false,
44
+ pretty: false,
45
+ polling: {
46
+ interval: 60,
47
+ sweepInterval: 300,
48
+ timeout: 0,
49
+ maxQueryErrors: 5,
50
+ allowDraft: false,
51
+ },
52
+ });
53
+ });
54
+
55
+ it("parses a frozen queued stack bottom-to-top", () => {
56
+ const parsed = parseArgs(
57
+ [
58
+ "--queued-stack",
59
+ "--stack-prs",
60
+ "#10, 11,#12",
61
+ "--interval",
62
+ "2.5",
63
+ "--sweep-interval",
64
+ "30",
65
+ "--timeout",
66
+ "0",
67
+ "--max-query-errors",
68
+ "3",
69
+ "--allow-draft",
70
+ "--pretty",
71
+ ],
72
+ silentIo
73
+ );
74
+ expect(parsed.mode).toBe("queued-stack");
75
+ expect(parsed.stackPrs.map(Number)).toEqual([10, 11, 12]);
76
+ expect(parsed.polling).toEqual({
77
+ interval: 2.5,
78
+ sweepInterval: 30,
79
+ timeout: 0,
80
+ maxQueryErrors: 3,
81
+ allowDraft: true,
82
+ });
83
+ expect(parsed.pretty).toBe(true);
84
+ });
85
+
86
+ it("rejects every invalid mode and numeric shape as usage", async () => {
87
+ const invalid = [
88
+ ["--unknown"],
89
+ ["--interval", "0"],
90
+ ["--sweep-interval", "-1"],
91
+ ["--timeout", "-1"],
92
+ ["--max-query-errors", "1.5"],
93
+ ["--stack", "--queued-stack"],
94
+ ["--stack-prs", "1,2"],
95
+ ["--queued-stack", "--stack-prs", "1,1"],
96
+ ];
97
+ for (const argv of invalid) {
98
+ const harness = testRuntime(fakeReader());
99
+ expect(await main(argv, harness.runtime)).toBe(64);
100
+ expect(harness.stdout).toEqual([]);
101
+ expect(harness.stderr.join("")).toContain("error:");
102
+ }
103
+ });
104
+ });
105
+
106
+ describe("rendering", () => {
107
+ const context = {
108
+ owner: "owner",
109
+ repo: "repo",
110
+ number: parsePrNumber(1),
111
+ };
112
+ const status = {
113
+ schemaVersion: 1,
114
+ sequence: 1,
115
+ observedAt: "2026-07-26T00:00:00.000Z",
116
+ mode: "single",
117
+ kind: "STATUS",
118
+ terminal: true,
119
+ exitCode: 0,
120
+ reason: "status-only",
121
+ rows: [
122
+ {
123
+ kind: "merged",
124
+ context,
125
+ facts: {
126
+ context,
127
+ mergeable: "MERGEABLE",
128
+ mergeStateStatus: "CLEAN",
129
+ reviewDecision: "APPROVED",
130
+ headRefOid: "head",
131
+ headRefName: "feature",
132
+ baseRefName: "main",
133
+ state: "MERGED",
134
+ mergedAt: "now",
135
+ isDraft: false,
136
+ },
137
+ },
138
+ ],
139
+ } satisfies WatcherVerdict;
140
+
141
+ it("emits compact valid JSON by default", () => {
142
+ const rendered = renderJson(status);
143
+ expect(rendered.endsWith("\n")).toBe(true);
144
+ expect(JSON.parse(rendered)).toEqual(status);
145
+ });
146
+
147
+ it("renders the Markdown table from the same verdict only", () => {
148
+ const rendered = renderPretty(status);
149
+ expect(rendered).toContain("| PR | CI | Review | Merge |");
150
+ expect(rendered).toContain(
151
+ "| [#1](https://github.com/owner/repo/pull/1) | \u2014 | \u2014 | ✅ merged |"
152
+ );
153
+ });
154
+ });
155
+
156
+ describe("main", () => {
157
+ it("returns EX_USAGE 64 and writes usage errors only to stderr", async () => {
158
+ const harness = testRuntime(fakeReader());
159
+ expect(await main(["--interval", "0"], harness.runtime)).toBe(64);
160
+ expect(harness.stdout).toEqual([]);
161
+ expect(harness.stderr.join("")).toContain(
162
+ "option '--interval <seconds>' argument '0' is invalid"
163
+ );
164
+ });
165
+
166
+ it("bypasses the queue machine for queued-stack status-only", async () => {
167
+ const reader = fakeReader();
168
+ const harness = testRuntime(reader);
169
+ const code = await main(
170
+ [
171
+ "--owner",
172
+ "owner",
173
+ "--repo",
174
+ "repo",
175
+ "--queued-stack",
176
+ "--stack-prs",
177
+ "1",
178
+ "--status-only",
179
+ ],
180
+ harness.runtime
181
+ );
182
+ expect(code).toBe(0);
183
+ expect(harness.stdout).toHaveLength(1);
184
+ const verdict: unknown = JSON.parse(harness.stdout[0]);
185
+ expect(verdict).toMatchObject({
186
+ kind: "STATUS",
187
+ terminal: true,
188
+ exitCode: 0,
189
+ mode: "queued-stack",
190
+ });
191
+ expect(harness.stdout[0]).not.toContain('"kind":"QUEUE"');
192
+ });
193
+
194
+ it("returns exit 4 for a hidden GitHub-side CI refusal", async () => {
195
+ const reader = fakeReader({
196
+ facts: { mergeStateStatus: "BLOCKED" },
197
+ fastPath: { kind: "checks", checks: [passingCheck()] },
198
+ commitRollups: [{ oid: "head", state: "FAILURE" }],
199
+ });
200
+ const harness = testRuntime(reader);
201
+ const code = await main(
202
+ ["--owner", "owner", "--repo", "repo", "--pr", "1"],
203
+ harness.runtime
204
+ );
205
+ expect(code).toBe(4);
206
+ expect(harness.stdout).toHaveLength(1);
207
+ expect(JSON.parse(harness.stdout[0])).toMatchObject({
208
+ kind: "BLOCKER",
209
+ exitCode: 4,
210
+ blocker: {
211
+ kind: "failing-checks",
212
+ ci: { kind: "ci-github-rejected" },
213
+ },
214
+ });
215
+ });
216
+
217
+ it("shows help without touching the reader", async () => {
218
+ const reader = fakeReader();
219
+ const harness = testRuntime(reader);
220
+ expect(await main(["--help"], harness.runtime)).toBe(0);
221
+ expect(harness.stdout.join("")).toContain("JSON (NDJSON while polling)");
222
+ expect(reader.calls).toEqual([]);
223
+ });
224
+ });
@@ -0,0 +1,223 @@
1
+ import { setTimeout as delay } from "node:timers/promises";
2
+ import {
3
+ Command,
4
+ CommanderError,
5
+ InvalidArgumentError,
6
+ Option,
7
+ } from "commander";
8
+ import {
9
+ GhGitHubReader,
10
+ WatcherQueryError,
11
+ discoverStack,
12
+ resolveContext,
13
+ } from "./github.ts";
14
+ import {
15
+ runQueued,
16
+ runSimple,
17
+ statusQueryVerdict,
18
+ verdictFactory,
19
+ type WatchClock,
20
+ } from "./policy.ts";
21
+ import { renderJson, renderPretty } from "./render.ts";
22
+ import type * as T from "./types.ts";
23
+ import { nonEmpty, parsePrNumber } from "./types.ts";
24
+ export interface CliOptions {
25
+ readonly owner: string | null;
26
+ readonly repo: string | null;
27
+ readonly pr: T.PrNumber | null;
28
+ readonly mode: T.WatchMode;
29
+ readonly stackPrs: readonly T.PrNumber[];
30
+ readonly statusOnly: boolean;
31
+ readonly pretty: boolean;
32
+ readonly polling: T.PollingOptions;
33
+ }
34
+ function positiveNumber(value: string): number {
35
+ const parsed = Number(value);
36
+ if (!Number.isFinite(parsed) || parsed <= 0)
37
+ throw new InvalidArgumentError("must be greater than zero");
38
+ return parsed;
39
+ }
40
+ function nonNegativeNumber(value: string): number {
41
+ const parsed = Number(value);
42
+ if (!Number.isFinite(parsed) || parsed < 0)
43
+ throw new InvalidArgumentError("must be zero or greater");
44
+ return parsed;
45
+ }
46
+ function positiveInteger(value: string): number {
47
+ const parsed = Number(value);
48
+ if (!Number.isInteger(parsed) || parsed <= 0)
49
+ throw new InvalidArgumentError("must be a positive integer");
50
+ return parsed;
51
+ }
52
+ function prNumber(value: string): T.PrNumber {
53
+ try {
54
+ return parsePrNumber(Number(value.replace(/^#/, "")));
55
+ } catch {
56
+ throw new InvalidArgumentError("must be a positive integer");
57
+ }
58
+ }
59
+ function stackPrList(value: string): T.NonEmpty<T.PrNumber> {
60
+ const numbers = value.split(",").map((part) => prNumber(part.trim()));
61
+ if (new Set(numbers).size !== numbers.length)
62
+ throw new InvalidArgumentError("contains a duplicate PR");
63
+ const parsed = nonEmpty(numbers);
64
+ if (parsed === null) throw new InvalidArgumentError("cannot be empty");
65
+ return parsed;
66
+ }
67
+ interface RawOptions {
68
+ readonly owner?: string;
69
+ readonly repo?: string;
70
+ readonly pr?: T.PrNumber;
71
+ readonly stack: boolean;
72
+ readonly queuedStack: boolean;
73
+ readonly stackPrs?: T.NonEmpty<T.PrNumber>;
74
+ readonly interval: number;
75
+ readonly sweepInterval: number;
76
+ readonly timeout: number;
77
+ readonly maxQueryErrors: number;
78
+ readonly statusOnly: boolean;
79
+ readonly allowDraft: boolean;
80
+ readonly pretty: boolean;
81
+ }
82
+ export function parseArgs(
83
+ argv: readonly string[],
84
+ io: Pick<CliRuntime, "stdout" | "stderr">
85
+ ): CliOptions {
86
+ const program = new Command("watch-pr")
87
+ .description(
88
+ "Watch one pull request, a connected stack, or an immutable queued stack.\nJSON (NDJSON while polling) is the default; --pretty renders human text."
89
+ )
90
+ .configureOutput({ writeOut: io.stdout, writeErr: io.stderr })
91
+ .exitOverride()
92
+ .option("--owner <owner>", "GitHub repository owner")
93
+ .option("--repo <repo>", "GitHub repository name")
94
+ .option("--pr <number>", "pull request number", prNumber)
95
+ .addOption(
96
+ new Option("--stack", "watch the connected open stack")
97
+ .default(false)
98
+ .conflicts("queuedStack")
99
+ )
100
+ .option(
101
+ "--queued-stack",
102
+ "watch the captured stack until all PRs merge",
103
+ false
104
+ )
105
+ .option(
106
+ "--stack-prs <n,...>",
107
+ "frozen bottom-to-top queue (queued mode only)",
108
+ stackPrList
109
+ )
110
+ .option("--interval <seconds>", "poll interval", positiveNumber, 60)
111
+ .option(
112
+ "--sweep-interval <seconds>",
113
+ "whole-stack sweep interval",
114
+ positiveNumber,
115
+ 300
116
+ )
117
+ .option(
118
+ "--timeout <seconds>",
119
+ "deadline; 0 disables it",
120
+ nonNegativeNumber,
121
+ 0
122
+ )
123
+ .option(
124
+ "--max-query-errors <count>",
125
+ "consecutive query-error budget",
126
+ positiveInteger,
127
+ 5
128
+ )
129
+ .option("--status-only", "print one status table and exit 0", false)
130
+ .option("--allow-draft", "do not treat a draft as a merge gate", false)
131
+ .option("--pretty", "render human text instead of JSON", false);
132
+ program.parse(argv, { from: "user" });
133
+ const raw = program.opts<RawOptions>();
134
+ if (raw.stackPrs !== undefined && !raw.queuedStack)
135
+ program.error("error: --stack-prs requires --queued-stack");
136
+ return {
137
+ owner: raw.owner ?? null,
138
+ repo: raw.repo ?? null,
139
+ pr: raw.pr ?? null,
140
+ mode: raw.queuedStack ? "queued-stack" : raw.stack ? "stack" : "single",
141
+ stackPrs: raw.stackPrs ?? [],
142
+ statusOnly: raw.statusOnly,
143
+ pretty: raw.pretty,
144
+ polling: {
145
+ interval: raw.interval,
146
+ sweepInterval: raw.sweepInterval,
147
+ timeout: raw.timeout,
148
+ maxQueryErrors: raw.maxQueryErrors,
149
+ allowDraft: raw.allowDraft,
150
+ },
151
+ };
152
+ }
153
+ export interface CliRuntime {
154
+ readonly reader: T.GitHubReader;
155
+ readonly clock: WatchClock;
156
+ readonly stdout: (value: string) => void;
157
+ readonly stderr: (value: string) => void;
158
+ }
159
+ function realRuntime(): CliRuntime {
160
+ return {
161
+ reader: new GhGitHubReader(),
162
+ clock: {
163
+ now: () => performance.now() / 1_000,
164
+ observedAt: () => new Date().toISOString(),
165
+ sleep: async (seconds) => {
166
+ await delay(seconds * 1_000);
167
+ },
168
+ },
169
+ stdout: (value) => process.stdout.write(value),
170
+ stderr: (value) => process.stderr.write(value),
171
+ };
172
+ }
173
+ export async function main(
174
+ argv: readonly string[],
175
+ runtime: CliRuntime = realRuntime()
176
+ ): Promise<number> {
177
+ let options: CliOptions;
178
+ try {
179
+ options = parseArgs(argv, runtime);
180
+ } catch (error) {
181
+ if (!(error instanceof CommanderError)) throw error;
182
+ return error.exitCode === 0 ? 0 : 64;
183
+ }
184
+ const render = options.pretty ? renderPretty : renderJson;
185
+ const emit = (verdict: T.ProgressVerdict): void =>
186
+ runtime.stdout(render(verdict));
187
+ let contexts: T.NonEmpty<T.PrContext>;
188
+ try {
189
+ const seed = await resolveContext({
190
+ reader: runtime.reader,
191
+ owner: options.owner,
192
+ repo: options.repo,
193
+ pr: options.pr ?? options.stackPrs[0] ?? null,
194
+ });
195
+ contexts =
196
+ nonEmpty(options.stackPrs.map((number) => ({ ...seed, number }))) ??
197
+ (options.mode === "single"
198
+ ? [seed]
199
+ : await discoverStack(runtime.reader, seed));
200
+ } catch (error) {
201
+ if (!(error instanceof WatcherQueryError)) throw error;
202
+ const verdict = statusQueryVerdict(
203
+ verdictFactory(runtime.clock, options.mode),
204
+ 1,
205
+ error.failure
206
+ );
207
+ runtime.stdout(render(verdict));
208
+ return verdict.exitCode;
209
+ }
210
+ const dependencies = { reader: runtime.reader, clock: runtime.clock, emit };
211
+ const verdict =
212
+ options.mode === "queued-stack" && !options.statusOnly
213
+ ? await runQueued({ dependencies, contexts, options: options.polling })
214
+ : await runSimple({
215
+ dependencies,
216
+ contexts,
217
+ mode: options.mode,
218
+ statusOnly: options.statusOnly,
219
+ options: options.polling,
220
+ });
221
+ runtime.stdout(render(verdict));
222
+ return verdict.exitCode;
223
+ }
@@ -0,0 +1,118 @@
1
+ import type {
2
+ Check,
3
+ ChecksFastPath,
4
+ CommitRollup,
5
+ GitHubReader,
6
+ OpenPullRequest,
7
+ PrContext,
8
+ PullRequestFacts,
9
+ Repository,
10
+ ReviewThread,
11
+ RollupPage,
12
+ } from "./types.ts";
13
+ import { parsePrNumber } from "./types.ts";
14
+
15
+ export interface FakeReaderOptions {
16
+ readonly facts?: Partial<Omit<PullRequestFacts, "context">>;
17
+ readonly fastPath?: ChecksFastPath;
18
+ readonly rollupPages?: readonly RollupPage[];
19
+ readonly threads?: readonly ReviewThread[];
20
+ readonly commitRollups?: readonly CommitRollup[];
21
+ readonly openPullRequests?: readonly OpenPullRequest[];
22
+ readonly origin?: Repository | null;
23
+ readonly current?: PrContext;
24
+ }
25
+
26
+ export function passingCheck(name = "ci"): Check {
27
+ return {
28
+ kind: "passed",
29
+ name,
30
+ reportedState: "SUCCESS",
31
+ description: "",
32
+ link: "",
33
+ workflow: "",
34
+ };
35
+ }
36
+
37
+ export function pendingCheck(name = "ci"): Check {
38
+ return {
39
+ kind: "pending",
40
+ name,
41
+ reportedState: "PENDING",
42
+ description: "",
43
+ link: "",
44
+ workflow: "",
45
+ };
46
+ }
47
+
48
+ export function failedCheck(name = "ci"): Check {
49
+ return {
50
+ kind: "failed",
51
+ name,
52
+ reportedState: "FAILURE",
53
+ description: "",
54
+ link: "",
55
+ workflow: "",
56
+ };
57
+ }
58
+
59
+ export function fakeReader(
60
+ options: FakeReaderOptions = {}
61
+ ): GitHubReader & { readonly calls: readonly string[] } {
62
+ const calls: string[] = [];
63
+ const context = options.current ?? {
64
+ owner: "owner",
65
+ repo: "repo",
66
+ number: parsePrNumber(1),
67
+ };
68
+ const defaults: PullRequestFacts = {
69
+ context,
70
+ mergeable: "MERGEABLE",
71
+ mergeStateStatus: "CLEAN",
72
+ reviewDecision: "APPROVED",
73
+ headRefOid: "head",
74
+ headRefName: "feature",
75
+ baseRefName: "main",
76
+ state: "OPEN",
77
+ mergedAt: null,
78
+ isDraft: false,
79
+ };
80
+ let page = 0;
81
+ return {
82
+ calls,
83
+ async originRepo() {
84
+ calls.push("originRepo");
85
+ return options.origin === undefined
86
+ ? { owner: "owner", repo: "repo" }
87
+ : options.origin;
88
+ },
89
+ async currentPr(pr) {
90
+ calls.push("currentPr");
91
+ return { ...context, number: pr ?? context.number };
92
+ },
93
+ async pullRequest(requested) {
94
+ calls.push("pullRequest");
95
+ return { ...defaults, ...options.facts, context: requested };
96
+ },
97
+ async openPullRequests() {
98
+ calls.push("openPullRequests");
99
+ return options.openPullRequests ?? [];
100
+ },
101
+ async checksFastPath() {
102
+ calls.push("checksFastPath");
103
+ return options.fastPath ?? { kind: "checks", checks: [passingCheck()] };
104
+ },
105
+ async checkRollupPage(_requested, after) {
106
+ calls.push(`checkRollupPage:${after ?? "null"}`);
107
+ return options.rollupPages?.[page++] ?? { checks: [], endCursor: null };
108
+ },
109
+ async reviewThreads() {
110
+ calls.push("reviewThreads");
111
+ return options.threads ?? [];
112
+ },
113
+ async commitRollups() {
114
+ calls.push("commitRollups");
115
+ return options.commitRollups ?? [{ oid: "head", state: "SUCCESS" }];
116
+ },
117
+ };
118
+ }