@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/audit.d.ts +15 -0
- package/dist/audit.js +131 -31
- package/dist/coordination-write.d.ts +1 -1
- package/dist/coordination.d.ts +85 -0
- package/dist/dispatch.d.ts +15 -1
- package/dist/engine.js +1269 -131
- package/dist/index.d.ts +4 -4
- package/dist/iteration.d.ts +33 -1
- package/dist/migrate.d.ts +40 -1
- package/dist/sdd.d.ts +9 -1
- package/dist/workflow.d.ts +324 -2
- package/package.json +1 -1
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
|
|
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
|
|
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
|
|
180
|
-
import { basename as basename4, join as join9, resolve as
|
|
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
|
|
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
|
|
188
|
-
import { dirname as dirname5, join as join7, resolve as
|
|
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(
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
2556
|
-
const projectKey = basename4(
|
|
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 =
|
|
2601
|
-
const projectKey = basename4(
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
3177
|
+
const start = resolve9(startDir);
|
|
2756
3178
|
const explicit = opts.harnessDir ?? process.env.MSTAR_HARNESS_DIR;
|
|
2757
3179
|
if (explicit)
|
|
2758
|
-
return
|
|
2759
|
-
const boundary =
|
|
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
|
|
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
|
|
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 =
|
|
3224
|
+
const dir = resolve9(harnessDir);
|
|
2803
3225
|
const rc = loadMstarc(dir, dirname6(dir));
|
|
2804
3226
|
const declared = rc?.config[key];
|
|
2805
|
-
return declared ?
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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(
|
|
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(
|
|
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 ${
|
|
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(
|
|
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 =
|
|
2907
|
-
const boundary =
|
|
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 ?
|
|
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(
|
|
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(
|
|
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 =
|
|
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) :
|
|
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
|
|
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
|
|
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 =
|
|
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 (
|
|
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 =
|
|
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
|
|
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
|
|
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
|
|
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 =
|
|
4789
|
-
const startInside = isInside(
|
|
4790
|
-
let current =
|
|
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 =
|
|
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 =
|
|
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(
|
|
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 ?
|
|
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 ?
|
|
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 ?
|
|
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 ?
|
|
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
|
|
5531
|
+
function classifyWorkflowRegisterState(controlHarnessRoot) {
|
|
5110
5532
|
const statusPath = join13(controlHarnessRoot, "status.json");
|
|
5111
5533
|
if (!isFile2(statusPath))
|
|
5112
|
-
return
|
|
5534
|
+
return { state: "absent" };
|
|
5113
5535
|
let doc;
|
|
5114
5536
|
try {
|
|
5115
5537
|
doc = readJson(statusPath);
|
|
5116
5538
|
} catch {
|
|
5117
|
-
return
|
|
5539
|
+
return { state: "unreadable" };
|
|
5118
5540
|
}
|
|
5119
5541
|
if (!isPlainObject7(doc) || doc.version !== 2 || !Array.isArray(doc.workflows))
|
|
5120
|
-
return
|
|
5121
|
-
const
|
|
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
|
-
|
|
5546
|
+
activeIds.add(entry.id);
|
|
5125
5547
|
}
|
|
5126
|
-
return
|
|
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 =
|
|
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 =
|
|
5783
|
+
const cwdResolved = resolve11(action.cwd);
|
|
5347
5784
|
const checkTarget = (baseDir, target, kind) => {
|
|
5348
5785
|
if (featureReal === null)
|
|
5349
5786
|
return;
|
|
5350
|
-
const targetAbs =
|
|
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 =
|
|
5815
|
+
const targetAbs = resolve11(cwdResolved, action.target);
|
|
5379
5816
|
const canonical = canonicalizeNearestExisting(targetAbs);
|
|
5380
5817
|
if (!isInside(canonical, canonicalSddDir) && canonical !== canonicalPlanFile) {
|
|
5381
|
-
const rawSddDir =
|
|
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
|
|
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 =
|
|
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 =
|
|
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 {
|
|
5642
|
-
import { basename as basename9, dirname as dirname11, isAbsolute as isAbsolute9, join as join15, relative as relative5, resolve as
|
|
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 =
|
|
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 =
|
|
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 =
|
|
6115
|
-
const workflowRoot =
|
|
6116
|
-
const projectRoot =
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
6613
|
-
import { basename as basename10, join as join16, resolve as
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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:
|
|
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
|
-
|
|
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 =
|
|
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 (
|
|
7303
|
-
|
|
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 =
|
|
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 ${
|
|
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 =
|
|
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
|
|
7377
|
-
import { basename as basename11, isAbsolute as isAbsolute10, join as join17, relative as relative6, resolve as
|
|
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 =
|
|
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(
|
|
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 =
|
|
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 =
|
|
8274
|
+
const resolved = resolve15(path);
|
|
7768
8275
|
for (const root of allowedRoots) {
|
|
7769
|
-
const r =
|
|
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
|
|
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 =
|
|
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
|
|
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 ${
|
|
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
|
|
9483
|
-
const start =
|
|
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 =
|
|
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 ${
|
|
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 =
|
|
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 — ${
|
|
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 ${
|
|
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, {
|
|
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(
|
|
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,
|