@webpieces/pr-gate 0.4.497 → 0.4.499
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 +3 -3
- package/src/dashboard/dashboard.d.ts +15 -0
- package/src/dashboard/dashboard.js +25 -2
- package/src/dashboard/dashboard.js.map +1 -1
- package/src/scripts/commands/cleanup-command.d.ts +42 -3
- package/src/scripts/commands/cleanup-command.js +69 -11
- package/src/scripts/commands/cleanup-command.js.map +1 -1
- package/src/scripts/commands/finish-upsert-pr-command.d.ts +55 -5
- package/src/scripts/commands/finish-upsert-pr-command.js +162 -30
- package/src/scripts/commands/finish-upsert-pr-command.js.map +1 -1
- package/src/scripts/commands/land-pr-command.d.ts +17 -2
- package/src/scripts/commands/land-pr-command.js +32 -3
- package/src/scripts/commands/land-pr-command.js.map +1 -1
- package/src/scripts/commands/review-upsert-pr-command.d.ts +88 -0
- package/src/scripts/commands/review-upsert-pr-command.js +244 -0
- package/src/scripts/commands/review-upsert-pr-command.js.map +1 -0
- package/src/scripts/commands/start-upsert-pr-command.d.ts +10 -15
- package/src/scripts/commands/start-upsert-pr-command.js +41 -47
- package/src/scripts/commands/start-upsert-pr-command.js.map +1 -1
- package/src/scripts/commands/worktree-cleanup.d.ts +45 -0
- package/src/scripts/commands/worktree-cleanup.js +130 -0
- package/src/scripts/commands/worktree-cleanup.js.map +1 -0
- package/src/scripts/pr-gate-app.d.ts +9 -5
- package/src/scripts/pr-gate-app.js +12 -8
- package/src/scripts/pr-gate-app.js.map +1 -1
- package/src/scripts/workflow/checklist-scanner.d.ts +27 -11
- package/src/scripts/workflow/checklist-scanner.js +37 -15
- package/src/scripts/workflow/checklist-scanner.js.map +1 -1
- package/src/scripts/workflow/diff-basis.d.ts +53 -0
- package/src/scripts/workflow/diff-basis.js +113 -0
- package/src/scripts/workflow/diff-basis.js.map +1 -0
- package/src/scripts/workflow/diff-materializer.d.ts +85 -0
- package/src/scripts/workflow/diff-materializer.js +253 -0
- package/src/scripts/workflow/diff-materializer.js.map +1 -0
- package/src/scripts/workflow/git-findForkPoint.d.ts +1 -1
- package/src/scripts/workflow/git-findForkPoint.js +1 -1
- package/src/scripts/workflow/git-findForkPoint.js.map +1 -1
- package/src/scripts/workflow/pr-context-writer.d.ts +16 -10
- package/src/scripts/workflow/pr-context-writer.js +20 -14
- package/src/scripts/workflow/pr-context-writer.js.map +1 -1
- package/src/scripts/workflow/review-stage-receipt.d.ts +36 -0
- package/src/scripts/workflow/review-stage-receipt.js +82 -0
- package/src/scripts/workflow/review-stage-receipt.js.map +1 -0
- package/src/scripts/workflow/reviewer-briefing-builder.d.ts +36 -0
- package/src/scripts/workflow/reviewer-briefing-builder.js +104 -0
- package/src/scripts/workflow/reviewer-briefing-builder.js.map +1 -0
- package/src/scripts/{wp-checklist.js → wp-review-upsert-pr.js} +3 -3
- package/src/scripts/wp-review-upsert-pr.js.map +1 -0
- package/src/scripts/commands/checklist-command.d.ts +0 -27
- package/src/scripts/commands/checklist-command.js +0 -97
- package/src/scripts/commands/checklist-command.js.map +0 -1
- package/src/scripts/wp-checklist.js.map +0 -1
- /package/src/scripts/{wp-checklist.d.ts → wp-review-upsert-pr.d.ts} +0 -0
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ReviewUpsertPrCommand = void 0;
|
|
4
|
+
const tslib_1 = require("tslib");
|
|
5
|
+
const fs = tslib_1.__importStar(require("fs"));
|
|
6
|
+
const path = tslib_1.__importStar(require("path"));
|
|
7
|
+
const rules_config_1 = require("@webpieces/rules-config");
|
|
8
|
+
const inversify_1 = require("inversify");
|
|
9
|
+
const git_readAiBranchName_1 = require("../workflow/git-readAiBranchName");
|
|
10
|
+
const build_affected_1 = require("../workflow/build-affected");
|
|
11
|
+
const checklist_notice_1 = require("../workflow/checklist-notice");
|
|
12
|
+
const checklist_scanner_1 = require("../workflow/checklist-scanner");
|
|
13
|
+
const diff_materializer_1 = require("../workflow/diff-materializer");
|
|
14
|
+
const git_exec_1 = require("../workflow/git-exec");
|
|
15
|
+
const merge_start_1 = require("../workflow/merge-start");
|
|
16
|
+
const merge_end_1 = require("../workflow/merge-end");
|
|
17
|
+
const merge_state_1 = require("../workflow/merge-state");
|
|
18
|
+
const reviewer_briefing_builder_1 = require("../workflow/reviewer-briefing-builder");
|
|
19
|
+
const review_stage_receipt_1 = require("../workflow/review-stage-receipt");
|
|
20
|
+
const SEP = '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n';
|
|
21
|
+
/**
|
|
22
|
+
* `wp-review-upsert-pr` — STAGE ② of the three-stage PR flow, between `wp-start-upsert-pr` (sync from main)
|
|
23
|
+
* and `wp-finish-upsert-pr` (post the PR).
|
|
24
|
+
*
|
|
25
|
+
* It exists because reviewing came before verifying. Under the old two-command flow the 3-point merge was
|
|
26
|
+
* finalized and the build gate ran inside `wp-finish-upsert-pr` — i.e. AFTER the reviewers had already run.
|
|
27
|
+
* A reviewer could therefore spend a full review on an unresolved merge, or on code that does not compile,
|
|
28
|
+
* and only afterwards would the branch be checked. This command puts the verification first:
|
|
29
|
+
*
|
|
30
|
+
* 1. finalize + VALIDATE any in-progress 3-point merge (moved here from finish)
|
|
31
|
+
* 2. assert a clean tree (meaningful now — step 1 committed the resolution)
|
|
32
|
+
* 3. run the build gate (on the merged, committed tree)
|
|
33
|
+
* 4. scan the checklists, EXTRACT the diff, BRIEF the reviewers
|
|
34
|
+
* 5. write the stage receipt, then print what to spawn + the review.json schema
|
|
35
|
+
*
|
|
36
|
+
* Unlike the report-only command it replaces — which always exited 0, because reporting is not gating — this
|
|
37
|
+
* command CAN FAIL, at steps 1, 2 and 3. That is the point: it fails BEFORE any reviewer is spawned, so a
|
|
38
|
+
* branch with an unresolved merge or a broken build costs zero reviewer tokens.
|
|
39
|
+
*
|
|
40
|
+
* The ordering of 1 → 2 → 3 is load-bearing. MergeEnd stages and commits the conflict resolution, so the
|
|
41
|
+
* tree is clean by the time the build runs; building first would either build a dirty tree (proving nothing
|
|
42
|
+
* about what ships) or refuse a tree that is legitimately mid-resolution.
|
|
43
|
+
*/
|
|
44
|
+
let ReviewUpsertPrCommand = class ReviewUpsertPrCommand {
|
|
45
|
+
repoRootFinder;
|
|
46
|
+
aiBranchName;
|
|
47
|
+
gitExec;
|
|
48
|
+
buildAffected;
|
|
49
|
+
mergeState;
|
|
50
|
+
mergeEnd;
|
|
51
|
+
checklistScanner;
|
|
52
|
+
checklistNotice;
|
|
53
|
+
instructions;
|
|
54
|
+
materializer;
|
|
55
|
+
briefingBuilder;
|
|
56
|
+
reviewerInstructions;
|
|
57
|
+
receipts;
|
|
58
|
+
// eslint-disable-next-line @typescript-eslint/max-params
|
|
59
|
+
constructor(repoRootFinder, aiBranchName, gitExec, buildAffected, mergeState, mergeEnd, checklistScanner, checklistNotice, instructions, materializer, briefingBuilder, reviewerInstructions, receipts) {
|
|
60
|
+
this.repoRootFinder = repoRootFinder;
|
|
61
|
+
this.aiBranchName = aiBranchName;
|
|
62
|
+
this.gitExec = gitExec;
|
|
63
|
+
this.buildAffected = buildAffected;
|
|
64
|
+
this.mergeState = mergeState;
|
|
65
|
+
this.mergeEnd = mergeEnd;
|
|
66
|
+
this.checklistScanner = checklistScanner;
|
|
67
|
+
this.checklistNotice = checklistNotice;
|
|
68
|
+
this.instructions = instructions;
|
|
69
|
+
this.materializer = materializer;
|
|
70
|
+
this.briefingBuilder = briefingBuilder;
|
|
71
|
+
this.reviewerInstructions = reviewerInstructions;
|
|
72
|
+
this.receipts = receipts;
|
|
73
|
+
}
|
|
74
|
+
async run() {
|
|
75
|
+
const repoRoot = this.repoRootFinder.resolveRepoRoot(process.cwd());
|
|
76
|
+
(0, rules_config_1.writeTemplate)(repoRoot, 'webpieces.git-workflow.md');
|
|
77
|
+
(0, rules_config_1.writeTemplate)(repoRoot, 'webpieces.review-checklists.md');
|
|
78
|
+
const featureName = this.aiBranchName.getFeatureName();
|
|
79
|
+
const config = (0, rules_config_1.loadAndValidate)(repoRoot).prGate;
|
|
80
|
+
const mergeValidated = await this.finalizeAnyInProgressMerge(repoRoot);
|
|
81
|
+
this.gitExec.assertCleanTree(repoRoot);
|
|
82
|
+
const buildPassedAt = this.runBuildGate(repoRoot);
|
|
83
|
+
const scan = this.checklistScanner.scan(repoRoot, config.checklists, new checklist_scanner_1.ChecklistScanOptions(false));
|
|
84
|
+
const briefings = this.briefReviewers(repoRoot, featureName, scan, config);
|
|
85
|
+
this.receipts.write(repoRoot, featureName, new review_stage_receipt_1.ReviewStageReceipt(scan.basis.headSha, mergeValidated, this.buildAffected.resolveBuildCommand(repoRoot), buildPassedAt, briefings.map((b) => b.subagent)));
|
|
86
|
+
this.report(repoRoot, featureName, scan, briefings);
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Validate + commit + finalize a conflict resolution, if one is in flight. MOVED here from
|
|
90
|
+
* `wp-finish-upsert-pr` so that exactly one command owns merge finalization — two owners is the drift
|
|
91
|
+
* failure `PrContextWriter`'s own docstring records having already happened once in this codebase.
|
|
92
|
+
*
|
|
93
|
+
* MergeEnd's three checks are what "did the AI do the 3-point merge right?" means concretely: no
|
|
94
|
+
* conflict markers left in the scoped files, `git diff --diff-filter=U` empty, and a non-empty
|
|
95
|
+
* merge-explanation.md beside every conflicted file.
|
|
96
|
+
*
|
|
97
|
+
* Returns true when a merge was finalized OR there was none to finalize; it throws rather than returning
|
|
98
|
+
* false when validation fails, so the receipt can never record an unvalidated merge as validated.
|
|
99
|
+
*/
|
|
100
|
+
async finalizeAnyInProgressMerge(repoRoot) {
|
|
101
|
+
const home = this.mergeState.mergeDirFor(repoRoot, this.aiBranchName.getFeatureName());
|
|
102
|
+
const activeDir = this.mergeState.findActiveMergeRunDir(home);
|
|
103
|
+
const marker = activeDir ? this.mergeState.readMergeMarker(activeDir) : null;
|
|
104
|
+
if (!activeDir || !marker || marker.validated)
|
|
105
|
+
return true;
|
|
106
|
+
process.stdout.write('\n' + SEP + '① Validating your 3-point merge\n' + SEP + '\n');
|
|
107
|
+
// pushRemote:false — finish still owns the single push.
|
|
108
|
+
await this.mergeEnd.mergeEnd(repoRoot, 'wp-review-upsert-pr', activeDir, new merge_start_1.MergeContext(marker.currentBranch, marker.squashBranch, marker.backupBranch, marker.prNumber), new merge_end_1.MergeEndOptions(marker.conflictedFiles, false));
|
|
109
|
+
return true;
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* The build gate, run BEFORE any reviewer is briefed. Returns the ISO time it passed, for the receipt —
|
|
113
|
+
* which is what lets `wp-finish-upsert-pr` skip its own gate when HEAD has not moved, so moving the gate
|
|
114
|
+
* earlier did not turn one build into two.
|
|
115
|
+
*/
|
|
116
|
+
runBuildGate(repoRoot) {
|
|
117
|
+
this.buildAffected.runBuildGate(repoRoot, new build_affected_1.BuildGateOptions('🛠️ Build gate (pre-review)', 'pnpm wp-review-upsert-pr', 'Build failed — NO reviewer was briefed and no diff was extracted. Fix it, then re-run.'));
|
|
118
|
+
return new Date().toISOString();
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Extract the diff once, then write one instructions file per reviewer.
|
|
122
|
+
*
|
|
123
|
+
* The changed-file set and the basis both come off the SCAN rather than being recomputed, so the diff a
|
|
124
|
+
* reviewer reads covers exactly the files its checklist matched against.
|
|
125
|
+
*/
|
|
126
|
+
briefReviewers(repoRoot, featureName, scan, config) {
|
|
127
|
+
if (scan.basis.unresolved)
|
|
128
|
+
return [];
|
|
129
|
+
const manifest = this.materializer.materialize(repoRoot, featureName, scan.basis, scan.changedFiles, config.reviewDiffExclude);
|
|
130
|
+
const diffDir = this.materializer.diffDirFor(repoRoot, featureName);
|
|
131
|
+
// Re-point pr-context.json at the extracted diff now that there is one — the scan wrote it before
|
|
132
|
+
// materialization, so without this the reviewers' own context file would not mention the diff dir.
|
|
133
|
+
this.rewritePrContext(repoRoot, featureName, diffDir);
|
|
134
|
+
const briefings = this.briefingBuilder.build(repoRoot, scan, manifest, diffDir, config);
|
|
135
|
+
const dir = this.reviewerInstructions.instructionsDirFor(repoRoot, featureName);
|
|
136
|
+
fs.rmSync(dir, { recursive: true, force: true }); // stale instructions read as current are worse than none
|
|
137
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
138
|
+
for (const b of briefings) {
|
|
139
|
+
fs.writeFileSync(this.reviewerInstructions.pathFor(repoRoot, featureName, b.subagent), this.reviewerInstructions.render(b));
|
|
140
|
+
}
|
|
141
|
+
this.printDiffSummary(manifest, diffDir);
|
|
142
|
+
return briefings;
|
|
143
|
+
}
|
|
144
|
+
// pr-context.json is written by the scan (before the diff exists). Patch in the dir rather than
|
|
145
|
+
// re-deriving the whole context: re-deriving means a second basis resolution, which is the drift.
|
|
146
|
+
rewritePrContext(repoRoot, featureName, diffDir) {
|
|
147
|
+
const p = path.join(repoRoot, '.webpieces', 'pr-review', featureName, 'pr-context.json');
|
|
148
|
+
if (!fs.existsSync(p))
|
|
149
|
+
return;
|
|
150
|
+
// webpieces-disable no-unmanaged-exceptions -- chokepoint: an unreadable context is left as-is, never fatal
|
|
151
|
+
// eslint-disable-next-line @webpieces/no-unmanaged-exceptions
|
|
152
|
+
try {
|
|
153
|
+
// webpieces-disable no-any-unknown -- opaque parsed JSON, one key added
|
|
154
|
+
const raw = JSON.parse(fs.readFileSync(p, 'utf8'));
|
|
155
|
+
raw['diffDir'] = diffDir;
|
|
156
|
+
fs.writeFileSync(p, JSON.stringify(raw, null, 2) + '\n');
|
|
157
|
+
}
|
|
158
|
+
catch (err) {
|
|
159
|
+
const error = (0, rules_config_1.toError)(err);
|
|
160
|
+
void error; // leave the file as-is — the instructions files carry the same paths directly
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
printDiffSummary(manifest, diffDir) {
|
|
164
|
+
process.stdout.write('\n' + SEP + '② Diff extracted for the reviewers\n' + SEP + '\n');
|
|
165
|
+
process.stdout.write(` ${manifest.entries.length} file(s) → ${diffDir}\n`);
|
|
166
|
+
if (manifest.excluded.length > 0) {
|
|
167
|
+
process.stdout.write(` ${manifest.excluded.length} excluded by pr-gate.reviewDiffExclude (stubbed, still matched)\n`);
|
|
168
|
+
}
|
|
169
|
+
const truncated = manifest.entries.filter((e) => e.truncated).length;
|
|
170
|
+
if (truncated > 0)
|
|
171
|
+
process.stdout.write(` ${truncated} truncated at the per-file cap (each says so in its footer)\n`);
|
|
172
|
+
}
|
|
173
|
+
/** What the AI must do next: spawn each reviewer, then write review.json. */
|
|
174
|
+
report(repoRoot, featureName, scan, briefings) {
|
|
175
|
+
process.stdout.write('\n' + SEP + '③ Review, then finish\n' + SEP + '\n');
|
|
176
|
+
if (scan.applicable.length === 0) {
|
|
177
|
+
process.stdout.write(this.checklistNotice.build(scan.defined.length, 'wp-finish-upsert-pr'));
|
|
178
|
+
}
|
|
179
|
+
else {
|
|
180
|
+
process.stdout.write(this.spawnBlocks(repoRoot, featureName, scan, briefings));
|
|
181
|
+
}
|
|
182
|
+
process.stdout.write(`\nThen review your own changes and\n${(0, rules_config_1.reviewJsonSchemaHint)(scan.reviewPath)}\n\n` +
|
|
183
|
+
`Finally run: pnpm wp-finish-upsert-pr\n` +
|
|
184
|
+
`(The build gate is already green for this commit — finish reuses it unless HEAD moves.)\n\n`);
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* One copy-paste block per owed reviewer. The prompt is deliberately a POINTER and nothing else: the
|
|
188
|
+
* generated instructions file is the contract, so anything restated here is a second copy that can go
|
|
189
|
+
* stale — which is exactly how a removed `success` field outlived its own removal in print.
|
|
190
|
+
*/
|
|
191
|
+
// eslint-disable-next-line @typescript-eslint/max-params
|
|
192
|
+
spawnBlocks(repoRoot, featureName, scan, briefings) {
|
|
193
|
+
const reviewedIds = new Set(scan.reviewed.map((r) => r.id));
|
|
194
|
+
const owed = briefings.filter((b) => !reviewedIds.has(b.checklistId));
|
|
195
|
+
const lines = [];
|
|
196
|
+
for (const r of scan.reviewed) {
|
|
197
|
+
lines.push(` ✓ ${r.subagent} — already reviewed on this branch (reusing its review-${r.id}.json)`);
|
|
198
|
+
}
|
|
199
|
+
// A verdict file that EXISTS but is unreadable as a verdict is called out here. Without it this
|
|
200
|
+
// reports the checklist as simply owed, and the AI re-runs a reviewer that already ran instead of
|
|
201
|
+
// correcting the file sitting right there.
|
|
202
|
+
for (const e of scan.formatErrors)
|
|
203
|
+
lines.push(` ⛔ ${e}`);
|
|
204
|
+
if (owed.length === 0) {
|
|
205
|
+
lines.push('', '✅ Every checklist that applies is already reviewed — nothing to spawn.');
|
|
206
|
+
return lines.join('\n') + '\n';
|
|
207
|
+
}
|
|
208
|
+
lines.push('', `Spawn these ${owed.length} reviewer subagent(s) — a SEPARATE one each. You may NOT review your own`, 'work, and you may NOT write a reviewer\'s verdict file on its behalf.', '');
|
|
209
|
+
for (const b of owed) {
|
|
210
|
+
lines.push(` ▶ ${b.subagent} — ${this.why(b)}`);
|
|
211
|
+
lines.push(` subagent_type: ${b.subagent}`);
|
|
212
|
+
lines.push(' prompt: Read your instructions file FIRST and follow it exactly:');
|
|
213
|
+
lines.push(` ${this.reviewerInstructions.pathFor(repoRoot, featureName, b.subagent)}`);
|
|
214
|
+
lines.push('');
|
|
215
|
+
}
|
|
216
|
+
return lines.join('\n');
|
|
217
|
+
}
|
|
218
|
+
// Why this one is in scope. A patternless checklist is NOT "matched" — it always runs, over the whole
|
|
219
|
+
// diff, and saying so is what tells a repo its checklist is firing on docs-only PRs by design.
|
|
220
|
+
why(b) {
|
|
221
|
+
if (b.matchedPatterns.length === 0) {
|
|
222
|
+
return `ALWAYS RUNS (no "patterns" configured), whole diff in scope — ${b.myFiles.length} file(s)`;
|
|
223
|
+
}
|
|
224
|
+
return `${b.myFiles.length} file(s) matched ${b.matchedPatterns.map((p) => `"${p}"`).join(', ')}`;
|
|
225
|
+
}
|
|
226
|
+
};
|
|
227
|
+
exports.ReviewUpsertPrCommand = ReviewUpsertPrCommand;
|
|
228
|
+
exports.ReviewUpsertPrCommand = ReviewUpsertPrCommand = tslib_1.__decorate([
|
|
229
|
+
(0, inversify_1.injectable)(inversify_1.bindingScopeValues.Singleton),
|
|
230
|
+
tslib_1.__metadata("design:paramtypes", [rules_config_1.RepoRootFinder,
|
|
231
|
+
git_readAiBranchName_1.AiBranchName,
|
|
232
|
+
git_exec_1.GitExec,
|
|
233
|
+
build_affected_1.BuildAffected,
|
|
234
|
+
merge_state_1.MergeState,
|
|
235
|
+
merge_end_1.MergeEnd,
|
|
236
|
+
checklist_scanner_1.ChecklistScanner,
|
|
237
|
+
checklist_notice_1.ChecklistNotice,
|
|
238
|
+
rules_config_1.ChecklistInstructionsService,
|
|
239
|
+
diff_materializer_1.DiffMaterializer,
|
|
240
|
+
reviewer_briefing_builder_1.ReviewerBriefingBuilder,
|
|
241
|
+
rules_config_1.ReviewerInstructionsService,
|
|
242
|
+
review_stage_receipt_1.ReviewStageReceiptService])
|
|
243
|
+
], ReviewUpsertPrCommand);
|
|
244
|
+
//# sourceMappingURL=review-upsert-pr-command.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"review-upsert-pr-command.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/pr-gate/src/scripts/commands/review-upsert-pr-command.ts"],"names":[],"mappings":"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","sourcesContent":["import * as fs from 'fs';\nimport * as path from 'path';\nimport {\n loadAndValidate, reviewJsonSchemaHint, writeTemplate, PrGateConfig, RepoRootFinder,\n ChecklistInstructionsService, RequiredChecklist, ReviewerBriefing, ReviewerInstructionsService, toError,\n} from '@webpieces/rules-config';\nimport { injectable, bindingScopeValues } from 'inversify';\nimport { AiBranchName } from '../workflow/git-readAiBranchName';\nimport { BuildAffected, BuildGateOptions } from '../workflow/build-affected';\nimport { ChecklistNotice } from '../workflow/checklist-notice';\nimport { ChecklistScan, ChecklistScanOptions, ChecklistScanner } from '../workflow/checklist-scanner';\nimport { DiffManifest, DiffManifestEntry, DiffMaterializer } from '../workflow/diff-materializer';\nimport { GitExec } from '../workflow/git-exec';\nimport { MergeContext } from '../workflow/merge-start';\nimport { MergeEnd, MergeEndOptions } from '../workflow/merge-end';\nimport { MergeState } from '../workflow/merge-state';\nimport { ReviewerBriefingBuilder } from '../workflow/reviewer-briefing-builder';\nimport { ReviewStageReceipt, ReviewStageReceiptService } from '../workflow/review-stage-receipt';\n\nconst SEP = '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\\n';\n\n/**\n * `wp-review-upsert-pr` — STAGE ② of the three-stage PR flow, between `wp-start-upsert-pr` (sync from main)\n * and `wp-finish-upsert-pr` (post the PR).\n *\n * It exists because reviewing came before verifying. Under the old two-command flow the 3-point merge was\n * finalized and the build gate ran inside `wp-finish-upsert-pr` — i.e. AFTER the reviewers had already run.\n * A reviewer could therefore spend a full review on an unresolved merge, or on code that does not compile,\n * and only afterwards would the branch be checked. This command puts the verification first:\n *\n * 1. finalize + VALIDATE any in-progress 3-point merge (moved here from finish)\n * 2. assert a clean tree (meaningful now — step 1 committed the resolution)\n * 3. run the build gate (on the merged, committed tree)\n * 4. scan the checklists, EXTRACT the diff, BRIEF the reviewers\n * 5. write the stage receipt, then print what to spawn + the review.json schema\n *\n * Unlike the report-only command it replaces — which always exited 0, because reporting is not gating — this\n * command CAN FAIL, at steps 1, 2 and 3. That is the point: it fails BEFORE any reviewer is spawned, so a\n * branch with an unresolved merge or a broken build costs zero reviewer tokens.\n *\n * The ordering of 1 → 2 → 3 is load-bearing. MergeEnd stages and commits the conflict resolution, so the\n * tree is clean by the time the build runs; building first would either build a dirty tree (proving nothing\n * about what ships) or refuse a tree that is legitimately mid-resolution.\n */\n@injectable(bindingScopeValues.Singleton)\nexport class ReviewUpsertPrCommand {\n // eslint-disable-next-line @typescript-eslint/max-params\n constructor(\n private readonly repoRootFinder: RepoRootFinder,\n private readonly aiBranchName: AiBranchName,\n private readonly gitExec: GitExec,\n private readonly buildAffected: BuildAffected,\n private readonly mergeState: MergeState,\n private readonly mergeEnd: MergeEnd,\n private readonly checklistScanner: ChecklistScanner,\n private readonly checklistNotice: ChecklistNotice,\n private readonly instructions: ChecklistInstructionsService,\n private readonly materializer: DiffMaterializer,\n private readonly briefingBuilder: ReviewerBriefingBuilder,\n private readonly reviewerInstructions: ReviewerInstructionsService,\n private readonly receipts: ReviewStageReceiptService,\n ) {}\n\n async run(): Promise<void> {\n const repoRoot = this.repoRootFinder.resolveRepoRoot(process.cwd());\n writeTemplate(repoRoot, 'webpieces.git-workflow.md');\n writeTemplate(repoRoot, 'webpieces.review-checklists.md');\n const featureName = this.aiBranchName.getFeatureName();\n const config = loadAndValidate(repoRoot).prGate;\n\n const mergeValidated = await this.finalizeAnyInProgressMerge(repoRoot);\n this.gitExec.assertCleanTree(repoRoot);\n const buildPassedAt = this.runBuildGate(repoRoot);\n\n const scan = this.checklistScanner.scan(repoRoot, config.checklists, new ChecklistScanOptions(false));\n const briefings = this.briefReviewers(repoRoot, featureName, scan, config);\n this.receipts.write(repoRoot, featureName, new ReviewStageReceipt(\n scan.basis.headSha, mergeValidated, this.buildAffected.resolveBuildCommand(repoRoot), buildPassedAt,\n briefings.map((b: ReviewerBriefing): string => b.subagent),\n ));\n this.report(repoRoot, featureName, scan, briefings);\n }\n\n /**\n * Validate + commit + finalize a conflict resolution, if one is in flight. MOVED here from\n * `wp-finish-upsert-pr` so that exactly one command owns merge finalization — two owners is the drift\n * failure `PrContextWriter`'s own docstring records having already happened once in this codebase.\n *\n * MergeEnd's three checks are what \"did the AI do the 3-point merge right?\" means concretely: no\n * conflict markers left in the scoped files, `git diff --diff-filter=U` empty, and a non-empty\n * merge-explanation.md beside every conflicted file.\n *\n * Returns true when a merge was finalized OR there was none to finalize; it throws rather than returning\n * false when validation fails, so the receipt can never record an unvalidated merge as validated.\n */\n private async finalizeAnyInProgressMerge(repoRoot: string): Promise<boolean> {\n const home = this.mergeState.mergeDirFor(repoRoot, this.aiBranchName.getFeatureName());\n const activeDir = this.mergeState.findActiveMergeRunDir(home);\n const marker = activeDir ? this.mergeState.readMergeMarker(activeDir) : null;\n if (!activeDir || !marker || marker.validated) return true;\n process.stdout.write('\\n' + SEP + '① Validating your 3-point merge\\n' + SEP + '\\n');\n // pushRemote:false — finish still owns the single push.\n await this.mergeEnd.mergeEnd(\n repoRoot, 'wp-review-upsert-pr', activeDir,\n new MergeContext(marker.currentBranch, marker.squashBranch, marker.backupBranch, marker.prNumber),\n new MergeEndOptions(marker.conflictedFiles, false),\n );\n return true;\n }\n\n /**\n * The build gate, run BEFORE any reviewer is briefed. Returns the ISO time it passed, for the receipt —\n * which is what lets `wp-finish-upsert-pr` skip its own gate when HEAD has not moved, so moving the gate\n * earlier did not turn one build into two.\n */\n private runBuildGate(repoRoot: string): string {\n this.buildAffected.runBuildGate(repoRoot, new BuildGateOptions(\n '🛠️ Build gate (pre-review)',\n 'pnpm wp-review-upsert-pr',\n 'Build failed — NO reviewer was briefed and no diff was extracted. Fix it, then re-run.',\n ));\n return new Date().toISOString();\n }\n\n /**\n * Extract the diff once, then write one instructions file per reviewer.\n *\n * The changed-file set and the basis both come off the SCAN rather than being recomputed, so the diff a\n * reviewer reads covers exactly the files its checklist matched against.\n */\n private briefReviewers(repoRoot: string, featureName: string, scan: ChecklistScan, config: PrGateConfig): ReviewerBriefing[] {\n if (scan.basis.unresolved) return [];\n const manifest = this.materializer.materialize(\n repoRoot, featureName, scan.basis, scan.changedFiles, config.reviewDiffExclude);\n const diffDir = this.materializer.diffDirFor(repoRoot, featureName);\n // Re-point pr-context.json at the extracted diff now that there is one — the scan wrote it before\n // materialization, so without this the reviewers' own context file would not mention the diff dir.\n this.rewritePrContext(repoRoot, featureName, diffDir);\n const briefings = this.briefingBuilder.build(repoRoot, scan, manifest, diffDir, config);\n const dir = this.reviewerInstructions.instructionsDirFor(repoRoot, featureName);\n fs.rmSync(dir, { recursive: true, force: true }); // stale instructions read as current are worse than none\n fs.mkdirSync(dir, { recursive: true });\n for (const b of briefings) {\n fs.writeFileSync(this.reviewerInstructions.pathFor(repoRoot, featureName, b.subagent),\n this.reviewerInstructions.render(b));\n }\n this.printDiffSummary(manifest, diffDir);\n return briefings;\n }\n\n // pr-context.json is written by the scan (before the diff exists). Patch in the dir rather than\n // re-deriving the whole context: re-deriving means a second basis resolution, which is the drift.\n private rewritePrContext(repoRoot: string, featureName: string, diffDir: string): void {\n const p = path.join(repoRoot, '.webpieces', 'pr-review', featureName, 'pr-context.json');\n if (!fs.existsSync(p)) return;\n // webpieces-disable no-unmanaged-exceptions -- chokepoint: an unreadable context is left as-is, never fatal\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n // webpieces-disable no-any-unknown -- opaque parsed JSON, one key added\n const raw = JSON.parse(fs.readFileSync(p, 'utf8')) as Record<string, unknown>;\n raw['diffDir'] = diffDir;\n fs.writeFileSync(p, JSON.stringify(raw, null, 2) + '\\n');\n } catch (err: unknown) {\n const error = toError(err);\n void error; // leave the file as-is — the instructions files carry the same paths directly\n }\n }\n\n private printDiffSummary(manifest: DiffManifest, diffDir: string): void {\n process.stdout.write('\\n' + SEP + '② Diff extracted for the reviewers\\n' + SEP + '\\n');\n process.stdout.write(` ${manifest.entries.length} file(s) → ${diffDir}\\n`);\n if (manifest.excluded.length > 0) {\n process.stdout.write(` ${manifest.excluded.length} excluded by pr-gate.reviewDiffExclude (stubbed, still matched)\\n`);\n }\n const truncated = manifest.entries.filter((e: DiffManifestEntry): boolean => e.truncated).length;\n if (truncated > 0) process.stdout.write(` ${truncated} truncated at the per-file cap (each says so in its footer)\\n`);\n }\n\n /** What the AI must do next: spawn each reviewer, then write review.json. */\n private report(repoRoot: string, featureName: string, scan: ChecklistScan, briefings: readonly ReviewerBriefing[]): void {\n process.stdout.write('\\n' + SEP + '③ Review, then finish\\n' + SEP + '\\n');\n if (scan.applicable.length === 0) {\n process.stdout.write(this.checklistNotice.build(scan.defined.length, 'wp-finish-upsert-pr'));\n } else {\n process.stdout.write(this.spawnBlocks(repoRoot, featureName, scan, briefings));\n }\n process.stdout.write(\n `\\nThen review your own changes and\\n${reviewJsonSchemaHint(scan.reviewPath)}\\n\\n` +\n `Finally run: pnpm wp-finish-upsert-pr\\n` +\n `(The build gate is already green for this commit — finish reuses it unless HEAD moves.)\\n\\n`);\n }\n\n /**\n * One copy-paste block per owed reviewer. The prompt is deliberately a POINTER and nothing else: the\n * generated instructions file is the contract, so anything restated here is a second copy that can go\n * stale — which is exactly how a removed `success` field outlived its own removal in print.\n */\n // eslint-disable-next-line @typescript-eslint/max-params\n private spawnBlocks(repoRoot: string, featureName: string, scan: ChecklistScan, briefings: readonly ReviewerBriefing[]): string {\n const reviewedIds = new Set(scan.reviewed.map((r: RequiredChecklist): string => r.id));\n const owed = briefings.filter((b: ReviewerBriefing): boolean => !reviewedIds.has(b.checklistId));\n const lines: string[] = [];\n for (const r of scan.reviewed) {\n lines.push(` ✓ ${r.subagent} — already reviewed on this branch (reusing its review-${r.id}.json)`);\n }\n // A verdict file that EXISTS but is unreadable as a verdict is called out here. Without it this\n // reports the checklist as simply owed, and the AI re-runs a reviewer that already ran instead of\n // correcting the file sitting right there.\n for (const e of scan.formatErrors) lines.push(` ⛔ ${e}`);\n if (owed.length === 0) {\n lines.push('', '✅ Every checklist that applies is already reviewed — nothing to spawn.');\n return lines.join('\\n') + '\\n';\n }\n lines.push('',\n `Spawn these ${owed.length} reviewer subagent(s) — a SEPARATE one each. You may NOT review your own`,\n 'work, and you may NOT write a reviewer\\'s verdict file on its behalf.',\n '');\n for (const b of owed) {\n lines.push(` ▶ ${b.subagent} — ${this.why(b)}`);\n lines.push(` subagent_type: ${b.subagent}`);\n lines.push(' prompt: Read your instructions file FIRST and follow it exactly:');\n lines.push(` ${this.reviewerInstructions.pathFor(repoRoot, featureName, b.subagent)}`);\n lines.push('');\n }\n return lines.join('\\n');\n }\n\n // Why this one is in scope. A patternless checklist is NOT \"matched\" — it always runs, over the whole\n // diff, and saying so is what tells a repo its checklist is firing on docs-only PRs by design.\n private why(b: ReviewerBriefing): string {\n if (b.matchedPatterns.length === 0) {\n return `ALWAYS RUNS (no \"patterns\" configured), whole diff in scope — ${b.myFiles.length} file(s)`;\n }\n return `${b.myFiles.length} file(s) matched ${b.matchedPatterns.map((p: string): string => `\"${p}\"`).join(', ')}`;\n }\n}\n"]}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { RepoRootFinder } from '@webpieces/rules-config';
|
|
2
2
|
import { AiBranchName } from '../workflow/git-readAiBranchName';
|
|
3
3
|
import { BranchNaming } from '../workflow/branch-naming';
|
|
4
|
+
import { DiffBasisResolver } from '../workflow/diff-basis';
|
|
4
5
|
import { GitExec } from '../workflow/git-exec';
|
|
5
|
-
import { ForkPoint } from '../workflow/git-findForkPoint';
|
|
6
6
|
import { PrContextWriter } from '../workflow/pr-context-writer';
|
|
7
7
|
import { RunUpdate } from '../workflow/run-update';
|
|
8
8
|
export declare class StartUpsertPrCommand {
|
|
@@ -11,26 +11,21 @@ export declare class StartUpsertPrCommand {
|
|
|
11
11
|
private readonly branchNaming;
|
|
12
12
|
private readonly gitExec;
|
|
13
13
|
private readonly runUpdate;
|
|
14
|
-
private readonly
|
|
14
|
+
private readonly diffBasisResolver;
|
|
15
15
|
private readonly prContextWriter;
|
|
16
|
-
constructor(repoRootFinder: RepoRootFinder, aiBranchName: AiBranchName, branchNaming: BranchNaming, gitExec: GitExec, runUpdate: RunUpdate,
|
|
16
|
+
constructor(repoRootFinder: RepoRootFinder, aiBranchName: AiBranchName, branchNaming: BranchNaming, gitExec: GitExec, runUpdate: RunUpdate, diffBasisResolver: DiffBasisResolver, prContextWriter: PrContextWriter);
|
|
17
17
|
run(): Promise<void>;
|
|
18
18
|
/**
|
|
19
|
-
* Hand the AI its next step:
|
|
19
|
+
* Hand the AI its ONE next step: `wp-review-upsert-pr`.
|
|
20
20
|
*
|
|
21
|
-
* This
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
* the
|
|
25
|
-
*
|
|
21
|
+
* This prints exactly one command on purpose. It used to print three (run the checklist, write
|
|
22
|
+
* review.json, then finish), and a three-item list is a list with a step to skip. Stage ② now owns all
|
|
23
|
+
* of it — it validates the 3-point merge, builds, materializes the diff, briefs the reviewers, AND
|
|
24
|
+
* prints the review.json schema — so there is nothing here to enumerate. Notably review.json is NOT
|
|
25
|
+
* requested here any more: asking for a written review before the branch is known to compile invites
|
|
26
|
+
* one that describes code that does not build.
|
|
26
27
|
*/
|
|
27
28
|
private handOffToReview;
|
|
28
|
-
/**
|
|
29
|
-
* Step 1 of the review: find out what review this diff owes. A repo with checklists gets pointed at
|
|
30
|
-
* `wp-checklist`; a repo with none is told plainly that it has none, because printing "run wp-checklist"
|
|
31
|
-
* forever at a repo that will never have one is noise, and silence reads as "a checklist passed".
|
|
32
|
-
*/
|
|
33
|
-
private checklistPointer;
|
|
34
29
|
private contextLines;
|
|
35
30
|
private scaffoldCiWorkflow;
|
|
36
31
|
private updateBranchFromMain;
|
|
@@ -6,32 +6,37 @@ const rules_config_1 = require("@webpieces/rules-config");
|
|
|
6
6
|
const inversify_1 = require("inversify");
|
|
7
7
|
const git_readAiBranchName_1 = require("../workflow/git-readAiBranchName");
|
|
8
8
|
const branch_naming_1 = require("../workflow/branch-naming");
|
|
9
|
+
const diff_basis_1 = require("../workflow/diff-basis");
|
|
9
10
|
const git_exec_1 = require("../workflow/git-exec");
|
|
10
|
-
const git_findForkPoint_1 = require("../workflow/git-findForkPoint");
|
|
11
11
|
const pr_context_writer_1 = require("../workflow/pr-context-writer");
|
|
12
12
|
const run_update_1 = require("../workflow/run-update");
|
|
13
13
|
const SEP = '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n';
|
|
14
|
-
//
|
|
15
|
-
//
|
|
16
|
-
//
|
|
17
|
-
// build gate is
|
|
18
|
-
//
|
|
19
|
-
//
|
|
14
|
+
// STAGE ① of the AI-first PR flow: the deterministic setup — update from main / drive the 3-point merge —
|
|
15
|
+
// then hand the AI ONE next command, `wp-review-upsert-pr`.
|
|
16
|
+
//
|
|
17
|
+
// It runs NO build gate, and that is still deliberate, but the reason has changed. It is not that the build
|
|
18
|
+
// belongs only at the end: it is that at THIS point a conflicted 3-point merge may still be unresolved, so
|
|
19
|
+
// there is nothing worth building yet. The gate moved FORWARD, not away — stage ② finalizes the merge and
|
|
20
|
+
// then builds, so reviewers never judge a branch that does not compile, and wp-finish-upsert-pr skips the
|
|
21
|
+
// rebuild when HEAD has not moved since. `pr-gate.buildCommand` is therefore no longer a finish-only knob.
|
|
22
|
+
//
|
|
23
|
+
// This command NEVER creates/updates a PR and NEVER pushes: all `gh` posting and the ONE push live in
|
|
24
|
+
// finish, behind review.json + the checklists + the build gate.
|
|
20
25
|
let StartUpsertPrCommand = class StartUpsertPrCommand {
|
|
21
26
|
repoRootFinder;
|
|
22
27
|
aiBranchName;
|
|
23
28
|
branchNaming;
|
|
24
29
|
gitExec;
|
|
25
30
|
runUpdate;
|
|
26
|
-
|
|
31
|
+
diffBasisResolver;
|
|
27
32
|
prContextWriter;
|
|
28
|
-
constructor(repoRootFinder, aiBranchName, branchNaming, gitExec, runUpdate,
|
|
33
|
+
constructor(repoRootFinder, aiBranchName, branchNaming, gitExec, runUpdate, diffBasisResolver, prContextWriter) {
|
|
29
34
|
this.repoRootFinder = repoRootFinder;
|
|
30
35
|
this.aiBranchName = aiBranchName;
|
|
31
36
|
this.branchNaming = branchNaming;
|
|
32
37
|
this.gitExec = gitExec;
|
|
33
38
|
this.runUpdate = runUpdate;
|
|
34
|
-
this.
|
|
39
|
+
this.diffBasisResolver = diffBasisResolver;
|
|
35
40
|
this.prContextWriter = prContextWriter;
|
|
36
41
|
}
|
|
37
42
|
async run() {
|
|
@@ -51,54 +56,41 @@ let StartUpsertPrCommand = class StartUpsertPrCommand {
|
|
|
51
56
|
// every BLOCK checklist, and the authoritative build gate — so no unreviewed commit reaches the
|
|
52
57
|
// remote, and there is no early `synchronize` firing against a PR body with a stale gate token.
|
|
53
58
|
await this.updateBranchFromMain(repoRoot);
|
|
54
|
-
// No build gate here —
|
|
55
|
-
//
|
|
59
|
+
// No build gate here — a conflicted merge may still be unresolved, so there is nothing worth
|
|
60
|
+
// building. Stage ② finalizes the merge FIRST, then builds; see the class comment.
|
|
56
61
|
this.handOffToReview(repoRoot);
|
|
57
62
|
}
|
|
58
63
|
/**
|
|
59
|
-
* Hand the AI its next step:
|
|
64
|
+
* Hand the AI its ONE next step: `wp-review-upsert-pr`.
|
|
60
65
|
*
|
|
61
|
-
* This
|
|
62
|
-
*
|
|
63
|
-
*
|
|
64
|
-
* the
|
|
65
|
-
*
|
|
66
|
+
* This prints exactly one command on purpose. It used to print three (run the checklist, write
|
|
67
|
+
* review.json, then finish), and a three-item list is a list with a step to skip. Stage ② now owns all
|
|
68
|
+
* of it — it validates the 3-point merge, builds, materializes the diff, briefs the reviewers, AND
|
|
69
|
+
* prints the review.json schema — so there is nothing here to enumerate. Notably review.json is NOT
|
|
70
|
+
* requested here any more: asking for a written review before the branch is known to compile invites
|
|
71
|
+
* one that describes code that does not build.
|
|
66
72
|
*/
|
|
67
73
|
handOffToReview(repoRoot) {
|
|
68
|
-
const reviewPath = (0, rules_config_1.reviewJsonPath)(repoRoot, this.aiBranchName.getFeatureName());
|
|
69
74
|
// Persist the review-format + process instructions where any failure message can cite them.
|
|
70
75
|
(0, rules_config_1.writeTemplate)(repoRoot, 'webpieces.review-checklists.md');
|
|
71
|
-
// Persist the PR diff context (base sha + the full changed-file set)
|
|
72
|
-
//
|
|
73
|
-
const context = this.prContextWriter.ensure(repoRoot, this.aiBranchName.getFeatureName(), this.
|
|
76
|
+
// Persist the PR diff context (base sha + the full changed-file set) so it exists even if the AI
|
|
77
|
+
// stops here. Stage ② rewrites it with the materialized-diff dir once it has one.
|
|
78
|
+
const context = this.prContextWriter.ensure(repoRoot, this.aiBranchName.getFeatureName(), this.diffBasisResolver.resolve(repoRoot));
|
|
74
79
|
process.stdout.write('\n' + SEP + '② Review the PR, then finish\n' + SEP + '\n');
|
|
75
80
|
process.stdout.write(`Branch is updated (nothing pushed yet — finish does the one push, behind the build gate).\n` +
|
|
76
81
|
`${this.contextLines(context)}\n` +
|
|
77
|
-
|
|
78
|
-
`
|
|
79
|
-
|
|
80
|
-
`
|
|
81
|
-
`(It re-validates the build, re-checks every checklist, renders the dashboard, and creates/updates the PR.)\n\n`);
|
|
82
|
+
`▶ NEXT run: pnpm wp-review-upsert-pr\n` +
|
|
83
|
+
` It validates the 3-point merge, runs the build gate, extracts this branch's diff for the\n` +
|
|
84
|
+
` reviewers, and prints what to spawn plus the review.json schema. Everything else waits on it —\n` +
|
|
85
|
+
` wp-finish-upsert-pr refuses to open a PR until it has run.\n\n`);
|
|
82
86
|
}
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
* `wp-checklist`; a repo with none is told plainly that it has none, because printing "run wp-checklist"
|
|
86
|
-
* forever at a repo that will never have one is noise, and silence reads as "a checklist passed".
|
|
87
|
-
*/
|
|
88
|
-
checklistPointer(repoRoot) {
|
|
89
|
-
if ((0, rules_config_1.loadAndValidate)(repoRoot).prGate.checklists.length === 0) {
|
|
90
|
-
return '📋 Review checklists: NONE CONFIGURED for this repo — nothing to run, and that is fine.\n';
|
|
91
|
-
}
|
|
92
|
-
return ('📋 FIRST run: pnpm wp-checklist\n' +
|
|
93
|
-
' It validates this repo\'s checklist patterns against your diff and prints WHICH reviewer\n' +
|
|
94
|
-
' subagents you must spawn, plus the exact review-<id>.json each must write. Do that BEFORE\n' +
|
|
95
|
-
' finishing — wp-finish-upsert-pr refuses to open the PR until every one of them has run.\n');
|
|
96
|
-
}
|
|
97
|
-
// The diff facts, stated where the instruction to use them is — not in a separate block above it.
|
|
87
|
+
// The diff facts, stated where the instruction to use them is — not in a separate block above it. The
|
|
88
|
+
// command comes from the resolved basis; hand-writing `<base> HEAD` here is empty on a dirty tree.
|
|
98
89
|
contextLines(context) {
|
|
99
90
|
if (context.baseSha.trim() === '')
|
|
100
91
|
return '';
|
|
101
|
-
|
|
92
|
+
const cmd = context.fileDiffCommand.replace(' -- <file>', '');
|
|
93
|
+
return (`${cmd === '' ? '' : `Your diff: ${cmd}\n`}` +
|
|
102
94
|
`Full changed-file set + base/head sha: ${context.prContextPath}\n`);
|
|
103
95
|
}
|
|
104
96
|
// Scaffold the server-side CI check when (and only when) this repo set a gateSalt. Written to the
|
|
@@ -113,13 +105,15 @@ let StartUpsertPrCommand = class StartUpsertPrCommand {
|
|
|
113
105
|
` • mark the "webpieces-pr-gate" check REQUIRED in branch protection (repo admin only)\n`);
|
|
114
106
|
}
|
|
115
107
|
// Bring the branch up to date with main via the shared 3-point engine (in-process). On conflict the
|
|
116
|
-
// merge process doc it writes names `wp-
|
|
108
|
+
// merge process doc it writes names `wp-review-upsert-pr` as the finish command — that is the command
|
|
109
|
+
// that now validates and commits a conflict resolution, so it is what the doc must send the AI to.
|
|
117
110
|
async updateBranchFromMain(repoRoot) {
|
|
118
111
|
process.stdout.write('\n' + SEP + '① Updating branch from main\n' + SEP + '\n');
|
|
119
112
|
// pushRemote=false — finish owns the single push (see MergeEndOptions).
|
|
120
|
-
const outcome = await this.runUpdate.runUpdateFromMain(repoRoot, 'wp-start-upsert-pr', 'wp-
|
|
113
|
+
const outcome = await this.runUpdate.runUpdateFromMain(repoRoot, 'wp-start-upsert-pr', 'wp-review-upsert-pr', false);
|
|
121
114
|
if (outcome === 'conflict' || outcome === 'unvalidatedResume') {
|
|
122
|
-
throw new rules_config_1.CliExitError(2, '\n⏸️ Conflicts — resolve them, then run pnpm wp-
|
|
115
|
+
throw new rules_config_1.CliExitError(2, '\n⏸️ Conflicts — resolve them, then run pnpm wp-review-upsert-pr (it validates the merge, builds it,\n' +
|
|
116
|
+
' and only then briefs the reviewers).');
|
|
123
117
|
}
|
|
124
118
|
}
|
|
125
119
|
};
|
|
@@ -131,7 +125,7 @@ exports.StartUpsertPrCommand = StartUpsertPrCommand = tslib_1.__decorate([
|
|
|
131
125
|
branch_naming_1.BranchNaming,
|
|
132
126
|
git_exec_1.GitExec,
|
|
133
127
|
run_update_1.RunUpdate,
|
|
134
|
-
|
|
128
|
+
diff_basis_1.DiffBasisResolver,
|
|
135
129
|
pr_context_writer_1.PrContextWriter])
|
|
136
130
|
], StartUpsertPrCommand);
|
|
137
131
|
//# sourceMappingURL=start-upsert-pr-command.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"start-upsert-pr-command.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/pr-gate/src/scripts/commands/start-upsert-pr-command.ts"],"names":[],"mappings":";;;;AACA,0DAA6L;AAC7L,yCAA2D;AAC3D,2EAAgE;AAChE,6DAAyD;AACzD,mDAA+C;AAC/C,qEAA0D;AAC1D,qEAAgE;AAChE,uDAAmD;AAEnD,MAAM,GAAG,GAAG,0DAA0D,CAAC;AAEvE,+FAA+F;AAC/F,uGAAuG;AACvG,sGAAsG;AACtG,+FAA+F;AAC/F,wGAAwG;AACxG,+FAA+F;AAExF,IAAM,oBAAoB,GAA1B,MAAM,oBAAoB;IAER;IACA;IACA;IACA;IACA;IACA;IACA;IAPrB,YACqB,cAA8B,EAC9B,YAA0B,EAC1B,YAA0B,EAC1B,OAAgB,EAChB,SAAoB,EACpB,SAAoB,EACpB,eAAgC;QANhC,mBAAc,GAAd,cAAc,CAAgB;QAC9B,iBAAY,GAAZ,YAAY,CAAc;QAC1B,iBAAY,GAAZ,YAAY,CAAc;QAC1B,YAAO,GAAP,OAAO,CAAS;QAChB,cAAS,GAAT,SAAS,CAAW;QACpB,cAAS,GAAT,SAAS,CAAW;QACpB,oBAAe,GAAf,eAAe,CAAiB;IAClD,CAAC;IAEJ,KAAK,CAAC,GAAG;QACL,MAAM,QAAQ,GAAG,IAAI,CAAC,cAAc,CAAC,eAAe,CAAC,OAAO,CAAC,GAAG,EAAE,CAAC,CAAC;QACpE,gGAAgG;QAChG,IAAA,4BAAa,EAAC,QAAQ,EAAE,2BAA2B,CAAC,CAAC;QACrD,gGAAgG;QAChG,mGAAmG;QACnG,mGAAmG;QACnG,6CAA6C;QAC7C,IAAI,CAAC,kBAAkB,CAAC,QAAQ,CAAC,CAAC;QAElC,gGAAgG;QAChG,4FAA4F;QAC5F,iGAAiG;QACjG,IAAI,CAAC,OAAO,CAAC,eAAe,CAAC,QAAQ,CAAC,CAAC;QAEvC,iGAAiG;QACjG,gGAAgG;QAChG,gGAAgG;QAChG,MAAM,IAAI,CAAC,oBAAoB,CAAC,QAAQ,CAAC,CAAC;QAE1C,8FAA8F;QAC9F,8FAA8F;QAC9F,IAAI,CAAC,eAAe,CAAC,QAAQ,CAAC,CAAC;IACnC,CAAC;IAED;;;;;;;;OAQG;IACK,eAAe,CAAC,QAAgB;QACpC,MAAM,UAAU,GAAG,IAAA,6BAAc,EAAC,QAAQ,EAAE,IAAI,CAAC,YAAY,CAAC,cAAc,EAAE,CAAC,CAAC;QAChF,4FAA4F;QAC5F,IAAA,4BAAa,EAAC,QAAQ,EAAE,gCAAgC,CAAC,CAAC;QAC1D,uGAAuG;QACvG,yDAAyD;QACzD,MAAM,OAAO,GAAG,IAAI,CAAC,eAAe,CAAC,MAAM,CAAC,QAAQ,EAAE,IAAI,CAAC,YAAY,CAAC,cAAc,EAAE,EAAE,IAAI,CAAC,SAAS,CAAC,gBAAgB,CAAC,QAAQ,CAAC,CAAC,CAAC;QACrI,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,GAAG,GAAG,GAAG,gCAAgC,GAAG,GAAG,GAAG,IAAI,CAAC,CAAC;QACjF,OAAO,CAAC,MAAM,CAAC,KAAK,CAChB,6FAA6F;YAC7F,GAAG,IAAI,CAAC,YAAY,CAAC,OAAO,CAAC,IAAI;YACjC,GAAG,IAAI,CAAC,gBAAgB,CAAC,QAAQ,CAAC,IAAI;YACtC,oCAAoC;YACpC,GAAG,IAAA,mCAAoB,EAAC,UAAU,CAAC,MAAM;YACzC,0CAA0C;YAC1C,gHAAgH,CACnH,CAAC;IACN,CAAC;IAED;;;;OAIG;IACK,gBAAgB,CAAC,QAAgB;QACrC,IAAI,IAAA,8BAAe,EAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,UAAU,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YAC3D,OAAO,2FAA2F,CAAC;QACvG,CAAC;QACD,OAAO,CACH,oCAAoC;YACpC,+FAA+F;YAC/F,gGAAgG;YAChG,8FAA8F,CACjG,CAAC;IACN,CAAC;IAED,kGAAkG;IAC1F,YAAY,CAAC,OAA+B;QAChD,IAAI,OAAO,CAAC,OAAO,CAAC,IAAI,EAAE,KAAK,EAAE;YAAE,OAAO,EAAE,CAAC;QAC7C,OAAO,CACH,wBAAwB,OAAO,CAAC,OAAO,SAAS;YAChD,2CAA2C,OAAO,CAAC,aAAa,IAAI,CACvE,CAAC;IACN,CAAC;IAED,kGAAkG;IAClG,oGAAoG;IACpG,mGAAmG;IAC3F,kBAAkB,CAAC,QAAgB;QACvC,IAAI,IAAA,8BAAe,EAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,QAAQ,CAAC,IAAI,EAAE,KAAK,EAAE;YAAE,OAAO;QACpE,IAAA,qCAAsB,EAAC,QAAQ,EAAE,uBAAuB,CAAC,CAAC;QAC1D,OAAO,CAAC,MAAM,CAAC,KAAK,CAChB,qFAAqF;YACrF,gGAAgG;YAChG,2FAA2F,CAC9F,CAAC;IACN,CAAC;IAED,oGAAoG;IACpG,iFAAiF;IACzE,KAAK,CAAC,oBAAoB,CAAC,QAAgB;QAC/C,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,GAAG,GAAG,GAAG,+BAA+B,GAAG,GAAG,GAAG,IAAI,CAAC,CAAC;QAChF,wEAAwE;QACxE,MAAM,OAAO,GAAG,MAAM,IAAI,CAAC,SAAS,CAAC,iBAAiB,CAAC,QAAQ,EAAE,oBAAoB,EAAE,qBAAqB,EAAE,KAAK,CAAC,CAAC;QACrH,IAAI,OAAO,KAAK,UAAU,IAAI,OAAO,KAAK,mBAAmB,EAAE,CAAC;YAC5D,MAAM,IAAI,2BAAY,CAAC,CAAC,EACpB,iHAAiH,CACpH,CAAC;QACN,CAAC;IACL,CAAC;CACJ,CAAA;AAnHY,oDAAoB;+BAApB,oBAAoB;IADhC,IAAA,sBAAU,EAAC,8BAAkB,CAAC,SAAS,CAAC;6CAGA,6BAAc;QAChB,mCAAY;QACZ,4BAAY;QACjB,kBAAO;QACL,sBAAS;QACT,6BAAS;QACH,mCAAe;GAR5C,oBAAoB,CAmHhC","sourcesContent":["import { execSync } from 'child_process';\nimport { loadAndValidate, reviewJsonPath, reviewJsonSchemaHint, writeTemplate, writeTemplateIfMissing, CliExitError, RepoRootFinder, ChecklistReviewContext } from '@webpieces/rules-config';\nimport { injectable, bindingScopeValues } from 'inversify';\nimport { AiBranchName } from '../workflow/git-readAiBranchName';\nimport { BranchNaming } from '../workflow/branch-naming';\nimport { GitExec } from '../workflow/git-exec';\nimport { ForkPoint } from '../workflow/git-findForkPoint';\nimport { PrContextWriter } from '../workflow/pr-context-writer';\nimport { RunUpdate } from '../workflow/run-update';\n\nconst SEP = '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\\n';\n\n// START of the AI-first PR flow: the deterministic setup — update from main — then hand the AI\n// instructions to run `wp-checklist`, WRITE review.json, and run `wp-finish-upsert-pr` (which reads it\n// and posts the PR). It runs NO build gate: start's job is the update / 3-point merge, and the SINGLE\n// build gate is authoritative and lives in wp-finish-upsert-pr (so `pr-gate.buildCommand` is a\n// finish-only knob). This command NEVER creates/updates a PR and NEVER pushes: all `gh` posting and the\n// ONE push live in finish, behind review.json + the checklists + the authoritative build gate.\n@injectable(bindingScopeValues.Singleton)\nexport class StartUpsertPrCommand {\n constructor(\n private readonly repoRootFinder: RepoRootFinder,\n private readonly aiBranchName: AiBranchName,\n private readonly branchNaming: BranchNaming,\n private readonly gitExec: GitExec,\n private readonly runUpdate: RunUpdate,\n private readonly forkPoint: ForkPoint,\n private readonly prContextWriter: PrContextWriter,\n ) {}\n\n async run(): Promise<void> {\n const repoRoot = this.repoRootFinder.resolveRepoRoot(process.cwd());\n // Refresh the AI-facing workflow doc so it's present + current for any failure message to cite.\n writeTemplate(repoRoot, 'webpieces.git-workflow.md');\n // When this repo has opted into server-side enforcement (a committed gateSalt), scaffold the CI\n // workflow into the gitignored instruct-ai dir (never .github directly — that would dirty the tree\n // before the clean-tree check) and tell the human to copy + require it. IfMissing so it is written\n // once and never clobbers a customized copy.\n this.scaffoldCiWorkflow(repoRoot);\n\n // Precondition: a fully-committed tree. This flow squash-updates the branch and builds it — the\n // tooling must not commit your work for you, and building a dirty working tree would let an\n // uncommitted change build green over a different commit than the one that ships. Fail if dirty.\n this.gitExec.assertCleanTree(repoRoot);\n\n // Nothing here pushes. This command reviews; wp-finish-upsert-pr pushes ONCE, after review.json,\n // every BLOCK checklist, and the authoritative build gate — so no unreviewed commit reaches the\n // remote, and there is no early `synchronize` firing against a PR body with a stale gate token.\n await this.updateBranchFromMain(repoRoot);\n\n // No build gate here — start only updates from main / drives the 3-point merge. The one build\n // gate is authoritative and runs in wp-finish-upsert-pr, after review.json + every checklist.\n this.handOffToReview(repoRoot);\n }\n\n /**\n * Hand the AI its next step: run `wp-checklist`, write review.json, then run finish (which posts the PR).\n *\n * This USED to compute the matched checklists and print every reviewer's full instructions inline — twice,\n * in two blocks that could drift. That detail now lives in ONE command, `wp-checklist`, which wp-finish\n * validates against; here we only point at it. Keeping this section short is the point: it is the block\n * the AI must actually act on, and burying the three steps in forty lines of reviewer detail is how a\n * step gets skipped.\n */\n private handOffToReview(repoRoot: string): void {\n const reviewPath = reviewJsonPath(repoRoot, this.aiBranchName.getFeatureName());\n // Persist the review-format + process instructions where any failure message can cite them.\n writeTemplate(repoRoot, 'webpieces.review-checklists.md');\n // Persist the PR diff context (base sha + the full changed-file set) — wp-checklist and every reviewer\n // subagent read it, so it must exist before either runs.\n const context = this.prContextWriter.ensure(repoRoot, this.aiBranchName.getFeatureName(), this.forkPoint.resolveForkPoint(repoRoot));\n process.stdout.write('\\n' + SEP + '② Review the PR, then finish\\n' + SEP + '\\n');\n process.stdout.write(\n `Branch is updated (nothing pushed yet — finish does the one push, behind the build gate).\\n` +\n `${this.contextLines(context)}\\n` +\n `${this.checklistPointer(repoRoot)}\\n` +\n `Then review your own changes and\\n` +\n `${reviewJsonSchemaHint(reviewPath)}\\n\\n` +\n `Finally run: pnpm wp-finish-upsert-pr\\n` +\n `(It re-validates the build, re-checks every checklist, renders the dashboard, and creates/updates the PR.)\\n\\n`,\n );\n }\n\n /**\n * Step 1 of the review: find out what review this diff owes. A repo with checklists gets pointed at\n * `wp-checklist`; a repo with none is told plainly that it has none, because printing \"run wp-checklist\"\n * forever at a repo that will never have one is noise, and silence reads as \"a checklist passed\".\n */\n private checklistPointer(repoRoot: string): string {\n if (loadAndValidate(repoRoot).prGate.checklists.length === 0) {\n return '📋 Review checklists: NONE CONFIGURED for this repo — nothing to run, and that is fine.\\n';\n }\n return (\n '📋 FIRST run: pnpm wp-checklist\\n' +\n ' It validates this repo\\'s checklist patterns against your diff and prints WHICH reviewer\\n' +\n ' subagents you must spawn, plus the exact review-<id>.json each must write. Do that BEFORE\\n' +\n ' finishing — wp-finish-upsert-pr refuses to open the PR until every one of them has run.\\n'\n );\n }\n\n // The diff facts, stated where the instruction to use them is — not in a separate block above it.\n private contextLines(context: ChecklistReviewContext): string {\n if (context.baseSha.trim() === '') return '';\n return (\n `Your diff: git diff ${context.baseSha} HEAD\\n` +\n `Full changed-file set + base/head sha: ${context.prContextPath}\\n`\n );\n }\n\n // Scaffold the server-side CI check when (and only when) this repo set a gateSalt. Written to the\n // gitignored instruct-ai dir so it never dirties the tree; the human copies it to .github/workflows\n // and marks it required (webpieces can't set branch protection). No-op for repos with no gateSalt.\n private scaffoldCiWorkflow(repoRoot: string): void {\n if (loadAndValidate(repoRoot).prGate.gateSalt.trim() === '') return;\n writeTemplateIfMissing(repoRoot, 'webpieces-pr-gate.yml');\n process.stdout.write(\n `\\nℹ️ Server-side gate enforcement is ON (gateSalt set). If you have not already:\\n` +\n ` • copy .webpieces/instruct-ai/webpieces-pr-gate.yml → .github/workflows/ and commit it\\n` +\n ` • mark the \"webpieces-pr-gate\" check REQUIRED in branch protection (repo admin only)\\n`,\n );\n }\n\n // Bring the branch up to date with main via the shared 3-point engine (in-process). On conflict the\n // merge process doc it writes names `wp-finish-upsert-pr` as the finish command.\n private async updateBranchFromMain(repoRoot: string): Promise<void> {\n process.stdout.write('\\n' + SEP + '① Updating branch from main\\n' + SEP + '\\n');\n // pushRemote=false — finish owns the single push (see MergeEndOptions).\n const outcome = await this.runUpdate.runUpdateFromMain(repoRoot, 'wp-start-upsert-pr', 'wp-finish-upsert-pr', false);\n if (outcome === 'conflict' || outcome === 'unvalidatedResume') {\n throw new CliExitError(2,\n '\\n⏸️ Conflicts — resolve them, then run pnpm wp-finish-upsert-pr (it validates the merge AND finishes the PR).',\n );\n }\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"start-upsert-pr-command.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/pr-gate/src/scripts/commands/start-upsert-pr-command.ts"],"names":[],"mappings":"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","sourcesContent":["import { execSync } from 'child_process';\nimport { loadAndValidate, writeTemplate, writeTemplateIfMissing, CliExitError, RepoRootFinder, ChecklistReviewContext } from '@webpieces/rules-config';\nimport { injectable, bindingScopeValues } from 'inversify';\nimport { AiBranchName } from '../workflow/git-readAiBranchName';\nimport { BranchNaming } from '../workflow/branch-naming';\nimport { DiffBasisResolver } from '../workflow/diff-basis';\nimport { GitExec } from '../workflow/git-exec';\nimport { PrContextWriter } from '../workflow/pr-context-writer';\nimport { RunUpdate } from '../workflow/run-update';\n\nconst SEP = '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\\n';\n\n// STAGE ① of the AI-first PR flow: the deterministic setup — update from main / drive the 3-point merge —\n// then hand the AI ONE next command, `wp-review-upsert-pr`.\n//\n// It runs NO build gate, and that is still deliberate, but the reason has changed. It is not that the build\n// belongs only at the end: it is that at THIS point a conflicted 3-point merge may still be unresolved, so\n// there is nothing worth building yet. The gate moved FORWARD, not away — stage ② finalizes the merge and\n// then builds, so reviewers never judge a branch that does not compile, and wp-finish-upsert-pr skips the\n// rebuild when HEAD has not moved since. `pr-gate.buildCommand` is therefore no longer a finish-only knob.\n//\n// This command NEVER creates/updates a PR and NEVER pushes: all `gh` posting and the ONE push live in\n// finish, behind review.json + the checklists + the build gate.\n@injectable(bindingScopeValues.Singleton)\nexport class StartUpsertPrCommand {\n constructor(\n private readonly repoRootFinder: RepoRootFinder,\n private readonly aiBranchName: AiBranchName,\n private readonly branchNaming: BranchNaming,\n private readonly gitExec: GitExec,\n private readonly runUpdate: RunUpdate,\n private readonly diffBasisResolver: DiffBasisResolver,\n private readonly prContextWriter: PrContextWriter,\n ) {}\n\n async run(): Promise<void> {\n const repoRoot = this.repoRootFinder.resolveRepoRoot(process.cwd());\n // Refresh the AI-facing workflow doc so it's present + current for any failure message to cite.\n writeTemplate(repoRoot, 'webpieces.git-workflow.md');\n // When this repo has opted into server-side enforcement (a committed gateSalt), scaffold the CI\n // workflow into the gitignored instruct-ai dir (never .github directly — that would dirty the tree\n // before the clean-tree check) and tell the human to copy + require it. IfMissing so it is written\n // once and never clobbers a customized copy.\n this.scaffoldCiWorkflow(repoRoot);\n\n // Precondition: a fully-committed tree. This flow squash-updates the branch and builds it — the\n // tooling must not commit your work for you, and building a dirty working tree would let an\n // uncommitted change build green over a different commit than the one that ships. Fail if dirty.\n this.gitExec.assertCleanTree(repoRoot);\n\n // Nothing here pushes. This command reviews; wp-finish-upsert-pr pushes ONCE, after review.json,\n // every BLOCK checklist, and the authoritative build gate — so no unreviewed commit reaches the\n // remote, and there is no early `synchronize` firing against a PR body with a stale gate token.\n await this.updateBranchFromMain(repoRoot);\n\n // No build gate here — a conflicted merge may still be unresolved, so there is nothing worth\n // building. Stage ② finalizes the merge FIRST, then builds; see the class comment.\n this.handOffToReview(repoRoot);\n }\n\n /**\n * Hand the AI its ONE next step: `wp-review-upsert-pr`.\n *\n * This prints exactly one command on purpose. It used to print three (run the checklist, write\n * review.json, then finish), and a three-item list is a list with a step to skip. Stage ② now owns all\n * of it — it validates the 3-point merge, builds, materializes the diff, briefs the reviewers, AND\n * prints the review.json schema — so there is nothing here to enumerate. Notably review.json is NOT\n * requested here any more: asking for a written review before the branch is known to compile invites\n * one that describes code that does not build.\n */\n private handOffToReview(repoRoot: string): void {\n // Persist the review-format + process instructions where any failure message can cite them.\n writeTemplate(repoRoot, 'webpieces.review-checklists.md');\n // Persist the PR diff context (base sha + the full changed-file set) so it exists even if the AI\n // stops here. Stage ② rewrites it with the materialized-diff dir once it has one.\n const context = this.prContextWriter.ensure(\n repoRoot, this.aiBranchName.getFeatureName(), this.diffBasisResolver.resolve(repoRoot));\n process.stdout.write('\\n' + SEP + '② Review the PR, then finish\\n' + SEP + '\\n');\n process.stdout.write(\n `Branch is updated (nothing pushed yet — finish does the one push, behind the build gate).\\n` +\n `${this.contextLines(context)}\\n` +\n `▶ NEXT run: pnpm wp-review-upsert-pr\\n` +\n ` It validates the 3-point merge, runs the build gate, extracts this branch's diff for the\\n` +\n ` reviewers, and prints what to spawn plus the review.json schema. Everything else waits on it —\\n` +\n ` wp-finish-upsert-pr refuses to open a PR until it has run.\\n\\n`,\n );\n }\n\n // The diff facts, stated where the instruction to use them is — not in a separate block above it. The\n // command comes from the resolved basis; hand-writing `<base> HEAD` here is empty on a dirty tree.\n private contextLines(context: ChecklistReviewContext): string {\n if (context.baseSha.trim() === '') return '';\n const cmd = context.fileDiffCommand.replace(' -- <file>', '');\n return (\n `${cmd === '' ? '' : `Your diff: ${cmd}\\n`}` +\n `Full changed-file set + base/head sha: ${context.prContextPath}\\n`\n );\n }\n\n // Scaffold the server-side CI check when (and only when) this repo set a gateSalt. Written to the\n // gitignored instruct-ai dir so it never dirties the tree; the human copies it to .github/workflows\n // and marks it required (webpieces can't set branch protection). No-op for repos with no gateSalt.\n private scaffoldCiWorkflow(repoRoot: string): void {\n if (loadAndValidate(repoRoot).prGate.gateSalt.trim() === '') return;\n writeTemplateIfMissing(repoRoot, 'webpieces-pr-gate.yml');\n process.stdout.write(\n `\\nℹ️ Server-side gate enforcement is ON (gateSalt set). If you have not already:\\n` +\n ` • copy .webpieces/instruct-ai/webpieces-pr-gate.yml → .github/workflows/ and commit it\\n` +\n ` • mark the \"webpieces-pr-gate\" check REQUIRED in branch protection (repo admin only)\\n`,\n );\n }\n\n // Bring the branch up to date with main via the shared 3-point engine (in-process). On conflict the\n // merge process doc it writes names `wp-review-upsert-pr` as the finish command — that is the command\n // that now validates and commits a conflict resolution, so it is what the doc must send the AI to.\n private async updateBranchFromMain(repoRoot: string): Promise<void> {\n process.stdout.write('\\n' + SEP + '① Updating branch from main\\n' + SEP + '\\n');\n // pushRemote=false — finish owns the single push (see MergeEndOptions).\n const outcome = await this.runUpdate.runUpdateFromMain(repoRoot, 'wp-start-upsert-pr', 'wp-review-upsert-pr', false);\n if (outcome === 'conflict' || outcome === 'unvalidatedResume') {\n throw new CliExitError(2,\n '\\n⏸️ Conflicts — resolve them, then run pnpm wp-review-upsert-pr (it validates the merge, builds it,\\n' +\n ' and only then briefs the reviewers).',\n );\n }\n }\n}\n"]}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { MutationVerb, DeletableWorktree, MergedBranchesService, WorktreeReapResult, WorktreeReaper } from '@webpieces/rules-config';
|
|
2
|
+
/**
|
|
3
|
+
* The WORKTREE half of `wp-cleanup` — the verdicts, the reap and the human-facing text.
|
|
4
|
+
*
|
|
5
|
+
* Split out of CleanupCommand rather than bolted onto it because the two halves answer different
|
|
6
|
+
* questions: a branch is a ref, a worktree is a DIRECTORY OF FILES, and the second one needs its own
|
|
7
|
+
* report (paths, not just names), its own restore command (`git worktree add`, not `git checkout -b`)
|
|
8
|
+
* and its own spared vocabulary (locked, detached, "you are standing in it"). Keeping them in one class
|
|
9
|
+
* meant every string had to hedge about which kind of thing it was talking about.
|
|
10
|
+
*
|
|
11
|
+
* WHY worktrees are reaped BEFORE branches in wp-cleanup: a worktree HOLDS its branch, so that branch
|
|
12
|
+
* is spared as `in-use` with "remove that worktree before deleting the branch". Reaping the worktree
|
|
13
|
+
* first is what makes the branch reapable — the reap takes the branch with it, and the branch pass that
|
|
14
|
+
* follows recomputes its verdicts against the post-removal truth. Run the other way round, every
|
|
15
|
+
* worktree-held branch survives forever, which is exactly the deadlock this change exists to break.
|
|
16
|
+
*/
|
|
17
|
+
export declare class WorktreeCleanupSection {
|
|
18
|
+
private readonly mergedBranches;
|
|
19
|
+
private readonly reaper;
|
|
20
|
+
constructor(mergedBranches: MergedBranchesService, reaper: WorktreeReaper);
|
|
21
|
+
/**
|
|
22
|
+
* FRESH verdicts — never the cache on disk. Same rule the branch half follows: the cached file is
|
|
23
|
+
* deliberately allowed to go stale, which is fine for BLOCKING a `git worktree add` and never fine
|
|
24
|
+
* for removing a directory, since the tree may have gained uncommitted work since it was written.
|
|
25
|
+
*/
|
|
26
|
+
verdicts(repoRoot: string): DeletableWorktree[];
|
|
27
|
+
provablyDead(verdicts: DeletableWorktree[]): DeletableWorktree[];
|
|
28
|
+
/**
|
|
29
|
+
* The spared worktrees a human can meaningfully rule on, grouped most-safe first — the same
|
|
30
|
+
* classification order the branch prompt uses, because it is literally the same verdict on the
|
|
31
|
+
* branch the worktree holds.
|
|
32
|
+
*
|
|
33
|
+
* Deliberately excluded: LOCKED (a human already said do not touch), CURRENT (removing your own cwd
|
|
34
|
+
* is not a thing to offer), DETACHED (no branch, so nothing to archive and nothing to judge) and
|
|
35
|
+
* PRUNABLE (already provably dead — it is in the auto-reap list, not this one).
|
|
36
|
+
*/
|
|
37
|
+
promptable(verdicts: DeletableWorktree[]): DeletableWorktree[];
|
|
38
|
+
reap(repoRoot: string, verb: MutationVerb, targets: DeletableWorktree[], retention: string): WorktreeReapResult;
|
|
39
|
+
report(result: WorktreeReapResult): string;
|
|
40
|
+
private reapedLine;
|
|
41
|
+
/** The spared worktrees, with WHY — including the ones nobody will ever be asked about. */
|
|
42
|
+
sparedBlock(verdicts: DeletableWorktree[], removed: DeletableWorktree[]): string;
|
|
43
|
+
private isMechanical;
|
|
44
|
+
promptBlock(promptable: DeletableWorktree[]): string;
|
|
45
|
+
}
|