@sabaiway/agent-workflow-kit 5.4.0 → 5.5.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.
- package/CHANGELOG.md +68 -0
- package/README.md +1 -0
- package/SKILL.md +5 -1
- package/bridges/antigravity-cli-bridge/SKILL.md +1 -1
- package/bridges/antigravity-cli-bridge/bin/agy-review.sh +1 -1
- package/bridges/antigravity-cli-bridge/capability.json +1 -1
- package/bridges/codex-cli-bridge/SKILL.md +51 -4
- package/bridges/codex-cli-bridge/bin/codex-exec.sh +616 -24
- package/bridges/codex-cli-bridge/bin/codex-exec.test.mjs +700 -1
- package/bridges/codex-cli-bridge/bin/codex-review.sh +1 -1
- package/bridges/codex-cli-bridge/capability.json +15 -10
- package/capability.json +1 -1
- package/package.json +1 -1
- package/references/modes/dispatch.md +29 -0
- package/references/modes/receipt-deadline.md +3 -3
- package/tools/commands.mjs +7 -0
- package/tools/core-evidence.mjs +37 -3
- package/tools/detect-backends.mjs +5 -4
- package/tools/dispatch-record.mjs +10 -3
- package/tools/dispatch-store.mjs +392 -0
- package/tools/dispatch.mjs +1779 -0
- package/tools/doc-parity.mjs +10 -2
- package/tools/exec-producer.mjs +483 -0
- package/tools/exec-receipt.mjs +263 -0
- package/tools/flow-store.mjs +111 -462
- package/tools/receipt-deadline.mjs +25 -3
- package/tools/release-scan.mjs +33 -0
- package/tools/store-append.mjs +444 -0
package/tools/flow-store.mjs
CHANGED
|
@@ -10,6 +10,12 @@
|
|
|
10
10
|
// validation, malformed-store refusal, replay refusal, chain-sequence and supersession legality) —
|
|
11
11
|
// an illegal record never lands.
|
|
12
12
|
//
|
|
13
|
+
// That lock/CAS + serialized-append machinery now lives in the PARAMETERIZED store-append.mjs leaf
|
|
14
|
+
// (delegation Plan 1 D12), extracted from here unchanged so a second store can ride the identical
|
|
15
|
+
// discipline instead of a second copy of it. This module keeps everything flow-SPECIFIC: the seams
|
|
16
|
+
// it injects (path resolution, nouns, knob names, validator, parser) and `flowSemanticPreflight` —
|
|
17
|
+
// the per-kind legality the lane runs inside the critical section.
|
|
18
|
+
//
|
|
13
19
|
// Phase 3 adds the mint primitives that need the tree: the adoption mint (frontmatter planId +
|
|
14
20
|
// plan content digest, #58), the canonical owning-worktree identity (#49), the generic reference
|
|
15
21
|
// validator + prior-terminal resolution in the append preflight (#63), and the bookkeeping-delta
|
|
@@ -23,23 +29,25 @@
|
|
|
23
29
|
// git dir, not a security boundary.
|
|
24
30
|
|
|
25
31
|
import { createHash } from 'node:crypto';
|
|
26
|
-
import { readFileSync,
|
|
27
|
-
import { join,
|
|
28
|
-
import { hostname } from 'node:os';
|
|
32
|
+
import { readFileSync, lstatSync, readlinkSync } from 'node:fs';
|
|
33
|
+
import { join, resolve } from 'node:path';
|
|
29
34
|
import { spawnSync } from 'node:child_process';
|
|
30
|
-
import {
|
|
31
|
-
import { parsePositiveIntKnob } from './changed-surface.mjs';
|
|
35
|
+
import { lstatNoFollow } from './atomic-write.mjs';
|
|
32
36
|
import { FLOW_SCHEMA_VERSION, CHAIN_KIND, validateFlowRecord, validateChainSequence, validateSupersessions, authoritativeFlowRecords, canonicalFlowDigest, flowRecordKey, subsetFoldBatchDigest, subsetGateIdsDigest, SUBSET_ATTEMPT_DIAGNOSIS_FROM } from './flow-record.mjs';
|
|
33
37
|
import { isNeverCommittableStat, isBinaryFile, lexicalRepoRelative, resolveBase, computeTreeFingerprint } from './core-evidence.mjs';
|
|
34
38
|
import { derivePregateSubsetIds, GATES_REL } from './gates-declaration.mjs';
|
|
35
39
|
import { CONFIG_REL } from './orchestration-config.mjs';
|
|
40
|
+
// The lock/CAS discipline and the serialized append itself live in the PARAMETERIZED
|
|
41
|
+
// store-append.mjs leaf (D12) — this module injects the flow store's own nouns, seams, validator
|
|
42
|
+
// and semantic preflight.
|
|
43
|
+
import { createStoreAppendLane } from './store-append.mjs';
|
|
36
44
|
// The read half lives in flow-store-read.mjs (it OWNS no write API — read-only surfaces like the
|
|
37
45
|
// procedures advisor import it directly) and is RE-EXPORTED here — every existing consumer keeps
|
|
38
46
|
// its import site.
|
|
39
47
|
import {
|
|
40
48
|
FLOW_STORE_STOP, flowStoreStop, FLOW_STORE_BASENAME, FLOW_LOCK_SUFFIX, gitLine,
|
|
41
49
|
resolveFlowStorePath, resolveFlowLockPath, parseFlowStoreText, readFlowStore,
|
|
42
|
-
|
|
50
|
+
deriveFlowOwner, describeNonRegular,
|
|
43
51
|
} from './flow-store-read.mjs';
|
|
44
52
|
|
|
45
53
|
export {
|
|
@@ -61,278 +69,29 @@ const gitBuf = (args, cwd) => {
|
|
|
61
69
|
};
|
|
62
70
|
const sha256Hex = (bytes) => createHash('sha256').update(bytes).digest('hex');
|
|
63
71
|
|
|
64
|
-
// ── the
|
|
65
|
-
|
|
66
|
-
//
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
//
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
return st;
|
|
85
|
-
};
|
|
86
|
-
|
|
87
|
-
// Bounded positional comparison of a HELD fd against the snapshot: at most snapshot-length bytes
|
|
88
|
-
// plus ONE growth-probe byte (positional — the fd offset sits at EOF). Changed bytes, truncation,
|
|
89
|
-
// or growth report false.
|
|
90
|
-
const READ_CHUNK_BYTES = 65536;
|
|
91
|
-
const fdContentEquals = (fd, expected) => {
|
|
92
|
-
const buf = Buffer.alloc(READ_CHUNK_BYTES);
|
|
93
|
-
let position = 0;
|
|
94
|
-
while (position < expected.length) {
|
|
95
|
-
const want = Math.min(buf.length, expected.length - position);
|
|
96
|
-
const n = readSync(fd, buf, 0, want, position);
|
|
97
|
-
if (n === 0) return false; // truncated below the snapshot length
|
|
98
|
-
if (!buf.subarray(0, n).equals(expected.subarray(position, position + n))) return false;
|
|
99
|
-
position += n;
|
|
100
|
-
}
|
|
101
|
-
return readSync(fd, buf, 0, 1, position) === 0; // any byte here means the store GREW
|
|
102
|
-
};
|
|
103
|
-
|
|
104
|
-
// Trusted only with parsed, valid metadata; anything else is the crash/corruption lane — never
|
|
105
|
-
// probed, never stolen.
|
|
106
|
-
const isValidHolder = (holder) =>
|
|
107
|
-
holder !== null && typeof holder === 'object' && !Array.isArray(holder)
|
|
108
|
-
&& Number.isInteger(holder.pid) && holder.pid > 0
|
|
109
|
-
&& typeof holder.host === 'string' && holder.host.length > 0;
|
|
110
|
-
|
|
111
|
-
const describeHolder = (holder) => `pid ${holder.pid} (host ${holder.host}, started ${holder.startedAt ?? 'unknown'})`;
|
|
112
|
-
|
|
113
|
-
// ESRCH on a same-host signal-0 probe only; a foreign host is unprobeable — never treated as dead.
|
|
114
|
-
const isProvablyDead = (holder) => {
|
|
115
|
-
if (holder.host !== hostname()) return false;
|
|
116
|
-
try {
|
|
117
|
-
process.kill(holder.pid, 0);
|
|
118
|
-
return false;
|
|
119
|
-
} catch (err) {
|
|
120
|
-
return err && err.code === 'ESRCH';
|
|
121
|
-
}
|
|
122
|
-
};
|
|
123
|
-
|
|
124
|
-
// The shared parser accepts any digit string — hundreds of digits parse to Infinity and would
|
|
125
|
-
// erase the wait bound; gated locally because the shared helper feeds the frozen core-evidence.
|
|
126
|
-
const parseLockKnob = (env, name, fallback) => {
|
|
127
|
-
const value = parsePositiveIntKnob(env, name, fallback, stop);
|
|
128
|
-
if (!Number.isSafeInteger(value)) {
|
|
129
|
-
throw stop(`${name} must be a positive safe integer — the provided value overflows (fail closed)`);
|
|
130
|
-
}
|
|
131
|
-
return value;
|
|
132
|
-
};
|
|
133
|
-
|
|
134
|
-
// Containment + canonical pinning, once per append: a symlinked IMMEDIATE parent refuses by name;
|
|
135
|
-
// the ancestor chain is then realpath-rebased so every spelling funnels to ONE physical store+lock
|
|
136
|
-
// pair (refusing ancestor links would break legitimately symlinked prefixes like a distro /home).
|
|
137
|
-
// realpath ENOENT keeps the lexical path — a missing parent still refuses at lock creation.
|
|
138
|
-
const canonicalFlowWritePaths = (resolvedStorePath, lstat) => {
|
|
139
|
-
const parent = dirname(resolvedStorePath);
|
|
140
|
-
if (lstatNoFollow(parent, lstat)?.isSymbolicLink()) {
|
|
141
|
-
throw stop(`${parent} is a symlink — refusing to write the flow store through a symlinked parent (pre-mutation containment)`);
|
|
142
|
-
}
|
|
143
|
-
let canonicalParent;
|
|
144
|
-
try {
|
|
145
|
-
canonicalParent = realpathSync(parent);
|
|
146
|
-
} catch (err) {
|
|
147
|
-
if (err && err.code === 'ENOENT') canonicalParent = parent;
|
|
148
|
-
else throw stop(`cannot canonicalize the flow-store parent dir ${parent} (${(err && err.code) || (err && err.message) || err}) — refusing to write through an unresolvable path (fail closed)`);
|
|
149
|
-
}
|
|
150
|
-
const storePath = join(canonicalParent, basename(resolvedStorePath));
|
|
151
|
-
const lockPath = resolveFlowLockPath(storePath);
|
|
152
|
-
assertRegularOrAbsent(storePath, 'flow store', lstat);
|
|
153
|
-
assertRegularOrAbsent(lockPath, 'flow-store lock', lstat);
|
|
154
|
-
return { storePath, lockPath };
|
|
155
|
-
};
|
|
156
|
-
|
|
157
|
-
// Returns the OWNED canonical { storePath, lockPath, lockFd, lockIdentity }; throws BEFORE
|
|
158
|
-
// ownership on every refusal lane. The caller must reuse exactly these values end-to-end.
|
|
159
|
-
const acquireFlowLock = (resolvedStorePath, env, deps) => {
|
|
160
|
-
const lstat = deps.lstat ?? lstatSync;
|
|
161
|
-
const openLock = deps.openLock ?? ((p) => openSync(p, 'wx'));
|
|
162
|
-
const sleep = deps.sleep ?? sleepSyncMs;
|
|
163
|
-
const now = deps.now ?? monotonicNowMs;
|
|
164
|
-
const waitBoundMs = parseLockKnob(env, 'AW_FLOW_LOCK_WAIT_MS', FLOW_LOCK_WAIT_MS);
|
|
165
|
-
const pollMs = parseLockKnob(env, 'AW_FLOW_LOCK_POLL_MS', FLOW_LOCK_POLL_MS);
|
|
166
|
-
const { storePath, lockPath } = canonicalFlowWritePaths(resolvedStorePath, lstat);
|
|
167
|
-
const holderBody = JSON.stringify({ pid: process.pid, host: hostname(), startedAt: new Date().toISOString() });
|
|
168
|
-
const deadline = now() + waitBoundMs;
|
|
169
|
-
// Every retry lane passes this gate — else lock churn extends the wait past the bound forever.
|
|
170
|
-
const refuseIfPastDeadline = (why) => {
|
|
171
|
-
if (now() >= deadline) {
|
|
172
|
-
throw stop(`the flow-store lock ${lockPath} could not be acquired within the ${waitBoundMs}ms wait (${why}) — retry, or raise AW_FLOW_LOCK_WAIT_MS`);
|
|
173
|
-
}
|
|
174
|
-
};
|
|
175
|
-
for (;;) {
|
|
176
|
-
// CAS: exclusive-create ('wx' also refuses a symlink leaf); the winning fd stamps the holder
|
|
177
|
-
// and yields the lock's {dev, ino} — a pathname stat could already see a replacement.
|
|
178
|
-
let fd = null;
|
|
179
|
-
try {
|
|
180
|
-
fd = openLock(lockPath);
|
|
181
|
-
} catch (err) {
|
|
182
|
-
if (!err || err.code !== 'EEXIST') {
|
|
183
|
-
throw stop(`cannot create the flow-store lock ${lockPath} (${(err && err.code) || (err && err.message) || err}) — the store's parent dir must exist and be writable`);
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
if (fd !== null) {
|
|
187
|
-
let won = false;
|
|
188
|
-
try {
|
|
189
|
-
writeSync(fd, holderBody);
|
|
190
|
-
const st = fstatSync(fd);
|
|
191
|
-
won = true;
|
|
192
|
-
// The fd stays open through the whole append — its inode cannot be recycled under us.
|
|
193
|
-
return { storePath, lockPath, lockFd: fd, lockIdentity: { dev: st.dev, ino: st.ino } };
|
|
194
|
-
} catch (err) {
|
|
195
|
-
// Without the fd-proven identity, removing the pathname would be an unproven-ownership rm.
|
|
196
|
-
throw stop(`cannot stamp or verify the just-created flow-store lock ${lockPath} (${(err && err.code) || (err && err.message) || err}) — the lock file is left in place; inspect it, then remove it by hand: rm -- ${shellQuotePath(lockPath)} (fail closed)`);
|
|
197
|
-
} finally {
|
|
198
|
-
if (!won) { try { closeSync(fd); } catch { /* the stamp failure above already decided the lane */ } }
|
|
199
|
-
}
|
|
200
|
-
}
|
|
201
|
-
// The holder read HOLDS its fd (keepFd) until the lane decides: while the fd is open the
|
|
202
|
-
// inode cannot be recycled, so the DEAD re-verify below can trust an identity match only
|
|
203
|
-
// together with the held inode still being linked (FLOW-LOCK-HOLDER-FD-RECHECK). The lane
|
|
204
|
-
// verdict is computed FIRST (its error captured), the held fd then closes unconditionally,
|
|
205
|
-
// and a close failure is a typed STOP — thrown alone, or preserved on the primary error as
|
|
206
|
-
// holderCloseFailure (the releaseFlowLock never-mask discipline; P28).
|
|
207
|
-
const holderIo = deps.holderIo ?? {};
|
|
208
|
-
// The read itself is wrapped: on the early error/foreign lanes the reader closes its own fd
|
|
209
|
-
// in a finally, and a close throw there would otherwise escape as a RAW error outside the
|
|
210
|
-
// typed-STOP guarantee.
|
|
211
|
-
let holderRead;
|
|
212
|
-
try {
|
|
213
|
-
holderRead = readRegularFileNoFollow(lockPath, { ...holderIo, keepFd: true });
|
|
214
|
-
} catch (err) {
|
|
215
|
-
throw stop(`cannot read the flow-store lock holder (${(err && err.code) || (err && err.message) || err}) — the read/close custody failed (fail closed)`);
|
|
216
|
-
}
|
|
217
|
-
if (holderRead.closeFailure !== undefined) {
|
|
218
|
-
throw stop(`cannot read the flow-store lock holder (${holderRead.closeFailure}) — the read/close custody failed (fail closed)`);
|
|
219
|
-
}
|
|
220
|
-
const holderFd = holderRead.outcome === 'ok' ? holderRead.fd : null;
|
|
221
|
-
let verdict = null;
|
|
222
|
-
let primary = null;
|
|
223
|
-
try {
|
|
224
|
-
verdict = (() => {
|
|
225
|
-
if (holderRead.outcome === 'absent') {
|
|
226
|
-
refuseIfPastDeadline('the lock kept appearing and vanishing (churn)');
|
|
227
|
-
return { retry: true }; // released between attempts — retry the CAS at once
|
|
228
|
-
}
|
|
229
|
-
if (holderRead.outcome === 'foreign') throw foreignObjectStop('flow-store lock', lockPath, holderRead.className, holderRead.isDirectory);
|
|
230
|
-
let holder = null;
|
|
231
|
-
if (holderRead.outcome === 'ok') {
|
|
232
|
-
try {
|
|
233
|
-
holder = JSON.parse(holderRead.content);
|
|
234
|
-
} catch { holder = null; }
|
|
235
|
-
}
|
|
236
|
-
const validHolder = isValidHolder(holder);
|
|
237
|
-
if (validHolder && isProvablyDead(holder)) {
|
|
238
|
-
// The DEAD verdict binds to the inode the holder was read from — a lock released or
|
|
239
|
-
// replaced since then means the observed holder is gone: retry, never refuse a
|
|
240
|
-
// vanished lock.
|
|
241
|
-
let lockNow = null;
|
|
242
|
-
try {
|
|
243
|
-
lockNow = lstatNoFollow(lockPath, lstat); // null ONLY on a true ENOENT
|
|
244
|
-
} catch (err) {
|
|
245
|
-
throw stop(`cannot re-verify the flow-store lock identity before the DEAD refusal (${(err && err.code) || (err && err.message) || err}) — refusing to guess (fail closed)`);
|
|
246
|
-
}
|
|
247
|
-
if (lockNow == null || lockNow.dev !== holderRead.dev || lockNow.ino !== holderRead.ino) {
|
|
248
|
-
refuseIfPastDeadline('the observed dead holder was released (churn)');
|
|
249
|
-
return { retry: true };
|
|
250
|
-
}
|
|
251
|
-
// A pathname identity match alone can be a recycled lie (release + re-create landing
|
|
252
|
-
// the same {dev, ino}); the held fd settles it — an unlinked held inode (nlink 0)
|
|
253
|
-
// proves the observed holder's lock is GONE, whatever the pathname claims.
|
|
254
|
-
let heldNow;
|
|
255
|
-
try {
|
|
256
|
-
heldNow = (holderIo.fstat ?? fstatSync)(holderFd);
|
|
257
|
-
} catch (err) {
|
|
258
|
-
throw stop(`cannot re-verify the flow-store lock through its held descriptor (${(err && err.code) || (err && err.message) || err}) — refusing to guess (fail closed)`);
|
|
259
|
-
}
|
|
260
|
-
if (heldNow.nlink === 0) {
|
|
261
|
-
refuseIfPastDeadline('the observed dead holder was released (churn)');
|
|
262
|
-
return { retry: true };
|
|
263
|
-
}
|
|
264
|
-
throw stop(`the flow-store lock ${lockPath} is held by a DEAD process (${describeHolder(holder)}) — a crashed appender left it behind. To recover: inspect it, then remove it by hand: rm -- ${shellQuotePath(lockPath)} — it is never stolen silently (a steal could tear a live append; fail closed)`);
|
|
265
|
-
}
|
|
266
|
-
// ONE observation drives the deadline check AND the sleep cap — no overshoot by a full poll.
|
|
267
|
-
const observedAt = now();
|
|
268
|
-
if (observedAt >= deadline) {
|
|
269
|
-
if (!validHolder) {
|
|
270
|
-
throw stop(`the flow-store lock ${lockPath} carries an UNREADABLE or malformed holder after the full ${waitBoundMs}ms wait — a crashed appender may have died before writing its holder line, or the file is corrupted. To recover: inspect it, then remove it by hand: rm -- ${shellQuotePath(lockPath)} — it is never stolen silently (fail closed)`);
|
|
271
|
-
}
|
|
272
|
-
if (holder.host !== hostname()) {
|
|
273
|
-
throw stop(`the flow-store lock ${lockPath} is still held by pid ${holder.pid} on host ${holder.host} (liveness unprobeable from ${hostname()}) after the full ${waitBoundMs}ms wait — retry after that holder finishes, or raise AW_FLOW_LOCK_WAIT_MS`);
|
|
274
|
-
}
|
|
275
|
-
throw stop(`the flow-store lock ${lockPath} is still held by ${describeHolder(holder)} after the full ${waitBoundMs}ms wait — retry after the holder finishes, or raise AW_FLOW_LOCK_WAIT_MS`);
|
|
276
|
-
}
|
|
277
|
-
return { sleepMs: Math.min(pollMs, deadline - observedAt) };
|
|
278
|
-
})();
|
|
279
|
-
} catch (err) {
|
|
280
|
-
primary = err;
|
|
281
|
-
}
|
|
282
|
-
if (holderFd !== null) {
|
|
283
|
-
try {
|
|
284
|
-
(holderIo.close ?? closeSync)(holderFd);
|
|
285
|
-
} catch (err) {
|
|
286
|
-
const closeStop = stop(`cannot close the held flow-store holder descriptor (${(err && err.code) || (err && err.message) || err}) — the fd-custody guarantee is violated (fail closed)`);
|
|
287
|
-
if (primary == null) primary = closeStop;
|
|
288
|
-
else primary.holderCloseFailure = closeStop.message;
|
|
289
|
-
}
|
|
290
|
-
}
|
|
291
|
-
if (primary != null) throw primary;
|
|
292
|
-
if (verdict.retry) continue;
|
|
293
|
-
sleep(verdict.sleepMs);
|
|
294
|
-
}
|
|
295
|
-
};
|
|
72
|
+
// ── the shared append lane (D12) ──────────────────────────────────────────────────────────────────
|
|
73
|
+
|
|
74
|
+
// The lock/CAS discipline, the fd-custody rules and the serialized append are the EXTRACTION of
|
|
75
|
+
// exactly this module's former code into store-append.mjs, so behavior is unchanged by
|
|
76
|
+
// construction: this store injects its nouns (every refusal still names the flow store), its env
|
|
77
|
+
// seam and knob names, its typed-STOP factory, its record validator, its store-text parser, and
|
|
78
|
+
// the SEMANTIC preflight below. The flow suites are the characterization bar for that claim.
|
|
79
|
+
const flowAppendLane = createStoreAppendLane({
|
|
80
|
+
nouns: { store: 'flow store', adj: 'flow-store', record: 'flow record' },
|
|
81
|
+
envNames: { store: 'AW_FLOW_STORE', waitKnob: 'AW_FLOW_LOCK_WAIT_MS', pollKnob: 'AW_FLOW_LOCK_POLL_MS' },
|
|
82
|
+
stop,
|
|
83
|
+
resolveStorePath: resolveFlowStorePath,
|
|
84
|
+
resolveLockPath: resolveFlowLockPath,
|
|
85
|
+
validateRecord: validateFlowRecord,
|
|
86
|
+
parseStoreText: parseFlowStoreText,
|
|
87
|
+
lockWaitMs: FLOW_LOCK_WAIT_MS,
|
|
88
|
+
lockPollMs: FLOW_LOCK_POLL_MS,
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
const captureRecordSnapshot = flowAppendLane.captureRecordSnapshot;
|
|
296
92
|
|
|
297
93
|
// ── the ONE append (validated, semantic-preflighted, lock-serialized, atomic) ─────────────────────
|
|
298
94
|
|
|
299
|
-
// ONE custody-checked release: only the inode the winning fd proved is ever removed (the fd is
|
|
300
|
-
// still open, so a pathname {dev, ino} match is proof of the same file); absent or replaced =
|
|
301
|
-
// a mutual-exclusion violation, the foreign lock stays. Closes the fd on EVERY outcome without
|
|
302
|
-
// losing a close failure. Returns a typed STOP or null, never throws — the caller sequences it
|
|
303
|
-
// after the body's own error so neither masks the other.
|
|
304
|
-
const releaseFlowLock = (lockPath, lockFd, lockIdentity, deps) => {
|
|
305
|
-
const lstat = deps.lstat ?? lstatSync;
|
|
306
|
-
const rm = deps.rm ?? ((p) => rmSync(p, { force: true }));
|
|
307
|
-
const close = deps.close ?? closeSync;
|
|
308
|
-
let issue = null;
|
|
309
|
-
let st = null;
|
|
310
|
-
try {
|
|
311
|
-
st = lstatNoFollow(lockPath, lstat); // null ONLY on a true ENOENT
|
|
312
|
-
} catch (err) {
|
|
313
|
-
issue = stop(`cannot verify the flow-store lock before release (${(err && err.code) || (err && err.message) || err}) — the lock is left in place; inspect ${lockPath} (fail closed)`);
|
|
314
|
-
}
|
|
315
|
-
if (issue == null) {
|
|
316
|
-
if (st == null || st.dev !== lockIdentity.dev || st.ino !== lockIdentity.ino) {
|
|
317
|
-
issue = stop(`the flow-store lock ${lockPath} was removed or replaced under this append — mutual exclusion was violated and another appender may have run concurrently; the current lock (if any) is left untouched; inspect the store and the lock (fail closed)`);
|
|
318
|
-
} else {
|
|
319
|
-
try {
|
|
320
|
-
rm(lockPath);
|
|
321
|
-
} catch (err) {
|
|
322
|
-
issue = stop(`cannot remove the flow-store lock at release (${(err && err.code) || (err && err.message) || err}) — inspect ${lockPath} (fail closed)`);
|
|
323
|
-
}
|
|
324
|
-
}
|
|
325
|
-
}
|
|
326
|
-
try {
|
|
327
|
-
close(lockFd);
|
|
328
|
-
} catch (err) {
|
|
329
|
-
const closeStop = stop(`cannot close the flow-store lock descriptor at release (${(err && err.code) || (err && err.message) || err})`);
|
|
330
|
-
if (issue == null) issue = closeStop;
|
|
331
|
-
else issue.closeFailure = closeStop.message;
|
|
332
|
-
}
|
|
333
|
-
return issue;
|
|
334
|
-
};
|
|
335
|
-
|
|
336
95
|
// The store path is always RESOLVED (cwd/env), never caller-supplied — a raw path param would
|
|
337
96
|
// bypass the absolute-normalization door the AW_FLOW_STORE seam enforces. Read, write, and unlock
|
|
338
97
|
// all use the CANONICAL pair acquire returned — nothing is re-derived mid-append.
|
|
@@ -344,7 +103,7 @@ export const appendFlowRecord = ({ cwd = process.cwd(), record, env = process.en
|
|
|
344
103
|
if (snapshot.kind === 'subset-attempt') {
|
|
345
104
|
throw stop('subset-attempt records are minted ONLY by the locked append factory (appendSubsetAttempt) — a hand-built record could forge a fresh counting context and bypass the hard-stop budget (fail closed)');
|
|
346
105
|
}
|
|
347
|
-
return
|
|
106
|
+
return flowAppendLane.appendResolvedRecord({ cwd, env, deps, preflight: flowSemanticPreflight, makeRecord: () => ({ line, snapshot }) });
|
|
348
107
|
};
|
|
349
108
|
|
|
350
109
|
// appendFlowRecordWithPreflight — the generic lane plus a caller `preflight(records)` hook that
|
|
@@ -360,7 +119,7 @@ export const appendFlowRecordWithPreflight = ({ cwd = process.cwd(), record, env
|
|
|
360
119
|
if (snapshot.kind === 'subset-attempt') {
|
|
361
120
|
throw stop('subset-attempt records are minted ONLY by the locked append factory (appendSubsetAttempt) — a hand-built record could forge a fresh counting context and bypass the hard-stop budget (fail closed)');
|
|
362
121
|
}
|
|
363
|
-
return
|
|
122
|
+
return flowAppendLane.appendResolvedRecord({ cwd, env, deps, preflight: flowSemanticPreflight, makeRecord: (records) => {
|
|
364
123
|
if (preflight != null) preflight(deepFreezeClone(records));
|
|
365
124
|
return { line, snapshot };
|
|
366
125
|
} });
|
|
@@ -377,184 +136,80 @@ const deepFreezeClone = (value) => {
|
|
|
377
136
|
return freeze(structuredClone(value));
|
|
378
137
|
};
|
|
379
138
|
|
|
380
|
-
//
|
|
381
|
-
//
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
};
|
|
395
|
-
|
|
396
|
-
//
|
|
397
|
-
//
|
|
398
|
-
//
|
|
399
|
-
|
|
400
|
-
const
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
const body = appendUnderLock({ storePath, makeRecord, deps });
|
|
407
|
-
const releaseIssue = releaseFlowLock(lockPath, lockFd, lockIdentity, deps);
|
|
408
|
-
if (body.err) {
|
|
409
|
-
if (releaseIssue) {
|
|
410
|
-
body.err.releaseViolation = releaseIssue.message;
|
|
411
|
-
if (releaseIssue.closeFailure) body.err.releaseCloseFailure = releaseIssue.closeFailure;
|
|
412
|
-
}
|
|
413
|
-
throw body.err;
|
|
414
|
-
}
|
|
415
|
-
if (releaseIssue) throw releaseIssue;
|
|
416
|
-
return body.value;
|
|
417
|
-
};
|
|
418
|
-
|
|
419
|
-
// Captured-result shape ({ value } | { err }) — never throws past the caller, so release always
|
|
420
|
-
// runs. The snapshot fd is held until after the final rename and closed on every exit lane.
|
|
421
|
-
const appendUnderLock = ({ storePath, makeRecord, deps }) => {
|
|
422
|
-
let snapshotFd = null;
|
|
423
|
-
try {
|
|
424
|
-
const storeRead = readRegularFileNoFollow(storePath, { keepFd: true });
|
|
425
|
-
if (storeRead.outcome === 'ok') snapshotFd = storeRead.fd;
|
|
426
|
-
if (storeRead.outcome === 'foreign') throw foreignObjectStop('flow store', storePath, storeRead.className, storeRead.isDirectory);
|
|
427
|
-
if (storeRead.outcome === 'error') throw stop(`cannot read the flow store before appending (${storeRead.code}) — refusing to overwrite it (fail closed)`);
|
|
428
|
-
// A second hard-link path would derive its OWN lock and the two appends would race one inode.
|
|
429
|
-
if (storeRead.outcome === 'ok' && storeRead.nlink !== 1) {
|
|
430
|
-
throw stop(`the flow store ${storePath} has ${storeRead.nlink} hard links — two path-derived locks would race one inode; remove the extra links and retry (fail closed)`);
|
|
431
|
-
}
|
|
432
|
-
const existing = storeRead.outcome === 'absent' ? '' : storeRead.content;
|
|
433
|
-
const parsed = parseFlowStoreText(existing);
|
|
434
|
-
if (parsed.malformed > 0) {
|
|
435
|
-
throw stop(`refusing to append to a flow store carrying ${parsed.malformed} malformed line(s) (${parsed.malformedReasons[0]}) — inspect ${storePath}; nothing was written (fail closed)`);
|
|
436
|
-
}
|
|
437
|
-
const { line, snapshot } = makeRecord(parsed.records);
|
|
438
|
-
if (existing.split('\n').some((l) => l === line)) {
|
|
439
|
-
throw stop('refusing a byte-identical replayed line (duplicate) — a genuine new record carries new content or timestamp; nothing was written');
|
|
440
|
-
}
|
|
441
|
-
if (snapshot.kind === CHAIN_KIND) {
|
|
442
|
-
const chain = parsed.records.filter((r) => r.kind === CHAIN_KIND && r.planId === snapshot.planId);
|
|
443
|
-
const existingSeq = validateChainSequence(chain);
|
|
444
|
-
if (!existingSeq.ok) {
|
|
445
|
-
throw stop(`refusing to append to a flow store whose existing chain for plan "${snapshot.planId}" is already illegal (${existingSeq.reason}) — inspect ${storePath}; nothing was written (fail closed)`);
|
|
139
|
+
// The SEMANTIC half of the append, handed to the shared lane and run by it INSIDE the critical
|
|
140
|
+
// section on the LOCKED store snapshot (a writer's lock-free walk is advisory — only the locked
|
|
141
|
+
// snapshot decides): per-kind chain legality, reference resolution, the closure rules, the
|
|
142
|
+
// counting-context gate, and supersession legality. An illegal record never lands. Throws a typed
|
|
143
|
+
// STOP; the lane releases the lock and re-throws.
|
|
144
|
+
const flowSemanticPreflight = ({ records, snapshot, storePath }) => {
|
|
145
|
+
if (snapshot.kind === CHAIN_KIND) {
|
|
146
|
+
const chain = records.filter((r) => r.kind === CHAIN_KIND && r.planId === snapshot.planId);
|
|
147
|
+
const existingSeq = validateChainSequence(chain);
|
|
148
|
+
if (!existingSeq.ok) {
|
|
149
|
+
throw stop(`refusing to append to a flow store whose existing chain for plan "${snapshot.planId}" is already illegal (${existingSeq.reason}) — inspect ${storePath}; nothing was written (fail closed)`);
|
|
150
|
+
}
|
|
151
|
+
const candidateSeq = validateChainSequence([...chain, snapshot]);
|
|
152
|
+
if (!candidateSeq.ok) {
|
|
153
|
+
throw stop(`refusing an illegal chain record: ${candidateSeq.reason} — the append-only store never absorbs a record that permanently reddens the checker; nothing was written`);
|
|
154
|
+
}
|
|
155
|
+
// Reference RESOLUTION (#63) on top of the structural half above: a step-OPENING round must
|
|
156
|
+
// digest-reference the chain's prior terminal; a round REVISION re-states its reference
|
|
157
|
+
// byte-bound (validateRoundRevision), so it is never re-classified against a moved terminal.
|
|
158
|
+
if (snapshot.purpose === 'round' && snapshot.opensFrom !== null && walkChainState(chain).mode === 'boundary') {
|
|
159
|
+
const ref = validateOpenerReference(records, snapshot);
|
|
160
|
+
if (!ref.ok) throw stop(`refusing a step-opening round: ${ref.reason} — nothing was written`);
|
|
161
|
+
}
|
|
162
|
+
if (snapshot.purpose === 'refresh') {
|
|
163
|
+
if (resolveRecordReference(records, snapshot.refreshedRecord) === undefined) {
|
|
164
|
+
throw stop(`refusing a refresh whose refreshedRecord does not match the store (no record digests to ${snapshot.refreshedRecord.slice(0, 12)}…) — a re-attestation binds an existing record; nothing was written`);
|
|
446
165
|
}
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
throw stop(`refusing an illegal chain record: ${candidateSeq.reason} — the append-only store never absorbs a record that permanently reddens the checker; nothing was written`);
|
|
450
|
-
}
|
|
451
|
-
// Reference RESOLUTION (#63) on top of the structural half above: a step-OPENING round must
|
|
452
|
-
// digest-reference the chain's prior terminal; a round REVISION re-states its reference
|
|
453
|
-
// byte-bound (validateRoundRevision), so it is never re-classified against a moved terminal.
|
|
454
|
-
if (snapshot.purpose === 'round' && snapshot.opensFrom !== null && walkChainState(chain).mode === 'boundary') {
|
|
455
|
-
const ref = validateOpenerReference(parsed.records, snapshot);
|
|
456
|
-
if (!ref.ok) throw stop(`refusing a step-opening round: ${ref.reason} — nothing was written`);
|
|
457
|
-
}
|
|
458
|
-
if (snapshot.purpose === 'refresh') {
|
|
459
|
-
if (resolveRecordReference(parsed.records, snapshot.refreshedRecord) === undefined) {
|
|
460
|
-
throw stop(`refusing a refresh whose refreshedRecord does not match the store (no record digests to ${snapshot.refreshedRecord.slice(0, 12)}…) — a re-attestation binds an existing record; nothing was written`);
|
|
461
|
-
}
|
|
462
|
-
if (!isAuthoritativeReferenceTarget(parsed.records, snapshot.refreshedRecord)) {
|
|
463
|
-
throw stop('refusing a refresh whose refreshedRecord targets a superseded record — a re-attestation binds the authoritative latest record of its key; nothing was written');
|
|
464
|
-
}
|
|
465
|
-
}
|
|
466
|
-
}
|
|
467
|
-
// The closure rule runs UNDER the lock on the captured snapshot — a writer's lock-free
|
|
468
|
-
// usability pre-check can race a concurrent up/clear, and a justification minted after its
|
|
469
|
-
// mark closed can never satisfy the decide layer (#25), so the store refuses to strand it.
|
|
470
|
-
if (snapshot.kind === 'degrade-justification') {
|
|
471
|
-
const closed = parsed.records.some((r) => (r.kind === 'down-mark-up' || r.kind === 'down-mark-clear') && r.target === snapshot.downMark);
|
|
472
|
-
if (closed) {
|
|
473
|
-
throw stop('refusing a degrade-justification whose down-mark is already closed by up/clear — minted-after-close can never satisfy (#25); nothing was written');
|
|
166
|
+
if (!isAuthoritativeReferenceTarget(records, snapshot.refreshedRecord)) {
|
|
167
|
+
throw stop('refusing a refresh whose refreshedRecord targets a superseded record — a re-attestation binds the authoritative latest record of its key; nothing was written');
|
|
474
168
|
}
|
|
475
169
|
}
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
if (!seq.ok) {
|
|
485
|
-
throw stop(`refusing a consult-attestation: the plan "${snapshot.planId}" chain is not a legal open carrier under the lock (${seq.reason}); nothing was written`);
|
|
486
|
-
}
|
|
487
|
-
const state = walkChainState(chain);
|
|
488
|
-
const open = state.mode === 'in-step' && !state.parked && !state.completed;
|
|
489
|
-
if (!open || state.stepId !== snapshot.stepId || state.cycle !== snapshot.cycle || state.round !== snapshot.round) {
|
|
490
|
-
const shown = !open
|
|
491
|
-
? (state.completed ? 'the plan is completed' : state.parked ? 'the plan is parked' : 'no step is open')
|
|
492
|
-
: `the open step is "${state.stepId}" (cycle ${state.cycle}, round ${state.round})`;
|
|
493
|
-
throw stop(`refusing a consult-attestation whose {cycle, stepId, round} does not match the OPEN step under the lock — ${shown}; a consult binds the open step's round, and a stale context can never satisfy; nothing was written`);
|
|
494
|
-
}
|
|
495
|
-
}
|
|
496
|
-
// The Decision-7/8 counting-context gate runs UNDER the lock for BOTH append lanes (the
|
|
497
|
-
// factory computes a passing record; a hand-built one must satisfy the same rules).
|
|
498
|
-
if (snapshot.kind === 'subset-attempt') {
|
|
499
|
-
const gate = subsetAttemptGate(parsed.records, snapshot);
|
|
500
|
-
if (!gate.ok) throw stop(`refusing a subset-attempt: ${gate.reason} — nothing was written`);
|
|
501
|
-
}
|
|
502
|
-
const existingSup = validateSupersessions(parsed.records);
|
|
503
|
-
if (!existingSup.ok) {
|
|
504
|
-
throw stop(`refusing to append to a flow store whose existing records already violate supersession legality (${existingSup.reason}) — inspect ${storePath}; nothing was written (fail closed)`);
|
|
170
|
+
}
|
|
171
|
+
// The closure rule runs UNDER the lock on the captured snapshot — a writer's lock-free
|
|
172
|
+
// usability pre-check can race a concurrent up/clear, and a justification minted after its
|
|
173
|
+
// mark closed can never satisfy the decide layer (#25), so the store refuses to strand it.
|
|
174
|
+
if (snapshot.kind === 'degrade-justification') {
|
|
175
|
+
const closed = records.some((r) => (r.kind === 'down-mark-up' || r.kind === 'down-mark-clear') && r.target === snapshot.downMark);
|
|
176
|
+
if (closed) {
|
|
177
|
+
throw stop('refusing a degrade-justification whose down-mark is already closed by up/clear — minted-after-close can never satisfy (#25); nothing was written');
|
|
505
178
|
}
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
179
|
+
}
|
|
180
|
+
// The same P3-26 discipline for the consult-attestation (Phase-4): the writer derives
|
|
181
|
+
// {cycle, stepId, round} lock-free, so a concurrent converged/park/complete can close or move
|
|
182
|
+
// the step first — under the lock the named plan's chain must be LEGAL and hold an OPEN step
|
|
183
|
+
// (in-step, not parked, not completed) whose {cycle, stepId, round} EQUALS the record's; a
|
|
184
|
+
// stale consult context can never satisfy the decide layer, so the store refuses to strand it.
|
|
185
|
+
if (snapshot.kind === 'consult-attestation') {
|
|
186
|
+
const chain = records.filter((r) => r.kind === CHAIN_KIND && r.planId === snapshot.planId);
|
|
187
|
+
const seq = chain.length === 0 ? { ok: false, reason: 'no chain exists for that plan' } : validateChainSequence(chain);
|
|
188
|
+
if (!seq.ok) {
|
|
189
|
+
throw stop(`refusing a consult-attestation: the plan "${snapshot.planId}" chain is not a legal open carrier under the lock (${seq.reason}); nothing was written`);
|
|
509
190
|
}
|
|
510
|
-
const
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
if (to === storePath) {
|
|
518
|
-
if (storeRead.outcome === 'ok') {
|
|
519
|
-
let same;
|
|
520
|
-
try {
|
|
521
|
-
same = fdContentEquals(snapshotFd, storeRead.bytes);
|
|
522
|
-
} catch (err) {
|
|
523
|
-
throw stop(`cannot re-read the flow store snapshot before the final rename (${(err && err.code) || (err && err.message) || err}) — nothing was written (fail closed)`);
|
|
524
|
-
}
|
|
525
|
-
if (!same) {
|
|
526
|
-
throw stop(`the flow store ${storePath} content changed under the lock (same-inode in-place mutation) — refusing the final rename; nothing was written (fail closed)`);
|
|
527
|
-
}
|
|
528
|
-
}
|
|
529
|
-
let leaf = null;
|
|
530
|
-
try {
|
|
531
|
-
leaf = lstatNoFollow(to, deps.lstat ?? lstatSync);
|
|
532
|
-
} catch (err) {
|
|
533
|
-
throw stop(`cannot verify the flow store leaf before the final rename (${(err && err.code) || (err && err.message) || err}) — nothing was written (fail closed)`);
|
|
534
|
-
}
|
|
535
|
-
const identityHeld = storeRead.outcome === 'absent'
|
|
536
|
-
? leaf == null
|
|
537
|
-
: leaf != null && leaf.isFile() && leaf.dev === storeRead.dev && leaf.ino === storeRead.ino;
|
|
538
|
-
if (!identityHeld) {
|
|
539
|
-
throw stop(`the flow store ${storePath} changed identity under the lock (concurrent or foreign mutation) — refusing the final rename; nothing was written (fail closed)`);
|
|
540
|
-
}
|
|
541
|
-
if (leaf != null && leaf.nlink !== 1) {
|
|
542
|
-
throw stop(`the flow store ${storePath} has ${leaf.nlink} hard links — two path-derived locks would race one inode; remove the extra links and retry (fail closed)`);
|
|
543
|
-
}
|
|
544
|
-
}
|
|
545
|
-
return renameBase(from, to);
|
|
546
|
-
};
|
|
547
|
-
writeContainedFileAtomic(dirname(storePath), storePath, `${prefix}${line}\n`, { ...deps, rename: guardedRename }, { stop, label: storePath });
|
|
548
|
-
if (snapshotFd !== null) {
|
|
549
|
-
const fd = snapshotFd;
|
|
550
|
-
snapshotFd = null;
|
|
551
|
-
closeSync(fd); // a success-lane close failure surfaces as the append's own error
|
|
191
|
+
const state = walkChainState(chain);
|
|
192
|
+
const open = state.mode === 'in-step' && !state.parked && !state.completed;
|
|
193
|
+
if (!open || state.stepId !== snapshot.stepId || state.cycle !== snapshot.cycle || state.round !== snapshot.round) {
|
|
194
|
+
const shown = !open
|
|
195
|
+
? (state.completed ? 'the plan is completed' : state.parked ? 'the plan is parked' : 'no step is open')
|
|
196
|
+
: `the open step is "${state.stepId}" (cycle ${state.cycle}, round ${state.round})`;
|
|
197
|
+
throw stop(`refusing a consult-attestation whose {cycle, stepId, round} does not match the OPEN step under the lock — ${shown}; a consult binds the open step's round, and a stale context can never satisfy; nothing was written`);
|
|
552
198
|
}
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
199
|
+
}
|
|
200
|
+
// The Decision-7/8 counting-context gate runs UNDER the lock for BOTH append lanes (the
|
|
201
|
+
// factory computes a passing record; a hand-built one must satisfy the same rules).
|
|
202
|
+
if (snapshot.kind === 'subset-attempt') {
|
|
203
|
+
const gate = subsetAttemptGate(records, snapshot);
|
|
204
|
+
if (!gate.ok) throw stop(`refusing a subset-attempt: ${gate.reason} — nothing was written`);
|
|
205
|
+
}
|
|
206
|
+
const existingSup = validateSupersessions(records);
|
|
207
|
+
if (!existingSup.ok) {
|
|
208
|
+
throw stop(`refusing to append to a flow store whose existing records already violate supersession legality (${existingSup.reason}) — inspect ${storePath}; nothing was written (fail closed)`);
|
|
209
|
+
}
|
|
210
|
+
const candidateSup = validateSupersessions([...records, snapshot]);
|
|
211
|
+
if (!candidateSup.ok) {
|
|
212
|
+
throw stop(`refusing an illegal supersession: ${candidateSup.reason} — the append-only store never absorbs a record that permanently reddens the checker; nothing was written`);
|
|
558
213
|
}
|
|
559
214
|
};
|
|
560
215
|
|
|
@@ -643,12 +298,9 @@ const subsetAttemptGate = (records, snapshot) => {
|
|
|
643
298
|
export const SUBSET_RUN_LOCK_INFIX = '.subset-run';
|
|
644
299
|
|
|
645
300
|
export const acquireSubsetRunLock = ({ cwd = process.cwd(), env = process.env, deps = {} } = {}) => {
|
|
646
|
-
const resolved =
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
}
|
|
650
|
-
const { lockPath, lockFd, lockIdentity } = acquireFlowLock(`${resolved}${SUBSET_RUN_LOCK_INFIX}`, env, deps);
|
|
651
|
-
return { lockPath, release: () => releaseFlowLock(lockPath, lockFd, lockIdentity, deps) };
|
|
301
|
+
const resolved = flowAppendLane.resolveOrStop(cwd, env, 'serialize a subset run against');
|
|
302
|
+
const { lockPath, lockFd, lockIdentity } = flowAppendLane.acquireLock(`${resolved}${SUBSET_RUN_LOCK_INFIX}`, env, deps);
|
|
303
|
+
return { lockPath, release: () => flowAppendLane.releaseLock(lockPath, lockFd, lockIdentity, deps) };
|
|
652
304
|
};
|
|
653
305
|
|
|
654
306
|
// The pre-gate append-lock readiness probe (round-8 fold): acquire and immediately release the
|
|
@@ -657,12 +309,9 @@ export const acquireSubsetRunLock = ({ cwd = process.cwd(), env = process.env, d
|
|
|
657
309
|
// Stated residual: a lock landing between this probe and the post-run append still refuses at
|
|
658
310
|
// append time — closing that would mean holding the append lock across the whole gate run.
|
|
659
311
|
export const probeFlowAppendLock = ({ cwd = process.cwd(), env = process.env, deps = {} } = {}) => {
|
|
660
|
-
const resolved =
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
}
|
|
664
|
-
const { lockPath, lockFd, lockIdentity } = acquireFlowLock(resolved, env, deps);
|
|
665
|
-
const issue = releaseFlowLock(lockPath, lockFd, lockIdentity, deps);
|
|
312
|
+
const resolved = flowAppendLane.resolveOrStop(cwd, env, 'probe');
|
|
313
|
+
const { lockPath, lockFd, lockIdentity } = flowAppendLane.acquireLock(resolved, env, deps);
|
|
314
|
+
const issue = flowAppendLane.releaseLock(lockPath, lockFd, lockIdentity, deps);
|
|
666
315
|
if (issue != null) throw issue;
|
|
667
316
|
};
|
|
668
317
|
|
|
@@ -703,7 +352,7 @@ export const appendSubsetAttempt = ({ cwd = process.cwd(), env = process.env, de
|
|
|
703
352
|
// reach the digest domain.
|
|
704
353
|
const subsetIds = Object.freeze([...derived]);
|
|
705
354
|
let minted = null;
|
|
706
|
-
const value =
|
|
355
|
+
const value = flowAppendLane.appendResolvedRecord({ cwd, env, deps, preflight: flowSemanticPreflight, makeRecord: (records) => {
|
|
707
356
|
const chain = records.filter((r) => r.kind === CHAIN_KIND && r.planId === expected.planId);
|
|
708
357
|
if (chain.length === 0) throw stop(`no chain exists for plan "${expected.planId}" under the lock — the captured identity is stale; re-run the subset under the current context (fail closed)`);
|
|
709
358
|
const seq = validateChainSequence(chain);
|