@mstar-harness/engine 2.1.1 → 2.3.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/README.md CHANGED
@@ -19,7 +19,7 @@ const version = readHarnessVersion(); // "1.8.8" — monorepo root package.json
19
19
  ## Scope
20
20
 
21
21
  - Importable library only — **no `bin`**; the CLI (`@mstar-harness/cli`) wraps engine functions as thin `mstar …` subcommands.
22
- - Dependencies: `node:*` only (zero external runtime deps — all validators hand-rolled; ajv pruned as phantom, zod removed 2026-08-08).
22
+ - Dependencies: `node:*` only (zero external runtime deps — all validators hand-rolled; ajv and zod were pruned as phantom dependencies).
23
23
  - Skill prose stays authoritative; engine exports are the machine-checkable mirror of the rules the `mstar-*` skills state.
24
24
 
25
25
  ## License
package/dist/engine.js CHANGED
@@ -83,24 +83,52 @@ function readHarnessVersion() {
83
83
  }
84
84
  // src/path.ts
85
85
  import { mkdirSync as mkdirSync2, readdirSync, readFileSync as readFileSync2, statSync } from "node:fs";
86
+ import { execFileSync } from "node:child_process";
86
87
  import { basename as basename2, dirname as dirname2, isAbsolute, join as join2, relative, resolve as resolve2 } from "node:path";
87
88
  function resolveHarnessDir(startDir = process.cwd(), opts = {}) {
88
89
  const start = resolve2(startDir);
89
90
  const explicit = opts.harnessDir ?? process.env.MSTAR_HARNESS_DIR;
90
91
  if (explicit)
91
92
  return resolve2(start, explicit);
93
+ const boundary = resolve2(start, opts.workspaceRoot ?? defaultWorkspaceRoot(start));
92
94
  let dir = start;
93
95
  for (;; ) {
96
+ if (!isAtOrBelow(dir, boundary))
97
+ return null;
94
98
  for (const candidate of [join2(dir, ".mstar"), join2(dir, ".agents"), join2(dir, ".plans"), join2(dir, "plans")]) {
95
99
  if (isDirectory(candidate))
96
100
  return candidate;
97
101
  }
102
+ if (dir === boundary)
103
+ return null;
98
104
  const parent = dirname2(dir);
99
105
  if (parent === dir)
100
106
  return null;
101
107
  dir = parent;
102
108
  }
103
109
  }
110
+ function defaultWorkspaceRoot(startDir) {
111
+ try {
112
+ const cdup = execFileSync("git", ["rev-parse", "--show-cdup"], {
113
+ cwd: startDir,
114
+ encoding: "utf8",
115
+ stdio: ["ignore", "pipe", "ignore"]
116
+ }).trim();
117
+ if (!cdup)
118
+ return startDir;
119
+ let boundary = startDir;
120
+ for (const segment of cdup.split(/[\\/]/)) {
121
+ if (segment && segment !== ".")
122
+ boundary = dirname2(boundary);
123
+ }
124
+ return resolve2(boundary);
125
+ } catch {}
126
+ return startDir;
127
+ }
128
+ function isAtOrBelow(dir, root) {
129
+ const rel = relative(root, dir);
130
+ return rel === "" || !rel.startsWith("..") && !isAbsolute(rel);
131
+ }
104
132
  function resolveSpecsDir(harnessDir, opts = {}) {
105
133
  const harness = resolve2(harnessDir);
106
134
  const repoRoot = dirname2(harness);
@@ -646,7 +674,7 @@ function parseAssignmentBranchForms(assignmentText) {
646
674
  if (fields.branchPolicy !== undefined && fields.branchPolicy !== "") {
647
675
  const direct = fields.branchPolicy.match(/^direct\s+on\s+(\S+)/i);
648
676
  if (direct) {
649
- const strict = fields.branchPolicy.match(/^direct\s+on\s+(\S+)(?:\s*(?:[—–]|--|-)\s*(.+))?$/);
677
+ const strict = fields.branchPolicy.match(/^direct\s+on\s+(\S+)(?:\s*(?:[\u2014\u2013]|--|-)\s*(.+))?$/);
650
678
  forms.directOn = { branch: direct[1].trim(), reason: strict ? (strict[2] ?? "").trim() : "" };
651
679
  }
652
680
  }
@@ -1150,7 +1178,7 @@ function techDebtRollup(docOrPath) {
1150
1178
  return { computed, stored, checks, overall };
1151
1179
  }
1152
1180
  // src/worktree.ts
1153
- import { execFileSync } from "node:child_process";
1181
+ import { execFileSync as execFileSync2 } from "node:child_process";
1154
1182
  import { existsSync as existsSync3 } from "node:fs";
1155
1183
  import { isAbsolute as isAbsolute3, resolve as resolve5 } from "node:path";
1156
1184
  var DEFAULT_PROBE_TIMEOUT_MS = 1e4;
@@ -1173,7 +1201,7 @@ function probeBranch(worktreePath, opts) {
1173
1201
  return { branch: precomputed };
1174
1202
  const timeout = opts.timeoutMs ?? probeTimeoutMs();
1175
1203
  try {
1176
- const stdout = execFileSync(opts.gitPath ?? "git", ["-C", worktreePath, "branch", "--show-current"], {
1204
+ const stdout = execFileSync2(opts.gitPath ?? "git", ["-C", worktreePath, "branch", "--show-current"], {
1177
1205
  encoding: "utf8",
1178
1206
  stdio: ["ignore", "pipe", "pipe"],
1179
1207
  timeout
@@ -1302,7 +1330,7 @@ function singleReviewSnapshot(assignments) {
1302
1330
  return gate(violations);
1303
1331
  }
1304
1332
  // src/sdd.ts
1305
- import { execFileSync as execFileSync2 } from "node:child_process";
1333
+ import { execFileSync as execFileSync3 } from "node:child_process";
1306
1334
  import { mkdirSync as mkdirSync4, readFileSync as readFileSync4, realpathSync, statSync as statSync3, writeFileSync as writeFileSync3 } from "node:fs";
1307
1335
  import { basename as basename3, dirname as dirname4, isAbsolute as isAbsolute4, join as join5, resolve as resolve6 } from "node:path";
1308
1336
  class SddScriptError extends Error {
@@ -1330,7 +1358,7 @@ function isFile(file) {
1330
1358
  var GIT_CAPTURE_MAX_BYTES = 64 * 1024 * 1024;
1331
1359
  function gitOut(cwd, args) {
1332
1360
  try {
1333
- return execFileSync2("git", args, {
1361
+ return execFileSync3("git", args, {
1334
1362
  cwd,
1335
1363
  encoding: "utf8",
1336
1364
  stdio: ["ignore", "pipe", "pipe"],
@@ -1463,7 +1491,7 @@ function reviewPackage(base, head, outFile, opts = {}) {
1463
1491
  const cwd = opts.cwd ?? process.cwd();
1464
1492
  const verifyRef = (ref, what) => {
1465
1493
  try {
1466
- execFileSync2("git", ["rev-parse", "--verify", "--quiet", ref], { cwd, stdio: ["ignore", "pipe", "pipe"] });
1494
+ execFileSync3("git", ["rev-parse", "--verify", "--quiet", ref], { cwd, stdio: ["ignore", "pipe", "pipe"] });
1467
1495
  } catch {
1468
1496
  throw new SddScriptError(`bad ${what}: ${ref}`, 2);
1469
1497
  }
@@ -1483,7 +1511,7 @@ function reviewPackage(base, head, outFile, opts = {}) {
1483
1511
  const shortHead = gitOut(cwd, ["rev-parse", "--short", head]) ?? head;
1484
1512
  out = join5(sddDir, `review-${shortBase}..${shortHead}.diff`);
1485
1513
  }
1486
- const run = (args) => execFileSync2("git", args, { cwd, maxBuffer: GIT_CAPTURE_MAX_BYTES });
1514
+ const run = (args) => execFileSync3("git", args, { cwd, maxBuffer: GIT_CAPTURE_MAX_BYTES });
1487
1515
  const parts = [
1488
1516
  Buffer.from(`# Review package: ${base}..${head}
1489
1517
 
@@ -1507,7 +1535,7 @@ function assertBaseSha(ref, opts = {}) {
1507
1535
  throw new SddScriptError(`assertBaseSha: BASE must be a commit SHA (full or prefix); got ${JSON.stringify(ref)}. ` + "Never use HEAD~1 as review BASE (multi-commit tasks truncate).", 2);
1508
1536
  }
1509
1537
  try {
1510
- execFileSync2("git", ["rev-parse", "--verify", "--quiet", `${ref}^{commit}`], {
1538
+ execFileSync3("git", ["rev-parse", "--verify", "--quiet", `${ref}^{commit}`], {
1511
1539
  cwd: opts.cwd,
1512
1540
  stdio: ["ignore", "pipe", "pipe"]
1513
1541
  });
@@ -2524,6 +2552,7 @@ function renderIndex(params) {
2524
2552
  if (rejectedRows !== "") {
2525
2553
  sections.push("", "## Findings considered and rejected", "", rejectedRows);
2526
2554
  }
2555
+ sections.push("", "## Red-team dispositions", "", "- <finding>: <survived / refuted / hallucination-dropped / uncovered-kept>, <one-line reason>");
2527
2556
  return `${sections.join(`
2528
2557
  `)}
2529
2558
  `;
@@ -3052,11 +3081,31 @@ function findTemporaryMarkers(fileText) {
3052
3081
  }
3053
3082
  return { ok: violations.length === 0, violations, markers };
3054
3083
  }
3084
+ var TASK_ARTIFACT_RE = /\btask-\d+(?:-(?:brief|report|fix-report|diff)|\.diff)\b/g;
3085
+ var SDD_DEEPLINK_RE = /\.(?:mstar|agents)\/sdd\/([^\s/<>{}\[\]"'\*\?]+)/g;
3086
+ function findEphemeralCitations(skillText) {
3087
+ const citations = [];
3088
+ const lines = skillText.split(/\r?\n/);
3089
+ for (let i = 0;i < lines.length; i++) {
3090
+ const found = [];
3091
+ for (const m of lines[i].matchAll(TASK_ARTIFACT_RE)) {
3092
+ found.push({ index: m.index, match: m[0], kind: "task-artifact" });
3093
+ }
3094
+ for (const m of lines[i].matchAll(SDD_DEEPLINK_RE)) {
3095
+ found.push({ index: m.index, match: m[0], kind: "sdd-deeplink" });
3096
+ }
3097
+ found.sort((a, b) => a.index - b.index);
3098
+ for (const f of found) {
3099
+ citations.push({ line: i + 1, match: f.match, kind: f.kind });
3100
+ }
3101
+ }
3102
+ return citations;
3103
+ }
3055
3104
  var TEST_FILE_PATH_RE = /[\w./-]+\.(?:test|spec)\.[a-z0-9]+/i;
3056
3105
  var TEST_FILE_PHRASE_RE = /\btest files?\b/i;
3057
3106
  var COMMAND_PROMPT_RE = /^\s*[$>]\s*\S/;
3058
3107
  var RUNNER_RE = /\b(?:bun|pnpm|npm|yarn|npx|bunx)\s+(?:test|run|exec)\b|\b(?:npx|bunx)\s+[\w./-]+\b|\b(?:tsc|vitest|jest|mocha|pytest)\b|\bgo\s+test\b|\bcargo\s+test\b/i;
3059
- var OUTPUT_TOKEN_RE = /[✓✔✗✘]|\b(?:PASS|FAIL)\b|\b\d+\s+(?:pass(?:es|ed)?|fail(?:s|ed|ing)?|skipped|tests?|ok)\b|\bok\s+\d+\b|\ball\s+ok\b|exit(?:ed)?\s+(?:with\s+)?(?:code\s+)?\d+/i;
3108
+ var OUTPUT_TOKEN_RE = /[\u2713\u2714\u2717\u2718]|\b(?:PASS|FAIL)\b|\b\d+\s+(?:pass(?:es|ed)?|fail(?:s|ed|ing)?|skipped|tests?|ok)\b|\bok\s+\d+\b|\ball\s+ok\b|exit(?:ed)?\s+(?:with\s+)?(?:code\s+)?\d+/i;
3060
3109
  function assertSddTddTriple(reportText) {
3061
3110
  const violations = [];
3062
3111
  const lines = reportText.split(/\r?\n/);
@@ -3377,6 +3426,8 @@ function detectHost(signals) {
3377
3426
  return "opencode";
3378
3427
  if (s.has("task_agent_batch") || s.has("ask") || s.has("hub"))
3379
3428
  return "omp";
3429
+ if (s.has("subagent"))
3430
+ return "dsh";
3380
3431
  if (s.has("AgentSwarm"))
3381
3432
  return "kimi";
3382
3433
  if (s.has("Agent") || s.has("AskUserQuestion") || s.has("EnterPlanMode") || s.has("TodoWrite"))
@@ -3400,9 +3451,10 @@ function resolveSkillRoot(host, paths) {
3400
3451
  case "kimi":
3401
3452
  case "zcode":
3402
3453
  return `./skills/${skill}${suffix}`;
3403
- case "pi":
3404
3454
  case "dsh":
3405
- return `deferred: ${host} has no plugin API in v1 — skill-root resolution lands with its adapter (roadmap §8.4)`;
3455
+ return `$DSH_BUNDLED_SKILL_DIR/${skill}${suffix}`;
3456
+ case "pi":
3457
+ return `deferred: pi has no plugin API in v1 — skill-root resolution lands with its adapter (roadmap §8.4)`;
3406
3458
  }
3407
3459
  }
3408
3460
  // src/skill-authoring.ts
@@ -3495,6 +3547,7 @@ export {
3495
3547
  findingsCleanupGate,
3496
3548
  findTemporaryMarkers,
3497
3549
  findSimplifyMarkers,
3550
+ findEphemeralCitations,
3498
3551
  executionModeToN,
3499
3552
  evaluatePhaseGate,
3500
3553
  emitGitignoreSnippet,
package/dist/host.d.ts CHANGED
@@ -11,7 +11,10 @@
11
11
  * resolution.
12
12
  * - `.harness/references/skill-programmatic-roadmap.md` §8.4 — the
13
13
  * `HostAdapter` shared contract (all hooks optional; no concrete adapters
14
- * in the engine; pi/dsh deferred).
14
+ * in the engine; pi deferred).
15
+ * - `.harness/references/dsh-adapter-roadmap.md` §4 D5 (this iteration) —
16
+ * the dsh detection row + skill-root form; mirror text lands in the
17
+ * mstar-host skill when this module is upstreamed.
15
18
  *
16
19
  * UX judgment (ambiguous-host fallback reasoning, plan-mode bridges) stays
17
20
  * prompt — this module only turns tool shapes into a host id.
@@ -19,18 +22,19 @@
19
22
  import type { GateResult, ValidationResult } from "./core.js";
20
23
  import type { AssignmentFields } from "./dispatch.js";
21
24
  import type { IntegrationMergeLease } from "./lease.js";
22
- /** All hosts the engine knows about (roadmap §8.4 host union). `pi` and
23
- * `dsh` have no plugin API in v1 — they appear in the union (and in
24
- * `HostAdapter.host`) but are never detected and get no adapters. */
25
+ /** All hosts the engine knows about (roadmap §8.4 host union). `pi` has no
26
+ * plugin API in v1 — it appears in the union (and in `HostAdapter.host`) but
27
+ * is never detected and gets no adapter. `dsh` is detected and resolved
28
+ * (roadmap §4 D5, this iteration); its adapter ships in the dsh plugin. */
25
29
  export type HostId = "opencode" | "omp" | "pi" | "dsh" | "cursor" | "codex" | "kimi" | "zcode";
26
- /** Result of `detectHost`: one of the six known hosts or `ambiguous`
30
+ /** Result of `detectHost`: one of the seven known hosts or `ambiguous`
27
31
  * (prompt judgment then applies per mstar-host). */
28
- export type DetectResult = "opencode" | "omp" | "cursor" | "codex" | "kimi" | "zcode" | "ambiguous";
32
+ export type DetectResult = "opencode" | "omp" | "dsh" | "cursor" | "codex" | "kimi" | "zcode" | "ambiguous";
29
33
  /** Tool-shape signal tokens accepted by `detectHost`, derived from the
30
34
  * mstar-host detection table. Plan-mode extras (CreatePlan/SwitchMode) and
31
35
  * Browser-plugin tools are documented in the table but are not part of the
32
36
  * v1 signal enum. */
33
- export type ToolSignal = "subagent_type" | "question" | "task_subagent" | "task_agent_batch" | "ask" | "hub" | "Agent" | "AgentSwarm" | "AskUserQuestion" | "EnterPlanMode" | "TodoWrite" | "plan_slash" | "goal" | "functions.*" | "tool_search";
37
+ export type ToolSignal = "subagent_type" | "question" | "task_subagent" | "task_agent_batch" | "ask" | "hub" | "subagent" | "Agent" | "AgentSwarm" | "AskUserQuestion" | "EnterPlanMode" | "TodoWrite" | "plan_slash" | "goal" | "functions.*" | "tool_search";
34
38
  /**
35
39
  * Detect the active host from session tool shapes, per the ordered
36
40
  * mstar-host table (ported verbatim):
@@ -40,14 +44,19 @@ export type ToolSignal = "subagent_type" | "question" | "task_subagent" | "task_
40
44
  * | `subagent_type` (Task param; plan mode + CreatePlan/SwitchMode) | cursor |
41
45
  * | `question`, or `task_subagent` (task tool, singular subagent, no batch) | opencode |
42
46
  * | `task_agent_batch` (task tool, agent/tasks[] batch), `ask`, `hub` | omp |
47
+ * | `subagent` (dsh's model-facing delegation tool) | dsh |
43
48
  * | `Agent`/`AskUserQuestion`/`EnterPlanMode` + `AgentSwarm` (Kimi-only) | kimi |
44
49
  * | `Agent`/`AskUserQuestion`/`EnterPlanMode`/`TodoWrite`, no `AgentSwarm` | zcode |
45
50
  * | `/plan`, `/goal`; Goal tools; `functions.*` namespaces; `tool_search` | codex |
46
51
  *
47
- * Order matters: cursor → opencode → omp → kimi → zcode → codex — the
48
- * sharpest Task-based split is `subagent_type` (Cursor) vs `subagent`
49
- * (OpenCode) vs `agent`/`tasks[]` (omp). Still ambiguous `"ambiguous"`
50
- * (prompt judgment stays in the skill).
52
+ * Order matters: cursor → opencode → omp → dsh → kimi → zcode → codex — the
53
+ * sharpest Task-based split is `subagent_type` (Cursor) vs `task_subagent`
54
+ * (OpenCode) vs `agent`/`tasks[]` (omp); dsh's `subagent` tool (roadmap §4
55
+ * D5) collides with no other row, so it sits with the agent-tool hosts.
56
+ * (`mstar-host` skill prose still says colloquial `subagent` for OpenCode —
57
+ * disambiguate it to `task_subagent` in the same upstream PR that carries
58
+ * this row, together with the CLI `HOST_SIGNALS` list.)
59
+ * Still ambiguous → `"ambiguous"` (prompt judgment stays in the skill).
51
60
  */
52
61
  export declare function detectHost(signals: readonly ToolSignal[]): DetectResult;
53
62
  /** Skill name + optional skill-relative path for skill-root resolution. */
@@ -69,7 +78,8 @@ export type SkillRootPaths = {
69
78
  * | codex | `skills/<name>[/<rel>]` (plugin-mounted; project command skills under `.agents/skills/<name>/`) |
70
79
  * | opencode | `harness-skills/<name>[/<rel>]` (package-internal via `@mstar-harness/opencode` — never `process.cwd()/skills/`) |
71
80
  * | kimi / zcode | `./skills/<name>[/<rel>]` (plugin mount from the installed plugin root) |
72
- * | pi / dsh | deferredno plugin API in v1 (roadmap §8.4) |
81
+ * | dsh | `$DSH_BUNDLED_SKILL_DIR/<name>[/<rel>]` (skill-local bundled root dsh-skill-local `bundledSkillDir` default; the single canonical mount per roadmap D6. Frozen this iteration; local dev mounts the mirror `skills/` via `customSkillDirs` instead, but the canonical published form stays the bundled root) |
82
+ * | pi | deferred — no plugin API in v1 (roadmap §8.4) |
73
83
  */
74
84
  export declare function resolveSkillRoot(host: HostId, paths: SkillRootPaths): string;
75
85
  /**
@@ -79,8 +89,8 @@ export declare function resolveSkillRoot(host: HostId, paths: SkillRootPaths): s
79
89
  * rule: skill text remains authoritative; the engine only returns results
80
90
  * the caller chooses to honor). `log` is required — adapters must be able to
81
91
  * report. No concrete adapters ship in the engine: opencode binds via its
82
- * own plugin code, omp via the command layer; pi/dsh adapters are deferred
83
- * until their plugin APIs land.
92
+ * own plugin code, omp via the command layer, dsh via the dsh plugin (this
93
+ * iteration); pi stays deferred until its plugin API lands.
84
94
  */
85
95
  export interface HostAdapter {
86
96
  host: "opencode" | "omp" | "pi" | "dsh" | "cursor" | "codex" | "kimi" | "zcode";
package/dist/index.d.ts CHANGED
@@ -42,8 +42,8 @@ export type { AuditCategory, AuditEffort, AuditFinding, AuditPriority, AuditRisk
42
42
  export { AUDIT_CATEGORIES, AUDIT_EFFORTS, AUDIT_PRIORITIES, AUDIT_RISKS, redactSecrets, scaffoldAuditPlan, validateAuditStatusBlocks, } from "./audit.js";
43
43
  export type { ReferenceCheckResult } from "./compound.js";
44
44
  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";
45
- export type { PlanQualityFinding, PlanQualityResult, SimplifyMarker, TemporaryMarker, TemporaryMarkerResult, } from "./lint.js";
46
- export { assertSddTddTriple, findSimplifyMarkers, findTemporaryMarkers, lintSkillFrontmatter, lintStrategySections, planQualityBar, } from "./lint.js";
45
+ export type { EphemeralCitation, PlanQualityFinding, PlanQualityResult, SimplifyMarker, TemporaryMarker, TemporaryMarkerResult, } from "./lint.js";
46
+ export { assertSddTddTriple, findEphemeralCitations, findSimplifyMarkers, findTemporaryMarkers, lintSkillFrontmatter, lintStrategySections, planQualityBar, } from "./lint.js";
47
47
  export type { DevTrackParam, QcReviewerParam, RoleFamily, RoleMappingEntry, RoleMappingOptions, } from "./roles.js";
48
48
  export { DEV_TRACK_PARAMS, QC_REVIEWER_PARAMS, ROLE_MAPPING, SHARED_FAMILIES, lintLoadOrder, validateRoleMapping, } from "./roles.js";
49
49
  export type { DetectResult, HostAdapter, HostId, SkillRootPaths, ToolSignal } from "./host.js";
package/dist/lint.d.ts CHANGED
@@ -27,6 +27,10 @@
27
27
  * contract (not a workflow summary), third person.
28
28
  * - STRATEGY.md structure: `mstar-strategy` SKILL.md § STRATEGY.md structure —
29
29
  * six required sections.
30
+ * - Ephemeral citations: knowledge `conventions/skill-content-porting-discipline.md`
31
+ * §3 ("No ephemeral citations in durable skill text") + session evaluation
32
+ * 2026-08-16 discrimination contract — concrete task-artifact references
33
+ * and SDD deeplinks are ephemeral; placeholder forms are not.
30
34
  *
31
35
  * Enforcement depth: roadmap §8.5 C4 — v1 lints are non-blocking
32
36
  * `ValidationResult`s; callers surface them as warnings.
@@ -95,6 +99,45 @@ export type TemporaryMarkerResult = GateResult & {
95
99
  * carry); do not special-case it without a real FP report.
96
100
  */
97
101
  export declare function findTemporaryMarkers(fileText: string): TemporaryMarkerResult;
102
+ /**
103
+ * An ephemeral citation found in durable skill text: a concrete reference to
104
+ * a per-task artifact or an SDD deeplink that survives nothing (knowledge
105
+ * conventions/skill-content-porting-discipline.md §3 — "No ephemeral
106
+ * citations in durable skill text": a calibration line citing an SDD task
107
+ * report violates standalone + survives nothing; instances/examples cite
108
+ * in-repo artifacts only).
109
+ */
110
+ export type EphemeralCitation = {
111
+ /** 1-based line number of the citation. */
112
+ line: number;
113
+ /** The matched citation token (artifact name or deeplink prefix). */
114
+ match: string;
115
+ /** `task-artifact`: `task-<digits>-(brief|report|fix-report|diff)`;
116
+ * `sdd-deeplink`: `.mstar/sdd/` / `.agents/sdd/` + a concrete first
117
+ * segment. */
118
+ kind: "task-artifact" | "sdd-deeplink";
119
+ };
120
+ /**
121
+ * Find ephemeral citations in skill text (knowledge
122
+ * conventions/skill-content-porting-discipline.md §3 + session evaluation
123
+ * 2026-08-16 discrimination contract).
124
+ *
125
+ * Discrimination (HARD — zero false positives on the skills corpus):
126
+ * - `task-<digits>-(brief|report|fix-report|diff)` with 1+ digits is a
127
+ * concrete instance → reported (`task-2-report`, `task-1.diff`).
128
+ * Placeholders (`task-N-brief`, `task-N-report`, `<plan-id>`,
129
+ * `{SDD_DIR}/task-N-report.md`) never match.
130
+ * - `.mstar/sdd/<segment>` / `.agents/sdd/<segment>` with a concrete first
131
+ * segment (`20260815-x`) → reported; `<plan-id>` / `{SDD_DIR}` segments
132
+ * are template forms → never match.
133
+ *
134
+ * Discovery only — a finder returning an array, same shape as
135
+ * `findSimplifyMarkers`, NOT a GateResult; callers wrap findings into
136
+ * `ViolationResult`s (codes `skill.ephemeral.task-artifact` /
137
+ * `skill.ephemeral.sdd-deeplink`). Citations are reported line by line in
138
+ * 1-based line order, source order within a line.
139
+ */
140
+ export declare function findEphemeralCitations(skillText: string): EphemeralCitation[];
98
141
  /**
99
142
  * Assert the SDD TDD triple is present in a `task-N-report.md` text
100
143
  * (mstar-coding-behavior § Integration Notes — "completion evidence must
package/dist/path.d.ts CHANGED
@@ -10,15 +10,32 @@ export type ResolveHarnessDirOptions = {
10
10
  * (the caller may scaffold it).
11
11
  */
12
12
  harnessDir?: string;
13
+ /**
14
+ * Workspace-root stop boundary (roadmap §7c / plan
15
+ * 20260810-harness-root-boundary). The upward probe keeps walking only
16
+ * while `dir` is at or below this root — a harness dir above it is never
17
+ * returned (the `~/.mstar` global-collision defect is the special case).
18
+ * Resolved against `startDir` when relative. When omitted, the default
19
+ * boundary is the git top-level of `startDir` (sync `git rev-parse
20
+ * --show-cdup`; on failure / non-git start it falls back to
21
+ * `startDir` itself — a non-git start probes only itself, never upward;
22
+ * deliberate tightening). The boundary is an explicit caller value: the
23
+ * engine git-probes only for this default resolution, never during the
24
+ * walk.
25
+ */
26
+ workspaceRoot?: string;
13
27
  };
14
28
  /**
15
29
  * Resolve `{HARNESS_DIR}` per plan-conventions § {HARNESS_DIR} 解析顺序
16
30
  * (find-first-stop): `.mstar/` → `.agents/` → `.plans/`/`plans/`, walking up
17
- * from `startDir`. Harness candidates are dir-existence (the empty-dir rule
18
- * applies to `{SPECS_DIR}` only). An explicit override via `opts.harnessDir`
19
- * or `MSTAR_HARNESS_DIR` wins over probing.
31
+ * from `startDir` but NEVER above the workspace root (`opts.workspaceRoot`,
32
+ * default = git top-level of `startDir`). Harness candidates are
33
+ * dir-existence (the empty-dir rule applies to `{SPECS_DIR}` only). An
34
+ * explicit override via `opts.harnessDir` or `MSTAR_HARNESS_DIR` wins over
35
+ * probing and short-circuits before any boundary logic.
20
36
  *
21
- * Returns the absolute harness dir, or `null` when no candidate exists.
37
+ * Returns the absolute harness dir, or `null` when no candidate exists
38
+ * within the workspace boundary.
22
39
  */
23
40
  export declare function resolveHarnessDir(startDir?: string, opts?: ResolveHarnessDirOptions): string | null;
24
41
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mstar-harness/engine",
3
- "version": "2.1.1",
3
+ "version": "2.3.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": {