@mstar-harness/cli 2.3.0 → 2.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/mstar-harness.js +937 -176
  2. package/package.json +3 -2
@@ -2344,9 +2344,9 @@ var require_commander = __commonJS((exports) => {
2344
2344
  });
2345
2345
 
2346
2346
  // src/index.ts
2347
- import { execFileSync as execFileSync7 } from "child_process";
2348
- import fs7 from "fs";
2349
- import path11 from "path";
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
- function isPlainObject2(value) {
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 (!isPlainObject2(row)) {
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 && !isPlainObject2(metadata)) {
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 && !isPlainObject2(execution_lease)) {
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 (!isPlainObject2(entry)) {
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 (!isPlainObject2(residual_findings)) {
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 (!isPlainObject2(metadata)) {
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 (!isPlainObject2(doc.residual_findings)) {
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) => isPlainObject2(e) && typeof e.id === "string" ? e.id : undefined).filter((id) => id !== undefined));
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 (!isPlainObject2(entry) || typeof entry.id !== "string")
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 lintFiveQuestion(bodyText) {
6355
- const headings = bodyText.split(/\r?\n/).filter((line) => HEADING_RE.test(line)).map((line) => line.replace(/^#{1,6}\s+/, "").trim().toLowerCase());
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 covered = headings.some((heading) => heading.includes(label));
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 existsSync6, mkdirSync as mkdirSync6, readFileSync as readFileSync8, renameSync as renameSync2, unlinkSync as unlinkSync3, writeFileSync as writeFileSync5 } from "node:fs";
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 (!existsSync6(filePath))
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 (existsSync6(join6(dir, "package.json")) || existsSync6(join6(dir, "bun.lock")))
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 violation(severity, code, message, fix) {
6853
+ function violation7(severity, code, message, fix) {
6475
6854
  return { ok: false, severity, code, message, fix };
6476
6855
  }
6477
- function validateNonEmptyString(violations, value, field, missingCode, invalidCode) {
6856
+ function validateNonEmptyString3(violations, value, field, missingCode, invalidCode) {
6478
6857
  if (value === undefined) {
6479
- violations.push(violation("high", missingCode, `missing required field: ${field}`));
6858
+ violations.push(violation7("high", missingCode, `missing required field: ${field}`));
6480
6859
  } else if (typeof value !== "string" || value.trim() === "") {
6481
- violations.push(violation("medium", invalidCode, `${field} must be a non-empty string`));
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 isValidClaimedAt(value) {
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
- violation("high", "lease.execution-lease.invalid", "execution_lease must be an object \u2014 null and tombstone objects are invalid; writers delete the key on release")
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
- validateNonEmptyString(violations, lease.holder, "holder", "lease.execution-lease.missing-holder", "lease.execution-lease.invalid-holder");
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(violation("high", "lease.execution-lease.missing-claimed-at", "missing required field: claimed_at"));
6503
- } else if (!isValidClaimedAt(lease.claimed_at)) {
6504
- violations.push(violation("medium", "lease.execution-lease.invalid-claimed-at", "claimed_at must be an RFC 3339 UTC timestamp with explicit Z (e.g. 2026-07-22T02:30:00Z) or a YYYY-MM-DD date"));
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(violation("high", "lease.execution-lease.missing-worktree-path", "missing required field: worktree_path"));
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(violation("medium", "lease.execution-lease.invalid-worktree-path", "worktree_path must be a non-empty string"));
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(violation("medium", "lease.execution-lease.invalid-worktree-path", "worktree_path must be an absolute path \u2014 it identifies the dedicated feature-worktree root (and MUST differ from metadata.control_worktree_path)"));
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
- validateNonEmptyString(violations, lease.working_branch, "working_branch", "lease.execution-lease.missing-working-branch", "lease.execution-lease.invalid-working-branch");
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(violation("medium", "lease.execution-lease.invalid-session-label", "session_label must be a string (display only \u2014 never used for ownership comparison)"));
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
- violation("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)")
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
- violation("high", "lease.verify.missing", `plan ${planId} has no execution_lease (neither plans[].execution_lease nor legacy plans[].metadata.execution_lease)`)
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(violation("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"));
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(violation("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)"));
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 path7 from "node:path";
7539
- import { execFileSync as execFileSync6 } from "node:child_process";
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
- execFileSync6("omp", ["--version"], { stdio: "pipe", encoding: "utf8" });
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
- execFileSync6("omp", args, { stdio: "pipe", encoding: "utf8" });
8103
+ execFileSync7("omp", args, { stdio: "pipe", encoding: "utf8" });
7557
8104
  }
7558
8105
  function listInstalledPlugins() {
7559
8106
  try {
7560
- const raw = execFileSync6("omp", ["plugin", "list", "--json"], {
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(`${path7.sep}morning-star`))
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 = path7.join(pluginRoot, marker);
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 = path7.join(pluginRoot, "skills", skill, "SKILL.md");
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 = path7.join(pluginRoot, "commands", `${command}.md`);
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 = path7.join(pluginRoot, "skills", "mstar-host", "references", "omp.md");
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 runInit2(scope, dryRun) {
8175
+ function runInit3(scope, dryRun) {
7629
8176
  const notes = ensureLocalHarnessRepo(dryRun);
7630
8177
  const projectRoot = resolveProjectRoot2();
7631
- if (fs5.existsSync(path7.join(HARNESS_REPO_PATH, ".git"))) {
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
- execFileSync6("git", ["-C", HARNESS_REPO_PATH, "pull", "--ff-only"], {
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 runDoctor2(scope) {
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 = path7.join(projectRoot, ".gitignore");
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) => runInit2(scope, dryRun),
7717
- runInstallDoctor: (scope) => runDoctor2(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 os4 from "node:os";
7722
- import path8 from "node:path";
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 path8.isAbsolute(raw) ? raw : path8.join(resolveProjectRoot2(), raw);
8290
+ return path9.isAbsolute(raw) ? raw : path9.join(resolveProjectRoot2(), raw);
7744
8291
  }
7745
8292
  if (scope === "global")
7746
- return path8.join(os4.homedir(), ".config", "opencode", "opencode.json");
7747
- return path8.join(resolveProjectRoot2(), "opencode.json");
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 os5 from "node:os";
7842
- import path9 from "node:path";
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 = path9.join(os5.homedir(), ".zcode", "cli", "plugins");
7854
- var KNOWN_MARKETPLACES_PATH = path9.join(ZCODE_PLUGINS_ROOT, "known_marketplaces.json");
7855
- var MARKETPLACE_DIR = path9.join(ZCODE_PLUGINS_ROOT, "marketplaces", MARKETPLACE_ID);
7856
- var MARKETPLACE_JSON_PATH = path9.join(MARKETPLACE_DIR, "marketplace.json");
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 = path9.join(pluginRoot, "agents");
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 = path9.join(agentsDir, `${agentName}.md`);
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 runInit3(scope, dryRun) {
8531
+ function runInit4(scope, dryRun) {
7985
8532
  const notes = ensureLocalHarnessRepo(dryRun);
7986
8533
  const projectRoot = resolveProjectRoot2();
7987
8534
  if (scope === "project") {
7988
- const checkoutPath = path9.join(projectRoot, ZCODE_PLUGIN_CHECKOUT_PROJECT);
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 runDoctor3(scope) {
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 = path9.join(projectRoot, ZCODE_PLUGIN_CHECKOUT_PROJECT);
8563
+ const checkoutPath = path10.join(projectRoot, ZCODE_PLUGIN_CHECKOUT_PROJECT);
8017
8564
  errors2.push(...validateGitCheckout(checkoutPath, ZCODE_PLUGIN_MARKER));
8018
- const gitignorePath = path9.join(projectRoot, ".gitignore");
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) => runInit3(scope, dryRun),
8038
- runInstallDoctor: (scope) => runDoctor3(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 path10 from "node:path";
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 path10.resolve(candidate);
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 runInit4(options) {
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 runDoctor4(options) {
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 path11.resolve(options.root);
8801
+ return path12.resolve(options.root);
8216
8802
  let candidate = resolveProjectRoot3();
8217
- while (!fs7.existsSync(path11.join(candidate, "plugin.json"))) {
8218
- const parent = path11.dirname(candidate);
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 runInit4(options);
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
- runDoctor4(options);
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 ? path11.resolve(pathArg) : process.cwd();
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 path11.resolve(pathArg);
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 path11.join(harnessDir, "status.json");
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 (!fs7.existsSync(statusPath)) {
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 violation7 of gate2.violations) {
8299
- console.error(` - [${violation7.severity}] ${violation7.code}: ${violation7.message}`);
8300
- if (violation7.fix)
8301
- console.error(` fix: ${violation7.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
- var leaseCommand = program2.command("lease").description("execution_lease checks (engine-backed) \u2014 integration_merge_lease validation stays import-only via @mstar-harness/engine until a dedicated subcommand exists");
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 path11.resolve(harnessArg);
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 = path11.join(harnessDir, "status.json");
8340
- if (!fs7.existsSync(statusPath)) {
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 violation7 of result.violations) {
8367
- console.error(` - [${violation7.severity}] ${violation7.code}: ${violation7.message}`);
8368
- if (violation7.fix)
8369
- console.error(` fix: ${violation7.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 violation7 of gate2.violations) {
8430
- console.error(` - [${violation7.severity}] ${violation7.code}: ${violation7.message}`);
8431
- if (violation7.fix)
8432
- console.error(` fix: ${violation7.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 = path11.resolve(options.status);
8438
- const compassPath = path11.resolve(options.compass);
8439
- if (!fs7.existsSync(statusPath))
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 (!fs7.existsSync(compassPath))
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 violation7 of result.violations) {
8467
- console.error(` - [${violation7.severity}] ${violation7.code}: ${violation7.message}`);
8468
- if (violation7.fix)
8469
- console.error(` fix: ${violation7.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 = path11.resolve(assignmentFile);
8480
- if (!fs7.existsSync(file)) {
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 = fs7.readFileSync(file, "utf8");
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 ? path11.resolve(options.status) : resolveStatusFilePath();
8539
- if (!fs7.existsSync(statusPath)) {
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 ? path11.resolve(options.control) : String(metadata.control_worktree_path ?? ""),
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 = path11.resolve(assignmentFile);
8589
- if (!fs7.existsSync(file)) {
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 = fs7.readFileSync(file, "utf8");
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 = path11.basename(filePath);
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 = path11.dirname(filePath);
8657
- if (dir.includes(`${path11.sep}plans${path11.sep}`) || dir.endsWith(`${path11.sep}plans`))
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[path11.extname(base).toLowerCase()] === true)
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 fs7.readdirSync(current, { withFileTypes: true })) {
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(path11.join(current, entry.name));
8672
- } else if (entry.isFile() && lintTargetType(path11.join(current, entry.name)) !== null) {
8673
- targets.push(path11.join(current, entry.name));
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 = path11.resolve(filePath);
8682
- const text = fs7.readFileSync(abs, "utf8");
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 "${path11.basename(abs)}" (lintable: plan files, SKILL.md, STRATEGY.md, task-N-report.md, code files)`, 2);
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 = path11.resolve(target);
8721
- if (!fs7.existsSync(abs))
9431
+ const abs = resolveCliPath(target);
9432
+ if (!fs8.existsSync(abs))
8722
9433
  throw new Error(`lint target not found: ${abs}`);
8723
- const targets = fs7.statSync(abs).isDirectory() ? collectLintTargets(abs) : [abs];
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 violation7 of result.violations) {
8751
- console.error(` - [${violation7.severity}] ${violation7.code}: ${violation7.message}`);
8752
- if (violation7.fix)
8753
- console.error(` fix: ${violation7.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 = path11.resolve(dir);
8779
- const lightPath = path11.join(abs, "DESIGN.md");
8780
- if (!fs7.existsSync(lightPath))
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 = fs7.readFileSync(lightPath, "utf8");
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 = path11.join(abs, "DESIGN.dark.md");
8788
- if (fs7.existsSync(darkPath)) {
8789
- const parity = assertLightDarkParity(light, fs7.readFileSync(darkPath, "utf8"));
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 = execFileSync7("git", ["rev-parse", "--short", "HEAD"], {
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 = path11.resolve(findingsFile);
8879
- if (!fs7.existsSync(abs))
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 ? path11.resolve(options.dir) : path11.resolve(`audit-${date}`);
8889
- const findings = parseAuditFindings(fs7.readFileSync(abs, "utf8"));
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 = path11.resolve(docPath);
8906
- if (!fs7.existsSync(abs))
9605
+ const abs = resolveCliPath(docPath);
9606
+ if (!fs8.existsSync(abs))
8907
9607
  throw new Error(`knowledge doc not found: ${abs}`);
8908
- const text = fs7.readFileSync(abs, "utf8");
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 = path11.resolve(options.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 = path11.join(path11.resolve(skillDir), "SKILL.md");
8979
- if (!fs7.existsSync(skillFile))
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 = fs7.readFileSync(skillFile, "utf8");
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 fiveQuestion = lintFiveQuestion(stripFrontmatter(text));
8987
- printChecklist("skill lint (five questions)", fiveQuestion);
8988
- violations.push(...fiveQuestion.violations);
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;