@mstar-harness/engine 3.6.3 → 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/audit.d.ts +11 -12
- package/dist/compound.d.ts +10 -10
- package/dist/core.d.ts +2 -3
- package/dist/dispatch.d.ts +57 -64
- package/dist/engine.js +467 -30
- package/dist/index.d.ts +6 -6
- package/dist/iteration.d.ts +3 -3
- package/dist/lint.d.ts +49 -49
- package/dist/migrate.d.ts +22 -22
- package/dist/path.d.ts +35 -25
- package/dist/project.d.ts +24 -26
- package/dist/prreview.d.ts +98 -99
- package/dist/roles.d.ts +22 -12
- package/dist/sdd.d.ts +198 -8
- package/dist/skill-authoring.d.ts +36 -9
- package/dist/status.d.ts +10 -11
- package/dist/store.d.ts +9 -12
- package/dist/workflow.d.ts +11 -11
- package/dist/worktree.d.ts +1 -2
- package/package.json +1 -1
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 {
|
|
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
|
|
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
|
|
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
|
|
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 (
|
|
3379
|
+
if (isPlainObject7(metadata.integration_merge_lease)) {
|
|
2977
3380
|
data.integration_merge_lease = metadata.integration_merge_lease;
|
|
2978
3381
|
}
|
|
2979
|
-
const legacyMetadata =
|
|
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) =>
|
|
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(
|
|
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 (!
|
|
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 =
|
|
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 =
|
|
3585
|
+
const residualFindings = isPlainObject7(legacy.residual_findings) ? legacy.residual_findings : {};
|
|
3183
3586
|
const register = buildRegister(residualFindings, byPlan, projectId, migratedAt);
|
|
3184
|
-
const programRoadmap =
|
|
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,
|
|
3254
|
-
const projectTargetOf = (canonicalFile) => join11(projectRoot,
|
|
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
|
|
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(
|
|
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
|
|
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 §
|
|
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 §
|
|
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
|
|
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 (!
|
|
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 (!
|
|
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 (!
|
|
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 (!
|
|
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 (!
|
|
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,
|