@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.
- package/claude-plugin/hooks/hooks.json +10 -1
- package/claude-plugin/hooks/session-banner.mjs +4 -3
- package/claude-plugin/hooks/session-prepare.mjs +9 -0
- package/opencode-plugin/src/add-dir.ts +10 -76
- package/opencode-plugin/src/audience-layer.ts +91 -0
- package/opencode-plugin/src/companion.ts +70 -18
- package/opencode-plugin/src/launch-environment.ts +29 -0
- package/opencode-plugin/src/server.ts +60 -8
- package/package.json +3 -3
- package/src/cli/commands/artifact/finish/openspec.ts +1 -1
- package/src/cli/commands/artifact/pending/discovery.ts +60 -21
- package/src/cli/commands/cap/index-cmd.ts +2 -11
- package/src/cli/commands/companion/link.ts +16 -11
- package/src/cli/commands/companion/list.ts +4 -5
- package/src/cli/commands/companion/open.ts +4 -5
- package/src/cli/commands/companion/tui.ts +1 -1
- package/src/cli/commands/doctor.ts +87 -5
- package/src/cli/commands/install.ts +8 -0
- package/src/cli/commands/report/renderer.ts +9 -19
- package/src/cli/commands/session/prepare.ts +119 -0
- package/src/cli/commands/shared/companion-selection.ts +2 -2
- package/src/cli/commands/studio/reports.ts +0 -1
- package/src/cli/commands/studio/server.ts +6 -2
- package/src/cli/commands/studio/terminal-launch.ts +16 -7
- package/src/cli/commands/unwrap.ts +28 -3
- package/src/cli/commands/wrap.ts +79 -60
- package/src/cli/companion-selector.tsx +1 -1
- package/src/cli/main.ts +17 -0
- package/src/cli/usage.ts +5 -2
- package/src/hooks/session-banner.ts +19 -10
- package/src/hooks/session-guidance.ts +20 -2
- package/src/hooks/session-prepare.ts +101 -0
- package/src/lib/fs-utils.ts +13 -0
- package/src/lib/orchestrator/adapters/base.ts +45 -9
- package/src/lib/orchestrator/adapters/claude.ts +241 -30
- package/src/lib/orchestrator/adapters/opencode.ts +72 -34
- package/src/lib/orchestrator/companion-git-sync.ts +28 -1
- package/src/lib/orchestrator/companion-hub.ts +2 -1
- package/src/lib/orchestrator/config-store.ts +83 -1
- package/src/lib/orchestrator/framework-context.ts +5 -29
- package/src/lib/orchestrator/launcher.ts +36 -171
- package/src/lib/orchestrator/openspec-store.ts +202 -0
- package/src/lib/orchestrator/projection-claude-entry.ts +17 -6
- package/src/lib/orchestrator/projection-claude-skills.ts +126 -41
- package/src/lib/orchestrator/projection-entries.ts +124 -11
- package/src/lib/orchestrator/projection-opencode-copies.ts +230 -0
- package/src/lib/orchestrator/projection-openspec-store.ts +164 -0
- package/src/lib/orchestrator/projection-record.ts +7 -1
- package/src/lib/orchestrator/projection-runtime-documents.ts +83 -130
- package/src/lib/orchestrator/projection-types.ts +47 -34
- package/src/lib/orchestrator/repository-link.ts +473 -0
- package/src/lib/orchestrator/root-context.ts +6 -2
- package/src/lib/orchestrator/session-prepare.ts +503 -0
- package/src/lib/orchestrator/setup-compatibilities.ts +1 -1
- package/src/lib/orchestrator/setup-preflight.ts +3 -1
- package/src/lib/orchestrator/working-repo-projection.ts +73 -160
- package/src/lib/orchestrator/workspace-inventory.ts +2 -2
- package/src/lib/orchestrator/wrap-pass.ts +222 -0
- package/src/lib/runtime-directory-refresh.ts +186 -0
- package/src/lib/update-checker.ts +6 -0
- package/src/opencode/companion-hooks.ts +30 -135
- package/src/opencode/companion-policy.ts +26 -10
- package/src/opencode/index.ts +1 -7
- package/src/opencode/projected-guidance.ts +6 -13
- package/src/opencode/session-notes.ts +76 -0
- package/src/opencode/session-prepare.ts +139 -0
- package/src/opencode/tui.tsx +56 -9
- package/src/runtime/audience-layer.ts +84 -0
- package/src/runtime/claude-delivery-plan.ts +73 -0
- package/src/runtime/claude-hook-group.ts +21 -0
- package/src/runtime/companion-lock.ts +121 -0
- package/src/runtime/companion-sync.ts +66 -9
- package/src/runtime/guidance.ts +17 -0
- package/src/runtime/opencode-audience-layer.ts +176 -0
- package/src/runtime/policy.ts +14 -1
- package/src/runtime/projection.ts +1 -1
- package/src/runtime/runtime-directory.ts +43 -0
- package/src/runtime/runtime-document-manifest.ts +352 -0
- package/src/runtime/session-prepare-runner.ts +148 -0
- package/src/runtime/wrap-state.ts +94 -0
- package/src/runtime/write-atomic.ts +27 -0
- package/src/templates/capabilities/openspec-cap/mate-minimal/schema.yaml +2 -1
- package/src/templates/capabilities/openspec-cap/mate-v1/schema.yaml +2 -1
- package/src/templates/capabilities/openspec-cap/openspec-conventions.yaml +5 -0
- package/src/templates/capabilities/react-doctor/mate-react-doctor/SKILL.md +84 -0
- package/src/templates/capabilities/react-doctor/{skill → mate-react-doctor}/references/explain.md +18 -15
- package/src/templates/mate-skills/agents/mate-openspec-backfill/SKILL.md +45 -29
- package/src/templates/mate-skills/agents/mate-show-me/SKILL.md +1 -1
- package/src/templates/mate-skills/claude/mate-openspec-backfill/SKILL.md +45 -29
- package/src/templates/mate-skills/claude/mate-show-me/SKILL.md +1 -1
- package/src/tools/setup/__snapshots__/runtime-surface-golden.test.ts.snap +850 -499
- package/src/tools/setup/audience-isolation.fixture.ts +325 -0
- package/src/tools/setup/audience-layer.ts +207 -0
- package/src/tools/setup/audience-registrations.ts +52 -0
- package/src/tools/setup/capabilities/openspec.ts +9 -0
- package/src/tools/setup/capabilities/react-doctor.ts +34 -50
- package/src/tools/setup/capabilities/tokensave.ts +11 -0
- package/src/tools/setup/contribution-ledger.ts +207 -0
- package/src/tools/setup/dynamic-plugins/audiences.ts +89 -9
- package/src/tools/setup/dynamic-plugins/declarations.ts +3 -2
- package/src/tools/setup/dynamic-plugins/hydrate.ts +8 -5
- package/src/tools/setup/engine.ts +152 -10
- package/src/tools/setup/isolation-diagnostics.ts +139 -0
- package/src/tools/setup/mate.ts +1 -1
- package/src/tools/setup/plugin.ts +19 -4
- package/src/tools/setup/plugins/gitignore.ts +7 -1
- package/src/tools/setup/providers/claude-format.ts +11 -18
- package/src/tools/setup/providers/claude-plugin-hooks.ts +61 -24
- package/src/tools/setup/providers/claude.ts +50 -8
- package/src/tools/setup/providers/opencode.ts +10 -0
- package/src/tools/setup/registry.ts +24 -2
- package/src/tools/setup/runtime-documents.ts +152 -32
- package/src/tools/setup/surface-target.ts +13 -4
- package/src/tools/setup/upgrade-sweep.ts +153 -0
- package/src/tools/setup/working-repo-cleanup.ts +2 -0
- package/src/tools/setup/working-repo-local-state.ts +91 -14
- package/src/tools/setup.ts +11 -8
- package/wrappers/bin/openspec +42 -1
- package/src/lib/orchestrator/companion-registry-reader.ts +0 -37
- package/src/lib/orchestrator/companion-resolver.ts +0 -179
- package/src/lib/orchestrator/companion-store.ts +0 -94
- package/src/lib/orchestrator/repo-local-registry.ts +0 -143
- package/src/templates/capabilities/react-doctor/claude/hooks/react-doctor.sh +0 -144
- package/src/templates/capabilities/react-doctor/skill/SKILL.md +0 -51
- package/src/templates/report-assets/README.md +0 -32
- package/src/templates/report-assets/mermaid.LICENSE +0 -21
- 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
|
+
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:
|
|
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).
|
package/src/templates/capabilities/react-doctor/{skill → mate-react-doctor}/references/explain.md
RENAMED
|
@@ -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
|
|
5
|
-
(that is the main `react-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
|
-
|
|
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
|
-
|
|
27
|
+
<bin> --verbose --scope changed --no-score
|
|
25
28
|
```
|
|
26
29
|
|
|
27
30
|
## Commands
|
|
28
31
|
|
|
29
32
|
```bash
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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
|
|
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
|
|
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: "
|
|
9
|
+
version: "2.0"
|
|
10
10
|
---
|
|
11
11
|
|
|
12
12
|
# Mate OpenSpec Backfill
|
|
13
13
|
|
|
14
|
-
|
|
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
|
|
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** (
|
|
34
|
-
- `[inferred]` findings → confirm, demote to out-of-scope, or
|
|
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. **
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
71
|
+
Fix every reported error and re-validate until it passes. Do not hand off a spec that fails validation.
|
|
57
72
|
|
|
58
|
-
|
|
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
|
|
65
|
-
- Never
|
|
66
|
-
- Never
|
|
67
|
-
-
|
|
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
|
|
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
|
|