@mstar-harness/engine 3.6.3 → 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/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 +820 -133
- package/dist/gates.d.ts +69 -0
- package/dist/gates.test.d.ts +1 -0
- package/dist/index.d.ts +13 -8
- 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 +30 -6
- 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);
|
|
@@ -2338,8 +2357,8 @@ function hasFiles(dir) {
|
|
|
2338
2357
|
}
|
|
2339
2358
|
// src/worktree.ts
|
|
2340
2359
|
import { execFileSync as execFileSync2 } from "node:child_process";
|
|
2341
|
-
import { existsSync as existsSync7 } from "node:fs";
|
|
2342
|
-
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";
|
|
2343
2362
|
var DEFAULT_PROBE_TIMEOUT_MS = 1e4;
|
|
2344
2363
|
function probeTimeoutMs() {
|
|
2345
2364
|
const raw = process.env.MSTAR_GIT_PROBE_TIMEOUT_MS;
|
|
@@ -2378,6 +2397,51 @@ function probeBranch(worktreePath, opts) {
|
|
|
2378
2397
|
return { error: detail };
|
|
2379
2398
|
}
|
|
2380
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
|
+
}
|
|
2381
2445
|
function l1PreDispatchCheck(input, opts = {}) {
|
|
2382
2446
|
const violations = [];
|
|
2383
2447
|
const { controlWorktreePath, leaseWorktreePath, leaseWorkingBranch, planId } = input;
|
|
@@ -2390,8 +2454,20 @@ function l1PreDispatchCheck(input, opts = {}) {
|
|
|
2390
2454
|
if (leaseWorkingBranch.trim() === "") {
|
|
2391
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"));
|
|
2392
2456
|
}
|
|
2393
|
-
if (controlWorktreePath !== "" && leaseWorktreePath !== ""
|
|
2394
|
-
|
|
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
|
+
}
|
|
2395
2471
|
}
|
|
2396
2472
|
if (leaseWorktreePath !== "" && !existsSync7(leaseWorktreePath)) {
|
|
2397
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>`));
|
|
@@ -2440,11 +2516,17 @@ function l2PreDispatchCheck(input, opts = {}) {
|
|
|
2440
2516
|
});
|
|
2441
2517
|
return gate(violations);
|
|
2442
2518
|
}
|
|
2443
|
-
function assertControlVsFeaturePath(controlWorktreePath, featureWorktreePath) {
|
|
2519
|
+
function assertControlVsFeaturePath(controlWorktreePath, featureWorktreePath, opts = {}) {
|
|
2444
2520
|
const violations = [];
|
|
2445
|
-
|
|
2446
|
-
|
|
2447
|
-
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>)"));
|
|
2448
2530
|
}
|
|
2449
2531
|
return gate(violations);
|
|
2450
2532
|
}
|
|
@@ -2489,9 +2571,10 @@ function singleReviewSnapshot(assignments) {
|
|
|
2489
2571
|
return gate(violations);
|
|
2490
2572
|
}
|
|
2491
2573
|
// src/sdd.ts
|
|
2492
|
-
import { execFileSync as execFileSync3 } from "node:child_process";
|
|
2493
|
-
import { mkdirSync as mkdirSync6, readdirSync as readdirSync6, readFileSync as readFileSync7, realpathSync as
|
|
2494
|
-
import {
|
|
2574
|
+
import { execFileSync as execFileSync3, spawn } from "node:child_process";
|
|
2575
|
+
import { mkdirSync as mkdirSync6, readdirSync as readdirSync6, readFileSync as readFileSync7, realpathSync as realpathSync4, statSync as statSync4, writeFileSync as writeFileSync4 } from "node:fs";
|
|
2576
|
+
import { constants as osConstants } from "node:os";
|
|
2577
|
+
import { basename as basename4, dirname as dirname6, isAbsolute as isAbsolute6, join as join11, relative as relative3, resolve as resolve9 } from "node:path";
|
|
2495
2578
|
class SddScriptError extends Error {
|
|
2496
2579
|
exitCode;
|
|
2497
2580
|
constructor(message, exitCode) {
|
|
@@ -2528,14 +2611,14 @@ function gitOut(cwd, args) {
|
|
|
2528
2611
|
}
|
|
2529
2612
|
}
|
|
2530
2613
|
function probeHarnessWithStatus(root) {
|
|
2531
|
-
if (isFile2(
|
|
2532
|
-
return
|
|
2533
|
-
if (isFile2(
|
|
2534
|
-
return
|
|
2535
|
-
if (hasWorkflowSnapshot(
|
|
2536
|
-
return
|
|
2537
|
-
if (hasWorkflowSnapshot(
|
|
2538
|
-
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");
|
|
2539
2622
|
return null;
|
|
2540
2623
|
}
|
|
2541
2624
|
function hasWorkflowSnapshot(harnessDir) {
|
|
@@ -2543,13 +2626,13 @@ function hasWorkflowSnapshot(harnessDir) {
|
|
|
2543
2626
|
try {
|
|
2544
2627
|
workflowsDir = resolveWorkflowDir(harnessDir, { harnessDir });
|
|
2545
2628
|
} catch {
|
|
2546
|
-
workflowsDir =
|
|
2629
|
+
workflowsDir = join11(harnessDir, "workflows");
|
|
2547
2630
|
}
|
|
2548
2631
|
if (!isDirectory2(workflowsDir))
|
|
2549
2632
|
return false;
|
|
2550
2633
|
try {
|
|
2551
2634
|
for (const entry of readdirSync6(workflowsDir, { withFileTypes: true })) {
|
|
2552
|
-
if (entry.isDirectory() && isFile2(
|
|
2635
|
+
if (entry.isDirectory() && isFile2(join11(workflowsDir, entry.name, "snapshot.json")))
|
|
2553
2636
|
return true;
|
|
2554
2637
|
}
|
|
2555
2638
|
} catch {
|
|
@@ -2562,14 +2645,14 @@ function isLinkedWorktree(root) {
|
|
|
2562
2645
|
const commonRaw = gitOut(root, ["rev-parse", "--git-common-dir"]);
|
|
2563
2646
|
if (gitDirRaw === null || commonRaw === null)
|
|
2564
2647
|
return false;
|
|
2565
|
-
const gitDir = isAbsolute6(gitDirRaw) ? gitDirRaw :
|
|
2566
|
-
const common = isAbsolute6(commonRaw) ? commonRaw :
|
|
2648
|
+
const gitDir = isAbsolute6(gitDirRaw) ? gitDirRaw : join11(root, gitDirRaw);
|
|
2649
|
+
const common = isAbsolute6(commonRaw) ? commonRaw : join11(root, commonRaw);
|
|
2567
2650
|
if (gitDir.includes("/.git/worktrees/") || gitDir.includes("/worktrees/"))
|
|
2568
2651
|
return true;
|
|
2569
2652
|
try {
|
|
2570
|
-
const gdParent =
|
|
2571
|
-
const cmAbs =
|
|
2572
|
-
return
|
|
2653
|
+
const gdParent = realpathSync4(dirname6(gitDir));
|
|
2654
|
+
const cmAbs = realpathSync4(common);
|
|
2655
|
+
return join11(gdParent, basename4(gitDir)) !== cmAbs && gitDir !== cmAbs;
|
|
2573
2656
|
} catch {
|
|
2574
2657
|
return false;
|
|
2575
2658
|
}
|
|
@@ -2586,10 +2669,10 @@ function sddWorkspace(planId, opts = {}) {
|
|
|
2586
2669
|
if (!isDirectory2(controlRoot)) {
|
|
2587
2670
|
throw new SddScriptError(`mstar sdd workspace: CONTROL_ROOT / MSTAR_CONTROL_ROOT is not a directory: ${controlRoot}`, 1);
|
|
2588
2671
|
}
|
|
2589
|
-
root =
|
|
2672
|
+
root = realpathSync4(controlRoot);
|
|
2590
2673
|
} else {
|
|
2591
2674
|
const topLevel = gitOut(cwd, ["rev-parse", "--show-toplevel"]);
|
|
2592
|
-
root =
|
|
2675
|
+
root = realpathSync4(topLevel ?? cwd);
|
|
2593
2676
|
}
|
|
2594
2677
|
if (!controlRoot && isLinkedWorktree(root)) {
|
|
2595
2678
|
throw new SddScriptError(`mstar sdd workspace: linked worktree at ${root} has no {HARNESS_DIR}/status.json (default gitignore).
|
|
@@ -2610,25 +2693,35 @@ function sddWorkspace(planId, opts = {}) {
|
|
|
2610
2693
|
const probed = probeHarnessWithStatus(root);
|
|
2611
2694
|
if (probed) {
|
|
2612
2695
|
harnessDir = probed;
|
|
2613
|
-
} else if (isDirectory2(
|
|
2614
|
-
harnessDir =
|
|
2615
|
-
} else if (isDirectory2(
|
|
2616
|
-
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");
|
|
2617
2700
|
} else {
|
|
2618
|
-
harnessDir =
|
|
2701
|
+
harnessDir = join11(root, ".mstar");
|
|
2619
2702
|
}
|
|
2620
2703
|
}
|
|
2621
2704
|
}
|
|
2622
2705
|
const sddDir = resolveSddDir(harnessDir, planId);
|
|
2623
2706
|
mkdirSync6(sddDir, { recursive: true });
|
|
2624
|
-
writeFileSync4(
|
|
2707
|
+
writeFileSync4(join11(sddDir, ".gitignore"), `*
|
|
2625
2708
|
`);
|
|
2626
|
-
return
|
|
2709
|
+
return realpathSync4(sddDir);
|
|
2627
2710
|
}
|
|
2628
2711
|
function taskBrief(planFile, taskN, outFile, opts = {}) {
|
|
2629
2712
|
if (!planFile || !Number.isInteger(taskN) || taskN < 1) {
|
|
2630
2713
|
throw new SddScriptError("usage: mstar sdd task-brief PLAN_FILE TASK_NUMBER [OUTFILE]", 2);
|
|
2631
2714
|
}
|
|
2715
|
+
const bound = opts.context;
|
|
2716
|
+
const observedCwd = opts.cwd ?? process.cwd();
|
|
2717
|
+
if (bound) {
|
|
2718
|
+
const inputPlan = canonicalizeNearestExisting(resolve9(observedCwd, planFile));
|
|
2719
|
+
if (inputPlan !== canonicalizeNearestExisting(bound.planFile)) {
|
|
2720
|
+
throwGateFail([
|
|
2721
|
+
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")
|
|
2722
|
+
]);
|
|
2723
|
+
}
|
|
2724
|
+
}
|
|
2632
2725
|
let content;
|
|
2633
2726
|
try {
|
|
2634
2727
|
content = readFileSync7(planFile, "utf8");
|
|
@@ -2636,15 +2729,26 @@ function taskBrief(planFile, taskN, outFile, opts = {}) {
|
|
|
2636
2729
|
throw new SddScriptError(`no such plan file: ${planFile}`, 2);
|
|
2637
2730
|
}
|
|
2638
2731
|
let out;
|
|
2732
|
+
let mkdirAfterGate = null;
|
|
2639
2733
|
if (outFile) {
|
|
2640
|
-
out = outFile;
|
|
2734
|
+
out = bound ? resolve9(observedCwd, outFile) : outFile;
|
|
2735
|
+
} else if (bound) {
|
|
2736
|
+
out = join11(bound.sddDir, `task-${taskN}-brief.md`);
|
|
2737
|
+
mkdirAfterGate = bound.sddDir;
|
|
2641
2738
|
} else {
|
|
2642
2739
|
const sddDir = opts.sddDir ?? process.env.SDD_DIR;
|
|
2643
2740
|
if (!sddDir) {
|
|
2644
2741
|
throw new SddScriptError("mstar sdd task-brief: set SDD_DIR or pass OUTFILE (run mstar sdd workspace PLAN_ID first)", 2);
|
|
2645
2742
|
}
|
|
2646
2743
|
mkdirSync6(sddDir, { recursive: true });
|
|
2647
|
-
out =
|
|
2744
|
+
out = join11(sddDir, `task-${taskN}-brief.md`);
|
|
2745
|
+
}
|
|
2746
|
+
if (bound) {
|
|
2747
|
+
const gate2 = checkSddAction(bound, { kind: "artifact", cwd: observedCwd, target: out });
|
|
2748
|
+
if (!gate2.ok)
|
|
2749
|
+
throwGateFail(gate2.violations);
|
|
2750
|
+
if (mkdirAfterGate !== null)
|
|
2751
|
+
mkdirSync6(mkdirAfterGate, { recursive: true });
|
|
2648
2752
|
}
|
|
2649
2753
|
const records = content.endsWith(`
|
|
2650
2754
|
`) ? content.split(`
|
|
@@ -2670,13 +2774,15 @@ function taskBrief(planFile, taskN, outFile, opts = {}) {
|
|
|
2670
2774
|
if (printed.length === 0) {
|
|
2671
2775
|
throw new SddScriptError(`task ${taskN} not found in ${planFile} (no heading matching Task ${taskN})`, 3);
|
|
2672
2776
|
}
|
|
2673
|
-
return out;
|
|
2777
|
+
return bound ? resolve9(observedCwd, out) : out;
|
|
2674
2778
|
}
|
|
2675
2779
|
function reviewPackage(base, head, outFile, opts = {}) {
|
|
2676
2780
|
if (!base || !head) {
|
|
2677
2781
|
throw new SddScriptError("usage: mstar sdd review-package BASE HEAD [OUTFILE]", 2);
|
|
2678
2782
|
}
|
|
2679
|
-
const
|
|
2783
|
+
const bound = opts.context;
|
|
2784
|
+
const cwd = bound && opts.cwd === undefined ? bound.featureCwd : opts.cwd ?? process.cwd();
|
|
2785
|
+
const observedCwd = opts.cwd ?? process.cwd();
|
|
2680
2786
|
const verifyRef = (ref, what) => {
|
|
2681
2787
|
try {
|
|
2682
2788
|
execFileSync3("git", ["rev-parse", "--verify", "--quiet", ref], { cwd, stdio: ["ignore", "pipe", "pipe"] });
|
|
@@ -2687,8 +2793,14 @@ function reviewPackage(base, head, outFile, opts = {}) {
|
|
|
2687
2793
|
verifyRef(base, "BASE");
|
|
2688
2794
|
verifyRef(head, "HEAD");
|
|
2689
2795
|
let out;
|
|
2796
|
+
let mkdirAfterGate = null;
|
|
2690
2797
|
if (outFile) {
|
|
2691
|
-
out = outFile;
|
|
2798
|
+
out = bound ? resolve9(observedCwd, outFile) : outFile;
|
|
2799
|
+
} else if (bound) {
|
|
2800
|
+
const shortBase = gitOut(cwd, ["rev-parse", "--short", base]) ?? base;
|
|
2801
|
+
const shortHead = gitOut(cwd, ["rev-parse", "--short", head]) ?? head;
|
|
2802
|
+
out = join11(bound.sddDir, `review-${shortBase}..${shortHead}.diff`);
|
|
2803
|
+
mkdirAfterGate = bound.sddDir;
|
|
2692
2804
|
} else {
|
|
2693
2805
|
const sddDir = opts.sddDir ?? process.env.SDD_DIR;
|
|
2694
2806
|
if (!sddDir) {
|
|
@@ -2697,7 +2809,14 @@ function reviewPackage(base, head, outFile, opts = {}) {
|
|
|
2697
2809
|
mkdirSync6(sddDir, { recursive: true });
|
|
2698
2810
|
const shortBase = gitOut(cwd, ["rev-parse", "--short", base]) ?? base;
|
|
2699
2811
|
const shortHead = gitOut(cwd, ["rev-parse", "--short", head]) ?? head;
|
|
2700
|
-
out =
|
|
2812
|
+
out = join11(sddDir, `review-${shortBase}..${shortHead}.diff`);
|
|
2813
|
+
}
|
|
2814
|
+
if (bound) {
|
|
2815
|
+
const gate2 = checkSddAction(bound, { kind: "artifact", cwd: observedCwd, target: out });
|
|
2816
|
+
if (!gate2.ok)
|
|
2817
|
+
throwGateFail(gate2.violations);
|
|
2818
|
+
if (mkdirAfterGate !== null)
|
|
2819
|
+
mkdirSync6(mkdirAfterGate, { recursive: true });
|
|
2701
2820
|
}
|
|
2702
2821
|
const run = (args) => execFileSync3("git", args, { cwd, maxBuffer: GIT_CAPTURE_MAX_BYTES });
|
|
2703
2822
|
const parts = [
|
|
@@ -2716,7 +2835,7 @@ function reviewPackage(base, head, outFile, opts = {}) {
|
|
|
2716
2835
|
run(["diff", "-U10", `${base}..${head}`])
|
|
2717
2836
|
];
|
|
2718
2837
|
writeFileSync4(out, Buffer.concat(parts));
|
|
2719
|
-
return out;
|
|
2838
|
+
return bound ? resolve9(observedCwd, out) : out;
|
|
2720
2839
|
}
|
|
2721
2840
|
function assertBaseSha(ref, opts = {}) {
|
|
2722
2841
|
if (typeof ref !== "string" || !/^[0-9a-f]{4,40}$/i.test(ref)) {
|
|
@@ -2733,7 +2852,7 @@ function assertBaseSha(ref, opts = {}) {
|
|
|
2733
2852
|
}
|
|
2734
2853
|
function taskReportExists(sddDir, taskN) {
|
|
2735
2854
|
try {
|
|
2736
|
-
const st = statSync4(
|
|
2855
|
+
const st = statSync4(join11(sddDir, `task-${taskN}-report.md`));
|
|
2737
2856
|
return st.isFile() && st.size > 0;
|
|
2738
2857
|
} catch {
|
|
2739
2858
|
return false;
|
|
@@ -2742,7 +2861,7 @@ function taskReportExists(sddDir, taskN) {
|
|
|
2742
2861
|
function readProgressLedger(sddDir) {
|
|
2743
2862
|
let content;
|
|
2744
2863
|
try {
|
|
2745
|
-
content = readFileSync7(
|
|
2864
|
+
content = readFileSync7(join11(sddDir, "progress.md"), "utf8");
|
|
2746
2865
|
} catch {
|
|
2747
2866
|
return [];
|
|
2748
2867
|
}
|
|
@@ -2774,9 +2893,535 @@ function implementerSessionStickyRules(input) {
|
|
|
2774
2893
|
}
|
|
2775
2894
|
return { resume: true, reason: `sticky resume OK: host_agent_id ${session.host_agent_id}, next task ${nextTask}` };
|
|
2776
2895
|
}
|
|
2896
|
+
function isPlainObject6(value) {
|
|
2897
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2898
|
+
}
|
|
2899
|
+
function isInside(child, ancestor) {
|
|
2900
|
+
const rel = relative3(ancestor, child);
|
|
2901
|
+
return rel === "" || !rel.startsWith("..") && !isAbsolute6(rel);
|
|
2902
|
+
}
|
|
2903
|
+
function canonicalDir(path) {
|
|
2904
|
+
try {
|
|
2905
|
+
return statSync4(path).isDirectory() ? realpathSync4(path) : null;
|
|
2906
|
+
} catch {
|
|
2907
|
+
return null;
|
|
2908
|
+
}
|
|
2909
|
+
}
|
|
2910
|
+
function contextViolation(severity, code, message, fix) {
|
|
2911
|
+
return { ok: false, severity, code, message, fix };
|
|
2912
|
+
}
|
|
2913
|
+
function throwGateFail(violations) {
|
|
2914
|
+
const detail = violations.map((v) => `${v.code}: ${v.message}${v.fix ? ` (fix: ${v.fix})` : ""}`).join(`
|
|
2915
|
+
`);
|
|
2916
|
+
throw new SddScriptError(`SDD execution context rejected:
|
|
2917
|
+
${detail}`, 1);
|
|
2918
|
+
}
|
|
2919
|
+
function throwUsage(message) {
|
|
2920
|
+
throw new SddScriptError(message, 2);
|
|
2921
|
+
}
|
|
2922
|
+
function readActiveWorkflowIds(controlHarnessRoot) {
|
|
2923
|
+
const statusPath = join11(controlHarnessRoot, "status.json");
|
|
2924
|
+
if (!isFile2(statusPath))
|
|
2925
|
+
return null;
|
|
2926
|
+
let doc;
|
|
2927
|
+
try {
|
|
2928
|
+
doc = readJson(statusPath);
|
|
2929
|
+
} catch {
|
|
2930
|
+
return null;
|
|
2931
|
+
}
|
|
2932
|
+
if (!isPlainObject6(doc) || doc.version !== 2 || !Array.isArray(doc.workflows))
|
|
2933
|
+
return null;
|
|
2934
|
+
const ids = new Set;
|
|
2935
|
+
for (const entry of doc.workflows) {
|
|
2936
|
+
if (isPlainObject6(entry) && typeof entry.id === "string")
|
|
2937
|
+
ids.add(entry.id);
|
|
2938
|
+
}
|
|
2939
|
+
return ids;
|
|
2940
|
+
}
|
|
2941
|
+
function findWorkflowPlanRow(controlHarnessRoot, planId) {
|
|
2942
|
+
let workflowsDir;
|
|
2943
|
+
try {
|
|
2944
|
+
workflowsDir = resolveWorkflowDir(controlHarnessRoot, { harnessDir: controlHarnessRoot });
|
|
2945
|
+
} catch {
|
|
2946
|
+
return { kind: "none" };
|
|
2947
|
+
}
|
|
2948
|
+
if (!isDirectory2(workflowsDir))
|
|
2949
|
+
return { kind: "none" };
|
|
2950
|
+
let workflowIds;
|
|
2951
|
+
try {
|
|
2952
|
+
workflowIds = readdirSync6(workflowsDir, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => entry.name);
|
|
2953
|
+
} catch {
|
|
2954
|
+
return { kind: "none" };
|
|
2955
|
+
}
|
|
2956
|
+
const matches = [];
|
|
2957
|
+
for (const id of workflowIds) {
|
|
2958
|
+
const snapshotPath = join11(workflowsDir, id, WORKFLOW_SNAPSHOT_FILE);
|
|
2959
|
+
if (!isFile2(snapshotPath))
|
|
2960
|
+
continue;
|
|
2961
|
+
let doc;
|
|
2962
|
+
try {
|
|
2963
|
+
doc = readJson(snapshotPath);
|
|
2964
|
+
} catch {
|
|
2965
|
+
continue;
|
|
2966
|
+
}
|
|
2967
|
+
const plans = doc.plans;
|
|
2968
|
+
if (!Array.isArray(plans))
|
|
2969
|
+
continue;
|
|
2970
|
+
for (const row of plans) {
|
|
2971
|
+
if (isPlainObject6(row) && (row.id === planId || row.plan_id === planId)) {
|
|
2972
|
+
matches.push({ workflowId: id, row });
|
|
2973
|
+
break;
|
|
2974
|
+
}
|
|
2975
|
+
}
|
|
2976
|
+
}
|
|
2977
|
+
if (matches.length === 0)
|
|
2978
|
+
return { kind: "none" };
|
|
2979
|
+
const registeredActive = readActiveWorkflowIds(controlHarnessRoot);
|
|
2980
|
+
if (registeredActive === null) {
|
|
2981
|
+
return { kind: "row", workflowId: matches[0].workflowId, row: matches[0].row };
|
|
2982
|
+
}
|
|
2983
|
+
const active = matches.filter((m) => registeredActive.has(m.workflowId));
|
|
2984
|
+
if (active.length === 0)
|
|
2985
|
+
return { kind: "none" };
|
|
2986
|
+
if (active.length > 1) {
|
|
2987
|
+
return { kind: "ambiguous", workflowIds: active.map((m) => m.workflowId) };
|
|
2988
|
+
}
|
|
2989
|
+
return { kind: "row", workflowId: active[0].workflowId, row: active[0].row };
|
|
2990
|
+
}
|
|
2991
|
+
function resolveSddExecutionContext(input) {
|
|
2992
|
+
const { planId, workingBranch } = input;
|
|
2993
|
+
if (typeof planId !== "string" || planId.trim() === "") {
|
|
2994
|
+
throwUsage("SddExecutionContext.planId must be a non-empty string");
|
|
2995
|
+
}
|
|
2996
|
+
if (typeof workingBranch !== "string" || workingBranch.trim() === "") {
|
|
2997
|
+
throwUsage("SddExecutionContext.workingBranch must be a non-empty string");
|
|
2998
|
+
}
|
|
2999
|
+
for (const field of ["controlHarnessRoot", "featureCwd", "planFile", "sddDir"]) {
|
|
3000
|
+
const value = input[field];
|
|
3001
|
+
if (typeof value !== "string" || value.trim() === "") {
|
|
3002
|
+
throwUsage(`SddExecutionContext.${field} must be a non-empty string`);
|
|
3003
|
+
}
|
|
3004
|
+
if (!isAbsolute6(value)) {
|
|
3005
|
+
throwUsage(`SddExecutionContext.${field} must be an absolute path (A3: all paths normalized absolute); got ${JSON.stringify(value)}`);
|
|
3006
|
+
}
|
|
3007
|
+
}
|
|
3008
|
+
try {
|
|
3009
|
+
assertSafePathComponent(planId, "SddExecutionContext.planId");
|
|
3010
|
+
} catch (err) {
|
|
3011
|
+
throwUsage(`SddExecutionContext rejected: ${err.message}`);
|
|
3012
|
+
}
|
|
3013
|
+
const canonicalControlHarnessRoot = canonicalDir(input.controlHarnessRoot);
|
|
3014
|
+
if (canonicalControlHarnessRoot === null) {
|
|
3015
|
+
throwGateFail([
|
|
3016
|
+
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)`)
|
|
3017
|
+
]);
|
|
3018
|
+
}
|
|
3019
|
+
const stem = basename4(input.planFile).replace(/\.md$/, "");
|
|
3020
|
+
if (stem !== planId) {
|
|
3021
|
+
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`);
|
|
3022
|
+
}
|
|
3023
|
+
if (!isFile2(input.planFile)) {
|
|
3024
|
+
throwUsage(`no such plan file: ${input.planFile}`);
|
|
3025
|
+
}
|
|
3026
|
+
const planGate = assertPlanWritingPath(input.planFile, input.controlHarnessRoot);
|
|
3027
|
+
if (!planGate.ok) {
|
|
3028
|
+
if (planGate.code === "plan-path.symlink-escape") {
|
|
3029
|
+
throwGateFail([planGate]);
|
|
3030
|
+
}
|
|
3031
|
+
throwUsage(`SddExecutionContext.planFile rejected: ${planGate.code}: ${planGate.message}`);
|
|
3032
|
+
}
|
|
3033
|
+
const composedSddDir = resolveSddDir(canonicalControlHarnessRoot, planId);
|
|
3034
|
+
const canonicalComposedSddDir = canonicalizeNearestExisting(composedSddDir);
|
|
3035
|
+
const canonicalSddDir = canonicalizeNearestExisting(input.sddDir);
|
|
3036
|
+
if (canonicalSddDir !== canonicalComposedSddDir) {
|
|
3037
|
+
if (!isInside(canonicalSddDir, canonicalControlHarnessRoot)) {
|
|
3038
|
+
throwGateFail([
|
|
3039
|
+
contextViolation("high", "sdd.context.sdd-dir-escape", `sddDir "${input.sddDir}" canonicalizes to "${canonicalSddDir}", outside the control harness "${canonicalControlHarnessRoot}" — symlink escape refused (environmental gate failure)`)
|
|
3040
|
+
]);
|
|
3041
|
+
}
|
|
3042
|
+
throwUsage(`SddExecutionContext.sddDir "${input.sddDir}" does not match plan "${planId}" — expected the {SDD_DIR} composition ${composedSddDir}`);
|
|
3043
|
+
}
|
|
3044
|
+
const canonicalFeatureCwd = canonicalDir(input.featureCwd);
|
|
3045
|
+
if (canonicalFeatureCwd === null) {
|
|
3046
|
+
throwGateFail([
|
|
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})`)
|
|
3048
|
+
]);
|
|
3049
|
+
}
|
|
3050
|
+
if (isInside(canonicalControlHarnessRoot, canonicalFeatureCwd)) {
|
|
3051
|
+
throwGateFail([
|
|
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`)
|
|
3053
|
+
]);
|
|
3054
|
+
}
|
|
3055
|
+
const controlCheckout = probeCheckoutRoot(canonicalControlHarnessRoot);
|
|
3056
|
+
if (controlCheckout === null) {
|
|
3057
|
+
throwGateFail([
|
|
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>)")
|
|
3064
|
+
]);
|
|
3065
|
+
}
|
|
3066
|
+
const match = findWorkflowPlanRow(canonicalControlHarnessRoot, planId);
|
|
3067
|
+
if (match.kind === "ambiguous") {
|
|
3068
|
+
throwGateFail([
|
|
3069
|
+
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")
|
|
3070
|
+
]);
|
|
3071
|
+
}
|
|
3072
|
+
const row = match.kind === "row" ? match.row : null;
|
|
3073
|
+
if (row !== null && row.execution_lease !== undefined) {
|
|
3074
|
+
const leaseVerify = verifyPlanExecutionLease(row, planId);
|
|
3075
|
+
if (!leaseVerify.ok)
|
|
3076
|
+
throwGateFail(leaseVerify.violations);
|
|
3077
|
+
const lease = leaseVerify.lease;
|
|
3078
|
+
const l1 = l1PreDispatchCheck({
|
|
3079
|
+
controlWorktreePath: controlCheckout,
|
|
3080
|
+
leaseWorktreePath: lease.worktree_path,
|
|
3081
|
+
leaseWorkingBranch: lease.working_branch,
|
|
3082
|
+
planId
|
|
3083
|
+
});
|
|
3084
|
+
if (!l1.ok)
|
|
3085
|
+
throwGateFail(l1.violations);
|
|
3086
|
+
if (canonicalizeNearestExisting(lease.worktree_path) !== canonicalFeatureCwd) {
|
|
3087
|
+
throwGateFail([
|
|
3088
|
+
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)`)
|
|
3089
|
+
]);
|
|
3090
|
+
}
|
|
3091
|
+
if (workingBranch !== lease.working_branch) {
|
|
3092
|
+
throwGateFail([
|
|
3093
|
+
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}")`)
|
|
3094
|
+
]);
|
|
3095
|
+
}
|
|
3096
|
+
} else if (row !== null && row.status === "InProgress") {
|
|
3097
|
+
throwGateFail(verifyPlanExecutionLease(row, planId).violations);
|
|
3098
|
+
} else {
|
|
3099
|
+
const branchGate = assertBranchAlignment(canonicalFeatureCwd, workingBranch);
|
|
3100
|
+
if (!branchGate.ok)
|
|
3101
|
+
throwGateFail(branchGate.violations);
|
|
3102
|
+
}
|
|
3103
|
+
return {
|
|
3104
|
+
planId,
|
|
3105
|
+
controlHarnessRoot: canonicalControlHarnessRoot,
|
|
3106
|
+
featureCwd: canonicalFeatureCwd,
|
|
3107
|
+
workingBranch,
|
|
3108
|
+
planFile: realpathSync4(input.planFile),
|
|
3109
|
+
sddDir: canonicalSddDir
|
|
3110
|
+
};
|
|
3111
|
+
}
|
|
3112
|
+
function checkSddAction(context, action) {
|
|
3113
|
+
const violations = [];
|
|
3114
|
+
const add = (code, message, fix) => {
|
|
3115
|
+
violations.push(contextViolation("high", code, message, fix));
|
|
3116
|
+
};
|
|
3117
|
+
if (action.kind !== "source" && action.kind !== "artifact" && action.kind !== "launch") {
|
|
3118
|
+
add("sdd.context.kind-unknown", `unknown action kind ${JSON.stringify(action.kind)} — expected "source" | "artifact" | "launch"`);
|
|
3119
|
+
return { ok: false, violations };
|
|
3120
|
+
}
|
|
3121
|
+
if (typeof action.cwd !== "string" || action.cwd.trim() === "") {
|
|
3122
|
+
add("sdd.context.cwd-missing", "action.cwd (the observed invocation cwd) is required — never an Assignment echo");
|
|
3123
|
+
return { ok: false, violations };
|
|
3124
|
+
}
|
|
3125
|
+
const featureReal = canonicalDir(context.featureCwd);
|
|
3126
|
+
const canonicalSddDir = canonicalizeNearestExisting(context.sddDir);
|
|
3127
|
+
const canonicalPlanFile = canonicalizeNearestExisting(context.planFile);
|
|
3128
|
+
const cwdResolved = resolve9(action.cwd);
|
|
3129
|
+
const checkTarget = (baseDir, target, kind) => {
|
|
3130
|
+
if (featureReal === null)
|
|
3131
|
+
return;
|
|
3132
|
+
const targetAbs = resolve9(baseDir, target);
|
|
3133
|
+
const canonical = canonicalizeNearestExisting(targetAbs);
|
|
3134
|
+
if (!isInside(canonical, featureReal)) {
|
|
3135
|
+
const declaredPrefix = targetAbs === featureReal || targetAbs.startsWith(`${featureReal}/`);
|
|
3136
|
+
if (declaredPrefix) {
|
|
3137
|
+
add("sdd.context.target-symlink-escape", `${kind} target "${target}" canonicalizes to "${canonical}", outside the feature worktree — symlink escape refused before mutation`);
|
|
3138
|
+
} else {
|
|
3139
|
+
add(`sdd.context.${kind}-target-outside-feature`, `${kind} target "${target}" resolves to "${targetAbs}", outside the feature worktree "${featureReal}" — refused before mutation`);
|
|
3140
|
+
}
|
|
3141
|
+
}
|
|
3142
|
+
};
|
|
3143
|
+
if (action.kind === "source") {
|
|
3144
|
+
const cwdReal = canonicalDir(cwdResolved);
|
|
3145
|
+
if (cwdReal === null) {
|
|
3146
|
+
add("sdd.context.cwd-missing", `observed source cwd "${action.cwd}" does not exist or is not a directory`);
|
|
3147
|
+
return { ok: false, violations };
|
|
3148
|
+
}
|
|
3149
|
+
if (featureReal === null || !isInside(cwdReal, featureReal)) {
|
|
3150
|
+
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}`);
|
|
3151
|
+
return { ok: false, violations };
|
|
3152
|
+
}
|
|
3153
|
+
if (action.target !== undefined)
|
|
3154
|
+
checkTarget(cwdReal, action.target, "source");
|
|
3155
|
+
} else if (action.kind === "artifact") {
|
|
3156
|
+
if (typeof action.target !== "string" || action.target.trim() === "") {
|
|
3157
|
+
add("sdd.context.target-missing", "artifact checks require the destination target");
|
|
3158
|
+
return { ok: false, violations };
|
|
3159
|
+
}
|
|
3160
|
+
const targetAbs = resolve9(cwdResolved, action.target);
|
|
3161
|
+
const canonical = canonicalizeNearestExisting(targetAbs);
|
|
3162
|
+
if (!isInside(canonical, canonicalSddDir) && canonical !== canonicalPlanFile) {
|
|
3163
|
+
const rawSddDir = resolve9(context.sddDir);
|
|
3164
|
+
const declaredPrefix = targetAbs === rawSddDir || targetAbs.startsWith(`${rawSddDir}/`) || targetAbs === canonicalSddDir || targetAbs.startsWith(`${canonicalSddDir}/`) || targetAbs === canonicalPlanFile;
|
|
3165
|
+
if (declaredPrefix) {
|
|
3166
|
+
add("sdd.context.artifact-symlink-escape", `artifact target "${targetAbs}" canonicalizes to "${canonical}", outside the plan's control sddDir — symlink escape refused before write`);
|
|
3167
|
+
} else {
|
|
3168
|
+
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)`);
|
|
3169
|
+
}
|
|
3170
|
+
}
|
|
3171
|
+
} else {
|
|
3172
|
+
if (featureReal === null) {
|
|
3173
|
+
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`);
|
|
3174
|
+
} else {
|
|
3175
|
+
violations.push(...assertBranchAlignment(featureReal, context.workingBranch).violations);
|
|
3176
|
+
}
|
|
3177
|
+
if (action.target !== undefined && action.target.trim() !== "" && featureReal !== null) {
|
|
3178
|
+
checkTarget(featureReal, action.target, "launch");
|
|
3179
|
+
}
|
|
3180
|
+
}
|
|
3181
|
+
return { ok: violations.length === 0, violations };
|
|
3182
|
+
}
|
|
3183
|
+
function signalExitNumber(signal) {
|
|
3184
|
+
return osConstants.signals[signal] ?? 0;
|
|
3185
|
+
}
|
|
3186
|
+
async function runInSddContext(context, argv) {
|
|
3187
|
+
if (!Array.isArray(argv) || argv.length === 0 || typeof argv[0] !== "string" || argv[0].trim() === "") {
|
|
3188
|
+
throwUsage("runInSddContext: argv must be [executable, ...args] with a non-empty executable — the array is passed to the child literally (no shell)");
|
|
3189
|
+
}
|
|
3190
|
+
const resolved = resolveSddExecutionContext(context);
|
|
3191
|
+
const gate2 = checkSddAction(resolved, { kind: "launch", cwd: process.cwd() });
|
|
3192
|
+
if (!gate2.ok)
|
|
3193
|
+
throwGateFail(gate2.violations);
|
|
3194
|
+
return await new Promise((settle, reject) => {
|
|
3195
|
+
let child;
|
|
3196
|
+
try {
|
|
3197
|
+
child = spawn(argv[0], argv.slice(1), {
|
|
3198
|
+
cwd: resolved.featureCwd,
|
|
3199
|
+
shell: false,
|
|
3200
|
+
stdio: "inherit"
|
|
3201
|
+
});
|
|
3202
|
+
} catch (err) {
|
|
3203
|
+
if (err.code === "ENOENT") {
|
|
3204
|
+
settle(127);
|
|
3205
|
+
return;
|
|
3206
|
+
}
|
|
3207
|
+
throw err;
|
|
3208
|
+
}
|
|
3209
|
+
let done = false;
|
|
3210
|
+
let spawnError = null;
|
|
3211
|
+
const forward = (signal) => {
|
|
3212
|
+
if (!done && child.exitCode === null && child.signalCode === null)
|
|
3213
|
+
child.kill(signal);
|
|
3214
|
+
};
|
|
3215
|
+
const cleanup = () => {
|
|
3216
|
+
process.removeListener("SIGINT", forward);
|
|
3217
|
+
process.removeListener("SIGTERM", forward);
|
|
3218
|
+
};
|
|
3219
|
+
process.on("SIGINT", forward);
|
|
3220
|
+
process.on("SIGTERM", forward);
|
|
3221
|
+
child.on("error", (err) => {
|
|
3222
|
+
spawnError = err;
|
|
3223
|
+
if (err.code !== "ENOENT" && !done) {
|
|
3224
|
+
done = true;
|
|
3225
|
+
cleanup();
|
|
3226
|
+
reject(err);
|
|
3227
|
+
}
|
|
3228
|
+
});
|
|
3229
|
+
child.on("close", (code, signal) => {
|
|
3230
|
+
if (done)
|
|
3231
|
+
return;
|
|
3232
|
+
done = true;
|
|
3233
|
+
cleanup();
|
|
3234
|
+
if (spawnError !== null) {
|
|
3235
|
+
if (spawnError.code === "ENOENT")
|
|
3236
|
+
settle(127);
|
|
3237
|
+
else
|
|
3238
|
+
reject(spawnError);
|
|
3239
|
+
return;
|
|
3240
|
+
}
|
|
3241
|
+
if (signal !== null)
|
|
3242
|
+
settle(128 + signalExitNumber(signal));
|
|
3243
|
+
else
|
|
3244
|
+
settle(code ?? 0);
|
|
3245
|
+
});
|
|
3246
|
+
});
|
|
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
|
+
}
|
|
2777
3422
|
// src/migrate.ts
|
|
2778
3423
|
import { copyFileSync, mkdirSync as mkdirSync7, readFileSync as readFileSync8, readdirSync as readdirSync7, writeFileSync as writeFileSync5 } from "node:fs";
|
|
2779
|
-
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";
|
|
2780
3425
|
var MIGRATE_STATUS_FILE = "status.json";
|
|
2781
3426
|
var ARCHIVED_STATUS_V1_FILE = "archived/status.v1.json";
|
|
2782
3427
|
var NOTES_LEDGER_FILE = "notes.jsonl";
|
|
@@ -2793,7 +3438,7 @@ var ROOT_METADATA_LIFT_KEYS = {
|
|
|
2793
3438
|
program_roadmap: true,
|
|
2794
3439
|
updated_at: true
|
|
2795
3440
|
};
|
|
2796
|
-
function
|
|
3441
|
+
function isPlainObject7(value) {
|
|
2797
3442
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2798
3443
|
}
|
|
2799
3444
|
function dateString(value) {
|
|
@@ -2816,7 +3461,7 @@ function todayString3() {
|
|
|
2816
3461
|
return `${now.getFullYear()}-${month}-${day}`;
|
|
2817
3462
|
}
|
|
2818
3463
|
function scanCompasses(harnessDir) {
|
|
2819
|
-
const iterationsDir =
|
|
3464
|
+
const iterationsDir = join13(harnessDir, "iterations");
|
|
2820
3465
|
const out = [];
|
|
2821
3466
|
let entries;
|
|
2822
3467
|
try {
|
|
@@ -2827,7 +3472,7 @@ function scanCompasses(harnessDir) {
|
|
|
2827
3472
|
for (const entry of entries) {
|
|
2828
3473
|
if (!entry.isDirectory())
|
|
2829
3474
|
continue;
|
|
2830
|
-
const compassPath =
|
|
3475
|
+
const compassPath = join13(iterationsDir, entry.name, "delivery-compass.md");
|
|
2831
3476
|
let content;
|
|
2832
3477
|
try {
|
|
2833
3478
|
content = readFileSync8(compassPath, "utf8");
|
|
@@ -2907,8 +3552,8 @@ function buildIterationSnapshot(compass, rowById, rootUpdatedAt) {
|
|
|
2907
3552
|
id: compass.id,
|
|
2908
3553
|
type: "iteration",
|
|
2909
3554
|
status,
|
|
2910
|
-
file:
|
|
2911
|
-
source:
|
|
3555
|
+
file: join13("workflows", compass.id, WORKFLOW_SNAPSHOT_FILE),
|
|
3556
|
+
source: join13("iterations", compass.id, "delivery-compass.md"),
|
|
2912
3557
|
data: snapshot
|
|
2913
3558
|
};
|
|
2914
3559
|
}
|
|
@@ -2943,7 +3588,7 @@ function buildStandaloneSnapshot(row, rootUpdatedAt, migrationNotes) {
|
|
|
2943
3588
|
id,
|
|
2944
3589
|
type: "plan",
|
|
2945
3590
|
status,
|
|
2946
|
-
file:
|
|
3591
|
+
file: join13("workflows", id, WORKFLOW_SNAPSHOT_FILE),
|
|
2947
3592
|
source: "status.json plans[] row",
|
|
2948
3593
|
data: snapshot
|
|
2949
3594
|
};
|
|
@@ -2973,10 +3618,10 @@ function applyRootMetadataLift(snapshot, metadata, migrationNotes, activeIterati
|
|
|
2973
3618
|
if (typeof metadata.control_worktree_path === "string" && metadata.control_worktree_path !== "") {
|
|
2974
3619
|
data.control_worktree_path = metadata.control_worktree_path;
|
|
2975
3620
|
}
|
|
2976
|
-
if (
|
|
3621
|
+
if (isPlainObject7(metadata.integration_merge_lease)) {
|
|
2977
3622
|
data.integration_merge_lease = metadata.integration_merge_lease;
|
|
2978
3623
|
}
|
|
2979
|
-
const legacyMetadata =
|
|
3624
|
+
const legacyMetadata = isPlainObject7(data.legacy_metadata) ? { ...data.legacy_metadata } : {};
|
|
2980
3625
|
for (const [key, value] of Object.entries(metadata)) {
|
|
2981
3626
|
if (key === "harness_root")
|
|
2982
3627
|
continue;
|
|
@@ -3032,7 +3677,7 @@ function buildRegister(residualFindings, byPlan, projectId, migratedAt) {
|
|
|
3032
3677
|
const raw = residualFindings[planId];
|
|
3033
3678
|
if (!Array.isArray(raw))
|
|
3034
3679
|
continue;
|
|
3035
|
-
const open = raw.filter((entry) =>
|
|
3680
|
+
const open = raw.filter((entry) => isPlainObject7(entry) && isOpenResidual(entry)).sort((a, b) => {
|
|
3036
3681
|
const aId = typeof a.id === "string" ? a.id : "";
|
|
3037
3682
|
const bId = typeof b.id === "string" ? b.id : "";
|
|
3038
3683
|
return compareIds(aId, bId);
|
|
@@ -3051,7 +3696,7 @@ function buildRegister(residualFindings, byPlan, projectId, migratedAt) {
|
|
|
3051
3696
|
return null;
|
|
3052
3697
|
const doc = { entries };
|
|
3053
3698
|
return {
|
|
3054
|
-
file:
|
|
3699
|
+
file: join13("projects", projectId, PROJECT_REGISTER_FILE),
|
|
3055
3700
|
source: "status.json residual_findings",
|
|
3056
3701
|
data: doc
|
|
3057
3702
|
};
|
|
@@ -3082,7 +3727,7 @@ function collectNotesFiles(snapshots) {
|
|
|
3082
3727
|
if (lines.length === 0)
|
|
3083
3728
|
continue;
|
|
3084
3729
|
out.push({
|
|
3085
|
-
file:
|
|
3730
|
+
file: join13(dirname8(snapshot.file), NOTES_LEDGER_FILE),
|
|
3086
3731
|
source,
|
|
3087
3732
|
lines
|
|
3088
3733
|
});
|
|
@@ -3090,11 +3735,11 @@ function collectNotesFiles(snapshots) {
|
|
|
3090
3735
|
return out;
|
|
3091
3736
|
}
|
|
3092
3737
|
function migrateHarnessTree(root, opts = {}) {
|
|
3093
|
-
const harnessDir =
|
|
3738
|
+
const harnessDir = resolve11(root);
|
|
3094
3739
|
const workflowDir = resolveWorkflowDir(harnessDir, { harnessDir });
|
|
3095
3740
|
const projectDir = resolveProjectDir(harnessDir, { harnessDir });
|
|
3096
3741
|
const projectId = opts.projectId ?? _DEFAULT_PROJECT;
|
|
3097
|
-
const statusPath =
|
|
3742
|
+
const statusPath = join13(harnessDir, MIGRATE_STATUS_FILE);
|
|
3098
3743
|
const legacy = readJson(statusPath);
|
|
3099
3744
|
if (legacy.version === 2) {
|
|
3100
3745
|
const updatedAt = typeof legacy.updated_at === "string" && legacy.updated_at !== "" ? legacy.updated_at : "1970-01-01";
|
|
@@ -3121,12 +3766,12 @@ function migrateHarnessTree(root, opts = {}) {
|
|
|
3121
3766
|
if (legacy.version === undefined) {
|
|
3122
3767
|
throw new Error(`refusing to migrate: no v1 status.json found at ${statusPath} (nothing to migrate)`);
|
|
3123
3768
|
}
|
|
3124
|
-
const rows = Array.isArray(legacy.plans) ? legacy.plans.filter(
|
|
3769
|
+
const rows = Array.isArray(legacy.plans) ? legacy.plans.filter(isPlainObject7) : [];
|
|
3125
3770
|
if (Array.isArray(legacy.plans)) {
|
|
3126
3771
|
const unLiftable = [];
|
|
3127
3772
|
const idCounts = new Map;
|
|
3128
3773
|
for (const row of legacy.plans) {
|
|
3129
|
-
if (!
|
|
3774
|
+
if (!isPlainObject7(row) || rowIdOf(row) === null) {
|
|
3130
3775
|
unLiftable.push(row);
|
|
3131
3776
|
continue;
|
|
3132
3777
|
}
|
|
@@ -3142,7 +3787,7 @@ function migrateHarnessTree(root, opts = {}) {
|
|
|
3142
3787
|
throw new Error(`refusing to migrate: ${duplicates.length} duplicate plan id(s) (${duplicates.join(", ")}) — every v1 row must land in exactly one snapshot`);
|
|
3143
3788
|
}
|
|
3144
3789
|
}
|
|
3145
|
-
const metadata =
|
|
3790
|
+
const metadata = isPlainObject7(legacy.metadata) ? legacy.metadata : {};
|
|
3146
3791
|
const rootUpdatedAt = dateString(legacy.updated_at) ?? dateString(metadata.updated_at) ?? todayString3();
|
|
3147
3792
|
const migratedAt = dateString(metadata.updated_at) ?? rootUpdatedAt;
|
|
3148
3793
|
const migrationNotes = [];
|
|
@@ -3179,9 +3824,9 @@ function migrateHarnessTree(root, opts = {}) {
|
|
|
3179
3824
|
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
3825
|
}
|
|
3181
3826
|
const notesFiles = collectNotesFiles(snapshots);
|
|
3182
|
-
const residualFindings =
|
|
3827
|
+
const residualFindings = isPlainObject7(legacy.residual_findings) ? legacy.residual_findings : {};
|
|
3183
3828
|
const register = buildRegister(residualFindings, byPlan, projectId, migratedAt);
|
|
3184
|
-
const programRoadmap =
|
|
3829
|
+
const programRoadmap = isPlainObject7(metadata.program_roadmap) ? metadata.program_roadmap : null;
|
|
3185
3830
|
let roadmap = null;
|
|
3186
3831
|
if (programRoadmap) {
|
|
3187
3832
|
const rawTitle = typeof programRoadmap.title === "string" && programRoadmap.title !== "" ? programRoadmap.title : "Program roadmap";
|
|
@@ -3190,7 +3835,7 @@ function migrateHarnessTree(root, opts = {}) {
|
|
|
3190
3835
|
migrationNotes.push(`roadmap title sanitized for frontmatter (line breaks replaced with spaces): ${JSON.stringify(rawTitle)}`);
|
|
3191
3836
|
}
|
|
3192
3837
|
roadmap = {
|
|
3193
|
-
file:
|
|
3838
|
+
file: join13("projects", projectId, PROJECT_ROADMAP_FILE),
|
|
3194
3839
|
source: "status.json metadata.program_roadmap",
|
|
3195
3840
|
content: buildRoadmap({ ...programRoadmap, title: sanitizedTitle }, projectId, migratedAt)
|
|
3196
3841
|
};
|
|
@@ -3239,19 +3884,19 @@ async function applyMigratePlan(plan) {
|
|
|
3239
3884
|
if (plan.dryRun) {
|
|
3240
3885
|
return { applied: false, message: `dry-run: ${plan.steps.length} steps planned (source → destination), zero writes` };
|
|
3241
3886
|
}
|
|
3242
|
-
const statusPath =
|
|
3887
|
+
const statusPath = join13(plan.root, MIGRATE_STATUS_FILE);
|
|
3243
3888
|
const current = readJson(statusPath);
|
|
3244
3889
|
if (current.version === 2) {
|
|
3245
3890
|
return { applied: false, message: "no-op: status.json already at schema version 2 (migrated) — nothing to do" };
|
|
3246
3891
|
}
|
|
3247
|
-
const harnessRoot =
|
|
3248
|
-
const workflowRoot =
|
|
3249
|
-
const projectRoot =
|
|
3892
|
+
const harnessRoot = resolve11(plan.root);
|
|
3893
|
+
const workflowRoot = resolve11(plan.workflowDir);
|
|
3894
|
+
const projectRoot = resolve11(plan.projectDir);
|
|
3250
3895
|
if (!isAbsolute7(plan.workflowDir) || !isAbsolute7(plan.projectDir)) {
|
|
3251
3896
|
throw new Error(`refusing to apply migration: plan workflowDir/projectDir must be absolute (got ${JSON.stringify(plan.workflowDir)} / ${JSON.stringify(plan.projectDir)})`);
|
|
3252
3897
|
}
|
|
3253
|
-
const workflowTargetOf = (canonicalFile) =>
|
|
3254
|
-
const projectTargetOf = (canonicalFile) =>
|
|
3898
|
+
const workflowTargetOf = (canonicalFile) => join13(workflowRoot, relative5("workflows", canonicalFile));
|
|
3899
|
+
const projectTargetOf = (canonicalFile) => join13(projectRoot, relative5("projects", canonicalFile));
|
|
3255
3900
|
const allDestinations = [
|
|
3256
3901
|
plan.archive.file,
|
|
3257
3902
|
...plan.snapshots.map((snapshot) => snapshot.file),
|
|
@@ -3260,20 +3905,20 @@ async function applyMigratePlan(plan) {
|
|
|
3260
3905
|
...plan.roadmap !== null ? [plan.roadmap.file] : []
|
|
3261
3906
|
];
|
|
3262
3907
|
for (const destination of allDestinations) {
|
|
3263
|
-
const resolvedDest =
|
|
3908
|
+
const resolvedDest = resolve11(join13(plan.root, destination));
|
|
3264
3909
|
const inside = (dir) => resolvedDest === dir || resolvedDest.startsWith(`${dir}${sep2}`);
|
|
3265
3910
|
if (!inside(harnessRoot) && !inside(workflowRoot) && !inside(projectRoot)) {
|
|
3266
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)})`);
|
|
3267
3912
|
}
|
|
3268
3913
|
}
|
|
3269
|
-
mkdirSync7(
|
|
3270
|
-
copyFileSync(statusPath,
|
|
3914
|
+
mkdirSync7(join13(plan.root, dirname8(plan.archive.file)), { recursive: true });
|
|
3915
|
+
copyFileSync(statusPath, join13(plan.root, plan.archive.file));
|
|
3271
3916
|
for (const snapshot of plan.snapshots) {
|
|
3272
|
-
await writeWorkflowSnapshot(snapshot.data,
|
|
3917
|
+
await writeWorkflowSnapshot(snapshot.data, dirname8(workflowTargetOf(snapshot.file)));
|
|
3273
3918
|
}
|
|
3274
3919
|
for (const notes of plan.notesFiles) {
|
|
3275
3920
|
const filePath = workflowTargetOf(notes.file);
|
|
3276
|
-
mkdirSync7(
|
|
3921
|
+
mkdirSync7(dirname8(filePath), { recursive: true });
|
|
3277
3922
|
const content = notes.lines.length > 0 ? `${notes.lines.join(`
|
|
3278
3923
|
`)}
|
|
3279
3924
|
` : "";
|
|
@@ -3286,13 +3931,13 @@ async function applyMigratePlan(plan) {
|
|
|
3286
3931
|
}
|
|
3287
3932
|
if (Object.keys(plan.register.data.entries ?? {}).length > 0) {
|
|
3288
3933
|
const filePath = projectTargetOf(plan.register.file);
|
|
3289
|
-
mkdirSync7(
|
|
3934
|
+
mkdirSync7(dirname8(filePath), { recursive: true });
|
|
3290
3935
|
writeJson(filePath, plan.register.data);
|
|
3291
3936
|
}
|
|
3292
3937
|
}
|
|
3293
3938
|
if (plan.roadmap !== null) {
|
|
3294
3939
|
const filePath = projectTargetOf(plan.roadmap.file);
|
|
3295
|
-
mkdirSync7(
|
|
3940
|
+
mkdirSync7(dirname8(filePath), { recursive: true });
|
|
3296
3941
|
writeFileSync5(filePath, plan.roadmap.content, "utf8");
|
|
3297
3942
|
}
|
|
3298
3943
|
const rootGate = validateStatusV2(plan.rootV2.data, { harnessDir: plan.root });
|
|
@@ -3716,8 +4361,8 @@ function completenessLevel(frontmatterText, checklist) {
|
|
|
3716
4361
|
}
|
|
3717
4362
|
// src/audit.ts
|
|
3718
4363
|
import { execFileSync as execFileSync4 } from "node:child_process";
|
|
3719
|
-
import { existsSync as
|
|
3720
|
-
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";
|
|
3721
4366
|
function violation9(severity, code, message, fix) {
|
|
3722
4367
|
return { ok: false, severity, code, message, fix };
|
|
3723
4368
|
}
|
|
@@ -4018,7 +4663,7 @@ function scanSecrets(files) {
|
|
|
4018
4663
|
unreadableFiles++;
|
|
4019
4664
|
continue;
|
|
4020
4665
|
}
|
|
4021
|
-
const base =
|
|
4666
|
+
const base = basename6(file);
|
|
4022
4667
|
for (const entry of NEVER_COMMIT_FILENAMES) {
|
|
4023
4668
|
if (entry.re.test(base))
|
|
4024
4669
|
findings.push({ file, line: 1, type: entry.type });
|
|
@@ -4075,7 +4720,7 @@ function rootLockfiles(root) {
|
|
|
4075
4720
|
return [];
|
|
4076
4721
|
}
|
|
4077
4722
|
const names = new Set(LOCKFILE_NAMES);
|
|
4078
|
-
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));
|
|
4079
4724
|
if (present.length === 0)
|
|
4080
4725
|
return [];
|
|
4081
4726
|
try {
|
|
@@ -4084,7 +4729,7 @@ function rootLockfiles(root) {
|
|
|
4084
4729
|
encoding: "utf8",
|
|
4085
4730
|
stdio: ["ignore", "pipe", "ignore"]
|
|
4086
4731
|
}).split("\x00").filter((f) => f !== ""));
|
|
4087
|
-
return present.filter((p) => tracked.has(
|
|
4732
|
+
return present.filter((p) => tracked.has(basename6(p)));
|
|
4088
4733
|
} catch {
|
|
4089
4734
|
return present;
|
|
4090
4735
|
}
|
|
@@ -4100,7 +4745,7 @@ function supplyChainChecks(repoRoot) {
|
|
|
4100
4745
|
findings.push({ kind: "lockfile-duplicate", file: lockfiles.map((f) => f.replace(`${repoRoot}/`, "")).join(", ") });
|
|
4101
4746
|
violations.push(violation9("medium", "audit.supply.lockfile-duplicate", `multiple lockfiles at ${repoRoot}: ${lockfiles.join(", ")}`, "keep exactly one lockfile per package manager"));
|
|
4102
4747
|
}
|
|
4103
|
-
const workflowsDir =
|
|
4748
|
+
const workflowsDir = join14(repoRoot, ".github", "workflows");
|
|
4104
4749
|
let wfEntries = [];
|
|
4105
4750
|
try {
|
|
4106
4751
|
wfEntries = readdirSync8(workflowsDir, { withFileTypes: true });
|
|
@@ -4110,7 +4755,7 @@ function supplyChainChecks(repoRoot) {
|
|
|
4110
4755
|
for (const entry of wfEntries) {
|
|
4111
4756
|
if (!entry.isFile() || !/\.(?:ya?ml)$/.test(entry.name))
|
|
4112
4757
|
continue;
|
|
4113
|
-
const wfPath =
|
|
4758
|
+
const wfPath = join14(workflowsDir, entry.name);
|
|
4114
4759
|
const relPath = `.github/workflows/${entry.name}`;
|
|
4115
4760
|
let text;
|
|
4116
4761
|
try {
|
|
@@ -4267,8 +4912,8 @@ function scaffoldAuditPlan(outDir, findings, options = {}) {
|
|
|
4267
4912
|
const date = options.date ?? new Date().toISOString().slice(0, 10);
|
|
4268
4913
|
const plannedAt = options.plannedAt ?? { commit: options.repoShortSha ?? "unknown", date };
|
|
4269
4914
|
mkdirSync8(outDir, { recursive: true });
|
|
4270
|
-
const existingReadme =
|
|
4271
|
-
const carried =
|
|
4915
|
+
const existingReadme = join14(outDir, "README.md");
|
|
4916
|
+
const carried = existsSync9(existingReadme) ? extractSecurityDispositionSections(readFileSync9(existingReadme, "utf8")) : { needsVerification: [], hardeningChecked: [] };
|
|
4272
4917
|
const existing = readdirSync8(outDir).filter((f) => /^\d{3}-.*\.md$/.test(f));
|
|
4273
4918
|
let next = existing.reduce((max, f) => Math.max(max, Number(f.slice(0, 3))), 0) + 1;
|
|
4274
4919
|
const redactedFindings = findings.map(redactFinding);
|
|
@@ -4285,13 +4930,13 @@ function scaffoldAuditPlan(outDir, findings, options = {}) {
|
|
|
4285
4930
|
}
|
|
4286
4931
|
usedSlugs.add(slug);
|
|
4287
4932
|
const file = `${num}-${slug}.md`;
|
|
4288
|
-
writeFileSync6(
|
|
4933
|
+
writeFileSync6(join14(outDir, file), renderPlanFile(finding, plannedAt));
|
|
4289
4934
|
written.push(file);
|
|
4290
4935
|
next++;
|
|
4291
4936
|
}
|
|
4292
4937
|
const all = [...existing, ...written].sort();
|
|
4293
4938
|
const rows = all.map((file) => {
|
|
4294
|
-
const summary = readPlanFileSummary(
|
|
4939
|
+
const summary = readPlanFileSummary(join14(outDir, file));
|
|
4295
4940
|
const fields = summary.fields;
|
|
4296
4941
|
return {
|
|
4297
4942
|
num: file.slice(0, 3),
|
|
@@ -4325,7 +4970,7 @@ function scaffoldAuditPlan(outDir, findings, options = {}) {
|
|
|
4325
4970
|
});
|
|
4326
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;
|
|
4327
4972
|
const hardeningCheckedLines = options.hardeningChecked !== undefined ? options.hardeningChecked.map((hc) => `- ${hc.kind}: ${escapeCell(redactText(hc.text))}`) : carried.hardeningChecked;
|
|
4328
|
-
writeFileSync6(
|
|
4973
|
+
writeFileSync6(join14(outDir, "README.md"), renderIndex({
|
|
4329
4974
|
date,
|
|
4330
4975
|
repoName: options.repoName ?? "repo",
|
|
4331
4976
|
repoShortSha: options.repoShortSha ?? "unknown",
|
|
@@ -4334,7 +4979,7 @@ function scaffoldAuditPlan(outDir, findings, options = {}) {
|
|
|
4334
4979
|
needsVerification: needsVerificationLines,
|
|
4335
4980
|
hardeningChecked: hardeningCheckedLines
|
|
4336
4981
|
}));
|
|
4337
|
-
return { outDir:
|
|
4982
|
+
return { outDir: resolve12(outDir), date, files: written, nextNumber: next };
|
|
4338
4983
|
}
|
|
4339
4984
|
async function promoteAuditPlans(outDir, selected, options) {
|
|
4340
4985
|
if (selected.length === 0) {
|
|
@@ -4343,19 +4988,19 @@ async function promoteAuditPlans(outDir, selected, options) {
|
|
|
4343
4988
|
if (typeof options.harnessDir !== "string" || options.harnessDir.trim() === "") {
|
|
4344
4989
|
throw new Error("promoteAuditPlans: options.harnessDir is required (must contain status.json + workflows/)");
|
|
4345
4990
|
}
|
|
4346
|
-
const workflowId = options.workflowId ??
|
|
4991
|
+
const workflowId = options.workflowId ?? basename6(resolve12(outDir));
|
|
4347
4992
|
assertSafePathComponent(workflowId, "workflow id");
|
|
4348
|
-
const harnessDir =
|
|
4349
|
-
const statusPath =
|
|
4350
|
-
const workflowDir =
|
|
4351
|
-
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);
|
|
4352
4997
|
const planFiles = resolveSelectedPlanFiles(outDir, selected);
|
|
4353
4998
|
const indexRows = readExecutionOrderIndex(outDir);
|
|
4354
4999
|
const plans = planFiles.map((planFile) => {
|
|
4355
5000
|
const stem = planFile.replace(/\.md$/, "");
|
|
4356
5001
|
const num = stem.slice(0, 3);
|
|
4357
5002
|
const indexRow = indexRows.get(num);
|
|
4358
|
-
const title = indexRow?.title ?? readPlanFileSummary(
|
|
5003
|
+
const title = indexRow?.title ?? readPlanFileSummary(join14(outDir, planFile)).title;
|
|
4359
5004
|
return {
|
|
4360
5005
|
id: stem,
|
|
4361
5006
|
title,
|
|
@@ -4384,7 +5029,7 @@ async function promoteAuditPlans(outDir, selected, options) {
|
|
|
4384
5029
|
throw new Error(`refusing to register invalid workflow entry: ${entryGate.violations.map((v) => v.message).join("; ")}`);
|
|
4385
5030
|
}
|
|
4386
5031
|
await withStatusWriteLock(statusPath, async () => {
|
|
4387
|
-
if (
|
|
5032
|
+
if (existsSync9(snapshotPath)) {
|
|
4388
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`);
|
|
4389
5034
|
}
|
|
4390
5035
|
mkdirSync8(workflowDir, { recursive: true });
|
|
@@ -4420,7 +5065,7 @@ function resolveSelectedPlanFiles(outDir, selected) {
|
|
|
4420
5065
|
for (const id of selected) {
|
|
4421
5066
|
const file = byNum.get(id) ?? byStem.get(id) ?? byStem.get(id.replace(/\.md$/, ""));
|
|
4422
5067
|
if (file === undefined) {
|
|
4423
|
-
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)}`);
|
|
4424
5069
|
}
|
|
4425
5070
|
if (!seen.has(file)) {
|
|
4426
5071
|
seen.add(file);
|
|
@@ -4430,7 +5075,7 @@ function resolveSelectedPlanFiles(outDir, selected) {
|
|
|
4430
5075
|
return resolved;
|
|
4431
5076
|
}
|
|
4432
5077
|
function readExecutionOrderIndex(outDir) {
|
|
4433
|
-
const readmePath =
|
|
5078
|
+
const readmePath = join14(outDir, "README.md");
|
|
4434
5079
|
let text;
|
|
4435
5080
|
try {
|
|
4436
5081
|
text = readFileSync9(readmePath, "utf8");
|
|
@@ -4459,7 +5104,7 @@ function readExecutionOrderIndex(outDir) {
|
|
|
4459
5104
|
return rows;
|
|
4460
5105
|
}
|
|
4461
5106
|
function planFileRel(outDir, planFile) {
|
|
4462
|
-
const resolved =
|
|
5107
|
+
const resolved = resolve12(outDir);
|
|
4463
5108
|
const parts = resolved.split(sep3);
|
|
4464
5109
|
const plansIdx = parts.lastIndexOf("plans");
|
|
4465
5110
|
if (plansIdx >= 0) {
|
|
@@ -4468,8 +5113,8 @@ function planFileRel(outDir, planFile) {
|
|
|
4468
5113
|
return planFile;
|
|
4469
5114
|
}
|
|
4470
5115
|
// src/compound.ts
|
|
4471
|
-
import { existsSync as
|
|
4472
|
-
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";
|
|
4473
5118
|
function violation10(severity, code, message, fix) {
|
|
4474
5119
|
return { ok: false, severity, code, message, fix };
|
|
4475
5120
|
}
|
|
@@ -4764,7 +5409,7 @@ function referenceExists(repoRoot, docText) {
|
|
|
4764
5409
|
break;
|
|
4765
5410
|
if (entry.isDirectory()) {
|
|
4766
5411
|
if (!WALK_SKIP_DIRS.has(entry.name))
|
|
4767
|
-
stack.push(
|
|
5412
|
+
stack.push(join15(dir, entry.name));
|
|
4768
5413
|
} else if (!entry.isSymbolicLink()) {
|
|
4769
5414
|
const base = entry.name.replace(/\.(?:ts|tsx|js|jsx|mjs|cjs)$/, "");
|
|
4770
5415
|
if (moduleNames.has(base))
|
|
@@ -4776,7 +5421,7 @@ function referenceExists(repoRoot, docText) {
|
|
|
4776
5421
|
for (const { ref, isSymbol, module } of refs) {
|
|
4777
5422
|
if (!isSymbol || module === undefined) {
|
|
4778
5423
|
const candidate = ref.replace(LINE_SUFFIX_RE, "").replace(ANCHOR_RE, "");
|
|
4779
|
-
if (
|
|
5424
|
+
if (existsSync10(resolve13(repoRoot, candidate))) {
|
|
4780
5425
|
checked++;
|
|
4781
5426
|
} else {
|
|
4782
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"));
|
|
@@ -4803,11 +5448,11 @@ function collectKnowledgeDocs(dir) {
|
|
|
4803
5448
|
for (const entry of entries) {
|
|
4804
5449
|
if (entry.isSymbolicLink())
|
|
4805
5450
|
continue;
|
|
4806
|
-
const full =
|
|
5451
|
+
const full = join15(current, entry.name);
|
|
4807
5452
|
if (entry.isDirectory()) {
|
|
4808
5453
|
stack.push(full);
|
|
4809
5454
|
} else if (entry.name.endsWith(".md") && entry.name !== "README.md" && entry.name !== "index.md") {
|
|
4810
|
-
docs.push(
|
|
5455
|
+
docs.push(relative6(dir, full).split(sep4).join("/"));
|
|
4811
5456
|
}
|
|
4812
5457
|
}
|
|
4813
5458
|
}
|
|
@@ -4823,8 +5468,8 @@ function normalizeIndexRef(cell) {
|
|
|
4823
5468
|
}
|
|
4824
5469
|
function assertIndexRows(knowledgeDir) {
|
|
4825
5470
|
const violations = [];
|
|
4826
|
-
const readmePath =
|
|
4827
|
-
if (!
|
|
5471
|
+
const readmePath = join15(knowledgeDir, "README.md");
|
|
5472
|
+
if (!existsSync10(readmePath)) {
|
|
4828
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"));
|
|
4829
5474
|
return { ok: false, violations };
|
|
4830
5475
|
}
|
|
@@ -4849,19 +5494,19 @@ function assertIndexRows(knowledgeDir) {
|
|
|
4849
5494
|
}
|
|
4850
5495
|
function compoundRefreshScope(harnessDir, projectRoot) {
|
|
4851
5496
|
return [
|
|
4852
|
-
|
|
4853
|
-
|
|
4854
|
-
|
|
4855
|
-
|
|
5497
|
+
join15(harnessDir, "knowledge"),
|
|
5498
|
+
join15(harnessDir, "knowledge", "README.md"),
|
|
5499
|
+
join15(projectRoot, "CONCEPTS.md"),
|
|
5500
|
+
join15(harnessDir, "status.json")
|
|
4856
5501
|
];
|
|
4857
5502
|
}
|
|
4858
5503
|
function isFileLikeRoot(root) {
|
|
4859
|
-
return /^[^.]*\.[A-Za-z0-9]{1,10}$/.test(
|
|
5504
|
+
return /^[^.]*\.[A-Za-z0-9]{1,10}$/.test(basename7(root));
|
|
4860
5505
|
}
|
|
4861
5506
|
function scopeGuard(path, allowedRoots) {
|
|
4862
|
-
const resolved =
|
|
5507
|
+
const resolved = resolve13(path);
|
|
4863
5508
|
for (const root of allowedRoots) {
|
|
4864
|
-
const r =
|
|
5509
|
+
const r = resolve13(root);
|
|
4865
5510
|
if (isFileLikeRoot(r)) {
|
|
4866
5511
|
if (resolved === r)
|
|
4867
5512
|
return { ok: true, violations: [] };
|
|
@@ -4920,7 +5565,7 @@ function findTemporaryMarkers(fileText) {
|
|
|
4920
5565
|
}
|
|
4921
5566
|
markers.push({ line: i + 1, text, removalPath });
|
|
4922
5567
|
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
|
|
5568
|
+
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
5569
|
}
|
|
4925
5570
|
}
|
|
4926
5571
|
return { ok: violations.length === 0, violations, markers };
|
|
@@ -5109,8 +5754,8 @@ function lintStrategySections(docText) {
|
|
|
5109
5754
|
return { ok: violations.length === 0, violations };
|
|
5110
5755
|
}
|
|
5111
5756
|
// src/roles.ts
|
|
5112
|
-
import { existsSync as
|
|
5113
|
-
import { join as
|
|
5757
|
+
import { existsSync as existsSync11 } from "node:fs";
|
|
5758
|
+
import { join as join16 } from "node:path";
|
|
5114
5759
|
function violation12(severity, code, message, fix) {
|
|
5115
5760
|
return { ok: false, severity, code, message, fix };
|
|
5116
5761
|
}
|
|
@@ -5166,8 +5811,8 @@ function validateRoleMapping(rolesDir, options = {}) {
|
|
|
5166
5811
|
const violations = [];
|
|
5167
5812
|
const referenceById = new Map(mapping.map((m) => [m.agentId, m.reference]));
|
|
5168
5813
|
for (const { agentId, reference } of mapping) {
|
|
5169
|
-
if (!
|
|
5170
|
-
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`));
|
|
5171
5816
|
}
|
|
5172
5817
|
}
|
|
5173
5818
|
for (const { family, memberIds } of families) {
|
|
@@ -5219,6 +5864,14 @@ function validateRoleMapping(rolesDir, options = {}) {
|
|
|
5219
5864
|
return { ok: violations.length === 0, violations };
|
|
5220
5865
|
}
|
|
5221
5866
|
var LOAD_ORDER_HEADING_RE = /^#{1,6}\s+[^\r\n]*\b(?:load[\s-]*order|first\s+action)\b[^\r\n]*$/i;
|
|
5867
|
+
var HUB_BOOTSTRAP_ASSERTIONS = [
|
|
5868
|
+
{ marker: "identity-first", why: "identity boundary before any skill list" },
|
|
5869
|
+
{ marker: "skill presets", why: "Assignment Skill presets decision field" },
|
|
5870
|
+
{ marker: "none", why: "explicit none => identity only, no optional topic preset" },
|
|
5871
|
+
{ marker: "standard", why: "omitted on a substantive round => standard preset" },
|
|
5872
|
+
{ marker: "role-owned", why: "role-owned methods / evidence obligations load regardless of preset" },
|
|
5873
|
+
{ marker: "unknown preset", why: "unknown preset / missing identity => Needs Context / Blocked, never infer PM" }
|
|
5874
|
+
];
|
|
5222
5875
|
function extractLoadOrderSection(text) {
|
|
5223
5876
|
const lines = text.split(/\r?\n/);
|
|
5224
5877
|
let start = -1;
|
|
@@ -5252,11 +5905,22 @@ function lintLoadOrder(skillTexts) {
|
|
|
5252
5905
|
continue;
|
|
5253
5906
|
const section = extractLoadOrderSection(text);
|
|
5254
5907
|
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 §
|
|
5908
|
+
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`));
|
|
5909
|
+
continue;
|
|
5910
|
+
}
|
|
5911
|
+
if (name === "mstar-roles") {
|
|
5912
|
+
const lower = section.toLowerCase();
|
|
5913
|
+
const missing = HUB_BOOTSTRAP_ASSERTIONS.filter((a) => !lower.includes(a.marker.toLowerCase()));
|
|
5914
|
+
if (missing.length > 0) {
|
|
5915
|
+
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"));
|
|
5916
|
+
}
|
|
5917
|
+
if (!section.includes("mstar-harness-core")) {
|
|
5918
|
+
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"));
|
|
5919
|
+
}
|
|
5256
5920
|
continue;
|
|
5257
5921
|
}
|
|
5258
5922
|
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 §
|
|
5923
|
+
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
5924
|
}
|
|
5261
5925
|
}
|
|
5262
5926
|
return { ok: violations.length === 0, violations };
|
|
@@ -5338,6 +6002,16 @@ function collectHeadings(bodyText) {
|
|
|
5338
6002
|
}
|
|
5339
6003
|
return headings;
|
|
5340
6004
|
}
|
|
6005
|
+
function classifySkillLint(skillId) {
|
|
6006
|
+
const id = skillId ?? "";
|
|
6007
|
+
if (id === "mstar-harness-core")
|
|
6008
|
+
return { kind: "core", mode: null };
|
|
6009
|
+
if (id === "mstar-skill-authoring")
|
|
6010
|
+
return { kind: "authoring", mode: "authoring" };
|
|
6011
|
+
if (id.startsWith("mstar-"))
|
|
6012
|
+
return { kind: "runtime", mode: "runtime" };
|
|
6013
|
+
return { kind: "authoring", mode: "authoring" };
|
|
6014
|
+
}
|
|
5341
6015
|
var RUNTIME_HEADING_ALIASES = {
|
|
5342
6016
|
workflow: ["process", "playbook"],
|
|
5343
6017
|
"decision-rules": [
|
|
@@ -5373,7 +6047,7 @@ function resolveAssetPath(skillName, relPath, host) {
|
|
|
5373
6047
|
}
|
|
5374
6048
|
// src/prreview.ts
|
|
5375
6049
|
import { readdirSync as readdirSync10 } from "node:fs";
|
|
5376
|
-
import { isAbsolute as isAbsolute9, join as
|
|
6050
|
+
import { isAbsolute as isAbsolute9, join as join17 } from "node:path";
|
|
5377
6051
|
var MERGE_CLASSES = ["must-fix", "should-fix", "nit"];
|
|
5378
6052
|
var PR_VERDICTS = ["ship it", "needs fixes", "blocked"];
|
|
5379
6053
|
var REVIEW_EMOJI = {
|
|
@@ -5413,7 +6087,7 @@ function computePrTally(input) {
|
|
|
5413
6087
|
var REVIEW_SCHEMA_ID = "mstar.review/v1";
|
|
5414
6088
|
var INSPECTOR_VERDICTS = ["comment", "request_changes", "approve"];
|
|
5415
6089
|
var INSPECTOR_SEVERITIES = ["critical", "warning", "suggestion", "info"];
|
|
5416
|
-
function
|
|
6090
|
+
function isPlainObject8(value) {
|
|
5417
6091
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
5418
6092
|
}
|
|
5419
6093
|
var TALLY_COUNT_KEYS = ["mustFix", "shouldFix", "nit", "unverified"];
|
|
@@ -5424,7 +6098,7 @@ function checkProvidedTallyShape(tally, violations) {
|
|
|
5424
6098
|
if (typeof tally.scorePct !== "number" || !Number.isInteger(tally.scorePct) || tally.scorePct < 0 || tally.scorePct > 100) {
|
|
5425
6099
|
violations.push(violation14("high", "review.tally-malformed", `tally.scorePct must be an integer in [0, 100] - got ${String(tally.scorePct)}`));
|
|
5426
6100
|
}
|
|
5427
|
-
if (!
|
|
6101
|
+
if (!isPlainObject8(tally.tally)) {
|
|
5428
6102
|
violations.push(violation14("high", "review.tally-malformed", "tally.tally must be an object carrying the four class counts"));
|
|
5429
6103
|
} else {
|
|
5430
6104
|
for (const key of TALLY_COUNT_KEYS) {
|
|
@@ -5440,7 +6114,7 @@ function checkProvidedTallyShape(tally, violations) {
|
|
|
5440
6114
|
}
|
|
5441
6115
|
function validateMstarReviewV1(doc) {
|
|
5442
6116
|
const violations = [];
|
|
5443
|
-
if (!
|
|
6117
|
+
if (!isPlainObject8(doc)) {
|
|
5444
6118
|
return {
|
|
5445
6119
|
ok: false,
|
|
5446
6120
|
violations: [violation14("high", "review.not-object", "review document must be a JSON object")]
|
|
@@ -5467,7 +6141,7 @@ function validateMstarReviewV1(doc) {
|
|
|
5467
6141
|
violations.push(violation14("high", "review.findings-not-array", "findings must be an array"));
|
|
5468
6142
|
} else {
|
|
5469
6143
|
doc.findings.forEach((finding, index) => {
|
|
5470
|
-
if (!
|
|
6144
|
+
if (!isPlainObject8(finding)) {
|
|
5471
6145
|
violations.push(violation14("high", "review.invalid-finding", `findings[${index}] must be an object`));
|
|
5472
6146
|
return;
|
|
5473
6147
|
}
|
|
@@ -5507,7 +6181,7 @@ function validateMstarReviewV1(doc) {
|
|
|
5507
6181
|
});
|
|
5508
6182
|
}
|
|
5509
6183
|
if (doc.tally !== undefined) {
|
|
5510
|
-
if (!
|
|
6184
|
+
if (!isPlainObject8(doc.tally)) {
|
|
5511
6185
|
violations.push(violation14("high", "review.invalid-tally", "tally must be a PrTallyResult object"));
|
|
5512
6186
|
} else {
|
|
5513
6187
|
checkProvidedTallyShape(doc.tally, violations);
|
|
@@ -5517,7 +6191,7 @@ function validateMstarReviewV1(doc) {
|
|
|
5517
6191
|
}
|
|
5518
6192
|
}
|
|
5519
6193
|
if (doc.target !== undefined) {
|
|
5520
|
-
if (!
|
|
6194
|
+
if (!isPlainObject8(doc.target)) {
|
|
5521
6195
|
violations.push(violation14("high", "review.invalid-target", "target must be an object"));
|
|
5522
6196
|
} else {
|
|
5523
6197
|
if (doc.target.owner !== undefined && typeof doc.target.owner !== "string") {
|
|
@@ -5629,7 +6303,7 @@ function prReviewReportPath(opts) {
|
|
|
5629
6303
|
}
|
|
5630
6304
|
const revision = maxRevision + 1;
|
|
5631
6305
|
const name = revision === 1 ? `${finalStem}.md` : `${finalStem}-r${revision}.md`;
|
|
5632
|
-
return
|
|
6306
|
+
return join17(opts.reportsDir, name);
|
|
5633
6307
|
}
|
|
5634
6308
|
var PR_TIERS = ["quick", "default", "deep"];
|
|
5635
6309
|
function violation14(severity, code, message, fix) {
|
|
@@ -5946,7 +6620,7 @@ function prReviewSeatPrompt(opts) {
|
|
|
5946
6620
|
lines.push("");
|
|
5947
6621
|
lines.push("## Read first");
|
|
5948
6622
|
lines.push("");
|
|
5949
|
-
const prReviewRef =
|
|
6623
|
+
const prReviewRef = join17(skillRoot, "references", "pr-review.md");
|
|
5950
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";
|
|
5951
6625
|
lines.push(`1. \`${prReviewRef}\` — read at least these sections: ${sections}.`);
|
|
5952
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).`);
|
|
@@ -5954,9 +6628,9 @@ function prReviewSeatPrompt(opts) {
|
|
|
5954
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.`);
|
|
5955
6629
|
}
|
|
5956
6630
|
if (opts.stage === 2) {
|
|
5957
|
-
lines.push(`3. \`${
|
|
6631
|
+
lines.push(`3. \`${join17(skillRoot, "references", "finding-format.md")}\` — the template every finding follows.`);
|
|
5958
6632
|
if (opts.securitySeat === true) {
|
|
5959
|
-
lines.push(`4. \`${
|
|
6633
|
+
lines.push(`4. \`${join17(skillRoot, "references", "security-review.md")}\` — the security lens.`);
|
|
5960
6634
|
}
|
|
5961
6635
|
}
|
|
5962
6636
|
const budget = PR_REVIEW_TIER_BUDGETS[tier];
|
|
@@ -6154,6 +6828,7 @@ export {
|
|
|
6154
6828
|
KNOWLEDGE_REQUIRED_FIELDS,
|
|
6155
6829
|
KNOWLEDGE_RESOLUTION_TYPES,
|
|
6156
6830
|
KNOWLEDGE_SEVERITIES,
|
|
6831
|
+
MAX_STATUS_CONTENT_LENGTH,
|
|
6157
6832
|
MERGE_CLASSES,
|
|
6158
6833
|
MIGRATE_STATUS_FILE,
|
|
6159
6834
|
MSTARC_FILE,
|
|
@@ -6198,7 +6873,10 @@ export {
|
|
|
6198
6873
|
assertTriIdentity,
|
|
6199
6874
|
assignmentHeaderRegion,
|
|
6200
6875
|
canSteal,
|
|
6876
|
+
canonicalizeNearestExisting,
|
|
6877
|
+
checkSddAction,
|
|
6201
6878
|
claimLease,
|
|
6879
|
+
classifySkillLint,
|
|
6202
6880
|
closeProjectRegisterEntry,
|
|
6203
6881
|
completenessLevel,
|
|
6204
6882
|
composeDispatchGate,
|
|
@@ -6209,14 +6887,18 @@ export {
|
|
|
6209
6887
|
detectHost,
|
|
6210
6888
|
emitGitignoreSnippet,
|
|
6211
6889
|
evaluatePhaseGate,
|
|
6890
|
+
eventTargetPaths,
|
|
6212
6891
|
executionModeToN,
|
|
6213
6892
|
findEphemeralCitations,
|
|
6214
6893
|
findMstarc,
|
|
6215
6894
|
findSimplifyMarkers,
|
|
6216
6895
|
findTemporaryMarkers,
|
|
6217
6896
|
findingsCleanupGate,
|
|
6897
|
+
formatStatusWriteBlockReason,
|
|
6218
6898
|
getArtifactStore,
|
|
6899
|
+
harnessDocKindOfTarget,
|
|
6219
6900
|
implementerSessionStickyRules,
|
|
6901
|
+
isDistinctCheckout,
|
|
6220
6902
|
isReadOnlyAssignmentRole,
|
|
6221
6903
|
l1PreDispatchCheck,
|
|
6222
6904
|
l2PreDispatchCheck,
|
|
@@ -6245,6 +6927,7 @@ export {
|
|
|
6245
6927
|
prReviewSeatPrompt,
|
|
6246
6928
|
prReviewSizing,
|
|
6247
6929
|
preflightChangeset,
|
|
6930
|
+
probeCheckoutRoot,
|
|
6248
6931
|
promoteAuditPlans,
|
|
6249
6932
|
pushCadenceProbe,
|
|
6250
6933
|
readHarnessVersion,
|
|
@@ -6267,10 +6950,12 @@ export {
|
|
|
6267
6950
|
resolveRepoEnforcement,
|
|
6268
6951
|
resolveScaffoldDirs,
|
|
6269
6952
|
resolveSddDir,
|
|
6953
|
+
resolveSddExecutionContext,
|
|
6270
6954
|
resolveSkillRoot,
|
|
6271
6955
|
resolveSpecsDir,
|
|
6272
6956
|
resolveWorkflowDir,
|
|
6273
6957
|
reviewPackage,
|
|
6958
|
+
runInSddContext,
|
|
6274
6959
|
sameHolderResume,
|
|
6275
6960
|
scaffoldAuditPlan,
|
|
6276
6961
|
scaffoldHarness,
|
|
@@ -6304,9 +6989,11 @@ export {
|
|
|
6304
6989
|
validateSchemaYaml,
|
|
6305
6990
|
validateStatus,
|
|
6306
6991
|
validateStatusV2,
|
|
6992
|
+
validateStatusWriteDoc,
|
|
6307
6993
|
validateWorkflowEntry,
|
|
6308
6994
|
validateWorkflowSnapshot,
|
|
6309
6995
|
verifyPlanExecutionLease,
|
|
6996
|
+
violationLine,
|
|
6310
6997
|
withStatusWriteLock,
|
|
6311
6998
|
writeJson,
|
|
6312
6999
|
writeWorkflowSnapshot
|