@uniqbit/mate-core 0.15.4-canary.11 → 0.15.4-canary.12
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/package.json +1 -1
- package/src/hooks/artifact-finish-nudge.ts +35 -3
- package/src/lib/orchestrator/companion-git-sync.ts +67 -8
- package/src/opencode/companion-hooks.ts +33 -12
- package/src/playbooks/companion-guidance.ts +1 -1
- package/src/templates/capabilities/openspec-cap/mate-skills/agents/mate-openspec-backfill/SKILL.md +65 -0
- package/src/tools/setup/__snapshots__/runtime-surface-golden.test.ts.snap +41 -38
- package/src/tools/setup/capabilities/openspec.ts +1 -0
- package/src/tools/setup/engine.ts +2 -1
- package/src/tools/setup/mate.ts +10 -2
- package/src/tools/setup/plugin.ts +22 -2
- package/src/tools/setup/providers/claude.ts +26 -0
- package/src/tools/setup/providers/opencode.ts +27 -0
package/package.json
CHANGED
|
@@ -79,6 +79,36 @@ function extractChangeFromPath(value: string): string | null {
|
|
|
79
79
|
return match ? match[1].replace(/^\d{4}-\d{2}-\d{2}-/, "") : null;
|
|
80
80
|
}
|
|
81
81
|
|
|
82
|
+
// Best-effort shell variable substitution, NOT a shell interpreter: a change
|
|
83
|
+
// name is often built up through variables (DEST="...archive/$TARGET"; mv a
|
|
84
|
+
// "$DEST") rather than appearing as a literal path next to mv/archive. This
|
|
85
|
+
// resolves $NAME/${NAME} references against preceding literal NAME=value
|
|
86
|
+
// assignment tokens in the same command string; unresolved references are
|
|
87
|
+
// left as-is, which keeps prior (silent) behavior for anything it can't follow.
|
|
88
|
+
const ASSIGNMENT_PATTERN = /^([A-Za-z_][A-Za-z0-9_]*)=([\s\S]*)$/;
|
|
89
|
+
const VARIABLE_REFERENCE_PATTERN = /\$\{([A-Za-z_][A-Za-z0-9_]*)\}|\$([A-Za-z_][A-Za-z0-9_]*)/g;
|
|
90
|
+
|
|
91
|
+
function substituteVariables(value: string, vars: Map<string, string>): string {
|
|
92
|
+
return value.replace(VARIABLE_REFERENCE_PATTERN, (match, braced: string, bare: string) => {
|
|
93
|
+
const name = braced ?? bare;
|
|
94
|
+
return vars.has(name) ? vars.get(name)! : match;
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Assignments are collected in left-to-right token order so a later
|
|
99
|
+
// assignment (e.g. DEST) can reference an earlier one (e.g. TARGET) the same
|
|
100
|
+
// way the shell would resolve it at that point in the script.
|
|
101
|
+
function collectVariableAssignments(parts: string[]): Map<string, string> {
|
|
102
|
+
const vars = new Map<string, string>();
|
|
103
|
+
for (const part of parts) {
|
|
104
|
+
const match = part.match(ASSIGNMENT_PATTERN);
|
|
105
|
+
if (!match) continue;
|
|
106
|
+
const [, name, rawValue] = match;
|
|
107
|
+
vars.set(name, substituteVariables(rawValue, vars));
|
|
108
|
+
}
|
|
109
|
+
return vars;
|
|
110
|
+
}
|
|
111
|
+
|
|
82
112
|
// Shell syntax tokens are never change-name positionals: separators end the
|
|
83
113
|
// archive invocation, redirections are skipped (a bare operator also consumes
|
|
84
114
|
// its target token).
|
|
@@ -95,6 +125,7 @@ export function extractArchiveCommand(command: string): string | null {
|
|
|
95
125
|
(parts[position - 1] === "openspec" || parts[position - 1].endsWith("/openspec")),
|
|
96
126
|
);
|
|
97
127
|
if (index < 0) return null;
|
|
128
|
+
const vars = collectVariableAssignments(parts);
|
|
98
129
|
for (let position = index + 1; position < parts.length; position += 1) {
|
|
99
130
|
const part = parts[position];
|
|
100
131
|
if (!part || part === "--") continue;
|
|
@@ -107,7 +138,7 @@ export function extractArchiveCommand(command: string): string | null {
|
|
|
107
138
|
if (part === "--store") position += 1;
|
|
108
139
|
continue;
|
|
109
140
|
}
|
|
110
|
-
return part;
|
|
141
|
+
return substituteVariables(part, vars);
|
|
111
142
|
}
|
|
112
143
|
return null;
|
|
113
144
|
}
|
|
@@ -130,15 +161,16 @@ function isPython(part: string): boolean {
|
|
|
130
161
|
|
|
131
162
|
export function extractMoveCommand(command: string): string | null {
|
|
132
163
|
const parts = shellSplit(command);
|
|
164
|
+
const vars = collectVariableAssignments(parts);
|
|
133
165
|
for (let index = 0; index < parts.length; index += 1) {
|
|
134
166
|
if (MOVE_COMMANDS.has(parts[index].toLowerCase())) {
|
|
135
167
|
for (const part of parts.slice(index + 1)) {
|
|
136
|
-
const change = extractChangeFromPath(part);
|
|
168
|
+
const change = extractChangeFromPath(substituteVariables(part, vars));
|
|
137
169
|
if (change) return change;
|
|
138
170
|
}
|
|
139
171
|
}
|
|
140
172
|
if (isPython(parts[index]) && /(?:shutil\.move|os\.rename|\.rename\s*\()/.test(command)) {
|
|
141
|
-
return extractChangeFromPath(command);
|
|
173
|
+
return extractChangeFromPath(substituteVariables(command, vars));
|
|
142
174
|
}
|
|
143
175
|
}
|
|
144
176
|
return null;
|
|
@@ -9,6 +9,9 @@ import { LaunchPreflightError } from "./types";
|
|
|
9
9
|
|
|
10
10
|
const execFile = promisify(execFileCallback);
|
|
11
11
|
|
|
12
|
+
/** Bounded but far above any diagnostic output; default 1 MiB kills git mid-merge on large trees. */
|
|
13
|
+
const GIT_MAX_BUFFER = 64 * 1024 * 1024;
|
|
14
|
+
|
|
12
15
|
export interface GitCommandResult {
|
|
13
16
|
status: number;
|
|
14
17
|
stdout: string;
|
|
@@ -26,22 +29,30 @@ export interface CompanionGitSyncResult {
|
|
|
26
29
|
export class CompanionGitSyncError extends LaunchPreflightError {
|
|
27
30
|
readonly companionPath: string;
|
|
28
31
|
readonly conflictingPaths: string[];
|
|
32
|
+
readonly reason: string;
|
|
33
|
+
readonly recovery: string;
|
|
34
|
+
readonly stashRef: string | undefined;
|
|
29
35
|
|
|
30
36
|
constructor(
|
|
31
37
|
companionPath: string,
|
|
32
38
|
reason: string,
|
|
33
39
|
conflictingPaths: string[] = [],
|
|
34
40
|
recovery = "Resolve or abort the Git operation, then retry the launch.",
|
|
41
|
+
stashRef?: string,
|
|
35
42
|
) {
|
|
36
43
|
const conflicts = conflictingPaths.length
|
|
37
44
|
? `\n Conflicting paths:\n${conflictingPaths.map((entry) => ` - ${entry}`).join("\n")}`
|
|
38
45
|
: "";
|
|
46
|
+
const stash = stashRef
|
|
47
|
+
? ` Local changes were stashed as ${stashRef}. Recover with \`git stash apply ${stashRef}\` in the companion.`
|
|
48
|
+
: "";
|
|
39
49
|
super(
|
|
40
50
|
[
|
|
41
51
|
"mate: companion Git synchronization failed.",
|
|
42
52
|
` Companion: ${companionPath}`,
|
|
43
53
|
` ${reason}`,
|
|
44
54
|
conflicts,
|
|
55
|
+
stash,
|
|
45
56
|
` ${recovery}`,
|
|
46
57
|
" Bypass: `mate claude -- --no-git` or `mate opencode -- --no-git`.",
|
|
47
58
|
]
|
|
@@ -51,6 +62,9 @@ export class CompanionGitSyncError extends LaunchPreflightError {
|
|
|
51
62
|
this.name = "CompanionGitSyncError";
|
|
52
63
|
this.companionPath = companionPath;
|
|
53
64
|
this.conflictingPaths = conflictingPaths;
|
|
65
|
+
this.reason = reason;
|
|
66
|
+
this.recovery = recovery;
|
|
67
|
+
this.stashRef = stashRef;
|
|
54
68
|
}
|
|
55
69
|
}
|
|
56
70
|
|
|
@@ -68,6 +82,7 @@ export const companionGitSyncDeps: { runGit: GitRunner } = {
|
|
|
68
82
|
cwd,
|
|
69
83
|
encoding: "utf8",
|
|
70
84
|
env: gitEnv,
|
|
85
|
+
maxBuffer: GIT_MAX_BUFFER,
|
|
71
86
|
});
|
|
72
87
|
return { status: 0, stdout: String(result.stdout), stderr: String(result.stderr) };
|
|
73
88
|
} catch (error) {
|
|
@@ -106,6 +121,21 @@ function outputLines(stdout: string): string[] {
|
|
|
106
121
|
.filter(Boolean);
|
|
107
122
|
}
|
|
108
123
|
|
|
124
|
+
/** Matches Git progress-meter lines such as `Updating files: 76% (11340/14790)` or `..., done.` */
|
|
125
|
+
const GIT_PROGRESS_LINE = /^\S.*?: +\d+% \(\d+\/\d+\)(?:, done\.)?$/;
|
|
126
|
+
|
|
127
|
+
function stripGitProgress(text: string): string {
|
|
128
|
+
return text
|
|
129
|
+
.split(/\r\n|\r|\n/)
|
|
130
|
+
.filter((line) => !GIT_PROGRESS_LINE.test(line.trim()))
|
|
131
|
+
.join("\n")
|
|
132
|
+
.trim();
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export function describeGitFailure(result: GitCommandResult): string {
|
|
136
|
+
return stripGitProgress(result.stderr) || stripGitProgress(result.stdout) || "unknown Git error";
|
|
137
|
+
}
|
|
138
|
+
|
|
109
139
|
export class CompanionGitSync {
|
|
110
140
|
constructor(private readonly runGit: GitRunner = companionGitSyncDeps.runGit) {}
|
|
111
141
|
|
|
@@ -160,7 +190,42 @@ export class CompanionGitSync {
|
|
|
160
190
|
stashRef = ref.stdout.trim();
|
|
161
191
|
}
|
|
162
192
|
|
|
163
|
-
const
|
|
193
|
+
const headBefore = await this.command(companionPath, ["rev-parse", "HEAD"]);
|
|
194
|
+
try {
|
|
195
|
+
await this.mergeAndRestore(companionPath, target, stashRef);
|
|
196
|
+
} catch (error) {
|
|
197
|
+
if (stashRef && error instanceof CompanionGitSyncError && !error.stashRef) {
|
|
198
|
+
throw new CompanionGitSyncError(
|
|
199
|
+
error.companionPath,
|
|
200
|
+
error.reason,
|
|
201
|
+
error.conflictingPaths,
|
|
202
|
+
error.recovery,
|
|
203
|
+
stashRef,
|
|
204
|
+
);
|
|
205
|
+
}
|
|
206
|
+
throw error;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const headAfter = await this.command(companionPath, ["rev-parse", "HEAD"]);
|
|
210
|
+
return {
|
|
211
|
+
skipped: false,
|
|
212
|
+
changed: headBefore.stdout.trim() !== headAfter.stdout.trim(),
|
|
213
|
+
companionPath,
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
private async mergeAndRestore(
|
|
218
|
+
companionPath: string,
|
|
219
|
+
target: SyncTarget,
|
|
220
|
+
stashRef: string | undefined,
|
|
221
|
+
): Promise<void> {
|
|
222
|
+
const merge = await this.command(companionPath, [
|
|
223
|
+
"merge",
|
|
224
|
+
target.ref,
|
|
225
|
+
"--no-stat",
|
|
226
|
+
"--no-progress",
|
|
227
|
+
"--no-edit",
|
|
228
|
+
]);
|
|
164
229
|
if (merge.status !== 0) {
|
|
165
230
|
await this.resolveManagedConflicts(companionPath, target.ref);
|
|
166
231
|
const conflicts = await this.unresolvedPaths(companionPath);
|
|
@@ -220,12 +285,6 @@ export class CompanionGitSync {
|
|
|
220
285
|
// Keep the stash if dropping it fails; it remains a recoverable backup.
|
|
221
286
|
await this.command(companionPath, ["stash", "drop", stashRef]);
|
|
222
287
|
}
|
|
223
|
-
|
|
224
|
-
return {
|
|
225
|
-
skipped: false,
|
|
226
|
-
changed: merge.status === 0 && merge.stdout.trim() !== "",
|
|
227
|
-
companionPath,
|
|
228
|
-
};
|
|
229
288
|
}
|
|
230
289
|
|
|
231
290
|
private async assertSafeCompanionRoot(
|
|
@@ -410,7 +469,7 @@ export class CompanionGitSync {
|
|
|
410
469
|
}
|
|
411
470
|
|
|
412
471
|
private detail(result: GitCommandResult): string {
|
|
413
|
-
return (result
|
|
472
|
+
return describeGitFailure(result);
|
|
414
473
|
}
|
|
415
474
|
|
|
416
475
|
private failure(
|
|
@@ -179,15 +179,12 @@ function detectCommandArchive(input: unknown, output: unknown): string | null {
|
|
|
179
179
|
|
|
180
180
|
function detectNewlyArchivedChanges(archiveDir: string, snapshot: Set<string>): string[] {
|
|
181
181
|
const current = readArchiveEntries(archiveDir);
|
|
182
|
-
|
|
183
|
-
snapshot.clear();
|
|
184
|
-
for (const entry of current) snapshot.add(entry);
|
|
185
|
-
return newlyArchived;
|
|
182
|
+
return [...current].filter((entry) => !snapshot.has(entry)).toSorted();
|
|
186
183
|
}
|
|
187
184
|
|
|
188
185
|
function appendOpenSpecFinishNudge(
|
|
189
186
|
context: CompanionContext,
|
|
190
|
-
archiveSnapshot: Set<string
|
|
187
|
+
archiveSnapshot: Set<string> | undefined,
|
|
191
188
|
nudgedCommandChanges: Set<string>,
|
|
192
189
|
input: { tool?: unknown },
|
|
193
190
|
output: { output?: string },
|
|
@@ -196,8 +193,10 @@ function appendOpenSpecFinishNudge(
|
|
|
196
193
|
|
|
197
194
|
const archiveDir = path.join(context.companionPath, "openspec", "changes", "archive");
|
|
198
195
|
const changes: string[] = [];
|
|
199
|
-
|
|
200
|
-
|
|
196
|
+
if (archiveSnapshot) {
|
|
197
|
+
for (const entry of detectNewlyArchivedChanges(archiveDir, archiveSnapshot)) {
|
|
198
|
+
changes.push(entry.slice("YYYY-MM-DD-".length));
|
|
199
|
+
}
|
|
201
200
|
}
|
|
202
201
|
const commandChange =
|
|
203
202
|
String(input.tool ?? "").toLowerCase() === "bash" ? detectCommandArchive(input, output) : null;
|
|
@@ -313,22 +312,36 @@ async function runReactDoctorScan(
|
|
|
313
312
|
}
|
|
314
313
|
|
|
315
314
|
type PluginEventInput = Parameters<NonNullable<Hooks["event"]>>[0];
|
|
315
|
+
type ToolBeforeInput = Parameters<NonNullable<Hooks["tool.execute.before"]>>[0];
|
|
316
|
+
type ToolBeforeOutput = Parameters<NonNullable<Hooks["tool.execute.before"]>>[1];
|
|
316
317
|
type ToolAfterInput = Parameters<NonNullable<Hooks["tool.execute.after"]>>[0];
|
|
317
318
|
type ToolAfterOutput = Parameters<NonNullable<Hooks["tool.execute.after"]>>[1];
|
|
318
319
|
|
|
320
|
+
interface ArchiveCallSnapshot {
|
|
321
|
+
sessionID: string;
|
|
322
|
+
entries: Set<string>;
|
|
323
|
+
}
|
|
324
|
+
|
|
319
325
|
export const CompanionHooksPlugin: Plugin = async (pluginInput = {} as PluginInput) => {
|
|
320
326
|
const { client, $ } = pluginInput;
|
|
321
327
|
const context = readContext(process.env.MATE_ARTIFACT_PATH ?? "");
|
|
322
328
|
if (!context.companionPath || !context.repositoryPath) return {};
|
|
323
329
|
|
|
324
330
|
const archiveDir = path.join(context.companionPath, "openspec", "changes", "archive");
|
|
325
|
-
const
|
|
331
|
+
const archiveCallSnapshots = new Map<string, ArchiveCallSnapshot>();
|
|
326
332
|
const nudgedCommandChanges = new Set<string>();
|
|
327
333
|
const dirtyReactDoctorSessions = new Set<string>();
|
|
328
334
|
const reactDoctorScansInFlight = new Set<string>();
|
|
329
335
|
|
|
330
336
|
return {
|
|
331
337
|
event: async ({ event }: PluginEventInput) => {
|
|
338
|
+
if (event.type === "session.deleted") {
|
|
339
|
+
const sessionID = event.properties.info.id;
|
|
340
|
+
for (const [callID, snapshot] of archiveCallSnapshots) {
|
|
341
|
+
if (snapshot.sessionID === sessionID) archiveCallSnapshots.delete(callID);
|
|
342
|
+
}
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
332
345
|
if (event.type !== "session.idle") return;
|
|
333
346
|
const sessionID = event.properties.sessionID;
|
|
334
347
|
if (
|
|
@@ -340,10 +353,7 @@ export const CompanionHooksPlugin: Plugin = async (pluginInput = {} as PluginInp
|
|
|
340
353
|
}
|
|
341
354
|
await runReactDoctorScan(context, client, $, sessionID, reactDoctorScansInFlight);
|
|
342
355
|
},
|
|
343
|
-
"tool.execute.before": async (
|
|
344
|
-
input: { tool: unknown },
|
|
345
|
-
output: { args: { filePath?: unknown; patchText?: unknown } | undefined },
|
|
346
|
-
) => {
|
|
356
|
+
"tool.execute.before": async (input: ToolBeforeInput, output: ToolBeforeOutput) => {
|
|
347
357
|
const toolName = String(input.tool ?? "");
|
|
348
358
|
const args = output.args ?? {};
|
|
349
359
|
if (["write", "edit"].includes(toolName)) {
|
|
@@ -359,13 +369,24 @@ export const CompanionHooksPlugin: Plugin = async (pluginInput = {} as PluginInp
|
|
|
359
369
|
}
|
|
360
370
|
}
|
|
361
371
|
}
|
|
372
|
+
if (context.gitAutoModeEnabled) {
|
|
373
|
+
archiveCallSnapshots.set(input.callID, {
|
|
374
|
+
sessionID: input.sessionID,
|
|
375
|
+
entries: readArchiveEntries(archiveDir),
|
|
376
|
+
});
|
|
377
|
+
}
|
|
362
378
|
},
|
|
363
379
|
"tool.execute.after": async (input: ToolAfterInput, output: ToolAfterOutput) => {
|
|
364
380
|
if (context.reactDoctorEnabled && REACT_DOCTOR_EDIT_TOOLS.has(input.tool)) {
|
|
365
381
|
dirtyReactDoctorSessions.add(input.sessionID);
|
|
366
382
|
}
|
|
383
|
+
const archiveSnapshot = archiveCallSnapshots.get(input.callID)?.entries;
|
|
384
|
+
archiveCallSnapshots.delete(input.callID);
|
|
367
385
|
appendOpenSpecFinishNudge(context, archiveSnapshot, nudgedCommandChanges, input, output);
|
|
368
386
|
},
|
|
387
|
+
dispose: async () => {
|
|
388
|
+
archiveCallSnapshots.clear();
|
|
389
|
+
},
|
|
369
390
|
};
|
|
370
391
|
};
|
|
371
392
|
|
|
@@ -103,7 +103,7 @@ export function buildCompanionPolicyXml(
|
|
|
103
103
|
|
|
104
104
|
if (hasOpenspecCapability(context.capabilities)) {
|
|
105
105
|
lines.push(
|
|
106
|
-
` <rule id="openspec-finish" severity="critical">Finish OpenSpec changes
|
|
106
|
+
` <rule id="openspec-finish" severity="critical">Finish OpenSpec changes by archiving them: the archive triggers a nudge directing you to run ${FRAMEWORK_NAME} artifact finish "<name>" --json — if no nudge arrives, invoke that command yourself. It is the only sanctioned completion; never hand-commit or hand-tag a finish. Finishing a still-active change archives it and applies its delta specs itself, so do not pre-apply them to openspec/specs right before finishing. Finishing an already-archived change resumes without re-applying delta specs, so an archive flow that already synced specs (e.g. openspec-sync-specs) composes fine with a finish afterwards.</rule>`,
|
|
107
107
|
);
|
|
108
108
|
}
|
|
109
109
|
|
package/src/templates/capabilities/openspec-cap/mate-skills/agents/mate-openspec-backfill/SKILL.md
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
---
|
|
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:*)
|
|
5
|
+
license: MIT
|
|
6
|
+
compatibility: Requires the mate CLI and the openspec capability enabled.
|
|
7
|
+
metadata:
|
|
8
|
+
author: mate
|
|
9
|
+
version: "1.0"
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
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.
|
|
13
|
+
|
|
14
|
+
## Scope rules
|
|
15
|
+
|
|
16
|
+
- **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.
|
|
17
|
+
- **Interactive by design.** Every ambiguity and every suspected bug becomes a user question. Do not run this skill unattended.
|
|
18
|
+
|
|
19
|
+
## Steps
|
|
20
|
+
|
|
21
|
+
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.
|
|
22
|
+
|
|
23
|
+
2. **Sweep.** Extract candidate behaviors and tag each finding:
|
|
24
|
+
- `[test-backed]` — an existing test verifies it (strongest; scenarios translate almost directly from tests)
|
|
25
|
+
- `[code-only]` — observable in code but untested
|
|
26
|
+
- `[inferred]` — assumed intent without direct evidence
|
|
27
|
+
|
|
28
|
+
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.
|
|
29
|
+
|
|
30
|
+
3. **Ask.** Batch the open questions to the user:
|
|
31
|
+
- 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.
|
|
32
|
+
- `[inferred]` findings → confirm, demote to out-of-scope, or convert to a question the emitted proposal records as open.
|
|
33
|
+
|
|
34
|
+
4. **Emit.** Create the change and build its artifacts in dependency order:
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
openspec new change "backfill-spec-<capability>"
|
|
38
|
+
openspec status --change "backfill-spec-<capability>" --json
|
|
39
|
+
openspec instructions <artifact-id> --change "backfill-spec-<capability>" --json
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
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:
|
|
43
|
+
|
|
44
|
+
| Backfill role | Typical artifact (mate-v1 example) |
|
|
45
|
+
| ---------------------------------------------------------------------------------------------- | ---------------------------------- |
|
|
46
|
+
| Scope decisions and rulings from step 3 | explore-brief.md |
|
|
47
|
+
| "Documents existing behavior, no code changes" + open questions | proposal.md |
|
|
48
|
+
| `ADDED`/`MODIFIED` requirements, behavior only, one citation each | specs/ |
|
|
49
|
+
| As-built evidence dossier: entry points, test inventory, `file:line` citations per requirement | design.md |
|
|
50
|
+
| Verification checklist: one task per requirement, "confirm behavior at <citation>" | tasks.md |
|
|
51
|
+
|
|
52
|
+
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."
|
|
53
|
+
|
|
54
|
+
Requirements state observable contracts, never implementation detail ("propagates the child exit code", not "uses spawnSync").
|
|
55
|
+
|
|
56
|
+
5. **Stop.** Report the change as ready-to-finish and hand off:
|
|
57
|
+
- Verify: `openspec-apply-change` works through tasks.md, checking each requirement against the code.
|
|
58
|
+
- Finish: `mate-artifact-finish` applies the deltas to main specs and anchors the change.
|
|
59
|
+
|
|
60
|
+
## Guardrails
|
|
61
|
+
|
|
62
|
+
- Never write files under `openspec/specs/` — main specs change only through finished changes.
|
|
63
|
+
- Never invoke any finish flow (`mate artifact finish`, `openspec archive`); stop at ready-to-finish.
|
|
64
|
+
- Never emit a requirement without a citation, and never spec a suspected bug without the user's ruling.
|
|
65
|
+
- Keep capability ids opaque kebab-case; extend existing capabilities before creating new ones.
|
|
@@ -194,7 +194,7 @@ This block is kept for AGENTS.md compatibility and must not restate that policy.
|
|
|
194
194
|
"reserved": 15000,
|
|
195
195
|
},
|
|
196
196
|
"plugin": [
|
|
197
|
-
"@uniqbit/mate-opencode-plugin@0.15.4-canary.
|
|
197
|
+
"@uniqbit/mate-opencode-plugin@0.15.4-canary.11",
|
|
198
198
|
],
|
|
199
199
|
"references": {
|
|
200
200
|
"mate": "..",
|
|
@@ -230,7 +230,7 @@ Write chunks to $GRAPHIFY_OUT and read $GRAPHIFY_OUT later.
|
|
|
230
230
|
"opencodeTui": {
|
|
231
231
|
"$schema": "https://opencode.ai/tui.json",
|
|
232
232
|
"plugin": [
|
|
233
|
-
"@uniqbit/mate-opencode-plugin@0.15.4-canary.
|
|
233
|
+
"@uniqbit/mate-opencode-plugin@0.15.4-canary.11",
|
|
234
234
|
],
|
|
235
235
|
},
|
|
236
236
|
}
|
|
@@ -407,7 +407,7 @@ Write chunks to $GRAPHIFY_OUT and read $GRAPHIFY_OUT later.
|
|
|
407
407
|
"reserved": 15000,
|
|
408
408
|
},
|
|
409
409
|
"plugin": [
|
|
410
|
-
"@uniqbit/mate-opencode-plugin@0.15.4-canary.
|
|
410
|
+
"@uniqbit/mate-opencode-plugin@0.15.4-canary.11",
|
|
411
411
|
],
|
|
412
412
|
"references": {
|
|
413
413
|
"mate": "..",
|
|
@@ -443,7 +443,7 @@ Write chunks to $GRAPHIFY_OUT and read $GRAPHIFY_OUT later.
|
|
|
443
443
|
"opencodeTui": {
|
|
444
444
|
"$schema": "https://opencode.ai/tui.json",
|
|
445
445
|
"plugin": [
|
|
446
|
-
"@uniqbit/mate-opencode-plugin@0.15.4-canary.
|
|
446
|
+
"@uniqbit/mate-opencode-plugin@0.15.4-canary.11",
|
|
447
447
|
],
|
|
448
448
|
},
|
|
449
449
|
}
|
|
@@ -793,7 +793,7 @@ This block is kept for AGENTS.md compatibility and must not restate that policy.
|
|
|
793
793
|
},
|
|
794
794
|
},
|
|
795
795
|
"plugin": [
|
|
796
|
-
"@uniqbit/mate-opencode-plugin@0.15.4-canary.
|
|
796
|
+
"@uniqbit/mate-opencode-plugin@0.15.4-canary.11",
|
|
797
797
|
],
|
|
798
798
|
"references": {
|
|
799
799
|
"mate": "..",
|
|
@@ -808,7 +808,7 @@ This block is kept for AGENTS.md compatibility and must not restate that policy.
|
|
|
808
808
|
"opencodeTui": {
|
|
809
809
|
"$schema": "https://opencode.ai/tui.json",
|
|
810
810
|
"plugin": [
|
|
811
|
-
"@uniqbit/mate-opencode-plugin@0.15.4-canary.
|
|
811
|
+
"@uniqbit/mate-opencode-plugin@0.15.4-canary.11",
|
|
812
812
|
],
|
|
813
813
|
},
|
|
814
814
|
}
|
|
@@ -1009,7 +1009,7 @@ This block is kept for Claude/AGENTS.md compatibility and must not restate that
|
|
|
1009
1009
|
},
|
|
1010
1010
|
},
|
|
1011
1011
|
"plugin": [
|
|
1012
|
-
"@uniqbit/mate-opencode-plugin@0.15.4-canary.
|
|
1012
|
+
"@uniqbit/mate-opencode-plugin@0.15.4-canary.11",
|
|
1013
1013
|
],
|
|
1014
1014
|
"references": {
|
|
1015
1015
|
"mate": "..",
|
|
@@ -1024,7 +1024,7 @@ This block is kept for Claude/AGENTS.md compatibility and must not restate that
|
|
|
1024
1024
|
"opencodeTui": {
|
|
1025
1025
|
"$schema": "https://opencode.ai/tui.json",
|
|
1026
1026
|
"plugin": [
|
|
1027
|
-
"@uniqbit/mate-opencode-plugin@0.15.4-canary.
|
|
1027
|
+
"@uniqbit/mate-opencode-plugin@0.15.4-canary.11",
|
|
1028
1028
|
],
|
|
1029
1029
|
},
|
|
1030
1030
|
}
|
|
@@ -1312,7 +1312,7 @@ Canonical Mate policy and any more specific tool-routing instructions remain aut
|
|
|
1312
1312
|
"reserved": 15000,
|
|
1313
1313
|
},
|
|
1314
1314
|
"plugin": [
|
|
1315
|
-
"@uniqbit/mate-opencode-plugin@0.15.4-canary.
|
|
1315
|
+
"@uniqbit/mate-opencode-plugin@0.15.4-canary.11",
|
|
1316
1316
|
"context-mode@1.0.169",
|
|
1317
1317
|
],
|
|
1318
1318
|
"references": {
|
|
@@ -1328,7 +1328,7 @@ Canonical Mate policy and any more specific tool-routing instructions remain aut
|
|
|
1328
1328
|
"opencodeTui": {
|
|
1329
1329
|
"$schema": "https://opencode.ai/tui.json",
|
|
1330
1330
|
"plugin": [
|
|
1331
|
-
"@uniqbit/mate-opencode-plugin@0.15.4-canary.
|
|
1331
|
+
"@uniqbit/mate-opencode-plugin@0.15.4-canary.11",
|
|
1332
1332
|
"context-mode@1.0.169",
|
|
1333
1333
|
],
|
|
1334
1334
|
},
|
|
@@ -1472,7 +1472,7 @@ This block is kept for Claude/AGENTS.md compatibility and must not restate that
|
|
|
1472
1472
|
"reserved": 15000,
|
|
1473
1473
|
},
|
|
1474
1474
|
"plugin": [
|
|
1475
|
-
"@uniqbit/mate-opencode-plugin@0.15.4-canary.
|
|
1475
|
+
"@uniqbit/mate-opencode-plugin@0.15.4-canary.11",
|
|
1476
1476
|
"context-mode@1.0.169",
|
|
1477
1477
|
],
|
|
1478
1478
|
"references": {
|
|
@@ -1488,7 +1488,7 @@ This block is kept for Claude/AGENTS.md compatibility and must not restate that
|
|
|
1488
1488
|
"opencodeTui": {
|
|
1489
1489
|
"$schema": "https://opencode.ai/tui.json",
|
|
1490
1490
|
"plugin": [
|
|
1491
|
-
"@uniqbit/mate-opencode-plugin@0.15.4-canary.
|
|
1491
|
+
"@uniqbit/mate-opencode-plugin@0.15.4-canary.11",
|
|
1492
1492
|
"context-mode@1.0.169",
|
|
1493
1493
|
],
|
|
1494
1494
|
},
|
|
@@ -1784,7 +1784,7 @@ This block is kept for AGENTS.md compatibility and must not restate that policy.
|
|
|
1784
1784
|
"reserved": 15000,
|
|
1785
1785
|
},
|
|
1786
1786
|
"plugin": [
|
|
1787
|
-
"@uniqbit/mate-opencode-plugin@0.15.4-canary.
|
|
1787
|
+
"@uniqbit/mate-opencode-plugin@0.15.4-canary.11",
|
|
1788
1788
|
],
|
|
1789
1789
|
"references": {
|
|
1790
1790
|
"mate": "..",
|
|
@@ -1802,7 +1802,7 @@ This block is kept for AGENTS.md compatibility and must not restate that policy.
|
|
|
1802
1802
|
"opencodeTui": {
|
|
1803
1803
|
"$schema": "https://opencode.ai/tui.json",
|
|
1804
1804
|
"plugin": [
|
|
1805
|
-
"@uniqbit/mate-opencode-plugin@0.15.4-canary.
|
|
1805
|
+
"@uniqbit/mate-opencode-plugin@0.15.4-canary.11",
|
|
1806
1806
|
],
|
|
1807
1807
|
},
|
|
1808
1808
|
}
|
|
@@ -1968,7 +1968,7 @@ This block is kept for Claude/AGENTS.md compatibility and must not restate that
|
|
|
1968
1968
|
"reserved": 15000,
|
|
1969
1969
|
},
|
|
1970
1970
|
"plugin": [
|
|
1971
|
-
"@uniqbit/mate-opencode-plugin@0.15.4-canary.
|
|
1971
|
+
"@uniqbit/mate-opencode-plugin@0.15.4-canary.11",
|
|
1972
1972
|
],
|
|
1973
1973
|
"references": {
|
|
1974
1974
|
"mate": "..",
|
|
@@ -1986,7 +1986,7 @@ This block is kept for Claude/AGENTS.md compatibility and must not restate that
|
|
|
1986
1986
|
"opencodeTui": {
|
|
1987
1987
|
"$schema": "https://opencode.ai/tui.json",
|
|
1988
1988
|
"plugin": [
|
|
1989
|
-
"@uniqbit/mate-opencode-plugin@0.15.4-canary.
|
|
1989
|
+
"@uniqbit/mate-opencode-plugin@0.15.4-canary.11",
|
|
1990
1990
|
],
|
|
1991
1991
|
},
|
|
1992
1992
|
}
|
|
@@ -2155,6 +2155,7 @@ This block is kept for Claude/AGENTS.md compatibility and must not restate that
|
|
|
2155
2155
|
"Edit(<root>/companion/**)",
|
|
2156
2156
|
"Read(<root>/companion/**)",
|
|
2157
2157
|
"Skill(mate-artifact-finish)",
|
|
2158
|
+
"Skill(mate-openspec-backfill)",
|
|
2158
2159
|
"Skill(openspec-apply-change)",
|
|
2159
2160
|
"Skill(openspec-archive-change)",
|
|
2160
2161
|
"Skill(openspec-explore)",
|
|
@@ -2206,7 +2207,7 @@ exports[`runtime surface golden fixtures setup openspec × claude 2`] = `
|
|
|
2206
2207
|
<rule id="local-artifact-exception" severity="critical">Only write artifacts in <root>/working when the exact path is gitignored AND intentionally local-only; otherwise use <root>/companion.</rule>
|
|
2207
2208
|
<rule id="guardrail" severity="critical">Bad artifact writes to <root>/working are rejected. Classify correctly first.</rule>
|
|
2208
2209
|
<rule id="wrapper-only-cli-execution" severity="critical">For every CLI declared in cli-tools, invoke the exact path in its invokeAs attribute. Correct: <wrapper-bin>/openspec status ... . Incorrect: openspec status ... . Do not run bare openspec or graphify commands and do not rely on PATH, aliases, or shell functions. If the exact wrapper path is unavailable, stop and report it.</rule>
|
|
2209
|
-
<rule id="openspec-finish" severity="critical">Finish OpenSpec changes
|
|
2210
|
+
<rule id="openspec-finish" severity="critical">Finish OpenSpec changes by archiving them: the archive triggers a nudge directing you to run mate artifact finish "<name>" --json — if no nudge arrives, invoke that command yourself. It is the only sanctioned completion; never hand-commit or hand-tag a finish. Finishing a still-active change archives it and applies its delta specs itself, so do not pre-apply them to openspec/specs right before finishing. Finishing an already-archived change resumes without re-applying delta specs, so an archive flow that already synced specs (e.g. openspec-sync-specs) composes fine with a finish afterwards.</rule>
|
|
2210
2211
|
</mandatory-rules>
|
|
2211
2212
|
</companion-policy>"
|
|
2212
2213
|
,
|
|
@@ -2260,7 +2261,7 @@ This block is kept for AGENTS.md compatibility and must not restate that policy.
|
|
|
2260
2261
|
"reserved": 15000,
|
|
2261
2262
|
},
|
|
2262
2263
|
"plugin": [
|
|
2263
|
-
"@uniqbit/mate-opencode-plugin@0.15.4-canary.
|
|
2264
|
+
"@uniqbit/mate-opencode-plugin@0.15.4-canary.11",
|
|
2264
2265
|
],
|
|
2265
2266
|
"references": {
|
|
2266
2267
|
"mate": "..",
|
|
@@ -2280,7 +2281,7 @@ This block is kept for AGENTS.md compatibility and must not restate that policy.
|
|
|
2280
2281
|
"opencodeTui": {
|
|
2281
2282
|
"$schema": "https://opencode.ai/tui.json",
|
|
2282
2283
|
"plugin": [
|
|
2283
|
-
"@uniqbit/mate-opencode-plugin@0.15.4-canary.
|
|
2284
|
+
"@uniqbit/mate-opencode-plugin@0.15.4-canary.11",
|
|
2284
2285
|
],
|
|
2285
2286
|
},
|
|
2286
2287
|
}
|
|
@@ -2319,7 +2320,7 @@ exports[`runtime surface golden fixtures setup openspec × opencode 2`] = `
|
|
|
2319
2320
|
<rule id="local-artifact-exception" severity="critical">Only write artifacts in $MATE_REPO_PATH when the exact path is gitignored AND intentionally local-only; otherwise use $MATE_ARTIFACT_PATH.</rule>
|
|
2320
2321
|
<rule id="guardrail" severity="critical">Bad artifact writes to $MATE_REPO_PATH are rejected. Classify correctly first.</rule>
|
|
2321
2322
|
<rule id="wrapper-only-cli-execution" severity="critical">For every CLI declared in cli-tools, invoke the exact path in its invokeAs attribute. Correct: $MATE_WRAPPER_BIN_PATH/openspec status ... . Incorrect: openspec status ... . Do not run bare openspec or graphify commands and do not rely on PATH, aliases, or shell functions. If the exact wrapper path is unavailable, stop and report it.</rule>
|
|
2322
|
-
<rule id="openspec-finish" severity="critical">Finish OpenSpec changes
|
|
2323
|
+
<rule id="openspec-finish" severity="critical">Finish OpenSpec changes by archiving them: the archive triggers a nudge directing you to run mate artifact finish "<name>" --json — if no nudge arrives, invoke that command yourself. It is the only sanctioned completion; never hand-commit or hand-tag a finish. Finishing a still-active change archives it and applies its delta specs itself, so do not pre-apply them to openspec/specs right before finishing. Finishing an already-archived change resumes without re-applying delta specs, so an archive flow that already synced specs (e.g. openspec-sync-specs) composes fine with a finish afterwards.</rule>
|
|
2323
2324
|
</mandatory-rules>
|
|
2324
2325
|
</companion-policy>"
|
|
2325
2326
|
,
|
|
@@ -2406,6 +2407,7 @@ This block is kept for Claude/AGENTS.md compatibility and must not restate that
|
|
|
2406
2407
|
"Edit(<root>/companion/**)",
|
|
2407
2408
|
"Read(<root>/companion/**)",
|
|
2408
2409
|
"Skill(mate-artifact-finish)",
|
|
2410
|
+
"Skill(mate-openspec-backfill)",
|
|
2409
2411
|
"Skill(openspec-apply-change)",
|
|
2410
2412
|
"Skill(openspec-archive-change)",
|
|
2411
2413
|
"Skill(openspec-explore)",
|
|
@@ -2427,7 +2429,7 @@ This block is kept for Claude/AGENTS.md compatibility and must not restate that
|
|
|
2427
2429
|
"reserved": 15000,
|
|
2428
2430
|
},
|
|
2429
2431
|
"plugin": [
|
|
2430
|
-
"@uniqbit/mate-opencode-plugin@0.15.4-canary.
|
|
2432
|
+
"@uniqbit/mate-opencode-plugin@0.15.4-canary.11",
|
|
2431
2433
|
],
|
|
2432
2434
|
"references": {
|
|
2433
2435
|
"mate": "..",
|
|
@@ -2447,7 +2449,7 @@ This block is kept for Claude/AGENTS.md compatibility and must not restate that
|
|
|
2447
2449
|
"opencodeTui": {
|
|
2448
2450
|
"$schema": "https://opencode.ai/tui.json",
|
|
2449
2451
|
"plugin": [
|
|
2450
|
-
"@uniqbit/mate-opencode-plugin@0.15.4-canary.
|
|
2452
|
+
"@uniqbit/mate-opencode-plugin@0.15.4-canary.11",
|
|
2451
2453
|
],
|
|
2452
2454
|
},
|
|
2453
2455
|
}
|
|
@@ -2484,7 +2486,7 @@ exports[`runtime surface golden fixtures setup openspec × claude+opencode 2`] =
|
|
|
2484
2486
|
<rule id="local-artifact-exception" severity="critical">Only write artifacts in <root>/working when the exact path is gitignored AND intentionally local-only; otherwise use <root>/companion.</rule>
|
|
2485
2487
|
<rule id="guardrail" severity="critical">Bad artifact writes to <root>/working are rejected. Classify correctly first.</rule>
|
|
2486
2488
|
<rule id="wrapper-only-cli-execution" severity="critical">For every CLI declared in cli-tools, invoke the exact path in its invokeAs attribute. Correct: <wrapper-bin>/openspec status ... . Incorrect: openspec status ... . Do not run bare openspec or graphify commands and do not rely on PATH, aliases, or shell functions. If the exact wrapper path is unavailable, stop and report it.</rule>
|
|
2487
|
-
<rule id="openspec-finish" severity="critical">Finish OpenSpec changes
|
|
2489
|
+
<rule id="openspec-finish" severity="critical">Finish OpenSpec changes by archiving them: the archive triggers a nudge directing you to run mate artifact finish "<name>" --json — if no nudge arrives, invoke that command yourself. It is the only sanctioned completion; never hand-commit or hand-tag a finish. Finishing a still-active change archives it and applies its delta specs itself, so do not pre-apply them to openspec/specs right before finishing. Finishing an already-archived change resumes without re-applying delta specs, so an archive flow that already synced specs (e.g. openspec-sync-specs) composes fine with a finish afterwards.</rule>
|
|
2488
2490
|
</mandatory-rules>
|
|
2489
2491
|
</companion-policy>"
|
|
2490
2492
|
,
|
|
@@ -2531,7 +2533,7 @@ exports[`runtime surface golden fixtures setup openspec × claude+opencode 2`] =
|
|
|
2531
2533
|
<rule id="local-artifact-exception" severity="critical">Only write artifacts in $MATE_REPO_PATH when the exact path is gitignored AND intentionally local-only; otherwise use $MATE_ARTIFACT_PATH.</rule>
|
|
2532
2534
|
<rule id="guardrail" severity="critical">Bad artifact writes to $MATE_REPO_PATH are rejected. Classify correctly first.</rule>
|
|
2533
2535
|
<rule id="wrapper-only-cli-execution" severity="critical">For every CLI declared in cli-tools, invoke the exact path in its invokeAs attribute. Correct: $MATE_WRAPPER_BIN_PATH/openspec status ... . Incorrect: openspec status ... . Do not run bare openspec or graphify commands and do not rely on PATH, aliases, or shell functions. If the exact wrapper path is unavailable, stop and report it.</rule>
|
|
2534
|
-
<rule id="openspec-finish" severity="critical">Finish OpenSpec changes
|
|
2536
|
+
<rule id="openspec-finish" severity="critical">Finish OpenSpec changes by archiving them: the archive triggers a nudge directing you to run mate artifact finish "<name>" --json — if no nudge arrives, invoke that command yourself. It is the only sanctioned completion; never hand-commit or hand-tag a finish. Finishing a still-active change archives it and applies its delta specs itself, so do not pre-apply them to openspec/specs right before finishing. Finishing an already-archived change resumes without re-applying delta specs, so an archive flow that already synced specs (e.g. openspec-sync-specs) composes fine with a finish afterwards.</rule>
|
|
2535
2537
|
</mandatory-rules>
|
|
2536
2538
|
</companion-policy>"
|
|
2537
2539
|
,
|
|
@@ -2714,6 +2716,7 @@ This block is kept for Claude/AGENTS.md compatibility and must not restate that
|
|
|
2714
2716
|
"Skill(context-mode:context-mode)",
|
|
2715
2717
|
"Skill(graphify)",
|
|
2716
2718
|
"Skill(mate-artifact-finish)",
|
|
2719
|
+
"Skill(mate-openspec-backfill)",
|
|
2717
2720
|
"Skill(openspec-apply-change)",
|
|
2718
2721
|
"Skill(openspec-archive-change)",
|
|
2719
2722
|
"Skill(openspec-explore)",
|
|
@@ -2765,7 +2768,7 @@ Write chunks to $GRAPHIFY_OUT and read $GRAPHIFY_OUT later.
|
|
|
2765
2768
|
},
|
|
2766
2769
|
},
|
|
2767
2770
|
"plugin": [
|
|
2768
|
-
"@uniqbit/mate-opencode-plugin@0.15.4-canary.
|
|
2771
|
+
"@uniqbit/mate-opencode-plugin@0.15.4-canary.11",
|
|
2769
2772
|
"context-mode@1.0.169",
|
|
2770
2773
|
],
|
|
2771
2774
|
"references": {
|
|
@@ -2808,7 +2811,7 @@ Write chunks to $GRAPHIFY_OUT and read $GRAPHIFY_OUT later.
|
|
|
2808
2811
|
"opencodeTui": {
|
|
2809
2812
|
"$schema": "https://opencode.ai/tui.json",
|
|
2810
2813
|
"plugin": [
|
|
2811
|
-
"@uniqbit/mate-opencode-plugin@0.15.4-canary.
|
|
2814
|
+
"@uniqbit/mate-opencode-plugin@0.15.4-canary.11",
|
|
2812
2815
|
"context-mode@1.0.169",
|
|
2813
2816
|
],
|
|
2814
2817
|
},
|
|
@@ -2846,7 +2849,7 @@ exports[`runtime surface golden fixtures setup all capabilities × claude+openco
|
|
|
2846
2849
|
<rule id="local-artifact-exception" severity="critical">Only write artifacts in <root>/working when the exact path is gitignored AND intentionally local-only; otherwise use <root>/companion.</rule>
|
|
2847
2850
|
<rule id="guardrail" severity="critical">Bad artifact writes to <root>/working are rejected. Classify correctly first.</rule>
|
|
2848
2851
|
<rule id="wrapper-only-cli-execution" severity="critical">For every CLI declared in cli-tools, invoke the exact path in its invokeAs attribute. Correct: <wrapper-bin>/openspec status ... . Incorrect: openspec status ... . Do not run bare openspec or graphify commands and do not rely on PATH, aliases, or shell functions. If the exact wrapper path is unavailable, stop and report it.</rule>
|
|
2849
|
-
<rule id="openspec-finish" severity="critical">Finish OpenSpec changes
|
|
2852
|
+
<rule id="openspec-finish" severity="critical">Finish OpenSpec changes by archiving them: the archive triggers a nudge directing you to run mate artifact finish "<name>" --json — if no nudge arrives, invoke that command yourself. It is the only sanctioned completion; never hand-commit or hand-tag a finish. Finishing a still-active change archives it and applies its delta specs itself, so do not pre-apply them to openspec/specs right before finishing. Finishing an already-archived change resumes without re-applying delta specs, so an archive flow that already synced specs (e.g. openspec-sync-specs) composes fine with a finish afterwards.</rule>
|
|
2850
2853
|
</mandatory-rules>
|
|
2851
2854
|
</companion-policy>
|
|
2852
2855
|
|
|
@@ -2917,7 +2920,7 @@ exports[`runtime surface golden fixtures setup all capabilities × claude+openco
|
|
|
2917
2920
|
<rule id="local-artifact-exception" severity="critical">Only write artifacts in $MATE_REPO_PATH when the exact path is gitignored AND intentionally local-only; otherwise use $MATE_ARTIFACT_PATH.</rule>
|
|
2918
2921
|
<rule id="guardrail" severity="critical">Bad artifact writes to $MATE_REPO_PATH are rejected. Classify correctly first.</rule>
|
|
2919
2922
|
<rule id="wrapper-only-cli-execution" severity="critical">For every CLI declared in cli-tools, invoke the exact path in its invokeAs attribute. Correct: $MATE_WRAPPER_BIN_PATH/openspec status ... . Incorrect: openspec status ... . Do not run bare openspec or graphify commands and do not rely on PATH, aliases, or shell functions. If the exact wrapper path is unavailable, stop and report it.</rule>
|
|
2920
|
-
<rule id="openspec-finish" severity="critical">Finish OpenSpec changes
|
|
2923
|
+
<rule id="openspec-finish" severity="critical">Finish OpenSpec changes by archiving them: the archive triggers a nudge directing you to run mate artifact finish "<name>" --json — if no nudge arrives, invoke that command yourself. It is the only sanctioned completion; never hand-commit or hand-tag a finish. Finishing a still-active change archives it and applies its delta specs itself, so do not pre-apply them to openspec/specs right before finishing. Finishing an already-archived change resumes without re-applying delta specs, so an archive flow that already synced specs (e.g. openspec-sync-specs) composes fine with a finish afterwards.</rule>
|
|
2921
2924
|
</mandatory-rules>
|
|
2922
2925
|
</companion-policy>"
|
|
2923
2926
|
,
|
|
@@ -3012,7 +3015,7 @@ This block is kept for Claude/AGENTS.md compatibility and must not restate that
|
|
|
3012
3015
|
"reserved": 15000,
|
|
3013
3016
|
},
|
|
3014
3017
|
"plugin": [
|
|
3015
|
-
"@uniqbit/mate-opencode-plugin@0.15.4-canary.
|
|
3018
|
+
"@uniqbit/mate-opencode-plugin@0.15.4-canary.11",
|
|
3016
3019
|
],
|
|
3017
3020
|
"references": {
|
|
3018
3021
|
"mate": "..",
|
|
@@ -3027,7 +3030,7 @@ This block is kept for Claude/AGENTS.md compatibility and must not restate that
|
|
|
3027
3030
|
"opencodeTui": {
|
|
3028
3031
|
"$schema": "https://opencode.ai/tui.json",
|
|
3029
3032
|
"plugin": [
|
|
3030
|
-
"@uniqbit/mate-opencode-plugin@0.15.4-canary.
|
|
3033
|
+
"@uniqbit/mate-opencode-plugin@0.15.4-canary.11",
|
|
3031
3034
|
],
|
|
3032
3035
|
},
|
|
3033
3036
|
}
|
|
@@ -3100,7 +3103,7 @@ This block is kept for Claude/AGENTS.md compatibility and must not restate that
|
|
|
3100
3103
|
"reserved": 15000,
|
|
3101
3104
|
},
|
|
3102
3105
|
"plugin": [
|
|
3103
|
-
"@uniqbit/mate-opencode-plugin@0.15.4-canary.
|
|
3106
|
+
"@uniqbit/mate-opencode-plugin@0.15.4-canary.11",
|
|
3104
3107
|
],
|
|
3105
3108
|
"references": {
|
|
3106
3109
|
"mate": "..",
|
|
@@ -3115,7 +3118,7 @@ This block is kept for Claude/AGENTS.md compatibility and must not restate that
|
|
|
3115
3118
|
"opencodeTui": {
|
|
3116
3119
|
"$schema": "https://opencode.ai/tui.json",
|
|
3117
3120
|
"plugin": [
|
|
3118
|
-
"@uniqbit/mate-opencode-plugin@0.15.4-canary.
|
|
3121
|
+
"@uniqbit/mate-opencode-plugin@0.15.4-canary.11",
|
|
3119
3122
|
],
|
|
3120
3123
|
},
|
|
3121
3124
|
}
|
|
@@ -3189,7 +3192,7 @@ This block is kept for Claude/AGENTS.md compatibility and must not restate that
|
|
|
3189
3192
|
"reserved": 15000,
|
|
3190
3193
|
},
|
|
3191
3194
|
"plugin": [
|
|
3192
|
-
"@uniqbit/mate-opencode-plugin@0.15.4-canary.
|
|
3195
|
+
"@uniqbit/mate-opencode-plugin@0.15.4-canary.11",
|
|
3193
3196
|
],
|
|
3194
3197
|
"references": {
|
|
3195
3198
|
"mate": "..",
|
|
@@ -3204,7 +3207,7 @@ This block is kept for Claude/AGENTS.md compatibility and must not restate that
|
|
|
3204
3207
|
"opencodeTui": {
|
|
3205
3208
|
"$schema": "https://opencode.ai/tui.json",
|
|
3206
3209
|
"plugin": [
|
|
3207
|
-
"@uniqbit/mate-opencode-plugin@0.15.4-canary.
|
|
3210
|
+
"@uniqbit/mate-opencode-plugin@0.15.4-canary.11",
|
|
3208
3211
|
],
|
|
3209
3212
|
},
|
|
3210
3213
|
}
|
|
@@ -3277,7 +3280,7 @@ This block is kept for Claude/AGENTS.md compatibility and must not restate that
|
|
|
3277
3280
|
"reserved": 15000,
|
|
3278
3281
|
},
|
|
3279
3282
|
"plugin": [
|
|
3280
|
-
"@uniqbit/mate-opencode-plugin@0.15.4-canary.
|
|
3283
|
+
"@uniqbit/mate-opencode-plugin@0.15.4-canary.11",
|
|
3281
3284
|
],
|
|
3282
3285
|
"references": {
|
|
3283
3286
|
"mate": "..",
|
|
@@ -3292,7 +3295,7 @@ This block is kept for Claude/AGENTS.md compatibility and must not restate that
|
|
|
3292
3295
|
"opencodeTui": {
|
|
3293
3296
|
"$schema": "https://opencode.ai/tui.json",
|
|
3294
3297
|
"plugin": [
|
|
3295
|
-
"@uniqbit/mate-opencode-plugin@0.15.4-canary.
|
|
3298
|
+
"@uniqbit/mate-opencode-plugin@0.15.4-canary.11",
|
|
3296
3299
|
],
|
|
3297
3300
|
},
|
|
3298
3301
|
}
|
|
@@ -3365,7 +3368,7 @@ This block is kept for Claude/AGENTS.md compatibility and must not restate that
|
|
|
3365
3368
|
"reserved": 15000,
|
|
3366
3369
|
},
|
|
3367
3370
|
"plugin": [
|
|
3368
|
-
"@uniqbit/mate-opencode-plugin@0.15.4-canary.
|
|
3371
|
+
"@uniqbit/mate-opencode-plugin@0.15.4-canary.11",
|
|
3369
3372
|
],
|
|
3370
3373
|
"references": {
|
|
3371
3374
|
"mate": "..",
|
|
@@ -3380,7 +3383,7 @@ This block is kept for Claude/AGENTS.md compatibility and must not restate that
|
|
|
3380
3383
|
"opencodeTui": {
|
|
3381
3384
|
"$schema": "https://opencode.ai/tui.json",
|
|
3382
3385
|
"plugin": [
|
|
3383
|
-
"@uniqbit/mate-opencode-plugin@0.15.4-canary.
|
|
3386
|
+
"@uniqbit/mate-opencode-plugin@0.15.4-canary.11",
|
|
3384
3387
|
],
|
|
3385
3388
|
},
|
|
3386
3389
|
}
|
|
@@ -334,6 +334,7 @@ export function createOpenspecPlugin(deps: OpenSpecPluginDeps = {}): CapabilityP
|
|
|
334
334
|
"Skill(openspec-apply-change)",
|
|
335
335
|
"Skill(openspec-archive-change)",
|
|
336
336
|
"Skill(mate-artifact-finish)",
|
|
337
|
+
"Skill(mate-openspec-backfill)",
|
|
337
338
|
"Bash(openspec:*)",
|
|
338
339
|
`Bash(${FRAMEWORK_NAME} cap graphify:*)`,
|
|
339
340
|
`Bash(${path.join(getWrapperBinPath(), "openspec")}:*)`,
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
// oxlint-disable no-await-in-loop
|
|
1
2
|
import { getActiveDistribution } from "../../distribution";
|
|
2
3
|
import { FRAMEWORK_NAME } from "../../framework";
|
|
3
4
|
import type { FrameworkConfig } from "../../lib/orchestrator/types";
|
|
@@ -200,7 +201,7 @@ async function reconcileCapabilityContributions(
|
|
|
200
201
|
const capability = plugin as CapabilityPlugin;
|
|
201
202
|
if (!capability.getRuntimeContributions) continue;
|
|
202
203
|
const enabled = enabledByPluginId.get(capability.id) ?? false;
|
|
203
|
-
const byRuntime = capability.getRuntimeContributions(ctx);
|
|
204
|
+
const byRuntime = await capability.getRuntimeContributions(ctx);
|
|
204
205
|
for (const [runtimeId, contributions] of Object.entries(byRuntime)) {
|
|
205
206
|
if (!contributions) continue;
|
|
206
207
|
// Inactive runtimes still reconcile — with everything disabled — so a
|
package/src/tools/setup/mate.ts
CHANGED
|
@@ -4,7 +4,12 @@ import path from "node:path";
|
|
|
4
4
|
import { pruneEmptyAncestors } from "./utils";
|
|
5
5
|
|
|
6
6
|
export const MATE_ARTIFACT_SKILLS = ["mate-artifact-finish"] as const;
|
|
7
|
-
export const MATE_SKILLS = [
|
|
7
|
+
export const MATE_SKILLS = [
|
|
8
|
+
"mate-artifact-finish",
|
|
9
|
+
"mate-create-report",
|
|
10
|
+
"mate-openspec-backfill",
|
|
11
|
+
] as const;
|
|
12
|
+
const LEGACY_MATE_SKILLS = ["mate-openspec-artifact-finish"] as const;
|
|
8
13
|
|
|
9
14
|
const MATE_SKILLS_SOURCE = path.join(
|
|
10
15
|
import.meta.dirname,
|
|
@@ -98,6 +103,9 @@ async function resolveMateSkillSource(skill: string, tool: string): Promise<stri
|
|
|
98
103
|
}
|
|
99
104
|
|
|
100
105
|
export async function applyMateSkills(skillsDir: string, tool: string): Promise<void> {
|
|
106
|
+
for (const skill of LEGACY_MATE_SKILLS) {
|
|
107
|
+
await fs.rm(path.join(skillsDir, skill), { recursive: true, force: true });
|
|
108
|
+
}
|
|
101
109
|
for (const skill of MATE_SKILLS) {
|
|
102
110
|
const destination = path.join(skillsDir, skill);
|
|
103
111
|
if (skill === "mate-create-report") {
|
|
@@ -110,7 +118,7 @@ export async function applyMateSkills(skillsDir: string, tool: string): Promise<
|
|
|
110
118
|
}
|
|
111
119
|
|
|
112
120
|
export async function teardownMateSkills(skillsDir: string, companionPath: string): Promise<void> {
|
|
113
|
-
for (const skill of MATE_SKILLS) {
|
|
121
|
+
for (const skill of [...MATE_SKILLS, ...LEGACY_MATE_SKILLS]) {
|
|
114
122
|
try {
|
|
115
123
|
await fs.rm(path.join(skillsDir, skill), { recursive: true, force: true });
|
|
116
124
|
} catch {
|
|
@@ -157,6 +157,22 @@ export interface PluginReferenceContribution {
|
|
|
157
157
|
configFiles?: string[];
|
|
158
158
|
}
|
|
159
159
|
|
|
160
|
+
/**
|
|
161
|
+
* A real, provider-native agent definition file, written to
|
|
162
|
+
* `<runtime dir>/agents/<name>.md` and selectable via `--agent <name>`.
|
|
163
|
+
* Unlike a guidance section, the whole file is managed content (no merge with
|
|
164
|
+
* unmanaged text) — the Capability pre-renders `content` per runtime, since
|
|
165
|
+
* frontmatter shape (Claude's `name`/`hidden`, OpenCode's `mode`) differs.
|
|
166
|
+
* Reconciled in both companion and hub scope: an agent definition is exactly
|
|
167
|
+
* the kind of artifact a repo-less hub needs.
|
|
168
|
+
*/
|
|
169
|
+
export interface AgentDefinitionContribution {
|
|
170
|
+
/** Agent name; the file is written to `<runtime dir>/agents/<name>.md`. */
|
|
171
|
+
name: string;
|
|
172
|
+
/** Full file content, including frontmatter, ready to write as-is. */
|
|
173
|
+
content: string;
|
|
174
|
+
}
|
|
175
|
+
|
|
160
176
|
/**
|
|
161
177
|
* Declarative Agent Runtime contributions of one Capability for one runtime.
|
|
162
178
|
* The runtime's Runtime Surface reconciles these symmetrically: applied while
|
|
@@ -169,6 +185,7 @@ export interface RuntimeContributions {
|
|
|
169
185
|
guidanceSections?: GuidanceSectionContribution[];
|
|
170
186
|
skillTrees?: SkillTreeContribution[];
|
|
171
187
|
pluginReferences?: PluginReferenceContribution[];
|
|
188
|
+
agentDefinitions?: AgentDefinitionContribution[];
|
|
172
189
|
}
|
|
173
190
|
|
|
174
191
|
/**
|
|
@@ -197,9 +214,12 @@ export interface CapabilityPlugin extends Plugin {
|
|
|
197
214
|
* Declare Agent Runtime contributions as data. Called on every setup/sync
|
|
198
215
|
* pass for all registered Capabilities — enabled ones contribute their
|
|
199
216
|
* entries, disabled ones only widen the managed strip set so their previous
|
|
200
|
-
* entries are removed.
|
|
217
|
+
* entries are removed. May be async (e.g. reading a companion-local
|
|
218
|
+
* override file) — the engine always awaits the result.
|
|
201
219
|
*/
|
|
202
|
-
getRuntimeContributions?(
|
|
220
|
+
getRuntimeContributions?(
|
|
221
|
+
ctx: SetupContext,
|
|
222
|
+
): RuntimeContributionsByRuntime | Promise<RuntimeContributionsByRuntime>;
|
|
203
223
|
forProvider?: Record<
|
|
204
224
|
string,
|
|
205
225
|
{
|
|
@@ -348,6 +348,7 @@ export async function reconcileClaudeContributions(
|
|
|
348
348
|
|
|
349
349
|
if (ctx.scope === "hub") {
|
|
350
350
|
await reconcileClaudeMcpContributions(ctx, inputs);
|
|
351
|
+
await reconcileClaudeAgentDefinitionContributions(ctx, inputs);
|
|
351
352
|
return;
|
|
352
353
|
}
|
|
353
354
|
|
|
@@ -359,6 +360,7 @@ export async function reconcileClaudeContributions(
|
|
|
359
360
|
}
|
|
360
361
|
|
|
361
362
|
await reconcileClaudeMcpContributions(ctx, inputs);
|
|
363
|
+
await reconcileClaudeAgentDefinitionContributions(ctx, inputs);
|
|
362
364
|
|
|
363
365
|
for (const input of inputs) {
|
|
364
366
|
// Guidance sections are managed blocks in CLAUDE.md. Current sections are
|
|
@@ -404,6 +406,30 @@ async function reconcileClaudeMcpContributions(
|
|
|
404
406
|
}
|
|
405
407
|
}
|
|
406
408
|
|
|
409
|
+
/**
|
|
410
|
+
* Reconcile declared agent definition files under `.claude/agents/`. Runs in
|
|
411
|
+
* both companion and hub scope — an agent definition is fully self-contained
|
|
412
|
+
* (no shared-file merge), so it needs no companion-only surface.
|
|
413
|
+
*/
|
|
414
|
+
async function reconcileClaudeAgentDefinitionContributions(
|
|
415
|
+
ctx: SetupContext,
|
|
416
|
+
inputs: CapabilityContributionInput[],
|
|
417
|
+
): Promise<void> {
|
|
418
|
+
const agentsDir = path.join(ctx.companionPath, ".claude", "agents");
|
|
419
|
+
for (const input of inputs) {
|
|
420
|
+
for (const agent of input.contributions.agentDefinitions ?? []) {
|
|
421
|
+
const agentPath = path.join(agentsDir, `${agent.name}.md`);
|
|
422
|
+
if (input.enabled) {
|
|
423
|
+
await fs.mkdir(agentsDir, { recursive: true });
|
|
424
|
+
await fs.writeFile(agentPath, agent.content, "utf8");
|
|
425
|
+
} else {
|
|
426
|
+
await fs.rm(agentPath, { force: true });
|
|
427
|
+
await pruneEmptyAncestors(agentsDir, ctx.companionPath);
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
|
|
407
433
|
// Maintain the companion `.mcp.json` shell. Managed MCP servers are reconciled
|
|
408
434
|
// from declared Capability contributions (and legacy `ctx.mcp` hosting); this
|
|
409
435
|
// only guarantees the file exists with an `mcpServers` map. Loaded at launch
|
|
@@ -428,10 +428,12 @@ export async function reconcileOpenCodeContributions(
|
|
|
428
428
|
|
|
429
429
|
if (ctx.scope === "hub") {
|
|
430
430
|
await reconcileOpenCodeMcpContributions(ctx, inputs);
|
|
431
|
+
await reconcileOpenCodeAgentDefinitionContributions(ctx, inputs);
|
|
431
432
|
return;
|
|
432
433
|
}
|
|
433
434
|
|
|
434
435
|
await reconcileOpenCodeMcpContributions(ctx, inputs);
|
|
436
|
+
await reconcileOpenCodeAgentDefinitionContributions(ctx, inputs);
|
|
435
437
|
|
|
436
438
|
for (const input of inputs) {
|
|
437
439
|
for (const pluginReference of input.contributions.pluginReferences ?? []) {
|
|
@@ -494,6 +496,31 @@ async function reconcileOpenCodeMcpContributions(
|
|
|
494
496
|
}
|
|
495
497
|
}
|
|
496
498
|
|
|
499
|
+
/**
|
|
500
|
+
* Reconcile declared agent definition files under `.opencode/agents/`. Runs
|
|
501
|
+
* in both companion and hub scope — an agent definition is fully
|
|
502
|
+
* self-contained (no shared-file merge), so it needs no companion-only
|
|
503
|
+
* surface.
|
|
504
|
+
*/
|
|
505
|
+
async function reconcileOpenCodeAgentDefinitionContributions(
|
|
506
|
+
ctx: SetupContext,
|
|
507
|
+
inputs: CapabilityContributionInput[],
|
|
508
|
+
): Promise<void> {
|
|
509
|
+
const agentsDir = path.join(ctx.companionPath, ".opencode", "agents");
|
|
510
|
+
for (const input of inputs) {
|
|
511
|
+
for (const agent of input.contributions.agentDefinitions ?? []) {
|
|
512
|
+
const agentPath = path.join(agentsDir, `${agent.name}.md`);
|
|
513
|
+
if (input.enabled) {
|
|
514
|
+
await fs.mkdir(agentsDir, { recursive: true });
|
|
515
|
+
await fs.writeFile(agentPath, agent.content, "utf8");
|
|
516
|
+
} else {
|
|
517
|
+
await fs.rm(agentPath, { force: true });
|
|
518
|
+
await pruneEmptyAncestors(agentsDir, ctx.companionPath);
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
|
|
497
524
|
export function createOpenCodePlugin(): ProviderPlugin {
|
|
498
525
|
return {
|
|
499
526
|
id: "opencode",
|