@mstar-harness/engine 3.9.4 → 3.10.1

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/engine.js CHANGED
@@ -171,21 +171,21 @@ function isAtOrBelow(dir, root) {
171
171
  return rel === "" || !rel.startsWith("..") && !isAbsolute(rel);
172
172
  }
173
173
  // src/path.ts
174
- import { existsSync as existsSync8, mkdirSync as mkdirSync5, readdirSync as readdirSync5, readFileSync as readFileSync8, realpathSync as realpathSync3, statSync as statSync3, writeFileSync as writeFileSync3 } from "node:fs";
174
+ import { existsSync as existsSync8, mkdirSync as mkdirSync5, readdirSync as readdirSync6, readFileSync as readFileSync8, realpathSync as realpathSync3, statSync as statSync3, writeFileSync as writeFileSync3 } from "node:fs";
175
175
  import { execFileSync } from "node:child_process";
176
- import { basename as basename5, dirname as dirname6, isAbsolute as isAbsolute6, join as join10, relative as relative2, resolve as resolve8 } from "node:path";
176
+ import { basename as basename5, dirname as dirname6, isAbsolute as isAbsolute6, join as join10, relative as relative2, resolve as resolve9 } from "node:path";
177
177
 
178
178
  // src/project.ts
179
- import { existsSync as existsSync7, mkdirSync as mkdirSync4, readFileSync as readFileSync7, readdirSync as readdirSync4 } from "node:fs";
180
- import { basename as basename4, join as join9, resolve as resolve7 } from "node:path";
179
+ import { existsSync as existsSync7, mkdirSync as mkdirSync4, readFileSync as readFileSync7, readdirSync as readdirSync5 } from "node:fs";
180
+ import { basename as basename4, join as join9, resolve as resolve8 } from "node:path";
181
181
 
182
182
  // src/iteration.ts
183
- import { existsSync as existsSync6, readdirSync as readdirSync3, readFileSync as readFileSync6 } from "node:fs";
183
+ import { existsSync as existsSync6, readdirSync as readdirSync4, readFileSync as readFileSync6 } from "node:fs";
184
184
  import { join as join8 } from "node:path";
185
185
 
186
186
  // src/status.ts
187
- import { existsSync as existsSync5, readFileSync as readFileSync5, readdirSync as readdirSync2, realpathSync as realpathSync2 } from "node:fs";
188
- import { dirname as dirname5, join as join7, resolve as resolve6, sep as sep2 } from "node:path";
187
+ import { existsSync as existsSync5, readFileSync as readFileSync5, readdirSync as readdirSync3, realpathSync as realpathSync2 } from "node:fs";
188
+ import { dirname as dirname5, join as join7, resolve as resolve7, sep as sep2 } from "node:path";
189
189
 
190
190
  // src/lease.ts
191
191
  import { mkdirSync as mkdirSync2, rmdirSync, statSync as statSync2, unlinkSync as unlinkSync2, writeFileSync as writeFileSync2 } from "node:fs";
@@ -1041,6 +1041,7 @@ var REQUIRED_FIELDS = [
1041
1041
  var REVIEW_SEAT_ROLES = ["qc-specialist", "qc-specialist-2", "qc-specialist-3", "code-reviewer", "qa-engineer"];
1042
1042
  var BUDGET_LABELS = ["Budget (review / QC seats)", "Budget"];
1043
1043
  var RETURN_SHAPE_LABELS = ["Return shape (review / QC seats)", "Return shape"];
1044
+ var TASK_BUDGET_LABELS = ["Task budget (implement / ops rounds)", "Task budget"];
1044
1045
  function violation2(severity, code, message, fix) {
1045
1046
  return { ok: false, severity, code, message, fix };
1046
1047
  }
@@ -1068,6 +1069,8 @@ function parseAssignmentFields(assignmentText) {
1068
1069
  fields.budget = value;
1069
1070
  else if (RETURN_SHAPE_LABELS.includes(label))
1070
1071
  fields.returnShape = value;
1072
+ else if (TASK_BUDGET_LABELS.includes(label))
1073
+ fields.taskBudget = value;
1071
1074
  }
1072
1075
  return fields;
1073
1076
  }
@@ -1182,6 +1185,12 @@ function validateAssignmentFields(assignmentText, opts = {}) {
1182
1185
  violations.push(violation2("high", field.code, `${subject} must declare a round ${field.label} — ${missing}`, field.fix));
1183
1186
  }
1184
1187
  }
1188
+ if (!reviewSeat && !auditRound) {
1189
+ const missing = describeAbsence(fields.taskBudget);
1190
+ if (missing !== undefined) {
1191
+ violations.push(violation2("high", "assignment.field.task-budget-missing", `implement/ops round "${role}" must declare a Task budget — ${missing}`, `add "**Task budget (implement / ops rounds)**: <one implementer round closing the task's declared Files list and verification gates> — capacity contract spec § A1/A2"`));
1192
+ }
1193
+ }
1185
1194
  if (writable) {
1186
1195
  const workingPresent = fields.workingBranch !== undefined && fields.workingBranch !== "";
1187
1196
  const policyPresent = fields.branchPolicy !== undefined && fields.branchPolicy !== "";
@@ -1314,12 +1323,14 @@ function antiRecursionPrecheck(subagentType, executeAs) {
1314
1323
  }
1315
1324
 
1316
1325
  // src/workflow.ts
1317
- import { existsSync as existsSync4, mkdirSync as mkdirSync3, readFileSync as readFileSync4 } from "node:fs";
1318
- import { isAbsolute as isAbsolute5, join as join6 } from "node:path";
1326
+ import { existsSync as existsSync4, mkdirSync as mkdirSync3, readFileSync as readFileSync4, readdirSync as readdirSync2, rmdirSync as rmdirSync2 } from "node:fs";
1327
+ import { isAbsolute as isAbsolute5, join as join6, resolve as resolve6 } from "node:path";
1319
1328
  var WORKFLOW_SNAPSHOT_FILE = "snapshot.json";
1320
1329
  var WORKFLOW_LIFECYCLE_STATUSES = ["running", "paused", "completed", "failed", "stopped"];
1321
1330
  var WORKFLOW_TERMINAL_STATUSES = ["completed", "failed", "stopped"];
1322
1331
  var WORKFLOW_LIFECYCLE_TYPES = ["plan", "iteration"];
1332
+ var WORKFLOW_DELIVERY_KINDS = ["development", "verification/report-only"];
1333
+ var WORKFLOW_COMPOUND_OUTCOMES = ["created", "updated", "skipped"];
1323
1334
  function stableJson(value) {
1324
1335
  if (Array.isArray(value))
1325
1336
  return `[${value.map(stableJson).join(",")}]`;
@@ -1344,6 +1355,61 @@ function validateWorktreePathValue(violations, value, field) {
1344
1355
  violations.push(violation3("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)"));
1345
1356
  }
1346
1357
  }
1358
+ function deliveryEvidenceViolations(value, what) {
1359
+ const violations = [];
1360
+ const invalid2 = (message) => {
1361
+ violations.push(violation3("medium", "workflow.snapshot.invalid-delivery-evidence", `${what}: ${message}`));
1362
+ };
1363
+ if (!isPlainObject2(value)) {
1364
+ invalid2("must be an object");
1365
+ return violations;
1366
+ }
1367
+ const members = ["compound", "pr", "merge", "completion"];
1368
+ const unknownMembers = Object.keys(value).filter((key) => !members.includes(key));
1369
+ if (unknownMembers.length > 0)
1370
+ invalid2(`unknown member(s) ${unknownMembers.join(", ")} — expected ${members.join(" | ")}`);
1371
+ const compound = value.compound;
1372
+ if (compound !== undefined) {
1373
+ if (!isPlainObject2(compound))
1374
+ invalid2("compound must be an object");
1375
+ else {
1376
+ const unknown = Object.keys(compound).filter((key) => key !== "outcome" && key !== "reason");
1377
+ if (unknown.length > 0)
1378
+ invalid2(`compound has unknown key(s) ${unknown.join(", ")}`);
1379
+ if (typeof compound.outcome !== "string" || !WORKFLOW_COMPOUND_OUTCOMES.includes(compound.outcome)) {
1380
+ invalid2(`compound.outcome must be one of ${WORKFLOW_COMPOUND_OUTCOMES.join(" | ")} — got ${JSON.stringify(compound.outcome)}`);
1381
+ } else if (compound.outcome === "skipped" && (typeof compound.reason !== "string" || compound.reason.trim() === "")) {
1382
+ invalid2("compound reason is required when the disposition outcome is 'skipped' (contract §4c)");
1383
+ } else if (compound.reason !== undefined && (typeof compound.reason !== "string" || compound.reason.trim() === "")) {
1384
+ invalid2("compound.reason must be a non-empty string when given");
1385
+ }
1386
+ }
1387
+ }
1388
+ const stringMembers = {
1389
+ pr: ["repo", "head", "target"],
1390
+ merge: ["provider", "evidence"],
1391
+ completion: ["policy", "evidence"]
1392
+ };
1393
+ for (const member of ["pr", "merge", "completion"]) {
1394
+ const block = value[member];
1395
+ if (block === undefined)
1396
+ continue;
1397
+ if (!isPlainObject2(block)) {
1398
+ invalid2(`${member} must be an object`);
1399
+ continue;
1400
+ }
1401
+ const fields = stringMembers[member];
1402
+ const unknown = Object.keys(block).filter((key) => !fields.includes(key));
1403
+ if (unknown.length > 0)
1404
+ invalid2(`${member} has unknown key(s) ${unknown.join(", ")}`);
1405
+ for (const field of fields) {
1406
+ if (typeof block[field] !== "string" || block[field].trim() === "") {
1407
+ invalid2(`${member}.${field} must be a non-empty string`);
1408
+ }
1409
+ }
1410
+ }
1411
+ return violations;
1412
+ }
1347
1413
  function validateWorkflowSnapshot(doc) {
1348
1414
  const violations = [];
1349
1415
  if (!isPlainObject2(doc)) {
@@ -1409,7 +1475,7 @@ function validateWorkflowSnapshot(doc) {
1409
1475
  if (!isPlainObject2(doc.branch)) {
1410
1476
  violations.push(violation3("medium", "workflow.snapshot.invalid-branch", "branch must be an object"));
1411
1477
  } else {
1412
- for (const key of ["base", "integration", "target"]) {
1478
+ for (const key of ["base", "source", "integration", "target"]) {
1413
1479
  if (doc.branch[key] !== undefined && (typeof doc.branch[key] !== "string" || doc.branch[key].trim() === "")) {
1414
1480
  violations.push(violation3("medium", "workflow.snapshot.invalid-branch", `branch.${key} must be a non-empty string`));
1415
1481
  }
@@ -1435,6 +1501,20 @@ function validateWorkflowSnapshot(doc) {
1435
1501
  if (doc.compass_ref !== undefined) {
1436
1502
  validateNonEmptyString2(violations, doc.compass_ref, "compass_ref", "workflow.snapshot.missing-compass-ref", "workflow.snapshot.invalid-compass-ref");
1437
1503
  }
1504
+ if (doc.delivery_kind !== undefined) {
1505
+ if (typeof doc.delivery_kind !== "string" || !WORKFLOW_DELIVERY_KINDS.includes(doc.delivery_kind)) {
1506
+ violations.push(violation3("medium", "workflow.snapshot.invalid-delivery-kind", `delivery_kind must be one of ${WORKFLOW_DELIVERY_KINDS.join(" | ")} — got ${JSON.stringify(doc.delivery_kind)}`));
1507
+ }
1508
+ }
1509
+ if (doc.project !== undefined) {
1510
+ validateNonEmptyString2(violations, doc.project, "project", "workflow.snapshot.missing-project", "workflow.snapshot.invalid-project");
1511
+ }
1512
+ if (doc.completion_policy !== undefined) {
1513
+ validateNonEmptyString2(violations, doc.completion_policy, "completion_policy", "workflow.snapshot.missing-completion-policy", "workflow.snapshot.invalid-completion-policy");
1514
+ }
1515
+ if (doc.delivery !== undefined) {
1516
+ violations.push(...deliveryEvidenceViolations(doc.delivery, "delivery"));
1517
+ }
1438
1518
  const terminal = typeof doc.status === "string" && WORKFLOW_TERMINAL_STATUSES.includes(doc.status);
1439
1519
  if (terminal) {
1440
1520
  if (doc.ended_at === undefined) {
@@ -1565,6 +1645,74 @@ async function validateAndPutWorkflowSnapshot(store, snapshot, snapshotPath) {
1565
1645
  function isTerminalSnapshot(doc) {
1566
1646
  return WORKFLOW_TERMINAL_STATUSES.includes(doc.status);
1567
1647
  }
1648
+ function nonEmptyString(value) {
1649
+ return typeof value === "string" && value.trim() !== "";
1650
+ }
1651
+ function assertDeliveryRegistrationCoherence(kind, evidence, what) {
1652
+ if (kind === "development" && (!nonEmptyString(evidence.branchSource) || !nonEmptyString(evidence.branchTarget))) {
1653
+ 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)`);
1654
+ }
1655
+ if (kind === "verification/report-only" && !nonEmptyString(evidence.completionPolicy)) {
1656
+ throw new Error(`${what}: a verification/report-only workflow requires the completion policy (--completion-policy) naming the evidence that completes it (contract §1)`);
1657
+ }
1658
+ for (const [field, value] of Object.entries({ branchSource: evidence.branchSource, branchTarget: evidence.branchTarget, completionPolicy: evidence.completionPolicy })) {
1659
+ if (value !== undefined && !nonEmptyString(value)) {
1660
+ throw new Error(`${what}: ${field} must be a non-empty string when given`);
1661
+ }
1662
+ }
1663
+ }
1664
+ function consultDeliveryEvidence(snapshot) {
1665
+ if (snapshot.type !== "plan")
1666
+ return [];
1667
+ const workflowId = nonEmptyString(snapshot.id) ? snapshot.id : "<unknown>";
1668
+ const kind = snapshot.delivery_kind;
1669
+ if (typeof kind !== "string" || !WORKFLOW_DELIVERY_KINDS.includes(kind)) {
1670
+ return [
1671
+ violation3("high", "PHASE6_DELIVERY_KIND_UNREGISTERED", `Workflow '${workflowId}' is type 'plan' but carries no registered delivery_kind — a plan workflow declares its delivery kind at registration (mstar-artifacts/references/plan-workflow-lifecycle-contract.md §1), so this snapshot's delivery evidence cannot be consulted`, "Delivery evidence is declared at registration, before execution. A still-ACTIVE kind-less workflow is repaired by the authorized one-time declaration 'mstar workflow evidence --workflow <id> --declare-kind <development|verification/report-only> [--branch-source <b> --branch-target <b> | --completion-policy <text>] [--session <envelope>]' (declared once, never re-declared); a TERMINAL legacy snapshot cannot be backfilled — 'mstar workflow register' is create-only and its registration identity can never match a terminal snapshot — so repair requires an explicit owner snapshot amendment recording the declared kind (the known affected population — audit-promotion's grandfathered type: plan snapshots — is disclosed as a residual by plan QC), then re-run the close / 'mstar iteration gate --phase 6 --workflow <id>'")
1672
+ ];
1673
+ }
1674
+ const branch = isPlainObject2(snapshot.branch) ? snapshot.branch : undefined;
1675
+ const delivery = isPlainObject2(snapshot.delivery) ? snapshot.delivery : undefined;
1676
+ const missing = [];
1677
+ if (kind === "development") {
1678
+ if (!nonEmptyString(branch?.source))
1679
+ missing.push("branch.source");
1680
+ if (!nonEmptyString(branch?.target))
1681
+ missing.push("branch.target");
1682
+ if (!isPlainObject2(delivery?.compound))
1683
+ missing.push("delivery.compound (compound disposition, §4c)");
1684
+ const pr = isPlainObject2(delivery?.pr) ? delivery.pr : undefined;
1685
+ if (pr === undefined) {
1686
+ missing.push("delivery.pr (PR repo/head/target identity, §4d)");
1687
+ } else {
1688
+ if (nonEmptyString(branch?.source) && pr.head !== branch.source) {
1689
+ missing.push(`delivery.pr.head (recorded ${JSON.stringify(pr.head)}, registered branch.source ${JSON.stringify(branch.source)} — §4d: the recorded PR identity must be the registered delivery)`);
1690
+ }
1691
+ if (nonEmptyString(branch?.target) && pr.target !== branch.target) {
1692
+ missing.push(`delivery.pr.target (recorded ${JSON.stringify(pr.target)}, registered branch.target ${JSON.stringify(branch.target)} — §4d: the recorded PR identity must be the registered delivery)`);
1693
+ }
1694
+ }
1695
+ if (!isPlainObject2(delivery?.merge))
1696
+ missing.push("delivery.merge (PM-recorded verified-merge evidence, §4f)");
1697
+ } else {
1698
+ const policy = snapshot.completion_policy;
1699
+ if (!nonEmptyString(policy))
1700
+ missing.push("completion_policy (the recorded alternative completion policy, §1)");
1701
+ const completion = isPlainObject2(delivery?.completion) ? delivery.completion : undefined;
1702
+ if (completion === undefined) {
1703
+ missing.push("delivery.completion (the fulfilment record of the registered policy, §1)");
1704
+ } else if (nonEmptyString(policy) && completion.policy !== policy) {
1705
+ missing.push(`delivery.completion.policy (§1 — recorded ${JSON.stringify(completion.policy)}, registered completion_policy ${JSON.stringify(policy)})`);
1706
+ }
1707
+ }
1708
+ if (missing.length === 0)
1709
+ return [];
1710
+ const incomplete = (fix) => violation3("high", "PHASE6_DELIVERY_EVIDENCE_INCOMPLETE", `Workflow '${workflowId}' declares delivery_kind '${kind}' but its delivery evidence is incomplete — missing: ${missing.join(", ")} (mstar-artifacts/references/plan-workflow-lifecycle-contract.md §3 lifecycle stages + §4c/§4d/§4f)`, fix);
1711
+ const record = `Record the missing evidence with 'mstar workflow evidence --workflow ${workflowId} --file <payload.json>' (add --session <coordinator envelope> for a coordinated workflow)`;
1712
+ return [
1713
+ kind === "development" ? incomplete(`${record}: the compound disposition (§4c, before the PR head is finalized), the PR identity (§4d, at submission) and the verified-merge record (§4f, the PM's own check — the engine never verifies the remote merge). Done rows alone are not delivery evidence`) : incomplete(`${record}: the fulfilment record must name the registered completion policy and its evidence (§1)`)
1714
+ ];
1715
+ }
1568
1716
  function isCloseTimestamp(value) {
1569
1717
  if (typeof value !== "string")
1570
1718
  return false;
@@ -1597,11 +1745,275 @@ async function closeWorkflow(workflowId, dir, opts) {
1597
1745
  if (snapshot.plans.some((row) => row.status !== "Done")) {
1598
1746
  throw new Error("refusing to close workflow: every plan row must be Done");
1599
1747
  }
1748
+ const deliveryViolations = consultDeliveryEvidence(snapshot);
1749
+ if (deliveryViolations.length > 0) {
1750
+ const detail = deliveryViolations.map((v) => `${v.code}: ${v.message}${v.fix !== undefined ? ` (fix: ${v.fix})` : ""}`).join("; ");
1751
+ throw new Error(`refusing to close workflow: ${detail}`);
1752
+ }
1600
1753
  const completed = { ...snapshot, status: "completed", ended_at: opts.endedAt, updated_at: opts.endedAt };
1601
1754
  await validateAndPutWorkflowSnapshot(store, completed, snapshotPath);
1602
1755
  return completed;
1603
1756
  });
1604
1757
  }
1758
+ async function recordWorkflowDelivery(workflowId, dir, opts) {
1759
+ const evidence = opts.evidence;
1760
+ if (!isPlainObject2(evidence)) {
1761
+ throw new Error("recordWorkflowDelivery: options.evidence must be an object naming at least one delivery-evidence member");
1762
+ }
1763
+ const members = Object.keys(evidence);
1764
+ if (members.length === 0) {
1765
+ throw new Error("recordWorkflowDelivery: options.evidence must name at least one of compound | pr | merge | completion");
1766
+ }
1767
+ const nonObject = members.filter((member) => !isPlainObject2(evidence[member]));
1768
+ if (nonObject.length > 0) {
1769
+ throw new Error(`refusing to record delivery evidence: member(s) ${nonObject.join(", ")} must be objects — omit a member to leave it untouched`);
1770
+ }
1771
+ const shapeViolations = deliveryEvidenceViolations(evidence, "evidence");
1772
+ if (shapeViolations.length > 0) {
1773
+ throw new Error(`refusing to record invalid delivery evidence: ${shapeViolations.map((v) => v.message).join("; ")}`);
1774
+ }
1775
+ const at = opts.at ?? new Date().toISOString();
1776
+ if (!isCloseTimestamp(at)) {
1777
+ throw new Error("recordWorkflowDelivery: options.at must be a valid YYYY-MM-DD date or RFC3339 timestamp");
1778
+ }
1779
+ const snapshotPath = join6(dir, WORKFLOW_SNAPSHOT_FILE);
1780
+ const store = getArtifactStore();
1781
+ const ref = { kind: "snapshot", key: workflowId };
1782
+ assertFsStorePath(store, ref, snapshotPath);
1783
+ mkdirSync3(dir, { recursive: true });
1784
+ return withStatusWriteLock(snapshotPath, async () => {
1785
+ const doc = await store.get(ref);
1786
+ if (doc === undefined)
1787
+ throw new Error(`workflow snapshot not found: ${snapshotPath}`);
1788
+ const { snapshot } = normalizeWorkflowSnapshot(doc, snapshotPath);
1789
+ if (snapshot.id !== workflowId) {
1790
+ throw new Error(`workflow snapshot identity mismatch: expected ${workflowId}, got ${snapshot.id}`);
1791
+ }
1792
+ if (isTerminalSnapshot(snapshot)) {
1793
+ throw new Error(`refusing to record delivery evidence for workflow ${JSON.stringify(workflowId)}: the lifecycle is terminal (${snapshot.status}) — delivery evidence is recorded before the close and a closed lifecycle is never amended`);
1794
+ }
1795
+ assertCoordinatedSnapshotWriter(doc, snapshotPath, opts.sessionPath, "delivery-evidence write");
1796
+ const kind = snapshot.delivery_kind;
1797
+ if (snapshot.type !== "plan" || kind !== "development" && kind !== "verification/report-only") {
1798
+ throw new Error(`refusing to record delivery evidence for workflow ${JSON.stringify(workflowId)}: only a type: plan lifecycle with a registered delivery_kind carries delivery evidence (got type ${JSON.stringify(snapshot.type)} / delivery_kind ${JSON.stringify(kind)}) — the kind is declared at registration and never inferred (§1)`);
1799
+ }
1800
+ const allowed = kind === "development" ? ["compound", "pr", "merge"] : ["completion"];
1801
+ const unused = members.filter((member) => !allowed.includes(member));
1802
+ if (unused.length > 0) {
1803
+ throw new Error(`refusing to record delivery evidence for workflow ${JSON.stringify(workflowId)}: member(s) ${unused.join(", ")} do not belong to the declared delivery_kind '${kind}' (expected ${allowed.join(" | ")})`);
1804
+ }
1805
+ const stored = isPlainObject2(snapshot.delivery) ? snapshot.delivery : {};
1806
+ const recordedPr = isPlainObject2(stored.pr) ? stored.pr : undefined;
1807
+ const incomingPr = isPlainObject2(evidence.pr) ? evidence.pr : undefined;
1808
+ if (recordedPr !== undefined && incomingPr !== undefined && stableJson(recordedPr) !== stableJson(incomingPr)) {
1809
+ throw new Error(`refusing to rewrite the recorded PR identity of workflow ${JSON.stringify(workflowId)}: §4d records it once at submission — recorded ${JSON.stringify(recordedPr)}, refused ${JSON.stringify(incomingPr)} (a different PR is a different delivery, not an evidence update)`);
1810
+ }
1811
+ const merged = { ...stored, ...evidence };
1812
+ if (stableJson(snapshot.delivery ?? null) === stableJson(merged)) {
1813
+ return { snapshot, written: false };
1814
+ }
1815
+ const next = { ...snapshot, delivery: merged, updated_at: at };
1816
+ await validateAndPutWorkflowSnapshot(store, next, snapshotPath);
1817
+ return { snapshot: next, written: true };
1818
+ });
1819
+ }
1820
+ async function declareWorkflowDeliveryKind(workflowId, dir, opts) {
1821
+ const kind = opts.deliveryKind;
1822
+ if (typeof kind !== "string" || !WORKFLOW_DELIVERY_KINDS.includes(kind)) {
1823
+ throw new Error(`declareWorkflowDeliveryKind: deliveryKind must be one of ${WORKFLOW_DELIVERY_KINDS.join(" | ")} — got ${JSON.stringify(kind)}`);
1824
+ }
1825
+ assertDeliveryRegistrationCoherence(kind, opts, "declareWorkflowDeliveryKind");
1826
+ const at = opts.at ?? new Date().toISOString();
1827
+ if (!isCloseTimestamp(at)) {
1828
+ throw new Error("declareWorkflowDeliveryKind: options.at must be a valid YYYY-MM-DD date or RFC3339 timestamp");
1829
+ }
1830
+ const snapshotPath = join6(dir, WORKFLOW_SNAPSHOT_FILE);
1831
+ const store = getArtifactStore();
1832
+ const ref = { kind: "snapshot", key: workflowId };
1833
+ assertFsStorePath(store, ref, snapshotPath);
1834
+ mkdirSync3(dir, { recursive: true });
1835
+ return withStatusWriteLock(snapshotPath, async () => {
1836
+ const doc = await store.get(ref);
1837
+ if (doc === undefined)
1838
+ throw new Error(`workflow snapshot not found: ${snapshotPath}`);
1839
+ const { snapshot } = normalizeWorkflowSnapshot(doc, snapshotPath);
1840
+ if (snapshot.id !== workflowId) {
1841
+ throw new Error(`workflow snapshot identity mismatch: expected ${workflowId}, got ${snapshot.id}`);
1842
+ }
1843
+ if (isTerminalSnapshot(snapshot)) {
1844
+ throw new Error(`refusing to declare a delivery kind for workflow ${JSON.stringify(workflowId)}: the lifecycle is terminal (${snapshot.status}) — a closed lifecycle is never amended (§5); a terminal snapshot without a registered kind stays on the documented owner-amendment path`);
1845
+ }
1846
+ assertCoordinatedSnapshotWriter(doc, snapshotPath, opts.sessionPath, "delivery-kind declaration");
1847
+ if (snapshot.type !== "plan") {
1848
+ throw new Error(`refusing to declare a delivery kind for workflow ${JSON.stringify(workflowId)}: only a type: plan lifecycle declares one (got type ${JSON.stringify(snapshot.type)})`);
1849
+ }
1850
+ if (snapshot.delivery_kind !== undefined) {
1851
+ throw new Error(`refusing to declare a delivery kind for workflow ${JSON.stringify(workflowId)}: it already declares ${JSON.stringify(snapshot.delivery_kind)} — the kind is registration evidence, declared once and never re-inferred (§1)`);
1852
+ }
1853
+ const branch = isPlainObject2(snapshot.branch) ? { ...snapshot.branch } : {};
1854
+ for (const [anchor, supplied] of [
1855
+ ["source", opts.branchSource],
1856
+ ["target", opts.branchTarget]
1857
+ ]) {
1858
+ if (supplied === undefined)
1859
+ continue;
1860
+ const registered = branch[anchor];
1861
+ if (registered !== undefined && registered !== supplied) {
1862
+ throw new Error(`refusing to declare a delivery kind for workflow ${JSON.stringify(workflowId)}: branch.${anchor} is already ` + `${JSON.stringify(registered)} — the supplied ${JSON.stringify(supplied)} conflicts with the anchor the snapshot ` + `registers, and the registered delivery anchor is never overwritten (§1)`);
1863
+ }
1864
+ branch[anchor] = supplied;
1865
+ }
1866
+ const next = { ...snapshot, delivery_kind: kind, updated_at: at };
1867
+ if (Object.keys(branch).length > 0)
1868
+ next.branch = branch;
1869
+ if (opts.completionPolicy !== undefined)
1870
+ next.completion_policy = opts.completionPolicy;
1871
+ await validateAndPutWorkflowSnapshot(store, next, snapshotPath);
1872
+ return next;
1873
+ });
1874
+ }
1875
+ function planWorkflowRegistrationIdentity(snapshot) {
1876
+ const coordinator = snapshot.coordination?.coordinator;
1877
+ return stableJson({
1878
+ type: snapshot.type,
1879
+ status: snapshot.status,
1880
+ delivery_kind: snapshot.delivery_kind ?? null,
1881
+ project: snapshot.project ?? null,
1882
+ completion_policy: snapshot.completion_policy ?? null,
1883
+ branch: snapshot.branch ?? null,
1884
+ plans: snapshot.plans,
1885
+ coordinator: coordinator ? { session_id: coordinator.session_id, session_file: coordinator.session_file } : null
1886
+ });
1887
+ }
1888
+ async function registerPlanWorkflow(workflowId, options) {
1889
+ if (typeof options.harnessDir !== "string" || options.harnessDir.trim() === "") {
1890
+ throw new Error("registerPlanWorkflow: options.harnessDir is required (must contain status.json + workflows/)");
1891
+ }
1892
+ assertSafePathComponent(workflowId, "workflow id");
1893
+ const { plan, deliveryKind } = options;
1894
+ if (!isPlainObject2(plan)) {
1895
+ throw new Error("registerPlanWorkflow: options.plan is required (the owned plan: id, title, file)");
1896
+ }
1897
+ for (const field of ["id", "title", "file"]) {
1898
+ if (typeof plan[field] !== "string" || plan[field].trim() === "") {
1899
+ throw new Error(`registerPlanWorkflow: options.plan.${field} must be a non-empty string`);
1900
+ }
1901
+ }
1902
+ if (typeof deliveryKind !== "string" || !WORKFLOW_DELIVERY_KINDS.includes(deliveryKind)) {
1903
+ throw new Error(`registerPlanWorkflow: options.deliveryKind must be one of ${WORKFLOW_DELIVERY_KINDS.join(" | ")} — got ${JSON.stringify(deliveryKind)}`);
1904
+ }
1905
+ assertDeliveryRegistrationCoherence(deliveryKind, options, "registerPlanWorkflow");
1906
+ for (const field of ["project", "branchSource", "branchTarget"]) {
1907
+ const value = options[field];
1908
+ if (value !== undefined && (typeof value !== "string" || value.trim() === "")) {
1909
+ throw new Error(`registerPlanWorkflow: options.${field} must be a non-empty string when given`);
1910
+ }
1911
+ }
1912
+ if (options.coordinator !== undefined) {
1913
+ const { coordinator } = options;
1914
+ if (typeof coordinator.session_id !== "string" || coordinator.session_id.trim() === "") {
1915
+ throw new Error("registerPlanWorkflow: options.coordinator.session_id must be a non-empty string");
1916
+ }
1917
+ if (typeof coordinator.session_file !== "string" || !isAbsolute5(coordinator.session_file)) {
1918
+ throw new Error("registerPlanWorkflow: options.coordinator.session_file must be an absolute path");
1919
+ }
1920
+ }
1921
+ const startedAt = options.startedAt ?? new Date().toISOString();
1922
+ if (!isCloseTimestamp(startedAt)) {
1923
+ throw new Error("registerPlanWorkflow: options.startedAt must be a valid YYYY-MM-DD date or RFC3339 timestamp");
1924
+ }
1925
+ const harnessDir = resolve6(options.harnessDir);
1926
+ const statusPath = join6(harnessDir, "status.json");
1927
+ const workflowDir = join6(harnessDir, "workflows", workflowId);
1928
+ const snapshotPath = join6(workflowDir, WORKFLOW_SNAPSHOT_FILE);
1929
+ const store = getArtifactStore();
1930
+ const planRow = { id: plan.id, title: plan.title, file: plan.file, status: "Todo" };
1931
+ const snapshot = {
1932
+ schema_version: 1,
1933
+ id: workflowId,
1934
+ type: "plan",
1935
+ status: "running",
1936
+ started_at: startedAt,
1937
+ updated_at: startedAt.slice(0, 10),
1938
+ plans: [planRow],
1939
+ delivery_kind: deliveryKind
1940
+ };
1941
+ if (options.project !== undefined)
1942
+ snapshot.project = options.project;
1943
+ if (options.completionPolicy !== undefined)
1944
+ snapshot.completion_policy = options.completionPolicy;
1945
+ if (options.branchSource !== undefined || options.branchTarget !== undefined) {
1946
+ snapshot.branch = {
1947
+ ...options.branchSource !== undefined ? { source: options.branchSource } : {},
1948
+ ...options.branchTarget !== undefined ? { target: options.branchTarget } : {}
1949
+ };
1950
+ }
1951
+ if (options.coordinator !== undefined) {
1952
+ snapshot.coordination = {
1953
+ coordinator: { session_id: options.coordinator.session_id, session_file: options.coordinator.session_file, bound_at: startedAt }
1954
+ };
1955
+ }
1956
+ const entry = {
1957
+ id: workflowId,
1958
+ type: "plan",
1959
+ started_at: snapshot.started_at,
1960
+ dir: `workflows/${workflowId}`
1961
+ };
1962
+ const entryGate = validateWorkflowEntry(entry);
1963
+ if (!entryGate.ok) {
1964
+ throw new Error(`refusing to register invalid workflow entry: ${entryGate.violations.map((v) => v.message).join("; ")}`);
1965
+ }
1966
+ return withStatusWriteLock(statusPath, async () => {
1967
+ if (existsSync4(snapshotPath)) {
1968
+ const rootDoc = readJson(statusPath);
1969
+ const workflows = Array.isArray(rootDoc.workflows) ? rootDoc.workflows : [];
1970
+ if (workflows.some((candidate) => isPlainObject2(candidate) && candidate.id === workflowId)) {
1971
+ throw new Error(`refusing to register workflow ${JSON.stringify(workflowId)}: it is already registered ` + `(snapshot at ${snapshotPath}, root entry in ${statusPath}) — registration is create-only; ` + `remove that workflow before registering again`);
1972
+ }
1973
+ const existing = readWorkflowSnapshot(workflowDir);
1974
+ if (planWorkflowRegistrationIdentity(existing.snapshot) !== planWorkflowRegistrationIdentity(snapshot)) {
1975
+ throw new Error(`refusing to register workflow ${JSON.stringify(workflowId)}: snapshot ${snapshotPath} already exists ` + `with a different registration identity — remove that workflow or register under a different id`);
1976
+ }
1977
+ const recoveryEntry = {
1978
+ id: workflowId,
1979
+ type: "plan",
1980
+ started_at: existing.snapshot.started_at,
1981
+ dir: `workflows/${workflowId}`
1982
+ };
1983
+ const recoveryGate = validateWorkflowEntry(recoveryEntry);
1984
+ if (!recoveryGate.ok) {
1985
+ throw new Error(`refusing to register invalid workflow entry: ${recoveryGate.violations.map((v) => v.message).join("; ")}`);
1986
+ }
1987
+ await registerWorkflowEntryLocked(statusPath, recoveryEntry);
1988
+ return { workflowId, snapshotPath, recovered: true };
1989
+ }
1990
+ let createdVersion;
1991
+ try {
1992
+ await writeWorkflowSnapshot(snapshot, workflowDir, { createOnly: true });
1993
+ createdVersion = readArtifactBytes(snapshotPath)?.version;
1994
+ await registerWorkflowEntryLocked(statusPath, entry);
1995
+ } catch (error) {
1996
+ if (createdVersion !== undefined) {
1997
+ await withStatusWriteLock(snapshotPath, async () => {
1998
+ const current = readArtifactBytes(snapshotPath);
1999
+ if (current === undefined || current.version !== createdVersion)
2000
+ return;
2001
+ const remove = store.delete?.bind(store);
2002
+ if (remove !== undefined) {
2003
+ await withProtectedWrite(snapshotPath, "delete", () => remove({ kind: "snapshot", key: workflowId }));
2004
+ }
2005
+ });
2006
+ }
2007
+ try {
2008
+ if (readdirSync2(workflowDir).length === 0) {
2009
+ rmdirSync2(workflowDir);
2010
+ }
2011
+ } catch {}
2012
+ throw error;
2013
+ }
2014
+ return { workflowId, snapshotPath, recovered: false };
2015
+ });
2016
+ }
1605
2017
 
1606
2018
  // src/status.ts
1607
2019
  var DATE_RE = /^\d{4}-\d{2}-\d{2}$/;
@@ -1773,7 +2185,7 @@ function validateStatusV2(docOrPath, opts = {}) {
1773
2185
  if (typeof docOrPath === "string") {
1774
2186
  try {
1775
2187
  doc = readJson(docOrPath);
1776
- harnessDir = dirname5(resolve6(docOrPath));
2188
+ harnessDir = dirname5(resolve7(docOrPath));
1777
2189
  } catch (error) {
1778
2190
  return {
1779
2191
  ok: false,
@@ -1935,14 +2347,14 @@ async function registerWorkflow(root, entry) {
1935
2347
  if (!entryGate.ok) {
1936
2348
  throw new Error(`refusing to register invalid workflow entry: ${entryGate.violations.map((v) => v.message).join("; ")}`);
1937
2349
  }
1938
- const statusPath = resolve6(root);
2350
+ const statusPath = resolve7(root);
1939
2351
  return withStatusWriteLock(statusPath, () => registerWorkflowEntryLocked(statusPath, entry));
1940
2352
  }
1941
2353
  async function unregisterWorkflow(root, id) {
1942
2354
  if (typeof id !== "string" || id.trim() === "") {
1943
2355
  throw new Error("refusing to unregister workflow: id must be a non-empty string");
1944
2356
  }
1945
- const statusPath = resolve6(root);
2357
+ const statusPath = resolve7(root);
1946
2358
  const store = getArtifactStore();
1947
2359
  assertFsStorePath(store, { kind: "status", key: "root" }, statusPath);
1948
2360
  const harnessDir = dirname5(statusPath);
@@ -1980,7 +2392,7 @@ function resolveCompassEnforcement(harnessDir) {
1980
2392
  return { hard: false, source: "none" };
1981
2393
  let entries;
1982
2394
  try {
1983
- entries = readdirSync2(iterationsDir, { withFileTypes: true });
2395
+ entries = readdirSync3(iterationsDir, { withFileTypes: true });
1984
2396
  } catch {
1985
2397
  return { hard: false, source: "none" };
1986
2398
  }
@@ -2007,7 +2419,7 @@ function resolveCompassEnforcement(harnessDir) {
2007
2419
  return { hard: false, source: "none" };
2008
2420
  }
2009
2421
  function resolveMstarcEnforcement(harnessDir) {
2010
- const dir = resolve6(harnessDir);
2422
+ const dir = resolve7(harnessDir);
2011
2423
  const rc = loadMstarc(dir, dirname5(dir));
2012
2424
  const value = rc?.config.enforcement;
2013
2425
  if (value === "hard")
@@ -2264,6 +2676,16 @@ function evaluatePostMergeClose(snapshotDoc, rootDoc) {
2264
2676
  violations.push(violation5("high", "PHASE6_DANGLING_LEASE", "Terminal snapshot still carries a lease (plans[].execution_lease or integration_merge_lease) — close never releases leases", "Release the lease(s) with the owner action, then re-run 'mstar iteration gate --phase 6 --workflow <id>'"));
2265
2677
  }
2266
2678
  const workflowId = isPlainObject3(snapshotDoc) && typeof snapshotDoc.id === "string" ? snapshotDoc.id : null;
2679
+ if (shapeOk && terminal && isPlainObject3(snapshotDoc) && snapshotDoc.type === "plan" && snapshotDoc.status === "completed") {
2680
+ violations.push(...consultDeliveryEvidence(snapshotDoc));
2681
+ if (Array.isArray(snapshotDoc.plans)) {
2682
+ for (const row of snapshotDoc.plans) {
2683
+ if (isPlainObject3(row) && row.status !== PLAN_STATUS_DONE) {
2684
+ violations.push(violation5("high", "PHASE6_PLAN_ROW_NOT_DONE", `Workflow '${String(workflowId)}' is completed but owned plan row '${String(row.id)}' is ${JSON.stringify(row.status)} — a completed close requires every plan row Done (mstar-artifacts/references/plan-workflow-lifecycle-contract.md §3 terminal stage)`, "Bring the owned plan row to Done (or close the lifecycle as failed/stopped with a recorded reason), then re-run 'mstar iteration gate --phase 6 --workflow <id>'"));
2685
+ }
2686
+ }
2687
+ }
2688
+ }
2267
2689
  const rootGate = validateStatusV2(rootDoc);
2268
2690
  if (!rootGate.ok) {
2269
2691
  violations.push(violation5("high", "PHASE6_INVALID_ROOT", "Root status.json is not a readable v2 workflow registry (workflows[] missing or not an array) — an invalid/unreadable root is not proof that the entry is gone", "Repair or migrate {HARNESS_DIR}/status.json to the v2 shape, then re-run the gate"));
@@ -2291,7 +2713,7 @@ function assertIndexRowObligations(iterationsDir) {
2291
2713
  ]
2292
2714
  };
2293
2715
  }
2294
- const iterationIds = readdirSync3(iterationsDir, { withFileTypes: true }).filter((entry) => entry.isDirectory()).filter((entry) => existsSync6(join8(iterationsDir, entry.name, COMPASS_FILE))).map((entry) => entry.name).sort();
2716
+ const iterationIds = readdirSync4(iterationsDir, { withFileTypes: true }).filter((entry) => entry.isDirectory()).filter((entry) => existsSync6(join8(iterationsDir, entry.name, COMPASS_FILE))).map((entry) => entry.name).sort();
2295
2717
  const readmePath = join8(iterationsDir, INDEX_README);
2296
2718
  if (!existsSync6(readmePath)) {
2297
2719
  return {
@@ -2552,8 +2974,8 @@ async function appendProjectRegisterEntries(opts) {
2552
2974
  if (opts.entries.length === 0) {
2553
2975
  throw new Error("refusing to append residual entries: entries must not be empty");
2554
2976
  }
2555
- const registerPath = resolve7(join9(opts.projectDir, PROJECT_REGISTER_FILE));
2556
- const projectKey = basename4(resolve7(opts.projectDir));
2977
+ const registerPath = resolve8(join9(opts.projectDir, PROJECT_REGISTER_FILE));
2978
+ const projectKey = basename4(resolve8(opts.projectDir));
2557
2979
  const store = getArtifactStore();
2558
2980
  assertFsStorePath(store, { kind: "residuals", key: projectKey }, registerPath);
2559
2981
  mkdirSync4(opts.projectDir, { recursive: true });
@@ -2597,8 +3019,8 @@ async function appendProjectRegisterEntries(opts) {
2597
3019
  });
2598
3020
  }
2599
3021
  async function closeProjectRegisterEntry(opts) {
2600
- const registerPath = resolve7(join9(opts.projectDir, PROJECT_REGISTER_FILE));
2601
- const projectKey = basename4(resolve7(opts.projectDir));
3022
+ const registerPath = resolve8(join9(opts.projectDir, PROJECT_REGISTER_FILE));
3023
+ const projectKey = basename4(resolve8(opts.projectDir));
2602
3024
  const store = getArtifactStore();
2603
3025
  assertFsStorePath(store, { kind: "residuals", key: projectKey }, registerPath);
2604
3026
  mkdirSync4(opts.projectDir, { recursive: true });
@@ -2677,7 +3099,7 @@ function techDebtRollup(projectDir) {
2677
3099
  const items = [];
2678
3100
  let entries;
2679
3101
  try {
2680
- entries = readdirSync4(projectDir, { withFileTypes: true });
3102
+ entries = readdirSync5(projectDir, { withFileTypes: true });
2681
3103
  } catch {
2682
3104
  entries = [];
2683
3105
  }
@@ -2724,7 +3146,7 @@ function listProjectReferenceFiles(projectDir) {
2724
3146
  const root = join9(projectDir, PROJECT_REFERENCES_DIR);
2725
3147
  let entries;
2726
3148
  try {
2727
- entries = readdirSync4(root, { withFileTypes: true });
3149
+ entries = readdirSync5(root, { withFileTypes: true });
2728
3150
  } catch {
2729
3151
  return [];
2730
3152
  }
@@ -2737,7 +3159,7 @@ function listProjectReferenceFiles(projectDir) {
2737
3159
  } else if (entry.isDirectory()) {
2738
3160
  let nested;
2739
3161
  try {
2740
- nested = readdirSync4(join9(root, entry.name), { withFileTypes: true });
3162
+ nested = readdirSync5(join9(root, entry.name), { withFileTypes: true });
2741
3163
  } catch {
2742
3164
  continue;
2743
3165
  }
@@ -2752,14 +3174,14 @@ function listProjectReferenceFiles(projectDir) {
2752
3174
 
2753
3175
  // src/path.ts
2754
3176
  function resolveHarnessDir(startDir = process.cwd(), opts = {}) {
2755
- const start = resolve8(startDir);
3177
+ const start = resolve9(startDir);
2756
3178
  const explicit = opts.harnessDir ?? process.env.MSTAR_HARNESS_DIR;
2757
3179
  if (explicit)
2758
- return resolve8(start, explicit);
2759
- const boundary = resolve8(start, opts.workspaceRoot ?? defaultWorkspaceRoot(start));
3180
+ return resolve9(start, explicit);
3181
+ const boundary = resolve9(start, opts.workspaceRoot ?? defaultWorkspaceRoot(start));
2760
3182
  const rc = loadMstarc(start, boundary);
2761
3183
  if (rc !== null && rc.config.harnessDir)
2762
- return resolve8(rc.dir, rc.config.harnessDir);
3184
+ return resolve9(rc.dir, rc.config.harnessDir);
2763
3185
  let dir = start;
2764
3186
  for (;; ) {
2765
3187
  if (!isAtOrBelow2(dir, boundary))
@@ -2790,7 +3212,7 @@ function defaultWorkspaceRoot(startDir) {
2790
3212
  if (segment && segment !== ".")
2791
3213
  boundary = dirname6(boundary);
2792
3214
  }
2793
- return resolve8(boundary);
3215
+ return resolve9(boundary);
2794
3216
  } catch {}
2795
3217
  return startDir;
2796
3218
  }
@@ -2799,10 +3221,10 @@ function isAtOrBelow2(dir, root) {
2799
3221
  return rel === "" || !rel.startsWith("..") && !isAbsolute6(rel);
2800
3222
  }
2801
3223
  function mstarcDirOverride(harnessDir, key) {
2802
- const dir = resolve8(harnessDir);
3224
+ const dir = resolve9(harnessDir);
2803
3225
  const rc = loadMstarc(dir, dirname6(dir));
2804
3226
  const declared = rc?.config[key];
2805
- return declared ? resolve8(rc.dir, declared) : null;
3227
+ return declared ? resolve9(rc.dir, declared) : null;
2806
3228
  }
2807
3229
  function resolveSpecsDir(harnessDir, opts = {}) {
2808
3230
  const declared = mstarcDirOverride(harnessDir, "specsDir");
@@ -2811,7 +3233,7 @@ function resolveSpecsDir(harnessDir, opts = {}) {
2811
3233
  mkdirSync5(declared, { recursive: true });
2812
3234
  return declared;
2813
3235
  }
2814
- const harness = resolve8(harnessDir);
3236
+ const harness = resolve9(harnessDir);
2815
3237
  const repoRoot = dirname6(harness);
2816
3238
  const candidates = [
2817
3239
  join10(harness, "specs"),
@@ -2833,7 +3255,7 @@ function resolvePlanDir(harnessDir) {
2833
3255
  const declared = mstarcDirOverride(harnessDir, "planDir");
2834
3256
  if (declared !== null)
2835
3257
  return declared;
2836
- const dir = resolve8(harnessDir);
3258
+ const dir = resolve9(harnessDir);
2837
3259
  const name = basename5(dir);
2838
3260
  if (name === ".plans" || name === "plans")
2839
3261
  return dir;
@@ -2845,7 +3267,7 @@ function assertSafePathComponent(value, what) {
2845
3267
  }
2846
3268
  }
2847
3269
  function canonicalizeNearestExisting(path) {
2848
- const abs = resolve8(path);
3270
+ const abs = resolve9(path);
2849
3271
  let dir = abs;
2850
3272
  const tail = [];
2851
3273
  for (;; ) {
@@ -2865,7 +3287,7 @@ function canonicalizeNearestExisting(path) {
2865
3287
  }
2866
3288
  function resolveSddDir(harnessDir, planId) {
2867
3289
  assertSafePathComponent(planId, "planId");
2868
- const base = resolve8(harnessDir);
3290
+ const base = resolve9(harnessDir);
2869
3291
  const declared = mstarcDirOverride(base, "sddDir");
2870
3292
  const sddBase = declared !== null ? declared : join10(base, "sdd");
2871
3293
  return join10(sddBase, planId);
@@ -2874,21 +3296,21 @@ function resolveIterationDir(harnessDir) {
2874
3296
  const declared = mstarcDirOverride(harnessDir, "iterationDir");
2875
3297
  if (declared !== null)
2876
3298
  return declared;
2877
- return join10(resolve8(harnessDir), "iterations");
3299
+ return join10(resolve9(harnessDir), "iterations");
2878
3300
  }
2879
3301
  function resolveKnowledgeDir(harnessDir) {
2880
3302
  const declared = mstarcDirOverride(harnessDir, "knowledgeDir");
2881
3303
  if (declared !== null)
2882
3304
  return declared;
2883
- return join10(resolve8(harnessDir), "knowledge");
3305
+ return join10(resolve9(harnessDir), "knowledge");
2884
3306
  }
2885
3307
  function resolveHarnessSubdir(startDir, opts, key, fallback) {
2886
3308
  const harness = resolveHarnessDir(startDir, opts);
2887
3309
  if (harness === null) {
2888
- 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)`);
3310
+ throw new Error(`harness dir not found from ${resolve9(startDir)} — cannot resolve the ${fallback} dir (run \`mstar harness scaffold\`, pass opts.harnessDir, or set MSTAR_HARNESS_DIR)`);
2889
3311
  }
2890
3312
  const declared = mstarcDirOverride(harness, key);
2891
- return declared !== null ? declared : join10(resolve8(harness), fallback);
3313
+ return declared !== null ? declared : join10(resolve9(harness), fallback);
2892
3314
  }
2893
3315
  function resolveWorkflowDir(startDir = process.cwd(), opts = {}) {
2894
3316
  return resolveHarnessSubdir(startDir, opts, "workflowDir", "workflows");
@@ -2903,11 +3325,11 @@ var EMPTY_STATUS_TEMPLATE = {
2903
3325
  };
2904
3326
  var SCAFFOLD_DIRS = ["plans", "iterations", "knowledge", "specs", "sdd"];
2905
3327
  function resolveScaffoldDirs(root) {
2906
- const start = resolve8(root);
2907
- const boundary = resolve8(start, defaultWorkspaceRoot(start));
3328
+ const start = resolve9(root);
3329
+ const boundary = resolve9(start, defaultWorkspaceRoot(start));
2908
3330
  const rc = loadMstarc(start, boundary);
2909
3331
  const explicit = process.env.MSTAR_HARNESS_DIR;
2910
- const harnessDir = explicit ? resolve8(start, explicit) : rc !== null && rc.config.harnessDir ? resolve8(rc.dir, rc.config.harnessDir) : join10(start, ".mstar");
3332
+ const harnessDir = explicit ? resolve9(start, explicit) : rc !== null && rc.config.harnessDir ? resolve9(rc.dir, rc.config.harnessDir) : join10(start, ".mstar");
2911
3333
  const declaredProjectDir = mstarcDirOverride(harnessDir, "projectDir");
2912
3334
  const projectDir = declaredProjectDir !== null ? declaredProjectDir : join10(harnessDir, "projects");
2913
3335
  return { harnessDir, projectDir };
@@ -2997,7 +3419,7 @@ function emitGitignoreSnippet(kind) {
2997
3419
  return `${GITIGNORE_SNIPPET}${GITIGNORE_SNIPPET_AGENTS}`;
2998
3420
  }
2999
3421
  function validateGitignore(root) {
3000
- const gitignorePath = join10(resolve8(root), ".gitignore");
3422
+ const gitignorePath = join10(resolve9(root), ".gitignore");
3001
3423
  const kind = detectHarnessKind(resolveHarnessDir(root));
3002
3424
  let content;
3003
3425
  try {
@@ -3045,7 +3467,7 @@ function validateGitignore(root) {
3045
3467
  function detectHarnessKind(harnessDir) {
3046
3468
  if (!harnessDir)
3047
3469
  return null;
3048
- const name = basename5(resolve8(harnessDir));
3470
+ const name = basename5(resolve9(harnessDir));
3049
3471
  if (name === ".mstar")
3050
3472
  return "mstar";
3051
3473
  if (name === ".agents")
@@ -3053,7 +3475,7 @@ function detectHarnessKind(harnessDir) {
3053
3475
  return null;
3054
3476
  }
3055
3477
  function assertPlanWritingPath(planPath, harnessDir) {
3056
- const planAbs = resolve8(planPath);
3478
+ const planAbs = resolve9(planPath);
3057
3479
  if (!harnessDir) {
3058
3480
  return {
3059
3481
  ok: false,
@@ -3078,7 +3500,7 @@ function assertPlanWritingPath(planPath, harnessDir) {
3078
3500
  if (existsSync8(planAbs)) {
3079
3501
  try {
3080
3502
  const canonicalPlan = realpathSync3(planAbs);
3081
- const canonicalPlanDir = existsSync8(planDir) ? realpathSync3(planDir) : resolve8(planDir);
3503
+ const canonicalPlanDir = existsSync8(planDir) ? realpathSync3(planDir) : resolve9(planDir);
3082
3504
  const canonicalRel = relative2(canonicalPlanDir, canonicalPlan);
3083
3505
  const canonicalInside = canonicalRel === "" || !canonicalRel.startsWith("..") && !isAbsolute6(canonicalRel);
3084
3506
  if (!canonicalInside) {
@@ -3108,7 +3530,7 @@ function isDirectory(dir) {
3108
3530
  }
3109
3531
  function hasFiles(dir) {
3110
3532
  try {
3111
- for (const entry of readdirSync5(dir, { withFileTypes: true })) {
3533
+ for (const entry of readdirSync6(dir, { withFileTypes: true })) {
3112
3534
  if (entry.isDirectory()) {
3113
3535
  if (hasFiles(join10(dir, entry.name)))
3114
3536
  return true;
@@ -4284,7 +4706,7 @@ function assessSddEvidenceReuse(record, artifacts, expected, target) {
4284
4706
  // src/worktree.ts
4285
4707
  import { execFileSync as execFileSync2 } from "node:child_process";
4286
4708
  import { existsSync as existsSync9, realpathSync as realpathSync4 } from "node:fs";
4287
- import { isAbsolute as isAbsolute7, join as join11, resolve as resolve9 } from "node:path";
4709
+ import { isAbsolute as isAbsolute7, join as join11, resolve as resolve10 } from "node:path";
4288
4710
  var DEFAULT_PROBE_TIMEOUT_MS = 1e4;
4289
4711
  function gitProbeTimeoutMs() {
4290
4712
  const raw = process.env.MSTAR_GIT_PROBE_TIMEOUT_MS;
@@ -4429,7 +4851,7 @@ function l1PreDispatchCheck(input, opts = {}) {
4429
4851
  const violations = [];
4430
4852
  const checkoutCache = new Map;
4431
4853
  const checkout = (path) => {
4432
- const key = resolve9(path);
4854
+ const key = resolve10(path);
4433
4855
  if (!checkoutCache.has(key))
4434
4856
  checkoutCache.set(key, probeCheckout(path, opts));
4435
4857
  return checkoutCache.get(key);
@@ -4480,7 +4902,7 @@ function l1PreDispatchCheck(input, opts = {}) {
4480
4902
  }
4481
4903
  }
4482
4904
  const identityViolation = (aPath, bPath, code, describe, fix) => {
4483
- if (resolve9(aPath) === resolve9(bPath)) {
4905
+ if (resolve10(aPath) === resolve10(bPath)) {
4484
4906
  return violation8("critical", code, `${describe} — the same checkout (normalized path equality) is not isolation`, fix);
4485
4907
  }
4486
4908
  if (!existsSync9(aPath) || !existsSync9(bPath))
@@ -4541,7 +4963,7 @@ function l2PreDispatchCheck(input, opts = {}) {
4541
4963
  violations.push(violation8("high", "worktree.l2.track-path-relative", `track ${index + 1} worktreePath "${track.worktreePath}" is not an absolute path — L2 tracks MUST use absolute worktree checkout paths (consistent with the lease validator's absolute worktree_path enforcement)`, `use an absolute path for track ${index + 1} (e.g. /Users/<you>/worktrees/<branch>)`));
4542
4964
  return;
4543
4965
  }
4544
- const normalized = resolve9(track.worktreePath);
4966
+ const normalized = resolve10(track.worktreePath);
4545
4967
  if (seenPaths.has(normalized)) {
4546
4968
  violations.push(violation8("high", "worktree.l2.track-path-collision", `duplicate worktreePath "${track.worktreePath}" across parallel tracks — L2 parallel-writable isolation requires a distinct absolute Worktree path per track (N parallel invokes ≠ isolation)`, "give every parallel track its own git worktree checkout"));
4547
4969
  return;
@@ -4715,9 +5137,9 @@ function scanActiveLifecycleBranches(harnessDir, governingWorkflowId) {
4715
5137
 
4716
5138
  // src/sdd.ts
4717
5139
  import { execFileSync as execFileSync3, spawn } from "node:child_process";
4718
- import { mkdirSync as mkdirSync6, readdirSync as readdirSync6, readFileSync as readFileSync9, realpathSync as realpathSync5, statSync as statSync4, writeFileSync as writeFileSync4 } from "node:fs";
5140
+ import { mkdirSync as mkdirSync6, readdirSync as readdirSync7, readFileSync as readFileSync9, realpathSync as realpathSync5, statSync as statSync4, writeFileSync as writeFileSync4 } from "node:fs";
4719
5141
  import { constants as osConstants } from "node:os";
4720
- import { basename as basename7, dirname as dirname9, isAbsolute as isAbsolute8, join as join13, relative as relative3, resolve as resolve10 } from "node:path";
5142
+ import { basename as basename7, dirname as dirname9, isAbsolute as isAbsolute8, join as join13, relative as relative3, resolve as resolve11 } from "node:path";
4721
5143
  class SddScriptError extends Error {
4722
5144
  exitCode;
4723
5145
  constructor(message, exitCode) {
@@ -4775,7 +5197,7 @@ function hasWorkflowSnapshot(harnessDir) {
4775
5197
  if (!isDirectory2(workflowsDir))
4776
5198
  return false;
4777
5199
  try {
4778
- for (const entry of readdirSync6(workflowsDir, { withFileTypes: true })) {
5200
+ for (const entry of readdirSync7(workflowsDir, { withFileTypes: true })) {
4779
5201
  if (entry.isDirectory() && isFile2(join13(workflowsDir, entry.name, "snapshot.json")))
4780
5202
  return true;
4781
5203
  }
@@ -4785,9 +5207,9 @@ function hasWorkflowSnapshot(harnessDir) {
4785
5207
  return false;
4786
5208
  }
4787
5209
  function checkoutRootNearestExisting(dir, boundary) {
4788
- const stop = resolve10(boundary);
4789
- const startInside = isInside(resolve10(dir), stop);
4790
- let current = resolve10(dir);
5210
+ const stop = resolve11(boundary);
5211
+ const startInside = isInside(resolve11(dir), stop);
5212
+ let current = resolve11(dir);
4791
5213
  for (;; ) {
4792
5214
  const probed = probeCheckoutRoot(current);
4793
5215
  if (probed !== null)
@@ -4841,12 +5263,12 @@ function sddWorkspace(planId, opts = {}) {
4841
5263
  const harnessOverride = opts.harnessDir ?? (process.env.MSTAR_HARNESS_DIR || undefined);
4842
5264
  let harnessDir;
4843
5265
  if (harnessOverride) {
4844
- harnessDir = resolve10(root, harnessOverride);
5266
+ harnessDir = resolve11(root, harnessOverride);
4845
5267
  } else {
4846
5268
  const rc = findMstarc(root, root);
4847
5269
  const rcHarnessDir = rc !== null ? parseMstarc(readFileSync9(rc, "utf8")).harnessDir : undefined;
4848
5270
  if (rcHarnessDir) {
4849
- harnessDir = resolve10(rc !== null ? dirname9(rc) : root, rcHarnessDir);
5271
+ harnessDir = resolve11(rc !== null ? dirname9(rc) : root, rcHarnessDir);
4850
5272
  } else {
4851
5273
  const probed = probeHarnessWithStatus(root);
4852
5274
  if (probed) {
@@ -4878,7 +5300,7 @@ function taskBrief(planFile, taskN, outFile, opts = {}) {
4878
5300
  const bound = opts.context;
4879
5301
  const observedCwd = opts.cwd ?? process.cwd();
4880
5302
  if (bound) {
4881
- const inputPlan = canonicalizeNearestExisting(resolve10(observedCwd, planFile));
5303
+ const inputPlan = canonicalizeNearestExisting(resolve11(observedCwd, planFile));
4882
5304
  if (inputPlan !== canonicalizeNearestExisting(bound.planFile)) {
4883
5305
  throwGateFail([
4884
5306
  contextViolation("high", "sdd.context.plan-file-mismatch", `plan file "${planFile}" does not match the bound context plan file "${bound.planFile}" — ` + "bound mode extracts only the resolved context's plan; refused before any read or write")
@@ -4894,7 +5316,7 @@ function taskBrief(planFile, taskN, outFile, opts = {}) {
4894
5316
  let out;
4895
5317
  let mkdirAfterGate = null;
4896
5318
  if (outFile) {
4897
- out = bound ? resolve10(observedCwd, outFile) : outFile;
5319
+ out = bound ? resolve11(observedCwd, outFile) : outFile;
4898
5320
  } else if (bound) {
4899
5321
  out = join13(bound.sddDir, `task-${taskN}-brief.md`);
4900
5322
  mkdirAfterGate = bound.sddDir;
@@ -4937,7 +5359,7 @@ function taskBrief(planFile, taskN, outFile, opts = {}) {
4937
5359
  if (printed.length === 0) {
4938
5360
  throw new SddScriptError(`task ${taskN} not found in ${planFile} (no heading matching Task ${taskN})`, 3);
4939
5361
  }
4940
- return bound ? resolve10(observedCwd, out) : out;
5362
+ return bound ? resolve11(observedCwd, out) : out;
4941
5363
  }
4942
5364
  function reviewPackage(base, head, outFile, opts = {}) {
4943
5365
  if (!base || !head) {
@@ -4958,7 +5380,7 @@ function reviewPackage(base, head, outFile, opts = {}) {
4958
5380
  let out;
4959
5381
  let mkdirAfterGate = null;
4960
5382
  if (outFile) {
4961
- out = bound ? resolve10(observedCwd, outFile) : outFile;
5383
+ out = bound ? resolve11(observedCwd, outFile) : outFile;
4962
5384
  } else if (bound) {
4963
5385
  const shortBase = gitOut(cwd, ["rev-parse", "--short", base]) ?? base;
4964
5386
  const shortHead = gitOut(cwd, ["rev-parse", "--short", head]) ?? head;
@@ -5002,7 +5424,7 @@ function reviewPackage(base, head, outFile, opts = {}) {
5002
5424
  run(["diff", "-U10", `${base}..${head}`])
5003
5425
  ];
5004
5426
  writeFileSync4(out, Buffer.concat(parts));
5005
- return bound ? resolve10(observedCwd, out) : out;
5427
+ return bound ? resolve11(observedCwd, out) : out;
5006
5428
  }
5007
5429
  function assertBaseSha(ref, opts = {}) {
5008
5430
  if (typeof ref !== "string" || !/^[0-9a-f]{4,40}$/i.test(ref)) {
@@ -5106,24 +5528,28 @@ function normalizeSnapshotWorktreePath(doc) {
5106
5528
  const { control_worktree_path: _dropped, ...rest } = doc;
5107
5529
  return { ...rest, integration_worktree_path: legacy };
5108
5530
  }
5109
- function readActiveWorkflowIds(controlHarnessRoot) {
5531
+ function classifyWorkflowRegisterState(controlHarnessRoot) {
5110
5532
  const statusPath = join13(controlHarnessRoot, "status.json");
5111
5533
  if (!isFile2(statusPath))
5112
- return null;
5534
+ return { state: "absent" };
5113
5535
  let doc;
5114
5536
  try {
5115
5537
  doc = readJson(statusPath);
5116
5538
  } catch {
5117
- return null;
5539
+ return { state: "unreadable" };
5118
5540
  }
5119
5541
  if (!isPlainObject7(doc) || doc.version !== 2 || !Array.isArray(doc.workflows))
5120
- return null;
5121
- const ids = new Set;
5542
+ return { state: "unreadable" };
5543
+ const activeIds = new Set;
5122
5544
  for (const entry of doc.workflows) {
5123
5545
  if (isPlainObject7(entry) && typeof entry.id === "string")
5124
- ids.add(entry.id);
5546
+ activeIds.add(entry.id);
5125
5547
  }
5126
- return ids;
5548
+ return { state: "readable", activeIds };
5549
+ }
5550
+ function readActiveWorkflowIds(controlHarnessRoot) {
5551
+ const register = classifyWorkflowRegisterState(controlHarnessRoot);
5552
+ return register.state === "readable" ? register.activeIds : null;
5127
5553
  }
5128
5554
  function findWorkflowPlanRow(controlHarnessRoot, planId) {
5129
5555
  let workflowsDir;
@@ -5136,7 +5562,7 @@ function findWorkflowPlanRow(controlHarnessRoot, planId) {
5136
5562
  return { kind: "none" };
5137
5563
  let workflowIds;
5138
5564
  try {
5139
- workflowIds = readdirSync6(workflowsDir, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => entry.name);
5565
+ workflowIds = readdirSync7(workflowsDir, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => entry.name);
5140
5566
  } catch {
5141
5567
  return { kind: "none" };
5142
5568
  }
@@ -5272,6 +5698,12 @@ function resolveSddExecutionContext(input) {
5272
5698
  contextViolation("critical", "sdd.context.feature-in-control", `featureCwd "${canonicalFeatureCwd}" is inside the control checkout "${controlCheckout}" and is not a distinct Git checkout — a plain subdirectory or symlink alias of the control checkout is not isolation; product edits never land in the control checkout (execution_lease.worktree_path MUST be a distinct checkout)`, "use a distinct feature worktree for the plan (git worktree add <path> <branch>)")
5273
5699
  ]);
5274
5700
  }
5701
+ const registerState = classifyWorkflowRegisterState(canonicalControlHarnessRoot);
5702
+ if (registerState.state === "unreadable") {
5703
+ throwGateFail([
5704
+ contextViolation("critical", "sdd.context.register-unreadable", `control harness root "${canonicalControlHarnessRoot}" has a status.json that is present but not a readable v2 workflow register (malformed JSON, non-v2 version, or missing workflows[]) — ` + 'a damaged register is never treated as "no register": registered active lifecycles cannot be resolved over it, so execution is refused (plan-workflow-lifecycle contract §6 S2) instead of silently continuing on branch alignment alone', "repair or migrate the control root's status.json to the v2 shape ('mstar migrate' / register repair), then retry the execution")
5705
+ ]);
5706
+ }
5275
5707
  const match = findWorkflowPlanRow(canonicalControlHarnessRoot, planId);
5276
5708
  if (match.kind === "ambiguous") {
5277
5709
  throwGateFail([
@@ -5314,6 +5746,11 @@ function resolveSddExecutionContext(input) {
5314
5746
  } else if (match.kind === "row" && match.row.status === "InProgress") {
5315
5747
  throwGateFail(verifyPlanExecutionLease(match.row, planId).violations);
5316
5748
  } else {
5749
+ if (match.kind === "none" && registerState.state === "readable") {
5750
+ throwGateFail([
5751
+ contextViolation("high", "sdd.context.plan-not-registered", `plan "${planId}" has no active registered workflow row in the control harness's workflow register — ` + "on the normal plan route, SDD execution requires a registered running workflow row (plan-workflow-lifecycle contract §6 S2; a retained terminal snapshot's row is history, never registration evidence): " + "register it with `mstar workflow register --workflow <id> --plan-id <id> --plan-title <title> --plan-file <path> " + `--delivery-kind <${WORKFLOW_DELIVERY_KINDS.join("|")}> ...` + "` (`registerPlanWorkflow`), then retry — registration is create-only and preserves prior state; " + "re-running the same register command completes an interrupted registration without duplicating identity", "register the plan workflow, then retry the execution")
5752
+ ]);
5753
+ }
5317
5754
  const branchGate = assertBranchAlignment(canonicalFeatureCwd, workingBranch);
5318
5755
  if (!branchGate.ok)
5319
5756
  throwGateFail(branchGate.violations);
@@ -5343,11 +5780,11 @@ function checkSddAction(context, action) {
5343
5780
  const featureReal = canonicalDir(context.featureCwd);
5344
5781
  const canonicalSddDir = canonicalizeNearestExisting(context.sddDir);
5345
5782
  const canonicalPlanFile = canonicalizeNearestExisting(context.planFile);
5346
- const cwdResolved = resolve10(action.cwd);
5783
+ const cwdResolved = resolve11(action.cwd);
5347
5784
  const checkTarget = (baseDir, target, kind) => {
5348
5785
  if (featureReal === null)
5349
5786
  return;
5350
- const targetAbs = resolve10(baseDir, target);
5787
+ const targetAbs = resolve11(baseDir, target);
5351
5788
  const canonical = canonicalizeNearestExisting(targetAbs);
5352
5789
  if (!isInside(canonical, featureReal)) {
5353
5790
  const declaredPrefix = targetAbs === featureReal || targetAbs.startsWith(`${featureReal}/`);
@@ -5375,10 +5812,10 @@ function checkSddAction(context, action) {
5375
5812
  add("sdd.context.target-missing", "artifact checks require the destination target");
5376
5813
  return { ok: false, violations };
5377
5814
  }
5378
- const targetAbs = resolve10(cwdResolved, action.target);
5815
+ const targetAbs = resolve11(cwdResolved, action.target);
5379
5816
  const canonical = canonicalizeNearestExisting(targetAbs);
5380
5817
  if (!isInside(canonical, canonicalSddDir) && canonical !== canonicalPlanFile) {
5381
- const rawSddDir = resolve10(context.sddDir);
5818
+ const rawSddDir = resolve11(context.sddDir);
5382
5819
  const declaredPrefix = targetAbs === rawSddDir || targetAbs.startsWith(`${rawSddDir}/`) || targetAbs === canonicalSddDir || targetAbs.startsWith(`${canonicalSddDir}/`) || targetAbs === canonicalPlanFile;
5383
5820
  if (declaredPrefix) {
5384
5821
  add("sdd.context.artifact-symlink-escape", `artifact target "${targetAbs}" canonicalizes to "${canonical}", outside the plan's control sddDir — symlink escape refused before write`);
@@ -5465,7 +5902,7 @@ async function runInSddContext(context, argv) {
5465
5902
  }
5466
5903
  // src/gates.ts
5467
5904
  import { existsSync as existsSync11, statSync as statSync5 } from "node:fs";
5468
- import { basename as basename8, dirname as dirname10, join as join14, relative as relative4, resolve as resolve11 } from "node:path";
5905
+ import { basename as basename8, dirname as dirname10, join as join14, relative as relative4, resolve as resolve12 } from "node:path";
5469
5906
  var STATUS_FILE = "status.json";
5470
5907
  var SNAPSHOT_FILE = "snapshot.json";
5471
5908
  var REGISTER_FILE = "residuals.json";
@@ -5505,7 +5942,7 @@ function hasHarnessRootMarkers(dir) {
5505
5942
  }
5506
5943
  }
5507
5944
  function resolveHarnessRootOf(target) {
5508
- let dir = resolve11(target);
5945
+ let dir = resolve12(target);
5509
5946
  for (;; ) {
5510
5947
  if (hasHarnessRootMarkers(dir))
5511
5948
  return dir;
@@ -5518,7 +5955,7 @@ function resolveHarnessRootOf(target) {
5518
5955
  function harnessDocKindOfTarget(targetPath) {
5519
5956
  if (typeof targetPath !== "string" || targetPath.trim() === "")
5520
5957
  return null;
5521
- const resolved = resolve11(targetPath);
5958
+ const resolved = resolve12(targetPath);
5522
5959
  const name = basename8(resolved);
5523
5960
  if (name !== STATUS_FILE && name !== SNAPSHOT_FILE && name !== REGISTER_FILE)
5524
5961
  return null;
@@ -5638,8 +6075,8 @@ function formatStatusWriteBlockReason(violations, skillPointer) {
5638
6075
  `);
5639
6076
  }
5640
6077
  // src/migrate.ts
5641
- import { copyFileSync, mkdirSync as mkdirSync7, readFileSync as readFileSync10, readdirSync as readdirSync7, writeFileSync as writeFileSync5 } from "node:fs";
5642
- import { basename as basename9, dirname as dirname11, isAbsolute as isAbsolute9, join as join15, relative as relative5, resolve as resolve12, sep as sep3 } from "node:path";
6078
+ import { mkdirSync as mkdirSync7, readFileSync as readFileSync10, readdirSync as readdirSync8, writeFileSync as writeFileSync5 } from "node:fs";
6079
+ import { basename as basename9, dirname as dirname11, isAbsolute as isAbsolute9, join as join15, relative as relative5, resolve as resolve13, sep as sep3 } from "node:path";
5643
6080
  var MIGRATE_STATUS_FILE = "status.json";
5644
6081
  var ARCHIVED_STATUS_V1_FILE = "archived/status.v1.json";
5645
6082
  var NOTES_LEDGER_FILE = "notes.jsonl";
@@ -5683,7 +6120,7 @@ function scanCompasses(harnessDir) {
5683
6120
  const out = [];
5684
6121
  let entries;
5685
6122
  try {
5686
- entries = readdirSync7(iterationsDir, { withFileTypes: true });
6123
+ entries = readdirSync8(iterationsDir, { withFileTypes: true });
5687
6124
  } catch {
5688
6125
  return out;
5689
6126
  }
@@ -5775,7 +6212,7 @@ function buildIterationSnapshot(compass, rowById, rootUpdatedAt) {
5775
6212
  data: snapshot
5776
6213
  };
5777
6214
  }
5778
- function buildStandaloneSnapshot(row, rootUpdatedAt, migrationNotes) {
6215
+ function buildStandaloneSnapshot(row, rootUpdatedAt, migrationNotes, declaration = {}) {
5779
6216
  const id = rowIdOf(row) ?? "<unnamed>";
5780
6217
  const statusValue = row.status;
5781
6218
  let status;
@@ -5802,6 +6239,17 @@ function buildStandaloneSnapshot(row, rootUpdatedAt, migrationNotes) {
5802
6239
  updated_at: status === "completed" ? endedAt : updatedAt,
5803
6240
  plans: [row]
5804
6241
  };
6242
+ if (status !== "completed" && declaration.deliveryKind !== undefined) {
6243
+ snapshot.delivery_kind = declaration.deliveryKind;
6244
+ if (declaration.branchSource !== undefined || declaration.branchTarget !== undefined) {
6245
+ snapshot.branch = {
6246
+ ...declaration.branchSource !== undefined ? { source: declaration.branchSource } : {},
6247
+ ...declaration.branchTarget !== undefined ? { target: declaration.branchTarget } : {}
6248
+ };
6249
+ }
6250
+ if (declaration.completionPolicy !== undefined)
6251
+ snapshot.completion_policy = declaration.completionPolicy;
6252
+ }
5805
6253
  return {
5806
6254
  id,
5807
6255
  type: "plan",
@@ -5953,10 +6401,22 @@ function collectNotesFiles(snapshots) {
5953
6401
  return out;
5954
6402
  }
5955
6403
  function migrateHarnessTree(root, opts = {}) {
5956
- const harnessDir = resolve12(root);
6404
+ const harnessDir = resolve13(root);
5957
6405
  const workflowDir = resolveWorkflowDir(harnessDir, { harnessDir });
5958
6406
  const projectDir = resolveProjectDir(harnessDir, { harnessDir });
5959
6407
  const projectId = opts.projectId ?? _DEFAULT_PROJECT;
6408
+ if (opts.deliveryKind !== undefined && !WORKFLOW_DELIVERY_KINDS.includes(opts.deliveryKind)) {
6409
+ throw new Error(`refusing to migrate: options.deliveryKind must be one of ${WORKFLOW_DELIVERY_KINDS.join(" | ")} — got ${JSON.stringify(opts.deliveryKind)}`);
6410
+ }
6411
+ if (opts.deliveryKind !== undefined) {
6412
+ assertDeliveryRegistrationCoherence(opts.deliveryKind, opts, "migrate");
6413
+ }
6414
+ const declaration = {
6415
+ deliveryKind: opts.deliveryKind,
6416
+ branchSource: opts.branchSource,
6417
+ branchTarget: opts.branchTarget,
6418
+ completionPolicy: opts.completionPolicy
6419
+ };
5960
6420
  const statusPath = join15(harnessDir, MIGRATE_STATUS_FILE);
5961
6421
  const source = readArtifactBytes(statusPath);
5962
6422
  const sourceVersion = source?.version ?? "absent";
@@ -5972,6 +6432,8 @@ function migrateHarnessTree(root, opts = {}) {
5972
6432
  alreadyMigrated: true,
5973
6433
  message: `no-op: ${statusPath} is already at schema version 2 (migrated) — nothing to do`,
5974
6434
  snapshots: [],
6435
+ deliveryKindRequired: [],
6436
+ deliveryKindAmbiguous: [],
5975
6437
  notesFiles: [],
5976
6438
  register: null,
5977
6439
  roadmap: null,
@@ -6022,8 +6484,11 @@ function migrateHarnessTree(root, opts = {}) {
6022
6484
  for (const row of rows) {
6023
6485
  const id = rowIdOf(row);
6024
6486
  if (id !== null && !byPlan.has(id))
6025
- snapshots.push(buildStandaloneSnapshot(row, rootUpdatedAt, migrationNotes));
6487
+ snapshots.push(buildStandaloneSnapshot(row, rootUpdatedAt, migrationNotes, declaration));
6026
6488
  }
6489
+ const deliveryKindRequired = snapshots.filter((snapshot) => snapshot.type === "plan" && snapshot.status !== "completed" && snapshot.data.delivery_kind === undefined).map((snapshot) => snapshot.id);
6490
+ const declaredKindTargets = declaration.deliveryKind === undefined ? [] : snapshots.filter((snapshot) => snapshot.type === "plan" && snapshot.status !== "completed" && snapshot.data.delivery_kind !== undefined).map((snapshot) => snapshot.id).sort(compareIds);
6491
+ const deliveryKindAmbiguous = declaredKindTargets.length > 1 ? declaredKindTargets : [];
6027
6492
  const lifecycleSources = new Map;
6028
6493
  for (const snapshot of snapshots) {
6029
6494
  const sources = lifecycleSources.get(snapshot.id) ?? [];
@@ -6093,6 +6558,8 @@ function migrateHarnessTree(root, opts = {}) {
6093
6558
  alreadyMigrated: false,
6094
6559
  message: `planned migration of ${snapshots.length} lifecycles (${steps.length} steps)`,
6095
6560
  snapshots,
6561
+ deliveryKindRequired,
6562
+ deliveryKindAmbiguous,
6096
6563
  notesFiles,
6097
6564
  register,
6098
6565
  roadmap,
@@ -6106,14 +6573,20 @@ async function applyMigratePlan(plan) {
6106
6573
  if (plan.dryRun) {
6107
6574
  return { applied: false, message: `dry-run: ${plan.steps.length} steps planned (source → destination), zero writes` };
6108
6575
  }
6576
+ if (plan.deliveryKindAmbiguous.length > 0) {
6577
+ throw new Error(`refusing to apply migration: one delivery declaration cannot describe ` + `${plan.deliveryKindAmbiguous.length} active standalone plan lifts (${plan.deliveryKindAmbiguous.join(", ")}) — ` + `migrate them in batches of one declared lifecycle (CLI: run migrate once per plan, each with its own --delivery-kind)`);
6578
+ }
6579
+ if (plan.deliveryKindRequired.length > 0) {
6580
+ throw new Error(`refusing to apply migration: ${plan.deliveryKindRequired.length} active standalone plan snapshot(s) ` + `(${plan.deliveryKindRequired.join(", ")}) would be lifted without a declared delivery kind — ` + `re-plan with an explicit deliveryKind (CLI: --delivery-kind <${WORKFLOW_DELIVERY_KINDS.join("|")}>)`);
6581
+ }
6109
6582
  const statusPath = join15(plan.root, MIGRATE_STATUS_FILE);
6110
6583
  const current = readJson(statusPath);
6111
6584
  if (current.version === 2) {
6112
6585
  return { applied: false, message: "no-op: status.json already at schema version 2 (migrated) — nothing to do" };
6113
6586
  }
6114
- const harnessRoot = resolve12(plan.root);
6115
- const workflowRoot = resolve12(plan.workflowDir);
6116
- const projectRoot = resolve12(plan.projectDir);
6587
+ const harnessRoot = resolve13(plan.root);
6588
+ const workflowRoot = resolve13(plan.workflowDir);
6589
+ const projectRoot = resolve13(plan.projectDir);
6117
6590
  if (!isAbsolute9(plan.workflowDir) || !isAbsolute9(plan.projectDir)) {
6118
6591
  throw new Error(`refusing to apply migration: plan workflowDir/projectDir must be absolute (got ${JSON.stringify(plan.workflowDir)} / ${JSON.stringify(plan.projectDir)})`);
6119
6592
  }
@@ -6125,7 +6598,7 @@ async function applyMigratePlan(plan) {
6125
6598
  ...plan.roadmap !== null ? [plan.roadmap.file] : []
6126
6599
  ];
6127
6600
  for (const destination of allDestinations) {
6128
- const resolvedDest = resolve12(join15(plan.root, destination));
6601
+ const resolvedDest = resolve13(join15(plan.root, destination));
6129
6602
  const inside = (dir) => resolvedDest === dir || resolvedDest.startsWith(`${dir}${sep3}`);
6130
6603
  if (!inside(harnessRoot) && !inside(workflowRoot) && !inside(projectRoot)) {
6131
6604
  throw new Error(`refusing to apply migration: destination escapes the harness dir (${JSON.stringify(destination)}) — every write must stay under ${JSON.stringify(plan.root)}, the workflow dir (${JSON.stringify(plan.workflowDir)}) or the project dir (${JSON.stringify(plan.projectDir)})`);
@@ -6134,6 +6607,32 @@ async function applyMigratePlan(plan) {
6134
6607
  const store = getArtifactStore();
6135
6608
  return withStatusWriteLock(statusPath, () => applyMigratePlanLocked(plan, statusPath, store, workflowRoot, projectRoot));
6136
6609
  }
6610
+ function rawTargetStoreError(filePath, error) {
6611
+ return new CoordinationError("coordination.store", `cannot write migration target ${filePath}: ${error instanceof Error ? error.message : String(error)}`, { path: filePath });
6612
+ }
6613
+ function writeRawMigrateTarget(filePath, expected) {
6614
+ try {
6615
+ mkdirSync7(dirname11(filePath), { recursive: true });
6616
+ } catch (error) {
6617
+ throw rawTargetStoreError(filePath, error);
6618
+ }
6619
+ try {
6620
+ writeFileSync5(filePath, expected, { flag: "wx" });
6621
+ return;
6622
+ } catch (error) {
6623
+ if (error.code !== "EEXIST")
6624
+ throw rawTargetStoreError(filePath, error);
6625
+ }
6626
+ let existing;
6627
+ try {
6628
+ existing = readFileSync10(filePath);
6629
+ } catch (error) {
6630
+ throw rawTargetStoreError(filePath, error);
6631
+ }
6632
+ if (Buffer.compare(existing, expected) !== 0) {
6633
+ throw new CoordinationError("coordination.version-conflict", `refusing to apply migration: ${filePath} already exists with different content — migration is additive-only`, { path: filePath });
6634
+ }
6635
+ }
6137
6636
  async function applyMigratePlanLocked(plan, statusPath, store, workflowRoot, projectRoot) {
6138
6637
  const workflowTargetOf = (canonicalFile) => join15(workflowRoot, relative5("workflows", canonicalFile));
6139
6638
  const projectTargetOf = (canonicalFile) => join15(projectRoot, relative5("projects", canonicalFile));
@@ -6146,8 +6645,7 @@ async function applyMigratePlanLocked(plan, statusPath, store, workflowRoot, pro
6146
6645
  if (sourceVersion !== plan.sourceVersion) {
6147
6646
  throw new CoordinationError("coordination.version-conflict", `refusing to apply migration: ${statusPath} changed since the plan was built (planned ${plan.sourceVersion}, found ${sourceVersion}) — re-run the plan against the current root`, { path: statusPath, expected: plan.sourceVersion, actual: sourceVersion });
6148
6647
  }
6149
- mkdirSync7(join15(plan.root, dirname11(plan.archive.file)), { recursive: true });
6150
- copyFileSync(statusPath, join15(plan.root, plan.archive.file));
6648
+ writeRawMigrateTarget(join15(plan.root, plan.archive.file), readFileSync10(statusPath));
6151
6649
  for (const snapshot of plan.snapshots) {
6152
6650
  const snapshotDir = dirname11(workflowTargetOf(snapshot.file));
6153
6651
  const snapshotPath = join15(snapshotDir, WORKFLOW_SNAPSHOT_FILE);
@@ -6161,12 +6659,10 @@ async function applyMigratePlanLocked(plan, statusPath, store, workflowRoot, pro
6161
6659
  }
6162
6660
  }
6163
6661
  for (const notes of plan.notesFiles) {
6164
- const filePath = workflowTargetOf(notes.file);
6165
- mkdirSync7(dirname11(filePath), { recursive: true });
6166
6662
  const content = notes.lines.length > 0 ? `${notes.lines.join(`
6167
6663
  `)}
6168
6664
  ` : "";
6169
- writeFileSync5(filePath, content, "utf8");
6665
+ writeRawMigrateTarget(workflowTargetOf(notes.file), Buffer.from(content, "utf8"));
6170
6666
  }
6171
6667
  if (plan.register !== null) {
6172
6668
  const registerData = plan.register.data;
@@ -6192,9 +6688,7 @@ async function applyMigratePlanLocked(plan, statusPath, store, workflowRoot, pro
6192
6688
  }
6193
6689
  }
6194
6690
  if (plan.roadmap !== null) {
6195
- const filePath = projectTargetOf(plan.roadmap.file);
6196
- mkdirSync7(dirname11(filePath), { recursive: true });
6197
- writeFileSync5(filePath, plan.roadmap.content, "utf8");
6691
+ writeRawMigrateTarget(projectTargetOf(plan.roadmap.file), Buffer.from(plan.roadmap.content, "utf8"));
6198
6692
  }
6199
6693
  const rootGate = validateStatusV2(plan.rootV2.data, { harnessDir: plan.root });
6200
6694
  if (!rootGate.ok) {
@@ -6609,8 +7103,8 @@ function completenessLevel(frontmatterText, checklist) {
6609
7103
  }
6610
7104
  // src/audit.ts
6611
7105
  import { execFileSync as execFileSync4 } from "node:child_process";
6612
- import { existsSync as existsSync12, mkdirSync as mkdirSync8, readdirSync as readdirSync8, readFileSync as readFileSync11, rmdirSync as rmdirSync2, writeFileSync as writeFileSync6 } from "node:fs";
6613
- import { basename as basename10, join as join16, resolve as resolve13, sep as sep4 } from "node:path";
7106
+ import { existsSync as existsSync12, mkdirSync as mkdirSync8, readdirSync as readdirSync9, readFileSync as readFileSync11, rmdirSync as rmdirSync3, writeFileSync as writeFileSync6 } from "node:fs";
7107
+ import { basename as basename10, join as join16, resolve as resolve14, sep as sep4 } from "node:path";
6614
7108
  function violation10(severity, code, message, fix) {
6615
7109
  return { ok: false, severity, code, message, fix };
6616
7110
  }
@@ -6963,7 +7457,7 @@ var LOCKFILE_NAMES = [
6963
7457
  function rootLockfiles(root) {
6964
7458
  let entries;
6965
7459
  try {
6966
- entries = readdirSync8(root, { withFileTypes: true });
7460
+ entries = readdirSync9(root, { withFileTypes: true });
6967
7461
  } catch {
6968
7462
  return [];
6969
7463
  }
@@ -6996,7 +7490,7 @@ function supplyChainChecks(repoRoot) {
6996
7490
  const workflowsDir = join16(repoRoot, ".github", "workflows");
6997
7491
  let wfEntries = [];
6998
7492
  try {
6999
- wfEntries = readdirSync8(workflowsDir, { withFileTypes: true });
7493
+ wfEntries = readdirSync9(workflowsDir, { withFileTypes: true });
7000
7494
  } catch {
7001
7495
  wfEntries = [];
7002
7496
  }
@@ -7162,7 +7656,7 @@ function scaffoldAuditPlan(outDir, findings, options = {}) {
7162
7656
  mkdirSync8(outDir, { recursive: true });
7163
7657
  const existingReadme = join16(outDir, "README.md");
7164
7658
  const carried = existsSync12(existingReadme) ? extractSecurityDispositionSections(readFileSync11(existingReadme, "utf8")) : { needsVerification: [], hardeningChecked: [] };
7165
- const existing = readdirSync8(outDir).filter((f) => /^\d{3}-.*\.md$/.test(f));
7659
+ const existing = readdirSync9(outDir).filter((f) => /^\d{3}-.*\.md$/.test(f));
7166
7660
  let next = existing.reduce((max, f) => Math.max(max, Number(f.slice(0, 3))), 0) + 1;
7167
7661
  const redactedFindings = findings.map(redactFinding);
7168
7662
  const written = [];
@@ -7227,7 +7721,7 @@ function scaffoldAuditPlan(outDir, findings, options = {}) {
7227
7721
  needsVerification: needsVerificationLines,
7228
7722
  hardeningChecked: hardeningCheckedLines
7229
7723
  }));
7230
- return { outDir: resolve13(outDir), date, files: written, nextNumber: next };
7724
+ return { outDir: resolve14(outDir), date, files: written, nextNumber: next };
7231
7725
  }
7232
7726
  async function promoteAuditPlans(outDir, selected, options) {
7233
7727
  if (selected.length === 0) {
@@ -7236,9 +7730,13 @@ async function promoteAuditPlans(outDir, selected, options) {
7236
7730
  if (typeof options.harnessDir !== "string" || options.harnessDir.trim() === "") {
7237
7731
  throw new Error("promoteAuditPlans: options.harnessDir is required (must contain status.json + workflows/)");
7238
7732
  }
7239
- const workflowId = options.workflowId ?? basename10(resolve13(outDir));
7733
+ if (typeof options.deliveryKind !== "string" || !WORKFLOW_DELIVERY_KINDS.includes(options.deliveryKind)) {
7734
+ 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)`);
7735
+ }
7736
+ assertDeliveryRegistrationCoherence(options.deliveryKind, options, "promoteAuditPlans");
7737
+ const workflowId = options.workflowId ?? basename10(resolve14(outDir));
7240
7738
  assertSafePathComponent(workflowId, "workflow id");
7241
- const harnessDir = resolve13(options.harnessDir);
7739
+ const harnessDir = resolve14(options.harnessDir);
7242
7740
  const statusPath = join16(harnessDir, "status.json");
7243
7741
  const workflowDir = join16(harnessDir, "workflows", workflowId);
7244
7742
  const snapshotPath = join16(workflowDir, WORKFLOW_SNAPSHOT_FILE);
@@ -7265,8 +7763,17 @@ async function promoteAuditPlans(outDir, selected, options) {
7265
7763
  status: "running",
7266
7764
  started_at: now.toISOString(),
7267
7765
  updated_at: now.toISOString().slice(0, 10),
7268
- plans
7766
+ plans,
7767
+ delivery_kind: options.deliveryKind
7269
7768
  };
7769
+ if (options.branchSource !== undefined || options.branchTarget !== undefined) {
7770
+ snapshot.branch = {
7771
+ ...options.branchSource !== undefined ? { source: options.branchSource } : {},
7772
+ ...options.branchTarget !== undefined ? { target: options.branchTarget } : {}
7773
+ };
7774
+ }
7775
+ if (options.completionPolicy !== undefined)
7776
+ snapshot.completion_policy = options.completionPolicy;
7270
7777
  const entry = {
7271
7778
  id: workflowId,
7272
7779
  type: "plan",
@@ -7299,8 +7806,8 @@ async function promoteAuditPlans(outDir, selected, options) {
7299
7806
  });
7300
7807
  }
7301
7808
  try {
7302
- if (readdirSync8(workflowDir).length === 0) {
7303
- rmdirSync2(workflowDir);
7809
+ if (readdirSync9(workflowDir).length === 0) {
7810
+ rmdirSync3(workflowDir);
7304
7811
  }
7305
7812
  } catch {}
7306
7813
  throw error;
@@ -7310,7 +7817,7 @@ async function promoteAuditPlans(outDir, selected, options) {
7310
7817
  return { workflowId, snapshotPath };
7311
7818
  }
7312
7819
  function resolveSelectedPlanFiles(outDir, selected) {
7313
- const files = readdirSync8(outDir).filter((f) => /^\d{3}-.*\.md$/.test(f)).sort();
7820
+ const files = readdirSync9(outDir).filter((f) => /^\d{3}-.*\.md$/.test(f)).sort();
7314
7821
  const byNum = new Map;
7315
7822
  const byStem = new Map;
7316
7823
  for (const file of files) {
@@ -7325,7 +7832,7 @@ function resolveSelectedPlanFiles(outDir, selected) {
7325
7832
  for (const id of selected) {
7326
7833
  const file = byNum.get(id) ?? byStem.get(id) ?? byStem.get(id.replace(/\.md$/, ""));
7327
7834
  if (file === undefined) {
7328
- throw new Error(`promoteAuditPlans: selected plan ${JSON.stringify(id)} does not match any NNN-*.md file in ${resolve13(outDir)}`);
7835
+ throw new Error(`promoteAuditPlans: selected plan ${JSON.stringify(id)} does not match any NNN-*.md file in ${resolve14(outDir)}`);
7329
7836
  }
7330
7837
  if (!seen.has(file)) {
7331
7838
  seen.add(file);
@@ -7364,7 +7871,7 @@ function readExecutionOrderIndex(outDir) {
7364
7871
  return rows;
7365
7872
  }
7366
7873
  function planFileRel(outDir, planFile) {
7367
- const resolved = resolve13(outDir);
7874
+ const resolved = resolve14(outDir);
7368
7875
  const parts = resolved.split(sep4);
7369
7876
  const plansIdx = parts.lastIndexOf("plans");
7370
7877
  if (plansIdx >= 0) {
@@ -7373,8 +7880,8 @@ function planFileRel(outDir, planFile) {
7373
7880
  return planFile;
7374
7881
  }
7375
7882
  // src/compound.ts
7376
- import { existsSync as existsSync13, readdirSync as readdirSync9, readFileSync as readFileSync12 } from "node:fs";
7377
- import { basename as basename11, isAbsolute as isAbsolute10, join as join17, relative as relative6, resolve as resolve14, sep as sep5 } from "node:path";
7883
+ import { existsSync as existsSync13, readdirSync as readdirSync10, readFileSync as readFileSync12 } from "node:fs";
7884
+ import { basename as basename11, isAbsolute as isAbsolute10, join as join17, relative as relative6, resolve as resolve15, sep as sep5 } from "node:path";
7378
7885
  function violation11(severity, code, message, fix) {
7379
7886
  return { ok: false, severity, code, message, fix };
7380
7887
  }
@@ -7660,7 +8167,7 @@ function referenceExists(repoRoot, docText) {
7660
8167
  const dir = stack.pop();
7661
8168
  let entries;
7662
8169
  try {
7663
- entries = readdirSync9(dir, { withFileTypes: true });
8170
+ entries = readdirSync10(dir, { withFileTypes: true });
7664
8171
  } catch {
7665
8172
  continue;
7666
8173
  }
@@ -7681,7 +8188,7 @@ function referenceExists(repoRoot, docText) {
7681
8188
  for (const { ref, isSymbol, module } of refs) {
7682
8189
  if (!isSymbol || module === undefined) {
7683
8190
  const candidate = ref.replace(LINE_SUFFIX_RE, "").replace(ANCHOR_RE, "");
7684
- if (existsSync13(resolve14(repoRoot, candidate))) {
8191
+ if (existsSync13(resolve15(repoRoot, candidate))) {
7685
8192
  checked++;
7686
8193
  } else {
7687
8194
  violations.push(violation11("medium", "compound.reference.missing-file", `referenced path \`${ref}\` does not exist under ${repoRoot} (compound-refresh Phase 2: referenced code still exists?)`, "update the doc to reference an existing path, or delete the stale reference"));
@@ -7701,7 +8208,7 @@ function collectKnowledgeDocs(dir) {
7701
8208
  const current = stack.pop();
7702
8209
  let entries;
7703
8210
  try {
7704
- entries = readdirSync9(current, { withFileTypes: true });
8211
+ entries = readdirSync10(current, { withFileTypes: true });
7705
8212
  } catch {
7706
8213
  continue;
7707
8214
  }
@@ -7764,9 +8271,9 @@ function isFileLikeRoot(root) {
7764
8271
  return /^[^.]*\.[A-Za-z0-9]{1,10}$/.test(basename11(root));
7765
8272
  }
7766
8273
  function scopeGuard(path, allowedRoots) {
7767
- const resolved = resolve14(path);
8274
+ const resolved = resolve15(path);
7768
8275
  for (const root of allowedRoots) {
7769
- const r = resolve14(root);
8276
+ const r = resolve15(root);
7770
8277
  if (isFileLikeRoot(r)) {
7771
8278
  if (resolved === r)
7772
8279
  return { ok: true, violations: [] };
@@ -8395,7 +8902,7 @@ function resolveAssetPath(skillName, relPath, host) {
8395
8902
  return `skill \`${skillName}\` → ${relPath} (${resolveSkillRoot(host, { skill: skillName, rel: relPath })})`;
8396
8903
  }
8397
8904
  // src/prreview.ts
8398
- import { readdirSync as readdirSync10 } from "node:fs";
8905
+ import { readdirSync as readdirSync11 } from "node:fs";
8399
8906
  import { isAbsolute as isAbsolute11, join as join19 } from "node:path";
8400
8907
  var MERGE_CLASSES = ["must-fix", "should-fix", "nit"];
8401
8908
  var PR_VERDICTS = ["ship it", "needs fixes", "blocked"];
@@ -8636,7 +9143,7 @@ function prReviewReportPath(opts) {
8636
9143
  const sameStem = new RegExp(`^${escaped}(?:-r([0-9]+))?\\.md$`);
8637
9144
  let dirents;
8638
9145
  try {
8639
- dirents = readdirSync10(opts.reportsDir, { withFileTypes: true });
9146
+ dirents = readdirSync11(opts.reportsDir, { withFileTypes: true });
8640
9147
  } catch (error) {
8641
9148
  if (error.code === "ENOENT")
8642
9149
  dirents = [];
@@ -9361,7 +9868,7 @@ function validateQcReport(text) {
9361
9868
  import { execFileSync as execFileSync5 } from "node:child_process";
9362
9869
  import { randomUUID as randomUUID2 } from "node:crypto";
9363
9870
  import { existsSync as existsSync15, mkdirSync as mkdirSync9, readFileSync as readFileSync13, statSync as statSync6, unlinkSync as unlinkSync4, writeFileSync as writeFileSync7 } from "node:fs";
9364
- import { basename as basename12, dirname as dirname12, isAbsolute as isAbsolute12, join as join20, resolve as resolve15, sep as sep6 } from "node:path";
9871
+ import { basename as basename12, dirname as dirname12, isAbsolute as isAbsolute12, join as join20, resolve as resolve16, sep as sep6 } from "node:path";
9365
9872
  var SESSION_DIR = "sessions";
9366
9873
  var SNAPSHOT_FILE2 = "snapshot.json";
9367
9874
  var ENVELOPE_KEYS = ["schema_version", "role", "session_id", "workflow_id", "plan_id", "harness_root"];
@@ -9460,7 +9967,7 @@ function localStore(harnessRoot) {
9460
9967
  try {
9461
9968
  store = getArtifactStore();
9462
9969
  } catch (error) {
9463
- throw new CoordinationError("coordination.local-store-required", `no artifact store is resolvable from ${resolve15(process.cwd())} — scoped coordination requires createFsStore(<control harness root>): ${errorMessage(error)}`, { harness_root: harnessRoot });
9970
+ throw new CoordinationError("coordination.local-store-required", `no artifact store is resolvable from ${resolve16(process.cwd())} — scoped coordination requires createFsStore(<control harness root>): ${errorMessage(error)}`, { harness_root: harnessRoot });
9464
9971
  }
9465
9972
  const root = "root" in store ? store.root : undefined;
9466
9973
  if (typeof root !== "string") {
@@ -9479,8 +9986,8 @@ function isWithin(parent, child) {
9479
9986
  }
9480
9987
  function resolveProcessHarnessDir(cwd = process.cwd(), harnessDir) {
9481
9988
  if (isNonEmptyString(harnessDir))
9482
- return resolve15(cwd, harnessDir);
9483
- const start = resolve15(cwd);
9989
+ return resolve16(cwd, harnessDir);
9990
+ const start = resolve16(cwd);
9484
9991
  const main = readMainWorktree(start);
9485
9992
  if (main !== null)
9486
9993
  return resolveHarnessDir(main.root);
@@ -9520,7 +10027,7 @@ var ASSIGNMENT_FIELDS = [
9520
10027
  ];
9521
10028
  var ABSOLUTE_PATH_HEADERS = ["control harness root", "plan path", "worktree path", "sdd dir"];
9522
10029
  function parseAssignmentFile(assignmentPath) {
9523
- const abs = resolve15(assignmentPath);
10030
+ const abs = resolve16(assignmentPath);
9524
10031
  if (!existsSync15(abs)) {
9525
10032
  throw new CoordinationError("coordination.assignment-invalid", `Assignment not found: ${abs}`, { path: abs });
9526
10033
  }
@@ -9652,7 +10159,7 @@ function registerPathOf(harnessRoot, projectId) {
9652
10159
  }
9653
10160
  function scopeFromAssignment(assignment, cwd, options) {
9654
10161
  const harnessRoot = assignment.controlHarnessRoot;
9655
- const processRoot = resolveProcessHarnessDir(cwd);
10162
+ const processRoot = options.chosenRoot === undefined ? resolveProcessHarnessDir(cwd) : null;
9656
10163
  if (processRoot !== null && canonicalTarget(processRoot) !== harnessRoot) {
9657
10164
  throw new CoordinationError("coordination.scope-mismatch", `Assignment "Control harness root" ${harnessRoot} does not match the process harness root ${canonicalTarget(processRoot)}`, { expected: harnessRoot, actual: canonicalTarget(processRoot) });
9658
10165
  }
@@ -9719,7 +10226,7 @@ async function resolvePlanScope(input, cwd = process.cwd()) {
9719
10226
  }
9720
10227
  const harnessRoot = resolveProcessHarnessDir(cwd, harnessDir);
9721
10228
  if (harnessRoot === null) {
9722
- throw new CoordinationError("coordination.harness-not-found", `no harness root is resolvable from ${resolve15(cwd)} — pass the control harness root explicitly`, { cwd: resolve15(cwd) });
10229
+ throw new CoordinationError("coordination.harness-not-found", `no harness root is resolvable from ${resolve16(cwd)} — pass the control harness root explicitly`, { cwd: resolve16(cwd) });
9723
10230
  }
9724
10231
  const snapshot = readSnapshot(dirname12(snapshotPathOf(harnessRoot, workflowId)));
9725
10232
  const { row } = findPlanRow2(snapshot, planId);
@@ -9734,7 +10241,7 @@ function readSessionEnvelope(sessionPath) {
9734
10241
  if (!isNonEmptyString(sessionPath) || !isAbsolute12(sessionPath)) {
9735
10242
  throw invalidInput("sessionPath must be an absolute path");
9736
10243
  }
9737
- const abs = resolve15(sessionPath);
10244
+ const abs = resolve16(sessionPath);
9738
10245
  if (!existsSync15(abs)) {
9739
10246
  throw new CoordinationError("coordination.session-not-found", `session envelope not found: ${abs}`, { path: abs });
9740
10247
  }
@@ -10081,11 +10588,12 @@ function assertRootRegisterEntry(harnessRoot, workflowId) {
10081
10588
  if (entry === undefined) {
10082
10589
  throw new CoordinationError("coordination.workflow-not-found", `workflow ${workflowId} is not an active entry of ${path} (kind ${store.root})`, { path, workflow_id: workflowId });
10083
10590
  }
10591
+ return entry;
10084
10592
  }
10085
10593
  function assertCoordinatorResidency(cwd, snapshot) {
10086
10594
  const main = readMainWorktree(cwd);
10087
10595
  if (main === null) {
10088
- throw new CoordinationError("coordination.not-in-git", `coordinator binding requires a Git process root — ${resolve15(cwd)} has no readable main worktree`, { cwd: resolve15(cwd) });
10596
+ throw new CoordinationError("coordination.not-in-git", `coordinator binding requires a Git process root — ${resolve16(cwd)} has no readable main worktree`, { cwd: resolve16(cwd) });
10089
10597
  }
10090
10598
  const here = canonicalizeNearestExisting(cwd);
10091
10599
  const allowed = [canonicalTarget(main.root)];
@@ -10142,7 +10650,7 @@ async function bindCoordinatorSession(cwd, workflowId, harnessDir) {
10142
10650
  function requireProcessRoot(cwd, harnessDir) {
10143
10651
  const root = resolveProcessHarnessDir(cwd, harnessDir);
10144
10652
  if (root === null) {
10145
- throw new CoordinationError("coordination.harness-not-found", `no harness root is resolvable from ${resolve15(cwd)} — pass the control harness root explicitly`, { cwd: resolve15(cwd) });
10653
+ throw new CoordinationError("coordination.harness-not-found", `no harness root is resolvable from ${resolve16(cwd)} — pass the control harness root explicitly`, { cwd: resolve16(cwd) });
10146
10654
  }
10147
10655
  return root;
10148
10656
  }
@@ -10657,7 +11165,10 @@ async function mutatePlanCoordination(request) {
10657
11165
  throw invalidInput("prepare requires an absolute assignmentPath");
10658
11166
  }
10659
11167
  const assignment = parseAssignmentFile(operation.assignmentPath);
10660
- const scope = scopeFromAssignment(assignment, session.harness_root, { requirePrepared: false });
11168
+ const scope = scopeFromAssignment(assignment, session.harness_root, {
11169
+ requirePrepared: false,
11170
+ chosenRoot: session.harness_root
11171
+ });
10661
11172
  if (request.planId !== undefined && request.planId !== scope.planId) {
10662
11173
  throw new CoordinationError("coordination.scope-mismatch", `request planId ${request.planId} is not the Assignment's plan ${scope.planId}`, { expected: scope.planId, actual: request.planId });
10663
11174
  }
@@ -10833,7 +11344,7 @@ function gitCheckout(path) {
10833
11344
  let operation;
10834
11345
  for (const [marker, label] of UNFINISHED_GIT_OPERATIONS) {
10835
11346
  const markerPath = gitRead(path, ["rev-parse", "--git-path", marker]);
10836
- if (markerPath !== undefined && markerPath.length > 0 && existsSync15(resolve15(path, markerPath))) {
11347
+ if (markerPath !== undefined && markerPath.length > 0 && existsSync15(resolve16(path, markerPath))) {
10837
11348
  operation = label;
10838
11349
  break;
10839
11350
  }
@@ -11762,6 +12273,624 @@ async function replaceProjectRegister(input, harnessRoot) {
11762
12273
  });
11763
12274
  });
11764
12275
  }
12276
+ var PREPARE_PHASE = "phase-1-prepare";
12277
+ var PREPARE_PATCH_KEYS = [
12278
+ "mainWorktreeBranch",
12279
+ "appendPlans",
12280
+ "integrationWorktreePath",
12281
+ "planParallelism"
12282
+ ];
12283
+ var PREPARE_APPEND_KEYS = ["id", "title", "file", "metadata"];
12284
+ var PREPARE_APPEND_METADATA_KEYS = [
12285
+ "primary_spec",
12286
+ "spec_refs",
12287
+ "iteration_compass",
12288
+ "iteration_refs",
12289
+ "working_branch",
12290
+ "spec_integration_branch",
12291
+ "merge_target"
12292
+ ];
12293
+ var PLAN_PARALLELISM_VALUES = ["serial", "parallel"];
12294
+ function prepareAmendmentRefusal(reason, message, details = {}) {
12295
+ const code = `coordination.prepare-amendment.${reason}`;
12296
+ return new CoordinationError(code, message, details);
12297
+ }
12298
+ function prepareWorkflowScope(sessionPath, cwd) {
12299
+ const session = readSessionEnvelope(sessionPath);
12300
+ if (session.role !== "coordinator") {
12301
+ throw new CoordinationError("coordination.session-role", `workflow Prepare verbs require a coordinator session, not ${session.role}`, { actual: session.role });
12302
+ }
12303
+ const harnessRoot = canonicalizeNearestExisting(session.harness_root);
12304
+ localStore(harnessRoot);
12305
+ const processRoot = resolveProcessHarnessDir(cwd);
12306
+ if (processRoot !== null && canonicalTarget(processRoot) !== harnessRoot) {
12307
+ throw new CoordinationError("coordination.scope-mismatch", `session ${session.session_id} belongs to harness root ${harnessRoot}, but this process resolves ${canonicalTarget(processRoot)}`, { expected: harnessRoot, actual: canonicalTarget(processRoot) });
12308
+ }
12309
+ const workflowId = safePlanId(session.workflow_id, "workflow_id");
12310
+ const snapshotPath = assertSnapshotPath(harnessRoot, workflowId, snapshotPathOf(harnessRoot, workflowId));
12311
+ return { session, sessionPath: canonicalTarget(sessionPath), harnessRoot, workflowId, snapshotPath };
12312
+ }
12313
+ function readPrepareSnapshot(snapshotPath) {
12314
+ const bytes = readArtifactBytes(snapshotPath);
12315
+ if (bytes === undefined) {
12316
+ throw new CoordinationError("coordination.workflow-not-found", `workflow snapshot not found: ${snapshotPath}`, {
12317
+ path: snapshotPath
12318
+ });
12319
+ }
12320
+ return { snapshot: readSnapshot(dirname12(snapshotPath)), version: bytes.version };
12321
+ }
12322
+ function prepareCompassDeclaration(frontmatter, key, workflowId, path) {
12323
+ const value = frontmatter[key];
12324
+ if (value === undefined)
12325
+ return;
12326
+ if (!isNonEmptyString(value)) {
12327
+ throw prepareAmendmentRefusal("compass-mismatch", `workflow ${workflowId} compass ${path} declares a malformed ${key} — every declaration must be a non-empty string`, { workflow_id: workflowId, path, field: key, actual: value });
12328
+ }
12329
+ return value;
12330
+ }
12331
+ function readPrepareCompass(harnessRoot, snapshot) {
12332
+ const ref = snapshot.compass_ref;
12333
+ if (!isNonEmptyString(ref) || isAbsolute12(ref)) {
12334
+ throw prepareAmendmentRefusal("compass-mismatch", `workflow ${snapshot.id} declares no usable compass_ref — the amendment requires the reviewed iteration compass`, { workflow_id: snapshot.id, actual: ref ?? null });
12335
+ }
12336
+ const root = canonicalizeNearestExisting(harnessRoot);
12337
+ const path = canonicalTarget(join20(harnessRoot, ref));
12338
+ if (!isWithin(root, path)) {
12339
+ throw prepareAmendmentRefusal("compass-mismatch", `workflow ${snapshot.id} compass_ref ${JSON.stringify(ref)} resolves outside the harness root ${root}`, { workflow_id: snapshot.id, path, expected: ref });
12340
+ }
12341
+ let bytes;
12342
+ try {
12343
+ bytes = readFileSync13(path);
12344
+ } catch (error) {
12345
+ throw prepareAmendmentRefusal("compass-mismatch", `workflow ${snapshot.id} compass ${path} is unreadable: ${errorMessage(error)}`, { workflow_id: snapshot.id, path });
12346
+ }
12347
+ let frontmatter;
12348
+ try {
12349
+ frontmatter = parseCompassFrontmatterText(bytes.toString("utf8"), path);
12350
+ } catch (error) {
12351
+ throw prepareAmendmentRefusal("compass-mismatch", `workflow ${snapshot.id} compass ${path} has no parsable frontmatter: ${errorMessage(error)}`, { workflow_id: snapshot.id, path });
12352
+ }
12353
+ const iterationId = frontmatter.iteration_id;
12354
+ if (!isNonEmptyString(iterationId)) {
12355
+ throw prepareAmendmentRefusal("compass-mismatch", `workflow ${snapshot.id} compass ${path} declares no iteration_id — a lifecycle amends only its own compass`, { workflow_id: snapshot.id, path, actual: iterationId ?? null });
12356
+ }
12357
+ if (iterationId !== snapshot.id) {
12358
+ throw prepareAmendmentRefusal("compass-mismatch", `compass ${path} declares iteration_id ${iterationId}, not workflow ${snapshot.id} — a lifecycle amends only its own compass`, { workflow_id: snapshot.id, expected: snapshot.id, actual: iterationId });
12359
+ }
12360
+ const declaredPlans = frontmatter.plans;
12361
+ if (!Array.isArray(declaredPlans) || declaredPlans.length === 0) {
12362
+ throw prepareAmendmentRefusal("compass-mismatch", `workflow ${snapshot.id} compass ${path} declares no plan ids in its frontmatter — the amendment cannot verify the approved plan set`, { workflow_id: snapshot.id, path, actual: declaredPlans ?? null });
12363
+ }
12364
+ const malformed = declaredPlans.filter((entry) => !isNonEmptyString(entry));
12365
+ if (malformed.length > 0) {
12366
+ throw prepareAmendmentRefusal("compass-mismatch", `workflow ${snapshot.id} compass ${path} declares ${malformed.length} malformed plan id(s) — every plans entry must be a non-empty string`, { workflow_id: snapshot.id, path, plans: declaredPlans });
12367
+ }
12368
+ const planIds = declaredPlans;
12369
+ if (new Set(planIds).size !== planIds.length) {
12370
+ throw prepareAmendmentRefusal("compass-mismatch", `workflow ${snapshot.id} compass ${path} declares the same plan id more than once — the reviewed plan set is not a multiset`, { workflow_id: snapshot.id, path, plans: planIds });
12371
+ }
12372
+ const specIntegrationBranch = prepareCompassDeclaration(frontmatter, "spec_integration_branch", snapshot.id, path);
12373
+ const integrationWorktreePath = prepareCompassDeclaration(frontmatter, "integration_worktree_path", snapshot.id, path);
12374
+ return {
12375
+ path,
12376
+ version: `sha256:${sha256Bytes(bytes)}`,
12377
+ planIds,
12378
+ ...specIntegrationBranch !== undefined ? { specIntegrationBranch } : {},
12379
+ ...integrationWorktreePath !== undefined ? { integrationWorktreePath } : {}
12380
+ };
12381
+ }
12382
+ function prepareAdmission(harnessRoot, workflowId, snapshot) {
12383
+ const entry = assertRootRegisterEntry(harnessRoot, workflowId);
12384
+ if (isNonEmptyString(entry.status) && entry.status !== "running") {
12385
+ return {
12386
+ ok: false,
12387
+ reason: "not-prepare",
12388
+ message: `root register entry ${workflowId} is ${entry.status} — the amendment is available only while the lifecycle runs`,
12389
+ details: { workflow_id: workflowId, status: entry.status }
12390
+ };
12391
+ }
12392
+ if (snapshot.status !== "running") {
12393
+ return {
12394
+ ok: false,
12395
+ reason: "not-prepare",
12396
+ message: `workflow ${workflowId} is ${snapshot.status} — the amendment is available only in Prepare`,
12397
+ details: { workflow_id: workflowId, status: snapshot.status }
12398
+ };
12399
+ }
12400
+ if (snapshot.phase !== PREPARE_PHASE) {
12401
+ return {
12402
+ ok: false,
12403
+ reason: "not-prepare",
12404
+ message: `workflow ${workflowId} is in ${snapshot.phase ?? "no declared phase"}, not ${PREPARE_PHASE}`,
12405
+ details: { workflow_id: workflowId, expected: PREPARE_PHASE, actual: snapshot.phase ?? null }
12406
+ };
12407
+ }
12408
+ if (snapshot.integration_merge_lease !== undefined) {
12409
+ return {
12410
+ ok: false,
12411
+ reason: "execution-started",
12412
+ message: `workflow ${workflowId} carries an integration merge lease — execution ownership already exists`,
12413
+ details: { workflow_id: workflowId }
12414
+ };
12415
+ }
12416
+ for (const row of snapshot.plans) {
12417
+ const planId = rowPlanIds(row)[0] ?? "";
12418
+ const status = rowStatusOf(row);
12419
+ if (status !== "Todo") {
12420
+ return {
12421
+ ok: false,
12422
+ reason: "execution-started",
12423
+ message: `plan ${planId} is ${status} — the amendment is available only while every row is Todo`,
12424
+ details: { workflow_id: workflowId, plan_id: planId, actual: status }
12425
+ };
12426
+ }
12427
+ if (row.progress !== undefined && row.progress !== 0) {
12428
+ return {
12429
+ ok: false,
12430
+ reason: "execution-started",
12431
+ message: `plan ${planId} reports progress ${JSON.stringify(row.progress)} — the amendment must not rewrite executed work`,
12432
+ details: { workflow_id: workflowId, plan_id: planId, actual: row.progress }
12433
+ };
12434
+ }
12435
+ if (row.execution_lease !== undefined) {
12436
+ return {
12437
+ ok: false,
12438
+ reason: "execution-started",
12439
+ message: `plan ${planId} carries an execution lease — the plan has an owner and is no longer in Prepare`,
12440
+ details: { workflow_id: workflowId, plan_id: planId, holder: isPlainObject2(row.execution_lease) ? row.execution_lease.holder : null }
12441
+ };
12442
+ }
12443
+ if (row.coordination !== undefined) {
12444
+ return {
12445
+ ok: false,
12446
+ reason: "execution-started",
12447
+ message: `plan ${planId} carries a coordination block — preparation or execution evidence already exists for this row`,
12448
+ details: { workflow_id: workflowId, plan_id: planId, revision: rowCoordinationOf(row)?.revision ?? null }
12449
+ };
12450
+ }
12451
+ }
12452
+ return { ok: true };
12453
+ }
12454
+ var PLAN_CONSULTED_HEADERS = {
12455
+ plan_id: true,
12456
+ "main worktree branch": true,
12457
+ "working branch": true
12458
+ };
12459
+ function planHeadersOf(planPath, planId) {
12460
+ const headers = new Map;
12461
+ let marker;
12462
+ let markerLength = 0;
12463
+ for (const raw of readFileSync13(planPath, "utf8").split(/\r?\n/)) {
12464
+ const line = raw.trim();
12465
+ const fence = /^(`{3,}|~{3,})/.exec(line);
12466
+ if (fence !== null) {
12467
+ const run = fence[1];
12468
+ if (marker === undefined) {
12469
+ marker = run.charAt(0);
12470
+ markerLength = run.length;
12471
+ } else if (run.charAt(0) === marker && run.length >= markerLength) {
12472
+ marker = undefined;
12473
+ }
12474
+ continue;
12475
+ }
12476
+ if (marker !== undefined)
12477
+ continue;
12478
+ const match = /^\*{0,2}([^:*]+?)\*{0,2}:\*{0,2}\s*(\S.*)$/.exec(line);
12479
+ if (match === null)
12480
+ continue;
12481
+ const label = match[1].trim();
12482
+ const key = label.toLowerCase();
12483
+ if (PLAN_CONSULTED_HEADERS[key] !== true)
12484
+ continue;
12485
+ const value = match[2].trim();
12486
+ const prior = headers.get(key);
12487
+ if (prior !== undefined && prior !== value) {
12488
+ throw prepareAmendmentRefusal("invalid-plan", `plan ${planId} markdown declares conflicting "${label}" headers (${prior} vs ${value})`, { plan_id: planId, header: label });
12489
+ }
12490
+ headers.set(key, value);
12491
+ }
12492
+ return headers;
12493
+ }
12494
+ function prepareReferencePath(harnessRoot, value, field, planId) {
12495
+ if (!isNonEmptyString(value) || !isAbsolute12(value)) {
12496
+ throw prepareAmendmentRefusal("invalid-plan", `plan ${planId} ${field} must be an absolute path — got ${JSON.stringify(value ?? null)}`, { plan_id: planId, field, actual: value ?? null });
12497
+ }
12498
+ const canonical = canonicalTarget(value);
12499
+ if (!isWithin(canonicalizeNearestExisting(harnessRoot), canonical)) {
12500
+ throw prepareAmendmentRefusal("invalid-plan", `plan ${planId} ${field} ${canonical} escapes the harness root ${harnessRoot}`, { plan_id: planId, field, path: canonical, expected: harnessRoot });
12501
+ }
12502
+ if (!existsSync15(canonical) || !statSync6(canonical).isFile()) {
12503
+ throw prepareAmendmentRefusal("invalid-plan", `plan ${planId} ${field} does not exist: ${canonical}`, {
12504
+ plan_id: planId,
12505
+ field,
12506
+ path: canonical
12507
+ });
12508
+ }
12509
+ return canonical;
12510
+ }
12511
+ function prepareReferenceList(harnessRoot, value, field, planId) {
12512
+ if (!Array.isArray(value)) {
12513
+ throw prepareAmendmentRefusal("invalid-plan", `plan ${planId} ${field} must be an array of absolute paths — got ${JSON.stringify(value ?? null)}`, { plan_id: planId, field, actual: value ?? null });
12514
+ }
12515
+ return value.map((entry, index) => prepareReferencePath(harnessRoot, entry, `${field}[${index}]`, planId));
12516
+ }
12517
+ function prepareMetadataString(metadata, key, planId) {
12518
+ const value = metadata[key];
12519
+ if (!isNonEmptyString(value)) {
12520
+ throw prepareAmendmentRefusal("invalid-plan", `plan ${planId} metadata.${key} must be a non-empty string — got ${JSON.stringify(value ?? null)}`, { plan_id: planId, field: `metadata.${key}`, actual: value ?? null });
12521
+ }
12522
+ return value;
12523
+ }
12524
+ function readPlanAppend(value, context) {
12525
+ if (!isPlainObject2(value)) {
12526
+ throw prepareAmendmentRefusal("invalid-plan", "every appendPlans entry must be an object", { actual: value ?? null });
12527
+ }
12528
+ const unexpected = Object.keys(value).filter((key) => !PREPARE_APPEND_KEYS.includes(key));
12529
+ if (unexpected.length > 0) {
12530
+ throw prepareAmendmentRefusal("invalid-plan", `a plan append accepts only ${PREPARE_APPEND_KEYS.join(", ")} — unexpected key(s): ${unexpected.join(", ")}`, { allowed: [...PREPARE_APPEND_KEYS], unexpected });
12531
+ }
12532
+ const id = value.id;
12533
+ if (!isNonEmptyString(id)) {
12534
+ throw prepareAmendmentRefusal("invalid-plan", `a plan append requires a non-empty id — got ${JSON.stringify(id ?? null)}`, {
12535
+ actual: id ?? null
12536
+ });
12537
+ }
12538
+ try {
12539
+ assertSafePathComponent(id, "plan id");
12540
+ } catch (error) {
12541
+ throw prepareAmendmentRefusal("invalid-plan", `plan id ${JSON.stringify(id)} is not a safe path component: ${errorMessage(error)}`, {
12542
+ plan_id: id
12543
+ });
12544
+ }
12545
+ const title = value.title;
12546
+ if (!isNonEmptyString(title)) {
12547
+ throw prepareAmendmentRefusal("invalid-plan", `plan ${id} requires a non-empty title`, { plan_id: id, actual: title ?? null });
12548
+ }
12549
+ const declaredFile = value.file;
12550
+ if (!isNonEmptyString(declaredFile) || !isAbsolute12(declaredFile)) {
12551
+ throw prepareAmendmentRefusal("invalid-plan", `plan ${id} file must be the absolute plan path — got ${JSON.stringify(declaredFile ?? null)}`, { plan_id: id, actual: declaredFile ?? null });
12552
+ }
12553
+ const planPath = canonicalTarget(declaredFile);
12554
+ const planDir = canonicalizeNearestExisting(resolvePlanDir(context.harnessRoot));
12555
+ if (dirname12(planPath) !== planDir || basename12(planPath) !== `${id}.md`) {
12556
+ throw prepareAmendmentRefusal("invalid-plan", `plan ${id} file ${planPath} is not ${join20(planDir, `${id}.md`)}`, { plan_id: id, expected: join20(planDir, `${id}.md`), actual: planPath });
12557
+ }
12558
+ if (!existsSync15(planPath) || !statSync6(planPath).isFile()) {
12559
+ throw prepareAmendmentRefusal("invalid-plan", `plan ${id} markdown not found: ${planPath}`, { plan_id: id, path: planPath });
12560
+ }
12561
+ const headers = planHeadersOf(planPath, id);
12562
+ const declaredPlanId = headers.get("plan_id");
12563
+ if (declaredPlanId === undefined) {
12564
+ throw prepareAmendmentRefusal("invalid-plan", `plan ${id} markdown ${planPath} declares no plan_id header — the row cannot be traced to its reviewed plan`, { plan_id: id, path: planPath });
12565
+ }
12566
+ if (declaredPlanId !== id) {
12567
+ throw prepareAmendmentRefusal("invalid-plan", `plan append id ${id} does not match the plan markdown header plan_id ${declaredPlanId} (${planPath})`, { plan_id: id, expected: id, actual: declaredPlanId, path: planPath });
12568
+ }
12569
+ const metadata = value.metadata;
12570
+ if (!isPlainObject2(metadata)) {
12571
+ throw prepareAmendmentRefusal("invalid-plan", `plan ${id} metadata must be an object`, { plan_id: id, actual: metadata ?? null });
12572
+ }
12573
+ const unexpectedMetadata = Object.keys(metadata).filter((key) => !PREPARE_APPEND_METADATA_KEYS.includes(key));
12574
+ if (unexpectedMetadata.length > 0) {
12575
+ throw prepareAmendmentRefusal("invalid-plan", `plan ${id} metadata accepts only ${PREPARE_APPEND_METADATA_KEYS.join(", ")} — unexpected key(s): ${unexpectedMetadata.join(", ")}`, { plan_id: id, allowed: [...PREPARE_APPEND_METADATA_KEYS], unexpected: unexpectedMetadata });
12576
+ }
12577
+ const primarySpec = prepareReferencePath(context.harnessRoot, metadata.primary_spec, "metadata.primary_spec", id);
12578
+ const specRefs = prepareReferenceList(context.harnessRoot, metadata.spec_refs, "metadata.spec_refs", id);
12579
+ const iterationCompass = prepareReferencePath(context.harnessRoot, metadata.iteration_compass, "metadata.iteration_compass", id);
12580
+ if (iterationCompass !== context.compass.path) {
12581
+ throw prepareAmendmentRefusal("compass-mismatch", `plan ${id} metadata.iteration_compass ${iterationCompass} is not this workflow's reviewed compass ${context.compass.path}`, { plan_id: id, expected: context.compass.path, actual: iterationCompass });
12582
+ }
12583
+ const iterationRefs = prepareReferenceList(context.harnessRoot, metadata.iteration_refs, "metadata.iteration_refs", id);
12584
+ const workingBranch = prepareMetadataString(metadata, "working_branch", id);
12585
+ const specIntegrationBranch = prepareMetadataString(metadata, "spec_integration_branch", id);
12586
+ const mergeTarget = prepareMetadataString(metadata, "merge_target", id);
12587
+ const anchors = context.snapshot.branch ?? {};
12588
+ for (const [label, branch] of [
12589
+ ["branch.base", anchors.base],
12590
+ ["branch.integration", anchors.integration],
12591
+ ["branch.target", anchors.target]
12592
+ ]) {
12593
+ if (isNonEmptyString(branch) && branch === workingBranch) {
12594
+ throw prepareAmendmentRefusal("invalid-plan", `plan ${id} metadata.working_branch ${workingBranch} is the workflow's ${label} — a plan row owns its own feature branch`, { plan_id: id, field: "metadata.working_branch", actual: workingBranch });
12595
+ }
12596
+ }
12597
+ const declaredWorking = headers.get("working branch");
12598
+ if (declaredWorking === undefined) {
12599
+ throw prepareAmendmentRefusal("invalid-plan", `plan ${id} markdown ${planPath} declares no Working branch header — the appended row's branch metadata cannot be verified against the reviewed plan`, { plan_id: id, field: "metadata.working_branch", path: planPath });
12600
+ }
12601
+ if (declaredWorking !== workingBranch) {
12602
+ throw prepareAmendmentRefusal("invalid-plan", `plan ${id} metadata.working_branch ${workingBranch} does not match the Working branch declared by ${planPath} (${declaredWorking})`, { plan_id: id, expected: declaredWorking, actual: workingBranch, path: planPath });
12603
+ }
12604
+ const declaredMain = headers.get("main worktree branch");
12605
+ if (declaredMain === undefined) {
12606
+ throw prepareAmendmentRefusal("invalid-plan", `plan ${id} markdown ${planPath} declares no Main worktree branch header — the amendment cannot verify the branch the reviewed plan was written against`, { plan_id: id, field: "mainWorktreeBranch", path: planPath });
12607
+ }
12608
+ if (declaredMain !== context.mainWorktreeBranch) {
12609
+ throw prepareAmendmentRefusal("invalid-plan", `plan ${id} metadata records main worktree branch ${context.mainWorktreeBranch}, but ${planPath} declares ${declaredMain}`, { plan_id: id, expected: declaredMain, actual: context.mainWorktreeBranch, path: planPath });
12610
+ }
12611
+ if (isNonEmptyString(anchors.integration)) {
12612
+ for (const [field, recorded] of [
12613
+ ["metadata.spec_integration_branch", specIntegrationBranch],
12614
+ ["metadata.merge_target", mergeTarget]
12615
+ ]) {
12616
+ if (recorded !== anchors.integration) {
12617
+ throw prepareAmendmentRefusal("invalid-plan", `plan ${id} ${field} ${recorded} is not the workflow's integration branch ${anchors.integration}`, { plan_id: id, expected: anchors.integration, actual: recorded });
12618
+ }
12619
+ }
12620
+ }
12621
+ if (context.compass.specIntegrationBranch !== undefined && specIntegrationBranch !== context.compass.specIntegrationBranch) {
12622
+ throw prepareAmendmentRefusal("compass-mismatch", `plan ${id} metadata.spec_integration_branch ${specIntegrationBranch} is not the reviewed compass integration branch ${context.compass.specIntegrationBranch}`, { plan_id: id, expected: context.compass.specIntegrationBranch, actual: specIntegrationBranch });
12623
+ }
12624
+ const row = {
12625
+ id,
12626
+ title,
12627
+ file: planPath,
12628
+ status: "Todo",
12629
+ owner: "project-manager",
12630
+ progress: 0,
12631
+ created_at: nowIso(),
12632
+ metadata: {
12633
+ primary_spec: primarySpec,
12634
+ spec_refs: specRefs,
12635
+ iteration_compass: iterationCompass,
12636
+ iteration_refs: iterationRefs,
12637
+ working_branch: workingBranch,
12638
+ spec_integration_branch: specIntegrationBranch,
12639
+ merge_target: mergeTarget
12640
+ }
12641
+ };
12642
+ const gate2 = validatePlanRow(row);
12643
+ if (!gate2.ok) {
12644
+ throw prepareAmendmentRefusal("invalid-plan", `constructed row ${id} fails plan-row validation — ${summarize(gate2.violations)}`, {
12645
+ plan_id: id,
12646
+ violations: gate2.violations.map((entry) => entry.code)
12647
+ });
12648
+ }
12649
+ return row;
12650
+ }
12651
+ function readIntegrationWorktreePath(value, context) {
12652
+ const refuse = (message, details = {}) => {
12653
+ throw prepareAmendmentRefusal("invalid-worktree", message, details);
12654
+ };
12655
+ if (!isNonEmptyString(value) || !isAbsolute12(value)) {
12656
+ return refuse(`integrationWorktreePath must be an absolute path — got ${JSON.stringify(value ?? null)}`, {
12657
+ actual: value ?? null
12658
+ });
12659
+ }
12660
+ const path = canonicalTarget(value);
12661
+ if (!existsSync15(path) || !statSync6(path).isDirectory()) {
12662
+ return refuse(`integration checkout ${path} does not exist`, { path, expected: value });
12663
+ }
12664
+ const mainRoot = canonicalTarget(context.main.root);
12665
+ const control = canonicalizeNearestExisting(context.harnessRoot);
12666
+ const controlRepo = readMainWorktree(control);
12667
+ if (controlRepo === null) {
12668
+ return refuse(`the control harness root ${control} has no readable Git main worktree — the integration checkout cannot be proven to be a checkout of this repository`, { path, harness_root: control });
12669
+ }
12670
+ const controlMainRoot = canonicalTarget(controlRepo.root);
12671
+ if (controlMainRoot !== mainRoot) {
12672
+ return refuse(`this call runs from the main worktree of ${mainRoot}, not the repository owning the control harness root ${control} (${controlMainRoot}) — the recorded checkout must belong to that repository`, { path, expected: controlMainRoot, actual: mainRoot });
12673
+ }
12674
+ if (path === mainRoot || path === control) {
12675
+ return refuse(`integration checkout ${path} is the main/control checkout — a dedicated integration worktree is required`, {
12676
+ path,
12677
+ expected: `a checkout distinct from ${mainRoot}`
12678
+ });
12679
+ }
12680
+ const repo = readMainWorktree(path);
12681
+ if (repo === null || canonicalTarget(repo.root) !== controlMainRoot) {
12682
+ return refuse(`integration checkout ${path} is not a checkout of the repository owning the control harness root ${control} (${controlMainRoot})`, {
12683
+ path,
12684
+ expected: controlMainRoot,
12685
+ actual: repo === null ? null : canonicalTarget(repo.root)
12686
+ });
12687
+ }
12688
+ if (!isDistinctCheckout(context.main.root, path)) {
12689
+ return refuse(`integration checkout ${path} is not a distinct checkout (same Git checkout as ${mainRoot})`, {
12690
+ path,
12691
+ expected: `a checkout distinct from ${mainRoot}`
12692
+ });
12693
+ }
12694
+ const integrationBranch = context.snapshot.branch?.integration;
12695
+ if (!isNonEmptyString(integrationBranch)) {
12696
+ return refuse(`workflow ${context.snapshot.id} records no branch.integration — the integration checkout cannot be verified`, { workflow_id: context.snapshot.id, path });
12697
+ }
12698
+ const branch = gitRead(path, ["rev-parse", "--abbrev-ref", "HEAD"]);
12699
+ if (branch !== integrationBranch) {
12700
+ return refuse(`integration checkout ${path} is on ${branch === undefined ? "an unreadable checkout" : branch || "a detached HEAD"}, not the recorded ${integrationBranch}`, { path, expected: integrationBranch, actual: branch ?? null });
12701
+ }
12702
+ return path;
12703
+ }
12704
+ function samePlanIdSet(left, right) {
12705
+ const leftSet = new Set(left);
12706
+ const rightSet = new Set(right);
12707
+ if (leftSet.size !== rightSet.size)
12708
+ return false;
12709
+ for (const id of leftSet)
12710
+ if (!rightSet.has(id))
12711
+ return false;
12712
+ return true;
12713
+ }
12714
+ function readPreparePatch(patch, context) {
12715
+ if (!isPlainObject2(patch)) {
12716
+ throw prepareAmendmentRefusal("invalid-patch", "the amendment patch must be an object", { actual: patch ?? null });
12717
+ }
12718
+ const unexpected = Object.keys(patch).filter((key) => !PREPARE_PATCH_KEYS.includes(key));
12719
+ if (unexpected.length > 0) {
12720
+ throw prepareAmendmentRefusal("invalid-patch", `the amendment patch accepts only ${PREPARE_PATCH_KEYS.join(", ")} — unexpected key(s): ${unexpected.join(", ")}`, { allowed: [...PREPARE_PATCH_KEYS], unexpected });
12721
+ }
12722
+ const mainWorktreeBranch = patch.mainWorktreeBranch;
12723
+ if (!isNonEmptyString(mainWorktreeBranch)) {
12724
+ throw prepareAmendmentRefusal("invalid-patch", `the amendment patch requires mainWorktreeBranch as a non-empty string — got ${JSON.stringify(mainWorktreeBranch ?? null)}`, { actual: mainWorktreeBranch ?? null });
12725
+ }
12726
+ if (context.main.branch !== mainWorktreeBranch) {
12727
+ throw new CoordinationError("coordination.scope-mismatch", `the main worktree ${context.main.root} is on ${context.main.branch === "" ? "a detached HEAD" : context.main.branch}, but this amendment declares ${mainWorktreeBranch}`, { expected: mainWorktreeBranch, actual: context.main.branch });
12728
+ }
12729
+ const rawAppends = patch.appendPlans;
12730
+ if (!Array.isArray(rawAppends)) {
12731
+ throw prepareAmendmentRefusal("invalid-patch", "the amendment patch requires appendPlans as an array", {
12732
+ actual: rawAppends ?? null
12733
+ });
12734
+ }
12735
+ const requestedParallelism = patch.planParallelism;
12736
+ if (requestedParallelism !== undefined && !(isNonEmptyString(requestedParallelism) && PLAN_PARALLELISM_VALUES.includes(requestedParallelism))) {
12737
+ throw prepareAmendmentRefusal("invalid-patch", `planParallelism must be one of ${PLAN_PARALLELISM_VALUES.join(" | ")} — got ${JSON.stringify(requestedParallelism ?? null)}`, { allowed: [...PLAN_PARALLELISM_VALUES], actual: requestedParallelism ?? null });
12738
+ }
12739
+ const planParallelism = isNonEmptyString(requestedParallelism) ? requestedParallelism : undefined;
12740
+ const existingIds = new Set(context.snapshot.plans.flatMap((row) => rowPlanIds(row)));
12741
+ const declaredIds = new Set;
12742
+ for (const entry of rawAppends) {
12743
+ const id = isPlainObject2(entry) ? entry.id : undefined;
12744
+ if (!isNonEmptyString(id))
12745
+ continue;
12746
+ if (existingIds.has(id)) {
12747
+ throw prepareAmendmentRefusal("duplicate-plan", `plan ${id} is already a row of workflow ${context.snapshot.id} — the amendment appends new rows and never replaces existing ones`, { plan_id: id, workflow_id: context.snapshot.id });
12748
+ }
12749
+ if (declaredIds.has(id)) {
12750
+ throw prepareAmendmentRefusal("duplicate-plan", `plan ${id} appears twice in one patch`, { plan_id: id });
12751
+ }
12752
+ declaredIds.add(id);
12753
+ }
12754
+ const rows = rawAppends.map((entry) => readPlanAppend(entry, {
12755
+ harnessRoot: context.harnessRoot,
12756
+ snapshot: context.snapshot,
12757
+ compass: context.compass,
12758
+ mainWorktreeBranch
12759
+ }));
12760
+ const integrationWorktreePath = patch.integrationWorktreePath === undefined ? undefined : readIntegrationWorktreePath(patch.integrationWorktreePath, context);
12761
+ const recordedPath = isNonEmptyString(context.snapshot.integration_worktree_path) ? canonicalTarget(context.snapshot.integration_worktree_path) : undefined;
12762
+ const recordedParallelism = isPlainObject2(context.snapshot.execution_policy) ? context.snapshot.execution_policy.plan_parallelism : undefined;
12763
+ const changesPath = integrationWorktreePath !== undefined && integrationWorktreePath !== recordedPath;
12764
+ const changesPolicy = planParallelism !== undefined && planParallelism !== recordedParallelism;
12765
+ if (rows.length === 0 && !changesPath && !changesPolicy) {
12766
+ throw prepareAmendmentRefusal("invalid-patch", `the patch changes nothing on workflow ${context.snapshot.id} — it appends no plan, records no new integration checkout and no different plan parallelism`, { workflow_id: context.snapshot.id });
12767
+ }
12768
+ if (context.compass.specIntegrationBranch !== undefined) {
12769
+ const recordedBranch = context.snapshot.branch?.integration;
12770
+ if (!isNonEmptyString(recordedBranch) || recordedBranch !== context.compass.specIntegrationBranch) {
12771
+ throw prepareAmendmentRefusal("compass-mismatch", `workflow ${context.snapshot.id} records integration branch ${JSON.stringify(recordedBranch ?? null)}, but the reviewed compass ${context.compass.path} declares ${context.compass.specIntegrationBranch}`, { expected: context.compass.specIntegrationBranch, actual: recordedBranch ?? null });
12772
+ }
12773
+ }
12774
+ if (context.compass.integrationWorktreePath !== undefined) {
12775
+ const declaredPath = canonicalTarget(context.compass.integrationWorktreePath);
12776
+ const effectivePath = integrationWorktreePath ?? recordedPath;
12777
+ if (effectivePath !== declaredPath) {
12778
+ throw prepareAmendmentRefusal("compass-mismatch", `workflow ${context.snapshot.id} would record integration checkout ${effectivePath ?? "(none)"}, but the reviewed compass ${context.compass.path} declares ${declaredPath}`, { path: effectivePath ?? null, expected: declaredPath, actual: effectivePath ?? null });
12779
+ }
12780
+ }
12781
+ const proposedIds = [
12782
+ ...context.snapshot.plans.flatMap((row) => rowPlanIds(row)),
12783
+ ...rows.map((row) => rowPlanIds(row)[0] ?? "")
12784
+ ];
12785
+ if (!samePlanIdSet(context.compass.planIds, proposedIds)) {
12786
+ const missing = context.compass.planIds.filter((id) => !proposedIds.includes(id));
12787
+ const undeclared = proposedIds.filter((id) => !context.compass.planIds.includes(id));
12788
+ throw prepareAmendmentRefusal("compass-mismatch", `the amended workflow's plan ids must match the reviewed compass ${context.compass.path} exactly — missing: ${missing.join(", ") || "(none)"}; not declared by the compass: ${undeclared.join(", ") || "(none)"}`, { expected: [...context.compass.planIds], actual: proposedIds, missing, undeclared });
12789
+ }
12790
+ return {
12791
+ rows,
12792
+ ...integrationWorktreePath !== undefined ? { integrationWorktreePath } : {},
12793
+ ...planParallelism !== undefined ? { planParallelism } : {}
12794
+ };
12795
+ }
12796
+ function prepareVersionToken(value, field) {
12797
+ if (!isNonEmptyString(value)) {
12798
+ throw invalidInput(`prepare amendment ${field} is required — read it from \`show-prepare\``, { field });
12799
+ }
12800
+ const bare = value.startsWith("sha256:") ? value.slice("sha256:".length) : value;
12801
+ if (!/^[0-9a-f]{64}$/.test(bare)) {
12802
+ throw invalidInput(`prepare amendment ${field} must be a raw-byte sha256 version ("sha256:<64 hex>" or "<64 hex>") — got ${JSON.stringify(value)}`, { field });
12803
+ }
12804
+ return value;
12805
+ }
12806
+ function prepareVersionDigest(token) {
12807
+ return token.startsWith("sha256:") ? token.slice("sha256:".length) : token;
12808
+ }
12809
+ async function showPrepareWorkflow(input) {
12810
+ assertExactKeys(input, ["sessionPath", "cwd"], "prepare workflow read");
12811
+ const cwd = input.cwd ?? process.cwd();
12812
+ const scope = prepareWorkflowScope(input.sessionPath, cwd);
12813
+ const { snapshot, version } = readPrepareSnapshot(scope.snapshotPath);
12814
+ assertCoordinatorBinding(scope.session, scope.sessionPath, snapshot);
12815
+ const compass = readPrepareCompass(scope.harnessRoot, snapshot);
12816
+ const admission = prepareAdmission(scope.harnessRoot, scope.workflowId, snapshot);
12817
+ return {
12818
+ ok: true,
12819
+ operation: "show-prepare",
12820
+ session: scope.session,
12821
+ session_file: scope.sessionPath,
12822
+ view: {
12823
+ workflowId: scope.workflowId,
12824
+ snapshotVersion: version,
12825
+ compassVersion: compass.version,
12826
+ planIds: snapshot.plans.map((row) => rowPlanIds(row)[0] ?? ""),
12827
+ allowed: admission.ok,
12828
+ blockers: admission.ok ? [] : [`${admission.reason}: ${admission.message}`]
12829
+ }
12830
+ };
12831
+ }
12832
+ async function amendPrepareWorkflow(input) {
12833
+ assertExactKeys(input, ["sessionPath", "cwd", "expectedSnapshotVersion", "expectedCompassVersion", "patch"], "prepare amendment input");
12834
+ const cwd = input.cwd ?? process.cwd();
12835
+ const expectedSnapshotVersion = prepareVersionToken(input.expectedSnapshotVersion, "expectedSnapshotVersion");
12836
+ const expectedCompassVersion = prepareVersionToken(input.expectedCompassVersion, "expectedCompassVersion");
12837
+ const scope = prepareWorkflowScope(input.sessionPath, cwd);
12838
+ const committed = await withStatusWriteLock(scope.snapshotPath, async () => {
12839
+ const { snapshot, version } = readPrepareSnapshot(scope.snapshotPath);
12840
+ assertCoordinatorBinding(scope.session, scope.sessionPath, snapshot);
12841
+ const main = assertCoordinatorResidency(cwd, snapshot);
12842
+ if (prepareVersionDigest(version) !== prepareVersionDigest(expectedSnapshotVersion)) {
12843
+ throw prepareAmendmentRefusal("stale", `snapshot ${scope.snapshotPath} is at ${version}, this call expected ${expectedSnapshotVersion} — re-read \`workflow show-prepare\` and review again`, { path: scope.snapshotPath, expected: expectedSnapshotVersion, actual: version });
12844
+ }
12845
+ const compass = readPrepareCompass(scope.harnessRoot, snapshot);
12846
+ if (prepareVersionDigest(compass.version) !== prepareVersionDigest(expectedCompassVersion)) {
12847
+ throw prepareAmendmentRefusal("stale", `compass ${compass.path} is at ${compass.version}, this call expected ${expectedCompassVersion} — re-read \`workflow show-prepare\` and review again`, { path: compass.path, expected: expectedCompassVersion, actual: compass.version });
12848
+ }
12849
+ const admission = prepareAdmission(scope.harnessRoot, scope.workflowId, snapshot);
12850
+ if (!admission.ok)
12851
+ throw prepareAmendmentRefusal(admission.reason, admission.message, admission.details);
12852
+ const proposal = readPreparePatch(input.patch, { harnessRoot: scope.harnessRoot, snapshot, compass, main });
12853
+ const executionPolicy = { ...snapshot.execution_policy ?? {} };
12854
+ if (proposal.planParallelism !== undefined)
12855
+ executionPolicy.plan_parallelism = proposal.planParallelism;
12856
+ const next = {
12857
+ ...snapshot,
12858
+ updated_at: nowIso(),
12859
+ plans: [...snapshot.plans, ...proposal.rows],
12860
+ ...proposal.integrationWorktreePath !== undefined ? { integration_worktree_path: proposal.integrationWorktreePath } : {},
12861
+ ...proposal.planParallelism !== undefined ? { execution_policy: executionPolicy } : {}
12862
+ };
12863
+ const rechecked = readPrepareCompass(scope.harnessRoot, snapshot);
12864
+ if (prepareVersionDigest(rechecked.version) !== prepareVersionDigest(compass.version)) {
12865
+ throw prepareAmendmentRefusal("stale", `compass ${compass.path} changed while this amendment was being applied (${compass.version} → ${rechecked.version}) — re-read \`workflow show-prepare\` and review again`, { path: compass.path, expected: compass.version, actual: rechecked.version });
12866
+ }
12867
+ await commitSnapshot(scope.harnessRoot, scope.workflowId, scope.snapshotPath, next);
12868
+ const written = readArtifactBytes(scope.snapshotPath);
12869
+ if (written === undefined) {
12870
+ throw new CoordinationError("coordination.store", `snapshot ${scope.snapshotPath} is unreadable after this call committed it`, { path: scope.snapshotPath });
12871
+ }
12872
+ return {
12873
+ planIds: next.plans.map((row) => rowPlanIds(row)[0] ?? ""),
12874
+ compassVersion: compass.version,
12875
+ snapshotVersion: written.version
12876
+ };
12877
+ });
12878
+ return {
12879
+ ok: true,
12880
+ operation: "amend-prepare",
12881
+ session: scope.session,
12882
+ session_file: scope.sessionPath,
12883
+ outcome: "amended",
12884
+ view: {
12885
+ workflowId: scope.workflowId,
12886
+ snapshotVersion: committed.snapshotVersion,
12887
+ compassVersion: committed.compassVersion,
12888
+ planIds: committed.planIds,
12889
+ allowed: true,
12890
+ blockers: []
12891
+ }
12892
+ };
12893
+ }
11765
12894
  export {
11766
12895
  ARCHIVED_STATUS_V1_FILE,
11767
12896
  AUDIT_CATEGORIES,
@@ -11805,12 +12934,15 @@ export {
11805
12934
  SEVERITY_ORDER,
11806
12935
  SHARED_FAMILIES,
11807
12936
  SddScriptError,
12937
+ WORKFLOW_COMPOUND_OUTCOMES,
12938
+ WORKFLOW_DELIVERY_KINDS,
11808
12939
  WORKFLOW_LIFECYCLE_STATUSES,
11809
12940
  WORKFLOW_LIFECYCLE_TYPES,
11810
12941
  WORKFLOW_SNAPSHOT_FILE,
11811
12942
  WORKFLOW_TERMINAL_STATUSES,
11812
12943
  WorkflowSnapshotValidationError,
11813
12944
  _DEFAULT_PROJECT,
12945
+ amendPrepareWorkflow,
11814
12946
  antiRecursionPrecheck,
11815
12947
  appendProjectRegisterEntries,
11816
12948
  applyEnforcement,
@@ -11819,6 +12951,7 @@ export {
11819
12951
  assertBranchAlignment,
11820
12952
  assertControlVsFeaturePath,
11821
12953
  assertDefaultBranchProtected,
12954
+ assertDeliveryRegistrationCoherence,
11822
12955
  assertFsStorePath,
11823
12956
  assertIndexRowObligations,
11824
12957
  assertIndexRows,
@@ -11843,7 +12976,9 @@ export {
11843
12976
  composeDispatchGate,
11844
12977
  compoundRefreshScope,
11845
12978
  computePrTally,
12979
+ consultDeliveryEvidence,
11846
12980
  createFsStore,
12981
+ declareWorkflowDeliveryKind,
11847
12982
  detectHarnessKind,
11848
12983
  detectHost,
11849
12984
  emitGitignoreSnippet,
@@ -11905,7 +13040,9 @@ export {
11905
13040
  readProgressLedger,
11906
13041
  readSessionEnvelope,
11907
13042
  readWorkflowSnapshot,
13043
+ recordWorkflowDelivery,
11908
13044
  referenceExists,
13045
+ registerPlanWorkflow,
11909
13046
  registerWorkflow,
11910
13047
  releaseLease,
11911
13048
  replaceCoordinatedArtifact,
@@ -11939,6 +13076,7 @@ export {
11939
13076
  scopeGuard,
11940
13077
  sddWorkspace,
11941
13078
  setArtifactStore,
13079
+ showPrepareWorkflow,
11942
13080
  singleReviewSnapshot,
11943
13081
  stripFrontmatter,
11944
13082
  supplyChainChecks,