@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
|
@@ -1,11 +1,26 @@
|
|
|
1
|
+
// oxlint-disable no-await-in-loop
|
|
1
2
|
import fs from "node:fs/promises";
|
|
2
3
|
import path from "node:path";
|
|
3
4
|
|
|
5
|
+
import { CURRENT_LAYER_LINK } from "../../runtime/audience-layer";
|
|
4
6
|
import { readProjectionFile } from "../../runtime/projection";
|
|
7
|
+
import { realLinkTarget, realOrResolved } from "../fs-utils";
|
|
8
|
+
import {
|
|
9
|
+
links,
|
|
10
|
+
readManifest,
|
|
11
|
+
updateManifest,
|
|
12
|
+
withLinks,
|
|
13
|
+
type LinkRecord,
|
|
14
|
+
} from "../../runtime/runtime-document-manifest";
|
|
5
15
|
import { pruneEmptyAncestors } from "../../tools/setup/utils";
|
|
6
16
|
|
|
7
17
|
const CLAUDE_SKILLS_PATH = path.join(".claude", "skills");
|
|
8
18
|
|
|
19
|
+
/** The manifest key of a skill link: repo-relative, POSIX separators. */
|
|
20
|
+
function recordKey(name: string): string {
|
|
21
|
+
return `.claude/skills/${name}`;
|
|
22
|
+
}
|
|
23
|
+
|
|
9
24
|
async function skillNames(root: string): Promise<string[]> {
|
|
10
25
|
const entries = await fs.readdir(root, { withFileTypes: true }).catch(() => []);
|
|
11
26
|
const names: string[] = [];
|
|
@@ -25,13 +40,7 @@ async function skillNames(root: string): Promise<string[]> {
|
|
|
25
40
|
|
|
26
41
|
async function linkTarget(link: string): Promise<string | null> {
|
|
27
42
|
const target = await fs.readlink(link).catch(() => null);
|
|
28
|
-
return target === null ? null :
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
async function isManagedSkillLink(link: string, roots: string[]): Promise<boolean> {
|
|
32
|
-
const target = await linkTarget(link);
|
|
33
|
-
if (target === null) return false;
|
|
34
|
-
return roots.some((root) => path.dirname(target) === root);
|
|
43
|
+
return target === null ? null : realLinkTarget(link, target);
|
|
35
44
|
}
|
|
36
45
|
|
|
37
46
|
async function registeredSkillRoots(
|
|
@@ -45,67 +54,143 @@ async function registeredSkillRoots(
|
|
|
45
54
|
companionPath ?? "",
|
|
46
55
|
projection?.projection.companionPath ?? "",
|
|
47
56
|
];
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
57
|
+
const roots = await Promise.all(
|
|
58
|
+
candidates
|
|
59
|
+
.filter(Boolean)
|
|
60
|
+
.map((candidate) => realOrResolved(path.resolve(candidate, CLAUDE_SKILLS_PATH))),
|
|
61
|
+
);
|
|
62
|
+
return [...new Set(roots)];
|
|
53
63
|
}
|
|
54
64
|
|
|
55
65
|
async function createSkillLink(target: string, link: string): Promise<void> {
|
|
56
66
|
await fs.symlink(target, link, process.platform === "win32" ? "junction" : "dir");
|
|
57
67
|
}
|
|
58
68
|
|
|
69
|
+
const CLAUDE_AGENTS_PATH = path.join(".claude", "agents");
|
|
70
|
+
|
|
71
|
+
/** The manifest key of an agent link: repo-relative, POSIX separators. */
|
|
72
|
+
function agentKey(file: string): string {
|
|
73
|
+
return `.claude/agents/${file}`;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async function agentFiles(root: string): Promise<string[]> {
|
|
77
|
+
const entries = await fs.readdir(root, { withFileTypes: true }).catch(() => []);
|
|
78
|
+
return entries
|
|
79
|
+
.filter((entry) => (entry.isFile() || entry.isSymbolicLink()) && entry.name.endsWith(".md"))
|
|
80
|
+
.map((entry) => entry.name);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Every link the wrap wants, by manifest key: the companion's skills, then the
|
|
85
|
+
* wrapped audience's skills and agents. An audience link targets the layer
|
|
86
|
+
* through its `current` link, so a re-rendered layer never strands it. A
|
|
87
|
+
* companion skill wins a name both provide.
|
|
88
|
+
*/
|
|
89
|
+
async function wantedLinks(
|
|
90
|
+
currentRoot: string | null,
|
|
91
|
+
layerRoot: string | null,
|
|
92
|
+
): Promise<Map<string, string>> {
|
|
93
|
+
const wanted = new Map<string, string>();
|
|
94
|
+
if (currentRoot) {
|
|
95
|
+
for (const name of await skillNames(currentRoot)) {
|
|
96
|
+
wanted.set(recordKey(name), path.join(currentRoot, name));
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
if (layerRoot) {
|
|
100
|
+
const layerSkills = path.join(layerRoot, "skills");
|
|
101
|
+
for (const name of await skillNames(layerSkills)) {
|
|
102
|
+
if (!wanted.has(recordKey(name))) wanted.set(recordKey(name), path.join(layerSkills, name));
|
|
103
|
+
}
|
|
104
|
+
const layerAgents = path.join(layerRoot, "agents");
|
|
105
|
+
for (const file of await agentFiles(layerAgents)) {
|
|
106
|
+
wanted.set(agentKey(file), path.join(layerAgents, file));
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return wanted;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Recorded link: Mate's while it resolves to its recorded target. Unrecorded:
|
|
114
|
+
* a skill link is adopted only when it resolves into a registered, current or
|
|
115
|
+
* projected companion; an agent link never is. Records follow the filesystem,
|
|
116
|
+
* so a partial failure re-adopts.
|
|
117
|
+
*/
|
|
59
118
|
export async function reconcileWorkingRepoClaudeSkillLinks(
|
|
60
119
|
repoPath: string,
|
|
61
120
|
companionPath: string | null,
|
|
62
121
|
enabled: boolean,
|
|
63
122
|
registeredCompanionPaths: string[] = [],
|
|
123
|
+
audienceLayerDir: string | null = null,
|
|
64
124
|
): Promise<"written" | "current"> {
|
|
125
|
+
const repoRoot = path.resolve(repoPath);
|
|
65
126
|
const currentRoot = companionPath ? path.resolve(companionPath, CLAUDE_SKILLS_PATH) : null;
|
|
127
|
+
const layerRoot =
|
|
128
|
+
audienceLayerDir && enabled ? path.join(audienceLayerDir, CURRENT_LAYER_LINK, ".claude") : null;
|
|
66
129
|
const roots = await registeredSkillRoots(repoPath, companionPath, registeredCompanionPaths);
|
|
67
|
-
const
|
|
68
|
-
const
|
|
69
|
-
const
|
|
70
|
-
currentRoot && enabled ? new Set(await skillNames(currentRoot)) : new Set<string>();
|
|
130
|
+
const recorded = links(await readManifest(repoPath), "claude");
|
|
131
|
+
const wanted = await wantedLinks(enabled ? currentRoot : null, layerRoot);
|
|
132
|
+
const records: Record<string, LinkRecord> = {};
|
|
71
133
|
let changed = false;
|
|
72
134
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
const
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
target
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
135
|
+
const linkDirs = [
|
|
136
|
+
{ dir: path.join(repoRoot, CLAUDE_SKILLS_PATH), keyOf: recordKey, adoptable: true },
|
|
137
|
+
{ dir: path.join(repoRoot, CLAUDE_AGENTS_PATH), keyOf: agentKey, adoptable: false },
|
|
138
|
+
];
|
|
139
|
+
for (const { dir, keyOf, adoptable } of linkDirs) {
|
|
140
|
+
const existing = await fs.readdir(dir, { withFileTypes: true }).catch(() => []);
|
|
141
|
+
for (const entry of existing) {
|
|
142
|
+
if (!entry.isSymbolicLink()) continue;
|
|
143
|
+
const link = path.join(dir, entry.name);
|
|
144
|
+
const target = await linkTarget(link);
|
|
145
|
+
if (target === null) continue;
|
|
146
|
+
const key = keyOf(entry.name);
|
|
147
|
+
const record = recorded[key];
|
|
148
|
+
const owned = record
|
|
149
|
+
? (await realOrResolved(record.target)) === target
|
|
150
|
+
: adoptable && roots.some((root) => path.dirname(target) === root);
|
|
151
|
+
if (!owned) continue;
|
|
87
152
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
153
|
+
const want = wanted.get(key);
|
|
154
|
+
if (want && (await realOrResolved(want)) === target) {
|
|
155
|
+
records[key] = { runtime: "claude", target: want };
|
|
156
|
+
continue;
|
|
157
|
+
}
|
|
91
158
|
|
|
92
|
-
|
|
93
|
-
await fs.mkdir(targetRoot, { recursive: true });
|
|
94
|
-
for (const name of sourceNames) {
|
|
95
|
-
const link = path.join(targetRoot, name);
|
|
96
|
-
const existingLink = await fs.lstat(link).catch(() => null);
|
|
97
|
-
if (existingLink) continue;
|
|
98
|
-
await createSkillLink(path.join(currentRoot, name), link);
|
|
159
|
+
await fs.unlink(link);
|
|
99
160
|
changed = true;
|
|
100
161
|
}
|
|
101
162
|
}
|
|
102
163
|
|
|
164
|
+
for (const [key, target] of wanted) {
|
|
165
|
+
const link = path.join(repoRoot, ...key.split("/"));
|
|
166
|
+
const existingLink = await fs.lstat(link).catch(() => null);
|
|
167
|
+
if (existingLink) continue;
|
|
168
|
+
await fs.mkdir(path.dirname(link), { recursive: true });
|
|
169
|
+
if (key.startsWith(".claude/agents/")) await fs.symlink(target, link, "file");
|
|
170
|
+
else await createSkillLink(target, link);
|
|
171
|
+
records[key] = { runtime: "claude", target };
|
|
172
|
+
changed = true;
|
|
173
|
+
}
|
|
174
|
+
|
|
103
175
|
if (changed || !enabled) {
|
|
104
|
-
await pruneEmptyAncestors(
|
|
176
|
+
for (const { dir } of linkDirs) await pruneEmptyAncestors(dir, repoRoot);
|
|
177
|
+
}
|
|
178
|
+
if (JSON.stringify(sortedEntries(records)) !== JSON.stringify(sortedEntries(recorded))) {
|
|
179
|
+
await updateManifest(repoPath, (manifest) => withLinks(manifest, "claude", records));
|
|
180
|
+
changed = true;
|
|
105
181
|
}
|
|
106
182
|
return changed ? "written" : "current";
|
|
107
183
|
}
|
|
108
184
|
|
|
185
|
+
function sortedEntries(records: Record<string, LinkRecord>): [string, LinkRecord][] {
|
|
186
|
+
return Object.entries(records)
|
|
187
|
+
.map(([key, record]): [string, LinkRecord] => [
|
|
188
|
+
key,
|
|
189
|
+
{ runtime: record.runtime, target: record.target },
|
|
190
|
+
])
|
|
191
|
+
.toSorted(([a], [b]) => a.localeCompare(b));
|
|
192
|
+
}
|
|
193
|
+
|
|
109
194
|
export async function removeWorkingRepoClaudeSkillLinks(
|
|
110
195
|
repoPath: string,
|
|
111
196
|
registeredCompanionPaths: string[] = [],
|
|
@@ -13,6 +13,11 @@ import {
|
|
|
13
13
|
repoLocalDirPath,
|
|
14
14
|
repoLocalRegistryPath,
|
|
15
15
|
} from "../../runtime/repo-local";
|
|
16
|
+
import {
|
|
17
|
+
documentRuntime,
|
|
18
|
+
documents as recordedDocuments,
|
|
19
|
+
readManifest,
|
|
20
|
+
} from "../../runtime/runtime-document-manifest";
|
|
16
21
|
import { TOKENSAVE_WORKING_REPO_EXCLUDE_ENTRIES } from "../../tools/setup/capabilities/tokensave-shared";
|
|
17
22
|
import {
|
|
18
23
|
ensureWorkingRepoLocalExcludes,
|
|
@@ -26,6 +31,21 @@ import {
|
|
|
26
31
|
reconcileWorkingRepoClaudeSkillLinks,
|
|
27
32
|
removeWorkingRepoClaudeSkillLinks,
|
|
28
33
|
} from "./projection-claude-skills";
|
|
34
|
+
import {
|
|
35
|
+
companionCopiesPresent,
|
|
36
|
+
reconcileCompanionCopies,
|
|
37
|
+
removeCompanionCopies,
|
|
38
|
+
withdrawCompanionCopies,
|
|
39
|
+
} from "./projection-opencode-copies";
|
|
40
|
+
import {
|
|
41
|
+
OPENSPEC_POINTER_EXCLUDE,
|
|
42
|
+
OPENSPEC_POINTER_PATH,
|
|
43
|
+
projectStorePointer,
|
|
44
|
+
reconcileStorePointerExclude,
|
|
45
|
+
removeStorePointer,
|
|
46
|
+
removeStorePointerExclude,
|
|
47
|
+
storePointerPresent,
|
|
48
|
+
} from "./projection-openspec-store";
|
|
29
49
|
import { buildProjection } from "./projection-record";
|
|
30
50
|
import {
|
|
31
51
|
CLAUDE_WORKING_SETTINGS_PATH,
|
|
@@ -41,16 +61,22 @@ import {
|
|
|
41
61
|
CLAUDE_MCP_DOCUMENT,
|
|
42
62
|
CLAUDE_SETTINGS_DOCUMENT,
|
|
43
63
|
OPENCODE_CONFIG_DOCUMENT,
|
|
64
|
+
isExternalDocument,
|
|
44
65
|
placeRuntimeDocument,
|
|
45
66
|
removeRuntimeDocument,
|
|
46
|
-
|
|
67
|
+
trackedDocumentWarnings,
|
|
47
68
|
} from "./projection-runtime-documents";
|
|
48
69
|
import {
|
|
49
70
|
upsertRepoLocalCompanionPointer,
|
|
50
71
|
upsertRepoLocalLinkedRepository,
|
|
51
72
|
writeRepoLocalFrameworkConfig,
|
|
52
73
|
} from "./repo-local-store";
|
|
53
|
-
import type {
|
|
74
|
+
import type {
|
|
75
|
+
ProjectionEntry,
|
|
76
|
+
ProjectionInput,
|
|
77
|
+
ProjectionWriteReport,
|
|
78
|
+
ProjectionWriteState,
|
|
79
|
+
} from "./projection-types";
|
|
54
80
|
|
|
55
81
|
const WORKSPACE_DOCUMENT_FILE = "workspace.code-workspace";
|
|
56
82
|
|
|
@@ -68,6 +94,11 @@ async function pathExists(candidatePath: string): Promise<boolean> {
|
|
|
68
94
|
}
|
|
69
95
|
}
|
|
70
96
|
|
|
97
|
+
/** Present when Mate recorded regions for it, not merely when the file exists. */
|
|
98
|
+
async function runtimeDocumentRecorded(repoPath: string, documentPath: string): Promise<boolean> {
|
|
99
|
+
return recordedDocuments(await readManifest(repoPath)).includes(documentPath);
|
|
100
|
+
}
|
|
101
|
+
|
|
71
102
|
async function writeProjectionPairEntry(input: ProjectionInput): Promise<"written" | "current"> {
|
|
72
103
|
const { companionPath, repository } = input;
|
|
73
104
|
const resolvedCompanionPath = path.resolve(companionPath!);
|
|
@@ -96,6 +127,26 @@ async function writeProjectionPairEntry(input: ProjectionInput): Promise<"writte
|
|
|
96
127
|
return "written";
|
|
97
128
|
}
|
|
98
129
|
|
|
130
|
+
/**
|
|
131
|
+
* Places one rendered document and, when this pass wrote Mate's regions into a
|
|
132
|
+
* file inside the repository, asks Git whether it tracks that file. Only a
|
|
133
|
+
* write is worth the question: an unchanged document was reported when it was
|
|
134
|
+
* written, and a removal puts no companion path into it.
|
|
135
|
+
*/
|
|
136
|
+
async function writeRuntimeDocumentEntry(
|
|
137
|
+
input: ProjectionInput,
|
|
138
|
+
documentPath: string,
|
|
139
|
+
): Promise<ProjectionWriteState | ProjectionWriteReport> {
|
|
140
|
+
const state = await placeRuntimeDocument(input.repoPath, documentPath, input.runtimeDocuments);
|
|
141
|
+
const rendered = input.runtimeDocuments?.some((document) => document.path === documentPath);
|
|
142
|
+
if (state !== "written" || !rendered || isExternalDocument(documentPath)) return state;
|
|
143
|
+
const repositoryId = input.repository?.id ?? path.basename(path.resolve(input.repoPath));
|
|
144
|
+
return {
|
|
145
|
+
state,
|
|
146
|
+
warnings: await trackedDocumentWarnings(input.repoPath, documentPath, repositoryId),
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
|
|
99
150
|
function tokensaveEnabled(input: ProjectionInput): boolean {
|
|
100
151
|
return (input.config?.capabilities ?? []).some((capability) => capability.name === "tokensave");
|
|
101
152
|
}
|
|
@@ -198,16 +249,53 @@ function buildEntries(): readonly ProjectionEntry[] {
|
|
|
198
249
|
id: "projection-pair",
|
|
199
250
|
kind: "owned",
|
|
200
251
|
path: path.join(localDir, PROJECTION_YAML_FILE),
|
|
201
|
-
scopes: ["session"],
|
|
252
|
+
scopes: ["session", "wrap"],
|
|
202
253
|
write: async (input) =>
|
|
203
254
|
input.companionPath && input.repository ? writeProjectionPairEntry(input) : "skipped",
|
|
204
255
|
removal: { by: "entry", entry: "projection-root" },
|
|
205
256
|
},
|
|
257
|
+
/**
|
|
258
|
+
* The OpenSpec store pointer's exclusion, then the pointer: the line is on
|
|
259
|
+
* disk before the file it hides. Its own line outside the managed block,
|
|
260
|
+
* disjoint from `capability-excludes`, so neither strips the other's.
|
|
261
|
+
*/
|
|
262
|
+
{
|
|
263
|
+
id: "openspec-store-exclude",
|
|
264
|
+
kind: "merged",
|
|
265
|
+
path: path.join(".git", "info", "exclude"),
|
|
266
|
+
scopes: ["session", "wrap"],
|
|
267
|
+
write: async (input) =>
|
|
268
|
+
(await reconcileStorePointerExclude(input.repoPath)) ? "written" : "current",
|
|
269
|
+
degradable: true,
|
|
270
|
+
removal: { by: "self", remove: (input) => removeStorePointerExclude(input.repoPath) },
|
|
271
|
+
present: (repoPath) =>
|
|
272
|
+
workingRepoCapabilityExcludesPresent(repoPath, [OPENSPEC_POINTER_EXCLUDE]),
|
|
273
|
+
},
|
|
274
|
+
/**
|
|
275
|
+
* Degradable: without it OpenSpec still plans in the companion, it only
|
|
276
|
+
* names no Working Repository edit root — and the wrapper says so.
|
|
277
|
+
*/
|
|
278
|
+
{
|
|
279
|
+
id: "openspec-store-pointer",
|
|
280
|
+
kind: "owned",
|
|
281
|
+
path: OPENSPEC_POINTER_PATH,
|
|
282
|
+
scopes: ["session", "wrap"],
|
|
283
|
+
write: async (input) =>
|
|
284
|
+
input.companionPath ? projectStorePointer(input.repoPath, input.companionPath) : "skipped",
|
|
285
|
+
degradable: true,
|
|
286
|
+
removal: {
|
|
287
|
+
by: "self",
|
|
288
|
+
remove: (input) => removeStorePointer(input.repoPath),
|
|
289
|
+
retains: "openspec-store-registration",
|
|
290
|
+
},
|
|
291
|
+
present: storePointerPresent,
|
|
292
|
+
},
|
|
206
293
|
{
|
|
207
294
|
id: "claude-skill-links",
|
|
208
295
|
kind: "merged",
|
|
209
296
|
path: path.join(".claude", "skills"),
|
|
210
297
|
scopes: ["wrap"],
|
|
298
|
+
runtime: "claude",
|
|
211
299
|
removeOnUnwrap: true,
|
|
212
300
|
write: async (input) => {
|
|
213
301
|
if (!input.config) return "skipped";
|
|
@@ -219,6 +307,7 @@ function buildEntries(): readonly ProjectionEntry[] {
|
|
|
219
307
|
input.companionPath ?? null,
|
|
220
308
|
input.config.allowedAgents?.includes("claude") ?? false,
|
|
221
309
|
registeredCompanionPaths,
|
|
310
|
+
input.audienceLayerDir ?? null,
|
|
222
311
|
);
|
|
223
312
|
},
|
|
224
313
|
removal: {
|
|
@@ -236,7 +325,7 @@ function buildEntries(): readonly ProjectionEntry[] {
|
|
|
236
325
|
id: "capability-excludes",
|
|
237
326
|
kind: "merged",
|
|
238
327
|
path: path.join(".git", "info", "exclude"),
|
|
239
|
-
scopes: ["launch"],
|
|
328
|
+
scopes: ["launch", "wrap"],
|
|
240
329
|
write: async (input) => {
|
|
241
330
|
if (!input.config) return "skipped";
|
|
242
331
|
return (await reconcileWorkingRepoCapabilityExcludes(
|
|
@@ -259,6 +348,7 @@ function buildEntries(): readonly ProjectionEntry[] {
|
|
|
259
348
|
kind: "merged",
|
|
260
349
|
path: CLAUDE_WORKING_SETTINGS_PATH,
|
|
261
350
|
scopes: ["launch", "wrap"],
|
|
351
|
+
runtime: "claude",
|
|
262
352
|
write: async (input) =>
|
|
263
353
|
input.companionPath
|
|
264
354
|
? syncWorkingRepoClaudeAdditionalDirectories(
|
|
@@ -276,14 +366,14 @@ function buildEntries(): readonly ProjectionEntry[] {
|
|
|
276
366
|
},
|
|
277
367
|
/**
|
|
278
368
|
* Removal-only: Mate never writes this block, an external installer does.
|
|
279
|
-
* Its `write` is the same strip, so a launch heals a repository
|
|
280
|
-
* installer touched without waiting for a cleanup.
|
|
369
|
+
* Its `write` is the same strip, so a launch or a wrap heals a repository
|
|
370
|
+
* the installer touched without waiting for a cleanup.
|
|
281
371
|
*/
|
|
282
372
|
{
|
|
283
373
|
id: "legacy-tokensave-claude-md",
|
|
284
374
|
kind: "merged",
|
|
285
375
|
path: LEGACY_TOKENSAVE_CLAUDE_MD_PATH,
|
|
286
|
-
scopes: ["launch"],
|
|
376
|
+
scopes: ["launch", "wrap"],
|
|
287
377
|
write: async (input) =>
|
|
288
378
|
(await stripTokensaveClaudeMdAppend(input.repoPath)) === "removed" ? "written" : "current",
|
|
289
379
|
removal: { by: "self", remove: (input) => stripTokensaveClaudeMdAppend(input.repoPath) },
|
|
@@ -336,6 +426,7 @@ function buildEntries(): readonly ProjectionEntry[] {
|
|
|
336
426
|
kind: "merged",
|
|
337
427
|
path: CLAUDE_MCP_DOCUMENT,
|
|
338
428
|
scopes: ["launch", "wrap"],
|
|
429
|
+
runtime: "claude",
|
|
339
430
|
write: async (input) =>
|
|
340
431
|
(await removeRuntimeDocument(input.repoPath, CLAUDE_MCP_DOCUMENT)) === "removed"
|
|
341
432
|
? "written"
|
|
@@ -344,7 +435,7 @@ function buildEntries(): readonly ProjectionEntry[] {
|
|
|
344
435
|
by: "self",
|
|
345
436
|
remove: (input) => removeRuntimeDocument(input.repoPath, CLAUDE_MCP_DOCUMENT),
|
|
346
437
|
},
|
|
347
|
-
present: (repoPath) =>
|
|
438
|
+
present: (repoPath) => runtimeDocumentRecorded(repoPath, CLAUDE_MCP_DOCUMENT),
|
|
348
439
|
},
|
|
349
440
|
...(
|
|
350
441
|
[
|
|
@@ -357,15 +448,37 @@ function buildEntries(): readonly ProjectionEntry[] {
|
|
|
357
448
|
kind: "merged" as const,
|
|
358
449
|
path: documentPath.split("/").join(path.sep),
|
|
359
450
|
scopes: ["launch", "wrap"] as const,
|
|
360
|
-
|
|
361
|
-
|
|
451
|
+
runtime: documentRuntime(documentPath),
|
|
452
|
+
write: (input: ProjectionInput) => writeRuntimeDocumentEntry(input, documentPath),
|
|
362
453
|
removal: {
|
|
363
454
|
by: "self" as const,
|
|
364
455
|
remove: (input: { repoPath: string }) =>
|
|
365
456
|
removeRuntimeDocument(input.repoPath, documentPath),
|
|
366
457
|
},
|
|
367
|
-
present: (repoPath: string) =>
|
|
458
|
+
present: (repoPath: string) => runtimeDocumentRecorded(repoPath, documentPath),
|
|
368
459
|
})),
|
|
460
|
+
/**
|
|
461
|
+
* After the OpenCode document, so its copies arrive once the document that
|
|
462
|
+
* loads them is in place. A companion that dropped OpenCode has the copies
|
|
463
|
+
* an earlier wrap made withdrawn; the outcome names each copy written or
|
|
464
|
+
* withdrawn and warns about shadowed and locally edited ones.
|
|
465
|
+
*/
|
|
466
|
+
{
|
|
467
|
+
id: "opencode-companion-copies",
|
|
468
|
+
kind: "merged",
|
|
469
|
+
path: ".opencode",
|
|
470
|
+
scopes: ["wrap"],
|
|
471
|
+
runtime: "opencode",
|
|
472
|
+
removeOnUnwrap: true,
|
|
473
|
+
write: async (input) => {
|
|
474
|
+
if (!input.config || !input.companionPath) return "skipped";
|
|
475
|
+
return (input.config.allowedAgents?.includes("opencode") ?? false)
|
|
476
|
+
? reconcileCompanionCopies(input.repoPath, input.companionPath)
|
|
477
|
+
: withdrawCompanionCopies(input.repoPath);
|
|
478
|
+
},
|
|
479
|
+
removal: { by: "self", remove: (input) => removeCompanionCopies(input.repoPath) },
|
|
480
|
+
present: companionCopiesPresent,
|
|
481
|
+
},
|
|
369
482
|
];
|
|
370
483
|
}
|
|
371
484
|
|