@mstar-harness/opencode 3.1.2 → 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 +14 -0
  2. package/dist/mstar.js +195 -108
  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,20 @@ 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
+
9
23
  ## [3.1.2] - 2026-08-21
10
24
 
11
25
  ### Harness
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,
@@ -185,11 +185,11 @@ import { existsSync as existsSync6, readFileSync as readFileSync7, readdirSync a
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 existsSync7, 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 existsSync8 } 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 };
@@ -1434,40 +1434,41 @@ function validateStatusV2(docOrPath, opts = {}) {
1434
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`));
1435
1435
  }
1436
1436
  if (typeof entry.started_at === "string" && typeof snapshot.started_at === "string" && entry.started_at !== snapshot.started_at) {
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)} — 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`));
1438
1438
  }
1439
1439
  }
1440
1440
  }
1441
1441
  return { ok: violations.length === 0, violations };
1442
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
+ }
1443
1465
  async function registerWorkflow(root, entry) {
1444
1466
  const entryGate = validateWorkflowEntry(entry);
1445
1467
  if (!entryGate.ok) {
1446
1468
  throw new Error(`refusing to register invalid workflow entry: ${entryGate.violations.map((v) => v.message).join("; ")}`);
1447
1469
  }
1448
1470
  const statusPath = resolve5(root);
1449
- const harnessDir = dirname5(statusPath);
1450
- return withStatusWriteLock(statusPath, () => {
1451
- const current = readJson(statusPath);
1452
- const fresh = Object.keys(current).length === 0;
1453
- const doc = fresh ? { version: 2, updated_at: todayString(), workflows: [] } : current;
1454
- if (!fresh && !Array.isArray(doc.workflows)) {
1455
- throw new Error("refusing to modify status.json: workflows must be an array — a v1 root must be migrated first (run `mstar migrate`)");
1456
- }
1457
- const existing = doc.workflows.findIndex((wf) => wf.id === entry.id);
1458
- if (existing >= 0) {
1459
- doc.workflows[existing] = entry;
1460
- } else {
1461
- doc.workflows.push(entry);
1462
- }
1463
- doc.updated_at = todayString();
1464
- const gate = validateStatusV2(doc, { harnessDir });
1465
- if (!gate.ok) {
1466
- throw new Error(`refusing to write invalid status.json: ${gate.violations.map((v) => v.message).join("; ")}`);
1467
- }
1468
- writeJson(statusPath, doc);
1469
- return doc;
1470
- });
1471
+ return withStatusWriteLock(statusPath, () => registerWorkflowEntryLocked(statusPath, entry));
1471
1472
  }
1472
1473
  async function unregisterWorkflow(root, id) {
1473
1474
  if (typeof id !== "string" || id.trim() === "") {
@@ -3012,9 +3013,11 @@ async function applyMigratePlan(plan) {
3012
3013
  if (!gate2.ok) {
3013
3014
  throw new Error(`refusing to apply migration: invalid project register: ${gate2.violations.map((v) => v.message).join("; ")}`);
3014
3015
  }
3015
- const filePath = projectTargetOf(plan.register.file);
3016
- mkdirSync6(dirname7(filePath), { recursive: true });
3017
- 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
+ }
3018
3021
  }
3019
3022
  if (plan.roadmap !== null) {
3020
3023
  const filePath = projectTargetOf(plan.roadmap.file);
@@ -3471,60 +3474,6 @@ function validateAuditStatusBlocks(planText) {
3471
3474
  });
3472
3475
  return { ok: violations.length === 0, violations };
3473
3476
  }
3474
- function buildLineStarts(text) {
3475
- const starts = [0];
3476
- for (let i = 0;i < text.length; i++) {
3477
- if (text[i] === `
3478
- `)
3479
- starts.push(i + 1);
3480
- }
3481
- return starts;
3482
- }
3483
- function lineAt(starts, index) {
3484
- let lo = 0;
3485
- let hi = starts.length - 1;
3486
- while (lo < hi) {
3487
- const mid = lo + hi + 1 >> 1;
3488
- if (starts[mid] <= index)
3489
- lo = mid;
3490
- else
3491
- hi = mid - 1;
3492
- }
3493
- return lo + 1;
3494
- }
3495
- function redactSecrets(text, filePath) {
3496
- const starts = buildLineStarts(text);
3497
- const marker = (type, index) => `[REDACTED ${type}@${lineAt(starts, index)}${filePath === undefined ? "" : ` in ${filePath}`}]`;
3498
- const replacements = [];
3499
- const findings = [];
3500
- for (const pattern of WHOLE_MATCH_PATTERNS) {
3501
- for (const match of text.matchAll(pattern.re)) {
3502
- if (match.index === undefined)
3503
- continue;
3504
- replacements.push({ index: match.index, length: match[0].length, text: marker(pattern.type, match.index) });
3505
- findings.push({ line: lineAt(starts, match.index), type: pattern.type });
3506
- }
3507
- }
3508
- for (const pattern of VALUE_PATTERNS) {
3509
- for (const match of text.matchAll(pattern.re)) {
3510
- if (match.index === undefined)
3511
- continue;
3512
- const type = pattern.typeOf(match[2]);
3513
- const replacement = `${match[1]}${match[2]}${match[3]}${match[4]}${marker(type, match.index)}`;
3514
- replacements.push({ index: match.index, length: match[0].length, text: replacement });
3515
- findings.push({ line: lineAt(starts, match.index), type });
3516
- }
3517
- }
3518
- replacements.sort((a, b) => b.index - a.index);
3519
- let out = text;
3520
- for (const r of replacements)
3521
- out = out.slice(0, r.index) + r.text + out.slice(r.index + r.length);
3522
- const deduped = new Map;
3523
- for (const f of findings)
3524
- deduped.set(`${f.line}:${f.type}`, f);
3525
- const sorted = [...deduped.values()].sort((a, b) => a.line - b.line || a.type.localeCompare(b.type));
3526
- return { text: out, findings: sorted };
3527
- }
3528
3477
  function slugify(title) {
3529
3478
  return title.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
3530
3479
  }
@@ -3659,6 +3608,137 @@ function scaffoldAuditPlan(outDir, findings, options = {}) {
3659
3608
  }));
3660
3609
  return { outDir: resolve9(outDir), date, files: written, nextNumber: next };
3661
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
+ }
3662
3742
  function violation10(severity, code, message, fix) {
3663
3743
  return { ok: false, severity, code, message, fix };
3664
3744
  }
@@ -3875,7 +3955,7 @@ function referenceExists(repoRoot, docText) {
3875
3955
  for (const { ref, isSymbol, module } of refs) {
3876
3956
  if (!isSymbol || module === undefined) {
3877
3957
  const candidate = ref.replace(LINE_SUFFIX_RE, "").replace(ANCHOR_RE, "");
3878
- if (existsSync7(resolve10(repoRoot, candidate))) {
3958
+ if (existsSync8(resolve10(repoRoot, candidate))) {
3879
3959
  checked++;
3880
3960
  } else {
3881
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"));
@@ -3906,7 +3986,7 @@ function collectKnowledgeDocs(dir) {
3906
3986
  if (entry.isDirectory()) {
3907
3987
  stack.push(full);
3908
3988
  } else if (entry.name.endsWith(".md") && entry.name !== "README.md" && entry.name !== "index.md") {
3909
- docs.push(relative4(dir, full).split(sep3).join("/"));
3989
+ docs.push(relative4(dir, full).split(sep4).join("/"));
3910
3990
  }
3911
3991
  }
3912
3992
  }
@@ -3923,7 +4003,7 @@ function normalizeIndexRef(cell) {
3923
4003
  function assertIndexRows(knowledgeDir) {
3924
4004
  const violations = [];
3925
4005
  const readmePath = join12(knowledgeDir, "README.md");
3926
- if (!existsSync7(readmePath)) {
4006
+ if (!existsSync8(readmePath)) {
3927
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"));
3928
4008
  return { ok: false, violations };
3929
4009
  }
@@ -3955,7 +4035,7 @@ function compoundRefreshScope(harnessDir, projectRoot) {
3955
4035
  ];
3956
4036
  }
3957
4037
  function isFileLikeRoot(root) {
3958
- return /^[^.]*\.[A-Za-z0-9]{1,10}$/.test(basename4(root));
4038
+ return /^[^.]*\.[A-Za-z0-9]{1,10}$/.test(basename5(root));
3959
4039
  }
3960
4040
  function scopeGuard(path, allowedRoots) {
3961
4041
  const resolved = resolve10(path);
@@ -3964,7 +4044,7 @@ function scopeGuard(path, allowedRoots) {
3964
4044
  if (isFileLikeRoot(r)) {
3965
4045
  if (resolved === r)
3966
4046
  return { ok: true, violations: [] };
3967
- } else if (resolved === r || resolved.startsWith(r + sep3)) {
4047
+ } else if (resolved === r || resolved.startsWith(r + sep4)) {
3968
4048
  return { ok: true, violations: [] };
3969
4049
  }
3970
4050
  }
@@ -4186,7 +4266,7 @@ function validateRoleMapping(rolesDir, options = {}) {
4186
4266
  const violations = [];
4187
4267
  const referenceById = new Map(mapping.map((m) => [m.agentId, m.reference]));
4188
4268
  for (const { agentId, reference } of mapping) {
4189
- if (!existsSync8(join13(rolesDir, reference))) {
4269
+ if (!existsSync9(join13(rolesDir, reference))) {
4190
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`));
4191
4271
  }
4192
4272
  }
@@ -4382,7 +4462,7 @@ var SEVERITY_ORDER, MSTARC_FILE = ".mstarc", MSTARC_SECTION = "config", MSTARC_H
4382
4462
  !.agents/knowledge/**
4383
4463
  !.agents/specs/
4384
4464
  !.agents/specs/**
4385
- `, 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;
4386
4466
  var init_engine = __esm(() => {
4387
4467
  SEVERITY_ORDER = ["critical", "high", "medium", "low", "nit"];
4388
4468
  CONFIG_KEYS = {
@@ -4497,20 +4577,6 @@ var init_engine = __esm(() => {
4497
4577
  "direction"
4498
4578
  ];
4499
4579
  AUDIT_STATUS_FIELDS = ["Priority", "Effort", "Risk", "Depends on", "Category", "Planned at"];
4500
- WHOLE_MATCH_PATTERNS = [
4501
- { type: "private-key", re: /-----BEGIN [A-Z0-9 ]*PRIVATE KEY-----/g },
4502
- { type: "aws-access-key", re: /\b(?:AKIA|ASIA)[0-9A-Z]{16}\b/g },
4503
- { type: "github-token", re: /\bgh[pousr]_[A-Za-z0-9]{36,}\b/g },
4504
- { type: "slack-token", re: /\bxox[baprs]-[A-Za-z0-9-]{10,}\b/g },
4505
- { type: "jwt", re: /\beyJ[A-Za-z0-9_-]{10,1024}\.[A-Za-z0-9_-]{10,1024}\.[A-Za-z0-9_-]{10,1024}\b/g },
4506
- { type: "api-secret-key", re: /\bsk-[A-Za-z0-9-]{20,}\b/g }
4507
- ];
4508
- VALUE_PATTERNS = [
4509
- {
4510
- typeOf: (key) => key.replace(/([a-z0-9])([A-Z])/g, "$1-$2").toLowerCase().replace(/[_-]+/g, "-"),
4511
- 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
4512
- }
4513
- ];
4514
4580
  KNOWLEDGE_REQUIRED_FIELDS = ["module", "date", "problem_type", "category", "severity"];
4515
4581
  KNOWLEDGE_PROBLEM_TYPES = [
4516
4582
  "build_error",
@@ -5143,7 +5209,28 @@ var MorningStarHarnessPlugin = async () => {
5143
5209
  defaultStatusLogger("error", "hard-gate blocked (hardBlocked=true) — refusal requires a host refusal channel");
5144
5210
  }
5145
5211
  } else if (input.tool === "edit") {
5146
- 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);
5147
5234
  if (gate2?.hardBlocked) {
5148
5235
  defaultStatusLogger("error", "hard-gate blocked (hardBlocked=true) — refusal requires a host refusal channel");
5149
5236
  }
@@ -15,114 +15,33 @@ Drive the active Morning Star iteration forward. **Boot loads skills; this comma
15
15
 
16
16
  **Done 定义**:**仅** Phase 5 §5.5 exit checklist 全 `[x]`。**Phase 3 close ≠ Done;Phase 4 开 PR ≠ Done。**
17
17
 
18
- ## Continuous execution(HARD Phase 2–5)
18
+ ## 共享 invariants / preflight / todos / STOP
19
19
 
20
- Execute **`mstar-iteration` §2.6**(Continuous execution SSOT:自 Phase 2 进入至 Phase 5 §5.5 exit 前 PM **连续编排**,进度汇报后下一条必须是 dispatch 或下一 phase 步骤,不向用户例行 yes/no check-in)。
21
-
22
- **合法 STOP(仅此升级用户)**:
23
-
24
- - §2.0 / Phase 4 branch metadata 缺失(`iteration_base_branch` / `target_branch`)
25
- - **`Blocked`**:真冲突、secrets、不可逆范围缺口、Phase 5 多轮仍无法 merge-ready
26
- - 用户本轮显式打断
27
-
28
- **Turn 收束纪律**:若 host 即将结束本轮,最后一条 assistant 内容必须是 **in-flight 动作**(invoke 已发出、或写明下一 dispatch 的 Assignment 字段),**不得**以对用户的确认问句结尾。
29
-
30
- ## PM invariants(Phase 2–5 全程有效)
31
-
32
- | 禁止(PM 线程) | 必须 |
33
- |-----------------|------|
34
- | Write/Edit/Shell 产品代码、写测试、跑 QC 审查(Phase 2) | 每条 implement/QC/QA Assignment ⇒ **1 次 `Task`** |
35
- | **多 task plan 用 inline 大包派发**(整份 plan / T1–Tn 贴进一个 dev Assignment) | **SDD**:`mstar-sdd` per-task 循环 — `mstar-harness sdd task-brief` → implementer → `mstar-harness sdd review-package` → task reviewer → `progress.md` |
36
- | 只写 Assignment 就进入下一 gate | 同轮 dispatch:`Subagent invokes issued: N`(N = Assignment 条数) |
37
- | 最后一个 plan `Done` 后直接开 PR / 汇报结束 | **Phase 3 → 4 → 5** 顺序执行 |
38
- | Phase 4 开 PR 后停止 | Phase 5 loop 至 merge-ready;**禁止**未过 §5.5 就结束会话 |
39
- | Phase 5 自己改产品代码 | 需改产品代码时 **dispatch** `fullstack-dev` / `ops-engineer` |
40
-
41
- 派发细则 → **`mstar-dispatch-gates`** + **`mstar-host`**。Phase 3 细则 → **`mstar-iteration` §3** + **`mstar-compound`**。
42
-
43
- **Session todos**:
44
-
45
- | Todo id | 何时追加 | 何时可勾掉 |
46
- |---------|----------|------------|
47
- | plan-wave todos | 进入 Phase 2 | 各 plan `Done` |
48
- | `phase-3-iteration-close` | 仅剩 1 个非 `Done` plan | Phase 3 §3.5 exit 全 `[x]` |
49
- | `phase-4-create-pr` | Phase 3 完成后 | PR 已创建并记录 URL/number |
50
- | `phase-5-pr-merge-ready` | Phase 4 完成后 | Phase 5 §5.5 exit 全 `[x]` |
20
+ Phase 25 共享内容(PM invariants、assignment preflight、session todos、continuous-execution STOP)→ **`mstar-iteration/references/command-shared-invariants.md`**(SSOT;不在本命令重复)。
51
21
 
52
22
  ## Boot
53
23
 
54
- 1. `mstar-harness-core`
55
- 2. `mstar-roles` → `references/project-manager.md`
56
- 3. `mstar-iteration` → **§ Phase 2–5**(§3 close gate;§4 PR delivery;§5 merge-ready loop)
57
- 4. `mstar-compound` — Phase 3 §3.2 前加载(含 Phase 6 索引登记)
58
- 5. `mstar-dispatch-gates` + host reference
59
- 6. **`mstar-sdd`** — **before first implement dispatch** in Phase 2(per-task loop SSOT;多 task plan 默认 `Execution mode: sdd`)
60
- 7. `mstar-review-qc` — before first QC dispatch in Phase 2
61
- 8. `mstar-plan-artifacts`, `mstar-plan-conventions`, `mstar-branch-worktree`
62
- 9. **`mstar-iteration/references/phase-2-worktree-lease.md`** — Phase 2 control worktree、`execution_lease`、serial `integration_merge_lease`(§2.0 #5 未 waive)
24
+ **`mstar-iteration`** Load order 加载(`mstar-harness-core` → `mstar-roles` → `references/project-manager.md` → `mstar-iteration` § Phase 2–5 + `command-shared-invariants.md` → `mstar-compound` → `mstar-dispatch-gates` + host reference → **`mstar-sdd`**(first implement dispatch 前)→ `mstar-review-qc`(first QC 前)→ `mstar-plan-artifacts` / `mstar-plan-conventions` / `mstar-branch-worktree` → **`mstar-iteration/references/phase-2-worktree-lease.md`**)。完整 load list → **`mstar-roles`**。
63
25
 
64
26
  ## Phase 2: Autonomous Execute
65
27
 
66
- **Continuous execution applies**(上节)。Execute **`mstar-iteration` § Phase 2** exactly:§2.0 前置五道闸(含 branch metadata #4 + control-worktree/lease #5)→ §2.1 session todos → §2.2 read backlog(`status.json` + branch metadata)→ §2.3 integration branch + control worktree(record `metadata.control_worktree_path`;status/SDD via control path)→ §2.4 per-plan loop(**lease-gated parallel** across plan IDs unless `Plan parallelism: serial`;claim/resume `execution_lease`;feature worktree;**SDD** per-task 串行;QC full tri-review **N=3** + QA;serial merge via `integration_merge_lease`;cross-plan sync → compass)→ §2.5 dispatch-first → §2.6 push 纪律。全部 plan `Done` → **STOP**(Phase flow)→ 打印 `## Phase 3: iteration-close`;**不得**进入 Phase 4。
67
-
68
- **Assignment preflight**(`mstar-harness` bin 未安装时静默跳过): 在每次 implement/QC/QA 派发前(**SDD** 下为最新 `{SDD_DIR}/task-N-brief.md` 或临时写盘的 Assignment),若本机装有 CLI,校验最新 Assignment。模式由迭代 compass frontmatter 的 `enforcement` 键决定(Slice 5):
69
-
70
- - **默认(compass 无 `enforcement: hard`)— 可选 warn-only**:exit 1 仅提示,不阻断派发(Slice 3 行为不变):
71
-
72
- ```bash
73
- command -v mstar-harness >/dev/null 2>&1 && mstar-harness dispatch validate "<latest-assignment-file>"
74
- ```
75
-
76
- - **`enforcement: hard`(迭代 compass frontmatter 声明)— fail-fast**:校验失败即 `exit 1` 阻断派发(bin 缺失仍静默跳过):
28
+ Execute **`mstar-iteration` § Phase 2** exactly(§2.0 五道闸 §2.1 session todos → §2.2 backlog §2.3 integration branch + control worktree §2.4 per-plan looplease-gatedSDD per-taskQC tri N=3 + QA;serial merge)→ §2.5 dispatch-first → §2.6 push 纪律)。全部 plan `Done` → **STOP** 打印 `## Phase 3: iteration-close`。
77
29
 
78
- ```bash
79
- if command -v mstar-harness >/dev/null 2>&1; then mstar-harness dispatch validate "<latest-assignment-file>" || exit 1; fi
80
- ```
81
-
82
- > 路径必须加引号且替换为具体文件(如最新 `{SDD_DIR}/task-N-brief.md`,勿留尖括号)——agent 代入的路径不得进入 shell 无引号展开。
30
+ **Assignment preflight**:每次 implement/QC/QA 派发前按 **`mstar-iteration/references/command-shared-invariants.md`** 执行。
83
31
 
84
32
  ## Phase 3: iteration-close
85
33
 
86
- 当 **every** plan 为 `Done`:
87
-
88
- 1. **STOP** per-plan loop — 不得开 PR、不得进入 Phase 4/5、不得输出「迭代完成」摘要。
89
- 2. 打印标题 **`## Phase 3: iteration-close`**(用户可见的 phase 边界)。
90
- 3. Execute **`mstar-iteration` § Phase 3**(→ **`references/phase-3-iteration-close.md`** §3.0→§3.5):§3.0.5 compass 规范化 → §3.1 close entry checklist(**HARD GATE**,必须打印)→ §3.2 compound + **`<iteration-id>/` package promotion**(`mstar-compound` Phase 6 索引登记)→ §3.3 roadmap → §3.4 frontmatter `status: completed` + `end_date` → §3.5 close exit checklist + commit → push。
91
- 4. §3.5 exit 全 `[x]` 且 compass `status: completed` 后 → 打印 **`## Phase 4: Create PR`**。
92
-
93
- **Phase 3 exit(非 iteration-drive Done)**: §3.5 checklist `[x]` + compass frontmatter `completed`。
34
+ 当 **every** plan 为 `Done`:**STOP** per-plan loop → 打印 **`## Phase 3: iteration-close`** → execute **`mstar-iteration` § Phase 3**(→ **`references/phase-3-iteration-close.md`** §3.0→§3.5;§3.1 entry checklist HARD GATE;§3.2 compound + package promotion;§3.4 `status: completed` + `end_date`)→ §3.5 exit 全 `[x]` 后打印 **`## Phase 4: Create PR`**。
94
35
 
95
36
  ## Phase 4: Create PR
96
37
 
97
- Execute **`mstar-iteration` § Phase 4**(→ **`references/phase-4-5-pr-delivery.md`**):打印 `## Phase 4: Create PR` → resolve `metadata.target_branch`(compass fallback;缺失 → **STOP**,never default `main`/`master`)→ 创建 PR `spec_integration_branch` → `target_branch` → 记录 PR URL + number(Phase 5 SSOT)→ 勾掉 host todo `phase-4-create-pr` → **Immediately** 打印 **`## Phase 5: PR merge-ready`**(**禁止**在此停止或汇报 Done)。
98
-
99
- **Phase 4 exit(非 iteration-drive Done)**: PR 已创建且 head = `spec_integration_branch`。
38
+ Execute **`mstar-iteration` § Phase 4**(→ **`references/phase-4-5-pr-delivery.md`**):打印 `## Phase 4: Create PR` → resolve `metadata.target_branch`(缺失 → **STOP**,never default `main`/`master`)→ 创建 PR `spec_integration_branch` → `target_branch` → 记录 PR URL + number 勾掉 `phase-4-create-pr` → **Immediately** 打印 **`## Phase 5: PR merge-ready`**(**禁止**在此停止或汇报 Done)。
100
39
 
101
40
  ## Phase 5: PR merge-ready(babysit loop)
102
41
 
103
- Execute **`mstar-iteration` § Phase 5**(→ **`references/phase-4-5-pr-delivery.md`** §5.0–§5.2;**§5.1a push cadence HARD**)。**§5.5 exit 全 `[x]` = iteration-drive Done.**
104
-
105
- 本 command **叠加**可选 helper skill 发现(**non-`mstar-*`**;不写入 `mstar-*` load order):
106
-
107
- ### 5.0 Discover optional helper skills
108
-
109
- Helper 搜索路径 → **`mstar-iteration/references/phase5-helper-discovery.md`**(babysit / `*-babysit` / greploop 路径清单;first readable `SKILL.md` wins per name)。
110
-
111
- | Priority | Condition | Read before loop | Primary done signal |
112
- |----------|-----------|------------------|---------------------|
113
- | 1 | `babysit` **or** any `*-babysit` found | that skill’s `SKILL.md`(prefer exact `babysit`, else first matching `*-babysit`) | Required CI **all green** + **all** review threads **resolved** |
114
- | 2 | `greploop` found **and** repo has Greptile/greploop | `greploop` SKILL.md | Greptile score **5/5** on this PR(**additive** — does not replace priority-1 gates) |
115
- | 3 | else neither babysit/`*-babysit` | —(fallback = babysit 同级 CI + reviews 门禁) | Required CI **all green** + **all** review threads **resolved** |
116
-
117
- **Both babysit/`*-babysit` and greploop apply**: run **babysit/`*-babysit` first**(CI + reviews),then optional greploop until Greptile **5/5**(串行;§5.5 仍须满足全部 exit 项)。Do **not** prefer greploop over babysit。**No greploop / repo without Greptile**: skip greploop entirely。**All modes** share §5.5 exit checklist(Greptile 5/5 only when greploop mode ran or repo shows a Greptile score)。
118
-
119
- ### 5.1 Loop + review fix hygiene(all modes)
120
-
121
- Execute **`mstar-iteration` §5.1 loop**(`references/phase-4-5-pr-delivery.md` §5.1):status(`gh pr view <number> --json mergeable,mergeStateStatus,statusCheckRollup,reviewDecision` 或宿主等价 API)→ merge conflicts → unresolved reviews → CI → **§5.1a idle push**(一批修复一次 push)→ mode-specific pass(babysit SKILL / greploop / fallback)→ §5.1 review fix hygiene(**comment on same thread + resolve**)→ repeat until §5.5。Fixes **push 到 `spec_integration_branch`**(PR head);禁止另开分支替代。**Checkout(HARD,§5.0)**:直接在 control / 集成分支 checkout 上修;**禁止**另开 Phase 5 fix worktree,**禁止**套用 Phase 2 control 产品编辑禁令。产品代码修复 → **dispatch** dev/ops(Assignment cwd = control);PM 线程不代写。**禁止**为「让 CI 变绿」而改 workflow,除非用户明确授权。多轮仍 blocked → 升级用户。
122
-
123
- ### 5.2 Phase 5 exit checklist(iteration-drive Done)
42
+ Execute **`mstar-iteration` § Phase 5**(→ **`references/phase-4-5-pr-delivery.md`** §5.0–§5.2;**§5.1a push cadence HARD**)。**§5.5 exit checklist 全 `[x]` = command Done.**
124
43
 
125
- Print **`## Phase 5 exit checklist`** per **`mstar-iteration` §5.2**(`references/phase-4-5-pr-delivery.md` §5.2:PR mergeable、required CI green、review threads resolved(或用户书面 waive)、§5.1 comment + resolve、todo `phase-5-pr-merge-ready`;Greptile 5/5 仅当 greploop mode / repo 可见分数)→ 全 `[x]` 后结束本 command。
44
+ command **叠加**可选 helper skill 发现(**non-`mstar-*`**;不写入 `mstar-*` load order)→ **`mstar-iteration/references/phase5-helper-discovery.md`**(babysit / `*-babysit` / greploop / fallback 路径清单;first readable `SKILL.md` wins)。Loop + review fix hygiene + exit checklist §5.1–§5.2(同上 reference)。
126
45
 
127
46
  **Then** report: iteration id, plans completed, compound summary, PR link, merge-ready evidence(CI snapshot + review resolution + Greptile if applicable)。
128
47