@mstar-harness/cli 3.2.5 → 3.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/mstar-harness.js +1615 -1190
- package/package.json +1 -1
package/dist/mstar-harness.js
CHANGED
|
@@ -4073,25 +4073,25 @@ import { randomUUID } from "node:crypto";
|
|
|
4073
4073
|
import { basename, dirname, join, resolve } from "node:path";
|
|
4074
4074
|
import { readFileSync as readFileSync2, statSync } from "node:fs";
|
|
4075
4075
|
import { dirname as dirname2, isAbsolute, join as join2, relative, resolve as resolve2 } from "node:path";
|
|
4076
|
-
import { existsSync as
|
|
4076
|
+
import { existsSync as existsSync5, mkdirSync as mkdirSync4, readdirSync as readdirSync4, readFileSync as readFileSync6, realpathSync as realpathSync2, statSync as statSync3, writeFileSync as writeFileSync3 } from "node:fs";
|
|
4077
4077
|
import { execFileSync } from "node:child_process";
|
|
4078
|
-
import { basename as basename2, dirname as
|
|
4079
|
-
import { existsSync as
|
|
4080
|
-
import {
|
|
4081
|
-
import {
|
|
4082
|
-
import {
|
|
4078
|
+
import { basename as basename2, dirname as dirname5, isAbsolute as isAbsolute3, join as join8, relative as relative2, resolve as resolve5 } from "node:path";
|
|
4079
|
+
import { existsSync as existsSync4, readFileSync as readFileSync5, readdirSync as readdirSync3 } from "node:fs";
|
|
4080
|
+
import { join as join7 } from "node:path";
|
|
4081
|
+
import { existsSync as existsSync2, readdirSync, readFileSync as readFileSync3 } from "node:fs";
|
|
4082
|
+
import { existsSync as existsSync3, readFileSync as readFileSync4, readdirSync as readdirSync2, realpathSync } from "node:fs";
|
|
4083
|
+
import { dirname as dirname4, join as join6, resolve as resolve4, sep } from "node:path";
|
|
4084
|
+
import { mkdirSync as mkdirSync2, rmdirSync, statSync as statSync2, unlinkSync as unlinkSync2, writeFileSync as writeFileSync2 } from "node:fs";
|
|
4085
|
+
import { dirname as dirname3, isAbsolute as isAbsolute2, join as join4, resolve as resolve3 } from "node:path";
|
|
4083
4086
|
import { setTimeout as sleep } from "node:timers/promises";
|
|
4084
4087
|
import { AsyncLocalStorage as AsyncLocalStorage2 } from "node:async_hooks";
|
|
4085
4088
|
import { execFileSync as execFileSync2 } from "node:child_process";
|
|
4086
|
-
import { existsSync as
|
|
4089
|
+
import { existsSync as existsSync6 } from "node:fs";
|
|
4087
4090
|
import { isAbsolute as isAbsolute4, resolve as resolve6 } from "node:path";
|
|
4088
4091
|
import { execFileSync as execFileSync3 } from "node:child_process";
|
|
4089
|
-
import { mkdirSync as mkdirSync5, readdirSync as
|
|
4090
|
-
import { basename as basename3, dirname as dirname6, isAbsolute as isAbsolute5, join as
|
|
4091
|
-
import { existsSync as
|
|
4092
|
-
import { existsSync as existsSync6, readFileSync as readFileSync7, readdirSync as readdirSync5 } from "node:fs";
|
|
4093
|
-
import { join as join9 } from "node:path";
|
|
4094
|
-
import { existsSync as existsSync7, mkdirSync as mkdirSync7, readdirSync as readdirSync7, readFileSync as readFileSync9, rmdirSync as rmdirSync2, rmSync, writeFileSync as writeFileSync5 } from "node:fs";
|
|
4092
|
+
import { mkdirSync as mkdirSync5, readdirSync as readdirSync5, readFileSync as readFileSync7, realpathSync as realpathSync3, statSync as statSync4, writeFileSync as writeFileSync4 } from "node:fs";
|
|
4093
|
+
import { basename as basename3, dirname as dirname6, isAbsolute as isAbsolute5, join as join9, resolve as resolve7 } from "node:path";
|
|
4094
|
+
import { existsSync as existsSync7, mkdirSync as mkdirSync7, readdirSync as readdirSync7, readFileSync as readFileSync9, rmdirSync as rmdirSync2, rmSync, writeFileSync as writeFileSync6 } from "node:fs";
|
|
4095
4095
|
import { basename as basename4, join as join11, resolve as resolve9, sep as sep3 } from "node:path";
|
|
4096
4096
|
import { existsSync as existsSync8, readdirSync as readdirSync8, readFileSync as readFileSync10 } from "node:fs";
|
|
4097
4097
|
import { basename as basename5, isAbsolute as isAbsolute7, join as join12, relative as relative4, resolve as resolve10, sep as sep4 } from "node:path";
|
|
@@ -4210,248 +4210,384 @@ function isAtOrBelow(dir, root) {
|
|
|
4210
4210
|
const rel = relative(root, dir);
|
|
4211
4211
|
return rel === "" || !rel.startsWith("..") && !isAbsolute(rel);
|
|
4212
4212
|
}
|
|
4213
|
-
|
|
4214
|
-
|
|
4215
|
-
|
|
4216
|
-
|
|
4217
|
-
|
|
4218
|
-
|
|
4219
|
-
|
|
4220
|
-
|
|
4221
|
-
|
|
4222
|
-
let dir = start;
|
|
4223
|
-
for (;; ) {
|
|
4224
|
-
if (!isAtOrBelow2(dir, boundary))
|
|
4225
|
-
return null;
|
|
4226
|
-
for (const candidate of [join3(dir, ".mstar"), join3(dir, ".agents"), join3(dir, ".plans"), join3(dir, "plans")]) {
|
|
4227
|
-
if (isDirectory(candidate))
|
|
4228
|
-
return candidate;
|
|
4229
|
-
}
|
|
4230
|
-
if (dir === boundary)
|
|
4231
|
-
return null;
|
|
4232
|
-
const parent = dirname3(dir);
|
|
4233
|
-
if (parent === dir)
|
|
4234
|
-
return null;
|
|
4235
|
-
dir = parent;
|
|
4236
|
-
}
|
|
4213
|
+
var COMPASS_STATUSES = ["active", "locked", "completed"];
|
|
4214
|
+
var DATE_RE = /^\d{4}-\d{2}-\d{2}$/;
|
|
4215
|
+
var PLAN_STATUS_DONE = "Done";
|
|
4216
|
+
function typeName(value) {
|
|
4217
|
+
if (value === null)
|
|
4218
|
+
return "null";
|
|
4219
|
+
if (Array.isArray(value))
|
|
4220
|
+
return "array";
|
|
4221
|
+
return typeof value;
|
|
4237
4222
|
}
|
|
4238
|
-
function
|
|
4239
|
-
|
|
4240
|
-
|
|
4241
|
-
|
|
4242
|
-
|
|
4243
|
-
|
|
4244
|
-
|
|
4245
|
-
if (!cdup)
|
|
4246
|
-
return startDir;
|
|
4247
|
-
let boundary = startDir;
|
|
4248
|
-
for (const segment of cdup.split(/[\\/]/)) {
|
|
4249
|
-
if (segment && segment !== ".")
|
|
4250
|
-
boundary = dirname3(boundary);
|
|
4223
|
+
function validateCompassShape(doc) {
|
|
4224
|
+
const issues = [];
|
|
4225
|
+
const expectString = (key, opts = {}) => {
|
|
4226
|
+
const value = doc[key];
|
|
4227
|
+
if (typeof value !== "string") {
|
|
4228
|
+
issues.push({ path: [key], message: `expected string, received ${typeName(value)}` });
|
|
4229
|
+
return;
|
|
4251
4230
|
}
|
|
4252
|
-
|
|
4253
|
-
|
|
4254
|
-
|
|
4255
|
-
}
|
|
4256
|
-
|
|
4257
|
-
|
|
4258
|
-
|
|
4259
|
-
}
|
|
4260
|
-
|
|
4261
|
-
|
|
4262
|
-
const
|
|
4263
|
-
|
|
4264
|
-
|
|
4265
|
-
|
|
4266
|
-
|
|
4267
|
-
|
|
4268
|
-
if (declared !== null) {
|
|
4269
|
-
if (opts.create !== false)
|
|
4270
|
-
mkdirSync2(declared, { recursive: true });
|
|
4271
|
-
return declared;
|
|
4272
|
-
}
|
|
4273
|
-
const harness = resolve3(harnessDir);
|
|
4274
|
-
const repoRoot = dirname3(harness);
|
|
4275
|
-
const candidates = [
|
|
4276
|
-
join3(harness, "specs"),
|
|
4277
|
-
join3(repoRoot, "docs", "specs"),
|
|
4278
|
-
join3(repoRoot, "specs"),
|
|
4279
|
-
join3(harness, "designs"),
|
|
4280
|
-
join3(repoRoot, "designs")
|
|
4281
|
-
];
|
|
4282
|
-
for (const candidate of candidates) {
|
|
4283
|
-
if (isDirectory(candidate) && hasFiles(candidate))
|
|
4284
|
-
return candidate;
|
|
4285
|
-
}
|
|
4286
|
-
const fallback = join3(harness, "specs");
|
|
4287
|
-
if (opts.create !== false)
|
|
4288
|
-
mkdirSync2(fallback, { recursive: true });
|
|
4289
|
-
return fallback;
|
|
4290
|
-
}
|
|
4291
|
-
function assertSafePathComponent(value, what) {
|
|
4292
|
-
if (value === "" || value === "." || value === ".." || !/^[A-Za-z0-9._-]+$/.test(value)) {
|
|
4293
|
-
throw new Error(`${what} must be a single safe path component ([A-Za-z0-9._-]+; not "", ".", "..", or containing "/" or "\\") \u2014 got ${JSON.stringify(value)}`);
|
|
4294
|
-
}
|
|
4295
|
-
}
|
|
4296
|
-
function resolveSddDir(harnessDir, planId) {
|
|
4297
|
-
assertSafePathComponent(planId, "planId");
|
|
4298
|
-
const base = resolve3(harnessDir);
|
|
4299
|
-
const declared = mstarcDirOverride(base, "sddDir");
|
|
4300
|
-
const sddBase = declared !== null ? declared : join3(base, "sdd");
|
|
4301
|
-
return join3(sddBase, planId);
|
|
4302
|
-
}
|
|
4303
|
-
function resolveHarnessSubdir(startDir, opts, key, fallback) {
|
|
4304
|
-
const harness = resolveHarnessDir(startDir, opts);
|
|
4305
|
-
if (harness === null) {
|
|
4306
|
-
throw new Error(`harness dir not found from ${resolve3(startDir)} \u2014 cannot resolve the ${fallback} dir (run \`mstar init\`, pass opts.harnessDir, or set MSTAR_HARNESS_DIR)`);
|
|
4231
|
+
if (opts.min !== undefined && value.length < opts.min) {
|
|
4232
|
+
issues.push({ path: [key], message: `string must contain at least ${opts.min} character(s)` });
|
|
4233
|
+
return;
|
|
4234
|
+
}
|
|
4235
|
+
if (opts.regex !== undefined && !opts.regex.test(value)) {
|
|
4236
|
+
issues.push({ path: [key], message: `string must match ${opts.regex}` });
|
|
4237
|
+
}
|
|
4238
|
+
};
|
|
4239
|
+
expectString("iteration_id", { min: 1 });
|
|
4240
|
+
expectString("start_date", { regex: DATE_RE });
|
|
4241
|
+
const status = doc.status;
|
|
4242
|
+
if (typeof status !== "string" || !COMPASS_STATUSES.includes(status)) {
|
|
4243
|
+
issues.push({
|
|
4244
|
+
path: ["status"],
|
|
4245
|
+
message: `expected one of ${COMPASS_STATUSES.map((s) => `'${s}'`).join(" | ")}, received ${typeName(status)}`
|
|
4246
|
+
});
|
|
4307
4247
|
}
|
|
4308
|
-
|
|
4309
|
-
|
|
4310
|
-
|
|
4311
|
-
|
|
4312
|
-
|
|
4313
|
-
}
|
|
4314
|
-
|
|
4315
|
-
|
|
4316
|
-
|
|
4317
|
-
|
|
4318
|
-
|
|
4319
|
-
|
|
4320
|
-
|
|
4321
|
-
|
|
4322
|
-
|
|
4323
|
-
!.mstar/knowledge/**
|
|
4324
|
-
!.mstar/specs/
|
|
4325
|
-
!.mstar/specs/**
|
|
4326
|
-
# .mstarc \u2014 repo-local harness config (may declare [config] harness_dir=<name>)
|
|
4327
|
-
.mstarc
|
|
4328
|
-
`;
|
|
4329
|
-
var GITIGNORE_SNIPPET_AGENTS = `# Morning Star harness (.agents/) \u2014 legacy
|
|
4330
|
-
# Default-ignore everything under .agents/, then re-include the tracked results.
|
|
4331
|
-
.agents/**
|
|
4332
|
-
!.agents/AGENTS.md
|
|
4333
|
-
!.agents/knowledge/
|
|
4334
|
-
!.agents/knowledge/**
|
|
4335
|
-
!.agents/specs/
|
|
4336
|
-
!.agents/specs/**
|
|
4337
|
-
`;
|
|
4338
|
-
var GITIGNORE_PROCESS_ENTRIES = GITIGNORE_SNIPPET.split(`
|
|
4339
|
-
`).filter((line) => line.startsWith(".mstar/") || line.startsWith("!.mstar/")).map((line) => line.trim());
|
|
4340
|
-
var GITIGNORE_PROCESS_ENTRIES_AGENTS = GITIGNORE_SNIPPET_AGENTS.split(`
|
|
4341
|
-
`).filter((line) => line.startsWith(".agents/") || line.startsWith("!.agents/")).map((line) => line.trim());
|
|
4342
|
-
function isDirectory(dir) {
|
|
4343
|
-
try {
|
|
4344
|
-
return statSync2(dir).isDirectory();
|
|
4345
|
-
} catch {
|
|
4346
|
-
return false;
|
|
4248
|
+
expectString("iteration_base_branch", { min: 1 });
|
|
4249
|
+
expectString("target_branch", { min: 1 });
|
|
4250
|
+
const plans = doc.plans;
|
|
4251
|
+
if (plans !== undefined) {
|
|
4252
|
+
if (!Array.isArray(plans)) {
|
|
4253
|
+
issues.push({ path: ["plans"], message: `expected array, received ${typeName(plans)}` });
|
|
4254
|
+
} else {
|
|
4255
|
+
plans.forEach((entry, index) => {
|
|
4256
|
+
if (typeof entry !== "string") {
|
|
4257
|
+
issues.push({ path: ["plans", index], message: `expected string, received ${typeName(entry)}` });
|
|
4258
|
+
} else if (entry.length < 1) {
|
|
4259
|
+
issues.push({ path: ["plans", index], message: "string must contain at least 1 character(s)" });
|
|
4260
|
+
}
|
|
4261
|
+
});
|
|
4262
|
+
}
|
|
4347
4263
|
}
|
|
4348
|
-
|
|
4349
|
-
|
|
4350
|
-
|
|
4351
|
-
|
|
4352
|
-
|
|
4353
|
-
|
|
4354
|
-
return true;
|
|
4355
|
-
} else if (entry.isFile()) {
|
|
4356
|
-
return true;
|
|
4357
|
-
}
|
|
4264
|
+
const end_date = doc.end_date;
|
|
4265
|
+
if (end_date !== undefined) {
|
|
4266
|
+
if (typeof end_date !== "string") {
|
|
4267
|
+
issues.push({ path: ["end_date"], message: `expected string, received ${typeName(end_date)}` });
|
|
4268
|
+
} else if (!DATE_RE.test(end_date)) {
|
|
4269
|
+
issues.push({ path: ["end_date"], message: `string must match ${DATE_RE}` });
|
|
4358
4270
|
}
|
|
4359
|
-
return false;
|
|
4360
|
-
} catch {
|
|
4361
|
-
return false;
|
|
4362
4271
|
}
|
|
4363
|
-
|
|
4364
|
-
|
|
4365
|
-
return
|
|
4272
|
+
if (issues.length > 0)
|
|
4273
|
+
return { ok: false, issues };
|
|
4274
|
+
return {
|
|
4275
|
+
ok: true,
|
|
4276
|
+
data: {
|
|
4277
|
+
iteration_id: doc.iteration_id,
|
|
4278
|
+
start_date: doc.start_date,
|
|
4279
|
+
status,
|
|
4280
|
+
iteration_base_branch: doc.iteration_base_branch,
|
|
4281
|
+
target_branch: doc.target_branch,
|
|
4282
|
+
...plans !== undefined ? { plans } : {},
|
|
4283
|
+
...end_date !== undefined ? { end_date } : {}
|
|
4284
|
+
}
|
|
4285
|
+
};
|
|
4366
4286
|
}
|
|
4367
4287
|
function violation(severity, code, message, fix) {
|
|
4368
4288
|
return { ok: false, severity, code, message, fix };
|
|
4369
4289
|
}
|
|
4370
|
-
function
|
|
4371
|
-
|
|
4372
|
-
violations.push(violation("high", missingCode, `missing required field: ${field}`));
|
|
4373
|
-
} else if (typeof value !== "string" || value.trim() === "") {
|
|
4374
|
-
violations.push(violation("medium", invalidCode, `${field} must be a non-empty string`));
|
|
4375
|
-
}
|
|
4376
|
-
}
|
|
4377
|
-
var DATE_PART = String.raw`\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12]\d|3[01])`;
|
|
4378
|
-
var RFC3339_Z_RE = new RegExp(String.raw`^${DATE_PART}T\d{2}:\d{2}:\d{2}(\.\d+)?Z$`);
|
|
4379
|
-
var DATE_ONLY_RE = new RegExp(String.raw`^${DATE_PART}$`);
|
|
4380
|
-
function isValidClaimedAt(value) {
|
|
4381
|
-
return typeof value === "string" && (RFC3339_Z_RE.test(value) || DATE_ONLY_RE.test(value));
|
|
4290
|
+
function isPlainObject2(value) {
|
|
4291
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
4382
4292
|
}
|
|
4383
|
-
function
|
|
4384
|
-
|
|
4385
|
-
if (!isPlainObject2(lease)) {
|
|
4293
|
+
function validateCompassFrontmatter(doc) {
|
|
4294
|
+
if (!isPlainObject2(doc)) {
|
|
4386
4295
|
return {
|
|
4387
4296
|
ok: false,
|
|
4388
4297
|
violations: [
|
|
4389
|
-
violation("
|
|
4298
|
+
violation("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 \u00a71.3)", "Fix the frontmatter of {ITERATION_DIR}/<iteration-id>/delivery-compass.md")
|
|
4390
4299
|
]
|
|
4391
4300
|
};
|
|
4392
4301
|
}
|
|
4393
|
-
|
|
4394
|
-
if (
|
|
4395
|
-
violations.push(violation("high", "lease.execution-lease.missing-claimed-at", "missing required field: claimed_at"));
|
|
4396
|
-
} else if (!isValidClaimedAt(lease.claimed_at)) {
|
|
4397
|
-
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"));
|
|
4398
|
-
}
|
|
4399
|
-
if (lease.worktree_path === undefined) {
|
|
4400
|
-
violations.push(violation("high", "lease.execution-lease.missing-worktree-path", "missing required field: worktree_path"));
|
|
4401
|
-
} else if (typeof lease.worktree_path !== "string" || lease.worktree_path.trim() === "") {
|
|
4402
|
-
violations.push(violation("medium", "lease.execution-lease.invalid-worktree-path", "worktree_path must be a non-empty string"));
|
|
4403
|
-
} else if (!isAbsolute3(lease.worktree_path)) {
|
|
4404
|
-
violations.push(violation("medium", "lease.execution-lease.invalid-worktree-path", "worktree_path must be an absolute path \u2014 it identifies the dedicated feature-worktree root (and MUST differ from metadata.control_worktree_path)"));
|
|
4405
|
-
}
|
|
4406
|
-
validateNonEmptyString(violations, lease.working_branch, "working_branch", "lease.execution-lease.missing-working-branch", "lease.execution-lease.invalid-working-branch");
|
|
4407
|
-
if (lease.session_label !== undefined && typeof lease.session_label !== "string") {
|
|
4408
|
-
violations.push(violation("medium", "lease.execution-lease.invalid-session-label", "session_label must be a string (display only \u2014 never used for ownership comparison)"));
|
|
4409
|
-
}
|
|
4410
|
-
return { ok: violations.length === 0, violations };
|
|
4411
|
-
}
|
|
4412
|
-
function validateIntegrationMergeLease(lease) {
|
|
4413
|
-
const violations = [];
|
|
4414
|
-
if (!isPlainObject2(lease)) {
|
|
4302
|
+
const parsed = validateCompassShape(doc);
|
|
4303
|
+
if (!parsed.ok) {
|
|
4415
4304
|
return {
|
|
4416
4305
|
ok: false,
|
|
4417
|
-
violations:
|
|
4418
|
-
|
|
4419
|
-
|
|
4306
|
+
violations: parsed.issues.map((issue) => {
|
|
4307
|
+
const field = issue.path.join(".") || "(root)";
|
|
4308
|
+
return violation("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 \u00a71.3)`);
|
|
4309
|
+
})
|
|
4420
4310
|
};
|
|
4421
4311
|
}
|
|
4422
|
-
|
|
4423
|
-
|
|
4424
|
-
|
|
4425
|
-
|
|
4426
|
-
violations.push(violation("medium", "lease.merge-lease.invalid-claimed-at", "claimed_at must be an RFC 3339 UTC timestamp with explicit Z (e.g. 2026-07-22T04:00:00Z) or a YYYY-MM-DD date"));
|
|
4312
|
+
const violations = [];
|
|
4313
|
+
const { status, end_date } = parsed.data;
|
|
4314
|
+
if (status === "completed" && end_date === undefined) {
|
|
4315
|
+
violations.push(violation("high", "COMPASS_END_DATE_REQUIRED", "Compass frontmatter status is 'completed' but end_date is missing \u2014 end_date is required at iteration-close (mstar-iteration \u00a73.4, template Fields guide)", "Add `end_date: YYYY-MM-DD` to the frontmatter"));
|
|
4427
4316
|
}
|
|
4428
|
-
|
|
4429
|
-
|
|
4430
|
-
validateNonEmptyString(violations, lease.target_branch, "target_branch", "lease.merge-lease.missing-target-branch", "lease.merge-lease.invalid-target-branch");
|
|
4431
|
-
if (lease.session_label !== undefined && typeof lease.session_label !== "string") {
|
|
4432
|
-
violations.push(violation("medium", "lease.merge-lease.invalid-session-label", "session_label must be a string (display only \u2014 never used for ownership comparison)"));
|
|
4317
|
+
if (status !== "completed" && end_date !== undefined) {
|
|
4318
|
+
violations.push(violation("medium", "COMPASS_END_DATE_NOT_ALLOWED", `Compass frontmatter sets end_date while status is '${status}' \u2014 end_date is only written at iteration-close (mstar-iteration \u00a73.4)`, "Remove end_date until iteration-close"));
|
|
4433
4319
|
}
|
|
4434
4320
|
return { ok: violations.length === 0, violations };
|
|
4435
4321
|
}
|
|
4436
|
-
|
|
4437
|
-
|
|
4438
|
-
|
|
4439
|
-
|
|
4440
|
-
|
|
4441
|
-
|
|
4442
|
-
if (
|
|
4443
|
-
|
|
4322
|
+
function registeredPlanIds(compassDoc) {
|
|
4323
|
+
if (!Array.isArray(compassDoc.plans))
|
|
4324
|
+
return [];
|
|
4325
|
+
return compassDoc.plans.filter((plan) => typeof plan === "string" && plan.length > 0);
|
|
4326
|
+
}
|
|
4327
|
+
function findPlanRow(snapshotDoc, planId) {
|
|
4328
|
+
if (!Array.isArray(snapshotDoc.plans))
|
|
4329
|
+
return null;
|
|
4330
|
+
for (const row of snapshotDoc.plans) {
|
|
4331
|
+
if (!isPlainObject2(row))
|
|
4332
|
+
continue;
|
|
4333
|
+
const rowId = typeof row.id === "string" ? row.id : typeof row.plan_id === "string" ? row.plan_id : null;
|
|
4334
|
+
if (rowId === planId)
|
|
4335
|
+
return row;
|
|
4444
4336
|
}
|
|
4445
|
-
|
|
4446
|
-
|
|
4447
|
-
|
|
4448
|
-
|
|
4449
|
-
|
|
4450
|
-
|
|
4451
|
-
|
|
4452
|
-
|
|
4453
|
-
|
|
4454
|
-
|
|
4337
|
+
return null;
|
|
4338
|
+
}
|
|
4339
|
+
function entryPlansAllDone(snapshotDoc, registered) {
|
|
4340
|
+
const violations = [];
|
|
4341
|
+
if (registered.length === 0) {
|
|
4342
|
+
violations.push(violation("medium", "COMPASS_NO_PLANS", "Compass frontmatter registers no plans \u2014 the all-plans-Done transition cannot be verified (mstar-iteration \u00a71.3 / Phase transition gates)", "List the iteration's plan ids in the compass frontmatter `plans`"));
|
|
4343
|
+
return violations;
|
|
4344
|
+
}
|
|
4345
|
+
for (const planId of registered) {
|
|
4346
|
+
const row = findPlanRow(snapshotDoc, planId);
|
|
4347
|
+
if (row === null) {
|
|
4348
|
+
violations.push(violation("high", "PLAN_NOT_IN_STATUS", `Plan '${planId}' is registered in the compass frontmatter but has no row in the workflow snapshot plans[] (mstar-iteration \u00a73.1 entry item 1)`, "Add the plan row to {HARNESS_DIR}/workflows/<id>/snapshot.json"));
|
|
4349
|
+
continue;
|
|
4350
|
+
}
|
|
4351
|
+
if (row.status !== PLAN_STATUS_DONE) {
|
|
4352
|
+
violations.push(violation("high", "PLAN_NOT_DONE", `Plan '${planId}' status is ${JSON.stringify(row.status)} in the workflow snapshot \u2014 all compass-registered plans must be 'Done' before iteration-close (mstar-iteration \u00a73.1 entry item 1)`));
|
|
4353
|
+
}
|
|
4354
|
+
}
|
|
4355
|
+
return violations;
|
|
4356
|
+
}
|
|
4357
|
+
function entryFrontmatterComplete(compassDoc) {
|
|
4358
|
+
return validateCompassFrontmatter(compassDoc).violations;
|
|
4359
|
+
}
|
|
4360
|
+
function exitFrontmatterClosed(compassDoc) {
|
|
4361
|
+
const violations = [];
|
|
4362
|
+
if (compassDoc.status !== "completed") {
|
|
4363
|
+
violations.push(violation("high", "EXIT_STATUS_NOT_COMPLETED", `Compass frontmatter status must be 'completed' at close exit \u2014 current: ${JSON.stringify(compassDoc.status)} (mstar-iteration \u00a73.4 / \u00a73.5 exit item 4)`));
|
|
4364
|
+
}
|
|
4365
|
+
const endDate = compassDoc.end_date;
|
|
4366
|
+
if (typeof endDate !== "string" || !DATE_RE.test(endDate)) {
|
|
4367
|
+
violations.push(violation("high", "EXIT_END_DATE_REQUIRED", "Compass frontmatter end_date (YYYY-MM-DD) is required when closing (mstar-iteration \u00a73.4 / \u00a73.5 exit item 4)"));
|
|
4368
|
+
}
|
|
4369
|
+
return violations;
|
|
4370
|
+
}
|
|
4371
|
+
function exitBranchCheck(opts) {
|
|
4372
|
+
const violations = [];
|
|
4373
|
+
const { currentBranch, specIntegrationBranch } = opts;
|
|
4374
|
+
if (currentBranch === undefined || specIntegrationBranch === undefined) {
|
|
4375
|
+
violations.push(violation("medium", "EXIT_BRANCH_UNVERIFIABLE", "Cannot verify the current branch is spec_integration_branch \u2014 missing currentBranch / specIntegrationBranch probe inputs (mstar-iteration \u00a73.5 exit item 5)"));
|
|
4376
|
+
} else if (currentBranch !== specIntegrationBranch) {
|
|
4377
|
+
violations.push(violation("high", "EXIT_BRANCH_MISMATCH", `Current branch '${currentBranch}' is not the spec_integration_branch '${specIntegrationBranch}' (mstar-iteration \u00a73.5 exit item 5)`));
|
|
4378
|
+
}
|
|
4379
|
+
return violations;
|
|
4380
|
+
}
|
|
4381
|
+
function exitPrBaseCheck(compassDoc, opts) {
|
|
4382
|
+
const violations = [];
|
|
4383
|
+
const target = compassDoc.target_branch;
|
|
4384
|
+
const { prBaseBranch } = opts;
|
|
4385
|
+
if (prBaseBranch === undefined) {
|
|
4386
|
+
violations.push(violation("medium", "EXIT_PR_BASE_UNVERIFIABLE", "Cannot verify the PR base \u2014 missing prBaseBranch probe input (mstar-iteration \u00a73.5 exit item 6)"));
|
|
4387
|
+
} else if (typeof target !== "string" || prBaseBranch !== target) {
|
|
4388
|
+
violations.push(violation("high", "EXIT_PR_BASE_MISMATCH", `PR base '${prBaseBranch}' must equal the compass target_branch '${String(target)}' \u2014 not an undocumented branch (mstar-iteration \u00a73.5 exit item 6)`));
|
|
4389
|
+
}
|
|
4390
|
+
return violations;
|
|
4391
|
+
}
|
|
4392
|
+
function evaluatePhaseGate(snapshotDoc, compassDoc, opts = {}) {
|
|
4393
|
+
const registered = registeredPlanIds(compassDoc);
|
|
4394
|
+
const entryViolations = [
|
|
4395
|
+
...entryPlansAllDone(snapshotDoc, registered),
|
|
4396
|
+
...entryFrontmatterComplete(compassDoc)
|
|
4397
|
+
];
|
|
4398
|
+
const exitViolations = [
|
|
4399
|
+
...exitFrontmatterClosed(compassDoc),
|
|
4400
|
+
...exitBranchCheck(opts),
|
|
4401
|
+
...exitPrBaseCheck(compassDoc, opts)
|
|
4402
|
+
];
|
|
4403
|
+
const allPlansDone = registered.length > 0 && registered.every((planId) => {
|
|
4404
|
+
const row = findPlanRow(snapshotDoc, planId);
|
|
4405
|
+
return row !== null && row.status === PLAN_STATUS_DONE;
|
|
4406
|
+
});
|
|
4407
|
+
const entry = { ok: entryViolations.length === 0, violations: entryViolations };
|
|
4408
|
+
const exit = { ok: exitViolations.length === 0, violations: exitViolations };
|
|
4409
|
+
let transition;
|
|
4410
|
+
if (!allPlansDone)
|
|
4411
|
+
transition = "phase-2-execute";
|
|
4412
|
+
else if (entry.ok && exit.ok)
|
|
4413
|
+
transition = "phase-4-pr-delivery";
|
|
4414
|
+
else
|
|
4415
|
+
transition = "phase-3-close";
|
|
4416
|
+
const gateBlocking = allPlansDone ? [...entryViolations, ...exitViolations] : [];
|
|
4417
|
+
return {
|
|
4418
|
+
transition,
|
|
4419
|
+
allPlansDone,
|
|
4420
|
+
entry,
|
|
4421
|
+
exit,
|
|
4422
|
+
ok: gateBlocking.length === 0,
|
|
4423
|
+
violations: gateBlocking
|
|
4424
|
+
};
|
|
4425
|
+
}
|
|
4426
|
+
function pushCadenceProbe(ciRunning, reviewWaveActive) {
|
|
4427
|
+
const violations = [];
|
|
4428
|
+
if (ciRunning) {
|
|
4429
|
+
violations.push(violation("high", "PUSH_BLOCKED_CI", "CI checks are still queued/in_progress on the current head \u2014 do not push until the wave completes (mstar-iteration \u00a75.1a push gate 1)", "Wait for CI to settle, then push once with the whole local batch"));
|
|
4430
|
+
}
|
|
4431
|
+
if (reviewWaveActive) {
|
|
4432
|
+
violations.push(violation("high", "PUSH_BLOCKED_REVIEW_WAVE", "An AI/bot review wave is still running on the current head \u2014 do not push until it settles (mstar-iteration \u00a75.1a push gate 2)", "Wait for the review wave, then push once"));
|
|
4433
|
+
}
|
|
4434
|
+
return { ok: violations.length === 0, violations };
|
|
4435
|
+
}
|
|
4436
|
+
function parseCompassFrontmatter(filePath) {
|
|
4437
|
+
return parseCompassFrontmatterText(readFileSync3(filePath, "utf8"), filePath);
|
|
4438
|
+
}
|
|
4439
|
+
function parseCompassFrontmatterText(content, filePath) {
|
|
4440
|
+
const lines = content.split(/\r?\n/);
|
|
4441
|
+
if (lines[0]?.trim() !== "---") {
|
|
4442
|
+
throw new Error(`no YAML frontmatter fence in ${filePath} (expected first line "---")`);
|
|
4443
|
+
}
|
|
4444
|
+
const end = lines.indexOf("---", 1);
|
|
4445
|
+
if (end === -1) {
|
|
4446
|
+
throw new Error(`unterminated YAML frontmatter in ${filePath} (no closing "---")`);
|
|
4447
|
+
}
|
|
4448
|
+
const doc = {};
|
|
4449
|
+
let listKey = null;
|
|
4450
|
+
for (let i = 1;i < end; i += 1) {
|
|
4451
|
+
const line = lines[i] ?? "";
|
|
4452
|
+
if (!line.trim() || line.trim().startsWith("#"))
|
|
4453
|
+
continue;
|
|
4454
|
+
if (listKey !== null && /^\s*-\s+/.test(line)) {
|
|
4455
|
+
const item = line.replace(/^\s*-\s+/, "").trim().replace(/^["']|["']$/g, "");
|
|
4456
|
+
if (!Array.isArray(doc[listKey]))
|
|
4457
|
+
doc[listKey] = [];
|
|
4458
|
+
doc[listKey].push(item);
|
|
4459
|
+
continue;
|
|
4460
|
+
}
|
|
4461
|
+
listKey = null;
|
|
4462
|
+
const kv = line.match(/^([A-Za-z_][A-Za-z0-9_-]*):\s*(.*)$/);
|
|
4463
|
+
if (!kv) {
|
|
4464
|
+
throw new Error(`unsupported frontmatter line in ${filePath}: ${JSON.stringify(line)}`);
|
|
4465
|
+
}
|
|
4466
|
+
const value = kv[2].trim();
|
|
4467
|
+
doc[kv[1]] = value === "" ? null : /^\[.*\]$/.test(value) ? parseFlowArray(value, filePath) : value.replace(/^["']|["']$/g, "");
|
|
4468
|
+
listKey = value === "" ? kv[1] : null;
|
|
4469
|
+
}
|
|
4470
|
+
return doc;
|
|
4471
|
+
}
|
|
4472
|
+
function parseFlowArray(raw, filePath) {
|
|
4473
|
+
const inner = raw.slice(1, -1);
|
|
4474
|
+
if (/[[\]]/.test(inner)) {
|
|
4475
|
+
throw new Error(`nested flow-style array in ${filePath}: ${JSON.stringify(raw)} \u2014 only flat scalar items are supported (e.g. [a, b])`);
|
|
4476
|
+
}
|
|
4477
|
+
let quote = null;
|
|
4478
|
+
for (const ch of inner) {
|
|
4479
|
+
if (ch === '"' || ch === "'") {
|
|
4480
|
+
if (quote === null)
|
|
4481
|
+
quote = ch;
|
|
4482
|
+
else if (quote === ch)
|
|
4483
|
+
quote = null;
|
|
4484
|
+
} else if (ch === "," && quote !== null) {
|
|
4485
|
+
throw new Error(`ambiguous flow-style array in ${filePath}: ${JSON.stringify(raw)} \u2014 quoted item containing comma cannot be split unambiguously (flat scalar items only)`);
|
|
4486
|
+
}
|
|
4487
|
+
}
|
|
4488
|
+
if (quote !== null) {
|
|
4489
|
+
throw new Error(`unterminated ${quote} quote in flow-style array in ${filePath}: ${JSON.stringify(raw)}`);
|
|
4490
|
+
}
|
|
4491
|
+
const items = [];
|
|
4492
|
+
for (const part of inner.split(",")) {
|
|
4493
|
+
const item = part.trim().replace(/^["']|["']$/g, "");
|
|
4494
|
+
if (item === "")
|
|
4495
|
+
continue;
|
|
4496
|
+
items.push(item);
|
|
4497
|
+
}
|
|
4498
|
+
return items;
|
|
4499
|
+
}
|
|
4500
|
+
function isPlainObject22(value) {
|
|
4501
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
4502
|
+
}
|
|
4503
|
+
function violation2(severity, code, message, fix) {
|
|
4504
|
+
return { ok: false, severity, code, message, fix };
|
|
4505
|
+
}
|
|
4506
|
+
function validateNonEmptyString(violations, value, field, missingCode, invalidCode) {
|
|
4507
|
+
if (value === undefined) {
|
|
4508
|
+
violations.push(violation2("high", missingCode, `missing required field: ${field}`));
|
|
4509
|
+
} else if (typeof value !== "string" || value.trim() === "") {
|
|
4510
|
+
violations.push(violation2("medium", invalidCode, `${field} must be a non-empty string`));
|
|
4511
|
+
}
|
|
4512
|
+
}
|
|
4513
|
+
var DATE_PART = String.raw`\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12]\d|3[01])`;
|
|
4514
|
+
var RFC3339_Z_RE = new RegExp(String.raw`^${DATE_PART}T\d{2}:\d{2}:\d{2}(\.\d+)?Z$`);
|
|
4515
|
+
var DATE_ONLY_RE = new RegExp(String.raw`^${DATE_PART}$`);
|
|
4516
|
+
function isValidClaimedAt(value) {
|
|
4517
|
+
return typeof value === "string" && (RFC3339_Z_RE.test(value) || DATE_ONLY_RE.test(value));
|
|
4518
|
+
}
|
|
4519
|
+
function validateExecutionLease(lease) {
|
|
4520
|
+
const violations = [];
|
|
4521
|
+
if (!isPlainObject22(lease)) {
|
|
4522
|
+
return {
|
|
4523
|
+
ok: false,
|
|
4524
|
+
violations: [
|
|
4525
|
+
violation2("high", "lease.execution-lease.invalid", "execution_lease must be an object \u2014 null and tombstone objects are invalid; writers delete the key on release")
|
|
4526
|
+
]
|
|
4527
|
+
};
|
|
4528
|
+
}
|
|
4529
|
+
validateNonEmptyString(violations, lease.holder, "holder", "lease.execution-lease.missing-holder", "lease.execution-lease.invalid-holder");
|
|
4530
|
+
if (lease.claimed_at === undefined) {
|
|
4531
|
+
violations.push(violation2("high", "lease.execution-lease.missing-claimed-at", "missing required field: claimed_at"));
|
|
4532
|
+
} else if (!isValidClaimedAt(lease.claimed_at)) {
|
|
4533
|
+
violations.push(violation2("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"));
|
|
4534
|
+
}
|
|
4535
|
+
if (lease.worktree_path === undefined) {
|
|
4536
|
+
violations.push(violation2("high", "lease.execution-lease.missing-worktree-path", "missing required field: worktree_path"));
|
|
4537
|
+
} else if (typeof lease.worktree_path !== "string" || lease.worktree_path.trim() === "") {
|
|
4538
|
+
violations.push(violation2("medium", "lease.execution-lease.invalid-worktree-path", "worktree_path must be a non-empty string"));
|
|
4539
|
+
} else if (!isAbsolute2(lease.worktree_path)) {
|
|
4540
|
+
violations.push(violation2("medium", "lease.execution-lease.invalid-worktree-path", "worktree_path must be an absolute path \u2014 it identifies the dedicated feature-worktree root (and MUST differ from metadata.control_worktree_path)"));
|
|
4541
|
+
}
|
|
4542
|
+
validateNonEmptyString(violations, lease.working_branch, "working_branch", "lease.execution-lease.missing-working-branch", "lease.execution-lease.invalid-working-branch");
|
|
4543
|
+
if (lease.session_label !== undefined && typeof lease.session_label !== "string") {
|
|
4544
|
+
violations.push(violation2("medium", "lease.execution-lease.invalid-session-label", "session_label must be a string (display only \u2014 never used for ownership comparison)"));
|
|
4545
|
+
}
|
|
4546
|
+
return { ok: violations.length === 0, violations };
|
|
4547
|
+
}
|
|
4548
|
+
function validateIntegrationMergeLease(lease) {
|
|
4549
|
+
const violations = [];
|
|
4550
|
+
if (!isPlainObject22(lease)) {
|
|
4551
|
+
return {
|
|
4552
|
+
ok: false,
|
|
4553
|
+
violations: [
|
|
4554
|
+
violation2("high", "lease.merge-lease.invalid", "integration_merge_lease must be an object \u2014 absent means unclaimed; null and tombstone objects are invalid; writers delete the key on release")
|
|
4555
|
+
]
|
|
4556
|
+
};
|
|
4557
|
+
}
|
|
4558
|
+
validateNonEmptyString(violations, lease.holder, "holder", "lease.merge-lease.missing-holder", "lease.merge-lease.invalid-holder");
|
|
4559
|
+
if (lease.claimed_at === undefined) {
|
|
4560
|
+
violations.push(violation2("high", "lease.merge-lease.missing-claimed-at", "missing required field: claimed_at"));
|
|
4561
|
+
} else if (!isValidClaimedAt(lease.claimed_at)) {
|
|
4562
|
+
violations.push(violation2("medium", "lease.merge-lease.invalid-claimed-at", "claimed_at must be an RFC 3339 UTC timestamp with explicit Z (e.g. 2026-07-22T04:00:00Z) or a YYYY-MM-DD date"));
|
|
4563
|
+
}
|
|
4564
|
+
validateNonEmptyString(violations, lease.plan_id, "plan_id", "lease.merge-lease.missing-plan-id", "lease.merge-lease.invalid-plan-id");
|
|
4565
|
+
validateNonEmptyString(violations, lease.source_branch, "source_branch", "lease.merge-lease.missing-source-branch", "lease.merge-lease.invalid-source-branch");
|
|
4566
|
+
validateNonEmptyString(violations, lease.target_branch, "target_branch", "lease.merge-lease.missing-target-branch", "lease.merge-lease.invalid-target-branch");
|
|
4567
|
+
if (lease.session_label !== undefined && typeof lease.session_label !== "string") {
|
|
4568
|
+
violations.push(violation2("medium", "lease.merge-lease.invalid-session-label", "session_label must be a string (display only \u2014 never used for ownership comparison)"));
|
|
4569
|
+
}
|
|
4570
|
+
return { ok: violations.length === 0, violations };
|
|
4571
|
+
}
|
|
4572
|
+
var STATUS_WRITE_LOCKDIR = ".status-write.lockdir";
|
|
4573
|
+
var LOCKDIR_HOLDER_PID = "holder.pid";
|
|
4574
|
+
var heldLockDirs = new AsyncLocalStorage2;
|
|
4575
|
+
async function withStatusWriteLock(statusPath, fn, opts = {}) {
|
|
4576
|
+
const lockDir = join4(dirname3(resolve3(statusPath)), STATUS_WRITE_LOCKDIR);
|
|
4577
|
+
const held = heldLockDirs.getStore();
|
|
4578
|
+
if (held !== undefined && held.has(lockDir)) {
|
|
4579
|
+
throw new Error(`${lockDir} is already held by this process in this async context \u2014 withStatusWriteLock is not reentrant; a nested acquisition on the same status.json is a bug`);
|
|
4580
|
+
}
|
|
4581
|
+
const timeoutMs = opts.timeoutMs ?? 30000;
|
|
4582
|
+
const pollMs = opts.pollMs ?? 25;
|
|
4583
|
+
const deadline = Date.now() + timeoutMs;
|
|
4584
|
+
let acquired = null;
|
|
4585
|
+
for (;; ) {
|
|
4586
|
+
try {
|
|
4587
|
+
mkdirSync2(lockDir);
|
|
4588
|
+
const st = statSync2(lockDir);
|
|
4589
|
+
acquired = { dev: st.dev, ino: st.ino };
|
|
4590
|
+
break;
|
|
4455
4591
|
} catch (error) {
|
|
4456
4592
|
if (error.code !== "EEXIST")
|
|
4457
4593
|
throw error;
|
|
@@ -4471,7 +4607,7 @@ async function withStatusWriteLock(statusPath, fn, opts = {}) {
|
|
|
4471
4607
|
} finally {
|
|
4472
4608
|
owns.delete(lockDir);
|
|
4473
4609
|
try {
|
|
4474
|
-
const current =
|
|
4610
|
+
const current = statSync2(lockDir);
|
|
4475
4611
|
if (acquired !== null && current.dev === acquired.dev && current.ino === acquired.ino) {
|
|
4476
4612
|
try {
|
|
4477
4613
|
unlinkSync2(join4(lockDir, LOCKDIR_HOLDER_PID));
|
|
@@ -4487,7 +4623,7 @@ var REQUIRED_FIELDS = [
|
|
|
4487
4623
|
{ key: "delegation", label: "Delegation", code: "delegation" },
|
|
4488
4624
|
{ key: "taskCategory", label: "Task category", code: "task-category" }
|
|
4489
4625
|
];
|
|
4490
|
-
function
|
|
4626
|
+
function violation3(severity, code, message, fix) {
|
|
4491
4627
|
return { ok: false, severity, code, message, fix };
|
|
4492
4628
|
}
|
|
4493
4629
|
function parseAssignmentFields(assignmentText) {
|
|
@@ -4512,11 +4648,11 @@ function parseAssignmentFields(assignmentText) {
|
|
|
4512
4648
|
}
|
|
4513
4649
|
function requireField(violations, value, label, code) {
|
|
4514
4650
|
if (value === undefined) {
|
|
4515
|
-
const v =
|
|
4651
|
+
const v = violation3("high", `assignment.field.missing-${code}`, `missing required Assignment field: ${label}`, `add "**${label}**: <value>" to the Assignment`);
|
|
4516
4652
|
v.aliases = [`assignment.presence.missing-${code}`];
|
|
4517
4653
|
violations.push(v);
|
|
4518
4654
|
} else if (value === "") {
|
|
4519
|
-
const v =
|
|
4655
|
+
const v = violation3("high", `assignment.field.invalid-${code}`, `${label} must be non-empty`, `fill in "**${label}**: <value>"`);
|
|
4520
4656
|
v.aliases = [`assignment.presence.missing-${code}`];
|
|
4521
4657
|
violations.push(v);
|
|
4522
4658
|
}
|
|
@@ -4575,20 +4711,20 @@ function validateAssignmentFields(assignmentText, opts = {}) {
|
|
|
4575
4711
|
const formCount = Number(workingPresent) + Number(policyPresent);
|
|
4576
4712
|
const forms = parseAssignmentBranchForms(assignmentText);
|
|
4577
4713
|
if (formCount === 0) {
|
|
4578
|
-
violations.push(
|
|
4714
|
+
violations.push(violation3("high", "assignment.field.branch-missing", "writable assignment must contain exactly one branch form", `add exactly one of: ${BRANCH_FORMS_HINT}`));
|
|
4579
4715
|
} else if (formCount > 1) {
|
|
4580
|
-
violations.push(
|
|
4716
|
+
violations.push(violation3("high", "assignment.field.branch-multiple", `writable assignment contains ${formCount} branch forms (Working branch + Branch policy) \u2014 exactly one required`, `keep exactly one of: ${BRANCH_FORMS_HINT}`));
|
|
4581
4717
|
} else if (workingPresent) {
|
|
4582
4718
|
const create = forms.createForm;
|
|
4583
4719
|
if (create !== undefined && (create.base === undefined || create.base.trim() === "" || create.name.trim() === "")) {
|
|
4584
|
-
violations.push(
|
|
4720
|
+
violations.push(violation3("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`)"));
|
|
4585
4721
|
}
|
|
4586
4722
|
} else if (policyPresent) {
|
|
4587
4723
|
const direct = forms.directOn;
|
|
4588
4724
|
if (direct === undefined) {
|
|
4589
|
-
violations.push(
|
|
4725
|
+
violations.push(violation3("high", "assignment.field.branch-policy-missing-branch", `unparseable Branch policy: "${fields.branchPolicy}" (expected "direct on <branch> \u2014 <reason>")`, "start the field with `direct on <branch>`"));
|
|
4590
4726
|
} else if (direct.reason === "") {
|
|
4591
|
-
violations.push(
|
|
4727
|
+
violations.push(violation3("high", "assignment.field.branch-policy-missing-reason", `Branch policy "direct on ${direct.branch}" is missing the reason`, 'append "\u2014 <reason>" after the branch name'));
|
|
4592
4728
|
}
|
|
4593
4729
|
}
|
|
4594
4730
|
}
|
|
@@ -4599,7 +4735,7 @@ function assertDefaultBranchProtected(branch, opts = {}) {
|
|
|
4599
4735
|
const violations = [];
|
|
4600
4736
|
const normalized = branch.trim();
|
|
4601
4737
|
if (normalized !== "" && defaultBranches.includes(normalized) && opts.directOnException !== true) {
|
|
4602
|
-
violations.push(
|
|
4738
|
+
violations.push(violation3("high", "dispatch.default-branch.protected", `writable work on default protected branch "${normalized}" requires an explicit direct-on exception`, `add "Branch policy: direct on ${normalized} \u2014 <reason>" to the Assignment, or use a feature branch`));
|
|
4603
4739
|
}
|
|
4604
4740
|
return { ok: violations.length === 0, violations };
|
|
4605
4741
|
}
|
|
@@ -4608,7 +4744,7 @@ function executionModeToN(executionMode, opts = {}) {
|
|
|
4608
4744
|
const mode = executionMode.trim().toLowerCase().split(/\s+/)[0] ?? "";
|
|
4609
4745
|
let n;
|
|
4610
4746
|
if (mode === "") {
|
|
4611
|
-
violations.push(
|
|
4747
|
+
violations.push(violation3("high", "dispatch.execution-mode.missing", "missing required Assignment field: Execution mode", 'add "**Execution mode**: sdd | inline | targeted"'));
|
|
4612
4748
|
} else if (mode === "sdd") {
|
|
4613
4749
|
n = 3;
|
|
4614
4750
|
} else if (mode === "inline") {
|
|
@@ -4616,14 +4752,14 @@ function executionModeToN(executionMode, opts = {}) {
|
|
|
4616
4752
|
} else if (mode === "targeted") {
|
|
4617
4753
|
const seats = [...new Set((opts.seats ?? []).map((s) => s.trim()).filter((s) => s !== ""))];
|
|
4618
4754
|
if (seats.length === 0) {
|
|
4619
|
-
violations.push(
|
|
4755
|
+
violations.push(violation3("high", "dispatch.execution-mode.missing-seats", 'execution mode "targeted" requires listed reviewer seats', 'add "QC re-review: targeted \u2014 reviewers: <role-id>, \u2026" to the Assignment and pass the seats'));
|
|
4620
4756
|
} else if (seats.length > 3) {
|
|
4621
|
-
violations.push(
|
|
4757
|
+
violations.push(violation3("high", "dispatch.execution-mode.too-many-seats", `execution mode "targeted" lists ${seats.length} reviewer seats \u2014 at most 3 (targeted re-review seats are the tri seats, N = 1\u20133)`, "list at most three reviewer seats for the targeted re-review"));
|
|
4622
4758
|
} else {
|
|
4623
4759
|
n = seats.length;
|
|
4624
4760
|
}
|
|
4625
4761
|
} else {
|
|
4626
|
-
violations.push(
|
|
4762
|
+
violations.push(violation3("high", "dispatch.execution-mode.unknown", `unknown execution mode "${executionMode.trim()}" (expected sdd | inline | targeted)`, "fix the Execution mode field"));
|
|
4627
4763
|
}
|
|
4628
4764
|
return n === undefined ? { ok: false, violations } : { ok: true, violations, n };
|
|
4629
4765
|
}
|
|
@@ -4637,7 +4773,7 @@ function assertTriIdentity(reviewerRoles) {
|
|
|
4637
4773
|
return {
|
|
4638
4774
|
ok: false,
|
|
4639
4775
|
violations: [
|
|
4640
|
-
|
|
4776
|
+
violation3("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")
|
|
4641
4777
|
]
|
|
4642
4778
|
};
|
|
4643
4779
|
}
|
|
@@ -4645,45 +4781,45 @@ var WORKFLOW_SNAPSHOT_FILE = "snapshot.json";
|
|
|
4645
4781
|
var WORKFLOW_LIFECYCLE_STATUSES = ["running", "paused", "completed", "failed", "stopped"];
|
|
4646
4782
|
var WORKFLOW_TERMINAL_STATUSES = ["completed", "failed", "stopped"];
|
|
4647
4783
|
var WORKFLOW_LIFECYCLE_TYPES = ["plan", "iteration"];
|
|
4648
|
-
function
|
|
4784
|
+
function isPlainObject3(value) {
|
|
4649
4785
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
4650
4786
|
}
|
|
4651
|
-
function
|
|
4787
|
+
function violation4(severity, code, message, fix) {
|
|
4652
4788
|
return { ok: false, severity, code, message, fix };
|
|
4653
4789
|
}
|
|
4654
4790
|
function validateNonEmptyString2(violations, value, field, missingCode, invalidCode) {
|
|
4655
4791
|
if (value === undefined) {
|
|
4656
|
-
violations.push(
|
|
4792
|
+
violations.push(violation4("high", missingCode, `missing required field: ${field}`));
|
|
4657
4793
|
} else if (typeof value !== "string" || value.trim() === "") {
|
|
4658
|
-
violations.push(
|
|
4794
|
+
violations.push(violation4("medium", invalidCode, `${field} must be a non-empty string`));
|
|
4659
4795
|
}
|
|
4660
4796
|
}
|
|
4661
4797
|
function validateWorkflowSnapshot(doc) {
|
|
4662
4798
|
const violations = [];
|
|
4663
|
-
if (!
|
|
4799
|
+
if (!isPlainObject3(doc)) {
|
|
4664
4800
|
return {
|
|
4665
4801
|
ok: false,
|
|
4666
|
-
violations: [
|
|
4802
|
+
violations: [violation4("high", "workflow.snapshot.invalid", "workflow snapshot must be an object")]
|
|
4667
4803
|
};
|
|
4668
4804
|
}
|
|
4669
4805
|
if (doc.schema_version === undefined) {
|
|
4670
|
-
violations.push(
|
|
4806
|
+
violations.push(violation4("high", "workflow.snapshot.missing-schema-version", "missing required field: schema_version"));
|
|
4671
4807
|
} else if (doc.schema_version !== 1) {
|
|
4672
|
-
violations.push(
|
|
4808
|
+
violations.push(violation4("high", "workflow.snapshot.invalid-schema-version", `schema_version must be 1 \u2014 got ${JSON.stringify(doc.schema_version)} (version is reserved for the root file discriminator)`));
|
|
4673
4809
|
}
|
|
4674
4810
|
if (doc.version !== undefined) {
|
|
4675
|
-
violations.push(
|
|
4811
|
+
violations.push(violation4("medium", "workflow.snapshot.reserved-version", `top-level version is reserved for the root status.json discriminator \u2014 snapshots use schema_version; remove the version key (got ${JSON.stringify(doc.version)})`, "remove the version key from the snapshot"));
|
|
4676
4812
|
}
|
|
4677
4813
|
validateNonEmptyString2(violations, doc.id, "id", "workflow.snapshot.missing-id", "workflow.snapshot.invalid-id");
|
|
4678
4814
|
if (doc.type === undefined) {
|
|
4679
|
-
violations.push(
|
|
4815
|
+
violations.push(violation4("high", "workflow.snapshot.missing-type", "missing required field: type"));
|
|
4680
4816
|
} else if (typeof doc.type !== "string" || !WORKFLOW_LIFECYCLE_TYPES.includes(doc.type)) {
|
|
4681
|
-
violations.push(
|
|
4817
|
+
violations.push(violation4("medium", "workflow.snapshot.invalid-type", `type must be one of ${WORKFLOW_LIFECYCLE_TYPES.join(" | ")} \u2014 got ${JSON.stringify(doc.type)}`));
|
|
4682
4818
|
}
|
|
4683
4819
|
if (doc.status === undefined) {
|
|
4684
|
-
violations.push(
|
|
4820
|
+
violations.push(violation4("high", "workflow.snapshot.missing-status", "missing required field: status"));
|
|
4685
4821
|
} else if (typeof doc.status !== "string" || !WORKFLOW_LIFECYCLE_STATUSES.includes(doc.status)) {
|
|
4686
|
-
violations.push(
|
|
4822
|
+
violations.push(violation4("medium", "workflow.snapshot.invalid-status", `status must be one of ${WORKFLOW_LIFECYCLE_STATUSES.join(" | ")} \u2014 got ${JSON.stringify(doc.status)}`));
|
|
4687
4823
|
}
|
|
4688
4824
|
validateNonEmptyString2(violations, doc.started_at, "started_at", "workflow.snapshot.missing-started-at", "workflow.snapshot.invalid-started-at");
|
|
4689
4825
|
validateNonEmptyString2(violations, doc.updated_at, "updated_at", "workflow.snapshot.missing-updated-at", "workflow.snapshot.invalid-updated-at");
|
|
@@ -4691,35 +4827,35 @@ function validateWorkflowSnapshot(doc) {
|
|
|
4691
4827
|
validateNonEmptyString2(violations, doc.ended_at, "ended_at", "workflow.snapshot.missing-ended-at", "workflow.snapshot.invalid-ended-at");
|
|
4692
4828
|
}
|
|
4693
4829
|
if (doc.phase !== undefined && typeof doc.phase !== "string") {
|
|
4694
|
-
violations.push(
|
|
4830
|
+
violations.push(violation4("medium", "workflow.snapshot.invalid-phase", "phase must be a string (free-form phase machine label)"));
|
|
4695
4831
|
}
|
|
4696
4832
|
if (doc.plans === undefined) {
|
|
4697
|
-
violations.push(
|
|
4833
|
+
violations.push(violation4("high", "workflow.snapshot.missing-plans", "missing required field: plans"));
|
|
4698
4834
|
} else if (!Array.isArray(doc.plans)) {
|
|
4699
|
-
violations.push(
|
|
4835
|
+
violations.push(violation4("high", "workflow.snapshot.invalid-plans", "plans must be an array of legacy plan rows"));
|
|
4700
4836
|
} else {
|
|
4701
4837
|
for (const row of doc.plans) {
|
|
4702
4838
|
violations.push(...validatePlanRow(row).violations);
|
|
4703
|
-
if (
|
|
4839
|
+
if (isPlainObject3(row) && row.execution_lease !== undefined) {
|
|
4704
4840
|
violations.push(...validateExecutionLease(row.execution_lease).violations);
|
|
4705
4841
|
}
|
|
4706
4842
|
}
|
|
4707
4843
|
}
|
|
4708
4844
|
if (doc.execution_policy !== undefined) {
|
|
4709
|
-
if (!
|
|
4710
|
-
violations.push(
|
|
4845
|
+
if (!isPlainObject3(doc.execution_policy)) {
|
|
4846
|
+
violations.push(violation4("medium", "workflow.snapshot.invalid-execution-policy", "execution_policy must be an object"));
|
|
4711
4847
|
}
|
|
4712
4848
|
}
|
|
4713
4849
|
if (doc.integration_merge_lease !== undefined) {
|
|
4714
4850
|
violations.push(...validateIntegrationMergeLease(doc.integration_merge_lease).violations);
|
|
4715
4851
|
}
|
|
4716
4852
|
if (doc.branch !== undefined) {
|
|
4717
|
-
if (!
|
|
4718
|
-
violations.push(
|
|
4853
|
+
if (!isPlainObject3(doc.branch)) {
|
|
4854
|
+
violations.push(violation4("medium", "workflow.snapshot.invalid-branch", "branch must be an object"));
|
|
4719
4855
|
} else {
|
|
4720
4856
|
for (const key of ["base", "integration", "target"]) {
|
|
4721
4857
|
if (doc.branch[key] !== undefined && (typeof doc.branch[key] !== "string" || doc.branch[key].trim() === "")) {
|
|
4722
|
-
violations.push(
|
|
4858
|
+
violations.push(violation4("medium", "workflow.snapshot.invalid-branch", `branch.${key} must be a non-empty string`));
|
|
4723
4859
|
}
|
|
4724
4860
|
}
|
|
4725
4861
|
}
|
|
@@ -4727,8 +4863,8 @@ function validateWorkflowSnapshot(doc) {
|
|
|
4727
4863
|
if (doc.control_worktree_path !== undefined) {
|
|
4728
4864
|
validateNonEmptyString2(violations, doc.control_worktree_path, "control_worktree_path", "workflow.snapshot.missing-control-worktree-path", "workflow.snapshot.invalid-control-worktree-path");
|
|
4729
4865
|
}
|
|
4730
|
-
if (doc.legacy_metadata !== undefined && !
|
|
4731
|
-
violations.push(
|
|
4866
|
+
if (doc.legacy_metadata !== undefined && !isPlainObject3(doc.legacy_metadata)) {
|
|
4867
|
+
violations.push(violation4("medium", "workflow.snapshot.invalid-legacy-metadata", "legacy_metadata must be an object"));
|
|
4732
4868
|
}
|
|
4733
4869
|
if (doc.compass_ref !== undefined) {
|
|
4734
4870
|
validateNonEmptyString2(violations, doc.compass_ref, "compass_ref", "workflow.snapshot.missing-compass-ref", "workflow.snapshot.invalid-compass-ref");
|
|
@@ -4736,27 +4872,27 @@ function validateWorkflowSnapshot(doc) {
|
|
|
4736
4872
|
const terminal = typeof doc.status === "string" && WORKFLOW_TERMINAL_STATUSES.includes(doc.status);
|
|
4737
4873
|
if (terminal) {
|
|
4738
4874
|
if (doc.ended_at === undefined) {
|
|
4739
|
-
violations.push(
|
|
4875
|
+
violations.push(violation4("high", "workflow.snapshot.missing-ended-at", `terminal status ${JSON.stringify(doc.status)} requires ended_at \u2014 a terminal snapshot must record when the lifecycle ended`));
|
|
4740
4876
|
}
|
|
4741
4877
|
if (Array.isArray(doc.plans)) {
|
|
4742
4878
|
for (const row of doc.plans) {
|
|
4743
|
-
if (
|
|
4744
|
-
violations.push(
|
|
4879
|
+
if (isPlainObject3(row) && row.execution_lease !== undefined) {
|
|
4880
|
+
violations.push(violation4("high", "workflow.snapshot.terminal-dangling-execution-lease", `terminal snapshot must not carry a row execution_lease (dangling lease) \u2014 release every lease before the lifecycle ends`));
|
|
4745
4881
|
}
|
|
4746
4882
|
}
|
|
4747
4883
|
}
|
|
4748
4884
|
if (doc.integration_merge_lease !== undefined) {
|
|
4749
|
-
violations.push(
|
|
4885
|
+
violations.push(violation4("high", "workflow.snapshot.terminal-dangling-merge-lease", "terminal snapshot must not carry integration_merge_lease (dangling lease) \u2014 release the merge lease before the lifecycle ends"));
|
|
4750
4886
|
}
|
|
4751
4887
|
}
|
|
4752
4888
|
return { ok: violations.length === 0, violations };
|
|
4753
4889
|
}
|
|
4754
|
-
var
|
|
4890
|
+
var DATE_RE2 = /^\d{4}-\d{2}-\d{2}$/;
|
|
4755
4891
|
var PLAN_STATUSES = ["Todo", "InProgress", "InReview", "Blocked", "Done"];
|
|
4756
|
-
function
|
|
4892
|
+
function isPlainObject4(value) {
|
|
4757
4893
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
4758
4894
|
}
|
|
4759
|
-
function
|
|
4895
|
+
function violation5(severity, code, message, fix) {
|
|
4760
4896
|
return { ok: false, severity, code, message, fix };
|
|
4761
4897
|
}
|
|
4762
4898
|
function todayString() {
|
|
@@ -4779,19 +4915,19 @@ function isOpenResidual(entry) {
|
|
|
4779
4915
|
}
|
|
4780
4916
|
function validateNonEmptyString3(violations, value, field, missingCode, invalidCode) {
|
|
4781
4917
|
if (value === undefined) {
|
|
4782
|
-
violations.push(
|
|
4918
|
+
violations.push(violation5("high", missingCode, `missing required field: ${field}`));
|
|
4783
4919
|
} else if (typeof value !== "string" || value.trim() === "") {
|
|
4784
|
-
violations.push(
|
|
4920
|
+
violations.push(violation5("medium", invalidCode, `${field} must be a non-empty string`));
|
|
4785
4921
|
}
|
|
4786
4922
|
}
|
|
4787
4923
|
function validatePlanRow(row) {
|
|
4788
4924
|
const violations = [];
|
|
4789
|
-
if (!
|
|
4790
|
-
return { ok: false, violations: [
|
|
4925
|
+
if (!isPlainObject4(row)) {
|
|
4926
|
+
return { ok: false, violations: [violation5("high", "status.plan-row.invalid", "plan row must be an object")] };
|
|
4791
4927
|
}
|
|
4792
4928
|
const { id, plan_id: planId, title, file, status, metadata, execution_lease } = row;
|
|
4793
4929
|
if (id === undefined && planId === undefined) {
|
|
4794
|
-
violations.push(
|
|
4930
|
+
violations.push(violation5("high", "status.plan-row.missing-id", "missing required field: id (or legacy plan_id)"));
|
|
4795
4931
|
} else {
|
|
4796
4932
|
if (id !== undefined) {
|
|
4797
4933
|
validateNonEmptyString3(violations, id, "id", "status.plan-row.missing-id", "status.plan-row.invalid-id");
|
|
@@ -4800,24 +4936,24 @@ function validatePlanRow(row) {
|
|
|
4800
4936
|
validateNonEmptyString3(violations, planId, "plan_id", "status.plan-row.missing-plan-id", "status.plan-row.invalid-plan-id");
|
|
4801
4937
|
}
|
|
4802
4938
|
if (id !== undefined && planId !== undefined && id !== planId) {
|
|
4803
|
-
violations.push(
|
|
4939
|
+
violations.push(violation5("medium", "status.plan-row.dual-id", "row has both id and plan_id with different values \u2014 write one canonical key (prefer id)"));
|
|
4804
4940
|
}
|
|
4805
4941
|
}
|
|
4806
4942
|
validateNonEmptyString3(violations, title, "title", "status.plan-row.missing-title", "status.plan-row.invalid-title");
|
|
4807
4943
|
validateNonEmptyString3(violations, file, "file", "status.plan-row.missing-file", "status.plan-row.invalid-file");
|
|
4808
4944
|
if (status === undefined) {
|
|
4809
|
-
violations.push(
|
|
4945
|
+
violations.push(violation5("high", "status.plan-row.missing-status", "missing required field: status"));
|
|
4810
4946
|
} else if (typeof status !== "string" || !PLAN_STATUSES.includes(status)) {
|
|
4811
|
-
violations.push(
|
|
4947
|
+
violations.push(violation5("medium", "status.plan-row.invalid-status", `status must be one of ${PLAN_STATUSES.join(" | ")} \u2014 got ${JSON.stringify(status)}`));
|
|
4812
4948
|
}
|
|
4813
|
-
if (metadata !== undefined && !
|
|
4814
|
-
violations.push(
|
|
4949
|
+
if (metadata !== undefined && !isPlainObject4(metadata)) {
|
|
4950
|
+
violations.push(violation5("medium", "status.plan-row.invalid-metadata", "metadata must be an object"));
|
|
4815
4951
|
}
|
|
4816
|
-
if (execution_lease !== undefined && !
|
|
4817
|
-
violations.push(
|
|
4952
|
+
if (execution_lease !== undefined && !isPlainObject4(execution_lease)) {
|
|
4953
|
+
violations.push(violation5("medium", "status.plan-row.invalid-execution-lease", "execution_lease must be an object"));
|
|
4818
4954
|
}
|
|
4819
4955
|
if (status === "Done" && execution_lease !== undefined) {
|
|
4820
|
-
violations.push(
|
|
4956
|
+
violations.push(violation5("medium", "status.plan-row.done-with-lease", 'plan status Done must not carry an execution_lease \u2014 the Done authority deletes the lease in the same complete-file update as status: "Done" (status-and-residuals.md \u00a7 Hold, release, and override)', 'delete plans[].execution_lease in the same update that sets status: "Done"'));
|
|
4821
4957
|
}
|
|
4822
4958
|
return { ok: violations.length === 0, violations };
|
|
4823
4959
|
}
|
|
@@ -4830,25 +4966,25 @@ function isHarnessRelativePath(dir) {
|
|
|
4830
4966
|
}
|
|
4831
4967
|
function validateWorkflowEntry(entry) {
|
|
4832
4968
|
const violations = [];
|
|
4833
|
-
if (!
|
|
4969
|
+
if (!isPlainObject4(entry)) {
|
|
4834
4970
|
return {
|
|
4835
4971
|
ok: false,
|
|
4836
|
-
violations: [
|
|
4972
|
+
violations: [violation5("high", "status.workflow.invalid", "workflow entry must be an object")]
|
|
4837
4973
|
};
|
|
4838
4974
|
}
|
|
4839
4975
|
validateNonEmptyString3(violations, entry.id, "id", "status.workflow.missing-id", "status.workflow.invalid-id");
|
|
4840
4976
|
if (entry.type === undefined) {
|
|
4841
|
-
violations.push(
|
|
4977
|
+
violations.push(violation5("high", "status.workflow.missing-type", "missing required field: type"));
|
|
4842
4978
|
} else if (typeof entry.type !== "string" || !WORKFLOW_LIFECYCLE_TYPES.includes(entry.type)) {
|
|
4843
|
-
violations.push(
|
|
4979
|
+
violations.push(violation5("medium", "status.workflow.invalid-type", `type must be one of ${WORKFLOW_LIFECYCLE_TYPES.join(" | ")} \u2014 got ${JSON.stringify(entry.type)}`));
|
|
4844
4980
|
}
|
|
4845
4981
|
validateNonEmptyString3(violations, entry.started_at, "started_at", "status.workflow.missing-started-at", "status.workflow.invalid-started-at");
|
|
4846
4982
|
if (entry.dir === undefined) {
|
|
4847
|
-
violations.push(
|
|
4983
|
+
violations.push(violation5("high", "status.workflow.missing-dir", "missing required field: dir"));
|
|
4848
4984
|
} else if (typeof entry.dir !== "string" || entry.dir.trim() === "") {
|
|
4849
|
-
violations.push(
|
|
4985
|
+
violations.push(violation5("medium", "status.workflow.invalid-dir", "dir must be a non-empty string"));
|
|
4850
4986
|
} else if (!isHarnessRelativePath(entry.dir)) {
|
|
4851
|
-
violations.push(
|
|
4987
|
+
violations.push(violation5("medium", "status.workflow.invalid-dir", `dir must be a harness-relative path (no absolute paths, no ".." segments) \u2014 got ${JSON.stringify(entry.dir)}`));
|
|
4852
4988
|
}
|
|
4853
4989
|
return { ok: violations.length === 0, violations };
|
|
4854
4990
|
}
|
|
@@ -4858,24 +4994,24 @@ function validateStatusV2(docOrPath, opts = {}) {
|
|
|
4858
4994
|
if (typeof docOrPath === "string") {
|
|
4859
4995
|
try {
|
|
4860
4996
|
doc = readJson(docOrPath);
|
|
4861
|
-
harnessDir =
|
|
4997
|
+
harnessDir = dirname4(resolve4(docOrPath));
|
|
4862
4998
|
} catch (error) {
|
|
4863
4999
|
return {
|
|
4864
5000
|
ok: false,
|
|
4865
|
-
violations: [
|
|
5001
|
+
violations: [violation5("high", "status.invalid-json", error.message)]
|
|
4866
5002
|
};
|
|
4867
5003
|
}
|
|
4868
5004
|
} else {
|
|
4869
5005
|
doc = docOrPath;
|
|
4870
5006
|
}
|
|
4871
|
-
if (!
|
|
4872
|
-
return { ok: false, violations: [
|
|
5007
|
+
if (!isPlainObject4(doc)) {
|
|
5008
|
+
return { ok: false, violations: [violation5("high", "status.invalid-doc", "status document must be an object")] };
|
|
4873
5009
|
}
|
|
4874
5010
|
if (doc.version !== 2) {
|
|
4875
5011
|
return {
|
|
4876
5012
|
ok: false,
|
|
4877
5013
|
violations: [
|
|
4878
|
-
|
|
5014
|
+
violation5("high", "status.migration-required", `status.json schema version 2 required \u2014 got ${JSON.stringify(doc.version)} (v1 or unknown version); run \`mstar migrate\` to convert the tree`, "run `mstar migrate`")
|
|
4879
5015
|
]
|
|
4880
5016
|
};
|
|
4881
5017
|
}
|
|
@@ -4883,7 +5019,7 @@ function validateStatusV2(docOrPath, opts = {}) {
|
|
|
4883
5019
|
return {
|
|
4884
5020
|
ok: false,
|
|
4885
5021
|
violations: [
|
|
4886
|
-
|
|
5022
|
+
violation5("high", "status.migration-required", "v1-shaped status.json (root plans[]) is not a v2 document \u2014 run `mstar migrate` to convert the tree", "run `mstar migrate`")
|
|
4887
5023
|
]
|
|
4888
5024
|
};
|
|
4889
5025
|
}
|
|
@@ -4891,27 +5027,27 @@ function validateStatusV2(docOrPath, opts = {}) {
|
|
|
4891
5027
|
return {
|
|
4892
5028
|
ok: false,
|
|
4893
5029
|
violations: [
|
|
4894
|
-
|
|
5030
|
+
violation5("high", "status.migration-required", "v1-shaped status.json (root residual_findings) is not a v2 document \u2014 run `mstar migrate` to convert the tree", "run `mstar migrate`")
|
|
4895
5031
|
]
|
|
4896
5032
|
};
|
|
4897
5033
|
}
|
|
4898
5034
|
const violations = [];
|
|
4899
5035
|
if (doc.updated_at === undefined) {
|
|
4900
|
-
violations.push(
|
|
4901
|
-
} else if (typeof doc.updated_at !== "string" || !
|
|
4902
|
-
violations.push(
|
|
5036
|
+
violations.push(violation5("high", "status.missing-updated-at", "missing required field: updated_at"));
|
|
5037
|
+
} else if (typeof doc.updated_at !== "string" || !DATE_RE2.test(doc.updated_at)) {
|
|
5038
|
+
violations.push(violation5("medium", "status.invalid-updated-at", "updated_at must be YYYY-MM-DD"));
|
|
4903
5039
|
}
|
|
4904
5040
|
if (doc.workflows === undefined) {
|
|
4905
|
-
violations.push(
|
|
5041
|
+
violations.push(violation5("high", "status.missing-workflows", "missing required field: workflows"));
|
|
4906
5042
|
} else if (!Array.isArray(doc.workflows)) {
|
|
4907
|
-
violations.push(
|
|
5043
|
+
violations.push(violation5("high", "status.invalid-workflows", "workflows must be an array"));
|
|
4908
5044
|
} else {
|
|
4909
5045
|
const seen = new Set;
|
|
4910
5046
|
for (const entry of doc.workflows) {
|
|
4911
5047
|
violations.push(...validateWorkflowEntry(entry).violations);
|
|
4912
|
-
if (
|
|
5048
|
+
if (isPlainObject4(entry) && typeof entry.id === "string") {
|
|
4913
5049
|
if (seen.has(entry.id)) {
|
|
4914
|
-
violations.push(
|
|
5050
|
+
violations.push(violation5("medium", "status.workflow.duplicate-id", `duplicate workflow id in workflows[]: ${JSON.stringify(entry.id)}`));
|
|
4915
5051
|
}
|
|
4916
5052
|
seen.add(entry.id);
|
|
4917
5053
|
}
|
|
@@ -4920,40 +5056,40 @@ function validateStatusV2(docOrPath, opts = {}) {
|
|
|
4920
5056
|
if (harnessDir !== undefined && Array.isArray(doc.workflows)) {
|
|
4921
5057
|
let realHarnessDir = null;
|
|
4922
5058
|
try {
|
|
4923
|
-
realHarnessDir =
|
|
5059
|
+
realHarnessDir = realpathSync(harnessDir);
|
|
4924
5060
|
} catch {}
|
|
4925
5061
|
for (const entry of doc.workflows) {
|
|
4926
|
-
if (!
|
|
5062
|
+
if (!isPlainObject4(entry) || typeof entry.dir !== "string")
|
|
4927
5063
|
continue;
|
|
4928
5064
|
const relSnapshot = join6(entry.dir, WORKFLOW_SNAPSHOT_FILE);
|
|
4929
5065
|
const snapshotPath = join6(harnessDir, relSnapshot);
|
|
4930
5066
|
const label = typeof entry.id === "string" ? entry.id : relSnapshot;
|
|
4931
5067
|
let physical;
|
|
4932
5068
|
try {
|
|
4933
|
-
physical =
|
|
5069
|
+
physical = realpathSync(snapshotPath);
|
|
4934
5070
|
} catch {
|
|
4935
|
-
violations.push(
|
|
5071
|
+
violations.push(violation5("high", "status.workflow.snapshot-missing", `workflows[] lists ${JSON.stringify(label)} but its snapshot does not exist at ${JSON.stringify(relSnapshot)} \u2014 the root holds active lifecycles only; unregister the id when its snapshot is removed`));
|
|
4936
5072
|
continue;
|
|
4937
5073
|
}
|
|
4938
5074
|
if (realHarnessDir !== null && physical !== realHarnessDir && !physical.startsWith(`${realHarnessDir}${sep}`)) {
|
|
4939
|
-
violations.push(
|
|
5075
|
+
violations.push(violation5("high", "status.workflow.snapshot-outside-harness", `workflows[] lists ${JSON.stringify(label)} but its snapshot resolves outside the harness dir (${JSON.stringify(physical)}) \u2014 symlinked snapshot paths are rejected; the snapshot must physically live under ${JSON.stringify(harnessDir)}`));
|
|
4940
5076
|
continue;
|
|
4941
5077
|
}
|
|
4942
5078
|
let snapshot;
|
|
4943
5079
|
try {
|
|
4944
5080
|
snapshot = readJson(snapshotPath);
|
|
4945
5081
|
} catch (error) {
|
|
4946
|
-
violations.push(
|
|
5082
|
+
violations.push(violation5("high", "status.workflow.snapshot-invalid", `snapshot at ${JSON.stringify(relSnapshot)} is not valid JSON: ${error.message}`));
|
|
4947
5083
|
continue;
|
|
4948
5084
|
}
|
|
4949
5085
|
if (typeof snapshot.status === "string" && WORKFLOW_TERMINAL_STATUSES.includes(snapshot.status)) {
|
|
4950
|
-
violations.push(
|
|
5086
|
+
violations.push(violation5("high", "status.workflow.terminal-listed", `workflows[] lists ${JSON.stringify(label)} whose snapshot status is terminal (${snapshot.status}) \u2014 removal-at-terminal: terminal writers unregister AFTER the snapshot write`));
|
|
4951
5087
|
}
|
|
4952
5088
|
if (typeof entry.type === "string" && typeof snapshot.type === "string" && entry.type !== snapshot.type) {
|
|
4953
|
-
violations.push(
|
|
5089
|
+
violations.push(violation5("medium", "status.workflow.mismatched-type", `workflows[] entry ${JSON.stringify(label)} type ${JSON.stringify(entry.type)} does not match its snapshot type ${JSON.stringify(snapshot.type)} \u2014 the root entry mirrors the snapshot; align them`));
|
|
4954
5090
|
}
|
|
4955
5091
|
if (typeof entry.started_at === "string" && typeof snapshot.started_at === "string" && entry.started_at !== snapshot.started_at) {
|
|
4956
|
-
violations.push(
|
|
5092
|
+
violations.push(violation5("medium", "status.workflow.mismatched-started-at", `workflows[] entry ${JSON.stringify(label)} started_at ${JSON.stringify(entry.started_at)} does not match its snapshot started_at ${JSON.stringify(snapshot.started_at)} \u2014 workflow ${JSON.stringify(label)} collided with another writer (e.g. a concurrent/re-run \`audit promote\` with the same workflow id rewrote the snapshot); the root entry mirrors the snapshot \u2014 align them or remove the colliding workflow`));
|
|
4957
5093
|
}
|
|
4958
5094
|
}
|
|
4959
5095
|
}
|
|
@@ -4961,7 +5097,7 @@ function validateStatusV2(docOrPath, opts = {}) {
|
|
|
4961
5097
|
}
|
|
4962
5098
|
var validateStatus = validateStatusV2;
|
|
4963
5099
|
function registerWorkflowEntryLocked(statusPath, entry) {
|
|
4964
|
-
const harnessDir =
|
|
5100
|
+
const harnessDir = dirname4(statusPath);
|
|
4965
5101
|
const current = readJson(statusPath);
|
|
4966
5102
|
const fresh = Object.keys(current).length === 0;
|
|
4967
5103
|
const doc = fresh ? { version: 2, updated_at: todayString(), workflows: [] } : current;
|
|
@@ -4982,15 +5118,336 @@ function registerWorkflowEntryLocked(statusPath, entry) {
|
|
|
4982
5118
|
writeJson(statusPath, doc);
|
|
4983
5119
|
return doc;
|
|
4984
5120
|
}
|
|
4985
|
-
var
|
|
4986
|
-
|
|
4987
|
-
|
|
4988
|
-
|
|
4989
|
-
|
|
4990
|
-
|
|
4991
|
-
return Number.isFinite(parsed) && parsed > 0 ? parsed : DEFAULT_PROBE_TIMEOUT_MS;
|
|
5121
|
+
var PROJECT_ROADMAP_FILE = "roadmap.md";
|
|
5122
|
+
var PROJECT_REGISTER_FILE = "residuals.json";
|
|
5123
|
+
var _DEFAULT_PROJECT = "_default";
|
|
5124
|
+
var ROLLUP_FIELDS = ["total_open", "by_severity", "by_target", "by_plan"];
|
|
5125
|
+
function isPlainObject5(value) {
|
|
5126
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
4992
5127
|
}
|
|
4993
|
-
function
|
|
5128
|
+
function violation6(severity, code, message, fix) {
|
|
5129
|
+
return { ok: false, severity, code, message, fix };
|
|
5130
|
+
}
|
|
5131
|
+
function findingsCleanupGate(register, planId, opts) {
|
|
5132
|
+
const mode = opts?.mode ?? "allow-residual";
|
|
5133
|
+
const violations = [];
|
|
5134
|
+
const entries = isPlainObject5(register.entries) ? register.entries[planId] : undefined;
|
|
5135
|
+
if (entries === undefined) {
|
|
5136
|
+
return { ok: true, violations };
|
|
5137
|
+
}
|
|
5138
|
+
if (!Array.isArray(entries)) {
|
|
5139
|
+
violations.push(violation6("high", "project.register.invalid-entry-list", `entries[${JSON.stringify(planId)}] must be an array of residual entries (one entry per residual; v1 multi-finding semantics)`));
|
|
5140
|
+
return { ok: false, violations };
|
|
5141
|
+
}
|
|
5142
|
+
if (entries.length === 0) {
|
|
5143
|
+
return { ok: true, violations };
|
|
5144
|
+
}
|
|
5145
|
+
for (const entry of entries) {
|
|
5146
|
+
if (!isOpenResidual(entry))
|
|
5147
|
+
continue;
|
|
5148
|
+
const id = typeof entry.id === "string" ? entry.id : "<unnamed>";
|
|
5149
|
+
const label = `R#${id}`;
|
|
5150
|
+
if (mode === "zero-residual") {
|
|
5151
|
+
if (entry.severity === "nit") {
|
|
5152
|
+
violations.push(violation6("medium", "findings.zero-residual-nit", `${label}: style-only nits must be fixed in-session or dropped \u2014 never left open under zero-residual`));
|
|
5153
|
+
} else if (entry.decision === "risk-accepted" || entry.lifecycle === "waived") {
|
|
5154
|
+
violations.push(violation6("medium", "findings.zero-residual-risk-accepted", `${label}: waived/risk-accepted findings must be closed/archived, not left open under zero-residual`));
|
|
5155
|
+
} else if (entry.decision === "defer") {
|
|
5156
|
+
if (typeof entry.target !== "string" || entry.target.trim() === "") {
|
|
5157
|
+
violations.push(violation6("medium", "findings.zero-residual-defer-no-target", `${label}: blocker-defer requires a target (next iteration/milestone) under zero-residual`));
|
|
5158
|
+
}
|
|
5159
|
+
} else {
|
|
5160
|
+
violations.push(violation6("medium", "findings.zero-residual-open-fixable", `${label}: fixable finding must not remain open under zero-residual \u2014 fix now or convert to a blocker-defer`));
|
|
5161
|
+
}
|
|
5162
|
+
} else if (normalizeSeverity(entry.severity) === "critical") {
|
|
5163
|
+
violations.push(violation6("high", "findings.allow-residual-critical", `${label}: unresolved critical blocks Approve with residuals`));
|
|
5164
|
+
}
|
|
5165
|
+
}
|
|
5166
|
+
return { ok: violations.length === 0, violations };
|
|
5167
|
+
}
|
|
5168
|
+
function groupCount(values) {
|
|
5169
|
+
const counts = new Map;
|
|
5170
|
+
for (const value of values) {
|
|
5171
|
+
const key = typeof value === "string" ? value : String(value);
|
|
5172
|
+
counts.set(key, (counts.get(key) ?? 0) + 1);
|
|
5173
|
+
}
|
|
5174
|
+
return Object.fromEntries([...counts.entries()].sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0));
|
|
5175
|
+
}
|
|
5176
|
+
function techDebtRollup(projectDir) {
|
|
5177
|
+
const items = [];
|
|
5178
|
+
let entries;
|
|
5179
|
+
try {
|
|
5180
|
+
entries = readdirSync3(projectDir, { withFileTypes: true });
|
|
5181
|
+
} catch {
|
|
5182
|
+
entries = [];
|
|
5183
|
+
}
|
|
5184
|
+
for (const project of entries) {
|
|
5185
|
+
if (!project.isDirectory())
|
|
5186
|
+
continue;
|
|
5187
|
+
const registerPath = join7(projectDir, project.name, PROJECT_REGISTER_FILE);
|
|
5188
|
+
if (!existsSync4(registerPath))
|
|
5189
|
+
continue;
|
|
5190
|
+
let register;
|
|
5191
|
+
try {
|
|
5192
|
+
register = readJson(registerPath);
|
|
5193
|
+
} catch {
|
|
5194
|
+
continue;
|
|
5195
|
+
}
|
|
5196
|
+
if (!isPlainObject5(register) || !isPlainObject5(register.entries))
|
|
5197
|
+
continue;
|
|
5198
|
+
for (const [plan, planEntries] of Object.entries(register.entries)) {
|
|
5199
|
+
if (!Array.isArray(planEntries))
|
|
5200
|
+
continue;
|
|
5201
|
+
for (const entry of planEntries) {
|
|
5202
|
+
if (!isPlainObject5(entry) || !isOpenResidual(entry))
|
|
5203
|
+
continue;
|
|
5204
|
+
items.push({ plan, entry });
|
|
5205
|
+
}
|
|
5206
|
+
}
|
|
5207
|
+
}
|
|
5208
|
+
const bySeverity = {};
|
|
5209
|
+
for (const severity of SEVERITY_ORDER) {
|
|
5210
|
+
bySeverity[severity] = items.filter(({ entry }) => normalizeSeverity(entry.severity) === severity).length;
|
|
5211
|
+
}
|
|
5212
|
+
const computed = {
|
|
5213
|
+
total_open: items.length,
|
|
5214
|
+
by_severity: bySeverity,
|
|
5215
|
+
by_target: groupCount(items.map(({ entry }) => entry.target ?? "unspecified")),
|
|
5216
|
+
by_plan: groupCount(items.map(({ plan }) => plan))
|
|
5217
|
+
};
|
|
5218
|
+
const stored = null;
|
|
5219
|
+
const checks = ROLLUP_FIELDS.map((field) => ({ field, status: "DRIFT" }));
|
|
5220
|
+
const overall = "DRIFT";
|
|
5221
|
+
return { computed, stored, checks, overall };
|
|
5222
|
+
}
|
|
5223
|
+
function resolveHarnessDir(startDir = process.cwd(), opts = {}) {
|
|
5224
|
+
const start = resolve5(startDir);
|
|
5225
|
+
const explicit = opts.harnessDir ?? process.env.MSTAR_HARNESS_DIR;
|
|
5226
|
+
if (explicit)
|
|
5227
|
+
return resolve5(start, explicit);
|
|
5228
|
+
const boundary = resolve5(start, opts.workspaceRoot ?? defaultWorkspaceRoot(start));
|
|
5229
|
+
const rc = loadMstarc(start, boundary);
|
|
5230
|
+
if (rc !== null && rc.config.harnessDir)
|
|
5231
|
+
return resolve5(rc.dir, rc.config.harnessDir);
|
|
5232
|
+
let dir = start;
|
|
5233
|
+
for (;; ) {
|
|
5234
|
+
if (!isAtOrBelow2(dir, boundary))
|
|
5235
|
+
return null;
|
|
5236
|
+
for (const candidate of [join8(dir, ".mstar"), join8(dir, ".agents"), join8(dir, ".plans"), join8(dir, "plans")]) {
|
|
5237
|
+
if (isDirectory(candidate))
|
|
5238
|
+
return candidate;
|
|
5239
|
+
}
|
|
5240
|
+
if (dir === boundary)
|
|
5241
|
+
return null;
|
|
5242
|
+
const parent = dirname5(dir);
|
|
5243
|
+
if (parent === dir)
|
|
5244
|
+
return null;
|
|
5245
|
+
dir = parent;
|
|
5246
|
+
}
|
|
5247
|
+
}
|
|
5248
|
+
function defaultWorkspaceRoot(startDir) {
|
|
5249
|
+
try {
|
|
5250
|
+
const cdup = execFileSync("git", ["rev-parse", "--show-cdup"], {
|
|
5251
|
+
cwd: startDir,
|
|
5252
|
+
encoding: "utf8",
|
|
5253
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
5254
|
+
}).trim();
|
|
5255
|
+
if (!cdup)
|
|
5256
|
+
return startDir;
|
|
5257
|
+
let boundary = startDir;
|
|
5258
|
+
for (const segment of cdup.split(/[\\/]/)) {
|
|
5259
|
+
if (segment && segment !== ".")
|
|
5260
|
+
boundary = dirname5(boundary);
|
|
5261
|
+
}
|
|
5262
|
+
return resolve5(boundary);
|
|
5263
|
+
} catch {}
|
|
5264
|
+
return startDir;
|
|
5265
|
+
}
|
|
5266
|
+
function isAtOrBelow2(dir, root) {
|
|
5267
|
+
const rel = relative2(root, dir);
|
|
5268
|
+
return rel === "" || !rel.startsWith("..") && !isAbsolute3(rel);
|
|
5269
|
+
}
|
|
5270
|
+
function mstarcDirOverride(harnessDir, key) {
|
|
5271
|
+
const dir = resolve5(harnessDir);
|
|
5272
|
+
const rc = loadMstarc(dir, dirname5(dir));
|
|
5273
|
+
const declared = rc?.config[key];
|
|
5274
|
+
return declared ? resolve5(rc.dir, declared) : null;
|
|
5275
|
+
}
|
|
5276
|
+
function resolveSpecsDir(harnessDir, opts = {}) {
|
|
5277
|
+
const declared = mstarcDirOverride(harnessDir, "specsDir");
|
|
5278
|
+
if (declared !== null) {
|
|
5279
|
+
if (opts.create !== false)
|
|
5280
|
+
mkdirSync4(declared, { recursive: true });
|
|
5281
|
+
return declared;
|
|
5282
|
+
}
|
|
5283
|
+
const harness = resolve5(harnessDir);
|
|
5284
|
+
const repoRoot = dirname5(harness);
|
|
5285
|
+
const candidates = [
|
|
5286
|
+
join8(harness, "specs"),
|
|
5287
|
+
join8(repoRoot, "docs", "specs"),
|
|
5288
|
+
join8(repoRoot, "specs"),
|
|
5289
|
+
join8(harness, "designs"),
|
|
5290
|
+
join8(repoRoot, "designs")
|
|
5291
|
+
];
|
|
5292
|
+
for (const candidate of candidates) {
|
|
5293
|
+
if (isDirectory(candidate) && hasFiles(candidate))
|
|
5294
|
+
return candidate;
|
|
5295
|
+
}
|
|
5296
|
+
const fallback = join8(harness, "specs");
|
|
5297
|
+
if (opts.create !== false)
|
|
5298
|
+
mkdirSync4(fallback, { recursive: true });
|
|
5299
|
+
return fallback;
|
|
5300
|
+
}
|
|
5301
|
+
function assertSafePathComponent(value, what) {
|
|
5302
|
+
if (value === "" || value === "." || value === ".." || !/^[A-Za-z0-9._-]+$/.test(value)) {
|
|
5303
|
+
throw new Error(`${what} must be a single safe path component ([A-Za-z0-9._-]+; not "", ".", "..", or containing "/" or "\\") \u2014 got ${JSON.stringify(value)}`);
|
|
5304
|
+
}
|
|
5305
|
+
}
|
|
5306
|
+
function resolveSddDir(harnessDir, planId) {
|
|
5307
|
+
assertSafePathComponent(planId, "planId");
|
|
5308
|
+
const base = resolve5(harnessDir);
|
|
5309
|
+
const declared = mstarcDirOverride(base, "sddDir");
|
|
5310
|
+
const sddBase = declared !== null ? declared : join8(base, "sdd");
|
|
5311
|
+
return join8(sddBase, planId);
|
|
5312
|
+
}
|
|
5313
|
+
function resolveHarnessSubdir(startDir, opts, key, fallback) {
|
|
5314
|
+
const harness = resolveHarnessDir(startDir, opts);
|
|
5315
|
+
if (harness === null) {
|
|
5316
|
+
throw new Error(`harness dir not found from ${resolve5(startDir)} \u2014 cannot resolve the ${fallback} dir (run \`mstar harness scaffold\`, pass opts.harnessDir, or set MSTAR_HARNESS_DIR)`);
|
|
5317
|
+
}
|
|
5318
|
+
const declared = mstarcDirOverride(harness, key);
|
|
5319
|
+
return declared !== null ? declared : join8(resolve5(harness), fallback);
|
|
5320
|
+
}
|
|
5321
|
+
function resolveWorkflowDir(startDir = process.cwd(), opts = {}) {
|
|
5322
|
+
return resolveHarnessSubdir(startDir, opts, "workflowDir", "workflows");
|
|
5323
|
+
}
|
|
5324
|
+
function resolveProjectDir(startDir = process.cwd(), opts = {}) {
|
|
5325
|
+
return resolveHarnessSubdir(startDir, opts, "projectDir", "projects");
|
|
5326
|
+
}
|
|
5327
|
+
var EMPTY_STATUS_TEMPLATE = {
|
|
5328
|
+
version: 2,
|
|
5329
|
+
updated_at: "1970-01-01",
|
|
5330
|
+
workflows: []
|
|
5331
|
+
};
|
|
5332
|
+
var SCAFFOLD_DIRS = ["plans", "iterations", "knowledge", "specs", "sdd"];
|
|
5333
|
+
function resolveScaffoldDirs(root) {
|
|
5334
|
+
const start = resolve5(root);
|
|
5335
|
+
const boundary = resolve5(start, defaultWorkspaceRoot(start));
|
|
5336
|
+
const rc = loadMstarc(start, boundary);
|
|
5337
|
+
const explicit = process.env.MSTAR_HARNESS_DIR;
|
|
5338
|
+
const harnessDir = explicit ? resolve5(start, explicit) : rc !== null && rc.config.harnessDir ? resolve5(rc.dir, rc.config.harnessDir) : join8(start, ".mstar");
|
|
5339
|
+
const declaredProjectDir = mstarcDirOverride(harnessDir, "projectDir");
|
|
5340
|
+
const projectDir = declaredProjectDir !== null ? declaredProjectDir : join8(harnessDir, "projects");
|
|
5341
|
+
return { harnessDir, projectDir };
|
|
5342
|
+
}
|
|
5343
|
+
var ROADMAP_TEMPLATE = `---
|
|
5344
|
+
project_id: _default
|
|
5345
|
+
title: Default Project
|
|
5346
|
+
status: active
|
|
5347
|
+
created_at: {created_at}
|
|
5348
|
+
---
|
|
5349
|
+
|
|
5350
|
+
# Roadmap
|
|
5351
|
+
|
|
5352
|
+
## Direction
|
|
5353
|
+
|
|
5354
|
+
State the project direction here.
|
|
5355
|
+
`;
|
|
5356
|
+
var EMPTY_REGISTER_TEMPLATE = {
|
|
5357
|
+
entries: {}
|
|
5358
|
+
};
|
|
5359
|
+
function scaffoldHarness(root) {
|
|
5360
|
+
const { harnessDir, projectDir } = resolveScaffoldDirs(root);
|
|
5361
|
+
for (const dir of SCAFFOLD_DIRS)
|
|
5362
|
+
mkdirSync4(join8(harnessDir, dir), { recursive: true });
|
|
5363
|
+
const statusPath = join8(harnessDir, "status.json");
|
|
5364
|
+
if (Object.keys(readJson(statusPath)).length === 0)
|
|
5365
|
+
writeJson(statusPath, EMPTY_STATUS_TEMPLATE);
|
|
5366
|
+
const defaultProjectDir = join8(projectDir, _DEFAULT_PROJECT);
|
|
5367
|
+
mkdirSync4(defaultProjectDir, { recursive: true });
|
|
5368
|
+
const roadmapPath = join8(defaultProjectDir, PROJECT_ROADMAP_FILE);
|
|
5369
|
+
if (!existsSync5(roadmapPath)) {
|
|
5370
|
+
const created = new Date().toISOString().slice(0, 10);
|
|
5371
|
+
writeFileSync3(roadmapPath, ROADMAP_TEMPLATE.replace("{created_at}", created), "utf8");
|
|
5372
|
+
}
|
|
5373
|
+
const registerPath = join8(defaultProjectDir, PROJECT_REGISTER_FILE);
|
|
5374
|
+
if (Object.keys(readJson(registerPath)).length === 0)
|
|
5375
|
+
writeJson(registerPath, EMPTY_REGISTER_TEMPLATE);
|
|
5376
|
+
return harnessDir;
|
|
5377
|
+
}
|
|
5378
|
+
var GITIGNORE_SNIPPET = `# Morning Star harness (.mstar/)
|
|
5379
|
+
# Principle: process stays local; results are shared with the team.
|
|
5380
|
+
# Default-ignore everything under .mstar/, then re-include the tracked results.
|
|
5381
|
+
.mstar/**
|
|
5382
|
+
!.mstar/AGENTS.md
|
|
5383
|
+
!.mstar/knowledge/
|
|
5384
|
+
!.mstar/knowledge/**
|
|
5385
|
+
!.mstar/specs/
|
|
5386
|
+
!.mstar/specs/**
|
|
5387
|
+
# .mstarc \u2014 repo-local harness config (may declare [config] harness_dir=<name>)
|
|
5388
|
+
.mstarc
|
|
5389
|
+
`;
|
|
5390
|
+
var GITIGNORE_SNIPPET_AGENTS = `# Morning Star harness (.agents/) \u2014 legacy
|
|
5391
|
+
# Default-ignore everything under .agents/, then re-include the tracked results.
|
|
5392
|
+
.agents/**
|
|
5393
|
+
!.agents/AGENTS.md
|
|
5394
|
+
!.agents/knowledge/
|
|
5395
|
+
!.agents/knowledge/**
|
|
5396
|
+
!.agents/specs/
|
|
5397
|
+
!.agents/specs/**
|
|
5398
|
+
`;
|
|
5399
|
+
var GITIGNORE_PROCESS_ENTRIES = GITIGNORE_SNIPPET.split(`
|
|
5400
|
+
`).filter((line) => line.startsWith(".mstar/") || line.startsWith("!.mstar/")).map((line) => line.trim());
|
|
5401
|
+
var GITIGNORE_PROCESS_ENTRIES_AGENTS = GITIGNORE_SNIPPET_AGENTS.split(`
|
|
5402
|
+
`).filter((line) => line.startsWith(".agents/") || line.startsWith("!.agents/")).map((line) => line.trim());
|
|
5403
|
+
function emitGitignoreSnippet(kind) {
|
|
5404
|
+
if (kind === "agents")
|
|
5405
|
+
return GITIGNORE_SNIPPET_AGENTS;
|
|
5406
|
+
if (kind === "mstar")
|
|
5407
|
+
return GITIGNORE_SNIPPET;
|
|
5408
|
+
return `${GITIGNORE_SNIPPET}${GITIGNORE_SNIPPET_AGENTS}`;
|
|
5409
|
+
}
|
|
5410
|
+
function detectHarnessKind(harnessDir) {
|
|
5411
|
+
if (!harnessDir)
|
|
5412
|
+
return null;
|
|
5413
|
+
const name = basename2(resolve5(harnessDir));
|
|
5414
|
+
if (name === ".mstar")
|
|
5415
|
+
return "mstar";
|
|
5416
|
+
if (name === ".agents")
|
|
5417
|
+
return "agents";
|
|
5418
|
+
return null;
|
|
5419
|
+
}
|
|
5420
|
+
function isDirectory(dir) {
|
|
5421
|
+
try {
|
|
5422
|
+
return statSync3(dir).isDirectory();
|
|
5423
|
+
} catch {
|
|
5424
|
+
return false;
|
|
5425
|
+
}
|
|
5426
|
+
}
|
|
5427
|
+
function hasFiles(dir) {
|
|
5428
|
+
try {
|
|
5429
|
+
for (const entry of readdirSync4(dir, { withFileTypes: true })) {
|
|
5430
|
+
if (entry.isDirectory()) {
|
|
5431
|
+
if (hasFiles(join8(dir, entry.name)))
|
|
5432
|
+
return true;
|
|
5433
|
+
} else if (entry.isFile()) {
|
|
5434
|
+
return true;
|
|
5435
|
+
}
|
|
5436
|
+
}
|
|
5437
|
+
return false;
|
|
5438
|
+
} catch {
|
|
5439
|
+
return false;
|
|
5440
|
+
}
|
|
5441
|
+
}
|
|
5442
|
+
var DEFAULT_PROBE_TIMEOUT_MS = 1e4;
|
|
5443
|
+
function probeTimeoutMs() {
|
|
5444
|
+
const raw = process.env.MSTAR_GIT_PROBE_TIMEOUT_MS;
|
|
5445
|
+
if (raw === undefined || raw.trim() === "")
|
|
5446
|
+
return DEFAULT_PROBE_TIMEOUT_MS;
|
|
5447
|
+
const parsed = Number(raw);
|
|
5448
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : DEFAULT_PROBE_TIMEOUT_MS;
|
|
5449
|
+
}
|
|
5450
|
+
function violation7(severity, code, message, fix) {
|
|
4994
5451
|
return { ok: false, severity, code, message, fix };
|
|
4995
5452
|
}
|
|
4996
5453
|
function gate(violations) {
|
|
@@ -5024,25 +5481,25 @@ function l1PreDispatchCheck(input, opts = {}) {
|
|
|
5024
5481
|
const violations = [];
|
|
5025
5482
|
const { controlWorktreePath, leaseWorktreePath, leaseWorkingBranch, planId } = input;
|
|
5026
5483
|
if (controlWorktreePath.trim() === "") {
|
|
5027
|
-
violations.push(
|
|
5484
|
+
violations.push(violation7("high", "worktree.l1.control-missing", "metadata.control_worktree_path is not recorded \u2014 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"));
|
|
5028
5485
|
}
|
|
5029
5486
|
if (leaseWorktreePath.trim() === "") {
|
|
5030
|
-
violations.push(
|
|
5487
|
+
violations.push(violation7("high", "worktree.l1.lease-missing", `execution_lease.worktree_path is empty for plan "${planId}" \u2014 no verified execution_lease to dispatch against`, "claim the execution_lease with an absolute feature worktree path before dispatch"));
|
|
5031
5488
|
}
|
|
5032
5489
|
if (leaseWorkingBranch.trim() === "") {
|
|
5033
|
-
violations.push(
|
|
5490
|
+
violations.push(violation7("high", "worktree.l1.lease-branch-missing", `execution_lease.working_branch is empty for plan "${planId}"`, "record the lease working_branch before dispatch"));
|
|
5034
5491
|
}
|
|
5035
5492
|
if (controlWorktreePath !== "" && leaseWorktreePath !== "" && resolve6(controlWorktreePath) === resolve6(leaseWorktreePath)) {
|
|
5036
|
-
violations.push(
|
|
5493
|
+
violations.push(violation7("critical", "worktree.l1.lease-equals-control", `execution_lease.worktree_path "${leaseWorktreePath}" equals metadata.control_worktree_path \u2014 the feature worktree MUST differ from the control worktree (L1 isolation; product edits never land in the control checkout)`, "use a distinct feature worktree for the plan (git worktree add <path> <branch>) and update the lease"));
|
|
5037
5494
|
}
|
|
5038
|
-
if (leaseWorktreePath !== "" && !
|
|
5039
|
-
violations.push(
|
|
5495
|
+
if (leaseWorktreePath !== "" && !existsSync6(leaseWorktreePath)) {
|
|
5496
|
+
violations.push(violation7("high", "worktree.l1.feature-missing", `feature worktree directory "${leaseWorktreePath}" does not exist for plan "${planId}"`, `create it before dispatch: git worktree add ${leaseWorktreePath} <working-branch>`));
|
|
5040
5497
|
} else if (leaseWorktreePath !== "" && leaseWorkingBranch !== "") {
|
|
5041
5498
|
const probe = probeBranch(leaseWorktreePath, opts);
|
|
5042
5499
|
if ("error" in probe) {
|
|
5043
|
-
violations.push(
|
|
5500
|
+
violations.push(violation7("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)"));
|
|
5044
5501
|
} else if (probe.branch !== leaseWorkingBranch) {
|
|
5045
|
-
violations.push(
|
|
5502
|
+
violations.push(violation7("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`));
|
|
5046
5503
|
}
|
|
5047
5504
|
}
|
|
5048
5505
|
return gate(violations);
|
|
@@ -5052,32 +5509,32 @@ function l2PreDispatchCheck(input, opts = {}) {
|
|
|
5052
5509
|
const tracks = input.tracks ?? [];
|
|
5053
5510
|
const seenPaths = new Set;
|
|
5054
5511
|
if (tracks.length < 1) {
|
|
5055
|
-
violations.push(
|
|
5512
|
+
violations.push(violation7("high", "worktree.l2.no-tracks", "no parallel writable tracks \u2014 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"));
|
|
5056
5513
|
}
|
|
5057
5514
|
tracks.forEach((track, index) => {
|
|
5058
5515
|
if (track.worktreePath.trim() === "" || track.workingBranch.trim() === "") {
|
|
5059
|
-
violations.push(
|
|
5516
|
+
violations.push(violation7("high", "worktree.l2.track-invalid", `track ${index + 1} is missing worktreePath and/or workingBranch`, "fill both fields for every track"));
|
|
5060
5517
|
return;
|
|
5061
5518
|
}
|
|
5062
5519
|
if (!isAbsolute4(track.worktreePath)) {
|
|
5063
|
-
violations.push(
|
|
5520
|
+
violations.push(violation7("high", "worktree.l2.track-path-relative", `track ${index + 1} worktreePath "${track.worktreePath}" is not an absolute path \u2014 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>)`));
|
|
5064
5521
|
return;
|
|
5065
5522
|
}
|
|
5066
5523
|
const normalized = resolve6(track.worktreePath);
|
|
5067
5524
|
if (seenPaths.has(normalized)) {
|
|
5068
|
-
violations.push(
|
|
5525
|
+
violations.push(violation7("high", "worktree.l2.track-path-collision", `duplicate worktreePath "${track.worktreePath}" across parallel tracks \u2014 L2 parallel-writable isolation requires a distinct absolute Worktree path per track (N parallel invokes \u2260 isolation)`, "give every parallel track its own git worktree checkout"));
|
|
5069
5526
|
return;
|
|
5070
5527
|
}
|
|
5071
5528
|
seenPaths.add(normalized);
|
|
5072
|
-
if (!
|
|
5073
|
-
violations.push(
|
|
5529
|
+
if (!existsSync6(track.worktreePath)) {
|
|
5530
|
+
violations.push(violation7("high", "worktree.l2.track-missing", `track worktree directory "${track.worktreePath}" does not exist`, `create it before dispatch: git worktree add ${track.worktreePath} ${track.workingBranch}`));
|
|
5074
5531
|
return;
|
|
5075
5532
|
}
|
|
5076
5533
|
const probe = probeBranch(track.worktreePath, opts);
|
|
5077
5534
|
if ("error" in probe) {
|
|
5078
|
-
violations.push(
|
|
5535
|
+
violations.push(violation7("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)"));
|
|
5079
5536
|
} else if (probe.branch !== track.workingBranch) {
|
|
5080
|
-
violations.push(
|
|
5537
|
+
violations.push(violation7("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`));
|
|
5081
5538
|
}
|
|
5082
5539
|
});
|
|
5083
5540
|
return gate(violations);
|
|
@@ -5093,7 +5550,7 @@ function assertQcAlignment(assignments) {
|
|
|
5093
5550
|
for (const { key, label } of QC_ALIGNMENT_FIELDS) {
|
|
5094
5551
|
const distinct = [...new Set(list.map((a) => a[key]))];
|
|
5095
5552
|
if (distinct.length > 1) {
|
|
5096
|
-
violations.push(
|
|
5553
|
+
violations.push(violation7("high", "qc.alignment.mismatch", `QC/QA alignment field "${label}" is not byte-identical across ${list.length} assignments: ${distinct.map((v) => `"${v}"`).join(" vs ")}`, `copy the same ${label} value verbatim into every QC tri and QA Assignment`));
|
|
5097
5554
|
}
|
|
5098
5555
|
}
|
|
5099
5556
|
return gate(violations);
|
|
@@ -5134,14 +5591,14 @@ function gitOut(cwd, args) {
|
|
|
5134
5591
|
}
|
|
5135
5592
|
}
|
|
5136
5593
|
function probeHarnessWithStatus(root) {
|
|
5137
|
-
if (isFile2(
|
|
5138
|
-
return
|
|
5139
|
-
if (isFile2(
|
|
5140
|
-
return
|
|
5141
|
-
if (hasWorkflowSnapshot(
|
|
5142
|
-
return
|
|
5143
|
-
if (hasWorkflowSnapshot(
|
|
5144
|
-
return
|
|
5594
|
+
if (isFile2(join9(root, ".mstar", "status.json")))
|
|
5595
|
+
return join9(root, ".mstar");
|
|
5596
|
+
if (isFile2(join9(root, ".agents", "status.json")))
|
|
5597
|
+
return join9(root, ".agents");
|
|
5598
|
+
if (hasWorkflowSnapshot(join9(root, ".mstar")))
|
|
5599
|
+
return join9(root, ".mstar");
|
|
5600
|
+
if (hasWorkflowSnapshot(join9(root, ".agents")))
|
|
5601
|
+
return join9(root, ".agents");
|
|
5145
5602
|
return null;
|
|
5146
5603
|
}
|
|
5147
5604
|
function hasWorkflowSnapshot(harnessDir) {
|
|
@@ -5149,13 +5606,13 @@ function hasWorkflowSnapshot(harnessDir) {
|
|
|
5149
5606
|
try {
|
|
5150
5607
|
workflowsDir = resolveWorkflowDir(harnessDir, { harnessDir });
|
|
5151
5608
|
} catch {
|
|
5152
|
-
workflowsDir =
|
|
5609
|
+
workflowsDir = join9(harnessDir, "workflows");
|
|
5153
5610
|
}
|
|
5154
5611
|
if (!isDirectory2(workflowsDir))
|
|
5155
5612
|
return false;
|
|
5156
5613
|
try {
|
|
5157
|
-
for (const entry of
|
|
5158
|
-
if (entry.isDirectory() && isFile2(
|
|
5614
|
+
for (const entry of readdirSync5(workflowsDir, { withFileTypes: true })) {
|
|
5615
|
+
if (entry.isDirectory() && isFile2(join9(workflowsDir, entry.name, "snapshot.json")))
|
|
5159
5616
|
return true;
|
|
5160
5617
|
}
|
|
5161
5618
|
} catch {
|
|
@@ -5168,14 +5625,14 @@ function isLinkedWorktree(root) {
|
|
|
5168
5625
|
const commonRaw = gitOut(root, ["rev-parse", "--git-common-dir"]);
|
|
5169
5626
|
if (gitDirRaw === null || commonRaw === null)
|
|
5170
5627
|
return false;
|
|
5171
|
-
const gitDir = isAbsolute5(gitDirRaw) ? gitDirRaw :
|
|
5172
|
-
const common2 = isAbsolute5(commonRaw) ? commonRaw :
|
|
5628
|
+
const gitDir = isAbsolute5(gitDirRaw) ? gitDirRaw : join9(root, gitDirRaw);
|
|
5629
|
+
const common2 = isAbsolute5(commonRaw) ? commonRaw : join9(root, commonRaw);
|
|
5173
5630
|
if (gitDir.includes("/.git/worktrees/") || gitDir.includes("/worktrees/"))
|
|
5174
5631
|
return true;
|
|
5175
5632
|
try {
|
|
5176
5633
|
const gdParent = realpathSync3(dirname6(gitDir));
|
|
5177
5634
|
const cmAbs = realpathSync3(common2);
|
|
5178
|
-
return
|
|
5635
|
+
return join9(gdParent, basename3(gitDir)) !== cmAbs && gitDir !== cmAbs;
|
|
5179
5636
|
} catch {
|
|
5180
5637
|
return false;
|
|
5181
5638
|
}
|
|
@@ -5209,25 +5666,25 @@ function sddWorkspace(planId, opts = {}) {
|
|
|
5209
5666
|
harnessDir = resolve7(root, harnessOverride);
|
|
5210
5667
|
} else {
|
|
5211
5668
|
const rc = findMstarc(root, root);
|
|
5212
|
-
const rcHarnessDir = rc !== null ? parseMstarc(
|
|
5669
|
+
const rcHarnessDir = rc !== null ? parseMstarc(readFileSync7(rc, "utf8")).harnessDir : undefined;
|
|
5213
5670
|
if (rcHarnessDir) {
|
|
5214
5671
|
harnessDir = resolve7(rc !== null ? dirname6(rc) : root, rcHarnessDir);
|
|
5215
5672
|
} else {
|
|
5216
5673
|
const probed = probeHarnessWithStatus(root);
|
|
5217
5674
|
if (probed) {
|
|
5218
5675
|
harnessDir = probed;
|
|
5219
|
-
} else if (isDirectory2(
|
|
5220
|
-
harnessDir =
|
|
5221
|
-
} else if (isDirectory2(
|
|
5222
|
-
harnessDir =
|
|
5676
|
+
} else if (isDirectory2(join9(root, ".mstar"))) {
|
|
5677
|
+
harnessDir = join9(root, ".mstar");
|
|
5678
|
+
} else if (isDirectory2(join9(root, ".agents"))) {
|
|
5679
|
+
harnessDir = join9(root, ".agents");
|
|
5223
5680
|
} else {
|
|
5224
|
-
harnessDir =
|
|
5681
|
+
harnessDir = join9(root, ".mstar");
|
|
5225
5682
|
}
|
|
5226
5683
|
}
|
|
5227
5684
|
}
|
|
5228
5685
|
const sddDir = resolveSddDir(harnessDir, planId);
|
|
5229
5686
|
mkdirSync5(sddDir, { recursive: true });
|
|
5230
|
-
|
|
5687
|
+
writeFileSync4(join9(sddDir, ".gitignore"), `*
|
|
5231
5688
|
`);
|
|
5232
5689
|
return realpathSync3(sddDir);
|
|
5233
5690
|
}
|
|
@@ -5237,480 +5694,92 @@ function taskBrief(planFile, taskN, outFile, opts = {}) {
|
|
|
5237
5694
|
}
|
|
5238
5695
|
let content;
|
|
5239
5696
|
try {
|
|
5240
|
-
content =
|
|
5697
|
+
content = readFileSync7(planFile, "utf8");
|
|
5241
5698
|
} catch {
|
|
5242
5699
|
throw new SddScriptError(`no such plan file: ${planFile}`, 2);
|
|
5243
5700
|
}
|
|
5244
5701
|
let out;
|
|
5245
5702
|
if (outFile) {
|
|
5246
|
-
out = outFile;
|
|
5247
|
-
} else {
|
|
5248
|
-
const sddDir = opts.sddDir ?? process.env.SDD_DIR;
|
|
5249
|
-
if (!sddDir) {
|
|
5250
|
-
throw new SddScriptError("mstar sdd task-brief: set SDD_DIR or pass OUTFILE (run mstar sdd workspace PLAN_ID first)", 2);
|
|
5251
|
-
}
|
|
5252
|
-
mkdirSync5(sddDir, { recursive: true });
|
|
5253
|
-
out = join7(sddDir, `task-${taskN}-brief.md`);
|
|
5254
|
-
}
|
|
5255
|
-
const records = content.endsWith(`
|
|
5256
|
-
`) ? content.split(`
|
|
5257
|
-
`).slice(0, -1) : content.split(`
|
|
5258
|
-
`);
|
|
5259
|
-
const headingRe = /^#+[ \t]+Task[ \t]+[0-9]+/;
|
|
5260
|
-
const targetRe = new RegExp(`^#+[ ]+Task[ ]+${taskN}([^0-9]|$)`);
|
|
5261
|
-
let infence = false;
|
|
5262
|
-
let intask = false;
|
|
5263
|
-
const printed = [];
|
|
5264
|
-
for (const line of records) {
|
|
5265
|
-
if (/^```/.test(line))
|
|
5266
|
-
infence = !infence;
|
|
5267
|
-
if (!infence && headingRe.test(line))
|
|
5268
|
-
intask = targetRe.test(line);
|
|
5269
|
-
if (intask)
|
|
5270
|
-
printed.push(line);
|
|
5271
|
-
}
|
|
5272
|
-
const output = printed.length > 0 ? `${printed.join(`
|
|
5273
|
-
`)}
|
|
5274
|
-
` : "";
|
|
5275
|
-
writeFileSync3(out, output);
|
|
5276
|
-
if (printed.length === 0) {
|
|
5277
|
-
throw new SddScriptError(`task ${taskN} not found in ${planFile} (no heading matching Task ${taskN})`, 3);
|
|
5278
|
-
}
|
|
5279
|
-
return out;
|
|
5280
|
-
}
|
|
5281
|
-
function reviewPackage(base, head, outFile, opts = {}) {
|
|
5282
|
-
if (!base || !head) {
|
|
5283
|
-
throw new SddScriptError("usage: mstar sdd review-package BASE HEAD [OUTFILE]", 2);
|
|
5284
|
-
}
|
|
5285
|
-
const cwd = opts.cwd ?? process.cwd();
|
|
5286
|
-
const verifyRef = (ref, what) => {
|
|
5287
|
-
try {
|
|
5288
|
-
execFileSync3("git", ["rev-parse", "--verify", "--quiet", ref], { cwd, stdio: ["ignore", "pipe", "pipe"] });
|
|
5289
|
-
} catch {
|
|
5290
|
-
throw new SddScriptError(`bad ${what}: ${ref}`, 2);
|
|
5291
|
-
}
|
|
5292
|
-
};
|
|
5293
|
-
verifyRef(base, "BASE");
|
|
5294
|
-
verifyRef(head, "HEAD");
|
|
5295
|
-
let out;
|
|
5296
|
-
if (outFile) {
|
|
5297
|
-
out = outFile;
|
|
5298
|
-
} else {
|
|
5299
|
-
const sddDir = opts.sddDir ?? process.env.SDD_DIR;
|
|
5300
|
-
if (!sddDir) {
|
|
5301
|
-
throw new SddScriptError("mstar sdd review-package: set SDD_DIR or pass OUTFILE", 2);
|
|
5302
|
-
}
|
|
5303
|
-
mkdirSync5(sddDir, { recursive: true });
|
|
5304
|
-
const shortBase = gitOut(cwd, ["rev-parse", "--short", base]) ?? base;
|
|
5305
|
-
const shortHead = gitOut(cwd, ["rev-parse", "--short", head]) ?? head;
|
|
5306
|
-
out = join7(sddDir, `review-${shortBase}..${shortHead}.diff`);
|
|
5307
|
-
}
|
|
5308
|
-
const run = (args) => execFileSync3("git", args, { cwd, maxBuffer: GIT_CAPTURE_MAX_BYTES });
|
|
5309
|
-
const parts = [
|
|
5310
|
-
Buffer.from(`# Review package: ${base}..${head}
|
|
5311
|
-
|
|
5312
|
-
## Commits
|
|
5313
|
-
`),
|
|
5314
|
-
run(["log", "--oneline", `${base}..${head}`]),
|
|
5315
|
-
Buffer.from(`
|
|
5316
|
-
## Files changed
|
|
5317
|
-
`),
|
|
5318
|
-
run(["diff", "--stat", `${base}..${head}`]),
|
|
5319
|
-
Buffer.from(`
|
|
5320
|
-
## Diff
|
|
5321
|
-
`),
|
|
5322
|
-
run(["diff", "-U10", `${base}..${head}`])
|
|
5323
|
-
];
|
|
5324
|
-
writeFileSync3(out, Buffer.concat(parts));
|
|
5325
|
-
return out;
|
|
5326
|
-
}
|
|
5327
|
-
var COMPASS_STATUSES = ["active", "locked", "completed"];
|
|
5328
|
-
var DATE_RE2 = /^\d{4}-\d{2}-\d{2}$/;
|
|
5329
|
-
var PLAN_STATUS_DONE = "Done";
|
|
5330
|
-
function typeName(value) {
|
|
5331
|
-
if (value === null)
|
|
5332
|
-
return "null";
|
|
5333
|
-
if (Array.isArray(value))
|
|
5334
|
-
return "array";
|
|
5335
|
-
return typeof value;
|
|
5336
|
-
}
|
|
5337
|
-
function validateCompassShape(doc) {
|
|
5338
|
-
const issues = [];
|
|
5339
|
-
const expectString = (key, opts = {}) => {
|
|
5340
|
-
const value = doc[key];
|
|
5341
|
-
if (typeof value !== "string") {
|
|
5342
|
-
issues.push({ path: [key], message: `expected string, received ${typeName(value)}` });
|
|
5343
|
-
return;
|
|
5344
|
-
}
|
|
5345
|
-
if (opts.min !== undefined && value.length < opts.min) {
|
|
5346
|
-
issues.push({ path: [key], message: `string must contain at least ${opts.min} character(s)` });
|
|
5347
|
-
return;
|
|
5348
|
-
}
|
|
5349
|
-
if (opts.regex !== undefined && !opts.regex.test(value)) {
|
|
5350
|
-
issues.push({ path: [key], message: `string must match ${opts.regex}` });
|
|
5351
|
-
}
|
|
5352
|
-
};
|
|
5353
|
-
expectString("iteration_id", { min: 1 });
|
|
5354
|
-
expectString("start_date", { regex: DATE_RE2 });
|
|
5355
|
-
const status = doc.status;
|
|
5356
|
-
if (typeof status !== "string" || !COMPASS_STATUSES.includes(status)) {
|
|
5357
|
-
issues.push({
|
|
5358
|
-
path: ["status"],
|
|
5359
|
-
message: `expected one of ${COMPASS_STATUSES.map((s) => `'${s}'`).join(" | ")}, received ${typeName(status)}`
|
|
5360
|
-
});
|
|
5361
|
-
}
|
|
5362
|
-
expectString("iteration_base_branch", { min: 1 });
|
|
5363
|
-
expectString("target_branch", { min: 1 });
|
|
5364
|
-
const plans = doc.plans;
|
|
5365
|
-
if (plans !== undefined) {
|
|
5366
|
-
if (!Array.isArray(plans)) {
|
|
5367
|
-
issues.push({ path: ["plans"], message: `expected array, received ${typeName(plans)}` });
|
|
5368
|
-
} else {
|
|
5369
|
-
plans.forEach((entry, index) => {
|
|
5370
|
-
if (typeof entry !== "string") {
|
|
5371
|
-
issues.push({ path: ["plans", index], message: `expected string, received ${typeName(entry)}` });
|
|
5372
|
-
} else if (entry.length < 1) {
|
|
5373
|
-
issues.push({ path: ["plans", index], message: "string must contain at least 1 character(s)" });
|
|
5374
|
-
}
|
|
5375
|
-
});
|
|
5376
|
-
}
|
|
5377
|
-
}
|
|
5378
|
-
const end_date = doc.end_date;
|
|
5379
|
-
if (end_date !== undefined) {
|
|
5380
|
-
if (typeof end_date !== "string") {
|
|
5381
|
-
issues.push({ path: ["end_date"], message: `expected string, received ${typeName(end_date)}` });
|
|
5382
|
-
} else if (!DATE_RE2.test(end_date)) {
|
|
5383
|
-
issues.push({ path: ["end_date"], message: `string must match ${DATE_RE2}` });
|
|
5384
|
-
}
|
|
5385
|
-
}
|
|
5386
|
-
if (issues.length > 0)
|
|
5387
|
-
return { ok: false, issues };
|
|
5388
|
-
return {
|
|
5389
|
-
ok: true,
|
|
5390
|
-
data: {
|
|
5391
|
-
iteration_id: doc.iteration_id,
|
|
5392
|
-
start_date: doc.start_date,
|
|
5393
|
-
status,
|
|
5394
|
-
iteration_base_branch: doc.iteration_base_branch,
|
|
5395
|
-
target_branch: doc.target_branch,
|
|
5396
|
-
...plans !== undefined ? { plans } : {},
|
|
5397
|
-
...end_date !== undefined ? { end_date } : {}
|
|
5398
|
-
}
|
|
5399
|
-
};
|
|
5400
|
-
}
|
|
5401
|
-
function violation6(severity, code, message, fix) {
|
|
5402
|
-
return { ok: false, severity, code, message, fix };
|
|
5403
|
-
}
|
|
5404
|
-
function isPlainObject4(value) {
|
|
5405
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
5406
|
-
}
|
|
5407
|
-
function validateCompassFrontmatter(doc) {
|
|
5408
|
-
if (!isPlainObject4(doc)) {
|
|
5409
|
-
return {
|
|
5410
|
-
ok: false,
|
|
5411
|
-
violations: [
|
|
5412
|
-
violation6("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 \u00a71.3)", "Fix the frontmatter of {ITERATION_DIR}/<iteration-id>/delivery-compass.md")
|
|
5413
|
-
]
|
|
5414
|
-
};
|
|
5415
|
-
}
|
|
5416
|
-
const parsed = validateCompassShape(doc);
|
|
5417
|
-
if (!parsed.ok) {
|
|
5418
|
-
return {
|
|
5419
|
-
ok: false,
|
|
5420
|
-
violations: parsed.issues.map((issue) => {
|
|
5421
|
-
const field = issue.path.join(".") || "(root)";
|
|
5422
|
-
return violation6("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 \u00a71.3)`);
|
|
5423
|
-
})
|
|
5424
|
-
};
|
|
5425
|
-
}
|
|
5426
|
-
const violations = [];
|
|
5427
|
-
const { status, end_date } = parsed.data;
|
|
5428
|
-
if (status === "completed" && end_date === undefined) {
|
|
5429
|
-
violations.push(violation6("high", "COMPASS_END_DATE_REQUIRED", "Compass frontmatter status is 'completed' but end_date is missing \u2014 end_date is required at iteration-close (mstar-iteration \u00a73.4, template Fields guide)", "Add `end_date: YYYY-MM-DD` to the frontmatter"));
|
|
5430
|
-
}
|
|
5431
|
-
if (status !== "completed" && end_date !== undefined) {
|
|
5432
|
-
violations.push(violation6("medium", "COMPASS_END_DATE_NOT_ALLOWED", `Compass frontmatter sets end_date while status is '${status}' \u2014 end_date is only written at iteration-close (mstar-iteration \u00a73.4)`, "Remove end_date until iteration-close"));
|
|
5433
|
-
}
|
|
5434
|
-
return { ok: violations.length === 0, violations };
|
|
5435
|
-
}
|
|
5436
|
-
function registeredPlanIds(compassDoc) {
|
|
5437
|
-
if (!Array.isArray(compassDoc.plans))
|
|
5438
|
-
return [];
|
|
5439
|
-
return compassDoc.plans.filter((plan) => typeof plan === "string" && plan.length > 0);
|
|
5440
|
-
}
|
|
5441
|
-
function findPlanRow(snapshotDoc, planId) {
|
|
5442
|
-
if (!Array.isArray(snapshotDoc.plans))
|
|
5443
|
-
return null;
|
|
5444
|
-
for (const row of snapshotDoc.plans) {
|
|
5445
|
-
if (!isPlainObject4(row))
|
|
5446
|
-
continue;
|
|
5447
|
-
const rowId = typeof row.id === "string" ? row.id : typeof row.plan_id === "string" ? row.plan_id : null;
|
|
5448
|
-
if (rowId === planId)
|
|
5449
|
-
return row;
|
|
5450
|
-
}
|
|
5451
|
-
return null;
|
|
5452
|
-
}
|
|
5453
|
-
function entryPlansAllDone(snapshotDoc, registered) {
|
|
5454
|
-
const violations = [];
|
|
5455
|
-
if (registered.length === 0) {
|
|
5456
|
-
violations.push(violation6("medium", "COMPASS_NO_PLANS", "Compass frontmatter registers no plans \u2014 the all-plans-Done transition cannot be verified (mstar-iteration \u00a71.3 / Phase transition gates)", "List the iteration's plan ids in the compass frontmatter `plans`"));
|
|
5457
|
-
return violations;
|
|
5458
|
-
}
|
|
5459
|
-
for (const planId of registered) {
|
|
5460
|
-
const row = findPlanRow(snapshotDoc, planId);
|
|
5461
|
-
if (row === null) {
|
|
5462
|
-
violations.push(violation6("high", "PLAN_NOT_IN_STATUS", `Plan '${planId}' is registered in the compass frontmatter but has no row in the workflow snapshot plans[] (mstar-iteration \u00a73.1 entry item 1)`, "Add the plan row to {HARNESS_DIR}/workflows/<id>/snapshot.json"));
|
|
5463
|
-
continue;
|
|
5464
|
-
}
|
|
5465
|
-
if (row.status !== PLAN_STATUS_DONE) {
|
|
5466
|
-
violations.push(violation6("high", "PLAN_NOT_DONE", `Plan '${planId}' status is ${JSON.stringify(row.status)} in the workflow snapshot \u2014 all compass-registered plans must be 'Done' before iteration-close (mstar-iteration \u00a73.1 entry item 1)`));
|
|
5467
|
-
}
|
|
5468
|
-
}
|
|
5469
|
-
return violations;
|
|
5470
|
-
}
|
|
5471
|
-
function entryFrontmatterComplete(compassDoc) {
|
|
5472
|
-
return validateCompassFrontmatter(compassDoc).violations;
|
|
5473
|
-
}
|
|
5474
|
-
function exitFrontmatterClosed(compassDoc) {
|
|
5475
|
-
const violations = [];
|
|
5476
|
-
if (compassDoc.status !== "completed") {
|
|
5477
|
-
violations.push(violation6("high", "EXIT_STATUS_NOT_COMPLETED", `Compass frontmatter status must be 'completed' at close exit \u2014 current: ${JSON.stringify(compassDoc.status)} (mstar-iteration \u00a73.4 / \u00a73.5 exit item 4)`));
|
|
5478
|
-
}
|
|
5479
|
-
const endDate = compassDoc.end_date;
|
|
5480
|
-
if (typeof endDate !== "string" || !DATE_RE2.test(endDate)) {
|
|
5481
|
-
violations.push(violation6("high", "EXIT_END_DATE_REQUIRED", "Compass frontmatter end_date (YYYY-MM-DD) is required when closing (mstar-iteration \u00a73.4 / \u00a73.5 exit item 4)"));
|
|
5482
|
-
}
|
|
5483
|
-
return violations;
|
|
5484
|
-
}
|
|
5485
|
-
function exitBranchCheck(opts) {
|
|
5486
|
-
const violations = [];
|
|
5487
|
-
const { currentBranch, specIntegrationBranch } = opts;
|
|
5488
|
-
if (currentBranch === undefined || specIntegrationBranch === undefined) {
|
|
5489
|
-
violations.push(violation6("medium", "EXIT_BRANCH_UNVERIFIABLE", "Cannot verify the current branch is spec_integration_branch \u2014 missing currentBranch / specIntegrationBranch probe inputs (mstar-iteration \u00a73.5 exit item 5)"));
|
|
5490
|
-
} else if (currentBranch !== specIntegrationBranch) {
|
|
5491
|
-
violations.push(violation6("high", "EXIT_BRANCH_MISMATCH", `Current branch '${currentBranch}' is not the spec_integration_branch '${specIntegrationBranch}' (mstar-iteration \u00a73.5 exit item 5)`));
|
|
5492
|
-
}
|
|
5493
|
-
return violations;
|
|
5494
|
-
}
|
|
5495
|
-
function exitPrBaseCheck(compassDoc, opts) {
|
|
5496
|
-
const violations = [];
|
|
5497
|
-
const target = compassDoc.target_branch;
|
|
5498
|
-
const { prBaseBranch } = opts;
|
|
5499
|
-
if (prBaseBranch === undefined) {
|
|
5500
|
-
violations.push(violation6("medium", "EXIT_PR_BASE_UNVERIFIABLE", "Cannot verify the PR base \u2014 missing prBaseBranch probe input (mstar-iteration \u00a73.5 exit item 6)"));
|
|
5501
|
-
} else if (typeof target !== "string" || prBaseBranch !== target) {
|
|
5502
|
-
violations.push(violation6("high", "EXIT_PR_BASE_MISMATCH", `PR base '${prBaseBranch}' must equal the compass target_branch '${String(target)}' \u2014 not an undocumented branch (mstar-iteration \u00a73.5 exit item 6)`));
|
|
5503
|
-
}
|
|
5504
|
-
return violations;
|
|
5505
|
-
}
|
|
5506
|
-
function evaluatePhaseGate(snapshotDoc, compassDoc, opts = {}) {
|
|
5507
|
-
const registered = registeredPlanIds(compassDoc);
|
|
5508
|
-
const entryViolations = [
|
|
5509
|
-
...entryPlansAllDone(snapshotDoc, registered),
|
|
5510
|
-
...entryFrontmatterComplete(compassDoc)
|
|
5511
|
-
];
|
|
5512
|
-
const exitViolations = [
|
|
5513
|
-
...exitFrontmatterClosed(compassDoc),
|
|
5514
|
-
...exitBranchCheck(opts),
|
|
5515
|
-
...exitPrBaseCheck(compassDoc, opts)
|
|
5516
|
-
];
|
|
5517
|
-
const allPlansDone = registered.length > 0 && registered.every((planId) => {
|
|
5518
|
-
const row = findPlanRow(snapshotDoc, planId);
|
|
5519
|
-
return row !== null && row.status === PLAN_STATUS_DONE;
|
|
5520
|
-
});
|
|
5521
|
-
const entry = { ok: entryViolations.length === 0, violations: entryViolations };
|
|
5522
|
-
const exit = { ok: exitViolations.length === 0, violations: exitViolations };
|
|
5523
|
-
let transition;
|
|
5524
|
-
if (!allPlansDone)
|
|
5525
|
-
transition = "phase-2-execute";
|
|
5526
|
-
else if (entry.ok && exit.ok)
|
|
5527
|
-
transition = "phase-4-pr-delivery";
|
|
5528
|
-
else
|
|
5529
|
-
transition = "phase-3-close";
|
|
5530
|
-
const gateBlocking = allPlansDone ? [...entryViolations, ...exitViolations] : [];
|
|
5531
|
-
return {
|
|
5532
|
-
transition,
|
|
5533
|
-
allPlansDone,
|
|
5534
|
-
entry,
|
|
5535
|
-
exit,
|
|
5536
|
-
ok: gateBlocking.length === 0,
|
|
5537
|
-
violations: gateBlocking
|
|
5538
|
-
};
|
|
5539
|
-
}
|
|
5540
|
-
function pushCadenceProbe(ciRunning, reviewWaveActive) {
|
|
5541
|
-
const violations = [];
|
|
5542
|
-
if (ciRunning) {
|
|
5543
|
-
violations.push(violation6("high", "PUSH_BLOCKED_CI", "CI checks are still queued/in_progress on the current head \u2014 do not push until the wave completes (mstar-iteration \u00a75.1a push gate 1)", "Wait for CI to settle, then push once with the whole local batch"));
|
|
5544
|
-
}
|
|
5545
|
-
if (reviewWaveActive) {
|
|
5546
|
-
violations.push(violation6("high", "PUSH_BLOCKED_REVIEW_WAVE", "An AI/bot review wave is still running on the current head \u2014 do not push until it settles (mstar-iteration \u00a75.1a push gate 2)", "Wait for the review wave, then push once"));
|
|
5547
|
-
}
|
|
5548
|
-
return { ok: violations.length === 0, violations };
|
|
5549
|
-
}
|
|
5550
|
-
function parseCompassFrontmatter(filePath) {
|
|
5551
|
-
return parseCompassFrontmatterText(readFileSync6(filePath, "utf8"), filePath);
|
|
5552
|
-
}
|
|
5553
|
-
function parseCompassFrontmatterText(content, filePath) {
|
|
5554
|
-
const lines = content.split(/\r?\n/);
|
|
5555
|
-
if (lines[0]?.trim() !== "---") {
|
|
5556
|
-
throw new Error(`no YAML frontmatter fence in ${filePath} (expected first line "---")`);
|
|
5557
|
-
}
|
|
5558
|
-
const end = lines.indexOf("---", 1);
|
|
5559
|
-
if (end === -1) {
|
|
5560
|
-
throw new Error(`unterminated YAML frontmatter in ${filePath} (no closing "---")`);
|
|
5561
|
-
}
|
|
5562
|
-
const doc = {};
|
|
5563
|
-
let listKey = null;
|
|
5564
|
-
for (let i = 1;i < end; i += 1) {
|
|
5565
|
-
const line = lines[i] ?? "";
|
|
5566
|
-
if (!line.trim() || line.trim().startsWith("#"))
|
|
5567
|
-
continue;
|
|
5568
|
-
if (listKey !== null && /^\s*-\s+/.test(line)) {
|
|
5569
|
-
const item = line.replace(/^\s*-\s+/, "").trim().replace(/^["']|["']$/g, "");
|
|
5570
|
-
if (!Array.isArray(doc[listKey]))
|
|
5571
|
-
doc[listKey] = [];
|
|
5572
|
-
doc[listKey].push(item);
|
|
5573
|
-
continue;
|
|
5574
|
-
}
|
|
5575
|
-
listKey = null;
|
|
5576
|
-
const kv = line.match(/^([A-Za-z_][A-Za-z0-9_-]*):\s*(.*)$/);
|
|
5577
|
-
if (!kv) {
|
|
5578
|
-
throw new Error(`unsupported frontmatter line in ${filePath}: ${JSON.stringify(line)}`);
|
|
5579
|
-
}
|
|
5580
|
-
const value = kv[2].trim();
|
|
5581
|
-
doc[kv[1]] = value === "" ? null : /^\[.*\]$/.test(value) ? parseFlowArray(value, filePath) : value.replace(/^["']|["']$/g, "");
|
|
5582
|
-
listKey = value === "" ? kv[1] : null;
|
|
5583
|
-
}
|
|
5584
|
-
return doc;
|
|
5585
|
-
}
|
|
5586
|
-
function parseFlowArray(raw, filePath) {
|
|
5587
|
-
const inner = raw.slice(1, -1);
|
|
5588
|
-
if (/[[\]]/.test(inner)) {
|
|
5589
|
-
throw new Error(`nested flow-style array in ${filePath}: ${JSON.stringify(raw)} \u2014 only flat scalar items are supported (e.g. [a, b])`);
|
|
5590
|
-
}
|
|
5591
|
-
let quote = null;
|
|
5592
|
-
for (const ch of inner) {
|
|
5593
|
-
if (ch === '"' || ch === "'") {
|
|
5594
|
-
if (quote === null)
|
|
5595
|
-
quote = ch;
|
|
5596
|
-
else if (quote === ch)
|
|
5597
|
-
quote = null;
|
|
5598
|
-
} else if (ch === "," && quote !== null) {
|
|
5599
|
-
throw new Error(`ambiguous flow-style array in ${filePath}: ${JSON.stringify(raw)} \u2014 quoted item containing comma cannot be split unambiguously (flat scalar items only)`);
|
|
5600
|
-
}
|
|
5601
|
-
}
|
|
5602
|
-
if (quote !== null) {
|
|
5603
|
-
throw new Error(`unterminated ${quote} quote in flow-style array in ${filePath}: ${JSON.stringify(raw)}`);
|
|
5604
|
-
}
|
|
5605
|
-
const items = [];
|
|
5606
|
-
for (const part of inner.split(",")) {
|
|
5607
|
-
const item = part.trim().replace(/^["']|["']$/g, "");
|
|
5608
|
-
if (item === "")
|
|
5609
|
-
continue;
|
|
5610
|
-
items.push(item);
|
|
5611
|
-
}
|
|
5612
|
-
return items;
|
|
5613
|
-
}
|
|
5614
|
-
var PROJECT_REGISTER_FILE = "residuals.json";
|
|
5615
|
-
var _DEFAULT_PROJECT = "_default";
|
|
5616
|
-
var ROLLUP_FIELDS = ["total_open", "by_severity", "by_target", "by_plan"];
|
|
5617
|
-
function isPlainObject5(value) {
|
|
5618
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
5619
|
-
}
|
|
5620
|
-
function violation7(severity, code, message, fix) {
|
|
5621
|
-
return { ok: false, severity, code, message, fix };
|
|
5622
|
-
}
|
|
5623
|
-
function findingsCleanupGate(register, planId, opts) {
|
|
5624
|
-
const mode = opts?.mode ?? "allow-residual";
|
|
5625
|
-
const violations = [];
|
|
5626
|
-
const entries = isPlainObject5(register.entries) ? register.entries[planId] : undefined;
|
|
5627
|
-
if (entries === undefined) {
|
|
5628
|
-
return { ok: true, violations };
|
|
5629
|
-
}
|
|
5630
|
-
if (!Array.isArray(entries)) {
|
|
5631
|
-
violations.push(violation7("high", "project.register.invalid-entry-list", `entries[${JSON.stringify(planId)}] must be an array of residual entries (one entry per residual; v1 multi-finding semantics)`));
|
|
5632
|
-
return { ok: false, violations };
|
|
5633
|
-
}
|
|
5634
|
-
if (entries.length === 0) {
|
|
5635
|
-
return { ok: true, violations };
|
|
5636
|
-
}
|
|
5637
|
-
for (const entry of entries) {
|
|
5638
|
-
if (!isOpenResidual(entry))
|
|
5639
|
-
continue;
|
|
5640
|
-
const id = typeof entry.id === "string" ? entry.id : "<unnamed>";
|
|
5641
|
-
const label = `R#${id}`;
|
|
5642
|
-
if (mode === "zero-residual") {
|
|
5643
|
-
if (entry.severity === "nit") {
|
|
5644
|
-
violations.push(violation7("medium", "findings.zero-residual-nit", `${label}: style-only nits must be fixed in-session or dropped \u2014 never left open under zero-residual`));
|
|
5645
|
-
} else if (entry.decision === "risk-accepted" || entry.lifecycle === "waived") {
|
|
5646
|
-
violations.push(violation7("medium", "findings.zero-residual-risk-accepted", `${label}: waived/risk-accepted findings must be closed/archived, not left open under zero-residual`));
|
|
5647
|
-
} else if (entry.decision === "defer") {
|
|
5648
|
-
if (typeof entry.target !== "string" || entry.target.trim() === "") {
|
|
5649
|
-
violations.push(violation7("medium", "findings.zero-residual-defer-no-target", `${label}: blocker-defer requires a target (next iteration/milestone) under zero-residual`));
|
|
5650
|
-
}
|
|
5651
|
-
} else {
|
|
5652
|
-
violations.push(violation7("medium", "findings.zero-residual-open-fixable", `${label}: fixable finding must not remain open under zero-residual \u2014 fix now or convert to a blocker-defer`));
|
|
5653
|
-
}
|
|
5654
|
-
} else if (normalizeSeverity(entry.severity) === "critical") {
|
|
5655
|
-
violations.push(violation7("high", "findings.allow-residual-critical", `${label}: unresolved critical blocks Approve with residuals`));
|
|
5703
|
+
out = outFile;
|
|
5704
|
+
} else {
|
|
5705
|
+
const sddDir = opts.sddDir ?? process.env.SDD_DIR;
|
|
5706
|
+
if (!sddDir) {
|
|
5707
|
+
throw new SddScriptError("mstar sdd task-brief: set SDD_DIR or pass OUTFILE (run mstar sdd workspace PLAN_ID first)", 2);
|
|
5656
5708
|
}
|
|
5709
|
+
mkdirSync5(sddDir, { recursive: true });
|
|
5710
|
+
out = join9(sddDir, `task-${taskN}-brief.md`);
|
|
5657
5711
|
}
|
|
5658
|
-
|
|
5659
|
-
|
|
5660
|
-
|
|
5661
|
-
|
|
5662
|
-
|
|
5663
|
-
|
|
5664
|
-
|
|
5712
|
+
const records = content.endsWith(`
|
|
5713
|
+
`) ? content.split(`
|
|
5714
|
+
`).slice(0, -1) : content.split(`
|
|
5715
|
+
`);
|
|
5716
|
+
const headingRe = /^#+[ \t]+Task[ \t]+[0-9]+/;
|
|
5717
|
+
const targetRe = new RegExp(`^#+[ ]+Task[ ]+${taskN}([^0-9]|$)`);
|
|
5718
|
+
let infence = false;
|
|
5719
|
+
let intask = false;
|
|
5720
|
+
const printed = [];
|
|
5721
|
+
for (const line of records) {
|
|
5722
|
+
if (/^```/.test(line))
|
|
5723
|
+
infence = !infence;
|
|
5724
|
+
if (!infence && headingRe.test(line))
|
|
5725
|
+
intask = targetRe.test(line);
|
|
5726
|
+
if (intask)
|
|
5727
|
+
printed.push(line);
|
|
5665
5728
|
}
|
|
5666
|
-
|
|
5729
|
+
const output = printed.length > 0 ? `${printed.join(`
|
|
5730
|
+
`)}
|
|
5731
|
+
` : "";
|
|
5732
|
+
writeFileSync4(out, output);
|
|
5733
|
+
if (printed.length === 0) {
|
|
5734
|
+
throw new SddScriptError(`task ${taskN} not found in ${planFile} (no heading matching Task ${taskN})`, 3);
|
|
5735
|
+
}
|
|
5736
|
+
return out;
|
|
5667
5737
|
}
|
|
5668
|
-
function
|
|
5669
|
-
|
|
5670
|
-
|
|
5671
|
-
try {
|
|
5672
|
-
entries = readdirSync5(projectDir, { withFileTypes: true });
|
|
5673
|
-
} catch {
|
|
5674
|
-
entries = [];
|
|
5738
|
+
function reviewPackage(base, head, outFile, opts = {}) {
|
|
5739
|
+
if (!base || !head) {
|
|
5740
|
+
throw new SddScriptError("usage: mstar sdd review-package BASE HEAD [OUTFILE]", 2);
|
|
5675
5741
|
}
|
|
5676
|
-
|
|
5677
|
-
|
|
5678
|
-
continue;
|
|
5679
|
-
const registerPath = join9(projectDir, project.name, PROJECT_REGISTER_FILE);
|
|
5680
|
-
if (!existsSync6(registerPath))
|
|
5681
|
-
continue;
|
|
5682
|
-
let register;
|
|
5742
|
+
const cwd = opts.cwd ?? process.cwd();
|
|
5743
|
+
const verifyRef = (ref, what) => {
|
|
5683
5744
|
try {
|
|
5684
|
-
|
|
5745
|
+
execFileSync3("git", ["rev-parse", "--verify", "--quiet", ref], { cwd, stdio: ["ignore", "pipe", "pipe"] });
|
|
5685
5746
|
} catch {
|
|
5686
|
-
|
|
5747
|
+
throw new SddScriptError(`bad ${what}: ${ref}`, 2);
|
|
5687
5748
|
}
|
|
5688
|
-
|
|
5689
|
-
|
|
5690
|
-
|
|
5691
|
-
|
|
5692
|
-
|
|
5693
|
-
|
|
5694
|
-
|
|
5695
|
-
|
|
5696
|
-
|
|
5697
|
-
|
|
5749
|
+
};
|
|
5750
|
+
verifyRef(base, "BASE");
|
|
5751
|
+
verifyRef(head, "HEAD");
|
|
5752
|
+
let out;
|
|
5753
|
+
if (outFile) {
|
|
5754
|
+
out = outFile;
|
|
5755
|
+
} else {
|
|
5756
|
+
const sddDir = opts.sddDir ?? process.env.SDD_DIR;
|
|
5757
|
+
if (!sddDir) {
|
|
5758
|
+
throw new SddScriptError("mstar sdd review-package: set SDD_DIR or pass OUTFILE", 2);
|
|
5698
5759
|
}
|
|
5760
|
+
mkdirSync5(sddDir, { recursive: true });
|
|
5761
|
+
const shortBase = gitOut(cwd, ["rev-parse", "--short", base]) ?? base;
|
|
5762
|
+
const shortHead = gitOut(cwd, ["rev-parse", "--short", head]) ?? head;
|
|
5763
|
+
out = join9(sddDir, `review-${shortBase}..${shortHead}.diff`);
|
|
5699
5764
|
}
|
|
5700
|
-
const
|
|
5701
|
-
|
|
5702
|
-
|
|
5703
|
-
|
|
5704
|
-
|
|
5705
|
-
|
|
5706
|
-
|
|
5707
|
-
|
|
5708
|
-
|
|
5709
|
-
|
|
5710
|
-
|
|
5711
|
-
|
|
5712
|
-
|
|
5713
|
-
|
|
5765
|
+
const run = (args) => execFileSync3("git", args, { cwd, maxBuffer: GIT_CAPTURE_MAX_BYTES });
|
|
5766
|
+
const parts = [
|
|
5767
|
+
Buffer.from(`# Review package: ${base}..${head}
|
|
5768
|
+
|
|
5769
|
+
## Commits
|
|
5770
|
+
`),
|
|
5771
|
+
run(["log", "--oneline", `${base}..${head}`]),
|
|
5772
|
+
Buffer.from(`
|
|
5773
|
+
## Files changed
|
|
5774
|
+
`),
|
|
5775
|
+
run(["diff", "--stat", `${base}..${head}`]),
|
|
5776
|
+
Buffer.from(`
|
|
5777
|
+
## Diff
|
|
5778
|
+
`),
|
|
5779
|
+
run(["diff", "-U10", `${base}..${head}`])
|
|
5780
|
+
];
|
|
5781
|
+
writeFileSync4(out, Buffer.concat(parts));
|
|
5782
|
+
return out;
|
|
5714
5783
|
}
|
|
5715
5784
|
function violation8(severity, code, message, fix) {
|
|
5716
5785
|
return { ok: false, severity, code, message, fix };
|
|
@@ -6186,8 +6255,17 @@ function readPlanFileSummary(filePath) {
|
|
|
6186
6255
|
const blocks = parseStatusBlocks(text);
|
|
6187
6256
|
return { title: title.trim(), fields: blocks.length > 0 ? blocks[0].fields : new Map };
|
|
6188
6257
|
}
|
|
6258
|
+
function extractSecurityDispositionSections(text) {
|
|
6259
|
+
const grab = (heading) => {
|
|
6260
|
+
const match = text.match(new RegExp(`(?:^|\\r?\\n)##[ \\t]+${heading}[ \\t]*\\r?\\n(?:[ \\t]*\\r?\\n)?([\\s\\S]*?)(?=\\r?\\n## |$)`, "i"));
|
|
6261
|
+
if (!match)
|
|
6262
|
+
return [];
|
|
6263
|
+
return match[1].split(/\r?\n/).map((line) => line.replace(/\r$/, "")).filter((line) => line.startsWith("- "));
|
|
6264
|
+
};
|
|
6265
|
+
return { needsVerification: grab("Needs verification"), hardeningChecked: grab("Hardening & checked notes") };
|
|
6266
|
+
}
|
|
6189
6267
|
function renderIndex(params) {
|
|
6190
|
-
const { date, repoName, repoShortSha, rows, rejected } = params;
|
|
6268
|
+
const { date, repoName, repoShortSha, rows, rejected, needsVerification, hardeningChecked } = params;
|
|
6191
6269
|
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(`
|
|
6192
6270
|
`);
|
|
6193
6271
|
const directionRows = rows.filter((r) => r.category === "direction").map((r) => `- ${escapeCell(r.title)} \u2014 ${escapeCell(truncate(r.impact, 120))}`).join(`
|
|
@@ -6208,6 +6286,12 @@ function renderIndex(params) {
|
|
|
6208
6286
|
if (directionRows !== "") {
|
|
6209
6287
|
sections.push("", "## Direction", "", directionRows);
|
|
6210
6288
|
}
|
|
6289
|
+
if (needsVerification.length > 0) {
|
|
6290
|
+
sections.push("", "## Needs verification", "", ...needsVerification);
|
|
6291
|
+
}
|
|
6292
|
+
if (hardeningChecked.length > 0) {
|
|
6293
|
+
sections.push("", "## Hardening & checked notes", "", ...hardeningChecked);
|
|
6294
|
+
}
|
|
6211
6295
|
sections.push("", "## Execution order & status", "", "| Plan | Title | Priority | Effort | Depends on | Status |", "|------|-------|----------|--------|------------|--------|", executionRows);
|
|
6212
6296
|
if (rejectedRows !== "") {
|
|
6213
6297
|
sections.push("", "## Findings considered and rejected", "", rejectedRows);
|
|
@@ -6221,6 +6305,8 @@ function scaffoldAuditPlan(outDir, findings, options = {}) {
|
|
|
6221
6305
|
const date = options.date ?? new Date().toISOString().slice(0, 10);
|
|
6222
6306
|
const plannedAt = options.plannedAt ?? { commit: options.repoShortSha ?? "unknown", date };
|
|
6223
6307
|
mkdirSync7(outDir, { recursive: true });
|
|
6308
|
+
const existingReadme = join11(outDir, "README.md");
|
|
6309
|
+
const carried = existsSync7(existingReadme) ? extractSecurityDispositionSections(readFileSync9(existingReadme, "utf8")) : { needsVerification: [], hardeningChecked: [] };
|
|
6224
6310
|
const existing = readdirSync7(outDir).filter((f) => /^\d{3}-.*\.md$/.test(f));
|
|
6225
6311
|
let next = existing.reduce((max, f) => Math.max(max, Number(f.slice(0, 3))), 0) + 1;
|
|
6226
6312
|
const written = [];
|
|
@@ -6236,7 +6322,7 @@ function scaffoldAuditPlan(outDir, findings, options = {}) {
|
|
|
6236
6322
|
}
|
|
6237
6323
|
usedSlugs.add(slug);
|
|
6238
6324
|
const file = `${num}-${slug}.md`;
|
|
6239
|
-
|
|
6325
|
+
writeFileSync6(join11(outDir, file), renderPlanFile(finding, plannedAt));
|
|
6240
6326
|
written.push(file);
|
|
6241
6327
|
next++;
|
|
6242
6328
|
}
|
|
@@ -6274,12 +6360,16 @@ function scaffoldAuditPlan(outDir, findings, options = {}) {
|
|
|
6274
6360
|
row.dependsOn = finding.dependsOn ?? "none";
|
|
6275
6361
|
}
|
|
6276
6362
|
});
|
|
6277
|
-
|
|
6363
|
+
const needsVerificationLines = options.needsVerification !== undefined ? options.needsVerification.map((nv) => `- ${escapeCell(nv.lead)}: ${escapeCell(nv.how)}${nv.evidence ? ` (${escapeCell(nv.evidence)})` : ""}`) : carried.needsVerification;
|
|
6364
|
+
const hardeningCheckedLines = options.hardeningChecked !== undefined ? options.hardeningChecked.map((hc) => `- ${hc.kind}: ${escapeCell(hc.text)}`) : carried.hardeningChecked;
|
|
6365
|
+
writeFileSync6(join11(outDir, "README.md"), renderIndex({
|
|
6278
6366
|
date,
|
|
6279
6367
|
repoName: options.repoName ?? "repo",
|
|
6280
6368
|
repoShortSha: options.repoShortSha ?? "unknown",
|
|
6281
6369
|
rows,
|
|
6282
|
-
rejected: options.rejected ?? []
|
|
6370
|
+
rejected: options.rejected ?? [],
|
|
6371
|
+
needsVerification: needsVerificationLines,
|
|
6372
|
+
hardeningChecked: hardeningCheckedLines
|
|
6283
6373
|
}));
|
|
6284
6374
|
return { outDir: resolve9(outDir), date, files: written, nextNumber: next };
|
|
6285
6375
|
}
|
|
@@ -7218,24 +7308,24 @@ function lintFiveQuestion(bodyText, mode = "authoring") {
|
|
|
7218
7308
|
}
|
|
7219
7309
|
|
|
7220
7310
|
// ../engine/dist/engine.js
|
|
7221
|
-
import { existsSync as existsSync10, mkdirSync as
|
|
7311
|
+
import { existsSync as existsSync10, mkdirSync as mkdirSync3, readFileSync as readFileSync8, renameSync as renameSync2, unlinkSync as unlinkSync3, writeFileSync as writeFileSync5 } from "node:fs";
|
|
7222
7312
|
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
7223
|
-
import { basename as basename6, dirname as dirname7, join as
|
|
7313
|
+
import { basename as basename6, dirname as dirname7, join as join3, resolve as resolve8 } from "node:path";
|
|
7224
7314
|
import { fileURLToPath } from "node:url";
|
|
7225
7315
|
import { readFileSync as readFileSync22, statSync as statSync5 } from "node:fs";
|
|
7226
7316
|
import { dirname as dirname22, isAbsolute as isAbsolute6, join as join22, relative as relative3, resolve as resolve22 } from "node:path";
|
|
7227
|
-
import { existsSync as
|
|
7317
|
+
import { existsSync as existsSync52, mkdirSync as mkdirSync42, readdirSync as readdirSync42, readFileSync as readFileSync62, realpathSync as realpathSync22, statSync as statSync32, writeFileSync as writeFileSync32 } from "node:fs";
|
|
7228
7318
|
import { execFileSync as execFileSync4 } from "node:child_process";
|
|
7229
|
-
import { basename as basename22, dirname as
|
|
7230
|
-
import { existsSync as existsSync32, readFileSync as readFileSync42, readdirSync as readdirSync22, realpathSync as
|
|
7231
|
-
import { dirname as
|
|
7232
|
-
import { mkdirSync as
|
|
7233
|
-
import { dirname as
|
|
7319
|
+
import { basename as basename22, dirname as dirname52, isAbsolute as isAbsolute32, join as join82, relative as relative22, resolve as resolve52 } from "node:path";
|
|
7320
|
+
import { existsSync as existsSync32, readFileSync as readFileSync42, readdirSync as readdirSync22, realpathSync as realpathSync4 } from "node:fs";
|
|
7321
|
+
import { dirname as dirname42, join as join62, resolve as resolve42, sep as sep2 } from "node:path";
|
|
7322
|
+
import { mkdirSync as mkdirSync22, rmdirSync as rmdirSync3, statSync as statSync22, unlinkSync as unlinkSync22, writeFileSync as writeFileSync22 } from "node:fs";
|
|
7323
|
+
import { dirname as dirname32, isAbsolute as isAbsolute22, join as join42, resolve as resolve32 } from "node:path";
|
|
7234
7324
|
import { setTimeout as sleep2 } from "node:timers/promises";
|
|
7235
7325
|
import { AsyncLocalStorage as AsyncLocalStorage3 } from "node:async_hooks";
|
|
7236
|
-
import { mkdirSync as
|
|
7237
|
-
import { join as
|
|
7238
|
-
import { copyFileSync, mkdirSync as mkdirSync6, readFileSync as readFileSync82, readdirSync as
|
|
7326
|
+
import { mkdirSync as mkdirSync32 } from "node:fs";
|
|
7327
|
+
import { join as join5 } from "node:path";
|
|
7328
|
+
import { copyFileSync, mkdirSync as mkdirSync6, readFileSync as readFileSync82, readdirSync as readdirSync6, writeFileSync as writeFileSync52 } from "node:fs";
|
|
7239
7329
|
import { dirname as dirname72, isAbsolute as isAbsolute62, join as join10, relative as relative32, resolve as resolve82, sep as sep22 } from "node:path";
|
|
7240
7330
|
var SEVERITY_ORDER2 = ["critical", "high", "medium", "low", "nit"];
|
|
7241
7331
|
function readJson2(filePath) {
|
|
@@ -7252,10 +7342,10 @@ function readJson2(filePath) {
|
|
|
7252
7342
|
}
|
|
7253
7343
|
function writeJson2(filePath, value) {
|
|
7254
7344
|
const parent = dirname7(filePath);
|
|
7255
|
-
|
|
7256
|
-
const tmp =
|
|
7345
|
+
mkdirSync3(parent, { recursive: true });
|
|
7346
|
+
const tmp = join3(parent, `.${basename6(filePath)}.${process.pid}.${randomUUID2()}.tmp`);
|
|
7257
7347
|
try {
|
|
7258
|
-
|
|
7348
|
+
writeFileSync5(tmp, `${JSON.stringify(value, null, 2)}
|
|
7259
7349
|
`, "utf8");
|
|
7260
7350
|
renameSync2(tmp, filePath);
|
|
7261
7351
|
} catch (error) {
|
|
@@ -7269,7 +7359,7 @@ function resolveProjectRoot(startDir = process.cwd()) {
|
|
|
7269
7359
|
const start = resolve8(startDir);
|
|
7270
7360
|
let dir = start;
|
|
7271
7361
|
for (;; ) {
|
|
7272
|
-
if (existsSync10(
|
|
7362
|
+
if (existsSync10(join3(dir, "package.json")) || existsSync10(join3(dir, "bun.lock")))
|
|
7273
7363
|
return dir;
|
|
7274
7364
|
const parent = dirname7(dir);
|
|
7275
7365
|
if (parent === dir)
|
|
@@ -7293,7 +7383,7 @@ function findRootPackageJson(startDir) {
|
|
|
7293
7383
|
}
|
|
7294
7384
|
}
|
|
7295
7385
|
function harnessVersionFrom(moduleDir) {
|
|
7296
|
-
const ownManifest =
|
|
7386
|
+
const ownManifest = join3(moduleDir, "..", "package.json");
|
|
7297
7387
|
try {
|
|
7298
7388
|
const pkg = JSON.parse(readFileSync8(ownManifest, "utf8"));
|
|
7299
7389
|
if (typeof pkg.version === "string" && pkg.version !== "")
|
|
@@ -7397,121 +7487,78 @@ function isAtOrBelow3(dir, root) {
|
|
|
7397
7487
|
const rel = relative3(root, dir);
|
|
7398
7488
|
return rel === "" || !rel.startsWith("..") && !isAbsolute6(rel);
|
|
7399
7489
|
}
|
|
7400
|
-
function
|
|
7401
|
-
const
|
|
7402
|
-
|
|
7403
|
-
|
|
7404
|
-
|
|
7405
|
-
const
|
|
7406
|
-
|
|
7407
|
-
|
|
7408
|
-
|
|
7409
|
-
|
|
7410
|
-
|
|
7411
|
-
|
|
7412
|
-
|
|
7413
|
-
|
|
7414
|
-
|
|
7415
|
-
|
|
7490
|
+
function parseCompassFrontmatterText2(content, filePath) {
|
|
7491
|
+
const lines = content.split(/\r?\n/);
|
|
7492
|
+
if (lines[0]?.trim() !== "---") {
|
|
7493
|
+
throw new Error(`no YAML frontmatter fence in ${filePath} (expected first line "---")`);
|
|
7494
|
+
}
|
|
7495
|
+
const end = lines.indexOf("---", 1);
|
|
7496
|
+
if (end === -1) {
|
|
7497
|
+
throw new Error(`unterminated YAML frontmatter in ${filePath} (no closing "---")`);
|
|
7498
|
+
}
|
|
7499
|
+
const doc = {};
|
|
7500
|
+
let listKey = null;
|
|
7501
|
+
for (let i = 1;i < end; i += 1) {
|
|
7502
|
+
const line = lines[i] ?? "";
|
|
7503
|
+
if (!line.trim() || line.trim().startsWith("#"))
|
|
7504
|
+
continue;
|
|
7505
|
+
if (listKey !== null && /^\s*-\s+/.test(line)) {
|
|
7506
|
+
const item = line.replace(/^\s*-\s+/, "").trim().replace(/^["']|["']$/g, "");
|
|
7507
|
+
if (!Array.isArray(doc[listKey]))
|
|
7508
|
+
doc[listKey] = [];
|
|
7509
|
+
doc[listKey].push(item);
|
|
7510
|
+
continue;
|
|
7416
7511
|
}
|
|
7417
|
-
|
|
7418
|
-
|
|
7419
|
-
|
|
7420
|
-
|
|
7421
|
-
|
|
7422
|
-
|
|
7512
|
+
listKey = null;
|
|
7513
|
+
const kv = line.match(/^([A-Za-z_][A-Za-z0-9_-]*):\s*(.*)$/);
|
|
7514
|
+
if (!kv) {
|
|
7515
|
+
throw new Error(`unsupported frontmatter line in ${filePath}: ${JSON.stringify(line)}`);
|
|
7516
|
+
}
|
|
7517
|
+
const value = kv[2].trim();
|
|
7518
|
+
doc[kv[1]] = value === "" ? null : /^\[.*\]$/.test(value) ? parseFlowArray2(value, filePath) : value.replace(/^["']|["']$/g, "");
|
|
7519
|
+
listKey = value === "" ? kv[1] : null;
|
|
7423
7520
|
}
|
|
7521
|
+
return doc;
|
|
7424
7522
|
}
|
|
7425
|
-
function
|
|
7426
|
-
|
|
7427
|
-
|
|
7428
|
-
|
|
7429
|
-
|
|
7430
|
-
|
|
7431
|
-
|
|
7432
|
-
if (
|
|
7433
|
-
|
|
7434
|
-
|
|
7435
|
-
|
|
7436
|
-
|
|
7437
|
-
|
|
7523
|
+
function parseFlowArray2(raw, filePath) {
|
|
7524
|
+
const inner = raw.slice(1, -1);
|
|
7525
|
+
if (/[[\]]/.test(inner)) {
|
|
7526
|
+
throw new Error(`nested flow-style array in ${filePath}: ${JSON.stringify(raw)} \u2014 only flat scalar items are supported (e.g. [a, b])`);
|
|
7527
|
+
}
|
|
7528
|
+
let quote = null;
|
|
7529
|
+
for (const ch of inner) {
|
|
7530
|
+
if (ch === '"' || ch === "'") {
|
|
7531
|
+
if (quote === null)
|
|
7532
|
+
quote = ch;
|
|
7533
|
+
else if (quote === ch)
|
|
7534
|
+
quote = null;
|
|
7535
|
+
} else if (ch === "," && quote !== null) {
|
|
7536
|
+
throw new Error(`ambiguous flow-style array in ${filePath}: ${JSON.stringify(raw)} \u2014 quoted item containing comma cannot be split unambiguously (flat scalar items only)`);
|
|
7438
7537
|
}
|
|
7439
|
-
return resolve32(boundary);
|
|
7440
|
-
} catch {}
|
|
7441
|
-
return startDir;
|
|
7442
|
-
}
|
|
7443
|
-
function isAtOrBelow22(dir, root) {
|
|
7444
|
-
const rel = relative22(root, dir);
|
|
7445
|
-
return rel === "" || !rel.startsWith("..") && !isAbsolute22(rel);
|
|
7446
|
-
}
|
|
7447
|
-
function mstarcDirOverride2(harnessDir, key) {
|
|
7448
|
-
const dir = resolve32(harnessDir);
|
|
7449
|
-
const rc = loadMstarc2(dir, dirname32(dir));
|
|
7450
|
-
const declared = rc?.config[key];
|
|
7451
|
-
return declared ? resolve32(rc.dir, declared) : null;
|
|
7452
|
-
}
|
|
7453
|
-
function assertSafePathComponent2(value, what) {
|
|
7454
|
-
if (value === "" || value === "." || value === ".." || !/^[A-Za-z0-9._-]+$/.test(value)) {
|
|
7455
|
-
throw new Error(`${what} must be a single safe path component ([A-Za-z0-9._-]+; not "", ".", "..", or containing "/" or "\\") \u2014 got ${JSON.stringify(value)}`);
|
|
7456
7538
|
}
|
|
7457
|
-
|
|
7458
|
-
|
|
7459
|
-
const harness = resolveHarnessDir2(startDir, opts);
|
|
7460
|
-
if (harness === null) {
|
|
7461
|
-
throw new Error(`harness dir not found from ${resolve32(startDir)} \u2014 cannot resolve the ${fallback} dir (run \`mstar init\`, pass opts.harnessDir, or set MSTAR_HARNESS_DIR)`);
|
|
7539
|
+
if (quote !== null) {
|
|
7540
|
+
throw new Error(`unterminated ${quote} quote in flow-style array in ${filePath}: ${JSON.stringify(raw)}`);
|
|
7462
7541
|
}
|
|
7463
|
-
const
|
|
7464
|
-
|
|
7465
|
-
|
|
7466
|
-
|
|
7467
|
-
|
|
7468
|
-
|
|
7469
|
-
function resolveProjectDir2(startDir = process.cwd(), opts = {}) {
|
|
7470
|
-
return resolveHarnessSubdir2(startDir, opts, "projectDir", "projects");
|
|
7471
|
-
}
|
|
7472
|
-
var GITIGNORE_SNIPPET2 = `# Morning Star harness (.mstar/)
|
|
7473
|
-
# Principle: process stays local; results are shared with the team.
|
|
7474
|
-
# Default-ignore everything under .mstar/, then re-include the tracked results.
|
|
7475
|
-
.mstar/**
|
|
7476
|
-
!.mstar/AGENTS.md
|
|
7477
|
-
!.mstar/knowledge/
|
|
7478
|
-
!.mstar/knowledge/**
|
|
7479
|
-
!.mstar/specs/
|
|
7480
|
-
!.mstar/specs/**
|
|
7481
|
-
# .mstarc \u2014 repo-local harness config (may declare [config] harness_dir=<name>)
|
|
7482
|
-
.mstarc
|
|
7483
|
-
`;
|
|
7484
|
-
var GITIGNORE_SNIPPET_AGENTS2 = `# Morning Star harness (.agents/) \u2014 legacy
|
|
7485
|
-
# Default-ignore everything under .agents/, then re-include the tracked results.
|
|
7486
|
-
.agents/**
|
|
7487
|
-
!.agents/AGENTS.md
|
|
7488
|
-
!.agents/knowledge/
|
|
7489
|
-
!.agents/knowledge/**
|
|
7490
|
-
!.agents/specs/
|
|
7491
|
-
!.agents/specs/**
|
|
7492
|
-
`;
|
|
7493
|
-
var GITIGNORE_PROCESS_ENTRIES2 = GITIGNORE_SNIPPET2.split(`
|
|
7494
|
-
`).filter((line) => line.startsWith(".mstar/") || line.startsWith("!.mstar/")).map((line) => line.trim());
|
|
7495
|
-
var GITIGNORE_PROCESS_ENTRIES_AGENTS2 = GITIGNORE_SNIPPET_AGENTS2.split(`
|
|
7496
|
-
`).filter((line) => line.startsWith(".agents/") || line.startsWith("!.agents/")).map((line) => line.trim());
|
|
7497
|
-
function isDirectory3(dir) {
|
|
7498
|
-
try {
|
|
7499
|
-
return statSync22(dir).isDirectory();
|
|
7500
|
-
} catch {
|
|
7501
|
-
return false;
|
|
7542
|
+
const items = [];
|
|
7543
|
+
for (const part of inner.split(",")) {
|
|
7544
|
+
const item = part.trim().replace(/^["']|["']$/g, "");
|
|
7545
|
+
if (item === "")
|
|
7546
|
+
continue;
|
|
7547
|
+
items.push(item);
|
|
7502
7548
|
}
|
|
7549
|
+
return items;
|
|
7503
7550
|
}
|
|
7504
|
-
function
|
|
7551
|
+
function isPlainObject23(value) {
|
|
7505
7552
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
7506
7553
|
}
|
|
7507
|
-
function
|
|
7554
|
+
function violation22(severity, code, message, fix) {
|
|
7508
7555
|
return { ok: false, severity, code, message, fix };
|
|
7509
7556
|
}
|
|
7510
7557
|
function validateNonEmptyString4(violations, value, field, missingCode, invalidCode) {
|
|
7511
7558
|
if (value === undefined) {
|
|
7512
|
-
violations.push(
|
|
7559
|
+
violations.push(violation22("high", missingCode, `missing required field: ${field}`));
|
|
7513
7560
|
} else if (typeof value !== "string" || value.trim() === "") {
|
|
7514
|
-
violations.push(
|
|
7561
|
+
violations.push(violation22("medium", invalidCode, `${field} must be a non-empty string`));
|
|
7515
7562
|
}
|
|
7516
7563
|
}
|
|
7517
7564
|
var DATE_PART2 = String.raw`\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12]\d|3[01])`;
|
|
@@ -7522,54 +7569,54 @@ function isValidClaimedAt2(value) {
|
|
|
7522
7569
|
}
|
|
7523
7570
|
function validateExecutionLease2(lease) {
|
|
7524
7571
|
const violations = [];
|
|
7525
|
-
if (!
|
|
7572
|
+
if (!isPlainObject23(lease)) {
|
|
7526
7573
|
return {
|
|
7527
7574
|
ok: false,
|
|
7528
7575
|
violations: [
|
|
7529
|
-
|
|
7576
|
+
violation22("high", "lease.execution-lease.invalid", "execution_lease must be an object \u2014 null and tombstone objects are invalid; writers delete the key on release")
|
|
7530
7577
|
]
|
|
7531
7578
|
};
|
|
7532
7579
|
}
|
|
7533
7580
|
validateNonEmptyString4(violations, lease.holder, "holder", "lease.execution-lease.missing-holder", "lease.execution-lease.invalid-holder");
|
|
7534
7581
|
if (lease.claimed_at === undefined) {
|
|
7535
|
-
violations.push(
|
|
7582
|
+
violations.push(violation22("high", "lease.execution-lease.missing-claimed-at", "missing required field: claimed_at"));
|
|
7536
7583
|
} else if (!isValidClaimedAt2(lease.claimed_at)) {
|
|
7537
|
-
violations.push(
|
|
7584
|
+
violations.push(violation22("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"));
|
|
7538
7585
|
}
|
|
7539
7586
|
if (lease.worktree_path === undefined) {
|
|
7540
|
-
violations.push(
|
|
7587
|
+
violations.push(violation22("high", "lease.execution-lease.missing-worktree-path", "missing required field: worktree_path"));
|
|
7541
7588
|
} else if (typeof lease.worktree_path !== "string" || lease.worktree_path.trim() === "") {
|
|
7542
|
-
violations.push(
|
|
7543
|
-
} else if (!
|
|
7544
|
-
violations.push(
|
|
7589
|
+
violations.push(violation22("medium", "lease.execution-lease.invalid-worktree-path", "worktree_path must be a non-empty string"));
|
|
7590
|
+
} else if (!isAbsolute22(lease.worktree_path)) {
|
|
7591
|
+
violations.push(violation22("medium", "lease.execution-lease.invalid-worktree-path", "worktree_path must be an absolute path \u2014 it identifies the dedicated feature-worktree root (and MUST differ from metadata.control_worktree_path)"));
|
|
7545
7592
|
}
|
|
7546
7593
|
validateNonEmptyString4(violations, lease.working_branch, "working_branch", "lease.execution-lease.missing-working-branch", "lease.execution-lease.invalid-working-branch");
|
|
7547
7594
|
if (lease.session_label !== undefined && typeof lease.session_label !== "string") {
|
|
7548
|
-
violations.push(
|
|
7595
|
+
violations.push(violation22("medium", "lease.execution-lease.invalid-session-label", "session_label must be a string (display only \u2014 never used for ownership comparison)"));
|
|
7549
7596
|
}
|
|
7550
7597
|
return { ok: violations.length === 0, violations };
|
|
7551
7598
|
}
|
|
7552
7599
|
function validateIntegrationMergeLease2(lease) {
|
|
7553
7600
|
const violations = [];
|
|
7554
|
-
if (!
|
|
7601
|
+
if (!isPlainObject23(lease)) {
|
|
7555
7602
|
return {
|
|
7556
7603
|
ok: false,
|
|
7557
7604
|
violations: [
|
|
7558
|
-
|
|
7605
|
+
violation22("high", "lease.merge-lease.invalid", "integration_merge_lease must be an object \u2014 absent means unclaimed; null and tombstone objects are invalid; writers delete the key on release")
|
|
7559
7606
|
]
|
|
7560
7607
|
};
|
|
7561
7608
|
}
|
|
7562
7609
|
validateNonEmptyString4(violations, lease.holder, "holder", "lease.merge-lease.missing-holder", "lease.merge-lease.invalid-holder");
|
|
7563
7610
|
if (lease.claimed_at === undefined) {
|
|
7564
|
-
violations.push(
|
|
7611
|
+
violations.push(violation22("high", "lease.merge-lease.missing-claimed-at", "missing required field: claimed_at"));
|
|
7565
7612
|
} else if (!isValidClaimedAt2(lease.claimed_at)) {
|
|
7566
|
-
violations.push(
|
|
7613
|
+
violations.push(violation22("medium", "lease.merge-lease.invalid-claimed-at", "claimed_at must be an RFC 3339 UTC timestamp with explicit Z (e.g. 2026-07-22T04:00:00Z) or a YYYY-MM-DD date"));
|
|
7567
7614
|
}
|
|
7568
7615
|
validateNonEmptyString4(violations, lease.plan_id, "plan_id", "lease.merge-lease.missing-plan-id", "lease.merge-lease.invalid-plan-id");
|
|
7569
7616
|
validateNonEmptyString4(violations, lease.source_branch, "source_branch", "lease.merge-lease.missing-source-branch", "lease.merge-lease.invalid-source-branch");
|
|
7570
7617
|
validateNonEmptyString4(violations, lease.target_branch, "target_branch", "lease.merge-lease.missing-target-branch", "lease.merge-lease.invalid-target-branch");
|
|
7571
7618
|
if (lease.session_label !== undefined && typeof lease.session_label !== "string") {
|
|
7572
|
-
violations.push(
|
|
7619
|
+
violations.push(violation22("medium", "lease.merge-lease.invalid-session-label", "session_label must be a string (display only \u2014 never used for ownership comparison)"));
|
|
7573
7620
|
}
|
|
7574
7621
|
return { ok: violations.length === 0, violations };
|
|
7575
7622
|
}
|
|
@@ -7584,14 +7631,14 @@ function verifyPlanExecutionLease(row, planId) {
|
|
|
7584
7631
|
return {
|
|
7585
7632
|
ok: false,
|
|
7586
7633
|
violations: [
|
|
7587
|
-
|
|
7634
|
+
violation22("high", "lease.verify.orphan", "plan is InProgress without an execution_lease \u2014 orphan: STOP, no writable dispatch until recovery (status-and-residuals.md \u00a7 Orphan recovery)")
|
|
7588
7635
|
]
|
|
7589
7636
|
};
|
|
7590
7637
|
}
|
|
7591
7638
|
return {
|
|
7592
7639
|
ok: false,
|
|
7593
7640
|
violations: [
|
|
7594
|
-
|
|
7641
|
+
violation22("high", "lease.verify.missing", `plan ${planId} has no execution_lease at the SSOT location plans[].execution_lease`)
|
|
7595
7642
|
]
|
|
7596
7643
|
};
|
|
7597
7644
|
}
|
|
@@ -7603,7 +7650,7 @@ var STATUS_WRITE_LOCKDIR2 = ".status-write.lockdir";
|
|
|
7603
7650
|
var LOCKDIR_HOLDER_PID2 = "holder.pid";
|
|
7604
7651
|
var heldLockDirs2 = new AsyncLocalStorage3;
|
|
7605
7652
|
async function withStatusWriteLock2(statusPath, fn, opts = {}) {
|
|
7606
|
-
const lockDir = join42(
|
|
7653
|
+
const lockDir = join42(dirname32(resolve32(statusPath)), STATUS_WRITE_LOCKDIR2);
|
|
7607
7654
|
const held = heldLockDirs2.getStore();
|
|
7608
7655
|
if (held !== undefined && held.has(lockDir)) {
|
|
7609
7656
|
throw new Error(`${lockDir} is already held by this process in this async context \u2014 withStatusWriteLock is not reentrant; a nested acquisition on the same status.json is a bug`);
|
|
@@ -7614,8 +7661,8 @@ async function withStatusWriteLock2(statusPath, fn, opts = {}) {
|
|
|
7614
7661
|
let acquired = null;
|
|
7615
7662
|
for (;; ) {
|
|
7616
7663
|
try {
|
|
7617
|
-
|
|
7618
|
-
const st =
|
|
7664
|
+
mkdirSync22(lockDir);
|
|
7665
|
+
const st = statSync22(lockDir);
|
|
7619
7666
|
acquired = { dev: st.dev, ino: st.ino };
|
|
7620
7667
|
break;
|
|
7621
7668
|
} catch (error) {
|
|
@@ -7637,7 +7684,7 @@ async function withStatusWriteLock2(statusPath, fn, opts = {}) {
|
|
|
7637
7684
|
} finally {
|
|
7638
7685
|
owns.delete(lockDir);
|
|
7639
7686
|
try {
|
|
7640
|
-
const current =
|
|
7687
|
+
const current = statSync22(lockDir);
|
|
7641
7688
|
if (acquired !== null && current.dev === acquired.dev && current.ino === acquired.ino) {
|
|
7642
7689
|
try {
|
|
7643
7690
|
unlinkSync22(join42(lockDir, LOCKDIR_HOLDER_PID2));
|
|
@@ -7651,45 +7698,45 @@ var WORKFLOW_SNAPSHOT_FILE2 = "snapshot.json";
|
|
|
7651
7698
|
var WORKFLOW_LIFECYCLE_STATUSES2 = ["running", "paused", "completed", "failed", "stopped"];
|
|
7652
7699
|
var WORKFLOW_TERMINAL_STATUSES2 = ["completed", "failed", "stopped"];
|
|
7653
7700
|
var WORKFLOW_LIFECYCLE_TYPES2 = ["plan", "iteration"];
|
|
7654
|
-
function
|
|
7701
|
+
function isPlainObject32(value) {
|
|
7655
7702
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
7656
7703
|
}
|
|
7657
|
-
function
|
|
7704
|
+
function violation42(severity, code, message, fix) {
|
|
7658
7705
|
return { ok: false, severity, code, message, fix };
|
|
7659
7706
|
}
|
|
7660
7707
|
function validateNonEmptyString22(violations, value, field, missingCode, invalidCode) {
|
|
7661
7708
|
if (value === undefined) {
|
|
7662
|
-
violations.push(
|
|
7709
|
+
violations.push(violation42("high", missingCode, `missing required field: ${field}`));
|
|
7663
7710
|
} else if (typeof value !== "string" || value.trim() === "") {
|
|
7664
|
-
violations.push(
|
|
7711
|
+
violations.push(violation42("medium", invalidCode, `${field} must be a non-empty string`));
|
|
7665
7712
|
}
|
|
7666
7713
|
}
|
|
7667
7714
|
function validateWorkflowSnapshot2(doc) {
|
|
7668
7715
|
const violations = [];
|
|
7669
|
-
if (!
|
|
7716
|
+
if (!isPlainObject32(doc)) {
|
|
7670
7717
|
return {
|
|
7671
7718
|
ok: false,
|
|
7672
|
-
violations: [
|
|
7719
|
+
violations: [violation42("high", "workflow.snapshot.invalid", "workflow snapshot must be an object")]
|
|
7673
7720
|
};
|
|
7674
7721
|
}
|
|
7675
7722
|
if (doc.schema_version === undefined) {
|
|
7676
|
-
violations.push(
|
|
7723
|
+
violations.push(violation42("high", "workflow.snapshot.missing-schema-version", "missing required field: schema_version"));
|
|
7677
7724
|
} else if (doc.schema_version !== 1) {
|
|
7678
|
-
violations.push(
|
|
7725
|
+
violations.push(violation42("high", "workflow.snapshot.invalid-schema-version", `schema_version must be 1 \u2014 got ${JSON.stringify(doc.schema_version)} (version is reserved for the root file discriminator)`));
|
|
7679
7726
|
}
|
|
7680
7727
|
if (doc.version !== undefined) {
|
|
7681
|
-
violations.push(
|
|
7728
|
+
violations.push(violation42("medium", "workflow.snapshot.reserved-version", `top-level version is reserved for the root status.json discriminator \u2014 snapshots use schema_version; remove the version key (got ${JSON.stringify(doc.version)})`, "remove the version key from the snapshot"));
|
|
7682
7729
|
}
|
|
7683
7730
|
validateNonEmptyString22(violations, doc.id, "id", "workflow.snapshot.missing-id", "workflow.snapshot.invalid-id");
|
|
7684
7731
|
if (doc.type === undefined) {
|
|
7685
|
-
violations.push(
|
|
7732
|
+
violations.push(violation42("high", "workflow.snapshot.missing-type", "missing required field: type"));
|
|
7686
7733
|
} else if (typeof doc.type !== "string" || !WORKFLOW_LIFECYCLE_TYPES2.includes(doc.type)) {
|
|
7687
|
-
violations.push(
|
|
7734
|
+
violations.push(violation42("medium", "workflow.snapshot.invalid-type", `type must be one of ${WORKFLOW_LIFECYCLE_TYPES2.join(" | ")} \u2014 got ${JSON.stringify(doc.type)}`));
|
|
7688
7735
|
}
|
|
7689
7736
|
if (doc.status === undefined) {
|
|
7690
|
-
violations.push(
|
|
7737
|
+
violations.push(violation42("high", "workflow.snapshot.missing-status", "missing required field: status"));
|
|
7691
7738
|
} else if (typeof doc.status !== "string" || !WORKFLOW_LIFECYCLE_STATUSES2.includes(doc.status)) {
|
|
7692
|
-
violations.push(
|
|
7739
|
+
violations.push(violation42("medium", "workflow.snapshot.invalid-status", `status must be one of ${WORKFLOW_LIFECYCLE_STATUSES2.join(" | ")} \u2014 got ${JSON.stringify(doc.status)}`));
|
|
7693
7740
|
}
|
|
7694
7741
|
validateNonEmptyString22(violations, doc.started_at, "started_at", "workflow.snapshot.missing-started-at", "workflow.snapshot.invalid-started-at");
|
|
7695
7742
|
validateNonEmptyString22(violations, doc.updated_at, "updated_at", "workflow.snapshot.missing-updated-at", "workflow.snapshot.invalid-updated-at");
|
|
@@ -7697,35 +7744,35 @@ function validateWorkflowSnapshot2(doc) {
|
|
|
7697
7744
|
validateNonEmptyString22(violations, doc.ended_at, "ended_at", "workflow.snapshot.missing-ended-at", "workflow.snapshot.invalid-ended-at");
|
|
7698
7745
|
}
|
|
7699
7746
|
if (doc.phase !== undefined && typeof doc.phase !== "string") {
|
|
7700
|
-
violations.push(
|
|
7747
|
+
violations.push(violation42("medium", "workflow.snapshot.invalid-phase", "phase must be a string (free-form phase machine label)"));
|
|
7701
7748
|
}
|
|
7702
7749
|
if (doc.plans === undefined) {
|
|
7703
|
-
violations.push(
|
|
7750
|
+
violations.push(violation42("high", "workflow.snapshot.missing-plans", "missing required field: plans"));
|
|
7704
7751
|
} else if (!Array.isArray(doc.plans)) {
|
|
7705
|
-
violations.push(
|
|
7752
|
+
violations.push(violation42("high", "workflow.snapshot.invalid-plans", "plans must be an array of legacy plan rows"));
|
|
7706
7753
|
} else {
|
|
7707
7754
|
for (const row of doc.plans) {
|
|
7708
7755
|
violations.push(...validatePlanRow2(row).violations);
|
|
7709
|
-
if (
|
|
7756
|
+
if (isPlainObject32(row) && row.execution_lease !== undefined) {
|
|
7710
7757
|
violations.push(...validateExecutionLease2(row.execution_lease).violations);
|
|
7711
7758
|
}
|
|
7712
7759
|
}
|
|
7713
7760
|
}
|
|
7714
7761
|
if (doc.execution_policy !== undefined) {
|
|
7715
|
-
if (!
|
|
7716
|
-
violations.push(
|
|
7762
|
+
if (!isPlainObject32(doc.execution_policy)) {
|
|
7763
|
+
violations.push(violation42("medium", "workflow.snapshot.invalid-execution-policy", "execution_policy must be an object"));
|
|
7717
7764
|
}
|
|
7718
7765
|
}
|
|
7719
7766
|
if (doc.integration_merge_lease !== undefined) {
|
|
7720
7767
|
violations.push(...validateIntegrationMergeLease2(doc.integration_merge_lease).violations);
|
|
7721
7768
|
}
|
|
7722
7769
|
if (doc.branch !== undefined) {
|
|
7723
|
-
if (!
|
|
7724
|
-
violations.push(
|
|
7770
|
+
if (!isPlainObject32(doc.branch)) {
|
|
7771
|
+
violations.push(violation42("medium", "workflow.snapshot.invalid-branch", "branch must be an object"));
|
|
7725
7772
|
} else {
|
|
7726
7773
|
for (const key of ["base", "integration", "target"]) {
|
|
7727
7774
|
if (doc.branch[key] !== undefined && (typeof doc.branch[key] !== "string" || doc.branch[key].trim() === "")) {
|
|
7728
|
-
violations.push(
|
|
7775
|
+
violations.push(violation42("medium", "workflow.snapshot.invalid-branch", `branch.${key} must be a non-empty string`));
|
|
7729
7776
|
}
|
|
7730
7777
|
}
|
|
7731
7778
|
}
|
|
@@ -7733,8 +7780,8 @@ function validateWorkflowSnapshot2(doc) {
|
|
|
7733
7780
|
if (doc.control_worktree_path !== undefined) {
|
|
7734
7781
|
validateNonEmptyString22(violations, doc.control_worktree_path, "control_worktree_path", "workflow.snapshot.missing-control-worktree-path", "workflow.snapshot.invalid-control-worktree-path");
|
|
7735
7782
|
}
|
|
7736
|
-
if (doc.legacy_metadata !== undefined && !
|
|
7737
|
-
violations.push(
|
|
7783
|
+
if (doc.legacy_metadata !== undefined && !isPlainObject32(doc.legacy_metadata)) {
|
|
7784
|
+
violations.push(violation42("medium", "workflow.snapshot.invalid-legacy-metadata", "legacy_metadata must be an object"));
|
|
7738
7785
|
}
|
|
7739
7786
|
if (doc.compass_ref !== undefined) {
|
|
7740
7787
|
validateNonEmptyString22(violations, doc.compass_ref, "compass_ref", "workflow.snapshot.missing-compass-ref", "workflow.snapshot.invalid-compass-ref");
|
|
@@ -7742,17 +7789,17 @@ function validateWorkflowSnapshot2(doc) {
|
|
|
7742
7789
|
const terminal = typeof doc.status === "string" && WORKFLOW_TERMINAL_STATUSES2.includes(doc.status);
|
|
7743
7790
|
if (terminal) {
|
|
7744
7791
|
if (doc.ended_at === undefined) {
|
|
7745
|
-
violations.push(
|
|
7792
|
+
violations.push(violation42("high", "workflow.snapshot.missing-ended-at", `terminal status ${JSON.stringify(doc.status)} requires ended_at \u2014 a terminal snapshot must record when the lifecycle ended`));
|
|
7746
7793
|
}
|
|
7747
7794
|
if (Array.isArray(doc.plans)) {
|
|
7748
7795
|
for (const row of doc.plans) {
|
|
7749
|
-
if (
|
|
7750
|
-
violations.push(
|
|
7796
|
+
if (isPlainObject32(row) && row.execution_lease !== undefined) {
|
|
7797
|
+
violations.push(violation42("high", "workflow.snapshot.terminal-dangling-execution-lease", `terminal snapshot must not carry a row execution_lease (dangling lease) \u2014 release every lease before the lifecycle ends`));
|
|
7751
7798
|
}
|
|
7752
7799
|
}
|
|
7753
7800
|
}
|
|
7754
7801
|
if (doc.integration_merge_lease !== undefined) {
|
|
7755
|
-
violations.push(
|
|
7802
|
+
violations.push(violation42("high", "workflow.snapshot.terminal-dangling-merge-lease", "terminal snapshot must not carry integration_merge_lease (dangling lease) \u2014 release the merge lease before the lifecycle ends"));
|
|
7756
7803
|
}
|
|
7757
7804
|
}
|
|
7758
7805
|
return { ok: violations.length === 0, violations };
|
|
@@ -7763,20 +7810,20 @@ async function writeWorkflowSnapshot(snapshot, dir) {
|
|
|
7763
7810
|
const detail = gate2.violations.map((v) => v.message).join("; ");
|
|
7764
7811
|
throw new Error(`refusing to write invalid workflow snapshot: ${detail}`);
|
|
7765
7812
|
}
|
|
7766
|
-
const snapshotPath =
|
|
7767
|
-
|
|
7813
|
+
const snapshotPath = join5(dir, WORKFLOW_SNAPSHOT_FILE2);
|
|
7814
|
+
mkdirSync32(dir, { recursive: true });
|
|
7768
7815
|
await withStatusWriteLock2(snapshotPath, () => {
|
|
7769
7816
|
writeJson2(snapshotPath, snapshot);
|
|
7770
7817
|
});
|
|
7771
7818
|
}
|
|
7772
|
-
var
|
|
7819
|
+
var DATE_RE22 = /^\d{4}-\d{2}-\d{2}$/;
|
|
7773
7820
|
var PLAN_STATUSES2 = ["Todo", "InProgress", "InReview", "Blocked", "Done"];
|
|
7774
7821
|
var RESIDUAL_DECISIONS = ["defer", "accept", "risk-accepted"];
|
|
7775
7822
|
var RESIDUAL_LIFECYCLES = ["open", "resolved", "waived", "superseded", "duplicate"];
|
|
7776
|
-
function
|
|
7823
|
+
function isPlainObject42(value) {
|
|
7777
7824
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
7778
7825
|
}
|
|
7779
|
-
function
|
|
7826
|
+
function violation52(severity, code, message, fix) {
|
|
7780
7827
|
return { ok: false, severity, code, message, fix };
|
|
7781
7828
|
}
|
|
7782
7829
|
function isOpenResidual2(entry) {
|
|
@@ -7786,19 +7833,19 @@ function isOpenResidual2(entry) {
|
|
|
7786
7833
|
}
|
|
7787
7834
|
function validateNonEmptyString32(violations, value, field, missingCode, invalidCode) {
|
|
7788
7835
|
if (value === undefined) {
|
|
7789
|
-
violations.push(
|
|
7836
|
+
violations.push(violation52("high", missingCode, `missing required field: ${field}`));
|
|
7790
7837
|
} else if (typeof value !== "string" || value.trim() === "") {
|
|
7791
|
-
violations.push(
|
|
7838
|
+
violations.push(violation52("medium", invalidCode, `${field} must be a non-empty string`));
|
|
7792
7839
|
}
|
|
7793
7840
|
}
|
|
7794
7841
|
function validatePlanRow2(row) {
|
|
7795
7842
|
const violations = [];
|
|
7796
|
-
if (!
|
|
7797
|
-
return { ok: false, violations: [
|
|
7843
|
+
if (!isPlainObject42(row)) {
|
|
7844
|
+
return { ok: false, violations: [violation52("high", "status.plan-row.invalid", "plan row must be an object")] };
|
|
7798
7845
|
}
|
|
7799
7846
|
const { id, plan_id: planId, title, file, status, metadata, execution_lease } = row;
|
|
7800
7847
|
if (id === undefined && planId === undefined) {
|
|
7801
|
-
violations.push(
|
|
7848
|
+
violations.push(violation52("high", "status.plan-row.missing-id", "missing required field: id (or legacy plan_id)"));
|
|
7802
7849
|
} else {
|
|
7803
7850
|
if (id !== undefined) {
|
|
7804
7851
|
validateNonEmptyString32(violations, id, "id", "status.plan-row.missing-id", "status.plan-row.invalid-id");
|
|
@@ -7807,31 +7854,31 @@ function validatePlanRow2(row) {
|
|
|
7807
7854
|
validateNonEmptyString32(violations, planId, "plan_id", "status.plan-row.missing-plan-id", "status.plan-row.invalid-plan-id");
|
|
7808
7855
|
}
|
|
7809
7856
|
if (id !== undefined && planId !== undefined && id !== planId) {
|
|
7810
|
-
violations.push(
|
|
7857
|
+
violations.push(violation52("medium", "status.plan-row.dual-id", "row has both id and plan_id with different values \u2014 write one canonical key (prefer id)"));
|
|
7811
7858
|
}
|
|
7812
7859
|
}
|
|
7813
7860
|
validateNonEmptyString32(violations, title, "title", "status.plan-row.missing-title", "status.plan-row.invalid-title");
|
|
7814
7861
|
validateNonEmptyString32(violations, file, "file", "status.plan-row.missing-file", "status.plan-row.invalid-file");
|
|
7815
7862
|
if (status === undefined) {
|
|
7816
|
-
violations.push(
|
|
7863
|
+
violations.push(violation52("high", "status.plan-row.missing-status", "missing required field: status"));
|
|
7817
7864
|
} else if (typeof status !== "string" || !PLAN_STATUSES2.includes(status)) {
|
|
7818
|
-
violations.push(
|
|
7865
|
+
violations.push(violation52("medium", "status.plan-row.invalid-status", `status must be one of ${PLAN_STATUSES2.join(" | ")} \u2014 got ${JSON.stringify(status)}`));
|
|
7819
7866
|
}
|
|
7820
|
-
if (metadata !== undefined && !
|
|
7821
|
-
violations.push(
|
|
7867
|
+
if (metadata !== undefined && !isPlainObject42(metadata)) {
|
|
7868
|
+
violations.push(violation52("medium", "status.plan-row.invalid-metadata", "metadata must be an object"));
|
|
7822
7869
|
}
|
|
7823
|
-
if (execution_lease !== undefined && !
|
|
7824
|
-
violations.push(
|
|
7870
|
+
if (execution_lease !== undefined && !isPlainObject42(execution_lease)) {
|
|
7871
|
+
violations.push(violation52("medium", "status.plan-row.invalid-execution-lease", "execution_lease must be an object"));
|
|
7825
7872
|
}
|
|
7826
7873
|
if (status === "Done" && execution_lease !== undefined) {
|
|
7827
|
-
violations.push(
|
|
7874
|
+
violations.push(violation52("medium", "status.plan-row.done-with-lease", 'plan status Done must not carry an execution_lease \u2014 the Done authority deletes the lease in the same complete-file update as status: "Done" (status-and-residuals.md \u00a7 Hold, release, and override)', 'delete plans[].execution_lease in the same update that sets status: "Done"'));
|
|
7828
7875
|
}
|
|
7829
7876
|
return { ok: violations.length === 0, violations };
|
|
7830
7877
|
}
|
|
7831
7878
|
function validateResidual(entry) {
|
|
7832
7879
|
const violations = [];
|
|
7833
|
-
if (!
|
|
7834
|
-
return { ok: false, violations: [
|
|
7880
|
+
if (!isPlainObject42(entry)) {
|
|
7881
|
+
return { ok: false, violations: [violation52("high", "status.residual.invalid", "residual entry must be an object")] };
|
|
7835
7882
|
}
|
|
7836
7883
|
const { id, title, severity, source, scope, decision, owner, target, tracking, detail_doc, lifecycle, closed_at } = entry;
|
|
7837
7884
|
validateNonEmptyString32(violations, id, "id", "status.residual.missing-id", "status.residual.invalid-id");
|
|
@@ -7840,42 +7887,42 @@ function validateResidual(entry) {
|
|
|
7840
7887
|
validateNonEmptyString32(violations, scope, "scope", "status.residual.missing-scope", "status.residual.invalid-scope");
|
|
7841
7888
|
validateNonEmptyString32(violations, owner, "owner", "status.residual.missing-owner", "status.residual.invalid-owner");
|
|
7842
7889
|
if (severity === undefined) {
|
|
7843
|
-
violations.push(
|
|
7890
|
+
violations.push(violation52("high", "status.residual.missing-severity", "missing required field: severity"));
|
|
7844
7891
|
} else if (typeof severity !== "string" || !SEVERITY_ORDER2.includes(severity) && severity !== "warning") {
|
|
7845
|
-
violations.push(
|
|
7892
|
+
violations.push(violation52("medium", "status.residual.invalid-severity", `severity must be one of ${SEVERITY_ORDER2.join(" | ")} \u2014 got ${JSON.stringify(severity)}`));
|
|
7846
7893
|
} else if (severity === "warning") {
|
|
7847
|
-
violations.push(
|
|
7894
|
+
violations.push(violation52("low", "status.residual.legacy-warning", `severity "warning" is legacy \u2014 forbidden on new entries; read paths normalize it to "low"`, `use "low" (normalizeSeverity maps 'warning' \u2192 'low')`));
|
|
7848
7895
|
}
|
|
7849
7896
|
if (decision === undefined) {
|
|
7850
|
-
violations.push(
|
|
7897
|
+
violations.push(violation52("high", "status.residual.missing-decision", "missing required field: decision"));
|
|
7851
7898
|
} else if (typeof decision !== "string" || !RESIDUAL_DECISIONS.includes(decision)) {
|
|
7852
|
-
violations.push(
|
|
7899
|
+
violations.push(violation52("medium", "status.residual.invalid-decision", `decision must be one of ${RESIDUAL_DECISIONS.join(" | ")} \u2014 got ${JSON.stringify(decision)}`));
|
|
7853
7900
|
}
|
|
7854
7901
|
if (target === undefined) {
|
|
7855
|
-
violations.push(
|
|
7902
|
+
violations.push(violation52("high", "status.residual.missing-target", "missing required field: target"));
|
|
7856
7903
|
} else if (typeof target !== "string" && target !== null) {
|
|
7857
|
-
violations.push(
|
|
7904
|
+
violations.push(violation52("medium", "status.residual.invalid-target", "target must be a string or null"));
|
|
7858
7905
|
}
|
|
7859
7906
|
if (tracking === undefined) {
|
|
7860
|
-
violations.push(
|
|
7907
|
+
violations.push(violation52("high", "status.residual.missing-tracking", "missing required field: tracking"));
|
|
7861
7908
|
} else if (typeof tracking !== "string" && tracking !== null) {
|
|
7862
|
-
violations.push(
|
|
7909
|
+
violations.push(violation52("medium", "status.residual.invalid-tracking", "tracking must be a string or null"));
|
|
7863
7910
|
}
|
|
7864
7911
|
if (detail_doc !== undefined && typeof detail_doc !== "string" && detail_doc !== null) {
|
|
7865
|
-
violations.push(
|
|
7912
|
+
violations.push(violation52("medium", "status.residual.invalid-detail-doc", "detail_doc must be a string or null"));
|
|
7866
7913
|
}
|
|
7867
|
-
if (closed_at !== undefined && (typeof closed_at !== "string" || !
|
|
7868
|
-
violations.push(
|
|
7914
|
+
if (closed_at !== undefined && (typeof closed_at !== "string" || !DATE_RE22.test(closed_at))) {
|
|
7915
|
+
violations.push(violation52("medium", "status.residual.invalid-closed-at", "closed_at must be YYYY-MM-DD"));
|
|
7869
7916
|
}
|
|
7870
7917
|
if (lifecycle !== undefined) {
|
|
7871
7918
|
if (typeof lifecycle !== "string" || !RESIDUAL_LIFECYCLES.includes(lifecycle)) {
|
|
7872
|
-
violations.push(
|
|
7919
|
+
violations.push(violation52("medium", "status.residual.invalid-lifecycle", `lifecycle must be one of ${RESIDUAL_LIFECYCLES.join(" | ")} \u2014 got ${JSON.stringify(lifecycle)}`));
|
|
7873
7920
|
} else if (lifecycle !== "open") {
|
|
7874
7921
|
if (closed_at === undefined) {
|
|
7875
|
-
violations.push(
|
|
7922
|
+
violations.push(violation52("high", "status.residual.closed-missing-closed-at", `lifecycle "${lifecycle}" requires closed_at (YYYY-MM-DD)`, 'set closed_at (e.g. "2026-08-08")'));
|
|
7876
7923
|
}
|
|
7877
7924
|
if (entry.closure_note === undefined) {
|
|
7878
|
-
violations.push(
|
|
7925
|
+
violations.push(violation52("medium", "status.residual.closed-missing-closure-note", `lifecycle "${lifecycle}" requires closure_note (what changed; how verified)`, "add closure_note explaining the close"));
|
|
7879
7926
|
}
|
|
7880
7927
|
}
|
|
7881
7928
|
}
|
|
@@ -7890,25 +7937,25 @@ function isHarnessRelativePath2(dir) {
|
|
|
7890
7937
|
}
|
|
7891
7938
|
function validateWorkflowEntry2(entry) {
|
|
7892
7939
|
const violations = [];
|
|
7893
|
-
if (!
|
|
7940
|
+
if (!isPlainObject42(entry)) {
|
|
7894
7941
|
return {
|
|
7895
7942
|
ok: false,
|
|
7896
|
-
violations: [
|
|
7943
|
+
violations: [violation52("high", "status.workflow.invalid", "workflow entry must be an object")]
|
|
7897
7944
|
};
|
|
7898
7945
|
}
|
|
7899
7946
|
validateNonEmptyString32(violations, entry.id, "id", "status.workflow.missing-id", "status.workflow.invalid-id");
|
|
7900
7947
|
if (entry.type === undefined) {
|
|
7901
|
-
violations.push(
|
|
7948
|
+
violations.push(violation52("high", "status.workflow.missing-type", "missing required field: type"));
|
|
7902
7949
|
} else if (typeof entry.type !== "string" || !WORKFLOW_LIFECYCLE_TYPES2.includes(entry.type)) {
|
|
7903
|
-
violations.push(
|
|
7950
|
+
violations.push(violation52("medium", "status.workflow.invalid-type", `type must be one of ${WORKFLOW_LIFECYCLE_TYPES2.join(" | ")} \u2014 got ${JSON.stringify(entry.type)}`));
|
|
7904
7951
|
}
|
|
7905
7952
|
validateNonEmptyString32(violations, entry.started_at, "started_at", "status.workflow.missing-started-at", "status.workflow.invalid-started-at");
|
|
7906
7953
|
if (entry.dir === undefined) {
|
|
7907
|
-
violations.push(
|
|
7954
|
+
violations.push(violation52("high", "status.workflow.missing-dir", "missing required field: dir"));
|
|
7908
7955
|
} else if (typeof entry.dir !== "string" || entry.dir.trim() === "") {
|
|
7909
|
-
violations.push(
|
|
7956
|
+
violations.push(violation52("medium", "status.workflow.invalid-dir", "dir must be a non-empty string"));
|
|
7910
7957
|
} else if (!isHarnessRelativePath2(entry.dir)) {
|
|
7911
|
-
violations.push(
|
|
7958
|
+
violations.push(violation52("medium", "status.workflow.invalid-dir", `dir must be a harness-relative path (no absolute paths, no ".." segments) \u2014 got ${JSON.stringify(entry.dir)}`));
|
|
7912
7959
|
}
|
|
7913
7960
|
return { ok: violations.length === 0, violations };
|
|
7914
7961
|
}
|
|
@@ -7918,24 +7965,24 @@ function validateStatusV22(docOrPath, opts = {}) {
|
|
|
7918
7965
|
if (typeof docOrPath === "string") {
|
|
7919
7966
|
try {
|
|
7920
7967
|
doc = readJson2(docOrPath);
|
|
7921
|
-
harnessDir =
|
|
7968
|
+
harnessDir = dirname42(resolve42(docOrPath));
|
|
7922
7969
|
} catch (error) {
|
|
7923
7970
|
return {
|
|
7924
7971
|
ok: false,
|
|
7925
|
-
violations: [
|
|
7972
|
+
violations: [violation52("high", "status.invalid-json", error.message)]
|
|
7926
7973
|
};
|
|
7927
7974
|
}
|
|
7928
7975
|
} else {
|
|
7929
7976
|
doc = docOrPath;
|
|
7930
7977
|
}
|
|
7931
|
-
if (!
|
|
7932
|
-
return { ok: false, violations: [
|
|
7978
|
+
if (!isPlainObject42(doc)) {
|
|
7979
|
+
return { ok: false, violations: [violation52("high", "status.invalid-doc", "status document must be an object")] };
|
|
7933
7980
|
}
|
|
7934
7981
|
if (doc.version !== 2) {
|
|
7935
7982
|
return {
|
|
7936
7983
|
ok: false,
|
|
7937
7984
|
violations: [
|
|
7938
|
-
|
|
7985
|
+
violation52("high", "status.migration-required", `status.json schema version 2 required \u2014 got ${JSON.stringify(doc.version)} (v1 or unknown version); run \`mstar migrate\` to convert the tree`, "run `mstar migrate`")
|
|
7939
7986
|
]
|
|
7940
7987
|
};
|
|
7941
7988
|
}
|
|
@@ -7943,7 +7990,7 @@ function validateStatusV22(docOrPath, opts = {}) {
|
|
|
7943
7990
|
return {
|
|
7944
7991
|
ok: false,
|
|
7945
7992
|
violations: [
|
|
7946
|
-
|
|
7993
|
+
violation52("high", "status.migration-required", "v1-shaped status.json (root plans[]) is not a v2 document \u2014 run `mstar migrate` to convert the tree", "run `mstar migrate`")
|
|
7947
7994
|
]
|
|
7948
7995
|
};
|
|
7949
7996
|
}
|
|
@@ -7951,27 +7998,27 @@ function validateStatusV22(docOrPath, opts = {}) {
|
|
|
7951
7998
|
return {
|
|
7952
7999
|
ok: false,
|
|
7953
8000
|
violations: [
|
|
7954
|
-
|
|
8001
|
+
violation52("high", "status.migration-required", "v1-shaped status.json (root residual_findings) is not a v2 document \u2014 run `mstar migrate` to convert the tree", "run `mstar migrate`")
|
|
7955
8002
|
]
|
|
7956
8003
|
};
|
|
7957
8004
|
}
|
|
7958
8005
|
const violations = [];
|
|
7959
8006
|
if (doc.updated_at === undefined) {
|
|
7960
|
-
violations.push(
|
|
7961
|
-
} else if (typeof doc.updated_at !== "string" || !
|
|
7962
|
-
violations.push(
|
|
8007
|
+
violations.push(violation52("high", "status.missing-updated-at", "missing required field: updated_at"));
|
|
8008
|
+
} else if (typeof doc.updated_at !== "string" || !DATE_RE22.test(doc.updated_at)) {
|
|
8009
|
+
violations.push(violation52("medium", "status.invalid-updated-at", "updated_at must be YYYY-MM-DD"));
|
|
7963
8010
|
}
|
|
7964
8011
|
if (doc.workflows === undefined) {
|
|
7965
|
-
violations.push(
|
|
8012
|
+
violations.push(violation52("high", "status.missing-workflows", "missing required field: workflows"));
|
|
7966
8013
|
} else if (!Array.isArray(doc.workflows)) {
|
|
7967
|
-
violations.push(
|
|
8014
|
+
violations.push(violation52("high", "status.invalid-workflows", "workflows must be an array"));
|
|
7968
8015
|
} else {
|
|
7969
8016
|
const seen = new Set;
|
|
7970
8017
|
for (const entry of doc.workflows) {
|
|
7971
8018
|
violations.push(...validateWorkflowEntry2(entry).violations);
|
|
7972
|
-
if (
|
|
8019
|
+
if (isPlainObject42(entry) && typeof entry.id === "string") {
|
|
7973
8020
|
if (seen.has(entry.id)) {
|
|
7974
|
-
violations.push(
|
|
8021
|
+
violations.push(violation52("medium", "status.workflow.duplicate-id", `duplicate workflow id in workflows[]: ${JSON.stringify(entry.id)}`));
|
|
7975
8022
|
}
|
|
7976
8023
|
seen.add(entry.id);
|
|
7977
8024
|
}
|
|
@@ -7980,122 +8027,60 @@ function validateStatusV22(docOrPath, opts = {}) {
|
|
|
7980
8027
|
if (harnessDir !== undefined && Array.isArray(doc.workflows)) {
|
|
7981
8028
|
let realHarnessDir = null;
|
|
7982
8029
|
try {
|
|
7983
|
-
realHarnessDir =
|
|
8030
|
+
realHarnessDir = realpathSync4(harnessDir);
|
|
7984
8031
|
} catch {}
|
|
7985
8032
|
for (const entry of doc.workflows) {
|
|
7986
|
-
if (!
|
|
8033
|
+
if (!isPlainObject42(entry) || typeof entry.dir !== "string")
|
|
7987
8034
|
continue;
|
|
7988
8035
|
const relSnapshot = join62(entry.dir, WORKFLOW_SNAPSHOT_FILE2);
|
|
7989
8036
|
const snapshotPath = join62(harnessDir, relSnapshot);
|
|
7990
8037
|
const label = typeof entry.id === "string" ? entry.id : relSnapshot;
|
|
7991
8038
|
let physical;
|
|
7992
8039
|
try {
|
|
7993
|
-
physical =
|
|
8040
|
+
physical = realpathSync4(snapshotPath);
|
|
7994
8041
|
} catch {
|
|
7995
|
-
violations.push(
|
|
8042
|
+
violations.push(violation52("high", "status.workflow.snapshot-missing", `workflows[] lists ${JSON.stringify(label)} but its snapshot does not exist at ${JSON.stringify(relSnapshot)} \u2014 the root holds active lifecycles only; unregister the id when its snapshot is removed`));
|
|
7996
8043
|
continue;
|
|
7997
8044
|
}
|
|
7998
8045
|
if (realHarnessDir !== null && physical !== realHarnessDir && !physical.startsWith(`${realHarnessDir}${sep2}`)) {
|
|
7999
|
-
violations.push(
|
|
8046
|
+
violations.push(violation52("high", "status.workflow.snapshot-outside-harness", `workflows[] lists ${JSON.stringify(label)} but its snapshot resolves outside the harness dir (${JSON.stringify(physical)}) \u2014 symlinked snapshot paths are rejected; the snapshot must physically live under ${JSON.stringify(harnessDir)}`));
|
|
8000
8047
|
continue;
|
|
8001
8048
|
}
|
|
8002
8049
|
let snapshot;
|
|
8003
8050
|
try {
|
|
8004
8051
|
snapshot = readJson2(snapshotPath);
|
|
8005
8052
|
} catch (error) {
|
|
8006
|
-
violations.push(
|
|
8053
|
+
violations.push(violation52("high", "status.workflow.snapshot-invalid", `snapshot at ${JSON.stringify(relSnapshot)} is not valid JSON: ${error.message}`));
|
|
8007
8054
|
continue;
|
|
8008
8055
|
}
|
|
8009
8056
|
if (typeof snapshot.status === "string" && WORKFLOW_TERMINAL_STATUSES2.includes(snapshot.status)) {
|
|
8010
|
-
violations.push(
|
|
8057
|
+
violations.push(violation52("high", "status.workflow.terminal-listed", `workflows[] lists ${JSON.stringify(label)} whose snapshot status is terminal (${snapshot.status}) \u2014 removal-at-terminal: terminal writers unregister AFTER the snapshot write`));
|
|
8011
8058
|
}
|
|
8012
8059
|
if (typeof entry.type === "string" && typeof snapshot.type === "string" && entry.type !== snapshot.type) {
|
|
8013
|
-
violations.push(
|
|
8060
|
+
violations.push(violation52("medium", "status.workflow.mismatched-type", `workflows[] entry ${JSON.stringify(label)} type ${JSON.stringify(entry.type)} does not match its snapshot type ${JSON.stringify(snapshot.type)} \u2014 the root entry mirrors the snapshot; align them`));
|
|
8014
8061
|
}
|
|
8015
8062
|
if (typeof entry.started_at === "string" && typeof snapshot.started_at === "string" && entry.started_at !== snapshot.started_at) {
|
|
8016
|
-
violations.push(
|
|
8063
|
+
violations.push(violation52("medium", "status.workflow.mismatched-started-at", `workflows[] entry ${JSON.stringify(label)} started_at ${JSON.stringify(entry.started_at)} does not match its snapshot started_at ${JSON.stringify(snapshot.started_at)} \u2014 workflow ${JSON.stringify(label)} collided with another writer (e.g. a concurrent/re-run \`audit promote\` with the same workflow id rewrote the snapshot); the root entry mirrors the snapshot \u2014 align them or remove the colliding workflow`));
|
|
8017
8064
|
}
|
|
8018
8065
|
}
|
|
8019
8066
|
}
|
|
8020
8067
|
return { ok: violations.length === 0, violations };
|
|
8021
8068
|
}
|
|
8022
|
-
var
|
|
8023
|
-
function parseCompassFrontmatterText2(content, filePath) {
|
|
8024
|
-
const lines = content.split(/\r?\n/);
|
|
8025
|
-
if (lines[0]?.trim() !== "---") {
|
|
8026
|
-
throw new Error(`no YAML frontmatter fence in ${filePath} (expected first line "---")`);
|
|
8027
|
-
}
|
|
8028
|
-
const end = lines.indexOf("---", 1);
|
|
8029
|
-
if (end === -1) {
|
|
8030
|
-
throw new Error(`unterminated YAML frontmatter in ${filePath} (no closing "---")`);
|
|
8031
|
-
}
|
|
8032
|
-
const doc = {};
|
|
8033
|
-
let listKey = null;
|
|
8034
|
-
for (let i = 1;i < end; i += 1) {
|
|
8035
|
-
const line = lines[i] ?? "";
|
|
8036
|
-
if (!line.trim() || line.trim().startsWith("#"))
|
|
8037
|
-
continue;
|
|
8038
|
-
if (listKey !== null && /^\s*-\s+/.test(line)) {
|
|
8039
|
-
const item = line.replace(/^\s*-\s+/, "").trim().replace(/^["']|["']$/g, "");
|
|
8040
|
-
if (!Array.isArray(doc[listKey]))
|
|
8041
|
-
doc[listKey] = [];
|
|
8042
|
-
doc[listKey].push(item);
|
|
8043
|
-
continue;
|
|
8044
|
-
}
|
|
8045
|
-
listKey = null;
|
|
8046
|
-
const kv = line.match(/^([A-Za-z_][A-Za-z0-9_-]*):\s*(.*)$/);
|
|
8047
|
-
if (!kv) {
|
|
8048
|
-
throw new Error(`unsupported frontmatter line in ${filePath}: ${JSON.stringify(line)}`);
|
|
8049
|
-
}
|
|
8050
|
-
const value = kv[2].trim();
|
|
8051
|
-
doc[kv[1]] = value === "" ? null : /^\[.*\]$/.test(value) ? parseFlowArray2(value, filePath) : value.replace(/^["']|["']$/g, "");
|
|
8052
|
-
listKey = value === "" ? kv[1] : null;
|
|
8053
|
-
}
|
|
8054
|
-
return doc;
|
|
8055
|
-
}
|
|
8056
|
-
function parseFlowArray2(raw, filePath) {
|
|
8057
|
-
const inner = raw.slice(1, -1);
|
|
8058
|
-
if (/[[\]]/.test(inner)) {
|
|
8059
|
-
throw new Error(`nested flow-style array in ${filePath}: ${JSON.stringify(raw)} \u2014 only flat scalar items are supported (e.g. [a, b])`);
|
|
8060
|
-
}
|
|
8061
|
-
let quote = null;
|
|
8062
|
-
for (const ch of inner) {
|
|
8063
|
-
if (ch === '"' || ch === "'") {
|
|
8064
|
-
if (quote === null)
|
|
8065
|
-
quote = ch;
|
|
8066
|
-
else if (quote === ch)
|
|
8067
|
-
quote = null;
|
|
8068
|
-
} else if (ch === "," && quote !== null) {
|
|
8069
|
-
throw new Error(`ambiguous flow-style array in ${filePath}: ${JSON.stringify(raw)} \u2014 quoted item containing comma cannot be split unambiguously (flat scalar items only)`);
|
|
8070
|
-
}
|
|
8071
|
-
}
|
|
8072
|
-
if (quote !== null) {
|
|
8073
|
-
throw new Error(`unterminated ${quote} quote in flow-style array in ${filePath}: ${JSON.stringify(raw)}`);
|
|
8074
|
-
}
|
|
8075
|
-
const items = [];
|
|
8076
|
-
for (const part of inner.split(",")) {
|
|
8077
|
-
const item = part.trim().replace(/^["']|["']$/g, "");
|
|
8078
|
-
if (item === "")
|
|
8079
|
-
continue;
|
|
8080
|
-
items.push(item);
|
|
8081
|
-
}
|
|
8082
|
-
return items;
|
|
8083
|
-
}
|
|
8084
|
-
var PROJECT_ROADMAP_FILE = "roadmap.md";
|
|
8069
|
+
var PROJECT_ROADMAP_FILE2 = "roadmap.md";
|
|
8085
8070
|
var PROJECT_REGISTER_FILE2 = "residuals.json";
|
|
8086
8071
|
var _DEFAULT_PROJECT2 = "_default";
|
|
8087
|
-
var
|
|
8072
|
+
var DATE_RE3 = /^\d{4}-\d{2}-\d{2}$/;
|
|
8088
8073
|
function isPlainObject52(value) {
|
|
8089
8074
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
8090
8075
|
}
|
|
8091
|
-
function
|
|
8076
|
+
function violation62(severity, code, message, fix) {
|
|
8092
8077
|
return { ok: false, severity, code, message, fix };
|
|
8093
8078
|
}
|
|
8094
8079
|
function validateNonEmptyString42(violations, value, field, missingCode, invalidCode) {
|
|
8095
8080
|
if (value === undefined) {
|
|
8096
|
-
violations.push(
|
|
8081
|
+
violations.push(violation62("high", missingCode, `missing required field: ${field}`));
|
|
8097
8082
|
} else if (typeof value !== "string" || value.trim() === "") {
|
|
8098
|
-
violations.push(
|
|
8083
|
+
violations.push(violation62("medium", invalidCode, `${field} must be a non-empty string`));
|
|
8099
8084
|
}
|
|
8100
8085
|
}
|
|
8101
8086
|
function validateProjectRegister(doc) {
|
|
@@ -8103,20 +8088,20 @@ function validateProjectRegister(doc) {
|
|
|
8103
8088
|
if (!isPlainObject52(doc)) {
|
|
8104
8089
|
return {
|
|
8105
8090
|
ok: false,
|
|
8106
|
-
violations: [
|
|
8091
|
+
violations: [violation62("high", "project.register.invalid", "project register must be an object")]
|
|
8107
8092
|
};
|
|
8108
8093
|
}
|
|
8109
8094
|
if (doc.entries === undefined) {
|
|
8110
|
-
violations.push(
|
|
8095
|
+
violations.push(violation62("high", "project.register.missing-entries", "missing required field: entries"));
|
|
8111
8096
|
} else if (!isPlainObject52(doc.entries)) {
|
|
8112
|
-
violations.push(
|
|
8097
|
+
violations.push(violation62("high", "project.register.invalid-entries", "entries must be an object keyed by plan id"));
|
|
8113
8098
|
} else {
|
|
8114
8099
|
for (const [key, entries] of Object.entries(doc.entries)) {
|
|
8115
8100
|
if (key.trim() === "") {
|
|
8116
|
-
violations.push(
|
|
8101
|
+
violations.push(violation62("medium", "project.register.invalid-key", "entries keys must be non-empty plan ids"));
|
|
8117
8102
|
}
|
|
8118
8103
|
if (!Array.isArray(entries)) {
|
|
8119
|
-
violations.push(
|
|
8104
|
+
violations.push(violation62("high", "project.register.invalid-entry-list", `entries[${JSON.stringify(key)}] must be an array of residual entries (one entry per residual; v1 multi-finding semantics)`));
|
|
8120
8105
|
continue;
|
|
8121
8106
|
}
|
|
8122
8107
|
for (const entry of entries) {
|
|
@@ -8125,21 +8110,126 @@ function validateProjectRegister(doc) {
|
|
|
8125
8110
|
continue;
|
|
8126
8111
|
validateNonEmptyString42(violations, entry.source_plan, "source_plan", "project.register.missing-source-plan", "project.register.invalid-source-plan");
|
|
8127
8112
|
if (entry.registered_at === undefined) {
|
|
8128
|
-
violations.push(
|
|
8129
|
-
} else if (typeof entry.registered_at !== "string" || !
|
|
8130
|
-
violations.push(
|
|
8113
|
+
violations.push(violation62("high", "project.register.missing-registered-at", "missing required field: registered_at"));
|
|
8114
|
+
} else if (typeof entry.registered_at !== "string" || !DATE_RE3.test(entry.registered_at)) {
|
|
8115
|
+
violations.push(violation62("medium", "project.register.invalid-registered-at", "registered_at must be YYYY-MM-DD"));
|
|
8131
8116
|
}
|
|
8132
8117
|
if (entry.lifecycle_id !== undefined && (typeof entry.lifecycle_id !== "string" || entry.lifecycle_id.trim() === "")) {
|
|
8133
|
-
violations.push(
|
|
8118
|
+
violations.push(violation62("medium", "project.register.invalid-lifecycle-id", "lifecycle_id must be a non-empty string"));
|
|
8134
8119
|
}
|
|
8135
8120
|
if (typeof entry.source_plan === "string" && entry.source_plan.trim() !== "" && entry.source_plan !== key) {
|
|
8136
|
-
violations.push(
|
|
8121
|
+
violations.push(violation62("medium", "project.register.mismatched-source-plan", `source_plan ${JSON.stringify(entry.source_plan)} does not match the entries key ${JSON.stringify(key)} \u2014 entries are keyed by plan id`));
|
|
8137
8122
|
}
|
|
8138
8123
|
}
|
|
8139
8124
|
}
|
|
8140
8125
|
}
|
|
8141
8126
|
return { ok: violations.length === 0, violations };
|
|
8142
8127
|
}
|
|
8128
|
+
function resolveHarnessDir2(startDir = process.cwd(), opts = {}) {
|
|
8129
|
+
const start = resolve52(startDir);
|
|
8130
|
+
const explicit = opts.harnessDir ?? process.env.MSTAR_HARNESS_DIR;
|
|
8131
|
+
if (explicit)
|
|
8132
|
+
return resolve52(start, explicit);
|
|
8133
|
+
const boundary = resolve52(start, opts.workspaceRoot ?? defaultWorkspaceRoot2(start));
|
|
8134
|
+
const rc = loadMstarc2(start, boundary);
|
|
8135
|
+
if (rc !== null && rc.config.harnessDir)
|
|
8136
|
+
return resolve52(rc.dir, rc.config.harnessDir);
|
|
8137
|
+
let dir = start;
|
|
8138
|
+
for (;; ) {
|
|
8139
|
+
if (!isAtOrBelow22(dir, boundary))
|
|
8140
|
+
return null;
|
|
8141
|
+
for (const candidate of [join82(dir, ".mstar"), join82(dir, ".agents"), join82(dir, ".plans"), join82(dir, "plans")]) {
|
|
8142
|
+
if (isDirectory3(candidate))
|
|
8143
|
+
return candidate;
|
|
8144
|
+
}
|
|
8145
|
+
if (dir === boundary)
|
|
8146
|
+
return null;
|
|
8147
|
+
const parent = dirname52(dir);
|
|
8148
|
+
if (parent === dir)
|
|
8149
|
+
return null;
|
|
8150
|
+
dir = parent;
|
|
8151
|
+
}
|
|
8152
|
+
}
|
|
8153
|
+
function defaultWorkspaceRoot2(startDir) {
|
|
8154
|
+
try {
|
|
8155
|
+
const cdup = execFileSync4("git", ["rev-parse", "--show-cdup"], {
|
|
8156
|
+
cwd: startDir,
|
|
8157
|
+
encoding: "utf8",
|
|
8158
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
8159
|
+
}).trim();
|
|
8160
|
+
if (!cdup)
|
|
8161
|
+
return startDir;
|
|
8162
|
+
let boundary = startDir;
|
|
8163
|
+
for (const segment of cdup.split(/[\\/]/)) {
|
|
8164
|
+
if (segment && segment !== ".")
|
|
8165
|
+
boundary = dirname52(boundary);
|
|
8166
|
+
}
|
|
8167
|
+
return resolve52(boundary);
|
|
8168
|
+
} catch {}
|
|
8169
|
+
return startDir;
|
|
8170
|
+
}
|
|
8171
|
+
function isAtOrBelow22(dir, root) {
|
|
8172
|
+
const rel = relative22(root, dir);
|
|
8173
|
+
return rel === "" || !rel.startsWith("..") && !isAbsolute32(rel);
|
|
8174
|
+
}
|
|
8175
|
+
function mstarcDirOverride2(harnessDir, key) {
|
|
8176
|
+
const dir = resolve52(harnessDir);
|
|
8177
|
+
const rc = loadMstarc2(dir, dirname52(dir));
|
|
8178
|
+
const declared = rc?.config[key];
|
|
8179
|
+
return declared ? resolve52(rc.dir, declared) : null;
|
|
8180
|
+
}
|
|
8181
|
+
function assertSafePathComponent2(value, what) {
|
|
8182
|
+
if (value === "" || value === "." || value === ".." || !/^[A-Za-z0-9._-]+$/.test(value)) {
|
|
8183
|
+
throw new Error(`${what} must be a single safe path component ([A-Za-z0-9._-]+; not "", ".", "..", or containing "/" or "\\") \u2014 got ${JSON.stringify(value)}`);
|
|
8184
|
+
}
|
|
8185
|
+
}
|
|
8186
|
+
function resolveHarnessSubdir2(startDir, opts, key, fallback) {
|
|
8187
|
+
const harness = resolveHarnessDir2(startDir, opts);
|
|
8188
|
+
if (harness === null) {
|
|
8189
|
+
throw new Error(`harness dir not found from ${resolve52(startDir)} \u2014 cannot resolve the ${fallback} dir (run \`mstar harness scaffold\`, pass opts.harnessDir, or set MSTAR_HARNESS_DIR)`);
|
|
8190
|
+
}
|
|
8191
|
+
const declared = mstarcDirOverride2(harness, key);
|
|
8192
|
+
return declared !== null ? declared : join82(resolve52(harness), fallback);
|
|
8193
|
+
}
|
|
8194
|
+
function resolveWorkflowDir2(startDir = process.cwd(), opts = {}) {
|
|
8195
|
+
return resolveHarnessSubdir2(startDir, opts, "workflowDir", "workflows");
|
|
8196
|
+
}
|
|
8197
|
+
function resolveProjectDir2(startDir = process.cwd(), opts = {}) {
|
|
8198
|
+
return resolveHarnessSubdir2(startDir, opts, "projectDir", "projects");
|
|
8199
|
+
}
|
|
8200
|
+
var GITIGNORE_SNIPPET2 = `# Morning Star harness (.mstar/)
|
|
8201
|
+
# Principle: process stays local; results are shared with the team.
|
|
8202
|
+
# Default-ignore everything under .mstar/, then re-include the tracked results.
|
|
8203
|
+
.mstar/**
|
|
8204
|
+
!.mstar/AGENTS.md
|
|
8205
|
+
!.mstar/knowledge/
|
|
8206
|
+
!.mstar/knowledge/**
|
|
8207
|
+
!.mstar/specs/
|
|
8208
|
+
!.mstar/specs/**
|
|
8209
|
+
# .mstarc \u2014 repo-local harness config (may declare [config] harness_dir=<name>)
|
|
8210
|
+
.mstarc
|
|
8211
|
+
`;
|
|
8212
|
+
var GITIGNORE_SNIPPET_AGENTS2 = `# Morning Star harness (.agents/) \u2014 legacy
|
|
8213
|
+
# Default-ignore everything under .agents/, then re-include the tracked results.
|
|
8214
|
+
.agents/**
|
|
8215
|
+
!.agents/AGENTS.md
|
|
8216
|
+
!.agents/knowledge/
|
|
8217
|
+
!.agents/knowledge/**
|
|
8218
|
+
!.agents/specs/
|
|
8219
|
+
!.agents/specs/**
|
|
8220
|
+
`;
|
|
8221
|
+
var GITIGNORE_PROCESS_ENTRIES2 = GITIGNORE_SNIPPET2.split(`
|
|
8222
|
+
`).filter((line) => line.startsWith(".mstar/") || line.startsWith("!.mstar/")).map((line) => line.trim());
|
|
8223
|
+
var GITIGNORE_PROCESS_ENTRIES_AGENTS2 = GITIGNORE_SNIPPET_AGENTS2.split(`
|
|
8224
|
+
`).filter((line) => line.startsWith(".agents/") || line.startsWith("!.agents/")).map((line) => line.trim());
|
|
8225
|
+
function isDirectory3(dir) {
|
|
8226
|
+
try {
|
|
8227
|
+
return statSync32(dir).isDirectory();
|
|
8228
|
+
} catch {
|
|
8229
|
+
return false;
|
|
8230
|
+
}
|
|
8231
|
+
}
|
|
8232
|
+
var GIT_CAPTURE_MAX_BYTES2 = 64 * 1024 * 1024;
|
|
8143
8233
|
var MIGRATE_STATUS_FILE = "status.json";
|
|
8144
8234
|
var ARCHIVED_STATUS_V1_FILE = "archived/status.v1.json";
|
|
8145
8235
|
var NOTES_LEDGER_FILE = "notes.jsonl";
|
|
@@ -8156,7 +8246,7 @@ var ROOT_METADATA_LIFT_KEYS = {
|
|
|
8156
8246
|
program_roadmap: true,
|
|
8157
8247
|
updated_at: true
|
|
8158
8248
|
};
|
|
8159
|
-
function
|
|
8249
|
+
function isPlainObject6(value) {
|
|
8160
8250
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
8161
8251
|
}
|
|
8162
8252
|
function dateString(value) {
|
|
@@ -8183,7 +8273,7 @@ function scanCompasses(harnessDir) {
|
|
|
8183
8273
|
const out = [];
|
|
8184
8274
|
let entries;
|
|
8185
8275
|
try {
|
|
8186
|
-
entries =
|
|
8276
|
+
entries = readdirSync6(iterationsDir, { withFileTypes: true });
|
|
8187
8277
|
} catch {
|
|
8188
8278
|
return out;
|
|
8189
8279
|
}
|
|
@@ -8336,10 +8426,10 @@ function applyRootMetadataLift(snapshot, metadata, migrationNotes, activeIterati
|
|
|
8336
8426
|
if (typeof metadata.control_worktree_path === "string" && metadata.control_worktree_path !== "") {
|
|
8337
8427
|
data.control_worktree_path = metadata.control_worktree_path;
|
|
8338
8428
|
}
|
|
8339
|
-
if (
|
|
8429
|
+
if (isPlainObject6(metadata.integration_merge_lease)) {
|
|
8340
8430
|
data.integration_merge_lease = metadata.integration_merge_lease;
|
|
8341
8431
|
}
|
|
8342
|
-
const legacyMetadata =
|
|
8432
|
+
const legacyMetadata = isPlainObject6(data.legacy_metadata) ? { ...data.legacy_metadata } : {};
|
|
8343
8433
|
for (const [key, value] of Object.entries(metadata)) {
|
|
8344
8434
|
if (key === "harness_root")
|
|
8345
8435
|
continue;
|
|
@@ -8395,7 +8485,7 @@ function buildRegister(residualFindings, byPlan, projectId, migratedAt) {
|
|
|
8395
8485
|
const raw = residualFindings[planId];
|
|
8396
8486
|
if (!Array.isArray(raw))
|
|
8397
8487
|
continue;
|
|
8398
|
-
const open = raw.filter((entry) =>
|
|
8488
|
+
const open = raw.filter((entry) => isPlainObject6(entry) && isOpenResidual2(entry)).sort((a, b) => {
|
|
8399
8489
|
const aId = typeof a.id === "string" ? a.id : "";
|
|
8400
8490
|
const bId = typeof b.id === "string" ? b.id : "";
|
|
8401
8491
|
return compareIds(aId, bId);
|
|
@@ -8484,12 +8574,12 @@ function migrateHarnessTree(root, opts = {}) {
|
|
|
8484
8574
|
if (legacy.version === undefined) {
|
|
8485
8575
|
throw new Error(`refusing to migrate: no v1 status.json found at ${statusPath} (nothing to migrate)`);
|
|
8486
8576
|
}
|
|
8487
|
-
const rows = Array.isArray(legacy.plans) ? legacy.plans.filter(
|
|
8577
|
+
const rows = Array.isArray(legacy.plans) ? legacy.plans.filter(isPlainObject6) : [];
|
|
8488
8578
|
if (Array.isArray(legacy.plans)) {
|
|
8489
8579
|
const unLiftable = [];
|
|
8490
8580
|
const idCounts = new Map;
|
|
8491
8581
|
for (const row of legacy.plans) {
|
|
8492
|
-
if (!
|
|
8582
|
+
if (!isPlainObject6(row) || rowIdOf(row) === null) {
|
|
8493
8583
|
unLiftable.push(row);
|
|
8494
8584
|
continue;
|
|
8495
8585
|
}
|
|
@@ -8505,7 +8595,7 @@ function migrateHarnessTree(root, opts = {}) {
|
|
|
8505
8595
|
throw new Error(`refusing to migrate: ${duplicates.length} duplicate plan id(s) (${duplicates.join(", ")}) \u2014 every v1 row must land in exactly one snapshot`);
|
|
8506
8596
|
}
|
|
8507
8597
|
}
|
|
8508
|
-
const metadata =
|
|
8598
|
+
const metadata = isPlainObject6(legacy.metadata) ? legacy.metadata : {};
|
|
8509
8599
|
const rootUpdatedAt = dateString(legacy.updated_at) ?? dateString(metadata.updated_at) ?? todayString2();
|
|
8510
8600
|
const migratedAt = dateString(metadata.updated_at) ?? rootUpdatedAt;
|
|
8511
8601
|
const migrationNotes = [];
|
|
@@ -8542,9 +8632,9 @@ function migrateHarnessTree(root, opts = {}) {
|
|
|
8542
8632
|
migrationNotes.push("no active iteration snapshot found \u2014 root metadata execution-policy/branch keys have no lift home and stay unmapped (visible here, not silently dropped)");
|
|
8543
8633
|
}
|
|
8544
8634
|
const notesFiles = collectNotesFiles(snapshots);
|
|
8545
|
-
const residualFindings =
|
|
8635
|
+
const residualFindings = isPlainObject6(legacy.residual_findings) ? legacy.residual_findings : {};
|
|
8546
8636
|
const register = buildRegister(residualFindings, byPlan, projectId, migratedAt);
|
|
8547
|
-
const programRoadmap =
|
|
8637
|
+
const programRoadmap = isPlainObject6(metadata.program_roadmap) ? metadata.program_roadmap : null;
|
|
8548
8638
|
let roadmap = null;
|
|
8549
8639
|
if (programRoadmap) {
|
|
8550
8640
|
const rawTitle = typeof programRoadmap.title === "string" && programRoadmap.title !== "" ? programRoadmap.title : "Program roadmap";
|
|
@@ -8553,7 +8643,7 @@ function migrateHarnessTree(root, opts = {}) {
|
|
|
8553
8643
|
migrationNotes.push(`roadmap title sanitized for frontmatter (line breaks replaced with spaces): ${JSON.stringify(rawTitle)}`);
|
|
8554
8644
|
}
|
|
8555
8645
|
roadmap = {
|
|
8556
|
-
file: join10("projects", projectId,
|
|
8646
|
+
file: join10("projects", projectId, PROJECT_ROADMAP_FILE2),
|
|
8557
8647
|
source: "status.json metadata.program_roadmap",
|
|
8558
8648
|
content: buildRoadmap({ ...programRoadmap, title: sanitizedTitle }, projectId, migratedAt)
|
|
8559
8649
|
};
|
|
@@ -8640,7 +8730,7 @@ async function applyMigratePlan(plan) {
|
|
|
8640
8730
|
const content = notes.lines.length > 0 ? `${notes.lines.join(`
|
|
8641
8731
|
`)}
|
|
8642
8732
|
` : "";
|
|
8643
|
-
|
|
8733
|
+
writeFileSync52(filePath, content, "utf8");
|
|
8644
8734
|
}
|
|
8645
8735
|
if (plan.register !== null) {
|
|
8646
8736
|
const gate2 = validateProjectRegister(plan.register.data);
|
|
@@ -8656,7 +8746,7 @@ async function applyMigratePlan(plan) {
|
|
|
8656
8746
|
if (plan.roadmap !== null) {
|
|
8657
8747
|
const filePath = projectTargetOf(plan.roadmap.file);
|
|
8658
8748
|
mkdirSync6(dirname72(filePath), { recursive: true });
|
|
8659
|
-
|
|
8749
|
+
writeFileSync52(filePath, plan.roadmap.content, "utf8");
|
|
8660
8750
|
}
|
|
8661
8751
|
const rootGate = validateStatusV22(plan.rootV2.data, { harnessDir: plan.root });
|
|
8662
8752
|
if (!rootGate.ok) {
|
|
@@ -8686,16 +8776,16 @@ function validatePlannedDocs(plan) {
|
|
|
8686
8776
|
for (const snapshot of plan.snapshots) {
|
|
8687
8777
|
const gate2 = validateWorkflowSnapshot2(snapshot.data);
|
|
8688
8778
|
if (!gate2.ok) {
|
|
8689
|
-
for (const
|
|
8690
|
-
warnings.push(`[${
|
|
8779
|
+
for (const violation9 of gate2.violations) {
|
|
8780
|
+
warnings.push(`[${violation9.severity}] ${violation9.code}: ${violation9.message} (planned snapshot ${snapshot.file})`);
|
|
8691
8781
|
}
|
|
8692
8782
|
}
|
|
8693
8783
|
}
|
|
8694
8784
|
if (plan.register !== null) {
|
|
8695
8785
|
const gate2 = validateProjectRegister(plan.register.data);
|
|
8696
8786
|
if (!gate2.ok) {
|
|
8697
|
-
for (const
|
|
8698
|
-
warnings.push(`[${
|
|
8787
|
+
for (const violation9 of gate2.violations) {
|
|
8788
|
+
warnings.push(`[${violation9.severity}] ${violation9.code}: ${violation9.message} (planned register ${plan.register.file})`);
|
|
8699
8789
|
}
|
|
8700
8790
|
}
|
|
8701
8791
|
}
|
|
@@ -8857,7 +8947,7 @@ function describeType(value) {
|
|
|
8857
8947
|
return "array";
|
|
8858
8948
|
return typeof value;
|
|
8859
8949
|
}
|
|
8860
|
-
function
|
|
8950
|
+
function isPlainObject7(value) {
|
|
8861
8951
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
8862
8952
|
}
|
|
8863
8953
|
function parseScalar3(raw) {
|
|
@@ -8901,7 +8991,7 @@ function isValidMcpUrl(raw) {
|
|
|
8901
8991
|
return true;
|
|
8902
8992
|
}
|
|
8903
8993
|
function validateManifest(manifest, errors2, warnings) {
|
|
8904
|
-
if (!
|
|
8994
|
+
if (!isPlainObject7(manifest)) {
|
|
8905
8995
|
errors2.push("plugin.json: manifest must be a JSON object");
|
|
8906
8996
|
return;
|
|
8907
8997
|
}
|
|
@@ -8937,7 +9027,7 @@ function validateManifest(manifest, errors2, warnings) {
|
|
|
8937
9027
|
}
|
|
8938
9028
|
}
|
|
8939
9029
|
if (doc.author !== undefined) {
|
|
8940
|
-
if (!
|
|
9030
|
+
if (!isPlainObject7(doc.author)) {
|
|
8941
9031
|
errors2.push('plugin.json: "author" must be an object with optional string fields name/email/url');
|
|
8942
9032
|
} else {
|
|
8943
9033
|
const author = doc.author;
|
|
@@ -8960,11 +9050,11 @@ function validateManifest(manifest, errors2, warnings) {
|
|
|
8960
9050
|
}
|
|
8961
9051
|
}
|
|
8962
9052
|
if (doc.extensions !== undefined) {
|
|
8963
|
-
if (!
|
|
9053
|
+
if (!isPlainObject7(doc.extensions)) {
|
|
8964
9054
|
warnings.push('plugin.json: "extensions" is not an object \u2014 ignored');
|
|
8965
9055
|
} else {
|
|
8966
9056
|
for (const [namespace, value] of Object.entries(doc.extensions)) {
|
|
8967
|
-
if (!
|
|
9057
|
+
if (!isPlainObject7(value)) {
|
|
8968
9058
|
warnings.push(`plugin.json: "extensions.${namespace}" is not an object \u2014 ignored`);
|
|
8969
9059
|
}
|
|
8970
9060
|
}
|
|
@@ -8973,7 +9063,7 @@ function validateManifest(manifest, errors2, warnings) {
|
|
|
8973
9063
|
}
|
|
8974
9064
|
function validateMcpServer(name, entry, errors2) {
|
|
8975
9065
|
const prefix = `mcp.json: mcpServers.${name}`;
|
|
8976
|
-
if (!
|
|
9066
|
+
if (!isPlainObject7(entry)) {
|
|
8977
9067
|
errors2.push(`${prefix} must be an object`);
|
|
8978
9068
|
return;
|
|
8979
9069
|
}
|
|
@@ -9007,7 +9097,7 @@ function validateMcpServer(name, entry, errors2) {
|
|
|
9007
9097
|
}
|
|
9008
9098
|
}
|
|
9009
9099
|
if (server.env !== undefined) {
|
|
9010
|
-
if (!
|
|
9100
|
+
if (!isPlainObject7(server.env)) {
|
|
9011
9101
|
errors2.push(`${prefix}: "env" must be an object of strings`);
|
|
9012
9102
|
} else {
|
|
9013
9103
|
for (const [key, value] of Object.entries(server.env)) {
|
|
@@ -9046,7 +9136,7 @@ function validateMcpServer(name, entry, errors2) {
|
|
|
9046
9136
|
errors2.push(`${prefix}: "url" must be an absolute http(s) URL without user info or fragment; non-loopback endpoints must use https`);
|
|
9047
9137
|
}
|
|
9048
9138
|
if (server.headers !== undefined) {
|
|
9049
|
-
if (!
|
|
9139
|
+
if (!isPlainObject7(server.headers)) {
|
|
9050
9140
|
errors2.push(`${prefix}: "headers" must be an object of strings`);
|
|
9051
9141
|
} else {
|
|
9052
9142
|
const seen = new Set;
|
|
@@ -9083,7 +9173,7 @@ function validateMcp(root, manifestSchema, errors2) {
|
|
|
9083
9173
|
errors2.push(`mcp.json: ${error.message}`);
|
|
9084
9174
|
return;
|
|
9085
9175
|
}
|
|
9086
|
-
if (!
|
|
9176
|
+
if (!isPlainObject7(parsed)) {
|
|
9087
9177
|
errors2.push("mcp.json: configuration must be a JSON object");
|
|
9088
9178
|
return;
|
|
9089
9179
|
}
|
|
@@ -9106,7 +9196,7 @@ function validateMcp(root, manifestSchema, errors2) {
|
|
|
9106
9196
|
}
|
|
9107
9197
|
}
|
|
9108
9198
|
const servers = doc.mcpServers;
|
|
9109
|
-
if (!
|
|
9199
|
+
if (!isPlainObject7(servers)) {
|
|
9110
9200
|
errors2.push('mcp.json: "mcpServers" is required and must be an object');
|
|
9111
9201
|
return;
|
|
9112
9202
|
}
|
|
@@ -9191,7 +9281,7 @@ function validateAgentPlugin(root) {
|
|
|
9191
9281
|
return { ok: false, errors: errors2, warnings };
|
|
9192
9282
|
}
|
|
9193
9283
|
validateManifest(manifest, errors2, warnings);
|
|
9194
|
-
const manifestSchema =
|
|
9284
|
+
const manifestSchema = isPlainObject7(manifest) ? manifest["$schema"] : undefined;
|
|
9195
9285
|
validateMcp(root, manifestSchema, errors2);
|
|
9196
9286
|
validateSkills(root, errors2, warnings);
|
|
9197
9287
|
return { ok: errors2.length === 0, errors: errors2, warnings };
|
|
@@ -10688,10 +10778,281 @@ function runPluginValidate(options) {
|
|
|
10688
10778
|
}
|
|
10689
10779
|
process.exitCode = 1;
|
|
10690
10780
|
}
|
|
10781
|
+
var HARNESS_AGENTS_TEMPLATE = `# AGENTS.md \u2014 .mstar/ (harness layer)
|
|
10782
|
+
|
|
10783
|
+
- Path symbols: {HARNESS_DIR} = .mstar/; {PLAN_DIR} = plans/; {SDD_DIR} = sdd/<plan-id>/;
|
|
10784
|
+
{ITERATION_DIR} = iterations/; {KNOWLEDGE_DIR} = knowledge/; {SPECS_DIR} = specs/;
|
|
10785
|
+
{WORKFLOW_DIR} = workflows/; {PROJECT_DIR} = projects/ (SSOT: skills/mstar-conventions).
|
|
10786
|
+
- Process vs results: process artifacts (plans/, iterations/, sdd/, status.json, workflows/,
|
|
10787
|
+
projects/) stay local and gitignored; results (this file, knowledge/, specs/) are tracked
|
|
10788
|
+
and shared across clones.
|
|
10789
|
+
- Done: only @project-manager or @qa-engineer may set Done; implementers set InReview.
|
|
10790
|
+
`;
|
|
10791
|
+
var CANONICAL_NEGATIONS = {
|
|
10792
|
+
"!.mstar/AGENTS.md": true,
|
|
10793
|
+
"!.mstar/knowledge/": true,
|
|
10794
|
+
"!.mstar/knowledge/**": true,
|
|
10795
|
+
"!.mstar/specs/": true,
|
|
10796
|
+
"!.mstar/specs/**": true
|
|
10797
|
+
};
|
|
10798
|
+
function gitWorkspaceRoot(startDir) {
|
|
10799
|
+
try {
|
|
10800
|
+
const cdup = execFileSync8("git", ["rev-parse", "--show-cdup"], {
|
|
10801
|
+
cwd: startDir,
|
|
10802
|
+
encoding: "utf8",
|
|
10803
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
10804
|
+
}).trim();
|
|
10805
|
+
if (!cdup)
|
|
10806
|
+
return startDir;
|
|
10807
|
+
let boundary = startDir;
|
|
10808
|
+
for (const segment of cdup.split(/[\\/]/)) {
|
|
10809
|
+
if (segment && segment !== ".")
|
|
10810
|
+
boundary = path12.dirname(boundary);
|
|
10811
|
+
}
|
|
10812
|
+
return path12.resolve(boundary);
|
|
10813
|
+
} catch {}
|
|
10814
|
+
return startDir;
|
|
10815
|
+
}
|
|
10816
|
+
function runScaffold(pathArg) {
|
|
10817
|
+
const root = pathArg ? path12.resolve(pathArg) : process.cwd();
|
|
10818
|
+
const harnessDir = scaffoldHarness(root);
|
|
10819
|
+
const projectDir = resolveProjectDir(root, { harnessDir });
|
|
10820
|
+
const created = [];
|
|
10821
|
+
const skipped = [];
|
|
10822
|
+
const workspaceRoot = gitWorkspaceRoot(root);
|
|
10823
|
+
const harnessKind = detectHarnessKind(harnessDir);
|
|
10824
|
+
if (harnessKind === "mstar" && path12.resolve(harnessDir) === path12.join(workspaceRoot, ".mstar")) {
|
|
10825
|
+
const gitignorePath = path12.join(workspaceRoot, ".gitignore");
|
|
10826
|
+
const snippet = emitGitignoreSnippet("mstar");
|
|
10827
|
+
const current = fs8.existsSync(gitignorePath) ? fs8.readFileSync(gitignorePath, "utf8") : "";
|
|
10828
|
+
const lines = new Set(current.split(/\r?\n/).map((line) => line.trim()));
|
|
10829
|
+
const snippetLines = snippet.split(`
|
|
10830
|
+
`).map((line) => line.trim());
|
|
10831
|
+
const fenceEntries = snippetLines.filter((line) => line.startsWith(".mstar/") || line.startsWith("!.mstar/") || line.startsWith(".mstarc"));
|
|
10832
|
+
const commentLines = snippetLines.filter((line) => line.startsWith("#"));
|
|
10833
|
+
const missing = fenceEntries.filter((entry) => !lines.has(entry));
|
|
10834
|
+
if (missing.length > 0) {
|
|
10835
|
+
const missingComments = commentLines.filter((line) => !lines.has(line));
|
|
10836
|
+
const broadRule = ".mstar/**";
|
|
10837
|
+
const currentLines = current.split(/\r?\n/);
|
|
10838
|
+
const firstNegation = currentLines.findIndex((line) => line.trim().startsWith("!.mstar/"));
|
|
10839
|
+
if (!lines.has(broadRule) && firstNegation !== -1) {
|
|
10840
|
+
const blockStart = [...missingComments, broadRule, ...missing.filter((entry) => entry.startsWith("!.mstar/"))];
|
|
10841
|
+
currentLines.splice(firstNegation, 0, ...blockStart);
|
|
10842
|
+
let next = currentLines.join(`
|
|
10843
|
+
`);
|
|
10844
|
+
if (missing.includes(".mstarc"))
|
|
10845
|
+
next = `${next}${next.endsWith(`
|
|
10846
|
+
`) ? "" : `
|
|
10847
|
+
`}.mstarc
|
|
10848
|
+
`;
|
|
10849
|
+
fs8.writeFileSync(gitignorePath, next, "utf8");
|
|
10850
|
+
} else {
|
|
10851
|
+
const prefix = current && !current.endsWith(`
|
|
10852
|
+
`) ? `
|
|
10853
|
+
` : "";
|
|
10854
|
+
fs8.appendFileSync(gitignorePath, `${prefix}${[...missingComments, ...missing].join(`
|
|
10855
|
+
`)}
|
|
10856
|
+
`, "utf8");
|
|
10857
|
+
}
|
|
10858
|
+
created.push(".gitignore (canonical harness snippet)");
|
|
10859
|
+
}
|
|
10860
|
+
const finalLines = fs8.readFileSync(gitignorePath, "utf8").split(/\r?\n/);
|
|
10861
|
+
const broadIndexes = finalLines.map((line, index) => line.trim() === ".mstar/**" ? index : -1).filter((index) => index !== -1);
|
|
10862
|
+
const canonicalNegationIndexes = finalLines.map((line, index) => CANONICAL_NEGATIONS[line.trim()] === true ? index : -1).filter((index) => index !== -1);
|
|
10863
|
+
const firstCanonicalNegationIndex = canonicalNegationIndexes[0] ?? -1;
|
|
10864
|
+
let normalized = false;
|
|
10865
|
+
if (broadIndexes.length > 0) {
|
|
10866
|
+
let removed = 0;
|
|
10867
|
+
let lastRetainedBroadIndex = broadIndexes[0];
|
|
10868
|
+
for (let i = 1;i < broadIndexes.length; i++) {
|
|
10869
|
+
const candidate = broadIndexes[i] - removed;
|
|
10870
|
+
let crossable = true;
|
|
10871
|
+
for (let j = lastRetainedBroadIndex + 1;j < candidate; j++) {
|
|
10872
|
+
const line = finalLines[j].trim();
|
|
10873
|
+
if (line === "")
|
|
10874
|
+
continue;
|
|
10875
|
+
if (line.startsWith("#"))
|
|
10876
|
+
continue;
|
|
10877
|
+
if (line === ".mstar/**")
|
|
10878
|
+
continue;
|
|
10879
|
+
if (CANONICAL_NEGATIONS[line] === true)
|
|
10880
|
+
continue;
|
|
10881
|
+
if (line === ".mstarc")
|
|
10882
|
+
continue;
|
|
10883
|
+
crossable = false;
|
|
10884
|
+
break;
|
|
10885
|
+
}
|
|
10886
|
+
if (crossable) {
|
|
10887
|
+
finalLines.splice(candidate, 1);
|
|
10888
|
+
removed++;
|
|
10889
|
+
normalized = true;
|
|
10890
|
+
} else {
|
|
10891
|
+
lastRetainedBroadIndex = candidate;
|
|
10892
|
+
}
|
|
10893
|
+
}
|
|
10894
|
+
const keptIndex = finalLines.findIndex((line) => line.trim() === ".mstar/**");
|
|
10895
|
+
if (firstCanonicalNegationIndex !== -1 && keptIndex > firstCanonicalNegationIndex) {
|
|
10896
|
+
let feasible = true;
|
|
10897
|
+
for (let i = firstCanonicalNegationIndex;i < keptIndex; i++) {
|
|
10898
|
+
const line = finalLines[i].trim();
|
|
10899
|
+
if (line === "")
|
|
10900
|
+
continue;
|
|
10901
|
+
if (line.startsWith("#"))
|
|
10902
|
+
continue;
|
|
10903
|
+
if (line === ".mstar/**")
|
|
10904
|
+
continue;
|
|
10905
|
+
if (CANONICAL_NEGATIONS[line] === true)
|
|
10906
|
+
continue;
|
|
10907
|
+
if (line === ".mstarc")
|
|
10908
|
+
continue;
|
|
10909
|
+
feasible = false;
|
|
10910
|
+
break;
|
|
10911
|
+
}
|
|
10912
|
+
if (feasible) {
|
|
10913
|
+
const [broadLine] = finalLines.splice(keptIndex, 1);
|
|
10914
|
+
finalLines.splice(firstCanonicalNegationIndex, 0, broadLine);
|
|
10915
|
+
normalized = true;
|
|
10916
|
+
}
|
|
10917
|
+
}
|
|
10918
|
+
const isTargetedUserMstarRule = (line) => {
|
|
10919
|
+
const trimmed = line.trim();
|
|
10920
|
+
if (trimmed === "" || trimmed.startsWith("#"))
|
|
10921
|
+
return false;
|
|
10922
|
+
if (trimmed === ".mstar/**" || trimmed === ".mstarc")
|
|
10923
|
+
return false;
|
|
10924
|
+
if (CANONICAL_NEGATIONS[trimmed] === true)
|
|
10925
|
+
return false;
|
|
10926
|
+
if (trimmed.startsWith("!.mstar/") || trimmed.startsWith(".mstar/"))
|
|
10927
|
+
return true;
|
|
10928
|
+
return false;
|
|
10929
|
+
};
|
|
10930
|
+
const isOwnedLine = (line) => !isTargetedUserMstarRule(line);
|
|
10931
|
+
const targetedRules = finalLines.filter((line) => isTargetedUserMstarRule(line));
|
|
10932
|
+
if (targetedRules.length > 0) {
|
|
10933
|
+
const owned = finalLines.filter(isOwnedLine);
|
|
10934
|
+
const hadTrailingNewline = owned[owned.length - 1] === "";
|
|
10935
|
+
const ownedBody = hadTrailingNewline ? owned.slice(0, -1) : owned;
|
|
10936
|
+
const tail = [];
|
|
10937
|
+
const ensuredDirs = new Set;
|
|
10938
|
+
const broadPositionsPrePartition = finalLines.map((line, index) => line.trim() === ".mstar/**" ? index : -1).filter((index) => index !== -1);
|
|
10939
|
+
const lastBroadPrePartition = broadPositionsPrePartition[broadPositionsPrePartition.length - 1] ?? -1;
|
|
10940
|
+
for (const rule of targetedRules) {
|
|
10941
|
+
const trimmedRule = rule.trim();
|
|
10942
|
+
if (trimmedRule.startsWith("!") && trimmedRule.endsWith("/**")) {
|
|
10943
|
+
const inner = trimmedRule.slice(1, -3);
|
|
10944
|
+
const segments = inner.split("/").slice(1);
|
|
10945
|
+
let prefix = inner.split("/")[0];
|
|
10946
|
+
for (const segment of segments) {
|
|
10947
|
+
prefix += "/" + segment;
|
|
10948
|
+
const dirNegation = "!" + prefix + "/";
|
|
10949
|
+
const alreadyQueued = ensuredDirs.has(dirNegation);
|
|
10950
|
+
const alreadyPresent = !alreadyQueued && finalLines.some((line, index) => index > lastBroadPrePartition && line.trim() === dirNegation);
|
|
10951
|
+
if (!alreadyQueued && !alreadyPresent) {
|
|
10952
|
+
ensuredDirs.add(dirNegation);
|
|
10953
|
+
tail.push(dirNegation);
|
|
10954
|
+
}
|
|
10955
|
+
}
|
|
10956
|
+
}
|
|
10957
|
+
tail.push(rule);
|
|
10958
|
+
}
|
|
10959
|
+
const rebuilt = [...ownedBody, ...tail];
|
|
10960
|
+
const current2 = finalLines[finalLines.length - 1] === "" ? finalLines.slice(0, -1) : finalLines;
|
|
10961
|
+
if (rebuilt.join(`
|
|
10962
|
+
`) !== current2.join(`
|
|
10963
|
+
`)) {
|
|
10964
|
+
finalLines.length = 0;
|
|
10965
|
+
finalLines.push(...rebuilt);
|
|
10966
|
+
normalized = true;
|
|
10967
|
+
}
|
|
10968
|
+
}
|
|
10969
|
+
const broadAfter = finalLines.map((line, index) => line.trim() === ".mstar/**" ? index : -1).filter((index) => index !== -1);
|
|
10970
|
+
if (broadAfter.length > 1) {
|
|
10971
|
+
const [first, ...duplicates] = broadAfter;
|
|
10972
|
+
let removed2 = 0;
|
|
10973
|
+
for (const duplicate of duplicates) {
|
|
10974
|
+
finalLines.splice(duplicate - removed2, 1);
|
|
10975
|
+
removed2++;
|
|
10976
|
+
}
|
|
10977
|
+
if (removed2 > 0)
|
|
10978
|
+
normalized = true;
|
|
10979
|
+
}
|
|
10980
|
+
const broadIndexesFinal = finalLines.map((line, index) => line.trim() === ".mstar/**" ? index : -1).filter((index) => index !== -1);
|
|
10981
|
+
const canonicalNegationIndexesFinal = finalLines.map((line, index) => CANONICAL_NEGATIONS[line.trim()] === true ? index : -1).filter((index) => index !== -1);
|
|
10982
|
+
const firstCanonicalNegationIndexFinal = canonicalNegationIndexesFinal[0] ?? -1;
|
|
10983
|
+
if (broadIndexesFinal.length > 0 && firstCanonicalNegationIndexFinal !== -1 && broadIndexesFinal[0] > firstCanonicalNegationIndexFinal) {
|
|
10984
|
+
const keptIndex2 = broadIndexesFinal[0];
|
|
10985
|
+
let feasible = true;
|
|
10986
|
+
for (let i = firstCanonicalNegationIndexFinal;i < keptIndex2; i++) {
|
|
10987
|
+
const line = finalLines[i].trim();
|
|
10988
|
+
if (line === "" || line.startsWith("#"))
|
|
10989
|
+
continue;
|
|
10990
|
+
if (line === ".mstar/**" || line === ".mstarc")
|
|
10991
|
+
continue;
|
|
10992
|
+
if (CANONICAL_NEGATIONS[line] === true)
|
|
10993
|
+
continue;
|
|
10994
|
+
feasible = false;
|
|
10995
|
+
break;
|
|
10996
|
+
}
|
|
10997
|
+
if (feasible) {
|
|
10998
|
+
const [broadLine] = finalLines.splice(keptIndex2, 1);
|
|
10999
|
+
finalLines.splice(firstCanonicalNegationIndexFinal, 0, broadLine);
|
|
11000
|
+
normalized = true;
|
|
11001
|
+
}
|
|
11002
|
+
}
|
|
11003
|
+
const broadIndexesLast = finalLines.map((line, index) => line.trim() === ".mstar/**" ? index : -1).filter((index) => index !== -1);
|
|
11004
|
+
if (broadIndexesLast.length > 0) {
|
|
11005
|
+
const lastBroadIndex = broadIndexesLast[broadIndexesLast.length - 1];
|
|
11006
|
+
let insertIndex = finalLines.findIndex((line) => isTargetedUserMstarRule(line));
|
|
11007
|
+
if (insertIndex === -1)
|
|
11008
|
+
insertIndex = finalLines.length;
|
|
11009
|
+
for (const negation of Object.keys(CANONICAL_NEGATIONS)) {
|
|
11010
|
+
const covered = finalLines.some((line, index) => index > lastBroadIndex && line.trim() === negation);
|
|
11011
|
+
if (!covered) {
|
|
11012
|
+
finalLines.splice(insertIndex, 0, negation);
|
|
11013
|
+
insertIndex++;
|
|
11014
|
+
normalized = true;
|
|
11015
|
+
}
|
|
11016
|
+
}
|
|
11017
|
+
}
|
|
11018
|
+
if (normalized && finalLines[finalLines.length - 1] !== "")
|
|
11019
|
+
finalLines.push("");
|
|
11020
|
+
}
|
|
11021
|
+
if (normalized) {
|
|
11022
|
+
fs8.writeFileSync(gitignorePath, finalLines.join(`
|
|
11023
|
+
`), "utf8");
|
|
11024
|
+
created.push(".gitignore (canonical harness snippet reordered)");
|
|
11025
|
+
} else if (missing.length === 0) {
|
|
11026
|
+
skipped.push(".gitignore (canonical harness snippet already present)");
|
|
11027
|
+
}
|
|
11028
|
+
} else {
|
|
11029
|
+
skipped.push(".gitignore (canonical harness snippet) \u2014 custom harness layout manages its own ignore rules");
|
|
11030
|
+
}
|
|
11031
|
+
const agentsPath = path12.join(harnessDir, "AGENTS.md");
|
|
11032
|
+
const agentsLabel = `${path12.basename(harnessDir)}/AGENTS.md`;
|
|
11033
|
+
if (!fs8.existsSync(agentsPath)) {
|
|
11034
|
+
fs8.writeFileSync(agentsPath, HARNESS_AGENTS_TEMPLATE, "utf8");
|
|
11035
|
+
created.push(agentsLabel);
|
|
11036
|
+
} else {
|
|
11037
|
+
skipped.push(`${agentsLabel} (already present)`);
|
|
11038
|
+
}
|
|
11039
|
+
const headline = created.length > 0 ? `scaffold: harness initialized at ${harnessDir}` : `scaffold: harness ensured at ${harnessDir}`;
|
|
11040
|
+
console.log(import_picocolors2.default.green(headline));
|
|
11041
|
+
console.log(` harness dir: ${harnessDir}`);
|
|
11042
|
+
console.log(` project dir: ${projectDir}`);
|
|
11043
|
+
for (const item of created)
|
|
11044
|
+
console.log(` created: ${item}`);
|
|
11045
|
+
for (const item of skipped)
|
|
11046
|
+
console.log(` skipped: ${item}`);
|
|
11047
|
+
}
|
|
10691
11048
|
program2.name("mstar-harness").description("Morning Star harness CLI for target-based agent bootstrap").version(packageVersion);
|
|
10692
11049
|
program2.command("init").description("Interactive/non-interactive setup for target agent bootstrap").option("-y, --yes", "Non-interactive mode").option("--target <target>", "Install target", "opencode").option("--scope <scope>", "Config scope: global|project (default: project)").option("--output <path>", "Config file path override, relative to project root").option("--dry-run", "Preview result without writing config").option("--no-fallbacks", "Skip installing the dsh-llm-fallbacks plugin (dsh target only)").option("--pm-model <model>", "Optional: model for project-manager (advanced override)").option("--strategic-models <a,b,c>", "Optional: models for architect/product-manager/prompt-engineer").option("--dev-models <a,b,c>", "Optional: models for fullstack-dev/fullstack-dev-2/frontend-dev").option("--qc-models <a,b,c>", "Optional: models for qc trio").option("--other-models <a,b,c>", "Optional: models for remaining roles").action(async (options) => {
|
|
10693
11050
|
await runInit5({ ...options, noFallbacks: options.fallbacks === false });
|
|
10694
11051
|
});
|
|
11052
|
+
var harnessCommand = program2.command("harness").description("harness bootstrap / dir-resolution helpers (engine-backed)");
|
|
11053
|
+
harnessCommand.command("scaffold").description("One-shot harness bootstrap: create the harness dir (default .mstar/, honoring .mstarc harness_dir/project_dir) " + "with dirs + v2 status.json + projects/_default/, append the canonical .gitignore snippet when absent " + "(skipped for non-.mstar layouts), and write a minimal {HARNESS_DIR}/AGENTS.md when absent").argument("[path]", "Root to scaffold (default: cwd)").action((pathArg) => {
|
|
11054
|
+
runScaffold(pathArg);
|
|
11055
|
+
});
|
|
10695
11056
|
program2.command("doctor").description("Validate Morning Star setup for a target agent config").option("--target <target>", "Target agent for doctor checks", "opencode").option("--scope <scope>", "Config scope: global|project", "project").option("--output <path>", "Config file path override, relative to project root").action((options) => {
|
|
10696
11057
|
runDoctor5(options);
|
|
10697
11058
|
});
|
|
@@ -10704,7 +11065,7 @@ pathCommand.command("resolve").description("Resolve {HARNESS_DIR} + {SPECS_DIR}
|
|
|
10704
11065
|
const startDir = pathArg ? path12.resolve(pathArg) : process.cwd();
|
|
10705
11066
|
const harnessDir = resolveHarnessDir(startDir);
|
|
10706
11067
|
if (!harnessDir) {
|
|
10707
|
-
const guidance = "no harness dir found \u2014 the bounded probe (.mstar/, .agents/, .plans/, plans/) walked up from " + `${startDir} only within the workspace root (git top-level of the start dir; non-git start probes only itself) \u2014 run \`mstar
|
|
11068
|
+
const guidance = "no harness dir found \u2014 the bounded probe (.mstar/, .agents/, .plans/, plans/) walked up from " + `${startDir} only within the workspace root (git top-level of the start dir; non-git start probes only itself) \u2014 run \`mstar harness scaffold\` to bootstrap, or pass a start dir inside a harness-enabled project`;
|
|
10708
11069
|
if (options.json) {
|
|
10709
11070
|
console.log(JSON.stringify({ ok: false, startDir, harnessDir: null, specsDir: null, workflowDir: null, projectDir: null, guidance }));
|
|
10710
11071
|
} else {
|
|
@@ -10750,10 +11111,10 @@ statusCommand.command("validate").description("Validate a v2 status.json root or
|
|
|
10750
11111
|
}
|
|
10751
11112
|
const count = gate2.violations.length;
|
|
10752
11113
|
console.error(import_picocolors2.default.red(`${statusPath}: FAIL (${count} violation${count === 1 ? "" : "s"})`));
|
|
10753
|
-
for (const
|
|
10754
|
-
console.error(` - [${
|
|
10755
|
-
if (
|
|
10756
|
-
console.error(` fix: ${
|
|
11114
|
+
for (const violation9 of gate2.violations) {
|
|
11115
|
+
console.error(` - [${violation9.severity}] ${violation9.code}: ${violation9.message}`);
|
|
11116
|
+
if (violation9.fix)
|
|
11117
|
+
console.error(` fix: ${violation9.fix}`);
|
|
10757
11118
|
}
|
|
10758
11119
|
process.exitCode = 1;
|
|
10759
11120
|
} catch (error) {
|
|
@@ -10846,10 +11207,10 @@ function verifyLeaseRow(row, planId, snapshotPath) {
|
|
|
10846
11207
|
}
|
|
10847
11208
|
const count = result.violations.length;
|
|
10848
11209
|
console.error(import_picocolors2.default.red(`${snapshotPath}: FAIL plan ${planId} (${count} violation${count === 1 ? "" : "s"})`));
|
|
10849
|
-
for (const
|
|
10850
|
-
console.error(` - [${
|
|
10851
|
-
if (
|
|
10852
|
-
console.error(` fix: ${
|
|
11210
|
+
for (const violation9 of result.violations) {
|
|
11211
|
+
console.error(` - [${violation9.severity}] ${violation9.code}: ${violation9.message}`);
|
|
11212
|
+
if (violation9.fix)
|
|
11213
|
+
console.error(` fix: ${violation9.fix}`);
|
|
10853
11214
|
}
|
|
10854
11215
|
process.exitCode = 1;
|
|
10855
11216
|
}
|
|
@@ -10969,10 +11330,10 @@ function printChecklist(label, gate2) {
|
|
|
10969
11330
|
}
|
|
10970
11331
|
const count = gate2.violations.length;
|
|
10971
11332
|
console.error(import_picocolors2.default.red(`${label}: FAIL (${count} violation${count === 1 ? "" : "s"})`));
|
|
10972
|
-
for (const
|
|
10973
|
-
console.error(` - [${
|
|
10974
|
-
if (
|
|
10975
|
-
console.error(` fix: ${
|
|
11333
|
+
for (const violation9 of gate2.violations) {
|
|
11334
|
+
console.error(` - [${violation9.severity}] ${violation9.code}: ${violation9.message}`);
|
|
11335
|
+
if (violation9.fix)
|
|
11336
|
+
console.error(` fix: ${violation9.fix}`);
|
|
10976
11337
|
}
|
|
10977
11338
|
}
|
|
10978
11339
|
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("--workflow <id>", "Workflow id whose snapshot is evaluated").requiredOption("--compass <path>", "delivery-compass.md path").option("--harness <path>", "Harness dir override (default: resolved {HARNESS_DIR})").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) => {
|
|
@@ -11006,10 +11367,10 @@ iterationCommand.command("push-cadence").description("\xA75.1a push-cadence prob
|
|
|
11006
11367
|
}
|
|
11007
11368
|
const count = result.violations.length;
|
|
11008
11369
|
console.error(import_picocolors2.default.red(`push blocked (${count} violation${count === 1 ? "" : "s"})`));
|
|
11009
|
-
for (const
|
|
11010
|
-
console.error(` - [${
|
|
11011
|
-
if (
|
|
11012
|
-
console.error(` fix: ${
|
|
11370
|
+
for (const violation9 of result.violations) {
|
|
11371
|
+
console.error(` - [${violation9.severity}] ${violation9.code}: ${violation9.message}`);
|
|
11372
|
+
if (violation9.fix)
|
|
11373
|
+
console.error(` fix: ${violation9.fix}`);
|
|
11013
11374
|
}
|
|
11014
11375
|
process.exitCode = 1;
|
|
11015
11376
|
});
|
|
@@ -11347,10 +11708,10 @@ lintCommand.description("Lint <target> (file or dir) \u2014 exit 1 on violations
|
|
|
11347
11708
|
violations += result.violations.length;
|
|
11348
11709
|
const count = result.violations.length;
|
|
11349
11710
|
console.error(import_picocolors2.default.red(`${label}: FAIL (${count} violation${count === 1 ? "" : "s"})`));
|
|
11350
|
-
for (const
|
|
11351
|
-
console.error(` - [${
|
|
11352
|
-
if (
|
|
11353
|
-
console.error(` fix: ${
|
|
11711
|
+
for (const violation9 of result.violations) {
|
|
11712
|
+
console.error(` - [${violation9.severity}] ${violation9.code}: ${violation9.message}`);
|
|
11713
|
+
if (violation9.fix)
|
|
11714
|
+
console.error(` fix: ${violation9.fix}`);
|
|
11354
11715
|
}
|
|
11355
11716
|
}
|
|
11356
11717
|
if (violations > 0)
|
|
@@ -11392,16 +11753,31 @@ designMdCommand.command("validate").description("Validate DESIGN.md in <dir>: to
|
|
|
11392
11753
|
failScript(error, "design-md validate");
|
|
11393
11754
|
}
|
|
11394
11755
|
});
|
|
11395
|
-
|
|
11756
|
+
var AUDIT_HARDENING_KINDS = ["Hardening", "Checked and clean"];
|
|
11757
|
+
function parseAuditScaffoldInput(text) {
|
|
11396
11758
|
let parsed;
|
|
11397
11759
|
try {
|
|
11398
11760
|
parsed = JSON.parse(text);
|
|
11399
11761
|
} catch {
|
|
11400
11762
|
throw new SddScriptError("usage: audit scaffold \u2014 findings file is not valid JSON", 2);
|
|
11401
11763
|
}
|
|
11402
|
-
|
|
11403
|
-
|
|
11404
|
-
|
|
11764
|
+
let root;
|
|
11765
|
+
let needsVerificationRaw;
|
|
11766
|
+
let hardeningCheckedRaw;
|
|
11767
|
+
if (Array.isArray(parsed)) {
|
|
11768
|
+
root = parsed;
|
|
11769
|
+
} else if (typeof parsed === "object" && parsed !== null) {
|
|
11770
|
+
const doc = parsed;
|
|
11771
|
+
if (!Array.isArray(doc.findings)) {
|
|
11772
|
+
throw new SddScriptError("usage: audit scaffold \u2014 findings file must be a JSON array or an object with a findings array", 2);
|
|
11773
|
+
}
|
|
11774
|
+
root = doc.findings;
|
|
11775
|
+
needsVerificationRaw = doc.needsVerification;
|
|
11776
|
+
hardeningCheckedRaw = doc.hardeningChecked;
|
|
11777
|
+
} else {
|
|
11778
|
+
throw new SddScriptError("usage: audit scaffold \u2014 findings file must be a JSON array or an object with a findings array", 2);
|
|
11779
|
+
}
|
|
11780
|
+
const findings = root.map((raw, index) => {
|
|
11405
11781
|
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
|
|
11406
11782
|
throw new SddScriptError(`usage: audit scaffold \u2014 findings[${index}] is not an object`, 2);
|
|
11407
11783
|
}
|
|
@@ -11444,6 +11820,49 @@ function parseAuditFindings(text) {
|
|
|
11444
11820
|
dependsOn
|
|
11445
11821
|
};
|
|
11446
11822
|
});
|
|
11823
|
+
return { findings, needsVerification: parseNeedsVerification(needsVerificationRaw), hardeningChecked: parseHardeningChecked(hardeningCheckedRaw) };
|
|
11824
|
+
}
|
|
11825
|
+
function parseNeedsVerification(raw) {
|
|
11826
|
+
if (raw === undefined)
|
|
11827
|
+
return;
|
|
11828
|
+
if (!Array.isArray(raw)) {
|
|
11829
|
+
throw new SddScriptError("usage: audit scaffold \u2014 needsVerification must be an array of {lead, how, evidence?}", 2);
|
|
11830
|
+
}
|
|
11831
|
+
return raw.map((entry, index) => {
|
|
11832
|
+
if (typeof entry !== "object" || entry === null || Array.isArray(entry)) {
|
|
11833
|
+
throw new SddScriptError(`usage: audit scaffold \u2014 needsVerification[${index}] is not an object`, 2);
|
|
11834
|
+
}
|
|
11835
|
+
const e = entry;
|
|
11836
|
+
const lead = typeof e.lead === "string" ? e.lead.trim() : "";
|
|
11837
|
+
const how = typeof e.how === "string" ? e.how.trim() : "";
|
|
11838
|
+
if (lead === "" || how === "") {
|
|
11839
|
+
throw new SddScriptError(`usage: audit scaffold \u2014 needsVerification[${index}] needs non-empty lead and how`, 2);
|
|
11840
|
+
}
|
|
11841
|
+
const evidence = typeof e.evidence === "string" && e.evidence.trim() !== "" ? e.evidence.trim() : undefined;
|
|
11842
|
+
return { lead, how, evidence };
|
|
11843
|
+
});
|
|
11844
|
+
}
|
|
11845
|
+
function parseHardeningChecked(raw) {
|
|
11846
|
+
if (raw === undefined)
|
|
11847
|
+
return;
|
|
11848
|
+
if (!Array.isArray(raw)) {
|
|
11849
|
+
throw new SddScriptError("usage: audit scaffold \u2014 hardeningChecked must be an array of {kind, text}", 2);
|
|
11850
|
+
}
|
|
11851
|
+
return raw.map((entry, index) => {
|
|
11852
|
+
if (typeof entry !== "object" || entry === null || Array.isArray(entry)) {
|
|
11853
|
+
throw new SddScriptError(`usage: audit scaffold \u2014 hardeningChecked[${index}] is not an object`, 2);
|
|
11854
|
+
}
|
|
11855
|
+
const e = entry;
|
|
11856
|
+
const kind = typeof e.kind === "string" ? e.kind : "";
|
|
11857
|
+
const text = typeof e.text === "string" ? e.text.trim() : "";
|
|
11858
|
+
if (!AUDIT_HARDENING_KINDS.includes(kind)) {
|
|
11859
|
+
throw new SddScriptError(`usage: audit scaffold \u2014 hardeningChecked[${index}].kind must be one of ${AUDIT_HARDENING_KINDS.join("|")}`, 2);
|
|
11860
|
+
}
|
|
11861
|
+
if (text === "") {
|
|
11862
|
+
throw new SddScriptError(`usage: audit scaffold \u2014 hardeningChecked[${index}] needs non-empty text`, 2);
|
|
11863
|
+
}
|
|
11864
|
+
return { kind, text };
|
|
11865
|
+
});
|
|
11447
11866
|
}
|
|
11448
11867
|
var auditCommand = program2.command("audit").description("audit plan scaffold (engine-backed)");
|
|
11449
11868
|
function resolveAuditShortSha(cwd, override) {
|
|
@@ -11460,7 +11879,7 @@ function resolveAuditShortSha(cwd, override) {
|
|
|
11460
11879
|
return "unknown";
|
|
11461
11880
|
}
|
|
11462
11881
|
}
|
|
11463
|
-
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) => {
|
|
11882
|
+
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}, or object {findings: [...], needsVerification?: [{lead, how, evidence?}], hardeningChecked?: [{kind, text}]}").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) => {
|
|
11464
11883
|
try {
|
|
11465
11884
|
if (!findingsFile)
|
|
11466
11885
|
throw new SddScriptError("usage: audit scaffold <findings-file> [--dir <out-dir>]", 2);
|
|
@@ -11475,9 +11894,15 @@ auditCommand.command("scaffold").description("Scaffold an audit-<date>/ plan dir
|
|
|
11475
11894
|
}
|
|
11476
11895
|
const date = options.date ?? new Date().toISOString().slice(0, 10);
|
|
11477
11896
|
const outDir = options.dir !== undefined ? resolveCliPath(options.dir) : resolveCliPath(`audit-${date}`);
|
|
11478
|
-
const
|
|
11897
|
+
const input = parseAuditScaffoldInput(fs8.readFileSync(abs, "utf8"));
|
|
11479
11898
|
const sha = resolveAuditShortSha(process.cwd(), options.sha);
|
|
11480
|
-
const result = scaffoldAuditPlan(outDir, findings, {
|
|
11899
|
+
const result = scaffoldAuditPlan(outDir, input.findings, {
|
|
11900
|
+
date,
|
|
11901
|
+
repoName: options.repo,
|
|
11902
|
+
repoShortSha: sha,
|
|
11903
|
+
needsVerification: input.needsVerification,
|
|
11904
|
+
hardeningChecked: input.hardeningChecked
|
|
11905
|
+
});
|
|
11481
11906
|
const count = result.files.length;
|
|
11482
11907
|
console.log(import_picocolors2.default.green(`audit scaffold: OK \u2014 ${count} plan file${count === 1 ? "" : "s"} in ${result.outDir}`));
|
|
11483
11908
|
for (const file of result.files)
|