@mstar-harness/opencode 3.2.5 → 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 (24) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/dist/mstar.js +1329 -1271
  3. package/harness-skills/mstar-audit/SKILL.md +3 -2
  4. package/harness-skills/mstar-audit/references/audit-playbook.md +19 -1
  5. package/harness-skills/mstar-audit/references/codebase-audit.md +19 -3
  6. package/harness-skills/mstar-audit/references/finding-format.md +5 -1
  7. package/harness-skills/mstar-audit/references/pr-review.md +78 -9
  8. package/harness-skills/mstar-audit/references/security-review.md +221 -0
  9. package/harness-skills/mstar-conventions/SKILL.md +4 -2
  10. package/harness-skills/mstar-conventions/references/harness-bootstrap-and-agents-layering.md +5 -1
  11. package/harness-skills/mstar-harness-core/SKILL.md +1 -1
  12. package/harness-skills/mstar-roles/SKILL.md +17 -13
  13. package/harness-skills/mstar-roles/references/architect.md +9 -11
  14. package/harness-skills/mstar-roles/references/code-reviewer.md +12 -15
  15. package/harness-skills/mstar-roles/references/frontend-dev.md +9 -11
  16. package/harness-skills/mstar-roles/references/fullstack-dev-shared.md +9 -11
  17. package/harness-skills/mstar-roles/references/ops-engineer.md +9 -11
  18. package/harness-skills/mstar-roles/references/product-manager.md +9 -11
  19. package/harness-skills/mstar-roles/references/project-manager/dispatch-and-assignment.md +2 -0
  20. package/harness-skills/mstar-roles/references/prompt-engineer.md +10 -13
  21. package/harness-skills/mstar-roles/references/qa-engineer.md +8 -12
  22. package/harness-skills/mstar-roles/references/qc-specialist-shared.md +10 -17
  23. package/harness-skills/mstar-roles/references/writing-specialist.md +8 -9
  24. package/package.json +1 -1
package/dist/mstar.js CHANGED
@@ -50,6 +50,7 @@ __export(exports_engine, {
50
50
  resolveSpecsDir: () => resolveSpecsDir,
51
51
  resolveSkillRoot: () => resolveSkillRoot,
52
52
  resolveSddDir: () => resolveSddDir,
53
+ resolveScaffoldDirs: () => resolveScaffoldDirs,
53
54
  resolveRepoEnforcement: () => resolveRepoEnforcement,
54
55
  resolveProjectRoot: () => resolveProjectRoot,
55
56
  resolveProjectDir: () => resolveProjectDir,
@@ -99,6 +100,7 @@ __export(exports_engine, {
99
100
  evaluatePhaseGate: () => evaluatePhaseGate,
100
101
  emitGitignoreSnippet: () => emitGitignoreSnippet,
101
102
  detectHost: () => detectHost,
103
+ detectHarnessKind: () => detectHarnessKind,
102
104
  compoundRefreshScope: () => compoundRefreshScope,
103
105
  composeDispatchGate: () => composeDispatchGate,
104
106
  completenessLevel: () => completenessLevel,
@@ -162,30 +164,30 @@ import { basename, dirname, join, resolve } from "node:path";
162
164
  import { fileURLToPath } from "node:url";
163
165
  import { readFileSync as readFileSync2, statSync } from "node:fs";
164
166
  import { dirname as dirname2, isAbsolute, join as join2, relative, resolve as resolve2 } from "node:path";
165
- import { existsSync as existsSync2, mkdirSync as mkdirSync2, readdirSync, readFileSync as readFileSync3, realpathSync, statSync as statSync2 } from "node:fs";
167
+ 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";
166
168
  import { execFileSync } from "node:child_process";
167
- import { basename as basename2, dirname as dirname3, isAbsolute as isAbsolute2, join as join3, relative as relative2, resolve as resolve3 } from "node:path";
168
- import { existsSync as existsSync3, readFileSync as readFileSync4, readdirSync as readdirSync2, realpathSync as realpathSync2 } from "node:fs";
169
- import { dirname as dirname5, join as join6, resolve as resolve5, sep } from "node:path";
170
- import { mkdirSync as mkdirSync3, rmdirSync, statSync as statSync3, unlinkSync as unlinkSync2, writeFileSync as writeFileSync2 } from "node:fs";
171
- import { dirname as dirname4, isAbsolute as isAbsolute3, join as join4, resolve as resolve4 } from "node:path";
169
+ import { basename as basename2, dirname as dirname5, isAbsolute as isAbsolute3, join as join8, relative as relative2, resolve as resolve5 } from "node:path";
170
+ import { existsSync as existsSync4, readFileSync as readFileSync5, readdirSync as readdirSync3 } from "node:fs";
171
+ import { join as join7 } from "node:path";
172
+ import { existsSync as existsSync2, readdirSync, readFileSync as readFileSync3 } from "node:fs";
173
+ import { join as join3 } from "node:path";
174
+ import { existsSync as existsSync3, readFileSync as readFileSync4, readdirSync as readdirSync2, realpathSync } from "node:fs";
175
+ import { dirname as dirname4, join as join6, resolve as resolve4, sep } from "node:path";
176
+ import { mkdirSync as mkdirSync2, rmdirSync, statSync as statSync2, unlinkSync as unlinkSync2, writeFileSync as writeFileSync2 } from "node:fs";
177
+ import { dirname as dirname3, isAbsolute as isAbsolute2, join as join4, resolve as resolve3 } from "node:path";
172
178
  import { setTimeout as sleep } from "node:timers/promises";
173
179
  import { AsyncLocalStorage } from "node:async_hooks";
174
- import { mkdirSync as mkdirSync4 } from "node:fs";
180
+ import { mkdirSync as mkdirSync3 } from "node:fs";
175
181
  import { join as join5 } from "node:path";
176
182
  import { execFileSync as execFileSync2 } from "node:child_process";
177
- import { existsSync as existsSync4 } from "node:fs";
183
+ import { existsSync as existsSync6 } from "node:fs";
178
184
  import { isAbsolute as isAbsolute4, resolve as resolve6 } from "node:path";
179
185
  import { execFileSync as execFileSync3 } from "node:child_process";
180
- import { mkdirSync as mkdirSync5, readdirSync as readdirSync3, readFileSync as readFileSync5, realpathSync as realpathSync3, statSync as statSync4, writeFileSync as writeFileSync3 } from "node:fs";
181
- import { basename as basename3, dirname as dirname6, isAbsolute as isAbsolute5, join as join7, resolve as resolve7 } from "node:path";
182
- import { existsSync as existsSync5, readdirSync as readdirSync4, readFileSync as readFileSync6 } from "node:fs";
183
- import { join as join8 } from "node:path";
184
- import { existsSync as existsSync6, readFileSync as readFileSync7, readdirSync as readdirSync5 } from "node:fs";
185
- import { join as join9 } from "node:path";
186
- import { copyFileSync, mkdirSync as mkdirSync6, readFileSync as readFileSync8, readdirSync as readdirSync6, writeFileSync as writeFileSync4 } from "node:fs";
186
+ import { mkdirSync as mkdirSync5, readdirSync as readdirSync5, readFileSync as readFileSync7, realpathSync as realpathSync3, statSync as statSync4, writeFileSync as writeFileSync4 } from "node:fs";
187
+ import { basename as basename3, dirname as dirname6, isAbsolute as isAbsolute5, join as join9, resolve as resolve7 } from "node:path";
188
+ import { copyFileSync, mkdirSync as mkdirSync6, readFileSync as readFileSync8, readdirSync as readdirSync6, writeFileSync as writeFileSync5 } from "node:fs";
187
189
  import { dirname as dirname7, isAbsolute as isAbsolute6, join as join10, relative as relative3, resolve as resolve8, sep as sep2 } from "node:path";
188
- import { existsSync as existsSync7, mkdirSync as mkdirSync7, readdirSync as readdirSync7, readFileSync as readFileSync9, rmdirSync as rmdirSync2, rmSync, writeFileSync as writeFileSync5 } from "node:fs";
190
+ import { existsSync as existsSync7, mkdirSync as mkdirSync7, readdirSync as readdirSync7, readFileSync as readFileSync9, rmdirSync as rmdirSync2, rmSync, writeFileSync as writeFileSync6 } from "node:fs";
189
191
  import { basename as basename4, join as join11, resolve as resolve9, sep as sep3 } from "node:path";
190
192
  import { existsSync as existsSync8, readdirSync as readdirSync8, readFileSync as readFileSync10 } from "node:fs";
191
193
  import { basename as basename5, isAbsolute as isAbsolute7, join as join12, relative as relative4, resolve as resolve10, sep as sep4 } from "node:path";
@@ -331,284 +333,338 @@ function isAtOrBelow(dir, root) {
331
333
  const rel = relative(root, dir);
332
334
  return rel === "" || !rel.startsWith("..") && !isAbsolute(rel);
333
335
  }
334
- function resolveHarnessDir(startDir = process.cwd(), opts = {}) {
335
- const start = resolve3(startDir);
336
- const explicit = opts.harnessDir ?? process.env.MSTAR_HARNESS_DIR;
337
- if (explicit)
338
- return resolve3(start, explicit);
339
- const boundary = resolve3(start, opts.workspaceRoot ?? defaultWorkspaceRoot(start));
340
- const rc = loadMstarc(start, boundary);
341
- if (rc !== null && rc.config.harnessDir)
342
- return resolve3(rc.dir, rc.config.harnessDir);
343
- let dir = start;
344
- for (;; ) {
345
- if (!isAtOrBelow2(dir, boundary))
346
- return null;
347
- for (const candidate of [join3(dir, ".mstar"), join3(dir, ".agents"), join3(dir, ".plans"), join3(dir, "plans")]) {
348
- if (isDirectory(candidate))
349
- return candidate;
336
+ function typeName(value) {
337
+ if (value === null)
338
+ return "null";
339
+ if (Array.isArray(value))
340
+ return "array";
341
+ return typeof value;
342
+ }
343
+ function validateCompassShape(doc) {
344
+ const issues = [];
345
+ const expectString = (key, opts = {}) => {
346
+ const value = doc[key];
347
+ if (typeof value !== "string") {
348
+ issues.push({ path: [key], message: `expected string, received ${typeName(value)}` });
349
+ return;
350
350
  }
351
- if (dir === boundary)
352
- return null;
353
- const parent = dirname3(dir);
354
- if (parent === dir)
355
- return null;
356
- dir = parent;
351
+ if (opts.min !== undefined && value.length < opts.min) {
352
+ issues.push({ path: [key], message: `string must contain at least ${opts.min} character(s)` });
353
+ return;
354
+ }
355
+ if (opts.regex !== undefined && !opts.regex.test(value)) {
356
+ issues.push({ path: [key], message: `string must match ${opts.regex}` });
357
+ }
358
+ };
359
+ expectString("iteration_id", { min: 1 });
360
+ expectString("start_date", { regex: DATE_RE });
361
+ const status = doc.status;
362
+ if (typeof status !== "string" || !COMPASS_STATUSES.includes(status)) {
363
+ issues.push({
364
+ path: ["status"],
365
+ message: `expected one of ${COMPASS_STATUSES.map((s) => `'${s}'`).join(" | ")}, received ${typeName(status)}`
366
+ });
357
367
  }
358
- }
359
- function defaultWorkspaceRoot(startDir) {
360
- try {
361
- const cdup = execFileSync("git", ["rev-parse", "--show-cdup"], {
362
- cwd: startDir,
363
- encoding: "utf8",
364
- stdio: ["ignore", "pipe", "ignore"]
365
- }).trim();
366
- if (!cdup)
367
- return startDir;
368
- let boundary = startDir;
369
- for (const segment of cdup.split(/[\\/]/)) {
370
- if (segment && segment !== ".")
371
- boundary = dirname3(boundary);
368
+ expectString("iteration_base_branch", { min: 1 });
369
+ expectString("target_branch", { min: 1 });
370
+ const plans = doc.plans;
371
+ if (plans !== undefined) {
372
+ if (!Array.isArray(plans)) {
373
+ issues.push({ path: ["plans"], message: `expected array, received ${typeName(plans)}` });
374
+ } else {
375
+ plans.forEach((entry, index) => {
376
+ if (typeof entry !== "string") {
377
+ issues.push({ path: ["plans", index], message: `expected string, received ${typeName(entry)}` });
378
+ } else if (entry.length < 1) {
379
+ issues.push({ path: ["plans", index], message: "string must contain at least 1 character(s)" });
380
+ }
381
+ });
372
382
  }
373
- return resolve3(boundary);
374
- } catch {}
375
- return startDir;
383
+ }
384
+ const end_date = doc.end_date;
385
+ if (end_date !== undefined) {
386
+ if (typeof end_date !== "string") {
387
+ issues.push({ path: ["end_date"], message: `expected string, received ${typeName(end_date)}` });
388
+ } else if (!DATE_RE.test(end_date)) {
389
+ issues.push({ path: ["end_date"], message: `string must match ${DATE_RE}` });
390
+ }
391
+ }
392
+ if (issues.length > 0)
393
+ return { ok: false, issues };
394
+ return {
395
+ ok: true,
396
+ data: {
397
+ iteration_id: doc.iteration_id,
398
+ start_date: doc.start_date,
399
+ status,
400
+ iteration_base_branch: doc.iteration_base_branch,
401
+ target_branch: doc.target_branch,
402
+ ...plans !== undefined ? { plans } : {},
403
+ ...end_date !== undefined ? { end_date } : {}
404
+ }
405
+ };
376
406
  }
377
- function isAtOrBelow2(dir, root) {
378
- const rel = relative2(root, dir);
379
- return rel === "" || !rel.startsWith("..") && !isAbsolute2(rel);
407
+ function violation(severity, code, message, fix) {
408
+ return { ok: false, severity, code, message, fix };
380
409
  }
381
- function mstarcDirOverride(harnessDir, key) {
382
- const dir = resolve3(harnessDir);
383
- const rc = loadMstarc(dir, dirname3(dir));
384
- const declared = rc?.config[key];
385
- return declared ? resolve3(rc.dir, declared) : null;
410
+ function isPlainObject(value) {
411
+ return typeof value === "object" && value !== null && !Array.isArray(value);
386
412
  }
387
- function resolveSpecsDir(harnessDir, opts = {}) {
388
- const declared = mstarcDirOverride(harnessDir, "specsDir");
389
- if (declared !== null) {
390
- if (opts.create !== false)
391
- mkdirSync2(declared, { recursive: true });
392
- return declared;
413
+ function validateCompassFrontmatter(doc) {
414
+ if (!isPlainObject(doc)) {
415
+ return {
416
+ ok: false,
417
+ violations: [
418
+ 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 §1.3)", "Fix the frontmatter of {ITERATION_DIR}/<iteration-id>/delivery-compass.md")
419
+ ]
420
+ };
393
421
  }
394
- const harness = resolve3(harnessDir);
395
- const repoRoot = dirname3(harness);
396
- const candidates = [
397
- join3(harness, "specs"),
398
- join3(repoRoot, "docs", "specs"),
399
- join3(repoRoot, "specs"),
400
- join3(harness, "designs"),
401
- join3(repoRoot, "designs")
402
- ];
403
- for (const candidate of candidates) {
404
- if (isDirectory(candidate) && hasFiles(candidate))
405
- return candidate;
422
+ const parsed = validateCompassShape(doc);
423
+ if (!parsed.ok) {
424
+ return {
425
+ ok: false,
426
+ violations: parsed.issues.map((issue) => {
427
+ const field = issue.path.join(".") || "(root)";
428
+ 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 §1.3)`);
429
+ })
430
+ };
406
431
  }
407
- const fallback = join3(harness, "specs");
408
- if (opts.create !== false)
409
- mkdirSync2(fallback, { recursive: true });
410
- return fallback;
432
+ const violations = [];
433
+ const { status, end_date } = parsed.data;
434
+ if (status === "completed" && end_date === undefined) {
435
+ violations.push(violation("high", "COMPASS_END_DATE_REQUIRED", "Compass frontmatter status is 'completed' but end_date is missing — end_date is required at iteration-close (mstar-iteration §3.4, template Fields guide)", "Add `end_date: YYYY-MM-DD` to the frontmatter"));
436
+ }
437
+ if (status !== "completed" && end_date !== undefined) {
438
+ violations.push(violation("medium", "COMPASS_END_DATE_NOT_ALLOWED", `Compass frontmatter sets end_date while status is '${status}' — end_date is only written at iteration-close (mstar-iteration §3.4)`, "Remove end_date until iteration-close"));
439
+ }
440
+ return { ok: violations.length === 0, violations };
411
441
  }
412
- function resolvePlanDir(harnessDir) {
413
- const declared = mstarcDirOverride(harnessDir, "planDir");
414
- if (declared !== null)
415
- return declared;
416
- const dir = resolve3(harnessDir);
417
- const name = basename2(dir);
418
- if (name === ".plans" || name === "plans")
419
- return dir;
420
- return join3(dir, "plans");
442
+ function registeredPlanIds(compassDoc) {
443
+ if (!Array.isArray(compassDoc.plans))
444
+ return [];
445
+ return compassDoc.plans.filter((plan) => typeof plan === "string" && plan.length > 0);
421
446
  }
422
- function assertSafePathComponent(value, what) {
423
- if (value === "" || value === "." || value === ".." || !/^[A-Za-z0-9._-]+$/.test(value)) {
424
- throw new Error(`${what} must be a single safe path component ([A-Za-z0-9._-]+; not "", ".", "..", or containing "/" or "\\") — got ${JSON.stringify(value)}`);
447
+ function findPlanRow(snapshotDoc, planId) {
448
+ if (!Array.isArray(snapshotDoc.plans))
449
+ return null;
450
+ for (const row of snapshotDoc.plans) {
451
+ if (!isPlainObject(row))
452
+ continue;
453
+ const rowId = typeof row.id === "string" ? row.id : typeof row.plan_id === "string" ? row.plan_id : null;
454
+ if (rowId === planId)
455
+ return row;
425
456
  }
457
+ return null;
426
458
  }
427
- function resolveSddDir(harnessDir, planId) {
428
- assertSafePathComponent(planId, "planId");
429
- const base = resolve3(harnessDir);
430
- const declared = mstarcDirOverride(base, "sddDir");
431
- const sddBase = declared !== null ? declared : join3(base, "sdd");
432
- return join3(sddBase, planId);
433
- }
434
- function resolveIterationDir(harnessDir) {
435
- const declared = mstarcDirOverride(harnessDir, "iterationDir");
436
- if (declared !== null)
437
- return declared;
438
- return join3(resolve3(harnessDir), "iterations");
459
+ function entryPlansAllDone(snapshotDoc, registered) {
460
+ const violations = [];
461
+ if (registered.length === 0) {
462
+ violations.push(violation("medium", "COMPASS_NO_PLANS", "Compass frontmatter registers no plans — the all-plans-Done transition cannot be verified (mstar-iteration §1.3 / Phase transition gates)", "List the iteration's plan ids in the compass frontmatter `plans`"));
463
+ return violations;
464
+ }
465
+ for (const planId of registered) {
466
+ const row = findPlanRow(snapshotDoc, planId);
467
+ if (row === null) {
468
+ 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 §3.1 entry item 1)`, "Add the plan row to {HARNESS_DIR}/workflows/<id>/snapshot.json"));
469
+ continue;
470
+ }
471
+ if (row.status !== PLAN_STATUS_DONE) {
472
+ violations.push(violation("high", "PLAN_NOT_DONE", `Plan '${planId}' status is ${JSON.stringify(row.status)} in the workflow snapshot — all compass-registered plans must be 'Done' before iteration-close (mstar-iteration §3.1 entry item 1)`));
473
+ }
474
+ }
475
+ return violations;
439
476
  }
440
- function resolveKnowledgeDir(harnessDir) {
441
- const declared = mstarcDirOverride(harnessDir, "knowledgeDir");
442
- if (declared !== null)
443
- return declared;
444
- return join3(resolve3(harnessDir), "knowledge");
477
+ function entryFrontmatterComplete(compassDoc) {
478
+ return validateCompassFrontmatter(compassDoc).violations;
445
479
  }
446
- function resolveHarnessSubdir(startDir, opts, key, fallback) {
447
- const harness = resolveHarnessDir(startDir, opts);
448
- if (harness === null) {
449
- throw new Error(`harness dir not found from ${resolve3(startDir)} cannot resolve the ${fallback} dir (run \`mstar init\`, pass opts.harnessDir, or set MSTAR_HARNESS_DIR)`);
480
+ function exitFrontmatterClosed(compassDoc) {
481
+ const violations = [];
482
+ if (compassDoc.status !== "completed") {
483
+ violations.push(violation("high", "EXIT_STATUS_NOT_COMPLETED", `Compass frontmatter status must be 'completed' at close exit current: ${JSON.stringify(compassDoc.status)} (mstar-iteration §3.4 / §3.5 exit item 4)`));
450
484
  }
451
- const declared = mstarcDirOverride(harness, key);
452
- return declared !== null ? declared : join3(resolve3(harness), fallback);
485
+ const endDate = compassDoc.end_date;
486
+ if (typeof endDate !== "string" || !DATE_RE.test(endDate)) {
487
+ violations.push(violation("high", "EXIT_END_DATE_REQUIRED", "Compass frontmatter end_date (YYYY-MM-DD) is required when closing (mstar-iteration §3.4 / §3.5 exit item 4)"));
488
+ }
489
+ return violations;
453
490
  }
454
- function resolveWorkflowDir(startDir = process.cwd(), opts = {}) {
455
- return resolveHarnessSubdir(startDir, opts, "workflowDir", "workflows");
491
+ function exitBranchCheck(opts) {
492
+ const violations = [];
493
+ const { currentBranch, specIntegrationBranch } = opts;
494
+ if (currentBranch === undefined || specIntegrationBranch === undefined) {
495
+ violations.push(violation("medium", "EXIT_BRANCH_UNVERIFIABLE", "Cannot verify the current branch is spec_integration_branch — missing currentBranch / specIntegrationBranch probe inputs (mstar-iteration §3.5 exit item 5)"));
496
+ } else if (currentBranch !== specIntegrationBranch) {
497
+ violations.push(violation("high", "EXIT_BRANCH_MISMATCH", `Current branch '${currentBranch}' is not the spec_integration_branch '${specIntegrationBranch}' (mstar-iteration §3.5 exit item 5)`));
498
+ }
499
+ return violations;
456
500
  }
457
- function resolveProjectDir(startDir = process.cwd(), opts = {}) {
458
- return resolveHarnessSubdir(startDir, opts, "projectDir", "projects");
501
+ function exitPrBaseCheck(compassDoc, opts) {
502
+ const violations = [];
503
+ const target = compassDoc.target_branch;
504
+ const { prBaseBranch } = opts;
505
+ if (prBaseBranch === undefined) {
506
+ violations.push(violation("medium", "EXIT_PR_BASE_UNVERIFIABLE", "Cannot verify the PR base — missing prBaseBranch probe input (mstar-iteration §3.5 exit item 6)"));
507
+ } else if (typeof target !== "string" || prBaseBranch !== target) {
508
+ violations.push(violation("high", "EXIT_PR_BASE_MISMATCH", `PR base '${prBaseBranch}' must equal the compass target_branch '${String(target)}' — not an undocumented branch (mstar-iteration §3.5 exit item 6)`));
509
+ }
510
+ return violations;
459
511
  }
460
- function scaffoldHarness(root) {
461
- const harnessDir = join3(resolve3(root), ".mstar");
462
- for (const dir of SCAFFOLD_DIRS)
463
- mkdirSync2(join3(harnessDir, dir), { recursive: true });
464
- const statusPath = join3(harnessDir, "status.json");
465
- if (Object.keys(readJson(statusPath)).length === 0)
466
- writeJson(statusPath, EMPTY_STATUS_TEMPLATE);
467
- return harnessDir;
512
+ function evaluatePhaseGate(snapshotDoc, compassDoc, opts = {}) {
513
+ const registered = registeredPlanIds(compassDoc);
514
+ const entryViolations = [
515
+ ...entryPlansAllDone(snapshotDoc, registered),
516
+ ...entryFrontmatterComplete(compassDoc)
517
+ ];
518
+ const exitViolations = [
519
+ ...exitFrontmatterClosed(compassDoc),
520
+ ...exitBranchCheck(opts),
521
+ ...exitPrBaseCheck(compassDoc, opts)
522
+ ];
523
+ const allPlansDone = registered.length > 0 && registered.every((planId) => {
524
+ const row = findPlanRow(snapshotDoc, planId);
525
+ return row !== null && row.status === PLAN_STATUS_DONE;
526
+ });
527
+ const entry = { ok: entryViolations.length === 0, violations: entryViolations };
528
+ const exit = { ok: exitViolations.length === 0, violations: exitViolations };
529
+ let transition;
530
+ if (!allPlansDone)
531
+ transition = "phase-2-execute";
532
+ else if (entry.ok && exit.ok)
533
+ transition = "phase-4-pr-delivery";
534
+ else
535
+ transition = "phase-3-close";
536
+ const gateBlocking = allPlansDone ? [...entryViolations, ...exitViolations] : [];
537
+ return {
538
+ transition,
539
+ allPlansDone,
540
+ entry,
541
+ exit,
542
+ ok: gateBlocking.length === 0,
543
+ violations: gateBlocking
544
+ };
468
545
  }
469
- function emitGitignoreSnippet(kind) {
470
- if (kind === "agents")
471
- return GITIGNORE_SNIPPET_AGENTS;
472
- if (kind === "mstar")
473
- return GITIGNORE_SNIPPET;
474
- return `${GITIGNORE_SNIPPET}${GITIGNORE_SNIPPET_AGENTS}`;
546
+ function pushCadenceProbe(ciRunning, reviewWaveActive) {
547
+ const violations = [];
548
+ if (ciRunning) {
549
+ violations.push(violation("high", "PUSH_BLOCKED_CI", "CI checks are still queued/in_progress on the current head — do not push until the wave completes (mstar-iteration §5.1a push gate 1)", "Wait for CI to settle, then push once with the whole local batch"));
550
+ }
551
+ if (reviewWaveActive) {
552
+ violations.push(violation("high", "PUSH_BLOCKED_REVIEW_WAVE", "An AI/bot review wave is still running on the current head — do not push until it settles (mstar-iteration §5.1a push gate 2)", "Wait for the review wave, then push once"));
553
+ }
554
+ return { ok: violations.length === 0, violations };
475
555
  }
476
- function validateGitignore(root) {
477
- const gitignorePath = join3(resolve3(root), ".gitignore");
478
- const kind = detectHarnessKind(resolveHarnessDir(root));
479
- let content;
480
- try {
481
- content = readFileSync3(gitignorePath, "utf8");
482
- } catch {
556
+ function assertIndexRowObligations(iterationsDir) {
557
+ if (!existsSync2(iterationsDir)) {
483
558
  return {
484
559
  ok: false,
485
- severity: "medium",
486
- code: "gitignore.missing",
487
- message: `no .gitignore found at ${gitignorePath}`,
488
- fix: `append the canonical snippet (emitGitignoreSnippet(${kind ? `"${kind}"` : ""})) to ${gitignorePath}`
560
+ violations: [
561
+ violation("high", "INDEX_ITERATIONS_DIR_MISSING", `{ITERATION_DIR} '${iterationsDir}' does not exist (mstar-iteration §1.4)`, "Create the iterations directory (path.resolveIterationDir)")
562
+ ]
489
563
  };
490
564
  }
491
- const lines = new Set(content.split(/\r?\n/).map((line) => line.trim()).filter((line) => line.length > 0));
492
- const mstarMissing = GITIGNORE_PROCESS_ENTRIES.filter((entry) => !lines.has(entry));
493
- const agentsMissing = GITIGNORE_PROCESS_ENTRIES_AGENTS.filter((entry) => !lines.has(entry));
494
- let missing;
495
- let label;
496
- if (kind === "agents") {
497
- missing = agentsMissing;
498
- label = ".agents/ set";
499
- } else if (kind === "mstar") {
500
- missing = mstarMissing;
501
- label = ".mstar/ set";
502
- } else {
503
- label = "either .mstar/ or .agents/ set";
504
- missing = mstarMissing.length === 0 || agentsMissing.length === 0 ? [] : mstarMissing.length <= agentsMissing.length ? mstarMissing : agentsMissing;
505
- }
506
- if (missing.length > 0) {
565
+ const iterationIds = readdirSync(iterationsDir, { withFileTypes: true }).filter((entry) => entry.isDirectory()).filter((entry) => existsSync2(join3(iterationsDir, entry.name, COMPASS_FILE))).map((entry) => entry.name).sort();
566
+ const readmePath = join3(iterationsDir, INDEX_README);
567
+ if (!existsSync2(readmePath)) {
507
568
  return {
508
569
  ok: false,
509
- severity: "medium",
510
- code: "gitignore.missing-entries",
511
- message: `.gitignore at ${gitignorePath} is missing canonical harness ignore entries (${label}): ${missing.join(", ")}`,
512
- fix: `append the canonical snippet (emitGitignoreSnippet(${kind ? `"${kind}"` : ""})) to ${gitignorePath}`
570
+ violations: [
571
+ violation("high", "INDEX_README_MISSING", `{ITERATION_DIR}/README.md does not exist — one row per iteration is required (mstar-iteration §1.4)`, `Create {ITERATION_DIR}/README.md with the header '${INDEX_HEADER}' and one row per iteration`)
572
+ ]
513
573
  };
514
574
  }
515
- return {
516
- ok: true,
517
- severity: "low",
518
- code: "gitignore.ok",
519
- message: `.gitignore at ${gitignorePath} contains a complete canonical harness ignore set — default-ignore + tracked re-includes (${label})`
520
- };
575
+ const violations = [];
576
+ const lines = readFileSync3(readmePath, "utf8").split(/\r?\n/);
577
+ if (!lines.some((line) => line.includes(INDEX_HEADER))) {
578
+ violations.push(violation("medium", "INDEX_HEADER_MISSING", `{ITERATION_DIR}/README.md lacks the table header '${INDEX_HEADER}' (mstar-iteration §1.4)`, "Add the header row on first creation"));
579
+ }
580
+ const indexed = new Set;
581
+ for (const line of lines) {
582
+ const match = line.match(/^\s*\|\s*`([^`]+)`\s*\|/);
583
+ if (match)
584
+ indexed.add(match[1].trim());
585
+ }
586
+ for (const id of iterationIds) {
587
+ if (!indexed.has(id)) {
588
+ violations.push(violation("medium", "INDEX_ROW_MISSING", `Iteration '${id}' has a delivery-compass.md but no index row in {ITERATION_DIR}/README.md — one row per iteration (mstar-iteration §1.4)`, `Add | \`${id}\` | [\`${id}/\`](${id}/) | <description> | <status> |`));
589
+ }
590
+ }
591
+ return { ok: violations.length === 0, violations };
521
592
  }
522
- function detectHarnessKind(harnessDir) {
523
- if (!harnessDir)
524
- return null;
525
- const name = basename2(resolve3(harnessDir));
526
- if (name === ".mstar")
527
- return "mstar";
528
- if (name === ".agents")
529
- return "agents";
530
- return null;
593
+ function parseCompassFrontmatter(filePath) {
594
+ return parseCompassFrontmatterText(readFileSync3(filePath, "utf8"), filePath);
531
595
  }
532
- function assertPlanWritingPath(planPath, harnessDir) {
533
- const planAbs = resolve3(planPath);
534
- if (!harnessDir) {
535
- return {
536
- ok: false,
537
- severity: "high",
538
- code: "plan-path.no-harness",
539
- message: `persistent plan tracking is not enabled — cannot place plan ${planAbs} under {PLAN_DIR}`,
540
- fix: "initialize the harness (scaffoldHarness) so plans land in {PLAN_DIR}"
541
- };
596
+ function parseCompassFrontmatterText(content, filePath) {
597
+ const lines = content.split(/\r?\n/);
598
+ if (lines[0]?.trim() !== "---") {
599
+ throw new Error(`no YAML frontmatter fence in ${filePath} (expected first line "---")`);
542
600
  }
543
- const planDir = resolvePlanDir(harnessDir);
544
- const rel = relative2(planDir, planAbs);
545
- const inside = rel === "" || !rel.startsWith("..") && !isAbsolute2(rel);
546
- if (!inside) {
547
- return {
548
- ok: false,
549
- severity: "high",
550
- code: "plan-path.outside-plan-dir",
551
- message: `plan file ${planAbs} is outside {PLAN_DIR} (${planDir})`,
552
- fix: `write the plan under ${planDir}`
553
- };
601
+ const end = lines.indexOf("---", 1);
602
+ if (end === -1) {
603
+ throw new Error(`unterminated YAML frontmatter in ${filePath} (no closing "---")`);
554
604
  }
555
- if (existsSync2(planAbs)) {
556
- try {
557
- const canonicalPlan = realpathSync(planAbs);
558
- const canonicalPlanDir = existsSync2(planDir) ? realpathSync(planDir) : resolve3(planDir);
559
- const canonicalRel = relative2(canonicalPlanDir, canonicalPlan);
560
- const canonicalInside = canonicalRel === "" || !canonicalRel.startsWith("..") && !isAbsolute2(canonicalRel);
561
- if (!canonicalInside) {
562
- return {
563
- ok: false,
564
- severity: "high",
565
- code: "plan-path.symlink-escape",
566
- message: `plan file ${planAbs} resolves to ${canonicalPlan}, outside {PLAN_DIR} (${canonicalPlanDir})`,
567
- fix: `write the plan under ${planDir}`
568
- };
569
- }
570
- } catch {}
605
+ const doc = {};
606
+ let listKey = null;
607
+ for (let i = 1;i < end; i += 1) {
608
+ const line = lines[i] ?? "";
609
+ if (!line.trim() || line.trim().startsWith("#"))
610
+ continue;
611
+ if (listKey !== null && /^\s*-\s+/.test(line)) {
612
+ const item = line.replace(/^\s*-\s+/, "").trim().replace(/^["']|["']$/g, "");
613
+ if (!Array.isArray(doc[listKey]))
614
+ doc[listKey] = [];
615
+ doc[listKey].push(item);
616
+ continue;
617
+ }
618
+ listKey = null;
619
+ const kv = line.match(/^([A-Za-z_][A-Za-z0-9_-]*):\s*(.*)$/);
620
+ if (!kv) {
621
+ throw new Error(`unsupported frontmatter line in ${filePath}: ${JSON.stringify(line)}`);
622
+ }
623
+ const value = kv[2].trim();
624
+ doc[kv[1]] = value === "" ? null : /^\[.*\]$/.test(value) ? parseFlowArray(value, filePath) : value.replace(/^["']|["']$/g, "");
625
+ listKey = value === "" ? kv[1] : null;
571
626
  }
572
- return {
573
- ok: true,
574
- severity: "low",
575
- code: "plan-path.ok",
576
- message: `plan file ${planAbs} lives under {PLAN_DIR} (${planDir})`
577
- };
627
+ return doc;
578
628
  }
579
- function isDirectory(dir) {
580
- try {
581
- return statSync2(dir).isDirectory();
582
- } catch {
583
- return false;
629
+ function parseFlowArray(raw, filePath) {
630
+ const inner = raw.slice(1, -1);
631
+ if (/[[\]]/.test(inner)) {
632
+ throw new Error(`nested flow-style array in ${filePath}: ${JSON.stringify(raw)} — only flat scalar items are supported (e.g. [a, b])`);
584
633
  }
585
- }
586
- function hasFiles(dir) {
587
- try {
588
- for (const entry of readdirSync(dir, { withFileTypes: true })) {
589
- if (entry.isDirectory()) {
590
- if (hasFiles(join3(dir, entry.name)))
591
- return true;
592
- } else if (entry.isFile()) {
593
- return true;
594
- }
634
+ let quote = null;
635
+ for (const ch of inner) {
636
+ if (ch === '"' || ch === "'") {
637
+ if (quote === null)
638
+ quote = ch;
639
+ else if (quote === ch)
640
+ quote = null;
641
+ } else if (ch === "," && quote !== null) {
642
+ throw new Error(`ambiguous flow-style array in ${filePath}: ${JSON.stringify(raw)} — quoted item containing comma cannot be split unambiguously (flat scalar items only)`);
595
643
  }
596
- return false;
597
- } catch {
598
- return false;
599
644
  }
645
+ if (quote !== null) {
646
+ throw new Error(`unterminated ${quote} quote in flow-style array in ${filePath}: ${JSON.stringify(raw)}`);
647
+ }
648
+ const items = [];
649
+ for (const part of inner.split(",")) {
650
+ const item = part.trim().replace(/^["']|["']$/g, "");
651
+ if (item === "")
652
+ continue;
653
+ items.push(item);
654
+ }
655
+ return items;
600
656
  }
601
- function isPlainObject(value) {
657
+ function isPlainObject2(value) {
602
658
  return typeof value === "object" && value !== null && !Array.isArray(value);
603
659
  }
604
- function violation(severity, code, message, fix) {
660
+ function violation2(severity, code, message, fix) {
605
661
  return { ok: false, severity, code, message, fix };
606
662
  }
607
663
  function validateNonEmptyString(violations, value, field, missingCode, invalidCode) {
608
664
  if (value === undefined) {
609
- violations.push(violation("high", missingCode, `missing required field: ${field}`));
665
+ violations.push(violation2("high", missingCode, `missing required field: ${field}`));
610
666
  } else if (typeof value !== "string" || value.trim() === "") {
611
- violations.push(violation("medium", invalidCode, `${field} must be a non-empty string`));
667
+ violations.push(violation2("medium", invalidCode, `${field} must be a non-empty string`));
612
668
  }
613
669
  }
614
670
  function isValidClaimedAt(value) {
@@ -616,66 +672,66 @@ function isValidClaimedAt(value) {
616
672
  }
617
673
  function validateExecutionLease(lease) {
618
674
  const violations = [];
619
- if (!isPlainObject(lease)) {
675
+ if (!isPlainObject2(lease)) {
620
676
  return {
621
677
  ok: false,
622
678
  violations: [
623
- violation("high", "lease.execution-lease.invalid", "execution_lease must be an object — null and tombstone objects are invalid; writers delete the key on release")
679
+ violation2("high", "lease.execution-lease.invalid", "execution_lease must be an object — null and tombstone objects are invalid; writers delete the key on release")
624
680
  ]
625
681
  };
626
682
  }
627
683
  validateNonEmptyString(violations, lease.holder, "holder", "lease.execution-lease.missing-holder", "lease.execution-lease.invalid-holder");
628
684
  if (lease.claimed_at === undefined) {
629
- violations.push(violation("high", "lease.execution-lease.missing-claimed-at", "missing required field: claimed_at"));
685
+ violations.push(violation2("high", "lease.execution-lease.missing-claimed-at", "missing required field: claimed_at"));
630
686
  } else if (!isValidClaimedAt(lease.claimed_at)) {
631
- 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"));
687
+ 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"));
632
688
  }
633
689
  if (lease.worktree_path === undefined) {
634
- violations.push(violation("high", "lease.execution-lease.missing-worktree-path", "missing required field: worktree_path"));
690
+ violations.push(violation2("high", "lease.execution-lease.missing-worktree-path", "missing required field: worktree_path"));
635
691
  } else if (typeof lease.worktree_path !== "string" || lease.worktree_path.trim() === "") {
636
- violations.push(violation("medium", "lease.execution-lease.invalid-worktree-path", "worktree_path must be a non-empty string"));
637
- } else if (!isAbsolute3(lease.worktree_path)) {
638
- violations.push(violation("medium", "lease.execution-lease.invalid-worktree-path", "worktree_path must be an absolute path — it identifies the dedicated feature-worktree root (and MUST differ from metadata.control_worktree_path)"));
692
+ violations.push(violation2("medium", "lease.execution-lease.invalid-worktree-path", "worktree_path must be a non-empty string"));
693
+ } else if (!isAbsolute2(lease.worktree_path)) {
694
+ violations.push(violation2("medium", "lease.execution-lease.invalid-worktree-path", "worktree_path must be an absolute path — it identifies the dedicated feature-worktree root (and MUST differ from metadata.control_worktree_path)"));
639
695
  }
640
696
  validateNonEmptyString(violations, lease.working_branch, "working_branch", "lease.execution-lease.missing-working-branch", "lease.execution-lease.invalid-working-branch");
641
697
  if (lease.session_label !== undefined && typeof lease.session_label !== "string") {
642
- violations.push(violation("medium", "lease.execution-lease.invalid-session-label", "session_label must be a string (display only — never used for ownership comparison)"));
698
+ violations.push(violation2("medium", "lease.execution-lease.invalid-session-label", "session_label must be a string (display only — never used for ownership comparison)"));
643
699
  }
644
700
  return { ok: violations.length === 0, violations };
645
701
  }
646
702
  function validateIntegrationMergeLease(lease) {
647
703
  const violations = [];
648
- if (!isPlainObject(lease)) {
704
+ if (!isPlainObject2(lease)) {
649
705
  return {
650
706
  ok: false,
651
707
  violations: [
652
- violation("high", "lease.merge-lease.invalid", "integration_merge_lease must be an object — absent means unclaimed; null and tombstone objects are invalid; writers delete the key on release")
708
+ violation2("high", "lease.merge-lease.invalid", "integration_merge_lease must be an object — absent means unclaimed; null and tombstone objects are invalid; writers delete the key on release")
653
709
  ]
654
710
  };
655
711
  }
656
712
  validateNonEmptyString(violations, lease.holder, "holder", "lease.merge-lease.missing-holder", "lease.merge-lease.invalid-holder");
657
713
  if (lease.claimed_at === undefined) {
658
- violations.push(violation("high", "lease.merge-lease.missing-claimed-at", "missing required field: claimed_at"));
714
+ violations.push(violation2("high", "lease.merge-lease.missing-claimed-at", "missing required field: claimed_at"));
659
715
  } else if (!isValidClaimedAt(lease.claimed_at)) {
660
- 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"));
716
+ 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"));
661
717
  }
662
718
  validateNonEmptyString(violations, lease.plan_id, "plan_id", "lease.merge-lease.missing-plan-id", "lease.merge-lease.invalid-plan-id");
663
719
  validateNonEmptyString(violations, lease.source_branch, "source_branch", "lease.merge-lease.missing-source-branch", "lease.merge-lease.invalid-source-branch");
664
720
  validateNonEmptyString(violations, lease.target_branch, "target_branch", "lease.merge-lease.missing-target-branch", "lease.merge-lease.invalid-target-branch");
665
721
  if (lease.session_label !== undefined && typeof lease.session_label !== "string") {
666
- violations.push(violation("medium", "lease.merge-lease.invalid-session-label", "session_label must be a string (display only — never used for ownership comparison)"));
722
+ violations.push(violation2("medium", "lease.merge-lease.invalid-session-label", "session_label must be a string (display only — never used for ownership comparison)"));
667
723
  }
668
724
  return { ok: violations.length === 0, violations };
669
725
  }
670
726
  function claimLease(row, holder, fields) {
671
727
  const lease = row.execution_lease;
672
728
  if (lease !== undefined) {
673
- if (!isPlainObject(lease)) {
729
+ if (!isPlainObject2(lease)) {
674
730
  return {
675
731
  ok: false,
676
732
  row,
677
733
  violations: [
678
- violation("high", "lease.claim.tombstone", "execution_lease must be an object — null and tombstone objects are invalid; resolve the corrupt state before claiming")
734
+ violation2("high", "lease.claim.tombstone", "execution_lease must be an object — null and tombstone objects are invalid; resolve the corrupt state before claiming")
679
735
  ]
680
736
  };
681
737
  }
@@ -684,7 +740,7 @@ function claimLease(row, holder, fields) {
684
740
  ok: false,
685
741
  row,
686
742
  violations: [
687
- violation("high", "lease.claim.other-holder", `execution_lease held by ${JSON.stringify(lease.holder)} — no timestamp makes it stealable; Blocked unless the current-turn user explicitly overrides (then audit plans[].notes)`)
743
+ violation2("high", "lease.claim.other-holder", `execution_lease held by ${JSON.stringify(lease.holder)} — no timestamp makes it stealable; Blocked unless the current-turn user explicitly overrides (then audit plans[].notes)`)
688
744
  ]
689
745
  };
690
746
  }
@@ -693,7 +749,7 @@ function claimLease(row, holder, fields) {
693
749
  ok: false,
694
750
  row,
695
751
  violations: [
696
- violation("high", "lease.claim.verify-held-lease", `same holder but lease ${lease.worktree_path} @ ${lease.working_branch} does not match the Assignment ${fields.worktree_path} @ ${fields.working_branch} — verify-held-lease failed`)
752
+ violation2("high", "lease.claim.verify-held-lease", `same holder but lease ${lease.worktree_path} @ ${lease.working_branch} does not match the Assignment ${fields.worktree_path} @ ${fields.working_branch} — verify-held-lease failed`)
697
753
  ]
698
754
  };
699
755
  }
@@ -704,7 +760,7 @@ function claimLease(row, holder, fields) {
704
760
  ok: false,
705
761
  row,
706
762
  violations: [
707
- violation("high", "lease.claim.orphan", "plan is InProgress without an execution_lease — orphan: STOP, no writable dispatch until recovery (status-and-residuals.md § Orphan recovery); do not invent a lease")
763
+ violation2("high", "lease.claim.orphan", "plan is InProgress without an execution_lease — orphan: STOP, no writable dispatch until recovery (status-and-residuals.md § Orphan recovery); do not invent a lease")
708
764
  ]
709
765
  };
710
766
  }
@@ -713,7 +769,7 @@ function claimLease(row, holder, fields) {
713
769
  ok: false,
714
770
  row,
715
771
  violations: [
716
- violation("high", "lease.claim.status", `claim requires status Todo or Blocked (got ${JSON.stringify(row.status)}) — claim-before-InProgress contract`)
772
+ violation2("high", "lease.claim.status", `claim requires status Todo or Blocked (got ${JSON.stringify(row.status)}) — claim-before-InProgress contract`)
717
773
  ]
718
774
  };
719
775
  }
@@ -734,12 +790,12 @@ function releaseLease(row, holder) {
734
790
  if (row.execution_lease === undefined) {
735
791
  return { ok: true, row, outcome: "released", violations: [] };
736
792
  }
737
- if (!isPlainObject(row.execution_lease)) {
793
+ if (!isPlainObject2(row.execution_lease)) {
738
794
  return {
739
795
  ok: false,
740
796
  row,
741
797
  violations: [
742
- violation("high", "lease.release.tombstone", "execution_lease must be an object — null and tombstone objects are invalid; resolve the corrupt state before releasing")
798
+ violation2("high", "lease.release.tombstone", "execution_lease must be an object — null and tombstone objects are invalid; resolve the corrupt state before releasing")
743
799
  ]
744
800
  };
745
801
  }
@@ -748,7 +804,7 @@ function releaseLease(row, holder) {
748
804
  ok: false,
749
805
  row,
750
806
  violations: [
751
- violation("high", "lease.release.other-holder", `execution_lease held by ${JSON.stringify(row.execution_lease.holder)} — release requires the same-session holder; a different holder must Blocked (no timestamp makes it stealable)`)
807
+ violation2("high", "lease.release.other-holder", `execution_lease held by ${JSON.stringify(row.execution_lease.holder)} — release requires the same-session holder; a different holder must Blocked (no timestamp makes it stealable)`)
752
808
  ]
753
809
  };
754
810
  }
@@ -756,10 +812,10 @@ function releaseLease(row, holder) {
756
812
  return { ok: true, row: rest, outcome: "released", violations: [] };
757
813
  }
758
814
  function sameHolderResume(lease, holder) {
759
- return isPlainObject(lease) && lease.holder === holder;
815
+ return isPlainObject2(lease) && lease.holder === holder;
760
816
  }
761
817
  function canSteal(lease, holder, opts = {}) {
762
- if (!isPlainObject(lease) || lease.holder === holder)
818
+ if (!isPlainObject2(lease) || lease.holder === holder)
763
819
  return false;
764
820
  return opts.userOverride === true;
765
821
  }
@@ -774,14 +830,14 @@ function verifyPlanExecutionLease(row, planId) {
774
830
  return {
775
831
  ok: false,
776
832
  violations: [
777
- violation("high", "lease.verify.orphan", "plan is InProgress without an execution_lease — orphan: STOP, no writable dispatch until recovery (status-and-residuals.md § Orphan recovery)")
833
+ violation2("high", "lease.verify.orphan", "plan is InProgress without an execution_lease — orphan: STOP, no writable dispatch until recovery (status-and-residuals.md § Orphan recovery)")
778
834
  ]
779
835
  };
780
836
  }
781
837
  return {
782
838
  ok: false,
783
839
  violations: [
784
- violation("high", "lease.verify.missing", `plan ${planId} has no execution_lease at the SSOT location plans[].execution_lease`)
840
+ violation2("high", "lease.verify.missing", `plan ${planId} has no execution_lease at the SSOT location plans[].execution_lease`)
785
841
  ]
786
842
  };
787
843
  }
@@ -790,7 +846,7 @@ function verifyPlanExecutionLease(row, planId) {
790
846
  return { ok: violations.length === 0, violations, lease };
791
847
  }
792
848
  async function withStatusWriteLock(statusPath, fn, opts = {}) {
793
- const lockDir = join4(dirname4(resolve4(statusPath)), STATUS_WRITE_LOCKDIR);
849
+ const lockDir = join4(dirname3(resolve3(statusPath)), STATUS_WRITE_LOCKDIR);
794
850
  const held = heldLockDirs.getStore();
795
851
  if (held !== undefined && held.has(lockDir)) {
796
852
  throw new Error(`${lockDir} is already held by this process in this async context — withStatusWriteLock is not reentrant; a nested acquisition on the same status.json is a bug`);
@@ -801,8 +857,8 @@ async function withStatusWriteLock(statusPath, fn, opts = {}) {
801
857
  let acquired = null;
802
858
  for (;; ) {
803
859
  try {
804
- mkdirSync3(lockDir);
805
- const st = statSync3(lockDir);
860
+ mkdirSync2(lockDir);
861
+ const st = statSync2(lockDir);
806
862
  acquired = { dev: st.dev, ino: st.ino };
807
863
  break;
808
864
  } catch (error) {
@@ -824,7 +880,7 @@ async function withStatusWriteLock(statusPath, fn, opts = {}) {
824
880
  } finally {
825
881
  owns.delete(lockDir);
826
882
  try {
827
- const current = statSync3(lockDir);
883
+ const current = statSync2(lockDir);
828
884
  if (acquired !== null && current.dev === acquired.dev && current.ino === acquired.ino) {
829
885
  try {
830
886
  unlinkSync2(join4(lockDir, LOCKDIR_HOLDER_PID));
@@ -834,7 +890,7 @@ async function withStatusWriteLock(statusPath, fn, opts = {}) {
834
890
  } catch {}
835
891
  }
836
892
  }
837
- function violation2(severity, code, message, fix) {
893
+ function violation3(severity, code, message, fix) {
838
894
  return { ok: false, severity, code, message, fix };
839
895
  }
840
896
  function parseAssignmentFields(assignmentText) {
@@ -880,11 +936,11 @@ function parseEnforcementFlag(text) {
880
936
  }
881
937
  function requireField(violations, value, label, code) {
882
938
  if (value === undefined) {
883
- const v = violation2("high", `assignment.field.missing-${code}`, `missing required Assignment field: ${label}`, `add "**${label}**: <value>" to the Assignment`);
939
+ const v = violation3("high", `assignment.field.missing-${code}`, `missing required Assignment field: ${label}`, `add "**${label}**: <value>" to the Assignment`);
884
940
  v.aliases = [`assignment.presence.missing-${code}`];
885
941
  violations.push(v);
886
942
  } else if (value === "") {
887
- const v = violation2("high", `assignment.field.invalid-${code}`, `${label} must be non-empty`, `fill in "**${label}**: <value>"`);
943
+ const v = violation3("high", `assignment.field.invalid-${code}`, `${label} must be non-empty`, `fill in "**${label}**: <value>"`);
888
944
  v.aliases = [`assignment.presence.missing-${code}`];
889
945
  violations.push(v);
890
946
  }
@@ -943,20 +999,20 @@ function validateAssignmentFields(assignmentText, opts = {}) {
943
999
  const formCount = Number(workingPresent) + Number(policyPresent);
944
1000
  const forms = parseAssignmentBranchForms(assignmentText);
945
1001
  if (formCount === 0) {
946
- violations.push(violation2("high", "assignment.field.branch-missing", "writable assignment must contain exactly one branch form", `add exactly one of: ${BRANCH_FORMS_HINT}`));
1002
+ violations.push(violation3("high", "assignment.field.branch-missing", "writable assignment must contain exactly one branch form", `add exactly one of: ${BRANCH_FORMS_HINT}`));
947
1003
  } else if (formCount > 1) {
948
- violations.push(violation2("high", "assignment.field.branch-multiple", `writable assignment contains ${formCount} branch forms (Working branch + Branch policy) — exactly one required`, `keep exactly one of: ${BRANCH_FORMS_HINT}`));
1004
+ violations.push(violation3("high", "assignment.field.branch-multiple", `writable assignment contains ${formCount} branch forms (Working branch + Branch policy) — exactly one required`, `keep exactly one of: ${BRANCH_FORMS_HINT}`));
949
1005
  } else if (workingPresent) {
950
1006
  const create = forms.createForm;
951
1007
  if (create !== undefined && (create.base === undefined || create.base.trim() === "" || create.name.trim() === "")) {
952
- 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`)"));
1008
+ 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`)"));
953
1009
  }
954
1010
  } else if (policyPresent) {
955
1011
  const direct = forms.directOn;
956
1012
  if (direct === undefined) {
957
- violations.push(violation2("high", "assignment.field.branch-policy-missing-branch", `unparseable Branch policy: "${fields.branchPolicy}" (expected "direct on <branch> — <reason>")`, "start the field with `direct on <branch>`"));
1013
+ violations.push(violation3("high", "assignment.field.branch-policy-missing-branch", `unparseable Branch policy: "${fields.branchPolicy}" (expected "direct on <branch> — <reason>")`, "start the field with `direct on <branch>`"));
958
1014
  } else if (direct.reason === "") {
959
- violations.push(violation2("high", "assignment.field.branch-policy-missing-reason", `Branch policy "direct on ${direct.branch}" is missing the reason`, 'append "— <reason>" after the branch name'));
1015
+ violations.push(violation3("high", "assignment.field.branch-policy-missing-reason", `Branch policy "direct on ${direct.branch}" is missing the reason`, 'append "— <reason>" after the branch name'));
960
1016
  }
961
1017
  }
962
1018
  }
@@ -967,7 +1023,7 @@ function assertDefaultBranchProtected(branch, opts = {}) {
967
1023
  const violations = [];
968
1024
  const normalized = branch.trim();
969
1025
  if (normalized !== "" && defaultBranches.includes(normalized) && opts.directOnException !== true) {
970
- 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} — <reason>" to the Assignment, or use a feature branch`));
1026
+ 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} — <reason>" to the Assignment, or use a feature branch`));
971
1027
  }
972
1028
  return { ok: violations.length === 0, violations };
973
1029
  }
@@ -976,7 +1032,7 @@ function executionModeToN(executionMode, opts = {}) {
976
1032
  const mode = executionMode.trim().toLowerCase().split(/\s+/)[0] ?? "";
977
1033
  let n;
978
1034
  if (mode === "") {
979
- violations.push(violation2("high", "dispatch.execution-mode.missing", "missing required Assignment field: Execution mode", 'add "**Execution mode**: sdd | inline | targeted"'));
1035
+ violations.push(violation3("high", "dispatch.execution-mode.missing", "missing required Assignment field: Execution mode", 'add "**Execution mode**: sdd | inline | targeted"'));
980
1036
  } else if (mode === "sdd") {
981
1037
  n = 3;
982
1038
  } else if (mode === "inline") {
@@ -984,14 +1040,14 @@ function executionModeToN(executionMode, opts = {}) {
984
1040
  } else if (mode === "targeted") {
985
1041
  const seats = [...new Set((opts.seats ?? []).map((s) => s.trim()).filter((s) => s !== ""))];
986
1042
  if (seats.length === 0) {
987
- violations.push(violation2("high", "dispatch.execution-mode.missing-seats", 'execution mode "targeted" requires listed reviewer seats', 'add "QC re-review: targeted — reviewers: <role-id>, …" to the Assignment and pass the seats'));
1043
+ violations.push(violation3("high", "dispatch.execution-mode.missing-seats", 'execution mode "targeted" requires listed reviewer seats', 'add "QC re-review: targeted — reviewers: <role-id>, …" to the Assignment and pass the seats'));
988
1044
  } else if (seats.length > 3) {
989
- violations.push(violation2("high", "dispatch.execution-mode.too-many-seats", `execution mode "targeted" lists ${seats.length} reviewer seats — at most 3 (targeted re-review seats are the tri seats, N = 1–3)`, "list at most three reviewer seats for the targeted re-review"));
1045
+ violations.push(violation3("high", "dispatch.execution-mode.too-many-seats", `execution mode "targeted" lists ${seats.length} reviewer seats — at most 3 (targeted re-review seats are the tri seats, N = 1–3)`, "list at most three reviewer seats for the targeted re-review"));
990
1046
  } else {
991
1047
  n = seats.length;
992
1048
  }
993
1049
  } else {
994
- violations.push(violation2("high", "dispatch.execution-mode.unknown", `unknown execution mode "${executionMode.trim()}" (expected sdd | inline | targeted)`, "fix the Execution mode field"));
1050
+ violations.push(violation3("high", "dispatch.execution-mode.unknown", `unknown execution mode "${executionMode.trim()}" (expected sdd | inline | targeted)`, "fix the Execution mode field"));
995
1051
  }
996
1052
  return n === undefined ? { ok: false, violations } : { ok: true, violations, n };
997
1053
  }
@@ -1005,7 +1061,7 @@ function assertTriIdentity(reviewerRoles) {
1005
1061
  return {
1006
1062
  ok: false,
1007
1063
  violations: [
1008
- 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")
1064
+ 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")
1009
1065
  ]
1010
1066
  };
1011
1067
  }
@@ -1049,7 +1105,7 @@ function antiRecursionPrecheck(subagentType, executeAs) {
1049
1105
  return {
1050
1106
  ok: false,
1051
1107
  violations: [
1052
- violation2("critical", "dispatch.anti-recursion.empty-binding", `empty host role binding — the host cannot report which agent is calling, so anti-recursion cannot be proven (a dispatch could silently recurse)`, "set the host role-binding field (omp task entry `agent` / opencode `subagent` / cursor `subagent_type` / dsh `dispatchBinding`) before dispatching")
1108
+ violation3("critical", "dispatch.anti-recursion.empty-binding", `empty host role binding — the host cannot report which agent is calling, so anti-recursion cannot be proven (a dispatch could silently recurse)`, "set the host role-binding field (omp task entry `agent` / opencode `subagent` / cursor `subagent_type` / dsh `dispatchBinding`) before dispatching")
1053
1109
  ]
1054
1110
  };
1055
1111
  }
@@ -1057,51 +1113,51 @@ function antiRecursionPrecheck(subagentType, executeAs) {
1057
1113
  return {
1058
1114
  ok: false,
1059
1115
  violations: [
1060
- violation2("critical", "dispatch.anti-recursion.self-type", `recursive dispatch refused: role binding "${subagentType}" equals Execute as "${executeAs}" (leaf executors must not re-invoke their own role)`, "complete the work in this session, or return Blocked to project-manager")
1116
+ violation3("critical", "dispatch.anti-recursion.self-type", `recursive dispatch refused: role binding "${subagentType}" equals Execute as "${executeAs}" (leaf executors must not re-invoke their own role)`, "complete the work in this session, or return Blocked to project-manager")
1061
1117
  ]
1062
1118
  };
1063
1119
  }
1064
1120
  return { ok: true, violations: [] };
1065
1121
  }
1066
- function isPlainObject2(value) {
1122
+ function isPlainObject3(value) {
1067
1123
  return typeof value === "object" && value !== null && !Array.isArray(value);
1068
1124
  }
1069
- function violation3(severity, code, message, fix) {
1125
+ function violation4(severity, code, message, fix) {
1070
1126
  return { ok: false, severity, code, message, fix };
1071
1127
  }
1072
1128
  function validateNonEmptyString2(violations, value, field, missingCode, invalidCode) {
1073
1129
  if (value === undefined) {
1074
- violations.push(violation3("high", missingCode, `missing required field: ${field}`));
1130
+ violations.push(violation4("high", missingCode, `missing required field: ${field}`));
1075
1131
  } else if (typeof value !== "string" || value.trim() === "") {
1076
- violations.push(violation3("medium", invalidCode, `${field} must be a non-empty string`));
1132
+ violations.push(violation4("medium", invalidCode, `${field} must be a non-empty string`));
1077
1133
  }
1078
1134
  }
1079
1135
  function validateWorkflowSnapshot(doc) {
1080
1136
  const violations = [];
1081
- if (!isPlainObject2(doc)) {
1137
+ if (!isPlainObject3(doc)) {
1082
1138
  return {
1083
1139
  ok: false,
1084
- violations: [violation3("high", "workflow.snapshot.invalid", "workflow snapshot must be an object")]
1140
+ violations: [violation4("high", "workflow.snapshot.invalid", "workflow snapshot must be an object")]
1085
1141
  };
1086
1142
  }
1087
1143
  if (doc.schema_version === undefined) {
1088
- violations.push(violation3("high", "workflow.snapshot.missing-schema-version", "missing required field: schema_version"));
1144
+ violations.push(violation4("high", "workflow.snapshot.missing-schema-version", "missing required field: schema_version"));
1089
1145
  } else if (doc.schema_version !== 1) {
1090
- violations.push(violation3("high", "workflow.snapshot.invalid-schema-version", `schema_version must be 1 — got ${JSON.stringify(doc.schema_version)} (version is reserved for the root file discriminator)`));
1146
+ violations.push(violation4("high", "workflow.snapshot.invalid-schema-version", `schema_version must be 1 — got ${JSON.stringify(doc.schema_version)} (version is reserved for the root file discriminator)`));
1091
1147
  }
1092
1148
  if (doc.version !== undefined) {
1093
- violations.push(violation3("medium", "workflow.snapshot.reserved-version", `top-level version is reserved for the root status.json discriminator — snapshots use schema_version; remove the version key (got ${JSON.stringify(doc.version)})`, "remove the version key from the snapshot"));
1149
+ violations.push(violation4("medium", "workflow.snapshot.reserved-version", `top-level version is reserved for the root status.json discriminator — snapshots use schema_version; remove the version key (got ${JSON.stringify(doc.version)})`, "remove the version key from the snapshot"));
1094
1150
  }
1095
1151
  validateNonEmptyString2(violations, doc.id, "id", "workflow.snapshot.missing-id", "workflow.snapshot.invalid-id");
1096
1152
  if (doc.type === undefined) {
1097
- violations.push(violation3("high", "workflow.snapshot.missing-type", "missing required field: type"));
1153
+ violations.push(violation4("high", "workflow.snapshot.missing-type", "missing required field: type"));
1098
1154
  } else if (typeof doc.type !== "string" || !WORKFLOW_LIFECYCLE_TYPES.includes(doc.type)) {
1099
- violations.push(violation3("medium", "workflow.snapshot.invalid-type", `type must be one of ${WORKFLOW_LIFECYCLE_TYPES.join(" | ")} — got ${JSON.stringify(doc.type)}`));
1155
+ violations.push(violation4("medium", "workflow.snapshot.invalid-type", `type must be one of ${WORKFLOW_LIFECYCLE_TYPES.join(" | ")} — got ${JSON.stringify(doc.type)}`));
1100
1156
  }
1101
1157
  if (doc.status === undefined) {
1102
- violations.push(violation3("high", "workflow.snapshot.missing-status", "missing required field: status"));
1158
+ violations.push(violation4("high", "workflow.snapshot.missing-status", "missing required field: status"));
1103
1159
  } else if (typeof doc.status !== "string" || !WORKFLOW_LIFECYCLE_STATUSES.includes(doc.status)) {
1104
- violations.push(violation3("medium", "workflow.snapshot.invalid-status", `status must be one of ${WORKFLOW_LIFECYCLE_STATUSES.join(" | ")} — got ${JSON.stringify(doc.status)}`));
1160
+ violations.push(violation4("medium", "workflow.snapshot.invalid-status", `status must be one of ${WORKFLOW_LIFECYCLE_STATUSES.join(" | ")} — got ${JSON.stringify(doc.status)}`));
1105
1161
  }
1106
1162
  validateNonEmptyString2(violations, doc.started_at, "started_at", "workflow.snapshot.missing-started-at", "workflow.snapshot.invalid-started-at");
1107
1163
  validateNonEmptyString2(violations, doc.updated_at, "updated_at", "workflow.snapshot.missing-updated-at", "workflow.snapshot.invalid-updated-at");
@@ -1109,35 +1165,35 @@ function validateWorkflowSnapshot(doc) {
1109
1165
  validateNonEmptyString2(violations, doc.ended_at, "ended_at", "workflow.snapshot.missing-ended-at", "workflow.snapshot.invalid-ended-at");
1110
1166
  }
1111
1167
  if (doc.phase !== undefined && typeof doc.phase !== "string") {
1112
- violations.push(violation3("medium", "workflow.snapshot.invalid-phase", "phase must be a string (free-form phase machine label)"));
1168
+ violations.push(violation4("medium", "workflow.snapshot.invalid-phase", "phase must be a string (free-form phase machine label)"));
1113
1169
  }
1114
1170
  if (doc.plans === undefined) {
1115
- violations.push(violation3("high", "workflow.snapshot.missing-plans", "missing required field: plans"));
1171
+ violations.push(violation4("high", "workflow.snapshot.missing-plans", "missing required field: plans"));
1116
1172
  } else if (!Array.isArray(doc.plans)) {
1117
- violations.push(violation3("high", "workflow.snapshot.invalid-plans", "plans must be an array of legacy plan rows"));
1173
+ violations.push(violation4("high", "workflow.snapshot.invalid-plans", "plans must be an array of legacy plan rows"));
1118
1174
  } else {
1119
1175
  for (const row of doc.plans) {
1120
1176
  violations.push(...validatePlanRow(row).violations);
1121
- if (isPlainObject2(row) && row.execution_lease !== undefined) {
1177
+ if (isPlainObject3(row) && row.execution_lease !== undefined) {
1122
1178
  violations.push(...validateExecutionLease(row.execution_lease).violations);
1123
1179
  }
1124
1180
  }
1125
1181
  }
1126
1182
  if (doc.execution_policy !== undefined) {
1127
- if (!isPlainObject2(doc.execution_policy)) {
1128
- violations.push(violation3("medium", "workflow.snapshot.invalid-execution-policy", "execution_policy must be an object"));
1183
+ if (!isPlainObject3(doc.execution_policy)) {
1184
+ violations.push(violation4("medium", "workflow.snapshot.invalid-execution-policy", "execution_policy must be an object"));
1129
1185
  }
1130
1186
  }
1131
1187
  if (doc.integration_merge_lease !== undefined) {
1132
1188
  violations.push(...validateIntegrationMergeLease(doc.integration_merge_lease).violations);
1133
1189
  }
1134
1190
  if (doc.branch !== undefined) {
1135
- if (!isPlainObject2(doc.branch)) {
1136
- violations.push(violation3("medium", "workflow.snapshot.invalid-branch", "branch must be an object"));
1191
+ if (!isPlainObject3(doc.branch)) {
1192
+ violations.push(violation4("medium", "workflow.snapshot.invalid-branch", "branch must be an object"));
1137
1193
  } else {
1138
1194
  for (const key of ["base", "integration", "target"]) {
1139
1195
  if (doc.branch[key] !== undefined && (typeof doc.branch[key] !== "string" || doc.branch[key].trim() === "")) {
1140
- violations.push(violation3("medium", "workflow.snapshot.invalid-branch", `branch.${key} must be a non-empty string`));
1196
+ violations.push(violation4("medium", "workflow.snapshot.invalid-branch", `branch.${key} must be a non-empty string`));
1141
1197
  }
1142
1198
  }
1143
1199
  }
@@ -1145,8 +1201,8 @@ function validateWorkflowSnapshot(doc) {
1145
1201
  if (doc.control_worktree_path !== undefined) {
1146
1202
  validateNonEmptyString2(violations, doc.control_worktree_path, "control_worktree_path", "workflow.snapshot.missing-control-worktree-path", "workflow.snapshot.invalid-control-worktree-path");
1147
1203
  }
1148
- if (doc.legacy_metadata !== undefined && !isPlainObject2(doc.legacy_metadata)) {
1149
- violations.push(violation3("medium", "workflow.snapshot.invalid-legacy-metadata", "legacy_metadata must be an object"));
1204
+ if (doc.legacy_metadata !== undefined && !isPlainObject3(doc.legacy_metadata)) {
1205
+ violations.push(violation4("medium", "workflow.snapshot.invalid-legacy-metadata", "legacy_metadata must be an object"));
1150
1206
  }
1151
1207
  if (doc.compass_ref !== undefined) {
1152
1208
  validateNonEmptyString2(violations, doc.compass_ref, "compass_ref", "workflow.snapshot.missing-compass-ref", "workflow.snapshot.invalid-compass-ref");
@@ -1154,17 +1210,17 @@ function validateWorkflowSnapshot(doc) {
1154
1210
  const terminal = typeof doc.status === "string" && WORKFLOW_TERMINAL_STATUSES.includes(doc.status);
1155
1211
  if (terminal) {
1156
1212
  if (doc.ended_at === undefined) {
1157
- violations.push(violation3("high", "workflow.snapshot.missing-ended-at", `terminal status ${JSON.stringify(doc.status)} requires ended_at — a terminal snapshot must record when the lifecycle ended`));
1213
+ violations.push(violation4("high", "workflow.snapshot.missing-ended-at", `terminal status ${JSON.stringify(doc.status)} requires ended_at — a terminal snapshot must record when the lifecycle ended`));
1158
1214
  }
1159
1215
  if (Array.isArray(doc.plans)) {
1160
1216
  for (const row of doc.plans) {
1161
- if (isPlainObject2(row) && row.execution_lease !== undefined) {
1162
- violations.push(violation3("high", "workflow.snapshot.terminal-dangling-execution-lease", `terminal snapshot must not carry a row execution_lease (dangling lease) — release every lease before the lifecycle ends`));
1217
+ if (isPlainObject3(row) && row.execution_lease !== undefined) {
1218
+ violations.push(violation4("high", "workflow.snapshot.terminal-dangling-execution-lease", `terminal snapshot must not carry a row execution_lease (dangling lease) — release every lease before the lifecycle ends`));
1163
1219
  }
1164
1220
  }
1165
1221
  }
1166
1222
  if (doc.integration_merge_lease !== undefined) {
1167
- violations.push(violation3("high", "workflow.snapshot.terminal-dangling-merge-lease", "terminal snapshot must not carry integration_merge_lease (dangling lease) — release the merge lease before the lifecycle ends"));
1223
+ violations.push(violation4("high", "workflow.snapshot.terminal-dangling-merge-lease", "terminal snapshot must not carry integration_merge_lease (dangling lease) — release the merge lease before the lifecycle ends"));
1168
1224
  }
1169
1225
  }
1170
1226
  return { ok: violations.length === 0, violations };
@@ -1176,15 +1232,15 @@ async function writeWorkflowSnapshot(snapshot, dir) {
1176
1232
  throw new Error(`refusing to write invalid workflow snapshot: ${detail}`);
1177
1233
  }
1178
1234
  const snapshotPath = join5(dir, WORKFLOW_SNAPSHOT_FILE);
1179
- mkdirSync4(dir, { recursive: true });
1235
+ mkdirSync3(dir, { recursive: true });
1180
1236
  await withStatusWriteLock(snapshotPath, () => {
1181
1237
  writeJson(snapshotPath, snapshot);
1182
1238
  });
1183
1239
  }
1184
- function isPlainObject3(value) {
1240
+ function isPlainObject4(value) {
1185
1241
  return typeof value === "object" && value !== null && !Array.isArray(value);
1186
1242
  }
1187
- function violation4(severity, code, message, fix) {
1243
+ function violation5(severity, code, message, fix) {
1188
1244
  return { ok: false, severity, code, message, fix };
1189
1245
  }
1190
1246
  function todayString() {
@@ -1207,19 +1263,19 @@ function isOpenResidual(entry) {
1207
1263
  }
1208
1264
  function validateNonEmptyString3(violations, value, field, missingCode, invalidCode) {
1209
1265
  if (value === undefined) {
1210
- violations.push(violation4("high", missingCode, `missing required field: ${field}`));
1266
+ violations.push(violation5("high", missingCode, `missing required field: ${field}`));
1211
1267
  } else if (typeof value !== "string" || value.trim() === "") {
1212
- violations.push(violation4("medium", invalidCode, `${field} must be a non-empty string`));
1268
+ violations.push(violation5("medium", invalidCode, `${field} must be a non-empty string`));
1213
1269
  }
1214
1270
  }
1215
1271
  function validatePlanRow(row) {
1216
1272
  const violations = [];
1217
- if (!isPlainObject3(row)) {
1218
- return { ok: false, violations: [violation4("high", "status.plan-row.invalid", "plan row must be an object")] };
1273
+ if (!isPlainObject4(row)) {
1274
+ return { ok: false, violations: [violation5("high", "status.plan-row.invalid", "plan row must be an object")] };
1219
1275
  }
1220
1276
  const { id, plan_id: planId, title, file, status, metadata, execution_lease } = row;
1221
1277
  if (id === undefined && planId === undefined) {
1222
- violations.push(violation4("high", "status.plan-row.missing-id", "missing required field: id (or legacy plan_id)"));
1278
+ violations.push(violation5("high", "status.plan-row.missing-id", "missing required field: id (or legacy plan_id)"));
1223
1279
  } else {
1224
1280
  if (id !== undefined) {
1225
1281
  validateNonEmptyString3(violations, id, "id", "status.plan-row.missing-id", "status.plan-row.invalid-id");
@@ -1228,31 +1284,31 @@ function validatePlanRow(row) {
1228
1284
  validateNonEmptyString3(violations, planId, "plan_id", "status.plan-row.missing-plan-id", "status.plan-row.invalid-plan-id");
1229
1285
  }
1230
1286
  if (id !== undefined && planId !== undefined && id !== planId) {
1231
- violations.push(violation4("medium", "status.plan-row.dual-id", "row has both id and plan_id with different values — write one canonical key (prefer id)"));
1287
+ violations.push(violation5("medium", "status.plan-row.dual-id", "row has both id and plan_id with different values — write one canonical key (prefer id)"));
1232
1288
  }
1233
1289
  }
1234
1290
  validateNonEmptyString3(violations, title, "title", "status.plan-row.missing-title", "status.plan-row.invalid-title");
1235
1291
  validateNonEmptyString3(violations, file, "file", "status.plan-row.missing-file", "status.plan-row.invalid-file");
1236
1292
  if (status === undefined) {
1237
- violations.push(violation4("high", "status.plan-row.missing-status", "missing required field: status"));
1293
+ violations.push(violation5("high", "status.plan-row.missing-status", "missing required field: status"));
1238
1294
  } else if (typeof status !== "string" || !PLAN_STATUSES.includes(status)) {
1239
- violations.push(violation4("medium", "status.plan-row.invalid-status", `status must be one of ${PLAN_STATUSES.join(" | ")} — got ${JSON.stringify(status)}`));
1295
+ violations.push(violation5("medium", "status.plan-row.invalid-status", `status must be one of ${PLAN_STATUSES.join(" | ")} — got ${JSON.stringify(status)}`));
1240
1296
  }
1241
- if (metadata !== undefined && !isPlainObject3(metadata)) {
1242
- violations.push(violation4("medium", "status.plan-row.invalid-metadata", "metadata must be an object"));
1297
+ if (metadata !== undefined && !isPlainObject4(metadata)) {
1298
+ violations.push(violation5("medium", "status.plan-row.invalid-metadata", "metadata must be an object"));
1243
1299
  }
1244
- if (execution_lease !== undefined && !isPlainObject3(execution_lease)) {
1245
- violations.push(violation4("medium", "status.plan-row.invalid-execution-lease", "execution_lease must be an object"));
1300
+ if (execution_lease !== undefined && !isPlainObject4(execution_lease)) {
1301
+ violations.push(violation5("medium", "status.plan-row.invalid-execution-lease", "execution_lease must be an object"));
1246
1302
  }
1247
1303
  if (status === "Done" && execution_lease !== undefined) {
1248
- violations.push(violation4("medium", "status.plan-row.done-with-lease", 'plan status Done must not carry an execution_lease — the Done authority deletes the lease in the same complete-file update as status: "Done" (status-and-residuals.md § Hold, release, and override)', 'delete plans[].execution_lease in the same update that sets status: "Done"'));
1304
+ violations.push(violation5("medium", "status.plan-row.done-with-lease", 'plan status Done must not carry an execution_lease — the Done authority deletes the lease in the same complete-file update as status: "Done" (status-and-residuals.md § Hold, release, and override)', 'delete plans[].execution_lease in the same update that sets status: "Done"'));
1249
1305
  }
1250
1306
  return { ok: violations.length === 0, violations };
1251
1307
  }
1252
1308
  function validateResidual(entry) {
1253
1309
  const violations = [];
1254
- if (!isPlainObject3(entry)) {
1255
- return { ok: false, violations: [violation4("high", "status.residual.invalid", "residual entry must be an object")] };
1310
+ if (!isPlainObject4(entry)) {
1311
+ return { ok: false, violations: [violation5("high", "status.residual.invalid", "residual entry must be an object")] };
1256
1312
  }
1257
1313
  const { id, title, severity, source, scope, decision, owner, target, tracking, detail_doc, lifecycle, closed_at } = entry;
1258
1314
  validateNonEmptyString3(violations, id, "id", "status.residual.missing-id", "status.residual.invalid-id");
@@ -1261,42 +1317,42 @@ function validateResidual(entry) {
1261
1317
  validateNonEmptyString3(violations, scope, "scope", "status.residual.missing-scope", "status.residual.invalid-scope");
1262
1318
  validateNonEmptyString3(violations, owner, "owner", "status.residual.missing-owner", "status.residual.invalid-owner");
1263
1319
  if (severity === undefined) {
1264
- violations.push(violation4("high", "status.residual.missing-severity", "missing required field: severity"));
1320
+ violations.push(violation5("high", "status.residual.missing-severity", "missing required field: severity"));
1265
1321
  } else if (typeof severity !== "string" || !SEVERITY_ORDER.includes(severity) && severity !== "warning") {
1266
- violations.push(violation4("medium", "status.residual.invalid-severity", `severity must be one of ${SEVERITY_ORDER.join(" | ")} — got ${JSON.stringify(severity)}`));
1322
+ violations.push(violation5("medium", "status.residual.invalid-severity", `severity must be one of ${SEVERITY_ORDER.join(" | ")} — got ${JSON.stringify(severity)}`));
1267
1323
  } else if (severity === "warning") {
1268
- violations.push(violation4("low", "status.residual.legacy-warning", `severity "warning" is legacy — forbidden on new entries; read paths normalize it to "low"`, `use "low" (normalizeSeverity maps 'warning' → 'low')`));
1324
+ violations.push(violation5("low", "status.residual.legacy-warning", `severity "warning" is legacy — forbidden on new entries; read paths normalize it to "low"`, `use "low" (normalizeSeverity maps 'warning' → 'low')`));
1269
1325
  }
1270
1326
  if (decision === undefined) {
1271
- violations.push(violation4("high", "status.residual.missing-decision", "missing required field: decision"));
1327
+ violations.push(violation5("high", "status.residual.missing-decision", "missing required field: decision"));
1272
1328
  } else if (typeof decision !== "string" || !RESIDUAL_DECISIONS.includes(decision)) {
1273
- violations.push(violation4("medium", "status.residual.invalid-decision", `decision must be one of ${RESIDUAL_DECISIONS.join(" | ")} — got ${JSON.stringify(decision)}`));
1329
+ violations.push(violation5("medium", "status.residual.invalid-decision", `decision must be one of ${RESIDUAL_DECISIONS.join(" | ")} — got ${JSON.stringify(decision)}`));
1274
1330
  }
1275
1331
  if (target === undefined) {
1276
- violations.push(violation4("high", "status.residual.missing-target", "missing required field: target"));
1332
+ violations.push(violation5("high", "status.residual.missing-target", "missing required field: target"));
1277
1333
  } else if (typeof target !== "string" && target !== null) {
1278
- violations.push(violation4("medium", "status.residual.invalid-target", "target must be a string or null"));
1334
+ violations.push(violation5("medium", "status.residual.invalid-target", "target must be a string or null"));
1279
1335
  }
1280
1336
  if (tracking === undefined) {
1281
- violations.push(violation4("high", "status.residual.missing-tracking", "missing required field: tracking"));
1337
+ violations.push(violation5("high", "status.residual.missing-tracking", "missing required field: tracking"));
1282
1338
  } else if (typeof tracking !== "string" && tracking !== null) {
1283
- violations.push(violation4("medium", "status.residual.invalid-tracking", "tracking must be a string or null"));
1339
+ violations.push(violation5("medium", "status.residual.invalid-tracking", "tracking must be a string or null"));
1284
1340
  }
1285
1341
  if (detail_doc !== undefined && typeof detail_doc !== "string" && detail_doc !== null) {
1286
- violations.push(violation4("medium", "status.residual.invalid-detail-doc", "detail_doc must be a string or null"));
1342
+ violations.push(violation5("medium", "status.residual.invalid-detail-doc", "detail_doc must be a string or null"));
1287
1343
  }
1288
- if (closed_at !== undefined && (typeof closed_at !== "string" || !DATE_RE.test(closed_at))) {
1289
- violations.push(violation4("medium", "status.residual.invalid-closed-at", "closed_at must be YYYY-MM-DD"));
1344
+ if (closed_at !== undefined && (typeof closed_at !== "string" || !DATE_RE2.test(closed_at))) {
1345
+ violations.push(violation5("medium", "status.residual.invalid-closed-at", "closed_at must be YYYY-MM-DD"));
1290
1346
  }
1291
1347
  if (lifecycle !== undefined) {
1292
1348
  if (typeof lifecycle !== "string" || !RESIDUAL_LIFECYCLES.includes(lifecycle)) {
1293
- violations.push(violation4("medium", "status.residual.invalid-lifecycle", `lifecycle must be one of ${RESIDUAL_LIFECYCLES.join(" | ")} — got ${JSON.stringify(lifecycle)}`));
1349
+ violations.push(violation5("medium", "status.residual.invalid-lifecycle", `lifecycle must be one of ${RESIDUAL_LIFECYCLES.join(" | ")} — got ${JSON.stringify(lifecycle)}`));
1294
1350
  } else if (lifecycle !== "open") {
1295
1351
  if (closed_at === undefined) {
1296
- violations.push(violation4("high", "status.residual.closed-missing-closed-at", `lifecycle "${lifecycle}" requires closed_at (YYYY-MM-DD)`, 'set closed_at (e.g. "2026-08-08")'));
1352
+ violations.push(violation5("high", "status.residual.closed-missing-closed-at", `lifecycle "${lifecycle}" requires closed_at (YYYY-MM-DD)`, 'set closed_at (e.g. "2026-08-08")'));
1297
1353
  }
1298
1354
  if (entry.closure_note === undefined) {
1299
- violations.push(violation4("medium", "status.residual.closed-missing-closure-note", `lifecycle "${lifecycle}" requires closure_note (what changed; how verified)`, "add closure_note explaining the close"));
1355
+ violations.push(violation5("medium", "status.residual.closed-missing-closure-note", `lifecycle "${lifecycle}" requires closure_note (what changed; how verified)`, "add closure_note explaining the close"));
1300
1356
  }
1301
1357
  }
1302
1358
  }
@@ -1311,25 +1367,25 @@ function isHarnessRelativePath(dir) {
1311
1367
  }
1312
1368
  function validateWorkflowEntry(entry) {
1313
1369
  const violations = [];
1314
- if (!isPlainObject3(entry)) {
1370
+ if (!isPlainObject4(entry)) {
1315
1371
  return {
1316
1372
  ok: false,
1317
- violations: [violation4("high", "status.workflow.invalid", "workflow entry must be an object")]
1373
+ violations: [violation5("high", "status.workflow.invalid", "workflow entry must be an object")]
1318
1374
  };
1319
1375
  }
1320
1376
  validateNonEmptyString3(violations, entry.id, "id", "status.workflow.missing-id", "status.workflow.invalid-id");
1321
1377
  if (entry.type === undefined) {
1322
- violations.push(violation4("high", "status.workflow.missing-type", "missing required field: type"));
1378
+ violations.push(violation5("high", "status.workflow.missing-type", "missing required field: type"));
1323
1379
  } else if (typeof entry.type !== "string" || !WORKFLOW_LIFECYCLE_TYPES.includes(entry.type)) {
1324
- violations.push(violation4("medium", "status.workflow.invalid-type", `type must be one of ${WORKFLOW_LIFECYCLE_TYPES.join(" | ")} — got ${JSON.stringify(entry.type)}`));
1380
+ violations.push(violation5("medium", "status.workflow.invalid-type", `type must be one of ${WORKFLOW_LIFECYCLE_TYPES.join(" | ")} — got ${JSON.stringify(entry.type)}`));
1325
1381
  }
1326
1382
  validateNonEmptyString3(violations, entry.started_at, "started_at", "status.workflow.missing-started-at", "status.workflow.invalid-started-at");
1327
1383
  if (entry.dir === undefined) {
1328
- violations.push(violation4("high", "status.workflow.missing-dir", "missing required field: dir"));
1384
+ violations.push(violation5("high", "status.workflow.missing-dir", "missing required field: dir"));
1329
1385
  } else if (typeof entry.dir !== "string" || entry.dir.trim() === "") {
1330
- violations.push(violation4("medium", "status.workflow.invalid-dir", "dir must be a non-empty string"));
1386
+ violations.push(violation5("medium", "status.workflow.invalid-dir", "dir must be a non-empty string"));
1331
1387
  } else if (!isHarnessRelativePath(entry.dir)) {
1332
- violations.push(violation4("medium", "status.workflow.invalid-dir", `dir must be a harness-relative path (no absolute paths, no ".." segments) — got ${JSON.stringify(entry.dir)}`));
1388
+ violations.push(violation5("medium", "status.workflow.invalid-dir", `dir must be a harness-relative path (no absolute paths, no ".." segments) — got ${JSON.stringify(entry.dir)}`));
1333
1389
  }
1334
1390
  return { ok: violations.length === 0, violations };
1335
1391
  }
@@ -1339,24 +1395,24 @@ function validateStatusV2(docOrPath, opts = {}) {
1339
1395
  if (typeof docOrPath === "string") {
1340
1396
  try {
1341
1397
  doc = readJson(docOrPath);
1342
- harnessDir = dirname5(resolve5(docOrPath));
1398
+ harnessDir = dirname4(resolve4(docOrPath));
1343
1399
  } catch (error) {
1344
1400
  return {
1345
1401
  ok: false,
1346
- violations: [violation4("high", "status.invalid-json", error.message)]
1402
+ violations: [violation5("high", "status.invalid-json", error.message)]
1347
1403
  };
1348
1404
  }
1349
1405
  } else {
1350
1406
  doc = docOrPath;
1351
1407
  }
1352
- if (!isPlainObject3(doc)) {
1353
- return { ok: false, violations: [violation4("high", "status.invalid-doc", "status document must be an object")] };
1408
+ if (!isPlainObject4(doc)) {
1409
+ return { ok: false, violations: [violation5("high", "status.invalid-doc", "status document must be an object")] };
1354
1410
  }
1355
1411
  if (doc.version !== 2) {
1356
1412
  return {
1357
1413
  ok: false,
1358
1414
  violations: [
1359
- violation4("high", "status.migration-required", `status.json schema version 2 required — got ${JSON.stringify(doc.version)} (v1 or unknown version); run \`mstar migrate\` to convert the tree`, "run `mstar migrate`")
1415
+ violation5("high", "status.migration-required", `status.json schema version 2 required — got ${JSON.stringify(doc.version)} (v1 or unknown version); run \`mstar migrate\` to convert the tree`, "run `mstar migrate`")
1360
1416
  ]
1361
1417
  };
1362
1418
  }
@@ -1364,7 +1420,7 @@ function validateStatusV2(docOrPath, opts = {}) {
1364
1420
  return {
1365
1421
  ok: false,
1366
1422
  violations: [
1367
- violation4("high", "status.migration-required", "v1-shaped status.json (root plans[]) is not a v2 document — run `mstar migrate` to convert the tree", "run `mstar migrate`")
1423
+ violation5("high", "status.migration-required", "v1-shaped status.json (root plans[]) is not a v2 document — run `mstar migrate` to convert the tree", "run `mstar migrate`")
1368
1424
  ]
1369
1425
  };
1370
1426
  }
@@ -1372,27 +1428,27 @@ function validateStatusV2(docOrPath, opts = {}) {
1372
1428
  return {
1373
1429
  ok: false,
1374
1430
  violations: [
1375
- violation4("high", "status.migration-required", "v1-shaped status.json (root residual_findings) is not a v2 document — run `mstar migrate` to convert the tree", "run `mstar migrate`")
1431
+ violation5("high", "status.migration-required", "v1-shaped status.json (root residual_findings) is not a v2 document — run `mstar migrate` to convert the tree", "run `mstar migrate`")
1376
1432
  ]
1377
1433
  };
1378
1434
  }
1379
1435
  const violations = [];
1380
1436
  if (doc.updated_at === undefined) {
1381
- violations.push(violation4("high", "status.missing-updated-at", "missing required field: updated_at"));
1382
- } else if (typeof doc.updated_at !== "string" || !DATE_RE.test(doc.updated_at)) {
1383
- violations.push(violation4("medium", "status.invalid-updated-at", "updated_at must be YYYY-MM-DD"));
1437
+ violations.push(violation5("high", "status.missing-updated-at", "missing required field: updated_at"));
1438
+ } else if (typeof doc.updated_at !== "string" || !DATE_RE2.test(doc.updated_at)) {
1439
+ violations.push(violation5("medium", "status.invalid-updated-at", "updated_at must be YYYY-MM-DD"));
1384
1440
  }
1385
1441
  if (doc.workflows === undefined) {
1386
- violations.push(violation4("high", "status.missing-workflows", "missing required field: workflows"));
1442
+ violations.push(violation5("high", "status.missing-workflows", "missing required field: workflows"));
1387
1443
  } else if (!Array.isArray(doc.workflows)) {
1388
- violations.push(violation4("high", "status.invalid-workflows", "workflows must be an array"));
1444
+ violations.push(violation5("high", "status.invalid-workflows", "workflows must be an array"));
1389
1445
  } else {
1390
1446
  const seen = new Set;
1391
1447
  for (const entry of doc.workflows) {
1392
1448
  violations.push(...validateWorkflowEntry(entry).violations);
1393
- if (isPlainObject3(entry) && typeof entry.id === "string") {
1449
+ if (isPlainObject4(entry) && typeof entry.id === "string") {
1394
1450
  if (seen.has(entry.id)) {
1395
- violations.push(violation4("medium", "status.workflow.duplicate-id", `duplicate workflow id in workflows[]: ${JSON.stringify(entry.id)}`));
1451
+ violations.push(violation5("medium", "status.workflow.duplicate-id", `duplicate workflow id in workflows[]: ${JSON.stringify(entry.id)}`));
1396
1452
  }
1397
1453
  seen.add(entry.id);
1398
1454
  }
@@ -1401,47 +1457,47 @@ function validateStatusV2(docOrPath, opts = {}) {
1401
1457
  if (harnessDir !== undefined && Array.isArray(doc.workflows)) {
1402
1458
  let realHarnessDir = null;
1403
1459
  try {
1404
- realHarnessDir = realpathSync2(harnessDir);
1460
+ realHarnessDir = realpathSync(harnessDir);
1405
1461
  } catch {}
1406
1462
  for (const entry of doc.workflows) {
1407
- if (!isPlainObject3(entry) || typeof entry.dir !== "string")
1463
+ if (!isPlainObject4(entry) || typeof entry.dir !== "string")
1408
1464
  continue;
1409
1465
  const relSnapshot = join6(entry.dir, WORKFLOW_SNAPSHOT_FILE);
1410
1466
  const snapshotPath = join6(harnessDir, relSnapshot);
1411
1467
  const label = typeof entry.id === "string" ? entry.id : relSnapshot;
1412
1468
  let physical;
1413
1469
  try {
1414
- physical = realpathSync2(snapshotPath);
1470
+ physical = realpathSync(snapshotPath);
1415
1471
  } catch {
1416
- violations.push(violation4("high", "status.workflow.snapshot-missing", `workflows[] lists ${JSON.stringify(label)} but its snapshot does not exist at ${JSON.stringify(relSnapshot)} — the root holds active lifecycles only; unregister the id when its snapshot is removed`));
1472
+ violations.push(violation5("high", "status.workflow.snapshot-missing", `workflows[] lists ${JSON.stringify(label)} but its snapshot does not exist at ${JSON.stringify(relSnapshot)} — the root holds active lifecycles only; unregister the id when its snapshot is removed`));
1417
1473
  continue;
1418
1474
  }
1419
1475
  if (realHarnessDir !== null && physical !== realHarnessDir && !physical.startsWith(`${realHarnessDir}${sep}`)) {
1420
- 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)}) — symlinked snapshot paths are rejected; the snapshot must physically live under ${JSON.stringify(harnessDir)}`));
1476
+ 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)}) — symlinked snapshot paths are rejected; the snapshot must physically live under ${JSON.stringify(harnessDir)}`));
1421
1477
  continue;
1422
1478
  }
1423
1479
  let snapshot;
1424
1480
  try {
1425
1481
  snapshot = readJson(snapshotPath);
1426
1482
  } catch (error) {
1427
- violations.push(violation4("high", "status.workflow.snapshot-invalid", `snapshot at ${JSON.stringify(relSnapshot)} is not valid JSON: ${error.message}`));
1483
+ violations.push(violation5("high", "status.workflow.snapshot-invalid", `snapshot at ${JSON.stringify(relSnapshot)} is not valid JSON: ${error.message}`));
1428
1484
  continue;
1429
1485
  }
1430
1486
  if (typeof snapshot.status === "string" && WORKFLOW_TERMINAL_STATUSES.includes(snapshot.status)) {
1431
- violations.push(violation4("high", "status.workflow.terminal-listed", `workflows[] lists ${JSON.stringify(label)} whose snapshot status is terminal (${snapshot.status}) — removal-at-terminal: terminal writers unregister AFTER the snapshot write`));
1487
+ violations.push(violation5("high", "status.workflow.terminal-listed", `workflows[] lists ${JSON.stringify(label)} whose snapshot status is terminal (${snapshot.status}) — removal-at-terminal: terminal writers unregister AFTER the snapshot write`));
1432
1488
  }
1433
1489
  if (typeof entry.type === "string" && typeof snapshot.type === "string" && entry.type !== snapshot.type) {
1434
- 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)} — the root entry mirrors the snapshot; align them`));
1490
+ 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)} — the root entry mirrors the snapshot; align them`));
1435
1491
  }
1436
1492
  if (typeof entry.started_at === "string" && typeof snapshot.started_at === "string" && entry.started_at !== snapshot.started_at) {
1437
- 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)} — 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 — align them or remove the colliding workflow`));
1493
+ 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)} — 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 — align them or remove the colliding workflow`));
1438
1494
  }
1439
1495
  }
1440
1496
  }
1441
1497
  return { ok: violations.length === 0, violations };
1442
1498
  }
1443
1499
  function registerWorkflowEntryLocked(statusPath, entry) {
1444
- const harnessDir = dirname5(statusPath);
1500
+ const harnessDir = dirname4(statusPath);
1445
1501
  const current = readJson(statusPath);
1446
1502
  const fresh = Object.keys(current).length === 0;
1447
1503
  const doc = fresh ? { version: 2, updated_at: todayString(), workflows: [] } : current;
@@ -1467,15 +1523,15 @@ async function registerWorkflow(root, entry) {
1467
1523
  if (!entryGate.ok) {
1468
1524
  throw new Error(`refusing to register invalid workflow entry: ${entryGate.violations.map((v) => v.message).join("; ")}`);
1469
1525
  }
1470
- const statusPath = resolve5(root);
1526
+ const statusPath = resolve4(root);
1471
1527
  return withStatusWriteLock(statusPath, () => registerWorkflowEntryLocked(statusPath, entry));
1472
1528
  }
1473
1529
  async function unregisterWorkflow(root, id) {
1474
1530
  if (typeof id !== "string" || id.trim() === "") {
1475
1531
  throw new Error("refusing to unregister workflow: id must be a non-empty string");
1476
1532
  }
1477
- const statusPath = resolve5(root);
1478
- const harnessDir = dirname5(statusPath);
1533
+ const statusPath = resolve4(root);
1534
+ const harnessDir = dirname4(statusPath);
1479
1535
  return withStatusWriteLock(statusPath, () => {
1480
1536
  const current = readJson(statusPath);
1481
1537
  if (Object.keys(current).length === 0) {
@@ -1532,8 +1588,8 @@ function resolveCompassEnforcement(harnessDir) {
1532
1588
  return { hard: false, source: "none" };
1533
1589
  }
1534
1590
  function resolveMstarcEnforcement(harnessDir) {
1535
- const dir = resolve5(harnessDir);
1536
- const rc = loadMstarc(dir, dirname5(dir));
1591
+ const dir = resolve4(harnessDir);
1592
+ const rc = loadMstarc(dir, dirname4(dir));
1537
1593
  const value = rc?.config.enforcement;
1538
1594
  if (value === "hard")
1539
1595
  return { hard: true, source: "mstarc" };
@@ -1547,980 +1603,946 @@ function resolveRepoEnforcement(harnessDir) {
1547
1603
  return rc;
1548
1604
  return resolveCompassEnforcement(harnessDir);
1549
1605
  }
1550
- function probeTimeoutMs() {
1551
- const raw = process.env.MSTAR_GIT_PROBE_TIMEOUT_MS;
1552
- if (raw === undefined || raw.trim() === "")
1553
- return DEFAULT_PROBE_TIMEOUT_MS;
1554
- const parsed = Number(raw);
1555
- return Number.isFinite(parsed) && parsed > 0 ? parsed : DEFAULT_PROBE_TIMEOUT_MS;
1606
+ function isPlainObject5(value) {
1607
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1556
1608
  }
1557
- function violation5(severity, code, message, fix) {
1609
+ function violation6(severity, code, message, fix) {
1558
1610
  return { ok: false, severity, code, message, fix };
1559
1611
  }
1560
- function gate(violations) {
1561
- return { ok: violations.length === 0, violations };
1562
- }
1563
- function probeBranch(worktreePath, opts) {
1564
- const precomputed = opts.branchOf?.(worktreePath);
1565
- if (precomputed !== undefined)
1566
- return { branch: precomputed };
1567
- const timeout = opts.timeoutMs ?? probeTimeoutMs();
1568
- try {
1569
- const stdout = execFileSync2(opts.gitPath ?? "git", ["-C", worktreePath, "branch", "--show-current"], {
1570
- encoding: "utf8",
1571
- stdio: ["ignore", "pipe", "pipe"],
1572
- timeout
1573
- });
1574
- const branch = stdout.trim();
1575
- if (branch === "")
1576
- return { error: `no branch checked out (detached HEAD?) at "${worktreePath}"` };
1577
- return { branch };
1578
- } catch (err) {
1579
- const e = err;
1580
- if (e.killed === true || e.signal !== undefined) {
1581
- return { error: `git probe timed out after ${timeout}ms (killed by ${e.signal ?? "SIGTERM"})` };
1582
- }
1583
- const detail = (e.stderr !== undefined ? e.stderr.toString().trim() : "") || e.message || "git probe failed";
1584
- return { error: detail };
1585
- }
1586
- }
1587
- function l1PreDispatchCheck(input, opts = {}) {
1588
- const violations = [];
1589
- const { controlWorktreePath, leaseWorktreePath, leaseWorkingBranch, planId } = input;
1590
- if (controlWorktreePath.trim() === "") {
1591
- violations.push(violation5("high", "worktree.l1.control-missing", "metadata.control_worktree_path is not recorded — 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"));
1592
- }
1593
- if (leaseWorktreePath.trim() === "") {
1594
- violations.push(violation5("high", "worktree.l1.lease-missing", `execution_lease.worktree_path is empty for plan "${planId}" — no verified execution_lease to dispatch against`, "claim the execution_lease with an absolute feature worktree path before dispatch"));
1595
- }
1596
- if (leaseWorkingBranch.trim() === "") {
1597
- 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"));
1598
- }
1599
- if (controlWorktreePath !== "" && leaseWorktreePath !== "" && resolve6(controlWorktreePath) === resolve6(leaseWorktreePath)) {
1600
- violations.push(violation5("critical", "worktree.l1.lease-equals-control", `execution_lease.worktree_path "${leaseWorktreePath}" equals metadata.control_worktree_path — 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"));
1601
- }
1602
- if (leaseWorktreePath !== "" && !existsSync4(leaseWorktreePath)) {
1603
- 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>`));
1604
- } else if (leaseWorktreePath !== "" && leaseWorkingBranch !== "") {
1605
- const probe = probeBranch(leaseWorktreePath, opts);
1606
- if ("error" in probe) {
1607
- 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)"));
1608
- } else if (probe.branch !== leaseWorkingBranch) {
1609
- 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`));
1610
- }
1611
- }
1612
- return gate(violations);
1613
- }
1614
- function l2PreDispatchCheck(input, opts = {}) {
1615
- const violations = [];
1616
- const tracks = input.tracks ?? [];
1617
- const seenPaths = new Set;
1618
- if (tracks.length < 1) {
1619
- violations.push(violation5("high", "worktree.l2.no-tracks", "no parallel writable tracks — 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"));
1620
- }
1621
- tracks.forEach((track, index) => {
1622
- if (track.worktreePath.trim() === "" || track.workingBranch.trim() === "") {
1623
- violations.push(violation5("high", "worktree.l2.track-invalid", `track ${index + 1} is missing worktreePath and/or workingBranch`, "fill both fields for every track"));
1624
- return;
1625
- }
1626
- if (!isAbsolute4(track.worktreePath)) {
1627
- violations.push(violation5("high", "worktree.l2.track-path-relative", `track ${index + 1} worktreePath "${track.worktreePath}" is not an absolute path — L2 tracks MUST use absolute worktree checkout paths (consistent with the lease validator's absolute worktree_path enforcement)`, `use an absolute path for track ${index + 1} (e.g. /Users/<you>/worktrees/<branch>)`));
1628
- return;
1629
- }
1630
- const normalized = resolve6(track.worktreePath);
1631
- if (seenPaths.has(normalized)) {
1632
- violations.push(violation5("high", "worktree.l2.track-path-collision", `duplicate worktreePath "${track.worktreePath}" across parallel tracks — L2 parallel-writable isolation requires a distinct absolute Worktree path per track (N parallel invokes ≠ isolation)`, "give every parallel track its own git worktree checkout"));
1633
- return;
1634
- }
1635
- seenPaths.add(normalized);
1636
- if (!existsSync4(track.worktreePath)) {
1637
- 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}`));
1638
- return;
1639
- }
1640
- const probe = probeBranch(track.worktreePath, opts);
1641
- if ("error" in probe) {
1642
- 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)"));
1643
- } else if (probe.branch !== track.workingBranch) {
1644
- 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`));
1645
- }
1646
- });
1647
- return gate(violations);
1648
- }
1649
- function assertControlVsFeaturePath(controlWorktreePath, featureWorktreePath) {
1650
- const violations = [];
1651
- const samePath = controlWorktreePath === "" && featureWorktreePath === "" || controlWorktreePath !== "" && featureWorktreePath !== "" && resolve6(controlWorktreePath) === resolve6(featureWorktreePath);
1652
- if (samePath) {
1653
- violations.push(violation5("critical", "worktree.control-feature.same", `control worktree path equals feature/lease worktree path "${controlWorktreePath}" — execution_lease.worktree_path MUST differ from metadata.control_worktree_path`, "use a distinct feature worktree for the plan's product edits"));
1654
- }
1655
- return gate(violations);
1656
- }
1657
- function assertBranchAlignment(worktreePath, expectedBranch, opts = {}) {
1658
- const violations = [];
1659
- const probe = probeBranch(worktreePath, opts);
1660
- if ("error" in probe) {
1661
- violations.push(violation5("high", "worktree.branch-probe-failed", `cannot probe branch at "${worktreePath}": ${probe.error}`, "verify the path is a git worktree checkout on the expected branch (not detached)"));
1662
- } else if (probe.branch !== expectedBranch) {
1663
- violations.push(violation5("high", "worktree.branch-mismatch", `worktree "${worktreePath}" is on branch "${probe.branch}", expected "${expectedBranch}" (Assignment Working branch)`, `checkout ${expectedBranch} in that worktree`));
1664
- }
1665
- return gate(violations);
1666
- }
1667
- function assertQcAlignment(assignments) {
1668
- const violations = [];
1669
- const list = assignments ?? [];
1670
- for (const { key, label } of QC_ALIGNMENT_FIELDS) {
1671
- const distinct = [...new Set(list.map((a) => a[key]))];
1672
- if (distinct.length > 1) {
1673
- 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`));
1674
- }
1675
- }
1676
- return gate(violations);
1677
- }
1678
- function singleReviewSnapshot(assignments) {
1679
- const violations = [];
1680
- const list = assignments ?? [];
1681
- list.forEach((a, index) => {
1682
- if ((a.head ?? "").trim() === "") {
1683
- violations.push(violation5("high", "qc.alignment.snapshot-missing", `review head not provided for assignment ${index + 1} (plan_id "${a.planId}") — cannot confirm the single review snapshot precondition`, "precompute and pass the review HEAD (full SHA) for every assignment"));
1684
- }
1685
- });
1686
- const distinct = [...new Set(list.map((a) => a.head ?? "").filter((h) => h.trim() !== ""))];
1687
- if (distinct.length > 1) {
1688
- violations.push(violation5("high", "qc.alignment.single-snapshot", `assignments cover ${distinct.length} different review heads (${distinct.join(", ")}) — all reviewable commits must sit on ONE Working branch HEAD before QC tri + QA`, "merge the parallel tracks to a single Working branch HEAD, then re-derive the heads"));
1689
- }
1690
- return gate(violations);
1691
- }
1692
- function isDirectory2(dir) {
1693
- try {
1694
- return statSync4(dir).isDirectory();
1695
- } catch {
1696
- return false;
1697
- }
1698
- }
1699
- function isFile2(file) {
1700
- try {
1701
- return statSync4(file).isFile();
1702
- } catch {
1703
- return false;
1704
- }
1705
- }
1706
- function gitOut(cwd, args) {
1707
- try {
1708
- return execFileSync3("git", args, {
1709
- cwd,
1710
- encoding: "utf8",
1711
- stdio: ["ignore", "pipe", "pipe"],
1712
- maxBuffer: GIT_CAPTURE_MAX_BYTES
1713
- }).trim();
1714
- } catch {
1715
- return null;
1716
- }
1717
- }
1718
- function probeHarnessWithStatus(root) {
1719
- if (isFile2(join7(root, ".mstar", "status.json")))
1720
- return join7(root, ".mstar");
1721
- if (isFile2(join7(root, ".agents", "status.json")))
1722
- return join7(root, ".agents");
1723
- if (hasWorkflowSnapshot(join7(root, ".mstar")))
1724
- return join7(root, ".mstar");
1725
- if (hasWorkflowSnapshot(join7(root, ".agents")))
1726
- return join7(root, ".agents");
1727
- return null;
1728
- }
1729
- function hasWorkflowSnapshot(harnessDir) {
1730
- let workflowsDir;
1731
- try {
1732
- workflowsDir = resolveWorkflowDir(harnessDir, { harnessDir });
1733
- } catch {
1734
- workflowsDir = join7(harnessDir, "workflows");
1735
- }
1736
- if (!isDirectory2(workflowsDir))
1737
- return false;
1738
- try {
1739
- for (const entry of readdirSync3(workflowsDir, { withFileTypes: true })) {
1740
- if (entry.isDirectory() && isFile2(join7(workflowsDir, entry.name, "snapshot.json")))
1741
- return true;
1742
- }
1743
- } catch {
1744
- return false;
1745
- }
1746
- return false;
1747
- }
1748
- function isLinkedWorktree(root) {
1749
- const gitDirRaw = gitOut(root, ["rev-parse", "--git-dir"]);
1750
- const commonRaw = gitOut(root, ["rev-parse", "--git-common-dir"]);
1751
- if (gitDirRaw === null || commonRaw === null)
1752
- return false;
1753
- const gitDir = isAbsolute5(gitDirRaw) ? gitDirRaw : join7(root, gitDirRaw);
1754
- const common = isAbsolute5(commonRaw) ? commonRaw : join7(root, commonRaw);
1755
- if (gitDir.includes("/.git/worktrees/") || gitDir.includes("/worktrees/"))
1756
- return true;
1612
+ function validateNonEmptyString4(violations, value, field, missingCode, invalidCode) {
1613
+ if (value === undefined) {
1614
+ violations.push(violation6("high", missingCode, `missing required field: ${field}`));
1615
+ } else if (typeof value !== "string" || value.trim() === "") {
1616
+ violations.push(violation6("medium", invalidCode, `${field} must be a non-empty string`));
1617
+ }
1618
+ }
1619
+ function validateRoadmap(filePath) {
1620
+ const violations = [];
1621
+ let content;
1757
1622
  try {
1758
- const gdParent = realpathSync3(dirname6(gitDir));
1759
- const cmAbs = realpathSync3(common);
1760
- return join7(gdParent, basename3(gitDir)) !== cmAbs && gitDir !== cmAbs;
1623
+ content = readFileSync5(filePath, "utf8");
1761
1624
  } catch {
1762
- return false;
1625
+ return {
1626
+ ok: false,
1627
+ violations: [violation6("high", "project.roadmap.unreadable", `cannot read roadmap file: ${filePath}`)],
1628
+ warnings: []
1629
+ };
1763
1630
  }
1764
- }
1765
- function sddWorkspace(planId, opts = {}) {
1766
- if (!planId) {
1767
- throw new SddScriptError(`usage: mstar sdd workspace PLAN_ID [CONTROL_ROOT]
1768
- ` + " Set MSTAR_CONTROL_ROOT=<control_worktree_path> when running from a feature worktree.", 2);
1631
+ let doc;
1632
+ try {
1633
+ doc = parseCompassFrontmatterText(content, filePath);
1634
+ } catch (err) {
1635
+ const message = err instanceof Error ? err.message : `invalid roadmap frontmatter in ${filePath}`;
1636
+ return { ok: false, violations: [violation6("high", "project.roadmap.invalid-frontmatter", message)], warnings: [] };
1769
1637
  }
1770
- const cwd = opts.cwd ?? process.cwd();
1771
- const controlRoot = opts.controlRoot ?? (process.env.MSTAR_CONTROL_ROOT || undefined);
1772
- let root;
1773
- if (controlRoot) {
1774
- if (!isDirectory2(controlRoot)) {
1775
- throw new SddScriptError(`mstar sdd workspace: CONTROL_ROOT / MSTAR_CONTROL_ROOT is not a directory: ${controlRoot}`, 1);
1776
- }
1777
- root = realpathSync3(controlRoot);
1778
- } else {
1779
- const topLevel = gitOut(cwd, ["rev-parse", "--show-toplevel"]);
1780
- root = realpathSync3(topLevel ?? cwd);
1638
+ validateNonEmptyString4(violations, doc.project_id, "project_id", "project.roadmap.missing-project-id", "project.roadmap.invalid-project-id");
1639
+ validateNonEmptyString4(violations, doc.title, "title", "project.roadmap.missing-title", "project.roadmap.invalid-title");
1640
+ if (doc.status === undefined) {
1641
+ violations.push(violation6("high", "project.roadmap.missing-status", "missing required field: status"));
1642
+ } else if (typeof doc.status !== "string" || !ROADMAP_STATUSES.includes(doc.status)) {
1643
+ violations.push(violation6("medium", "project.roadmap.invalid-status", `status must be one of ${ROADMAP_STATUSES.join(" | ")} got ${JSON.stringify(doc.status)}`));
1781
1644
  }
1782
- if (!controlRoot && isLinkedWorktree(root)) {
1783
- throw new SddScriptError(`mstar sdd workspace: linked worktree at ${root} has no {HARNESS_DIR}/status.json (default gitignore).
1784
- ` + ` Refusing to create a second SDD tree under the feature checkout.
1785
- ` + ` Re-run with MSTAR_CONTROL_ROOT=<control_worktree_path> or: mstar sdd workspace ${planId} <control_worktree_path>
1786
- ` + ` See mstar-branch-worktree «Harness path SSOT under default gitignore».`, 1);
1645
+ if (doc.created_at === undefined) {
1646
+ violations.push(violation6("high", "project.roadmap.missing-created-at", "missing required field: created_at"));
1647
+ } else if (typeof doc.created_at !== "string" || !DATE_RE3.test(doc.created_at)) {
1648
+ violations.push(violation6("medium", "project.roadmap.invalid-created-at", "created_at must be YYYY-MM-DD"));
1787
1649
  }
1788
- const harnessOverride = opts.harnessDir ?? (process.env.MSTAR_HARNESS_DIR || undefined);
1789
- let harnessDir;
1790
- if (harnessOverride) {
1791
- harnessDir = resolve7(root, harnessOverride);
1792
- } else {
1793
- const rc = findMstarc(root, root);
1794
- const rcHarnessDir = rc !== null ? parseMstarc(readFileSync5(rc, "utf8")).harnessDir : undefined;
1795
- if (rcHarnessDir) {
1796
- harnessDir = resolve7(rc !== null ? dirname6(rc) : root, rcHarnessDir);
1650
+ if (doc.milestones !== undefined && doc.milestones !== null) {
1651
+ if (!Array.isArray(doc.milestones)) {
1652
+ violations.push(violation6("medium", "project.roadmap.invalid-milestones", "milestones must be a list of milestone names"));
1797
1653
  } else {
1798
- const probed = probeHarnessWithStatus(root);
1799
- if (probed) {
1800
- harnessDir = probed;
1801
- } else if (isDirectory2(join7(root, ".mstar"))) {
1802
- harnessDir = join7(root, ".mstar");
1803
- } else if (isDirectory2(join7(root, ".agents"))) {
1804
- harnessDir = join7(root, ".agents");
1805
- } else {
1806
- harnessDir = join7(root, ".mstar");
1654
+ for (const item of doc.milestones) {
1655
+ if (typeof item !== "string" || item.trim() === "") {
1656
+ violations.push(violation6("medium", "project.roadmap.invalid-milestones", "milestones items must be non-empty strings"));
1657
+ break;
1658
+ }
1807
1659
  }
1808
1660
  }
1809
1661
  }
1810
- const sddDir = resolveSddDir(harnessDir, planId);
1811
- mkdirSync5(sddDir, { recursive: true });
1812
- writeFileSync3(join7(sddDir, ".gitignore"), `*
1813
- `);
1814
- return realpathSync3(sddDir);
1815
- }
1816
- function taskBrief(planFile, taskN, outFile, opts = {}) {
1817
- if (!planFile || !Number.isInteger(taskN) || taskN < 1) {
1818
- throw new SddScriptError("usage: mstar sdd task-brief PLAN_FILE TASK_NUMBER [OUTFILE]", 2);
1819
- }
1820
- let content;
1821
- try {
1822
- content = readFileSync5(planFile, "utf8");
1823
- } catch {
1824
- throw new SddScriptError(`no such plan file: ${planFile}`, 2);
1825
- }
1826
- let out;
1827
- if (outFile) {
1828
- out = outFile;
1829
- } else {
1830
- const sddDir = opts.sddDir ?? process.env.SDD_DIR;
1831
- if (!sddDir) {
1832
- throw new SddScriptError("mstar sdd task-brief: set SDD_DIR or pass OUTFILE (run mstar sdd workspace PLAN_ID first)", 2);
1662
+ if (doc.residuals_ref !== undefined && doc.residuals_ref !== null) {
1663
+ if (typeof doc.residuals_ref !== "string" || doc.residuals_ref.trim() === "") {
1664
+ violations.push(violation6("medium", "project.roadmap.invalid-residuals-ref", "residuals_ref must be a non-empty string"));
1833
1665
  }
1834
- mkdirSync5(sddDir, { recursive: true });
1835
- out = join7(sddDir, `task-${taskN}-brief.md`);
1836
1666
  }
1837
- const records = content.endsWith(`
1838
- `) ? content.split(`
1839
- `).slice(0, -1) : content.split(`
1667
+ const warnings = [];
1668
+ const fenceEnd = linesIndexOfClosingFence(content);
1669
+ const body = content.split(/\r?\n/).slice(fenceEnd + 1).join(`
1840
1670
  `);
1841
- const headingRe = /^#+[ \t]+Task[ \t]+[0-9]+/;
1842
- const targetRe = new RegExp(`^#+[ ]+Task[ ]+${taskN}([^0-9]|$)`);
1843
- let infence = false;
1844
- let intask = false;
1845
- const printed = [];
1846
- for (const line of records) {
1847
- if (/^```/.test(line))
1848
- infence = !infence;
1849
- if (!infence && headingRe.test(line))
1850
- intask = targetRe.test(line);
1851
- if (intask)
1852
- printed.push(line);
1671
+ if (!/^##\s+Direction\s*$/m.test(body)) {
1672
+ warnings.push(violation6("low", "project.roadmap.body.missing-direction", "roadmap body has no `## Direction` section (documented body convention) — state the project direction there"));
1853
1673
  }
1854
- const output = printed.length > 0 ? `${printed.join(`
1855
- `)}
1856
- ` : "";
1857
- writeFileSync3(out, output);
1858
- if (printed.length === 0) {
1859
- throw new SddScriptError(`task ${taskN} not found in ${planFile} (no heading matching Task ${taskN})`, 3);
1674
+ if (!/^\s*[-*]\s+\[[xX ]\]/m.test(body)) {
1675
+ warnings.push(violation6("low", "project.roadmap.body.no-goal-items", "roadmap body has no goal-item task list (documented body convention) — list goals as `- [ ]` / `- [x]` markdown task items"));
1860
1676
  }
1861
- return out;
1677
+ return { ok: violations.length === 0, violations, warnings };
1862
1678
  }
1863
- function reviewPackage(base, head, outFile, opts = {}) {
1864
- if (!base || !head) {
1865
- throw new SddScriptError("usage: mstar sdd review-package BASE HEAD [OUTFILE]", 2);
1679
+ function linesIndexOfClosingFence(content) {
1680
+ return content.split(/\r?\n/).indexOf("---", 1);
1681
+ }
1682
+ function validateProjectRegister(doc) {
1683
+ const violations = [];
1684
+ if (!isPlainObject5(doc)) {
1685
+ return {
1686
+ ok: false,
1687
+ violations: [violation6("high", "project.register.invalid", "project register must be an object")]
1688
+ };
1866
1689
  }
1867
- const cwd = opts.cwd ?? process.cwd();
1868
- const verifyRef = (ref, what) => {
1869
- try {
1870
- execFileSync3("git", ["rev-parse", "--verify", "--quiet", ref], { cwd, stdio: ["ignore", "pipe", "pipe"] });
1871
- } catch {
1872
- throw new SddScriptError(`bad ${what}: ${ref}`, 2);
1873
- }
1874
- };
1875
- verifyRef(base, "BASE");
1876
- verifyRef(head, "HEAD");
1877
- let out;
1878
- if (outFile) {
1879
- out = outFile;
1690
+ if (doc.entries === undefined) {
1691
+ violations.push(violation6("high", "project.register.missing-entries", "missing required field: entries"));
1692
+ } else if (!isPlainObject5(doc.entries)) {
1693
+ violations.push(violation6("high", "project.register.invalid-entries", "entries must be an object keyed by plan id"));
1880
1694
  } else {
1881
- const sddDir = opts.sddDir ?? process.env.SDD_DIR;
1882
- if (!sddDir) {
1883
- throw new SddScriptError("mstar sdd review-package: set SDD_DIR or pass OUTFILE", 2);
1695
+ for (const [key, entries] of Object.entries(doc.entries)) {
1696
+ if (key.trim() === "") {
1697
+ violations.push(violation6("medium", "project.register.invalid-key", "entries keys must be non-empty plan ids"));
1698
+ }
1699
+ if (!Array.isArray(entries)) {
1700
+ violations.push(violation6("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)`));
1701
+ continue;
1702
+ }
1703
+ for (const entry of entries) {
1704
+ violations.push(...validateResidual(entry).violations);
1705
+ if (!isPlainObject5(entry))
1706
+ continue;
1707
+ validateNonEmptyString4(violations, entry.source_plan, "source_plan", "project.register.missing-source-plan", "project.register.invalid-source-plan");
1708
+ if (entry.registered_at === undefined) {
1709
+ violations.push(violation6("high", "project.register.missing-registered-at", "missing required field: registered_at"));
1710
+ } else if (typeof entry.registered_at !== "string" || !DATE_RE3.test(entry.registered_at)) {
1711
+ violations.push(violation6("medium", "project.register.invalid-registered-at", "registered_at must be YYYY-MM-DD"));
1712
+ }
1713
+ if (entry.lifecycle_id !== undefined && (typeof entry.lifecycle_id !== "string" || entry.lifecycle_id.trim() === "")) {
1714
+ violations.push(violation6("medium", "project.register.invalid-lifecycle-id", "lifecycle_id must be a non-empty string"));
1715
+ }
1716
+ if (typeof entry.source_plan === "string" && entry.source_plan.trim() !== "" && entry.source_plan !== key) {
1717
+ violations.push(violation6("medium", "project.register.mismatched-source-plan", `source_plan ${JSON.stringify(entry.source_plan)} does not match the entries key ${JSON.stringify(key)} — entries are keyed by plan id`));
1718
+ }
1719
+ }
1884
1720
  }
1885
- mkdirSync5(sddDir, { recursive: true });
1886
- const shortBase = gitOut(cwd, ["rev-parse", "--short", base]) ?? base;
1887
- const shortHead = gitOut(cwd, ["rev-parse", "--short", head]) ?? head;
1888
- out = join7(sddDir, `review-${shortBase}..${shortHead}.diff`);
1889
- }
1890
- const run = (args) => execFileSync3("git", args, { cwd, maxBuffer: GIT_CAPTURE_MAX_BYTES });
1891
- const parts = [
1892
- Buffer.from(`# Review package: ${base}..${head}
1893
-
1894
- ## Commits
1895
- `),
1896
- run(["log", "--oneline", `${base}..${head}`]),
1897
- Buffer.from(`
1898
- ## Files changed
1899
- `),
1900
- run(["diff", "--stat", `${base}..${head}`]),
1901
- Buffer.from(`
1902
- ## Diff
1903
- `),
1904
- run(["diff", "-U10", `${base}..${head}`])
1905
- ];
1906
- writeFileSync3(out, Buffer.concat(parts));
1907
- return out;
1908
- }
1909
- function assertBaseSha(ref, opts = {}) {
1910
- if (typeof ref !== "string" || !/^[0-9a-f]{4,40}$/i.test(ref)) {
1911
- throw new SddScriptError(`assertBaseSha: BASE must be a commit SHA (full or prefix); got ${JSON.stringify(ref)}. ` + "Never use HEAD~1 as review BASE (multi-commit tasks truncate).", 2);
1912
- }
1913
- try {
1914
- execFileSync3("git", ["rev-parse", "--verify", "--quiet", `${ref}^{commit}`], {
1915
- cwd: opts.cwd,
1916
- stdio: ["ignore", "pipe", "pipe"]
1917
- });
1918
- } catch {
1919
- throw new SddScriptError(`assertBaseSha: commit not found: ${ref}`, 2);
1920
- }
1921
- }
1922
- function taskReportExists(sddDir, taskN) {
1923
- try {
1924
- const st = statSync4(join7(sddDir, `task-${taskN}-report.md`));
1925
- return st.isFile() && st.size > 0;
1926
- } catch {
1927
- return false;
1928
- }
1929
- }
1930
- function readProgressLedger(sddDir) {
1931
- let content;
1932
- try {
1933
- content = readFileSync5(join7(sddDir, "progress.md"), "utf8");
1934
- } catch {
1935
- return [];
1936
1721
  }
1937
- return content.split(`
1938
- `).map((line) => line.trim()).filter((line) => line.length > 0);
1722
+ return { ok: violations.length === 0, violations };
1939
1723
  }
1940
- function implementerSessionStickyRules(input) {
1941
- const { session, nextTask, microBatchTasks = 1 } = input;
1942
- if (session.session_mode !== "sticky") {
1943
- return { resume: false, reason: `session_mode is '${session.session_mode}'; sticky resume requires 'sticky'` };
1724
+ function findingsCleanupGate(register, planId, opts) {
1725
+ const mode = opts?.mode ?? "allow-residual";
1726
+ const violations = [];
1727
+ const entries = isPlainObject5(register.entries) ? register.entries[planId] : undefined;
1728
+ if (entries === undefined) {
1729
+ return { ok: true, violations };
1944
1730
  }
1945
- if (typeof session.host_agent_id !== "string" || session.host_agent_id.length === 0) {
1946
- return {
1947
- resume: false,
1948
- reason: "host_agent_id is missing from implementer-session.json; fall back to fresh for this task " + "(mstar-sdd SKILL.md red flag: resume implementer without host_agent_id)"
1949
- };
1731
+ if (!Array.isArray(entries)) {
1732
+ 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)`));
1733
+ return { ok: false, violations };
1950
1734
  }
1951
- if (nextTask <= session.last_task) {
1952
- return {
1953
- resume: false,
1954
- reason: `nextTask ${nextTask} <= last_task ${session.last_task}; task already completed in this session`
1955
- };
1735
+ if (entries.length === 0) {
1736
+ return { ok: true, violations };
1956
1737
  }
1957
- if (microBatchTasks < 1 || microBatchTasks > 3) {
1958
- return {
1959
- resume: false,
1960
- reason: `micro-batch of ${microBatchTasks} tasks is outside 1..3 (max 3 without user override, ` + "sticky-implementer-session.md § Micro-batch fallback)"
1961
- };
1738
+ for (const entry of entries) {
1739
+ if (!isOpenResidual(entry))
1740
+ continue;
1741
+ const id = typeof entry.id === "string" ? entry.id : "<unnamed>";
1742
+ const label = `R#${id}`;
1743
+ if (mode === "zero-residual") {
1744
+ if (entry.severity === "nit") {
1745
+ violations.push(violation6("medium", "findings.zero-residual-nit", `${label}: style-only nits must be fixed in-session or dropped — never left open under zero-residual`));
1746
+ } else if (entry.decision === "risk-accepted" || entry.lifecycle === "waived") {
1747
+ violations.push(violation6("medium", "findings.zero-residual-risk-accepted", `${label}: waived/risk-accepted findings must be closed/archived, not left open under zero-residual`));
1748
+ } else if (entry.decision === "defer") {
1749
+ if (typeof entry.target !== "string" || entry.target.trim() === "") {
1750
+ violations.push(violation6("medium", "findings.zero-residual-defer-no-target", `${label}: blocker-defer requires a target (next iteration/milestone) under zero-residual`));
1751
+ }
1752
+ } else {
1753
+ violations.push(violation6("medium", "findings.zero-residual-open-fixable", `${label}: fixable finding must not remain open under zero-residual — fix now or convert to a blocker-defer`));
1754
+ }
1755
+ } else if (normalizeSeverity(entry.severity) === "critical") {
1756
+ violations.push(violation6("high", "findings.allow-residual-critical", `${label}: unresolved critical blocks Approve with residuals`));
1757
+ }
1962
1758
  }
1963
- return { resume: true, reason: `sticky resume OK: host_agent_id ${session.host_agent_id}, next task ${nextTask}` };
1759
+ return { ok: violations.length === 0, violations };
1964
1760
  }
1965
- function typeName(value) {
1966
- if (value === null)
1967
- return "null";
1968
- if (Array.isArray(value))
1969
- return "array";
1970
- return typeof value;
1761
+ function groupCount(values) {
1762
+ const counts = new Map;
1763
+ for (const value of values) {
1764
+ const key = typeof value === "string" ? value : String(value);
1765
+ counts.set(key, (counts.get(key) ?? 0) + 1);
1766
+ }
1767
+ return Object.fromEntries([...counts.entries()].sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0));
1971
1768
  }
1972
- function validateCompassShape(doc) {
1973
- const issues = [];
1974
- const expectString = (key, opts = {}) => {
1975
- const value = doc[key];
1976
- if (typeof value !== "string") {
1977
- issues.push({ path: [key], message: `expected string, received ${typeName(value)}` });
1978
- return;
1979
- }
1980
- if (opts.min !== undefined && value.length < opts.min) {
1981
- issues.push({ path: [key], message: `string must contain at least ${opts.min} character(s)` });
1982
- return;
1769
+ function techDebtRollup(projectDir) {
1770
+ const items = [];
1771
+ let entries;
1772
+ try {
1773
+ entries = readdirSync3(projectDir, { withFileTypes: true });
1774
+ } catch {
1775
+ entries = [];
1776
+ }
1777
+ for (const project of entries) {
1778
+ if (!project.isDirectory())
1779
+ continue;
1780
+ const registerPath = join7(projectDir, project.name, PROJECT_REGISTER_FILE);
1781
+ if (!existsSync4(registerPath))
1782
+ continue;
1783
+ let register;
1784
+ try {
1785
+ register = readJson(registerPath);
1786
+ } catch {
1787
+ continue;
1983
1788
  }
1984
- if (opts.regex !== undefined && !opts.regex.test(value)) {
1985
- issues.push({ path: [key], message: `string must match ${opts.regex}` });
1789
+ if (!isPlainObject5(register) || !isPlainObject5(register.entries))
1790
+ continue;
1791
+ for (const [plan, planEntries] of Object.entries(register.entries)) {
1792
+ if (!Array.isArray(planEntries))
1793
+ continue;
1794
+ for (const entry of planEntries) {
1795
+ if (!isPlainObject5(entry) || !isOpenResidual(entry))
1796
+ continue;
1797
+ items.push({ plan, entry });
1798
+ }
1986
1799
  }
1800
+ }
1801
+ const bySeverity = {};
1802
+ for (const severity of SEVERITY_ORDER) {
1803
+ bySeverity[severity] = items.filter(({ entry }) => normalizeSeverity(entry.severity) === severity).length;
1804
+ }
1805
+ const computed = {
1806
+ total_open: items.length,
1807
+ by_severity: bySeverity,
1808
+ by_target: groupCount(items.map(({ entry }) => entry.target ?? "unspecified")),
1809
+ by_plan: groupCount(items.map(({ plan }) => plan))
1987
1810
  };
1988
- expectString("iteration_id", { min: 1 });
1989
- expectString("start_date", { regex: DATE_RE2 });
1990
- const status = doc.status;
1991
- if (typeof status !== "string" || !COMPASS_STATUSES.includes(status)) {
1992
- issues.push({
1993
- path: ["status"],
1994
- message: `expected one of ${COMPASS_STATUSES.map((s) => `'${s}'`).join(" | ")}, received ${typeName(status)}`
1995
- });
1811
+ const stored = null;
1812
+ const checks = ROLLUP_FIELDS.map((field) => ({ field, status: "DRIFT" }));
1813
+ const overall = "DRIFT";
1814
+ return { computed, stored, checks, overall };
1815
+ }
1816
+ function listProjectReferenceFiles(projectDir) {
1817
+ const root = join7(projectDir, PROJECT_REFERENCES_DIR);
1818
+ let entries;
1819
+ try {
1820
+ entries = readdirSync3(root, { withFileTypes: true });
1821
+ } catch {
1822
+ return [];
1996
1823
  }
1997
- expectString("iteration_base_branch", { min: 1 });
1998
- expectString("target_branch", { min: 1 });
1999
- const plans = doc.plans;
2000
- if (plans !== undefined) {
2001
- if (!Array.isArray(plans)) {
2002
- issues.push({ path: ["plans"], message: `expected array, received ${typeName(plans)}` });
2003
- } else {
2004
- plans.forEach((entry, index) => {
2005
- if (typeof entry !== "string") {
2006
- issues.push({ path: ["plans", index], message: `expected string, received ${typeName(entry)}` });
2007
- } else if (entry.length < 1) {
2008
- issues.push({ path: ["plans", index], message: "string must contain at least 1 character(s)" });
2009
- }
2010
- });
1824
+ const files = [];
1825
+ for (const entry of entries) {
1826
+ if (entry.name === PROJECT_ROADMAP_FILE || entry.name === PROJECT_REGISTER_FILE)
1827
+ continue;
1828
+ if (entry.isFile()) {
1829
+ files.push(entry.name);
1830
+ } else if (entry.isDirectory()) {
1831
+ let nested;
1832
+ try {
1833
+ nested = readdirSync3(join7(root, entry.name), { withFileTypes: true });
1834
+ } catch {
1835
+ continue;
1836
+ }
1837
+ for (const child of nested) {
1838
+ if (child.isFile())
1839
+ files.push(`${entry.name}/${child.name}`);
1840
+ }
2011
1841
  }
2012
1842
  }
2013
- const end_date = doc.end_date;
2014
- if (end_date !== undefined) {
2015
- if (typeof end_date !== "string") {
2016
- issues.push({ path: ["end_date"], message: `expected string, received ${typeName(end_date)}` });
2017
- } else if (!DATE_RE2.test(end_date)) {
2018
- issues.push({ path: ["end_date"], message: `string must match ${DATE_RE2}` });
1843
+ return files.sort();
1844
+ }
1845
+ function resolveHarnessDir(startDir = process.cwd(), opts = {}) {
1846
+ const start = resolve5(startDir);
1847
+ const explicit = opts.harnessDir ?? process.env.MSTAR_HARNESS_DIR;
1848
+ if (explicit)
1849
+ return resolve5(start, explicit);
1850
+ const boundary = resolve5(start, opts.workspaceRoot ?? defaultWorkspaceRoot(start));
1851
+ const rc = loadMstarc(start, boundary);
1852
+ if (rc !== null && rc.config.harnessDir)
1853
+ return resolve5(rc.dir, rc.config.harnessDir);
1854
+ let dir = start;
1855
+ for (;; ) {
1856
+ if (!isAtOrBelow2(dir, boundary))
1857
+ return null;
1858
+ for (const candidate of [join8(dir, ".mstar"), join8(dir, ".agents"), join8(dir, ".plans"), join8(dir, "plans")]) {
1859
+ if (isDirectory(candidate))
1860
+ return candidate;
2019
1861
  }
1862
+ if (dir === boundary)
1863
+ return null;
1864
+ const parent = dirname5(dir);
1865
+ if (parent === dir)
1866
+ return null;
1867
+ dir = parent;
2020
1868
  }
2021
- if (issues.length > 0)
2022
- return { ok: false, issues };
2023
- return {
2024
- ok: true,
2025
- data: {
2026
- iteration_id: doc.iteration_id,
2027
- start_date: doc.start_date,
2028
- status,
2029
- iteration_base_branch: doc.iteration_base_branch,
2030
- target_branch: doc.target_branch,
2031
- ...plans !== undefined ? { plans } : {},
2032
- ...end_date !== undefined ? { end_date } : {}
1869
+ }
1870
+ function defaultWorkspaceRoot(startDir) {
1871
+ try {
1872
+ const cdup = execFileSync("git", ["rev-parse", "--show-cdup"], {
1873
+ cwd: startDir,
1874
+ encoding: "utf8",
1875
+ stdio: ["ignore", "pipe", "ignore"]
1876
+ }).trim();
1877
+ if (!cdup)
1878
+ return startDir;
1879
+ let boundary = startDir;
1880
+ for (const segment of cdup.split(/[\\/]/)) {
1881
+ if (segment && segment !== ".")
1882
+ boundary = dirname5(boundary);
2033
1883
  }
2034
- };
1884
+ return resolve5(boundary);
1885
+ } catch {}
1886
+ return startDir;
2035
1887
  }
2036
- function violation6(severity, code, message, fix) {
2037
- return { ok: false, severity, code, message, fix };
1888
+ function isAtOrBelow2(dir, root) {
1889
+ const rel = relative2(root, dir);
1890
+ return rel === "" || !rel.startsWith("..") && !isAbsolute3(rel);
2038
1891
  }
2039
- function isPlainObject4(value) {
2040
- return typeof value === "object" && value !== null && !Array.isArray(value);
1892
+ function mstarcDirOverride(harnessDir, key) {
1893
+ const dir = resolve5(harnessDir);
1894
+ const rc = loadMstarc(dir, dirname5(dir));
1895
+ const declared = rc?.config[key];
1896
+ return declared ? resolve5(rc.dir, declared) : null;
2041
1897
  }
2042
- function validateCompassFrontmatter(doc) {
2043
- if (!isPlainObject4(doc)) {
2044
- return {
2045
- ok: false,
2046
- violations: [
2047
- 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 §1.3)", "Fix the frontmatter of {ITERATION_DIR}/<iteration-id>/delivery-compass.md")
2048
- ]
2049
- };
2050
- }
2051
- const parsed = validateCompassShape(doc);
2052
- if (!parsed.ok) {
2053
- return {
2054
- ok: false,
2055
- violations: parsed.issues.map((issue) => {
2056
- const field = issue.path.join(".") || "(root)";
2057
- 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 §1.3)`);
2058
- })
2059
- };
2060
- }
2061
- const violations = [];
2062
- const { status, end_date } = parsed.data;
2063
- if (status === "completed" && end_date === undefined) {
2064
- violations.push(violation6("high", "COMPASS_END_DATE_REQUIRED", "Compass frontmatter status is 'completed' but end_date is missing — end_date is required at iteration-close (mstar-iteration §3.4, template Fields guide)", "Add `end_date: YYYY-MM-DD` to the frontmatter"));
1898
+ function resolveSpecsDir(harnessDir, opts = {}) {
1899
+ const declared = mstarcDirOverride(harnessDir, "specsDir");
1900
+ if (declared !== null) {
1901
+ if (opts.create !== false)
1902
+ mkdirSync4(declared, { recursive: true });
1903
+ return declared;
2065
1904
  }
2066
- if (status !== "completed" && end_date !== undefined) {
2067
- violations.push(violation6("medium", "COMPASS_END_DATE_NOT_ALLOWED", `Compass frontmatter sets end_date while status is '${status}' — end_date is only written at iteration-close (mstar-iteration §3.4)`, "Remove end_date until iteration-close"));
1905
+ const harness = resolve5(harnessDir);
1906
+ const repoRoot = dirname5(harness);
1907
+ const candidates = [
1908
+ join8(harness, "specs"),
1909
+ join8(repoRoot, "docs", "specs"),
1910
+ join8(repoRoot, "specs"),
1911
+ join8(harness, "designs"),
1912
+ join8(repoRoot, "designs")
1913
+ ];
1914
+ for (const candidate of candidates) {
1915
+ if (isDirectory(candidate) && hasFiles(candidate))
1916
+ return candidate;
2068
1917
  }
2069
- return { ok: violations.length === 0, violations };
1918
+ const fallback = join8(harness, "specs");
1919
+ if (opts.create !== false)
1920
+ mkdirSync4(fallback, { recursive: true });
1921
+ return fallback;
2070
1922
  }
2071
- function registeredPlanIds(compassDoc) {
2072
- if (!Array.isArray(compassDoc.plans))
2073
- return [];
2074
- return compassDoc.plans.filter((plan) => typeof plan === "string" && plan.length > 0);
1923
+ function resolvePlanDir(harnessDir) {
1924
+ const declared = mstarcDirOverride(harnessDir, "planDir");
1925
+ if (declared !== null)
1926
+ return declared;
1927
+ const dir = resolve5(harnessDir);
1928
+ const name = basename2(dir);
1929
+ if (name === ".plans" || name === "plans")
1930
+ return dir;
1931
+ return join8(dir, "plans");
2075
1932
  }
2076
- function findPlanRow(snapshotDoc, planId) {
2077
- if (!Array.isArray(snapshotDoc.plans))
2078
- return null;
2079
- for (const row of snapshotDoc.plans) {
2080
- if (!isPlainObject4(row))
2081
- continue;
2082
- const rowId = typeof row.id === "string" ? row.id : typeof row.plan_id === "string" ? row.plan_id : null;
2083
- if (rowId === planId)
2084
- return row;
1933
+ function assertSafePathComponent(value, what) {
1934
+ if (value === "" || value === "." || value === ".." || !/^[A-Za-z0-9._-]+$/.test(value)) {
1935
+ throw new Error(`${what} must be a single safe path component ([A-Za-z0-9._-]+; not "", ".", "..", or containing "/" or "\\") — got ${JSON.stringify(value)}`);
2085
1936
  }
2086
- return null;
2087
1937
  }
2088
- function entryPlansAllDone(snapshotDoc, registered) {
2089
- const violations = [];
2090
- if (registered.length === 0) {
2091
- violations.push(violation6("medium", "COMPASS_NO_PLANS", "Compass frontmatter registers no plans — the all-plans-Done transition cannot be verified (mstar-iteration §1.3 / Phase transition gates)", "List the iteration's plan ids in the compass frontmatter `plans`"));
2092
- return violations;
2093
- }
2094
- for (const planId of registered) {
2095
- const row = findPlanRow(snapshotDoc, planId);
2096
- if (row === null) {
2097
- 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 §3.1 entry item 1)`, "Add the plan row to {HARNESS_DIR}/workflows/<id>/snapshot.json"));
2098
- continue;
2099
- }
2100
- if (row.status !== PLAN_STATUS_DONE) {
2101
- violations.push(violation6("high", "PLAN_NOT_DONE", `Plan '${planId}' status is ${JSON.stringify(row.status)} in the workflow snapshot — all compass-registered plans must be 'Done' before iteration-close (mstar-iteration §3.1 entry item 1)`));
2102
- }
1938
+ function resolveSddDir(harnessDir, planId) {
1939
+ assertSafePathComponent(planId, "planId");
1940
+ const base = resolve5(harnessDir);
1941
+ const declared = mstarcDirOverride(base, "sddDir");
1942
+ const sddBase = declared !== null ? declared : join8(base, "sdd");
1943
+ return join8(sddBase, planId);
1944
+ }
1945
+ function resolveIterationDir(harnessDir) {
1946
+ const declared = mstarcDirOverride(harnessDir, "iterationDir");
1947
+ if (declared !== null)
1948
+ return declared;
1949
+ return join8(resolve5(harnessDir), "iterations");
1950
+ }
1951
+ function resolveKnowledgeDir(harnessDir) {
1952
+ const declared = mstarcDirOverride(harnessDir, "knowledgeDir");
1953
+ if (declared !== null)
1954
+ return declared;
1955
+ return join8(resolve5(harnessDir), "knowledge");
1956
+ }
1957
+ function resolveHarnessSubdir(startDir, opts, key, fallback) {
1958
+ const harness = resolveHarnessDir(startDir, opts);
1959
+ if (harness === null) {
1960
+ throw new Error(`harness dir not found from ${resolve5(startDir)} — cannot resolve the ${fallback} dir (run \`mstar harness scaffold\`, pass opts.harnessDir, or set MSTAR_HARNESS_DIR)`);
2103
1961
  }
2104
- return violations;
1962
+ const declared = mstarcDirOverride(harness, key);
1963
+ return declared !== null ? declared : join8(resolve5(harness), fallback);
2105
1964
  }
2106
- function entryFrontmatterComplete(compassDoc) {
2107
- return validateCompassFrontmatter(compassDoc).violations;
1965
+ function resolveWorkflowDir(startDir = process.cwd(), opts = {}) {
1966
+ return resolveHarnessSubdir(startDir, opts, "workflowDir", "workflows");
2108
1967
  }
2109
- function exitFrontmatterClosed(compassDoc) {
2110
- const violations = [];
2111
- if (compassDoc.status !== "completed") {
2112
- violations.push(violation6("high", "EXIT_STATUS_NOT_COMPLETED", `Compass frontmatter status must be 'completed' at close exit — current: ${JSON.stringify(compassDoc.status)} (mstar-iteration §3.4 / §3.5 exit item 4)`));
2113
- }
2114
- const endDate = compassDoc.end_date;
2115
- if (typeof endDate !== "string" || !DATE_RE2.test(endDate)) {
2116
- violations.push(violation6("high", "EXIT_END_DATE_REQUIRED", "Compass frontmatter end_date (YYYY-MM-DD) is required when closing (mstar-iteration §3.4 / §3.5 exit item 4)"));
2117
- }
2118
- return violations;
1968
+ function resolveProjectDir(startDir = process.cwd(), opts = {}) {
1969
+ return resolveHarnessSubdir(startDir, opts, "projectDir", "projects");
2119
1970
  }
2120
- function exitBranchCheck(opts) {
2121
- const violations = [];
2122
- const { currentBranch, specIntegrationBranch } = opts;
2123
- if (currentBranch === undefined || specIntegrationBranch === undefined) {
2124
- violations.push(violation6("medium", "EXIT_BRANCH_UNVERIFIABLE", "Cannot verify the current branch is spec_integration_branch — missing currentBranch / specIntegrationBranch probe inputs (mstar-iteration §3.5 exit item 5)"));
2125
- } else if (currentBranch !== specIntegrationBranch) {
2126
- violations.push(violation6("high", "EXIT_BRANCH_MISMATCH", `Current branch '${currentBranch}' is not the spec_integration_branch '${specIntegrationBranch}' (mstar-iteration §3.5 exit item 5)`));
2127
- }
2128
- return violations;
1971
+ function resolveScaffoldDirs(root) {
1972
+ const start = resolve5(root);
1973
+ const boundary = resolve5(start, defaultWorkspaceRoot(start));
1974
+ const rc = loadMstarc(start, boundary);
1975
+ const explicit = process.env.MSTAR_HARNESS_DIR;
1976
+ const harnessDir = explicit ? resolve5(start, explicit) : rc !== null && rc.config.harnessDir ? resolve5(rc.dir, rc.config.harnessDir) : join8(start, ".mstar");
1977
+ const declaredProjectDir = mstarcDirOverride(harnessDir, "projectDir");
1978
+ const projectDir = declaredProjectDir !== null ? declaredProjectDir : join8(harnessDir, "projects");
1979
+ return { harnessDir, projectDir };
2129
1980
  }
2130
- function exitPrBaseCheck(compassDoc, opts) {
2131
- const violations = [];
2132
- const target = compassDoc.target_branch;
2133
- const { prBaseBranch } = opts;
2134
- if (prBaseBranch === undefined) {
2135
- violations.push(violation6("medium", "EXIT_PR_BASE_UNVERIFIABLE", "Cannot verify the PR base — missing prBaseBranch probe input (mstar-iteration §3.5 exit item 6)"));
2136
- } else if (typeof target !== "string" || prBaseBranch !== target) {
2137
- violations.push(violation6("high", "EXIT_PR_BASE_MISMATCH", `PR base '${prBaseBranch}' must equal the compass target_branch '${String(target)}' — not an undocumented branch (mstar-iteration §3.5 exit item 6)`));
2138
- }
2139
- return violations;
1981
+ function scaffoldHarness(root) {
1982
+ const { harnessDir, projectDir } = resolveScaffoldDirs(root);
1983
+ for (const dir of SCAFFOLD_DIRS)
1984
+ mkdirSync4(join8(harnessDir, dir), { recursive: true });
1985
+ const statusPath = join8(harnessDir, "status.json");
1986
+ if (Object.keys(readJson(statusPath)).length === 0)
1987
+ writeJson(statusPath, EMPTY_STATUS_TEMPLATE);
1988
+ const defaultProjectDir = join8(projectDir, _DEFAULT_PROJECT);
1989
+ mkdirSync4(defaultProjectDir, { recursive: true });
1990
+ const roadmapPath = join8(defaultProjectDir, PROJECT_ROADMAP_FILE);
1991
+ if (!existsSync5(roadmapPath)) {
1992
+ const created = new Date().toISOString().slice(0, 10);
1993
+ writeFileSync3(roadmapPath, ROADMAP_TEMPLATE.replace("{created_at}", created), "utf8");
1994
+ }
1995
+ const registerPath = join8(defaultProjectDir, PROJECT_REGISTER_FILE);
1996
+ if (Object.keys(readJson(registerPath)).length === 0)
1997
+ writeJson(registerPath, EMPTY_REGISTER_TEMPLATE);
1998
+ return harnessDir;
2140
1999
  }
2141
- function evaluatePhaseGate(snapshotDoc, compassDoc, opts = {}) {
2142
- const registered = registeredPlanIds(compassDoc);
2143
- const entryViolations = [
2144
- ...entryPlansAllDone(snapshotDoc, registered),
2145
- ...entryFrontmatterComplete(compassDoc)
2146
- ];
2147
- const exitViolations = [
2148
- ...exitFrontmatterClosed(compassDoc),
2149
- ...exitBranchCheck(opts),
2150
- ...exitPrBaseCheck(compassDoc, opts)
2151
- ];
2152
- const allPlansDone = registered.length > 0 && registered.every((planId) => {
2153
- const row = findPlanRow(snapshotDoc, planId);
2154
- return row !== null && row.status === PLAN_STATUS_DONE;
2155
- });
2156
- const entry = { ok: entryViolations.length === 0, violations: entryViolations };
2157
- const exit = { ok: exitViolations.length === 0, violations: exitViolations };
2158
- let transition;
2159
- if (!allPlansDone)
2160
- transition = "phase-2-execute";
2161
- else if (entry.ok && exit.ok)
2162
- transition = "phase-4-pr-delivery";
2163
- else
2164
- transition = "phase-3-close";
2165
- const gateBlocking = allPlansDone ? [...entryViolations, ...exitViolations] : [];
2166
- return {
2167
- transition,
2168
- allPlansDone,
2169
- entry,
2170
- exit,
2171
- ok: gateBlocking.length === 0,
2172
- violations: gateBlocking
2173
- };
2000
+ function emitGitignoreSnippet(kind) {
2001
+ if (kind === "agents")
2002
+ return GITIGNORE_SNIPPET_AGENTS;
2003
+ if (kind === "mstar")
2004
+ return GITIGNORE_SNIPPET;
2005
+ return `${GITIGNORE_SNIPPET}${GITIGNORE_SNIPPET_AGENTS}`;
2174
2006
  }
2175
- function pushCadenceProbe(ciRunning, reviewWaveActive) {
2176
- const violations = [];
2177
- if (ciRunning) {
2178
- violations.push(violation6("high", "PUSH_BLOCKED_CI", "CI checks are still queued/in_progress on the current head — do not push until the wave completes (mstar-iteration §5.1a push gate 1)", "Wait for CI to settle, then push once with the whole local batch"));
2007
+ function validateGitignore(root) {
2008
+ const gitignorePath = join8(resolve5(root), ".gitignore");
2009
+ const kind = detectHarnessKind(resolveHarnessDir(root));
2010
+ let content;
2011
+ try {
2012
+ content = readFileSync6(gitignorePath, "utf8");
2013
+ } catch {
2014
+ return {
2015
+ ok: false,
2016
+ severity: "medium",
2017
+ code: "gitignore.missing",
2018
+ message: `no .gitignore found at ${gitignorePath}`,
2019
+ fix: `append the canonical snippet (emitGitignoreSnippet(${kind ? `"${kind}"` : ""})) to ${gitignorePath}`
2020
+ };
2179
2021
  }
2180
- if (reviewWaveActive) {
2181
- violations.push(violation6("high", "PUSH_BLOCKED_REVIEW_WAVE", "An AI/bot review wave is still running on the current head — do not push until it settles (mstar-iteration §5.1a push gate 2)", "Wait for the review wave, then push once"));
2022
+ const lines = new Set(content.split(/\r?\n/).map((line) => line.trim()).filter((line) => line.length > 0));
2023
+ const mstarMissing = GITIGNORE_PROCESS_ENTRIES.filter((entry) => !lines.has(entry));
2024
+ const agentsMissing = GITIGNORE_PROCESS_ENTRIES_AGENTS.filter((entry) => !lines.has(entry));
2025
+ let missing;
2026
+ let label;
2027
+ if (kind === "agents") {
2028
+ missing = agentsMissing;
2029
+ label = ".agents/ set";
2030
+ } else if (kind === "mstar") {
2031
+ missing = mstarMissing;
2032
+ label = ".mstar/ set";
2033
+ } else {
2034
+ label = "either .mstar/ or .agents/ set";
2035
+ missing = mstarMissing.length === 0 || agentsMissing.length === 0 ? [] : mstarMissing.length <= agentsMissing.length ? mstarMissing : agentsMissing;
2182
2036
  }
2183
- return { ok: violations.length === 0, violations };
2037
+ if (missing.length > 0) {
2038
+ return {
2039
+ ok: false,
2040
+ severity: "medium",
2041
+ code: "gitignore.missing-entries",
2042
+ message: `.gitignore at ${gitignorePath} is missing canonical harness ignore entries (${label}): ${missing.join(", ")}`,
2043
+ fix: `append the canonical snippet (emitGitignoreSnippet(${kind ? `"${kind}"` : ""})) to ${gitignorePath}`
2044
+ };
2045
+ }
2046
+ return {
2047
+ ok: true,
2048
+ severity: "low",
2049
+ code: "gitignore.ok",
2050
+ message: `.gitignore at ${gitignorePath} contains a complete canonical harness ignore set — default-ignore + tracked re-includes (${label})`
2051
+ };
2184
2052
  }
2185
- function assertIndexRowObligations(iterationsDir) {
2186
- if (!existsSync5(iterationsDir)) {
2053
+ function detectHarnessKind(harnessDir) {
2054
+ if (!harnessDir)
2055
+ return null;
2056
+ const name = basename2(resolve5(harnessDir));
2057
+ if (name === ".mstar")
2058
+ return "mstar";
2059
+ if (name === ".agents")
2060
+ return "agents";
2061
+ return null;
2062
+ }
2063
+ function assertPlanWritingPath(planPath, harnessDir) {
2064
+ const planAbs = resolve5(planPath);
2065
+ if (!harnessDir) {
2187
2066
  return {
2188
2067
  ok: false,
2189
- violations: [
2190
- violation6("high", "INDEX_ITERATIONS_DIR_MISSING", `{ITERATION_DIR} '${iterationsDir}' does not exist (mstar-iteration §1.4)`, "Create the iterations directory (path.resolveIterationDir)")
2191
- ]
2068
+ severity: "high",
2069
+ code: "plan-path.no-harness",
2070
+ message: `persistent plan tracking is not enabled — cannot place plan ${planAbs} under {PLAN_DIR}`,
2071
+ fix: "initialize the harness (scaffoldHarness) so plans land in {PLAN_DIR}"
2192
2072
  };
2193
2073
  }
2194
- const iterationIds = readdirSync4(iterationsDir, { withFileTypes: true }).filter((entry) => entry.isDirectory()).filter((entry) => existsSync5(join8(iterationsDir, entry.name, COMPASS_FILE))).map((entry) => entry.name).sort();
2195
- const readmePath = join8(iterationsDir, INDEX_README);
2196
- if (!existsSync5(readmePath)) {
2074
+ const planDir = resolvePlanDir(harnessDir);
2075
+ const rel = relative2(planDir, planAbs);
2076
+ const inside = rel === "" || !rel.startsWith("..") && !isAbsolute3(rel);
2077
+ if (!inside) {
2197
2078
  return {
2198
2079
  ok: false,
2199
- violations: [
2200
- violation6("high", "INDEX_README_MISSING", `{ITERATION_DIR}/README.md does not exist — one row per iteration is required (mstar-iteration §1.4)`, `Create {ITERATION_DIR}/README.md with the header '${INDEX_HEADER}' and one row per iteration`)
2201
- ]
2080
+ severity: "high",
2081
+ code: "plan-path.outside-plan-dir",
2082
+ message: `plan file ${planAbs} is outside {PLAN_DIR} (${planDir})`,
2083
+ fix: `write the plan under ${planDir}`
2202
2084
  };
2203
2085
  }
2204
- const violations = [];
2205
- const lines = readFileSync6(readmePath, "utf8").split(/\r?\n/);
2206
- if (!lines.some((line) => line.includes(INDEX_HEADER))) {
2207
- violations.push(violation6("medium", "INDEX_HEADER_MISSING", `{ITERATION_DIR}/README.md lacks the table header '${INDEX_HEADER}' (mstar-iteration §1.4)`, "Add the header row on first creation"));
2086
+ if (existsSync5(planAbs)) {
2087
+ try {
2088
+ const canonicalPlan = realpathSync2(planAbs);
2089
+ const canonicalPlanDir = existsSync5(planDir) ? realpathSync2(planDir) : resolve5(planDir);
2090
+ const canonicalRel = relative2(canonicalPlanDir, canonicalPlan);
2091
+ const canonicalInside = canonicalRel === "" || !canonicalRel.startsWith("..") && !isAbsolute3(canonicalRel);
2092
+ if (!canonicalInside) {
2093
+ return {
2094
+ ok: false,
2095
+ severity: "high",
2096
+ code: "plan-path.symlink-escape",
2097
+ message: `plan file ${planAbs} resolves to ${canonicalPlan}, outside {PLAN_DIR} (${canonicalPlanDir})`,
2098
+ fix: `write the plan under ${planDir}`
2099
+ };
2100
+ }
2101
+ } catch {}
2208
2102
  }
2209
- const indexed = new Set;
2210
- for (const line of lines) {
2211
- const match = line.match(/^\s*\|\s*`([^`]+)`\s*\|/);
2212
- if (match)
2213
- indexed.add(match[1].trim());
2103
+ return {
2104
+ ok: true,
2105
+ severity: "low",
2106
+ code: "plan-path.ok",
2107
+ message: `plan file ${planAbs} lives under {PLAN_DIR} (${planDir})`
2108
+ };
2109
+ }
2110
+ function isDirectory(dir) {
2111
+ try {
2112
+ return statSync3(dir).isDirectory();
2113
+ } catch {
2114
+ return false;
2214
2115
  }
2215
- for (const id of iterationIds) {
2216
- if (!indexed.has(id)) {
2217
- violations.push(violation6("medium", "INDEX_ROW_MISSING", `Iteration '${id}' has a delivery-compass.md but no index row in {ITERATION_DIR}/README.md — one row per iteration (mstar-iteration §1.4)`, `Add | \`${id}\` | [\`${id}/\`](${id}/) | <description> | <status> |`));
2116
+ }
2117
+ function hasFiles(dir) {
2118
+ try {
2119
+ for (const entry of readdirSync4(dir, { withFileTypes: true })) {
2120
+ if (entry.isDirectory()) {
2121
+ if (hasFiles(join8(dir, entry.name)))
2122
+ return true;
2123
+ } else if (entry.isFile()) {
2124
+ return true;
2125
+ }
2218
2126
  }
2127
+ return false;
2128
+ } catch {
2129
+ return false;
2219
2130
  }
2220
- return { ok: violations.length === 0, violations };
2221
2131
  }
2222
- function parseCompassFrontmatter(filePath) {
2223
- return parseCompassFrontmatterText(readFileSync6(filePath, "utf8"), filePath);
2132
+ function probeTimeoutMs() {
2133
+ const raw = process.env.MSTAR_GIT_PROBE_TIMEOUT_MS;
2134
+ if (raw === undefined || raw.trim() === "")
2135
+ return DEFAULT_PROBE_TIMEOUT_MS;
2136
+ const parsed = Number(raw);
2137
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : DEFAULT_PROBE_TIMEOUT_MS;
2224
2138
  }
2225
- function parseCompassFrontmatterText(content, filePath) {
2226
- const lines = content.split(/\r?\n/);
2227
- if (lines[0]?.trim() !== "---") {
2228
- throw new Error(`no YAML frontmatter fence in ${filePath} (expected first line "---")`);
2229
- }
2230
- const end = lines.indexOf("---", 1);
2231
- if (end === -1) {
2232
- throw new Error(`unterminated YAML frontmatter in ${filePath} (no closing "---")`);
2233
- }
2234
- const doc = {};
2235
- let listKey = null;
2236
- for (let i = 1;i < end; i += 1) {
2237
- const line = lines[i] ?? "";
2238
- if (!line.trim() || line.trim().startsWith("#"))
2239
- continue;
2240
- if (listKey !== null && /^\s*-\s+/.test(line)) {
2241
- const item = line.replace(/^\s*-\s+/, "").trim().replace(/^["']|["']$/g, "");
2242
- if (!Array.isArray(doc[listKey]))
2243
- doc[listKey] = [];
2244
- doc[listKey].push(item);
2245
- continue;
2246
- }
2247
- listKey = null;
2248
- const kv = line.match(/^([A-Za-z_][A-Za-z0-9_-]*):\s*(.*)$/);
2249
- if (!kv) {
2250
- throw new Error(`unsupported frontmatter line in ${filePath}: ${JSON.stringify(line)}`);
2139
+ function violation7(severity, code, message, fix) {
2140
+ return { ok: false, severity, code, message, fix };
2141
+ }
2142
+ function gate(violations) {
2143
+ return { ok: violations.length === 0, violations };
2144
+ }
2145
+ function probeBranch(worktreePath, opts) {
2146
+ const precomputed = opts.branchOf?.(worktreePath);
2147
+ if (precomputed !== undefined)
2148
+ return { branch: precomputed };
2149
+ const timeout = opts.timeoutMs ?? probeTimeoutMs();
2150
+ try {
2151
+ const stdout = execFileSync2(opts.gitPath ?? "git", ["-C", worktreePath, "branch", "--show-current"], {
2152
+ encoding: "utf8",
2153
+ stdio: ["ignore", "pipe", "pipe"],
2154
+ timeout
2155
+ });
2156
+ const branch = stdout.trim();
2157
+ if (branch === "")
2158
+ return { error: `no branch checked out (detached HEAD?) at "${worktreePath}"` };
2159
+ return { branch };
2160
+ } catch (err) {
2161
+ const e = err;
2162
+ if (e.killed === true || e.signal !== undefined) {
2163
+ return { error: `git probe timed out after ${timeout}ms (killed by ${e.signal ?? "SIGTERM"})` };
2251
2164
  }
2252
- const value = kv[2].trim();
2253
- doc[kv[1]] = value === "" ? null : /^\[.*\]$/.test(value) ? parseFlowArray(value, filePath) : value.replace(/^["']|["']$/g, "");
2254
- listKey = value === "" ? kv[1] : null;
2165
+ const detail = (e.stderr !== undefined ? e.stderr.toString().trim() : "") || e.message || "git probe failed";
2166
+ return { error: detail };
2255
2167
  }
2256
- return doc;
2257
2168
  }
2258
- function parseFlowArray(raw, filePath) {
2259
- const inner = raw.slice(1, -1);
2260
- if (/[[\]]/.test(inner)) {
2261
- throw new Error(`nested flow-style array in ${filePath}: ${JSON.stringify(raw)} only flat scalar items are supported (e.g. [a, b])`);
2169
+ function l1PreDispatchCheck(input, opts = {}) {
2170
+ const violations = [];
2171
+ const { controlWorktreePath, leaseWorktreePath, leaseWorkingBranch, planId } = input;
2172
+ if (controlWorktreePath.trim() === "") {
2173
+ violations.push(violation7("high", "worktree.l1.control-missing", "metadata.control_worktree_path is not recorded — 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"));
2262
2174
  }
2263
- let quote = null;
2264
- for (const ch of inner) {
2265
- if (ch === '"' || ch === "'") {
2266
- if (quote === null)
2267
- quote = ch;
2268
- else if (quote === ch)
2269
- quote = null;
2270
- } else if (ch === "," && quote !== null) {
2271
- throw new Error(`ambiguous flow-style array in ${filePath}: ${JSON.stringify(raw)} — quoted item containing comma cannot be split unambiguously (flat scalar items only)`);
2272
- }
2175
+ if (leaseWorktreePath.trim() === "") {
2176
+ violations.push(violation7("high", "worktree.l1.lease-missing", `execution_lease.worktree_path is empty for plan "${planId}" no verified execution_lease to dispatch against`, "claim the execution_lease with an absolute feature worktree path before dispatch"));
2273
2177
  }
2274
- if (quote !== null) {
2275
- throw new Error(`unterminated ${quote} quote in flow-style array in ${filePath}: ${JSON.stringify(raw)}`);
2178
+ if (leaseWorkingBranch.trim() === "") {
2179
+ 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"));
2276
2180
  }
2277
- const items = [];
2278
- for (const part of inner.split(",")) {
2279
- const item = part.trim().replace(/^["']|["']$/g, "");
2280
- if (item === "")
2281
- continue;
2282
- items.push(item);
2181
+ if (controlWorktreePath !== "" && leaseWorktreePath !== "" && resolve6(controlWorktreePath) === resolve6(leaseWorktreePath)) {
2182
+ violations.push(violation7("critical", "worktree.l1.lease-equals-control", `execution_lease.worktree_path "${leaseWorktreePath}" equals metadata.control_worktree_path — 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"));
2283
2183
  }
2284
- return items;
2285
- }
2286
- function isPlainObject5(value) {
2287
- return typeof value === "object" && value !== null && !Array.isArray(value);
2288
- }
2289
- function violation7(severity, code, message, fix) {
2290
- return { ok: false, severity, code, message, fix };
2291
- }
2292
- function validateNonEmptyString4(violations, value, field, missingCode, invalidCode) {
2293
- if (value === undefined) {
2294
- violations.push(violation7("high", missingCode, `missing required field: ${field}`));
2295
- } else if (typeof value !== "string" || value.trim() === "") {
2296
- violations.push(violation7("medium", invalidCode, `${field} must be a non-empty string`));
2184
+ if (leaseWorktreePath !== "" && !existsSync6(leaseWorktreePath)) {
2185
+ 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>`));
2186
+ } else if (leaseWorktreePath !== "" && leaseWorkingBranch !== "") {
2187
+ const probe = probeBranch(leaseWorktreePath, opts);
2188
+ if ("error" in probe) {
2189
+ 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)"));
2190
+ } else if (probe.branch !== leaseWorkingBranch) {
2191
+ 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`));
2192
+ }
2297
2193
  }
2194
+ return gate(violations);
2298
2195
  }
2299
- function validateRoadmap(filePath) {
2196
+ function l2PreDispatchCheck(input, opts = {}) {
2300
2197
  const violations = [];
2301
- let content;
2302
- try {
2303
- content = readFileSync7(filePath, "utf8");
2304
- } catch {
2305
- return {
2306
- ok: false,
2307
- violations: [violation7("high", "project.roadmap.unreadable", `cannot read roadmap file: ${filePath}`)],
2308
- warnings: []
2309
- };
2310
- }
2311
- let doc;
2312
- try {
2313
- doc = parseCompassFrontmatterText(content, filePath);
2314
- } catch (err) {
2315
- const message = err instanceof Error ? err.message : `invalid roadmap frontmatter in ${filePath}`;
2316
- return { ok: false, violations: [violation7("high", "project.roadmap.invalid-frontmatter", message)], warnings: [] };
2198
+ const tracks = input.tracks ?? [];
2199
+ const seenPaths = new Set;
2200
+ if (tracks.length < 1) {
2201
+ violations.push(violation7("high", "worktree.l2.no-tracks", "no parallel writable tracks — 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"));
2317
2202
  }
2318
- validateNonEmptyString4(violations, doc.project_id, "project_id", "project.roadmap.missing-project-id", "project.roadmap.invalid-project-id");
2319
- validateNonEmptyString4(violations, doc.title, "title", "project.roadmap.missing-title", "project.roadmap.invalid-title");
2320
- if (doc.status === undefined) {
2321
- violations.push(violation7("high", "project.roadmap.missing-status", "missing required field: status"));
2322
- } else if (typeof doc.status !== "string" || !ROADMAP_STATUSES.includes(doc.status)) {
2323
- violations.push(violation7("medium", "project.roadmap.invalid-status", `status must be one of ${ROADMAP_STATUSES.join(" | ")} — got ${JSON.stringify(doc.status)}`));
2203
+ tracks.forEach((track, index) => {
2204
+ if (track.worktreePath.trim() === "" || track.workingBranch.trim() === "") {
2205
+ violations.push(violation7("high", "worktree.l2.track-invalid", `track ${index + 1} is missing worktreePath and/or workingBranch`, "fill both fields for every track"));
2206
+ return;
2207
+ }
2208
+ if (!isAbsolute4(track.worktreePath)) {
2209
+ violations.push(violation7("high", "worktree.l2.track-path-relative", `track ${index + 1} worktreePath "${track.worktreePath}" is not an absolute path — L2 tracks MUST use absolute worktree checkout paths (consistent with the lease validator's absolute worktree_path enforcement)`, `use an absolute path for track ${index + 1} (e.g. /Users/<you>/worktrees/<branch>)`));
2210
+ return;
2211
+ }
2212
+ const normalized = resolve6(track.worktreePath);
2213
+ if (seenPaths.has(normalized)) {
2214
+ violations.push(violation7("high", "worktree.l2.track-path-collision", `duplicate worktreePath "${track.worktreePath}" across parallel tracks — L2 parallel-writable isolation requires a distinct absolute Worktree path per track (N parallel invokes ≠ isolation)`, "give every parallel track its own git worktree checkout"));
2215
+ return;
2216
+ }
2217
+ seenPaths.add(normalized);
2218
+ if (!existsSync6(track.worktreePath)) {
2219
+ 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}`));
2220
+ return;
2221
+ }
2222
+ const probe = probeBranch(track.worktreePath, opts);
2223
+ if ("error" in probe) {
2224
+ 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)"));
2225
+ } else if (probe.branch !== track.workingBranch) {
2226
+ 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`));
2227
+ }
2228
+ });
2229
+ return gate(violations);
2230
+ }
2231
+ function assertControlVsFeaturePath(controlWorktreePath, featureWorktreePath) {
2232
+ const violations = [];
2233
+ const samePath = controlWorktreePath === "" && featureWorktreePath === "" || controlWorktreePath !== "" && featureWorktreePath !== "" && resolve6(controlWorktreePath) === resolve6(featureWorktreePath);
2234
+ if (samePath) {
2235
+ violations.push(violation7("critical", "worktree.control-feature.same", `control worktree path equals feature/lease worktree path "${controlWorktreePath}" — execution_lease.worktree_path MUST differ from metadata.control_worktree_path`, "use a distinct feature worktree for the plan's product edits"));
2324
2236
  }
2325
- if (doc.created_at === undefined) {
2326
- violations.push(violation7("high", "project.roadmap.missing-created-at", "missing required field: created_at"));
2327
- } else if (typeof doc.created_at !== "string" || !DATE_RE3.test(doc.created_at)) {
2328
- violations.push(violation7("medium", "project.roadmap.invalid-created-at", "created_at must be YYYY-MM-DD"));
2237
+ return gate(violations);
2238
+ }
2239
+ function assertBranchAlignment(worktreePath, expectedBranch, opts = {}) {
2240
+ const violations = [];
2241
+ const probe = probeBranch(worktreePath, opts);
2242
+ if ("error" in probe) {
2243
+ violations.push(violation7("high", "worktree.branch-probe-failed", `cannot probe branch at "${worktreePath}": ${probe.error}`, "verify the path is a git worktree checkout on the expected branch (not detached)"));
2244
+ } else if (probe.branch !== expectedBranch) {
2245
+ violations.push(violation7("high", "worktree.branch-mismatch", `worktree "${worktreePath}" is on branch "${probe.branch}", expected "${expectedBranch}" (Assignment Working branch)`, `checkout ${expectedBranch} in that worktree`));
2329
2246
  }
2330
- if (doc.milestones !== undefined && doc.milestones !== null) {
2331
- if (!Array.isArray(doc.milestones)) {
2332
- violations.push(violation7("medium", "project.roadmap.invalid-milestones", "milestones must be a list of milestone names"));
2333
- } else {
2334
- for (const item of doc.milestones) {
2335
- if (typeof item !== "string" || item.trim() === "") {
2336
- violations.push(violation7("medium", "project.roadmap.invalid-milestones", "milestones items must be non-empty strings"));
2337
- break;
2338
- }
2339
- }
2247
+ return gate(violations);
2248
+ }
2249
+ function assertQcAlignment(assignments) {
2250
+ const violations = [];
2251
+ const list = assignments ?? [];
2252
+ for (const { key, label } of QC_ALIGNMENT_FIELDS) {
2253
+ const distinct = [...new Set(list.map((a) => a[key]))];
2254
+ if (distinct.length > 1) {
2255
+ 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`));
2340
2256
  }
2341
2257
  }
2342
- if (doc.residuals_ref !== undefined && doc.residuals_ref !== null) {
2343
- if (typeof doc.residuals_ref !== "string" || doc.residuals_ref.trim() === "") {
2344
- violations.push(violation7("medium", "project.roadmap.invalid-residuals-ref", "residuals_ref must be a non-empty string"));
2258
+ return gate(violations);
2259
+ }
2260
+ function singleReviewSnapshot(assignments) {
2261
+ const violations = [];
2262
+ const list = assignments ?? [];
2263
+ list.forEach((a, index) => {
2264
+ if ((a.head ?? "").trim() === "") {
2265
+ violations.push(violation7("high", "qc.alignment.snapshot-missing", `review head not provided for assignment ${index + 1} (plan_id "${a.planId}") — cannot confirm the single review snapshot precondition`, "precompute and pass the review HEAD (full SHA) for every assignment"));
2345
2266
  }
2267
+ });
2268
+ const distinct = [...new Set(list.map((a) => a.head ?? "").filter((h) => h.trim() !== ""))];
2269
+ if (distinct.length > 1) {
2270
+ violations.push(violation7("high", "qc.alignment.single-snapshot", `assignments cover ${distinct.length} different review heads (${distinct.join(", ")}) — all reviewable commits must sit on ONE Working branch HEAD before QC tri + QA`, "merge the parallel tracks to a single Working branch HEAD, then re-derive the heads"));
2346
2271
  }
2347
- const warnings = [];
2348
- const fenceEnd = linesIndexOfClosingFence(content);
2349
- const body = content.split(/\r?\n/).slice(fenceEnd + 1).join(`
2350
- `);
2351
- if (!/^##\s+Direction\s*$/m.test(body)) {
2352
- warnings.push(violation7("low", "project.roadmap.body.missing-direction", "roadmap body has no `## Direction` section (documented body convention) — state the project direction there"));
2272
+ return gate(violations);
2273
+ }
2274
+ function isDirectory2(dir) {
2275
+ try {
2276
+ return statSync4(dir).isDirectory();
2277
+ } catch {
2278
+ return false;
2353
2279
  }
2354
- if (!/^\s*[-*]\s+\[[xX ]\]/m.test(body)) {
2355
- warnings.push(violation7("low", "project.roadmap.body.no-goal-items", "roadmap body has no goal-item task list (documented body convention) — list goals as `- [ ]` / `- [x]` markdown task items"));
2280
+ }
2281
+ function isFile2(file) {
2282
+ try {
2283
+ return statSync4(file).isFile();
2284
+ } catch {
2285
+ return false;
2356
2286
  }
2357
- return { ok: violations.length === 0, violations, warnings };
2358
2287
  }
2359
- function linesIndexOfClosingFence(content) {
2360
- return content.split(/\r?\n/).indexOf("---", 1);
2288
+ function gitOut(cwd, args) {
2289
+ try {
2290
+ return execFileSync3("git", args, {
2291
+ cwd,
2292
+ encoding: "utf8",
2293
+ stdio: ["ignore", "pipe", "pipe"],
2294
+ maxBuffer: GIT_CAPTURE_MAX_BYTES
2295
+ }).trim();
2296
+ } catch {
2297
+ return null;
2298
+ }
2361
2299
  }
2362
- function validateProjectRegister(doc) {
2363
- const violations = [];
2364
- if (!isPlainObject5(doc)) {
2365
- return {
2366
- ok: false,
2367
- violations: [violation7("high", "project.register.invalid", "project register must be an object")]
2368
- };
2300
+ function probeHarnessWithStatus(root) {
2301
+ if (isFile2(join9(root, ".mstar", "status.json")))
2302
+ return join9(root, ".mstar");
2303
+ if (isFile2(join9(root, ".agents", "status.json")))
2304
+ return join9(root, ".agents");
2305
+ if (hasWorkflowSnapshot(join9(root, ".mstar")))
2306
+ return join9(root, ".mstar");
2307
+ if (hasWorkflowSnapshot(join9(root, ".agents")))
2308
+ return join9(root, ".agents");
2309
+ return null;
2310
+ }
2311
+ function hasWorkflowSnapshot(harnessDir) {
2312
+ let workflowsDir;
2313
+ try {
2314
+ workflowsDir = resolveWorkflowDir(harnessDir, { harnessDir });
2315
+ } catch {
2316
+ workflowsDir = join9(harnessDir, "workflows");
2369
2317
  }
2370
- if (doc.entries === undefined) {
2371
- violations.push(violation7("high", "project.register.missing-entries", "missing required field: entries"));
2372
- } else if (!isPlainObject5(doc.entries)) {
2373
- violations.push(violation7("high", "project.register.invalid-entries", "entries must be an object keyed by plan id"));
2374
- } else {
2375
- for (const [key, entries] of Object.entries(doc.entries)) {
2376
- if (key.trim() === "") {
2377
- violations.push(violation7("medium", "project.register.invalid-key", "entries keys must be non-empty plan ids"));
2378
- }
2379
- if (!Array.isArray(entries)) {
2380
- violations.push(violation7("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)`));
2381
- continue;
2382
- }
2383
- for (const entry of entries) {
2384
- violations.push(...validateResidual(entry).violations);
2385
- if (!isPlainObject5(entry))
2386
- continue;
2387
- validateNonEmptyString4(violations, entry.source_plan, "source_plan", "project.register.missing-source-plan", "project.register.invalid-source-plan");
2388
- if (entry.registered_at === undefined) {
2389
- violations.push(violation7("high", "project.register.missing-registered-at", "missing required field: registered_at"));
2390
- } else if (typeof entry.registered_at !== "string" || !DATE_RE3.test(entry.registered_at)) {
2391
- violations.push(violation7("medium", "project.register.invalid-registered-at", "registered_at must be YYYY-MM-DD"));
2392
- }
2393
- if (entry.lifecycle_id !== undefined && (typeof entry.lifecycle_id !== "string" || entry.lifecycle_id.trim() === "")) {
2394
- violations.push(violation7("medium", "project.register.invalid-lifecycle-id", "lifecycle_id must be a non-empty string"));
2395
- }
2396
- if (typeof entry.source_plan === "string" && entry.source_plan.trim() !== "" && entry.source_plan !== key) {
2397
- violations.push(violation7("medium", "project.register.mismatched-source-plan", `source_plan ${JSON.stringify(entry.source_plan)} does not match the entries key ${JSON.stringify(key)} — entries are keyed by plan id`));
2398
- }
2399
- }
2318
+ if (!isDirectory2(workflowsDir))
2319
+ return false;
2320
+ try {
2321
+ for (const entry of readdirSync5(workflowsDir, { withFileTypes: true })) {
2322
+ if (entry.isDirectory() && isFile2(join9(workflowsDir, entry.name, "snapshot.json")))
2323
+ return true;
2400
2324
  }
2325
+ } catch {
2326
+ return false;
2401
2327
  }
2402
- return { ok: violations.length === 0, violations };
2328
+ return false;
2403
2329
  }
2404
- function findingsCleanupGate(register, planId, opts) {
2405
- const mode = opts?.mode ?? "allow-residual";
2406
- const violations = [];
2407
- const entries = isPlainObject5(register.entries) ? register.entries[planId] : undefined;
2408
- if (entries === undefined) {
2409
- return { ok: true, violations };
2410
- }
2411
- if (!Array.isArray(entries)) {
2412
- 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)`));
2413
- return { ok: false, violations };
2330
+ function isLinkedWorktree(root) {
2331
+ const gitDirRaw = gitOut(root, ["rev-parse", "--git-dir"]);
2332
+ const commonRaw = gitOut(root, ["rev-parse", "--git-common-dir"]);
2333
+ if (gitDirRaw === null || commonRaw === null)
2334
+ return false;
2335
+ const gitDir = isAbsolute5(gitDirRaw) ? gitDirRaw : join9(root, gitDirRaw);
2336
+ const common = isAbsolute5(commonRaw) ? commonRaw : join9(root, commonRaw);
2337
+ if (gitDir.includes("/.git/worktrees/") || gitDir.includes("/worktrees/"))
2338
+ return true;
2339
+ try {
2340
+ const gdParent = realpathSync3(dirname6(gitDir));
2341
+ const cmAbs = realpathSync3(common);
2342
+ return join9(gdParent, basename3(gitDir)) !== cmAbs && gitDir !== cmAbs;
2343
+ } catch {
2344
+ return false;
2414
2345
  }
2415
- if (entries.length === 0) {
2416
- return { ok: true, violations };
2346
+ }
2347
+ function sddWorkspace(planId, opts = {}) {
2348
+ if (!planId) {
2349
+ throw new SddScriptError(`usage: mstar sdd workspace PLAN_ID [CONTROL_ROOT]
2350
+ ` + " Set MSTAR_CONTROL_ROOT=<control_worktree_path> when running from a feature worktree.", 2);
2417
2351
  }
2418
- for (const entry of entries) {
2419
- if (!isOpenResidual(entry))
2420
- continue;
2421
- const id = typeof entry.id === "string" ? entry.id : "<unnamed>";
2422
- const label = `R#${id}`;
2423
- if (mode === "zero-residual") {
2424
- if (entry.severity === "nit") {
2425
- violations.push(violation7("medium", "findings.zero-residual-nit", `${label}: style-only nits must be fixed in-session or dropped — never left open under zero-residual`));
2426
- } else if (entry.decision === "risk-accepted" || entry.lifecycle === "waived") {
2427
- violations.push(violation7("medium", "findings.zero-residual-risk-accepted", `${label}: waived/risk-accepted findings must be closed/archived, not left open under zero-residual`));
2428
- } else if (entry.decision === "defer") {
2429
- if (typeof entry.target !== "string" || entry.target.trim() === "") {
2430
- violations.push(violation7("medium", "findings.zero-residual-defer-no-target", `${label}: blocker-defer requires a target (next iteration/milestone) under zero-residual`));
2431
- }
2352
+ const cwd = opts.cwd ?? process.cwd();
2353
+ const controlRoot = opts.controlRoot ?? (process.env.MSTAR_CONTROL_ROOT || undefined);
2354
+ let root;
2355
+ if (controlRoot) {
2356
+ if (!isDirectory2(controlRoot)) {
2357
+ throw new SddScriptError(`mstar sdd workspace: CONTROL_ROOT / MSTAR_CONTROL_ROOT is not a directory: ${controlRoot}`, 1);
2358
+ }
2359
+ root = realpathSync3(controlRoot);
2360
+ } else {
2361
+ const topLevel = gitOut(cwd, ["rev-parse", "--show-toplevel"]);
2362
+ root = realpathSync3(topLevel ?? cwd);
2363
+ }
2364
+ if (!controlRoot && isLinkedWorktree(root)) {
2365
+ throw new SddScriptError(`mstar sdd workspace: linked worktree at ${root} has no {HARNESS_DIR}/status.json (default gitignore).
2366
+ ` + ` Refusing to create a second SDD tree under the feature checkout.
2367
+ ` + ` Re-run with MSTAR_CONTROL_ROOT=<control_worktree_path> or: mstar sdd workspace ${planId} <control_worktree_path>
2368
+ ` + ` See mstar-branch-worktree «Harness path SSOT under default gitignore».`, 1);
2369
+ }
2370
+ const harnessOverride = opts.harnessDir ?? (process.env.MSTAR_HARNESS_DIR || undefined);
2371
+ let harnessDir;
2372
+ if (harnessOverride) {
2373
+ harnessDir = resolve7(root, harnessOverride);
2374
+ } else {
2375
+ const rc = findMstarc(root, root);
2376
+ const rcHarnessDir = rc !== null ? parseMstarc(readFileSync7(rc, "utf8")).harnessDir : undefined;
2377
+ if (rcHarnessDir) {
2378
+ harnessDir = resolve7(rc !== null ? dirname6(rc) : root, rcHarnessDir);
2379
+ } else {
2380
+ const probed = probeHarnessWithStatus(root);
2381
+ if (probed) {
2382
+ harnessDir = probed;
2383
+ } else if (isDirectory2(join9(root, ".mstar"))) {
2384
+ harnessDir = join9(root, ".mstar");
2385
+ } else if (isDirectory2(join9(root, ".agents"))) {
2386
+ harnessDir = join9(root, ".agents");
2432
2387
  } else {
2433
- violations.push(violation7("medium", "findings.zero-residual-open-fixable", `${label}: fixable finding must not remain open under zero-residual — fix now or convert to a blocker-defer`));
2388
+ harnessDir = join9(root, ".mstar");
2434
2389
  }
2435
- } else if (normalizeSeverity(entry.severity) === "critical") {
2436
- violations.push(violation7("high", "findings.allow-residual-critical", `${label}: unresolved critical blocks Approve with residuals`));
2437
2390
  }
2438
2391
  }
2439
- return { ok: violations.length === 0, violations };
2392
+ const sddDir = resolveSddDir(harnessDir, planId);
2393
+ mkdirSync5(sddDir, { recursive: true });
2394
+ writeFileSync4(join9(sddDir, ".gitignore"), `*
2395
+ `);
2396
+ return realpathSync3(sddDir);
2440
2397
  }
2441
- function groupCount(values) {
2442
- const counts = new Map;
2443
- for (const value of values) {
2444
- const key = typeof value === "string" ? value : String(value);
2445
- counts.set(key, (counts.get(key) ?? 0) + 1);
2398
+ function taskBrief(planFile, taskN, outFile, opts = {}) {
2399
+ if (!planFile || !Number.isInteger(taskN) || taskN < 1) {
2400
+ throw new SddScriptError("usage: mstar sdd task-brief PLAN_FILE TASK_NUMBER [OUTFILE]", 2);
2446
2401
  }
2447
- return Object.fromEntries([...counts.entries()].sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0));
2448
- }
2449
- function techDebtRollup(projectDir) {
2450
- const items = [];
2451
- let entries;
2402
+ let content;
2452
2403
  try {
2453
- entries = readdirSync5(projectDir, { withFileTypes: true });
2404
+ content = readFileSync7(planFile, "utf8");
2454
2405
  } catch {
2455
- entries = [];
2406
+ throw new SddScriptError(`no such plan file: ${planFile}`, 2);
2456
2407
  }
2457
- for (const project of entries) {
2458
- if (!project.isDirectory())
2459
- continue;
2460
- const registerPath = join9(projectDir, project.name, PROJECT_REGISTER_FILE);
2461
- if (!existsSync6(registerPath))
2462
- continue;
2463
- let register;
2408
+ let out;
2409
+ if (outFile) {
2410
+ out = outFile;
2411
+ } else {
2412
+ const sddDir = opts.sddDir ?? process.env.SDD_DIR;
2413
+ if (!sddDir) {
2414
+ throw new SddScriptError("mstar sdd task-brief: set SDD_DIR or pass OUTFILE (run mstar sdd workspace PLAN_ID first)", 2);
2415
+ }
2416
+ mkdirSync5(sddDir, { recursive: true });
2417
+ out = join9(sddDir, `task-${taskN}-brief.md`);
2418
+ }
2419
+ const records = content.endsWith(`
2420
+ `) ? content.split(`
2421
+ `).slice(0, -1) : content.split(`
2422
+ `);
2423
+ const headingRe = /^#+[ \t]+Task[ \t]+[0-9]+/;
2424
+ const targetRe = new RegExp(`^#+[ ]+Task[ ]+${taskN}([^0-9]|$)`);
2425
+ let infence = false;
2426
+ let intask = false;
2427
+ const printed = [];
2428
+ for (const line of records) {
2429
+ if (/^```/.test(line))
2430
+ infence = !infence;
2431
+ if (!infence && headingRe.test(line))
2432
+ intask = targetRe.test(line);
2433
+ if (intask)
2434
+ printed.push(line);
2435
+ }
2436
+ const output = printed.length > 0 ? `${printed.join(`
2437
+ `)}
2438
+ ` : "";
2439
+ writeFileSync4(out, output);
2440
+ if (printed.length === 0) {
2441
+ throw new SddScriptError(`task ${taskN} not found in ${planFile} (no heading matching Task ${taskN})`, 3);
2442
+ }
2443
+ return out;
2444
+ }
2445
+ function reviewPackage(base, head, outFile, opts = {}) {
2446
+ if (!base || !head) {
2447
+ throw new SddScriptError("usage: mstar sdd review-package BASE HEAD [OUTFILE]", 2);
2448
+ }
2449
+ const cwd = opts.cwd ?? process.cwd();
2450
+ const verifyRef = (ref, what) => {
2464
2451
  try {
2465
- register = readJson(registerPath);
2452
+ execFileSync3("git", ["rev-parse", "--verify", "--quiet", ref], { cwd, stdio: ["ignore", "pipe", "pipe"] });
2466
2453
  } catch {
2467
- continue;
2454
+ throw new SddScriptError(`bad ${what}: ${ref}`, 2);
2468
2455
  }
2469
- if (!isPlainObject5(register) || !isPlainObject5(register.entries))
2470
- continue;
2471
- for (const [plan, planEntries] of Object.entries(register.entries)) {
2472
- if (!Array.isArray(planEntries))
2473
- continue;
2474
- for (const entry of planEntries) {
2475
- if (!isPlainObject5(entry) || !isOpenResidual(entry))
2476
- continue;
2477
- items.push({ plan, entry });
2478
- }
2456
+ };
2457
+ verifyRef(base, "BASE");
2458
+ verifyRef(head, "HEAD");
2459
+ let out;
2460
+ if (outFile) {
2461
+ out = outFile;
2462
+ } else {
2463
+ const sddDir = opts.sddDir ?? process.env.SDD_DIR;
2464
+ if (!sddDir) {
2465
+ throw new SddScriptError("mstar sdd review-package: set SDD_DIR or pass OUTFILE", 2);
2479
2466
  }
2467
+ mkdirSync5(sddDir, { recursive: true });
2468
+ const shortBase = gitOut(cwd, ["rev-parse", "--short", base]) ?? base;
2469
+ const shortHead = gitOut(cwd, ["rev-parse", "--short", head]) ?? head;
2470
+ out = join9(sddDir, `review-${shortBase}..${shortHead}.diff`);
2480
2471
  }
2481
- const bySeverity = {};
2482
- for (const severity of SEVERITY_ORDER) {
2483
- bySeverity[severity] = items.filter(({ entry }) => normalizeSeverity(entry.severity) === severity).length;
2472
+ const run = (args) => execFileSync3("git", args, { cwd, maxBuffer: GIT_CAPTURE_MAX_BYTES });
2473
+ const parts = [
2474
+ Buffer.from(`# Review package: ${base}..${head}
2475
+
2476
+ ## Commits
2477
+ `),
2478
+ run(["log", "--oneline", `${base}..${head}`]),
2479
+ Buffer.from(`
2480
+ ## Files changed
2481
+ `),
2482
+ run(["diff", "--stat", `${base}..${head}`]),
2483
+ Buffer.from(`
2484
+ ## Diff
2485
+ `),
2486
+ run(["diff", "-U10", `${base}..${head}`])
2487
+ ];
2488
+ writeFileSync4(out, Buffer.concat(parts));
2489
+ return out;
2490
+ }
2491
+ function assertBaseSha(ref, opts = {}) {
2492
+ if (typeof ref !== "string" || !/^[0-9a-f]{4,40}$/i.test(ref)) {
2493
+ throw new SddScriptError(`assertBaseSha: BASE must be a commit SHA (full or prefix); got ${JSON.stringify(ref)}. ` + "Never use HEAD~1 as review BASE (multi-commit tasks truncate).", 2);
2494
+ }
2495
+ try {
2496
+ execFileSync3("git", ["rev-parse", "--verify", "--quiet", `${ref}^{commit}`], {
2497
+ cwd: opts.cwd,
2498
+ stdio: ["ignore", "pipe", "pipe"]
2499
+ });
2500
+ } catch {
2501
+ throw new SddScriptError(`assertBaseSha: commit not found: ${ref}`, 2);
2484
2502
  }
2485
- const computed = {
2486
- total_open: items.length,
2487
- by_severity: bySeverity,
2488
- by_target: groupCount(items.map(({ entry }) => entry.target ?? "unspecified")),
2489
- by_plan: groupCount(items.map(({ plan }) => plan))
2490
- };
2491
- const stored = null;
2492
- const checks = ROLLUP_FIELDS.map((field) => ({ field, status: "DRIFT" }));
2493
- const overall = "DRIFT";
2494
- return { computed, stored, checks, overall };
2495
2503
  }
2496
- function listProjectReferenceFiles(projectDir) {
2497
- const root = join9(projectDir, PROJECT_REFERENCES_DIR);
2498
- let entries;
2504
+ function taskReportExists(sddDir, taskN) {
2505
+ try {
2506
+ const st = statSync4(join9(sddDir, `task-${taskN}-report.md`));
2507
+ return st.isFile() && st.size > 0;
2508
+ } catch {
2509
+ return false;
2510
+ }
2511
+ }
2512
+ function readProgressLedger(sddDir) {
2513
+ let content;
2499
2514
  try {
2500
- entries = readdirSync5(root, { withFileTypes: true });
2515
+ content = readFileSync7(join9(sddDir, "progress.md"), "utf8");
2501
2516
  } catch {
2502
2517
  return [];
2503
2518
  }
2504
- const files = [];
2505
- for (const entry of entries) {
2506
- if (entry.name === PROJECT_ROADMAP_FILE || entry.name === PROJECT_REGISTER_FILE)
2507
- continue;
2508
- if (entry.isFile()) {
2509
- files.push(entry.name);
2510
- } else if (entry.isDirectory()) {
2511
- let nested;
2512
- try {
2513
- nested = readdirSync5(join9(root, entry.name), { withFileTypes: true });
2514
- } catch {
2515
- continue;
2516
- }
2517
- for (const child of nested) {
2518
- if (child.isFile())
2519
- files.push(`${entry.name}/${child.name}`);
2520
- }
2521
- }
2519
+ return content.split(`
2520
+ `).map((line) => line.trim()).filter((line) => line.length > 0);
2521
+ }
2522
+ function implementerSessionStickyRules(input) {
2523
+ const { session, nextTask, microBatchTasks = 1 } = input;
2524
+ if (session.session_mode !== "sticky") {
2525
+ return { resume: false, reason: `session_mode is '${session.session_mode}'; sticky resume requires 'sticky'` };
2522
2526
  }
2523
- return files.sort();
2527
+ if (typeof session.host_agent_id !== "string" || session.host_agent_id.length === 0) {
2528
+ return {
2529
+ resume: false,
2530
+ reason: "host_agent_id is missing from implementer-session.json; fall back to fresh for this task " + "(mstar-sdd SKILL.md red flag: resume implementer without host_agent_id)"
2531
+ };
2532
+ }
2533
+ if (nextTask <= session.last_task) {
2534
+ return {
2535
+ resume: false,
2536
+ reason: `nextTask ${nextTask} <= last_task ${session.last_task}; task already completed in this session`
2537
+ };
2538
+ }
2539
+ if (microBatchTasks < 1 || microBatchTasks > 3) {
2540
+ return {
2541
+ resume: false,
2542
+ reason: `micro-batch of ${microBatchTasks} tasks is outside 1..3 (max 3 without user override, ` + "sticky-implementer-session.md § Micro-batch fallback)"
2543
+ };
2544
+ }
2545
+ return { resume: true, reason: `sticky resume OK: host_agent_id ${session.host_agent_id}, next task ${nextTask}` };
2524
2546
  }
2525
2547
  function isPlainObject6(value) {
2526
2548
  return typeof value === "object" && value !== null && !Array.isArray(value);
@@ -3006,7 +3028,7 @@ async function applyMigratePlan(plan) {
3006
3028
  const content = notes.lines.length > 0 ? `${notes.lines.join(`
3007
3029
  `)}
3008
3030
  ` : "";
3009
- writeFileSync4(filePath, content, "utf8");
3031
+ writeFileSync5(filePath, content, "utf8");
3010
3032
  }
3011
3033
  if (plan.register !== null) {
3012
3034
  const gate2 = validateProjectRegister(plan.register.data);
@@ -3022,7 +3044,7 @@ async function applyMigratePlan(plan) {
3022
3044
  if (plan.roadmap !== null) {
3023
3045
  const filePath = projectTargetOf(plan.roadmap.file);
3024
3046
  mkdirSync6(dirname7(filePath), { recursive: true });
3025
- writeFileSync4(filePath, plan.roadmap.content, "utf8");
3047
+ writeFileSync5(filePath, plan.roadmap.content, "utf8");
3026
3048
  }
3027
3049
  const rootGate = validateStatusV2(plan.rootV2.data, { harnessDir: plan.root });
3028
3050
  if (!rootGate.ok) {
@@ -3511,8 +3533,17 @@ function readPlanFileSummary(filePath) {
3511
3533
  const blocks = parseStatusBlocks(text);
3512
3534
  return { title: title.trim(), fields: blocks.length > 0 ? blocks[0].fields : new Map };
3513
3535
  }
3536
+ function extractSecurityDispositionSections(text) {
3537
+ const grab = (heading) => {
3538
+ const match = text.match(new RegExp(`(?:^|\\r?\\n)##[ \\t]+${heading}[ \\t]*\\r?\\n(?:[ \\t]*\\r?\\n)?([\\s\\S]*?)(?=\\r?\\n## |$)`, "i"));
3539
+ if (!match)
3540
+ return [];
3541
+ return match[1].split(/\r?\n/).map((line) => line.replace(/\r$/, "")).filter((line) => line.startsWith("- "));
3542
+ };
3543
+ return { needsVerification: grab("Needs verification"), hardeningChecked: grab("Hardening & checked notes") };
3544
+ }
3514
3545
  function renderIndex(params) {
3515
- const { date, repoName, repoShortSha, rows, rejected } = params;
3546
+ const { date, repoName, repoShortSha, rows, rejected, needsVerification, hardeningChecked } = params;
3516
3547
  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(`
3517
3548
  `);
3518
3549
  const directionRows = rows.filter((r) => r.category === "direction").map((r) => `- ${escapeCell(r.title)} — ${escapeCell(truncate(r.impact, 120))}`).join(`
@@ -3533,6 +3564,12 @@ function renderIndex(params) {
3533
3564
  if (directionRows !== "") {
3534
3565
  sections.push("", "## Direction", "", directionRows);
3535
3566
  }
3567
+ if (needsVerification.length > 0) {
3568
+ sections.push("", "## Needs verification", "", ...needsVerification);
3569
+ }
3570
+ if (hardeningChecked.length > 0) {
3571
+ sections.push("", "## Hardening & checked notes", "", ...hardeningChecked);
3572
+ }
3536
3573
  sections.push("", "## Execution order & status", "", "| Plan | Title | Priority | Effort | Depends on | Status |", "|------|-------|----------|--------|------------|--------|", executionRows);
3537
3574
  if (rejectedRows !== "") {
3538
3575
  sections.push("", "## Findings considered and rejected", "", rejectedRows);
@@ -3546,6 +3583,8 @@ function scaffoldAuditPlan(outDir, findings, options = {}) {
3546
3583
  const date = options.date ?? new Date().toISOString().slice(0, 10);
3547
3584
  const plannedAt = options.plannedAt ?? { commit: options.repoShortSha ?? "unknown", date };
3548
3585
  mkdirSync7(outDir, { recursive: true });
3586
+ const existingReadme = join11(outDir, "README.md");
3587
+ const carried = existsSync7(existingReadme) ? extractSecurityDispositionSections(readFileSync9(existingReadme, "utf8")) : { needsVerification: [], hardeningChecked: [] };
3549
3588
  const existing = readdirSync7(outDir).filter((f) => /^\d{3}-.*\.md$/.test(f));
3550
3589
  let next = existing.reduce((max, f) => Math.max(max, Number(f.slice(0, 3))), 0) + 1;
3551
3590
  const written = [];
@@ -3561,7 +3600,7 @@ function scaffoldAuditPlan(outDir, findings, options = {}) {
3561
3600
  }
3562
3601
  usedSlugs.add(slug);
3563
3602
  const file = `${num}-${slug}.md`;
3564
- writeFileSync5(join11(outDir, file), renderPlanFile(finding, plannedAt));
3603
+ writeFileSync6(join11(outDir, file), renderPlanFile(finding, plannedAt));
3565
3604
  written.push(file);
3566
3605
  next++;
3567
3606
  }
@@ -3599,12 +3638,16 @@ function scaffoldAuditPlan(outDir, findings, options = {}) {
3599
3638
  row.dependsOn = finding.dependsOn ?? "none";
3600
3639
  }
3601
3640
  });
3602
- writeFileSync5(join11(outDir, "README.md"), renderIndex({
3641
+ const needsVerificationLines = options.needsVerification !== undefined ? options.needsVerification.map((nv) => `- ${escapeCell(nv.lead)}: ${escapeCell(nv.how)}${nv.evidence ? ` (${escapeCell(nv.evidence)})` : ""}`) : carried.needsVerification;
3642
+ const hardeningCheckedLines = options.hardeningChecked !== undefined ? options.hardeningChecked.map((hc) => `- ${hc.kind}: ${escapeCell(hc.text)}`) : carried.hardeningChecked;
3643
+ writeFileSync6(join11(outDir, "README.md"), renderIndex({
3603
3644
  date,
3604
3645
  repoName: options.repoName ?? "repo",
3605
3646
  repoShortSha: options.repoShortSha ?? "unknown",
3606
3647
  rows,
3607
- rejected: options.rejected ?? []
3648
+ rejected: options.rejected ?? [],
3649
+ needsVerification: needsVerificationLines,
3650
+ hardeningChecked: hardeningCheckedLines
3608
3651
  }));
3609
3652
  return { outDir: resolve9(outDir), date, files: written, nextNumber: next };
3610
3653
  }
@@ -4443,7 +4486,19 @@ function lintFiveQuestion(bodyText, mode = "authoring") {
4443
4486
  function resolveAssetPath(skillName, relPath, host) {
4444
4487
  return `skill \`${skillName}\` → ${relPath} (${resolveSkillRoot(host, { skill: skillName, rel: relPath })})`;
4445
4488
  }
4446
- var SEVERITY_ORDER, MSTARC_FILE = ".mstarc", MSTARC_SECTION = "config", MSTARC_HARNESS_DIR_KEY = "harness_dir", MSTARC_PLAN_DIR_KEY = "plan_dir", MSTARC_SDD_DIR_KEY = "sdd_dir", MSTARC_ITERATION_DIR_KEY = "iteration_dir", MSTARC_KNOWLEDGE_DIR_KEY = "knowledge_dir", MSTARC_SPECS_DIR_KEY = "specs_dir", MSTARC_WORKFLOW_DIR_KEY = "workflow_dir", MSTARC_PROJECT_DIR_KEY = "project_dir", MSTARC_ENFORCEMENT_KEY = "enforcement", CONFIG_KEYS, EMPTY_STATUS_TEMPLATE, SCAFFOLD_DIRS, GITIGNORE_SNIPPET = `# Morning Star harness (.mstar/)
4489
+ var SEVERITY_ORDER, MSTARC_FILE = ".mstarc", MSTARC_SECTION = "config", MSTARC_HARNESS_DIR_KEY = "harness_dir", MSTARC_PLAN_DIR_KEY = "plan_dir", MSTARC_SDD_DIR_KEY = "sdd_dir", MSTARC_ITERATION_DIR_KEY = "iteration_dir", MSTARC_KNOWLEDGE_DIR_KEY = "knowledge_dir", MSTARC_SPECS_DIR_KEY = "specs_dir", MSTARC_WORKFLOW_DIR_KEY = "workflow_dir", MSTARC_PROJECT_DIR_KEY = "project_dir", MSTARC_ENFORCEMENT_KEY = "enforcement", CONFIG_KEYS, COMPASS_STATUSES, DATE_RE, PLAN_STATUS_DONE = "Done", COMPASS_FILE = "delivery-compass.md", INDEX_README = "README.md", INDEX_HEADER = "| Iteration | Path | Description | Status |", DATE_PART, RFC3339_Z_RE, DATE_ONLY_RE, STATUS_WRITE_LOCKDIR = ".status-write.lockdir", LOCKDIR_HOLDER_PID = "holder.pid", heldLockDirs, BRANCH_FORMS_HINT = '"Working branch: <existing>" | "Working branch: create <new> from <base>" | "Branch policy: direct on <branch> — <reason>"', REQUIRED_FIELDS, ASSIGNMENT_ENFORCEMENT_BOLD_RE, ASSIGNMENT_ENFORCEMENT_PLAIN_RE, COMPASS_ENFORCEMENT_RE, ASSIGNMENT_BODY_START_RE, ASSIGNMENT_HEADING_RE, ASSIGNMENT_FIELD_RE, WORKFLOW_SNAPSHOT_FILE = "snapshot.json", WORKFLOW_LIFECYCLE_STATUSES, WORKFLOW_TERMINAL_STATUSES, WORKFLOW_LIFECYCLE_TYPES, DATE_RE2, PLAN_STATUSES, RESIDUAL_DECISIONS, RESIDUAL_LIFECYCLES, validateStatus, PROJECT_ROADMAP_FILE = "roadmap.md", PROJECT_REFERENCES_DIR = "references", PROJECT_REGISTER_FILE = "residuals.json", _DEFAULT_PROJECT = "_default", ROADMAP_STATUSES, DATE_RE3, ROLLUP_FIELDS, EMPTY_STATUS_TEMPLATE, SCAFFOLD_DIRS, ROADMAP_TEMPLATE = `---
4490
+ project_id: _default
4491
+ title: Default Project
4492
+ status: active
4493
+ created_at: {created_at}
4494
+ ---
4495
+
4496
+ # Roadmap
4497
+
4498
+ ## Direction
4499
+
4500
+ State the project direction here.
4501
+ `, EMPTY_REGISTER_TEMPLATE, GITIGNORE_SNIPPET = `# Morning Star harness (.mstar/)
4447
4502
  # Principle: process stays local; results are shared with the team.
4448
4503
  # Default-ignore everything under .mstar/, then re-include the tracked results.
4449
4504
  .mstar/**
@@ -4462,7 +4517,7 @@ var SEVERITY_ORDER, MSTARC_FILE = ".mstarc", MSTARC_SECTION = "config", MSTARC_H
4462
4517
  !.agents/knowledge/**
4463
4518
  !.agents/specs/
4464
4519
  !.agents/specs/**
4465
- `, GITIGNORE_PROCESS_ENTRIES, GITIGNORE_PROCESS_ENTRIES_AGENTS, DATE_PART, RFC3339_Z_RE, DATE_ONLY_RE, STATUS_WRITE_LOCKDIR = ".status-write.lockdir", LOCKDIR_HOLDER_PID = "holder.pid", heldLockDirs, BRANCH_FORMS_HINT = '"Working branch: <existing>" | "Working branch: create <new> from <base>" | "Branch policy: direct on <branch> — <reason>"', REQUIRED_FIELDS, ASSIGNMENT_ENFORCEMENT_BOLD_RE, ASSIGNMENT_ENFORCEMENT_PLAIN_RE, COMPASS_ENFORCEMENT_RE, ASSIGNMENT_BODY_START_RE, ASSIGNMENT_HEADING_RE, ASSIGNMENT_FIELD_RE, WORKFLOW_SNAPSHOT_FILE = "snapshot.json", WORKFLOW_LIFECYCLE_STATUSES, WORKFLOW_TERMINAL_STATUSES, WORKFLOW_LIFECYCLE_TYPES, DATE_RE, PLAN_STATUSES, RESIDUAL_DECISIONS, RESIDUAL_LIFECYCLES, validateStatus, DEFAULT_PROBE_TIMEOUT_MS = 1e4, QC_ALIGNMENT_FIELDS, SddScriptError, GIT_CAPTURE_MAX_BYTES, COMPASS_STATUSES, DATE_RE2, PLAN_STATUS_DONE = "Done", COMPASS_FILE = "delivery-compass.md", INDEX_README = "README.md", INDEX_HEADER = "| Iteration | Path | Description | Status |", PROJECT_ROADMAP_FILE = "roadmap.md", PROJECT_REFERENCES_DIR = "references", PROJECT_REGISTER_FILE = "residuals.json", _DEFAULT_PROJECT = "_default", ROADMAP_STATUSES, DATE_RE3, ROLLUP_FIELDS, MIGRATE_STATUS_FILE = "status.json", ARCHIVED_STATUS_V1_FILE = "archived/status.v1.json", NOTES_LEDGER_FILE = "notes.jsonl", DATE_RE4, ROOT_METADATA_LIFT_KEYS, RAW_GROUP = "__raw", isMap = (v) => typeof v === "object" && v !== null && !Array.isArray(v), HEX_RE, OKLCH_RE, PX_RE, PLACEHOLDER_RE, TYPOGRAPHY_PROPS, REF_RE, REF_GROUPS, isPlaceholder = (value) => PLACEHOLDER_RE.test(value), PARITY_GROUPS, GRAY_STEPS, ALPHA_STEPS, ACCENT_SCALES, L3_BODY_ITEM_IDS, LEVEL_RANK, AUDIT_PRIORITIES, AUDIT_EFFORTS, AUDIT_RISKS, AUDIT_CATEGORIES, AUDIT_STATUS_FIELDS, escapeCell = (value) => value.replace(/\|/g, "\\|"), truncate = (value, max) => value.length > max ? `${value.slice(0, max)}…` : value, KNOWLEDGE_REQUIRED_FIELDS, KNOWLEDGE_PROBLEM_TYPES, KNOWLEDGE_BUG_PROBLEM_TYPES, KNOWLEDGE_KNOWLEDGE_PROBLEM_TYPES, KNOWLEDGE_SEVERITIES, KNOWLEDGE_RESOLUTION_TYPES, KNOWLEDGE_CATEGORY_MAP, DATE_RE5, isMap2 = (v) => typeof v === "object" && v !== null && !Array.isArray(v), REF_EXT_RE, SYMBOL_REF_RE, SCHEME_RE, LINE_SUFFIX_RE, ANCHOR_RE, WALK_SKIP_DIRS, MAX_WALK_FILES = 5000, COMMENT_INTRODUCER = "(?:\\/\\/|\\/\\*|#|;|--|\\s\\*)", REMOVAL_PATH_PATTERNS, TASK_ARTIFACT_RE, SDD_DEEPLINK_RE, TEST_FILE_PATH_RE, TEST_FILE_PHRASE_RE, COMMAND_PROMPT_RE, RUNNER_RE, OUTPUT_TOKEN_RE, PLACEHOLDER_TOKEN_RE, ELLIPSIS_RE, NEGATION_RE, WORKFLOW_VERB_START_RE, PRONOUN_RE, DESCRIPTION_MAX_WORDS = 120, REQUIRED_STRATEGY_SECTIONS, ROLE_MAPPING, SHARED_FAMILIES, DEV_TRACK_PARAMS, QC_REVIEWER_PARAMS, LOAD_ORDER_HEADING_RE, FIVE_QUESTION_SECTIONS, HEADING_RE, RUNTIME_HEADING_ALIASES;
4520
+ `, GITIGNORE_PROCESS_ENTRIES, GITIGNORE_PROCESS_ENTRIES_AGENTS, DEFAULT_PROBE_TIMEOUT_MS = 1e4, QC_ALIGNMENT_FIELDS, SddScriptError, GIT_CAPTURE_MAX_BYTES, MIGRATE_STATUS_FILE = "status.json", ARCHIVED_STATUS_V1_FILE = "archived/status.v1.json", NOTES_LEDGER_FILE = "notes.jsonl", DATE_RE4, ROOT_METADATA_LIFT_KEYS, RAW_GROUP = "__raw", isMap = (v) => typeof v === "object" && v !== null && !Array.isArray(v), HEX_RE, OKLCH_RE, PX_RE, PLACEHOLDER_RE, TYPOGRAPHY_PROPS, REF_RE, REF_GROUPS, isPlaceholder = (value) => PLACEHOLDER_RE.test(value), PARITY_GROUPS, GRAY_STEPS, ALPHA_STEPS, ACCENT_SCALES, L3_BODY_ITEM_IDS, LEVEL_RANK, AUDIT_PRIORITIES, AUDIT_EFFORTS, AUDIT_RISKS, AUDIT_CATEGORIES, AUDIT_STATUS_FIELDS, escapeCell = (value) => value.replace(/\|/g, "\\|"), truncate = (value, max) => value.length > max ? `${value.slice(0, max)}…` : value, KNOWLEDGE_REQUIRED_FIELDS, KNOWLEDGE_PROBLEM_TYPES, KNOWLEDGE_BUG_PROBLEM_TYPES, KNOWLEDGE_KNOWLEDGE_PROBLEM_TYPES, KNOWLEDGE_SEVERITIES, KNOWLEDGE_RESOLUTION_TYPES, KNOWLEDGE_CATEGORY_MAP, DATE_RE5, isMap2 = (v) => typeof v === "object" && v !== null && !Array.isArray(v), REF_EXT_RE, SYMBOL_REF_RE, SCHEME_RE, LINE_SUFFIX_RE, ANCHOR_RE, WALK_SKIP_DIRS, MAX_WALK_FILES = 5000, COMMENT_INTRODUCER = "(?:\\/\\/|\\/\\*|#|;|--|\\s\\*)", REMOVAL_PATH_PATTERNS, TASK_ARTIFACT_RE, SDD_DEEPLINK_RE, TEST_FILE_PATH_RE, TEST_FILE_PHRASE_RE, COMMAND_PROMPT_RE, RUNNER_RE, OUTPUT_TOKEN_RE, PLACEHOLDER_TOKEN_RE, ELLIPSIS_RE, NEGATION_RE, WORKFLOW_VERB_START_RE, PRONOUN_RE, DESCRIPTION_MAX_WORDS = 120, REQUIRED_STRATEGY_SECTIONS, ROLE_MAPPING, SHARED_FAMILIES, DEV_TRACK_PARAMS, QC_REVIEWER_PARAMS, LOAD_ORDER_HEADING_RE, FIVE_QUESTION_SECTIONS, HEADING_RE, RUNTIME_HEADING_ALIASES;
4466
4521
  var init_engine = __esm(() => {
4467
4522
  SEVERITY_ORDER = ["critical", "high", "medium", "low", "nit"];
4468
4523
  CONFIG_KEYS = {
@@ -4476,16 +4531,8 @@ var init_engine = __esm(() => {
4476
4531
  [MSTARC_PROJECT_DIR_KEY]: "projectDir",
4477
4532
  [MSTARC_ENFORCEMENT_KEY]: "enforcement"
4478
4533
  };
4479
- EMPTY_STATUS_TEMPLATE = {
4480
- version: 2,
4481
- updated_at: "1970-01-01",
4482
- workflows: []
4483
- };
4484
- SCAFFOLD_DIRS = ["plans", "iterations", "knowledge", "specs", "sdd"];
4485
- GITIGNORE_PROCESS_ENTRIES = GITIGNORE_SNIPPET.split(`
4486
- `).filter((line) => line.startsWith(".mstar/") || line.startsWith("!.mstar/")).map((line) => line.trim());
4487
- GITIGNORE_PROCESS_ENTRIES_AGENTS = GITIGNORE_SNIPPET_AGENTS.split(`
4488
- `).filter((line) => line.startsWith(".agents/") || line.startsWith("!.agents/")).map((line) => line.trim());
4534
+ COMPASS_STATUSES = ["active", "locked", "completed"];
4535
+ DATE_RE = /^\d{4}-\d{2}-\d{2}$/;
4489
4536
  DATE_PART = String.raw`\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12]\d|3[01])`;
4490
4537
  RFC3339_Z_RE = new RegExp(String.raw`^${DATE_PART}T\d{2}:\d{2}:\d{2}(\.\d+)?Z$`);
4491
4538
  DATE_ONLY_RE = new RegExp(String.raw`^${DATE_PART}$`);
@@ -4504,11 +4551,27 @@ var init_engine = __esm(() => {
4504
4551
  WORKFLOW_LIFECYCLE_STATUSES = ["running", "paused", "completed", "failed", "stopped"];
4505
4552
  WORKFLOW_TERMINAL_STATUSES = ["completed", "failed", "stopped"];
4506
4553
  WORKFLOW_LIFECYCLE_TYPES = ["plan", "iteration"];
4507
- DATE_RE = /^\d{4}-\d{2}-\d{2}$/;
4554
+ DATE_RE2 = /^\d{4}-\d{2}-\d{2}$/;
4508
4555
  PLAN_STATUSES = ["Todo", "InProgress", "InReview", "Blocked", "Done"];
4509
4556
  RESIDUAL_DECISIONS = ["defer", "accept", "risk-accepted"];
4510
4557
  RESIDUAL_LIFECYCLES = ["open", "resolved", "waived", "superseded", "duplicate"];
4511
4558
  validateStatus = validateStatusV2;
4559
+ ROADMAP_STATUSES = ["active", "paused", "completed"];
4560
+ DATE_RE3 = /^\d{4}-\d{2}-\d{2}$/;
4561
+ ROLLUP_FIELDS = ["total_open", "by_severity", "by_target", "by_plan"];
4562
+ EMPTY_STATUS_TEMPLATE = {
4563
+ version: 2,
4564
+ updated_at: "1970-01-01",
4565
+ workflows: []
4566
+ };
4567
+ SCAFFOLD_DIRS = ["plans", "iterations", "knowledge", "specs", "sdd"];
4568
+ EMPTY_REGISTER_TEMPLATE = {
4569
+ entries: {}
4570
+ };
4571
+ GITIGNORE_PROCESS_ENTRIES = GITIGNORE_SNIPPET.split(`
4572
+ `).filter((line) => line.startsWith(".mstar/") || line.startsWith("!.mstar/")).map((line) => line.trim());
4573
+ GITIGNORE_PROCESS_ENTRIES_AGENTS = GITIGNORE_SNIPPET_AGENTS.split(`
4574
+ `).filter((line) => line.startsWith(".agents/") || line.startsWith("!.agents/")).map((line) => line.trim());
4512
4575
  QC_ALIGNMENT_FIELDS = [
4513
4576
  { key: "planId", label: "plan_id" },
4514
4577
  { key: "reviewRange", label: "Review range" },
@@ -4523,11 +4586,6 @@ var init_engine = __esm(() => {
4523
4586
  }
4524
4587
  };
4525
4588
  GIT_CAPTURE_MAX_BYTES = 64 * 1024 * 1024;
4526
- COMPASS_STATUSES = ["active", "locked", "completed"];
4527
- DATE_RE2 = /^\d{4}-\d{2}-\d{2}$/;
4528
- ROADMAP_STATUSES = ["active", "paused", "completed"];
4529
- DATE_RE3 = /^\d{4}-\d{2}-\d{2}$/;
4530
- ROLLUP_FIELDS = ["total_open", "by_severity", "by_target", "by_plan"];
4531
4589
  DATE_RE4 = /^\d{4}-\d{2}-\d{2}$/;
4532
4590
  ROOT_METADATA_LIFT_KEYS = {
4533
4591
  plan_parallelism: true,