@mstar-harness/cli 3.1.1 → 3.1.3
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/mstar-harness.js +365 -111
- package/package.json +1 -1
package/dist/mstar-harness.js
CHANGED
|
@@ -2414,7 +2414,7 @@ var require_picocolors2 = __commonJS((exports, module) => {
|
|
|
2414
2414
|
});
|
|
2415
2415
|
|
|
2416
2416
|
// src/index.ts
|
|
2417
|
-
import { execFileSync as
|
|
2417
|
+
import { execFileSync as execFileSync8 } from "child_process";
|
|
2418
2418
|
import fs8 from "fs";
|
|
2419
2419
|
import path12 from "path";
|
|
2420
2420
|
|
|
@@ -4069,29 +4069,33 @@ var {
|
|
|
4069
4069
|
|
|
4070
4070
|
// ../engine/dist/engine.js
|
|
4071
4071
|
import { existsSync, mkdirSync, readFileSync, renameSync, unlinkSync, writeFileSync } from "node:fs";
|
|
4072
|
+
import { randomUUID } from "node:crypto";
|
|
4073
|
+
import { basename, dirname, join, resolve } from "node:path";
|
|
4072
4074
|
import { readFileSync as readFileSync2, statSync } from "node:fs";
|
|
4073
4075
|
import { dirname as dirname2, isAbsolute, join as join2, relative, resolve as resolve2 } from "node:path";
|
|
4074
|
-
import { mkdirSync as mkdirSync2, readdirSync, readFileSync as readFileSync3, statSync as statSync2 } from "node:fs";
|
|
4076
|
+
import { existsSync as existsSync2, mkdirSync as mkdirSync2, readdirSync, readFileSync as readFileSync3, realpathSync, statSync as statSync2 } from "node:fs";
|
|
4075
4077
|
import { execFileSync } from "node:child_process";
|
|
4076
4078
|
import { basename as basename2, dirname as dirname3, isAbsolute as isAbsolute2, join as join3, relative as relative2, resolve as resolve3 } from "node:path";
|
|
4077
|
-
import { existsSync as
|
|
4079
|
+
import { existsSync as existsSync3, readFileSync as readFileSync4, readdirSync as readdirSync2, realpathSync as realpathSync2 } from "node:fs";
|
|
4078
4080
|
import { dirname as dirname5, join as join6, resolve as resolve5, sep } from "node:path";
|
|
4081
|
+
import { mkdirSync as mkdirSync3, rmdirSync, statSync as statSync3, unlinkSync as unlinkSync2, writeFileSync as writeFileSync2 } from "node:fs";
|
|
4079
4082
|
import { dirname as dirname4, isAbsolute as isAbsolute3, join as join4, resolve as resolve4 } from "node:path";
|
|
4083
|
+
import { setTimeout as sleep } from "node:timers/promises";
|
|
4080
4084
|
import { AsyncLocalStorage as AsyncLocalStorage2 } from "node:async_hooks";
|
|
4081
4085
|
import { execFileSync as execFileSync2 } from "node:child_process";
|
|
4082
|
-
import { existsSync as
|
|
4086
|
+
import { existsSync as existsSync4 } from "node:fs";
|
|
4083
4087
|
import { isAbsolute as isAbsolute4, resolve as resolve6 } from "node:path";
|
|
4084
4088
|
import { execFileSync as execFileSync3 } from "node:child_process";
|
|
4085
|
-
import { mkdirSync as mkdirSync5, readdirSync as readdirSync3, readFileSync as readFileSync5, realpathSync as
|
|
4089
|
+
import { mkdirSync as mkdirSync5, readdirSync as readdirSync3, readFileSync as readFileSync5, realpathSync as realpathSync3, statSync as statSync4, writeFileSync as writeFileSync3 } from "node:fs";
|
|
4086
4090
|
import { basename as basename3, dirname as dirname6, isAbsolute as isAbsolute5, join as join7, resolve as resolve7 } from "node:path";
|
|
4087
|
-
import { existsSync as
|
|
4088
|
-
import { existsSync as
|
|
4091
|
+
import { existsSync as existsSync5, readdirSync as readdirSync4, readFileSync as readFileSync6 } from "node:fs";
|
|
4092
|
+
import { existsSync as existsSync6, readFileSync as readFileSync7, readdirSync as readdirSync5 } from "node:fs";
|
|
4089
4093
|
import { join as join9 } from "node:path";
|
|
4090
|
-
import { mkdirSync as mkdirSync7, readdirSync as readdirSync7, readFileSync as readFileSync9, writeFileSync as writeFileSync5 } from "node:fs";
|
|
4091
|
-
import { join as join11, resolve as resolve9 } from "node:path";
|
|
4092
|
-
import { existsSync as
|
|
4093
|
-
import { basename as
|
|
4094
|
-
import { existsSync as
|
|
4094
|
+
import { existsSync as existsSync7, mkdirSync as mkdirSync7, readdirSync as readdirSync7, readFileSync as readFileSync9, rmdirSync as rmdirSync2, rmSync, writeFileSync as writeFileSync5 } from "node:fs";
|
|
4095
|
+
import { basename as basename4, join as join11, resolve as resolve9, sep as sep3 } from "node:path";
|
|
4096
|
+
import { existsSync as existsSync8, readdirSync as readdirSync8, readFileSync as readFileSync10 } from "node:fs";
|
|
4097
|
+
import { basename as basename5, isAbsolute as isAbsolute7, join as join12, relative as relative4, resolve as resolve10, sep as sep4 } from "node:path";
|
|
4098
|
+
import { existsSync as existsSync9 } from "node:fs";
|
|
4095
4099
|
import { join as join13 } from "node:path";
|
|
4096
4100
|
var SEVERITY_ORDER = ["critical", "high", "medium", "low", "nit"];
|
|
4097
4101
|
function readJson(filePath) {
|
|
@@ -4106,6 +4110,21 @@ function readJson(filePath) {
|
|
|
4106
4110
|
throw new Error(`Invalid JSON in ${filePath}: ${error.message}`);
|
|
4107
4111
|
}
|
|
4108
4112
|
}
|
|
4113
|
+
function writeJson(filePath, value) {
|
|
4114
|
+
const parent = dirname(filePath);
|
|
4115
|
+
mkdirSync(parent, { recursive: true });
|
|
4116
|
+
const tmp = join(parent, `.${basename(filePath)}.${process.pid}.${randomUUID()}.tmp`);
|
|
4117
|
+
try {
|
|
4118
|
+
writeFileSync(tmp, `${JSON.stringify(value, null, 2)}
|
|
4119
|
+
`, "utf8");
|
|
4120
|
+
renameSync(tmp, filePath);
|
|
4121
|
+
} catch (error) {
|
|
4122
|
+
try {
|
|
4123
|
+
unlinkSync(tmp);
|
|
4124
|
+
} catch {}
|
|
4125
|
+
throw error;
|
|
4126
|
+
}
|
|
4127
|
+
}
|
|
4109
4128
|
var MSTARC_FILE = ".mstarc";
|
|
4110
4129
|
var MSTARC_SECTION = "config";
|
|
4111
4130
|
var MSTARC_HARNESS_DIR_KEY = "harness_dir";
|
|
@@ -4414,7 +4433,54 @@ function validateIntegrationMergeLease(lease) {
|
|
|
4414
4433
|
}
|
|
4415
4434
|
return { ok: violations.length === 0, violations };
|
|
4416
4435
|
}
|
|
4436
|
+
var STATUS_WRITE_LOCKDIR = ".status-write.lockdir";
|
|
4437
|
+
var LOCKDIR_HOLDER_PID = "holder.pid";
|
|
4417
4438
|
var heldLockDirs = new AsyncLocalStorage2;
|
|
4439
|
+
async function withStatusWriteLock(statusPath, fn, opts = {}) {
|
|
4440
|
+
const lockDir = join4(dirname4(resolve4(statusPath)), STATUS_WRITE_LOCKDIR);
|
|
4441
|
+
const held = heldLockDirs.getStore();
|
|
4442
|
+
if (held !== undefined && held.has(lockDir)) {
|
|
4443
|
+
throw new Error(`${lockDir} is already held by this process in this async context \u2014 withStatusWriteLock is not reentrant; a nested acquisition on the same status.json is a bug`);
|
|
4444
|
+
}
|
|
4445
|
+
const timeoutMs = opts.timeoutMs ?? 30000;
|
|
4446
|
+
const pollMs = opts.pollMs ?? 25;
|
|
4447
|
+
const deadline = Date.now() + timeoutMs;
|
|
4448
|
+
let acquired = null;
|
|
4449
|
+
for (;; ) {
|
|
4450
|
+
try {
|
|
4451
|
+
mkdirSync3(lockDir);
|
|
4452
|
+
const st = statSync3(lockDir);
|
|
4453
|
+
acquired = { dev: st.dev, ino: st.ino };
|
|
4454
|
+
break;
|
|
4455
|
+
} catch (error) {
|
|
4456
|
+
if (error.code !== "EEXIST")
|
|
4457
|
+
throw error;
|
|
4458
|
+
if (Date.now() >= deadline) {
|
|
4459
|
+
throw new Error(`${lockDir} already exists \u2014 another writer holds the status write lock; Blocked (same-host exclusive lock; status-and-residuals.md \u00a7 Same-host exclusive write lock). ` + `Recovery: remove ${lockDir} if no writer is alive (holder.pid inside names the acquiring process)`);
|
|
4460
|
+
}
|
|
4461
|
+
await sleep(pollMs);
|
|
4462
|
+
}
|
|
4463
|
+
}
|
|
4464
|
+
try {
|
|
4465
|
+
writeFileSync2(join4(lockDir, LOCKDIR_HOLDER_PID), String(process.pid), "utf8");
|
|
4466
|
+
} catch {}
|
|
4467
|
+
const owns = held ?? new Set;
|
|
4468
|
+
owns.add(lockDir);
|
|
4469
|
+
try {
|
|
4470
|
+
return await heldLockDirs.run(owns, fn);
|
|
4471
|
+
} finally {
|
|
4472
|
+
owns.delete(lockDir);
|
|
4473
|
+
try {
|
|
4474
|
+
const current = statSync3(lockDir);
|
|
4475
|
+
if (acquired !== null && current.dev === acquired.dev && current.ino === acquired.ino) {
|
|
4476
|
+
try {
|
|
4477
|
+
unlinkSync2(join4(lockDir, LOCKDIR_HOLDER_PID));
|
|
4478
|
+
} catch {}
|
|
4479
|
+
rmdirSync(lockDir);
|
|
4480
|
+
}
|
|
4481
|
+
} catch {}
|
|
4482
|
+
}
|
|
4483
|
+
}
|
|
4418
4484
|
var BRANCH_FORMS_HINT = '"Working branch: <existing>" | "Working branch: create <new> from <base>" | "Branch policy: direct on <branch> \u2014 <reason>"';
|
|
4419
4485
|
var REQUIRED_FIELDS = [
|
|
4420
4486
|
{ key: "executeAs", label: "Execute as", code: "execute-as" },
|
|
@@ -4693,6 +4759,12 @@ function isPlainObject3(value) {
|
|
|
4693
4759
|
function violation4(severity, code, message, fix) {
|
|
4694
4760
|
return { ok: false, severity, code, message, fix };
|
|
4695
4761
|
}
|
|
4762
|
+
function todayString() {
|
|
4763
|
+
const now = new Date;
|
|
4764
|
+
const month = String(now.getMonth() + 1).padStart(2, "0");
|
|
4765
|
+
const day = String(now.getDate()).padStart(2, "0");
|
|
4766
|
+
return `${now.getFullYear()}-${month}-${day}`;
|
|
4767
|
+
}
|
|
4696
4768
|
function normalizeSeverity(value) {
|
|
4697
4769
|
if (value === "warning")
|
|
4698
4770
|
return "low";
|
|
@@ -4848,7 +4920,7 @@ function validateStatusV2(docOrPath, opts = {}) {
|
|
|
4848
4920
|
if (harnessDir !== undefined && Array.isArray(doc.workflows)) {
|
|
4849
4921
|
let realHarnessDir = null;
|
|
4850
4922
|
try {
|
|
4851
|
-
realHarnessDir =
|
|
4923
|
+
realHarnessDir = realpathSync2(harnessDir);
|
|
4852
4924
|
} catch {}
|
|
4853
4925
|
for (const entry of doc.workflows) {
|
|
4854
4926
|
if (!isPlainObject3(entry) || typeof entry.dir !== "string")
|
|
@@ -4858,7 +4930,7 @@ function validateStatusV2(docOrPath, opts = {}) {
|
|
|
4858
4930
|
const label = typeof entry.id === "string" ? entry.id : relSnapshot;
|
|
4859
4931
|
let physical;
|
|
4860
4932
|
try {
|
|
4861
|
-
physical =
|
|
4933
|
+
physical = realpathSync2(snapshotPath);
|
|
4862
4934
|
} catch {
|
|
4863
4935
|
violations.push(violation4("high", "status.workflow.snapshot-missing", `workflows[] lists ${JSON.stringify(label)} but its snapshot does not exist at ${JSON.stringify(relSnapshot)} \u2014 the root holds active lifecycles only; unregister the id when its snapshot is removed`));
|
|
4864
4936
|
continue;
|
|
@@ -4881,13 +4953,35 @@ function validateStatusV2(docOrPath, opts = {}) {
|
|
|
4881
4953
|
violations.push(violation4("medium", "status.workflow.mismatched-type", `workflows[] entry ${JSON.stringify(label)} type ${JSON.stringify(entry.type)} does not match its snapshot type ${JSON.stringify(snapshot.type)} \u2014 the root entry mirrors the snapshot; align them`));
|
|
4882
4954
|
}
|
|
4883
4955
|
if (typeof entry.started_at === "string" && typeof snapshot.started_at === "string" && entry.started_at !== snapshot.started_at) {
|
|
4884
|
-
violations.push(violation4("medium", "status.workflow.mismatched-started-at", `workflows[] entry ${JSON.stringify(label)} started_at ${JSON.stringify(entry.started_at)} does not match its snapshot started_at ${JSON.stringify(snapshot.started_at)} \u2014 the root entry mirrors the snapshot
|
|
4956
|
+
violations.push(violation4("medium", "status.workflow.mismatched-started-at", `workflows[] entry ${JSON.stringify(label)} started_at ${JSON.stringify(entry.started_at)} does not match its snapshot started_at ${JSON.stringify(snapshot.started_at)} \u2014 workflow ${JSON.stringify(label)} collided with another writer (e.g. a concurrent/re-run \`audit promote\` with the same workflow id rewrote the snapshot); the root entry mirrors the snapshot \u2014 align them or remove the colliding workflow`));
|
|
4885
4957
|
}
|
|
4886
4958
|
}
|
|
4887
4959
|
}
|
|
4888
4960
|
return { ok: violations.length === 0, violations };
|
|
4889
4961
|
}
|
|
4890
4962
|
var validateStatus = validateStatusV2;
|
|
4963
|
+
function registerWorkflowEntryLocked(statusPath, entry) {
|
|
4964
|
+
const harnessDir = dirname5(statusPath);
|
|
4965
|
+
const current = readJson(statusPath);
|
|
4966
|
+
const fresh = Object.keys(current).length === 0;
|
|
4967
|
+
const doc = fresh ? { version: 2, updated_at: todayString(), workflows: [] } : current;
|
|
4968
|
+
if (!fresh && !Array.isArray(doc.workflows)) {
|
|
4969
|
+
throw new Error("refusing to modify status.json: workflows must be an array \u2014 a v1 root must be migrated first (run `mstar migrate`)");
|
|
4970
|
+
}
|
|
4971
|
+
const existing = doc.workflows.findIndex((wf) => wf.id === entry.id);
|
|
4972
|
+
if (existing >= 0) {
|
|
4973
|
+
doc.workflows[existing] = entry;
|
|
4974
|
+
} else {
|
|
4975
|
+
doc.workflows.push(entry);
|
|
4976
|
+
}
|
|
4977
|
+
doc.updated_at = todayString();
|
|
4978
|
+
const gate = validateStatusV2(doc, { harnessDir });
|
|
4979
|
+
if (!gate.ok) {
|
|
4980
|
+
throw new Error(`refusing to write invalid status.json: ${gate.violations.map((v) => v.message).join("; ")}`);
|
|
4981
|
+
}
|
|
4982
|
+
writeJson(statusPath, doc);
|
|
4983
|
+
return doc;
|
|
4984
|
+
}
|
|
4891
4985
|
var DEFAULT_PROBE_TIMEOUT_MS = 1e4;
|
|
4892
4986
|
function probeTimeoutMs() {
|
|
4893
4987
|
const raw = process.env.MSTAR_GIT_PROBE_TIMEOUT_MS;
|
|
@@ -4941,7 +5035,7 @@ function l1PreDispatchCheck(input, opts = {}) {
|
|
|
4941
5035
|
if (controlWorktreePath !== "" && leaseWorktreePath !== "" && resolve6(controlWorktreePath) === resolve6(leaseWorktreePath)) {
|
|
4942
5036
|
violations.push(violation5("critical", "worktree.l1.lease-equals-control", `execution_lease.worktree_path "${leaseWorktreePath}" equals metadata.control_worktree_path \u2014 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"));
|
|
4943
5037
|
}
|
|
4944
|
-
if (leaseWorktreePath !== "" && !
|
|
5038
|
+
if (leaseWorktreePath !== "" && !existsSync4(leaseWorktreePath)) {
|
|
4945
5039
|
violations.push(violation5("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>`));
|
|
4946
5040
|
} else if (leaseWorktreePath !== "" && leaseWorkingBranch !== "") {
|
|
4947
5041
|
const probe = probeBranch(leaseWorktreePath, opts);
|
|
@@ -4975,7 +5069,7 @@ function l2PreDispatchCheck(input, opts = {}) {
|
|
|
4975
5069
|
return;
|
|
4976
5070
|
}
|
|
4977
5071
|
seenPaths.add(normalized);
|
|
4978
|
-
if (!
|
|
5072
|
+
if (!existsSync4(track.worktreePath)) {
|
|
4979
5073
|
violations.push(violation5("high", "worktree.l2.track-missing", `track worktree directory "${track.worktreePath}" does not exist`, `create it before dispatch: git worktree add ${track.worktreePath} ${track.workingBranch}`));
|
|
4980
5074
|
return;
|
|
4981
5075
|
}
|
|
@@ -5079,8 +5173,8 @@ function isLinkedWorktree(root) {
|
|
|
5079
5173
|
if (gitDir.includes("/.git/worktrees/") || gitDir.includes("/worktrees/"))
|
|
5080
5174
|
return true;
|
|
5081
5175
|
try {
|
|
5082
|
-
const gdParent =
|
|
5083
|
-
const cmAbs =
|
|
5176
|
+
const gdParent = realpathSync3(dirname6(gitDir));
|
|
5177
|
+
const cmAbs = realpathSync3(common2);
|
|
5084
5178
|
return join7(gdParent, basename3(gitDir)) !== cmAbs && gitDir !== cmAbs;
|
|
5085
5179
|
} catch {
|
|
5086
5180
|
return false;
|
|
@@ -5098,10 +5192,10 @@ function sddWorkspace(planId, opts = {}) {
|
|
|
5098
5192
|
if (!isDirectory2(controlRoot)) {
|
|
5099
5193
|
throw new SddScriptError(`mstar sdd workspace: CONTROL_ROOT / MSTAR_CONTROL_ROOT is not a directory: ${controlRoot}`, 1);
|
|
5100
5194
|
}
|
|
5101
|
-
root =
|
|
5195
|
+
root = realpathSync3(controlRoot);
|
|
5102
5196
|
} else {
|
|
5103
5197
|
const topLevel = gitOut(cwd, ["rev-parse", "--show-toplevel"]);
|
|
5104
|
-
root =
|
|
5198
|
+
root = realpathSync3(topLevel ?? cwd);
|
|
5105
5199
|
}
|
|
5106
5200
|
if (!controlRoot && isLinkedWorktree(root)) {
|
|
5107
5201
|
throw new SddScriptError(`mstar sdd workspace: linked worktree at ${root} has no {HARNESS_DIR}/status.json (default gitignore).
|
|
@@ -5135,7 +5229,7 @@ function sddWorkspace(planId, opts = {}) {
|
|
|
5135
5229
|
mkdirSync5(sddDir, { recursive: true });
|
|
5136
5230
|
writeFileSync3(join7(sddDir, ".gitignore"), `*
|
|
5137
5231
|
`);
|
|
5138
|
-
return
|
|
5232
|
+
return realpathSync3(sddDir);
|
|
5139
5233
|
}
|
|
5140
5234
|
function taskBrief(planFile, taskN, outFile, opts = {}) {
|
|
5141
5235
|
if (!planFile || !Number.isInteger(taskN) || taskN < 1) {
|
|
@@ -5583,7 +5677,7 @@ function techDebtRollup(projectDir) {
|
|
|
5583
5677
|
if (!project.isDirectory())
|
|
5584
5678
|
continue;
|
|
5585
5679
|
const registerPath = join9(projectDir, project.name, PROJECT_REGISTER_FILE);
|
|
5586
|
-
if (!
|
|
5680
|
+
if (!existsSync6(registerPath))
|
|
5587
5681
|
continue;
|
|
5588
5682
|
let register;
|
|
5589
5683
|
try {
|
|
@@ -6189,6 +6283,137 @@ function scaffoldAuditPlan(outDir, findings, options = {}) {
|
|
|
6189
6283
|
}));
|
|
6190
6284
|
return { outDir: resolve9(outDir), date, files: written, nextNumber: next };
|
|
6191
6285
|
}
|
|
6286
|
+
async function promoteAuditPlans(outDir, selected, options) {
|
|
6287
|
+
if (selected.length === 0) {
|
|
6288
|
+
throw new Error("promoteAuditPlans: at least one plan id must be selected (--plans 001,002,\u2026)");
|
|
6289
|
+
}
|
|
6290
|
+
if (typeof options.harnessDir !== "string" || options.harnessDir.trim() === "") {
|
|
6291
|
+
throw new Error("promoteAuditPlans: options.harnessDir is required (must contain status.json + workflows/)");
|
|
6292
|
+
}
|
|
6293
|
+
const workflowId = options.workflowId ?? basename4(resolve9(outDir));
|
|
6294
|
+
assertSafePathComponent(workflowId, "workflow id");
|
|
6295
|
+
const harnessDir = resolve9(options.harnessDir);
|
|
6296
|
+
const statusPath = join11(harnessDir, "status.json");
|
|
6297
|
+
const workflowDir = join11(harnessDir, "workflows", workflowId);
|
|
6298
|
+
const snapshotPath = join11(workflowDir, WORKFLOW_SNAPSHOT_FILE);
|
|
6299
|
+
const planFiles = resolveSelectedPlanFiles(outDir, selected);
|
|
6300
|
+
const indexRows = readExecutionOrderIndex(outDir);
|
|
6301
|
+
const plans = planFiles.map((planFile) => {
|
|
6302
|
+
const stem = planFile.replace(/\.md$/, "");
|
|
6303
|
+
const num = stem.slice(0, 3);
|
|
6304
|
+
const indexRow = indexRows.get(num);
|
|
6305
|
+
const title = indexRow?.title ?? readPlanFileSummary(join11(outDir, planFile)).title;
|
|
6306
|
+
return {
|
|
6307
|
+
id: stem,
|
|
6308
|
+
title,
|
|
6309
|
+
file: planFileRel(outDir, planFile),
|
|
6310
|
+
status: "Todo"
|
|
6311
|
+
};
|
|
6312
|
+
});
|
|
6313
|
+
const now = new Date;
|
|
6314
|
+
const snapshot = {
|
|
6315
|
+
schema_version: 1,
|
|
6316
|
+
id: workflowId,
|
|
6317
|
+
type: "plan",
|
|
6318
|
+
status: "running",
|
|
6319
|
+
started_at: now.toISOString(),
|
|
6320
|
+
updated_at: now.toISOString().slice(0, 10),
|
|
6321
|
+
plans
|
|
6322
|
+
};
|
|
6323
|
+
const entry = {
|
|
6324
|
+
id: workflowId,
|
|
6325
|
+
type: "plan",
|
|
6326
|
+
started_at: snapshot.started_at,
|
|
6327
|
+
dir: `workflows/${workflowId}`
|
|
6328
|
+
};
|
|
6329
|
+
const entryGate = validateWorkflowEntry(entry);
|
|
6330
|
+
if (!entryGate.ok) {
|
|
6331
|
+
throw new Error(`refusing to register invalid workflow entry: ${entryGate.violations.map((v) => v.message).join("; ")}`);
|
|
6332
|
+
}
|
|
6333
|
+
await withStatusWriteLock(statusPath, () => {
|
|
6334
|
+
if (existsSync7(snapshotPath)) {
|
|
6335
|
+
throw new Error(`refusing to promote audit plans: workflow ${JSON.stringify(workflowId)} already exists ` + `(snapshot at ${snapshotPath}) \u2014 re-promote would drop its registered plan rows; ` + `remove that workflow before promoting again`);
|
|
6336
|
+
}
|
|
6337
|
+
mkdirSync7(workflowDir, { recursive: true });
|
|
6338
|
+
try {
|
|
6339
|
+
writeJson(snapshotPath, snapshot);
|
|
6340
|
+
registerWorkflowEntryLocked(statusPath, entry);
|
|
6341
|
+
} catch (error) {
|
|
6342
|
+
rmSync(snapshotPath, { force: true });
|
|
6343
|
+
try {
|
|
6344
|
+
if (readdirSync7(workflowDir).length === 0) {
|
|
6345
|
+
rmdirSync2(workflowDir);
|
|
6346
|
+
}
|
|
6347
|
+
} catch {}
|
|
6348
|
+
throw error;
|
|
6349
|
+
}
|
|
6350
|
+
return { workflowId, snapshotPath };
|
|
6351
|
+
});
|
|
6352
|
+
return { workflowId, snapshotPath };
|
|
6353
|
+
}
|
|
6354
|
+
function resolveSelectedPlanFiles(outDir, selected) {
|
|
6355
|
+
const files = readdirSync7(outDir).filter((f) => /^\d{3}-.*\.md$/.test(f)).sort();
|
|
6356
|
+
const byNum = new Map;
|
|
6357
|
+
const byStem = new Map;
|
|
6358
|
+
for (const file of files) {
|
|
6359
|
+
const stem = file.replace(/\.md$/, "");
|
|
6360
|
+
if (!byNum.has(stem.slice(0, 3))) {
|
|
6361
|
+
byNum.set(stem.slice(0, 3), file);
|
|
6362
|
+
}
|
|
6363
|
+
byStem.set(stem, file);
|
|
6364
|
+
}
|
|
6365
|
+
const resolved = [];
|
|
6366
|
+
const seen = new Set;
|
|
6367
|
+
for (const id of selected) {
|
|
6368
|
+
const file = byNum.get(id) ?? byStem.get(id) ?? byStem.get(id.replace(/\.md$/, ""));
|
|
6369
|
+
if (file === undefined) {
|
|
6370
|
+
throw new Error(`promoteAuditPlans: selected plan ${JSON.stringify(id)} does not match any NNN-*.md file in ${resolve9(outDir)}`);
|
|
6371
|
+
}
|
|
6372
|
+
if (!seen.has(file)) {
|
|
6373
|
+
seen.add(file);
|
|
6374
|
+
resolved.push(file);
|
|
6375
|
+
}
|
|
6376
|
+
}
|
|
6377
|
+
return resolved;
|
|
6378
|
+
}
|
|
6379
|
+
function readExecutionOrderIndex(outDir) {
|
|
6380
|
+
const readmePath = join11(outDir, "README.md");
|
|
6381
|
+
let text;
|
|
6382
|
+
try {
|
|
6383
|
+
text = readFileSync9(readmePath, "utf8");
|
|
6384
|
+
} catch {
|
|
6385
|
+
return new Map;
|
|
6386
|
+
}
|
|
6387
|
+
const rows = new Map;
|
|
6388
|
+
const lines = text.split(`
|
|
6389
|
+
`);
|
|
6390
|
+
let inSection = false;
|
|
6391
|
+
for (const line of lines) {
|
|
6392
|
+
if (/^##\s+Execution order & status/.test(line)) {
|
|
6393
|
+
inSection = true;
|
|
6394
|
+
continue;
|
|
6395
|
+
}
|
|
6396
|
+
if (inSection && /^#/.test(line)) {
|
|
6397
|
+
break;
|
|
6398
|
+
}
|
|
6399
|
+
if (!inSection)
|
|
6400
|
+
continue;
|
|
6401
|
+
const cells = line.split(/(?<!\\)\|/).map((c) => c.trim());
|
|
6402
|
+
if (cells.length >= 3 && /^\d{3}$/.test(cells[1])) {
|
|
6403
|
+
rows.set(cells[1], { title: cells[2].replace(/\\\|/g, "|") });
|
|
6404
|
+
}
|
|
6405
|
+
}
|
|
6406
|
+
return rows;
|
|
6407
|
+
}
|
|
6408
|
+
function planFileRel(outDir, planFile) {
|
|
6409
|
+
const resolved = resolve9(outDir);
|
|
6410
|
+
const parts = resolved.split(sep3);
|
|
6411
|
+
const plansIdx = parts.lastIndexOf("plans");
|
|
6412
|
+
if (plansIdx >= 0) {
|
|
6413
|
+
return `${parts.slice(plansIdx + 1).join(sep3)}${sep3}${planFile}`;
|
|
6414
|
+
}
|
|
6415
|
+
return planFile;
|
|
6416
|
+
}
|
|
6192
6417
|
function violation10(severity, code, message, fix) {
|
|
6193
6418
|
return { ok: false, severity, code, message, fix };
|
|
6194
6419
|
}
|
|
@@ -6444,7 +6669,7 @@ function collectKnowledgeDocs(dir) {
|
|
|
6444
6669
|
if (entry.isDirectory()) {
|
|
6445
6670
|
stack.push(full);
|
|
6446
6671
|
} else if (entry.name.endsWith(".md") && entry.name !== "README.md" && entry.name !== "index.md") {
|
|
6447
|
-
docs.push(relative4(dir, full).split(
|
|
6672
|
+
docs.push(relative4(dir, full).split(sep4).join("/"));
|
|
6448
6673
|
}
|
|
6449
6674
|
}
|
|
6450
6675
|
}
|
|
@@ -6461,7 +6686,7 @@ function normalizeIndexRef(cell) {
|
|
|
6461
6686
|
function assertIndexRows(knowledgeDir) {
|
|
6462
6687
|
const violations = [];
|
|
6463
6688
|
const readmePath = join12(knowledgeDir, "README.md");
|
|
6464
|
-
if (!
|
|
6689
|
+
if (!existsSync8(readmePath)) {
|
|
6465
6690
|
violations.push(violation10("medium", "compound.index.missing-readme", `missing ${readmePath} \u2014 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"));
|
|
6466
6691
|
return { ok: false, violations };
|
|
6467
6692
|
}
|
|
@@ -6485,7 +6710,7 @@ function assertIndexRows(knowledgeDir) {
|
|
|
6485
6710
|
return { ok: violations.length === 0, violations };
|
|
6486
6711
|
}
|
|
6487
6712
|
function isFileLikeRoot(root) {
|
|
6488
|
-
return /^[^.]*\.[A-Za-z0-9]{1,10}$/.test(
|
|
6713
|
+
return /^[^.]*\.[A-Za-z0-9]{1,10}$/.test(basename5(root));
|
|
6489
6714
|
}
|
|
6490
6715
|
function scopeGuard(path2, allowedRoots) {
|
|
6491
6716
|
const resolved = resolve10(path2);
|
|
@@ -6494,7 +6719,7 @@ function scopeGuard(path2, allowedRoots) {
|
|
|
6494
6719
|
if (isFileLikeRoot(r)) {
|
|
6495
6720
|
if (resolved === r)
|
|
6496
6721
|
return { ok: true, violations: [] };
|
|
6497
|
-
} else if (resolved === r || resolved.startsWith(r +
|
|
6722
|
+
} else if (resolved === r || resolved.startsWith(r + sep4)) {
|
|
6498
6723
|
return { ok: true, violations: [] };
|
|
6499
6724
|
}
|
|
6500
6725
|
}
|
|
@@ -6791,7 +7016,7 @@ function validateRoleMapping(rolesDir, options = {}) {
|
|
|
6791
7016
|
const violations = [];
|
|
6792
7017
|
const referenceById = new Map(mapping.map((m) => [m.agentId, m.reference]));
|
|
6793
7018
|
for (const { agentId, reference } of mapping) {
|
|
6794
|
-
if (!
|
|
7019
|
+
if (!existsSync9(join13(rolesDir, reference))) {
|
|
6795
7020
|
violations.push(violation12("medium", "roles.mapping.reference.missing", `role "${agentId}" maps to ${reference} which does not exist under ${rolesDir} (mstar-roles \u00a7 Role Reference Mapping)`, `create ${join13(rolesDir, reference)} or fix the mapping row`));
|
|
6796
7021
|
}
|
|
6797
7022
|
}
|
|
@@ -6993,28 +7218,28 @@ function lintFiveQuestion(bodyText, mode = "authoring") {
|
|
|
6993
7218
|
}
|
|
6994
7219
|
|
|
6995
7220
|
// ../engine/dist/engine.js
|
|
6996
|
-
import { existsSync as
|
|
6997
|
-
import { randomUUID } from "node:crypto";
|
|
6998
|
-
import { basename, dirname, join, resolve } from "node:path";
|
|
7221
|
+
import { existsSync as existsSync10, mkdirSync as mkdirSync4, readFileSync as readFileSync8, renameSync as renameSync2, unlinkSync as unlinkSync3, writeFileSync as writeFileSync4 } from "node:fs";
|
|
7222
|
+
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
7223
|
+
import { basename as basename6, dirname as dirname7, join as join5, resolve as resolve8 } from "node:path";
|
|
6999
7224
|
import { fileURLToPath } from "node:url";
|
|
7000
|
-
import { readFileSync as readFileSync22, statSync as
|
|
7225
|
+
import { readFileSync as readFileSync22, statSync as statSync5 } from "node:fs";
|
|
7001
7226
|
import { dirname as dirname22, isAbsolute as isAbsolute6, join as join22, relative as relative3, resolve as resolve22 } from "node:path";
|
|
7002
|
-
import { mkdirSync as mkdirSync22, readdirSync as readdirSync6, readFileSync as readFileSync32, statSync as statSync22 } from "node:fs";
|
|
7227
|
+
import { existsSync as existsSync22, mkdirSync as mkdirSync22, readdirSync as readdirSync6, readFileSync as readFileSync32, realpathSync as realpathSync4, statSync as statSync22 } from "node:fs";
|
|
7003
7228
|
import { execFileSync as execFileSync4 } from "node:child_process";
|
|
7004
7229
|
import { basename as basename22, dirname as dirname32, isAbsolute as isAbsolute22, join as join32, relative as relative22, resolve as resolve32 } from "node:path";
|
|
7005
|
-
import { existsSync as
|
|
7230
|
+
import { existsSync as existsSync32, readFileSync as readFileSync42, readdirSync as readdirSync22, realpathSync as realpathSync22 } from "node:fs";
|
|
7006
7231
|
import { dirname as dirname52, join as join62, resolve as resolve52, sep as sep2 } from "node:path";
|
|
7007
|
-
import { mkdirSync as mkdirSync32, rmdirSync, statSync as statSync32, unlinkSync as unlinkSync22, writeFileSync as writeFileSync22 } from "node:fs";
|
|
7232
|
+
import { mkdirSync as mkdirSync32, rmdirSync as rmdirSync3, statSync as statSync32, unlinkSync as unlinkSync22, writeFileSync as writeFileSync22 } from "node:fs";
|
|
7008
7233
|
import { dirname as dirname42, isAbsolute as isAbsolute32, join as join42, resolve as resolve42 } from "node:path";
|
|
7009
|
-
import { setTimeout as
|
|
7234
|
+
import { setTimeout as sleep2 } from "node:timers/promises";
|
|
7010
7235
|
import { AsyncLocalStorage as AsyncLocalStorage3 } from "node:async_hooks";
|
|
7011
|
-
import { mkdirSync as
|
|
7012
|
-
import { join as
|
|
7013
|
-
import { copyFileSync, mkdirSync as mkdirSync6, readFileSync as readFileSync82, readdirSync as readdirSync62, writeFileSync as
|
|
7014
|
-
import { dirname as
|
|
7236
|
+
import { mkdirSync as mkdirSync42 } from "node:fs";
|
|
7237
|
+
import { join as join52 } from "node:path";
|
|
7238
|
+
import { copyFileSync, mkdirSync as mkdirSync6, readFileSync as readFileSync82, readdirSync as readdirSync62, writeFileSync as writeFileSync42 } from "node:fs";
|
|
7239
|
+
import { dirname as dirname72, isAbsolute as isAbsolute62, join as join10, relative as relative32, resolve as resolve82, sep as sep22 } from "node:path";
|
|
7015
7240
|
var SEVERITY_ORDER2 = ["critical", "high", "medium", "low", "nit"];
|
|
7016
7241
|
function readJson2(filePath) {
|
|
7017
|
-
if (!
|
|
7242
|
+
if (!existsSync10(filePath))
|
|
7018
7243
|
return {};
|
|
7019
7244
|
const content = readFileSync8(filePath, "utf8").trim();
|
|
7020
7245
|
if (!content)
|
|
@@ -7025,28 +7250,28 @@ function readJson2(filePath) {
|
|
|
7025
7250
|
throw new Error(`Invalid JSON in ${filePath}: ${error.message}`);
|
|
7026
7251
|
}
|
|
7027
7252
|
}
|
|
7028
|
-
function
|
|
7029
|
-
const parent =
|
|
7030
|
-
|
|
7031
|
-
const tmp =
|
|
7253
|
+
function writeJson2(filePath, value) {
|
|
7254
|
+
const parent = dirname7(filePath);
|
|
7255
|
+
mkdirSync4(parent, { recursive: true });
|
|
7256
|
+
const tmp = join5(parent, `.${basename6(filePath)}.${process.pid}.${randomUUID2()}.tmp`);
|
|
7032
7257
|
try {
|
|
7033
|
-
|
|
7258
|
+
writeFileSync4(tmp, `${JSON.stringify(value, null, 2)}
|
|
7034
7259
|
`, "utf8");
|
|
7035
7260
|
renameSync2(tmp, filePath);
|
|
7036
7261
|
} catch (error) {
|
|
7037
7262
|
try {
|
|
7038
|
-
|
|
7263
|
+
unlinkSync3(tmp);
|
|
7039
7264
|
} catch {}
|
|
7040
7265
|
throw error;
|
|
7041
7266
|
}
|
|
7042
7267
|
}
|
|
7043
7268
|
function resolveProjectRoot(startDir = process.cwd()) {
|
|
7044
|
-
const start =
|
|
7269
|
+
const start = resolve8(startDir);
|
|
7045
7270
|
let dir = start;
|
|
7046
7271
|
for (;; ) {
|
|
7047
|
-
if (
|
|
7272
|
+
if (existsSync10(join5(dir, "package.json")) || existsSync10(join5(dir, "bun.lock")))
|
|
7048
7273
|
return dir;
|
|
7049
|
-
const parent =
|
|
7274
|
+
const parent = dirname7(dir);
|
|
7050
7275
|
if (parent === dir)
|
|
7051
7276
|
return start;
|
|
7052
7277
|
dir = parent;
|
|
@@ -7055,20 +7280,20 @@ function resolveProjectRoot(startDir = process.cwd()) {
|
|
|
7055
7280
|
function findRootPackageJson(startDir) {
|
|
7056
7281
|
let dir = startDir;
|
|
7057
7282
|
for (;; ) {
|
|
7058
|
-
const candidate =
|
|
7283
|
+
const candidate = resolve8(dir, "package.json");
|
|
7059
7284
|
try {
|
|
7060
7285
|
const pkg = JSON.parse(readFileSync8(candidate, "utf8"));
|
|
7061
7286
|
if (pkg.name === "morning-star")
|
|
7062
7287
|
return candidate;
|
|
7063
7288
|
} catch {}
|
|
7064
|
-
const parent =
|
|
7289
|
+
const parent = dirname7(dir);
|
|
7065
7290
|
if (parent === dir)
|
|
7066
7291
|
return null;
|
|
7067
7292
|
dir = parent;
|
|
7068
7293
|
}
|
|
7069
7294
|
}
|
|
7070
7295
|
function harnessVersionFrom(moduleDir) {
|
|
7071
|
-
const ownManifest =
|
|
7296
|
+
const ownManifest = join5(moduleDir, "..", "package.json");
|
|
7072
7297
|
try {
|
|
7073
7298
|
const pkg = JSON.parse(readFileSync8(ownManifest, "utf8"));
|
|
7074
7299
|
if (typeof pkg.version === "string" && pkg.version !== "")
|
|
@@ -7085,7 +7310,7 @@ function harnessVersionFrom(moduleDir) {
|
|
|
7085
7310
|
}
|
|
7086
7311
|
}
|
|
7087
7312
|
function readHarnessVersion() {
|
|
7088
|
-
return harnessVersionFrom(
|
|
7313
|
+
return harnessVersionFrom(dirname7(fileURLToPath(import.meta.url)));
|
|
7089
7314
|
}
|
|
7090
7315
|
var MSTARC_FILE2 = ".mstarc";
|
|
7091
7316
|
var MSTARC_SECTION2 = "config";
|
|
@@ -7140,7 +7365,7 @@ function parseMstarc2(text) {
|
|
|
7140
7365
|
}
|
|
7141
7366
|
function isFile3(file) {
|
|
7142
7367
|
try {
|
|
7143
|
-
return
|
|
7368
|
+
return statSync5(file).isFile();
|
|
7144
7369
|
} catch {
|
|
7145
7370
|
return false;
|
|
7146
7371
|
}
|
|
@@ -7374,11 +7599,11 @@ function verifyPlanExecutionLease(row, planId) {
|
|
|
7374
7599
|
violations.push(...validateExecutionLease2(lease).violations);
|
|
7375
7600
|
return { ok: violations.length === 0, violations, lease };
|
|
7376
7601
|
}
|
|
7377
|
-
var
|
|
7378
|
-
var
|
|
7602
|
+
var STATUS_WRITE_LOCKDIR2 = ".status-write.lockdir";
|
|
7603
|
+
var LOCKDIR_HOLDER_PID2 = "holder.pid";
|
|
7379
7604
|
var heldLockDirs2 = new AsyncLocalStorage3;
|
|
7380
|
-
async function
|
|
7381
|
-
const lockDir = join42(dirname42(resolve42(statusPath)),
|
|
7605
|
+
async function withStatusWriteLock2(statusPath, fn, opts = {}) {
|
|
7606
|
+
const lockDir = join42(dirname42(resolve42(statusPath)), STATUS_WRITE_LOCKDIR2);
|
|
7382
7607
|
const held = heldLockDirs2.getStore();
|
|
7383
7608
|
if (held !== undefined && held.has(lockDir)) {
|
|
7384
7609
|
throw new Error(`${lockDir} is already held by this process in this async context \u2014 withStatusWriteLock is not reentrant; a nested acquisition on the same status.json is a bug`);
|
|
@@ -7399,11 +7624,11 @@ async function withStatusWriteLock(statusPath, fn, opts = {}) {
|
|
|
7399
7624
|
if (Date.now() >= deadline) {
|
|
7400
7625
|
throw new Error(`${lockDir} already exists \u2014 another writer holds the status write lock; Blocked (same-host exclusive lock; status-and-residuals.md \u00a7 Same-host exclusive write lock). ` + `Recovery: remove ${lockDir} if no writer is alive (holder.pid inside names the acquiring process)`);
|
|
7401
7626
|
}
|
|
7402
|
-
await
|
|
7627
|
+
await sleep2(pollMs);
|
|
7403
7628
|
}
|
|
7404
7629
|
}
|
|
7405
7630
|
try {
|
|
7406
|
-
writeFileSync22(join42(lockDir,
|
|
7631
|
+
writeFileSync22(join42(lockDir, LOCKDIR_HOLDER_PID2), String(process.pid), "utf8");
|
|
7407
7632
|
} catch {}
|
|
7408
7633
|
const owns = held ?? new Set;
|
|
7409
7634
|
owns.add(lockDir);
|
|
@@ -7415,9 +7640,9 @@ async function withStatusWriteLock(statusPath, fn, opts = {}) {
|
|
|
7415
7640
|
const current = statSync32(lockDir);
|
|
7416
7641
|
if (acquired !== null && current.dev === acquired.dev && current.ino === acquired.ino) {
|
|
7417
7642
|
try {
|
|
7418
|
-
unlinkSync22(join42(lockDir,
|
|
7643
|
+
unlinkSync22(join42(lockDir, LOCKDIR_HOLDER_PID2));
|
|
7419
7644
|
} catch {}
|
|
7420
|
-
|
|
7645
|
+
rmdirSync3(lockDir);
|
|
7421
7646
|
}
|
|
7422
7647
|
} catch {}
|
|
7423
7648
|
}
|
|
@@ -7538,10 +7763,10 @@ async function writeWorkflowSnapshot(snapshot, dir) {
|
|
|
7538
7763
|
const detail = gate2.violations.map((v) => v.message).join("; ");
|
|
7539
7764
|
throw new Error(`refusing to write invalid workflow snapshot: ${detail}`);
|
|
7540
7765
|
}
|
|
7541
|
-
const snapshotPath =
|
|
7542
|
-
|
|
7543
|
-
await
|
|
7544
|
-
|
|
7766
|
+
const snapshotPath = join52(dir, WORKFLOW_SNAPSHOT_FILE2);
|
|
7767
|
+
mkdirSync42(dir, { recursive: true });
|
|
7768
|
+
await withStatusWriteLock2(snapshotPath, () => {
|
|
7769
|
+
writeJson2(snapshotPath, snapshot);
|
|
7545
7770
|
});
|
|
7546
7771
|
}
|
|
7547
7772
|
var DATE_RE3 = /^\d{4}-\d{2}-\d{2}$/;
|
|
@@ -7755,7 +7980,7 @@ function validateStatusV22(docOrPath, opts = {}) {
|
|
|
7755
7980
|
if (harnessDir !== undefined && Array.isArray(doc.workflows)) {
|
|
7756
7981
|
let realHarnessDir = null;
|
|
7757
7982
|
try {
|
|
7758
|
-
realHarnessDir =
|
|
7983
|
+
realHarnessDir = realpathSync22(harnessDir);
|
|
7759
7984
|
} catch {}
|
|
7760
7985
|
for (const entry of doc.workflows) {
|
|
7761
7986
|
if (!isPlainObject32(entry) || typeof entry.dir !== "string")
|
|
@@ -7765,7 +7990,7 @@ function validateStatusV22(docOrPath, opts = {}) {
|
|
|
7765
7990
|
const label = typeof entry.id === "string" ? entry.id : relSnapshot;
|
|
7766
7991
|
let physical;
|
|
7767
7992
|
try {
|
|
7768
|
-
physical =
|
|
7993
|
+
physical = realpathSync22(snapshotPath);
|
|
7769
7994
|
} catch {
|
|
7770
7995
|
violations.push(violation42("high", "status.workflow.snapshot-missing", `workflows[] lists ${JSON.stringify(label)} but its snapshot does not exist at ${JSON.stringify(relSnapshot)} \u2014 the root holds active lifecycles only; unregister the id when its snapshot is removed`));
|
|
7771
7996
|
continue;
|
|
@@ -7788,7 +8013,7 @@ function validateStatusV22(docOrPath, opts = {}) {
|
|
|
7788
8013
|
violations.push(violation42("medium", "status.workflow.mismatched-type", `workflows[] entry ${JSON.stringify(label)} type ${JSON.stringify(entry.type)} does not match its snapshot type ${JSON.stringify(snapshot.type)} \u2014 the root entry mirrors the snapshot; align them`));
|
|
7789
8014
|
}
|
|
7790
8015
|
if (typeof entry.started_at === "string" && typeof snapshot.started_at === "string" && entry.started_at !== snapshot.started_at) {
|
|
7791
|
-
violations.push(violation42("medium", "status.workflow.mismatched-started-at", `workflows[] entry ${JSON.stringify(label)} started_at ${JSON.stringify(entry.started_at)} does not match its snapshot started_at ${JSON.stringify(snapshot.started_at)} \u2014 the root entry mirrors the snapshot
|
|
8016
|
+
violations.push(violation42("medium", "status.workflow.mismatched-started-at", `workflows[] entry ${JSON.stringify(label)} started_at ${JSON.stringify(entry.started_at)} does not match its snapshot started_at ${JSON.stringify(snapshot.started_at)} \u2014 workflow ${JSON.stringify(label)} collided with another writer (e.g. a concurrent/re-run \`audit promote\` with the same workflow id rewrote the snapshot); the root entry mirrors the snapshot \u2014 align them or remove the colliding workflow`));
|
|
7792
8017
|
}
|
|
7793
8018
|
}
|
|
7794
8019
|
}
|
|
@@ -8220,7 +8445,7 @@ function collectNotesFiles(snapshots) {
|
|
|
8220
8445
|
if (lines.length === 0)
|
|
8221
8446
|
continue;
|
|
8222
8447
|
out.push({
|
|
8223
|
-
file: join10(
|
|
8448
|
+
file: join10(dirname72(snapshot.file), NOTES_LEDGER_FILE),
|
|
8224
8449
|
source,
|
|
8225
8450
|
lines
|
|
8226
8451
|
});
|
|
@@ -8228,7 +8453,7 @@ function collectNotesFiles(snapshots) {
|
|
|
8228
8453
|
return out;
|
|
8229
8454
|
}
|
|
8230
8455
|
function migrateHarnessTree(root, opts = {}) {
|
|
8231
|
-
const harnessDir =
|
|
8456
|
+
const harnessDir = resolve82(root);
|
|
8232
8457
|
const workflowDir = resolveWorkflowDir2(harnessDir, { harnessDir });
|
|
8233
8458
|
const projectDir = resolveProjectDir2(harnessDir, { harnessDir });
|
|
8234
8459
|
const projectId = opts.projectId ?? _DEFAULT_PROJECT2;
|
|
@@ -8382,9 +8607,9 @@ async function applyMigratePlan(plan) {
|
|
|
8382
8607
|
if (current.version === 2) {
|
|
8383
8608
|
return { applied: false, message: "no-op: status.json already at schema version 2 (migrated) \u2014 nothing to do" };
|
|
8384
8609
|
}
|
|
8385
|
-
const harnessRoot =
|
|
8386
|
-
const workflowRoot =
|
|
8387
|
-
const projectRoot =
|
|
8610
|
+
const harnessRoot = resolve82(plan.root);
|
|
8611
|
+
const workflowRoot = resolve82(plan.workflowDir);
|
|
8612
|
+
const projectRoot = resolve82(plan.projectDir);
|
|
8388
8613
|
if (!isAbsolute62(plan.workflowDir) || !isAbsolute62(plan.projectDir)) {
|
|
8389
8614
|
throw new Error(`refusing to apply migration: plan workflowDir/projectDir must be absolute (got ${JSON.stringify(plan.workflowDir)} / ${JSON.stringify(plan.projectDir)})`);
|
|
8390
8615
|
}
|
|
@@ -8398,44 +8623,46 @@ async function applyMigratePlan(plan) {
|
|
|
8398
8623
|
...plan.roadmap !== null ? [plan.roadmap.file] : []
|
|
8399
8624
|
];
|
|
8400
8625
|
for (const destination of allDestinations) {
|
|
8401
|
-
const resolvedDest =
|
|
8626
|
+
const resolvedDest = resolve82(join10(plan.root, destination));
|
|
8402
8627
|
const inside = (dir) => resolvedDest === dir || resolvedDest.startsWith(`${dir}${sep22}`);
|
|
8403
8628
|
if (!inside(harnessRoot) && !inside(workflowRoot) && !inside(projectRoot)) {
|
|
8404
8629
|
throw new Error(`refusing to apply migration: destination escapes the harness dir (${JSON.stringify(destination)}) \u2014 every write must stay under ${JSON.stringify(plan.root)}, the workflow dir (${JSON.stringify(plan.workflowDir)}) or the project dir (${JSON.stringify(plan.projectDir)})`);
|
|
8405
8630
|
}
|
|
8406
8631
|
}
|
|
8407
|
-
mkdirSync6(join10(plan.root,
|
|
8632
|
+
mkdirSync6(join10(plan.root, dirname72(plan.archive.file)), { recursive: true });
|
|
8408
8633
|
copyFileSync(statusPath, join10(plan.root, plan.archive.file));
|
|
8409
8634
|
for (const snapshot of plan.snapshots) {
|
|
8410
|
-
await writeWorkflowSnapshot(snapshot.data,
|
|
8635
|
+
await writeWorkflowSnapshot(snapshot.data, dirname72(workflowTargetOf(snapshot.file)));
|
|
8411
8636
|
}
|
|
8412
8637
|
for (const notes of plan.notesFiles) {
|
|
8413
8638
|
const filePath = workflowTargetOf(notes.file);
|
|
8414
|
-
mkdirSync6(
|
|
8639
|
+
mkdirSync6(dirname72(filePath), { recursive: true });
|
|
8415
8640
|
const content = notes.lines.length > 0 ? `${notes.lines.join(`
|
|
8416
8641
|
`)}
|
|
8417
8642
|
` : "";
|
|
8418
|
-
|
|
8643
|
+
writeFileSync42(filePath, content, "utf8");
|
|
8419
8644
|
}
|
|
8420
8645
|
if (plan.register !== null) {
|
|
8421
8646
|
const gate2 = validateProjectRegister(plan.register.data);
|
|
8422
8647
|
if (!gate2.ok) {
|
|
8423
8648
|
throw new Error(`refusing to apply migration: invalid project register: ${gate2.violations.map((v) => v.message).join("; ")}`);
|
|
8424
8649
|
}
|
|
8425
|
-
|
|
8426
|
-
|
|
8427
|
-
|
|
8650
|
+
if (Object.keys(plan.register.data.entries ?? {}).length > 0) {
|
|
8651
|
+
const filePath = projectTargetOf(plan.register.file);
|
|
8652
|
+
mkdirSync6(dirname72(filePath), { recursive: true });
|
|
8653
|
+
writeJson2(filePath, plan.register.data);
|
|
8654
|
+
}
|
|
8428
8655
|
}
|
|
8429
8656
|
if (plan.roadmap !== null) {
|
|
8430
8657
|
const filePath = projectTargetOf(plan.roadmap.file);
|
|
8431
|
-
mkdirSync6(
|
|
8432
|
-
|
|
8658
|
+
mkdirSync6(dirname72(filePath), { recursive: true });
|
|
8659
|
+
writeFileSync42(filePath, plan.roadmap.content, "utf8");
|
|
8433
8660
|
}
|
|
8434
8661
|
const rootGate = validateStatusV22(plan.rootV2.data, { harnessDir: plan.root });
|
|
8435
8662
|
if (!rootGate.ok) {
|
|
8436
8663
|
throw new Error(`refusing to apply migration: invalid v2 root: ${rootGate.violations.map((v) => v.message).join("; ")}`);
|
|
8437
8664
|
}
|
|
8438
|
-
return
|
|
8665
|
+
return withStatusWriteLock2(statusPath, () => {
|
|
8439
8666
|
const latest = readJson2(statusPath);
|
|
8440
8667
|
if (latest.version === 2) {
|
|
8441
8668
|
return {
|
|
@@ -8443,7 +8670,7 @@ async function applyMigratePlan(plan) {
|
|
|
8443
8670
|
message: "no-op: status.json already at schema version 2 (migrated) \u2014 nothing to do"
|
|
8444
8671
|
};
|
|
8445
8672
|
}
|
|
8446
|
-
|
|
8673
|
+
writeJson2(statusPath, plan.rootV2.data);
|
|
8447
8674
|
return {
|
|
8448
8675
|
applied: true,
|
|
8449
8676
|
message: `migrated ${plan.snapshots.length} lifecycles into workflows/, project layer seeded, root status.json replaced (v1 archived to ${plan.archive.file})`
|
|
@@ -9028,7 +9255,22 @@ import path5 from "node:path";
|
|
|
9028
9255
|
import fs2 from "node:fs";
|
|
9029
9256
|
import os from "node:os";
|
|
9030
9257
|
import path4 from "node:path";
|
|
9258
|
+
|
|
9259
|
+
// src/exec.ts
|
|
9031
9260
|
import { execFileSync as execFileSync6 } from "node:child_process";
|
|
9261
|
+
function runCliCommand(command, opts = {}) {
|
|
9262
|
+
if (opts.dryRun)
|
|
9263
|
+
return "";
|
|
9264
|
+
return execFileSync6(command[0], command.slice(1), {
|
|
9265
|
+
cwd: opts.cwd,
|
|
9266
|
+
encoding: "utf8",
|
|
9267
|
+
env: opts.env,
|
|
9268
|
+
stdio: "pipe",
|
|
9269
|
+
timeout: opts.timeoutMs
|
|
9270
|
+
});
|
|
9271
|
+
}
|
|
9272
|
+
|
|
9273
|
+
// src/adapters/shared-install.ts
|
|
9032
9274
|
var REPO_URL = "https://github.com/btspoony/mstar-harness.git";
|
|
9033
9275
|
var PLUGIN_NAME = "morning-star-harness";
|
|
9034
9276
|
var HARNESS_REPO_PATH = path4.join(os.homedir(), ".mstar", "harness");
|
|
@@ -9060,9 +9302,7 @@ function ensureDir(dirPath, dryRun) {
|
|
|
9060
9302
|
fs2.mkdirSync(dirPath, { recursive: true });
|
|
9061
9303
|
}
|
|
9062
9304
|
function runCommand(command, cwd, dryRun) {
|
|
9063
|
-
|
|
9064
|
-
return;
|
|
9065
|
-
execFileSync6(command[0], command.slice(1), { cwd, stdio: "pipe", encoding: "utf8" });
|
|
9305
|
+
runCliCommand(command, { cwd, dryRun });
|
|
9066
9306
|
}
|
|
9067
9307
|
function ensureLocalHarnessRepo(dryRun) {
|
|
9068
9308
|
const notes = [];
|
|
@@ -9411,7 +9651,7 @@ function runInit(scope, dryRun) {
|
|
|
9411
9651
|
}
|
|
9412
9652
|
notes.push(...ensureAgentLinks(scope, dryRun));
|
|
9413
9653
|
if (!dryRun)
|
|
9414
|
-
|
|
9654
|
+
writeJson2(pathToMarketplace, next);
|
|
9415
9655
|
notes.push(existingEntry ? `Updated ${PLUGIN_NAME} local marketplace entry.` : `Added ${PLUGIN_NAME} local marketplace entry.`);
|
|
9416
9656
|
notes.push(`Source path: ${mstarEntry(scope).source.path}`);
|
|
9417
9657
|
notes.push(`Install after init: codex plugin add ${PLUGIN_NAME} --marketplace ${MARKETPLACE_NAME}`);
|
|
@@ -9547,7 +9787,6 @@ var cursorAdapter = {
|
|
|
9547
9787
|
};
|
|
9548
9788
|
|
|
9549
9789
|
// src/adapters/dsh.ts
|
|
9550
|
-
import { execFileSync as execFileSync7 } from "node:child_process";
|
|
9551
9790
|
import os4 from "node:os";
|
|
9552
9791
|
import path7 from "node:path";
|
|
9553
9792
|
var DSH_BIN = "dsh";
|
|
@@ -9571,14 +9810,7 @@ var DSH_PLUGIN_SPECS = ["@mstar-harness/dsh", "dsh-llm-fallbacks"];
|
|
|
9571
9810
|
var DSH_FALLBACKS_SPEC = DSH_PLUGIN_SPECS[1];
|
|
9572
9811
|
var DSH_INSTALL_HINT = "Install the DeepSeek Harness CLI (@deepseek-ai/dsh), e.g. `pnpm add -g @deepseek-ai/dsh` or `npm install -g @deepseek-ai/dsh`, then re-run init.";
|
|
9573
9812
|
function runDsh(args, dryRun, timeoutMs) {
|
|
9574
|
-
|
|
9575
|
-
return "";
|
|
9576
|
-
return execFileSync7(DSH_BIN, args, {
|
|
9577
|
-
stdio: "pipe",
|
|
9578
|
-
encoding: "utf8",
|
|
9579
|
-
env: process.env,
|
|
9580
|
-
timeout: timeoutMs
|
|
9581
|
-
});
|
|
9813
|
+
return runCliCommand([DSH_BIN, ...args], { dryRun, timeoutMs, env: process.env });
|
|
9582
9814
|
}
|
|
9583
9815
|
function dshAvailable() {
|
|
9584
9816
|
try {
|
|
@@ -9717,7 +9949,7 @@ var dshAdapter = {
|
|
|
9717
9949
|
// src/adapters/omp.ts
|
|
9718
9950
|
import fs5 from "node:fs";
|
|
9719
9951
|
import path8 from "node:path";
|
|
9720
|
-
import { execFileSync as
|
|
9952
|
+
import { execFileSync as execFileSync7 } from "node:child_process";
|
|
9721
9953
|
var OMP_PLUGIN_MARKER = ".omp-plugin/plugin.json";
|
|
9722
9954
|
var CLAUDE_PLUGIN_MARKER = ".claude-plugin/plugin.json";
|
|
9723
9955
|
var PACKAGE_NAMES = new Set(["morning-star", PLUGIN_NAME, "github:btspoony/mstar-harness"]);
|
|
@@ -9725,20 +9957,18 @@ var SKILL_SMOKE = ["mstar-host", "mstar-harness-core", "pm"];
|
|
|
9725
9957
|
var COMMAND_SMOKE = ["iteration-start", "iteration-drive", "iteration-loop", "codebase-audit"];
|
|
9726
9958
|
function ompAvailable() {
|
|
9727
9959
|
try {
|
|
9728
|
-
|
|
9960
|
+
execFileSync7("omp", ["--version"], { stdio: "pipe", encoding: "utf8" });
|
|
9729
9961
|
return true;
|
|
9730
9962
|
} catch {
|
|
9731
9963
|
return false;
|
|
9732
9964
|
}
|
|
9733
9965
|
}
|
|
9734
9966
|
function runOmp(args, dryRun) {
|
|
9735
|
-
|
|
9736
|
-
return;
|
|
9737
|
-
execFileSync8("omp", args, { stdio: "pipe", encoding: "utf8" });
|
|
9967
|
+
runCliCommand(["omp", ...args], { dryRun });
|
|
9738
9968
|
}
|
|
9739
9969
|
function listInstalledPlugins() {
|
|
9740
9970
|
try {
|
|
9741
|
-
const raw =
|
|
9971
|
+
const raw = execFileSync7("omp", ["plugin", "list", "--json"], {
|
|
9742
9972
|
stdio: "pipe",
|
|
9743
9973
|
encoding: "utf8"
|
|
9744
9974
|
});
|
|
@@ -9814,7 +10044,7 @@ function runInit3(scope, dryRun) {
|
|
|
9814
10044
|
notes.push(`Would update local harness repo: git -C ${HARNESS_REPO_PATH} pull --ff-only`);
|
|
9815
10045
|
} else {
|
|
9816
10046
|
try {
|
|
9817
|
-
|
|
10047
|
+
execFileSync7("git", ["-C", HARNESS_REPO_PATH, "pull", "--ff-only"], {
|
|
9818
10048
|
stdio: "pipe",
|
|
9819
10049
|
encoding: "utf8"
|
|
9820
10050
|
});
|
|
@@ -10175,13 +10405,13 @@ function runInit4(scope, dryRun) {
|
|
|
10175
10405
|
if (!dryRun) {
|
|
10176
10406
|
if (!fs6.existsSync(MARKETPLACE_DIR))
|
|
10177
10407
|
fs6.mkdirSync(MARKETPLACE_DIR, { recursive: true });
|
|
10178
|
-
|
|
10408
|
+
writeJson2(MARKETPLACE_JSON_PATH, buildMarketplaceJson());
|
|
10179
10409
|
}
|
|
10180
10410
|
notes.push(`Wrote ZCode marketplace: ${MARKETPLACE_JSON_PATH}`);
|
|
10181
10411
|
const knownRaw = readJson2(KNOWN_MARKETPLACES_PATH);
|
|
10182
10412
|
const knownNext = upsertKnownMarketplace(knownRaw);
|
|
10183
10413
|
if (!dryRun)
|
|
10184
|
-
|
|
10414
|
+
writeJson2(KNOWN_MARKETPLACES_PATH, knownNext);
|
|
10185
10415
|
notes.push(`Registered ${MARKETPLACE_ID} marketplace in ${KNOWN_MARKETPLACES_PATH}`);
|
|
10186
10416
|
notes.push(`Then in ZCode: Settings \u2192 Plugin Management \u2192 Discover \u2192 install ${PLUGIN_NAME} from the ${MARKETPLACE_ID} marketplace.`);
|
|
10187
10417
|
return {
|
|
@@ -10364,7 +10594,7 @@ async function runInit5(options) {
|
|
|
10364
10594
|
- `)}`);
|
|
10365
10595
|
}
|
|
10366
10596
|
if (!options.dryRun) {
|
|
10367
|
-
|
|
10597
|
+
writeJson2(configPath, updated);
|
|
10368
10598
|
const persistedErrors = adapter.validateConfig?.(readJson2(configPath)) || [];
|
|
10369
10599
|
if (persistedErrors.length) {
|
|
10370
10600
|
throw new Error(`Post-write verification failed:
|
|
@@ -11219,7 +11449,7 @@ function resolveAuditShortSha(cwd, override) {
|
|
|
11219
11449
|
if (override !== undefined && override !== "")
|
|
11220
11450
|
return override;
|
|
11221
11451
|
try {
|
|
11222
|
-
const out =
|
|
11452
|
+
const out = execFileSync8("git", ["rev-parse", "--short", "HEAD"], {
|
|
11223
11453
|
cwd,
|
|
11224
11454
|
encoding: "utf8",
|
|
11225
11455
|
stdio: ["ignore", "pipe", "ignore"]
|
|
@@ -11255,6 +11485,30 @@ auditCommand.command("scaffold").description("Scaffold an audit-<date>/ plan dir
|
|
|
11255
11485
|
failScript(error, "audit scaffold");
|
|
11256
11486
|
}
|
|
11257
11487
|
});
|
|
11488
|
+
auditCommand.command("promote").description("Promote selected audit plans into the v2 workflow lifecycle: write the workflow snapshot " + "(type plan, Todo rows) then register the workflow in {HARNESS_DIR}/status.json " + "(exit 2 on usage, 1 when the harness dir cannot be resolved)").argument("[audit-dir]", "audit-<date>/ directory under {PLAN_DIR}").option("--plans <ids>", "Comma-separated selected plan ids (README Plan column `001`, stem, or basename)").option("--workflow <id>", "Workflow id (default: audit-<date> basename)").option("--harness <dir>", "Harness dir containing status.json (default: resolveHarnessDir() / MSTAR_HARNESS_DIR)").action(async (auditDir, options) => {
|
|
11489
|
+
try {
|
|
11490
|
+
if (!auditDir) {
|
|
11491
|
+
throw new SddScriptError("usage: audit promote <audit-dir> --plans <ids> [--workflow <id>] [--harness <dir>]", 2);
|
|
11492
|
+
}
|
|
11493
|
+
const selected = parseCsv(options.plans);
|
|
11494
|
+
if (!selected || selected.length === 0) {
|
|
11495
|
+
throw new SddScriptError("usage: audit promote <audit-dir> --plans <ids> [--workflow <id>] [--harness <dir>]", 2);
|
|
11496
|
+
}
|
|
11497
|
+
const outDir = resolveCliPath(auditDir);
|
|
11498
|
+
if (!fs8.existsSync(outDir)) {
|
|
11499
|
+
throw new Error(`audit dir not found: ${outDir}`);
|
|
11500
|
+
}
|
|
11501
|
+
const harnessDir = resolveLeaseHarnessDir(options.harness);
|
|
11502
|
+
const result = await promoteAuditPlans(outDir, selected, {
|
|
11503
|
+
harnessDir,
|
|
11504
|
+
...options.workflow !== undefined ? { workflowId: options.workflow } : {}
|
|
11505
|
+
});
|
|
11506
|
+
console.log(import_picocolors2.default.green(`audit promote: OK \u2014 workflow ${result.workflowId} registered`));
|
|
11507
|
+
console.log(` snapshot: ${result.snapshotPath}`);
|
|
11508
|
+
} catch (error) {
|
|
11509
|
+
failScript(error, "audit promote");
|
|
11510
|
+
}
|
|
11511
|
+
});
|
|
11258
11512
|
var compoundCommand = program2.command("compound").description("knowledge-doc schema / index / scope checks (engine-backed)");
|
|
11259
11513
|
compoundCommand.command("validate").description("Validate a knowledge doc frontmatter (schema.yaml contract); with --knowledge-dir, also assert the " + "knowledge README index rows and guard the doc inside the knowledge scope (exit 1 on violations, 2 on usage)").argument("[doc-path]", "Knowledge doc (markdown with YAML frontmatter)").option("--knowledge-dir <dir>", "Knowledge directory (enables index-row asserts + scope guard)").action((docPath, options) => {
|
|
11260
11514
|
try {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mstar-harness/cli",
|
|
3
|
-
"version": "3.1.
|
|
3
|
+
"version": "3.1.3",
|
|
4
4
|
"description": "Morning Star harness CLI — installer bootstrap + mstar workflow verbs (path/status/lease/sdd/iteration/dispatch/worktree/lint/design-md/audit/compound/host/skill).",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|