@mstar-harness/cli 1.8.9 → 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 +3376 -178
  2. package/package.json +6 -2
@@ -2344,7 +2344,8 @@ var require_commander = __commonJS((exports) => {
2344
2344
  });
2345
2345
 
2346
2346
  // src/index.ts
2347
- import fs9 from "fs";
2347
+ import { execFileSync as execFileSync6 } from "child_process";
2348
+ import fs7 from "fs";
2348
2349
  import path11 from "path";
2349
2350
 
2350
2351
  // ../../node_modules/@inquirer/core/dist/lib/key.js
@@ -3973,52 +3974,2535 @@ var {
3973
3974
  Help
3974
3975
  } = import__.default;
3975
3976
 
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"];
4000
+ function readJson(filePath) {
4001
+ if (!existsSync(filePath))
4002
+ return {};
4003
+ const content = readFileSync(filePath, "utf8").trim();
4004
+ if (!content)
4005
+ return {};
4006
+ try {
4007
+ return JSON.parse(content);
4008
+ } catch (error) {
4009
+ throw new Error(`Invalid JSON in ${filePath}: ${error.message}`);
4010
+ }
4011
+ }
4012
+ function writeJson(filePath, value) {
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)}
4018
+ `, "utf8");
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) {
4097
+ try {
4098
+ return statSync(dir).isDirectory();
4099
+ } catch {
4100
+ return false;
4101
+ }
4102
+ }
4103
+ function hasFiles(dir) {
4104
+ try {
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;
4114
+ } catch {
4115
+ return false;
4116
+ }
4117
+ }
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);
4147
+ }
4148
+ }
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
+ }
4168
+ }
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;
4195
+ }
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);
4207
+ }
4208
+ }
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"));
4307
+ } else {
4308
+ n = seats.length;
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"));
4312
+ }
4313
+ return n === undefined ? { ok: false, violations } : { ok: true, violations, n };
4314
+ }
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
+ }
4375
+ }
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)}`));
4382
+ }
4383
+ if (metadata !== undefined && !isPlainObject2(metadata)) {
4384
+ violations.push(violation3("medium", "status.plan-row.invalid-metadata", "metadata must be an object"));
4385
+ }
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"));
4388
+ }
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 };
4393
+ }
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
+ }
4443
+ }
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
+ };
4457
+ }
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);
4482
+ }
4483
+ }
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
+ }
4500
+ }
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"));
4507
+ }
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`);
4514
+ }
4515
+ assertSafePathComponent(planId, "planId");
4516
+ const statusPath = join4(dir, "status.json");
4517
+ if (!existsSync2(statusPath)) {
4518
+ throw new Error(`status file not found: ${statusPath}`);
4519
+ }
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
+ });
4547
+ }
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"]);
3976
6490
  // src/agent-plugins.ts
3977
- import fs2 from "node:fs";
6491
+ import fs from "node:fs";
3978
6492
  import path3 from "node:path";
3979
6493
 
3980
6494
  // src/utils.ts
3981
- import fs from "node:fs";
3982
6495
  import path2 from "node:path";
3983
- import { fileURLToPath } from "node:url";
3984
6496
  function ensureObject(value) {
3985
6497
  if (value && typeof value === "object" && !Array.isArray(value))
3986
6498
  return value;
3987
6499
  return {};
3988
6500
  }
3989
- function readJson(filePath) {
3990
- if (!fs.existsSync(filePath))
3991
- return {};
3992
- const content = fs.readFileSync(filePath, "utf8").trim();
3993
- if (!content)
3994
- return {};
3995
- try {
3996
- return JSON.parse(content);
3997
- } catch (error) {
3998
- throw new Error(`Invalid JSON in ${filePath}: ${error.message}`);
3999
- }
4000
- }
4001
- function writeJson(filePath, value) {
4002
- const parent = path2.dirname(filePath);
4003
- if (!fs.existsSync(parent))
4004
- fs.mkdirSync(parent, { recursive: true });
4005
- fs.writeFileSync(filePath, `${JSON.stringify(value, null, 2)}
4006
- `, "utf8");
4007
- }
4008
- function resolveProjectRoot() {
6501
+ function resolveProjectRoot2() {
4009
6502
  const candidate = process.env.MSTAR_CLI_PROJECT_ROOT || process.env.INIT_CWD || process.env.PWD;
4010
6503
  if (candidate && candidate.trim())
4011
6504
  return path2.resolve(candidate);
4012
- return process.cwd();
4013
- }
4014
- function readHarnessVersion() {
4015
- const packageJsonPath = path2.resolve(path2.dirname(fileURLToPath(import.meta.url)), "../package.json");
4016
- try {
4017
- const parsed = JSON.parse(fs.readFileSync(packageJsonPath, "utf8"));
4018
- return parsed.version || "0.0.0";
4019
- } catch {
4020
- return "0.0.0";
4021
- }
6505
+ return resolveProjectRoot();
4022
6506
  }
4023
6507
 
4024
6508
  // src/agent-plugins.ts
@@ -4069,18 +6553,18 @@ function describeType(value) {
4069
6553
  return "array";
4070
6554
  return typeof value;
4071
6555
  }
4072
- function isPlainObject2(value) {
6556
+ function isPlainObject6(value) {
4073
6557
  return typeof value === "object" && value !== null && !Array.isArray(value);
4074
6558
  }
4075
- function parseScalar(raw) {
6559
+ function parseScalar3(raw) {
4076
6560
  const trimmed = raw.trim();
4077
6561
  if (trimmed.length >= 2 && (trimmed.startsWith('"') && trimmed.endsWith('"') || trimmed.startsWith("'") && trimmed.endsWith("'"))) {
4078
6562
  return trimmed.slice(1, -1);
4079
6563
  }
4080
6564
  return trimmed;
4081
6565
  }
4082
- function parseFrontmatter(filePath) {
4083
- const content = fs2.readFileSync(filePath, "utf8");
6566
+ function parseFrontmatter2(filePath) {
6567
+ const content = fs.readFileSync(filePath, "utf8");
4084
6568
  const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n/);
4085
6569
  if (!match)
4086
6570
  return null;
@@ -4089,7 +6573,7 @@ function parseFrontmatter(filePath) {
4089
6573
  const field = line.match(/^([A-Za-z0-9_-]+):\s*(.*)$/);
4090
6574
  if (!field)
4091
6575
  continue;
4092
- result[field[1]] = parseScalar(field[2]);
6576
+ result[field[1]] = parseScalar3(field[2]);
4093
6577
  }
4094
6578
  return result;
4095
6579
  }
@@ -4113,7 +6597,7 @@ function isValidMcpUrl(raw) {
4113
6597
  return true;
4114
6598
  }
4115
6599
  function validateManifest(manifest, errors2, warnings) {
4116
- if (!isPlainObject2(manifest)) {
6600
+ if (!isPlainObject6(manifest)) {
4117
6601
  errors2.push("plugin.json: manifest must be a JSON object");
4118
6602
  return;
4119
6603
  }
@@ -4149,7 +6633,7 @@ function validateManifest(manifest, errors2, warnings) {
4149
6633
  }
4150
6634
  }
4151
6635
  if (doc.author !== undefined) {
4152
- if (!isPlainObject2(doc.author)) {
6636
+ if (!isPlainObject6(doc.author)) {
4153
6637
  errors2.push('plugin.json: "author" must be an object with optional string fields name/email/url');
4154
6638
  } else {
4155
6639
  const author = doc.author;
@@ -4172,11 +6656,11 @@ function validateManifest(manifest, errors2, warnings) {
4172
6656
  }
4173
6657
  }
4174
6658
  if (doc.extensions !== undefined) {
4175
- if (!isPlainObject2(doc.extensions)) {
6659
+ if (!isPlainObject6(doc.extensions)) {
4176
6660
  warnings.push('plugin.json: "extensions" is not an object — ignored');
4177
6661
  } else {
4178
6662
  for (const [namespace, value] of Object.entries(doc.extensions)) {
4179
- if (!isPlainObject2(value)) {
6663
+ if (!isPlainObject6(value)) {
4180
6664
  warnings.push(`plugin.json: "extensions.${namespace}" is not an object — ignored`);
4181
6665
  }
4182
6666
  }
@@ -4185,7 +6669,7 @@ function validateManifest(manifest, errors2, warnings) {
4185
6669
  }
4186
6670
  function validateMcpServer(name, entry, errors2) {
4187
6671
  const prefix = `mcp.json: mcpServers.${name}`;
4188
- if (!isPlainObject2(entry)) {
6672
+ if (!isPlainObject6(entry)) {
4189
6673
  errors2.push(`${prefix} must be an object`);
4190
6674
  return;
4191
6675
  }
@@ -4219,7 +6703,7 @@ function validateMcpServer(name, entry, errors2) {
4219
6703
  }
4220
6704
  }
4221
6705
  if (server.env !== undefined) {
4222
- if (!isPlainObject2(server.env)) {
6706
+ if (!isPlainObject6(server.env)) {
4223
6707
  errors2.push(`${prefix}: "env" must be an object of strings`);
4224
6708
  } else {
4225
6709
  for (const [key, value] of Object.entries(server.env)) {
@@ -4258,7 +6742,7 @@ function validateMcpServer(name, entry, errors2) {
4258
6742
  errors2.push(`${prefix}: "url" must be an absolute http(s) URL without user info or fragment; non-loopback endpoints must use https`);
4259
6743
  }
4260
6744
  if (server.headers !== undefined) {
4261
- if (!isPlainObject2(server.headers)) {
6745
+ if (!isPlainObject6(server.headers)) {
4262
6746
  errors2.push(`${prefix}: "headers" must be an object of strings`);
4263
6747
  } else {
4264
6748
  const seen = new Set;
@@ -4286,16 +6770,16 @@ function validateMcpServer(name, entry, errors2) {
4286
6770
  }
4287
6771
  function validateMcp(root, manifestSchema, errors2) {
4288
6772
  const mcpPath = path3.join(root, "mcp.json");
4289
- if (!fs2.existsSync(mcpPath))
6773
+ if (!fs.existsSync(mcpPath))
4290
6774
  return;
4291
6775
  let parsed;
4292
6776
  try {
4293
- parsed = readJson(mcpPath);
6777
+ parsed = readJson2(mcpPath);
4294
6778
  } catch (error) {
4295
6779
  errors2.push(`mcp.json: ${error.message}`);
4296
6780
  return;
4297
6781
  }
4298
- if (!isPlainObject2(parsed)) {
6782
+ if (!isPlainObject6(parsed)) {
4299
6783
  errors2.push("mcp.json: configuration must be a JSON object");
4300
6784
  return;
4301
6785
  }
@@ -4318,7 +6802,7 @@ function validateMcp(root, manifestSchema, errors2) {
4318
6802
  }
4319
6803
  }
4320
6804
  const servers = doc.mcpServers;
4321
- if (!isPlainObject2(servers)) {
6805
+ if (!isPlainObject6(servers)) {
4322
6806
  errors2.push('mcp.json: "mcpServers" is required and must be an object');
4323
6807
  return;
4324
6808
  }
@@ -4329,36 +6813,36 @@ function validateMcp(root, manifestSchema, errors2) {
4329
6813
  function validateSkills(root, errors2, warnings) {
4330
6814
  const skillsPath = path3.join(root, "skills");
4331
6815
  try {
4332
- if (!fs2.existsSync(skillsPath))
6816
+ if (!fs.existsSync(skillsPath))
4333
6817
  return;
4334
- if (!fs2.statSync(skillsPath).isDirectory()) {
6818
+ if (!fs.statSync(skillsPath).isDirectory()) {
4335
6819
  errors2.push("skills: skills/ is not a directory (component type invalid)");
4336
6820
  return;
4337
6821
  }
4338
- const entries = fs2.readdirSync(skillsPath, { withFileTypes: true });
4339
- const realRoot = fs2.realpathSync(root);
6822
+ const entries = fs.readdirSync(skillsPath, { withFileTypes: true });
6823
+ const realRoot = fs.realpathSync(root);
4340
6824
  for (const entry of entries) {
4341
6825
  if (!entry.isDirectory() && !entry.isSymbolicLink())
4342
6826
  continue;
4343
6827
  const skillDir = entry.name;
4344
6828
  let realSkillPath;
4345
6829
  try {
4346
- realSkillPath = fs2.realpathSync(path3.join(skillsPath, skillDir));
6830
+ realSkillPath = fs.realpathSync(path3.join(skillsPath, skillDir));
4347
6831
  } catch (error) {
4348
6832
  warnings.push(`skills: ${skillDir}/ cannot be resolved (${error.message}; skill skipped)`);
4349
6833
  continue;
4350
6834
  }
4351
- const relative = path3.relative(realRoot, realSkillPath);
4352
- if (relative === ".." || relative.startsWith(`..${path3.sep}`) || path3.isAbsolute(relative)) {
6835
+ const relative3 = path3.relative(realRoot, realSkillPath);
6836
+ if (relative3 === ".." || relative3.startsWith(`..${path3.sep}`) || path3.isAbsolute(relative3)) {
4353
6837
  warnings.push(`skills: ${skillDir}/ resolves outside the plugin root (${realSkillPath}; skill skipped)`);
4354
6838
  continue;
4355
6839
  }
4356
6840
  const skillMdPath = path3.join(skillsPath, skillDir, "SKILL.md");
4357
- if (!fs2.existsSync(skillMdPath) || !fs2.statSync(skillMdPath).isFile()) {
6841
+ if (!fs.existsSync(skillMdPath) || !fs.statSync(skillMdPath).isFile()) {
4358
6842
  warnings.push(`skills: ${skillDir}/ has no SKILL.md (directory is not a skill; ignored)`);
4359
6843
  continue;
4360
6844
  }
4361
- const frontmatter = parseFrontmatter(skillMdPath);
6845
+ const frontmatter = parseFrontmatter2(skillMdPath);
4362
6846
  if (!frontmatter) {
4363
6847
  warnings.push(`skills: ${skillDir}/SKILL.md is missing YAML frontmatter (name and description are required; skill skipped)`);
4364
6848
  continue;
@@ -4391,19 +6875,19 @@ function validateAgentPlugin(root) {
4391
6875
  const errors2 = [];
4392
6876
  const warnings = [];
4393
6877
  const manifestPath = path3.join(root, "plugin.json");
4394
- if (!fs2.existsSync(manifestPath)) {
6878
+ if (!fs.existsSync(manifestPath)) {
4395
6879
  errors2.push(`plugin.json: manifest not found at ${manifestPath} (plugin root must contain plugin.json)`);
4396
6880
  return { ok: false, errors: errors2, warnings };
4397
6881
  }
4398
6882
  let manifest;
4399
6883
  try {
4400
- manifest = readJson(manifestPath);
6884
+ manifest = readJson2(manifestPath);
4401
6885
  } catch (error) {
4402
6886
  errors2.push(`plugin.json: ${error.message}`);
4403
6887
  return { ok: false, errors: errors2, warnings };
4404
6888
  }
4405
6889
  validateManifest(manifest, errors2, warnings);
4406
- const manifestSchema = isPlainObject2(manifest) ? manifest["$schema"] : undefined;
6890
+ const manifestSchema = isPlainObject6(manifest) ? manifest["$schema"] : undefined;
4407
6891
  validateMcp(root, manifestSchema, errors2);
4408
6892
  validateSkills(root, errors2, warnings);
4409
6893
  return { ok: errors2.length === 0, errors: errors2, warnings };
@@ -4459,15 +6943,15 @@ function buildModelAssignments(selections) {
4459
6943
  }
4460
6944
 
4461
6945
  // src/adapters/codex.ts
4462
- import fs4 from "node:fs";
6946
+ import fs3 from "node:fs";
4463
6947
  import os2 from "node:os";
4464
6948
  import path5 from "node:path";
4465
6949
 
4466
6950
  // src/adapters/shared-install.ts
4467
- import fs3 from "node:fs";
6951
+ import fs2 from "node:fs";
4468
6952
  import os from "node:os";
4469
6953
  import path4 from "node:path";
4470
- import { execFileSync } from "node:child_process";
6954
+ import { execFileSync as execFileSync3 } from "node:child_process";
4471
6955
  var REPO_URL = "https://github.com/btspoony/mstar-harness.git";
4472
6956
  var PLUGIN_NAME = "morning-star-harness";
4473
6957
  var HARNESS_REPO_PATH = path4.join(os.homedir(), ".mstar", "harness");
@@ -4479,14 +6963,14 @@ var HARNESS_MARKERS = [
4479
6963
  function harnessMarkerPath() {
4480
6964
  for (const marker of HARNESS_MARKERS) {
4481
6965
  const candidate = path4.join(HARNESS_REPO_PATH, marker);
4482
- if (fs3.existsSync(candidate))
6966
+ if (fs2.existsSync(candidate))
4483
6967
  return candidate;
4484
6968
  }
4485
6969
  return path4.join(HARNESS_REPO_PATH, HARNESS_MARKERS[0]);
4486
6970
  }
4487
6971
  function pathOrSymlinkExists(filePath) {
4488
6972
  try {
4489
- fs3.lstatSync(filePath);
6973
+ fs2.lstatSync(filePath);
4490
6974
  return true;
4491
6975
  } catch {
4492
6976
  return false;
@@ -4495,17 +6979,17 @@ function pathOrSymlinkExists(filePath) {
4495
6979
  function ensureDir(dirPath, dryRun) {
4496
6980
  if (dryRun)
4497
6981
  return;
4498
- if (!fs3.existsSync(dirPath))
4499
- fs3.mkdirSync(dirPath, { recursive: true });
6982
+ if (!fs2.existsSync(dirPath))
6983
+ fs2.mkdirSync(dirPath, { recursive: true });
4500
6984
  }
4501
6985
  function runCommand(command, cwd, dryRun) {
4502
6986
  if (dryRun)
4503
6987
  return;
4504
- execFileSync(command[0], command.slice(1), { cwd, stdio: "pipe", encoding: "utf8" });
6988
+ execFileSync3(command[0], command.slice(1), { cwd, stdio: "pipe", encoding: "utf8" });
4505
6989
  }
4506
6990
  function ensureLocalHarnessRepo(dryRun) {
4507
6991
  const notes = [];
4508
- if (fs3.existsSync(HARNESS_REPO_PATH)) {
6992
+ if (fs2.existsSync(HARNESS_REPO_PATH)) {
4509
6993
  const errors2 = validateLocalHarnessRepo();
4510
6994
  if (errors2.length) {
4511
6995
  throw new Error(errors2.join(`
@@ -4521,12 +7005,12 @@ function ensureLocalHarnessRepo(dryRun) {
4521
7005
  }
4522
7006
  function validateLocalHarnessRepo() {
4523
7007
  const errors2 = [];
4524
- if (!fs3.existsSync(HARNESS_REPO_PATH)) {
7008
+ if (!fs2.existsSync(HARNESS_REPO_PATH)) {
4525
7009
  errors2.push(`Missing local harness repo: ${HARNESS_REPO_PATH}`);
4526
7010
  return errors2;
4527
7011
  }
4528
7012
  const marker = harnessMarkerPath();
4529
- if (!fs3.existsSync(marker)) {
7013
+ if (!fs2.existsSync(marker)) {
4530
7014
  errors2.push(`Local harness repo is missing a plugin marker (expected one of: ${HARNESS_MARKERS.join(", ")}).`);
4531
7015
  }
4532
7016
  return errors2;
@@ -4535,13 +7019,13 @@ function ensureGitCheckout(repoUrl, checkoutPath, dryRun) {
4535
7019
  const notes = [];
4536
7020
  const parentDir = path4.dirname(checkoutPath);
4537
7021
  if (pathOrSymlinkExists(checkoutPath)) {
4538
- const stat = fs3.lstatSync(checkoutPath);
7022
+ const stat = fs2.lstatSync(checkoutPath);
4539
7023
  if (stat.isSymbolicLink()) {
4540
7024
  notes.push(dryRun ? `Would remove symlink at ${checkoutPath} and clone ${repoUrl}` : `Removed symlink at ${checkoutPath} (Cursor requires a real directory)`);
4541
7025
  if (dryRun)
4542
7026
  return notes;
4543
- fs3.unlinkSync(checkoutPath);
4544
- } else if (fs3.existsSync(path4.join(checkoutPath, ".git"))) {
7027
+ fs2.unlinkSync(checkoutPath);
7028
+ } else if (fs2.existsSync(path4.join(checkoutPath, ".git"))) {
4545
7029
  if (!dryRun) {
4546
7030
  runCommand(["git", "-C", checkoutPath, "pull", "--ff-only"], checkoutPath, dryRun);
4547
7031
  }
@@ -4564,31 +7048,31 @@ function validateGitCheckout(checkoutPath, markerRelativePath) {
4564
7048
  errors2.push(`Missing checkout directory: ${checkoutPath}`);
4565
7049
  return errors2;
4566
7050
  }
4567
- const stat = fs3.lstatSync(checkoutPath);
7051
+ const stat = fs2.lstatSync(checkoutPath);
4568
7052
  if (stat.isSymbolicLink()) {
4569
7053
  errors2.push(`Path must be a real directory, not a symlink: ${checkoutPath}. Run: mstar-harness init --target cursor`);
4570
7054
  return errors2;
4571
7055
  }
4572
- if (!fs3.existsSync(path4.join(checkoutPath, ".git"))) {
7056
+ if (!fs2.existsSync(path4.join(checkoutPath, ".git"))) {
4573
7057
  errors2.push(`Path is not a git checkout: ${checkoutPath}`);
4574
7058
  }
4575
7059
  const marker = path4.join(checkoutPath, markerRelativePath);
4576
- if (!fs3.existsSync(marker)) {
7060
+ if (!fs2.existsSync(marker)) {
4577
7061
  errors2.push(`Missing marker file: ${marker}`);
4578
7062
  }
4579
7063
  return errors2;
4580
7064
  }
4581
7065
  function ensureSymlink(target, linkPath, dryRun) {
4582
7066
  if (pathOrSymlinkExists(linkPath)) {
4583
- const stat = fs3.lstatSync(linkPath);
7067
+ const stat = fs2.lstatSync(linkPath);
4584
7068
  if (!stat.isSymbolicLink()) {
4585
7069
  throw new Error(`Path exists and is not a symlink: ${linkPath}`);
4586
7070
  }
4587
- if (!fs3.existsSync(target)) {
7071
+ if (!fs2.existsSync(target)) {
4588
7072
  throw new Error(`Symlink target is missing: ${target}`);
4589
7073
  }
4590
- const actual = fs3.realpathSync(linkPath);
4591
- const expected = fs3.realpathSync(target);
7074
+ const actual = fs2.realpathSync(linkPath);
7075
+ const expected = fs2.realpathSync(target);
4592
7076
  if (actual !== expected) {
4593
7077
  throw new Error(`Symlink ${linkPath} points to ${actual}, expected ${expected}`);
4594
7078
  }
@@ -4596,7 +7080,7 @@ function ensureSymlink(target, linkPath, dryRun) {
4596
7080
  }
4597
7081
  ensureDir(path4.dirname(linkPath), dryRun);
4598
7082
  if (!dryRun)
4599
- fs3.symlinkSync(target, linkPath);
7083
+ fs2.symlinkSync(target, linkPath);
4600
7084
  return `Linked ${linkPath} -> ${target}`;
4601
7085
  }
4602
7086
  function validateSymlink(target, linkPath) {
@@ -4605,17 +7089,17 @@ function validateSymlink(target, linkPath) {
4605
7089
  errors2.push(`Missing symlink: ${linkPath}`);
4606
7090
  return errors2;
4607
7091
  }
4608
- const stat = fs3.lstatSync(linkPath);
7092
+ const stat = fs2.lstatSync(linkPath);
4609
7093
  if (!stat.isSymbolicLink()) {
4610
7094
  errors2.push(`Path exists but is not a symlink: ${linkPath}`);
4611
7095
  return errors2;
4612
7096
  }
4613
- if (!fs3.existsSync(target)) {
7097
+ if (!fs2.existsSync(target)) {
4614
7098
  errors2.push(`Symlink target is missing: ${target}`);
4615
7099
  return errors2;
4616
7100
  }
4617
- const actual = fs3.realpathSync(linkPath);
4618
- const expected = fs3.realpathSync(target);
7101
+ const actual = fs2.realpathSync(linkPath);
7102
+ const expected = fs2.realpathSync(target);
4619
7103
  if (actual !== expected) {
4620
7104
  errors2.push(`Symlink ${linkPath} points to ${actual}, expected ${expected}`);
4621
7105
  }
@@ -4641,7 +7125,7 @@ function missingHarnessProcessGitignoreEntries(gitignoreContent) {
4641
7125
  }
4642
7126
  function appendGitignore(projectRoot, entries, dryRun) {
4643
7127
  const gitignorePath = path4.join(projectRoot, ".gitignore");
4644
- const current = fs3.existsSync(gitignorePath) ? fs3.readFileSync(gitignorePath, "utf8") : "";
7128
+ const current = fs2.existsSync(gitignorePath) ? fs2.readFileSync(gitignorePath, "utf8") : "";
4645
7129
  const lines = new Set(current.split(/\r?\n/).map((line) => line.trim()));
4646
7130
  const missing = entries.filter((entry) => !lines.has(entry));
4647
7131
  if (!missing.length)
@@ -4650,7 +7134,7 @@ function appendGitignore(projectRoot, entries, dryRun) {
4650
7134
  const prefix = current && !current.endsWith(`
4651
7135
  `) ? `
4652
7136
  ` : "";
4653
- fs3.appendFileSync(gitignorePath, `${prefix}${missing.join(`
7137
+ fs2.appendFileSync(gitignorePath, `${prefix}${missing.join(`
4654
7138
  `)}
4655
7139
  `, "utf8");
4656
7140
  }
@@ -4695,7 +7179,7 @@ function globalMarketplacePath() {
4695
7179
  return GLOBAL_MARKETPLACE_PATH;
4696
7180
  }
4697
7181
  function projectMarketplacePath() {
4698
- return path5.join(resolveProjectRoot(), ".agents", "plugins", "marketplace.json");
7182
+ return path5.join(resolveProjectRoot2(), ".agents", "plugins", "marketplace.json");
4699
7183
  }
4700
7184
  function agentSourcePath(agentName) {
4701
7185
  return path5.join(HARNESS_REPO_PATH, "codex", "agents", `${agentName}.toml`);
@@ -4704,7 +7188,7 @@ function globalAgentLinkPath(agentName) {
4704
7188
  return path5.join(os2.homedir(), ".codex", "agents", `${agentName}.toml`);
4705
7189
  }
4706
7190
  function projectAgentLinkPath(agentName) {
4707
- return path5.join(resolveProjectRoot(), ".codex", "agents", `${agentName}.toml`);
7191
+ return path5.join(resolveProjectRoot2(), ".codex", "agents", `${agentName}.toml`);
4708
7192
  }
4709
7193
  function mstarEntry(scope) {
4710
7194
  const sourcePath = scope === "global" ? homeRelativeSourcePath(HARNESS_REPO_PATH) : `./${CODEX_PLUGIN_LINK}`;
@@ -4798,14 +7282,14 @@ function iterationCommandSourcePath(skillName) {
4798
7282
  return path5.join(HARNESS_REPO_PATH, "commands", `${skillName}.md`);
4799
7283
  }
4800
7284
  function projectIterationSkillLinkPath(skillName) {
4801
- return path5.join(resolveProjectRoot(), ".agents", "skills", skillName, "SKILL.md");
7285
+ return path5.join(resolveProjectRoot2(), ".agents", "skills", skillName, "SKILL.md");
4802
7286
  }
4803
7287
  function iterationSkillGitignoreEntry(skillName) {
4804
7288
  return `.agents/skills/${skillName}`;
4805
7289
  }
4806
7290
  function ensureIterationSkillLinks(dryRun) {
4807
7291
  const notes = [];
4808
- const projectRoot = resolveProjectRoot();
7292
+ const projectRoot = resolveProjectRoot2();
4809
7293
  for (const skillName of CODEX_PROJECT_COMMAND_NAMES) {
4810
7294
  const source = iterationCommandSourcePath(skillName);
4811
7295
  const linkPath = projectIterationSkillLinkPath(skillName);
@@ -4818,9 +7302,9 @@ function ensureIterationSkillLinks(dryRun) {
4818
7302
  }
4819
7303
  function validateIterationSkillLinks() {
4820
7304
  const errors2 = [];
4821
- const projectRoot = resolveProjectRoot();
7305
+ const projectRoot = resolveProjectRoot2();
4822
7306
  const gitignorePath = path5.join(projectRoot, ".gitignore");
4823
- const gitignore = fs4.existsSync(gitignorePath) ? fs4.readFileSync(gitignorePath, "utf8") : "";
7307
+ const gitignore = fs3.existsSync(gitignorePath) ? fs3.readFileSync(gitignorePath, "utf8") : "";
4824
7308
  const lines = gitignore.split(/\r?\n/);
4825
7309
  for (const skillName of CODEX_PROJECT_COMMAND_NAMES) {
4826
7310
  const source = iterationCommandSourcePath(skillName);
@@ -4834,12 +7318,12 @@ function validateIterationSkillLinks() {
4834
7318
  }
4835
7319
  function runInit(scope, dryRun) {
4836
7320
  const pathToMarketplace = marketplacePath(scope);
4837
- const current = readJson(pathToMarketplace);
7321
+ const current = readJson2(pathToMarketplace);
4838
7322
  const next = upsertEntry(current, scope);
4839
7323
  const existingEntry = findEntry(current);
4840
7324
  const notes = ensureLocalHarnessRepo(dryRun);
4841
7325
  if (scope === "project") {
4842
- const projectRoot = resolveProjectRoot();
7326
+ const projectRoot = resolveProjectRoot2();
4843
7327
  notes.push(ensureSymlink(HARNESS_REPO_PATH, path5.join(projectRoot, CODEX_PLUGIN_LINK), dryRun));
4844
7328
  notes.push(...appendGitignore(projectRoot, [CODEX_PLUGIN_LINK, ".codex/agents/*.toml"], dryRun));
4845
7329
  notes.push(...appendHarnessProjectGitignore(projectRoot, dryRun));
@@ -4849,7 +7333,7 @@ function runInit(scope, dryRun) {
4849
7333
  }
4850
7334
  notes.push(...ensureAgentLinks(scope, dryRun));
4851
7335
  if (!dryRun)
4852
- writeJson(pathToMarketplace, next);
7336
+ writeJson2(pathToMarketplace, next);
4853
7337
  notes.push(existingEntry ? `Updated ${PLUGIN_NAME} local marketplace entry.` : `Added ${PLUGIN_NAME} local marketplace entry.`);
4854
7338
  notes.push(`Source path: ${mstarEntry(scope).source.path}`);
4855
7339
  notes.push(`Install after init: codex plugin add ${PLUGIN_NAME} --marketplace ${MARKETPLACE_NAME}`);
@@ -4861,19 +7345,19 @@ function runInit(scope, dryRun) {
4861
7345
  function runDoctor(scope) {
4862
7346
  const pathToMarketplace = marketplacePath(scope);
4863
7347
  const errors2 = validateLocalHarnessRepo();
4864
- if (!fs4.existsSync(pathToMarketplace)) {
7348
+ if (!fs3.existsSync(pathToMarketplace)) {
4865
7349
  return { location: pathToMarketplace, errors: [...errors2, `Missing Codex marketplace: ${pathToMarketplace}`] };
4866
7350
  }
4867
- const marketplace = readJson(pathToMarketplace);
7351
+ const marketplace = readJson2(pathToMarketplace);
4868
7352
  if (marketplace.name !== MARKETPLACE_NAME) {
4869
7353
  errors2.push(`Codex personal marketplace name must be ${MARKETPLACE_NAME}.`);
4870
7354
  }
4871
7355
  errors2.push(...validateEntryShape(findEntry(marketplace), scope, pathToMarketplace));
4872
7356
  if (scope === "project") {
4873
- const projectRoot = resolveProjectRoot();
7357
+ const projectRoot = resolveProjectRoot2();
4874
7358
  errors2.push(...validateSymlink(HARNESS_REPO_PATH, path5.join(projectRoot, CODEX_PLUGIN_LINK)));
4875
7359
  const gitignorePath = path5.join(projectRoot, ".gitignore");
4876
- const gitignore = fs4.existsSync(gitignorePath) ? fs4.readFileSync(gitignorePath, "utf8") : "";
7360
+ const gitignore = fs3.existsSync(gitignorePath) ? fs3.readFileSync(gitignorePath, "utf8") : "";
4877
7361
  const lines = gitignore.split(/\r?\n/);
4878
7362
  if (!lines.includes(CODEX_PLUGIN_LINK))
4879
7363
  errors2.push(`Missing .gitignore entry: ${CODEX_PLUGIN_LINK}`);
@@ -4895,7 +7379,7 @@ var codexAdapter = {
4895
7379
  };
4896
7380
 
4897
7381
  // src/adapters/cursor.ts
4898
- import fs5 from "node:fs";
7382
+ import fs4 from "node:fs";
4899
7383
  import os3 from "node:os";
4900
7384
  import path6 from "node:path";
4901
7385
  var CURSOR_PLUGIN_NAME = "morning-star-harness";
@@ -4906,22 +7390,22 @@ function globalInstallPath() {
4906
7390
  return path6.join(os3.homedir(), ".cursor", "plugins", "local", CURSOR_PLUGIN_NAME);
4907
7391
  }
4908
7392
  function projectInstallPath() {
4909
- return path6.join(resolveProjectRoot(), CURSOR_PLUGIN_LINK);
7393
+ return path6.join(resolveProjectRoot2(), CURSOR_PLUGIN_LINK);
4910
7394
  }
4911
7395
  function validatePluginAgents(pluginRoot) {
4912
7396
  const errors2 = [];
4913
7397
  const agentsDir = path6.join(pluginRoot, "agents");
4914
- if (!fs5.existsSync(agentsDir)) {
7398
+ if (!fs4.existsSync(agentsDir)) {
4915
7399
  errors2.push(`Missing plugin agents directory: ${agentsDir}`);
4916
7400
  return errors2;
4917
7401
  }
4918
7402
  for (const agentName of CURSOR_AGENT_SMOKE_NAMES) {
4919
7403
  const agentPath = path6.join(agentsDir, `${agentName}.md`);
4920
- if (!fs5.existsSync(agentPath)) {
7404
+ if (!fs4.existsSync(agentPath)) {
4921
7405
  errors2.push(`Missing plugin agent file: ${agentPath}`);
4922
7406
  continue;
4923
7407
  }
4924
- const content = fs5.readFileSync(agentPath, "utf8");
7408
+ const content = fs4.readFileSync(agentPath, "utf8");
4925
7409
  if (!/^---\nname:\s/m.test(content)) {
4926
7410
  errors2.push(`Plugin agent ${agentName}.md must use Cursor-first frontmatter (name, description, model before OpenCode fields).`);
4927
7411
  }
@@ -4938,7 +7422,7 @@ function globalInit(dryRun) {
4938
7422
  return { location, notes };
4939
7423
  }
4940
7424
  function projectInit(dryRun) {
4941
- const projectRoot = resolveProjectRoot();
7425
+ const projectRoot = resolveProjectRoot2();
4942
7426
  const location = projectInstallPath();
4943
7427
  const notes = ensureLocalHarnessRepo(dryRun);
4944
7428
  notes.push(...ensureCursorPluginCheckout(location, dryRun));
@@ -4954,12 +7438,12 @@ function globalDoctor() {
4954
7438
  return { location, errors: errors2 };
4955
7439
  }
4956
7440
  function projectDoctor() {
4957
- const projectRoot = resolveProjectRoot();
7441
+ const projectRoot = resolveProjectRoot2();
4958
7442
  const location = projectInstallPath();
4959
7443
  const errors2 = validateLocalHarnessRepo();
4960
7444
  errors2.push(...validateGitCheckout(location, CURSOR_PLUGIN_MARKER));
4961
7445
  const gitignorePath = path6.join(projectRoot, ".gitignore");
4962
- const gitignore = fs5.existsSync(gitignorePath) ? fs5.readFileSync(gitignorePath, "utf8") : "";
7446
+ const gitignore = fs4.existsSync(gitignorePath) ? fs4.readFileSync(gitignorePath, "utf8") : "";
4963
7447
  if (!gitignore.split(/\r?\n/).includes(CURSOR_PLUGIN_LINK)) {
4964
7448
  errors2.push(`Missing .gitignore entry: ${CURSOR_PLUGIN_LINK}`);
4965
7449
  }
@@ -4985,9 +7469,9 @@ var cursorAdapter = {
4985
7469
  };
4986
7470
 
4987
7471
  // src/adapters/omp.ts
4988
- import fs6 from "node:fs";
7472
+ import fs5 from "node:fs";
4989
7473
  import path7 from "node:path";
4990
- import { execFileSync as execFileSync2 } from "node:child_process";
7474
+ import { execFileSync as execFileSync5 } from "node:child_process";
4991
7475
  var OMP_PLUGIN_MARKER = ".omp-plugin/plugin.json";
4992
7476
  var CLAUDE_PLUGIN_MARKER = ".claude-plugin/plugin.json";
4993
7477
  var PACKAGE_NAMES = new Set(["morning-star", PLUGIN_NAME, "github:btspoony/mstar-harness"]);
@@ -4995,7 +7479,7 @@ var SKILL_SMOKE = ["mstar-host", "mstar-harness-core", "pm"];
4995
7479
  var COMMAND_SMOKE = ["iteration-start", "iteration-drive", "iteration-loop", "codebase-audit"];
4996
7480
  function ompAvailable() {
4997
7481
  try {
4998
- execFileSync2("omp", ["--version"], { stdio: "pipe", encoding: "utf8" });
7482
+ execFileSync5("omp", ["--version"], { stdio: "pipe", encoding: "utf8" });
4999
7483
  return true;
5000
7484
  } catch {
5001
7485
  return false;
@@ -5004,11 +7488,11 @@ function ompAvailable() {
5004
7488
  function runOmp(args, dryRun) {
5005
7489
  if (dryRun)
5006
7490
  return;
5007
- execFileSync2("omp", args, { stdio: "pipe", encoding: "utf8" });
7491
+ execFileSync5("omp", args, { stdio: "pipe", encoding: "utf8" });
5008
7492
  }
5009
7493
  function listInstalledPlugins() {
5010
7494
  try {
5011
- const raw = execFileSync2("omp", ["plugin", "list", "--json"], {
7495
+ const raw = execFileSync5("omp", ["plugin", "list", "--json"], {
5012
7496
  stdio: "pipe",
5013
7497
  encoding: "utf8"
5014
7498
  });
@@ -5057,34 +7541,34 @@ function validatePluginTree(pluginRoot) {
5057
7541
  const errors2 = [];
5058
7542
  for (const marker of [OMP_PLUGIN_MARKER, CLAUDE_PLUGIN_MARKER]) {
5059
7543
  const markerPath = path7.join(pluginRoot, marker);
5060
- if (!fs6.existsSync(markerPath)) {
7544
+ if (!fs5.existsSync(markerPath)) {
5061
7545
  errors2.push(`Missing omp plugin marker: ${markerPath}`);
5062
7546
  }
5063
7547
  }
5064
7548
  for (const skill of SKILL_SMOKE) {
5065
7549
  const skillPath = path7.join(pluginRoot, "skills", skill, "SKILL.md");
5066
- if (!fs6.existsSync(skillPath))
7550
+ if (!fs5.existsSync(skillPath))
5067
7551
  errors2.push(`Missing skill: ${skillPath}`);
5068
7552
  }
5069
7553
  for (const command of COMMAND_SMOKE) {
5070
7554
  const commandPath = path7.join(pluginRoot, "commands", `${command}.md`);
5071
- if (!fs6.existsSync(commandPath))
7555
+ if (!fs5.existsSync(commandPath))
5072
7556
  errors2.push(`Missing command: ${commandPath}`);
5073
7557
  }
5074
7558
  const hostRef = path7.join(pluginRoot, "skills", "mstar-host", "references", "omp.md");
5075
- if (!fs6.existsSync(hostRef))
7559
+ if (!fs5.existsSync(hostRef))
5076
7560
  errors2.push(`Missing omp host reference: ${hostRef}`);
5077
7561
  return errors2;
5078
7562
  }
5079
7563
  function runInit2(scope, dryRun) {
5080
7564
  const notes = ensureLocalHarnessRepo(dryRun);
5081
- const projectRoot = resolveProjectRoot();
5082
- if (fs6.existsSync(path7.join(HARNESS_REPO_PATH, ".git"))) {
7565
+ const projectRoot = resolveProjectRoot2();
7566
+ if (fs5.existsSync(path7.join(HARNESS_REPO_PATH, ".git"))) {
5083
7567
  if (dryRun) {
5084
7568
  notes.push(`Would update local harness repo: git -C ${HARNESS_REPO_PATH} pull --ff-only`);
5085
7569
  } else {
5086
7570
  try {
5087
- execFileSync2("git", ["-C", HARNESS_REPO_PATH, "pull", "--ff-only"], {
7571
+ execFileSync5("git", ["-C", HARNESS_REPO_PATH, "pull", "--ff-only"], {
5088
7572
  stdio: "pipe",
5089
7573
  encoding: "utf8"
5090
7574
  });
@@ -5152,9 +7636,9 @@ function runDoctor2(scope) {
5152
7636
  }
5153
7637
  }
5154
7638
  if (scope === "project") {
5155
- const projectRoot = resolveProjectRoot();
7639
+ const projectRoot = resolveProjectRoot2();
5156
7640
  const gitignorePath = path7.join(projectRoot, ".gitignore");
5157
- const gitignore = fs6.existsSync(gitignorePath) ? fs6.readFileSync(gitignorePath, "utf8") : "";
7641
+ const gitignore = fs5.existsSync(gitignorePath) ? fs5.readFileSync(gitignorePath, "utf8") : "";
5158
7642
  for (const entry of missingHarnessProcessGitignoreEntries(gitignore)) {
5159
7643
  errors2.push(`Missing .gitignore entry: ${entry}`);
5160
7644
  }
@@ -5191,11 +7675,11 @@ function isAnyMstarHarnessOpencodeSlot(plugin) {
5191
7675
  function resolveOpencodeConfigPath(scope, outputPath) {
5192
7676
  if (outputPath && outputPath.trim()) {
5193
7677
  const raw = outputPath.trim();
5194
- return path8.isAbsolute(raw) ? raw : path8.join(resolveProjectRoot(), raw);
7678
+ return path8.isAbsolute(raw) ? raw : path8.join(resolveProjectRoot2(), raw);
5195
7679
  }
5196
7680
  if (scope === "global")
5197
7681
  return path8.join(os4.homedir(), ".config", "opencode", "opencode.json");
5198
- return path8.join(resolveProjectRoot(), "opencode.json");
7682
+ return path8.join(resolveProjectRoot2(), "opencode.json");
5199
7683
  }
5200
7684
  function ensureConfigSchema(config) {
5201
7685
  const next = ensureObject(config);
@@ -5288,7 +7772,7 @@ var opencodeAdapter = {
5288
7772
  };
5289
7773
 
5290
7774
  // src/adapters/zcode.ts
5291
- import fs7 from "node:fs";
7775
+ import fs6 from "node:fs";
5292
7776
  import os5 from "node:os";
5293
7777
  import path9 from "node:path";
5294
7778
  var MARKETPLACE_ID = "mstar-local";
@@ -5360,11 +7844,11 @@ function findMarketplacePlugin(raw) {
5360
7844
  }
5361
7845
  function validateMarketplaceJson() {
5362
7846
  const errors2 = [];
5363
- if (!fs7.existsSync(MARKETPLACE_JSON_PATH)) {
7847
+ if (!fs6.existsSync(MARKETPLACE_JSON_PATH)) {
5364
7848
  errors2.push(`Missing ZCode marketplace: ${MARKETPLACE_JSON_PATH}`);
5365
7849
  return errors2;
5366
7850
  }
5367
- const raw = readJson(MARKETPLACE_JSON_PATH);
7851
+ const raw = readJson2(MARKETPLACE_JSON_PATH);
5368
7852
  if (raw.name !== MARKETPLACE_NAME2) {
5369
7853
  errors2.push(`ZCode marketplace name must be ${MARKETPLACE_NAME2} (in ${MARKETPLACE_JSON_PATH}).`);
5370
7854
  }
@@ -5389,11 +7873,11 @@ function validateMarketplaceJson() {
5389
7873
  }
5390
7874
  function validateKnownMarketplaces() {
5391
7875
  const errors2 = [];
5392
- if (!fs7.existsSync(KNOWN_MARKETPLACES_PATH)) {
7876
+ if (!fs6.existsSync(KNOWN_MARKETPLACES_PATH)) {
5393
7877
  errors2.push(`Missing ZCode known_marketplaces.json: ${KNOWN_MARKETPLACES_PATH}`);
5394
7878
  return errors2;
5395
7879
  }
5396
- const raw = readJson(KNOWN_MARKETPLACES_PATH);
7880
+ const raw = readJson2(KNOWN_MARKETPLACES_PATH);
5397
7881
  const entry = findKnownMarketplace(raw);
5398
7882
  if (!entry) {
5399
7883
  errors2.push(`Missing ${MARKETPLACE_ID} entry in ${KNOWN_MARKETPLACES_PATH}.`);
@@ -5413,13 +7897,13 @@ function validateKnownMarketplaces() {
5413
7897
  function validatePluginAgents2(pluginRoot) {
5414
7898
  const errors2 = [];
5415
7899
  const agentsDir = path9.join(pluginRoot, "agents");
5416
- if (!fs7.existsSync(agentsDir)) {
7900
+ if (!fs6.existsSync(agentsDir)) {
5417
7901
  errors2.push(`Missing plugin agents directory: ${agentsDir}`);
5418
7902
  return errors2;
5419
7903
  }
5420
7904
  for (const agentName of ZCODE_AGENT_SMOKE_NAMES) {
5421
7905
  const agentPath = path9.join(agentsDir, `${agentName}.md`);
5422
- if (!fs7.existsSync(agentPath)) {
7906
+ if (!fs6.existsSync(agentPath)) {
5423
7907
  errors2.push(`Missing plugin agent file: ${agentPath}`);
5424
7908
  }
5425
7909
  }
@@ -5434,7 +7918,7 @@ function buildMarketplaceJson() {
5434
7918
  }
5435
7919
  function runInit3(scope, dryRun) {
5436
7920
  const notes = ensureLocalHarnessRepo(dryRun);
5437
- const projectRoot = resolveProjectRoot();
7921
+ const projectRoot = resolveProjectRoot2();
5438
7922
  if (scope === "project") {
5439
7923
  const checkoutPath = path9.join(projectRoot, ZCODE_PLUGIN_CHECKOUT_PROJECT);
5440
7924
  notes.push(...ensureGitCheckout(REPO_URL, checkoutPath, dryRun));
@@ -5443,15 +7927,15 @@ function runInit3(scope, dryRun) {
5443
7927
  notes.push(`Materialized local ZCode plugin checkout at ${ZCODE_PLUGIN_CHECKOUT_PROJECT} for smoke checks (the registered marketplace still points at the github repo).`);
5444
7928
  }
5445
7929
  if (!dryRun) {
5446
- if (!fs7.existsSync(MARKETPLACE_DIR))
5447
- fs7.mkdirSync(MARKETPLACE_DIR, { recursive: true });
5448
- writeJson(MARKETPLACE_JSON_PATH, buildMarketplaceJson());
7930
+ if (!fs6.existsSync(MARKETPLACE_DIR))
7931
+ fs6.mkdirSync(MARKETPLACE_DIR, { recursive: true });
7932
+ writeJson2(MARKETPLACE_JSON_PATH, buildMarketplaceJson());
5449
7933
  }
5450
7934
  notes.push(`Wrote ZCode marketplace: ${MARKETPLACE_JSON_PATH}`);
5451
- const knownRaw = readJson(KNOWN_MARKETPLACES_PATH);
7935
+ const knownRaw = readJson2(KNOWN_MARKETPLACES_PATH);
5452
7936
  const knownNext = upsertKnownMarketplace(knownRaw);
5453
7937
  if (!dryRun)
5454
- writeJson(KNOWN_MARKETPLACES_PATH, knownNext);
7938
+ writeJson2(KNOWN_MARKETPLACES_PATH, knownNext);
5455
7939
  notes.push(`Registered ${MARKETPLACE_ID} marketplace in ${KNOWN_MARKETPLACES_PATH}`);
5456
7940
  notes.push(`Then in ZCode: Settings → Plugin Management → Discover → install ${PLUGIN_NAME} from the ${MARKETPLACE_ID} marketplace.`);
5457
7941
  return {
@@ -5463,11 +7947,11 @@ function runDoctor3(scope) {
5463
7947
  const errors2 = [];
5464
7948
  errors2.push(...validateLocalHarnessRepo());
5465
7949
  if (scope === "project") {
5466
- const projectRoot = resolveProjectRoot();
7950
+ const projectRoot = resolveProjectRoot2();
5467
7951
  const checkoutPath = path9.join(projectRoot, ZCODE_PLUGIN_CHECKOUT_PROJECT);
5468
7952
  errors2.push(...validateGitCheckout(checkoutPath, ZCODE_PLUGIN_MARKER));
5469
7953
  const gitignorePath = path9.join(projectRoot, ".gitignore");
5470
- const gitignore = fs7.existsSync(gitignorePath) ? fs7.readFileSync(gitignorePath, "utf8") : "";
7954
+ const gitignore = fs6.existsSync(gitignorePath) ? fs6.readFileSync(gitignorePath, "utf8") : "";
5471
7955
  if (!gitignore.split(/\r?\n/).includes(ZCODE_PLUGIN_CHECKOUT_PROJECT)) {
5472
7956
  errors2.push(`Missing .gitignore entry: ${ZCODE_PLUGIN_CHECKOUT_PROJECT}`);
5473
7957
  }
@@ -5508,51 +7992,21 @@ function getAdapter(target) {
5508
7992
  var SUPPORTED_TARGETS = ["opencode", "cursor", "codex", "zcode", "omp"];
5509
7993
 
5510
7994
  // src/utils.ts
5511
- import fs8 from "node:fs";
5512
7995
  import path10 from "node:path";
5513
- import { fileURLToPath as fileURLToPath2 } from "node:url";
5514
7996
  function parseCsv(raw) {
5515
7997
  if (!raw)
5516
7998
  return;
5517
7999
  return raw.split(",").map((item) => item.trim()).filter(Boolean);
5518
8000
  }
5519
- function readJson2(filePath) {
5520
- if (!fs8.existsSync(filePath))
5521
- return {};
5522
- const content = fs8.readFileSync(filePath, "utf8").trim();
5523
- if (!content)
5524
- return {};
5525
- try {
5526
- return JSON.parse(content);
5527
- } catch (error) {
5528
- throw new Error(`Invalid JSON in ${filePath}: ${error.message}`);
5529
- }
5530
- }
5531
- function writeJson2(filePath, value) {
5532
- const parent = path10.dirname(filePath);
5533
- if (!fs8.existsSync(parent))
5534
- fs8.mkdirSync(parent, { recursive: true });
5535
- fs8.writeFileSync(filePath, `${JSON.stringify(value, null, 2)}
5536
- `, "utf8");
5537
- }
5538
- function resolveProjectRoot2() {
8001
+ function resolveProjectRoot3() {
5539
8002
  const candidate = process.env.MSTAR_CLI_PROJECT_ROOT || process.env.INIT_CWD || process.env.PWD;
5540
8003
  if (candidate && candidate.trim())
5541
8004
  return path10.resolve(candidate);
5542
- return process.cwd();
5543
- }
5544
- function readHarnessVersion2() {
5545
- const packageJsonPath = path10.resolve(path10.dirname(fileURLToPath2(import.meta.url)), "../package.json");
5546
- try {
5547
- const parsed = JSON.parse(fs8.readFileSync(packageJsonPath, "utf8"));
5548
- return parsed.version || "0.0.0";
5549
- } catch {
5550
- return "0.0.0";
5551
- }
8005
+ return resolveProjectRoot();
5552
8006
  }
5553
8007
 
5554
8008
  // src/index.ts
5555
- var packageVersion = readHarnessVersion2();
8009
+ var packageVersion = readHarnessVersion();
5556
8010
  var program2 = new Command;
5557
8011
  function logStep(message) {
5558
8012
  console.log(import_picocolors.default.cyan(message));
@@ -5694,8 +8148,8 @@ function runDoctor4(options) {
5694
8148
  function resolvePluginRoot(options) {
5695
8149
  if (options.root)
5696
8150
  return path11.resolve(options.root);
5697
- let candidate = resolveProjectRoot2();
5698
- while (!fs9.existsSync(path11.join(candidate, "plugin.json"))) {
8151
+ let candidate = resolveProjectRoot3();
8152
+ while (!fs7.existsSync(path11.join(candidate, "plugin.json"))) {
5699
8153
  const parent = path11.dirname(candidate);
5700
8154
  if (parent === candidate)
5701
8155
  break;
@@ -5729,7 +8183,751 @@ var pluginCommand = program2.command("plugin").description("Agent Plugins v1.0.0
5729
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) => {
5730
8184
  runPluginValidate(options);
5731
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
+ });
5732
8930
  program2.parseAsync(process.argv).catch((error) => {
5733
8931
  console.error(import_picocolors.default.red(`Setup failed: ${error.message}`));
5734
- process.exit(1);
8932
+ process.exitCode = 1;
5735
8933
  });