@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 CHANGED
@@ -1,4 +1,5 @@
1
1
  import type { GateResult } from "./core.js";
2
+ import { type WorkflowDeliveryKind } from "./workflow.js";
2
3
  /** Priority values (mstar-audit SKILL.md § Plan output (all variants) Status block). */
3
4
  export declare const AUDIT_PRIORITIES: readonly ["P1", "P2", "P3"];
4
5
  export type AuditPriority = (typeof AUDIT_PRIORITIES)[number];
@@ -226,6 +227,20 @@ export type PromoteAuditPlansOptions = {
226
227
  harnessDir: string;
227
228
  /** Default: basename of `outDir` (e.g. `audit-2026-08-22`). */
228
229
  workflowId?: string;
230
+ /**
231
+ * Delivery kind declared for the promoted plans (contract §1/§4a). Required —
232
+ * the promoted lifecycle is a `type: plan` workflow, so its delivery kind is
233
+ * declared at registration, never inferred and never defaulted in code (an
234
+ * audit promotion that left it unset minted an active snapshot no close path
235
+ * could complete).
236
+ */
237
+ deliveryKind: WorkflowDeliveryKind;
238
+ /** Delivery source branch, recorded as `branch.source`. Required for `development`. */
239
+ branchSource?: string;
240
+ /** Delivery target branch, recorded as `branch.target`. Required for `development`. */
241
+ branchTarget?: string;
242
+ /** Completion policy for `verification/report-only` promotions. Required for that kind. */
243
+ completionPolicy?: string;
229
244
  };
230
245
  /**
231
246
  * Promote selected audit plans into the v2 workflow lifecycle as a
package/dist/audit.js CHANGED
@@ -1,7 +1,7 @@
1
1
  // src/audit.ts
2
2
  import { execFileSync as execFileSync2 } from "node:child_process";
3
- import { existsSync as existsSync7, mkdirSync as mkdirSync5, readdirSync as readdirSync4, readFileSync as readFileSync7, rmdirSync as rmdirSync2, writeFileSync as writeFileSync4 } from "node:fs";
4
- import { basename as basename5, join as join9, resolve as resolve8, sep as sep3 } from "node:path";
3
+ import { existsSync as existsSync7, mkdirSync as mkdirSync5, readdirSync as readdirSync5, readFileSync as readFileSync7, rmdirSync as rmdirSync3, writeFileSync as writeFileSync4 } from "node:fs";
4
+ import { basename as basename5, join as join9, resolve as resolve9, sep as sep3 } from "node:path";
5
5
 
6
6
  // src/lease.ts
7
7
  import { mkdirSync, rmdirSync, statSync, unlinkSync, writeFileSync } from "node:fs";
@@ -500,9 +500,9 @@ function isArtifactVersion(value) {
500
500
  }
501
501
 
502
502
  // src/path.ts
503
- import { existsSync as existsSync6, mkdirSync as mkdirSync4, readdirSync as readdirSync3, readFileSync as readFileSync6, realpathSync as realpathSync3, statSync as statSync3, writeFileSync as writeFileSync3 } from "node:fs";
503
+ import { existsSync as existsSync6, mkdirSync as mkdirSync4, readdirSync as readdirSync4, readFileSync as readFileSync6, realpathSync as realpathSync3, statSync as statSync3, writeFileSync as writeFileSync3 } from "node:fs";
504
504
  import { execFileSync } from "node:child_process";
505
- import { basename as basename4, dirname as dirname6, isAbsolute as isAbsolute6, join as join8, relative as relative2, resolve as resolve7 } from "node:path";
505
+ import { basename as basename4, dirname as dirname6, isAbsolute as isAbsolute6, join as join8, relative as relative2, resolve as resolve8 } from "node:path";
506
506
 
507
507
  // src/mstarc.ts
508
508
  import { readFileSync as readFileSync2, statSync as statSync2 } from "node:fs";
@@ -626,8 +626,8 @@ function writeJson(filePath, value) {
626
626
  }
627
627
 
628
628
  // src/status.ts
629
- import { existsSync as existsSync5, readFileSync as readFileSync5, readdirSync as readdirSync2, realpathSync as realpathSync2 } from "node:fs";
630
- import { dirname as dirname5, join as join7, resolve as resolve6, sep as sep2 } from "node:path";
629
+ import { existsSync as existsSync5, readFileSync as readFileSync5, readdirSync as readdirSync3, realpathSync as realpathSync2 } from "node:fs";
630
+ import { dirname as dirname5, join as join7, resolve as resolve7, sep as sep2 } from "node:path";
631
631
 
632
632
  // src/store.ts
633
633
  import { existsSync as existsSync3, readdirSync, unlinkSync as unlinkSync3 } from "node:fs";
@@ -789,12 +789,14 @@ function assertFsStorePath(store, ref, expectedPath) {
789
789
  }
790
790
 
791
791
  // src/workflow.ts
792
- import { existsSync as existsSync4, mkdirSync as mkdirSync3, readFileSync as readFileSync4 } from "node:fs";
793
- import { isAbsolute as isAbsolute5, join as join6 } from "node:path";
792
+ import { existsSync as existsSync4, mkdirSync as mkdirSync3, readFileSync as readFileSync4, readdirSync as readdirSync2, rmdirSync as rmdirSync2 } from "node:fs";
793
+ import { isAbsolute as isAbsolute5, join as join6, resolve as resolve6 } from "node:path";
794
794
  var WORKFLOW_SNAPSHOT_FILE = "snapshot.json";
795
795
  var WORKFLOW_LIFECYCLE_STATUSES = ["running", "paused", "completed", "failed", "stopped"];
796
796
  var WORKFLOW_TERMINAL_STATUSES = ["completed", "failed", "stopped"];
797
797
  var WORKFLOW_LIFECYCLE_TYPES = ["plan", "iteration"];
798
+ var WORKFLOW_DELIVERY_KINDS = ["development", "verification/report-only"];
799
+ var WORKFLOW_COMPOUND_OUTCOMES = ["created", "updated", "skipped"];
798
800
  function stableJson(value) {
799
801
  if (Array.isArray(value))
800
802
  return `[${value.map(stableJson).join(",")}]`;
@@ -819,6 +821,61 @@ function validateWorktreePathValue(violations, value, field) {
819
821
  violations.push(violation2("high", "workflow.snapshot.invalid-integration-worktree-path", `${field} must be a non-empty absolute path — got ${JSON.stringify(value)}`, "record the absolute integration checkout path (integration_worktree_path)"));
820
822
  }
821
823
  }
824
+ function deliveryEvidenceViolations(value, what) {
825
+ const violations = [];
826
+ const invalid2 = (message) => {
827
+ violations.push(violation2("medium", "workflow.snapshot.invalid-delivery-evidence", `${what}: ${message}`));
828
+ };
829
+ if (!isPlainObject2(value)) {
830
+ invalid2("must be an object");
831
+ return violations;
832
+ }
833
+ const members = ["compound", "pr", "merge", "completion"];
834
+ const unknownMembers = Object.keys(value).filter((key) => !members.includes(key));
835
+ if (unknownMembers.length > 0)
836
+ invalid2(`unknown member(s) ${unknownMembers.join(", ")} — expected ${members.join(" | ")}`);
837
+ const compound = value.compound;
838
+ if (compound !== undefined) {
839
+ if (!isPlainObject2(compound))
840
+ invalid2("compound must be an object");
841
+ else {
842
+ const unknown = Object.keys(compound).filter((key) => key !== "outcome" && key !== "reason");
843
+ if (unknown.length > 0)
844
+ invalid2(`compound has unknown key(s) ${unknown.join(", ")}`);
845
+ if (typeof compound.outcome !== "string" || !WORKFLOW_COMPOUND_OUTCOMES.includes(compound.outcome)) {
846
+ invalid2(`compound.outcome must be one of ${WORKFLOW_COMPOUND_OUTCOMES.join(" | ")} — got ${JSON.stringify(compound.outcome)}`);
847
+ } else if (compound.outcome === "skipped" && (typeof compound.reason !== "string" || compound.reason.trim() === "")) {
848
+ invalid2("compound reason is required when the disposition outcome is 'skipped' (contract §4c)");
849
+ } else if (compound.reason !== undefined && (typeof compound.reason !== "string" || compound.reason.trim() === "")) {
850
+ invalid2("compound.reason must be a non-empty string when given");
851
+ }
852
+ }
853
+ }
854
+ const stringMembers = {
855
+ pr: ["repo", "head", "target"],
856
+ merge: ["provider", "evidence"],
857
+ completion: ["policy", "evidence"]
858
+ };
859
+ for (const member of ["pr", "merge", "completion"]) {
860
+ const block = value[member];
861
+ if (block === undefined)
862
+ continue;
863
+ if (!isPlainObject2(block)) {
864
+ invalid2(`${member} must be an object`);
865
+ continue;
866
+ }
867
+ const fields = stringMembers[member];
868
+ const unknown = Object.keys(block).filter((key) => !fields.includes(key));
869
+ if (unknown.length > 0)
870
+ invalid2(`${member} has unknown key(s) ${unknown.join(", ")}`);
871
+ for (const field of fields) {
872
+ if (typeof block[field] !== "string" || block[field].trim() === "") {
873
+ invalid2(`${member}.${field} must be a non-empty string`);
874
+ }
875
+ }
876
+ }
877
+ return violations;
878
+ }
822
879
  function validateWorkflowSnapshot(doc) {
823
880
  const violations = [];
824
881
  if (!isPlainObject2(doc)) {
@@ -884,7 +941,7 @@ function validateWorkflowSnapshot(doc) {
884
941
  if (!isPlainObject2(doc.branch)) {
885
942
  violations.push(violation2("medium", "workflow.snapshot.invalid-branch", "branch must be an object"));
886
943
  } else {
887
- for (const key of ["base", "integration", "target"]) {
944
+ for (const key of ["base", "source", "integration", "target"]) {
888
945
  if (doc.branch[key] !== undefined && (typeof doc.branch[key] !== "string" || doc.branch[key].trim() === "")) {
889
946
  violations.push(violation2("medium", "workflow.snapshot.invalid-branch", `branch.${key} must be a non-empty string`));
890
947
  }
@@ -910,6 +967,20 @@ function validateWorkflowSnapshot(doc) {
910
967
  if (doc.compass_ref !== undefined) {
911
968
  validateNonEmptyString2(violations, doc.compass_ref, "compass_ref", "workflow.snapshot.missing-compass-ref", "workflow.snapshot.invalid-compass-ref");
912
969
  }
970
+ if (doc.delivery_kind !== undefined) {
971
+ if (typeof doc.delivery_kind !== "string" || !WORKFLOW_DELIVERY_KINDS.includes(doc.delivery_kind)) {
972
+ violations.push(violation2("medium", "workflow.snapshot.invalid-delivery-kind", `delivery_kind must be one of ${WORKFLOW_DELIVERY_KINDS.join(" | ")} — got ${JSON.stringify(doc.delivery_kind)}`));
973
+ }
974
+ }
975
+ if (doc.project !== undefined) {
976
+ validateNonEmptyString2(violations, doc.project, "project", "workflow.snapshot.missing-project", "workflow.snapshot.invalid-project");
977
+ }
978
+ if (doc.completion_policy !== undefined) {
979
+ validateNonEmptyString2(violations, doc.completion_policy, "completion_policy", "workflow.snapshot.missing-completion-policy", "workflow.snapshot.invalid-completion-policy");
980
+ }
981
+ if (doc.delivery !== undefined) {
982
+ violations.push(...deliveryEvidenceViolations(doc.delivery, "delivery"));
983
+ }
913
984
  const terminal = typeof doc.status === "string" && WORKFLOW_TERMINAL_STATUSES.includes(doc.status);
914
985
  if (terminal) {
915
986
  if (doc.ended_at === undefined) {
@@ -986,6 +1057,22 @@ function mergePhaseProjection(stored, incoming) {
986
1057
  }
987
1058
  return next;
988
1059
  }
1060
+ function nonEmptyString(value) {
1061
+ return typeof value === "string" && value.trim() !== "";
1062
+ }
1063
+ function assertDeliveryRegistrationCoherence(kind, evidence, what) {
1064
+ if (kind === "development" && (!nonEmptyString(evidence.branchSource) || !nonEmptyString(evidence.branchTarget))) {
1065
+ throw new Error(`${what}: a development workflow requires its delivery source and target branches (--branch-source/--branch-target) — missing branch fields are incomplete registration, not an exempt workflow (contract §1/§4a)`);
1066
+ }
1067
+ if (kind === "verification/report-only" && !nonEmptyString(evidence.completionPolicy)) {
1068
+ throw new Error(`${what}: a verification/report-only workflow requires the completion policy (--completion-policy) naming the evidence that completes it (contract §1)`);
1069
+ }
1070
+ for (const [field, value] of Object.entries({ branchSource: evidence.branchSource, branchTarget: evidence.branchTarget, completionPolicy: evidence.completionPolicy })) {
1071
+ if (value !== undefined && !nonEmptyString(value)) {
1072
+ throw new Error(`${what}: ${field} must be a non-empty string when given`);
1073
+ }
1074
+ }
1075
+ }
989
1076
 
990
1077
  // src/status.ts
991
1078
  var DATE_RE = /^\d{4}-\d{2}-\d{2}$/;
@@ -1080,7 +1167,7 @@ function validateStatusV2(docOrPath, opts = {}) {
1080
1167
  if (typeof docOrPath === "string") {
1081
1168
  try {
1082
1169
  doc = readJson(docOrPath);
1083
- harnessDir = dirname5(resolve6(docOrPath));
1170
+ harnessDir = dirname5(resolve7(docOrPath));
1084
1171
  } catch (error) {
1085
1172
  return {
1086
1173
  ok: false,
@@ -1239,14 +1326,14 @@ function readRegisteredSnapshot(snapshotPath) {
1239
1326
 
1240
1327
  // src/path.ts
1241
1328
  function resolveHarnessDir(startDir = process.cwd(), opts = {}) {
1242
- const start = resolve7(startDir);
1329
+ const start = resolve8(startDir);
1243
1330
  const explicit = opts.harnessDir ?? process.env.MSTAR_HARNESS_DIR;
1244
1331
  if (explicit)
1245
- return resolve7(start, explicit);
1246
- const boundary = resolve7(start, opts.workspaceRoot ?? defaultWorkspaceRoot(start));
1332
+ return resolve8(start, explicit);
1333
+ const boundary = resolve8(start, opts.workspaceRoot ?? defaultWorkspaceRoot(start));
1247
1334
  const rc = loadMstarc(start, boundary);
1248
1335
  if (rc !== null && rc.config.harnessDir)
1249
- return resolve7(rc.dir, rc.config.harnessDir);
1336
+ return resolve8(rc.dir, rc.config.harnessDir);
1250
1337
  let dir = start;
1251
1338
  for (;; ) {
1252
1339
  if (!isAtOrBelow2(dir, boundary))
@@ -1277,7 +1364,7 @@ function defaultWorkspaceRoot(startDir) {
1277
1364
  if (segment && segment !== ".")
1278
1365
  boundary = dirname6(boundary);
1279
1366
  }
1280
- return resolve7(boundary);
1367
+ return resolve8(boundary);
1281
1368
  } catch {}
1282
1369
  return startDir;
1283
1370
  }
@@ -1286,10 +1373,10 @@ function isAtOrBelow2(dir, root) {
1286
1373
  return rel === "" || !rel.startsWith("..") && !isAbsolute6(rel);
1287
1374
  }
1288
1375
  function mstarcDirOverride(harnessDir, key) {
1289
- const dir = resolve7(harnessDir);
1376
+ const dir = resolve8(harnessDir);
1290
1377
  const rc = loadMstarc(dir, dirname6(dir));
1291
1378
  const declared = rc?.config[key];
1292
- return declared ? resolve7(rc.dir, declared) : null;
1379
+ return declared ? resolve8(rc.dir, declared) : null;
1293
1380
  }
1294
1381
  function assertSafePathComponent(value, what) {
1295
1382
  if (value === "" || value === "." || value === ".." || !/^[A-Za-z0-9._-]+$/.test(value)) {
@@ -1299,10 +1386,10 @@ function assertSafePathComponent(value, what) {
1299
1386
  function resolveHarnessSubdir(startDir, opts, key, fallback) {
1300
1387
  const harness = resolveHarnessDir(startDir, opts);
1301
1388
  if (harness === null) {
1302
- throw new Error(`harness dir not found from ${resolve7(startDir)} — cannot resolve the ${fallback} dir (run \`mstar harness scaffold\`, pass opts.harnessDir, or set MSTAR_HARNESS_DIR)`);
1389
+ throw new Error(`harness dir not found from ${resolve8(startDir)} — cannot resolve the ${fallback} dir (run \`mstar harness scaffold\`, pass opts.harnessDir, or set MSTAR_HARNESS_DIR)`);
1303
1390
  }
1304
1391
  const declared = mstarcDirOverride(harness, key);
1305
- return declared !== null ? declared : join8(resolve7(harness), fallback);
1392
+ return declared !== null ? declared : join8(resolve8(harness), fallback);
1306
1393
  }
1307
1394
  function resolveWorkflowDir(startDir = process.cwd(), opts = {}) {
1308
1395
  return resolveHarnessSubdir(startDir, opts, "workflowDir", "workflows");
@@ -1696,7 +1783,7 @@ var LOCKFILE_NAMES = [
1696
1783
  function rootLockfiles(root) {
1697
1784
  let entries;
1698
1785
  try {
1699
- entries = readdirSync4(root, { withFileTypes: true });
1786
+ entries = readdirSync5(root, { withFileTypes: true });
1700
1787
  } catch {
1701
1788
  return [];
1702
1789
  }
@@ -1729,7 +1816,7 @@ function supplyChainChecks(repoRoot) {
1729
1816
  const workflowsDir = join9(repoRoot, ".github", "workflows");
1730
1817
  let wfEntries = [];
1731
1818
  try {
1732
- wfEntries = readdirSync4(workflowsDir, { withFileTypes: true });
1819
+ wfEntries = readdirSync5(workflowsDir, { withFileTypes: true });
1733
1820
  } catch {
1734
1821
  wfEntries = [];
1735
1822
  }
@@ -1895,7 +1982,7 @@ function scaffoldAuditPlan(outDir, findings, options = {}) {
1895
1982
  mkdirSync5(outDir, { recursive: true });
1896
1983
  const existingReadme = join9(outDir, "README.md");
1897
1984
  const carried = existsSync7(existingReadme) ? extractSecurityDispositionSections(readFileSync7(existingReadme, "utf8")) : { needsVerification: [], hardeningChecked: [] };
1898
- const existing = readdirSync4(outDir).filter((f) => /^\d{3}-.*\.md$/.test(f));
1985
+ const existing = readdirSync5(outDir).filter((f) => /^\d{3}-.*\.md$/.test(f));
1899
1986
  let next = existing.reduce((max, f) => Math.max(max, Number(f.slice(0, 3))), 0) + 1;
1900
1987
  const redactedFindings = findings.map(redactFinding);
1901
1988
  const written = [];
@@ -1960,7 +2047,7 @@ function scaffoldAuditPlan(outDir, findings, options = {}) {
1960
2047
  needsVerification: needsVerificationLines,
1961
2048
  hardeningChecked: hardeningCheckedLines
1962
2049
  }));
1963
- return { outDir: resolve8(outDir), date, files: written, nextNumber: next };
2050
+ return { outDir: resolve9(outDir), date, files: written, nextNumber: next };
1964
2051
  }
1965
2052
  async function promoteAuditPlans(outDir, selected, options) {
1966
2053
  if (selected.length === 0) {
@@ -1969,9 +2056,13 @@ async function promoteAuditPlans(outDir, selected, options) {
1969
2056
  if (typeof options.harnessDir !== "string" || options.harnessDir.trim() === "") {
1970
2057
  throw new Error("promoteAuditPlans: options.harnessDir is required (must contain status.json + workflows/)");
1971
2058
  }
1972
- const workflowId = options.workflowId ?? basename5(resolve8(outDir));
2059
+ if (typeof options.deliveryKind !== "string" || !WORKFLOW_DELIVERY_KINDS.includes(options.deliveryKind)) {
2060
+ throw new Error(`promoteAuditPlans: options.deliveryKind must be one of ${WORKFLOW_DELIVERY_KINDS.join(" | ")} — got ${JSON.stringify(options.deliveryKind)} (a promoted plan workflow declares its delivery kind at registration; it is never inferred and never defaulted)`);
2061
+ }
2062
+ assertDeliveryRegistrationCoherence(options.deliveryKind, options, "promoteAuditPlans");
2063
+ const workflowId = options.workflowId ?? basename5(resolve9(outDir));
1973
2064
  assertSafePathComponent(workflowId, "workflow id");
1974
- const harnessDir = resolve8(options.harnessDir);
2065
+ const harnessDir = resolve9(options.harnessDir);
1975
2066
  const statusPath = join9(harnessDir, "status.json");
1976
2067
  const workflowDir = join9(harnessDir, "workflows", workflowId);
1977
2068
  const snapshotPath = join9(workflowDir, WORKFLOW_SNAPSHOT_FILE);
@@ -1998,8 +2089,17 @@ async function promoteAuditPlans(outDir, selected, options) {
1998
2089
  status: "running",
1999
2090
  started_at: now.toISOString(),
2000
2091
  updated_at: now.toISOString().slice(0, 10),
2001
- plans
2092
+ plans,
2093
+ delivery_kind: options.deliveryKind
2002
2094
  };
2095
+ if (options.branchSource !== undefined || options.branchTarget !== undefined) {
2096
+ snapshot.branch = {
2097
+ ...options.branchSource !== undefined ? { source: options.branchSource } : {},
2098
+ ...options.branchTarget !== undefined ? { target: options.branchTarget } : {}
2099
+ };
2100
+ }
2101
+ if (options.completionPolicy !== undefined)
2102
+ snapshot.completion_policy = options.completionPolicy;
2003
2103
  const entry = {
2004
2104
  id: workflowId,
2005
2105
  type: "plan",
@@ -2032,8 +2132,8 @@ async function promoteAuditPlans(outDir, selected, options) {
2032
2132
  });
2033
2133
  }
2034
2134
  try {
2035
- if (readdirSync4(workflowDir).length === 0) {
2036
- rmdirSync2(workflowDir);
2135
+ if (readdirSync5(workflowDir).length === 0) {
2136
+ rmdirSync3(workflowDir);
2037
2137
  }
2038
2138
  } catch {}
2039
2139
  throw error;
@@ -2043,7 +2143,7 @@ async function promoteAuditPlans(outDir, selected, options) {
2043
2143
  return { workflowId, snapshotPath };
2044
2144
  }
2045
2145
  function resolveSelectedPlanFiles(outDir, selected) {
2046
- const files = readdirSync4(outDir).filter((f) => /^\d{3}-.*\.md$/.test(f)).sort();
2146
+ const files = readdirSync5(outDir).filter((f) => /^\d{3}-.*\.md$/.test(f)).sort();
2047
2147
  const byNum = new Map;
2048
2148
  const byStem = new Map;
2049
2149
  for (const file of files) {
@@ -2058,7 +2158,7 @@ function resolveSelectedPlanFiles(outDir, selected) {
2058
2158
  for (const id of selected) {
2059
2159
  const file = byNum.get(id) ?? byStem.get(id) ?? byStem.get(id.replace(/\.md$/, ""));
2060
2160
  if (file === undefined) {
2061
- throw new Error(`promoteAuditPlans: selected plan ${JSON.stringify(id)} does not match any NNN-*.md file in ${resolve8(outDir)}`);
2161
+ throw new Error(`promoteAuditPlans: selected plan ${JSON.stringify(id)} does not match any NNN-*.md file in ${resolve9(outDir)}`);
2062
2162
  }
2063
2163
  if (!seen.has(file)) {
2064
2164
  seen.add(file);
@@ -2097,7 +2197,7 @@ function readExecutionOrderIndex(outDir) {
2097
2197
  return rows;
2098
2198
  }
2099
2199
  function planFileRel(outDir, planFile) {
2100
- const resolved = resolve8(outDir);
2200
+ const resolved = resolve9(outDir);
2101
2201
  const parts = resolved.split(sep3);
2102
2202
  const plansIdx = parts.lastIndexOf("plans");
2103
2203
  if (plansIdx >= 0) {
@@ -1,6 +1,6 @@
1
1
  import type { ValidationResult } from "./core.js";
2
2
  /** Stable refusal codes of the scoped coordination surface (spec §C4). */
3
- export declare const COORDINATION_ERROR_CODES: readonly ["coordination.harness-not-found", "coordination.workflow-not-found", "coordination.plan-not-found", "coordination.scope-mismatch", "coordination.path-mismatch", "coordination.assignment-invalid", "coordination.assignment-stale", "coordination.not-prepared", "coordination.duplicate-holder", "coordination.session-mismatch", "coordination.session-not-found", "coordination.session-role", "coordination.version-conflict", "coordination.expected-version-required", "coordination.invalid-transition", "coordination.invalid-input", "coordination.forbidden-field", "coordination.not-in-git", "coordination.git-unavailable", "coordination.git-proof", "coordination.evidence-stale", "coordination.integration-unresolved", "coordination.integration-diverged", "coordination.local-store-required", "coordination.direct-write-refused", "coordination.scoped-writer-required", "coordination.unknown-operation", "coordination.not-implemented", "coordination.lock", "coordination.store"];
3
+ export declare const COORDINATION_ERROR_CODES: readonly ["coordination.harness-not-found", "coordination.workflow-not-found", "coordination.plan-not-found", "coordination.scope-mismatch", "coordination.path-mismatch", "coordination.assignment-invalid", "coordination.assignment-stale", "coordination.not-prepared", "coordination.duplicate-holder", "coordination.session-mismatch", "coordination.session-not-found", "coordination.session-role", "coordination.version-conflict", "coordination.expected-version-required", "coordination.invalid-transition", "coordination.invalid-input", "coordination.forbidden-field", "coordination.not-in-git", "coordination.git-unavailable", "coordination.git-proof", "coordination.evidence-stale", "coordination.integration-unresolved", "coordination.integration-diverged", "coordination.local-store-required", "coordination.direct-write-refused", "coordination.scoped-writer-required", "coordination.unknown-operation", "coordination.store", "coordination.prepare-amendment.stale", "coordination.prepare-amendment.invalid-patch", "coordination.prepare-amendment.not-prepare", "coordination.prepare-amendment.execution-started", "coordination.prepare-amendment.duplicate-plan", "coordination.prepare-amendment.invalid-plan", "coordination.prepare-amendment.compass-mismatch", "coordination.prepare-amendment.invalid-worktree"];
4
4
  export type CoordinationErrorCode = (typeof COORDINATION_ERROR_CODES)[number];
5
5
  /**
6
6
  * Stable exception of the coordination surface: `code` is the consumer
@@ -282,3 +282,88 @@ export declare function mutatePlanCoordination(request: CoordinationRequest): Pr
282
282
  * they refuse explicitly instead of silently no-opping.
283
283
  */
284
284
  export declare function replaceCoordinatedArtifact(input: CoordinatedReplacement): Promise<VersionedArtifact>;
285
+ /**
286
+ * The guarded, coordinator-authenticated Prepare-stage amendment (§ New API and
287
+ * CLI, frozen). One approved plan row: the plan markdown that carries it plus
288
+ * the metadata the row records. Nothing else is addressable — the verb cannot
289
+ * edit an existing row.
290
+ */
291
+ export type PreparePlanAppend = Readonly<{
292
+ id: string;
293
+ title: string;
294
+ file: string;
295
+ metadata: Readonly<{
296
+ primary_spec: string;
297
+ spec_refs: readonly string[];
298
+ iteration_compass: string;
299
+ iteration_refs: readonly string[];
300
+ working_branch: string;
301
+ spec_integration_branch: string;
302
+ merge_target: string;
303
+ }>;
304
+ }>;
305
+ /**
306
+ * The whole structural delta one amendment may apply (§ Admission and mutation
307
+ * step 5): the caller's main-worktree branch, the approved plan appends, the
308
+ * reviewed integration checkout, and the single approved execution-policy key.
309
+ */
310
+ export type PrepareWorkflowPatch = Readonly<{
311
+ mainWorktreeBranch: string;
312
+ appendPlans: readonly PreparePlanAppend[];
313
+ integrationWorktreePath?: string;
314
+ planParallelism?: "serial" | "parallel";
315
+ }>;
316
+ /** What a coordinator observes about one workflow before amending it. */
317
+ export type PrepareWorkflowView = Readonly<{
318
+ workflowId: string;
319
+ /** `sha256:<64 hex>` of the snapshot bytes (the amendment CAS token). */
320
+ snapshotVersion: string;
321
+ /** `sha256:<64 hex>` of the reviewed compass Markdown bytes (CAS token). */
322
+ compassVersion: string;
323
+ /** Plan ids in row order — including this call's own appends on success. */
324
+ planIds: readonly string[];
325
+ /** `true` when `amendPrepareWorkflow` is admitted with fresh tokens. */
326
+ allowed: boolean;
327
+ /** One `<reason>: <message>` line per admission blocker; empty when allowed. */
328
+ blockers: readonly string[];
329
+ }>;
330
+ /** Success shape of the verb: the existing envelope with a workflow view. */
331
+ export type PrepareWorkflowResult = Omit<CoordinationResult, "view"> & {
332
+ view: PrepareWorkflowView;
333
+ };
334
+ /**
335
+ * Read the workflow-level Prepare view of one coordinator-bound workflow
336
+ * (spec § New API and CLI). Read-only: no snapshot lock, nothing written. The
337
+ * two byte versions are the tokens `amendPrepareWorkflow` requires, and
338
+ * `allowed`/`blockers` report the Prepare/no-execution admission exactly as the
339
+ * mutation evaluates it — an inadmissible *lifecycle state* is readable, not an
340
+ * error, so a caller can inspect a workflow before deciding to amend it.
341
+ * Problems that make the documents or the caller's identity unusable — a
342
+ * missing/foreign/mismatched envelope, an unregistered or unreadable snapshot,
343
+ * an unreadable or borrowed compass — still refuse with their own code, because
344
+ * no trustworthy answer can be produced from them.
345
+ */
346
+ export declare function showPrepareWorkflow(input: Readonly<{
347
+ sessionPath: string;
348
+ cwd?: string;
349
+ }>): Promise<PrepareWorkflowResult>;
350
+ /**
351
+ * Apply one approved Prepare structural amendment (spec § Admission and
352
+ * mutation). The CAS read, both version comparisons, the admission, the whole
353
+ * patch validation, the compass recheck and the single atomic write all run
354
+ * under the canonical snapshot write lock, so two callers presenting the same
355
+ * tokens cannot both succeed: the loser inspects the winner's bytes and
356
+ * refuses as `stale`. Every refusal happens before any write — the protected
357
+ * snapshot, root register, other workflows and the compass stay byte-identical.
358
+ *
359
+ * A lock that cannot be acquired refuses explicitly (the shared
360
+ * `withStatusWriteLock` Blocked error); Git-unavailable probes refuse through
361
+ * the existing `coordination.git-unavailable`.
362
+ */
363
+ export declare function amendPrepareWorkflow(input: Readonly<{
364
+ sessionPath: string;
365
+ cwd?: string;
366
+ expectedSnapshotVersion: string;
367
+ expectedCompassVersion: string;
368
+ patch: PrepareWorkflowPatch;
369
+ }>): Promise<PrepareWorkflowResult>;
@@ -19,6 +19,17 @@ export type AssignmentFields = {
19
19
  * violation code per field so the PM sees exactly which label is missing.
20
20
  */
21
21
  returnShape?: string;
22
+ /**
23
+ * `Task budget (implement / ops rounds)` — the declared capacity of ONE
24
+ * implementer round closing the task's declared Files list and verification
25
+ * gates (capacity contract spec § A1/A2; the value cites the task's
26
+ * `Effort (agent-oriented)` band). Required, presence-only, on the exact
27
+ * complement of the review/audit rounds — implement/ops rounds including
28
+ * non-audit docs/Prepare specialists and orientation roles. The value is
29
+ * prose; it is never parsed numerically (adequacy stays in PM's Prepare
30
+ * check).
31
+ */
32
+ taskBudget?: string;
22
33
  };
23
34
  export type ValidateAssignmentFieldsOptions = {
24
35
  /**
@@ -161,7 +172,10 @@ export declare function isReadOnlyAssignmentRole(roleId: string): boolean;
161
172
  * non-empty values (paste-only shells are caught here — every field missing).
162
173
  * Review-seat and audit rounds must additionally declare both round-bounding
163
174
  * fields, `Budget (review / QC seats)` and `Return shape (review / QC
164
- * seats)` (one violation code per missing label). Writable assignments must
175
+ * seats)` (one violation code per missing label). The complement — every
176
+ * implement/ops round, including non-audit docs/Prepare and orientation
177
+ * roles — must declare `Task budget (implement / ops rounds)`, presence-only.
178
+ * Writable assignments must
165
179
  * carry EXACTLY ONE branch form; `create <new>`
166
180
  * from <base>` without `<base>` (incl. the dangling `create <new> from`
167
181
  * / `create from <base>` typos) and `Branch policy` without branch/reason