@mstar-harness/cli 3.2.6 → 3.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/mstar-harness.js +1615 -1190
  2. package/package.json +1 -1
@@ -4073,25 +4073,25 @@ import { randomUUID } from "node:crypto";
4073
4073
  import { basename, dirname, join, resolve } from "node:path";
4074
4074
  import { readFileSync as readFileSync2, statSync } from "node:fs";
4075
4075
  import { dirname as dirname2, isAbsolute, join as join2, relative, resolve as resolve2 } from "node:path";
4076
- import { existsSync as existsSync2, mkdirSync as mkdirSync2, readdirSync, readFileSync as readFileSync3, realpathSync, statSync as statSync2 } from "node:fs";
4076
+ import { existsSync as existsSync5, mkdirSync as mkdirSync4, readdirSync as readdirSync4, readFileSync as readFileSync6, realpathSync as realpathSync2, statSync as statSync3, writeFileSync as writeFileSync3 } from "node:fs";
4077
4077
  import { execFileSync } from "node:child_process";
4078
- import { basename as basename2, dirname as dirname3, isAbsolute as isAbsolute2, join as join3, relative as relative2, resolve as resolve3 } from "node:path";
4079
- import { existsSync as existsSync3, readFileSync as readFileSync4, readdirSync as readdirSync2, realpathSync as realpathSync2 } from "node:fs";
4080
- import { dirname as dirname5, join as join6, resolve as resolve5, sep } from "node:path";
4081
- import { mkdirSync as mkdirSync3, rmdirSync, statSync as statSync3, unlinkSync as unlinkSync2, writeFileSync as writeFileSync2 } from "node:fs";
4082
- import { dirname as dirname4, isAbsolute as isAbsolute3, join as join4, resolve as resolve4 } from "node:path";
4078
+ import { basename as basename2, dirname as dirname5, isAbsolute as isAbsolute3, join as join8, relative as relative2, resolve as resolve5 } from "node:path";
4079
+ import { existsSync as existsSync4, readFileSync as readFileSync5, readdirSync as readdirSync3 } from "node:fs";
4080
+ import { join as join7 } from "node:path";
4081
+ import { existsSync as existsSync2, readdirSync, readFileSync as readFileSync3 } from "node:fs";
4082
+ import { existsSync as existsSync3, readFileSync as readFileSync4, readdirSync as readdirSync2, realpathSync } from "node:fs";
4083
+ import { dirname as dirname4, join as join6, resolve as resolve4, sep } from "node:path";
4084
+ import { mkdirSync as mkdirSync2, rmdirSync, statSync as statSync2, unlinkSync as unlinkSync2, writeFileSync as writeFileSync2 } from "node:fs";
4085
+ import { dirname as dirname3, isAbsolute as isAbsolute2, join as join4, resolve as resolve3 } from "node:path";
4083
4086
  import { setTimeout as sleep } from "node:timers/promises";
4084
4087
  import { AsyncLocalStorage as AsyncLocalStorage2 } from "node:async_hooks";
4085
4088
  import { execFileSync as execFileSync2 } from "node:child_process";
4086
- import { existsSync as existsSync4 } from "node:fs";
4089
+ import { existsSync as existsSync6 } from "node:fs";
4087
4090
  import { isAbsolute as isAbsolute4, resolve as resolve6 } from "node:path";
4088
4091
  import { execFileSync as execFileSync3 } from "node:child_process";
4089
- import { mkdirSync as mkdirSync5, readdirSync as readdirSync3, readFileSync as readFileSync5, realpathSync as realpathSync3, statSync as statSync4, writeFileSync as writeFileSync3 } from "node:fs";
4090
- import { basename as basename3, dirname as dirname6, isAbsolute as isAbsolute5, join as join7, resolve as resolve7 } from "node:path";
4091
- import { existsSync as existsSync5, readdirSync as readdirSync4, readFileSync as readFileSync6 } from "node:fs";
4092
- import { existsSync as existsSync6, readFileSync as readFileSync7, readdirSync as readdirSync5 } from "node:fs";
4093
- import { join as join9 } from "node:path";
4094
- import { existsSync as existsSync7, mkdirSync as mkdirSync7, readdirSync as readdirSync7, readFileSync as readFileSync9, rmdirSync as rmdirSync2, rmSync, writeFileSync as writeFileSync5 } from "node:fs";
4092
+ import { mkdirSync as mkdirSync5, readdirSync as readdirSync5, readFileSync as readFileSync7, realpathSync as realpathSync3, statSync as statSync4, writeFileSync as writeFileSync4 } from "node:fs";
4093
+ import { basename as basename3, dirname as dirname6, isAbsolute as isAbsolute5, join as join9, resolve as resolve7 } from "node:path";
4094
+ import { existsSync as existsSync7, mkdirSync as mkdirSync7, readdirSync as readdirSync7, readFileSync as readFileSync9, rmdirSync as rmdirSync2, rmSync, writeFileSync as writeFileSync6 } from "node:fs";
4095
4095
  import { basename as basename4, join as join11, resolve as resolve9, sep as sep3 } from "node:path";
4096
4096
  import { existsSync as existsSync8, readdirSync as readdirSync8, readFileSync as readFileSync10 } from "node:fs";
4097
4097
  import { basename as basename5, isAbsolute as isAbsolute7, join as join12, relative as relative4, resolve as resolve10, sep as sep4 } from "node:path";
@@ -4210,248 +4210,384 @@ function isAtOrBelow(dir, root) {
4210
4210
  const rel = relative(root, dir);
4211
4211
  return rel === "" || !rel.startsWith("..") && !isAbsolute(rel);
4212
4212
  }
4213
- function resolveHarnessDir(startDir = process.cwd(), opts = {}) {
4214
- const start = resolve3(startDir);
4215
- const explicit = opts.harnessDir ?? process.env.MSTAR_HARNESS_DIR;
4216
- if (explicit)
4217
- return resolve3(start, explicit);
4218
- const boundary = resolve3(start, opts.workspaceRoot ?? defaultWorkspaceRoot(start));
4219
- const rc = loadMstarc(start, boundary);
4220
- if (rc !== null && rc.config.harnessDir)
4221
- return resolve3(rc.dir, rc.config.harnessDir);
4222
- let dir = start;
4223
- for (;; ) {
4224
- if (!isAtOrBelow2(dir, boundary))
4225
- return null;
4226
- for (const candidate of [join3(dir, ".mstar"), join3(dir, ".agents"), join3(dir, ".plans"), join3(dir, "plans")]) {
4227
- if (isDirectory(candidate))
4228
- return candidate;
4229
- }
4230
- if (dir === boundary)
4231
- return null;
4232
- const parent = dirname3(dir);
4233
- if (parent === dir)
4234
- return null;
4235
- dir = parent;
4236
- }
4213
+ var COMPASS_STATUSES = ["active", "locked", "completed"];
4214
+ var DATE_RE = /^\d{4}-\d{2}-\d{2}$/;
4215
+ var PLAN_STATUS_DONE = "Done";
4216
+ function typeName(value) {
4217
+ if (value === null)
4218
+ return "null";
4219
+ if (Array.isArray(value))
4220
+ return "array";
4221
+ return typeof value;
4237
4222
  }
4238
- function defaultWorkspaceRoot(startDir) {
4239
- try {
4240
- const cdup = execFileSync("git", ["rev-parse", "--show-cdup"], {
4241
- cwd: startDir,
4242
- encoding: "utf8",
4243
- stdio: ["ignore", "pipe", "ignore"]
4244
- }).trim();
4245
- if (!cdup)
4246
- return startDir;
4247
- let boundary = startDir;
4248
- for (const segment of cdup.split(/[\\/]/)) {
4249
- if (segment && segment !== ".")
4250
- boundary = dirname3(boundary);
4223
+ function validateCompassShape(doc) {
4224
+ const issues = [];
4225
+ const expectString = (key, opts = {}) => {
4226
+ const value = doc[key];
4227
+ if (typeof value !== "string") {
4228
+ issues.push({ path: [key], message: `expected string, received ${typeName(value)}` });
4229
+ return;
4251
4230
  }
4252
- return resolve3(boundary);
4253
- } catch {}
4254
- return startDir;
4255
- }
4256
- function isAtOrBelow2(dir, root) {
4257
- const rel = relative2(root, dir);
4258
- return rel === "" || !rel.startsWith("..") && !isAbsolute2(rel);
4259
- }
4260
- function mstarcDirOverride(harnessDir, key) {
4261
- const dir = resolve3(harnessDir);
4262
- const rc = loadMstarc(dir, dirname3(dir));
4263
- const declared = rc?.config[key];
4264
- return declared ? resolve3(rc.dir, declared) : null;
4265
- }
4266
- function resolveSpecsDir(harnessDir, opts = {}) {
4267
- const declared = mstarcDirOverride(harnessDir, "specsDir");
4268
- if (declared !== null) {
4269
- if (opts.create !== false)
4270
- mkdirSync2(declared, { recursive: true });
4271
- return declared;
4272
- }
4273
- const harness = resolve3(harnessDir);
4274
- const repoRoot = dirname3(harness);
4275
- const candidates = [
4276
- join3(harness, "specs"),
4277
- join3(repoRoot, "docs", "specs"),
4278
- join3(repoRoot, "specs"),
4279
- join3(harness, "designs"),
4280
- join3(repoRoot, "designs")
4281
- ];
4282
- for (const candidate of candidates) {
4283
- if (isDirectory(candidate) && hasFiles(candidate))
4284
- return candidate;
4285
- }
4286
- const fallback = join3(harness, "specs");
4287
- if (opts.create !== false)
4288
- mkdirSync2(fallback, { recursive: true });
4289
- return fallback;
4290
- }
4291
- function assertSafePathComponent(value, what) {
4292
- if (value === "" || value === "." || value === ".." || !/^[A-Za-z0-9._-]+$/.test(value)) {
4293
- throw new Error(`${what} must be a single safe path component ([A-Za-z0-9._-]+; not "", ".", "..", or containing "/" or "\\") \u2014 got ${JSON.stringify(value)}`);
4294
- }
4295
- }
4296
- function resolveSddDir(harnessDir, planId) {
4297
- assertSafePathComponent(planId, "planId");
4298
- const base = resolve3(harnessDir);
4299
- const declared = mstarcDirOverride(base, "sddDir");
4300
- const sddBase = declared !== null ? declared : join3(base, "sdd");
4301
- return join3(sddBase, planId);
4302
- }
4303
- function resolveHarnessSubdir(startDir, opts, key, fallback) {
4304
- const harness = resolveHarnessDir(startDir, opts);
4305
- if (harness === null) {
4306
- throw new Error(`harness dir not found from ${resolve3(startDir)} \u2014 cannot resolve the ${fallback} dir (run \`mstar init\`, pass opts.harnessDir, or set MSTAR_HARNESS_DIR)`);
4231
+ if (opts.min !== undefined && value.length < opts.min) {
4232
+ issues.push({ path: [key], message: `string must contain at least ${opts.min} character(s)` });
4233
+ return;
4234
+ }
4235
+ if (opts.regex !== undefined && !opts.regex.test(value)) {
4236
+ issues.push({ path: [key], message: `string must match ${opts.regex}` });
4237
+ }
4238
+ };
4239
+ expectString("iteration_id", { min: 1 });
4240
+ expectString("start_date", { regex: DATE_RE });
4241
+ const status = doc.status;
4242
+ if (typeof status !== "string" || !COMPASS_STATUSES.includes(status)) {
4243
+ issues.push({
4244
+ path: ["status"],
4245
+ message: `expected one of ${COMPASS_STATUSES.map((s) => `'${s}'`).join(" | ")}, received ${typeName(status)}`
4246
+ });
4307
4247
  }
4308
- const declared = mstarcDirOverride(harness, key);
4309
- return declared !== null ? declared : join3(resolve3(harness), fallback);
4310
- }
4311
- function resolveWorkflowDir(startDir = process.cwd(), opts = {}) {
4312
- return resolveHarnessSubdir(startDir, opts, "workflowDir", "workflows");
4313
- }
4314
- function resolveProjectDir(startDir = process.cwd(), opts = {}) {
4315
- return resolveHarnessSubdir(startDir, opts, "projectDir", "projects");
4316
- }
4317
- var GITIGNORE_SNIPPET = `# Morning Star harness (.mstar/)
4318
- # Principle: process stays local; results are shared with the team.
4319
- # Default-ignore everything under .mstar/, then re-include the tracked results.
4320
- .mstar/**
4321
- !.mstar/AGENTS.md
4322
- !.mstar/knowledge/
4323
- !.mstar/knowledge/**
4324
- !.mstar/specs/
4325
- !.mstar/specs/**
4326
- # .mstarc \u2014 repo-local harness config (may declare [config] harness_dir=<name>)
4327
- .mstarc
4328
- `;
4329
- var GITIGNORE_SNIPPET_AGENTS = `# Morning Star harness (.agents/) \u2014 legacy
4330
- # Default-ignore everything under .agents/, then re-include the tracked results.
4331
- .agents/**
4332
- !.agents/AGENTS.md
4333
- !.agents/knowledge/
4334
- !.agents/knowledge/**
4335
- !.agents/specs/
4336
- !.agents/specs/**
4337
- `;
4338
- var GITIGNORE_PROCESS_ENTRIES = GITIGNORE_SNIPPET.split(`
4339
- `).filter((line) => line.startsWith(".mstar/") || line.startsWith("!.mstar/")).map((line) => line.trim());
4340
- var GITIGNORE_PROCESS_ENTRIES_AGENTS = GITIGNORE_SNIPPET_AGENTS.split(`
4341
- `).filter((line) => line.startsWith(".agents/") || line.startsWith("!.agents/")).map((line) => line.trim());
4342
- function isDirectory(dir) {
4343
- try {
4344
- return statSync2(dir).isDirectory();
4345
- } catch {
4346
- return false;
4248
+ expectString("iteration_base_branch", { min: 1 });
4249
+ expectString("target_branch", { min: 1 });
4250
+ const plans = doc.plans;
4251
+ if (plans !== undefined) {
4252
+ if (!Array.isArray(plans)) {
4253
+ issues.push({ path: ["plans"], message: `expected array, received ${typeName(plans)}` });
4254
+ } else {
4255
+ plans.forEach((entry, index) => {
4256
+ if (typeof entry !== "string") {
4257
+ issues.push({ path: ["plans", index], message: `expected string, received ${typeName(entry)}` });
4258
+ } else if (entry.length < 1) {
4259
+ issues.push({ path: ["plans", index], message: "string must contain at least 1 character(s)" });
4260
+ }
4261
+ });
4262
+ }
4347
4263
  }
4348
- }
4349
- function hasFiles(dir) {
4350
- try {
4351
- for (const entry of readdirSync(dir, { withFileTypes: true })) {
4352
- if (entry.isDirectory()) {
4353
- if (hasFiles(join3(dir, entry.name)))
4354
- return true;
4355
- } else if (entry.isFile()) {
4356
- return true;
4357
- }
4264
+ const end_date = doc.end_date;
4265
+ if (end_date !== undefined) {
4266
+ if (typeof end_date !== "string") {
4267
+ issues.push({ path: ["end_date"], message: `expected string, received ${typeName(end_date)}` });
4268
+ } else if (!DATE_RE.test(end_date)) {
4269
+ issues.push({ path: ["end_date"], message: `string must match ${DATE_RE}` });
4358
4270
  }
4359
- return false;
4360
- } catch {
4361
- return false;
4362
4271
  }
4363
- }
4364
- function isPlainObject2(value) {
4365
- return typeof value === "object" && value !== null && !Array.isArray(value);
4272
+ if (issues.length > 0)
4273
+ return { ok: false, issues };
4274
+ return {
4275
+ ok: true,
4276
+ data: {
4277
+ iteration_id: doc.iteration_id,
4278
+ start_date: doc.start_date,
4279
+ status,
4280
+ iteration_base_branch: doc.iteration_base_branch,
4281
+ target_branch: doc.target_branch,
4282
+ ...plans !== undefined ? { plans } : {},
4283
+ ...end_date !== undefined ? { end_date } : {}
4284
+ }
4285
+ };
4366
4286
  }
4367
4287
  function violation(severity, code, message, fix) {
4368
4288
  return { ok: false, severity, code, message, fix };
4369
4289
  }
4370
- function validateNonEmptyString(violations, value, field, missingCode, invalidCode) {
4371
- if (value === undefined) {
4372
- violations.push(violation("high", missingCode, `missing required field: ${field}`));
4373
- } else if (typeof value !== "string" || value.trim() === "") {
4374
- violations.push(violation("medium", invalidCode, `${field} must be a non-empty string`));
4375
- }
4376
- }
4377
- var DATE_PART = String.raw`\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12]\d|3[01])`;
4378
- var RFC3339_Z_RE = new RegExp(String.raw`^${DATE_PART}T\d{2}:\d{2}:\d{2}(\.\d+)?Z$`);
4379
- var DATE_ONLY_RE = new RegExp(String.raw`^${DATE_PART}$`);
4380
- function isValidClaimedAt(value) {
4381
- return typeof value === "string" && (RFC3339_Z_RE.test(value) || DATE_ONLY_RE.test(value));
4290
+ function isPlainObject2(value) {
4291
+ return typeof value === "object" && value !== null && !Array.isArray(value);
4382
4292
  }
4383
- function validateExecutionLease(lease) {
4384
- const violations = [];
4385
- if (!isPlainObject2(lease)) {
4293
+ function validateCompassFrontmatter(doc) {
4294
+ if (!isPlainObject2(doc)) {
4386
4295
  return {
4387
4296
  ok: false,
4388
4297
  violations: [
4389
- violation("high", "lease.execution-lease.invalid", "execution_lease must be an object \u2014 null and tombstone objects are invalid; writers delete the key on release")
4298
+ violation("medium", "COMPASS_INVALID_FIELD", "Compass frontmatter must be a YAML object with iteration_id / start_date / status / iteration_base_branch / target_branch (template: mstar-iteration \u00a71.3)", "Fix the frontmatter of {ITERATION_DIR}/<iteration-id>/delivery-compass.md")
4390
4299
  ]
4391
4300
  };
4392
4301
  }
4393
- validateNonEmptyString(violations, lease.holder, "holder", "lease.execution-lease.missing-holder", "lease.execution-lease.invalid-holder");
4394
- if (lease.claimed_at === undefined) {
4395
- violations.push(violation("high", "lease.execution-lease.missing-claimed-at", "missing required field: claimed_at"));
4396
- } else if (!isValidClaimedAt(lease.claimed_at)) {
4397
- violations.push(violation("medium", "lease.execution-lease.invalid-claimed-at", "claimed_at must be an RFC 3339 UTC timestamp with explicit Z (e.g. 2026-07-22T02:30:00Z) or a YYYY-MM-DD date"));
4398
- }
4399
- if (lease.worktree_path === undefined) {
4400
- violations.push(violation("high", "lease.execution-lease.missing-worktree-path", "missing required field: worktree_path"));
4401
- } else if (typeof lease.worktree_path !== "string" || lease.worktree_path.trim() === "") {
4402
- violations.push(violation("medium", "lease.execution-lease.invalid-worktree-path", "worktree_path must be a non-empty string"));
4403
- } else if (!isAbsolute3(lease.worktree_path)) {
4404
- violations.push(violation("medium", "lease.execution-lease.invalid-worktree-path", "worktree_path must be an absolute path \u2014 it identifies the dedicated feature-worktree root (and MUST differ from metadata.control_worktree_path)"));
4405
- }
4406
- validateNonEmptyString(violations, lease.working_branch, "working_branch", "lease.execution-lease.missing-working-branch", "lease.execution-lease.invalid-working-branch");
4407
- if (lease.session_label !== undefined && typeof lease.session_label !== "string") {
4408
- violations.push(violation("medium", "lease.execution-lease.invalid-session-label", "session_label must be a string (display only \u2014 never used for ownership comparison)"));
4409
- }
4410
- return { ok: violations.length === 0, violations };
4411
- }
4412
- function validateIntegrationMergeLease(lease) {
4413
- const violations = [];
4414
- if (!isPlainObject2(lease)) {
4302
+ const parsed = validateCompassShape(doc);
4303
+ if (!parsed.ok) {
4415
4304
  return {
4416
4305
  ok: false,
4417
- violations: [
4418
- violation("high", "lease.merge-lease.invalid", "integration_merge_lease must be an object \u2014 absent means unclaimed; null and tombstone objects are invalid; writers delete the key on release")
4419
- ]
4306
+ violations: parsed.issues.map((issue) => {
4307
+ const field = issue.path.join(".") || "(root)";
4308
+ return violation("medium", "COMPASS_INVALID_FIELD", `Compass frontmatter field '${field}' is invalid: ${issue.message}`, `Fix '${field}' in {ITERATION_DIR}/<iteration-id>/delivery-compass.md frontmatter (template: mstar-iteration \u00a71.3)`);
4309
+ })
4420
4310
  };
4421
4311
  }
4422
- validateNonEmptyString(violations, lease.holder, "holder", "lease.merge-lease.missing-holder", "lease.merge-lease.invalid-holder");
4423
- if (lease.claimed_at === undefined) {
4424
- violations.push(violation("high", "lease.merge-lease.missing-claimed-at", "missing required field: claimed_at"));
4425
- } else if (!isValidClaimedAt(lease.claimed_at)) {
4426
- violations.push(violation("medium", "lease.merge-lease.invalid-claimed-at", "claimed_at must be an RFC 3339 UTC timestamp with explicit Z (e.g. 2026-07-22T04:00:00Z) or a YYYY-MM-DD date"));
4312
+ const violations = [];
4313
+ const { status, end_date } = parsed.data;
4314
+ if (status === "completed" && end_date === undefined) {
4315
+ violations.push(violation("high", "COMPASS_END_DATE_REQUIRED", "Compass frontmatter status is 'completed' but end_date is missing \u2014 end_date is required at iteration-close (mstar-iteration \u00a73.4, template Fields guide)", "Add `end_date: YYYY-MM-DD` to the frontmatter"));
4427
4316
  }
4428
- validateNonEmptyString(violations, lease.plan_id, "plan_id", "lease.merge-lease.missing-plan-id", "lease.merge-lease.invalid-plan-id");
4429
- validateNonEmptyString(violations, lease.source_branch, "source_branch", "lease.merge-lease.missing-source-branch", "lease.merge-lease.invalid-source-branch");
4430
- validateNonEmptyString(violations, lease.target_branch, "target_branch", "lease.merge-lease.missing-target-branch", "lease.merge-lease.invalid-target-branch");
4431
- if (lease.session_label !== undefined && typeof lease.session_label !== "string") {
4432
- violations.push(violation("medium", "lease.merge-lease.invalid-session-label", "session_label must be a string (display only \u2014 never used for ownership comparison)"));
4317
+ if (status !== "completed" && end_date !== undefined) {
4318
+ violations.push(violation("medium", "COMPASS_END_DATE_NOT_ALLOWED", `Compass frontmatter sets end_date while status is '${status}' \u2014 end_date is only written at iteration-close (mstar-iteration \u00a73.4)`, "Remove end_date until iteration-close"));
4433
4319
  }
4434
4320
  return { ok: violations.length === 0, violations };
4435
4321
  }
4436
- var STATUS_WRITE_LOCKDIR = ".status-write.lockdir";
4437
- var LOCKDIR_HOLDER_PID = "holder.pid";
4438
- var heldLockDirs = new AsyncLocalStorage2;
4439
- async function withStatusWriteLock(statusPath, fn, opts = {}) {
4440
- const lockDir = join4(dirname4(resolve4(statusPath)), STATUS_WRITE_LOCKDIR);
4441
- const held = heldLockDirs.getStore();
4442
- if (held !== undefined && held.has(lockDir)) {
4443
- throw new Error(`${lockDir} is already held by this process in this async context \u2014 withStatusWriteLock is not reentrant; a nested acquisition on the same status.json is a bug`);
4322
+ function registeredPlanIds(compassDoc) {
4323
+ if (!Array.isArray(compassDoc.plans))
4324
+ return [];
4325
+ return compassDoc.plans.filter((plan) => typeof plan === "string" && plan.length > 0);
4326
+ }
4327
+ function findPlanRow(snapshotDoc, planId) {
4328
+ if (!Array.isArray(snapshotDoc.plans))
4329
+ return null;
4330
+ for (const row of snapshotDoc.plans) {
4331
+ if (!isPlainObject2(row))
4332
+ continue;
4333
+ const rowId = typeof row.id === "string" ? row.id : typeof row.plan_id === "string" ? row.plan_id : null;
4334
+ if (rowId === planId)
4335
+ return row;
4444
4336
  }
4445
- const timeoutMs = opts.timeoutMs ?? 30000;
4446
- const pollMs = opts.pollMs ?? 25;
4447
- const deadline = Date.now() + timeoutMs;
4448
- let acquired = null;
4449
- for (;; ) {
4450
- try {
4451
- mkdirSync3(lockDir);
4452
- const st = statSync3(lockDir);
4453
- acquired = { dev: st.dev, ino: st.ino };
4454
- break;
4337
+ return null;
4338
+ }
4339
+ function entryPlansAllDone(snapshotDoc, registered) {
4340
+ const violations = [];
4341
+ if (registered.length === 0) {
4342
+ violations.push(violation("medium", "COMPASS_NO_PLANS", "Compass frontmatter registers no plans \u2014 the all-plans-Done transition cannot be verified (mstar-iteration \u00a71.3 / Phase transition gates)", "List the iteration's plan ids in the compass frontmatter `plans`"));
4343
+ return violations;
4344
+ }
4345
+ for (const planId of registered) {
4346
+ const row = findPlanRow(snapshotDoc, planId);
4347
+ if (row === null) {
4348
+ violations.push(violation("high", "PLAN_NOT_IN_STATUS", `Plan '${planId}' is registered in the compass frontmatter but has no row in the workflow snapshot plans[] (mstar-iteration \u00a73.1 entry item 1)`, "Add the plan row to {HARNESS_DIR}/workflows/<id>/snapshot.json"));
4349
+ continue;
4350
+ }
4351
+ if (row.status !== PLAN_STATUS_DONE) {
4352
+ violations.push(violation("high", "PLAN_NOT_DONE", `Plan '${planId}' status is ${JSON.stringify(row.status)} in the workflow snapshot \u2014 all compass-registered plans must be 'Done' before iteration-close (mstar-iteration \u00a73.1 entry item 1)`));
4353
+ }
4354
+ }
4355
+ return violations;
4356
+ }
4357
+ function entryFrontmatterComplete(compassDoc) {
4358
+ return validateCompassFrontmatter(compassDoc).violations;
4359
+ }
4360
+ function exitFrontmatterClosed(compassDoc) {
4361
+ const violations = [];
4362
+ if (compassDoc.status !== "completed") {
4363
+ violations.push(violation("high", "EXIT_STATUS_NOT_COMPLETED", `Compass frontmatter status must be 'completed' at close exit \u2014 current: ${JSON.stringify(compassDoc.status)} (mstar-iteration \u00a73.4 / \u00a73.5 exit item 4)`));
4364
+ }
4365
+ const endDate = compassDoc.end_date;
4366
+ if (typeof endDate !== "string" || !DATE_RE.test(endDate)) {
4367
+ violations.push(violation("high", "EXIT_END_DATE_REQUIRED", "Compass frontmatter end_date (YYYY-MM-DD) is required when closing (mstar-iteration \u00a73.4 / \u00a73.5 exit item 4)"));
4368
+ }
4369
+ return violations;
4370
+ }
4371
+ function exitBranchCheck(opts) {
4372
+ const violations = [];
4373
+ const { currentBranch, specIntegrationBranch } = opts;
4374
+ if (currentBranch === undefined || specIntegrationBranch === undefined) {
4375
+ violations.push(violation("medium", "EXIT_BRANCH_UNVERIFIABLE", "Cannot verify the current branch is spec_integration_branch \u2014 missing currentBranch / specIntegrationBranch probe inputs (mstar-iteration \u00a73.5 exit item 5)"));
4376
+ } else if (currentBranch !== specIntegrationBranch) {
4377
+ violations.push(violation("high", "EXIT_BRANCH_MISMATCH", `Current branch '${currentBranch}' is not the spec_integration_branch '${specIntegrationBranch}' (mstar-iteration \u00a73.5 exit item 5)`));
4378
+ }
4379
+ return violations;
4380
+ }
4381
+ function exitPrBaseCheck(compassDoc, opts) {
4382
+ const violations = [];
4383
+ const target = compassDoc.target_branch;
4384
+ const { prBaseBranch } = opts;
4385
+ if (prBaseBranch === undefined) {
4386
+ violations.push(violation("medium", "EXIT_PR_BASE_UNVERIFIABLE", "Cannot verify the PR base \u2014 missing prBaseBranch probe input (mstar-iteration \u00a73.5 exit item 6)"));
4387
+ } else if (typeof target !== "string" || prBaseBranch !== target) {
4388
+ violations.push(violation("high", "EXIT_PR_BASE_MISMATCH", `PR base '${prBaseBranch}' must equal the compass target_branch '${String(target)}' \u2014 not an undocumented branch (mstar-iteration \u00a73.5 exit item 6)`));
4389
+ }
4390
+ return violations;
4391
+ }
4392
+ function evaluatePhaseGate(snapshotDoc, compassDoc, opts = {}) {
4393
+ const registered = registeredPlanIds(compassDoc);
4394
+ const entryViolations = [
4395
+ ...entryPlansAllDone(snapshotDoc, registered),
4396
+ ...entryFrontmatterComplete(compassDoc)
4397
+ ];
4398
+ const exitViolations = [
4399
+ ...exitFrontmatterClosed(compassDoc),
4400
+ ...exitBranchCheck(opts),
4401
+ ...exitPrBaseCheck(compassDoc, opts)
4402
+ ];
4403
+ const allPlansDone = registered.length > 0 && registered.every((planId) => {
4404
+ const row = findPlanRow(snapshotDoc, planId);
4405
+ return row !== null && row.status === PLAN_STATUS_DONE;
4406
+ });
4407
+ const entry = { ok: entryViolations.length === 0, violations: entryViolations };
4408
+ const exit = { ok: exitViolations.length === 0, violations: exitViolations };
4409
+ let transition;
4410
+ if (!allPlansDone)
4411
+ transition = "phase-2-execute";
4412
+ else if (entry.ok && exit.ok)
4413
+ transition = "phase-4-pr-delivery";
4414
+ else
4415
+ transition = "phase-3-close";
4416
+ const gateBlocking = allPlansDone ? [...entryViolations, ...exitViolations] : [];
4417
+ return {
4418
+ transition,
4419
+ allPlansDone,
4420
+ entry,
4421
+ exit,
4422
+ ok: gateBlocking.length === 0,
4423
+ violations: gateBlocking
4424
+ };
4425
+ }
4426
+ function pushCadenceProbe(ciRunning, reviewWaveActive) {
4427
+ const violations = [];
4428
+ if (ciRunning) {
4429
+ violations.push(violation("high", "PUSH_BLOCKED_CI", "CI checks are still queued/in_progress on the current head \u2014 do not push until the wave completes (mstar-iteration \u00a75.1a push gate 1)", "Wait for CI to settle, then push once with the whole local batch"));
4430
+ }
4431
+ if (reviewWaveActive) {
4432
+ violations.push(violation("high", "PUSH_BLOCKED_REVIEW_WAVE", "An AI/bot review wave is still running on the current head \u2014 do not push until it settles (mstar-iteration \u00a75.1a push gate 2)", "Wait for the review wave, then push once"));
4433
+ }
4434
+ return { ok: violations.length === 0, violations };
4435
+ }
4436
+ function parseCompassFrontmatter(filePath) {
4437
+ return parseCompassFrontmatterText(readFileSync3(filePath, "utf8"), filePath);
4438
+ }
4439
+ function parseCompassFrontmatterText(content, filePath) {
4440
+ const lines = content.split(/\r?\n/);
4441
+ if (lines[0]?.trim() !== "---") {
4442
+ throw new Error(`no YAML frontmatter fence in ${filePath} (expected first line "---")`);
4443
+ }
4444
+ const end = lines.indexOf("---", 1);
4445
+ if (end === -1) {
4446
+ throw new Error(`unterminated YAML frontmatter in ${filePath} (no closing "---")`);
4447
+ }
4448
+ const doc = {};
4449
+ let listKey = null;
4450
+ for (let i = 1;i < end; i += 1) {
4451
+ const line = lines[i] ?? "";
4452
+ if (!line.trim() || line.trim().startsWith("#"))
4453
+ continue;
4454
+ if (listKey !== null && /^\s*-\s+/.test(line)) {
4455
+ const item = line.replace(/^\s*-\s+/, "").trim().replace(/^["']|["']$/g, "");
4456
+ if (!Array.isArray(doc[listKey]))
4457
+ doc[listKey] = [];
4458
+ doc[listKey].push(item);
4459
+ continue;
4460
+ }
4461
+ listKey = null;
4462
+ const kv = line.match(/^([A-Za-z_][A-Za-z0-9_-]*):\s*(.*)$/);
4463
+ if (!kv) {
4464
+ throw new Error(`unsupported frontmatter line in ${filePath}: ${JSON.stringify(line)}`);
4465
+ }
4466
+ const value = kv[2].trim();
4467
+ doc[kv[1]] = value === "" ? null : /^\[.*\]$/.test(value) ? parseFlowArray(value, filePath) : value.replace(/^["']|["']$/g, "");
4468
+ listKey = value === "" ? kv[1] : null;
4469
+ }
4470
+ return doc;
4471
+ }
4472
+ function parseFlowArray(raw, filePath) {
4473
+ const inner = raw.slice(1, -1);
4474
+ if (/[[\]]/.test(inner)) {
4475
+ throw new Error(`nested flow-style array in ${filePath}: ${JSON.stringify(raw)} \u2014 only flat scalar items are supported (e.g. [a, b])`);
4476
+ }
4477
+ let quote = null;
4478
+ for (const ch of inner) {
4479
+ if (ch === '"' || ch === "'") {
4480
+ if (quote === null)
4481
+ quote = ch;
4482
+ else if (quote === ch)
4483
+ quote = null;
4484
+ } else if (ch === "," && quote !== null) {
4485
+ throw new Error(`ambiguous flow-style array in ${filePath}: ${JSON.stringify(raw)} \u2014 quoted item containing comma cannot be split unambiguously (flat scalar items only)`);
4486
+ }
4487
+ }
4488
+ if (quote !== null) {
4489
+ throw new Error(`unterminated ${quote} quote in flow-style array in ${filePath}: ${JSON.stringify(raw)}`);
4490
+ }
4491
+ const items = [];
4492
+ for (const part of inner.split(",")) {
4493
+ const item = part.trim().replace(/^["']|["']$/g, "");
4494
+ if (item === "")
4495
+ continue;
4496
+ items.push(item);
4497
+ }
4498
+ return items;
4499
+ }
4500
+ function isPlainObject22(value) {
4501
+ return typeof value === "object" && value !== null && !Array.isArray(value);
4502
+ }
4503
+ function violation2(severity, code, message, fix) {
4504
+ return { ok: false, severity, code, message, fix };
4505
+ }
4506
+ function validateNonEmptyString(violations, value, field, missingCode, invalidCode) {
4507
+ if (value === undefined) {
4508
+ violations.push(violation2("high", missingCode, `missing required field: ${field}`));
4509
+ } else if (typeof value !== "string" || value.trim() === "") {
4510
+ violations.push(violation2("medium", invalidCode, `${field} must be a non-empty string`));
4511
+ }
4512
+ }
4513
+ var DATE_PART = String.raw`\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12]\d|3[01])`;
4514
+ var RFC3339_Z_RE = new RegExp(String.raw`^${DATE_PART}T\d{2}:\d{2}:\d{2}(\.\d+)?Z$`);
4515
+ var DATE_ONLY_RE = new RegExp(String.raw`^${DATE_PART}$`);
4516
+ function isValidClaimedAt(value) {
4517
+ return typeof value === "string" && (RFC3339_Z_RE.test(value) || DATE_ONLY_RE.test(value));
4518
+ }
4519
+ function validateExecutionLease(lease) {
4520
+ const violations = [];
4521
+ if (!isPlainObject22(lease)) {
4522
+ return {
4523
+ ok: false,
4524
+ violations: [
4525
+ violation2("high", "lease.execution-lease.invalid", "execution_lease must be an object \u2014 null and tombstone objects are invalid; writers delete the key on release")
4526
+ ]
4527
+ };
4528
+ }
4529
+ validateNonEmptyString(violations, lease.holder, "holder", "lease.execution-lease.missing-holder", "lease.execution-lease.invalid-holder");
4530
+ if (lease.claimed_at === undefined) {
4531
+ violations.push(violation2("high", "lease.execution-lease.missing-claimed-at", "missing required field: claimed_at"));
4532
+ } else if (!isValidClaimedAt(lease.claimed_at)) {
4533
+ violations.push(violation2("medium", "lease.execution-lease.invalid-claimed-at", "claimed_at must be an RFC 3339 UTC timestamp with explicit Z (e.g. 2026-07-22T02:30:00Z) or a YYYY-MM-DD date"));
4534
+ }
4535
+ if (lease.worktree_path === undefined) {
4536
+ violations.push(violation2("high", "lease.execution-lease.missing-worktree-path", "missing required field: worktree_path"));
4537
+ } else if (typeof lease.worktree_path !== "string" || lease.worktree_path.trim() === "") {
4538
+ violations.push(violation2("medium", "lease.execution-lease.invalid-worktree-path", "worktree_path must be a non-empty string"));
4539
+ } else if (!isAbsolute2(lease.worktree_path)) {
4540
+ violations.push(violation2("medium", "lease.execution-lease.invalid-worktree-path", "worktree_path must be an absolute path \u2014 it identifies the dedicated feature-worktree root (and MUST differ from metadata.control_worktree_path)"));
4541
+ }
4542
+ validateNonEmptyString(violations, lease.working_branch, "working_branch", "lease.execution-lease.missing-working-branch", "lease.execution-lease.invalid-working-branch");
4543
+ if (lease.session_label !== undefined && typeof lease.session_label !== "string") {
4544
+ violations.push(violation2("medium", "lease.execution-lease.invalid-session-label", "session_label must be a string (display only \u2014 never used for ownership comparison)"));
4545
+ }
4546
+ return { ok: violations.length === 0, violations };
4547
+ }
4548
+ function validateIntegrationMergeLease(lease) {
4549
+ const violations = [];
4550
+ if (!isPlainObject22(lease)) {
4551
+ return {
4552
+ ok: false,
4553
+ violations: [
4554
+ violation2("high", "lease.merge-lease.invalid", "integration_merge_lease must be an object \u2014 absent means unclaimed; null and tombstone objects are invalid; writers delete the key on release")
4555
+ ]
4556
+ };
4557
+ }
4558
+ validateNonEmptyString(violations, lease.holder, "holder", "lease.merge-lease.missing-holder", "lease.merge-lease.invalid-holder");
4559
+ if (lease.claimed_at === undefined) {
4560
+ violations.push(violation2("high", "lease.merge-lease.missing-claimed-at", "missing required field: claimed_at"));
4561
+ } else if (!isValidClaimedAt(lease.claimed_at)) {
4562
+ violations.push(violation2("medium", "lease.merge-lease.invalid-claimed-at", "claimed_at must be an RFC 3339 UTC timestamp with explicit Z (e.g. 2026-07-22T04:00:00Z) or a YYYY-MM-DD date"));
4563
+ }
4564
+ validateNonEmptyString(violations, lease.plan_id, "plan_id", "lease.merge-lease.missing-plan-id", "lease.merge-lease.invalid-plan-id");
4565
+ validateNonEmptyString(violations, lease.source_branch, "source_branch", "lease.merge-lease.missing-source-branch", "lease.merge-lease.invalid-source-branch");
4566
+ validateNonEmptyString(violations, lease.target_branch, "target_branch", "lease.merge-lease.missing-target-branch", "lease.merge-lease.invalid-target-branch");
4567
+ if (lease.session_label !== undefined && typeof lease.session_label !== "string") {
4568
+ violations.push(violation2("medium", "lease.merge-lease.invalid-session-label", "session_label must be a string (display only \u2014 never used for ownership comparison)"));
4569
+ }
4570
+ return { ok: violations.length === 0, violations };
4571
+ }
4572
+ var STATUS_WRITE_LOCKDIR = ".status-write.lockdir";
4573
+ var LOCKDIR_HOLDER_PID = "holder.pid";
4574
+ var heldLockDirs = new AsyncLocalStorage2;
4575
+ async function withStatusWriteLock(statusPath, fn, opts = {}) {
4576
+ const lockDir = join4(dirname3(resolve3(statusPath)), STATUS_WRITE_LOCKDIR);
4577
+ const held = heldLockDirs.getStore();
4578
+ if (held !== undefined && held.has(lockDir)) {
4579
+ throw new Error(`${lockDir} is already held by this process in this async context \u2014 withStatusWriteLock is not reentrant; a nested acquisition on the same status.json is a bug`);
4580
+ }
4581
+ const timeoutMs = opts.timeoutMs ?? 30000;
4582
+ const pollMs = opts.pollMs ?? 25;
4583
+ const deadline = Date.now() + timeoutMs;
4584
+ let acquired = null;
4585
+ for (;; ) {
4586
+ try {
4587
+ mkdirSync2(lockDir);
4588
+ const st = statSync2(lockDir);
4589
+ acquired = { dev: st.dev, ino: st.ino };
4590
+ break;
4455
4591
  } catch (error) {
4456
4592
  if (error.code !== "EEXIST")
4457
4593
  throw error;
@@ -4471,7 +4607,7 @@ async function withStatusWriteLock(statusPath, fn, opts = {}) {
4471
4607
  } finally {
4472
4608
  owns.delete(lockDir);
4473
4609
  try {
4474
- const current = statSync3(lockDir);
4610
+ const current = statSync2(lockDir);
4475
4611
  if (acquired !== null && current.dev === acquired.dev && current.ino === acquired.ino) {
4476
4612
  try {
4477
4613
  unlinkSync2(join4(lockDir, LOCKDIR_HOLDER_PID));
@@ -4487,7 +4623,7 @@ var REQUIRED_FIELDS = [
4487
4623
  { key: "delegation", label: "Delegation", code: "delegation" },
4488
4624
  { key: "taskCategory", label: "Task category", code: "task-category" }
4489
4625
  ];
4490
- function violation2(severity, code, message, fix) {
4626
+ function violation3(severity, code, message, fix) {
4491
4627
  return { ok: false, severity, code, message, fix };
4492
4628
  }
4493
4629
  function parseAssignmentFields(assignmentText) {
@@ -4512,11 +4648,11 @@ function parseAssignmentFields(assignmentText) {
4512
4648
  }
4513
4649
  function requireField(violations, value, label, code) {
4514
4650
  if (value === undefined) {
4515
- const v = violation2("high", `assignment.field.missing-${code}`, `missing required Assignment field: ${label}`, `add "**${label}**: <value>" to the Assignment`);
4651
+ const v = violation3("high", `assignment.field.missing-${code}`, `missing required Assignment field: ${label}`, `add "**${label}**: <value>" to the Assignment`);
4516
4652
  v.aliases = [`assignment.presence.missing-${code}`];
4517
4653
  violations.push(v);
4518
4654
  } else if (value === "") {
4519
- const v = violation2("high", `assignment.field.invalid-${code}`, `${label} must be non-empty`, `fill in "**${label}**: <value>"`);
4655
+ const v = violation3("high", `assignment.field.invalid-${code}`, `${label} must be non-empty`, `fill in "**${label}**: <value>"`);
4520
4656
  v.aliases = [`assignment.presence.missing-${code}`];
4521
4657
  violations.push(v);
4522
4658
  }
@@ -4575,20 +4711,20 @@ function validateAssignmentFields(assignmentText, opts = {}) {
4575
4711
  const formCount = Number(workingPresent) + Number(policyPresent);
4576
4712
  const forms = parseAssignmentBranchForms(assignmentText);
4577
4713
  if (formCount === 0) {
4578
- violations.push(violation2("high", "assignment.field.branch-missing", "writable assignment must contain exactly one branch form", `add exactly one of: ${BRANCH_FORMS_HINT}`));
4714
+ violations.push(violation3("high", "assignment.field.branch-missing", "writable assignment must contain exactly one branch form", `add exactly one of: ${BRANCH_FORMS_HINT}`));
4579
4715
  } else if (formCount > 1) {
4580
- violations.push(violation2("high", "assignment.field.branch-multiple", `writable assignment contains ${formCount} branch forms (Working branch + Branch policy) \u2014 exactly one required`, `keep exactly one of: ${BRANCH_FORMS_HINT}`));
4716
+ violations.push(violation3("high", "assignment.field.branch-multiple", `writable assignment contains ${formCount} branch forms (Working branch + Branch policy) \u2014 exactly one required`, `keep exactly one of: ${BRANCH_FORMS_HINT}`));
4581
4717
  } else if (workingPresent) {
4582
4718
  const create = forms.createForm;
4583
4719
  if (create !== undefined && (create.base === undefined || create.base.trim() === "" || create.name.trim() === "")) {
4584
- violations.push(violation2("high", "assignment.field.branch-missing-base", `create-form Working branch is incomplete: "${fields.workingBranch}" (expected "create <new-branch> from <base>")`, "write both the new branch name and the ancestor branch after `from` (main / existing feature branch / remote-tracking branch / `current`)"));
4720
+ violations.push(violation3("high", "assignment.field.branch-missing-base", `create-form Working branch is incomplete: "${fields.workingBranch}" (expected "create <new-branch> from <base>")`, "write both the new branch name and the ancestor branch after `from` (main / existing feature branch / remote-tracking branch / `current`)"));
4585
4721
  }
4586
4722
  } else if (policyPresent) {
4587
4723
  const direct = forms.directOn;
4588
4724
  if (direct === undefined) {
4589
- violations.push(violation2("high", "assignment.field.branch-policy-missing-branch", `unparseable Branch policy: "${fields.branchPolicy}" (expected "direct on <branch> \u2014 <reason>")`, "start the field with `direct on <branch>`"));
4725
+ violations.push(violation3("high", "assignment.field.branch-policy-missing-branch", `unparseable Branch policy: "${fields.branchPolicy}" (expected "direct on <branch> \u2014 <reason>")`, "start the field with `direct on <branch>`"));
4590
4726
  } else if (direct.reason === "") {
4591
- violations.push(violation2("high", "assignment.field.branch-policy-missing-reason", `Branch policy "direct on ${direct.branch}" is missing the reason`, 'append "\u2014 <reason>" after the branch name'));
4727
+ violations.push(violation3("high", "assignment.field.branch-policy-missing-reason", `Branch policy "direct on ${direct.branch}" is missing the reason`, 'append "\u2014 <reason>" after the branch name'));
4592
4728
  }
4593
4729
  }
4594
4730
  }
@@ -4599,7 +4735,7 @@ function assertDefaultBranchProtected(branch, opts = {}) {
4599
4735
  const violations = [];
4600
4736
  const normalized = branch.trim();
4601
4737
  if (normalized !== "" && defaultBranches.includes(normalized) && opts.directOnException !== true) {
4602
- violations.push(violation2("high", "dispatch.default-branch.protected", `writable work on default protected branch "${normalized}" requires an explicit direct-on exception`, `add "Branch policy: direct on ${normalized} \u2014 <reason>" to the Assignment, or use a feature branch`));
4738
+ violations.push(violation3("high", "dispatch.default-branch.protected", `writable work on default protected branch "${normalized}" requires an explicit direct-on exception`, `add "Branch policy: direct on ${normalized} \u2014 <reason>" to the Assignment, or use a feature branch`));
4603
4739
  }
4604
4740
  return { ok: violations.length === 0, violations };
4605
4741
  }
@@ -4608,7 +4744,7 @@ function executionModeToN(executionMode, opts = {}) {
4608
4744
  const mode = executionMode.trim().toLowerCase().split(/\s+/)[0] ?? "";
4609
4745
  let n;
4610
4746
  if (mode === "") {
4611
- violations.push(violation2("high", "dispatch.execution-mode.missing", "missing required Assignment field: Execution mode", 'add "**Execution mode**: sdd | inline | targeted"'));
4747
+ violations.push(violation3("high", "dispatch.execution-mode.missing", "missing required Assignment field: Execution mode", 'add "**Execution mode**: sdd | inline | targeted"'));
4612
4748
  } else if (mode === "sdd") {
4613
4749
  n = 3;
4614
4750
  } else if (mode === "inline") {
@@ -4616,14 +4752,14 @@ function executionModeToN(executionMode, opts = {}) {
4616
4752
  } else if (mode === "targeted") {
4617
4753
  const seats = [...new Set((opts.seats ?? []).map((s) => s.trim()).filter((s) => s !== ""))];
4618
4754
  if (seats.length === 0) {
4619
- violations.push(violation2("high", "dispatch.execution-mode.missing-seats", 'execution mode "targeted" requires listed reviewer seats', 'add "QC re-review: targeted \u2014 reviewers: <role-id>, \u2026" to the Assignment and pass the seats'));
4755
+ violations.push(violation3("high", "dispatch.execution-mode.missing-seats", 'execution mode "targeted" requires listed reviewer seats', 'add "QC re-review: targeted \u2014 reviewers: <role-id>, \u2026" to the Assignment and pass the seats'));
4620
4756
  } else if (seats.length > 3) {
4621
- violations.push(violation2("high", "dispatch.execution-mode.too-many-seats", `execution mode "targeted" lists ${seats.length} reviewer seats \u2014 at most 3 (targeted re-review seats are the tri seats, N = 1\u20133)`, "list at most three reviewer seats for the targeted re-review"));
4757
+ violations.push(violation3("high", "dispatch.execution-mode.too-many-seats", `execution mode "targeted" lists ${seats.length} reviewer seats \u2014 at most 3 (targeted re-review seats are the tri seats, N = 1\u20133)`, "list at most three reviewer seats for the targeted re-review"));
4622
4758
  } else {
4623
4759
  n = seats.length;
4624
4760
  }
4625
4761
  } else {
4626
- violations.push(violation2("high", "dispatch.execution-mode.unknown", `unknown execution mode "${executionMode.trim()}" (expected sdd | inline | targeted)`, "fix the Execution mode field"));
4762
+ violations.push(violation3("high", "dispatch.execution-mode.unknown", `unknown execution mode "${executionMode.trim()}" (expected sdd | inline | targeted)`, "fix the Execution mode field"));
4627
4763
  }
4628
4764
  return n === undefined ? { ok: false, violations } : { ok: true, violations, n };
4629
4765
  }
@@ -4637,7 +4773,7 @@ function assertTriIdentity(reviewerRoles) {
4637
4773
  return {
4638
4774
  ok: false,
4639
4775
  violations: [
4640
- violation2("high", "dispatch.tri-identity.invalid", `tri-review initial wave must be exactly qc-specialist / qc-specialist-2 / qc-specialist-3, got: ${got}`, "dispatch qc-specialist, qc-specialist-2 and qc-specialist-3 for the initial wave")
4776
+ violation3("high", "dispatch.tri-identity.invalid", `tri-review initial wave must be exactly qc-specialist / qc-specialist-2 / qc-specialist-3, got: ${got}`, "dispatch qc-specialist, qc-specialist-2 and qc-specialist-3 for the initial wave")
4641
4777
  ]
4642
4778
  };
4643
4779
  }
@@ -4645,45 +4781,45 @@ var WORKFLOW_SNAPSHOT_FILE = "snapshot.json";
4645
4781
  var WORKFLOW_LIFECYCLE_STATUSES = ["running", "paused", "completed", "failed", "stopped"];
4646
4782
  var WORKFLOW_TERMINAL_STATUSES = ["completed", "failed", "stopped"];
4647
4783
  var WORKFLOW_LIFECYCLE_TYPES = ["plan", "iteration"];
4648
- function isPlainObject22(value) {
4784
+ function isPlainObject3(value) {
4649
4785
  return typeof value === "object" && value !== null && !Array.isArray(value);
4650
4786
  }
4651
- function violation3(severity, code, message, fix) {
4787
+ function violation4(severity, code, message, fix) {
4652
4788
  return { ok: false, severity, code, message, fix };
4653
4789
  }
4654
4790
  function validateNonEmptyString2(violations, value, field, missingCode, invalidCode) {
4655
4791
  if (value === undefined) {
4656
- violations.push(violation3("high", missingCode, `missing required field: ${field}`));
4792
+ violations.push(violation4("high", missingCode, `missing required field: ${field}`));
4657
4793
  } else if (typeof value !== "string" || value.trim() === "") {
4658
- violations.push(violation3("medium", invalidCode, `${field} must be a non-empty string`));
4794
+ violations.push(violation4("medium", invalidCode, `${field} must be a non-empty string`));
4659
4795
  }
4660
4796
  }
4661
4797
  function validateWorkflowSnapshot(doc) {
4662
4798
  const violations = [];
4663
- if (!isPlainObject22(doc)) {
4799
+ if (!isPlainObject3(doc)) {
4664
4800
  return {
4665
4801
  ok: false,
4666
- violations: [violation3("high", "workflow.snapshot.invalid", "workflow snapshot must be an object")]
4802
+ violations: [violation4("high", "workflow.snapshot.invalid", "workflow snapshot must be an object")]
4667
4803
  };
4668
4804
  }
4669
4805
  if (doc.schema_version === undefined) {
4670
- violations.push(violation3("high", "workflow.snapshot.missing-schema-version", "missing required field: schema_version"));
4806
+ violations.push(violation4("high", "workflow.snapshot.missing-schema-version", "missing required field: schema_version"));
4671
4807
  } else if (doc.schema_version !== 1) {
4672
- violations.push(violation3("high", "workflow.snapshot.invalid-schema-version", `schema_version must be 1 \u2014 got ${JSON.stringify(doc.schema_version)} (version is reserved for the root file discriminator)`));
4808
+ violations.push(violation4("high", "workflow.snapshot.invalid-schema-version", `schema_version must be 1 \u2014 got ${JSON.stringify(doc.schema_version)} (version is reserved for the root file discriminator)`));
4673
4809
  }
4674
4810
  if (doc.version !== undefined) {
4675
- violations.push(violation3("medium", "workflow.snapshot.reserved-version", `top-level version is reserved for the root status.json discriminator \u2014 snapshots use schema_version; remove the version key (got ${JSON.stringify(doc.version)})`, "remove the version key from the snapshot"));
4811
+ violations.push(violation4("medium", "workflow.snapshot.reserved-version", `top-level version is reserved for the root status.json discriminator \u2014 snapshots use schema_version; remove the version key (got ${JSON.stringify(doc.version)})`, "remove the version key from the snapshot"));
4676
4812
  }
4677
4813
  validateNonEmptyString2(violations, doc.id, "id", "workflow.snapshot.missing-id", "workflow.snapshot.invalid-id");
4678
4814
  if (doc.type === undefined) {
4679
- violations.push(violation3("high", "workflow.snapshot.missing-type", "missing required field: type"));
4815
+ violations.push(violation4("high", "workflow.snapshot.missing-type", "missing required field: type"));
4680
4816
  } else if (typeof doc.type !== "string" || !WORKFLOW_LIFECYCLE_TYPES.includes(doc.type)) {
4681
- violations.push(violation3("medium", "workflow.snapshot.invalid-type", `type must be one of ${WORKFLOW_LIFECYCLE_TYPES.join(" | ")} \u2014 got ${JSON.stringify(doc.type)}`));
4817
+ violations.push(violation4("medium", "workflow.snapshot.invalid-type", `type must be one of ${WORKFLOW_LIFECYCLE_TYPES.join(" | ")} \u2014 got ${JSON.stringify(doc.type)}`));
4682
4818
  }
4683
4819
  if (doc.status === undefined) {
4684
- violations.push(violation3("high", "workflow.snapshot.missing-status", "missing required field: status"));
4820
+ violations.push(violation4("high", "workflow.snapshot.missing-status", "missing required field: status"));
4685
4821
  } else if (typeof doc.status !== "string" || !WORKFLOW_LIFECYCLE_STATUSES.includes(doc.status)) {
4686
- violations.push(violation3("medium", "workflow.snapshot.invalid-status", `status must be one of ${WORKFLOW_LIFECYCLE_STATUSES.join(" | ")} \u2014 got ${JSON.stringify(doc.status)}`));
4822
+ violations.push(violation4("medium", "workflow.snapshot.invalid-status", `status must be one of ${WORKFLOW_LIFECYCLE_STATUSES.join(" | ")} \u2014 got ${JSON.stringify(doc.status)}`));
4687
4823
  }
4688
4824
  validateNonEmptyString2(violations, doc.started_at, "started_at", "workflow.snapshot.missing-started-at", "workflow.snapshot.invalid-started-at");
4689
4825
  validateNonEmptyString2(violations, doc.updated_at, "updated_at", "workflow.snapshot.missing-updated-at", "workflow.snapshot.invalid-updated-at");
@@ -4691,35 +4827,35 @@ function validateWorkflowSnapshot(doc) {
4691
4827
  validateNonEmptyString2(violations, doc.ended_at, "ended_at", "workflow.snapshot.missing-ended-at", "workflow.snapshot.invalid-ended-at");
4692
4828
  }
4693
4829
  if (doc.phase !== undefined && typeof doc.phase !== "string") {
4694
- violations.push(violation3("medium", "workflow.snapshot.invalid-phase", "phase must be a string (free-form phase machine label)"));
4830
+ violations.push(violation4("medium", "workflow.snapshot.invalid-phase", "phase must be a string (free-form phase machine label)"));
4695
4831
  }
4696
4832
  if (doc.plans === undefined) {
4697
- violations.push(violation3("high", "workflow.snapshot.missing-plans", "missing required field: plans"));
4833
+ violations.push(violation4("high", "workflow.snapshot.missing-plans", "missing required field: plans"));
4698
4834
  } else if (!Array.isArray(doc.plans)) {
4699
- violations.push(violation3("high", "workflow.snapshot.invalid-plans", "plans must be an array of legacy plan rows"));
4835
+ violations.push(violation4("high", "workflow.snapshot.invalid-plans", "plans must be an array of legacy plan rows"));
4700
4836
  } else {
4701
4837
  for (const row of doc.plans) {
4702
4838
  violations.push(...validatePlanRow(row).violations);
4703
- if (isPlainObject22(row) && row.execution_lease !== undefined) {
4839
+ if (isPlainObject3(row) && row.execution_lease !== undefined) {
4704
4840
  violations.push(...validateExecutionLease(row.execution_lease).violations);
4705
4841
  }
4706
4842
  }
4707
4843
  }
4708
4844
  if (doc.execution_policy !== undefined) {
4709
- if (!isPlainObject22(doc.execution_policy)) {
4710
- violations.push(violation3("medium", "workflow.snapshot.invalid-execution-policy", "execution_policy must be an object"));
4845
+ if (!isPlainObject3(doc.execution_policy)) {
4846
+ violations.push(violation4("medium", "workflow.snapshot.invalid-execution-policy", "execution_policy must be an object"));
4711
4847
  }
4712
4848
  }
4713
4849
  if (doc.integration_merge_lease !== undefined) {
4714
4850
  violations.push(...validateIntegrationMergeLease(doc.integration_merge_lease).violations);
4715
4851
  }
4716
4852
  if (doc.branch !== undefined) {
4717
- if (!isPlainObject22(doc.branch)) {
4718
- violations.push(violation3("medium", "workflow.snapshot.invalid-branch", "branch must be an object"));
4853
+ if (!isPlainObject3(doc.branch)) {
4854
+ violations.push(violation4("medium", "workflow.snapshot.invalid-branch", "branch must be an object"));
4719
4855
  } else {
4720
4856
  for (const key of ["base", "integration", "target"]) {
4721
4857
  if (doc.branch[key] !== undefined && (typeof doc.branch[key] !== "string" || doc.branch[key].trim() === "")) {
4722
- violations.push(violation3("medium", "workflow.snapshot.invalid-branch", `branch.${key} must be a non-empty string`));
4858
+ violations.push(violation4("medium", "workflow.snapshot.invalid-branch", `branch.${key} must be a non-empty string`));
4723
4859
  }
4724
4860
  }
4725
4861
  }
@@ -4727,8 +4863,8 @@ function validateWorkflowSnapshot(doc) {
4727
4863
  if (doc.control_worktree_path !== undefined) {
4728
4864
  validateNonEmptyString2(violations, doc.control_worktree_path, "control_worktree_path", "workflow.snapshot.missing-control-worktree-path", "workflow.snapshot.invalid-control-worktree-path");
4729
4865
  }
4730
- if (doc.legacy_metadata !== undefined && !isPlainObject22(doc.legacy_metadata)) {
4731
- violations.push(violation3("medium", "workflow.snapshot.invalid-legacy-metadata", "legacy_metadata must be an object"));
4866
+ if (doc.legacy_metadata !== undefined && !isPlainObject3(doc.legacy_metadata)) {
4867
+ violations.push(violation4("medium", "workflow.snapshot.invalid-legacy-metadata", "legacy_metadata must be an object"));
4732
4868
  }
4733
4869
  if (doc.compass_ref !== undefined) {
4734
4870
  validateNonEmptyString2(violations, doc.compass_ref, "compass_ref", "workflow.snapshot.missing-compass-ref", "workflow.snapshot.invalid-compass-ref");
@@ -4736,27 +4872,27 @@ function validateWorkflowSnapshot(doc) {
4736
4872
  const terminal = typeof doc.status === "string" && WORKFLOW_TERMINAL_STATUSES.includes(doc.status);
4737
4873
  if (terminal) {
4738
4874
  if (doc.ended_at === undefined) {
4739
- violations.push(violation3("high", "workflow.snapshot.missing-ended-at", `terminal status ${JSON.stringify(doc.status)} requires ended_at \u2014 a terminal snapshot must record when the lifecycle ended`));
4875
+ violations.push(violation4("high", "workflow.snapshot.missing-ended-at", `terminal status ${JSON.stringify(doc.status)} requires ended_at \u2014 a terminal snapshot must record when the lifecycle ended`));
4740
4876
  }
4741
4877
  if (Array.isArray(doc.plans)) {
4742
4878
  for (const row of doc.plans) {
4743
- if (isPlainObject22(row) && row.execution_lease !== undefined) {
4744
- violations.push(violation3("high", "workflow.snapshot.terminal-dangling-execution-lease", `terminal snapshot must not carry a row execution_lease (dangling lease) \u2014 release every lease before the lifecycle ends`));
4879
+ if (isPlainObject3(row) && row.execution_lease !== undefined) {
4880
+ violations.push(violation4("high", "workflow.snapshot.terminal-dangling-execution-lease", `terminal snapshot must not carry a row execution_lease (dangling lease) \u2014 release every lease before the lifecycle ends`));
4745
4881
  }
4746
4882
  }
4747
4883
  }
4748
4884
  if (doc.integration_merge_lease !== undefined) {
4749
- violations.push(violation3("high", "workflow.snapshot.terminal-dangling-merge-lease", "terminal snapshot must not carry integration_merge_lease (dangling lease) \u2014 release the merge lease before the lifecycle ends"));
4885
+ violations.push(violation4("high", "workflow.snapshot.terminal-dangling-merge-lease", "terminal snapshot must not carry integration_merge_lease (dangling lease) \u2014 release the merge lease before the lifecycle ends"));
4750
4886
  }
4751
4887
  }
4752
4888
  return { ok: violations.length === 0, violations };
4753
4889
  }
4754
- var DATE_RE = /^\d{4}-\d{2}-\d{2}$/;
4890
+ var DATE_RE2 = /^\d{4}-\d{2}-\d{2}$/;
4755
4891
  var PLAN_STATUSES = ["Todo", "InProgress", "InReview", "Blocked", "Done"];
4756
- function isPlainObject3(value) {
4892
+ function isPlainObject4(value) {
4757
4893
  return typeof value === "object" && value !== null && !Array.isArray(value);
4758
4894
  }
4759
- function violation4(severity, code, message, fix) {
4895
+ function violation5(severity, code, message, fix) {
4760
4896
  return { ok: false, severity, code, message, fix };
4761
4897
  }
4762
4898
  function todayString() {
@@ -4779,19 +4915,19 @@ function isOpenResidual(entry) {
4779
4915
  }
4780
4916
  function validateNonEmptyString3(violations, value, field, missingCode, invalidCode) {
4781
4917
  if (value === undefined) {
4782
- violations.push(violation4("high", missingCode, `missing required field: ${field}`));
4918
+ violations.push(violation5("high", missingCode, `missing required field: ${field}`));
4783
4919
  } else if (typeof value !== "string" || value.trim() === "") {
4784
- violations.push(violation4("medium", invalidCode, `${field} must be a non-empty string`));
4920
+ violations.push(violation5("medium", invalidCode, `${field} must be a non-empty string`));
4785
4921
  }
4786
4922
  }
4787
4923
  function validatePlanRow(row) {
4788
4924
  const violations = [];
4789
- if (!isPlainObject3(row)) {
4790
- return { ok: false, violations: [violation4("high", "status.plan-row.invalid", "plan row must be an object")] };
4925
+ if (!isPlainObject4(row)) {
4926
+ return { ok: false, violations: [violation5("high", "status.plan-row.invalid", "plan row must be an object")] };
4791
4927
  }
4792
4928
  const { id, plan_id: planId, title, file, status, metadata, execution_lease } = row;
4793
4929
  if (id === undefined && planId === undefined) {
4794
- violations.push(violation4("high", "status.plan-row.missing-id", "missing required field: id (or legacy plan_id)"));
4930
+ violations.push(violation5("high", "status.plan-row.missing-id", "missing required field: id (or legacy plan_id)"));
4795
4931
  } else {
4796
4932
  if (id !== undefined) {
4797
4933
  validateNonEmptyString3(violations, id, "id", "status.plan-row.missing-id", "status.plan-row.invalid-id");
@@ -4800,24 +4936,24 @@ function validatePlanRow(row) {
4800
4936
  validateNonEmptyString3(violations, planId, "plan_id", "status.plan-row.missing-plan-id", "status.plan-row.invalid-plan-id");
4801
4937
  }
4802
4938
  if (id !== undefined && planId !== undefined && id !== planId) {
4803
- violations.push(violation4("medium", "status.plan-row.dual-id", "row has both id and plan_id with different values \u2014 write one canonical key (prefer id)"));
4939
+ violations.push(violation5("medium", "status.plan-row.dual-id", "row has both id and plan_id with different values \u2014 write one canonical key (prefer id)"));
4804
4940
  }
4805
4941
  }
4806
4942
  validateNonEmptyString3(violations, title, "title", "status.plan-row.missing-title", "status.plan-row.invalid-title");
4807
4943
  validateNonEmptyString3(violations, file, "file", "status.plan-row.missing-file", "status.plan-row.invalid-file");
4808
4944
  if (status === undefined) {
4809
- violations.push(violation4("high", "status.plan-row.missing-status", "missing required field: status"));
4945
+ violations.push(violation5("high", "status.plan-row.missing-status", "missing required field: status"));
4810
4946
  } else if (typeof status !== "string" || !PLAN_STATUSES.includes(status)) {
4811
- violations.push(violation4("medium", "status.plan-row.invalid-status", `status must be one of ${PLAN_STATUSES.join(" | ")} \u2014 got ${JSON.stringify(status)}`));
4947
+ violations.push(violation5("medium", "status.plan-row.invalid-status", `status must be one of ${PLAN_STATUSES.join(" | ")} \u2014 got ${JSON.stringify(status)}`));
4812
4948
  }
4813
- if (metadata !== undefined && !isPlainObject3(metadata)) {
4814
- violations.push(violation4("medium", "status.plan-row.invalid-metadata", "metadata must be an object"));
4949
+ if (metadata !== undefined && !isPlainObject4(metadata)) {
4950
+ violations.push(violation5("medium", "status.plan-row.invalid-metadata", "metadata must be an object"));
4815
4951
  }
4816
- if (execution_lease !== undefined && !isPlainObject3(execution_lease)) {
4817
- violations.push(violation4("medium", "status.plan-row.invalid-execution-lease", "execution_lease must be an object"));
4952
+ if (execution_lease !== undefined && !isPlainObject4(execution_lease)) {
4953
+ violations.push(violation5("medium", "status.plan-row.invalid-execution-lease", "execution_lease must be an object"));
4818
4954
  }
4819
4955
  if (status === "Done" && execution_lease !== undefined) {
4820
- violations.push(violation4("medium", "status.plan-row.done-with-lease", 'plan status Done must not carry an execution_lease \u2014 the Done authority deletes the lease in the same complete-file update as status: "Done" (status-and-residuals.md \u00a7 Hold, release, and override)', 'delete plans[].execution_lease in the same update that sets status: "Done"'));
4956
+ violations.push(violation5("medium", "status.plan-row.done-with-lease", 'plan status Done must not carry an execution_lease \u2014 the Done authority deletes the lease in the same complete-file update as status: "Done" (status-and-residuals.md \u00a7 Hold, release, and override)', 'delete plans[].execution_lease in the same update that sets status: "Done"'));
4821
4957
  }
4822
4958
  return { ok: violations.length === 0, violations };
4823
4959
  }
@@ -4830,25 +4966,25 @@ function isHarnessRelativePath(dir) {
4830
4966
  }
4831
4967
  function validateWorkflowEntry(entry) {
4832
4968
  const violations = [];
4833
- if (!isPlainObject3(entry)) {
4969
+ if (!isPlainObject4(entry)) {
4834
4970
  return {
4835
4971
  ok: false,
4836
- violations: [violation4("high", "status.workflow.invalid", "workflow entry must be an object")]
4972
+ violations: [violation5("high", "status.workflow.invalid", "workflow entry must be an object")]
4837
4973
  };
4838
4974
  }
4839
4975
  validateNonEmptyString3(violations, entry.id, "id", "status.workflow.missing-id", "status.workflow.invalid-id");
4840
4976
  if (entry.type === undefined) {
4841
- violations.push(violation4("high", "status.workflow.missing-type", "missing required field: type"));
4977
+ violations.push(violation5("high", "status.workflow.missing-type", "missing required field: type"));
4842
4978
  } else if (typeof entry.type !== "string" || !WORKFLOW_LIFECYCLE_TYPES.includes(entry.type)) {
4843
- violations.push(violation4("medium", "status.workflow.invalid-type", `type must be one of ${WORKFLOW_LIFECYCLE_TYPES.join(" | ")} \u2014 got ${JSON.stringify(entry.type)}`));
4979
+ violations.push(violation5("medium", "status.workflow.invalid-type", `type must be one of ${WORKFLOW_LIFECYCLE_TYPES.join(" | ")} \u2014 got ${JSON.stringify(entry.type)}`));
4844
4980
  }
4845
4981
  validateNonEmptyString3(violations, entry.started_at, "started_at", "status.workflow.missing-started-at", "status.workflow.invalid-started-at");
4846
4982
  if (entry.dir === undefined) {
4847
- violations.push(violation4("high", "status.workflow.missing-dir", "missing required field: dir"));
4983
+ violations.push(violation5("high", "status.workflow.missing-dir", "missing required field: dir"));
4848
4984
  } else if (typeof entry.dir !== "string" || entry.dir.trim() === "") {
4849
- violations.push(violation4("medium", "status.workflow.invalid-dir", "dir must be a non-empty string"));
4985
+ violations.push(violation5("medium", "status.workflow.invalid-dir", "dir must be a non-empty string"));
4850
4986
  } else if (!isHarnessRelativePath(entry.dir)) {
4851
- violations.push(violation4("medium", "status.workflow.invalid-dir", `dir must be a harness-relative path (no absolute paths, no ".." segments) \u2014 got ${JSON.stringify(entry.dir)}`));
4987
+ violations.push(violation5("medium", "status.workflow.invalid-dir", `dir must be a harness-relative path (no absolute paths, no ".." segments) \u2014 got ${JSON.stringify(entry.dir)}`));
4852
4988
  }
4853
4989
  return { ok: violations.length === 0, violations };
4854
4990
  }
@@ -4858,24 +4994,24 @@ function validateStatusV2(docOrPath, opts = {}) {
4858
4994
  if (typeof docOrPath === "string") {
4859
4995
  try {
4860
4996
  doc = readJson(docOrPath);
4861
- harnessDir = dirname5(resolve5(docOrPath));
4997
+ harnessDir = dirname4(resolve4(docOrPath));
4862
4998
  } catch (error) {
4863
4999
  return {
4864
5000
  ok: false,
4865
- violations: [violation4("high", "status.invalid-json", error.message)]
5001
+ violations: [violation5("high", "status.invalid-json", error.message)]
4866
5002
  };
4867
5003
  }
4868
5004
  } else {
4869
5005
  doc = docOrPath;
4870
5006
  }
4871
- if (!isPlainObject3(doc)) {
4872
- return { ok: false, violations: [violation4("high", "status.invalid-doc", "status document must be an object")] };
5007
+ if (!isPlainObject4(doc)) {
5008
+ return { ok: false, violations: [violation5("high", "status.invalid-doc", "status document must be an object")] };
4873
5009
  }
4874
5010
  if (doc.version !== 2) {
4875
5011
  return {
4876
5012
  ok: false,
4877
5013
  violations: [
4878
- violation4("high", "status.migration-required", `status.json schema version 2 required \u2014 got ${JSON.stringify(doc.version)} (v1 or unknown version); run \`mstar migrate\` to convert the tree`, "run `mstar migrate`")
5014
+ violation5("high", "status.migration-required", `status.json schema version 2 required \u2014 got ${JSON.stringify(doc.version)} (v1 or unknown version); run \`mstar migrate\` to convert the tree`, "run `mstar migrate`")
4879
5015
  ]
4880
5016
  };
4881
5017
  }
@@ -4883,7 +5019,7 @@ function validateStatusV2(docOrPath, opts = {}) {
4883
5019
  return {
4884
5020
  ok: false,
4885
5021
  violations: [
4886
- violation4("high", "status.migration-required", "v1-shaped status.json (root plans[]) is not a v2 document \u2014 run `mstar migrate` to convert the tree", "run `mstar migrate`")
5022
+ violation5("high", "status.migration-required", "v1-shaped status.json (root plans[]) is not a v2 document \u2014 run `mstar migrate` to convert the tree", "run `mstar migrate`")
4887
5023
  ]
4888
5024
  };
4889
5025
  }
@@ -4891,27 +5027,27 @@ function validateStatusV2(docOrPath, opts = {}) {
4891
5027
  return {
4892
5028
  ok: false,
4893
5029
  violations: [
4894
- violation4("high", "status.migration-required", "v1-shaped status.json (root residual_findings) is not a v2 document \u2014 run `mstar migrate` to convert the tree", "run `mstar migrate`")
5030
+ violation5("high", "status.migration-required", "v1-shaped status.json (root residual_findings) is not a v2 document \u2014 run `mstar migrate` to convert the tree", "run `mstar migrate`")
4895
5031
  ]
4896
5032
  };
4897
5033
  }
4898
5034
  const violations = [];
4899
5035
  if (doc.updated_at === undefined) {
4900
- violations.push(violation4("high", "status.missing-updated-at", "missing required field: updated_at"));
4901
- } else if (typeof doc.updated_at !== "string" || !DATE_RE.test(doc.updated_at)) {
4902
- violations.push(violation4("medium", "status.invalid-updated-at", "updated_at must be YYYY-MM-DD"));
5036
+ violations.push(violation5("high", "status.missing-updated-at", "missing required field: updated_at"));
5037
+ } else if (typeof doc.updated_at !== "string" || !DATE_RE2.test(doc.updated_at)) {
5038
+ violations.push(violation5("medium", "status.invalid-updated-at", "updated_at must be YYYY-MM-DD"));
4903
5039
  }
4904
5040
  if (doc.workflows === undefined) {
4905
- violations.push(violation4("high", "status.missing-workflows", "missing required field: workflows"));
5041
+ violations.push(violation5("high", "status.missing-workflows", "missing required field: workflows"));
4906
5042
  } else if (!Array.isArray(doc.workflows)) {
4907
- violations.push(violation4("high", "status.invalid-workflows", "workflows must be an array"));
5043
+ violations.push(violation5("high", "status.invalid-workflows", "workflows must be an array"));
4908
5044
  } else {
4909
5045
  const seen = new Set;
4910
5046
  for (const entry of doc.workflows) {
4911
5047
  violations.push(...validateWorkflowEntry(entry).violations);
4912
- if (isPlainObject3(entry) && typeof entry.id === "string") {
5048
+ if (isPlainObject4(entry) && typeof entry.id === "string") {
4913
5049
  if (seen.has(entry.id)) {
4914
- violations.push(violation4("medium", "status.workflow.duplicate-id", `duplicate workflow id in workflows[]: ${JSON.stringify(entry.id)}`));
5050
+ violations.push(violation5("medium", "status.workflow.duplicate-id", `duplicate workflow id in workflows[]: ${JSON.stringify(entry.id)}`));
4915
5051
  }
4916
5052
  seen.add(entry.id);
4917
5053
  }
@@ -4920,40 +5056,40 @@ function validateStatusV2(docOrPath, opts = {}) {
4920
5056
  if (harnessDir !== undefined && Array.isArray(doc.workflows)) {
4921
5057
  let realHarnessDir = null;
4922
5058
  try {
4923
- realHarnessDir = realpathSync2(harnessDir);
5059
+ realHarnessDir = realpathSync(harnessDir);
4924
5060
  } catch {}
4925
5061
  for (const entry of doc.workflows) {
4926
- if (!isPlainObject3(entry) || typeof entry.dir !== "string")
5062
+ if (!isPlainObject4(entry) || typeof entry.dir !== "string")
4927
5063
  continue;
4928
5064
  const relSnapshot = join6(entry.dir, WORKFLOW_SNAPSHOT_FILE);
4929
5065
  const snapshotPath = join6(harnessDir, relSnapshot);
4930
5066
  const label = typeof entry.id === "string" ? entry.id : relSnapshot;
4931
5067
  let physical;
4932
5068
  try {
4933
- physical = realpathSync2(snapshotPath);
5069
+ physical = realpathSync(snapshotPath);
4934
5070
  } catch {
4935
- violations.push(violation4("high", "status.workflow.snapshot-missing", `workflows[] lists ${JSON.stringify(label)} but its snapshot does not exist at ${JSON.stringify(relSnapshot)} \u2014 the root holds active lifecycles only; unregister the id when its snapshot is removed`));
5071
+ violations.push(violation5("high", "status.workflow.snapshot-missing", `workflows[] lists ${JSON.stringify(label)} but its snapshot does not exist at ${JSON.stringify(relSnapshot)} \u2014 the root holds active lifecycles only; unregister the id when its snapshot is removed`));
4936
5072
  continue;
4937
5073
  }
4938
5074
  if (realHarnessDir !== null && physical !== realHarnessDir && !physical.startsWith(`${realHarnessDir}${sep}`)) {
4939
- violations.push(violation4("high", "status.workflow.snapshot-outside-harness", `workflows[] lists ${JSON.stringify(label)} but its snapshot resolves outside the harness dir (${JSON.stringify(physical)}) \u2014 symlinked snapshot paths are rejected; the snapshot must physically live under ${JSON.stringify(harnessDir)}`));
5075
+ violations.push(violation5("high", "status.workflow.snapshot-outside-harness", `workflows[] lists ${JSON.stringify(label)} but its snapshot resolves outside the harness dir (${JSON.stringify(physical)}) \u2014 symlinked snapshot paths are rejected; the snapshot must physically live under ${JSON.stringify(harnessDir)}`));
4940
5076
  continue;
4941
5077
  }
4942
5078
  let snapshot;
4943
5079
  try {
4944
5080
  snapshot = readJson(snapshotPath);
4945
5081
  } catch (error) {
4946
- violations.push(violation4("high", "status.workflow.snapshot-invalid", `snapshot at ${JSON.stringify(relSnapshot)} is not valid JSON: ${error.message}`));
5082
+ violations.push(violation5("high", "status.workflow.snapshot-invalid", `snapshot at ${JSON.stringify(relSnapshot)} is not valid JSON: ${error.message}`));
4947
5083
  continue;
4948
5084
  }
4949
5085
  if (typeof snapshot.status === "string" && WORKFLOW_TERMINAL_STATUSES.includes(snapshot.status)) {
4950
- violations.push(violation4("high", "status.workflow.terminal-listed", `workflows[] lists ${JSON.stringify(label)} whose snapshot status is terminal (${snapshot.status}) \u2014 removal-at-terminal: terminal writers unregister AFTER the snapshot write`));
5086
+ violations.push(violation5("high", "status.workflow.terminal-listed", `workflows[] lists ${JSON.stringify(label)} whose snapshot status is terminal (${snapshot.status}) \u2014 removal-at-terminal: terminal writers unregister AFTER the snapshot write`));
4951
5087
  }
4952
5088
  if (typeof entry.type === "string" && typeof snapshot.type === "string" && entry.type !== snapshot.type) {
4953
- violations.push(violation4("medium", "status.workflow.mismatched-type", `workflows[] entry ${JSON.stringify(label)} type ${JSON.stringify(entry.type)} does not match its snapshot type ${JSON.stringify(snapshot.type)} \u2014 the root entry mirrors the snapshot; align them`));
5089
+ violations.push(violation5("medium", "status.workflow.mismatched-type", `workflows[] entry ${JSON.stringify(label)} type ${JSON.stringify(entry.type)} does not match its snapshot type ${JSON.stringify(snapshot.type)} \u2014 the root entry mirrors the snapshot; align them`));
4954
5090
  }
4955
5091
  if (typeof entry.started_at === "string" && typeof snapshot.started_at === "string" && entry.started_at !== snapshot.started_at) {
4956
- violations.push(violation4("medium", "status.workflow.mismatched-started-at", `workflows[] entry ${JSON.stringify(label)} started_at ${JSON.stringify(entry.started_at)} does not match its snapshot started_at ${JSON.stringify(snapshot.started_at)} \u2014 workflow ${JSON.stringify(label)} collided with another writer (e.g. a concurrent/re-run \`audit promote\` with the same workflow id rewrote the snapshot); the root entry mirrors the snapshot \u2014 align them or remove the colliding workflow`));
5092
+ violations.push(violation5("medium", "status.workflow.mismatched-started-at", `workflows[] entry ${JSON.stringify(label)} started_at ${JSON.stringify(entry.started_at)} does not match its snapshot started_at ${JSON.stringify(snapshot.started_at)} \u2014 workflow ${JSON.stringify(label)} collided with another writer (e.g. a concurrent/re-run \`audit promote\` with the same workflow id rewrote the snapshot); the root entry mirrors the snapshot \u2014 align them or remove the colliding workflow`));
4957
5093
  }
4958
5094
  }
4959
5095
  }
@@ -4961,7 +5097,7 @@ function validateStatusV2(docOrPath, opts = {}) {
4961
5097
  }
4962
5098
  var validateStatus = validateStatusV2;
4963
5099
  function registerWorkflowEntryLocked(statusPath, entry) {
4964
- const harnessDir = dirname5(statusPath);
5100
+ const harnessDir = dirname4(statusPath);
4965
5101
  const current = readJson(statusPath);
4966
5102
  const fresh = Object.keys(current).length === 0;
4967
5103
  const doc = fresh ? { version: 2, updated_at: todayString(), workflows: [] } : current;
@@ -4982,15 +5118,336 @@ function registerWorkflowEntryLocked(statusPath, entry) {
4982
5118
  writeJson(statusPath, doc);
4983
5119
  return doc;
4984
5120
  }
4985
- var DEFAULT_PROBE_TIMEOUT_MS = 1e4;
4986
- function probeTimeoutMs() {
4987
- const raw = process.env.MSTAR_GIT_PROBE_TIMEOUT_MS;
4988
- if (raw === undefined || raw.trim() === "")
4989
- return DEFAULT_PROBE_TIMEOUT_MS;
4990
- const parsed = Number(raw);
4991
- return Number.isFinite(parsed) && parsed > 0 ? parsed : DEFAULT_PROBE_TIMEOUT_MS;
5121
+ var PROJECT_ROADMAP_FILE = "roadmap.md";
5122
+ var PROJECT_REGISTER_FILE = "residuals.json";
5123
+ var _DEFAULT_PROJECT = "_default";
5124
+ var ROLLUP_FIELDS = ["total_open", "by_severity", "by_target", "by_plan"];
5125
+ function isPlainObject5(value) {
5126
+ return typeof value === "object" && value !== null && !Array.isArray(value);
4992
5127
  }
4993
- function violation5(severity, code, message, fix) {
5128
+ function violation6(severity, code, message, fix) {
5129
+ return { ok: false, severity, code, message, fix };
5130
+ }
5131
+ function findingsCleanupGate(register, planId, opts) {
5132
+ const mode = opts?.mode ?? "allow-residual";
5133
+ const violations = [];
5134
+ const entries = isPlainObject5(register.entries) ? register.entries[planId] : undefined;
5135
+ if (entries === undefined) {
5136
+ return { ok: true, violations };
5137
+ }
5138
+ if (!Array.isArray(entries)) {
5139
+ violations.push(violation6("high", "project.register.invalid-entry-list", `entries[${JSON.stringify(planId)}] must be an array of residual entries (one entry per residual; v1 multi-finding semantics)`));
5140
+ return { ok: false, violations };
5141
+ }
5142
+ if (entries.length === 0) {
5143
+ return { ok: true, violations };
5144
+ }
5145
+ for (const entry of entries) {
5146
+ if (!isOpenResidual(entry))
5147
+ continue;
5148
+ const id = typeof entry.id === "string" ? entry.id : "<unnamed>";
5149
+ const label = `R#${id}`;
5150
+ if (mode === "zero-residual") {
5151
+ if (entry.severity === "nit") {
5152
+ violations.push(violation6("medium", "findings.zero-residual-nit", `${label}: style-only nits must be fixed in-session or dropped \u2014 never left open under zero-residual`));
5153
+ } else if (entry.decision === "risk-accepted" || entry.lifecycle === "waived") {
5154
+ violations.push(violation6("medium", "findings.zero-residual-risk-accepted", `${label}: waived/risk-accepted findings must be closed/archived, not left open under zero-residual`));
5155
+ } else if (entry.decision === "defer") {
5156
+ if (typeof entry.target !== "string" || entry.target.trim() === "") {
5157
+ violations.push(violation6("medium", "findings.zero-residual-defer-no-target", `${label}: blocker-defer requires a target (next iteration/milestone) under zero-residual`));
5158
+ }
5159
+ } else {
5160
+ violations.push(violation6("medium", "findings.zero-residual-open-fixable", `${label}: fixable finding must not remain open under zero-residual \u2014 fix now or convert to a blocker-defer`));
5161
+ }
5162
+ } else if (normalizeSeverity(entry.severity) === "critical") {
5163
+ violations.push(violation6("high", "findings.allow-residual-critical", `${label}: unresolved critical blocks Approve with residuals`));
5164
+ }
5165
+ }
5166
+ return { ok: violations.length === 0, violations };
5167
+ }
5168
+ function groupCount(values) {
5169
+ const counts = new Map;
5170
+ for (const value of values) {
5171
+ const key = typeof value === "string" ? value : String(value);
5172
+ counts.set(key, (counts.get(key) ?? 0) + 1);
5173
+ }
5174
+ return Object.fromEntries([...counts.entries()].sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0));
5175
+ }
5176
+ function techDebtRollup(projectDir) {
5177
+ const items = [];
5178
+ let entries;
5179
+ try {
5180
+ entries = readdirSync3(projectDir, { withFileTypes: true });
5181
+ } catch {
5182
+ entries = [];
5183
+ }
5184
+ for (const project of entries) {
5185
+ if (!project.isDirectory())
5186
+ continue;
5187
+ const registerPath = join7(projectDir, project.name, PROJECT_REGISTER_FILE);
5188
+ if (!existsSync4(registerPath))
5189
+ continue;
5190
+ let register;
5191
+ try {
5192
+ register = readJson(registerPath);
5193
+ } catch {
5194
+ continue;
5195
+ }
5196
+ if (!isPlainObject5(register) || !isPlainObject5(register.entries))
5197
+ continue;
5198
+ for (const [plan, planEntries] of Object.entries(register.entries)) {
5199
+ if (!Array.isArray(planEntries))
5200
+ continue;
5201
+ for (const entry of planEntries) {
5202
+ if (!isPlainObject5(entry) || !isOpenResidual(entry))
5203
+ continue;
5204
+ items.push({ plan, entry });
5205
+ }
5206
+ }
5207
+ }
5208
+ const bySeverity = {};
5209
+ for (const severity of SEVERITY_ORDER) {
5210
+ bySeverity[severity] = items.filter(({ entry }) => normalizeSeverity(entry.severity) === severity).length;
5211
+ }
5212
+ const computed = {
5213
+ total_open: items.length,
5214
+ by_severity: bySeverity,
5215
+ by_target: groupCount(items.map(({ entry }) => entry.target ?? "unspecified")),
5216
+ by_plan: groupCount(items.map(({ plan }) => plan))
5217
+ };
5218
+ const stored = null;
5219
+ const checks = ROLLUP_FIELDS.map((field) => ({ field, status: "DRIFT" }));
5220
+ const overall = "DRIFT";
5221
+ return { computed, stored, checks, overall };
5222
+ }
5223
+ function resolveHarnessDir(startDir = process.cwd(), opts = {}) {
5224
+ const start = resolve5(startDir);
5225
+ const explicit = opts.harnessDir ?? process.env.MSTAR_HARNESS_DIR;
5226
+ if (explicit)
5227
+ return resolve5(start, explicit);
5228
+ const boundary = resolve5(start, opts.workspaceRoot ?? defaultWorkspaceRoot(start));
5229
+ const rc = loadMstarc(start, boundary);
5230
+ if (rc !== null && rc.config.harnessDir)
5231
+ return resolve5(rc.dir, rc.config.harnessDir);
5232
+ let dir = start;
5233
+ for (;; ) {
5234
+ if (!isAtOrBelow2(dir, boundary))
5235
+ return null;
5236
+ for (const candidate of [join8(dir, ".mstar"), join8(dir, ".agents"), join8(dir, ".plans"), join8(dir, "plans")]) {
5237
+ if (isDirectory(candidate))
5238
+ return candidate;
5239
+ }
5240
+ if (dir === boundary)
5241
+ return null;
5242
+ const parent = dirname5(dir);
5243
+ if (parent === dir)
5244
+ return null;
5245
+ dir = parent;
5246
+ }
5247
+ }
5248
+ function defaultWorkspaceRoot(startDir) {
5249
+ try {
5250
+ const cdup = execFileSync("git", ["rev-parse", "--show-cdup"], {
5251
+ cwd: startDir,
5252
+ encoding: "utf8",
5253
+ stdio: ["ignore", "pipe", "ignore"]
5254
+ }).trim();
5255
+ if (!cdup)
5256
+ return startDir;
5257
+ let boundary = startDir;
5258
+ for (const segment of cdup.split(/[\\/]/)) {
5259
+ if (segment && segment !== ".")
5260
+ boundary = dirname5(boundary);
5261
+ }
5262
+ return resolve5(boundary);
5263
+ } catch {}
5264
+ return startDir;
5265
+ }
5266
+ function isAtOrBelow2(dir, root) {
5267
+ const rel = relative2(root, dir);
5268
+ return rel === "" || !rel.startsWith("..") && !isAbsolute3(rel);
5269
+ }
5270
+ function mstarcDirOverride(harnessDir, key) {
5271
+ const dir = resolve5(harnessDir);
5272
+ const rc = loadMstarc(dir, dirname5(dir));
5273
+ const declared = rc?.config[key];
5274
+ return declared ? resolve5(rc.dir, declared) : null;
5275
+ }
5276
+ function resolveSpecsDir(harnessDir, opts = {}) {
5277
+ const declared = mstarcDirOverride(harnessDir, "specsDir");
5278
+ if (declared !== null) {
5279
+ if (opts.create !== false)
5280
+ mkdirSync4(declared, { recursive: true });
5281
+ return declared;
5282
+ }
5283
+ const harness = resolve5(harnessDir);
5284
+ const repoRoot = dirname5(harness);
5285
+ const candidates = [
5286
+ join8(harness, "specs"),
5287
+ join8(repoRoot, "docs", "specs"),
5288
+ join8(repoRoot, "specs"),
5289
+ join8(harness, "designs"),
5290
+ join8(repoRoot, "designs")
5291
+ ];
5292
+ for (const candidate of candidates) {
5293
+ if (isDirectory(candidate) && hasFiles(candidate))
5294
+ return candidate;
5295
+ }
5296
+ const fallback = join8(harness, "specs");
5297
+ if (opts.create !== false)
5298
+ mkdirSync4(fallback, { recursive: true });
5299
+ return fallback;
5300
+ }
5301
+ function assertSafePathComponent(value, what) {
5302
+ if (value === "" || value === "." || value === ".." || !/^[A-Za-z0-9._-]+$/.test(value)) {
5303
+ throw new Error(`${what} must be a single safe path component ([A-Za-z0-9._-]+; not "", ".", "..", or containing "/" or "\\") \u2014 got ${JSON.stringify(value)}`);
5304
+ }
5305
+ }
5306
+ function resolveSddDir(harnessDir, planId) {
5307
+ assertSafePathComponent(planId, "planId");
5308
+ const base = resolve5(harnessDir);
5309
+ const declared = mstarcDirOverride(base, "sddDir");
5310
+ const sddBase = declared !== null ? declared : join8(base, "sdd");
5311
+ return join8(sddBase, planId);
5312
+ }
5313
+ function resolveHarnessSubdir(startDir, opts, key, fallback) {
5314
+ const harness = resolveHarnessDir(startDir, opts);
5315
+ if (harness === null) {
5316
+ throw new Error(`harness dir not found from ${resolve5(startDir)} \u2014 cannot resolve the ${fallback} dir (run \`mstar harness scaffold\`, pass opts.harnessDir, or set MSTAR_HARNESS_DIR)`);
5317
+ }
5318
+ const declared = mstarcDirOverride(harness, key);
5319
+ return declared !== null ? declared : join8(resolve5(harness), fallback);
5320
+ }
5321
+ function resolveWorkflowDir(startDir = process.cwd(), opts = {}) {
5322
+ return resolveHarnessSubdir(startDir, opts, "workflowDir", "workflows");
5323
+ }
5324
+ function resolveProjectDir(startDir = process.cwd(), opts = {}) {
5325
+ return resolveHarnessSubdir(startDir, opts, "projectDir", "projects");
5326
+ }
5327
+ var EMPTY_STATUS_TEMPLATE = {
5328
+ version: 2,
5329
+ updated_at: "1970-01-01",
5330
+ workflows: []
5331
+ };
5332
+ var SCAFFOLD_DIRS = ["plans", "iterations", "knowledge", "specs", "sdd"];
5333
+ function resolveScaffoldDirs(root) {
5334
+ const start = resolve5(root);
5335
+ const boundary = resolve5(start, defaultWorkspaceRoot(start));
5336
+ const rc = loadMstarc(start, boundary);
5337
+ const explicit = process.env.MSTAR_HARNESS_DIR;
5338
+ const harnessDir = explicit ? resolve5(start, explicit) : rc !== null && rc.config.harnessDir ? resolve5(rc.dir, rc.config.harnessDir) : join8(start, ".mstar");
5339
+ const declaredProjectDir = mstarcDirOverride(harnessDir, "projectDir");
5340
+ const projectDir = declaredProjectDir !== null ? declaredProjectDir : join8(harnessDir, "projects");
5341
+ return { harnessDir, projectDir };
5342
+ }
5343
+ var ROADMAP_TEMPLATE = `---
5344
+ project_id: _default
5345
+ title: Default Project
5346
+ status: active
5347
+ created_at: {created_at}
5348
+ ---
5349
+
5350
+ # Roadmap
5351
+
5352
+ ## Direction
5353
+
5354
+ State the project direction here.
5355
+ `;
5356
+ var EMPTY_REGISTER_TEMPLATE = {
5357
+ entries: {}
5358
+ };
5359
+ function scaffoldHarness(root) {
5360
+ const { harnessDir, projectDir } = resolveScaffoldDirs(root);
5361
+ for (const dir of SCAFFOLD_DIRS)
5362
+ mkdirSync4(join8(harnessDir, dir), { recursive: true });
5363
+ const statusPath = join8(harnessDir, "status.json");
5364
+ if (Object.keys(readJson(statusPath)).length === 0)
5365
+ writeJson(statusPath, EMPTY_STATUS_TEMPLATE);
5366
+ const defaultProjectDir = join8(projectDir, _DEFAULT_PROJECT);
5367
+ mkdirSync4(defaultProjectDir, { recursive: true });
5368
+ const roadmapPath = join8(defaultProjectDir, PROJECT_ROADMAP_FILE);
5369
+ if (!existsSync5(roadmapPath)) {
5370
+ const created = new Date().toISOString().slice(0, 10);
5371
+ writeFileSync3(roadmapPath, ROADMAP_TEMPLATE.replace("{created_at}", created), "utf8");
5372
+ }
5373
+ const registerPath = join8(defaultProjectDir, PROJECT_REGISTER_FILE);
5374
+ if (Object.keys(readJson(registerPath)).length === 0)
5375
+ writeJson(registerPath, EMPTY_REGISTER_TEMPLATE);
5376
+ return harnessDir;
5377
+ }
5378
+ var GITIGNORE_SNIPPET = `# Morning Star harness (.mstar/)
5379
+ # Principle: process stays local; results are shared with the team.
5380
+ # Default-ignore everything under .mstar/, then re-include the tracked results.
5381
+ .mstar/**
5382
+ !.mstar/AGENTS.md
5383
+ !.mstar/knowledge/
5384
+ !.mstar/knowledge/**
5385
+ !.mstar/specs/
5386
+ !.mstar/specs/**
5387
+ # .mstarc \u2014 repo-local harness config (may declare [config] harness_dir=<name>)
5388
+ .mstarc
5389
+ `;
5390
+ var GITIGNORE_SNIPPET_AGENTS = `# Morning Star harness (.agents/) \u2014 legacy
5391
+ # Default-ignore everything under .agents/, then re-include the tracked results.
5392
+ .agents/**
5393
+ !.agents/AGENTS.md
5394
+ !.agents/knowledge/
5395
+ !.agents/knowledge/**
5396
+ !.agents/specs/
5397
+ !.agents/specs/**
5398
+ `;
5399
+ var GITIGNORE_PROCESS_ENTRIES = GITIGNORE_SNIPPET.split(`
5400
+ `).filter((line) => line.startsWith(".mstar/") || line.startsWith("!.mstar/")).map((line) => line.trim());
5401
+ var GITIGNORE_PROCESS_ENTRIES_AGENTS = GITIGNORE_SNIPPET_AGENTS.split(`
5402
+ `).filter((line) => line.startsWith(".agents/") || line.startsWith("!.agents/")).map((line) => line.trim());
5403
+ function emitGitignoreSnippet(kind) {
5404
+ if (kind === "agents")
5405
+ return GITIGNORE_SNIPPET_AGENTS;
5406
+ if (kind === "mstar")
5407
+ return GITIGNORE_SNIPPET;
5408
+ return `${GITIGNORE_SNIPPET}${GITIGNORE_SNIPPET_AGENTS}`;
5409
+ }
5410
+ function detectHarnessKind(harnessDir) {
5411
+ if (!harnessDir)
5412
+ return null;
5413
+ const name = basename2(resolve5(harnessDir));
5414
+ if (name === ".mstar")
5415
+ return "mstar";
5416
+ if (name === ".agents")
5417
+ return "agents";
5418
+ return null;
5419
+ }
5420
+ function isDirectory(dir) {
5421
+ try {
5422
+ return statSync3(dir).isDirectory();
5423
+ } catch {
5424
+ return false;
5425
+ }
5426
+ }
5427
+ function hasFiles(dir) {
5428
+ try {
5429
+ for (const entry of readdirSync4(dir, { withFileTypes: true })) {
5430
+ if (entry.isDirectory()) {
5431
+ if (hasFiles(join8(dir, entry.name)))
5432
+ return true;
5433
+ } else if (entry.isFile()) {
5434
+ return true;
5435
+ }
5436
+ }
5437
+ return false;
5438
+ } catch {
5439
+ return false;
5440
+ }
5441
+ }
5442
+ var DEFAULT_PROBE_TIMEOUT_MS = 1e4;
5443
+ function probeTimeoutMs() {
5444
+ const raw = process.env.MSTAR_GIT_PROBE_TIMEOUT_MS;
5445
+ if (raw === undefined || raw.trim() === "")
5446
+ return DEFAULT_PROBE_TIMEOUT_MS;
5447
+ const parsed = Number(raw);
5448
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : DEFAULT_PROBE_TIMEOUT_MS;
5449
+ }
5450
+ function violation7(severity, code, message, fix) {
4994
5451
  return { ok: false, severity, code, message, fix };
4995
5452
  }
4996
5453
  function gate(violations) {
@@ -5024,25 +5481,25 @@ function l1PreDispatchCheck(input, opts = {}) {
5024
5481
  const violations = [];
5025
5482
  const { controlWorktreePath, leaseWorktreePath, leaseWorkingBranch, planId } = input;
5026
5483
  if (controlWorktreePath.trim() === "") {
5027
- violations.push(violation5("high", "worktree.l1.control-missing", "metadata.control_worktree_path is not recorded \u2014 the L1 control worktree (integration-branch checkout) must be recorded in status.json before writable dispatch", "record the control worktree path in status.json metadata.control_worktree_path"));
5484
+ violations.push(violation7("high", "worktree.l1.control-missing", "metadata.control_worktree_path is not recorded \u2014 the L1 control worktree (integration-branch checkout) must be recorded in status.json before writable dispatch", "record the control worktree path in status.json metadata.control_worktree_path"));
5028
5485
  }
5029
5486
  if (leaseWorktreePath.trim() === "") {
5030
- violations.push(violation5("high", "worktree.l1.lease-missing", `execution_lease.worktree_path is empty for plan "${planId}" \u2014 no verified execution_lease to dispatch against`, "claim the execution_lease with an absolute feature worktree path before dispatch"));
5487
+ violations.push(violation7("high", "worktree.l1.lease-missing", `execution_lease.worktree_path is empty for plan "${planId}" \u2014 no verified execution_lease to dispatch against`, "claim the execution_lease with an absolute feature worktree path before dispatch"));
5031
5488
  }
5032
5489
  if (leaseWorkingBranch.trim() === "") {
5033
- violations.push(violation5("high", "worktree.l1.lease-branch-missing", `execution_lease.working_branch is empty for plan "${planId}"`, "record the lease working_branch before dispatch"));
5490
+ violations.push(violation7("high", "worktree.l1.lease-branch-missing", `execution_lease.working_branch is empty for plan "${planId}"`, "record the lease working_branch before dispatch"));
5034
5491
  }
5035
5492
  if (controlWorktreePath !== "" && leaseWorktreePath !== "" && resolve6(controlWorktreePath) === resolve6(leaseWorktreePath)) {
5036
- violations.push(violation5("critical", "worktree.l1.lease-equals-control", `execution_lease.worktree_path "${leaseWorktreePath}" equals metadata.control_worktree_path \u2014 the feature worktree MUST differ from the control worktree (L1 isolation; product edits never land in the control checkout)`, "use a distinct feature worktree for the plan (git worktree add <path> <branch>) and update the lease"));
5493
+ violations.push(violation7("critical", "worktree.l1.lease-equals-control", `execution_lease.worktree_path "${leaseWorktreePath}" equals metadata.control_worktree_path \u2014 the feature worktree MUST differ from the control worktree (L1 isolation; product edits never land in the control checkout)`, "use a distinct feature worktree for the plan (git worktree add <path> <branch>) and update the lease"));
5037
5494
  }
5038
- if (leaseWorktreePath !== "" && !existsSync4(leaseWorktreePath)) {
5039
- violations.push(violation5("high", "worktree.l1.feature-missing", `feature worktree directory "${leaseWorktreePath}" does not exist for plan "${planId}"`, `create it before dispatch: git worktree add ${leaseWorktreePath} <working-branch>`));
5495
+ if (leaseWorktreePath !== "" && !existsSync6(leaseWorktreePath)) {
5496
+ violations.push(violation7("high", "worktree.l1.feature-missing", `feature worktree directory "${leaseWorktreePath}" does not exist for plan "${planId}"`, `create it before dispatch: git worktree add ${leaseWorktreePath} <working-branch>`));
5040
5497
  } else if (leaseWorktreePath !== "" && leaseWorkingBranch !== "") {
5041
5498
  const probe = probeBranch(leaseWorktreePath, opts);
5042
5499
  if ("error" in probe) {
5043
- violations.push(violation5("high", "worktree.l1.branch-probe-failed", `cannot probe branch at "${leaseWorktreePath}" for plan "${planId}": ${probe.error}`, "verify the path is a git worktree checkout on the lease working branch (not detached)"));
5500
+ violations.push(violation7("high", "worktree.l1.branch-probe-failed", `cannot probe branch at "${leaseWorktreePath}" for plan "${planId}": ${probe.error}`, "verify the path is a git worktree checkout on the lease working branch (not detached)"));
5044
5501
  } else if (probe.branch !== leaseWorkingBranch) {
5045
- violations.push(violation5("high", "worktree.l1.branch-mismatch", `feature worktree "${leaseWorktreePath}" is on branch "${probe.branch}", expected execution_lease.working_branch "${leaseWorkingBranch}" (plan "${planId}")`, `checkout ${leaseWorkingBranch} in the feature worktree`));
5502
+ violations.push(violation7("high", "worktree.l1.branch-mismatch", `feature worktree "${leaseWorktreePath}" is on branch "${probe.branch}", expected execution_lease.working_branch "${leaseWorkingBranch}" (plan "${planId}")`, `checkout ${leaseWorkingBranch} in the feature worktree`));
5046
5503
  }
5047
5504
  }
5048
5505
  return gate(violations);
@@ -5052,32 +5509,32 @@ function l2PreDispatchCheck(input, opts = {}) {
5052
5509
  const tracks = input.tracks ?? [];
5053
5510
  const seenPaths = new Set;
5054
5511
  if (tracks.length < 1) {
5055
- violations.push(violation5("high", "worktree.l2.no-tracks", "no parallel writable tracks \u2014 the L2 pre-dispatch checklist requires at least one track with an absolute worktreePath and Working branch", "pass each track's absolute Worktree path and PM-approved Working branch"));
5512
+ violations.push(violation7("high", "worktree.l2.no-tracks", "no parallel writable tracks \u2014 the L2 pre-dispatch checklist requires at least one track with an absolute worktreePath and Working branch", "pass each track's absolute Worktree path and PM-approved Working branch"));
5056
5513
  }
5057
5514
  tracks.forEach((track, index) => {
5058
5515
  if (track.worktreePath.trim() === "" || track.workingBranch.trim() === "") {
5059
- violations.push(violation5("high", "worktree.l2.track-invalid", `track ${index + 1} is missing worktreePath and/or workingBranch`, "fill both fields for every track"));
5516
+ violations.push(violation7("high", "worktree.l2.track-invalid", `track ${index + 1} is missing worktreePath and/or workingBranch`, "fill both fields for every track"));
5060
5517
  return;
5061
5518
  }
5062
5519
  if (!isAbsolute4(track.worktreePath)) {
5063
- violations.push(violation5("high", "worktree.l2.track-path-relative", `track ${index + 1} worktreePath "${track.worktreePath}" is not an absolute path \u2014 L2 tracks MUST use absolute worktree checkout paths (consistent with the lease validator's absolute worktree_path enforcement)`, `use an absolute path for track ${index + 1} (e.g. /Users/<you>/worktrees/<branch>)`));
5520
+ violations.push(violation7("high", "worktree.l2.track-path-relative", `track ${index + 1} worktreePath "${track.worktreePath}" is not an absolute path \u2014 L2 tracks MUST use absolute worktree checkout paths (consistent with the lease validator's absolute worktree_path enforcement)`, `use an absolute path for track ${index + 1} (e.g. /Users/<you>/worktrees/<branch>)`));
5064
5521
  return;
5065
5522
  }
5066
5523
  const normalized = resolve6(track.worktreePath);
5067
5524
  if (seenPaths.has(normalized)) {
5068
- violations.push(violation5("high", "worktree.l2.track-path-collision", `duplicate worktreePath "${track.worktreePath}" across parallel tracks \u2014 L2 parallel-writable isolation requires a distinct absolute Worktree path per track (N parallel invokes \u2260 isolation)`, "give every parallel track its own git worktree checkout"));
5525
+ violations.push(violation7("high", "worktree.l2.track-path-collision", `duplicate worktreePath "${track.worktreePath}" across parallel tracks \u2014 L2 parallel-writable isolation requires a distinct absolute Worktree path per track (N parallel invokes \u2260 isolation)`, "give every parallel track its own git worktree checkout"));
5069
5526
  return;
5070
5527
  }
5071
5528
  seenPaths.add(normalized);
5072
- if (!existsSync4(track.worktreePath)) {
5073
- violations.push(violation5("high", "worktree.l2.track-missing", `track worktree directory "${track.worktreePath}" does not exist`, `create it before dispatch: git worktree add ${track.worktreePath} ${track.workingBranch}`));
5529
+ if (!existsSync6(track.worktreePath)) {
5530
+ violations.push(violation7("high", "worktree.l2.track-missing", `track worktree directory "${track.worktreePath}" does not exist`, `create it before dispatch: git worktree add ${track.worktreePath} ${track.workingBranch}`));
5074
5531
  return;
5075
5532
  }
5076
5533
  const probe = probeBranch(track.worktreePath, opts);
5077
5534
  if ("error" in probe) {
5078
- violations.push(violation5("high", "worktree.l2.branch-probe-failed", `cannot probe branch at "${track.worktreePath}": ${probe.error}`, "verify the path is a git worktree checkout on its Working branch (not detached)"));
5535
+ violations.push(violation7("high", "worktree.l2.branch-probe-failed", `cannot probe branch at "${track.worktreePath}": ${probe.error}`, "verify the path is a git worktree checkout on its Working branch (not detached)"));
5079
5536
  } else if (probe.branch !== track.workingBranch) {
5080
- violations.push(violation5("high", "worktree.l2.branch-mismatch", `track worktree "${track.worktreePath}" is on branch "${probe.branch}", expected Working branch "${track.workingBranch}"`, `checkout ${track.workingBranch} in that worktree`));
5537
+ violations.push(violation7("high", "worktree.l2.branch-mismatch", `track worktree "${track.worktreePath}" is on branch "${probe.branch}", expected Working branch "${track.workingBranch}"`, `checkout ${track.workingBranch} in that worktree`));
5081
5538
  }
5082
5539
  });
5083
5540
  return gate(violations);
@@ -5093,7 +5550,7 @@ function assertQcAlignment(assignments) {
5093
5550
  for (const { key, label } of QC_ALIGNMENT_FIELDS) {
5094
5551
  const distinct = [...new Set(list.map((a) => a[key]))];
5095
5552
  if (distinct.length > 1) {
5096
- violations.push(violation5("high", "qc.alignment.mismatch", `QC/QA alignment field "${label}" is not byte-identical across ${list.length} assignments: ${distinct.map((v) => `"${v}"`).join(" vs ")}`, `copy the same ${label} value verbatim into every QC tri and QA Assignment`));
5553
+ violations.push(violation7("high", "qc.alignment.mismatch", `QC/QA alignment field "${label}" is not byte-identical across ${list.length} assignments: ${distinct.map((v) => `"${v}"`).join(" vs ")}`, `copy the same ${label} value verbatim into every QC tri and QA Assignment`));
5097
5554
  }
5098
5555
  }
5099
5556
  return gate(violations);
@@ -5134,14 +5591,14 @@ function gitOut(cwd, args) {
5134
5591
  }
5135
5592
  }
5136
5593
  function probeHarnessWithStatus(root) {
5137
- if (isFile2(join7(root, ".mstar", "status.json")))
5138
- return join7(root, ".mstar");
5139
- if (isFile2(join7(root, ".agents", "status.json")))
5140
- return join7(root, ".agents");
5141
- if (hasWorkflowSnapshot(join7(root, ".mstar")))
5142
- return join7(root, ".mstar");
5143
- if (hasWorkflowSnapshot(join7(root, ".agents")))
5144
- return join7(root, ".agents");
5594
+ if (isFile2(join9(root, ".mstar", "status.json")))
5595
+ return join9(root, ".mstar");
5596
+ if (isFile2(join9(root, ".agents", "status.json")))
5597
+ return join9(root, ".agents");
5598
+ if (hasWorkflowSnapshot(join9(root, ".mstar")))
5599
+ return join9(root, ".mstar");
5600
+ if (hasWorkflowSnapshot(join9(root, ".agents")))
5601
+ return join9(root, ".agents");
5145
5602
  return null;
5146
5603
  }
5147
5604
  function hasWorkflowSnapshot(harnessDir) {
@@ -5149,13 +5606,13 @@ function hasWorkflowSnapshot(harnessDir) {
5149
5606
  try {
5150
5607
  workflowsDir = resolveWorkflowDir(harnessDir, { harnessDir });
5151
5608
  } catch {
5152
- workflowsDir = join7(harnessDir, "workflows");
5609
+ workflowsDir = join9(harnessDir, "workflows");
5153
5610
  }
5154
5611
  if (!isDirectory2(workflowsDir))
5155
5612
  return false;
5156
5613
  try {
5157
- for (const entry of readdirSync3(workflowsDir, { withFileTypes: true })) {
5158
- if (entry.isDirectory() && isFile2(join7(workflowsDir, entry.name, "snapshot.json")))
5614
+ for (const entry of readdirSync5(workflowsDir, { withFileTypes: true })) {
5615
+ if (entry.isDirectory() && isFile2(join9(workflowsDir, entry.name, "snapshot.json")))
5159
5616
  return true;
5160
5617
  }
5161
5618
  } catch {
@@ -5168,14 +5625,14 @@ function isLinkedWorktree(root) {
5168
5625
  const commonRaw = gitOut(root, ["rev-parse", "--git-common-dir"]);
5169
5626
  if (gitDirRaw === null || commonRaw === null)
5170
5627
  return false;
5171
- const gitDir = isAbsolute5(gitDirRaw) ? gitDirRaw : join7(root, gitDirRaw);
5172
- const common2 = isAbsolute5(commonRaw) ? commonRaw : join7(root, commonRaw);
5628
+ const gitDir = isAbsolute5(gitDirRaw) ? gitDirRaw : join9(root, gitDirRaw);
5629
+ const common2 = isAbsolute5(commonRaw) ? commonRaw : join9(root, commonRaw);
5173
5630
  if (gitDir.includes("/.git/worktrees/") || gitDir.includes("/worktrees/"))
5174
5631
  return true;
5175
5632
  try {
5176
5633
  const gdParent = realpathSync3(dirname6(gitDir));
5177
5634
  const cmAbs = realpathSync3(common2);
5178
- return join7(gdParent, basename3(gitDir)) !== cmAbs && gitDir !== cmAbs;
5635
+ return join9(gdParent, basename3(gitDir)) !== cmAbs && gitDir !== cmAbs;
5179
5636
  } catch {
5180
5637
  return false;
5181
5638
  }
@@ -5209,25 +5666,25 @@ function sddWorkspace(planId, opts = {}) {
5209
5666
  harnessDir = resolve7(root, harnessOverride);
5210
5667
  } else {
5211
5668
  const rc = findMstarc(root, root);
5212
- const rcHarnessDir = rc !== null ? parseMstarc(readFileSync5(rc, "utf8")).harnessDir : undefined;
5669
+ const rcHarnessDir = rc !== null ? parseMstarc(readFileSync7(rc, "utf8")).harnessDir : undefined;
5213
5670
  if (rcHarnessDir) {
5214
5671
  harnessDir = resolve7(rc !== null ? dirname6(rc) : root, rcHarnessDir);
5215
5672
  } else {
5216
5673
  const probed = probeHarnessWithStatus(root);
5217
5674
  if (probed) {
5218
5675
  harnessDir = probed;
5219
- } else if (isDirectory2(join7(root, ".mstar"))) {
5220
- harnessDir = join7(root, ".mstar");
5221
- } else if (isDirectory2(join7(root, ".agents"))) {
5222
- harnessDir = join7(root, ".agents");
5676
+ } else if (isDirectory2(join9(root, ".mstar"))) {
5677
+ harnessDir = join9(root, ".mstar");
5678
+ } else if (isDirectory2(join9(root, ".agents"))) {
5679
+ harnessDir = join9(root, ".agents");
5223
5680
  } else {
5224
- harnessDir = join7(root, ".mstar");
5681
+ harnessDir = join9(root, ".mstar");
5225
5682
  }
5226
5683
  }
5227
5684
  }
5228
5685
  const sddDir = resolveSddDir(harnessDir, planId);
5229
5686
  mkdirSync5(sddDir, { recursive: true });
5230
- writeFileSync3(join7(sddDir, ".gitignore"), `*
5687
+ writeFileSync4(join9(sddDir, ".gitignore"), `*
5231
5688
  `);
5232
5689
  return realpathSync3(sddDir);
5233
5690
  }
@@ -5237,480 +5694,92 @@ function taskBrief(planFile, taskN, outFile, opts = {}) {
5237
5694
  }
5238
5695
  let content;
5239
5696
  try {
5240
- content = readFileSync5(planFile, "utf8");
5697
+ content = readFileSync7(planFile, "utf8");
5241
5698
  } catch {
5242
5699
  throw new SddScriptError(`no such plan file: ${planFile}`, 2);
5243
5700
  }
5244
5701
  let out;
5245
5702
  if (outFile) {
5246
- out = outFile;
5247
- } else {
5248
- const sddDir = opts.sddDir ?? process.env.SDD_DIR;
5249
- if (!sddDir) {
5250
- throw new SddScriptError("mstar sdd task-brief: set SDD_DIR or pass OUTFILE (run mstar sdd workspace PLAN_ID first)", 2);
5251
- }
5252
- mkdirSync5(sddDir, { recursive: true });
5253
- out = join7(sddDir, `task-${taskN}-brief.md`);
5254
- }
5255
- const records = content.endsWith(`
5256
- `) ? content.split(`
5257
- `).slice(0, -1) : content.split(`
5258
- `);
5259
- const headingRe = /^#+[ \t]+Task[ \t]+[0-9]+/;
5260
- const targetRe = new RegExp(`^#+[ ]+Task[ ]+${taskN}([^0-9]|$)`);
5261
- let infence = false;
5262
- let intask = false;
5263
- const printed = [];
5264
- for (const line of records) {
5265
- if (/^```/.test(line))
5266
- infence = !infence;
5267
- if (!infence && headingRe.test(line))
5268
- intask = targetRe.test(line);
5269
- if (intask)
5270
- printed.push(line);
5271
- }
5272
- const output = printed.length > 0 ? `${printed.join(`
5273
- `)}
5274
- ` : "";
5275
- writeFileSync3(out, output);
5276
- if (printed.length === 0) {
5277
- throw new SddScriptError(`task ${taskN} not found in ${planFile} (no heading matching Task ${taskN})`, 3);
5278
- }
5279
- return out;
5280
- }
5281
- function reviewPackage(base, head, outFile, opts = {}) {
5282
- if (!base || !head) {
5283
- throw new SddScriptError("usage: mstar sdd review-package BASE HEAD [OUTFILE]", 2);
5284
- }
5285
- const cwd = opts.cwd ?? process.cwd();
5286
- const verifyRef = (ref, what) => {
5287
- try {
5288
- execFileSync3("git", ["rev-parse", "--verify", "--quiet", ref], { cwd, stdio: ["ignore", "pipe", "pipe"] });
5289
- } catch {
5290
- throw new SddScriptError(`bad ${what}: ${ref}`, 2);
5291
- }
5292
- };
5293
- verifyRef(base, "BASE");
5294
- verifyRef(head, "HEAD");
5295
- let out;
5296
- if (outFile) {
5297
- out = outFile;
5298
- } else {
5299
- const sddDir = opts.sddDir ?? process.env.SDD_DIR;
5300
- if (!sddDir) {
5301
- throw new SddScriptError("mstar sdd review-package: set SDD_DIR or pass OUTFILE", 2);
5302
- }
5303
- mkdirSync5(sddDir, { recursive: true });
5304
- const shortBase = gitOut(cwd, ["rev-parse", "--short", base]) ?? base;
5305
- const shortHead = gitOut(cwd, ["rev-parse", "--short", head]) ?? head;
5306
- out = join7(sddDir, `review-${shortBase}..${shortHead}.diff`);
5307
- }
5308
- const run = (args) => execFileSync3("git", args, { cwd, maxBuffer: GIT_CAPTURE_MAX_BYTES });
5309
- const parts = [
5310
- Buffer.from(`# Review package: ${base}..${head}
5311
-
5312
- ## Commits
5313
- `),
5314
- run(["log", "--oneline", `${base}..${head}`]),
5315
- Buffer.from(`
5316
- ## Files changed
5317
- `),
5318
- run(["diff", "--stat", `${base}..${head}`]),
5319
- Buffer.from(`
5320
- ## Diff
5321
- `),
5322
- run(["diff", "-U10", `${base}..${head}`])
5323
- ];
5324
- writeFileSync3(out, Buffer.concat(parts));
5325
- return out;
5326
- }
5327
- var COMPASS_STATUSES = ["active", "locked", "completed"];
5328
- var DATE_RE2 = /^\d{4}-\d{2}-\d{2}$/;
5329
- var PLAN_STATUS_DONE = "Done";
5330
- function typeName(value) {
5331
- if (value === null)
5332
- return "null";
5333
- if (Array.isArray(value))
5334
- return "array";
5335
- return typeof value;
5336
- }
5337
- function validateCompassShape(doc) {
5338
- const issues = [];
5339
- const expectString = (key, opts = {}) => {
5340
- const value = doc[key];
5341
- if (typeof value !== "string") {
5342
- issues.push({ path: [key], message: `expected string, received ${typeName(value)}` });
5343
- return;
5344
- }
5345
- if (opts.min !== undefined && value.length < opts.min) {
5346
- issues.push({ path: [key], message: `string must contain at least ${opts.min} character(s)` });
5347
- return;
5348
- }
5349
- if (opts.regex !== undefined && !opts.regex.test(value)) {
5350
- issues.push({ path: [key], message: `string must match ${opts.regex}` });
5351
- }
5352
- };
5353
- expectString("iteration_id", { min: 1 });
5354
- expectString("start_date", { regex: DATE_RE2 });
5355
- const status = doc.status;
5356
- if (typeof status !== "string" || !COMPASS_STATUSES.includes(status)) {
5357
- issues.push({
5358
- path: ["status"],
5359
- message: `expected one of ${COMPASS_STATUSES.map((s) => `'${s}'`).join(" | ")}, received ${typeName(status)}`
5360
- });
5361
- }
5362
- expectString("iteration_base_branch", { min: 1 });
5363
- expectString("target_branch", { min: 1 });
5364
- const plans = doc.plans;
5365
- if (plans !== undefined) {
5366
- if (!Array.isArray(plans)) {
5367
- issues.push({ path: ["plans"], message: `expected array, received ${typeName(plans)}` });
5368
- } else {
5369
- plans.forEach((entry, index) => {
5370
- if (typeof entry !== "string") {
5371
- issues.push({ path: ["plans", index], message: `expected string, received ${typeName(entry)}` });
5372
- } else if (entry.length < 1) {
5373
- issues.push({ path: ["plans", index], message: "string must contain at least 1 character(s)" });
5374
- }
5375
- });
5376
- }
5377
- }
5378
- const end_date = doc.end_date;
5379
- if (end_date !== undefined) {
5380
- if (typeof end_date !== "string") {
5381
- issues.push({ path: ["end_date"], message: `expected string, received ${typeName(end_date)}` });
5382
- } else if (!DATE_RE2.test(end_date)) {
5383
- issues.push({ path: ["end_date"], message: `string must match ${DATE_RE2}` });
5384
- }
5385
- }
5386
- if (issues.length > 0)
5387
- return { ok: false, issues };
5388
- return {
5389
- ok: true,
5390
- data: {
5391
- iteration_id: doc.iteration_id,
5392
- start_date: doc.start_date,
5393
- status,
5394
- iteration_base_branch: doc.iteration_base_branch,
5395
- target_branch: doc.target_branch,
5396
- ...plans !== undefined ? { plans } : {},
5397
- ...end_date !== undefined ? { end_date } : {}
5398
- }
5399
- };
5400
- }
5401
- function violation6(severity, code, message, fix) {
5402
- return { ok: false, severity, code, message, fix };
5403
- }
5404
- function isPlainObject4(value) {
5405
- return typeof value === "object" && value !== null && !Array.isArray(value);
5406
- }
5407
- function validateCompassFrontmatter(doc) {
5408
- if (!isPlainObject4(doc)) {
5409
- return {
5410
- ok: false,
5411
- violations: [
5412
- violation6("medium", "COMPASS_INVALID_FIELD", "Compass frontmatter must be a YAML object with iteration_id / start_date / status / iteration_base_branch / target_branch (template: mstar-iteration \u00a71.3)", "Fix the frontmatter of {ITERATION_DIR}/<iteration-id>/delivery-compass.md")
5413
- ]
5414
- };
5415
- }
5416
- const parsed = validateCompassShape(doc);
5417
- if (!parsed.ok) {
5418
- return {
5419
- ok: false,
5420
- violations: parsed.issues.map((issue) => {
5421
- const field = issue.path.join(".") || "(root)";
5422
- return violation6("medium", "COMPASS_INVALID_FIELD", `Compass frontmatter field '${field}' is invalid: ${issue.message}`, `Fix '${field}' in {ITERATION_DIR}/<iteration-id>/delivery-compass.md frontmatter (template: mstar-iteration \u00a71.3)`);
5423
- })
5424
- };
5425
- }
5426
- const violations = [];
5427
- const { status, end_date } = parsed.data;
5428
- if (status === "completed" && end_date === undefined) {
5429
- violations.push(violation6("high", "COMPASS_END_DATE_REQUIRED", "Compass frontmatter status is 'completed' but end_date is missing \u2014 end_date is required at iteration-close (mstar-iteration \u00a73.4, template Fields guide)", "Add `end_date: YYYY-MM-DD` to the frontmatter"));
5430
- }
5431
- if (status !== "completed" && end_date !== undefined) {
5432
- violations.push(violation6("medium", "COMPASS_END_DATE_NOT_ALLOWED", `Compass frontmatter sets end_date while status is '${status}' \u2014 end_date is only written at iteration-close (mstar-iteration \u00a73.4)`, "Remove end_date until iteration-close"));
5433
- }
5434
- return { ok: violations.length === 0, violations };
5435
- }
5436
- function registeredPlanIds(compassDoc) {
5437
- if (!Array.isArray(compassDoc.plans))
5438
- return [];
5439
- return compassDoc.plans.filter((plan) => typeof plan === "string" && plan.length > 0);
5440
- }
5441
- function findPlanRow(snapshotDoc, planId) {
5442
- if (!Array.isArray(snapshotDoc.plans))
5443
- return null;
5444
- for (const row of snapshotDoc.plans) {
5445
- if (!isPlainObject4(row))
5446
- continue;
5447
- const rowId = typeof row.id === "string" ? row.id : typeof row.plan_id === "string" ? row.plan_id : null;
5448
- if (rowId === planId)
5449
- return row;
5450
- }
5451
- return null;
5452
- }
5453
- function entryPlansAllDone(snapshotDoc, registered) {
5454
- const violations = [];
5455
- if (registered.length === 0) {
5456
- violations.push(violation6("medium", "COMPASS_NO_PLANS", "Compass frontmatter registers no plans \u2014 the all-plans-Done transition cannot be verified (mstar-iteration \u00a71.3 / Phase transition gates)", "List the iteration's plan ids in the compass frontmatter `plans`"));
5457
- return violations;
5458
- }
5459
- for (const planId of registered) {
5460
- const row = findPlanRow(snapshotDoc, planId);
5461
- if (row === null) {
5462
- violations.push(violation6("high", "PLAN_NOT_IN_STATUS", `Plan '${planId}' is registered in the compass frontmatter but has no row in the workflow snapshot plans[] (mstar-iteration \u00a73.1 entry item 1)`, "Add the plan row to {HARNESS_DIR}/workflows/<id>/snapshot.json"));
5463
- continue;
5464
- }
5465
- if (row.status !== PLAN_STATUS_DONE) {
5466
- violations.push(violation6("high", "PLAN_NOT_DONE", `Plan '${planId}' status is ${JSON.stringify(row.status)} in the workflow snapshot \u2014 all compass-registered plans must be 'Done' before iteration-close (mstar-iteration \u00a73.1 entry item 1)`));
5467
- }
5468
- }
5469
- return violations;
5470
- }
5471
- function entryFrontmatterComplete(compassDoc) {
5472
- return validateCompassFrontmatter(compassDoc).violations;
5473
- }
5474
- function exitFrontmatterClosed(compassDoc) {
5475
- const violations = [];
5476
- if (compassDoc.status !== "completed") {
5477
- violations.push(violation6("high", "EXIT_STATUS_NOT_COMPLETED", `Compass frontmatter status must be 'completed' at close exit \u2014 current: ${JSON.stringify(compassDoc.status)} (mstar-iteration \u00a73.4 / \u00a73.5 exit item 4)`));
5478
- }
5479
- const endDate = compassDoc.end_date;
5480
- if (typeof endDate !== "string" || !DATE_RE2.test(endDate)) {
5481
- violations.push(violation6("high", "EXIT_END_DATE_REQUIRED", "Compass frontmatter end_date (YYYY-MM-DD) is required when closing (mstar-iteration \u00a73.4 / \u00a73.5 exit item 4)"));
5482
- }
5483
- return violations;
5484
- }
5485
- function exitBranchCheck(opts) {
5486
- const violations = [];
5487
- const { currentBranch, specIntegrationBranch } = opts;
5488
- if (currentBranch === undefined || specIntegrationBranch === undefined) {
5489
- violations.push(violation6("medium", "EXIT_BRANCH_UNVERIFIABLE", "Cannot verify the current branch is spec_integration_branch \u2014 missing currentBranch / specIntegrationBranch probe inputs (mstar-iteration \u00a73.5 exit item 5)"));
5490
- } else if (currentBranch !== specIntegrationBranch) {
5491
- violations.push(violation6("high", "EXIT_BRANCH_MISMATCH", `Current branch '${currentBranch}' is not the spec_integration_branch '${specIntegrationBranch}' (mstar-iteration \u00a73.5 exit item 5)`));
5492
- }
5493
- return violations;
5494
- }
5495
- function exitPrBaseCheck(compassDoc, opts) {
5496
- const violations = [];
5497
- const target = compassDoc.target_branch;
5498
- const { prBaseBranch } = opts;
5499
- if (prBaseBranch === undefined) {
5500
- violations.push(violation6("medium", "EXIT_PR_BASE_UNVERIFIABLE", "Cannot verify the PR base \u2014 missing prBaseBranch probe input (mstar-iteration \u00a73.5 exit item 6)"));
5501
- } else if (typeof target !== "string" || prBaseBranch !== target) {
5502
- violations.push(violation6("high", "EXIT_PR_BASE_MISMATCH", `PR base '${prBaseBranch}' must equal the compass target_branch '${String(target)}' \u2014 not an undocumented branch (mstar-iteration \u00a73.5 exit item 6)`));
5503
- }
5504
- return violations;
5505
- }
5506
- function evaluatePhaseGate(snapshotDoc, compassDoc, opts = {}) {
5507
- const registered = registeredPlanIds(compassDoc);
5508
- const entryViolations = [
5509
- ...entryPlansAllDone(snapshotDoc, registered),
5510
- ...entryFrontmatterComplete(compassDoc)
5511
- ];
5512
- const exitViolations = [
5513
- ...exitFrontmatterClosed(compassDoc),
5514
- ...exitBranchCheck(opts),
5515
- ...exitPrBaseCheck(compassDoc, opts)
5516
- ];
5517
- const allPlansDone = registered.length > 0 && registered.every((planId) => {
5518
- const row = findPlanRow(snapshotDoc, planId);
5519
- return row !== null && row.status === PLAN_STATUS_DONE;
5520
- });
5521
- const entry = { ok: entryViolations.length === 0, violations: entryViolations };
5522
- const exit = { ok: exitViolations.length === 0, violations: exitViolations };
5523
- let transition;
5524
- if (!allPlansDone)
5525
- transition = "phase-2-execute";
5526
- else if (entry.ok && exit.ok)
5527
- transition = "phase-4-pr-delivery";
5528
- else
5529
- transition = "phase-3-close";
5530
- const gateBlocking = allPlansDone ? [...entryViolations, ...exitViolations] : [];
5531
- return {
5532
- transition,
5533
- allPlansDone,
5534
- entry,
5535
- exit,
5536
- ok: gateBlocking.length === 0,
5537
- violations: gateBlocking
5538
- };
5539
- }
5540
- function pushCadenceProbe(ciRunning, reviewWaveActive) {
5541
- const violations = [];
5542
- if (ciRunning) {
5543
- violations.push(violation6("high", "PUSH_BLOCKED_CI", "CI checks are still queued/in_progress on the current head \u2014 do not push until the wave completes (mstar-iteration \u00a75.1a push gate 1)", "Wait for CI to settle, then push once with the whole local batch"));
5544
- }
5545
- if (reviewWaveActive) {
5546
- violations.push(violation6("high", "PUSH_BLOCKED_REVIEW_WAVE", "An AI/bot review wave is still running on the current head \u2014 do not push until it settles (mstar-iteration \u00a75.1a push gate 2)", "Wait for the review wave, then push once"));
5547
- }
5548
- return { ok: violations.length === 0, violations };
5549
- }
5550
- function parseCompassFrontmatter(filePath) {
5551
- return parseCompassFrontmatterText(readFileSync6(filePath, "utf8"), filePath);
5552
- }
5553
- function parseCompassFrontmatterText(content, filePath) {
5554
- const lines = content.split(/\r?\n/);
5555
- if (lines[0]?.trim() !== "---") {
5556
- throw new Error(`no YAML frontmatter fence in ${filePath} (expected first line "---")`);
5557
- }
5558
- const end = lines.indexOf("---", 1);
5559
- if (end === -1) {
5560
- throw new Error(`unterminated YAML frontmatter in ${filePath} (no closing "---")`);
5561
- }
5562
- const doc = {};
5563
- let listKey = null;
5564
- for (let i = 1;i < end; i += 1) {
5565
- const line = lines[i] ?? "";
5566
- if (!line.trim() || line.trim().startsWith("#"))
5567
- continue;
5568
- if (listKey !== null && /^\s*-\s+/.test(line)) {
5569
- const item = line.replace(/^\s*-\s+/, "").trim().replace(/^["']|["']$/g, "");
5570
- if (!Array.isArray(doc[listKey]))
5571
- doc[listKey] = [];
5572
- doc[listKey].push(item);
5573
- continue;
5574
- }
5575
- listKey = null;
5576
- const kv = line.match(/^([A-Za-z_][A-Za-z0-9_-]*):\s*(.*)$/);
5577
- if (!kv) {
5578
- throw new Error(`unsupported frontmatter line in ${filePath}: ${JSON.stringify(line)}`);
5579
- }
5580
- const value = kv[2].trim();
5581
- doc[kv[1]] = value === "" ? null : /^\[.*\]$/.test(value) ? parseFlowArray(value, filePath) : value.replace(/^["']|["']$/g, "");
5582
- listKey = value === "" ? kv[1] : null;
5583
- }
5584
- return doc;
5585
- }
5586
- function parseFlowArray(raw, filePath) {
5587
- const inner = raw.slice(1, -1);
5588
- if (/[[\]]/.test(inner)) {
5589
- throw new Error(`nested flow-style array in ${filePath}: ${JSON.stringify(raw)} \u2014 only flat scalar items are supported (e.g. [a, b])`);
5590
- }
5591
- let quote = null;
5592
- for (const ch of inner) {
5593
- if (ch === '"' || ch === "'") {
5594
- if (quote === null)
5595
- quote = ch;
5596
- else if (quote === ch)
5597
- quote = null;
5598
- } else if (ch === "," && quote !== null) {
5599
- throw new Error(`ambiguous flow-style array in ${filePath}: ${JSON.stringify(raw)} \u2014 quoted item containing comma cannot be split unambiguously (flat scalar items only)`);
5600
- }
5601
- }
5602
- if (quote !== null) {
5603
- throw new Error(`unterminated ${quote} quote in flow-style array in ${filePath}: ${JSON.stringify(raw)}`);
5604
- }
5605
- const items = [];
5606
- for (const part of inner.split(",")) {
5607
- const item = part.trim().replace(/^["']|["']$/g, "");
5608
- if (item === "")
5609
- continue;
5610
- items.push(item);
5611
- }
5612
- return items;
5613
- }
5614
- var PROJECT_REGISTER_FILE = "residuals.json";
5615
- var _DEFAULT_PROJECT = "_default";
5616
- var ROLLUP_FIELDS = ["total_open", "by_severity", "by_target", "by_plan"];
5617
- function isPlainObject5(value) {
5618
- return typeof value === "object" && value !== null && !Array.isArray(value);
5619
- }
5620
- function violation7(severity, code, message, fix) {
5621
- return { ok: false, severity, code, message, fix };
5622
- }
5623
- function findingsCleanupGate(register, planId, opts) {
5624
- const mode = opts?.mode ?? "allow-residual";
5625
- const violations = [];
5626
- const entries = isPlainObject5(register.entries) ? register.entries[planId] : undefined;
5627
- if (entries === undefined) {
5628
- return { ok: true, violations };
5629
- }
5630
- if (!Array.isArray(entries)) {
5631
- violations.push(violation7("high", "project.register.invalid-entry-list", `entries[${JSON.stringify(planId)}] must be an array of residual entries (one entry per residual; v1 multi-finding semantics)`));
5632
- return { ok: false, violations };
5633
- }
5634
- if (entries.length === 0) {
5635
- return { ok: true, violations };
5636
- }
5637
- for (const entry of entries) {
5638
- if (!isOpenResidual(entry))
5639
- continue;
5640
- const id = typeof entry.id === "string" ? entry.id : "<unnamed>";
5641
- const label = `R#${id}`;
5642
- if (mode === "zero-residual") {
5643
- if (entry.severity === "nit") {
5644
- violations.push(violation7("medium", "findings.zero-residual-nit", `${label}: style-only nits must be fixed in-session or dropped \u2014 never left open under zero-residual`));
5645
- } else if (entry.decision === "risk-accepted" || entry.lifecycle === "waived") {
5646
- violations.push(violation7("medium", "findings.zero-residual-risk-accepted", `${label}: waived/risk-accepted findings must be closed/archived, not left open under zero-residual`));
5647
- } else if (entry.decision === "defer") {
5648
- if (typeof entry.target !== "string" || entry.target.trim() === "") {
5649
- violations.push(violation7("medium", "findings.zero-residual-defer-no-target", `${label}: blocker-defer requires a target (next iteration/milestone) under zero-residual`));
5650
- }
5651
- } else {
5652
- violations.push(violation7("medium", "findings.zero-residual-open-fixable", `${label}: fixable finding must not remain open under zero-residual \u2014 fix now or convert to a blocker-defer`));
5653
- }
5654
- } else if (normalizeSeverity(entry.severity) === "critical") {
5655
- violations.push(violation7("high", "findings.allow-residual-critical", `${label}: unresolved critical blocks Approve with residuals`));
5703
+ out = outFile;
5704
+ } else {
5705
+ const sddDir = opts.sddDir ?? process.env.SDD_DIR;
5706
+ if (!sddDir) {
5707
+ throw new SddScriptError("mstar sdd task-brief: set SDD_DIR or pass OUTFILE (run mstar sdd workspace PLAN_ID first)", 2);
5656
5708
  }
5709
+ mkdirSync5(sddDir, { recursive: true });
5710
+ out = join9(sddDir, `task-${taskN}-brief.md`);
5657
5711
  }
5658
- return { ok: violations.length === 0, violations };
5659
- }
5660
- function groupCount(values) {
5661
- const counts = new Map;
5662
- for (const value of values) {
5663
- const key = typeof value === "string" ? value : String(value);
5664
- counts.set(key, (counts.get(key) ?? 0) + 1);
5712
+ const records = content.endsWith(`
5713
+ `) ? content.split(`
5714
+ `).slice(0, -1) : content.split(`
5715
+ `);
5716
+ const headingRe = /^#+[ \t]+Task[ \t]+[0-9]+/;
5717
+ const targetRe = new RegExp(`^#+[ ]+Task[ ]+${taskN}([^0-9]|$)`);
5718
+ let infence = false;
5719
+ let intask = false;
5720
+ const printed = [];
5721
+ for (const line of records) {
5722
+ if (/^```/.test(line))
5723
+ infence = !infence;
5724
+ if (!infence && headingRe.test(line))
5725
+ intask = targetRe.test(line);
5726
+ if (intask)
5727
+ printed.push(line);
5665
5728
  }
5666
- return Object.fromEntries([...counts.entries()].sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0));
5729
+ const output = printed.length > 0 ? `${printed.join(`
5730
+ `)}
5731
+ ` : "";
5732
+ writeFileSync4(out, output);
5733
+ if (printed.length === 0) {
5734
+ throw new SddScriptError(`task ${taskN} not found in ${planFile} (no heading matching Task ${taskN})`, 3);
5735
+ }
5736
+ return out;
5667
5737
  }
5668
- function techDebtRollup(projectDir) {
5669
- const items = [];
5670
- let entries;
5671
- try {
5672
- entries = readdirSync5(projectDir, { withFileTypes: true });
5673
- } catch {
5674
- entries = [];
5738
+ function reviewPackage(base, head, outFile, opts = {}) {
5739
+ if (!base || !head) {
5740
+ throw new SddScriptError("usage: mstar sdd review-package BASE HEAD [OUTFILE]", 2);
5675
5741
  }
5676
- for (const project of entries) {
5677
- if (!project.isDirectory())
5678
- continue;
5679
- const registerPath = join9(projectDir, project.name, PROJECT_REGISTER_FILE);
5680
- if (!existsSync6(registerPath))
5681
- continue;
5682
- let register;
5742
+ const cwd = opts.cwd ?? process.cwd();
5743
+ const verifyRef = (ref, what) => {
5683
5744
  try {
5684
- register = readJson(registerPath);
5745
+ execFileSync3("git", ["rev-parse", "--verify", "--quiet", ref], { cwd, stdio: ["ignore", "pipe", "pipe"] });
5685
5746
  } catch {
5686
- continue;
5747
+ throw new SddScriptError(`bad ${what}: ${ref}`, 2);
5687
5748
  }
5688
- if (!isPlainObject5(register) || !isPlainObject5(register.entries))
5689
- continue;
5690
- for (const [plan, planEntries] of Object.entries(register.entries)) {
5691
- if (!Array.isArray(planEntries))
5692
- continue;
5693
- for (const entry of planEntries) {
5694
- if (!isPlainObject5(entry) || !isOpenResidual(entry))
5695
- continue;
5696
- items.push({ plan, entry });
5697
- }
5749
+ };
5750
+ verifyRef(base, "BASE");
5751
+ verifyRef(head, "HEAD");
5752
+ let out;
5753
+ if (outFile) {
5754
+ out = outFile;
5755
+ } else {
5756
+ const sddDir = opts.sddDir ?? process.env.SDD_DIR;
5757
+ if (!sddDir) {
5758
+ throw new SddScriptError("mstar sdd review-package: set SDD_DIR or pass OUTFILE", 2);
5698
5759
  }
5760
+ mkdirSync5(sddDir, { recursive: true });
5761
+ const shortBase = gitOut(cwd, ["rev-parse", "--short", base]) ?? base;
5762
+ const shortHead = gitOut(cwd, ["rev-parse", "--short", head]) ?? head;
5763
+ out = join9(sddDir, `review-${shortBase}..${shortHead}.diff`);
5699
5764
  }
5700
- const bySeverity = {};
5701
- for (const severity of SEVERITY_ORDER) {
5702
- bySeverity[severity] = items.filter(({ entry }) => normalizeSeverity(entry.severity) === severity).length;
5703
- }
5704
- const computed = {
5705
- total_open: items.length,
5706
- by_severity: bySeverity,
5707
- by_target: groupCount(items.map(({ entry }) => entry.target ?? "unspecified")),
5708
- by_plan: groupCount(items.map(({ plan }) => plan))
5709
- };
5710
- const stored = null;
5711
- const checks = ROLLUP_FIELDS.map((field) => ({ field, status: "DRIFT" }));
5712
- const overall = "DRIFT";
5713
- return { computed, stored, checks, overall };
5765
+ const run = (args) => execFileSync3("git", args, { cwd, maxBuffer: GIT_CAPTURE_MAX_BYTES });
5766
+ const parts = [
5767
+ Buffer.from(`# Review package: ${base}..${head}
5768
+
5769
+ ## Commits
5770
+ `),
5771
+ run(["log", "--oneline", `${base}..${head}`]),
5772
+ Buffer.from(`
5773
+ ## Files changed
5774
+ `),
5775
+ run(["diff", "--stat", `${base}..${head}`]),
5776
+ Buffer.from(`
5777
+ ## Diff
5778
+ `),
5779
+ run(["diff", "-U10", `${base}..${head}`])
5780
+ ];
5781
+ writeFileSync4(out, Buffer.concat(parts));
5782
+ return out;
5714
5783
  }
5715
5784
  function violation8(severity, code, message, fix) {
5716
5785
  return { ok: false, severity, code, message, fix };
@@ -6186,8 +6255,17 @@ function readPlanFileSummary(filePath) {
6186
6255
  const blocks = parseStatusBlocks(text);
6187
6256
  return { title: title.trim(), fields: blocks.length > 0 ? blocks[0].fields : new Map };
6188
6257
  }
6258
+ function extractSecurityDispositionSections(text) {
6259
+ const grab = (heading) => {
6260
+ const match = text.match(new RegExp(`(?:^|\\r?\\n)##[ \\t]+${heading}[ \\t]*\\r?\\n(?:[ \\t]*\\r?\\n)?([\\s\\S]*?)(?=\\r?\\n## |$)`, "i"));
6261
+ if (!match)
6262
+ return [];
6263
+ return match[1].split(/\r?\n/).map((line) => line.replace(/\r$/, "")).filter((line) => line.startsWith("- "));
6264
+ };
6265
+ return { needsVerification: grab("Needs verification"), hardeningChecked: grab("Hardening & checked notes") };
6266
+ }
6189
6267
  function renderIndex(params) {
6190
- const { date, repoName, repoShortSha, rows, rejected } = params;
6268
+ const { date, repoName, repoShortSha, rows, rejected, needsVerification, hardeningChecked } = params;
6191
6269
  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(`
6192
6270
  `);
6193
6271
  const directionRows = rows.filter((r) => r.category === "direction").map((r) => `- ${escapeCell(r.title)} \u2014 ${escapeCell(truncate(r.impact, 120))}`).join(`
@@ -6208,6 +6286,12 @@ function renderIndex(params) {
6208
6286
  if (directionRows !== "") {
6209
6287
  sections.push("", "## Direction", "", directionRows);
6210
6288
  }
6289
+ if (needsVerification.length > 0) {
6290
+ sections.push("", "## Needs verification", "", ...needsVerification);
6291
+ }
6292
+ if (hardeningChecked.length > 0) {
6293
+ sections.push("", "## Hardening & checked notes", "", ...hardeningChecked);
6294
+ }
6211
6295
  sections.push("", "## Execution order & status", "", "| Plan | Title | Priority | Effort | Depends on | Status |", "|------|-------|----------|--------|------------|--------|", executionRows);
6212
6296
  if (rejectedRows !== "") {
6213
6297
  sections.push("", "## Findings considered and rejected", "", rejectedRows);
@@ -6221,6 +6305,8 @@ function scaffoldAuditPlan(outDir, findings, options = {}) {
6221
6305
  const date = options.date ?? new Date().toISOString().slice(0, 10);
6222
6306
  const plannedAt = options.plannedAt ?? { commit: options.repoShortSha ?? "unknown", date };
6223
6307
  mkdirSync7(outDir, { recursive: true });
6308
+ const existingReadme = join11(outDir, "README.md");
6309
+ const carried = existsSync7(existingReadme) ? extractSecurityDispositionSections(readFileSync9(existingReadme, "utf8")) : { needsVerification: [], hardeningChecked: [] };
6224
6310
  const existing = readdirSync7(outDir).filter((f) => /^\d{3}-.*\.md$/.test(f));
6225
6311
  let next = existing.reduce((max, f) => Math.max(max, Number(f.slice(0, 3))), 0) + 1;
6226
6312
  const written = [];
@@ -6236,7 +6322,7 @@ function scaffoldAuditPlan(outDir, findings, options = {}) {
6236
6322
  }
6237
6323
  usedSlugs.add(slug);
6238
6324
  const file = `${num}-${slug}.md`;
6239
- writeFileSync5(join11(outDir, file), renderPlanFile(finding, plannedAt));
6325
+ writeFileSync6(join11(outDir, file), renderPlanFile(finding, plannedAt));
6240
6326
  written.push(file);
6241
6327
  next++;
6242
6328
  }
@@ -6274,12 +6360,16 @@ function scaffoldAuditPlan(outDir, findings, options = {}) {
6274
6360
  row.dependsOn = finding.dependsOn ?? "none";
6275
6361
  }
6276
6362
  });
6277
- writeFileSync5(join11(outDir, "README.md"), renderIndex({
6363
+ const needsVerificationLines = options.needsVerification !== undefined ? options.needsVerification.map((nv) => `- ${escapeCell(nv.lead)}: ${escapeCell(nv.how)}${nv.evidence ? ` (${escapeCell(nv.evidence)})` : ""}`) : carried.needsVerification;
6364
+ const hardeningCheckedLines = options.hardeningChecked !== undefined ? options.hardeningChecked.map((hc) => `- ${hc.kind}: ${escapeCell(hc.text)}`) : carried.hardeningChecked;
6365
+ writeFileSync6(join11(outDir, "README.md"), renderIndex({
6278
6366
  date,
6279
6367
  repoName: options.repoName ?? "repo",
6280
6368
  repoShortSha: options.repoShortSha ?? "unknown",
6281
6369
  rows,
6282
- rejected: options.rejected ?? []
6370
+ rejected: options.rejected ?? [],
6371
+ needsVerification: needsVerificationLines,
6372
+ hardeningChecked: hardeningCheckedLines
6283
6373
  }));
6284
6374
  return { outDir: resolve9(outDir), date, files: written, nextNumber: next };
6285
6375
  }
@@ -7218,24 +7308,24 @@ function lintFiveQuestion(bodyText, mode = "authoring") {
7218
7308
  }
7219
7309
 
7220
7310
  // ../engine/dist/engine.js
7221
- import { existsSync as existsSync10, mkdirSync as mkdirSync4, readFileSync as readFileSync8, renameSync as renameSync2, unlinkSync as unlinkSync3, writeFileSync as writeFileSync4 } from "node:fs";
7311
+ import { existsSync as existsSync10, mkdirSync as mkdirSync3, readFileSync as readFileSync8, renameSync as renameSync2, unlinkSync as unlinkSync3, writeFileSync as writeFileSync5 } from "node:fs";
7222
7312
  import { randomUUID as randomUUID2 } from "node:crypto";
7223
- import { basename as basename6, dirname as dirname7, join as join5, resolve as resolve8 } from "node:path";
7313
+ import { basename as basename6, dirname as dirname7, join as join3, resolve as resolve8 } from "node:path";
7224
7314
  import { fileURLToPath } from "node:url";
7225
7315
  import { readFileSync as readFileSync22, statSync as statSync5 } from "node:fs";
7226
7316
  import { dirname as dirname22, isAbsolute as isAbsolute6, join as join22, relative as relative3, resolve as resolve22 } from "node:path";
7227
- import { existsSync as existsSync22, mkdirSync as mkdirSync22, readdirSync as readdirSync6, readFileSync as readFileSync32, realpathSync as realpathSync4, statSync as statSync22 } from "node:fs";
7317
+ import { existsSync as existsSync52, mkdirSync as mkdirSync42, readdirSync as readdirSync42, readFileSync as readFileSync62, realpathSync as realpathSync22, statSync as statSync32, writeFileSync as writeFileSync32 } from "node:fs";
7228
7318
  import { execFileSync as execFileSync4 } from "node:child_process";
7229
- import { basename as basename22, dirname as dirname32, isAbsolute as isAbsolute22, join as join32, relative as relative22, resolve as resolve32 } from "node:path";
7230
- import { existsSync as existsSync32, readFileSync as readFileSync42, readdirSync as readdirSync22, realpathSync as realpathSync22 } from "node:fs";
7231
- import { dirname as dirname52, join as join62, resolve as resolve52, sep as sep2 } from "node:path";
7232
- import { mkdirSync as mkdirSync32, rmdirSync as rmdirSync3, statSync as statSync32, unlinkSync as unlinkSync22, writeFileSync as writeFileSync22 } from "node:fs";
7233
- import { dirname as dirname42, isAbsolute as isAbsolute32, join as join42, resolve as resolve42 } from "node:path";
7319
+ import { basename as basename22, dirname as dirname52, isAbsolute as isAbsolute32, join as join82, relative as relative22, resolve as resolve52 } from "node:path";
7320
+ import { existsSync as existsSync32, readFileSync as readFileSync42, readdirSync as readdirSync22, realpathSync as realpathSync4 } from "node:fs";
7321
+ import { dirname as dirname42, join as join62, resolve as resolve42, sep as sep2 } from "node:path";
7322
+ import { mkdirSync as mkdirSync22, rmdirSync as rmdirSync3, statSync as statSync22, unlinkSync as unlinkSync22, writeFileSync as writeFileSync22 } from "node:fs";
7323
+ import { dirname as dirname32, isAbsolute as isAbsolute22, join as join42, resolve as resolve32 } from "node:path";
7234
7324
  import { setTimeout as sleep2 } from "node:timers/promises";
7235
7325
  import { AsyncLocalStorage as AsyncLocalStorage3 } from "node:async_hooks";
7236
- import { mkdirSync as mkdirSync42 } from "node:fs";
7237
- import { join as join52 } from "node:path";
7238
- import { copyFileSync, mkdirSync as mkdirSync6, readFileSync as readFileSync82, readdirSync as readdirSync62, writeFileSync as writeFileSync42 } from "node:fs";
7326
+ import { mkdirSync as mkdirSync32 } from "node:fs";
7327
+ import { join as join5 } from "node:path";
7328
+ import { copyFileSync, mkdirSync as mkdirSync6, readFileSync as readFileSync82, readdirSync as readdirSync6, writeFileSync as writeFileSync52 } from "node:fs";
7239
7329
  import { dirname as dirname72, isAbsolute as isAbsolute62, join as join10, relative as relative32, resolve as resolve82, sep as sep22 } from "node:path";
7240
7330
  var SEVERITY_ORDER2 = ["critical", "high", "medium", "low", "nit"];
7241
7331
  function readJson2(filePath) {
@@ -7252,10 +7342,10 @@ function readJson2(filePath) {
7252
7342
  }
7253
7343
  function writeJson2(filePath, value) {
7254
7344
  const parent = dirname7(filePath);
7255
- mkdirSync4(parent, { recursive: true });
7256
- const tmp = join5(parent, `.${basename6(filePath)}.${process.pid}.${randomUUID2()}.tmp`);
7345
+ mkdirSync3(parent, { recursive: true });
7346
+ const tmp = join3(parent, `.${basename6(filePath)}.${process.pid}.${randomUUID2()}.tmp`);
7257
7347
  try {
7258
- writeFileSync4(tmp, `${JSON.stringify(value, null, 2)}
7348
+ writeFileSync5(tmp, `${JSON.stringify(value, null, 2)}
7259
7349
  `, "utf8");
7260
7350
  renameSync2(tmp, filePath);
7261
7351
  } catch (error) {
@@ -7269,7 +7359,7 @@ function resolveProjectRoot(startDir = process.cwd()) {
7269
7359
  const start = resolve8(startDir);
7270
7360
  let dir = start;
7271
7361
  for (;; ) {
7272
- if (existsSync10(join5(dir, "package.json")) || existsSync10(join5(dir, "bun.lock")))
7362
+ if (existsSync10(join3(dir, "package.json")) || existsSync10(join3(dir, "bun.lock")))
7273
7363
  return dir;
7274
7364
  const parent = dirname7(dir);
7275
7365
  if (parent === dir)
@@ -7293,7 +7383,7 @@ function findRootPackageJson(startDir) {
7293
7383
  }
7294
7384
  }
7295
7385
  function harnessVersionFrom(moduleDir) {
7296
- const ownManifest = join5(moduleDir, "..", "package.json");
7386
+ const ownManifest = join3(moduleDir, "..", "package.json");
7297
7387
  try {
7298
7388
  const pkg = JSON.parse(readFileSync8(ownManifest, "utf8"));
7299
7389
  if (typeof pkg.version === "string" && pkg.version !== "")
@@ -7397,121 +7487,78 @@ function isAtOrBelow3(dir, root) {
7397
7487
  const rel = relative3(root, dir);
7398
7488
  return rel === "" || !rel.startsWith("..") && !isAbsolute6(rel);
7399
7489
  }
7400
- function resolveHarnessDir2(startDir = process.cwd(), opts = {}) {
7401
- const start = resolve32(startDir);
7402
- const explicit = opts.harnessDir ?? process.env.MSTAR_HARNESS_DIR;
7403
- if (explicit)
7404
- return resolve32(start, explicit);
7405
- const boundary = resolve32(start, opts.workspaceRoot ?? defaultWorkspaceRoot2(start));
7406
- const rc = loadMstarc2(start, boundary);
7407
- if (rc !== null && rc.config.harnessDir)
7408
- return resolve32(rc.dir, rc.config.harnessDir);
7409
- let dir = start;
7410
- for (;; ) {
7411
- if (!isAtOrBelow22(dir, boundary))
7412
- return null;
7413
- for (const candidate of [join32(dir, ".mstar"), join32(dir, ".agents"), join32(dir, ".plans"), join32(dir, "plans")]) {
7414
- if (isDirectory3(candidate))
7415
- return candidate;
7490
+ function parseCompassFrontmatterText2(content, filePath) {
7491
+ const lines = content.split(/\r?\n/);
7492
+ if (lines[0]?.trim() !== "---") {
7493
+ throw new Error(`no YAML frontmatter fence in ${filePath} (expected first line "---")`);
7494
+ }
7495
+ const end = lines.indexOf("---", 1);
7496
+ if (end === -1) {
7497
+ throw new Error(`unterminated YAML frontmatter in ${filePath} (no closing "---")`);
7498
+ }
7499
+ const doc = {};
7500
+ let listKey = null;
7501
+ for (let i = 1;i < end; i += 1) {
7502
+ const line = lines[i] ?? "";
7503
+ if (!line.trim() || line.trim().startsWith("#"))
7504
+ continue;
7505
+ if (listKey !== null && /^\s*-\s+/.test(line)) {
7506
+ const item = line.replace(/^\s*-\s+/, "").trim().replace(/^["']|["']$/g, "");
7507
+ if (!Array.isArray(doc[listKey]))
7508
+ doc[listKey] = [];
7509
+ doc[listKey].push(item);
7510
+ continue;
7416
7511
  }
7417
- if (dir === boundary)
7418
- return null;
7419
- const parent = dirname32(dir);
7420
- if (parent === dir)
7421
- return null;
7422
- dir = parent;
7512
+ listKey = null;
7513
+ const kv = line.match(/^([A-Za-z_][A-Za-z0-9_-]*):\s*(.*)$/);
7514
+ if (!kv) {
7515
+ throw new Error(`unsupported frontmatter line in ${filePath}: ${JSON.stringify(line)}`);
7516
+ }
7517
+ const value = kv[2].trim();
7518
+ doc[kv[1]] = value === "" ? null : /^\[.*\]$/.test(value) ? parseFlowArray2(value, filePath) : value.replace(/^["']|["']$/g, "");
7519
+ listKey = value === "" ? kv[1] : null;
7423
7520
  }
7521
+ return doc;
7424
7522
  }
7425
- function defaultWorkspaceRoot2(startDir) {
7426
- try {
7427
- const cdup = execFileSync4("git", ["rev-parse", "--show-cdup"], {
7428
- cwd: startDir,
7429
- encoding: "utf8",
7430
- stdio: ["ignore", "pipe", "ignore"]
7431
- }).trim();
7432
- if (!cdup)
7433
- return startDir;
7434
- let boundary = startDir;
7435
- for (const segment of cdup.split(/[\\/]/)) {
7436
- if (segment && segment !== ".")
7437
- boundary = dirname32(boundary);
7523
+ function parseFlowArray2(raw, filePath) {
7524
+ const inner = raw.slice(1, -1);
7525
+ if (/[[\]]/.test(inner)) {
7526
+ throw new Error(`nested flow-style array in ${filePath}: ${JSON.stringify(raw)} \u2014 only flat scalar items are supported (e.g. [a, b])`);
7527
+ }
7528
+ let quote = null;
7529
+ for (const ch of inner) {
7530
+ if (ch === '"' || ch === "'") {
7531
+ if (quote === null)
7532
+ quote = ch;
7533
+ else if (quote === ch)
7534
+ quote = null;
7535
+ } else if (ch === "," && quote !== null) {
7536
+ throw new Error(`ambiguous flow-style array in ${filePath}: ${JSON.stringify(raw)} \u2014 quoted item containing comma cannot be split unambiguously (flat scalar items only)`);
7438
7537
  }
7439
- return resolve32(boundary);
7440
- } catch {}
7441
- return startDir;
7442
- }
7443
- function isAtOrBelow22(dir, root) {
7444
- const rel = relative22(root, dir);
7445
- return rel === "" || !rel.startsWith("..") && !isAbsolute22(rel);
7446
- }
7447
- function mstarcDirOverride2(harnessDir, key) {
7448
- const dir = resolve32(harnessDir);
7449
- const rc = loadMstarc2(dir, dirname32(dir));
7450
- const declared = rc?.config[key];
7451
- return declared ? resolve32(rc.dir, declared) : null;
7452
- }
7453
- function assertSafePathComponent2(value, what) {
7454
- if (value === "" || value === "." || value === ".." || !/^[A-Za-z0-9._-]+$/.test(value)) {
7455
- throw new Error(`${what} must be a single safe path component ([A-Za-z0-9._-]+; not "", ".", "..", or containing "/" or "\\") \u2014 got ${JSON.stringify(value)}`);
7456
7538
  }
7457
- }
7458
- function resolveHarnessSubdir2(startDir, opts, key, fallback) {
7459
- const harness = resolveHarnessDir2(startDir, opts);
7460
- if (harness === null) {
7461
- throw new Error(`harness dir not found from ${resolve32(startDir)} \u2014 cannot resolve the ${fallback} dir (run \`mstar init\`, pass opts.harnessDir, or set MSTAR_HARNESS_DIR)`);
7539
+ if (quote !== null) {
7540
+ throw new Error(`unterminated ${quote} quote in flow-style array in ${filePath}: ${JSON.stringify(raw)}`);
7462
7541
  }
7463
- const declared = mstarcDirOverride2(harness, key);
7464
- return declared !== null ? declared : join32(resolve32(harness), fallback);
7465
- }
7466
- function resolveWorkflowDir2(startDir = process.cwd(), opts = {}) {
7467
- return resolveHarnessSubdir2(startDir, opts, "workflowDir", "workflows");
7468
- }
7469
- function resolveProjectDir2(startDir = process.cwd(), opts = {}) {
7470
- return resolveHarnessSubdir2(startDir, opts, "projectDir", "projects");
7471
- }
7472
- var GITIGNORE_SNIPPET2 = `# Morning Star harness (.mstar/)
7473
- # Principle: process stays local; results are shared with the team.
7474
- # Default-ignore everything under .mstar/, then re-include the tracked results.
7475
- .mstar/**
7476
- !.mstar/AGENTS.md
7477
- !.mstar/knowledge/
7478
- !.mstar/knowledge/**
7479
- !.mstar/specs/
7480
- !.mstar/specs/**
7481
- # .mstarc \u2014 repo-local harness config (may declare [config] harness_dir=<name>)
7482
- .mstarc
7483
- `;
7484
- var GITIGNORE_SNIPPET_AGENTS2 = `# Morning Star harness (.agents/) \u2014 legacy
7485
- # Default-ignore everything under .agents/, then re-include the tracked results.
7486
- .agents/**
7487
- !.agents/AGENTS.md
7488
- !.agents/knowledge/
7489
- !.agents/knowledge/**
7490
- !.agents/specs/
7491
- !.agents/specs/**
7492
- `;
7493
- var GITIGNORE_PROCESS_ENTRIES2 = GITIGNORE_SNIPPET2.split(`
7494
- `).filter((line) => line.startsWith(".mstar/") || line.startsWith("!.mstar/")).map((line) => line.trim());
7495
- var GITIGNORE_PROCESS_ENTRIES_AGENTS2 = GITIGNORE_SNIPPET_AGENTS2.split(`
7496
- `).filter((line) => line.startsWith(".agents/") || line.startsWith("!.agents/")).map((line) => line.trim());
7497
- function isDirectory3(dir) {
7498
- try {
7499
- return statSync22(dir).isDirectory();
7500
- } catch {
7501
- return false;
7542
+ const items = [];
7543
+ for (const part of inner.split(",")) {
7544
+ const item = part.trim().replace(/^["']|["']$/g, "");
7545
+ if (item === "")
7546
+ continue;
7547
+ items.push(item);
7502
7548
  }
7549
+ return items;
7503
7550
  }
7504
- function isPlainObject6(value) {
7551
+ function isPlainObject23(value) {
7505
7552
  return typeof value === "object" && value !== null && !Array.isArray(value);
7506
7553
  }
7507
- function violation9(severity, code, message, fix) {
7554
+ function violation22(severity, code, message, fix) {
7508
7555
  return { ok: false, severity, code, message, fix };
7509
7556
  }
7510
7557
  function validateNonEmptyString4(violations, value, field, missingCode, invalidCode) {
7511
7558
  if (value === undefined) {
7512
- violations.push(violation9("high", missingCode, `missing required field: ${field}`));
7559
+ violations.push(violation22("high", missingCode, `missing required field: ${field}`));
7513
7560
  } else if (typeof value !== "string" || value.trim() === "") {
7514
- violations.push(violation9("medium", invalidCode, `${field} must be a non-empty string`));
7561
+ violations.push(violation22("medium", invalidCode, `${field} must be a non-empty string`));
7515
7562
  }
7516
7563
  }
7517
7564
  var DATE_PART2 = String.raw`\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12]\d|3[01])`;
@@ -7522,54 +7569,54 @@ function isValidClaimedAt2(value) {
7522
7569
  }
7523
7570
  function validateExecutionLease2(lease) {
7524
7571
  const violations = [];
7525
- if (!isPlainObject6(lease)) {
7572
+ if (!isPlainObject23(lease)) {
7526
7573
  return {
7527
7574
  ok: false,
7528
7575
  violations: [
7529
- violation9("high", "lease.execution-lease.invalid", "execution_lease must be an object \u2014 null and tombstone objects are invalid; writers delete the key on release")
7576
+ violation22("high", "lease.execution-lease.invalid", "execution_lease must be an object \u2014 null and tombstone objects are invalid; writers delete the key on release")
7530
7577
  ]
7531
7578
  };
7532
7579
  }
7533
7580
  validateNonEmptyString4(violations, lease.holder, "holder", "lease.execution-lease.missing-holder", "lease.execution-lease.invalid-holder");
7534
7581
  if (lease.claimed_at === undefined) {
7535
- violations.push(violation9("high", "lease.execution-lease.missing-claimed-at", "missing required field: claimed_at"));
7582
+ violations.push(violation22("high", "lease.execution-lease.missing-claimed-at", "missing required field: claimed_at"));
7536
7583
  } else if (!isValidClaimedAt2(lease.claimed_at)) {
7537
- violations.push(violation9("medium", "lease.execution-lease.invalid-claimed-at", "claimed_at must be an RFC 3339 UTC timestamp with explicit Z (e.g. 2026-07-22T02:30:00Z) or a YYYY-MM-DD date"));
7584
+ violations.push(violation22("medium", "lease.execution-lease.invalid-claimed-at", "claimed_at must be an RFC 3339 UTC timestamp with explicit Z (e.g. 2026-07-22T02:30:00Z) or a YYYY-MM-DD date"));
7538
7585
  }
7539
7586
  if (lease.worktree_path === undefined) {
7540
- violations.push(violation9("high", "lease.execution-lease.missing-worktree-path", "missing required field: worktree_path"));
7587
+ violations.push(violation22("high", "lease.execution-lease.missing-worktree-path", "missing required field: worktree_path"));
7541
7588
  } else if (typeof lease.worktree_path !== "string" || lease.worktree_path.trim() === "") {
7542
- violations.push(violation9("medium", "lease.execution-lease.invalid-worktree-path", "worktree_path must be a non-empty string"));
7543
- } else if (!isAbsolute32(lease.worktree_path)) {
7544
- violations.push(violation9("medium", "lease.execution-lease.invalid-worktree-path", "worktree_path must be an absolute path \u2014 it identifies the dedicated feature-worktree root (and MUST differ from metadata.control_worktree_path)"));
7589
+ violations.push(violation22("medium", "lease.execution-lease.invalid-worktree-path", "worktree_path must be a non-empty string"));
7590
+ } else if (!isAbsolute22(lease.worktree_path)) {
7591
+ violations.push(violation22("medium", "lease.execution-lease.invalid-worktree-path", "worktree_path must be an absolute path \u2014 it identifies the dedicated feature-worktree root (and MUST differ from metadata.control_worktree_path)"));
7545
7592
  }
7546
7593
  validateNonEmptyString4(violations, lease.working_branch, "working_branch", "lease.execution-lease.missing-working-branch", "lease.execution-lease.invalid-working-branch");
7547
7594
  if (lease.session_label !== undefined && typeof lease.session_label !== "string") {
7548
- violations.push(violation9("medium", "lease.execution-lease.invalid-session-label", "session_label must be a string (display only \u2014 never used for ownership comparison)"));
7595
+ violations.push(violation22("medium", "lease.execution-lease.invalid-session-label", "session_label must be a string (display only \u2014 never used for ownership comparison)"));
7549
7596
  }
7550
7597
  return { ok: violations.length === 0, violations };
7551
7598
  }
7552
7599
  function validateIntegrationMergeLease2(lease) {
7553
7600
  const violations = [];
7554
- if (!isPlainObject6(lease)) {
7601
+ if (!isPlainObject23(lease)) {
7555
7602
  return {
7556
7603
  ok: false,
7557
7604
  violations: [
7558
- violation9("high", "lease.merge-lease.invalid", "integration_merge_lease must be an object \u2014 absent means unclaimed; null and tombstone objects are invalid; writers delete the key on release")
7605
+ violation22("high", "lease.merge-lease.invalid", "integration_merge_lease must be an object \u2014 absent means unclaimed; null and tombstone objects are invalid; writers delete the key on release")
7559
7606
  ]
7560
7607
  };
7561
7608
  }
7562
7609
  validateNonEmptyString4(violations, lease.holder, "holder", "lease.merge-lease.missing-holder", "lease.merge-lease.invalid-holder");
7563
7610
  if (lease.claimed_at === undefined) {
7564
- violations.push(violation9("high", "lease.merge-lease.missing-claimed-at", "missing required field: claimed_at"));
7611
+ violations.push(violation22("high", "lease.merge-lease.missing-claimed-at", "missing required field: claimed_at"));
7565
7612
  } else if (!isValidClaimedAt2(lease.claimed_at)) {
7566
- violations.push(violation9("medium", "lease.merge-lease.invalid-claimed-at", "claimed_at must be an RFC 3339 UTC timestamp with explicit Z (e.g. 2026-07-22T04:00:00Z) or a YYYY-MM-DD date"));
7613
+ violations.push(violation22("medium", "lease.merge-lease.invalid-claimed-at", "claimed_at must be an RFC 3339 UTC timestamp with explicit Z (e.g. 2026-07-22T04:00:00Z) or a YYYY-MM-DD date"));
7567
7614
  }
7568
7615
  validateNonEmptyString4(violations, lease.plan_id, "plan_id", "lease.merge-lease.missing-plan-id", "lease.merge-lease.invalid-plan-id");
7569
7616
  validateNonEmptyString4(violations, lease.source_branch, "source_branch", "lease.merge-lease.missing-source-branch", "lease.merge-lease.invalid-source-branch");
7570
7617
  validateNonEmptyString4(violations, lease.target_branch, "target_branch", "lease.merge-lease.missing-target-branch", "lease.merge-lease.invalid-target-branch");
7571
7618
  if (lease.session_label !== undefined && typeof lease.session_label !== "string") {
7572
- violations.push(violation9("medium", "lease.merge-lease.invalid-session-label", "session_label must be a string (display only \u2014 never used for ownership comparison)"));
7619
+ violations.push(violation22("medium", "lease.merge-lease.invalid-session-label", "session_label must be a string (display only \u2014 never used for ownership comparison)"));
7573
7620
  }
7574
7621
  return { ok: violations.length === 0, violations };
7575
7622
  }
@@ -7584,14 +7631,14 @@ function verifyPlanExecutionLease(row, planId) {
7584
7631
  return {
7585
7632
  ok: false,
7586
7633
  violations: [
7587
- violation9("high", "lease.verify.orphan", "plan is InProgress without an execution_lease \u2014 orphan: STOP, no writable dispatch until recovery (status-and-residuals.md \u00a7 Orphan recovery)")
7634
+ violation22("high", "lease.verify.orphan", "plan is InProgress without an execution_lease \u2014 orphan: STOP, no writable dispatch until recovery (status-and-residuals.md \u00a7 Orphan recovery)")
7588
7635
  ]
7589
7636
  };
7590
7637
  }
7591
7638
  return {
7592
7639
  ok: false,
7593
7640
  violations: [
7594
- violation9("high", "lease.verify.missing", `plan ${planId} has no execution_lease at the SSOT location plans[].execution_lease`)
7641
+ violation22("high", "lease.verify.missing", `plan ${planId} has no execution_lease at the SSOT location plans[].execution_lease`)
7595
7642
  ]
7596
7643
  };
7597
7644
  }
@@ -7603,7 +7650,7 @@ var STATUS_WRITE_LOCKDIR2 = ".status-write.lockdir";
7603
7650
  var LOCKDIR_HOLDER_PID2 = "holder.pid";
7604
7651
  var heldLockDirs2 = new AsyncLocalStorage3;
7605
7652
  async function withStatusWriteLock2(statusPath, fn, opts = {}) {
7606
- const lockDir = join42(dirname42(resolve42(statusPath)), STATUS_WRITE_LOCKDIR2);
7653
+ const lockDir = join42(dirname32(resolve32(statusPath)), STATUS_WRITE_LOCKDIR2);
7607
7654
  const held = heldLockDirs2.getStore();
7608
7655
  if (held !== undefined && held.has(lockDir)) {
7609
7656
  throw new Error(`${lockDir} is already held by this process in this async context \u2014 withStatusWriteLock is not reentrant; a nested acquisition on the same status.json is a bug`);
@@ -7614,8 +7661,8 @@ async function withStatusWriteLock2(statusPath, fn, opts = {}) {
7614
7661
  let acquired = null;
7615
7662
  for (;; ) {
7616
7663
  try {
7617
- mkdirSync32(lockDir);
7618
- const st = statSync32(lockDir);
7664
+ mkdirSync22(lockDir);
7665
+ const st = statSync22(lockDir);
7619
7666
  acquired = { dev: st.dev, ino: st.ino };
7620
7667
  break;
7621
7668
  } catch (error) {
@@ -7637,7 +7684,7 @@ async function withStatusWriteLock2(statusPath, fn, opts = {}) {
7637
7684
  } finally {
7638
7685
  owns.delete(lockDir);
7639
7686
  try {
7640
- const current = statSync32(lockDir);
7687
+ const current = statSync22(lockDir);
7641
7688
  if (acquired !== null && current.dev === acquired.dev && current.ino === acquired.ino) {
7642
7689
  try {
7643
7690
  unlinkSync22(join42(lockDir, LOCKDIR_HOLDER_PID2));
@@ -7651,45 +7698,45 @@ var WORKFLOW_SNAPSHOT_FILE2 = "snapshot.json";
7651
7698
  var WORKFLOW_LIFECYCLE_STATUSES2 = ["running", "paused", "completed", "failed", "stopped"];
7652
7699
  var WORKFLOW_TERMINAL_STATUSES2 = ["completed", "failed", "stopped"];
7653
7700
  var WORKFLOW_LIFECYCLE_TYPES2 = ["plan", "iteration"];
7654
- function isPlainObject23(value) {
7701
+ function isPlainObject32(value) {
7655
7702
  return typeof value === "object" && value !== null && !Array.isArray(value);
7656
7703
  }
7657
- function violation32(severity, code, message, fix) {
7704
+ function violation42(severity, code, message, fix) {
7658
7705
  return { ok: false, severity, code, message, fix };
7659
7706
  }
7660
7707
  function validateNonEmptyString22(violations, value, field, missingCode, invalidCode) {
7661
7708
  if (value === undefined) {
7662
- violations.push(violation32("high", missingCode, `missing required field: ${field}`));
7709
+ violations.push(violation42("high", missingCode, `missing required field: ${field}`));
7663
7710
  } else if (typeof value !== "string" || value.trim() === "") {
7664
- violations.push(violation32("medium", invalidCode, `${field} must be a non-empty string`));
7711
+ violations.push(violation42("medium", invalidCode, `${field} must be a non-empty string`));
7665
7712
  }
7666
7713
  }
7667
7714
  function validateWorkflowSnapshot2(doc) {
7668
7715
  const violations = [];
7669
- if (!isPlainObject23(doc)) {
7716
+ if (!isPlainObject32(doc)) {
7670
7717
  return {
7671
7718
  ok: false,
7672
- violations: [violation32("high", "workflow.snapshot.invalid", "workflow snapshot must be an object")]
7719
+ violations: [violation42("high", "workflow.snapshot.invalid", "workflow snapshot must be an object")]
7673
7720
  };
7674
7721
  }
7675
7722
  if (doc.schema_version === undefined) {
7676
- violations.push(violation32("high", "workflow.snapshot.missing-schema-version", "missing required field: schema_version"));
7723
+ violations.push(violation42("high", "workflow.snapshot.missing-schema-version", "missing required field: schema_version"));
7677
7724
  } else if (doc.schema_version !== 1) {
7678
- violations.push(violation32("high", "workflow.snapshot.invalid-schema-version", `schema_version must be 1 \u2014 got ${JSON.stringify(doc.schema_version)} (version is reserved for the root file discriminator)`));
7725
+ violations.push(violation42("high", "workflow.snapshot.invalid-schema-version", `schema_version must be 1 \u2014 got ${JSON.stringify(doc.schema_version)} (version is reserved for the root file discriminator)`));
7679
7726
  }
7680
7727
  if (doc.version !== undefined) {
7681
- violations.push(violation32("medium", "workflow.snapshot.reserved-version", `top-level version is reserved for the root status.json discriminator \u2014 snapshots use schema_version; remove the version key (got ${JSON.stringify(doc.version)})`, "remove the version key from the snapshot"));
7728
+ violations.push(violation42("medium", "workflow.snapshot.reserved-version", `top-level version is reserved for the root status.json discriminator \u2014 snapshots use schema_version; remove the version key (got ${JSON.stringify(doc.version)})`, "remove the version key from the snapshot"));
7682
7729
  }
7683
7730
  validateNonEmptyString22(violations, doc.id, "id", "workflow.snapshot.missing-id", "workflow.snapshot.invalid-id");
7684
7731
  if (doc.type === undefined) {
7685
- violations.push(violation32("high", "workflow.snapshot.missing-type", "missing required field: type"));
7732
+ violations.push(violation42("high", "workflow.snapshot.missing-type", "missing required field: type"));
7686
7733
  } else if (typeof doc.type !== "string" || !WORKFLOW_LIFECYCLE_TYPES2.includes(doc.type)) {
7687
- violations.push(violation32("medium", "workflow.snapshot.invalid-type", `type must be one of ${WORKFLOW_LIFECYCLE_TYPES2.join(" | ")} \u2014 got ${JSON.stringify(doc.type)}`));
7734
+ violations.push(violation42("medium", "workflow.snapshot.invalid-type", `type must be one of ${WORKFLOW_LIFECYCLE_TYPES2.join(" | ")} \u2014 got ${JSON.stringify(doc.type)}`));
7688
7735
  }
7689
7736
  if (doc.status === undefined) {
7690
- violations.push(violation32("high", "workflow.snapshot.missing-status", "missing required field: status"));
7737
+ violations.push(violation42("high", "workflow.snapshot.missing-status", "missing required field: status"));
7691
7738
  } else if (typeof doc.status !== "string" || !WORKFLOW_LIFECYCLE_STATUSES2.includes(doc.status)) {
7692
- violations.push(violation32("medium", "workflow.snapshot.invalid-status", `status must be one of ${WORKFLOW_LIFECYCLE_STATUSES2.join(" | ")} \u2014 got ${JSON.stringify(doc.status)}`));
7739
+ violations.push(violation42("medium", "workflow.snapshot.invalid-status", `status must be one of ${WORKFLOW_LIFECYCLE_STATUSES2.join(" | ")} \u2014 got ${JSON.stringify(doc.status)}`));
7693
7740
  }
7694
7741
  validateNonEmptyString22(violations, doc.started_at, "started_at", "workflow.snapshot.missing-started-at", "workflow.snapshot.invalid-started-at");
7695
7742
  validateNonEmptyString22(violations, doc.updated_at, "updated_at", "workflow.snapshot.missing-updated-at", "workflow.snapshot.invalid-updated-at");
@@ -7697,35 +7744,35 @@ function validateWorkflowSnapshot2(doc) {
7697
7744
  validateNonEmptyString22(violations, doc.ended_at, "ended_at", "workflow.snapshot.missing-ended-at", "workflow.snapshot.invalid-ended-at");
7698
7745
  }
7699
7746
  if (doc.phase !== undefined && typeof doc.phase !== "string") {
7700
- violations.push(violation32("medium", "workflow.snapshot.invalid-phase", "phase must be a string (free-form phase machine label)"));
7747
+ violations.push(violation42("medium", "workflow.snapshot.invalid-phase", "phase must be a string (free-form phase machine label)"));
7701
7748
  }
7702
7749
  if (doc.plans === undefined) {
7703
- violations.push(violation32("high", "workflow.snapshot.missing-plans", "missing required field: plans"));
7750
+ violations.push(violation42("high", "workflow.snapshot.missing-plans", "missing required field: plans"));
7704
7751
  } else if (!Array.isArray(doc.plans)) {
7705
- violations.push(violation32("high", "workflow.snapshot.invalid-plans", "plans must be an array of legacy plan rows"));
7752
+ violations.push(violation42("high", "workflow.snapshot.invalid-plans", "plans must be an array of legacy plan rows"));
7706
7753
  } else {
7707
7754
  for (const row of doc.plans) {
7708
7755
  violations.push(...validatePlanRow2(row).violations);
7709
- if (isPlainObject23(row) && row.execution_lease !== undefined) {
7756
+ if (isPlainObject32(row) && row.execution_lease !== undefined) {
7710
7757
  violations.push(...validateExecutionLease2(row.execution_lease).violations);
7711
7758
  }
7712
7759
  }
7713
7760
  }
7714
7761
  if (doc.execution_policy !== undefined) {
7715
- if (!isPlainObject23(doc.execution_policy)) {
7716
- violations.push(violation32("medium", "workflow.snapshot.invalid-execution-policy", "execution_policy must be an object"));
7762
+ if (!isPlainObject32(doc.execution_policy)) {
7763
+ violations.push(violation42("medium", "workflow.snapshot.invalid-execution-policy", "execution_policy must be an object"));
7717
7764
  }
7718
7765
  }
7719
7766
  if (doc.integration_merge_lease !== undefined) {
7720
7767
  violations.push(...validateIntegrationMergeLease2(doc.integration_merge_lease).violations);
7721
7768
  }
7722
7769
  if (doc.branch !== undefined) {
7723
- if (!isPlainObject23(doc.branch)) {
7724
- violations.push(violation32("medium", "workflow.snapshot.invalid-branch", "branch must be an object"));
7770
+ if (!isPlainObject32(doc.branch)) {
7771
+ violations.push(violation42("medium", "workflow.snapshot.invalid-branch", "branch must be an object"));
7725
7772
  } else {
7726
7773
  for (const key of ["base", "integration", "target"]) {
7727
7774
  if (doc.branch[key] !== undefined && (typeof doc.branch[key] !== "string" || doc.branch[key].trim() === "")) {
7728
- violations.push(violation32("medium", "workflow.snapshot.invalid-branch", `branch.${key} must be a non-empty string`));
7775
+ violations.push(violation42("medium", "workflow.snapshot.invalid-branch", `branch.${key} must be a non-empty string`));
7729
7776
  }
7730
7777
  }
7731
7778
  }
@@ -7733,8 +7780,8 @@ function validateWorkflowSnapshot2(doc) {
7733
7780
  if (doc.control_worktree_path !== undefined) {
7734
7781
  validateNonEmptyString22(violations, doc.control_worktree_path, "control_worktree_path", "workflow.snapshot.missing-control-worktree-path", "workflow.snapshot.invalid-control-worktree-path");
7735
7782
  }
7736
- if (doc.legacy_metadata !== undefined && !isPlainObject23(doc.legacy_metadata)) {
7737
- violations.push(violation32("medium", "workflow.snapshot.invalid-legacy-metadata", "legacy_metadata must be an object"));
7783
+ if (doc.legacy_metadata !== undefined && !isPlainObject32(doc.legacy_metadata)) {
7784
+ violations.push(violation42("medium", "workflow.snapshot.invalid-legacy-metadata", "legacy_metadata must be an object"));
7738
7785
  }
7739
7786
  if (doc.compass_ref !== undefined) {
7740
7787
  validateNonEmptyString22(violations, doc.compass_ref, "compass_ref", "workflow.snapshot.missing-compass-ref", "workflow.snapshot.invalid-compass-ref");
@@ -7742,17 +7789,17 @@ function validateWorkflowSnapshot2(doc) {
7742
7789
  const terminal = typeof doc.status === "string" && WORKFLOW_TERMINAL_STATUSES2.includes(doc.status);
7743
7790
  if (terminal) {
7744
7791
  if (doc.ended_at === undefined) {
7745
- violations.push(violation32("high", "workflow.snapshot.missing-ended-at", `terminal status ${JSON.stringify(doc.status)} requires ended_at \u2014 a terminal snapshot must record when the lifecycle ended`));
7792
+ violations.push(violation42("high", "workflow.snapshot.missing-ended-at", `terminal status ${JSON.stringify(doc.status)} requires ended_at \u2014 a terminal snapshot must record when the lifecycle ended`));
7746
7793
  }
7747
7794
  if (Array.isArray(doc.plans)) {
7748
7795
  for (const row of doc.plans) {
7749
- if (isPlainObject23(row) && row.execution_lease !== undefined) {
7750
- violations.push(violation32("high", "workflow.snapshot.terminal-dangling-execution-lease", `terminal snapshot must not carry a row execution_lease (dangling lease) \u2014 release every lease before the lifecycle ends`));
7796
+ if (isPlainObject32(row) && row.execution_lease !== undefined) {
7797
+ violations.push(violation42("high", "workflow.snapshot.terminal-dangling-execution-lease", `terminal snapshot must not carry a row execution_lease (dangling lease) \u2014 release every lease before the lifecycle ends`));
7751
7798
  }
7752
7799
  }
7753
7800
  }
7754
7801
  if (doc.integration_merge_lease !== undefined) {
7755
- violations.push(violation32("high", "workflow.snapshot.terminal-dangling-merge-lease", "terminal snapshot must not carry integration_merge_lease (dangling lease) \u2014 release the merge lease before the lifecycle ends"));
7802
+ violations.push(violation42("high", "workflow.snapshot.terminal-dangling-merge-lease", "terminal snapshot must not carry integration_merge_lease (dangling lease) \u2014 release the merge lease before the lifecycle ends"));
7756
7803
  }
7757
7804
  }
7758
7805
  return { ok: violations.length === 0, violations };
@@ -7763,20 +7810,20 @@ async function writeWorkflowSnapshot(snapshot, dir) {
7763
7810
  const detail = gate2.violations.map((v) => v.message).join("; ");
7764
7811
  throw new Error(`refusing to write invalid workflow snapshot: ${detail}`);
7765
7812
  }
7766
- const snapshotPath = join52(dir, WORKFLOW_SNAPSHOT_FILE2);
7767
- mkdirSync42(dir, { recursive: true });
7813
+ const snapshotPath = join5(dir, WORKFLOW_SNAPSHOT_FILE2);
7814
+ mkdirSync32(dir, { recursive: true });
7768
7815
  await withStatusWriteLock2(snapshotPath, () => {
7769
7816
  writeJson2(snapshotPath, snapshot);
7770
7817
  });
7771
7818
  }
7772
- var DATE_RE3 = /^\d{4}-\d{2}-\d{2}$/;
7819
+ var DATE_RE22 = /^\d{4}-\d{2}-\d{2}$/;
7773
7820
  var PLAN_STATUSES2 = ["Todo", "InProgress", "InReview", "Blocked", "Done"];
7774
7821
  var RESIDUAL_DECISIONS = ["defer", "accept", "risk-accepted"];
7775
7822
  var RESIDUAL_LIFECYCLES = ["open", "resolved", "waived", "superseded", "duplicate"];
7776
- function isPlainObject32(value) {
7823
+ function isPlainObject42(value) {
7777
7824
  return typeof value === "object" && value !== null && !Array.isArray(value);
7778
7825
  }
7779
- function violation42(severity, code, message, fix) {
7826
+ function violation52(severity, code, message, fix) {
7780
7827
  return { ok: false, severity, code, message, fix };
7781
7828
  }
7782
7829
  function isOpenResidual2(entry) {
@@ -7786,19 +7833,19 @@ function isOpenResidual2(entry) {
7786
7833
  }
7787
7834
  function validateNonEmptyString32(violations, value, field, missingCode, invalidCode) {
7788
7835
  if (value === undefined) {
7789
- violations.push(violation42("high", missingCode, `missing required field: ${field}`));
7836
+ violations.push(violation52("high", missingCode, `missing required field: ${field}`));
7790
7837
  } else if (typeof value !== "string" || value.trim() === "") {
7791
- violations.push(violation42("medium", invalidCode, `${field} must be a non-empty string`));
7838
+ violations.push(violation52("medium", invalidCode, `${field} must be a non-empty string`));
7792
7839
  }
7793
7840
  }
7794
7841
  function validatePlanRow2(row) {
7795
7842
  const violations = [];
7796
- if (!isPlainObject32(row)) {
7797
- return { ok: false, violations: [violation42("high", "status.plan-row.invalid", "plan row must be an object")] };
7843
+ if (!isPlainObject42(row)) {
7844
+ return { ok: false, violations: [violation52("high", "status.plan-row.invalid", "plan row must be an object")] };
7798
7845
  }
7799
7846
  const { id, plan_id: planId, title, file, status, metadata, execution_lease } = row;
7800
7847
  if (id === undefined && planId === undefined) {
7801
- violations.push(violation42("high", "status.plan-row.missing-id", "missing required field: id (or legacy plan_id)"));
7848
+ violations.push(violation52("high", "status.plan-row.missing-id", "missing required field: id (or legacy plan_id)"));
7802
7849
  } else {
7803
7850
  if (id !== undefined) {
7804
7851
  validateNonEmptyString32(violations, id, "id", "status.plan-row.missing-id", "status.plan-row.invalid-id");
@@ -7807,31 +7854,31 @@ function validatePlanRow2(row) {
7807
7854
  validateNonEmptyString32(violations, planId, "plan_id", "status.plan-row.missing-plan-id", "status.plan-row.invalid-plan-id");
7808
7855
  }
7809
7856
  if (id !== undefined && planId !== undefined && id !== planId) {
7810
- violations.push(violation42("medium", "status.plan-row.dual-id", "row has both id and plan_id with different values \u2014 write one canonical key (prefer id)"));
7857
+ violations.push(violation52("medium", "status.plan-row.dual-id", "row has both id and plan_id with different values \u2014 write one canonical key (prefer id)"));
7811
7858
  }
7812
7859
  }
7813
7860
  validateNonEmptyString32(violations, title, "title", "status.plan-row.missing-title", "status.plan-row.invalid-title");
7814
7861
  validateNonEmptyString32(violations, file, "file", "status.plan-row.missing-file", "status.plan-row.invalid-file");
7815
7862
  if (status === undefined) {
7816
- violations.push(violation42("high", "status.plan-row.missing-status", "missing required field: status"));
7863
+ violations.push(violation52("high", "status.plan-row.missing-status", "missing required field: status"));
7817
7864
  } else if (typeof status !== "string" || !PLAN_STATUSES2.includes(status)) {
7818
- violations.push(violation42("medium", "status.plan-row.invalid-status", `status must be one of ${PLAN_STATUSES2.join(" | ")} \u2014 got ${JSON.stringify(status)}`));
7865
+ violations.push(violation52("medium", "status.plan-row.invalid-status", `status must be one of ${PLAN_STATUSES2.join(" | ")} \u2014 got ${JSON.stringify(status)}`));
7819
7866
  }
7820
- if (metadata !== undefined && !isPlainObject32(metadata)) {
7821
- violations.push(violation42("medium", "status.plan-row.invalid-metadata", "metadata must be an object"));
7867
+ if (metadata !== undefined && !isPlainObject42(metadata)) {
7868
+ violations.push(violation52("medium", "status.plan-row.invalid-metadata", "metadata must be an object"));
7822
7869
  }
7823
- if (execution_lease !== undefined && !isPlainObject32(execution_lease)) {
7824
- violations.push(violation42("medium", "status.plan-row.invalid-execution-lease", "execution_lease must be an object"));
7870
+ if (execution_lease !== undefined && !isPlainObject42(execution_lease)) {
7871
+ violations.push(violation52("medium", "status.plan-row.invalid-execution-lease", "execution_lease must be an object"));
7825
7872
  }
7826
7873
  if (status === "Done" && execution_lease !== undefined) {
7827
- violations.push(violation42("medium", "status.plan-row.done-with-lease", 'plan status Done must not carry an execution_lease \u2014 the Done authority deletes the lease in the same complete-file update as status: "Done" (status-and-residuals.md \u00a7 Hold, release, and override)', 'delete plans[].execution_lease in the same update that sets status: "Done"'));
7874
+ violations.push(violation52("medium", "status.plan-row.done-with-lease", 'plan status Done must not carry an execution_lease \u2014 the Done authority deletes the lease in the same complete-file update as status: "Done" (status-and-residuals.md \u00a7 Hold, release, and override)', 'delete plans[].execution_lease in the same update that sets status: "Done"'));
7828
7875
  }
7829
7876
  return { ok: violations.length === 0, violations };
7830
7877
  }
7831
7878
  function validateResidual(entry) {
7832
7879
  const violations = [];
7833
- if (!isPlainObject32(entry)) {
7834
- return { ok: false, violations: [violation42("high", "status.residual.invalid", "residual entry must be an object")] };
7880
+ if (!isPlainObject42(entry)) {
7881
+ return { ok: false, violations: [violation52("high", "status.residual.invalid", "residual entry must be an object")] };
7835
7882
  }
7836
7883
  const { id, title, severity, source, scope, decision, owner, target, tracking, detail_doc, lifecycle, closed_at } = entry;
7837
7884
  validateNonEmptyString32(violations, id, "id", "status.residual.missing-id", "status.residual.invalid-id");
@@ -7840,42 +7887,42 @@ function validateResidual(entry) {
7840
7887
  validateNonEmptyString32(violations, scope, "scope", "status.residual.missing-scope", "status.residual.invalid-scope");
7841
7888
  validateNonEmptyString32(violations, owner, "owner", "status.residual.missing-owner", "status.residual.invalid-owner");
7842
7889
  if (severity === undefined) {
7843
- violations.push(violation42("high", "status.residual.missing-severity", "missing required field: severity"));
7890
+ violations.push(violation52("high", "status.residual.missing-severity", "missing required field: severity"));
7844
7891
  } else if (typeof severity !== "string" || !SEVERITY_ORDER2.includes(severity) && severity !== "warning") {
7845
- violations.push(violation42("medium", "status.residual.invalid-severity", `severity must be one of ${SEVERITY_ORDER2.join(" | ")} \u2014 got ${JSON.stringify(severity)}`));
7892
+ violations.push(violation52("medium", "status.residual.invalid-severity", `severity must be one of ${SEVERITY_ORDER2.join(" | ")} \u2014 got ${JSON.stringify(severity)}`));
7846
7893
  } else if (severity === "warning") {
7847
- violations.push(violation42("low", "status.residual.legacy-warning", `severity "warning" is legacy \u2014 forbidden on new entries; read paths normalize it to "low"`, `use "low" (normalizeSeverity maps 'warning' \u2192 'low')`));
7894
+ violations.push(violation52("low", "status.residual.legacy-warning", `severity "warning" is legacy \u2014 forbidden on new entries; read paths normalize it to "low"`, `use "low" (normalizeSeverity maps 'warning' \u2192 'low')`));
7848
7895
  }
7849
7896
  if (decision === undefined) {
7850
- violations.push(violation42("high", "status.residual.missing-decision", "missing required field: decision"));
7897
+ violations.push(violation52("high", "status.residual.missing-decision", "missing required field: decision"));
7851
7898
  } else if (typeof decision !== "string" || !RESIDUAL_DECISIONS.includes(decision)) {
7852
- violations.push(violation42("medium", "status.residual.invalid-decision", `decision must be one of ${RESIDUAL_DECISIONS.join(" | ")} \u2014 got ${JSON.stringify(decision)}`));
7899
+ violations.push(violation52("medium", "status.residual.invalid-decision", `decision must be one of ${RESIDUAL_DECISIONS.join(" | ")} \u2014 got ${JSON.stringify(decision)}`));
7853
7900
  }
7854
7901
  if (target === undefined) {
7855
- violations.push(violation42("high", "status.residual.missing-target", "missing required field: target"));
7902
+ violations.push(violation52("high", "status.residual.missing-target", "missing required field: target"));
7856
7903
  } else if (typeof target !== "string" && target !== null) {
7857
- violations.push(violation42("medium", "status.residual.invalid-target", "target must be a string or null"));
7904
+ violations.push(violation52("medium", "status.residual.invalid-target", "target must be a string or null"));
7858
7905
  }
7859
7906
  if (tracking === undefined) {
7860
- violations.push(violation42("high", "status.residual.missing-tracking", "missing required field: tracking"));
7907
+ violations.push(violation52("high", "status.residual.missing-tracking", "missing required field: tracking"));
7861
7908
  } else if (typeof tracking !== "string" && tracking !== null) {
7862
- violations.push(violation42("medium", "status.residual.invalid-tracking", "tracking must be a string or null"));
7909
+ violations.push(violation52("medium", "status.residual.invalid-tracking", "tracking must be a string or null"));
7863
7910
  }
7864
7911
  if (detail_doc !== undefined && typeof detail_doc !== "string" && detail_doc !== null) {
7865
- violations.push(violation42("medium", "status.residual.invalid-detail-doc", "detail_doc must be a string or null"));
7912
+ violations.push(violation52("medium", "status.residual.invalid-detail-doc", "detail_doc must be a string or null"));
7866
7913
  }
7867
- if (closed_at !== undefined && (typeof closed_at !== "string" || !DATE_RE3.test(closed_at))) {
7868
- violations.push(violation42("medium", "status.residual.invalid-closed-at", "closed_at must be YYYY-MM-DD"));
7914
+ if (closed_at !== undefined && (typeof closed_at !== "string" || !DATE_RE22.test(closed_at))) {
7915
+ violations.push(violation52("medium", "status.residual.invalid-closed-at", "closed_at must be YYYY-MM-DD"));
7869
7916
  }
7870
7917
  if (lifecycle !== undefined) {
7871
7918
  if (typeof lifecycle !== "string" || !RESIDUAL_LIFECYCLES.includes(lifecycle)) {
7872
- violations.push(violation42("medium", "status.residual.invalid-lifecycle", `lifecycle must be one of ${RESIDUAL_LIFECYCLES.join(" | ")} \u2014 got ${JSON.stringify(lifecycle)}`));
7919
+ violations.push(violation52("medium", "status.residual.invalid-lifecycle", `lifecycle must be one of ${RESIDUAL_LIFECYCLES.join(" | ")} \u2014 got ${JSON.stringify(lifecycle)}`));
7873
7920
  } else if (lifecycle !== "open") {
7874
7921
  if (closed_at === undefined) {
7875
- violations.push(violation42("high", "status.residual.closed-missing-closed-at", `lifecycle "${lifecycle}" requires closed_at (YYYY-MM-DD)`, 'set closed_at (e.g. "2026-08-08")'));
7922
+ violations.push(violation52("high", "status.residual.closed-missing-closed-at", `lifecycle "${lifecycle}" requires closed_at (YYYY-MM-DD)`, 'set closed_at (e.g. "2026-08-08")'));
7876
7923
  }
7877
7924
  if (entry.closure_note === undefined) {
7878
- violations.push(violation42("medium", "status.residual.closed-missing-closure-note", `lifecycle "${lifecycle}" requires closure_note (what changed; how verified)`, "add closure_note explaining the close"));
7925
+ violations.push(violation52("medium", "status.residual.closed-missing-closure-note", `lifecycle "${lifecycle}" requires closure_note (what changed; how verified)`, "add closure_note explaining the close"));
7879
7926
  }
7880
7927
  }
7881
7928
  }
@@ -7890,25 +7937,25 @@ function isHarnessRelativePath2(dir) {
7890
7937
  }
7891
7938
  function validateWorkflowEntry2(entry) {
7892
7939
  const violations = [];
7893
- if (!isPlainObject32(entry)) {
7940
+ if (!isPlainObject42(entry)) {
7894
7941
  return {
7895
7942
  ok: false,
7896
- violations: [violation42("high", "status.workflow.invalid", "workflow entry must be an object")]
7943
+ violations: [violation52("high", "status.workflow.invalid", "workflow entry must be an object")]
7897
7944
  };
7898
7945
  }
7899
7946
  validateNonEmptyString32(violations, entry.id, "id", "status.workflow.missing-id", "status.workflow.invalid-id");
7900
7947
  if (entry.type === undefined) {
7901
- violations.push(violation42("high", "status.workflow.missing-type", "missing required field: type"));
7948
+ violations.push(violation52("high", "status.workflow.missing-type", "missing required field: type"));
7902
7949
  } else if (typeof entry.type !== "string" || !WORKFLOW_LIFECYCLE_TYPES2.includes(entry.type)) {
7903
- violations.push(violation42("medium", "status.workflow.invalid-type", `type must be one of ${WORKFLOW_LIFECYCLE_TYPES2.join(" | ")} \u2014 got ${JSON.stringify(entry.type)}`));
7950
+ violations.push(violation52("medium", "status.workflow.invalid-type", `type must be one of ${WORKFLOW_LIFECYCLE_TYPES2.join(" | ")} \u2014 got ${JSON.stringify(entry.type)}`));
7904
7951
  }
7905
7952
  validateNonEmptyString32(violations, entry.started_at, "started_at", "status.workflow.missing-started-at", "status.workflow.invalid-started-at");
7906
7953
  if (entry.dir === undefined) {
7907
- violations.push(violation42("high", "status.workflow.missing-dir", "missing required field: dir"));
7954
+ violations.push(violation52("high", "status.workflow.missing-dir", "missing required field: dir"));
7908
7955
  } else if (typeof entry.dir !== "string" || entry.dir.trim() === "") {
7909
- violations.push(violation42("medium", "status.workflow.invalid-dir", "dir must be a non-empty string"));
7956
+ violations.push(violation52("medium", "status.workflow.invalid-dir", "dir must be a non-empty string"));
7910
7957
  } else if (!isHarnessRelativePath2(entry.dir)) {
7911
- violations.push(violation42("medium", "status.workflow.invalid-dir", `dir must be a harness-relative path (no absolute paths, no ".." segments) \u2014 got ${JSON.stringify(entry.dir)}`));
7958
+ violations.push(violation52("medium", "status.workflow.invalid-dir", `dir must be a harness-relative path (no absolute paths, no ".." segments) \u2014 got ${JSON.stringify(entry.dir)}`));
7912
7959
  }
7913
7960
  return { ok: violations.length === 0, violations };
7914
7961
  }
@@ -7918,24 +7965,24 @@ function validateStatusV22(docOrPath, opts = {}) {
7918
7965
  if (typeof docOrPath === "string") {
7919
7966
  try {
7920
7967
  doc = readJson2(docOrPath);
7921
- harnessDir = dirname52(resolve52(docOrPath));
7968
+ harnessDir = dirname42(resolve42(docOrPath));
7922
7969
  } catch (error) {
7923
7970
  return {
7924
7971
  ok: false,
7925
- violations: [violation42("high", "status.invalid-json", error.message)]
7972
+ violations: [violation52("high", "status.invalid-json", error.message)]
7926
7973
  };
7927
7974
  }
7928
7975
  } else {
7929
7976
  doc = docOrPath;
7930
7977
  }
7931
- if (!isPlainObject32(doc)) {
7932
- return { ok: false, violations: [violation42("high", "status.invalid-doc", "status document must be an object")] };
7978
+ if (!isPlainObject42(doc)) {
7979
+ return { ok: false, violations: [violation52("high", "status.invalid-doc", "status document must be an object")] };
7933
7980
  }
7934
7981
  if (doc.version !== 2) {
7935
7982
  return {
7936
7983
  ok: false,
7937
7984
  violations: [
7938
- violation42("high", "status.migration-required", `status.json schema version 2 required \u2014 got ${JSON.stringify(doc.version)} (v1 or unknown version); run \`mstar migrate\` to convert the tree`, "run `mstar migrate`")
7985
+ violation52("high", "status.migration-required", `status.json schema version 2 required \u2014 got ${JSON.stringify(doc.version)} (v1 or unknown version); run \`mstar migrate\` to convert the tree`, "run `mstar migrate`")
7939
7986
  ]
7940
7987
  };
7941
7988
  }
@@ -7943,7 +7990,7 @@ function validateStatusV22(docOrPath, opts = {}) {
7943
7990
  return {
7944
7991
  ok: false,
7945
7992
  violations: [
7946
- violation42("high", "status.migration-required", "v1-shaped status.json (root plans[]) is not a v2 document \u2014 run `mstar migrate` to convert the tree", "run `mstar migrate`")
7993
+ violation52("high", "status.migration-required", "v1-shaped status.json (root plans[]) is not a v2 document \u2014 run `mstar migrate` to convert the tree", "run `mstar migrate`")
7947
7994
  ]
7948
7995
  };
7949
7996
  }
@@ -7951,27 +7998,27 @@ function validateStatusV22(docOrPath, opts = {}) {
7951
7998
  return {
7952
7999
  ok: false,
7953
8000
  violations: [
7954
- violation42("high", "status.migration-required", "v1-shaped status.json (root residual_findings) is not a v2 document \u2014 run `mstar migrate` to convert the tree", "run `mstar migrate`")
8001
+ violation52("high", "status.migration-required", "v1-shaped status.json (root residual_findings) is not a v2 document \u2014 run `mstar migrate` to convert the tree", "run `mstar migrate`")
7955
8002
  ]
7956
8003
  };
7957
8004
  }
7958
8005
  const violations = [];
7959
8006
  if (doc.updated_at === undefined) {
7960
- violations.push(violation42("high", "status.missing-updated-at", "missing required field: updated_at"));
7961
- } else if (typeof doc.updated_at !== "string" || !DATE_RE3.test(doc.updated_at)) {
7962
- violations.push(violation42("medium", "status.invalid-updated-at", "updated_at must be YYYY-MM-DD"));
8007
+ violations.push(violation52("high", "status.missing-updated-at", "missing required field: updated_at"));
8008
+ } else if (typeof doc.updated_at !== "string" || !DATE_RE22.test(doc.updated_at)) {
8009
+ violations.push(violation52("medium", "status.invalid-updated-at", "updated_at must be YYYY-MM-DD"));
7963
8010
  }
7964
8011
  if (doc.workflows === undefined) {
7965
- violations.push(violation42("high", "status.missing-workflows", "missing required field: workflows"));
8012
+ violations.push(violation52("high", "status.missing-workflows", "missing required field: workflows"));
7966
8013
  } else if (!Array.isArray(doc.workflows)) {
7967
- violations.push(violation42("high", "status.invalid-workflows", "workflows must be an array"));
8014
+ violations.push(violation52("high", "status.invalid-workflows", "workflows must be an array"));
7968
8015
  } else {
7969
8016
  const seen = new Set;
7970
8017
  for (const entry of doc.workflows) {
7971
8018
  violations.push(...validateWorkflowEntry2(entry).violations);
7972
- if (isPlainObject32(entry) && typeof entry.id === "string") {
8019
+ if (isPlainObject42(entry) && typeof entry.id === "string") {
7973
8020
  if (seen.has(entry.id)) {
7974
- violations.push(violation42("medium", "status.workflow.duplicate-id", `duplicate workflow id in workflows[]: ${JSON.stringify(entry.id)}`));
8021
+ violations.push(violation52("medium", "status.workflow.duplicate-id", `duplicate workflow id in workflows[]: ${JSON.stringify(entry.id)}`));
7975
8022
  }
7976
8023
  seen.add(entry.id);
7977
8024
  }
@@ -7980,122 +8027,60 @@ function validateStatusV22(docOrPath, opts = {}) {
7980
8027
  if (harnessDir !== undefined && Array.isArray(doc.workflows)) {
7981
8028
  let realHarnessDir = null;
7982
8029
  try {
7983
- realHarnessDir = realpathSync22(harnessDir);
8030
+ realHarnessDir = realpathSync4(harnessDir);
7984
8031
  } catch {}
7985
8032
  for (const entry of doc.workflows) {
7986
- if (!isPlainObject32(entry) || typeof entry.dir !== "string")
8033
+ if (!isPlainObject42(entry) || typeof entry.dir !== "string")
7987
8034
  continue;
7988
8035
  const relSnapshot = join62(entry.dir, WORKFLOW_SNAPSHOT_FILE2);
7989
8036
  const snapshotPath = join62(harnessDir, relSnapshot);
7990
8037
  const label = typeof entry.id === "string" ? entry.id : relSnapshot;
7991
8038
  let physical;
7992
8039
  try {
7993
- physical = realpathSync22(snapshotPath);
8040
+ physical = realpathSync4(snapshotPath);
7994
8041
  } catch {
7995
- violations.push(violation42("high", "status.workflow.snapshot-missing", `workflows[] lists ${JSON.stringify(label)} but its snapshot does not exist at ${JSON.stringify(relSnapshot)} \u2014 the root holds active lifecycles only; unregister the id when its snapshot is removed`));
8042
+ violations.push(violation52("high", "status.workflow.snapshot-missing", `workflows[] lists ${JSON.stringify(label)} but its snapshot does not exist at ${JSON.stringify(relSnapshot)} \u2014 the root holds active lifecycles only; unregister the id when its snapshot is removed`));
7996
8043
  continue;
7997
8044
  }
7998
8045
  if (realHarnessDir !== null && physical !== realHarnessDir && !physical.startsWith(`${realHarnessDir}${sep2}`)) {
7999
- violations.push(violation42("high", "status.workflow.snapshot-outside-harness", `workflows[] lists ${JSON.stringify(label)} but its snapshot resolves outside the harness dir (${JSON.stringify(physical)}) \u2014 symlinked snapshot paths are rejected; the snapshot must physically live under ${JSON.stringify(harnessDir)}`));
8046
+ violations.push(violation52("high", "status.workflow.snapshot-outside-harness", `workflows[] lists ${JSON.stringify(label)} but its snapshot resolves outside the harness dir (${JSON.stringify(physical)}) \u2014 symlinked snapshot paths are rejected; the snapshot must physically live under ${JSON.stringify(harnessDir)}`));
8000
8047
  continue;
8001
8048
  }
8002
8049
  let snapshot;
8003
8050
  try {
8004
8051
  snapshot = readJson2(snapshotPath);
8005
8052
  } catch (error) {
8006
- violations.push(violation42("high", "status.workflow.snapshot-invalid", `snapshot at ${JSON.stringify(relSnapshot)} is not valid JSON: ${error.message}`));
8053
+ violations.push(violation52("high", "status.workflow.snapshot-invalid", `snapshot at ${JSON.stringify(relSnapshot)} is not valid JSON: ${error.message}`));
8007
8054
  continue;
8008
8055
  }
8009
8056
  if (typeof snapshot.status === "string" && WORKFLOW_TERMINAL_STATUSES2.includes(snapshot.status)) {
8010
- violations.push(violation42("high", "status.workflow.terminal-listed", `workflows[] lists ${JSON.stringify(label)} whose snapshot status is terminal (${snapshot.status}) \u2014 removal-at-terminal: terminal writers unregister AFTER the snapshot write`));
8057
+ violations.push(violation52("high", "status.workflow.terminal-listed", `workflows[] lists ${JSON.stringify(label)} whose snapshot status is terminal (${snapshot.status}) \u2014 removal-at-terminal: terminal writers unregister AFTER the snapshot write`));
8011
8058
  }
8012
8059
  if (typeof entry.type === "string" && typeof snapshot.type === "string" && entry.type !== snapshot.type) {
8013
- violations.push(violation42("medium", "status.workflow.mismatched-type", `workflows[] entry ${JSON.stringify(label)} type ${JSON.stringify(entry.type)} does not match its snapshot type ${JSON.stringify(snapshot.type)} \u2014 the root entry mirrors the snapshot; align them`));
8060
+ violations.push(violation52("medium", "status.workflow.mismatched-type", `workflows[] entry ${JSON.stringify(label)} type ${JSON.stringify(entry.type)} does not match its snapshot type ${JSON.stringify(snapshot.type)} \u2014 the root entry mirrors the snapshot; align them`));
8014
8061
  }
8015
8062
  if (typeof entry.started_at === "string" && typeof snapshot.started_at === "string" && entry.started_at !== snapshot.started_at) {
8016
- violations.push(violation42("medium", "status.workflow.mismatched-started-at", `workflows[] entry ${JSON.stringify(label)} started_at ${JSON.stringify(entry.started_at)} does not match its snapshot started_at ${JSON.stringify(snapshot.started_at)} \u2014 workflow ${JSON.stringify(label)} collided with another writer (e.g. a concurrent/re-run \`audit promote\` with the same workflow id rewrote the snapshot); the root entry mirrors the snapshot \u2014 align them or remove the colliding workflow`));
8063
+ violations.push(violation52("medium", "status.workflow.mismatched-started-at", `workflows[] entry ${JSON.stringify(label)} started_at ${JSON.stringify(entry.started_at)} does not match its snapshot started_at ${JSON.stringify(snapshot.started_at)} \u2014 workflow ${JSON.stringify(label)} collided with another writer (e.g. a concurrent/re-run \`audit promote\` with the same workflow id rewrote the snapshot); the root entry mirrors the snapshot \u2014 align them or remove the colliding workflow`));
8017
8064
  }
8018
8065
  }
8019
8066
  }
8020
8067
  return { ok: violations.length === 0, violations };
8021
8068
  }
8022
- var GIT_CAPTURE_MAX_BYTES2 = 64 * 1024 * 1024;
8023
- function parseCompassFrontmatterText2(content, filePath) {
8024
- const lines = content.split(/\r?\n/);
8025
- if (lines[0]?.trim() !== "---") {
8026
- throw new Error(`no YAML frontmatter fence in ${filePath} (expected first line "---")`);
8027
- }
8028
- const end = lines.indexOf("---", 1);
8029
- if (end === -1) {
8030
- throw new Error(`unterminated YAML frontmatter in ${filePath} (no closing "---")`);
8031
- }
8032
- const doc = {};
8033
- let listKey = null;
8034
- for (let i = 1;i < end; i += 1) {
8035
- const line = lines[i] ?? "";
8036
- if (!line.trim() || line.trim().startsWith("#"))
8037
- continue;
8038
- if (listKey !== null && /^\s*-\s+/.test(line)) {
8039
- const item = line.replace(/^\s*-\s+/, "").trim().replace(/^["']|["']$/g, "");
8040
- if (!Array.isArray(doc[listKey]))
8041
- doc[listKey] = [];
8042
- doc[listKey].push(item);
8043
- continue;
8044
- }
8045
- listKey = null;
8046
- const kv = line.match(/^([A-Za-z_][A-Za-z0-9_-]*):\s*(.*)$/);
8047
- if (!kv) {
8048
- throw new Error(`unsupported frontmatter line in ${filePath}: ${JSON.stringify(line)}`);
8049
- }
8050
- const value = kv[2].trim();
8051
- doc[kv[1]] = value === "" ? null : /^\[.*\]$/.test(value) ? parseFlowArray2(value, filePath) : value.replace(/^["']|["']$/g, "");
8052
- listKey = value === "" ? kv[1] : null;
8053
- }
8054
- return doc;
8055
- }
8056
- function parseFlowArray2(raw, filePath) {
8057
- const inner = raw.slice(1, -1);
8058
- if (/[[\]]/.test(inner)) {
8059
- throw new Error(`nested flow-style array in ${filePath}: ${JSON.stringify(raw)} \u2014 only flat scalar items are supported (e.g. [a, b])`);
8060
- }
8061
- let quote = null;
8062
- for (const ch of inner) {
8063
- if (ch === '"' || ch === "'") {
8064
- if (quote === null)
8065
- quote = ch;
8066
- else if (quote === ch)
8067
- quote = null;
8068
- } else if (ch === "," && quote !== null) {
8069
- throw new Error(`ambiguous flow-style array in ${filePath}: ${JSON.stringify(raw)} \u2014 quoted item containing comma cannot be split unambiguously (flat scalar items only)`);
8070
- }
8071
- }
8072
- if (quote !== null) {
8073
- throw new Error(`unterminated ${quote} quote in flow-style array in ${filePath}: ${JSON.stringify(raw)}`);
8074
- }
8075
- const items = [];
8076
- for (const part of inner.split(",")) {
8077
- const item = part.trim().replace(/^["']|["']$/g, "");
8078
- if (item === "")
8079
- continue;
8080
- items.push(item);
8081
- }
8082
- return items;
8083
- }
8084
- var PROJECT_ROADMAP_FILE = "roadmap.md";
8069
+ var PROJECT_ROADMAP_FILE2 = "roadmap.md";
8085
8070
  var PROJECT_REGISTER_FILE2 = "residuals.json";
8086
8071
  var _DEFAULT_PROJECT2 = "_default";
8087
- var DATE_RE32 = /^\d{4}-\d{2}-\d{2}$/;
8072
+ var DATE_RE3 = /^\d{4}-\d{2}-\d{2}$/;
8088
8073
  function isPlainObject52(value) {
8089
8074
  return typeof value === "object" && value !== null && !Array.isArray(value);
8090
8075
  }
8091
- function violation72(severity, code, message, fix) {
8076
+ function violation62(severity, code, message, fix) {
8092
8077
  return { ok: false, severity, code, message, fix };
8093
8078
  }
8094
8079
  function validateNonEmptyString42(violations, value, field, missingCode, invalidCode) {
8095
8080
  if (value === undefined) {
8096
- violations.push(violation72("high", missingCode, `missing required field: ${field}`));
8081
+ violations.push(violation62("high", missingCode, `missing required field: ${field}`));
8097
8082
  } else if (typeof value !== "string" || value.trim() === "") {
8098
- violations.push(violation72("medium", invalidCode, `${field} must be a non-empty string`));
8083
+ violations.push(violation62("medium", invalidCode, `${field} must be a non-empty string`));
8099
8084
  }
8100
8085
  }
8101
8086
  function validateProjectRegister(doc) {
@@ -8103,20 +8088,20 @@ function validateProjectRegister(doc) {
8103
8088
  if (!isPlainObject52(doc)) {
8104
8089
  return {
8105
8090
  ok: false,
8106
- violations: [violation72("high", "project.register.invalid", "project register must be an object")]
8091
+ violations: [violation62("high", "project.register.invalid", "project register must be an object")]
8107
8092
  };
8108
8093
  }
8109
8094
  if (doc.entries === undefined) {
8110
- violations.push(violation72("high", "project.register.missing-entries", "missing required field: entries"));
8095
+ violations.push(violation62("high", "project.register.missing-entries", "missing required field: entries"));
8111
8096
  } else if (!isPlainObject52(doc.entries)) {
8112
- violations.push(violation72("high", "project.register.invalid-entries", "entries must be an object keyed by plan id"));
8097
+ violations.push(violation62("high", "project.register.invalid-entries", "entries must be an object keyed by plan id"));
8113
8098
  } else {
8114
8099
  for (const [key, entries] of Object.entries(doc.entries)) {
8115
8100
  if (key.trim() === "") {
8116
- violations.push(violation72("medium", "project.register.invalid-key", "entries keys must be non-empty plan ids"));
8101
+ violations.push(violation62("medium", "project.register.invalid-key", "entries keys must be non-empty plan ids"));
8117
8102
  }
8118
8103
  if (!Array.isArray(entries)) {
8119
- violations.push(violation72("high", "project.register.invalid-entry-list", `entries[${JSON.stringify(key)}] must be an array of residual entries (one entry per residual; v1 multi-finding semantics)`));
8104
+ violations.push(violation62("high", "project.register.invalid-entry-list", `entries[${JSON.stringify(key)}] must be an array of residual entries (one entry per residual; v1 multi-finding semantics)`));
8120
8105
  continue;
8121
8106
  }
8122
8107
  for (const entry of entries) {
@@ -8125,21 +8110,126 @@ function validateProjectRegister(doc) {
8125
8110
  continue;
8126
8111
  validateNonEmptyString42(violations, entry.source_plan, "source_plan", "project.register.missing-source-plan", "project.register.invalid-source-plan");
8127
8112
  if (entry.registered_at === undefined) {
8128
- violations.push(violation72("high", "project.register.missing-registered-at", "missing required field: registered_at"));
8129
- } else if (typeof entry.registered_at !== "string" || !DATE_RE32.test(entry.registered_at)) {
8130
- violations.push(violation72("medium", "project.register.invalid-registered-at", "registered_at must be YYYY-MM-DD"));
8113
+ violations.push(violation62("high", "project.register.missing-registered-at", "missing required field: registered_at"));
8114
+ } else if (typeof entry.registered_at !== "string" || !DATE_RE3.test(entry.registered_at)) {
8115
+ violations.push(violation62("medium", "project.register.invalid-registered-at", "registered_at must be YYYY-MM-DD"));
8131
8116
  }
8132
8117
  if (entry.lifecycle_id !== undefined && (typeof entry.lifecycle_id !== "string" || entry.lifecycle_id.trim() === "")) {
8133
- violations.push(violation72("medium", "project.register.invalid-lifecycle-id", "lifecycle_id must be a non-empty string"));
8118
+ violations.push(violation62("medium", "project.register.invalid-lifecycle-id", "lifecycle_id must be a non-empty string"));
8134
8119
  }
8135
8120
  if (typeof entry.source_plan === "string" && entry.source_plan.trim() !== "" && entry.source_plan !== key) {
8136
- violations.push(violation72("medium", "project.register.mismatched-source-plan", `source_plan ${JSON.stringify(entry.source_plan)} does not match the entries key ${JSON.stringify(key)} \u2014 entries are keyed by plan id`));
8121
+ violations.push(violation62("medium", "project.register.mismatched-source-plan", `source_plan ${JSON.stringify(entry.source_plan)} does not match the entries key ${JSON.stringify(key)} \u2014 entries are keyed by plan id`));
8137
8122
  }
8138
8123
  }
8139
8124
  }
8140
8125
  }
8141
8126
  return { ok: violations.length === 0, violations };
8142
8127
  }
8128
+ function resolveHarnessDir2(startDir = process.cwd(), opts = {}) {
8129
+ const start = resolve52(startDir);
8130
+ const explicit = opts.harnessDir ?? process.env.MSTAR_HARNESS_DIR;
8131
+ if (explicit)
8132
+ return resolve52(start, explicit);
8133
+ const boundary = resolve52(start, opts.workspaceRoot ?? defaultWorkspaceRoot2(start));
8134
+ const rc = loadMstarc2(start, boundary);
8135
+ if (rc !== null && rc.config.harnessDir)
8136
+ return resolve52(rc.dir, rc.config.harnessDir);
8137
+ let dir = start;
8138
+ for (;; ) {
8139
+ if (!isAtOrBelow22(dir, boundary))
8140
+ return null;
8141
+ for (const candidate of [join82(dir, ".mstar"), join82(dir, ".agents"), join82(dir, ".plans"), join82(dir, "plans")]) {
8142
+ if (isDirectory3(candidate))
8143
+ return candidate;
8144
+ }
8145
+ if (dir === boundary)
8146
+ return null;
8147
+ const parent = dirname52(dir);
8148
+ if (parent === dir)
8149
+ return null;
8150
+ dir = parent;
8151
+ }
8152
+ }
8153
+ function defaultWorkspaceRoot2(startDir) {
8154
+ try {
8155
+ const cdup = execFileSync4("git", ["rev-parse", "--show-cdup"], {
8156
+ cwd: startDir,
8157
+ encoding: "utf8",
8158
+ stdio: ["ignore", "pipe", "ignore"]
8159
+ }).trim();
8160
+ if (!cdup)
8161
+ return startDir;
8162
+ let boundary = startDir;
8163
+ for (const segment of cdup.split(/[\\/]/)) {
8164
+ if (segment && segment !== ".")
8165
+ boundary = dirname52(boundary);
8166
+ }
8167
+ return resolve52(boundary);
8168
+ } catch {}
8169
+ return startDir;
8170
+ }
8171
+ function isAtOrBelow22(dir, root) {
8172
+ const rel = relative22(root, dir);
8173
+ return rel === "" || !rel.startsWith("..") && !isAbsolute32(rel);
8174
+ }
8175
+ function mstarcDirOverride2(harnessDir, key) {
8176
+ const dir = resolve52(harnessDir);
8177
+ const rc = loadMstarc2(dir, dirname52(dir));
8178
+ const declared = rc?.config[key];
8179
+ return declared ? resolve52(rc.dir, declared) : null;
8180
+ }
8181
+ function assertSafePathComponent2(value, what) {
8182
+ if (value === "" || value === "." || value === ".." || !/^[A-Za-z0-9._-]+$/.test(value)) {
8183
+ throw new Error(`${what} must be a single safe path component ([A-Za-z0-9._-]+; not "", ".", "..", or containing "/" or "\\") \u2014 got ${JSON.stringify(value)}`);
8184
+ }
8185
+ }
8186
+ function resolveHarnessSubdir2(startDir, opts, key, fallback) {
8187
+ const harness = resolveHarnessDir2(startDir, opts);
8188
+ if (harness === null) {
8189
+ throw new Error(`harness dir not found from ${resolve52(startDir)} \u2014 cannot resolve the ${fallback} dir (run \`mstar harness scaffold\`, pass opts.harnessDir, or set MSTAR_HARNESS_DIR)`);
8190
+ }
8191
+ const declared = mstarcDirOverride2(harness, key);
8192
+ return declared !== null ? declared : join82(resolve52(harness), fallback);
8193
+ }
8194
+ function resolveWorkflowDir2(startDir = process.cwd(), opts = {}) {
8195
+ return resolveHarnessSubdir2(startDir, opts, "workflowDir", "workflows");
8196
+ }
8197
+ function resolveProjectDir2(startDir = process.cwd(), opts = {}) {
8198
+ return resolveHarnessSubdir2(startDir, opts, "projectDir", "projects");
8199
+ }
8200
+ var GITIGNORE_SNIPPET2 = `# Morning Star harness (.mstar/)
8201
+ # Principle: process stays local; results are shared with the team.
8202
+ # Default-ignore everything under .mstar/, then re-include the tracked results.
8203
+ .mstar/**
8204
+ !.mstar/AGENTS.md
8205
+ !.mstar/knowledge/
8206
+ !.mstar/knowledge/**
8207
+ !.mstar/specs/
8208
+ !.mstar/specs/**
8209
+ # .mstarc \u2014 repo-local harness config (may declare [config] harness_dir=<name>)
8210
+ .mstarc
8211
+ `;
8212
+ var GITIGNORE_SNIPPET_AGENTS2 = `# Morning Star harness (.agents/) \u2014 legacy
8213
+ # Default-ignore everything under .agents/, then re-include the tracked results.
8214
+ .agents/**
8215
+ !.agents/AGENTS.md
8216
+ !.agents/knowledge/
8217
+ !.agents/knowledge/**
8218
+ !.agents/specs/
8219
+ !.agents/specs/**
8220
+ `;
8221
+ var GITIGNORE_PROCESS_ENTRIES2 = GITIGNORE_SNIPPET2.split(`
8222
+ `).filter((line) => line.startsWith(".mstar/") || line.startsWith("!.mstar/")).map((line) => line.trim());
8223
+ var GITIGNORE_PROCESS_ENTRIES_AGENTS2 = GITIGNORE_SNIPPET_AGENTS2.split(`
8224
+ `).filter((line) => line.startsWith(".agents/") || line.startsWith("!.agents/")).map((line) => line.trim());
8225
+ function isDirectory3(dir) {
8226
+ try {
8227
+ return statSync32(dir).isDirectory();
8228
+ } catch {
8229
+ return false;
8230
+ }
8231
+ }
8232
+ var GIT_CAPTURE_MAX_BYTES2 = 64 * 1024 * 1024;
8143
8233
  var MIGRATE_STATUS_FILE = "status.json";
8144
8234
  var ARCHIVED_STATUS_V1_FILE = "archived/status.v1.json";
8145
8235
  var NOTES_LEDGER_FILE = "notes.jsonl";
@@ -8156,7 +8246,7 @@ var ROOT_METADATA_LIFT_KEYS = {
8156
8246
  program_roadmap: true,
8157
8247
  updated_at: true
8158
8248
  };
8159
- function isPlainObject62(value) {
8249
+ function isPlainObject6(value) {
8160
8250
  return typeof value === "object" && value !== null && !Array.isArray(value);
8161
8251
  }
8162
8252
  function dateString(value) {
@@ -8183,7 +8273,7 @@ function scanCompasses(harnessDir) {
8183
8273
  const out = [];
8184
8274
  let entries;
8185
8275
  try {
8186
- entries = readdirSync62(iterationsDir, { withFileTypes: true });
8276
+ entries = readdirSync6(iterationsDir, { withFileTypes: true });
8187
8277
  } catch {
8188
8278
  return out;
8189
8279
  }
@@ -8336,10 +8426,10 @@ function applyRootMetadataLift(snapshot, metadata, migrationNotes, activeIterati
8336
8426
  if (typeof metadata.control_worktree_path === "string" && metadata.control_worktree_path !== "") {
8337
8427
  data.control_worktree_path = metadata.control_worktree_path;
8338
8428
  }
8339
- if (isPlainObject62(metadata.integration_merge_lease)) {
8429
+ if (isPlainObject6(metadata.integration_merge_lease)) {
8340
8430
  data.integration_merge_lease = metadata.integration_merge_lease;
8341
8431
  }
8342
- const legacyMetadata = isPlainObject62(data.legacy_metadata) ? { ...data.legacy_metadata } : {};
8432
+ const legacyMetadata = isPlainObject6(data.legacy_metadata) ? { ...data.legacy_metadata } : {};
8343
8433
  for (const [key, value] of Object.entries(metadata)) {
8344
8434
  if (key === "harness_root")
8345
8435
  continue;
@@ -8395,7 +8485,7 @@ function buildRegister(residualFindings, byPlan, projectId, migratedAt) {
8395
8485
  const raw = residualFindings[planId];
8396
8486
  if (!Array.isArray(raw))
8397
8487
  continue;
8398
- const open = raw.filter((entry) => isPlainObject62(entry) && isOpenResidual2(entry)).sort((a, b) => {
8488
+ const open = raw.filter((entry) => isPlainObject6(entry) && isOpenResidual2(entry)).sort((a, b) => {
8399
8489
  const aId = typeof a.id === "string" ? a.id : "";
8400
8490
  const bId = typeof b.id === "string" ? b.id : "";
8401
8491
  return compareIds(aId, bId);
@@ -8484,12 +8574,12 @@ function migrateHarnessTree(root, opts = {}) {
8484
8574
  if (legacy.version === undefined) {
8485
8575
  throw new Error(`refusing to migrate: no v1 status.json found at ${statusPath} (nothing to migrate)`);
8486
8576
  }
8487
- const rows = Array.isArray(legacy.plans) ? legacy.plans.filter(isPlainObject62) : [];
8577
+ const rows = Array.isArray(legacy.plans) ? legacy.plans.filter(isPlainObject6) : [];
8488
8578
  if (Array.isArray(legacy.plans)) {
8489
8579
  const unLiftable = [];
8490
8580
  const idCounts = new Map;
8491
8581
  for (const row of legacy.plans) {
8492
- if (!isPlainObject62(row) || rowIdOf(row) === null) {
8582
+ if (!isPlainObject6(row) || rowIdOf(row) === null) {
8493
8583
  unLiftable.push(row);
8494
8584
  continue;
8495
8585
  }
@@ -8505,7 +8595,7 @@ function migrateHarnessTree(root, opts = {}) {
8505
8595
  throw new Error(`refusing to migrate: ${duplicates.length} duplicate plan id(s) (${duplicates.join(", ")}) \u2014 every v1 row must land in exactly one snapshot`);
8506
8596
  }
8507
8597
  }
8508
- const metadata = isPlainObject62(legacy.metadata) ? legacy.metadata : {};
8598
+ const metadata = isPlainObject6(legacy.metadata) ? legacy.metadata : {};
8509
8599
  const rootUpdatedAt = dateString(legacy.updated_at) ?? dateString(metadata.updated_at) ?? todayString2();
8510
8600
  const migratedAt = dateString(metadata.updated_at) ?? rootUpdatedAt;
8511
8601
  const migrationNotes = [];
@@ -8542,9 +8632,9 @@ function migrateHarnessTree(root, opts = {}) {
8542
8632
  migrationNotes.push("no active iteration snapshot found \u2014 root metadata execution-policy/branch keys have no lift home and stay unmapped (visible here, not silently dropped)");
8543
8633
  }
8544
8634
  const notesFiles = collectNotesFiles(snapshots);
8545
- const residualFindings = isPlainObject62(legacy.residual_findings) ? legacy.residual_findings : {};
8635
+ const residualFindings = isPlainObject6(legacy.residual_findings) ? legacy.residual_findings : {};
8546
8636
  const register = buildRegister(residualFindings, byPlan, projectId, migratedAt);
8547
- const programRoadmap = isPlainObject62(metadata.program_roadmap) ? metadata.program_roadmap : null;
8637
+ const programRoadmap = isPlainObject6(metadata.program_roadmap) ? metadata.program_roadmap : null;
8548
8638
  let roadmap = null;
8549
8639
  if (programRoadmap) {
8550
8640
  const rawTitle = typeof programRoadmap.title === "string" && programRoadmap.title !== "" ? programRoadmap.title : "Program roadmap";
@@ -8553,7 +8643,7 @@ function migrateHarnessTree(root, opts = {}) {
8553
8643
  migrationNotes.push(`roadmap title sanitized for frontmatter (line breaks replaced with spaces): ${JSON.stringify(rawTitle)}`);
8554
8644
  }
8555
8645
  roadmap = {
8556
- file: join10("projects", projectId, PROJECT_ROADMAP_FILE),
8646
+ file: join10("projects", projectId, PROJECT_ROADMAP_FILE2),
8557
8647
  source: "status.json metadata.program_roadmap",
8558
8648
  content: buildRoadmap({ ...programRoadmap, title: sanitizedTitle }, projectId, migratedAt)
8559
8649
  };
@@ -8640,7 +8730,7 @@ async function applyMigratePlan(plan) {
8640
8730
  const content = notes.lines.length > 0 ? `${notes.lines.join(`
8641
8731
  `)}
8642
8732
  ` : "";
8643
- writeFileSync42(filePath, content, "utf8");
8733
+ writeFileSync52(filePath, content, "utf8");
8644
8734
  }
8645
8735
  if (plan.register !== null) {
8646
8736
  const gate2 = validateProjectRegister(plan.register.data);
@@ -8656,7 +8746,7 @@ async function applyMigratePlan(plan) {
8656
8746
  if (plan.roadmap !== null) {
8657
8747
  const filePath = projectTargetOf(plan.roadmap.file);
8658
8748
  mkdirSync6(dirname72(filePath), { recursive: true });
8659
- writeFileSync42(filePath, plan.roadmap.content, "utf8");
8749
+ writeFileSync52(filePath, plan.roadmap.content, "utf8");
8660
8750
  }
8661
8751
  const rootGate = validateStatusV22(plan.rootV2.data, { harnessDir: plan.root });
8662
8752
  if (!rootGate.ok) {
@@ -8686,16 +8776,16 @@ function validatePlannedDocs(plan) {
8686
8776
  for (const snapshot of plan.snapshots) {
8687
8777
  const gate2 = validateWorkflowSnapshot2(snapshot.data);
8688
8778
  if (!gate2.ok) {
8689
- for (const violation14 of gate2.violations) {
8690
- warnings.push(`[${violation14.severity}] ${violation14.code}: ${violation14.message} (planned snapshot ${snapshot.file})`);
8779
+ for (const violation9 of gate2.violations) {
8780
+ warnings.push(`[${violation9.severity}] ${violation9.code}: ${violation9.message} (planned snapshot ${snapshot.file})`);
8691
8781
  }
8692
8782
  }
8693
8783
  }
8694
8784
  if (plan.register !== null) {
8695
8785
  const gate2 = validateProjectRegister(plan.register.data);
8696
8786
  if (!gate2.ok) {
8697
- for (const violation14 of gate2.violations) {
8698
- warnings.push(`[${violation14.severity}] ${violation14.code}: ${violation14.message} (planned register ${plan.register.file})`);
8787
+ for (const violation9 of gate2.violations) {
8788
+ warnings.push(`[${violation9.severity}] ${violation9.code}: ${violation9.message} (planned register ${plan.register.file})`);
8699
8789
  }
8700
8790
  }
8701
8791
  }
@@ -8857,7 +8947,7 @@ function describeType(value) {
8857
8947
  return "array";
8858
8948
  return typeof value;
8859
8949
  }
8860
- function isPlainObject8(value) {
8950
+ function isPlainObject7(value) {
8861
8951
  return typeof value === "object" && value !== null && !Array.isArray(value);
8862
8952
  }
8863
8953
  function parseScalar3(raw) {
@@ -8901,7 +8991,7 @@ function isValidMcpUrl(raw) {
8901
8991
  return true;
8902
8992
  }
8903
8993
  function validateManifest(manifest, errors2, warnings) {
8904
- if (!isPlainObject8(manifest)) {
8994
+ if (!isPlainObject7(manifest)) {
8905
8995
  errors2.push("plugin.json: manifest must be a JSON object");
8906
8996
  return;
8907
8997
  }
@@ -8937,7 +9027,7 @@ function validateManifest(manifest, errors2, warnings) {
8937
9027
  }
8938
9028
  }
8939
9029
  if (doc.author !== undefined) {
8940
- if (!isPlainObject8(doc.author)) {
9030
+ if (!isPlainObject7(doc.author)) {
8941
9031
  errors2.push('plugin.json: "author" must be an object with optional string fields name/email/url');
8942
9032
  } else {
8943
9033
  const author = doc.author;
@@ -8960,11 +9050,11 @@ function validateManifest(manifest, errors2, warnings) {
8960
9050
  }
8961
9051
  }
8962
9052
  if (doc.extensions !== undefined) {
8963
- if (!isPlainObject8(doc.extensions)) {
9053
+ if (!isPlainObject7(doc.extensions)) {
8964
9054
  warnings.push('plugin.json: "extensions" is not an object \u2014 ignored');
8965
9055
  } else {
8966
9056
  for (const [namespace, value] of Object.entries(doc.extensions)) {
8967
- if (!isPlainObject8(value)) {
9057
+ if (!isPlainObject7(value)) {
8968
9058
  warnings.push(`plugin.json: "extensions.${namespace}" is not an object \u2014 ignored`);
8969
9059
  }
8970
9060
  }
@@ -8973,7 +9063,7 @@ function validateManifest(manifest, errors2, warnings) {
8973
9063
  }
8974
9064
  function validateMcpServer(name, entry, errors2) {
8975
9065
  const prefix = `mcp.json: mcpServers.${name}`;
8976
- if (!isPlainObject8(entry)) {
9066
+ if (!isPlainObject7(entry)) {
8977
9067
  errors2.push(`${prefix} must be an object`);
8978
9068
  return;
8979
9069
  }
@@ -9007,7 +9097,7 @@ function validateMcpServer(name, entry, errors2) {
9007
9097
  }
9008
9098
  }
9009
9099
  if (server.env !== undefined) {
9010
- if (!isPlainObject8(server.env)) {
9100
+ if (!isPlainObject7(server.env)) {
9011
9101
  errors2.push(`${prefix}: "env" must be an object of strings`);
9012
9102
  } else {
9013
9103
  for (const [key, value] of Object.entries(server.env)) {
@@ -9046,7 +9136,7 @@ function validateMcpServer(name, entry, errors2) {
9046
9136
  errors2.push(`${prefix}: "url" must be an absolute http(s) URL without user info or fragment; non-loopback endpoints must use https`);
9047
9137
  }
9048
9138
  if (server.headers !== undefined) {
9049
- if (!isPlainObject8(server.headers)) {
9139
+ if (!isPlainObject7(server.headers)) {
9050
9140
  errors2.push(`${prefix}: "headers" must be an object of strings`);
9051
9141
  } else {
9052
9142
  const seen = new Set;
@@ -9083,7 +9173,7 @@ function validateMcp(root, manifestSchema, errors2) {
9083
9173
  errors2.push(`mcp.json: ${error.message}`);
9084
9174
  return;
9085
9175
  }
9086
- if (!isPlainObject8(parsed)) {
9176
+ if (!isPlainObject7(parsed)) {
9087
9177
  errors2.push("mcp.json: configuration must be a JSON object");
9088
9178
  return;
9089
9179
  }
@@ -9106,7 +9196,7 @@ function validateMcp(root, manifestSchema, errors2) {
9106
9196
  }
9107
9197
  }
9108
9198
  const servers = doc.mcpServers;
9109
- if (!isPlainObject8(servers)) {
9199
+ if (!isPlainObject7(servers)) {
9110
9200
  errors2.push('mcp.json: "mcpServers" is required and must be an object');
9111
9201
  return;
9112
9202
  }
@@ -9191,7 +9281,7 @@ function validateAgentPlugin(root) {
9191
9281
  return { ok: false, errors: errors2, warnings };
9192
9282
  }
9193
9283
  validateManifest(manifest, errors2, warnings);
9194
- const manifestSchema = isPlainObject8(manifest) ? manifest["$schema"] : undefined;
9284
+ const manifestSchema = isPlainObject7(manifest) ? manifest["$schema"] : undefined;
9195
9285
  validateMcp(root, manifestSchema, errors2);
9196
9286
  validateSkills(root, errors2, warnings);
9197
9287
  return { ok: errors2.length === 0, errors: errors2, warnings };
@@ -10688,10 +10778,281 @@ function runPluginValidate(options) {
10688
10778
  }
10689
10779
  process.exitCode = 1;
10690
10780
  }
10781
+ var HARNESS_AGENTS_TEMPLATE = `# AGENTS.md \u2014 .mstar/ (harness layer)
10782
+
10783
+ - Path symbols: {HARNESS_DIR} = .mstar/; {PLAN_DIR} = plans/; {SDD_DIR} = sdd/<plan-id>/;
10784
+ {ITERATION_DIR} = iterations/; {KNOWLEDGE_DIR} = knowledge/; {SPECS_DIR} = specs/;
10785
+ {WORKFLOW_DIR} = workflows/; {PROJECT_DIR} = projects/ (SSOT: skills/mstar-conventions).
10786
+ - Process vs results: process artifacts (plans/, iterations/, sdd/, status.json, workflows/,
10787
+ projects/) stay local and gitignored; results (this file, knowledge/, specs/) are tracked
10788
+ and shared across clones.
10789
+ - Done: only @project-manager or @qa-engineer may set Done; implementers set InReview.
10790
+ `;
10791
+ var CANONICAL_NEGATIONS = {
10792
+ "!.mstar/AGENTS.md": true,
10793
+ "!.mstar/knowledge/": true,
10794
+ "!.mstar/knowledge/**": true,
10795
+ "!.mstar/specs/": true,
10796
+ "!.mstar/specs/**": true
10797
+ };
10798
+ function gitWorkspaceRoot(startDir) {
10799
+ try {
10800
+ const cdup = execFileSync8("git", ["rev-parse", "--show-cdup"], {
10801
+ cwd: startDir,
10802
+ encoding: "utf8",
10803
+ stdio: ["ignore", "pipe", "ignore"]
10804
+ }).trim();
10805
+ if (!cdup)
10806
+ return startDir;
10807
+ let boundary = startDir;
10808
+ for (const segment of cdup.split(/[\\/]/)) {
10809
+ if (segment && segment !== ".")
10810
+ boundary = path12.dirname(boundary);
10811
+ }
10812
+ return path12.resolve(boundary);
10813
+ } catch {}
10814
+ return startDir;
10815
+ }
10816
+ function runScaffold(pathArg) {
10817
+ const root = pathArg ? path12.resolve(pathArg) : process.cwd();
10818
+ const harnessDir = scaffoldHarness(root);
10819
+ const projectDir = resolveProjectDir(root, { harnessDir });
10820
+ const created = [];
10821
+ const skipped = [];
10822
+ const workspaceRoot = gitWorkspaceRoot(root);
10823
+ const harnessKind = detectHarnessKind(harnessDir);
10824
+ if (harnessKind === "mstar" && path12.resolve(harnessDir) === path12.join(workspaceRoot, ".mstar")) {
10825
+ const gitignorePath = path12.join(workspaceRoot, ".gitignore");
10826
+ const snippet = emitGitignoreSnippet("mstar");
10827
+ const current = fs8.existsSync(gitignorePath) ? fs8.readFileSync(gitignorePath, "utf8") : "";
10828
+ const lines = new Set(current.split(/\r?\n/).map((line) => line.trim()));
10829
+ const snippetLines = snippet.split(`
10830
+ `).map((line) => line.trim());
10831
+ const fenceEntries = snippetLines.filter((line) => line.startsWith(".mstar/") || line.startsWith("!.mstar/") || line.startsWith(".mstarc"));
10832
+ const commentLines = snippetLines.filter((line) => line.startsWith("#"));
10833
+ const missing = fenceEntries.filter((entry) => !lines.has(entry));
10834
+ if (missing.length > 0) {
10835
+ const missingComments = commentLines.filter((line) => !lines.has(line));
10836
+ const broadRule = ".mstar/**";
10837
+ const currentLines = current.split(/\r?\n/);
10838
+ const firstNegation = currentLines.findIndex((line) => line.trim().startsWith("!.mstar/"));
10839
+ if (!lines.has(broadRule) && firstNegation !== -1) {
10840
+ const blockStart = [...missingComments, broadRule, ...missing.filter((entry) => entry.startsWith("!.mstar/"))];
10841
+ currentLines.splice(firstNegation, 0, ...blockStart);
10842
+ let next = currentLines.join(`
10843
+ `);
10844
+ if (missing.includes(".mstarc"))
10845
+ next = `${next}${next.endsWith(`
10846
+ `) ? "" : `
10847
+ `}.mstarc
10848
+ `;
10849
+ fs8.writeFileSync(gitignorePath, next, "utf8");
10850
+ } else {
10851
+ const prefix = current && !current.endsWith(`
10852
+ `) ? `
10853
+ ` : "";
10854
+ fs8.appendFileSync(gitignorePath, `${prefix}${[...missingComments, ...missing].join(`
10855
+ `)}
10856
+ `, "utf8");
10857
+ }
10858
+ created.push(".gitignore (canonical harness snippet)");
10859
+ }
10860
+ const finalLines = fs8.readFileSync(gitignorePath, "utf8").split(/\r?\n/);
10861
+ const broadIndexes = finalLines.map((line, index) => line.trim() === ".mstar/**" ? index : -1).filter((index) => index !== -1);
10862
+ const canonicalNegationIndexes = finalLines.map((line, index) => CANONICAL_NEGATIONS[line.trim()] === true ? index : -1).filter((index) => index !== -1);
10863
+ const firstCanonicalNegationIndex = canonicalNegationIndexes[0] ?? -1;
10864
+ let normalized = false;
10865
+ if (broadIndexes.length > 0) {
10866
+ let removed = 0;
10867
+ let lastRetainedBroadIndex = broadIndexes[0];
10868
+ for (let i = 1;i < broadIndexes.length; i++) {
10869
+ const candidate = broadIndexes[i] - removed;
10870
+ let crossable = true;
10871
+ for (let j = lastRetainedBroadIndex + 1;j < candidate; j++) {
10872
+ const line = finalLines[j].trim();
10873
+ if (line === "")
10874
+ continue;
10875
+ if (line.startsWith("#"))
10876
+ continue;
10877
+ if (line === ".mstar/**")
10878
+ continue;
10879
+ if (CANONICAL_NEGATIONS[line] === true)
10880
+ continue;
10881
+ if (line === ".mstarc")
10882
+ continue;
10883
+ crossable = false;
10884
+ break;
10885
+ }
10886
+ if (crossable) {
10887
+ finalLines.splice(candidate, 1);
10888
+ removed++;
10889
+ normalized = true;
10890
+ } else {
10891
+ lastRetainedBroadIndex = candidate;
10892
+ }
10893
+ }
10894
+ const keptIndex = finalLines.findIndex((line) => line.trim() === ".mstar/**");
10895
+ if (firstCanonicalNegationIndex !== -1 && keptIndex > firstCanonicalNegationIndex) {
10896
+ let feasible = true;
10897
+ for (let i = firstCanonicalNegationIndex;i < keptIndex; i++) {
10898
+ const line = finalLines[i].trim();
10899
+ if (line === "")
10900
+ continue;
10901
+ if (line.startsWith("#"))
10902
+ continue;
10903
+ if (line === ".mstar/**")
10904
+ continue;
10905
+ if (CANONICAL_NEGATIONS[line] === true)
10906
+ continue;
10907
+ if (line === ".mstarc")
10908
+ continue;
10909
+ feasible = false;
10910
+ break;
10911
+ }
10912
+ if (feasible) {
10913
+ const [broadLine] = finalLines.splice(keptIndex, 1);
10914
+ finalLines.splice(firstCanonicalNegationIndex, 0, broadLine);
10915
+ normalized = true;
10916
+ }
10917
+ }
10918
+ const isTargetedUserMstarRule = (line) => {
10919
+ const trimmed = line.trim();
10920
+ if (trimmed === "" || trimmed.startsWith("#"))
10921
+ return false;
10922
+ if (trimmed === ".mstar/**" || trimmed === ".mstarc")
10923
+ return false;
10924
+ if (CANONICAL_NEGATIONS[trimmed] === true)
10925
+ return false;
10926
+ if (trimmed.startsWith("!.mstar/") || trimmed.startsWith(".mstar/"))
10927
+ return true;
10928
+ return false;
10929
+ };
10930
+ const isOwnedLine = (line) => !isTargetedUserMstarRule(line);
10931
+ const targetedRules = finalLines.filter((line) => isTargetedUserMstarRule(line));
10932
+ if (targetedRules.length > 0) {
10933
+ const owned = finalLines.filter(isOwnedLine);
10934
+ const hadTrailingNewline = owned[owned.length - 1] === "";
10935
+ const ownedBody = hadTrailingNewline ? owned.slice(0, -1) : owned;
10936
+ const tail = [];
10937
+ const ensuredDirs = new Set;
10938
+ const broadPositionsPrePartition = finalLines.map((line, index) => line.trim() === ".mstar/**" ? index : -1).filter((index) => index !== -1);
10939
+ const lastBroadPrePartition = broadPositionsPrePartition[broadPositionsPrePartition.length - 1] ?? -1;
10940
+ for (const rule of targetedRules) {
10941
+ const trimmedRule = rule.trim();
10942
+ if (trimmedRule.startsWith("!") && trimmedRule.endsWith("/**")) {
10943
+ const inner = trimmedRule.slice(1, -3);
10944
+ const segments = inner.split("/").slice(1);
10945
+ let prefix = inner.split("/")[0];
10946
+ for (const segment of segments) {
10947
+ prefix += "/" + segment;
10948
+ const dirNegation = "!" + prefix + "/";
10949
+ const alreadyQueued = ensuredDirs.has(dirNegation);
10950
+ const alreadyPresent = !alreadyQueued && finalLines.some((line, index) => index > lastBroadPrePartition && line.trim() === dirNegation);
10951
+ if (!alreadyQueued && !alreadyPresent) {
10952
+ ensuredDirs.add(dirNegation);
10953
+ tail.push(dirNegation);
10954
+ }
10955
+ }
10956
+ }
10957
+ tail.push(rule);
10958
+ }
10959
+ const rebuilt = [...ownedBody, ...tail];
10960
+ const current2 = finalLines[finalLines.length - 1] === "" ? finalLines.slice(0, -1) : finalLines;
10961
+ if (rebuilt.join(`
10962
+ `) !== current2.join(`
10963
+ `)) {
10964
+ finalLines.length = 0;
10965
+ finalLines.push(...rebuilt);
10966
+ normalized = true;
10967
+ }
10968
+ }
10969
+ const broadAfter = finalLines.map((line, index) => line.trim() === ".mstar/**" ? index : -1).filter((index) => index !== -1);
10970
+ if (broadAfter.length > 1) {
10971
+ const [first, ...duplicates] = broadAfter;
10972
+ let removed2 = 0;
10973
+ for (const duplicate of duplicates) {
10974
+ finalLines.splice(duplicate - removed2, 1);
10975
+ removed2++;
10976
+ }
10977
+ if (removed2 > 0)
10978
+ normalized = true;
10979
+ }
10980
+ const broadIndexesFinal = finalLines.map((line, index) => line.trim() === ".mstar/**" ? index : -1).filter((index) => index !== -1);
10981
+ const canonicalNegationIndexesFinal = finalLines.map((line, index) => CANONICAL_NEGATIONS[line.trim()] === true ? index : -1).filter((index) => index !== -1);
10982
+ const firstCanonicalNegationIndexFinal = canonicalNegationIndexesFinal[0] ?? -1;
10983
+ if (broadIndexesFinal.length > 0 && firstCanonicalNegationIndexFinal !== -1 && broadIndexesFinal[0] > firstCanonicalNegationIndexFinal) {
10984
+ const keptIndex2 = broadIndexesFinal[0];
10985
+ let feasible = true;
10986
+ for (let i = firstCanonicalNegationIndexFinal;i < keptIndex2; i++) {
10987
+ const line = finalLines[i].trim();
10988
+ if (line === "" || line.startsWith("#"))
10989
+ continue;
10990
+ if (line === ".mstar/**" || line === ".mstarc")
10991
+ continue;
10992
+ if (CANONICAL_NEGATIONS[line] === true)
10993
+ continue;
10994
+ feasible = false;
10995
+ break;
10996
+ }
10997
+ if (feasible) {
10998
+ const [broadLine] = finalLines.splice(keptIndex2, 1);
10999
+ finalLines.splice(firstCanonicalNegationIndexFinal, 0, broadLine);
11000
+ normalized = true;
11001
+ }
11002
+ }
11003
+ const broadIndexesLast = finalLines.map((line, index) => line.trim() === ".mstar/**" ? index : -1).filter((index) => index !== -1);
11004
+ if (broadIndexesLast.length > 0) {
11005
+ const lastBroadIndex = broadIndexesLast[broadIndexesLast.length - 1];
11006
+ let insertIndex = finalLines.findIndex((line) => isTargetedUserMstarRule(line));
11007
+ if (insertIndex === -1)
11008
+ insertIndex = finalLines.length;
11009
+ for (const negation of Object.keys(CANONICAL_NEGATIONS)) {
11010
+ const covered = finalLines.some((line, index) => index > lastBroadIndex && line.trim() === negation);
11011
+ if (!covered) {
11012
+ finalLines.splice(insertIndex, 0, negation);
11013
+ insertIndex++;
11014
+ normalized = true;
11015
+ }
11016
+ }
11017
+ }
11018
+ if (normalized && finalLines[finalLines.length - 1] !== "")
11019
+ finalLines.push("");
11020
+ }
11021
+ if (normalized) {
11022
+ fs8.writeFileSync(gitignorePath, finalLines.join(`
11023
+ `), "utf8");
11024
+ created.push(".gitignore (canonical harness snippet reordered)");
11025
+ } else if (missing.length === 0) {
11026
+ skipped.push(".gitignore (canonical harness snippet already present)");
11027
+ }
11028
+ } else {
11029
+ skipped.push(".gitignore (canonical harness snippet) \u2014 custom harness layout manages its own ignore rules");
11030
+ }
11031
+ const agentsPath = path12.join(harnessDir, "AGENTS.md");
11032
+ const agentsLabel = `${path12.basename(harnessDir)}/AGENTS.md`;
11033
+ if (!fs8.existsSync(agentsPath)) {
11034
+ fs8.writeFileSync(agentsPath, HARNESS_AGENTS_TEMPLATE, "utf8");
11035
+ created.push(agentsLabel);
11036
+ } else {
11037
+ skipped.push(`${agentsLabel} (already present)`);
11038
+ }
11039
+ const headline = created.length > 0 ? `scaffold: harness initialized at ${harnessDir}` : `scaffold: harness ensured at ${harnessDir}`;
11040
+ console.log(import_picocolors2.default.green(headline));
11041
+ console.log(` harness dir: ${harnessDir}`);
11042
+ console.log(` project dir: ${projectDir}`);
11043
+ for (const item of created)
11044
+ console.log(` created: ${item}`);
11045
+ for (const item of skipped)
11046
+ console.log(` skipped: ${item}`);
11047
+ }
10691
11048
  program2.name("mstar-harness").description("Morning Star harness CLI for target-based agent bootstrap").version(packageVersion);
10692
11049
  program2.command("init").description("Interactive/non-interactive setup for target agent bootstrap").option("-y, --yes", "Non-interactive mode").option("--target <target>", "Install target", "opencode").option("--scope <scope>", "Config scope: global|project (default: project)").option("--output <path>", "Config file path override, relative to project root").option("--dry-run", "Preview result without writing config").option("--no-fallbacks", "Skip installing the dsh-llm-fallbacks plugin (dsh target only)").option("--pm-model <model>", "Optional: model for project-manager (advanced override)").option("--strategic-models <a,b,c>", "Optional: models for architect/product-manager/prompt-engineer").option("--dev-models <a,b,c>", "Optional: models for fullstack-dev/fullstack-dev-2/frontend-dev").option("--qc-models <a,b,c>", "Optional: models for qc trio").option("--other-models <a,b,c>", "Optional: models for remaining roles").action(async (options) => {
10693
11050
  await runInit5({ ...options, noFallbacks: options.fallbacks === false });
10694
11051
  });
11052
+ var harnessCommand = program2.command("harness").description("harness bootstrap / dir-resolution helpers (engine-backed)");
11053
+ harnessCommand.command("scaffold").description("One-shot harness bootstrap: create the harness dir (default .mstar/, honoring .mstarc harness_dir/project_dir) " + "with dirs + v2 status.json + projects/_default/, append the canonical .gitignore snippet when absent " + "(skipped for non-.mstar layouts), and write a minimal {HARNESS_DIR}/AGENTS.md when absent").argument("[path]", "Root to scaffold (default: cwd)").action((pathArg) => {
11054
+ runScaffold(pathArg);
11055
+ });
10695
11056
  program2.command("doctor").description("Validate Morning Star setup for a target agent config").option("--target <target>", "Target agent for doctor checks", "opencode").option("--scope <scope>", "Config scope: global|project", "project").option("--output <path>", "Config file path override, relative to project root").action((options) => {
10696
11057
  runDoctor5(options);
10697
11058
  });
@@ -10704,7 +11065,7 @@ pathCommand.command("resolve").description("Resolve {HARNESS_DIR} + {SPECS_DIR}
10704
11065
  const startDir = pathArg ? path12.resolve(pathArg) : process.cwd();
10705
11066
  const harnessDir = resolveHarnessDir(startDir);
10706
11067
  if (!harnessDir) {
10707
- const guidance = "no harness dir found \u2014 the bounded probe (.mstar/, .agents/, .plans/, plans/) walked up from " + `${startDir} only within the workspace root (git top-level of the start dir; non-git start probes only itself) \u2014 run \`mstar init\` to bootstrap, or pass a start dir inside a harness-enabled project`;
11068
+ const guidance = "no harness dir found \u2014 the bounded probe (.mstar/, .agents/, .plans/, plans/) walked up from " + `${startDir} only within the workspace root (git top-level of the start dir; non-git start probes only itself) \u2014 run \`mstar harness scaffold\` to bootstrap, or pass a start dir inside a harness-enabled project`;
10708
11069
  if (options.json) {
10709
11070
  console.log(JSON.stringify({ ok: false, startDir, harnessDir: null, specsDir: null, workflowDir: null, projectDir: null, guidance }));
10710
11071
  } else {
@@ -10750,10 +11111,10 @@ statusCommand.command("validate").description("Validate a v2 status.json root or
10750
11111
  }
10751
11112
  const count = gate2.violations.length;
10752
11113
  console.error(import_picocolors2.default.red(`${statusPath}: FAIL (${count} violation${count === 1 ? "" : "s"})`));
10753
- for (const violation14 of gate2.violations) {
10754
- console.error(` - [${violation14.severity}] ${violation14.code}: ${violation14.message}`);
10755
- if (violation14.fix)
10756
- console.error(` fix: ${violation14.fix}`);
11114
+ for (const violation9 of gate2.violations) {
11115
+ console.error(` - [${violation9.severity}] ${violation9.code}: ${violation9.message}`);
11116
+ if (violation9.fix)
11117
+ console.error(` fix: ${violation9.fix}`);
10757
11118
  }
10758
11119
  process.exitCode = 1;
10759
11120
  } catch (error) {
@@ -10846,10 +11207,10 @@ function verifyLeaseRow(row, planId, snapshotPath) {
10846
11207
  }
10847
11208
  const count = result.violations.length;
10848
11209
  console.error(import_picocolors2.default.red(`${snapshotPath}: FAIL plan ${planId} (${count} violation${count === 1 ? "" : "s"})`));
10849
- for (const violation14 of result.violations) {
10850
- console.error(` - [${violation14.severity}] ${violation14.code}: ${violation14.message}`);
10851
- if (violation14.fix)
10852
- console.error(` fix: ${violation14.fix}`);
11210
+ for (const violation9 of result.violations) {
11211
+ console.error(` - [${violation9.severity}] ${violation9.code}: ${violation9.message}`);
11212
+ if (violation9.fix)
11213
+ console.error(` fix: ${violation9.fix}`);
10853
11214
  }
10854
11215
  process.exitCode = 1;
10855
11216
  }
@@ -10969,10 +11330,10 @@ function printChecklist(label, gate2) {
10969
11330
  }
10970
11331
  const count = gate2.violations.length;
10971
11332
  console.error(import_picocolors2.default.red(`${label}: FAIL (${count} violation${count === 1 ? "" : "s"})`));
10972
- for (const violation14 of gate2.violations) {
10973
- console.error(` - [${violation14.severity}] ${violation14.code}: ${violation14.message}`);
10974
- if (violation14.fix)
10975
- console.error(` fix: ${violation14.fix}`);
11333
+ for (const violation9 of gate2.violations) {
11334
+ console.error(` - [${violation9.severity}] ${violation9.code}: ${violation9.message}`);
11335
+ if (violation9.fix)
11336
+ console.error(` fix: ${violation9.fix}`);
10976
11337
  }
10977
11338
  }
10978
11339
  iterationCommand.command("gate").description("Evaluate the phase-transition gate: prints the transition (phase-2-execute / phase-3-close / phase-4-pr-delivery) " + "plus the \xA73.1 entry and \xA73.5 exit checklists. Exit 1 when the gate verdict fails \u2014 during the Phase-3 window " + "(transition: phase-3-close) exit 1 is EXPECTED until the \xA73.4 close items (status: completed + end_date) are " + "written: the exit checklist gates Phase 4, not the Phase-3 entry (qc2 F-003)").requiredOption("--workflow <id>", "Workflow id whose snapshot is evaluated").requiredOption("--compass <path>", "delivery-compass.md path").option("--harness <path>", "Harness dir override (default: resolved {HARNESS_DIR})").option("--branch <branch>", "Current branch probe (exit \xA73.5 item 5)").option("--integration <branch>", "Spec integration branch probe (exit \xA73.5 item 5)").option("--target <branch>", "PR base branch probe (exit \xA73.5 item 6)").action((options) => {
@@ -11006,10 +11367,10 @@ iterationCommand.command("push-cadence").description("\xA75.1a push-cadence prob
11006
11367
  }
11007
11368
  const count = result.violations.length;
11008
11369
  console.error(import_picocolors2.default.red(`push blocked (${count} violation${count === 1 ? "" : "s"})`));
11009
- for (const violation14 of result.violations) {
11010
- console.error(` - [${violation14.severity}] ${violation14.code}: ${violation14.message}`);
11011
- if (violation14.fix)
11012
- console.error(` fix: ${violation14.fix}`);
11370
+ for (const violation9 of result.violations) {
11371
+ console.error(` - [${violation9.severity}] ${violation9.code}: ${violation9.message}`);
11372
+ if (violation9.fix)
11373
+ console.error(` fix: ${violation9.fix}`);
11013
11374
  }
11014
11375
  process.exitCode = 1;
11015
11376
  });
@@ -11347,10 +11708,10 @@ lintCommand.description("Lint <target> (file or dir) \u2014 exit 1 on violations
11347
11708
  violations += result.violations.length;
11348
11709
  const count = result.violations.length;
11349
11710
  console.error(import_picocolors2.default.red(`${label}: FAIL (${count} violation${count === 1 ? "" : "s"})`));
11350
- for (const violation14 of result.violations) {
11351
- console.error(` - [${violation14.severity}] ${violation14.code}: ${violation14.message}`);
11352
- if (violation14.fix)
11353
- console.error(` fix: ${violation14.fix}`);
11711
+ for (const violation9 of result.violations) {
11712
+ console.error(` - [${violation9.severity}] ${violation9.code}: ${violation9.message}`);
11713
+ if (violation9.fix)
11714
+ console.error(` fix: ${violation9.fix}`);
11354
11715
  }
11355
11716
  }
11356
11717
  if (violations > 0)
@@ -11392,16 +11753,31 @@ designMdCommand.command("validate").description("Validate DESIGN.md in <dir>: to
11392
11753
  failScript(error, "design-md validate");
11393
11754
  }
11394
11755
  });
11395
- function parseAuditFindings(text) {
11756
+ var AUDIT_HARDENING_KINDS = ["Hardening", "Checked and clean"];
11757
+ function parseAuditScaffoldInput(text) {
11396
11758
  let parsed;
11397
11759
  try {
11398
11760
  parsed = JSON.parse(text);
11399
11761
  } catch {
11400
11762
  throw new SddScriptError("usage: audit scaffold \u2014 findings file is not valid JSON", 2);
11401
11763
  }
11402
- if (!Array.isArray(parsed))
11403
- throw new SddScriptError("usage: audit scaffold \u2014 findings file must be a JSON array", 2);
11404
- return parsed.map((raw, index) => {
11764
+ let root;
11765
+ let needsVerificationRaw;
11766
+ let hardeningCheckedRaw;
11767
+ if (Array.isArray(parsed)) {
11768
+ root = parsed;
11769
+ } else if (typeof parsed === "object" && parsed !== null) {
11770
+ const doc = parsed;
11771
+ if (!Array.isArray(doc.findings)) {
11772
+ throw new SddScriptError("usage: audit scaffold \u2014 findings file must be a JSON array or an object with a findings array", 2);
11773
+ }
11774
+ root = doc.findings;
11775
+ needsVerificationRaw = doc.needsVerification;
11776
+ hardeningCheckedRaw = doc.hardeningChecked;
11777
+ } else {
11778
+ throw new SddScriptError("usage: audit scaffold \u2014 findings file must be a JSON array or an object with a findings array", 2);
11779
+ }
11780
+ const findings = root.map((raw, index) => {
11405
11781
  if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
11406
11782
  throw new SddScriptError(`usage: audit scaffold \u2014 findings[${index}] is not an object`, 2);
11407
11783
  }
@@ -11444,6 +11820,49 @@ function parseAuditFindings(text) {
11444
11820
  dependsOn
11445
11821
  };
11446
11822
  });
11823
+ return { findings, needsVerification: parseNeedsVerification(needsVerificationRaw), hardeningChecked: parseHardeningChecked(hardeningCheckedRaw) };
11824
+ }
11825
+ function parseNeedsVerification(raw) {
11826
+ if (raw === undefined)
11827
+ return;
11828
+ if (!Array.isArray(raw)) {
11829
+ throw new SddScriptError("usage: audit scaffold \u2014 needsVerification must be an array of {lead, how, evidence?}", 2);
11830
+ }
11831
+ return raw.map((entry, index) => {
11832
+ if (typeof entry !== "object" || entry === null || Array.isArray(entry)) {
11833
+ throw new SddScriptError(`usage: audit scaffold \u2014 needsVerification[${index}] is not an object`, 2);
11834
+ }
11835
+ const e = entry;
11836
+ const lead = typeof e.lead === "string" ? e.lead.trim() : "";
11837
+ const how = typeof e.how === "string" ? e.how.trim() : "";
11838
+ if (lead === "" || how === "") {
11839
+ throw new SddScriptError(`usage: audit scaffold \u2014 needsVerification[${index}] needs non-empty lead and how`, 2);
11840
+ }
11841
+ const evidence = typeof e.evidence === "string" && e.evidence.trim() !== "" ? e.evidence.trim() : undefined;
11842
+ return { lead, how, evidence };
11843
+ });
11844
+ }
11845
+ function parseHardeningChecked(raw) {
11846
+ if (raw === undefined)
11847
+ return;
11848
+ if (!Array.isArray(raw)) {
11849
+ throw new SddScriptError("usage: audit scaffold \u2014 hardeningChecked must be an array of {kind, text}", 2);
11850
+ }
11851
+ return raw.map((entry, index) => {
11852
+ if (typeof entry !== "object" || entry === null || Array.isArray(entry)) {
11853
+ throw new SddScriptError(`usage: audit scaffold \u2014 hardeningChecked[${index}] is not an object`, 2);
11854
+ }
11855
+ const e = entry;
11856
+ const kind = typeof e.kind === "string" ? e.kind : "";
11857
+ const text = typeof e.text === "string" ? e.text.trim() : "";
11858
+ if (!AUDIT_HARDENING_KINDS.includes(kind)) {
11859
+ throw new SddScriptError(`usage: audit scaffold \u2014 hardeningChecked[${index}].kind must be one of ${AUDIT_HARDENING_KINDS.join("|")}`, 2);
11860
+ }
11861
+ if (text === "") {
11862
+ throw new SddScriptError(`usage: audit scaffold \u2014 hardeningChecked[${index}] needs non-empty text`, 2);
11863
+ }
11864
+ return { kind, text };
11865
+ });
11447
11866
  }
11448
11867
  var auditCommand = program2.command("audit").description("audit plan scaffold (engine-backed)");
11449
11868
  function resolveAuditShortSha(cwd, override) {
@@ -11460,7 +11879,7 @@ function resolveAuditShortSha(cwd, override) {
11460
11879
  return "unknown";
11461
11880
  }
11462
11881
  }
11463
- auditCommand.command("scaffold").description("Scaffold an audit-<date>/ plan directory (numbered plan files + README index) from a JSON findings file " + "(exit 2 on usage)").argument("[findings-file]", "JSON file: array of {title, priority, effort, risk, category, dependsOn?, description}").option("--dir <out-dir>", "Output directory (default: ./audit-<date> from --date or today)").option("--sha <commit>", "Short commit SHA for the Planned-at field (default: git rev-parse --short HEAD)").option("--date <YYYY-MM-DD>", "Audit date (default: today)").option("--repo <name>", "Repository name for the README title (default: repo)").action((findingsFile, options) => {
11882
+ auditCommand.command("scaffold").description("Scaffold an audit-<date>/ plan directory (numbered plan files + README index) from a JSON findings file " + "(exit 2 on usage)").argument("[findings-file]", "JSON file: array of {title, priority, effort, risk, category, dependsOn?, description}, or object {findings: [...], needsVerification?: [{lead, how, evidence?}], hardeningChecked?: [{kind, text}]}").option("--dir <out-dir>", "Output directory (default: ./audit-<date> from --date or today)").option("--sha <commit>", "Short commit SHA for the Planned-at field (default: git rev-parse --short HEAD)").option("--date <YYYY-MM-DD>", "Audit date (default: today)").option("--repo <name>", "Repository name for the README title (default: repo)").action((findingsFile, options) => {
11464
11883
  try {
11465
11884
  if (!findingsFile)
11466
11885
  throw new SddScriptError("usage: audit scaffold <findings-file> [--dir <out-dir>]", 2);
@@ -11475,9 +11894,15 @@ auditCommand.command("scaffold").description("Scaffold an audit-<date>/ plan dir
11475
11894
  }
11476
11895
  const date = options.date ?? new Date().toISOString().slice(0, 10);
11477
11896
  const outDir = options.dir !== undefined ? resolveCliPath(options.dir) : resolveCliPath(`audit-${date}`);
11478
- const findings = parseAuditFindings(fs8.readFileSync(abs, "utf8"));
11897
+ const input = parseAuditScaffoldInput(fs8.readFileSync(abs, "utf8"));
11479
11898
  const sha = resolveAuditShortSha(process.cwd(), options.sha);
11480
- const result = scaffoldAuditPlan(outDir, findings, { date, repoName: options.repo, repoShortSha: sha });
11899
+ const result = scaffoldAuditPlan(outDir, input.findings, {
11900
+ date,
11901
+ repoName: options.repo,
11902
+ repoShortSha: sha,
11903
+ needsVerification: input.needsVerification,
11904
+ hardeningChecked: input.hardeningChecked
11905
+ });
11481
11906
  const count = result.files.length;
11482
11907
  console.log(import_picocolors2.default.green(`audit scaffold: OK \u2014 ${count} plan file${count === 1 ? "" : "s"} in ${result.outDir}`));
11483
11908
  for (const file of result.files)