@mstar-harness/cli 1.8.8 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/mstar-harness.js +3939 -308
  2. package/package.json +6 -2
@@ -2343,6 +2343,11 @@ var require_commander = __commonJS((exports) => {
2343
2343
  exports.InvalidOptionArgumentError = InvalidArgumentError;
2344
2344
  });
2345
2345
 
2346
+ // src/index.ts
2347
+ import { execFileSync as execFileSync6 } from "child_process";
2348
+ import fs7 from "fs";
2349
+ import path11 from "path";
2350
+
2346
2351
  // ../../node_modules/@inquirer/core/dist/lib/key.js
2347
2352
  var isUpKey = (key, keybindings = []) => key.name === "up" || keybindings.includes("vim") && key.name === "k" || keybindings.includes("emacs") && key.ctrl && key.name === "p";
2348
2353
  var isDownKey = (key, keybindings = []) => key.name === "down" || keybindings.includes("vim") && key.name === "j" || keybindings.includes("emacs") && key.ctrl && key.name === "n";
@@ -3969,73 +3974,33 @@ var {
3969
3974
  Help
3970
3975
  } = import__.default;
3971
3976
 
3972
- // src/constants.ts
3973
- var ALL_ROLES = [
3974
- "project-manager",
3975
- "architect",
3976
- "product-manager",
3977
- "prompt-engineer",
3978
- "fullstack-dev",
3979
- "fullstack-dev-2",
3980
- "frontend-dev",
3981
- "qc-specialist",
3982
- "qc-specialist-2",
3983
- "qc-specialist-3",
3984
- "qa-engineer",
3985
- "ops-engineer",
3986
- "writing-specialist"
3987
- ];
3988
- var ROLE_GROUPS = {
3989
- strategic: ["architect", "product-manager", "prompt-engineer"],
3990
- dev: ["fullstack-dev", "fullstack-dev-2", "frontend-dev"],
3991
- qc: ["qc-specialist", "qc-specialist-2", "qc-specialist-3"]
3992
- };
3993
-
3994
- // src/assignment.ts
3995
- function pickByOrder(roleIds, selectedModels) {
3996
- const assignments = {};
3997
- for (let i = 0;i < roleIds.length; i += 1) {
3998
- assignments[roleIds[i]] = selectedModels[i % selectedModels.length];
3999
- }
4000
- return assignments;
4001
- }
4002
- function pickRandom(roleIds, selectedModels) {
4003
- const assignments = {};
4004
- for (const roleId of roleIds) {
4005
- assignments[roleId] = selectedModels[Math.floor(Math.random() * selectedModels.length)];
4006
- }
4007
- return assignments;
4008
- }
4009
- function buildModelAssignments(selections) {
4010
- const assignments = {
4011
- "project-manager": selections.pm[0],
4012
- ...pickByOrder(ROLE_GROUPS.strategic, selections.strategic),
4013
- ...pickByOrder(ROLE_GROUPS.dev, selections.dev),
4014
- ...pickByOrder(ROLE_GROUPS.qc, selections.qc)
4015
- };
4016
- const assigned = new Set(Object.keys(assignments));
4017
- const others = ALL_ROLES.filter((role) => !assigned.has(role));
4018
- return { ...assignments, ...pickRandom(others, selections.others) };
4019
- }
4020
-
4021
- // src/adapters/codex.ts
4022
- import fs3 from "node:fs";
4023
- import os2 from "node:os";
4024
- import path4 from "node:path";
4025
-
4026
- // src/utils.ts
4027
- import fs from "node:fs";
4028
- import path2 from "node:path";
4029
- import { fileURLToPath } from "node:url";
4030
- function ensureObject(value) {
4031
- if (value && typeof value === "object" && !Array.isArray(value))
4032
- return value;
4033
- return {};
4034
- }
3977
+ // ../engine/dist/engine.js
3978
+ import { existsSync, mkdirSync, readFileSync, renameSync, unlinkSync, writeFileSync } from "node:fs";
3979
+ import { randomUUID } from "node:crypto";
3980
+ import { basename, dirname, join, resolve } from "node:path";
3981
+ import { mkdirSync as mkdirSync2, readdirSync, readFileSync as readFileSync2, statSync } from "node:fs";
3982
+ import { basename as basename2, dirname as dirname2, isAbsolute, join as join2, relative, resolve as resolve2 } from "node:path";
3983
+ import { existsSync as existsSync2, readFileSync as readFileSync3, readdirSync as readdirSync2 } from "node:fs";
3984
+ import { join as join4, resolve as resolve4 } from "node:path";
3985
+ import { mkdirSync as mkdirSync3, rmdirSync, statSync as statSync2, unlinkSync as unlinkSync2, writeFileSync as writeFileSync2 } from "node:fs";
3986
+ import { dirname as dirname3, isAbsolute as isAbsolute2, join as join3, resolve as resolve3 } from "node:path";
3987
+ import { setTimeout as sleep } from "node:timers/promises";
3988
+ import { AsyncLocalStorage as AsyncLocalStorage2 } from "node:async_hooks";
3989
+ import { execFileSync } from "node:child_process";
3990
+ import { existsSync as existsSync3 } from "node:fs";
3991
+ import { isAbsolute as isAbsolute3, resolve as resolve5 } from "node:path";
3992
+ import { execFileSync as execFileSync2 } from "node:child_process";
3993
+ import { mkdirSync as mkdirSync4, readFileSync as readFileSync4, realpathSync, statSync as statSync3, writeFileSync as writeFileSync3 } from "node:fs";
3994
+ import { basename as basename3, dirname as dirname4, isAbsolute as isAbsolute4, join as join5, resolve as resolve6 } from "node:path";
3995
+ import { mkdirSync as mkdirSync5, readdirSync as readdirSync4, readFileSync as readFileSync6, writeFileSync as writeFileSync4 } from "node:fs";
3996
+ import { join as join7, resolve as resolve7 } from "node:path";
3997
+ import { existsSync as existsSync5, readdirSync as readdirSync5, readFileSync as readFileSync7 } from "node:fs";
3998
+ import { basename as basename4, isAbsolute as isAbsolute5, join as join8, relative as relative2, resolve as resolve8, sep } from "node:path";
3999
+ var SEVERITY_ORDER = ["critical", "high", "medium", "low", "nit"];
4035
4000
  function readJson(filePath) {
4036
- if (!fs.existsSync(filePath))
4001
+ if (!existsSync(filePath))
4037
4002
  return {};
4038
- const content = fs.readFileSync(filePath, "utf8").trim();
4003
+ const content = readFileSync(filePath, "utf8").trim();
4039
4004
  if (!content)
4040
4005
  return {};
4041
4006
  try {
@@ -4045,187 +4010,3102 @@ function readJson(filePath) {
4045
4010
  }
4046
4011
  }
4047
4012
  function writeJson(filePath, value) {
4048
- const parent = path2.dirname(filePath);
4049
- if (!fs.existsSync(parent))
4050
- fs.mkdirSync(parent, { recursive: true });
4051
- fs.writeFileSync(filePath, `${JSON.stringify(value, null, 2)}
4013
+ const parent = dirname(filePath);
4014
+ mkdirSync(parent, { recursive: true });
4015
+ const tmp = join(parent, `.${basename(filePath)}.${process.pid}.${randomUUID()}.tmp`);
4016
+ try {
4017
+ writeFileSync(tmp, `${JSON.stringify(value, null, 2)}
4052
4018
  `, "utf8");
4053
- }
4054
- function resolveProjectRoot() {
4055
- const candidate = process.env.MSTAR_CLI_PROJECT_ROOT || process.env.INIT_CWD || process.env.PWD;
4056
- if (candidate && candidate.trim())
4057
- return path2.resolve(candidate);
4058
- return process.cwd();
4059
- }
4060
- function readHarnessVersion() {
4061
- const packageJsonPath = path2.resolve(path2.dirname(fileURLToPath(import.meta.url)), "../package.json");
4019
+ renameSync(tmp, filePath);
4020
+ } catch (error) {
4021
+ try {
4022
+ unlinkSync(tmp);
4023
+ } catch {}
4024
+ throw error;
4025
+ }
4026
+ }
4027
+ function resolveHarnessDir(startDir = process.cwd(), opts = {}) {
4028
+ const start = resolve2(startDir);
4029
+ const explicit = opts.harnessDir ?? process.env.MSTAR_HARNESS_DIR;
4030
+ if (explicit)
4031
+ return resolve2(start, explicit);
4032
+ let dir = start;
4033
+ for (;; ) {
4034
+ for (const candidate of [join2(dir, ".mstar"), join2(dir, ".agents"), join2(dir, ".plans"), join2(dir, "plans")]) {
4035
+ if (isDirectory(candidate))
4036
+ return candidate;
4037
+ }
4038
+ const parent = dirname2(dir);
4039
+ if (parent === dir)
4040
+ return null;
4041
+ dir = parent;
4042
+ }
4043
+ }
4044
+ function resolveSpecsDir(harnessDir, opts = {}) {
4045
+ const harness = resolve2(harnessDir);
4046
+ const repoRoot = dirname2(harness);
4047
+ const candidates = [
4048
+ join2(harness, "specs"),
4049
+ join2(repoRoot, "docs", "specs"),
4050
+ join2(repoRoot, "specs"),
4051
+ join2(harness, "designs"),
4052
+ join2(repoRoot, "designs")
4053
+ ];
4054
+ for (const candidate of candidates) {
4055
+ if (isDirectory(candidate) && hasFiles(candidate))
4056
+ return candidate;
4057
+ }
4058
+ const fallback = join2(harness, "specs");
4059
+ if (opts.create !== false)
4060
+ mkdirSync2(fallback, { recursive: true });
4061
+ return fallback;
4062
+ }
4063
+ function assertSafePathComponent(value, what) {
4064
+ if (value === "" || value === "." || value === ".." || !/^[A-Za-z0-9._-]+$/.test(value)) {
4065
+ throw new Error(`${what} must be a single safe path component ([A-Za-z0-9._-]+; not "", ".", "..", or containing "/" or "\\") — got ${JSON.stringify(value)}`);
4066
+ }
4067
+ }
4068
+ function resolveSddDir(harnessDir, planId) {
4069
+ assertSafePathComponent(planId, "planId");
4070
+ return join2(resolve2(harnessDir), "sdd", planId);
4071
+ }
4072
+ var GITIGNORE_SNIPPET = `# Morning Star harness (.mstar/)
4073
+ # Principle: process stays local; results are shared with the team.
4074
+ # Ignored (process / coordination):
4075
+ .mstar/archived/
4076
+ .mstar/iterations/
4077
+ .mstar/plans/
4078
+ .mstar/sdd/
4079
+ .mstar/notes.json
4080
+ .mstar/status.json
4081
+ # Tracked (results): .mstar/AGENTS.md, .mstar/knowledge/, .mstar/specs/
4082
+ `;
4083
+ var GITIGNORE_SNIPPET_AGENTS = `# Morning Star harness (.agents/) — legacy
4084
+ .agents/archived/
4085
+ .agents/iterations/
4086
+ .agents/plans/
4087
+ .agents/sdd/
4088
+ .agents/notes.json
4089
+ .agents/status.json
4090
+ # Tracked (results): .agents/AGENTS.md, .agents/knowledge/, .agents/specs/
4091
+ `;
4092
+ var GITIGNORE_PROCESS_ENTRIES = GITIGNORE_SNIPPET.split(`
4093
+ `).filter((line) => line.startsWith(".mstar/")).map((line) => line.trim());
4094
+ var GITIGNORE_PROCESS_ENTRIES_AGENTS = GITIGNORE_SNIPPET_AGENTS.split(`
4095
+ `).filter((line) => line.startsWith(".agents/")).map((line) => line.trim());
4096
+ function isDirectory(dir) {
4062
4097
  try {
4063
- const parsed = JSON.parse(fs.readFileSync(packageJsonPath, "utf8"));
4064
- return parsed.version || "0.0.0";
4098
+ return statSync(dir).isDirectory();
4065
4099
  } catch {
4066
- return "0.0.0";
4067
- }
4068
- }
4069
-
4070
- // src/adapters/shared-install.ts
4071
- import fs2 from "node:fs";
4072
- import os from "node:os";
4073
- import path3 from "node:path";
4074
- import { execFileSync } from "node:child_process";
4075
- var REPO_URL = "https://github.com/btspoony/mstar-harness.git";
4076
- var PLUGIN_NAME = "morning-star-harness";
4077
- var HARNESS_REPO_PATH = path3.join(os.homedir(), ".mstar", "harness");
4078
- var HARNESS_MARKERS = [
4079
- ".codex-plugin/plugin.json",
4080
- ".zcode-plugin/plugin.json",
4081
- ".omp-plugin/plugin.json"
4082
- ];
4083
- function harnessMarkerPath() {
4084
- for (const marker of HARNESS_MARKERS) {
4085
- const candidate = path3.join(HARNESS_REPO_PATH, marker);
4086
- if (fs2.existsSync(candidate))
4087
- return candidate;
4100
+ return false;
4088
4101
  }
4089
- return path3.join(HARNESS_REPO_PATH, HARNESS_MARKERS[0]);
4090
4102
  }
4091
- function pathOrSymlinkExists(filePath) {
4103
+ function hasFiles(dir) {
4092
4104
  try {
4093
- fs2.lstatSync(filePath);
4094
- return true;
4105
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
4106
+ if (entry.isDirectory()) {
4107
+ if (hasFiles(join2(dir, entry.name)))
4108
+ return true;
4109
+ } else if (entry.isFile()) {
4110
+ return true;
4111
+ }
4112
+ }
4113
+ return false;
4095
4114
  } catch {
4096
4115
  return false;
4097
4116
  }
4098
4117
  }
4099
- function ensureDir(dirPath, dryRun) {
4100
- if (dryRun)
4101
- return;
4102
- if (!fs2.existsSync(dirPath))
4103
- fs2.mkdirSync(dirPath, { recursive: true });
4104
- }
4105
- function runCommand(command, cwd, dryRun) {
4106
- if (dryRun)
4107
- return;
4108
- execFileSync(command[0], command.slice(1), { cwd, stdio: "pipe", encoding: "utf8" });
4109
- }
4110
- function ensureLocalHarnessRepo(dryRun) {
4111
- const notes = [];
4112
- if (fs2.existsSync(HARNESS_REPO_PATH)) {
4113
- const errors2 = validateLocalHarnessRepo();
4114
- if (errors2.length) {
4115
- throw new Error(errors2.join(`
4116
- `));
4118
+ var DATE_PART = String.raw`\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12]\d|3[01])`;
4119
+ var RFC3339_Z_RE = new RegExp(String.raw`^${DATE_PART}T\d{2}:\d{2}:\d{2}(\.\d+)?Z$`);
4120
+ var DATE_ONLY_RE = new RegExp(String.raw`^${DATE_PART}$`);
4121
+ var STATUS_WRITE_LOCKDIR = ".status-write.lockdir";
4122
+ var LOCKDIR_HOLDER_PID = "holder.pid";
4123
+ var heldLockDirs = new AsyncLocalStorage2;
4124
+ async function withStatusWriteLock(statusPath, fn, opts = {}) {
4125
+ const lockDir = join3(dirname3(resolve3(statusPath)), STATUS_WRITE_LOCKDIR);
4126
+ const held = heldLockDirs.getStore();
4127
+ if (held !== undefined && held.has(lockDir)) {
4128
+ throw new Error(`${lockDir} is already held by this process in this async context — withStatusWriteLock is not reentrant; a nested acquisition on the same status.json is a bug`);
4129
+ }
4130
+ const timeoutMs = opts.timeoutMs ?? 30000;
4131
+ const pollMs = opts.pollMs ?? 25;
4132
+ const deadline = Date.now() + timeoutMs;
4133
+ let acquired = null;
4134
+ for (;; ) {
4135
+ try {
4136
+ mkdirSync3(lockDir);
4137
+ const st = statSync2(lockDir);
4138
+ acquired = { dev: st.dev, ino: st.ino };
4139
+ break;
4140
+ } catch (error) {
4141
+ if (error.code !== "EEXIST")
4142
+ throw error;
4143
+ if (Date.now() >= deadline) {
4144
+ throw new Error(`${lockDir} already exists — another writer holds the status write lock; Blocked (same-host exclusive lock; status-and-residuals.md § Same-host exclusive write lock). ` + `Recovery: remove ${lockDir} if no writer is alive (holder.pid inside names the acquiring process)`);
4145
+ }
4146
+ await sleep(pollMs);
4117
4147
  }
4118
- notes.push(`Using existing local harness repo at ${HARNESS_REPO_PATH}`);
4119
- return notes;
4120
4148
  }
4121
- ensureDir(path3.dirname(HARNESS_REPO_PATH), dryRun);
4122
- runCommand(["git", "clone", REPO_URL, HARNESS_REPO_PATH], path3.dirname(HARNESS_REPO_PATH), dryRun);
4123
- notes.push(`Cloned ${REPO_URL} to ${HARNESS_REPO_PATH}`);
4124
- return notes;
4149
+ try {
4150
+ writeFileSync2(join3(lockDir, LOCKDIR_HOLDER_PID), String(process.pid), "utf8");
4151
+ } catch {}
4152
+ const owns = held ?? new Set;
4153
+ owns.add(lockDir);
4154
+ try {
4155
+ return await heldLockDirs.run(owns, fn);
4156
+ } finally {
4157
+ owns.delete(lockDir);
4158
+ try {
4159
+ const current = statSync2(lockDir);
4160
+ if (acquired !== null && current.dev === acquired.dev && current.ino === acquired.ino) {
4161
+ try {
4162
+ unlinkSync2(join3(lockDir, LOCKDIR_HOLDER_PID));
4163
+ } catch {}
4164
+ rmdirSync(lockDir);
4165
+ }
4166
+ } catch {}
4167
+ }
4125
4168
  }
4126
- function validateLocalHarnessRepo() {
4127
- const errors2 = [];
4128
- if (!fs2.existsSync(HARNESS_REPO_PATH)) {
4129
- errors2.push(`Missing local harness repo: ${HARNESS_REPO_PATH}`);
4130
- return errors2;
4169
+ var BRANCH_FORMS_HINT = '"Working branch: <existing>" | "Working branch: create <new> from <base>" | "Branch policy: direct on <branch> — <reason>"';
4170
+ var REQUIRED_FIELDS = [
4171
+ { key: "executeAs", label: "Execute as", code: "execute-as" },
4172
+ { key: "delegation", label: "Delegation", code: "delegation" },
4173
+ { key: "taskCategory", label: "Task category", code: "task-category" }
4174
+ ];
4175
+ function violation2(severity, code, message, fix) {
4176
+ return { ok: false, severity, code, message, fix };
4177
+ }
4178
+ function parseAssignmentFields(assignmentText) {
4179
+ const fields = {};
4180
+ for (const line of assignmentText.split(/\r?\n/)) {
4181
+ const match = line.match(/^[ \t]*(?:[-*][ \t]+)?\*\*\s*([^*:]+?)\s*\*\*\s*:\s*(.*)$/) ?? line.match(/^[ \t]*(?:[-*][ \t]+)?([A-Za-z][A-Za-z -]*?)\s*:\s*(.*)$/);
4182
+ if (!match)
4183
+ continue;
4184
+ const label = match[1].trim();
4185
+ const value = match[2].trim();
4186
+ const known = REQUIRED_FIELDS.find((f) => f.label === label);
4187
+ if (known) {
4188
+ fields[known.key] = value;
4189
+ continue;
4190
+ }
4191
+ if (label === "Working branch")
4192
+ fields.workingBranch = value;
4193
+ else if (label === "Branch policy")
4194
+ fields.branchPolicy = value;
4131
4195
  }
4132
- const marker = harnessMarkerPath();
4133
- if (!fs2.existsSync(marker)) {
4134
- errors2.push(`Local harness repo is missing a plugin marker (expected one of: ${HARNESS_MARKERS.join(", ")}).`);
4196
+ return fields;
4197
+ }
4198
+ function requireField(violations, value, label, code) {
4199
+ if (value === undefined) {
4200
+ const v = violation2("high", `assignment.field.missing-${code}`, `missing required Assignment field: ${label}`, `add "**${label}**: <value>" to the Assignment`);
4201
+ v.aliases = [`assignment.presence.missing-${code}`];
4202
+ violations.push(v);
4203
+ } else if (value === "") {
4204
+ const v = violation2("high", `assignment.field.invalid-${code}`, `${label} must be non-empty`, `fill in "**${label}**: <value>"`);
4205
+ v.aliases = [`assignment.presence.missing-${code}`];
4206
+ violations.push(v);
4135
4207
  }
4136
- return errors2;
4137
4208
  }
4138
- function ensureGitCheckout(repoUrl, checkoutPath, dryRun) {
4139
- const notes = [];
4140
- const parentDir = path3.dirname(checkoutPath);
4141
- if (pathOrSymlinkExists(checkoutPath)) {
4142
- const stat = fs2.lstatSync(checkoutPath);
4143
- if (stat.isSymbolicLink()) {
4144
- notes.push(dryRun ? `Would remove symlink at ${checkoutPath} and clone ${repoUrl}` : `Removed symlink at ${checkoutPath} (Cursor requires a real directory)`);
4145
- if (dryRun)
4146
- return notes;
4147
- fs2.unlinkSync(checkoutPath);
4148
- } else if (fs2.existsSync(path3.join(checkoutPath, ".git"))) {
4149
- if (!dryRun) {
4150
- runCommand(["git", "-C", checkoutPath, "pull", "--ff-only"], checkoutPath, dryRun);
4151
- }
4152
- notes.push(`Updated git checkout at ${checkoutPath}`);
4153
- return notes;
4209
+ function parseWorkingBranchValue(value) {
4210
+ if (value === "")
4211
+ return {};
4212
+ const create = value.match(/^create\s+(\S+)(?:\s+from\s+(\S+))?$/i);
4213
+ if (create)
4214
+ return { createForm: { name: create[1], base: create[2] } };
4215
+ const danglingFrom = value.match(/^create\s+(\S+)\s+from$/i);
4216
+ if (danglingFrom)
4217
+ return { createForm: { name: danglingFrom[1], base: "" } };
4218
+ const missingName = value.match(/^create\s+from\s+(\S+)$/i);
4219
+ if (missingName)
4220
+ return { createForm: { name: "", base: missingName[1] } };
4221
+ return { workingBranch: value.split(/\s+/)[0] };
4222
+ }
4223
+ function parseAssignmentBranchForms(assignmentText) {
4224
+ const fields = parseAssignmentFields(assignmentText);
4225
+ const forms = {};
4226
+ if (fields.workingBranch !== undefined && fields.workingBranch !== "") {
4227
+ const parsed = parseWorkingBranchValue(fields.workingBranch);
4228
+ if (parsed.createForm !== undefined)
4229
+ forms.createForm = parsed.createForm;
4230
+ else
4231
+ forms.workingBranch = parsed.workingBranch;
4232
+ }
4233
+ if (fields.branchPolicy !== undefined && fields.branchPolicy !== "") {
4234
+ const direct = fields.branchPolicy.match(/^direct\s+on\s+(\S+)/i);
4235
+ if (direct) {
4236
+ const strict = fields.branchPolicy.match(/^direct\s+on\s+(\S+)(?:\s*(?:[—–]|--|-)\s*(.+))?$/);
4237
+ forms.directOn = { branch: direct[1].trim(), reason: strict ? (strict[2] ?? "").trim() : "" };
4238
+ }
4239
+ }
4240
+ return forms;
4241
+ }
4242
+ function parseBranchPolicyDirectOnBranch(assignmentText) {
4243
+ const directOn = parseAssignmentBranchForms(assignmentText).directOn;
4244
+ return directOn !== undefined && directOn.reason !== "" ? directOn.branch : undefined;
4245
+ }
4246
+ function isReadOnlyAssignmentRole(roleId) {
4247
+ const role = roleId.trim().toLowerCase();
4248
+ return role === "scout" || role === "explore";
4249
+ }
4250
+ function validateAssignmentFields(assignmentText, opts = {}) {
4251
+ const violations = [];
4252
+ const fields = parseAssignmentFields(assignmentText);
4253
+ const writable = opts.writable !== false;
4254
+ for (const { key, label, code } of REQUIRED_FIELDS) {
4255
+ requireField(violations, fields[key], label, code);
4256
+ }
4257
+ if (writable) {
4258
+ const workingPresent = fields.workingBranch !== undefined && fields.workingBranch !== "";
4259
+ const policyPresent = fields.branchPolicy !== undefined && fields.branchPolicy !== "";
4260
+ const formCount = Number(workingPresent) + Number(policyPresent);
4261
+ const forms = parseAssignmentBranchForms(assignmentText);
4262
+ if (formCount === 0) {
4263
+ violations.push(violation2("high", "assignment.field.branch-missing", "writable assignment must contain exactly one branch form", `add exactly one of: ${BRANCH_FORMS_HINT}`));
4264
+ } else if (formCount > 1) {
4265
+ violations.push(violation2("high", "assignment.field.branch-multiple", `writable assignment contains ${formCount} branch forms (Working branch + Branch policy) — exactly one required`, `keep exactly one of: ${BRANCH_FORMS_HINT}`));
4266
+ } else if (workingPresent) {
4267
+ const create = forms.createForm;
4268
+ if (create !== undefined && (create.base === undefined || create.base.trim() === "" || create.name.trim() === "")) {
4269
+ violations.push(violation2("high", "assignment.field.branch-missing-base", `create-form Working branch is incomplete: "${fields.workingBranch}" (expected "create <new-branch> from <base>")`, "write both the new branch name and the ancestor branch after `from` (main / existing feature branch / remote-tracking branch / `current`)"));
4270
+ }
4271
+ } else if (policyPresent) {
4272
+ const direct = forms.directOn;
4273
+ if (direct === undefined) {
4274
+ violations.push(violation2("high", "assignment.field.branch-policy-missing-branch", `unparseable Branch policy: "${fields.branchPolicy}" (expected "direct on <branch> — <reason>")`, "start the field with `direct on <branch>`"));
4275
+ } else if (direct.reason === "") {
4276
+ violations.push(violation2("high", "assignment.field.branch-policy-missing-reason", `Branch policy "direct on ${direct.branch}" is missing the reason`, 'append "— <reason>" after the branch name'));
4277
+ }
4278
+ }
4279
+ }
4280
+ return { ok: violations.length === 0, violations };
4281
+ }
4282
+ function assertDefaultBranchProtected(branch, opts = {}) {
4283
+ const defaultBranches = opts.defaultBranches ?? ["main", "master"];
4284
+ const violations = [];
4285
+ const normalized = branch.trim();
4286
+ if (normalized !== "" && defaultBranches.includes(normalized) && opts.directOnException !== true) {
4287
+ violations.push(violation2("high", "dispatch.default-branch.protected", `writable work on default protected branch "${normalized}" requires an explicit direct-on exception`, `add "Branch policy: direct on ${normalized} — <reason>" to the Assignment, or use a feature branch`));
4288
+ }
4289
+ return { ok: violations.length === 0, violations };
4290
+ }
4291
+ function executionModeToN(executionMode, opts = {}) {
4292
+ const violations = [];
4293
+ const mode = executionMode.trim().toLowerCase().split(/\s+/)[0] ?? "";
4294
+ let n;
4295
+ if (mode === "") {
4296
+ violations.push(violation2("high", "dispatch.execution-mode.missing", "missing required Assignment field: Execution mode", 'add "**Execution mode**: sdd | inline | targeted"'));
4297
+ } else if (mode === "sdd") {
4298
+ n = 3;
4299
+ } else if (mode === "inline") {
4300
+ n = 1;
4301
+ } else if (mode === "targeted") {
4302
+ const seats = [...new Set((opts.seats ?? []).map((s) => s.trim()).filter((s) => s !== ""))];
4303
+ if (seats.length === 0) {
4304
+ violations.push(violation2("high", "dispatch.execution-mode.missing-seats", 'execution mode "targeted" requires listed reviewer seats', 'add "QC re-review: targeted — reviewers: <role-id>, …" to the Assignment and pass the seats'));
4305
+ } else if (seats.length > 3) {
4306
+ violations.push(violation2("high", "dispatch.execution-mode.too-many-seats", `execution mode "targeted" lists ${seats.length} reviewer seats — at most 3 (targeted re-review seats are the tri seats, N = 1–3)`, "list at most three reviewer seats for the targeted re-review"));
4154
4307
  } else {
4155
- throw new Error(`Path exists but is not a git checkout: ${checkoutPath}`);
4308
+ n = seats.length;
4156
4309
  }
4310
+ } else {
4311
+ violations.push(violation2("high", "dispatch.execution-mode.unknown", `unknown execution mode "${executionMode.trim()}" (expected sdd | inline | targeted)`, "fix the Execution mode field"));
4157
4312
  }
4158
- ensureDir(parentDir, dryRun);
4159
- if (!dryRun) {
4160
- runCommand(["git", "clone", repoUrl, checkoutPath], parentDir, dryRun);
4161
- }
4162
- notes.push(`${dryRun ? "Would clone" : "Cloned"} ${repoUrl} to ${checkoutPath}`);
4163
- return notes;
4313
+ return n === undefined ? { ok: false, violations } : { ok: true, violations, n };
4164
4314
  }
4165
- function validateGitCheckout(checkoutPath, markerRelativePath) {
4166
- const errors2 = [];
4167
- if (!pathOrSymlinkExists(checkoutPath)) {
4168
- errors2.push(`Missing checkout directory: ${checkoutPath}`);
4169
- return errors2;
4315
+ function assertTriIdentity(reviewerRoles) {
4316
+ const tri = ["qc-specialist", "qc-specialist-2", "qc-specialist-3"];
4317
+ const roles = reviewerRoles.map((r) => r.trim().toLowerCase()).filter((r) => r !== "");
4318
+ const valid = roles.length === tri.length && new Set(roles).size === tri.length && roles.every((r) => tri.includes(r));
4319
+ if (valid)
4320
+ return { ok: true, violations: [] };
4321
+ const got = roles.length > 0 ? roles.join(", ") : "(none)";
4322
+ return {
4323
+ ok: false,
4324
+ violations: [
4325
+ violation2("high", "dispatch.tri-identity.invalid", `tri-review initial wave must be exactly qc-specialist / qc-specialist-2 / qc-specialist-3, got: ${got}`, "dispatch qc-specialist, qc-specialist-2 and qc-specialist-3 for the initial wave")
4326
+ ]
4327
+ };
4328
+ }
4329
+ var DATE_RE = /^\d{4}-\d{2}-\d{2}$/;
4330
+ var PLAN_STATUSES = ["Todo", "InProgress", "InReview", "Blocked", "Done"];
4331
+ var RESIDUAL_DECISIONS = ["defer", "accept", "risk-accepted"];
4332
+ var RESIDUAL_LIFECYCLES = ["open", "resolved", "waived", "superseded", "duplicate"];
4333
+ function isPlainObject2(value) {
4334
+ return typeof value === "object" && value !== null && !Array.isArray(value);
4335
+ }
4336
+ function violation3(severity, code, message, fix) {
4337
+ return { ok: false, severity, code, message, fix };
4338
+ }
4339
+ function todayString() {
4340
+ const now = new Date;
4341
+ const month = String(now.getMonth() + 1).padStart(2, "0");
4342
+ const day = String(now.getDate()).padStart(2, "0");
4343
+ return `${now.getFullYear()}-${month}-${day}`;
4344
+ }
4345
+ function isOpenResidual(entry) {
4346
+ const lifecycle = entry.lifecycle;
4347
+ const effective = lifecycle === false || lifecycle === null || lifecycle === undefined ? "open" : lifecycle;
4348
+ return effective === "open";
4349
+ }
4350
+ function validateNonEmptyString2(violations, value, field, missingCode, invalidCode) {
4351
+ if (value === undefined) {
4352
+ violations.push(violation3("high", missingCode, `missing required field: ${field}`));
4353
+ } else if (typeof value !== "string" || value.trim() === "") {
4354
+ violations.push(violation3("medium", invalidCode, `${field} must be a non-empty string`));
4355
+ }
4356
+ }
4357
+ function validatePlanRow(row) {
4358
+ const violations = [];
4359
+ if (!isPlainObject2(row)) {
4360
+ return { ok: false, violations: [violation3("high", "status.plan-row.invalid", "plan row must be an object")] };
4361
+ }
4362
+ const { id, plan_id: planId, title, file, status, metadata, execution_lease } = row;
4363
+ if (id === undefined && planId === undefined) {
4364
+ violations.push(violation3("high", "status.plan-row.missing-id", "missing required field: id (or legacy plan_id)"));
4365
+ } else {
4366
+ if (id !== undefined) {
4367
+ validateNonEmptyString2(violations, id, "id", "status.plan-row.missing-id", "status.plan-row.invalid-id");
4368
+ }
4369
+ if (planId !== undefined) {
4370
+ validateNonEmptyString2(violations, planId, "plan_id", "status.plan-row.missing-plan-id", "status.plan-row.invalid-plan-id");
4371
+ }
4372
+ if (id !== undefined && planId !== undefined && id !== planId) {
4373
+ violations.push(violation3("medium", "status.plan-row.dual-id", "row has both id and plan_id with different values — write one canonical key (prefer id)"));
4374
+ }
4170
4375
  }
4171
- const stat = fs2.lstatSync(checkoutPath);
4172
- if (stat.isSymbolicLink()) {
4173
- errors2.push(`Path must be a real directory, not a symlink: ${checkoutPath}. Run: mstar-harness init --target cursor`);
4174
- return errors2;
4376
+ validateNonEmptyString2(violations, title, "title", "status.plan-row.missing-title", "status.plan-row.invalid-title");
4377
+ validateNonEmptyString2(violations, file, "file", "status.plan-row.missing-file", "status.plan-row.invalid-file");
4378
+ if (status === undefined) {
4379
+ violations.push(violation3("high", "status.plan-row.missing-status", "missing required field: status"));
4380
+ } else if (typeof status !== "string" || !PLAN_STATUSES.includes(status)) {
4381
+ violations.push(violation3("medium", "status.plan-row.invalid-status", `status must be one of ${PLAN_STATUSES.join(" | ")} — got ${JSON.stringify(status)}`));
4175
4382
  }
4176
- if (!fs2.existsSync(path3.join(checkoutPath, ".git"))) {
4177
- errors2.push(`Path is not a git checkout: ${checkoutPath}`);
4383
+ if (metadata !== undefined && !isPlainObject2(metadata)) {
4384
+ violations.push(violation3("medium", "status.plan-row.invalid-metadata", "metadata must be an object"));
4178
4385
  }
4179
- const marker = path3.join(checkoutPath, markerRelativePath);
4180
- if (!fs2.existsSync(marker)) {
4181
- errors2.push(`Missing marker file: ${marker}`);
4386
+ if (execution_lease !== undefined && !isPlainObject2(execution_lease)) {
4387
+ violations.push(violation3("medium", "status.plan-row.invalid-execution-lease", "execution_lease must be an object"));
4182
4388
  }
4183
- return errors2;
4389
+ if (status === "Done" && execution_lease !== undefined) {
4390
+ violations.push(violation3("medium", "status.plan-row.done-with-lease", 'plan status Done must not carry an execution_lease — the Done authority deletes the lease in the same complete-file update as status: "Done" (status-and-residuals.md § Hold, release, and override)', 'delete plans[].execution_lease in the same update that sets status: "Done"'));
4391
+ }
4392
+ return { ok: violations.length === 0, violations };
4184
4393
  }
4185
- function ensureSymlink(target, linkPath, dryRun) {
4186
- if (pathOrSymlinkExists(linkPath)) {
4187
- const stat = fs2.lstatSync(linkPath);
4188
- if (!stat.isSymbolicLink()) {
4189
- throw new Error(`Path exists and is not a symlink: ${linkPath}`);
4394
+ function validateResidual(entry) {
4395
+ const violations = [];
4396
+ if (!isPlainObject2(entry)) {
4397
+ return { ok: false, violations: [violation3("high", "status.residual.invalid", "residual entry must be an object")] };
4398
+ }
4399
+ const { id, title, severity, source, scope, decision, owner, target, tracking, detail_doc, lifecycle, closed_at } = entry;
4400
+ validateNonEmptyString2(violations, id, "id", "status.residual.missing-id", "status.residual.invalid-id");
4401
+ validateNonEmptyString2(violations, title, "title", "status.residual.missing-title", "status.residual.invalid-title");
4402
+ validateNonEmptyString2(violations, source, "source", "status.residual.missing-source", "status.residual.invalid-source");
4403
+ validateNonEmptyString2(violations, scope, "scope", "status.residual.missing-scope", "status.residual.invalid-scope");
4404
+ validateNonEmptyString2(violations, owner, "owner", "status.residual.missing-owner", "status.residual.invalid-owner");
4405
+ if (severity === undefined) {
4406
+ violations.push(violation3("high", "status.residual.missing-severity", "missing required field: severity"));
4407
+ } else if (typeof severity !== "string" || !SEVERITY_ORDER.includes(severity) && severity !== "warning") {
4408
+ violations.push(violation3("medium", "status.residual.invalid-severity", `severity must be one of ${SEVERITY_ORDER.join(" | ")} — got ${JSON.stringify(severity)}`));
4409
+ } else if (severity === "warning") {
4410
+ violations.push(violation3("low", "status.residual.legacy-warning", `severity "warning" is legacy — forbidden on new entries; read paths normalize it to "low"`, `use "low" (normalizeSeverity maps 'warning' → 'low')`));
4411
+ }
4412
+ if (decision === undefined) {
4413
+ violations.push(violation3("high", "status.residual.missing-decision", "missing required field: decision"));
4414
+ } else if (typeof decision !== "string" || !RESIDUAL_DECISIONS.includes(decision)) {
4415
+ violations.push(violation3("medium", "status.residual.invalid-decision", `decision must be one of ${RESIDUAL_DECISIONS.join(" | ")} — got ${JSON.stringify(decision)}`));
4416
+ }
4417
+ if (target === undefined) {
4418
+ violations.push(violation3("high", "status.residual.missing-target", "missing required field: target"));
4419
+ } else if (typeof target !== "string" && target !== null) {
4420
+ violations.push(violation3("medium", "status.residual.invalid-target", "target must be a string or null"));
4421
+ }
4422
+ if (tracking === undefined) {
4423
+ violations.push(violation3("high", "status.residual.missing-tracking", "missing required field: tracking"));
4424
+ } else if (typeof tracking !== "string" && tracking !== null) {
4425
+ violations.push(violation3("medium", "status.residual.invalid-tracking", "tracking must be a string or null"));
4426
+ }
4427
+ if (detail_doc !== undefined && typeof detail_doc !== "string" && detail_doc !== null) {
4428
+ violations.push(violation3("medium", "status.residual.invalid-detail-doc", "detail_doc must be a string or null"));
4429
+ }
4430
+ if (closed_at !== undefined && (typeof closed_at !== "string" || !DATE_RE.test(closed_at))) {
4431
+ violations.push(violation3("medium", "status.residual.invalid-closed-at", "closed_at must be YYYY-MM-DD"));
4432
+ }
4433
+ if (lifecycle !== undefined) {
4434
+ if (typeof lifecycle !== "string" || !RESIDUAL_LIFECYCLES.includes(lifecycle)) {
4435
+ violations.push(violation3("medium", "status.residual.invalid-lifecycle", `lifecycle must be one of ${RESIDUAL_LIFECYCLES.join(" | ")} — got ${JSON.stringify(lifecycle)}`));
4436
+ } else if (lifecycle !== "open") {
4437
+ if (closed_at === undefined) {
4438
+ violations.push(violation3("high", "status.residual.closed-missing-closed-at", `lifecycle "${lifecycle}" requires closed_at (YYYY-MM-DD)`, 'set closed_at (e.g. "2026-08-08")'));
4439
+ }
4440
+ if (entry.closure_note === undefined) {
4441
+ violations.push(violation3("medium", "status.residual.closed-missing-closure-note", `lifecycle "${lifecycle}" requires closure_note (what changed; how verified)`, "add closure_note explaining the close"));
4442
+ }
4190
4443
  }
4191
- if (!fs2.existsSync(target)) {
4192
- throw new Error(`Symlink target is missing: ${target}`);
4444
+ }
4445
+ return { ok: violations.length === 0, violations };
4446
+ }
4447
+ function validateStatus(docOrPath) {
4448
+ let doc;
4449
+ if (typeof docOrPath === "string") {
4450
+ try {
4451
+ doc = readJson(docOrPath);
4452
+ } catch (error) {
4453
+ return {
4454
+ ok: false,
4455
+ violations: [violation3("high", "status.invalid-json", error.message)]
4456
+ };
4193
4457
  }
4194
- const actual = fs2.realpathSync(linkPath);
4195
- const expected = fs2.realpathSync(target);
4196
- if (actual !== expected) {
4197
- throw new Error(`Symlink ${linkPath} points to ${actual}, expected ${expected}`);
4458
+ } else {
4459
+ doc = docOrPath;
4460
+ }
4461
+ const violations = [];
4462
+ const { version, updated_at, plans, residual_findings, metadata } = doc;
4463
+ if (version === undefined) {
4464
+ violations.push(violation3("high", "status.missing-version", "missing required field: version"));
4465
+ } else if (typeof version !== "number" || !Number.isInteger(version)) {
4466
+ violations.push(violation3("high", "status.invalid-version", "version must be an integer"));
4467
+ } else if (version !== 1) {
4468
+ violations.push(violation3("medium", "status.unsupported-version", `unsupported status.json schema version ${version} — expected 1`));
4469
+ }
4470
+ if (updated_at === undefined) {
4471
+ violations.push(violation3("high", "status.missing-updated-at", "missing required field: updated_at"));
4472
+ } else if (typeof updated_at !== "string" || !DATE_RE.test(updated_at)) {
4473
+ violations.push(violation3("medium", "status.invalid-updated-at", "updated_at must be YYYY-MM-DD"));
4474
+ }
4475
+ if (plans === undefined) {
4476
+ violations.push(violation3("high", "status.missing-plans", "missing required field: plans"));
4477
+ } else if (!Array.isArray(plans)) {
4478
+ violations.push(violation3("high", "status.invalid-plans", "plans must be an array"));
4479
+ } else {
4480
+ for (const row of plans) {
4481
+ violations.push(...validatePlanRow(row).violations);
4198
4482
  }
4199
- return `Symlink already exists: ${linkPath} -> ${target}`;
4200
4483
  }
4201
- ensureDir(path3.dirname(linkPath), dryRun);
4202
- if (!dryRun)
4203
- fs2.symlinkSync(target, linkPath);
4204
- return `Linked ${linkPath} -> ${target}`;
4205
- }
4206
- function validateSymlink(target, linkPath) {
4207
- const errors2 = [];
4208
- if (!pathOrSymlinkExists(linkPath)) {
4209
- errors2.push(`Missing symlink: ${linkPath}`);
4210
- return errors2;
4484
+ if (residual_findings === undefined) {
4485
+ violations.push(violation3("high", "status.missing-residual-findings", "missing required field: residual_findings (root-only canonical)"));
4486
+ } else if (!isPlainObject2(residual_findings)) {
4487
+ violations.push(violation3("high", "status.invalid-residual-findings", "residual_findings must be an object at root"));
4488
+ } else {
4489
+ for (const [planId, list] of Object.entries(residual_findings)) {
4490
+ if (!Array.isArray(list)) {
4491
+ violations.push(violation3("high", "status.residual.invalid-list", `residual_findings["${planId}"] must be an array`));
4492
+ } else if (list.length === 0) {
4493
+ violations.push(violation3("low", "status.residual.empty-key", `residual_findings["${planId}"] is empty — delete the key (no "plan-id": [])`));
4494
+ } else {
4495
+ for (const entry of list) {
4496
+ violations.push(...validateResidual(entry).violations);
4497
+ }
4498
+ }
4499
+ }
4211
4500
  }
4212
- const stat = fs2.lstatSync(linkPath);
4213
- if (!stat.isSymbolicLink()) {
4214
- errors2.push(`Path exists but is not a symlink: ${linkPath}`);
4215
- return errors2;
4501
+ if (metadata === undefined) {
4502
+ violations.push(violation3("high", "status.missing-metadata", "missing required field: metadata"));
4503
+ } else if (!isPlainObject2(metadata)) {
4504
+ violations.push(violation3("high", "status.invalid-metadata", "metadata must be an object"));
4505
+ } else if (Object.prototype.hasOwnProperty.call(metadata, "residual_findings")) {
4506
+ violations.push(violation3("medium", "status.dual-write-residuals", "residual_findings must be root-only — metadata.residual_findings is legacy read-only; remove it (no dual-write)", "move entries to root residual_findings and delete metadata.residual_findings"));
4216
4507
  }
4217
- if (!fs2.existsSync(target)) {
4218
- errors2.push(`Symlink target is missing: ${target}`);
4219
- return errors2;
4508
+ return { ok: violations.length === 0, violations };
4509
+ }
4510
+ async function archiveResiduals(planId, harnessDir) {
4511
+ const dir = harnessDir !== undefined ? resolve4(harnessDir) : resolveHarnessDir();
4512
+ if (dir === null) {
4513
+ throw new Error(`harness dir not found from ${process.cwd()} — pass harnessDir or set MSTAR_HARNESS_DIR`);
4220
4514
  }
4221
- const actual = fs2.realpathSync(linkPath);
4222
- const expected = fs2.realpathSync(target);
4223
- if (actual !== expected) {
4224
- errors2.push(`Symlink ${linkPath} points to ${actual}, expected ${expected}`);
4515
+ assertSafePathComponent(planId, "planId");
4516
+ const statusPath = join4(dir, "status.json");
4517
+ if (!existsSync2(statusPath)) {
4518
+ throw new Error(`status file not found: ${statusPath}`);
4225
4519
  }
4226
- return errors2;
4520
+ return withStatusWriteLock(statusPath, () => {
4521
+ const doc = readJson(statusPath);
4522
+ if (!isPlainObject2(doc.residual_findings)) {
4523
+ throw new Error(`status.json residual_findings must be an object: ${statusPath}`);
4524
+ }
4525
+ const open = doc.residual_findings[planId];
4526
+ const archivePath = join4(dir, "archived", "residuals", `${planId}.json`);
4527
+ if (!Array.isArray(open) || open.length === 0) {
4528
+ return { planId, archived: 0, archivePath };
4529
+ }
4530
+ const archive = readJson(archivePath);
4531
+ const existing = Array.isArray(archive.entries) ? archive.entries : [];
4532
+ const existingIds = new Set(existing.map((e) => isPlainObject2(e) && typeof e.id === "string" ? e.id : undefined).filter((id) => id !== undefined));
4533
+ const today = todayString();
4534
+ const moved = open.filter((entry) => {
4535
+ if (!isPlainObject2(entry) || typeof entry.id !== "string")
4536
+ return true;
4537
+ return !existingIds.has(entry.id);
4538
+ }).map((entry) => ({ ...entry, archived_at: today }));
4539
+ if (moved.length > 0) {
4540
+ writeJson(archivePath, { plan_id: planId, schema_version: 1, entries: [...existing, ...moved] });
4541
+ }
4542
+ delete doc.residual_findings[planId];
4543
+ doc.updated_at = today;
4544
+ writeJson(statusPath, doc);
4545
+ return { planId, archived: moved.length, archivePath };
4546
+ });
4227
4547
  }
4228
- var HARNESS_PROCESS_GITIGNORE = [
4548
+ var DEFAULT_PROBE_TIMEOUT_MS = 1e4;
4549
+ function probeTimeoutMs() {
4550
+ const raw = process.env.MSTAR_GIT_PROBE_TIMEOUT_MS;
4551
+ if (raw === undefined || raw.trim() === "")
4552
+ return DEFAULT_PROBE_TIMEOUT_MS;
4553
+ const parsed = Number(raw);
4554
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : DEFAULT_PROBE_TIMEOUT_MS;
4555
+ }
4556
+ function violation4(severity, code, message, fix) {
4557
+ return { ok: false, severity, code, message, fix };
4558
+ }
4559
+ function gate(violations) {
4560
+ return { ok: violations.length === 0, violations };
4561
+ }
4562
+ function probeBranch(worktreePath, opts) {
4563
+ const precomputed = opts.branchOf?.(worktreePath);
4564
+ if (precomputed !== undefined)
4565
+ return { branch: precomputed };
4566
+ const timeout = opts.timeoutMs ?? probeTimeoutMs();
4567
+ try {
4568
+ const stdout = execFileSync(opts.gitPath ?? "git", ["-C", worktreePath, "branch", "--show-current"], {
4569
+ encoding: "utf8",
4570
+ stdio: ["ignore", "pipe", "pipe"],
4571
+ timeout
4572
+ });
4573
+ const branch = stdout.trim();
4574
+ if (branch === "")
4575
+ return { error: `no branch checked out (detached HEAD?) at "${worktreePath}"` };
4576
+ return { branch };
4577
+ } catch (err) {
4578
+ const e = err;
4579
+ if (e.killed === true || e.signal !== undefined) {
4580
+ return { error: `git probe timed out after ${timeout}ms (killed by ${e.signal ?? "SIGTERM"})` };
4581
+ }
4582
+ const detail = (e.stderr !== undefined ? e.stderr.toString().trim() : "") || e.message || "git probe failed";
4583
+ return { error: detail };
4584
+ }
4585
+ }
4586
+ function l1PreDispatchCheck(input, opts = {}) {
4587
+ const violations = [];
4588
+ const { controlWorktreePath, leaseWorktreePath, leaseWorkingBranch, planId } = input;
4589
+ if (controlWorktreePath.trim() === "") {
4590
+ violations.push(violation4("high", "worktree.l1.control-missing", "metadata.control_worktree_path is not recorded — the L1 control worktree (integration-branch checkout) must be recorded in status.json before writable dispatch", "record the control worktree path in status.json metadata.control_worktree_path"));
4591
+ }
4592
+ if (leaseWorktreePath.trim() === "") {
4593
+ violations.push(violation4("high", "worktree.l1.lease-missing", `execution_lease.worktree_path is empty for plan "${planId}" — no verified execution_lease to dispatch against`, "claim the execution_lease with an absolute feature worktree path before dispatch"));
4594
+ }
4595
+ if (leaseWorkingBranch.trim() === "") {
4596
+ violations.push(violation4("high", "worktree.l1.lease-branch-missing", `execution_lease.working_branch is empty for plan "${planId}"`, "record the lease working_branch before dispatch"));
4597
+ }
4598
+ if (controlWorktreePath !== "" && leaseWorktreePath !== "" && resolve5(controlWorktreePath) === resolve5(leaseWorktreePath)) {
4599
+ violations.push(violation4("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"));
4600
+ }
4601
+ if (leaseWorktreePath !== "" && !existsSync3(leaseWorktreePath)) {
4602
+ violations.push(violation4("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>`));
4603
+ } else if (leaseWorktreePath !== "" && leaseWorkingBranch !== "") {
4604
+ const probe = probeBranch(leaseWorktreePath, opts);
4605
+ if ("error" in probe) {
4606
+ violations.push(violation4("high", "worktree.l1.branch-probe-failed", `cannot probe branch at "${leaseWorktreePath}" for plan "${planId}": ${probe.error}`, "verify the path is a git worktree checkout on the lease working branch (not detached)"));
4607
+ } else if (probe.branch !== leaseWorkingBranch) {
4608
+ violations.push(violation4("high", "worktree.l1.branch-mismatch", `feature worktree "${leaseWorktreePath}" is on branch "${probe.branch}", expected execution_lease.working_branch "${leaseWorkingBranch}" (plan "${planId}")`, `checkout ${leaseWorkingBranch} in the feature worktree`));
4609
+ }
4610
+ }
4611
+ return gate(violations);
4612
+ }
4613
+ function l2PreDispatchCheck(input, opts = {}) {
4614
+ const violations = [];
4615
+ const tracks = input.tracks ?? [];
4616
+ const seenPaths = new Set;
4617
+ if (tracks.length < 1) {
4618
+ violations.push(violation4("high", "worktree.l2.no-tracks", "no parallel writable tracks — the L2 pre-dispatch checklist requires at least one track with an absolute worktreePath and Working branch", "pass each track's absolute Worktree path and PM-approved Working branch"));
4619
+ }
4620
+ tracks.forEach((track, index) => {
4621
+ if (track.worktreePath.trim() === "" || track.workingBranch.trim() === "") {
4622
+ violations.push(violation4("high", "worktree.l2.track-invalid", `track ${index + 1} is missing worktreePath and/or workingBranch`, "fill both fields for every track"));
4623
+ return;
4624
+ }
4625
+ if (!isAbsolute3(track.worktreePath)) {
4626
+ violations.push(violation4("high", "worktree.l2.track-path-relative", `track ${index + 1} worktreePath "${track.worktreePath}" is not an absolute path — L2 tracks MUST use absolute worktree checkout paths (consistent with the lease validator's absolute worktree_path enforcement)`, `use an absolute path for track ${index + 1} (e.g. /Users/<you>/worktrees/<branch>)`));
4627
+ return;
4628
+ }
4629
+ const normalized = resolve5(track.worktreePath);
4630
+ if (seenPaths.has(normalized)) {
4631
+ violations.push(violation4("high", "worktree.l2.track-path-collision", `duplicate worktreePath "${track.worktreePath}" across parallel tracks — L2 parallel-writable isolation requires a distinct absolute Worktree path per track (N parallel invokes ≠ isolation)`, "give every parallel track its own git worktree checkout"));
4632
+ return;
4633
+ }
4634
+ seenPaths.add(normalized);
4635
+ if (!existsSync3(track.worktreePath)) {
4636
+ violations.push(violation4("high", "worktree.l2.track-missing", `track worktree directory "${track.worktreePath}" does not exist`, `create it before dispatch: git worktree add ${track.worktreePath} ${track.workingBranch}`));
4637
+ return;
4638
+ }
4639
+ const probe = probeBranch(track.worktreePath, opts);
4640
+ if ("error" in probe) {
4641
+ violations.push(violation4("high", "worktree.l2.branch-probe-failed", `cannot probe branch at "${track.worktreePath}": ${probe.error}`, "verify the path is a git worktree checkout on its Working branch (not detached)"));
4642
+ } else if (probe.branch !== track.workingBranch) {
4643
+ violations.push(violation4("high", "worktree.l2.branch-mismatch", `track worktree "${track.worktreePath}" is on branch "${probe.branch}", expected Working branch "${track.workingBranch}"`, `checkout ${track.workingBranch} in that worktree`));
4644
+ }
4645
+ });
4646
+ return gate(violations);
4647
+ }
4648
+ class SddScriptError extends Error {
4649
+ exitCode;
4650
+ constructor(message, exitCode) {
4651
+ super(message);
4652
+ this.name = "SddScriptError";
4653
+ this.exitCode = exitCode;
4654
+ }
4655
+ }
4656
+ function isDirectory2(dir) {
4657
+ try {
4658
+ return statSync3(dir).isDirectory();
4659
+ } catch {
4660
+ return false;
4661
+ }
4662
+ }
4663
+ function isFile(file) {
4664
+ try {
4665
+ return statSync3(file).isFile();
4666
+ } catch {
4667
+ return false;
4668
+ }
4669
+ }
4670
+ var GIT_CAPTURE_MAX_BYTES = 64 * 1024 * 1024;
4671
+ function gitOut(cwd, args) {
4672
+ try {
4673
+ return execFileSync2("git", args, {
4674
+ cwd,
4675
+ encoding: "utf8",
4676
+ stdio: ["ignore", "pipe", "pipe"],
4677
+ maxBuffer: GIT_CAPTURE_MAX_BYTES
4678
+ }).trim();
4679
+ } catch {
4680
+ return null;
4681
+ }
4682
+ }
4683
+ function probeHarnessWithStatus(root) {
4684
+ if (isFile(join5(root, ".mstar", "status.json")))
4685
+ return join5(root, ".mstar");
4686
+ if (isFile(join5(root, ".agents", "status.json")))
4687
+ return join5(root, ".agents");
4688
+ return null;
4689
+ }
4690
+ function isLinkedWorktree(root) {
4691
+ const gitDirRaw = gitOut(root, ["rev-parse", "--git-dir"]);
4692
+ const commonRaw = gitOut(root, ["rev-parse", "--git-common-dir"]);
4693
+ if (gitDirRaw === null || commonRaw === null)
4694
+ return false;
4695
+ const gitDir = isAbsolute4(gitDirRaw) ? gitDirRaw : join5(root, gitDirRaw);
4696
+ const common2 = isAbsolute4(commonRaw) ? commonRaw : join5(root, commonRaw);
4697
+ if (gitDir.includes("/.git/worktrees/") || gitDir.includes("/worktrees/"))
4698
+ return true;
4699
+ try {
4700
+ const gdParent = realpathSync(dirname4(gitDir));
4701
+ const cmAbs = realpathSync(common2);
4702
+ return join5(gdParent, basename3(gitDir)) !== cmAbs && gitDir !== cmAbs;
4703
+ } catch {
4704
+ return false;
4705
+ }
4706
+ }
4707
+ function sddWorkspace(planId, opts = {}) {
4708
+ if (!planId) {
4709
+ throw new SddScriptError(`usage: mstar sdd workspace PLAN_ID [CONTROL_ROOT]
4710
+ ` + " Set MSTAR_CONTROL_ROOT=<control_worktree_path> when running from a feature worktree.", 2);
4711
+ }
4712
+ const cwd = opts.cwd ?? process.cwd();
4713
+ const controlRoot = opts.controlRoot ?? (process.env.MSTAR_CONTROL_ROOT || undefined);
4714
+ let root;
4715
+ if (controlRoot) {
4716
+ if (!isDirectory2(controlRoot)) {
4717
+ throw new SddScriptError(`mstar sdd workspace: CONTROL_ROOT / MSTAR_CONTROL_ROOT is not a directory: ${controlRoot}`, 1);
4718
+ }
4719
+ root = realpathSync(controlRoot);
4720
+ } else {
4721
+ const topLevel = gitOut(cwd, ["rev-parse", "--show-toplevel"]);
4722
+ root = realpathSync(topLevel ?? cwd);
4723
+ }
4724
+ if (!controlRoot && isLinkedWorktree(root)) {
4725
+ throw new SddScriptError(`mstar sdd workspace: linked worktree at ${root} has no {HARNESS_DIR}/status.json (default gitignore).
4726
+ ` + ` Refusing to create a second SDD tree under the feature checkout.
4727
+ ` + ` Re-run with MSTAR_CONTROL_ROOT=<control_worktree_path> or: mstar sdd workspace ${planId} <control_worktree_path>
4728
+ ` + ` See mstar-branch-worktree «Harness path SSOT under default gitignore».`, 1);
4729
+ }
4730
+ const harnessOverride = opts.harnessDir ?? (process.env.MSTAR_HARNESS_DIR || undefined);
4731
+ let harnessDir;
4732
+ if (harnessOverride) {
4733
+ harnessDir = resolve6(root, harnessOverride);
4734
+ } else {
4735
+ const probed = probeHarnessWithStatus(root);
4736
+ if (probed) {
4737
+ harnessDir = probed;
4738
+ } else if (isDirectory2(join5(root, ".mstar"))) {
4739
+ harnessDir = join5(root, ".mstar");
4740
+ } else if (isDirectory2(join5(root, ".agents"))) {
4741
+ harnessDir = join5(root, ".agents");
4742
+ } else {
4743
+ harnessDir = join5(root, ".mstar");
4744
+ }
4745
+ }
4746
+ const sddDir = resolveSddDir(harnessDir, planId);
4747
+ mkdirSync4(sddDir, { recursive: true });
4748
+ writeFileSync3(join5(sddDir, ".gitignore"), `*
4749
+ `);
4750
+ return realpathSync(sddDir);
4751
+ }
4752
+ function taskBrief(planFile, taskN, outFile, opts = {}) {
4753
+ if (!planFile || !Number.isInteger(taskN) || taskN < 1) {
4754
+ throw new SddScriptError("usage: mstar sdd task-brief PLAN_FILE TASK_NUMBER [OUTFILE]", 2);
4755
+ }
4756
+ let content;
4757
+ try {
4758
+ content = readFileSync4(planFile, "utf8");
4759
+ } catch {
4760
+ throw new SddScriptError(`no such plan file: ${planFile}`, 2);
4761
+ }
4762
+ let out;
4763
+ if (outFile) {
4764
+ out = outFile;
4765
+ } else {
4766
+ const sddDir = opts.sddDir ?? process.env.SDD_DIR;
4767
+ if (!sddDir) {
4768
+ throw new SddScriptError("mstar sdd task-brief: set SDD_DIR or pass OUTFILE (run mstar sdd workspace PLAN_ID first)", 2);
4769
+ }
4770
+ mkdirSync4(sddDir, { recursive: true });
4771
+ out = join5(sddDir, `task-${taskN}-brief.md`);
4772
+ }
4773
+ const records = content.endsWith(`
4774
+ `) ? content.split(`
4775
+ `).slice(0, -1) : content.split(`
4776
+ `);
4777
+ const headingRe = /^#+[ \t]+Task[ \t]+[0-9]+/;
4778
+ const targetRe = new RegExp(`^#+[ ]+Task[ ]+${taskN}([^0-9]|$)`);
4779
+ let infence = false;
4780
+ let intask = false;
4781
+ const printed = [];
4782
+ for (const line of records) {
4783
+ if (/^```/.test(line))
4784
+ infence = !infence;
4785
+ if (!infence && headingRe.test(line))
4786
+ intask = targetRe.test(line);
4787
+ if (intask)
4788
+ printed.push(line);
4789
+ }
4790
+ const output = printed.length > 0 ? `${printed.join(`
4791
+ `)}
4792
+ ` : "";
4793
+ writeFileSync3(out, output);
4794
+ if (printed.length === 0) {
4795
+ throw new SddScriptError(`task ${taskN} not found in ${planFile} (no heading matching Task ${taskN})`, 3);
4796
+ }
4797
+ return out;
4798
+ }
4799
+ function reviewPackage(base, head, outFile, opts = {}) {
4800
+ if (!base || !head) {
4801
+ throw new SddScriptError("usage: mstar sdd review-package BASE HEAD [OUTFILE]", 2);
4802
+ }
4803
+ const cwd = opts.cwd ?? process.cwd();
4804
+ const verifyRef = (ref, what) => {
4805
+ try {
4806
+ execFileSync2("git", ["rev-parse", "--verify", "--quiet", ref], { cwd, stdio: ["ignore", "pipe", "pipe"] });
4807
+ } catch {
4808
+ throw new SddScriptError(`bad ${what}: ${ref}`, 2);
4809
+ }
4810
+ };
4811
+ verifyRef(base, "BASE");
4812
+ verifyRef(head, "HEAD");
4813
+ let out;
4814
+ if (outFile) {
4815
+ out = outFile;
4816
+ } else {
4817
+ const sddDir = opts.sddDir ?? process.env.SDD_DIR;
4818
+ if (!sddDir) {
4819
+ throw new SddScriptError("mstar sdd review-package: set SDD_DIR or pass OUTFILE", 2);
4820
+ }
4821
+ mkdirSync4(sddDir, { recursive: true });
4822
+ const shortBase = gitOut(cwd, ["rev-parse", "--short", base]) ?? base;
4823
+ const shortHead = gitOut(cwd, ["rev-parse", "--short", head]) ?? head;
4824
+ out = join5(sddDir, `review-${shortBase}..${shortHead}.diff`);
4825
+ }
4826
+ const run = (args) => execFileSync2("git", args, { cwd, maxBuffer: GIT_CAPTURE_MAX_BYTES });
4827
+ const parts = [
4828
+ Buffer.from(`# Review package: ${base}..${head}
4829
+
4830
+ ## Commits
4831
+ `),
4832
+ run(["log", "--oneline", `${base}..${head}`]),
4833
+ Buffer.from(`
4834
+ ## Files changed
4835
+ `),
4836
+ run(["diff", "--stat", `${base}..${head}`]),
4837
+ Buffer.from(`
4838
+ ## Diff
4839
+ `),
4840
+ run(["diff", "-U10", `${base}..${head}`])
4841
+ ];
4842
+ writeFileSync3(out, Buffer.concat(parts));
4843
+ return out;
4844
+ }
4845
+ var COMPASS_STATUSES = ["active", "locked", "completed"];
4846
+ var DATE_RE2 = /^\d{4}-\d{2}-\d{2}$/;
4847
+ var PLAN_STATUS_DONE = "Done";
4848
+ function typeName(value) {
4849
+ if (value === null)
4850
+ return "null";
4851
+ if (Array.isArray(value))
4852
+ return "array";
4853
+ return typeof value;
4854
+ }
4855
+ function validateCompassShape(doc) {
4856
+ const issues = [];
4857
+ const expectString = (key, opts = {}) => {
4858
+ const value = doc[key];
4859
+ if (typeof value !== "string") {
4860
+ issues.push({ path: [key], message: `expected string, received ${typeName(value)}` });
4861
+ return;
4862
+ }
4863
+ if (opts.min !== undefined && value.length < opts.min) {
4864
+ issues.push({ path: [key], message: `string must contain at least ${opts.min} character(s)` });
4865
+ return;
4866
+ }
4867
+ if (opts.regex !== undefined && !opts.regex.test(value)) {
4868
+ issues.push({ path: [key], message: `string must match ${opts.regex}` });
4869
+ }
4870
+ };
4871
+ expectString("iteration_id", { min: 1 });
4872
+ expectString("start_date", { regex: DATE_RE2 });
4873
+ const status = doc.status;
4874
+ if (typeof status !== "string" || !COMPASS_STATUSES.includes(status)) {
4875
+ issues.push({
4876
+ path: ["status"],
4877
+ message: `expected one of ${COMPASS_STATUSES.map((s) => `'${s}'`).join(" | ")}, received ${typeName(status)}`
4878
+ });
4879
+ }
4880
+ expectString("iteration_base_branch", { min: 1 });
4881
+ expectString("target_branch", { min: 1 });
4882
+ const plans = doc.plans;
4883
+ if (plans !== undefined) {
4884
+ if (!Array.isArray(plans)) {
4885
+ issues.push({ path: ["plans"], message: `expected array, received ${typeName(plans)}` });
4886
+ } else {
4887
+ plans.forEach((entry, index) => {
4888
+ if (typeof entry !== "string") {
4889
+ issues.push({ path: ["plans", index], message: `expected string, received ${typeName(entry)}` });
4890
+ } else if (entry.length < 1) {
4891
+ issues.push({ path: ["plans", index], message: "string must contain at least 1 character(s)" });
4892
+ }
4893
+ });
4894
+ }
4895
+ }
4896
+ const end_date = doc.end_date;
4897
+ if (end_date !== undefined) {
4898
+ if (typeof end_date !== "string") {
4899
+ issues.push({ path: ["end_date"], message: `expected string, received ${typeName(end_date)}` });
4900
+ } else if (!DATE_RE2.test(end_date)) {
4901
+ issues.push({ path: ["end_date"], message: `string must match ${DATE_RE2}` });
4902
+ }
4903
+ }
4904
+ if (issues.length > 0)
4905
+ return { ok: false, issues };
4906
+ return {
4907
+ ok: true,
4908
+ data: {
4909
+ iteration_id: doc.iteration_id,
4910
+ start_date: doc.start_date,
4911
+ status,
4912
+ iteration_base_branch: doc.iteration_base_branch,
4913
+ target_branch: doc.target_branch,
4914
+ ...plans !== undefined ? { plans } : {},
4915
+ ...end_date !== undefined ? { end_date } : {}
4916
+ }
4917
+ };
4918
+ }
4919
+ function violation5(severity, code, message, fix) {
4920
+ return { ok: false, severity, code, message, fix };
4921
+ }
4922
+ function isPlainObject3(value) {
4923
+ return typeof value === "object" && value !== null && !Array.isArray(value);
4924
+ }
4925
+ function validateCompassFrontmatter(doc) {
4926
+ if (!isPlainObject3(doc)) {
4927
+ return {
4928
+ ok: false,
4929
+ violations: [
4930
+ violation5("medium", "COMPASS_INVALID_FIELD", "Compass frontmatter must be a YAML object with iteration_id / start_date / status / iteration_base_branch / target_branch (template: mstar-iteration §1.3)", "Fix the frontmatter of {ITERATION_DIR}/<iteration-id>/delivery-compass.md")
4931
+ ]
4932
+ };
4933
+ }
4934
+ const parsed = validateCompassShape(doc);
4935
+ if (!parsed.ok) {
4936
+ return {
4937
+ ok: false,
4938
+ violations: parsed.issues.map((issue) => {
4939
+ const field = issue.path.join(".") || "(root)";
4940
+ return violation5("medium", "COMPASS_INVALID_FIELD", `Compass frontmatter field '${field}' is invalid: ${issue.message}`, `Fix '${field}' in {ITERATION_DIR}/<iteration-id>/delivery-compass.md frontmatter (template: mstar-iteration §1.3)`);
4941
+ })
4942
+ };
4943
+ }
4944
+ const violations = [];
4945
+ const { status, end_date } = parsed.data;
4946
+ if (status === "completed" && end_date === undefined) {
4947
+ violations.push(violation5("high", "COMPASS_END_DATE_REQUIRED", "Compass frontmatter status is 'completed' but end_date is missing — end_date is required at iteration-close (mstar-iteration §3.4, template Fields guide)", "Add `end_date: YYYY-MM-DD` to the frontmatter"));
4948
+ }
4949
+ if (status !== "completed" && end_date !== undefined) {
4950
+ violations.push(violation5("medium", "COMPASS_END_DATE_NOT_ALLOWED", `Compass frontmatter sets end_date while status is '${status}' — end_date is only written at iteration-close (mstar-iteration §3.4)`, "Remove end_date until iteration-close"));
4951
+ }
4952
+ return { ok: violations.length === 0, violations };
4953
+ }
4954
+ function registeredPlanIds(compassDoc) {
4955
+ if (!Array.isArray(compassDoc.plans))
4956
+ return [];
4957
+ return compassDoc.plans.filter((plan) => typeof plan === "string" && plan.length > 0);
4958
+ }
4959
+ function findPlanRow(statusDoc, planId) {
4960
+ if (!Array.isArray(statusDoc.plans))
4961
+ return null;
4962
+ for (const row of statusDoc.plans) {
4963
+ if (!isPlainObject3(row))
4964
+ continue;
4965
+ const rowId = typeof row.id === "string" ? row.id : typeof row.plan_id === "string" ? row.plan_id : null;
4966
+ if (rowId === planId)
4967
+ return row;
4968
+ }
4969
+ return null;
4970
+ }
4971
+ function entryPlansAllDone(statusDoc, registered) {
4972
+ const violations = [];
4973
+ if (registered.length === 0) {
4974
+ violations.push(violation5("medium", "COMPASS_NO_PLANS", "Compass frontmatter registers no plans — the all-plans-Done transition cannot be verified (mstar-iteration §1.3 / Phase transition gates)", "List the iteration's plan ids in the compass frontmatter `plans`"));
4975
+ return violations;
4976
+ }
4977
+ for (const planId of registered) {
4978
+ const row = findPlanRow(statusDoc, planId);
4979
+ if (row === null) {
4980
+ violations.push(violation5("high", "PLAN_NOT_IN_STATUS", `Plan '${planId}' is registered in the compass frontmatter but has no row in status.json plans[] (mstar-iteration §3.1 entry item 1)`, "Add the plan row to {HARNESS_DIR}/status.json"));
4981
+ continue;
4982
+ }
4983
+ if (row.status !== PLAN_STATUS_DONE) {
4984
+ violations.push(violation5("high", "PLAN_NOT_DONE", `Plan '${planId}' status is ${JSON.stringify(row.status)} in status.json — all compass-registered plans must be 'Done' before iteration-close (mstar-iteration §3.1 entry item 1)`));
4985
+ }
4986
+ }
4987
+ return violations;
4988
+ }
4989
+ function entryResidualsOpen(statusDoc, planId) {
4990
+ const violations = [];
4991
+ const residualRoot = statusDoc.residual_findings;
4992
+ if (residualRoot === undefined || residualRoot === null)
4993
+ return violations;
4994
+ if (!isPlainObject3(residualRoot)) {
4995
+ violations.push(violation5("medium", "RESIDUAL_MALFORMED", "status.json residual_findings must be a plan-id → entries object (mstar-iteration §3.1 entry item 2)"));
4996
+ return violations;
4997
+ }
4998
+ const entries = residualRoot[planId];
4999
+ if (entries === undefined)
5000
+ return violations;
5001
+ if (!Array.isArray(entries)) {
5002
+ violations.push(violation5("medium", "RESIDUAL_MALFORMED", `status.json residual_findings['${planId}'] must be an array of residual entries (mstar-iteration §3.1 entry item 2)`));
5003
+ return violations;
5004
+ }
5005
+ const openIds = [];
5006
+ for (const entry of entries) {
5007
+ if (!isPlainObject3(entry) || !isOpenResidual(entry))
5008
+ continue;
5009
+ const isBlockerDefer = entry.decision === "defer" && typeof entry.target === "string" && entry.target.trim() !== "";
5010
+ if (isBlockerDefer)
5011
+ continue;
5012
+ openIds.push(typeof entry.id === "string" ? entry.id : "<unnamed>");
5013
+ }
5014
+ if (openIds.length > 0) {
5015
+ violations.push(violation5("high", "OPEN_RESIDUALS", `Plan '${planId}' has ${openIds.length} open residual finding(s) not exempted as blocker-defers (${openIds.join(", ")}) — residuals must be closed/archived before iteration-close; only zero-residual blocker-defers (decision: defer + target) may stay open (mstar-iteration §3.1 entry item 2)`, "Close or archive the open residuals, or convert them into blocker-defers (decision: defer + non-empty target) per mstar-plan-artifacts Findings cleanup modes"));
5016
+ }
5017
+ return violations;
5018
+ }
5019
+ function entryFrontmatterComplete(compassDoc) {
5020
+ return validateCompassFrontmatter(compassDoc).violations;
5021
+ }
5022
+ function exitFrontmatterClosed(compassDoc) {
5023
+ const violations = [];
5024
+ if (compassDoc.status !== "completed") {
5025
+ violations.push(violation5("high", "EXIT_STATUS_NOT_COMPLETED", `Compass frontmatter status must be 'completed' at close exit — current: ${JSON.stringify(compassDoc.status)} (mstar-iteration §3.4 / §3.5 exit item 4)`));
5026
+ }
5027
+ const endDate = compassDoc.end_date;
5028
+ if (typeof endDate !== "string" || !DATE_RE2.test(endDate)) {
5029
+ violations.push(violation5("high", "EXIT_END_DATE_REQUIRED", "Compass frontmatter end_date (YYYY-MM-DD) is required when closing (mstar-iteration §3.4 / §3.5 exit item 4)"));
5030
+ }
5031
+ return violations;
5032
+ }
5033
+ function exitBranchCheck(opts) {
5034
+ const violations = [];
5035
+ const { currentBranch, specIntegrationBranch } = opts;
5036
+ if (currentBranch === undefined || specIntegrationBranch === undefined) {
5037
+ violations.push(violation5("medium", "EXIT_BRANCH_UNVERIFIABLE", "Cannot verify the current branch is spec_integration_branch — missing currentBranch / specIntegrationBranch probe inputs (mstar-iteration §3.5 exit item 5)"));
5038
+ } else if (currentBranch !== specIntegrationBranch) {
5039
+ violations.push(violation5("high", "EXIT_BRANCH_MISMATCH", `Current branch '${currentBranch}' is not the spec_integration_branch '${specIntegrationBranch}' (mstar-iteration §3.5 exit item 5)`));
5040
+ }
5041
+ return violations;
5042
+ }
5043
+ function exitPrBaseCheck(compassDoc, opts) {
5044
+ const violations = [];
5045
+ const target = compassDoc.target_branch;
5046
+ const { prBaseBranch } = opts;
5047
+ if (prBaseBranch === undefined) {
5048
+ violations.push(violation5("medium", "EXIT_PR_BASE_UNVERIFIABLE", "Cannot verify the PR base — missing prBaseBranch probe input (mstar-iteration §3.5 exit item 6)"));
5049
+ } else if (typeof target !== "string" || prBaseBranch !== target) {
5050
+ violations.push(violation5("high", "EXIT_PR_BASE_MISMATCH", `PR base '${prBaseBranch}' must equal the compass target_branch '${String(target)}' — not an undocumented branch (mstar-iteration §3.5 exit item 6)`));
5051
+ }
5052
+ return violations;
5053
+ }
5054
+ function evaluatePhaseGate(statusDoc, compassDoc, opts = {}) {
5055
+ const registered = registeredPlanIds(compassDoc);
5056
+ const entryViolations = [
5057
+ ...entryPlansAllDone(statusDoc, registered),
5058
+ ...registered.flatMap((planId) => entryResidualsOpen(statusDoc, planId)),
5059
+ ...entryFrontmatterComplete(compassDoc)
5060
+ ];
5061
+ const exitViolations = [
5062
+ ...exitFrontmatterClosed(compassDoc),
5063
+ ...exitBranchCheck(opts),
5064
+ ...exitPrBaseCheck(compassDoc, opts)
5065
+ ];
5066
+ const allPlansDone = registered.length > 0 && registered.every((planId) => {
5067
+ const row = findPlanRow(statusDoc, planId);
5068
+ return row !== null && row.status === PLAN_STATUS_DONE;
5069
+ });
5070
+ const entry = { ok: entryViolations.length === 0, violations: entryViolations };
5071
+ const exit = { ok: exitViolations.length === 0, violations: exitViolations };
5072
+ let transition;
5073
+ if (!allPlansDone)
5074
+ transition = "phase-2-execute";
5075
+ else if (entry.ok && exit.ok)
5076
+ transition = "phase-4-pr-delivery";
5077
+ else
5078
+ transition = "phase-3-close";
5079
+ const gateBlocking = allPlansDone ? [...entryViolations, ...exitViolations] : [];
5080
+ return {
5081
+ transition,
5082
+ allPlansDone,
5083
+ entry,
5084
+ exit,
5085
+ ok: gateBlocking.length === 0,
5086
+ violations: gateBlocking
5087
+ };
5088
+ }
5089
+ function pushCadenceProbe(ciRunning, reviewWaveActive) {
5090
+ const violations = [];
5091
+ if (ciRunning) {
5092
+ violations.push(violation5("high", "PUSH_BLOCKED_CI", "CI checks are still queued/in_progress on the current head — do not push until the wave completes (mstar-iteration §5.1a push gate 1)", "Wait for CI to settle, then push once with the whole local batch"));
5093
+ }
5094
+ if (reviewWaveActive) {
5095
+ violations.push(violation5("high", "PUSH_BLOCKED_REVIEW_WAVE", "An AI/bot review wave is still running on the current head — do not push until it settles (mstar-iteration §5.1a push gate 2)", "Wait for the review wave, then push once"));
5096
+ }
5097
+ return { ok: violations.length === 0, violations };
5098
+ }
5099
+ function violation6(severity, code, message, fix) {
5100
+ return { ok: false, severity, code, message, fix };
5101
+ }
5102
+ var RAW_GROUP = "__raw";
5103
+ var isMap = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
5104
+ function parseScalar(raw) {
5105
+ const trimmed = raw.trim();
5106
+ const quoted = /^"([^"]*)"$/.exec(trimmed) ?? /^'([^']*)'$/.exec(trimmed);
5107
+ if (quoted)
5108
+ return quoted[1];
5109
+ const cut = trimmed.split(/\s+#/)[0].trim();
5110
+ if (/^-?\d+(?:\.\d+)?$/.test(cut))
5111
+ return Number(cut);
5112
+ if (/^(?:true|false)$/.test(cut))
5113
+ return cut === "true";
5114
+ return cut;
5115
+ }
5116
+ function parseMapBlock(lines, start, indent) {
5117
+ const map = {};
5118
+ let i = start;
5119
+ while (i < lines.length && lines[i].indent === indent) {
5120
+ const match = /^([^:]+):(.*)$/.exec(lines[i].text);
5121
+ if (match === null) {
5122
+ i++;
5123
+ continue;
5124
+ }
5125
+ const key = parseScalar(match[1].trim()).toString();
5126
+ const rest = match[2].trim();
5127
+ if (rest === "") {
5128
+ const nested = i + 1 < lines.length && lines[i + 1].indent > indent;
5129
+ if (nested) {
5130
+ const child = parseBlock(lines, i + 1, lines[i + 1].indent);
5131
+ map[key] = child.value;
5132
+ i = child.next;
5133
+ } else {
5134
+ map[key] = "";
5135
+ i++;
5136
+ }
5137
+ } else {
5138
+ map[key] = parseScalar(rest);
5139
+ i++;
5140
+ }
5141
+ }
5142
+ return { value: map, next: i };
5143
+ }
5144
+ function parseListBlock(lines, start, indent) {
5145
+ const list = [];
5146
+ let i = start;
5147
+ while (i < lines.length && lines[i].indent === indent && lines[i].text.startsWith("-")) {
5148
+ const rest = lines[i].text.slice(1).trim();
5149
+ const match = /^([^:]+):(.*)$/.exec(rest);
5150
+ if (match !== null && match[2].trim() === "" && i + 1 < lines.length && lines[i + 1].indent > indent) {
5151
+ const child = parseBlock(lines, i + 1, lines[i + 1].indent);
5152
+ list.push({ [parseScalar(match[1].trim()).toString()]: child.value });
5153
+ i = child.next;
5154
+ } else if (match !== null) {
5155
+ list.push({ [parseScalar(match[1].trim()).toString()]: parseScalar(match[2].trim()) });
5156
+ i++;
5157
+ } else {
5158
+ list.push(parseScalar(rest));
5159
+ i++;
5160
+ }
5161
+ }
5162
+ return { value: list, next: i };
5163
+ }
5164
+ function parseBlock(lines, start, indent) {
5165
+ if (lines[start] !== undefined && lines[start].text.startsWith("-"))
5166
+ return parseListBlock(lines, start, indent);
5167
+ return parseMapBlock(lines, start, indent);
5168
+ }
5169
+ function parseDesignFrontmatter(frontmatterText) {
5170
+ const body = frontmatterText.replace(/^\uFEFF/, "");
5171
+ const lines = body.split(/\r?\n/);
5172
+ if (lines.length === 0 || !lines[0].trim().startsWith("---"))
5173
+ return null;
5174
+ const inner = [];
5175
+ for (let i = 1;i < lines.length; i++) {
5176
+ const trimmed = lines[i].trim();
5177
+ if (trimmed === "---")
5178
+ break;
5179
+ if (trimmed === "" || trimmed.startsWith("#"))
5180
+ continue;
5181
+ const indent = lines[i].match(/^ */)[0].length;
5182
+ inner.push({ indent, text: lines[i].slice(indent) });
5183
+ }
5184
+ if (inner.length === 0)
5185
+ return null;
5186
+ const top = parseBlock(inner, 0, inner[0].indent);
5187
+ if (!isMap(top.value))
5188
+ return null;
5189
+ const fm = { colors: {}, typography: {}, spacing: {}, rounded: {}, components: {} };
5190
+ for (const [key, value] of Object.entries(top.value)) {
5191
+ if (key === "version" || key === "name" || key === "description") {
5192
+ if (typeof value === "string")
5193
+ fm[key] = value;
5194
+ } else if (key === "colors" || key === "typography" || key === "spacing" || key === "rounded" || key === "components") {
5195
+ fm[key] = isMap(value) ? value : { [RAW_GROUP]: value };
5196
+ }
5197
+ }
5198
+ return fm;
5199
+ }
5200
+ var HEX_RE = /^#(?:[0-9a-f]{3,4}|[0-9a-f]{6}|[0-9a-f]{8})$/i;
5201
+ var OKLCH_RE = /^oklch\([^)]*\)$/i;
5202
+ var PX_RE = /^-?\d+(?:\.\d+)?px$/;
5203
+ var PLACEHOLDER_RE = /^\[.*\]$/;
5204
+ var TYPOGRAPHY_PROPS = ["fontFamily", "fontSize", "fontWeight", "lineHeight", "letterSpacing"];
5205
+ var REF_RE = /^\{([a-z]+)\.([^}]+)\}$/;
5206
+ var REF_GROUPS = ["colors", "typography", "rounded"];
5207
+ var isPlaceholder = (value) => PLACEHOLDER_RE.test(value);
5208
+ function validateDesignTokenFrontmatter(frontmatterText) {
5209
+ const violations = [];
5210
+ const fm = parseDesignFrontmatter(frontmatterText);
5211
+ if (fm === null) {
5212
+ violations.push(violation6("medium", "design-md.tokens.missing-frontmatter", "no `---` YAML frontmatter block found — DESIGN.md must open with a fenced frontmatter holding the token SSOT (design-md-spec §1.5)", "add a `---` fenced frontmatter with version, name, description, and the colors/typography/spacing/rounded groups"));
5213
+ return { ok: false, violations };
5214
+ }
5215
+ const groupEntries = (group) => {
5216
+ const value = fm[group];
5217
+ if (!isMap(value) || RAW_GROUP in value)
5218
+ return [];
5219
+ return Object.entries(value);
5220
+ };
5221
+ const groupIsMap = (group) => {
5222
+ const value = fm[group];
5223
+ return isMap(value) && !(RAW_GROUP in value);
5224
+ };
5225
+ for (const group of ["colors", "typography", "spacing", "rounded"]) {
5226
+ if (!isMap(fm[group])) {
5227
+ violations.push(violation6("medium", "design-md.tokens.group-not-map", `token group "${group}" must be a YAML map, not a scalar (design-md-spec §1.5)`, `rewrite \`${group}\` as a nested map`));
5228
+ } else if (!groupIsMap(group)) {
5229
+ violations.push(violation6("medium", "design-md.tokens.group-not-map", `token group "${group}" must be a YAML map, not a scalar (design-md-spec §1.5)`, `rewrite \`${group}\` as a nested map`));
5230
+ } else if (groupEntries(group).length === 0) {
5231
+ violations.push(violation6("medium", "design-md.tokens.missing-group", `missing required token group "${group}" — colors/typography/spacing/rounded are required by the frontmatter SSOT (design-md-spec §1.5)`, `add an active \`${group}:\` block with concrete token values`));
5232
+ }
5233
+ }
5234
+ if (!isMap(fm.components) || !groupIsMap("components")) {
5235
+ violations.push(violation6("medium", "design-md.tokens.group-not-map", `token group "components" must be a YAML map, not a scalar (design-md-spec §1.5)`, `rewrite \`components\` as a nested map`));
5236
+ }
5237
+ const placeholder = (group, name, value) => violations.push(violation6("low", "design-md.tokens.placeholder", `token "${group}.${name}" uses a "[...]" template value — placeholders never count as concrete tokens (completeness-checklist § How to use item 5)`, `replace \`${value}\` with a concrete value`));
5238
+ for (const [name, value] of groupEntries("colors")) {
5239
+ if (typeof value !== "string") {
5240
+ violations.push(violation6("medium", "design-md.tokens.color-format", `color "${name}" must be a string value (design-md-spec §2.2)`, "quote the color value"));
5241
+ continue;
5242
+ }
5243
+ if (isPlaceholder(value)) {
5244
+ placeholder("colors", name, value);
5245
+ } else if (!HEX_RE.test(value) && !OKLCH_RE.test(value)) {
5246
+ violations.push(violation6("medium", "design-md.tokens.color-format", `color "${name}" = "${value}" is not a hex (\`#rrggbb\`/\`#rrggbbaa\`) or oklch() value (design-md-spec §2.2)`, "use an sRGB hex value, optionally with a `-p3` oklch() twin"));
5247
+ }
5248
+ }
5249
+ for (const [name, value] of groupEntries("typography")) {
5250
+ if (!isMap(value)) {
5251
+ violations.push(violation6("medium", "design-md.tokens.typography-shape", `typography token "${name}" must be a map of the five properties (design-md-spec §1.5)`, "give it fontFamily/fontSize/fontWeight/lineHeight/letterSpacing"));
5252
+ continue;
5253
+ }
5254
+ const keys = Object.keys(value);
5255
+ const missing = TYPOGRAPHY_PROPS.filter((p) => !keys.includes(p));
5256
+ const extra = keys.filter((k) => !TYPOGRAPHY_PROPS.includes(k));
5257
+ if (missing.length > 0 || extra.length > 0) {
5258
+ violations.push(violation6("medium", "design-md.tokens.typography-shape", `typography token "${name}" must have exactly the five properties fontFamily/fontSize/fontWeight/lineHeight/letterSpacing (design-md-spec §1.5)${missing.length > 0 ? ` — missing: ${missing.join(", ")}` : ""}${extra.length > 0 ? ` — extra: ${extra.join(", ")}` : ""}`, "align the token with the five-property shape"));
5259
+ }
5260
+ for (const prop of ["fontFamily", "fontSize"]) {
5261
+ const v = value[prop];
5262
+ if (typeof v === "string" && isPlaceholder(v))
5263
+ placeholder("typography", name, v);
5264
+ else if (typeof v !== "string" || v.trim() === "") {
5265
+ violations.push(violation6("medium", "design-md.tokens.typography-shape", `typography token "${name}" has an empty \`${prop}\` (design-md-spec §1.5)`, `fill \`${prop}\` with a concrete value`));
5266
+ }
5267
+ }
5268
+ }
5269
+ if (groupIsMap("spacing")) {
5270
+ const spacing = fm.spacing;
5271
+ if (!Object.prototype.hasOwnProperty.call(spacing, "base")) {
5272
+ violations.push(violation6("medium", "design-md.tokens.spacing-base", "spacing must declare the base unit as `base` (design-md-spec §2.4)", "add `base: 4px` (or 8px) to the spacing group"));
5273
+ }
5274
+ for (const [name, value] of Object.entries(spacing)) {
5275
+ if (name !== "base" && name !== RAW_GROUP && !/^\d+$/.test(name)) {
5276
+ violations.push(violation6("medium", "design-md.tokens.spacing-key", `spacing key "${name}" must be \`base\` or a numeric multiplier (design-md-spec §1.5)`, "use numeric scale-step keys or `base`"));
5277
+ }
5278
+ if (typeof value === "string" && isPlaceholder(value)) {
5279
+ placeholder("spacing", name, value);
5280
+ } else if (typeof value !== "string" || !PX_RE.test(value)) {
5281
+ violations.push(violation6("medium", "design-md.tokens.spacing-format", `spacing value "${name}" = "${String(value)}" is not a px length (design-md-spec §1.5)`, "use a pixel value like `4px`"));
5282
+ }
5283
+ }
5284
+ }
5285
+ for (const [name, value] of groupEntries("rounded")) {
5286
+ if (typeof value === "string" && isPlaceholder(value)) {
5287
+ placeholder("rounded", name, value);
5288
+ } else if (typeof value !== "string" || !PX_RE.test(value)) {
5289
+ violations.push(violation6("medium", "design-md.tokens.rounded-format", `rounded value "${name}" = "${String(value)}" is not a px length (design-md-spec §1.5)`, "use a pixel value like `6px`"));
5290
+ }
5291
+ }
5292
+ for (const [name, value] of groupEntries("components")) {
5293
+ if (!isMap(value)) {
5294
+ violations.push(violation6("medium", "design-md.tokens.components-shape", `component token "${name}" must be a map of properties (design-md-spec §2.8)`, "give it backgroundColor/textColor/typography/rounded/padding/height"));
5295
+ continue;
5296
+ }
5297
+ for (const [prop, v] of Object.entries(value)) {
5298
+ if (typeof v !== "string")
5299
+ continue;
5300
+ if (isPlaceholder(v)) {
5301
+ placeholder("components", `${name}.${prop}`, v);
5302
+ continue;
5303
+ }
5304
+ const ref = REF_RE.exec(v);
5305
+ if (ref === null)
5306
+ continue;
5307
+ const [, refGroup, refKey] = ref;
5308
+ const resolves = REF_GROUPS.includes(refGroup) && groupIsMap(refGroup) && Object.prototype.hasOwnProperty.call(fm[refGroup], refKey);
5309
+ if (!resolves) {
5310
+ violations.push(violation6("medium", "design-md.tokens.ref-unresolved", `component "${name}" references "${v}" which does not resolve to an active token in this frontmatter (design-md-spec §6 — {path} refs MUST trace back to a key)`, `add the referenced token or use a literal value`));
5311
+ }
5312
+ }
5313
+ }
5314
+ return { ok: violations.length === 0, violations };
5315
+ }
5316
+ var PARITY_GROUPS = ["colors", "typography", "spacing", "rounded", "components"];
5317
+ function assertLightDarkParity(lightFm, darkFm) {
5318
+ const violations = [];
5319
+ const light = parseDesignFrontmatter(lightFm);
5320
+ const dark = parseDesignFrontmatter(darkFm);
5321
+ if (light === null || dark === null) {
5322
+ violations.push(violation6("medium", "design-md.parity.missing-frontmatter", `light/dark parity needs a YAML frontmatter in both files — ${light === null ? "DESIGN.md" : "DESIGN.dark.md"} has none (design-md-spec §4 rules 1–2)`, "add the fenced frontmatter to both theme files"));
5323
+ return { ok: false, violations };
5324
+ }
5325
+ const activeKeys = (fm) => {
5326
+ const keys = new Set;
5327
+ for (const group of PARITY_GROUPS) {
5328
+ const value = fm[group];
5329
+ if (!isMap(value) || RAW_GROUP in value)
5330
+ continue;
5331
+ for (const key of Object.keys(value))
5332
+ keys.add(`${group}.${key}`);
5333
+ }
5334
+ return keys;
5335
+ };
5336
+ const lightKeys = activeKeys(light);
5337
+ const darkKeys = activeKeys(dark);
5338
+ for (const key of lightKeys) {
5339
+ if (!darkKeys.has(key)) {
5340
+ violations.push(violation6("medium", "design-md.parity.missing-dark", `token "${key}" is active in DESIGN.md but missing from DESIGN.dark.md — both files must define the same token set (design-md-spec §4 rule 3)`, "add the token to DESIGN.dark.md with a dark-appropriate value"));
5341
+ }
5342
+ }
5343
+ for (const key of darkKeys) {
5344
+ if (!lightKeys.has(key)) {
5345
+ violations.push(violation6("medium", "design-md.parity.missing-light", `token "${key}" is active in DESIGN.dark.md but missing from DESIGN.md — DESIGN.md is the SSOT for token names (design-md-spec §4 rules 3–4)`, "add the token to DESIGN.md, or remove it from the dark file"));
5346
+ }
5347
+ }
5348
+ return { ok: violations.length === 0, violations };
5349
+ }
5350
+ var GRAY_STEPS = ["100", "200", "300", "400", "500", "600", "700", "800", "900", "1000"];
5351
+ var ALPHA_STEPS = ["100", "200", "300", "400", "500", "600"];
5352
+ var ACCENT_SCALES = ["blue", "red", "amber", "green", "teal", "purple", "pink"];
5353
+ var L3_BODY_ITEM_IDS = [
5354
+ "dark-exists",
5355
+ "dark-parity",
5356
+ "elevation-shadows",
5357
+ "motion-easing",
5358
+ "motion-durations",
5359
+ "motion-reduced",
5360
+ "voice-content"
5361
+ ];
5362
+ function isConcrete(value) {
5363
+ if (typeof value === "number")
5364
+ return true;
5365
+ return typeof value === "string" && value !== "" && !isPlaceholder(value);
5366
+ }
5367
+ function groupHasConcrete(fm, group, key) {
5368
+ if (fm === null)
5369
+ return false;
5370
+ const value = fm[group];
5371
+ if (!isMap(value) || RAW_GROUP in value)
5372
+ return false;
5373
+ const entry = value[key];
5374
+ return entry !== undefined && isConcrete(entry);
5375
+ }
5376
+ function typographyTokenComplete(fm, key) {
5377
+ if (fm === null)
5378
+ return false;
5379
+ const group = fm.typography;
5380
+ if (!isMap(group) || RAW_GROUP in group)
5381
+ return false;
5382
+ const entry = group[key];
5383
+ if (!isMap(entry))
5384
+ return false;
5385
+ return TYPOGRAPHY_PROPS.every((p) => Object.prototype.hasOwnProperty.call(entry, p) && isConcrete(entry[p]));
5386
+ }
5387
+ function countRoleTokens(fm, role) {
5388
+ if (fm === null)
5389
+ return 0;
5390
+ const group = fm.typography;
5391
+ if (!isMap(group) || RAW_GROUP in group)
5392
+ return 0;
5393
+ return Object.keys(group).filter((k) => k.startsWith(`${role}-`) && typographyTokenComplete(fm, k)).length;
5394
+ }
5395
+ function countNumericSpacingSteps(fm) {
5396
+ if (fm === null)
5397
+ return 0;
5398
+ const group = fm.spacing;
5399
+ if (!isMap(group) || RAW_GROUP in group)
5400
+ return 0;
5401
+ return Object.keys(group).filter((k) => k !== "base" && k !== RAW_GROUP && /^\d+$/.test(k)).length;
5402
+ }
5403
+ function hasComponent(fm, name) {
5404
+ if (fm === null)
5405
+ return false;
5406
+ const group = fm.components;
5407
+ if (!isMap(group) || RAW_GROUP in group)
5408
+ return false;
5409
+ return isMap(group[name]);
5410
+ }
5411
+ var LEVEL_RANK = { BELOW_MVP: 0, MVP: 1, Standard: 2, Production: 3 };
5412
+ function completenessLevel(frontmatterText, checklist) {
5413
+ const fm = parseDesignFrontmatter(frontmatterText);
5414
+ const bodyUnverified = checklist === undefined;
5415
+ const bodyOk = (id) => (checklist ?? []).includes(id);
5416
+ const items = [];
5417
+ const add = (id, level2, source, ok) => {
5418
+ items.push({ id, level: level2, ok, source });
5419
+ };
5420
+ add("fm-exists", 1, "frontmatter", fm !== null);
5421
+ add("version", 1, "frontmatter", fm !== null && typeof fm.version === "string" && isConcrete(fm.version));
5422
+ add("name-description", 1, "frontmatter", fm !== null && typeof fm.name === "string" && isConcrete(fm.name) && typeof fm.description === "string" && isConcrete(fm.description));
5423
+ add("colors-background", 1, "frontmatter", groupHasConcrete(fm, "colors", "background-100"));
5424
+ add("colors-text", 1, "frontmatter", groupHasConcrete(fm, "colors", "gray-1000") && groupHasConcrete(fm, "colors", "gray-900"));
5425
+ add("colors-accent", 1, "frontmatter", ACCENT_SCALES.some((a) => groupHasConcrete(fm, "colors", `${a}-700`)));
5426
+ add("colors-semantic", 1, "frontmatter", groupHasConcrete(fm, "colors", "red-700") && groupHasConcrete(fm, "colors", "amber-700"));
5427
+ add("type-copy", 1, "frontmatter", countRoleTokens(fm, "copy") >= 1);
5428
+ add("type-heading", 1, "frontmatter", countRoleTokens(fm, "heading") >= 1);
5429
+ add("spacing-scale", 1, "frontmatter", groupHasConcrete(fm, "spacing", "base") && countNumericSpacingSteps(fm) >= 5);
5430
+ add("rounded-sm", 1, "frontmatter", groupHasConcrete(fm, "rounded", "sm"));
5431
+ add("breakpoints-2", 1, "body", bodyOk("breakpoints-2"));
5432
+ add("colors-background-scale", 2, "frontmatter", ["background-100", "background-200", "background-300"].every((k) => groupHasConcrete(fm, "colors", k)));
5433
+ add("colors-gray-scale", 2, "frontmatter", GRAY_STEPS.every((s) => groupHasConcrete(fm, "colors", `gray-${s}`)));
5434
+ add("colors-alpha-scale", 2, "frontmatter", ALPHA_STEPS.every((s) => groupHasConcrete(fm, "colors", `gray-alpha-${s}`)));
5435
+ add("colors-accent-scales", 2, "frontmatter", ACCENT_SCALES.every((a) => ["700", "800", "900", "1000"].every((s) => groupHasConcrete(fm, "colors", `${a}-${s}`))));
5436
+ add("type-headings-3", 2, "frontmatter", countRoleTokens(fm, "heading") >= 3);
5437
+ add("type-label", 2, "frontmatter", countRoleTokens(fm, "label") >= 1);
5438
+ add("type-button", 2, "frontmatter", countRoleTokens(fm, "button") >= 1);
5439
+ add("spacing-full", 2, "frontmatter", countNumericSpacingSteps(fm) >= 9);
5440
+ add("rounded-full", 2, "frontmatter", ["sm", "md", "lg", "full"].every((k) => groupHasConcrete(fm, "rounded", k)));
5441
+ add("components-button", 2, "frontmatter", hasComponent(fm, "button-primary") && hasComponent(fm, "button-secondary") && hasComponent(fm, "button-small"));
5442
+ add("components-input", 2, "frontmatter", hasComponent(fm, "input"));
5443
+ add("breakpoints-4", 2, "body", bodyOk("breakpoints-4"));
5444
+ add("components-button-states", 2, "body", bodyOk("components-button-states"));
5445
+ add("components-input-states", 2, "body", bodyOk("components-input-states"));
5446
+ add("spacing-rhythm", 2, "body", bodyOk("spacing-rhythm"));
5447
+ const componentNames = [];
5448
+ if (fm !== null && isMap(fm.components)) {
5449
+ for (const key of Object.keys(fm.components)) {
5450
+ if (key !== RAW_GROUP && isMap(fm.components[key]))
5451
+ componentNames.push(key);
5452
+ }
5453
+ }
5454
+ const namesJoined = componentNames.join(" ");
5455
+ add("components-library", 3, "frontmatter", componentNames.length >= 4 && /card/i.test(namesJoined) && /modal/i.test(namesJoined) && /tooltip/i.test(namesJoined) && /menu|dropdown/i.test(namesJoined));
5456
+ add("dark-exists", 3, "body", bodyOk("dark-exists"));
5457
+ add("dark-parity", 3, "body", bodyOk("dark-parity"));
5458
+ add("elevation-shadows", 3, "body", bodyOk("elevation-shadows"));
5459
+ add("motion-easing", 3, "body", bodyOk("motion-easing"));
5460
+ add("motion-durations", 3, "body", bodyOk("motion-durations"));
5461
+ add("motion-reduced", 3, "body", bodyOk("motion-reduced"));
5462
+ add("voice-content", 3, "body", bodyOk("voice-content"));
5463
+ const participating = items.filter((it) => it.source === "frontmatter" || !bodyUnverified);
5464
+ const failing = (level2) => participating.filter((it) => it.level === level2 && !it.ok).map((it) => it.id);
5465
+ const fail1 = failing(1);
5466
+ const fail2 = failing(2);
5467
+ const fail3 = failing(3);
5468
+ let level;
5469
+ let missing;
5470
+ if (fail1.length > 0) {
5471
+ level = "BELOW_MVP";
5472
+ missing = fail1;
5473
+ } else if (fail2.length > 0) {
5474
+ level = "MVP";
5475
+ missing = fail2;
5476
+ } else if (fail3.length > 0) {
5477
+ level = "Standard";
5478
+ missing = fail3;
5479
+ } else {
5480
+ level = "Production";
5481
+ missing = [];
5482
+ }
5483
+ if (level === "Production" && bodyUnverified) {
5484
+ level = "Standard";
5485
+ missing = [...L3_BODY_ITEM_IDS];
5486
+ }
5487
+ const placeholders = [];
5488
+ frontmatterText.split(/\r?\n/).forEach((line, index) => {
5489
+ const m = /\b(LEVEL([23])_PLACEHOLDER)\b/.exec(line);
5490
+ if (m !== null)
5491
+ placeholders.push({ level: Number(m[2]), marker: m[1], line: index + 1 });
5492
+ });
5493
+ const rank = LEVEL_RANK[level];
5494
+ const candidate = placeholders.map((p) => p.level).filter((l) => l > rank).sort((a, b) => b - a)[0];
5495
+ const upgradeTo = candidate === undefined ? null : candidate;
5496
+ return { level, items, missing, placeholders, upgradeTo, bodyUnverified };
5497
+ }
5498
+ var AUDIT_PRIORITIES = ["P1", "P2", "P3"];
5499
+ var AUDIT_EFFORTS = ["XS", "S", "M", "L", "XL"];
5500
+ var AUDIT_RISKS = ["LOW", "MED", "HIGH"];
5501
+ var AUDIT_CATEGORIES = [
5502
+ "bug",
5503
+ "security",
5504
+ "perf",
5505
+ "tests",
5506
+ "tech-debt",
5507
+ "migration",
5508
+ "dx",
5509
+ "docs",
5510
+ "direction"
5511
+ ];
5512
+ function parseStatusBlocks(planText) {
5513
+ const blocks = [];
5514
+ let current = null;
5515
+ for (const line of planText.split(/\r?\n/)) {
5516
+ const trimmed = line.trim();
5517
+ if (trimmed === "## Status") {
5518
+ current = new Map;
5519
+ blocks.push({ fields: current });
5520
+ continue;
5521
+ }
5522
+ if (current === null)
5523
+ continue;
5524
+ if (trimmed.startsWith("#")) {
5525
+ current = null;
5526
+ continue;
5527
+ }
5528
+ const match = /^-\s*\*\*([^*]+)\*\*:\s*(.*)$/.exec(trimmed);
5529
+ if (match !== null)
5530
+ current.set(match[1].trim(), match[2].trim());
5531
+ }
5532
+ return blocks;
5533
+ }
5534
+ function slugify(title) {
5535
+ return title.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
5536
+ }
5537
+ var escapeCell = (value) => value.replace(/\|/g, "\\|");
5538
+ var truncate = (value, max) => value.length > max ? `${value.slice(0, max)}…` : value;
5539
+ function renderPlanFile(finding, plannedAt) {
5540
+ const sections = [
5541
+ `# ${finding.title}`,
5542
+ "",
5543
+ "## Status",
5544
+ `- **Priority**: ${finding.priority}`,
5545
+ `- **Effort**: ${finding.effort}`,
5546
+ `- **Risk**: ${finding.risk}`,
5547
+ `- **Depends on**: ${finding.dependsOn ?? "none"}`,
5548
+ `- **Category**: ${finding.category}`,
5549
+ `- **Planned at**: commit \`${plannedAt.commit}\`, ${plannedAt.date}`,
5550
+ "",
5551
+ "## Impact",
5552
+ finding.impact
5553
+ ];
5554
+ if (finding.evidence.length > 0) {
5555
+ sections.push("", "## Evidence", ...finding.evidence.map((e) => `- ${e}`));
5556
+ }
5557
+ if (finding.fixSketch !== undefined) {
5558
+ sections.push("", "## Fix sketch", finding.fixSketch);
5559
+ }
5560
+ if (finding.verification !== undefined) {
5561
+ sections.push("", "## Verification", finding.verification);
5562
+ }
5563
+ return `${sections.join(`
5564
+ `)}
5565
+ `;
5566
+ }
5567
+ function readPlanFileSummary(filePath) {
5568
+ const text = readFileSync6(filePath, "utf8");
5569
+ const title = (text.match(/^# (.+)$/m) ?? [])[1] ?? filePath;
5570
+ const blocks = parseStatusBlocks(text);
5571
+ return { title: title.trim(), fields: blocks.length > 0 ? blocks[0].fields : new Map };
5572
+ }
5573
+ function renderIndex(params) {
5574
+ const { date, repoName, repoShortSha, rows, rejected } = params;
5575
+ const findingsRows = rows.map((r) => `| ${r.num} | ${escapeCell(r.title)} | ${r.category} | ${escapeCell(truncate(r.impact, 80))} | ${r.effort} | ${r.risk} | ${r.confidence} | ${escapeCell(truncate(r.evidence, 80))} |`).join(`
5576
+ `);
5577
+ const directionRows = rows.filter((r) => r.category === "direction").map((r) => `- ${escapeCell(r.title)} — ${escapeCell(truncate(r.impact, 120))}`).join(`
5578
+ `);
5579
+ const executionRows = rows.map((r) => `| ${r.num} | ${escapeCell(r.title)} | ${r.priority} | ${r.effort} | ${r.dependsOn} | TODO |`).join(`
5580
+ `);
5581
+ const rejectedRows = rejected.map((r) => `- ${escapeCell(r.title)}: ${escapeCell(r.reason)}`).join(`
5582
+ `);
5583
+ const sections = [
5584
+ `# Audit Report — ${repoName} @ ${repoShortSha} (${date})`,
5585
+ "",
5586
+ "## Findings",
5587
+ "",
5588
+ "| # | Finding | Category | Impact | Effort | Risk | Confidence | Evidence |",
5589
+ "|---|---------|----------|--------|--------|------|------------|----------|",
5590
+ findingsRows
5591
+ ];
5592
+ if (directionRows !== "") {
5593
+ sections.push("", "## Direction", "", directionRows);
5594
+ }
5595
+ sections.push("", "## Execution order & status", "", "| Plan | Title | Priority | Effort | Depends on | Status |", "|------|-------|----------|--------|------------|--------|", executionRows);
5596
+ if (rejectedRows !== "") {
5597
+ sections.push("", "## Findings considered and rejected", "", rejectedRows);
5598
+ }
5599
+ return `${sections.join(`
5600
+ `)}
5601
+ `;
5602
+ }
5603
+ function scaffoldAuditPlan(outDir, findings, options = {}) {
5604
+ const date = options.date ?? new Date().toISOString().slice(0, 10);
5605
+ const plannedAt = options.plannedAt ?? { commit: options.repoShortSha ?? "unknown", date };
5606
+ mkdirSync5(outDir, { recursive: true });
5607
+ const existing = readdirSync4(outDir).filter((f) => /^\d{3}-.*\.md$/.test(f));
5608
+ let next = existing.reduce((max, f) => Math.max(max, Number(f.slice(0, 3))), 0) + 1;
5609
+ const written = [];
5610
+ const usedSlugs = new Set;
5611
+ for (const finding of findings) {
5612
+ const num = String(next).padStart(3, "0");
5613
+ let slug = slugify(finding.title);
5614
+ if (usedSlugs.has(slug)) {
5615
+ let n = 2;
5616
+ while (usedSlugs.has(`${slug}-${n}`))
5617
+ n++;
5618
+ slug = `${slug}-${n}`;
5619
+ }
5620
+ usedSlugs.add(slug);
5621
+ const file = `${num}-${slug}.md`;
5622
+ writeFileSync4(join7(outDir, file), renderPlanFile(finding, plannedAt));
5623
+ written.push(file);
5624
+ next++;
5625
+ }
5626
+ const all = [...existing, ...written].sort();
5627
+ const rows = all.map((file) => {
5628
+ const summary = readPlanFileSummary(join7(outDir, file));
5629
+ const fields = summary.fields;
5630
+ return {
5631
+ num: file.slice(0, 3),
5632
+ title: summary.title,
5633
+ category: fields.get("Category") ?? "—",
5634
+ impact: "see plan file",
5635
+ effort: fields.get("Effort") ?? "—",
5636
+ risk: fields.get("Risk") ?? "—",
5637
+ confidence: "—",
5638
+ evidence: fields.get("Evidence") ?? "—",
5639
+ priority: fields.get("Priority") ?? "—",
5640
+ dependsOn: fields.get("Depends on") ?? "—"
5641
+ };
5642
+ });
5643
+ const byNum = new Map(rows.map((r) => [r.num, r]));
5644
+ written.forEach((file, i) => {
5645
+ const finding = findings[i];
5646
+ if (finding === undefined)
5647
+ return;
5648
+ const row = byNum.get(file.slice(0, 3));
5649
+ if (row !== undefined) {
5650
+ row.category = finding.category;
5651
+ row.impact = finding.impact;
5652
+ row.effort = finding.effort;
5653
+ row.risk = finding.risk;
5654
+ row.confidence = finding.confidence;
5655
+ row.evidence = finding.evidence[0] ?? "";
5656
+ row.priority = finding.priority;
5657
+ row.dependsOn = finding.dependsOn ?? "none";
5658
+ }
5659
+ });
5660
+ writeFileSync4(join7(outDir, "README.md"), renderIndex({
5661
+ date,
5662
+ repoName: options.repoName ?? "repo",
5663
+ repoShortSha: options.repoShortSha ?? "unknown",
5664
+ rows,
5665
+ rejected: options.rejected ?? []
5666
+ }));
5667
+ return { outDir: resolve7(outDir), date, files: written, nextNumber: next };
5668
+ }
5669
+ function violation8(severity, code, message, fix) {
5670
+ return { ok: false, severity, code, message, fix };
5671
+ }
5672
+ var KNOWLEDGE_REQUIRED_FIELDS = ["module", "date", "problem_type", "category", "severity"];
5673
+ var KNOWLEDGE_PROBLEM_TYPES = [
5674
+ "build_error",
5675
+ "test_failure",
5676
+ "runtime_error",
5677
+ "performance_issue",
5678
+ "database_issue",
5679
+ "security_issue",
5680
+ "ui_bug",
5681
+ "integration_issue",
5682
+ "logic_error",
5683
+ "config_error",
5684
+ "developer_experience",
5685
+ "workflow_issue",
5686
+ "best_practice",
5687
+ "documentation_gap",
5688
+ "architecture_pattern",
5689
+ "design_pattern",
5690
+ "tooling_decision",
5691
+ "convention",
5692
+ "api_design",
5693
+ "testing_pattern"
5694
+ ];
5695
+ var KNOWLEDGE_BUG_PROBLEM_TYPES = [
5696
+ "build_error",
5697
+ "test_failure",
5698
+ "runtime_error",
5699
+ "performance_issue",
5700
+ "database_issue",
5701
+ "security_issue",
5702
+ "ui_bug",
5703
+ "integration_issue",
5704
+ "logic_error",
5705
+ "config_error"
5706
+ ];
5707
+ var KNOWLEDGE_SEVERITIES = ["critical", "high", "medium", "low"];
5708
+ var KNOWLEDGE_RESOLUTION_TYPES = [
5709
+ "code_fix",
5710
+ "migration",
5711
+ "config_change",
5712
+ "test_fix",
5713
+ "dependency_update",
5714
+ "environment_setup",
5715
+ "workflow_improvement",
5716
+ "documentation_update",
5717
+ "tooling_addition"
5718
+ ];
5719
+ var KNOWLEDGE_CATEGORY_MAP = {
5720
+ build_error: "build-errors",
5721
+ test_failure: "test-failures",
5722
+ runtime_error: "runtime-errors",
5723
+ performance_issue: "performance-issues",
5724
+ database_issue: "database-issues",
5725
+ security_issue: "security-issues",
5726
+ ui_bug: "ui-bugs",
5727
+ integration_issue: "integration-issues",
5728
+ logic_error: "logic-errors",
5729
+ config_error: "config-errors",
5730
+ best_practice: "best-practices",
5731
+ convention: "conventions",
5732
+ architecture_pattern: "architecture-patterns",
5733
+ design_pattern: "design-patterns",
5734
+ tooling_decision: "tooling-decisions",
5735
+ testing_pattern: "testing-patterns",
5736
+ api_design: "api-design",
5737
+ workflow_issue: "workflow-patterns",
5738
+ developer_experience: "developer-experience",
5739
+ documentation_gap: "documentation"
5740
+ };
5741
+ var DATE_RE3 = /^\d{4}-\d{2}-\d{2}$/;
5742
+ var isMap2 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
5743
+ function parseScalar2(raw) {
5744
+ const trimmed = raw.trim();
5745
+ const quoted = /^"([^"]*)"$/.exec(trimmed) ?? /^'([^']*)'$/.exec(trimmed);
5746
+ if (quoted)
5747
+ return quoted[1];
5748
+ const cut = trimmed.split(/\s+#/)[0].trim();
5749
+ if (/^-?\d+(?:\.\d+)?$/.test(cut))
5750
+ return Number(cut);
5751
+ if (/^(?:true|false)$/.test(cut))
5752
+ return cut === "true";
5753
+ return cut;
5754
+ }
5755
+ function parseMapBlock2(lines, start, indent) {
5756
+ const map = {};
5757
+ let i = start;
5758
+ while (i < lines.length && lines[i].indent === indent) {
5759
+ const match = /^([^:]+):(.*)$/.exec(lines[i].text);
5760
+ if (match === null) {
5761
+ i++;
5762
+ continue;
5763
+ }
5764
+ const key = parseScalar2(match[1].trim()).toString();
5765
+ const rest = match[2].trim();
5766
+ if (rest === "") {
5767
+ const nested = i + 1 < lines.length && lines[i + 1].indent > indent;
5768
+ if (nested) {
5769
+ const child = parseBlock2(lines, i + 1, lines[i + 1].indent);
5770
+ map[key] = child.value;
5771
+ i = child.next;
5772
+ } else {
5773
+ map[key] = "";
5774
+ i++;
5775
+ }
5776
+ } else {
5777
+ map[key] = parseScalar2(rest);
5778
+ i++;
5779
+ }
5780
+ }
5781
+ return { value: map, next: i };
5782
+ }
5783
+ function parseListBlock2(lines, start, indent) {
5784
+ const list = [];
5785
+ let i = start;
5786
+ while (i < lines.length && lines[i].indent === indent && lines[i].text.startsWith("-")) {
5787
+ const rest = lines[i].text.slice(1).trim();
5788
+ const match = /^([^:]+):(.*)$/.exec(rest);
5789
+ if (match !== null && match[2].trim() === "" && i + 1 < lines.length && lines[i + 1].indent > indent) {
5790
+ const child = parseBlock2(lines, i + 1, lines[i + 1].indent);
5791
+ list.push({ [parseScalar2(match[1].trim()).toString()]: child.value });
5792
+ i = child.next;
5793
+ } else if (match !== null) {
5794
+ list.push({ [parseScalar2(match[1].trim()).toString()]: parseScalar2(match[2].trim()) });
5795
+ i++;
5796
+ } else {
5797
+ list.push(parseScalar2(rest));
5798
+ i++;
5799
+ }
5800
+ }
5801
+ return { value: list, next: i };
5802
+ }
5803
+ function parseBlock2(lines, start, indent) {
5804
+ if (lines[start] !== undefined && lines[start].text.startsWith("-"))
5805
+ return parseListBlock2(lines, start, indent);
5806
+ return parseMapBlock2(lines, start, indent);
5807
+ }
5808
+ function parseYamlLite(text) {
5809
+ const body = text.replace(/^\uFEFF/, "");
5810
+ const lines = body.split(/\r?\n/);
5811
+ if (lines.length === 0 || !lines[0].trim().startsWith("---"))
5812
+ return null;
5813
+ const inner = [];
5814
+ for (let i = 1;i < lines.length; i++) {
5815
+ const trimmed = lines[i].trim();
5816
+ if (trimmed === "---")
5817
+ break;
5818
+ if (trimmed === "" || trimmed.startsWith("#"))
5819
+ continue;
5820
+ const indent = lines[i].match(/^ */)[0].length;
5821
+ inner.push({ indent, text: lines[i].slice(indent) });
5822
+ }
5823
+ if (inner.length === 0)
5824
+ return null;
5825
+ const top = parseBlock2(inner, 0, inner[0].indent);
5826
+ if (!isMap2(top.value))
5827
+ return null;
5828
+ return top.value;
5829
+ }
5830
+ function validateSchemaYaml(frontmatterText) {
5831
+ const violations = [];
5832
+ const doc = parseYamlLite(frontmatterText);
5833
+ if (doc === null) {
5834
+ violations.push(violation8("medium", "compound.schema.missing-frontmatter", "no `---` YAML frontmatter block found — knowledge docs must open with the schema.yaml contract (mstar-compound/references/schema.yaml)", "add the fenced frontmatter with module, date, problem_type, category, severity"));
5835
+ return { ok: false, violations };
5836
+ }
5837
+ const isStr = (v) => typeof v === "string";
5838
+ const missing = (field) => violations.push(violation8("medium", "compound.schema.missing-field", `missing required frontmatter field "${field}" (schema.yaml required_fields)`, `add \`${field}: <value>\` to the frontmatter`));
5839
+ for (const field of KNOWLEDGE_REQUIRED_FIELDS) {
5840
+ if (!(field in doc) || doc[field] === "")
5841
+ missing(field);
5842
+ }
5843
+ if (doc.date !== undefined && (!isStr(doc.date) || !DATE_RE3.test(doc.date))) {
5844
+ violations.push(violation8("medium", "compound.schema.invalid-date", `date "${String(doc.date)}" must be a YYYY-MM-DD string (schema.yaml required_fields.date)`, "use `YYYY-MM-DD`"));
5845
+ }
5846
+ const problemType = doc.problem_type;
5847
+ if (problemType !== undefined && !isStr(problemType)) {
5848
+ violations.push(violation8("medium", "compound.schema.invalid-problem-type", `problem_type "${String(problemType)}" must be a string — one of the schema.yaml enum values (bug: build_error…config_error; knowledge: developer_experience…testing_pattern)`, "pick the narrowest applicable problem_type from schema.yaml"));
5849
+ }
5850
+ const problemTypeValid = isStr(problemType) && KNOWLEDGE_PROBLEM_TYPES.includes(problemType);
5851
+ if (isStr(problemType) && !problemTypeValid) {
5852
+ violations.push(violation8("medium", "compound.schema.invalid-problem-type", `problem_type "${problemType}" is not one of the schema.yaml enum values (bug: build_error…config_error; knowledge: developer_experience…testing_pattern)`, "pick the narrowest applicable problem_type from schema.yaml"));
5853
+ }
5854
+ if (doc.severity !== undefined && !isStr(doc.severity)) {
5855
+ violations.push(violation8("medium", "compound.schema.invalid-severity", `severity "${String(doc.severity)}" must be a string — critical | high | medium | low (schema.yaml required_fields.severity)`, "use one of the four severity values"));
5856
+ }
5857
+ if (isStr(doc.severity) && !KNOWLEDGE_SEVERITIES.includes(doc.severity)) {
5858
+ violations.push(violation8("medium", "compound.schema.invalid-severity", `severity "${doc.severity}" must be critical | high | medium | low (schema.yaml required_fields.severity)`, "use one of the four severity values"));
5859
+ }
5860
+ if (problemTypeValid && isStr(doc.category)) {
5861
+ const expected = KNOWLEDGE_CATEGORY_MAP[problemType];
5862
+ if (doc.category !== expected) {
5863
+ violations.push(violation8("medium", "compound.schema.category-mismatch", `category "${doc.category}" does not match problem_type "${problemType}" — category-mapping.md rule 1 maps it to "${expected}"`, `set \`category: ${expected}\` (the directory name under {KNOWLEDGE_DIR})`));
5864
+ }
5865
+ }
5866
+ if (problemTypeValid) {
5867
+ const isBug = KNOWLEDGE_BUG_PROBLEM_TYPES.includes(problemType);
5868
+ if (isBug) {
5869
+ for (const field of ["symptoms", "root_cause", "resolution_type"]) {
5870
+ if (!(field in doc)) {
5871
+ violations.push(violation8("medium", "compound.schema.missing-track-field", `bug-track doc missing required field "${field}" (schema.yaml track_rules.bug)`, `add \`${field}:\` to the frontmatter`));
5872
+ }
5873
+ }
5874
+ if (doc.symptoms !== undefined && !Array.isArray(doc.symptoms)) {
5875
+ violations.push(violation8("medium", "compound.schema.invalid-symptoms", "bug-track `symptoms` must be a YAML list (schema.yaml track_rules.bug)", "list the observable symptoms under `symptoms:`"));
5876
+ }
5877
+ if (doc.root_cause !== undefined && !isStr(doc.root_cause)) {
5878
+ violations.push(violation8("medium", "compound.schema.invalid-root-cause", "bug-track `root_cause` must be a string (schema.yaml track_rules.bug)", "write the fundamental technical cause as a string"));
5879
+ }
5880
+ if (isStr(doc.resolution_type) && !KNOWLEDGE_RESOLUTION_TYPES.includes(doc.resolution_type)) {
5881
+ violations.push(violation8("medium", "compound.schema.invalid-resolution-type", `resolution_type "${doc.resolution_type}" is not one of the schema.yaml track_rules.bug enum values`, "use code_fix | migration | config_change | test_fix | dependency_update | environment_setup | workflow_improvement | documentation_update | tooling_addition"));
5882
+ }
5883
+ } else if (doc.applies_when !== undefined && !Array.isArray(doc.applies_when)) {
5884
+ violations.push(violation8("low", "compound.schema.invalid-applies-when", "knowledge-track `applies_when` must be a YAML list when present (schema.yaml track_rules.knowledge)", "list the conditions under `applies_when:`"));
5885
+ }
5886
+ }
5887
+ if (doc.plan_id !== undefined && !isStr(doc.plan_id)) {
5888
+ violations.push(violation8("low", "compound.schema.invalid-plan-id", "optional `plan_id` must be a string (schema.yaml optional_fields.plan_id)", "reference the status.json plan id as a string"));
5889
+ }
5890
+ if (doc.tags !== undefined) {
5891
+ if (!Array.isArray(doc.tags)) {
5892
+ violations.push(violation8("low", "compound.schema.invalid-tags", "optional `tags` must be a YAML list (schema.yaml optional_fields.tags)", "list lowercase, hyphen-separated keywords"));
5893
+ } else if (doc.tags.length > 8) {
5894
+ violations.push(violation8("low", "compound.schema.tags-too-many", `tags has ${doc.tags.length} entries — max 8 (schema.yaml optional_fields.tags.max_items)`, "trim the tag list to at most 8 keywords"));
5895
+ }
5896
+ }
5897
+ if (doc.last_updated !== undefined && (!isStr(doc.last_updated) || !DATE_RE3.test(doc.last_updated))) {
5898
+ violations.push(violation8("low", "compound.schema.invalid-last-updated", `last_updated "${String(doc.last_updated)}" must be YYYY-MM-DD (schema.yaml optional_fields.last_updated)`, "use `YYYY-MM-DD`"));
5899
+ }
5900
+ if (doc.related_components !== undefined && !Array.isArray(doc.related_components)) {
5901
+ violations.push(violation8("low", "compound.schema.invalid-related-components", "optional `related_components` must be a YAML list (schema.yaml optional_fields.related_components)", "list the other components involved"));
5902
+ }
5903
+ return { ok: violations.length === 0, violations };
5904
+ }
5905
+ var WALK_SKIP_DIRS = new Set(["node_modules", ".git", "dist"]);
5906
+ function collectKnowledgeDocs(dir) {
5907
+ const docs = [];
5908
+ const stack = [dir];
5909
+ while (stack.length > 0) {
5910
+ const current = stack.pop();
5911
+ let entries;
5912
+ try {
5913
+ entries = readdirSync5(current, { withFileTypes: true });
5914
+ } catch {
5915
+ continue;
5916
+ }
5917
+ for (const entry of entries) {
5918
+ if (entry.isSymbolicLink())
5919
+ continue;
5920
+ const full = join8(current, entry.name);
5921
+ if (entry.isDirectory()) {
5922
+ stack.push(full);
5923
+ } else if (entry.name.endsWith(".md") && entry.name !== "README.md" && entry.name !== "index.md") {
5924
+ docs.push(relative2(dir, full).split(sep).join("/"));
5925
+ }
5926
+ }
5927
+ }
5928
+ return docs.sort();
5929
+ }
5930
+ function normalizeIndexRef(cell) {
5931
+ const link = /\[[^\]]*\]\(([^)]+)\)/.exec(cell);
5932
+ let value = link !== null ? link[1] : cell;
5933
+ value = value.replace(/`/g, "").replace(/^\.\//, "");
5934
+ if (value.startsWith("knowledge/"))
5935
+ value = value.slice("knowledge/".length);
5936
+ return value.trim();
5937
+ }
5938
+ function assertIndexRows(knowledgeDir) {
5939
+ const violations = [];
5940
+ const readmePath = join8(knowledgeDir, "README.md");
5941
+ if (!existsSync5(readmePath)) {
5942
+ violations.push(violation8("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"));
5943
+ return { ok: false, violations };
5944
+ }
5945
+ const docs = collectKnowledgeDocs(knowledgeDir);
5946
+ const rows = new Set;
5947
+ for (const line of readFileSync7(readmePath, "utf8").split(/\r?\n/)) {
5948
+ if (!line.trim().startsWith("|"))
5949
+ continue;
5950
+ const cells = line.split("|").map((c) => c.trim());
5951
+ if (cells.length < 2)
5952
+ continue;
5953
+ const normalized = normalizeIndexRef(cells[1]);
5954
+ if (normalized !== "")
5955
+ rows.add(normalized);
5956
+ }
5957
+ for (const doc of docs) {
5958
+ if (!rows.has(doc)) {
5959
+ violations.push(violation8("medium", "compound.index.missing-row", `knowledge doc "${doc}" has no row in knowledge/README.md index (mstar-compound Phase 6 index obligations)`, `add a row \`| [<title>](${doc}) | <source plan> | <description> | <status> |\` to knowledge/README.md`));
5960
+ }
5961
+ }
5962
+ return { ok: violations.length === 0, violations };
5963
+ }
5964
+ function isFileLikeRoot(root) {
5965
+ return /^[^.]*\.[A-Za-z0-9]{1,10}$/.test(basename4(root));
5966
+ }
5967
+ function scopeGuard(path2, allowedRoots) {
5968
+ const resolved = resolve8(path2);
5969
+ for (const root of allowedRoots) {
5970
+ const r = resolve8(root);
5971
+ if (isFileLikeRoot(r)) {
5972
+ if (resolved === r)
5973
+ return { ok: true, violations: [] };
5974
+ } else if (resolved === r || resolved.startsWith(r + sep)) {
5975
+ return { ok: true, violations: [] };
5976
+ }
5977
+ }
5978
+ return {
5979
+ ok: false,
5980
+ violations: [
5981
+ violation8("medium", "compound.scope.outside", `path "${path2}" is outside the compound-refresh scope (allowed: ${allowedRoots.join(", ")}) — compound-refresh operates only on {HARNESS_DIR}/knowledge/**, {HARNESS_DIR}/knowledge/README.md, <repo-root>/CONCEPTS.md, {HARNESS_DIR}/status.json (mstar-compound-refresh SKILL.md § 产物与操作路径)`, "point the operation at one of the allowed paths")
5982
+ ]
5983
+ };
5984
+ }
5985
+ function violation9(severity, code, message, fix) {
5986
+ return { ok: false, severity, code, message, fix };
5987
+ }
5988
+ var COMMENT_INTRODUCER = "(?:\\/\\/|\\/\\*|#|;|--|\\s\\*)";
5989
+ function findSimplifyMarkers(fileText) {
5990
+ const markers = [];
5991
+ const re = new RegExp(`${COMMENT_INTRODUCER}\\s*simplify\\s*:`, "i");
5992
+ const lines = fileText.split(/\r?\n/);
5993
+ for (let i = 0;i < lines.length; i++) {
5994
+ if (re.test(lines[i]))
5995
+ markers.push({ line: i + 1, text: lines[i].trim() });
5996
+ }
5997
+ return markers;
5998
+ }
5999
+ var REMOVAL_PATH_PATTERNS = [
6000
+ /status\.json/i,
6001
+ /R#\d+/i,
6002
+ /\bresiduals?\b/i,
6003
+ /plans?\/[\w./-]+/i,
6004
+ /\bplans?\s+20\d{6}[-.\w]*/i,
6005
+ /\b(?:tracked|recorded|logged|scheduled|listed|noted)\s+in\s+[\w./-]+/i,
6006
+ /removal\s+path\s*[:=]\s*["'`]?[\w./-]+/i
6007
+ ];
6008
+ function findTemporaryMarkers(fileText) {
6009
+ const markers = [];
6010
+ const violations = [];
6011
+ const re = new RegExp(`${COMMENT_INTRODUCER}\\s*temporary\\b`, "i");
6012
+ const lines = fileText.split(/\r?\n/);
6013
+ for (let i = 0;i < lines.length; i++) {
6014
+ const line = lines[i];
6015
+ if (!re.test(line))
6016
+ continue;
6017
+ const text = line.trim();
6018
+ let removalPath = null;
6019
+ for (const pattern of REMOVAL_PATH_PATTERNS) {
6020
+ const match = pattern.exec(text);
6021
+ if (match) {
6022
+ removalPath = match[0];
6023
+ break;
6024
+ }
6025
+ }
6026
+ markers.push({ line: i + 1, text, removalPath });
6027
+ if (removalPath === null) {
6028
+ violations.push(violation9("medium", "lint.temporary.no-removal-path", `temporary marker at line ${i + 1} records no removal path (plan/status artifact reference) — record one before claiming the task complete (mstar-coding-behavior § Simplification markers)`, 'add a plan/status reference to the marker, e.g. "removal tracked in status.json" or "plan 20260808-slice2 removes this"'));
6029
+ }
6030
+ }
6031
+ return { ok: violations.length === 0, violations, markers };
6032
+ }
6033
+ var TEST_FILE_PATH_RE = /[\w./-]+\.(?:test|spec)\.[a-z0-9]+/i;
6034
+ var TEST_FILE_PHRASE_RE = /\btest files?\b/i;
6035
+ var COMMAND_PROMPT_RE = /^\s*[$>]\s*\S/;
6036
+ var RUNNER_RE = /\b(?:bun|pnpm|npm|yarn|npx|bunx)\s+(?:test|run|exec)\b|\b(?:npx|bunx)\s+[\w./-]+\b|\b(?:tsc|vitest|jest|mocha|pytest)\b|\bgo\s+test\b|\bcargo\s+test\b/i;
6037
+ var OUTPUT_TOKEN_RE = /[✓✔✗✘]|\b(?:PASS|FAIL)\b|\b\d+\s+(?:pass(?:es|ed)?|fail(?:s|ed|ing)?|skipped|tests?|ok)\b|\bok\s+\d+\b|\ball\s+ok\b|exit(?:ed)?\s+(?:with\s+)?(?:code\s+)?\d+/i;
6038
+ function assertSddTddTriple(reportText) {
6039
+ const violations = [];
6040
+ const lines = reportText.split(/\r?\n/);
6041
+ let hasTests = false;
6042
+ let hasCommand = false;
6043
+ let hasOutput = false;
6044
+ for (const line of lines) {
6045
+ if (!hasTests && (TEST_FILE_PATH_RE.test(line) || TEST_FILE_PHRASE_RE.test(line)))
6046
+ hasTests = true;
6047
+ if (!hasCommand && (COMMAND_PROMPT_RE.test(line) || RUNNER_RE.test(line)))
6048
+ hasCommand = true;
6049
+ if (!hasOutput && OUTPUT_TOKEN_RE.test(line))
6050
+ hasOutput = true;
6051
+ if (hasTests && hasCommand && hasOutput)
6052
+ break;
6053
+ }
6054
+ if (!hasTests) {
6055
+ violations.push(violation9("medium", "lint.sdd-tdd.missing-tests", "task report carries no test file reference — the TDD triple needs covering test file(s) (mstar-coding-behavior § Integration Notes; mstar-sdd/references/file-handoffs.md)", 'add a "Covering test file(s): <path>.test.ts" line or a `.test.<ext>` path to the report'));
6056
+ }
6057
+ if (!hasCommand) {
6058
+ violations.push(violation9("medium", "lint.sdd-tdd.missing-command", "task report carries no command — the TDD triple needs the exact command run (mstar-coding-behavior § Integration Notes; mstar-sdd/references/file-handoffs.md)", 'add a "Command run: `bun test <file>`" line to the report'));
6059
+ }
6060
+ if (!hasOutput) {
6061
+ violations.push(violation9("medium", "lint.sdd-tdd.missing-output", "task report carries no output evidence — the TDD triple needs the run output (pass/fail counts or exit code) (mstar-coding-behavior § Integration Notes; mstar-sdd/references/file-handoffs.md)", 'paste the test-run output (e.g. "12 pass / 0 fail") into the report'));
6062
+ }
6063
+ return { ok: violations.length === 0, violations };
6064
+ }
6065
+ var PLACEHOLDER_TOKEN_RE = /\b(TBDs?|TODOs?|TBAs?)\b/gi;
6066
+ var ELLIPSIS_RE = /\.\.\./;
6067
+ var NEGATION_RE = /\b(?:no|not|without|none)\b/i;
6068
+ function stripInlineCode(line) {
6069
+ return line.replace(/`[^`]*`/g, " ");
6070
+ }
6071
+ function planQualityBar(planText) {
6072
+ const findings = [];
6073
+ const violations = [];
6074
+ const lines = planText.split(/\r?\n/);
6075
+ let inFence = false;
6076
+ for (let i = 0;i < lines.length; i++) {
6077
+ const trimmed = lines[i].trim();
6078
+ if (/^```/.test(trimmed) || /^~~~/.test(trimmed)) {
6079
+ inFence = !inFence;
6080
+ continue;
6081
+ }
6082
+ if (inFence)
6083
+ continue;
6084
+ const stripped = stripInlineCode(lines[i]);
6085
+ const segmentStartBefore = (index) => Math.max(stripped.lastIndexOf("(", index - 1), stripped.lastIndexOf("[", index - 1), stripped.lastIndexOf("{", index - 1), stripped.lastIndexOf(".", index - 1), stripped.lastIndexOf(";", index - 1), stripped.lastIndexOf(",", index - 1));
6086
+ let token = null;
6087
+ for (const wordMatch of stripped.matchAll(PLACEHOLDER_TOKEN_RE)) {
6088
+ if (wordMatch.index === undefined)
6089
+ continue;
6090
+ const negated = NEGATION_RE.test(stripped.slice(segmentStartBefore(wordMatch.index) + 1, wordMatch.index));
6091
+ if (!negated) {
6092
+ token = wordMatch[0].replace(/s$/i, "").toUpperCase();
6093
+ break;
6094
+ }
6095
+ }
6096
+ if (token === null && ELLIPSIS_RE.test(stripped)) {
6097
+ token = "...";
6098
+ }
6099
+ if (token !== null) {
6100
+ const text = trimmed.length > 80 ? `${trimmed.slice(0, 77)}...` : trimmed;
6101
+ findings.push({ token, line: i + 1, text });
6102
+ violations.push(violation9("medium", "lint.plan-quality.placeholder", `placeholder token "${token}" at line ${i + 1}: "${text}"`, "replace the placeholder with concrete content before locking the plan (mstar-plan-artifacts/references/plan-quality-bar.md; templates/plan.main.md placeholder scan)"));
6103
+ }
6104
+ }
6105
+ return { ok: violations.length === 0, violations, findings };
6106
+ }
6107
+ var WORKFLOW_VERB_START_RE = /^(?:explains?|describes?|covers?|provides?|walks?|guides?|shows?|lists?|details?|demonstrates?|outlines?|teaches?|summarizes?)\b/i;
6108
+ var PRONOUN_RE = /\bI\b(?!\/)|\b(?:we|you|my|our|your|us)\b/gi;
6109
+ var DESCRIPTION_MAX_WORDS = 120;
6110
+ function lintSkillFrontmatter(frontmatterText) {
6111
+ const violations = [];
6112
+ const fm = parseFrontmatter(frontmatterText);
6113
+ if (fm === null) {
6114
+ violations.push(violation9("medium", "lint.frontmatter.missing", "no YAML frontmatter block found — a skill file must open with a `---` fenced frontmatter (mstar-skill-authoring § Frontmatter Contract)", "add a frontmatter block with `name` and `description` at the top of the file"));
6115
+ return { ok: false, violations };
6116
+ }
6117
+ const name = fm.name ?? "";
6118
+ if (name === "") {
6119
+ violations.push(violation9("medium", "lint.frontmatter.name.missing", "frontmatter `name` is missing — required (mstar-skill-authoring § Frontmatter Contract)", "add `name: <lowercase-hyphen-id>` to the frontmatter"));
6120
+ } else if (!/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(name)) {
6121
+ violations.push(violation9("medium", "lint.frontmatter.name.format", `frontmatter \`name\` must be lowercase-hyphen ("${name}") — e.g. example-skill (mstar-skill-authoring § Frontmatter Contract)`, "rename to a stable lowercase-hyphen id, e.g. `name: example-skill`"));
6122
+ }
6123
+ const description = fm.description ?? "";
6124
+ if (description === "") {
6125
+ violations.push(violation9("medium", "lint.frontmatter.description.missing", "frontmatter `description` is missing — the trigger contract is required (mstar-skill-authoring § Frontmatter Contract)", "add a `description:` that states when the skill loads (symptoms, context, roles, exclusions)"));
6126
+ } else {
6127
+ const stripped = description.replace(/`[^`]*`/g, " ").replace(/'[^']*'/g, " ").replace(/"[^"]*"/g, " ");
6128
+ let pronoun = null;
6129
+ for (const m of stripped.matchAll(PRONOUN_RE)) {
6130
+ if (m[0] === "US")
6131
+ continue;
6132
+ pronoun = m;
6133
+ break;
6134
+ }
6135
+ if (pronoun !== null) {
6136
+ violations.push(violation9("low", "lint.frontmatter.description.person", `description uses first/second-person pronoun "${pronoun[0]}" — keep the trigger contract third person (mstar-skill-authoring § Frontmatter Contract)`, 'rewrite without I/we/you/my/our/your/us, e.g. "Use when the user asks …"'));
6137
+ }
6138
+ const start = description.trim().replace(/^[*_#>]+/, "").replace(/^["'`]+/, "").trim();
6139
+ if (WORKFLOW_VERB_START_RE.test(start)) {
6140
+ violations.push(violation9("low", "lint.frontmatter.description.workflow", 'description reads as a workflow summary ("Explains/Describes/Covers …") — the description is the trigger contract, not a summary of steps (mstar-skill-authoring § Frontmatter Contract)', "describe when to load the skill (symptoms, context, roles, exclusions) instead of summarizing its steps"));
6141
+ } else {
6142
+ const words = description.trim().split(/\s+/).filter(Boolean).length;
6143
+ if (words > DESCRIPTION_MAX_WORDS) {
6144
+ violations.push(violation9("low", "lint.frontmatter.description.workflow", `description is ${words} words — paragraph-length summaries bury the trigger contract (threshold ${DESCRIPTION_MAX_WORDS}, above the longest corpus description at 114 words, mstar-design-md; mstar-skill-authoring § Frontmatter Contract)`, "trim the description to a scannable trigger contract and move detail into the body"));
6145
+ }
6146
+ }
6147
+ }
6148
+ return { ok: violations.length === 0, violations };
6149
+ }
6150
+ function parseFrontmatter(text) {
6151
+ const body = text.replace(/^\uFEFF/, "").replace(/^\s*/, "");
6152
+ const lines = body.split(/\r?\n/);
6153
+ const fields = {};
6154
+ let inBlock = body.startsWith("---");
6155
+ for (let i = inBlock ? 1 : 0;i < lines.length; i++) {
6156
+ const line = lines[i];
6157
+ if (inBlock && line.trim() === "---")
6158
+ break;
6159
+ const keyMatch = /^([A-Za-z_][\w-]*):\s*(.*)$/.exec(line);
6160
+ if (keyMatch) {
6161
+ fields[keyMatch[1].toLowerCase()] = keyMatch[2].trim().replace(/^["']|["']$/g, "");
6162
+ } else if (inBlock && fields.description !== undefined) {
6163
+ fields.description = `${fields.description} ${line.trim()}`.trim();
6164
+ } else if (!inBlock && i >= 10) {
6165
+ break;
6166
+ }
6167
+ }
6168
+ if (Object.keys(fields).length === 0)
6169
+ return null;
6170
+ return fields;
6171
+ }
6172
+ var REQUIRED_STRATEGY_SECTIONS = [
6173
+ "Vision",
6174
+ "What we build",
6175
+ "What we don't build",
6176
+ "Guiding Principles",
6177
+ "Technology Direction",
6178
+ "Decision Log"
6179
+ ];
6180
+ function lintStrategySections(docText) {
6181
+ const violations = [];
6182
+ const headings = new Set;
6183
+ for (const line of docText.split(/\r?\n/)) {
6184
+ const match = /^#{1,6}\s+(.+)$/.exec(line.trim());
6185
+ if (!match)
6186
+ continue;
6187
+ headings.add(match[1].replace(/[*_`]/g, "").trim().toLowerCase());
6188
+ }
6189
+ for (const required of REQUIRED_STRATEGY_SECTIONS) {
6190
+ if (!headings.has(required.toLowerCase())) {
6191
+ violations.push(violation9("medium", "lint.strategy.missing-section", `missing required section "${required}" (mstar-strategy § STRATEGY.md structure)`, "add a `## <Section>` heading; required: Vision, What we build, What we don't build, Guiding Principles, Technology Direction, Decision Log"));
6192
+ }
6193
+ }
6194
+ return { ok: violations.length === 0, violations };
6195
+ }
6196
+ function detectHost(signals2) {
6197
+ const s = new Set(signals2);
6198
+ if (s.has("subagent_type"))
6199
+ return "cursor";
6200
+ if (s.has("question") || s.has("task_subagent"))
6201
+ return "opencode";
6202
+ if (s.has("task_agent_batch") || s.has("ask") || s.has("hub"))
6203
+ return "omp";
6204
+ if (s.has("AgentSwarm"))
6205
+ return "kimi";
6206
+ if (s.has("Agent") || s.has("AskUserQuestion") || s.has("EnterPlanMode") || s.has("TodoWrite"))
6207
+ return "zcode";
6208
+ if (s.has("plan_slash") || s.has("goal") || s.has("functions.*") || s.has("tool_search"))
6209
+ return "codex";
6210
+ return "ambiguous";
6211
+ }
6212
+ function violation11(severity, code, message, fix) {
6213
+ return { ok: false, severity, code, message, fix };
6214
+ }
6215
+ var FIVE_QUESTION_SECTIONS = [
6216
+ { key: "load-order", label: "Load Order", question: "when to load the skill (triggers / exclusions)" },
6217
+ { key: "workflow", label: "Workflow", question: "the order of execution and key decision points" },
6218
+ { key: "decision-rules", label: "Decision Rules", question: "constraints / invariants that must never be violated" },
6219
+ { key: "evidence", label: "Evidence", question: "what a correct result looks like (success criteria / evidence)" },
6220
+ { key: "references", label: "References", question: "additional resources to open when the main path is not enough" }
6221
+ ];
6222
+ var HEADING_RE = /^#{1,6}\s+[^\r\n]+$/;
6223
+ function lintFiveQuestion(bodyText) {
6224
+ const headings = bodyText.split(/\r?\n/).filter((line) => HEADING_RE.test(line)).map((line) => line.replace(/^#{1,6}\s+/, "").trim().toLowerCase());
6225
+ const violations = [];
6226
+ for (const section of FIVE_QUESTION_SECTIONS) {
6227
+ const label = section.label.toLowerCase();
6228
+ const covered = headings.some((heading) => heading.includes(label));
6229
+ if (!covered) {
6230
+ violations.push(violation11("low", `skill-authoring.five-question.${section.key}`, `body does not answer "${section.question}" — no "${section.label}" section (mstar-skill-authoring § Body 必须回答的 5 问 / § 默认 Body 结构)`, `add a "## ${section.label}" section covering ${section.question}`));
6231
+ }
6232
+ }
6233
+ return { ok: violations.length === 0, violations };
6234
+ }
6235
+
6236
+ // src/compass.ts
6237
+ import { readFileSync as readFileSync5 } from "node:fs";
6238
+ function parseCompassFrontmatter(filePath) {
6239
+ const content = readFileSync5(filePath, "utf8");
6240
+ const lines = content.split(/\r?\n/);
6241
+ if (lines[0]?.trim() !== "---") {
6242
+ throw new Error(`no YAML frontmatter fence in ${filePath} (expected first line "---")`);
6243
+ }
6244
+ const end = lines.indexOf("---", 1);
6245
+ if (end === -1) {
6246
+ throw new Error(`unterminated YAML frontmatter in ${filePath} (no closing "---")`);
6247
+ }
6248
+ const doc = {};
6249
+ let listKey = null;
6250
+ for (let i = 1;i < end; i += 1) {
6251
+ const line = lines[i] ?? "";
6252
+ if (!line.trim() || line.trim().startsWith("#"))
6253
+ continue;
6254
+ if (listKey !== null && /^\s*-\s+/.test(line)) {
6255
+ const item = line.replace(/^\s*-\s+/, "").trim().replace(/^["']|["']$/g, "");
6256
+ if (!Array.isArray(doc[listKey]))
6257
+ doc[listKey] = [];
6258
+ doc[listKey].push(item);
6259
+ continue;
6260
+ }
6261
+ listKey = null;
6262
+ const kv = line.match(/^([A-Za-z_][A-Za-z0-9_-]*):\s*(.*)$/);
6263
+ if (!kv) {
6264
+ throw new Error(`unsupported frontmatter line in ${filePath}: ${JSON.stringify(line)}`);
6265
+ }
6266
+ const value = kv[2].trim();
6267
+ doc[kv[1]] = value === "" ? null : /^\[.*\]$/.test(value) ? parseFlowArray(value, filePath) : value.replace(/^["']|["']$/g, "");
6268
+ listKey = value === "" ? kv[1] : null;
6269
+ }
6270
+ return doc;
6271
+ }
6272
+ function parseFlowArray(raw, filePath) {
6273
+ const inner = raw.slice(1, -1);
6274
+ if (/[[\]]/.test(inner)) {
6275
+ throw new Error(`nested flow-style array in ${filePath}: ${JSON.stringify(raw)} — only flat scalar items are supported (e.g. [a, b])`);
6276
+ }
6277
+ let quote = null;
6278
+ for (const ch of inner) {
6279
+ if (ch === '"' || ch === "'") {
6280
+ if (quote === null)
6281
+ quote = ch;
6282
+ else if (quote === ch)
6283
+ quote = null;
6284
+ } else if (ch === "," && quote !== null) {
6285
+ throw new Error(`ambiguous flow-style array in ${filePath}: ${JSON.stringify(raw)} — quoted item containing comma cannot be split unambiguously (flat scalar items only)`);
6286
+ }
6287
+ }
6288
+ if (quote !== null) {
6289
+ throw new Error(`unterminated ${quote} quote in flow-style array in ${filePath}: ${JSON.stringify(raw)}`);
6290
+ }
6291
+ const items = [];
6292
+ for (const part of inner.split(",")) {
6293
+ const item = part.trim().replace(/^["']|["']$/g, "");
6294
+ if (item === "")
6295
+ continue;
6296
+ items.push(item);
6297
+ }
6298
+ return items;
6299
+ }
6300
+
6301
+ // ../engine/dist/engine.js
6302
+ import { existsSync as existsSync4, mkdirSync as mkdirSync6, readFileSync as readFileSync8, renameSync as renameSync2, unlinkSync as unlinkSync3, writeFileSync as writeFileSync5 } from "node:fs";
6303
+ import { randomUUID as randomUUID2 } from "node:crypto";
6304
+ import { basename as basename5, dirname as dirname5, join as join6, resolve as resolve9 } from "node:path";
6305
+ import { fileURLToPath } from "node:url";
6306
+ import { dirname as dirname32, isAbsolute as isAbsolute22, join as join32, resolve as resolve32 } from "node:path";
6307
+ import { AsyncLocalStorage as AsyncLocalStorage3 } from "node:async_hooks";
6308
+ function readJson2(filePath) {
6309
+ if (!existsSync4(filePath))
6310
+ return {};
6311
+ const content = readFileSync8(filePath, "utf8").trim();
6312
+ if (!content)
6313
+ return {};
6314
+ try {
6315
+ return JSON.parse(content);
6316
+ } catch (error) {
6317
+ throw new Error(`Invalid JSON in ${filePath}: ${error.message}`);
6318
+ }
6319
+ }
6320
+ function writeJson2(filePath, value) {
6321
+ const parent = dirname5(filePath);
6322
+ mkdirSync6(parent, { recursive: true });
6323
+ const tmp = join6(parent, `.${basename5(filePath)}.${process.pid}.${randomUUID2()}.tmp`);
6324
+ try {
6325
+ writeFileSync5(tmp, `${JSON.stringify(value, null, 2)}
6326
+ `, "utf8");
6327
+ renameSync2(tmp, filePath);
6328
+ } catch (error) {
6329
+ try {
6330
+ unlinkSync3(tmp);
6331
+ } catch {}
6332
+ throw error;
6333
+ }
6334
+ }
6335
+ function resolveProjectRoot(startDir = process.cwd()) {
6336
+ const start = resolve9(startDir);
6337
+ let dir = start;
6338
+ for (;; ) {
6339
+ if (existsSync4(join6(dir, "package.json")) || existsSync4(join6(dir, "bun.lock")))
6340
+ return dir;
6341
+ const parent = dirname5(dir);
6342
+ if (parent === dir)
6343
+ return start;
6344
+ dir = parent;
6345
+ }
6346
+ }
6347
+ function findRootPackageJson(startDir) {
6348
+ let dir = startDir;
6349
+ for (;; ) {
6350
+ const candidate = resolve9(dir, "package.json");
6351
+ try {
6352
+ const pkg = JSON.parse(readFileSync8(candidate, "utf8"));
6353
+ if (pkg.name === "morning-star")
6354
+ return candidate;
6355
+ } catch {}
6356
+ const parent = dirname5(dir);
6357
+ if (parent === dir)
6358
+ return null;
6359
+ dir = parent;
6360
+ }
6361
+ }
6362
+ function harnessVersionFrom(moduleDir) {
6363
+ const ownManifest = join6(moduleDir, "..", "package.json");
6364
+ try {
6365
+ const pkg = JSON.parse(readFileSync8(ownManifest, "utf8"));
6366
+ if (typeof pkg.version === "string" && pkg.version !== "")
6367
+ return pkg.version;
6368
+ } catch {}
6369
+ const root = findRootPackageJson(moduleDir);
6370
+ if (!root)
6371
+ return "0.0.0";
6372
+ try {
6373
+ const pkg = JSON.parse(readFileSync8(root, "utf8"));
6374
+ return pkg.version || "0.0.0";
6375
+ } catch {
6376
+ return "0.0.0";
6377
+ }
6378
+ }
6379
+ function readHarnessVersion() {
6380
+ return harnessVersionFrom(dirname5(fileURLToPath(import.meta.url)));
6381
+ }
6382
+ var GITIGNORE_SNIPPET2 = `# Morning Star harness (.mstar/)
6383
+ # Principle: process stays local; results are shared with the team.
6384
+ # Ignored (process / coordination):
6385
+ .mstar/archived/
6386
+ .mstar/iterations/
6387
+ .mstar/plans/
6388
+ .mstar/sdd/
6389
+ .mstar/notes.json
6390
+ .mstar/status.json
6391
+ # Tracked (results): .mstar/AGENTS.md, .mstar/knowledge/, .mstar/specs/
6392
+ `;
6393
+ var GITIGNORE_SNIPPET_AGENTS2 = `# Morning Star harness (.agents/) — legacy
6394
+ .agents/archived/
6395
+ .agents/iterations/
6396
+ .agents/plans/
6397
+ .agents/sdd/
6398
+ .agents/notes.json
6399
+ .agents/status.json
6400
+ # Tracked (results): .agents/AGENTS.md, .agents/knowledge/, .agents/specs/
6401
+ `;
6402
+ var GITIGNORE_PROCESS_ENTRIES2 = GITIGNORE_SNIPPET2.split(`
6403
+ `).filter((line) => line.startsWith(".mstar/")).map((line) => line.trim());
6404
+ var GITIGNORE_PROCESS_ENTRIES_AGENTS2 = GITIGNORE_SNIPPET_AGENTS2.split(`
6405
+ `).filter((line) => line.startsWith(".agents/")).map((line) => line.trim());
6406
+ function isPlainObject4(value) {
6407
+ return typeof value === "object" && value !== null && !Array.isArray(value);
6408
+ }
6409
+ function violation(severity, code, message, fix) {
6410
+ return { ok: false, severity, code, message, fix };
6411
+ }
6412
+ function validateNonEmptyString(violations, value, field, missingCode, invalidCode) {
6413
+ if (value === undefined) {
6414
+ violations.push(violation("high", missingCode, `missing required field: ${field}`));
6415
+ } else if (typeof value !== "string" || value.trim() === "") {
6416
+ violations.push(violation("medium", invalidCode, `${field} must be a non-empty string`));
6417
+ }
6418
+ }
6419
+ var DATE_PART2 = String.raw`\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12]\d|3[01])`;
6420
+ var RFC3339_Z_RE2 = new RegExp(String.raw`^${DATE_PART2}T\d{2}:\d{2}:\d{2}(\.\d+)?Z$`);
6421
+ var DATE_ONLY_RE2 = new RegExp(String.raw`^${DATE_PART2}$`);
6422
+ function isValidClaimedAt(value) {
6423
+ return typeof value === "string" && (RFC3339_Z_RE2.test(value) || DATE_ONLY_RE2.test(value));
6424
+ }
6425
+ function validateExecutionLease(lease) {
6426
+ const violations = [];
6427
+ if (!isPlainObject4(lease)) {
6428
+ return {
6429
+ ok: false,
6430
+ violations: [
6431
+ violation("high", "lease.execution-lease.invalid", "execution_lease must be an object — null and tombstone objects are invalid; writers delete the key on release")
6432
+ ]
6433
+ };
6434
+ }
6435
+ validateNonEmptyString(violations, lease.holder, "holder", "lease.execution-lease.missing-holder", "lease.execution-lease.invalid-holder");
6436
+ if (lease.claimed_at === undefined) {
6437
+ violations.push(violation("high", "lease.execution-lease.missing-claimed-at", "missing required field: claimed_at"));
6438
+ } else if (!isValidClaimedAt(lease.claimed_at)) {
6439
+ violations.push(violation("medium", "lease.execution-lease.invalid-claimed-at", "claimed_at must be an RFC 3339 UTC timestamp with explicit Z (e.g. 2026-07-22T02:30:00Z) or a YYYY-MM-DD date"));
6440
+ }
6441
+ if (lease.worktree_path === undefined) {
6442
+ violations.push(violation("high", "lease.execution-lease.missing-worktree-path", "missing required field: worktree_path"));
6443
+ } else if (typeof lease.worktree_path !== "string" || lease.worktree_path.trim() === "") {
6444
+ violations.push(violation("medium", "lease.execution-lease.invalid-worktree-path", "worktree_path must be a non-empty string"));
6445
+ } else if (!isAbsolute22(lease.worktree_path)) {
6446
+ violations.push(violation("medium", "lease.execution-lease.invalid-worktree-path", "worktree_path must be an absolute path — it identifies the dedicated feature-worktree root (and MUST differ from metadata.control_worktree_path)"));
6447
+ }
6448
+ validateNonEmptyString(violations, lease.working_branch, "working_branch", "lease.execution-lease.missing-working-branch", "lease.execution-lease.invalid-working-branch");
6449
+ if (lease.session_label !== undefined && typeof lease.session_label !== "string") {
6450
+ violations.push(violation("medium", "lease.execution-lease.invalid-session-label", "session_label must be a string (display only — never used for ownership comparison)"));
6451
+ }
6452
+ return { ok: violations.length === 0, violations };
6453
+ }
6454
+ function planExecutionLeaseLocations(row) {
6455
+ const meta = row.metadata;
6456
+ const metadataLease = meta && typeof meta === "object" && !Array.isArray(meta) ? meta.execution_lease : undefined;
6457
+ return { row: row.execution_lease, metadata: metadataLease };
6458
+ }
6459
+ function verifyPlanExecutionLease(row, planId) {
6460
+ const { row: rowLease, metadata: metadataLease } = planExecutionLeaseLocations(row);
6461
+ const lease = rowLease !== undefined ? rowLease : metadataLease;
6462
+ if (lease === undefined) {
6463
+ if (row.status === "InProgress") {
6464
+ return {
6465
+ ok: false,
6466
+ violations: [
6467
+ violation("high", "lease.verify.orphan", "plan is InProgress without an execution_lease — orphan: STOP, no writable dispatch until recovery (status-and-residuals.md § Orphan recovery)")
6468
+ ]
6469
+ };
6470
+ }
6471
+ return {
6472
+ ok: false,
6473
+ violations: [
6474
+ violation("high", "lease.verify.missing", `plan ${planId} has no execution_lease (neither plans[].execution_lease nor legacy plans[].metadata.execution_lease)`)
6475
+ ]
6476
+ };
6477
+ }
6478
+ const violations = [];
6479
+ if (rowLease !== undefined && metadataLease !== undefined) {
6480
+ violations.push(violation("high", "lease.verify.dual-write", "execution_lease present in BOTH plans[].execution_lease (SSOT) and plans[].metadata.execution_lease — the row-level lease wins; delete the metadata copy to remove the dual write"));
6481
+ } else if (rowLease === undefined) {
6482
+ violations.push(violation("high", "lease.verify.non-ssot-location", "execution_lease found only under plans[].metadata.execution_lease — the SSOT location is plans[].execution_lease; the metadata location is a legacy/hand-written read-compat fallback, not equivalent to SSOT success (migrate the lease to the plan row)"));
6483
+ }
6484
+ violations.push(...validateExecutionLease(lease).violations);
6485
+ return { ok: violations.length === 0, violations, lease };
6486
+ }
6487
+ var heldLockDirs2 = new AsyncLocalStorage3;
6488
+ var GIT_CAPTURE_MAX_BYTES2 = 64 * 1024 * 1024;
6489
+ var WALK_SKIP_DIRS2 = new Set(["node_modules", ".git", "dist"]);
6490
+ // src/agent-plugins.ts
6491
+ import fs from "node:fs";
6492
+ import path3 from "node:path";
6493
+
6494
+ // src/utils.ts
6495
+ import path2 from "node:path";
6496
+ function ensureObject(value) {
6497
+ if (value && typeof value === "object" && !Array.isArray(value))
6498
+ return value;
6499
+ return {};
6500
+ }
6501
+ function resolveProjectRoot2() {
6502
+ const candidate = process.env.MSTAR_CLI_PROJECT_ROOT || process.env.INIT_CWD || process.env.PWD;
6503
+ if (candidate && candidate.trim())
6504
+ return path2.resolve(candidate);
6505
+ return resolveProjectRoot();
6506
+ }
6507
+
6508
+ // src/agent-plugins.ts
6509
+ var PLUGIN_SCHEMA_URL = "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json";
6510
+ var MCP_SCHEMA_URL = "https://agent-plugins.org/schemas/1.0.0/mcp.schema.json";
6511
+ var PLUGIN_TOP_LEVEL_FIELDS = {
6512
+ $schema: true,
6513
+ name: true,
6514
+ version: true,
6515
+ description: true,
6516
+ author: true,
6517
+ homepage: true,
6518
+ repository: true,
6519
+ license: true,
6520
+ keywords: true,
6521
+ extensions: true
6522
+ };
6523
+ var PLUGIN_NAME_PATTERN = /^(?!.*(?:--|\.\.))[a-z0-9](?:[a-z0-9.-]*[a-z0-9])?$/;
6524
+ function stripMcpPathPrefix(raw) {
6525
+ if (raw.startsWith("./"))
6526
+ return raw.slice(2);
6527
+ if (raw.startsWith("${PLUGIN_ROOT}/"))
6528
+ return raw.slice("${PLUGIN_ROOT}/".length);
6529
+ if (raw.startsWith("${PLUGIN_DATA}/"))
6530
+ return raw.slice("${PLUGIN_DATA}/".length);
6531
+ if (raw === "${PLUGIN_ROOT}" || raw === "${PLUGIN_DATA}")
6532
+ return "";
6533
+ return null;
6534
+ }
6535
+ function escapesPluginRoot(remainder) {
6536
+ const normalized = path3.posix.normalize(remainder);
6537
+ return normalized.startsWith("..") || path3.posix.isAbsolute(normalized);
6538
+ }
6539
+ var SKILL_NAME_PATTERN = /^(?!.*--)[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/;
6540
+ var HTTP_HEADER_NAME_PATTERN = /^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/;
6541
+ var MCP_SERVER_TYPES = {
6542
+ stdio: true,
6543
+ "streamable-http": true,
6544
+ sse: true
6545
+ };
6546
+ var STDIO_FIELDS = { type: true, command: true, args: true, env: true, cwd: true };
6547
+ var REMOTE_FIELDS = { type: true, url: true, headers: true };
6548
+ var AUTHOR_FIELDS = { name: true, email: true, url: true };
6549
+ function describeType(value) {
6550
+ if (value === null)
6551
+ return "null";
6552
+ if (Array.isArray(value))
6553
+ return "array";
6554
+ return typeof value;
6555
+ }
6556
+ function isPlainObject6(value) {
6557
+ return typeof value === "object" && value !== null && !Array.isArray(value);
6558
+ }
6559
+ function parseScalar3(raw) {
6560
+ const trimmed = raw.trim();
6561
+ if (trimmed.length >= 2 && (trimmed.startsWith('"') && trimmed.endsWith('"') || trimmed.startsWith("'") && trimmed.endsWith("'"))) {
6562
+ return trimmed.slice(1, -1);
6563
+ }
6564
+ return trimmed;
6565
+ }
6566
+ function parseFrontmatter2(filePath) {
6567
+ const content = fs.readFileSync(filePath, "utf8");
6568
+ const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n/);
6569
+ if (!match)
6570
+ return null;
6571
+ const result = {};
6572
+ for (const line of match[1].split(/\r?\n/)) {
6573
+ const field = line.match(/^([A-Za-z0-9_-]+):\s*(.*)$/);
6574
+ if (!field)
6575
+ continue;
6576
+ result[field[1]] = parseScalar3(field[2]);
6577
+ }
6578
+ return result;
6579
+ }
6580
+ function isValidMcpUrl(raw) {
6581
+ let parsed;
6582
+ try {
6583
+ parsed = new URL(raw);
6584
+ } catch {
6585
+ return false;
6586
+ }
6587
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:")
6588
+ return false;
6589
+ if (!parsed.hostname)
6590
+ return false;
6591
+ if (parsed.username || parsed.password || parsed.hash)
6592
+ return false;
6593
+ const host = parsed.hostname;
6594
+ const isLoopback = host === "localhost" || host === "::1" || host === "[::1]" || /^127\.\d+\.\d+\.\d+$/.test(host);
6595
+ if (!isLoopback && parsed.protocol !== "https:")
6596
+ return false;
6597
+ return true;
6598
+ }
6599
+ function validateManifest(manifest, errors2, warnings) {
6600
+ if (!isPlainObject6(manifest)) {
6601
+ errors2.push("plugin.json: manifest must be a JSON object");
6602
+ return;
6603
+ }
6604
+ const doc = manifest;
6605
+ for (const key of Object.keys(doc)) {
6606
+ if (!Object.hasOwn(PLUGIN_TOP_LEVEL_FIELDS, key)) {
6607
+ warnings.push(`plugin.json: unknown top-level field "${key}" (ignored; client-specific data belongs under "extensions")`);
6608
+ }
6609
+ }
6610
+ const schema = doc["$schema"];
6611
+ if (typeof schema !== "string") {
6612
+ errors2.push(`plugin.json: "$schema" is required and must be the string ${PLUGIN_SCHEMA_URL}`);
6613
+ } else if (schema !== PLUGIN_SCHEMA_URL) {
6614
+ errors2.push(`plugin.json: unsupported "$schema" ${JSON.stringify(schema)} (expected ${PLUGIN_SCHEMA_URL})`);
6615
+ }
6616
+ const name = doc.name;
6617
+ if (typeof name !== "string" || name.length === 0) {
6618
+ errors2.push('plugin.json: "name" is required and must be a non-empty string');
6619
+ } else {
6620
+ if (name.length > 64) {
6621
+ errors2.push(`plugin.json: "name" must be 1-64 characters (got ${name.length})`);
6622
+ }
6623
+ if (!PLUGIN_NAME_PATTERN.test(name)) {
6624
+ errors2.push(`plugin.json: "name" ${JSON.stringify(name)} violates Agent Plugins name rules ` + `(lowercase alphanumerics, hyphens, periods; no "--" or ".."; must start and end alphanumeric)`);
6625
+ }
6626
+ }
6627
+ for (const field of ["version", "description", "homepage", "repository", "license"]) {
6628
+ const value = doc[field];
6629
+ if (value === undefined)
6630
+ continue;
6631
+ if (typeof value !== "string") {
6632
+ errors2.push(`plugin.json: "${field}" must be a string (got ${describeType(value)})`);
6633
+ }
6634
+ }
6635
+ if (doc.author !== undefined) {
6636
+ if (!isPlainObject6(doc.author)) {
6637
+ errors2.push('plugin.json: "author" must be an object with optional string fields name/email/url');
6638
+ } else {
6639
+ const author = doc.author;
6640
+ for (const key of Object.keys(author)) {
6641
+ if (!Object.hasOwn(AUTHOR_FIELDS, key)) {
6642
+ errors2.push(`plugin.json: "author" has unknown field "${key}" (only name, email, url are allowed)`);
6643
+ }
6644
+ }
6645
+ for (const key of ["name", "email", "url"]) {
6646
+ const value = author[key];
6647
+ if (value !== undefined && typeof value !== "string") {
6648
+ errors2.push(`plugin.json: "author.${key}" must be a string (got ${describeType(value)})`);
6649
+ }
6650
+ }
6651
+ }
6652
+ }
6653
+ if (doc.keywords !== undefined) {
6654
+ if (!Array.isArray(doc.keywords) || doc.keywords.some((entry) => typeof entry !== "string")) {
6655
+ errors2.push('plugin.json: "keywords" must be an array of strings');
6656
+ }
6657
+ }
6658
+ if (doc.extensions !== undefined) {
6659
+ if (!isPlainObject6(doc.extensions)) {
6660
+ warnings.push('plugin.json: "extensions" is not an object — ignored');
6661
+ } else {
6662
+ for (const [namespace, value] of Object.entries(doc.extensions)) {
6663
+ if (!isPlainObject6(value)) {
6664
+ warnings.push(`plugin.json: "extensions.${namespace}" is not an object — ignored`);
6665
+ }
6666
+ }
6667
+ }
6668
+ }
6669
+ }
6670
+ function validateMcpServer(name, entry, errors2) {
6671
+ const prefix = `mcp.json: mcpServers.${name}`;
6672
+ if (!isPlainObject6(entry)) {
6673
+ errors2.push(`${prefix} must be an object`);
6674
+ return;
6675
+ }
6676
+ const server = entry;
6677
+ const type = server.type;
6678
+ if (typeof type !== "string" || !Object.hasOwn(MCP_SERVER_TYPES, type)) {
6679
+ errors2.push(`${prefix}: "type" must be one of "stdio" | "streamable-http" | "sse" (got ${JSON.stringify(type)})`);
6680
+ return;
6681
+ }
6682
+ if (type === "stdio") {
6683
+ for (const key of Object.keys(server)) {
6684
+ if (!Object.hasOwn(STDIO_FIELDS, key)) {
6685
+ errors2.push(`${prefix}: unknown field "${key}" for stdio server (allowed: type, command, args, env, cwd)`);
6686
+ }
6687
+ }
6688
+ const command = server.command;
6689
+ if (typeof command !== "string" || command.length === 0) {
6690
+ errors2.push(`${prefix}: "command" is required and must be a non-empty string`);
6691
+ } else {
6692
+ if (/\s/.test(command)) {
6693
+ errors2.push(`${prefix}: "command" must be a single executable token, not a shell command string`);
6694
+ } else if (command.includes("/") && !command.startsWith("./")) {
6695
+ errors2.push(`${prefix}: "command" must be a bare executable name or a plugin-relative path beginning with "./"`);
6696
+ } else if (command.startsWith("./") && escapesPluginRoot(command.slice(2))) {
6697
+ errors2.push(`${prefix}: "command" must remain within the plugin root (got "${command}")`);
6698
+ }
6699
+ }
6700
+ if (server.args !== undefined) {
6701
+ if (!Array.isArray(server.args) || server.args.some((arg) => typeof arg !== "string")) {
6702
+ errors2.push(`${prefix}: "args" must be an array of strings`);
6703
+ }
6704
+ }
6705
+ if (server.env !== undefined) {
6706
+ if (!isPlainObject6(server.env)) {
6707
+ errors2.push(`${prefix}: "env" must be an object of strings`);
6708
+ } else {
6709
+ for (const [key, value] of Object.entries(server.env)) {
6710
+ if (key === "PLUGIN_ROOT" || key === "PLUGIN_DATA") {
6711
+ errors2.push(`${prefix}: "env" must not set reserved variable "${key}" (clients supply it themselves)`);
6712
+ }
6713
+ if (typeof value !== "string") {
6714
+ errors2.push(`${prefix}: "env.${key}" must be a string`);
6715
+ }
6716
+ }
6717
+ }
6718
+ }
6719
+ if (server.cwd !== undefined) {
6720
+ if (typeof server.cwd !== "string") {
6721
+ errors2.push(`${prefix}: "cwd" must be a string`);
6722
+ } else {
6723
+ const remainder = stripMcpPathPrefix(server.cwd);
6724
+ if (remainder === null) {
6725
+ errors2.push(`${prefix}: "cwd" must be "./…", "${"${PLUGIN_ROOT}"}…", or "${"${PLUGIN_DATA}"}…"`);
6726
+ } else if (escapesPluginRoot(remainder)) {
6727
+ errors2.push(`${prefix}: "cwd" must remain within the plugin root (got "${server.cwd}")`);
6728
+ }
6729
+ }
6730
+ }
6731
+ return;
6732
+ }
6733
+ for (const key of Object.keys(server)) {
6734
+ if (!Object.hasOwn(REMOTE_FIELDS, key)) {
6735
+ errors2.push(`${prefix}: unknown field "${key}" for ${type} server (allowed: type, url, headers)`);
6736
+ }
6737
+ }
6738
+ const url = server.url;
6739
+ if (typeof url !== "string" || url.length === 0) {
6740
+ errors2.push(`${prefix}: "url" is required and must be a non-empty string`);
6741
+ } else if (!isValidMcpUrl(url)) {
6742
+ errors2.push(`${prefix}: "url" must be an absolute http(s) URL without user info or fragment; non-loopback endpoints must use https`);
6743
+ }
6744
+ if (server.headers !== undefined) {
6745
+ if (!isPlainObject6(server.headers)) {
6746
+ errors2.push(`${prefix}: "headers" must be an object of strings`);
6747
+ } else {
6748
+ const seen = new Set;
6749
+ for (const [key, value] of Object.entries(server.headers)) {
6750
+ if (typeof value !== "string") {
6751
+ errors2.push(`${prefix}: "headers.${key}" must be a string`);
6752
+ continue;
6753
+ }
6754
+ if (value.includes("\r") || value.includes(`
6755
+ `)) {
6756
+ errors2.push(`${prefix}: "headers.${key}" value must be a single HTTP header value`);
6757
+ }
6758
+ if (!HTTP_HEADER_NAME_PATTERN.test(key)) {
6759
+ errors2.push(`${prefix}: "headers.${key}" is not a valid HTTP header name`);
6760
+ } else {
6761
+ const lower = key.toLowerCase();
6762
+ if (seen.has(lower)) {
6763
+ errors2.push(`${prefix}: header "${key}" is duplicated (case-insensitive)`);
6764
+ }
6765
+ seen.add(lower);
6766
+ }
6767
+ }
6768
+ }
6769
+ }
6770
+ }
6771
+ function validateMcp(root, manifestSchema, errors2) {
6772
+ const mcpPath = path3.join(root, "mcp.json");
6773
+ if (!fs.existsSync(mcpPath))
6774
+ return;
6775
+ let parsed;
6776
+ try {
6777
+ parsed = readJson2(mcpPath);
6778
+ } catch (error) {
6779
+ errors2.push(`mcp.json: ${error.message}`);
6780
+ return;
6781
+ }
6782
+ if (!isPlainObject6(parsed)) {
6783
+ errors2.push("mcp.json: configuration must be a JSON object");
6784
+ return;
6785
+ }
6786
+ const doc = parsed;
6787
+ for (const key of Object.keys(doc)) {
6788
+ if (key !== "$schema" && key !== "mcpServers") {
6789
+ errors2.push(`mcp.json: unknown top-level field "${key}" (only "$schema" and "mcpServers" allowed)`);
6790
+ }
6791
+ }
6792
+ const schema = doc["$schema"];
6793
+ if (typeof schema !== "string") {
6794
+ errors2.push(`mcp.json: "$schema" is required and must be the string ${MCP_SCHEMA_URL}`);
6795
+ } else if (schema !== MCP_SCHEMA_URL) {
6796
+ errors2.push(`mcp.json: unsupported "$schema" ${JSON.stringify(schema)} (expected ${MCP_SCHEMA_URL})`);
6797
+ } else {
6798
+ const manifestVersion = typeof manifestSchema === "string" ? manifestSchema.match(/^https:\/\/agent-plugins\.org\/schemas\/([^/]+)\/plugin\.schema\.json$/)?.[1] : undefined;
6799
+ const mcpVersion = schema.match(/^https:\/\/agent-plugins\.org\/schemas\/([^/]+)\/mcp\.schema\.json$/)?.[1];
6800
+ if (manifestVersion && mcpVersion && manifestVersion !== mcpVersion) {
6801
+ errors2.push(`mcp.json: "$schema" targets Agent Plugins ${mcpVersion} but plugin.json targets ${manifestVersion} (versions must match)`);
6802
+ }
6803
+ }
6804
+ const servers = doc.mcpServers;
6805
+ if (!isPlainObject6(servers)) {
6806
+ errors2.push('mcp.json: "mcpServers" is required and must be an object');
6807
+ return;
6808
+ }
6809
+ for (const [serverName, entry] of Object.entries(servers)) {
6810
+ validateMcpServer(serverName, entry, errors2);
6811
+ }
6812
+ }
6813
+ function validateSkills(root, errors2, warnings) {
6814
+ const skillsPath = path3.join(root, "skills");
6815
+ try {
6816
+ if (!fs.existsSync(skillsPath))
6817
+ return;
6818
+ if (!fs.statSync(skillsPath).isDirectory()) {
6819
+ errors2.push("skills: skills/ is not a directory (component type invalid)");
6820
+ return;
6821
+ }
6822
+ const entries = fs.readdirSync(skillsPath, { withFileTypes: true });
6823
+ const realRoot = fs.realpathSync(root);
6824
+ for (const entry of entries) {
6825
+ if (!entry.isDirectory() && !entry.isSymbolicLink())
6826
+ continue;
6827
+ const skillDir = entry.name;
6828
+ let realSkillPath;
6829
+ try {
6830
+ realSkillPath = fs.realpathSync(path3.join(skillsPath, skillDir));
6831
+ } catch (error) {
6832
+ warnings.push(`skills: ${skillDir}/ cannot be resolved (${error.message}; skill skipped)`);
6833
+ continue;
6834
+ }
6835
+ const relative3 = path3.relative(realRoot, realSkillPath);
6836
+ if (relative3 === ".." || relative3.startsWith(`..${path3.sep}`) || path3.isAbsolute(relative3)) {
6837
+ warnings.push(`skills: ${skillDir}/ resolves outside the plugin root (${realSkillPath}; skill skipped)`);
6838
+ continue;
6839
+ }
6840
+ const skillMdPath = path3.join(skillsPath, skillDir, "SKILL.md");
6841
+ if (!fs.existsSync(skillMdPath) || !fs.statSync(skillMdPath).isFile()) {
6842
+ warnings.push(`skills: ${skillDir}/ has no SKILL.md (directory is not a skill; ignored)`);
6843
+ continue;
6844
+ }
6845
+ const frontmatter = parseFrontmatter2(skillMdPath);
6846
+ if (!frontmatter) {
6847
+ warnings.push(`skills: ${skillDir}/SKILL.md is missing YAML frontmatter (name and description are required; skill skipped)`);
6848
+ continue;
6849
+ }
6850
+ const skillName = frontmatter.name;
6851
+ const problems = [];
6852
+ if (skillName !== skillDir) {
6853
+ problems.push(`frontmatter "name" ${JSON.stringify(skillName)} must equal the directory name "${skillDir}"`);
6854
+ } else if (!SKILL_NAME_PATTERN.test(skillName)) {
6855
+ problems.push(`frontmatter "name" violates Agent Skills name rules ` + `(lowercase alphanumerics and hyphens, no "--", no leading or trailing hyphen)`);
6856
+ }
6857
+ if (typeof skillName === "string" && skillName.length > 64) {
6858
+ problems.push(`frontmatter "name" must be at most 64 characters (got ${skillName.length})`);
6859
+ }
6860
+ const description = frontmatter.description;
6861
+ if (typeof description !== "string" || description.trim().length === 0) {
6862
+ problems.push(`frontmatter "description" is required and must be non-empty`);
6863
+ } else if (description.length > 1024) {
6864
+ problems.push(`frontmatter "description" must be at most 1024 characters (got ${description.length})`);
6865
+ }
6866
+ for (const problem of problems) {
6867
+ warnings.push(`skills: ${skillDir}/SKILL.md ${problem} (skill skipped)`);
6868
+ }
6869
+ }
6870
+ } catch (error) {
6871
+ errors2.push(`skills: ${error.message}`);
6872
+ }
6873
+ }
6874
+ function validateAgentPlugin(root) {
6875
+ const errors2 = [];
6876
+ const warnings = [];
6877
+ const manifestPath = path3.join(root, "plugin.json");
6878
+ if (!fs.existsSync(manifestPath)) {
6879
+ errors2.push(`plugin.json: manifest not found at ${manifestPath} (plugin root must contain plugin.json)`);
6880
+ return { ok: false, errors: errors2, warnings };
6881
+ }
6882
+ let manifest;
6883
+ try {
6884
+ manifest = readJson2(manifestPath);
6885
+ } catch (error) {
6886
+ errors2.push(`plugin.json: ${error.message}`);
6887
+ return { ok: false, errors: errors2, warnings };
6888
+ }
6889
+ validateManifest(manifest, errors2, warnings);
6890
+ const manifestSchema = isPlainObject6(manifest) ? manifest["$schema"] : undefined;
6891
+ validateMcp(root, manifestSchema, errors2);
6892
+ validateSkills(root, errors2, warnings);
6893
+ return { ok: errors2.length === 0, errors: errors2, warnings };
6894
+ }
6895
+
6896
+ // src/constants.ts
6897
+ var ALL_ROLES = [
6898
+ "project-manager",
6899
+ "architect",
6900
+ "product-manager",
6901
+ "prompt-engineer",
6902
+ "fullstack-dev",
6903
+ "fullstack-dev-2",
6904
+ "frontend-dev",
6905
+ "qc-specialist",
6906
+ "qc-specialist-2",
6907
+ "qc-specialist-3",
6908
+ "qa-engineer",
6909
+ "ops-engineer",
6910
+ "writing-specialist"
6911
+ ];
6912
+ var ROLE_GROUPS = {
6913
+ strategic: ["architect", "product-manager", "prompt-engineer"],
6914
+ dev: ["fullstack-dev", "fullstack-dev-2", "frontend-dev"],
6915
+ qc: ["qc-specialist", "qc-specialist-2", "qc-specialist-3"]
6916
+ };
6917
+
6918
+ // src/assignment.ts
6919
+ function pickByOrder(roleIds, selectedModels) {
6920
+ const assignments = {};
6921
+ for (let i = 0;i < roleIds.length; i += 1) {
6922
+ assignments[roleIds[i]] = selectedModels[i % selectedModels.length];
6923
+ }
6924
+ return assignments;
6925
+ }
6926
+ function pickRandom(roleIds, selectedModels) {
6927
+ const assignments = {};
6928
+ for (const roleId of roleIds) {
6929
+ assignments[roleId] = selectedModels[Math.floor(Math.random() * selectedModels.length)];
6930
+ }
6931
+ return assignments;
6932
+ }
6933
+ function buildModelAssignments(selections) {
6934
+ const assignments = {
6935
+ "project-manager": selections.pm[0],
6936
+ ...pickByOrder(ROLE_GROUPS.strategic, selections.strategic),
6937
+ ...pickByOrder(ROLE_GROUPS.dev, selections.dev),
6938
+ ...pickByOrder(ROLE_GROUPS.qc, selections.qc)
6939
+ };
6940
+ const assigned = new Set(Object.keys(assignments));
6941
+ const others = ALL_ROLES.filter((role) => !assigned.has(role));
6942
+ return { ...assignments, ...pickRandom(others, selections.others) };
6943
+ }
6944
+
6945
+ // src/adapters/codex.ts
6946
+ import fs3 from "node:fs";
6947
+ import os2 from "node:os";
6948
+ import path5 from "node:path";
6949
+
6950
+ // src/adapters/shared-install.ts
6951
+ import fs2 from "node:fs";
6952
+ import os from "node:os";
6953
+ import path4 from "node:path";
6954
+ import { execFileSync as execFileSync3 } from "node:child_process";
6955
+ var REPO_URL = "https://github.com/btspoony/mstar-harness.git";
6956
+ var PLUGIN_NAME = "morning-star-harness";
6957
+ var HARNESS_REPO_PATH = path4.join(os.homedir(), ".mstar", "harness");
6958
+ var HARNESS_MARKERS = [
6959
+ ".codex-plugin/plugin.json",
6960
+ ".zcode-plugin/plugin.json",
6961
+ ".omp-plugin/plugin.json"
6962
+ ];
6963
+ function harnessMarkerPath() {
6964
+ for (const marker of HARNESS_MARKERS) {
6965
+ const candidate = path4.join(HARNESS_REPO_PATH, marker);
6966
+ if (fs2.existsSync(candidate))
6967
+ return candidate;
6968
+ }
6969
+ return path4.join(HARNESS_REPO_PATH, HARNESS_MARKERS[0]);
6970
+ }
6971
+ function pathOrSymlinkExists(filePath) {
6972
+ try {
6973
+ fs2.lstatSync(filePath);
6974
+ return true;
6975
+ } catch {
6976
+ return false;
6977
+ }
6978
+ }
6979
+ function ensureDir(dirPath, dryRun) {
6980
+ if (dryRun)
6981
+ return;
6982
+ if (!fs2.existsSync(dirPath))
6983
+ fs2.mkdirSync(dirPath, { recursive: true });
6984
+ }
6985
+ function runCommand(command, cwd, dryRun) {
6986
+ if (dryRun)
6987
+ return;
6988
+ execFileSync3(command[0], command.slice(1), { cwd, stdio: "pipe", encoding: "utf8" });
6989
+ }
6990
+ function ensureLocalHarnessRepo(dryRun) {
6991
+ const notes = [];
6992
+ if (fs2.existsSync(HARNESS_REPO_PATH)) {
6993
+ const errors2 = validateLocalHarnessRepo();
6994
+ if (errors2.length) {
6995
+ throw new Error(errors2.join(`
6996
+ `));
6997
+ }
6998
+ notes.push(`Using existing local harness repo at ${HARNESS_REPO_PATH}`);
6999
+ return notes;
7000
+ }
7001
+ ensureDir(path4.dirname(HARNESS_REPO_PATH), dryRun);
7002
+ runCommand(["git", "clone", REPO_URL, HARNESS_REPO_PATH], path4.dirname(HARNESS_REPO_PATH), dryRun);
7003
+ notes.push(`Cloned ${REPO_URL} to ${HARNESS_REPO_PATH}`);
7004
+ return notes;
7005
+ }
7006
+ function validateLocalHarnessRepo() {
7007
+ const errors2 = [];
7008
+ if (!fs2.existsSync(HARNESS_REPO_PATH)) {
7009
+ errors2.push(`Missing local harness repo: ${HARNESS_REPO_PATH}`);
7010
+ return errors2;
7011
+ }
7012
+ const marker = harnessMarkerPath();
7013
+ if (!fs2.existsSync(marker)) {
7014
+ errors2.push(`Local harness repo is missing a plugin marker (expected one of: ${HARNESS_MARKERS.join(", ")}).`);
7015
+ }
7016
+ return errors2;
7017
+ }
7018
+ function ensureGitCheckout(repoUrl, checkoutPath, dryRun) {
7019
+ const notes = [];
7020
+ const parentDir = path4.dirname(checkoutPath);
7021
+ if (pathOrSymlinkExists(checkoutPath)) {
7022
+ const stat = fs2.lstatSync(checkoutPath);
7023
+ if (stat.isSymbolicLink()) {
7024
+ notes.push(dryRun ? `Would remove symlink at ${checkoutPath} and clone ${repoUrl}` : `Removed symlink at ${checkoutPath} (Cursor requires a real directory)`);
7025
+ if (dryRun)
7026
+ return notes;
7027
+ fs2.unlinkSync(checkoutPath);
7028
+ } else if (fs2.existsSync(path4.join(checkoutPath, ".git"))) {
7029
+ if (!dryRun) {
7030
+ runCommand(["git", "-C", checkoutPath, "pull", "--ff-only"], checkoutPath, dryRun);
7031
+ }
7032
+ notes.push(`Updated git checkout at ${checkoutPath}`);
7033
+ return notes;
7034
+ } else {
7035
+ throw new Error(`Path exists but is not a git checkout: ${checkoutPath}`);
7036
+ }
7037
+ }
7038
+ ensureDir(parentDir, dryRun);
7039
+ if (!dryRun) {
7040
+ runCommand(["git", "clone", repoUrl, checkoutPath], parentDir, dryRun);
7041
+ }
7042
+ notes.push(`${dryRun ? "Would clone" : "Cloned"} ${repoUrl} to ${checkoutPath}`);
7043
+ return notes;
7044
+ }
7045
+ function validateGitCheckout(checkoutPath, markerRelativePath) {
7046
+ const errors2 = [];
7047
+ if (!pathOrSymlinkExists(checkoutPath)) {
7048
+ errors2.push(`Missing checkout directory: ${checkoutPath}`);
7049
+ return errors2;
7050
+ }
7051
+ const stat = fs2.lstatSync(checkoutPath);
7052
+ if (stat.isSymbolicLink()) {
7053
+ errors2.push(`Path must be a real directory, not a symlink: ${checkoutPath}. Run: mstar-harness init --target cursor`);
7054
+ return errors2;
7055
+ }
7056
+ if (!fs2.existsSync(path4.join(checkoutPath, ".git"))) {
7057
+ errors2.push(`Path is not a git checkout: ${checkoutPath}`);
7058
+ }
7059
+ const marker = path4.join(checkoutPath, markerRelativePath);
7060
+ if (!fs2.existsSync(marker)) {
7061
+ errors2.push(`Missing marker file: ${marker}`);
7062
+ }
7063
+ return errors2;
7064
+ }
7065
+ function ensureSymlink(target, linkPath, dryRun) {
7066
+ if (pathOrSymlinkExists(linkPath)) {
7067
+ const stat = fs2.lstatSync(linkPath);
7068
+ if (!stat.isSymbolicLink()) {
7069
+ throw new Error(`Path exists and is not a symlink: ${linkPath}`);
7070
+ }
7071
+ if (!fs2.existsSync(target)) {
7072
+ throw new Error(`Symlink target is missing: ${target}`);
7073
+ }
7074
+ const actual = fs2.realpathSync(linkPath);
7075
+ const expected = fs2.realpathSync(target);
7076
+ if (actual !== expected) {
7077
+ throw new Error(`Symlink ${linkPath} points to ${actual}, expected ${expected}`);
7078
+ }
7079
+ return `Symlink already exists: ${linkPath} -> ${target}`;
7080
+ }
7081
+ ensureDir(path4.dirname(linkPath), dryRun);
7082
+ if (!dryRun)
7083
+ fs2.symlinkSync(target, linkPath);
7084
+ return `Linked ${linkPath} -> ${target}`;
7085
+ }
7086
+ function validateSymlink(target, linkPath) {
7087
+ const errors2 = [];
7088
+ if (!pathOrSymlinkExists(linkPath)) {
7089
+ errors2.push(`Missing symlink: ${linkPath}`);
7090
+ return errors2;
7091
+ }
7092
+ const stat = fs2.lstatSync(linkPath);
7093
+ if (!stat.isSymbolicLink()) {
7094
+ errors2.push(`Path exists but is not a symlink: ${linkPath}`);
7095
+ return errors2;
7096
+ }
7097
+ if (!fs2.existsSync(target)) {
7098
+ errors2.push(`Symlink target is missing: ${target}`);
7099
+ return errors2;
7100
+ }
7101
+ const actual = fs2.realpathSync(linkPath);
7102
+ const expected = fs2.realpathSync(target);
7103
+ if (actual !== expected) {
7104
+ errors2.push(`Symlink ${linkPath} points to ${actual}, expected ${expected}`);
7105
+ }
7106
+ return errors2;
7107
+ }
7108
+ var HARNESS_PROCESS_GITIGNORE = [
4229
7109
  ".mstar/archived/",
4230
7110
  ".mstar/iterations/",
4231
7111
  ".mstar/plans/",
@@ -4244,7 +7124,7 @@ function missingHarnessProcessGitignoreEntries(gitignoreContent) {
4244
7124
  return HARNESS_PROCESS_GITIGNORE.filter((entry) => !lines.includes(entry));
4245
7125
  }
4246
7126
  function appendGitignore(projectRoot, entries, dryRun) {
4247
- const gitignorePath = path3.join(projectRoot, ".gitignore");
7127
+ const gitignorePath = path4.join(projectRoot, ".gitignore");
4248
7128
  const current = fs2.existsSync(gitignorePath) ? fs2.readFileSync(gitignorePath, "utf8") : "";
4249
7129
  const lines = new Set(current.split(/\r?\n/).map((line) => line.trim()));
4250
7130
  const missing = entries.filter((entry) => !lines.has(entry));
@@ -4264,14 +7144,14 @@ function appendHarnessProjectGitignore(projectRoot, dryRun) {
4264
7144
  return appendGitignore(projectRoot, HARNESS_PROCESS_GITIGNORE, dryRun);
4265
7145
  }
4266
7146
  function homeRelativeSourcePath(targetPath) {
4267
- const rel = path3.relative(os.homedir(), targetPath).split(path3.sep).join("/");
7147
+ const rel = path4.relative(os.homedir(), targetPath).split(path4.sep).join("/");
4268
7148
  return rel.startsWith("..") ? targetPath : `./${rel}`;
4269
7149
  }
4270
7150
 
4271
7151
  // src/adapters/codex.ts
4272
7152
  var MARKETPLACE_NAME = "personal";
4273
7153
  var MARKETPLACE_DISPLAY_NAME = "Personal";
4274
- var GLOBAL_MARKETPLACE_PATH = path4.join(os2.homedir(), ".agents", "plugins", "marketplace.json");
7154
+ var GLOBAL_MARKETPLACE_PATH = path5.join(os2.homedir(), ".agents", "plugins", "marketplace.json");
4275
7155
  var PLUGIN_CATEGORY = "Productivity";
4276
7156
  var CODEX_PLUGIN_LINK = ".codex/plugins/mstar-harness";
4277
7157
  var CODEX_AGENT_NAMES = [
@@ -4299,16 +7179,16 @@ function globalMarketplacePath() {
4299
7179
  return GLOBAL_MARKETPLACE_PATH;
4300
7180
  }
4301
7181
  function projectMarketplacePath() {
4302
- return path4.join(resolveProjectRoot(), ".agents", "plugins", "marketplace.json");
7182
+ return path5.join(resolveProjectRoot2(), ".agents", "plugins", "marketplace.json");
4303
7183
  }
4304
7184
  function agentSourcePath(agentName) {
4305
- return path4.join(HARNESS_REPO_PATH, "codex", "agents", `${agentName}.toml`);
7185
+ return path5.join(HARNESS_REPO_PATH, "codex", "agents", `${agentName}.toml`);
4306
7186
  }
4307
7187
  function globalAgentLinkPath(agentName) {
4308
- return path4.join(os2.homedir(), ".codex", "agents", `${agentName}.toml`);
7188
+ return path5.join(os2.homedir(), ".codex", "agents", `${agentName}.toml`);
4309
7189
  }
4310
7190
  function projectAgentLinkPath(agentName) {
4311
- return path4.join(resolveProjectRoot(), ".codex", "agents", `${agentName}.toml`);
7191
+ return path5.join(resolveProjectRoot2(), ".codex", "agents", `${agentName}.toml`);
4312
7192
  }
4313
7193
  function mstarEntry(scope) {
4314
7194
  const sourcePath = scope === "global" ? homeRelativeSourcePath(HARNESS_REPO_PATH) : `./${CODEX_PLUGIN_LINK}`;
@@ -4399,17 +7279,17 @@ function validateAgentLinks(scope) {
4399
7279
  return errors2;
4400
7280
  }
4401
7281
  function iterationCommandSourcePath(skillName) {
4402
- return path4.join(HARNESS_REPO_PATH, "commands", `${skillName}.md`);
7282
+ return path5.join(HARNESS_REPO_PATH, "commands", `${skillName}.md`);
4403
7283
  }
4404
7284
  function projectIterationSkillLinkPath(skillName) {
4405
- return path4.join(resolveProjectRoot(), ".agents", "skills", skillName, "SKILL.md");
7285
+ return path5.join(resolveProjectRoot2(), ".agents", "skills", skillName, "SKILL.md");
4406
7286
  }
4407
7287
  function iterationSkillGitignoreEntry(skillName) {
4408
7288
  return `.agents/skills/${skillName}`;
4409
7289
  }
4410
7290
  function ensureIterationSkillLinks(dryRun) {
4411
7291
  const notes = [];
4412
- const projectRoot = resolveProjectRoot();
7292
+ const projectRoot = resolveProjectRoot2();
4413
7293
  for (const skillName of CODEX_PROJECT_COMMAND_NAMES) {
4414
7294
  const source = iterationCommandSourcePath(skillName);
4415
7295
  const linkPath = projectIterationSkillLinkPath(skillName);
@@ -4422,8 +7302,8 @@ function ensureIterationSkillLinks(dryRun) {
4422
7302
  }
4423
7303
  function validateIterationSkillLinks() {
4424
7304
  const errors2 = [];
4425
- const projectRoot = resolveProjectRoot();
4426
- const gitignorePath = path4.join(projectRoot, ".gitignore");
7305
+ const projectRoot = resolveProjectRoot2();
7306
+ const gitignorePath = path5.join(projectRoot, ".gitignore");
4427
7307
  const gitignore = fs3.existsSync(gitignorePath) ? fs3.readFileSync(gitignorePath, "utf8") : "";
4428
7308
  const lines = gitignore.split(/\r?\n/);
4429
7309
  for (const skillName of CODEX_PROJECT_COMMAND_NAMES) {
@@ -4438,13 +7318,13 @@ function validateIterationSkillLinks() {
4438
7318
  }
4439
7319
  function runInit(scope, dryRun) {
4440
7320
  const pathToMarketplace = marketplacePath(scope);
4441
- const current = readJson(pathToMarketplace);
7321
+ const current = readJson2(pathToMarketplace);
4442
7322
  const next = upsertEntry(current, scope);
4443
7323
  const existingEntry = findEntry(current);
4444
7324
  const notes = ensureLocalHarnessRepo(dryRun);
4445
7325
  if (scope === "project") {
4446
- const projectRoot = resolveProjectRoot();
4447
- notes.push(ensureSymlink(HARNESS_REPO_PATH, path4.join(projectRoot, CODEX_PLUGIN_LINK), dryRun));
7326
+ const projectRoot = resolveProjectRoot2();
7327
+ notes.push(ensureSymlink(HARNESS_REPO_PATH, path5.join(projectRoot, CODEX_PLUGIN_LINK), dryRun));
4448
7328
  notes.push(...appendGitignore(projectRoot, [CODEX_PLUGIN_LINK, ".codex/agents/*.toml"], dryRun));
4449
7329
  notes.push(...appendHarnessProjectGitignore(projectRoot, dryRun));
4450
7330
  notes.push(...ensureIterationSkillLinks(dryRun));
@@ -4453,7 +7333,7 @@ function runInit(scope, dryRun) {
4453
7333
  }
4454
7334
  notes.push(...ensureAgentLinks(scope, dryRun));
4455
7335
  if (!dryRun)
4456
- writeJson(pathToMarketplace, next);
7336
+ writeJson2(pathToMarketplace, next);
4457
7337
  notes.push(existingEntry ? `Updated ${PLUGIN_NAME} local marketplace entry.` : `Added ${PLUGIN_NAME} local marketplace entry.`);
4458
7338
  notes.push(`Source path: ${mstarEntry(scope).source.path}`);
4459
7339
  notes.push(`Install after init: codex plugin add ${PLUGIN_NAME} --marketplace ${MARKETPLACE_NAME}`);
@@ -4468,15 +7348,15 @@ function runDoctor(scope) {
4468
7348
  if (!fs3.existsSync(pathToMarketplace)) {
4469
7349
  return { location: pathToMarketplace, errors: [...errors2, `Missing Codex marketplace: ${pathToMarketplace}`] };
4470
7350
  }
4471
- const marketplace = readJson(pathToMarketplace);
7351
+ const marketplace = readJson2(pathToMarketplace);
4472
7352
  if (marketplace.name !== MARKETPLACE_NAME) {
4473
7353
  errors2.push(`Codex personal marketplace name must be ${MARKETPLACE_NAME}.`);
4474
7354
  }
4475
7355
  errors2.push(...validateEntryShape(findEntry(marketplace), scope, pathToMarketplace));
4476
7356
  if (scope === "project") {
4477
- const projectRoot = resolveProjectRoot();
4478
- errors2.push(...validateSymlink(HARNESS_REPO_PATH, path4.join(projectRoot, CODEX_PLUGIN_LINK)));
4479
- const gitignorePath = path4.join(projectRoot, ".gitignore");
7357
+ const projectRoot = resolveProjectRoot2();
7358
+ errors2.push(...validateSymlink(HARNESS_REPO_PATH, path5.join(projectRoot, CODEX_PLUGIN_LINK)));
7359
+ const gitignorePath = path5.join(projectRoot, ".gitignore");
4480
7360
  const gitignore = fs3.existsSync(gitignorePath) ? fs3.readFileSync(gitignorePath, "utf8") : "";
4481
7361
  const lines = gitignore.split(/\r?\n/);
4482
7362
  if (!lines.includes(CODEX_PLUGIN_LINK))
@@ -4501,26 +7381,26 @@ var codexAdapter = {
4501
7381
  // src/adapters/cursor.ts
4502
7382
  import fs4 from "node:fs";
4503
7383
  import os3 from "node:os";
4504
- import path5 from "node:path";
7384
+ import path6 from "node:path";
4505
7385
  var CURSOR_PLUGIN_NAME = "morning-star-harness";
4506
7386
  var CURSOR_PLUGIN_MARKER = ".cursor-plugin/plugin.json";
4507
7387
  var CURSOR_PLUGIN_LINK = ".cursor/plugins/morning-star-harness";
4508
7388
  var CURSOR_AGENT_SMOKE_NAMES = ["fullstack-dev", "qc-specialist"];
4509
7389
  function globalInstallPath() {
4510
- return path5.join(os3.homedir(), ".cursor", "plugins", "local", CURSOR_PLUGIN_NAME);
7390
+ return path6.join(os3.homedir(), ".cursor", "plugins", "local", CURSOR_PLUGIN_NAME);
4511
7391
  }
4512
7392
  function projectInstallPath() {
4513
- return path5.join(resolveProjectRoot(), CURSOR_PLUGIN_LINK);
7393
+ return path6.join(resolveProjectRoot2(), CURSOR_PLUGIN_LINK);
4514
7394
  }
4515
7395
  function validatePluginAgents(pluginRoot) {
4516
7396
  const errors2 = [];
4517
- const agentsDir = path5.join(pluginRoot, "agents");
7397
+ const agentsDir = path6.join(pluginRoot, "agents");
4518
7398
  if (!fs4.existsSync(agentsDir)) {
4519
7399
  errors2.push(`Missing plugin agents directory: ${agentsDir}`);
4520
7400
  return errors2;
4521
7401
  }
4522
7402
  for (const agentName of CURSOR_AGENT_SMOKE_NAMES) {
4523
- const agentPath = path5.join(agentsDir, `${agentName}.md`);
7403
+ const agentPath = path6.join(agentsDir, `${agentName}.md`);
4524
7404
  if (!fs4.existsSync(agentPath)) {
4525
7405
  errors2.push(`Missing plugin agent file: ${agentPath}`);
4526
7406
  continue;
@@ -4542,7 +7422,7 @@ function globalInit(dryRun) {
4542
7422
  return { location, notes };
4543
7423
  }
4544
7424
  function projectInit(dryRun) {
4545
- const projectRoot = resolveProjectRoot();
7425
+ const projectRoot = resolveProjectRoot2();
4546
7426
  const location = projectInstallPath();
4547
7427
  const notes = ensureLocalHarnessRepo(dryRun);
4548
7428
  notes.push(...ensureCursorPluginCheckout(location, dryRun));
@@ -4558,11 +7438,11 @@ function globalDoctor() {
4558
7438
  return { location, errors: errors2 };
4559
7439
  }
4560
7440
  function projectDoctor() {
4561
- const projectRoot = resolveProjectRoot();
7441
+ const projectRoot = resolveProjectRoot2();
4562
7442
  const location = projectInstallPath();
4563
7443
  const errors2 = validateLocalHarnessRepo();
4564
7444
  errors2.push(...validateGitCheckout(location, CURSOR_PLUGIN_MARKER));
4565
- const gitignorePath = path5.join(projectRoot, ".gitignore");
7445
+ const gitignorePath = path6.join(projectRoot, ".gitignore");
4566
7446
  const gitignore = fs4.existsSync(gitignorePath) ? fs4.readFileSync(gitignorePath, "utf8") : "";
4567
7447
  if (!gitignore.split(/\r?\n/).includes(CURSOR_PLUGIN_LINK)) {
4568
7448
  errors2.push(`Missing .gitignore entry: ${CURSOR_PLUGIN_LINK}`);
@@ -4590,8 +7470,8 @@ var cursorAdapter = {
4590
7470
 
4591
7471
  // src/adapters/omp.ts
4592
7472
  import fs5 from "node:fs";
4593
- import path6 from "node:path";
4594
- import { execFileSync as execFileSync2 } from "node:child_process";
7473
+ import path7 from "node:path";
7474
+ import { execFileSync as execFileSync5 } from "node:child_process";
4595
7475
  var OMP_PLUGIN_MARKER = ".omp-plugin/plugin.json";
4596
7476
  var CLAUDE_PLUGIN_MARKER = ".claude-plugin/plugin.json";
4597
7477
  var PACKAGE_NAMES = new Set(["morning-star", PLUGIN_NAME, "github:btspoony/mstar-harness"]);
@@ -4599,7 +7479,7 @@ var SKILL_SMOKE = ["mstar-host", "mstar-harness-core", "pm"];
4599
7479
  var COMMAND_SMOKE = ["iteration-start", "iteration-drive", "iteration-loop", "codebase-audit"];
4600
7480
  function ompAvailable() {
4601
7481
  try {
4602
- execFileSync2("omp", ["--version"], { stdio: "pipe", encoding: "utf8" });
7482
+ execFileSync5("omp", ["--version"], { stdio: "pipe", encoding: "utf8" });
4603
7483
  return true;
4604
7484
  } catch {
4605
7485
  return false;
@@ -4608,11 +7488,11 @@ function ompAvailable() {
4608
7488
  function runOmp(args, dryRun) {
4609
7489
  if (dryRun)
4610
7490
  return;
4611
- execFileSync2("omp", args, { stdio: "pipe", encoding: "utf8" });
7491
+ execFileSync5("omp", args, { stdio: "pipe", encoding: "utf8" });
4612
7492
  }
4613
7493
  function listInstalledPlugins() {
4614
7494
  try {
4615
- const raw = execFileSync2("omp", ["plugin", "list", "--json"], {
7495
+ const raw = execFileSync5("omp", ["plugin", "list", "--json"], {
4616
7496
  stdio: "pipe",
4617
7497
  encoding: "utf8"
4618
7498
  });
@@ -4652,7 +7532,7 @@ function findInstalledPlugin(plugins) {
4652
7532
  return true;
4653
7533
  if (name.includes("morning-star") || manifestName.includes("morning-star"))
4654
7534
  return true;
4655
- if (pathValue.includes("mstar-harness") || pathValue.includes(`${path6.sep}morning-star`))
7535
+ if (pathValue.includes("mstar-harness") || pathValue.includes(`${path7.sep}morning-star`))
4656
7536
  return true;
4657
7537
  return false;
4658
7538
  });
@@ -4660,35 +7540,35 @@ function findInstalledPlugin(plugins) {
4660
7540
  function validatePluginTree(pluginRoot) {
4661
7541
  const errors2 = [];
4662
7542
  for (const marker of [OMP_PLUGIN_MARKER, CLAUDE_PLUGIN_MARKER]) {
4663
- const markerPath = path6.join(pluginRoot, marker);
7543
+ const markerPath = path7.join(pluginRoot, marker);
4664
7544
  if (!fs5.existsSync(markerPath)) {
4665
7545
  errors2.push(`Missing omp plugin marker: ${markerPath}`);
4666
7546
  }
4667
7547
  }
4668
7548
  for (const skill of SKILL_SMOKE) {
4669
- const skillPath = path6.join(pluginRoot, "skills", skill, "SKILL.md");
7549
+ const skillPath = path7.join(pluginRoot, "skills", skill, "SKILL.md");
4670
7550
  if (!fs5.existsSync(skillPath))
4671
7551
  errors2.push(`Missing skill: ${skillPath}`);
4672
7552
  }
4673
7553
  for (const command of COMMAND_SMOKE) {
4674
- const commandPath = path6.join(pluginRoot, "commands", `${command}.md`);
7554
+ const commandPath = path7.join(pluginRoot, "commands", `${command}.md`);
4675
7555
  if (!fs5.existsSync(commandPath))
4676
7556
  errors2.push(`Missing command: ${commandPath}`);
4677
7557
  }
4678
- const hostRef = path6.join(pluginRoot, "skills", "mstar-host", "references", "omp.md");
7558
+ const hostRef = path7.join(pluginRoot, "skills", "mstar-host", "references", "omp.md");
4679
7559
  if (!fs5.existsSync(hostRef))
4680
7560
  errors2.push(`Missing omp host reference: ${hostRef}`);
4681
7561
  return errors2;
4682
7562
  }
4683
7563
  function runInit2(scope, dryRun) {
4684
7564
  const notes = ensureLocalHarnessRepo(dryRun);
4685
- const projectRoot = resolveProjectRoot();
4686
- if (fs5.existsSync(path6.join(HARNESS_REPO_PATH, ".git"))) {
7565
+ const projectRoot = resolveProjectRoot2();
7566
+ if (fs5.existsSync(path7.join(HARNESS_REPO_PATH, ".git"))) {
4687
7567
  if (dryRun) {
4688
7568
  notes.push(`Would update local harness repo: git -C ${HARNESS_REPO_PATH} pull --ff-only`);
4689
7569
  } else {
4690
7570
  try {
4691
- execFileSync2("git", ["-C", HARNESS_REPO_PATH, "pull", "--ff-only"], {
7571
+ execFileSync5("git", ["-C", HARNESS_REPO_PATH, "pull", "--ff-only"], {
4692
7572
  stdio: "pipe",
4693
7573
  encoding: "utf8"
4694
7574
  });
@@ -4756,8 +7636,8 @@ function runDoctor2(scope) {
4756
7636
  }
4757
7637
  }
4758
7638
  if (scope === "project") {
4759
- const projectRoot = resolveProjectRoot();
4760
- const gitignorePath = path6.join(projectRoot, ".gitignore");
7639
+ const projectRoot = resolveProjectRoot2();
7640
+ const gitignorePath = path7.join(projectRoot, ".gitignore");
4761
7641
  const gitignore = fs5.existsSync(gitignorePath) ? fs5.readFileSync(gitignorePath, "utf8") : "";
4762
7642
  for (const entry of missingHarnessProcessGitignoreEntries(gitignore)) {
4763
7643
  errors2.push(`Missing .gitignore entry: ${entry}`);
@@ -4774,7 +7654,7 @@ var ompAdapter = {
4774
7654
 
4775
7655
  // src/adapters/opencode.ts
4776
7656
  import os4 from "node:os";
4777
- import path7 from "node:path";
7657
+ import path8 from "node:path";
4778
7658
  var OPENCODE_CONFIG_SCHEMA = "https://opencode.ai/config.json";
4779
7659
  var MSTAR_OPENCODE_PLUGIN = "@mstar-harness/opencode@latest";
4780
7660
  function isLegacyMorningStarGitPlugin(plugin) {
@@ -4795,11 +7675,11 @@ function isAnyMstarHarnessOpencodeSlot(plugin) {
4795
7675
  function resolveOpencodeConfigPath(scope, outputPath) {
4796
7676
  if (outputPath && outputPath.trim()) {
4797
7677
  const raw = outputPath.trim();
4798
- return path7.isAbsolute(raw) ? raw : path7.join(resolveProjectRoot(), raw);
7678
+ return path8.isAbsolute(raw) ? raw : path8.join(resolveProjectRoot2(), raw);
4799
7679
  }
4800
7680
  if (scope === "global")
4801
- return path7.join(os4.homedir(), ".config", "opencode", "opencode.json");
4802
- return path7.join(resolveProjectRoot(), "opencode.json");
7681
+ return path8.join(os4.homedir(), ".config", "opencode", "opencode.json");
7682
+ return path8.join(resolveProjectRoot2(), "opencode.json");
4803
7683
  }
4804
7684
  function ensureConfigSchema(config) {
4805
7685
  const next = ensureObject(config);
@@ -4894,7 +7774,7 @@ var opencodeAdapter = {
4894
7774
  // src/adapters/zcode.ts
4895
7775
  import fs6 from "node:fs";
4896
7776
  import os5 from "node:os";
4897
- import path8 from "node:path";
7777
+ import path9 from "node:path";
4898
7778
  var MARKETPLACE_ID = "mstar-local";
4899
7779
  var MARKETPLACE_NAME2 = "mstar-local";
4900
7780
  var MARKETPLACE_DESCRIPTION = "Morning Star harness marketplace (GitHub source).";
@@ -4905,10 +7785,10 @@ var GITHUB_REF = "main";
4905
7785
  var ZCODE_PLUGIN_MARKER = ".zcode-plugin/plugin.json";
4906
7786
  var ZCODE_PLUGIN_CHECKOUT_PROJECT = ".zcode/plugin-checkout";
4907
7787
  var ZCODE_AGENT_SMOKE_NAMES = ["fullstack-dev", "qc-specialist"];
4908
- var ZCODE_PLUGINS_ROOT = path8.join(os5.homedir(), ".zcode", "cli", "plugins");
4909
- var KNOWN_MARKETPLACES_PATH = path8.join(ZCODE_PLUGINS_ROOT, "known_marketplaces.json");
4910
- var MARKETPLACE_DIR = path8.join(ZCODE_PLUGINS_ROOT, "marketplaces", MARKETPLACE_ID);
4911
- var MARKETPLACE_JSON_PATH = path8.join(MARKETPLACE_DIR, "marketplace.json");
7788
+ var ZCODE_PLUGINS_ROOT = path9.join(os5.homedir(), ".zcode", "cli", "plugins");
7789
+ var KNOWN_MARKETPLACES_PATH = path9.join(ZCODE_PLUGINS_ROOT, "known_marketplaces.json");
7790
+ var MARKETPLACE_DIR = path9.join(ZCODE_PLUGINS_ROOT, "marketplaces", MARKETPLACE_ID);
7791
+ var MARKETPLACE_JSON_PATH = path9.join(MARKETPLACE_DIR, "marketplace.json");
4912
7792
  var GITHUB_SOURCE = { source: "github", repo: GITHUB_REPO, ref: GITHUB_REF };
4913
7793
  function nowIso() {
4914
7794
  return new Date().toISOString();
@@ -4968,7 +7848,7 @@ function validateMarketplaceJson() {
4968
7848
  errors2.push(`Missing ZCode marketplace: ${MARKETPLACE_JSON_PATH}`);
4969
7849
  return errors2;
4970
7850
  }
4971
- const raw = readJson(MARKETPLACE_JSON_PATH);
7851
+ const raw = readJson2(MARKETPLACE_JSON_PATH);
4972
7852
  if (raw.name !== MARKETPLACE_NAME2) {
4973
7853
  errors2.push(`ZCode marketplace name must be ${MARKETPLACE_NAME2} (in ${MARKETPLACE_JSON_PATH}).`);
4974
7854
  }
@@ -4997,7 +7877,7 @@ function validateKnownMarketplaces() {
4997
7877
  errors2.push(`Missing ZCode known_marketplaces.json: ${KNOWN_MARKETPLACES_PATH}`);
4998
7878
  return errors2;
4999
7879
  }
5000
- const raw = readJson(KNOWN_MARKETPLACES_PATH);
7880
+ const raw = readJson2(KNOWN_MARKETPLACES_PATH);
5001
7881
  const entry = findKnownMarketplace(raw);
5002
7882
  if (!entry) {
5003
7883
  errors2.push(`Missing ${MARKETPLACE_ID} entry in ${KNOWN_MARKETPLACES_PATH}.`);
@@ -5016,13 +7896,13 @@ function validateKnownMarketplaces() {
5016
7896
  }
5017
7897
  function validatePluginAgents2(pluginRoot) {
5018
7898
  const errors2 = [];
5019
- const agentsDir = path8.join(pluginRoot, "agents");
7899
+ const agentsDir = path9.join(pluginRoot, "agents");
5020
7900
  if (!fs6.existsSync(agentsDir)) {
5021
7901
  errors2.push(`Missing plugin agents directory: ${agentsDir}`);
5022
7902
  return errors2;
5023
7903
  }
5024
7904
  for (const agentName of ZCODE_AGENT_SMOKE_NAMES) {
5025
- const agentPath = path8.join(agentsDir, `${agentName}.md`);
7905
+ const agentPath = path9.join(agentsDir, `${agentName}.md`);
5026
7906
  if (!fs6.existsSync(agentPath)) {
5027
7907
  errors2.push(`Missing plugin agent file: ${agentPath}`);
5028
7908
  }
@@ -5038,9 +7918,9 @@ function buildMarketplaceJson() {
5038
7918
  }
5039
7919
  function runInit3(scope, dryRun) {
5040
7920
  const notes = ensureLocalHarnessRepo(dryRun);
5041
- const projectRoot = resolveProjectRoot();
7921
+ const projectRoot = resolveProjectRoot2();
5042
7922
  if (scope === "project") {
5043
- const checkoutPath = path8.join(projectRoot, ZCODE_PLUGIN_CHECKOUT_PROJECT);
7923
+ const checkoutPath = path9.join(projectRoot, ZCODE_PLUGIN_CHECKOUT_PROJECT);
5044
7924
  notes.push(...ensureGitCheckout(REPO_URL, checkoutPath, dryRun));
5045
7925
  notes.push(...appendGitignore(projectRoot, [ZCODE_PLUGIN_CHECKOUT_PROJECT], dryRun));
5046
7926
  notes.push(...appendHarnessProjectGitignore(projectRoot, dryRun));
@@ -5049,13 +7929,13 @@ function runInit3(scope, dryRun) {
5049
7929
  if (!dryRun) {
5050
7930
  if (!fs6.existsSync(MARKETPLACE_DIR))
5051
7931
  fs6.mkdirSync(MARKETPLACE_DIR, { recursive: true });
5052
- writeJson(MARKETPLACE_JSON_PATH, buildMarketplaceJson());
7932
+ writeJson2(MARKETPLACE_JSON_PATH, buildMarketplaceJson());
5053
7933
  }
5054
7934
  notes.push(`Wrote ZCode marketplace: ${MARKETPLACE_JSON_PATH}`);
5055
- const knownRaw = readJson(KNOWN_MARKETPLACES_PATH);
7935
+ const knownRaw = readJson2(KNOWN_MARKETPLACES_PATH);
5056
7936
  const knownNext = upsertKnownMarketplace(knownRaw);
5057
7937
  if (!dryRun)
5058
- writeJson(KNOWN_MARKETPLACES_PATH, knownNext);
7938
+ writeJson2(KNOWN_MARKETPLACES_PATH, knownNext);
5059
7939
  notes.push(`Registered ${MARKETPLACE_ID} marketplace in ${KNOWN_MARKETPLACES_PATH}`);
5060
7940
  notes.push(`Then in ZCode: Settings → Plugin Management → Discover → install ${PLUGIN_NAME} from the ${MARKETPLACE_ID} marketplace.`);
5061
7941
  return {
@@ -5067,10 +7947,10 @@ function runDoctor3(scope) {
5067
7947
  const errors2 = [];
5068
7948
  errors2.push(...validateLocalHarnessRepo());
5069
7949
  if (scope === "project") {
5070
- const projectRoot = resolveProjectRoot();
5071
- const checkoutPath = path8.join(projectRoot, ZCODE_PLUGIN_CHECKOUT_PROJECT);
7950
+ const projectRoot = resolveProjectRoot2();
7951
+ const checkoutPath = path9.join(projectRoot, ZCODE_PLUGIN_CHECKOUT_PROJECT);
5072
7952
  errors2.push(...validateGitCheckout(checkoutPath, ZCODE_PLUGIN_MARKER));
5073
- const gitignorePath = path8.join(projectRoot, ".gitignore");
7953
+ const gitignorePath = path9.join(projectRoot, ".gitignore");
5074
7954
  const gitignore = fs6.existsSync(gitignorePath) ? fs6.readFileSync(gitignorePath, "utf8") : "";
5075
7955
  if (!gitignore.split(/\r?\n/).includes(ZCODE_PLUGIN_CHECKOUT_PROJECT)) {
5076
7956
  errors2.push(`Missing .gitignore entry: ${ZCODE_PLUGIN_CHECKOUT_PROJECT}`);
@@ -5112,45 +7992,21 @@ function getAdapter(target) {
5112
7992
  var SUPPORTED_TARGETS = ["opencode", "cursor", "codex", "zcode", "omp"];
5113
7993
 
5114
7994
  // src/utils.ts
5115
- import fs7 from "node:fs";
5116
- import path9 from "node:path";
5117
- import { fileURLToPath as fileURLToPath2 } from "node:url";
7995
+ import path10 from "node:path";
5118
7996
  function parseCsv(raw) {
5119
7997
  if (!raw)
5120
7998
  return;
5121
7999
  return raw.split(",").map((item) => item.trim()).filter(Boolean);
5122
8000
  }
5123
- function readJson2(filePath) {
5124
- if (!fs7.existsSync(filePath))
5125
- return {};
5126
- const content = fs7.readFileSync(filePath, "utf8").trim();
5127
- if (!content)
5128
- return {};
5129
- try {
5130
- return JSON.parse(content);
5131
- } catch (error) {
5132
- throw new Error(`Invalid JSON in ${filePath}: ${error.message}`);
5133
- }
5134
- }
5135
- function writeJson2(filePath, value) {
5136
- const parent = path9.dirname(filePath);
5137
- if (!fs7.existsSync(parent))
5138
- fs7.mkdirSync(parent, { recursive: true });
5139
- fs7.writeFileSync(filePath, `${JSON.stringify(value, null, 2)}
5140
- `, "utf8");
5141
- }
5142
- function readHarnessVersion2() {
5143
- const packageJsonPath = path9.resolve(path9.dirname(fileURLToPath2(import.meta.url)), "../package.json");
5144
- try {
5145
- const parsed = JSON.parse(fs7.readFileSync(packageJsonPath, "utf8"));
5146
- return parsed.version || "0.0.0";
5147
- } catch {
5148
- return "0.0.0";
5149
- }
8001
+ function resolveProjectRoot3() {
8002
+ const candidate = process.env.MSTAR_CLI_PROJECT_ROOT || process.env.INIT_CWD || process.env.PWD;
8003
+ if (candidate && candidate.trim())
8004
+ return path10.resolve(candidate);
8005
+ return resolveProjectRoot();
5150
8006
  }
5151
8007
 
5152
8008
  // src/index.ts
5153
- var packageVersion = readHarnessVersion2();
8009
+ var packageVersion = readHarnessVersion();
5154
8010
  var program2 = new Command;
5155
8011
  function logStep(message) {
5156
8012
  console.log(import_picocolors.default.cyan(message));
@@ -5289,6 +8145,33 @@ function runDoctor4(options) {
5289
8145
  console.log(` - ${issue}`);
5290
8146
  process.exitCode = 1;
5291
8147
  }
8148
+ function resolvePluginRoot(options) {
8149
+ if (options.root)
8150
+ return path11.resolve(options.root);
8151
+ let candidate = resolveProjectRoot3();
8152
+ while (!fs7.existsSync(path11.join(candidate, "plugin.json"))) {
8153
+ const parent = path11.dirname(candidate);
8154
+ if (parent === candidate)
8155
+ break;
8156
+ candidate = parent;
8157
+ }
8158
+ return candidate;
8159
+ }
8160
+ function runPluginValidate(options) {
8161
+ const root = resolvePluginRoot(options);
8162
+ const result = validateAgentPlugin(root);
8163
+ for (const warning of result.warnings) {
8164
+ console.warn(import_picocolors.default.yellow(warning));
8165
+ }
8166
+ if (result.ok) {
8167
+ console.log(import_picocolors.default.green(`OK ${root}: Agent Plugins v1.0.0 conformant`));
8168
+ return;
8169
+ }
8170
+ for (const error of result.errors) {
8171
+ console.error(import_picocolors.default.red(error));
8172
+ }
8173
+ process.exitCode = 1;
8174
+ }
5292
8175
  program2.name("mstar-harness").description("Morning Star harness CLI for target-based agent bootstrap").version(packageVersion);
5293
8176
  program2.command("init").description("Interactive/non-interactive setup for target agent bootstrap").option("-y, --yes", "Non-interactive mode").option("--target <target>", "Install target", "opencode").option("--scope <scope>", "Config scope: global|project (default: project)").option("--output <path>", "Config file path override, relative to project root").option("--dry-run", "Preview result without writing config").option("--pm-model <model>", "Optional: model for project-manager (advanced override)").option("--strategic-models <a,b,c>", "Optional: models for architect/product-manager/prompt-engineer").option("--dev-models <a,b,c>", "Optional: models for fullstack-dev/fullstack-dev-2/frontend-dev").option("--qc-models <a,b,c>", "Optional: models for qc trio").option("--other-models <a,b,c>", "Optional: models for remaining roles").action(async (options) => {
5294
8177
  await runInit4(options);
@@ -5296,7 +8179,755 @@ program2.command("init").description("Interactive/non-interactive setup for targ
5296
8179
  program2.command("doctor").description("Validate Morning Star setup for a target agent config").option("--target <target>", "Target agent for doctor checks", "opencode").option("--scope <scope>", "Config scope: global|project", "project").option("--output <path>", "Config file path override, relative to project root").action((options) => {
5297
8180
  runDoctor4(options);
5298
8181
  });
8182
+ var pluginCommand = program2.command("plugin").description("Agent Plugins v1.0.0 portable package commands");
8183
+ pluginCommand.command("validate").description("Validate a plugin package against Agent Plugins v1.0.0").option("--root <path>", "Plugin root directory to validate (default: project root)").action((options) => {
8184
+ runPluginValidate(options);
8185
+ });
8186
+ var pathCommand = program2.command("path").description("harness/specs dir resolution checks (engine-backed)");
8187
+ pathCommand.command("resolve").description("Resolve {HARNESS_DIR} + {SPECS_DIR} from a start dir (exit 1 when no harness dir resolves)").argument("[path]", "Start dir to resolve from (default: cwd)").option("--json", "Machine-readable JSON output (ok, harnessDir, specsDir, guidance on failure)").action((pathArg, options) => {
8188
+ const startDir = pathArg ? path11.resolve(pathArg) : process.cwd();
8189
+ const harnessDir = resolveHarnessDir(startDir);
8190
+ if (!harnessDir) {
8191
+ const guidance = "no harness dir found (probed .mstar/, .agents/, .plans/, plans/ walking up from " + `${startDir}) \u2014 run \`mstar init\` to bootstrap, or pass a start dir inside a harness-enabled project`;
8192
+ if (options.json) {
8193
+ console.log(JSON.stringify({ ok: false, startDir, harnessDir: null, specsDir: null, guidance }));
8194
+ } else {
8195
+ console.error(import_picocolors.default.red(`path resolve: no harness dir from ${startDir}`));
8196
+ console.error(` guidance: ${guidance}`);
8197
+ }
8198
+ process.exitCode = 1;
8199
+ return;
8200
+ }
8201
+ const specsDir = resolveSpecsDir(harnessDir, { create: false });
8202
+ if (options.json) {
8203
+ console.log(JSON.stringify({ ok: true, startDir, harnessDir, specsDir }));
8204
+ } else {
8205
+ console.log(import_picocolors.default.green(`harness dir: ${harnessDir}`));
8206
+ console.log(import_picocolors.default.green(`specs dir: ${specsDir}`));
8207
+ }
8208
+ });
8209
+ var statusCommand = program2.command("status").description("status.json schema + residual lifecycle checks (engine-backed)");
8210
+ function resolveStatusFilePath(pathArg) {
8211
+ if (pathArg)
8212
+ return path11.resolve(pathArg);
8213
+ const harnessDir = resolveHarnessDir();
8214
+ if (!harnessDir) {
8215
+ throw new Error(`harness dir not found from ${process.cwd()} \u2014 pass a status.json path or set MSTAR_HARNESS_DIR`);
8216
+ }
8217
+ return path11.join(harnessDir, "status.json");
8218
+ }
8219
+ statusCommand.command("validate").description("Validate status.json (schema, severity enum, root-only residual_findings)").argument("[path]", "status.json path (default: {HARNESS_DIR}/status.json)").action((pathArg) => {
8220
+ let statusPath;
8221
+ try {
8222
+ statusPath = resolveStatusFilePath(pathArg);
8223
+ if (!fs7.existsSync(statusPath)) {
8224
+ throw new Error(`status file not found: ${statusPath}`);
8225
+ }
8226
+ const gate2 = validateStatus(statusPath);
8227
+ if (gate2.ok) {
8228
+ console.log(import_picocolors.default.green(`${statusPath}: OK`));
8229
+ return;
8230
+ }
8231
+ const count = gate2.violations.length;
8232
+ console.error(import_picocolors.default.red(`${statusPath}: FAIL (${count} violation${count === 1 ? "" : "s"})`));
8233
+ for (const violation7 of gate2.violations) {
8234
+ console.error(` - [${violation7.severity}] ${violation7.code}: ${violation7.message}`);
8235
+ if (violation7.fix)
8236
+ console.error(` fix: ${violation7.fix}`);
8237
+ }
8238
+ process.exitCode = 1;
8239
+ } catch (error) {
8240
+ console.error(import_picocolors.default.red(`status validate failed: ${error.message}`));
8241
+ process.exitCode = 1;
8242
+ }
8243
+ });
8244
+ statusCommand.command("archive-residuals").description("Archive a plan's open residuals to archived/residuals/<plan-id>.json").argument("<plan-id>", "Plan id whose open residuals are archived").option("--harness <path>", "Harness dir override (default: resolved {HARNESS_DIR})").action(async (planId, options) => {
8245
+ try {
8246
+ const harnessDir = options.harness ?? resolveHarnessDir();
8247
+ if (!harnessDir) {
8248
+ throw new Error(`harness dir not found from ${process.cwd()} \u2014 pass --harness or set MSTAR_HARNESS_DIR`);
8249
+ }
8250
+ const result = await archiveResiduals(planId, harnessDir);
8251
+ if (result.archived === 0) {
8252
+ console.log(import_picocolors.default.yellow(`No open residuals for plan ${planId}`));
8253
+ } else {
8254
+ console.log(import_picocolors.default.green(`Archived ${result.archived} residual(s) for ${planId} -> ${result.archivePath}`));
8255
+ }
8256
+ } catch (error) {
8257
+ console.error(import_picocolors.default.red(`archive-residuals failed: ${error.message}`));
8258
+ process.exitCode = 1;
8259
+ }
8260
+ });
8261
+ var leaseCommand = program2.command("lease").description("execution_lease checks (engine-backed) \u2014 integration_merge_lease validation stays import-only via @mstar-harness/engine until a dedicated subcommand exists");
8262
+ function resolveLeaseHarnessDir(harnessArg) {
8263
+ if (harnessArg)
8264
+ return path11.resolve(harnessArg);
8265
+ const harnessDir = resolveHarnessDir();
8266
+ if (!harnessDir) {
8267
+ throw new Error(`harness dir not found from ${process.cwd()} \u2014 pass --harness or set MSTAR_HARNESS_DIR`);
8268
+ }
8269
+ return harnessDir;
8270
+ }
8271
+ leaseCommand.command("verify").description("Verify a plan's execution_lease (missing/invalid/non-SSOT location \u2192 exit 1 with violations)").argument("<plan-id>", "Plan id whose execution_lease is verified").option("--harness <path>", "Harness dir override (default: resolved {HARNESS_DIR})").action((planId, options) => {
8272
+ try {
8273
+ const harnessDir = resolveLeaseHarnessDir(options.harness);
8274
+ const statusPath = path11.join(harnessDir, "status.json");
8275
+ if (!fs7.existsSync(statusPath)) {
8276
+ throw new Error(`status file not found: ${statusPath}`);
8277
+ }
8278
+ const doc = readJson2(statusPath);
8279
+ const plans = Array.isArray(doc.plans) ? doc.plans : [];
8280
+ const matches = plans.filter((row) => row?.id === planId || row?.plan_id === planId);
8281
+ if (matches.length === 0) {
8282
+ console.error(import_picocolors.default.red(`${statusPath}: FAIL plan ${planId}`));
8283
+ console.error(` - [high] lease.verify.plan-not-found: no plan row with id/plan_id ${planId}`);
8284
+ process.exitCode = 1;
8285
+ return;
8286
+ }
8287
+ if (matches.length > 1) {
8288
+ console.error(import_picocolors.default.red(`${statusPath}: FAIL plan ${planId}`));
8289
+ console.error(" - [high] lease.verify.ambiguous: multiple plan rows match (id and plan_id both present)");
8290
+ process.exitCode = 1;
8291
+ return;
8292
+ }
8293
+ const result = verifyPlanExecutionLease(matches[0], planId);
8294
+ if (result.ok) {
8295
+ const holder = String(result.lease.holder ?? "");
8296
+ console.log(import_picocolors.default.green(`${statusPath}: OK plan ${planId} \u2014 execution_lease valid (holder ${holder})`));
8297
+ return;
8298
+ }
8299
+ const count = result.violations.length;
8300
+ console.error(import_picocolors.default.red(`${statusPath}: FAIL plan ${planId} (${count} violation${count === 1 ? "" : "s"})`));
8301
+ for (const violation7 of result.violations) {
8302
+ console.error(` - [${violation7.severity}] ${violation7.code}: ${violation7.message}`);
8303
+ if (violation7.fix)
8304
+ console.error(` fix: ${violation7.fix}`);
8305
+ }
8306
+ process.exitCode = 1;
8307
+ } catch (error) {
8308
+ console.error(import_picocolors.default.red(`lease verify failed: ${error.message}`));
8309
+ process.exitCode = 1;
8310
+ }
8311
+ });
8312
+ function failScript(error, context) {
8313
+ if (error instanceof SddScriptError) {
8314
+ console.error(import_picocolors.default.red(`${context} failed: ${error.message}`));
8315
+ process.exitCode = error.exitCode;
8316
+ return;
8317
+ }
8318
+ console.error(import_picocolors.default.red(`${context} failed: ${error.message}`));
8319
+ process.exitCode = 1;
8320
+ }
8321
+ var sddCommand = program2.command("sdd").description("SDD workspace / task-brief / review-package helpers (engine-backed)");
8322
+ sddCommand.command("workspace").description("Resolve and ensure {SDD_DIR} for a plan (exit 1 on resolution failures, 2 on usage errors)").argument("[plan-id]", "Plan id whose SDD dir is resolved/created").argument("[control-root]", "Control worktree root (default: MSTAR_CONTROL_ROOT or the cwd's git top-level)").action((planId, controlRoot) => {
8323
+ try {
8324
+ if (!planId) {
8325
+ throw new SddScriptError(`usage: mstar sdd workspace PLAN_ID [CONTROL_ROOT]
8326
+ ` + " Set MSTAR_CONTROL_ROOT=<control_worktree_path> when running from a feature worktree.", 2);
8327
+ }
8328
+ const sddDir = sddWorkspace(planId, controlRoot ? { controlRoot } : {});
8329
+ console.log(import_picocolors.default.green(`sdd dir: ${sddDir}`));
8330
+ } catch (error) {
8331
+ failScript(error, "sdd workspace");
8332
+ }
8333
+ });
8334
+ sddCommand.command("task-brief").description("Extract the `## Task N` section of a plan into a brief file (exit 3 when task N is missing)").argument("[plan-file]", "Plan markdown file").argument("[task-number]", "Task number whose brief is extracted").argument("[outfile]", "Output file (default: {SDD_DIR}/task-N-brief.md)").action((planFile, taskNumber, outfile) => {
8335
+ try {
8336
+ if (!planFile || !taskNumber) {
8337
+ throw new SddScriptError("usage: mstar sdd task-brief PLAN_FILE TASK_NUMBER [OUTFILE]", 2);
8338
+ }
8339
+ const out = taskBrief(planFile, Number(taskNumber), outfile);
8340
+ console.log(import_picocolors.default.green(`task ${taskNumber} brief: ${out}`));
8341
+ } catch (error) {
8342
+ failScript(error, "sdd task-brief");
8343
+ }
8344
+ });
8345
+ sddCommand.command("review-package").description("Write commits + stat + diff -U10 for BASE..HEAD into a review file (exit 2 on bad refs)").argument("[base]", "Base ref (commit SHA)").argument("[head]", "Head ref (commit SHA)").argument("[outfile]", "Output file (default: {SDD_DIR}/review-<short-base>..<short-head>.diff)").action((base, head, outfile) => {
8346
+ try {
8347
+ if (!base || !head) {
8348
+ throw new SddScriptError("usage: mstar sdd review-package BASE HEAD [OUTFILE]", 2);
8349
+ }
8350
+ const out = reviewPackage(base, head, outfile);
8351
+ console.log(import_picocolors.default.green(`review package: ${out}`));
8352
+ } catch (error) {
8353
+ failScript(error, "sdd review-package");
8354
+ }
8355
+ });
8356
+ var iterationCommand = program2.command("iteration").description("iteration phase-gate + push-cadence checks (engine-backed)");
8357
+ function printChecklist(label, gate2) {
8358
+ if (gate2.ok) {
8359
+ console.log(import_picocolors.default.green(`${label}: OK`));
8360
+ return;
8361
+ }
8362
+ const count = gate2.violations.length;
8363
+ console.error(import_picocolors.default.red(`${label}: FAIL (${count} violation${count === 1 ? "" : "s"})`));
8364
+ for (const violation7 of gate2.violations) {
8365
+ console.error(` - [${violation7.severity}] ${violation7.code}: ${violation7.message}`);
8366
+ if (violation7.fix)
8367
+ console.error(` fix: ${violation7.fix}`);
8368
+ }
8369
+ }
8370
+ iterationCommand.command("gate").description("Evaluate the phase-transition gate: prints the transition (phase-2-execute / phase-3-close / phase-4-pr-delivery) " + "plus the \xA73.1 entry and \xA73.5 exit checklists. Exit 1 when the gate verdict fails \u2014 during the Phase-3 window " + "(transition: phase-3-close) exit 1 is EXPECTED until the \xA73.4 close items (status: completed + end_date) are " + "written: the exit checklist gates Phase 4, not the Phase-3 entry (qc2 F-003)").requiredOption("--status <path>", "status.json path").requiredOption("--compass <path>", "delivery-compass.md path").option("--branch <branch>", "Current branch probe (exit \xA73.5 item 5)").option("--integration <branch>", "Spec integration branch probe (exit \xA73.5 item 5)").option("--target <branch>", "PR base branch probe (exit \xA73.5 item 6)").action((options) => {
8371
+ try {
8372
+ const statusPath = path11.resolve(options.status);
8373
+ const compassPath = path11.resolve(options.compass);
8374
+ if (!fs7.existsSync(statusPath))
8375
+ throw new Error(`status file not found: ${statusPath}`);
8376
+ if (!fs7.existsSync(compassPath))
8377
+ throw new Error(`compass file not found: ${compassPath}`);
8378
+ const result = evaluatePhaseGate(readJson2(statusPath), parseCompassFrontmatter(compassPath), {
8379
+ currentBranch: options.branch,
8380
+ specIntegrationBranch: options.integration,
8381
+ prBaseBranch: options.target
8382
+ });
8383
+ console.log(`transition: ${result.transition}`);
8384
+ printChecklist("entry (close \xA73.1)", result.entry);
8385
+ printChecklist("exit (close \xA73.5)", result.exit);
8386
+ if (!result.ok)
8387
+ process.exitCode = 1;
8388
+ } catch (error) {
8389
+ console.error(import_picocolors.default.red(`iteration gate failed: ${error.message}`));
8390
+ process.exitCode = 1;
8391
+ }
8392
+ });
8393
+ iterationCommand.command("push-cadence").description("\xA75.1a push-cadence probe: never push while CI or an AI review wave is running (exit 1 when blocked)").option("--ci-running", "CI checks are still queued/in_progress on the current head").option("--review-wave", "An AI/bot review wave is still running on the current head").action((options) => {
8394
+ const result = pushCadenceProbe(Boolean(options.ciRunning), Boolean(options.reviewWave));
8395
+ if (result.ok) {
8396
+ console.log(import_picocolors.default.green("push allowed: CI idle, no AI review wave"));
8397
+ return;
8398
+ }
8399
+ const count = result.violations.length;
8400
+ console.error(import_picocolors.default.red(`push blocked (${count} violation${count === 1 ? "" : "s"})`));
8401
+ for (const violation7 of result.violations) {
8402
+ console.error(` - [${violation7.severity}] ${violation7.code}: ${violation7.message}`);
8403
+ if (violation7.fix)
8404
+ console.error(` fix: ${violation7.fix}`);
8405
+ }
8406
+ process.exitCode = 1;
8407
+ });
8408
+ var dispatchCommand = program2.command("dispatch").description("Assignment field + default-branch gate checks (engine-backed)");
8409
+ dispatchCommand.command("validate").description("Validate an Assignment markdown file: required header fields + exactly-one branch form, " + "then the default-protected-branch gate (exit 1 on violations, 2 on usage)").argument("[assignment-file]", "Assignment markdown file").option("--branch <branch>", "Gate branch context (default: derived from the Assignment's own branch forms, then $MSTAR_WORKING_BRANCH)").action((assignmentFile, options) => {
8410
+ try {
8411
+ if (!assignmentFile) {
8412
+ throw new SddScriptError("usage: dispatch validate <assignment-file> [--branch <branch>]", 2);
8413
+ }
8414
+ const file = path11.resolve(assignmentFile);
8415
+ if (!fs7.existsSync(file)) {
8416
+ throw new Error(`assignment file not found: ${file}`);
8417
+ }
8418
+ const text = fs7.readFileSync(file, "utf8");
8419
+ const readOnly = isReadOnlyAssignmentRole(parseAssignmentFields(text).executeAs ?? "");
8420
+ const violations = [...validateAssignmentFields(text, { writable: readOnly ? false : undefined }).violations];
8421
+ if (!readOnly) {
8422
+ const forms = parseAssignmentBranchForms(text);
8423
+ const branch = forms.createForm?.name ?? forms.workingBranch ?? forms.directOn?.branch ?? options.branch ?? process.env.MSTAR_WORKING_BRANCH;
8424
+ if (branch !== undefined && branch.trim() !== "") {
8425
+ const directOnException = parseBranchPolicyDirectOnBranch(text) === branch.trim();
8426
+ violations.push(...assertDefaultBranchProtected(branch, { directOnException }).violations);
8427
+ }
8428
+ }
8429
+ printChecklist("dispatch validate", { ok: violations.length === 0, violations });
8430
+ if (violations.length > 0)
8431
+ process.exitCode = 1;
8432
+ } catch (error) {
8433
+ failScript(error, "dispatch validate");
8434
+ }
8435
+ });
8436
+ var worktreeCommand = program2.command("worktree").description("L1/L2 pre-dispatch worktree checks (engine-backed)");
8437
+ function parseTracksArg(tracksJson) {
8438
+ let parsed;
8439
+ try {
8440
+ parsed = JSON.parse(tracksJson);
8441
+ } catch {
8442
+ throw new SddScriptError("usage: worktree check --l2 --tracks <json> \u2014 invalid JSON", 2);
8443
+ }
8444
+ if (!Array.isArray(parsed)) {
8445
+ throw new SddScriptError("usage: worktree check --l2 --tracks <json> \u2014 expected a JSON array of {worktreePath, workingBranch}", 2);
8446
+ }
8447
+ const tracks = [];
8448
+ for (const item of parsed) {
8449
+ const record = item;
8450
+ if (record === null || typeof record !== "object" || typeof record.worktreePath !== "string" || typeof record.workingBranch !== "string") {
8451
+ throw new SddScriptError("usage: worktree check --l2 --tracks <json> \u2014 every track needs string worktreePath + workingBranch", 2);
8452
+ }
8453
+ tracks.push({ worktreePath: record.worktreePath, workingBranch: record.workingBranch });
8454
+ }
8455
+ return tracks;
8456
+ }
8457
+ worktreeCommand.command("check").description("L1: verify the plan's execution_lease worktree vs control path (isolation, existence, branch alignment) from " + "status.json; --l2: verify parallel writable tracks (exit 1 on violations, 2 on usage)").argument("[plan-id]", "Plan id whose execution_lease drives the L1 input (alternative to --plan)").option("--plan <plan-id>", "Plan id whose execution_lease drives the L1 input").option("--status <path>", "status.json path override (default: {HARNESS_DIR}/status.json)").option("--control <path>", "Control worktree path override (default: status.json metadata.control_worktree_path)").option("--l2", "Run the L2 within-plan check (parallel writable tracks) instead of L1").option("--tracks <json>", 'L2 tracks JSON: [{"worktreePath": "/abs/path", "workingBranch": "feature/x"}] (required with --l2)').action((planId, options) => {
8458
+ try {
8459
+ if (options.l2) {
8460
+ if (!options.tracks) {
8461
+ throw new SddScriptError("usage: worktree check --l2 --tracks <json>", 2);
8462
+ }
8463
+ const gate3 = l2PreDispatchCheck({ tracks: parseTracksArg(options.tracks) });
8464
+ printChecklist("worktree L2 check", gate3);
8465
+ if (!gate3.ok)
8466
+ process.exitCode = 1;
8467
+ return;
8468
+ }
8469
+ const plan = options.plan ?? planId;
8470
+ if (!plan) {
8471
+ throw new SddScriptError("usage: worktree check <plan-id> [--status <path>] [--control <path>] (or --plan <plan-id>)", 2);
8472
+ }
8473
+ const statusPath = options.status ? path11.resolve(options.status) : resolveStatusFilePath();
8474
+ if (!fs7.existsSync(statusPath)) {
8475
+ throw new Error(`status file not found: ${statusPath}`);
8476
+ }
8477
+ const doc = readJson2(statusPath);
8478
+ const plans = Array.isArray(doc.plans) ? doc.plans : [];
8479
+ const matches = plans.filter((row2) => row2?.id === plan || row2?.plan_id === plan);
8480
+ if (matches.length === 0) {
8481
+ console.error(import_picocolors.default.red(`${statusPath}: FAIL plan ${plan}`));
8482
+ console.error(` - [high] worktree.l1.plan-not-found: no plan row with id/plan_id ${plan}`);
8483
+ process.exitCode = 1;
8484
+ return;
8485
+ }
8486
+ if (matches.length > 1) {
8487
+ console.error(import_picocolors.default.red(`${statusPath}: FAIL plan ${plan}`));
8488
+ console.error(" - [high] worktree.l1.ambiguous: multiple plan rows match (id and plan_id both present)");
8489
+ process.exitCode = 1;
8490
+ return;
8491
+ }
8492
+ const row = matches[0];
8493
+ const lease = row.execution_lease ?? {};
8494
+ const metadata = doc.metadata ?? {};
8495
+ const input = {
8496
+ controlWorktreePath: options.control ? path11.resolve(options.control) : String(metadata.control_worktree_path ?? ""),
8497
+ leaseWorktreePath: String(lease.worktree_path ?? ""),
8498
+ leaseWorkingBranch: String(lease.working_branch ?? ""),
8499
+ planId: plan
8500
+ };
8501
+ const gate2 = l1PreDispatchCheck(input);
8502
+ printChecklist("worktree L1 check", gate2);
8503
+ if (!gate2.ok)
8504
+ process.exitCode = 1;
8505
+ } catch (error) {
8506
+ failScript(error, "worktree check");
8507
+ }
8508
+ });
8509
+ var reviewCommand = program2.command("review").description("QC seat-mapping checks (engine-backed)");
8510
+ function parseAssignmentExecutionMode(assignmentText) {
8511
+ for (const line of assignmentText.split(/\r?\n/)) {
8512
+ const match = line.match(/^\*\*\s*Execution mode\s*\*\*\s*:\s*(.*)$/) ?? line.match(/^Execution mode\s*:\s*(.*)$/);
8513
+ if (match)
8514
+ return match[1].trim();
8515
+ }
8516
+ return "";
8517
+ }
8518
+ reviewCommand.command("seats").description("Map an Assignment's execution mode to its QC seat count N; with --reviewers, verify tri identity on sdd " + "(exit 1 on violations, 2 on usage)").argument("[assignment-file]", "Assignment markdown file").option("--mode <mode>", "Execution mode override (sdd | inline | targeted; default: the Assignment's Execution mode field)").option("--reviewers <list>", "Comma-separated reviewer roles (targeted seats; tri-identity checked when mode is sdd)").action((assignmentFile, options) => {
8519
+ try {
8520
+ if (!assignmentFile) {
8521
+ throw new SddScriptError("usage: review seats <assignment-file> [--mode sdd|inline|targeted] [--reviewers <role1,role2,...>]", 2);
8522
+ }
8523
+ const file = path11.resolve(assignmentFile);
8524
+ if (!fs7.existsSync(file)) {
8525
+ throw new Error(`assignment file not found: ${file}`);
8526
+ }
8527
+ const text = fs7.readFileSync(file, "utf8");
8528
+ const mode = options.mode ?? parseAssignmentExecutionMode(text);
8529
+ const reviewers = (options.reviewers ?? "").split(",").map((role) => role.trim()).filter((role) => role !== "");
8530
+ const result = executionModeToN(mode, { seats: reviewers });
8531
+ if (!result.ok) {
8532
+ printChecklist("review seats", result);
8533
+ process.exitCode = 1;
8534
+ return;
8535
+ }
8536
+ const normalizedMode = mode.trim().toLowerCase().split(/\s+/)[0] ?? "";
8537
+ if (normalizedMode === "sdd" && reviewers.length > 0) {
8538
+ const tri = assertTriIdentity(reviewers);
8539
+ if (!tri.ok) {
8540
+ printChecklist("review seats (tri identity)", tri);
8541
+ process.exitCode = 1;
8542
+ return;
8543
+ }
8544
+ }
8545
+ console.log(import_picocolors.default.green(`seats: ${result.n}`));
8546
+ } catch (error) {
8547
+ failScript(error, "review seats");
8548
+ }
8549
+ });
8550
+ function lookupTable(values) {
8551
+ const table = {};
8552
+ for (const value of values)
8553
+ table[value] = true;
8554
+ return table;
8555
+ }
8556
+ var LINT_SKIP_DIRS = {
8557
+ node_modules: true,
8558
+ ".git": true,
8559
+ dist: true,
8560
+ coverage: true,
8561
+ ".turbo": true
8562
+ };
8563
+ var LINT_CODE_EXTENSIONS = {
8564
+ ".ts": true,
8565
+ ".tsx": true,
8566
+ ".mts": true,
8567
+ ".cts": true,
8568
+ ".js": true,
8569
+ ".jsx": true,
8570
+ ".mjs": true,
8571
+ ".cjs": true,
8572
+ ".py": true,
8573
+ ".go": true,
8574
+ ".rs": true,
8575
+ ".sh": true,
8576
+ ".bash": true,
8577
+ ".zsh": true,
8578
+ ".rb": true,
8579
+ ".java": true,
8580
+ ".kt": true,
8581
+ ".swift": true
8582
+ };
8583
+ function lintTargetType(filePath) {
8584
+ const base = path11.basename(filePath);
8585
+ if (base === "STRATEGY.md")
8586
+ return "strategy";
8587
+ if (base === "SKILL.md")
8588
+ return "skill";
8589
+ if (/^task-\d+-report\.md$/i.test(base))
8590
+ return "report";
8591
+ const dir = path11.dirname(filePath);
8592
+ if (dir.includes(`${path11.sep}plans${path11.sep}`) || dir.endsWith(`${path11.sep}plans`))
8593
+ return "plan";
8594
+ if (/^\d{8}-[a-z0-9.-]+\.md$/i.test(base))
8595
+ return "plan";
8596
+ if (LINT_CODE_EXTENSIONS[path11.extname(base).toLowerCase()] === true)
8597
+ return "code";
8598
+ return null;
8599
+ }
8600
+ function collectLintTargets(dir) {
8601
+ const targets = [];
8602
+ const walk = (current) => {
8603
+ for (const entry of fs7.readdirSync(current, { withFileTypes: true })) {
8604
+ if (entry.isDirectory()) {
8605
+ if (LINT_SKIP_DIRS[entry.name] !== true)
8606
+ walk(path11.join(current, entry.name));
8607
+ } else if (entry.isFile() && lintTargetType(path11.join(current, entry.name)) !== null) {
8608
+ targets.push(path11.join(current, entry.name));
8609
+ }
8610
+ }
8611
+ };
8612
+ walk(dir);
8613
+ return targets;
8614
+ }
8615
+ function lintOneFile(filePath) {
8616
+ const abs = path11.resolve(filePath);
8617
+ const text = fs7.readFileSync(abs, "utf8");
8618
+ const violations = [];
8619
+ const markers = [];
8620
+ switch (lintTargetType(abs)) {
8621
+ case "plan":
8622
+ violations.push(...planQualityBar(text).violations);
8623
+ break;
8624
+ case "skill":
8625
+ violations.push(...lintSkillFrontmatter(text).violations);
8626
+ break;
8627
+ case "strategy":
8628
+ violations.push(...lintStrategySections(text).violations);
8629
+ break;
8630
+ case "report":
8631
+ violations.push(...assertSddTddTriple(text).violations);
8632
+ break;
8633
+ case "code": {
8634
+ for (const marker of findSimplifyMarkers(text)) {
8635
+ markers.push(`simplify marker @${marker.line}: ${marker.text}`);
8636
+ }
8637
+ const temporary = findTemporaryMarkers(text);
8638
+ for (const marker of temporary.markers) {
8639
+ const removal = marker.removalPath === null ? "no removal path" : `removal: ${marker.removalPath}`;
8640
+ markers.push(`temporary marker @${marker.line}: ${marker.text} (${removal})`);
8641
+ }
8642
+ violations.push(...temporary.violations);
8643
+ break;
8644
+ }
8645
+ default:
8646
+ throw new SddScriptError(`usage: lint <target> \u2014 unsupported file type "${path11.basename(abs)}" (lintable: plan files, SKILL.md, STRATEGY.md, task-N-report.md, code files)`, 2);
8647
+ }
8648
+ return { violations, markers };
8649
+ }
8650
+ var lintCommand = program2.command("lint").description("lint harness artifacts by content type (engine-backed): plan files \u2192 quality bar, SKILL.md \u2192 frontmatter, " + "STRATEGY.md \u2192 required sections, task-N-report.md \u2192 SDD TDD triple, code files \u2192 simplify:/temporary markers");
8651
+ lintCommand.description("Lint <target> (file or dir) \u2014 exit 1 on violations, 2 on usage").argument("[target]", "File or directory to lint").action((target) => {
8652
+ try {
8653
+ if (!target)
8654
+ throw new SddScriptError("usage: lint <target> (file or dir)", 2);
8655
+ const abs = path11.resolve(target);
8656
+ if (!fs7.existsSync(abs))
8657
+ throw new Error(`lint target not found: ${abs}`);
8658
+ const targets = fs7.statSync(abs).isDirectory() ? collectLintTargets(abs) : [abs];
8659
+ if (targets.length === 0) {
8660
+ console.log(import_picocolors.default.yellow(`lint: no lintable files under ${target}`));
8661
+ return;
8662
+ }
8663
+ let violations = 0;
8664
+ for (const file of targets) {
8665
+ const label = `lint ${file}`;
8666
+ let result;
8667
+ try {
8668
+ result = lintOneFile(file);
8669
+ } catch (error) {
8670
+ if (error instanceof SddScriptError)
8671
+ throw error;
8672
+ console.error(import_picocolors.default.red(`${label}: ERROR \u2014 ${error.message}`));
8673
+ violations++;
8674
+ continue;
8675
+ }
8676
+ for (const marker of result.markers)
8677
+ console.log(` ${import_picocolors.default.cyan(marker)}`);
8678
+ if (result.violations.length === 0) {
8679
+ console.log(import_picocolors.default.green(`${label}: OK`));
8680
+ continue;
8681
+ }
8682
+ violations += result.violations.length;
8683
+ const count = result.violations.length;
8684
+ console.error(import_picocolors.default.red(`${label}: FAIL (${count} violation${count === 1 ? "" : "s"})`));
8685
+ for (const violation7 of result.violations) {
8686
+ console.error(` - [${violation7.severity}] ${violation7.code}: ${violation7.message}`);
8687
+ if (violation7.fix)
8688
+ console.error(` fix: ${violation7.fix}`);
8689
+ }
8690
+ }
8691
+ if (violations > 0)
8692
+ process.exitCode = 1;
8693
+ } catch (error) {
8694
+ failScript(error, "lint");
8695
+ }
8696
+ });
8697
+ function stripFrontmatter(text) {
8698
+ const lines = text.replace(/^\uFEFF/, "").split(/\r?\n/);
8699
+ if (lines.length === 0 || lines[0].trim() !== "---")
8700
+ return text;
8701
+ for (let i = 1;i < lines.length; i++) {
8702
+ if (lines[i].trim() === "---")
8703
+ return lines.slice(i + 1).join(`
8704
+ `);
8705
+ }
8706
+ return text;
8707
+ }
8708
+ var designMdCommand = program2.command("design-md").description("DESIGN.md token frontmatter / light-dark parity / completeness checks (engine-backed)");
8709
+ designMdCommand.command("validate").description("Validate DESIGN.md in <dir>: token frontmatter, light/dark parity when DESIGN.dark.md exists, " + "and the completeness level (exit 1 on violations, 2 on usage)").argument("[dir]", "Directory containing DESIGN.md").action((dir) => {
8710
+ try {
8711
+ if (!dir)
8712
+ throw new SddScriptError("usage: design-md validate <dir>", 2);
8713
+ const abs = path11.resolve(dir);
8714
+ const lightPath = path11.join(abs, "DESIGN.md");
8715
+ if (!fs7.existsSync(lightPath))
8716
+ throw new Error(`design file not found: ${lightPath}`);
8717
+ const light = fs7.readFileSync(lightPath, "utf8");
8718
+ const violations = [];
8719
+ const tokens = validateDesignTokenFrontmatter(light);
8720
+ printChecklist("design-md validate (tokens)", tokens);
8721
+ violations.push(...tokens.violations);
8722
+ const darkPath = path11.join(abs, "DESIGN.dark.md");
8723
+ if (fs7.existsSync(darkPath)) {
8724
+ const parity = assertLightDarkParity(light, fs7.readFileSync(darkPath, "utf8"));
8725
+ printChecklist("design-md validate (light/dark parity)", parity);
8726
+ violations.push(...parity.violations);
8727
+ }
8728
+ const level = completenessLevel(light);
8729
+ console.log(`design-md completeness level: ${level.level}`);
8730
+ if (level.missing.length > 0)
8731
+ console.log(` missing for next level: ${level.missing.join(", ")}`);
8732
+ if (level.bodyUnverified) {
8733
+ console.log(import_picocolors.default.yellow(" note: body-only checklist items not verified \u2014 Production caps at Standard"));
8734
+ }
8735
+ if (violations.length > 0)
8736
+ process.exitCode = 1;
8737
+ } catch (error) {
8738
+ failScript(error, "design-md validate");
8739
+ }
8740
+ });
8741
+ function parseAuditFindings(text) {
8742
+ let parsed;
8743
+ try {
8744
+ parsed = JSON.parse(text);
8745
+ } catch {
8746
+ throw new SddScriptError("usage: audit scaffold \u2014 findings file is not valid JSON", 2);
8747
+ }
8748
+ if (!Array.isArray(parsed))
8749
+ throw new SddScriptError("usage: audit scaffold \u2014 findings file must be a JSON array", 2);
8750
+ return parsed.map((raw, index) => {
8751
+ if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
8752
+ throw new SddScriptError(`usage: audit scaffold \u2014 findings[${index}] is not an object`, 2);
8753
+ }
8754
+ const finding = raw;
8755
+ const title = typeof finding.title === "string" ? finding.title.trim() : "";
8756
+ const impact = typeof finding.description === "string" ? finding.description.trim() : "";
8757
+ const priority = typeof finding.priority === "string" ? finding.priority : "";
8758
+ const effort = typeof finding.effort === "string" ? finding.effort : "";
8759
+ const risk = typeof finding.risk === "string" ? finding.risk : "";
8760
+ const category = typeof finding.category === "string" ? finding.category : "";
8761
+ if (title === "" || impact === "") {
8762
+ throw new SddScriptError(`usage: audit scaffold \u2014 findings[${index}] needs non-empty title and description`, 2);
8763
+ }
8764
+ if (AUDIT_PRIORITY_LOOKUP[priority] !== true) {
8765
+ throw new SddScriptError(`usage: audit scaffold \u2014 findings[${index}].priority must be one of ${AUDIT_PRIORITIES.join("|")}`, 2);
8766
+ }
8767
+ if (AUDIT_EFFORT_LOOKUP[effort] !== true) {
8768
+ throw new SddScriptError(`usage: audit scaffold \u2014 findings[${index}].effort must be one of ${AUDIT_EFFORTS.join("|")}`, 2);
8769
+ }
8770
+ if (AUDIT_RISK_LOOKUP[risk] !== true) {
8771
+ throw new SddScriptError(`usage: audit scaffold \u2014 findings[${index}].risk must be one of ${AUDIT_RISKS.join("|")}`, 2);
8772
+ }
8773
+ if (AUDIT_CATEGORY_LOOKUP[category] !== true) {
8774
+ throw new SddScriptError(`usage: audit scaffold \u2014 findings[${index}].category must be one of ${AUDIT_CATEGORIES.join("|")}`, 2);
8775
+ }
8776
+ const rawDependsOn = typeof finding.dependsOn === "string" && finding.dependsOn.trim() !== "" ? finding.dependsOn.trim() : undefined;
8777
+ if (rawDependsOn !== undefined && !/^(?:none|plans\/\d{3}-[\w.*-]+\.md|\d{3})$/i.test(rawDependsOn)) {
8778
+ throw new SddScriptError(`usage: audit scaffold \u2014 findings[${index}].dependsOn must be "none", "plans/NNN-*.md", or a plan number NNN`, 2);
8779
+ }
8780
+ const dependsOn = rawDependsOn === undefined ? undefined : /^\d{3}$/.test(rawDependsOn) ? `plans/${rawDependsOn}-*.md` : rawDependsOn;
8781
+ return {
8782
+ title,
8783
+ category,
8784
+ impact,
8785
+ effort,
8786
+ risk,
8787
+ confidence: "MED",
8788
+ evidence: [],
8789
+ priority,
8790
+ dependsOn
8791
+ };
8792
+ });
8793
+ }
8794
+ var auditCommand = program2.command("audit").description("audit plan scaffold (engine-backed)");
8795
+ function resolveAuditShortSha(cwd, override) {
8796
+ if (override !== undefined && override !== "")
8797
+ return override;
8798
+ try {
8799
+ const out = execFileSync6("git", ["rev-parse", "--short", "HEAD"], {
8800
+ cwd,
8801
+ encoding: "utf8",
8802
+ stdio: ["ignore", "pipe", "ignore"]
8803
+ });
8804
+ return out.trim();
8805
+ } catch {
8806
+ return "unknown";
8807
+ }
8808
+ }
8809
+ auditCommand.command("scaffold").description("Scaffold an audit-<date>/ plan directory (numbered plan files + README index) from a JSON findings file " + "(exit 2 on usage)").argument("[findings-file]", "JSON file: array of {title, priority, effort, risk, category, dependsOn?, description}").option("--dir <out-dir>", "Output directory (default: ./audit-<date> from --date or today)").option("--sha <commit>", "Short commit SHA for the Planned-at field (default: git rev-parse --short HEAD)").option("--date <YYYY-MM-DD>", "Audit date (default: today)").option("--repo <name>", "Repository name for the README title (default: repo)").action((findingsFile, options) => {
8810
+ try {
8811
+ if (!findingsFile)
8812
+ throw new SddScriptError("usage: audit scaffold <findings-file> [--dir <out-dir>]", 2);
8813
+ const abs = path11.resolve(findingsFile);
8814
+ if (!fs7.existsSync(abs))
8815
+ throw new Error(`findings file not found: ${abs}`);
8816
+ if (options.date !== undefined && !/^\d{4}-\d{2}-\d{2}$/.test(options.date)) {
8817
+ throw new SddScriptError("usage: audit scaffold \u2014 --date must be YYYY-MM-DD", 2);
8818
+ }
8819
+ if (options.sha !== undefined && !/^[0-9a-f]{7,40}$/.test(options.sha)) {
8820
+ throw new SddScriptError("usage: audit scaffold \u2014 --sha must be a 7-40 char hex commit SHA", 2);
8821
+ }
8822
+ const date = options.date ?? new Date().toISOString().slice(0, 10);
8823
+ const outDir = options.dir !== undefined ? path11.resolve(options.dir) : path11.resolve(`audit-${date}`);
8824
+ const findings = parseAuditFindings(fs7.readFileSync(abs, "utf8"));
8825
+ const sha = resolveAuditShortSha(process.cwd(), options.sha);
8826
+ const result = scaffoldAuditPlan(outDir, findings, { date, repoName: options.repo, repoShortSha: sha });
8827
+ const count = result.files.length;
8828
+ console.log(import_picocolors.default.green(`audit scaffold: OK \u2014 ${count} plan file${count === 1 ? "" : "s"} in ${result.outDir}`));
8829
+ for (const file of result.files)
8830
+ console.log(` created: ${file}`);
8831
+ } catch (error) {
8832
+ failScript(error, "audit scaffold");
8833
+ }
8834
+ });
8835
+ var compoundCommand = program2.command("compound").description("knowledge-doc schema / index / scope checks (engine-backed)");
8836
+ 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) => {
8837
+ try {
8838
+ if (!docPath)
8839
+ throw new SddScriptError("usage: compound validate <doc-path> [--knowledge-dir <dir>]", 2);
8840
+ const abs = path11.resolve(docPath);
8841
+ if (!fs7.existsSync(abs))
8842
+ throw new Error(`knowledge doc not found: ${abs}`);
8843
+ const text = fs7.readFileSync(abs, "utf8");
8844
+ const violations = [];
8845
+ const schema = validateSchemaYaml(text);
8846
+ printChecklist("compound validate (schema)", schema);
8847
+ violations.push(...schema.violations);
8848
+ if (options.knowledgeDir !== undefined) {
8849
+ const knowledgeDir = path11.resolve(options.knowledgeDir);
8850
+ const index = assertIndexRows(knowledgeDir);
8851
+ printChecklist("compound validate (index rows)", index);
8852
+ violations.push(...index.violations);
8853
+ const scope = scopeGuard(abs, [knowledgeDir]);
8854
+ printChecklist("compound validate (scope guard)", scope);
8855
+ violations.push(...scope.violations);
8856
+ }
8857
+ if (violations.length > 0)
8858
+ process.exitCode = 1;
8859
+ } catch (error) {
8860
+ failScript(error, "compound validate");
8861
+ }
8862
+ });
8863
+ var AUDIT_PRIORITY_LOOKUP = lookupTable(AUDIT_PRIORITIES);
8864
+ var AUDIT_EFFORT_LOOKUP = lookupTable(AUDIT_EFFORTS);
8865
+ var AUDIT_RISK_LOOKUP = lookupTable(AUDIT_RISKS);
8866
+ var AUDIT_CATEGORY_LOOKUP = lookupTable(AUDIT_CATEGORIES);
8867
+ var HOST_SIGNALS = [
8868
+ "subagent_type",
8869
+ "question",
8870
+ "task_subagent",
8871
+ "task_agent_batch",
8872
+ "ask",
8873
+ "hub",
8874
+ "Agent",
8875
+ "AgentSwarm",
8876
+ "AskUserQuestion",
8877
+ "EnterPlanMode",
8878
+ "TodoWrite",
8879
+ "plan_slash",
8880
+ "goal",
8881
+ "functions.*",
8882
+ "tool_search"
8883
+ ];
8884
+ var HOST_SIGNAL_LOOKUP = lookupTable(HOST_SIGNALS);
8885
+ var hostCommand = program2.command("host").description("host detection from session tool shapes (engine-backed)");
8886
+ hostCommand.command("detect").description("Detect the active host from --signals (comma-separated tool-shape tokens): prints the host id or " + "'ambiguous' (exit 2 on usage)").option("--signals <list>", "Comma-separated tool-shape signals (e.g. question,ask,hub)").action((options) => {
8887
+ try {
8888
+ if (!options.signals)
8889
+ throw new SddScriptError("usage: host detect --signals <comma-list>", 2);
8890
+ const signals2 = options.signals.split(",").map((signal) => signal.trim()).filter((signal) => signal !== "");
8891
+ if (signals2.length === 0)
8892
+ throw new SddScriptError("usage: host detect --signals <comma-list>", 2);
8893
+ for (const signal of signals2) {
8894
+ if (HOST_SIGNAL_LOOKUP[signal] !== true) {
8895
+ throw new SddScriptError(`usage: host detect \u2014 unknown signal "${signal}" (valid: ${HOST_SIGNALS.join(", ")})`, 2);
8896
+ }
8897
+ }
8898
+ const result = detectHost(signals2);
8899
+ if (result === "ambiguous") {
8900
+ console.log(import_picocolors.default.yellow("host: ambiguous \u2014 apply the mstar-host detection table and prompt judgment"));
8901
+ } else {
8902
+ console.log(import_picocolors.default.green(`host: ${result}`));
8903
+ }
8904
+ } catch (error) {
8905
+ failScript(error, "host detect");
8906
+ }
8907
+ });
8908
+ var skillCommand = program2.command("skill").description("skill-authoring lints (engine-backed)");
8909
+ skillCommand.command("lint").description("Lint <skill-dir>/SKILL.md: frontmatter contract (name lowercase-hyphen, description trigger contract) " + "+ the five-question body (exit 1 on violations, 2 on usage)").argument("[skill-dir]", "Skill directory containing SKILL.md").action((skillDir) => {
8910
+ try {
8911
+ if (!skillDir)
8912
+ throw new SddScriptError("usage: skill lint <skill-dir>", 2);
8913
+ const skillFile = path11.join(path11.resolve(skillDir), "SKILL.md");
8914
+ if (!fs7.existsSync(skillFile))
8915
+ throw new Error(`SKILL.md not found: ${skillFile}`);
8916
+ const text = fs7.readFileSync(skillFile, "utf8");
8917
+ const violations = [];
8918
+ const frontmatter = lintSkillFrontmatter(text);
8919
+ printChecklist("skill lint (frontmatter)", frontmatter);
8920
+ violations.push(...frontmatter.violations);
8921
+ const fiveQuestion = lintFiveQuestion(stripFrontmatter(text));
8922
+ printChecklist("skill lint (five questions)", fiveQuestion);
8923
+ violations.push(...fiveQuestion.violations);
8924
+ if (violations.length > 0)
8925
+ process.exitCode = 1;
8926
+ } catch (error) {
8927
+ failScript(error, "skill lint");
8928
+ }
8929
+ });
5299
8930
  program2.parseAsync(process.argv).catch((error) => {
5300
8931
  console.error(import_picocolors.default.red(`Setup failed: ${error.message}`));
5301
- process.exit(1);
8932
+ process.exitCode = 1;
5302
8933
  });