@mstar-harness/engine 3.10.3 → 3.11.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
@@ -167,11 +167,47 @@ export type AuditFinding = {
167
167
  effort: AuditEffort;
168
168
  risk: AuditRisk;
169
169
  confidence: AuditConfidence;
170
- evidence: readonly string[];
170
+ /** Free-text string (legacy form, rendered byte-for-byte) or a structured
171
+ * location. Authored JSON should prefer the structured form. */
172
+ evidence: readonly (string | AuditEvidence)[];
171
173
  priority: AuditPriority;
172
174
  fixSketch?: string;
173
175
  verification?: string;
174
176
  dependsOn?: string;
177
+ /** Source-derived root-cause identity (audit-finding-contract.md §4).
178
+ * Optional; never invented by the engine. */
179
+ fingerprint?: string;
180
+ /** Entry-point → propagation → sink path (§5). Supplements evidence. */
181
+ trace?: readonly AuditTraceStep[];
182
+ /** Author-supplied severity assessment (§6). Never inferred from
183
+ * priority/risk/confidence. */
184
+ severity?: AuditSeverity;
185
+ };
186
+ /** One structured evidence location. `file` is a safe repository-relative
187
+ * POSIX path; `line` is omitted when unknown (never invented). */
188
+ export type AuditEvidence = {
189
+ file: string;
190
+ line?: number;
191
+ description: string;
192
+ };
193
+ /** Trace-step kind (§5): where data enters, how it travels, where it lands. */
194
+ export type AuditTraceKind = "entrypoint" | "propagation" | "sink";
195
+ /** One trace step. `line` is a positive safe integer; `scope` names the
196
+ * enclosing route/function/module. */
197
+ export type AuditTraceStep = {
198
+ kind: AuditTraceKind;
199
+ file: string;
200
+ line: number;
201
+ scope: string;
202
+ description: string;
203
+ };
204
+ /** Severity rank ordinal labels (§6). Harness diagnostic `Severity` is a
205
+ * DIFFERENT enum (`nit`, no `informational`) — never conflate them. */
206
+ export type AuditSeverityRank = "informational" | "low" | "medium" | "high" | "critical";
207
+ export type AuditSeverity = {
208
+ likelihood: AuditSeverityRank;
209
+ impact: AuditSeverityRank;
210
+ overall: AuditSeverityRank;
175
211
  };
176
212
  /** Options for `scaffoldAuditPlan`. `plannedAt` defaults to the
177
213
  * `repoShortSha` + `date`; `date` defaults to today (YYYY-MM-DD). */
@@ -205,6 +241,21 @@ export type ScaffoldAuditPlanResult = {
205
241
  files: string[];
206
242
  nextNumber: number;
207
243
  };
244
+ /**
245
+ * Deterministic finding gates (audit-finding-contract.md §§2–6): fingerprint
246
+ * grammar / exact uniqueness / ordinal ordering of the supplied subsequence,
247
+ * `severity.overall ≤ severity.impact`, nonempty trace topology, positive
248
+ * safe-integer trace lines, safe typed paths, visible well-formed text, and
249
+ * opaque-field credential rejection (a fingerprint or typed location that
250
+ * `redactSecrets` would alter is REJECTED, never redacted into a different
251
+ * identity). Contextual judgement (reportability, semantic exclusion,
252
+ * coverage) stays in skill prose (§2).
253
+ *
254
+ * Violation codes are stable `audit.finding.<family>.<rule>`; messages carry
255
+ * `findings[index].field` paths only — never raw submitted values. Pure:
256
+ * the input is never mutated and nothing is sorted or repaired.
257
+ */
258
+ export declare function validateAuditFindingGates(findings: readonly AuditFinding[]): GateResult;
208
259
  /**
209
260
  * Scaffold an audit plan directory (`{PLAN_DIR}/audit-<date>/` layout,
210
261
  * mstar-audit SKILL.md § Plan output (all variants)): numbered `NNN-<slug>.md` plan files from
package/dist/audit.js CHANGED
@@ -1944,6 +1944,176 @@ function slugify(title) {
1944
1944
  }
1945
1945
  var escapeCell = (value) => value.replace(/\|/g, "\\|");
1946
1946
  var truncate = (value, max) => value.length > max ? `${value.slice(0, max)}…` : value;
1947
+ var collapseEvidenceWs = (value) => value.replace(/\s*[\r\n]\s*/g, " ");
1948
+ var evidenceText = (item) => typeof item === "string" ? item : `${item.file}${item.line !== undefined ? `:${item.line}` : ""} — ${item.description}`;
1949
+ var hasEnrichedMetadata = (finding) => finding.fingerprint !== undefined || finding.trace !== undefined || finding.severity !== undefined || finding.evidence.some((item) => typeof item !== "string");
1950
+ var AUDIT_FINGERPRINT_RE = /^[A-Za-z0-9][A-Za-z0-9._:/@+-]*(?![\s\S])/;
1951
+ var AUDIT_SEVERITY_ORDER = { informational: 0, low: 1, medium: 2, high: 3, critical: 4 };
1952
+ var DEFAULT_IGNORABLE_RE = /[\u00AD\u034F\u061C\u115F\u1160\u17B4\u17B5\u180B-\u180F\u200B-\u200F\u202A-\u202E\u2060-\u206F\u3164\uFE00-\uFE0F\uFEFF\uFFA0\uFFF0-\uFFF8\u{1BCA0}-\u{1BCA3}\u{1D173}-\u{1D17A}\u{E0000}-\u{E0FFF}]/u;
1953
+ var LONE_SURROGATE_RE = /[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?<![\uD800-\uDBFF])[\uDC00-\uDFFF]/;
1954
+ var isPlainObject3 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
1955
+ function isVisibleText(value) {
1956
+ if (LONE_SURROGATE_RE.test(value))
1957
+ return false;
1958
+ for (const ch of value) {
1959
+ if (!/\p{White_Space}/u.test(ch) && !DEFAULT_IGNORABLE_RE.test(ch))
1960
+ return true;
1961
+ }
1962
+ return false;
1963
+ }
1964
+ function safeAuditPath(value) {
1965
+ if (typeof value !== "string")
1966
+ return false;
1967
+ if (value === "")
1968
+ return false;
1969
+ if (value.includes("\\"))
1970
+ return false;
1971
+ if (/[\u0000-\u001F\u007F-\u009F]/.test(value))
1972
+ return false;
1973
+ if (LONE_SURROGATE_RE.test(value))
1974
+ return false;
1975
+ if (value.startsWith("/"))
1976
+ return false;
1977
+ if (/^[A-Za-z]:/.test(value))
1978
+ return false;
1979
+ for (const segment of value.split("/")) {
1980
+ if (segment === "" || segment === "." || segment === "..")
1981
+ return false;
1982
+ if (segment.endsWith(".") || segment.endsWith(" "))
1983
+ return false;
1984
+ }
1985
+ return true;
1986
+ }
1987
+ function validateAuditFindingGates(findings) {
1988
+ const violations = [];
1989
+ const seen = new Map;
1990
+ let previousFingerprint;
1991
+ if (!Array.isArray(findings)) {
1992
+ violations.push(violation4("high", "audit.finding.shape", "findings — audit.finding.shape"));
1993
+ return { ok: false, violations };
1994
+ }
1995
+ findings.forEach((finding, index) => {
1996
+ const at = (field) => `findings[${index}].${field}`;
1997
+ const push = (code, field) => {
1998
+ violations.push(violation4("high", code, `${at(field)} — ${code}`));
1999
+ };
2000
+ if (!isPlainObject3(finding)) {
2001
+ violations.push(violation4("high", "audit.finding.shape", `findings[${index}] — audit.finding.shape`));
2002
+ return;
2003
+ }
2004
+ if (finding.fingerprint !== undefined) {
2005
+ const fingerprint = finding.fingerprint;
2006
+ if (typeof fingerprint !== "string" || !AUDIT_FINGERPRINT_RE.test(fingerprint))
2007
+ push("audit.finding.fingerprint.grammar", "fingerprint");
2008
+ else if (redactSecrets(fingerprint).text !== fingerprint)
2009
+ push("audit.finding.fingerprint.secret", "fingerprint");
2010
+ else {
2011
+ const firstAt = seen.get(fingerprint);
2012
+ if (firstAt !== undefined)
2013
+ push("audit.finding.fingerprint.duplicate", "fingerprint");
2014
+ else
2015
+ seen.set(fingerprint, index);
2016
+ if (previousFingerprint !== undefined && fingerprint < previousFingerprint) {
2017
+ push("audit.finding.fingerprint.order", "fingerprint");
2018
+ }
2019
+ previousFingerprint = fingerprint;
2020
+ }
2021
+ }
2022
+ if (finding.severity !== undefined) {
2023
+ if (!isPlainObject3(finding.severity)) {
2024
+ push("audit.finding.severity.shape", "severity");
2025
+ } else {
2026
+ const { likelihood, impact, overall } = finding.severity;
2027
+ const rankOf = (r) => AUDIT_SEVERITY_ORDER[r];
2028
+ for (const [field, value] of [
2029
+ ["likelihood", likelihood],
2030
+ ["impact", impact],
2031
+ ["overall", overall]
2032
+ ]) {
2033
+ if (rankOf(value) === undefined)
2034
+ push("audit.finding.severity.rank", `severity.${field}`);
2035
+ }
2036
+ const impactRank = rankOf(impact);
2037
+ const overallRank = rankOf(overall);
2038
+ if (impactRank !== undefined && overallRank !== undefined && overallRank > impactRank) {
2039
+ push("audit.finding.severity.overall-exceeds-impact", "severity.overall");
2040
+ }
2041
+ }
2042
+ }
2043
+ const textViolation = (field, value) => {
2044
+ if (value === undefined)
2045
+ return;
2046
+ if (typeof value !== "string")
2047
+ push("audit.finding.text.type", field);
2048
+ else if (LONE_SURROGATE_RE.test(value))
2049
+ push("audit.finding.text.surrogate", field);
2050
+ else if (!isVisibleText(value))
2051
+ push("audit.finding.text.invisible", field);
2052
+ };
2053
+ if (finding.trace !== undefined) {
2054
+ if (!Array.isArray(finding.trace))
2055
+ push("audit.finding.trace.shape", "trace");
2056
+ else if (finding.trace.length === 0)
2057
+ push("audit.finding.trace.empty", "trace");
2058
+ else {
2059
+ const kinds = finding.trace.map((s) => isPlainObject3(s) ? s.kind : undefined);
2060
+ const topologyOk = finding.trace.length === 1 ? kinds[0] === "entrypoint" || kinds[0] === "sink" : kinds[0] === "entrypoint" && kinds[kinds.length - 1] === "sink" && kinds.slice(1, -1).every((k) => k === "propagation");
2061
+ if (!topologyOk)
2062
+ push("audit.finding.trace.topology", "trace");
2063
+ finding.trace.forEach((step, stepIndex) => {
2064
+ const stepAt = `trace[${stepIndex}]`;
2065
+ if (!isPlainObject3(step)) {
2066
+ push("audit.finding.trace.shape", stepAt);
2067
+ return;
2068
+ }
2069
+ if (typeof step.line !== "number" || !Number.isSafeInteger(step.line) || step.line <= 0) {
2070
+ push("audit.finding.trace.line", `${stepAt}.line`);
2071
+ }
2072
+ const stepFile = step.file;
2073
+ if (typeof stepFile !== "string" || !safeAuditPath(stepFile))
2074
+ push("audit.finding.path.unsafe", `${stepAt}.file`);
2075
+ else if (redactSecrets(stepFile).text !== stepFile)
2076
+ push("audit.finding.path.secret", `${stepAt}.file`);
2077
+ if (step.scope === undefined)
2078
+ push("audit.finding.trace.shape", `${stepAt}.scope`);
2079
+ else
2080
+ textViolation(`${stepAt}.scope`, step.scope);
2081
+ if (step.description === undefined)
2082
+ push("audit.finding.trace.shape", `${stepAt}.description`);
2083
+ else
2084
+ textViolation(`${stepAt}.description`, step.description);
2085
+ });
2086
+ }
2087
+ }
2088
+ if (!Array.isArray(finding.evidence))
2089
+ push("audit.finding.evidence.shape", "evidence");
2090
+ else
2091
+ finding.evidence.forEach((item, itemIndex) => {
2092
+ if (typeof item === "string") {
2093
+ textViolation(`evidence[${itemIndex}]`, item);
2094
+ return;
2095
+ }
2096
+ if (!isPlainObject3(item)) {
2097
+ push("audit.finding.evidence.shape", `evidence[${itemIndex}]`);
2098
+ return;
2099
+ }
2100
+ const itemFile = item.file;
2101
+ if (typeof itemFile !== "string" || !safeAuditPath(itemFile))
2102
+ push("audit.finding.path.unsafe", `evidence[${itemIndex}].file`);
2103
+ else if (redactSecrets(itemFile).text !== itemFile)
2104
+ push("audit.finding.path.secret", `evidence[${itemIndex}].file`);
2105
+ if (item.line !== undefined && (typeof item.line !== "number" || !Number.isSafeInteger(item.line) || item.line <= 0)) {
2106
+ push("audit.finding.evidence.line", `evidence[${itemIndex}].line`);
2107
+ }
2108
+ textViolation(`evidence[${itemIndex}].description`, item.description);
2109
+ });
2110
+ textViolation("title", finding.title);
2111
+ textViolation("impact", finding.impact);
2112
+ textViolation("fixSketch", finding.fixSketch);
2113
+ textViolation("verification", finding.verification);
2114
+ });
2115
+ return { ok: violations.length === 0, violations };
2116
+ }
1947
2117
  function renderPlanFile(finding, plannedAt) {
1948
2118
  const sections = [
1949
2119
  `# ${finding.title}`,
@@ -1952,15 +2122,22 @@ function renderPlanFile(finding, plannedAt) {
1952
2122
  `- **Priority**: ${finding.priority}`,
1953
2123
  `- **Effort**: ${finding.effort}`,
1954
2124
  `- **Risk**: ${finding.risk}`,
2125
+ ...finding.confidence !== "MED" || hasEnrichedMetadata(finding) ? [`- **Confidence**: ${finding.confidence}`] : [],
2126
+ ...finding.fingerprint !== undefined ? [`- **Fingerprint**: ${finding.fingerprint}`] : [],
2127
+ ...finding.severity !== undefined ? [`- **Likelihood**: ${finding.severity.likelihood}`, `- **Severity impact**: ${finding.severity.impact}`, `- **Severity**: ${finding.severity.overall}`] : [],
1955
2128
  `- **Depends on**: ${finding.dependsOn ?? "none"}`,
1956
2129
  `- **Category**: ${finding.category}`,
2130
+ ...finding.evidence.length > 0 ? [`- **Evidence**: ${collapseEvidenceWs(evidenceText(finding.evidence[0]))}`] : [],
1957
2131
  `- **Planned at**: commit \`${plannedAt.commit}\`, ${plannedAt.date}`,
1958
2132
  "",
1959
2133
  "## Impact",
1960
2134
  finding.impact
1961
2135
  ];
1962
2136
  if (finding.evidence.length > 0) {
1963
- sections.push("", "## Evidence", ...finding.evidence.map((e) => `- ${e}`));
2137
+ sections.push("", "## Evidence", ...finding.evidence.map((item) => `- ${evidenceText(item)}`));
2138
+ }
2139
+ if (finding.trace !== undefined) {
2140
+ sections.push("", "## Trace", "", "| Kind | Location | Scope / Description |", "|------|----------|---------------------|", ...finding.trace.map((step) => `| ${escapeCell(step.kind)} | ${escapeCell(`${step.file}:${step.line}`)} | ${escapeCell(`${step.scope} — ${step.description}`).replace(/\r\n|\r|\n/g, "\\n")} |`));
1964
2141
  }
1965
2142
  if (finding.fixSketch !== undefined) {
1966
2143
  sections.push("", "## Fix sketch", finding.fixSketch);
@@ -1980,7 +2157,8 @@ function redactFinding(finding) {
1980
2157
  ...finding,
1981
2158
  title: redactText(finding.title),
1982
2159
  impact: redactText(finding.impact),
1983
- evidence: finding.evidence.map(redactText),
2160
+ evidence: finding.evidence.map((item) => typeof item === "string" ? redactText(item) : { ...item, description: redactText(item.description) }),
2161
+ ...finding.trace !== undefined ? { trace: finding.trace.map((step) => ({ ...step, scope: redactText(step.scope), description: redactText(step.description) })) } : {},
1984
2162
  ...finding.fixSketch !== undefined ? { fixSketch: redactText(finding.fixSketch) } : {},
1985
2163
  ...finding.verification !== undefined ? { verification: redactText(finding.verification) } : {}
1986
2164
  };
@@ -2002,7 +2180,10 @@ function extractSecurityDispositionSections(text) {
2002
2180
  }
2003
2181
  function renderIndex(params) {
2004
2182
  const { date, repoName, repoShortSha, rows, rejected, needsVerification, hardeningChecked } = params;
2005
- const findingsRows = rows.map((r) => `| ${r.num} | ${escapeCell(r.title)} | ${r.category} | ${escapeCell(truncate(r.impact, 80))} | ${r.effort} | ${r.risk} | ${r.confidence} | ${escapeCell(truncate(r.evidence, 80))} |`).join(`
2183
+ const showFingerprint = rows.some((r) => r.fingerprint !== undefined);
2184
+ const showSeverity = rows.some((r) => r.likelihood !== undefined || r.severityImpact !== undefined || r.severity !== undefined);
2185
+ const cell = (value) => escapeCell(value ?? "—");
2186
+ const findingsRows = rows.map((r) => `| ${r.num} | ${escapeCell(r.title)} | ${r.category} | ${escapeCell(truncate(r.impact, 80))} | ${r.effort} | ${r.risk} | ${r.confidence} | ${escapeCell(truncate(r.evidence, 80))}` + (showFingerprint ? ` | ${cell(r.fingerprint)}` : "") + (showSeverity ? ` | ${cell(r.likelihood)} | ${cell(r.severityImpact)} | ${cell(r.severity)}` : "") + " |").join(`
2006
2187
  `);
2007
2188
  const directionRows = rows.filter((r) => r.category === "direction").map((r) => `- ${escapeCell(r.title)} — ${escapeCell(truncate(r.impact, 120))}`).join(`
2008
2189
  `);
@@ -2015,8 +2196,8 @@ function renderIndex(params) {
2015
2196
  "",
2016
2197
  "## Findings",
2017
2198
  "",
2018
- "| # | Finding | Category | Impact | Effort | Risk | Confidence | Evidence |",
2019
- "|---|---------|----------|--------|--------|------|------------|----------|",
2199
+ "| # | Finding | Category | Impact | Effort | Risk | Confidence | Evidence" + (showFingerprint ? " | Fingerprint" : "") + (showSeverity ? " | Likelihood | Severity impact | Severity" : "") + " |",
2200
+ "|---|---------|----------|--------|--------|------|------------|----------" + (showFingerprint ? "|------------" : "") + (showSeverity ? "|------------|-----------------|----------" : "") + "|",
2020
2201
  findingsRows
2021
2202
  ];
2022
2203
  if (directionRows !== "") {
@@ -2040,6 +2221,11 @@ function renderIndex(params) {
2040
2221
  function scaffoldAuditPlan(outDir, findings, options = {}) {
2041
2222
  const date = options.date ?? new Date().toISOString().slice(0, 10);
2042
2223
  const plannedAt = options.plannedAt ?? { commit: options.repoShortSha ?? "unknown", date };
2224
+ const gate = validateAuditFindingGates(findings);
2225
+ if (!gate.ok) {
2226
+ const first = gate.violations[0];
2227
+ throw new TypeError(`invalid audit findings — ${first.code}: ${first.message}`);
2228
+ }
2043
2229
  mkdirSync5(outDir, { recursive: true });
2044
2230
  const existingReadme = join9(outDir, "README.md");
2045
2231
  const carried = existsSync7(existingReadme) ? extractSecurityDispositionSections(readFileSync7(existingReadme, "utf8")) : { needsVerification: [], hardeningChecked: [] };
@@ -2074,10 +2260,14 @@ function scaffoldAuditPlan(outDir, findings, options = {}) {
2074
2260
  impact: "see plan file",
2075
2261
  effort: fields.get("Effort") ?? "—",
2076
2262
  risk: fields.get("Risk") ?? "—",
2077
- confidence: "—",
2263
+ confidence: fields.get("Confidence") ?? "—",
2078
2264
  evidence: fields.get("Evidence") ?? "—",
2079
2265
  priority: fields.get("Priority") ?? "—",
2080
- dependsOn: fields.get("Depends on") ?? "—"
2266
+ dependsOn: fields.get("Depends on") ?? "—",
2267
+ fingerprint: fields.get("Fingerprint"),
2268
+ likelihood: fields.get("Likelihood"),
2269
+ severityImpact: fields.get("Severity impact"),
2270
+ severity: fields.get("Severity")
2081
2271
  };
2082
2272
  });
2083
2273
  const byNum = new Map(rows.map((r) => [r.num, r]));
@@ -2092,9 +2282,13 @@ function scaffoldAuditPlan(outDir, findings, options = {}) {
2092
2282
  row.effort = finding.effort;
2093
2283
  row.risk = finding.risk;
2094
2284
  row.confidence = finding.confidence;
2095
- row.evidence = finding.evidence[0] ?? "";
2285
+ row.evidence = finding.evidence.length > 0 ? collapseEvidenceWs(evidenceText(finding.evidence[0])) : "";
2096
2286
  row.priority = finding.priority;
2097
2287
  row.dependsOn = finding.dependsOn ?? "none";
2288
+ row.fingerprint = finding.fingerprint;
2289
+ row.likelihood = finding.severity?.likelihood;
2290
+ row.severityImpact = finding.severity?.impact;
2291
+ row.severity = finding.severity?.overall;
2098
2292
  }
2099
2293
  });
2100
2294
  const needsVerificationLines = options.needsVerification !== undefined ? options.needsVerification.map((nv) => `- ${escapeCell(redactText(nv.lead))}: ${escapeCell(redactText(nv.how))}${nv.evidence ? ` (${escapeCell(redactText(nv.evidence))})` : ""}`) : carried.needsVerification;
@@ -2281,5 +2475,6 @@ export {
2281
2475
  scaffoldAuditPlan,
2282
2476
  scanSecrets,
2283
2477
  supplyChainChecks,
2478
+ validateAuditFindingGates,
2284
2479
  validateAuditStatusBlocks
2285
2480
  };
@@ -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.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", "coordination.delivery-source-repair.unsupported-workflow", "coordination.delivery-source-repair.terminal", "coordination.delivery-source-repair.no-accepted-handoff", "coordination.delivery-source-repair.already-aligned", "coordination.delivery-source-repair.not-legacy-shape", "coordination.delivery-source-repair.pr-conflict"];
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.invalid-session-id", "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", "coordination.delivery-source-repair.unsupported-workflow", "coordination.delivery-source-repair.terminal", "coordination.delivery-source-repair.no-accepted-handoff", "coordination.delivery-source-repair.already-aligned", "coordination.delivery-source-repair.not-legacy-shape", "coordination.delivery-source-repair.pr-conflict"];
4
4
  export type CoordinationErrorCode = (typeof COORDINATION_ERROR_CODES)[number];
5
5
  /**
6
6
  * Stable exception of the coordination surface: `code` is the consumer
@@ -38,7 +38,7 @@ export type ResolvedPlanScope = {
38
38
  };
39
39
  /**
40
40
  * Session envelope persisted at
41
- * `{WORKFLOW_DIR}/<workflow-id>/sessions/<session-id>.json` (mode `0600`).
41
+ * `{WORKFLOW_DIR}/<workflow-id>/sessions/<role>-<session-id>.json` (mode `0600`).
42
42
  * The envelope is the durable proof of who holds the session: the snapshot
43
43
  * stores its canonical path and every later call must present the same file.
44
44
  */
@@ -51,20 +51,24 @@ export type CoordinationSession = {
51
51
  harness_root: string;
52
52
  };
53
53
  /**
54
- * Bind addressing (spec §B). A fresh bind never supplies a session path: the
55
- * engine generates the session UUID and creates
56
- * `<workflow-dir>/<workflow-id>/sessions/<session-id>.json` itself. An existing
57
- * session is reached only through its explicit `resumePath`, which resumes
58
- * read-only and never writes.
54
+ * Bind addressing (spec §B). A fresh bind generates the session UUID and
55
+ * creates `<workflow-dir>/<workflow-id>/sessions/<role>-<session-id>.json`
56
+ * itself, unless the caller supplies `sessionId` then the engine adopts that
57
+ * value as the identity after validating it as a single safe path component (it
58
+ * names the envelope's file, prefixed by the role). An existing session is
59
+ * reached only through its explicit `resumePath`, which resumes read-only,
60
+ * never writes and never re-identifies.
59
61
  */
60
62
  export type BindPlanSessionInput = {
61
63
  scope: PlanScopeInput;
62
64
  cwd: string;
65
+ sessionId?: string;
63
66
  } | {
64
67
  coordinator: true;
65
68
  workflowId: string;
66
69
  harnessDir?: string;
67
70
  cwd: string;
71
+ sessionId?: string;
68
72
  } | {
69
73
  resumePath: string;
70
74
  cwd: string;
@@ -263,8 +267,10 @@ export declare function readCoordinatedArtifact(harnessRoot: string, ref: Artifa
263
267
  */
264
268
  /**
265
269
  * Bind a session (spec §B, §C2). Fresh addressing never supplies a session
266
- * path: the engine generates the UUID and creates the envelope. `resumePath`
267
- * names an existing envelope and resumes read-only.
270
+ * path: the engine generates the UUID or adopts the caller-supplied
271
+ * `sessionId`, refused unless it is a single safe path component — and creates
272
+ * the envelope. `resumePath` names an existing envelope and resumes read-only,
273
+ * so it takes no identity input at all.
268
274
  */
269
275
  export declare function bindPlanSession(input: BindPlanSessionInput): Promise<CoordinationResult>;
270
276
  /**
package/dist/engine.js CHANGED
@@ -2084,6 +2084,158 @@ async function registerPlanWorkflow(workflowId, options) {
2084
2084
  return { workflowId, snapshotPath, recovered: false };
2085
2085
  });
2086
2086
  }
2087
+ function iterationWorkflowRegistrationIdentity(snapshot) {
2088
+ const coordinator = snapshot.coordination?.coordinator;
2089
+ return stableJson({
2090
+ id: snapshot.id,
2091
+ type: snapshot.type,
2092
+ status: snapshot.status,
2093
+ compass_ref: snapshot.compass_ref ?? null,
2094
+ branch: snapshot.branch ?? null,
2095
+ project: snapshot.project ?? null,
2096
+ plans: snapshot.plans,
2097
+ coordinator: coordinator ? { session_id: coordinator.session_id, session_file: coordinator.session_file } : null
2098
+ });
2099
+ }
2100
+ async function registerIterationWorkflow(workflowId, options) {
2101
+ const refuse = (detail) => new Error(`registerIterationWorkflow: ${detail}`);
2102
+ if (typeof options !== "object" || options === null || Array.isArray(options)) {
2103
+ throw refuse("options must be an object (harnessDir, compassRef, branch, rows)");
2104
+ }
2105
+ if (typeof options.harnessDir !== "string" || options.harnessDir.trim() === "") {
2106
+ throw refuse("options.harnessDir is required (must contain status.json + workflows/)");
2107
+ }
2108
+ assertSafePathComponent(workflowId, "workflow id");
2109
+ if (typeof options.compassRef !== "string" || options.compassRef.trim() === "") {
2110
+ throw refuse("options.compassRef must be a non-empty string");
2111
+ }
2112
+ if (typeof options.branch !== "object" || options.branch === null || Array.isArray(options.branch)) {
2113
+ throw refuse("options.branch must be an object (base, integration, target)");
2114
+ }
2115
+ for (const anchor of ["base", "integration", "target"]) {
2116
+ const value = options.branch[anchor];
2117
+ if (typeof value !== "string" || value.trim() === "") {
2118
+ throw refuse(`options.branch.${anchor} must be a non-empty string`);
2119
+ }
2120
+ }
2121
+ if (options.project !== undefined && (typeof options.project !== "string" || options.project.trim() === "")) {
2122
+ throw refuse("options.project must be a non-empty string when given");
2123
+ }
2124
+ const rows = options.rows;
2125
+ if (!Array.isArray(rows) || rows.length === 0) {
2126
+ throw refuse("options.rows must be a non-empty array of { id, title, file }");
2127
+ }
2128
+ const seenRowIds = new Set;
2129
+ for (const row of rows) {
2130
+ if (typeof row !== "object" || row === null || Array.isArray(row)) {
2131
+ throw refuse("each row must be an object ({ id, title, file })");
2132
+ }
2133
+ for (const field of ["id", "title", "file"]) {
2134
+ if (typeof row[field] !== "string" || row[field].trim() === "") {
2135
+ throw refuse(`each row.${field} must be a non-empty string`);
2136
+ }
2137
+ }
2138
+ if ("status" in row) {
2139
+ throw refuse(`row ${JSON.stringify(row.id)} supplies a status — rows are always written status "Todo"; ` + "a state transition is requested through the lifecycle seams, never at registration");
2140
+ }
2141
+ if (seenRowIds.has(row.id)) {
2142
+ throw refuse(`duplicate row id ${JSON.stringify(row.id)} — plan row ids are unique per workflow`);
2143
+ }
2144
+ seenRowIds.add(row.id);
2145
+ }
2146
+ const startedAt = options.startedAt ?? new Date().toISOString();
2147
+ if (!isCloseTimestamp(startedAt)) {
2148
+ throw refuse("options.startedAt must be a valid YYYY-MM-DD date or RFC3339 timestamp");
2149
+ }
2150
+ const harnessDir = resolve6(options.harnessDir);
2151
+ const statusPath = join6(harnessDir, "status.json");
2152
+ const workflowDir = join6(harnessDir, "workflows", workflowId);
2153
+ const snapshotPath = join6(workflowDir, WORKFLOW_SNAPSHOT_FILE);
2154
+ const store = getArtifactStore();
2155
+ const snapshot = {
2156
+ schema_version: 1,
2157
+ id: workflowId,
2158
+ type: "iteration",
2159
+ status: "running",
2160
+ started_at: startedAt,
2161
+ updated_at: startedAt.slice(0, 10),
2162
+ compass_ref: options.compassRef,
2163
+ branch: { base: options.branch.base, integration: options.branch.integration, target: options.branch.target },
2164
+ plans: rows.map((r) => ({
2165
+ id: r.id,
2166
+ title: r.title,
2167
+ file: r.file,
2168
+ status: "Todo",
2169
+ metadata: {
2170
+ iteration_refs: [options.compassRef],
2171
+ spec_integration_branch: options.branch.integration,
2172
+ merge_target: options.branch.integration
2173
+ }
2174
+ }))
2175
+ };
2176
+ if (options.project !== undefined)
2177
+ snapshot.project = options.project;
2178
+ const entry = {
2179
+ id: workflowId,
2180
+ type: "iteration",
2181
+ started_at: snapshot.started_at,
2182
+ dir: `workflows/${workflowId}`
2183
+ };
2184
+ const entryGate = validateWorkflowEntry(entry);
2185
+ if (!entryGate.ok) {
2186
+ throw new Error(`refusing to register invalid workflow entry: ${entryGate.violations.map((v) => v.message).join("; ")}`);
2187
+ }
2188
+ return withStatusWriteLock(statusPath, async () => {
2189
+ const rootDoc = readJson(statusPath);
2190
+ const workflows = Array.isArray(rootDoc.workflows) ? rootDoc.workflows : [];
2191
+ if (workflows.some((candidate) => isPlainObject2(candidate) && candidate.id === workflowId)) {
2192
+ throw new Error(`refusing to register workflow ${JSON.stringify(workflowId)}: it is already registered ` + `(root entry in ${statusPath}) — registration is create-only; ` + "remove that workflow before registering again");
2193
+ }
2194
+ if (existsSync4(snapshotPath)) {
2195
+ const existing = readWorkflowSnapshot(workflowDir);
2196
+ if (existing.snapshot.id !== workflowId || iterationWorkflowRegistrationIdentity(existing.snapshot) !== iterationWorkflowRegistrationIdentity(snapshot)) {
2197
+ 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");
2198
+ }
2199
+ const recoveryEntry = {
2200
+ id: workflowId,
2201
+ type: "iteration",
2202
+ started_at: existing.snapshot.started_at,
2203
+ dir: `workflows/${workflowId}`
2204
+ };
2205
+ const recoveryGate = validateWorkflowEntry(recoveryEntry);
2206
+ if (!recoveryGate.ok) {
2207
+ throw new Error(`refusing to register invalid workflow entry: ${recoveryGate.violations.map((v) => v.message).join("; ")}`);
2208
+ }
2209
+ await registerWorkflowEntryLocked(statusPath, recoveryEntry);
2210
+ return { workflowId, snapshotPath, recovered: true };
2211
+ }
2212
+ let createdVersion;
2213
+ try {
2214
+ await writeWorkflowSnapshot(snapshot, workflowDir, { createOnly: true });
2215
+ createdVersion = readArtifactBytes(snapshotPath)?.version;
2216
+ await registerWorkflowEntryLocked(statusPath, entry);
2217
+ } catch (error) {
2218
+ if (createdVersion !== undefined) {
2219
+ await withStatusWriteLock(snapshotPath, async () => {
2220
+ const current = readArtifactBytes(snapshotPath);
2221
+ if (current === undefined || current.version !== createdVersion)
2222
+ return;
2223
+ const remove = store.delete?.bind(store);
2224
+ if (remove !== undefined) {
2225
+ await withProtectedWrite(snapshotPath, "delete", () => remove({ kind: "snapshot", key: workflowId }));
2226
+ }
2227
+ });
2228
+ }
2229
+ try {
2230
+ if (readdirSync2(workflowDir).length === 0) {
2231
+ rmdirSync2(workflowDir);
2232
+ }
2233
+ } catch {}
2234
+ throw error;
2235
+ }
2236
+ return { workflowId, snapshotPath, recovered: false };
2237
+ });
2238
+ }
2087
2239
 
2088
2240
  // src/status.ts
2089
2241
  var DATE_RE = /^\d{4}-\d{2}-\d{2}$/;
@@ -7627,6 +7779,176 @@ function slugify(title) {
7627
7779
  }
7628
7780
  var escapeCell = (value) => value.replace(/\|/g, "\\|");
7629
7781
  var truncate = (value, max) => value.length > max ? `${value.slice(0, max)}…` : value;
7782
+ var collapseEvidenceWs = (value) => value.replace(/\s*[\r\n]\s*/g, " ");
7783
+ var evidenceText = (item) => typeof item === "string" ? item : `${item.file}${item.line !== undefined ? `:${item.line}` : ""} — ${item.description}`;
7784
+ var hasEnrichedMetadata = (finding) => finding.fingerprint !== undefined || finding.trace !== undefined || finding.severity !== undefined || finding.evidence.some((item) => typeof item !== "string");
7785
+ var AUDIT_FINGERPRINT_RE = /^[A-Za-z0-9][A-Za-z0-9._:/@+-]*(?![\s\S])/;
7786
+ var AUDIT_SEVERITY_ORDER = { informational: 0, low: 1, medium: 2, high: 3, critical: 4 };
7787
+ var DEFAULT_IGNORABLE_RE = /[\u00AD\u034F\u061C\u115F\u1160\u17B4\u17B5\u180B-\u180F\u200B-\u200F\u202A-\u202E\u2060-\u206F\u3164\uFE00-\uFE0F\uFEFF\uFFA0\uFFF0-\uFFF8\u{1BCA0}-\u{1BCA3}\u{1D173}-\u{1D17A}\u{E0000}-\u{E0FFF}]/u;
7788
+ var LONE_SURROGATE_RE = /[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?<![\uD800-\uDBFF])[\uDC00-\uDFFF]/;
7789
+ var isPlainObject9 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
7790
+ function isVisibleText(value) {
7791
+ if (LONE_SURROGATE_RE.test(value))
7792
+ return false;
7793
+ for (const ch of value) {
7794
+ if (!/\p{White_Space}/u.test(ch) && !DEFAULT_IGNORABLE_RE.test(ch))
7795
+ return true;
7796
+ }
7797
+ return false;
7798
+ }
7799
+ function safeAuditPath(value) {
7800
+ if (typeof value !== "string")
7801
+ return false;
7802
+ if (value === "")
7803
+ return false;
7804
+ if (value.includes("\\"))
7805
+ return false;
7806
+ if (/[\u0000-\u001F\u007F-\u009F]/.test(value))
7807
+ return false;
7808
+ if (LONE_SURROGATE_RE.test(value))
7809
+ return false;
7810
+ if (value.startsWith("/"))
7811
+ return false;
7812
+ if (/^[A-Za-z]:/.test(value))
7813
+ return false;
7814
+ for (const segment of value.split("/")) {
7815
+ if (segment === "" || segment === "." || segment === "..")
7816
+ return false;
7817
+ if (segment.endsWith(".") || segment.endsWith(" "))
7818
+ return false;
7819
+ }
7820
+ return true;
7821
+ }
7822
+ function validateAuditFindingGates(findings) {
7823
+ const violations = [];
7824
+ const seen = new Map;
7825
+ let previousFingerprint;
7826
+ if (!Array.isArray(findings)) {
7827
+ violations.push(violation10("high", "audit.finding.shape", "findings — audit.finding.shape"));
7828
+ return { ok: false, violations };
7829
+ }
7830
+ findings.forEach((finding, index) => {
7831
+ const at = (field) => `findings[${index}].${field}`;
7832
+ const push = (code, field) => {
7833
+ violations.push(violation10("high", code, `${at(field)} — ${code}`));
7834
+ };
7835
+ if (!isPlainObject9(finding)) {
7836
+ violations.push(violation10("high", "audit.finding.shape", `findings[${index}] — audit.finding.shape`));
7837
+ return;
7838
+ }
7839
+ if (finding.fingerprint !== undefined) {
7840
+ const fingerprint = finding.fingerprint;
7841
+ if (typeof fingerprint !== "string" || !AUDIT_FINGERPRINT_RE.test(fingerprint))
7842
+ push("audit.finding.fingerprint.grammar", "fingerprint");
7843
+ else if (redactSecrets(fingerprint).text !== fingerprint)
7844
+ push("audit.finding.fingerprint.secret", "fingerprint");
7845
+ else {
7846
+ const firstAt = seen.get(fingerprint);
7847
+ if (firstAt !== undefined)
7848
+ push("audit.finding.fingerprint.duplicate", "fingerprint");
7849
+ else
7850
+ seen.set(fingerprint, index);
7851
+ if (previousFingerprint !== undefined && fingerprint < previousFingerprint) {
7852
+ push("audit.finding.fingerprint.order", "fingerprint");
7853
+ }
7854
+ previousFingerprint = fingerprint;
7855
+ }
7856
+ }
7857
+ if (finding.severity !== undefined) {
7858
+ if (!isPlainObject9(finding.severity)) {
7859
+ push("audit.finding.severity.shape", "severity");
7860
+ } else {
7861
+ const { likelihood, impact, overall } = finding.severity;
7862
+ const rankOf = (r) => AUDIT_SEVERITY_ORDER[r];
7863
+ for (const [field, value] of [
7864
+ ["likelihood", likelihood],
7865
+ ["impact", impact],
7866
+ ["overall", overall]
7867
+ ]) {
7868
+ if (rankOf(value) === undefined)
7869
+ push("audit.finding.severity.rank", `severity.${field}`);
7870
+ }
7871
+ const impactRank = rankOf(impact);
7872
+ const overallRank = rankOf(overall);
7873
+ if (impactRank !== undefined && overallRank !== undefined && overallRank > impactRank) {
7874
+ push("audit.finding.severity.overall-exceeds-impact", "severity.overall");
7875
+ }
7876
+ }
7877
+ }
7878
+ const textViolation = (field, value) => {
7879
+ if (value === undefined)
7880
+ return;
7881
+ if (typeof value !== "string")
7882
+ push("audit.finding.text.type", field);
7883
+ else if (LONE_SURROGATE_RE.test(value))
7884
+ push("audit.finding.text.surrogate", field);
7885
+ else if (!isVisibleText(value))
7886
+ push("audit.finding.text.invisible", field);
7887
+ };
7888
+ if (finding.trace !== undefined) {
7889
+ if (!Array.isArray(finding.trace))
7890
+ push("audit.finding.trace.shape", "trace");
7891
+ else if (finding.trace.length === 0)
7892
+ push("audit.finding.trace.empty", "trace");
7893
+ else {
7894
+ const kinds = finding.trace.map((s) => isPlainObject9(s) ? s.kind : undefined);
7895
+ const topologyOk = finding.trace.length === 1 ? kinds[0] === "entrypoint" || kinds[0] === "sink" : kinds[0] === "entrypoint" && kinds[kinds.length - 1] === "sink" && kinds.slice(1, -1).every((k) => k === "propagation");
7896
+ if (!topologyOk)
7897
+ push("audit.finding.trace.topology", "trace");
7898
+ finding.trace.forEach((step, stepIndex) => {
7899
+ const stepAt = `trace[${stepIndex}]`;
7900
+ if (!isPlainObject9(step)) {
7901
+ push("audit.finding.trace.shape", stepAt);
7902
+ return;
7903
+ }
7904
+ if (typeof step.line !== "number" || !Number.isSafeInteger(step.line) || step.line <= 0) {
7905
+ push("audit.finding.trace.line", `${stepAt}.line`);
7906
+ }
7907
+ const stepFile = step.file;
7908
+ if (typeof stepFile !== "string" || !safeAuditPath(stepFile))
7909
+ push("audit.finding.path.unsafe", `${stepAt}.file`);
7910
+ else if (redactSecrets(stepFile).text !== stepFile)
7911
+ push("audit.finding.path.secret", `${stepAt}.file`);
7912
+ if (step.scope === undefined)
7913
+ push("audit.finding.trace.shape", `${stepAt}.scope`);
7914
+ else
7915
+ textViolation(`${stepAt}.scope`, step.scope);
7916
+ if (step.description === undefined)
7917
+ push("audit.finding.trace.shape", `${stepAt}.description`);
7918
+ else
7919
+ textViolation(`${stepAt}.description`, step.description);
7920
+ });
7921
+ }
7922
+ }
7923
+ if (!Array.isArray(finding.evidence))
7924
+ push("audit.finding.evidence.shape", "evidence");
7925
+ else
7926
+ finding.evidence.forEach((item, itemIndex) => {
7927
+ if (typeof item === "string") {
7928
+ textViolation(`evidence[${itemIndex}]`, item);
7929
+ return;
7930
+ }
7931
+ if (!isPlainObject9(item)) {
7932
+ push("audit.finding.evidence.shape", `evidence[${itemIndex}]`);
7933
+ return;
7934
+ }
7935
+ const itemFile = item.file;
7936
+ if (typeof itemFile !== "string" || !safeAuditPath(itemFile))
7937
+ push("audit.finding.path.unsafe", `evidence[${itemIndex}].file`);
7938
+ else if (redactSecrets(itemFile).text !== itemFile)
7939
+ push("audit.finding.path.secret", `evidence[${itemIndex}].file`);
7940
+ if (item.line !== undefined && (typeof item.line !== "number" || !Number.isSafeInteger(item.line) || item.line <= 0)) {
7941
+ push("audit.finding.evidence.line", `evidence[${itemIndex}].line`);
7942
+ }
7943
+ textViolation(`evidence[${itemIndex}].description`, item.description);
7944
+ });
7945
+ textViolation("title", finding.title);
7946
+ textViolation("impact", finding.impact);
7947
+ textViolation("fixSketch", finding.fixSketch);
7948
+ textViolation("verification", finding.verification);
7949
+ });
7950
+ return { ok: violations.length === 0, violations };
7951
+ }
7630
7952
  function renderPlanFile(finding, plannedAt) {
7631
7953
  const sections = [
7632
7954
  `# ${finding.title}`,
@@ -7635,15 +7957,22 @@ function renderPlanFile(finding, plannedAt) {
7635
7957
  `- **Priority**: ${finding.priority}`,
7636
7958
  `- **Effort**: ${finding.effort}`,
7637
7959
  `- **Risk**: ${finding.risk}`,
7960
+ ...finding.confidence !== "MED" || hasEnrichedMetadata(finding) ? [`- **Confidence**: ${finding.confidence}`] : [],
7961
+ ...finding.fingerprint !== undefined ? [`- **Fingerprint**: ${finding.fingerprint}`] : [],
7962
+ ...finding.severity !== undefined ? [`- **Likelihood**: ${finding.severity.likelihood}`, `- **Severity impact**: ${finding.severity.impact}`, `- **Severity**: ${finding.severity.overall}`] : [],
7638
7963
  `- **Depends on**: ${finding.dependsOn ?? "none"}`,
7639
7964
  `- **Category**: ${finding.category}`,
7965
+ ...finding.evidence.length > 0 ? [`- **Evidence**: ${collapseEvidenceWs(evidenceText(finding.evidence[0]))}`] : [],
7640
7966
  `- **Planned at**: commit \`${plannedAt.commit}\`, ${plannedAt.date}`,
7641
7967
  "",
7642
7968
  "## Impact",
7643
7969
  finding.impact
7644
7970
  ];
7645
7971
  if (finding.evidence.length > 0) {
7646
- sections.push("", "## Evidence", ...finding.evidence.map((e) => `- ${e}`));
7972
+ sections.push("", "## Evidence", ...finding.evidence.map((item) => `- ${evidenceText(item)}`));
7973
+ }
7974
+ if (finding.trace !== undefined) {
7975
+ sections.push("", "## Trace", "", "| Kind | Location | Scope / Description |", "|------|----------|---------------------|", ...finding.trace.map((step) => `| ${escapeCell(step.kind)} | ${escapeCell(`${step.file}:${step.line}`)} | ${escapeCell(`${step.scope} — ${step.description}`).replace(/\r\n|\r|\n/g, "\\n")} |`));
7647
7976
  }
7648
7977
  if (finding.fixSketch !== undefined) {
7649
7978
  sections.push("", "## Fix sketch", finding.fixSketch);
@@ -7663,7 +7992,8 @@ function redactFinding(finding) {
7663
7992
  ...finding,
7664
7993
  title: redactText(finding.title),
7665
7994
  impact: redactText(finding.impact),
7666
- evidence: finding.evidence.map(redactText),
7995
+ evidence: finding.evidence.map((item) => typeof item === "string" ? redactText(item) : { ...item, description: redactText(item.description) }),
7996
+ ...finding.trace !== undefined ? { trace: finding.trace.map((step) => ({ ...step, scope: redactText(step.scope), description: redactText(step.description) })) } : {},
7667
7997
  ...finding.fixSketch !== undefined ? { fixSketch: redactText(finding.fixSketch) } : {},
7668
7998
  ...finding.verification !== undefined ? { verification: redactText(finding.verification) } : {}
7669
7999
  };
@@ -7685,7 +8015,10 @@ function extractSecurityDispositionSections(text) {
7685
8015
  }
7686
8016
  function renderIndex(params) {
7687
8017
  const { date, repoName, repoShortSha, rows, rejected, needsVerification, hardeningChecked } = params;
7688
- const findingsRows = rows.map((r) => `| ${r.num} | ${escapeCell(r.title)} | ${r.category} | ${escapeCell(truncate(r.impact, 80))} | ${r.effort} | ${r.risk} | ${r.confidence} | ${escapeCell(truncate(r.evidence, 80))} |`).join(`
8018
+ const showFingerprint = rows.some((r) => r.fingerprint !== undefined);
8019
+ const showSeverity = rows.some((r) => r.likelihood !== undefined || r.severityImpact !== undefined || r.severity !== undefined);
8020
+ const cell = (value) => escapeCell(value ?? "—");
8021
+ const findingsRows = rows.map((r) => `| ${r.num} | ${escapeCell(r.title)} | ${r.category} | ${escapeCell(truncate(r.impact, 80))} | ${r.effort} | ${r.risk} | ${r.confidence} | ${escapeCell(truncate(r.evidence, 80))}` + (showFingerprint ? ` | ${cell(r.fingerprint)}` : "") + (showSeverity ? ` | ${cell(r.likelihood)} | ${cell(r.severityImpact)} | ${cell(r.severity)}` : "") + " |").join(`
7689
8022
  `);
7690
8023
  const directionRows = rows.filter((r) => r.category === "direction").map((r) => `- ${escapeCell(r.title)} — ${escapeCell(truncate(r.impact, 120))}`).join(`
7691
8024
  `);
@@ -7698,8 +8031,8 @@ function renderIndex(params) {
7698
8031
  "",
7699
8032
  "## Findings",
7700
8033
  "",
7701
- "| # | Finding | Category | Impact | Effort | Risk | Confidence | Evidence |",
7702
- "|---|---------|----------|--------|--------|------|------------|----------|",
8034
+ "| # | Finding | Category | Impact | Effort | Risk | Confidence | Evidence" + (showFingerprint ? " | Fingerprint" : "") + (showSeverity ? " | Likelihood | Severity impact | Severity" : "") + " |",
8035
+ "|---|---------|----------|--------|--------|------|------------|----------" + (showFingerprint ? "|------------" : "") + (showSeverity ? "|------------|-----------------|----------" : "") + "|",
7703
8036
  findingsRows
7704
8037
  ];
7705
8038
  if (directionRows !== "") {
@@ -7723,6 +8056,11 @@ function renderIndex(params) {
7723
8056
  function scaffoldAuditPlan(outDir, findings, options = {}) {
7724
8057
  const date = options.date ?? new Date().toISOString().slice(0, 10);
7725
8058
  const plannedAt = options.plannedAt ?? { commit: options.repoShortSha ?? "unknown", date };
8059
+ const gate2 = validateAuditFindingGates(findings);
8060
+ if (!gate2.ok) {
8061
+ const first = gate2.violations[0];
8062
+ throw new TypeError(`invalid audit findings — ${first.code}: ${first.message}`);
8063
+ }
7726
8064
  mkdirSync8(outDir, { recursive: true });
7727
8065
  const existingReadme = join16(outDir, "README.md");
7728
8066
  const carried = existsSync12(existingReadme) ? extractSecurityDispositionSections(readFileSync11(existingReadme, "utf8")) : { needsVerification: [], hardeningChecked: [] };
@@ -7757,10 +8095,14 @@ function scaffoldAuditPlan(outDir, findings, options = {}) {
7757
8095
  impact: "see plan file",
7758
8096
  effort: fields.get("Effort") ?? "—",
7759
8097
  risk: fields.get("Risk") ?? "—",
7760
- confidence: "—",
8098
+ confidence: fields.get("Confidence") ?? "—",
7761
8099
  evidence: fields.get("Evidence") ?? "—",
7762
8100
  priority: fields.get("Priority") ?? "—",
7763
- dependsOn: fields.get("Depends on") ?? "—"
8101
+ dependsOn: fields.get("Depends on") ?? "—",
8102
+ fingerprint: fields.get("Fingerprint"),
8103
+ likelihood: fields.get("Likelihood"),
8104
+ severityImpact: fields.get("Severity impact"),
8105
+ severity: fields.get("Severity")
7764
8106
  };
7765
8107
  });
7766
8108
  const byNum = new Map(rows.map((r) => [r.num, r]));
@@ -7775,9 +8117,13 @@ function scaffoldAuditPlan(outDir, findings, options = {}) {
7775
8117
  row.effort = finding.effort;
7776
8118
  row.risk = finding.risk;
7777
8119
  row.confidence = finding.confidence;
7778
- row.evidence = finding.evidence[0] ?? "";
8120
+ row.evidence = finding.evidence.length > 0 ? collapseEvidenceWs(evidenceText(finding.evidence[0])) : "";
7779
8121
  row.priority = finding.priority;
7780
8122
  row.dependsOn = finding.dependsOn ?? "none";
8123
+ row.fingerprint = finding.fingerprint;
8124
+ row.likelihood = finding.severity?.likelihood;
8125
+ row.severityImpact = finding.severity?.impact;
8126
+ row.severity = finding.severity?.overall;
7781
8127
  }
7782
8128
  });
7783
8129
  const needsVerificationLines = options.needsVerification !== undefined ? options.needsVerification.map((nv) => `- ${escapeCell(redactText(nv.lead))}: ${escapeCell(redactText(nv.how))}${nv.evidence ? ` (${escapeCell(redactText(nv.evidence))})` : ""}`) : carried.needsVerification;
@@ -9013,7 +9359,7 @@ function computePrTally(input) {
9013
9359
  var REVIEW_SCHEMA_ID = "mstar.review/v1";
9014
9360
  var INSPECTOR_VERDICTS = ["comment", "request_changes", "approve"];
9015
9361
  var INSPECTOR_SEVERITIES = ["critical", "warning", "suggestion", "info"];
9016
- function isPlainObject9(value) {
9362
+ function isPlainObject10(value) {
9017
9363
  return typeof value === "object" && value !== null && !Array.isArray(value);
9018
9364
  }
9019
9365
  var TALLY_COUNT_KEYS = ["mustFix", "shouldFix", "nit", "unverified"];
@@ -9024,7 +9370,7 @@ function checkProvidedTallyShape(tally, violations) {
9024
9370
  if (typeof tally.scorePct !== "number" || !Number.isInteger(tally.scorePct) || tally.scorePct < 0 || tally.scorePct > 100) {
9025
9371
  violations.push(violation15("high", "review.tally-malformed", `tally.scorePct must be an integer in [0, 100] - got ${String(tally.scorePct)}`));
9026
9372
  }
9027
- if (!isPlainObject9(tally.tally)) {
9373
+ if (!isPlainObject10(tally.tally)) {
9028
9374
  violations.push(violation15("high", "review.tally-malformed", "tally.tally must be an object carrying the four class counts"));
9029
9375
  } else {
9030
9376
  for (const key of TALLY_COUNT_KEYS) {
@@ -9040,7 +9386,7 @@ function checkProvidedTallyShape(tally, violations) {
9040
9386
  }
9041
9387
  function validateMstarReviewV1(doc) {
9042
9388
  const violations = [];
9043
- if (!isPlainObject9(doc)) {
9389
+ if (!isPlainObject10(doc)) {
9044
9390
  return {
9045
9391
  ok: false,
9046
9392
  violations: [violation15("high", "review.not-object", "review document must be a JSON object")]
@@ -9067,7 +9413,7 @@ function validateMstarReviewV1(doc) {
9067
9413
  violations.push(violation15("high", "review.findings-not-array", "findings must be an array"));
9068
9414
  } else {
9069
9415
  doc.findings.forEach((finding, index) => {
9070
- if (!isPlainObject9(finding)) {
9416
+ if (!isPlainObject10(finding)) {
9071
9417
  violations.push(violation15("high", "review.invalid-finding", `findings[${index}] must be an object`));
9072
9418
  return;
9073
9419
  }
@@ -9107,7 +9453,7 @@ function validateMstarReviewV1(doc) {
9107
9453
  });
9108
9454
  }
9109
9455
  if (doc.tally !== undefined) {
9110
- if (!isPlainObject9(doc.tally)) {
9456
+ if (!isPlainObject10(doc.tally)) {
9111
9457
  violations.push(violation15("high", "review.invalid-tally", "tally must be a PrTallyResult object"));
9112
9458
  } else {
9113
9459
  checkProvidedTallyShape(doc.tally, violations);
@@ -9117,7 +9463,7 @@ function validateMstarReviewV1(doc) {
9117
9463
  }
9118
9464
  }
9119
9465
  if (doc.target !== undefined) {
9120
- if (!isPlainObject9(doc.target)) {
9466
+ if (!isPlainObject10(doc.target)) {
9121
9467
  violations.push(violation15("high", "review.invalid-target", "target must be an object"));
9122
9468
  } else {
9123
9469
  if (doc.target.owner !== undefined && typeof doc.target.owner !== "string") {
@@ -10030,8 +10376,8 @@ function reportedBranchesOf(row) {
10030
10376
  function snapshotPathOf(harnessRoot, workflowId) {
10031
10377
  return join20(resolveWorkflowDir(harnessRoot, { harnessDir: harnessRoot }), workflowId, SNAPSHOT_FILE2);
10032
10378
  }
10033
- function sessionFilePath(harnessRoot, workflowId, sessionId) {
10034
- return join20(resolveWorkflowDir(harnessRoot, { harnessDir: harnessRoot }), workflowId, SESSION_DIR, `${sessionId}.json`);
10379
+ function sessionFilePath(harnessRoot, workflowId, role, sessionId) {
10380
+ return join20(resolveWorkflowDir(harnessRoot, { harnessDir: harnessRoot }), workflowId, SESSION_DIR, `${role}-${sessionId}.json`);
10035
10381
  }
10036
10382
  function localStore(harnessRoot) {
10037
10383
  let store;
@@ -10192,6 +10538,22 @@ function safePlanId(planId, where) {
10192
10538
  }
10193
10539
  return planId;
10194
10540
  }
10541
+ var SESSION_ID_MAX_LENGTH = 128;
10542
+ function safeSessionId(value) {
10543
+ if (value === undefined)
10544
+ return;
10545
+ if (typeof value !== "string")
10546
+ throw invalidInput("sessionId must be a string");
10547
+ if (value.length > SESSION_ID_MAX_LENGTH) {
10548
+ throw new CoordinationError("coordination.invalid-session-id", `session id is longer than ${SESSION_ID_MAX_LENGTH} characters: ${JSON.stringify(value)}`, { session_id: value, max_length: SESSION_ID_MAX_LENGTH });
10549
+ }
10550
+ try {
10551
+ assertSafePathComponent(value, "session id");
10552
+ } catch (error) {
10553
+ throw new CoordinationError("coordination.invalid-session-id", `session id ${JSON.stringify(value)} is not a safe path component: ${errorMessage(error)}`, { session_id: value });
10554
+ }
10555
+ return value;
10556
+ }
10195
10557
  function readSnapshot(dir) {
10196
10558
  const snapshotPath = join20(dir, SNAPSHOT_FILE2);
10197
10559
  if (!existsSync15(snapshotPath)) {
@@ -10375,7 +10737,7 @@ function readSessionEnvelope(sessionPath) {
10375
10737
  return session;
10376
10738
  }
10377
10739
  function createSessionEnvelope(session) {
10378
- const path = sessionFilePath(session.harness_root, session.workflow_id, session.session_id);
10740
+ const path = sessionFilePath(session.harness_root, session.workflow_id, session.role, session.session_id);
10379
10741
  mkdirSync9(dirname12(path), { recursive: true });
10380
10742
  try {
10381
10743
  writeFileSync7(path, `${JSON.stringify(session, null, 2)}
@@ -10680,7 +11042,7 @@ function assertCoordinatorResidency(cwd, snapshot) {
10680
11042
  }
10681
11043
  return main;
10682
11044
  }
10683
- async function bindCoordinatorSession(cwd, workflowId, harnessDir) {
11045
+ async function bindCoordinatorSession(cwd, workflowId, harnessDir, sessionId) {
10684
11046
  const harnessRoot = requireProcessRoot(cwd, harnessDir);
10685
11047
  safePlanId(workflowId, "workflowId");
10686
11048
  const snapshotPath = snapshotPathOf(harnessRoot, workflowId);
@@ -10689,7 +11051,7 @@ async function bindCoordinatorSession(cwd, workflowId, harnessDir) {
10689
11051
  const session = {
10690
11052
  schema_version: 1,
10691
11053
  role: "coordinator",
10692
- session_id: randomUUID2(),
11054
+ session_id: sessionId ?? randomUUID2(),
10693
11055
  workflow_id: workflowId,
10694
11056
  harness_root: harnessRoot
10695
11057
  };
@@ -10729,7 +11091,7 @@ function requireProcessRoot(cwd, harnessDir) {
10729
11091
  }
10730
11092
  return root;
10731
11093
  }
10732
- async function bindPlanSessionForPlan(scope) {
11094
+ async function bindPlanSessionForPlan(scope, sessionId) {
10733
11095
  const { harnessRoot, workflowId, planId } = scope;
10734
11096
  const snapshotPath = snapshotPathOf(harnessRoot, workflowId);
10735
11097
  assertSnapshotPath(harnessRoot, workflowId, snapshotPath);
@@ -10752,7 +11114,7 @@ async function bindPlanSessionForPlan(scope) {
10752
11114
  const session = {
10753
11115
  schema_version: 1,
10754
11116
  role: "plan-pm",
10755
- session_id: randomUUID2(),
11117
+ session_id: sessionId ?? randomUUID2(),
10756
11118
  workflow_id: workflowId,
10757
11119
  plan_id: planId,
10758
11120
  harness_root: harnessRoot
@@ -10819,19 +11181,21 @@ async function bindPlanSession(input) {
10819
11181
  return resumeBoundSession(input.resumePath);
10820
11182
  }
10821
11183
  if ("coordinator" in input) {
10822
- assertExactKeys(input, ["coordinator", "workflowId", "harnessDir", "cwd"], "bind coordinator input");
11184
+ assertExactKeys(input, ["coordinator", "workflowId", "harnessDir", "cwd", "sessionId"], "bind coordinator input");
10823
11185
  if (input.coordinator !== true)
10824
11186
  throw invalidInput("`coordinator` is only meaningful as true");
10825
11187
  if (!isNonEmptyString(input.workflowId))
10826
11188
  throw invalidInput("workflowId is required");
10827
11189
  requireCwd(input.cwd);
10828
- return bindCoordinatorSession(input.cwd, input.workflowId, input.harnessDir);
11190
+ const sessionId2 = safeSessionId(input.sessionId);
11191
+ return bindCoordinatorSession(input.cwd, input.workflowId, input.harnessDir, sessionId2);
10829
11192
  }
10830
- assertExactKeys(input, ["scope", "cwd"], "bind plan input");
11193
+ assertExactKeys(input, ["scope", "cwd", "sessionId"], "bind plan input");
10831
11194
  if (!isPlainObject2(input.scope))
10832
11195
  throw invalidInput("a plan bind requires a scope");
10833
11196
  requireCwd(input.cwd);
10834
- return bindPlanSessionForPlan(await resolvePlanScope(input.scope, input.cwd));
11197
+ const sessionId = safeSessionId(input.sessionId);
11198
+ return bindPlanSessionForPlan(await resolvePlanScope(input.scope, input.cwd), sessionId);
10835
11199
  }
10836
11200
  function requireCwd(cwd) {
10837
11201
  if (!isNonEmptyString(cwd) || !isAbsolute12(cwd))
@@ -13430,6 +13794,7 @@ export {
13430
13794
  readWorkflowSnapshot,
13431
13795
  recordWorkflowDelivery,
13432
13796
  referenceExists,
13797
+ registerIterationWorkflow,
13433
13798
  registerPlanWorkflow,
13434
13799
  registerWorkflow,
13435
13800
  releaseLease,
@@ -13474,6 +13839,7 @@ export {
13474
13839
  techDebtRollup,
13475
13840
  unregisterWorkflow,
13476
13841
  validateAssignmentFields,
13842
+ validateAuditFindingGates,
13477
13843
  validateAuditStatusBlocks,
13478
13844
  validateCompassFrontmatter,
13479
13845
  validateDesignTokenFrontmatter,
package/dist/index.d.ts CHANGED
@@ -45,8 +45,8 @@ export type { PlanRow, ResidualEntry, StatusDoc, StatusV2Doc, WorkflowEntry, } f
45
45
  export { normalizeSeverity, registerWorkflow, resolveCompassEnforcement, resolveMstarcEnforcement, resolveRepoEnforcement, unregisterWorkflow, validatePlanRow, validateResidual, validateStatus, validateStatusV2, validateWorkflowEntry, } from "./status.js";
46
46
  export type { ClaimLeaseFields, ExecutionLease, ExecutionLeaseLocations, IntegrationMergeLease, LeaseTransition, LeaseVerifyResult, } from "./lease.js";
47
47
  export { canSteal, claimLease, planExecutionLeaseLocations, releaseLease, sameHolderResume, validateExecutionLease, validateIntegrationMergeLease, verifyPlanExecutionLease, withStatusWriteLock, } from "./lease.js";
48
- export type { CloseWorkflowOptions, DeclareWorkflowDeliveryKindOptions, DeliveryRegistrationEvidence, RecordWorkflowDeliveryOptions, RecordWorkflowDeliveryResult, RegisterPlanWorkflowOptions, RegisterPlanWorkflowResult, WorkflowBranchAnchors, WorkflowCompoundOutcome, WorkflowDeliveryEvidence, WorkflowDeliveryKind, WorkflowExecutionPolicy, WorkflowLifecycleStatus, WorkflowLifecycleType, WorkflowSnapshot, WorkflowSnapshotRead, } from "./workflow.js";
49
- export { assertDeliveryRegistrationCoherence, closeWorkflow, consultDeliveryEvidence, declareWorkflowDeliveryKind, isTerminalSnapshot, LEGACY_WORKTREE_PATH_CODE, recordWorkflowDelivery, registerPlanWorkflow, WORKFLOW_COMPOUND_OUTCOMES, WORKFLOW_DELIVERY_KINDS, WORKFLOW_LIFECYCLE_STATUSES, WORKFLOW_LIFECYCLE_TYPES, WORKFLOW_SNAPSHOT_FILE, WORKFLOW_TERMINAL_STATUSES, readWorkflowSnapshot, validateWorkflowSnapshot, writeWorkflowSnapshot, } from "./workflow.js";
48
+ export type { CloseWorkflowOptions, DeclareWorkflowDeliveryKindOptions, DeliveryRegistrationEvidence, RecordWorkflowDeliveryOptions, RecordWorkflowDeliveryResult, RegisterIterationWorkflowOptions, RegisterIterationWorkflowResult, RegisterPlanWorkflowOptions, RegisterPlanWorkflowResult, WorkflowBranchAnchors, WorkflowCompoundOutcome, WorkflowDeliveryEvidence, WorkflowDeliveryKind, WorkflowExecutionPolicy, WorkflowLifecycleStatus, WorkflowLifecycleType, WorkflowSnapshot, WorkflowSnapshotRead, } from "./workflow.js";
49
+ export { assertDeliveryRegistrationCoherence, closeWorkflow, consultDeliveryEvidence, declareWorkflowDeliveryKind, isTerminalSnapshot, LEGACY_WORKTREE_PATH_CODE, recordWorkflowDelivery, registerIterationWorkflow, registerPlanWorkflow, WORKFLOW_COMPOUND_OUTCOMES, WORKFLOW_DELIVERY_KINDS, WORKFLOW_LIFECYCLE_STATUSES, WORKFLOW_LIFECYCLE_TYPES, WORKFLOW_SNAPSHOT_FILE, WORKFLOW_TERMINAL_STATUSES, readWorkflowSnapshot, validateWorkflowSnapshot, writeWorkflowSnapshot, } from "./workflow.js";
50
50
  export type { CleanupDecision, CleanupFacts, CleanupTarget, CleanupTargetKind, } from "./cleanup.js";
51
51
  export { planWorktreeCleanup } from "./cleanup.js";
52
52
  export type { EvidenceArtifactFact, EvidenceAssessment, EvidenceCaptureRequest, EvidenceCoverage, EvidenceEnvironmentKey, EvidenceExpectation, EvidenceInputEntry, EvidenceInputSnapshot, EvidenceInputSpec, EvidenceLimits, EvidenceLog, EvidenceOutcome, EvidenceToolFingerprint, SddEvidenceRecord, } from "./evidence.js";
@@ -67,8 +67,8 @@ export type { MigrateNotesFile, MigrateOptions, MigratePlan, MigrateRegister, Mi
67
67
  export { ARCHIVED_STATUS_V1_FILE, MIGRATE_STATUS_FILE, NOTES_LEDGER_FILE, applyMigratePlan, migrateHarnessTree, } from "./migrate.js";
68
68
  export type { CompletenessItem, CompletenessLevel, CompletenessPlaceholder, CompletenessResult, DesignFrontmatter, } from "./design-md.js";
69
69
  export { assertLightDarkParity, completenessLevel, parseDesignFrontmatter, validateDesignTokenFrontmatter, } from "./design-md.js";
70
- export type { AuditCategory, AuditConfidence, AuditEffort, AuditFinding, AuditPriority, AuditRisk, PromoteAuditPlansOptions, RedactResult, ScaffoldAuditPlanOptions, ScaffoldAuditPlanResult, ScannedSecret, SecretFinding, SupplyChainFinding, SupplyChainFindingKind, SupplyChainResult, } from "./audit.js";
71
- export { AUDIT_CATEGORIES, AUDIT_CONFIDENCES, AUDIT_EFFORTS, AUDIT_PRIORITIES, AUDIT_RISKS, promoteAuditPlans, scaffoldAuditPlan, scanSecrets, supplyChainChecks, validateAuditStatusBlocks, } from "./audit.js";
70
+ export type { AuditCategory, AuditConfidence, AuditEffort, AuditEvidence, AuditFinding, AuditPriority, AuditRisk, AuditSeverity, AuditSeverityRank, AuditTraceKind, AuditTraceStep, PromoteAuditPlansOptions, RedactResult, ScaffoldAuditPlanOptions, ScaffoldAuditPlanResult, ScannedSecret, SecretFinding, SupplyChainFinding, SupplyChainFindingKind, SupplyChainResult, } from "./audit.js";
71
+ export { AUDIT_CATEGORIES, AUDIT_CONFIDENCES, AUDIT_EFFORTS, AUDIT_PRIORITIES, AUDIT_RISKS, promoteAuditPlans, scaffoldAuditPlan, scanSecrets, supplyChainChecks, validateAuditFindingGates, validateAuditStatusBlocks, } from "./audit.js";
72
72
  export { KNOWLEDGE_BUG_PROBLEM_TYPES, KNOWLEDGE_CATEGORY_MAP, KNOWLEDGE_KNOWLEDGE_PROBLEM_TYPES, KNOWLEDGE_PROBLEM_TYPES, KNOWLEDGE_REQUIRED_FIELDS, KNOWLEDGE_RESOLUTION_TYPES, KNOWLEDGE_SEVERITIES, assertIndexRows, compoundRefreshScope, referenceExists, scopeGuard, validateSchemaYaml, } from "./compound.js";
73
73
  export type { ReferenceCheckResult } from "./compound.js";
74
74
  export type { EphemeralCitation, PlanQualityFinding, PlanQualityResult, ProvenanceCitation, SimplifyMarker, TemporaryMarker, TemporaryMarkerResult, } from "./lint.js";
@@ -522,3 +522,78 @@ export type RegisterPlanWorkflowResult = {
522
522
  * root could differ — the routed writers fail loud on a path mismatch.
523
523
  */
524
524
  export declare function registerPlanWorkflow(workflowId: string, options: RegisterPlanWorkflowOptions): Promise<RegisterPlanWorkflowResult>;
525
+ /** Options for `registerIterationWorkflow`. `harnessDir` is required — the
526
+ * snapshot and `status.json` live under the harness root. */
527
+ export type RegisterIterationWorkflowOptions = {
528
+ /** Absolute harness dir that contains `status.json` + `workflows/`. Required. */
529
+ harnessDir: string;
530
+ /** Harness-relative compass ref recorded on the snapshot, e.g. `iterations/<id>/delivery-compass.md`. Required. */
531
+ compassRef: string;
532
+ /** The three iteration branch anchors. All three required. */
533
+ branch: {
534
+ base: string;
535
+ integration: string;
536
+ target: string;
537
+ };
538
+ /** Initial plan rows. At least one. Every row is written `status: "Todo"`. */
539
+ rows: Array<{
540
+ id: string;
541
+ title: string;
542
+ file: string;
543
+ }>;
544
+ /** Project register id recorded on the snapshot. */
545
+ project?: string;
546
+ /** Registration timestamp (YYYY-MM-DD or RFC3339). Default: now. */
547
+ startedAt?: string;
548
+ };
549
+ export type RegisterIterationWorkflowResult = {
550
+ workflowId: string;
551
+ snapshotPath: string;
552
+ /**
553
+ * True on orphan recovery: existing snapshot bytes/timestamps are preserved
554
+ * and only the missing root entry is written.
555
+ */
556
+ recovered: boolean;
557
+ };
558
+ /**
559
+ * Register an iteration workflow (seam S1 sibling of `registerPlanWorkflow`).
560
+ * Builds the create-only `type: iteration` snapshot (workflow id, compass
561
+ * ref, the three branch anchors, Todo rows whose `metadata` —
562
+ * `iteration_refs`, `spec_integration_branch`, `merge_target` — is derived
563
+ * from the already-required compass/integration inputs; registration never
564
+ * authorizes implementation) and the root `workflows[]` entry, then writes
565
+ * BOTH under one atomic section of the root `withStatusWriteLock` — the same
566
+ * create-only `writeWorkflowSnapshot(..., { createOnly: true })` →
567
+ * `registerWorkflowEntryLocked` primitive sequence the plan producer uses —
568
+ * with rollback that removes ONLY the exact snapshot version this call
569
+ * created (plus the now-empty workflow dir; a non-empty directory is never
570
+ * force-removed).
571
+ *
572
+ * Refusals (fail-loud, before any write): non-empty `workflowId`
573
+ * (`assertSafePathComponent`), `harnessDir`, `compassRef`, all three branch
574
+ * anchors, `project` when given, and each row's `id`/`title`/`file`;
575
+ * non-empty row array; duplicate row ids; a SUPPLIED row `status` (a
576
+ * requested state transition is never treated as successful registration);
577
+ * `startedAt` must be a valid YYYY-MM-DD / RFC3339 timestamp. Rows are
578
+ * constructed from the declared fields — untrusted row objects are never
579
+ * spread.
580
+ *
581
+ * Already-registered refusals: inside the root lock, an existing entry for
582
+ * the requested id refuses BEFORE either branch — including a stale entry
583
+ * whose snapshot is missing. This producer is not the low-level upsert and
584
+ * does not repair/repoint an existing identity. Malformed/v1 roots refuse
585
+ * without replacing their bytes.
586
+ *
587
+ * Crash/retry recovery: a snapshot without its root entry (an orphan)
588
+ * re-registers only when `iterationWorkflowRegistrationIdentity` matches —
589
+ * the existing snapshot's bytes and timestamps are preserved verbatim and
590
+ * the root entry is written with the orphan's `started_at` (`recovered:
591
+ * true`). A differing identity (including a mismatched snapshot id or an
592
+ * existing coordinator binding) refuses; a failed recovery root write never
593
+ * deletes the orphan.
594
+ *
595
+ * The caller must pin the artifact store to the harness root first
596
+ * (`setArtifactStore(createFsStore(harnessDir))`) when the active store's
597
+ * root could differ — the routed writers fail loud on a path mismatch.
598
+ */
599
+ export declare function registerIterationWorkflow(workflowId: string, options: RegisterIterationWorkflowOptions): Promise<RegisterIterationWorkflowResult>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mstar-harness/engine",
3
- "version": "3.10.3",
3
+ "version": "3.11.1",
4
4
  "description": "Morning Star Harness Workflow Engine — deterministic workflow enforcement library (path, status, lease, dispatch, sdd, iteration, lint gates).",
5
5
  "license": "MIT",
6
6
  "repository": {