@webpieces/pr-gate 0.4.692 → 0.4.694
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 +2 -2
- package/src/scripts/commands/build-command.d.ts +3 -3
- package/src/scripts/commands/build-command.js +4 -4
- package/src/scripts/commands/build-command.js.map +1 -1
- package/src/scripts/commands/finish-upsert-pr-command.d.ts +12 -3
- package/src/scripts/commands/finish-upsert-pr-command.js +25 -10
- package/src/scripts/commands/finish-upsert-pr-command.js.map +1 -1
- package/src/scripts/commands/review-upsert-pr-command.d.ts +15 -1
- package/src/scripts/commands/review-upsert-pr-command.js +25 -4
- package/src/scripts/commands/review-upsert-pr-command.js.map +1 -1
- package/src/scripts/workflow/build-affected.d.ts +11 -30
- package/src/scripts/workflow/build-affected.js +25 -61
- package/src/scripts/workflow/build-affected.js.map +1 -1
- package/src/scripts/workflow/build-gate-log.d.ts +14 -29
- package/src/scripts/workflow/build-gate-log.js +34 -91
- package/src/scripts/workflow/build-gate-log.js.map +1 -1
- package/src/scripts/workflow/gate-log-file.d.ts +55 -0
- package/src/scripts/workflow/gate-log-file.js +136 -0
- package/src/scripts/workflow/gate-log-file.js.map +1 -0
- package/src/scripts/workflow/stage-output-log.d.ts +40 -0
- package/src/scripts/workflow/stage-output-log.js +137 -0
- package/src/scripts/workflow/stage-output-log.js.map +1 -0
|
@@ -20,6 +20,7 @@ const pr_context_writer_1 = require("../workflow/pr-context-writer");
|
|
|
20
20
|
const reviewer_briefing_builder_1 = require("../workflow/reviewer-briefing-builder");
|
|
21
21
|
const review_report_1 = require("../workflow/review-report");
|
|
22
22
|
const review_stage_receipt_1 = require("../workflow/review-stage-receipt");
|
|
23
|
+
const stage_output_log_1 = require("../workflow/stage-output-log");
|
|
23
24
|
const SEP = '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n';
|
|
24
25
|
/** The `--no-optional` decision, as data (per CLAUDE.md) rather than a bare boolean parameter. */
|
|
25
26
|
class ReviewUpsertPrOptions {
|
|
@@ -75,10 +76,11 @@ let ReviewUpsertPrCommand = class ReviewUpsertPrCommand {
|
|
|
75
76
|
receipts;
|
|
76
77
|
activeHatchReport;
|
|
77
78
|
reviewJsonService;
|
|
79
|
+
stageConsole;
|
|
78
80
|
// eslint-disable-next-line @typescript-eslint/max-params
|
|
79
81
|
constructor(repoRootFinder, aiBranchName, gitExec, buildAffected, buildArtifactGate, mergeState, mergeEnd, checklistScanner, reviewReport, materializer, prContextWriter, briefingBuilder, reviewerInstructions, receipts, activeHatchReport,
|
|
80
82
|
// Injected only to RESOLVE + RENDER refusals (see refusals()). This stage never archives a verdict.
|
|
81
|
-
reviewJsonService) {
|
|
83
|
+
reviewJsonService, stageConsole) {
|
|
82
84
|
this.repoRootFinder = repoRootFinder;
|
|
83
85
|
this.aiBranchName = aiBranchName;
|
|
84
86
|
this.gitExec = gitExec;
|
|
@@ -95,9 +97,24 @@ let ReviewUpsertPrCommand = class ReviewUpsertPrCommand {
|
|
|
95
97
|
this.receipts = receipts;
|
|
96
98
|
this.activeHatchReport = activeHatchReport;
|
|
97
99
|
this.reviewJsonService = reviewJsonService;
|
|
100
|
+
this.stageConsole = stageConsole;
|
|
98
101
|
}
|
|
102
|
+
/**
|
|
103
|
+
* Everything below runs with this stage's console CAPTURED to a file (see StageOutputLog). Only the
|
|
104
|
+
* lines an agent must act on before it can do anything else reach the terminal — the build heartbeat
|
|
105
|
+
* and result, and the closing instruction block. The merge validation, the hatch report, the checklist
|
|
106
|
+
* scan and the diff extraction are all in the file, one `grep` away.
|
|
107
|
+
*
|
|
108
|
+
* This is the FIRST half of the fix for the piped-command watchdog kill: an agent pipes a command it
|
|
109
|
+
* expects to flood, a pipe withholds every byte until exit, and the harness kills a command that has
|
|
110
|
+
* printed nothing for 600 seconds — after a full build. Guarding the pipe without first shrinking the
|
|
111
|
+
* output would only have traded that kill for a flooded context.
|
|
112
|
+
*/
|
|
99
113
|
async run(opts = new ReviewUpsertPrOptions()) {
|
|
100
114
|
const repoRoot = this.repoRootFinder.resolveRepoRoot(process.cwd());
|
|
115
|
+
await this.stageConsole.withCapture(repoRoot, stage_output_log_1.REVIEW_CONSOLE_LOG, () => this.runStage(repoRoot, opts));
|
|
116
|
+
}
|
|
117
|
+
async runStage(repoRoot, opts) {
|
|
101
118
|
(0, rules_config_1.writeTemplate)(repoRoot, 'webpieces.git-workflow.md');
|
|
102
119
|
(0, rules_config_1.writeTemplate)(repoRoot, 'webpieces.review-checklists.md');
|
|
103
120
|
const featureName = this.aiBranchName.getFeatureName();
|
|
@@ -152,7 +169,7 @@ let ReviewUpsertPrCommand = class ReviewUpsertPrCommand {
|
|
|
152
169
|
* earlier did not turn one build into two.
|
|
153
170
|
*/
|
|
154
171
|
async runBuildGate(repoRoot) {
|
|
155
|
-
await this.buildAffected.runBuildGate(repoRoot, new build_affected_1.BuildGateOptions('🛠️ Build gate', 'pnpm wp-review-upsert-pr', 'Build failed — NO reviewer was briefed and no diff was extracted. Fix it, then re-run.', build_gate_log_1.REVIEW_STAGE
|
|
172
|
+
await this.buildAffected.runBuildGate(repoRoot, new build_affected_1.BuildGateOptions('🛠️ Build gate', 'pnpm wp-review-upsert-pr', 'Build failed — NO reviewer was briefed and no diff was extracted. Fix it, then re-run.', build_gate_log_1.REVIEW_STAGE));
|
|
156
173
|
// Repo-wide: the build must not have left anything uncommitted AND unstaged. Runs HERE, not in
|
|
157
174
|
// finish, because this is the stage that ran buildCommand and is therefore holding the dirty
|
|
158
175
|
// tree at the exact moment the question is answerable. Replaces the per-project
|
|
@@ -203,7 +220,10 @@ let ReviewUpsertPrCommand = class ReviewUpsertPrCommand {
|
|
|
203
220
|
input.briefings = briefings.slice();
|
|
204
221
|
input.refused = this.refusals(scan);
|
|
205
222
|
input.skipOptional = opts.skipOptional;
|
|
206
|
-
|
|
223
|
+
// `say`: this block IS the next action — which reviewers to spawn, where review.json goes, and
|
|
224
|
+
// the command after that. Capturing it into the log would leave the terminal with a pointer and
|
|
225
|
+
// no instruction, which is the one thing this stage may never do.
|
|
226
|
+
this.stageConsole.say(this.reviewReport.render(input));
|
|
207
227
|
}
|
|
208
228
|
/**
|
|
209
229
|
* The applicable checklists that ALREADY ANSWERED and refused, each with its refusal rendered by the ONE
|
|
@@ -242,6 +262,7 @@ exports.ReviewUpsertPrCommand = ReviewUpsertPrCommand = tslib_1.__decorate([
|
|
|
242
262
|
rules_config_1.ReviewerInstructionsService,
|
|
243
263
|
review_stage_receipt_1.ReviewStageReceiptService,
|
|
244
264
|
active_hatches_1.ActiveHatchReport,
|
|
245
|
-
rules_config_1.ReviewJsonService
|
|
265
|
+
rules_config_1.ReviewJsonService,
|
|
266
|
+
stage_output_log_1.StageOutputLog])
|
|
246
267
|
], ReviewUpsertPrCommand);
|
|
247
268
|
//# sourceMappingURL=review-upsert-pr-command.js.map
|
|
@@ -1 +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 {\n loadAndValidate, writeTemplate, PrGateConfig, RepoRootFinder, RequiredChecklist,\n ReviewerBriefing, ReviewerInstructionsService, ReviewJsonService,\n} from '@webpieces/rules-config';\nimport { injectable, bindingScopeValues } from 'inversify';\nimport { ActiveHatch, ActiveHatchReport } from '../workflow/active-hatches';\nimport { AiBranchName } from '../workflow/git-readAiBranchName';\nimport { BuildAffected, BuildGateOptions } from '../workflow/build-affected';\nimport { REVIEW_STAGE } from '../workflow/build-gate-log';\nimport { BuildArtifactGate } from '../workflow/build-artifact-gate';\nimport { ChecklistScan, ChecklistScanOptions, ChecklistScanner } from '../workflow/checklist-scanner';\nimport { 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 { PrContextWriter } from '../workflow/pr-context-writer';\nimport { ReviewerBriefingBuilder } from '../workflow/reviewer-briefing-builder';\nimport { RefusedReviewer, ReviewReport, ReviewReportInput } from '../workflow/review-report';\nimport { ReviewStageReceipt, ReviewStageReceiptService } from '../workflow/review-stage-receipt';\n\nconst SEP = '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\\n';\n\n/** The `--no-optional` decision, as data (per CLAUDE.md) rather than a bare boolean parameter. */\nexport class ReviewUpsertPrOptions {\n /**\n * true ⇒ the human ALREADY said to submit without the optional reviews, so stage ② must not print the\n * block telling the AI to offer them. It changes nothing about what BLOCKS: required checklists are\n * unaffected, and an optional checklist that already has a red verdict on this branch still refuses the\n * PR at finish. This only suppresses the offer.\n */\n skipOptional: boolean;\n\n constructor(skipOptional = false) {\n this.skipOptional = skipOptional;\n }\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 buildArtifactGate: BuildArtifactGate,\n private readonly mergeState: MergeState,\n private readonly mergeEnd: MergeEnd,\n private readonly checklistScanner: ChecklistScanner,\n private readonly reviewReport: ReviewReport,\n private readonly materializer: DiffMaterializer,\n private readonly prContextWriter: PrContextWriter,\n private readonly briefingBuilder: ReviewerBriefingBuilder,\n private readonly reviewerInstructions: ReviewerInstructionsService,\n private readonly receipts: ReviewStageReceiptService,\n private readonly activeHatchReport: ActiveHatchReport,\n // Injected only to RESOLVE + RENDER refusals (see refusals()). This stage never archives a verdict.\n private readonly reviewJsonService: ReviewJsonService,\n ) {}\n\n async run(opts: ReviewUpsertPrOptions = new ReviewUpsertPrOptions()): 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 = await this.runBuildGate(repoRoot);\n\n const scan = this.checklistScanner.scan(repoRoot, config.checklists, new ChecklistScanOptions(false, '')); // '' — THIS command writes the context itself, after materializing\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.reportActiveHatches(repoRoot);\n this.report(repoRoot, featureName, scan, briefings, opts);\n }\n\n /**\n * List every rule that is currently switched off in webpieces.config.json. NON-BLOCKING and printed\n * before the instruction block, because it is context for the review rather than a step in it.\n *\n * A hatch is the one config change that makes the build QUIETER, so no gate, reviewer or red check\n * will ever mention it — which is how a repo ends up with rules off for weeks that nobody chose to\n * leave off. This is the only place in the flow that says so out loud.\n */\n private reportActiveHatches(repoRoot: string): void {\n const hatches: ActiveHatch[] = this.activeHatchReport.scan(repoRoot);\n process.stdout.write(this.activeHatchReport.render(hatches));\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 async runBuildGate(repoRoot: string): Promise<string> {\n await this.buildAffected.runBuildGate(repoRoot, new BuildGateOptions(\n '🛠️ Build gate',\n 'pnpm wp-review-upsert-pr',\n 'Build failed — NO reviewer was briefed and no diff was extracted. Fix it, then re-run.',\n REVIEW_STAGE,\n false,\n ));\n // Repo-wide: the build must not have left anything uncommitted AND unstaged. Runs HERE, not in\n // finish, because this is the stage that ran buildCommand and is therefore holding the dirty\n // tree at the exact moment the question is answerable. Replaces the per-project\n // validate-di-graph-unchanged nx target — see BuildArtifactGate for why that one had to go.\n this.buildArtifactGate.assertBuildLeftNothingUncommitted(repoRoot);\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 // Write pr-context.json ONCE, here — after materializing, so `diffDir` is populated on the first\n // write. The scan deliberately did not write it (ChecklistScanOptions.contextStage === ''); it used\n // to, which meant this command wrote the same file twice, the first time with an empty diffDir.\n // `scan.changedFiles` is passed through so the context is not recomputed from a second git call.\n scan.context = this.prContextWriter.ensure(\n repoRoot, featureName, scan.basis, 'stage2-review', scan.changedFiles, 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 return briefings;\n }\n\n /**\n * What the AI must do next — rendered by `ReviewReport`, which owns the ordering.\n *\n * The rendering moved OUT of this command so the ordering could be asserted on as a string. It had a\n * real bug: the zero-checklist notice signed off with \"Carry on and run: pnpm wp-finish-upsert-pr\"\n * while the block right beneath it said to write review.json first. An agent that reads top to bottom\n * obeyed the first line and posted a PR with no review at all.\n */\n // eslint-disable-next-line @typescript-eslint/max-params\n private report(\n repoRoot: string, featureName: string, scan: ChecklistScan, briefings: readonly ReviewerBriefing[],\n opts: ReviewUpsertPrOptions,\n ): void {\n const input = new ReviewReportInput(repoRoot, featureName, scan.reviewPath);\n input.definedCount = scan.defined.length;\n input.applicableCount = scan.applicable.length;\n input.reviewed = scan.reviewed.slice();\n input.formatErrors = scan.formatErrors.slice();\n input.briefings = briefings.slice();\n input.refused = this.refusals(scan);\n input.skipOptional = opts.skipOptional;\n process.stdout.write(this.reviewReport.render(input));\n }\n\n /**\n * The applicable checklists that ALREADY ANSWERED and refused, each with its refusal rendered by the ONE\n * renderer `wp-finish-upsert-pr` also uses.\n *\n * Stage ② had the identical defect finish did: a refused checklist has no passing verdict, so it is not\n * in `scan.reviewed`, so it was printed as an ordinary owed reviewer and re-instructed word for word —\n * and an agent that obeys spawns it against unchanged code, gets the same refusal, and loops one stage\n * earlier than the loop that was reported.\n *\n * NO archive path is passed, deliberately, and this stage moves nothing. Retiring a verdict is finish's\n * act on the refusal it is actually enforcing; doing it here would delete the live verdict of a branch\n * that has not even been asked to finish yet, and `refusalError`'s un-archived wording — \"set an override\n * in review-<id>.json\" — is only correct while that file is still there, which here it is.\n */\n private refusals(scan: ChecklistScan): RefusedReviewer[] {\n const refused = this.reviewJsonService.refusedChecklists(scan.applicable, scan.results);\n return refused.map((req: RequiredChecklist): RefusedReviewer => new RefusedReviewer(\n req.id, this.reviewJsonService.refusalError(req, this.reviewJsonService.resolveVerdict(req, scan.results))));\n }\n}\n"]}
|
|
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 {\n loadAndValidate, writeTemplate, PrGateConfig, RepoRootFinder, RequiredChecklist,\n ReviewerBriefing, ReviewerInstructionsService, ReviewJsonService,\n} from '@webpieces/rules-config';\nimport { injectable, bindingScopeValues } from 'inversify';\nimport { ActiveHatch, ActiveHatchReport } from '../workflow/active-hatches';\nimport { AiBranchName } from '../workflow/git-readAiBranchName';\nimport { BuildAffected, BuildGateOptions } from '../workflow/build-affected';\nimport { REVIEW_STAGE } from '../workflow/build-gate-log';\nimport { BuildArtifactGate } from '../workflow/build-artifact-gate';\nimport { ChecklistScan, ChecklistScanOptions, ChecklistScanner } from '../workflow/checklist-scanner';\nimport { 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 { PrContextWriter } from '../workflow/pr-context-writer';\nimport { ReviewerBriefingBuilder } from '../workflow/reviewer-briefing-builder';\nimport { RefusedReviewer, ReviewReport, ReviewReportInput } from '../workflow/review-report';\nimport { ReviewStageReceipt, ReviewStageReceiptService } from '../workflow/review-stage-receipt';\nimport { StageOutputLog, REVIEW_CONSOLE_LOG } from '../workflow/stage-output-log';\n\nconst SEP = '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\\n';\n\n/** The `--no-optional` decision, as data (per CLAUDE.md) rather than a bare boolean parameter. */\nexport class ReviewUpsertPrOptions {\n /**\n * true ⇒ the human ALREADY said to submit without the optional reviews, so stage ② must not print the\n * block telling the AI to offer them. It changes nothing about what BLOCKS: required checklists are\n * unaffected, and an optional checklist that already has a red verdict on this branch still refuses the\n * PR at finish. This only suppresses the offer.\n */\n skipOptional: boolean;\n\n constructor(skipOptional = false) {\n this.skipOptional = skipOptional;\n }\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 buildArtifactGate: BuildArtifactGate,\n private readonly mergeState: MergeState,\n private readonly mergeEnd: MergeEnd,\n private readonly checklistScanner: ChecklistScanner,\n private readonly reviewReport: ReviewReport,\n private readonly materializer: DiffMaterializer,\n private readonly prContextWriter: PrContextWriter,\n private readonly briefingBuilder: ReviewerBriefingBuilder,\n private readonly reviewerInstructions: ReviewerInstructionsService,\n private readonly receipts: ReviewStageReceiptService,\n private readonly activeHatchReport: ActiveHatchReport,\n // Injected only to RESOLVE + RENDER refusals (see refusals()). This stage never archives a verdict.\n private readonly reviewJsonService: ReviewJsonService,\n private readonly stageConsole: StageOutputLog,\n ) {}\n\n /**\n * Everything below runs with this stage's console CAPTURED to a file (see StageOutputLog). Only the\n * lines an agent must act on before it can do anything else reach the terminal — the build heartbeat\n * and result, and the closing instruction block. The merge validation, the hatch report, the checklist\n * scan and the diff extraction are all in the file, one `grep` away.\n *\n * This is the FIRST half of the fix for the piped-command watchdog kill: an agent pipes a command it\n * expects to flood, a pipe withholds every byte until exit, and the harness kills a command that has\n * printed nothing for 600 seconds — after a full build. Guarding the pipe without first shrinking the\n * output would only have traded that kill for a flooded context.\n */\n async run(opts: ReviewUpsertPrOptions = new ReviewUpsertPrOptions()): Promise<void> {\n const repoRoot = this.repoRootFinder.resolveRepoRoot(process.cwd());\n await this.stageConsole.withCapture(\n repoRoot, REVIEW_CONSOLE_LOG, (): Promise<void> => this.runStage(repoRoot, opts));\n }\n\n private async runStage(repoRoot: string, opts: ReviewUpsertPrOptions): Promise<void> {\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 = await this.runBuildGate(repoRoot);\n\n const scan = this.checklistScanner.scan(repoRoot, config.checklists, new ChecklistScanOptions(false, '')); // '' — THIS command writes the context itself, after materializing\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.reportActiveHatches(repoRoot);\n this.report(repoRoot, featureName, scan, briefings, opts);\n }\n\n /**\n * List every rule that is currently switched off in webpieces.config.json. NON-BLOCKING and printed\n * before the instruction block, because it is context for the review rather than a step in it.\n *\n * A hatch is the one config change that makes the build QUIETER, so no gate, reviewer or red check\n * will ever mention it — which is how a repo ends up with rules off for weeks that nobody chose to\n * leave off. This is the only place in the flow that says so out loud.\n */\n private reportActiveHatches(repoRoot: string): void {\n const hatches: ActiveHatch[] = this.activeHatchReport.scan(repoRoot);\n process.stdout.write(this.activeHatchReport.render(hatches));\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 async runBuildGate(repoRoot: string): Promise<string> {\n await this.buildAffected.runBuildGate(repoRoot, new BuildGateOptions(\n '🛠️ Build gate',\n 'pnpm wp-review-upsert-pr',\n 'Build failed — NO reviewer was briefed and no diff was extracted. Fix it, then re-run.',\n REVIEW_STAGE,\n ));\n // Repo-wide: the build must not have left anything uncommitted AND unstaged. Runs HERE, not in\n // finish, because this is the stage that ran buildCommand and is therefore holding the dirty\n // tree at the exact moment the question is answerable. Replaces the per-project\n // validate-di-graph-unchanged nx target — see BuildArtifactGate for why that one had to go.\n this.buildArtifactGate.assertBuildLeftNothingUncommitted(repoRoot);\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 // Write pr-context.json ONCE, here — after materializing, so `diffDir` is populated on the first\n // write. The scan deliberately did not write it (ChecklistScanOptions.contextStage === ''); it used\n // to, which meant this command wrote the same file twice, the first time with an empty diffDir.\n // `scan.changedFiles` is passed through so the context is not recomputed from a second git call.\n scan.context = this.prContextWriter.ensure(\n repoRoot, featureName, scan.basis, 'stage2-review', scan.changedFiles, 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 return briefings;\n }\n\n /**\n * What the AI must do next — rendered by `ReviewReport`, which owns the ordering.\n *\n * The rendering moved OUT of this command so the ordering could be asserted on as a string. It had a\n * real bug: the zero-checklist notice signed off with \"Carry on and run: pnpm wp-finish-upsert-pr\"\n * while the block right beneath it said to write review.json first. An agent that reads top to bottom\n * obeyed the first line and posted a PR with no review at all.\n */\n // eslint-disable-next-line @typescript-eslint/max-params\n private report(\n repoRoot: string, featureName: string, scan: ChecklistScan, briefings: readonly ReviewerBriefing[],\n opts: ReviewUpsertPrOptions,\n ): void {\n const input = new ReviewReportInput(repoRoot, featureName, scan.reviewPath);\n input.definedCount = scan.defined.length;\n input.applicableCount = scan.applicable.length;\n input.reviewed = scan.reviewed.slice();\n input.formatErrors = scan.formatErrors.slice();\n input.briefings = briefings.slice();\n input.refused = this.refusals(scan);\n input.skipOptional = opts.skipOptional;\n // `say`: this block IS the next action — which reviewers to spawn, where review.json goes, and\n // the command after that. Capturing it into the log would leave the terminal with a pointer and\n // no instruction, which is the one thing this stage may never do.\n this.stageConsole.say(this.reviewReport.render(input));\n }\n\n /**\n * The applicable checklists that ALREADY ANSWERED and refused, each with its refusal rendered by the ONE\n * renderer `wp-finish-upsert-pr` also uses.\n *\n * Stage ② had the identical defect finish did: a refused checklist has no passing verdict, so it is not\n * in `scan.reviewed`, so it was printed as an ordinary owed reviewer and re-instructed word for word —\n * and an agent that obeys spawns it against unchanged code, gets the same refusal, and loops one stage\n * earlier than the loop that was reported.\n *\n * NO archive path is passed, deliberately, and this stage moves nothing. Retiring a verdict is finish's\n * act on the refusal it is actually enforcing; doing it here would delete the live verdict of a branch\n * that has not even been asked to finish yet, and `refusalError`'s un-archived wording — \"set an override\n * in review-<id>.json\" — is only correct while that file is still there, which here it is.\n */\n private refusals(scan: ChecklistScan): RefusedReviewer[] {\n const refused = this.reviewJsonService.refusedChecklists(scan.applicable, scan.results);\n return refused.map((req: RequiredChecklist): RefusedReviewer => new RefusedReviewer(\n req.id, this.reviewJsonService.refusalError(req, this.reviewJsonService.resolveVerdict(req, scan.results))));\n }\n}\n"]}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { BuildsLog } from '@webpieces/rules-config';
|
|
2
2
|
import { BuildGateLog } from './build-gate-log';
|
|
3
|
+
import { StageOutputLog } from './stage-output-log';
|
|
3
4
|
/**
|
|
4
5
|
* The caller-supplied framing for the build gate (label, re-run command, failure headline, stage id). Kept
|
|
5
6
|
* as a parameter object so runBuildGate stays agnostic of who invokes it. A class (not an object literal)
|
|
@@ -10,42 +11,19 @@ export declare class BuildGateOptions {
|
|
|
10
11
|
rerunCommand: string;
|
|
11
12
|
failureHeadline: string;
|
|
12
13
|
stage: string;
|
|
13
|
-
|
|
14
|
-
constructor(label: string, rerunCommand: string, failureHeadline: string, stage: string, alwaysCapture: boolean);
|
|
14
|
+
constructor(label: string, rerunCommand: string, failureHeadline: string, stage: string);
|
|
15
15
|
}
|
|
16
16
|
/** Runs the authoritative nx-affected build gate for wp-review-upsert-pr and wp-finish-upsert-pr. */
|
|
17
17
|
export declare class BuildAffected {
|
|
18
|
-
private readonly homeConfig;
|
|
19
18
|
private readonly buildLog;
|
|
20
19
|
private readonly buildsLog;
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
* EXPERIMENTAL, and OFF unless the OPTIONAL machine-local `~/.webpieces/config.json` turns it on.
|
|
24
|
-
*
|
|
25
|
-
* That file does not exist for essentially anyone, and its absence is not an error, a warning or a
|
|
26
|
-
* behaviour change of any kind — `HomeConfigService.load` returns all-defaults silently. False here is
|
|
27
|
-
* therefore the state every consumer is in, and false means runBuildGate executes exactly the code it
|
|
28
|
-
* executed before this feature existed.
|
|
29
|
-
*/
|
|
30
|
-
isCaptureEnabled(): boolean;
|
|
20
|
+
private readonly stageConsole;
|
|
21
|
+
constructor(buildLog: BuildGateLog, buildsLog: BuildsLog, stageConsole: StageOutputLog);
|
|
31
22
|
/**
|
|
32
23
|
* Resolve the exact build command this gate will run: the project's configured
|
|
33
24
|
* PrGateConfig.buildCommand, or the default affected-ci command when none is set.
|
|
34
25
|
*/
|
|
35
26
|
resolveBuildCommand(repoRoot: string): string;
|
|
36
|
-
/**
|
|
37
|
-
* Spawn the resolved build command, streamed to the terminal. Returns the exit code (0 = pass).
|
|
38
|
-
*
|
|
39
|
-
* PRIVATE, and that is the point. This and `runConfiguredBuildGate` used to be PUBLIC side doors that
|
|
40
|
-
* spawned `buildCommand` with no caller identity and no ledger row — so "run the build" had three
|
|
41
|
-
* spellings, two of which were invisible to `~/.webpieces/builds.log`. They are folded into this one
|
|
42
|
-
* private method, reachable only through `runBuildGate`, which means an unlogged build no longer
|
|
43
|
-
* COMPILES. That is the one-spelling rule from CLAUDE.md § "NO webpieces surface is released
|
|
44
|
-
* backwards-compatible" applied to a behaviour rather than a type.
|
|
45
|
-
*
|
|
46
|
-
* Prints NOTHING itself — `runBuildGate` announces the command in one line.
|
|
47
|
-
*/
|
|
48
|
-
private spawnBuild;
|
|
49
27
|
/**
|
|
50
28
|
* Run the configured build gate with consistent framing, throwing CliExitError(buildCode) on
|
|
51
29
|
* failure so the bin's main()/runMain owns the exit. THE single source of truth for running a build:
|
|
@@ -63,9 +41,12 @@ export declare class BuildAffected {
|
|
|
63
41
|
*/
|
|
64
42
|
runBuildGate(repoRoot: string, opts: BuildGateOptions): Promise<void>;
|
|
65
43
|
/**
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
44
|
+
* The failure text: this gate's own headline, then the pointer at the log the build already wrote.
|
|
45
|
+
*
|
|
46
|
+
* There is no second, streamed variant any more. Capturing used to be behind an EXPERIMENTAL
|
|
47
|
+
* machine-local opt-in, which meant the refusal an agent read depended on a file almost nobody had —
|
|
48
|
+
* and the un-captured branch's advice was "run the build again yourself", which is the single most
|
|
49
|
+
* expensive thing an agent can be told. Reading a FILE is now the only answer this gate gives.
|
|
69
50
|
*/
|
|
70
51
|
private failureText;
|
|
71
52
|
}
|
|
@@ -2,17 +2,16 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.BuildAffected = exports.BuildGateOptions = void 0;
|
|
4
4
|
const tslib_1 = require("tslib");
|
|
5
|
-
const child_process_1 = require("child_process");
|
|
6
5
|
const rules_config_1 = require("@webpieces/rules-config");
|
|
7
6
|
const inversify_1 = require("inversify");
|
|
8
7
|
const build_gate_log_1 = require("./build-gate-log");
|
|
8
|
+
const stage_output_log_1 = require("./stage-output-log");
|
|
9
9
|
// Single source of truth for RUNNING the build gate. `wp-start-upsert-pr` runs NO build; stage ②
|
|
10
10
|
// (`wp-review-upsert-pr`) runs it authoritatively before any reviewer is spawned, and stage ③
|
|
11
11
|
// (`wp-finish-upsert-pr`) re-runs it only when HEAD has moved since. nx `affected` only rebuilds
|
|
12
12
|
// changed projects. The fallback command itself lives in @webpieces/rules-config as
|
|
13
13
|
// DEFAULT_BUILD_COMMAND — whole-repo-build-guard prints the same string, and one definition is what
|
|
14
14
|
// keeps the refusal message naming the build that actually runs.
|
|
15
|
-
const SEP = '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n';
|
|
16
15
|
/**
|
|
17
16
|
* The caller-supplied framing for the build gate (label, re-run command, failure headline, stage id). Kept
|
|
18
17
|
* as a parameter object so runBuildGate stays agnostic of who invokes it. A class (not an object literal)
|
|
@@ -25,40 +24,23 @@ class BuildGateOptions {
|
|
|
25
24
|
// WHICH stage's gate this is — REVIEW_STAGE, FINISH_STAGE or BUILD_STAGE. Required, with no default:
|
|
26
25
|
// it decides the captured log's filename, and a default would silently make two stages share one file.
|
|
27
26
|
stage;
|
|
28
|
-
|
|
29
|
-
// entire contract IS the log file — its console output is a heartbeat and a pointer at that file, so a
|
|
30
|
-
// wp-build that did not capture would have nothing to point at. The PR-flow stages pass false and stay
|
|
31
|
-
// on the opt-in until the experiment lands for them too.
|
|
32
|
-
alwaysCapture;
|
|
33
|
-
constructor(label, rerunCommand, failureHeadline, stage, alwaysCapture) {
|
|
27
|
+
constructor(label, rerunCommand, failureHeadline, stage) {
|
|
34
28
|
this.label = label;
|
|
35
29
|
this.rerunCommand = rerunCommand;
|
|
36
30
|
this.failureHeadline = failureHeadline;
|
|
37
31
|
this.stage = stage;
|
|
38
|
-
this.alwaysCapture = alwaysCapture;
|
|
39
32
|
}
|
|
40
33
|
}
|
|
41
34
|
exports.BuildGateOptions = BuildGateOptions;
|
|
42
35
|
/** Runs the authoritative nx-affected build gate for wp-review-upsert-pr and wp-finish-upsert-pr. */
|
|
43
36
|
let BuildAffected = class BuildAffected {
|
|
44
|
-
homeConfig;
|
|
45
37
|
buildLog;
|
|
46
38
|
buildsLog;
|
|
47
|
-
|
|
48
|
-
|
|
39
|
+
stageConsole;
|
|
40
|
+
constructor(buildLog, buildsLog, stageConsole) {
|
|
49
41
|
this.buildLog = buildLog;
|
|
50
42
|
this.buildsLog = buildsLog;
|
|
51
|
-
|
|
52
|
-
/**
|
|
53
|
-
* EXPERIMENTAL, and OFF unless the OPTIONAL machine-local `~/.webpieces/config.json` turns it on.
|
|
54
|
-
*
|
|
55
|
-
* That file does not exist for essentially anyone, and its absence is not an error, a warning or a
|
|
56
|
-
* behaviour change of any kind — `HomeConfigService.load` returns all-defaults silently. False here is
|
|
57
|
-
* therefore the state every consumer is in, and false means runBuildGate executes exactly the code it
|
|
58
|
-
* executed before this feature existed.
|
|
59
|
-
*/
|
|
60
|
-
isCaptureEnabled() {
|
|
61
|
-
return this.homeConfig.load().buildGateLogCapture;
|
|
43
|
+
this.stageConsole = stageConsole;
|
|
62
44
|
}
|
|
63
45
|
/**
|
|
64
46
|
* Resolve the exact build command this gate will run: the project's configured
|
|
@@ -68,24 +50,6 @@ let BuildAffected = class BuildAffected {
|
|
|
68
50
|
const configured = (0, rules_config_1.loadAndValidate)(repoRoot).prGate.buildCommand;
|
|
69
51
|
return configured !== undefined && configured.trim() !== '' ? configured : rules_config_1.DEFAULT_BUILD_COMMAND;
|
|
70
52
|
}
|
|
71
|
-
/**
|
|
72
|
-
* Spawn the resolved build command, streamed to the terminal. Returns the exit code (0 = pass).
|
|
73
|
-
*
|
|
74
|
-
* PRIVATE, and that is the point. This and `runConfiguredBuildGate` used to be PUBLIC side doors that
|
|
75
|
-
* spawned `buildCommand` with no caller identity and no ledger row — so "run the build" had three
|
|
76
|
-
* spellings, two of which were invisible to `~/.webpieces/builds.log`. They are folded into this one
|
|
77
|
-
* private method, reachable only through `runBuildGate`, which means an unlogged build no longer
|
|
78
|
-
* COMPILES. That is the one-spelling rule from CLAUDE.md § "NO webpieces surface is released
|
|
79
|
-
* backwards-compatible" applied to a behaviour rather than a type.
|
|
80
|
-
*
|
|
81
|
-
* Prints NOTHING itself — `runBuildGate` announces the command in one line.
|
|
82
|
-
*/
|
|
83
|
-
spawnBuild(repoRoot) {
|
|
84
|
-
const configured = (0, rules_config_1.loadAndValidate)(repoRoot).prGate.buildCommand;
|
|
85
|
-
const cmd = configured !== undefined && configured.trim() !== '' ? configured : rules_config_1.DEFAULT_BUILD_COMMAND;
|
|
86
|
-
const result = (0, child_process_1.spawnSync)(cmd, { stdio: 'inherit', cwd: repoRoot, shell: true });
|
|
87
|
-
return result.status ?? 1;
|
|
88
|
-
}
|
|
89
53
|
/**
|
|
90
54
|
* Run the configured build gate with consistent framing, throwing CliExitError(buildCode) on
|
|
91
55
|
* failure so the bin's main()/runMain owns the exit. THE single source of truth for running a build:
|
|
@@ -106,46 +70,46 @@ let BuildAffected = class BuildAffected {
|
|
|
106
70
|
// TWO lines on the happy path — the command, then the result. The old framing spent a banner and a
|
|
107
71
|
// paragraph explaining how to reproduce a build that was about to pass anyway; that explanation is
|
|
108
72
|
// only useful when the build FAILS, so it now lives solely on the failure path below.
|
|
109
|
-
|
|
110
|
-
//
|
|
111
|
-
//
|
|
112
|
-
//
|
|
113
|
-
|
|
73
|
+
//
|
|
74
|
+
// `say`, so it survives a stage that is capturing its own console (see StageOutputLog): the
|
|
75
|
+
// command being run and the result of running it are exactly what a caller watching a long build
|
|
76
|
+
// needs to see live.
|
|
77
|
+
this.stageConsole.say(`\n${opts.label}: ${buildCommand}\n`);
|
|
78
|
+
const logPath = this.buildLog.pathFor(repoRoot, opts.stage);
|
|
114
79
|
const ticket = this.buildsLog.start(opts.stage, repoRoot);
|
|
115
80
|
let buildCode = 1;
|
|
116
81
|
// webpieces-disable no-unmanaged-exceptions -- chokepoint: the DONE row must be written whether the
|
|
117
82
|
// build passed, failed, or the spawn itself blew up; the throw is re-raised untouched below
|
|
118
83
|
// eslint-disable-next-line @webpieces/no-unmanaged-exceptions
|
|
119
84
|
try {
|
|
120
|
-
buildCode =
|
|
121
|
-
? this.spawnBuild(repoRoot)
|
|
122
|
-
: await this.buildLog.run(repoRoot, buildCommand, logPath);
|
|
85
|
+
buildCode = await this.buildLog.run(repoRoot, buildCommand, logPath);
|
|
123
86
|
}
|
|
124
87
|
finally {
|
|
125
88
|
this.buildsLog.finish(ticket, buildCode);
|
|
126
89
|
}
|
|
127
90
|
if (buildCode !== 0)
|
|
128
91
|
throw new rules_config_1.CliExitError(buildCode, this.failureText(opts, buildCommand, logPath));
|
|
129
|
-
|
|
92
|
+
this.stageConsole.say(this.buildLog.successMessage(logPath));
|
|
130
93
|
}
|
|
131
94
|
/**
|
|
132
|
-
*
|
|
133
|
-
*
|
|
134
|
-
*
|
|
95
|
+
* The failure text: this gate's own headline, then the pointer at the log the build already wrote.
|
|
96
|
+
*
|
|
97
|
+
* There is no second, streamed variant any more. Capturing used to be behind an EXPERIMENTAL
|
|
98
|
+
* machine-local opt-in, which meant the refusal an agent read depended on a file almost nobody had —
|
|
99
|
+
* and the un-captured branch's advice was "run the build again yourself", which is the single most
|
|
100
|
+
* expensive thing an agent can be told. Reading a FILE is now the only answer this gate gives.
|
|
135
101
|
*/
|
|
136
102
|
failureText(opts, buildCommand, logPath) {
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
`Run THIS exact command to reproduce and fix all errors, then re-run ${opts.rerunCommand}:\n\n` +
|
|
141
|
-
` ${buildCommand}\n`;
|
|
103
|
+
return `\n❌ ${opts.failureHeadline}\n` +
|
|
104
|
+
this.buildLog.failureMessage(buildCommand, logPath) +
|
|
105
|
+
`Fix what that log shows, then re-run ${opts.rerunCommand}.\n`;
|
|
142
106
|
}
|
|
143
107
|
};
|
|
144
108
|
exports.BuildAffected = BuildAffected;
|
|
145
109
|
exports.BuildAffected = BuildAffected = tslib_1.__decorate([
|
|
146
110
|
(0, inversify_1.injectable)(inversify_1.bindingScopeValues.Singleton),
|
|
147
|
-
tslib_1.__metadata("design:paramtypes", [
|
|
148
|
-
|
|
149
|
-
|
|
111
|
+
tslib_1.__metadata("design:paramtypes", [build_gate_log_1.BuildGateLog,
|
|
112
|
+
rules_config_1.BuildsLog,
|
|
113
|
+
stage_output_log_1.StageOutputLog])
|
|
150
114
|
], BuildAffected);
|
|
151
115
|
//# sourceMappingURL=build-affected.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"build-affected.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/pr-gate/src/scripts/workflow/build-affected.ts"],"names":[],"mappings":"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","sourcesContent":["import { spawnSync } from 'child_process';\nimport {\n loadAndValidate, CliExitError, HomeConfigService, DEFAULT_BUILD_COMMAND, BuildsLog,\n} from '@webpieces/rules-config';\nimport { injectable, bindingScopeValues } from 'inversify';\nimport { BuildGateLog } from './build-gate-log';\n\n// Single source of truth for RUNNING the build gate. `wp-start-upsert-pr` runs NO build; stage ②\n// (`wp-review-upsert-pr`) runs it authoritatively before any reviewer is spawned, and stage ③\n// (`wp-finish-upsert-pr`) re-runs it only when HEAD has moved since. nx `affected` only rebuilds\n// changed projects. The fallback command itself lives in @webpieces/rules-config as\n// DEFAULT_BUILD_COMMAND — whole-repo-build-guard prints the same string, and one definition is what\n// keeps the refusal message naming the build that actually runs.\n\nconst SEP = '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\\n';\n\n/**\n * The caller-supplied framing for the build gate (label, re-run command, failure headline, stage id). Kept\n * as a parameter object so runBuildGate stays agnostic of who invokes it. A class (not an object literal)\n * per the codebase's data-structure convention.\n */\nexport class BuildGateOptions {\n label: string; // section header shown above the gate\n rerunCommand: string; // command the AI re-runs after fixing the build\n failureHeadline: string; // first line printed on failure\n // WHICH stage's gate this is — REVIEW_STAGE, FINISH_STAGE or BUILD_STAGE. Required, with no default:\n // it decides the captured log's filename, and a default would silently make two stages share one file.\n stage: string;\n // Capture regardless of the EXPERIMENTAL `~/.webpieces/config.json` opt-in. True for `wp-build`, whose\n // entire contract IS the log file — its console output is a heartbeat and a pointer at that file, so a\n // wp-build that did not capture would have nothing to point at. The PR-flow stages pass false and stay\n // on the opt-in until the experiment lands for them too.\n alwaysCapture: boolean;\n\n constructor(label: string, rerunCommand: string, failureHeadline: string, stage: string, alwaysCapture: boolean) {\n this.label = label;\n this.rerunCommand = rerunCommand;\n this.failureHeadline = failureHeadline;\n this.stage = stage;\n this.alwaysCapture = alwaysCapture;\n }\n}\n\n/** Runs the authoritative nx-affected build gate for wp-review-upsert-pr and wp-finish-upsert-pr. */\n@injectable(bindingScopeValues.Singleton)\nexport class BuildAffected {\n constructor(\n private readonly homeConfig: HomeConfigService,\n private readonly buildLog: BuildGateLog,\n private readonly buildsLog: BuildsLog,\n ) {}\n\n /**\n * EXPERIMENTAL, and OFF unless the OPTIONAL machine-local `~/.webpieces/config.json` turns it on.\n *\n * That file does not exist for essentially anyone, and its absence is not an error, a warning or a\n * behaviour change of any kind — `HomeConfigService.load` returns all-defaults silently. False here is\n * therefore the state every consumer is in, and false means runBuildGate executes exactly the code it\n * executed before this feature existed.\n */\n isCaptureEnabled(): boolean {\n return this.homeConfig.load().buildGateLogCapture;\n }\n\n /**\n * Resolve the exact build command this gate will run: the project's configured\n * PrGateConfig.buildCommand, or the default affected-ci command when none is set.\n */\n resolveBuildCommand(repoRoot: string): string {\n const configured = loadAndValidate(repoRoot).prGate.buildCommand;\n return configured !== undefined && configured.trim() !== '' ? configured : DEFAULT_BUILD_COMMAND;\n }\n\n /**\n * Spawn the resolved build command, streamed to the terminal. Returns the exit code (0 = pass).\n *\n * PRIVATE, and that is the point. This and `runConfiguredBuildGate` used to be PUBLIC side doors that\n * spawned `buildCommand` with no caller identity and no ledger row — so \"run the build\" had three\n * spellings, two of which were invisible to `~/.webpieces/builds.log`. They are folded into this one\n * private method, reachable only through `runBuildGate`, which means an unlogged build no longer\n * COMPILES. That is the one-spelling rule from CLAUDE.md § \"NO webpieces surface is released\n * backwards-compatible\" applied to a behaviour rather than a type.\n *\n * Prints NOTHING itself — `runBuildGate` announces the command in one line.\n */\n private spawnBuild(repoRoot: string): number {\n const configured = loadAndValidate(repoRoot).prGate.buildCommand;\n const cmd = configured !== undefined && configured.trim() !== '' ? configured : DEFAULT_BUILD_COMMAND;\n const result = spawnSync(cmd, { stdio: 'inherit', cwd: repoRoot, shell: true });\n return result.status ?? 1;\n }\n\n /**\n * Run the configured build gate with consistent framing, throwing CliExitError(buildCode) on\n * failure so the bin's main()/runMain owns the exit. THE single source of truth for running a build:\n * wp-build, wp-review-upsert-pr and wp-finish-upsert-pr all call THIS (only the BuildGateOptions\n * differ), and there is no other way to reach the spawn.\n *\n * ─── EVERY BUILD ON THIS MACHINE IS LEDGERED FROM HERE ───────────────────────────────────────────\n * A START row goes into `~/.webpieces/builds.log` before the spawn and a DONE row after it, in a\n * `finally` so a throw on the failure path still closes the pair. `opts.stage` — `build` | `review` |\n * `finish` — IS the caller id the row records; there is deliberately no second `caller` field, since\n * two spellings of one fact is the shim CLAUDE.md rejects.\n *\n * The ledger is best-effort by construction (see BuildsLog): it can never throw, so no build can die\n * because a log file was busy.\n */\n async runBuildGate(repoRoot: string, opts: BuildGateOptions): Promise<void> {\n const buildCommand = this.resolveBuildCommand(repoRoot);\n // TWO lines on the happy path — the command, then the result. The old framing spent a banner and a\n // paragraph explaining how to reproduce a build that was about to pass anyway; that explanation is\n // only useful when the build FAILS, so it now lives solely on the failure path below.\n process.stdout.write(`\\n${opts.label}: ${buildCommand}\\n`);\n // '' means NOT capturing: a PR-flow stage on a machine that has not created the OPTIONAL\n // `~/.webpieces/config.json`. Everything below then runs exactly the code it ran before capture\n // existed — same spawn, streamed to the terminal, same message, no extra file.\n const logPath = opts.alwaysCapture || this.isCaptureEnabled() ? this.buildLog.pathFor(repoRoot, opts.stage) : '';\n const ticket = this.buildsLog.start(opts.stage, repoRoot);\n let buildCode = 1;\n // webpieces-disable no-unmanaged-exceptions -- chokepoint: the DONE row must be written whether the\n // build passed, failed, or the spawn itself blew up; the throw is re-raised untouched below\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n buildCode = logPath === ''\n ? this.spawnBuild(repoRoot)\n : await this.buildLog.run(repoRoot, buildCommand, logPath);\n } finally {\n this.buildsLog.finish(ticket, buildCode);\n }\n if (buildCode !== 0) throw new CliExitError(buildCode, this.failureText(opts, buildCommand, logPath));\n process.stdout.write(logPath === '' ? '\\n✅ Build passed.\\n' : this.buildLog.successMessage(logPath));\n }\n\n /**\n * On failure, WITHOUT capture: the pre-existing text, which tells the AI to re-run the build itself.\n * WITH capture: a deliberately tiny pointer at the log the gate already wrote — the whole point of the\n * feature is that the agent reads one file instead of rebuilding the repo and eating the transcript.\n */\n private failureText(opts: BuildGateOptions, buildCommand: string, logPath: string): string {\n if (logPath !== '') return this.buildLog.failureMessage(buildCommand, logPath);\n return `\\n❌ ${opts.failureHeadline}\\n\\n` +\n `Run THIS exact command to reproduce and fix all errors, then re-run ${opts.rerunCommand}:\\n\\n` +\n ` ${buildCommand}\\n`;\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"build-affected.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/pr-gate/src/scripts/workflow/build-affected.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n loadAndValidate, CliExitError, DEFAULT_BUILD_COMMAND, BuildsLog,\n} from '@webpieces/rules-config';\nimport { injectable, bindingScopeValues } from 'inversify';\nimport { BuildGateLog } from './build-gate-log';\nimport { StageOutputLog } from './stage-output-log';\n\n// Single source of truth for RUNNING the build gate. `wp-start-upsert-pr` runs NO build; stage ②\n// (`wp-review-upsert-pr`) runs it authoritatively before any reviewer is spawned, and stage ③\n// (`wp-finish-upsert-pr`) re-runs it only when HEAD has moved since. nx `affected` only rebuilds\n// changed projects. The fallback command itself lives in @webpieces/rules-config as\n// DEFAULT_BUILD_COMMAND — whole-repo-build-guard prints the same string, and one definition is what\n// keeps the refusal message naming the build that actually runs.\n\n\n/**\n * The caller-supplied framing for the build gate (label, re-run command, failure headline, stage id). Kept\n * as a parameter object so runBuildGate stays agnostic of who invokes it. A class (not an object literal)\n * per the codebase's data-structure convention.\n */\nexport class BuildGateOptions {\n label: string; // section header shown above the gate\n rerunCommand: string; // command the AI re-runs after fixing the build\n failureHeadline: string; // first line printed on failure\n // WHICH stage's gate this is — REVIEW_STAGE, FINISH_STAGE or BUILD_STAGE. Required, with no default:\n // it decides the captured log's filename, and a default would silently make two stages share one file.\n stage: string;\n\n constructor(label: string, rerunCommand: string, failureHeadline: string, stage: string) {\n this.label = label;\n this.rerunCommand = rerunCommand;\n this.failureHeadline = failureHeadline;\n this.stage = stage;\n }\n}\n\n/** Runs the authoritative nx-affected build gate for wp-review-upsert-pr and wp-finish-upsert-pr. */\n@injectable(bindingScopeValues.Singleton)\nexport class BuildAffected {\n constructor(\n private readonly buildLog: BuildGateLog,\n private readonly buildsLog: BuildsLog,\n private readonly stageConsole: StageOutputLog,\n ) {}\n\n /**\n * Resolve the exact build command this gate will run: the project's configured\n * PrGateConfig.buildCommand, or the default affected-ci command when none is set.\n */\n resolveBuildCommand(repoRoot: string): string {\n const configured = loadAndValidate(repoRoot).prGate.buildCommand;\n return configured !== undefined && configured.trim() !== '' ? configured : DEFAULT_BUILD_COMMAND;\n }\n\n /**\n * Run the configured build gate with consistent framing, throwing CliExitError(buildCode) on\n * failure so the bin's main()/runMain owns the exit. THE single source of truth for running a build:\n * wp-build, wp-review-upsert-pr and wp-finish-upsert-pr all call THIS (only the BuildGateOptions\n * differ), and there is no other way to reach the spawn.\n *\n * ─── EVERY BUILD ON THIS MACHINE IS LEDGERED FROM HERE ───────────────────────────────────────────\n * A START row goes into `~/.webpieces/builds.log` before the spawn and a DONE row after it, in a\n * `finally` so a throw on the failure path still closes the pair. `opts.stage` — `build` | `review` |\n * `finish` — IS the caller id the row records; there is deliberately no second `caller` field, since\n * two spellings of one fact is the shim CLAUDE.md rejects.\n *\n * The ledger is best-effort by construction (see BuildsLog): it can never throw, so no build can die\n * because a log file was busy.\n */\n async runBuildGate(repoRoot: string, opts: BuildGateOptions): Promise<void> {\n const buildCommand = this.resolveBuildCommand(repoRoot);\n // TWO lines on the happy path — the command, then the result. The old framing spent a banner and a\n // paragraph explaining how to reproduce a build that was about to pass anyway; that explanation is\n // only useful when the build FAILS, so it now lives solely on the failure path below.\n //\n // `say`, so it survives a stage that is capturing its own console (see StageOutputLog): the\n // command being run and the result of running it are exactly what a caller watching a long build\n // needs to see live.\n this.stageConsole.say(`\\n${opts.label}: ${buildCommand}\\n`);\n const logPath = this.buildLog.pathFor(repoRoot, opts.stage);\n const ticket = this.buildsLog.start(opts.stage, repoRoot);\n let buildCode = 1;\n // webpieces-disable no-unmanaged-exceptions -- chokepoint: the DONE row must be written whether the\n // build passed, failed, or the spawn itself blew up; the throw is re-raised untouched below\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n buildCode = await this.buildLog.run(repoRoot, buildCommand, logPath);\n } finally {\n this.buildsLog.finish(ticket, buildCode);\n }\n if (buildCode !== 0) throw new CliExitError(buildCode, this.failureText(opts, buildCommand, logPath));\n this.stageConsole.say(this.buildLog.successMessage(logPath));\n }\n\n /**\n * The failure text: this gate's own headline, then the pointer at the log the build already wrote.\n *\n * There is no second, streamed variant any more. Capturing used to be behind an EXPERIMENTAL\n * machine-local opt-in, which meant the refusal an agent read depended on a file almost nobody had —\n * and the un-captured branch's advice was \"run the build again yourself\", which is the single most\n * expensive thing an agent can be told. Reading a FILE is now the only answer this gate gives.\n */\n private failureText(opts: BuildGateOptions, buildCommand: string, logPath: string): string {\n return `\\n❌ ${opts.failureHeadline}\\n` +\n this.buildLog.failureMessage(buildCommand, logPath) +\n `Fix what that log shows, then re-run ${opts.rerunCommand}.\\n`;\n }\n}\n"]}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { GateLogFile } from './gate-log-file';
|
|
2
|
+
import { StageOutputLog } from './stage-output-log';
|
|
1
3
|
/** How often the heartbeat reports the log's size. Hardcoded: a knob here would be a knob the PR gate's
|
|
2
4
|
* own build never receives, and the two must stay the same command. */
|
|
3
5
|
export declare const HEARTBEAT_MS = 10000;
|
|
@@ -22,31 +24,29 @@ export declare const BUILD_LOG_NAME = "build.log";
|
|
|
22
24
|
* BUILD from a stalled REPORTER.
|
|
23
25
|
*/
|
|
24
26
|
export declare class BuildLogHeartbeat {
|
|
27
|
+
private readonly files;
|
|
25
28
|
private readonly logPath;
|
|
26
29
|
private readonly displayPath;
|
|
27
30
|
private previous;
|
|
28
|
-
constructor(logPath: string, displayPath: string);
|
|
31
|
+
constructor(files: GateLogFile, logPath: string, displayPath: string);
|
|
29
32
|
/** One heartbeat line — `<path> size <n> lines`, plus ` still` when <n> has not moved. */
|
|
30
33
|
tick(): string;
|
|
31
|
-
private lineCount;
|
|
32
34
|
}
|
|
33
35
|
/**
|
|
34
36
|
* Captures the build gate's full output to a file, reports progress while it runs, and renders the
|
|
35
37
|
* pointer the caller is handed instead of a rebuild instruction.
|
|
36
38
|
*
|
|
39
|
+
* WHERE the file goes, how the previous run is kept, and what the pointer reads like are `GateLogFile`'s
|
|
40
|
+
* — one mechanism, shared with the stage console log, so there is a single answer to "where is it?".
|
|
41
|
+
*
|
|
37
42
|
* ─── Two naming schemes, one rule each ─────────────────────────────────────────────────────────────────
|
|
38
43
|
* • `wp-build` (BUILD_STAGE) writes ONE fixed path, `.webpieces/build.log`. It is fixed because a HUMAN
|
|
39
44
|
* OR AN AGENT TYPES IT — `grep -n error .webpieces/build.log` has to be writable from memory, and a
|
|
40
|
-
* name carrying a branch and a sha is not. History comes from the rotation
|
|
45
|
+
* name carrying a branch and a sha is not. History comes from the rotation instead.
|
|
41
46
|
* • stage ② and stage ③ write `logs/build-gate-<stage>-<branch>-<shortSha>.log`, because those two
|
|
42
47
|
* gates CAN run against one commit and a failure message from one must not be pointing at a file the
|
|
43
48
|
* other overwrote. Nobody types those names; the failure message prints them.
|
|
44
49
|
*
|
|
45
|
-
* ─── Rotation, everywhere ──────────────────────────────────────────────────────────────────────────────
|
|
46
|
-
* Every run moves an existing log to `<log>.bak` before writing, so the last TWO runs are always on disk.
|
|
47
|
-
* One rule for every stage: no branch, and the previous run of a re-run at the same commit survives
|
|
48
|
-
* instead of being truncated away.
|
|
49
|
-
*
|
|
50
50
|
* ─── Concurrency ───────────────────────────────────────────────────────────────────────────────────────
|
|
51
51
|
* The DIRECTORY is `dotWebpieces.local()`-scoped — `<primary>/.webpieces/worktrees/<git worktree name>/`
|
|
52
52
|
* in a linked worktree — so "N agents in N worktrees" is safe by construction rather than by naming. The
|
|
@@ -54,23 +54,19 @@ export declare class BuildLogHeartbeat {
|
|
|
54
54
|
* driving one git index.
|
|
55
55
|
*/
|
|
56
56
|
export declare class BuildGateLog {
|
|
57
|
+
private readonly files;
|
|
58
|
+
private readonly stageConsole;
|
|
59
|
+
constructor(files: GateLogFile, stageConsole: StageOutputLog);
|
|
57
60
|
/** Absolute path of the log for `stage` at the current HEAD, creating its directory. */
|
|
58
61
|
pathFor(repoRoot: string, stage: string): string;
|
|
59
|
-
/** The same path
|
|
62
|
+
/** The same path, and '' when no log has been WRITTEN there yet. Used by finish's skip path. */
|
|
60
63
|
existingLogFor(repoRoot: string, stage: string): string;
|
|
61
64
|
/** `build.log` for `wp-build`; `build-gate-<stage>-<branch>-<shortSha>.log` for the PR-flow stages. */
|
|
62
65
|
fileNameFor(repoRoot: string, stage: string): string;
|
|
63
|
-
/** Where the PREVIOUS run of `logPath` is kept — always `<logPath>.bak`. */
|
|
64
|
-
backupPathFor(logPath: string): string;
|
|
65
|
-
/**
|
|
66
|
-
* Move an existing log aside to `<log>.bak`, overwriting any previous backup. A missing log is the
|
|
67
|
-
* normal first-run state and is not an error.
|
|
68
|
-
*/
|
|
69
|
-
rotate(logPath: string): void;
|
|
70
66
|
/**
|
|
71
67
|
* Run `buildCommand` with its stdout AND stderr redirected in full to `logPath`, printing a heartbeat
|
|
72
|
-
*
|
|
73
|
-
*
|
|
68
|
+
* every HEARTBEAT_MS so the caller can see it is alive. Returns the BUILD's exit code. Nothing is
|
|
69
|
+
* truncated and nothing is streamed.
|
|
74
70
|
*/
|
|
75
71
|
run(repoRoot: string, buildCommand: string, logPath: string): Promise<number>;
|
|
76
72
|
/**
|
|
@@ -87,18 +83,7 @@ export declare class BuildGateLog {
|
|
|
87
83
|
* agent guessing or rebuilding — so it is told to surface the contradiction to the human and stop.
|
|
88
84
|
*/
|
|
89
85
|
failureMessage(buildCommand: string, logPath: string): string;
|
|
90
|
-
private logPointer;
|
|
91
|
-
/**
|
|
92
|
-
* The log's last FAILURE_TAIL_LINES lines, or a plain statement of why there are none.
|
|
93
|
-
*
|
|
94
|
-
* A read that fails is REPORTED, never allowed to throw: this renders the message for a build that has
|
|
95
|
-
* ALREADY failed, so an I/O error escaping here would replace the real failure with the renderer's own
|
|
96
|
-
* — the caller would lose the build error and be handed a filesystem error instead. The full log is
|
|
97
|
-
* still named on the line above, so nothing is hidden by degrading to one line.
|
|
98
|
-
*/
|
|
99
|
-
private tail;
|
|
100
86
|
private awaitExit;
|
|
101
|
-
private displayPath;
|
|
102
87
|
private resolvePath;
|
|
103
88
|
private slug;
|
|
104
89
|
private git;
|