vouchington-tooling 0.0.21 → 0.1.0

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.
Files changed (44) hide show
  1. package/README.md +28 -0
  2. package/dist/browser-session-runner/attempt.d.mts +2 -0
  3. package/dist/browser-session-runner/attempt.mjs +195 -0
  4. package/dist/browser-session-runner/index.d.mts +4 -0
  5. package/dist/browser-session-runner/index.mjs +64 -0
  6. package/dist/browser-session-runner/process-group.d.mts +5 -0
  7. package/dist/browser-session-runner/process-group.mjs +23 -0
  8. package/dist/browser-session-runner/result.d.mts +2 -0
  9. package/dist/browser-session-runner/result.mjs +11 -0
  10. package/dist/browser-session-runner/tail-queue.d.mts +8 -0
  11. package/dist/browser-session-runner/tail-queue.mjs +30 -0
  12. package/dist/browser-session-runner/tail.d.mts +1 -0
  13. package/dist/browser-session-runner/tail.mjs +12 -0
  14. package/dist/browser-session-runner/types.d.mts +53 -0
  15. package/dist/browser-session-runner/types.mjs +1 -0
  16. package/dist/cli/commands/retrospective-transcript.d.mts +1 -0
  17. package/dist/cli/commands/retrospective-transcript.mjs +31 -0
  18. package/dist/cli/index.mjs +3 -0
  19. package/dist/cli/parse.d.mts +3 -0
  20. package/dist/cli/parse.mjs +2 -0
  21. package/dist/cli/usage.d.mts +1 -1
  22. package/dist/cli/usage.mjs +2 -0
  23. package/dist/coverage-transport/outcome.mjs +3 -1
  24. package/dist/index.d.mts +6 -0
  25. package/dist/index.mjs +3 -0
  26. package/dist/retrospective-transcript/claude.d.mts +2 -0
  27. package/dist/retrospective-transcript/claude.mjs +67 -0
  28. package/dist/retrospective-transcript/codex.d.mts +11 -0
  29. package/dist/retrospective-transcript/codex.mjs +174 -0
  30. package/dist/retrospective-transcript/format.d.mts +4 -0
  31. package/dist/retrospective-transcript/format.mjs +22 -0
  32. package/dist/retrospective-transcript/index.d.mts +22 -0
  33. package/dist/retrospective-transcript/index.mjs +151 -0
  34. package/dist/retrospective-transcript/javascript-command.d.mts +1 -0
  35. package/dist/retrospective-transcript/javascript-command.mjs +25 -0
  36. package/dist/retrospective-transcript/shared.d.mts +31 -0
  37. package/dist/retrospective-transcript/shared.mjs +213 -0
  38. package/dist/vitest-diagnostics/directory.d.mts +12 -0
  39. package/dist/vitest-diagnostics/directory.mjs +44 -0
  40. package/dist/vitest-diagnostics/index.d.mts +22 -0
  41. package/dist/vitest-diagnostics/index.mjs +123 -0
  42. package/dist/vitest-diagnostics/read-file.d.mts +2 -0
  43. package/dist/vitest-diagnostics/read-file.mjs +37 -0
  44. 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,2 @@
1
+ import type { BrowserSessionDeps, BrowserSessionOptions, BrowserSessionResult } from './types.mts';
2
+ export declare function runAttempt(options: BrowserSessionOptions, deps: BrowserSessionDeps, deadline: number, attempt: number): Promise<BrowserSessionResult>;
@@ -0,0 +1,195 @@
1
+ import { StringDecoder } from 'node:string_decoder';
2
+ import { boundPendingLine, splitCompleteLines } from '../process-line-buffer/index.mjs';
3
+ import { tailText } from './tail.mjs';
4
+ import { TailQueue } from './tail-queue.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 listeners = [];
24
+ const finish = (exit) => {
25
+ if (complete)
26
+ return;
27
+ complete = true;
28
+ deps.clearTimeout(deadlineTimer);
29
+ if (killTimer)
30
+ deps.clearTimeout(killTimer);
31
+ deps.clearTimeout(stallTimer);
32
+ for (const remove of listeners)
33
+ remove();
34
+ const result = {
35
+ attempts: 0,
36
+ deadlineExceeded: reason === 'deadline',
37
+ diagnosticTail: tailText(tail.toBuffer(), maxTail),
38
+ exit,
39
+ reason,
40
+ semanticProgress,
41
+ startupProgress,
42
+ };
43
+ if (hasFailure)
44
+ reject(failure);
45
+ else
46
+ resolve(result);
47
+ };
48
+ const signal = (value) => {
49
+ try {
50
+ deps.killProcessGroup(process.processGroupId, value);
51
+ }
52
+ catch { }
53
+ try {
54
+ process.kill(value);
55
+ }
56
+ catch { }
57
+ };
58
+ const terminate = (nextReason) => {
59
+ if (nextReason === 'deadline' && childExited && reason === 'exit')
60
+ return;
61
+ if (nextReason === 'parent-signal' || nextReason === 'deadline') {
62
+ if (reason === 'exit' || reason === 'startup-stall' || reason === 'semantic-stall')
63
+ reason = nextReason;
64
+ else if (reason !== nextReason)
65
+ return;
66
+ }
67
+ else if (reason !== 'exit')
68
+ return;
69
+ else if (nextReason !== 'exit')
70
+ reason = nextReason;
71
+ if (terminating)
72
+ return;
73
+ terminating = true;
74
+ killTimer = deps.setTimeout(() => {
75
+ if (deps.isProcessGroupAlive(process.processGroupId))
76
+ signal('SIGKILL');
77
+ }, options.graceMs);
78
+ signal('SIGTERM');
79
+ };
80
+ const fail = (error) => {
81
+ if (hasFailure)
82
+ return;
83
+ failure = error;
84
+ hasFailure = true;
85
+ terminate('exit');
86
+ };
87
+ const drain = () => {
88
+ if (draining)
89
+ return;
90
+ draining = true;
91
+ void deps.waitForProcessGroupExit(process.processGroupId, drainTimeoutMs).then(() => {
92
+ drainDone = true;
93
+ if (closeExit)
94
+ finish(closeExit);
95
+ }, (error) => {
96
+ fail(error);
97
+ finish(closeExit ?? { code: null, signal: null });
98
+ });
99
+ };
100
+ const armStall = (ms, nextReason) => {
101
+ if (stallTimer)
102
+ deps.clearTimeout(stallTimer);
103
+ stallTimer = deps.setTimeout(() => terminate(nextReason), ms);
104
+ };
105
+ const consume = () => {
106
+ const decoder = new StringDecoder('utf8');
107
+ let pending = '';
108
+ const line = (value) => {
109
+ const event = options.onLine(value);
110
+ if (event === 'startup' && !startupProgress) {
111
+ startupProgress = true;
112
+ if (!childExited)
113
+ armStall(options.semanticStallMs, 'semantic-stall');
114
+ }
115
+ if (event === 'semantic') {
116
+ semanticProgress = true;
117
+ if (startupProgress && !childExited)
118
+ armStall(options.semanticStallMs, 'semantic-stall');
119
+ }
120
+ };
121
+ const appendLines = (text) => {
122
+ tail.append(text);
123
+ const split = splitCompleteLines(pending + text);
124
+ pending = boundPendingLine(split.pending);
125
+ for (const value of split.complete)
126
+ line(value);
127
+ };
128
+ return {
129
+ flush: () => {
130
+ appendLines(decoder.end());
131
+ if (pending)
132
+ line(pending);
133
+ },
134
+ write: (chunk) => appendLines(decoder.write(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk))),
135
+ };
136
+ };
137
+ const streams = [process.stdout, process.stderr].map((stream) => stream ? consume() : undefined);
138
+ for (const [index, stream] of [process.stdout, process.stderr].entries())
139
+ stream?.on('data', (chunk) => {
140
+ try {
141
+ streams[index].write(chunk);
142
+ }
143
+ catch (error) {
144
+ fail(error);
145
+ }
146
+ });
147
+ for (const stream of [process.stdout, process.stderr])
148
+ stream?.on('error', fail);
149
+ const remainingDeadlineMs = deadline - deps.now();
150
+ const deadlineTimer = deps.setTimeout(() => terminate('deadline'), Math.max(1, remainingDeadlineMs));
151
+ const remainingStartupMs = options.startupStallMs - (deps.now() - startedAt);
152
+ armStall(Math.max(1, remainingStartupMs), 'startup-stall');
153
+ if (remainingStartupMs <= 0)
154
+ terminate('startup-stall');
155
+ if (remainingDeadlineMs <= 0)
156
+ terminate('deadline');
157
+ for (const value of ['SIGINT', 'SIGTERM']) {
158
+ const listener = () => terminate('parent-signal');
159
+ deps.onParentSignal(value, listener);
160
+ listeners.push(() => deps.offParentSignal(value, listener));
161
+ }
162
+ process.on('error', () => terminate('exit'));
163
+ process.on('exit', () => {
164
+ if (reason === 'exit' && deps.now() >= deadline)
165
+ reason = 'deadline';
166
+ childExited = true;
167
+ deps.clearTimeout(stallTimer);
168
+ if (!deps.isProcessGroupAlive(process.processGroupId))
169
+ return;
170
+ terminate(reason);
171
+ drain();
172
+ });
173
+ process.on('close', (code, value) => {
174
+ for (const stream of streams)
175
+ try {
176
+ stream?.flush();
177
+ }
178
+ catch (error) {
179
+ fail(error);
180
+ }
181
+ if (!childExited &&
182
+ deps.now() >= deadline &&
183
+ reason !== 'parent-signal' &&
184
+ reason !== 'deadline')
185
+ reason = 'deadline';
186
+ closeExit = { code, signal: value };
187
+ if (!deps.isProcessGroupAlive(process.processGroupId))
188
+ return finish(closeExit);
189
+ terminate(reason);
190
+ drain();
191
+ if (drainDone || hasFailure)
192
+ finish(closeExit);
193
+ });
194
+ });
195
+ }
@@ -0,0 +1,4 @@
1
+ import type { BrowserSessionDeps, BrowserSessionOptions, BrowserSessionResult } from './types.mts';
2
+ export type { BrowserSessionDeps, BrowserSessionEvent, BrowserSessionExit, BrowserSessionOptions, BrowserSessionProcess, BrowserSessionResult, } from './types.mts';
3
+ export { ProcessGroupDrainTimeoutError } from './process-group.mts';
4
+ export declare function runBrowserSession(options: BrowserSessionOptions, deps?: BrowserSessionDeps): Promise<BrowserSessionResult>;
@@ -0,0 +1,64 @@
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);
35
+ if (last.reason === 'parent-signal' ||
36
+ last.reason === 'deadline' ||
37
+ options.classifyExit(last.exit, omitAttempts(last)) !== 'retry')
38
+ return { ...last, attempts };
39
+ }
40
+ if (attempts === options.attempts)
41
+ return { ...last, attempts };
42
+ return { ...(last ?? expiredResult()), attempts, deadlineExceeded: true, reason: 'deadline' };
43
+ }
44
+ function validateOptions(options) {
45
+ const maxTimerDelay = 2_147_483_647;
46
+ for (const [name, value] of [
47
+ ['attempts', options.attempts],
48
+ ['deadlineMs', options.deadlineMs],
49
+ ['graceMs', options.graceMs],
50
+ ['processGroupDrainMs', options.processGroupDrainMs ?? options.graceMs],
51
+ ['semanticStallMs', options.semanticStallMs],
52
+ ['startupStallMs', options.startupStallMs],
53
+ ['watchdogIntervalMs', options.watchdogIntervalMs ?? 1000],
54
+ ]) {
55
+ if (!Number.isSafeInteger(value) || value <= 0 || value > maxTimerDelay)
56
+ throw new RangeError(`${name} must be a positive Node timer delay`);
57
+ }
58
+ const tailBytes = options.diagnosticTailBytes ?? 4096;
59
+ if (!Number.isSafeInteger(tailBytes) || tailBytes <= 0)
60
+ throw new RangeError('diagnosticTailBytes must be positive');
61
+ }
62
+ function omitAttempts({ attempts: _attempts, ...result }) {
63
+ return result;
64
+ }
@@ -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,2 @@
1
+ import type { BrowserSessionResult } from './types.mts';
2
+ export declare function expiredResult(): BrowserSessionResult;
@@ -0,0 +1,11 @@
1
+ export function expiredResult() {
2
+ return {
3
+ attempts: 0,
4
+ deadlineExceeded: true,
5
+ diagnosticTail: '',
6
+ exit: { code: null, signal: null },
7
+ reason: 'deadline',
8
+ semanticProgress: false,
9
+ startupProgress: false,
10
+ };
11
+ }
@@ -0,0 +1,8 @@
1
+ export declare class TailQueue {
2
+ readonly maxBytes: number;
3
+ readonly chunks: Buffer[];
4
+ bytes: number;
5
+ constructor(maxBytes: number);
6
+ append(text: string): void;
7
+ toBuffer(): Buffer;
8
+ }
@@ -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,53 @@
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
+ type BrowserSessionStream = {
8
+ on(event: 'data', listener: (chunk: string | Buffer) => void): unknown;
9
+ on(event: 'error', listener: (error: Error) => void): unknown;
10
+ };
11
+ export type BrowserSessionResult = {
12
+ attempts: number;
13
+ deadlineExceeded: boolean;
14
+ diagnosticTail: string;
15
+ exit: BrowserSessionExit;
16
+ reason: 'exit' | 'parent-signal' | 'semantic-stall' | 'startup-stall' | 'deadline';
17
+ startupProgress: boolean;
18
+ semanticProgress: boolean;
19
+ };
20
+ export type BrowserSessionProcess = Pick<ChildProcess, 'kill'> & {
21
+ processGroupId: number;
22
+ on(event: 'close', listener: (code: number | null, signal: NodeJS.Signals | null) => void): unknown;
23
+ on(event: 'error', listener: (error: Error) => void): unknown;
24
+ on(event: 'exit', listener: (code: number | null, signal: NodeJS.Signals | null) => void): unknown;
25
+ stderr?: BrowserSessionStream | null;
26
+ stdout?: BrowserSessionStream | null;
27
+ };
28
+ export type BrowserSessionDeps = {
29
+ clearInterval(handle: unknown): void;
30
+ clearTimeout(handle: unknown): void;
31
+ isProcessGroupAlive(processGroupId: number): boolean;
32
+ killProcessGroup(processGroupId: number, signal: NodeJS.Signals): void;
33
+ now(): number;
34
+ offParentSignal(signal: NodeJS.Signals, listener: () => void): void;
35
+ onParentSignal(signal: NodeJS.Signals, listener: () => void): void;
36
+ setInterval(callback: () => void, ms: number): unknown;
37
+ setTimeout(callback: () => void, ms: number): unknown;
38
+ waitForProcessGroupExit(processGroupId: number, timeoutMs: number): Promise<void>;
39
+ };
40
+ export type BrowserSessionOptions = {
41
+ attempts: number;
42
+ classifyExit(exit: BrowserSessionExit, result: Omit<BrowserSessionResult, 'attempts'>): 'retry' | 'return';
43
+ deadlineMs: number;
44
+ diagnosticTailBytes?: number;
45
+ graceMs: number;
46
+ onLine(line: string): BrowserSessionEvent | undefined;
47
+ processGroupDrainMs?: number;
48
+ semanticStallMs: number;
49
+ start(attempt: number): BrowserSessionProcess;
50
+ startupStallMs: number;
51
+ watchdogIntervalMs?: number;
52
+ };
53
+ export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -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
+ }
@@ -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() {
@@ -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;
@@ -47,6 +47,8 @@ export function parseCli(argv) {
47
47
  return { kind: 'stage-review-payload', args: rest };
48
48
  if (command === 'http-origin')
49
49
  return parseHttpOrigin(rest);
50
+ if (command === 'retrospective-transcript')
51
+ return { kind: 'retrospective-transcript', args: rest };
50
52
  if (command === 'gha-artifacts-cleanup')
51
53
  return parseGhaArtifactsCleanup(rest);
52
54
  if (command !== undefined && SCRIPT_COMMANDS.has(command)) {
@@ -1,2 +1,2 @@
1
- export declare const USAGE = "Usage: vouchington <command> [options]\n\nCommands:\n runner-port-policy Print or validate a runner port policy\n with-host-lock Run a command under a host-wide lock\n gha-runtime-audit Audit successful GitHub Actions job runtimes\n gha-output Write a collision-safe multiline GITHUB_OUTPUT record\n gha-needs-results Fail if required GitHub Actions job results failed\n download-with-diagnostics Download a URL and report HTTP status on failure\n host-pressure-diagnostics Print a bounded host memory/OOM/PSI snapshot\n allocate-browser-safe-ports Allocate Fetch-safe localhost ports\n diagnose-port-collision Capture bounded localhost port diagnostics\n prepare-trivy-db Download the Trivy vulnerability database\n gha-artifacts-cleanup Delete classified GitHub Actions artifacts\n http-origin Validate an optional HTTP(S) origin\n vitest-blob-manifest Stamp a vitest-blob-manifest:v1 identity file\n pnpm-install Install a pnpm workspace with retry and release-age fail-fast\n check-cache-size Measure a path and decide whether to save a GHA cache\n make-shard-matrix Emit a [1..N] GitHub Actions shard matrix\n load-runner-env Overlay a runner env file onto GITHUB_ENV with injection guards\n clean-workspace Reset a persistent-runner workspace with a fork-PR trust gate\n install-github-release Download a checksum-verified GitHub Release binary\n run-with-timeout Run a command with GNU timeout or a Perl fallback\n lint-links Two-pass lychee: internal links fail, external warn\n materialize-pr-context Dump PR title/body/files/diff/comments and #N crawl\n wait-for-apt-locks Wait until apt/dpkg lock files are free\n install-playwright-chromium-arm64 Install Playwright Chromium from browsers.json\n ghcr-package-retention Delete old GHCR package versions past KEEP_MIN\n nuget-central-version Validate a Directory.Packages.props PackageVersion delta\n swift-semantic-equal Compare Swift sources ignoring comments and whitespace\n post-review Post one COMMENT review from a staged payload file\n stage-review-payload Validate a review payload file into a staging directory\n\nOptions:\n -h, --help Show this help\n -v, --version Print the package version\n\nrunner-port-policy\n (no args) Print the shipped policy as JSON\n --file <path> Validate and print a policy file\n --reserved <port> Print true if the port is reserved\n\nwith-host-lock\n --name <family>\n [--slots <n>]\n --timeout-seconds <n>\n [--command-timeout-seconds <n>]\n [--failure-diagnostics <absolute-script>]\n [--on-acquire-timeout fail|run-unlocked]\n -- <command> [args...]\n\ngha-runtime-audit\n [--repository owner/name] Default GITHUB_REPOSITORY\n [--branch main]\n --pr-workflow <name|/regex/> Repeatable\n --push-workflow <name|/regex/> Repeatable\n\ngha-output <name>\ngha-needs-results [label]\ndownload-with-diagnostics <url> <destination> [-- curl-args...]\nhost-pressure-diagnostics\nallocate-browser-safe-ports [count] [--policy path] [--forbidden-ports path]\ndiagnose-port-collision [--ports \"2200 2216\"] [--output-dir PATH]\nprepare-trivy-db\ngha-artifacts-cleanup run --run-id <id> [--keep-pattern glob] [--delete-pattern glob] [--patterns-file json]\ngha-artifacts-cleanup sweep --older-than-hours <n> [--keep-pattern glob] [--delete-pattern glob] [--patterns-file json]\nhttp-origin [--field NAME] [value]\nvitest-blob-manifest <suite> [reports-directory]\npnpm-install --runner-lifecycle persistent|ephemeral|ephemeral-full --install-scripts true|false\ncheck-cache-size <path> <max-bytes> <label>\nmake-shard-matrix <total>\nload-runner-env\nclean-workspace\ninstall-github-release --repo owner/name --version X --asset 'name-{platform}.tar.gz' --bin name [--tag-prefix PREFIX] [--no-checksum] [--checksums-asset NAME] [--version-flag FLAG] [--bin-dir DIR]\nrun-with-timeout <timeout-seconds> <kill-after-seconds> <command...>\nlint-links [--offline] [--config PATH] [--glob PATTERN] [files...]\nmaterialize-pr-context\nwait-for-apt-locks\ninstall-playwright-chromium-arm64 [name:archive...]\nghcr-package-retention <url-encoded-package>...\nnuget-central-version <trusted-props> <candidate-props> <metadata-json> <output-props>\nswift-semantic-equal <base> <head> <file.swift>\npost-review\nstage-review-payload optional|required <source> <destination>\n";
1
+ export declare const USAGE = "Usage: vouchington <command> [options]\n\nCommands:\n runner-port-policy Print or validate a runner port policy\n with-host-lock Run a command under a host-wide lock\n gha-runtime-audit Audit successful GitHub Actions job runtimes\n gha-output Write a collision-safe multiline GITHUB_OUTPUT record\n gha-needs-results Fail if required GitHub Actions job results failed\n download-with-diagnostics Download a URL and report HTTP status on failure\n host-pressure-diagnostics Print a bounded host memory/OOM/PSI snapshot\n allocate-browser-safe-ports Allocate Fetch-safe localhost ports\n diagnose-port-collision Capture bounded localhost port diagnostics\n prepare-trivy-db Download the Trivy vulnerability database\n gha-artifacts-cleanup Delete classified GitHub Actions artifacts\n http-origin Validate an optional HTTP(S) origin\n vitest-blob-manifest Stamp a vitest-blob-manifest:v1 identity file\n pnpm-install Install a pnpm workspace with retry and release-age fail-fast\n check-cache-size Measure a path and decide whether to save a GHA cache\n make-shard-matrix Emit a [1..N] GitHub Actions shard matrix\n load-runner-env Overlay a runner env file onto GITHUB_ENV with injection guards\n clean-workspace Reset a persistent-runner workspace with a fork-PR trust gate\n install-github-release Download a checksum-verified GitHub Release binary\n run-with-timeout Run a command with GNU timeout or a Perl fallback\n lint-links Two-pass lychee: internal links fail, external warn\n materialize-pr-context Dump PR title/body/files/diff/comments and #N crawl\n wait-for-apt-locks Wait until apt/dpkg lock files are free\n install-playwright-chromium-arm64 Install Playwright Chromium from browsers.json\n ghcr-package-retention Delete old GHCR package versions past KEEP_MIN\n nuget-central-version Validate a Directory.Packages.props PackageVersion delta\n swift-semantic-equal Compare Swift sources ignoring comments and whitespace\n post-review Post one COMMENT review from a staged payload file\n stage-review-payload Validate a review payload file into a staging directory\n retrospective-transcript Format facts from Claude-compatible, Codex, or Grok transcripts\n\nOptions:\n -h, --help Show this help\n -v, --version Print the package version\n\nrunner-port-policy\n (no args) Print the shipped policy as JSON\n --file <path> Validate and print a policy file\n --reserved <port> Print true if the port is reserved\n\nwith-host-lock\n --name <family>\n [--slots <n>]\n --timeout-seconds <n>\n [--command-timeout-seconds <n>]\n [--failure-diagnostics <absolute-script>]\n [--on-acquire-timeout fail|run-unlocked]\n -- <command> [args...]\n\ngha-runtime-audit\n [--repository owner/name] Default GITHUB_REPOSITORY\n [--branch main]\n --pr-workflow <name|/regex/> Repeatable\n --push-workflow <name|/regex/> Repeatable\n\ngha-output <name>\ngha-needs-results [label]\ndownload-with-diagnostics <url> <destination> [-- curl-args...]\nhost-pressure-diagnostics\nallocate-browser-safe-ports [count] [--policy path] [--forbidden-ports path]\ndiagnose-port-collision [--ports \"2200 2216\"] [--output-dir PATH]\nprepare-trivy-db\ngha-artifacts-cleanup run --run-id <id> [--keep-pattern glob] [--delete-pattern glob] [--patterns-file json]\ngha-artifacts-cleanup sweep --older-than-hours <n> [--keep-pattern glob] [--delete-pattern glob] [--patterns-file json]\nhttp-origin [--field NAME] [value]\nvitest-blob-manifest <suite> [reports-directory]\npnpm-install --runner-lifecycle persistent|ephemeral|ephemeral-full --install-scripts true|false\ncheck-cache-size <path> <max-bytes> <label>\nmake-shard-matrix <total>\nload-runner-env\nclean-workspace\ninstall-github-release --repo owner/name --version X --asset 'name-{platform}.tar.gz' --bin name [--tag-prefix PREFIX] [--no-checksum] [--checksums-asset NAME] [--version-flag FLAG] [--bin-dir DIR]\nrun-with-timeout <timeout-seconds> <kill-after-seconds> <command...>\nlint-links [--offline] [--config PATH] [--glob PATTERN] [files...]\nmaterialize-pr-context\nwait-for-apt-locks\ninstall-playwright-chromium-arm64 [name:archive...]\nghcr-package-retention <url-encoded-package>...\nnuget-central-version <trusted-props> <candidate-props> <metadata-json> <output-props>\nswift-semantic-equal <base> <head> <file.swift>\npost-review\nstage-review-payload optional|required <source> <destination>\nretrospective-transcript [--session-id ID] [--jsonl PATH] [--projects-dir PATH] [--codex-sessions-dir PATH] [--grok-sessions-dir PATH]\n";
2
2
  export declare function printUsage(stream?: NodeJS.WritableStream): void;
@@ -30,6 +30,7 @@ Commands:
30
30
  swift-semantic-equal Compare Swift sources ignoring comments and whitespace
31
31
  post-review Post one COMMENT review from a staged payload file
32
32
  stage-review-payload Validate a review payload file into a staging directory
33
+ retrospective-transcript Format facts from Claude-compatible, Codex, or Grok transcripts
33
34
 
34
35
  Options:
35
36
  -h, --help Show this help
@@ -82,6 +83,7 @@ nuget-central-version <trusted-props> <candidate-props> <metadata-json> <output-
82
83
  swift-semantic-equal <base> <head> <file.swift>
83
84
  post-review
84
85
  stage-review-payload optional|required <source> <destination>
86
+ retrospective-transcript [--session-id ID] [--jsonl PATH] [--projects-dir PATH] [--codex-sessions-dir PATH] [--grok-sessions-dir PATH]
85
87
  `;
86
88
  export function printUsage(stream = process.stdout) {
87
89
  stream.write(USAGE);
@@ -23,7 +23,9 @@ export function assertCoverageTransportOutcome(suite, primary, artifactAttempt1,
23
23
  return true;
24
24
  }
25
25
  if (artifactSucceeded) {
26
- emit(`::warning::Coverage persisted only to GitHub artifacts for suite=${suite}; S3 primary is degraded.`);
26
+ if (primary !== 'skipped') {
27
+ emit(`::warning::Coverage persisted only to GitHub artifacts for suite=${suite}; S3 primary is degraded.`);
28
+ }
27
29
  return true;
28
30
  }
29
31
  emit(`::error::COVERAGE_TRANSPORT_EXHAUSTED suite=${suite} Neither S3 nor GitHub artifacts persisted the coverage pair.`);
package/dist/index.d.mts CHANGED
@@ -1,3 +1,5 @@
1
+ export { codexChildren, codexIdentity, computeTranscriptFacts, formatTranscriptFacts, formatUnavailable, resolveTranscriptFile, runRetrospectiveTranscript, } from './retrospective-transcript/index.mts';
2
+ export type { ResolveOptions, TokenTotals, TranscriptFacts, } from './retrospective-transcript/index.mts';
1
3
  export { EphemeralListenerAttemptsExhaustedError, isRunnerReservedPort, listenOnRunnerUnreservedEphemeralPort, loadRunnerPortPolicy, runnerPortPolicy, validateRunnerPortPolicy, } from './runner-port-policy/index.mts';
2
4
  export type { EphemeralListenerOptions, RunnerPortPolicy } from './runner-port-policy/index.mts';
3
5
  export { extractAlterTableAddColumnLocations, extractCreateIndexMetadata, extractCreateTableMetadata, extractDefaultFunction, extractDropIndexMetadata, extractFuncCallArgColumnNames, extractMigrationConstraintMetadata, initSqlAst, lineOfUtf8ByteOffset, MissingSqlAstParserError, parseSql, } from './sql-ast/index.mts';
@@ -20,6 +22,8 @@ export { createArtifactClassifier, parseArtifactPatternsJson, planRunDeletions,
20
22
  export type { ArtifactClassification, ArtifactClassifier, ArtifactPatterns, CleanupRequest, DeletionSummary, } from './gha-artifacts-cleanup/index.mts';
21
23
  export { validateOptionalHttpOrigin } from './http-origin/index.mts';
22
24
  export { boundPendingLine, DEFAULT_MAX_PENDING_LINE_LENGTH, DEFAULT_TRUNCATED_LINE_MARKER, splitCompleteLines, } from './process-line-buffer/index.mts';
25
+ export { ProcessGroupDrainTimeoutError, runBrowserSession, } from './browser-session-runner/index.mts';
26
+ export type { BrowserSessionDeps, BrowserSessionEvent, BrowserSessionExit, BrowserSessionOptions, BrowserSessionProcess, BrowserSessionResult, } from './browser-session-runner/index.mts';
23
27
  export { buildSchemaSnapshot, detectRenamedIndexes, generateSchemaSnapshot, indexShapeKey, readSchemaCatalog, renderSchemaMarkdown, stableStringify, writeSchemaSnapshot, } from './pg-schema-snapshot/index.mts';
24
28
  export type { CatalogQuery, PartitionPolicy, SchemaCatalog, SchemaGrowthMaps, SchemaSnapshot, SchemaTableSnapshot, } from './pg-schema-snapshot/index.mts';
25
29
  export { buildOpenApiDocument, hashContractSchema, nodeToOpenApi, writeOpenApi, } from './openapi-document/index.mts';
@@ -56,3 +60,5 @@ export { normalizeSwiftSource } from './swift-semantic-equal/index.mts';
56
60
  export { isSwiftCodeOffset, parseUniqueSwiftBinaryTargetChecksum, } from './swift-source-offset/index.mts';
57
61
  export { validateResolvedPinDelta } from './swift-resolved-pin-delta/index.mts';
58
62
  export type { ResolvedDocument, ResolvedPin, ValidateResolvedPinDeltaOptions, } from './swift-resolved-pin-delta/index.mts';
63
+ export { DEFAULT_MAX_DIAGNOSTIC_REPORTS, DEFAULT_MAX_FORMATTED_DIAGNOSTIC_REPORTS, formatDiagnosticReportSummaries, HARD_MAX_DIAGNOSTIC_REPORTS, readDiagnosticReportSummaries, summarizeDiagnosticReport, } from './vitest-diagnostics/index.mts';
64
+ export type { DiagnosticReportLimitOptions, DiagnosticReportSummary, } from './vitest-diagnostics/index.mts';