@allocator-one/rcl 4.5.21 → 4.5.22
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 +2 -0
- package/dist/converge/fresh-review-close.d.ts +89 -0
- package/dist/converge/fresh-review-close.js +222 -0
- package/dist/converge/fresh-review-close.js.map +1 -0
- package/dist/converge/fresh-review.d.ts +52 -0
- package/dist/converge/fresh-review.js +35 -0
- package/dist/converge/fresh-review.js.map +1 -1
- package/dist/converge/pr-snapshot.d.ts +53 -0
- package/dist/converge/pr-snapshot.js +76 -0
- package/dist/converge/pr-snapshot.js.map +1 -0
- package/dist/index.js +72 -0
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { constants } from 'node:fs';
|
|
2
|
+
import { lstat, open } from 'node:fs/promises';
|
|
3
|
+
import { z } from 'zod';
|
|
4
|
+
import { readStable, sha256 } from '../telemetry/recovery/files.js';
|
|
5
|
+
/**
|
|
6
|
+
* A pull request's live state, read once by a parent process that holds
|
|
7
|
+
* GitHub credentials and handed to a review process that holds none (RCL-216).
|
|
8
|
+
*
|
|
9
|
+
* Grid hosts strip GitHub credentials from every reviewer process on purpose,
|
|
10
|
+
* yet a review cycle must confirm that the pull request is still open at the
|
|
11
|
+
* head under review. The parent captures that state with `rcl pr-snapshot`
|
|
12
|
+
* shortly before launching, and the review reads it with `--pr-snapshot`
|
|
13
|
+
* instead of calling GitHub. Harness still verifies the reviewed head against
|
|
14
|
+
* the pull request when the evidence arrives; this snapshot only replaces the
|
|
15
|
+
* reviewer's own pre-launch read.
|
|
16
|
+
*/
|
|
17
|
+
export const PR_SNAPSHOT_MAX_AGE_MS = 15 * 60 * 1000;
|
|
18
|
+
const MAX_FUTURE_SKEW_MS = 60 * 1000;
|
|
19
|
+
const MAX_SNAPSHOT_BYTES = 16 * 1024;
|
|
20
|
+
export const prSnapshotSchema = z.object({
|
|
21
|
+
kind: z.literal('rcl-pr-snapshot'),
|
|
22
|
+
version: z.literal(1),
|
|
23
|
+
repo: z.string().regex(/^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/),
|
|
24
|
+
prNumber: z.number().int().positive().safe(),
|
|
25
|
+
headSha: z.string().regex(/^[a-f0-9]{40}$/),
|
|
26
|
+
baseSha: z.string().regex(/^[a-f0-9]{40}$/),
|
|
27
|
+
baseRef: z.string().min(1).max(255),
|
|
28
|
+
merged: z.boolean(),
|
|
29
|
+
state: z.string().min(1).max(32),
|
|
30
|
+
fetchedAt: z.string().datetime({ offset: true }),
|
|
31
|
+
}).strict();
|
|
32
|
+
/** The fields a snapshot keeps from GitHub's pull request object. */
|
|
33
|
+
export function prSnapshotFrom(repo, prNumber, pr, now = new Date()) {
|
|
34
|
+
return prSnapshotSchema.parse({ kind: 'rcl-pr-snapshot', version: 1, repo: repo.toLowerCase(), prNumber,
|
|
35
|
+
headSha: pr.head.sha, baseSha: pr.base.sha, baseRef: pr.base.ref, merged: pr.merged === true, state: pr.state,
|
|
36
|
+
fetchedAt: now.toISOString() });
|
|
37
|
+
}
|
|
38
|
+
/** Write a new snapshot exclusively, readable by its owner only; answer its SHA-256. */
|
|
39
|
+
export async function writePrSnapshot(path, snapshot) {
|
|
40
|
+
const bytes = Buffer.from(`${JSON.stringify(prSnapshotSchema.parse(snapshot), null, 2)}\n`);
|
|
41
|
+
const handle = await open(path, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | constants.O_NOFOLLOW, 0o600);
|
|
42
|
+
try {
|
|
43
|
+
await handle.writeFile(bytes);
|
|
44
|
+
await handle.sync();
|
|
45
|
+
}
|
|
46
|
+
finally {
|
|
47
|
+
await handle.close();
|
|
48
|
+
}
|
|
49
|
+
return sha256(bytes);
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Read a snapshot for `repo#prNumber` and answer the live state it records.
|
|
53
|
+
* Refuses a file another user could have written, one for another pull
|
|
54
|
+
* request, and one older than {@link PR_SNAPSHOT_MAX_AGE_MS}: an old reading
|
|
55
|
+
* cannot stand in for the live check.
|
|
56
|
+
*/
|
|
57
|
+
export async function readPrSnapshot(path, repo, prNumber, now = Date.now()) {
|
|
58
|
+
const stat = await lstat(path);
|
|
59
|
+
if (!stat.isFile() || stat.isSymbolicLink() || (process.platform !== 'win32' &&
|
|
60
|
+
(stat.uid !== process.geteuid?.() || (stat.mode & 0o022) !== 0)))
|
|
61
|
+
throw new Error('pr_snapshot_unsafe_file');
|
|
62
|
+
let snapshot;
|
|
63
|
+
try {
|
|
64
|
+
snapshot = prSnapshotSchema.parse(JSON.parse((await readStable(path, MAX_SNAPSHOT_BYTES)).text));
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
throw new Error('pr_snapshot_invalid');
|
|
68
|
+
}
|
|
69
|
+
if (snapshot.repo.toLowerCase() !== repo.toLowerCase() || snapshot.prNumber !== prNumber)
|
|
70
|
+
throw new Error('pr_snapshot_pull_request_mismatch');
|
|
71
|
+
const age = now - Date.parse(snapshot.fetchedAt);
|
|
72
|
+
if (age > PR_SNAPSHOT_MAX_AGE_MS || age < -MAX_FUTURE_SKEW_MS)
|
|
73
|
+
throw new Error('pr_snapshot_stale');
|
|
74
|
+
return { headSha: snapshot.headSha, merged: snapshot.merged, state: snapshot.state };
|
|
75
|
+
}
|
|
76
|
+
//# sourceMappingURL=pr-snapshot.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pr-snapshot.js","sourceRoot":"","sources":["../../src/converge/pr-snapshot.ts"],"names":[],"mappings":"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"}
|
package/dist/index.js
CHANGED
|
@@ -4,6 +4,8 @@ import { guardedDeliveryState } from './telemetry/deliver.js';
|
|
|
4
4
|
import { AmbiguousReviewerIdentityError, assertUnambiguousReviewerIdentities } from './dispatch/reviewer-identity.js';
|
|
5
5
|
import { retainAssemblyRefusal } from './output/assembly-refusal.js';
|
|
6
6
|
import { createReviewCycleRemote } from './converge/cycle-remote.js';
|
|
7
|
+
import { prSnapshotFrom, readPrSnapshot, writePrSnapshot } from './converge/pr-snapshot.js';
|
|
8
|
+
import { applyFreshReviewClose, previewFreshReviewClose } from './converge/fresh-review-close.js';
|
|
7
9
|
import { assertNoPendingFreshReview, freshReviewOutputPaths } from './converge/fresh-review.js';
|
|
8
10
|
import { previewStaleReport, applyStaleReport } from './converge/stale-report.js';
|
|
9
11
|
import { previewHistoricalDeliveryReconciliation, applyHistoricalDeliveryReconciliation } from './converge/historical-delivery-reconciliation.js';
|
|
@@ -239,6 +241,7 @@ program
|
|
|
239
241
|
.option('--round <n>', 'Converge round number (or RCL_CONVERGE_ROUND)')
|
|
240
242
|
.option('--attempt <n>', 'Converge attempt number (or RCL_CONVERGE_ATTEMPT)')
|
|
241
243
|
.option('--start-over', 'Start an explicitly requested fresh review with a new normal budget; retain all prior evidence and spending')
|
|
244
|
+
.option('--pr-snapshot <path>', 'Live pull request state from `rcl pr-snapshot`, captured by a parent that holds GitHub credentials; review cycles read it instead of GitHub (RCL-216)')
|
|
242
245
|
.option('--guarded-converge', 'Validate and claim inside this review process, or consume an exact recovered-v3 --attempt; derive the round from native state')
|
|
243
246
|
.option('--bound-fix-recovery <run-id>', 'Review unchanged inputs once live Harness evidence proves a retained bound fix obligation after this native dismissal-only run')
|
|
244
247
|
.option('--launch-intent <intent>', 'Guarded intent: review, stop-upstream, stop-review, or retry-delivery')
|
|
@@ -519,6 +522,71 @@ program
|
|
|
519
522
|
process.exitCode = 3;
|
|
520
523
|
}
|
|
521
524
|
});
|
|
525
|
+
program
|
|
526
|
+
.command('pr-snapshot <pr>')
|
|
527
|
+
.description('Capture a pull request\'s live state (head, base, open/merged) for a review process that holds no GitHub ' +
|
|
528
|
+
'credentials: run it in the parent, then pass the file to `rcl review --pr-snapshot` within 15 minutes (RCL-216)')
|
|
529
|
+
.requiredOption('--out <path>', 'New file to write (refuses an existing one); readable by its owner only')
|
|
530
|
+
.option('--config <path>', 'Path to config file')
|
|
531
|
+
.option('--json')
|
|
532
|
+
.action(async (pr, opts) => {
|
|
533
|
+
try {
|
|
534
|
+
if (!isGitHubTarget(pr))
|
|
535
|
+
throw new Error('pr_snapshot_requires_pull_request');
|
|
536
|
+
const target = parseGitHubTarget(pr);
|
|
537
|
+
const config = await loadConfig(opts.config);
|
|
538
|
+
const { data } = await getGitHubPullRequest(await createGitHubClient(config.githubToken), target);
|
|
539
|
+
const snapshot = prSnapshotFrom(`${target.owner}/${target.repo}`, target.number, data);
|
|
540
|
+
const digest = await writePrSnapshot(opts.out, snapshot);
|
|
541
|
+
console.log(opts.json ? JSON.stringify({ snapshot, path: opts.out, sha256: digest })
|
|
542
|
+
: `Wrote ${opts.out}: ${snapshot.repo}#${snapshot.prNumber} ${snapshot.state} at ${snapshot.headSha} (sha256 ${digest})`);
|
|
543
|
+
}
|
|
544
|
+
catch (error) {
|
|
545
|
+
console.error(JSON.stringify({ error: { code: 'RCL_PR_SNAPSHOT', message: error instanceof Error ? error.message : String(error) } }));
|
|
546
|
+
process.exitCode = 3;
|
|
547
|
+
}
|
|
548
|
+
});
|
|
549
|
+
program
|
|
550
|
+
.command('converge-close-fresh')
|
|
551
|
+
.description('Preview or apply an audited close of a fresh-review operation whose launch died before writing any report; ' +
|
|
552
|
+
'never creates a round, finding, verdict or attempt (RCL-216)')
|
|
553
|
+
.option('--preview')
|
|
554
|
+
.option('--apply')
|
|
555
|
+
.option('--target <target>')
|
|
556
|
+
.option('--operation <uuid>', 'The fresh-review operation to close (its native current operation)')
|
|
557
|
+
.requiredOption('--manifest <path>', 'Exclusive preview manifest; the reviewed manifest for apply')
|
|
558
|
+
.option('--manifest-sha256 <sha256>', 'Exact reviewed manifest bytes; required for apply')
|
|
559
|
+
.option('--json')
|
|
560
|
+
.action(async (opts) => {
|
|
561
|
+
try {
|
|
562
|
+
if ([opts.preview, opts.apply].filter(Boolean).length !== 1)
|
|
563
|
+
throw new Error('choose_exactly_one_fresh_close_mode');
|
|
564
|
+
const gitCommonDir = await resolveGitCommonDir();
|
|
565
|
+
let result;
|
|
566
|
+
if (opts.preview) {
|
|
567
|
+
if (opts.manifestSha256 !== undefined)
|
|
568
|
+
throw new Error('preview_does_not_accept_manifest_digest');
|
|
569
|
+
if (typeof opts.target !== 'string' || typeof opts.operation !== 'string')
|
|
570
|
+
throw new Error('fresh_close_preview_arguments_required');
|
|
571
|
+
const manifest = await previewFreshReviewClose({ target: opts.target, operationId: opts.operation }, gitCommonDir);
|
|
572
|
+
await writeExclusive(opts.manifest, manifest, 256 * 1024);
|
|
573
|
+
result = { mode: 'preview', manifest, manifestSha256: sha256(serializeRecoveryDocument(manifest)), accounting: 'unchanged',
|
|
574
|
+
scope: 'local operation close; no admission or approval' };
|
|
575
|
+
}
|
|
576
|
+
else {
|
|
577
|
+
if (opts.target !== undefined || opts.operation !== undefined)
|
|
578
|
+
throw new Error('apply_uses_only_pinned_manifest');
|
|
579
|
+
result = { mode: 'apply', result: await applyFreshReviewClose({ manifest: opts.manifest,
|
|
580
|
+
manifestSha256: opts.manifestSha256 }, gitCommonDir), accounting: 'unchanged',
|
|
581
|
+
scope: 'local operation close; no admission or approval' };
|
|
582
|
+
}
|
|
583
|
+
console.log(JSON.stringify(result));
|
|
584
|
+
}
|
|
585
|
+
catch (error) {
|
|
586
|
+
console.error(JSON.stringify({ error: { code: 'RCL_CONVERGE_CLOSE_FRESH', message: error instanceof Error ? error.message : String(error) } }));
|
|
587
|
+
process.exitCode = 3;
|
|
588
|
+
}
|
|
589
|
+
});
|
|
522
590
|
program
|
|
523
591
|
.command('converge-gap')
|
|
524
592
|
.description('Preview, apply or resume one evidenced missing-terminal-report audit gap; never creates a round, finding, verdict or attempt')
|
|
@@ -2022,6 +2090,10 @@ async function executeCouncil(spinner, prepared, diff, opts, extra, preparedWork
|
|
|
2022
2090
|
throw new Error('Fresh review cycles require full Harness evidence and an actor credential');
|
|
2023
2091
|
const prTarget = parseGitHubTarget(`${extra.target.repo}#${extra.target.prNumber}`);
|
|
2024
2092
|
cycleRemote = createReviewCycleRemote(runtime.sink, extra.target.repo, extra.target.prNumber, opts.expectPrHeadSha ?? extra.target.headSha ?? '', async () => {
|
|
2093
|
+
// A parent with GitHub credentials hands the live state over, so this
|
|
2094
|
+
// review process needs none (RCL-216).
|
|
2095
|
+
if (opts.prSnapshot !== undefined)
|
|
2096
|
+
return readPrSnapshot(opts.prSnapshot, extra.target.repo, extra.target.prNumber);
|
|
2025
2097
|
const client = await createGitHubClient(config.githubToken);
|
|
2026
2098
|
const auth = await client.auth();
|
|
2027
2099
|
if (typeof auth !== 'object' || auth === null || !('type' in auth) || auth.type !== 'token') {
|