@webpieces/pr-gate 0.4.744 → 0.4.746

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@webpieces/pr-gate",
3
- "version": "0.4.744",
3
+ "version": "0.4.746",
4
4
  "description": "Gated PR system: 3-point squash-merge, merge validation gate, and red/yellow/green PR dashboard. Standalone scripts, no Nx dependency required.",
5
5
  "type": "commonjs",
6
6
  "main": "./src/index.js",
@@ -16,8 +16,8 @@
16
16
  },
17
17
  "dependencies": {
18
18
  "@inversifyjs/binding-decorators": "1.1.5",
19
- "@webpieces/ai-hook-rules": "0.4.744",
20
- "@webpieces/rules-config": "0.4.744",
19
+ "@webpieces/ai-hook-rules": "0.4.746",
20
+ "@webpieces/rules-config": "0.4.746",
21
21
  "inversify": "7.10.4",
22
22
  "reflect-metadata": "0.2.2"
23
23
  },
@@ -37,6 +37,8 @@
37
37
  "wp-check-pr": "./src/scripts/wp-check-pr.js",
38
38
  "wp-review-upsert-pr": "./src/scripts/wp-review-upsert-pr.js",
39
39
  "wp-push-dev": "./src/scripts/wp-push-dev.js",
40
- "wp-finish-push-dev": "./src/scripts/wp-finish-push-dev.js"
40
+ "wp-finish-push-dev": "./src/scripts/wp-finish-push-dev.js",
41
+ "wp-await-reviews": "./src/scripts/wp-await-reviews.js",
42
+ "wp-await-checks": "./src/scripts/wp-await-checks.js"
41
43
  }
42
44
  }
@@ -0,0 +1,88 @@
1
+ import { AwaitLoop, WaitOutcome, WaitProbe } from '../workflow/await-loop';
2
+ import { StageOutputLog } from '../workflow/stage-output-log';
3
+ /** What `wp-await-checks` was asked to wait on. Data-only (a class, per CLAUDE.md). */
4
+ export declare class AwaitChecksOptions {
5
+ /** The PR number, exactly as `--pr` gave it. REQUIRED — there is no "guess which PR" branch. */
6
+ prNumber: string;
7
+ constructor(prNumber: string);
8
+ }
9
+ /**
10
+ * `wp-await-checks --pr <n>` — BLOCK until GitHub's checks for that PR stop being in flight, then print
11
+ * where they landed.
12
+ *
13
+ * ─── OFFERED, NEVER INSTRUCTED ─────────────────────────────────────────────────────────────────────
14
+ * This exists for the see-it-land case, and for nothing else. `wp-finish-upsert-pr` still ends with
15
+ * "Nothing else is owed by the tooling — a person merges it. You can stop here.", and that stays the
16
+ * DEFAULT: several of this repo's workflows deliberately stop at a green PR, and landing is the
17
+ * developer's call, not the tooling's. Nothing here or in the finish banner tells an agent to wait —
18
+ * the banner names this command as an option and says out loud that stopping is still correct.
19
+ *
20
+ * What it replaces is not "stopping"; it is `gh pr checks --watch` in a loop, or the `echo .` keep-alive
21
+ * an agent falls back to when it has decided to see the PR land. See {@link AwaitLoop} for why a
22
+ * blocking command is the only wait a worktree-isolated subagent can express.
23
+ *
24
+ * ─── It reports, it does not judge ─────────────────────────────────────────────────────────────────
25
+ * A red check is an ANSWER, so the wait ends and the state is printed. Deciding what a failure means is
26
+ * the caller's, and turning a red check into a non-zero exit here would make "the checks finished" and
27
+ * "the checks passed" the same signal — which is exactly how a red PR gets reported as landed.
28
+ */
29
+ export declare class AwaitChecksCommand {
30
+ private readonly awaitLoop;
31
+ private readonly stageConsole;
32
+ constructor(awaitLoop: AwaitLoop, stageConsole: StageOutputLog);
33
+ run(opts: AwaitChecksOptions): Promise<void>;
34
+ }
35
+ /** One check run's rollup state, as GitHub reports it. Data-only. */
36
+ export declare class ChecksState {
37
+ /** How many checks GitHub currently lists. 0 means it has not created any yet — QUEUED, not absent. */
38
+ total: number;
39
+ pending: number;
40
+ failed: number;
41
+ /** True when `gh` could not be asked at all — the wait keeps going rather than claiming an answer. */
42
+ unreadable: boolean;
43
+ constructor(total: number, pending: number, failed: number, unreadable: boolean);
44
+ /**
45
+ * Settled means GitHub has created checks AND none of them is still running.
46
+ *
47
+ * `total === 0` is deliberately NOT settled. An empty rollup seconds after a push means the workflow
48
+ * has not been created yet — QUEUED — and treating it as "no checks, all clear" is how a PR gets
49
+ * called green before CI has started. The wait's own ceiling is what ends that case, and it ends it
50
+ * by saying "still waiting", which is the truth.
51
+ */
52
+ get settled(): boolean;
53
+ describe(): string;
54
+ }
55
+ /**
56
+ * The CI wait: one `gh pr view` per poll, classified into {@link ChecksState}.
57
+ *
58
+ * `gh pr view --json statusCheckRollup` rather than `gh pr checks`, because the rollup field is the
59
+ * stable JSON surface and returns an EMPTY array — not an error exit — for a PR whose checks have not
60
+ * been created yet. `gh pr checks` exits non-zero in several distinct "nothing to report" situations,
61
+ * which would make "queued" and "gh is broken" the same observation.
62
+ */
63
+ export declare class ChecksWaitProbe implements WaitProbe {
64
+ private readonly prNumber;
65
+ readonly label = "CI checks";
66
+ readonly pollMs = 15000;
67
+ private state;
68
+ constructor(prNumber: string);
69
+ done(): boolean;
70
+ describe(): string;
71
+ settledReport(outcome: WaitOutcome): string;
72
+ stillWaitingReport(outcome: WaitOutcome): string;
73
+ /**
74
+ * One read of the rollup. Fails to `unreadable` rather than to "settled": a `gh` that cannot answer
75
+ * must never be able to end the wait, because the only thing worse than waiting too long is
76
+ * announcing an outcome nobody observed.
77
+ */
78
+ private read;
79
+ private classify;
80
+ }
81
+ /**
82
+ * `--pr` is required, and this is where that is enforced — before the loop starts, so a caller that
83
+ * forgot it is told immediately rather than after a nine-minute wait on nothing.
84
+ */
85
+ export declare class AwaitChecksArgs {
86
+ /** The PR number from argv, or a CliExitError naming the flag it needs. */
87
+ parse(value: string): AwaitChecksOptions;
88
+ }
@@ -0,0 +1,174 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.AwaitChecksArgs = exports.ChecksWaitProbe = exports.ChecksState = exports.AwaitChecksCommand = exports.AwaitChecksOptions = void 0;
4
+ const tslib_1 = require("tslib");
5
+ const child_process_1 = require("child_process");
6
+ const rules_config_1 = require("@webpieces/rules-config");
7
+ const inversify_1 = require("inversify");
8
+ const await_loop_1 = require("../workflow/await-loop");
9
+ const stage_output_log_1 = require("../workflow/stage-output-log");
10
+ /** What `wp-await-checks` was asked to wait on. Data-only (a class, per CLAUDE.md). */
11
+ class AwaitChecksOptions {
12
+ /** The PR number, exactly as `--pr` gave it. REQUIRED — there is no "guess which PR" branch. */
13
+ prNumber;
14
+ // REQUIRED, no default. A defaulted "current branch's PR" would be a second spelling of the one
15
+ // question this command asks, reachable by typing less, and the failure mode is watching the wrong
16
+ // PR go green.
17
+ constructor(prNumber) {
18
+ this.prNumber = prNumber;
19
+ }
20
+ }
21
+ exports.AwaitChecksOptions = AwaitChecksOptions;
22
+ /**
23
+ * `wp-await-checks --pr <n>` — BLOCK until GitHub's checks for that PR stop being in flight, then print
24
+ * where they landed.
25
+ *
26
+ * ─── OFFERED, NEVER INSTRUCTED ─────────────────────────────────────────────────────────────────────
27
+ * This exists for the see-it-land case, and for nothing else. `wp-finish-upsert-pr` still ends with
28
+ * "Nothing else is owed by the tooling — a person merges it. You can stop here.", and that stays the
29
+ * DEFAULT: several of this repo's workflows deliberately stop at a green PR, and landing is the
30
+ * developer's call, not the tooling's. Nothing here or in the finish banner tells an agent to wait —
31
+ * the banner names this command as an option and says out loud that stopping is still correct.
32
+ *
33
+ * What it replaces is not "stopping"; it is `gh pr checks --watch` in a loop, or the `echo .` keep-alive
34
+ * an agent falls back to when it has decided to see the PR land. See {@link AwaitLoop} for why a
35
+ * blocking command is the only wait a worktree-isolated subagent can express.
36
+ *
37
+ * ─── It reports, it does not judge ─────────────────────────────────────────────────────────────────
38
+ * A red check is an ANSWER, so the wait ends and the state is printed. Deciding what a failure means is
39
+ * the caller's, and turning a red check into a non-zero exit here would make "the checks finished" and
40
+ * "the checks passed" the same signal — which is exactly how a red PR gets reported as landed.
41
+ */
42
+ let AwaitChecksCommand = class AwaitChecksCommand {
43
+ awaitLoop;
44
+ stageConsole;
45
+ constructor(awaitLoop, stageConsole) {
46
+ this.awaitLoop = awaitLoop;
47
+ this.stageConsole = stageConsole;
48
+ }
49
+ async run(opts) {
50
+ const probe = new ChecksWaitProbe(opts.prNumber);
51
+ const outcome = await this.awaitLoop.run(probe);
52
+ this.stageConsole.say(outcome.done ? probe.settledReport(outcome) : probe.stillWaitingReport(outcome));
53
+ }
54
+ };
55
+ exports.AwaitChecksCommand = AwaitChecksCommand;
56
+ exports.AwaitChecksCommand = AwaitChecksCommand = tslib_1.__decorate([
57
+ (0, inversify_1.injectable)(inversify_1.bindingScopeValues.Singleton),
58
+ tslib_1.__metadata("design:paramtypes", [await_loop_1.AwaitLoop,
59
+ stage_output_log_1.StageOutputLog])
60
+ ], AwaitChecksCommand);
61
+ /** One check run's rollup state, as GitHub reports it. Data-only. */
62
+ class ChecksState {
63
+ /** How many checks GitHub currently lists. 0 means it has not created any yet — QUEUED, not absent. */
64
+ total;
65
+ pending;
66
+ failed;
67
+ /** True when `gh` could not be asked at all — the wait keeps going rather than claiming an answer. */
68
+ unreadable;
69
+ // eslint-disable-next-line @typescript-eslint/max-params
70
+ constructor(total, pending, failed, unreadable) {
71
+ this.total = total;
72
+ this.pending = pending;
73
+ this.failed = failed;
74
+ this.unreadable = unreadable;
75
+ }
76
+ /**
77
+ * Settled means GitHub has created checks AND none of them is still running.
78
+ *
79
+ * `total === 0` is deliberately NOT settled. An empty rollup seconds after a push means the workflow
80
+ * has not been created yet — QUEUED — and treating it as "no checks, all clear" is how a PR gets
81
+ * called green before CI has started. The wait's own ceiling is what ends that case, and it ends it
82
+ * by saying "still waiting", which is the truth.
83
+ */
84
+ get settled() {
85
+ return !this.unreadable && this.total > 0 && this.pending === 0;
86
+ }
87
+ describe() {
88
+ if (this.unreadable)
89
+ return 'gh could not be read';
90
+ if (this.total === 0)
91
+ return 'no checks reported yet (queued)';
92
+ return `${String(this.total - this.pending)} of ${String(this.total)} done, `
93
+ + `${String(this.failed)} failed`;
94
+ }
95
+ }
96
+ exports.ChecksState = ChecksState;
97
+ /**
98
+ * The CI wait: one `gh pr view` per poll, classified into {@link ChecksState}.
99
+ *
100
+ * `gh pr view --json statusCheckRollup` rather than `gh pr checks`, because the rollup field is the
101
+ * stable JSON surface and returns an EMPTY array — not an error exit — for a PR whose checks have not
102
+ * been created yet. `gh pr checks` exits non-zero in several distinct "nothing to report" situations,
103
+ * which would make "queued" and "gh is broken" the same observation.
104
+ */
105
+ class ChecksWaitProbe {
106
+ prNumber;
107
+ label = 'CI checks';
108
+ // A network round trip per poll, so this is deliberately slow. CI on this repo runs ~1 minute; a
109
+ // 15-second poll finds it within a heartbeat of finishing and costs ~36 API calls per full wait.
110
+ pollMs = 15_000;
111
+ state = new ChecksState(0, 0, 0, true);
112
+ constructor(prNumber) {
113
+ this.prNumber = prNumber;
114
+ }
115
+ done() {
116
+ this.state = this.read();
117
+ return this.state.settled;
118
+ }
119
+ describe() {
120
+ return this.state.describe();
121
+ }
122
+ settledReport(outcome) {
123
+ const verdict = this.state.failed === 0
124
+ ? `🟢 All ${String(this.state.total)} check(s) passed`
125
+ : `🔴 ${String(this.state.failed)} of ${String(this.state.total)} check(s) FAILED`;
126
+ return `\n${verdict} after ${String(outcome.waitedSeconds)}s.\n`
127
+ + ` Read the detail with: gh pr checks ${this.prNumber}\n`;
128
+ }
129
+ stillWaitingReport(outcome) {
130
+ return `\n⏳ Checks are still in flight after ${String(outcome.waitedSeconds)}s (${this.state.describe()})`
131
+ + ` — run me again: pnpm wp-await-checks --pr ${this.prNumber}\n`
132
+ + ' (This is not a failure. The wait returns before the harness kills a quiet command.)\n';
133
+ }
134
+ /**
135
+ * One read of the rollup. Fails to `unreadable` rather than to "settled": a `gh` that cannot answer
136
+ * must never be able to end the wait, because the only thing worse than waiting too long is
137
+ * announcing an outcome nobody observed.
138
+ */
139
+ read() {
140
+ const result = (0, child_process_1.spawnSync)('gh', ['pr', 'view', this.prNumber, '--json', 'statusCheckRollup',
141
+ '--jq', '[.statusCheckRollup[] | (.status // .state)] | @tsv'], { encoding: 'utf8' });
142
+ if (result.status !== 0)
143
+ return new ChecksState(0, 0, 0, true);
144
+ return this.classify((result.stdout ?? '').trim());
145
+ }
146
+ // `status` is COMPLETED / IN_PROGRESS / QUEUED for a check run; a plain commit status has no
147
+ // `status` and its `state` is SUCCESS / PENDING / FAILURE / ERROR. The jq above collapses the two
148
+ // into one token per check, and everything that is not finished counts as pending.
149
+ classify(tsv) {
150
+ const tokens = tsv === '' ? [] : tsv.split(/\s+/);
151
+ const pending = tokens.filter((t) => PENDING_TOKENS.has(t)).length;
152
+ const failed = tokens.filter((t) => FAILED_TOKENS.has(t)).length;
153
+ return new ChecksState(tokens.length, pending, failed, false);
154
+ }
155
+ }
156
+ exports.ChecksWaitProbe = ChecksWaitProbe;
157
+ const PENDING_TOKENS = new Set(['QUEUED', 'IN_PROGRESS', 'PENDING', 'WAITING', 'REQUESTED']);
158
+ const FAILED_TOKENS = new Set(['FAILURE', 'ERROR', 'TIMED_OUT', 'CANCELLED', 'ACTION_REQUIRED', 'STARTUP_FAILURE']);
159
+ /**
160
+ * `--pr` is required, and this is where that is enforced — before the loop starts, so a caller that
161
+ * forgot it is told immediately rather than after a nine-minute wait on nothing.
162
+ */
163
+ class AwaitChecksArgs {
164
+ /** The PR number from argv, or a CliExitError naming the flag it needs. */
165
+ parse(value) {
166
+ if (!/^\d+$/.test(value.trim())) {
167
+ throw new rules_config_1.CliExitError(2, '❌ wp-await-checks needs the PR number to wait on: pnpm wp-await-checks --pr <n>\n'
168
+ + ' `gh pr view --json number` prints it for the current branch.');
169
+ }
170
+ return new AwaitChecksOptions(value.trim());
171
+ }
172
+ }
173
+ exports.AwaitChecksArgs = AwaitChecksArgs;
174
+ //# sourceMappingURL=await-checks-command.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"await-checks-command.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/pr-gate/src/scripts/commands/await-checks-command.ts"],"names":[],"mappings":"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","sourcesContent":["import { spawnSync } from 'child_process';\nimport { CliExitError } from '@webpieces/rules-config';\nimport { injectable, bindingScopeValues } from 'inversify';\n\nimport { AwaitLoop, WaitOutcome, WaitProbe } from '../workflow/await-loop';\nimport { StageOutputLog } from '../workflow/stage-output-log';\n\n/** What `wp-await-checks` was asked to wait on. Data-only (a class, per CLAUDE.md). */\nexport class AwaitChecksOptions {\n /** The PR number, exactly as `--pr` gave it. REQUIRED — there is no \"guess which PR\" branch. */\n prNumber: string;\n\n // REQUIRED, no default. A defaulted \"current branch's PR\" would be a second spelling of the one\n // question this command asks, reachable by typing less, and the failure mode is watching the wrong\n // PR go green.\n constructor(prNumber: string) {\n this.prNumber = prNumber;\n }\n}\n\n/**\n * `wp-await-checks --pr <n>` — BLOCK until GitHub's checks for that PR stop being in flight, then print\n * where they landed.\n *\n * ─── OFFERED, NEVER INSTRUCTED ─────────────────────────────────────────────────────────────────────\n * This exists for the see-it-land case, and for nothing else. `wp-finish-upsert-pr` still ends with\n * \"Nothing else is owed by the tooling — a person merges it. You can stop here.\", and that stays the\n * DEFAULT: several of this repo's workflows deliberately stop at a green PR, and landing is the\n * developer's call, not the tooling's. Nothing here or in the finish banner tells an agent to wait —\n * the banner names this command as an option and says out loud that stopping is still correct.\n *\n * What it replaces is not \"stopping\"; it is `gh pr checks --watch` in a loop, or the `echo .` keep-alive\n * an agent falls back to when it has decided to see the PR land. See {@link AwaitLoop} for why a\n * blocking command is the only wait a worktree-isolated subagent can express.\n *\n * ─── It reports, it does not judge ─────────────────────────────────────────────────────────────────\n * A red check is an ANSWER, so the wait ends and the state is printed. Deciding what a failure means is\n * the caller's, and turning a red check into a non-zero exit here would make \"the checks finished\" and\n * \"the checks passed\" the same signal — which is exactly how a red PR gets reported as landed.\n */\n@injectable(bindingScopeValues.Singleton)\nexport class AwaitChecksCommand {\n constructor(\n private readonly awaitLoop: AwaitLoop,\n private readonly stageConsole: StageOutputLog,\n ) {}\n\n async run(opts: AwaitChecksOptions): Promise<void> {\n const probe = new ChecksWaitProbe(opts.prNumber);\n const outcome = await this.awaitLoop.run(probe);\n this.stageConsole.say(outcome.done ? probe.settledReport(outcome) : probe.stillWaitingReport(outcome));\n }\n}\n\n/** One check run's rollup state, as GitHub reports it. Data-only. */\nexport class ChecksState {\n /** How many checks GitHub currently lists. 0 means it has not created any yet — QUEUED, not absent. */\n total: number;\n pending: number;\n failed: number;\n /** True when `gh` could not be asked at all — the wait keeps going rather than claiming an answer. */\n unreadable: boolean;\n\n // eslint-disable-next-line @typescript-eslint/max-params\n constructor(total: number, pending: number, failed: number, unreadable: boolean) {\n this.total = total;\n this.pending = pending;\n this.failed = failed;\n this.unreadable = unreadable;\n }\n\n /**\n * Settled means GitHub has created checks AND none of them is still running.\n *\n * `total === 0` is deliberately NOT settled. An empty rollup seconds after a push means the workflow\n * has not been created yet — QUEUED — and treating it as \"no checks, all clear\" is how a PR gets\n * called green before CI has started. The wait's own ceiling is what ends that case, and it ends it\n * by saying \"still waiting\", which is the truth.\n */\n get settled(): boolean {\n return !this.unreadable && this.total > 0 && this.pending === 0;\n }\n\n describe(): string {\n if (this.unreadable) return 'gh could not be read';\n if (this.total === 0) return 'no checks reported yet (queued)';\n return `${String(this.total - this.pending)} of ${String(this.total)} done, `\n + `${String(this.failed)} failed`;\n }\n}\n\n/**\n * The CI wait: one `gh pr view` per poll, classified into {@link ChecksState}.\n *\n * `gh pr view --json statusCheckRollup` rather than `gh pr checks`, because the rollup field is the\n * stable JSON surface and returns an EMPTY array — not an error exit — for a PR whose checks have not\n * been created yet. `gh pr checks` exits non-zero in several distinct \"nothing to report\" situations,\n * which would make \"queued\" and \"gh is broken\" the same observation.\n */\nexport class ChecksWaitProbe implements WaitProbe {\n readonly label = 'CI checks';\n // A network round trip per poll, so this is deliberately slow. CI on this repo runs ~1 minute; a\n // 15-second poll finds it within a heartbeat of finishing and costs ~36 API calls per full wait.\n readonly pollMs = 15_000;\n\n private state: ChecksState = new ChecksState(0, 0, 0, true);\n\n constructor(private readonly prNumber: string) {}\n\n done(): boolean {\n this.state = this.read();\n return this.state.settled;\n }\n\n describe(): string {\n return this.state.describe();\n }\n\n settledReport(outcome: WaitOutcome): string {\n const verdict = this.state.failed === 0\n ? `🟢 All ${String(this.state.total)} check(s) passed`\n : `🔴 ${String(this.state.failed)} of ${String(this.state.total)} check(s) FAILED`;\n return `\\n${verdict} after ${String(outcome.waitedSeconds)}s.\\n`\n + ` Read the detail with: gh pr checks ${this.prNumber}\\n`;\n }\n\n stillWaitingReport(outcome: WaitOutcome): string {\n return `\\n⏳ Checks are still in flight after ${String(outcome.waitedSeconds)}s (${this.state.describe()})`\n + ` — run me again: pnpm wp-await-checks --pr ${this.prNumber}\\n`\n + ' (This is not a failure. The wait returns before the harness kills a quiet command.)\\n';\n }\n\n /**\n * One read of the rollup. Fails to `unreadable` rather than to \"settled\": a `gh` that cannot answer\n * must never be able to end the wait, because the only thing worse than waiting too long is\n * announcing an outcome nobody observed.\n */\n private read(): ChecksState {\n const result = spawnSync(\n 'gh',\n ['pr', 'view', this.prNumber, '--json', 'statusCheckRollup',\n '--jq', '[.statusCheckRollup[] | (.status // .state)] | @tsv'],\n { encoding: 'utf8' });\n if (result.status !== 0) return new ChecksState(0, 0, 0, true);\n return this.classify((result.stdout ?? '').trim());\n }\n\n // `status` is COMPLETED / IN_PROGRESS / QUEUED for a check run; a plain commit status has no\n // `status` and its `state` is SUCCESS / PENDING / FAILURE / ERROR. The jq above collapses the two\n // into one token per check, and everything that is not finished counts as pending.\n private classify(tsv: string): ChecksState {\n const tokens = tsv === '' ? [] : tsv.split(/\\s+/);\n const pending = tokens.filter((t: string): boolean => PENDING_TOKENS.has(t)).length;\n const failed = tokens.filter((t: string): boolean => FAILED_TOKENS.has(t)).length;\n return new ChecksState(tokens.length, pending, failed, false);\n }\n}\n\nconst PENDING_TOKENS: ReadonlySet<string> = new Set(['QUEUED', 'IN_PROGRESS', 'PENDING', 'WAITING', 'REQUESTED']);\nconst FAILED_TOKENS: ReadonlySet<string> = new Set(['FAILURE', 'ERROR', 'TIMED_OUT', 'CANCELLED', 'ACTION_REQUIRED', 'STARTUP_FAILURE']);\n\n/**\n * `--pr` is required, and this is where that is enforced — before the loop starts, so a caller that\n * forgot it is told immediately rather than after a nine-minute wait on nothing.\n */\nexport class AwaitChecksArgs {\n /** The PR number from argv, or a CliExitError naming the flag it needs. */\n parse(value: string): AwaitChecksOptions {\n if (!/^\\d+$/.test(value.trim())) {\n throw new CliExitError(2,\n '❌ wp-await-checks needs the PR number to wait on: pnpm wp-await-checks --pr <n>\\n'\n + ' `gh pr view --json number` prints it for the current branch.');\n }\n return new AwaitChecksOptions(value.trim());\n }\n}\n"]}
@@ -0,0 +1,85 @@
1
+ import { RepoRootFinder, RequiredChecklist, ReviewJsonService } from '@webpieces/rules-config';
2
+ import { AwaitLoop, WaitOutcome, WaitProbe } from '../workflow/await-loop';
3
+ import { ChecklistScanner } from '../workflow/checklist-scanner';
4
+ import { StageOutputLog } from '../workflow/stage-output-log';
5
+ /**
6
+ * `wp-await-reviews` — BLOCK until every reviewer stage ② requested for this branch has written its
7
+ * verdict, then print what each one said.
8
+ *
9
+ * ─── Why a command and not an instruction ──────────────────────────────────────────────────────────
10
+ * An agent that has just spawned four reviewer subagents has nothing to do until they answer, and a
11
+ * worktree-isolated subagent cannot simply wait: ending its turn ends its run. See {@link AwaitLoop}
12
+ * for the full chain and the measurement (18.3% of all fleet tokens in one day spent on `echo .`).
13
+ * A blocking Bash call is the only wait primitive it has, so webpieces provides one.
14
+ *
15
+ * ─── It INVENTS NO STATE ───────────────────────────────────────────────────────────────────────────
16
+ * The set of reviewers a branch owes is `ChecklistScanner`'s answer — the SAME one `wp-review-upsert-pr`
17
+ * lists and `wp-finish-upsert-pr` blocks on — and the verdicts are the `review-<id>.json` files those
18
+ * two already read. Nothing here writes anything, and there is no third opinion about who is owed: a
19
+ * waiter that computed its own set could finish while finish still refused, which is the one outcome
20
+ * that would make waiting worse than not waiting.
21
+ *
22
+ * The scan runs ONCE. Its `applicable` set is a function of the diff, which does not change while the
23
+ * agent sits still, so re-scanning per poll would re-run `git diff` every few seconds to re-derive an
24
+ * unchanged answer. Only the verdict FILES are re-read, which is the only thing a reviewer changes.
25
+ *
26
+ * ─── What it does NOT do ───────────────────────────────────────────────────────────────────────────
27
+ * It does not judge. A red verdict is a completed review, so this command RETURNS on it and prints it;
28
+ * refusing the PR over it is `wp-finish-upsert-pr`'s job and stays there. It exits non-zero only when it
29
+ * cannot do its own job at all.
30
+ */
31
+ export declare class AwaitReviewsCommand {
32
+ private readonly repoRootFinder;
33
+ private readonly checklistScanner;
34
+ private readonly reviewJsonService;
35
+ private readonly awaitLoop;
36
+ private readonly stageConsole;
37
+ constructor(repoRootFinder: RepoRootFinder, checklistScanner: ChecklistScanner, reviewJsonService: ReviewJsonService, awaitLoop: AwaitLoop, stageConsole: StageOutputLog);
38
+ run(): Promise<void>;
39
+ private reportNothingOwed;
40
+ private reportDisabled;
41
+ private report;
42
+ }
43
+ /**
44
+ * The reviewer wait itself: which checklists are owed, and what the files on disk say about them right
45
+ * now. It owns the reporting too, because every line it prints is derived from the same verdict read —
46
+ * a report that re-read the files could disagree with the answer that ended the wait.
47
+ */
48
+ export declare class ReviewerWaitProbe implements WaitProbe {
49
+ private readonly reviewJsonService;
50
+ private readonly reviewPath;
51
+ private readonly waitedOn;
52
+ private readonly applicable;
53
+ readonly label = "reviewers";
54
+ readonly pollMs = 3000;
55
+ private results;
56
+ constructor(reviewJsonService: ReviewJsonService, reviewPath: string, waitedOn: readonly RequiredChecklist[], applicable: readonly RequiredChecklist[]);
57
+ /**
58
+ * Done when every awaited reviewer has ANSWERED — not when every answer is a pass.
59
+ *
60
+ * That distinction is the whole contract of a wait, and getting it wrong makes the command useless
61
+ * in the case it matters most. `ReviewJsonService.pendingChecklists` is finish's GATE predicate and
62
+ * counts a RED verdict as still owed, correctly: the PR is refused until the finding is fixed. But
63
+ * a red reviewer has finished; it will never write a different answer on its own, so waiting for it
64
+ * to stop being red is waiting forever. The wait therefore ends on ARRIVAL — a verdict file that
65
+ * exists, whatever it says — and `wp-finish-upsert-pr` keeps sole ownership of the gate.
66
+ *
67
+ * `CK_BAD_FORMAT` counts as arrived too: the file is there and unreadable, which is a thing to FIX
68
+ * (four characters of JSON), not a thing to wait longer for.
69
+ */
70
+ done(): boolean;
71
+ describe(): string;
72
+ /** Every reviewer's answer, printed once the wait is over — the reason to wait rather than sleep. */
73
+ verdictReport(outcome: WaitOutcome): string;
74
+ /**
75
+ * The timeout report. It is a NORMAL exit and says so, because an agent that reads this as a failure
76
+ * goes hunting for a broken reviewer instead of running one more plain command.
77
+ */
78
+ stillWaitingReport(outcome: WaitOutcome): string;
79
+ private oneVerdict;
80
+ private detail;
81
+ private icon;
82
+ private nextStep;
83
+ private pending;
84
+ private reload;
85
+ }
@@ -0,0 +1,199 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ReviewerWaitProbe = exports.AwaitReviewsCommand = void 0;
4
+ const tslib_1 = require("tslib");
5
+ const rules_config_1 = require("@webpieces/rules-config");
6
+ const inversify_1 = require("inversify");
7
+ const await_loop_1 = require("../workflow/await-loop");
8
+ const checklist_scanner_1 = require("../workflow/checklist-scanner");
9
+ const stage_output_log_1 = require("../workflow/stage-output-log");
10
+ /**
11
+ * `wp-await-reviews` — BLOCK until every reviewer stage ② requested for this branch has written its
12
+ * verdict, then print what each one said.
13
+ *
14
+ * ─── Why a command and not an instruction ──────────────────────────────────────────────────────────
15
+ * An agent that has just spawned four reviewer subagents has nothing to do until they answer, and a
16
+ * worktree-isolated subagent cannot simply wait: ending its turn ends its run. See {@link AwaitLoop}
17
+ * for the full chain and the measurement (18.3% of all fleet tokens in one day spent on `echo .`).
18
+ * A blocking Bash call is the only wait primitive it has, so webpieces provides one.
19
+ *
20
+ * ─── It INVENTS NO STATE ───────────────────────────────────────────────────────────────────────────
21
+ * The set of reviewers a branch owes is `ChecklistScanner`'s answer — the SAME one `wp-review-upsert-pr`
22
+ * lists and `wp-finish-upsert-pr` blocks on — and the verdicts are the `review-<id>.json` files those
23
+ * two already read. Nothing here writes anything, and there is no third opinion about who is owed: a
24
+ * waiter that computed its own set could finish while finish still refused, which is the one outcome
25
+ * that would make waiting worse than not waiting.
26
+ *
27
+ * The scan runs ONCE. Its `applicable` set is a function of the diff, which does not change while the
28
+ * agent sits still, so re-scanning per poll would re-run `git diff` every few seconds to re-derive an
29
+ * unchanged answer. Only the verdict FILES are re-read, which is the only thing a reviewer changes.
30
+ *
31
+ * ─── What it does NOT do ───────────────────────────────────────────────────────────────────────────
32
+ * It does not judge. A red verdict is a completed review, so this command RETURNS on it and prints it;
33
+ * refusing the PR over it is `wp-finish-upsert-pr`'s job and stays there. It exits non-zero only when it
34
+ * cannot do its own job at all.
35
+ */
36
+ let AwaitReviewsCommand = class AwaitReviewsCommand {
37
+ repoRootFinder;
38
+ checklistScanner;
39
+ reviewJsonService;
40
+ awaitLoop;
41
+ stageConsole;
42
+ constructor(repoRootFinder, checklistScanner, reviewJsonService, awaitLoop, stageConsole) {
43
+ this.repoRootFinder = repoRootFinder;
44
+ this.checklistScanner = checklistScanner;
45
+ this.reviewJsonService = reviewJsonService;
46
+ this.awaitLoop = awaitLoop;
47
+ this.stageConsole = stageConsole;
48
+ }
49
+ async run() {
50
+ const repoRoot = this.repoRootFinder.resolveRepoRoot(process.cwd());
51
+ const config = (0, rules_config_1.loadAndValidate)(repoRoot).prGate;
52
+ // contextStage '' — a WAIT must not write pr-context.json. That file records the diff a review
53
+ // was briefed against, and rewriting it mid-review would restate the briefing under the
54
+ // reviewers currently reading it.
55
+ const scan = this.checklistScanner.scan(repoRoot, config.checklists, new checklist_scanner_1.ChecklistScanOptions(true, ''));
56
+ if (scan.reviewersDisabled)
57
+ return this.reportDisabled(scan.suppressed);
58
+ if (scan.applicable.length === 0)
59
+ return this.reportNothingOwed();
60
+ // WAIT on the set finish blocks on (`outstanding`), REPORT on everything that applies. The two
61
+ // sets differ by exactly the optional checklists nobody ran — `required: false` means the PR does
62
+ // not wait for them, and a wait that did would never end, because nothing is ever going to write
63
+ // a verdict for a reviewer that was deliberately never spawned. (The SET is shared with finish;
64
+ // the PREDICATE is not — see `done()` for why a wait ends on arrival and finish does not.)
65
+ const probe = new ReviewerWaitProbe(this.reviewJsonService, scan.reviewPath, scan.outstanding, scan.applicable);
66
+ const outcome = await this.awaitLoop.run(probe);
67
+ this.report(probe, outcome);
68
+ }
69
+ // Nothing to wait for, and saying so is the whole value: an agent that got no answer here would
70
+ // otherwise assume the wait is still owed and start spinning again.
71
+ reportNothingOwed() {
72
+ this.stageConsole.say('\n✅ No reviewer was requested for this branch — nothing to wait for.\n');
73
+ }
74
+ reportDisabled(suppressed) {
75
+ this.stageConsole.say(`\n⚫ Every reviewer is switched off on this machine (experimental.turnOffAllReviewers), so `
76
+ + `${String(suppressed.length)} checklist(s) that would have run are not running. There is `
77
+ + 'nothing to wait for.\n');
78
+ }
79
+ report(probe, outcome) {
80
+ this.stageConsole.say(outcome.done ? probe.verdictReport(outcome) : probe.stillWaitingReport(outcome));
81
+ }
82
+ };
83
+ exports.AwaitReviewsCommand = AwaitReviewsCommand;
84
+ exports.AwaitReviewsCommand = AwaitReviewsCommand = tslib_1.__decorate([
85
+ (0, inversify_1.injectable)(inversify_1.bindingScopeValues.Singleton),
86
+ tslib_1.__metadata("design:paramtypes", [rules_config_1.RepoRootFinder,
87
+ checklist_scanner_1.ChecklistScanner,
88
+ rules_config_1.ReviewJsonService,
89
+ await_loop_1.AwaitLoop,
90
+ stage_output_log_1.StageOutputLog])
91
+ ], AwaitReviewsCommand);
92
+ /**
93
+ * The reviewer wait itself: which checklists are owed, and what the files on disk say about them right
94
+ * now. It owns the reporting too, because every line it prints is derived from the same verdict read —
95
+ * a report that re-read the files could disagree with the answer that ended the wait.
96
+ */
97
+ class ReviewerWaitProbe {
98
+ reviewJsonService;
99
+ reviewPath;
100
+ waitedOn;
101
+ applicable;
102
+ label = 'reviewers';
103
+ // Verdict files are local reads, so polling them costs nothing worth economizing on. Fast enough
104
+ // that the command returns within seconds of the last reviewer finishing, which is the point.
105
+ pollMs = 3_000;
106
+ results = [];
107
+ constructor(reviewJsonService, reviewPath, waitedOn, applicable) {
108
+ this.reviewJsonService = reviewJsonService;
109
+ this.reviewPath = reviewPath;
110
+ this.waitedOn = waitedOn;
111
+ this.applicable = applicable;
112
+ this.reload();
113
+ }
114
+ /**
115
+ * Done when every awaited reviewer has ANSWERED — not when every answer is a pass.
116
+ *
117
+ * That distinction is the whole contract of a wait, and getting it wrong makes the command useless
118
+ * in the case it matters most. `ReviewJsonService.pendingChecklists` is finish's GATE predicate and
119
+ * counts a RED verdict as still owed, correctly: the PR is refused until the finding is fixed. But
120
+ * a red reviewer has finished; it will never write a different answer on its own, so waiting for it
121
+ * to stop being red is waiting forever. The wait therefore ends on ARRIVAL — a verdict file that
122
+ * exists, whatever it says — and `wp-finish-upsert-pr` keeps sole ownership of the gate.
123
+ *
124
+ * `CK_BAD_FORMAT` counts as arrived too: the file is there and unreadable, which is a thing to FIX
125
+ * (four characters of JSON), not a thing to wait longer for.
126
+ */
127
+ done() {
128
+ this.reload();
129
+ return this.pending().length === 0;
130
+ }
131
+ describe() {
132
+ const owed = this.pending().length;
133
+ const total = this.waitedOn.length;
134
+ return `${String(total - owed)} of ${String(total)} verdicts in`;
135
+ }
136
+ /** Every reviewer's answer, printed once the wait is over — the reason to wait rather than sleep. */
137
+ verdictReport(outcome) {
138
+ // The headline counts what was WAITED ON; the list below covers everything that APPLIES, so an
139
+ // optional checklist nobody ran shows as ❓ rather than being quietly dropped. Reporting only the
140
+ // waited-on set would let a shorter list imply the others passed.
141
+ const lines = [
142
+ `\n✅ All ${String(this.waitedOn.length)} awaited reviewer verdict(s) are in after `
143
+ + `${String(outcome.waitedSeconds)}s:\n`,
144
+ ];
145
+ for (const req of this.applicable)
146
+ lines.push(this.oneVerdict(req));
147
+ lines.push(this.nextStep());
148
+ return lines.join('\n') + '\n';
149
+ }
150
+ /**
151
+ * The timeout report. It is a NORMAL exit and says so, because an agent that reads this as a failure
152
+ * goes hunting for a broken reviewer instead of running one more plain command.
153
+ */
154
+ stillWaitingReport(outcome) {
155
+ const owed = this.pending();
156
+ return `\n⏳ Still waiting after ${String(outcome.waitedSeconds)}s — run me again: pnpm wp-await-reviews\n`
157
+ + ' (This is not a failure. The wait returns before the harness kills a quiet command, so a\n'
158
+ + ' long review costs a handful of calls instead of hundreds of keep-alive turns.)\n\n'
159
+ + ` ${String(owed.length)} of ${String(this.waitedOn.length)} still owe a verdict: `
160
+ + `${owed.map((r) => r.id).join(', ')}\n`;
161
+ }
162
+ oneVerdict(req) {
163
+ const verdict = this.reviewJsonService.resolveVerdict(req, this.results);
164
+ return ` ${this.icon(verdict)} ${verdict.id} — ${verdict.status}${this.detail(verdict)}`;
165
+ }
166
+ // Only a NON-passing verdict's detail is printed. A green reviewer's output is already on the PR,
167
+ // and reprinting four of them here would bury the one line that needs acting on.
168
+ detail(verdict) {
169
+ if (verdict.status !== rules_config_1.CK_FAIL || verdict.detail === '')
170
+ return '';
171
+ return `\n ${verdict.detail.split('\n').join('\n ')}`;
172
+ }
173
+ icon(verdict) {
174
+ if (verdict.status === rules_config_1.CK_FAIL)
175
+ return '🔴';
176
+ if (verdict.status === rules_config_1.CK_MISSING || verdict.status === rules_config_1.CK_BAD_FORMAT)
177
+ return '❓';
178
+ return '🟢';
179
+ }
180
+ // Named because a wait that ends without saying what it unblocked leaves the agent to guess, and
181
+ // guessing here means spinning again. A red verdict changes the next move, so it is called out.
182
+ nextStep() {
183
+ const anyRed = this.applicable.some((req) => this.reviewJsonService.resolveVerdict(req, this.results).status === rules_config_1.CK_FAIL);
184
+ return anyRed
185
+ ? '\n A reviewer REFUSED. Fix what it found — re-spawning it against unchanged code buys the\n'
186
+ + ' same answer — then re-run the review stage.\n'
187
+ : '\n Nothing is owed on the review front.\n';
188
+ }
189
+ // The awaited checklists whose verdict file has not appeared yet — see `done()` for why this is
190
+ // arrival and not the gate's own "still owed".
191
+ pending() {
192
+ return this.waitedOn.filter((req) => this.reviewJsonService.resolveVerdict(req, this.results).status === rules_config_1.CK_MISSING);
193
+ }
194
+ reload() {
195
+ this.results = this.reviewJsonService.loadChecklistResults(this.reviewPath, this.applicable);
196
+ }
197
+ }
198
+ exports.ReviewerWaitProbe = ReviewerWaitProbe;
199
+ //# sourceMappingURL=await-reviews-command.js.map