@mstar-harness/opencode 3.1.1 → 3.1.3

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 (31) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/dist/mstar.js +244 -134
  3. package/harness-commands/iteration-drive.md +9 -90
  4. package/harness-commands/iteration-loop.md +22 -62
  5. package/harness-commands/iteration-start.md +20 -77
  6. package/harness-skills/mstar-audit/SKILL.md +2 -2
  7. package/harness-skills/mstar-compound-refresh/SKILL.md +1 -1
  8. package/harness-skills/mstar-dispatch-gates/SKILL.md +1 -1
  9. package/harness-skills/mstar-harness-core/SKILL.md +4 -3
  10. package/harness-skills/mstar-iteration/SKILL.md +16 -10
  11. package/harness-skills/mstar-iteration/references/command-shared-invariants.md +62 -0
  12. package/harness-skills/mstar-iteration/references/phase-2-worktree-lease.md +3 -3
  13. package/harness-skills/mstar-iteration/references/phase-4-5-pr-delivery.md +1 -1
  14. package/harness-skills/mstar-iteration/references/phase5-helper-discovery.md +1 -1
  15. package/harness-skills/mstar-phase-gates/SKILL.md +1 -1
  16. package/harness-skills/mstar-plan-artifacts/references/knowledge-and-designs.md +2 -2
  17. package/harness-skills/mstar-plan-artifacts/references/status-and-residuals.md +1 -1
  18. package/harness-skills/mstar-plan-conventions/SKILL.md +1 -1
  19. package/harness-skills/mstar-roles/references/_shared/leaf-executor-core.md +11 -1
  20. package/harness-skills/mstar-roles/references/architect.md +4 -7
  21. package/harness-skills/mstar-roles/references/code-reviewer.md +1 -4
  22. package/harness-skills/mstar-roles/references/frontend-dev.md +2 -4
  23. package/harness-skills/mstar-roles/references/fullstack-dev-shared.md +2 -4
  24. package/harness-skills/mstar-roles/references/ops-engineer.md +2 -6
  25. package/harness-skills/mstar-roles/references/product-manager.md +4 -7
  26. package/harness-skills/mstar-roles/references/prompt-engineer.md +2 -5
  27. package/harness-skills/mstar-roles/references/qa-engineer.md +2 -6
  28. package/harness-skills/mstar-roles/references/qc-specialist-shared.md +1 -4
  29. package/harness-skills/mstar-roles/references/writing-specialist.md +2 -5
  30. package/harness-skills/mstar-strategy/SKILL.md +5 -35
  31. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -6,6 +6,32 @@ The monorepo root [CHANGELOG.md](../../CHANGELOG.md) summarizes cross-surface re
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [3.1.3] - 2026-08-23
10
+
11
+ ### Harness
12
+
13
+ - **`mstar audit promote` (CLI)**: selected audit plans can now enter the v2 workflow lifecycle as `type: plan` — `promoteAuditPlans` writes the workflow snapshot (`{HARNESS_DIR}/workflows/<id>/snapshot.json`, one Todo `PlanRow` per selected plan, `id` + `title` + `file`) **before** registering the workflow in root `status.json`, so the snapshot-before-register invariant holds. `--plans` accepts the README Plan column id, stem, or basename; `--workflow` defaults to the `audit-<date>` dir basename; `--harness` defaults to the resolved `{HARNESS_DIR}`. Promote stays an explicit post-selection action — the audit itself never registers (advisory contract preserved).
14
+ - **Engine**: `promoteAuditPlans` exported from `@mstar-harness/engine` (titles come from the audit README index, falling back to `readPlanFileSummary`).
15
+ - **Harness skills**: `mstar-audit` Handoff step 1 now names `mstar audit promote <audit-dir> --plans <ids>` as the first-class v2 path (manual `mstar-plan-artifacts` wording kept as fallback).
16
+ - **Thinned the iteration slash commands** (`/iteration-start`, `/iteration-drive`, `/iteration-loop`) into wrappers: shared PM invariants, session todos, the continuous-execution STOP list, and the assignment-preflight bash (warn-only + `enforcement: hard` fail-fast) now live once in `skills/mstar-iteration/references/command-shared-invariants.md`, and `mstar-iteration` points at it. Frontmatter (`name` / `description` / `agent` / `input`) and each command's unique bits (start grill-me, drive helper discovery, loop do-not-load-grill-me) are unchanged.
17
+ - **Trigger-strong `mstar-iteration` description**: the skill now loads for "start an iteration" / "drive the iteration" / "run an autonomous loop" even when no `/iteration-*` slash command fires; phase labels no longer treat command names as phase names.
18
+
19
+ - Version alignment with harness **3.1.3** (no OpenCode package API change).
20
+
21
+ See root [CHANGELOG.md](../../CHANGELOG.md) **3.1.3**.
22
+
23
+ ## [3.1.2] - 2026-08-21
24
+
25
+ ### Harness
26
+
27
+ - **Anti-recursion fails closed on an empty host binding**: `dispatch.antiRecursionPrecheck` now returns a **critical** `dispatch.anti-recursion.empty-binding` violation when the host role-binding field (`omp task entry agent` / opencode `subagent` / cursor `subagent_type` / dsh `dispatchBinding`) is empty, omitted, or whitespace-only — the host cannot prove the dispatching agent is not recursing, so the dispatch must not proceed as if the NEVER red line held. `composeDispatchGate` no longer skips the precheck for empty bindings (the `if (agent !== "")` skip is removed); the precheck is the single decision point and runs for every Assignment-shaped text, including read-only (scout/explore) Assignments (no carve-out). A set binding equal to `Execute as` keeps the existing critical `dispatch.anti-recursion.self-type`; a set binding with an empty `Execute as` stays ok on the anti-recursion leg (field presence remains `validateAssignmentFields`' job).
28
+ - **OpenCode surface**: `validateDispatchAssignment` (via `composeDispatchGate`) now warns `dispatch.anti-recursion.empty-binding` at critical severity when the task tool carries no `subagent` / `subagent_type` key; under the Assignment's own `**Enforcement**: hard` the empty binding hard-blocks (`hardBlocked: true`). No `src` change — the hook already flows the default `""` binding through the engine composition.
29
+ - **dsh surface**: `dispatchGateCore` passes `config.dispatchBinding ?? ''` into the engine composition, so an unset binding now emits `dispatch.anti-recursion.empty-binding` (critical) on every Assignment-shaped dispatch — advisory in warn mode, `PreToolDecision { kind: 'deny' }` under hard. The boot-time warn string no longer claims the precheck is "skipped": an unset `dispatchBinding` under hard enforcement now fails closed until the binding is set. The Zod `dispatchBinding` schema is untouched.
30
+
31
+ - Version alignment with harness **3.1.2** (no OpenCode package API change).
32
+
33
+ See root [CHANGELOG.md](../../CHANGELOG.md) **3.1.2**.
34
+
9
35
  ## [3.1.1] - 2026-08-20
10
36
 
11
37
  ### Bundled harness skills (`harness-skills/` at publish)
package/dist/mstar.js CHANGED
@@ -63,11 +63,11 @@ __export(exports_engine, {
63
63
  releaseLease: () => releaseLease,
64
64
  registerWorkflow: () => registerWorkflow,
65
65
  referenceExists: () => referenceExists,
66
- redactSecrets: () => redactSecrets,
67
66
  readProgressLedger: () => readProgressLedger,
68
67
  readJson: () => readJson,
69
68
  readHarnessVersion: () => readHarnessVersion,
70
69
  pushCadenceProbe: () => pushCadenceProbe,
70
+ promoteAuditPlans: () => promoteAuditPlans,
71
71
  planQualityBar: () => planQualityBar,
72
72
  planExecutionLeaseLocations: () => planExecutionLeaseLocations,
73
73
  parseMstarc: () => parseMstarc,
@@ -162,10 +162,10 @@ import { basename, dirname, join, resolve } from "node:path";
162
162
  import { fileURLToPath } from "node:url";
163
163
  import { readFileSync as readFileSync2, statSync } from "node:fs";
164
164
  import { dirname as dirname2, isAbsolute, join as join2, relative, resolve as resolve2 } from "node:path";
165
- import { mkdirSync as mkdirSync2, readdirSync, readFileSync as readFileSync3, statSync as statSync2 } from "node:fs";
165
+ import { existsSync as existsSync2, mkdirSync as mkdirSync2, readdirSync, readFileSync as readFileSync3, realpathSync, statSync as statSync2 } from "node:fs";
166
166
  import { execFileSync } from "node:child_process";
167
167
  import { basename as basename2, dirname as dirname3, isAbsolute as isAbsolute2, join as join3, relative as relative2, resolve as resolve3 } from "node:path";
168
- import { existsSync as existsSync2, readFileSync as readFileSync4, readdirSync as readdirSync2, realpathSync } from "node:fs";
168
+ import { existsSync as existsSync3, readFileSync as readFileSync4, readdirSync as readdirSync2, realpathSync as realpathSync2 } from "node:fs";
169
169
  import { dirname as dirname5, join as join6, resolve as resolve5, sep } from "node:path";
170
170
  import { mkdirSync as mkdirSync3, rmdirSync, statSync as statSync3, unlinkSync as unlinkSync2, writeFileSync as writeFileSync2 } from "node:fs";
171
171
  import { dirname as dirname4, isAbsolute as isAbsolute3, join as join4, resolve as resolve4 } from "node:path";
@@ -174,22 +174,22 @@ import { AsyncLocalStorage } from "node:async_hooks";
174
174
  import { mkdirSync as mkdirSync4 } from "node:fs";
175
175
  import { join as join5 } from "node:path";
176
176
  import { execFileSync as execFileSync2 } from "node:child_process";
177
- import { existsSync as existsSync3 } from "node:fs";
177
+ import { existsSync as existsSync4 } from "node:fs";
178
178
  import { isAbsolute as isAbsolute4, resolve as resolve6 } from "node:path";
179
179
  import { execFileSync as execFileSync3 } from "node:child_process";
180
- import { mkdirSync as mkdirSync5, readdirSync as readdirSync3, readFileSync as readFileSync5, realpathSync as realpathSync2, statSync as statSync4, writeFileSync as writeFileSync3 } from "node:fs";
180
+ import { mkdirSync as mkdirSync5, readdirSync as readdirSync3, readFileSync as readFileSync5, realpathSync as realpathSync3, statSync as statSync4, writeFileSync as writeFileSync3 } from "node:fs";
181
181
  import { basename as basename3, dirname as dirname6, isAbsolute as isAbsolute5, join as join7, resolve as resolve7 } from "node:path";
182
- import { existsSync as existsSync4, readdirSync as readdirSync4, readFileSync as readFileSync6 } from "node:fs";
182
+ import { existsSync as existsSync5, readdirSync as readdirSync4, readFileSync as readFileSync6 } from "node:fs";
183
183
  import { join as join8 } from "node:path";
184
- import { existsSync as existsSync5, readFileSync as readFileSync7, readdirSync as readdirSync5 } from "node:fs";
184
+ import { existsSync as existsSync6, readFileSync as readFileSync7, readdirSync as readdirSync5 } from "node:fs";
185
185
  import { join as join9 } from "node:path";
186
186
  import { copyFileSync, mkdirSync as mkdirSync6, readFileSync as readFileSync8, readdirSync as readdirSync6, writeFileSync as writeFileSync4 } from "node:fs";
187
187
  import { dirname as dirname7, isAbsolute as isAbsolute6, join as join10, relative as relative3, resolve as resolve8, sep as sep2 } from "node:path";
188
- import { mkdirSync as mkdirSync7, readdirSync as readdirSync7, readFileSync as readFileSync9, writeFileSync as writeFileSync5 } from "node:fs";
189
- import { join as join11, resolve as resolve9 } from "node:path";
190
- import { existsSync as existsSync6, readdirSync as readdirSync8, readFileSync as readFileSync10 } from "node:fs";
191
- import { basename as basename4, isAbsolute as isAbsolute7, join as join12, relative as relative4, resolve as resolve10, sep as sep3 } from "node:path";
192
- import { existsSync as existsSync7 } from "node:fs";
188
+ import { existsSync as existsSync7, mkdirSync as mkdirSync7, readdirSync as readdirSync7, readFileSync as readFileSync9, rmdirSync as rmdirSync2, rmSync, writeFileSync as writeFileSync5 } from "node:fs";
189
+ import { basename as basename4, join as join11, resolve as resolve9, sep as sep3 } from "node:path";
190
+ import { existsSync as existsSync8, readdirSync as readdirSync8, readFileSync as readFileSync10 } from "node:fs";
191
+ import { basename as basename5, isAbsolute as isAbsolute7, join as join12, relative as relative4, resolve as resolve10, sep as sep4 } from "node:path";
192
+ import { existsSync as existsSync9 } from "node:fs";
193
193
  import { join as join13 } from "node:path";
194
194
  function applyEnforcement(gate, opts) {
195
195
  return { ...gate, hardBlocked: opts.hard && gate.violations.length > 0 };
@@ -552,6 +552,23 @@ function assertPlanWritingPath(planPath, harnessDir) {
552
552
  fix: `write the plan under ${planDir}`
553
553
  };
554
554
  }
555
+ if (existsSync2(planAbs)) {
556
+ try {
557
+ const canonicalPlan = realpathSync(planAbs);
558
+ const canonicalPlanDir = existsSync2(planDir) ? realpathSync(planDir) : resolve3(planDir);
559
+ const canonicalRel = relative2(canonicalPlanDir, canonicalPlan);
560
+ const canonicalInside = canonicalRel === "" || !canonicalRel.startsWith("..") && !isAbsolute2(canonicalRel);
561
+ if (!canonicalInside) {
562
+ return {
563
+ ok: false,
564
+ severity: "high",
565
+ code: "plan-path.symlink-escape",
566
+ message: `plan file ${planAbs} resolves to ${canonicalPlan}, outside {PLAN_DIR} (${canonicalPlanDir})`,
567
+ fix: `write the plan under ${planDir}`
568
+ };
569
+ }
570
+ } catch {}
571
+ }
555
572
  return {
556
573
  ok: true,
557
574
  severity: "low",
@@ -1008,10 +1025,8 @@ function composeDispatchGate(text, opts = {}) {
1008
1025
  const violations = [];
1009
1026
  const writable = opts.writable !== false;
1010
1027
  violations.push(...validateAssignmentFields(text, { writable }).violations);
1011
- const agent = (opts.agent ?? "").trim();
1012
- if (agent !== "") {
1013
- violations.push(...antiRecursionPrecheck(agent, parseAssignmentFields(text).executeAs ?? "").violations);
1014
- }
1028
+ const agent = opts.agent ?? "";
1029
+ violations.push(...antiRecursionPrecheck(agent, parseAssignmentFields(text).executeAs ?? "").violations);
1015
1030
  if (writable) {
1016
1031
  const forms = parseAssignmentBranchForms(text);
1017
1032
  const branch = forms.createForm?.name ?? forms.workingBranch ?? forms.directOn?.branch ?? process.env.MSTAR_WORKING_BRANCH;
@@ -1030,7 +1045,15 @@ function composeDispatchGate(text, opts = {}) {
1030
1045
  function antiRecursionPrecheck(subagentType, executeAs) {
1031
1046
  const binding = subagentType.trim().toLowerCase();
1032
1047
  const role = executeAs.trim().toLowerCase();
1033
- if (binding !== "" && binding === role) {
1048
+ if (binding === "") {
1049
+ return {
1050
+ ok: false,
1051
+ violations: [
1052
+ violation2("critical", "dispatch.anti-recursion.empty-binding", `empty host role binding — the host cannot report which agent is calling, so anti-recursion cannot be proven (a dispatch could silently recurse)`, "set the host role-binding field (omp task entry `agent` / opencode `subagent` / cursor `subagent_type` / dsh `dispatchBinding`) before dispatching")
1053
+ ]
1054
+ };
1055
+ }
1056
+ if (binding === role) {
1034
1057
  return {
1035
1058
  ok: false,
1036
1059
  violations: [
@@ -1378,7 +1401,7 @@ function validateStatusV2(docOrPath, opts = {}) {
1378
1401
  if (harnessDir !== undefined && Array.isArray(doc.workflows)) {
1379
1402
  let realHarnessDir = null;
1380
1403
  try {
1381
- realHarnessDir = realpathSync(harnessDir);
1404
+ realHarnessDir = realpathSync2(harnessDir);
1382
1405
  } catch {}
1383
1406
  for (const entry of doc.workflows) {
1384
1407
  if (!isPlainObject3(entry) || typeof entry.dir !== "string")
@@ -1388,7 +1411,7 @@ function validateStatusV2(docOrPath, opts = {}) {
1388
1411
  const label = typeof entry.id === "string" ? entry.id : relSnapshot;
1389
1412
  let physical;
1390
1413
  try {
1391
- physical = realpathSync(snapshotPath);
1414
+ physical = realpathSync2(snapshotPath);
1392
1415
  } catch {
1393
1416
  violations.push(violation4("high", "status.workflow.snapshot-missing", `workflows[] lists ${JSON.stringify(label)} but its snapshot does not exist at ${JSON.stringify(relSnapshot)} — the root holds active lifecycles only; unregister the id when its snapshot is removed`));
1394
1417
  continue;
@@ -1411,40 +1434,41 @@ function validateStatusV2(docOrPath, opts = {}) {
1411
1434
  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`));
1412
1435
  }
1413
1436
  if (typeof entry.started_at === "string" && typeof snapshot.started_at === "string" && entry.started_at !== snapshot.started_at) {
1414
- 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`));
1437
+ 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`));
1415
1438
  }
1416
1439
  }
1417
1440
  }
1418
1441
  return { ok: violations.length === 0, violations };
1419
1442
  }
1443
+ function registerWorkflowEntryLocked(statusPath, entry) {
1444
+ const harnessDir = dirname5(statusPath);
1445
+ const current = readJson(statusPath);
1446
+ const fresh = Object.keys(current).length === 0;
1447
+ const doc = fresh ? { version: 2, updated_at: todayString(), workflows: [] } : current;
1448
+ if (!fresh && !Array.isArray(doc.workflows)) {
1449
+ throw new Error("refusing to modify status.json: workflows must be an array — a v1 root must be migrated first (run `mstar migrate`)");
1450
+ }
1451
+ const existing = doc.workflows.findIndex((wf) => wf.id === entry.id);
1452
+ if (existing >= 0) {
1453
+ doc.workflows[existing] = entry;
1454
+ } else {
1455
+ doc.workflows.push(entry);
1456
+ }
1457
+ doc.updated_at = todayString();
1458
+ const gate = validateStatusV2(doc, { harnessDir });
1459
+ if (!gate.ok) {
1460
+ throw new Error(`refusing to write invalid status.json: ${gate.violations.map((v) => v.message).join("; ")}`);
1461
+ }
1462
+ writeJson(statusPath, doc);
1463
+ return doc;
1464
+ }
1420
1465
  async function registerWorkflow(root, entry) {
1421
1466
  const entryGate = validateWorkflowEntry(entry);
1422
1467
  if (!entryGate.ok) {
1423
1468
  throw new Error(`refusing to register invalid workflow entry: ${entryGate.violations.map((v) => v.message).join("; ")}`);
1424
1469
  }
1425
1470
  const statusPath = resolve5(root);
1426
- const harnessDir = dirname5(statusPath);
1427
- return withStatusWriteLock(statusPath, () => {
1428
- const current = readJson(statusPath);
1429
- const fresh = Object.keys(current).length === 0;
1430
- const doc = fresh ? { version: 2, updated_at: todayString(), workflows: [] } : current;
1431
- if (!fresh && !Array.isArray(doc.workflows)) {
1432
- throw new Error("refusing to modify status.json: workflows must be an array — a v1 root must be migrated first (run `mstar migrate`)");
1433
- }
1434
- const existing = doc.workflows.findIndex((wf) => wf.id === entry.id);
1435
- if (existing >= 0) {
1436
- doc.workflows[existing] = entry;
1437
- } else {
1438
- doc.workflows.push(entry);
1439
- }
1440
- doc.updated_at = todayString();
1441
- const gate = validateStatusV2(doc, { harnessDir });
1442
- if (!gate.ok) {
1443
- throw new Error(`refusing to write invalid status.json: ${gate.violations.map((v) => v.message).join("; ")}`);
1444
- }
1445
- writeJson(statusPath, doc);
1446
- return doc;
1447
- });
1471
+ return withStatusWriteLock(statusPath, () => registerWorkflowEntryLocked(statusPath, entry));
1448
1472
  }
1449
1473
  async function unregisterWorkflow(root, id) {
1450
1474
  if (typeof id !== "string" || id.trim() === "") {
@@ -1477,7 +1501,7 @@ async function unregisterWorkflow(root, id) {
1477
1501
  }
1478
1502
  function resolveCompassEnforcement(harnessDir) {
1479
1503
  const iterationsDir = resolveIterationDir(harnessDir);
1480
- if (!existsSync2(iterationsDir))
1504
+ if (!existsSync3(iterationsDir))
1481
1505
  return { hard: false, source: "none" };
1482
1506
  let entries;
1483
1507
  try {
@@ -1489,7 +1513,7 @@ function resolveCompassEnforcement(harnessDir) {
1489
1513
  if (!entry.isDirectory())
1490
1514
  continue;
1491
1515
  const compassPath = join6(iterationsDir, entry.name, "delivery-compass.md");
1492
- if (!existsSync2(compassPath))
1516
+ if (!existsSync3(compassPath))
1493
1517
  continue;
1494
1518
  let content;
1495
1519
  try {
@@ -1575,7 +1599,7 @@ function l1PreDispatchCheck(input, opts = {}) {
1575
1599
  if (controlWorktreePath !== "" && leaseWorktreePath !== "" && resolve6(controlWorktreePath) === resolve6(leaseWorktreePath)) {
1576
1600
  violations.push(violation5("critical", "worktree.l1.lease-equals-control", `execution_lease.worktree_path "${leaseWorktreePath}" equals metadata.control_worktree_path — the feature worktree MUST differ from the control worktree (L1 isolation; product edits never land in the control checkout)`, "use a distinct feature worktree for the plan (git worktree add <path> <branch>) and update the lease"));
1577
1601
  }
1578
- if (leaseWorktreePath !== "" && !existsSync3(leaseWorktreePath)) {
1602
+ if (leaseWorktreePath !== "" && !existsSync4(leaseWorktreePath)) {
1579
1603
  violations.push(violation5("high", "worktree.l1.feature-missing", `feature worktree directory "${leaseWorktreePath}" does not exist for plan "${planId}"`, `create it before dispatch: git worktree add ${leaseWorktreePath} <working-branch>`));
1580
1604
  } else if (leaseWorktreePath !== "" && leaseWorkingBranch !== "") {
1581
1605
  const probe = probeBranch(leaseWorktreePath, opts);
@@ -1609,7 +1633,7 @@ function l2PreDispatchCheck(input, opts = {}) {
1609
1633
  return;
1610
1634
  }
1611
1635
  seenPaths.add(normalized);
1612
- if (!existsSync3(track.worktreePath)) {
1636
+ if (!existsSync4(track.worktreePath)) {
1613
1637
  violations.push(violation5("high", "worktree.l2.track-missing", `track worktree directory "${track.worktreePath}" does not exist`, `create it before dispatch: git worktree add ${track.worktreePath} ${track.workingBranch}`));
1614
1638
  return;
1615
1639
  }
@@ -1731,8 +1755,8 @@ function isLinkedWorktree(root) {
1731
1755
  if (gitDir.includes("/.git/worktrees/") || gitDir.includes("/worktrees/"))
1732
1756
  return true;
1733
1757
  try {
1734
- const gdParent = realpathSync2(dirname6(gitDir));
1735
- const cmAbs = realpathSync2(common);
1758
+ const gdParent = realpathSync3(dirname6(gitDir));
1759
+ const cmAbs = realpathSync3(common);
1736
1760
  return join7(gdParent, basename3(gitDir)) !== cmAbs && gitDir !== cmAbs;
1737
1761
  } catch {
1738
1762
  return false;
@@ -1750,10 +1774,10 @@ function sddWorkspace(planId, opts = {}) {
1750
1774
  if (!isDirectory2(controlRoot)) {
1751
1775
  throw new SddScriptError(`mstar sdd workspace: CONTROL_ROOT / MSTAR_CONTROL_ROOT is not a directory: ${controlRoot}`, 1);
1752
1776
  }
1753
- root = realpathSync2(controlRoot);
1777
+ root = realpathSync3(controlRoot);
1754
1778
  } else {
1755
1779
  const topLevel = gitOut(cwd, ["rev-parse", "--show-toplevel"]);
1756
- root = realpathSync2(topLevel ?? cwd);
1780
+ root = realpathSync3(topLevel ?? cwd);
1757
1781
  }
1758
1782
  if (!controlRoot && isLinkedWorktree(root)) {
1759
1783
  throw new SddScriptError(`mstar sdd workspace: linked worktree at ${root} has no {HARNESS_DIR}/status.json (default gitignore).
@@ -1787,7 +1811,7 @@ function sddWorkspace(planId, opts = {}) {
1787
1811
  mkdirSync5(sddDir, { recursive: true });
1788
1812
  writeFileSync3(join7(sddDir, ".gitignore"), `*
1789
1813
  `);
1790
- return realpathSync2(sddDir);
1814
+ return realpathSync3(sddDir);
1791
1815
  }
1792
1816
  function taskBrief(planFile, taskN, outFile, opts = {}) {
1793
1817
  if (!planFile || !Number.isInteger(taskN) || taskN < 1) {
@@ -2159,7 +2183,7 @@ function pushCadenceProbe(ciRunning, reviewWaveActive) {
2159
2183
  return { ok: violations.length === 0, violations };
2160
2184
  }
2161
2185
  function assertIndexRowObligations(iterationsDir) {
2162
- if (!existsSync4(iterationsDir)) {
2186
+ if (!existsSync5(iterationsDir)) {
2163
2187
  return {
2164
2188
  ok: false,
2165
2189
  violations: [
@@ -2167,9 +2191,9 @@ function assertIndexRowObligations(iterationsDir) {
2167
2191
  ]
2168
2192
  };
2169
2193
  }
2170
- const iterationIds = readdirSync4(iterationsDir, { withFileTypes: true }).filter((entry) => entry.isDirectory()).filter((entry) => existsSync4(join8(iterationsDir, entry.name, COMPASS_FILE))).map((entry) => entry.name).sort();
2194
+ const iterationIds = readdirSync4(iterationsDir, { withFileTypes: true }).filter((entry) => entry.isDirectory()).filter((entry) => existsSync5(join8(iterationsDir, entry.name, COMPASS_FILE))).map((entry) => entry.name).sort();
2171
2195
  const readmePath = join8(iterationsDir, INDEX_README);
2172
- if (!existsSync4(readmePath)) {
2196
+ if (!existsSync5(readmePath)) {
2173
2197
  return {
2174
2198
  ok: false,
2175
2199
  violations: [
@@ -2434,7 +2458,7 @@ function techDebtRollup(projectDir) {
2434
2458
  if (!project.isDirectory())
2435
2459
  continue;
2436
2460
  const registerPath = join9(projectDir, project.name, PROJECT_REGISTER_FILE);
2437
- if (!existsSync5(registerPath))
2461
+ if (!existsSync6(registerPath))
2438
2462
  continue;
2439
2463
  let register;
2440
2464
  try {
@@ -2989,9 +3013,11 @@ async function applyMigratePlan(plan) {
2989
3013
  if (!gate2.ok) {
2990
3014
  throw new Error(`refusing to apply migration: invalid project register: ${gate2.violations.map((v) => v.message).join("; ")}`);
2991
3015
  }
2992
- const filePath = projectTargetOf(plan.register.file);
2993
- mkdirSync6(dirname7(filePath), { recursive: true });
2994
- writeJson(filePath, plan.register.data);
3016
+ if (Object.keys(plan.register.data.entries ?? {}).length > 0) {
3017
+ const filePath = projectTargetOf(plan.register.file);
3018
+ mkdirSync6(dirname7(filePath), { recursive: true });
3019
+ writeJson(filePath, plan.register.data);
3020
+ }
2995
3021
  }
2996
3022
  if (plan.roadmap !== null) {
2997
3023
  const filePath = projectTargetOf(plan.roadmap.file);
@@ -3448,60 +3474,6 @@ function validateAuditStatusBlocks(planText) {
3448
3474
  });
3449
3475
  return { ok: violations.length === 0, violations };
3450
3476
  }
3451
- function buildLineStarts(text) {
3452
- const starts = [0];
3453
- for (let i = 0;i < text.length; i++) {
3454
- if (text[i] === `
3455
- `)
3456
- starts.push(i + 1);
3457
- }
3458
- return starts;
3459
- }
3460
- function lineAt(starts, index) {
3461
- let lo = 0;
3462
- let hi = starts.length - 1;
3463
- while (lo < hi) {
3464
- const mid = lo + hi + 1 >> 1;
3465
- if (starts[mid] <= index)
3466
- lo = mid;
3467
- else
3468
- hi = mid - 1;
3469
- }
3470
- return lo + 1;
3471
- }
3472
- function redactSecrets(text, filePath) {
3473
- const starts = buildLineStarts(text);
3474
- const marker = (type, index) => `[REDACTED ${type}@${lineAt(starts, index)}${filePath === undefined ? "" : ` in ${filePath}`}]`;
3475
- const replacements = [];
3476
- const findings = [];
3477
- for (const pattern of WHOLE_MATCH_PATTERNS) {
3478
- for (const match of text.matchAll(pattern.re)) {
3479
- if (match.index === undefined)
3480
- continue;
3481
- replacements.push({ index: match.index, length: match[0].length, text: marker(pattern.type, match.index) });
3482
- findings.push({ line: lineAt(starts, match.index), type: pattern.type });
3483
- }
3484
- }
3485
- for (const pattern of VALUE_PATTERNS) {
3486
- for (const match of text.matchAll(pattern.re)) {
3487
- if (match.index === undefined)
3488
- continue;
3489
- const type = pattern.typeOf(match[2]);
3490
- const replacement = `${match[1]}${match[2]}${match[3]}${match[4]}${marker(type, match.index)}`;
3491
- replacements.push({ index: match.index, length: match[0].length, text: replacement });
3492
- findings.push({ line: lineAt(starts, match.index), type });
3493
- }
3494
- }
3495
- replacements.sort((a, b) => b.index - a.index);
3496
- let out = text;
3497
- for (const r of replacements)
3498
- out = out.slice(0, r.index) + r.text + out.slice(r.index + r.length);
3499
- const deduped = new Map;
3500
- for (const f of findings)
3501
- deduped.set(`${f.line}:${f.type}`, f);
3502
- const sorted = [...deduped.values()].sort((a, b) => a.line - b.line || a.type.localeCompare(b.type));
3503
- return { text: out, findings: sorted };
3504
- }
3505
3477
  function slugify(title) {
3506
3478
  return title.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
3507
3479
  }
@@ -3636,6 +3608,137 @@ function scaffoldAuditPlan(outDir, findings, options = {}) {
3636
3608
  }));
3637
3609
  return { outDir: resolve9(outDir), date, files: written, nextNumber: next };
3638
3610
  }
3611
+ async function promoteAuditPlans(outDir, selected, options) {
3612
+ if (selected.length === 0) {
3613
+ throw new Error("promoteAuditPlans: at least one plan id must be selected (--plans 001,002,…)");
3614
+ }
3615
+ if (typeof options.harnessDir !== "string" || options.harnessDir.trim() === "") {
3616
+ throw new Error("promoteAuditPlans: options.harnessDir is required (must contain status.json + workflows/)");
3617
+ }
3618
+ const workflowId = options.workflowId ?? basename4(resolve9(outDir));
3619
+ assertSafePathComponent(workflowId, "workflow id");
3620
+ const harnessDir = resolve9(options.harnessDir);
3621
+ const statusPath = join11(harnessDir, "status.json");
3622
+ const workflowDir = join11(harnessDir, "workflows", workflowId);
3623
+ const snapshotPath = join11(workflowDir, WORKFLOW_SNAPSHOT_FILE);
3624
+ const planFiles = resolveSelectedPlanFiles(outDir, selected);
3625
+ const indexRows = readExecutionOrderIndex(outDir);
3626
+ const plans = planFiles.map((planFile) => {
3627
+ const stem = planFile.replace(/\.md$/, "");
3628
+ const num = stem.slice(0, 3);
3629
+ const indexRow = indexRows.get(num);
3630
+ const title = indexRow?.title ?? readPlanFileSummary(join11(outDir, planFile)).title;
3631
+ return {
3632
+ id: stem,
3633
+ title,
3634
+ file: planFileRel(outDir, planFile),
3635
+ status: "Todo"
3636
+ };
3637
+ });
3638
+ const now = new Date;
3639
+ const snapshot = {
3640
+ schema_version: 1,
3641
+ id: workflowId,
3642
+ type: "plan",
3643
+ status: "running",
3644
+ started_at: now.toISOString(),
3645
+ updated_at: now.toISOString().slice(0, 10),
3646
+ plans
3647
+ };
3648
+ const entry = {
3649
+ id: workflowId,
3650
+ type: "plan",
3651
+ started_at: snapshot.started_at,
3652
+ dir: `workflows/${workflowId}`
3653
+ };
3654
+ const entryGate = validateWorkflowEntry(entry);
3655
+ if (!entryGate.ok) {
3656
+ throw new Error(`refusing to register invalid workflow entry: ${entryGate.violations.map((v) => v.message).join("; ")}`);
3657
+ }
3658
+ await withStatusWriteLock(statusPath, () => {
3659
+ if (existsSync7(snapshotPath)) {
3660
+ 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`);
3661
+ }
3662
+ mkdirSync7(workflowDir, { recursive: true });
3663
+ try {
3664
+ writeJson(snapshotPath, snapshot);
3665
+ registerWorkflowEntryLocked(statusPath, entry);
3666
+ } catch (error) {
3667
+ rmSync(snapshotPath, { force: true });
3668
+ try {
3669
+ if (readdirSync7(workflowDir).length === 0) {
3670
+ rmdirSync2(workflowDir);
3671
+ }
3672
+ } catch {}
3673
+ throw error;
3674
+ }
3675
+ return { workflowId, snapshotPath };
3676
+ });
3677
+ return { workflowId, snapshotPath };
3678
+ }
3679
+ function resolveSelectedPlanFiles(outDir, selected) {
3680
+ const files = readdirSync7(outDir).filter((f) => /^\d{3}-.*\.md$/.test(f)).sort();
3681
+ const byNum = new Map;
3682
+ const byStem = new Map;
3683
+ for (const file of files) {
3684
+ const stem = file.replace(/\.md$/, "");
3685
+ if (!byNum.has(stem.slice(0, 3))) {
3686
+ byNum.set(stem.slice(0, 3), file);
3687
+ }
3688
+ byStem.set(stem, file);
3689
+ }
3690
+ const resolved = [];
3691
+ const seen = new Set;
3692
+ for (const id of selected) {
3693
+ const file = byNum.get(id) ?? byStem.get(id) ?? byStem.get(id.replace(/\.md$/, ""));
3694
+ if (file === undefined) {
3695
+ throw new Error(`promoteAuditPlans: selected plan ${JSON.stringify(id)} does not match any NNN-*.md file in ${resolve9(outDir)}`);
3696
+ }
3697
+ if (!seen.has(file)) {
3698
+ seen.add(file);
3699
+ resolved.push(file);
3700
+ }
3701
+ }
3702
+ return resolved;
3703
+ }
3704
+ function readExecutionOrderIndex(outDir) {
3705
+ const readmePath = join11(outDir, "README.md");
3706
+ let text;
3707
+ try {
3708
+ text = readFileSync9(readmePath, "utf8");
3709
+ } catch {
3710
+ return new Map;
3711
+ }
3712
+ const rows = new Map;
3713
+ const lines = text.split(`
3714
+ `);
3715
+ let inSection = false;
3716
+ for (const line of lines) {
3717
+ if (/^##\s+Execution order & status/.test(line)) {
3718
+ inSection = true;
3719
+ continue;
3720
+ }
3721
+ if (inSection && /^#/.test(line)) {
3722
+ break;
3723
+ }
3724
+ if (!inSection)
3725
+ continue;
3726
+ const cells = line.split(/(?<!\\)\|/).map((c) => c.trim());
3727
+ if (cells.length >= 3 && /^\d{3}$/.test(cells[1])) {
3728
+ rows.set(cells[1], { title: cells[2].replace(/\\\|/g, "|") });
3729
+ }
3730
+ }
3731
+ return rows;
3732
+ }
3733
+ function planFileRel(outDir, planFile) {
3734
+ const resolved = resolve9(outDir);
3735
+ const parts = resolved.split(sep3);
3736
+ const plansIdx = parts.lastIndexOf("plans");
3737
+ if (plansIdx >= 0) {
3738
+ return `${parts.slice(plansIdx + 1).join(sep3)}${sep3}${planFile}`;
3739
+ }
3740
+ return planFile;
3741
+ }
3639
3742
  function violation10(severity, code, message, fix) {
3640
3743
  return { ok: false, severity, code, message, fix };
3641
3744
  }
@@ -3852,7 +3955,7 @@ function referenceExists(repoRoot, docText) {
3852
3955
  for (const { ref, isSymbol, module } of refs) {
3853
3956
  if (!isSymbol || module === undefined) {
3854
3957
  const candidate = ref.replace(LINE_SUFFIX_RE, "").replace(ANCHOR_RE, "");
3855
- if (existsSync6(resolve10(repoRoot, candidate))) {
3958
+ if (existsSync8(resolve10(repoRoot, candidate))) {
3856
3959
  checked++;
3857
3960
  } else {
3858
3961
  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"));
@@ -3883,7 +3986,7 @@ function collectKnowledgeDocs(dir) {
3883
3986
  if (entry.isDirectory()) {
3884
3987
  stack.push(full);
3885
3988
  } else if (entry.name.endsWith(".md") && entry.name !== "README.md" && entry.name !== "index.md") {
3886
- docs.push(relative4(dir, full).split(sep3).join("/"));
3989
+ docs.push(relative4(dir, full).split(sep4).join("/"));
3887
3990
  }
3888
3991
  }
3889
3992
  }
@@ -3900,7 +4003,7 @@ function normalizeIndexRef(cell) {
3900
4003
  function assertIndexRows(knowledgeDir) {
3901
4004
  const violations = [];
3902
4005
  const readmePath = join12(knowledgeDir, "README.md");
3903
- if (!existsSync6(readmePath)) {
4006
+ if (!existsSync8(readmePath)) {
3904
4007
  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"));
3905
4008
  return { ok: false, violations };
3906
4009
  }
@@ -3932,7 +4035,7 @@ function compoundRefreshScope(harnessDir, projectRoot) {
3932
4035
  ];
3933
4036
  }
3934
4037
  function isFileLikeRoot(root) {
3935
- return /^[^.]*\.[A-Za-z0-9]{1,10}$/.test(basename4(root));
4038
+ return /^[^.]*\.[A-Za-z0-9]{1,10}$/.test(basename5(root));
3936
4039
  }
3937
4040
  function scopeGuard(path, allowedRoots) {
3938
4041
  const resolved = resolve10(path);
@@ -3941,7 +4044,7 @@ function scopeGuard(path, allowedRoots) {
3941
4044
  if (isFileLikeRoot(r)) {
3942
4045
  if (resolved === r)
3943
4046
  return { ok: true, violations: [] };
3944
- } else if (resolved === r || resolved.startsWith(r + sep3)) {
4047
+ } else if (resolved === r || resolved.startsWith(r + sep4)) {
3945
4048
  return { ok: true, violations: [] };
3946
4049
  }
3947
4050
  }
@@ -4163,7 +4266,7 @@ function validateRoleMapping(rolesDir, options = {}) {
4163
4266
  const violations = [];
4164
4267
  const referenceById = new Map(mapping.map((m) => [m.agentId, m.reference]));
4165
4268
  for (const { agentId, reference } of mapping) {
4166
- if (!existsSync7(join13(rolesDir, reference))) {
4269
+ if (!existsSync9(join13(rolesDir, reference))) {
4167
4270
  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`));
4168
4271
  }
4169
4272
  }
@@ -4359,7 +4462,7 @@ var SEVERITY_ORDER, MSTARC_FILE = ".mstarc", MSTARC_SECTION = "config", MSTARC_H
4359
4462
  !.agents/knowledge/**
4360
4463
  !.agents/specs/
4361
4464
  !.agents/specs/**
4362
- `, GITIGNORE_PROCESS_ENTRIES, GITIGNORE_PROCESS_ENTRIES_AGENTS, DATE_PART, RFC3339_Z_RE, DATE_ONLY_RE, STATUS_WRITE_LOCKDIR = ".status-write.lockdir", LOCKDIR_HOLDER_PID = "holder.pid", heldLockDirs, BRANCH_FORMS_HINT = '"Working branch: <existing>" | "Working branch: create <new> from <base>" | "Branch policy: direct on <branch> — <reason>"', REQUIRED_FIELDS, ASSIGNMENT_ENFORCEMENT_BOLD_RE, ASSIGNMENT_ENFORCEMENT_PLAIN_RE, COMPASS_ENFORCEMENT_RE, ASSIGNMENT_BODY_START_RE, ASSIGNMENT_HEADING_RE, ASSIGNMENT_FIELD_RE, WORKFLOW_SNAPSHOT_FILE = "snapshot.json", WORKFLOW_LIFECYCLE_STATUSES, WORKFLOW_TERMINAL_STATUSES, WORKFLOW_LIFECYCLE_TYPES, DATE_RE, PLAN_STATUSES, RESIDUAL_DECISIONS, RESIDUAL_LIFECYCLES, validateStatus, DEFAULT_PROBE_TIMEOUT_MS = 1e4, QC_ALIGNMENT_FIELDS, SddScriptError, GIT_CAPTURE_MAX_BYTES, COMPASS_STATUSES, DATE_RE2, PLAN_STATUS_DONE = "Done", COMPASS_FILE = "delivery-compass.md", INDEX_README = "README.md", INDEX_HEADER = "| Iteration | Path | Description | Status |", PROJECT_ROADMAP_FILE = "roadmap.md", PROJECT_REFERENCES_DIR = "references", PROJECT_REGISTER_FILE = "residuals.json", _DEFAULT_PROJECT = "_default", ROADMAP_STATUSES, DATE_RE3, ROLLUP_FIELDS, MIGRATE_STATUS_FILE = "status.json", ARCHIVED_STATUS_V1_FILE = "archived/status.v1.json", NOTES_LEDGER_FILE = "notes.jsonl", DATE_RE4, ROOT_METADATA_LIFT_KEYS, RAW_GROUP = "__raw", isMap = (v) => typeof v === "object" && v !== null && !Array.isArray(v), HEX_RE, OKLCH_RE, PX_RE, PLACEHOLDER_RE, TYPOGRAPHY_PROPS, REF_RE, REF_GROUPS, isPlaceholder = (value) => PLACEHOLDER_RE.test(value), PARITY_GROUPS, GRAY_STEPS, ALPHA_STEPS, ACCENT_SCALES, L3_BODY_ITEM_IDS, LEVEL_RANK, AUDIT_PRIORITIES, AUDIT_EFFORTS, AUDIT_RISKS, AUDIT_CATEGORIES, AUDIT_STATUS_FIELDS, WHOLE_MATCH_PATTERNS, VALUE_PATTERNS, escapeCell = (value) => value.replace(/\|/g, "\\|"), truncate = (value, max) => value.length > max ? `${value.slice(0, max)}…` : value, KNOWLEDGE_REQUIRED_FIELDS, KNOWLEDGE_PROBLEM_TYPES, KNOWLEDGE_BUG_PROBLEM_TYPES, KNOWLEDGE_KNOWLEDGE_PROBLEM_TYPES, KNOWLEDGE_SEVERITIES, KNOWLEDGE_RESOLUTION_TYPES, KNOWLEDGE_CATEGORY_MAP, DATE_RE5, isMap2 = (v) => typeof v === "object" && v !== null && !Array.isArray(v), REF_EXT_RE, SYMBOL_REF_RE, SCHEME_RE, LINE_SUFFIX_RE, ANCHOR_RE, WALK_SKIP_DIRS, MAX_WALK_FILES = 5000, COMMENT_INTRODUCER = "(?:\\/\\/|\\/\\*|#|;|--|\\s\\*)", REMOVAL_PATH_PATTERNS, TASK_ARTIFACT_RE, SDD_DEEPLINK_RE, TEST_FILE_PATH_RE, TEST_FILE_PHRASE_RE, COMMAND_PROMPT_RE, RUNNER_RE, OUTPUT_TOKEN_RE, PLACEHOLDER_TOKEN_RE, ELLIPSIS_RE, NEGATION_RE, WORKFLOW_VERB_START_RE, PRONOUN_RE, DESCRIPTION_MAX_WORDS = 120, REQUIRED_STRATEGY_SECTIONS, ROLE_MAPPING, SHARED_FAMILIES, DEV_TRACK_PARAMS, QC_REVIEWER_PARAMS, LOAD_ORDER_HEADING_RE, FIVE_QUESTION_SECTIONS, HEADING_RE, RUNTIME_HEADING_ALIASES;
4465
+ `, GITIGNORE_PROCESS_ENTRIES, GITIGNORE_PROCESS_ENTRIES_AGENTS, DATE_PART, RFC3339_Z_RE, DATE_ONLY_RE, STATUS_WRITE_LOCKDIR = ".status-write.lockdir", LOCKDIR_HOLDER_PID = "holder.pid", heldLockDirs, BRANCH_FORMS_HINT = '"Working branch: <existing>" | "Working branch: create <new> from <base>" | "Branch policy: direct on <branch> — <reason>"', REQUIRED_FIELDS, ASSIGNMENT_ENFORCEMENT_BOLD_RE, ASSIGNMENT_ENFORCEMENT_PLAIN_RE, COMPASS_ENFORCEMENT_RE, ASSIGNMENT_BODY_START_RE, ASSIGNMENT_HEADING_RE, ASSIGNMENT_FIELD_RE, WORKFLOW_SNAPSHOT_FILE = "snapshot.json", WORKFLOW_LIFECYCLE_STATUSES, WORKFLOW_TERMINAL_STATUSES, WORKFLOW_LIFECYCLE_TYPES, DATE_RE, PLAN_STATUSES, RESIDUAL_DECISIONS, RESIDUAL_LIFECYCLES, validateStatus, DEFAULT_PROBE_TIMEOUT_MS = 1e4, QC_ALIGNMENT_FIELDS, SddScriptError, GIT_CAPTURE_MAX_BYTES, COMPASS_STATUSES, DATE_RE2, PLAN_STATUS_DONE = "Done", COMPASS_FILE = "delivery-compass.md", INDEX_README = "README.md", INDEX_HEADER = "| Iteration | Path | Description | Status |", PROJECT_ROADMAP_FILE = "roadmap.md", PROJECT_REFERENCES_DIR = "references", PROJECT_REGISTER_FILE = "residuals.json", _DEFAULT_PROJECT = "_default", ROADMAP_STATUSES, DATE_RE3, ROLLUP_FIELDS, MIGRATE_STATUS_FILE = "status.json", ARCHIVED_STATUS_V1_FILE = "archived/status.v1.json", NOTES_LEDGER_FILE = "notes.jsonl", DATE_RE4, ROOT_METADATA_LIFT_KEYS, RAW_GROUP = "__raw", isMap = (v) => typeof v === "object" && v !== null && !Array.isArray(v), HEX_RE, OKLCH_RE, PX_RE, PLACEHOLDER_RE, TYPOGRAPHY_PROPS, REF_RE, REF_GROUPS, isPlaceholder = (value) => PLACEHOLDER_RE.test(value), PARITY_GROUPS, GRAY_STEPS, ALPHA_STEPS, ACCENT_SCALES, L3_BODY_ITEM_IDS, LEVEL_RANK, AUDIT_PRIORITIES, AUDIT_EFFORTS, AUDIT_RISKS, AUDIT_CATEGORIES, AUDIT_STATUS_FIELDS, escapeCell = (value) => value.replace(/\|/g, "\\|"), truncate = (value, max) => value.length > max ? `${value.slice(0, max)}…` : value, KNOWLEDGE_REQUIRED_FIELDS, KNOWLEDGE_PROBLEM_TYPES, KNOWLEDGE_BUG_PROBLEM_TYPES, KNOWLEDGE_KNOWLEDGE_PROBLEM_TYPES, KNOWLEDGE_SEVERITIES, KNOWLEDGE_RESOLUTION_TYPES, KNOWLEDGE_CATEGORY_MAP, DATE_RE5, isMap2 = (v) => typeof v === "object" && v !== null && !Array.isArray(v), REF_EXT_RE, SYMBOL_REF_RE, SCHEME_RE, LINE_SUFFIX_RE, ANCHOR_RE, WALK_SKIP_DIRS, MAX_WALK_FILES = 5000, COMMENT_INTRODUCER = "(?:\\/\\/|\\/\\*|#|;|--|\\s\\*)", REMOVAL_PATH_PATTERNS, TASK_ARTIFACT_RE, SDD_DEEPLINK_RE, TEST_FILE_PATH_RE, TEST_FILE_PHRASE_RE, COMMAND_PROMPT_RE, RUNNER_RE, OUTPUT_TOKEN_RE, PLACEHOLDER_TOKEN_RE, ELLIPSIS_RE, NEGATION_RE, WORKFLOW_VERB_START_RE, PRONOUN_RE, DESCRIPTION_MAX_WORDS = 120, REQUIRED_STRATEGY_SECTIONS, ROLE_MAPPING, SHARED_FAMILIES, DEV_TRACK_PARAMS, QC_REVIEWER_PARAMS, LOAD_ORDER_HEADING_RE, FIVE_QUESTION_SECTIONS, HEADING_RE, RUNTIME_HEADING_ALIASES;
4363
4466
  var init_engine = __esm(() => {
4364
4467
  SEVERITY_ORDER = ["critical", "high", "medium", "low", "nit"];
4365
4468
  CONFIG_KEYS = {
@@ -4474,20 +4577,6 @@ var init_engine = __esm(() => {
4474
4577
  "direction"
4475
4578
  ];
4476
4579
  AUDIT_STATUS_FIELDS = ["Priority", "Effort", "Risk", "Depends on", "Category", "Planned at"];
4477
- WHOLE_MATCH_PATTERNS = [
4478
- { type: "private-key", re: /-----BEGIN [A-Z0-9 ]*PRIVATE KEY-----/g },
4479
- { type: "aws-access-key", re: /\b(?:AKIA|ASIA)[0-9A-Z]{16}\b/g },
4480
- { type: "github-token", re: /\bgh[pousr]_[A-Za-z0-9]{36,}\b/g },
4481
- { type: "slack-token", re: /\bxox[baprs]-[A-Za-z0-9-]{10,}\b/g },
4482
- { type: "jwt", re: /\beyJ[A-Za-z0-9_-]{10,1024}\.[A-Za-z0-9_-]{10,1024}\.[A-Za-z0-9_-]{10,1024}\b/g },
4483
- { type: "api-secret-key", re: /\bsk-[A-Za-z0-9-]{20,}\b/g }
4484
- ];
4485
- VALUE_PATTERNS = [
4486
- {
4487
- typeOf: (key) => key.replace(/([a-z0-9])([A-Z])/g, "$1-$2").toLowerCase().replace(/[_-]+/g, "-"),
4488
- 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
4489
- }
4490
- ];
4491
4580
  KNOWLEDGE_REQUIRED_FIELDS = ["module", "date", "problem_type", "category", "severity"];
4492
4581
  KNOWLEDGE_PROBLEM_TYPES = [
4493
4582
  "build_error",
@@ -5120,7 +5209,28 @@ var MorningStarHarnessPlugin = async () => {
5120
5209
  defaultStatusLogger("error", "hard-gate blocked (hardBlocked=true) — refusal requires a host refusal channel");
5121
5210
  }
5122
5211
  } else if (input.tool === "edit") {
5123
- const gate2 = await validateStatusWrite(filePath);
5212
+ classifyDirResolvers = await dirResolversLoader.load();
5213
+ if (harnessDocKindOfTarget(filePath) === null)
5214
+ return;
5215
+ const oldString = args.oldString;
5216
+ const newString = args.newString;
5217
+ const replaceAll = args.replaceAll;
5218
+ let patchedDoc;
5219
+ if (typeof oldString === "string" && oldString.length > 0 && typeof newString === "string" && (replaceAll === undefined || typeof replaceAll === "boolean")) {
5220
+ try {
5221
+ const text = fs.readFileSync(filePath, "utf8");
5222
+ const hits = text.split(oldString).length - 1;
5223
+ if (replaceAll === true || hits === 1) {
5224
+ const patched = text.split(oldString).join(newString);
5225
+ try {
5226
+ patchedDoc = JSON.parse(patched);
5227
+ } catch {
5228
+ patchedDoc = null;
5229
+ }
5230
+ }
5231
+ } catch {}
5232
+ }
5233
+ const gate2 = patchedDoc !== undefined ? await validateStatusWrite(filePath, { doc: patchedDoc }) : await validateStatusWrite(filePath);
5124
5234
  if (gate2?.hardBlocked) {
5125
5235
  defaultStatusLogger("error", "hard-gate blocked (hardBlocked=true) — refusal requires a host refusal channel");
5126
5236
  }