@mstar-harness/engine 3.7.0 → 3.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/engine.js CHANGED
@@ -2357,8 +2357,8 @@ function hasFiles(dir) {
2357
2357
  }
2358
2358
  // src/worktree.ts
2359
2359
  import { execFileSync as execFileSync2 } from "node:child_process";
2360
- import { existsSync as existsSync7 } from "node:fs";
2361
- import { isAbsolute as isAbsolute5, resolve as resolve8 } from "node:path";
2360
+ import { existsSync as existsSync7, realpathSync as realpathSync3 } from "node:fs";
2361
+ import { isAbsolute as isAbsolute5, join as join10, resolve as resolve8 } from "node:path";
2362
2362
  var DEFAULT_PROBE_TIMEOUT_MS = 1e4;
2363
2363
  function probeTimeoutMs() {
2364
2364
  const raw = process.env.MSTAR_GIT_PROBE_TIMEOUT_MS;
@@ -2397,6 +2397,51 @@ function probeBranch(worktreePath, opts) {
2397
2397
  return { error: detail };
2398
2398
  }
2399
2399
  }
2400
+ function probeCheckout(worktreePath, opts) {
2401
+ const timeout = opts.timeoutMs ?? probeTimeoutMs();
2402
+ try {
2403
+ const stdout = execFileSync2(opts.gitPath ?? "git", ["-C", worktreePath, "rev-parse", "--git-dir"], {
2404
+ encoding: "utf8",
2405
+ stdio: ["ignore", "pipe", "pipe"],
2406
+ timeout
2407
+ });
2408
+ const raw = stdout.trim();
2409
+ if (raw === "")
2410
+ return { error: `no git dir reported at "${worktreePath}"` };
2411
+ const abs = isAbsolute5(raw) ? raw : join10(worktreePath, raw);
2412
+ return { gitDir: realpathSync3(abs) };
2413
+ } catch (err) {
2414
+ const e = err;
2415
+ if (e.killed === true || e.signal !== undefined) {
2416
+ return { error: `git probe timed out after ${timeout}ms (killed by ${e.signal ?? "SIGTERM"})` };
2417
+ }
2418
+ const detail = (e.stderr !== undefined ? e.stderr.toString().trim() : "") || e.message || "git probe failed";
2419
+ return { error: detail };
2420
+ }
2421
+ }
2422
+ function isDistinctCheckout(controlPath, candidatePath, opts = {}) {
2423
+ const control = probeCheckout(controlPath, opts);
2424
+ const candidate = probeCheckout(candidatePath, opts);
2425
+ if ("error" in control || "error" in candidate)
2426
+ return false;
2427
+ return control.gitDir !== candidate.gitDir;
2428
+ }
2429
+ function probeCheckoutRoot(path, opts = {}) {
2430
+ const timeout = opts.timeoutMs ?? probeTimeoutMs();
2431
+ try {
2432
+ const stdout = execFileSync2(opts.gitPath ?? "git", ["-C", path, "rev-parse", "--show-toplevel"], {
2433
+ encoding: "utf8",
2434
+ stdio: ["ignore", "pipe", "pipe"],
2435
+ timeout
2436
+ });
2437
+ const raw = stdout.trim();
2438
+ if (raw === "")
2439
+ return null;
2440
+ return realpathSync3(raw);
2441
+ } catch {
2442
+ return null;
2443
+ }
2444
+ }
2400
2445
  function l1PreDispatchCheck(input, opts = {}) {
2401
2446
  const violations = [];
2402
2447
  const { controlWorktreePath, leaseWorktreePath, leaseWorkingBranch, planId } = input;
@@ -2409,8 +2454,20 @@ function l1PreDispatchCheck(input, opts = {}) {
2409
2454
  if (leaseWorkingBranch.trim() === "") {
2410
2455
  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"));
2411
2456
  }
2412
- if (controlWorktreePath !== "" && leaseWorktreePath !== "" && resolve8(controlWorktreePath) === resolve8(leaseWorktreePath)) {
2413
- 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"));
2457
+ if (controlWorktreePath !== "" && leaseWorktreePath !== "") {
2458
+ if (resolve8(controlWorktreePath) === resolve8(leaseWorktreePath)) {
2459
+ 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"));
2460
+ } else if (existsSync7(leaseWorktreePath)) {
2461
+ const controlProbe = probeCheckout(controlWorktreePath, opts);
2462
+ const leaseProbe = probeCheckout(leaseWorktreePath, opts);
2463
+ if ("error" in controlProbe) {
2464
+ violations.push(violation7("high", "worktree.l1.checkout-probe-failed", `cannot establish the lease worktree "${leaseWorktreePath}" is a distinct Git checkout from the control worktree "${controlWorktreePath}" for plan "${planId}": ${controlProbe.error}`, "verify the control worktree path is a git checkout (integration-branch checkout)"));
2465
+ } else if ("error" in leaseProbe) {
2466
+ violations.push(violation7("high", "worktree.l1.checkout-probe-failed", `cannot establish the lease worktree "${leaseWorktreePath}" is a distinct Git checkout from the control worktree "${controlWorktreePath}" for plan "${planId}": ${leaseProbe.error}`, "verify the lease worktree path is a git checkout (git worktree add <path> <branch>)"));
2467
+ } else if (controlProbe.gitDir === leaseProbe.gitDir) {
2468
+ violations.push(violation7("critical", "worktree.l1.lease-equals-control", `execution_lease.worktree_path "${leaseWorktreePath}" is the same Git checkout as metadata.control_worktree_path "${controlWorktreePath}" — a plain subdirectory or symlink alias of the control checkout is not isolation; the feature worktree MUST be a distinct checkout`, "use a distinct feature worktree for the plan (git worktree add <path> <branch>) and update the lease"));
2469
+ }
2470
+ }
2414
2471
  }
2415
2472
  if (leaseWorktreePath !== "" && !existsSync7(leaseWorktreePath)) {
2416
2473
  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>`));
@@ -2459,11 +2516,17 @@ function l2PreDispatchCheck(input, opts = {}) {
2459
2516
  });
2460
2517
  return gate(violations);
2461
2518
  }
2462
- function assertControlVsFeaturePath(controlWorktreePath, featureWorktreePath) {
2519
+ function assertControlVsFeaturePath(controlWorktreePath, featureWorktreePath, opts = {}) {
2463
2520
  const violations = [];
2464
- const samePath = controlWorktreePath === "" && featureWorktreePath === "" || controlWorktreePath !== "" && featureWorktreePath !== "" && resolve8(controlWorktreePath) === resolve8(featureWorktreePath);
2465
- if (samePath) {
2466
- 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"));
2521
+ if (controlWorktreePath === "" && featureWorktreePath === "") {
2522
+ violations.push(violation7("critical", "worktree.control-feature.same", `control worktree path and feature/lease worktree path are both empty — execution_lease.worktree_path MUST differ from metadata.control_worktree_path`, "record a distinct feature worktree path"));
2523
+ return gate(violations);
2524
+ }
2525
+ if (controlWorktreePath === "" || featureWorktreePath === "") {
2526
+ return gate(violations);
2527
+ }
2528
+ if (!isDistinctCheckout(controlWorktreePath, featureWorktreePath, opts)) {
2529
+ violations.push(violation7("critical", "worktree.control-feature.same", `control worktree path "${controlWorktreePath}" and feature/lease worktree path "${featureWorktreePath}" are not distinct Git checkouts — a plain subdirectory or symlink alias of the control checkout is not isolation; execution_lease.worktree_path MUST be a distinct checkout`, "use a distinct feature worktree for the plan's product edits (git worktree add <path> <branch>)"));
2467
2530
  }
2468
2531
  return gate(violations);
2469
2532
  }
@@ -2509,9 +2572,9 @@ function singleReviewSnapshot(assignments) {
2509
2572
  }
2510
2573
  // src/sdd.ts
2511
2574
  import { execFileSync as execFileSync3, spawn } from "node:child_process";
2512
- import { mkdirSync as mkdirSync6, readdirSync as readdirSync6, readFileSync as readFileSync7, realpathSync as realpathSync3, statSync as statSync4, writeFileSync as writeFileSync4 } from "node:fs";
2575
+ import { mkdirSync as mkdirSync6, readdirSync as readdirSync6, readFileSync as readFileSync7, realpathSync as realpathSync4, statSync as statSync4, writeFileSync as writeFileSync4 } from "node:fs";
2513
2576
  import { constants as osConstants } from "node:os";
2514
- import { basename as basename4, dirname as dirname6, isAbsolute as isAbsolute6, join as join10, relative as relative3, resolve as resolve9 } from "node:path";
2577
+ import { basename as basename4, dirname as dirname6, isAbsolute as isAbsolute6, join as join11, relative as relative3, resolve as resolve9 } from "node:path";
2515
2578
  class SddScriptError extends Error {
2516
2579
  exitCode;
2517
2580
  constructor(message, exitCode) {
@@ -2548,14 +2611,14 @@ function gitOut(cwd, args) {
2548
2611
  }
2549
2612
  }
2550
2613
  function probeHarnessWithStatus(root) {
2551
- if (isFile2(join10(root, ".mstar", "status.json")))
2552
- return join10(root, ".mstar");
2553
- if (isFile2(join10(root, ".agents", "status.json")))
2554
- return join10(root, ".agents");
2555
- if (hasWorkflowSnapshot(join10(root, ".mstar")))
2556
- return join10(root, ".mstar");
2557
- if (hasWorkflowSnapshot(join10(root, ".agents")))
2558
- return join10(root, ".agents");
2614
+ if (isFile2(join11(root, ".mstar", "status.json")))
2615
+ return join11(root, ".mstar");
2616
+ if (isFile2(join11(root, ".agents", "status.json")))
2617
+ return join11(root, ".agents");
2618
+ if (hasWorkflowSnapshot(join11(root, ".mstar")))
2619
+ return join11(root, ".mstar");
2620
+ if (hasWorkflowSnapshot(join11(root, ".agents")))
2621
+ return join11(root, ".agents");
2559
2622
  return null;
2560
2623
  }
2561
2624
  function hasWorkflowSnapshot(harnessDir) {
@@ -2563,13 +2626,13 @@ function hasWorkflowSnapshot(harnessDir) {
2563
2626
  try {
2564
2627
  workflowsDir = resolveWorkflowDir(harnessDir, { harnessDir });
2565
2628
  } catch {
2566
- workflowsDir = join10(harnessDir, "workflows");
2629
+ workflowsDir = join11(harnessDir, "workflows");
2567
2630
  }
2568
2631
  if (!isDirectory2(workflowsDir))
2569
2632
  return false;
2570
2633
  try {
2571
2634
  for (const entry of readdirSync6(workflowsDir, { withFileTypes: true })) {
2572
- if (entry.isDirectory() && isFile2(join10(workflowsDir, entry.name, "snapshot.json")))
2635
+ if (entry.isDirectory() && isFile2(join11(workflowsDir, entry.name, "snapshot.json")))
2573
2636
  return true;
2574
2637
  }
2575
2638
  } catch {
@@ -2582,14 +2645,14 @@ function isLinkedWorktree(root) {
2582
2645
  const commonRaw = gitOut(root, ["rev-parse", "--git-common-dir"]);
2583
2646
  if (gitDirRaw === null || commonRaw === null)
2584
2647
  return false;
2585
- const gitDir = isAbsolute6(gitDirRaw) ? gitDirRaw : join10(root, gitDirRaw);
2586
- const common = isAbsolute6(commonRaw) ? commonRaw : join10(root, commonRaw);
2648
+ const gitDir = isAbsolute6(gitDirRaw) ? gitDirRaw : join11(root, gitDirRaw);
2649
+ const common = isAbsolute6(commonRaw) ? commonRaw : join11(root, commonRaw);
2587
2650
  if (gitDir.includes("/.git/worktrees/") || gitDir.includes("/worktrees/"))
2588
2651
  return true;
2589
2652
  try {
2590
- const gdParent = realpathSync3(dirname6(gitDir));
2591
- const cmAbs = realpathSync3(common);
2592
- return join10(gdParent, basename4(gitDir)) !== cmAbs && gitDir !== cmAbs;
2653
+ const gdParent = realpathSync4(dirname6(gitDir));
2654
+ const cmAbs = realpathSync4(common);
2655
+ return join11(gdParent, basename4(gitDir)) !== cmAbs && gitDir !== cmAbs;
2593
2656
  } catch {
2594
2657
  return false;
2595
2658
  }
@@ -2606,10 +2669,10 @@ function sddWorkspace(planId, opts = {}) {
2606
2669
  if (!isDirectory2(controlRoot)) {
2607
2670
  throw new SddScriptError(`mstar sdd workspace: CONTROL_ROOT / MSTAR_CONTROL_ROOT is not a directory: ${controlRoot}`, 1);
2608
2671
  }
2609
- root = realpathSync3(controlRoot);
2672
+ root = realpathSync4(controlRoot);
2610
2673
  } else {
2611
2674
  const topLevel = gitOut(cwd, ["rev-parse", "--show-toplevel"]);
2612
- root = realpathSync3(topLevel ?? cwd);
2675
+ root = realpathSync4(topLevel ?? cwd);
2613
2676
  }
2614
2677
  if (!controlRoot && isLinkedWorktree(root)) {
2615
2678
  throw new SddScriptError(`mstar sdd workspace: linked worktree at ${root} has no {HARNESS_DIR}/status.json (default gitignore).
@@ -2630,20 +2693,20 @@ function sddWorkspace(planId, opts = {}) {
2630
2693
  const probed = probeHarnessWithStatus(root);
2631
2694
  if (probed) {
2632
2695
  harnessDir = probed;
2633
- } else if (isDirectory2(join10(root, ".mstar"))) {
2634
- harnessDir = join10(root, ".mstar");
2635
- } else if (isDirectory2(join10(root, ".agents"))) {
2636
- harnessDir = join10(root, ".agents");
2696
+ } else if (isDirectory2(join11(root, ".mstar"))) {
2697
+ harnessDir = join11(root, ".mstar");
2698
+ } else if (isDirectory2(join11(root, ".agents"))) {
2699
+ harnessDir = join11(root, ".agents");
2637
2700
  } else {
2638
- harnessDir = join10(root, ".mstar");
2701
+ harnessDir = join11(root, ".mstar");
2639
2702
  }
2640
2703
  }
2641
2704
  }
2642
2705
  const sddDir = resolveSddDir(harnessDir, planId);
2643
2706
  mkdirSync6(sddDir, { recursive: true });
2644
- writeFileSync4(join10(sddDir, ".gitignore"), `*
2707
+ writeFileSync4(join11(sddDir, ".gitignore"), `*
2645
2708
  `);
2646
- return realpathSync3(sddDir);
2709
+ return realpathSync4(sddDir);
2647
2710
  }
2648
2711
  function taskBrief(planFile, taskN, outFile, opts = {}) {
2649
2712
  if (!planFile || !Number.isInteger(taskN) || taskN < 1) {
@@ -2670,7 +2733,7 @@ function taskBrief(planFile, taskN, outFile, opts = {}) {
2670
2733
  if (outFile) {
2671
2734
  out = bound ? resolve9(observedCwd, outFile) : outFile;
2672
2735
  } else if (bound) {
2673
- out = join10(bound.sddDir, `task-${taskN}-brief.md`);
2736
+ out = join11(bound.sddDir, `task-${taskN}-brief.md`);
2674
2737
  mkdirAfterGate = bound.sddDir;
2675
2738
  } else {
2676
2739
  const sddDir = opts.sddDir ?? process.env.SDD_DIR;
@@ -2678,7 +2741,7 @@ function taskBrief(planFile, taskN, outFile, opts = {}) {
2678
2741
  throw new SddScriptError("mstar sdd task-brief: set SDD_DIR or pass OUTFILE (run mstar sdd workspace PLAN_ID first)", 2);
2679
2742
  }
2680
2743
  mkdirSync6(sddDir, { recursive: true });
2681
- out = join10(sddDir, `task-${taskN}-brief.md`);
2744
+ out = join11(sddDir, `task-${taskN}-brief.md`);
2682
2745
  }
2683
2746
  if (bound) {
2684
2747
  const gate2 = checkSddAction(bound, { kind: "artifact", cwd: observedCwd, target: out });
@@ -2736,7 +2799,7 @@ function reviewPackage(base, head, outFile, opts = {}) {
2736
2799
  } else if (bound) {
2737
2800
  const shortBase = gitOut(cwd, ["rev-parse", "--short", base]) ?? base;
2738
2801
  const shortHead = gitOut(cwd, ["rev-parse", "--short", head]) ?? head;
2739
- out = join10(bound.sddDir, `review-${shortBase}..${shortHead}.diff`);
2802
+ out = join11(bound.sddDir, `review-${shortBase}..${shortHead}.diff`);
2740
2803
  mkdirAfterGate = bound.sddDir;
2741
2804
  } else {
2742
2805
  const sddDir = opts.sddDir ?? process.env.SDD_DIR;
@@ -2746,7 +2809,7 @@ function reviewPackage(base, head, outFile, opts = {}) {
2746
2809
  mkdirSync6(sddDir, { recursive: true });
2747
2810
  const shortBase = gitOut(cwd, ["rev-parse", "--short", base]) ?? base;
2748
2811
  const shortHead = gitOut(cwd, ["rev-parse", "--short", head]) ?? head;
2749
- out = join10(sddDir, `review-${shortBase}..${shortHead}.diff`);
2812
+ out = join11(sddDir, `review-${shortBase}..${shortHead}.diff`);
2750
2813
  }
2751
2814
  if (bound) {
2752
2815
  const gate2 = checkSddAction(bound, { kind: "artifact", cwd: observedCwd, target: out });
@@ -2789,7 +2852,7 @@ function assertBaseSha(ref, opts = {}) {
2789
2852
  }
2790
2853
  function taskReportExists(sddDir, taskN) {
2791
2854
  try {
2792
- const st = statSync4(join10(sddDir, `task-${taskN}-report.md`));
2855
+ const st = statSync4(join11(sddDir, `task-${taskN}-report.md`));
2793
2856
  return st.isFile() && st.size > 0;
2794
2857
  } catch {
2795
2858
  return false;
@@ -2798,7 +2861,7 @@ function taskReportExists(sddDir, taskN) {
2798
2861
  function readProgressLedger(sddDir) {
2799
2862
  let content;
2800
2863
  try {
2801
- content = readFileSync7(join10(sddDir, "progress.md"), "utf8");
2864
+ content = readFileSync7(join11(sddDir, "progress.md"), "utf8");
2802
2865
  } catch {
2803
2866
  return [];
2804
2867
  }
@@ -2839,7 +2902,7 @@ function isInside(child, ancestor) {
2839
2902
  }
2840
2903
  function canonicalDir(path) {
2841
2904
  try {
2842
- return statSync4(path).isDirectory() ? realpathSync3(path) : null;
2905
+ return statSync4(path).isDirectory() ? realpathSync4(path) : null;
2843
2906
  } catch {
2844
2907
  return null;
2845
2908
  }
@@ -2857,7 +2920,7 @@ function throwUsage(message) {
2857
2920
  throw new SddScriptError(message, 2);
2858
2921
  }
2859
2922
  function readActiveWorkflowIds(controlHarnessRoot) {
2860
- const statusPath = join10(controlHarnessRoot, "status.json");
2923
+ const statusPath = join11(controlHarnessRoot, "status.json");
2861
2924
  if (!isFile2(statusPath))
2862
2925
  return null;
2863
2926
  let doc;
@@ -2892,7 +2955,7 @@ function findWorkflowPlanRow(controlHarnessRoot, planId) {
2892
2955
  }
2893
2956
  const matches = [];
2894
2957
  for (const id of workflowIds) {
2895
- const snapshotPath = join10(workflowsDir, id, WORKFLOW_SNAPSHOT_FILE);
2958
+ const snapshotPath = join11(workflowsDir, id, WORKFLOW_SNAPSHOT_FILE);
2896
2959
  if (!isFile2(snapshotPath))
2897
2960
  continue;
2898
2961
  let doc;
@@ -2984,15 +3047,20 @@ function resolveSddExecutionContext(input) {
2984
3047
  contextViolation("high", "sdd.context.feature-cwd-missing", `featureCwd "${input.featureCwd}" does not exist or is not a directory — the feature worktree is the required cwd for product edits`, `create the feature worktree first (git worktree add ${input.featureCwd} ${workingBranch})`)
2985
3048
  ]);
2986
3049
  }
2987
- const controlCheckout = dirname6(canonicalControlHarnessRoot);
2988
- if (isInside(canonicalFeatureCwd, controlCheckout)) {
3050
+ if (isInside(canonicalControlHarnessRoot, canonicalFeatureCwd)) {
2989
3051
  throwGateFail([
2990
- contextViolation("critical", "sdd.context.feature-in-control", `featureCwd "${canonicalFeatureCwd}" is inside the control checkout "${controlCheckout}" — product edits never land in the control checkout (execution_lease.worktree_path MUST differ from metadata.control_worktree_path)`, "use a distinct feature worktree for the plan")
3052
+ contextViolation("critical", "sdd.context.control-inside-feature", `controlHarnessRoot "${canonicalControlHarnessRoot}" is inside featureCwd "${canonicalFeatureCwd}" — a feature worktree's same-looking {HARNESS_DIR} is not the SSOT; the control harness must live outside the feature checkout`)
2991
3053
  ]);
2992
3054
  }
2993
- if (isInside(canonicalControlHarnessRoot, canonicalFeatureCwd)) {
3055
+ const controlCheckout = probeCheckoutRoot(canonicalControlHarnessRoot);
3056
+ if (controlCheckout === null) {
2994
3057
  throwGateFail([
2995
- contextViolation("critical", "sdd.context.control-inside-feature", `controlHarnessRoot "${canonicalControlHarnessRoot}" is inside featureCwd "${canonicalFeatureCwd}"a feature worktree's same-looking {HARNESS_DIR} is not the SSOT; the control harness must live outside the feature checkout`)
3058
+ contextViolation("high", "sdd.context.control-root-unresolvable", `cannot resolve the control checkout root for controlHarnessRoot "${canonicalControlHarnessRoot}" (git rev-parse --show-toplevel failed)the control harness must live inside a git checkout`, "verify the control harness root is inside the control worktree checkout")
3059
+ ]);
3060
+ }
3061
+ if (isInside(canonicalFeatureCwd, controlCheckout) && !isDistinctCheckout(canonicalControlHarnessRoot, canonicalFeatureCwd)) {
3062
+ throwGateFail([
3063
+ contextViolation("critical", "sdd.context.feature-in-control", `featureCwd "${canonicalFeatureCwd}" is inside the control checkout "${controlCheckout}" and is not a distinct Git checkout — a plain subdirectory or symlink alias of the control checkout is not isolation; product edits never land in the control checkout (execution_lease.worktree_path MUST be a distinct checkout)`, "use a distinct feature worktree for the plan (git worktree add <path> <branch>)")
2996
3064
  ]);
2997
3065
  }
2998
3066
  const match = findWorkflowPlanRow(canonicalControlHarnessRoot, planId);
@@ -3037,7 +3105,7 @@ function resolveSddExecutionContext(input) {
3037
3105
  controlHarnessRoot: canonicalControlHarnessRoot,
3038
3106
  featureCwd: canonicalFeatureCwd,
3039
3107
  workingBranch,
3040
- planFile: realpathSync3(input.planFile),
3108
+ planFile: realpathSync4(input.planFile),
3041
3109
  sddDir: canonicalSddDir
3042
3110
  };
3043
3111
  }
@@ -3177,9 +3245,183 @@ async function runInSddContext(context, argv) {
3177
3245
  });
3178
3246
  });
3179
3247
  }
3248
+ // src/gates.ts
3249
+ import { existsSync as existsSync8, statSync as statSync5 } from "node:fs";
3250
+ import { basename as basename5, dirname as dirname7, join as join12, relative as relative4, resolve as resolve10 } from "node:path";
3251
+ var STATUS_FILE = "status.json";
3252
+ var SNAPSHOT_FILE = "snapshot.json";
3253
+ var REGISTER_FILE = "residuals.json";
3254
+ function eventTargetPaths(input) {
3255
+ if (typeof input !== "object" || input === null)
3256
+ return [];
3257
+ const record = input;
3258
+ const paths = [];
3259
+ const push = (value) => {
3260
+ if (typeof value === "string" && value.trim() !== "")
3261
+ paths.push(value);
3262
+ };
3263
+ push(record.path);
3264
+ if (Array.isArray(record.paths)) {
3265
+ for (const value of record.paths)
3266
+ push(value);
3267
+ }
3268
+ return paths;
3269
+ }
3270
+ function hasEntry(dir, name) {
3271
+ try {
3272
+ statSync5(join12(dir, name));
3273
+ return true;
3274
+ } catch {
3275
+ return false;
3276
+ }
3277
+ }
3278
+ function hasHarnessRootMarkers(dir) {
3279
+ if (!hasEntry(dir, STATUS_FILE))
3280
+ return false;
3281
+ if (hasEntry(dir, "workflows") && hasEntry(dir, "projects"))
3282
+ return true;
3283
+ try {
3284
+ return hasEntry(resolveWorkflowDir(dir, { harnessDir: dir }), "") && hasEntry(resolveProjectDir(dir, { harnessDir: dir }), "");
3285
+ } catch {
3286
+ return false;
3287
+ }
3288
+ }
3289
+ function resolveHarnessRootOf(target) {
3290
+ let dir = resolve10(target);
3291
+ for (;; ) {
3292
+ if (hasHarnessRootMarkers(dir))
3293
+ return dir;
3294
+ const parent = dirname7(dir);
3295
+ if (parent === dir)
3296
+ return null;
3297
+ dir = parent;
3298
+ }
3299
+ }
3300
+ function harnessDocKindOfTarget(targetPath) {
3301
+ if (typeof targetPath !== "string" || targetPath.trim() === "")
3302
+ return null;
3303
+ const resolved = resolve10(targetPath);
3304
+ const name = basename5(resolved);
3305
+ if (name !== STATUS_FILE && name !== SNAPSHOT_FILE && name !== REGISTER_FILE)
3306
+ return null;
3307
+ const classify = (harnessDir2) => {
3308
+ const rel = relative4(harnessDir2, resolved);
3309
+ if (name === STATUS_FILE && rel === STATUS_FILE)
3310
+ return { harnessDir: harnessDir2, kind: "status" };
3311
+ let workflowDir;
3312
+ let projectDir;
3313
+ try {
3314
+ workflowDir = resolveWorkflowDir(harnessDir2, { harnessDir: harnessDir2 });
3315
+ projectDir = resolveProjectDir(harnessDir2, { harnessDir: harnessDir2 });
3316
+ } catch {
3317
+ workflowDir = join12(harnessDir2, "workflows");
3318
+ projectDir = join12(harnessDir2, "projects");
3319
+ }
3320
+ if (name === SNAPSHOT_FILE && /^[^/]+\/snapshot\.json$/.test(relative4(workflowDir, resolved))) {
3321
+ return { harnessDir: harnessDir2, kind: "snapshot" };
3322
+ }
3323
+ if (name === REGISTER_FILE && /^[^/]+\/residuals\.json$/.test(relative4(projectDir, resolved))) {
3324
+ return { harnessDir: harnessDir2, kind: "register" };
3325
+ }
3326
+ return null;
3327
+ };
3328
+ const probeRoot = resolveHarnessRootOf(dirname7(resolved));
3329
+ const harnessDir = probeRoot ?? resolveHarnessDir(dirname7(resolved));
3330
+ if (harnessDir === null)
3331
+ return null;
3332
+ const classified = classify(harnessDir);
3333
+ if (classified !== null)
3334
+ return classified;
3335
+ if (probeRoot === null)
3336
+ return null;
3337
+ const fallbackDir = resolveHarnessDir(dirname7(resolved));
3338
+ if (fallbackDir === null || fallbackDir === probeRoot)
3339
+ return null;
3340
+ return classify(fallbackDir);
3341
+ }
3342
+ function violationLine(violation8) {
3343
+ return `[${violation8.severity}] ${violation8.code}: ${violation8.message}${violation8.fix ? ` (fix: ${violation8.fix})` : ""}`;
3344
+ }
3345
+ var MAX_STATUS_CONTENT_LENGTH = 2 * 1024 * 1024;
3346
+ function oversizedViolation(filePath) {
3347
+ return {
3348
+ ok: false,
3349
+ severity: "high",
3350
+ code: "status.oversized",
3351
+ message: `${basename5(filePath)} exceeds the ${MAX_STATUS_CONTENT_LENGTH}-byte (2 MiB) coordination-document validation budget — repair out of band or disable for this session with MSTAR_WRITE_GATE=off`
3352
+ };
3353
+ }
3354
+ function validateStatusWriteDoc(content, filePath, kind, options = {}) {
3355
+ const oversized = options.oversized ?? "pass";
3356
+ if (typeof content === "string") {
3357
+ if (content.length > MAX_STATUS_CONTENT_LENGTH) {
3358
+ return oversized === "violate" ? [oversizedViolation(filePath)] : [];
3359
+ }
3360
+ let doc2;
3361
+ try {
3362
+ doc2 = JSON.parse(content);
3363
+ } catch (error) {
3364
+ return [
3365
+ {
3366
+ ok: false,
3367
+ severity: "high",
3368
+ code: "status.invalid-json",
3369
+ message: error.message
3370
+ }
3371
+ ];
3372
+ }
3373
+ if (doc2 === null || typeof doc2 !== "object" || Array.isArray(doc2)) {
3374
+ return [
3375
+ {
3376
+ ok: false,
3377
+ severity: "high",
3378
+ code: "status.invalid-json",
3379
+ message: `${basename5(filePath)} content must be a JSON object`
3380
+ }
3381
+ ];
3382
+ }
3383
+ return validateDocByKind(doc2, kind);
3384
+ }
3385
+ if (!existsSync8(filePath))
3386
+ return [];
3387
+ try {
3388
+ if (statSync5(filePath).size > MAX_STATUS_CONTENT_LENGTH) {
3389
+ return oversized === "violate" ? [oversizedViolation(filePath)] : [];
3390
+ }
3391
+ } catch {
3392
+ return [];
3393
+ }
3394
+ if (kind === "status")
3395
+ return validateStatus(filePath).violations;
3396
+ let doc;
3397
+ try {
3398
+ doc = readJson(filePath);
3399
+ } catch (error) {
3400
+ return [
3401
+ {
3402
+ ok: false,
3403
+ severity: "high",
3404
+ code: "status.invalid-json",
3405
+ message: error.message
3406
+ }
3407
+ ];
3408
+ }
3409
+ return validateDocByKind(doc, kind);
3410
+ }
3411
+ function validateDocByKind(doc, kind) {
3412
+ if (kind === "snapshot")
3413
+ return validateWorkflowSnapshot(doc).violations;
3414
+ if (kind === "register")
3415
+ return validateProjectRegister(doc).violations;
3416
+ return validateStatus(doc).violations;
3417
+ }
3418
+ function formatStatusWriteBlockReason(violations, skillPointer) {
3419
+ return violations.map((v) => `${violationLine(v)} (${skillPointer})`).join(`
3420
+ `);
3421
+ }
3180
3422
  // src/migrate.ts
3181
3423
  import { copyFileSync, mkdirSync as mkdirSync7, readFileSync as readFileSync8, readdirSync as readdirSync7, writeFileSync as writeFileSync5 } from "node:fs";
3182
- import { dirname as dirname7, isAbsolute as isAbsolute7, join as join11, relative as relative4, resolve as resolve10, sep as sep2 } from "node:path";
3424
+ import { dirname as dirname8, isAbsolute as isAbsolute7, join as join13, relative as relative5, resolve as resolve11, sep as sep2 } from "node:path";
3183
3425
  var MIGRATE_STATUS_FILE = "status.json";
3184
3426
  var ARCHIVED_STATUS_V1_FILE = "archived/status.v1.json";
3185
3427
  var NOTES_LEDGER_FILE = "notes.jsonl";
@@ -3219,7 +3461,7 @@ function todayString3() {
3219
3461
  return `${now.getFullYear()}-${month}-${day}`;
3220
3462
  }
3221
3463
  function scanCompasses(harnessDir) {
3222
- const iterationsDir = join11(harnessDir, "iterations");
3464
+ const iterationsDir = join13(harnessDir, "iterations");
3223
3465
  const out = [];
3224
3466
  let entries;
3225
3467
  try {
@@ -3230,7 +3472,7 @@ function scanCompasses(harnessDir) {
3230
3472
  for (const entry of entries) {
3231
3473
  if (!entry.isDirectory())
3232
3474
  continue;
3233
- const compassPath = join11(iterationsDir, entry.name, "delivery-compass.md");
3475
+ const compassPath = join13(iterationsDir, entry.name, "delivery-compass.md");
3234
3476
  let content;
3235
3477
  try {
3236
3478
  content = readFileSync8(compassPath, "utf8");
@@ -3310,8 +3552,8 @@ function buildIterationSnapshot(compass, rowById, rootUpdatedAt) {
3310
3552
  id: compass.id,
3311
3553
  type: "iteration",
3312
3554
  status,
3313
- file: join11("workflows", compass.id, WORKFLOW_SNAPSHOT_FILE),
3314
- source: join11("iterations", compass.id, "delivery-compass.md"),
3555
+ file: join13("workflows", compass.id, WORKFLOW_SNAPSHOT_FILE),
3556
+ source: join13("iterations", compass.id, "delivery-compass.md"),
3315
3557
  data: snapshot
3316
3558
  };
3317
3559
  }
@@ -3346,7 +3588,7 @@ function buildStandaloneSnapshot(row, rootUpdatedAt, migrationNotes) {
3346
3588
  id,
3347
3589
  type: "plan",
3348
3590
  status,
3349
- file: join11("workflows", id, WORKFLOW_SNAPSHOT_FILE),
3591
+ file: join13("workflows", id, WORKFLOW_SNAPSHOT_FILE),
3350
3592
  source: "status.json plans[] row",
3351
3593
  data: snapshot
3352
3594
  };
@@ -3454,7 +3696,7 @@ function buildRegister(residualFindings, byPlan, projectId, migratedAt) {
3454
3696
  return null;
3455
3697
  const doc = { entries };
3456
3698
  return {
3457
- file: join11("projects", projectId, PROJECT_REGISTER_FILE),
3699
+ file: join13("projects", projectId, PROJECT_REGISTER_FILE),
3458
3700
  source: "status.json residual_findings",
3459
3701
  data: doc
3460
3702
  };
@@ -3485,7 +3727,7 @@ function collectNotesFiles(snapshots) {
3485
3727
  if (lines.length === 0)
3486
3728
  continue;
3487
3729
  out.push({
3488
- file: join11(dirname7(snapshot.file), NOTES_LEDGER_FILE),
3730
+ file: join13(dirname8(snapshot.file), NOTES_LEDGER_FILE),
3489
3731
  source,
3490
3732
  lines
3491
3733
  });
@@ -3493,11 +3735,11 @@ function collectNotesFiles(snapshots) {
3493
3735
  return out;
3494
3736
  }
3495
3737
  function migrateHarnessTree(root, opts = {}) {
3496
- const harnessDir = resolve10(root);
3738
+ const harnessDir = resolve11(root);
3497
3739
  const workflowDir = resolveWorkflowDir(harnessDir, { harnessDir });
3498
3740
  const projectDir = resolveProjectDir(harnessDir, { harnessDir });
3499
3741
  const projectId = opts.projectId ?? _DEFAULT_PROJECT;
3500
- const statusPath = join11(harnessDir, MIGRATE_STATUS_FILE);
3742
+ const statusPath = join13(harnessDir, MIGRATE_STATUS_FILE);
3501
3743
  const legacy = readJson(statusPath);
3502
3744
  if (legacy.version === 2) {
3503
3745
  const updatedAt = typeof legacy.updated_at === "string" && legacy.updated_at !== "" ? legacy.updated_at : "1970-01-01";
@@ -3593,7 +3835,7 @@ function migrateHarnessTree(root, opts = {}) {
3593
3835
  migrationNotes.push(`roadmap title sanitized for frontmatter (line breaks replaced with spaces): ${JSON.stringify(rawTitle)}`);
3594
3836
  }
3595
3837
  roadmap = {
3596
- file: join11("projects", projectId, PROJECT_ROADMAP_FILE),
3838
+ file: join13("projects", projectId, PROJECT_ROADMAP_FILE),
3597
3839
  source: "status.json metadata.program_roadmap",
3598
3840
  content: buildRoadmap({ ...programRoadmap, title: sanitizedTitle }, projectId, migratedAt)
3599
3841
  };
@@ -3642,19 +3884,19 @@ async function applyMigratePlan(plan) {
3642
3884
  if (plan.dryRun) {
3643
3885
  return { applied: false, message: `dry-run: ${plan.steps.length} steps planned (source → destination), zero writes` };
3644
3886
  }
3645
- const statusPath = join11(plan.root, MIGRATE_STATUS_FILE);
3887
+ const statusPath = join13(plan.root, MIGRATE_STATUS_FILE);
3646
3888
  const current = readJson(statusPath);
3647
3889
  if (current.version === 2) {
3648
3890
  return { applied: false, message: "no-op: status.json already at schema version 2 (migrated) — nothing to do" };
3649
3891
  }
3650
- const harnessRoot = resolve10(plan.root);
3651
- const workflowRoot = resolve10(plan.workflowDir);
3652
- const projectRoot = resolve10(plan.projectDir);
3892
+ const harnessRoot = resolve11(plan.root);
3893
+ const workflowRoot = resolve11(plan.workflowDir);
3894
+ const projectRoot = resolve11(plan.projectDir);
3653
3895
  if (!isAbsolute7(plan.workflowDir) || !isAbsolute7(plan.projectDir)) {
3654
3896
  throw new Error(`refusing to apply migration: plan workflowDir/projectDir must be absolute (got ${JSON.stringify(plan.workflowDir)} / ${JSON.stringify(plan.projectDir)})`);
3655
3897
  }
3656
- const workflowTargetOf = (canonicalFile) => join11(workflowRoot, relative4("workflows", canonicalFile));
3657
- const projectTargetOf = (canonicalFile) => join11(projectRoot, relative4("projects", canonicalFile));
3898
+ const workflowTargetOf = (canonicalFile) => join13(workflowRoot, relative5("workflows", canonicalFile));
3899
+ const projectTargetOf = (canonicalFile) => join13(projectRoot, relative5("projects", canonicalFile));
3658
3900
  const allDestinations = [
3659
3901
  plan.archive.file,
3660
3902
  ...plan.snapshots.map((snapshot) => snapshot.file),
@@ -3663,20 +3905,20 @@ async function applyMigratePlan(plan) {
3663
3905
  ...plan.roadmap !== null ? [plan.roadmap.file] : []
3664
3906
  ];
3665
3907
  for (const destination of allDestinations) {
3666
- const resolvedDest = resolve10(join11(plan.root, destination));
3908
+ const resolvedDest = resolve11(join13(plan.root, destination));
3667
3909
  const inside = (dir) => resolvedDest === dir || resolvedDest.startsWith(`${dir}${sep2}`);
3668
3910
  if (!inside(harnessRoot) && !inside(workflowRoot) && !inside(projectRoot)) {
3669
3911
  throw new Error(`refusing to apply migration: destination escapes the harness dir (${JSON.stringify(destination)}) — every write must stay under ${JSON.stringify(plan.root)}, the workflow dir (${JSON.stringify(plan.workflowDir)}) or the project dir (${JSON.stringify(plan.projectDir)})`);
3670
3912
  }
3671
3913
  }
3672
- mkdirSync7(join11(plan.root, dirname7(plan.archive.file)), { recursive: true });
3673
- copyFileSync(statusPath, join11(plan.root, plan.archive.file));
3914
+ mkdirSync7(join13(plan.root, dirname8(plan.archive.file)), { recursive: true });
3915
+ copyFileSync(statusPath, join13(plan.root, plan.archive.file));
3674
3916
  for (const snapshot of plan.snapshots) {
3675
- await writeWorkflowSnapshot(snapshot.data, dirname7(workflowTargetOf(snapshot.file)));
3917
+ await writeWorkflowSnapshot(snapshot.data, dirname8(workflowTargetOf(snapshot.file)));
3676
3918
  }
3677
3919
  for (const notes of plan.notesFiles) {
3678
3920
  const filePath = workflowTargetOf(notes.file);
3679
- mkdirSync7(dirname7(filePath), { recursive: true });
3921
+ mkdirSync7(dirname8(filePath), { recursive: true });
3680
3922
  const content = notes.lines.length > 0 ? `${notes.lines.join(`
3681
3923
  `)}
3682
3924
  ` : "";
@@ -3689,13 +3931,13 @@ async function applyMigratePlan(plan) {
3689
3931
  }
3690
3932
  if (Object.keys(plan.register.data.entries ?? {}).length > 0) {
3691
3933
  const filePath = projectTargetOf(plan.register.file);
3692
- mkdirSync7(dirname7(filePath), { recursive: true });
3934
+ mkdirSync7(dirname8(filePath), { recursive: true });
3693
3935
  writeJson(filePath, plan.register.data);
3694
3936
  }
3695
3937
  }
3696
3938
  if (plan.roadmap !== null) {
3697
3939
  const filePath = projectTargetOf(plan.roadmap.file);
3698
- mkdirSync7(dirname7(filePath), { recursive: true });
3940
+ mkdirSync7(dirname8(filePath), { recursive: true });
3699
3941
  writeFileSync5(filePath, plan.roadmap.content, "utf8");
3700
3942
  }
3701
3943
  const rootGate = validateStatusV2(plan.rootV2.data, { harnessDir: plan.root });
@@ -4119,8 +4361,8 @@ function completenessLevel(frontmatterText, checklist) {
4119
4361
  }
4120
4362
  // src/audit.ts
4121
4363
  import { execFileSync as execFileSync4 } from "node:child_process";
4122
- import { existsSync as existsSync8, mkdirSync as mkdirSync8, readdirSync as readdirSync8, readFileSync as readFileSync9, rmdirSync as rmdirSync2, rmSync, writeFileSync as writeFileSync6 } from "node:fs";
4123
- import { basename as basename5, join as join12, resolve as resolve11, sep as sep3 } from "node:path";
4364
+ import { existsSync as existsSync9, mkdirSync as mkdirSync8, readdirSync as readdirSync8, readFileSync as readFileSync9, rmdirSync as rmdirSync2, rmSync, writeFileSync as writeFileSync6 } from "node:fs";
4365
+ import { basename as basename6, join as join14, resolve as resolve12, sep as sep3 } from "node:path";
4124
4366
  function violation9(severity, code, message, fix) {
4125
4367
  return { ok: false, severity, code, message, fix };
4126
4368
  }
@@ -4421,7 +4663,7 @@ function scanSecrets(files) {
4421
4663
  unreadableFiles++;
4422
4664
  continue;
4423
4665
  }
4424
- const base = basename5(file);
4666
+ const base = basename6(file);
4425
4667
  for (const entry of NEVER_COMMIT_FILENAMES) {
4426
4668
  if (entry.re.test(base))
4427
4669
  findings.push({ file, line: 1, type: entry.type });
@@ -4478,7 +4720,7 @@ function rootLockfiles(root) {
4478
4720
  return [];
4479
4721
  }
4480
4722
  const names = new Set(LOCKFILE_NAMES);
4481
- const present = entries.filter((entry) => entry.isFile() && names.has(entry.name)).map((entry) => join12(root, entry.name));
4723
+ const present = entries.filter((entry) => entry.isFile() && names.has(entry.name)).map((entry) => join14(root, entry.name));
4482
4724
  if (present.length === 0)
4483
4725
  return [];
4484
4726
  try {
@@ -4487,7 +4729,7 @@ function rootLockfiles(root) {
4487
4729
  encoding: "utf8",
4488
4730
  stdio: ["ignore", "pipe", "ignore"]
4489
4731
  }).split("\x00").filter((f) => f !== ""));
4490
- return present.filter((p) => tracked.has(basename5(p)));
4732
+ return present.filter((p) => tracked.has(basename6(p)));
4491
4733
  } catch {
4492
4734
  return present;
4493
4735
  }
@@ -4503,7 +4745,7 @@ function supplyChainChecks(repoRoot) {
4503
4745
  findings.push({ kind: "lockfile-duplicate", file: lockfiles.map((f) => f.replace(`${repoRoot}/`, "")).join(", ") });
4504
4746
  violations.push(violation9("medium", "audit.supply.lockfile-duplicate", `multiple lockfiles at ${repoRoot}: ${lockfiles.join(", ")}`, "keep exactly one lockfile per package manager"));
4505
4747
  }
4506
- const workflowsDir = join12(repoRoot, ".github", "workflows");
4748
+ const workflowsDir = join14(repoRoot, ".github", "workflows");
4507
4749
  let wfEntries = [];
4508
4750
  try {
4509
4751
  wfEntries = readdirSync8(workflowsDir, { withFileTypes: true });
@@ -4513,7 +4755,7 @@ function supplyChainChecks(repoRoot) {
4513
4755
  for (const entry of wfEntries) {
4514
4756
  if (!entry.isFile() || !/\.(?:ya?ml)$/.test(entry.name))
4515
4757
  continue;
4516
- const wfPath = join12(workflowsDir, entry.name);
4758
+ const wfPath = join14(workflowsDir, entry.name);
4517
4759
  const relPath = `.github/workflows/${entry.name}`;
4518
4760
  let text;
4519
4761
  try {
@@ -4670,8 +4912,8 @@ function scaffoldAuditPlan(outDir, findings, options = {}) {
4670
4912
  const date = options.date ?? new Date().toISOString().slice(0, 10);
4671
4913
  const plannedAt = options.plannedAt ?? { commit: options.repoShortSha ?? "unknown", date };
4672
4914
  mkdirSync8(outDir, { recursive: true });
4673
- const existingReadme = join12(outDir, "README.md");
4674
- const carried = existsSync8(existingReadme) ? extractSecurityDispositionSections(readFileSync9(existingReadme, "utf8")) : { needsVerification: [], hardeningChecked: [] };
4915
+ const existingReadme = join14(outDir, "README.md");
4916
+ const carried = existsSync9(existingReadme) ? extractSecurityDispositionSections(readFileSync9(existingReadme, "utf8")) : { needsVerification: [], hardeningChecked: [] };
4675
4917
  const existing = readdirSync8(outDir).filter((f) => /^\d{3}-.*\.md$/.test(f));
4676
4918
  let next = existing.reduce((max, f) => Math.max(max, Number(f.slice(0, 3))), 0) + 1;
4677
4919
  const redactedFindings = findings.map(redactFinding);
@@ -4688,13 +4930,13 @@ function scaffoldAuditPlan(outDir, findings, options = {}) {
4688
4930
  }
4689
4931
  usedSlugs.add(slug);
4690
4932
  const file = `${num}-${slug}.md`;
4691
- writeFileSync6(join12(outDir, file), renderPlanFile(finding, plannedAt));
4933
+ writeFileSync6(join14(outDir, file), renderPlanFile(finding, plannedAt));
4692
4934
  written.push(file);
4693
4935
  next++;
4694
4936
  }
4695
4937
  const all = [...existing, ...written].sort();
4696
4938
  const rows = all.map((file) => {
4697
- const summary = readPlanFileSummary(join12(outDir, file));
4939
+ const summary = readPlanFileSummary(join14(outDir, file));
4698
4940
  const fields = summary.fields;
4699
4941
  return {
4700
4942
  num: file.slice(0, 3),
@@ -4728,7 +4970,7 @@ function scaffoldAuditPlan(outDir, findings, options = {}) {
4728
4970
  });
4729
4971
  const needsVerificationLines = options.needsVerification !== undefined ? options.needsVerification.map((nv) => `- ${escapeCell(redactText(nv.lead))}: ${escapeCell(redactText(nv.how))}${nv.evidence ? ` (${escapeCell(redactText(nv.evidence))})` : ""}`) : carried.needsVerification;
4730
4972
  const hardeningCheckedLines = options.hardeningChecked !== undefined ? options.hardeningChecked.map((hc) => `- ${hc.kind}: ${escapeCell(redactText(hc.text))}`) : carried.hardeningChecked;
4731
- writeFileSync6(join12(outDir, "README.md"), renderIndex({
4973
+ writeFileSync6(join14(outDir, "README.md"), renderIndex({
4732
4974
  date,
4733
4975
  repoName: options.repoName ?? "repo",
4734
4976
  repoShortSha: options.repoShortSha ?? "unknown",
@@ -4737,7 +4979,7 @@ function scaffoldAuditPlan(outDir, findings, options = {}) {
4737
4979
  needsVerification: needsVerificationLines,
4738
4980
  hardeningChecked: hardeningCheckedLines
4739
4981
  }));
4740
- return { outDir: resolve11(outDir), date, files: written, nextNumber: next };
4982
+ return { outDir: resolve12(outDir), date, files: written, nextNumber: next };
4741
4983
  }
4742
4984
  async function promoteAuditPlans(outDir, selected, options) {
4743
4985
  if (selected.length === 0) {
@@ -4746,19 +4988,19 @@ async function promoteAuditPlans(outDir, selected, options) {
4746
4988
  if (typeof options.harnessDir !== "string" || options.harnessDir.trim() === "") {
4747
4989
  throw new Error("promoteAuditPlans: options.harnessDir is required (must contain status.json + workflows/)");
4748
4990
  }
4749
- const workflowId = options.workflowId ?? basename5(resolve11(outDir));
4991
+ const workflowId = options.workflowId ?? basename6(resolve12(outDir));
4750
4992
  assertSafePathComponent(workflowId, "workflow id");
4751
- const harnessDir = resolve11(options.harnessDir);
4752
- const statusPath = join12(harnessDir, "status.json");
4753
- const workflowDir = join12(harnessDir, "workflows", workflowId);
4754
- const snapshotPath = join12(workflowDir, WORKFLOW_SNAPSHOT_FILE);
4993
+ const harnessDir = resolve12(options.harnessDir);
4994
+ const statusPath = join14(harnessDir, "status.json");
4995
+ const workflowDir = join14(harnessDir, "workflows", workflowId);
4996
+ const snapshotPath = join14(workflowDir, WORKFLOW_SNAPSHOT_FILE);
4755
4997
  const planFiles = resolveSelectedPlanFiles(outDir, selected);
4756
4998
  const indexRows = readExecutionOrderIndex(outDir);
4757
4999
  const plans = planFiles.map((planFile) => {
4758
5000
  const stem = planFile.replace(/\.md$/, "");
4759
5001
  const num = stem.slice(0, 3);
4760
5002
  const indexRow = indexRows.get(num);
4761
- const title = indexRow?.title ?? readPlanFileSummary(join12(outDir, planFile)).title;
5003
+ const title = indexRow?.title ?? readPlanFileSummary(join14(outDir, planFile)).title;
4762
5004
  return {
4763
5005
  id: stem,
4764
5006
  title,
@@ -4787,7 +5029,7 @@ async function promoteAuditPlans(outDir, selected, options) {
4787
5029
  throw new Error(`refusing to register invalid workflow entry: ${entryGate.violations.map((v) => v.message).join("; ")}`);
4788
5030
  }
4789
5031
  await withStatusWriteLock(statusPath, async () => {
4790
- if (existsSync8(snapshotPath)) {
5032
+ if (existsSync9(snapshotPath)) {
4791
5033
  throw new Error(`refusing to promote audit plans: workflow ${JSON.stringify(workflowId)} already exists ` + `(snapshot at ${snapshotPath}) — re-promote would drop its registered plan rows; ` + `remove that workflow before promoting again`);
4792
5034
  }
4793
5035
  mkdirSync8(workflowDir, { recursive: true });
@@ -4823,7 +5065,7 @@ function resolveSelectedPlanFiles(outDir, selected) {
4823
5065
  for (const id of selected) {
4824
5066
  const file = byNum.get(id) ?? byStem.get(id) ?? byStem.get(id.replace(/\.md$/, ""));
4825
5067
  if (file === undefined) {
4826
- throw new Error(`promoteAuditPlans: selected plan ${JSON.stringify(id)} does not match any NNN-*.md file in ${resolve11(outDir)}`);
5068
+ throw new Error(`promoteAuditPlans: selected plan ${JSON.stringify(id)} does not match any NNN-*.md file in ${resolve12(outDir)}`);
4827
5069
  }
4828
5070
  if (!seen.has(file)) {
4829
5071
  seen.add(file);
@@ -4833,7 +5075,7 @@ function resolveSelectedPlanFiles(outDir, selected) {
4833
5075
  return resolved;
4834
5076
  }
4835
5077
  function readExecutionOrderIndex(outDir) {
4836
- const readmePath = join12(outDir, "README.md");
5078
+ const readmePath = join14(outDir, "README.md");
4837
5079
  let text;
4838
5080
  try {
4839
5081
  text = readFileSync9(readmePath, "utf8");
@@ -4862,7 +5104,7 @@ function readExecutionOrderIndex(outDir) {
4862
5104
  return rows;
4863
5105
  }
4864
5106
  function planFileRel(outDir, planFile) {
4865
- const resolved = resolve11(outDir);
5107
+ const resolved = resolve12(outDir);
4866
5108
  const parts = resolved.split(sep3);
4867
5109
  const plansIdx = parts.lastIndexOf("plans");
4868
5110
  if (plansIdx >= 0) {
@@ -4871,8 +5113,8 @@ function planFileRel(outDir, planFile) {
4871
5113
  return planFile;
4872
5114
  }
4873
5115
  // src/compound.ts
4874
- import { existsSync as existsSync9, readdirSync as readdirSync9, readFileSync as readFileSync10 } from "node:fs";
4875
- import { basename as basename6, isAbsolute as isAbsolute8, join as join13, relative as relative5, resolve as resolve12, sep as sep4 } from "node:path";
5116
+ import { existsSync as existsSync10, readdirSync as readdirSync9, readFileSync as readFileSync10 } from "node:fs";
5117
+ import { basename as basename7, isAbsolute as isAbsolute8, join as join15, relative as relative6, resolve as resolve13, sep as sep4 } from "node:path";
4876
5118
  function violation10(severity, code, message, fix) {
4877
5119
  return { ok: false, severity, code, message, fix };
4878
5120
  }
@@ -5167,7 +5409,7 @@ function referenceExists(repoRoot, docText) {
5167
5409
  break;
5168
5410
  if (entry.isDirectory()) {
5169
5411
  if (!WALK_SKIP_DIRS.has(entry.name))
5170
- stack.push(join13(dir, entry.name));
5412
+ stack.push(join15(dir, entry.name));
5171
5413
  } else if (!entry.isSymbolicLink()) {
5172
5414
  const base = entry.name.replace(/\.(?:ts|tsx|js|jsx|mjs|cjs)$/, "");
5173
5415
  if (moduleNames.has(base))
@@ -5179,7 +5421,7 @@ function referenceExists(repoRoot, docText) {
5179
5421
  for (const { ref, isSymbol, module } of refs) {
5180
5422
  if (!isSymbol || module === undefined) {
5181
5423
  const candidate = ref.replace(LINE_SUFFIX_RE, "").replace(ANCHOR_RE, "");
5182
- if (existsSync9(resolve12(repoRoot, candidate))) {
5424
+ if (existsSync10(resolve13(repoRoot, candidate))) {
5183
5425
  checked++;
5184
5426
  } else {
5185
5427
  violations.push(violation10("medium", "compound.reference.missing-file", `referenced path \`${ref}\` does not exist under ${repoRoot} (compound-refresh Phase 2: referenced code still exists?)`, "update the doc to reference an existing path, or delete the stale reference"));
@@ -5206,11 +5448,11 @@ function collectKnowledgeDocs(dir) {
5206
5448
  for (const entry of entries) {
5207
5449
  if (entry.isSymbolicLink())
5208
5450
  continue;
5209
- const full = join13(current, entry.name);
5451
+ const full = join15(current, entry.name);
5210
5452
  if (entry.isDirectory()) {
5211
5453
  stack.push(full);
5212
5454
  } else if (entry.name.endsWith(".md") && entry.name !== "README.md" && entry.name !== "index.md") {
5213
- docs.push(relative5(dir, full).split(sep4).join("/"));
5455
+ docs.push(relative6(dir, full).split(sep4).join("/"));
5214
5456
  }
5215
5457
  }
5216
5458
  }
@@ -5226,8 +5468,8 @@ function normalizeIndexRef(cell) {
5226
5468
  }
5227
5469
  function assertIndexRows(knowledgeDir) {
5228
5470
  const violations = [];
5229
- const readmePath = join13(knowledgeDir, "README.md");
5230
- if (!existsSync9(readmePath)) {
5471
+ const readmePath = join15(knowledgeDir, "README.md");
5472
+ if (!existsSync10(readmePath)) {
5231
5473
  violations.push(violation10("medium", "compound.index.missing-readme", `missing ${readmePath} — the knowledge index is required (mstar-compound Phase 6: every doc gets a README.md row)`, "create knowledge/README.md with a Document / Source Plan / Description / Status table"));
5232
5474
  return { ok: false, violations };
5233
5475
  }
@@ -5252,19 +5494,19 @@ function assertIndexRows(knowledgeDir) {
5252
5494
  }
5253
5495
  function compoundRefreshScope(harnessDir, projectRoot) {
5254
5496
  return [
5255
- join13(harnessDir, "knowledge"),
5256
- join13(harnessDir, "knowledge", "README.md"),
5257
- join13(projectRoot, "CONCEPTS.md"),
5258
- join13(harnessDir, "status.json")
5497
+ join15(harnessDir, "knowledge"),
5498
+ join15(harnessDir, "knowledge", "README.md"),
5499
+ join15(projectRoot, "CONCEPTS.md"),
5500
+ join15(harnessDir, "status.json")
5259
5501
  ];
5260
5502
  }
5261
5503
  function isFileLikeRoot(root) {
5262
- return /^[^.]*\.[A-Za-z0-9]{1,10}$/.test(basename6(root));
5504
+ return /^[^.]*\.[A-Za-z0-9]{1,10}$/.test(basename7(root));
5263
5505
  }
5264
5506
  function scopeGuard(path, allowedRoots) {
5265
- const resolved = resolve12(path);
5507
+ const resolved = resolve13(path);
5266
5508
  for (const root of allowedRoots) {
5267
- const r = resolve12(root);
5509
+ const r = resolve13(root);
5268
5510
  if (isFileLikeRoot(r)) {
5269
5511
  if (resolved === r)
5270
5512
  return { ok: true, violations: [] };
@@ -5512,8 +5754,8 @@ function lintStrategySections(docText) {
5512
5754
  return { ok: violations.length === 0, violations };
5513
5755
  }
5514
5756
  // src/roles.ts
5515
- import { existsSync as existsSync10 } from "node:fs";
5516
- import { join as join14 } from "node:path";
5757
+ import { existsSync as existsSync11 } from "node:fs";
5758
+ import { join as join16 } from "node:path";
5517
5759
  function violation12(severity, code, message, fix) {
5518
5760
  return { ok: false, severity, code, message, fix };
5519
5761
  }
@@ -5569,8 +5811,8 @@ function validateRoleMapping(rolesDir, options = {}) {
5569
5811
  const violations = [];
5570
5812
  const referenceById = new Map(mapping.map((m) => [m.agentId, m.reference]));
5571
5813
  for (const { agentId, reference } of mapping) {
5572
- if (!existsSync10(join14(rolesDir, reference))) {
5573
- violations.push(violation12("medium", "roles.mapping.reference.missing", `role "${agentId}" maps to ${reference} which does not exist under ${rolesDir} (mstar-roles § Role Reference Mapping)`, `create ${join14(rolesDir, reference)} or fix the mapping row`));
5814
+ if (!existsSync11(join16(rolesDir, reference))) {
5815
+ violations.push(violation12("medium", "roles.mapping.reference.missing", `role "${agentId}" maps to ${reference} which does not exist under ${rolesDir} (mstar-roles § Role Reference Mapping)`, `create ${join16(rolesDir, reference)} or fix the mapping row`));
5574
5816
  }
5575
5817
  }
5576
5818
  for (const { family, memberIds } of families) {
@@ -5805,7 +6047,7 @@ function resolveAssetPath(skillName, relPath, host) {
5805
6047
  }
5806
6048
  // src/prreview.ts
5807
6049
  import { readdirSync as readdirSync10 } from "node:fs";
5808
- import { isAbsolute as isAbsolute9, join as join15 } from "node:path";
6050
+ import { isAbsolute as isAbsolute9, join as join17 } from "node:path";
5809
6051
  var MERGE_CLASSES = ["must-fix", "should-fix", "nit"];
5810
6052
  var PR_VERDICTS = ["ship it", "needs fixes", "blocked"];
5811
6053
  var REVIEW_EMOJI = {
@@ -6061,7 +6303,7 @@ function prReviewReportPath(opts) {
6061
6303
  }
6062
6304
  const revision = maxRevision + 1;
6063
6305
  const name = revision === 1 ? `${finalStem}.md` : `${finalStem}-r${revision}.md`;
6064
- return join15(opts.reportsDir, name);
6306
+ return join17(opts.reportsDir, name);
6065
6307
  }
6066
6308
  var PR_TIERS = ["quick", "default", "deep"];
6067
6309
  function violation14(severity, code, message, fix) {
@@ -6378,7 +6620,7 @@ function prReviewSeatPrompt(opts) {
6378
6620
  lines.push("");
6379
6621
  lines.push("## Read first");
6380
6622
  lines.push("");
6381
- const prReviewRef = join15(skillRoot, "references", "pr-review.md");
6623
+ const prReviewRef = join17(skillRoot, "references", "pr-review.md");
6382
6624
  const sections = opts.stage === 1 ? tier === "quick" ? "Scoping, Evidence rules" : "Review pipeline, Worktree isolation, Scoping, Evidence rules" : "Merge class, Attack and vet, Evidence rules, Sizing & change shape";
6383
6625
  lines.push(`1. \`${prReviewRef}\` — read at least these sections: ${sections}.`);
6384
6626
  lines.push(`2. The review worktree: \`${worktreePath}\` — your ONLY working directory this session; read-only (no edits, no fixes, no stash, no commits, no posts).`);
@@ -6386,9 +6628,9 @@ function prReviewSeatPrompt(opts) {
6386
6628
  lines.push(`- Read the pinned diff snapshot FIRST: \`${opts.diffFile}\` — it is the review's diff basis (already computed at setup); read it before opening files.`);
6387
6629
  }
6388
6630
  if (opts.stage === 2) {
6389
- lines.push(`3. \`${join15(skillRoot, "references", "finding-format.md")}\` — the template every finding follows.`);
6631
+ lines.push(`3. \`${join17(skillRoot, "references", "finding-format.md")}\` — the template every finding follows.`);
6390
6632
  if (opts.securitySeat === true) {
6391
- lines.push(`4. \`${join15(skillRoot, "references", "security-review.md")}\` — the security lens.`);
6633
+ lines.push(`4. \`${join17(skillRoot, "references", "security-review.md")}\` — the security lens.`);
6392
6634
  }
6393
6635
  }
6394
6636
  const budget = PR_REVIEW_TIER_BUDGETS[tier];
@@ -6586,6 +6828,7 @@ export {
6586
6828
  KNOWLEDGE_REQUIRED_FIELDS,
6587
6829
  KNOWLEDGE_RESOLUTION_TYPES,
6588
6830
  KNOWLEDGE_SEVERITIES,
6831
+ MAX_STATUS_CONTENT_LENGTH,
6589
6832
  MERGE_CLASSES,
6590
6833
  MIGRATE_STATUS_FILE,
6591
6834
  MSTARC_FILE,
@@ -6644,14 +6887,18 @@ export {
6644
6887
  detectHost,
6645
6888
  emitGitignoreSnippet,
6646
6889
  evaluatePhaseGate,
6890
+ eventTargetPaths,
6647
6891
  executionModeToN,
6648
6892
  findEphemeralCitations,
6649
6893
  findMstarc,
6650
6894
  findSimplifyMarkers,
6651
6895
  findTemporaryMarkers,
6652
6896
  findingsCleanupGate,
6897
+ formatStatusWriteBlockReason,
6653
6898
  getArtifactStore,
6899
+ harnessDocKindOfTarget,
6654
6900
  implementerSessionStickyRules,
6901
+ isDistinctCheckout,
6655
6902
  isReadOnlyAssignmentRole,
6656
6903
  l1PreDispatchCheck,
6657
6904
  l2PreDispatchCheck,
@@ -6680,6 +6927,7 @@ export {
6680
6927
  prReviewSeatPrompt,
6681
6928
  prReviewSizing,
6682
6929
  preflightChangeset,
6930
+ probeCheckoutRoot,
6683
6931
  promoteAuditPlans,
6684
6932
  pushCadenceProbe,
6685
6933
  readHarnessVersion,
@@ -6741,9 +6989,11 @@ export {
6741
6989
  validateSchemaYaml,
6742
6990
  validateStatus,
6743
6991
  validateStatusV2,
6992
+ validateStatusWriteDoc,
6744
6993
  validateWorkflowEntry,
6745
6994
  validateWorkflowSnapshot,
6746
6995
  verifyPlanExecutionLease,
6996
+ violationLine,
6747
6997
  withStatusWriteLock,
6748
6998
  writeJson,
6749
6999
  writeWorkflowSnapshot
@@ -0,0 +1,69 @@
1
+ import { type ValidationResult } from "./core.js";
2
+ /** Target paths from a write/edit event: `input.path` (string) plus `input.paths` (array). */
3
+ export declare function eventTargetPaths(input: unknown): string[];
4
+ /** Gated harness coordination documents in v3 (compass ruling 7 — hard
5
+ * cutover): the root `status.json` (v2), workflow snapshots
6
+ * (`workflows/<id>/snapshot.json`) and project registers
7
+ * (`projects/<id>/residuals.json`). Each kind maps to its engine
8
+ * validator; everything else is not a gated coordination write. */
9
+ export type HarnessDocKind = "status" | "snapshot" | "register";
10
+ /**
11
+ * Classify `targetPath` as a canonical `{HARNESS_DIR}` coordination
12
+ * document: basename is `status.json` at the harness root, `snapshot.json`
13
+ * under `{WORKFLOW_DIR}/<id>/`, or `residuals.json` under
14
+ * `{PROJECT_DIR}/<id>/` (harness-relative, one path component each), AND
15
+ * the harness root resolves — marker probe first (custom-layout-aware
16
+ * Phase-5 F1), `resolveHarnessDir` as the declared-root fallback. The
17
+ * snapshot/register rel is computed against the RESOLVED layout dirs
18
+ * (`.mstarc` `workflow_dir`/`project_dir` honored, defaults
19
+ * `workflows`/`projects`), so a custom layout classifies at the same
20
+ * location the runtime writes. Everything else is not a gated write.
21
+ * Returns the harness dir + doc kind when gated.
22
+ */
23
+ export declare function harnessDocKindOfTarget(targetPath: string): {
24
+ harnessDir: string;
25
+ kind: HarnessDocKind;
26
+ } | null;
27
+ export declare function violationLine(violation: ValidationResult): string;
28
+ /**
29
+ * Size guard: content strings beyond ~2MB are skipped without
30
+ * parsing — a pathologically large write must not approach a host's
31
+ * handler timeout (which fails CLOSED even in soft mode). The oversized
32
+ * write passes silently; documented in the host gate contract.
33
+ */
34
+ export declare const MAX_STATUS_CONTENT_LENGTH: number;
35
+ /**
36
+ * Options for {@link validateStatusWriteDoc}.
37
+ */
38
+ export interface ValidateStatusWriteDocOptions {
39
+ /**
40
+ * Behavior when the content (string form) or the on-disk gated document
41
+ * (edit form) exceeds `MAX_STATUS_CONTENT_LENGTH`. `"pass"` (default)
42
+ * keeps the documented silent-pass degradation; `"violate"` reports a
43
+ * `status.oversized` violation instead — for hosts whose block dialect
44
+ * makes the oversized write an enforceable refusal rather than a
45
+ * permission (the size check stays O(1), before any parse).
46
+ */
47
+ oversized?: "pass" | "violate";
48
+ }
49
+ /**
50
+ * Validate the document being written to a gated harness coordination
51
+ * document. `content` as a string is the new document: JSON.parse it
52
+ * and run the matching engine validator on the parsed doc — a parse failure
53
+ * is a violation (`status.invalid-json`, the same code/message shape the
54
+ * engine emits for an unparseable file). Parsed `null` / non-object / array
55
+ * content is a `status.invalid-json` violation too (the JSON
56
+ * literal `null` would otherwise slip through `validateStatus`'s
57
+ * destructuring into the outer catch's silent pass). Without a content
58
+ * string (edit-style events) the on-disk file is validated — unless it does
59
+ * not exist yet (fresh scaffold/init write): nothing to validate, silent
60
+ * pass. Never throws (the validators catch their own read errors).
61
+ */
62
+ export declare function validateStatusWriteDoc(content: unknown, filePath: string, kind: HarnessDocKind, options?: ValidateStatusWriteDocOptions): ValidationResult[];
63
+ /**
64
+ * Format the gate block reason: one `violationLine` per violation, each
65
+ * suffixed with the host's skill pointer (omp/ZCode parity: both hosts
66
+ * pass `skill: mstar-artifacts/references/status-and-residuals.md`),
67
+ * joined with newlines.
68
+ */
69
+ export declare function formatStatusWriteBlockReason(violations: ValidationResult[], skillPointer: string): string;
@@ -0,0 +1 @@
1
+ export {};
package/dist/index.d.ts CHANGED
@@ -20,7 +20,10 @@
20
20
  * constants (mstar-audit pr-review.md § Tally and derived score), `host`
21
21
  * detects the active
22
22
  * host from tool shapes, resolves skill roots and defines the type-only
23
- * `HostAdapter` contract, and `skill-authoring` lints frontmatter +
23
+ * `HostAdapter` contract, `gates` is the host-neutral coordination-write
24
+ * gate core (target classification + content/edit validation + reason
25
+ * formatting, shared by the omp and ZCode host gates), and `skill-authoring`
26
+ * lints frontmatter +
24
27
  * 5-question bodies and resolves skill-relative asset paths.
25
28
  */
26
29
  export type { GateResult, Severity, ValidationResult } from "./core.js";
@@ -38,13 +41,15 @@ export { WORKFLOW_LIFECYCLE_STATUSES, WORKFLOW_LIFECYCLE_TYPES, WORKFLOW_SNAPSHO
38
41
  export type { AssignmentBranchForms, AssignmentFields, ComposeDispatchGateOptions, ComposeDispatchGateResult, DefaultBranchOptions, EnforcementFlag, EnforcementSource, ExecutionModeToNOptions, ExecutionModeToNResult, ValidateAssignmentFieldsOptions, } from "./dispatch.js";
39
42
  export { antiRecursionPrecheck, assertDefaultBranchProtected, assertTriIdentity, assignmentHeaderRegion, composeDispatchGate, executionModeToN, isReadOnlyAssignmentRole, parseAssignmentBranchForms, parseAssignmentFields, parseBranchPolicyDirectOnBranch, parseEnforcementFlag, validateAssignmentFields, } from "./dispatch.js";
40
43
  export type { BranchProbeOptions, L1PreDispatchInput, L2PreDispatchInput, QcAlignmentAssignment, QcSnapshotAssignment, WorktreeTrack, } from "./worktree.js";
41
- export { assertBranchAlignment, assertControlVsFeaturePath, assertQcAlignment, l1PreDispatchCheck, l2PreDispatchCheck, singleReviewSnapshot, } from "./worktree.js";
44
+ export { assertBranchAlignment, assertControlVsFeaturePath, assertQcAlignment, isDistinctCheckout, l1PreDispatchCheck, l2PreDispatchCheck, probeCheckoutRoot, singleReviewSnapshot, } from "./worktree.js";
42
45
  export type { ImplementerSessionLedger, ReviewPackageOptions, SddAction, SddActionKind, SddExecutionContext, SddWorkspaceOptions, StickyRulesInput, StickyRulesResult, TaskBriefOptions, } from "./sdd.js";
43
46
  export { GIT_CAPTURE_MAX_BYTES, SddScriptError, assertBaseSha, checkSddAction, implementerSessionStickyRules, readProgressLedger, resolveSddExecutionContext, reviewPackage, runInSddContext, sddWorkspace, taskBrief, taskReportExists, } from "./sdd.js";
44
47
  export type { CompassDoc, PhaseGateOptions, PhaseGateResult, PhaseTransition, } from "./iteration.js";
45
48
  export { assertIndexRowObligations, evaluatePhaseGate, parseCompassFrontmatter, parseCompassFrontmatterText, pushCadenceProbe, validateCompassFrontmatter, } from "./iteration.js";
46
49
  export type { AppendProjectRegisterEntriesOpts, CloseProjectRegisterEntryOpts, FindingsCleanupMode, ProjectRegisterDoc, ProjectRegisterEntry, RoadmapFrontmatter, RoadmapStatus, RoadmapValidation, TechDebtCheck, TechDebtRollup, TechDebtSummary, } from "./project.js";
47
50
  export { PROJECT_REFERENCES_DIR, PROJECT_REGISTER_FILE, PROJECT_ROADMAP_FILE, ROADMAP_STATUSES, _DEFAULT_PROJECT, appendProjectRegisterEntries, closeProjectRegisterEntry, findingsCleanupGate, listProjectReferenceFiles, techDebtRollup, validateProjectRegister, validateRoadmap, } from "./project.js";
51
+ export type { HarnessDocKind, ValidateStatusWriteDocOptions } from "./gates.js";
52
+ export { MAX_STATUS_CONTENT_LENGTH, eventTargetPaths, formatStatusWriteBlockReason, harnessDocKindOfTarget, validateStatusWriteDoc, violationLine, } from "./gates.js";
48
53
  export type { MigrateNotesFile, MigrateOptions, MigratePlan, MigrateRegister, MigrateResult, MigrateRoadmap, MigrateRootV2, MigrateSnapshot, MigrateStep, } from "./migrate.js";
49
54
  export { ARCHIVED_STATUS_V1_FILE, MIGRATE_STATUS_FILE, NOTES_LEDGER_FILE, applyMigratePlan, migrateHarnessTree, } from "./migrate.js";
50
55
  export type { CompletenessItem, CompletenessLevel, CompletenessPlaceholder, CompletenessResult, DesignFrontmatter, } from "./design-md.js";
@@ -55,6 +55,27 @@ export type QcSnapshotAssignment = QcAlignmentAssignment & {
55
55
  /** Precomputed review HEAD (full SHA preferred) for that assignment. */
56
56
  head?: string;
57
57
  };
58
+ /**
59
+ * True when `candidatePath` is a Git checkout DISTINCT from `controlPath` —
60
+ * the canonical per-worktree git dirs differ. A linked worktree from
61
+ * `git worktree add` (nested inside the control checkout or a sibling) has
62
+ * its own git dir and is distinct; the same checkout, a plain subdirectory
63
+ * of it, or a symlink alias of it resolves to the same git dir and is NOT
64
+ * distinct. Everything unprovable — a non-repo path or a probe failure —
65
+ * is NOT distinct: fail closed, never guess an identity.
66
+ */
67
+ export declare function isDistinctCheckout(controlPath: string, candidatePath: string, opts?: BranchProbeOptions): boolean;
68
+ /**
69
+ * Probe the repository top-level (worktree root) of a checkout via
70
+ * `git -C <path> rev-parse --show-toplevel` — bounded exactly like
71
+ * `probeBranch` / `probeCheckout`, fail-closed (null on any failure). The
72
+ * canonical top-level is the checkout root regardless of where inside it
73
+ * the probed path sits — a `.mstarc`-declared nested harness dir like
74
+ * `<control>/state/.mstar` included — so callers never infer the checkout
75
+ * root from `dirname(harness)` (a layout assumption that only holds when
76
+ * the harness sits directly under the checkout).
77
+ */
78
+ export declare function probeCheckoutRoot(path: string, opts?: BranchProbeOptions): string | null;
58
79
  /**
59
80
  * L1 cross-plan pre-dispatch checklist (mstar-branch-worktree L1 table +
60
81
  * Harness path SSOT hard rules): control path recorded, feature worktree
@@ -74,11 +95,15 @@ export declare function l1PreDispatchCheck(input: L1PreDispatchInput, opts?: Bra
74
95
  export declare function l2PreDispatchCheck(input: L2PreDispatchInput, opts?: BranchProbeOptions): GateResult;
75
96
  /**
76
97
  * L1 hard rule (Harness path SSOT): `execution_lease.worktree_path` MUST
77
- * differ from `metadata.control_worktree_path`. String equality on the two
78
- * paths canonical absolute paths are the caller's contract (the lease
79
- * validator already requires `worktree_path` to be absolute).
98
+ * be a Git checkout DISTINCT from `metadata.control_worktree_path` the
99
+ * same checkout, a plain subdirectory, or a symlink alias of the control
100
+ * checkout is refused (checkout identity via the canonical per-worktree
101
+ * git dir; probe failure fails closed). Both-empty stays a match (nothing
102
+ * recorded, per the lease validator contract); one empty has nothing to
103
+ * compare and passes (the lease validator's absolute-path requirement owns
104
+ * empty lease paths).
80
105
  */
81
- export declare function assertControlVsFeaturePath(controlWorktreePath: string, featureWorktreePath: string): GateResult;
106
+ export declare function assertControlVsFeaturePath(controlWorktreePath: string, featureWorktreePath: string, opts?: BranchProbeOptions): GateResult;
82
107
  /**
83
108
  * Assert the branch checked out at `worktreePath` matches `expectedBranch`
84
109
  * (the Assignment Working branch). Probe = `git -C <path> branch
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mstar-harness/engine",
3
- "version": "3.7.0",
3
+ "version": "3.7.1",
4
4
  "description": "Morning Star Harness Workflow Engine — deterministic workflow enforcement library (path, status, lease, dispatch, sdd, iteration, lint gates).",
5
5
  "license": "MIT",
6
6
  "repository": {