opencode-swarm 7.164.0 → 7.164.2
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/.opencode/skills/codebase-review-swarm/SKILL.md +4 -0
- package/.opencode/skills/commit-pr/SKILL.md +4 -1
- package/.opencode/skills/critic-gate/SKILL.md +4 -0
- package/.opencode/skills/deep-dive/SKILL.md +4 -0
- package/.opencode/skills/engineering-conventions/SKILL.md +2 -0
- package/.opencode/skills/execute/SKILL.md +4 -0
- package/.opencode/skills/phase-wrap/SKILL.md +4 -0
- package/.opencode/skills/plan/SKILL.md +4 -0
- package/.opencode/skills/running-tests/SKILL.md +4 -0
- package/.opencode/skills/swarm-pr-review/SKILL.md +4 -0
- package/.opencode/skills/swarm-pr-review/references/prompt-templates.md +2 -0
- package/.opencode/skills/writing-tests/SKILL.md +4 -0
- package/dist/agents/critic.d.ts +6 -6
- package/dist/cli/{coder-settlement-84je6bza.js → coder-settlement-kv1x94w6.js} +12 -13
- package/dist/cli/{config-doctor-vfze38e1.js → config-doctor-8d7ydxck.js} +3 -3
- package/dist/cli/{core-q5hv8e2w.js → core-7jjgxczp.js} +2 -2
- package/dist/cli/{curation-policy-a2zty4br.js → curation-policy-7pnmnfct.js} +6 -7
- package/dist/cli/{curator-4pckmmyf.js → curator-5nyth2rj.js} +35 -38
- package/dist/cli/{curator-drift-wy7ejcvw.js → curator-drift-ykrdf08r.js} +4 -5
- package/dist/cli/curator-llm-factory-d415s169.js +66 -0
- package/dist/cli/{evidence-summary-service-b15yes21.js → evidence-summary-service-aebn7nb9.js} +16 -17
- package/dist/cli/{gate-evidence-70h0w4tb.js → gate-evidence-es59pysy.js} +8 -9
- package/dist/cli/guardrail-explain-mn2x7w2y.js +67 -0
- package/dist/cli/{guardrail-log-7rxbd20p.js → guardrail-log-fd3c5rks.js} +8 -9
- package/dist/cli/{guardrail-reset-3b4gs357.js → guardrail-reset-r49rvptv.js} +35 -38
- package/dist/cli/{hive-promoter-s80g1gwb.js → hive-promoter-qp2kjhhp.js} +35 -38
- package/dist/cli/{index-93sepnc9.js → index-0145kpjr.js} +2 -2
- package/dist/cli/{index-v3a9rmyh.js → index-1r6p1wpa.js} +4 -4
- package/dist/cli/{index-c4pynht5.js → index-2xj34nkh.js} +1 -1
- package/dist/cli/{index-g64154pd.js → index-34exj2e3.js} +2 -2
- package/dist/cli/{index-td6n935w.js → index-3704v4e6.js} +2 -2
- package/dist/cli/{index-ynqp3jb6.js → index-5d7bzpfn.js} +1 -1
- package/dist/cli/{index-hyq20s2v.js → index-6vtm4rd0.js} +12 -12
- package/dist/cli/{index-4afh2cz7.js → index-92q98ry8.js} +7 -7
- package/dist/cli/{index-wdtckfnf.js → index-ac39znep.js} +2 -2
- package/dist/cli/{index-4jersar8.js → index-axtnc0xf.js} +5 -5
- package/dist/cli/{index-e3hwzcjv.js → index-b85jnfp4.js} +2 -2
- package/dist/cli/{index-bm73pych.js → index-br7q79t2.js} +7 -7
- package/dist/cli/{index-1b1h770t.js → index-bzd67m2j.js} +3 -3
- package/dist/cli/{index-ma8w6pfp.js → index-c8k4cs94.js} +5 -5
- package/dist/cli/{index-t90gjc0k.js → index-cm4r8gh2.js} +1 -1
- package/dist/cli/{index-3b762978.js → index-f9nfknjg.js} +182 -37
- package/dist/cli/{index-t9rhtvy9.js → index-fyt5byhd.js} +3 -3
- package/dist/cli/{index-zbpcpsk0.js → index-gz5rn6w6.js} +9 -9
- package/dist/cli/{index-7r9vpkwa.js → index-h9wnkr2e.js} +5 -5
- package/dist/cli/{index-1hpjwz3s.js → index-hcn69kp0.js} +1 -1
- package/dist/cli/{index-12f1tsn2.js → index-hnwg5gt6.js} +655 -16
- package/dist/cli/{index-qkk2skff.js → index-jw9wbwdh.js} +37 -40
- package/dist/cli/{index-pvgcex0d.js → index-kwd09y5a.js} +4 -4
- package/dist/cli/{index-9xd073d4.js → index-m1nrd3yd.js} +9 -2
- package/dist/cli/{index-c69jbpex.js → index-pcnhh2vt.js} +2 -2
- package/dist/cli/{index-gxd8fwyt.js → index-q3xn3m3g.js} +4 -4
- package/dist/cli/{index-fckfgptr.js → index-qbr05tbw.js} +2 -2
- package/dist/cli/{index-9sy65e94.js → index-qmeewt3g.js} +32 -34
- package/dist/cli/{index-tb1k8jya.js → index-sb4eanpv.js} +1 -1
- package/dist/cli/{index-dtn0pne5.js → index-v56mhyhs.js} +1 -1
- package/dist/cli/{index-2s4s9n82.js → index-w5jpjaqe.js} +2 -2
- package/dist/cli/{index-1z4jzwja.js → index-w8qt2612.js} +3 -3
- package/dist/cli/{index-nhxaa9j5.js → index-xm6c1tnf.js} +1 -1
- package/dist/cli/{index-0515j0cz.js → index-y2f5s2cf.js} +1 -1
- package/dist/cli/{index-36nt17m8.js → index-yep24wn4.js} +1 -1
- package/dist/cli/{index-xk0kc1py.js → index-yjftcanv.js} +7767 -2575
- package/dist/cli/{index-t5t82jy8.js → index-yqhwrjw9.js} +5 -5
- package/dist/cli/{index-nzrhw31r.js → index-zgeqwbhw.js} +2 -2
- package/dist/cli/{index-70bfj3v1.js → index-zs1jrsta.js} +2 -2
- package/dist/cli/index.js +35 -38
- package/dist/cli/{knowledge-escalator-sttdnwf8.js → knowledge-escalator-439jajb3.js} +14 -15
- package/dist/cli/{knowledge-events-4h88stj7.js → knowledge-events-wz3h98kk.js} +12 -13
- package/dist/cli/{knowledge-link-yykmxdyw.js → knowledge-link-2gh1cnk3.js} +5 -6
- package/dist/cli/{knowledge-store-j832fxpx.js → knowledge-store-y1cwe80n.js} +6 -7
- package/dist/cli/{knowledge-validator-edbg761g.js → knowledge-validator-bk2qfqe2.js} +8 -9
- package/dist/cli/{model-preflight-c3mc8njg.js → model-preflight-pmnbfzjv.js} +1 -1
- package/dist/cli/{pending-delegations-sphntrs0.js → pending-delegations-kh4mkvzb.js} +6 -8
- package/dist/cli/{pr-subscriptions-1a4gcnzv.js → pr-subscriptions-sdv0n59c.js} +6 -7
- package/dist/cli/pr-workflow-gate-a9vj11e5.js +221 -0
- package/dist/cli/{runner-d294h1by.js → runner-m95v9d47.js} +6 -7
- package/dist/cli/{scan-cursor-5frvp7nb.js → scan-cursor-pjek24k7.js} +7 -8
- package/dist/cli/{schema-9xkt5hvd.js → schema-674qw3aj.js} +2 -2
- package/dist/cli/{scope-persistence-gk55h3sp.js → scope-persistence-sq331eqh.js} +10 -11
- package/dist/cli/{skill-generator-sxxf7jbb.js → skill-generator-pcw7c50r.js} +16 -17
- package/dist/cli/{telemetry-q51s6tyt.js → telemetry-c4j18da8.js} +1 -1
- package/dist/cli/{worktree-collision-ownership-7kj2ryt1.js → worktree-collision-ownership-vnj31rgc.js} +7 -9
- package/dist/cli/{worktree-isolation-67fs6rz9.js → worktree-isolation-tje6p4zt.js} +35 -38
- package/dist/hooks/delegate-ack-collector.d.ts +5 -0
- package/dist/hooks/delegation-gate.d.ts +14 -2
- package/dist/hooks/plan-task-id-context.d.ts +24 -0
- package/dist/hooks/pr-workflow-gate.d.ts +14 -8
- package/dist/hooks/pr-workflow-response-gate.d.ts +9 -12
- package/dist/hooks/skill-injection.d.ts +7 -3
- package/dist/hooks/skill-propagation-gate.d.ts +1 -1
- package/dist/hooks/task-id-resolver.d.ts +46 -0
- package/dist/index.js +118 -92
- package/dist/pr-review/authorization.d.ts +18 -12
- package/dist/tools/authorize-pr-review-reentry.d.ts +12 -8
- package/dist/tools/tool-metadata.d.ts +1 -1
- package/opencode-swarm.schema.json +4 -1
- package/package.json +1 -1
- package/dist/cli/authorization-vstrh7ra.js +0 -23
- package/dist/cli/curator-llm-factory-msbj3hpn.js +0 -69
- package/dist/cli/guardrail-explain-kksdy032.js +0 -70
- package/dist/cli/index-ed5ykebp.js +0 -151
- package/dist/cli/index-mmngnxev.js +0 -883
- package/dist/cli/index-w7ekntej.js +0 -643
|
@@ -1,883 +0,0 @@
|
|
|
1
|
-
// @bun
|
|
2
|
-
import {
|
|
3
|
-
PrReviewRunIdSchema
|
|
4
|
-
} from "./index-w7ekntej.js";
|
|
5
|
-
import {
|
|
6
|
-
validateSwarmPath
|
|
7
|
-
} from "./index-dtn0pne5.js";
|
|
8
|
-
import {
|
|
9
|
-
require_proper_lockfile
|
|
10
|
-
} from "./index-7g4c7s5r.js";
|
|
11
|
-
import {
|
|
12
|
-
exports_external,
|
|
13
|
-
init_zod
|
|
14
|
-
} from "./index-bpmtbmy9.js";
|
|
15
|
-
import {
|
|
16
|
-
__toESM
|
|
17
|
-
} from "./index-a76rekgs.js";
|
|
18
|
-
|
|
19
|
-
// src/pr-review/authorization.ts
|
|
20
|
-
init_zod();
|
|
21
|
-
var import_proper_lockfile = __toESM(require_proper_lockfile(), 1);
|
|
22
|
-
import { randomUUID as randomUUID2 } from "crypto";
|
|
23
|
-
import * as fsp2 from "fs/promises";
|
|
24
|
-
import * as path2 from "path";
|
|
25
|
-
|
|
26
|
-
// src/pr-review/persistence.ts
|
|
27
|
-
import { createHash, randomUUID } from "crypto";
|
|
28
|
-
import * as fsp from "fs/promises";
|
|
29
|
-
import * as path from "path";
|
|
30
|
-
function normalizeSessionID(sessionID) {
|
|
31
|
-
const normalized = sessionID.trim();
|
|
32
|
-
if (!normalized) {
|
|
33
|
-
throw new Error("BLOCKED: PR workflow gate requires a non-empty sessionID");
|
|
34
|
-
}
|
|
35
|
-
return normalized;
|
|
36
|
-
}
|
|
37
|
-
function prWorkflowSessionFileStem(sessionID) {
|
|
38
|
-
const normalized = normalizeSessionID(sessionID);
|
|
39
|
-
const slug = normalized.replace(/[^A-Za-z0-9_.-]/g, "_").slice(0, 80) || "session";
|
|
40
|
-
const digest = createHash("sha256").update(normalized).digest("hex").slice(0, 12);
|
|
41
|
-
return `${slug}-${digest}`;
|
|
42
|
-
}
|
|
43
|
-
var WORKFLOW_GATE_DIR = "pr-workflow-gates";
|
|
44
|
-
function workflowGateStateRelativePath(sessionID) {
|
|
45
|
-
return path.join(WORKFLOW_GATE_DIR, `${prWorkflowSessionFileStem(sessionID)}.json`);
|
|
46
|
-
}
|
|
47
|
-
function workflowGateStateLockRelativePath(sessionID) {
|
|
48
|
-
return path.join(WORKFLOW_GATE_DIR, `${prWorkflowSessionFileStem(sessionID)}.lock`);
|
|
49
|
-
}
|
|
50
|
-
function workflowCheckoutMutationLockRelativePath() {
|
|
51
|
-
return path.join(WORKFLOW_GATE_DIR, "checkout.lock");
|
|
52
|
-
}
|
|
53
|
-
function workflowGateStatePath(directory, sessionID) {
|
|
54
|
-
return validateSwarmPath(directory, workflowGateStateRelativePath(sessionID));
|
|
55
|
-
}
|
|
56
|
-
function workflowGateStateLockPath(directory, sessionID) {
|
|
57
|
-
return validateSwarmPath(directory, workflowGateStateLockRelativePath(sessionID));
|
|
58
|
-
}
|
|
59
|
-
function workflowCheckoutMutationLockPath(directory) {
|
|
60
|
-
return validateSwarmPath(directory, workflowCheckoutMutationLockRelativePath());
|
|
61
|
-
}
|
|
62
|
-
var MAX_TRACKED_SESSIONS = 200;
|
|
63
|
-
var MAX_COMPLETED_CHECKOUT_LOCK_OWNERS = 64;
|
|
64
|
-
var WINDOWS_RENAME_MAX_RETRIES = 3;
|
|
65
|
-
var RENAME_RETRY_DELAY_MS = 10;
|
|
66
|
-
var STATE_MUTATION_LOCK_MAX_ATTEMPTS = 50;
|
|
67
|
-
var STATE_MUTATION_LOCK_RETRY_DELAY_MS = 10;
|
|
68
|
-
var STATE_MUTATION_LOCK_UNINITIALIZED_STALE_MS = 30000;
|
|
69
|
-
var CHECKOUT_MUTATION_ACTION_TIMEOUT_MS = 5 * 60000;
|
|
70
|
-
var trackedStatesByProjectSession = new Map;
|
|
71
|
-
var pendingStateMutationsByProjectSession = new Map;
|
|
72
|
-
var pendingCheckoutMutationsByProject = new Map;
|
|
73
|
-
var completedCheckoutLockOwners = new Map;
|
|
74
|
-
function isoNow() {
|
|
75
|
-
return new Date().toISOString();
|
|
76
|
-
}
|
|
77
|
-
function delay(ms) {
|
|
78
|
-
return new Promise((resolve2) => setTimeout(resolve2, ms));
|
|
79
|
-
}
|
|
80
|
-
function sameBigIntFileIdentity(left, right) {
|
|
81
|
-
return left.dev === right.dev && left.ino === right.ino;
|
|
82
|
-
}
|
|
83
|
-
function normalizeComparableFsPath(value) {
|
|
84
|
-
const normalized = path.normalize(path.resolve(value));
|
|
85
|
-
return process.platform === "win32" ? normalized.toLowerCase() : normalized;
|
|
86
|
-
}
|
|
87
|
-
function stateCacheKey(directory, sessionID) {
|
|
88
|
-
const resolved = path.normalize(path.resolve(directory));
|
|
89
|
-
const canonicalDirectory = process.platform === "win32" ? resolved.toLowerCase() : resolved;
|
|
90
|
-
return `${canonicalDirectory}\x00${normalizeSessionID(sessionID)}`;
|
|
91
|
-
}
|
|
92
|
-
function rememberState(directory, state) {
|
|
93
|
-
const cacheKey = stateCacheKey(directory, state.sessionID);
|
|
94
|
-
trackedStatesByProjectSession.delete(cacheKey);
|
|
95
|
-
trackedStatesByProjectSession.set(cacheKey, state);
|
|
96
|
-
while (trackedStatesByProjectSession.size > MAX_TRACKED_SESSIONS) {
|
|
97
|
-
const oldestKey = trackedStatesByProjectSession.keys().next().value;
|
|
98
|
-
if (!oldestKey)
|
|
99
|
-
break;
|
|
100
|
-
trackedStatesByProjectSession.delete(oldestKey);
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
function forgetTrackedPrWorkflowState(directory, sessionID) {
|
|
104
|
-
trackedStatesByProjectSession.delete(stateCacheKey(directory, sessionID));
|
|
105
|
-
}
|
|
106
|
-
function resetPrReviewPersistenceCaches() {
|
|
107
|
-
trackedStatesByProjectSession.clear();
|
|
108
|
-
pendingStateMutationsByProjectSession.clear();
|
|
109
|
-
pendingCheckoutMutationsByProject.clear();
|
|
110
|
-
completedCheckoutLockOwners.clear();
|
|
111
|
-
}
|
|
112
|
-
function defaultIsProcessAlive(pid) {
|
|
113
|
-
try {
|
|
114
|
-
process.kill(pid, 0);
|
|
115
|
-
return true;
|
|
116
|
-
} catch (error) {
|
|
117
|
-
return error.code !== "ESRCH";
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
var defaultPersistenceHooks = {
|
|
121
|
-
nowMs: () => Date.now(),
|
|
122
|
-
isProcessAlive: defaultIsProcessAlive,
|
|
123
|
-
openCheckoutLock: (lockPath) => fsp.open(lockPath, "wx"),
|
|
124
|
-
removeCheckoutLock: (lockPath) => fsp.rm(lockPath),
|
|
125
|
-
checkoutMutationActionTimeoutMs: CHECKOUT_MUTATION_ACTION_TIMEOUT_MS,
|
|
126
|
-
rename: (from, to) => fsp.rename(from, to)
|
|
127
|
-
};
|
|
128
|
-
var hooks = defaultPersistenceHooks;
|
|
129
|
-
function bindPrReviewPersistenceHooks(nextHooks) {
|
|
130
|
-
hooks = nextHooks;
|
|
131
|
-
}
|
|
132
|
-
var codec;
|
|
133
|
-
function bindPrReviewStateCodec(nextCodec) {
|
|
134
|
-
codec = nextCodec;
|
|
135
|
-
}
|
|
136
|
-
function requireCodec() {
|
|
137
|
-
if (!codec) {
|
|
138
|
-
throw new Error("BLOCKED: PR workflow persistence codec is not bound (the gate must bind it at module init)");
|
|
139
|
-
}
|
|
140
|
-
return codec;
|
|
141
|
-
}
|
|
142
|
-
async function ensurePrWorkflowSafeParentDirectory(directory, filePath) {
|
|
143
|
-
const swarmRoot = path.resolve(directory, ".swarm");
|
|
144
|
-
const parentPath = path.dirname(path.resolve(filePath));
|
|
145
|
-
const relativeParent = path.relative(swarmRoot, parentPath);
|
|
146
|
-
if (relativeParent === ".." || relativeParent.startsWith(`..${path.sep}`) || path.isAbsolute(relativeParent)) {
|
|
147
|
-
throw new Error("BLOCKED: PR workflow atomic destination escapes the project .swarm directory");
|
|
148
|
-
}
|
|
149
|
-
await fsp.mkdir(swarmRoot, { recursive: true });
|
|
150
|
-
let currentPath = swarmRoot;
|
|
151
|
-
let currentIdentity = await assertSafeDirectory(currentPath, undefined);
|
|
152
|
-
for (const segment of relativeParent.split(path.sep).filter(Boolean)) {
|
|
153
|
-
const nextPath = path.join(currentPath, segment);
|
|
154
|
-
await hooks.beforeSafeDirectoryCreate?.(currentPath, nextPath);
|
|
155
|
-
await assertSafeDirectory(currentPath, currentIdentity);
|
|
156
|
-
try {
|
|
157
|
-
await fsp.mkdir(nextPath);
|
|
158
|
-
} catch (error) {
|
|
159
|
-
if (error.code !== "EEXIST")
|
|
160
|
-
throw error;
|
|
161
|
-
}
|
|
162
|
-
await assertSafeDirectory(currentPath, currentIdentity);
|
|
163
|
-
const nextIdentity = await assertSafeDirectory(nextPath, undefined);
|
|
164
|
-
const [realCurrent, realNext] = await Promise.all([
|
|
165
|
-
fsp.realpath(currentPath),
|
|
166
|
-
fsp.realpath(nextPath)
|
|
167
|
-
]);
|
|
168
|
-
if (normalizeComparableFsPath(path.dirname(realNext)) !== normalizeComparableFsPath(realCurrent)) {
|
|
169
|
-
throw new Error("BLOCKED: PR workflow directory creation escaped the project .swarm tree");
|
|
170
|
-
}
|
|
171
|
-
currentPath = nextPath;
|
|
172
|
-
currentIdentity = nextIdentity;
|
|
173
|
-
}
|
|
174
|
-
const realRoot = await fsp.realpath(swarmRoot);
|
|
175
|
-
const realParent = await fsp.realpath(parentPath);
|
|
176
|
-
const relativeRealParent = path.relative(realRoot, realParent);
|
|
177
|
-
if (relativeRealParent === ".." || relativeRealParent.startsWith(`..${path.sep}`) || path.isAbsolute(relativeRealParent)) {
|
|
178
|
-
throw new Error("BLOCKED: PR workflow atomic parent escapes the project .swarm directory");
|
|
179
|
-
}
|
|
180
|
-
return realParent;
|
|
181
|
-
}
|
|
182
|
-
async function assertSafeDirectory(directoryPath, expectedIdentity) {
|
|
183
|
-
const stat2 = await fsp.lstat(directoryPath, { bigint: true });
|
|
184
|
-
if (stat2.isSymbolicLink() || !stat2.isDirectory() || expectedIdentity && !sameBigIntFileIdentity(stat2, expectedIdentity)) {
|
|
185
|
-
throw new Error("BLOCKED: PR workflow .swarm path must be a real directory and must not change during the operation");
|
|
186
|
-
}
|
|
187
|
-
return { dev: stat2.dev, ino: stat2.ino };
|
|
188
|
-
}
|
|
189
|
-
async function assertOpenedSwarmFileIdentity(directory, filePath, handle, expectedParent, label) {
|
|
190
|
-
const openedStat = await handle.stat({ bigint: true });
|
|
191
|
-
if (!openedStat.isFile())
|
|
192
|
-
throw new Error(`BLOCKED: ${label} is not a file`);
|
|
193
|
-
await assertClosedSwarmFileIdentity(directory, filePath, openedStat, expectedParent, label);
|
|
194
|
-
return { dev: openedStat.dev, ino: openedStat.ino };
|
|
195
|
-
}
|
|
196
|
-
async function assertClosedSwarmFileIdentity(directory, filePath, expectedIdentity, expectedParent, label) {
|
|
197
|
-
if (!expectedIdentity)
|
|
198
|
-
throw new Error(`BLOCKED: ${label} has no identity`);
|
|
199
|
-
const [stat2, realRoot, realFile, realParent] = await Promise.all([
|
|
200
|
-
fsp.lstat(filePath, { bigint: true }),
|
|
201
|
-
fsp.realpath(path.resolve(directory, ".swarm")),
|
|
202
|
-
fsp.realpath(filePath),
|
|
203
|
-
fsp.realpath(path.dirname(filePath))
|
|
204
|
-
]);
|
|
205
|
-
const relativeFile = path.relative(realRoot, realFile);
|
|
206
|
-
if (stat2.isSymbolicLink() || !stat2.isFile() || !sameBigIntFileIdentity(stat2, expectedIdentity) || normalizeComparableFsPath(realParent) !== normalizeComparableFsPath(expectedParent) || relativeFile === "" || relativeFile === ".." || relativeFile.startsWith(`..${path.sep}`) || path.isAbsolute(relativeFile)) {
|
|
207
|
-
throw new Error(`BLOCKED: ${label} changed or escaped .swarm`);
|
|
208
|
-
}
|
|
209
|
-
}
|
|
210
|
-
async function writeAtomicJson(directory, filePath, value) {
|
|
211
|
-
const safeParent = await ensurePrWorkflowSafeParentDirectory(directory, filePath);
|
|
212
|
-
const tempPath = `${filePath}.tmp.${process.pid}.${randomUUID()}`;
|
|
213
|
-
let tempIdentity;
|
|
214
|
-
let lastError;
|
|
215
|
-
try {
|
|
216
|
-
const handle = await fsp.open(tempPath, "wx");
|
|
217
|
-
try {
|
|
218
|
-
await hooks.beforeAtomicTempWrite?.();
|
|
219
|
-
tempIdentity = await assertOpenedSwarmFileIdentity(directory, tempPath, handle, safeParent, "PR workflow atomic temporary file");
|
|
220
|
-
await handle.writeFile(JSON.stringify(value, null, 2), "utf-8");
|
|
221
|
-
await handle.sync();
|
|
222
|
-
} finally {
|
|
223
|
-
await handle.close();
|
|
224
|
-
}
|
|
225
|
-
await hooks.beforeAtomicRename?.();
|
|
226
|
-
await assertClosedSwarmFileIdentity(directory, tempPath, tempIdentity, safeParent, "PR workflow atomic temporary file");
|
|
227
|
-
for (let attempt = 0;attempt < WINDOWS_RENAME_MAX_RETRIES; attempt++) {
|
|
228
|
-
try {
|
|
229
|
-
await hooks.rename(tempPath, filePath);
|
|
230
|
-
lastError = undefined;
|
|
231
|
-
break;
|
|
232
|
-
} catch (error) {
|
|
233
|
-
lastError = error;
|
|
234
|
-
const code = error.code;
|
|
235
|
-
if (code !== "EEXIST" && code !== "EPERM" && code !== "EBUSY") {
|
|
236
|
-
break;
|
|
237
|
-
}
|
|
238
|
-
if (attempt < WINDOWS_RENAME_MAX_RETRIES - 1) {
|
|
239
|
-
await delay(RENAME_RETRY_DELAY_MS);
|
|
240
|
-
}
|
|
241
|
-
}
|
|
242
|
-
}
|
|
243
|
-
if (lastError) {
|
|
244
|
-
throw lastError;
|
|
245
|
-
}
|
|
246
|
-
await assertClosedSwarmFileIdentity(directory, filePath, tempIdentity, safeParent, "PR workflow atomic destination file");
|
|
247
|
-
} finally {
|
|
248
|
-
try {
|
|
249
|
-
await fsp.rm(tempPath, { force: true });
|
|
250
|
-
} catch {}
|
|
251
|
-
}
|
|
252
|
-
}
|
|
253
|
-
async function readPrWorkflowGateStateFromDisk(directory, sessionID) {
|
|
254
|
-
const filePath = workflowGateStatePath(directory, sessionID);
|
|
255
|
-
return readPrWorkflowGateStateFileFromDisk(filePath, sessionID);
|
|
256
|
-
}
|
|
257
|
-
async function readPrWorkflowGateStateFileFromDisk(filePath, stateLabel) {
|
|
258
|
-
let raw;
|
|
259
|
-
try {
|
|
260
|
-
raw = await fsp.readFile(filePath, "utf-8");
|
|
261
|
-
} catch (error) {
|
|
262
|
-
if (error.code === "ENOENT")
|
|
263
|
-
return null;
|
|
264
|
-
throw error;
|
|
265
|
-
}
|
|
266
|
-
let parsedJson;
|
|
267
|
-
try {
|
|
268
|
-
parsedJson = JSON.parse(raw);
|
|
269
|
-
} catch {
|
|
270
|
-
throw new Error(`BLOCKED: PR workflow gate state for session "${stateLabel}" is not valid JSON`);
|
|
271
|
-
}
|
|
272
|
-
const bound = requireCodec();
|
|
273
|
-
const parsed = bound.safeParse(parsedJson);
|
|
274
|
-
if (!parsed.success) {
|
|
275
|
-
throw new Error(`BLOCKED: PR workflow gate state for session "${stateLabel}" is invalid`);
|
|
276
|
-
}
|
|
277
|
-
return parsed.data;
|
|
278
|
-
}
|
|
279
|
-
async function writeStateWhileLocked(directory, state, options = {}) {
|
|
280
|
-
const bound = requireCodec();
|
|
281
|
-
const validated = bound.parse(state);
|
|
282
|
-
const current = await readPrWorkflowGateStateFromDisk(directory, validated.sessionID);
|
|
283
|
-
if (current ? current.revision !== validated.revision : validated.revision !== 0) {
|
|
284
|
-
throw new Error("BLOCKED: PR workflow gate state changed concurrently; reload the active session state before retrying");
|
|
285
|
-
}
|
|
286
|
-
if (current?.workflowInstanceId && validated.workflowInstanceId !== current.workflowInstanceId && options.replaceWorkflowInstanceId !== current.workflowInstanceId) {
|
|
287
|
-
throw new Error("BLOCKED: PR workflow gate state changed concurrently; reload the active session state before retrying");
|
|
288
|
-
}
|
|
289
|
-
const nextState = bound.parse({
|
|
290
|
-
...validated,
|
|
291
|
-
revision: validated.revision + 1
|
|
292
|
-
});
|
|
293
|
-
const filePath = workflowGateStatePath(directory, validated.sessionID);
|
|
294
|
-
await writeAtomicJson(directory, filePath, nextState);
|
|
295
|
-
rememberState(directory, nextState);
|
|
296
|
-
return nextState;
|
|
297
|
-
}
|
|
298
|
-
function checkoutMutationProjectKey(directory) {
|
|
299
|
-
return normalizeComparableFsPath(directory);
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
class PrWorkflowCheckoutMutationTimeoutError extends Error {
|
|
303
|
-
phase;
|
|
304
|
-
code = "PR_WORKFLOW_CHECKOUT_MUTATION_TIMEOUT";
|
|
305
|
-
retryable = false;
|
|
306
|
-
constructor(phase) {
|
|
307
|
-
super(phase === "queue" ? "BLOCKED: timed out waiting for the active PR workflow checkout mutation; the existing owner still holds serialization and must settle before retrying" : "BLOCKED: PR workflow checkout mutation exceeded its execution deadline; serialization remains held until the in-flight action actually settles");
|
|
308
|
-
this.phase = phase;
|
|
309
|
-
this.name = "PrWorkflowCheckoutMutationTimeoutError";
|
|
310
|
-
}
|
|
311
|
-
}
|
|
312
|
-
async function withCheckoutMutationDeadline(promise, phase) {
|
|
313
|
-
let timeout;
|
|
314
|
-
try {
|
|
315
|
-
return await Promise.race([
|
|
316
|
-
promise,
|
|
317
|
-
new Promise((resolve2) => {
|
|
318
|
-
timeout = setTimeout(() => {
|
|
319
|
-
resolve2({
|
|
320
|
-
status: "timeout",
|
|
321
|
-
error: new PrWorkflowCheckoutMutationTimeoutError(phase)
|
|
322
|
-
});
|
|
323
|
-
}, hooks.checkoutMutationActionTimeoutMs);
|
|
324
|
-
})
|
|
325
|
-
]);
|
|
326
|
-
} finally {
|
|
327
|
-
if (timeout)
|
|
328
|
-
clearTimeout(timeout);
|
|
329
|
-
}
|
|
330
|
-
}
|
|
331
|
-
async function withPrWorkflowCheckoutMutationLock(directory, action) {
|
|
332
|
-
const key = checkoutMutationProjectKey(directory);
|
|
333
|
-
const previous = pendingCheckoutMutationsByProject.get(key) ?? Promise.resolve();
|
|
334
|
-
let release;
|
|
335
|
-
const current = new Promise((resolve2) => {
|
|
336
|
-
release = resolve2;
|
|
337
|
-
});
|
|
338
|
-
const queued = previous.then(() => current);
|
|
339
|
-
pendingCheckoutMutationsByProject.set(key, queued);
|
|
340
|
-
const previousResult = await withCheckoutMutationDeadline(previous.then(() => ({ status: "ready" })), "queue");
|
|
341
|
-
if (previousResult.status === "timeout") {
|
|
342
|
-
release();
|
|
343
|
-
queued.then(() => {
|
|
344
|
-
if (pendingCheckoutMutationsByProject.get(key) === queued) {
|
|
345
|
-
pendingCheckoutMutationsByProject.delete(key);
|
|
346
|
-
}
|
|
347
|
-
});
|
|
348
|
-
throw previousResult.error;
|
|
349
|
-
}
|
|
350
|
-
let lock;
|
|
351
|
-
try {
|
|
352
|
-
lock = await acquireCheckoutMutationLock(directory);
|
|
353
|
-
} catch (error) {
|
|
354
|
-
release();
|
|
355
|
-
if (pendingCheckoutMutationsByProject.get(key) === queued) {
|
|
356
|
-
pendingCheckoutMutationsByProject.delete(key);
|
|
357
|
-
}
|
|
358
|
-
throw error;
|
|
359
|
-
}
|
|
360
|
-
const actionOutcome = Promise.resolve().then(action).then((value) => ({ status: "fulfilled", value }), (error) => ({ status: "rejected", error }));
|
|
361
|
-
const outcome = await withCheckoutMutationDeadline(actionOutcome, "action");
|
|
362
|
-
if (outcome.status === "timeout") {
|
|
363
|
-
actionOutcome.then(async () => {
|
|
364
|
-
try {
|
|
365
|
-
await releaseCheckoutMutationLock(lock);
|
|
366
|
-
} catch {} finally {
|
|
367
|
-
release();
|
|
368
|
-
if (pendingCheckoutMutationsByProject.get(key) === queued) {
|
|
369
|
-
pendingCheckoutMutationsByProject.delete(key);
|
|
370
|
-
}
|
|
371
|
-
}
|
|
372
|
-
});
|
|
373
|
-
throw outcome.error;
|
|
374
|
-
}
|
|
375
|
-
try {
|
|
376
|
-
if (outcome.status === "rejected")
|
|
377
|
-
throw outcome.error;
|
|
378
|
-
return outcome.value;
|
|
379
|
-
} finally {
|
|
380
|
-
try {
|
|
381
|
-
await releaseCheckoutMutationLock(lock);
|
|
382
|
-
} finally {
|
|
383
|
-
release();
|
|
384
|
-
if (pendingCheckoutMutationsByProject.get(key) === queued) {
|
|
385
|
-
pendingCheckoutMutationsByProject.delete(key);
|
|
386
|
-
}
|
|
387
|
-
}
|
|
388
|
-
}
|
|
389
|
-
}
|
|
390
|
-
async function acquireCheckoutMutationLock(directory) {
|
|
391
|
-
const lockPath = workflowCheckoutMutationLockPath(directory);
|
|
392
|
-
const verifiedParent = await ensurePrWorkflowSafeParentDirectory(directory, lockPath);
|
|
393
|
-
for (let attempt = 0;attempt < STATE_MUTATION_LOCK_MAX_ATTEMPTS; attempt++) {
|
|
394
|
-
try {
|
|
395
|
-
const handle = await hooks.openCheckoutLock(lockPath);
|
|
396
|
-
const lock = {
|
|
397
|
-
ownerToken: randomUUID(),
|
|
398
|
-
pid: process.pid,
|
|
399
|
-
createdAtMs: hooks.nowMs()
|
|
400
|
-
};
|
|
401
|
-
let lockIdentity;
|
|
402
|
-
let writeError;
|
|
403
|
-
try {
|
|
404
|
-
const openedStat = await handle.stat({ bigint: true });
|
|
405
|
-
lockIdentity = { dev: openedStat.dev, ino: openedStat.ino };
|
|
406
|
-
lockIdentity = await assertOpenedSwarmFileIdentity(directory, lockPath, handle, verifiedParent, "PR workflow checkout mutation lock");
|
|
407
|
-
await hooks.beforeCheckoutLockWrite?.();
|
|
408
|
-
await handle.writeFile(JSON.stringify(lock), "utf-8");
|
|
409
|
-
} catch (error) {
|
|
410
|
-
writeError = error;
|
|
411
|
-
} finally {
|
|
412
|
-
await handle.close().catch(() => {
|
|
413
|
-
return;
|
|
414
|
-
});
|
|
415
|
-
}
|
|
416
|
-
if (writeError) {
|
|
417
|
-
if (lockIdentity) {
|
|
418
|
-
await removeCheckoutMutationLockByIdentity(directory, lockPath, lockIdentity, verifiedParent);
|
|
419
|
-
}
|
|
420
|
-
throw writeError;
|
|
421
|
-
}
|
|
422
|
-
return { path: lockPath, ownerToken: lock.ownerToken };
|
|
423
|
-
} catch (error) {
|
|
424
|
-
if (error.code !== "EEXIST")
|
|
425
|
-
throw error;
|
|
426
|
-
if (await reclaimAbandonedCheckoutMutationLock(lockPath))
|
|
427
|
-
continue;
|
|
428
|
-
if (attempt < STATE_MUTATION_LOCK_MAX_ATTEMPTS - 1) {
|
|
429
|
-
await delay(STATE_MUTATION_LOCK_RETRY_DELAY_MS);
|
|
430
|
-
}
|
|
431
|
-
}
|
|
432
|
-
}
|
|
433
|
-
throw new Error("BLOCKED: PR workflow checkout mutation is being handled by another process; retry after that checkout settles");
|
|
434
|
-
}
|
|
435
|
-
async function releaseCheckoutMutationLock(lock) {
|
|
436
|
-
let lastError;
|
|
437
|
-
for (let attempt = 0;attempt < WINDOWS_RENAME_MAX_RETRIES; attempt++) {
|
|
438
|
-
try {
|
|
439
|
-
if (await removeCheckoutMutationLockIfOwned(lock.path, lock.ownerToken)) {
|
|
440
|
-
completedCheckoutLockOwners.delete(lock.path);
|
|
441
|
-
return;
|
|
442
|
-
}
|
|
443
|
-
lastError = new Error("PR workflow checkout mutation lock ownership changed before release");
|
|
444
|
-
} catch (error) {
|
|
445
|
-
lastError = error;
|
|
446
|
-
}
|
|
447
|
-
if (attempt < WINDOWS_RENAME_MAX_RETRIES - 1) {
|
|
448
|
-
await delay(RENAME_RETRY_DELAY_MS);
|
|
449
|
-
}
|
|
450
|
-
}
|
|
451
|
-
rememberCompletedCheckoutLockOwner(lock.path, lock.ownerToken);
|
|
452
|
-
throw lastError instanceof Error ? lastError : new Error("PR workflow checkout mutation lock release failed");
|
|
453
|
-
}
|
|
454
|
-
async function reclaimAbandonedCheckoutMutationLock(lockPath) {
|
|
455
|
-
const lock = await readCheckoutMutationLock(lockPath);
|
|
456
|
-
if (lock) {
|
|
457
|
-
if (lock.pid === process.pid && completedCheckoutLockOwners.get(lockPath) === lock.ownerToken) {
|
|
458
|
-
const removed = await removeCheckoutMutationLockIfOwned(lockPath, lock.ownerToken);
|
|
459
|
-
if (removed)
|
|
460
|
-
completedCheckoutLockOwners.delete(lockPath);
|
|
461
|
-
return removed;
|
|
462
|
-
}
|
|
463
|
-
if (hooks.isProcessAlive(lock.pid))
|
|
464
|
-
return false;
|
|
465
|
-
return removeCheckoutMutationLockIfOwned(lockPath, lock.ownerToken);
|
|
466
|
-
}
|
|
467
|
-
try {
|
|
468
|
-
const stat2 = await fsp.stat(lockPath);
|
|
469
|
-
if (hooks.nowMs() - stat2.mtimeMs < STATE_MUTATION_LOCK_UNINITIALIZED_STALE_MS) {
|
|
470
|
-
return false;
|
|
471
|
-
}
|
|
472
|
-
await fsp.rm(lockPath, { force: true });
|
|
473
|
-
return true;
|
|
474
|
-
} catch (error) {
|
|
475
|
-
if (error.code === "ENOENT")
|
|
476
|
-
return true;
|
|
477
|
-
throw error;
|
|
478
|
-
}
|
|
479
|
-
}
|
|
480
|
-
async function readCheckoutMutationLock(lockPath) {
|
|
481
|
-
let raw;
|
|
482
|
-
try {
|
|
483
|
-
raw = await fsp.readFile(lockPath, "utf-8");
|
|
484
|
-
} catch (error) {
|
|
485
|
-
if (error.code === "ENOENT")
|
|
486
|
-
return null;
|
|
487
|
-
throw error;
|
|
488
|
-
}
|
|
489
|
-
try {
|
|
490
|
-
const parsed = JSON.parse(raw);
|
|
491
|
-
if (typeof parsed === "object" && parsed !== null && typeof parsed.ownerToken === "string" && parsed.ownerToken.length > 0 && typeof parsed.pid === "number" && Number.isInteger(parsed.pid) && parsed.pid > 0 && typeof parsed.createdAtMs === "number" && Number.isFinite(parsed.createdAtMs)) {
|
|
492
|
-
return parsed;
|
|
493
|
-
}
|
|
494
|
-
} catch {}
|
|
495
|
-
return null;
|
|
496
|
-
}
|
|
497
|
-
async function removeCheckoutMutationLockIfOwned(lockPath, ownerToken) {
|
|
498
|
-
const lock = await readCheckoutMutationLock(lockPath);
|
|
499
|
-
if (!lock || lock.ownerToken !== ownerToken)
|
|
500
|
-
return false;
|
|
501
|
-
try {
|
|
502
|
-
await hooks.removeCheckoutLock(lockPath);
|
|
503
|
-
return true;
|
|
504
|
-
} catch (error) {
|
|
505
|
-
if (error.code === "ENOENT")
|
|
506
|
-
return false;
|
|
507
|
-
throw error;
|
|
508
|
-
}
|
|
509
|
-
}
|
|
510
|
-
async function removeCheckoutMutationLockByIdentity(directory, lockPath, identity, verifiedParent) {
|
|
511
|
-
let lastError;
|
|
512
|
-
for (let attempt = 0;attempt < WINDOWS_RENAME_MAX_RETRIES; attempt++) {
|
|
513
|
-
try {
|
|
514
|
-
await assertClosedSwarmFileIdentity(directory, lockPath, identity, verifiedParent, "PR workflow checkout mutation lock");
|
|
515
|
-
await hooks.removeCheckoutLock(lockPath);
|
|
516
|
-
return;
|
|
517
|
-
} catch (error) {
|
|
518
|
-
if (error.code === "ENOENT")
|
|
519
|
-
return;
|
|
520
|
-
lastError = error;
|
|
521
|
-
if (attempt < WINDOWS_RENAME_MAX_RETRIES - 1) {
|
|
522
|
-
await delay(RENAME_RETRY_DELAY_MS);
|
|
523
|
-
}
|
|
524
|
-
}
|
|
525
|
-
}
|
|
526
|
-
throw lastError instanceof Error ? lastError : new Error("PR workflow checkout mutation lock cleanup failed");
|
|
527
|
-
}
|
|
528
|
-
function rememberCompletedCheckoutLockOwner(lockPath, ownerToken) {
|
|
529
|
-
completedCheckoutLockOwners.delete(lockPath);
|
|
530
|
-
completedCheckoutLockOwners.set(lockPath, ownerToken);
|
|
531
|
-
while (completedCheckoutLockOwners.size > MAX_COMPLETED_CHECKOUT_LOCK_OWNERS) {
|
|
532
|
-
const oldest = completedCheckoutLockOwners.keys().next().value;
|
|
533
|
-
if (oldest === undefined)
|
|
534
|
-
break;
|
|
535
|
-
completedCheckoutLockOwners.delete(oldest);
|
|
536
|
-
}
|
|
537
|
-
}
|
|
538
|
-
async function withSessionStateMutation(directory, sessionID, action) {
|
|
539
|
-
const key = stateCacheKey(directory, sessionID);
|
|
540
|
-
const previous = pendingStateMutationsByProjectSession.get(key) ?? Promise.resolve();
|
|
541
|
-
let release;
|
|
542
|
-
const current = new Promise((resolve2) => {
|
|
543
|
-
release = resolve2;
|
|
544
|
-
});
|
|
545
|
-
const queued = previous.then(() => current);
|
|
546
|
-
pendingStateMutationsByProjectSession.set(key, queued);
|
|
547
|
-
await previous;
|
|
548
|
-
try {
|
|
549
|
-
const lock = await acquireSessionStateMutationLock(directory, sessionID);
|
|
550
|
-
try {
|
|
551
|
-
return await action();
|
|
552
|
-
} finally {
|
|
553
|
-
await releaseSessionStateMutationLock(lock);
|
|
554
|
-
}
|
|
555
|
-
} finally {
|
|
556
|
-
release();
|
|
557
|
-
if (pendingStateMutationsByProjectSession.get(key) === queued) {
|
|
558
|
-
pendingStateMutationsByProjectSession.delete(key);
|
|
559
|
-
}
|
|
560
|
-
}
|
|
561
|
-
}
|
|
562
|
-
async function acquireSessionStateMutationLock(directory, sessionID) {
|
|
563
|
-
const lockPath = workflowGateStateLockPath(directory, sessionID);
|
|
564
|
-
const verifiedParent = await ensurePrWorkflowSafeParentDirectory(directory, lockPath);
|
|
565
|
-
for (let attempt = 0;attempt < STATE_MUTATION_LOCK_MAX_ATTEMPTS; attempt++) {
|
|
566
|
-
try {
|
|
567
|
-
const handle = await fsp.open(lockPath, "wx");
|
|
568
|
-
const lock = {
|
|
569
|
-
ownerToken: randomUUID(),
|
|
570
|
-
pid: process.pid,
|
|
571
|
-
createdAtMs: hooks.nowMs()
|
|
572
|
-
};
|
|
573
|
-
let writeError;
|
|
574
|
-
try {
|
|
575
|
-
await hooks.beforeSessionStateLockWrite?.();
|
|
576
|
-
await assertOpenedSwarmFileIdentity(directory, lockPath, handle, verifiedParent, "PR workflow state mutation lock");
|
|
577
|
-
await handle.writeFile(JSON.stringify(lock), "utf-8");
|
|
578
|
-
} catch (error) {
|
|
579
|
-
writeError = error;
|
|
580
|
-
} finally {
|
|
581
|
-
await handle.close().catch(() => {
|
|
582
|
-
return;
|
|
583
|
-
});
|
|
584
|
-
}
|
|
585
|
-
if (writeError) {
|
|
586
|
-
await removeSessionStateMutationLockIfOwned(lockPath, lock.ownerToken);
|
|
587
|
-
throw writeError;
|
|
588
|
-
}
|
|
589
|
-
return { path: lockPath, ownerToken: lock.ownerToken };
|
|
590
|
-
} catch (error) {
|
|
591
|
-
if (error.code !== "EEXIST")
|
|
592
|
-
throw error;
|
|
593
|
-
if (await reclaimAbandonedSessionStateMutationLock(lockPath))
|
|
594
|
-
continue;
|
|
595
|
-
if (attempt < STATE_MUTATION_LOCK_MAX_ATTEMPTS - 1) {
|
|
596
|
-
await delay(STATE_MUTATION_LOCK_RETRY_DELAY_MS);
|
|
597
|
-
}
|
|
598
|
-
}
|
|
599
|
-
}
|
|
600
|
-
throw new Error("BLOCKED: PR workflow gate state is being mutated by another process; retry after that session transition finishes");
|
|
601
|
-
}
|
|
602
|
-
async function releaseSessionStateMutationLock(lock) {
|
|
603
|
-
try {
|
|
604
|
-
await removeSessionStateMutationLockIfOwned(lock.path, lock.ownerToken);
|
|
605
|
-
} catch {}
|
|
606
|
-
}
|
|
607
|
-
async function reclaimAbandonedSessionStateMutationLock(lockPath) {
|
|
608
|
-
const lock = await readSessionStateMutationLock(lockPath);
|
|
609
|
-
if (lock) {
|
|
610
|
-
if (hooks.isProcessAlive(lock.pid))
|
|
611
|
-
return false;
|
|
612
|
-
return removeSessionStateMutationLockIfOwned(lockPath, lock.ownerToken);
|
|
613
|
-
}
|
|
614
|
-
try {
|
|
615
|
-
const stat2 = await fsp.stat(lockPath);
|
|
616
|
-
if (hooks.nowMs() - stat2.mtimeMs < STATE_MUTATION_LOCK_UNINITIALIZED_STALE_MS) {
|
|
617
|
-
return false;
|
|
618
|
-
}
|
|
619
|
-
await fsp.rm(lockPath, { force: true });
|
|
620
|
-
return true;
|
|
621
|
-
} catch (error) {
|
|
622
|
-
if (error.code === "ENOENT")
|
|
623
|
-
return true;
|
|
624
|
-
throw error;
|
|
625
|
-
}
|
|
626
|
-
}
|
|
627
|
-
async function readSessionStateMutationLock(lockPath) {
|
|
628
|
-
let raw;
|
|
629
|
-
try {
|
|
630
|
-
raw = await fsp.readFile(lockPath, "utf-8");
|
|
631
|
-
} catch (error) {
|
|
632
|
-
if (error.code === "ENOENT")
|
|
633
|
-
return null;
|
|
634
|
-
throw error;
|
|
635
|
-
}
|
|
636
|
-
try {
|
|
637
|
-
const parsed = JSON.parse(raw);
|
|
638
|
-
if (typeof parsed === "object" && parsed !== null && typeof parsed.ownerToken === "string" && parsed.ownerToken.length > 0 && typeof parsed.pid === "number" && Number.isInteger(parsed.pid) && parsed.pid > 0 && typeof parsed.createdAtMs === "number" && Number.isFinite(parsed.createdAtMs)) {
|
|
639
|
-
return parsed;
|
|
640
|
-
}
|
|
641
|
-
} catch {}
|
|
642
|
-
return null;
|
|
643
|
-
}
|
|
644
|
-
async function removeSessionStateMutationLockIfOwned(lockPath, ownerToken) {
|
|
645
|
-
const lock = await readSessionStateMutationLock(lockPath);
|
|
646
|
-
if (!lock || lock.ownerToken !== ownerToken)
|
|
647
|
-
return false;
|
|
648
|
-
try {
|
|
649
|
-
await fsp.rm(lockPath);
|
|
650
|
-
return true;
|
|
651
|
-
} catch (error) {
|
|
652
|
-
if (error.code === "ENOENT")
|
|
653
|
-
return false;
|
|
654
|
-
throw error;
|
|
655
|
-
}
|
|
656
|
-
}
|
|
657
|
-
|
|
658
|
-
// src/pr-review/authorization.ts
|
|
659
|
-
var AUTHORIZATION_TTL_MS = 10 * 60 * 1000;
|
|
660
|
-
var MAX_ACTIVE_AUTHORIZATIONS = 8;
|
|
661
|
-
var MAX_PERSISTED_AUTHORIZATIONS = 32;
|
|
662
|
-
var REENTRY_AUTHORIZATIONS_MAX_BYTES = 64 * 1024;
|
|
663
|
-
var bindingReader;
|
|
664
|
-
function bindPrReviewReentryBindingReader(reader) {
|
|
665
|
-
bindingReader = reader;
|
|
666
|
-
}
|
|
667
|
-
async function currentBinding(directory, sessionID) {
|
|
668
|
-
if (!bindingReader) {
|
|
669
|
-
throw new Error("BLOCKED: re-entry authorization binding reader is not bound (the PR workflow gate must bind it at module init)");
|
|
670
|
-
}
|
|
671
|
-
return bindingReader(directory, sessionID);
|
|
672
|
-
}
|
|
673
|
-
var PrReviewReentryAuthorizationRecordSchema = exports_external.object({
|
|
674
|
-
schemaVersion: exports_external.literal(1),
|
|
675
|
-
authorizationId: exports_external.string().min(1).max(64),
|
|
676
|
-
sessionId: exports_external.string().min(1).max(256),
|
|
677
|
-
workflowInstanceId: exports_external.string().min(1).max(128).optional(),
|
|
678
|
-
runId: PrReviewRunIdSchema.optional(),
|
|
679
|
-
prHeadSha: exports_external.string().regex(/^[0-9a-f]{6,64}$/i),
|
|
680
|
-
revisionDigest: exports_external.string().min(1).max(256),
|
|
681
|
-
role: exports_external.enum(["reviewer", "test_engineer"]),
|
|
682
|
-
generation: exports_external.number().int().nonnegative(),
|
|
683
|
-
createdAt: exports_external.string().datetime(),
|
|
684
|
-
expiresAt: exports_external.string().datetime(),
|
|
685
|
-
consumedAt: exports_external.string().datetime().optional(),
|
|
686
|
-
consumedCallId: exports_external.string().min(1).max(128).optional()
|
|
687
|
-
}).strict();
|
|
688
|
-
var AuthorizationFileSchema = exports_external.object({
|
|
689
|
-
schemaVersion: exports_external.literal(1),
|
|
690
|
-
sessionId: exports_external.string().min(1).max(256),
|
|
691
|
-
authorizations: exports_external.array(PrReviewReentryAuthorizationRecordSchema).max(MAX_PERSISTED_AUTHORIZATIONS)
|
|
692
|
-
}).strict();
|
|
693
|
-
function reentryAuthorizationFilePath(directory, sessionID) {
|
|
694
|
-
const relative2 = path2.join("pr-review", "reentry-authorizations", `${prWorkflowSessionFileStem(sessionID)}.json`);
|
|
695
|
-
return validateSwarmPath(directory, relative2);
|
|
696
|
-
}
|
|
697
|
-
async function readAuthorizationFile(filePath) {
|
|
698
|
-
let raw;
|
|
699
|
-
try {
|
|
700
|
-
const stat3 = await fsp2.stat(filePath);
|
|
701
|
-
if (!stat3.isFile() || stat3.size > REENTRY_AUTHORIZATIONS_MAX_BYTES) {
|
|
702
|
-
throw new Error("reentry authorization store is not a bounded regular file");
|
|
703
|
-
}
|
|
704
|
-
raw = await fsp2.readFile(filePath, "utf8");
|
|
705
|
-
} catch (error) {
|
|
706
|
-
if (error.code === "ENOENT")
|
|
707
|
-
return null;
|
|
708
|
-
throw error;
|
|
709
|
-
}
|
|
710
|
-
const decoded = JSON.parse(raw);
|
|
711
|
-
const parsed = AuthorizationFileSchema.safeParse(decoded);
|
|
712
|
-
if (!parsed.success) {
|
|
713
|
-
throw new Error(`reentry authorization store is invalid: ${parsed.error.issues.map((issue) => `${issue.path.join(".")}: ${issue.message}`).join("; ")}`);
|
|
714
|
-
}
|
|
715
|
-
return parsed.data;
|
|
716
|
-
}
|
|
717
|
-
var RENAME_RETRY_DELAYS_MS = [10, 25, 50, 100, 200];
|
|
718
|
-
var RETRYABLE_RENAME_CODES = new Set(["EPERM", "EBUSY", "EACCES", "EEXIST"]);
|
|
719
|
-
async function renameWithRetryAsync(tempPath, targetPath) {
|
|
720
|
-
let lastError;
|
|
721
|
-
for (let attempt = 0;attempt <= _internals.renameRetryDelaysMs.length; attempt++) {
|
|
722
|
-
try {
|
|
723
|
-
await _internals.renameImpl(tempPath, targetPath);
|
|
724
|
-
return;
|
|
725
|
-
} catch (err) {
|
|
726
|
-
lastError = err;
|
|
727
|
-
const code = err?.code;
|
|
728
|
-
if (!code || !RETRYABLE_RENAME_CODES.has(code))
|
|
729
|
-
throw err;
|
|
730
|
-
if (attempt < _internals.renameRetryDelaysMs.length) {
|
|
731
|
-
await new Promise((resolve2) => setTimeout(resolve2, _internals.renameRetryDelaysMs[attempt]));
|
|
732
|
-
}
|
|
733
|
-
}
|
|
734
|
-
}
|
|
735
|
-
throw lastError;
|
|
736
|
-
}
|
|
737
|
-
async function writeAuthorizationFile(filePath, file) {
|
|
738
|
-
const serialized = `${JSON.stringify(file, null, 2)}
|
|
739
|
-
`;
|
|
740
|
-
if (Buffer.byteLength(serialized, "utf8") > REENTRY_AUTHORIZATIONS_MAX_BYTES) {
|
|
741
|
-
throw new Error("reentry authorization store exceeds its write bound");
|
|
742
|
-
}
|
|
743
|
-
await fsp2.mkdir(path2.dirname(filePath), { recursive: true });
|
|
744
|
-
const tempPath = path2.join(path2.dirname(filePath), `.${path2.basename(filePath)}.${randomUUID2()}.tmp`);
|
|
745
|
-
try {
|
|
746
|
-
await fsp2.writeFile(tempPath, serialized, { encoding: "utf8", flag: "wx" });
|
|
747
|
-
await renameWithRetryAsync(tempPath, filePath);
|
|
748
|
-
} finally {
|
|
749
|
-
await fsp2.rm(tempPath, { force: true }).catch(() => {
|
|
750
|
-
return;
|
|
751
|
-
});
|
|
752
|
-
}
|
|
753
|
-
}
|
|
754
|
-
function pruneAuthorizations(records, nowMs) {
|
|
755
|
-
const kept = [];
|
|
756
|
-
for (const record of [...records].sort((left, right) => right.createdAt.localeCompare(left.createdAt)).slice(0, MAX_PERSISTED_AUTHORIZATIONS)) {
|
|
757
|
-
if (record.consumedAt)
|
|
758
|
-
continue;
|
|
759
|
-
if (Date.parse(record.expiresAt) <= nowMs)
|
|
760
|
-
continue;
|
|
761
|
-
kept.push(record);
|
|
762
|
-
}
|
|
763
|
-
return kept;
|
|
764
|
-
}
|
|
765
|
-
var lf = import_proper_lockfile.default;
|
|
766
|
-
async function withAuthorizationLock(filePath, write) {
|
|
767
|
-
await fsp2.mkdir(path2.dirname(filePath), { recursive: true });
|
|
768
|
-
const release = await lf.lock(path2.dirname(filePath), {
|
|
769
|
-
lockfilePath: `${filePath}.lock`,
|
|
770
|
-
retries: { retries: 8, minTimeout: 10, maxTimeout: 100 },
|
|
771
|
-
stale: 1e4,
|
|
772
|
-
update: 1000,
|
|
773
|
-
realpath: false
|
|
774
|
-
});
|
|
775
|
-
try {
|
|
776
|
-
return await write();
|
|
777
|
-
} finally {
|
|
778
|
-
await release().catch(() => {
|
|
779
|
-
return;
|
|
780
|
-
});
|
|
781
|
-
}
|
|
782
|
-
}
|
|
783
|
-
async function issuePrReviewReentryAuthorization(directory, sessionID, request) {
|
|
784
|
-
const trimmedSession = sessionID.trim();
|
|
785
|
-
if (!trimmedSession) {
|
|
786
|
-
throw new Error("BLOCKED: re-entry authorization requires an active sessionID");
|
|
787
|
-
}
|
|
788
|
-
const binding = await currentBinding(directory, trimmedSession);
|
|
789
|
-
if (!binding || binding.prHeadSha !== request.prHeadSha.toLowerCase()) {
|
|
790
|
-
throw new Error(`BLOCKED: re-entry authorization requires an active PR_REVIEW workflow bound to the declared head (active: ${binding?.prHeadSha ?? "(none)"})`);
|
|
791
|
-
}
|
|
792
|
-
if (request.runId && binding.runId && request.runId !== binding.runId) {
|
|
793
|
-
throw new Error("BLOCKED: re-entry authorization run does not match the active PR-review run");
|
|
794
|
-
}
|
|
795
|
-
const filePath = reentryAuthorizationFilePath(directory, trimmedSession);
|
|
796
|
-
return withAuthorizationLock(filePath, async () => {
|
|
797
|
-
const existing = await readAuthorizationFile(filePath);
|
|
798
|
-
const nowMs = Date.now();
|
|
799
|
-
const now = new Date(nowMs).toISOString();
|
|
800
|
-
const records = pruneAuthorizations(existing?.authorizations ?? [], nowMs);
|
|
801
|
-
const active = records.filter((record2) => !record2.consumedAt && record2.role === request.role && record2.generation === binding.generation);
|
|
802
|
-
if (active.length > 0) {
|
|
803
|
-
throw new Error(`BLOCKED: an unconsumed re-entry authorization for role "${request.role}" at generation ${binding.generation} already exists; use it (one immediate Task dispatch) before issuing another`);
|
|
804
|
-
}
|
|
805
|
-
if (records.length >= MAX_ACTIVE_AUTHORIZATIONS) {
|
|
806
|
-
throw new Error(`BLOCKED: re-entry authorization store is at its bound of ${MAX_ACTIVE_AUTHORIZATIONS} active authorizations`);
|
|
807
|
-
}
|
|
808
|
-
const record = {
|
|
809
|
-
schemaVersion: 1,
|
|
810
|
-
authorizationId: randomUUID2(),
|
|
811
|
-
sessionId: trimmedSession,
|
|
812
|
-
...binding.workflowInstanceId ? { workflowInstanceId: binding.workflowInstanceId } : {},
|
|
813
|
-
...binding.runId ? { runId: binding.runId } : {},
|
|
814
|
-
prHeadSha: binding.prHeadSha,
|
|
815
|
-
revisionDigest: binding.revisionDigest,
|
|
816
|
-
role: request.role,
|
|
817
|
-
generation: binding.generation,
|
|
818
|
-
createdAt: now,
|
|
819
|
-
expiresAt: new Date(nowMs + AUTHORIZATION_TTL_MS).toISOString()
|
|
820
|
-
};
|
|
821
|
-
await writeAuthorizationFile(filePath, {
|
|
822
|
-
schemaVersion: 1,
|
|
823
|
-
sessionId: trimmedSession,
|
|
824
|
-
authorizations: [record, ...records]
|
|
825
|
-
});
|
|
826
|
-
return record;
|
|
827
|
-
});
|
|
828
|
-
}
|
|
829
|
-
async function consumePrReviewReentryAuthorization(directory, sessionID, request) {
|
|
830
|
-
const trimmedSession = sessionID.trim();
|
|
831
|
-
if (!trimmedSession)
|
|
832
|
-
return null;
|
|
833
|
-
const filePath = reentryAuthorizationFilePath(directory, trimmedSession);
|
|
834
|
-
try {
|
|
835
|
-
return await withAuthorizationLock(filePath, async () => {
|
|
836
|
-
const existing = await readAuthorizationFile(filePath);
|
|
837
|
-
if (!existing)
|
|
838
|
-
return null;
|
|
839
|
-
const nowMs = Date.now();
|
|
840
|
-
const records = pruneAuthorizations(existing.authorizations, nowMs);
|
|
841
|
-
const index = records.findIndex((record) => !record.consumedAt && record.role === request.role && record.sessionId === trimmedSession && Date.parse(record.expiresAt) > nowMs);
|
|
842
|
-
if (index < 0) {
|
|
843
|
-
if (records.length !== existing.authorizations.length) {
|
|
844
|
-
await writeAuthorizationFile(filePath, {
|
|
845
|
-
...existing,
|
|
846
|
-
authorizations: records
|
|
847
|
-
});
|
|
848
|
-
}
|
|
849
|
-
return null;
|
|
850
|
-
}
|
|
851
|
-
const binding = await currentBinding(directory, trimmedSession);
|
|
852
|
-
const candidate = records[index];
|
|
853
|
-
if (!binding || binding.prHeadSha !== candidate.prHeadSha || binding.revisionDigest !== candidate.revisionDigest || binding.generation !== candidate.generation || candidate.workflowInstanceId !== undefined && binding.workflowInstanceId !== candidate.workflowInstanceId || candidate.workflowInstanceId === undefined && binding.workflowInstanceId !== undefined) {
|
|
854
|
-
return null;
|
|
855
|
-
}
|
|
856
|
-
const consumed = {
|
|
857
|
-
...candidate,
|
|
858
|
-
consumedAt: new Date(nowMs).toISOString(),
|
|
859
|
-
consumedCallId: request.callID
|
|
860
|
-
};
|
|
861
|
-
records[index] = consumed;
|
|
862
|
-
await writeAuthorizationFile(filePath, {
|
|
863
|
-
...existing,
|
|
864
|
-
authorizations: records
|
|
865
|
-
});
|
|
866
|
-
return consumed;
|
|
867
|
-
});
|
|
868
|
-
} catch {
|
|
869
|
-
return null;
|
|
870
|
-
}
|
|
871
|
-
}
|
|
872
|
-
var _internals = {
|
|
873
|
-
AUTHORIZATION_TTL_MS,
|
|
874
|
-
MAX_ACTIVE_AUTHORIZATIONS,
|
|
875
|
-
reentryAuthorizationFilePath,
|
|
876
|
-
pruneAuthorizations,
|
|
877
|
-
renameImpl: (from, to) => fsp2.rename(from, to),
|
|
878
|
-
renameRetryDelaysMs: RENAME_RETRY_DELAYS_MS,
|
|
879
|
-
renameWithRetryAsync,
|
|
880
|
-
writeAuthorizationFile
|
|
881
|
-
};
|
|
882
|
-
|
|
883
|
-
export { normalizeSessionID, workflowGateStateRelativePath, workflowGateStateLockRelativePath, workflowCheckoutMutationLockRelativePath, workflowGateStatePath, MAX_TRACKED_SESSIONS, CHECKOUT_MUTATION_ACTION_TIMEOUT_MS, isoNow, sameBigIntFileIdentity, normalizeComparableFsPath, rememberState, forgetTrackedPrWorkflowState, resetPrReviewPersistenceCaches, defaultPersistenceHooks, bindPrReviewPersistenceHooks, bindPrReviewStateCodec, writeAtomicJson, readPrWorkflowGateStateFromDisk, writeStateWhileLocked, withPrWorkflowCheckoutMutationLock, withSessionStateMutation, bindPrReviewReentryBindingReader, issuePrReviewReentryAuthorization, consumePrReviewReentryAuthorization, _internals };
|