@mstar-harness/engine 3.1.2 → 3.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/core.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Severity levels used across harness validation results.
3
3
  *
4
- * Machine SSOT — `mstar-plan-artifacts/references/status-and-residuals.md`
4
+ * Machine SSOT — `mstar-artifacts/references/status-and-residuals.md`
5
5
  * § "Residual findings: `severity` (SSOT, machine field)" defines the same
6
6
  * five lowercase-English values; `warning` / `Major` / any other value are
7
7
  * forbidden in JSON severity fields.
@@ -83,7 +83,7 @@ export declare function readJson(filePath: string): Record<string, unknown>;
83
83
  * stored state; revisit if the harness moves to a filesystem without
84
84
  * rename-atomicity guarantees.
85
85
  */
86
- export declare function writeJson(filePath: string, value: Record<string, unknown>): void;
86
+ export declare function writeJson<T>(filePath: string, value: T): void;
87
87
  /**
88
88
  * Resolve the project root by walking up from `startDir` (default: cwd) to
89
89
  * the nearest ancestor containing `package.json` or `bun.lock`. Falls back
package/dist/engine.js CHANGED
@@ -1350,41 +1350,42 @@ function validateStatusV2(docOrPath, opts = {}) {
1350
1350
  violations.push(violation4("medium", "status.workflow.mismatched-type", `workflows[] entry ${JSON.stringify(label)} type ${JSON.stringify(entry.type)} does not match its snapshot type ${JSON.stringify(snapshot.type)} — the root entry mirrors the snapshot; align them`));
1351
1351
  }
1352
1352
  if (typeof entry.started_at === "string" && typeof snapshot.started_at === "string" && entry.started_at !== snapshot.started_at) {
1353
- violations.push(violation4("medium", "status.workflow.mismatched-started-at", `workflows[] entry ${JSON.stringify(label)} started_at ${JSON.stringify(entry.started_at)} does not match its snapshot started_at ${JSON.stringify(snapshot.started_at)} — the root entry mirrors the snapshot; align them`));
1353
+ violations.push(violation4("medium", "status.workflow.mismatched-started-at", `workflows[] entry ${JSON.stringify(label)} started_at ${JSON.stringify(entry.started_at)} does not match its snapshot started_at ${JSON.stringify(snapshot.started_at)} — workflow ${JSON.stringify(label)} collided with another writer (e.g. a concurrent/re-run \`audit promote\` with the same workflow id rewrote the snapshot); the root entry mirrors the snapshot align them or remove the colliding workflow`));
1354
1354
  }
1355
1355
  }
1356
1356
  }
1357
1357
  return { ok: violations.length === 0, violations };
1358
1358
  }
1359
1359
  var validateStatus = validateStatusV2;
1360
+ function registerWorkflowEntryLocked(statusPath, entry) {
1361
+ const harnessDir = dirname5(statusPath);
1362
+ const current = readJson(statusPath);
1363
+ const fresh = Object.keys(current).length === 0;
1364
+ const doc = fresh ? { version: 2, updated_at: todayString(), workflows: [] } : current;
1365
+ if (!fresh && !Array.isArray(doc.workflows)) {
1366
+ throw new Error("refusing to modify status.json: workflows must be an array — a v1 root must be migrated first (run `mstar migrate`)");
1367
+ }
1368
+ const existing = doc.workflows.findIndex((wf) => wf.id === entry.id);
1369
+ if (existing >= 0) {
1370
+ doc.workflows[existing] = entry;
1371
+ } else {
1372
+ doc.workflows.push(entry);
1373
+ }
1374
+ doc.updated_at = todayString();
1375
+ const gate = validateStatusV2(doc, { harnessDir });
1376
+ if (!gate.ok) {
1377
+ throw new Error(`refusing to write invalid status.json: ${gate.violations.map((v) => v.message).join("; ")}`);
1378
+ }
1379
+ writeJson(statusPath, doc);
1380
+ return doc;
1381
+ }
1360
1382
  async function registerWorkflow(root, entry) {
1361
1383
  const entryGate = validateWorkflowEntry(entry);
1362
1384
  if (!entryGate.ok) {
1363
1385
  throw new Error(`refusing to register invalid workflow entry: ${entryGate.violations.map((v) => v.message).join("; ")}`);
1364
1386
  }
1365
1387
  const statusPath = resolve5(root);
1366
- const harnessDir = dirname5(statusPath);
1367
- return withStatusWriteLock(statusPath, () => {
1368
- const current = readJson(statusPath);
1369
- const fresh = Object.keys(current).length === 0;
1370
- const doc = fresh ? { version: 2, updated_at: todayString(), workflows: [] } : current;
1371
- if (!fresh && !Array.isArray(doc.workflows)) {
1372
- throw new Error("refusing to modify status.json: workflows must be an array — a v1 root must be migrated first (run `mstar migrate`)");
1373
- }
1374
- const existing = doc.workflows.findIndex((wf) => wf.id === entry.id);
1375
- if (existing >= 0) {
1376
- doc.workflows[existing] = entry;
1377
- } else {
1378
- doc.workflows.push(entry);
1379
- }
1380
- doc.updated_at = todayString();
1381
- const gate = validateStatusV2(doc, { harnessDir });
1382
- if (!gate.ok) {
1383
- throw new Error(`refusing to write invalid status.json: ${gate.violations.map((v) => v.message).join("; ")}`);
1384
- }
1385
- writeJson(statusPath, doc);
1386
- return doc;
1387
- });
1388
+ return withStatusWriteLock(statusPath, () => registerWorkflowEntryLocked(statusPath, entry));
1388
1389
  }
1389
1390
  async function unregisterWorkflow(root, id) {
1390
1391
  if (typeof id !== "string" || id.trim() === "") {
@@ -2990,9 +2991,11 @@ async function applyMigratePlan(plan) {
2990
2991
  if (!gate2.ok) {
2991
2992
  throw new Error(`refusing to apply migration: invalid project register: ${gate2.violations.map((v) => v.message).join("; ")}`);
2992
2993
  }
2993
- const filePath = projectTargetOf(plan.register.file);
2994
- mkdirSync6(dirname7(filePath), { recursive: true });
2995
- writeJson(filePath, plan.register.data);
2994
+ if (Object.keys(plan.register.data.entries ?? {}).length > 0) {
2995
+ const filePath = projectTargetOf(plan.register.file);
2996
+ mkdirSync6(dirname7(filePath), { recursive: true });
2997
+ writeJson(filePath, plan.register.data);
2998
+ }
2996
2999
  }
2997
3000
  if (plan.roadmap !== null) {
2998
3001
  const filePath = projectTargetOf(plan.roadmap.file);
@@ -3419,8 +3422,8 @@ function completenessLevel(frontmatterText, checklist) {
3419
3422
  return { level, items, missing, placeholders, upgradeTo, bodyUnverified };
3420
3423
  }
3421
3424
  // src/audit.ts
3422
- import { mkdirSync as mkdirSync7, readdirSync as readdirSync7, readFileSync as readFileSync9, writeFileSync as writeFileSync5 } from "node:fs";
3423
- import { join as join11, resolve as resolve9 } from "node:path";
3425
+ import { existsSync as existsSync7, mkdirSync as mkdirSync7, readdirSync as readdirSync7, readFileSync as readFileSync9, rmdirSync as rmdirSync2, rmSync, writeFileSync as writeFileSync5 } from "node:fs";
3426
+ import { basename as basename4, join as join11, resolve as resolve9, sep as sep3 } from "node:path";
3424
3427
  function violation9(severity, code, message, fix) {
3425
3428
  return { ok: false, severity, code, message, fix };
3426
3429
  }
@@ -3465,14 +3468,14 @@ function validateAuditStatusBlocks(planText) {
3465
3468
  const violations = [];
3466
3469
  const blocks = parseStatusBlocks(planText);
3467
3470
  if (blocks.length === 0) {
3468
- violations.push(violation9("medium", "audit.status.missing-block", "no `## Status` block found — audit plan files carry the Status block fields (mstar-audit SKILL § Plan files)", "add a `## Status` block with Priority, Effort, Risk, Depends on, Category, Planned at"));
3471
+ violations.push(violation9("medium", "audit.status.missing-block", "no `## Status` block found — audit plan files carry the Status block fields (mstar-audit SKILL.md § Plan output)", "add a `## Status` block with Priority, Effort, Risk, Depends on, Category, Planned at"));
3469
3472
  return { ok: false, violations };
3470
3473
  }
3471
3474
  blocks.forEach((block, index) => {
3472
3475
  const label = blocks.length > 1 ? ` #${index + 1}` : "";
3473
3476
  for (const field of AUDIT_STATUS_FIELDS) {
3474
3477
  if (!block.fields.has(field)) {
3475
- violations.push(violation9("medium", "audit.status.missing-field", `Status block${label} missing required field "${field}" (mstar-audit SKILL § Plan files)`, `add \`- **${field}**: <value>\` to the Status block`));
3478
+ violations.push(violation9("medium", "audit.status.missing-field", `Status block${label} missing required field "${field}" (mstar-audit SKILL.md § Plan output)`, `add \`- **${field}**: <value>\` to the Status block`));
3476
3479
  }
3477
3480
  }
3478
3481
  const check = (field, pattern, code, expected) => {
@@ -3480,7 +3483,7 @@ function validateAuditStatusBlocks(planText) {
3480
3483
  if (value === undefined)
3481
3484
  return;
3482
3485
  if (!pattern.test(value)) {
3483
- violations.push(violation9("medium", code, `Status block${label} "${field}" = "${value}" — expected ${expected} (mstar-audit SKILL § Plan files)`, `fix \`- **${field}**:\` to one of: ${expected}`));
3486
+ violations.push(violation9("medium", code, `Status block${label} "${field}" = "${value}" — expected ${expected} (mstar-audit SKILL.md § Plan output)`, `fix \`- **${field}**:\` to one of: ${expected}`));
3484
3487
  }
3485
3488
  };
3486
3489
  check("Priority", /^P[123]$/, "audit.status.invalid-priority", "P1 | P2 | P3");
@@ -3492,74 +3495,6 @@ function validateAuditStatusBlocks(planText) {
3492
3495
  });
3493
3496
  return { ok: violations.length === 0, violations };
3494
3497
  }
3495
- var WHOLE_MATCH_PATTERNS = [
3496
- { type: "private-key", re: /-----BEGIN [A-Z0-9 ]*PRIVATE KEY-----/g },
3497
- { type: "aws-access-key", re: /\b(?:AKIA|ASIA)[0-9A-Z]{16}\b/g },
3498
- { type: "github-token", re: /\bgh[pousr]_[A-Za-z0-9]{36,}\b/g },
3499
- { type: "slack-token", re: /\bxox[baprs]-[A-Za-z0-9-]{10,}\b/g },
3500
- { type: "jwt", re: /\beyJ[A-Za-z0-9_-]{10,1024}\.[A-Za-z0-9_-]{10,1024}\.[A-Za-z0-9_-]{10,1024}\b/g },
3501
- { type: "api-secret-key", re: /\bsk-[A-Za-z0-9-]{20,}\b/g }
3502
- ];
3503
- var VALUE_PATTERNS = [
3504
- {
3505
- typeOf: (key) => key.replace(/([a-z0-9])([A-Z])/g, "$1-$2").toLowerCase().replace(/[_-]+/g, "-"),
3506
- re: /(["']?)\b(password|passwd|api[_-]?key|access[_-]?token|auth[_-]?token|secret|token)\b(["']?)(\s*[:=]\s*)("[^"\n]{8,}"|'[^'\n]{8,}'|[A-Za-z0-9_./+\-=]{16,})/gi
3507
- }
3508
- ];
3509
- function buildLineStarts(text) {
3510
- const starts = [0];
3511
- for (let i = 0;i < text.length; i++) {
3512
- if (text[i] === `
3513
- `)
3514
- starts.push(i + 1);
3515
- }
3516
- return starts;
3517
- }
3518
- function lineAt(starts, index) {
3519
- let lo = 0;
3520
- let hi = starts.length - 1;
3521
- while (lo < hi) {
3522
- const mid = lo + hi + 1 >> 1;
3523
- if (starts[mid] <= index)
3524
- lo = mid;
3525
- else
3526
- hi = mid - 1;
3527
- }
3528
- return lo + 1;
3529
- }
3530
- function redactSecrets(text, filePath) {
3531
- const starts = buildLineStarts(text);
3532
- const marker = (type, index) => `[REDACTED ${type}@${lineAt(starts, index)}${filePath === undefined ? "" : ` in ${filePath}`}]`;
3533
- const replacements = [];
3534
- const findings = [];
3535
- for (const pattern of WHOLE_MATCH_PATTERNS) {
3536
- for (const match of text.matchAll(pattern.re)) {
3537
- if (match.index === undefined)
3538
- continue;
3539
- replacements.push({ index: match.index, length: match[0].length, text: marker(pattern.type, match.index) });
3540
- findings.push({ line: lineAt(starts, match.index), type: pattern.type });
3541
- }
3542
- }
3543
- for (const pattern of VALUE_PATTERNS) {
3544
- for (const match of text.matchAll(pattern.re)) {
3545
- if (match.index === undefined)
3546
- continue;
3547
- const type = pattern.typeOf(match[2]);
3548
- const replacement = `${match[1]}${match[2]}${match[3]}${match[4]}${marker(type, match.index)}`;
3549
- replacements.push({ index: match.index, length: match[0].length, text: replacement });
3550
- findings.push({ line: lineAt(starts, match.index), type });
3551
- }
3552
- }
3553
- replacements.sort((a, b) => b.index - a.index);
3554
- let out = text;
3555
- for (const r of replacements)
3556
- out = out.slice(0, r.index) + r.text + out.slice(r.index + r.length);
3557
- const deduped = new Map;
3558
- for (const f of findings)
3559
- deduped.set(`${f.line}:${f.type}`, f);
3560
- const sorted = [...deduped.values()].sort((a, b) => a.line - b.line || a.type.localeCompare(b.type));
3561
- return { text: out, findings: sorted };
3562
- }
3563
3498
  function slugify(title) {
3564
3499
  return title.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
3565
3500
  }
@@ -3696,9 +3631,140 @@ function scaffoldAuditPlan(outDir, findings, options = {}) {
3696
3631
  }));
3697
3632
  return { outDir: resolve9(outDir), date, files: written, nextNumber: next };
3698
3633
  }
3634
+ async function promoteAuditPlans(outDir, selected, options) {
3635
+ if (selected.length === 0) {
3636
+ throw new Error("promoteAuditPlans: at least one plan id must be selected (--plans 001,002,…)");
3637
+ }
3638
+ if (typeof options.harnessDir !== "string" || options.harnessDir.trim() === "") {
3639
+ throw new Error("promoteAuditPlans: options.harnessDir is required (must contain status.json + workflows/)");
3640
+ }
3641
+ const workflowId = options.workflowId ?? basename4(resolve9(outDir));
3642
+ assertSafePathComponent(workflowId, "workflow id");
3643
+ const harnessDir = resolve9(options.harnessDir);
3644
+ const statusPath = join11(harnessDir, "status.json");
3645
+ const workflowDir = join11(harnessDir, "workflows", workflowId);
3646
+ const snapshotPath = join11(workflowDir, WORKFLOW_SNAPSHOT_FILE);
3647
+ const planFiles = resolveSelectedPlanFiles(outDir, selected);
3648
+ const indexRows = readExecutionOrderIndex(outDir);
3649
+ const plans = planFiles.map((planFile) => {
3650
+ const stem = planFile.replace(/\.md$/, "");
3651
+ const num = stem.slice(0, 3);
3652
+ const indexRow = indexRows.get(num);
3653
+ const title = indexRow?.title ?? readPlanFileSummary(join11(outDir, planFile)).title;
3654
+ return {
3655
+ id: stem,
3656
+ title,
3657
+ file: planFileRel(outDir, planFile),
3658
+ status: "Todo"
3659
+ };
3660
+ });
3661
+ const now = new Date;
3662
+ const snapshot = {
3663
+ schema_version: 1,
3664
+ id: workflowId,
3665
+ type: "plan",
3666
+ status: "running",
3667
+ started_at: now.toISOString(),
3668
+ updated_at: now.toISOString().slice(0, 10),
3669
+ plans
3670
+ };
3671
+ const entry = {
3672
+ id: workflowId,
3673
+ type: "plan",
3674
+ started_at: snapshot.started_at,
3675
+ dir: `workflows/${workflowId}`
3676
+ };
3677
+ const entryGate = validateWorkflowEntry(entry);
3678
+ if (!entryGate.ok) {
3679
+ throw new Error(`refusing to register invalid workflow entry: ${entryGate.violations.map((v) => v.message).join("; ")}`);
3680
+ }
3681
+ await withStatusWriteLock(statusPath, () => {
3682
+ if (existsSync7(snapshotPath)) {
3683
+ throw new Error(`refusing to promote audit plans: workflow ${JSON.stringify(workflowId)} already exists ` + `(snapshot at ${snapshotPath}) — re-promote would drop its registered plan rows; ` + `remove that workflow before promoting again`);
3684
+ }
3685
+ mkdirSync7(workflowDir, { recursive: true });
3686
+ try {
3687
+ writeJson(snapshotPath, snapshot);
3688
+ registerWorkflowEntryLocked(statusPath, entry);
3689
+ } catch (error) {
3690
+ rmSync(snapshotPath, { force: true });
3691
+ try {
3692
+ if (readdirSync7(workflowDir).length === 0) {
3693
+ rmdirSync2(workflowDir);
3694
+ }
3695
+ } catch {}
3696
+ throw error;
3697
+ }
3698
+ return { workflowId, snapshotPath };
3699
+ });
3700
+ return { workflowId, snapshotPath };
3701
+ }
3702
+ function resolveSelectedPlanFiles(outDir, selected) {
3703
+ const files = readdirSync7(outDir).filter((f) => /^\d{3}-.*\.md$/.test(f)).sort();
3704
+ const byNum = new Map;
3705
+ const byStem = new Map;
3706
+ for (const file of files) {
3707
+ const stem = file.replace(/\.md$/, "");
3708
+ if (!byNum.has(stem.slice(0, 3))) {
3709
+ byNum.set(stem.slice(0, 3), file);
3710
+ }
3711
+ byStem.set(stem, file);
3712
+ }
3713
+ const resolved = [];
3714
+ const seen = new Set;
3715
+ for (const id of selected) {
3716
+ const file = byNum.get(id) ?? byStem.get(id) ?? byStem.get(id.replace(/\.md$/, ""));
3717
+ if (file === undefined) {
3718
+ throw new Error(`promoteAuditPlans: selected plan ${JSON.stringify(id)} does not match any NNN-*.md file in ${resolve9(outDir)}`);
3719
+ }
3720
+ if (!seen.has(file)) {
3721
+ seen.add(file);
3722
+ resolved.push(file);
3723
+ }
3724
+ }
3725
+ return resolved;
3726
+ }
3727
+ function readExecutionOrderIndex(outDir) {
3728
+ const readmePath = join11(outDir, "README.md");
3729
+ let text;
3730
+ try {
3731
+ text = readFileSync9(readmePath, "utf8");
3732
+ } catch {
3733
+ return new Map;
3734
+ }
3735
+ const rows = new Map;
3736
+ const lines = text.split(`
3737
+ `);
3738
+ let inSection = false;
3739
+ for (const line of lines) {
3740
+ if (/^##\s+Execution order & status/.test(line)) {
3741
+ inSection = true;
3742
+ continue;
3743
+ }
3744
+ if (inSection && /^#/.test(line)) {
3745
+ break;
3746
+ }
3747
+ if (!inSection)
3748
+ continue;
3749
+ const cells = line.split(/(?<!\\)\|/).map((c) => c.trim());
3750
+ if (cells.length >= 3 && /^\d{3}$/.test(cells[1])) {
3751
+ rows.set(cells[1], { title: cells[2].replace(/\\\|/g, "|") });
3752
+ }
3753
+ }
3754
+ return rows;
3755
+ }
3756
+ function planFileRel(outDir, planFile) {
3757
+ const resolved = resolve9(outDir);
3758
+ const parts = resolved.split(sep3);
3759
+ const plansIdx = parts.lastIndexOf("plans");
3760
+ if (plansIdx >= 0) {
3761
+ return `${parts.slice(plansIdx + 1).join(sep3)}${sep3}${planFile}`;
3762
+ }
3763
+ return planFile;
3764
+ }
3699
3765
  // src/compound.ts
3700
- import { existsSync as existsSync7, readdirSync as readdirSync8, readFileSync as readFileSync10 } from "node:fs";
3701
- import { basename as basename4, isAbsolute as isAbsolute7, join as join12, relative as relative4, resolve as resolve10, sep as sep3 } from "node:path";
3766
+ import { existsSync as existsSync8, readdirSync as readdirSync8, readFileSync as readFileSync10 } from "node:fs";
3767
+ import { basename as basename5, isAbsolute as isAbsolute7, join as join12, relative as relative4, resolve as resolve10, sep as sep4 } from "node:path";
3702
3768
  function violation10(severity, code, message, fix) {
3703
3769
  return { ok: false, severity, code, message, fix };
3704
3770
  }
@@ -4005,7 +4071,7 @@ function referenceExists(repoRoot, docText) {
4005
4071
  for (const { ref, isSymbol, module } of refs) {
4006
4072
  if (!isSymbol || module === undefined) {
4007
4073
  const candidate = ref.replace(LINE_SUFFIX_RE, "").replace(ANCHOR_RE, "");
4008
- if (existsSync7(resolve10(repoRoot, candidate))) {
4074
+ if (existsSync8(resolve10(repoRoot, candidate))) {
4009
4075
  checked++;
4010
4076
  } else {
4011
4077
  violations.push(violation10("medium", "compound.reference.missing-file", `referenced path \`${ref}\` does not exist under ${repoRoot} (compound-refresh Phase 2: referenced code still exists?)`, "update the doc to reference an existing path, or delete the stale reference"));
@@ -4036,7 +4102,7 @@ function collectKnowledgeDocs(dir) {
4036
4102
  if (entry.isDirectory()) {
4037
4103
  stack.push(full);
4038
4104
  } else if (entry.name.endsWith(".md") && entry.name !== "README.md" && entry.name !== "index.md") {
4039
- docs.push(relative4(dir, full).split(sep3).join("/"));
4105
+ docs.push(relative4(dir, full).split(sep4).join("/"));
4040
4106
  }
4041
4107
  }
4042
4108
  }
@@ -4053,7 +4119,7 @@ function normalizeIndexRef(cell) {
4053
4119
  function assertIndexRows(knowledgeDir) {
4054
4120
  const violations = [];
4055
4121
  const readmePath = join12(knowledgeDir, "README.md");
4056
- if (!existsSync7(readmePath)) {
4122
+ if (!existsSync8(readmePath)) {
4057
4123
  violations.push(violation10("medium", "compound.index.missing-readme", `missing ${readmePath} — the knowledge index is required (mstar-compound Phase 6: every doc gets a README.md row)`, "create knowledge/README.md with a Document / Source Plan / Description / Status table"));
4058
4124
  return { ok: false, violations };
4059
4125
  }
@@ -4085,7 +4151,7 @@ function compoundRefreshScope(harnessDir, projectRoot) {
4085
4151
  ];
4086
4152
  }
4087
4153
  function isFileLikeRoot(root) {
4088
- return /^[^.]*\.[A-Za-z0-9]{1,10}$/.test(basename4(root));
4154
+ return /^[^.]*\.[A-Za-z0-9]{1,10}$/.test(basename5(root));
4089
4155
  }
4090
4156
  function scopeGuard(path, allowedRoots) {
4091
4157
  const resolved = resolve10(path);
@@ -4094,7 +4160,7 @@ function scopeGuard(path, allowedRoots) {
4094
4160
  if (isFileLikeRoot(r)) {
4095
4161
  if (resolved === r)
4096
4162
  return { ok: true, violations: [] };
4097
- } else if (resolved === r || resolved.startsWith(r + sep3)) {
4163
+ } else if (resolved === r || resolved.startsWith(r + sep4)) {
4098
4164
  return { ok: true, violations: [] };
4099
4165
  }
4100
4166
  }
@@ -4243,7 +4309,7 @@ function planQualityBar(planText) {
4243
4309
  if (token !== null) {
4244
4310
  const text = trimmed.length > 80 ? `${trimmed.slice(0, 77)}...` : trimmed;
4245
4311
  findings.push({ token, line: i + 1, text });
4246
- violations.push(violation11("medium", "lint.plan-quality.placeholder", `placeholder token "${token}" at line ${i + 1}: "${text}"`, "replace the placeholder with concrete content before locking the plan (mstar-plan-artifacts/references/plan-quality-bar.md; templates/plan.main.md placeholder scan)"));
4312
+ violations.push(violation11("medium", "lint.plan-quality.placeholder", `placeholder token "${token}" at line ${i + 1}: "${text}"`, "replace the placeholder with concrete content before locking the plan (mstar-artifacts/references/plan-quality-bar.md; templates/plan.main.md placeholder scan)"));
4247
4313
  }
4248
4314
  }
4249
4315
  return { ok: violations.length === 0, violations, findings };
@@ -4338,7 +4404,7 @@ function lintStrategySections(docText) {
4338
4404
  return { ok: violations.length === 0, violations };
4339
4405
  }
4340
4406
  // src/roles.ts
4341
- import { existsSync as existsSync8 } from "node:fs";
4407
+ import { existsSync as existsSync9 } from "node:fs";
4342
4408
  import { join as join13 } from "node:path";
4343
4409
  function violation12(severity, code, message, fix) {
4344
4410
  return { ok: false, severity, code, message, fix };
@@ -4395,7 +4461,7 @@ function validateRoleMapping(rolesDir, options = {}) {
4395
4461
  const violations = [];
4396
4462
  const referenceById = new Map(mapping.map((m) => [m.agentId, m.reference]));
4397
4463
  for (const { agentId, reference } of mapping) {
4398
- if (!existsSync8(join13(rolesDir, reference))) {
4464
+ if (!existsSync9(join13(rolesDir, reference))) {
4399
4465
  violations.push(violation12("medium", "roles.mapping.reference.missing", `role "${agentId}" maps to ${reference} which does not exist under ${rolesDir} (mstar-roles § Role Reference Mapping)`, `create ${join13(rolesDir, reference)} or fix the mapping row`));
4400
4466
  }
4401
4467
  }
@@ -4651,11 +4717,11 @@ export {
4651
4717
  releaseLease,
4652
4718
  registerWorkflow,
4653
4719
  referenceExists,
4654
- redactSecrets,
4655
4720
  readProgressLedger,
4656
4721
  readJson,
4657
4722
  readHarnessVersion,
4658
4723
  pushCadenceProbe,
4724
+ promoteAuditPlans,
4659
4725
  planQualityBar,
4660
4726
  planExecutionLeaseLocations,
4661
4727
  parseMstarc,
package/dist/index.d.ts CHANGED
@@ -46,8 +46,8 @@ export type { MigrateNotesFile, MigrateOptions, MigratePlan, MigrateRegister, Mi
46
46
  export { ARCHIVED_STATUS_V1_FILE, MIGRATE_STATUS_FILE, NOTES_LEDGER_FILE, applyMigratePlan, migrateHarnessTree, } from "./migrate.js";
47
47
  export type { CompletenessItem, CompletenessLevel, CompletenessPlaceholder, CompletenessResult, DesignFrontmatter, } from "./design-md.js";
48
48
  export { assertLightDarkParity, completenessLevel, parseDesignFrontmatter, validateDesignTokenFrontmatter, } from "./design-md.js";
49
- export type { AuditCategory, AuditEffort, AuditFinding, AuditPriority, AuditRisk, RedactResult, ScaffoldAuditPlanOptions, ScaffoldAuditPlanResult, SecretFinding, } from "./audit.js";
50
- export { AUDIT_CATEGORIES, AUDIT_EFFORTS, AUDIT_PRIORITIES, AUDIT_RISKS, redactSecrets, scaffoldAuditPlan, validateAuditStatusBlocks, } from "./audit.js";
49
+ export type { AuditCategory, AuditEffort, AuditFinding, AuditPriority, AuditRisk, PromoteAuditPlansOptions, RedactResult, ScaffoldAuditPlanOptions, ScaffoldAuditPlanResult, SecretFinding, } from "./audit.js";
50
+ export { AUDIT_CATEGORIES, AUDIT_EFFORTS, AUDIT_PRIORITIES, AUDIT_RISKS, promoteAuditPlans, scaffoldAuditPlan, validateAuditStatusBlocks, } from "./audit.js";
51
51
  export type { ReferenceCheckResult } from "./compound.js";
52
52
  export { KNOWLEDGE_BUG_PROBLEM_TYPES, KNOWLEDGE_CATEGORY_MAP, KNOWLEDGE_KNOWLEDGE_PROBLEM_TYPES, KNOWLEDGE_PROBLEM_TYPES, KNOWLEDGE_REQUIRED_FIELDS, KNOWLEDGE_RESOLUTION_TYPES, KNOWLEDGE_SEVERITIES, assertIndexRows, compoundRefreshScope, referenceExists, scopeGuard, validateSchemaYaml, } from "./compound.js";
53
53
  export type { EphemeralCitation, PlanQualityFinding, PlanQualityResult, SimplifyMarker, TemporaryMarker, TemporaryMarkerResult, } from "./lint.js";
package/dist/lint.d.ts CHANGED
@@ -18,7 +18,7 @@
18
18
  * completion evidence must include the TDD triple (test file(s), command,
19
19
  * output) in `task-N-report.md`; `mstar-sdd/references/file-handoffs.md` —
20
20
  * fix subagents append covering test file(s), command run, output.
21
- * - Plan quality bar: `mstar-plan-artifacts/references/plan-quality-bar.md`
21
+ * - Plan quality bar: `mstar-artifacts/references/plan-quality-bar.md`
22
22
  * § Quality checklist + `templates/plan.main.md` self-review
23
23
  * ("Placeholder scan: no TBD").
24
24
  * - Skill frontmatter contract: `mstar-skill-authoring` SKILL.md § Frontmatter
@@ -180,7 +180,7 @@ export type PlanQualityResult = GateResult & {
180
180
  findings: PlanQualityFinding[];
181
181
  };
182
182
  /**
183
- * Plan quality bar — placeholder scan (mstar-plan-artifacts
183
+ * Plan quality bar — placeholder scan (mstar-artifacts
184
184
  * `references/plan-quality-bar.md` § Quality checklist + `templates/
185
185
  * plan.main.md` self-review "Placeholder scan: no TBD"). Every placeholder
186
186
  * token found becomes one `lint.plan-quality.placeholder` violation whose
package/dist/status.d.ts CHANGED
@@ -136,6 +136,26 @@ export declare function validateStatusV2(docOrPath: StatusV2Doc | string, opts?:
136
136
  * closed on v1 input with the `mstar migrate` hint.
137
137
  */
138
138
  export declare const validateStatus: typeof validateStatusV2;
139
+ /**
140
+ * Root-file workflow upsert, to be called ONLY while the caller holds the
141
+ * root `withStatusWriteLock(statusPath)` (see `registerWorkflow` and the
142
+ * audit promote path, which call this from inside their lock — the root
143
+ * lock is the serialization point for read-check-replace-verify).
144
+ *
145
+ * Idempotent upsert by entry `id`, bumping root `updated_at`. A
146
+ * missing/empty root file is initialized from the v2 template (never a v1
147
+ * tree); a v1 root is refused with the `mstar migrate` hint (no silent
148
+ * mutation of an un-migrated tree). The final document is validated with
149
+ * `validateStatusV2` (including the removal-at-terminal snapshot invariant
150
+ * against `dirname(statusPath)`) before the write — an entry whose
151
+ * snapshot is missing or terminal is refused and nothing is written.
152
+ *
153
+ * The caller must validate the entry (`validateWorkflowEntry`) before
154
+ * acquiring the lock; this helper asserts it as a safety net (cheap —
155
+ * an invalid entry would fail `validateStatusV2` anyway, but the explicit
156
+ * gate keeps the pre-lock fail-fast contract of `registerWorkflow`).
157
+ */
158
+ export declare function registerWorkflowEntryLocked(statusPath: string, entry: WorkflowEntry): StatusV2Doc;
139
159
  /**
140
160
  * Register one active workflow entry in the v2 root file (plan Task 3).
141
161
  * Idempotent upsert by entry `id` under the root-file `withStatusWriteLock`,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mstar-harness/engine",
3
- "version": "3.1.2",
3
+ "version": "3.2.0",
4
4
  "description": "Morning Star Harness Workflow Engine — deterministic workflow enforcement library (path, status, lease, dispatch, sdd, iteration, lint gates).",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -16,13 +16,17 @@
16
16
  "types": "./dist/index.d.ts",
17
17
  "default": "./dist/engine.js"
18
18
  },
19
- "./package.json": "./package.json"
19
+ "./package.json": "./package.json",
20
+ "./src/audit": {
21
+ "types": "./dist/audit.d.ts",
22
+ "default": "./dist/audit.js"
23
+ }
20
24
  },
21
25
  "files": [
22
26
  "dist"
23
27
  ],
24
28
  "scripts": {
25
- "build": "rm -rf dist && bun build src/index.ts --target node --outfile dist/engine.js && bunx tsc",
29
+ "build": "rm -rf dist && bun build src/index.ts --target node --outfile dist/engine.js && bun build src/audit.ts --target node --outfile dist/audit.js && bunx tsc",
26
30
  "test": "bun test",
27
31
  "prepublishOnly": "bun run build"
28
32
  },