@mstar-harness/engine 3.9.3 → 3.10.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/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 readdirSync5, readFileSync as readFileSync7, rmdirSync as rmdirSync3, writeFileSync as writeFileSync4 } from "node:fs";
4
+ import { basename as basename5, join as join9, resolve as resolve9, 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 readdirSync4, 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 resolve8 } 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 readdirSync3, realpathSync as realpathSync2 } from "node:fs";
630
+ import { dirname as dirname5, join as join7, resolve as resolve7, 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,302 @@ 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, readdirSync as readdirSync2, rmdirSync as rmdirSync2 } from "node:fs";
793
+ import { isAbsolute as isAbsolute5, join as join6, resolve as resolve6 } 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
+ var WORKFLOW_DELIVERY_KINDS = ["development", "verification/report-only"];
799
+ var WORKFLOW_COMPOUND_OUTCOMES = ["created", "updated", "skipped"];
800
+ function stableJson(value) {
801
+ if (Array.isArray(value))
802
+ return `[${value.map(stableJson).join(",")}]`;
803
+ if (isPlainObject2(value)) {
804
+ const keys = Object.keys(value).sort();
805
+ return `{${keys.map((key) => `${JSON.stringify(key)}:${stableJson(value[key])}`).join(",")}}`;
806
+ }
807
+ return JSON.stringify(value) ?? "undefined";
808
+ }
809
+ function violation2(severity, code, message, fix) {
810
+ return { ok: false, severity, code, message, fix };
811
+ }
812
+ function validateNonEmptyString2(violations, value, field, missingCode, invalidCode) {
813
+ if (value === undefined) {
814
+ violations.push(violation2("high", missingCode, `missing required field: ${field}`));
815
+ } else if (typeof value !== "string" || value.trim() === "") {
816
+ violations.push(violation2("medium", invalidCode, `${field} must be a non-empty string`));
817
+ }
818
+ }
819
+ function validateWorktreePathValue(violations, value, field) {
820
+ if (typeof value !== "string" || value.trim() === "" || !isAbsolute5(value)) {
821
+ 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)"));
822
+ }
823
+ }
824
+ function deliveryEvidenceViolations(value, what) {
825
+ const violations = [];
826
+ const invalid2 = (message) => {
827
+ violations.push(violation2("medium", "workflow.snapshot.invalid-delivery-evidence", `${what}: ${message}`));
828
+ };
829
+ if (!isPlainObject2(value)) {
830
+ invalid2("must be an object");
831
+ return violations;
832
+ }
833
+ const members = ["compound", "pr", "merge", "completion"];
834
+ const unknownMembers = Object.keys(value).filter((key) => !members.includes(key));
835
+ if (unknownMembers.length > 0)
836
+ invalid2(`unknown member(s) ${unknownMembers.join(", ")} — expected ${members.join(" | ")}`);
837
+ const compound = value.compound;
838
+ if (compound !== undefined) {
839
+ if (!isPlainObject2(compound))
840
+ invalid2("compound must be an object");
841
+ else {
842
+ const unknown = Object.keys(compound).filter((key) => key !== "outcome" && key !== "reason");
843
+ if (unknown.length > 0)
844
+ invalid2(`compound has unknown key(s) ${unknown.join(", ")}`);
845
+ if (typeof compound.outcome !== "string" || !WORKFLOW_COMPOUND_OUTCOMES.includes(compound.outcome)) {
846
+ invalid2(`compound.outcome must be one of ${WORKFLOW_COMPOUND_OUTCOMES.join(" | ")} — got ${JSON.stringify(compound.outcome)}`);
847
+ } else if (compound.outcome === "skipped" && (typeof compound.reason !== "string" || compound.reason.trim() === "")) {
848
+ invalid2("compound reason is required when the disposition outcome is 'skipped' (contract §4c)");
849
+ } else if (compound.reason !== undefined && (typeof compound.reason !== "string" || compound.reason.trim() === "")) {
850
+ invalid2("compound.reason must be a non-empty string when given");
851
+ }
852
+ }
853
+ }
854
+ const stringMembers = {
855
+ pr: ["repo", "head", "target"],
856
+ merge: ["provider", "evidence"],
857
+ completion: ["policy", "evidence"]
858
+ };
859
+ for (const member of ["pr", "merge", "completion"]) {
860
+ const block = value[member];
861
+ if (block === undefined)
862
+ continue;
863
+ if (!isPlainObject2(block)) {
864
+ invalid2(`${member} must be an object`);
865
+ continue;
866
+ }
867
+ const fields = stringMembers[member];
868
+ const unknown = Object.keys(block).filter((key) => !fields.includes(key));
869
+ if (unknown.length > 0)
870
+ invalid2(`${member} has unknown key(s) ${unknown.join(", ")}`);
871
+ for (const field of fields) {
872
+ if (typeof block[field] !== "string" || block[field].trim() === "") {
873
+ invalid2(`${member}.${field} must be a non-empty string`);
874
+ }
875
+ }
876
+ }
877
+ return violations;
878
+ }
879
+ function validateWorkflowSnapshot(doc) {
880
+ const violations = [];
881
+ if (!isPlainObject2(doc)) {
882
+ return {
883
+ ok: false,
884
+ violations: [violation2("high", "workflow.snapshot.invalid", "workflow snapshot must be an object")]
885
+ };
886
+ }
887
+ if (doc.schema_version === undefined) {
888
+ violations.push(violation2("high", "workflow.snapshot.missing-schema-version", "missing required field: schema_version"));
889
+ } else if (doc.schema_version !== 1) {
890
+ 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)`));
891
+ }
892
+ if (doc.version !== undefined) {
893
+ 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"));
894
+ }
895
+ validateNonEmptyString2(violations, doc.id, "id", "workflow.snapshot.missing-id", "workflow.snapshot.invalid-id");
896
+ if (doc.type === undefined) {
897
+ violations.push(violation2("high", "workflow.snapshot.missing-type", "missing required field: type"));
898
+ } else if (typeof doc.type !== "string" || !WORKFLOW_LIFECYCLE_TYPES.includes(doc.type)) {
899
+ violations.push(violation2("medium", "workflow.snapshot.invalid-type", `type must be one of ${WORKFLOW_LIFECYCLE_TYPES.join(" | ")} — got ${JSON.stringify(doc.type)}`));
900
+ }
901
+ if (doc.status === undefined) {
902
+ violations.push(violation2("high", "workflow.snapshot.missing-status", "missing required field: status"));
903
+ } else if (typeof doc.status !== "string" || !WORKFLOW_LIFECYCLE_STATUSES.includes(doc.status)) {
904
+ violations.push(violation2("medium", "workflow.snapshot.invalid-status", `status must be one of ${WORKFLOW_LIFECYCLE_STATUSES.join(" | ")} — got ${JSON.stringify(doc.status)}`));
905
+ }
906
+ validateNonEmptyString2(violations, doc.started_at, "started_at", "workflow.snapshot.missing-started-at", "workflow.snapshot.invalid-started-at");
907
+ validateNonEmptyString2(violations, doc.updated_at, "updated_at", "workflow.snapshot.missing-updated-at", "workflow.snapshot.invalid-updated-at");
908
+ if (doc.ended_at !== undefined) {
909
+ validateNonEmptyString2(violations, doc.ended_at, "ended_at", "workflow.snapshot.missing-ended-at", "workflow.snapshot.invalid-ended-at");
910
+ }
911
+ if (doc.phase !== undefined && typeof doc.phase !== "string") {
912
+ violations.push(violation2("medium", "workflow.snapshot.invalid-phase", "phase must be a string (free-form phase machine label)"));
913
+ }
914
+ if (doc.plans === undefined) {
915
+ violations.push(violation2("high", "workflow.snapshot.missing-plans", "missing required field: plans"));
916
+ } else if (!Array.isArray(doc.plans)) {
917
+ violations.push(violation2("high", "workflow.snapshot.invalid-plans", "plans must be an array of legacy plan rows"));
918
+ } else {
919
+ for (const row of doc.plans) {
920
+ violations.push(...validatePlanRow(row).violations);
921
+ if (isPlainObject2(row) && row.execution_lease !== undefined) {
922
+ violations.push(...validateExecutionLease(row.execution_lease).violations);
923
+ }
924
+ if (isPlainObject2(row) && row.coordination !== undefined) {
925
+ violations.push(...validateRowCoordination(row.coordination, `plans[${String(row.id)}].coordination`));
926
+ }
927
+ }
928
+ }
929
+ if (doc.coordination !== undefined) {
930
+ violations.push(...validateSnapshotCoordination(doc.coordination));
931
+ }
932
+ if (doc.execution_policy !== undefined) {
933
+ if (!isPlainObject2(doc.execution_policy)) {
934
+ violations.push(violation2("medium", "workflow.snapshot.invalid-execution-policy", "execution_policy must be an object"));
935
+ }
936
+ }
937
+ if (doc.integration_merge_lease !== undefined) {
938
+ violations.push(...validateIntegrationMergeLease(doc.integration_merge_lease).violations);
939
+ }
940
+ if (doc.branch !== undefined) {
941
+ if (!isPlainObject2(doc.branch)) {
942
+ violations.push(violation2("medium", "workflow.snapshot.invalid-branch", "branch must be an object"));
943
+ } else {
944
+ for (const key of ["base", "source", "integration", "target"]) {
945
+ if (doc.branch[key] !== undefined && (typeof doc.branch[key] !== "string" || doc.branch[key].trim() === "")) {
946
+ violations.push(violation2("medium", "workflow.snapshot.invalid-branch", `branch.${key} must be a non-empty string`));
947
+ }
948
+ }
949
+ }
950
+ }
951
+ const legacyWorktreePath = doc.control_worktree_path;
952
+ const canonicalWorktreePath = doc.integration_worktree_path;
953
+ if (legacyWorktreePath !== undefined && canonicalWorktreePath !== undefined) {
954
+ 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"));
955
+ } else {
956
+ if (canonicalWorktreePath !== undefined) {
957
+ validateWorktreePathValue(violations, canonicalWorktreePath, "integration_worktree_path");
958
+ }
959
+ if (legacyWorktreePath !== undefined) {
960
+ 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"));
961
+ validateWorktreePathValue(violations, legacyWorktreePath, "control_worktree_path (legacy alias)");
962
+ }
963
+ }
964
+ if (doc.legacy_metadata !== undefined && !isPlainObject2(doc.legacy_metadata)) {
965
+ violations.push(violation2("medium", "workflow.snapshot.invalid-legacy-metadata", "legacy_metadata must be an object"));
966
+ }
967
+ if (doc.compass_ref !== undefined) {
968
+ validateNonEmptyString2(violations, doc.compass_ref, "compass_ref", "workflow.snapshot.missing-compass-ref", "workflow.snapshot.invalid-compass-ref");
969
+ }
970
+ if (doc.delivery_kind !== undefined) {
971
+ if (typeof doc.delivery_kind !== "string" || !WORKFLOW_DELIVERY_KINDS.includes(doc.delivery_kind)) {
972
+ violations.push(violation2("medium", "workflow.snapshot.invalid-delivery-kind", `delivery_kind must be one of ${WORKFLOW_DELIVERY_KINDS.join(" | ")} — got ${JSON.stringify(doc.delivery_kind)}`));
973
+ }
974
+ }
975
+ if (doc.project !== undefined) {
976
+ validateNonEmptyString2(violations, doc.project, "project", "workflow.snapshot.missing-project", "workflow.snapshot.invalid-project");
977
+ }
978
+ if (doc.completion_policy !== undefined) {
979
+ validateNonEmptyString2(violations, doc.completion_policy, "completion_policy", "workflow.snapshot.missing-completion-policy", "workflow.snapshot.invalid-completion-policy");
980
+ }
981
+ if (doc.delivery !== undefined) {
982
+ violations.push(...deliveryEvidenceViolations(doc.delivery, "delivery"));
983
+ }
984
+ const terminal = typeof doc.status === "string" && WORKFLOW_TERMINAL_STATUSES.includes(doc.status);
985
+ if (terminal) {
986
+ if (doc.ended_at === undefined) {
987
+ 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`));
988
+ }
989
+ if (Array.isArray(doc.plans)) {
990
+ for (const row of doc.plans) {
991
+ if (isPlainObject2(row) && row.execution_lease !== undefined) {
992
+ 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`));
993
+ }
994
+ }
995
+ }
996
+ if (doc.integration_merge_lease !== undefined) {
997
+ 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"));
998
+ }
999
+ }
1000
+ return { ok: violations.length === 0, violations };
1001
+ }
1002
+ async function writeWorkflowSnapshot(snapshot, dir, opts = {}) {
1003
+ const gate = validateWorkflowSnapshot(snapshot);
1004
+ if (!gate.ok) {
1005
+ const detail = gate.violations.map((v) => v.message).join("; ");
1006
+ throw new Error(`refusing to write invalid workflow snapshot: ${detail}`);
1007
+ }
1008
+ if (opts.expectedVersion !== undefined && !isArtifactVersion(opts.expectedVersion)) {
1009
+ throw new CoordinationError("coordination.invalid-input", `expectedVersion must be "absent" or sha256:<64 hex> — got ${JSON.stringify(opts.expectedVersion)}`, { expected: opts.expectedVersion });
1010
+ }
1011
+ if (opts.createOnly === true && opts.expectedVersion !== undefined && opts.expectedVersion !== "absent") {
1012
+ throw new CoordinationError("coordination.invalid-input", `createOnly implies expectedVersion "absent" — got ${JSON.stringify(opts.expectedVersion)}`, { expected: opts.expectedVersion });
1013
+ }
1014
+ const snapshotPath = join6(dir, WORKFLOW_SNAPSHOT_FILE);
1015
+ const store = getArtifactStore();
1016
+ assertFsStorePath(store, { kind: "snapshot", key: snapshot.id }, snapshotPath);
1017
+ mkdirSync3(dir, { recursive: true });
1018
+ await withStatusWriteLock(snapshotPath, async () => {
1019
+ const current = readArtifactBytes(snapshotPath);
1020
+ const currentVersion = current?.version ?? "absent";
1021
+ const required = opts.createOnly === true ? "absent" : opts.expectedVersion ?? "absent";
1022
+ if (required !== currentVersion) {
1023
+ const missingToken = opts.createOnly !== true && opts.expectedVersion === undefined;
1024
+ 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 });
1025
+ }
1026
+ const payload = current === undefined ? snapshot : mergePhaseProjection(current.payload, snapshot);
1027
+ assertCoordinatedSnapshotWriter(current?.payload, snapshotPath, opts.sessionPath);
1028
+ await withProtectedWrite(snapshotPath, "put", () => store.put({ kind: "snapshot", key: snapshot.id, payload }));
1029
+ });
1030
+ }
1031
+ function assertCoordinatedSnapshotWriter(stored, snapshotPath, sessionPath, action = "replacement") {
1032
+ const coordination = isPlainObject2(stored) ? stored.coordination : undefined;
1033
+ if (coordination === undefined)
1034
+ return;
1035
+ const bound = isPlainObject2(coordination) && isPlainObject2(coordination.coordinator) ? coordination.coordinator.session_file : undefined;
1036
+ if (sessionPath === undefined || typeof bound !== "string" || canonicalTarget(sessionPath) !== canonicalTarget(bound)) {
1037
+ throw new CoordinationError("coordination.session-mismatch", `snapshot ${snapshotPath} is coordinated — ${action} requires --session <coordinator envelope>`, { path: snapshotPath, expected: bound, actual: sessionPath });
1038
+ }
1039
+ }
1040
+ function mergePhaseProjection(stored, incoming) {
1041
+ if (!isPlainObject2(stored)) {
1042
+ throw new CoordinationError("coordination.version-conflict", "stored workflow snapshot is not an object — refusing a field-scoped rewrite over it", {});
1043
+ }
1044
+ const allowed = ["phase", "updated_at"];
1045
+ const keys = new Set([...Object.keys(stored), ...Object.keys(incoming)]);
1046
+ const drifted = [...keys].filter((key) => !allowed.includes(key) && stableJson(stored[key]) !== stableJson(incoming[key]));
1047
+ if (drifted.length > 0) {
1048
+ 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 });
1049
+ }
1050
+ const next = { ...stored };
1051
+ for (const key of allowed) {
1052
+ const value = incoming[key];
1053
+ if (value === undefined)
1054
+ delete next[key];
1055
+ else
1056
+ next[key] = value;
1057
+ }
1058
+ return next;
1059
+ }
1060
+ function nonEmptyString(value) {
1061
+ return typeof value === "string" && value.trim() !== "";
1062
+ }
1063
+ function assertDeliveryRegistrationCoherence(kind, evidence, what) {
1064
+ if (kind === "development" && (!nonEmptyString(evidence.branchSource) || !nonEmptyString(evidence.branchTarget))) {
1065
+ throw new Error(`${what}: a development workflow requires its delivery source and target branches (--branch-source/--branch-target) — missing branch fields are incomplete registration, not an exempt workflow (contract §1/§4a)`);
1066
+ }
1067
+ if (kind === "verification/report-only" && !nonEmptyString(evidence.completionPolicy)) {
1068
+ throw new Error(`${what}: a verification/report-only workflow requires the completion policy (--completion-policy) naming the evidence that completes it (contract §1)`);
1069
+ }
1070
+ for (const [field, value] of Object.entries({ branchSource: evidence.branchSource, branchTarget: evidence.branchTarget, completionPolicy: evidence.completionPolicy })) {
1071
+ if (value !== undefined && !nonEmptyString(value)) {
1072
+ throw new Error(`${what}: ${field} must be a non-empty string when given`);
1073
+ }
1074
+ }
1075
+ }
329
1076
 
330
1077
  // src/status.ts
331
1078
  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) {
1079
+ var PLAN_STATUSES = ["Todo", "InProgress", "InReview", "Blocked", "Done"];
1080
+ function violation3(severity, code, message, fix) {
336
1081
  return { ok: false, severity, code, message, fix };
337
1082
  }
338
1083
  function todayString() {
@@ -341,12 +1086,49 @@ function todayString() {
341
1086
  const day = String(now.getDate()).padStart(2, "0");
342
1087
  return `${now.getFullYear()}-${month}-${day}`;
343
1088
  }
344
- function validateNonEmptyString(violations, value, field, missingCode, invalidCode) {
1089
+ function validateNonEmptyString3(violations, value, field, missingCode, invalidCode) {
345
1090
  if (value === undefined) {
346
- violations.push(violation("high", missingCode, `missing required field: ${field}`));
1091
+ violations.push(violation3("high", missingCode, `missing required field: ${field}`));
347
1092
  } else if (typeof value !== "string" || value.trim() === "") {
348
- violations.push(violation("medium", invalidCode, `${field} must be a non-empty string`));
1093
+ violations.push(violation3("medium", invalidCode, `${field} must be a non-empty string`));
1094
+ }
1095
+ }
1096
+ function validatePlanRow(row) {
1097
+ const violations = [];
1098
+ if (!isPlainObject2(row)) {
1099
+ return { ok: false, violations: [violation3("high", "status.plan-row.invalid", "plan row must be an object")] };
1100
+ }
1101
+ const { id, plan_id: planId, title, file, status, metadata, execution_lease } = row;
1102
+ if (id === undefined && planId === undefined) {
1103
+ violations.push(violation3("high", "status.plan-row.missing-id", "missing required field: id (or legacy plan_id)"));
1104
+ } else {
1105
+ if (id !== undefined) {
1106
+ validateNonEmptyString3(violations, id, "id", "status.plan-row.missing-id", "status.plan-row.invalid-id");
1107
+ }
1108
+ if (planId !== undefined) {
1109
+ validateNonEmptyString3(violations, planId, "plan_id", "status.plan-row.missing-plan-id", "status.plan-row.invalid-plan-id");
1110
+ }
1111
+ if (id !== undefined && planId !== undefined && id !== planId) {
1112
+ 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)"));
1113
+ }
1114
+ }
1115
+ validateNonEmptyString3(violations, title, "title", "status.plan-row.missing-title", "status.plan-row.invalid-title");
1116
+ validateNonEmptyString3(violations, file, "file", "status.plan-row.missing-file", "status.plan-row.invalid-file");
1117
+ if (status === undefined) {
1118
+ violations.push(violation3("high", "status.plan-row.missing-status", "missing required field: status"));
1119
+ } else if (typeof status !== "string" || !PLAN_STATUSES.includes(status)) {
1120
+ violations.push(violation3("medium", "status.plan-row.invalid-status", `status must be one of ${PLAN_STATUSES.join(" | ")} — got ${JSON.stringify(status)}`));
1121
+ }
1122
+ if (metadata !== undefined && !isPlainObject2(metadata)) {
1123
+ violations.push(violation3("medium", "status.plan-row.invalid-metadata", "metadata must be an object"));
1124
+ }
1125
+ if (execution_lease !== undefined && !isPlainObject2(execution_lease)) {
1126
+ violations.push(violation3("medium", "status.plan-row.invalid-execution-lease", "execution_lease must be an object"));
1127
+ }
1128
+ if (status === "Done" && execution_lease !== undefined) {
1129
+ 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
1130
  }
1131
+ return { ok: violations.length === 0, violations };
350
1132
  }
351
1133
  function isHarnessRelativePath(dir) {
352
1134
  if (dir.startsWith("/") || dir.startsWith("\\"))
@@ -357,25 +1139,25 @@ function isHarnessRelativePath(dir) {
357
1139
  }
358
1140
  function validateWorkflowEntry(entry) {
359
1141
  const violations = [];
360
- if (!isPlainObject(entry)) {
1142
+ if (!isPlainObject2(entry)) {
361
1143
  return {
362
1144
  ok: false,
363
- violations: [violation("high", "status.workflow.invalid", "workflow entry must be an object")]
1145
+ violations: [violation3("high", "status.workflow.invalid", "workflow entry must be an object")]
364
1146
  };
365
1147
  }
366
- validateNonEmptyString(violations, entry.id, "id", "status.workflow.missing-id", "status.workflow.invalid-id");
1148
+ validateNonEmptyString3(violations, entry.id, "id", "status.workflow.missing-id", "status.workflow.invalid-id");
367
1149
  if (entry.type === undefined) {
368
- violations.push(violation("high", "status.workflow.missing-type", "missing required field: type"));
1150
+ violations.push(violation3("high", "status.workflow.missing-type", "missing required field: type"));
369
1151
  } 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)}`));
1152
+ violations.push(violation3("medium", "status.workflow.invalid-type", `type must be one of ${WORKFLOW_LIFECYCLE_TYPES.join(" | ")} — got ${JSON.stringify(entry.type)}`));
371
1153
  }
372
- validateNonEmptyString(violations, entry.started_at, "started_at", "status.workflow.missing-started-at", "status.workflow.invalid-started-at");
1154
+ validateNonEmptyString3(violations, entry.started_at, "started_at", "status.workflow.missing-started-at", "status.workflow.invalid-started-at");
373
1155
  if (entry.dir === undefined) {
374
- violations.push(violation("high", "status.workflow.missing-dir", "missing required field: dir"));
1156
+ violations.push(violation3("high", "status.workflow.missing-dir", "missing required field: dir"));
375
1157
  } 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"));
1158
+ violations.push(violation3("medium", "status.workflow.invalid-dir", "dir must be a non-empty string"));
377
1159
  } 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)}`));
1160
+ 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
1161
  }
380
1162
  return { ok: violations.length === 0, violations };
381
1163
  }
@@ -385,24 +1167,24 @@ function validateStatusV2(docOrPath, opts = {}) {
385
1167
  if (typeof docOrPath === "string") {
386
1168
  try {
387
1169
  doc = readJson(docOrPath);
388
- harnessDir = dirname4(resolve5(docOrPath));
1170
+ harnessDir = dirname5(resolve7(docOrPath));
389
1171
  } catch (error) {
390
1172
  return {
391
1173
  ok: false,
392
- violations: [violation("high", "status.invalid-json", error.message)]
1174
+ violations: [violation3("high", "status.invalid-json", error.message)]
393
1175
  };
394
1176
  }
395
1177
  } else {
396
1178
  doc = docOrPath;
397
1179
  }
398
- if (!isPlainObject(doc)) {
399
- return { ok: false, violations: [violation("high", "status.invalid-doc", "status document must be an object")] };
1180
+ if (!isPlainObject2(doc)) {
1181
+ return { ok: false, violations: [violation3("high", "status.invalid-doc", "status document must be an object")] };
400
1182
  }
401
1183
  if (doc.version !== 2) {
402
1184
  return {
403
1185
  ok: false,
404
1186
  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`")
1187
+ 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
1188
  ]
407
1189
  };
408
1190
  }
@@ -410,7 +1192,7 @@ function validateStatusV2(docOrPath, opts = {}) {
410
1192
  return {
411
1193
  ok: false,
412
1194
  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`")
1195
+ 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
1196
  ]
415
1197
  };
416
1198
  }
@@ -418,27 +1200,27 @@ function validateStatusV2(docOrPath, opts = {}) {
418
1200
  return {
419
1201
  ok: false,
420
1202
  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`")
1203
+ 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
1204
  ]
423
1205
  };
424
1206
  }
425
1207
  const violations = [];
426
1208
  if (doc.updated_at === undefined) {
427
- violations.push(violation("high", "status.missing-updated-at", "missing required field: updated_at"));
1209
+ violations.push(violation3("high", "status.missing-updated-at", "missing required field: updated_at"));
428
1210
  } 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"));
1211
+ violations.push(violation3("medium", "status.invalid-updated-at", "updated_at must be YYYY-MM-DD"));
430
1212
  }
431
1213
  if (doc.workflows === undefined) {
432
- violations.push(violation("high", "status.missing-workflows", "missing required field: workflows"));
1214
+ violations.push(violation3("high", "status.missing-workflows", "missing required field: workflows"));
433
1215
  } else if (!Array.isArray(doc.workflows)) {
434
- violations.push(violation("high", "status.invalid-workflows", "workflows must be an array"));
1216
+ violations.push(violation3("high", "status.invalid-workflows", "workflows must be an array"));
435
1217
  } else {
436
1218
  const seen = new Set;
437
1219
  for (const entry of doc.workflows) {
438
1220
  violations.push(...validateWorkflowEntry(entry).violations);
439
- if (isPlainObject(entry) && typeof entry.id === "string") {
1221
+ if (isPlainObject2(entry) && typeof entry.id === "string") {
440
1222
  if (seen.has(entry.id)) {
441
- violations.push(violation("medium", "status.workflow.duplicate-id", `duplicate workflow id in workflows[]: ${JSON.stringify(entry.id)}`));
1223
+ violations.push(violation3("medium", "status.workflow.duplicate-id", `duplicate workflow id in workflows[]: ${JSON.stringify(entry.id)}`));
442
1224
  }
443
1225
  seen.add(entry.id);
444
1226
  }
@@ -447,47 +1229,47 @@ function validateStatusV2(docOrPath, opts = {}) {
447
1229
  if (harnessDir !== undefined && Array.isArray(doc.workflows)) {
448
1230
  let realHarnessDir = null;
449
1231
  try {
450
- realHarnessDir = realpathSync(harnessDir);
1232
+ realHarnessDir = realpathSync2(harnessDir);
451
1233
  } catch {}
452
1234
  for (const entry of doc.workflows) {
453
- if (!isPlainObject(entry) || typeof entry.dir !== "string")
1235
+ if (!isPlainObject2(entry) || typeof entry.dir !== "string")
454
1236
  continue;
455
- const relSnapshot = join5(entry.dir, WORKFLOW_SNAPSHOT_FILE);
456
- const snapshotPath = join5(harnessDir, relSnapshot);
1237
+ const relSnapshot = join7(entry.dir, WORKFLOW_SNAPSHOT_FILE);
1238
+ const snapshotPath = join7(harnessDir, relSnapshot);
457
1239
  const label = typeof entry.id === "string" ? entry.id : relSnapshot;
458
1240
  let physical;
459
1241
  try {
460
- physical = realpathSync(snapshotPath);
1242
+ physical = realpathSync2(snapshotPath);
461
1243
  } 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`));
1244
+ 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
1245
  continue;
464
1246
  }
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)}`));
1247
+ if (realHarnessDir !== null && physical !== realHarnessDir && !physical.startsWith(`${realHarnessDir}${sep2}`)) {
1248
+ 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
1249
  continue;
468
1250
  }
469
1251
  let snapshot;
470
1252
  try {
471
1253
  snapshot = readJson(snapshotPath);
472
1254
  } catch (error) {
473
- violations.push(violation("high", "status.workflow.snapshot-invalid", `snapshot at ${JSON.stringify(relSnapshot)} is not valid JSON: ${error.message}`));
1255
+ violations.push(violation3("high", "status.workflow.snapshot-invalid", `snapshot at ${JSON.stringify(relSnapshot)} is not valid JSON: ${error.message}`));
474
1256
  continue;
475
1257
  }
476
1258
  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`));
1259
+ 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
1260
  }
479
1261
  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`));
1262
+ 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
1263
  }
482
1264
  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`));
1265
+ 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
1266
  }
485
1267
  }
486
1268
  }
487
1269
  return { ok: violations.length === 0, violations };
488
1270
  }
489
1271
  async function registerWorkflowEntryLocked(statusPath, entry) {
490
- const harnessDir = dirname4(statusPath);
1272
+ const harnessDir = dirname5(statusPath);
491
1273
  const store = getArtifactStore();
492
1274
  assertFsStorePath(store, { kind: "status", key: "root" }, statusPath);
493
1275
  const current = readJson(statusPath);
@@ -497,41 +1279,72 @@ async function registerWorkflowEntryLocked(statusPath, entry) {
497
1279
  throw new Error("refusing to modify status.json: workflows must be an array — a v1 root must be migrated first (run `mstar migrate`)");
498
1280
  }
499
1281
  const existing = doc.workflows.findIndex((wf) => wf.id === entry.id);
500
- if (existing >= 0) {
501
- doc.workflows[existing] = entry;
502
- } else {
1282
+ const commit = async () => {
1283
+ doc.updated_at = todayString();
1284
+ const gate = validateStatusV2(doc, { harnessDir });
1285
+ if (!gate.ok) {
1286
+ throw new Error(`refusing to write invalid status.json: ${gate.violations.map((v) => v.message).join("; ")}`);
1287
+ }
1288
+ await withProtectedWrite(statusPath, "put", () => store.put({ kind: "status", key: "root", payload: doc }));
1289
+ return doc;
1290
+ };
1291
+ if (existing < 0) {
503
1292
  doc.workflows.push(entry);
1293
+ return commit();
1294
+ }
1295
+ const prior = doc.workflows[existing];
1296
+ return withRegisteredSnapshotLock(harnessDir, prior, async (snapshot) => {
1297
+ const priorCoordination = snapshot?.coordination;
1298
+ if (isPlainObject2(priorCoordination) && isPlainObject2(priorCoordination.coordinator)) {
1299
+ const drifted = ["dir", "type", "started_at"].filter((field) => prior[field] !== entry[field]);
1300
+ if (drifted.length > 0) {
1301
+ 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] });
1302
+ }
1303
+ }
1304
+ doc.workflows[existing] = entry;
1305
+ return commit();
1306
+ });
1307
+ }
1308
+ async function withRegisteredSnapshotLock(harnessDir, entry, fn) {
1309
+ if (typeof entry.dir !== "string")
1310
+ return fn(undefined);
1311
+ const snapshotPath = join7(harnessDir, entry.dir, WORKFLOW_SNAPSHOT_FILE);
1312
+ if (!existsSync5(snapshotPath))
1313
+ return fn(undefined);
1314
+ return withStatusWriteLock(snapshotPath, () => fn(readRegisteredSnapshot(snapshotPath)));
1315
+ }
1316
+ function readRegisteredSnapshot(snapshotPath) {
1317
+ if (!existsSync5(snapshotPath))
1318
+ return;
1319
+ try {
1320
+ const parsed = JSON.parse(readFileSync5(snapshotPath, "utf8"));
1321
+ return isPlainObject2(parsed) ? parsed : undefined;
1322
+ } catch {
1323
+ return;
504
1324
  }
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
1325
  }
513
1326
 
514
1327
  // src/path.ts
515
1328
  function resolveHarnessDir(startDir = process.cwd(), opts = {}) {
516
- const start = resolve6(startDir);
1329
+ const start = resolve8(startDir);
517
1330
  const explicit = opts.harnessDir ?? process.env.MSTAR_HARNESS_DIR;
518
1331
  if (explicit)
519
- return resolve6(start, explicit);
520
- const boundary = resolve6(start, opts.workspaceRoot ?? defaultWorkspaceRoot(start));
1332
+ return resolve8(start, explicit);
1333
+ const boundary = resolve8(start, opts.workspaceRoot ?? defaultWorkspaceRoot(start));
521
1334
  const rc = loadMstarc(start, boundary);
522
1335
  if (rc !== null && rc.config.harnessDir)
523
- return resolve6(rc.dir, rc.config.harnessDir);
1336
+ return resolve8(rc.dir, rc.config.harnessDir);
524
1337
  let dir = start;
525
1338
  for (;; ) {
526
1339
  if (!isAtOrBelow2(dir, boundary))
527
1340
  return null;
528
- for (const candidate of [join6(dir, ".mstar"), join6(dir, ".agents"), join6(dir, ".plans"), join6(dir, "plans")]) {
1341
+ for (const candidate of [join8(dir, ".mstar"), join8(dir, ".agents"), join8(dir, ".plans"), join8(dir, "plans")]) {
529
1342
  if (isDirectory(candidate))
530
1343
  return candidate;
531
1344
  }
532
1345
  if (dir === boundary)
533
1346
  return null;
534
- const parent = dirname5(dir);
1347
+ const parent = dirname6(dir);
535
1348
  if (parent === dir)
536
1349
  return null;
537
1350
  dir = parent;
@@ -549,21 +1362,21 @@ function defaultWorkspaceRoot(startDir) {
549
1362
  let boundary = startDir;
550
1363
  for (const segment of cdup.split(/[\\/]/)) {
551
1364
  if (segment && segment !== ".")
552
- boundary = dirname5(boundary);
1365
+ boundary = dirname6(boundary);
553
1366
  }
554
- return resolve6(boundary);
1367
+ return resolve8(boundary);
555
1368
  } catch {}
556
1369
  return startDir;
557
1370
  }
558
1371
  function isAtOrBelow2(dir, root) {
559
1372
  const rel = relative2(root, dir);
560
- return rel === "" || !rel.startsWith("..") && !isAbsolute4(rel);
1373
+ return rel === "" || !rel.startsWith("..") && !isAbsolute6(rel);
561
1374
  }
562
1375
  function mstarcDirOverride(harnessDir, key) {
563
- const dir = resolve6(harnessDir);
564
- const rc = loadMstarc(dir, dirname5(dir));
1376
+ const dir = resolve8(harnessDir);
1377
+ const rc = loadMstarc(dir, dirname6(dir));
565
1378
  const declared = rc?.config[key];
566
- return declared ? resolve6(rc.dir, declared) : null;
1379
+ return declared ? resolve8(rc.dir, declared) : null;
567
1380
  }
568
1381
  function assertSafePathComponent(value, what) {
569
1382
  if (value === "" || value === "." || value === ".." || !/^[A-Za-z0-9._-]+$/.test(value)) {
@@ -573,10 +1386,10 @@ function assertSafePathComponent(value, what) {
573
1386
  function resolveHarnessSubdir(startDir, opts, key, fallback) {
574
1387
  const harness = resolveHarnessDir(startDir, opts);
575
1388
  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)`);
1389
+ throw new Error(`harness dir not found from ${resolve8(startDir)} — cannot resolve the ${fallback} dir (run \`mstar harness scaffold\`, pass opts.harnessDir, or set MSTAR_HARNESS_DIR)`);
577
1390
  }
578
1391
  const declared = mstarcDirOverride(harness, key);
579
- return declared !== null ? declared : join6(resolve6(harness), fallback);
1392
+ return declared !== null ? declared : join8(resolve8(harness), fallback);
580
1393
  }
581
1394
  function resolveWorkflowDir(startDir = process.cwd(), opts = {}) {
582
1395
  return resolveHarnessSubdir(startDir, opts, "workflowDir", "workflows");
@@ -618,7 +1431,7 @@ function isDirectory(dir) {
618
1431
  }
619
1432
 
620
1433
  // src/audit.ts
621
- function violation2(severity, code, message, fix) {
1434
+ function violation4(severity, code, message, fix) {
622
1435
  return { ok: false, severity, code, message, fix };
623
1436
  }
624
1437
  var AUDIT_PRIORITIES = ["P1", "P2", "P3"];
@@ -663,14 +1476,14 @@ function validateAuditStatusBlocks(planText) {
663
1476
  const violations = [];
664
1477
  const blocks = parseStatusBlocks(planText);
665
1478
  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"));
1479
+ 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
1480
  return { ok: false, violations };
668
1481
  }
669
1482
  blocks.forEach((block, index) => {
670
1483
  const label = blocks.length > 1 ? ` #${index + 1}` : "";
671
1484
  for (const field of AUDIT_STATUS_FIELDS) {
672
1485
  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`));
1486
+ 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
1487
  }
675
1488
  }
676
1489
  const check = (field, pattern, code, expected) => {
@@ -678,7 +1491,7 @@ function validateAuditStatusBlocks(planText) {
678
1491
  if (value === undefined)
679
1492
  return;
680
1493
  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}`));
1494
+ 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
1495
  }
683
1496
  };
684
1497
  check("Priority", /^P[123]$/, "audit.status.invalid-priority", "P1 | P2 | P3");
@@ -913,12 +1726,12 @@ function scanSecrets(files) {
913
1726
  for (const file of files) {
914
1727
  let text;
915
1728
  try {
916
- text = readFileSync5(file, "utf8");
1729
+ text = readFileSync7(file, "utf8");
917
1730
  } catch {
918
1731
  unreadableFiles++;
919
1732
  continue;
920
1733
  }
921
- const base = basename3(file);
1734
+ const base = basename5(file);
922
1735
  for (const entry of NEVER_COMMIT_FILENAMES) {
923
1736
  if (entry.re.test(base))
924
1737
  findings.push({ file, line: 1, type: entry.type });
@@ -970,12 +1783,12 @@ var LOCKFILE_NAMES = [
970
1783
  function rootLockfiles(root) {
971
1784
  let entries;
972
1785
  try {
973
- entries = readdirSync4(root, { withFileTypes: true });
1786
+ entries = readdirSync5(root, { withFileTypes: true });
974
1787
  } catch {
975
1788
  return [];
976
1789
  }
977
1790
  const names = new Set(LOCKFILE_NAMES);
978
- const present = entries.filter((entry) => entry.isFile() && names.has(entry.name)).map((entry) => join7(root, entry.name));
1791
+ const present = entries.filter((entry) => entry.isFile() && names.has(entry.name)).map((entry) => join9(root, entry.name));
979
1792
  if (present.length === 0)
980
1793
  return [];
981
1794
  try {
@@ -984,7 +1797,7 @@ function rootLockfiles(root) {
984
1797
  encoding: "utf8",
985
1798
  stdio: ["ignore", "pipe", "ignore"]
986
1799
  }).split("\x00").filter((f) => f !== ""));
987
- return present.filter((p) => tracked.has(basename3(p)));
1800
+ return present.filter((p) => tracked.has(basename5(p)));
988
1801
  } catch {
989
1802
  return present;
990
1803
  }
@@ -995,26 +1808,26 @@ function supplyChainChecks(repoRoot) {
995
1808
  const lockfiles = rootLockfiles(repoRoot);
996
1809
  if (lockfiles.length === 0) {
997
1810
  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, …)"));
1811
+ 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
1812
  } else if (lockfiles.length > 1) {
1000
1813
  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"));
1814
+ violations.push(violation4("medium", "audit.supply.lockfile-duplicate", `multiple lockfiles at ${repoRoot}: ${lockfiles.join(", ")}`, "keep exactly one lockfile per package manager"));
1002
1815
  }
1003
- const workflowsDir = join7(repoRoot, ".github", "workflows");
1816
+ const workflowsDir = join9(repoRoot, ".github", "workflows");
1004
1817
  let wfEntries = [];
1005
1818
  try {
1006
- wfEntries = readdirSync4(workflowsDir, { withFileTypes: true });
1819
+ wfEntries = readdirSync5(workflowsDir, { withFileTypes: true });
1007
1820
  } catch {
1008
1821
  wfEntries = [];
1009
1822
  }
1010
1823
  for (const entry of wfEntries) {
1011
1824
  if (!entry.isFile() || !/\.(?:ya?ml)$/.test(entry.name))
1012
1825
  continue;
1013
- const wfPath = join7(workflowsDir, entry.name);
1826
+ const wfPath = join9(workflowsDir, entry.name);
1014
1827
  const relPath = `.github/workflows/${entry.name}`;
1015
1828
  let text;
1016
1829
  try {
1017
- text = readFileSync5(wfPath, "utf8");
1830
+ text = readFileSync7(wfPath, "utf8");
1018
1831
  } catch {
1019
1832
  continue;
1020
1833
  }
@@ -1054,12 +1867,12 @@ function supplyChainChecks(repoRoot) {
1054
1867
  const versionLike = /^v\d+(?:\.\d+)*$/.test(ref);
1055
1868
  if (!shaLike && !versionLike) {
1056
1869
  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"));
1870
+ 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
1871
  }
1059
1872
  }
1060
1873
  if (hasPrt && prtHeadSteps.has(i)) {
1061
1874
  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"));
1875
+ 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
1876
  }
1064
1877
  }
1065
1878
  }
@@ -1112,7 +1925,7 @@ function redactFinding(finding) {
1112
1925
  };
1113
1926
  }
1114
1927
  function readPlanFileSummary(filePath) {
1115
- const text = readFileSync5(filePath, "utf8");
1928
+ const text = readFileSync7(filePath, "utf8");
1116
1929
  const title = (text.match(/^# (.+)$/m) ?? [])[1] ?? filePath;
1117
1930
  const blocks = parseStatusBlocks(text);
1118
1931
  return { title: title.trim(), fields: blocks.length > 0 ? blocks[0].fields : new Map };
@@ -1166,10 +1979,10 @@ function renderIndex(params) {
1166
1979
  function scaffoldAuditPlan(outDir, findings, options = {}) {
1167
1980
  const date = options.date ?? new Date().toISOString().slice(0, 10);
1168
1981
  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: [] };
1172
- const existing = readdirSync4(outDir).filter((f) => /^\d{3}-.*\.md$/.test(f));
1982
+ mkdirSync5(outDir, { recursive: true });
1983
+ const existingReadme = join9(outDir, "README.md");
1984
+ const carried = existsSync7(existingReadme) ? extractSecurityDispositionSections(readFileSync7(existingReadme, "utf8")) : { needsVerification: [], hardeningChecked: [] };
1985
+ const existing = readdirSync5(outDir).filter((f) => /^\d{3}-.*\.md$/.test(f));
1173
1986
  let next = existing.reduce((max, f) => Math.max(max, Number(f.slice(0, 3))), 0) + 1;
1174
1987
  const redactedFindings = findings.map(redactFinding);
1175
1988
  const written = [];
@@ -1185,13 +1998,13 @@ function scaffoldAuditPlan(outDir, findings, options = {}) {
1185
1998
  }
1186
1999
  usedSlugs.add(slug);
1187
2000
  const file = `${num}-${slug}.md`;
1188
- writeFileSync4(join7(outDir, file), renderPlanFile(finding, plannedAt));
2001
+ writeFileSync4(join9(outDir, file), renderPlanFile(finding, plannedAt));
1189
2002
  written.push(file);
1190
2003
  next++;
1191
2004
  }
1192
2005
  const all = [...existing, ...written].sort();
1193
2006
  const rows = all.map((file) => {
1194
- const summary = readPlanFileSummary(join7(outDir, file));
2007
+ const summary = readPlanFileSummary(join9(outDir, file));
1195
2008
  const fields = summary.fields;
1196
2009
  return {
1197
2010
  num: file.slice(0, 3),
@@ -1225,7 +2038,7 @@ function scaffoldAuditPlan(outDir, findings, options = {}) {
1225
2038
  });
1226
2039
  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
2040
  const hardeningCheckedLines = options.hardeningChecked !== undefined ? options.hardeningChecked.map((hc) => `- ${hc.kind}: ${escapeCell(redactText(hc.text))}`) : carried.hardeningChecked;
1228
- writeFileSync4(join7(outDir, "README.md"), renderIndex({
2041
+ writeFileSync4(join9(outDir, "README.md"), renderIndex({
1229
2042
  date,
1230
2043
  repoName: options.repoName ?? "repo",
1231
2044
  repoShortSha: options.repoShortSha ?? "unknown",
@@ -1234,7 +2047,7 @@ function scaffoldAuditPlan(outDir, findings, options = {}) {
1234
2047
  needsVerification: needsVerificationLines,
1235
2048
  hardeningChecked: hardeningCheckedLines
1236
2049
  }));
1237
- return { outDir: resolve7(outDir), date, files: written, nextNumber: next };
2050
+ return { outDir: resolve9(outDir), date, files: written, nextNumber: next };
1238
2051
  }
1239
2052
  async function promoteAuditPlans(outDir, selected, options) {
1240
2053
  if (selected.length === 0) {
@@ -1243,19 +2056,24 @@ async function promoteAuditPlans(outDir, selected, options) {
1243
2056
  if (typeof options.harnessDir !== "string" || options.harnessDir.trim() === "") {
1244
2057
  throw new Error("promoteAuditPlans: options.harnessDir is required (must contain status.json + workflows/)");
1245
2058
  }
1246
- const workflowId = options.workflowId ?? basename3(resolve7(outDir));
2059
+ if (typeof options.deliveryKind !== "string" || !WORKFLOW_DELIVERY_KINDS.includes(options.deliveryKind)) {
2060
+ throw new Error(`promoteAuditPlans: options.deliveryKind must be one of ${WORKFLOW_DELIVERY_KINDS.join(" | ")} — got ${JSON.stringify(options.deliveryKind)} (a promoted plan workflow declares its delivery kind at registration; it is never inferred and never defaulted)`);
2061
+ }
2062
+ assertDeliveryRegistrationCoherence(options.deliveryKind, options, "promoteAuditPlans");
2063
+ const workflowId = options.workflowId ?? basename5(resolve9(outDir));
1247
2064
  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);
2065
+ const harnessDir = resolve9(options.harnessDir);
2066
+ const statusPath = join9(harnessDir, "status.json");
2067
+ const workflowDir = join9(harnessDir, "workflows", workflowId);
2068
+ const snapshotPath = join9(workflowDir, WORKFLOW_SNAPSHOT_FILE);
2069
+ const store = getArtifactStore();
1252
2070
  const planFiles = resolveSelectedPlanFiles(outDir, selected);
1253
2071
  const indexRows = readExecutionOrderIndex(outDir);
1254
2072
  const plans = planFiles.map((planFile) => {
1255
2073
  const stem = planFile.replace(/\.md$/, "");
1256
2074
  const num = stem.slice(0, 3);
1257
2075
  const indexRow = indexRows.get(num);
1258
- const title = indexRow?.title ?? readPlanFileSummary(join7(outDir, planFile)).title;
2076
+ const title = indexRow?.title ?? readPlanFileSummary(join9(outDir, planFile)).title;
1259
2077
  return {
1260
2078
  id: stem,
1261
2079
  title,
@@ -1271,8 +2089,17 @@ async function promoteAuditPlans(outDir, selected, options) {
1271
2089
  status: "running",
1272
2090
  started_at: now.toISOString(),
1273
2091
  updated_at: now.toISOString().slice(0, 10),
1274
- plans
2092
+ plans,
2093
+ delivery_kind: options.deliveryKind
1275
2094
  };
2095
+ if (options.branchSource !== undefined || options.branchTarget !== undefined) {
2096
+ snapshot.branch = {
2097
+ ...options.branchSource !== undefined ? { source: options.branchSource } : {},
2098
+ ...options.branchTarget !== undefined ? { target: options.branchTarget } : {}
2099
+ };
2100
+ }
2101
+ if (options.completionPolicy !== undefined)
2102
+ snapshot.completion_policy = options.completionPolicy;
1276
2103
  const entry = {
1277
2104
  id: workflowId,
1278
2105
  type: "plan",
@@ -1284,18 +2111,29 @@ async function promoteAuditPlans(outDir, selected, options) {
1284
2111
  throw new Error(`refusing to register invalid workflow entry: ${entryGate.violations.map((v) => v.message).join("; ")}`);
1285
2112
  }
1286
2113
  await withStatusWriteLock(statusPath, async () => {
1287
- if (existsSync5(snapshotPath)) {
2114
+ if (existsSync7(snapshotPath)) {
1288
2115
  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
2116
  }
1290
- mkdirSync4(workflowDir, { recursive: true });
2117
+ let createdVersion;
1291
2118
  try {
1292
- writeJson(snapshotPath, snapshot);
2119
+ await writeWorkflowSnapshot(snapshot, workflowDir, { createOnly: true });
2120
+ createdVersion = readArtifactBytes(snapshotPath)?.version;
1293
2121
  await registerWorkflowEntryLocked(statusPath, entry);
1294
2122
  } catch (error) {
1295
- rmSync(snapshotPath, { force: true });
2123
+ if (createdVersion !== undefined) {
2124
+ await withStatusWriteLock(snapshotPath, async () => {
2125
+ const current = readArtifactBytes(snapshotPath);
2126
+ if (current === undefined || current.version !== createdVersion)
2127
+ return;
2128
+ const remove = store.delete?.bind(store);
2129
+ if (remove !== undefined) {
2130
+ await withProtectedWrite(snapshotPath, "delete", () => remove({ kind: "snapshot", key: workflowId }));
2131
+ }
2132
+ });
2133
+ }
1296
2134
  try {
1297
- if (readdirSync4(workflowDir).length === 0) {
1298
- rmdirSync2(workflowDir);
2135
+ if (readdirSync5(workflowDir).length === 0) {
2136
+ rmdirSync3(workflowDir);
1299
2137
  }
1300
2138
  } catch {}
1301
2139
  throw error;
@@ -1305,7 +2143,7 @@ async function promoteAuditPlans(outDir, selected, options) {
1305
2143
  return { workflowId, snapshotPath };
1306
2144
  }
1307
2145
  function resolveSelectedPlanFiles(outDir, selected) {
1308
- const files = readdirSync4(outDir).filter((f) => /^\d{3}-.*\.md$/.test(f)).sort();
2146
+ const files = readdirSync5(outDir).filter((f) => /^\d{3}-.*\.md$/.test(f)).sort();
1309
2147
  const byNum = new Map;
1310
2148
  const byStem = new Map;
1311
2149
  for (const file of files) {
@@ -1320,7 +2158,7 @@ function resolveSelectedPlanFiles(outDir, selected) {
1320
2158
  for (const id of selected) {
1321
2159
  const file = byNum.get(id) ?? byStem.get(id) ?? byStem.get(id.replace(/\.md$/, ""));
1322
2160
  if (file === undefined) {
1323
- throw new Error(`promoteAuditPlans: selected plan ${JSON.stringify(id)} does not match any NNN-*.md file in ${resolve7(outDir)}`);
2161
+ throw new Error(`promoteAuditPlans: selected plan ${JSON.stringify(id)} does not match any NNN-*.md file in ${resolve9(outDir)}`);
1324
2162
  }
1325
2163
  if (!seen.has(file)) {
1326
2164
  seen.add(file);
@@ -1330,10 +2168,10 @@ function resolveSelectedPlanFiles(outDir, selected) {
1330
2168
  return resolved;
1331
2169
  }
1332
2170
  function readExecutionOrderIndex(outDir) {
1333
- const readmePath = join7(outDir, "README.md");
2171
+ const readmePath = join9(outDir, "README.md");
1334
2172
  let text;
1335
2173
  try {
1336
- text = readFileSync5(readmePath, "utf8");
2174
+ text = readFileSync7(readmePath, "utf8");
1337
2175
  } catch {
1338
2176
  return new Map;
1339
2177
  }
@@ -1359,11 +2197,11 @@ function readExecutionOrderIndex(outDir) {
1359
2197
  return rows;
1360
2198
  }
1361
2199
  function planFileRel(outDir, planFile) {
1362
- const resolved = resolve7(outDir);
1363
- const parts = resolved.split(sep2);
2200
+ const resolved = resolve9(outDir);
2201
+ const parts = resolved.split(sep3);
1364
2202
  const plansIdx = parts.lastIndexOf("plans");
1365
2203
  if (plansIdx >= 0) {
1366
- return `${parts.slice(plansIdx + 1).join(sep2)}${sep2}${planFile}`;
2204
+ return `${parts.slice(plansIdx + 1).join(sep3)}${sep3}${planFile}`;
1367
2205
  }
1368
2206
  return planFile;
1369
2207
  }