@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.
@@ -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, false));
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
- process.stdout.write(this.reviewReport.render(input));
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":";;;;AAAA,+CAAyB;AACzB,0DAGiC;AACjC,yCAA2D;AAC3D,+DAA4E;AAC5E,2EAAgE;AAChE,+DAA6E;AAC7E,+DAA0D;AAC1D,yEAAoE;AACpE,qEAAsG;AACtG,qEAAiE;AACjE,mDAA+C;AAC/C,yDAAuD;AACvD,qDAAkE;AAClE,yDAAqD;AACrD,qEAAgE;AAChE,qFAAgF;AAChF,6DAA6F;AAC7F,2EAAiG;AAEjG,MAAM,GAAG,GAAG,0DAA0D,CAAC;AAEvE,kGAAkG;AAClG,MAAa,qBAAqB;IAC9B;;;;;OAKG;IACH,YAAY,CAAU;IAEtB,YAAY,YAAY,GAAG,KAAK;QAC5B,IAAI,CAAC,YAAY,GAAG,YAAY,CAAC;IACrC,CAAC;CACJ;AAZD,sDAYC;AAED;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEI,IAAM,qBAAqB,GAA3B,MAAM,qBAAqB;IAGT;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IAEA;IAlBrB,yDAAyD;IACzD,YACqB,cAA8B,EAC9B,YAA0B,EAC1B,OAAgB,EAChB,aAA4B,EAC5B,iBAAoC,EACpC,UAAsB,EACtB,QAAkB,EAClB,gBAAkC,EAClC,YAA0B,EAC1B,YAA8B,EAC9B,eAAgC,EAChC,eAAwC,EACxC,oBAAiD,EACjD,QAAmC,EACnC,iBAAoC;IACrD,oGAAoG;IACnF,iBAAoC;QAhBpC,mBAAc,GAAd,cAAc,CAAgB;QAC9B,iBAAY,GAAZ,YAAY,CAAc;QAC1B,YAAO,GAAP,OAAO,CAAS;QAChB,kBAAa,GAAb,aAAa,CAAe;QAC5B,sBAAiB,GAAjB,iBAAiB,CAAmB;QACpC,eAAU,GAAV,UAAU,CAAY;QACtB,aAAQ,GAAR,QAAQ,CAAU;QAClB,qBAAgB,GAAhB,gBAAgB,CAAkB;QAClC,iBAAY,GAAZ,YAAY,CAAc;QAC1B,iBAAY,GAAZ,YAAY,CAAkB;QAC9B,oBAAe,GAAf,eAAe,CAAiB;QAChC,oBAAe,GAAf,eAAe,CAAyB;QACxC,yBAAoB,GAApB,oBAAoB,CAA6B;QACjD,aAAQ,GAAR,QAAQ,CAA2B;QACnC,sBAAiB,GAAjB,iBAAiB,CAAmB;QAEpC,sBAAiB,GAAjB,iBAAiB,CAAmB;IACtD,CAAC;IAEJ,KAAK,CAAC,GAAG,CAAC,OAA8B,IAAI,qBAAqB,EAAE;QAC/D,MAAM,QAAQ,GAAG,IAAI,CAAC,cAAc,CAAC,eAAe,CAAC,OAAO,CAAC,GAAG,EAAE,CAAC,CAAC;QACpE,IAAA,4BAAa,EAAC,QAAQ,EAAE,2BAA2B,CAAC,CAAC;QACrD,IAAA,4BAAa,EAAC,QAAQ,EAAE,gCAAgC,CAAC,CAAC;QAC1D,MAAM,WAAW,GAAG,IAAI,CAAC,YAAY,CAAC,cAAc,EAAE,CAAC;QACvD,MAAM,MAAM,GAAG,IAAA,8BAAe,EAAC,QAAQ,CAAC,CAAC,MAAM,CAAC;QAEhD,MAAM,cAAc,GAAG,MAAM,IAAI,CAAC,0BAA0B,CAAC,QAAQ,CAAC,CAAC;QACvE,IAAI,CAAC,OAAO,CAAC,eAAe,CAAC,QAAQ,CAAC,CAAC;QACvC,MAAM,aAAa,GAAG,MAAM,IAAI,CAAC,YAAY,CAAC,QAAQ,CAAC,CAAC;QAExD,MAAM,IAAI,GAAG,IAAI,CAAC,gBAAgB,CAAC,IAAI,CAAC,QAAQ,EAAE,MAAM,CAAC,UAAU,EAAE,IAAI,wCAAoB,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC,CAAC,CAAE,mEAAmE;QAC/K,MAAM,SAAS,GAAG,IAAI,CAAC,cAAc,CAAC,QAAQ,EAAE,WAAW,EAAE,IAAI,EAAE,MAAM,CAAC,CAAC;QAC3E,IAAI,CAAC,QAAQ,CAAC,KAAK,CAAC,QAAQ,EAAE,WAAW,EAAE,IAAI,yCAAkB,CAC7D,IAAI,CAAC,KAAK,CAAC,OAAO,EAAE,cAAc,EAAE,IAAI,CAAC,aAAa,CAAC,mBAAmB,CAAC,QAAQ,CAAC,EAAE,aAAa,EACnG,SAAS,CAAC,GAAG,CAAC,CAAC,CAAmB,EAAU,EAAE,CAAC,CAAC,CAAC,QAAQ,CAAC,CAC7D,CAAC,CAAC;QACH,IAAI,CAAC,mBAAmB,CAAC,QAAQ,CAAC,CAAC;QACnC,IAAI,CAAC,MAAM,CAAC,QAAQ,EAAE,WAAW,EAAE,IAAI,EAAE,SAAS,EAAE,IAAI,CAAC,CAAC;IAC9D,CAAC;IAED;;;;;;;OAOG;IACK,mBAAmB,CAAC,QAAgB;QACxC,MAAM,OAAO,GAAkB,IAAI,CAAC,iBAAiB,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;QACrE,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,iBAAiB,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC;IACjE,CAAC;IAED;;;;;;;;;;;OAWG;IACK,KAAK,CAAC,0BAA0B,CAAC,QAAgB;QACrD,MAAM,IAAI,GAAG,IAAI,CAAC,UAAU,CAAC,WAAW,CAAC,QAAQ,EAAE,IAAI,CAAC,YAAY,CAAC,cAAc,EAAE,CAAC,CAAC;QACvF,MAAM,SAAS,GAAG,IAAI,CAAC,UAAU,CAAC,qBAAqB,CAAC,IAAI,CAAC,CAAC;QAC9D,MAAM,MAAM,GAAG,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,UAAU,CAAC,eAAe,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;QAC7E,IAAI,CAAC,SAAS,IAAI,CAAC,MAAM,IAAI,MAAM,CAAC,SAAS;YAAE,OAAO,IAAI,CAAC;QAC3D,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,GAAG,GAAG,GAAG,mCAAmC,GAAG,GAAG,GAAG,IAAI,CAAC,CAAC;QACpF,wDAAwD;QACxD,MAAM,IAAI,CAAC,QAAQ,CAAC,QAAQ,CACxB,QAAQ,EAAE,qBAAqB,EAAE,SAAS,EAC1C,IAAI,0BAAY,CAAC,MAAM,CAAC,aAAa,EAAE,MAAM,CAAC,YAAY,EAAE,MAAM,CAAC,YAAY,EAAE,MAAM,CAAC,QAAQ,CAAC,EACjG,IAAI,2BAAe,CAAC,MAAM,CAAC,eAAe,EAAE,KAAK,CAAC,CACrD,CAAC;QACF,OAAO,IAAI,CAAC;IAChB,CAAC;IAED;;;;OAIG;IACK,KAAK,CAAC,YAAY,CAAC,QAAgB;QACvC,MAAM,IAAI,CAAC,aAAa,CAAC,YAAY,CAAC,QAAQ,EAAE,IAAI,iCAAgB,CAChE,iBAAiB,EACjB,0BAA0B,EAC1B,wFAAwF,EACxF,6BAAY,EACZ,KAAK,CACR,CAAC,CAAC;QACH,+FAA+F;QAC/F,6FAA6F;QAC7F,gFAAgF;QAChF,4FAA4F;QAC5F,IAAI,CAAC,iBAAiB,CAAC,iCAAiC,CAAC,QAAQ,CAAC,CAAC;QACnE,OAAO,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC;IACpC,CAAC;IAED;;;;;OAKG;IACK,cAAc,CAAC,QAAgB,EAAE,WAAmB,EAAE,IAAmB,EAAE,MAAoB;QACnG,IAAI,IAAI,CAAC,KAAK,CAAC,UAAU;YAAE,OAAO,EAAE,CAAC;QACrC,MAAM,QAAQ,GAAG,IAAI,CAAC,YAAY,CAAC,WAAW,CAC1C,QAAQ,EAAE,WAAW,EAAE,IAAI,CAAC,KAAK,EAAE,IAAI,CAAC,YAAY,EAAE,MAAM,CAAC,iBAAiB,CAAC,CAAC;QACpF,MAAM,OAAO,GAAG,IAAI,CAAC,YAAY,CAAC,UAAU,CAAC,QAAQ,EAAE,WAAW,CAAC,CAAC;QACpE,iGAAiG;QACjG,oGAAoG;QACpG,gGAAgG;QAChG,iGAAiG;QACjG,IAAI,CAAC,OAAO,GAAG,IAAI,CAAC,eAAe,CAAC,MAAM,CACtC,QAAQ,EAAE,WAAW,EAAE,IAAI,CAAC,KAAK,EAAE,eAAe,EAAE,IAAI,CAAC,YAAY,EAAE,OAAO,CAAC,CAAC;QACpF,MAAM,SAAS,GAAG,IAAI,CAAC,eAAe,CAAC,KAAK,CAAC,QAAQ,EAAE,IAAI,EAAE,QAAQ,EAAE,OAAO,EAAE,MAAM,CAAC,CAAC;QACxF,MAAM,GAAG,GAAG,IAAI,CAAC,oBAAoB,CAAC,kBAAkB,CAAC,QAAQ,EAAE,WAAW,CAAC,CAAC;QAChF,EAAE,CAAC,MAAM,CAAC,GAAG,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,yDAAyD;QAC3G,EAAE,CAAC,SAAS,CAAC,GAAG,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;QACvC,KAAK,MAAM,CAAC,IAAI,SAAS,EAAE,CAAC;YACxB,EAAE,CAAC,aAAa,CAAC,IAAI,CAAC,oBAAoB,CAAC,OAAO,CAAC,QAAQ,EAAE,WAAW,EAAE,CAAC,CAAC,QAAQ,CAAC,EACjF,IAAI,CAAC,oBAAoB,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC;QAC7C,CAAC;QACD,OAAO,SAAS,CAAC;IACrB,CAAC;IAED;;;;;;;OAOG;IACH,yDAAyD;IACjD,MAAM,CACV,QAAgB,EAAE,WAAmB,EAAE,IAAmB,EAAE,SAAsC,EAClG,IAA2B;QAE3B,MAAM,KAAK,GAAG,IAAI,iCAAiB,CAAC,QAAQ,EAAE,WAAW,EAAE,IAAI,CAAC,UAAU,CAAC,CAAC;QAC5E,KAAK,CAAC,YAAY,GAAG,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC;QACzC,KAAK,CAAC,eAAe,GAAG,IAAI,CAAC,UAAU,CAAC,MAAM,CAAC;QAC/C,KAAK,CAAC,QAAQ,GAAG,IAAI,CAAC,QAAQ,CAAC,KAAK,EAAE,CAAC;QACvC,KAAK,CAAC,YAAY,GAAG,IAAI,CAAC,YAAY,CAAC,KAAK,EAAE,CAAC;QAC/C,KAAK,CAAC,SAAS,GAAG,SAAS,CAAC,KAAK,EAAE,CAAC;QACpC,KAAK,CAAC,OAAO,GAAG,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC;QACpC,KAAK,CAAC,YAAY,GAAG,IAAI,CAAC,YAAY,CAAC;QACvC,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,YAAY,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC;IAC1D,CAAC;IAED;;;;;;;;;;;;;OAaG;IACK,QAAQ,CAAC,IAAmB;QAChC,MAAM,OAAO,GAAG,IAAI,CAAC,iBAAiB,CAAC,iBAAiB,CAAC,IAAI,CAAC,UAAU,EAAE,IAAI,CAAC,OAAO,CAAC,CAAC;QACxF,OAAO,OAAO,CAAC,GAAG,CAAC,CAAC,GAAsB,EAAmB,EAAE,CAAC,IAAI,+BAAe,CAC/E,GAAG,CAAC,EAAE,EAAE,IAAI,CAAC,iBAAiB,CAAC,YAAY,CAAC,GAAG,EAAE,IAAI,CAAC,iBAAiB,CAAC,cAAc,CAAC,GAAG,EAAE,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC;IACrH,CAAC;CACJ,CAAA;AA/KY,sDAAqB;gCAArB,qBAAqB;IADjC,IAAA,sBAAU,EAAC,8BAAkB,CAAC,SAAS,CAAC;6CAIA,6BAAc;QAChB,mCAAY;QACjB,kBAAO;QACD,8BAAa;QACT,uCAAiB;QACxB,wBAAU;QACZ,oBAAQ;QACA,oCAAgB;QACpB,4BAAY;QACZ,oCAAgB;QACb,mCAAe;QACf,mDAAuB;QAClB,0CAA2B;QACvC,gDAAyB;QAChB,kCAAiB;QAEjB,gCAAiB;GAnBhD,qBAAqB,CA+KjC","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 { HomeConfigService, BuildsLog } from '@webpieces/rules-config';
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
- alwaysCapture: boolean;
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
- constructor(homeConfig: HomeConfigService, buildLog: BuildGateLog, buildsLog: BuildsLog);
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
- * On failure, WITHOUT capture: the pre-existing text, which tells the AI to re-run the build itself.
67
- * WITH capture: a deliberately tiny pointer at the log the gate already wrote — the whole point of the
68
- * feature is that the agent reads one file instead of rebuilding the repo and eating the transcript.
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
- // Capture regardless of the EXPERIMENTAL `~/.webpieces/config.json` opt-in. True for `wp-build`, whose
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
- constructor(homeConfig, buildLog, buildsLog) {
48
- this.homeConfig = homeConfig;
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
- process.stdout.write(`\n${opts.label}: ${buildCommand}\n`);
110
- // '' means NOT capturing: a PR-flow stage on a machine that has not created the OPTIONAL
111
- // `~/.webpieces/config.json`. Everything below then runs exactly the code it ran before capture
112
- // existed — same spawn, streamed to the terminal, same message, no extra file.
113
- const logPath = opts.alwaysCapture || this.isCaptureEnabled() ? this.buildLog.pathFor(repoRoot, opts.stage) : '';
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 = logPath === ''
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
- process.stdout.write(logPath === '' ? '\n✅ Build passed.\n' : this.buildLog.successMessage(logPath));
92
+ this.stageConsole.say(this.buildLog.successMessage(logPath));
130
93
  }
131
94
  /**
132
- * On failure, WITHOUT capture: the pre-existing text, which tells the AI to re-run the build itself.
133
- * WITH capture: a deliberately tiny pointer at the log the gate already wrote — the whole point of the
134
- * feature is that the agent reads one file instead of rebuilding the repo and eating the transcript.
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
- if (logPath !== '')
138
- return this.buildLog.failureMessage(buildCommand, logPath);
139
- return `\n❌ ${opts.failureHeadline}\n\n` +
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", [rules_config_1.HomeConfigService,
148
- build_gate_log_1.BuildGateLog,
149
- rules_config_1.BuildsLog])
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 below instead.
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 WITHOUT creating anything, and '' when no such log exists. Used by finish's skip 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
- * to the console every HEARTBEAT_MS so the caller can see it is alive. Returns the BUILD's exit code.
73
- * Nothing is truncated and nothing is streamed.
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;