@mstar-harness/cli 1.8.9 → 2.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/mstar-harness.js +3376 -178
- package/package.json +6 -2
package/dist/mstar-harness.js
CHANGED
|
@@ -2344,7 +2344,8 @@ var require_commander = __commonJS((exports) => {
|
|
|
2344
2344
|
});
|
|
2345
2345
|
|
|
2346
2346
|
// src/index.ts
|
|
2347
|
-
import
|
|
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
|
|
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
|
|
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
|
|
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
|
|
6556
|
+
function isPlainObject6(value) {
|
|
4073
6557
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
4074
6558
|
}
|
|
4075
|
-
function
|
|
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
|
|
4083
|
-
const content =
|
|
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]] =
|
|
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 (!
|
|
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 (!
|
|
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 (!
|
|
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 (!
|
|
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 (!
|
|
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 (!
|
|
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 (!
|
|
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 (!
|
|
6773
|
+
if (!fs.existsSync(mcpPath))
|
|
4290
6774
|
return;
|
|
4291
6775
|
let parsed;
|
|
4292
6776
|
try {
|
|
4293
|
-
parsed =
|
|
6777
|
+
parsed = readJson2(mcpPath);
|
|
4294
6778
|
} catch (error) {
|
|
4295
6779
|
errors2.push(`mcp.json: ${error.message}`);
|
|
4296
6780
|
return;
|
|
4297
6781
|
}
|
|
4298
|
-
if (!
|
|
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 (!
|
|
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 (!
|
|
6816
|
+
if (!fs.existsSync(skillsPath))
|
|
4333
6817
|
return;
|
|
4334
|
-
if (!
|
|
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 =
|
|
4339
|
-
const realRoot =
|
|
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 =
|
|
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
|
|
4352
|
-
if (
|
|
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 (!
|
|
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 =
|
|
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 (!
|
|
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 =
|
|
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 =
|
|
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
|
|
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
|
|
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 (
|
|
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
|
-
|
|
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 (!
|
|
4499
|
-
|
|
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
|
-
|
|
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 (
|
|
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 (!
|
|
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 (!
|
|
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 =
|
|
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
|
-
|
|
4544
|
-
} else if (
|
|
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 =
|
|
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 (!
|
|
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 (!
|
|
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 =
|
|
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 (!
|
|
7071
|
+
if (!fs2.existsSync(target)) {
|
|
4588
7072
|
throw new Error(`Symlink target is missing: ${target}`);
|
|
4589
7073
|
}
|
|
4590
|
-
const actual =
|
|
4591
|
-
const expected =
|
|
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
|
-
|
|
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 =
|
|
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 (!
|
|
7097
|
+
if (!fs2.existsSync(target)) {
|
|
4614
7098
|
errors2.push(`Symlink target is missing: ${target}`);
|
|
4615
7099
|
return errors2;
|
|
4616
7100
|
}
|
|
4617
|
-
const actual =
|
|
4618
|
-
const expected =
|
|
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 =
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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 =
|
|
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 =
|
|
7305
|
+
const projectRoot = resolveProjectRoot2();
|
|
4822
7306
|
const gitignorePath = path5.join(projectRoot, ".gitignore");
|
|
4823
|
-
const gitignore =
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
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 (!
|
|
7348
|
+
if (!fs3.existsSync(pathToMarketplace)) {
|
|
4865
7349
|
return { location: pathToMarketplace, errors: [...errors2, `Missing Codex marketplace: ${pathToMarketplace}`] };
|
|
4866
7350
|
}
|
|
4867
|
-
const marketplace =
|
|
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 =
|
|
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 =
|
|
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
|
|
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(
|
|
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 (!
|
|
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 (!
|
|
7404
|
+
if (!fs4.existsSync(agentPath)) {
|
|
4921
7405
|
errors2.push(`Missing plugin agent file: ${agentPath}`);
|
|
4922
7406
|
continue;
|
|
4923
7407
|
}
|
|
4924
|
-
const content =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
|
7472
|
+
import fs5 from "node:fs";
|
|
4989
7473
|
import path7 from "node:path";
|
|
4990
|
-
import { execFileSync as
|
|
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
|
-
|
|
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
|
-
|
|
7491
|
+
execFileSync5("omp", args, { stdio: "pipe", encoding: "utf8" });
|
|
5008
7492
|
}
|
|
5009
7493
|
function listInstalledPlugins() {
|
|
5010
7494
|
try {
|
|
5011
|
-
const raw =
|
|
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 (!
|
|
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 (!
|
|
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 (!
|
|
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 (!
|
|
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 =
|
|
5082
|
-
if (
|
|
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
|
-
|
|
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 =
|
|
7639
|
+
const projectRoot = resolveProjectRoot2();
|
|
5156
7640
|
const gitignorePath = path7.join(projectRoot, ".gitignore");
|
|
5157
|
-
const gitignore =
|
|
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(
|
|
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(
|
|
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
|
|
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 (!
|
|
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 =
|
|
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 (!
|
|
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 =
|
|
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 (!
|
|
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 (!
|
|
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 =
|
|
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 (!
|
|
5447
|
-
|
|
5448
|
-
|
|
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 =
|
|
7935
|
+
const knownRaw = readJson2(KNOWN_MARKETPLACES_PATH);
|
|
5452
7936
|
const knownNext = upsertKnownMarketplace(knownRaw);
|
|
5453
7937
|
if (!dryRun)
|
|
5454
|
-
|
|
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 =
|
|
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 =
|
|
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
|
|
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
|
|
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 =
|
|
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 =
|
|
5698
|
-
while (!
|
|
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.
|
|
8932
|
+
process.exitCode = 1;
|
|
5735
8933
|
});
|