vouchington-tooling 0.0.22 → 0.1.1
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/README.md +28 -0
- package/dist/browser-session-runner/attempt.d.mts +2 -0
- package/dist/browser-session-runner/attempt.mjs +196 -0
- package/dist/browser-session-runner/index.d.mts +4 -0
- package/dist/browser-session-runner/index.mjs +68 -0
- package/dist/browser-session-runner/output.d.mts +8 -0
- package/dist/browser-session-runner/output.mjs +24 -0
- package/dist/browser-session-runner/process-group.d.mts +5 -0
- package/dist/browser-session-runner/process-group.mjs +23 -0
- package/dist/browser-session-runner/result.d.mts +2 -0
- package/dist/browser-session-runner/result.mjs +11 -0
- package/dist/browser-session-runner/tail-queue.d.mts +8 -0
- package/dist/browser-session-runner/tail-queue.mjs +30 -0
- package/dist/browser-session-runner/tail.d.mts +1 -0
- package/dist/browser-session-runner/tail.mjs +12 -0
- package/dist/browser-session-runner/types.d.mts +70 -0
- package/dist/browser-session-runner/types.mjs +1 -0
- package/dist/browser-session-runner/watchdog.d.mts +10 -0
- package/dist/browser-session-runner/watchdog.mjs +38 -0
- package/dist/cli/commands/retrospective-transcript.d.mts +1 -0
- package/dist/cli/commands/retrospective-transcript.mjs +31 -0
- package/dist/cli/index.mjs +3 -0
- package/dist/cli/parse.d.mts +3 -0
- package/dist/cli/parse.mjs +2 -0
- package/dist/cli/usage.d.mts +1 -1
- package/dist/cli/usage.mjs +2 -0
- package/dist/index.d.mts +6 -0
- package/dist/index.mjs +3 -0
- package/dist/retrospective-transcript/claude.d.mts +2 -0
- package/dist/retrospective-transcript/claude.mjs +67 -0
- package/dist/retrospective-transcript/codex-segment.d.mts +2 -0
- package/dist/retrospective-transcript/codex-segment.mjs +73 -0
- package/dist/retrospective-transcript/codex.d.mts +10 -0
- package/dist/retrospective-transcript/codex.mjs +170 -0
- package/dist/retrospective-transcript/format.d.mts +4 -0
- package/dist/retrospective-transcript/format.mjs +22 -0
- package/dist/retrospective-transcript/index.d.mts +22 -0
- package/dist/retrospective-transcript/index.mjs +157 -0
- package/dist/retrospective-transcript/javascript-command.d.mts +1 -0
- package/dist/retrospective-transcript/javascript-command.mjs +25 -0
- package/dist/retrospective-transcript/shared.d.mts +31 -0
- package/dist/retrospective-transcript/shared.mjs +213 -0
- package/dist/vitest-diagnostics/directory.d.mts +12 -0
- package/dist/vitest-diagnostics/directory.mjs +44 -0
- package/dist/vitest-diagnostics/index.d.mts +22 -0
- package/dist/vitest-diagnostics/index.mjs +123 -0
- package/dist/vitest-diagnostics/read-file.d.mts +2 -0
- package/dist/vitest-diagnostics/read-file.mjs +37 -0
- package/package.json +16 -1
package/README.md
CHANGED
|
@@ -37,6 +37,7 @@ vouchington run-with-timeout 120 10 docker push example
|
|
|
37
37
|
vouchington lint-links --offline
|
|
38
38
|
vouchington materialize-pr-context
|
|
39
39
|
vouchington wait-for-apt-locks
|
|
40
|
+
vouchington retrospective-transcript --jsonl /path/to/transcript.jsonl
|
|
40
41
|
vouchington install-playwright-chromium-arm64
|
|
41
42
|
vouchington ghcr-package-retention example%2Fapi
|
|
42
43
|
vouchington nuget-central-version trusted.props candidate.props metadata.json out.props
|
|
@@ -45,6 +46,12 @@ vouchington post-review
|
|
|
45
46
|
vouchington stage-review-payload optional|required <source> <destination>
|
|
46
47
|
```
|
|
47
48
|
|
|
49
|
+
`retrospective-transcript` discovers Codex and Claude transcripts by default. It also reads a
|
|
50
|
+
Claude-compatible transcript when `CURSOR_SESSION_ID` is set, and Grok's `updates.jsonl` session
|
|
51
|
+
layout when `GROK_SESSION_ID` is set. Use `--grok-sessions-dir` to point discovery at a nondefault
|
|
52
|
+
Grok session root. Without `--session-id`, it reads those session identities from the host
|
|
53
|
+
environment.
|
|
54
|
+
|
|
48
55
|
Host-lock environment:
|
|
49
56
|
|
|
50
57
|
| Variable | Default | Meaning |
|
|
@@ -83,6 +90,7 @@ import {
|
|
|
83
90
|
import { createArtifactClassifier, runCleanup } from 'vouchington-tooling/gha-artifacts-cleanup'
|
|
84
91
|
import { validateOptionalHttpOrigin } from 'vouchington-tooling/http-origin'
|
|
85
92
|
import { boundPendingLine, splitCompleteLines } from 'vouchington-tooling/process-line-buffer'
|
|
93
|
+
import { runBrowserSession } from 'vouchington-tooling/browser-session-runner'
|
|
86
94
|
import {
|
|
87
95
|
generateSchemaSnapshot,
|
|
88
96
|
renderSchemaMarkdown,
|
|
@@ -111,8 +119,28 @@ import { validateNugetUpdate } from 'vouchington-tooling/nuget-central-version'
|
|
|
111
119
|
import { normalizeSwiftSource } from 'vouchington-tooling/swift-semantic-equal'
|
|
112
120
|
import { parseUniqueSwiftBinaryTargetChecksum } from 'vouchington-tooling/swift-source-offset'
|
|
113
121
|
import { validateResolvedPinDelta } from 'vouchington-tooling/swift-resolved-pin-delta'
|
|
122
|
+
import {
|
|
123
|
+
formatDiagnosticReportSummaries,
|
|
124
|
+
readDiagnosticReportSummaries,
|
|
125
|
+
} from 'vouchington-tooling/vitest-diagnostics'
|
|
126
|
+
import { runRetrospectiveTranscript } from 'vouchington-tooling/retrospective-transcript'
|
|
114
127
|
```
|
|
115
128
|
|
|
116
129
|
The artifact, review-payload, HTTP body, and pagination APIs validate untrusted inputs at their
|
|
117
130
|
boundaries. Review posting lives in `gha-post-review` and talks to GitHub only through caller-supplied
|
|
118
131
|
credentials (job token or a minted Claude GitHub App token).
|
|
132
|
+
|
|
133
|
+
`vitest-diagnostics` reads Node diagnostic report JSON from a caller-selected directory. It sorts
|
|
134
|
+
filenames, tolerates partial files, returns only a bounded field allowlist, and never emits raw
|
|
135
|
+
native frame symbols. Both structured reads and text rendering have hard report-count limits.
|
|
136
|
+
|
|
137
|
+
`browser-session-runner` supervises caller-created browser-test processes. Callers supply command
|
|
138
|
+
construction, line classification, retry/outcome policy, and budgets; the library owns process-group
|
|
139
|
+
termination, output line buffering, shared deadlines, progress watchdogs, diagnostics, and parent signals.
|
|
140
|
+
The returned process must identify a dedicated process group, such as a child spawned with
|
|
141
|
+
`detached: true`; its `processGroupId` is signalled without assuming the child PID is a group ID.
|
|
142
|
+
Stall exits use the caller's `classifyExit` policy, while parent signals and shared-deadline expiration
|
|
143
|
+
are terminal. Output is decoded independently per stream; unfinished lines are classified at close and
|
|
144
|
+
`diagnosticTailBytes` retains a UTF-8-safe tail no larger than its byte budget.
|
|
145
|
+
The runner uses monotonic elapsed time, rejects timer budgets above Node's maximum delay, and waits for
|
|
146
|
+
the dedicated process group to exit after the direct child closes so descendants cannot overlap a retry.
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import { createOutputConsumer } from './output.mjs';
|
|
2
|
+
import { tailText } from './tail.mjs';
|
|
3
|
+
import { TailQueue } from './tail-queue.mjs';
|
|
4
|
+
import { createWatchdogLifecycle, registerWatchdogSetup } from './watchdog.mjs';
|
|
5
|
+
export function runAttempt(options, deps, deadline, attempt) {
|
|
6
|
+
return new Promise((resolve, reject) => {
|
|
7
|
+
const startedAt = deps.now(), process = options.start(attempt);
|
|
8
|
+
if (!Number.isSafeInteger(process.processGroupId) ||
|
|
9
|
+
process.processGroupId <= 0 ||
|
|
10
|
+
process.processGroupId > 2_147_483_647) {
|
|
11
|
+
try {
|
|
12
|
+
process.kill('SIGKILL');
|
|
13
|
+
}
|
|
14
|
+
catch { }
|
|
15
|
+
throw new RangeError('processGroupId must be positive');
|
|
16
|
+
}
|
|
17
|
+
const maxTail = options.diagnosticTailBytes ?? 4096;
|
|
18
|
+
const drainTimeoutMs = options.graceMs + (options.processGroupDrainMs ?? options.graceMs);
|
|
19
|
+
const tail = new TailQueue(maxTail);
|
|
20
|
+
let startupProgress = false, semanticProgress = false;
|
|
21
|
+
let failure, hasFailure = false, childExited = false, closeExit, draining = false, drainDone = false, reason = 'exit', complete = false, terminating = false;
|
|
22
|
+
let killTimer, stallTimer;
|
|
23
|
+
const watchdog = createWatchdogLifecycle();
|
|
24
|
+
const listeners = [];
|
|
25
|
+
const captureFailure = (error) => !hasFailure && ((failure = error), (hasFailure = true));
|
|
26
|
+
const finish = (exit) => {
|
|
27
|
+
if (complete)
|
|
28
|
+
return;
|
|
29
|
+
complete = true;
|
|
30
|
+
deps.clearTimeout(deadlineTimer);
|
|
31
|
+
if (killTimer)
|
|
32
|
+
deps.clearTimeout(killTimer);
|
|
33
|
+
deps.clearTimeout(stallTimer);
|
|
34
|
+
watchdog.complete(captureFailure);
|
|
35
|
+
for (const remove of listeners)
|
|
36
|
+
remove();
|
|
37
|
+
const result = {
|
|
38
|
+
attempts: 0,
|
|
39
|
+
deadlineExceeded: reason === 'deadline',
|
|
40
|
+
diagnosticTail: tailText(tail.toBuffer(), maxTail),
|
|
41
|
+
exit,
|
|
42
|
+
reason,
|
|
43
|
+
semanticProgress,
|
|
44
|
+
startupProgress,
|
|
45
|
+
};
|
|
46
|
+
if (hasFailure)
|
|
47
|
+
reject(failure);
|
|
48
|
+
else
|
|
49
|
+
resolve(result);
|
|
50
|
+
};
|
|
51
|
+
const signal = (value) => {
|
|
52
|
+
try {
|
|
53
|
+
deps.killProcessGroup(process.processGroupId, value);
|
|
54
|
+
}
|
|
55
|
+
catch { }
|
|
56
|
+
try {
|
|
57
|
+
process.kill(value);
|
|
58
|
+
}
|
|
59
|
+
catch { }
|
|
60
|
+
};
|
|
61
|
+
const terminate = (nextReason) => {
|
|
62
|
+
if (nextReason === 'deadline' && childExited && reason === 'exit')
|
|
63
|
+
return;
|
|
64
|
+
if (nextReason === 'parent-signal' || nextReason === 'deadline') {
|
|
65
|
+
if (reason === 'exit' || reason === 'startup-stall' || reason === 'semantic-stall')
|
|
66
|
+
reason = nextReason;
|
|
67
|
+
else if (reason !== nextReason)
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
else if (reason !== 'exit')
|
|
71
|
+
return;
|
|
72
|
+
else if (nextReason !== 'exit')
|
|
73
|
+
reason = nextReason;
|
|
74
|
+
if (terminating)
|
|
75
|
+
return;
|
|
76
|
+
terminating = true;
|
|
77
|
+
killTimer = deps.setTimeout(() => {
|
|
78
|
+
if (deps.isProcessGroupAlive(process.processGroupId))
|
|
79
|
+
signal('SIGKILL');
|
|
80
|
+
}, options.graceMs);
|
|
81
|
+
signal('SIGTERM');
|
|
82
|
+
};
|
|
83
|
+
const fail = (error) => {
|
|
84
|
+
if (hasFailure || complete)
|
|
85
|
+
return;
|
|
86
|
+
captureFailure(error);
|
|
87
|
+
terminate('exit');
|
|
88
|
+
};
|
|
89
|
+
const drain = () => {
|
|
90
|
+
if (draining)
|
|
91
|
+
return;
|
|
92
|
+
draining = true;
|
|
93
|
+
void deps.waitForProcessGroupExit(process.processGroupId, drainTimeoutMs).then(() => {
|
|
94
|
+
drainDone = true;
|
|
95
|
+
if (closeExit)
|
|
96
|
+
finish(closeExit);
|
|
97
|
+
}, (error) => {
|
|
98
|
+
fail(error);
|
|
99
|
+
finish(closeExit ?? { code: null, signal: null });
|
|
100
|
+
});
|
|
101
|
+
};
|
|
102
|
+
const armStall = (ms, nextReason) => {
|
|
103
|
+
if (stallTimer)
|
|
104
|
+
deps.clearTimeout(stallTimer);
|
|
105
|
+
stallTimer = deps.setTimeout(() => terminate(nextReason), ms);
|
|
106
|
+
};
|
|
107
|
+
const line = (value) => {
|
|
108
|
+
const event = options.onLine(value);
|
|
109
|
+
if (event === 'startup' && !startupProgress) {
|
|
110
|
+
startupProgress = true;
|
|
111
|
+
if (!childExited)
|
|
112
|
+
armStall(options.semanticStallMs, 'semantic-stall');
|
|
113
|
+
}
|
|
114
|
+
if (event === 'semantic') {
|
|
115
|
+
semanticProgress = true;
|
|
116
|
+
if (startupProgress && !childExited)
|
|
117
|
+
armStall(options.semanticStallMs, 'semantic-stall');
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
const streams = [process.stdout, process.stderr].map((stream, index) => stream
|
|
121
|
+
? createOutputConsumer(options, tail, index === 0 ? 'stdout' : 'stderr', line)
|
|
122
|
+
: undefined);
|
|
123
|
+
for (const [index, stream] of [process.stdout, process.stderr].entries())
|
|
124
|
+
stream?.on('data', (chunk) => {
|
|
125
|
+
try {
|
|
126
|
+
streams[index].write(chunk);
|
|
127
|
+
}
|
|
128
|
+
catch (error) {
|
|
129
|
+
fail(error);
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
for (const stream of [process.stdout, process.stderr])
|
|
133
|
+
stream?.on('error', fail);
|
|
134
|
+
const remainingDeadlineMs = deadline - deps.now();
|
|
135
|
+
const deadlineTimer = deps.setTimeout(() => terminate('deadline'), Math.max(1, remainingDeadlineMs));
|
|
136
|
+
const remainingStartupMs = options.startupStallMs - (deps.now() - startedAt);
|
|
137
|
+
armStall(Math.max(1, remainingStartupMs), 'startup-stall');
|
|
138
|
+
if (remainingStartupMs <= 0)
|
|
139
|
+
terminate('startup-stall');
|
|
140
|
+
if (remainingDeadlineMs <= 0)
|
|
141
|
+
terminate('deadline');
|
|
142
|
+
for (const value of ['SIGINT', 'SIGTERM']) {
|
|
143
|
+
const listener = () => terminate('parent-signal');
|
|
144
|
+
deps.onParentSignal(value, listener);
|
|
145
|
+
listeners.push(() => deps.offParentSignal(value, listener));
|
|
146
|
+
}
|
|
147
|
+
process.on('error', () => terminate('exit'));
|
|
148
|
+
process.on('exit', () => {
|
|
149
|
+
if (reason === 'exit' && deps.now() >= deadline)
|
|
150
|
+
reason = 'deadline';
|
|
151
|
+
childExited = true;
|
|
152
|
+
deps.clearTimeout(stallTimer);
|
|
153
|
+
if (!deps.isProcessGroupAlive(process.processGroupId))
|
|
154
|
+
return;
|
|
155
|
+
terminate(reason);
|
|
156
|
+
drain();
|
|
157
|
+
});
|
|
158
|
+
process.on('close', (code, value) => {
|
|
159
|
+
for (const stream of streams)
|
|
160
|
+
try {
|
|
161
|
+
stream?.flush();
|
|
162
|
+
}
|
|
163
|
+
catch (error) {
|
|
164
|
+
fail(error);
|
|
165
|
+
}
|
|
166
|
+
if (!childExited &&
|
|
167
|
+
deps.now() >= deadline &&
|
|
168
|
+
reason !== 'parent-signal' &&
|
|
169
|
+
reason !== 'deadline')
|
|
170
|
+
reason = 'deadline';
|
|
171
|
+
closeExit = { code, signal: value };
|
|
172
|
+
if (!deps.isProcessGroupAlive(process.processGroupId))
|
|
173
|
+
return finish(closeExit);
|
|
174
|
+
terminate(reason);
|
|
175
|
+
drain();
|
|
176
|
+
if (drainDone || hasFailure)
|
|
177
|
+
finish(closeExit);
|
|
178
|
+
});
|
|
179
|
+
try {
|
|
180
|
+
const setup = options.watchdog?.({
|
|
181
|
+
attempt,
|
|
182
|
+
deadline,
|
|
183
|
+
now: () => deps.now(),
|
|
184
|
+
process,
|
|
185
|
+
terminate: () => {
|
|
186
|
+
if (!childExited)
|
|
187
|
+
terminate('provider-watchdog');
|
|
188
|
+
},
|
|
189
|
+
});
|
|
190
|
+
registerWatchdogSetup(setup, watchdog, fail);
|
|
191
|
+
}
|
|
192
|
+
catch (error) {
|
|
193
|
+
fail(error);
|
|
194
|
+
}
|
|
195
|
+
});
|
|
196
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { BrowserSessionDeps, BrowserSessionOptions, BrowserSessionResult } from './types.mts';
|
|
2
|
+
export type { BrowserSessionDeps, BrowserSessionEvent, BrowserSessionExit, BrowserSessionOptions, BrowserSessionOutput, BrowserSessionProcess, BrowserSessionResult, BrowserSessionTerminationReason, BrowserSessionWatchdog, BrowserSessionWatchdogCleanup, BrowserSessionWatchdogController, } from './types.mts';
|
|
3
|
+
export { ProcessGroupDrainTimeoutError } from './process-group.mts';
|
|
4
|
+
export declare function runBrowserSession(options: BrowserSessionOptions, deps?: BrowserSessionDeps): Promise<BrowserSessionResult>;
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { runAttempt } from './attempt.mjs';
|
|
2
|
+
import { expiredResult } from './result.mjs';
|
|
3
|
+
import { isProcessGroupAlive, waitForProcessGroupExit } from './process-group.mjs';
|
|
4
|
+
export { ProcessGroupDrainTimeoutError } from './process-group.mjs';
|
|
5
|
+
const defaultDeps = {
|
|
6
|
+
clearInterval,
|
|
7
|
+
clearTimeout,
|
|
8
|
+
isProcessGroupAlive,
|
|
9
|
+
killProcessGroup: (processGroupId, signal) => {
|
|
10
|
+
try {
|
|
11
|
+
process.kill(-processGroupId, signal);
|
|
12
|
+
}
|
|
13
|
+
catch (error) {
|
|
14
|
+
if (error.code !== 'ESRCH')
|
|
15
|
+
throw error;
|
|
16
|
+
}
|
|
17
|
+
},
|
|
18
|
+
now: () => performance.now(),
|
|
19
|
+
offParentSignal: (signal, listener) => process.off(signal, listener),
|
|
20
|
+
onParentSignal: (signal, listener) => process.on(signal, listener),
|
|
21
|
+
setInterval,
|
|
22
|
+
setTimeout,
|
|
23
|
+
waitForProcessGroupExit,
|
|
24
|
+
};
|
|
25
|
+
export async function runBrowserSession(options, deps = defaultDeps) {
|
|
26
|
+
validateOptions(options);
|
|
27
|
+
if (deps === defaultDeps && process.platform === 'win32')
|
|
28
|
+
throw new Error('browser-session-runner default process-group control is unsupported on Windows');
|
|
29
|
+
const deadline = deps.now() + options.deadlineMs;
|
|
30
|
+
let last;
|
|
31
|
+
let attempts = 0;
|
|
32
|
+
for (let attempt = 1; attempt <= options.attempts && deps.now() < deadline; attempt += 1) {
|
|
33
|
+
attempts = attempt;
|
|
34
|
+
last = { ...(await runAttempt(options, deps, deadline, attempt)), attempts };
|
|
35
|
+
options.onAttemptComplete?.(snapshotResult(last));
|
|
36
|
+
if (last.reason === 'parent-signal' ||
|
|
37
|
+
last.reason === 'deadline' ||
|
|
38
|
+
options.classifyExit(last.exit, omitAttempts(last)) !== 'retry')
|
|
39
|
+
return last;
|
|
40
|
+
}
|
|
41
|
+
if (attempts === options.attempts)
|
|
42
|
+
return { ...last, attempts };
|
|
43
|
+
return { ...(last ?? expiredResult()), attempts, deadlineExceeded: true, reason: 'deadline' };
|
|
44
|
+
}
|
|
45
|
+
function validateOptions(options) {
|
|
46
|
+
const maxTimerDelay = 2_147_483_647;
|
|
47
|
+
for (const [name, value] of [
|
|
48
|
+
['attempts', options.attempts],
|
|
49
|
+
['deadlineMs', options.deadlineMs],
|
|
50
|
+
['graceMs', options.graceMs],
|
|
51
|
+
['processGroupDrainMs', options.processGroupDrainMs ?? options.graceMs],
|
|
52
|
+
['semanticStallMs', options.semanticStallMs],
|
|
53
|
+
['startupStallMs', options.startupStallMs],
|
|
54
|
+
['watchdogIntervalMs', options.watchdogIntervalMs ?? 1000],
|
|
55
|
+
]) {
|
|
56
|
+
if (!Number.isSafeInteger(value) || value <= 0 || value > maxTimerDelay)
|
|
57
|
+
throw new RangeError(`${name} must be a positive Node timer delay`);
|
|
58
|
+
}
|
|
59
|
+
const tailBytes = options.diagnosticTailBytes ?? 4096;
|
|
60
|
+
if (!Number.isSafeInteger(tailBytes) || tailBytes <= 0)
|
|
61
|
+
throw new RangeError('diagnosticTailBytes must be positive');
|
|
62
|
+
}
|
|
63
|
+
function omitAttempts({ attempts: _attempts, ...result }) {
|
|
64
|
+
return result;
|
|
65
|
+
}
|
|
66
|
+
function snapshotResult(result) {
|
|
67
|
+
return { ...result, exit: { ...result.exit } };
|
|
68
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { BrowserSessionOptions, BrowserSessionOutput } from './types.mts';
|
|
2
|
+
import type { TailQueue } from './tail-queue.mts';
|
|
3
|
+
type OutputConsumer = {
|
|
4
|
+
flush(): void;
|
|
5
|
+
write(chunk: string | Buffer): void;
|
|
6
|
+
};
|
|
7
|
+
export declare function createOutputConsumer(options: BrowserSessionOptions, tail: TailQueue, source: BrowserSessionOutput['source'], onLine: (line: string) => void): OutputConsumer;
|
|
8
|
+
export {};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { StringDecoder } from 'node:string_decoder';
|
|
2
|
+
import { boundPendingLine, splitCompleteLines } from '../process-line-buffer/index.mjs';
|
|
3
|
+
export function createOutputConsumer(options, tail, source, onLine) {
|
|
4
|
+
const decoder = new StringDecoder('utf8');
|
|
5
|
+
let pending = '';
|
|
6
|
+
const appendLines = (text) => {
|
|
7
|
+
tail.append(text);
|
|
8
|
+
const split = splitCompleteLines(pending + text);
|
|
9
|
+
pending = boundPendingLine(split.pending);
|
|
10
|
+
for (const value of split.complete)
|
|
11
|
+
onLine(value);
|
|
12
|
+
};
|
|
13
|
+
return {
|
|
14
|
+
flush: () => {
|
|
15
|
+
appendLines(decoder.end());
|
|
16
|
+
if (pending)
|
|
17
|
+
onLine(pending);
|
|
18
|
+
},
|
|
19
|
+
write: (chunk) => {
|
|
20
|
+
options.onOutput?.({ chunk, source });
|
|
21
|
+
appendLines(decoder.write(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)));
|
|
22
|
+
},
|
|
23
|
+
};
|
|
24
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export declare class ProcessGroupDrainTimeoutError extends Error {
|
|
2
|
+
constructor(processGroupId: number, timeoutMs: number);
|
|
3
|
+
}
|
|
4
|
+
export declare function isProcessGroupAlive(processGroupId: number): boolean;
|
|
5
|
+
export declare function waitForProcessGroupExit(processGroupId: number, timeoutMs: number): Promise<void>;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export class ProcessGroupDrainTimeoutError extends Error {
|
|
2
|
+
constructor(processGroupId, timeoutMs) {
|
|
3
|
+
super(`Process group ${processGroupId} did not exit within ${timeoutMs}ms after SIGKILL`);
|
|
4
|
+
this.name = 'ProcessGroupDrainTimeoutError';
|
|
5
|
+
}
|
|
6
|
+
}
|
|
7
|
+
export function isProcessGroupAlive(processGroupId) {
|
|
8
|
+
try {
|
|
9
|
+
process.kill(-processGroupId, 0);
|
|
10
|
+
return true;
|
|
11
|
+
}
|
|
12
|
+
catch (error) {
|
|
13
|
+
return error.code !== 'ESRCH';
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
export async function waitForProcessGroupExit(processGroupId, timeoutMs) {
|
|
17
|
+
const deadline = performance.now() + timeoutMs;
|
|
18
|
+
while (isProcessGroupAlive(processGroupId)) {
|
|
19
|
+
if (performance.now() >= deadline)
|
|
20
|
+
throw new ProcessGroupDrainTimeoutError(processGroupId, timeoutMs);
|
|
21
|
+
await new Promise((resolve) => setTimeout(resolve, Math.min(10, deadline - performance.now())));
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
export class TailQueue {
|
|
2
|
+
maxBytes;
|
|
3
|
+
chunks = [];
|
|
4
|
+
bytes = 0;
|
|
5
|
+
constructor(maxBytes) {
|
|
6
|
+
this.maxBytes = maxBytes;
|
|
7
|
+
}
|
|
8
|
+
append(text) {
|
|
9
|
+
const chunk = Buffer.from(text);
|
|
10
|
+
if (chunk.length >= this.maxBytes) {
|
|
11
|
+
this.chunks.splice(0, this.chunks.length, chunk.subarray(-this.maxBytes));
|
|
12
|
+
this.bytes = this.maxBytes;
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
this.chunks.push(chunk);
|
|
16
|
+
this.bytes += chunk.length;
|
|
17
|
+
while (this.bytes > this.maxBytes) {
|
|
18
|
+
const first = this.chunks[0];
|
|
19
|
+
const overflow = this.bytes - this.maxBytes;
|
|
20
|
+
if (first.length <= overflow)
|
|
21
|
+
this.chunks.shift();
|
|
22
|
+
else
|
|
23
|
+
this.chunks[0] = first.subarray(overflow);
|
|
24
|
+
this.bytes -= Math.min(first.length, overflow);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
toBuffer() {
|
|
28
|
+
return Buffer.concat(this.chunks, this.bytes);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function tailText(tail: Buffer, maxBytes: number): string;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export function tailText(tail, maxBytes) {
|
|
2
|
+
const bounded = tail.subarray(Math.max(0, tail.length - maxBytes));
|
|
3
|
+
for (let start = 0; start < Math.min(4, bounded.length); start += 1) {
|
|
4
|
+
for (let end = 0; end < Math.min(4, bounded.length - start); end += 1) {
|
|
5
|
+
try {
|
|
6
|
+
return new TextDecoder('utf-8', { fatal: true }).decode(bounded.subarray(start, bounded.length - end));
|
|
7
|
+
}
|
|
8
|
+
catch { }
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
return '';
|
|
12
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import type { ChildProcess } from 'node:child_process';
|
|
2
|
+
export type BrowserSessionEvent = 'startup' | 'semantic';
|
|
3
|
+
export type BrowserSessionExit = {
|
|
4
|
+
code: number | null;
|
|
5
|
+
signal: NodeJS.Signals | null;
|
|
6
|
+
};
|
|
7
|
+
export type BrowserSessionOutput = {
|
|
8
|
+
chunk: string | Buffer;
|
|
9
|
+
source: 'stderr' | 'stdout';
|
|
10
|
+
};
|
|
11
|
+
export type BrowserSessionTerminationReason = 'deadline' | 'parent-signal' | 'provider-watchdog' | 'semantic-stall' | 'startup-stall';
|
|
12
|
+
type BrowserSessionStream = {
|
|
13
|
+
on(event: 'data', listener: (chunk: string | Buffer) => void): unknown;
|
|
14
|
+
on(event: 'error', listener: (error: Error) => void): unknown;
|
|
15
|
+
};
|
|
16
|
+
export type BrowserSessionResult = {
|
|
17
|
+
attempts: number;
|
|
18
|
+
deadlineExceeded: boolean;
|
|
19
|
+
diagnosticTail: string;
|
|
20
|
+
exit: BrowserSessionExit;
|
|
21
|
+
reason: 'exit' | BrowserSessionTerminationReason;
|
|
22
|
+
startupProgress: boolean;
|
|
23
|
+
semanticProgress: boolean;
|
|
24
|
+
};
|
|
25
|
+
export type BrowserSessionProcess = Pick<ChildProcess, 'kill'> & {
|
|
26
|
+
processGroupId: number;
|
|
27
|
+
on(event: 'close', listener: (code: number | null, signal: NodeJS.Signals | null) => void): unknown;
|
|
28
|
+
on(event: 'error', listener: (error: Error) => void): unknown;
|
|
29
|
+
on(event: 'exit', listener: (code: number | null, signal: NodeJS.Signals | null) => void): unknown;
|
|
30
|
+
stderr?: BrowserSessionStream | null;
|
|
31
|
+
stdout?: BrowserSessionStream | null;
|
|
32
|
+
};
|
|
33
|
+
export type BrowserSessionDeps = {
|
|
34
|
+
clearInterval(handle: unknown): void;
|
|
35
|
+
clearTimeout(handle: unknown): void;
|
|
36
|
+
isProcessGroupAlive(processGroupId: number): boolean;
|
|
37
|
+
killProcessGroup(processGroupId: number, signal: NodeJS.Signals): void;
|
|
38
|
+
now(): number;
|
|
39
|
+
offParentSignal(signal: NodeJS.Signals, listener: () => void): void;
|
|
40
|
+
onParentSignal(signal: NodeJS.Signals, listener: () => void): void;
|
|
41
|
+
setInterval(callback: () => void, ms: number): unknown;
|
|
42
|
+
setTimeout(callback: () => void, ms: number): unknown;
|
|
43
|
+
waitForProcessGroupExit(processGroupId: number, timeoutMs: number): Promise<void>;
|
|
44
|
+
};
|
|
45
|
+
export type BrowserSessionWatchdogController = {
|
|
46
|
+
attempt: number;
|
|
47
|
+
deadline: number;
|
|
48
|
+
now(): number;
|
|
49
|
+
process: BrowserSessionProcess;
|
|
50
|
+
terminate(reason?: 'provider-watchdog'): void;
|
|
51
|
+
};
|
|
52
|
+
export type BrowserSessionWatchdogCleanup = () => void;
|
|
53
|
+
export type BrowserSessionWatchdog = (controller: BrowserSessionWatchdogController) => void | BrowserSessionWatchdogCleanup | Promise<void | BrowserSessionWatchdogCleanup>;
|
|
54
|
+
export type BrowserSessionOptions = {
|
|
55
|
+
attempts: number;
|
|
56
|
+
classifyExit(exit: BrowserSessionExit, result: Omit<BrowserSessionResult, 'attempts'>): 'retry' | 'return';
|
|
57
|
+
deadlineMs: number;
|
|
58
|
+
diagnosticTailBytes?: number;
|
|
59
|
+
graceMs: number;
|
|
60
|
+
onLine(line: string): BrowserSessionEvent | undefined;
|
|
61
|
+
onOutput?(output: BrowserSessionOutput): void;
|
|
62
|
+
onAttemptComplete?(result: BrowserSessionResult): void;
|
|
63
|
+
processGroupDrainMs?: number;
|
|
64
|
+
semanticStallMs: number;
|
|
65
|
+
start(attempt: number): BrowserSessionProcess;
|
|
66
|
+
startupStallMs: number;
|
|
67
|
+
watchdogIntervalMs?: number;
|
|
68
|
+
watchdog?: BrowserSessionWatchdog;
|
|
69
|
+
};
|
|
70
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export type WatchdogCleanup = () => void;
|
|
2
|
+
type WatchdogSetup = void | WatchdogCleanup | Promise<void | WatchdogCleanup>;
|
|
3
|
+
type WatchdogLifecycle = ReturnType<typeof createWatchdogLifecycle>;
|
|
4
|
+
export declare function createWatchdogLifecycle(): {
|
|
5
|
+
complete: (onError: (error: unknown) => void) => void;
|
|
6
|
+
isComplete: () => boolean;
|
|
7
|
+
register: (next: void | WatchdogCleanup, onError: (error: unknown) => void) => void;
|
|
8
|
+
};
|
|
9
|
+
export declare function registerWatchdogSetup(setup: WatchdogSetup, lifecycle: WatchdogLifecycle, onError: (error: unknown) => void): void;
|
|
10
|
+
export {};
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
export function createWatchdogLifecycle() {
|
|
2
|
+
let complete = false;
|
|
3
|
+
let cleanup;
|
|
4
|
+
const run = (next, onError) => {
|
|
5
|
+
try {
|
|
6
|
+
next();
|
|
7
|
+
}
|
|
8
|
+
catch (error) {
|
|
9
|
+
onError(error);
|
|
10
|
+
}
|
|
11
|
+
};
|
|
12
|
+
return {
|
|
13
|
+
complete: (onError) => {
|
|
14
|
+
complete = true;
|
|
15
|
+
if (!cleanup)
|
|
16
|
+
return;
|
|
17
|
+
const next = cleanup;
|
|
18
|
+
cleanup = undefined;
|
|
19
|
+
run(next, onError);
|
|
20
|
+
},
|
|
21
|
+
isComplete: () => complete,
|
|
22
|
+
register: (next, onError) => {
|
|
23
|
+
if (!next)
|
|
24
|
+
return;
|
|
25
|
+
if (complete)
|
|
26
|
+
return run(next, onError);
|
|
27
|
+
cleanup = next;
|
|
28
|
+
},
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
export function registerWatchdogSetup(setup, lifecycle, onError) {
|
|
32
|
+
if (!(setup instanceof Promise))
|
|
33
|
+
return lifecycle.register(setup, onError);
|
|
34
|
+
void setup.then((cleanup) => lifecycle.register(cleanup, onError), (error) => {
|
|
35
|
+
if (!lifecycle.isComplete())
|
|
36
|
+
onError(error);
|
|
37
|
+
});
|
|
38
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function runRetrospectiveTranscriptCommand(args: string[]): Promise<number>;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { parseArgs } from 'node:util';
|
|
2
|
+
import { runRetrospectiveTranscript, } from '../../retrospective-transcript/index.mjs';
|
|
3
|
+
export async function runRetrospectiveTranscriptCommand(args) {
|
|
4
|
+
try {
|
|
5
|
+
const { values } = parseArgs({
|
|
6
|
+
args,
|
|
7
|
+
strict: true,
|
|
8
|
+
options: {
|
|
9
|
+
'session-id': { type: 'string' },
|
|
10
|
+
jsonl: { type: 'string' },
|
|
11
|
+
'projects-dir': { type: 'string' },
|
|
12
|
+
'codex-sessions-dir': { type: 'string' },
|
|
13
|
+
'grok-sessions-dir': { type: 'string' },
|
|
14
|
+
},
|
|
15
|
+
});
|
|
16
|
+
const options = {
|
|
17
|
+
...(values['session-id'] ? { sessionId: values['session-id'] } : {}),
|
|
18
|
+
...(values.jsonl ? { jsonlPath: values.jsonl } : {}),
|
|
19
|
+
...(values['projects-dir'] ? { projectsDir: values['projects-dir'] } : {}),
|
|
20
|
+
...(values['codex-sessions-dir'] ? { codexSessionsDir: values['codex-sessions-dir'] } : {}),
|
|
21
|
+
...(values['grok-sessions-dir'] ? { grokSessionsDir: values['grok-sessions-dir'] } : {}),
|
|
22
|
+
env: process.env,
|
|
23
|
+
};
|
|
24
|
+
process.stdout.write(await runRetrospectiveTranscript(options));
|
|
25
|
+
return 0;
|
|
26
|
+
}
|
|
27
|
+
catch (error) {
|
|
28
|
+
process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
|
|
29
|
+
return 2;
|
|
30
|
+
}
|
|
31
|
+
}
|
package/dist/cli/index.mjs
CHANGED
|
@@ -14,6 +14,7 @@ import { runPostReviewCommand } from './commands/post-review.mjs';
|
|
|
14
14
|
import { runStageReviewPayloadCommand } from './commands/stage-review-payload.mjs';
|
|
15
15
|
import { runSwiftSemanticEqualCommand } from './commands/swift-semantic-equal.mjs';
|
|
16
16
|
import { runVitestBlobManifestCommand } from './commands/vitest-blob-manifest.mjs';
|
|
17
|
+
import { runRetrospectiveTranscriptCommand } from './commands/retrospective-transcript.mjs';
|
|
17
18
|
import { runWithHostLock } from './commands/with-host-lock.mjs';
|
|
18
19
|
import { parseCli } from './parse.mjs';
|
|
19
20
|
import { packageScriptPath } from './script-path.mjs';
|
|
@@ -92,6 +93,8 @@ export function runCli(argv = process.argv) {
|
|
|
92
93
|
return runHttpOrigin(parsed.field, parsed.value);
|
|
93
94
|
case 'gha-artifacts-cleanup':
|
|
94
95
|
return runGhaArtifactsCleanup(parsed);
|
|
96
|
+
case 'retrospective-transcript':
|
|
97
|
+
return runRetrospectiveTranscriptCommand(parsed.args);
|
|
95
98
|
}
|
|
96
99
|
}
|
|
97
100
|
function readInstalledVersion() {
|
package/dist/cli/parse.d.mts
CHANGED
|
@@ -40,6 +40,9 @@ export type ParsedCli = {
|
|
|
40
40
|
kind: 'http-origin';
|
|
41
41
|
field: string;
|
|
42
42
|
value: string;
|
|
43
|
+
} | {
|
|
44
|
+
kind: 'retrospective-transcript';
|
|
45
|
+
args: string[];
|
|
43
46
|
} | ParsedGhaRuntimeAudit | ParsedGhaArtifactsCleanup;
|
|
44
47
|
export type ScriptCommand = 'gha-output' | 'gha-needs-results' | 'download-with-diagnostics' | 'host-pressure-diagnostics' | 'allocate-browser-safe-ports' | 'diagnose-port-collision' | 'prepare-trivy-db' | 'check-cache-size' | 'make-shard-matrix' | 'load-runner-env' | 'clean-workspace' | 'install-github-release' | 'run-with-timeout' | 'lint-links' | 'materialize-pr-context' | 'wait-for-apt-locks' | 'install-playwright-chromium-arm64' | 'ghcr-package-retention';
|
|
45
48
|
export declare function parseCli(argv: readonly string[]): ParsedCli;
|