@mstar-harness/engine 3.6.2 → 3.7.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.
package/dist/engine.js CHANGED
@@ -2095,6 +2095,25 @@ function assertSafePathComponent(value, what) {
2095
2095
  throw new Error(`${what} must be a single safe path component ([A-Za-z0-9._-]+; not "", ".", "..", or containing "/" or "\\") — got ${JSON.stringify(value)}`);
2096
2096
  }
2097
2097
  }
2098
+ function canonicalizeNearestExisting(path) {
2099
+ const abs = resolve7(path);
2100
+ let dir = abs;
2101
+ const tail = [];
2102
+ for (;; ) {
2103
+ if (existsSync6(dir)) {
2104
+ try {
2105
+ return join9(realpathSync2(dir), ...tail);
2106
+ } catch {
2107
+ return abs;
2108
+ }
2109
+ }
2110
+ const parent = dirname5(dir);
2111
+ if (parent === dir)
2112
+ return abs;
2113
+ tail.unshift(basename3(dir));
2114
+ dir = parent;
2115
+ }
2116
+ }
2098
2117
  function resolveSddDir(harnessDir, planId) {
2099
2118
  assertSafePathComponent(planId, "planId");
2100
2119
  const base = resolve7(harnessDir);
@@ -2489,9 +2508,10 @@ function singleReviewSnapshot(assignments) {
2489
2508
  return gate(violations);
2490
2509
  }
2491
2510
  // src/sdd.ts
2492
- import { execFileSync as execFileSync3 } from "node:child_process";
2511
+ import { execFileSync as execFileSync3, spawn } from "node:child_process";
2493
2512
  import { mkdirSync as mkdirSync6, readdirSync as readdirSync6, readFileSync as readFileSync7, realpathSync as realpathSync3, statSync as statSync4, writeFileSync as writeFileSync4 } from "node:fs";
2494
- import { basename as basename4, dirname as dirname6, isAbsolute as isAbsolute6, join as join10, resolve as resolve9 } from "node:path";
2513
+ 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";
2495
2515
  class SddScriptError extends Error {
2496
2516
  exitCode;
2497
2517
  constructor(message, exitCode) {
@@ -2629,6 +2649,16 @@ function taskBrief(planFile, taskN, outFile, opts = {}) {
2629
2649
  if (!planFile || !Number.isInteger(taskN) || taskN < 1) {
2630
2650
  throw new SddScriptError("usage: mstar sdd task-brief PLAN_FILE TASK_NUMBER [OUTFILE]", 2);
2631
2651
  }
2652
+ const bound = opts.context;
2653
+ const observedCwd = opts.cwd ?? process.cwd();
2654
+ if (bound) {
2655
+ const inputPlan = canonicalizeNearestExisting(resolve9(observedCwd, planFile));
2656
+ if (inputPlan !== canonicalizeNearestExisting(bound.planFile)) {
2657
+ throwGateFail([
2658
+ contextViolation("high", "sdd.context.plan-file-mismatch", `plan file "${planFile}" does not match the bound context plan file "${bound.planFile}" — ` + "bound mode extracts only the resolved context's plan; refused before any read or write")
2659
+ ]);
2660
+ }
2661
+ }
2632
2662
  let content;
2633
2663
  try {
2634
2664
  content = readFileSync7(planFile, "utf8");
@@ -2636,8 +2666,12 @@ function taskBrief(planFile, taskN, outFile, opts = {}) {
2636
2666
  throw new SddScriptError(`no such plan file: ${planFile}`, 2);
2637
2667
  }
2638
2668
  let out;
2669
+ let mkdirAfterGate = null;
2639
2670
  if (outFile) {
2640
- out = outFile;
2671
+ out = bound ? resolve9(observedCwd, outFile) : outFile;
2672
+ } else if (bound) {
2673
+ out = join10(bound.sddDir, `task-${taskN}-brief.md`);
2674
+ mkdirAfterGate = bound.sddDir;
2641
2675
  } else {
2642
2676
  const sddDir = opts.sddDir ?? process.env.SDD_DIR;
2643
2677
  if (!sddDir) {
@@ -2646,6 +2680,13 @@ function taskBrief(planFile, taskN, outFile, opts = {}) {
2646
2680
  mkdirSync6(sddDir, { recursive: true });
2647
2681
  out = join10(sddDir, `task-${taskN}-brief.md`);
2648
2682
  }
2683
+ if (bound) {
2684
+ const gate2 = checkSddAction(bound, { kind: "artifact", cwd: observedCwd, target: out });
2685
+ if (!gate2.ok)
2686
+ throwGateFail(gate2.violations);
2687
+ if (mkdirAfterGate !== null)
2688
+ mkdirSync6(mkdirAfterGate, { recursive: true });
2689
+ }
2649
2690
  const records = content.endsWith(`
2650
2691
  `) ? content.split(`
2651
2692
  `).slice(0, -1) : content.split(`
@@ -2670,13 +2711,15 @@ function taskBrief(planFile, taskN, outFile, opts = {}) {
2670
2711
  if (printed.length === 0) {
2671
2712
  throw new SddScriptError(`task ${taskN} not found in ${planFile} (no heading matching Task ${taskN})`, 3);
2672
2713
  }
2673
- return out;
2714
+ return bound ? resolve9(observedCwd, out) : out;
2674
2715
  }
2675
2716
  function reviewPackage(base, head, outFile, opts = {}) {
2676
2717
  if (!base || !head) {
2677
2718
  throw new SddScriptError("usage: mstar sdd review-package BASE HEAD [OUTFILE]", 2);
2678
2719
  }
2679
- const cwd = opts.cwd ?? process.cwd();
2720
+ const bound = opts.context;
2721
+ const cwd = bound && opts.cwd === undefined ? bound.featureCwd : opts.cwd ?? process.cwd();
2722
+ const observedCwd = opts.cwd ?? process.cwd();
2680
2723
  const verifyRef = (ref, what) => {
2681
2724
  try {
2682
2725
  execFileSync3("git", ["rev-parse", "--verify", "--quiet", ref], { cwd, stdio: ["ignore", "pipe", "pipe"] });
@@ -2687,8 +2730,14 @@ function reviewPackage(base, head, outFile, opts = {}) {
2687
2730
  verifyRef(base, "BASE");
2688
2731
  verifyRef(head, "HEAD");
2689
2732
  let out;
2733
+ let mkdirAfterGate = null;
2690
2734
  if (outFile) {
2691
- out = outFile;
2735
+ out = bound ? resolve9(observedCwd, outFile) : outFile;
2736
+ } else if (bound) {
2737
+ const shortBase = gitOut(cwd, ["rev-parse", "--short", base]) ?? base;
2738
+ const shortHead = gitOut(cwd, ["rev-parse", "--short", head]) ?? head;
2739
+ out = join10(bound.sddDir, `review-${shortBase}..${shortHead}.diff`);
2740
+ mkdirAfterGate = bound.sddDir;
2692
2741
  } else {
2693
2742
  const sddDir = opts.sddDir ?? process.env.SDD_DIR;
2694
2743
  if (!sddDir) {
@@ -2699,6 +2748,13 @@ function reviewPackage(base, head, outFile, opts = {}) {
2699
2748
  const shortHead = gitOut(cwd, ["rev-parse", "--short", head]) ?? head;
2700
2749
  out = join10(sddDir, `review-${shortBase}..${shortHead}.diff`);
2701
2750
  }
2751
+ if (bound) {
2752
+ const gate2 = checkSddAction(bound, { kind: "artifact", cwd: observedCwd, target: out });
2753
+ if (!gate2.ok)
2754
+ throwGateFail(gate2.violations);
2755
+ if (mkdirAfterGate !== null)
2756
+ mkdirSync6(mkdirAfterGate, { recursive: true });
2757
+ }
2702
2758
  const run = (args) => execFileSync3("git", args, { cwd, maxBuffer: GIT_CAPTURE_MAX_BYTES });
2703
2759
  const parts = [
2704
2760
  Buffer.from(`# Review package: ${base}..${head}
@@ -2716,7 +2772,7 @@ function reviewPackage(base, head, outFile, opts = {}) {
2716
2772
  run(["diff", "-U10", `${base}..${head}`])
2717
2773
  ];
2718
2774
  writeFileSync4(out, Buffer.concat(parts));
2719
- return out;
2775
+ return bound ? resolve9(observedCwd, out) : out;
2720
2776
  }
2721
2777
  function assertBaseSha(ref, opts = {}) {
2722
2778
  if (typeof ref !== "string" || !/^[0-9a-f]{4,40}$/i.test(ref)) {
@@ -2774,9 +2830,356 @@ function implementerSessionStickyRules(input) {
2774
2830
  }
2775
2831
  return { resume: true, reason: `sticky resume OK: host_agent_id ${session.host_agent_id}, next task ${nextTask}` };
2776
2832
  }
2833
+ function isPlainObject6(value) {
2834
+ return typeof value === "object" && value !== null && !Array.isArray(value);
2835
+ }
2836
+ function isInside(child, ancestor) {
2837
+ const rel = relative3(ancestor, child);
2838
+ return rel === "" || !rel.startsWith("..") && !isAbsolute6(rel);
2839
+ }
2840
+ function canonicalDir(path) {
2841
+ try {
2842
+ return statSync4(path).isDirectory() ? realpathSync3(path) : null;
2843
+ } catch {
2844
+ return null;
2845
+ }
2846
+ }
2847
+ function contextViolation(severity, code, message, fix) {
2848
+ return { ok: false, severity, code, message, fix };
2849
+ }
2850
+ function throwGateFail(violations) {
2851
+ const detail = violations.map((v) => `${v.code}: ${v.message}${v.fix ? ` (fix: ${v.fix})` : ""}`).join(`
2852
+ `);
2853
+ throw new SddScriptError(`SDD execution context rejected:
2854
+ ${detail}`, 1);
2855
+ }
2856
+ function throwUsage(message) {
2857
+ throw new SddScriptError(message, 2);
2858
+ }
2859
+ function readActiveWorkflowIds(controlHarnessRoot) {
2860
+ const statusPath = join10(controlHarnessRoot, "status.json");
2861
+ if (!isFile2(statusPath))
2862
+ return null;
2863
+ let doc;
2864
+ try {
2865
+ doc = readJson(statusPath);
2866
+ } catch {
2867
+ return null;
2868
+ }
2869
+ if (!isPlainObject6(doc) || doc.version !== 2 || !Array.isArray(doc.workflows))
2870
+ return null;
2871
+ const ids = new Set;
2872
+ for (const entry of doc.workflows) {
2873
+ if (isPlainObject6(entry) && typeof entry.id === "string")
2874
+ ids.add(entry.id);
2875
+ }
2876
+ return ids;
2877
+ }
2878
+ function findWorkflowPlanRow(controlHarnessRoot, planId) {
2879
+ let workflowsDir;
2880
+ try {
2881
+ workflowsDir = resolveWorkflowDir(controlHarnessRoot, { harnessDir: controlHarnessRoot });
2882
+ } catch {
2883
+ return { kind: "none" };
2884
+ }
2885
+ if (!isDirectory2(workflowsDir))
2886
+ return { kind: "none" };
2887
+ let workflowIds;
2888
+ try {
2889
+ workflowIds = readdirSync6(workflowsDir, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => entry.name);
2890
+ } catch {
2891
+ return { kind: "none" };
2892
+ }
2893
+ const matches = [];
2894
+ for (const id of workflowIds) {
2895
+ const snapshotPath = join10(workflowsDir, id, WORKFLOW_SNAPSHOT_FILE);
2896
+ if (!isFile2(snapshotPath))
2897
+ continue;
2898
+ let doc;
2899
+ try {
2900
+ doc = readJson(snapshotPath);
2901
+ } catch {
2902
+ continue;
2903
+ }
2904
+ const plans = doc.plans;
2905
+ if (!Array.isArray(plans))
2906
+ continue;
2907
+ for (const row of plans) {
2908
+ if (isPlainObject6(row) && (row.id === planId || row.plan_id === planId)) {
2909
+ matches.push({ workflowId: id, row });
2910
+ break;
2911
+ }
2912
+ }
2913
+ }
2914
+ if (matches.length === 0)
2915
+ return { kind: "none" };
2916
+ const registeredActive = readActiveWorkflowIds(controlHarnessRoot);
2917
+ if (registeredActive === null) {
2918
+ return { kind: "row", workflowId: matches[0].workflowId, row: matches[0].row };
2919
+ }
2920
+ const active = matches.filter((m) => registeredActive.has(m.workflowId));
2921
+ if (active.length === 0)
2922
+ return { kind: "none" };
2923
+ if (active.length > 1) {
2924
+ return { kind: "ambiguous", workflowIds: active.map((m) => m.workflowId) };
2925
+ }
2926
+ return { kind: "row", workflowId: active[0].workflowId, row: active[0].row };
2927
+ }
2928
+ function resolveSddExecutionContext(input) {
2929
+ const { planId, workingBranch } = input;
2930
+ if (typeof planId !== "string" || planId.trim() === "") {
2931
+ throwUsage("SddExecutionContext.planId must be a non-empty string");
2932
+ }
2933
+ if (typeof workingBranch !== "string" || workingBranch.trim() === "") {
2934
+ throwUsage("SddExecutionContext.workingBranch must be a non-empty string");
2935
+ }
2936
+ for (const field of ["controlHarnessRoot", "featureCwd", "planFile", "sddDir"]) {
2937
+ const value = input[field];
2938
+ if (typeof value !== "string" || value.trim() === "") {
2939
+ throwUsage(`SddExecutionContext.${field} must be a non-empty string`);
2940
+ }
2941
+ if (!isAbsolute6(value)) {
2942
+ throwUsage(`SddExecutionContext.${field} must be an absolute path (A3: all paths normalized absolute); got ${JSON.stringify(value)}`);
2943
+ }
2944
+ }
2945
+ try {
2946
+ assertSafePathComponent(planId, "SddExecutionContext.planId");
2947
+ } catch (err) {
2948
+ throwUsage(`SddExecutionContext rejected: ${err.message}`);
2949
+ }
2950
+ const canonicalControlHarnessRoot = canonicalDir(input.controlHarnessRoot);
2951
+ if (canonicalControlHarnessRoot === null) {
2952
+ throwGateFail([
2953
+ contextViolation("high", "sdd.context.control-root-missing", `controlHarnessRoot "${input.controlHarnessRoot}" does not exist or is not a directory — a declared control root is authoritative and is never re-inferred from the feature cwd (A3)`)
2954
+ ]);
2955
+ }
2956
+ const stem = basename4(input.planFile).replace(/\.md$/, "");
2957
+ if (stem !== planId) {
2958
+ throwUsage(`SddExecutionContext.planFile "${input.planFile}" does not match plan "${planId}" — the plan file must be {PLAN_DIR}/<plan-id>.md under the declared control harness`);
2959
+ }
2960
+ if (!isFile2(input.planFile)) {
2961
+ throwUsage(`no such plan file: ${input.planFile}`);
2962
+ }
2963
+ const planGate = assertPlanWritingPath(input.planFile, input.controlHarnessRoot);
2964
+ if (!planGate.ok) {
2965
+ if (planGate.code === "plan-path.symlink-escape") {
2966
+ throwGateFail([planGate]);
2967
+ }
2968
+ throwUsage(`SddExecutionContext.planFile rejected: ${planGate.code}: ${planGate.message}`);
2969
+ }
2970
+ const composedSddDir = resolveSddDir(canonicalControlHarnessRoot, planId);
2971
+ const canonicalComposedSddDir = canonicalizeNearestExisting(composedSddDir);
2972
+ const canonicalSddDir = canonicalizeNearestExisting(input.sddDir);
2973
+ if (canonicalSddDir !== canonicalComposedSddDir) {
2974
+ if (!isInside(canonicalSddDir, canonicalControlHarnessRoot)) {
2975
+ throwGateFail([
2976
+ contextViolation("high", "sdd.context.sdd-dir-escape", `sddDir "${input.sddDir}" canonicalizes to "${canonicalSddDir}", outside the control harness "${canonicalControlHarnessRoot}" — symlink escape refused (environmental gate failure)`)
2977
+ ]);
2978
+ }
2979
+ throwUsage(`SddExecutionContext.sddDir "${input.sddDir}" does not match plan "${planId}" — expected the {SDD_DIR} composition ${composedSddDir}`);
2980
+ }
2981
+ const canonicalFeatureCwd = canonicalDir(input.featureCwd);
2982
+ if (canonicalFeatureCwd === null) {
2983
+ throwGateFail([
2984
+ 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
+ ]);
2986
+ }
2987
+ const controlCheckout = dirname6(canonicalControlHarnessRoot);
2988
+ if (isInside(canonicalFeatureCwd, controlCheckout)) {
2989
+ 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")
2991
+ ]);
2992
+ }
2993
+ if (isInside(canonicalControlHarnessRoot, canonicalFeatureCwd)) {
2994
+ 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`)
2996
+ ]);
2997
+ }
2998
+ const match = findWorkflowPlanRow(canonicalControlHarnessRoot, planId);
2999
+ if (match.kind === "ambiguous") {
3000
+ throwGateFail([
3001
+ contextViolation("high", "sdd.context.workflow-plan-ambiguous", `plan "${planId}" appears in multiple registered active workflows (${match.workflowIds.join(", ")}) — ` + "the governing execution_lease is undecidable; resolve the duplicate registration before dispatch")
3002
+ ]);
3003
+ }
3004
+ const row = match.kind === "row" ? match.row : null;
3005
+ if (row !== null && row.execution_lease !== undefined) {
3006
+ const leaseVerify = verifyPlanExecutionLease(row, planId);
3007
+ if (!leaseVerify.ok)
3008
+ throwGateFail(leaseVerify.violations);
3009
+ const lease = leaseVerify.lease;
3010
+ const l1 = l1PreDispatchCheck({
3011
+ controlWorktreePath: controlCheckout,
3012
+ leaseWorktreePath: lease.worktree_path,
3013
+ leaseWorkingBranch: lease.working_branch,
3014
+ planId
3015
+ });
3016
+ if (!l1.ok)
3017
+ throwGateFail(l1.violations);
3018
+ if (canonicalizeNearestExisting(lease.worktree_path) !== canonicalFeatureCwd) {
3019
+ throwGateFail([
3020
+ contextViolation("high", "sdd.context.lease-worktree-mismatch", `SddExecutionContext.featureCwd "${canonicalFeatureCwd}" does not match the verified execution_lease.worktree_path "${String(lease.worktree_path)}" — the context must match the verified lease (A3)`)
3021
+ ]);
3022
+ }
3023
+ if (workingBranch !== lease.working_branch) {
3024
+ throwGateFail([
3025
+ contextViolation("high", "sdd.context.lease-branch-mismatch", `SddExecutionContext.workingBranch "${workingBranch}" does not match the verified execution_lease.working_branch "${String(lease.working_branch)}" (plan "${planId}")`)
3026
+ ]);
3027
+ }
3028
+ } else if (row !== null && row.status === "InProgress") {
3029
+ throwGateFail(verifyPlanExecutionLease(row, planId).violations);
3030
+ } else {
3031
+ const branchGate = assertBranchAlignment(canonicalFeatureCwd, workingBranch);
3032
+ if (!branchGate.ok)
3033
+ throwGateFail(branchGate.violations);
3034
+ }
3035
+ return {
3036
+ planId,
3037
+ controlHarnessRoot: canonicalControlHarnessRoot,
3038
+ featureCwd: canonicalFeatureCwd,
3039
+ workingBranch,
3040
+ planFile: realpathSync3(input.planFile),
3041
+ sddDir: canonicalSddDir
3042
+ };
3043
+ }
3044
+ function checkSddAction(context, action) {
3045
+ const violations = [];
3046
+ const add = (code, message, fix) => {
3047
+ violations.push(contextViolation("high", code, message, fix));
3048
+ };
3049
+ if (action.kind !== "source" && action.kind !== "artifact" && action.kind !== "launch") {
3050
+ add("sdd.context.kind-unknown", `unknown action kind ${JSON.stringify(action.kind)} — expected "source" | "artifact" | "launch"`);
3051
+ return { ok: false, violations };
3052
+ }
3053
+ if (typeof action.cwd !== "string" || action.cwd.trim() === "") {
3054
+ add("sdd.context.cwd-missing", "action.cwd (the observed invocation cwd) is required — never an Assignment echo");
3055
+ return { ok: false, violations };
3056
+ }
3057
+ const featureReal = canonicalDir(context.featureCwd);
3058
+ const canonicalSddDir = canonicalizeNearestExisting(context.sddDir);
3059
+ const canonicalPlanFile = canonicalizeNearestExisting(context.planFile);
3060
+ const cwdResolved = resolve9(action.cwd);
3061
+ const checkTarget = (baseDir, target, kind) => {
3062
+ if (featureReal === null)
3063
+ return;
3064
+ const targetAbs = resolve9(baseDir, target);
3065
+ const canonical = canonicalizeNearestExisting(targetAbs);
3066
+ if (!isInside(canonical, featureReal)) {
3067
+ const declaredPrefix = targetAbs === featureReal || targetAbs.startsWith(`${featureReal}/`);
3068
+ if (declaredPrefix) {
3069
+ add("sdd.context.target-symlink-escape", `${kind} target "${target}" canonicalizes to "${canonical}", outside the feature worktree — symlink escape refused before mutation`);
3070
+ } else {
3071
+ add(`sdd.context.${kind}-target-outside-feature`, `${kind} target "${target}" resolves to "${targetAbs}", outside the feature worktree "${featureReal}" — refused before mutation`);
3072
+ }
3073
+ }
3074
+ };
3075
+ if (action.kind === "source") {
3076
+ const cwdReal = canonicalDir(cwdResolved);
3077
+ if (cwdReal === null) {
3078
+ add("sdd.context.cwd-missing", `observed source cwd "${action.cwd}" does not exist or is not a directory`);
3079
+ return { ok: false, violations };
3080
+ }
3081
+ if (featureReal === null || !isInside(cwdReal, featureReal)) {
3082
+ add("sdd.context.source-cwd-outside-feature", `observed source cwd "${cwdReal}" is outside the feature worktree "${context.featureCwd}" — a declared-correct context does not make a wrong-checkout write safe (A3)`, `run the source action from inside ${context.featureCwd}`);
3083
+ return { ok: false, violations };
3084
+ }
3085
+ if (action.target !== undefined)
3086
+ checkTarget(cwdReal, action.target, "source");
3087
+ } else if (action.kind === "artifact") {
3088
+ if (typeof action.target !== "string" || action.target.trim() === "") {
3089
+ add("sdd.context.target-missing", "artifact checks require the destination target");
3090
+ return { ok: false, violations };
3091
+ }
3092
+ const targetAbs = resolve9(cwdResolved, action.target);
3093
+ const canonical = canonicalizeNearestExisting(targetAbs);
3094
+ if (!isInside(canonical, canonicalSddDir) && canonical !== canonicalPlanFile) {
3095
+ const rawSddDir = resolve9(context.sddDir);
3096
+ const declaredPrefix = targetAbs === rawSddDir || targetAbs.startsWith(`${rawSddDir}/`) || targetAbs === canonicalSddDir || targetAbs.startsWith(`${canonicalSddDir}/`) || targetAbs === canonicalPlanFile;
3097
+ if (declaredPrefix) {
3098
+ add("sdd.context.artifact-symlink-escape", `artifact target "${targetAbs}" canonicalizes to "${canonical}", outside the plan's control sddDir — symlink escape refused before write`);
3099
+ } else {
3100
+ add("sdd.context.artifact-outside-plan", `artifact target "${targetAbs}" is outside the plan's control sddDir "${context.sddDir}" and is not the declared planFile "${context.planFile}" — legitimate control artifact edits stay inside the plan's artifacts; arbitrary control source edits are not allowed (A3)`);
3101
+ }
3102
+ }
3103
+ } else {
3104
+ if (featureReal === null) {
3105
+ add("sdd.context.launch-cwd-missing", `feature worktree "${context.featureCwd}" does not exist or is not a directory — cannot bind the child's starting cwd`);
3106
+ } else {
3107
+ violations.push(...assertBranchAlignment(featureReal, context.workingBranch).violations);
3108
+ }
3109
+ if (action.target !== undefined && action.target.trim() !== "" && featureReal !== null) {
3110
+ checkTarget(featureReal, action.target, "launch");
3111
+ }
3112
+ }
3113
+ return { ok: violations.length === 0, violations };
3114
+ }
3115
+ function signalExitNumber(signal) {
3116
+ return osConstants.signals[signal] ?? 0;
3117
+ }
3118
+ async function runInSddContext(context, argv) {
3119
+ if (!Array.isArray(argv) || argv.length === 0 || typeof argv[0] !== "string" || argv[0].trim() === "") {
3120
+ throwUsage("runInSddContext: argv must be [executable, ...args] with a non-empty executable — the array is passed to the child literally (no shell)");
3121
+ }
3122
+ const resolved = resolveSddExecutionContext(context);
3123
+ const gate2 = checkSddAction(resolved, { kind: "launch", cwd: process.cwd() });
3124
+ if (!gate2.ok)
3125
+ throwGateFail(gate2.violations);
3126
+ return await new Promise((settle, reject) => {
3127
+ let child;
3128
+ try {
3129
+ child = spawn(argv[0], argv.slice(1), {
3130
+ cwd: resolved.featureCwd,
3131
+ shell: false,
3132
+ stdio: "inherit"
3133
+ });
3134
+ } catch (err) {
3135
+ if (err.code === "ENOENT") {
3136
+ settle(127);
3137
+ return;
3138
+ }
3139
+ throw err;
3140
+ }
3141
+ let done = false;
3142
+ let spawnError = null;
3143
+ const forward = (signal) => {
3144
+ if (!done && child.exitCode === null && child.signalCode === null)
3145
+ child.kill(signal);
3146
+ };
3147
+ const cleanup = () => {
3148
+ process.removeListener("SIGINT", forward);
3149
+ process.removeListener("SIGTERM", forward);
3150
+ };
3151
+ process.on("SIGINT", forward);
3152
+ process.on("SIGTERM", forward);
3153
+ child.on("error", (err) => {
3154
+ spawnError = err;
3155
+ if (err.code !== "ENOENT" && !done) {
3156
+ done = true;
3157
+ cleanup();
3158
+ reject(err);
3159
+ }
3160
+ });
3161
+ child.on("close", (code, signal) => {
3162
+ if (done)
3163
+ return;
3164
+ done = true;
3165
+ cleanup();
3166
+ if (spawnError !== null) {
3167
+ if (spawnError.code === "ENOENT")
3168
+ settle(127);
3169
+ else
3170
+ reject(spawnError);
3171
+ return;
3172
+ }
3173
+ if (signal !== null)
3174
+ settle(128 + signalExitNumber(signal));
3175
+ else
3176
+ settle(code ?? 0);
3177
+ });
3178
+ });
3179
+ }
2777
3180
  // src/migrate.ts
2778
3181
  import { copyFileSync, mkdirSync as mkdirSync7, readFileSync as readFileSync8, readdirSync as readdirSync7, writeFileSync as writeFileSync5 } from "node:fs";
2779
- import { dirname as dirname7, isAbsolute as isAbsolute7, join as join11, relative as relative3, resolve as resolve10, sep as sep2 } from "node:path";
3182
+ import { dirname as dirname7, isAbsolute as isAbsolute7, join as join11, relative as relative4, resolve as resolve10, sep as sep2 } from "node:path";
2780
3183
  var MIGRATE_STATUS_FILE = "status.json";
2781
3184
  var ARCHIVED_STATUS_V1_FILE = "archived/status.v1.json";
2782
3185
  var NOTES_LEDGER_FILE = "notes.jsonl";
@@ -2793,7 +3196,7 @@ var ROOT_METADATA_LIFT_KEYS = {
2793
3196
  program_roadmap: true,
2794
3197
  updated_at: true
2795
3198
  };
2796
- function isPlainObject6(value) {
3199
+ function isPlainObject7(value) {
2797
3200
  return typeof value === "object" && value !== null && !Array.isArray(value);
2798
3201
  }
2799
3202
  function dateString(value) {
@@ -2973,10 +3376,10 @@ function applyRootMetadataLift(snapshot, metadata, migrationNotes, activeIterati
2973
3376
  if (typeof metadata.control_worktree_path === "string" && metadata.control_worktree_path !== "") {
2974
3377
  data.control_worktree_path = metadata.control_worktree_path;
2975
3378
  }
2976
- if (isPlainObject6(metadata.integration_merge_lease)) {
3379
+ if (isPlainObject7(metadata.integration_merge_lease)) {
2977
3380
  data.integration_merge_lease = metadata.integration_merge_lease;
2978
3381
  }
2979
- const legacyMetadata = isPlainObject6(data.legacy_metadata) ? { ...data.legacy_metadata } : {};
3382
+ const legacyMetadata = isPlainObject7(data.legacy_metadata) ? { ...data.legacy_metadata } : {};
2980
3383
  for (const [key, value] of Object.entries(metadata)) {
2981
3384
  if (key === "harness_root")
2982
3385
  continue;
@@ -3032,7 +3435,7 @@ function buildRegister(residualFindings, byPlan, projectId, migratedAt) {
3032
3435
  const raw = residualFindings[planId];
3033
3436
  if (!Array.isArray(raw))
3034
3437
  continue;
3035
- const open = raw.filter((entry) => isPlainObject6(entry) && isOpenResidual(entry)).sort((a, b) => {
3438
+ const open = raw.filter((entry) => isPlainObject7(entry) && isOpenResidual(entry)).sort((a, b) => {
3036
3439
  const aId = typeof a.id === "string" ? a.id : "";
3037
3440
  const bId = typeof b.id === "string" ? b.id : "";
3038
3441
  return compareIds(aId, bId);
@@ -3121,12 +3524,12 @@ function migrateHarnessTree(root, opts = {}) {
3121
3524
  if (legacy.version === undefined) {
3122
3525
  throw new Error(`refusing to migrate: no v1 status.json found at ${statusPath} (nothing to migrate)`);
3123
3526
  }
3124
- const rows = Array.isArray(legacy.plans) ? legacy.plans.filter(isPlainObject6) : [];
3527
+ const rows = Array.isArray(legacy.plans) ? legacy.plans.filter(isPlainObject7) : [];
3125
3528
  if (Array.isArray(legacy.plans)) {
3126
3529
  const unLiftable = [];
3127
3530
  const idCounts = new Map;
3128
3531
  for (const row of legacy.plans) {
3129
- if (!isPlainObject6(row) || rowIdOf(row) === null) {
3532
+ if (!isPlainObject7(row) || rowIdOf(row) === null) {
3130
3533
  unLiftable.push(row);
3131
3534
  continue;
3132
3535
  }
@@ -3142,7 +3545,7 @@ function migrateHarnessTree(root, opts = {}) {
3142
3545
  throw new Error(`refusing to migrate: ${duplicates.length} duplicate plan id(s) (${duplicates.join(", ")}) — every v1 row must land in exactly one snapshot`);
3143
3546
  }
3144
3547
  }
3145
- const metadata = isPlainObject6(legacy.metadata) ? legacy.metadata : {};
3548
+ const metadata = isPlainObject7(legacy.metadata) ? legacy.metadata : {};
3146
3549
  const rootUpdatedAt = dateString(legacy.updated_at) ?? dateString(metadata.updated_at) ?? todayString3();
3147
3550
  const migratedAt = dateString(metadata.updated_at) ?? rootUpdatedAt;
3148
3551
  const migrationNotes = [];
@@ -3179,9 +3582,9 @@ function migrateHarnessTree(root, opts = {}) {
3179
3582
  migrationNotes.push("no active iteration snapshot found — root metadata execution-policy/branch keys have no lift home and stay unmapped (visible here, not silently dropped)");
3180
3583
  }
3181
3584
  const notesFiles = collectNotesFiles(snapshots);
3182
- const residualFindings = isPlainObject6(legacy.residual_findings) ? legacy.residual_findings : {};
3585
+ const residualFindings = isPlainObject7(legacy.residual_findings) ? legacy.residual_findings : {};
3183
3586
  const register = buildRegister(residualFindings, byPlan, projectId, migratedAt);
3184
- const programRoadmap = isPlainObject6(metadata.program_roadmap) ? metadata.program_roadmap : null;
3587
+ const programRoadmap = isPlainObject7(metadata.program_roadmap) ? metadata.program_roadmap : null;
3185
3588
  let roadmap = null;
3186
3589
  if (programRoadmap) {
3187
3590
  const rawTitle = typeof programRoadmap.title === "string" && programRoadmap.title !== "" ? programRoadmap.title : "Program roadmap";
@@ -3250,8 +3653,8 @@ async function applyMigratePlan(plan) {
3250
3653
  if (!isAbsolute7(plan.workflowDir) || !isAbsolute7(plan.projectDir)) {
3251
3654
  throw new Error(`refusing to apply migration: plan workflowDir/projectDir must be absolute (got ${JSON.stringify(plan.workflowDir)} / ${JSON.stringify(plan.projectDir)})`);
3252
3655
  }
3253
- const workflowTargetOf = (canonicalFile) => join11(workflowRoot, relative3("workflows", canonicalFile));
3254
- const projectTargetOf = (canonicalFile) => join11(projectRoot, relative3("projects", canonicalFile));
3656
+ const workflowTargetOf = (canonicalFile) => join11(workflowRoot, relative4("workflows", canonicalFile));
3657
+ const projectTargetOf = (canonicalFile) => join11(projectRoot, relative4("projects", canonicalFile));
3255
3658
  const allDestinations = [
3256
3659
  plan.archive.file,
3257
3660
  ...plan.snapshots.map((snapshot) => snapshot.file),
@@ -4469,7 +4872,7 @@ function planFileRel(outDir, planFile) {
4469
4872
  }
4470
4873
  // src/compound.ts
4471
4874
  import { existsSync as existsSync9, readdirSync as readdirSync9, readFileSync as readFileSync10 } from "node:fs";
4472
- import { basename as basename6, isAbsolute as isAbsolute8, join as join13, relative as relative4, resolve as resolve12, sep as sep4 } from "node:path";
4875
+ import { basename as basename6, isAbsolute as isAbsolute8, join as join13, relative as relative5, resolve as resolve12, sep as sep4 } from "node:path";
4473
4876
  function violation10(severity, code, message, fix) {
4474
4877
  return { ok: false, severity, code, message, fix };
4475
4878
  }
@@ -4807,7 +5210,7 @@ function collectKnowledgeDocs(dir) {
4807
5210
  if (entry.isDirectory()) {
4808
5211
  stack.push(full);
4809
5212
  } else if (entry.name.endsWith(".md") && entry.name !== "README.md" && entry.name !== "index.md") {
4810
- docs.push(relative4(dir, full).split(sep4).join("/"));
5213
+ docs.push(relative5(dir, full).split(sep4).join("/"));
4811
5214
  }
4812
5215
  }
4813
5216
  }
@@ -4920,7 +5323,7 @@ function findTemporaryMarkers(fileText) {
4920
5323
  }
4921
5324
  markers.push({ line: i + 1, text, removalPath });
4922
5325
  if (removalPath === null) {
4923
- violations.push(violation11("medium", "lint.temporary.no-removal-path", `temporary marker at line ${i + 1} records no removal path (plan/status artifact reference) — record one before claiming the task complete (mstar-coding-behavior § Simplification markers)`, 'add a plan/status reference to the marker, e.g. "removal tracked in status.json" or "plan 20260808-slice2 removes this"'));
5326
+ violations.push(violation11("medium", "lint.temporary.no-removal-path", `temporary marker at line ${i + 1} records no removal path (plan/status artifact reference) — record one before claiming the task complete (mstar-coding-behavior § Simplification markers)`, 'add a plan/status reference to the marker, e.g. "removal tracked in status.json" or "plan 20991231-example-plan removes this"'));
4924
5327
  }
4925
5328
  }
4926
5329
  return { ok: violations.length === 0, violations, markers };
@@ -5219,6 +5622,14 @@ function validateRoleMapping(rolesDir, options = {}) {
5219
5622
  return { ok: violations.length === 0, violations };
5220
5623
  }
5221
5624
  var LOAD_ORDER_HEADING_RE = /^#{1,6}\s+[^\r\n]*\b(?:load[\s-]*order|first\s+action)\b[^\r\n]*$/i;
5625
+ var HUB_BOOTSTRAP_ASSERTIONS = [
5626
+ { marker: "identity-first", why: "identity boundary before any skill list" },
5627
+ { marker: "skill presets", why: "Assignment Skill presets decision field" },
5628
+ { marker: "none", why: "explicit none => identity only, no optional topic preset" },
5629
+ { marker: "standard", why: "omitted on a substantive round => standard preset" },
5630
+ { marker: "role-owned", why: "role-owned methods / evidence obligations load regardless of preset" },
5631
+ { marker: "unknown preset", why: "unknown preset / missing identity => Needs Context / Blocked, never infer PM" }
5632
+ ];
5222
5633
  function extractLoadOrderSection(text) {
5223
5634
  const lines = text.split(/\r?\n/);
5224
5635
  let start = -1;
@@ -5252,11 +5663,22 @@ function lintLoadOrder(skillTexts) {
5252
5663
  continue;
5253
5664
  const section = extractLoadOrderSection(text);
5254
5665
  if (section === null) {
5255
- violations.push(violation12("medium", "roles.loadorder.section.missing", `skill "${name}" has no Load Order / First action section — every mstar-* topic skill must declare its first read (mstar-harness-core § 加载约定; mstar-roles § Load Order (Required))`, `add a "## Load Order" section naming mstar-harness-core as the first read`));
5666
+ violations.push(violation12("medium", "roles.loadorder.section.missing", `skill "${name}" has no Load Order / First action section — every mstar-* topic skill must declare its first read (mstar-harness-core § 加载契约; mstar-roles § Load Order = single load-selection authority)`, `add a "## Load Order" section: topics name mstar-harness-core first; mstar-roles declares the Skill presets decision matrix`));
5667
+ continue;
5668
+ }
5669
+ if (name === "mstar-roles") {
5670
+ const lower = section.toLowerCase();
5671
+ const missing = HUB_BOOTSTRAP_ASSERTIONS.filter((a) => !lower.includes(a.marker.toLowerCase()));
5672
+ if (missing.length > 0) {
5673
+ violations.push(violation12("medium", "roles.loadorder.hub.bootstrap.missing", `skill "${name}" Load Order section is missing the hub bootstrap decision matrix: ${missing.map((m) => `"${m.marker}" (${m.why})`).join("; ")} (mstar-roles § Load Order = single load-selection authority)`, "declare identity-first, the Skill presets none/standard decision, role-owned methods, and the unknown-preset / missing-identity refusal in the Load Order section"));
5674
+ }
5675
+ if (!section.includes("mstar-harness-core")) {
5676
+ violations.push(violation12("medium", "roles.loadorder.core.missing", `skill "${name}" Load Order section does not point at mstar-harness-core — the hub bootstrap is exempt from core-first, but core remains the lifecycle/authorization conflict authority and global entry whenever loaded`, "keep the conditional mstar-harness-core pointer (global entry / conflict authority) in the Load Order section"));
5677
+ }
5256
5678
  continue;
5257
5679
  }
5258
5680
  if (!section.includes("mstar-harness-core")) {
5259
- violations.push(violation12("medium", "roles.loadorder.core.missing", `skill "${name}" Load Order section does not declare mstar-harness-core as its first dependency (mstar-harness-core § 加载约定: mstar-*(name mstar-harness-core)假定读者已 Read skill)`, "name mstar-harness-core first in the Load Order section"));
5681
+ violations.push(violation12("medium", "roles.loadorder.core.missing", `skill "${name}" Load Order section does not declare mstar-harness-core as its first dependency (mstar-harness-core § 加载契约: directly-invoked topics keep core as first dependency; the mstar-roles hub bootstrap is the only exception)`, "name mstar-harness-core first in the Load Order section"));
5260
5682
  }
5261
5683
  }
5262
5684
  return { ok: violations.length === 0, violations };
@@ -5338,6 +5760,16 @@ function collectHeadings(bodyText) {
5338
5760
  }
5339
5761
  return headings;
5340
5762
  }
5763
+ function classifySkillLint(skillId) {
5764
+ const id = skillId ?? "";
5765
+ if (id === "mstar-harness-core")
5766
+ return { kind: "core", mode: null };
5767
+ if (id === "mstar-skill-authoring")
5768
+ return { kind: "authoring", mode: "authoring" };
5769
+ if (id.startsWith("mstar-"))
5770
+ return { kind: "runtime", mode: "runtime" };
5771
+ return { kind: "authoring", mode: "authoring" };
5772
+ }
5341
5773
  var RUNTIME_HEADING_ALIASES = {
5342
5774
  workflow: ["process", "playbook"],
5343
5775
  "decision-rules": [
@@ -5413,7 +5845,7 @@ function computePrTally(input) {
5413
5845
  var REVIEW_SCHEMA_ID = "mstar.review/v1";
5414
5846
  var INSPECTOR_VERDICTS = ["comment", "request_changes", "approve"];
5415
5847
  var INSPECTOR_SEVERITIES = ["critical", "warning", "suggestion", "info"];
5416
- function isPlainObject7(value) {
5848
+ function isPlainObject8(value) {
5417
5849
  return typeof value === "object" && value !== null && !Array.isArray(value);
5418
5850
  }
5419
5851
  var TALLY_COUNT_KEYS = ["mustFix", "shouldFix", "nit", "unverified"];
@@ -5424,7 +5856,7 @@ function checkProvidedTallyShape(tally, violations) {
5424
5856
  if (typeof tally.scorePct !== "number" || !Number.isInteger(tally.scorePct) || tally.scorePct < 0 || tally.scorePct > 100) {
5425
5857
  violations.push(violation14("high", "review.tally-malformed", `tally.scorePct must be an integer in [0, 100] - got ${String(tally.scorePct)}`));
5426
5858
  }
5427
- if (!isPlainObject7(tally.tally)) {
5859
+ if (!isPlainObject8(tally.tally)) {
5428
5860
  violations.push(violation14("high", "review.tally-malformed", "tally.tally must be an object carrying the four class counts"));
5429
5861
  } else {
5430
5862
  for (const key of TALLY_COUNT_KEYS) {
@@ -5440,7 +5872,7 @@ function checkProvidedTallyShape(tally, violations) {
5440
5872
  }
5441
5873
  function validateMstarReviewV1(doc) {
5442
5874
  const violations = [];
5443
- if (!isPlainObject7(doc)) {
5875
+ if (!isPlainObject8(doc)) {
5444
5876
  return {
5445
5877
  ok: false,
5446
5878
  violations: [violation14("high", "review.not-object", "review document must be a JSON object")]
@@ -5467,7 +5899,7 @@ function validateMstarReviewV1(doc) {
5467
5899
  violations.push(violation14("high", "review.findings-not-array", "findings must be an array"));
5468
5900
  } else {
5469
5901
  doc.findings.forEach((finding, index) => {
5470
- if (!isPlainObject7(finding)) {
5902
+ if (!isPlainObject8(finding)) {
5471
5903
  violations.push(violation14("high", "review.invalid-finding", `findings[${index}] must be an object`));
5472
5904
  return;
5473
5905
  }
@@ -5507,7 +5939,7 @@ function validateMstarReviewV1(doc) {
5507
5939
  });
5508
5940
  }
5509
5941
  if (doc.tally !== undefined) {
5510
- if (!isPlainObject7(doc.tally)) {
5942
+ if (!isPlainObject8(doc.tally)) {
5511
5943
  violations.push(violation14("high", "review.invalid-tally", "tally must be a PrTallyResult object"));
5512
5944
  } else {
5513
5945
  checkProvidedTallyShape(doc.tally, violations);
@@ -5517,7 +5949,7 @@ function validateMstarReviewV1(doc) {
5517
5949
  }
5518
5950
  }
5519
5951
  if (doc.target !== undefined) {
5520
- if (!isPlainObject7(doc.target)) {
5952
+ if (!isPlainObject8(doc.target)) {
5521
5953
  violations.push(violation14("high", "review.invalid-target", "target must be an object"));
5522
5954
  } else {
5523
5955
  if (doc.target.owner !== undefined && typeof doc.target.owner !== "string") {
@@ -6198,7 +6630,10 @@ export {
6198
6630
  assertTriIdentity,
6199
6631
  assignmentHeaderRegion,
6200
6632
  canSteal,
6633
+ canonicalizeNearestExisting,
6634
+ checkSddAction,
6201
6635
  claimLease,
6636
+ classifySkillLint,
6202
6637
  closeProjectRegisterEntry,
6203
6638
  completenessLevel,
6204
6639
  composeDispatchGate,
@@ -6267,10 +6702,12 @@ export {
6267
6702
  resolveRepoEnforcement,
6268
6703
  resolveScaffoldDirs,
6269
6704
  resolveSddDir,
6705
+ resolveSddExecutionContext,
6270
6706
  resolveSkillRoot,
6271
6707
  resolveSpecsDir,
6272
6708
  resolveWorkflowDir,
6273
6709
  reviewPackage,
6710
+ runInSddContext,
6274
6711
  sameHolderResume,
6275
6712
  scaffoldAuditPlan,
6276
6713
  scaffoldHarness,