@mstar-harness/engine 3.9.2 → 3.9.4

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/dist/audit.js CHANGED
@@ -1,53 +1,90 @@
1
1
  // src/audit.ts
2
2
  import { execFileSync as execFileSync2 } from "node:child_process";
3
- import { existsSync as existsSync5, mkdirSync as mkdirSync4, readdirSync as readdirSync4, readFileSync as readFileSync5, rmdirSync as rmdirSync2, rmSync, writeFileSync as writeFileSync4 } from "node:fs";
4
- import { basename as basename3, join as join7, resolve as resolve7, sep as sep2 } from "node:path";
5
-
6
- // src/core.ts
7
- import { existsSync, mkdirSync, readFileSync, renameSync, unlinkSync, writeFileSync } from "node:fs";
8
- import { randomUUID } from "node:crypto";
9
- import { basename, dirname, join, resolve } from "node:path";
10
- function readJson(filePath) {
11
- if (!existsSync(filePath))
12
- return {};
13
- const content = readFileSync(filePath, "utf8").trim();
14
- if (!content)
15
- return {};
16
- try {
17
- return JSON.parse(content);
18
- } catch (error) {
19
- throw new Error(`Invalid JSON in ${filePath}: ${error.message}`);
20
- }
21
- }
22
- function writeJson(filePath, value) {
23
- const parent = dirname(filePath);
24
- mkdirSync(parent, { recursive: true });
25
- const tmp = join(parent, `.${basename(filePath)}.${process.pid}.${randomUUID()}.tmp`);
26
- try {
27
- writeFileSync(tmp, `${JSON.stringify(value, null, 2)}
28
- `, "utf8");
29
- renameSync(tmp, filePath);
30
- } catch (error) {
31
- try {
32
- unlinkSync(tmp);
33
- } catch {}
34
- throw error;
35
- }
36
- }
3
+ import { existsSync as existsSync7, mkdirSync as mkdirSync5, readdirSync as readdirSync4, readFileSync as readFileSync7, rmdirSync as rmdirSync2, writeFileSync as writeFileSync4 } from "node:fs";
4
+ import { basename as basename5, join as join9, resolve as resolve8, sep as sep3 } from "node:path";
37
5
 
38
6
  // src/lease.ts
39
- import { mkdirSync as mkdirSync2, rmdirSync, statSync, unlinkSync as unlinkSync2, writeFileSync as writeFileSync2 } from "node:fs";
40
- import { dirname as dirname2, isAbsolute, join as join2, resolve as resolve2 } from "node:path";
7
+ import { mkdirSync, rmdirSync, statSync, unlinkSync, writeFileSync } from "node:fs";
8
+ import { dirname, isAbsolute, join, resolve } from "node:path";
41
9
  import { setTimeout as sleep } from "node:timers/promises";
42
10
  import { AsyncLocalStorage } from "node:async_hooks";
11
+ function isPlainObject(value) {
12
+ return typeof value === "object" && value !== null && !Array.isArray(value);
13
+ }
14
+ function violation(severity, code, message, fix) {
15
+ return { ok: false, severity, code, message, fix };
16
+ }
17
+ function validateNonEmptyString(violations, value, field, missingCode, invalidCode) {
18
+ if (value === undefined) {
19
+ violations.push(violation("high", missingCode, `missing required field: ${field}`));
20
+ } else if (typeof value !== "string" || value.trim() === "") {
21
+ violations.push(violation("medium", invalidCode, `${field} must be a non-empty string`));
22
+ }
23
+ }
43
24
  var DATE_PART = String.raw`\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12]\d|3[01])`;
44
25
  var RFC3339_Z_RE = new RegExp(String.raw`^${DATE_PART}T\d{2}:\d{2}:\d{2}(\.\d+)?Z$`);
45
26
  var DATE_ONLY_RE = new RegExp(String.raw`^${DATE_PART}$`);
27
+ function isValidClaimedAt(value) {
28
+ return typeof value === "string" && (RFC3339_Z_RE.test(value) || DATE_ONLY_RE.test(value));
29
+ }
30
+ function validateExecutionLease(lease) {
31
+ const violations = [];
32
+ if (!isPlainObject(lease)) {
33
+ return {
34
+ ok: false,
35
+ violations: [
36
+ violation("high", "lease.execution-lease.invalid", "execution_lease must be an object — null and tombstone objects are invalid; writers delete the key on release")
37
+ ]
38
+ };
39
+ }
40
+ validateNonEmptyString(violations, lease.holder, "holder", "lease.execution-lease.missing-holder", "lease.execution-lease.invalid-holder");
41
+ if (lease.claimed_at === undefined) {
42
+ violations.push(violation("high", "lease.execution-lease.missing-claimed-at", "missing required field: claimed_at"));
43
+ } else if (!isValidClaimedAt(lease.claimed_at)) {
44
+ violations.push(violation("medium", "lease.execution-lease.invalid-claimed-at", "claimed_at must be an RFC 3339 UTC timestamp with explicit Z (e.g. 2026-07-22T02:30:00Z) or a YYYY-MM-DD date"));
45
+ }
46
+ if (lease.worktree_path === undefined) {
47
+ violations.push(violation("high", "lease.execution-lease.missing-worktree-path", "missing required field: worktree_path"));
48
+ } else if (typeof lease.worktree_path !== "string" || lease.worktree_path.trim() === "") {
49
+ violations.push(violation("medium", "lease.execution-lease.invalid-worktree-path", "worktree_path must be a non-empty string"));
50
+ } else if (!isAbsolute(lease.worktree_path)) {
51
+ violations.push(violation("medium", "lease.execution-lease.invalid-worktree-path", "worktree_path must be an absolute path — it identifies the dedicated feature-worktree root (a Git checkout distinct from the main worktree and the integration worktree)"));
52
+ }
53
+ validateNonEmptyString(violations, lease.working_branch, "working_branch", "lease.execution-lease.missing-working-branch", "lease.execution-lease.invalid-working-branch");
54
+ if (lease.session_label !== undefined && typeof lease.session_label !== "string") {
55
+ violations.push(violation("medium", "lease.execution-lease.invalid-session-label", "session_label must be a string (display only — never used for ownership comparison)"));
56
+ }
57
+ return { ok: violations.length === 0, violations };
58
+ }
59
+ function validateIntegrationMergeLease(lease) {
60
+ const violations = [];
61
+ if (!isPlainObject(lease)) {
62
+ return {
63
+ ok: false,
64
+ violations: [
65
+ violation("high", "lease.merge-lease.invalid", "integration_merge_lease must be an object — absent means unclaimed; null and tombstone objects are invalid; writers delete the key on release")
66
+ ]
67
+ };
68
+ }
69
+ validateNonEmptyString(violations, lease.holder, "holder", "lease.merge-lease.missing-holder", "lease.merge-lease.invalid-holder");
70
+ if (lease.claimed_at === undefined) {
71
+ violations.push(violation("high", "lease.merge-lease.missing-claimed-at", "missing required field: claimed_at"));
72
+ } else if (!isValidClaimedAt(lease.claimed_at)) {
73
+ violations.push(violation("medium", "lease.merge-lease.invalid-claimed-at", "claimed_at must be an RFC 3339 UTC timestamp with explicit Z (e.g. 2026-07-22T04:00:00Z) or a YYYY-MM-DD date"));
74
+ }
75
+ validateNonEmptyString(violations, lease.plan_id, "plan_id", "lease.merge-lease.missing-plan-id", "lease.merge-lease.invalid-plan-id");
76
+ validateNonEmptyString(violations, lease.source_branch, "source_branch", "lease.merge-lease.missing-source-branch", "lease.merge-lease.invalid-source-branch");
77
+ validateNonEmptyString(violations, lease.target_branch, "target_branch", "lease.merge-lease.missing-target-branch", "lease.merge-lease.invalid-target-branch");
78
+ if (lease.session_label !== undefined && typeof lease.session_label !== "string") {
79
+ violations.push(violation("medium", "lease.merge-lease.invalid-session-label", "session_label must be a string (display only — never used for ownership comparison)"));
80
+ }
81
+ return { ok: violations.length === 0, violations };
82
+ }
46
83
  var STATUS_WRITE_LOCKDIR = ".status-write.lockdir";
47
84
  var LOCKDIR_HOLDER_PID = "holder.pid";
48
85
  var heldLockDirs = new AsyncLocalStorage;
49
86
  async function withStatusWriteLock(statusPath, fn, opts = {}) {
50
- const lockDir = join2(dirname2(resolve2(statusPath)), STATUS_WRITE_LOCKDIR);
87
+ const lockDir = join(dirname(resolve(statusPath)), STATUS_WRITE_LOCKDIR);
51
88
  const held = heldLockDirs.getStore();
52
89
  if (held !== undefined && held.has(lockDir)) {
53
90
  throw new Error(`${lockDir} is already held by this process in this async context — withStatusWriteLock is not reentrant; a nested acquisition on the same status.json is a bug`);
@@ -58,7 +95,7 @@ async function withStatusWriteLock(statusPath, fn, opts = {}) {
58
95
  let acquired = null;
59
96
  for (;; ) {
60
97
  try {
61
- mkdirSync2(lockDir);
98
+ mkdirSync(lockDir);
62
99
  const st = statSync(lockDir);
63
100
  acquired = { dev: st.dev, ino: st.ino };
64
101
  break;
@@ -72,7 +109,7 @@ async function withStatusWriteLock(statusPath, fn, opts = {}) {
72
109
  }
73
110
  }
74
111
  try {
75
- writeFileSync2(join2(lockDir, LOCKDIR_HOLDER_PID), String(process.pid), "utf8");
112
+ writeFileSync(join(lockDir, LOCKDIR_HOLDER_PID), String(process.pid), "utf8");
76
113
  } catch {}
77
114
  const owns = held ?? new Set;
78
115
  owns.add(lockDir);
@@ -84,7 +121,7 @@ async function withStatusWriteLock(statusPath, fn, opts = {}) {
84
121
  const current = statSync(lockDir);
85
122
  if (acquired !== null && current.dev === acquired.dev && current.ino === acquired.ino) {
86
123
  try {
87
- unlinkSync2(join2(lockDir, LOCKDIR_HOLDER_PID));
124
+ unlinkSync(join(lockDir, LOCKDIR_HOLDER_PID));
88
125
  } catch {}
89
126
  rmdirSync(lockDir);
90
127
  }
@@ -92,14 +129,384 @@ async function withStatusWriteLock(statusPath, fn, opts = {}) {
92
129
  }
93
130
  }
94
131
 
132
+ // src/coordination-write.ts
133
+ import { AsyncLocalStorage as AsyncLocalStorage2 } from "node:async_hooks";
134
+ import { createHash } from "node:crypto";
135
+ import { existsSync, readFileSync, realpathSync } from "node:fs";
136
+ import { basename, dirname as dirname2, isAbsolute as isAbsolute2, join as join2, resolve as resolve2 } from "node:path";
137
+ class CoordinationError extends Error {
138
+ code;
139
+ details;
140
+ constructor(code, message, details = {}) {
141
+ super(message);
142
+ this.name = "CoordinationError";
143
+ this.code = code;
144
+ this.details = details;
145
+ }
146
+ }
147
+ function isPlainObject2(value) {
148
+ return typeof value === "object" && value !== null && !Array.isArray(value);
149
+ }
150
+ function isNonEmptyString(value) {
151
+ return typeof value === "string" && value.trim() !== "";
152
+ }
153
+ function artifactVersion(bytes) {
154
+ return `sha256:${createHash("sha256").update(bytes).digest("hex")}`;
155
+ }
156
+ function readArtifactBytes(filePath) {
157
+ if (!existsSync(filePath))
158
+ return;
159
+ const bytes = readFileSync(filePath);
160
+ const version = artifactVersion(bytes);
161
+ let payload;
162
+ try {
163
+ payload = JSON.parse(bytes.toString("utf8"));
164
+ } catch (error) {
165
+ throw new CoordinationError("coordination.store", `Invalid JSON in ${filePath}: ${error.message}`, {
166
+ path: filePath
167
+ });
168
+ }
169
+ return { payload, version };
170
+ }
171
+ function canonicalTarget(target) {
172
+ const abs = resolve2(target);
173
+ let dir = abs;
174
+ const tail = [];
175
+ for (;; ) {
176
+ if (existsSync(dir)) {
177
+ try {
178
+ return join2(realpathSync(dir), ...tail);
179
+ } catch {
180
+ return abs;
181
+ }
182
+ }
183
+ const parent = dirname2(dir);
184
+ if (parent === dir)
185
+ return abs;
186
+ tail.unshift(basename(dir));
187
+ dir = parent;
188
+ }
189
+ }
190
+ var writeAuthorizations = new AsyncLocalStorage2;
191
+ async function withProtectedWrite(target, operation, fn) {
192
+ const inherited = writeAuthorizations.getStore() ?? [];
193
+ return writeAuthorizations.run([...inherited, { target: canonicalTarget(target), operation }], async () => fn());
194
+ }
195
+ function isWriteAuthorized(target, operation) {
196
+ const active = writeAuthorizations.getStore();
197
+ if (active === undefined || active.length === 0)
198
+ return false;
199
+ const canonical = canonicalTarget(target);
200
+ return active.some((entry) => entry.target === canonical && entry.operation === operation);
201
+ }
202
+ function assertProtectedWriteAuthorized(target, operation, kind) {
203
+ const canonical = canonicalTarget(target);
204
+ if (isWriteAuthorized(canonical, operation))
205
+ return;
206
+ throw new CoordinationError("coordination.direct-write-refused", `${canonical} is a protected coordination document (${kind}) — a raw store.${operation} is refused; use the coordination API (bind/prepare/progress/residual/handoff/accept/return/complete) or the locked writer`, { path: canonical, operation, kind });
207
+ }
208
+ var HANDOFF_STATES = [
209
+ "submitted",
210
+ "accepted",
211
+ "returned",
212
+ "integrating",
213
+ "merged",
214
+ "completed"
215
+ ];
216
+ var PLAN_PROGRESS_STATUSES = ["InProgress", "InReview", "Blocked"];
217
+ var SHA256_HEX = /^[0-9a-f]{64}$/;
218
+ var GIT_SHA = /^(?:[0-9a-f]{40}|[0-9a-f]{64})$/;
219
+ var HASH_RE = /^sha256:[0-9a-f]{64}$/;
220
+ function invalid(code, message) {
221
+ return { ok: false, severity: "high", code, message };
222
+ }
223
+ function validateBinding(value, what) {
224
+ if (!isPlainObject2(value))
225
+ return [invalid("coordination.row.binding-shape", `${what} must be an object`)];
226
+ const violations = [];
227
+ const extra = Object.keys(value).filter((key) => !["session_id", "session_file", "bound_at"].includes(key));
228
+ if (extra.length > 0) {
229
+ violations.push(invalid("coordination.row.binding-field", `${what} has unexpected key(s): ${extra.join(", ")}`));
230
+ }
231
+ if (!isNonEmptyString(value.session_id)) {
232
+ violations.push(invalid("coordination.row.binding-field", `${what}.session_id must be a non-empty string`));
233
+ }
234
+ if (!isNonEmptyString(value.session_file) || !isAbsolute2(String(value.session_file))) {
235
+ violations.push(invalid("coordination.row.binding-field", `${what}.session_file must be an absolute path`));
236
+ }
237
+ if (!isNonEmptyString(value.bound_at)) {
238
+ violations.push(invalid("coordination.row.binding-field", `${what}.bound_at must be a timestamp`));
239
+ }
240
+ return violations;
241
+ }
242
+ function validateEvidenceRef(value, what) {
243
+ if (!isPlainObject2(value))
244
+ return [invalid("coordination.row.evidence-shape", `${what} must be a hash-pinned reference`)];
245
+ const violations = [];
246
+ const extra = Object.keys(value).filter((key) => !["path", "sha256"].includes(key));
247
+ if (extra.length > 0) {
248
+ violations.push(invalid("coordination.row.evidence-field", `${what} has unexpected key(s): ${extra.join(", ")}`));
249
+ }
250
+ if (!isNonEmptyString(value.path) || !isAbsolute2(String(value.path))) {
251
+ violations.push(invalid("coordination.row.evidence-field", `${what}.path must be an absolute path`));
252
+ }
253
+ if (!isNonEmptyString(value.sha256) || !SHA256_HEX.test(String(value.sha256))) {
254
+ violations.push(invalid("coordination.row.evidence-field", `${what}.sha256 must be 64 lowercase hex`));
255
+ }
256
+ return violations;
257
+ }
258
+ function validatePlanProgress(value, what = "coordination.progress") {
259
+ if (!isPlainObject2(value))
260
+ return [invalid("coordination.row.progress-shape", `${what} must be an object`)];
261
+ const violations = [];
262
+ const extra = Object.keys(value).filter((key) => !["status", "summary", "evidence_paths", "track_branches"].includes(key));
263
+ if (extra.length > 0) {
264
+ violations.push(invalid("coordination.row.progress-field", `${what} has unexpected key(s): ${extra.join(", ")}`));
265
+ }
266
+ if (!PLAN_PROGRESS_STATUSES.includes(value.status)) {
267
+ violations.push(invalid("coordination.row.progress-field", `${what}.status must be one of ${PLAN_PROGRESS_STATUSES.join(", ")}`));
268
+ }
269
+ if (!isNonEmptyString(value.summary)) {
270
+ violations.push(invalid("coordination.row.progress-field", `${what}.summary must be a non-empty string`));
271
+ }
272
+ if (!Array.isArray(value.evidence_paths) || !value.evidence_paths.every(isNonEmptyString)) {
273
+ violations.push(invalid("coordination.row.progress-field", `${what}.evidence_paths must be an array of paths`));
274
+ }
275
+ if (value.track_branches !== undefined) {
276
+ if (!Array.isArray(value.track_branches) || !value.track_branches.every(isNonEmptyString)) {
277
+ violations.push(invalid("coordination.row.progress-field", `${what}.track_branches must be an array of branch names`));
278
+ }
279
+ }
280
+ return violations;
281
+ }
282
+ function validatePlanHandoff(value, what = "coordination.handoff") {
283
+ if (!isPlainObject2(value))
284
+ return [invalid("coordination.row.handoff-shape", `${what} must be an object`)];
285
+ const allowed = [
286
+ "id",
287
+ "attempt",
288
+ "state",
289
+ "submitted_by",
290
+ "submitted_at",
291
+ "source_branch",
292
+ "source_sha",
293
+ "worktree_path",
294
+ "review_base",
295
+ "review_head",
296
+ "qc",
297
+ "qa",
298
+ "accepted_by",
299
+ "accepted_at",
300
+ "returned_at",
301
+ "return_reason",
302
+ "integration",
303
+ "completed_at"
304
+ ];
305
+ const violations = [];
306
+ const extra = Object.keys(value).filter((key) => !allowed.includes(key));
307
+ if (extra.length > 0) {
308
+ violations.push(invalid("coordination.row.handoff-field", `${what} has unexpected key(s): ${extra.join(", ")}`));
309
+ }
310
+ const required = [
311
+ "id",
312
+ "attempt",
313
+ "state",
314
+ "submitted_by",
315
+ "submitted_at",
316
+ "source_branch",
317
+ "source_sha",
318
+ "worktree_path",
319
+ "review_base",
320
+ "review_head"
321
+ ];
322
+ for (const key of required) {
323
+ if (value[key] === undefined) {
324
+ violations.push(invalid("coordination.row.handoff-field", `${what}.${key} is required`));
325
+ }
326
+ }
327
+ if (!Number.isInteger(value.attempt) || value.attempt < 1) {
328
+ violations.push(invalid("coordination.row.handoff-field", `${what}.attempt must be a positive integer`));
329
+ }
330
+ if (!HANDOFF_STATES.includes(value.state)) {
331
+ violations.push(invalid("coordination.row.handoff-field", `${what}.state must be one of ${HANDOFF_STATES.join(", ")}`));
332
+ }
333
+ if (value.id !== undefined && !isNonEmptyString(value.id)) {
334
+ violations.push(invalid("coordination.row.handoff-field", `${what}.id must be a non-empty string`));
335
+ }
336
+ for (const key of ["source_sha", "review_base", "review_head"]) {
337
+ if (value[key] !== undefined && !GIT_SHA.test(String(value[key]))) {
338
+ violations.push(invalid("coordination.row.handoff-field", `${what}.${key} must be a 40-hex git object id`));
339
+ }
340
+ }
341
+ for (const key of ["submitted_at", "accepted_at", "returned_at", "completed_at"]) {
342
+ if (value[key] !== undefined && !isNonEmptyString(value[key])) {
343
+ violations.push(invalid("coordination.row.handoff-field", `${what}.${key} must be a timestamp`));
344
+ }
345
+ }
346
+ if (value.worktree_path !== undefined && (!isNonEmptyString(value.worktree_path) || !isAbsolute2(String(value.worktree_path)))) {
347
+ violations.push(invalid("coordination.row.handoff-field", `${what}.worktree_path must be an absolute path`));
348
+ }
349
+ if (value.qc !== undefined) {
350
+ if (!isPlainObject2(value.qc)) {
351
+ violations.push(invalid("coordination.row.handoff-shape", `${what}.qc must be an object`));
352
+ } else {
353
+ const qc = value.qc;
354
+ const qcExtra = Object.keys(qc).filter((key) => !["decision", "reports", "consolidated"].includes(key));
355
+ if (qcExtra.length > 0) {
356
+ violations.push(invalid("coordination.row.handoff-field", `${what}.qc has unexpected key(s): ${qcExtra.join(", ")}`));
357
+ }
358
+ if (!isNonEmptyString(qc.decision)) {
359
+ violations.push(invalid("coordination.row.handoff-field", `${what}.qc.decision must be a non-empty string`));
360
+ }
361
+ if (!Array.isArray(qc.reports) || qc.reports.length === 0) {
362
+ violations.push(invalid("coordination.row.handoff-field", `${what}.qc.reports must be a non-empty array`));
363
+ } else {
364
+ qc.reports.forEach((ref, index) => {
365
+ violations.push(...validateEvidenceRef(ref, `${what}.qc.reports[${index}]`));
366
+ });
367
+ }
368
+ violations.push(...validateEvidenceRef(qc.consolidated, `${what}.qc.consolidated`));
369
+ }
370
+ }
371
+ if (value.qa !== undefined) {
372
+ if (!isPlainObject2(value.qa)) {
373
+ violations.push(invalid("coordination.row.handoff-shape", `${what}.qa must be an object`));
374
+ } else {
375
+ const qa = value.qa;
376
+ const qaExtra = Object.keys(qa).filter((key) => !["gate", "decision", "report"].includes(key));
377
+ if (qaExtra.length > 0) {
378
+ violations.push(invalid("coordination.row.handoff-field", `${what}.qa has unexpected key(s): ${qaExtra.join(", ")}`));
379
+ }
380
+ if (!isNonEmptyString(qa.gate)) {
381
+ violations.push(invalid("coordination.row.handoff-field", `${what}.qa.gate must be a non-empty string`));
382
+ }
383
+ if (!isNonEmptyString(qa.decision)) {
384
+ violations.push(invalid("coordination.row.handoff-field", `${what}.qa.decision must be a non-empty string`));
385
+ }
386
+ violations.push(...validateEvidenceRef(qa.report, `${what}.qa.report`));
387
+ }
388
+ }
389
+ if (value.integration !== undefined) {
390
+ if (!isPlainObject2(value.integration)) {
391
+ violations.push(invalid("coordination.row.handoff-shape", `${what}.integration must be an object`));
392
+ } else {
393
+ const integration = value.integration;
394
+ const integrationAllowed = [
395
+ "target_branch",
396
+ "worktree_path",
397
+ "base_sha",
398
+ "started_at",
399
+ "result_sha",
400
+ "verified_at"
401
+ ];
402
+ const integrationExtra = Object.keys(integration).filter((key) => !integrationAllowed.includes(key));
403
+ if (integrationExtra.length > 0) {
404
+ violations.push(invalid("coordination.row.handoff-field", `${what}.integration has unexpected key(s): ${integrationExtra.join(", ")}`));
405
+ }
406
+ for (const key of ["target_branch", "worktree_path", "base_sha", "started_at"]) {
407
+ if (!isNonEmptyString(integration[key])) {
408
+ violations.push(invalid("coordination.row.handoff-field", `${what}.integration.${key} is required`));
409
+ }
410
+ }
411
+ for (const key of ["base_sha", "result_sha"]) {
412
+ if (integration[key] !== undefined && !GIT_SHA.test(String(integration[key]))) {
413
+ violations.push(invalid("coordination.row.handoff-field", `${what}.integration.${key} must be a 40-hex git object id`));
414
+ }
415
+ }
416
+ if (integration.result_sha !== undefined && integration.verified_at === undefined) {
417
+ violations.push(invalid("coordination.row.handoff-field", `${what}.integration.result_sha requires verified_at`));
418
+ }
419
+ }
420
+ }
421
+ if ((value.state === "integrating" || value.state === "merged" || value.state === "completed") && value.integration === undefined) {
422
+ violations.push(invalid("coordination.row.handoff-field", `${what}.state ${String(value.state)} requires integration`));
423
+ }
424
+ return violations;
425
+ }
426
+ function validatePreparedCoordination(value, what = "coordination.prepared") {
427
+ if (!isPlainObject2(value))
428
+ return [invalid("coordination.row.prepared-shape", `${what} must be an object`)];
429
+ const allowed = [
430
+ "assignment_path",
431
+ "assignment_sha256",
432
+ "plan_sha256",
433
+ "qa_gate",
434
+ "findings_cleanup",
435
+ "prepared_by",
436
+ "prepared_at"
437
+ ];
438
+ const violations = [];
439
+ const extra = Object.keys(value).filter((key) => !allowed.includes(key));
440
+ if (extra.length > 0) {
441
+ violations.push(invalid("coordination.row.prepared-field", `${what} has unexpected key(s): ${extra.join(", ")}`));
442
+ }
443
+ for (const key of allowed) {
444
+ if (!isNonEmptyString(value[key])) {
445
+ violations.push(invalid("coordination.row.prepared-field", `${what}.${key} is required`));
446
+ }
447
+ }
448
+ if (value.assignment_path !== undefined && !isAbsolute2(String(value.assignment_path))) {
449
+ violations.push(invalid("coordination.row.prepared-field", `${what}.assignment_path must be absolute`));
450
+ }
451
+ for (const key of ["assignment_sha256", "plan_sha256"]) {
452
+ if (value[key] !== undefined && !SHA256_HEX.test(String(value[key]))) {
453
+ violations.push(invalid("coordination.row.prepared-field", `${what}.${key} must be 64 lowercase hex`));
454
+ }
455
+ }
456
+ return violations;
457
+ }
458
+ function validateRowCoordination(value, what = "coordination") {
459
+ if (!isPlainObject2(value))
460
+ return [invalid("coordination.row.shape", `${what} must be an object`)];
461
+ const allowed = ["revision", "prepared", "session", "progress", "handoff"];
462
+ const violations = [];
463
+ const extra = Object.keys(value).filter((key) => !allowed.includes(key));
464
+ if (extra.length > 0) {
465
+ violations.push(invalid("coordination.row.field", `${what} has unexpected key(s): ${extra.join(", ")}`));
466
+ }
467
+ if (!Number.isInteger(value.revision) || value.revision < 0) {
468
+ violations.push(invalid("coordination.row.revision", `${what}.revision must be a non-negative integer`));
469
+ }
470
+ if (value.prepared !== undefined)
471
+ violations.push(...validatePreparedCoordination(value.prepared, `${what}.prepared`));
472
+ if (value.session !== undefined)
473
+ violations.push(...validateBinding(value.session, `${what}.session`));
474
+ if (value.progress !== undefined)
475
+ violations.push(...validatePlanProgress(value.progress, `${what}.progress`));
476
+ if (value.handoff !== undefined)
477
+ violations.push(...validatePlanHandoff(value.handoff, `${what}.handoff`));
478
+ if (value.handoff !== undefined && value.session === undefined) {
479
+ violations.push(invalid("coordination.row.handoff-field", `${what}.handoff requires a bound plan session`));
480
+ }
481
+ return violations;
482
+ }
483
+ function validateSnapshotCoordination(value, what = "coordination") {
484
+ if (!isPlainObject2(value))
485
+ return [invalid("coordination.snapshot.shape", `${what} must be an object`)];
486
+ const violations = [];
487
+ const extra = Object.keys(value).filter((key) => key !== "coordinator");
488
+ if (extra.length > 0) {
489
+ violations.push(invalid("coordination.snapshot.field", `${what} has unexpected key(s): ${extra.join(", ")}`));
490
+ }
491
+ if (value.coordinator === undefined) {
492
+ violations.push(invalid("coordination.snapshot.field", `${what}.coordinator is required`));
493
+ } else {
494
+ violations.push(...validateBinding(value.coordinator, `${what}.coordinator`));
495
+ }
496
+ return violations;
497
+ }
498
+ function isArtifactVersion(value) {
499
+ return typeof value === "string" && (value === "absent" || HASH_RE.test(value));
500
+ }
501
+
95
502
  // src/path.ts
96
- import { existsSync as existsSync4, mkdirSync as mkdirSync3, readdirSync as readdirSync3, readFileSync as readFileSync4, realpathSync as realpathSync2, statSync as statSync3, writeFileSync as writeFileSync3 } from "node:fs";
503
+ import { existsSync as existsSync6, mkdirSync as mkdirSync4, readdirSync as readdirSync3, readFileSync as readFileSync6, realpathSync as realpathSync3, statSync as statSync3, writeFileSync as writeFileSync3 } from "node:fs";
97
504
  import { execFileSync } from "node:child_process";
98
- import { basename as basename2, dirname as dirname5, isAbsolute as isAbsolute4, join as join6, relative as relative2, resolve as resolve6 } from "node:path";
505
+ import { basename as basename4, dirname as dirname6, isAbsolute as isAbsolute6, join as join8, relative as relative2, resolve as resolve7 } from "node:path";
99
506
 
100
507
  // src/mstarc.ts
101
508
  import { readFileSync as readFileSync2, statSync as statSync2 } from "node:fs";
102
- import { dirname as dirname3, isAbsolute as isAbsolute2, join as join3, relative, resolve as resolve3 } from "node:path";
509
+ import { dirname as dirname3, isAbsolute as isAbsolute3, join as join3, relative, resolve as resolve3 } from "node:path";
103
510
  var MSTARC_FILE = ".mstarc";
104
511
  var MSTARC_SECTION = "config";
105
512
  var MSTARC_HARNESS_DIR_KEY = "harness_dir";
@@ -183,21 +590,53 @@ function loadMstarc(startDir, boundary) {
183
590
  }
184
591
  function isAtOrBelow(dir, root) {
185
592
  const rel = relative(root, dir);
186
- return rel === "" || !rel.startsWith("..") && !isAbsolute2(rel);
593
+ return rel === "" || !rel.startsWith("..") && !isAbsolute3(rel);
594
+ }
595
+
596
+ // src/core.ts
597
+ import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync3, renameSync, unlinkSync as unlinkSync2, writeFileSync as writeFileSync2 } from "node:fs";
598
+ import { randomUUID } from "node:crypto";
599
+ import { basename as basename2, dirname as dirname4, join as join4, resolve as resolve4 } from "node:path";
600
+ function readJson(filePath) {
601
+ if (!existsSync2(filePath))
602
+ return {};
603
+ const content = readFileSync3(filePath, "utf8").trim();
604
+ if (!content)
605
+ return {};
606
+ try {
607
+ return JSON.parse(content);
608
+ } catch (error) {
609
+ throw new Error(`Invalid JSON in ${filePath}: ${error.message}`);
610
+ }
611
+ }
612
+ function writeJson(filePath, value) {
613
+ const parent = dirname4(filePath);
614
+ mkdirSync2(parent, { recursive: true });
615
+ const tmp = join4(parent, `.${basename2(filePath)}.${process.pid}.${randomUUID()}.tmp`);
616
+ try {
617
+ writeFileSync2(tmp, `${JSON.stringify(value, null, 2)}
618
+ `, "utf8");
619
+ renameSync(tmp, filePath);
620
+ } catch (error) {
621
+ try {
622
+ unlinkSync2(tmp);
623
+ } catch {}
624
+ throw error;
625
+ }
187
626
  }
188
627
 
189
628
  // src/status.ts
190
- import { existsSync as existsSync3, readFileSync as readFileSync3, readdirSync as readdirSync2, realpathSync } from "node:fs";
191
- import { dirname as dirname4, join as join5, resolve as resolve5, sep } from "node:path";
629
+ import { existsSync as existsSync5, readFileSync as readFileSync5, readdirSync as readdirSync2, realpathSync as realpathSync2 } from "node:fs";
630
+ import { dirname as dirname5, join as join7, resolve as resolve6, sep as sep2 } from "node:path";
192
631
 
193
632
  // src/store.ts
194
- import { existsSync as existsSync2, readdirSync, unlinkSync as unlinkSync3 } from "node:fs";
195
- import { isAbsolute as isAbsolute3, join as join4, resolve as resolve4 } from "node:path";
633
+ import { existsSync as existsSync3, readdirSync, unlinkSync as unlinkSync3 } from "node:fs";
634
+ import { basename as basename3, isAbsolute as isAbsolute4, join as join5, resolve as resolve5, sep } from "node:path";
196
635
  var PLAN_SHAPED_KEY_RE = /^[0-9]{8}-[a-z0-9-]+$/;
197
636
  function resolveArtifactPath(harnessRoot, ref) {
198
637
  const { kind, key } = ref;
199
638
  if (kind === "json") {
200
- if (!isAbsolute3(key)) {
639
+ if (!isAbsolute4(key)) {
201
640
  throw new Error(`ArtifactStore json key must be an absolute path — got ${JSON.stringify(key)}`);
202
641
  }
203
642
  if (key.split(/[\\/]+/).includes("..")) {
@@ -210,18 +649,18 @@ function resolveArtifactPath(harnessRoot, ref) {
210
649
  if (key !== "root") {
211
650
  throw new Error(`ArtifactStore status key must be "root" — got ${JSON.stringify(key)}`);
212
651
  }
213
- return join4(harnessRoot, "status.json");
652
+ return join5(harnessRoot, "status.json");
214
653
  }
215
654
  if (kind === "snapshot") {
216
- return join4(resolveWorkflowDir(harnessRoot, { harnessDir: harnessRoot }), key, "snapshot.json");
655
+ return join5(resolveWorkflowDir(harnessRoot, { harnessDir: harnessRoot }), key, "snapshot.json");
217
656
  }
218
657
  if (kind === "residuals") {
219
- return join4(resolveProjectDir(harnessRoot, { harnessDir: harnessRoot }), key, "residuals.json");
658
+ return join5(resolveProjectDir(harnessRoot, { harnessDir: harnessRoot }), key, "residuals.json");
220
659
  }
221
660
  if (PLAN_SHAPED_KEY_RE.test(key)) {
222
- return join4(harnessRoot, "sdd", key, "review", "report.json");
661
+ return join5(harnessRoot, "sdd", key, "review", "report.json");
223
662
  }
224
- return join4(harnessRoot, "sdd", "_reviews", `${key}.json`);
663
+ return join5(harnessRoot, "sdd", "_reviews", `${key}.json`);
225
664
  }
226
665
  function listDirNames(dir) {
227
666
  return readDirEntries(dir).filter((entry) => entry.isDirectory()).map((entry) => entry.name);
@@ -239,6 +678,26 @@ function readDirEntries(dir) {
239
678
  throw error;
240
679
  }
241
680
  }
681
+ function protectedKindOf(root, ref, filePath) {
682
+ if (ref.kind === "status")
683
+ return "root";
684
+ if (ref.kind === "snapshot")
685
+ return "snapshot";
686
+ if (ref.kind === "residuals")
687
+ return "register";
688
+ if (ref.kind !== "json")
689
+ return null;
690
+ const canonical = canonicalTarget(filePath);
691
+ if (canonical === canonicalTarget(resolveArtifactPath(root, { kind: "status", key: "root" })))
692
+ return "root";
693
+ const workflowDir = canonicalTarget(resolveWorkflowDir(root, { harnessDir: root }));
694
+ if (basename3(canonical) === "snapshot.json" && canonical.startsWith(`${workflowDir}${sep}`))
695
+ return "snapshot";
696
+ const projectDir = canonicalTarget(resolveProjectDir(root, { harnessDir: root }));
697
+ if (basename3(canonical) === "residuals.json" && canonical.startsWith(`${projectDir}${sep}`))
698
+ return "register";
699
+ return null;
700
+ }
242
701
  function tryResolveGetPath(root, kind, key) {
243
702
  try {
244
703
  return resolveArtifactPath(root, { kind, key });
@@ -247,24 +706,31 @@ function tryResolveGetPath(root, kind, key) {
247
706
  }
248
707
  }
249
708
  function createFsStore(harnessRoot) {
250
- const root = resolve4(harnessRoot);
709
+ const root = resolve5(harnessRoot);
251
710
  return {
252
711
  root,
253
712
  async put(doc) {
254
713
  if (doc.schema !== undefined) {
255
714
  throw new Error("FsStore does not persist schema ids — omit --schema or inject a store module that persists it");
256
715
  }
257
- writeJson(resolveArtifactPath(root, doc), doc.payload);
716
+ const filePath = resolveArtifactPath(root, doc);
717
+ const protectedKind = protectedKindOf(root, doc, filePath);
718
+ if (protectedKind !== null)
719
+ assertProtectedWriteAuthorized(filePath, "put", protectedKind);
720
+ writeJson(filePath, doc.payload);
258
721
  },
259
722
  async get(ref) {
260
723
  const filePath = resolveArtifactPath(root, ref);
261
- if (!existsSync2(filePath))
724
+ if (!existsSync3(filePath))
262
725
  return;
263
726
  return readJson(filePath);
264
727
  },
265
728
  async delete(ref) {
266
729
  const filePath = resolveArtifactPath(root, ref);
267
- if (existsSync2(filePath))
730
+ const protectedKind = protectedKindOf(root, ref, filePath);
731
+ if (protectedKind !== null)
732
+ assertProtectedWriteAuthorized(filePath, "delete", protectedKind);
733
+ if (existsSync3(filePath))
268
734
  unlinkSync3(filePath);
269
735
  },
270
736
  async list(kind) {
@@ -273,18 +739,18 @@ function createFsStore(harnessRoot) {
273
739
  }
274
740
  const keys = [];
275
741
  if (kind === "status") {
276
- if (existsSync2(resolveArtifactPath(root, { kind, key: "root" })))
742
+ if (existsSync3(resolveArtifactPath(root, { kind, key: "root" })))
277
743
  keys.push("root");
278
744
  } else if (kind === "snapshot" || kind === "residuals") {
279
745
  const baseDir = kind === "snapshot" ? resolveWorkflowDir(root, { harnessDir: root }) : resolveProjectDir(root, { harnessDir: root });
280
746
  for (const name of listDirNames(baseDir)) {
281
747
  const getPath = tryResolveGetPath(root, kind, name);
282
- if (getPath !== undefined && existsSync2(getPath))
748
+ if (getPath !== undefined && existsSync3(getPath))
283
749
  keys.push(name);
284
750
  }
285
751
  } else {
286
- const sddDir = join4(root, "sdd");
287
- for (const key of listJsonKeys(join4(sddDir, "_reviews"))) {
752
+ const sddDir = join5(root, "sdd");
753
+ for (const key of listJsonKeys(join5(sddDir, "_reviews"))) {
288
754
  if (!PLAN_SHAPED_KEY_RE.test(key) && tryResolveGetPath(root, kind, key) !== undefined) {
289
755
  keys.push(key);
290
756
  }
@@ -292,7 +758,7 @@ function createFsStore(harnessRoot) {
292
758
  for (const name of listDirNames(sddDir)) {
293
759
  if (PLAN_SHAPED_KEY_RE.test(name)) {
294
760
  const getPath = tryResolveGetPath(root, kind, name);
295
- if (getPath !== undefined && existsSync2(getPath))
761
+ if (getPath !== undefined && existsSync3(getPath))
296
762
  keys.push(name);
297
763
  }
298
764
  }
@@ -307,7 +773,7 @@ function getArtifactStore() {
307
773
  return injectedStore;
308
774
  const root = resolveHarnessDir(process.cwd());
309
775
  if (root === null) {
310
- throw new Error(`harness dir not found from ${resolve4(process.cwd())} — cannot create the default FsStore (run \`mstar harness scaffold\`, pass opts.harnessDir, or set MSTAR_HARNESS_DIR)`);
776
+ throw new Error(`harness dir not found from ${resolve5(process.cwd())} — cannot create the default FsStore (run \`mstar harness scaffold\`, pass opts.harnessDir, or set MSTAR_HARNESS_DIR)`);
311
777
  }
312
778
  return createFsStore(root);
313
779
  }
@@ -316,23 +782,215 @@ function assertFsStorePath(store, ref, expectedPath) {
316
782
  if (typeof root !== "string")
317
783
  return;
318
784
  const storePath = resolveArtifactPath(root, ref);
319
- const expected = resolve4(expectedPath);
785
+ const expected = resolve5(expectedPath);
320
786
  if (storePath !== expected) {
321
787
  throw new Error(`routed writer path mismatch: the active FsStore resolves ${ref.kind}/${JSON.stringify(ref.key)} to ${JSON.stringify(storePath)} but the caller's target is ${JSON.stringify(expected)} — call setArtifactStore(createFsStore(<root>)) first when the write target differs from the active store's root`);
322
788
  }
323
789
  }
324
790
 
325
791
  // src/workflow.ts
792
+ import { existsSync as existsSync4, mkdirSync as mkdirSync3, readFileSync as readFileSync4 } from "node:fs";
793
+ import { isAbsolute as isAbsolute5, join as join6 } from "node:path";
326
794
  var WORKFLOW_SNAPSHOT_FILE = "snapshot.json";
795
+ var WORKFLOW_LIFECYCLE_STATUSES = ["running", "paused", "completed", "failed", "stopped"];
327
796
  var WORKFLOW_TERMINAL_STATUSES = ["completed", "failed", "stopped"];
328
797
  var WORKFLOW_LIFECYCLE_TYPES = ["plan", "iteration"];
798
+ function stableJson(value) {
799
+ if (Array.isArray(value))
800
+ return `[${value.map(stableJson).join(",")}]`;
801
+ if (isPlainObject2(value)) {
802
+ const keys = Object.keys(value).sort();
803
+ return `{${keys.map((key) => `${JSON.stringify(key)}:${stableJson(value[key])}`).join(",")}}`;
804
+ }
805
+ return JSON.stringify(value) ?? "undefined";
806
+ }
807
+ function violation2(severity, code, message, fix) {
808
+ return { ok: false, severity, code, message, fix };
809
+ }
810
+ function validateNonEmptyString2(violations, value, field, missingCode, invalidCode) {
811
+ if (value === undefined) {
812
+ violations.push(violation2("high", missingCode, `missing required field: ${field}`));
813
+ } else if (typeof value !== "string" || value.trim() === "") {
814
+ violations.push(violation2("medium", invalidCode, `${field} must be a non-empty string`));
815
+ }
816
+ }
817
+ function validateWorktreePathValue(violations, value, field) {
818
+ if (typeof value !== "string" || value.trim() === "" || !isAbsolute5(value)) {
819
+ violations.push(violation2("high", "workflow.snapshot.invalid-integration-worktree-path", `${field} must be a non-empty absolute path — got ${JSON.stringify(value)}`, "record the absolute integration checkout path (integration_worktree_path)"));
820
+ }
821
+ }
822
+ function validateWorkflowSnapshot(doc) {
823
+ const violations = [];
824
+ if (!isPlainObject2(doc)) {
825
+ return {
826
+ ok: false,
827
+ violations: [violation2("high", "workflow.snapshot.invalid", "workflow snapshot must be an object")]
828
+ };
829
+ }
830
+ if (doc.schema_version === undefined) {
831
+ violations.push(violation2("high", "workflow.snapshot.missing-schema-version", "missing required field: schema_version"));
832
+ } else if (doc.schema_version !== 1) {
833
+ violations.push(violation2("high", "workflow.snapshot.invalid-schema-version", `schema_version must be 1 — got ${JSON.stringify(doc.schema_version)} (version is reserved for the root file discriminator)`));
834
+ }
835
+ if (doc.version !== undefined) {
836
+ violations.push(violation2("medium", "workflow.snapshot.reserved-version", `top-level version is reserved for the root status.json discriminator — snapshots use schema_version; remove the version key (got ${JSON.stringify(doc.version)})`, "remove the version key from the snapshot"));
837
+ }
838
+ validateNonEmptyString2(violations, doc.id, "id", "workflow.snapshot.missing-id", "workflow.snapshot.invalid-id");
839
+ if (doc.type === undefined) {
840
+ violations.push(violation2("high", "workflow.snapshot.missing-type", "missing required field: type"));
841
+ } else if (typeof doc.type !== "string" || !WORKFLOW_LIFECYCLE_TYPES.includes(doc.type)) {
842
+ violations.push(violation2("medium", "workflow.snapshot.invalid-type", `type must be one of ${WORKFLOW_LIFECYCLE_TYPES.join(" | ")} — got ${JSON.stringify(doc.type)}`));
843
+ }
844
+ if (doc.status === undefined) {
845
+ violations.push(violation2("high", "workflow.snapshot.missing-status", "missing required field: status"));
846
+ } else if (typeof doc.status !== "string" || !WORKFLOW_LIFECYCLE_STATUSES.includes(doc.status)) {
847
+ violations.push(violation2("medium", "workflow.snapshot.invalid-status", `status must be one of ${WORKFLOW_LIFECYCLE_STATUSES.join(" | ")} — got ${JSON.stringify(doc.status)}`));
848
+ }
849
+ validateNonEmptyString2(violations, doc.started_at, "started_at", "workflow.snapshot.missing-started-at", "workflow.snapshot.invalid-started-at");
850
+ validateNonEmptyString2(violations, doc.updated_at, "updated_at", "workflow.snapshot.missing-updated-at", "workflow.snapshot.invalid-updated-at");
851
+ if (doc.ended_at !== undefined) {
852
+ validateNonEmptyString2(violations, doc.ended_at, "ended_at", "workflow.snapshot.missing-ended-at", "workflow.snapshot.invalid-ended-at");
853
+ }
854
+ if (doc.phase !== undefined && typeof doc.phase !== "string") {
855
+ violations.push(violation2("medium", "workflow.snapshot.invalid-phase", "phase must be a string (free-form phase machine label)"));
856
+ }
857
+ if (doc.plans === undefined) {
858
+ violations.push(violation2("high", "workflow.snapshot.missing-plans", "missing required field: plans"));
859
+ } else if (!Array.isArray(doc.plans)) {
860
+ violations.push(violation2("high", "workflow.snapshot.invalid-plans", "plans must be an array of legacy plan rows"));
861
+ } else {
862
+ for (const row of doc.plans) {
863
+ violations.push(...validatePlanRow(row).violations);
864
+ if (isPlainObject2(row) && row.execution_lease !== undefined) {
865
+ violations.push(...validateExecutionLease(row.execution_lease).violations);
866
+ }
867
+ if (isPlainObject2(row) && row.coordination !== undefined) {
868
+ violations.push(...validateRowCoordination(row.coordination, `plans[${String(row.id)}].coordination`));
869
+ }
870
+ }
871
+ }
872
+ if (doc.coordination !== undefined) {
873
+ violations.push(...validateSnapshotCoordination(doc.coordination));
874
+ }
875
+ if (doc.execution_policy !== undefined) {
876
+ if (!isPlainObject2(doc.execution_policy)) {
877
+ violations.push(violation2("medium", "workflow.snapshot.invalid-execution-policy", "execution_policy must be an object"));
878
+ }
879
+ }
880
+ if (doc.integration_merge_lease !== undefined) {
881
+ violations.push(...validateIntegrationMergeLease(doc.integration_merge_lease).violations);
882
+ }
883
+ if (doc.branch !== undefined) {
884
+ if (!isPlainObject2(doc.branch)) {
885
+ violations.push(violation2("medium", "workflow.snapshot.invalid-branch", "branch must be an object"));
886
+ } else {
887
+ for (const key of ["base", "integration", "target"]) {
888
+ if (doc.branch[key] !== undefined && (typeof doc.branch[key] !== "string" || doc.branch[key].trim() === "")) {
889
+ violations.push(violation2("medium", "workflow.snapshot.invalid-branch", `branch.${key} must be a non-empty string`));
890
+ }
891
+ }
892
+ }
893
+ }
894
+ const legacyWorktreePath = doc.control_worktree_path;
895
+ const canonicalWorktreePath = doc.integration_worktree_path;
896
+ if (legacyWorktreePath !== undefined && canonicalWorktreePath !== undefined) {
897
+ violations.push(violation2("high", "workflow.snapshot.conflicting-worktree-paths", "both integration_worktree_path and the legacy control_worktree_path key are present — the canonical snapshot carries only integration_worktree_path (refused even when the values are equal)", "remove the legacy control_worktree_path key"));
898
+ } else {
899
+ if (canonicalWorktreePath !== undefined) {
900
+ validateWorktreePathValue(violations, canonicalWorktreePath, "integration_worktree_path");
901
+ }
902
+ if (legacyWorktreePath !== undefined) {
903
+ violations.push(violation2("medium", "workflow.snapshot.legacy-control-worktree-path", "legacy control_worktree_path is present — the canonical reader normalizes it to integration_worktree_path in memory; migrate on the next authorized write (writers emit only the canonical key)", "rename control_worktree_path to integration_worktree_path on the next authorized write"));
904
+ validateWorktreePathValue(violations, legacyWorktreePath, "control_worktree_path (legacy alias)");
905
+ }
906
+ }
907
+ if (doc.legacy_metadata !== undefined && !isPlainObject2(doc.legacy_metadata)) {
908
+ violations.push(violation2("medium", "workflow.snapshot.invalid-legacy-metadata", "legacy_metadata must be an object"));
909
+ }
910
+ if (doc.compass_ref !== undefined) {
911
+ validateNonEmptyString2(violations, doc.compass_ref, "compass_ref", "workflow.snapshot.missing-compass-ref", "workflow.snapshot.invalid-compass-ref");
912
+ }
913
+ const terminal = typeof doc.status === "string" && WORKFLOW_TERMINAL_STATUSES.includes(doc.status);
914
+ if (terminal) {
915
+ if (doc.ended_at === undefined) {
916
+ violations.push(violation2("high", "workflow.snapshot.missing-ended-at", `terminal status ${JSON.stringify(doc.status)} requires ended_at — a terminal snapshot must record when the lifecycle ended`));
917
+ }
918
+ if (Array.isArray(doc.plans)) {
919
+ for (const row of doc.plans) {
920
+ if (isPlainObject2(row) && row.execution_lease !== undefined) {
921
+ violations.push(violation2("high", "workflow.snapshot.terminal-dangling-execution-lease", `terminal snapshot must not carry a row execution_lease (dangling lease) — release every lease before the lifecycle ends`));
922
+ }
923
+ }
924
+ }
925
+ if (doc.integration_merge_lease !== undefined) {
926
+ violations.push(violation2("high", "workflow.snapshot.terminal-dangling-merge-lease", "terminal snapshot must not carry integration_merge_lease (dangling lease) — release the merge lease before the lifecycle ends"));
927
+ }
928
+ }
929
+ return { ok: violations.length === 0, violations };
930
+ }
931
+ async function writeWorkflowSnapshot(snapshot, dir, opts = {}) {
932
+ const gate = validateWorkflowSnapshot(snapshot);
933
+ if (!gate.ok) {
934
+ const detail = gate.violations.map((v) => v.message).join("; ");
935
+ throw new Error(`refusing to write invalid workflow snapshot: ${detail}`);
936
+ }
937
+ if (opts.expectedVersion !== undefined && !isArtifactVersion(opts.expectedVersion)) {
938
+ throw new CoordinationError("coordination.invalid-input", `expectedVersion must be "absent" or sha256:<64 hex> — got ${JSON.stringify(opts.expectedVersion)}`, { expected: opts.expectedVersion });
939
+ }
940
+ if (opts.createOnly === true && opts.expectedVersion !== undefined && opts.expectedVersion !== "absent") {
941
+ throw new CoordinationError("coordination.invalid-input", `createOnly implies expectedVersion "absent" — got ${JSON.stringify(opts.expectedVersion)}`, { expected: opts.expectedVersion });
942
+ }
943
+ const snapshotPath = join6(dir, WORKFLOW_SNAPSHOT_FILE);
944
+ const store = getArtifactStore();
945
+ assertFsStorePath(store, { kind: "snapshot", key: snapshot.id }, snapshotPath);
946
+ mkdirSync3(dir, { recursive: true });
947
+ await withStatusWriteLock(snapshotPath, async () => {
948
+ const current = readArtifactBytes(snapshotPath);
949
+ const currentVersion = current?.version ?? "absent";
950
+ const required = opts.createOnly === true ? "absent" : opts.expectedVersion ?? "absent";
951
+ if (required !== currentVersion) {
952
+ const missingToken = opts.createOnly !== true && opts.expectedVersion === undefined;
953
+ throw new CoordinationError(missingToken ? "coordination.expected-version-required" : "coordination.version-conflict", missingToken ? `snapshot ${snapshotPath} already exists — replace it with an explicit expectedVersion (its current version is ${currentVersion}) or write a new snapshot` : `snapshot ${snapshotPath} is at ${currentVersion}, writer required ${required}`, { path: snapshotPath, expected: required, actual: currentVersion });
954
+ }
955
+ const payload = current === undefined ? snapshot : mergePhaseProjection(current.payload, snapshot);
956
+ assertCoordinatedSnapshotWriter(current?.payload, snapshotPath, opts.sessionPath);
957
+ await withProtectedWrite(snapshotPath, "put", () => store.put({ kind: "snapshot", key: snapshot.id, payload }));
958
+ });
959
+ }
960
+ function assertCoordinatedSnapshotWriter(stored, snapshotPath, sessionPath, action = "replacement") {
961
+ const coordination = isPlainObject2(stored) ? stored.coordination : undefined;
962
+ if (coordination === undefined)
963
+ return;
964
+ const bound = isPlainObject2(coordination) && isPlainObject2(coordination.coordinator) ? coordination.coordinator.session_file : undefined;
965
+ if (sessionPath === undefined || typeof bound !== "string" || canonicalTarget(sessionPath) !== canonicalTarget(bound)) {
966
+ throw new CoordinationError("coordination.session-mismatch", `snapshot ${snapshotPath} is coordinated — ${action} requires --session <coordinator envelope>`, { path: snapshotPath, expected: bound, actual: sessionPath });
967
+ }
968
+ }
969
+ function mergePhaseProjection(stored, incoming) {
970
+ if (!isPlainObject2(stored)) {
971
+ throw new CoordinationError("coordination.version-conflict", "stored workflow snapshot is not an object — refusing a field-scoped rewrite over it", {});
972
+ }
973
+ const allowed = ["phase", "updated_at"];
974
+ const keys = new Set([...Object.keys(stored), ...Object.keys(incoming)]);
975
+ const drifted = [...keys].filter((key) => !allowed.includes(key) && stableJson(stored[key]) !== stableJson(incoming[key]));
976
+ if (drifted.length > 0) {
977
+ throw new CoordinationError("coordination.direct-write-refused", `refusing snapshot write: field(s) ${drifted.join(", ")} differ from disk — this writer may only change ${allowed.join(", ")}`, { fields: drifted, allowed });
978
+ }
979
+ const next = { ...stored };
980
+ for (const key of allowed) {
981
+ const value = incoming[key];
982
+ if (value === undefined)
983
+ delete next[key];
984
+ else
985
+ next[key] = value;
986
+ }
987
+ return next;
988
+ }
329
989
 
330
990
  // src/status.ts
331
991
  var DATE_RE = /^\d{4}-\d{2}-\d{2}$/;
332
- function isPlainObject(value) {
333
- return typeof value === "object" && value !== null && !Array.isArray(value);
334
- }
335
- function violation(severity, code, message, fix) {
992
+ var PLAN_STATUSES = ["Todo", "InProgress", "InReview", "Blocked", "Done"];
993
+ function violation3(severity, code, message, fix) {
336
994
  return { ok: false, severity, code, message, fix };
337
995
  }
338
996
  function todayString() {
@@ -341,12 +999,49 @@ function todayString() {
341
999
  const day = String(now.getDate()).padStart(2, "0");
342
1000
  return `${now.getFullYear()}-${month}-${day}`;
343
1001
  }
344
- function validateNonEmptyString(violations, value, field, missingCode, invalidCode) {
1002
+ function validateNonEmptyString3(violations, value, field, missingCode, invalidCode) {
345
1003
  if (value === undefined) {
346
- violations.push(violation("high", missingCode, `missing required field: ${field}`));
1004
+ violations.push(violation3("high", missingCode, `missing required field: ${field}`));
347
1005
  } else if (typeof value !== "string" || value.trim() === "") {
348
- violations.push(violation("medium", invalidCode, `${field} must be a non-empty string`));
1006
+ violations.push(violation3("medium", invalidCode, `${field} must be a non-empty string`));
1007
+ }
1008
+ }
1009
+ function validatePlanRow(row) {
1010
+ const violations = [];
1011
+ if (!isPlainObject2(row)) {
1012
+ return { ok: false, violations: [violation3("high", "status.plan-row.invalid", "plan row must be an object")] };
1013
+ }
1014
+ const { id, plan_id: planId, title, file, status, metadata, execution_lease } = row;
1015
+ if (id === undefined && planId === undefined) {
1016
+ violations.push(violation3("high", "status.plan-row.missing-id", "missing required field: id (or legacy plan_id)"));
1017
+ } else {
1018
+ if (id !== undefined) {
1019
+ validateNonEmptyString3(violations, id, "id", "status.plan-row.missing-id", "status.plan-row.invalid-id");
1020
+ }
1021
+ if (planId !== undefined) {
1022
+ validateNonEmptyString3(violations, planId, "plan_id", "status.plan-row.missing-plan-id", "status.plan-row.invalid-plan-id");
1023
+ }
1024
+ if (id !== undefined && planId !== undefined && id !== planId) {
1025
+ violations.push(violation3("medium", "status.plan-row.dual-id", "row has both id and plan_id with different values — write one canonical key (prefer id)"));
1026
+ }
1027
+ }
1028
+ validateNonEmptyString3(violations, title, "title", "status.plan-row.missing-title", "status.plan-row.invalid-title");
1029
+ validateNonEmptyString3(violations, file, "file", "status.plan-row.missing-file", "status.plan-row.invalid-file");
1030
+ if (status === undefined) {
1031
+ violations.push(violation3("high", "status.plan-row.missing-status", "missing required field: status"));
1032
+ } else if (typeof status !== "string" || !PLAN_STATUSES.includes(status)) {
1033
+ violations.push(violation3("medium", "status.plan-row.invalid-status", `status must be one of ${PLAN_STATUSES.join(" | ")} — got ${JSON.stringify(status)}`));
1034
+ }
1035
+ if (metadata !== undefined && !isPlainObject2(metadata)) {
1036
+ violations.push(violation3("medium", "status.plan-row.invalid-metadata", "metadata must be an object"));
1037
+ }
1038
+ if (execution_lease !== undefined && !isPlainObject2(execution_lease)) {
1039
+ violations.push(violation3("medium", "status.plan-row.invalid-execution-lease", "execution_lease must be an object"));
1040
+ }
1041
+ if (status === "Done" && execution_lease !== undefined) {
1042
+ violations.push(violation3("medium", "status.plan-row.done-with-lease", 'plan status Done must not carry an execution_lease — the Done authority deletes the lease in the same complete-file update as status: "Done" (status-and-residuals.md § Hold, release, and override)', 'delete plans[].execution_lease in the same update that sets status: "Done"'));
349
1043
  }
1044
+ return { ok: violations.length === 0, violations };
350
1045
  }
351
1046
  function isHarnessRelativePath(dir) {
352
1047
  if (dir.startsWith("/") || dir.startsWith("\\"))
@@ -357,25 +1052,25 @@ function isHarnessRelativePath(dir) {
357
1052
  }
358
1053
  function validateWorkflowEntry(entry) {
359
1054
  const violations = [];
360
- if (!isPlainObject(entry)) {
1055
+ if (!isPlainObject2(entry)) {
361
1056
  return {
362
1057
  ok: false,
363
- violations: [violation("high", "status.workflow.invalid", "workflow entry must be an object")]
1058
+ violations: [violation3("high", "status.workflow.invalid", "workflow entry must be an object")]
364
1059
  };
365
1060
  }
366
- validateNonEmptyString(violations, entry.id, "id", "status.workflow.missing-id", "status.workflow.invalid-id");
1061
+ validateNonEmptyString3(violations, entry.id, "id", "status.workflow.missing-id", "status.workflow.invalid-id");
367
1062
  if (entry.type === undefined) {
368
- violations.push(violation("high", "status.workflow.missing-type", "missing required field: type"));
1063
+ violations.push(violation3("high", "status.workflow.missing-type", "missing required field: type"));
369
1064
  } else if (typeof entry.type !== "string" || !WORKFLOW_LIFECYCLE_TYPES.includes(entry.type)) {
370
- violations.push(violation("medium", "status.workflow.invalid-type", `type must be one of ${WORKFLOW_LIFECYCLE_TYPES.join(" | ")} — got ${JSON.stringify(entry.type)}`));
1065
+ violations.push(violation3("medium", "status.workflow.invalid-type", `type must be one of ${WORKFLOW_LIFECYCLE_TYPES.join(" | ")} — got ${JSON.stringify(entry.type)}`));
371
1066
  }
372
- validateNonEmptyString(violations, entry.started_at, "started_at", "status.workflow.missing-started-at", "status.workflow.invalid-started-at");
1067
+ validateNonEmptyString3(violations, entry.started_at, "started_at", "status.workflow.missing-started-at", "status.workflow.invalid-started-at");
373
1068
  if (entry.dir === undefined) {
374
- violations.push(violation("high", "status.workflow.missing-dir", "missing required field: dir"));
1069
+ violations.push(violation3("high", "status.workflow.missing-dir", "missing required field: dir"));
375
1070
  } else if (typeof entry.dir !== "string" || entry.dir.trim() === "") {
376
- violations.push(violation("medium", "status.workflow.invalid-dir", "dir must be a non-empty string"));
1071
+ violations.push(violation3("medium", "status.workflow.invalid-dir", "dir must be a non-empty string"));
377
1072
  } else if (!isHarnessRelativePath(entry.dir)) {
378
- violations.push(violation("medium", "status.workflow.invalid-dir", `dir must be a harness-relative path (no absolute paths, no ".." segments) — got ${JSON.stringify(entry.dir)}`));
1073
+ violations.push(violation3("medium", "status.workflow.invalid-dir", `dir must be a harness-relative path (no absolute paths, no ".." segments) — got ${JSON.stringify(entry.dir)}`));
379
1074
  }
380
1075
  return { ok: violations.length === 0, violations };
381
1076
  }
@@ -385,24 +1080,24 @@ function validateStatusV2(docOrPath, opts = {}) {
385
1080
  if (typeof docOrPath === "string") {
386
1081
  try {
387
1082
  doc = readJson(docOrPath);
388
- harnessDir = dirname4(resolve5(docOrPath));
1083
+ harnessDir = dirname5(resolve6(docOrPath));
389
1084
  } catch (error) {
390
1085
  return {
391
1086
  ok: false,
392
- violations: [violation("high", "status.invalid-json", error.message)]
1087
+ violations: [violation3("high", "status.invalid-json", error.message)]
393
1088
  };
394
1089
  }
395
1090
  } else {
396
1091
  doc = docOrPath;
397
1092
  }
398
- if (!isPlainObject(doc)) {
399
- return { ok: false, violations: [violation("high", "status.invalid-doc", "status document must be an object")] };
1093
+ if (!isPlainObject2(doc)) {
1094
+ return { ok: false, violations: [violation3("high", "status.invalid-doc", "status document must be an object")] };
400
1095
  }
401
1096
  if (doc.version !== 2) {
402
1097
  return {
403
1098
  ok: false,
404
1099
  violations: [
405
- violation("high", "status.migration-required", `status.json schema version 2 required — got ${JSON.stringify(doc.version)} (v1 or unknown version); run \`mstar migrate\` to convert the tree`, "run `mstar migrate`")
1100
+ violation3("high", "status.migration-required", `status.json schema version 2 required — got ${JSON.stringify(doc.version)} (v1 or unknown version); run \`mstar migrate\` to convert the tree`, "run `mstar migrate`")
406
1101
  ]
407
1102
  };
408
1103
  }
@@ -410,7 +1105,7 @@ function validateStatusV2(docOrPath, opts = {}) {
410
1105
  return {
411
1106
  ok: false,
412
1107
  violations: [
413
- violation("high", "status.migration-required", "v1-shaped status.json (root plans[]) is not a v2 document — run `mstar migrate` to convert the tree", "run `mstar migrate`")
1108
+ violation3("high", "status.migration-required", "v1-shaped status.json (root plans[]) is not a v2 document — run `mstar migrate` to convert the tree", "run `mstar migrate`")
414
1109
  ]
415
1110
  };
416
1111
  }
@@ -418,27 +1113,27 @@ function validateStatusV2(docOrPath, opts = {}) {
418
1113
  return {
419
1114
  ok: false,
420
1115
  violations: [
421
- violation("high", "status.migration-required", "v1-shaped status.json (root residual_findings) is not a v2 document — run `mstar migrate` to convert the tree", "run `mstar migrate`")
1116
+ violation3("high", "status.migration-required", "v1-shaped status.json (root residual_findings) is not a v2 document — run `mstar migrate` to convert the tree", "run `mstar migrate`")
422
1117
  ]
423
1118
  };
424
1119
  }
425
1120
  const violations = [];
426
1121
  if (doc.updated_at === undefined) {
427
- violations.push(violation("high", "status.missing-updated-at", "missing required field: updated_at"));
1122
+ violations.push(violation3("high", "status.missing-updated-at", "missing required field: updated_at"));
428
1123
  } else if (typeof doc.updated_at !== "string" || !DATE_RE.test(doc.updated_at)) {
429
- violations.push(violation("medium", "status.invalid-updated-at", "updated_at must be YYYY-MM-DD"));
1124
+ violations.push(violation3("medium", "status.invalid-updated-at", "updated_at must be YYYY-MM-DD"));
430
1125
  }
431
1126
  if (doc.workflows === undefined) {
432
- violations.push(violation("high", "status.missing-workflows", "missing required field: workflows"));
1127
+ violations.push(violation3("high", "status.missing-workflows", "missing required field: workflows"));
433
1128
  } else if (!Array.isArray(doc.workflows)) {
434
- violations.push(violation("high", "status.invalid-workflows", "workflows must be an array"));
1129
+ violations.push(violation3("high", "status.invalid-workflows", "workflows must be an array"));
435
1130
  } else {
436
1131
  const seen = new Set;
437
1132
  for (const entry of doc.workflows) {
438
1133
  violations.push(...validateWorkflowEntry(entry).violations);
439
- if (isPlainObject(entry) && typeof entry.id === "string") {
1134
+ if (isPlainObject2(entry) && typeof entry.id === "string") {
440
1135
  if (seen.has(entry.id)) {
441
- violations.push(violation("medium", "status.workflow.duplicate-id", `duplicate workflow id in workflows[]: ${JSON.stringify(entry.id)}`));
1136
+ violations.push(violation3("medium", "status.workflow.duplicate-id", `duplicate workflow id in workflows[]: ${JSON.stringify(entry.id)}`));
442
1137
  }
443
1138
  seen.add(entry.id);
444
1139
  }
@@ -447,47 +1142,47 @@ function validateStatusV2(docOrPath, opts = {}) {
447
1142
  if (harnessDir !== undefined && Array.isArray(doc.workflows)) {
448
1143
  let realHarnessDir = null;
449
1144
  try {
450
- realHarnessDir = realpathSync(harnessDir);
1145
+ realHarnessDir = realpathSync2(harnessDir);
451
1146
  } catch {}
452
1147
  for (const entry of doc.workflows) {
453
- if (!isPlainObject(entry) || typeof entry.dir !== "string")
1148
+ if (!isPlainObject2(entry) || typeof entry.dir !== "string")
454
1149
  continue;
455
- const relSnapshot = join5(entry.dir, WORKFLOW_SNAPSHOT_FILE);
456
- const snapshotPath = join5(harnessDir, relSnapshot);
1150
+ const relSnapshot = join7(entry.dir, WORKFLOW_SNAPSHOT_FILE);
1151
+ const snapshotPath = join7(harnessDir, relSnapshot);
457
1152
  const label = typeof entry.id === "string" ? entry.id : relSnapshot;
458
1153
  let physical;
459
1154
  try {
460
- physical = realpathSync(snapshotPath);
1155
+ physical = realpathSync2(snapshotPath);
461
1156
  } catch {
462
- violations.push(violation("high", "status.workflow.snapshot-missing", `workflows[] lists ${JSON.stringify(label)} but its snapshot does not exist at ${JSON.stringify(relSnapshot)} — the root holds active lifecycles only; unregister the id when its snapshot is removed`));
1157
+ violations.push(violation3("high", "status.workflow.snapshot-missing", `workflows[] lists ${JSON.stringify(label)} but its snapshot does not exist at ${JSON.stringify(relSnapshot)} — the root holds active lifecycles only; unregister the id when its snapshot is removed`));
463
1158
  continue;
464
1159
  }
465
- if (realHarnessDir !== null && physical !== realHarnessDir && !physical.startsWith(`${realHarnessDir}${sep}`)) {
466
- violations.push(violation("high", "status.workflow.snapshot-outside-harness", `workflows[] lists ${JSON.stringify(label)} but its snapshot resolves outside the harness dir (${JSON.stringify(physical)}) — symlinked snapshot paths are rejected; the snapshot must physically live under ${JSON.stringify(harnessDir)}`));
1160
+ if (realHarnessDir !== null && physical !== realHarnessDir && !physical.startsWith(`${realHarnessDir}${sep2}`)) {
1161
+ violations.push(violation3("high", "status.workflow.snapshot-outside-harness", `workflows[] lists ${JSON.stringify(label)} but its snapshot resolves outside the harness dir (${JSON.stringify(physical)}) — symlinked snapshot paths are rejected; the snapshot must physically live under ${JSON.stringify(harnessDir)}`));
467
1162
  continue;
468
1163
  }
469
1164
  let snapshot;
470
1165
  try {
471
1166
  snapshot = readJson(snapshotPath);
472
1167
  } catch (error) {
473
- violations.push(violation("high", "status.workflow.snapshot-invalid", `snapshot at ${JSON.stringify(relSnapshot)} is not valid JSON: ${error.message}`));
1168
+ violations.push(violation3("high", "status.workflow.snapshot-invalid", `snapshot at ${JSON.stringify(relSnapshot)} is not valid JSON: ${error.message}`));
474
1169
  continue;
475
1170
  }
476
1171
  if (typeof snapshot.status === "string" && WORKFLOW_TERMINAL_STATUSES.includes(snapshot.status)) {
477
- violations.push(violation("high", "status.workflow.terminal-listed", `workflows[] lists ${JSON.stringify(label)} whose snapshot status is terminal (${snapshot.status}) — removal-at-terminal: terminal writers unregister AFTER the snapshot write`));
1172
+ violations.push(violation3("high", "status.workflow.terminal-listed", `workflows[] lists ${JSON.stringify(label)} whose snapshot status is terminal (${snapshot.status}) — removal-at-terminal: terminal writers unregister AFTER the snapshot write`));
478
1173
  }
479
1174
  if (typeof entry.type === "string" && typeof snapshot.type === "string" && entry.type !== snapshot.type) {
480
- violations.push(violation("medium", "status.workflow.mismatched-type", `workflows[] entry ${JSON.stringify(label)} type ${JSON.stringify(entry.type)} does not match its snapshot type ${JSON.stringify(snapshot.type)} — the root entry mirrors the snapshot; align them`));
1175
+ violations.push(violation3("medium", "status.workflow.mismatched-type", `workflows[] entry ${JSON.stringify(label)} type ${JSON.stringify(entry.type)} does not match its snapshot type ${JSON.stringify(snapshot.type)} — the root entry mirrors the snapshot; align them`));
481
1176
  }
482
1177
  if (typeof entry.started_at === "string" && typeof snapshot.started_at === "string" && entry.started_at !== snapshot.started_at) {
483
- violations.push(violation("medium", "status.workflow.mismatched-started-at", `workflows[] entry ${JSON.stringify(label)} started_at ${JSON.stringify(entry.started_at)} does not match its snapshot started_at ${JSON.stringify(snapshot.started_at)} — workflow ${JSON.stringify(label)} collided with another writer (e.g. a concurrent/re-run \`audit promote\` with the same workflow id rewrote the snapshot); the root entry mirrors the snapshot — align them or remove the colliding workflow`));
1178
+ violations.push(violation3("medium", "status.workflow.mismatched-started-at", `workflows[] entry ${JSON.stringify(label)} started_at ${JSON.stringify(entry.started_at)} does not match its snapshot started_at ${JSON.stringify(snapshot.started_at)} — workflow ${JSON.stringify(label)} collided with another writer (e.g. a concurrent/re-run \`audit promote\` with the same workflow id rewrote the snapshot); the root entry mirrors the snapshot — align them or remove the colliding workflow`));
484
1179
  }
485
1180
  }
486
1181
  }
487
1182
  return { ok: violations.length === 0, violations };
488
1183
  }
489
1184
  async function registerWorkflowEntryLocked(statusPath, entry) {
490
- const harnessDir = dirname4(statusPath);
1185
+ const harnessDir = dirname5(statusPath);
491
1186
  const store = getArtifactStore();
492
1187
  assertFsStorePath(store, { kind: "status", key: "root" }, statusPath);
493
1188
  const current = readJson(statusPath);
@@ -497,41 +1192,72 @@ async function registerWorkflowEntryLocked(statusPath, entry) {
497
1192
  throw new Error("refusing to modify status.json: workflows must be an array — a v1 root must be migrated first (run `mstar migrate`)");
498
1193
  }
499
1194
  const existing = doc.workflows.findIndex((wf) => wf.id === entry.id);
500
- if (existing >= 0) {
501
- doc.workflows[existing] = entry;
502
- } else {
1195
+ const commit = async () => {
1196
+ doc.updated_at = todayString();
1197
+ const gate = validateStatusV2(doc, { harnessDir });
1198
+ if (!gate.ok) {
1199
+ throw new Error(`refusing to write invalid status.json: ${gate.violations.map((v) => v.message).join("; ")}`);
1200
+ }
1201
+ await withProtectedWrite(statusPath, "put", () => store.put({ kind: "status", key: "root", payload: doc }));
1202
+ return doc;
1203
+ };
1204
+ if (existing < 0) {
503
1205
  doc.workflows.push(entry);
1206
+ return commit();
1207
+ }
1208
+ const prior = doc.workflows[existing];
1209
+ return withRegisteredSnapshotLock(harnessDir, prior, async (snapshot) => {
1210
+ const priorCoordination = snapshot?.coordination;
1211
+ if (isPlainObject2(priorCoordination) && isPlainObject2(priorCoordination.coordinator)) {
1212
+ const drifted = ["dir", "type", "started_at"].filter((field) => prior[field] !== entry[field]);
1213
+ if (drifted.length > 0) {
1214
+ throw new CoordinationError("coordination.invalid-transition", `refusing to re-register workflow ${JSON.stringify(entry.id)}: it is coordinated and its ${drifted.join("/")} must not change`, { workflow_id: entry.id, fields: [...drifted] });
1215
+ }
1216
+ }
1217
+ doc.workflows[existing] = entry;
1218
+ return commit();
1219
+ });
1220
+ }
1221
+ async function withRegisteredSnapshotLock(harnessDir, entry, fn) {
1222
+ if (typeof entry.dir !== "string")
1223
+ return fn(undefined);
1224
+ const snapshotPath = join7(harnessDir, entry.dir, WORKFLOW_SNAPSHOT_FILE);
1225
+ if (!existsSync5(snapshotPath))
1226
+ return fn(undefined);
1227
+ return withStatusWriteLock(snapshotPath, () => fn(readRegisteredSnapshot(snapshotPath)));
1228
+ }
1229
+ function readRegisteredSnapshot(snapshotPath) {
1230
+ if (!existsSync5(snapshotPath))
1231
+ return;
1232
+ try {
1233
+ const parsed = JSON.parse(readFileSync5(snapshotPath, "utf8"));
1234
+ return isPlainObject2(parsed) ? parsed : undefined;
1235
+ } catch {
1236
+ return;
504
1237
  }
505
- doc.updated_at = todayString();
506
- const gate = validateStatusV2(doc, { harnessDir });
507
- if (!gate.ok) {
508
- throw new Error(`refusing to write invalid status.json: ${gate.violations.map((v) => v.message).join("; ")}`);
509
- }
510
- await store.put({ kind: "status", key: "root", payload: doc });
511
- return doc;
512
1238
  }
513
1239
 
514
1240
  // src/path.ts
515
1241
  function resolveHarnessDir(startDir = process.cwd(), opts = {}) {
516
- const start = resolve6(startDir);
1242
+ const start = resolve7(startDir);
517
1243
  const explicit = opts.harnessDir ?? process.env.MSTAR_HARNESS_DIR;
518
1244
  if (explicit)
519
- return resolve6(start, explicit);
520
- const boundary = resolve6(start, opts.workspaceRoot ?? defaultWorkspaceRoot(start));
1245
+ return resolve7(start, explicit);
1246
+ const boundary = resolve7(start, opts.workspaceRoot ?? defaultWorkspaceRoot(start));
521
1247
  const rc = loadMstarc(start, boundary);
522
1248
  if (rc !== null && rc.config.harnessDir)
523
- return resolve6(rc.dir, rc.config.harnessDir);
1249
+ return resolve7(rc.dir, rc.config.harnessDir);
524
1250
  let dir = start;
525
1251
  for (;; ) {
526
1252
  if (!isAtOrBelow2(dir, boundary))
527
1253
  return null;
528
- for (const candidate of [join6(dir, ".mstar"), join6(dir, ".agents"), join6(dir, ".plans"), join6(dir, "plans")]) {
1254
+ for (const candidate of [join8(dir, ".mstar"), join8(dir, ".agents"), join8(dir, ".plans"), join8(dir, "plans")]) {
529
1255
  if (isDirectory(candidate))
530
1256
  return candidate;
531
1257
  }
532
1258
  if (dir === boundary)
533
1259
  return null;
534
- const parent = dirname5(dir);
1260
+ const parent = dirname6(dir);
535
1261
  if (parent === dir)
536
1262
  return null;
537
1263
  dir = parent;
@@ -549,21 +1275,21 @@ function defaultWorkspaceRoot(startDir) {
549
1275
  let boundary = startDir;
550
1276
  for (const segment of cdup.split(/[\\/]/)) {
551
1277
  if (segment && segment !== ".")
552
- boundary = dirname5(boundary);
1278
+ boundary = dirname6(boundary);
553
1279
  }
554
- return resolve6(boundary);
1280
+ return resolve7(boundary);
555
1281
  } catch {}
556
1282
  return startDir;
557
1283
  }
558
1284
  function isAtOrBelow2(dir, root) {
559
1285
  const rel = relative2(root, dir);
560
- return rel === "" || !rel.startsWith("..") && !isAbsolute4(rel);
1286
+ return rel === "" || !rel.startsWith("..") && !isAbsolute6(rel);
561
1287
  }
562
1288
  function mstarcDirOverride(harnessDir, key) {
563
- const dir = resolve6(harnessDir);
564
- const rc = loadMstarc(dir, dirname5(dir));
1289
+ const dir = resolve7(harnessDir);
1290
+ const rc = loadMstarc(dir, dirname6(dir));
565
1291
  const declared = rc?.config[key];
566
- return declared ? resolve6(rc.dir, declared) : null;
1292
+ return declared ? resolve7(rc.dir, declared) : null;
567
1293
  }
568
1294
  function assertSafePathComponent(value, what) {
569
1295
  if (value === "" || value === "." || value === ".." || !/^[A-Za-z0-9._-]+$/.test(value)) {
@@ -573,10 +1299,10 @@ function assertSafePathComponent(value, what) {
573
1299
  function resolveHarnessSubdir(startDir, opts, key, fallback) {
574
1300
  const harness = resolveHarnessDir(startDir, opts);
575
1301
  if (harness === null) {
576
- throw new Error(`harness dir not found from ${resolve6(startDir)} — cannot resolve the ${fallback} dir (run \`mstar harness scaffold\`, pass opts.harnessDir, or set MSTAR_HARNESS_DIR)`);
1302
+ throw new Error(`harness dir not found from ${resolve7(startDir)} — cannot resolve the ${fallback} dir (run \`mstar harness scaffold\`, pass opts.harnessDir, or set MSTAR_HARNESS_DIR)`);
577
1303
  }
578
1304
  const declared = mstarcDirOverride(harness, key);
579
- return declared !== null ? declared : join6(resolve6(harness), fallback);
1305
+ return declared !== null ? declared : join8(resolve7(harness), fallback);
580
1306
  }
581
1307
  function resolveWorkflowDir(startDir = process.cwd(), opts = {}) {
582
1308
  return resolveHarnessSubdir(startDir, opts, "workflowDir", "workflows");
@@ -618,7 +1344,7 @@ function isDirectory(dir) {
618
1344
  }
619
1345
 
620
1346
  // src/audit.ts
621
- function violation2(severity, code, message, fix) {
1347
+ function violation4(severity, code, message, fix) {
622
1348
  return { ok: false, severity, code, message, fix };
623
1349
  }
624
1350
  var AUDIT_PRIORITIES = ["P1", "P2", "P3"];
@@ -663,14 +1389,14 @@ function validateAuditStatusBlocks(planText) {
663
1389
  const violations = [];
664
1390
  const blocks = parseStatusBlocks(planText);
665
1391
  if (blocks.length === 0) {
666
- violations.push(violation2("medium", "audit.status.missing-block", "no `## Status` block found — audit plan files carry the Status block fields (mstar-audit SKILL.md § Plan output)", "add a `## Status` block with Priority, Effort, Risk, Depends on, Category, Planned at"));
1392
+ violations.push(violation4("medium", "audit.status.missing-block", "no `## Status` block found — audit plan files carry the Status block fields (mstar-audit SKILL.md § Plan output)", "add a `## Status` block with Priority, Effort, Risk, Depends on, Category, Planned at"));
667
1393
  return { ok: false, violations };
668
1394
  }
669
1395
  blocks.forEach((block, index) => {
670
1396
  const label = blocks.length > 1 ? ` #${index + 1}` : "";
671
1397
  for (const field of AUDIT_STATUS_FIELDS) {
672
1398
  if (!block.fields.has(field)) {
673
- violations.push(violation2("medium", "audit.status.missing-field", `Status block${label} missing required field "${field}" (mstar-audit SKILL.md § Plan output)`, `add \`- **${field}**: <value>\` to the Status block`));
1399
+ violations.push(violation4("medium", "audit.status.missing-field", `Status block${label} missing required field "${field}" (mstar-audit SKILL.md § Plan output)`, `add \`- **${field}**: <value>\` to the Status block`));
674
1400
  }
675
1401
  }
676
1402
  const check = (field, pattern, code, expected) => {
@@ -678,7 +1404,7 @@ function validateAuditStatusBlocks(planText) {
678
1404
  if (value === undefined)
679
1405
  return;
680
1406
  if (!pattern.test(value)) {
681
- violations.push(violation2("medium", code, `Status block${label} "${field}" = "${value}" — expected ${expected} (mstar-audit SKILL.md § Plan output)`, `fix \`- **${field}**:\` to one of: ${expected}`));
1407
+ violations.push(violation4("medium", code, `Status block${label} "${field}" = "${value}" — expected ${expected} (mstar-audit SKILL.md § Plan output)`, `fix \`- **${field}**:\` to one of: ${expected}`));
682
1408
  }
683
1409
  };
684
1410
  check("Priority", /^P[123]$/, "audit.status.invalid-priority", "P1 | P2 | P3");
@@ -913,12 +1639,12 @@ function scanSecrets(files) {
913
1639
  for (const file of files) {
914
1640
  let text;
915
1641
  try {
916
- text = readFileSync5(file, "utf8");
1642
+ text = readFileSync7(file, "utf8");
917
1643
  } catch {
918
1644
  unreadableFiles++;
919
1645
  continue;
920
1646
  }
921
- const base = basename3(file);
1647
+ const base = basename5(file);
922
1648
  for (const entry of NEVER_COMMIT_FILENAMES) {
923
1649
  if (entry.re.test(base))
924
1650
  findings.push({ file, line: 1, type: entry.type });
@@ -975,7 +1701,7 @@ function rootLockfiles(root) {
975
1701
  return [];
976
1702
  }
977
1703
  const names = new Set(LOCKFILE_NAMES);
978
- const present = entries.filter((entry) => entry.isFile() && names.has(entry.name)).map((entry) => join7(root, entry.name));
1704
+ const present = entries.filter((entry) => entry.isFile() && names.has(entry.name)).map((entry) => join9(root, entry.name));
979
1705
  if (present.length === 0)
980
1706
  return [];
981
1707
  try {
@@ -984,7 +1710,7 @@ function rootLockfiles(root) {
984
1710
  encoding: "utf8",
985
1711
  stdio: ["ignore", "pipe", "ignore"]
986
1712
  }).split("\x00").filter((f) => f !== ""));
987
- return present.filter((p) => tracked.has(basename3(p)));
1713
+ return present.filter((p) => tracked.has(basename5(p)));
988
1714
  } catch {
989
1715
  return present;
990
1716
  }
@@ -995,12 +1721,12 @@ function supplyChainChecks(repoRoot) {
995
1721
  const lockfiles = rootLockfiles(repoRoot);
996
1722
  if (lockfiles.length === 0) {
997
1723
  findings.push({ kind: "lockfile-missing", file: repoRoot });
998
- violations.push(violation2("medium", "audit.supply.lockfile-missing", `no recognized lockfile at ${repoRoot}`, "commit a lockfile (package-lock.json, pnpm-lock.yaml, yarn.lock, bun.lock, …)"));
1724
+ violations.push(violation4("medium", "audit.supply.lockfile-missing", `no recognized lockfile at ${repoRoot}`, "commit a lockfile (package-lock.json, pnpm-lock.yaml, yarn.lock, bun.lock, …)"));
999
1725
  } else if (lockfiles.length > 1) {
1000
1726
  findings.push({ kind: "lockfile-duplicate", file: lockfiles.map((f) => f.replace(`${repoRoot}/`, "")).join(", ") });
1001
- violations.push(violation2("medium", "audit.supply.lockfile-duplicate", `multiple lockfiles at ${repoRoot}: ${lockfiles.join(", ")}`, "keep exactly one lockfile per package manager"));
1727
+ violations.push(violation4("medium", "audit.supply.lockfile-duplicate", `multiple lockfiles at ${repoRoot}: ${lockfiles.join(", ")}`, "keep exactly one lockfile per package manager"));
1002
1728
  }
1003
- const workflowsDir = join7(repoRoot, ".github", "workflows");
1729
+ const workflowsDir = join9(repoRoot, ".github", "workflows");
1004
1730
  let wfEntries = [];
1005
1731
  try {
1006
1732
  wfEntries = readdirSync4(workflowsDir, { withFileTypes: true });
@@ -1010,11 +1736,11 @@ function supplyChainChecks(repoRoot) {
1010
1736
  for (const entry of wfEntries) {
1011
1737
  if (!entry.isFile() || !/\.(?:ya?ml)$/.test(entry.name))
1012
1738
  continue;
1013
- const wfPath = join7(workflowsDir, entry.name);
1739
+ const wfPath = join9(workflowsDir, entry.name);
1014
1740
  const relPath = `.github/workflows/${entry.name}`;
1015
1741
  let text;
1016
1742
  try {
1017
- text = readFileSync5(wfPath, "utf8");
1743
+ text = readFileSync7(wfPath, "utf8");
1018
1744
  } catch {
1019
1745
  continue;
1020
1746
  }
@@ -1054,12 +1780,12 @@ function supplyChainChecks(repoRoot) {
1054
1780
  const versionLike = /^v\d+(?:\.\d+)*$/.test(ref);
1055
1781
  if (!shaLike && !versionLike) {
1056
1782
  findings.push({ kind: "action-unpinned", file: relPath, line: i + 1 });
1057
- violations.push(violation2("high", "audit.supply.action-unpinned", `${relPath}:${i + 1} uses \`${uses[1]}@${ref}\` — mutable ref`, "pin the action to a full commit SHA"));
1783
+ violations.push(violation4("high", "audit.supply.action-unpinned", `${relPath}:${i + 1} uses \`${uses[1]}@${ref}\` — mutable ref`, "pin the action to a full commit SHA"));
1058
1784
  }
1059
1785
  }
1060
1786
  if (hasPrt && prtHeadSteps.has(i)) {
1061
1787
  findings.push({ kind: "pull_request_target-head", file: relPath, line: i + 1 });
1062
- violations.push(violation2("high", "audit.supply.pull_request_target-head", `${relPath}:${i + 1} checks out the PR head under pull_request_target`, "check out the base ref or use a pull_request trigger for untrusted code"));
1788
+ violations.push(violation4("high", "audit.supply.pull_request_target-head", `${relPath}:${i + 1} checks out the PR head under pull_request_target`, "check out the base ref or use a pull_request trigger for untrusted code"));
1063
1789
  }
1064
1790
  }
1065
1791
  }
@@ -1112,7 +1838,7 @@ function redactFinding(finding) {
1112
1838
  };
1113
1839
  }
1114
1840
  function readPlanFileSummary(filePath) {
1115
- const text = readFileSync5(filePath, "utf8");
1841
+ const text = readFileSync7(filePath, "utf8");
1116
1842
  const title = (text.match(/^# (.+)$/m) ?? [])[1] ?? filePath;
1117
1843
  const blocks = parseStatusBlocks(text);
1118
1844
  return { title: title.trim(), fields: blocks.length > 0 ? blocks[0].fields : new Map };
@@ -1166,9 +1892,9 @@ function renderIndex(params) {
1166
1892
  function scaffoldAuditPlan(outDir, findings, options = {}) {
1167
1893
  const date = options.date ?? new Date().toISOString().slice(0, 10);
1168
1894
  const plannedAt = options.plannedAt ?? { commit: options.repoShortSha ?? "unknown", date };
1169
- mkdirSync4(outDir, { recursive: true });
1170
- const existingReadme = join7(outDir, "README.md");
1171
- const carried = existsSync5(existingReadme) ? extractSecurityDispositionSections(readFileSync5(existingReadme, "utf8")) : { needsVerification: [], hardeningChecked: [] };
1895
+ mkdirSync5(outDir, { recursive: true });
1896
+ const existingReadme = join9(outDir, "README.md");
1897
+ const carried = existsSync7(existingReadme) ? extractSecurityDispositionSections(readFileSync7(existingReadme, "utf8")) : { needsVerification: [], hardeningChecked: [] };
1172
1898
  const existing = readdirSync4(outDir).filter((f) => /^\d{3}-.*\.md$/.test(f));
1173
1899
  let next = existing.reduce((max, f) => Math.max(max, Number(f.slice(0, 3))), 0) + 1;
1174
1900
  const redactedFindings = findings.map(redactFinding);
@@ -1185,13 +1911,13 @@ function scaffoldAuditPlan(outDir, findings, options = {}) {
1185
1911
  }
1186
1912
  usedSlugs.add(slug);
1187
1913
  const file = `${num}-${slug}.md`;
1188
- writeFileSync4(join7(outDir, file), renderPlanFile(finding, plannedAt));
1914
+ writeFileSync4(join9(outDir, file), renderPlanFile(finding, plannedAt));
1189
1915
  written.push(file);
1190
1916
  next++;
1191
1917
  }
1192
1918
  const all = [...existing, ...written].sort();
1193
1919
  const rows = all.map((file) => {
1194
- const summary = readPlanFileSummary(join7(outDir, file));
1920
+ const summary = readPlanFileSummary(join9(outDir, file));
1195
1921
  const fields = summary.fields;
1196
1922
  return {
1197
1923
  num: file.slice(0, 3),
@@ -1225,7 +1951,7 @@ function scaffoldAuditPlan(outDir, findings, options = {}) {
1225
1951
  });
1226
1952
  const needsVerificationLines = options.needsVerification !== undefined ? options.needsVerification.map((nv) => `- ${escapeCell(redactText(nv.lead))}: ${escapeCell(redactText(nv.how))}${nv.evidence ? ` (${escapeCell(redactText(nv.evidence))})` : ""}`) : carried.needsVerification;
1227
1953
  const hardeningCheckedLines = options.hardeningChecked !== undefined ? options.hardeningChecked.map((hc) => `- ${hc.kind}: ${escapeCell(redactText(hc.text))}`) : carried.hardeningChecked;
1228
- writeFileSync4(join7(outDir, "README.md"), renderIndex({
1954
+ writeFileSync4(join9(outDir, "README.md"), renderIndex({
1229
1955
  date,
1230
1956
  repoName: options.repoName ?? "repo",
1231
1957
  repoShortSha: options.repoShortSha ?? "unknown",
@@ -1234,7 +1960,7 @@ function scaffoldAuditPlan(outDir, findings, options = {}) {
1234
1960
  needsVerification: needsVerificationLines,
1235
1961
  hardeningChecked: hardeningCheckedLines
1236
1962
  }));
1237
- return { outDir: resolve7(outDir), date, files: written, nextNumber: next };
1963
+ return { outDir: resolve8(outDir), date, files: written, nextNumber: next };
1238
1964
  }
1239
1965
  async function promoteAuditPlans(outDir, selected, options) {
1240
1966
  if (selected.length === 0) {
@@ -1243,19 +1969,20 @@ async function promoteAuditPlans(outDir, selected, options) {
1243
1969
  if (typeof options.harnessDir !== "string" || options.harnessDir.trim() === "") {
1244
1970
  throw new Error("promoteAuditPlans: options.harnessDir is required (must contain status.json + workflows/)");
1245
1971
  }
1246
- const workflowId = options.workflowId ?? basename3(resolve7(outDir));
1972
+ const workflowId = options.workflowId ?? basename5(resolve8(outDir));
1247
1973
  assertSafePathComponent(workflowId, "workflow id");
1248
- const harnessDir = resolve7(options.harnessDir);
1249
- const statusPath = join7(harnessDir, "status.json");
1250
- const workflowDir = join7(harnessDir, "workflows", workflowId);
1251
- const snapshotPath = join7(workflowDir, WORKFLOW_SNAPSHOT_FILE);
1974
+ const harnessDir = resolve8(options.harnessDir);
1975
+ const statusPath = join9(harnessDir, "status.json");
1976
+ const workflowDir = join9(harnessDir, "workflows", workflowId);
1977
+ const snapshotPath = join9(workflowDir, WORKFLOW_SNAPSHOT_FILE);
1978
+ const store = getArtifactStore();
1252
1979
  const planFiles = resolveSelectedPlanFiles(outDir, selected);
1253
1980
  const indexRows = readExecutionOrderIndex(outDir);
1254
1981
  const plans = planFiles.map((planFile) => {
1255
1982
  const stem = planFile.replace(/\.md$/, "");
1256
1983
  const num = stem.slice(0, 3);
1257
1984
  const indexRow = indexRows.get(num);
1258
- const title = indexRow?.title ?? readPlanFileSummary(join7(outDir, planFile)).title;
1985
+ const title = indexRow?.title ?? readPlanFileSummary(join9(outDir, planFile)).title;
1259
1986
  return {
1260
1987
  id: stem,
1261
1988
  title,
@@ -1284,15 +2011,26 @@ async function promoteAuditPlans(outDir, selected, options) {
1284
2011
  throw new Error(`refusing to register invalid workflow entry: ${entryGate.violations.map((v) => v.message).join("; ")}`);
1285
2012
  }
1286
2013
  await withStatusWriteLock(statusPath, async () => {
1287
- if (existsSync5(snapshotPath)) {
2014
+ if (existsSync7(snapshotPath)) {
1288
2015
  throw new Error(`refusing to promote audit plans: workflow ${JSON.stringify(workflowId)} already exists ` + `(snapshot at ${snapshotPath}) — re-promote would drop its registered plan rows; ` + `remove that workflow before promoting again`);
1289
2016
  }
1290
- mkdirSync4(workflowDir, { recursive: true });
2017
+ let createdVersion;
1291
2018
  try {
1292
- writeJson(snapshotPath, snapshot);
2019
+ await writeWorkflowSnapshot(snapshot, workflowDir, { createOnly: true });
2020
+ createdVersion = readArtifactBytes(snapshotPath)?.version;
1293
2021
  await registerWorkflowEntryLocked(statusPath, entry);
1294
2022
  } catch (error) {
1295
- rmSync(snapshotPath, { force: true });
2023
+ if (createdVersion !== undefined) {
2024
+ await withStatusWriteLock(snapshotPath, async () => {
2025
+ const current = readArtifactBytes(snapshotPath);
2026
+ if (current === undefined || current.version !== createdVersion)
2027
+ return;
2028
+ const remove = store.delete?.bind(store);
2029
+ if (remove !== undefined) {
2030
+ await withProtectedWrite(snapshotPath, "delete", () => remove({ kind: "snapshot", key: workflowId }));
2031
+ }
2032
+ });
2033
+ }
1296
2034
  try {
1297
2035
  if (readdirSync4(workflowDir).length === 0) {
1298
2036
  rmdirSync2(workflowDir);
@@ -1320,7 +2058,7 @@ function resolveSelectedPlanFiles(outDir, selected) {
1320
2058
  for (const id of selected) {
1321
2059
  const file = byNum.get(id) ?? byStem.get(id) ?? byStem.get(id.replace(/\.md$/, ""));
1322
2060
  if (file === undefined) {
1323
- throw new Error(`promoteAuditPlans: selected plan ${JSON.stringify(id)} does not match any NNN-*.md file in ${resolve7(outDir)}`);
2061
+ throw new Error(`promoteAuditPlans: selected plan ${JSON.stringify(id)} does not match any NNN-*.md file in ${resolve8(outDir)}`);
1324
2062
  }
1325
2063
  if (!seen.has(file)) {
1326
2064
  seen.add(file);
@@ -1330,10 +2068,10 @@ function resolveSelectedPlanFiles(outDir, selected) {
1330
2068
  return resolved;
1331
2069
  }
1332
2070
  function readExecutionOrderIndex(outDir) {
1333
- const readmePath = join7(outDir, "README.md");
2071
+ const readmePath = join9(outDir, "README.md");
1334
2072
  let text;
1335
2073
  try {
1336
- text = readFileSync5(readmePath, "utf8");
2074
+ text = readFileSync7(readmePath, "utf8");
1337
2075
  } catch {
1338
2076
  return new Map;
1339
2077
  }
@@ -1359,11 +2097,11 @@ function readExecutionOrderIndex(outDir) {
1359
2097
  return rows;
1360
2098
  }
1361
2099
  function planFileRel(outDir, planFile) {
1362
- const resolved = resolve7(outDir);
1363
- const parts = resolved.split(sep2);
2100
+ const resolved = resolve8(outDir);
2101
+ const parts = resolved.split(sep3);
1364
2102
  const plansIdx = parts.lastIndexOf("plans");
1365
2103
  if (plansIdx >= 0) {
1366
- return `${parts.slice(plansIdx + 1).join(sep2)}${sep2}${planFile}`;
2104
+ return `${parts.slice(plansIdx + 1).join(sep3)}${sep3}${planFile}`;
1367
2105
  }
1368
2106
  return planFile;
1369
2107
  }