@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 +6 -4
- package/src/scripts/commands/await-checks-command.d.ts +88 -0
- package/src/scripts/commands/await-checks-command.js +174 -0
- package/src/scripts/commands/await-checks-command.js.map +1 -0
- package/src/scripts/commands/await-reviews-command.d.ts +85 -0
- package/src/scripts/commands/await-reviews-command.js +199 -0
- package/src/scripts/commands/await-reviews-command.js.map +1 -0
- package/src/scripts/pr-gate-app.d.ts +15 -1
- package/src/scripts/pr-gate-app.js +23 -1
- package/src/scripts/pr-gate-app.js.map +1 -1
- package/src/scripts/workflow/await-loop.d.ts +90 -0
- package/src/scripts/workflow/await-loop.js +131 -0
- package/src/scripts/workflow/await-loop.js.map +1 -0
- package/src/scripts/workflow/finish-banner.d.ts +11 -0
- package/src/scripts/workflow/finish-banner.js +18 -1
- package/src/scripts/workflow/finish-banner.js.map +1 -1
- package/src/scripts/workflow/review-report.d.ts +14 -0
- package/src/scripts/workflow/review-report.js +35 -4
- package/src/scripts/workflow/review-report.js.map +1 -1
- package/src/scripts/wp-await-checks.d.ts +2 -0
- package/src/scripts/wp-await-checks.js +27 -0
- package/src/scripts/wp-await-checks.js.map +1 -0
- package/src/scripts/wp-await-reviews.d.ts +2 -0
- package/src/scripts/wp-await-reviews.js +24 -0
- package/src/scripts/wp-await-reviews.js.map +1 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"await-reviews-command.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/pr-gate/src/scripts/commands/await-reviews-command.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n ChecklistResult, ChecklistVerdict, CK_FAIL, CK_MISSING, CK_BAD_FORMAT, loadAndValidate,\n RepoRootFinder, RequiredChecklist, ReviewJsonService,\n} from '@webpieces/rules-config';\nimport { injectable, bindingScopeValues } from 'inversify';\n\nimport { AwaitLoop, WaitOutcome, WaitProbe } from '../workflow/await-loop';\nimport { ChecklistScanOptions, ChecklistScanner } from '../workflow/checklist-scanner';\nimport { StageOutputLog } from '../workflow/stage-output-log';\n\n/**\n * `wp-await-reviews` — BLOCK until every reviewer stage ② requested for this branch has written its\n * verdict, then print what each one said.\n *\n * ─── Why a command and not an instruction ──────────────────────────────────────────────────────────\n * An agent that has just spawned four reviewer subagents has nothing to do until they answer, and a\n * worktree-isolated subagent cannot simply wait: ending its turn ends its run. See {@link AwaitLoop}\n * for the full chain and the measurement (18.3% of all fleet tokens in one day spent on `echo .`).\n * A blocking Bash call is the only wait primitive it has, so webpieces provides one.\n *\n * ─── It INVENTS NO STATE ───────────────────────────────────────────────────────────────────────────\n * The set of reviewers a branch owes is `ChecklistScanner`'s answer — the SAME one `wp-review-upsert-pr`\n * lists and `wp-finish-upsert-pr` blocks on — and the verdicts are the `review-<id>.json` files those\n * two already read. Nothing here writes anything, and there is no third opinion about who is owed: a\n * waiter that computed its own set could finish while finish still refused, which is the one outcome\n * that would make waiting worse than not waiting.\n *\n * The scan runs ONCE. Its `applicable` set is a function of the diff, which does not change while the\n * agent sits still, so re-scanning per poll would re-run `git diff` every few seconds to re-derive an\n * unchanged answer. Only the verdict FILES are re-read, which is the only thing a reviewer changes.\n *\n * ─── What it does NOT do ───────────────────────────────────────────────────────────────────────────\n * It does not judge. A red verdict is a completed review, so this command RETURNS on it and prints it;\n * refusing the PR over it is `wp-finish-upsert-pr`'s job and stays there. It exits non-zero only when it\n * cannot do its own job at all.\n */\n@injectable(bindingScopeValues.Singleton)\nexport class AwaitReviewsCommand {\n constructor(\n private readonly repoRootFinder: RepoRootFinder,\n private readonly checklistScanner: ChecklistScanner,\n private readonly reviewJsonService: ReviewJsonService,\n private readonly awaitLoop: AwaitLoop,\n private readonly stageConsole: StageOutputLog,\n ) {}\n\n async run(): Promise<void> {\n const repoRoot = this.repoRootFinder.resolveRepoRoot(process.cwd());\n const config = loadAndValidate(repoRoot).prGate;\n // contextStage '' — a WAIT must not write pr-context.json. That file records the diff a review\n // was briefed against, and rewriting it mid-review would restate the briefing under the\n // reviewers currently reading it.\n const scan = this.checklistScanner.scan(repoRoot, config.checklists, new ChecklistScanOptions(true, ''));\n if (scan.reviewersDisabled) return this.reportDisabled(scan.suppressed);\n if (scan.applicable.length === 0) return this.reportNothingOwed();\n // WAIT on the set finish blocks on (`outstanding`), REPORT on everything that applies. The two\n // sets differ by exactly the optional checklists nobody ran — `required: false` means the PR does\n // not wait for them, and a wait that did would never end, because nothing is ever going to write\n // a verdict for a reviewer that was deliberately never spawned. (The SET is shared with finish;\n // the PREDICATE is not — see `done()` for why a wait ends on arrival and finish does not.)\n const probe = new ReviewerWaitProbe(\n this.reviewJsonService, scan.reviewPath, scan.outstanding, scan.applicable);\n const outcome = await this.awaitLoop.run(probe);\n this.report(probe, outcome);\n }\n\n // Nothing to wait for, and saying so is the whole value: an agent that got no answer here would\n // otherwise assume the wait is still owed and start spinning again.\n private reportNothingOwed(): void {\n this.stageConsole.say('\\n✅ No reviewer was requested for this branch — nothing to wait for.\\n');\n }\n\n private reportDisabled(suppressed: readonly RequiredChecklist[]): void {\n this.stageConsole.say(\n `\\n⚫ Every reviewer is switched off on this machine (experimental.turnOffAllReviewers), so `\n + `${String(suppressed.length)} checklist(s) that would have run are not running. There is `\n + 'nothing to wait for.\\n');\n }\n\n private report(probe: ReviewerWaitProbe, outcome: WaitOutcome): void {\n this.stageConsole.say(outcome.done ? probe.verdictReport(outcome) : probe.stillWaitingReport(outcome));\n }\n}\n\n/**\n * The reviewer wait itself: which checklists are owed, and what the files on disk say about them right\n * now. It owns the reporting too, because every line it prints is derived from the same verdict read —\n * a report that re-read the files could disagree with the answer that ended the wait.\n */\nexport class ReviewerWaitProbe implements WaitProbe {\n readonly label = 'reviewers';\n // Verdict files are local reads, so polling them costs nothing worth economizing on. Fast enough\n // that the command returns within seconds of the last reviewer finishing, which is the point.\n readonly pollMs = 3_000;\n\n private results: ChecklistResult[] = [];\n\n constructor(\n private readonly reviewJsonService: ReviewJsonService,\n private readonly reviewPath: string,\n private readonly waitedOn: readonly RequiredChecklist[],\n private readonly applicable: readonly RequiredChecklist[],\n ) {\n this.reload();\n }\n\n /**\n * Done when every awaited reviewer has ANSWERED — not when every answer is a pass.\n *\n * That distinction is the whole contract of a wait, and getting it wrong makes the command useless\n * in the case it matters most. `ReviewJsonService.pendingChecklists` is finish's GATE predicate and\n * counts a RED verdict as still owed, correctly: the PR is refused until the finding is fixed. But\n * a red reviewer has finished; it will never write a different answer on its own, so waiting for it\n * to stop being red is waiting forever. The wait therefore ends on ARRIVAL — a verdict file that\n * exists, whatever it says — and `wp-finish-upsert-pr` keeps sole ownership of the gate.\n *\n * `CK_BAD_FORMAT` counts as arrived too: the file is there and unreadable, which is a thing to FIX\n * (four characters of JSON), not a thing to wait longer for.\n */\n done(): boolean {\n this.reload();\n return this.pending().length === 0;\n }\n\n describe(): string {\n const owed = this.pending().length;\n const total = this.waitedOn.length;\n return `${String(total - owed)} of ${String(total)} verdicts in`;\n }\n\n /** Every reviewer's answer, printed once the wait is over — the reason to wait rather than sleep. */\n verdictReport(outcome: WaitOutcome): string {\n // The headline counts what was WAITED ON; the list below covers everything that APPLIES, so an\n // optional checklist nobody ran shows as ❓ rather than being quietly dropped. Reporting only the\n // waited-on set would let a shorter list imply the others passed.\n const lines = [\n `\\n✅ All ${String(this.waitedOn.length)} awaited reviewer verdict(s) are in after `\n + `${String(outcome.waitedSeconds)}s:\\n`,\n ];\n for (const req of this.applicable) lines.push(this.oneVerdict(req));\n lines.push(this.nextStep());\n return lines.join('\\n') + '\\n';\n }\n\n /**\n * The timeout report. It is a NORMAL exit and says so, because an agent that reads this as a failure\n * goes hunting for a broken reviewer instead of running one more plain command.\n */\n stillWaitingReport(outcome: WaitOutcome): string {\n const owed = this.pending();\n return `\\n⏳ Still waiting after ${String(outcome.waitedSeconds)}s — run me again: pnpm wp-await-reviews\\n`\n + ' (This is not a failure. The wait returns before the harness kills a quiet command, so a\\n'\n + ' long review costs a handful of calls instead of hundreds of keep-alive turns.)\\n\\n'\n + ` ${String(owed.length)} of ${String(this.waitedOn.length)} still owe a verdict: `\n + `${owed.map((r: RequiredChecklist): string => r.id).join(', ')}\\n`;\n }\n\n private oneVerdict(req: RequiredChecklist): string {\n const verdict = this.reviewJsonService.resolveVerdict(req, this.results);\n return ` ${this.icon(verdict)} ${verdict.id} — ${verdict.status}${this.detail(verdict)}`;\n }\n\n // Only a NON-passing verdict's detail is printed. A green reviewer's output is already on the PR,\n // and reprinting four of them here would bury the one line that needs acting on.\n private detail(verdict: ChecklistVerdict): string {\n if (verdict.status !== CK_FAIL || verdict.detail === '') return '';\n return `\\n ${verdict.detail.split('\\n').join('\\n ')}`;\n }\n\n private icon(verdict: ChecklistVerdict): string {\n if (verdict.status === CK_FAIL) return '🔴';\n if (verdict.status === CK_MISSING || verdict.status === CK_BAD_FORMAT) return '❓';\n return '🟢';\n }\n\n // Named because a wait that ends without saying what it unblocked leaves the agent to guess, and\n // guessing here means spinning again. A red verdict changes the next move, so it is called out.\n private nextStep(): string {\n const anyRed = this.applicable.some((req: RequiredChecklist): boolean =>\n this.reviewJsonService.resolveVerdict(req, this.results).status === CK_FAIL);\n return anyRed\n ? '\\n A reviewer REFUSED. Fix what it found — re-spawning it against unchanged code buys the\\n'\n + ' same answer — then re-run the review stage.\\n'\n : '\\n Nothing is owed on the review front.\\n';\n }\n\n // The awaited checklists whose verdict file has not appeared yet — see `done()` for why this is\n // arrival and not the gate's own \"still owed\".\n private pending(): RequiredChecklist[] {\n return this.waitedOn.filter((req: RequiredChecklist): boolean =>\n this.reviewJsonService.resolveVerdict(req, this.results).status === CK_MISSING);\n }\n\n private reload(): void {\n this.results = this.reviewJsonService.loadChecklistResults(this.reviewPath, this.applicable);\n }\n}\n"]}
|
|
@@ -10,6 +10,8 @@ import { CheckPrCommand } from './commands/check-pr-command';
|
|
|
10
10
|
import { ReviewUpsertPrCommand, ReviewUpsertPrOptions } from './commands/review-upsert-pr-command';
|
|
11
11
|
import { ReapWorktreeCommand } from './commands/reap-worktree-command';
|
|
12
12
|
import { BuildCommand, BuildOptions } from './commands/build-command';
|
|
13
|
+
import { AwaitReviewsCommand } from './commands/await-reviews-command';
|
|
14
|
+
import { AwaitChecksCommand, AwaitChecksOptions } from './commands/await-checks-command';
|
|
13
15
|
import { PushDevCommand, PushDevOptions } from './commands/push-dev-command';
|
|
14
16
|
import { FinishPushDevCommand, FinishPushDevOptions } from './commands/finish-push-dev-command';
|
|
15
17
|
import { PushDevStateStore } from './workflow/push-dev-state';
|
|
@@ -32,11 +34,13 @@ export declare class PrGateApp {
|
|
|
32
34
|
private readonly reviewUpsertPrCommand;
|
|
33
35
|
private readonly reapWorktreeCommand;
|
|
34
36
|
private readonly buildCommand;
|
|
37
|
+
private readonly awaitReviewsCommand;
|
|
38
|
+
private readonly awaitChecksCommand;
|
|
35
39
|
private readonly pushDevCommand;
|
|
36
40
|
private readonly finishPushDevCommand;
|
|
37
41
|
private readonly pushDevStateStore;
|
|
38
42
|
private readonly repoRootFinder;
|
|
39
|
-
constructor(startUpdateCommand: StartUpdateCommand, finishUpdateCommand: FinishUpdateCommand, startUpsertPrCommand: StartUpsertPrCommand, finishUpsertPrCommand: FinishUpsertPrCommand, cleanupCommand: CleanupCommand, syncMainCommand: SyncMainCommand, landPrCommand: LandPrCommand, checkPrCommand: CheckPrCommand, reviewUpsertPrCommand: ReviewUpsertPrCommand, reapWorktreeCommand: ReapWorktreeCommand, buildCommand: BuildCommand, pushDevCommand: PushDevCommand, finishPushDevCommand: FinishPushDevCommand, pushDevStateStore: PushDevStateStore, repoRootFinder: RepoRootFinder);
|
|
43
|
+
constructor(startUpdateCommand: StartUpdateCommand, finishUpdateCommand: FinishUpdateCommand, startUpsertPrCommand: StartUpsertPrCommand, finishUpsertPrCommand: FinishUpsertPrCommand, cleanupCommand: CleanupCommand, syncMainCommand: SyncMainCommand, landPrCommand: LandPrCommand, checkPrCommand: CheckPrCommand, reviewUpsertPrCommand: ReviewUpsertPrCommand, reapWorktreeCommand: ReapWorktreeCommand, buildCommand: BuildCommand, awaitReviewsCommand: AwaitReviewsCommand, awaitChecksCommand: AwaitChecksCommand, pushDevCommand: PushDevCommand, finishPushDevCommand: FinishPushDevCommand, pushDevStateStore: PushDevStateStore, repoRootFinder: RepoRootFinder);
|
|
40
44
|
/**
|
|
41
45
|
* Refuse `command` while a `wp-push-dev --resolve` is half-finished.
|
|
42
46
|
*
|
|
@@ -62,6 +66,16 @@ export declare class PrGateApp {
|
|
|
62
66
|
* `wp-push-dev --resolve`: it mutates nothing and reads no branch state.
|
|
63
67
|
*/
|
|
64
68
|
build(opts: BuildOptions): Promise<void>;
|
|
69
|
+
/**
|
|
70
|
+
* `wp-await-reviews`: BLOCK until every reviewer verdict this branch owes has landed, then print
|
|
71
|
+
* them. Not blocked during a `wp-push-dev --resolve`: it reads verdict files and mutates nothing.
|
|
72
|
+
*/
|
|
73
|
+
awaitReviews(): Promise<void>;
|
|
74
|
+
/**
|
|
75
|
+
* `wp-await-checks --pr <n>`: BLOCK until that PR's checks stop running, then print where they
|
|
76
|
+
* landed. Read-only against GitHub, so it too is allowed during a resolve.
|
|
77
|
+
*/
|
|
78
|
+
awaitChecks(opts: AwaitChecksOptions): Promise<void>;
|
|
65
79
|
/** `wp-start-update`: 3-point squash-update from main (no PR). */
|
|
66
80
|
startUpdate(): Promise<void>;
|
|
67
81
|
/** `wp-finish-update`: validate + finalize a resolved 3-point merge (no PR). */
|
|
@@ -15,6 +15,8 @@ const check_pr_command_1 = require("./commands/check-pr-command");
|
|
|
15
15
|
const review_upsert_pr_command_1 = require("./commands/review-upsert-pr-command");
|
|
16
16
|
const reap_worktree_command_1 = require("./commands/reap-worktree-command");
|
|
17
17
|
const build_command_1 = require("./commands/build-command");
|
|
18
|
+
const await_reviews_command_1 = require("./commands/await-reviews-command");
|
|
19
|
+
const await_checks_command_1 = require("./commands/await-checks-command");
|
|
18
20
|
const push_dev_command_1 = require("./commands/push-dev-command");
|
|
19
21
|
const finish_push_dev_command_1 = require("./commands/finish-push-dev-command");
|
|
20
22
|
const push_dev_state_1 = require("./workflow/push-dev-state");
|
|
@@ -37,11 +39,13 @@ let PrGateApp = class PrGateApp {
|
|
|
37
39
|
reviewUpsertPrCommand;
|
|
38
40
|
reapWorktreeCommand;
|
|
39
41
|
buildCommand;
|
|
42
|
+
awaitReviewsCommand;
|
|
43
|
+
awaitChecksCommand;
|
|
40
44
|
pushDevCommand;
|
|
41
45
|
finishPushDevCommand;
|
|
42
46
|
pushDevStateStore;
|
|
43
47
|
repoRootFinder;
|
|
44
|
-
constructor(startUpdateCommand, finishUpdateCommand, startUpsertPrCommand, finishUpsertPrCommand, cleanupCommand, syncMainCommand, landPrCommand, checkPrCommand, reviewUpsertPrCommand, reapWorktreeCommand, buildCommand, pushDevCommand, finishPushDevCommand, pushDevStateStore, repoRootFinder) {
|
|
48
|
+
constructor(startUpdateCommand, finishUpdateCommand, startUpsertPrCommand, finishUpsertPrCommand, cleanupCommand, syncMainCommand, landPrCommand, checkPrCommand, reviewUpsertPrCommand, reapWorktreeCommand, buildCommand, awaitReviewsCommand, awaitChecksCommand, pushDevCommand, finishPushDevCommand, pushDevStateStore, repoRootFinder) {
|
|
45
49
|
this.startUpdateCommand = startUpdateCommand;
|
|
46
50
|
this.finishUpdateCommand = finishUpdateCommand;
|
|
47
51
|
this.startUpsertPrCommand = startUpsertPrCommand;
|
|
@@ -53,6 +57,8 @@ let PrGateApp = class PrGateApp {
|
|
|
53
57
|
this.reviewUpsertPrCommand = reviewUpsertPrCommand;
|
|
54
58
|
this.reapWorktreeCommand = reapWorktreeCommand;
|
|
55
59
|
this.buildCommand = buildCommand;
|
|
60
|
+
this.awaitReviewsCommand = awaitReviewsCommand;
|
|
61
|
+
this.awaitChecksCommand = awaitChecksCommand;
|
|
56
62
|
this.pushDevCommand = pushDevCommand;
|
|
57
63
|
this.finishPushDevCommand = finishPushDevCommand;
|
|
58
64
|
this.pushDevStateStore = pushDevStateStore;
|
|
@@ -95,6 +101,20 @@ let PrGateApp = class PrGateApp {
|
|
|
95
101
|
build(opts) {
|
|
96
102
|
return this.buildCommand.run(opts);
|
|
97
103
|
}
|
|
104
|
+
/**
|
|
105
|
+
* `wp-await-reviews`: BLOCK until every reviewer verdict this branch owes has landed, then print
|
|
106
|
+
* them. Not blocked during a `wp-push-dev --resolve`: it reads verdict files and mutates nothing.
|
|
107
|
+
*/
|
|
108
|
+
awaitReviews() {
|
|
109
|
+
return this.awaitReviewsCommand.run();
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* `wp-await-checks --pr <n>`: BLOCK until that PR's checks stop running, then print where they
|
|
113
|
+
* landed. Read-only against GitHub, so it too is allowed during a resolve.
|
|
114
|
+
*/
|
|
115
|
+
awaitChecks(opts) {
|
|
116
|
+
return this.awaitChecksCommand.run(opts);
|
|
117
|
+
}
|
|
98
118
|
/** `wp-start-update`: 3-point squash-update from main (no PR). */
|
|
99
119
|
startUpdate() {
|
|
100
120
|
this.assertNoResolveInProgress('wp-start-update');
|
|
@@ -170,6 +190,8 @@ exports.PrGateApp = PrGateApp = tslib_1.__decorate([
|
|
|
170
190
|
review_upsert_pr_command_1.ReviewUpsertPrCommand,
|
|
171
191
|
reap_worktree_command_1.ReapWorktreeCommand,
|
|
172
192
|
build_command_1.BuildCommand,
|
|
193
|
+
await_reviews_command_1.AwaitReviewsCommand,
|
|
194
|
+
await_checks_command_1.AwaitChecksCommand,
|
|
173
195
|
push_dev_command_1.PushDevCommand,
|
|
174
196
|
finish_push_dev_command_1.FinishPushDevCommand,
|
|
175
197
|
push_dev_state_1.PushDevStateStore,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"pr-gate-app.js","sourceRoot":"","sources":["../../../../../../packages/tooling/pr-gate/src/scripts/pr-gate-app.ts"],"names":[],"mappings":"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","sourcesContent":["import { DocumentDesign } from '@webpieces/rules-config';\nimport { injectable, bindingScopeValues } from 'inversify';\nimport { StartUpdateCommand } from './commands/start-update-command';\nimport { FinishUpdateCommand } from './commands/finish-update-command';\nimport { StartUpsertPrCommand } from './commands/start-upsert-pr-command';\nimport { FinishUpsertPrCommand } from './commands/finish-upsert-pr-command';\nimport { CleanupCommand } from './commands/cleanup-command';\nimport { CleanupOptions } from './commands/cleanup-options';\nimport { SyncMainCommand } from './commands/sync-main-command';\nimport { LandPrCommand, LandPrRequest } from './commands/land-pr-command';\nimport { CheckPrCommand } from './commands/check-pr-command';\nimport { ReviewUpsertPrCommand, ReviewUpsertPrOptions } from './commands/review-upsert-pr-command';\nimport { ReapWorktreeCommand } from './commands/reap-worktree-command';\nimport { BuildCommand, BuildOptions } from './commands/build-command';\nimport { PushDevCommand, PushDevOptions } from './commands/push-dev-command';\nimport { FinishPushDevCommand, FinishPushDevOptions } from './commands/finish-push-dev-command';\nimport { PushDevStateStore } from './workflow/push-dev-state';\nimport { RepoRootFinder } from '@webpieces/rules-config';\n\n/**\n * The pr-gate application root. `container.get(PrGateApp)` resolves the entire workflow DAG (the command\n * classes → the injected git/merge/dashboard services). `@DocumentDesign` marks it the\n * top-of-DAG the DI-design analyzer roots on, so `role:app` pr-gate draws its design. Each `bin/*`\n * entry resolves THIS and calls the matching command method.\n */\n@DocumentDesign()\n@injectable(bindingScopeValues.Singleton)\nexport class PrGateApp {\n constructor(\n private readonly startUpdateCommand: StartUpdateCommand,\n private readonly finishUpdateCommand: FinishUpdateCommand,\n private readonly startUpsertPrCommand: StartUpsertPrCommand,\n private readonly finishUpsertPrCommand: FinishUpsertPrCommand,\n private readonly cleanupCommand: CleanupCommand,\n private readonly syncMainCommand: SyncMainCommand,\n private readonly landPrCommand: LandPrCommand,\n private readonly checkPrCommand: CheckPrCommand,\n private readonly reviewUpsertPrCommand: ReviewUpsertPrCommand,\n private readonly reapWorktreeCommand: ReapWorktreeCommand,\n private readonly buildCommand: BuildCommand,\n private readonly pushDevCommand: PushDevCommand,\n private readonly finishPushDevCommand: FinishPushDevCommand,\n private readonly pushDevStateStore: PushDevStateStore,\n private readonly repoRootFinder: RepoRootFinder,\n ) {}\n\n /**\n * Refuse `command` while a `wp-push-dev --resolve` is half-finished.\n *\n * Enforced HERE, at the one place every bin funnels through, rather than in each command: a resolve\n * parks the checkout on a throwaway branch, so every command below would act on a branch that is not\n * the one it thinks it is. Putting the check in nine constructors is nine chances to forget it in the\n * tenth. PushDevStateStore owns the blocked list AND renders the hint from it, so the two cannot drift.\n */\n private assertNoResolveInProgress(command: string): void {\n if (!this.pushDevStateStore.isBlockedDuringResolve(command)) return;\n this.pushDevStateStore.assertIdle(this.repoRootFinder.resolveRepoRoot(process.cwd()), command);\n }\n\n /** `wp-push-dev`: publish a DISPOSABLE copy of this branch for the shared dev environment. No PR, no build. */\n pushDev(opts: PushDevOptions = new PushDevOptions()): Promise<void> {\n return this.pushDevCommand.run(opts);\n }\n\n /** `wp-finish-push-dev`: commit a resolved dev composition and publish it (conflict path only). */\n finishPushDev(opts: FinishPushDevOptions = new FinishPushDevOptions()): Promise<void> {\n return this.finishPushDevCommand.run(opts);\n }\n\n /**\n * INTERNAL (`wp-reap-worktree.js`, no `bin` entry): remove ONE named worktree and its branch.\n * `wp-land-pr` spawns it with cwd = the primary clone so the tree it just landed from can be\n * reaped by a process that is not standing in it.\n */\n reapWorktree(args: string[]): Promise<void> {\n return this.reapWorktreeCommand.run(args);\n }\n\n /**\n * `wp-build`: run this repo's ONE configured build (`commands.pr-gate.buildCommand`) — the same\n * command, resolved by the same resolver, that the PR gate's build stage runs. Not blocked during a\n * `wp-push-dev --resolve`: it mutates nothing and reads no branch state.\n */\n build(opts: BuildOptions): Promise<void> {\n return this.buildCommand.run(opts);\n }\n\n /** `wp-start-update`: 3-point squash-update from main (no PR). */\n startUpdate(): Promise<void> {\n this.assertNoResolveInProgress('wp-start-update');\n return this.startUpdateCommand.run();\n }\n\n /** `wp-finish-update`: validate + finalize a resolved 3-point merge (no PR). */\n finishUpdate(): Promise<void> {\n this.assertNoResolveInProgress('wp-finish-update');\n return this.finishUpdateCommand.run();\n }\n\n /** `wp-start-upsert-pr`: update from main (3-point merge), hand off review.json. No build gate, no push. */\n startUpsertPr(): Promise<void> {\n this.assertNoResolveInProgress('wp-start-upsert-pr');\n return this.startUpsertPrCommand.run();\n }\n\n /** `wp-finish-upsert-pr`: finalize merge, authoritative build gate, dashboard, create/update PR. */\n finishUpsertPr(): Promise<void> {\n this.assertNoResolveInProgress('wp-finish-upsert-pr');\n return this.finishUpsertPrCommand.run();\n }\n\n /**\n * `wp-cleanup`: reap what is provably merged AND every zero-commit husk, then report the rest with\n * the exact flags that take it. `options` carries what argv said — see CleanupOptions.\n */\n cleanup(options: CleanupOptions): Promise<void> {\n this.assertNoResolveInProgress('wp-cleanup');\n return this.cleanupCommand.run(options);\n }\n\n /**\n * `wp-sync-main`: go to main, fast-forward it, reap dead worktrees and branches, and sweep\n * the orphan directories a project move leaves behind. Replaces `git checkout main && git pull\n * origin main` outright — see SyncMainCommand for why the old pair must stop being accepted\n * rather than surviving beside this.\n */\n syncMain(): Promise<void> {\n this.assertNoResolveInProgress('wp-sync-main');\n return this.syncMainCommand.run();\n }\n\n /**\n * `wp-land-pr`: squash-merge a PR into main with the compact commit body — this branch's by default,\n * or the one `--pr <n>` names, which is how a coordinator finishes a gone agent's work.\n */\n landPr(request: LandPrRequest): Promise<void> {\n this.assertNoResolveInProgress('wp-land-pr');\n return this.landPrCommand.run(request);\n }\n\n /**\n * `wp-review-upsert-pr`: STAGE ② — validate the 3-point merge, run the build gate, extract this\n * branch's diff, and brief the reviewer subagents. Unlike the report-only command it replaces, this CAN\n * fail — before any reviewer is spawned, so a broken branch costs no reviewer tokens.\n */\n reviewUpsertPr(opts: ReviewUpsertPrOptions = new ReviewUpsertPrOptions()): Promise<void> {\n this.assertNoResolveInProgress('wp-review-upsert-pr');\n return this.reviewUpsertPrCommand.run(opts);\n }\n\n /** `wp-check-pr`: READ-ONLY CI check — verify the PR body carries a valid HMAC gate token for its head sha. */\n checkPr(): Promise<void> {\n return this.checkPrCommand.run();\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"pr-gate-app.js","sourceRoot":"","sources":["../../../../../../packages/tooling/pr-gate/src/scripts/pr-gate-app.ts"],"names":[],"mappings":"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","sourcesContent":["import { DocumentDesign } from '@webpieces/rules-config';\nimport { injectable, bindingScopeValues } from 'inversify';\nimport { StartUpdateCommand } from './commands/start-update-command';\nimport { FinishUpdateCommand } from './commands/finish-update-command';\nimport { StartUpsertPrCommand } from './commands/start-upsert-pr-command';\nimport { FinishUpsertPrCommand } from './commands/finish-upsert-pr-command';\nimport { CleanupCommand } from './commands/cleanup-command';\nimport { CleanupOptions } from './commands/cleanup-options';\nimport { SyncMainCommand } from './commands/sync-main-command';\nimport { LandPrCommand, LandPrRequest } from './commands/land-pr-command';\nimport { CheckPrCommand } from './commands/check-pr-command';\nimport { ReviewUpsertPrCommand, ReviewUpsertPrOptions } from './commands/review-upsert-pr-command';\nimport { ReapWorktreeCommand } from './commands/reap-worktree-command';\nimport { BuildCommand, BuildOptions } from './commands/build-command';\nimport { AwaitReviewsCommand } from './commands/await-reviews-command';\nimport { AwaitChecksCommand, AwaitChecksOptions } from './commands/await-checks-command';\nimport { PushDevCommand, PushDevOptions } from './commands/push-dev-command';\nimport { FinishPushDevCommand, FinishPushDevOptions } from './commands/finish-push-dev-command';\nimport { PushDevStateStore } from './workflow/push-dev-state';\nimport { RepoRootFinder } from '@webpieces/rules-config';\n\n/**\n * The pr-gate application root. `container.get(PrGateApp)` resolves the entire workflow DAG (the command\n * classes → the injected git/merge/dashboard services). `@DocumentDesign` marks it the\n * top-of-DAG the DI-design analyzer roots on, so `role:app` pr-gate draws its design. Each `bin/*`\n * entry resolves THIS and calls the matching command method.\n */\n@DocumentDesign()\n@injectable(bindingScopeValues.Singleton)\nexport class PrGateApp {\n constructor(\n private readonly startUpdateCommand: StartUpdateCommand,\n private readonly finishUpdateCommand: FinishUpdateCommand,\n private readonly startUpsertPrCommand: StartUpsertPrCommand,\n private readonly finishUpsertPrCommand: FinishUpsertPrCommand,\n private readonly cleanupCommand: CleanupCommand,\n private readonly syncMainCommand: SyncMainCommand,\n private readonly landPrCommand: LandPrCommand,\n private readonly checkPrCommand: CheckPrCommand,\n private readonly reviewUpsertPrCommand: ReviewUpsertPrCommand,\n private readonly reapWorktreeCommand: ReapWorktreeCommand,\n private readonly buildCommand: BuildCommand,\n private readonly awaitReviewsCommand: AwaitReviewsCommand,\n private readonly awaitChecksCommand: AwaitChecksCommand,\n private readonly pushDevCommand: PushDevCommand,\n private readonly finishPushDevCommand: FinishPushDevCommand,\n private readonly pushDevStateStore: PushDevStateStore,\n private readonly repoRootFinder: RepoRootFinder,\n ) {}\n\n /**\n * Refuse `command` while a `wp-push-dev --resolve` is half-finished.\n *\n * Enforced HERE, at the one place every bin funnels through, rather than in each command: a resolve\n * parks the checkout on a throwaway branch, so every command below would act on a branch that is not\n * the one it thinks it is. Putting the check in nine constructors is nine chances to forget it in the\n * tenth. PushDevStateStore owns the blocked list AND renders the hint from it, so the two cannot drift.\n */\n private assertNoResolveInProgress(command: string): void {\n if (!this.pushDevStateStore.isBlockedDuringResolve(command)) return;\n this.pushDevStateStore.assertIdle(this.repoRootFinder.resolveRepoRoot(process.cwd()), command);\n }\n\n /** `wp-push-dev`: publish a DISPOSABLE copy of this branch for the shared dev environment. No PR, no build. */\n pushDev(opts: PushDevOptions = new PushDevOptions()): Promise<void> {\n return this.pushDevCommand.run(opts);\n }\n\n /** `wp-finish-push-dev`: commit a resolved dev composition and publish it (conflict path only). */\n finishPushDev(opts: FinishPushDevOptions = new FinishPushDevOptions()): Promise<void> {\n return this.finishPushDevCommand.run(opts);\n }\n\n /**\n * INTERNAL (`wp-reap-worktree.js`, no `bin` entry): remove ONE named worktree and its branch.\n * `wp-land-pr` spawns it with cwd = the primary clone so the tree it just landed from can be\n * reaped by a process that is not standing in it.\n */\n reapWorktree(args: string[]): Promise<void> {\n return this.reapWorktreeCommand.run(args);\n }\n\n /**\n * `wp-build`: run this repo's ONE configured build (`commands.pr-gate.buildCommand`) — the same\n * command, resolved by the same resolver, that the PR gate's build stage runs. Not blocked during a\n * `wp-push-dev --resolve`: it mutates nothing and reads no branch state.\n */\n build(opts: BuildOptions): Promise<void> {\n return this.buildCommand.run(opts);\n }\n\n /**\n * `wp-await-reviews`: BLOCK until every reviewer verdict this branch owes has landed, then print\n * them. Not blocked during a `wp-push-dev --resolve`: it reads verdict files and mutates nothing.\n */\n awaitReviews(): Promise<void> {\n return this.awaitReviewsCommand.run();\n }\n\n /**\n * `wp-await-checks --pr <n>`: BLOCK until that PR's checks stop running, then print where they\n * landed. Read-only against GitHub, so it too is allowed during a resolve.\n */\n awaitChecks(opts: AwaitChecksOptions): Promise<void> {\n return this.awaitChecksCommand.run(opts);\n }\n\n /** `wp-start-update`: 3-point squash-update from main (no PR). */\n startUpdate(): Promise<void> {\n this.assertNoResolveInProgress('wp-start-update');\n return this.startUpdateCommand.run();\n }\n\n /** `wp-finish-update`: validate + finalize a resolved 3-point merge (no PR). */\n finishUpdate(): Promise<void> {\n this.assertNoResolveInProgress('wp-finish-update');\n return this.finishUpdateCommand.run();\n }\n\n /** `wp-start-upsert-pr`: update from main (3-point merge), hand off review.json. No build gate, no push. */\n startUpsertPr(): Promise<void> {\n this.assertNoResolveInProgress('wp-start-upsert-pr');\n return this.startUpsertPrCommand.run();\n }\n\n /** `wp-finish-upsert-pr`: finalize merge, authoritative build gate, dashboard, create/update PR. */\n finishUpsertPr(): Promise<void> {\n this.assertNoResolveInProgress('wp-finish-upsert-pr');\n return this.finishUpsertPrCommand.run();\n }\n\n /**\n * `wp-cleanup`: reap what is provably merged AND every zero-commit husk, then report the rest with\n * the exact flags that take it. `options` carries what argv said — see CleanupOptions.\n */\n cleanup(options: CleanupOptions): Promise<void> {\n this.assertNoResolveInProgress('wp-cleanup');\n return this.cleanupCommand.run(options);\n }\n\n /**\n * `wp-sync-main`: go to main, fast-forward it, reap dead worktrees and branches, and sweep\n * the orphan directories a project move leaves behind. Replaces `git checkout main && git pull\n * origin main` outright — see SyncMainCommand for why the old pair must stop being accepted\n * rather than surviving beside this.\n */\n syncMain(): Promise<void> {\n this.assertNoResolveInProgress('wp-sync-main');\n return this.syncMainCommand.run();\n }\n\n /**\n * `wp-land-pr`: squash-merge a PR into main with the compact commit body — this branch's by default,\n * or the one `--pr <n>` names, which is how a coordinator finishes a gone agent's work.\n */\n landPr(request: LandPrRequest): Promise<void> {\n this.assertNoResolveInProgress('wp-land-pr');\n return this.landPrCommand.run(request);\n }\n\n /**\n * `wp-review-upsert-pr`: STAGE ② — validate the 3-point merge, run the build gate, extract this\n * branch's diff, and brief the reviewer subagents. Unlike the report-only command it replaces, this CAN\n * fail — before any reviewer is spawned, so a broken branch costs no reviewer tokens.\n */\n reviewUpsertPr(opts: ReviewUpsertPrOptions = new ReviewUpsertPrOptions()): Promise<void> {\n this.assertNoResolveInProgress('wp-review-upsert-pr');\n return this.reviewUpsertPrCommand.run(opts);\n }\n\n /** `wp-check-pr`: READ-ONLY CI check — verify the PR body carries a valid HMAC gate token for its head sha. */\n checkPr(): Promise<void> {\n return this.checkPrCommand.run();\n }\n}\n"]}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { StageOutputLog } from './stage-output-log';
|
|
2
|
+
/**
|
|
3
|
+
* THE BLOCKING WAIT, and why webpieces has to own one (issue #874).
|
|
4
|
+
*
|
|
5
|
+
* A worktree-isolated subagent has exactly one way to pause: a Bash call, which blocks until its
|
|
6
|
+
* command exits. It cannot end its turn — a subagent that stops making tool calls is FINISHED, and its
|
|
7
|
+
* run returns to its parent. `Monitor` does not block; its own result says "Keep working — do not poll
|
|
8
|
+
* or sleep". And a `Monitor` carrying a real polling loop is refused by the HARNESS, because a
|
|
9
|
+
* `while`/`until` with a redirect cannot be statically proven to stay inside the worktree (178 of 553
|
|
10
|
+
* measured subagent Monitor calls). That refusal is Claude Code's and is not ours to relax.
|
|
11
|
+
*
|
|
12
|
+
* So the agent falls back to `echo .` every three seconds — measured at 920M tokens, 18.3% of every
|
|
13
|
+
* token the fleet spent in the 24h to 2026-09-07, because every turn resends the whole conversation at
|
|
14
|
+
* ~557,000 tokens. This class is the thing that makes that unnecessary, and `wait-spin-guard` is the
|
|
15
|
+
* thing that makes it unused.
|
|
16
|
+
*
|
|
17
|
+
* ─── THREE CONSTRAINTS, all of them the harness's, none of them negotiable ─────────────────────────
|
|
18
|
+
*
|
|
19
|
+
* 1. **What the AGENT TYPES must be a plain command.** No loop, no redirect, no pipe, no command
|
|
20
|
+
* substitution — those are exactly what the isolation check refuses. All the looping is in here,
|
|
21
|
+
* inside one process, behind a bare `pnpm wp-await-reviews`.
|
|
22
|
+
* 2. **It must print something roughly every {@link HEARTBEAT_MS}.** The harness backgrounds and then
|
|
23
|
+
* KILLS a foreground command that has produced no output for 600 seconds, and a killed wait throws
|
|
24
|
+
* away the wait. Same defence `wp-build` already runs, same `still` word for the same reason: a
|
|
25
|
+
* tick that has not moved must be distinguishable from a stalled reporter.
|
|
26
|
+
* 3. **It must RETURN before that ceiling on its own**, saying to run it again. A wait that dies at
|
|
27
|
+
* the watchdog looks like a crash; a wait that returns at {@link CEILING_MS} with "still waiting"
|
|
28
|
+
* turns a 100-minute wait into ~11 tool calls instead of the ~2,000 an `echo .` loop costs.
|
|
29
|
+
*
|
|
30
|
+
* `done` is a normal, ZERO exit either way. "Still waiting" is not a failure — it is an answer the agent
|
|
31
|
+
* acts on — and making it non-zero would put a red exit code on the healthy path.
|
|
32
|
+
*/
|
|
33
|
+
export declare class AwaitLoop {
|
|
34
|
+
private readonly stageConsole;
|
|
35
|
+
constructor(stageConsole: StageOutputLog);
|
|
36
|
+
/**
|
|
37
|
+
* Block until `probe` says it is done, or until {@link CEILING_MS} elapses, printing a heartbeat
|
|
38
|
+
* throughout. Returns WHICH of those happened; the caller renders the outcome.
|
|
39
|
+
*/
|
|
40
|
+
run(probe: WaitProbe): Promise<WaitOutcome>;
|
|
41
|
+
private sleep;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* WHAT is being waited for. An interface rather than a class because it is business logic — `done()`
|
|
45
|
+
* reads verdict files for one waiter and asks GitHub for the other — and the loop must not know which.
|
|
46
|
+
*/
|
|
47
|
+
export interface WaitProbe {
|
|
48
|
+
/** Names the wait in every heartbeat line, e.g. `reviewers` / `checks`. */
|
|
49
|
+
readonly label: string;
|
|
50
|
+
/** How long to sleep between two `done()` calls. A file read polls fast; a `gh` call does not. */
|
|
51
|
+
readonly pollMs: number;
|
|
52
|
+
/** True once the wait is over. Called before the first sleep, so an already-finished wait is free. */
|
|
53
|
+
done(): boolean;
|
|
54
|
+
/** The current state, in one short phrase, for the heartbeat: `2 of 4 verdicts in`. */
|
|
55
|
+
describe(): string;
|
|
56
|
+
}
|
|
57
|
+
/** How the wait ended: satisfied, or out of time. Data-only (a class, per CLAUDE.md). */
|
|
58
|
+
export declare class WaitOutcome {
|
|
59
|
+
done: boolean;
|
|
60
|
+
waitedMs: number;
|
|
61
|
+
constructor(done: boolean, waitedMs: number);
|
|
62
|
+
/** Whole seconds waited — what every message prints, so nothing formats it twice. */
|
|
63
|
+
get waitedSeconds(): number;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* The heartbeat's state: what the probe said on the PREVIOUS tick, so a tick that has not moved can say
|
|
67
|
+
* so. `still` is the load-bearing word, exactly as it is in `BuildLogHeartbeat`: a wait where nothing
|
|
68
|
+
* has happened for two minutes and a wait whose REPORTER has died look identical without it.
|
|
69
|
+
*/
|
|
70
|
+
export declare class WaitHeartbeat {
|
|
71
|
+
private readonly label;
|
|
72
|
+
private previous;
|
|
73
|
+
private nextDueMs;
|
|
74
|
+
constructor(label: string);
|
|
75
|
+
/** True when `elapsedMs` has passed the next heartbeat slot; consumes the slot when it has. */
|
|
76
|
+
isDue(elapsedMs: number): boolean;
|
|
77
|
+
/** One heartbeat line — `waiting on <label>: <state> (<n>s)`, plus ` still` when it has not moved. */
|
|
78
|
+
tick(state: string, elapsedMs: number): string;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* How often the wait proves it is alive. Hardcoded, like `wp-build`'s: the number that matters is the
|
|
82
|
+
* harness's 600-second silence watchdog, which no repo configures, so a knob here would only ever be a
|
|
83
|
+
* way to set it wrong.
|
|
84
|
+
*/
|
|
85
|
+
export declare const HEARTBEAT_MS = 20000;
|
|
86
|
+
/**
|
|
87
|
+
* When the wait gives up and asks to be re-run — comfortably under the harness's 600-second ceiling, so
|
|
88
|
+
* the command always returns its own answer rather than being killed holding one.
|
|
89
|
+
*/
|
|
90
|
+
export declare const CEILING_MS = 540000;
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.CEILING_MS = exports.HEARTBEAT_MS = exports.WaitHeartbeat = exports.WaitOutcome = exports.AwaitLoop = void 0;
|
|
4
|
+
const tslib_1 = require("tslib");
|
|
5
|
+
const inversify_1 = require("inversify");
|
|
6
|
+
const stage_output_log_1 = require("./stage-output-log");
|
|
7
|
+
/**
|
|
8
|
+
* THE BLOCKING WAIT, and why webpieces has to own one (issue #874).
|
|
9
|
+
*
|
|
10
|
+
* A worktree-isolated subagent has exactly one way to pause: a Bash call, which blocks until its
|
|
11
|
+
* command exits. It cannot end its turn — a subagent that stops making tool calls is FINISHED, and its
|
|
12
|
+
* run returns to its parent. `Monitor` does not block; its own result says "Keep working — do not poll
|
|
13
|
+
* or sleep". And a `Monitor` carrying a real polling loop is refused by the HARNESS, because a
|
|
14
|
+
* `while`/`until` with a redirect cannot be statically proven to stay inside the worktree (178 of 553
|
|
15
|
+
* measured subagent Monitor calls). That refusal is Claude Code's and is not ours to relax.
|
|
16
|
+
*
|
|
17
|
+
* So the agent falls back to `echo .` every three seconds — measured at 920M tokens, 18.3% of every
|
|
18
|
+
* token the fleet spent in the 24h to 2026-09-07, because every turn resends the whole conversation at
|
|
19
|
+
* ~557,000 tokens. This class is the thing that makes that unnecessary, and `wait-spin-guard` is the
|
|
20
|
+
* thing that makes it unused.
|
|
21
|
+
*
|
|
22
|
+
* ─── THREE CONSTRAINTS, all of them the harness's, none of them negotiable ─────────────────────────
|
|
23
|
+
*
|
|
24
|
+
* 1. **What the AGENT TYPES must be a plain command.** No loop, no redirect, no pipe, no command
|
|
25
|
+
* substitution — those are exactly what the isolation check refuses. All the looping is in here,
|
|
26
|
+
* inside one process, behind a bare `pnpm wp-await-reviews`.
|
|
27
|
+
* 2. **It must print something roughly every {@link HEARTBEAT_MS}.** The harness backgrounds and then
|
|
28
|
+
* KILLS a foreground command that has produced no output for 600 seconds, and a killed wait throws
|
|
29
|
+
* away the wait. Same defence `wp-build` already runs, same `still` word for the same reason: a
|
|
30
|
+
* tick that has not moved must be distinguishable from a stalled reporter.
|
|
31
|
+
* 3. **It must RETURN before that ceiling on its own**, saying to run it again. A wait that dies at
|
|
32
|
+
* the watchdog looks like a crash; a wait that returns at {@link CEILING_MS} with "still waiting"
|
|
33
|
+
* turns a 100-minute wait into ~11 tool calls instead of the ~2,000 an `echo .` loop costs.
|
|
34
|
+
*
|
|
35
|
+
* `done` is a normal, ZERO exit either way. "Still waiting" is not a failure — it is an answer the agent
|
|
36
|
+
* acts on — and making it non-zero would put a red exit code on the healthy path.
|
|
37
|
+
*/
|
|
38
|
+
let AwaitLoop = class AwaitLoop {
|
|
39
|
+
stageConsole;
|
|
40
|
+
constructor(stageConsole) {
|
|
41
|
+
this.stageConsole = stageConsole;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Block until `probe` says it is done, or until {@link CEILING_MS} elapses, printing a heartbeat
|
|
45
|
+
* throughout. Returns WHICH of those happened; the caller renders the outcome.
|
|
46
|
+
*/
|
|
47
|
+
async run(probe) {
|
|
48
|
+
const started = Date.now();
|
|
49
|
+
const heartbeat = new WaitHeartbeat(probe.label);
|
|
50
|
+
// `done()` FIRST, before anything is printed. It is what establishes the probe's state, so
|
|
51
|
+
// describing the wait before asking it would report a state nobody has measured — and an
|
|
52
|
+
// already-finished wait must cost nothing at all.
|
|
53
|
+
for (let first = true;; first = false) {
|
|
54
|
+
if (probe.done())
|
|
55
|
+
return new WaitOutcome(true, Date.now() - started);
|
|
56
|
+
const elapsed = Date.now() - started;
|
|
57
|
+
if (elapsed >= exports.CEILING_MS)
|
|
58
|
+
return new WaitOutcome(false, elapsed);
|
|
59
|
+
// One heartbeat per HEARTBEAT_MS of ELAPSED time, whatever the poll interval — a probe that
|
|
60
|
+
// costs a network round trip is polled far more slowly than one reading a file, and the
|
|
61
|
+
// watchdog counts seconds of silence, not polls.
|
|
62
|
+
if (first || heartbeat.isDue(elapsed)) {
|
|
63
|
+
this.stageConsole.say(`${heartbeat.tick(probe.describe(), elapsed)}\n`);
|
|
64
|
+
}
|
|
65
|
+
await this.sleep(probe.pollMs);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
sleep(ms) {
|
|
69
|
+
return new Promise((resolve) => {
|
|
70
|
+
setTimeout(resolve, ms);
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
exports.AwaitLoop = AwaitLoop;
|
|
75
|
+
exports.AwaitLoop = AwaitLoop = tslib_1.__decorate([
|
|
76
|
+
(0, inversify_1.injectable)(inversify_1.bindingScopeValues.Singleton),
|
|
77
|
+
tslib_1.__metadata("design:paramtypes", [stage_output_log_1.StageOutputLog])
|
|
78
|
+
], AwaitLoop);
|
|
79
|
+
/** How the wait ended: satisfied, or out of time. Data-only (a class, per CLAUDE.md). */
|
|
80
|
+
class WaitOutcome {
|
|
81
|
+
done;
|
|
82
|
+
waitedMs;
|
|
83
|
+
constructor(done, waitedMs) {
|
|
84
|
+
this.done = done;
|
|
85
|
+
this.waitedMs = waitedMs;
|
|
86
|
+
}
|
|
87
|
+
/** Whole seconds waited — what every message prints, so nothing formats it twice. */
|
|
88
|
+
get waitedSeconds() {
|
|
89
|
+
return Math.round(this.waitedMs / 1000);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
exports.WaitOutcome = WaitOutcome;
|
|
93
|
+
/**
|
|
94
|
+
* The heartbeat's state: what the probe said on the PREVIOUS tick, so a tick that has not moved can say
|
|
95
|
+
* so. `still` is the load-bearing word, exactly as it is in `BuildLogHeartbeat`: a wait where nothing
|
|
96
|
+
* has happened for two minutes and a wait whose REPORTER has died look identical without it.
|
|
97
|
+
*/
|
|
98
|
+
class WaitHeartbeat {
|
|
99
|
+
label;
|
|
100
|
+
previous = null;
|
|
101
|
+
nextDueMs = exports.HEARTBEAT_MS;
|
|
102
|
+
constructor(label) {
|
|
103
|
+
this.label = label;
|
|
104
|
+
}
|
|
105
|
+
/** True when `elapsedMs` has passed the next heartbeat slot; consumes the slot when it has. */
|
|
106
|
+
isDue(elapsedMs) {
|
|
107
|
+
if (elapsedMs < this.nextDueMs)
|
|
108
|
+
return false;
|
|
109
|
+
this.nextDueMs = elapsedMs + exports.HEARTBEAT_MS;
|
|
110
|
+
return true;
|
|
111
|
+
}
|
|
112
|
+
/** One heartbeat line — `waiting on <label>: <state> (<n>s)`, plus ` still` when it has not moved. */
|
|
113
|
+
tick(state, elapsedMs) {
|
|
114
|
+
const still = this.previous !== null && state === this.previous ? ' still' : '';
|
|
115
|
+
this.previous = state;
|
|
116
|
+
return `waiting on ${this.label}: ${state} (${String(Math.round(elapsedMs / 1000))}s)${still}`;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
exports.WaitHeartbeat = WaitHeartbeat;
|
|
120
|
+
/**
|
|
121
|
+
* How often the wait proves it is alive. Hardcoded, like `wp-build`'s: the number that matters is the
|
|
122
|
+
* harness's 600-second silence watchdog, which no repo configures, so a knob here would only ever be a
|
|
123
|
+
* way to set it wrong.
|
|
124
|
+
*/
|
|
125
|
+
exports.HEARTBEAT_MS = 20_000;
|
|
126
|
+
/**
|
|
127
|
+
* When the wait gives up and asks to be re-run — comfortably under the harness's 600-second ceiling, so
|
|
128
|
+
* the command always returns its own answer rather than being killed holding one.
|
|
129
|
+
*/
|
|
130
|
+
exports.CEILING_MS = 540_000;
|
|
131
|
+
//# sourceMappingURL=await-loop.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"await-loop.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/pr-gate/src/scripts/workflow/await-loop.ts"],"names":[],"mappings":"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","sourcesContent":["import { injectable, bindingScopeValues } from 'inversify';\n\nimport { StageOutputLog } from './stage-output-log';\n\n/**\n * THE BLOCKING WAIT, and why webpieces has to own one (issue #874).\n *\n * A worktree-isolated subagent has exactly one way to pause: a Bash call, which blocks until its\n * command exits. It cannot end its turn — a subagent that stops making tool calls is FINISHED, and its\n * run returns to its parent. `Monitor` does not block; its own result says \"Keep working — do not poll\n * or sleep\". And a `Monitor` carrying a real polling loop is refused by the HARNESS, because a\n * `while`/`until` with a redirect cannot be statically proven to stay inside the worktree (178 of 553\n * measured subagent Monitor calls). That refusal is Claude Code's and is not ours to relax.\n *\n * So the agent falls back to `echo .` every three seconds — measured at 920M tokens, 18.3% of every\n * token the fleet spent in the 24h to 2026-09-07, because every turn resends the whole conversation at\n * ~557,000 tokens. This class is the thing that makes that unnecessary, and `wait-spin-guard` is the\n * thing that makes it unused.\n *\n * ─── THREE CONSTRAINTS, all of them the harness's, none of them negotiable ─────────────────────────\n *\n * 1. **What the AGENT TYPES must be a plain command.** No loop, no redirect, no pipe, no command\n * substitution — those are exactly what the isolation check refuses. All the looping is in here,\n * inside one process, behind a bare `pnpm wp-await-reviews`.\n * 2. **It must print something roughly every {@link HEARTBEAT_MS}.** The harness backgrounds and then\n * KILLS a foreground command that has produced no output for 600 seconds, and a killed wait throws\n * away the wait. Same defence `wp-build` already runs, same `still` word for the same reason: a\n * tick that has not moved must be distinguishable from a stalled reporter.\n * 3. **It must RETURN before that ceiling on its own**, saying to run it again. A wait that dies at\n * the watchdog looks like a crash; a wait that returns at {@link CEILING_MS} with \"still waiting\"\n * turns a 100-minute wait into ~11 tool calls instead of the ~2,000 an `echo .` loop costs.\n *\n * `done` is a normal, ZERO exit either way. \"Still waiting\" is not a failure — it is an answer the agent\n * acts on — and making it non-zero would put a red exit code on the healthy path.\n */\n@injectable(bindingScopeValues.Singleton)\nexport class AwaitLoop {\n constructor(private readonly stageConsole: StageOutputLog) {}\n\n /**\n * Block until `probe` says it is done, or until {@link CEILING_MS} elapses, printing a heartbeat\n * throughout. Returns WHICH of those happened; the caller renders the outcome.\n */\n async run(probe: WaitProbe): Promise<WaitOutcome> {\n const started = Date.now();\n const heartbeat = new WaitHeartbeat(probe.label);\n // `done()` FIRST, before anything is printed. It is what establishes the probe's state, so\n // describing the wait before asking it would report a state nobody has measured — and an\n // already-finished wait must cost nothing at all.\n for (let first = true; ; first = false) {\n if (probe.done()) return new WaitOutcome(true, Date.now() - started);\n const elapsed = Date.now() - started;\n if (elapsed >= CEILING_MS) return new WaitOutcome(false, elapsed);\n // One heartbeat per HEARTBEAT_MS of ELAPSED time, whatever the poll interval — a probe that\n // costs a network round trip is polled far more slowly than one reading a file, and the\n // watchdog counts seconds of silence, not polls.\n if (first || heartbeat.isDue(elapsed)) {\n this.stageConsole.say(`${heartbeat.tick(probe.describe(), elapsed)}\\n`);\n }\n await this.sleep(probe.pollMs);\n }\n }\n\n private sleep(ms: number): Promise<void> {\n return new Promise<void>((resolve: () => void): void => {\n setTimeout(resolve, ms);\n });\n }\n}\n\n/**\n * WHAT is being waited for. An interface rather than a class because it is business logic — `done()`\n * reads verdict files for one waiter and asks GitHub for the other — and the loop must not know which.\n */\nexport interface WaitProbe {\n /** Names the wait in every heartbeat line, e.g. `reviewers` / `checks`. */\n readonly label: string;\n /** How long to sleep between two `done()` calls. A file read polls fast; a `gh` call does not. */\n readonly pollMs: number;\n /** True once the wait is over. Called before the first sleep, so an already-finished wait is free. */\n done(): boolean;\n /** The current state, in one short phrase, for the heartbeat: `2 of 4 verdicts in`. */\n describe(): string;\n}\n\n/** How the wait ended: satisfied, or out of time. Data-only (a class, per CLAUDE.md). */\nexport class WaitOutcome {\n done: boolean;\n waitedMs: number;\n\n constructor(done: boolean, waitedMs: number) {\n this.done = done;\n this.waitedMs = waitedMs;\n }\n\n /** Whole seconds waited — what every message prints, so nothing formats it twice. */\n get waitedSeconds(): number {\n return Math.round(this.waitedMs / 1000);\n }\n}\n\n/**\n * The heartbeat's state: what the probe said on the PREVIOUS tick, so a tick that has not moved can say\n * so. `still` is the load-bearing word, exactly as it is in `BuildLogHeartbeat`: a wait where nothing\n * has happened for two minutes and a wait whose REPORTER has died look identical without it.\n */\nexport class WaitHeartbeat {\n private previous: string | null = null;\n private nextDueMs = HEARTBEAT_MS;\n\n constructor(private readonly label: string) {}\n\n /** True when `elapsedMs` has passed the next heartbeat slot; consumes the slot when it has. */\n isDue(elapsedMs: number): boolean {\n if (elapsedMs < this.nextDueMs) return false;\n this.nextDueMs = elapsedMs + HEARTBEAT_MS;\n return true;\n }\n\n /** One heartbeat line — `waiting on <label>: <state> (<n>s)`, plus ` still` when it has not moved. */\n tick(state: string, elapsedMs: number): string {\n const still = this.previous !== null && state === this.previous ? ' still' : '';\n this.previous = state;\n return `waiting on ${this.label}: ${state} (${String(Math.round(elapsedMs / 1000))}s)${still}`;\n }\n}\n\n/**\n * How often the wait proves it is alive. Hardcoded, like `wp-build`'s: the number that matters is the\n * harness's 600-second silence watchdog, which no repo configures, so a knob here would only ever be a\n * way to set it wrong.\n */\nexport const HEARTBEAT_MS = 20_000;\n\n/**\n * When the wait gives up and asks to be re-run — comfortably under the harness's 600-second ceiling, so\n * the command always returns its own answer rather than being killed holding one.\n */\nexport const CEILING_MS = 540_000;\n"]}
|
|
@@ -47,6 +47,17 @@ export declare class FinishBanner {
|
|
|
47
47
|
private behindAsk;
|
|
48
48
|
private behindAskClose;
|
|
49
49
|
private doneNote;
|
|
50
|
+
/**
|
|
51
|
+
* The one OPTIONAL line about watching CI — offered, never instructed.
|
|
52
|
+
*
|
|
53
|
+
* "You can stop here" above stays the DEFAULT and is deliberately printed first: landing is the
|
|
54
|
+
* developer's call, and several of this repo's workflows stop at a green PR on purpose. This exists
|
|
55
|
+
* for the caller who has ALREADY decided to see it land, because the alternative they otherwise
|
|
56
|
+
* reach for is `gh pr checks --watch` in a loop or an `echo .` keep-alive — measured at 18.3% of
|
|
57
|
+
* every token the fleet spent in the 24h to 2026-09-07 (issue #874). Naming the blocking command is
|
|
58
|
+
* cheaper than the wait somebody was going to do anyway; it is not a reason to wait.
|
|
59
|
+
*/
|
|
60
|
+
private watchOffer;
|
|
50
61
|
/**
|
|
51
62
|
* The closing AI directive: the resolved PR's number/title/URL and an instruction to end the
|
|
52
63
|
* user-facing summary with EXACTLY `[#N title](url)` as the final line, nothing after it.
|
|
@@ -174,10 +174,12 @@ let FinishBanner = class FinishBanner {
|
|
|
174
174
|
// reading a queued PR does not go hunting for work that is not owed.
|
|
175
175
|
doneNote(merge) {
|
|
176
176
|
if (merge.result === pr_merger_1.MERGE_RESULT_AUTO_QUEUED) {
|
|
177
|
-
return ' Nothing else is owed: GitHub lands it when the checks go green. You can stop here.\n'
|
|
177
|
+
return ' Nothing else is owed: GitHub lands it when the checks go green. You can stop here.\n' +
|
|
178
|
+
this.watchOffer();
|
|
178
179
|
}
|
|
179
180
|
if (merge.result === pr_merger_1.MERGE_RESULT_LEFT_TO_HUMAN) {
|
|
180
181
|
return ' Nothing else is owed by the tooling — a person merges it. You can stop here.\n' +
|
|
182
|
+
this.watchOffer() +
|
|
181
183
|
'\n' +
|
|
182
184
|
' ℹ️ Clicking Merge in the GitHub UI is CORRECT here and produces the right history. The\n' +
|
|
183
185
|
' PR description IS the commit body this flow rendered — compact, non-green flags\n' +
|
|
@@ -189,6 +191,21 @@ let FinishBanner = class FinishBanner {
|
|
|
189
191
|
}
|
|
190
192
|
return '';
|
|
191
193
|
}
|
|
194
|
+
/**
|
|
195
|
+
* The one OPTIONAL line about watching CI — offered, never instructed.
|
|
196
|
+
*
|
|
197
|
+
* "You can stop here" above stays the DEFAULT and is deliberately printed first: landing is the
|
|
198
|
+
* developer's call, and several of this repo's workflows stop at a green PR on purpose. This exists
|
|
199
|
+
* for the caller who has ALREADY decided to see it land, because the alternative they otherwise
|
|
200
|
+
* reach for is `gh pr checks --watch` in a loop or an `echo .` keep-alive — measured at 18.3% of
|
|
201
|
+
* every token the fleet spent in the 24h to 2026-09-07 (issue #874). Naming the blocking command is
|
|
202
|
+
* cheaper than the wait somebody was going to do anyway; it is not a reason to wait.
|
|
203
|
+
*/
|
|
204
|
+
watchOffer() {
|
|
205
|
+
return ' (Optional — only if you have decided to watch it land, which nothing here asks you to\n' +
|
|
206
|
+
' do: `pnpm wp-await-checks --pr <n>` BLOCKS in one call until the checks stop running.\n' +
|
|
207
|
+
' Do not poll, and never `echo` to keep a turn alive — a turn costs your whole context.)\n';
|
|
208
|
+
}
|
|
192
209
|
/**
|
|
193
210
|
* The closing AI directive: the resolved PR's number/title/URL and an instruction to end the
|
|
194
211
|
* user-facing summary with EXACTLY `[#N title](url)` as the final line, nothing after it.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"finish-banner.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/pr-gate/src/scripts/workflow/finish-banner.ts"],"names":[],"mappings":"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","sourcesContent":["import { injectable, bindingScopeValues } from 'inversify';\nimport {\n MergeOutcome, MERGE_RESULT_MERGED, MERGE_RESULT_AUTO_QUEUED, MERGE_RESULT_LEFT_TO_HUMAN,\n MERGE_RESULT_BEHIND_CONFLICTING, MERGE_RESULT_BEHIND_UNKNOWN,\n} from './pr-merger';\n\nconst SEP = '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\\n';\n\n// Everything the closing block of wp-finish-upsert-pr needs to describe what actually happened. Both\n// `prNumber` and `prUrl` are '' when the PR could not be resolved (e.g. `gh pr create` failed).\nexport class FinishBannerInput {\n prNumber: string;\n prUrl: string;\n title: string;\n base: string;\n merge: MergeOutcome;\n\n // eslint-disable-next-line @typescript-eslint/max-params\n constructor(prNumber: string, prUrl: string, title: string, base: string, merge: MergeOutcome) {\n this.prNumber = prNumber;\n this.prUrl = prUrl;\n this.title = title;\n this.base = base;\n this.merge = merge;\n }\n}\n\n/**\n * Renders the closing block of `wp-finish-upsert-pr` — and it is the FRAME, not the merge message, that\n * this class exists to get right.\n *\n * PrMerger has long been honest in its `message`. The banner around it was not: it printed a hard-coded\n * `✅ PR finished` on every path, so a run whose merge failed still looked like a completed one at a\n * glance. The worst case is `mergeStateStatus: BEHIND` — unlike BLOCKED (waiting on checks), which\n * auto-merge resolves on its own, a BEHIND branch NEVER lands. Under a green checkmark, agents walked\n * away from stranded PRs. Three of them independently reported the output as untrustworthy.\n *\n * So the header is derived from `MergeOutcome.result`, and a not-done outcome is loud, distinct, and\n * carries the exact commands that fix it — including in the clickable-link directive, whose whole job\n * is to be the last thing the AI says.\n */\n@injectable(bindingScopeValues.Singleton)\nexport class FinishBanner {\n // The full closing recap: header keyed to the real outcome, the four things this command did, and —\n // when the PR is not done — what must happen next.\n render(input: FinishBannerInput): string {\n const prNum = input.prNumber;\n return '\\n' + SEP + this.header(input.merge) + SEP + '\\n' +\n ` 1. validated the build gate (authoritative)\\n` +\n ` 2. ${prNum ? `wrote the gated body to PR #${prNum}` : 'composed the gated PR body'} titled: \"${input.title}\"\\n` +\n ` 3. force-pushed your work to origin/${input.base} (after the body, so CI reads the right token)\\n` +\n ` 4. ${input.merge.message}\\n` +\n ` You are on ${input.base} — same name as the remote branch and the PR head.\\n` +\n this.whatIsOwed(input) + '\\n';\n }\n\n // TRUE only for the outcomes where nothing further is owed: merged, queued behind green-able checks,\n // or deliberately left for a human. Everything else — BEHIND, config mismatch, gh failure, no PR —\n // is unfinished work, and the banner must not decorate it with a checkmark.\n isDone(merge: MergeOutcome): boolean {\n return merge.result === MERGE_RESULT_MERGED\n || merge.result === MERGE_RESULT_AUTO_QUEUED\n || merge.result === MERGE_RESULT_LEFT_TO_HUMAN;\n }\n\n private header(merge: MergeOutcome): string {\n if (merge.result === MERGE_RESULT_MERGED) return '✅ PR finished AND MERGED — here is exactly what I did\\n';\n if (merge.result === MERGE_RESULT_AUTO_QUEUED) {\n return '✅ PR finished — auto-merge is ON; it lands itself when the checks pass\\n';\n }\n if (merge.result === MERGE_RESULT_LEFT_TO_HUMAN) {\n return '✅ PR finished — posted for a human to merge (that is this repo\\'s policy)\\n';\n }\n // NOT \"PR NOT FINISHED\". Everything this command owns SUCCEEDED — the branch is pushed, the body\n // is written, the build gate is green. What happened is that another author landed on main\n // between our fetch and our push. Reporting that as the author's failure sent agents off\n // re-auditing their own diff and hunting build flakes for a race they did not cause.\n if (merge.isBehind()) return this.behindHeader(merge);\n return '⚠️ PR NOT FINISHED — the PR is up, but the merge did NOT happen\\n';\n }\n\n private behindHeader(merge: MergeOutcome): string {\n if (merge.result === MERGE_RESULT_BEHIND_CONFLICTING) {\n return '⏸️ PR IS UP AND GREEN — someone landed on main first, and it CONFLICTS with your work\\n';\n }\n if (merge.result === MERGE_RESULT_BEHIND_UNKNOWN) {\n return '⏸️ PR IS UP AND GREEN — GitHub has not finished computing mergeability yet\\n';\n }\n return '⏸️ PR IS UP AND GREEN — someone landed on main first (no conflicts with your work)\\n';\n }\n\n // The follow-up block. '' for a done outcome — a finished run should not invent chores.\n private whatIsOwed(input: FinishBannerInput): string {\n if (this.isDone(input.merge)) return this.doneNote(input.merge);\n if (input.merge.isBehind()) return this.behindRemedy(input);\n return '\\n' + SEP +\n ' ⚠️ DO NOT report this PR as done. The merge failed for the reason in step 4 above.\\n' +\n ' Fix that, then re-run: pnpm wp-finish-upsert-pr\\n' +\n ` Confirm for yourself: gh pr view ${input.prNumber === '' ? '<n>' : input.prNumber} --json mergeable,mergeStateStatus,state\\n`;\n }\n\n /**\n * The BEHIND follow-up. Two rules govern every word here.\n *\n * FIRST: the remedy is the FULL ①②③, never a shortcut. `gh pr update-branch` looks like the obvious\n * one-command fix and is a trap — it rewrites the REMOTE branch while every fork-point consumer\n * (`ForkPoint.resolveForkPoint`, `nx affected --base=$(git merge-base ...)`, the review diff) computes\n * against the LOCAL HEAD. That splits reality in two: stage ③ force-pushes local over the remote and\n * silently reverts it, the recorded hash points describe a tree that is no longer the PR head, and the\n * rebased tree never passes a build gate. Only ① moves the fork point AND records it; only ② rebuilds\n * and re-receipts against the new one.\n *\n * SECOND: it ASKS, it does not order. An imperative command list is what turns an agent into a loop —\n * it complies, main moves again, it complies again. Asking forces a stop at a human, which is the only\n * thing that reliably terminates a race we cannot win by retrying.\n */\n private behindRemedy(input: FinishBannerInput): string {\n const num = input.prNumber === '' ? '<n>' : input.prNumber;\n return '\\n' + SEP + this.behindSituation(input.merge) + '\\n' +\n ' ⚠️ STOP HERE AND ASK THE HUMAN. Do NOT run these yourself — if main keeps moving,\\n' +\n ' running them on your own is an infinite loop with a full build inside it.\\n\\n' +\n ' Ask, in your own words:\\n' +\n ` \"${this.behindAsk(input.merge)}\\n` +\n ` ${this.behindAskClose(input.merge)}\"\\n\\n` +\n ' Only once they say yes:\\n\\n' +\n ' pnpm wp-start-upsert-pr # 3-point merge from main — re-forks onto the new main\\n' +\n ' pnpm wp-review-upsert-pr # re-validates the merge + REBUILDS on the new fork point\\n' +\n ' pnpm wp-finish-upsert-pr # gated body + merge, now up to date\\n\\n' +\n ' Do NOT skip ②. It is the only stage that validates the new merge and rebuilds against\\n' +\n ' the new fork point; skipping it publishes a PR whose tree was never gated.\\n' +\n ' Do NOT reach for `gh pr update-branch`. It rewrites the REMOTE branch only, which stage\\n' +\n ' ③ then force-pushes over — and it lands a tree no build gate ever saw.\\n' +\n ` Verify independently, do not trust this banner:\\n` +\n ` gh pr view ${num} --json mergeable,mergeStateStatus,state\\n`;\n }\n\n // What is actually true right now, per kind. The CLEAN case gets the caveat that it may not even\n // matter: \"out of date\" only blocks a merge on repos that require branches be up to date.\n private behindSituation(merge: MergeOutcome): string {\n if (merge.result === MERGE_RESULT_BEHIND_CONFLICTING) {\n return ' Your PR is pushed, its body is written, and the build gate passed. Then someone\\n' +\n ' else landed on main, and their change CONFLICTS with yours — same lines. Real\\n' +\n ' resolution is owed, and if people keep landing ahead of you it genuinely repeats.\\n' +\n ' That is inherent to concurrent editing, not a bug and not something you did wrong.\\n';\n }\n if (merge.result === MERGE_RESULT_BEHIND_UNKNOWN) {\n return ' Your PR is pushed, its body is written, and the build gate passed. GitHub has not\\n' +\n ' finished computing mergeability yet (it is asynchronous, and we asked seconds after\\n' +\n ' the push), so we do NOT know whether this conflicts. Re-check before doing anything:\\n' +\n ' a few seconds later the answer is usually CLEAN and no work is owed at all.\\n';\n }\n return ' Your PR is pushed, its body is written, and the build gate passed. Someone else simply\\n' +\n ' landed on main first. There are NO conflicts — nobody touched your lines.\\n' +\n ' This may not even matter: \"out of date\" blocks a merge only on repos that REQUIRE\\n' +\n ' branches be up to date. If yours does not, this PR can merge as-is.\\n';\n }\n\n // The one sentence to put to the human, in their terms.\n private behindAsk(merge: MergeOutcome): string {\n if (merge.result === MERGE_RESULT_BEHIND_CONFLICTING) {\n return 'Someone beat me to landing on main and there are conflicts. We MUST run a\\n' +\n ' 3-point merge so I can resolve them properly.';\n }\n if (merge.result === MERGE_RESULT_BEHIND_UNKNOWN) {\n return 'Someone beat me to landing on main. GitHub has not said yet whether it\\n' +\n ' conflicts, so I have not touched anything.';\n }\n return 'Someone beat me to landing on main. There are no conflicts, so this is just\\n' +\n ' a re-sync — but it costs a full rebuild.';\n }\n\n // The actual question. UNKNOWN gets a DIFFERENT one: with mergeability still uncomputed, proposing a\n // full re-run is proposing work we cannot yet show is needed — a re-check is free and often ends it.\n private behindAskClose(merge: MergeOutcome): string {\n if (merge.result === MERGE_RESULT_BEHIND_UNKNOWN) {\n return 'Shall I re-check in a moment, or start the wp-*-upsert-pr process over?';\n }\n return 'May I start the wp-*-upsert-pr process over again?';\n }\n\n // A short, positive \"you are free to stop\" line for the two non-merged-but-fine outcomes, so an AI\n // reading a queued PR does not go hunting for work that is not owed.\n private doneNote(merge: MergeOutcome): string {\n if (merge.result === MERGE_RESULT_AUTO_QUEUED) {\n return ' Nothing else is owed: GitHub lands it when the checks go green. You can stop here.\\n';\n }\n if (merge.result === MERGE_RESULT_LEFT_TO_HUMAN) {\n return ' Nothing else is owed by the tooling — a person merges it. You can stop here.\\n' +\n '\\n' +\n ' ℹ️ Clicking Merge in the GitHub UI is CORRECT here and produces the right history. The\\n' +\n ' PR description IS the commit body this flow rendered — compact, non-green flags\\n' +\n ' only, with the PR link on top — and this flow keeps the two repo settings that copy\\n' +\n ' it into main pinned itself (squash_merge_commit_title=PR_TITLE,\\n' +\n ' squash_merge_commit_message=PR_BODY). There is nothing for you to configure.\\n' +\n ' (`pnpm wp-land-pr` lands the identical bytes from the CLI, and also archives the\\n' +\n ' pre-squash tip and reaps the worktree. Either route is fine.)\\n';\n }\n return '';\n }\n\n /**\n * The closing AI directive: the resolved PR's number/title/URL and an instruction to end the\n * user-facing summary with EXACTLY `[#N title](url)` as the final line, nothing after it.\n *\n * The link text CARRIES the outcome. Ending on a bare cheerful link is exactly how a stranded PR got\n * reported as finished, and the directive says the link must be last — so the truth has to live\n * inside the link itself, not merely above it.\n *\n * '' when the PR could not be resolved: better to say nothing than to point at a link that is not real.\n */\n linkDirective(input: FinishBannerInput): string {\n if (input.prNumber === '' || input.prUrl === '') return '';\n const label = this.isDone(input.merge) ? input.title : `${input.title} — ${this.notDoneTag(input.merge)}`;\n const preamble = this.isDone(input.merge)\n ? ''\n : `AI — this PR is NOT done (${this.notDoneTag(input.merge)}). Your summary MUST say so plainly.\\n` +\n (input.merge.isBehind()\n ? `MUST END BY ASKING the human for permission to re-run the flow, and MUST NOT run any of\\n` +\n `the commands above until they answer. Say plainly that nothing they did caused this —\\n` +\n `another author landed on main first — and that the PR itself is pushed and gate-green.\\n\\n`\n : `MUST include the remedy commands printed above. Do not describe this run as complete.\\n\\n`);\n return SEP + `🔗 PR #${input.prNumber}: ${input.title}\\n ${input.prUrl}\\n\\n` + SEP + preamble +\n `AI — the LAST line of your summary to the user MUST be EXACTLY this clickable link, with\\n` +\n `NOTHING printed after it (no sign-off, no next steps, no blank commentary):\\n\\n` +\n ` [#${input.prNumber} ${label}](${input.prUrl})\\n\\n`;\n }\n\n // The tag rides INSIDE the clickable link, which the directive above forces to be the last line the\n // AI prints — so it is the one piece of wording guaranteed to reach the user. It says who is waiting\n // on whom: for every BEHIND flavour the answer is \"a human\", not \"more automation\".\n private notDoneTag(merge: MergeOutcome): string {\n if (merge.result === MERGE_RESULT_BEHIND_CONFLICTING) return 'NOT MERGED — main moved and it conflicts, needs your OK to re-sync';\n if (merge.result === MERGE_RESULT_BEHIND_UNKNOWN) return 'NOT MERGED — main moved, GitHub still computing mergeability';\n if (merge.isBehind()) return 'NOT MERGED — main moved (no conflicts), needs your OK to re-sync';\n return 'NOT MERGED';\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"finish-banner.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/pr-gate/src/scripts/workflow/finish-banner.ts"],"names":[],"mappings":";;;;AAAA,yCAA2D;AAC3D,2CAGqB;AAErB,MAAM,GAAG,GAAG,0DAA0D,CAAC;AAEvE,qGAAqG;AACrG,gGAAgG;AAChG,MAAa,iBAAiB;IAC1B,QAAQ,CAAS;IACjB,KAAK,CAAS;IACd,KAAK,CAAS;IACd,IAAI,CAAS;IACb,KAAK,CAAe;IAEpB,yDAAyD;IACzD,YAAY,QAAgB,EAAE,KAAa,EAAE,KAAa,EAAE,IAAY,EAAE,KAAmB;QACzF,IAAI,CAAC,QAAQ,GAAG,QAAQ,CAAC;QACzB,IAAI,CAAC,KAAK,GAAG,KAAK,CAAC;QACnB,IAAI,CAAC,KAAK,GAAG,KAAK,CAAC;QACnB,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;QACjB,IAAI,CAAC,KAAK,GAAG,KAAK,CAAC;IACvB,CAAC;CACJ;AAfD,8CAeC;AAED;;;;;;;;;;;;;GAaG;AAEI,IAAM,YAAY,GAAlB,MAAM,YAAY;IACrB,oGAAoG;IACpG,mDAAmD;IACnD,MAAM,CAAC,KAAwB;QAC3B,MAAM,KAAK,GAAG,KAAK,CAAC,QAAQ,CAAC;QAC7B,OAAO,IAAI,GAAG,GAAG,GAAG,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,KAAK,CAAC,GAAG,GAAG,GAAG,IAAI;YACrD,kDAAkD;YAClD,SAAS,KAAK,CAAC,CAAC,CAAC,+BAA+B,KAAK,EAAE,CAAC,CAAC,CAAC,4BAA4B,aAAa,KAAK,CAAC,KAAK,KAAK;YACnH,0CAA0C,KAAK,CAAC,IAAI,kDAAkD;YACtG,SAAS,KAAK,CAAC,KAAK,CAAC,OAAO,IAAI;YAChC,kBAAkB,KAAK,CAAC,IAAI,uDAAuD;YACnF,IAAI,CAAC,UAAU,CAAC,KAAK,CAAC,GAAG,IAAI,CAAC;IACtC,CAAC;IAED,qGAAqG;IACrG,mGAAmG;IACnG,4EAA4E;IAC5E,MAAM,CAAC,KAAmB;QACtB,OAAO,KAAK,CAAC,MAAM,KAAK,+BAAmB;eACpC,KAAK,CAAC,MAAM,KAAK,oCAAwB;eACzC,KAAK,CAAC,MAAM,KAAK,sCAA0B,CAAC;IACvD,CAAC;IAEO,MAAM,CAAC,KAAmB;QAC9B,IAAI,KAAK,CAAC,MAAM,KAAK,+BAAmB;YAAE,OAAO,yDAAyD,CAAC;QAC3G,IAAI,KAAK,CAAC,MAAM,KAAK,oCAAwB,EAAE,CAAC;YAC5C,OAAO,0EAA0E,CAAC;QACtF,CAAC;QACD,IAAI,KAAK,CAAC,MAAM,KAAK,sCAA0B,EAAE,CAAC;YAC9C,OAAO,6EAA6E,CAAC;QACzF,CAAC;QACD,iGAAiG;QACjG,2FAA2F;QAC3F,yFAAyF;QACzF,qFAAqF;QACrF,IAAI,KAAK,CAAC,QAAQ,EAAE;YAAE,OAAO,IAAI,CAAC,YAAY,CAAC,KAAK,CAAC,CAAC;QACtD,OAAO,oEAAoE,CAAC;IAChF,CAAC;IAEO,YAAY,CAAC,KAAmB;QACpC,IAAI,KAAK,CAAC,MAAM,KAAK,2CAA+B,EAAE,CAAC;YACnD,OAAO,0FAA0F,CAAC;QACtG,CAAC;QACD,IAAI,KAAK,CAAC,MAAM,KAAK,uCAA2B,EAAE,CAAC;YAC/C,OAAO,+EAA+E,CAAC;QAC3F,CAAC;QACD,OAAO,uFAAuF,CAAC;IACnG,CAAC;IAED,wFAAwF;IAChF,UAAU,CAAC,KAAwB;QACvC,IAAI,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,KAAK,CAAC;YAAE,OAAO,IAAI,CAAC,QAAQ,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;QAChE,IAAI,KAAK,CAAC,KAAK,CAAC,QAAQ,EAAE;YAAE,OAAO,IAAI,CAAC,YAAY,CAAC,KAAK,CAAC,CAAC;QAC5D,OAAO,IAAI,GAAG,GAAG;YACb,0FAA0F;YAC1F,2DAA2D;YAC3D,wCAAwC,KAAK,CAAC,QAAQ,KAAK,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,QAAQ,4CAA4C,CAAC;IAC3I,CAAC;IAED;;;;;;;;;;;;;;OAcG;IACK,YAAY,CAAC,KAAwB;QACzC,MAAM,GAAG,GAAG,KAAK,CAAC,QAAQ,KAAK,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,QAAQ,CAAC;QAC3D,OAAO,IAAI,GAAG,GAAG,GAAG,IAAI,CAAC,eAAe,CAAC,KAAK,CAAC,KAAK,CAAC,GAAG,IAAI;YACxD,yFAAyF;YACzF,sFAAsF;YACtF,8BAA8B;YAC9B,WAAW,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,KAAK,CAAC,IAAI;YAC1C,WAAW,IAAI,CAAC,cAAc,CAAC,KAAK,CAAC,KAAK,CAAC,OAAO;YAClD,gCAAgC;YAChC,6FAA6F;YAC7F,gGAAgG;YAChG,6EAA6E;YAC7E,4FAA4F;YAC5F,iFAAiF;YACjF,8FAA8F;YAC9F,6EAA6E;YAC7E,sDAAsD;YACtD,qBAAqB,GAAG,4CAA4C,CAAC;IAC7E,CAAC;IAED,iGAAiG;IACjG,0FAA0F;IAClF,eAAe,CAAC,KAAmB;QACvC,IAAI,KAAK,CAAC,MAAM,KAAK,2CAA+B,EAAE,CAAC;YACnD,OAAO,sFAAsF;gBACzF,oFAAoF;gBACpF,wFAAwF;gBACxF,yFAAyF,CAAC;QAClG,CAAC;QACD,IAAI,KAAK,CAAC,MAAM,KAAK,uCAA2B,EAAE,CAAC;YAC/C,OAAO,wFAAwF;gBAC3F,0FAA0F;gBAC1F,2FAA2F;gBAC3F,kFAAkF,CAAC;QAC3F,CAAC;QACD,OAAO,6FAA6F;YAChG,gFAAgF;YAChF,wFAAwF;YACxF,0EAA0E,CAAC;IACnF,CAAC;IAED,wDAAwD;IAChD,SAAS,CAAC,KAAmB;QACjC,IAAI,KAAK,CAAC,MAAM,KAAK,2CAA+B,EAAE,CAAC;YACnD,OAAO,6EAA6E;gBAChF,uDAAuD,CAAC;QAChE,CAAC;QACD,IAAI,KAAK,CAAC,MAAM,KAAK,uCAA2B,EAAE,CAAC;YAC/C,OAAO,0EAA0E;gBAC7E,oDAAoD,CAAC;QAC7D,CAAC;QACD,OAAO,+EAA+E;YAClF,kDAAkD,CAAC;IAC3D,CAAC;IAED,qGAAqG;IACrG,qGAAqG;IAC7F,cAAc,CAAC,KAAmB;QACtC,IAAI,KAAK,CAAC,MAAM,KAAK,uCAA2B,EAAE,CAAC;YAC/C,OAAO,yEAAyE,CAAC;QACrF,CAAC;QACD,OAAO,oDAAoD,CAAC;IAChE,CAAC;IAED,mGAAmG;IACnG,qEAAqE;IAC7D,QAAQ,CAAC,KAAmB;QAChC,IAAI,KAAK,CAAC,MAAM,KAAK,oCAAwB,EAAE,CAAC;YAC5C,OAAO,yFAAyF;gBAC5F,IAAI,CAAC,UAAU,EAAE,CAAC;QAC1B,CAAC;QACD,IAAI,KAAK,CAAC,MAAM,KAAK,sCAA0B,EAAE,CAAC;YAC9C,OAAO,mFAAmF;gBACtF,IAAI,CAAC,UAAU,EAAE;gBACjB,IAAI;gBACJ,8FAA8F;gBAC9F,0FAA0F;gBAC1F,8FAA8F;gBAC9F,0EAA0E;gBAC1E,uFAAuF;gBACvF,2FAA2F;gBAC3F,wEAAwE,CAAC;QACjF,CAAC;QACD,OAAO,EAAE,CAAC;IACd,CAAC;IAED;;;;;;;;;OASG;IACK,UAAU;QACd,OAAO,4FAA4F;YAC/F,6FAA6F;YAC7F,8FAA8F,CAAC;IACvG,CAAC;IAED;;;;;;;;;OASG;IACH,aAAa,CAAC,KAAwB;QAClC,IAAI,KAAK,CAAC,QAAQ,KAAK,EAAE,IAAI,KAAK,CAAC,KAAK,KAAK,EAAE;YAAE,OAAO,EAAE,CAAC;QAC3D,MAAM,KAAK,GAAG,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC,GAAG,KAAK,CAAC,KAAK,MAAM,IAAI,CAAC,UAAU,CAAC,KAAK,CAAC,KAAK,CAAC,EAAE,CAAC;QAC1G,MAAM,QAAQ,GAAG,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,KAAK,CAAC;YACrC,CAAC,CAAC,EAAE;YACJ,CAAC,CAAC,6BAA6B,IAAI,CAAC,UAAU,CAAC,KAAK,CAAC,KAAK,CAAC,wCAAwC;gBACjG,CAAC,KAAK,CAAC,KAAK,CAAC,QAAQ,EAAE;oBACnB,CAAC,CAAC,2FAA2F;wBAC3F,yFAAyF;wBACzF,4FAA4F;oBAC9F,CAAC,CAAC,2FAA2F,CAAC,CAAC;QACzG,OAAO,GAAG,GAAG,UAAU,KAAK,CAAC,QAAQ,KAAK,KAAK,CAAC,KAAK,QAAQ,KAAK,CAAC,KAAK,MAAM,GAAG,GAAG,GAAG,QAAQ;YAC3F,4FAA4F;YAC5F,iFAAiF;YACjF,QAAQ,KAAK,CAAC,QAAQ,IAAI,KAAK,KAAK,KAAK,CAAC,KAAK,OAAO,CAAC;IAC/D,CAAC;IAED,oGAAoG;IACpG,qGAAqG;IACrG,oFAAoF;IAC5E,UAAU,CAAC,KAAmB;QAClC,IAAI,KAAK,CAAC,MAAM,KAAK,2CAA+B;YAAE,OAAO,oEAAoE,CAAC;QAClI,IAAI,KAAK,CAAC,MAAM,KAAK,uCAA2B;YAAE,OAAO,8DAA8D,CAAC;QACxH,IAAI,KAAK,CAAC,QAAQ,EAAE;YAAE,OAAO,kEAAkE,CAAC;QAChG,OAAO,YAAY,CAAC;IACxB,CAAC;CACJ,CAAA;AApNY,oCAAY;uBAAZ,YAAY;IADxB,IAAA,sBAAU,EAAC,8BAAkB,CAAC,SAAS,CAAC;GAC5B,YAAY,CAoNxB","sourcesContent":["import { injectable, bindingScopeValues } from 'inversify';\nimport {\n MergeOutcome, MERGE_RESULT_MERGED, MERGE_RESULT_AUTO_QUEUED, MERGE_RESULT_LEFT_TO_HUMAN,\n MERGE_RESULT_BEHIND_CONFLICTING, MERGE_RESULT_BEHIND_UNKNOWN,\n} from './pr-merger';\n\nconst SEP = '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\\n';\n\n// Everything the closing block of wp-finish-upsert-pr needs to describe what actually happened. Both\n// `prNumber` and `prUrl` are '' when the PR could not be resolved (e.g. `gh pr create` failed).\nexport class FinishBannerInput {\n prNumber: string;\n prUrl: string;\n title: string;\n base: string;\n merge: MergeOutcome;\n\n // eslint-disable-next-line @typescript-eslint/max-params\n constructor(prNumber: string, prUrl: string, title: string, base: string, merge: MergeOutcome) {\n this.prNumber = prNumber;\n this.prUrl = prUrl;\n this.title = title;\n this.base = base;\n this.merge = merge;\n }\n}\n\n/**\n * Renders the closing block of `wp-finish-upsert-pr` — and it is the FRAME, not the merge message, that\n * this class exists to get right.\n *\n * PrMerger has long been honest in its `message`. The banner around it was not: it printed a hard-coded\n * `✅ PR finished` on every path, so a run whose merge failed still looked like a completed one at a\n * glance. The worst case is `mergeStateStatus: BEHIND` — unlike BLOCKED (waiting on checks), which\n * auto-merge resolves on its own, a BEHIND branch NEVER lands. Under a green checkmark, agents walked\n * away from stranded PRs. Three of them independently reported the output as untrustworthy.\n *\n * So the header is derived from `MergeOutcome.result`, and a not-done outcome is loud, distinct, and\n * carries the exact commands that fix it — including in the clickable-link directive, whose whole job\n * is to be the last thing the AI says.\n */\n@injectable(bindingScopeValues.Singleton)\nexport class FinishBanner {\n // The full closing recap: header keyed to the real outcome, the four things this command did, and —\n // when the PR is not done — what must happen next.\n render(input: FinishBannerInput): string {\n const prNum = input.prNumber;\n return '\\n' + SEP + this.header(input.merge) + SEP + '\\n' +\n ` 1. validated the build gate (authoritative)\\n` +\n ` 2. ${prNum ? `wrote the gated body to PR #${prNum}` : 'composed the gated PR body'} titled: \"${input.title}\"\\n` +\n ` 3. force-pushed your work to origin/${input.base} (after the body, so CI reads the right token)\\n` +\n ` 4. ${input.merge.message}\\n` +\n ` You are on ${input.base} — same name as the remote branch and the PR head.\\n` +\n this.whatIsOwed(input) + '\\n';\n }\n\n // TRUE only for the outcomes where nothing further is owed: merged, queued behind green-able checks,\n // or deliberately left for a human. Everything else — BEHIND, config mismatch, gh failure, no PR —\n // is unfinished work, and the banner must not decorate it with a checkmark.\n isDone(merge: MergeOutcome): boolean {\n return merge.result === MERGE_RESULT_MERGED\n || merge.result === MERGE_RESULT_AUTO_QUEUED\n || merge.result === MERGE_RESULT_LEFT_TO_HUMAN;\n }\n\n private header(merge: MergeOutcome): string {\n if (merge.result === MERGE_RESULT_MERGED) return '✅ PR finished AND MERGED — here is exactly what I did\\n';\n if (merge.result === MERGE_RESULT_AUTO_QUEUED) {\n return '✅ PR finished — auto-merge is ON; it lands itself when the checks pass\\n';\n }\n if (merge.result === MERGE_RESULT_LEFT_TO_HUMAN) {\n return '✅ PR finished — posted for a human to merge (that is this repo\\'s policy)\\n';\n }\n // NOT \"PR NOT FINISHED\". Everything this command owns SUCCEEDED — the branch is pushed, the body\n // is written, the build gate is green. What happened is that another author landed on main\n // between our fetch and our push. Reporting that as the author's failure sent agents off\n // re-auditing their own diff and hunting build flakes for a race they did not cause.\n if (merge.isBehind()) return this.behindHeader(merge);\n return '⚠️ PR NOT FINISHED — the PR is up, but the merge did NOT happen\\n';\n }\n\n private behindHeader(merge: MergeOutcome): string {\n if (merge.result === MERGE_RESULT_BEHIND_CONFLICTING) {\n return '⏸️ PR IS UP AND GREEN — someone landed on main first, and it CONFLICTS with your work\\n';\n }\n if (merge.result === MERGE_RESULT_BEHIND_UNKNOWN) {\n return '⏸️ PR IS UP AND GREEN — GitHub has not finished computing mergeability yet\\n';\n }\n return '⏸️ PR IS UP AND GREEN — someone landed on main first (no conflicts with your work)\\n';\n }\n\n // The follow-up block. '' for a done outcome — a finished run should not invent chores.\n private whatIsOwed(input: FinishBannerInput): string {\n if (this.isDone(input.merge)) return this.doneNote(input.merge);\n if (input.merge.isBehind()) return this.behindRemedy(input);\n return '\\n' + SEP +\n ' ⚠️ DO NOT report this PR as done. The merge failed for the reason in step 4 above.\\n' +\n ' Fix that, then re-run: pnpm wp-finish-upsert-pr\\n' +\n ` Confirm for yourself: gh pr view ${input.prNumber === '' ? '<n>' : input.prNumber} --json mergeable,mergeStateStatus,state\\n`;\n }\n\n /**\n * The BEHIND follow-up. Two rules govern every word here.\n *\n * FIRST: the remedy is the FULL ①②③, never a shortcut. `gh pr update-branch` looks like the obvious\n * one-command fix and is a trap — it rewrites the REMOTE branch while every fork-point consumer\n * (`ForkPoint.resolveForkPoint`, `nx affected --base=$(git merge-base ...)`, the review diff) computes\n * against the LOCAL HEAD. That splits reality in two: stage ③ force-pushes local over the remote and\n * silently reverts it, the recorded hash points describe a tree that is no longer the PR head, and the\n * rebased tree never passes a build gate. Only ① moves the fork point AND records it; only ② rebuilds\n * and re-receipts against the new one.\n *\n * SECOND: it ASKS, it does not order. An imperative command list is what turns an agent into a loop —\n * it complies, main moves again, it complies again. Asking forces a stop at a human, which is the only\n * thing that reliably terminates a race we cannot win by retrying.\n */\n private behindRemedy(input: FinishBannerInput): string {\n const num = input.prNumber === '' ? '<n>' : input.prNumber;\n return '\\n' + SEP + this.behindSituation(input.merge) + '\\n' +\n ' ⚠️ STOP HERE AND ASK THE HUMAN. Do NOT run these yourself — if main keeps moving,\\n' +\n ' running them on your own is an infinite loop with a full build inside it.\\n\\n' +\n ' Ask, in your own words:\\n' +\n ` \"${this.behindAsk(input.merge)}\\n` +\n ` ${this.behindAskClose(input.merge)}\"\\n\\n` +\n ' Only once they say yes:\\n\\n' +\n ' pnpm wp-start-upsert-pr # 3-point merge from main — re-forks onto the new main\\n' +\n ' pnpm wp-review-upsert-pr # re-validates the merge + REBUILDS on the new fork point\\n' +\n ' pnpm wp-finish-upsert-pr # gated body + merge, now up to date\\n\\n' +\n ' Do NOT skip ②. It is the only stage that validates the new merge and rebuilds against\\n' +\n ' the new fork point; skipping it publishes a PR whose tree was never gated.\\n' +\n ' Do NOT reach for `gh pr update-branch`. It rewrites the REMOTE branch only, which stage\\n' +\n ' ③ then force-pushes over — and it lands a tree no build gate ever saw.\\n' +\n ` Verify independently, do not trust this banner:\\n` +\n ` gh pr view ${num} --json mergeable,mergeStateStatus,state\\n`;\n }\n\n // What is actually true right now, per kind. The CLEAN case gets the caveat that it may not even\n // matter: \"out of date\" only blocks a merge on repos that require branches be up to date.\n private behindSituation(merge: MergeOutcome): string {\n if (merge.result === MERGE_RESULT_BEHIND_CONFLICTING) {\n return ' Your PR is pushed, its body is written, and the build gate passed. Then someone\\n' +\n ' else landed on main, and their change CONFLICTS with yours — same lines. Real\\n' +\n ' resolution is owed, and if people keep landing ahead of you it genuinely repeats.\\n' +\n ' That is inherent to concurrent editing, not a bug and not something you did wrong.\\n';\n }\n if (merge.result === MERGE_RESULT_BEHIND_UNKNOWN) {\n return ' Your PR is pushed, its body is written, and the build gate passed. GitHub has not\\n' +\n ' finished computing mergeability yet (it is asynchronous, and we asked seconds after\\n' +\n ' the push), so we do NOT know whether this conflicts. Re-check before doing anything:\\n' +\n ' a few seconds later the answer is usually CLEAN and no work is owed at all.\\n';\n }\n return ' Your PR is pushed, its body is written, and the build gate passed. Someone else simply\\n' +\n ' landed on main first. There are NO conflicts — nobody touched your lines.\\n' +\n ' This may not even matter: \"out of date\" blocks a merge only on repos that REQUIRE\\n' +\n ' branches be up to date. If yours does not, this PR can merge as-is.\\n';\n }\n\n // The one sentence to put to the human, in their terms.\n private behindAsk(merge: MergeOutcome): string {\n if (merge.result === MERGE_RESULT_BEHIND_CONFLICTING) {\n return 'Someone beat me to landing on main and there are conflicts. We MUST run a\\n' +\n ' 3-point merge so I can resolve them properly.';\n }\n if (merge.result === MERGE_RESULT_BEHIND_UNKNOWN) {\n return 'Someone beat me to landing on main. GitHub has not said yet whether it\\n' +\n ' conflicts, so I have not touched anything.';\n }\n return 'Someone beat me to landing on main. There are no conflicts, so this is just\\n' +\n ' a re-sync — but it costs a full rebuild.';\n }\n\n // The actual question. UNKNOWN gets a DIFFERENT one: with mergeability still uncomputed, proposing a\n // full re-run is proposing work we cannot yet show is needed — a re-check is free and often ends it.\n private behindAskClose(merge: MergeOutcome): string {\n if (merge.result === MERGE_RESULT_BEHIND_UNKNOWN) {\n return 'Shall I re-check in a moment, or start the wp-*-upsert-pr process over?';\n }\n return 'May I start the wp-*-upsert-pr process over again?';\n }\n\n // A short, positive \"you are free to stop\" line for the two non-merged-but-fine outcomes, so an AI\n // reading a queued PR does not go hunting for work that is not owed.\n private doneNote(merge: MergeOutcome): string {\n if (merge.result === MERGE_RESULT_AUTO_QUEUED) {\n return ' Nothing else is owed: GitHub lands it when the checks go green. You can stop here.\\n' +\n this.watchOffer();\n }\n if (merge.result === MERGE_RESULT_LEFT_TO_HUMAN) {\n return ' Nothing else is owed by the tooling — a person merges it. You can stop here.\\n' +\n this.watchOffer() +\n '\\n' +\n ' ℹ️ Clicking Merge in the GitHub UI is CORRECT here and produces the right history. The\\n' +\n ' PR description IS the commit body this flow rendered — compact, non-green flags\\n' +\n ' only, with the PR link on top — and this flow keeps the two repo settings that copy\\n' +\n ' it into main pinned itself (squash_merge_commit_title=PR_TITLE,\\n' +\n ' squash_merge_commit_message=PR_BODY). There is nothing for you to configure.\\n' +\n ' (`pnpm wp-land-pr` lands the identical bytes from the CLI, and also archives the\\n' +\n ' pre-squash tip and reaps the worktree. Either route is fine.)\\n';\n }\n return '';\n }\n\n /**\n * The one OPTIONAL line about watching CI — offered, never instructed.\n *\n * \"You can stop here\" above stays the DEFAULT and is deliberately printed first: landing is the\n * developer's call, and several of this repo's workflows stop at a green PR on purpose. This exists\n * for the caller who has ALREADY decided to see it land, because the alternative they otherwise\n * reach for is `gh pr checks --watch` in a loop or an `echo .` keep-alive — measured at 18.3% of\n * every token the fleet spent in the 24h to 2026-09-07 (issue #874). Naming the blocking command is\n * cheaper than the wait somebody was going to do anyway; it is not a reason to wait.\n */\n private watchOffer(): string {\n return ' (Optional — only if you have decided to watch it land, which nothing here asks you to\\n' +\n ' do: `pnpm wp-await-checks --pr <n>` BLOCKS in one call until the checks stop running.\\n' +\n ' Do not poll, and never `echo` to keep a turn alive — a turn costs your whole context.)\\n';\n }\n\n /**\n * The closing AI directive: the resolved PR's number/title/URL and an instruction to end the\n * user-facing summary with EXACTLY `[#N title](url)` as the final line, nothing after it.\n *\n * The link text CARRIES the outcome. Ending on a bare cheerful link is exactly how a stranded PR got\n * reported as finished, and the directive says the link must be last — so the truth has to live\n * inside the link itself, not merely above it.\n *\n * '' when the PR could not be resolved: better to say nothing than to point at a link that is not real.\n */\n linkDirective(input: FinishBannerInput): string {\n if (input.prNumber === '' || input.prUrl === '') return '';\n const label = this.isDone(input.merge) ? input.title : `${input.title} — ${this.notDoneTag(input.merge)}`;\n const preamble = this.isDone(input.merge)\n ? ''\n : `AI — this PR is NOT done (${this.notDoneTag(input.merge)}). Your summary MUST say so plainly.\\n` +\n (input.merge.isBehind()\n ? `MUST END BY ASKING the human for permission to re-run the flow, and MUST NOT run any of\\n` +\n `the commands above until they answer. Say plainly that nothing they did caused this —\\n` +\n `another author landed on main first — and that the PR itself is pushed and gate-green.\\n\\n`\n : `MUST include the remedy commands printed above. Do not describe this run as complete.\\n\\n`);\n return SEP + `🔗 PR #${input.prNumber}: ${input.title}\\n ${input.prUrl}\\n\\n` + SEP + preamble +\n `AI — the LAST line of your summary to the user MUST be EXACTLY this clickable link, with\\n` +\n `NOTHING printed after it (no sign-off, no next steps, no blank commentary):\\n\\n` +\n ` [#${input.prNumber} ${label}](${input.prUrl})\\n\\n`;\n }\n\n // The tag rides INSIDE the clickable link, which the directive above forces to be the last line the\n // AI prints — so it is the one piece of wording guaranteed to reach the user. It says who is waiting\n // on whom: for every BEHIND flavour the answer is \"a human\", not \"more automation\".\n private notDoneTag(merge: MergeOutcome): string {\n if (merge.result === MERGE_RESULT_BEHIND_CONFLICTING) return 'NOT MERGED — main moved and it conflicts, needs your OK to re-sync';\n if (merge.result === MERGE_RESULT_BEHIND_UNKNOWN) return 'NOT MERGED — main moved, GitHub still computing mergeability';\n if (merge.isBehind()) return 'NOT MERGED — main moved (no conflicts), needs your OK to re-sync';\n return 'NOT MERGED';\n }\n}\n"]}
|