@mstar-harness/cli 2.3.0 → 2.4.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 +937 -176
- package/package.json +3 -2
package/dist/mstar-harness.js
CHANGED
|
@@ -2344,9 +2344,9 @@ var require_commander = __commonJS((exports) => {
|
|
|
2344
2344
|
});
|
|
2345
2345
|
|
|
2346
2346
|
// src/index.ts
|
|
2347
|
-
import { execFileSync as
|
|
2348
|
-
import
|
|
2349
|
-
import
|
|
2347
|
+
import { execFileSync as execFileSync8 } from "child_process";
|
|
2348
|
+
import fs8 from "fs";
|
|
2349
|
+
import path12 from "path";
|
|
2350
2350
|
|
|
2351
2351
|
// ../../node_modules/@inquirer/core/dist/lib/key.js
|
|
2352
2352
|
var keybindings = ["emacs", "vim"];
|
|
@@ -4016,6 +4016,8 @@ import { mkdirSync as mkdirSync5, readdirSync as readdirSync4, readFileSync as r
|
|
|
4016
4016
|
import { join as join7, resolve as resolve7 } from "node:path";
|
|
4017
4017
|
import { existsSync as existsSync5, readdirSync as readdirSync5, readFileSync as readFileSync7 } from "node:fs";
|
|
4018
4018
|
import { basename as basename4, isAbsolute as isAbsolute5, join as join8, relative as relative2, resolve as resolve8, sep } from "node:path";
|
|
4019
|
+
import { existsSync as existsSync6 } from "node:fs";
|
|
4020
|
+
import { join as join9 } from "node:path";
|
|
4019
4021
|
var SEVERITY_ORDER = ["critical", "high", "medium", "low", "nit"];
|
|
4020
4022
|
function readJson(filePath) {
|
|
4021
4023
|
if (!existsSync(filePath))
|
|
@@ -4161,9 +4163,49 @@ function hasFiles(dir) {
|
|
|
4161
4163
|
return false;
|
|
4162
4164
|
}
|
|
4163
4165
|
}
|
|
4166
|
+
function isPlainObject2(value) {
|
|
4167
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
4168
|
+
}
|
|
4169
|
+
function violation(severity, code, message, fix) {
|
|
4170
|
+
return { ok: false, severity, code, message, fix };
|
|
4171
|
+
}
|
|
4172
|
+
function validateNonEmptyString(violations, value, field, missingCode, invalidCode) {
|
|
4173
|
+
if (value === undefined) {
|
|
4174
|
+
violations.push(violation("high", missingCode, `missing required field: ${field}`));
|
|
4175
|
+
} else if (typeof value !== "string" || value.trim() === "") {
|
|
4176
|
+
violations.push(violation("medium", invalidCode, `${field} must be a non-empty string`));
|
|
4177
|
+
}
|
|
4178
|
+
}
|
|
4164
4179
|
var DATE_PART = String.raw`\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12]\d|3[01])`;
|
|
4165
4180
|
var RFC3339_Z_RE = new RegExp(String.raw`^${DATE_PART}T\d{2}:\d{2}:\d{2}(\.\d+)?Z$`);
|
|
4166
4181
|
var DATE_ONLY_RE = new RegExp(String.raw`^${DATE_PART}$`);
|
|
4182
|
+
function isValidClaimedAt(value) {
|
|
4183
|
+
return typeof value === "string" && (RFC3339_Z_RE.test(value) || DATE_ONLY_RE.test(value));
|
|
4184
|
+
}
|
|
4185
|
+
function validateIntegrationMergeLease(lease) {
|
|
4186
|
+
const violations = [];
|
|
4187
|
+
if (!isPlainObject2(lease)) {
|
|
4188
|
+
return {
|
|
4189
|
+
ok: false,
|
|
4190
|
+
violations: [
|
|
4191
|
+
violation("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")
|
|
4192
|
+
]
|
|
4193
|
+
};
|
|
4194
|
+
}
|
|
4195
|
+
validateNonEmptyString(violations, lease.holder, "holder", "lease.merge-lease.missing-holder", "lease.merge-lease.invalid-holder");
|
|
4196
|
+
if (lease.claimed_at === undefined) {
|
|
4197
|
+
violations.push(violation("high", "lease.merge-lease.missing-claimed-at", "missing required field: claimed_at"));
|
|
4198
|
+
} else if (!isValidClaimedAt(lease.claimed_at)) {
|
|
4199
|
+
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"));
|
|
4200
|
+
}
|
|
4201
|
+
validateNonEmptyString(violations, lease.plan_id, "plan_id", "lease.merge-lease.missing-plan-id", "lease.merge-lease.invalid-plan-id");
|
|
4202
|
+
validateNonEmptyString(violations, lease.source_branch, "source_branch", "lease.merge-lease.missing-source-branch", "lease.merge-lease.invalid-source-branch");
|
|
4203
|
+
validateNonEmptyString(violations, lease.target_branch, "target_branch", "lease.merge-lease.missing-target-branch", "lease.merge-lease.invalid-target-branch");
|
|
4204
|
+
if (lease.session_label !== undefined && typeof lease.session_label !== "string") {
|
|
4205
|
+
violations.push(violation("medium", "lease.merge-lease.invalid-session-label", "session_label must be a string (display only \u2014 never used for ownership comparison)"));
|
|
4206
|
+
}
|
|
4207
|
+
return { ok: violations.length === 0, violations };
|
|
4208
|
+
}
|
|
4167
4209
|
var STATUS_WRITE_LOCKDIR = ".status-write.lockdir";
|
|
4168
4210
|
var LOCKDIR_HOLDER_PID = "holder.pid";
|
|
4169
4211
|
var heldLockDirs = new AsyncLocalStorage2;
|
|
@@ -4376,7 +4418,8 @@ var DATE_RE = /^\d{4}-\d{2}-\d{2}$/;
|
|
|
4376
4418
|
var PLAN_STATUSES = ["Todo", "InProgress", "InReview", "Blocked", "Done"];
|
|
4377
4419
|
var RESIDUAL_DECISIONS = ["defer", "accept", "risk-accepted"];
|
|
4378
4420
|
var RESIDUAL_LIFECYCLES = ["open", "resolved", "waived", "superseded", "duplicate"];
|
|
4379
|
-
|
|
4421
|
+
var ROLLUP_FIELDS = ["total_open", "by_severity", "by_target", "by_plan"];
|
|
4422
|
+
function isPlainObject22(value) {
|
|
4380
4423
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
4381
4424
|
}
|
|
4382
4425
|
function violation3(severity, code, message, fix) {
|
|
@@ -4388,6 +4431,13 @@ function todayString() {
|
|
|
4388
4431
|
const day = String(now.getDate()).padStart(2, "0");
|
|
4389
4432
|
return `${now.getFullYear()}-${month}-${day}`;
|
|
4390
4433
|
}
|
|
4434
|
+
function normalizeSeverity(value) {
|
|
4435
|
+
if (value === "warning")
|
|
4436
|
+
return "low";
|
|
4437
|
+
if (value === null || value === "")
|
|
4438
|
+
return "medium";
|
|
4439
|
+
return value;
|
|
4440
|
+
}
|
|
4391
4441
|
function isOpenResidual(entry) {
|
|
4392
4442
|
const lifecycle = entry.lifecycle;
|
|
4393
4443
|
const effective = lifecycle === false || lifecycle === null || lifecycle === undefined ? "open" : lifecycle;
|
|
@@ -4402,7 +4452,7 @@ function validateNonEmptyString2(violations, value, field, missingCode, invalidC
|
|
|
4402
4452
|
}
|
|
4403
4453
|
function validatePlanRow(row) {
|
|
4404
4454
|
const violations = [];
|
|
4405
|
-
if (!
|
|
4455
|
+
if (!isPlainObject22(row)) {
|
|
4406
4456
|
return { ok: false, violations: [violation3("high", "status.plan-row.invalid", "plan row must be an object")] };
|
|
4407
4457
|
}
|
|
4408
4458
|
const { id, plan_id: planId, title, file, status, metadata, execution_lease } = row;
|
|
@@ -4426,10 +4476,10 @@ function validatePlanRow(row) {
|
|
|
4426
4476
|
} else if (typeof status !== "string" || !PLAN_STATUSES.includes(status)) {
|
|
4427
4477
|
violations.push(violation3("medium", "status.plan-row.invalid-status", `status must be one of ${PLAN_STATUSES.join(" | ")} \u2014 got ${JSON.stringify(status)}`));
|
|
4428
4478
|
}
|
|
4429
|
-
if (metadata !== undefined && !
|
|
4479
|
+
if (metadata !== undefined && !isPlainObject22(metadata)) {
|
|
4430
4480
|
violations.push(violation3("medium", "status.plan-row.invalid-metadata", "metadata must be an object"));
|
|
4431
4481
|
}
|
|
4432
|
-
if (execution_lease !== undefined && !
|
|
4482
|
+
if (execution_lease !== undefined && !isPlainObject22(execution_lease)) {
|
|
4433
4483
|
violations.push(violation3("medium", "status.plan-row.invalid-execution-lease", "execution_lease must be an object"));
|
|
4434
4484
|
}
|
|
4435
4485
|
if (status === "Done" && execution_lease !== undefined) {
|
|
@@ -4439,7 +4489,7 @@ function validatePlanRow(row) {
|
|
|
4439
4489
|
}
|
|
4440
4490
|
function validateResidual(entry) {
|
|
4441
4491
|
const violations = [];
|
|
4442
|
-
if (!
|
|
4492
|
+
if (!isPlainObject22(entry)) {
|
|
4443
4493
|
return { ok: false, violations: [violation3("high", "status.residual.invalid", "residual entry must be an object")] };
|
|
4444
4494
|
}
|
|
4445
4495
|
const { id, title, severity, source, scope, decision, owner, target, tracking, detail_doc, lifecycle, closed_at } = entry;
|
|
@@ -4529,7 +4579,7 @@ function validateStatus(docOrPath) {
|
|
|
4529
4579
|
}
|
|
4530
4580
|
if (residual_findings === undefined) {
|
|
4531
4581
|
violations.push(violation3("high", "status.missing-residual-findings", "missing required field: residual_findings (root-only canonical)"));
|
|
4532
|
-
} else if (!
|
|
4582
|
+
} else if (!isPlainObject22(residual_findings)) {
|
|
4533
4583
|
violations.push(violation3("high", "status.invalid-residual-findings", "residual_findings must be an object at root"));
|
|
4534
4584
|
} else {
|
|
4535
4585
|
for (const [planId, list] of Object.entries(residual_findings)) {
|
|
@@ -4546,7 +4596,7 @@ function validateStatus(docOrPath) {
|
|
|
4546
4596
|
}
|
|
4547
4597
|
if (metadata === undefined) {
|
|
4548
4598
|
violations.push(violation3("high", "status.missing-metadata", "missing required field: metadata"));
|
|
4549
|
-
} else if (!
|
|
4599
|
+
} else if (!isPlainObject22(metadata)) {
|
|
4550
4600
|
violations.push(violation3("high", "status.invalid-metadata", "metadata must be an object"));
|
|
4551
4601
|
} else if (Object.prototype.hasOwnProperty.call(metadata, "residual_findings")) {
|
|
4552
4602
|
violations.push(violation3("medium", "status.dual-write-residuals", "residual_findings must be root-only \u2014 metadata.residual_findings is legacy read-only; remove it (no dual-write)", "move entries to root residual_findings and delete metadata.residual_findings"));
|
|
@@ -4565,7 +4615,7 @@ async function archiveResiduals(planId, harnessDir) {
|
|
|
4565
4615
|
}
|
|
4566
4616
|
return withStatusWriteLock(statusPath, () => {
|
|
4567
4617
|
const doc = readJson(statusPath);
|
|
4568
|
-
if (!
|
|
4618
|
+
if (!isPlainObject22(doc.residual_findings)) {
|
|
4569
4619
|
throw new Error(`status.json residual_findings must be an object: ${statusPath}`);
|
|
4570
4620
|
}
|
|
4571
4621
|
const open = doc.residual_findings[planId];
|
|
@@ -4575,10 +4625,10 @@ async function archiveResiduals(planId, harnessDir) {
|
|
|
4575
4625
|
}
|
|
4576
4626
|
const archive = readJson(archivePath);
|
|
4577
4627
|
const existing = Array.isArray(archive.entries) ? archive.entries : [];
|
|
4578
|
-
const existingIds = new Set(existing.map((e) =>
|
|
4628
|
+
const existingIds = new Set(existing.map((e) => isPlainObject22(e) && typeof e.id === "string" ? e.id : undefined).filter((id) => id !== undefined));
|
|
4579
4629
|
const today = todayString();
|
|
4580
4630
|
const moved = open.filter((entry) => {
|
|
4581
|
-
if (!
|
|
4631
|
+
if (!isPlainObject22(entry) || typeof entry.id !== "string")
|
|
4582
4632
|
return true;
|
|
4583
4633
|
return !existingIds.has(entry.id);
|
|
4584
4634
|
}).map((entry) => ({ ...entry, archived_at: today }));
|
|
@@ -4591,6 +4641,105 @@ async function archiveResiduals(planId, harnessDir) {
|
|
|
4591
4641
|
return { planId, archived: moved.length, archivePath };
|
|
4592
4642
|
});
|
|
4593
4643
|
}
|
|
4644
|
+
function planFindingsCleanup(doc, planId) {
|
|
4645
|
+
if (!Array.isArray(doc.plans))
|
|
4646
|
+
return;
|
|
4647
|
+
for (const row of doc.plans) {
|
|
4648
|
+
if (!isPlainObject22(row))
|
|
4649
|
+
continue;
|
|
4650
|
+
const rowId = row.id ?? row.plan_id;
|
|
4651
|
+
if (rowId !== planId)
|
|
4652
|
+
continue;
|
|
4653
|
+
if (!isPlainObject22(row.metadata))
|
|
4654
|
+
return;
|
|
4655
|
+
const mode = row.metadata.findings_cleanup;
|
|
4656
|
+
if (mode === "zero-residual" || mode === "allow-residual")
|
|
4657
|
+
return mode;
|
|
4658
|
+
return;
|
|
4659
|
+
}
|
|
4660
|
+
return;
|
|
4661
|
+
}
|
|
4662
|
+
function openResidualsOf(doc, planId) {
|
|
4663
|
+
if (!isPlainObject22(doc.residual_findings))
|
|
4664
|
+
return [];
|
|
4665
|
+
const list = doc.residual_findings[planId];
|
|
4666
|
+
if (!Array.isArray(list))
|
|
4667
|
+
return [];
|
|
4668
|
+
return list.filter((entry) => isPlainObject22(entry) && isOpenResidual(entry));
|
|
4669
|
+
}
|
|
4670
|
+
function findingsCleanupGate(doc, planId, opts) {
|
|
4671
|
+
const mode = opts?.mode ?? planFindingsCleanup(doc, planId) ?? "allow-residual";
|
|
4672
|
+
const violations = [];
|
|
4673
|
+
const residuals = openResidualsOf(doc, planId);
|
|
4674
|
+
for (const entry of residuals) {
|
|
4675
|
+
const id = typeof entry.id === "string" ? entry.id : "<unnamed>";
|
|
4676
|
+
const label = `R#${id}`;
|
|
4677
|
+
if (mode === "zero-residual") {
|
|
4678
|
+
if (entry.severity === "nit") {
|
|
4679
|
+
violations.push(violation3("medium", "findings.zero-residual-nit", `${label}: style-only nits must be fixed in-session or dropped \u2014 never left open under zero-residual`));
|
|
4680
|
+
} else if (entry.decision === "risk-accepted" || entry.lifecycle === "waived") {
|
|
4681
|
+
violations.push(violation3("medium", "findings.zero-residual-risk-accepted", `${label}: waived/risk-accepted findings must be closed/archived, not left open under zero-residual`));
|
|
4682
|
+
} else if (entry.decision === "defer") {
|
|
4683
|
+
if (typeof entry.target !== "string" || entry.target.trim() === "") {
|
|
4684
|
+
violations.push(violation3("medium", "findings.zero-residual-defer-no-target", `${label}: blocker-defer requires a target (next iteration/milestone) under zero-residual`));
|
|
4685
|
+
}
|
|
4686
|
+
} else {
|
|
4687
|
+
violations.push(violation3("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`));
|
|
4688
|
+
}
|
|
4689
|
+
} else if (normalizeSeverity(entry.severity) === "critical") {
|
|
4690
|
+
violations.push(violation3("high", "findings.allow-residual-critical", `${label}: unresolved critical blocks Approve with residuals`));
|
|
4691
|
+
}
|
|
4692
|
+
}
|
|
4693
|
+
return { ok: violations.length === 0, violations };
|
|
4694
|
+
}
|
|
4695
|
+
function groupCount(values) {
|
|
4696
|
+
const counts = new Map;
|
|
4697
|
+
for (const value of values) {
|
|
4698
|
+
const key = typeof value === "string" ? value : String(value);
|
|
4699
|
+
counts.set(key, (counts.get(key) ?? 0) + 1);
|
|
4700
|
+
}
|
|
4701
|
+
return Object.fromEntries([...counts.entries()].sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0));
|
|
4702
|
+
}
|
|
4703
|
+
function techDebtRollup(docOrPath) {
|
|
4704
|
+
const doc = typeof docOrPath === "string" ? readJson(docOrPath) : docOrPath;
|
|
4705
|
+
const canonical = isPlainObject22(doc.residual_findings) ? doc.residual_findings : {};
|
|
4706
|
+
const metadata = isPlainObject22(doc.metadata) ? doc.metadata : {};
|
|
4707
|
+
const legacy = isPlainObject22(metadata.residual_findings) ? metadata.residual_findings : {};
|
|
4708
|
+
const merged = { ...canonical, ...legacy };
|
|
4709
|
+
const items = [];
|
|
4710
|
+
for (const [plan, list] of Object.entries(merged)) {
|
|
4711
|
+
if (!Array.isArray(list))
|
|
4712
|
+
continue;
|
|
4713
|
+
for (const value of list) {
|
|
4714
|
+
if (!isPlainObject22(value) || !isOpenResidual(value))
|
|
4715
|
+
continue;
|
|
4716
|
+
items.push({ plan, entry: value });
|
|
4717
|
+
}
|
|
4718
|
+
}
|
|
4719
|
+
const bySeverity = {};
|
|
4720
|
+
for (const severity of SEVERITY_ORDER) {
|
|
4721
|
+
bySeverity[severity] = items.filter(({ entry }) => normalizeSeverity(entry.severity) === severity).length;
|
|
4722
|
+
}
|
|
4723
|
+
const computed = {
|
|
4724
|
+
total_open: items.length,
|
|
4725
|
+
by_severity: bySeverity,
|
|
4726
|
+
by_target: groupCount(items.map(({ entry }) => entry.target ?? "unspecified")),
|
|
4727
|
+
by_plan: groupCount(items.map(({ plan }) => plan))
|
|
4728
|
+
};
|
|
4729
|
+
const storedRaw = metadata.tech_debt_summary ?? null;
|
|
4730
|
+
const stored = storedRaw === null ? null : storedRaw;
|
|
4731
|
+
const checks = ROLLUP_FIELDS.map((field) => {
|
|
4732
|
+
const computedField = computed[field];
|
|
4733
|
+
if (stored === null)
|
|
4734
|
+
return { field, status: "DRIFT" };
|
|
4735
|
+
const storedField = stored[field];
|
|
4736
|
+
const storedCompared = storedField === false ? null : storedField ?? null;
|
|
4737
|
+
const status = JSON.stringify(computedField) === JSON.stringify(storedCompared) ? "PASS" : "DRIFT";
|
|
4738
|
+
return { field, status };
|
|
4739
|
+
});
|
|
4740
|
+
const overall = checks.every((check) => check.status === "PASS") ? "PASS" : "DRIFT";
|
|
4741
|
+
return { computed, stored, checks, overall };
|
|
4742
|
+
}
|
|
4594
4743
|
var DEFAULT_PROBE_TIMEOUT_MS = 1e4;
|
|
4595
4744
|
function probeTimeoutMs() {
|
|
4596
4745
|
const raw = process.env.MSTAR_GIT_PROBE_TIMEOUT_MS;
|
|
@@ -4691,6 +4840,22 @@ function l2PreDispatchCheck(input, opts = {}) {
|
|
|
4691
4840
|
});
|
|
4692
4841
|
return gate(violations);
|
|
4693
4842
|
}
|
|
4843
|
+
var QC_ALIGNMENT_FIELDS = [
|
|
4844
|
+
{ key: "planId", label: "plan_id" },
|
|
4845
|
+
{ key: "reviewRange", label: "Review range" },
|
|
4846
|
+
{ key: "diffBasis", label: "Diff basis" }
|
|
4847
|
+
];
|
|
4848
|
+
function assertQcAlignment(assignments) {
|
|
4849
|
+
const violations = [];
|
|
4850
|
+
const list = assignments ?? [];
|
|
4851
|
+
for (const { key, label } of QC_ALIGNMENT_FIELDS) {
|
|
4852
|
+
const distinct = [...new Set(list.map((a) => a[key]))];
|
|
4853
|
+
if (distinct.length > 1) {
|
|
4854
|
+
violations.push(violation4("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`));
|
|
4855
|
+
}
|
|
4856
|
+
}
|
|
4857
|
+
return gate(violations);
|
|
4858
|
+
}
|
|
4694
4859
|
class SddScriptError extends Error {
|
|
4695
4860
|
exitCode;
|
|
4696
4861
|
constructor(message, exitCode) {
|
|
@@ -6322,6 +6487,156 @@ function lintStrategySections(docText) {
|
|
|
6322
6487
|
}
|
|
6323
6488
|
return { ok: violations.length === 0, violations };
|
|
6324
6489
|
}
|
|
6490
|
+
function violation10(severity, code, message, fix) {
|
|
6491
|
+
return { ok: false, severity, code, message, fix };
|
|
6492
|
+
}
|
|
6493
|
+
var ROLE_MAPPING = [
|
|
6494
|
+
{ agentId: "project-manager", reference: "references/project-manager.md" },
|
|
6495
|
+
{ agentId: "product-manager", reference: "references/product-manager.md" },
|
|
6496
|
+
{ agentId: "architect", reference: "references/architect.md" },
|
|
6497
|
+
{ agentId: "code-reviewer", reference: "references/code-reviewer.md" },
|
|
6498
|
+
{ agentId: "fullstack-dev", reference: "references/fullstack-dev-shared.md" },
|
|
6499
|
+
{ agentId: "fullstack-dev-2", reference: "references/fullstack-dev-shared.md" },
|
|
6500
|
+
{ agentId: "frontend-dev", reference: "references/frontend-dev.md" },
|
|
6501
|
+
{ agentId: "qa-engineer", reference: "references/qa-engineer.md" },
|
|
6502
|
+
{ agentId: "qc-specialist", reference: "references/qc-specialist-shared.md" },
|
|
6503
|
+
{ agentId: "qc-specialist-2", reference: "references/qc-specialist-shared.md" },
|
|
6504
|
+
{ agentId: "qc-specialist-3", reference: "references/qc-specialist-shared.md" },
|
|
6505
|
+
{ agentId: "ops-engineer", reference: "references/ops-engineer.md" },
|
|
6506
|
+
{ agentId: "writing-specialist", reference: "references/writing-specialist.md" },
|
|
6507
|
+
{ agentId: "prompt-engineer", reference: "references/prompt-engineer.md" }
|
|
6508
|
+
];
|
|
6509
|
+
var SHARED_FAMILIES = [
|
|
6510
|
+
{ family: "fullstack-dev", memberIds: ["fullstack-dev", "fullstack-dev-2"] },
|
|
6511
|
+
{ family: "qc-specialist", memberIds: ["qc-specialist", "qc-specialist-2", "qc-specialist-3"] }
|
|
6512
|
+
];
|
|
6513
|
+
var DEV_TRACK_PARAMS = [
|
|
6514
|
+
{ roleId: "fullstack-dev", track: "primary" },
|
|
6515
|
+
{ roleId: "fullstack-dev-2", track: "parallel_secondary" }
|
|
6516
|
+
];
|
|
6517
|
+
var QC_REVIEWER_PARAMS = [
|
|
6518
|
+
{
|
|
6519
|
+
roleId: "qc-specialist",
|
|
6520
|
+
reviewerIndex: 1,
|
|
6521
|
+
focus: "Architecture coherence and maintainability risk",
|
|
6522
|
+
reportSuffix: "qc1"
|
|
6523
|
+
},
|
|
6524
|
+
{
|
|
6525
|
+
roleId: "qc-specialist-2",
|
|
6526
|
+
reviewerIndex: 2,
|
|
6527
|
+
focus: "Security and correctness risk",
|
|
6528
|
+
reportSuffix: "qc2"
|
|
6529
|
+
},
|
|
6530
|
+
{
|
|
6531
|
+
roleId: "qc-specialist-3",
|
|
6532
|
+
reviewerIndex: 3,
|
|
6533
|
+
focus: "Performance and reliability risk",
|
|
6534
|
+
reportSuffix: "qc3"
|
|
6535
|
+
}
|
|
6536
|
+
];
|
|
6537
|
+
function validateRoleMapping(rolesDir, options = {}) {
|
|
6538
|
+
const mapping = options.mapping ?? ROLE_MAPPING;
|
|
6539
|
+
const families = options.families ?? SHARED_FAMILIES;
|
|
6540
|
+
const devTrack = options.devTrack ?? DEV_TRACK_PARAMS;
|
|
6541
|
+
const qcReviewers = options.qcReviewers ?? QC_REVIEWER_PARAMS;
|
|
6542
|
+
const violations = [];
|
|
6543
|
+
const referenceById = new Map(mapping.map((m) => [m.agentId, m.reference]));
|
|
6544
|
+
for (const { agentId, reference } of mapping) {
|
|
6545
|
+
if (!existsSync6(join9(rolesDir, reference))) {
|
|
6546
|
+
violations.push(violation10("medium", "roles.mapping.reference.missing", `role "${agentId}" maps to ${reference} which does not exist under ${rolesDir} (mstar-roles \u00a7 Role Reference Mapping)`, `create ${join9(rolesDir, reference)} or fix the mapping row`));
|
|
6547
|
+
}
|
|
6548
|
+
}
|
|
6549
|
+
for (const { family, memberIds } of families) {
|
|
6550
|
+
const absent = memberIds.filter((id) => !referenceById.has(id));
|
|
6551
|
+
for (const id of absent) {
|
|
6552
|
+
violations.push(violation10("medium", "roles.mapping.family.member.missing", `shared family "${family}" member "${id}" is absent from the role mapping (mstar-roles \u00a7 Role Reference Mapping)`, `add "${id}" to the mapping`));
|
|
6553
|
+
}
|
|
6554
|
+
if (absent.length === 0) {
|
|
6555
|
+
const refs = new Set(memberIds.map((id) => referenceById.get(id)));
|
|
6556
|
+
if (refs.size !== 1) {
|
|
6557
|
+
violations.push(violation10("medium", "roles.mapping.family.shared", `shared family "${family}" (${memberIds.join(", ")}) must resolve to ONE shared reference file \u2014 got ${[...refs].join(", ")} (mstar-roles \u00a7 Maintenance Rules: "Keep shared-family roles on one shared reference file")`, `point every "${family}" member at the same references/<role>-shared.md`));
|
|
6558
|
+
}
|
|
6559
|
+
}
|
|
6560
|
+
}
|
|
6561
|
+
const tableByRole = new Map;
|
|
6562
|
+
const checkParamRoles = (rows, table) => {
|
|
6563
|
+
for (const row of rows) {
|
|
6564
|
+
const existing = tableByRole.get(row.roleId);
|
|
6565
|
+
if (existing !== undefined) {
|
|
6566
|
+
violations.push(violation10("medium", "roles.param.role.duplicate", `role "${row.roleId}" appears in both the ${existing} and ${table} parameter rows (mstar-roles \u00a7 Parameter Table (SSOT))`, "remove the duplicate row"));
|
|
6567
|
+
} else {
|
|
6568
|
+
tableByRole.set(row.roleId, table);
|
|
6569
|
+
}
|
|
6570
|
+
if (!referenceById.has(row.roleId)) {
|
|
6571
|
+
violations.push(violation10("medium", "roles.param.role.missing", `${table} parameter row references unknown role "${row.roleId}" (mstar-roles \u00a7 Parameter Table (SSOT))`, `add "${row.roleId}" to the role mapping or drop the row`));
|
|
6572
|
+
}
|
|
6573
|
+
}
|
|
6574
|
+
};
|
|
6575
|
+
checkParamRoles(devTrack, "dev track");
|
|
6576
|
+
checkParamRoles(qcReviewers, "QC reviewer");
|
|
6577
|
+
for (const row of devTrack) {
|
|
6578
|
+
if (row.track !== "primary" && row.track !== "parallel_secondary") {
|
|
6579
|
+
violations.push(violation10("medium", "roles.param.track", `dev track for "${row.roleId}" is "${String(row.track)}" \u2014 must be primary or parallel_secondary (mstar-roles \u00a7 Parameter Table (SSOT))`, 'set track to "primary" or "parallel_secondary"'));
|
|
6580
|
+
}
|
|
6581
|
+
}
|
|
6582
|
+
const indices = qcReviewers.map((r) => r.reviewerIndex).sort((a, b) => a - b);
|
|
6583
|
+
const unique = new Set(indices);
|
|
6584
|
+
if (indices.length !== 3 || unique.size !== 3 || indices[0] !== 1 || indices[1] !== 2 || indices[2] !== 3) {
|
|
6585
|
+
violations.push(violation10("high", "roles.param.qc.index.set", `QC reviewer_index must be exactly {1, 2, 3} across the three qc-specialist* seats \u2014 got [${indices.join(", ")}] (mstar-roles \u00a7 Parameter Table (SSOT))`, "assign reviewer_index 1/2/3 to qc-specialist / qc-specialist-2 / qc-specialist-3"));
|
|
6586
|
+
}
|
|
6587
|
+
for (const row of qcReviewers) {
|
|
6588
|
+
if (row.focus.trim() === "") {
|
|
6589
|
+
violations.push(violation10("medium", "roles.param.qc.focus.missing", `QC seat "${row.roleId}" (reviewer_index ${row.reviewerIndex}) has an empty focus (mstar-roles \u00a7 Parameter Table (SSOT))`, "add the review focus"));
|
|
6590
|
+
}
|
|
6591
|
+
if (row.reportSuffix !== `qc${row.reviewerIndex}`) {
|
|
6592
|
+
violations.push(violation10("medium", "roles.param.qc.suffix", `QC seat "${row.roleId}" report_suffix "${row.reportSuffix}" must equal qc${row.reviewerIndex} \u2014 tri reports land at {SDD_DIR}/review/qc1.md\u2026qc3.md (mstar-roles \u00a7 Parameter Table (SSOT))`, `set report_suffix to qc${row.reviewerIndex}`));
|
|
6593
|
+
}
|
|
6594
|
+
}
|
|
6595
|
+
return { ok: violations.length === 0, violations };
|
|
6596
|
+
}
|
|
6597
|
+
var LOAD_ORDER_HEADING_RE = /^#{1,6}\s+[^\r\n]*\b(?:load[\s-]*order|first\s+action)\b[^\r\n]*$/i;
|
|
6598
|
+
function extractLoadOrderSection(text) {
|
|
6599
|
+
const lines = text.split(/\r?\n/);
|
|
6600
|
+
let start = -1;
|
|
6601
|
+
let level = 0;
|
|
6602
|
+
for (let i = 0;i < lines.length; i++) {
|
|
6603
|
+
const m = /^(#{1,6})\s+/.exec(lines[i]);
|
|
6604
|
+
if (m === null)
|
|
6605
|
+
continue;
|
|
6606
|
+
if (LOAD_ORDER_HEADING_RE.test(lines[i])) {
|
|
6607
|
+
start = i;
|
|
6608
|
+
level = m[1].length;
|
|
6609
|
+
break;
|
|
6610
|
+
}
|
|
6611
|
+
}
|
|
6612
|
+
if (start === -1)
|
|
6613
|
+
return null;
|
|
6614
|
+
const section = [lines[start]];
|
|
6615
|
+
for (let i = start + 1;i < lines.length; i++) {
|
|
6616
|
+
const m = /^(#{1,6})\s+/.exec(lines[i]);
|
|
6617
|
+
if (m !== null && m[1].length <= level)
|
|
6618
|
+
break;
|
|
6619
|
+
section.push(lines[i]);
|
|
6620
|
+
}
|
|
6621
|
+
return section.join(`
|
|
6622
|
+
`);
|
|
6623
|
+
}
|
|
6624
|
+
function lintLoadOrder(skillTexts) {
|
|
6625
|
+
const violations = [];
|
|
6626
|
+
for (const [name, text] of Object.entries(skillTexts)) {
|
|
6627
|
+
if (!name.startsWith("mstar-") || name === "mstar-harness-core")
|
|
6628
|
+
continue;
|
|
6629
|
+
const section = extractLoadOrderSection(text);
|
|
6630
|
+
if (section === null) {
|
|
6631
|
+
violations.push(violation10("medium", "roles.loadorder.section.missing", `skill "${name}" has no Load Order / First action section \u2014 every mstar-* topic skill must declare its first read (mstar-harness-core \u00a7 \u52a0\u8f7d\u7ea6\u5b9a; mstar-roles \u00a7 Load Order (Required))`, `add a "## Load Order" section naming mstar-harness-core as the first read`));
|
|
6632
|
+
continue;
|
|
6633
|
+
}
|
|
6634
|
+
if (!section.includes("mstar-harness-core")) {
|
|
6635
|
+
violations.push(violation10("medium", "roles.loadorder.core.missing", `skill "${name}" Load Order section does not declare mstar-harness-core as its first dependency (mstar-harness-core \u00a7 \u52a0\u8f7d\u7ea6\u5b9a: \u51e1 mstar-*\uff08name \u2260 mstar-harness-core\uff09\u5047\u5b9a\u8bfb\u8005\u5df2 Read \u672c skill)`, "name mstar-harness-core first in the Load Order section"));
|
|
6636
|
+
}
|
|
6637
|
+
}
|
|
6638
|
+
return { ok: violations.length === 0, violations };
|
|
6639
|
+
}
|
|
6325
6640
|
function detectHost(signals2) {
|
|
6326
6641
|
const s = new Set(signals2);
|
|
6327
6642
|
if (s.has("subagent_type"))
|
|
@@ -6340,6 +6655,27 @@ function detectHost(signals2) {
|
|
|
6340
6655
|
return "codex";
|
|
6341
6656
|
return "ambiguous";
|
|
6342
6657
|
}
|
|
6658
|
+
function resolveSkillRoot(host, paths) {
|
|
6659
|
+
const { skill, rel } = paths;
|
|
6660
|
+
const suffix = rel === undefined || rel === "" ? "" : `/${rel}`;
|
|
6661
|
+
switch (host) {
|
|
6662
|
+
case "omp":
|
|
6663
|
+
return `skill://${skill}${suffix}`;
|
|
6664
|
+
case "cursor":
|
|
6665
|
+
return `~/.cursor/plugins/local/morning-star-harness/skills/${skill}${suffix}`;
|
|
6666
|
+
case "codex":
|
|
6667
|
+
return `skills/${skill}${suffix}`;
|
|
6668
|
+
case "opencode":
|
|
6669
|
+
return `harness-skills/${skill}${suffix}`;
|
|
6670
|
+
case "kimi":
|
|
6671
|
+
case "zcode":
|
|
6672
|
+
return `./skills/${skill}${suffix}`;
|
|
6673
|
+
case "dsh":
|
|
6674
|
+
return `$DSH_BUNDLED_SKILL_DIR/${skill}${suffix}`;
|
|
6675
|
+
case "pi":
|
|
6676
|
+
return `deferred: pi has no plugin API in v1 \u2014 skill-root resolution lands with its adapter (roadmap \u00a78.4)`;
|
|
6677
|
+
}
|
|
6678
|
+
}
|
|
6343
6679
|
function violation11(severity, code, message, fix) {
|
|
6344
6680
|
return { ok: false, severity, code, message, fix };
|
|
6345
6681
|
}
|
|
@@ -6350,13 +6686,56 @@ var FIVE_QUESTION_SECTIONS = [
|
|
|
6350
6686
|
{ key: "evidence", label: "Evidence", question: "what a correct result looks like (success criteria / evidence)" },
|
|
6351
6687
|
{ key: "references", label: "References", question: "additional resources to open when the main path is not enough" }
|
|
6352
6688
|
];
|
|
6689
|
+
function stripFrontmatter(text) {
|
|
6690
|
+
const lines = text.replace(/^\uFEFF/, "").split(/\r?\n/);
|
|
6691
|
+
if (lines.length === 0 || lines[0].trim() !== "---")
|
|
6692
|
+
return text;
|
|
6693
|
+
for (let i = 1;i < lines.length; i++) {
|
|
6694
|
+
if (lines[i].trim() === "---")
|
|
6695
|
+
return lines.slice(i + 1).join(`
|
|
6696
|
+
`);
|
|
6697
|
+
}
|
|
6698
|
+
return text;
|
|
6699
|
+
}
|
|
6353
6700
|
var HEADING_RE = /^#{1,6}\s+[^\r\n]+$/;
|
|
6354
|
-
function
|
|
6355
|
-
const headings =
|
|
6701
|
+
function collectHeadings(bodyText) {
|
|
6702
|
+
const headings = [];
|
|
6703
|
+
let inFence = false;
|
|
6704
|
+
for (const line of bodyText.split(/\r?\n/)) {
|
|
6705
|
+
if (line.startsWith("```") || line.startsWith("~~~")) {
|
|
6706
|
+
inFence = !inFence;
|
|
6707
|
+
continue;
|
|
6708
|
+
}
|
|
6709
|
+
if (!inFence && HEADING_RE.test(line)) {
|
|
6710
|
+
headings.push(line.replace(/^#{1,6}\s+/, "").trim().toLowerCase());
|
|
6711
|
+
}
|
|
6712
|
+
}
|
|
6713
|
+
return headings;
|
|
6714
|
+
}
|
|
6715
|
+
var RUNTIME_HEADING_ALIASES = {
|
|
6716
|
+
workflow: ["process", "playbook"],
|
|
6717
|
+
"decision-rules": [
|
|
6718
|
+
"hard rules",
|
|
6719
|
+
"core rules",
|
|
6720
|
+
"rule",
|
|
6721
|
+
"gate",
|
|
6722
|
+
"not to do",
|
|
6723
|
+
"red flags",
|
|
6724
|
+
"\u53cd\u6a21\u5f0f",
|
|
6725
|
+
"\u7ea2\u7ebf",
|
|
6726
|
+
"\u89c4\u5219",
|
|
6727
|
+
"\u95e8\u7981"
|
|
6728
|
+
],
|
|
6729
|
+
evidence: ["output format", "\u8bc1\u636e"],
|
|
6730
|
+
references: ["dependencies", "\u5173\u7cfb"]
|
|
6731
|
+
};
|
|
6732
|
+
function lintFiveQuestion(bodyText, mode = "authoring") {
|
|
6733
|
+
const headings = collectHeadings(bodyText);
|
|
6356
6734
|
const violations = [];
|
|
6357
6735
|
for (const section of FIVE_QUESTION_SECTIONS) {
|
|
6358
6736
|
const label = section.label.toLowerCase();
|
|
6359
|
-
const
|
|
6737
|
+
const aliases = mode === "runtime" ? RUNTIME_HEADING_ALIASES[section.key] ?? [] : [];
|
|
6738
|
+
const covered = headings.some((heading) => heading.includes(label) || aliases.some((alias) => heading.includes(alias)));
|
|
6360
6739
|
if (!covered) {
|
|
6361
6740
|
violations.push(violation11("low", `skill-authoring.five-question.${section.key}`, `body does not answer "${section.question}" \u2014 no "${section.label}" section (mstar-skill-authoring \u00a7 Body \u5fc5\u987b\u56de\u7b54\u7684 5 \u95ee / \u00a7 \u9ed8\u8ba4 Body \u7ed3\u6784)`, `add a "## ${section.label}" section covering ${section.question}`));
|
|
6362
6741
|
}
|
|
@@ -6365,14 +6744,14 @@ function lintFiveQuestion(bodyText) {
|
|
|
6365
6744
|
}
|
|
6366
6745
|
|
|
6367
6746
|
// ../engine/dist/engine.js
|
|
6368
|
-
import { existsSync as
|
|
6747
|
+
import { existsSync as existsSync7, mkdirSync as mkdirSync6, readFileSync as readFileSync8, renameSync as renameSync2, unlinkSync as unlinkSync3, writeFileSync as writeFileSync5 } from "node:fs";
|
|
6369
6748
|
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
6370
6749
|
import { basename as basename5, dirname as dirname5, join as join6, resolve as resolve9 } from "node:path";
|
|
6371
6750
|
import { fileURLToPath } from "node:url";
|
|
6372
6751
|
import { dirname as dirname32, isAbsolute as isAbsolute22, join as join32, resolve as resolve32 } from "node:path";
|
|
6373
6752
|
import { AsyncLocalStorage as AsyncLocalStorage3 } from "node:async_hooks";
|
|
6374
6753
|
function readJson2(filePath) {
|
|
6375
|
-
if (!
|
|
6754
|
+
if (!existsSync7(filePath))
|
|
6376
6755
|
return {};
|
|
6377
6756
|
const content = readFileSync8(filePath, "utf8").trim();
|
|
6378
6757
|
if (!content)
|
|
@@ -6402,7 +6781,7 @@ function resolveProjectRoot(startDir = process.cwd()) {
|
|
|
6402
6781
|
const start = resolve9(startDir);
|
|
6403
6782
|
let dir = start;
|
|
6404
6783
|
for (;; ) {
|
|
6405
|
-
if (
|
|
6784
|
+
if (existsSync7(join6(dir, "package.json")) || existsSync7(join6(dir, "bun.lock")))
|
|
6406
6785
|
return dir;
|
|
6407
6786
|
const parent = dirname5(dir);
|
|
6408
6787
|
if (parent === dir)
|
|
@@ -6471,20 +6850,20 @@ var GITIGNORE_PROCESS_ENTRIES_AGENTS2 = GITIGNORE_SNIPPET_AGENTS2.split(`
|
|
|
6471
6850
|
function isPlainObject4(value) {
|
|
6472
6851
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
6473
6852
|
}
|
|
6474
|
-
function
|
|
6853
|
+
function violation7(severity, code, message, fix) {
|
|
6475
6854
|
return { ok: false, severity, code, message, fix };
|
|
6476
6855
|
}
|
|
6477
|
-
function
|
|
6856
|
+
function validateNonEmptyString3(violations, value, field, missingCode, invalidCode) {
|
|
6478
6857
|
if (value === undefined) {
|
|
6479
|
-
violations.push(
|
|
6858
|
+
violations.push(violation7("high", missingCode, `missing required field: ${field}`));
|
|
6480
6859
|
} else if (typeof value !== "string" || value.trim() === "") {
|
|
6481
|
-
violations.push(
|
|
6860
|
+
violations.push(violation7("medium", invalidCode, `${field} must be a non-empty string`));
|
|
6482
6861
|
}
|
|
6483
6862
|
}
|
|
6484
6863
|
var DATE_PART2 = String.raw`\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12]\d|3[01])`;
|
|
6485
6864
|
var RFC3339_Z_RE2 = new RegExp(String.raw`^${DATE_PART2}T\d{2}:\d{2}:\d{2}(\.\d+)?Z$`);
|
|
6486
6865
|
var DATE_ONLY_RE2 = new RegExp(String.raw`^${DATE_PART2}$`);
|
|
6487
|
-
function
|
|
6866
|
+
function isValidClaimedAt2(value) {
|
|
6488
6867
|
return typeof value === "string" && (RFC3339_Z_RE2.test(value) || DATE_ONLY_RE2.test(value));
|
|
6489
6868
|
}
|
|
6490
6869
|
function validateExecutionLease(lease) {
|
|
@@ -6493,26 +6872,26 @@ function validateExecutionLease(lease) {
|
|
|
6493
6872
|
return {
|
|
6494
6873
|
ok: false,
|
|
6495
6874
|
violations: [
|
|
6496
|
-
|
|
6875
|
+
violation7("high", "lease.execution-lease.invalid", "execution_lease must be an object \u2014 null and tombstone objects are invalid; writers delete the key on release")
|
|
6497
6876
|
]
|
|
6498
6877
|
};
|
|
6499
6878
|
}
|
|
6500
|
-
|
|
6879
|
+
validateNonEmptyString3(violations, lease.holder, "holder", "lease.execution-lease.missing-holder", "lease.execution-lease.invalid-holder");
|
|
6501
6880
|
if (lease.claimed_at === undefined) {
|
|
6502
|
-
violations.push(
|
|
6503
|
-
} else if (!
|
|
6504
|
-
violations.push(
|
|
6881
|
+
violations.push(violation7("high", "lease.execution-lease.missing-claimed-at", "missing required field: claimed_at"));
|
|
6882
|
+
} else if (!isValidClaimedAt2(lease.claimed_at)) {
|
|
6883
|
+
violations.push(violation7("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"));
|
|
6505
6884
|
}
|
|
6506
6885
|
if (lease.worktree_path === undefined) {
|
|
6507
|
-
violations.push(
|
|
6886
|
+
violations.push(violation7("high", "lease.execution-lease.missing-worktree-path", "missing required field: worktree_path"));
|
|
6508
6887
|
} else if (typeof lease.worktree_path !== "string" || lease.worktree_path.trim() === "") {
|
|
6509
|
-
violations.push(
|
|
6888
|
+
violations.push(violation7("medium", "lease.execution-lease.invalid-worktree-path", "worktree_path must be a non-empty string"));
|
|
6510
6889
|
} else if (!isAbsolute22(lease.worktree_path)) {
|
|
6511
|
-
violations.push(
|
|
6890
|
+
violations.push(violation7("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)"));
|
|
6512
6891
|
}
|
|
6513
|
-
|
|
6892
|
+
validateNonEmptyString3(violations, lease.working_branch, "working_branch", "lease.execution-lease.missing-working-branch", "lease.execution-lease.invalid-working-branch");
|
|
6514
6893
|
if (lease.session_label !== undefined && typeof lease.session_label !== "string") {
|
|
6515
|
-
violations.push(
|
|
6894
|
+
violations.push(violation7("medium", "lease.execution-lease.invalid-session-label", "session_label must be a string (display only \u2014 never used for ownership comparison)"));
|
|
6516
6895
|
}
|
|
6517
6896
|
return { ok: violations.length === 0, violations };
|
|
6518
6897
|
}
|
|
@@ -6529,22 +6908,22 @@ function verifyPlanExecutionLease(row, planId) {
|
|
|
6529
6908
|
return {
|
|
6530
6909
|
ok: false,
|
|
6531
6910
|
violations: [
|
|
6532
|
-
|
|
6911
|
+
violation7("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)")
|
|
6533
6912
|
]
|
|
6534
6913
|
};
|
|
6535
6914
|
}
|
|
6536
6915
|
return {
|
|
6537
6916
|
ok: false,
|
|
6538
6917
|
violations: [
|
|
6539
|
-
|
|
6918
|
+
violation7("high", "lease.verify.missing", `plan ${planId} has no execution_lease (neither plans[].execution_lease nor legacy plans[].metadata.execution_lease)`)
|
|
6540
6919
|
]
|
|
6541
6920
|
};
|
|
6542
6921
|
}
|
|
6543
6922
|
const violations = [];
|
|
6544
6923
|
if (rowLease !== undefined && metadataLease !== undefined) {
|
|
6545
|
-
violations.push(
|
|
6924
|
+
violations.push(violation7("high", "lease.verify.dual-write", "execution_lease present in BOTH plans[].execution_lease (SSOT) and plans[].metadata.execution_lease \u2014 the row-level lease wins; delete the metadata copy to remove the dual write"));
|
|
6546
6925
|
} else if (rowLease === undefined) {
|
|
6547
|
-
violations.push(
|
|
6926
|
+
violations.push(violation7("high", "lease.verify.non-ssot-location", "execution_lease found only under plans[].metadata.execution_lease \u2014 the SSOT location is plans[].execution_lease; the metadata location is a legacy/hand-written read-compat fallback, not equivalent to SSOT success (migrate the lease to the plan row)"));
|
|
6548
6927
|
}
|
|
6549
6928
|
violations.push(...validateExecutionLease(lease).violations);
|
|
6550
6929
|
return { ok: violations.length === 0, violations, lease };
|
|
@@ -7533,10 +7912,178 @@ var cursorAdapter = {
|
|
|
7533
7912
|
}
|
|
7534
7913
|
};
|
|
7535
7914
|
|
|
7915
|
+
// src/adapters/dsh.ts
|
|
7916
|
+
import { execFileSync as execFileSync6 } from "node:child_process";
|
|
7917
|
+
import os4 from "node:os";
|
|
7918
|
+
import path7 from "node:path";
|
|
7919
|
+
var DSH_BIN = "dsh";
|
|
7920
|
+
var DSH_PROFILE = "web";
|
|
7921
|
+
var DSH_PROFILE_FLAG = "--profile";
|
|
7922
|
+
var DSH_DUMP_FLAG = "--dump-config";
|
|
7923
|
+
var DSH_HOME_ENV = "DSH_HOME";
|
|
7924
|
+
var DSH_HOME_SUBDIR = ".dsh";
|
|
7925
|
+
var DSH_PROFILES_DIR = "profiles";
|
|
7926
|
+
var DSH_LOCAL_TIMEOUT_MS = 1e4;
|
|
7927
|
+
var DSH_ADD_TIMEOUT_MS = 300000;
|
|
7928
|
+
var DSH_ADD_TIMEOUT_ENV = "MSTAR_DSH_SUBPROCESS_TIMEOUT_MS";
|
|
7929
|
+
function addTimeoutMs() {
|
|
7930
|
+
const raw = process.env[DSH_ADD_TIMEOUT_ENV];
|
|
7931
|
+
if (raw === undefined || raw.trim() === "")
|
|
7932
|
+
return DSH_ADD_TIMEOUT_MS;
|
|
7933
|
+
const parsed = Number(raw);
|
|
7934
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : DSH_ADD_TIMEOUT_MS;
|
|
7935
|
+
}
|
|
7936
|
+
var DSH_PLUGIN_SPECS = ["@mstar-harness/dsh", "dsh-llm-fallbacks"];
|
|
7937
|
+
var DSH_FALLBACKS_SPEC = DSH_PLUGIN_SPECS[1];
|
|
7938
|
+
var DSH_INSTALL_HINT = "Install the DeepSeek Harness CLI (@deepseek-ai/dsh), e.g. `pnpm add -g @deepseek-ai/dsh` or `npm install -g @deepseek-ai/dsh`, then re-run init.";
|
|
7939
|
+
function runDsh(args, dryRun, timeoutMs) {
|
|
7940
|
+
if (dryRun)
|
|
7941
|
+
return "";
|
|
7942
|
+
return execFileSync6(DSH_BIN, args, {
|
|
7943
|
+
stdio: "pipe",
|
|
7944
|
+
encoding: "utf8",
|
|
7945
|
+
env: process.env,
|
|
7946
|
+
timeout: timeoutMs
|
|
7947
|
+
});
|
|
7948
|
+
}
|
|
7949
|
+
function dshAvailable() {
|
|
7950
|
+
try {
|
|
7951
|
+
runDsh(["--version"], false, DSH_LOCAL_TIMEOUT_MS);
|
|
7952
|
+
return true;
|
|
7953
|
+
} catch {
|
|
7954
|
+
return false;
|
|
7955
|
+
}
|
|
7956
|
+
}
|
|
7957
|
+
function resolveProfileDir() {
|
|
7958
|
+
const dshHome = process.env[DSH_HOME_ENV] ?? path7.join(os4.homedir(), DSH_HOME_SUBDIR);
|
|
7959
|
+
return path7.join(dshHome, DSH_PROFILES_DIR, DSH_PROFILE);
|
|
7960
|
+
}
|
|
7961
|
+
var DISABLED_MARKERS = /\b(?:disabled: true|enabled: false)\b/;
|
|
7962
|
+
function parseLoaderEntries(dump) {
|
|
7963
|
+
const entries = [];
|
|
7964
|
+
let current = null;
|
|
7965
|
+
for (const line of dump.split(`
|
|
7966
|
+
`)) {
|
|
7967
|
+
if (/^- id: /.test(line)) {
|
|
7968
|
+
if (current)
|
|
7969
|
+
entries.push(current);
|
|
7970
|
+
current = { name: "", enabled: !DISABLED_MARKERS.test(line) };
|
|
7971
|
+
} else if (current) {
|
|
7972
|
+
const nameMatch = /^ name: (.+)$/.exec(line);
|
|
7973
|
+
if (nameMatch) {
|
|
7974
|
+
let name = nameMatch[1].trim();
|
|
7975
|
+
if (DISABLED_MARKERS.test(line)) {
|
|
7976
|
+
current.enabled = false;
|
|
7977
|
+
name = name.replace(/\s*,?\s*(?:disabled: true|enabled: false)\s*$/, "");
|
|
7978
|
+
}
|
|
7979
|
+
current.name = name.replace(/^['"]|['"]$/g, "");
|
|
7980
|
+
} else if (/^ disabled: true$/.test(line) || /^ enabled: false$/.test(line)) {
|
|
7981
|
+
current.enabled = false;
|
|
7982
|
+
} else if (line.trim() !== "" && !line.startsWith(" ")) {
|
|
7983
|
+
entries.push(current);
|
|
7984
|
+
current = null;
|
|
7985
|
+
}
|
|
7986
|
+
}
|
|
7987
|
+
}
|
|
7988
|
+
if (current)
|
|
7989
|
+
entries.push(current);
|
|
7990
|
+
if (dump.trim() !== "" && (entries.length === 0 || entries.some((entry) => !entry.name))) {
|
|
7991
|
+
return null;
|
|
7992
|
+
}
|
|
7993
|
+
return entries;
|
|
7994
|
+
}
|
|
7995
|
+
function runInit2(scope, dryRun, initFlags) {
|
|
7996
|
+
const notes = [];
|
|
7997
|
+
const profileDir = resolveProfileDir();
|
|
7998
|
+
if (!dryRun && !dshAvailable()) {
|
|
7999
|
+
throw new Error(`${DSH_BIN} CLI not found on PATH. ${DSH_INSTALL_HINT}`);
|
|
8000
|
+
}
|
|
8001
|
+
const installed = new Set;
|
|
8002
|
+
if (!dryRun) {
|
|
8003
|
+
try {
|
|
8004
|
+
const entries = parseLoaderEntries(runDsh([DSH_PROFILE_FLAG, DSH_PROFILE, DSH_DUMP_FLAG], dryRun, DSH_LOCAL_TIMEOUT_MS));
|
|
8005
|
+
if (entries === null) {
|
|
8006
|
+
notes.push("Warning: could not parse installed plugins from dump (unexpected format); proceeding with add (idempotent).");
|
|
8007
|
+
} else {
|
|
8008
|
+
for (const entry of entries)
|
|
8009
|
+
installed.add(entry.name);
|
|
8010
|
+
}
|
|
8011
|
+
} catch (error) {
|
|
8012
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
8013
|
+
notes.push(`Warning: could not probe installed plugins (${message}); proceeding with add (idempotent).`);
|
|
8014
|
+
}
|
|
8015
|
+
}
|
|
8016
|
+
for (const spec of DSH_PLUGIN_SPECS) {
|
|
8017
|
+
if (spec === DSH_FALLBACKS_SPEC && initFlags?.noFallbacks) {
|
|
8018
|
+
notes.push(`skipped-by-flag: ${spec} (--no-fallbacks)`);
|
|
8019
|
+
continue;
|
|
8020
|
+
}
|
|
8021
|
+
if (!dryRun && installed.has(spec)) {
|
|
8022
|
+
notes.push(`skipped-existing: ${spec} (already installed in profile ${DSH_PROFILE})`);
|
|
8023
|
+
continue;
|
|
8024
|
+
}
|
|
8025
|
+
const addArgs = ["plugin", DSH_PROFILE_FLAG, DSH_PROFILE, "add", spec];
|
|
8026
|
+
if (dryRun) {
|
|
8027
|
+
notes.push(`Would run: ${DSH_BIN} ${addArgs.join(" ")}`);
|
|
8028
|
+
continue;
|
|
8029
|
+
}
|
|
8030
|
+
try {
|
|
8031
|
+
runDsh(addArgs, dryRun, addTimeoutMs());
|
|
8032
|
+
notes.push(`installed: ${spec} (${DSH_BIN} ${addArgs.join(" ")})`);
|
|
8033
|
+
} catch (error) {
|
|
8034
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
8035
|
+
throw new Error(`Failed to install ${spec} via ${DSH_BIN}: ${message}`);
|
|
8036
|
+
}
|
|
8037
|
+
}
|
|
8038
|
+
notes.push(`Profile: ${DSH_PROFILE} at ${profileDir}`);
|
|
8039
|
+
notes.push("Verify with: mstar-harness doctor --target dsh");
|
|
8040
|
+
notes.push(`Alternate manual install: ${DSH_BIN} plugin ${DSH_PROFILE_FLAG} ${DSH_PROFILE} add ${DSH_PLUGIN_SPECS.join(` && ${DSH_BIN} plugin ${DSH_PROFILE_FLAG} ${DSH_PROFILE} add `)}`);
|
|
8041
|
+
return { location: profileDir, notes };
|
|
8042
|
+
}
|
|
8043
|
+
function runDoctor2(scope) {
|
|
8044
|
+
const errors2 = [];
|
|
8045
|
+
const notes = [];
|
|
8046
|
+
const profileDir = resolveProfileDir();
|
|
8047
|
+
if (!dshAvailable()) {
|
|
8048
|
+
errors2.push(`${DSH_BIN} CLI not found on PATH. ${DSH_INSTALL_HINT}`);
|
|
8049
|
+
return { location: profileDir, errors: errors2, notes };
|
|
8050
|
+
}
|
|
8051
|
+
let dump;
|
|
8052
|
+
try {
|
|
8053
|
+
dump = runDsh([DSH_PROFILE_FLAG, DSH_PROFILE, DSH_DUMP_FLAG], false, DSH_LOCAL_TIMEOUT_MS);
|
|
8054
|
+
} catch (error) {
|
|
8055
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
8056
|
+
errors2.push(`Warning: could not probe installed plugins (${message}); cannot verify install state.`);
|
|
8057
|
+
return { location: profileDir, errors: errors2, notes };
|
|
8058
|
+
}
|
|
8059
|
+
const entries = parseLoaderEntries(dump);
|
|
8060
|
+
if (entries === null) {
|
|
8061
|
+
errors2.push("Warning: could not parse installed plugins from dump (unexpected format); cannot verify install state.");
|
|
8062
|
+
return { location: profileDir, errors: errors2, notes };
|
|
8063
|
+
}
|
|
8064
|
+
const byName = new Map(entries.map((entry) => [entry.name, entry]));
|
|
8065
|
+
for (const spec of DSH_PLUGIN_SPECS) {
|
|
8066
|
+
const entry = byName.get(spec);
|
|
8067
|
+
const state = !entry ? "uninstalled" : entry.enabled ? "mounted" : "disabled";
|
|
8068
|
+
notes.push(`${spec}: ${state}`);
|
|
8069
|
+
if (state === "mounted")
|
|
8070
|
+
continue;
|
|
8071
|
+
const hint = state === "uninstalled" ? "Run: mstar-harness init --target dsh" : "Enable it (e.g. remove the disable entry from cordis.patch.yml) and re-run doctor.";
|
|
8072
|
+
errors2.push(`${spec} is ${state}. ${hint}`);
|
|
8073
|
+
}
|
|
8074
|
+
return { location: profileDir, errors: errors2, notes };
|
|
8075
|
+
}
|
|
8076
|
+
var dshAdapter = {
|
|
8077
|
+
target: "dsh",
|
|
8078
|
+
mode: "install",
|
|
8079
|
+
runInstallInit: (scope, dryRun, initFlags) => runInit2(scope, dryRun, initFlags),
|
|
8080
|
+
runInstallDoctor: (scope) => runDoctor2(scope)
|
|
8081
|
+
};
|
|
8082
|
+
|
|
7536
8083
|
// src/adapters/omp.ts
|
|
7537
8084
|
import fs5 from "node:fs";
|
|
7538
|
-
import
|
|
7539
|
-
import { execFileSync as
|
|
8085
|
+
import path8 from "node:path";
|
|
8086
|
+
import { execFileSync as execFileSync7 } from "node:child_process";
|
|
7540
8087
|
var OMP_PLUGIN_MARKER = ".omp-plugin/plugin.json";
|
|
7541
8088
|
var CLAUDE_PLUGIN_MARKER = ".claude-plugin/plugin.json";
|
|
7542
8089
|
var PACKAGE_NAMES = new Set(["morning-star", PLUGIN_NAME, "github:btspoony/mstar-harness"]);
|
|
@@ -7544,7 +8091,7 @@ var SKILL_SMOKE = ["mstar-host", "mstar-harness-core", "pm"];
|
|
|
7544
8091
|
var COMMAND_SMOKE = ["iteration-start", "iteration-drive", "iteration-loop", "codebase-audit"];
|
|
7545
8092
|
function ompAvailable() {
|
|
7546
8093
|
try {
|
|
7547
|
-
|
|
8094
|
+
execFileSync7("omp", ["--version"], { stdio: "pipe", encoding: "utf8" });
|
|
7548
8095
|
return true;
|
|
7549
8096
|
} catch {
|
|
7550
8097
|
return false;
|
|
@@ -7553,11 +8100,11 @@ function ompAvailable() {
|
|
|
7553
8100
|
function runOmp(args, dryRun) {
|
|
7554
8101
|
if (dryRun)
|
|
7555
8102
|
return;
|
|
7556
|
-
|
|
8103
|
+
execFileSync7("omp", args, { stdio: "pipe", encoding: "utf8" });
|
|
7557
8104
|
}
|
|
7558
8105
|
function listInstalledPlugins() {
|
|
7559
8106
|
try {
|
|
7560
|
-
const raw =
|
|
8107
|
+
const raw = execFileSync7("omp", ["plugin", "list", "--json"], {
|
|
7561
8108
|
stdio: "pipe",
|
|
7562
8109
|
encoding: "utf8"
|
|
7563
8110
|
});
|
|
@@ -7597,7 +8144,7 @@ function findInstalledPlugin(plugins) {
|
|
|
7597
8144
|
return true;
|
|
7598
8145
|
if (name.includes("morning-star") || manifestName.includes("morning-star"))
|
|
7599
8146
|
return true;
|
|
7600
|
-
if (pathValue.includes("mstar-harness") || pathValue.includes(`${
|
|
8147
|
+
if (pathValue.includes("mstar-harness") || pathValue.includes(`${path8.sep}morning-star`))
|
|
7601
8148
|
return true;
|
|
7602
8149
|
return false;
|
|
7603
8150
|
});
|
|
@@ -7605,35 +8152,35 @@ function findInstalledPlugin(plugins) {
|
|
|
7605
8152
|
function validatePluginTree(pluginRoot) {
|
|
7606
8153
|
const errors2 = [];
|
|
7607
8154
|
for (const marker of [OMP_PLUGIN_MARKER, CLAUDE_PLUGIN_MARKER]) {
|
|
7608
|
-
const markerPath =
|
|
8155
|
+
const markerPath = path8.join(pluginRoot, marker);
|
|
7609
8156
|
if (!fs5.existsSync(markerPath)) {
|
|
7610
8157
|
errors2.push(`Missing omp plugin marker: ${markerPath}`);
|
|
7611
8158
|
}
|
|
7612
8159
|
}
|
|
7613
8160
|
for (const skill of SKILL_SMOKE) {
|
|
7614
|
-
const skillPath =
|
|
8161
|
+
const skillPath = path8.join(pluginRoot, "skills", skill, "SKILL.md");
|
|
7615
8162
|
if (!fs5.existsSync(skillPath))
|
|
7616
8163
|
errors2.push(`Missing skill: ${skillPath}`);
|
|
7617
8164
|
}
|
|
7618
8165
|
for (const command of COMMAND_SMOKE) {
|
|
7619
|
-
const commandPath =
|
|
8166
|
+
const commandPath = path8.join(pluginRoot, "commands", `${command}.md`);
|
|
7620
8167
|
if (!fs5.existsSync(commandPath))
|
|
7621
8168
|
errors2.push(`Missing command: ${commandPath}`);
|
|
7622
8169
|
}
|
|
7623
|
-
const hostRef =
|
|
8170
|
+
const hostRef = path8.join(pluginRoot, "skills", "mstar-host", "references", "omp.md");
|
|
7624
8171
|
if (!fs5.existsSync(hostRef))
|
|
7625
8172
|
errors2.push(`Missing omp host reference: ${hostRef}`);
|
|
7626
8173
|
return errors2;
|
|
7627
8174
|
}
|
|
7628
|
-
function
|
|
8175
|
+
function runInit3(scope, dryRun) {
|
|
7629
8176
|
const notes = ensureLocalHarnessRepo(dryRun);
|
|
7630
8177
|
const projectRoot = resolveProjectRoot2();
|
|
7631
|
-
if (fs5.existsSync(
|
|
8178
|
+
if (fs5.existsSync(path8.join(HARNESS_REPO_PATH, ".git"))) {
|
|
7632
8179
|
if (dryRun) {
|
|
7633
8180
|
notes.push(`Would update local harness repo: git -C ${HARNESS_REPO_PATH} pull --ff-only`);
|
|
7634
8181
|
} else {
|
|
7635
8182
|
try {
|
|
7636
|
-
|
|
8183
|
+
execFileSync7("git", ["-C", HARNESS_REPO_PATH, "pull", "--ff-only"], {
|
|
7637
8184
|
stdio: "pipe",
|
|
7638
8185
|
encoding: "utf8"
|
|
7639
8186
|
});
|
|
@@ -7685,7 +8232,7 @@ function runInit2(scope, dryRun) {
|
|
|
7685
8232
|
notes
|
|
7686
8233
|
};
|
|
7687
8234
|
}
|
|
7688
|
-
function
|
|
8235
|
+
function runDoctor3(scope) {
|
|
7689
8236
|
const errors2 = [];
|
|
7690
8237
|
errors2.push(...validateLocalHarnessRepo());
|
|
7691
8238
|
errors2.push(...validatePluginTree(HARNESS_REPO_PATH));
|
|
@@ -7702,7 +8249,7 @@ function runDoctor2(scope) {
|
|
|
7702
8249
|
}
|
|
7703
8250
|
if (scope === "project") {
|
|
7704
8251
|
const projectRoot = resolveProjectRoot2();
|
|
7705
|
-
const gitignorePath =
|
|
8252
|
+
const gitignorePath = path8.join(projectRoot, ".gitignore");
|
|
7706
8253
|
const gitignore = fs5.existsSync(gitignorePath) ? fs5.readFileSync(gitignorePath, "utf8") : "";
|
|
7707
8254
|
for (const entry of missingHarnessProcessGitignoreEntries(gitignore)) {
|
|
7708
8255
|
errors2.push(`Missing .gitignore entry: ${entry}`);
|
|
@@ -7713,13 +8260,13 @@ function runDoctor2(scope) {
|
|
|
7713
8260
|
var ompAdapter = {
|
|
7714
8261
|
target: "omp",
|
|
7715
8262
|
mode: "install",
|
|
7716
|
-
runInstallInit: (scope, dryRun) =>
|
|
7717
|
-
runInstallDoctor: (scope) =>
|
|
8263
|
+
runInstallInit: (scope, dryRun) => runInit3(scope, dryRun),
|
|
8264
|
+
runInstallDoctor: (scope) => runDoctor3(scope)
|
|
7718
8265
|
};
|
|
7719
8266
|
|
|
7720
8267
|
// src/adapters/opencode.ts
|
|
7721
|
-
import
|
|
7722
|
-
import
|
|
8268
|
+
import os5 from "node:os";
|
|
8269
|
+
import path9 from "node:path";
|
|
7723
8270
|
var OPENCODE_CONFIG_SCHEMA = "https://opencode.ai/config.json";
|
|
7724
8271
|
var MSTAR_OPENCODE_PLUGIN = "@mstar-harness/opencode@latest";
|
|
7725
8272
|
function isLegacyMorningStarGitPlugin(plugin) {
|
|
@@ -7740,11 +8287,11 @@ function isAnyMstarHarnessOpencodeSlot(plugin) {
|
|
|
7740
8287
|
function resolveOpencodeConfigPath(scope, outputPath) {
|
|
7741
8288
|
if (outputPath && outputPath.trim()) {
|
|
7742
8289
|
const raw = outputPath.trim();
|
|
7743
|
-
return
|
|
8290
|
+
return path9.isAbsolute(raw) ? raw : path9.join(resolveProjectRoot2(), raw);
|
|
7744
8291
|
}
|
|
7745
8292
|
if (scope === "global")
|
|
7746
|
-
return
|
|
7747
|
-
return
|
|
8293
|
+
return path9.join(os5.homedir(), ".config", "opencode", "opencode.json");
|
|
8294
|
+
return path9.join(resolveProjectRoot2(), "opencode.json");
|
|
7748
8295
|
}
|
|
7749
8296
|
function ensureConfigSchema(config) {
|
|
7750
8297
|
const next = ensureObject(config);
|
|
@@ -7838,8 +8385,8 @@ var opencodeAdapter = {
|
|
|
7838
8385
|
|
|
7839
8386
|
// src/adapters/zcode.ts
|
|
7840
8387
|
import fs6 from "node:fs";
|
|
7841
|
-
import
|
|
7842
|
-
import
|
|
8388
|
+
import os6 from "node:os";
|
|
8389
|
+
import path10 from "node:path";
|
|
7843
8390
|
var MARKETPLACE_ID = "mstar-local";
|
|
7844
8391
|
var MARKETPLACE_NAME2 = "mstar-local";
|
|
7845
8392
|
var MARKETPLACE_DESCRIPTION = "Morning Star harness marketplace (GitHub source).";
|
|
@@ -7850,10 +8397,10 @@ var GITHUB_REF = "main";
|
|
|
7850
8397
|
var ZCODE_PLUGIN_MARKER = ".zcode-plugin/plugin.json";
|
|
7851
8398
|
var ZCODE_PLUGIN_CHECKOUT_PROJECT = ".zcode/plugin-checkout";
|
|
7852
8399
|
var ZCODE_AGENT_SMOKE_NAMES = ["fullstack-dev", "qc-specialist"];
|
|
7853
|
-
var ZCODE_PLUGINS_ROOT =
|
|
7854
|
-
var KNOWN_MARKETPLACES_PATH =
|
|
7855
|
-
var MARKETPLACE_DIR =
|
|
7856
|
-
var MARKETPLACE_JSON_PATH =
|
|
8400
|
+
var ZCODE_PLUGINS_ROOT = path10.join(os6.homedir(), ".zcode", "cli", "plugins");
|
|
8401
|
+
var KNOWN_MARKETPLACES_PATH = path10.join(ZCODE_PLUGINS_ROOT, "known_marketplaces.json");
|
|
8402
|
+
var MARKETPLACE_DIR = path10.join(ZCODE_PLUGINS_ROOT, "marketplaces", MARKETPLACE_ID);
|
|
8403
|
+
var MARKETPLACE_JSON_PATH = path10.join(MARKETPLACE_DIR, "marketplace.json");
|
|
7857
8404
|
var GITHUB_SOURCE = { source: "github", repo: GITHUB_REPO, ref: GITHUB_REF };
|
|
7858
8405
|
function nowIso() {
|
|
7859
8406
|
return new Date().toISOString();
|
|
@@ -7961,13 +8508,13 @@ function validateKnownMarketplaces() {
|
|
|
7961
8508
|
}
|
|
7962
8509
|
function validatePluginAgents2(pluginRoot) {
|
|
7963
8510
|
const errors2 = [];
|
|
7964
|
-
const agentsDir =
|
|
8511
|
+
const agentsDir = path10.join(pluginRoot, "agents");
|
|
7965
8512
|
if (!fs6.existsSync(agentsDir)) {
|
|
7966
8513
|
errors2.push(`Missing plugin agents directory: ${agentsDir}`);
|
|
7967
8514
|
return errors2;
|
|
7968
8515
|
}
|
|
7969
8516
|
for (const agentName of ZCODE_AGENT_SMOKE_NAMES) {
|
|
7970
|
-
const agentPath =
|
|
8517
|
+
const agentPath = path10.join(agentsDir, `${agentName}.md`);
|
|
7971
8518
|
if (!fs6.existsSync(agentPath)) {
|
|
7972
8519
|
errors2.push(`Missing plugin agent file: ${agentPath}`);
|
|
7973
8520
|
}
|
|
@@ -7981,11 +8528,11 @@ function buildMarketplaceJson() {
|
|
|
7981
8528
|
plugins: [marketplacePluginEntry()]
|
|
7982
8529
|
};
|
|
7983
8530
|
}
|
|
7984
|
-
function
|
|
8531
|
+
function runInit4(scope, dryRun) {
|
|
7985
8532
|
const notes = ensureLocalHarnessRepo(dryRun);
|
|
7986
8533
|
const projectRoot = resolveProjectRoot2();
|
|
7987
8534
|
if (scope === "project") {
|
|
7988
|
-
const checkoutPath =
|
|
8535
|
+
const checkoutPath = path10.join(projectRoot, ZCODE_PLUGIN_CHECKOUT_PROJECT);
|
|
7989
8536
|
notes.push(...ensureGitCheckout(REPO_URL, checkoutPath, dryRun));
|
|
7990
8537
|
notes.push(...appendGitignore(projectRoot, [ZCODE_PLUGIN_CHECKOUT_PROJECT], dryRun));
|
|
7991
8538
|
notes.push(...appendHarnessProjectGitignore(projectRoot, dryRun));
|
|
@@ -8008,14 +8555,14 @@ function runInit3(scope, dryRun) {
|
|
|
8008
8555
|
notes
|
|
8009
8556
|
};
|
|
8010
8557
|
}
|
|
8011
|
-
function
|
|
8558
|
+
function runDoctor4(scope) {
|
|
8012
8559
|
const errors2 = [];
|
|
8013
8560
|
errors2.push(...validateLocalHarnessRepo());
|
|
8014
8561
|
if (scope === "project") {
|
|
8015
8562
|
const projectRoot = resolveProjectRoot2();
|
|
8016
|
-
const checkoutPath =
|
|
8563
|
+
const checkoutPath = path10.join(projectRoot, ZCODE_PLUGIN_CHECKOUT_PROJECT);
|
|
8017
8564
|
errors2.push(...validateGitCheckout(checkoutPath, ZCODE_PLUGIN_MARKER));
|
|
8018
|
-
const gitignorePath =
|
|
8565
|
+
const gitignorePath = path10.join(projectRoot, ".gitignore");
|
|
8019
8566
|
const gitignore = fs6.existsSync(gitignorePath) ? fs6.readFileSync(gitignorePath, "utf8") : "";
|
|
8020
8567
|
if (!gitignore.split(/\r?\n/).includes(ZCODE_PLUGIN_CHECKOUT_PROJECT)) {
|
|
8021
8568
|
errors2.push(`Missing .gitignore entry: ${ZCODE_PLUGIN_CHECKOUT_PROJECT}`);
|
|
@@ -8034,8 +8581,8 @@ function runDoctor3(scope) {
|
|
|
8034
8581
|
var zcodeAdapter = {
|
|
8035
8582
|
target: "zcode",
|
|
8036
8583
|
mode: "install",
|
|
8037
|
-
runInstallInit: (scope, dryRun) =>
|
|
8038
|
-
runInstallDoctor: (scope) =>
|
|
8584
|
+
runInstallInit: (scope, dryRun) => runInit4(scope, dryRun),
|
|
8585
|
+
runInstallDoctor: (scope) => runDoctor4(scope)
|
|
8039
8586
|
};
|
|
8040
8587
|
|
|
8041
8588
|
// src/adapters/index.ts
|
|
@@ -8044,7 +8591,8 @@ var adapters = {
|
|
|
8044
8591
|
cursor: cursorAdapter,
|
|
8045
8592
|
codex: codexAdapter,
|
|
8046
8593
|
zcode: zcodeAdapter,
|
|
8047
|
-
omp: ompAdapter
|
|
8594
|
+
omp: ompAdapter,
|
|
8595
|
+
dsh: dshAdapter
|
|
8048
8596
|
};
|
|
8049
8597
|
function getAdapter(target) {
|
|
8050
8598
|
const adapter = adapters[target];
|
|
@@ -8054,10 +8602,11 @@ function getAdapter(target) {
|
|
|
8054
8602
|
}
|
|
8055
8603
|
|
|
8056
8604
|
// src/types.ts
|
|
8057
|
-
var SUPPORTED_TARGETS = ["opencode", "cursor", "codex", "zcode", "omp"];
|
|
8605
|
+
var SUPPORTED_TARGETS = ["opencode", "cursor", "codex", "zcode", "omp", "dsh"];
|
|
8058
8606
|
|
|
8059
8607
|
// src/utils.ts
|
|
8060
|
-
import
|
|
8608
|
+
import fs7 from "node:fs";
|
|
8609
|
+
import path11 from "node:path";
|
|
8061
8610
|
function parseCsv(raw) {
|
|
8062
8611
|
if (!raw)
|
|
8063
8612
|
return;
|
|
@@ -8066,9 +8615,43 @@ function parseCsv(raw) {
|
|
|
8066
8615
|
function resolveProjectRoot3() {
|
|
8067
8616
|
const candidate = process.env.MSTAR_CLI_PROJECT_ROOT || process.env.INIT_CWD || process.env.PWD;
|
|
8068
8617
|
if (candidate && candidate.trim())
|
|
8069
|
-
return
|
|
8618
|
+
return path11.resolve(candidate);
|
|
8070
8619
|
return resolveProjectRoot();
|
|
8071
8620
|
}
|
|
8621
|
+
function findUpPackageRoot(startDir, predicate) {
|
|
8622
|
+
let dir = path11.resolve(startDir);
|
|
8623
|
+
for (;; ) {
|
|
8624
|
+
try {
|
|
8625
|
+
const manifest = JSON.parse(fs7.readFileSync(path11.join(dir, "package.json"), "utf8"));
|
|
8626
|
+
if (predicate(manifest))
|
|
8627
|
+
return dir;
|
|
8628
|
+
} catch {}
|
|
8629
|
+
const parent = path11.dirname(dir);
|
|
8630
|
+
if (parent === dir)
|
|
8631
|
+
return null;
|
|
8632
|
+
dir = parent;
|
|
8633
|
+
}
|
|
8634
|
+
}
|
|
8635
|
+
function declaresWorkspaces(manifest) {
|
|
8636
|
+
return Array.isArray(manifest.workspaces) || typeof manifest.workspaces === "string" || manifest.workspaces !== undefined && manifest.workspaces !== null && typeof manifest.workspaces === "object";
|
|
8637
|
+
}
|
|
8638
|
+
function resolveCliProjectRoot() {
|
|
8639
|
+
const override = process.env.MSTAR_CLI_PROJECT_ROOT;
|
|
8640
|
+
if (override && override.trim())
|
|
8641
|
+
return path11.resolve(override);
|
|
8642
|
+
const monorepoRoot = findUpPackageRoot(process.cwd(), declaresWorkspaces);
|
|
8643
|
+
if (monorepoRoot)
|
|
8644
|
+
return monorepoRoot;
|
|
8645
|
+
const packageRoot = findUpPackageRoot(process.cwd(), () => true);
|
|
8646
|
+
if (packageRoot)
|
|
8647
|
+
return packageRoot;
|
|
8648
|
+
return process.cwd();
|
|
8649
|
+
}
|
|
8650
|
+
function resolveCliPath(userPath) {
|
|
8651
|
+
if (path11.isAbsolute(userPath))
|
|
8652
|
+
return userPath;
|
|
8653
|
+
return path11.resolve(resolveCliProjectRoot(), userPath);
|
|
8654
|
+
}
|
|
8072
8655
|
|
|
8073
8656
|
// src/index.ts
|
|
8074
8657
|
var packageVersion = readHarnessVersion();
|
|
@@ -8104,7 +8687,7 @@ function resolveExplicitModelAssignments(options) {
|
|
|
8104
8687
|
others: allow("other-models", parseCsv(options.otherModels), 3, true)
|
|
8105
8688
|
});
|
|
8106
8689
|
}
|
|
8107
|
-
async function
|
|
8690
|
+
async function runInit5(options) {
|
|
8108
8691
|
const target = options.target || (options.yes ? "opencode" : await pickTargetInteractive());
|
|
8109
8692
|
const scope = options.scope || "project";
|
|
8110
8693
|
const adapter = getAdapter(target);
|
|
@@ -8113,7 +8696,7 @@ async function runInit4(options) {
|
|
|
8113
8696
|
}
|
|
8114
8697
|
if (adapter.mode === "install") {
|
|
8115
8698
|
logStep("Step 2/2 - Run target install flow");
|
|
8116
|
-
const installResult = adapter.runInstallInit?.(scope, !!options.dryRun);
|
|
8699
|
+
const installResult = adapter.runInstallInit?.(scope, !!options.dryRun, { noFallbacks: options.noFallbacks });
|
|
8117
8700
|
if (!installResult) {
|
|
8118
8701
|
throw new Error(`Adapter ${target} does not implement install init flow.`);
|
|
8119
8702
|
}
|
|
@@ -8167,7 +8750,7 @@ async function runInit4(options) {
|
|
|
8167
8750
|
}
|
|
8168
8751
|
}
|
|
8169
8752
|
}
|
|
8170
|
-
function
|
|
8753
|
+
function runDoctor5(options) {
|
|
8171
8754
|
const target = options.target || "opencode";
|
|
8172
8755
|
const adapter = getAdapter(target);
|
|
8173
8756
|
const scope = options.scope || "project";
|
|
@@ -8178,6 +8761,9 @@ function runDoctor4(options) {
|
|
|
8178
8761
|
throw new Error(`Adapter ${target} does not implement install doctor flow.`);
|
|
8179
8762
|
}
|
|
8180
8763
|
console.log(`Install location: ${result.location}`);
|
|
8764
|
+
for (const note of result.notes ?? []) {
|
|
8765
|
+
console.log(` - ${note}`);
|
|
8766
|
+
}
|
|
8181
8767
|
if (!result.errors.length) {
|
|
8182
8768
|
console.log(import_picocolors.default.green("Doctor result: healthy"));
|
|
8183
8769
|
return;
|
|
@@ -8212,10 +8798,10 @@ function runDoctor4(options) {
|
|
|
8212
8798
|
}
|
|
8213
8799
|
function resolvePluginRoot(options) {
|
|
8214
8800
|
if (options.root)
|
|
8215
|
-
return
|
|
8801
|
+
return path12.resolve(options.root);
|
|
8216
8802
|
let candidate = resolveProjectRoot3();
|
|
8217
|
-
while (!
|
|
8218
|
-
const parent =
|
|
8803
|
+
while (!fs8.existsSync(path12.join(candidate, "plugin.json"))) {
|
|
8804
|
+
const parent = path12.dirname(candidate);
|
|
8219
8805
|
if (parent === candidate)
|
|
8220
8806
|
break;
|
|
8221
8807
|
candidate = parent;
|
|
@@ -8238,11 +8824,11 @@ function runPluginValidate(options) {
|
|
|
8238
8824
|
process.exitCode = 1;
|
|
8239
8825
|
}
|
|
8240
8826
|
program2.name("mstar-harness").description("Morning Star harness CLI for target-based agent bootstrap").version(packageVersion);
|
|
8241
|
-
program2.command("init").description("Interactive/non-interactive setup for target agent bootstrap").option("-y, --yes", "Non-interactive mode").option("--target <target>", "Install target", "opencode").option("--scope <scope>", "Config scope: global|project (default: project)").option("--output <path>", "Config file path override, relative to project root").option("--dry-run", "Preview result without writing config").option("--pm-model <model>", "Optional: model for project-manager (advanced override)").option("--strategic-models <a,b,c>", "Optional: models for architect/product-manager/prompt-engineer").option("--dev-models <a,b,c>", "Optional: models for fullstack-dev/fullstack-dev-2/frontend-dev").option("--qc-models <a,b,c>", "Optional: models for qc trio").option("--other-models <a,b,c>", "Optional: models for remaining roles").action(async (options) => {
|
|
8242
|
-
await
|
|
8827
|
+
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) => {
|
|
8828
|
+
await runInit5({ ...options, noFallbacks: options.fallbacks === false });
|
|
8243
8829
|
});
|
|
8244
8830
|
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) => {
|
|
8245
|
-
|
|
8831
|
+
runDoctor5(options);
|
|
8246
8832
|
});
|
|
8247
8833
|
var pluginCommand = program2.command("plugin").description("Agent Plugins v1.0.0 portable package commands");
|
|
8248
8834
|
pluginCommand.command("validate").description("Validate a plugin package against Agent Plugins v1.0.0").option("--root <path>", "Plugin root directory to validate (default: project root)").action((options) => {
|
|
@@ -8250,7 +8836,7 @@ pluginCommand.command("validate").description("Validate a plugin package against
|
|
|
8250
8836
|
});
|
|
8251
8837
|
var pathCommand = program2.command("path").description("harness/specs dir resolution checks (engine-backed)");
|
|
8252
8838
|
pathCommand.command("resolve").description("Resolve {HARNESS_DIR} + {SPECS_DIR} from a start dir (exit 1 when no harness dir resolves)").argument("[path]", "Start dir to resolve from (default: cwd)").option("--json", "Machine-readable JSON output (ok, harnessDir, specsDir, guidance on failure)").action((pathArg, options) => {
|
|
8253
|
-
const startDir = pathArg ?
|
|
8839
|
+
const startDir = pathArg ? path12.resolve(pathArg) : process.cwd();
|
|
8254
8840
|
const harnessDir = resolveHarnessDir(startDir);
|
|
8255
8841
|
if (!harnessDir) {
|
|
8256
8842
|
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 init\` to bootstrap, or pass a start dir inside a harness-enabled project`;
|
|
@@ -8274,18 +8860,18 @@ pathCommand.command("resolve").description("Resolve {HARNESS_DIR} + {SPECS_DIR}
|
|
|
8274
8860
|
var statusCommand = program2.command("status").description("status.json schema + residual lifecycle checks (engine-backed)");
|
|
8275
8861
|
function resolveStatusFilePath(pathArg) {
|
|
8276
8862
|
if (pathArg)
|
|
8277
|
-
return
|
|
8863
|
+
return path12.resolve(pathArg);
|
|
8278
8864
|
const harnessDir = resolveHarnessDir();
|
|
8279
8865
|
if (!harnessDir) {
|
|
8280
8866
|
throw new Error(`harness dir not found from ${process.cwd()} \u2014 pass a status.json path or set MSTAR_HARNESS_DIR`);
|
|
8281
8867
|
}
|
|
8282
|
-
return
|
|
8868
|
+
return path12.join(harnessDir, "status.json");
|
|
8283
8869
|
}
|
|
8284
8870
|
statusCommand.command("validate").description("Validate status.json (schema, severity enum, root-only residual_findings)").argument("[path]", "status.json path (default: {HARNESS_DIR}/status.json)").action((pathArg) => {
|
|
8285
8871
|
let statusPath;
|
|
8286
8872
|
try {
|
|
8287
8873
|
statusPath = resolveStatusFilePath(pathArg);
|
|
8288
|
-
if (!
|
|
8874
|
+
if (!fs8.existsSync(statusPath)) {
|
|
8289
8875
|
throw new Error(`status file not found: ${statusPath}`);
|
|
8290
8876
|
}
|
|
8291
8877
|
const gate2 = validateStatus(statusPath);
|
|
@@ -8295,10 +8881,10 @@ statusCommand.command("validate").description("Validate status.json (schema, sev
|
|
|
8295
8881
|
}
|
|
8296
8882
|
const count = gate2.violations.length;
|
|
8297
8883
|
console.error(import_picocolors.default.red(`${statusPath}: FAIL (${count} violation${count === 1 ? "" : "s"})`));
|
|
8298
|
-
for (const
|
|
8299
|
-
console.error(` - [${
|
|
8300
|
-
if (
|
|
8301
|
-
console.error(` fix: ${
|
|
8884
|
+
for (const violation12 of gate2.violations) {
|
|
8885
|
+
console.error(` - [${violation12.severity}] ${violation12.code}: ${violation12.message}`);
|
|
8886
|
+
if (violation12.fix)
|
|
8887
|
+
console.error(` fix: ${violation12.fix}`);
|
|
8302
8888
|
}
|
|
8303
8889
|
process.exitCode = 1;
|
|
8304
8890
|
} catch (error) {
|
|
@@ -8323,10 +8909,54 @@ statusCommand.command("archive-residuals").description("Archive a plan's open re
|
|
|
8323
8909
|
process.exitCode = 1;
|
|
8324
8910
|
}
|
|
8325
8911
|
});
|
|
8326
|
-
|
|
8912
|
+
statusCommand.command("tech-debt").description("Print the residual tech-debt rollup (total_open / by_severity / by_target / by_plan) and PASS/DRIFT vs stored " + "metadata.tech_debt_summary (exit 1 on DRIFT \u2014 refresh the stored summary to clear)").argument("[path]", "status.json path (default: {HARNESS_DIR}/status.json)").action((pathArg) => {
|
|
8913
|
+
try {
|
|
8914
|
+
const statusPath = resolveStatusFilePath(pathArg);
|
|
8915
|
+
if (!fs8.existsSync(statusPath)) {
|
|
8916
|
+
throw new Error(`status file not found: ${statusPath}`);
|
|
8917
|
+
}
|
|
8918
|
+
const rollup = techDebtRollup(statusPath);
|
|
8919
|
+
console.log(`status tech-debt: ${statusPath}`);
|
|
8920
|
+
console.log(`total_open: ${rollup.computed.total_open}`);
|
|
8921
|
+
console.log(`by_severity: ${JSON.stringify(rollup.computed.by_severity)}`);
|
|
8922
|
+
console.log(`by_target: ${JSON.stringify(rollup.computed.by_target)}`);
|
|
8923
|
+
console.log(`by_plan: ${JSON.stringify(rollup.computed.by_plan)}`);
|
|
8924
|
+
if (rollup.overall === "PASS") {
|
|
8925
|
+
console.log(import_picocolors.default.green("tech_debt_summary: PASS (all 4 fields match stored)"));
|
|
8926
|
+
return;
|
|
8927
|
+
}
|
|
8928
|
+
const drifts = rollup.checks.filter((check) => check.status === "DRIFT").map((check) => check.field);
|
|
8929
|
+
const note = rollup.stored === null ? " (no stored metadata.tech_debt_summary)" : "";
|
|
8930
|
+
console.error(import_picocolors.default.red(`tech_debt_summary: DRIFT (${drifts.length}/4 fields: ${drifts.join(", ")})${note}`));
|
|
8931
|
+
process.exitCode = 1;
|
|
8932
|
+
} catch (error) {
|
|
8933
|
+
console.error(import_picocolors.default.red(`status tech-debt failed: ${error.message}`));
|
|
8934
|
+
process.exitCode = 1;
|
|
8935
|
+
}
|
|
8936
|
+
});
|
|
8937
|
+
statusCommand.command("findings-cleanup").description("Enforce a plan's findings-cleanup mode on its open residuals (zero-residual via Assignment/metadata, else " + "allow-residual; exit 1 on violations)").argument("<plan-id>", "Plan id whose open residuals are checked against the cleanup mode").option("--harness <path>", "Harness dir override (default: resolved {HARNESS_DIR})").action((planId, options) => {
|
|
8938
|
+
try {
|
|
8939
|
+
const statusPath = options.harness ? path12.join(path12.resolve(options.harness), "status.json") : resolveStatusFilePath();
|
|
8940
|
+
if (!fs8.existsSync(statusPath)) {
|
|
8941
|
+
throw new Error(`status file not found: ${statusPath}`);
|
|
8942
|
+
}
|
|
8943
|
+
const doc = readJson2(statusPath);
|
|
8944
|
+
const gate2 = findingsCleanupGate(doc, planId);
|
|
8945
|
+
if (gate2.ok) {
|
|
8946
|
+
console.log(import_picocolors.default.green(`findings-cleanup ${planId}: OK`));
|
|
8947
|
+
return;
|
|
8948
|
+
}
|
|
8949
|
+
printChecklist(`findings-cleanup ${planId}`, gate2);
|
|
8950
|
+
process.exitCode = 1;
|
|
8951
|
+
} catch (error) {
|
|
8952
|
+
console.error(import_picocolors.default.red(`status findings-cleanup failed: ${error.message}`));
|
|
8953
|
+
process.exitCode = 1;
|
|
8954
|
+
}
|
|
8955
|
+
});
|
|
8956
|
+
var leaseCommand = program2.command("lease").description("execution_lease / integration_merge_lease checks (engine-backed)");
|
|
8327
8957
|
function resolveLeaseHarnessDir(harnessArg) {
|
|
8328
8958
|
if (harnessArg)
|
|
8329
|
-
return
|
|
8959
|
+
return path12.resolve(harnessArg);
|
|
8330
8960
|
const harnessDir = resolveHarnessDir();
|
|
8331
8961
|
if (!harnessDir) {
|
|
8332
8962
|
throw new Error(`harness dir not found from ${process.cwd()} \u2014 pass --harness or set MSTAR_HARNESS_DIR`);
|
|
@@ -8336,8 +8966,8 @@ function resolveLeaseHarnessDir(harnessArg) {
|
|
|
8336
8966
|
leaseCommand.command("verify").description("Verify a plan's execution_lease (missing/invalid/non-SSOT location \u2192 exit 1 with violations)").argument("<plan-id>", "Plan id whose execution_lease is verified").option("--harness <path>", "Harness dir override (default: resolved {HARNESS_DIR})").action((planId, options) => {
|
|
8337
8967
|
try {
|
|
8338
8968
|
const harnessDir = resolveLeaseHarnessDir(options.harness);
|
|
8339
|
-
const statusPath =
|
|
8340
|
-
if (!
|
|
8969
|
+
const statusPath = path12.join(harnessDir, "status.json");
|
|
8970
|
+
if (!fs8.existsSync(statusPath)) {
|
|
8341
8971
|
throw new Error(`status file not found: ${statusPath}`);
|
|
8342
8972
|
}
|
|
8343
8973
|
const doc = readJson2(statusPath);
|
|
@@ -8363,10 +8993,10 @@ leaseCommand.command("verify").description("Verify a plan's execution_lease (mis
|
|
|
8363
8993
|
}
|
|
8364
8994
|
const count = result.violations.length;
|
|
8365
8995
|
console.error(import_picocolors.default.red(`${statusPath}: FAIL plan ${planId} (${count} violation${count === 1 ? "" : "s"})`));
|
|
8366
|
-
for (const
|
|
8367
|
-
console.error(` - [${
|
|
8368
|
-
if (
|
|
8369
|
-
console.error(` fix: ${
|
|
8996
|
+
for (const violation12 of result.violations) {
|
|
8997
|
+
console.error(` - [${violation12.severity}] ${violation12.code}: ${violation12.message}`);
|
|
8998
|
+
if (violation12.fix)
|
|
8999
|
+
console.error(` fix: ${violation12.fix}`);
|
|
8370
9000
|
}
|
|
8371
9001
|
process.exitCode = 1;
|
|
8372
9002
|
} catch (error) {
|
|
@@ -8374,6 +9004,32 @@ leaseCommand.command("verify").description("Verify a plan's execution_lease (mis
|
|
|
8374
9004
|
process.exitCode = 1;
|
|
8375
9005
|
}
|
|
8376
9006
|
});
|
|
9007
|
+
leaseCommand.command("verify-integration").description("Verify the root metadata.integration_merge_lease when present (absent/unclaimed \u2192 OK; invalid lease \u2192 exit 1)").option("--harness <path>", "Harness dir override (default: resolved {HARNESS_DIR})").action((options) => {
|
|
9008
|
+
try {
|
|
9009
|
+
const harnessDir = resolveLeaseHarnessDir(options.harness);
|
|
9010
|
+
const statusPath = path12.join(harnessDir, "status.json");
|
|
9011
|
+
if (!fs8.existsSync(statusPath)) {
|
|
9012
|
+
throw new Error(`status file not found: ${statusPath}`);
|
|
9013
|
+
}
|
|
9014
|
+
const doc = readJson2(statusPath);
|
|
9015
|
+
const metadata = doc.metadata ?? {};
|
|
9016
|
+
if (metadata.integration_merge_lease === undefined) {
|
|
9017
|
+
console.log(import_picocolors.default.green(`${statusPath}: OK \u2014 no integration_merge_lease (unclaimed)`));
|
|
9018
|
+
return;
|
|
9019
|
+
}
|
|
9020
|
+
const gate2 = validateIntegrationMergeLease(metadata.integration_merge_lease);
|
|
9021
|
+
if (gate2.ok) {
|
|
9022
|
+
const lease = metadata.integration_merge_lease;
|
|
9023
|
+
console.log(import_picocolors.default.green(`${statusPath}: OK \u2014 integration_merge_lease valid (holder ${String(lease.holder ?? "")})`));
|
|
9024
|
+
return;
|
|
9025
|
+
}
|
|
9026
|
+
printChecklist("lease verify-integration", gate2);
|
|
9027
|
+
process.exitCode = 1;
|
|
9028
|
+
} catch (error) {
|
|
9029
|
+
console.error(import_picocolors.default.red(`lease verify-integration failed: ${error.message}`));
|
|
9030
|
+
process.exitCode = 1;
|
|
9031
|
+
}
|
|
9032
|
+
});
|
|
8377
9033
|
function failScript(error, context) {
|
|
8378
9034
|
if (error instanceof SddScriptError) {
|
|
8379
9035
|
console.error(import_picocolors.default.red(`${context} failed: ${error.message}`));
|
|
@@ -8426,19 +9082,19 @@ function printChecklist(label, gate2) {
|
|
|
8426
9082
|
}
|
|
8427
9083
|
const count = gate2.violations.length;
|
|
8428
9084
|
console.error(import_picocolors.default.red(`${label}: FAIL (${count} violation${count === 1 ? "" : "s"})`));
|
|
8429
|
-
for (const
|
|
8430
|
-
console.error(` - [${
|
|
8431
|
-
if (
|
|
8432
|
-
console.error(` fix: ${
|
|
9085
|
+
for (const violation12 of gate2.violations) {
|
|
9086
|
+
console.error(` - [${violation12.severity}] ${violation12.code}: ${violation12.message}`);
|
|
9087
|
+
if (violation12.fix)
|
|
9088
|
+
console.error(` fix: ${violation12.fix}`);
|
|
8433
9089
|
}
|
|
8434
9090
|
}
|
|
8435
9091
|
iterationCommand.command("gate").description("Evaluate the phase-transition gate: prints the transition (phase-2-execute / phase-3-close / phase-4-pr-delivery) " + "plus the \xA73.1 entry and \xA73.5 exit checklists. Exit 1 when the gate verdict fails \u2014 during the Phase-3 window " + "(transition: phase-3-close) exit 1 is EXPECTED until the \xA73.4 close items (status: completed + end_date) are " + "written: the exit checklist gates Phase 4, not the Phase-3 entry (qc2 F-003)").requiredOption("--status <path>", "status.json path").requiredOption("--compass <path>", "delivery-compass.md path").option("--branch <branch>", "Current branch probe (exit \xA73.5 item 5)").option("--integration <branch>", "Spec integration branch probe (exit \xA73.5 item 5)").option("--target <branch>", "PR base branch probe (exit \xA73.5 item 6)").action((options) => {
|
|
8436
9092
|
try {
|
|
8437
|
-
const statusPath =
|
|
8438
|
-
const compassPath =
|
|
8439
|
-
if (!
|
|
9093
|
+
const statusPath = path12.resolve(options.status);
|
|
9094
|
+
const compassPath = path12.resolve(options.compass);
|
|
9095
|
+
if (!fs8.existsSync(statusPath))
|
|
8440
9096
|
throw new Error(`status file not found: ${statusPath}`);
|
|
8441
|
-
if (!
|
|
9097
|
+
if (!fs8.existsSync(compassPath))
|
|
8442
9098
|
throw new Error(`compass file not found: ${compassPath}`);
|
|
8443
9099
|
const result = evaluatePhaseGate(readJson2(statusPath), parseCompassFrontmatter(compassPath), {
|
|
8444
9100
|
currentBranch: options.branch,
|
|
@@ -8463,10 +9119,10 @@ iterationCommand.command("push-cadence").description("\xA75.1a push-cadence prob
|
|
|
8463
9119
|
}
|
|
8464
9120
|
const count = result.violations.length;
|
|
8465
9121
|
console.error(import_picocolors.default.red(`push blocked (${count} violation${count === 1 ? "" : "s"})`));
|
|
8466
|
-
for (const
|
|
8467
|
-
console.error(` - [${
|
|
8468
|
-
if (
|
|
8469
|
-
console.error(` fix: ${
|
|
9122
|
+
for (const violation12 of result.violations) {
|
|
9123
|
+
console.error(` - [${violation12.severity}] ${violation12.code}: ${violation12.message}`);
|
|
9124
|
+
if (violation12.fix)
|
|
9125
|
+
console.error(` fix: ${violation12.fix}`);
|
|
8470
9126
|
}
|
|
8471
9127
|
process.exitCode = 1;
|
|
8472
9128
|
});
|
|
@@ -8476,11 +9132,11 @@ dispatchCommand.command("validate").description("Validate an Assignment markdown
|
|
|
8476
9132
|
if (!assignmentFile) {
|
|
8477
9133
|
throw new SddScriptError("usage: dispatch validate <assignment-file> [--branch <branch>]", 2);
|
|
8478
9134
|
}
|
|
8479
|
-
const file =
|
|
8480
|
-
if (!
|
|
9135
|
+
const file = resolveCliPath(assignmentFile);
|
|
9136
|
+
if (!fs8.existsSync(file)) {
|
|
8481
9137
|
throw new Error(`assignment file not found: ${file}`);
|
|
8482
9138
|
}
|
|
8483
|
-
const text =
|
|
9139
|
+
const text = fs8.readFileSync(file, "utf8");
|
|
8484
9140
|
const readOnly = isReadOnlyAssignmentRole(parseAssignmentFields(text).executeAs ?? "");
|
|
8485
9141
|
const violations = [...validateAssignmentFields(text, { writable: readOnly ? false : undefined }).violations];
|
|
8486
9142
|
if (!readOnly) {
|
|
@@ -8535,8 +9191,8 @@ worktreeCommand.command("check").description("L1: verify the plan's execution_le
|
|
|
8535
9191
|
if (!plan) {
|
|
8536
9192
|
throw new SddScriptError("usage: worktree check <plan-id> [--status <path>] [--control <path>] (or --plan <plan-id>)", 2);
|
|
8537
9193
|
}
|
|
8538
|
-
const statusPath = options.status ?
|
|
8539
|
-
if (!
|
|
9194
|
+
const statusPath = options.status ? path12.resolve(options.status) : resolveStatusFilePath();
|
|
9195
|
+
if (!fs8.existsSync(statusPath)) {
|
|
8540
9196
|
throw new Error(`status file not found: ${statusPath}`);
|
|
8541
9197
|
}
|
|
8542
9198
|
const doc = readJson2(statusPath);
|
|
@@ -8558,7 +9214,7 @@ worktreeCommand.command("check").description("L1: verify the plan's execution_le
|
|
|
8558
9214
|
const lease = row.execution_lease ?? {};
|
|
8559
9215
|
const metadata = doc.metadata ?? {};
|
|
8560
9216
|
const input = {
|
|
8561
|
-
controlWorktreePath: options.control ?
|
|
9217
|
+
controlWorktreePath: options.control ? path12.resolve(options.control) : String(metadata.control_worktree_path ?? ""),
|
|
8562
9218
|
leaseWorktreePath: String(lease.worktree_path ?? ""),
|
|
8563
9219
|
leaseWorkingBranch: String(lease.working_branch ?? ""),
|
|
8564
9220
|
planId: plan
|
|
@@ -8571,6 +9227,61 @@ worktreeCommand.command("check").description("L1: verify the plan's execution_le
|
|
|
8571
9227
|
failScript(error, "worktree check");
|
|
8572
9228
|
}
|
|
8573
9229
|
});
|
|
9230
|
+
function parseAssignmentHeaderField(assignmentText, label) {
|
|
9231
|
+
const escaped = label.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
9232
|
+
const boldRe = new RegExp(`^[ \\t]*(?:[-*][ \\t]+)?\\*\\*\\s*${escaped}\\s*\\*\\*\\s*:\\s*(.*)$`);
|
|
9233
|
+
const plainRe = new RegExp(`^[ \\t]*(?:[-*][ \\t]+)?${escaped}\\s*:\\s*(.*)$`);
|
|
9234
|
+
for (const line of assignmentText.split(/\r?\n/)) {
|
|
9235
|
+
const match = line.match(boldRe) ?? line.match(plainRe);
|
|
9236
|
+
if (match)
|
|
9237
|
+
return match[1].trim();
|
|
9238
|
+
}
|
|
9239
|
+
return "";
|
|
9240
|
+
}
|
|
9241
|
+
var QC_ALIGNMENT_FIELDS2 = [
|
|
9242
|
+
{ key: "planId", label: "plan_id" },
|
|
9243
|
+
{ key: "reviewRange", label: "Review range" },
|
|
9244
|
+
{ key: "diffBasis", label: "Diff basis" }
|
|
9245
|
+
];
|
|
9246
|
+
worktreeCommand.command("qc-alignment").description("Assert the QC/QA alignment fields (plan_id / Review range / Diff basis) are byte-identical across the given " + "Assignment files (separate or combined `Review range / Diff basis` labels; exit 1 on mismatch or missing field, 2 on usage)").argument("[assignment-files...]", "QC tri + QA Assignment markdown files (at least one required)").action((files) => {
|
|
9247
|
+
try {
|
|
9248
|
+
if (files.length === 0) {
|
|
9249
|
+
throw new SddScriptError("usage: worktree qc-alignment <assignment-file>...", 2);
|
|
9250
|
+
}
|
|
9251
|
+
const assignments = [];
|
|
9252
|
+
for (const fileArg of files) {
|
|
9253
|
+
const file = path12.resolve(fileArg);
|
|
9254
|
+
if (!fs8.existsSync(file)) {
|
|
9255
|
+
throw new Error(`assignment file not found: ${file}`);
|
|
9256
|
+
}
|
|
9257
|
+
const text = fs8.readFileSync(file, "utf8");
|
|
9258
|
+
const combinedRange = parseAssignmentHeaderField(text, "Review range / Diff basis");
|
|
9259
|
+
const planId = parseAssignmentHeaderField(text, "plan_id");
|
|
9260
|
+
const reviewRange = parseAssignmentHeaderField(text, "Review range") || combinedRange;
|
|
9261
|
+
const diffBasis = parseAssignmentHeaderField(text, "Diff basis") || combinedRange;
|
|
9262
|
+
const values = { planId, reviewRange, diffBasis };
|
|
9263
|
+
const missing = QC_ALIGNMENT_FIELDS2.filter((field) => values[field.key] === "");
|
|
9264
|
+
if (missing.length > 0) {
|
|
9265
|
+
console.error(import_picocolors.default.red(`worktree qc-alignment: FAIL ${file}`));
|
|
9266
|
+
for (const field of missing) {
|
|
9267
|
+
console.error(` - [high] qc.alignment.field.missing: missing "${field.label}" header field`);
|
|
9268
|
+
}
|
|
9269
|
+
process.exitCode = 1;
|
|
9270
|
+
return;
|
|
9271
|
+
}
|
|
9272
|
+
assignments.push({ planId, reviewRange, diffBasis });
|
|
9273
|
+
}
|
|
9274
|
+
const gate2 = assertQcAlignment(assignments);
|
|
9275
|
+
if (gate2.ok) {
|
|
9276
|
+
console.log(import_picocolors.default.green(`worktree qc-alignment: OK (${assignments.length} assignment${assignments.length === 1 ? "" : "s"}, ` + `${QC_ALIGNMENT_FIELDS2.length} fields byte-identical)`));
|
|
9277
|
+
return;
|
|
9278
|
+
}
|
|
9279
|
+
printChecklist("worktree qc-alignment", gate2);
|
|
9280
|
+
process.exitCode = 1;
|
|
9281
|
+
} catch (error) {
|
|
9282
|
+
failScript(error, "worktree qc-alignment");
|
|
9283
|
+
}
|
|
9284
|
+
});
|
|
8574
9285
|
var reviewCommand = program2.command("review").description("QC seat-mapping checks (engine-backed)");
|
|
8575
9286
|
function parseAssignmentExecutionMode(assignmentText) {
|
|
8576
9287
|
for (const line of assignmentText.split(/\r?\n/)) {
|
|
@@ -8585,11 +9296,11 @@ reviewCommand.command("seats").description("Map an Assignment's execution mode t
|
|
|
8585
9296
|
if (!assignmentFile) {
|
|
8586
9297
|
throw new SddScriptError("usage: review seats <assignment-file> [--mode sdd|inline|targeted] [--reviewers <role1,role2,...>]", 2);
|
|
8587
9298
|
}
|
|
8588
|
-
const file =
|
|
8589
|
-
if (!
|
|
9299
|
+
const file = path12.resolve(assignmentFile);
|
|
9300
|
+
if (!fs8.existsSync(file)) {
|
|
8590
9301
|
throw new Error(`assignment file not found: ${file}`);
|
|
8591
9302
|
}
|
|
8592
|
-
const text =
|
|
9303
|
+
const text = fs8.readFileSync(file, "utf8");
|
|
8593
9304
|
const mode = options.mode ?? parseAssignmentExecutionMode(text);
|
|
8594
9305
|
const reviewers = (options.reviewers ?? "").split(",").map((role) => role.trim()).filter((role) => role !== "");
|
|
8595
9306
|
const result = executionModeToN(mode, { seats: reviewers });
|
|
@@ -8646,31 +9357,31 @@ var LINT_CODE_EXTENSIONS = {
|
|
|
8646
9357
|
".swift": true
|
|
8647
9358
|
};
|
|
8648
9359
|
function lintTargetType(filePath) {
|
|
8649
|
-
const base =
|
|
9360
|
+
const base = path12.basename(filePath);
|
|
8650
9361
|
if (base === "STRATEGY.md")
|
|
8651
9362
|
return "strategy";
|
|
8652
9363
|
if (base === "SKILL.md")
|
|
8653
9364
|
return "skill";
|
|
8654
9365
|
if (/^task-\d+-report\.md$/i.test(base))
|
|
8655
9366
|
return "report";
|
|
8656
|
-
const dir =
|
|
8657
|
-
if (dir.includes(`${
|
|
9367
|
+
const dir = path12.dirname(filePath);
|
|
9368
|
+
if (dir.includes(`${path12.sep}plans${path12.sep}`) || dir.endsWith(`${path12.sep}plans`))
|
|
8658
9369
|
return "plan";
|
|
8659
9370
|
if (/^\d{8}-[a-z0-9.-]+\.md$/i.test(base))
|
|
8660
9371
|
return "plan";
|
|
8661
|
-
if (LINT_CODE_EXTENSIONS[
|
|
9372
|
+
if (LINT_CODE_EXTENSIONS[path12.extname(base).toLowerCase()] === true)
|
|
8662
9373
|
return "code";
|
|
8663
9374
|
return null;
|
|
8664
9375
|
}
|
|
8665
9376
|
function collectLintTargets(dir) {
|
|
8666
9377
|
const targets = [];
|
|
8667
9378
|
const walk = (current) => {
|
|
8668
|
-
for (const entry of
|
|
9379
|
+
for (const entry of fs8.readdirSync(current, { withFileTypes: true })) {
|
|
8669
9380
|
if (entry.isDirectory()) {
|
|
8670
9381
|
if (LINT_SKIP_DIRS[entry.name] !== true)
|
|
8671
|
-
walk(
|
|
8672
|
-
} else if (entry.isFile() && lintTargetType(
|
|
8673
|
-
targets.push(
|
|
9382
|
+
walk(path12.join(current, entry.name));
|
|
9383
|
+
} else if (entry.isFile() && lintTargetType(path12.join(current, entry.name)) !== null) {
|
|
9384
|
+
targets.push(path12.join(current, entry.name));
|
|
8674
9385
|
}
|
|
8675
9386
|
}
|
|
8676
9387
|
};
|
|
@@ -8678,8 +9389,8 @@ function collectLintTargets(dir) {
|
|
|
8678
9389
|
return targets;
|
|
8679
9390
|
}
|
|
8680
9391
|
function lintOneFile(filePath) {
|
|
8681
|
-
const abs =
|
|
8682
|
-
const text =
|
|
9392
|
+
const abs = path12.resolve(filePath);
|
|
9393
|
+
const text = fs8.readFileSync(abs, "utf8");
|
|
8683
9394
|
const violations = [];
|
|
8684
9395
|
const markers = [];
|
|
8685
9396
|
switch (lintTargetType(abs)) {
|
|
@@ -8708,7 +9419,7 @@ function lintOneFile(filePath) {
|
|
|
8708
9419
|
break;
|
|
8709
9420
|
}
|
|
8710
9421
|
default:
|
|
8711
|
-
throw new SddScriptError(`usage: lint <target> \u2014 unsupported file type "${
|
|
9422
|
+
throw new SddScriptError(`usage: lint <target> \u2014 unsupported file type "${path12.basename(abs)}" (lintable: plan files, SKILL.md, STRATEGY.md, task-N-report.md, code files)`, 2);
|
|
8712
9423
|
}
|
|
8713
9424
|
return { violations, markers };
|
|
8714
9425
|
}
|
|
@@ -8717,10 +9428,10 @@ lintCommand.description("Lint <target> (file or dir) \u2014 exit 1 on violations
|
|
|
8717
9428
|
try {
|
|
8718
9429
|
if (!target)
|
|
8719
9430
|
throw new SddScriptError("usage: lint <target> (file or dir)", 2);
|
|
8720
|
-
const abs =
|
|
8721
|
-
if (!
|
|
9431
|
+
const abs = resolveCliPath(target);
|
|
9432
|
+
if (!fs8.existsSync(abs))
|
|
8722
9433
|
throw new Error(`lint target not found: ${abs}`);
|
|
8723
|
-
const targets =
|
|
9434
|
+
const targets = fs8.statSync(abs).isDirectory() ? collectLintTargets(abs) : [abs];
|
|
8724
9435
|
if (targets.length === 0) {
|
|
8725
9436
|
console.log(import_picocolors.default.yellow(`lint: no lintable files under ${target}`));
|
|
8726
9437
|
return;
|
|
@@ -8747,10 +9458,10 @@ lintCommand.description("Lint <target> (file or dir) \u2014 exit 1 on violations
|
|
|
8747
9458
|
violations += result.violations.length;
|
|
8748
9459
|
const count = result.violations.length;
|
|
8749
9460
|
console.error(import_picocolors.default.red(`${label}: FAIL (${count} violation${count === 1 ? "" : "s"})`));
|
|
8750
|
-
for (const
|
|
8751
|
-
console.error(` - [${
|
|
8752
|
-
if (
|
|
8753
|
-
console.error(` fix: ${
|
|
9461
|
+
for (const violation12 of result.violations) {
|
|
9462
|
+
console.error(` - [${violation12.severity}] ${violation12.code}: ${violation12.message}`);
|
|
9463
|
+
if (violation12.fix)
|
|
9464
|
+
console.error(` fix: ${violation12.fix}`);
|
|
8754
9465
|
}
|
|
8755
9466
|
}
|
|
8756
9467
|
if (violations > 0)
|
|
@@ -8759,34 +9470,23 @@ lintCommand.description("Lint <target> (file or dir) \u2014 exit 1 on violations
|
|
|
8759
9470
|
failScript(error, "lint");
|
|
8760
9471
|
}
|
|
8761
9472
|
});
|
|
8762
|
-
function stripFrontmatter(text) {
|
|
8763
|
-
const lines = text.replace(/^\uFEFF/, "").split(/\r?\n/);
|
|
8764
|
-
if (lines.length === 0 || lines[0].trim() !== "---")
|
|
8765
|
-
return text;
|
|
8766
|
-
for (let i = 1;i < lines.length; i++) {
|
|
8767
|
-
if (lines[i].trim() === "---")
|
|
8768
|
-
return lines.slice(i + 1).join(`
|
|
8769
|
-
`);
|
|
8770
|
-
}
|
|
8771
|
-
return text;
|
|
8772
|
-
}
|
|
8773
9473
|
var designMdCommand = program2.command("design-md").description("DESIGN.md token frontmatter / light-dark parity / completeness checks (engine-backed)");
|
|
8774
9474
|
designMdCommand.command("validate").description("Validate DESIGN.md in <dir>: token frontmatter, light/dark parity when DESIGN.dark.md exists, " + "and the completeness level (exit 1 on violations, 2 on usage)").argument("[dir]", "Directory containing DESIGN.md").action((dir) => {
|
|
8775
9475
|
try {
|
|
8776
9476
|
if (!dir)
|
|
8777
9477
|
throw new SddScriptError("usage: design-md validate <dir>", 2);
|
|
8778
|
-
const abs =
|
|
8779
|
-
const lightPath =
|
|
8780
|
-
if (!
|
|
9478
|
+
const abs = resolveCliPath(dir);
|
|
9479
|
+
const lightPath = path12.join(abs, "DESIGN.md");
|
|
9480
|
+
if (!fs8.existsSync(lightPath))
|
|
8781
9481
|
throw new Error(`design file not found: ${lightPath}`);
|
|
8782
|
-
const light =
|
|
9482
|
+
const light = fs8.readFileSync(lightPath, "utf8");
|
|
8783
9483
|
const violations = [];
|
|
8784
9484
|
const tokens = validateDesignTokenFrontmatter(light);
|
|
8785
9485
|
printChecklist("design-md validate (tokens)", tokens);
|
|
8786
9486
|
violations.push(...tokens.violations);
|
|
8787
|
-
const darkPath =
|
|
8788
|
-
if (
|
|
8789
|
-
const parity = assertLightDarkParity(light,
|
|
9487
|
+
const darkPath = path12.join(abs, "DESIGN.dark.md");
|
|
9488
|
+
if (fs8.existsSync(darkPath)) {
|
|
9489
|
+
const parity = assertLightDarkParity(light, fs8.readFileSync(darkPath, "utf8"));
|
|
8790
9490
|
printChecklist("design-md validate (light/dark parity)", parity);
|
|
8791
9491
|
violations.push(...parity.violations);
|
|
8792
9492
|
}
|
|
@@ -8861,7 +9561,7 @@ function resolveAuditShortSha(cwd, override) {
|
|
|
8861
9561
|
if (override !== undefined && override !== "")
|
|
8862
9562
|
return override;
|
|
8863
9563
|
try {
|
|
8864
|
-
const out =
|
|
9564
|
+
const out = execFileSync8("git", ["rev-parse", "--short", "HEAD"], {
|
|
8865
9565
|
cwd,
|
|
8866
9566
|
encoding: "utf8",
|
|
8867
9567
|
stdio: ["ignore", "pipe", "ignore"]
|
|
@@ -8875,8 +9575,8 @@ auditCommand.command("scaffold").description("Scaffold an audit-<date>/ plan dir
|
|
|
8875
9575
|
try {
|
|
8876
9576
|
if (!findingsFile)
|
|
8877
9577
|
throw new SddScriptError("usage: audit scaffold <findings-file> [--dir <out-dir>]", 2);
|
|
8878
|
-
const abs =
|
|
8879
|
-
if (!
|
|
9578
|
+
const abs = resolveCliPath(findingsFile);
|
|
9579
|
+
if (!fs8.existsSync(abs))
|
|
8880
9580
|
throw new Error(`findings file not found: ${abs}`);
|
|
8881
9581
|
if (options.date !== undefined && !/^\d{4}-\d{2}-\d{2}$/.test(options.date)) {
|
|
8882
9582
|
throw new SddScriptError("usage: audit scaffold \u2014 --date must be YYYY-MM-DD", 2);
|
|
@@ -8885,8 +9585,8 @@ auditCommand.command("scaffold").description("Scaffold an audit-<date>/ plan dir
|
|
|
8885
9585
|
throw new SddScriptError("usage: audit scaffold \u2014 --sha must be a 7-40 char hex commit SHA", 2);
|
|
8886
9586
|
}
|
|
8887
9587
|
const date = options.date ?? new Date().toISOString().slice(0, 10);
|
|
8888
|
-
const outDir = options.dir !== undefined ?
|
|
8889
|
-
const findings = parseAuditFindings(
|
|
9588
|
+
const outDir = options.dir !== undefined ? resolveCliPath(options.dir) : resolveCliPath(`audit-${date}`);
|
|
9589
|
+
const findings = parseAuditFindings(fs8.readFileSync(abs, "utf8"));
|
|
8890
9590
|
const sha = resolveAuditShortSha(process.cwd(), options.sha);
|
|
8891
9591
|
const result = scaffoldAuditPlan(outDir, findings, { date, repoName: options.repo, repoShortSha: sha });
|
|
8892
9592
|
const count = result.files.length;
|
|
@@ -8902,16 +9602,16 @@ compoundCommand.command("validate").description("Validate a knowledge doc frontm
|
|
|
8902
9602
|
try {
|
|
8903
9603
|
if (!docPath)
|
|
8904
9604
|
throw new SddScriptError("usage: compound validate <doc-path> [--knowledge-dir <dir>]", 2);
|
|
8905
|
-
const abs =
|
|
8906
|
-
if (!
|
|
9605
|
+
const abs = resolveCliPath(docPath);
|
|
9606
|
+
if (!fs8.existsSync(abs))
|
|
8907
9607
|
throw new Error(`knowledge doc not found: ${abs}`);
|
|
8908
|
-
const text =
|
|
9608
|
+
const text = fs8.readFileSync(abs, "utf8");
|
|
8909
9609
|
const violations = [];
|
|
8910
9610
|
const schema = validateSchemaYaml(text);
|
|
8911
9611
|
printChecklist("compound validate (schema)", schema);
|
|
8912
9612
|
violations.push(...schema.violations);
|
|
8913
9613
|
if (options.knowledgeDir !== undefined) {
|
|
8914
|
-
const knowledgeDir =
|
|
9614
|
+
const knowledgeDir = resolveCliPath(options.knowledgeDir);
|
|
8915
9615
|
const index = assertIndexRows(knowledgeDir);
|
|
8916
9616
|
printChecklist("compound validate (index rows)", index);
|
|
8917
9617
|
violations.push(...index.violations);
|
|
@@ -8947,6 +9647,8 @@ var HOST_SIGNALS = [
|
|
|
8947
9647
|
"tool_search"
|
|
8948
9648
|
];
|
|
8949
9649
|
var HOST_SIGNAL_LOOKUP = lookupTable(HOST_SIGNALS);
|
|
9650
|
+
var HOST_IDS = ["opencode", "omp", "pi", "dsh", "cursor", "codex", "kimi", "zcode"];
|
|
9651
|
+
var HOST_ID_LOOKUP = lookupTable(HOST_IDS);
|
|
8950
9652
|
var hostCommand = program2.command("host").description("host detection from session tool shapes (engine-backed)");
|
|
8951
9653
|
hostCommand.command("detect").description("Detect the active host from --signals (comma-separated tool-shape tokens): prints the host id or " + "'ambiguous' (exit 2 on usage)").option("--signals <list>", "Comma-separated tool-shape signals (e.g. question,ask,hub)").action((options) => {
|
|
8952
9654
|
try {
|
|
@@ -8970,22 +9672,47 @@ hostCommand.command("detect").description("Detect the active host from --signals
|
|
|
8970
9672
|
failScript(error, "host detect");
|
|
8971
9673
|
}
|
|
8972
9674
|
});
|
|
9675
|
+
hostCommand.command("skill-root").description("Resolve the loaded skill root for a host (mstar-host \xA7 Resolve loaded skill root): prints the canonical " + "skill-root string for --host / --skill (exit 1 on missing required options, 2 on usage errors)").requiredOption("--host <id>", "Host id (opencode | omp | pi | dsh | cursor | codex | kimi | zcode)").requiredOption("--skill <name>", "Skill name to resolve").option("--rel <path>", "Optional skill-relative path suffix").action((options) => {
|
|
9676
|
+
try {
|
|
9677
|
+
if (HOST_ID_LOOKUP[options.host] !== true) {
|
|
9678
|
+
throw new SddScriptError(`usage: host skill-root \u2014 unknown host "${options.host}" (valid: ${HOST_IDS.join(", ")})`, 2);
|
|
9679
|
+
}
|
|
9680
|
+
if (options.skill.trim() === "") {
|
|
9681
|
+
throw new SddScriptError("usage: host skill-root \u2014 --skill must be a non-empty skill name", 2);
|
|
9682
|
+
}
|
|
9683
|
+
const root = resolveSkillRoot(options.host, { skill: options.skill, rel: options.rel });
|
|
9684
|
+
if (options.host === "pi") {
|
|
9685
|
+
console.log(import_picocolors.default.yellow(root));
|
|
9686
|
+
} else {
|
|
9687
|
+
console.log(import_picocolors.default.green(root));
|
|
9688
|
+
}
|
|
9689
|
+
} catch (error) {
|
|
9690
|
+
failScript(error, "host skill-root");
|
|
9691
|
+
}
|
|
9692
|
+
});
|
|
8973
9693
|
var skillCommand = program2.command("skill").description("skill-authoring lints (engine-backed)");
|
|
8974
9694
|
skillCommand.command("lint").description("Lint <skill-dir>/SKILL.md: frontmatter contract (name lowercase-hyphen, description trigger contract) " + "+ the five-question body + ephemeral-citation scan (task-<digits>-* artifacts and .mstar/sdd/\u2026 deeplinks \u2014 " + "exit 1 on violations, 2 on usage)").argument("[skill-dir]", "Skill directory containing SKILL.md").action((skillDir) => {
|
|
8975
9695
|
try {
|
|
8976
9696
|
if (!skillDir)
|
|
8977
9697
|
throw new SddScriptError("usage: skill lint <skill-dir>", 2);
|
|
8978
|
-
const skillFile =
|
|
8979
|
-
if (!
|
|
9698
|
+
const skillFile = path12.join(resolveCliPath(skillDir), "SKILL.md");
|
|
9699
|
+
if (!fs8.existsSync(skillFile))
|
|
8980
9700
|
throw new Error(`SKILL.md not found: ${skillFile}`);
|
|
8981
|
-
const text =
|
|
9701
|
+
const text = fs8.readFileSync(skillFile, "utf8");
|
|
8982
9702
|
const violations = [];
|
|
8983
9703
|
const frontmatter = lintSkillFrontmatter(text);
|
|
8984
9704
|
printChecklist("skill lint (frontmatter)", frontmatter);
|
|
8985
9705
|
violations.push(...frontmatter.violations);
|
|
8986
|
-
const
|
|
8987
|
-
|
|
8988
|
-
|
|
9706
|
+
const skillBase = path12.basename(path12.dirname(skillFile));
|
|
9707
|
+
const isCore = skillBase === "mstar-harness-core";
|
|
9708
|
+
const fiveQuestionMode = skillBase.startsWith("mstar-") && !isCore && skillBase !== "mstar-skill-authoring" ? "runtime" : "authoring";
|
|
9709
|
+
if (isCore) {
|
|
9710
|
+
console.log(import_picocolors.default.yellow("skill lint (five questions): EXEMPT \u2014 mstar-harness-core is exempt by design (hub headings)"));
|
|
9711
|
+
} else {
|
|
9712
|
+
const fiveQuestion = lintFiveQuestion(stripFrontmatter(text), fiveQuestionMode);
|
|
9713
|
+
printChecklist("skill lint (five questions)", fiveQuestion);
|
|
9714
|
+
violations.push(...fiveQuestion.violations);
|
|
9715
|
+
}
|
|
8989
9716
|
const ephemeral = findEphemeralCitations(text);
|
|
8990
9717
|
const ephemeralGate = {
|
|
8991
9718
|
ok: ephemeral.length === 0,
|
|
@@ -9005,6 +9732,40 @@ skillCommand.command("lint").description("Lint <skill-dir>/SKILL.md: frontmatter
|
|
|
9005
9732
|
failScript(error, "skill lint");
|
|
9006
9733
|
}
|
|
9007
9734
|
});
|
|
9735
|
+
var rolesCommand = program2.command("roles").description("mstar-roles mapping / load-order checks (engine-backed)");
|
|
9736
|
+
rolesCommand.command("validate").description("Validate the mstar-roles skill-dir state: role mapping / parameter tables against the on-disk " + "references layout plus load-order declarations across sibling mstar-* skills " + "(exit 1 on violations)").option("--roles-dir <dir>", "mstar-roles skill directory (default: skills/mstar-roles, resolved against the project root)").option("--skills-dir <dir>", "Skills root scanned for sibling mstar-* skills (default: parent of the roles dir)").action((options) => {
|
|
9737
|
+
try {
|
|
9738
|
+
const rolesDir = resolveCliPath(options.rolesDir ?? "skills/mstar-roles");
|
|
9739
|
+
const skillsRoot = options.skillsDir ? resolveCliPath(options.skillsDir) : path12.dirname(rolesDir);
|
|
9740
|
+
const violations = [];
|
|
9741
|
+
const mapping = validateRoleMapping(rolesDir);
|
|
9742
|
+
printChecklist("roles validate (mapping)", mapping);
|
|
9743
|
+
violations.push(...mapping.violations);
|
|
9744
|
+
const skillTexts = {};
|
|
9745
|
+
for (const entry of fs8.readdirSync(skillsRoot, { withFileTypes: true })) {
|
|
9746
|
+
if (!entry.isDirectory() || !entry.name.startsWith("mstar-"))
|
|
9747
|
+
continue;
|
|
9748
|
+
const skillFile = path12.join(skillsRoot, entry.name, "SKILL.md");
|
|
9749
|
+
if (!fs8.existsSync(skillFile))
|
|
9750
|
+
continue;
|
|
9751
|
+
try {
|
|
9752
|
+
skillTexts[entry.name] = fs8.readFileSync(skillFile, "utf8");
|
|
9753
|
+
} catch {}
|
|
9754
|
+
}
|
|
9755
|
+
const loadOrder = lintLoadOrder(skillTexts);
|
|
9756
|
+
printChecklist("roles validate (load order)", loadOrder);
|
|
9757
|
+
violations.push(...loadOrder.violations);
|
|
9758
|
+
const total = violations.length;
|
|
9759
|
+
const siblingCount = Object.keys(skillTexts).length;
|
|
9760
|
+
const loadOrderChecked = Object.keys(skillTexts).filter((name) => name !== "mstar-harness-core").length;
|
|
9761
|
+
const coreExempt = loadOrderChecked !== siblingCount;
|
|
9762
|
+
console.log(`roles validate: ${total === 0 ? "OK" : "FAIL"} (${total} violation${total === 1 ? "" : "s"}, ` + `${siblingCount} sibling skill${siblingCount === 1 ? "" : "s"} scanned; ` + `load-order over ${loadOrderChecked}${coreExempt ? ", core exempt" : ""})`);
|
|
9763
|
+
if (total > 0)
|
|
9764
|
+
process.exitCode = 1;
|
|
9765
|
+
} catch (error) {
|
|
9766
|
+
failScript(error, "roles validate");
|
|
9767
|
+
}
|
|
9768
|
+
});
|
|
9008
9769
|
program2.parseAsync(process.argv).catch((error) => {
|
|
9009
9770
|
console.error(import_picocolors.default.red(`Setup failed: ${error.message}`));
|
|
9010
9771
|
process.exitCode = 1;
|