@uniqbit/mate-core 0.18.3-canary.1 → 0.18.4

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 (127) hide show
  1. package/claude-plugin/hooks/hooks.json +10 -1
  2. package/claude-plugin/hooks/session-banner.mjs +4 -3
  3. package/claude-plugin/hooks/session-prepare.mjs +9 -0
  4. package/opencode-plugin/src/add-dir.ts +10 -76
  5. package/opencode-plugin/src/audience-layer.ts +91 -0
  6. package/opencode-plugin/src/companion.ts +70 -18
  7. package/opencode-plugin/src/launch-environment.ts +29 -0
  8. package/opencode-plugin/src/server.ts +60 -8
  9. package/package.json +3 -3
  10. package/src/cli/commands/artifact/finish/openspec.ts +1 -1
  11. package/src/cli/commands/artifact/pending/discovery.ts +60 -21
  12. package/src/cli/commands/cap/index-cmd.ts +2 -11
  13. package/src/cli/commands/companion/link.ts +16 -11
  14. package/src/cli/commands/companion/list.ts +4 -5
  15. package/src/cli/commands/companion/open.ts +4 -5
  16. package/src/cli/commands/companion/tui.ts +1 -1
  17. package/src/cli/commands/doctor.ts +87 -5
  18. package/src/cli/commands/install.ts +8 -0
  19. package/src/cli/commands/report/renderer.ts +9 -19
  20. package/src/cli/commands/session/prepare.ts +119 -0
  21. package/src/cli/commands/shared/companion-selection.ts +2 -2
  22. package/src/cli/commands/studio/reports.ts +0 -1
  23. package/src/cli/commands/studio/server.ts +6 -2
  24. package/src/cli/commands/studio/terminal-launch.ts +16 -7
  25. package/src/cli/commands/unwrap.ts +28 -3
  26. package/src/cli/commands/wrap.ts +79 -60
  27. package/src/cli/companion-selector.tsx +1 -1
  28. package/src/cli/main.ts +17 -0
  29. package/src/cli/usage.ts +5 -2
  30. package/src/hooks/session-banner.ts +19 -10
  31. package/src/hooks/session-guidance.ts +20 -2
  32. package/src/hooks/session-prepare.ts +101 -0
  33. package/src/lib/fs-utils.ts +13 -0
  34. package/src/lib/orchestrator/adapters/base.ts +45 -9
  35. package/src/lib/orchestrator/adapters/claude.ts +241 -30
  36. package/src/lib/orchestrator/adapters/opencode.ts +72 -34
  37. package/src/lib/orchestrator/companion-git-sync.ts +28 -1
  38. package/src/lib/orchestrator/companion-hub.ts +2 -1
  39. package/src/lib/orchestrator/config-store.ts +83 -1
  40. package/src/lib/orchestrator/framework-context.ts +5 -29
  41. package/src/lib/orchestrator/launcher.ts +36 -171
  42. package/src/lib/orchestrator/openspec-store.ts +202 -0
  43. package/src/lib/orchestrator/projection-claude-entry.ts +17 -6
  44. package/src/lib/orchestrator/projection-claude-skills.ts +126 -41
  45. package/src/lib/orchestrator/projection-entries.ts +124 -11
  46. package/src/lib/orchestrator/projection-opencode-copies.ts +230 -0
  47. package/src/lib/orchestrator/projection-openspec-store.ts +164 -0
  48. package/src/lib/orchestrator/projection-record.ts +7 -1
  49. package/src/lib/orchestrator/projection-runtime-documents.ts +83 -130
  50. package/src/lib/orchestrator/projection-types.ts +47 -34
  51. package/src/lib/orchestrator/repository-link.ts +473 -0
  52. package/src/lib/orchestrator/root-context.ts +6 -2
  53. package/src/lib/orchestrator/session-prepare.ts +503 -0
  54. package/src/lib/orchestrator/setup-compatibilities.ts +1 -1
  55. package/src/lib/orchestrator/setup-preflight.ts +3 -1
  56. package/src/lib/orchestrator/working-repo-projection.ts +73 -160
  57. package/src/lib/orchestrator/workspace-inventory.ts +2 -2
  58. package/src/lib/orchestrator/wrap-pass.ts +222 -0
  59. package/src/lib/runtime-directory-refresh.ts +186 -0
  60. package/src/lib/update-checker.ts +6 -0
  61. package/src/opencode/companion-hooks.ts +30 -135
  62. package/src/opencode/companion-policy.ts +26 -10
  63. package/src/opencode/index.ts +1 -7
  64. package/src/opencode/projected-guidance.ts +6 -13
  65. package/src/opencode/session-notes.ts +76 -0
  66. package/src/opencode/session-prepare.ts +139 -0
  67. package/src/opencode/tui.tsx +56 -9
  68. package/src/runtime/audience-layer.ts +84 -0
  69. package/src/runtime/claude-delivery-plan.ts +73 -0
  70. package/src/runtime/claude-hook-group.ts +21 -0
  71. package/src/runtime/companion-lock.ts +121 -0
  72. package/src/runtime/companion-sync.ts +66 -9
  73. package/src/runtime/guidance.ts +17 -0
  74. package/src/runtime/opencode-audience-layer.ts +176 -0
  75. package/src/runtime/policy.ts +14 -1
  76. package/src/runtime/projection.ts +1 -1
  77. package/src/runtime/runtime-directory.ts +43 -0
  78. package/src/runtime/runtime-document-manifest.ts +352 -0
  79. package/src/runtime/session-prepare-runner.ts +148 -0
  80. package/src/runtime/wrap-state.ts +94 -0
  81. package/src/runtime/write-atomic.ts +27 -0
  82. package/src/templates/capabilities/openspec-cap/mate-minimal/schema.yaml +2 -1
  83. package/src/templates/capabilities/openspec-cap/mate-v1/schema.yaml +2 -1
  84. package/src/templates/capabilities/openspec-cap/openspec-conventions.yaml +5 -0
  85. package/src/templates/capabilities/react-doctor/mate-react-doctor/SKILL.md +84 -0
  86. package/src/templates/capabilities/react-doctor/{skill → mate-react-doctor}/references/explain.md +18 -15
  87. package/src/templates/mate-skills/agents/mate-openspec-backfill/SKILL.md +45 -29
  88. package/src/templates/mate-skills/agents/mate-show-me/SKILL.md +1 -1
  89. package/src/templates/mate-skills/claude/mate-openspec-backfill/SKILL.md +45 -29
  90. package/src/templates/mate-skills/claude/mate-show-me/SKILL.md +1 -1
  91. package/src/tools/setup/__snapshots__/runtime-surface-golden.test.ts.snap +850 -499
  92. package/src/tools/setup/audience-isolation.fixture.ts +325 -0
  93. package/src/tools/setup/audience-layer.ts +207 -0
  94. package/src/tools/setup/audience-registrations.ts +52 -0
  95. package/src/tools/setup/capabilities/openspec.ts +9 -0
  96. package/src/tools/setup/capabilities/react-doctor.ts +34 -50
  97. package/src/tools/setup/capabilities/tokensave.ts +11 -0
  98. package/src/tools/setup/contribution-ledger.ts +207 -0
  99. package/src/tools/setup/dynamic-plugins/audiences.ts +89 -9
  100. package/src/tools/setup/dynamic-plugins/declarations.ts +3 -2
  101. package/src/tools/setup/dynamic-plugins/hydrate.ts +8 -5
  102. package/src/tools/setup/engine.ts +152 -10
  103. package/src/tools/setup/isolation-diagnostics.ts +139 -0
  104. package/src/tools/setup/mate.ts +1 -1
  105. package/src/tools/setup/plugin.ts +19 -4
  106. package/src/tools/setup/plugins/gitignore.ts +7 -1
  107. package/src/tools/setup/providers/claude-format.ts +11 -18
  108. package/src/tools/setup/providers/claude-plugin-hooks.ts +61 -24
  109. package/src/tools/setup/providers/claude.ts +50 -8
  110. package/src/tools/setup/providers/opencode.ts +10 -0
  111. package/src/tools/setup/registry.ts +24 -2
  112. package/src/tools/setup/runtime-documents.ts +152 -32
  113. package/src/tools/setup/surface-target.ts +13 -4
  114. package/src/tools/setup/upgrade-sweep.ts +153 -0
  115. package/src/tools/setup/working-repo-cleanup.ts +2 -0
  116. package/src/tools/setup/working-repo-local-state.ts +91 -14
  117. package/src/tools/setup.ts +11 -8
  118. package/wrappers/bin/openspec +42 -1
  119. package/src/lib/orchestrator/companion-registry-reader.ts +0 -37
  120. package/src/lib/orchestrator/companion-resolver.ts +0 -179
  121. package/src/lib/orchestrator/companion-store.ts +0 -94
  122. package/src/lib/orchestrator/repo-local-registry.ts +0 -143
  123. package/src/templates/capabilities/react-doctor/claude/hooks/react-doctor.sh +0 -144
  124. package/src/templates/capabilities/react-doctor/skill/SKILL.md +0 -51
  125. package/src/templates/report-assets/README.md +0 -32
  126. package/src/templates/report-assets/mermaid.LICENSE +0 -21
  127. package/src/templates/report-assets/mermaid.min.js +0 -4376
@@ -0,0 +1,148 @@
1
+ import { spawn } from "node:child_process";
2
+
3
+ /**
4
+ * Client side of `mate session prepare`, shared by the OpenCode plugin and the
5
+ * Claude session-prepare hook: spawned through the runtime-directory launcher,
6
+ * so a crash or stale module never reaches the session that asked.
7
+ */
8
+
9
+ export interface PrepareRun {
10
+ ok: boolean;
11
+ /** Failed or warned steps, already worded for the operator. */
12
+ notes: string[];
13
+ /** The sync fast-forwarded the companion and re-projected it. */
14
+ changed?: boolean;
15
+ }
16
+
17
+ export interface PrepareRequest {
18
+ /** `--sync`: fast-forward the companion, re-project when it moved. */
19
+ sync: boolean;
20
+ /** Stop waiting after this long; the prepare itself is never terminated. */
21
+ timeoutMs?: number;
22
+ }
23
+
24
+ export type PrepareRunner = (request: PrepareRequest) => Promise<PrepareRun>;
25
+
26
+ /** A guard against a hung local phase; it contains no network work. */
27
+ export const PREPARE_TIMEOUT_MS = 30_000;
28
+
29
+ interface PrepareJson {
30
+ ok?: boolean;
31
+ steps?: Array<{ step?: string; outcome?: string; detail?: string }>;
32
+ warnings?: string[];
33
+ companionChanged?: boolean;
34
+ }
35
+
36
+ export function parsePrepareOutput(stdout: string, exitCode: number | null): PrepareRun {
37
+ let parsed: PrepareJson | null = null;
38
+ try {
39
+ parsed = JSON.parse(stdout) as PrepareJson;
40
+ } catch {
41
+ /* not JSON: the command failed before it could report */
42
+ }
43
+ if (!parsed) {
44
+ const tail = stdout.trim().split("\n").slice(-3).join(" ");
45
+ return { ok: false, notes: [`exited ${exitCode ?? "abnormally"}${tail ? `: ${tail}` : ""}`] };
46
+ }
47
+ const notes = [
48
+ ...(parsed.steps ?? [])
49
+ .filter((step) => step.outcome === "failed")
50
+ .map((step) => `${step.step}: ${step.detail ?? "failed"}`),
51
+ ...(parsed.warnings ?? []),
52
+ ];
53
+ const changed = parsed.companionChanged === true;
54
+ return { ok: parsed.ok === true && exitCode === 0, notes, ...(changed ? { changed } : {}) };
55
+ }
56
+
57
+ function quoteForCmd(arg: string): string {
58
+ return /[\s"&|<>^()]/.test(arg) ? `"${arg.replaceAll('"', '""')}"` : arg;
59
+ }
60
+
61
+ /**
62
+ * How to run the runtime launcher without a shell, so a path with spaces stays
63
+ * one argument. Windows cannot execute the `.cmd` launcher directly, so it goes
64
+ * through `cmd.exe` with the line quoted here rather than by a shell.
65
+ */
66
+ export function launcherInvocation(
67
+ mateBin: string,
68
+ args: readonly string[],
69
+ platform: NodeJS.Platform = process.platform,
70
+ ): { command: string; args: string[]; verbatim: boolean } {
71
+ if (platform !== "win32") return { command: mateBin, args: [...args], verbatim: false };
72
+ const line = [mateBin, ...args].map(quoteForCmd).join(" ");
73
+ return { command: "cmd.exe", args: ["/d", "/s", "/c", `"${line}"`], verbatim: true };
74
+ }
75
+
76
+ /** `stdio` is the caller's, so `spawn` still types the pipes it asked for. */
77
+ function prepareSpawn(mateBin: string, repoPath: string, sync: boolean) {
78
+ const invocation = launcherInvocation(mateBin, [
79
+ "session",
80
+ "prepare",
81
+ "--repo",
82
+ repoPath,
83
+ "--json",
84
+ ...(sync ? ["--sync"] : []),
85
+ ]);
86
+ const options = {
87
+ cwd: repoPath,
88
+ shell: false,
89
+ detached: sync,
90
+ windowsHide: true,
91
+ windowsVerbatimArguments: invocation.verbatim,
92
+ };
93
+ return { command: invocation.command, args: invocation.args, options };
94
+ }
95
+
96
+ /**
97
+ * Spawns `mate session prepare` through the runtime-directory launcher. The
98
+ * sync is detached so it outlives a closing location: a pull must not be cut
99
+ * off, and the companion lock stays held until its Git work ends.
100
+ */
101
+ export function spawnPrepareRunner(mateBin: string, repoPath: string): PrepareRunner {
102
+ return ({ sync, timeoutMs }) =>
103
+ new Promise((resolve) => {
104
+ const { command, args, options } = prepareSpawn(mateBin, repoPath, sync);
105
+ const child = spawn(command, args, { ...options, stdio: ["ignore", "pipe", "pipe"] });
106
+ if (sync) child.unref();
107
+ let settled = false;
108
+ const settle = (run: PrepareRun) => {
109
+ if (settled) return;
110
+ settled = true;
111
+ clearTimeout(timer);
112
+ resolve(run);
113
+ };
114
+ let stdout = "";
115
+ let stderr = "";
116
+ child.stdout.setEncoding("utf8").on("data", (chunk: string) => (stdout += chunk));
117
+ child.stderr.setEncoding("utf8").on("data", (chunk: string) => (stderr += chunk));
118
+ const timer =
119
+ timeoutMs === undefined
120
+ ? undefined
121
+ : setTimeout(() => {
122
+ /** Released, not killed, so a caller about to exit does not wait on it. */
123
+ child.unref();
124
+ for (const stream of [child.stdout, child.stderr]) {
125
+ (stream as unknown as { unref?: () => void }).unref?.();
126
+ }
127
+ settle({ ok: false, notes: [`timed out after ${timeoutMs / 1000}s`] });
128
+ }, timeoutMs);
129
+ child.once("error", (error) =>
130
+ settle({ ok: false, notes: [`cannot run ${mateBin}: ${error.message}`] }),
131
+ );
132
+ child.once("close", (code) => {
133
+ const run = parsePrepareOutput(stdout, code);
134
+ settle(run.ok || stdout.trim() ? run : { ok: false, notes: [...run.notes, stderr.trim()] });
135
+ });
136
+ });
137
+ }
138
+
139
+ /**
140
+ * The `--sync` variant for a caller about to exit: detached and unref'd with no
141
+ * pipes, so the caller's exit neither waits on it nor cuts its Git work short.
142
+ */
143
+ export function startDetachedPrepareSync(mateBin: string, repoPath: string): void {
144
+ const { command, args, options } = prepareSpawn(mateBin, repoPath, true);
145
+ const child = spawn(command, args, { ...options, stdio: "ignore" });
146
+ child.once("error", () => {});
147
+ child.unref();
148
+ }
@@ -0,0 +1,94 @@
1
+ import fs from "node:fs";
2
+
3
+ import { hookGroupKey, type ClaudeHookGroup } from "./claude-hook-group";
4
+ import {
5
+ CLAUDE_SETTINGS_DOCUMENT,
6
+ EMPTY_MANIFEST,
7
+ OPENCODE_CONFIG_DOCUMENT,
8
+ OPENCODE_MCP_SERVERS_AT,
9
+ copies,
10
+ documents,
11
+ links,
12
+ manifestPath,
13
+ parseManifest,
14
+ parseManifestStrict,
15
+ readManifestText,
16
+ recordedListValues,
17
+ recordedAudience,
18
+ recordedMapKeys,
19
+ type Manifest,
20
+ type WrapRuntime,
21
+ } from "./runtime-document-manifest";
22
+
23
+ /**
24
+ * Wrap State: the one read model over the runtime document manifest for "which
25
+ * runtimes are wrapped" and "which Claude hook groups does the wrap carry".
26
+ * Mate's own plugin groups are not folded in; `isMateGuardHookGroup` owns those.
27
+ */
28
+ export interface WrapState {
29
+ readonly runtimes: ReadonlySet<WrapRuntime>;
30
+ readonly carriedClaudeHookGroups: readonly ClaudeHookGroup[];
31
+ carriesClaudeHookGroup(group: ClaudeHookGroup): boolean;
32
+ /** MCP server names the wrap declared in `.opencode/opencode.json`; user servers are not listed. */
33
+ readonly openCodeMcpServers: readonly string[];
34
+ /** The audience the checkout is wrapped for; `null` for base only. */
35
+ readonly audience: string | null;
36
+ }
37
+
38
+ export const WRAP_RUNTIMES: readonly WrapRuntime[] = ["claude", "opencode"];
39
+
40
+ /** Wrapped for a runtime: a document, a copy or a link is recorded for it. */
41
+ function wrappedFor(manifest: Manifest, runtime: WrapRuntime): boolean {
42
+ return (
43
+ documents(manifest, runtime).length > 0 ||
44
+ Object.keys(copies(manifest, runtime)).length > 0 ||
45
+ Object.keys(links(manifest, runtime)).length > 0
46
+ );
47
+ }
48
+
49
+ function isHookGroup(value: unknown): value is ClaudeHookGroup {
50
+ return typeof value === "object" && value !== null && !Array.isArray(value);
51
+ }
52
+
53
+ export function wrapStateOf(manifest: Manifest): WrapState {
54
+ const carried = recordedListValues(manifest, CLAUDE_SETTINGS_DOCUMENT, ["hooks"]).filter(
55
+ isHookGroup,
56
+ );
57
+ const carriedKeys = new Set(carried.map(hookGroupKey));
58
+ return {
59
+ runtimes: new Set(WRAP_RUNTIMES.filter((runtime) => wrappedFor(manifest, runtime))),
60
+ carriedClaudeHookGroups: carried,
61
+ carriesClaudeHookGroup: (group) => carriedKeys.has(hookGroupKey(group)),
62
+ openCodeMcpServers: recordedMapKeys(
63
+ manifest,
64
+ OPENCODE_CONFIG_DOCUMENT,
65
+ OPENCODE_MCP_SERVERS_AT,
66
+ ),
67
+ audience: recordedAudience(manifest),
68
+ };
69
+ }
70
+
71
+ /** Where no Projection Root resolves: nothing is wrapped. */
72
+ export const NOT_WRAPPED: WrapState = wrapStateOf(EMPTY_MANIFEST);
73
+
74
+ /**
75
+ * Lenient, for session-start readers that must never fail: an absent or
76
+ * unreadable manifest reads as not wrapped.
77
+ */
78
+ export function readWrapStateSync(repoPath: string): WrapState {
79
+ let raw: string | null = null;
80
+ try {
81
+ raw = fs.readFileSync(manifestPath(repoPath), "utf8");
82
+ } catch {
83
+ /* absent or unreadable: not wrapped */
84
+ }
85
+ return wrapStateOf(parseManifest(raw));
86
+ }
87
+
88
+ /**
89
+ * Strict, for writers: a manifest that exists but cannot be read or parsed
90
+ * throws, so a rewrite never strips wrap-placed content it could not see.
91
+ */
92
+ export async function readWrapState(repoPath: string): Promise<WrapState> {
93
+ return wrapStateOf(parseManifestStrict(await readManifestText(repoPath)));
94
+ }
@@ -0,0 +1,27 @@
1
+ import crypto from "node:crypto";
2
+ import fs from "node:fs/promises";
3
+
4
+ /**
5
+ * Writes via a sibling temp file + `fs.rename()` so an interrupted write cannot
6
+ * leave a half-written document behind. A reader racing this one — a Claude
7
+ * Code session holding `~/.claude.json` open — observes the old file or the new
8
+ * one, never a truncated one.
9
+ */
10
+ export async function writeAtomic(
11
+ target: string,
12
+ contents: string | Buffer,
13
+ mode?: number,
14
+ ): Promise<void> {
15
+ const tempPath = `${target}.${crypto.randomUUID()}.tmp`;
16
+ try {
17
+ await fs.writeFile(
18
+ tempPath,
19
+ contents,
20
+ mode === undefined ? "utf8" : { encoding: "utf8", mode },
21
+ );
22
+ await fs.rename(tempPath, target);
23
+ } catch (error) {
24
+ await fs.rm(tempPath, { force: true }).catch(() => {});
25
+ throw error;
26
+ }
27
+ }
@@ -1,5 +1,5 @@
1
1
  name: mate-minimal
2
- version: 2
2
+ version: 3
3
3
  description: Mate's short OpenSpec workflow for one-Area, low-risk changes - specs → tasks
4
4
  artifacts:
5
5
  - id: specs
@@ -55,3 +55,4 @@ apply:
55
55
  instruction: |
56
56
  Read the delta specs and work through pending tasks only within the affected Areas. Mark complete as you go.
57
57
  Pause if you hit blockers or need clarification.
58
+ Before reporting the change complete, follow the verification rules in `openspec/mate-conventions.yaml`.
@@ -1,5 +1,5 @@
1
1
  name: mate-v1
2
- version: 8
2
+ version: 9
3
3
  description: Mate's complete OpenSpec workflow - proposal → specs → design → tasks
4
4
  artifacts:
5
5
  - id: proposal
@@ -242,3 +242,4 @@ apply:
242
242
  instruction: |
243
243
  Read context files and work through pending tasks only within the proposal's affected Areas. Mark complete as you go.
244
244
  Pause if you hit blockers or need clarification.
245
+ Before reporting the change complete, follow the verification rules in `openspec/mate-conventions.yaml`.
@@ -15,6 +15,11 @@ rules: |
15
15
  - Every requirement MUST carry an Area marker whose values appear in the delta frontmatter. This survives archive, which copies requirement blocks but discards delta frontmatter.
16
16
  - A canonical spec uses flat frontmatter: type: spec, capability, scalar repository, areas list, and tags: [openspec/spec]. It MUST NOT retain a nested scopes list or delta-only change and delta tags.
17
17
  - To project a delta, all its scope entries MUST name the same repository. Emit that repository as the canonical scalar and distinct Areas as areas in their original order. If repositories differ, split the capability before archive.
18
+ verification: |
19
+ Verification skills check a change before it is reported complete. They apply only when the skill is available in the current session; a skill that is not available adds no task and no step.
20
+ - mate-react-doctor: when writing tasks, end the task list of every Area whose tasks change React code (components, hooks, or other .tsx/.jsx modules) with one verification task, numbered like its neighbours, for example `- [ ] N.M Verify React changes with React Doctor [skill:mate-react-doctor]`. Areas whose tasks do not change React code get no verification task.
21
+ - During apply, a tagged verification task is worked like any other task by invoking its skill.
22
+ - Before reporting a change complete, check every Area that changed React code: if its tagged verification task is already checked, do nothing more; otherwise invoke the skill once for that Area.
18
23
  example_delta_frontmatter: |
19
24
  ---
20
25
  type: delta-spec
@@ -0,0 +1,84 @@
1
+ ---
2
+ name: mate-react-doctor
3
+ description: Verify React changes with Mate's pinned React Doctor. Use when a task is tagged `[skill:mate-react-doctor]`, when the shared Mate verification rules call for it before finishing an OpenSpec change, or when the user asks to scan, check, or triage React diagnostics for the current changes.
4
+ metadata:
5
+ credits:
6
+ skill: react-doctor
7
+ author: Million Software, Inc.
8
+ url: "https://github.com/millionco/react-doctor"
9
+ ---
10
+
11
+ # Mate React Doctor
12
+
13
+ Scan the React code the current change touched, fix the regressions this change introduced, and report the rest. React Doctor covers correctness, accessibility, performance, security, and maintainability.
14
+
15
+ ## Mate Workflow
16
+
17
+ - Run only Mate's pinned React Doctor binary. Never run `npx react-doctor`, never install another version, and never fetch remote prompts or playbooks.
18
+ - Scan the Working Repository, not the Companion Repository.
19
+ - Fix only what this change introduced. Leave findings that existed before the change alone.
20
+ - Never create issues, commits, or pull requests.
21
+ - Run `mate cap index --tokensave` after code changes.
22
+
23
+ ## Steps
24
+
25
+ ### 1. Resolve the binary
26
+
27
+ Print the path first, then call the absolute path it prints, so the command matches Mate's pre-seeded permission:
28
+
29
+ ```bash
30
+ printf '%s\n' "$MATE_REACT_DOCTOR_BIN_PATH"
31
+ ```
32
+
33
+ If the variable is empty or the path is not executable, stop. Report "React Doctor skipped: the pinned binary is unavailable (MATE_REACT_DOCTOR_BIN_PATH is not set; is the react-doctor capability enabled?)". A verification task may be checked with that note.
34
+
35
+ ### 2. Choose the base
36
+
37
+ Compare against the point where the change's branch left the default branch, so work already committed during apply is included:
38
+
39
+ ```bash
40
+ default_branch=$(git symbolic-ref --quiet --short refs/remotes/origin/HEAD 2>/dev/null || echo origin/main)
41
+ git merge-base HEAD "$default_branch"
42
+ ```
43
+
44
+ If no merge-base resolves, or HEAD is on the default branch, use `HEAD` (uncommitted changes only).
45
+
46
+ ### 3. Scan
47
+
48
+ Run from the root of the Area being verified (the package that holds the React code):
49
+
50
+ ```bash
51
+ <absolute-bin> --yes --verbose --scope changed --base <base> --no-score
52
+ ```
53
+
54
+ Add `--include-untracked` when the change created new files that are not committed yet.
55
+
56
+ ### 4. Separate environment problems from findings
57
+
58
+ These outputs mean the scan could not run, not that the code is wrong. Report a skip with the reason and change nothing:
59
+
60
+ | Output contains | Meaning |
61
+ | --------------------------- | ----------------------------------------- |
62
+ | `No React dependency found` | The Area is not a React project |
63
+ | `Could not resolve config` | The React Doctor config could not be read |
64
+ | `is not a git repository` | The scan root is outside a git checkout |
65
+
66
+ ### 5. Fix regressions introduced by the change
67
+
68
+ For each remaining finding:
69
+
70
+ 1. Confirm it is in code this change added or modified (`git diff <base> -- <file>`). If the finding predates the change, leave it and do not list it as a regression.
71
+ 2. Understand the rule before changing code: `<absolute-bin> rules explain <rule>`.
72
+ 3. Make the smallest fix that satisfies the rule and keeps the change's intended behavior. Run the relevant tests.
73
+
74
+ Then run the scan from step 3 again. Repeat until nothing introduced by the change remains, or only findings you cannot fix in scope are left.
75
+
76
+ ### 6. Report
77
+
78
+ Finish with a short summary:
79
+
80
+ - **Result:** passed, fixed (N findings), skipped (reason), or findings left.
81
+ - **Fixed:** rule and file:line for each fix.
82
+ - **Left:** rule, file:line, and why it was not fixed here. Typical reasons are that it needs a design decision, changes behavior outside the change's scope, or is a false positive. Do not file these anywhere.
83
+
84
+ Do not disable or downgrade rules to make a scan pass. Change the React Doctor configuration only when the user asks. For that, follow [references/explain.md](references/explain.md).
@@ -1,19 +1,22 @@
1
1
  # Explaining and configuring rules
2
2
 
3
3
  Explain React Doctor rules and edit `doctor.config.*` safely. Use this when a user
4
- wants to understand a rule or change which rules run — not for fixing diagnostics
5
- (that is the main `react-doctor` skill / `/doctor`).
4
+ wants to understand a rule or change which rules run, not for fixing diagnostics
5
+ (that is the main `mate-react-doctor` workflow).
6
6
 
7
7
  Triggers: "why did this rule fire", "I disagree with this rule", "turn this rule off",
8
8
  "stop flagging X", "too noisy", "disable design rules".
9
9
 
10
+ In every command below, `<bin>` is the absolute path printed by
11
+ `printf '%s\n' "$MATE_REACT_DOCTOR_BIN_PATH"`. Never substitute `npx react-doctor`.
12
+
10
13
  ## Workflow
11
14
 
12
15
  1. Identify the rule key from the diagnostic (e.g. `react-doctor/no-array-index-as-key`).
13
16
  2. Explain it before changing anything:
14
17
 
15
18
  ```bash
16
- npx react-doctor@latest rules explain react-doctor/no-array-index-as-key
19
+ <bin> rules explain react-doctor/no-array-index-as-key
17
20
  ```
18
21
 
19
22
  3. Pick the narrowest control that matches the user's intent (see decision guide).
@@ -21,22 +24,22 @@ npx react-doctor@latest rules explain react-doctor/no-array-index-as-key
21
24
  5. Validate the change did what they wanted:
22
25
 
23
26
  ```bash
24
- npx react-doctor@latest --verbose --diff
27
+ <bin> --verbose --scope changed --no-score
25
28
  ```
26
29
 
27
30
  ## Commands
28
31
 
29
32
  ```bash
30
- npx react-doctor@latest rules list # every rule + its effective severity
31
- npx react-doctor@latest rules list --configured # only what your config changed
32
- npx react-doctor@latest rules list --category Performance # filter by category
33
- npx react-doctor@latest rules explain <rule> # why it matters + how to configure
34
- npx react-doctor@latest rules disable <rule> # rule never runs
35
- npx react-doctor@latest rules enable <rule> # turn back on at its recommended severity
36
- npx react-doctor@latest rules set <rule> warn # off | warn | error
37
- npx react-doctor@latest rules category "React Native" off # whole category
38
- npx react-doctor@latest rules ignore-tag design # skip a rule family (design, test-noise, …)
39
- npx react-doctor@latest rules unignore-tag design
33
+ <bin> rules list # every rule + its effective severity
34
+ <bin> rules list --configured # only what your config changed
35
+ <bin> rules list --category Performance # filter by category
36
+ <bin> rules explain <rule> # why it matters + how to configure
37
+ <bin> rules disable <rule> # rule never runs
38
+ <bin> rules enable <rule> # turn back on at its recommended severity
39
+ <bin> rules set <rule> warn # off | warn | error
40
+ <bin> rules category "React Native" off # whole category
41
+ <bin> rules ignore-tag design # skip a rule family (design, test-noise, …)
42
+ <bin> rules unignore-tag design
40
43
  ```
41
44
 
42
45
  Rule references accept the full key (`react-doctor/no-danger`), the bare id (`no-danger`), or a legacy key (`react/no-danger`).
@@ -69,4 +72,4 @@ export default {
69
72
 
70
73
  ## Educating the user
71
74
 
72
- When explaining a rule, lead with the "Why it matters" guidance from `rules explain` and, when they want depth, the per-rule recipe at `https://www.react.doctor/prompts/rules/<plugin>/<rule>.md`. Only after they understand it should you offer to disable it — many "bad" rules are catching real issues.
75
+ When explaining a rule, lead with the "Why it matters" guidance from `<bin> rules explain <rule>`. Only after they understand it should you offer to disable it — many "bad" rules are catching real issues.
@@ -1,26 +1,26 @@
1
1
  ---
2
2
  name: mate-openspec-backfill
3
- description: Reverse-engineer an OpenSpec spec for one existing feature and emit a ready-to-finish backfill change. Use when the user wants to backfill, document, or spec existing or legacy behavior that has no spec yet.
4
- allowed-tools: Bash(openspec:*), Bash(mate:*)
3
+ description: Reverse-engineer an OpenSpec spec for one existing feature and write it directly to the canonical spec, ready to publish. Use when the user wants to backfill, document, or spec existing or legacy behavior that has no spec yet.
4
+ allowed-tools: Bash(openspec:*), Bash(mate:*), Bash(git remote:*), Bash(git diff:*)
5
5
  license: MIT
6
6
  compatibility: Requires the mate CLI and the openspec capability enabled.
7
7
  metadata:
8
8
  author: mate
9
- version: "1.0"
9
+ version: "2.0"
10
10
  ---
11
11
 
12
12
  # Mate OpenSpec Backfill
13
13
 
14
- Create a spec for one feature that already exists in the working repository. The run ends with a standard ready-to-finish change — it never edits main specs and never finishes.
14
+ Specify one feature that already exists in the working repository. A backfill records behavior that already shipped, so the run writes the canonical spec `openspec/specs/<capability>/spec.md` directly and leaves it uncommitted. It never creates a change and never publishes; publication happens afterwards through `mate-artifact-publish`.
15
15
 
16
16
  ## Scope rules
17
17
 
18
18
  - **One named feature per run.** Refuse Area-wide or repository-wide sweeps; ask the user to name a single feature and run the skill once per feature.
19
- - **Interactive by design.** Every ambiguity and every suspected bug becomes a user question. Do not run this skill unattended.
19
+ - **Interactive by design.** Every ambiguity and every suspected bug becomes a user question, and nothing is written before the user approves the review table. Do not run this skill unattended.
20
20
 
21
21
  ## Steps
22
22
 
23
- 1. **Scope.** Map the named feature to code: entry points, callees, tests. Use whatever exploration tooling this project has enabled (code-graph or index tools when present, otherwise search and targeted reading) — assume no specific capability is installed. Then check `openspec/specs/` for an existing capability covering this domain — prefer extending it (`MODIFIED`/`ADDED` deltas) over minting a new capability id.
23
+ 1. **Scope.** Map the named feature to code: entry points, callees, tests. Use whatever exploration tooling this project has enabled (code-graph or index tools when present, otherwise search and targeted reading) — assume no specific capability is installed. Then check `openspec/specs/` for an existing capability covering this domain — prefer extending it in place over minting a new capability id.
24
24
 
25
25
  2. **Sweep.** Extract candidate behaviors and tag each finding:
26
26
  - `[test-backed]` — an existing test verifies it (strongest; scenarios translate almost directly from tests)
@@ -30,38 +30,54 @@ Create a spec for one feature that already exists in the working repository. The
30
30
  Every candidate requirement needs at least one citation: a test name or `file:line`. Docs and comments corroborate but never stand alone. `[inferred]` findings are not requirements — they become questions for step 3.
31
31
 
32
32
  3. **Ask.** Batch the open questions to the user:
33
- - Behavior that looks unintended → the user rules **spec the actual behavior** or **spec the intent** (with a follow-up fix change). Suspected bugs never silently become requirements.
34
- - `[inferred]` findings → confirm, demote to out-of-scope, or convert to a question the emitted proposal records as open.
33
+ - Behavior that looks unintended → the user rules **spec the actual behavior** or **spec the intent** (and the user may open a separate fix change). Suspected bugs never silently become requirements.
34
+ - `[inferred]` findings → confirm, demote to out-of-scope, or drop.
35
35
 
36
- 4. **Emit.** Create the change and build its artifacts in dependency order:
36
+ 4. **Review.** Before writing any file, present every drafted requirement in one table and wait for the user's approval:
37
+
38
+ | Requirement | Evidence | Citation |
39
+ | ------------------------------- | --------------- | -------------------------------- |
40
+ | Propagates the child exit code | `[test-backed]` | `launch.test.ts` › "exits with…" |
41
+ | Refuses an unknown framework id | `[code-only]` | `src/launch.ts:142` |
42
+
43
+ Write only the requirements the user approves, worded as approved. A rejected requirement is not written. This table is the only place citations and evidence tags appear — they never go into the spec.
44
+
45
+ 5. **Learn the spec format.** Never assume a spec format. Read it from the schema active in this OpenSpec root, so the run follows whichever workflow the project uses:
37
46
 
38
47
  ```bash
39
- openspec new change "backfill-spec-<capability>"
40
- openspec status --change "backfill-spec-<capability>" --json
41
- openspec instructions <artifact-id> --change "backfill-spec-<capability>" --json
48
+ openspec schema which <schema-name> --json
42
49
  ```
43
50
 
44
- The artifact set comes from the active schema (`schemaName` in the status JSON) — never assume a fixed artifact list. Follow each artifact's returned instructions and template, and state the active schema in the proposal so reviewers know which workflow produced the change. Map the backfill roles onto whatever artifacts the schema defines:
51
+ - The active schema name is the `schema:` key in `openspec/config.yaml`. When the file or key is absent, it is OpenSpec's default, `spec-driven`.
52
+ - In the returned `path`, read `schema.yaml` and find the artifact that generates the specs (`generates: specs/**/*.md`). Read its `instruction`, its `template` under `templates/`, and every reference file the instruction tells you to read (for example `openspec/mate-conventions.yaml`).
53
+ - Those sources usually describe the **delta** a change writes. You write the **canonical** spec that archive would produce from it, so apply the schema's own delta-to-canonical rules: requirement blocks go under `## Requirements` instead of `## ADDED Requirements`, and frontmatter, per-requirement markers, and `## Purpose` take the canonical shape the schema or its references define. Leave out delta-only elements such as change backlinks and change or delta tags.
54
+ - When a spec already exists under `openspec/specs/`, use it as a concrete example of the canonical shape, but follow the schema where the two disagree.
55
+ - Derive any repository identity the format needs from the Git remote (`git remote get-url origin`), and any Area or scope value from the schema's rules. Never invent a format element the schema does not define.
56
+
57
+ 6. **Write.** Write the approved requirements to `openspec/specs/<capability>/spec.md` in the format from step 5.
58
+
59
+ **New capability.** Create the file with the schema's canonical frontmatter (when it defines one), a `## Purpose` section of one or two sentences, and a `## Requirements` section holding the approved requirements with their scenarios.
45
60
 
46
- | Backfill role | Typical artifact (mate-v1 example) |
47
- | ---------------------------------------------------------------------------------------------- | ---------------------------------- |
48
- | Scope decisions and rulings from step 3 | explore-brief.md |
49
- | "Documents existing behavior, no code changes" + open questions | proposal.md |
50
- | `ADDED`/`MODIFIED` requirements, behavior only, one citation each | specs/ |
51
- | As-built evidence dossier: entry points, test inventory, `file:line` citations per requirement | design.md |
52
- | Verification checklist: one task per requirement, "confirm behavior at <citation>" | tasks.md |
61
+ **Existing capability.** Edit its `spec.md` in place: append new requirements under `## Requirements` and rewrite only the requirements the user's rulings change. Preserve every other requirement, the frontmatter, and the Purpose exactly.
53
62
 
54
- The verification task artifact MUST open with this rule, verbatim, so the applying agent sees it without knowing this skill: "These are verification tasks for a docs-only backfill change. If a requirement fails verification, update the delta spec (reword, drop, or re-cite the requirement) — never modify code in this change. A real bug found here becomes a separate fix change."
63
+ In both cases every requirement you write carries each per-requirement marker the schema requires (for example an `**Area:**` marker whose value appears in the spec's frontmatter). Requirements state observable contracts, never implementation detail ("propagates the child exit code", not "uses spawnSync"). The spec contains no citations, no `file:line` references, and no evidence tags.
64
+
65
+ 7. **Validate.** Run:
66
+
67
+ ```bash
68
+ openspec validate <capability> --type spec
69
+ ```
55
70
 
56
- Requirements state observable contracts, never implementation detail ("propagates the child exit code", not "uses spawnSync").
71
+ Fix every reported error and re-validate until it passes. Do not hand off a spec that fails validation.
57
72
 
58
- 5. **Stop.** Report the change as ready-to-finish and hand off:
59
- - Verify: `openspec-apply-change` works through tasks.md, checking each requirement against the code.
60
- - Publish: `mate-artifact-publish` applies the deltas to main specs and anchors the change.
73
+ 8. **Hand off.** Report the written path and show the uncommitted diff (`git diff -- openspec/specs/<capability>/`, or the file itself when it is new). Point the user to `mate-artifact-publish`: the spec appears there under **Drifted specs** and can be published on its own or with the rest.
61
74
 
62
75
  ## Guardrails
63
76
 
64
- - Never write files under `openspec/specs/` — main specs change only through finished changes.
65
- - Never invoke any finish flow (`mate artifact publish`, `openspec archive`); stop at ready-to-finish.
66
- - Never emit a requirement without a citation, and never spec a suspected bug without the user's ruling.
67
- - Keep capability ids opaque kebab-case; extend existing capabilities before creating new ones.
77
+ - Never create an OpenSpec change, change artifacts, or verification tasks, and never archive.
78
+ - Never commit, tag, push, or publish — never run `mate artifact publish`. Publication belongs to `mate-artifact-publish`.
79
+ - Never write any file before the user approves the review table.
80
+ - Never write a requirement without a citation in the review table, and never spec a suspected bug without the user's ruling.
81
+ - Never write citations or evidence tags into the canonical spec.
82
+ - Never hard-code a spec format: take it from the active schema on every run.
83
+ - Keep capability ids opaque kebab-case; extend existing capabilities in place before creating new ones, and never drop or reword requirements outside the named feature.
@@ -137,7 +137,7 @@ If the document fails contract validation, report the diagnostic and correct the
137
137
  - Use Mermaid whenever the user asks for a diagram or the subject has meaningful control flow, sequencing, lifecycle, or relationships. A `text` payload is a deliberate fallback, not the default for runtime flows.
138
138
  - Pick the diagram type that matches the question: `classDiagram` for structure, `sequenceDiagram` for interaction between components, `stateDiagram-v2` for lifecycles, `flowchart TD` for control flow and call trees.
139
139
  - Keep labels short enough to read at report width. A label that needs a sentence belongs in the neighbouring `text` section.
140
- - Do not use ELK-only layouts or math labels. The report inlines the self-contained mermaid bundle, which may not carry those chunks.
140
+ - Do not use ELK-only layouts or math labels. The report loads the single-file mermaid build from a CDN, which does not carry those chunks.
141
141
  - Pair each Mermaid visual with a short neighbouring `text` section that calls out the ownership boundary or invariant the reader should notice.
142
142
  - Prefer the `text` payload for pseudocode, shallow file trees, or cases where a picture would genuinely obscure the point.
143
143