@webpieces/pr-gate 0.4.691 → 0.4.693
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/cleanup-command.d.ts +3 -2
- package/src/scripts/commands/cleanup-command.js +27 -20
- package/src/scripts/commands/cleanup-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/reap-worktree-command.js +3 -3
- package/src/scripts/commands/reap-worktree-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/commands/worktree-cleanup.d.ts +4 -3
- package/src/scripts/commands/worktree-cleanup.js +10 -4
- package/src/scripts/commands/worktree-cleanup.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/run-update.js +2 -1
- package/src/scripts/workflow/run-update.js.map +1 -1
- 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
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"gate-log-file.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/pr-gate/src/scripts/workflow/gate-log-file.ts"],"names":[],"mappings":"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","sourcesContent":["import * as fs from 'fs';\nimport * as path from 'path';\nimport { dotWebpieces, toError } from '@webpieces/rules-config';\nimport { injectable, bindingScopeValues } from 'inversify';\n\n// ─── Why this class exists ─────────────────────────────────────────────────────────────────────────────\n// TWO things in this package write a big blob of output to a file and hand the caller a one-line pointer\n// instead of the blob: the BUILD (BuildGateLog) and the STAGE CONSOLE (StageOutputLog). They are different\n// subjects — one spawns a child and heartbeats, the other intercepts this process's own stdout — but the\n// FILE half is identical: where the file lands, how the previous run is kept, and what the pointer at it\n// reads like.\n//\n// That half lives here, once. Cloning it would have given the two logs two answers to \"where is it?\" and\n// two rotation policies, and the whole value of the pointer is that a reader recognises it instantly and\n// can `grep` it without first working out which subsystem wrote it.\n//\n// ─── WHERE the files land, and why that shape ──────────────────────────────────────────────────────────\n// Through `dotWebpieces` — never `path.join(repoRoot, '.webpieces')`. In a LINKED WORKTREE `local()`\n// resolves to `<primary>/.webpieces/worktrees/<git worktree name>/`, so:\n// • N agents in N worktrees never write one another's log, by construction rather than by naming;\n// • the history OUTLIVES the worktree — `wp-cleanup` reaps a tree's directory and the logs it wrote are\n// still there under the primary clone, which is exactly when you want them;\n// • it self-limits, because `.webpieces/worktrees/**` is already swept once a namespace goes stale.\n// Nothing here needs a retention policy of its own.\n\n/** The suffix of the ONE previous generation kept beside a log. */\nconst BACKUP_SUFFIX = '.bak';\n\n/**\n * The file mechanics shared by every captured log in the PR gate: WHERE it goes, keeping the previous\n * run, the pointer the caller prints instead of the output, and the failure tail.\n *\n * It knows nothing about builds, stages or console interception — its callers own all of that. Singleton\n * because it is pure behaviour over paths; it holds no state.\n */\n@injectable(bindingScopeValues.Singleton)\nexport class GateLogFile {\n /**\n * `<local()>/logs/<name>` — the ONE log directory, per worktree namespace. Creates the directory.\n * Use for anything a MESSAGE points at; nobody types these names.\n */\n logsPath(repoRoot: string, name: string): string {\n return this.ensureDir(dotWebpieces.logsFile(repoRoot, name));\n }\n\n /**\n * `<local()>/<name>` — the ROOT of the state dir, for the one log a PERSON types from memory\n * (`.webpieces/build.log`). Creates the directory. Reach for `logsPath` unless the path itself has\n * to be memorable.\n */\n localPath(repoRoot: string, name: string): string {\n return this.ensureDir(dotWebpieces.localFile(repoRoot, name));\n }\n\n /** Where the PREVIOUS run of `logPath` is kept — always `<logPath>.bak`. */\n backupPathFor(logPath: string): string {\n return `${logPath}${BACKUP_SUFFIX}`;\n }\n\n /**\n * Move an existing log aside to `<log>.bak`, overwriting any previous backup, so the last TWO runs\n * are always on disk. A missing log is the normal first-run state and is not an error.\n *\n * ONE generation, everywhere: a re-run at the same commit does not truncate away the run before it,\n * and a reader never has to work out which of N dated files is the one they want.\n */\n rotate(logPath: string): void {\n fs.mkdirSync(path.dirname(logPath), { recursive: true });\n if (!fs.existsSync(logPath)) return;\n fs.rmSync(this.backupPathFor(logPath), { force: true });\n fs.renameSync(logPath, this.backupPathFor(logPath));\n }\n\n /**\n * The two lines that name a log — identical on success and on failure, so there is exactly one shape\n * to recognise.\n *\n * The backup line states what is TRUE RIGHT NOW: on the very first run in a tree there is no `.bak`\n * yet, and pointing a reader at a file that does not exist is the small lie that costs a wasted `cat`.\n */\n pointer(logPath: string): string {\n const name = path.basename(logPath);\n const backedUp = fs.existsSync(this.backupPathFor(logPath))\n ? `(${name} is backed up to ${name}${BACKUP_SUFFIX} every run so you have the last 2 runs of logs)`\n : `(the previous ${name} is kept as ${name}${BACKUP_SUFFIX} on every run — this is the first, so there is none yet)`;\n return `FullLog : ${logPath}\\n${backedUp}\\n`;\n }\n\n /**\n * The path as a progress line should show it: relative to the repo when it sits inside it (a linked\n * worktree's state lives under the PRIMARY clone, so it usually does not), absolute otherwise.\n */\n displayPath(repoRoot: string, logPath: string): string {\n const relative = path.relative(repoRoot, logPath);\n return relative === '' || relative.startsWith('..') || path.isAbsolute(relative) ? logPath : relative;\n }\n\n /**\n * The log's last `lines` lines, indented, or a plain statement of why there are none.\n *\n * A read that fails is REPORTED, never allowed to throw: this renders a message for something that\n * has ALREADY failed, so an I/O error escaping here would replace the real failure with the\n * renderer's own — the caller would lose the actual error and be handed a filesystem error instead.\n * The full log is still named on the line above, so nothing is hidden by degrading to one line.\n */\n tail(logPath: string, lines: number): string {\n if (!fs.existsSync(logPath)) return ` (no log file at ${logPath})\\n`;\n // webpieces-disable no-unmanaged-exceptions -- chokepoint: see above, the failure renderer may not\n // replace the caller's failure with its own.\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n const all = fs.readFileSync(logPath, 'utf8').split('\\n').filter((l: string): boolean => l !== '');\n if (all.length === 0) return ' (the log is empty)\\n';\n return all.slice(-lines).map((l: string): string => ` ${l}\\n`).join('');\n } catch (err: unknown) {\n const error = toError(err);\n return ` (could not read ${logPath}: ${error.message})\\n`;\n }\n }\n\n /** How many lines the log holds right now. A log that does not exist yet is zero lines, not an error. */\n lineCount(logPath: string): number {\n if (!fs.existsSync(logPath)) return 0;\n const body = fs.readFileSync(logPath, 'utf8');\n if (body === '') return 0;\n return body.split('\\n').length - (body.endsWith('\\n') ? 1 : 0);\n }\n\n private ensureDir(file: string): string {\n fs.mkdirSync(path.dirname(file), { recursive: true });\n return file;\n }\n}\n"]}
|
|
@@ -25,7 +25,8 @@ let RunUpdate = class RunUpdate {
|
|
|
25
25
|
}
|
|
26
26
|
// `finishCommand` is the command the AI is told to run after resolving conflicts (standalone passes
|
|
27
27
|
// `wp-finish-update`, PR flow passes `wp-finish-upsert-pr`). `verb` is the invoking bin, threaded so
|
|
28
|
-
// every branch mutation is recorded in
|
|
28
|
+
// every branch mutation is recorded in the branch-mutation log (BranchMutationLog.branchMutationLogPath
|
|
29
|
+
// resolves it for THIS tree — it is per-worktree). `pushRemote` is false
|
|
29
30
|
// for the PR flow, which pushes exactly once from wp-finish-upsert-pr (see MergeEndOptions).
|
|
30
31
|
// eslint-disable-next-line @typescript-eslint/max-params
|
|
31
32
|
async runUpdateFromMain(repoRoot, verb, finishCommand, pushRemote) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"run-update.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/pr-gate/src/scripts/workflow/run-update.ts"],"names":[],"mappings":";;;;AAAA,0DAA+F;AAC/F,yCAA2D;AAC3D,iEAAsD;AACtD,+CAA2C;AAC3C,+CAAyD;AACzD,2CAAwD;AAIxD,mGAAmG;AACnG,uGAAuG;AACvG,gGAAgG;AAChG,gGAAgG;AAEzF,IAAM,SAAS,GAAf,MAAM,SAAS;IAEG;IACA;IACA;IACA;IAJrB,YACqB,YAA0B,EAC1B,UAAsB,EACtB,UAAsB,EACtB,QAAkB;QAHlB,iBAAY,GAAZ,YAAY,CAAc;QAC1B,eAAU,GAAV,UAAU,CAAY;QACtB,eAAU,GAAV,UAAU,CAAY;QACtB,aAAQ,GAAR,QAAQ,CAAU;IACpC,CAAC;IAEJ,oGAAoG;IACpG,qGAAqG;IACrG,
|
|
1
|
+
{"version":3,"file":"run-update.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/pr-gate/src/scripts/workflow/run-update.ts"],"names":[],"mappings":"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","sourcesContent":["import { MutationVerb, BranchMutationEvent, logBranchMutation } from '@webpieces/rules-config';\nimport { injectable, bindingScopeValues } from 'inversify';\nimport { AiBranchName } from './git-readAiBranchName';\nimport { MergeState } from './merge-state';\nimport { MergeStart, MergeContext } from './merge-start';\nimport { MergeEnd, MergeEndOptions } from './merge-end';\n\nexport type UpdateOutcome = 'finalized' | 'conflict' | 'unvalidatedResume';\n\n// The shared \"sync this feature branch from main\" engine — a thin composition of the two lifecycle\n// primitives (merge-START brings main in, merge-END finalizes). INTERNAL, not a bin: `wp-start-update`\n// (standalone) and `wp-start-upsert-pr` (PR flow) both call it. It NEVER creates a PR and NEVER\n// process.exits; it RETURNS an outcome so each caller prints the handoff that fits its context.\n@injectable(bindingScopeValues.Singleton)\nexport class RunUpdate {\n constructor(\n private readonly aiBranchName: AiBranchName,\n private readonly mergeState: MergeState,\n private readonly mergeStart: MergeStart,\n private readonly mergeEnd: MergeEnd,\n ) {}\n\n // `finishCommand` is the command the AI is told to run after resolving conflicts (standalone passes\n // `wp-finish-update`, PR flow passes `wp-finish-upsert-pr`). `verb` is the invoking bin, threaded so\n // every branch mutation is recorded in the branch-mutation log (BranchMutationLog.branchMutationLogPath\n // resolves it for THIS tree — it is per-worktree). `pushRemote` is false\n // for the PR flow, which pushes exactly once from wp-finish-upsert-pr (see MergeEndOptions).\n // eslint-disable-next-line @typescript-eslint/max-params\n async runUpdateFromMain(repoRoot: string, verb: MutationVerb, finishCommand: string, pushRemote: boolean): Promise<UpdateOutcome> {\n logBranchMutation(repoRoot, new BranchMutationEvent(verb, 'START'));\n const outcome = await this.runUpdate(repoRoot, verb, finishCommand, pushRemote);\n const end = new BranchMutationEvent(verb, 'END');\n end.outcome = outcome;\n logBranchMutation(repoRoot, end);\n return outcome;\n }\n\n // eslint-disable-next-line @typescript-eslint/max-params\n private async runUpdate(repoRoot: string, verb: MutationVerb, finishCommand: string, pushRemote: boolean): Promise<UpdateOutcome> {\n const home = this.mergeState.mergeDirFor(repoRoot, this.aiBranchName.getFeatureName());\n\n // Resume path: an in-progress merge is the `merge-<n>/` run dir that holds a marker.\n const activeDir = this.mergeState.findActiveMergeRunDir(home);\n if (activeDir) {\n const existing = this.mergeState.readMergeMarker(activeDir);\n if (existing === null || !existing.validated) return 'unvalidatedResume';\n // Already validated → just finalize the branch swap (reads THIS run dir's marker).\n await this.mergeEnd.mergeEnd(\n repoRoot, verb, activeDir,\n new MergeContext(existing.currentBranch, existing.squashBranch, existing.backupBranch, existing.prNumber),\n new MergeEndOptions(null, pushRemote),\n );\n return 'finalized';\n }\n\n // Fresh update: mergeStart picks the slot number, creates its own `merge-<n>/` run dir, and\n // returns that path for merge-END to finalize against.\n const result = await this.mergeStart.mergeStart(repoRoot, verb, home, finishCommand);\n if (result.status === 'conflict' || result.context === null) {\n return 'conflict';\n }\n await this.mergeEnd.mergeEnd(repoRoot, verb, result.runDir, result.context, new MergeEndOptions(null, pushRemote));\n return 'finalized';\n }\n}\n"]}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { GateLogFile } from './gate-log-file';
|
|
2
|
+
/** Stage ②'s console log. Named for the command, because that is what a reader is looking for. */
|
|
3
|
+
export declare const REVIEW_CONSOLE_LOG = "wp-review-upsert-pr.log";
|
|
4
|
+
/** Stage ③'s console log. */
|
|
5
|
+
export declare const FINISH_CONSOLE_LOG = "wp-finish-upsert-pr.log";
|
|
6
|
+
/**
|
|
7
|
+
* Captures a stage's verbose console output to a file, keeps the handful of lines the caller must act on
|
|
8
|
+
* immediately on the terminal, and hands back the one-line pointer at the file.
|
|
9
|
+
*
|
|
10
|
+
* Singleton, and stateful FOR THE DURATION OF ONE `withCapture` call: a bin runs exactly one stage.
|
|
11
|
+
* Re-entering it is a programming error, not a supported mode, and it throws rather than silently
|
|
12
|
+
* capturing into the inner file and restoring the outer one.
|
|
13
|
+
*/
|
|
14
|
+
export declare class StageOutputLog {
|
|
15
|
+
private readonly files;
|
|
16
|
+
constructor(files: GateLogFile);
|
|
17
|
+
private logPath;
|
|
18
|
+
private fd;
|
|
19
|
+
private original;
|
|
20
|
+
/**
|
|
21
|
+
* Run `body` with this process's stdout captured to `<logs>/<fileName>`, then restore stdout and
|
|
22
|
+
* print the pointer at the file.
|
|
23
|
+
*
|
|
24
|
+
* The pointer is printed in a `finally`, so it is there on the FAILURE path too — which is the path
|
|
25
|
+
* where a reader most needs it, and the one where an uncaptured stage would have scrolled the cause
|
|
26
|
+
* off the top of the terminal.
|
|
27
|
+
*/
|
|
28
|
+
withCapture<T>(repoRoot: string, fileName: string, body: () => Promise<T>): Promise<T>;
|
|
29
|
+
/**
|
|
30
|
+
* Print `text` to the TERMINAL as well as the log — for the few things a caller must act on before it
|
|
31
|
+
* can do anything else: the build heartbeat (a silent command is a killed command), the build result,
|
|
32
|
+
* the review instructions, the PR link, and anything naming a file to read.
|
|
33
|
+
*
|
|
34
|
+
* Outside a capture this is an ordinary write, so a class that calls it is not coupled to the stage.
|
|
35
|
+
*/
|
|
36
|
+
say(text: string): void;
|
|
37
|
+
private intercept;
|
|
38
|
+
private release;
|
|
39
|
+
private appendToLog;
|
|
40
|
+
}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.StageOutputLog = exports.FINISH_CONSOLE_LOG = exports.REVIEW_CONSOLE_LOG = void 0;
|
|
4
|
+
const tslib_1 = require("tslib");
|
|
5
|
+
const fs = tslib_1.__importStar(require("fs"));
|
|
6
|
+
const inversify_1 = require("inversify");
|
|
7
|
+
const gate_log_file_1 = require("./gate-log-file");
|
|
8
|
+
// ─── Why ───────────────────────────────────────────────────────────────────────────────────────────────
|
|
9
|
+
// `wp-build` already solved this for the BUILD: its output goes to a FILE and the console gets a heartbeat
|
|
10
|
+
// plus a pointer. Stage ② and stage ③ had not been given the same treatment, so they printed everything —
|
|
11
|
+
// merge validation, the active-hatch report, the checklist scan, the diff extraction, the dashboard, the
|
|
12
|
+
// gh round trips — straight down the terminal.
|
|
13
|
+
//
|
|
14
|
+
// The consequence was measured, and it is not "a long transcript". An agent that expects a wall of output
|
|
15
|
+
// bounds it by reflex: `pnpm wp-review-upsert-pr 2>&1 | tail -50`. A PIPE WITHHOLDS EVERY BYTE UNTIL THE
|
|
16
|
+
// COMMAND EXITS, so the harness sees a command that has printed nothing for 600 seconds and kills it — a
|
|
17
|
+
// full build's worth of work thrown away, and then usually re-run the same way. In this repo's primary
|
|
18
|
+
// tree alone the call log holds 85 piped `wp-*` invocations, 42 of them on a command that runs a build
|
|
19
|
+
// (`wp-review-upsert-pr` 22, `wp-finish-upsert-pr` 17, `wp-build` 3).
|
|
20
|
+
//
|
|
21
|
+
// Shrinking the output is therefore the FIRST half of the fix and the guard against the pipe is the
|
|
22
|
+
// second, in that order. Blocking the pipe while the command still printed a thousand lines would only
|
|
23
|
+
// have traded a watchdog kill for a flooded context, and an agent would have routed around it with `>`.
|
|
24
|
+
//
|
|
25
|
+
// ─── Why it INTERCEPTS stdout instead of every writer calling it ───────────────────────────────────────
|
|
26
|
+
// There are ~160 `process.stdout.write` sites across this package, in classes shared by six other bins.
|
|
27
|
+
// Threading a logger through all of them would be a far larger diff with far more ways to miss one — and
|
|
28
|
+
// a MISSED one is the whole failure, because a single verbose writer that still reaches the console keeps
|
|
29
|
+
// the output big enough to be piped. Interception inverts the default: everything a stage prints is
|
|
30
|
+
// captured unless it says out loud that it must be seen NOW, and the "say it out loud" list is short
|
|
31
|
+
// enough to read (`say()` — the build heartbeat, the build result, the review instructions, the PR link).
|
|
32
|
+
//
|
|
33
|
+
// The interception is installed and removed by `withCapture`, in a `finally`, so a throw cannot leave a
|
|
34
|
+
// process with a patched stdout. Child processes that inherit fd 1 write straight past it, which is why
|
|
35
|
+
// the build gate redirects its child to a file of its own rather than relying on this.
|
|
36
|
+
//
|
|
37
|
+
// Also deliberately NOT intercepted: stderr. Failures must stay on the terminal — `runMain` renders every
|
|
38
|
+
// thrown CliExitError / RuleFailError there, and a refusal an agent cannot see is worse than a long one.
|
|
39
|
+
/** Stage ②'s console log. Named for the command, because that is what a reader is looking for. */
|
|
40
|
+
exports.REVIEW_CONSOLE_LOG = 'wp-review-upsert-pr.log';
|
|
41
|
+
/** Stage ③'s console log. */
|
|
42
|
+
exports.FINISH_CONSOLE_LOG = 'wp-finish-upsert-pr.log';
|
|
43
|
+
/**
|
|
44
|
+
* Captures a stage's verbose console output to a file, keeps the handful of lines the caller must act on
|
|
45
|
+
* immediately on the terminal, and hands back the one-line pointer at the file.
|
|
46
|
+
*
|
|
47
|
+
* Singleton, and stateful FOR THE DURATION OF ONE `withCapture` call: a bin runs exactly one stage.
|
|
48
|
+
* Re-entering it is a programming error, not a supported mode, and it throws rather than silently
|
|
49
|
+
* capturing into the inner file and restoring the outer one.
|
|
50
|
+
*/
|
|
51
|
+
let StageOutputLog = class StageOutputLog {
|
|
52
|
+
files;
|
|
53
|
+
constructor(files) {
|
|
54
|
+
this.files = files;
|
|
55
|
+
}
|
|
56
|
+
// The open log, or '' when nothing is being captured. `say` works either way — outside a capture it
|
|
57
|
+
// is a plain write to the terminal, which is what every other bin wants.
|
|
58
|
+
logPath = '';
|
|
59
|
+
fd = -1;
|
|
60
|
+
original = null;
|
|
61
|
+
/**
|
|
62
|
+
* Run `body` with this process's stdout captured to `<logs>/<fileName>`, then restore stdout and
|
|
63
|
+
* print the pointer at the file.
|
|
64
|
+
*
|
|
65
|
+
* The pointer is printed in a `finally`, so it is there on the FAILURE path too — which is the path
|
|
66
|
+
* where a reader most needs it, and the one where an uncaptured stage would have scrolled the cause
|
|
67
|
+
* off the top of the terminal.
|
|
68
|
+
*/
|
|
69
|
+
async withCapture(repoRoot, fileName, body) {
|
|
70
|
+
if (this.original !== null)
|
|
71
|
+
throw new Error(`StageOutputLog is already capturing to ${this.logPath}`);
|
|
72
|
+
this.logPath = this.files.logsPath(repoRoot, fileName);
|
|
73
|
+
this.files.rotate(this.logPath);
|
|
74
|
+
this.fd = fs.openSync(this.logPath, 'w');
|
|
75
|
+
// The REFERENCE, not a bound copy: `release` puts this exact function back, so a caller that
|
|
76
|
+
// installed its own stdout before us (a spec, a wrapping bin) gets ITS function back rather than
|
|
77
|
+
// a wrapper around it, and repeated captures cannot stack bound layers.
|
|
78
|
+
this.original = process.stdout.write;
|
|
79
|
+
process.stdout.write = this.intercept.bind(this);
|
|
80
|
+
// webpieces-disable no-unmanaged-exceptions -- chokepoint: stdout MUST be restored and the fd
|
|
81
|
+
// closed whatever `body` does; the throw is re-raised untouched.
|
|
82
|
+
// eslint-disable-next-line @webpieces/no-unmanaged-exceptions
|
|
83
|
+
try {
|
|
84
|
+
return await body();
|
|
85
|
+
}
|
|
86
|
+
finally {
|
|
87
|
+
this.release();
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Print `text` to the TERMINAL as well as the log — for the few things a caller must act on before it
|
|
92
|
+
* can do anything else: the build heartbeat (a silent command is a killed command), the build result,
|
|
93
|
+
* the review instructions, the PR link, and anything naming a file to read.
|
|
94
|
+
*
|
|
95
|
+
* Outside a capture this is an ordinary write, so a class that calls it is not coupled to the stage.
|
|
96
|
+
*/
|
|
97
|
+
say(text) {
|
|
98
|
+
this.appendToLog(text);
|
|
99
|
+
// `.call`, because `original` is the raw reference rather than a bound copy — see withCapture.
|
|
100
|
+
(this.original ?? process.stdout.write).call(process.stdout, text);
|
|
101
|
+
}
|
|
102
|
+
// The stdout replacement. Node calls this with (chunk), (chunk, encoding), (chunk, callback) or
|
|
103
|
+
// (chunk, encoding, callback); a callback that is never invoked can stall a writer, so it is always
|
|
104
|
+
// called. Returning true means "not backpressured", which is honest for a synchronous file write.
|
|
105
|
+
intercept(chunk, encodingOrCallback, callback) {
|
|
106
|
+
this.appendToLog(typeof chunk === 'string' ? chunk : Buffer.from(chunk).toString('utf8'));
|
|
107
|
+
const done = typeof encodingOrCallback === 'function' ? encodingOrCallback : callback;
|
|
108
|
+
if (done !== undefined)
|
|
109
|
+
done();
|
|
110
|
+
return true;
|
|
111
|
+
}
|
|
112
|
+
// Restore stdout, close the log, and print the pointer at it. Idempotent: a second call after the
|
|
113
|
+
// `finally` has already run is a no-op rather than a double pointer.
|
|
114
|
+
release() {
|
|
115
|
+
if (this.original === null)
|
|
116
|
+
return;
|
|
117
|
+
const restore = this.original;
|
|
118
|
+
this.original = null;
|
|
119
|
+
process.stdout.write = restore;
|
|
120
|
+
fs.closeSync(this.fd);
|
|
121
|
+
this.fd = -1;
|
|
122
|
+
restore.call(process.stdout, `\n${this.files.pointer(this.logPath)}`);
|
|
123
|
+
this.logPath = '';
|
|
124
|
+
}
|
|
125
|
+
// Nothing is captured when no capture is open — `say` still reaches the terminal, which is the point.
|
|
126
|
+
appendToLog(text) {
|
|
127
|
+
if (this.fd === -1)
|
|
128
|
+
return;
|
|
129
|
+
fs.writeSync(this.fd, text);
|
|
130
|
+
}
|
|
131
|
+
};
|
|
132
|
+
exports.StageOutputLog = StageOutputLog;
|
|
133
|
+
exports.StageOutputLog = StageOutputLog = tslib_1.__decorate([
|
|
134
|
+
(0, inversify_1.injectable)(inversify_1.bindingScopeValues.Singleton),
|
|
135
|
+
tslib_1.__metadata("design:paramtypes", [gate_log_file_1.GateLogFile])
|
|
136
|
+
], StageOutputLog);
|
|
137
|
+
//# sourceMappingURL=stage-output-log.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"stage-output-log.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/pr-gate/src/scripts/workflow/stage-output-log.ts"],"names":[],"mappings":"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","sourcesContent":["import * as fs from 'fs';\nimport { injectable, bindingScopeValues } from 'inversify';\n\nimport { GateLogFile } from './gate-log-file';\n\n// ─── Why ───────────────────────────────────────────────────────────────────────────────────────────────\n// `wp-build` already solved this for the BUILD: its output goes to a FILE and the console gets a heartbeat\n// plus a pointer. Stage ② and stage ③ had not been given the same treatment, so they printed everything —\n// merge validation, the active-hatch report, the checklist scan, the diff extraction, the dashboard, the\n// gh round trips — straight down the terminal.\n//\n// The consequence was measured, and it is not \"a long transcript\". An agent that expects a wall of output\n// bounds it by reflex: `pnpm wp-review-upsert-pr 2>&1 | tail -50`. A PIPE WITHHOLDS EVERY BYTE UNTIL THE\n// COMMAND EXITS, so the harness sees a command that has printed nothing for 600 seconds and kills it — a\n// full build's worth of work thrown away, and then usually re-run the same way. In this repo's primary\n// tree alone the call log holds 85 piped `wp-*` invocations, 42 of them on a command that runs a build\n// (`wp-review-upsert-pr` 22, `wp-finish-upsert-pr` 17, `wp-build` 3).\n//\n// Shrinking the output is therefore the FIRST half of the fix and the guard against the pipe is the\n// second, in that order. Blocking the pipe while the command still printed a thousand lines would only\n// have traded a watchdog kill for a flooded context, and an agent would have routed around it with `>`.\n//\n// ─── Why it INTERCEPTS stdout instead of every writer calling it ───────────────────────────────────────\n// There are ~160 `process.stdout.write` sites across this package, in classes shared by six other bins.\n// Threading a logger through all of them would be a far larger diff with far more ways to miss one — and\n// a MISSED one is the whole failure, because a single verbose writer that still reaches the console keeps\n// the output big enough to be piped. Interception inverts the default: everything a stage prints is\n// captured unless it says out loud that it must be seen NOW, and the \"say it out loud\" list is short\n// enough to read (`say()` — the build heartbeat, the build result, the review instructions, the PR link).\n//\n// The interception is installed and removed by `withCapture`, in a `finally`, so a throw cannot leave a\n// process with a patched stdout. Child processes that inherit fd 1 write straight past it, which is why\n// the build gate redirects its child to a file of its own rather than relying on this.\n//\n// Also deliberately NOT intercepted: stderr. Failures must stay on the terminal — `runMain` renders every\n// thrown CliExitError / RuleFailError there, and a refusal an agent cannot see is worse than a long one.\n\n/** Stage ②'s console log. Named for the command, because that is what a reader is looking for. */\nexport const REVIEW_CONSOLE_LOG = 'wp-review-upsert-pr.log';\n\n/** Stage ③'s console log. */\nexport const FINISH_CONSOLE_LOG = 'wp-finish-upsert-pr.log';\n\n/** The write function this class swaps out and restores — Node's own `process.stdout.write`. */\ntype StdoutWrite = typeof process.stdout.write;\n\n/** The optional completion callback Node's `write` accepts, in either of its two argument positions. */\ntype StdoutCallback = (err?: Error) => void;\n\n/**\n * Captures a stage's verbose console output to a file, keeps the handful of lines the caller must act on\n * immediately on the terminal, and hands back the one-line pointer at the file.\n *\n * Singleton, and stateful FOR THE DURATION OF ONE `withCapture` call: a bin runs exactly one stage.\n * Re-entering it is a programming error, not a supported mode, and it throws rather than silently\n * capturing into the inner file and restoring the outer one.\n */\n@injectable(bindingScopeValues.Singleton)\nexport class StageOutputLog {\n constructor(private readonly files: GateLogFile) {}\n\n // The open log, or '' when nothing is being captured. `say` works either way — outside a capture it\n // is a plain write to the terminal, which is what every other bin wants.\n private logPath = '';\n private fd = -1;\n private original: StdoutWrite | null = null;\n\n /**\n * Run `body` with this process's stdout captured to `<logs>/<fileName>`, then restore stdout and\n * print the pointer at the file.\n *\n * The pointer is printed in a `finally`, so it is there on the FAILURE path too — which is the path\n * where a reader most needs it, and the one where an uncaptured stage would have scrolled the cause\n * off the top of the terminal.\n */\n async withCapture<T>(repoRoot: string, fileName: string, body: () => Promise<T>): Promise<T> {\n if (this.original !== null) throw new Error(`StageOutputLog is already capturing to ${this.logPath}`);\n this.logPath = this.files.logsPath(repoRoot, fileName);\n this.files.rotate(this.logPath);\n this.fd = fs.openSync(this.logPath, 'w');\n // The REFERENCE, not a bound copy: `release` puts this exact function back, so a caller that\n // installed its own stdout before us (a spec, a wrapping bin) gets ITS function back rather than\n // a wrapper around it, and repeated captures cannot stack bound layers.\n this.original = process.stdout.write;\n process.stdout.write = this.intercept.bind(this) as StdoutWrite;\n // webpieces-disable no-unmanaged-exceptions -- chokepoint: stdout MUST be restored and the fd\n // closed whatever `body` does; the throw is re-raised untouched.\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n return await body();\n } finally {\n this.release();\n }\n }\n\n /**\n * Print `text` to the TERMINAL as well as the log — for the few things a caller must act on before it\n * can do anything else: the build heartbeat (a silent command is a killed command), the build result,\n * the review instructions, the PR link, and anything naming a file to read.\n *\n * Outside a capture this is an ordinary write, so a class that calls it is not coupled to the stage.\n */\n say(text: string): void {\n this.appendToLog(text);\n // `.call`, because `original` is the raw reference rather than a bound copy — see withCapture.\n (this.original ?? process.stdout.write).call(process.stdout, text);\n }\n\n // The stdout replacement. Node calls this with (chunk), (chunk, encoding), (chunk, callback) or\n // (chunk, encoding, callback); a callback that is never invoked can stall a writer, so it is always\n // called. Returning true means \"not backpressured\", which is honest for a synchronous file write.\n private intercept(\n chunk: string | Uint8Array, encodingOrCallback?: BufferEncoding | StdoutCallback, callback?: StdoutCallback,\n ): boolean {\n this.appendToLog(typeof chunk === 'string' ? chunk : Buffer.from(chunk).toString('utf8'));\n const done = typeof encodingOrCallback === 'function' ? encodingOrCallback : callback;\n if (done !== undefined) done();\n return true;\n }\n\n // Restore stdout, close the log, and print the pointer at it. Idempotent: a second call after the\n // `finally` has already run is a no-op rather than a double pointer.\n private release(): void {\n if (this.original === null) return;\n const restore = this.original;\n this.original = null;\n process.stdout.write = restore;\n fs.closeSync(this.fd);\n this.fd = -1;\n restore.call(process.stdout, `\\n${this.files.pointer(this.logPath)}`);\n this.logPath = '';\n }\n\n // Nothing is captured when no capture is open — `say` still reaches the terminal, which is the point.\n private appendToLog(text: string): void {\n if (this.fd === -1) return;\n fs.writeSync(this.fd, text);\n }\n}\n"]}
|