@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 +370 -120
- package/dist/gates.d.ts +69 -0
- package/dist/gates.test.d.ts +1 -0
- package/dist/index.d.ts +7 -2
- package/dist/worktree.d.ts +29 -4
- package/package.json +1 -1
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 !== ""
|
|
2413
|
-
|
|
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
|
-
|
|
2465
|
-
|
|
2466
|
-
violations
|
|
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
|
|
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
|
|
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(
|
|
2552
|
-
return
|
|
2553
|
-
if (isFile2(
|
|
2554
|
-
return
|
|
2555
|
-
if (hasWorkflowSnapshot(
|
|
2556
|
-
return
|
|
2557
|
-
if (hasWorkflowSnapshot(
|
|
2558
|
-
return
|
|
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 =
|
|
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(
|
|
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 :
|
|
2586
|
-
const common = isAbsolute6(commonRaw) ? 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 =
|
|
2591
|
-
const cmAbs =
|
|
2592
|
-
return
|
|
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 =
|
|
2672
|
+
root = realpathSync4(controlRoot);
|
|
2610
2673
|
} else {
|
|
2611
2674
|
const topLevel = gitOut(cwd, ["rev-parse", "--show-toplevel"]);
|
|
2612
|
-
root =
|
|
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(
|
|
2634
|
-
harnessDir =
|
|
2635
|
-
} else if (isDirectory2(
|
|
2636
|
-
harnessDir =
|
|
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 =
|
|
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(
|
|
2707
|
+
writeFileSync4(join11(sddDir, ".gitignore"), `*
|
|
2645
2708
|
`);
|
|
2646
|
-
return
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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(
|
|
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(
|
|
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() ?
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
2988
|
-
if (isInside(canonicalFeatureCwd, controlCheckout)) {
|
|
3050
|
+
if (isInside(canonicalControlHarnessRoot, canonicalFeatureCwd)) {
|
|
2989
3051
|
throwGateFail([
|
|
2990
|
-
contextViolation("critical", "sdd.context.
|
|
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
|
-
|
|
3055
|
+
const controlCheckout = probeCheckoutRoot(canonicalControlHarnessRoot);
|
|
3056
|
+
if (controlCheckout === null) {
|
|
2994
3057
|
throwGateFail([
|
|
2995
|
-
contextViolation("
|
|
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:
|
|
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
|
|
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 =
|
|
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 =
|
|
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:
|
|
3314
|
-
source:
|
|
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:
|
|
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:
|
|
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:
|
|
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 =
|
|
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 =
|
|
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:
|
|
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 =
|
|
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 =
|
|
3651
|
-
const workflowRoot =
|
|
3652
|
-
const projectRoot =
|
|
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) =>
|
|
3657
|
-
const projectTargetOf = (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 =
|
|
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(
|
|
3673
|
-
copyFileSync(statusPath,
|
|
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,
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
|
4123
|
-
import { basename as
|
|
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 =
|
|
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) =>
|
|
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(
|
|
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 =
|
|
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 =
|
|
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 =
|
|
4674
|
-
const carried =
|
|
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(
|
|
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(
|
|
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(
|
|
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:
|
|
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 ??
|
|
4991
|
+
const workflowId = options.workflowId ?? basename6(resolve12(outDir));
|
|
4750
4992
|
assertSafePathComponent(workflowId, "workflow id");
|
|
4751
|
-
const harnessDir =
|
|
4752
|
-
const statusPath =
|
|
4753
|
-
const workflowDir =
|
|
4754
|
-
const snapshotPath =
|
|
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(
|
|
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 (
|
|
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 ${
|
|
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 =
|
|
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 =
|
|
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
|
|
4875
|
-
import { basename as
|
|
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(
|
|
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 (
|
|
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 =
|
|
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(
|
|
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 =
|
|
5230
|
-
if (!
|
|
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
|
-
|
|
5256
|
-
|
|
5257
|
-
|
|
5258
|
-
|
|
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(
|
|
5504
|
+
return /^[^.]*\.[A-Za-z0-9]{1,10}$/.test(basename7(root));
|
|
5263
5505
|
}
|
|
5264
5506
|
function scopeGuard(path, allowedRoots) {
|
|
5265
|
-
const resolved =
|
|
5507
|
+
const resolved = resolve13(path);
|
|
5266
5508
|
for (const root of allowedRoots) {
|
|
5267
|
-
const r =
|
|
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
|
|
5516
|
-
import { join as
|
|
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 (!
|
|
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 ${
|
|
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
|
|
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
|
|
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 =
|
|
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. \`${
|
|
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. \`${
|
|
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
|
package/dist/gates.d.ts
ADDED
|
@@ -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,
|
|
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";
|
package/dist/worktree.d.ts
CHANGED
|
@@ -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
|
-
*
|
|
78
|
-
*
|
|
79
|
-
*
|
|
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.
|
|
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": {
|